diff --git a/docs/reference/universe/README.md b/docs/reference/universe/README.md index baff9870..27fbc85b 100644 --- a/docs/reference/universe/README.md +++ b/docs/reference/universe/README.md @@ -11,7 +11,8 @@ - [aws/s3](./aws/s3.md) - AWS Simple Storage Service - [dagger](./dagger/README.md) - Dagger core types - [dagger/op](./dagger/op.md) - op: low-level operations for Dagger processing pipelines -- [docker](./docker.md) - Docker container operations +- [docker](./docker/README.md) - Docker container operations +- [docker/compose](./docker/compose.md) - Docker-compose operations - [gcp](./gcp/README.md) - Google Cloud Platform - [gcp/gcr](./gcp/gcr.md) - Google Container Registry - [gcp/gke](./gcp/gke.md) - Google Kubernetes Engine diff --git a/docs/reference/universe/docker.md b/docs/reference/universe/docker/README.md similarity index 96% rename from docs/reference/universe/docker.md rename to docs/reference/universe/docker/README.md index f146a5a4..6ec5f28c 100644 --- a/docs/reference/universe/docker.md +++ b/docs/reference/universe/docker/README.md @@ -33,6 +33,7 @@ A container image that can run any docker command | Name | Type | Description | | ------------- |:-------------: |:-------------: | |*command* | `string` |Command to execute | +|*registries* | `[]` |Image registries | ### docker.#Command Outputs @@ -99,6 +100,7 @@ _No output._ |*run.ssh.key* | `dagger.#Secret` |private key | |*run.command* | `"""\n # Run detach container\n OPTS=""\n \n if [ ! -z "$CONTAINER_NAME" ]; then\n \tOPTS="$OPTS --name $CONTAINER_NAME"\n fi\n \n docker container run -d $OPTS "$IMAGE_REF"\n """` |Command to execute | |*run.env.IMAGE_REF* | `string` |- | +|*run.registries* | `[]` |Image registries | ### docker.#Run Outputs diff --git a/docs/reference/universe/docker/compose.md b/docs/reference/universe/docker/compose.md new file mode 100644 index 00000000..3cec19a8 --- /dev/null +++ b/docs/reference/universe/docker/compose.md @@ -0,0 +1,38 @@ +--- +sidebar_label: compose +--- + +# alpha.dagger.io/docker/compose + +Docker-compose operations + +```cue +import "alpha.dagger.io/docker/compose" +``` + +## compose.#App + +### compose.#App Inputs + +| Name | Type | Description | +| ------------- |:-------------: |:-------------: | +|*registries* | `[]` |Image registries | +|*run.command* | `"""\n if [ -n "$DOCKER_HOSTNAME" ]; then\n \tssh -i /key -fNT -o "StreamLocalBindUnlink=yes" -L "$(pwd)"/docker.sock:/var/run/docker.sock -p "$DOCKER_PORT" "$DOCKER_USERNAME"@"$DOCKER_HOSTNAME"\n \texport DOCKER_HOST="unix://$(pwd)/docker.sock"\n fi\n \n # Extend session duration\n echo "Host *\\nServerAliveInterval 240" \>\> "$HOME"/.ssh/config\n chmod 600 "$HOME"/.ssh/config\n \n # Move compose\n if [ -d "$SOURCE_DIR" ]; then\n \tif [ -f docker-compose.yaml ]; then\n \t\tcp docker-compose.yaml "$SOURCE_DIR"/docker-compose.yaml\n \tfi\n \tcd "$SOURCE_DIR"\n fi\n \n docker-compose build\n docker-compose up -d\n """` |Command to execute | +|*run.package."docker-compose"* | `true` |- | +|*run.registries* | `[]` |Image registries | + +### compose.#App Outputs + +_No output._ + +## compose.#Client + +A container image to run the docker-compose client + +### compose.#Client Inputs + +_No input._ + +### compose.#Client Outputs + +_No output._ diff --git a/stdlib/.dagger/env/docker-compose/.gitignore b/stdlib/.dagger/env/docker-compose/.gitignore new file mode 100644 index 00000000..01ec19b0 --- /dev/null +++ b/stdlib/.dagger/env/docker-compose/.gitignore @@ -0,0 +1,2 @@ +# dagger state +state/** diff --git a/stdlib/.dagger/env/docker-compose/values.yaml b/stdlib/.dagger/env/docker-compose/values.yaml new file mode 100644 index 00000000..e05bf4f3 --- /dev/null +++ b/stdlib/.dagger/env/docker-compose/values.yaml @@ -0,0 +1,34 @@ +plan: + module: ./docker/compose + package: ./tests +name: docker-compose +inputs: + TestSSH.host: + text: 143.198.64.230 + TestSSH.key: + secret: ENC[AES256_GCM,data:8heZn3UqcB0aV9XAn9uLx9gBcTqbmfNX7voBpYCNCGaX1nustSzKhbR29dxAoETIdfSFPetX2s4cCYbPTqFc6KTyRvfdmI8tXvb5+lin/CkdQJy7cR+RiynLEfbs32EPQilaph+kyGyGBAWAme49g8U2om/QObxCSes+Zn8ihfv7lBkLEj8hen2OC7YwIqjs9V8ozNCJ0zsk+NIk5LkryXdWTaYvgGHOmO/NuWz411L/pF1HieOV1L4Fe6E5hsUun0kVny8GxXQSbVCKle9A9TxD2bs+IBVNAUZVTsrjZYyTjYlNNgTaOqPASS5VDrOtR8csrGJ6GyGxtGrtRdhaoXYQA1zfA7uMfSMNlCwk3VB70P60s0U9tKr3HnRFX6rQdKcUwR2a1zbJ8UmHPy9apsM+tc+m/CAb8dkdu6UyiwvBK+kyPDyBVz4rwyhmtiB7cjw9vXYWX3hbZcE0e3RR4Upqw5NkIIlLfNH7T1fPDmVmIlXQk0wKIZJ//dwdJZyrraA+RRtG5b3PECSojIN3x57LnHdIXfp5drHK,iv:xqGFk9QgC6YwqFODSLRwShf+SMyY4PmtfWt5neHwfSY=,tag:vVZtGty5ehLvYzd8H0+Xsw==,type:str] + TestSSH.user: + text: root + repo: + dir: + path: ./docker/compose/tests/testdata +sops: + kms: [] + gcp_kms: [] + azure_kv: [] + hc_vault: [] + age: + - recipient: age1gxwmtwahzwdmrskhf90ppwlnze30lgpm056kuesrxzeuyclrwvpsupwtpk + enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBMSmROdEtUblF3REhwS3hC + bU9ObTE0aWQzdGJzRlB5WjA0UG5nZFFJTEJjClljQWFYYWNQaFozZGRMcWxyT3p3 + Y0ZVUlZISUNlVkxVSVBqY0RRaGxPN3cKLS0tIGhsUTFCK2ljaWZFekVQMlRSRmtD + bEY3N3ZLTFpUNzZVWVBOK3VNRk9hWlUKd9db3j7FqFW4t7TxFyzudKDPTVqr66v2 + KqedhRYCjF4ZozN0H++xQPH24RBRnwc6Uq0Vm38UYv1ozDN2L/l5DA== + -----END AGE ENCRYPTED FILE----- + lastmodified: "2021-06-25T13:17:45Z" + mac: ENC[AES256_GCM,data:RclYzWUgBU06881KztfHdaeBtHLOiQZ5xNp0taaxS152rUxyXCXEAFlOu/yqE3RvEoorUp473wDwFUa9DudHidY88YNdfdk5AUuZdsOXW4bRMFPF4eiFugqZJNPepaW1YBDUMeH7XQBN1jyEkFOvzyk2KKQhoUshWyrU5QDR2kc=,iv:MjhUSjzVm6nV2PnSNi346GxkirmF3yAti3Jmb94gLGg=,tag:t4fHAYh60//PlkwUPQh2uA==,type:str] + pgp: [] + encrypted_suffix: secret + version: 3.7.1 diff --git a/stdlib/docker/command.cue b/stdlib/docker/command.cue index e2be5084..e51f1aec 100644 --- a/stdlib/docker/command.cue +++ b/stdlib/docker/command.cue @@ -62,6 +62,19 @@ import ( [string]: true | false | string @dagger(input) } + // Image registries + registries: [...{ + target?: string + username: string + secret: dagger.#Secret + }] @dagger(input) + + // Copy contents from other artifacts + copy: [string]: from: dagger.#Artifact + + // Write file in the container + files: [string]: string + // Setup docker client and then execute the user command #code: #""" # Setup ssh @@ -103,7 +116,8 @@ import ( #up: [ op.#Load & { from: alpine.#Image & { - package: { + "package": { + package bash: true "openssh-client": true "docker-cli": true @@ -111,6 +125,34 @@ import ( } }, + for registry in registries { + op.#Exec & { + args: ["/bin/bash", "-c", #""" + echo "$TARGER_HOST" | docker login --username "$DOCKER_USERNAME" --password-stdin "$(cat /password)" + """#, + ] + env: { + TARGET_HOST: registry.target + DOCKER_USERNAME: registry.username + } + mount: "/password": secret: registry.password + } + }, + + for dest, content in files { + op.#WriteFile & { + "content": content + "dest": dest + } + }, + + for dest, src in copy { + op.#Copy & { + from: src.from + "dest": dest + } + }, + if ssh.keyPassphrase != _|_ { op.#WriteFile & { content: #""" @@ -137,7 +179,7 @@ import ( op.#Exec & { always: true args: [ - "/bin/sh", + "/bin/bash", "--noprofile", "--norc", "-eo", @@ -146,7 +188,6 @@ import ( ] "env": { env - if ssh != _|_ { DOCKER_HOSTNAME: ssh.host DOCKER_USERNAME: ssh.user @@ -161,6 +202,9 @@ import ( } } "mount": { + if ssh == _|_ { + "/var/run/docker.sock": from: "docker.sock" + } if ssh != _|_ { if ssh.key != _|_ { "/key": secret: ssh.key diff --git a/stdlib/docker/compose/client.cue b/stdlib/docker/compose/client.cue new file mode 100644 index 00000000..504462cf --- /dev/null +++ b/stdlib/docker/compose/client.cue @@ -0,0 +1,16 @@ +package compose + +import ( + "alpha.dagger.io/alpine" +) + +// A container image to run the docker-compose client +#Client: alpine.#Image & { + package: { + bash: true + jq: true + curl: true + "openssh-client": true + "docker-compose": true + } +} diff --git a/stdlib/docker/compose/compose.cue b/stdlib/docker/compose/compose.cue new file mode 100644 index 00000000..bff531aa --- /dev/null +++ b/stdlib/docker/compose/compose.cue @@ -0,0 +1,82 @@ +// Docker-compose operations +package compose + +import ( + "strconv" + "alpha.dagger.io/dagger" + "alpha.dagger.io/docker" +) + +#App: { + ssh?: { + // ssh host + host: string @dagger(input) + + // ssh user + user: string @dagger(input) + + // ssh port + port: *22 | int @dagger(input) + + // private key + key: dagger.#Secret @dagger(input) + + // fingerprint + fingerprint?: string @dagger(input) + + // ssh key passphrase + keyPassphrase?: dagger.#Secret @dagger(input) + } + + // Accept either a contaxt, a docker-compose or both together + source?: dagger.#Artifact @dagger(input) + composeFile?: string @dagger(input) + + // Image registries + registries: [...{ + target?: string + username: string + secret: dagger.#Secret + }] @dagger(input) + + #code: #""" + if [ -n "$DOCKER_HOSTNAME" ]; then + ssh -i /key -fNT -o "StreamLocalBindUnlink=yes" -L "$(pwd)"/docker.sock:/var/run/docker.sock -p "$DOCKER_PORT" "$DOCKER_USERNAME"@"$DOCKER_HOSTNAME" + export DOCKER_HOST="unix://$(pwd)/docker.sock" + fi + + # Extend session duration + echo "Host *\nServerAliveInterval 240" >> "$HOME"/.ssh/config + chmod 600 "$HOME"/.ssh/config + + # Move compose + if [ -d "$SOURCE_DIR" ]; then + if [ -f docker-compose.yaml ]; then + cp docker-compose.yaml "$SOURCE_DIR"/docker-compose.yaml + fi + cd "$SOURCE_DIR" + fi + + docker-compose build + docker-compose up -d + """# + + run: docker.#Command & { + "ssh": ssh + command: #code + package: "docker-compose": true + "registries": registries + if source != _|_ { + copy: "/source": from: source + } + if composeFile != _|_ { + files: "/docker-compose.yaml": composeFile + } + env: { + COMPOSE_HTTP_TIMEOUT: strconv.FormatInt(200, 10) + if source != _|_ { + SOURCE_DIR: "source" + } + } + } +} diff --git a/stdlib/docker/compose/tests/cleanup.cue b/stdlib/docker/compose/tests/cleanup.cue new file mode 100644 index 00000000..58b8a59b --- /dev/null +++ b/stdlib/docker/compose/tests/cleanup.cue @@ -0,0 +1,89 @@ +package compose + +import ( + "strconv" + + "alpha.dagger.io/dagger" + "alpha.dagger.io/dagger/op" +) + +#CleanupCompose: { + // docker-compose up context + context: dagger.#Artifact + + ssh: { + // ssh host + host: string @dagger(input) + + // ssh user + user: string @dagger(input) + + // ssh port + port: *22 | int @dagger(input) + + // private key + key: dagger.#Secret @dagger(input) + + // fingerprint + fingerprint?: string @dagger(input) + + // ssh key passphrase + keyPassphrase?: dagger.#Secret @dagger(input) + } + + #code: #""" + # Export host + export DOCKER_HOST="unix://$(pwd)/docker.sock" + + # Start ssh agent + eval $(ssh-agent) > /dev/null + ssh-add /key > /dev/null + + ssh -i /key -o "StreamLocalBindUnlink=yes" -fNT -L "$(pwd)"/docker.sock:/var/run/docker.sock -p "$DOCKER_PORT" "$DOCKER_USERNAME"@"$DOCKER_HOSTNAME" || true + + # Down + if [ -d /source ]; then + cd /source + fi + + docker-compose down -v + """# + + #up: [ + op.#Load & {from: context}, + + op.#WriteFile & { + content: #code + dest: "/entrypoint.sh" + }, + + op.#Exec & { + always: true + args: [ + "/bin/sh", + "--noprofile", + "--norc", + "-eo", + "pipefail", + "/entrypoint.sh", + ] + env: { + DOCKER_HOSTNAME: ssh.host + DOCKER_USERNAME: ssh.user + DOCKER_PORT: strconv.FormatInt(ssh.port, 10) + if ssh.keyPassphrase != _|_ { + SSH_ASKPASS: "/get_passphrase" + DISPLAY: "1" + } + } + mount: { + if ssh.key != _|_ { + "/key": secret: ssh.key + } + if ssh.keyPassphrase != _|_ { + "/passphrase": secret: ssh.keyPassphrase + } + } + }, + ] +} diff --git a/stdlib/docker/compose/tests/compose.cue b/stdlib/docker/compose/tests/compose.cue new file mode 100644 index 00000000..32d4caf5 --- /dev/null +++ b/stdlib/docker/compose/tests/compose.cue @@ -0,0 +1,75 @@ +package compose + +import ( + "alpha.dagger.io/dagger" + "alpha.dagger.io/docker" +) + +repo: dagger.#Artifact @dagger(input) + +TestSSH: { + key: dagger.#Secret @dagger(input) + host: string @dagger(input) + user: string @dagger(input) +} + +TestCompose: { + up: #App & { + ssh: { + key: TestSSH.key + host: TestSSH.host + user: TestSSH.user + } + source: repo + } + + verify: docker.#Command & { + ssh: up.run.ssh + command: #""" + docker container ls | grep "api" | grep "Up" + """# + } + + cleanup: #CleanupCompose & { + context: up.run + ssh: verify.ssh + } +} + +TestInlineCompose: { + up: #App & { + ssh: { + key: TestSSH.key + host: TestSSH.host + user: TestSSH.user + } + source: repo + composeFile: #""" + version: "3" + + services: + api-mix: + build: . + environment: + PORT: 7000 + ports: + - 7000:7000 + + networks: + default: + name: mix-context + """# + } + + verify: docker.#Command & { + ssh: up.run.ssh + command: #""" + docker container ls | grep "api-mix" | grep "Up" + """# + } + + cleanup: #CleanupCompose & { + context: up.run + ssh: verify.ssh + } +} diff --git a/stdlib/docker/compose/tests/testdata/Dockerfile b/stdlib/docker/compose/tests/testdata/Dockerfile new file mode 100644 index 00000000..25d4c068 --- /dev/null +++ b/stdlib/docker/compose/tests/testdata/Dockerfile @@ -0,0 +1,15 @@ +FROM node:12-alpine + +WORKDIR /app + +COPY package.json package.json + +RUN npm install + +COPY . . + +ENV PORT=8080 + +EXPOSE 8080 + +CMD ["npm", "start"] \ No newline at end of file diff --git a/stdlib/docker/compose/tests/testdata/docker-compose.yaml b/stdlib/docker/compose/tests/testdata/docker-compose.yaml new file mode 100644 index 00000000..908bc744 --- /dev/null +++ b/stdlib/docker/compose/tests/testdata/docker-compose.yaml @@ -0,0 +1,7 @@ +version: "3" + +services: + api: + build: "" + ports: + - "8080:8080" \ No newline at end of file diff --git a/stdlib/docker/compose/tests/testdata/index.ts b/stdlib/docker/compose/tests/testdata/index.ts new file mode 100644 index 00000000..d37fe572 --- /dev/null +++ b/stdlib/docker/compose/tests/testdata/index.ts @@ -0,0 +1,13 @@ +import express from "express"; +import { get } from "env-var"; + + +const app = express(); + +const port: number = get('PORT').required().asPortNumber(); + +app.get('/ping', (req, res) => { + res.status(200).send('pong') +}); + +app.listen(port, '0.0.0.0', () => console.log("Server listen on http://localhost:" + port)); \ No newline at end of file diff --git a/stdlib/docker/compose/tests/testdata/package.json b/stdlib/docker/compose/tests/testdata/package.json new file mode 100644 index 00000000..c2be41f2 --- /dev/null +++ b/stdlib/docker/compose/tests/testdata/package.json @@ -0,0 +1,22 @@ +{ + "name": "test", + "version": "1.0.0", + "description": "A simple api", + "main": "index.ts", + "scripts": { + "build": "tsc", + "start": "ts-node index.ts", + "test": "test" + }, + "author": "Tom Chauveau", + "license": "ISC", + "devDependencies": { + "ts-node": "^8.9.1", + "typescript": "^3.8.3" + }, + "dependencies": { + "@types/express": "^4.17.6", + "env-var": "^6.1.1", + "express": "^4.17.1" + } +} diff --git a/stdlib/docker/compose/tests/testdata/tsconfig.json b/stdlib/docker/compose/tests/testdata/tsconfig.json new file mode 100644 index 00000000..98907427 --- /dev/null +++ b/stdlib/docker/compose/tests/testdata/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "sourceMap": true, + "outDir": "dist", + "strict": true, + "lib": [ + "esnext", + "dom" + ], + "esModuleInterop": true + } +} diff --git a/stdlib/universe.bats b/stdlib/universe.bats index bb3de2ad..7d51ebb1 100644 --- a/stdlib/universe.bats +++ b/stdlib/universe.bats @@ -70,6 +70,10 @@ setup() { assert_failure } +@test "docker compose" { + dagger -e docker-compose up +} + @test "docker run: ssh" { dagger -e docker-run-ssh up }