Europa: integrate core packages, separate universe

Signed-off-by: Solomon Hykes <solomon@dagger.io>
This commit is contained in:
Solomon Hykes
2021-12-14 00:16:12 +00:00
parent dd4c360a7b
commit c1c585bcd5
57 changed files with 612 additions and 142 deletions

76
europa-universe/README.md Normal file
View File

@@ -0,0 +1,76 @@
# Europa Universe
## About this directory
`europa-universe/` is a staging area for the upcoming `universe.dagger.io` package namespace,
which will be shipped as part of the [Europa release](https://github.com/dagger/dagger/issues/1088).
## What is Universe?
The Dagger Universe is a catalog of reusable Cue packages, curated by Dagger but possibly authored by third parties. Most packages in Universe contain reusable actions; some may also contain entire configuration templates.
The import domain for Universe will be `universe.dagger.io`. It will deprecate the current domain `alpha.dagger.io`.
## Where is the `dagger` package?
Europa will also introduce a new package for the Dagger Core API: `dagger.io/dagger`.
This is a core package, and is *not* part of Universe (note the import domain).
The development version of the Europa core API can be imported as [alpha.dagger.io/europa/dagger](../stdlib/europa/dagger).
## Where is the `dagger/engine` package?
Europa will also introduce a new package for the Low-Level Dagger Engine API : `dagger.io/dagger/engine`.
This is a core package, and is *not* part of Universe (note the import domain).
The development version of the Europa Low-Level Engine API can be imported as either:
* [alpha.dagger.io/europa/dagger/engine/spec/engine](../stdlib/europa/dagger/engine/spec/engine) for the full spec
* [alpha.dagger.io/dagger/europa/engine](../stdlib/europa/dagger/engine) for the implemented subset of the spec
## Universe vs other packages
This table compares Dagger core packages, Dagger Universe packages, and the overall CUE package ecosystem.
| | *Dagger core* | *Dagger Universe* | *CUE ecosystem* |
|----------|--------------|------|
| Import path | `dagger.io` | `universe.dagger.io` | Everything else |
| Purpose | Access core Dagger features | Safely reuse code from the Dagger community | Reuse any CUE code from anyone |
| Author | Dagger team | Dagger community, curated by Dagger | Anyone |
| Release cycle | Released with Dagger engine | Released continuously | No release cycle |
| Size | Small | Large | Very large |
| Growth rate | Grows slowly, with engine features | Grows fast, with Dagger community | Grows even faster, with CUE ecosystem |
## Notable packages
### Docker API
*Import path: [`universe.dagger.io/docker`](./universe/docker)*
The `docker` package is a native Cue API for Docker. You can use it to build, run, push and pull Docker containers directly from Cue.
The Dagger container API defines the following types:
* `#Image`: a container image
* `#Run`: run a comand in a container
* `#Push`: upload an image to a repository
* `#Pull`: download an image from a repository
* `#Build`: build an image
### Examples
*Import path: [`universe.dagger.io/examples`](./examples)*
This package contains examples of complete Dagger configurations, including the result of following tutorials in the documentations.
For example, [the todoapp example](./examples/todoapp) corresponds to the [Getting Started tutorial](https://docs.dagger.io/1003/get-started/)
## TODO LIST
* Support native language dev in `docker.#Run` with good DX (Python, Go, Typescript etc.)
* Coding style. When to use verbs vs. nouns?
* Easy file injection API (`container.#Image.files` ?)
* Use file injection instead of inline for `#Command.script` (to avoid hitting arg character limits)
* Organize universe packages in sub-categories?

View File

@@ -0,0 +1,36 @@
// Base package for Alpine Linux
package alpine
import (
"universe.dagger.io/docker"
)
// Default Alpine version
let defaultVersion = "3.13.5@sha256:69e70a79f2d41ab5d637de98c1e0b055206ba40a8145e7bddb55ccc04e13cf8f"
// Build an Alpine Linux container image
#Build: {
// Alpine version to install
version: string | *defaultVersion
// List of packages to install
packages: [pkgName=string]: version: string | *""
docker.#Build & {
steps: [
docker.#Pull & {
source: "index.docker.io/alpine:\(version)"
},
for pkgName, pkg in packages {
run: cmd: {
name: "apk"
args: ["add", "\(pkgName)\(version)"]
flags: {
"-U": true
"--no-cache": true
}
}
},
]
}
}

