diff --git a/examples/README.md b/examples/README.md index a3ef5b88..20665522 100644 --- a/examples/README.md +++ b/examples/README.md @@ -29,14 +29,14 @@ dagger new ``` 3. Configure the deployment with your Netlify access token. -You can create new tokens from the [Netlify dashboard](https://app.netlify.com/user/applications/personal). + You can create new tokens from the [Netlify dashboard](https://app.netlify.com/user/applications/personal). ```sh dagger input text www.account.token MY_TOKEN ``` -*NOTE: there is a dedicated command for encrypted secret inputs, but it is -not yet implemented. Coming soon!* +_NOTE: there is a dedicated command for encrypted secret inputs, but it is +not yet implemented. Coming soon!_ 4. Deploy! @@ -44,6 +44,53 @@ not yet implemented. Coming soon!* dagger up ``` +## Deploy a complete JAMstack app + +This example shows how to deploy a complete app with a backend, a database and a frontend. + +This app assumes the following infrastructure is available: + +- AWS ECS Cluster +- AWS ALB with a TLS certificate +- AWS RDS Instance (MySQL or PostgreSQL) +- AWS ECR repository + +1. Prepare the app configuration + +Edit the file `./examples/jamstack/app_config.cue` and review all values to match to your own needs. + +2. Login your local docker daemon to ECR + +_This step is temporary and will be removed soon (gh issue #301)._ + +```sh +AWS_REGION="" +AWS_ID="" +aws ecr get-login-password --region "$AWS_REGION" | docker login --username AWS --password-stdin "${AWS_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com" +``` + +3. Deploy! + +```sh +cd ./examples/jamstack +dagger new +dagger up +``` + +The example `app_config.cue` from the `./examples/jamstack` directory takes the source code from a remote git repository, +but you can remove this from the file and instead points to a local source code: + +```sh +dagger input dir backend.source ./my/local/backend/code +``` + +And the same mechanism applies for every single key in this file. + +4. Get the App URL + +```sh +dagger query url +``` ## Provision a Kubernetes cluster on AWS @@ -79,7 +126,6 @@ dagger input text awsConfig.accessKey MY_AWS_ACCESS_KEY dagger input text awsConfig.secretKey MY_AWS_SECRET_KEY ``` - 4. Deploy! ```sh @@ -103,7 +149,6 @@ Components: - [Amazon Cloudwatch Synthetics](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Synthetics_Canaries.html) for hosting the monitoring scripts - [Amazon CloudFormation](https://aws.amazon.com/cloudformation) for infrastructure provisioning - 1. Change the current directory to the example deployment plan ```sh @@ -142,7 +187,6 @@ dagger input text email my_email@my_domain.tld dagger up ``` - ## Deploy an application to your Kubernetes cluster This example shows two different ways to deploy an application to an existing Kubernetes cluster: with and without a Helm chart. Read the deployment plan](https://github.com/dagger/dagger/tree/main/examples/kubernetes-app) @@ -157,7 +201,6 @@ Components: How to run: - 1. Change the current directory to the example deployment plan ```sh diff --git a/examples/jamstack/app_config.cue b/examples/jamstack/app_config.cue new file mode 100644 index 00000000..db7d44f9 --- /dev/null +++ b/examples/jamstack/app_config.cue @@ -0,0 +1,58 @@ +package main + +import ( + "dagger.io/git" +) + +name: "my-app" + +// DISCLAIMER: all values below are fake and are provided as examples + +infra: { + awsConfig: { + accessKey: "" + secretKey: "" + region: "us-east-1" + } + vpcId: "vpc-020ctgv0bcde4242" + ecrRepository: "8563296674124.dkr.ecr.us-east-1.amazonaws.com/apps" + ecsClusterName: "bl-ecs-acme-764-ECSCluster-lRIVVg09G4HX" + elbListenerArn: "arn:aws:elasticloadbalancing:us-east-1:8563296674124:listener/app/bl-ec-ECSAL-OSYI03K07BCO/3c2d3e78347bde5b/d02ac88cc007e24e" + rdsAdminSecretArn: "arn:aws:secretsmanager:us-east-1:8563296674124:secret:AdminPassword-NQbBi7oU4CYS9-IGgS3B" + rdsInstanceArn: "arn:aws:rds:us-east-1:8563296674124:cluster:bl-rds-acme-764-rdscluster-8eg3xbfjggkfdg" + netlifyAccount: token: "" +} + +database: dbType: "mysql" + +backend: { + source: git.#Repository & { + remote: "https://github.com/blocklayerhq/acme-clothing.git" + ref: "HEAD" + subdir: "./crate/code/api" + } + + // DNS needs to be already configured to the ALB load-balancer + // and a valid certificate needs to be configured for that listener + hostname: "\(name).acme-764-api.microstaging.io" + + container: { + healthCheckPath: "/health-check" + healthCheckTimeout: 40 + } +} + +frontend: { + source: git.#Repository & { + remote: "https://github.com/blocklayerhq/acme-clothing.git" + ref: "HEAD" + subdir: "./crate/code/web" + } + + writeEnvFile: ".env" + + yarn: { + buildDir: "public" + script: "build:client" + } +} diff --git a/examples/jamstack/backend.cue b/examples/jamstack/backend.cue new file mode 100644 index 00000000..40458217 --- /dev/null +++ b/examples/jamstack/backend.cue @@ -0,0 +1,104 @@ +package main + +import ( + "dagger.io/dagger" + "dagger.io/aws/ecs" +) + +// Backend configuration +backend: { + // Source code to build this container + source: dagger.#Artifact + + // Container environment variables + environment: [string]: string + + // Public hostname (need to match the master domain configures on the loadbalancer) + hostname: string + + // Container configuration + container: { + // Desired number of running containers + desiredCount: *1 | int + // Time to wait for the HTTP timeout to complete + healthCheckTimeout: *10 | int + // HTTP Path to perform the healthcheck request (HTTP Get) + healthCheckPath: *"/" | string + // Number of times the health check needs to fail before recycling the container + healthCheckUnhealthyThreshold: *2 | int + // Port used by the process inside the container + port: *80 | int + // Memory to allocate + memory: *1024 | int + // Override the default container command + command: [...string] + // Custom dockerfile path + dockerfilePath: *"" | string + // docker build args + dockerBuildArgs: [string]: string + } + + // Init container runs only once when the main container starts + initContainer: { + command: [...string] + environment: [string]: string + } +} + +// Backend deployment logic +backend: { + let slug = name + + // Docker image built from source, pushed to ECR + image: #ECRImage & { + source: backend.source + repository: infra.ecrRepository + tag: slug + awsConfig: infra.awsConfig + if backend.container.dockerfilePath != "" { + dockerfilePath: backend.container.dockerfilePath + } + buildArgs: backend.container.dockerBuildArgs + } + + // Creates an ECS Task + Service + deploy via Cloudformation + app: #ECSApp & { + awsConfig: infra.awsConfig + "slug": slug + clusterName: infra.ecsClusterName + vpcId: infra.vpcId + elbListenerArn: infra.elbListenerArn + if infra.ecsTaskRoleArn != _|_ { + taskRoleArn: infra.ecsTaskRoleArn + } + hostname: backend.hostname + healthCheck: { + timeout: backend.container.healthCheckTimeout + path: backend.container.healthCheckPath + unhealthyThresholdCount: backend.container.healthCheckUnhealthyThreshold + } + desiredCount: backend.container.desiredCount + container: { + command: backend.container.command + environment: backend.environment + port: backend.container.port + memory: backend.container.memory + "image": image.ref + } + } + + // Optional container to run one-time during the deploy (eg. db migration) + if len(backend.initContainer.command) > 0 { + initContainer: ecs.#RunTask & { + config: infra.awsConfig + containerName: slug + cluster: infra.ecsClusterName + if infra.ecsTaskRoleArn != _|_ { + roleArn: infra.ecsTaskRoleArn + } + containerEnvironment: backend.initContainer.environment + containerCommand: backend.initContainer.command + taskArn: app.taskArn + } + } +} diff --git a/examples/jamstack/database.cue b/examples/jamstack/database.cue new file mode 100644 index 00000000..a2ed656e --- /dev/null +++ b/examples/jamstack/database.cue @@ -0,0 +1,41 @@ +package main + +import ( + "encoding/base64" + "dagger.io/aws/rds" +) + +database: { + let slug = name + dbType: "mysql" | "postgresql" + + db: rds.#CreateDB & { + config: infra.awsConfig + name: slug + dbArn: infra.rdsInstanceArn + "dbType": dbType + secretArn: infra.rdsAdminSecretArn + } + + user: rds.#CreateUser & { + config: infra.awsConfig + dbArn: infra.rdsInstanceArn + "dbType": dbType + secretArn: infra.rdsAdminSecretArn + username: slug + // FIXME: make it secure (generate infra side?) + password: base64.Encode(null, "pwd-\(slug)") + grantDatabase: db.out + } + + instance: rds.#Instance & { + config: infra.awsConfig + dbArn: infra.rdsInstanceArn + } + + hostname: instance.hostname + port: instance.port + dbName: db.out + username: user.out + password: user.password +} diff --git a/examples/jamstack/ecr_image.cue b/examples/jamstack/ecr_image.cue new file mode 100644 index 00000000..81d44fe6 --- /dev/null +++ b/examples/jamstack/ecr_image.cue @@ -0,0 +1,52 @@ +package main + +import ( + "dagger.io/dagger" + "dagger.io/dagger/op" + "dagger.io/aws" + "dagger.io/aws/ecr" +) + +// Build an image and push it to ECR +#ECRImage: { + source: dagger.#Artifact + // Path of the Dockerfile + dockerfilePath?: string + repository: string + tag: string + awsConfig: aws.#Config + buildArgs: [string]: string + + pushTarget: "\(repository):\(tag)" + + // Build the image + buildImage: op.#DockerBuild & { + context: source + if dockerfilePath != _|_ { + "dockerfilePath": dockerfilePath + } + buildArg: buildArgs + } + + // Use these credentials to push + ecrCreds: ecr.#Credentials & { + config: awsConfig + target: pushTarget + } + + push: #up: [ + op.#DockerBuild & { + context: source + if dockerfilePath != _|_ { + "dockerfilePath": dockerfilePath + } + buildArg: buildArgs + }, + op.#PushContainer & { + ref: pushTarget + }, + ] + + // FIXME: ref does not include the sha256: https://github.com/dagger/dagger/issues/303 + ref: pushTarget +} diff --git a/examples/jamstack/ecs.cue b/examples/jamstack/ecs.cue new file mode 100644 index 00000000..e496c8ac --- /dev/null +++ b/examples/jamstack/ecs.cue @@ -0,0 +1,152 @@ +package main + +import ( + "encoding/json" + + "dagger.io/aws" + "dagger.io/aws/elb" + "dagger.io/aws/cloudformation" +) + +#ECSApp: { + awsConfig: aws.#Config + slug: string + clusterName: string + vpcId: string + elbListenerArn: string + taskRoleArn: *"" | string + hostname: string + healthCheck: { + timeout: *10 | int + path: *"/" | string + unhealthyThresholdCount: *2 | int + } + desiredCount: int + container: { + command: [...string] + environment: [string]: string + port: *80 | int + cpu: *256 | int + memory: *1024 | int + image: string + } + + taskArn: cfnStack.outputs.TaskArn + + elbRulePriority: elb.#RandomRulePriority & { + config: awsConfig + listenerArn: elbListenerArn + vhost: hostname + } + + cfnStack: cloudformation.#Stack & { + config: awsConfig + stackName: slug + onFailure: "DO_NOTHING" + parameters: { + ELBRulePriority: elbRulePriority.out + ImageRef: container.image + ELBListenerArn: elbListenerArn + } + source: json.Marshal(template) + } + + template: { + AWSTemplateFormatVersion: "2010-09-09" + Description: "Dagger deployed app" + Parameters: { + ELBRulePriority: Type: "Number" + ImageRef: Type: "String" + ELBListenerArn: Type: "String" + } + Resources: { + ECSTaskDefinition: { + Type: "AWS::ECS::TaskDefinition" + Properties: { + Cpu: "\(container.cpu)" + Memory: "\(container.memory)" + if taskRoleArn != "" { + TaskRoleArn: taskRoleArn + } + NetworkMode: "bridge" + ContainerDefinitions: [{ + if len(container.command) > 0 { + Command: container.command + } + Name: slug + Image: Ref: "ImageRef" + Essential: true + Environment: [ for k, v in container.environment { + Name: k + Value: v + }] + PortMappings: [{ + ContainerPort: container.port + }] + StopTimeout: 5 + LogConfiguration: { + LogDriver: "awslogs" + Options: { + "awslogs-group": "bl/provider/ecs/\(clusterName)" + "awslogs-region": Ref: "AWS::Region" + "awslogs-create-group": "true" + "awslogs-stream-prefix": slug + } + } + }] + } + } + ECSListenerRule: { + Type: "AWS::ElasticLoadBalancingV2::ListenerRule" + Properties: { + ListenerArn: Ref: "ELBListenerArn" + Priority: Ref: "ELBRulePriority" + Conditions: [{ + Field: "host-header" + Values: [hostname]}] + Actions: [{ + Type: "forward" + TargetGroupArn: Ref: "ECSTargetGroup" + }]}} + ECSTargetGroup: { + Type: "AWS::ElasticLoadBalancingV2::TargetGroup" + Properties: { + Protocol: "HTTP" + VpcId: vpcId + Port: 80 + HealthCheckPath: healthCheck.path + UnhealthyThresholdCount: healthCheck.unhealthyThresholdCount + HealthCheckTimeoutSeconds: healthCheck.timeout + HealthCheckIntervalSeconds: healthCheck.timeout + 1 + HealthyThresholdCount: 3 + TargetGroupAttributes: [{ + Value: "10" + Key: "deregistration_delay.timeout_seconds" + }]}} + ECSService: { + Type: "AWS::ECS::Service" + Properties: { + Cluster: clusterName + DesiredCount: desiredCount + LaunchType: "EC2" + LoadBalancers: [{ + ContainerPort: container.port + TargetGroupArn: Ref: "ECSTargetGroup" + ContainerName: slug + }] + ServiceName: slug + TaskDefinition: Ref: "ECSTaskDefinition" + DeploymentConfiguration: { + DeploymentCircuitBreaker: { + Enable: true + Rollback: true + } + MaximumPercent: 200 + MinimumHealthyPercent: 100 + }} + DependsOn: "ECSListenerRule" + } + } + Outputs: TaskArn: Value: Ref: "ECSTaskDefinition" + } +} diff --git a/examples/jamstack/frontend.cue b/examples/jamstack/frontend.cue new file mode 100644 index 00000000..7e052f99 --- /dev/null +++ b/examples/jamstack/frontend.cue @@ -0,0 +1,52 @@ +package main + +import ( + "dagger.io/dagger" + "dagger.io/netlify" + "dagger.io/js/react" +) + +frontend: { + // Source code to build the app + source: dagger.#Artifact + + writeEnvFile?: string + + // Yarn Build + yarn: { + // Run this yarn script + script: string | *"build" + + // Read build output from this directory + // (path must be relative to working directory). + buildDir: string | *"build" + } + + // Build environment variables + environment: [string]: string + environment: NODE_ENV: string | *"production" + environment: APP_URL: "https://\(name).netlify.app/" +} + +frontend: { + app: react.#App & { + source: frontend.source + env: frontend.environment + + if frontend.writeEnvFile != _|_ { + writeEnvFile: frontend.writeEnvFile + } + + yarn: { + script: frontend.yarn.script + buildDir: frontend.yarn.buildDir + } + } + + // Host the application with Netlify + site: netlify.#Site & { + "name": name + account: infra.netlifyAccount + contents: app.build + } +} diff --git a/examples/jamstack/infra.cue b/examples/jamstack/infra.cue new file mode 100644 index 00000000..9b04eccc --- /dev/null +++ b/examples/jamstack/infra.cue @@ -0,0 +1,35 @@ +package main + +import ( + "dagger.io/aws" + "dagger.io/netlify" +) + +infra: { + // AWS auth & default region + awsConfig: aws.#Config + + // VPC Id + vpcId: string + + // ECR Image repository + ecrRepository: string + + // ECS cluster name + ecsClusterName: string + + // Execution Role ARN used for all tasks running on the cluster + ecsTaskRoleArn?: string + + // ELB listener ARN + elbListenerArn: string + + // Secret ARN for the admin password of the RDS Instance + rdsAdminSecretArn: string + + // ARN of the RDS Instance + rdsInstanceArn: string + + // Netlify credentials + netlifyAccount: netlify.#Account +} diff --git a/examples/jamstack/main.cue b/examples/jamstack/main.cue new file mode 100644 index 00000000..e9e4d2ab --- /dev/null +++ b/examples/jamstack/main.cue @@ -0,0 +1,22 @@ +package main + +// Name of the application +name: string & =~"[a-z0-9-]+" + +// Inject db info in the container environment +backend: environment: { + DB_USERNAME: database.username + DB_HOSTNAME: database.hostname + DB_PASSWORD: database.password + DB_DBNAME: database.dbName + DB_PORT: "\(database.port)" + DB_TYPE: database.dbType +} + +// Configure the frontend with the API URL +frontend: environment: APP_URL_API: url.backendURL + +url: { + frontendURL: frontend.site.url + backendURL: "https://\(backend.hostname)/" +} diff --git a/stdlib/aws/aws.cue b/stdlib/aws/aws.cue index 12ed1b1b..1a3663c8 100644 --- a/stdlib/aws/aws.cue +++ b/stdlib/aws/aws.cue @@ -18,14 +18,94 @@ import ( // Re-usable aws-cli component #CLI: { + package: [string]: string + #up: [ op.#Load & { from: alpine.#Image & { - package: bash: "=5.1.0-r0" - package: jq: "=1.6-r1" - package: curl: "=7.74.0-r1" - package: "aws-cli": "=1.18.177-r0" + "package": package + "package": bash: "=5.1.0-r0" + "package": jq: "=1.6-r1" + "package": curl: "=7.74.0-r1" + "package": "aws-cli": "=1.18.177-r0" } }, ] } + +// Helper for writing scripts based on AWS CLI +#Script: { + // AWS code + config: #Config + + // Script code (bash) + code: string + + // Extra pkgs to install + package: [string]: string + + // Files to mount + files: [string]: string + + // Env variables + env: [string]: string + + // Export file + export: string + + // Always execute the script? + always?: bool + + out: { + string + + #up: [ + op.#Load & { + from: #CLI & { + "package": package + } + }, + op.#Mkdir & { + path: "/inputs" + }, + for k, v in files { + op.#WriteFile & { + dest: k + content: v + } + }, + op.#WriteFile & { + dest: "/entrypoint.sh" + content: code + }, + op.#Exec & { + if always != _|_ { + "always": always + } + args: [ + "/bin/bash", + "--noprofile", + "--norc", + "-eo", + "pipefail", + "/entrypoint.sh", + ] + "env": env + "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: "" + } + mount: "/cache/aws": "cache" + }, + op.#Export & { + source: export + format: "string" + }, + ] + } +} diff --git a/stdlib/aws/cloudformation/cloudformation.cue b/stdlib/aws/cloudformation/cloudformation.cue index 3fedd398..6501803b 100644 --- a/stdlib/aws/cloudformation/cloudformation.cue +++ b/stdlib/aws/cloudformation/cloudformation.cue @@ -60,6 +60,7 @@ import ( } }, op.#Exec & { + always: true args: [ "/bin/bash", "--noprofile", diff --git a/stdlib/aws/ecr/ecr.cue b/stdlib/aws/ecr/ecr.cue new file mode 100644 index 00000000..75b0c050 --- /dev/null +++ b/stdlib/aws/ecr/ecr.cue @@ -0,0 +1,36 @@ +package ecr + +import ( + "dagger.io/dagger" + "dagger.io/aws" +) + +// Credentials retriever for ECR +#Credentials: { + + // AWS Config + config: aws.#Config + + // Target is the ECR image + target: string + + out: dagger.#Secret + + // ECR credentials + credentials: dagger.#RegistryCredentials & { + username: "AWS" + secret: out + } + + aws.#Script & { + "config": config + export: "/out" + code: """ + aws ecr get-login-password > /out + """ + } + + // Authentication for ECR Registries + auth: dagger.#RegistryAuth + auth: "\(target)": credentials +} diff --git a/stdlib/aws/ecs/run-task.cue b/stdlib/aws/ecs/run-task.cue new file mode 100644 index 00000000..c1ce72cf --- /dev/null +++ b/stdlib/aws/ecs/run-task.cue @@ -0,0 +1,67 @@ +package ecs + +import ( + "dagger.io/aws" +) + +// RunTask implement ecs run-task +#RunTask: { + + // AWS Config + config: aws.#Config + + // ECS cluster name + cluster: string + + // Arn of the task to run + taskArn: string + + // Environment variables of the task + containerEnvironment: [string]: string + + // Container name + containerName: string + + // Container command to give + containerCommand: [...string] + + // Task role ARN + roleArn: string | *"" + + containerOverrides: { + containerOverrides: [{ + name: containerName + if len(containerCommand) > 0 { + command: containerCommand + } + if len(containerEnvironment) > 0 { + environment: [ for k, v in containerEnvironment { + name: k + value: v + }] + } + }] + if roleArn != "" { + taskRoleArn: roleArn + } + } + + aws.#Script & { + "config": config + export: "/out" + files: { + "/inputs/cluster": cluster + "/inputs/task_arn": taskArn + "/inputs/container_overrides": containerOverrides + } + code: #""" + cat /inputs/container_overrides | jq + + aws ecs run-task \ + --cluster "$(cat /inputs/cluster)" \ + --task-definition "$(cat /inputs/task_arn)" \ + --overrides "$(cat /inputs/container_overrides)" \ + > /out + """# + } +} diff --git a/stdlib/aws/elb/elb.cue b/stdlib/aws/elb/elb.cue new file mode 100644 index 00000000..c5077065 --- /dev/null +++ b/stdlib/aws/elb/elb.cue @@ -0,0 +1,71 @@ +package elb + +import ( + "dagger.io/aws" +) + +// Returns a non-taken rule priority (randomized) +#RandomRulePriority: { + // AWS Config + config: aws.#Config + + // ListenerArn + listenerArn: string + + // Optional vhost for reusing priorities + vhost?: string + + // exported priority + out: string + + aws.#Script & { + // FIXME: we should not rely on the cache for this but it's being + // executed several times if enabled: https://github.com/dagger/dagger/issues/42 + // always: true + + files: { + "/inputs/listenerArn": listenerArn + if vhost != _|_ { + "/inputs/vhost": vhost + } + } + + export: "/priority" + + //FIXME: The code below can end up not finding an available prio + // Better to exclude the existing allocated priorities from the random sequence + code: #""" + if [ -s /inputs/vhost ]; then + # We passed a vhost as input, try to recycle priority from previously allocated vhost + vhost="$(cat /inputs/vhost)" + + priority=$(aws elbv2 describe-rules \ + --listener-arn "$(cat /inputs/listenerArn)" | \ + jq -r --arg vhost "$vhost" '.Rules[] | select(.Conditions[].HostHeaderConfig.Values[] == $vhost) | .Priority') + + if [ -n "${priority}" ]; then + echo -n "${priority}" > /priority + exit 0 + fi + fi + + # Grab a priority random from 1-50k and check if available, retry 10 times if none available + priority=0 + for i in {1..10} + do + p=$(shuf -i 1-50000 -n 1) + # Find the next priority available that we can allocate + aws elbv2 describe-rules \ + --listener-arn "$(cat /inputs/listenerArn)" \ + | jq -e "select(.Rules[].Priority == \"${p}\") | true" && continue + priority="${p}" + break + done + if [ "${priority}" -lt 1 ]; then + echo "Error: cannot determine a Rule priority" + exit 1 + fi + echo -n "${priority}" > /priority + """# + } +} diff --git a/stdlib/aws/rds/rds.cue b/stdlib/aws/rds/rds.cue new file mode 100644 index 00000000..3865006e --- /dev/null +++ b/stdlib/aws/rds/rds.cue @@ -0,0 +1,196 @@ +package rds + +import ( + "encoding/json" + "dagger.io/dagger" + "dagger.io/aws" +) + +#CreateDB: { + // AWS Config + config: aws.#Config + + // DB name + name: string + + // ARN of the database instance + dbArn: string + + // ARN of the database secret (for connecting via rds api) + secretArn: string + + dbType: "mysql" | "postgres" + + // Name of the DB created + out: string + + aws.#Script & { + "config": config + + files: { + "/inputs/name": name + "/inputs/db_arn": dbArn + "/inputs/secret_arn": secretArn + "/inputs/db_type": dbType + } + + export: "/db_created" + + code: #""" + set +o pipefail + + dbType="$(cat /inputs/db_type)" + echo "dbType: $dbType" + + sql="CREATE DATABASE \`$(cat /inputs/name)\`" + if [ "$dbType" = postgres ]; then + sql="CREATE DATABASE \"$(cat /inputs/name)\"" + fi + + aws rds-data execute-statement \ + --resource-arn "$(cat /inputs/db_arn)" \ + --secret-arn "$(cat /inputs/secret_arn)" \ + --sql "$sql" \ + --database "$dbType" \ + --no-include-result-metadata \ + |& tee /tmp/out + exit_code=${PIPESTATUS[0]} + if [ $exit_code -ne 0 ]; then + cat /tmp/out + grep -q "database exists\|already exists" /tmp/out + [ $? -ne 0 ] && exit $exit_code + fi + cp /inputs/name /db_created + """# + } +} + +#CreateUser: { + // AWS Config + config: aws.#Config + + // Username + username: dagger.#Secret + + // Password + password: dagger.#Secret + + // ARN of the database instance + dbArn: string + + // ARN of the database secret (for connecting via rds api) + secretArn: string + + grantDatabase: string | *"" + + dbType: "mysql" | "postgres" + + // Outputed username + out: string + + aws.#Script & { + "config": config + + files: { + "/inputs/username": username + "/inputs/password": password + "/inputs/db_arn": dbArn + "/inputs/secret_arn": secretArn + "/inputs/grant_database": grantDatabase + "/inputs/db_type": dbType + } + + export: "/username" + + code: #""" + set +o pipefail + + dbType="$(cat /inputs/db_type)" + echo "dbType: $dbType" + + sql="CREATE USER '$(cat /inputs/username)'@'%' IDENTIFIED BY '$(cat /inputs/password)'" + if [ "$dbType" = postgres ]; then + sql="CREATE USER \"$(cat /inputs/username)\" WITH PASSWORD '$(cat /inputs/password)'" + fi + + aws rds-data execute-statement \ + --resource-arn "$(cat /inputs/db_arn)" \ + --secret-arn "$(cat /inputs/secret_arn)" \ + --sql "$sql" \ + --database "$dbType" \ + --no-include-result-metadata \ + |& tee tmp/out + exit_code=${PIPESTATUS[0]} + if [ $exit_code -ne 0 ]; then + cat tmp/out + grep -q "Operation CREATE USER failed for\|ERROR" tmp/out + [ $? -ne 0 ] && exit $exit_code + fi + cp /inputs/username /username + + sql="SET PASSWORD FOR '$(cat /inputs/username)'@'%' = PASSWORD('$(cat /inputs/password)')" + if [ "$dbType" = postgres ]; then + sql="ALTER ROLE \"$(cat /inputs/username)\" WITH PASSWORD '$(cat /inputs/password)'" + fi + + aws rds-data execute-statement \ + --resource-arn "$(cat /inputs/db_arn)" \ + --secret-arn "$(cat /inputs/secret_arn)" \ + --sql "$sql" \ + --database "$dbType" \ + --no-include-result-metadata + + sql="GRANT ALL ON \`$(cat /inputs/grant_database)\`.* to '$(cat /inputs/username)'@'%'" + if [ "$dbType" = postgres ]; then + sql="GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO \"$(cat /inputs/username)\"; GRANT ALL PRIVILEGES ON DATABASE \"$(cat /inputs/grant_database)\" to \"$(cat /inputs/username)\"; GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public TO \"$(cat /inputs/username)\"; ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL PRIVILEGES ON TABLES TO \"$(cat /inputs/username)\"; ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL PRIVILEGES ON SEQUENCES TO \"$(cat /inputs/username)\"; GRANT USAGE ON SCHEMA public TO \"$(cat /inputs/username)\";" + fi + + if [ -s /inputs/grant_database ]; then + aws rds-data execute-statement \ + --resource-arn "$(cat /inputs/db_arn)" \ + --secret-arn "$(cat /inputs/secret_arn)" \ + --sql "$sql" \ + --database "$dbType" \ + --no-include-result-metadata + fi + """# + } +} + +#Instance: { + // AWS Config + config: aws.#Config + + // ARN of the database instance + dbArn: string + + // DB hostname + hostname: info.hostname + + // DB port + port: info.port + + info: { + hostname: string + port: int + } + + info: json.Unmarshal(out) + out: string + + aws.#Script & { + "config": config + + files: "/inputs/db_arn": dbArn + + export: "/out" + + code: #""" + db_arn="$(cat /inputs/db_arn)" + data=$(aws rds describe-db-clusters --filters "Name=db-cluster-id,Values=$db_arn" ) + echo "$data" | jq -r '.DBClusters[].Endpoint' > /tmp/out + echo "$data" | jq -r '.DBClusters[].Port' >> /tmp/out + cat /tmp/out | jq -sR 'split("\n") | {hostname: .[0], port: (.[1] | tonumber)}' > /out + """# + } +} diff --git a/stdlib/git/git.cue b/stdlib/git/git.cue index 6babaacc..33de44c1 100644 --- a/stdlib/git/git.cue +++ b/stdlib/git/git.cue @@ -9,11 +9,17 @@ import ( remote: string ref: string + subdir: string | *"" #up: [ op.#FetchGit & { "remote": remote "ref": ref }, + if subdir != "" { + op.#Subdir & { + dir: subdir + } + }, ] } diff --git a/stdlib/js/react/react.cue b/stdlib/js/react/react.cue index 420e1452..a3fbc74e 100644 --- a/stdlib/js/react/react.cue +++ b/stdlib/js/react/react.cue @@ -1,9 +1,11 @@ package react import ( + "strings" + "dagger.io/dagger" + "dagger.io/dagger/op" "dagger.io/alpine" - "dagger.io/docker" ) // A ReactJS application @@ -11,6 +13,13 @@ import ( // Application source code source: dagger.#Artifact + // Environment variables + env: [string]: string + + // Write the contents of `environment` to this file, + // in the "envfile" format. + writeEnvFile: string | *"" + // Yarn-specific settings yarn: { // Read build output from this directory @@ -20,47 +29,49 @@ import ( // Run this yarn script script: string | *"build" } - setup: [ - "mkdir -p /cache/yarn", - ] - // Build the application in a container, using yarn - build: docker.#Container & { - image: alpine.#Image & { - package: bash: "=~5.1" - package: yarn: "=~1.22" - } - dir: "/src" - command: """ - yarn install --production false - yarn run "$YARN_BUILD_SCRIPT" - mv "$YARN_BUILD_DIRECTORY" \(outputDir) - """ - volume: { - src: { - from: source - dest: "/src" + build: #up: [ + op.#Load & { + from: alpine.#Image & { + package: bash: "=~5.1" + package: yarn: "=~1.22" } - // yarnCache: { - // type: "cache" - // dest: "/cache/yarn" - // } - } - outputDir: "/build" - shell: { - path: "/bin/bash" + }, + op.#Exec & { args: [ + "/bin/bash", "--noprofile", "--norc", - "-eo", "pipefail", + "-eo", + "pipefail", "-c", + """ + [ -n "$ENVFILE_NAME" ] && echo "$ENVFILE" > "$ENVFILE_NAME" + yarn install --production false + yarn run "$YARN_BUILD_SCRIPT" + mv "$YARN_BUILD_DIRECTORY" /build + """, ] - } - env: { - YARN_BUILD_SCRIPT: yarn.script - YARN_CACHE_FOLDER: "/cache/yarn" - YARN_BUILD_DIRECTORY: yarn.buildDir - } - } - + if env != _|_ { + "env": env + } + "env": { + YARN_BUILD_SCRIPT: yarn.script + YARN_CACHE_FOLDER: "/cache/yarn" + YARN_BUILD_DIRECTORY: yarn.buildDir + if writeEnvFile != "" { + ENVFILE_NAME: writeEnvFile + ENVFILE: strings.Join([ for k, v in env {"\(k)=\(v)"}], "\n") + } + } + dir: "/src" + mount: { + "/src": from: source + "/cache/yarn": "cache" + } + }, + op.#Subdir & { + dir: "/build" + }, + ] } diff --git a/stdlib/netlify/netlify.cue b/stdlib/netlify/netlify.cue index 012bb8df..b9410867 100644 --- a/stdlib/netlify/netlify.cue +++ b/stdlib/netlify/netlify.cue @@ -3,7 +3,7 @@ package netlify import ( "dagger.io/dagger" "dagger.io/alpine" - "dagger.io/docker" + "dagger.io/dagger/op" ) // A Netlify account @@ -42,52 +42,45 @@ import ( // Logs URL for this deployment logsUrl: string - // Deployment container - #deploy: docker.#Container & { - image: alpine.#Image & { - package: { - bash: "=~5.1" - jq: "=~1.6" - curl: "=~7.74" - yarn: "=~1.22" + #up: [ + op.#Load & { + from: alpine.#Image & { + package: bash: "=~5.1" + package: jq: "=~1.6" + package: curl: "=~7.74" + package: yarn: "=~1.22" } - } - setup: [ - "yarn global add netlify-cli@2.47.0", - ] - shell: { - path: "/bin/bash" + }, + op.#Exec & { + args: ["yarn", "global", "add", "netlify-cli@2.47.0"] + }, + op.#Exec & { args: [ + "/bin/bash", "--noprofile", "--norc", "-eo", "pipefail", "-c", + #code, ] - } - dir: "/src" - volume: "contents": { - dest: "/src" - from: contents - } - env: { - NETLIFY_SITE_NAME: name - if (create) { - NETLIFY_SITE_CREATE: "1" + env: { + NETLIFY_SITE_NAME: name + if (create) { + NETLIFY_SITE_CREATE: "1" + } + if customDomain != _|_ { + NETLIFY_DOMAIN: customDomain + } + NETLIFY_ACCOUNT: account.name + NETLIFY_AUTH_TOKEN: account.token } - if customDomain != _|_ { - NETLIFY_DOMAIN: customDomain - } - NETLIFY_ACCOUNT: account.name - NETLIFY_AUTH_TOKEN: account.token - } - export: { + dir: "/src" + mount: "/src": from: contents + }, + op.#Export & { source: "/output.json" format: "json" - } - } - - // FIXME: this is a hack to use docker.#Container while exporting - // values. - #up: #deploy.#up + }, + ] } diff --git a/stdlib/netlify/netlify.sh.cue b/stdlib/netlify/netlify.sh.cue index 604fa743..34d89ff1 100644 --- a/stdlib/netlify/netlify.sh.cue +++ b/stdlib/netlify/netlify.sh.cue @@ -1,6 +1,6 @@ package netlify -#Site: #deploy: command: #""" +#code: #""" create_site() { url="https://api.netlify.com/api/v1/${NETLIFY_ACCOUNT:-}/sites"