Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

runCommand feature + integration tests #359

Merged
merged 23 commits into from
Dec 14, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
d95ea2c
runCommand test
xlcheng1 Aug 31, 2022
69cee8e
Added code branch to execute commands without user and sudo, fixed su…
xlcheng1 Sep 15, 2022
14d96d2
refactoring
xlcheng1 Sep 19, 2022
edc2046
updated input struct, added unit tests
xlcheng1 Sep 23, 2022
d9d3a1f
Addressed comments from previous revision, added another unit test fo…
xlcheng1 Sep 29, 2022
f597b87
Addressed comments
xlcheng1 Oct 3, 2022
b345595
Added SplitByComma for command
xlcheng1 Oct 18, 2022
5ca719f
updated README for runCommand, fixed SIGFAULT when command is empty
xlcheng1 Nov 7, 2022
596d414
fixed a typo
xlcheng1 Nov 7, 2022
5d0729a
added README part asking user not to provide sudo inside command field
xlcheng1 Nov 8, 2022
b057725
addressed comments, changed type of command from console and job doc …
xlcheng1 Nov 30, 2022
148ad99
moved string manipulation functions to StringUtils, added unit tests
xlcheng1 Nov 30, 2022
29a8b81
Added trimming spaces for command field
xlcheng1 Dec 5, 2022
379e69e
fixed a typo in sample job doc
xlcheng1 Dec 7, 2022
2ccd387
Merge branch 'main' into runCmd
xlcheng1 Dec 7, 2022
e1d4875
fixed wrong command type in README
xlcheng1 Dec 7, 2022
12c6037
added integration tests for runCmd
xlcheng1 Dec 7, 2022
f81171e
fixed typo in job doc for integration test
xlcheng1 Dec 8, 2022
262d12d
Merge branch 'main' into runCmd
xlcheng1 Dec 9, 2022
5b91d0f
testing
xlcheng1 Dec 9, 2022
68c6519
resolved old job doc causing seg fault
xlcheng1 Dec 12, 2022
5f625ae
Merge branch 'main' into runCmd
xlcheng1 Dec 12, 2022
36178e8
removed debug cout
xlcheng1 Dec 12, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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