View File

@@ -0,0 +1,38 @@
package alpine
import (
"universe.dagger.io/docker"
)
TestImageVersion: {
build: #Build & {
// install an old version on purpose
version: "3.10.9"
}
check: docker.#Run & {
image: build.output
output: files: "/etc/alpine-release": contents: "3.10.9"
}
}
TestPackageInstall: {
build: #Build & {
packages: {
jq: {}
curl: {}
}
}
check: docker.#Run & {
script: """
jq --version > /jq-version.txt
curl --version > /curl-version.txt
"""
output: files: {
"/jq-version.txt": contents: "FIXME"
"/curl-version.txt": contents: "FIXME"
}
}
}

View File

@@ -0,0 +1,21 @@
// Helpers to run bash commands in containers
package bash
import (
"universe.dagger.io/docker"
)
// Run a bash command or script in a container
#Run: docker.#Run & {
script: string
cmd: {
name: "bash"
flags: {
"-c": script
"--noprofile": true
"--norc": true
"-e": true
"-o": "pipefail"
}
}
}

View File

@@ -0,0 +1 @@
module: "universe.dagger.io"

View File

@@ -0,0 +1 @@
../../../stdlib

View File

@@ -0,0 +1 @@
../../../stdlib/europa

View File

@@ -0,0 +1,97 @@
package docker
import (
"dagger.io/dagger"
"dagger.io/dagger/engine/spec/engine"
)
// Modular build API for Docker containers
#Build: {
steps: [#Step, ...#Step]
output: #Image
// Generate build DAG from linerar steps
dag: {
for idx, step in steps {
// As a special case, wrap #Run into a valid step
if step.run != _|_ {
"\(idx)": {
input: _
run: step & {
image: input
output: rootfs: _
}
output: {
config: input.config
rootfs: run.output.rootfs
}
}
}
// Otherwise, just use the step as is
if step.run == _|_ {
"\(idx)": {
run: false
step
}
}
// Either way, connect input to previous output
if idx > 0 {
"\(idx)": input: dag["\(idx-1)"].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
...
} | #Run
// Build step that copies files into the container image
#Copy: {
input: #Image
contents: dagger.#FS
source: string | *"/"
dest: string | *"/"
// Execute copy operation
copy: engine.#Copy & {
"input": input.rootfs
"source": {
root: contents
path: source
}
dest: copy.dest
}
output: #Image & {
config: input.config
rootfs: copy.output
}
}
// Build step that executes a Dockerfile
#Dockerfile: {
// Source directory
source: dagger.#FS
// FIXME: not yet implemented
*{
// Look for Dockerfile in source at default path
path: "Dockerfile"
} | {
// Look for Dockerfile in source at a custom path
path: string
} | {
// Custom dockerfile contents
contents: string
}
}

View File

