diff --git a/.github/workflows/build-with-dagger.yaml b/.github/workflows/build-with-dagger.yaml new file mode 100644 index 0000000..17054c2 --- /dev/null +++ b/.github/workflows/build-with-dagger.yaml @@ -0,0 +1,24 @@ +name: Build and Test with Dagger +on: + pull_request: + types: [opened, synchronize] + push: + branches: + - main + +jobs: + build: + name: build + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Build and test + id: build-and-test + uses: dagger/dagger-for-github@8.0.0 + with: + version: "0.16.3" + verb: call + args: build-and-test + cloud-token: ${{ secrets.DAGGER_CLOUD_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/deploy-with-dagger.yaml b/.github/workflows/deploy-with-dagger.yaml index cbeeef9..f503318 100644 --- a/.github/workflows/deploy-with-dagger.yaml +++ b/.github/workflows/deploy-with-dagger.yaml @@ -10,17 +10,19 @@ jobs: build: name: build, publish runs-on: ubuntu-latest + outputs: + image: ${{ steps.publish.outputs.output }} steps: - name: Checkout uses: actions/checkout@v4 - name: Build and publish image id: publish - uses: dagger/dagger-for-github@v7 + uses: dagger/dagger-for-github@8.0.0 with: - version: "latest" + version: "0.16.3" verb: call - args: publish --aws-access-key-id=env://AWS_ACCESS_KEY_ID --aws-secret-access-key=env://AWS_SECRET_ACCESS_KEY + args: build-and-push-image --aws-access-key-id=env://AWS_ACCESS_KEY_ID --aws-secret-access-key=env://AWS_SECRET_ACCESS_KEY --repo-name=${{ vars.REPO_NAME }} --account-id=${{ vars.ACCOUNT_ID }} --region=${{ vars.AWS_REGION }} cloud-token: ${{ secrets.DAGGER_CLOUD_TOKEN }} deploy: @@ -28,18 +30,21 @@ jobs: runs-on: ubuntu-latest needs: build steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Deploy to K8S - uses: dagger/dagger-for-github@v7 + uses: dagger/dagger-for-github@8.0.0 with: - version: "latest" + version: "0.16.3" verb: call - args: deploy --image=${{ steps.publish.outputs.output }} --aws-access-key-id=env://AWS_ACCESS_KEY_ID --aws-secret-access-key=env://AWS_SECRET_ACCESS_KEY --cluster-name ${{ vars.CLUSTER_NAME }} + args: deploy --image=${{ needs.build.outputs.image }} --aws-access-key-id=env://AWS_ACCESS_KEY_ID --aws-secret-access-key=env://AWS_SECRET_ACCESS_KEY --cluster-name ${{ vars.CLUSTER_NAME }} cloud-token: ${{ secrets.DAGGER_CLOUD_TOKEN }} - name: Get Public URL - uses: dagger/dagger-for-github@v7 + uses: dagger/dagger-for-github@8.0.0 with: - version: "latest" + version: "0.16.3" verb: call args: get-ingress --aws-access-key-id=env://AWS_ACCESS_KEY_ID --aws-secret-access-key=env://AWS_SECRET_ACCESS_KEY --cluster-name ${{ vars.CLUSTER_NAME }} cloud-token: ${{ secrets.DAGGER_CLOUD_TOKEN }} diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml index e0f5180..cfce6d5 100644 --- a/.github/workflows/deploy.yaml +++ b/.github/workflows/deploy.yaml @@ -50,20 +50,20 @@ jobs: - name: Build and push Docker image env: - IMAGE_TAG: ${{ secrets.AWS_ACCOUNT_ID }}.dkr.ecr.${{ secrets.AWS_REGION }}.amazonaws.com/parisjug-dagger-demo/translate-api:${{ env.TAG_VERSION }} + IMAGE_TAG: ${{ secrets.AWS_ACCOUNT_ID }}.dkr.ecr.${{ vars.AWS_REGION }}.amazonaws.com/${{ vars.REPO_NAME }}:${{ env.TAG_VERSION }} run: | docker build -f src/main/docker/Dockerfile.jvm --platform linux/amd64,linux/arm64 -t $IMAGE_TAG . docker push $IMAGE_TAG - name: update kube config run: | - aws eks --region ${{ secrets.AWS_REGION }} update-kubeconfig --name ${{ vars.CLUSTER_NAME }} + aws eks --region ${{ vars.AWS_REGION }} update-kubeconfig --name ${{ vars.CLUSTER_NAME }} - name: deploy application to EKS env: - IMAGE_TAG: ${{ secrets.AWS_ACCOUNT_ID }}.dkr.ecr.${{ secrets.AWS_REGION }}.amazonaws.com/parisjug-dagger-demo/translate-api:${{ env.TAG_VERSION }} + IMAGE_TAG: ${{ secrets.AWS_ACCOUNT_ID }}.dkr.ecr.${{ vars.AWS_REGION }}.amazonaws.com/${{ vars.REPO_NAME }}:${{ env.TAG_VERSION }} run: | envsubst < src/main/kube/app.yaml | kubectl apply -f - - name: Logout from Amazon ECR - run: docker logout ${{ secrets.AWS_ACCOUNT_ID }}.dkr.ecr.${{ secrets.AWS_REGION }}.amazonaws.com + run: docker logout ${{ secrets.AWS_ACCOUNT_ID }}.dkr.ecr.${{ vars.AWS_REGION }}.amazonaws.com diff --git a/ci/src/main/java/io/dagger/modules/ci/Ci.java b/ci/src/main/java/io/dagger/modules/ci/Ci.java index af2c25a..6a76d7b 100644 --- a/ci/src/main/java/io/dagger/modules/ci/Ci.java +++ b/ci/src/main/java/io/dagger/modules/ci/Ci.java @@ -1,12 +1,10 @@ package io.dagger.modules.ci; import static io.dagger.client.Dagger.dag; -import static io.dagger.modules.ci.Utils.*; -import io.dagger.client.AwsCli; import io.dagger.client.CacheVolume; +import io.dagger.client.Client.AwsCliArguments; import io.dagger.client.Container; -import io.dagger.client.Container.PublishArguments; import io.dagger.client.DaggerQueryException; import io.dagger.client.Directory; import io.dagger.client.Directory.DockerBuildArguments; @@ -27,93 +25,116 @@ public class Ci { static final Logger LOG = LoggerFactory.getLogger(Ci.class); - private static final List ARCHS = List.of("amd64", "arm64"); - - /** Build and test the application */ + /** + * Build and test the application + * + * @param source the project source directory. Default is the current directory + * @param skipTests whether to skip tests or not + */ @Function - public Container build(@DefaultPath(".") Directory source, @Default("false") boolean skipTests) - throws ExecutionException, DaggerQueryException, InterruptedException { - return buildEnv(source) + public Container buildAndTest(@DefaultPath(".") Directory source, @Default("false") boolean skipTests) { + CacheVolume mavenCache = dag().cacheVolume("m2"); + return dag() + .container() + .from("maven:3-eclipse-temurin-21") + .withDirectory("/src", source) + .withWorkdir("/src") + .withMountedCache("/root/.m2/", mavenCache) .withExec(List.of("mvn", "-B", "clean", "package", "-DskipTests=%s".formatted(skipTests))); } + private Secret ecrToken(Secret awsAccessKeyId, Secret awsSecretAccessKey, String region) { + return dag().awsCli() + .withRegion(region) + .withStaticCredentials(awsAccessKeyId, awsSecretAccessKey) + .ecr().getLoginPassword(); + } + + private Container buildImage(Directory source, Platform platform) { + Container ctr = buildAndTest(source, true); + return ctr.directory(".") + .dockerBuild(new DockerBuildArguments() + .withPlatform(platform) + .withDockerfile("src/main/docker/Dockerfile.jvm")); + } + + private String address(Directory source, String account, String region, String repoName) + throws ExecutionException, DaggerQueryException, InterruptedException { + String hash = dag().gitInfo(source).commitHash().substring(0,8); + return "%s.dkr.ecr.%s.amazonaws.com/%s:%s".formatted(account, region, repoName, hash); + } + /** * Builds the application and create a Docker image */ - @Function - public Container buildImage(@DefaultPath(".") Directory source) - throws ExecutionException, DaggerQueryException, InterruptedException { - Container ctr = build(source, true); - return ctr.directory(".") + // @Function + public Container buildImage(@DefaultPath(".") Directory source) { + return buildAndTest(source, true) + .directory(".") .dockerBuild(new DockerBuildArguments() .withDockerfile("src/main/docker/Dockerfile.jvm")); } /** - * Build a list of Docker images for multiple architectures - * @param source the source directory + * Pushes a Docker image to ECR + * + * @param image the Docker image to push + * @param address the ECR image address of the form .dkr.ecr..amazonaws.com/: + * @param token the ECR authentication token + * + * @return the image address */ - private List buildImageMultiarch(Directory source, List variants) { - List images = variants.stream().map(platform -> { - try { - LOG.info("Building image for {}", platform); - return buildImage(source, platform); - } catch (ExecutionException | DaggerQueryException | InterruptedException e) { - throw new RuntimeException(e); - } - }).toList(); - return images; - }; - - private Container buildImage(Directory source, String platform) + private String pushImage(Container image, String address, Secret token) throws ExecutionException, DaggerQueryException, InterruptedException { - Container ctr = build(source, true); - return ctr.directory(".") - .dockerBuild(new DockerBuildArguments() - .withPlatform(Platform.from(platform)) - .withDockerfile("src/main/docker/Dockerfile.jvm")); + return image + .withRegistryAuth(address, "AWS", token) + .publish(address); } /** - * Publishes the Docker image to ECR + * Builds and publishes the Docker image to ECR * * @param source the source directory * @param awsAccessKeyId the AWS access key ID * @param awsSecretAccessKey the AWS secret access key + * @param accountId the AWS account ID * @param region the AWS region */ @Function - public String publish(@DefaultPath(".") Directory source, Secret awsAccessKeyId, - Secret awsSecretAccessKey, @Default("eu-west-1") String region) + public String buildAndPushImage(@DefaultPath(".") Directory source, String repoName, Secret awsAccessKeyId, Secret awsSecretAccessKey, String accountId, @Default("eu-west-1") String region) throws ExecutionException, DaggerQueryException, InterruptedException { - AwsCli awsCli = aws(region, awsAccessKeyId, awsSecretAccessKey); - Secret token = awsCli.ecr().getLoginPassword(); - String accountId = awsCli.sts().getCallerIdentity().account(); - String address = "%s.dkr.ecr.%s.amazonaws.com/parisjug-dagger-demo/translate-api:%s" - .formatted(accountId, region, dag().gitInfo(source).commitHash().substring(0, 8)); - dag().container() - .withRegistryAuth(address, "AWS", token) - .publish(address, new PublishArguments() - .withPlatformVariants(buildImageMultiarch(source, ARCHS))); - return address; + Container image = buildImage(source, Platform.from("amd64")); + String address = address(source, accountId, region, repoName); + Secret token = ecrToken(awsAccessKeyId, awsSecretAccessKey, region); + return pushImage(image, address, token); + } + + private Container kubectl(String clusterName, Secret awsAccessKeyId, Secret awsSecretAccessKey, String region) { + Container customCtr = dag().container().from("alpine") + .withExec(List.of("apk", "add", "aws-cli", "kubectl")); + List cmd = List.of("eks", "update-kubeconfig", "--name", clusterName); + return dag().awsCli(new AwsCliArguments().withContainer(customCtr)) + .withRegion(region) + .withStaticCredentials(awsAccessKeyId, awsSecretAccessKey) + .exec(cmd); } /** * Deploys the application to EKS * - * @param source the source directory - * @param image the image address to deploy - * @param clusterName the name of the EKS cluster - * @param awsAccessKeyId the AWS access key ID + * @param source the source directory + * @param image the image address to deploy + * @param clusterName the name of the EKS cluster + * @param awsAccessKeyId the AWS access key ID * @param awsSecretAccessKey the AWS secret access key - * @param region the AWS region + * @param region the AWS region */ @Function public String deploy(@DefaultPath(".") Directory source, String image, String clusterName, Secret awsAccessKeyId, Secret awsSecretAccessKey, @Default("eu-west-1") String region) throws ExecutionException, DaggerQueryException, InterruptedException { - String appYaml = envsubst(source.file("src/main/kube/app.yaml").contents(), "IMAGE_TAG", image); - return kubectl(clusterName, region, awsAccessKeyId, awsSecretAccessKey) + String appYaml = source.file("src/main/kube/app.yaml").contents().replace("${IMAGE_TAG}", image); + return kubectl(clusterName, awsAccessKeyId, awsSecretAccessKey, region) .withNewFile("/tmp/app.yaml", appYaml) .withExec(List.of("kubectl", "apply", "-f", "/tmp/app.yaml")) .stdout(); @@ -121,26 +142,17 @@ public String deploy(@DefaultPath(".") Directory source, String image, String cl /** * Returns the ingress address of the application + * * @return the ingress address */ @Function public String getIngress(String clusterName, Secret awsAccessKeyId, Secret awsSecretAccessKey, @Default("eu-west-1") String region) throws ExecutionException, DaggerQueryException, InterruptedException { - String host = kubectl(clusterName, region, awsAccessKeyId, awsSecretAccessKey) - .withExec(List.of("kubectl", "-n", "devoxxfr-dagger", "get", "ingress", "-o", "jsonpath={.items[0].status.loadBalancer.ingress[0].hostname}")) + String host = kubectl(clusterName, awsAccessKeyId, awsSecretAccessKey, region) + .withExec(List.of("kubectl", "-n", "devoxxfr-dagger", "get", "ingress", "-o", + "jsonpath={.items[0].status.loadBalancer.ingress[0].hostname}")) .stdout(); return "http://%s".formatted(host); } - - /** Build a ready-to-use development environment */ - private Container buildEnv(Directory source) { - CacheVolume mavenCache = dag().cacheVolume("m2"); - return dag() - .container() - .from("maven:3-eclipse-temurin-21") - .withDirectory("/src", source) - .withMountedCache(".m2/", mavenCache) - .withWorkdir("/src"); - } } diff --git a/ci/src/main/java/io/dagger/modules/ci/Utils.java b/ci/src/main/java/io/dagger/modules/ci/Utils.java deleted file mode 100644 index 609e2ba..0000000 --- a/ci/src/main/java/io/dagger/modules/ci/Utils.java +++ /dev/null @@ -1,42 +0,0 @@ -package io.dagger.modules.ci; - -import static io.dagger.client.Dagger.dag; - -import io.dagger.client.AwsCli; -import io.dagger.client.Client.AwsCliArguments; -import io.dagger.client.Container; -import io.dagger.client.Container.WithExecArguments; -import io.dagger.client.DaggerQueryException; -import io.dagger.client.Secret; -import java.util.List; -import java.util.concurrent.ExecutionException; - -final class Utils { - - static AwsCli aws(String region, Secret awsAccessKeyId, Secret awsSecretAccessKey) { - return dag().awsCli() - .withRegion(region) - .withStaticCredentials(awsAccessKeyId, awsSecretAccessKey); - } - - static Container kubectl(String clusterName, String region, Secret awsAccessKeyId, Secret awsSecretAccessKey) { - Container customContainer = dag().container().from("alpine") - .withExec(List.of("apk", "add", "aws-cli", "kubectl")); - return dag().awsCli(new AwsCliArguments().withContainer(customContainer)) - .withRegion(region) - .withStaticCredentials(awsAccessKeyId, awsSecretAccessKey) - .exec(List.of("eks", "update-kubeconfig", "--name", clusterName)); - } - - static String envsubst(String content, String... substitutions) - throws ExecutionException, DaggerQueryException, InterruptedException { - Container container = dag().container().from("alpine") - .withExec(List.of("apk", "add", "envsubst")); - for (int i = 0; i < substitutions.length; i += 2) { - container = container.withEnvVariable(substitutions[i], substitutions[i + 1]); - } - return container - .withExec(List.of("envsubst"), new WithExecArguments().withStdin(content)) - .stdout(); - } -}