Skip to content

Commit

Permalink
feat: terragrunt iam role supported (#55)
Browse files Browse the repository at this point in the history
* feat: terragrunt iam role supported

* docs: describle new input
  • Loading branch information
mohdnr authored Jan 21, 2022
1 parent 3f3f3d5 commit 6c749ff
Show file tree
Hide file tree
Showing 6 changed files with 246 additions and 4 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ Use the following to control the action:
| `comment-title` | The title to give the PR comment | Plan changes |
| `directory` | Directory with the `*.tf` files to validate | . |
| `github-token` | GitHub Token used to add comment to PR (required to add comments). | |
| `iam-role` | IAM role to use with the terragrunt plan command (required to assume roles). | |
| `terraform-init` | Custom Terraform init args | |
| `terragrunt` | Use Terragrunt instead of Terraform | false |

Expand Down
4 changes: 4 additions & 0 deletions action.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ inputs:
description: 'GitHub Token used to add comment to PR'
required: false
default: 'false'
iam-role:
description: 'IAM role to use with the terragrunt plan command'
required: false
default: ''
terraform-init:
description: 'Custom Terraform init args'
required: false
Expand Down
165 changes: 163 additions & 2 deletions dist/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -134,12 +134,13 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
});
};
Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.getState = exports.saveState = exports.group = exports.endGroup = exports.startGroup = exports.info = exports.notice = exports.warning = exports.error = exports.debug = exports.isDebug = exports.setFailed = exports.setCommandEcho = exports.setOutput = exports.getBooleanInput = exports.getMultilineInput = exports.getInput = exports.addPath = exports.setSecret = exports.exportVariable = exports.ExitCode = void 0;
exports.getIDToken = exports.getState = exports.saveState = exports.group = exports.endGroup = exports.startGroup = exports.info = exports.notice = exports.warning = exports.error = exports.debug = exports.isDebug = exports.setFailed = exports.setCommandEcho = exports.setOutput = exports.getBooleanInput = exports.getMultilineInput = exports.getInput = exports.addPath = exports.setSecret = exports.exportVariable = exports.ExitCode = void 0;
const command_1 = __nccwpck_require__(7351);
const file_command_1 = __nccwpck_require__(717);
const utils_1 = __nccwpck_require__(5278);
const os = __importStar(__nccwpck_require__(2087));
const path = __importStar(__nccwpck_require__(5622));
const oidc_utils_1 = __nccwpck_require__(8041);
/**
* The code to exit an action
*/
Expand Down Expand Up @@ -408,6 +409,12 @@ function getState(name) {
return process.env[`STATE_${name}`] || '';
}
exports.getState = getState;
function getIDToken(aud) {
return __awaiter(this, void 0, void 0, function* () {
return yield oidc_utils_1.OidcClient.getIDToken(aud);
});
}
exports.getIDToken = getIDToken;
//# sourceMappingURL=core.js.map

/***/ }),
Expand Down Expand Up @@ -461,6 +468,90 @@ exports.issueCommand = issueCommand;

/***/ }),