@@ -0,0 +1,205 @@
// Build, ship and run Docker containers in Dagger
package docker
import (
"list"
"dagger.io/dagger/engine/spec/engine"
"dagger.io/dagger"
)
// A container image
#Image: {
// Root filesystem of the image.
rootfs: dagger.#FS
// Image config
config: engine.#ImageConfig
}
// Run a command in a container
#Run: {
run: true // FIXME
image: #Image
always: bool | *false
// Filesystem mounts
mounts: [name=string]: engine.#Mount
// Expose network ports
ports: [name=string]: {
frontend: dagger.#Service
backend: {
protocol: *"tcp" | "udp"
address: string
}
}
// Command to execute
cmd: {
// 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)
}
// Optionally pass a script to interpret
// Example: "echo hello\necho world"
script?: string
if script != _|_ {
// Default interpreter is /bin/sh -c
cmd: *{
name: "/bin/sh"
flags: "-c": script
} | {}
}
// Environment variables
// Example: {"DEBUG": "1"}
env: [string]: string
// 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
// Optionally attach to command standard streams
stdin: dagger.#Stream | *null
stdout: dagger.#Stream | *null
stderr: dagger.#Stream | *null
// 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
}
output: {
rootfs?: dagger.#FS & _exec.output
files: [path=string]: {
contents: string
contents: _read.contents
_read: engine.#ReadFile & {
input: _exec.output
"path": path
}
}
directories: [path=string]: {
contents: dagger.#FS
contents: (dagger.#Subdir & {
input: _exec.output
"path": path
}).output
}
}
}
// Actually execute the command
_exec: engine.#Exec & {
args: [cmd.name] + cmd._flatFlags + cmd.args
input: image.rootfs
"mounts": [ for mnt in mounts {mnt}]
environ: [ for k, v in env {"\(k)=\(v)"}]
"workdir": workdir
"stdin": stdin
// FIXME: user
}
}
// 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"
#Ref: engine.#Ref
// Download an image from a remote registry
#Pull: {
// Source ref.
source: #Ref
// Registry authentication
// Key must be registry address, for example "index.docker.io"
auth: [registry=string]: {
username: string
secret: dagger.#Secret
}
_op: engine.#Pull & {
"source": source
"auth": [ for target, creds in auth {
"target": target
creds
}]
}
// Downloaded image
image: #Image & {
rootfs: _op.output
config: _op.config
}
// FIXME: compat with Build API
output: image
}
// Upload an image to a remote repository
#Push: {
// Destination ref
dest: #Ref
// Complete ref after pushing (including digest)
result: #Ref & _push.result
// Registry authentication
// Key must be registry address
auth: [registry=string]: {
username: string
secret: dagger.#Secret
}
// Image to push
image: #Image
_push: engine.#Push & {
dest: dest
input: image.rootfs
config: image.config
}
}

View File

@@ -0,0 +1,15 @@
package docker
cmd: #Command & {
script: "echo hello world"
exec: {
name: "/bin/bash"
flags: {
"-c": script
"--noprofile": true
"--norc": true
"-e": true
"-o": "pipefail"
}
}
}

View File

@@ -0,0 +1,82 @@
package docker
import (
"dagger.io/dagger"
"universe.dagger.io/nginx"
)
build0: #Build & {
steps: [
{
output: #Image & {
config: user: "foo"
}
},
]
}
// Inventory of real-world build use cases
//
// 1. Build netlify image
// - import alpine base
// - execute 'yarn add netlify'
// 2. Build todoapp dev image
// - import nginx base
// - copy app directory into /usr/share/nginx/html
build2: {
source: dagger.#FS
build: #Build & {
steps: [
nginx.#Build & {
flavor: "alpine"
},
{
// Custom step to watermark the image
input: _
output: input & {
config: user: "42"
}
},
#Copy & {
contents: source
dest: "/usr/share/nginx/html"
},
]
}
img: build.output
// Assert:
img: config: user: "42"
}
// 3. Build alpine base image
// - pull from docker hub
// - execute 'apk add' once per package
// 4. Build app from dockerfile
// 5. execute several commands in a row
build3: {
build: #Build & {
steps: [
#Pull & {
source: "alpine"
},
#Run & {
script: "echo A > /A.txt"
},
#Run & {
script: "echo B > /B.txt"
},
#Run & {
script: "echo C > /C.txt"
},
]
}
result: build.output
}

View File

@@ -0,0 +1,3 @@
## Dagger examples
A collection of examples to help Dagger developers get started.

View File

