Skip to content
Next Next commit
use stackql-exec
  • Loading branch information
jeffreyaven committed Apr 28, 2024
commit 1d93bccb4ea0fcf6a51b2e109eed5d87a6f88cca
121 changes: 41 additions & 80 deletions action.yml
Original file line number Diff line number Diff line change
@@ -1,120 +1,81 @@
name: 'StackQL Studios - StackQL Assert'
description: 'run StackQL query to test and audit your infrastructure.'
description: 'Run StackQL query to test and audit your infrastructure.'
author: 'Yuncheng Yang, StackQL Studios'
inputs:
test_query:
description: stackql query to execute (need to supply either test_query or test_query_file_path)
description: 'StackQL query to execute (supply either test_query or test_query_file_path)'
required: false
test_query_file_path:
description: stackql query file to execute (need to supply either test_query or test_query_file_path)
description: 'StackQL query file to execute (supply either test_query or test_query_file_path)'
required: false
data_file_path:
description: path to data file to pass to the stackql query preprocessor (json or jsonnet file)
description: 'Path to data file to pass to the StackQL query preprocessor (JSON or Jsonnet file)'
required: false
vars:
description: comma delimited list of variables to pass to the stackql query preprocessor (supported with jsonnet config blocks or jsonnet data files only), accepts 'var1=val1,var2=val2', can be used to source environment variables into stackql queries
description: 'Comma delimited list of variables to pass to the StackQL query preprocessor (supported with Jsonnet config blocks or Jsonnet data files only)'
required: false
expected_rows:
description: expected number of rows from executing test query
expected_rows:
description: 'Expected number of rows from executing test query'
required: false
expected_results_str:
description: expected result (as a json string) from executing test query, overrides expected_results_file_path
description: 'Expected result (as a JSON string) from executing test query, overrides expected_results_file_path'
required: false
expected_results_file_path:
description: json file with the expected result
description: 'JSON file with the expected result'
required: false
auth_obj_path:
description: the path of json file that stores stackql AUTH string (only required when using non-standard environment variable names)
description: 'Path of JSON file that stores StackQL AUTH string (only required when using non-standard environment variable names)'
required: false
auth_str:
description: stackql AUTH string (only required when using non-standard environment variable names)
description: 'StackQL AUTH string (only required when using non-standard environment variable names)'
required: false

runs:
using: "composite"
steps:
- name: check if stackql is installed and set output
id: check-stackql
shell: bash
run: |
if command -v stackql &> /dev/null; then
echo "stackql_installed=true" >> $GITHUB_OUTPUT
else
echo "stackql_installed=false" >> $GITHUB_OUTPUT
fi

- name: setup stackql
uses: stackql/[email protected]
if: ${{steps.check-stackql.outputs.stackql_installed == 'false'}}
- name: Setup StackQL
uses: stackql/[email protected]
with:
use_wrapper: true

- name: setup auth
if: (inputs.auth_obj_path != '') || (inputs.auth_str != '')
id: setup-auth
uses: actions/github-script@v6
- name: Execute StackQL Command (Dry Run)
id: exec-query-dry-run
uses: stackql/[email protected]
with:
script: |
const path = require('path');
const utilsPath = path.join(process.env.GITHUB_ACTION_PATH, 'lib', 'utils.js')
const {setupAuth} = require(utilsPath)
setupAuth(core)
env:
AUTH_FILE_PATH: ${{ inputs.auth_obj_path }}
AUTH_STR: ${{inputs.auth_str}}

- name: get stackql command
uses: actions/github-script@v6
with:
script: |
const path = require('path');
const utilsPath = path.join(process.env.GITHUB_ACTION_PATH, 'lib', 'utils.js')
const {getStackqlCommand} = require(utilsPath)
getStackqlCommand(core)
env:
QUERY_FILE_PATH: ${{ inputs.test_query_file_path }}
QUERY: ${{inputs.test_query}}
DATA_FILE_PATH: ${{inputs.data_file_path}}
VARS: ${{inputs.vars}}
OUTPUT: 'json'
query: ${{ inputs.test_query }}
query_file_path: ${{ inputs.test_query_file_path }}
data_file_path: ${{ inputs.data_file_path }}
vars: ${{ inputs.vars }}
auth_obj_path: ${{ inputs.auth_obj_path }}
auth_str: ${{ inputs.auth_str }}
dry_run: true