/***/ 8041:
/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) {

"use strict";

var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.OidcClient = void 0;
const http_client_1 = __nccwpck_require__(9925);
const auth_1 = __nccwpck_require__(3702);
const core_1 = __nccwpck_require__(2186);
class OidcClient {
static createHttpClient(allowRetry = true, maxRetry = 10) {
const requestOptions = {
allowRetries: allowRetry,
maxRetries: maxRetry
};
return new http_client_1.HttpClient('actions/oidc-client', [new auth_1.BearerCredentialHandler(OidcClient.getRequestToken())], requestOptions);
}
static getRequestToken() {
const token = process.env['ACTIONS_ID_TOKEN_REQUEST_TOKEN'];
if (!token) {
throw new Error('Unable to get ACTIONS_ID_TOKEN_REQUEST_TOKEN env variable');
}
return token;
}
static getIDTokenUrl() {
const runtimeUrl = process.env['ACTIONS_ID_TOKEN_REQUEST_URL'];
if (!runtimeUrl) {
throw new Error('Unable to get ACTIONS_ID_TOKEN_REQUEST_URL env variable');
}
return runtimeUrl;
}
static getCall(id_token_url) {
var _a;
return __awaiter(this, void 0, void 0, function* () {
const httpclient = OidcClient.createHttpClient();
const res = yield httpclient
.getJson(id_token_url)
.catch(error => {
throw new Error(`Failed to get ID Token. \n
Error Code : ${error.statusCode}\n
Error Message: ${error.result.message}`);
});
const id_token = (_a = res.result) === null || _a === void 0 ? void 0 : _a.value;
if (!id_token) {
throw new Error('Response json body do not have ID Token field');
}
return id_token;
});
}
static getIDToken(audience) {
return __awaiter(this, void 0, void 0, function* () {
try {
// New ID Token is requested from action service
let id_token_url = OidcClient.getIDTokenUrl();
if (audience) {
const encodedAudience = encodeURIComponent(audience);
id_token_url = `${id_token_url}&audience=${encodedAudience}`;
}
core_1.debug(`ID token url is ${id_token_url}`);
const id_token = yield OidcClient.getCall(id_token_url);
core_1.setSecret(id_token);
return id_token;
}
catch (error) {
throw new Error(`Error message: ${error.message}`);
}
});
}
}
exports.OidcClient = OidcClient;
//# sourceMappingURL=oidc-utils.js.map

/***/ }),

