From c7ef4844331a32afda117f16d4fd686a9edc35fe Mon Sep 17 00:00:00 2001 From: Nicholas Hulston Date: Mon, 16 Dec 2024 17:07:20 -0500 Subject: [PATCH 1/2] automate layer releases --- .gitignore | 2 + .gitlab-ci.yml | 65 ++++++++++++++ .gitlab/Dockerfile | 8 ++ .gitlab/config.yaml | 14 +++ .gitlab/datasources/environments.yaml | 9 ++ .gitlab/datasources/regions.yaml | 31 +++++++ .gitlab/scripts/get_secrets.sh | 48 ++++++++++ .gitlab/scripts/publish_layers.sh | 109 +++++++++++++++++++++++ .gitlab/scripts/pull_and_zip_layers.sh | 23 +++++ .gitlab/scripts/sign_layers.sh | 116 +++++++++++++++++++++++++ .gitlab/template.yaml.tpl | 85 ++++++++++++++++++ 11 files changed, 510 insertions(+) create mode 100644 .gitlab-ci.yml create mode 100644 .gitlab/Dockerfile create mode 100644 .gitlab/config.yaml create mode 100644 .gitlab/datasources/environments.yaml create mode 100644 .gitlab/datasources/regions.yaml create mode 100755 .gitlab/scripts/get_secrets.sh create mode 100755 .gitlab/scripts/publish_layers.sh create mode 100644 .gitlab/scripts/pull_and_zip_layers.sh create mode 100644 .gitlab/scripts/sign_layers.sh create mode 100644 .gitlab/template.yaml.tpl diff --git a/.gitignore b/.gitignore index b1b55a9..8899e04 100644 --- a/.gitignore +++ b/.gitignore @@ -40,3 +40,5 @@ tests/testfunctions/bin/ tests/testfunctions/.serverless/ tests/testfunctions/.settings/ tests/testfunctions/build.gradle + +.gitlab/build-pipeline.yaml diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000..e175301 --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,65 @@ +variables: + DOCKER_TARGET_IMAGE: registry.ddbuild.io/ci/datadog-lambda-java + DOCKER_TARGET_VERSION: latest + # Default version for development builds + # This will be overwritten by the tag version if it is a release. + VERSION: dev + +stages: + - pre + - build + +.go-cache: &go-cache + key: datadog-lambda-java-go-cache + policy: pull + +ci image: + stage: build + image: registry.ddbuild.io/images/docker:20.10 + tags: ["arch:arm64"] + rules: + - if: '$CI_COMMIT_BRANCH == "main" && $CI_PIPELINE_SOURCE == "push"' + changes: + - .gitlab/Dockerfile + when: on_success + variables: + DOCKER_TARGET: ${DOCKER_TARGET_IMAGE}:${DOCKER_TARGET_VERSION} + script: + - docker buildx build --platform linux/amd64,linux/arm64 --no-cache --pull --push --tag ${DOCKER_TARGET} -f .gitlab/Dockerfile . + +generator: + stage: pre + image: registry.ddbuild.io/images/mirror/golang:alpine + tags: ["arch:amd64"] + cache: *go-cache + rules: + - if: '$CI_PIPELINE_SOURCE =~ /external_pull_request_event|merge_request_event|push/' + when: never + - when: always + script: + - if [[ "$CI_COMMIT_TAG" =~ ^v[0-9]+$ ]]; then echo "VERSION=${CI_COMMIT_TAG//[!0-9]/}" >> .env; fi + - apk add --no-cache gomplate + - gomplate --config .gitlab/config.yaml + artifacts: + paths: + - .gitlab/build-pipeline.yaml + reports: + dotenv: .env + +build: + stage: build + needs: ["generator"] + trigger: + include: + - artifact: .gitlab/build-pipeline.yaml + job: generator + strategy: depend + needs: + - job: generator + artifacts: true + rules: + - if: '$CI_PIPELINE_SOURCE =~ /external_pull_request_event|merge_request_event|push/' + when: never + - when: always + variables: + VERSION: $VERSION diff --git a/.gitlab/Dockerfile b/.gitlab/Dockerfile new file mode 100644 index 0000000..34de484 --- /dev/null +++ b/.gitlab/Dockerfile @@ -0,0 +1,8 @@ +FROM registry.ddbuild.io/images/docker:24.0.5 + +RUN apt-get update && apt-get install -y --no-install-recommends \ + curl gnupg unzip zip jq + +# Install AWS CLI +RUN curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip" +RUN unzip awscliv2.zip && ./aws/install diff --git a/.gitlab/config.yaml b/.gitlab/config.yaml new file mode 100644 index 0000000..c9ea317 --- /dev/null +++ b/.gitlab/config.yaml @@ -0,0 +1,14 @@ +# gomplate template generation pipeline + +inputFiles: + - .gitlab/template.yaml.tpl + +outputFiles: + - .gitlab/build-pipeline.yaml + +datasources: + environments: + url: .gitlab/datasources/environments.yaml + + regions: + url: .gitlab/datasources/regions.yaml diff --git a/.gitlab/datasources/environments.yaml b/.gitlab/datasources/environments.yaml new file mode 100644 index 0000000..90056ab --- /dev/null +++ b/.gitlab/datasources/environments.yaml @@ -0,0 +1,9 @@ +environments: + - name: sandbox + external_id: sandbox-publish-externalid + role_to_assume: sandbox-layer-deployer + account: 425362996713 + - name: prod + external_id: prod-publish-externalid + role_to_assume: dd-serverless-layer-deployer-role + account: 464622532012 diff --git a/.gitlab/datasources/regions.yaml b/.gitlab/datasources/regions.yaml new file mode 100644 index 0000000..93816ce --- /dev/null +++ b/.gitlab/datasources/regions.yaml @@ -0,0 +1,31 @@ +regions: + - code: "us-east-1" + - code: "us-east-2" + - code: "us-west-1" + - code: "us-west-2" + - code: "af-south-1" + - code: "ap-east-1" + - code: "ap-south-1" + - code: "ap-south-2" + - code: "ap-southeast-1" + - code: "ap-southeast-2" + - code: "ap-southeast-3" + - code: "ap-southeast-4" + - code: "ap-southeast-5" + - code: "ap-northeast-1" + - code: "ap-northeast-2" + - code: "ap-northeast-3" + - code: "ca-central-1" + - code: "ca-west-1" + - code: "eu-central-1" + - code: "eu-central-2" + - code: "eu-north-1" + - code: "eu-west-1" + - code: "eu-west-2" + - code: "eu-west-3" + - code: "eu-south-1" + - code: "eu-south-2" + - code: "il-central-1" + - code: "me-south-1" + - code: "me-central-1" + - code: "sa-east-1" diff --git a/.gitlab/scripts/get_secrets.sh b/.gitlab/scripts/get_secrets.sh new file mode 100755 index 0000000..51db71a --- /dev/null +++ b/.gitlab/scripts/get_secrets.sh @@ -0,0 +1,48 @@ +#!/bin/bash + +# Unless explicitly stated otherwise all files in this repository are licensed +# under the Apache License Version 2.0. +# This product includes software developed at Datadog (https://www.datadoghq.com/). +# Copyright 2024 Datadog, Inc. + +set -e + +if [ -z "$EXTERNAL_ID_NAME" ]; then + printf "[Error] No EXTERNAL_ID_NAME found.\n" + printf "Exiting script...\n" + exit 1 +fi + +if [ -z "$ROLE_TO_ASSUME" ]; then + printf "[Error] No ROLE_TO_ASSUME found.\n" + printf "Exiting script...\n" + exit 1 +fi + +printf "Getting AWS External ID...\n" + +EXTERNAL_ID=$(aws ssm get-parameter \ + --region us-east-1 \ + --name "ci.datadog-lambda-java.$EXTERNAL_ID_NAME" \ + --with-decryption \ + --query "Parameter.Value" \ + --out text) + +printf "Getting DD API KEY...\n" + +export DD_API_KEY=$(aws ssm get-parameter \ + --region us-east-1 \ + --name ci.datadog-lambda-java.dd-api-key \ + --with-decryption \ + --query "Parameter.Value" \ + --out text) + +printf "Assuming role...\n" + +export $(printf "AWS_ACCESS_KEY_ID=%s AWS_SECRET_ACCESS_KEY=%s AWS_SESSION_TOKEN=%s" \ + $(aws sts assume-role \ + --role-arn "arn:aws:iam::$AWS_ACCOUNT:role/$ROLE_TO_ASSUME" \ + --role-session-name "ci.datadog-lambda-java-$CI_JOB_ID-$CI_JOB_STAGE" \ + --query "Credentials.[AccessKeyId,SecretAccessKey,SessionToken]" \ + --external-id $EXTERNAL_ID \ + --output text)) diff --git a/.gitlab/scripts/publish_layers.sh b/.gitlab/scripts/publish_layers.sh new file mode 100755 index 0000000..798007c --- /dev/null +++ b/.gitlab/scripts/publish_layers.sh @@ -0,0 +1,109 @@ +#!/bin/bash + +# Unless explicitly stated otherwise all files in this repository are licensed +# under the Apache License Version 2.0. +# This product includes software developed at Datadog (https://www.datadoghq.com/). +# Copyright 2024 Datadog, Inc. + +set -e + +LAYER_NAME="dd-trace-java" + +publish_layer() { + region=$1 + + version_nbr=$(aws lambda publish-layer-version --layer-name $LAYER_NAME \ + --description "Datadog Tracer Lambda Layer for Java" \ + --compatible-runtimes "java8" "java11" "java17" "java21" \ + --compatible-architectures "x86_64" "arm64" \ + --zip-file "fileb://.layers/tracer.zip" \ + --region $region \ + | jq -r '.Version') + + # Add permissions only for prod + if [ "$STAGE" == "prod" ]; then + permission=$(aws lambda add-layer-version-permission --layer-name $LAYER_NAME \ + --version-number $version_nbr \ + --statement-id "release-$version_nbr" \ + --action lambda:GetLayerVersion \ + --principal "*" \ + --region $region + ) + fi + + echo $version_nbr +} + + +if [ ! -f ".layers/tracer.zip" ]; then + printf "[ERROR]: Could not find .layers/tracer.zip." + exit 1 +fi + +AVAILABLE_REGIONS=$(aws ec2 describe-regions | jq -r '.[] | .[] | .RegionName') + +if [ -z "$REGION" ]; then + printf "[ERROR]: REGION not specified." + exit 1 +else + printf "Region specified: $REGION\n" + if [[ ! "$AVAILABLE_REGIONS" == *"$REGION"* ]]; then + printf "Could not find $REGION in available regions: $AVAILABLE_REGIONS" + exit 1 + fi +fi + +if [ -z "$STAGE" ]; then + printf "[ERROR]: STAGE not specified.\n" + exit 1 +fi + +printf "[$REGION] Starting publishing layers...\n" + +if [[ "$STAGE" =~ ^(staging|sandbox)$ ]]; then + # Deploy latest version + latest_version=$(aws lambda list-layer-versions --region $REGION --layer-name $LAYER_NAME --query 'LayerVersions[0].Version || `0`') + VERSION=$(($latest_version + 1)) +else + # Running on prod + if [ -z "$CI_COMMIT_TAG" ]; then + printf "[ERROR]: No CI_COMMIT_TAG found.\n" + printf "Exiting script...\n" + exit 1 + else + printf "Tag found in environment: $CI_COMMIT_TAG\n" + fi + + VERSION="${CI_COMMIT_TAG//[!0-9]/}" + printf "Version: ${VERSION}\n" +fi + +if [ -z "$VERSION" ]; then + printf "[ERROR]: Layer VERSION not specified" + exit 1 +else + printf "Layer version parsed: $VERSION\n" +fi + +latest_version=$(aws lambda list-layer-versions --region $REGION --layer-name $LAYER_NAME --query 'LayerVersions[0].Version || `0`') +if [ $latest_version -ge $VERSION ]; then + printf "[$REGION] Layer $LAYER_NAME version $VERSION already exists in region $REGION, skipping...\n" + exit 1 +elif [ $latest_version -lt $((VERSION-1)) ]; then + printf "[$REGION][WARNING] The latest version of layer $LAYER_NAME in region $REGION is $latest_version, this will publish all the missing versions including $VERSION\n" +fi + +while [ $latest_version -lt $VERSION ]; do + latest_version=$(publish_layer $REGION) + printf "[$REGION] Published version $latest_version of layer $LAYER_NAME in region $REGION\n" + + # This shouldn't happen unless someone manually deleted the latest version, say 28, and + # then tries to republish 28 again. The published version would actually be 29, because + # Lambda layers are immutable and AWS will skip deleted version and use the next number. + if [ $latest_version -gt $VERSION ]; then + printf "[$REGION] Published version $latest_version is greater than the desired version $VERSION!" + exit 1 + fi +done + +printf "[$REGION] Finished publishing layer...\n\n" diff --git a/.gitlab/scripts/pull_and_zip_layers.sh b/.gitlab/scripts/pull_and_zip_layers.sh new file mode 100644 index 0000000..b945b73 --- /dev/null +++ b/.gitlab/scripts/pull_and_zip_layers.sh @@ -0,0 +1,23 @@ +#!/bin/bash + +# Unless explicitly stated otherwise all files in this repository are licensed +# under the Apache License Version 2.0. +# This product includes software developed at Datadog (https://www.datadoghq.com/). +# Copyright 2024 Datadog, Inc. + +set -e + +rm -f dd-java-agent.jar +wget -O dd-java-agent.jar https://dtdg.co/latest-java-tracer + +rm -rf .layers +mkdir .layers + +rm -rf layer-temp +mkdir -p layer-temp/java/lib + +cp dd-java-agent.jar layer-temp/java/lib/dd-java-agent.jar +cd layer-temp +zip -r ../.layers/tracer.zip java +cd .. +rm -rf layer-temp diff --git a/.gitlab/scripts/sign_layers.sh b/.gitlab/scripts/sign_layers.sh new file mode 100644 index 0000000..15c76c3 --- /dev/null +++ b/.gitlab/scripts/sign_layers.sh @@ -0,0 +1,116 @@ +#!/bin/bash + +# Unless explicitly stated otherwise all files in this repository are licensed +# under the Apache License Version 2.0. +# This product includes software developed at Datadog (https://www.datadoghq.com/). +# Copyright 2024 Datadog, Inc. +# +# Usage +# +# LAYER_FILE= .gitlab/scripts/sign_layers.sh +# +# LAYER_FILE=dd_trace_dotnet_amd64.zip .gitlab/scripts/sign_layers.sh sandbox + +set -e + +SIGNING_PROFILE_NAME="DatadogLambdaSigningProfile" +VALID_ACCOUNTS=("sandbox" "prod") + +# Check account parameter +if [ -z "$1" ]; then + echo "[ERROR]: Account parameter not specified." + exit 1 +fi + +if [[ ! "${VALID_ACCOUNTS[@]}" =~ $1 ]]; then + echo "[ERROR]: Account parameter is invalid. Not in `sandbox` or `prod`." + exit 1 +fi + +if [ ! -f ".layers/tracer.zip" ]; then + echo "[ERROR]: .layers/tracer.zip not found." + exit 1 +fi + +if [ "$1" = "sandbox" ]; then + REGION="sa-east-1" + S3_BUCKET_NAME="dd-lambda-signing-bucket-serverless-sandbox" +elif [ "$1" = "prod" ]; then + REGION="us-east-1" + S3_BUCKET_NAME="dd-lambda-signing-bucket" +fi + +echo "---" +echo "Signing layer for tracer.zip" + +# Upload the layer to S3 for signing +echo "---" +echo "Uploading layer to S3 for signing..." + +UUID=$(uuidgen) +S3_UNSIGNED_ZIP_KEY="${UUID}.zip" +S3_UNSIGNED_ZIP_URI="s3://${S3_BUCKET_NAME}/${S3_UNSIGNED_ZIP_KEY}" + +aws s3 cp ".layers/tracer.zip" $S3_UNSIGNED_ZIP_URI + +# Start a signing job +echo "---" +echo "Starting the signing job..." +SIGNING_JOB_ID=$(aws signer start-signing-job \ + --source "s3={bucketName=${S3_BUCKET_NAME},key=${S3_UNSIGNED_ZIP_KEY},version=null}" \ + --destination "s3={bucketName=${S3_BUCKET_NAME}}" \ + --profile-name $SIGNING_PROFILE_NAME \ + --region $REGION \ + | jq -r '.jobId' \ +) + +# Wait for the signing job to complete +echo "---" +echo "Waiting for the signing job to complete..." +SECONDS_WAITED_SO_FAR=0 +while : +do + sleep 3 + SECONDS_WAITED_SO_FAR=$((SECONDS_WAITED_SO_FAR + 3)) + + SIGNING_JOB_DESCRIPTION=$(aws signer describe-signing-job \ + --job-id $SIGNING_JOB_ID \ + --region $REGION\ + ) + SIGNING_JOB_STATUS=$(echo $SIGNING_JOB_DESCRIPTION | jq -r '.status') + SIGNING_JOB_STATUS_REASON=$(echo $SIGNING_JOB_DESCRIPTION | jq -r '.statusReason') + + echo "---" + if [ $SIGNING_JOB_STATUS = "Succeeded" ]; then + echo "Signing job succeeded!" + break + fi + + if [ $SIGNING_JOB_STATUS = "Failed" ]; then + echo "[ERROR]: Signing job failed" + echo $SIGNING_JOB_STATUS_REASON + exit 1 + fi + + if [ $SECONDS_WAITED_SO_FAR -ge 60 ]; then + echo "[ERROR]: Timed out waiting for the signing job to complete" + exit 1 + fi + + echo "Signing job still in progress..." +done + +# Download the signed ZIP, overwriting the original ZIP +echo "---" +echo "Replacing the local layer with the signed layer from S3..." +S3_SIGNED_ZIP_KEY="${SIGNING_JOB_ID}.zip" +S3_SIGNED_ZIP_URI="s3://${S3_BUCKET_NAME}/${S3_SIGNED_ZIP_KEY}" +aws s3 cp $S3_SIGNED_ZIP_URI ".layers/tracer.zip" + +# Delete the signed and unsigned ZIPs in S3 +echo "Cleaning up the S3 bucket..." +aws s3api delete-object --bucket $S3_BUCKET_NAME --key $S3_UNSIGNED_ZIP_KEY +aws s3api delete-object --bucket $S3_BUCKET_NAME --key $S3_SIGNED_ZIP_KEY + +echo "---" +echo "Successfully signed tracer.zip!" diff --git a/.gitlab/template.yaml.tpl b/.gitlab/template.yaml.tpl new file mode 100644 index 0000000..cbc33e7 --- /dev/null +++ b/.gitlab/template.yaml.tpl @@ -0,0 +1,85 @@ +stages: + - build + - sign + - publish + +default: + retry: + max: 1 + when: + - runner_system_failure + +variables: + CI_DOCKER_TARGET_IMAGE: registry.ddbuild.io/ci/datadog-lambda-java + CI_DOCKER_TARGET_VERSION: latest + +build layer: + stage: build + tags: ["arch:amd64"] + image: ${CI_DOCKER_TARGET_IMAGE}:${CI_DOCKER_TARGET_VERSION} + artifacts: + expire_in: 2 weeks + paths: + - .layers/tracer.zip + retry: 2 + script: + - .gitlab/scripts/pull_and_zip_layers.sh + +{{ range $environment := (ds "environments").environments }} + +{{ if eq $environment.name "prod" }} +sign layer: + stage: sign + tags: ["arch:amd64"] + image: ${CI_DOCKER_TARGET_IMAGE}:${CI_DOCKER_TARGET_VERSION} + rules: + - if: '$CI_COMMIT_TAG =~ /^v.*/' + when: manual + needs: + - build layer + dependencies: + - build layer + artifacts: # Re-specify artifacts so the modified signed file is passed + expire_in: 2 weeks + paths: + - .layers/tracer.zip + before_script: + - EXTERNAL_ID_NAME={{ $environment.external_id }} ROLE_TO_ASSUME={{ $environment.role_to_assume }} AWS_ACCOUNT={{ $environment.account }} source .gitlab/scripts/get_secrets.sh + script: + - .gitlab/scripts/sign_layers.sh {{ $environment.name }} +{{ end }} + +publish layer {{ $environment.name }}: + stage: publish + tags: ["arch:amd64"] + image: ${CI_DOCKER_TARGET_IMAGE}:${CI_DOCKER_TARGET_VERSION} + rules: + - if: '"{{ $environment.name }}" =~ /^(sandbox|staging)/' + when: manual + allow_failure: true + - if: '$CI_COMMIT_TAG =~ /^v.*/' + needs: +{{ if eq $environment.name "prod" }} + - sign layer +{{ else }} + - build layer +{{ end }} + dependencies: +{{ if eq $environment.name "prod" }} + - sign layer +{{ else }} + - build layer +{{ end }} + parallel: + matrix: + - REGION: {{ range (ds "regions").regions }} + - {{ .code }} + {{- end}} + variables: + STAGE: {{ $environment.name }} + before_script: + - EXTERNAL_ID_NAME={{ $environment.external_id }} ROLE_TO_ASSUME={{ $environment.role_to_assume }} AWS_ACCOUNT={{ $environment.account }} source .gitlab/scripts/get_secrets.sh + script: + - .gitlab/scripts/publish_layers.sh + +{{- end }} # environments end From 9b7c6ec8e978d7a242b5456dd559a9ce3d42baf6 Mon Sep 17 00:00:00 2001 From: Nicholas Hulston Date: Tue, 24 Dec 2024 11:43:01 -0800 Subject: [PATCH 2/2] try to trigger gitlab --- .gitlab/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitlab/Dockerfile b/.gitlab/Dockerfile index 34de484..b171eb3 100644 --- a/.gitlab/Dockerfile +++ b/.gitlab/Dockerfile @@ -3,6 +3,6 @@ FROM registry.ddbuild.io/images/docker:24.0.5 RUN apt-get update && apt-get install -y --no-install-recommends \ curl gnupg unzip zip jq -# Install AWS CLI +# Install AWS CLI. RUN curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip" RUN unzip awscliv2.zip && ./aws/install