diff --git a/stdlib/aws/aws.cue b/stdlib/aws/aws.cue new file mode 100644 index 00000000..479acfb5 --- /dev/null +++ b/stdlib/aws/aws.cue @@ -0,0 +1,10 @@ +package aws + +#Config: { + // AWS region + region: string + // AWS access key + accessKey: string // FIXME: should be a secret + // AWS secret key + secretKey: string // FIXME: should be a secret +} diff --git a/stdlib/aws/cloudformation/cloudformation.cue b/stdlib/aws/cloudformation/cloudformation.cue new file mode 100644 index 00000000..aa6c2f79 --- /dev/null +++ b/stdlib/aws/cloudformation/cloudformation.cue @@ -0,0 +1,101 @@ +package cloudformation + +import ( + "encoding/json" + + "dagger.io/dagger" + "dagger.io/alpine" + "dagger.io/aws" +) + +// AWS CloudFormation Stack +#Stack: { + + // AWS Config + config: aws.#Config + + // Source is the Cloudformation template (JSON/YAML string) + source: string + + // Stackname is the cloudformation stack + stackName: string + + // Stack parameters + parameters: [string]: _ + + // Behavior when failure to create/update the Stack + onFailure: *"DO_NOTHING" | "ROLLBACK" | "DELETE" + + // Timeout for waiting for the stack to be created/updated (in minutes) + timeout: *10 | uint + + // Never update the stack if already exists + neverUpdate: *false | bool + + #files: { + "/entrypoint.sh": #Code + "/src/template.json": source + if len(parameters) > 0 { + "/src/parameters.json": json.Marshal( + [ for key, val in parameters { + ParameterKey: "\(key)" + ParameterValue: "\(val)" + }]) + "/src/parameters_overrides.json": json.Marshal([ for key, val in parameters {"\(key)=\(val)"}]) + } + } + + outputs: { + [string]: string + + #dagger: compute: [ + dagger.#Load & { + from: alpine.#Image & { + package: bash: "=5.1.0-r0" + package: jq: "=1.6-r1" + package: "aws-cli": "=1.18.177-r0" + } + }, + dagger.#Mkdir & { + path: "/src" + }, + for dest, content in #files { + dagger.#WriteFile & { + "dest": dest + "content": content + } + }, + dagger.#Exec & { + args: [ + "/bin/bash", + "--noprofile", + "--norc", + "-eo", + "pipefail", + "/entrypoint.sh", + ] + env: { + AWS_CONFIG_FILE: "/cache/aws/config" + AWS_ACCESS_KEY_ID: config.accessKey + AWS_SECRET_ACCESS_KEY: config.secretKey + AWS_DEFAULT_REGION: config.region + AWS_REGION: config.region + AWS_DEFAULT_OUTPUT: "json" + AWS_PAGER: "" + if neverUpdate { + NEVER_UPDATE: "true" + } + STACK_NAME: stackName + TIMEOUT: "\(timeout)" + ON_FAILURE: onFailure + } + dir: "/src" + mount: "/cache/aws": "cache" + }, + dagger.#Export & { + source: "/outputs.json" + format: "json" + }, + ] + } +} diff --git a/stdlib/aws/cloudformation/code.cue b/stdlib/aws/cloudformation/code.cue new file mode 100644 index 00000000..7d0d3c6b --- /dev/null +++ b/stdlib/aws/cloudformation/code.cue @@ -0,0 +1,108 @@ +package cloudformation + +#Code: #""" + set +o pipefail + + aws cloudformation validate-template --template-body file:///src/template.json + parameters="" + + function getOutputs() { + aws cloudformation describe-stacks \ + --stack-name "$STACK_NAME" \ + --query 'Stacks[].Outputs' \ + --output json \ + | jq '.[] | map( { (.OutputKey|tostring): .OutputValue } ) | add' \ + > /outputs.json + } + + # Check if the stack exists + aws cloudformation describe-stacks --stack-name "$STACK_NAME" 2>/dev/null || { + if [ -f /src/parameters.json ]; then + parameters="--parameters file:///src/parameters.json" + cat /src/parameters.json + fi + + aws cloudformation create-stack \ + --stack-name "$STACK_NAME" \ + --template-body "file:///src/template.json" \ + --capabilities CAPABILITY_IAM \ + --on-failure "$ON_FAILURE" \ + --timeout-in-minutes "$TIMEOUT" \ + $parameters \ + || { + # Create failed, display errors + aws cloudformation describe-stack-events \ + --stack-name "$STACK_NAME" \ + --max-items 10 \ + | >&2 jq '.StackEvents[] | select((.ResourceStatus | contains("FAILED")) or (.ResourceStatus | contains("ERROR"))) | ("===> ERROR: " + .LogicalResourceId + ": " + .ResourceStatusReason)' + exit 1 + } + + aws cloudformation wait stack-create-complete \ + --stack-name "$STACK_NAME" + + getOutputs + exit 0 + } + + # In case there is an action already in progress, we wait for the corresponding action to complete + wait_action="" + stack_status=$(aws cloudformation describe-stacks --stack-name "$STACK_NAME" | jq -r '.Stacks[].StackStatus') + case "$stack_status" in + "CREATE_FAILED") + echo "Deleting previous failed stack..." + aws cloudformation delete-stack --stack-name "$STACK_NAME" + aws cloudformation wait stack-delete-complete --stack-name "$STACK_NAME" || true + ;; + "CREATE_IN_PROGRESS") + echo "Stack create already in progress, waiting..." + aws cloudformation wait stack-create-complete --stack-name "$STACK_NAME" || true + ;; + "UPDATE_IN_PROGRESS") + # Cancel update to avoid stacks stuck in deadlock (re-apply then works) + echo "Stack update already in progress, waiting..." + aws cloudformation cancel-update-stack --stack-name "$STACK_NAME" || true + ;; + "ROLLBACK_IN_PROGRESS") + echo "Stack rollback already in progress, waiting..." + aws cloudformation wait stack-rollback-complete --stack-name "$STACK_NAME" || true + ;; + "DELETE_IN_PROGRESS") + echo "Stack delete already in progress, waiting..." + aws cloudformation wait stack-delete-complete --stack-name "$STACK_NAME" || true + ;; + "UPDATE_COMPLETE_CLEANUP_IN_PROGRESS") + echo "Stack update almost completed, waiting..." + aws cloudformation wait stack-update-complete --stack-name "$STACK_NAME" || true + ;; + esac + + [ -n "$NEVER_UPDATE" ] && { + getOutputs + exit 0 + } + + # Stack exists, trigger an update via `deploy` + if [ -f /src/parameters_overrides.json ]; then + parameters="--parameter-overrides file:///src/parameters_overrides.json" + cat /src/parameters_overrides.json + fi + echo "Deploying stack $STACK_NAME" + aws cloudformation deploy \ + --stack-name "$STACK_NAME" \ + --template-file "/src/template.json" \ + --capabilities CAPABILITY_IAM \ + --no-fail-on-empty-changeset \ + $parameters \ + || { + # Deploy failed, display errors + echo "Failed to deploy stack $STACK_NAME" + aws cloudformation describe-stack-events \ + --stack-name "$STACK_NAME" \ + --max-items 10 \ + | >&2 jq '.StackEvents[] | select((.ResourceStatus | contains("FAILED")) or (.ResourceStatus | contains("ERROR"))) | ("===> ERROR: " + .LogicalResourceId + ": " + .ResourceStatusReason)' + exit 1 + } + + getOutputs + """#