Skip to content

Commit

Permalink
runCommand feature + integration tests (#359)
Browse files Browse the repository at this point in the history
* runCommand test

* Added code branch to execute commands without user and sudo, fixed sudo verification and jobdocument validation

* refactoring

* updated input struct, added unit tests

* Addressed comments from previous revision, added another unit test for command contains space

* Addressed comments

* Added SplitByComma for command

* updated README for runCommand, fixed SIGFAULT when command is empty

* fixed a typo

* added README part asking user not to provide sudo inside command field

* addressed comments, changed type of command from console and job doc to string, added a sample doc for runCommand

* moved string manipulation functions to StringUtils, added unit tests

* Added trimming spaces for command field

* fixed a typo in sample job doc

* fixed wrong command type in README

* added integration tests for runCmd

* fixed typo in job doc for integration test

* testing

* resolved old job doc causing seg fault

* removed debug cout
  • Loading branch information
xlcheng1 authored and RogerZhongAWS committed Jan 3, 2023
1 parent fc51029 commit 7d3c414
Show file tree
Hide file tree
Showing 12 changed files with 768 additions and 116 deletions.
11 changes: 11 additions & 0 deletions integration-tests/source/jobs/JobsIntegrationTests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ static constexpr char VERIFY_PACKAGES_REMOVED_JOB_DOC[] =
"{ \"version\": \"1.0\", \"steps\": [ { \"action\": { \"name\": \"Verify Packages Removed\", \"type\": "
"\"runHandler\", \"input\": { \"handler\": \"verify-packages-removed.sh\", \"args\": [ \"dos2unix\" ], \"path\": "
"\"default\" }, \"runAsUser\": \"root\" } }]}";
static constexpr char RUN_COMMAND_PRINT_GREETING_JOB_DOC[] =
"{ \"version\": \"1.0\", \"steps\": [ { \"action\": { \"name\": \"Print Greeting\", \"type\": "
"\"runCommand\", \"input\": { \"command\": \"echo,Hello World\" }, \"runAsUser\": \"root\" } }]}";

class TestJobsFixture : public ::testing::Test
{
Expand Down Expand Up @@ -82,3 +85,11 @@ TEST_F(TestJobsFixture, RemovePackages)

ASSERT_EQ(resourceHandler->GetJobExecutionStatusWithRetry(jobId), JobExecutionStatus::SUCCEEDED);
}

TEST_F(TestJobsFixture, PrintGreeting)
{
string jobId = "Print-Greeting-" + resourceHandler->GetTimeStamp();
resourceHandler->CreateJob(jobId, RUN_COMMAND_PRINT_GREETING_JOB_DOC);

ASSERT_EQ(resourceHandler->GetJobExecutionStatusWithRetry(jobId), JobExecutionStatus::SUCCEEDED);
}
16 changes: 16 additions & 0 deletions sample-job-docs/run-shellcommand.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"_comment": "This sample JSON file can be used for creating a new directory newDir at the current filepath.",
"version": "1.0",
"steps": [
{
"action": {
"name": "Create New Directory",
"type": "runCommand",
"input": {
"command": "mkdir,newDir"
},
"runAsUser": "root"
}
}
]
}
123 changes: 93 additions & 30 deletions source/jobs/JobDocument.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,22 @@

#include "JobDocument.h"
#include "../logging/LoggerFactory.h"
#include "../util/StringUtils.h"
#include <aws/crt/JsonObject.h>
#include <regex>
#include <set>

using namespace std;
using namespace Aws::Iot::DeviceClient::Jobs;
using namespace Aws::Iot;
using namespace Aws::Crt;
using namespace Aws::Iot::DeviceClient::Logging;
using namespace Aws::Iot::DeviceClient::Util;

constexpr char LoadableFromJobDocument::TAG[];

constexpr char PlainJobDocument::ACTION_TYPE_RUN_HANDLER[];
constexpr char PlainJobDocument::ACTION_TYPE_RUN_COMMAND[];

constexpr char PlainJobDocument::JSON_KEY_VERSION[];
constexpr char PlainJobDocument::JSON_KEY_INCLUDESTDOUT[];
Expand Down Expand Up @@ -52,16 +57,16 @@ void PlainJobDocument::LoadFromJobDocument(const JsonView &json)
if (json.ValueExists(jsonKey) && json.GetJsonObject(jsonKey).IsString())
{
JobAction jobAction;
JobAction::ActionHandlerInput temp;
// Save Job Action name and handler field value with operation field value
jobAction.name = json.GetString(jsonKey).c_str();
jobAction.input.handler = jobAction.name;
temp.handler = jobAction.name;

jsonKey = JSON_KEY_ARGS;
if (json.ValueExists(jsonKey) && json.GetJsonObject(jsonKey).IsListType())
{
jobAction.input.args = ParseToVectorString(json.GetJsonObject(jsonKey));
temp.args = Util::ParseToVectorString(json.GetJsonObject(jsonKey));
}

// Old Schema only supports runHandler type of action
jobAction.type = ACTION_TYPE_RUN_HANDLER;

Expand All @@ -74,9 +79,11 @@ void PlainJobDocument::LoadFromJobDocument(const JsonView &json)
jsonKey = JSON_KEY_PATH;
if (json.ValueExists(jsonKey) && json.GetJsonObject(jsonKey).IsString())
{
jobAction.input.path = json.GetString(jsonKey).c_str();
temp.path = json.GetString(jsonKey).c_str();
}

jobAction.handlerInput = temp;

steps.push_back(jobAction);
}
}
Expand Down Expand Up @@ -127,18 +134,6 @@ void PlainJobDocument::LoadFromJobDocument(const JsonView &json)
}
}