- name: dryrun stackql command
id: dryrun-query
shell: bash
run: |
${{ env.STACKQL_DRYRUN_COMMAND }}

- name: show rendered stackql query
uses: actions/github-script@v6
- name: Execute StackQL Command
id: exec-query
uses: stackql/[email protected]
with:
script: |
const path = require('path');
const utilsPath = path.join(process.env.GITHUB_ACTION_PATH, 'lib', 'utils.js')
const {showStackQLQuery} = require(utilsPath)
showStackQLQuery(core)
env:
DRYRUN_RESULT: ${{steps.dryrun-query.outputs.stdout}}
query: ${{ inputs.test_query }}
query_file_path: ${{ inputs.test_query_file_path }}
data_file_path: ${{ inputs.data_file_path }}
vars: ${{ inputs.vars }}
auth_obj_path: ${{ inputs.auth_obj_path }}
auth_str: ${{ inputs.auth_str }}
dry_run: false

- name: execute stackql command
id: exec-query
shell: bash
run: |
${{ env.STACKQL_COMMAND }}

- name: Check results
uses: actions/github-script@v6
- name: Check Results
uses: actions/[email protected]
with:
script: |
const path = require('path');
const assertPath = path.join(process.env.GITHUB_ACTION_PATH, 'stackql-assert.js')
const assertPath = path.join(process.env.GITHUB_ACTION_PATH, 'lib', 'assert.js')
const {assertResult} = require(assertPath)
assertResult(core)
env:
RESULT: ${{steps.exec-query.outputs.stdout}}
RESULT: ${{ steps.exec-query.outputs.stackql-query-results }}
EXPECTED_RESULTS_STR: ${{ inputs.expected_results_str }}
EXPECTED_RESULTS_FILE_PATH: ${{inputs.expected_results_file_path}}
EXPECTED_ROWS: ${{inputs.expected_rows}}
EXPECTED_RESULTS_FILE_PATH: ${{ inputs.expected_results_file_path }}
EXPECTED_ROWS: ${{ inputs.expected_rows }}

branding:
icon: 'terminal'
color: 'green'
color: 'green'
148 changes: 35 additions & 113 deletions lib/assert.js
Original file line number Diff line number Diff line change
@@ -1,136 +1,58 @@
let core;
const fs = require("fs");