@@ -0,0 +1,28 @@
// Deployment plan for Dagger's example todoapp
package todoapp
import (
"dagger.io/dagger"
"universe.dagger.io/git"
"universe.dagger.io/yarn"
)
dagger.#DAG & {
// Build the app with yarn
actions: build: yarn.#Build
// Wire up source code to build
{
input: directories: source: _
actions: build: source: input.directories.source.contents
} | {
actions: {
pull: git.#Pull & {
remote: "https://github.com/mdn/todo-react"
ref: "master"
}
build: source: pull.checkout
}
}
}

View File

@@ -0,0 +1,40 @@
// Local dev environment for todoapp
package todoapp
import (
"universe.dagger.io/docker"
"universe.dagger.io/nginx"
)
// Expose todoapp web port
proxy: web: _
actions: {
// Reference app build inherited from base config
build: _
_app: build.output
container: {
// Build a container image serving the app with nginx
build: docker.#Build & {
steps: [
nginx.#Build & {
flavor: "alpine"
},
docker.#Copy & {
contents: _app
dest: "/usr/share/nginx/html"
},
]
}
// Run the app in an ephemeral container
run: docker.#Run & {
image: build.output
ports: web: {
frontend: proxy.web.endpoint
backend: address: "localhost:5000"
}
}
}
}

View File

@@ -0,0 +1,25 @@
// Deploy to Netlify
package todoapp
import (
"universe.dagger.io/netlify"
)
// Netlify API token
input: secrets: netlify: _
// Must be a valid branch/PR name
environment: string
actions: {
// Yarn build inherited from base config
build: _
deploy: netlify.#Deploy & {
contents: build.output
token: input.secrets.netlify.contents
site: *"acme-inc-\(environment)" | string
team: *"acme-inc" | string
}
}

3
europa-universe/fmt.sh Executable file
View File

@@ -0,0 +1,3 @@
#!/bin/bash
find . -name '*.cue' -exec cue fmt -s {} \;

View File

@@ -0,0 +1,8 @@
package git
import (
"dagger.io/dagger"
)
#Pull: dagger.#GitPull
#Push: dagger.#GitPush

View File

@@ -0,0 +1 @@
deploy.sh.cue

View File

