This commit is contained in:
Morten Olsen
2022-04-03 22:15:45 +02:00
parent 56235d8f5e
commit cc7b7c6849
105 changed files with 15694 additions and 6 deletions

104
cue.mod/pkg/universe.dagger.io/docker/build.cue generated Executable file
View File

@@ -0,0 +1,104 @@
package docker
import (
"dagger.io/dagger"
"dagger.io/dagger/core"
)
// Modular build API for Docker containers
#Build: {
steps: [#Step, ...#Step]
output: #Image
// Generate build DAG from linear steps
_dag: {
for idx, step in steps {
"\(idx)": step & {
// connect input to previous output
if idx > 0 {
// FIXME: the intermediary `output` is needed because of a possible CUE bug.
// `._dag."0".output: 1 errors in empty disjunction::`
// See: https://github.com/cue-lang/cue/issues/1446
// input: _dag["\(idx-1)"].output
_output: _dag["\(idx-1)"].output
input: _output
}
}
}
}
if len(_dag) > 0 {
output: _dag["\(len(_dag)-1)"].output
}
}
// A build step is anything that produces a docker image
#Step: {
input?: #Image
output: #Image
...
}
// Build step that copies files into the container image
#Copy: {
input: #Image
contents: dagger.#FS
source: string | *"/"
dest: string | *"/"
// Execute copy operation
_copy: core.#Copy & {
"input": input.rootfs
"contents": contents
"source": source
"dest": dest
}
output: #Image & {
config: input.config
rootfs: _copy.output
}
}
// Build step that executes a Dockerfile
#Dockerfile: {
source: dagger.#FS
// Dockerfile definition or path into source
dockerfile: *{
path: string | *"Dockerfile"
} | {
contents: string
}
// Registry authentication
// Key must be registry address
auth: [registry=string]: {
username: string
secret: dagger.#Secret
}
platforms: [...string]
target?: string
buildArg: [string]: string
label: [string]: string
hosts: [string]: string
_build: core.#Dockerfile & {
"source": source
"auth": auth
"dockerfile": dockerfile
"platforms": platforms
if target != _|_ {
"target": target
}
"buildArg": buildArg
"label": label
"hosts": hosts
}
output: #Image & {
rootfs: _build.output
config: _build.config
}
}

View File

@@ -0,0 +1,92 @@
package cli
import (
"dagger.io/dagger"
"universe.dagger.io/docker"
)
// See https://github.com/dagger/dagger/discussions/1874
// Default image
#Image: docker.#Pull & {
source: "docker:20.10.13-alpine3.15"
}
// Run a docker CLI command
#Run: {
#RunSocket | #RunSSH | #RunTCP
_defaultImage: #Image
// As a convenience, input defaults to a ready-to-use docker environment
input: docker.#Image | *_defaultImage.output
}
// Connect via local docker socket
#RunSocket: {
host: dagger.#Socket
docker.#Run & {
mounts: docker: {
dest: "/var/run/docker.sock"
contents: host
}
}
}
// Connect via SSH
#RunSSH: {
host: =~"^ssh://.+"
ssh: {
// Private SSH key
key?: dagger.#Secret
// Known hosts file contents
knownHosts?: dagger.#Secret
// FIXME: implement keyPassphrase
}
docker.#Run & {
env: DOCKER_HOST: host
if ssh.key != _|_ {
mounts: ssh_key: {
dest: "/root/.ssh/id_rsa"
contents: ssh.key
}
}
if ssh.knownHosts != _|_ {
mounts: ssh_hosts: {
dest: "/root/.ssh/known_hosts"
contents: ssh.knownHosts
}
}
}
}
// Connect via HTTP/HTTPS
#RunTCP: {
host: =~"^tcp://.+"
docker.#Run & {
env: DOCKER_HOST: host
// Directory with certificates to verify ({ca,cert,key}.pem files).
// This enables HTTPS.
certs?: dagger.#FS
if certs != _|_ {
env: {
DOCKER_TLS_VERIFY: "1"
DOCKER_CERT_PATH: "/certs/client"
}
mounts: "certs": {
dest: "/certs/client"
contents: certs
}
}
}
}

View File