const parseResult = (resultStr, varName) => {
const regex = /\[(.*)\]/; // match the first occurrence of square brackets and capture everything in between
const matches = resultStr.match(regex);
if (matches) {
const jsonStr = matches[0]; // extract the captured string
try {
const jsonObj = JSON.parse(jsonStr); // parse the JSON string into an object
return jsonObj;
} catch (error) {
throw(`❌ Failed to parse ${varName} JSON
\nvalue: ${resultStr}
\nerror: ${error}`);
}
const parseResult = (resultStr, description = "result") => {
try {
return JSON.parse(resultStr);
} catch (error) {
throw new Error(`❌ Failed to parse ${description} JSON:
Value: ${resultStr}
Error: ${error.message}`);
}
return null; // return null if no JSON object was found in the string
};

const getExpectedResult = (expectedResultStr, expectedResultFilePath) => {
let expectedResult;
if (expectedResultStr) {
expectedResult = parseResult(expectedResultStr, "expectedResultStr");
return parseResult(expectedResultStr, "expected results string");
} else if (expectedResultFilePath) {
const fileContent = fs.readFileSync(expectedResultFilePath).toString();
expectedResult = parseResult(fileContent, "expectedResultFilePath");
const fileContent = fs.readFileSync(expectedResultFilePath, 'utf-8');
return parseResult(fileContent, "expected results file");
}

return expectedResult;
throw new Error("No expected result provided.");
};

const checkResult = (expectedResult, expectedRows, actualResult) => {
let equality;
let message;
expectedRows = parseInt(expectedRows);
const successMessage = `✅ StackQL Assert Successful`;
const failureMessage = `❌ StackQL Assert Failed`;
const checkResult = (core, expected, actual, expectedRows) => {
const actualLength = actual.length;

// if only passed expectedRows, check expectedRows
// if only passed expected result, only check expected result
// if both passed, check both
if (expectedRows) {
equality = actualResult.length === expectedRows;
if(equality) {
message = `============ ${successMessage} (row count) ============ \n
Expected Number of Rows: ${expectedRows} \n
Actual Number of Rows: ${actualResult.length} \n
`;
} else {
message = `============ ${failureMessage} (row count) ============ \n
Expected Number of Rows: ${expectedRows} \n
Actual Number of Rows: ${actualResult.length} \n
Execution Result: ${JSON.stringify(actualResult)} \n
`;
}
if (expectedRows && actualLength !== parseInt(expectedRows)) {
core.error(`Expected rows: ${expectedRows}, got: ${actualLength}`);
return false;
}

if (expectedResult) {
equality = JSON.stringify(expectedResult) === JSON.stringify(actualResult);
if(equality) {
message = `============ ${successMessage} (expected results) ============`;
} else {
message = `============ ${failureMessage} (expected results) ============ \n
Expected: ${JSON.stringify(expectedResult)}\n
Actual: ${JSON.stringify(actualResult)}
`;
}
if (expected && JSON.stringify(expected) !== JSON.stringify(actual)) {
core.error(`Expected results do not match actual results.\nExpected: ${JSON.stringify(expected)}\nActual: ${JSON.stringify(actual)}`);
return false;
}

return { equality, message };
return true;
};

function checkParameters(expectedResultStr, expectedResultFilePath, expectedRows) {
const params = [
{ name: "expectedResultStr", value: expectedResultStr },
{ name: "expectedResultFilePath", value: expectedResultFilePath },
{ name: "expectedRows", value: expectedRows },
];

const missingParams = params
.filter(param => !param.value || param.value === "undefined")
.map(param => param.name)

if (missingParams.length === 3) {
const errorMessage = "❌ Cannot find expected result, file path or expected rows";
throw errorMessage;
}
}
function assertResult(core) {
try {
const { RESULT, EXPECTED_RESULTS_STR, EXPECTED_RESULTS_FILE_PATH, EXPECTED_ROWS } = process.env;
if (!RESULT) throw new Error("Result from StackQL execution is missing.");

const actualResult = parseResult(RESULT);
const expectedResult = getExpectedResult(EXPECTED_RESULTS_STR, EXPECTED_RESULTS_FILE_PATH);

const assertResult = (coreObj) =>{
core = coreObj;
try {
let [
execResultStr,
expectedResultStr,
expectedResultFilePath,
expectedRows,
] = [
process.env.RESULT,
process.env.EXPECTED_RESULTS_STR,
process.env.EXPECTED_RESULTS_FILE_PATH,
process.env.EXPECTED_ROWS,
];

checkParameters(expectedResultStr, expectedResultFilePath, expectedRows)

let expectedResult = getExpectedResult(
expectedResultStr,
expectedResultFilePath
);

const actualResult = parseResult(execResultStr);

if (!actualResult) {
core.setFailed(`❌ No Output from executing query`);
}

const {equality, message} = checkResult(expectedResult, expectedRows, actualResult);
if (equality) {
core.info(message);
} else {
core.setFailed(message);
}
} catch (e) {
core.setFailed(e);
const resultSuccessful = checkResult(core, expectedResult, actualResult, EXPECTED_ROWS);
if (resultSuccessful) {
core.info("✅ StackQL Assert Successful");
} else {
core.setFailed("❌ StackQL Assert Failed");
}
} catch (error) {
core.setFailed(`Assertion error: ${error.message}`);
}
}

module.exports = {
assertResult,
parseResult,
checkResult,
getExpectedResult
};
module.exports = { assertResult };