@@ -0,0 +1,56 @@
package netlify
_deployScript: #"""
export NETLIFY_AUTH_TOKEN="$(cat /run/secrets/token)"
create_site() {
url="https://api.netlify.com/api/v1/${NETLIFY_ACCOUNT:-}/sites"
response=$(curl -s -S --fail-with-body -H "Authorization: Bearer $NETLIFY_AUTH_TOKEN" \
-X POST -H "Content-Type: application/json" \
$url \
-d "{\"name\": \"${NETLIFY_SITE_NAME}\", \"custom_domain\": \"${NETLIFY_DOMAIN}\"}" -o body
)
if [ $? -ne 0 ]; then
cat body >&2
exit 1
fi
cat body | jq -r '.site_id'
}
site_id=$(curl -s -S -f -H "Authorization: Bearer $NETLIFY_AUTH_TOKEN" \
https://api.netlify.com/api/v1/sites\?filter\=all | \
jq -r ".[] | select(.name==\"$NETLIFY_SITE_NAME\") | .id" \
)
if [ -z "$site_id" ] ; then
if [ "${NETLIFY_SITE_CREATE:-}" != 1 ]; then
echo "Site $NETLIFY_SITE_NAME does not exist"
exit 1
fi
site_id=$(create_site)
if [ -z "$site_id" ]; then
echo "create site failed"
exit 1
fi
fi
netlify link --id "$site_id"
netlify build
netlify deploy \
--dir="$(pwd)" \
--site="$site_id" \
--prod \
| tee /tmp/stdout
url=$(</tmp/stdout sed -n -e 's/^Website URL:.*\(https:\/\/.*\)$/\1/p' | tr -d '\n')
deployUrl=$(</tmp/stdout sed -n -e 's/^Unique Deploy URL:.*\(https:\/\/.*\)$/\1/p' | tr -d '\n')
logsUrl=$(</tmp/stdout sed -n -e 's/^Logs:.*\(https:\/\/.*\)$/\1/p' | tr -d '\n')
# Write output files
mkdir -p /netlify
printf "$url" > /netlify/url
printf "$deployUrl" > /netlify/deployUrl
printf "$logsUrl" > /netlify/logsUrl
"""#

View File

@@ -0,0 +1,99 @@
// Deploy to Netlify
// https://netlify.com
package netlify
import (
"dagger.io/dagger"
"universe.dagger.io/docker"
"universe.dagger.io/alpine"
"universe.dagger.io/bash"
)
// Deploy a site to Netlify
#Deploy: {
// Contents of the site
contents: dagger.#FS
// Name of the Netlify site
// Example: "my-super-site"
site: string
// Netlify API token
token: dagger.#Secret
// Name of the Netlify team (optional)
// Example: "acme-inc"
// Default: use the Netlify account's default team
team: string | *""
// Domain at which the site should be available (optional)
// If not set, Netlify will allocate one under netlify.app.
// Example: "www.mysupersite.tld"
domain: string | *null
// Create the site if it doesn't exist
create: *true | false
// Execute `netlify deploy` in a container
command: bash.#Run & {
// Container image. `netlify` must be available in the execution path
*{
_buildDefaultImage: docker.#Build & {
input: alpine.#Build & {
bash: version: "=~5.1"
jq: version: "=~1.6"
curl: {}
yarn: version: "=~1.22"
}
steps: [{
run: script: "yarn global add netlify-cli@3.38.10"
}]
}
// No nested tasks, boo hoo hoo
image: _buildDefaultImage.output
env: CUSTOM_IMAGE: "0"
} | {
env: CUSTOM_IMAGE: "1"
}
script: _deployScript // see deploy.sh
always: true
env: {
NETLIFY_SITE_NAME: site
if (create) {
NETLIFY_SITE_CREATE: "1"
}
if domain != null {
NETLIFY_DOMAIN: domain
}
NETLIFY_ACCOUNT: team
}
workdir: "/src"
mounts: {
"Site contents": {
dest: "/src"
"contents": contents
}
"Netlify token": {
dest: "/run/secrets/token"
contents: token
}
}
output: files: {
"/netlify/url": _
"/netlify/deployUrl": _
"/netlify/logsUrl": _
}
}
// URL of the deployed site
url: command.output.files."/netlify/url".contents
// URL of the latest deployment
deployUrl: command.output.files."/netlify/deployUrl".contents
// URL for logs of the latest deployment
logsUrl: command.output.files."/netlify/logsUrl".contents
}

View File

@@ -0,0 +1,9 @@
package netlify
import (
"dagger.io/dagger"
)
deploy: #Deploy & {
contents: dagger.#Scratch
}

View File

@@ -0,0 +1,26 @@
// Run and deploy the Nginx web server
// https://nginx.org
package nginx
import (
"universe.dagger.io/docker"
)
// Build a nginx container image
// FIXME: bootstrapping by wrapping "docker pull nginx"
// Possible ways to improve:
// 1. "docker build" the docker hub image ourselves: https://github.com/nginxinc/docker-nginx
// 2. Reimplement same docker build in pure Cue (no more Dockerfile)
// FIXME: build from source or package distro, instead of docker pull
#Build: {
output: docker.#Image & _pull.image
_pull: docker.#Pull
*{
flavor: "alpine"
_pull: source: "index.docker.io/nginx:stable-alpine"
} | {
flavor: "debian"
_pull: source: "index.docker.io/nginx:stable"
}
}

View File

@@ -0,0 +1,24 @@
// Helpers to run python programs
package python
import (
"universe.dagger.io/docker"
"universe.dagger.io/alpine"
)
// Run a python script in a container
#Run: docker.#Run & {
script: string
cmd: {
name: "python"
flags: "-c": script
}
// As a convenience, image defaults to a ready-to-use python environment
image: docker.#Image | *_defaultImage
_defaultImage: alpine.#Image & {
packages: python: version: "3"
}
}

3
europa-universe/search.sh Executable file
View File

@@ -0,0 +1,3 @@
#!/bin/bash
find . -name '*.cue' -exec grep -H "$1" {} \;

33
europa-universe/test.sh Executable file
View File

@@ -0,0 +1,33 @@
#!/bin/bash
set -e
targets=(
dagger.io/dagger
dagger.io/dagger/engine
./docker
./docker/test/build
./alpine
./alpine/tests/simple
./yarn
./yarn/tests/simple
./bash
./python
./git
./nginx
./netlify
./netlify/test/simple
./examples/todoapp
./examples/todoapp/dev
./examples/todoapp/staging
)
for t in "${targets[@]}"; do
echo "-- $t"
cue eval "$t" >/dev/null
done

View File

@@ -0,0 +1,11 @@
package yarn
import (
"dagger.io/dagger/engine"
)
b: #Build & {
source: engine.#Scratch.output
}
out: b.output

View File

@@ -0,0 +1,11 @@
{
"name": "test",
"main": "index.js",
"license": {
"type": "Apache-2.0",
"url": "https://opensource.org/licenses/apache2.0.php"
},
"scripts": {
"build": "mkdir -p ./build && echo output > ./build/test && touch .env && cp .env ./build/"
}
}

View File

@@ -0,0 +1,12 @@
{
"name": "test",
"main": "index.js",
"license": {
"type": "Apache-2.0",
"url": "https://opensource.org/licenses/apache2.0.php"
},
"scripts": {
"build": "mkdir -p ./build && cp /.env ./build/env"
}
}

View File

@@ -0,0 +1,96 @@
// Yarn is a package manager for Javascript applications
package yarn
import (
"strings"
"dagger.io/dagger"
"dagger.io/dagger/engine/spec/engine"
"universe.dagger.io/alpine"
"universe.dagger.io/bash"
)
// Build a Yarn package
#Build: {
// Application source code
source: dagger.#FS
// working directory to use
cwd: *"." | string
// Write the contents of `environment` to this file,
// in the "envfile" format
writeEnvFile: string | *""
// Read build output from this directory
// (path must be relative to working directory)
buildDir: string | *"build"
// Run this yarn script
script: string | *"build"
// Optional arguments for the script
args: [...string] | *[]
// Secret variables
secrets: [string]: dagger.#Secret
// Yarn version
yarnVersion: *"=~1.22" | string
// Run yarn in a containerized build environment
command: bash.#Run & {
*{
image: (alpine.#Build & {
bash: version: "=~5.1"
yarn: version: yarnVersion
}).image
env: CUSTOM_IMAGE: "0"
} | {
env: CUSTOM_IMAGE: "1"
}
script: """
# Create $ENVFILE_NAME file if set
[ -n "$ENVFILE_NAME" ] && echo "$ENVFILE" > "$ENVFILE_NAME"
yarn --cwd "$YARN_CWD" install --production false
opts=( $(echo $YARN_ARGS) )
yarn --cwd "$YARN_CWD" run "$YARN_BUILD_SCRIPT" ${opts[@]}
mv "$YARN_BUILD_DIRECTORY" /build
"""
mounts: {
"yarn cache": {
dest: "/cache/yarn"
contents: engine.#CacheDir
}
"package source": {
dest: "/src"
contents: source
}
// FIXME: mount secrets
}
output: directories: "/build": _
env: {
YARN_BUILD_SCRIPT: script
YARN_ARGS: strings.Join(args, "\n")
YARN_CACHE_FOLDER: "/cache/yarn"
YARN_CWD: cwd
YARN_BUILD_DIRECTORY: buildDir
if writeEnvFile != "" {
ENVFILE_NAME: writeEnvFile
ENVFILE: strings.Join([ for k, v in env {"\(k)=\(v)"}], "\n")
}
}
workdir: "/src"
}
// The final contents of the package after build
output: command.output.directories."/build".contents
}