@@ -0,0 +1,38 @@
package cli
import (
"dagger.io/dagger/core"
"universe.dagger.io/docker"
)
// Load an image into a docker daemon
#Load: {
// Image to load
image: docker.#Image
// Name and optionally a tag in the 'name:tag' format
tag: docker.#Ref
// Exported image ID
imageID: _export.imageID
// Root filesystem with exported file
result: _export.output
_export: core.#Export & {
"tag": tag
input: image.rootfs
config: image.config
}
#Run & {
mounts: src: {
dest: "/src"
contents: _export.output
}
command: {
name: "load"
flags: "-i": "/src/image.tar"
}
}
}

View File

@@ -0,0 +1,55 @@
package test
import (
"dagger.io/dagger"
"universe.dagger.io/alpine"
"universe.dagger.io/bash"
"universe.dagger.io/docker"
"universe.dagger.io/docker/cli"
)
dagger.#Plan & {
client: network: "unix:///var/run/docker.sock": connect: dagger.#Socket
actions: test: {
_cli: alpine.#Build & {
packages: {
bash: {}
"docker-cli": {}
}
}
_image: docker.#Run & {
input: _cli.output
command: {
name: "touch"
args: ["/foo.bar"]
}
}
load: cli.#Load & {
image: _image.output
host: client.network."unix:///var/run/docker.sock".connect
tag: "dagger:load"
}
verify: bash.#Run & {
input: _cli.output
mounts: docker: {
contents: client.network."unix:///var/run/docker.sock".connect
dest: "/var/run/docker.sock"
}
env: {
IMAGE_NAME: load.tag
IMAGE_ID: load.imageID
// FIXME: without this forced dependency, load.command might not run
DEP: "\(load.success)"
}
script: contents: #"""
test "$(docker image inspect $IMAGE_NAME -f '{{.Id}}')" = "$IMAGE_ID"
docker run --rm $IMAGE_NAME stat /foo.bar
"""#
}
}
}

View File

@@ -0,0 +1,48 @@
package test
import (
"dagger.io/dagger"
"universe.dagger.io/alpine"
"universe.dagger.io/docker"
"universe.dagger.io/docker/cli"
)
dagger.#Plan & {
client: network: "unix:///var/run/docker.sock": connect: dagger.#Socket
actions: test: {
run: cli.#Run & {
host: client.network."unix:///var/run/docker.sock".connect
command: name: "info"
}
differentImage: {
_cli: docker.#Build & {
steps: [
alpine.#Build & {
packages: "docker-cli": {}
},
docker.#Run & {
command: {
name: "sh"
flags: "-c": "echo -n foobar > /test.txt"
}
},
]
}
run: cli.#Run & {
input: _cli.output
host: client.network."unix:///var/run/docker.sock".connect
command: {
name: "docker"
args: ["info"]
}
export: files: "/test.txt": "foobar"
}
}
// FIXME: test remote connections with `docker:dind` image
// when we have long running tasks
}
}

View File

@@ -0,0 +1,10 @@
setup() {
load '../../../bats_helpers'
common_setup
}
@test "docker/cli" {
dagger "do" -p ./run.cue test
dagger "do" -p ./load.cue test
}

View File

@@ -0,0 +1,23 @@
package docker
import (
"dagger.io/dagger"
)
// A container image
#Image: {
// Root filesystem of the image.
rootfs: dagger.#FS
// Image config
config: dagger.#ImageConfig
}
// A ref is an address for a remote container image
// Examples:
// - "index.docker.io/dagger"
// - "dagger"
// - "index.docker.io/dagger:latest"
// - "index.docker.io/dagger:latest@sha256:a89cb097693dd354de598d279c304a1c73ee550fbfff6d9ee515568e0c749cfe"
// FIXME: add formatting constraints
#Ref: dagger.#Ref

35
cue.mod/pkg/universe.dagger.io/docker/pull.cue generated Executable file
View File

@@ -0,0 +1,35 @@
// Build, ship and run Docker containers in Dagger
package docker
import (
"dagger.io/dagger"
"dagger.io/dagger/core"
)
// Download an image from a remote registry
#Pull: {
// Source ref.
source: #Ref
// Registry authentication
auth?: {
username: string
secret: dagger.#Secret
}
_op: core.#Pull & {
"source": source
if auth != _|_ {
"auth": auth
}
}
// Downloaded image
image: #Image & {
rootfs: _op.output
config: _op.config
}
// FIXME: compat with Build API
output: image
}

33
cue.mod/pkg/universe.dagger.io/docker/push.cue generated Executable file
View File

@@ -0,0 +1,33 @@
package docker
import (
"dagger.io/dagger"
"dagger.io/dagger/core"
)
// Upload an image to a remote repository
#Push: {
// Destination ref
dest: #Ref
// Complete ref after pushing (including digest)
result: #Ref & _push.result
// Registry authentication
auth?: {
username: string
secret: dagger.#Secret
}
// Image to push
image: #Image
_push: core.#Push & {
"dest": dest
if auth != _|_ {
"auth": auth
}
input: image.rootfs
config: image.config
}
}

183
cue.mod/pkg/universe.dagger.io/docker/run.cue generated Executable file
View File

@@ -0,0 +1,183 @@
package docker
import (
"list"
"dagger.io/dagger"
"dagger.io/dagger/core"
)
// Run a command in a container
#Run: {
// Docker image to execute
input: #Image
always: bool | *false
// Filesystem mounts
mounts: [name=string]: core.#Mount
// Expose network ports
// FIXME: investigate feasibility
ports: [name=string]: {
frontend: dagger.#Socket
backend: {
protocol: *"tcp" | "udp"
address: string
}
}
// Entrypoint to prepend to command
entrypoint?: [...string]
// Command to execute
command?: {
// Name of the command to execute
// Examples: "ls", "/bin/bash"
name: string
// Positional arguments to the command
// Examples: ["/tmp"]
args: [...string]
// Command-line flags represented in a civilized form
// Example: {"-l": true, "-c": "echo hello world"}
flags: [string]: (string | true)
_flatFlags: list.FlattenN([
for k, v in flags {
if (v & bool) != _|_ {
[k]
}
if (v & string) != _|_ {
[k, v]
}
},
], 1)
}
// Environment variables
// Example: {"DEBUG": "1"}
env: [string]: string | dagger.#Secret
// Working directory for the command
// Example: "/src"
workdir: string
// Username or UID to ad
// User identity for this command
// Examples: "root", "0", "1002"
user: string
// Add defaults to image config
// This ensures these values are present
_defaults: core.#Set & {
"input": {
entrypoint: []
cmd: []
workdir: "/"
user: "root"
}
config: input.config
}
// Override with user config
_config: core.#Set & {
input: _defaults.output
config: {
if entrypoint != _|_ {
"entrypoint": entrypoint
}
if command != _|_ {
cmd: [command.name] + command._flatFlags + command.args
}
if workdir != _|_ {
"workdir": workdir
}
if user != _|_ {
"user": user
}
}
}
// Output fields
{
// Has the command completed?
completed: bool & (_exec.exit != _|_)
// Was completion successful?
success: bool & (_exec.exit == 0)
// Details on error, if any
error: {
// Error code
code: _exec.exit
// Error message
message: string | *null
}
export: {
rootfs: dagger.#FS & _exec.output
files: [path=string]: string
_files: {
for path, _ in files {
"\(path)": {
contents: string & _read.contents
_read: core.#ReadFile & {
input: _exec.output
"path": path
}
}
}
}
for path, output in _files {
files: "\(path)": output.contents
}
directories: [path=string]: dagger.#FS
_directories: {
for path, _ in directories {
"\(path)": {
contents: dagger.#FS & _subdir.output
_subdir: core.#Subdir & {
input: _exec.output
"path": path
}
}
}
}
for path, output in _directories {
directories: "\(path)": output.contents
}
}
}
// For compatibility with #Build
output: #Image & {
rootfs: _exec.output
config: input.config
}
// Actually execute the command
_exec: core.#Exec & {
"input": input.rootfs
"always": always
"mounts": mounts
args: _config.output.entrypoint + _config.output.cmd
workdir: _config.output.workdir
user: _config.output.user
"env": env
// env may contain secrets so we can't use core.#Set
if input.config.env != _|_ {
for key, val in input.config.env {
if env[key] == _|_ {
env: "\(key)": val
}
}
}
}
// Command exit code
exit: _exec.exit
}

26
cue.mod/pkg/universe.dagger.io/docker/set.cue generated Executable file
View File

@@ -0,0 +1,26 @@
package docker
import (
"dagger.io/dagger"
"dagger.io/dagger/core"
)
// Change image config
#Set: {
// The source image
input: #Image
// The image config to change
config: dagger.#ImageConfig
_set: core.#Set & {
"input": input.config
"config": config
}
// Resulting image with the config changes
output: #Image & {
rootfs: input.rootfs
config: _set.output
}
}

View File

@@ -0,0 +1,120 @@
package docker
import (
"dagger.io/dagger"
"dagger.io/dagger/core"
"universe.dagger.io/alpine"
"universe.dagger.io/docker"
)
dagger.#Plan & {
actions: test: build: {
// Test: simple docker.#Build
simple: {
#testValue: "hello world"
image: docker.#Build & {
steps: [
alpine.#Build,
docker.#Run & {
command: {
name: "sh"
flags: "-c": "echo -n $TEST >> /test.txt"
}
env: TEST: #testValue
},
]
}
verify: core.#ReadFile & {
input: image.output.rootfs
path: "/test.txt"
}
verify: contents: #testValue
}
// Test: docker.#Build with multiple steps
multiSteps: {
image: docker.#Build & {
steps: [
alpine.#Build,
docker.#Run & {
command: {
name: "sh"
flags: "-c": "echo -n hello > /bar.txt"
}
},
docker.#Run & {
command: {
name: "sh"
flags: "-c": "echo -n $(cat /bar.txt) world > /foo.txt"
}
},
docker.#Run & {
command: {
name: "sh"
flags: "-c": "echo -n $(cat /foo.txt) >> /test.txt"
}
},
]
}
verify: core.#ReadFile & {
input: image.output.rootfs
path: "/test.txt"
}
verify: contents: "hello world"
}
// Test: simple nesting of docker.#Build
nested: {
build: docker.#Build & {
steps: [
docker.#Build & {
steps: [
docker.#Pull & {
source: "alpine"
},
docker.#Run & {
command: name: "ls"
},
]
},
docker.#Run & {
command: name: "ls"
},
]
}
}
// Test: nested docker.#Build with 3+ levels of depth
// FIXME: this test currently fails.
nestedDeep: {
// build: docker.#Build & {
// steps: [
// docker.#Build & {
// steps: [
// docker.#Build & {
// steps: [
// docker.#Pull & {
// source: "alpine"
// },
// docker.#Run & {
// command: name: "ls"
// },
// ]
// },
// docker.#Run & {
// command: name: "ls"
// },
// ]
// },
// docker.#Run & {
// command: name: "ls"
// },
// ]
// }
}
}
}

View File

@@ -0,0 +1,70 @@
package docker
import (
"dagger.io/dagger"
"dagger.io/dagger/core"
"universe.dagger.io/docker"
)
dagger.#Plan & {
client: filesystem: "./testdata": read: contents: dagger.#FS
actions: test: dockerfile: {
simple: {
build: docker.#Build & {
steps: [
docker.#Dockerfile & {
source: dagger.#Scratch
dockerfile: contents: """
FROM alpine:3.15
RUN echo -n hello world >> /test.txt
"""
},
docker.#Run & {
command: {
name: "/bin/sh"
args: ["-c", """
# Verify that docker.#Dockerfile correctly connect output
# into other steps
grep -q "hello world" /test.txt
"""]
}
},
]
}
verify: core.#ReadFile & {
input: build.output.rootfs
path: "/test.txt"
} & {
contents: "hello world"
}
}
withInput: {
build: docker.#Build & {
steps: [
docker.#Dockerfile & {
source: client.filesystem."./testdata".read.contents
},
docker.#Run & {
command: {
name: "/bin/sh"
args: ["-c", """
hello >> /test.txt
"""]
}
},
]
}
verify: core.#ReadFile & {
input: build.output.rootfs
path: "/test.txt"
} & {
contents: "hello world"
}
}
}
}

View File

@@ -0,0 +1,121 @@
package docker
import (
"dagger.io/dagger"
"dagger.io/dagger/core"
"universe.dagger.io/docker"
)
dagger.#Plan & {
actions: test: image: {
// Test: change image config with docker.#Set
set: {
image: output: docker.#Image & {
rootfs: dagger.#Scratch
config: {
cmd: ["/bin/sh"]
env: PATH: "/sbin:/bin"
onbuild: ["COPY . /app"]
}
}
set: docker.#Set & {
input: image.output
config: {
env: FOO: "bar"
workdir: "/root"
onbuild: ["RUN /app/build.sh"]
}
}
verify: set.output.config & {
env: {
PATH: "/sbin:/bin"
FOO: "bar"
}
cmd: ["/bin/sh"]
workdir: "/root"
onbuild: [
"COPY . /app",
"RUN /app/build.sh",
]
}
}
// Test: image config behavior is correct
config: {
build: core.#Dockerfile & {
source: dagger.#Scratch
dockerfile: contents: """
FROM alpine:3.15.0
RUN echo -n 'not hello from dagger' > /dagger.txt
RUN echo '#!/bin/sh' > /bin/dagger
ENV HELLO_FROM=dagger
RUN echo 'echo -n "hello from $HELLO_FROM" > /dagger.txt' >> /bin/dagger
RUN chmod +x /bin/dagger
WORKDIR /bin
CMD /bin/dagger
"""
}
myimage: docker.#Image & {
rootfs: build.output
config: build.config
}
run: docker.#Run & {
input: myimage
command: name: "ls"
export: files: {
"/dagger.txt": _ & {
contents: "not hello from dagger"
}
"/bin/dagger": _ & {
contents: """
#!/bin/sh
echo -n "hello from $HELLO_FROM" > /dagger.txt
"""
}
}
}
verify_cmd_is_run: docker.#Run & {
input: myimage
export: files: "/dagger.txt": _ & {
contents: "hello from dagger"
}
}
verify_env_is_overridden: docker.#Run & {
input: myimage
export: files: "/dagger.txt": _ & {
contents: "hello from europa"
}
env: HELLO_FROM: "europa"
}
verify_working_directory: docker.#Run & {
input: myimage
command: {
name: "sh"
flags: "-c": #"""
pwd > dir.txt
"""#
}
export: files: "/bin/dir.txt": _ & {
contents: "/bin\n"
}
}
verify_working_directory_is_overridden: docker.#Run & {
input: myimage
workdir: "/"
command: {
name: "sh"
flags: "-c": #"""
pwd > dir.txt
"""#
}
export: files: "/dir.txt": _ & {
contents: "/\n"
}
}
}
}
}

View File

@@ -0,0 +1,108 @@
package docker
import (
"dagger.io/dagger"
"dagger.io/dagger/core"
"universe.dagger.io/docker"
"universe.dagger.io/alpine"
)
dagger.#Plan & {
actions: test: run: {
_build: alpine.#Build & {
packages: bash: _
}
_image: _build.output
// Test: run a simple shell command
simpleShell: {
run: docker.#Run & {
input: _image
command: {
name: "/bin/sh"
args: ["-c", "echo -n hello world >> /output.txt"]
}
}
verify: core.#ReadFile & {
input: run.output.rootfs
path: "/output.txt"
}
verify: contents: "hello world"
}
// Test: export a file
exportFile: {
run: docker.#Run & {
input: _image
command: {
name: "sh"
flags: "-c": #"""
echo -n hello world >> /output.txt
"""#
}
export: files: "/output.txt": string & "hello world"
}
}
// Test: export a directory
exportDirectory: {
run: docker.#Run & {
input: _image
command: {
name: "sh"
flags: "-c": #"""
mkdir -p /test
echo -n hello world >> /test/output.txt
"""#
}
export: directories: "/test": _
}
verify: core.#ReadFile & {
input: run.export.directories."/test"
path: "/output.txt"
}
verify: contents: "hello world"
}
// Test: configs overriding image defaults
configs: {
_base: docker.#Set & {
input: _image
config: {
user: "nobody"
workdir: "/sbin"
entrypoint: ["sh"]
cmd: ["-c", "echo -n $0 $PWD $(whoami) > /tmp/output.txt"]
}
}
// check defaults not overriden by image config
runDefaults: docker.#Run & {
input: _image
command: {
name: "sh"
flags: "-c": "echo -n $PWD $(whoami) > /output.txt"
}
export: files: "/output.txt": "/ root"
}
// check image defaults
imageDefaults: docker.#Run & {
input: _base.output
export: files: "/tmp/output.txt": "sh /sbin nobody"
}
// check overrides by user
overrides: docker.#Run & {
input: _base.output
entrypoint: ["bash"]
workdir: "/root"
user: "root"
export: files: "/tmp/output.txt": "bash /root root"
}
}
}
}

View File

@@ -0,0 +1,12 @@
setup() {
load '../../bats_helpers'
common_setup
}
@test "docker" {
dagger "do" -p ./build.cue test
dagger "do" -p ./dockerfile.cue test
dagger "do" -p ./run.cue test
dagger "do" -p ./image.cue test
}