/***/ 5278:
/***/ ((__unused_webpack_module, exports) => {

Expand Down Expand Up @@ -496,6 +587,7 @@ function toCommandProperties(annotationProperties) {
}
return {
title: annotationProperties.title,
file: annotationProperties.file,
line: annotationProperties.startLine,
endLine: annotationProperties.endLine,
col: annotationProperties.startColumn,
Expand Down Expand Up @@ -720,6 +812,72 @@ function getOctokitOptions(token, options) {
exports.getOctokitOptions = getOctokitOptions;
//# sourceMappingURL=utils.js.map

/***/ }),

/***/ 3702:
/***/ ((__unused_webpack_module, exports) => {

"use strict";

Object.defineProperty(exports, "__esModule", ({ value: true }));
class BasicCredentialHandler {
constructor(username, password) {
this.username = username;
this.password = password;
}
prepareRequest(options) {
options.headers['Authorization'] =
'Basic ' +
Buffer.from(this.username + ':' + this.password).toString('base64');
}
// This handler cannot handle 401
canHandleAuthentication(response) {
return false;
}
handleAuthentication(httpClient, requestInfo, objs) {
return null;
}
}
exports.BasicCredentialHandler = BasicCredentialHandler;
class BearerCredentialHandler {
constructor(token) {
this.token = token;
}
// currently implements pre-authorization
// TODO: support preAuth = false where it hooks on 401
prepareRequest(options) {
options.headers['Authorization'] = 'Bearer ' + this.token;
}
// This handler cannot handle 401
canHandleAuthentication(response) {
return false;
}
handleAuthentication(httpClient, requestInfo, objs) {
return null;
}
}
exports.BearerCredentialHandler = BearerCredentialHandler;
class PersonalAccessTokenCredentialHandler {
constructor(token) {
this.token = token;
}
// currently implements pre-authorization
// TODO: support preAuth = false where it hooks on 401
prepareRequest(options) {
options.headers['Authorization'] =
'Basic ' + Buffer.from('PAT:' + this.token).toString('base64');
}
// This handler cannot handle 401
canHandleAuthentication(response) {
return false;
}
handleAuthentication(httpClient, requestInfo, objs) {
return null;
}
}
exports.PersonalAccessTokenCredentialHandler = PersonalAccessTokenCredentialHandler;


/***/ }),

/***/ 9925:
Expand Down Expand Up @@ -21374,6 +21532,7 @@ const action = async () => {
const binary = isTerragrunt ? "terragrunt" : "terraform";
const commentTitle = core.getInput("comment-title");
const directory = core.getInput("directory");
const iamRole = isTerragrunt ? core.getInput("iam-role") : undefined;
const terraformInit = core.getMultilineInput("terraform-init");
const token = core.getInput("github-token");
const octokit = token !== "false" ? github.getOctokit(token) : undefined;
Expand All @@ -21395,7 +21554,9 @@ const action = async () => {
},
{
key: "plan",
exec: `${binary} plan -no-color -input=false -out=plan.tfplan`,
exec: `${binary} plan -no-color -input=false -out=plan.tfplan${
iamRole ? ` --terragrunt-iam-role ${iamRole}` : ""
}`,
},
{
key: "show",
Expand Down
2 changes: 1 addition & 1 deletion dist/index.js.map

Large diffs are not rendered by default.

5 changes: 4 additions & 1 deletion src/action.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ const action = async () => {
const binary = isTerragrunt ? "terragrunt" : "terraform";
const commentTitle = core.getInput("comment-title");
const directory = core.getInput("directory");
const iamRole = isTerragrunt ? core.getInput("iam-role") : undefined;
const terraformInit = core.getMultilineInput("terraform-init");
const token = core.getInput("github-token");
const octokit = token !== "false" ? github.getOctokit(token) : undefined;
Expand All @@ -39,7 +40,9 @@ const action = async () => {
},
{
key: "plan",
exec: `${binary} plan -no-color -input=false -out=plan.tfplan`,
exec: `${binary} plan -no-color -input=false -out=plan.tfplan${
iamRole ? ` --terragrunt-iam-role ${iamRole}` : ""
}`,
},
{
key: "show",
Expand Down
73 changes: 73 additions & 0 deletions test/action.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ describe("action", () => {
"-backend-config='bucket=some-bucket'",
"-backend-config='region=ca-central-1'",
]);
when(core.getInput).calledWith("iam-role").mockReturnValue("baz");

await action();

Expand Down Expand Up @@ -97,6 +98,7 @@ describe("action", () => {
execCommand.mockReturnValue({ isSuccess: true, output: "{}" });
when(core.getInput).calledWith("directory").mockReturnValue("bar");
when(core.getBooleanInput).calledWith("terragrunt").mockReturnValue(true);
when(core.getInput).calledWith("iam-role").mockReturnValue(undefined);

await action();

Expand Down Expand Up @@ -163,6 +165,77 @@ describe("action", () => {
expect(deleteComment.mock.calls.length).toBe(0);
});

test("terragrunt flow with assume iam role", async () => {
execCommand.mockReturnValue({ isSuccess: true, output: "{}" });
when(core.getInput).calledWith("directory").mockReturnValue("bar");
when(core.getBooleanInput).calledWith("terragrunt").mockReturnValue(true);
when(core.getInput).calledWith("iam-role").mockReturnValue("baz");

await action();

expect(execCommand.mock.calls.length).toBe(7);
expect(execCommand.mock.calls).toEqual([
[
{
key: "init",
exec: "terragrunt init -no-color ",
},
"bar",
],
[
{
key: "validate",
exec: "terragrunt validate -no-color",
},
"bar",
],
[
{
key: "fmt",
exec: "terragrunt fmt --check",
},
"bar",
],
[
{
key: "plan",
exec: "terragrunt plan -no-color -input=false -out=plan.tfplan --terragrunt-iam-role baz",
},
"bar",
],
[
{
key: "show",
exec: "terragrunt show -no-color -json plan.tfplan",
depends: "plan",
output: false,
},
"bar",
],
[
{
key: "show-json-out",
exec: "terragrunt show -no-color -json plan.tfplan > plan.json",
depends: "plan",
output: false,
},
"bar",
],
[
{
key: "conftest",
depends: "show-json-out",
exec: "conftest test plan.json --no-color --update git::https://github.com/cds-snc/opa_checks.git//aws_terraform",
output: true,
},
"bar",
],
]);
expect(getPlanChanges.mock.calls.length).toBe(1);
expect(addComment.mock.calls.length).toBe(0);
expect(deleteComment.mock.calls.length).toBe(0);
});

test("delete comment", async () => {
execCommand.mockReturnValue({ isSuccess: true, output: "{}" });
when(core.getBooleanInput)
Expand Down

0 comments on commit 6c749ff

Please sign in to comment.