vector<string> PlainJobDocument::ParseToVectorString(const JsonView &json)
{
vector<string> plainVector;

for (const auto &i : json.AsArray())
{
// cppcheck-suppress useStlAlgorithm
plainVector.push_back(i.AsString().c_str());
}
return plainVector;
}

bool PlainJobDocument::Validate() const
{
if (version.empty())
Expand Down Expand Up @@ -201,7 +196,7 @@ void PlainJobDocument::JobCondition::LoadFromJobDocument(const JsonView &json)
jsonKey = JSON_KEY_CONDITION_VALUE;
if (json.ValueExists(jsonKey) && json.GetJsonObject(jsonKey).IsListType())
{
conditionValue = ParseToVectorString(json.GetJsonObject(jsonKey));
conditionValue = Util::ParseToVectorString(json.GetJsonObject(jsonKey));
}

jsonKey = JSON_KEY_TYPE;
Expand Down Expand Up @@ -233,6 +228,9 @@ constexpr char PlainJobDocument::JobAction::JSON_KEY_INPUT[];
constexpr char PlainJobDocument::JobAction::JSON_KEY_RUNASUSER[];
constexpr char PlainJobDocument::JobAction::JSON_KEY_ALLOWSTDERR[];
constexpr char PlainJobDocument::JobAction::JSON_KEY_IGNORESTEPFAILURE[];
const static std::set<std::string> SUPPORTED_ACTION_TYPES{
Aws::Iot::DeviceClient::Jobs::PlainJobDocument::ACTION_TYPE_RUN_HANDLER,
Aws::Iot::DeviceClient::Jobs::PlainJobDocument::ACTION_TYPE_RUN_COMMAND};

void PlainJobDocument::JobAction::LoadFromJobDocument(const JsonView &json)
{
Expand All @@ -251,9 +249,18 @@ void PlainJobDocument::JobAction::LoadFromJobDocument(const JsonView &json)
jsonKey = JSON_KEY_INPUT;
if (json.ValueExists(jsonKey))
{
ActionInput temp;
temp.LoadFromJobDocument(json.GetJsonObject(jsonKey));
input = temp;
if (type == PlainJobDocument::ACTION_TYPE_RUN_HANDLER)
{
ActionHandlerInput temp;
temp.LoadFromJobDocument(json.GetJsonObject(jsonKey));
handlerInput = temp;
}
else if (type == PlainJobDocument::ACTION_TYPE_RUN_COMMAND)
{
ActionCommandInput temp;
temp.LoadFromJobDocument(json.GetJsonObject(jsonKey));
commandInput = temp;
}
}

jsonKey = JSON_KEY_RUNASUSER;
Expand Down Expand Up @@ -289,29 +296,39 @@ bool PlainJobDocument::JobAction::Validate() const
return false;
}

if (type != ACTION_TYPE_RUN_HANDLER)
if (SUPPORTED_ACTION_TYPES.count(type) == 0)
{
LOGM_ERROR(
TAG,
"*** %s: Required field Action Type with invalid value: %s ***",
DeviceClient::Jobs::DC_INVALID_JOB_DOC,
type.c_str());
Util::Sanitize(type).c_str());
return false;
}

if (!input.Validate())
if (type == PlainJobDocument::ACTION_TYPE_RUN_HANDLER)
{
return false;
if (!handlerInput->Validate())
{
return false;
}
}
else if (type == PlainJobDocument::ACTION_TYPE_RUN_COMMAND)
{
if (!commandInput->Validate())
{
return false;
}
}

return true;
}

constexpr char PlainJobDocument::JobAction::ActionInput::JSON_KEY_HANDLER[];
constexpr char PlainJobDocument::JobAction::ActionInput::JSON_KEY_ARGS[];
constexpr char PlainJobDocument::JobAction::ActionInput::JSON_KEY_PATH[];
constexpr char PlainJobDocument::JobAction::ActionHandlerInput::JSON_KEY_HANDLER[];
constexpr char PlainJobDocument::JobAction::ActionHandlerInput::JSON_KEY_ARGS[];
constexpr char PlainJobDocument::JobAction::ActionHandlerInput::JSON_KEY_PATH[];

void PlainJobDocument::JobAction::ActionInput::LoadFromJobDocument(const JsonView &json)
void PlainJobDocument::JobAction::ActionHandlerInput::LoadFromJobDocument(const JsonView &json)
{
const char *jsonKey = JSON_KEY_HANDLER;
if (json.ValueExists(jsonKey) && json.GetJsonObject(jsonKey).IsString())
Expand All @@ -322,7 +339,7 @@ void PlainJobDocument::JobAction::ActionInput::LoadFromJobDocument(const JsonVie
jsonKey = JSON_KEY_ARGS;
if (json.ValueExists(jsonKey) && json.GetJsonObject(jsonKey).IsListType())
{
args = ParseToVectorString(json.GetJsonObject(jsonKey));
args = Util::ParseToVectorString(json.GetJsonObject(jsonKey));
}

jsonKey = JSON_KEY_PATH;
Expand All @@ -332,7 +349,7 @@ void PlainJobDocument::JobAction::ActionInput::LoadFromJobDocument(const JsonVie
}
}

bool PlainJobDocument::JobAction::ActionInput::Validate() const
bool PlainJobDocument::JobAction::ActionHandlerInput::Validate() const
{
if (handler.empty())
{
Expand All @@ -343,3 +360,49 @@ bool PlainJobDocument::JobAction::ActionInput::Validate() const

return true;
}

constexpr char PlainJobDocument::JobAction::ActionCommandInput::JSON_KEY_COMMAND[];

void PlainJobDocument::JobAction::ActionCommandInput::LoadFromJobDocument(const JsonView &json)
{
const char *jsonKey = JSON_KEY_COMMAND;
if (json.ValueExists(jsonKey))
{
string commandString = json.GetString(jsonKey).c_str();

if (!commandString.empty())
{
vector<string> tokens = Util::SplitStringByComma(commandString);
for (auto token : tokens)
{
Util::replace_all(token, R"(\,)", ",");
// trim all leading and trailing space characters including tabs, newlines etc.
command.emplace_back(Util::TrimCopy(token, " \t\n\v\f\r"));
}
}
}
}

bool PlainJobDocument::JobAction::ActionCommandInput::Validate() const
{
if (command.empty())
{
LOGM_ERROR(
TAG, "*** %s: Required field ActionInput command is missing ***", DeviceClient::Jobs::DC_INVALID_JOB_DOC);
return false;
}

auto isSpace = [](const char &c) { return isspace(static_cast<unsigned char>(c)); };
const auto &firstCommand = command.front();
if (find_if(firstCommand.cbegin(), firstCommand.cend(), isSpace) != firstCommand.cend())
{
LOGM_ERROR(
TAG,
"*** %s: Required field ActionInput command's first word contains space characters: %s ***",
DeviceClient::Jobs::DC_INVALID_JOB_DOC,
Util::Sanitize(firstCommand).c_str());
return false;
}

return true;
}
23 changes: 20 additions & 3 deletions source/jobs/JobDocument.h
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,9 @@ namespace Aws
{
void LoadFromJobDocument(const JsonView &json) override;
bool Validate() const override;
static std::vector<std::string> ParseToVectorString(const JsonView &json);

static constexpr char ACTION_TYPE_RUN_HANDLER[] = "runHandler";
static constexpr char ACTION_TYPE_RUN_COMMAND[] = "runCommand";

static constexpr char JSON_KEY_VERSION[] = "version";
static constexpr char JSON_KEY_INCLUDESTDOUT[] = "includeStdOut";
Expand Down Expand Up @@ -87,7 +87,10 @@ namespace Aws
std::string name;
std::string type;

struct ActionInput : public LoadableFromJobDocument
/**
* ActionHandlerInput - Invokes a handler script specified in a job document.
*/
struct ActionHandlerInput : public LoadableFromJobDocument
{
void LoadFromJobDocument(const JsonView &json) override;
bool Validate() const override;
Expand All @@ -100,7 +103,21 @@ namespace Aws
Optional<std::vector<std::string>> args;
Optional<std::string> path;
};
ActionInput input;
Optional<ActionHandlerInput> handlerInput;

/**
* ActionCommandInput - Invokes arbitrary commands specified in a job document.
*/
struct ActionCommandInput : public LoadableFromJobDocument
{
void LoadFromJobDocument(const JsonView &json) override;
bool Validate() const override;

static constexpr char JSON_KEY_COMMAND[] = "command";

std::vector<std::string> command;
};
Optional<ActionCommandInput> commandInput;
Optional<std::string> runAsUser{""};
Optional<int> allowStdErr;
Optional<bool> ignoreStepFailure{false};
Expand Down
Loading

0 comments on commit 7d3c414

Please sign in to comment.