Docs: combine "user manual" and "programming manual" into "learn dagger"
Signed-off-by: Solomon Hykes <solomon@dagger.io>
This commit is contained in:
committed by
Solomon Hykes
parent
71c365542d
commit
9690981991
111
docs/learn/101-use.md
Normal file
111
docs/learn/101-use.md
Normal file
@@ -0,0 +1,111 @@
|
||||
---
|
||||
slug: /learn/101-basics
|
||||
---
|
||||
|
||||
# Dagger 101: basic usage
|
||||
|
||||
In this guide, you will learn the basics of Dagger by interacting with a pre-configured environment.
|
||||
Then you will move on to creating your own environment from scratch.
|
||||
|
||||
Our pre-configured environment deploys a simple [React](https://en.wikipedia.org/wiki/React_(JavaScript_library))
|
||||
application to a special hosting environment created and managed by us, the Dagger team for the purpose of this tutorial.
|
||||
This will allow you to deploy something "real" right away, without having to configure your own infrastructure first.
|
||||
|
||||
In later guides, you will learn how to configure Dagger to deploy to your own infrastructure. And, for advanced users,
|
||||
how to share access to your infrastructure in the same way that we are sharing access to ours now.
|
||||
|
||||
## Before we start
|
||||
|
||||
First, you'll need to make sure [you have installed dagger on your local machine](/install).
|
||||
|
||||
## Let's deploy our first application
|
||||
|
||||
**Step 1**: Clone the example repository
|
||||
|
||||
```sh
|
||||
git clone https://github.com/dagger/examples.git
|
||||
```
|
||||
|
||||
**Step 2**: Go the todoapp directory
|
||||
|
||||
`todoapp` is a simple Todo-list application written in Javascript using [React](https://reactjs.org/).
|
||||
|
||||
Go to the app directory:
|
||||
|
||||
```sh
|
||||
cd ./examples/todoapp
|
||||
```
|
||||
|
||||
**Step 3**: Decrypt the inputs
|
||||
|
||||
The example app contains encrypted secrets and other pre-configured inputs, here is how to decrypt them:
|
||||
|
||||
```sh
|
||||
dagger input list || curl -sfL https://releases.dagger.io/examples/key.txt >> ~/.config/dagger/keys.txt
|
||||
```
|
||||
|
||||
**Step 4**: Deploy!
|
||||
|
||||
```sh
|
||||
dagger up
|
||||
```
|
||||
|
||||
At the end of the deploy, you should see a list of outputs. There is one that is named `url`. This is the URL where our app has been deployed. If you go to this URL, you should see your application live!
|
||||
|
||||
## Change some code and re-deploy
|
||||
|
||||
This repository is already configured to deploy the code in the directory `./todoapp`, so you can change some code (or replace the app code with another react app!) and re-run the following command to re-deploy when you want your changes to be live:
|
||||
|
||||
```sh
|
||||
dagger up
|
||||
```
|
||||
|
||||
## Under the hood
|
||||
|
||||
This example showed you how to deploy and develop on an application that is already configured with dagger. Now, let's learn a few concepts to help you understand how this was put together.
|
||||
|
||||
### The Environment
|
||||
|
||||
An Environment holds the entire deployment configuration.
|
||||
|
||||
You can list existing environment from the `./todoapp` directory:
|
||||
|
||||
```sh
|
||||
dagger list
|
||||
```
|
||||
|
||||
You should see an environment named `s3`. You can have many environments within your app. For instance one for `staging`, one for `dev`, etc...
|
||||
|
||||
Each environment can have different kind of deployment code. For example, a `dev` environment can deploy locally, a `staging` environment can deploy to a remote infrastructure, and so on.
|
||||
|
||||
### The plan
|
||||
|
||||
The plan is the deployment code, that includes the logic to deploy the local application to an AWS S3 bucket. From the `todoapp` directory, you can list the code of the plan:
|
||||
|
||||
```sh
|
||||
ls -l .dagger/env/s3/plan/
|
||||
```
|
||||
|
||||
Any code change to the plan will be applied during the next `dagger up`.
|
||||
|
||||
### The inputs
|
||||
|
||||
The plan can define one or several `inputs` in order to take some information from the user. Here is how to list the current inputs:
|
||||
|
||||
```sh
|
||||
dagger input list
|
||||
```
|
||||
|
||||
The inputs are persisted inside the `.dagger` directory and pushed to your git repository. That's why this example application worked out of the box.
|
||||
|
||||
### The outputs
|
||||
|
||||
The plan defines one or several `outputs`. They can show useful information at the end of the deployment. That's how we read the deploy `url` at the end of the deployment. Here is the command to list all inputs:
|
||||
|
||||
```sh
|
||||
dagger output list
|
||||
```
|
||||
|
||||
## What's next?
|
||||
|
||||
At this point, you have deployed your first application using dagger and learned some dagger commands. You are now ready to [learn more about how to program dagger](/programming).
|
260
docs/learn/102-dev.md
Normal file
260
docs/learn/102-dev.md
Normal file
@@ -0,0 +1,260 @@
|
||||
---
|
||||
slug: /learn/102-dev
|
||||
---
|
||||
|
||||
# Dagger 102: create your first environment
|
||||
|
||||
## Overview
|
||||
|
||||
In this guide you will create your first Dagger environment from scratch,
|
||||
and use it to deploy a React application to 2 locations in parallel:
|
||||
a dedicated [Amazon S3](https://wikipedia.org/wiki/Amazon_S3) bucket, and a
|
||||
[Netlify](https://en.wikipedia.org/wiki/Netlify) site.
|
||||
|
||||
### Anatomy of a Dagger environment
|
||||
|
||||
A Dagger environment contains all the code and data necessary to delivery a particular application in a particular way.
|
||||
For example the same application might be delivered to a production and staging environment, each with their own
|
||||
configuration.
|
||||
|
||||
An environment is made of 3 parts:
|
||||
|
||||
* A *plan*, authored by the environment's *developer*, using the [Cue](https://cuelang.org) language. [Learn more about plans](FIXME)
|
||||
|
||||
* *Inputs*, supplied by the environment's *user* via the `dagger input` command, and written to a special file. Inputs may be configuration values, artifacts, or encrypted secrets. [Learn more about inputs](FIXME)
|
||||
|
||||
* *Outputs*, computed by the Dagger engine via the `dagger up` command, and recorded to a special directory. [Learn more about outputs](FIXME)
|
||||
|
||||
We will first develop our environment's *plan*, then configure its initial inputs, then finally run it to verify that it works.
|
||||
|
||||
## Developing your plan
|
||||
|
||||
### Anatomy of a plan
|
||||
|
||||
A _plan_ specifies, in code, how to deliver a particular application in a particular way.
|
||||
It is your environment's source code.
|
||||
|
||||
Unlike regular imperative programs which specify a sequence of instructions to execute,
|
||||
a Dagger plan is _declarative_: it lays out your application's supply chain as a graph
|
||||
of interconnected nodes.
|
||||
|
||||
Each node in the graph represents a component of the supply chain, for example:
|
||||
|
||||
* Development tools: source control, CI, build systems, testing systems
|
||||
* Hosting infrastructure: compute, storage, networking, databases, CDN..
|
||||
* Software dependencies: operating systems, languages, libraries, frameworks, etc.
|
||||
|
||||
Each link in the graph represents a flow of data between nodes. For example:
|
||||
|
||||
* source code flows from a git repository to a build system;
|
||||
* system dependencies are combined in a docker image, then uploaded to a registry;
|
||||
* configuration files are generated then sent to a compute cluster or load balancer;
|
||||
* etc.
|
||||
|
||||
### Introduction to Cue development
|
||||
|
||||
Dagger delivery plans are developed in [Cue](https://cuelang.org).
|
||||
Cue is a powerful declarative language by the creator of GQL, the language used to deploy all applications at Google. It is a superset of JSON, with additional features to make declarative, data-driven programming as pleasant and productive as regular imperative programming.
|
||||
|
||||
If you are new to Cue development, don't worry: this tutorial will walk you through the basic
|
||||
steps to get started, and give you resources to learn more.
|
||||
|
||||
In technical terms, our plan is a [Cue Package](https://cuelang.org/docs/concepts/packages/#packages). In this tutorial we will develop a new Cue package from scratch for our plan; but you can use any Cue package as a plan.
|
||||
|
||||
### Install Cue
|
||||
|
||||
Although not strictly necessary, for an optimal development experience we recommend
|
||||
installing a recent version of [Cue](https://github.com/cuelang/cue/releases/).
|
||||
|
||||
### (optional) Prepare Cue learning resources
|
||||
|
||||
If you are new to Cue, we recommend keeping the following resources in browser tabs:
|
||||
|
||||
* The unofficial but excellent [Cuetorials](https://cuetorials.com/overview/foundations/) in a browser tab, to look up Cue concepts as they appear.
|
||||
|
||||
* The official [Cue interactive sandbox](https://cuelang.org/play) for easy experimentation.
|
||||
|
||||
### Setup example app
|
||||
|
||||
You will need a local copy of the [Dagger examples repository](https://github.com/dagger/examples).
|
||||
|
||||
```bash
|
||||
git clone https://github.com/dagger/examples
|
||||
```
|
||||
|
||||
Make sure that all commands are run from the `voteapp` directory:
|
||||
|
||||
```bash
|
||||
cd examples/voteapp
|
||||
```
|
||||
|
||||
### Initialize a Cue module
|
||||
|
||||
Developing for Dagger takes place in a [https://cuelang.org/docs/concepts/packages/#modules](Cue module).
|
||||
If you are familiar with Go, Cue modules are directly inspired by Go modules.
|
||||
Otherwise, don't worry: a Cue module is simply a directory with one or more Cue packages in it. A Cue module has a `cue.mod` directory at its root.
|
||||
|
||||
In this guide we will use the same directory as the root of the Dagger workspace and the root of the Cue module; but you can create your Cue module anywhere inside the Dagger workspace.
|
||||
|
||||
```bash
|
||||
cue mod init
|
||||
```
|
||||
|
||||
### Organize your package
|
||||
|
||||
Now we start developing our Cue package at the root of our Cue module.
|
||||
|
||||
In this guide we will split our package in multiple files, one per component.
|
||||
But you can organize your package any way you want: the Cue evaluator simply merges together
|
||||
all files from the same package, as long as they are in the same directory and start with the same
|
||||
`package` clause. It is common for a Cue package to have only one file.
|
||||
See the [Cue documentation](https://cuelang.org/docs/concepts/packages/#files-belonging-to-a-package) for more details.
|
||||
|
||||
We will call our package `multibucket` because it sounds badass and vaguely explains what it does.
|
||||
But you can call your packages anything you want.
|
||||
|
||||
Let's layout the structure of our package by creating all the files in advance:
|
||||
|
||||
```bash
|
||||
pkg=multibucket; touch $pkg-source.cue $pkg-yarn.cue $pkg-netlify.cue
|
||||
```
|
||||
|
||||
### Component 1: app source code
|
||||
|
||||
The first component of our plan is the source code of our React application.
|
||||
|
||||
In Dagger terms, this component has 2 important properties:
|
||||
|
||||
1. It is an [artifact](FIXME): something that can be represented as a directory.
|
||||
2. It is an [input](FIXME): something that is provided by the end user.
|
||||
|
||||
Let's write the corresponding Cue code to `multibucket-source.cue`:
|
||||
|
||||
```cue
|
||||
package multibucket
|
||||
|
||||
import (
|
||||
"dagger.io/dagger"
|
||||
)
|
||||
|
||||
// Source code of the sample application
|
||||
src: dagger.#Artifact @dagger(input)
|
||||
```
|
||||
|
||||
This defines a component at the key `src`, of type `dagger.#Artifact`, annotated as an user input.
|
||||
|
||||
### Component 2: yarn package
|
||||
|
||||
The second component of our plan is the Yarn package built from the source code.
|
||||
|
||||
Let's write it to `multibucket-yarn.cue`:
|
||||
|
||||
```cue
|
||||
package multibucket
|
||||
|
||||
import (
|
||||
"dagger.io/js/yarn"
|
||||
)
|
||||
|
||||
// Build the source code using Yarn
|
||||
app: yarn.#Package & {
|
||||
source: src
|
||||
}
|
||||
```
|
||||
|
||||
Let's break it down:
|
||||
|
||||
* `package multibucket`: this file is part of the multibucket package
|
||||
* `import ( "dagger.io/js/yarn" )`: import a package from the [Dagger Universe](https://github.com/dagger/dagger/tree/main/stdlib).
|
||||
* `app: yarn.#Package`: apply the `#Package` definition at the key `app`
|
||||
* `&`: also merge the following values at the same key...
|
||||
* `{ source: src }`: set the key `app.source` to the value of `src`. This connects our 2 components, forming the first link in our DAG.
|
||||
|
||||
### Component 3: dedicated S3 bucket
|
||||
|
||||
*FIXME*: this section is not yet available, because the [Amazon S3 package](https://github.com/dagger/dagger/tree/main/stdlib/aws/s3) does [not yet support bucket creation](https://github.com/dagger/dagger/issues/623). We welcome external contributions :)
|
||||
|
||||
### Component 4: deploy to Netlify
|
||||
|
||||
The third component of our plan is the Netlify site to which the app will be deployed.
|
||||
|
||||
Let's write it to `multibucket-netlify.cue`:
|
||||
|
||||
```cue
|
||||
package multibucket
|
||||
|
||||
import (
|
||||
"dagger.io/netlify"
|
||||
)
|
||||
|
||||
// Netlify site
|
||||
site: "netlify": netlify.#Site & {
|
||||
contents: app.build
|
||||
}
|
||||
```
|
||||
|
||||
This is very similar to the previous component:
|
||||
|
||||
* We use the same package name as the other files
|
||||
* We import another package from the [Dagger Universe](https://github.com/dagger/dagger/tree/main/stdlib).
|
||||
* `site: "netlify": site.#Netlify`: apply the `#Site` definition at the key `site.netlify`. Note the use of quotes to protect the key from name conflict.
|
||||
* `&`: also merge the following values at the same key...
|
||||
* `{ contents: app.build }`: set the key `site.netlify.contents` to the value of `app.build`. This connects our components 2 and 3, forming the second link in our DAG.
|
||||
|
||||
### Exploring a package documentation
|
||||
|
||||
But wait: how did we know what fields were available in `yarn.#Package` and `netlify.#Site`?
|
||||
Answer: thanks to the `dagger doc` command, which prints the documentation of any package from [Dagger Universe](https://github.com/dagger/dagger/tree/main/stdlib).
|
||||
|
||||
```bash
|
||||
dagger doc dagger.io/netlify
|
||||
dagger doc dagger.io/js/yarn
|
||||
```
|
||||
|
||||
You can also browse the [API reference](/api/FIXME) section of the documentation.
|
||||
|
||||
## Setup the environment
|
||||
|
||||
### Create a new environment
|
||||
|
||||
Now that your Cue package is ready, let's create an environment to run it,
|
||||
|
||||
```bash
|
||||
dagger new 'multibucket'
|
||||
```
|
||||
|
||||
### Load the plan into the environment
|
||||
|
||||
Now let's configure the new environment to use our package as its plan:
|
||||
|
||||
```bash
|
||||
cp multibucket-*.cue .dagger/env/multibucket/plan/
|
||||
```
|
||||
|
||||
Note: you need to copy the files from your package into the environment's plan directory, as shown above. This means that, if you make more changes to your package, you will need to copy the new version into the plan directory, or it will not be used. If you prefer, you can also edit the cue files directly in the plan directory, but we don't recommend it. In the future, we will probably add the ability to reference your package to make the manual copy unnecessary.
|
||||
|
||||
### Configure user inputs
|
||||
|
||||
[This section is not yet written](https://github.com/dagger/dagger/blob/main/CONTRIBUTING.md)
|
||||
|
||||
### Deploy
|
||||
|
||||
[This section is not yet written](https://github.com/dagger/dagger/blob/main/CONTRIBUTING.md)
|
||||
|
||||
### Using the environment
|
||||
|
||||
[This section is not yet written](https://github.com/dagger/dagger/blob/main/CONTRIBUTING.md)
|
||||
|
||||
## Share your environment
|
||||
|
||||
### Introduction to gitops
|
||||
|
||||
[This section is not yet written](https://github.com/dagger/dagger/blob/main/CONTRIBUTING.md)
|
||||
|
||||
### Review changes
|
||||
|
||||
[This section is not yet written](https://github.com/dagger/dagger/blob/main/CONTRIBUTING.md)
|
||||
|
||||
### Commit changes
|
||||
|
||||
[This section is not yet written](https://github.com/dagger/dagger/blob/main/CONTRIBUTING.md)
|
618
docs/learn/107-kubernetes.md
Normal file
618
docs/learn/107-kubernetes.md
Normal file
@@ -0,0 +1,618 @@
|
||||
---
|
||||
slug: /learn/107-kubernetes
|
||||
---
|
||||
|
||||
# Dagger 107: deploy to Kubernetes
|
||||
|
||||
This tutorial illustrates how to use dagger to build, push and deploy Docker
|
||||
images to Kubernetes.
|
||||
|
||||
import Tabs from '@theme/Tabs';
|
||||
import TabItem from '@theme/TabItem';
|
||||
|
||||
## Prerequisites
|
||||
|
||||
For this tutorial, you will need a Kubernetes cluster.
|
||||
|
||||
<Tabs
|
||||
defaultValue="kind"
|
||||
groupId="provider"
|
||||
values={[
|
||||
{label: 'kind', value: 'kind'},
|
||||
{label: 'GKE', value: 'gke'},
|
||||
{label: 'EKS', value: 'eks'},
|
||||
]}>
|
||||
|
||||
<TabItem value="kind">
|
||||
|
||||
[Kind](https://kind.sigs.k8s.io/docs/user/quick-start) is a tool for running local Kubernetes clusters using Docker.
|
||||
|
||||
1\. Install kind
|
||||
|
||||
Follow [these instructions](https://kind.sigs.k8s.io/docs/user/quick-start) to
|
||||
install kind.
|
||||
|
||||
Alternatively, on macOS using [homebrew](https://brew.sh/):
|
||||
|
||||
```shell
|
||||
brew install kind
|
||||
```
|
||||
|
||||
2\. Start a local registry
|
||||
|
||||
```shell
|
||||
docker run -d -p 5000:5000 --name registry registry:2
|
||||
```
|
||||
|
||||
3\. Create a cluster with the local registry enabled in containerd
|
||||
|
||||
```bash
|
||||
cat <<EOF | kind create cluster --config=-
|
||||
kind: Cluster
|
||||
apiVersion: kind.x-k8s.io/v1alpha4
|
||||
containerdConfigPatches:
|
||||
- |-
|
||||
[plugins."io.containerd.grpc.v1.cri".registry.mirrors."localhost:5000"]
|
||||
endpoint = ["http://registry:5000"]
|
||||
EOF
|
||||
```
|
||||
|
||||
4\. Connect the registry to the cluster network
|
||||
|
||||
```shell
|
||||
docker network connect kind registry
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="gke">
|
||||
|
||||
This tutorial can be run against a [GCP GKE](https://cloud.google.com/kubernetes-engine) cluster and [GCR](https://cloud.google.com/container-registry)
|
||||
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="eks">
|
||||
|
||||
This tutorial can be run against a [AWS EKS](https://aws.amazon.com/eks/) cluster and [ECR](https://aws.amazon.com/ecr/)
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
## Initialize a Dagger Workspace and Environment
|
||||
|
||||
```shell
|
||||
mkdir dagger-kubernetes && cd dagger-kubernetes
|
||||
dagger init
|
||||
dagger new default
|
||||
```
|
||||
|
||||
## Create a basic plan
|
||||
|
||||
Create a file named `.dagger/env/default/plan/manifest.cue` and add the
|
||||
following configuration to it.
|
||||
|
||||
```cue title=".dagger/env/default/plan/manifest.cue"
|
||||
package main
|
||||
|
||||
// inlined kubernetes manifest as a string
|
||||
manifest: """
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: nginx
|
||||
labels:
|
||||
app: nginx
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: nginx
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: nginx
|
||||
spec:
|
||||
containers:
|
||||
- name: nginx
|
||||
image: nginx:1.14.2
|
||||
ports:
|
||||
- containerPort: 80
|
||||
"""
|
||||
```
|
||||
|
||||
This will define a `manifest` variable containing the inlined Kubernetes YAML
|
||||
used to create a _nginx_ deployment.
|
||||
|
||||
Next, create `.dagger/env/default/plan/main.cue`.
|
||||
|
||||
<Tabs
|
||||
defaultValue="kind"
|
||||
groupId="provider"
|
||||
values={[
|
||||
{label: 'kind', value: 'kind'},
|
||||
{label: 'GKE', value: 'gke'},
|
||||
{label: 'EKS', value: 'eks'},
|
||||
]}>
|
||||
|
||||
<TabItem value="kind">
|
||||
|
||||
```cue title=".dagger/env/default/plan/main.cue"
|
||||
package main
|
||||
|
||||
import (
|
||||
"dagger.io/dagger"
|
||||
"dagger.io/kubernetes"
|
||||
)
|
||||
|
||||
// input: ~/.kube/config file used for deployment
|
||||
// set with `dagger input secret kubeconfig -f ~/.kube/config`
|
||||
kubeconfig: dagger.#Secret @dagger(input)
|
||||
|
||||
// deploy uses the `dagger.io/kubernetes` package to apply a manifest to a
|
||||
// Kubernetes cluster.
|
||||
deploy: kubernetes.#Resources & {
|
||||
// reference the `kubeconfig` input above
|
||||
"kubeconfig": kubeconfig
|
||||
|
||||
// reference to the manifest defined in `manifest.cue`
|
||||
"manifest": manifest
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="gke">
|
||||
|
||||
```cue title=".dagger/env/default/plan/main.cue"
|
||||
package main
|
||||
|
||||
import (
|
||||
"dagger.io/kubernetes"
|
||||
"dagger.io/gcp/gke"
|
||||
)
|
||||
|
||||
// gkeConfig used for deployment
|
||||
gkeConfig: gke.#KubeConfig @dagger(input)
|
||||
|
||||
kubeconfig: gkeConfig.kubeconfig
|
||||
|
||||
// deploy uses the `dagger.io/kubernetes` package to apply a manifest to a
|
||||
// Kubernetes cluster.
|
||||
deploy: kubernetes.#Resources & {
|
||||
// reference the `kubeconfig` input above
|
||||
"kubeconfig": kubeconfig
|
||||
|
||||
// reference to the manifest defined in `manifest.cue`
|
||||
"manifest": manifest
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="eks">
|
||||
|
||||
```cue title=".dagger/env/default/plan/main.cue"
|
||||
package main
|
||||
|
||||
import (
|
||||
"dagger.io/kubernetes"
|
||||
"dagger.io/aws/eks"
|
||||
)
|
||||
|
||||
// eksConfig used for deployment
|
||||
eksConfig: eks.#KubeConfig @dagger(input)
|
||||
|
||||
kubeconfig: eksConfig.kubeconfig
|
||||
|
||||
// deploy uses the `dagger.io/kubernetes` package to apply a manifest to a
|
||||
// Kubernetes cluster.
|
||||
deploy: kubernetes.#Resources & {
|
||||
// reference the `kubeconfig` input above
|
||||
"kubeconfig": kubeconfig
|
||||
|
||||
// reference to the manifest defined in `manifest.cue`
|
||||
"manifest": manifest
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
This defines:
|
||||
|
||||
- `kubeconfig` a _string_ **input**: kubernetes configuration (`~/.kube/config`)
|
||||
used for `kubectl`
|
||||
- `deploy`: Deployment step using the package `dagger.io/kubernetes`. It takes
|
||||
the `manifest` defined earlier and deploys it to the Kubernetes cluster specified in `kubeconfig`.
|
||||
|
||||
### Configure the environment
|
||||
|
||||
Before we can bring up the deployment, we need to provide the `kubeconfig` input
|
||||
declared in the configuration. Otherwise, dagger will complain about a missing input:
|
||||
|
||||
```shell
|
||||
$ dagger up
|
||||
6:53PM ERR system | required input is missing input=kubeconfig
|
||||
```
|
||||
|
||||
You can inspect the list of inputs (both required and optional) using `dagger input list`:
|
||||
|
||||
<!--
|
||||
<Tabs
|
||||
defaultValue="kind"
|
||||
groupId="provider"
|
||||
values={[
|
||||
{label: 'kind', value: 'kind'},
|
||||
{label: 'GKE', value: 'gke'},
|
||||
{label: 'EKS', value: 'eks'},
|
||||
]}>
|
||||
|
||||
<TabItem value="kind">
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="gke">
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="eks">
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
-->
|
||||
|
||||
<Tabs
|
||||
defaultValue="kind"
|
||||
groupId="provider"
|
||||
values={[
|
||||
{label: 'kind', value: 'kind'},
|
||||
{label: 'GKE', value: 'gke'},
|
||||
{label: 'EKS', value: 'eks'},
|
||||
]}>
|
||||
|
||||
<TabItem value="kind">
|
||||
|
||||
```shell
|
||||
$ dagger input list
|
||||
Input Type Description
|
||||
kubeconfig string ~/.kube/config file used for deployment
|
||||
deploy.namespace string Kubernetes Namespace to deploy to
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="gke">
|
||||
|
||||
```shell
|
||||
$ dagger input list
|
||||
Input Type Description
|
||||
deploy.namespace string Kubernetes Namespace to deploy to
|
||||
gkeConfig.config.region string GCP region
|
||||
gkeConfig.config.project string GCP project
|
||||
gkeConfig.config.serviceKey dagger.#Secret GCP service key
|
||||
gkeConfig.clusterName string GKE cluster name
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="eks">
|
||||
|
||||
```shell
|
||||
$ dagger input list
|
||||
Input Type Description
|
||||
deploy.namespace string Kubernetes Namespace to deploy to
|
||||
eksConfig.config.region string AWS region
|
||||
eksConfig.config.accessKey dagger.#Secret AWS access key
|
||||
eksConfig.config.secretKey dagger.#Secret AWS secret key
|
||||
eksConfig.clusterName string EKS cluster name
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
Let's provide the missing inputs:
|
||||
|
||||
<Tabs
|
||||
defaultValue="kind"
|
||||
groupId="provider"
|
||||
values={[
|
||||
{label: 'kind', value: 'kind'},
|
||||
{label: 'GKE', value: 'gke'},
|
||||
{label: 'EKS', value: 'eks'},
|
||||
]}>
|
||||
|
||||
<TabItem value="kind">
|
||||
|
||||
```shell
|
||||
# we'll use the ~/.kube/config created by `kind`
|
||||
dagger input text kubeconfig -f ~/.kube/config
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="gke">
|
||||
|
||||
```shell
|
||||
dagger input text gkeConfig.config.project <PROJECT>
|
||||
dagger input text gkeConfig.config.region <REGION>
|
||||
dagger input text gkeConfig.clusterName <GKE CLUSTER NAME>
|
||||
dagger input secret gkeConfig.config.serviceKey -f <PATH TO THE SERVICEKEY.json>
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="eks">
|
||||
|
||||
```shell
|
||||
dagger input text eksConfig.config.region <REGION>
|
||||
dagger input text eksConfig.clusterName <EKS CLUSTER NAME>
|
||||
dagger input secret eksConfig.config.accessKey <ACCESS KEY>
|
||||
dagger input secret eksConfig.config.secretKey <SECRET KEY>
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
### Deploying
|
||||
|
||||
Now is time to deploy to kubernetes.
|
||||
|
||||
```shell
|
||||
$ dagger up
|
||||
deploy | computing
|
||||
deploy | #26 0.700 deployment.apps/nginx created
|
||||
deploy | completed duration=900ms
|
||||
```
|
||||
|
||||
Let's verify the deployment worked:
|
||||
|
||||
```shell
|
||||
$ kubectl get deployments
|
||||
NAME READY UP-TO-DATE AVAILABLE AGE
|
||||
nginx 1/1 1 1 1m
|
||||
```
|
||||
|
||||
## CUE Kubernetes manifests
|
||||
|
||||
In this section we will convert the inlined YAML manifest to CUE to take advantage of the language features.
|
||||
|
||||
For a more advanced example, see the
|
||||
[official CUE Kubernetes tutorial](https://github.com/cuelang/cue/blob/v0.4.0/doc/tutorial/kubernetes/README.md)
|
||||
|
||||
First, let's replace `manifest.cue` with the following configuration. This is a
|
||||
straightforward one-to-one conversion from YAML to CUE, only the syntax has changed.
|
||||
|
||||
```cue title=".dagger/env/default/plan/manifest.cue"
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/yaml"
|
||||
)
|
||||
|
||||
nginx: {
|
||||
apiVersion: "apps/v1"
|
||||
kind: "Deployment"
|
||||
metadata: {
|
||||
"name": "nginx"
|
||||
labels: app: "nginx"
|
||||
}
|
||||
spec: {
|
||||
replicas: 1
|
||||
selector: matchLabels: app: "nginx"
|
||||
template: {
|
||||
metadata: labels: app: "nginx"
|
||||
spec: containers: [{
|
||||
"name": "nginx"
|
||||
"image": image
|
||||
ports: [{
|
||||
containerPort: port
|
||||
}]
|
||||
}]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
manifest: yaml.Marshal(nginx)
|
||||
```
|
||||
|
||||
We're using the built-in `yaml.Marshal` function to convert CUE back to YAML so
|
||||
Kubernetes still receives the same manifest.
|
||||
|
||||
You can inspect the configuration using `dagger query` to verify it produces the
|
||||
same manifest:
|
||||
|
||||
```shell
|
||||
$ dagger query manifest -f text
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
...
|
||||
```
|
||||
|
||||
Now that the manifest is defined in CUE, we can take advantage of the language
|
||||
to remove a lot of boilerplate and repetition.
|
||||
|
||||
Let's define a re-usable `#Deployment` definition in `.dagger/env/default/plan/deployment.cue"`:
|
||||
|
||||
```cue title=".dagger/env/default/plan/deployment.cue"
|
||||
package main
|
||||
|
||||
// Deployment template containing all the common boilerplate shared by
|
||||
// deployments of this application.
|
||||
#Deployment: {
|
||||
// name of the deployment. This will be used to automatically label resouces
|
||||
// and generate selectors.
|
||||
name: string
|
||||
|
||||
// container image
|
||||
image: string
|
||||
|
||||
// 80 is the default port
|
||||
port: *80 | int
|
||||
|
||||
// 1 is the default, but we allow any number
|
||||
replicas: *1 | int
|
||||
|
||||
// Deployment manifest. Uses the name, image, port and replicas above to
|
||||
// generate the resource manifest.
|
||||
manifest: {
|
||||
apiVersion: "apps/v1"
|
||||
kind: "Deployment"
|
||||
metadata: {
|
||||
"name": name
|
||||
labels: app: name
|
||||
}
|
||||
spec: {
|
||||
"replicas": replicas
|
||||
selector: matchLabels: app: name
|
||||
template: {
|
||||
metadata: labels: app: name
|
||||
spec: containers: [{
|
||||
"name": name
|
||||
"image": image
|
||||
ports: [{
|
||||
containerPort: port
|
||||
}]
|
||||
}]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`manifest.cue` can be rewritten as follows:
|
||||
|
||||
```cue title=".dagger/env/default/plan/manifest.cue"
|
||||
import (
|
||||
"encoding/yaml"
|
||||
)
|
||||
|
||||
nginx: #Deployment & {
|
||||
name: "nginx"
|
||||
image: "nginx:1.14.2"
|
||||
}
|
||||
|
||||
manifest: yaml.Marshal(nginx.manifest)
|
||||
```
|
||||
|
||||
Let's make sure it yields the same result:
|
||||
|
||||
```shell
|
||||
$ dagger query deploy.manifest -f text
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
...
|
||||
```
|
||||
|
||||
And we can now deploy it:
|
||||
|
||||
```shell
|
||||
$ dagger up
|
||||
deploy | computing
|
||||
deploy | #26 0.700 deployment.apps/nginx unchanged
|
||||
deploy | completed duration=900ms
|
||||
```
|
||||
|
||||
## Building, pushing and deploying Docker images
|
||||
|
||||
Rather than deploying an existing (`nginx`) image, we're going to build a Docker
|
||||
image from source, push it to a registry and update the kubernetes configuration.
|
||||
|
||||
### Update the plan
|
||||
|
||||
The following configuration will:
|
||||
|
||||
- Declare a `repository` input as a `dagger.#Artifact`. This will be mapped to
|
||||
the source code directory.
|
||||
- Declare a `registry` input. This is the address used for docker push
|
||||
- Use `dagger.io/docker` to build and push the image
|
||||
- Use the registry image reference (`push.ref`) as the image for the deployment.
|
||||
|
||||
```cue title=".dagger/env/default/plan/manifest.cue"
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/yaml"
|
||||
|
||||
"dagger.io/dagger"
|
||||
"dagger.io/docker"
|
||||
)
|
||||
|
||||
// input: source code repository, must contain a Dockerfile
|
||||
// set with `dagger input dir repository ./app`
|
||||
repository: dagger.#Artifact @dagger(input)
|
||||
|
||||
// registry to push images to
|
||||
registry: string @dagger(input)
|
||||
|
||||
// docker build the `repository` directory
|
||||
image: docker.#Build & {
|
||||
source: repository
|
||||
}
|
||||
|
||||
// push the `image` to the `registry`
|
||||
push: docker.#Push & {
|
||||
source: image
|
||||
ref: registry
|
||||
}
|
||||
|
||||
// use the `#Deployment` template to generate the kubernetes manifest
|
||||
app: #Deployment & {
|
||||
name: "test"
|
||||
|
||||
// use the reference of the image we just pushed
|
||||
// this creates a dependency: `app` will only be deployed after the image is
|
||||
// built and pushed.
|
||||
"image": push.ref
|
||||
}
|
||||
|
||||
manifest: yaml.Marshal(app.manifest)
|
||||
```
|
||||
|
||||
### Connect the Inputs
|
||||
|
||||
Next, we'll provide the two new inputs, `repository` and `registry`.
|
||||
|
||||
For the purpose of this tutorial we'll be using
|
||||
[hello-go](https://github.com/aluzzardi/hello-go) as example source code.
|
||||
|
||||
```shell
|
||||
$ git clone https://github.com/aluzzardi/hello-go.git
|
||||
dagger input dir repository ./hello-go
|
||||
dagger input text registry "localhost:5000/image"
|
||||
```
|
||||
|
||||
### Bring up the changes
|
||||
|
||||
```shell
|
||||
$ dagger up
|
||||
repository | computing
|
||||
repository | completed duration=0s
|
||||
image | computing
|
||||
image | completed duration=1s
|
||||
deploy | computing
|
||||
deploy | #26 0.709 deployment.apps/hello created
|
||||
deploy | completed duration=900ms
|
||||
```
|
||||
|
||||
Let's verify the deployment worked:
|
||||
|
||||
```shell
|
||||
$ kubectl get deployments
|
||||
NAME READY UP-TO-DATE AVAILABLE AGE
|
||||
nginx 1/1 1 1 1m
|
||||
hello 1/1 1 1 1m
|
||||
```
|
||||
|
||||
## Next Steps
|
||||
|
||||
Deploy on a hosted Kubernetes cluster:
|
||||
|
||||
- [GKE](https://github.com/dagger/dagger/tree/main/stdlib/gcp/gke)
|
||||
- [EKS](https://github.com/dagger/dagger/tree/main/stdlib/aws/eks)
|
||||
|
||||
Authenticate to a remote registry:
|
||||
|
||||
- [ECR](https://github.com/dagger/dagger/tree/main/stdlib/aws/ecr)
|
||||
- [GCR](https://github.com/dagger/dagger/tree/main/stdlib/gcp/gcr)
|
||||
|
||||
Integrate kubernetes tools with Dagger:
|
||||
|
||||
- [Helm](https://github.com/dagger/dagger/tree/main/stdlib/kubernetes/helm)
|
||||
- [Kustomize](https://github.com/dagger/dagger/tree/main/stdlib/kubernetes/kustomize)
|
896
docs/learn/108-cloudformation.mdx
Normal file
896
docs/learn/108-cloudformation.mdx
Normal file
@@ -0,0 +1,896 @@
|
||||
---
|
||||
slug: /learn/108-cloudformation
|
||||
---
|
||||
|
||||
# Dagger 108: provision infrastructure on AWS
|
||||
|
||||
In this guide you will learn how to automatically [provision infrastructure](https://dzone.com/articles/infrastructure-provisioning-–) on AWS, by integrating [Amazon Cloudformation](https://aws.amazon.com/cloudformation/) in your Dagger environment.
|
||||
|
||||
We will start with something simple: provisioning a new bucket on [Amazon S3](https://en.wikipedia.org/wiki/Amazon_S3). But Cloudformation can provision almost any AWS resource; and Dagger can integrate with the full Cloudformation API.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
### Reminder
|
||||
|
||||
#### Guidelines
|
||||
|
||||
The provisioning strategy detailed below follows S3 best practices. In order to remain agnostic of your current AWS level, it deeply relies on S3 and Cloudformation documentation.
|
||||
|
||||
#### Relays
|
||||
|
||||
When developing a plan based on relays, the first thing to consider is to read their universe reference: it summarizes the expected inputs and their corresponding formats. [<u>Here</u>](https://dagger.io/aws/cloudformation) is the Cloudformation one.
|
||||
|
||||
### Setup
|
||||
|
||||
1. Initialize a new folder and a new workspace
|
||||
|
||||
```bash
|
||||
mkdir infra-provisioning
|
||||
cd ./infra-provisioning
|
||||
dagger init
|
||||
```
|
||||
|
||||
2. Create a new environment
|
||||
|
||||
```bash
|
||||
dagger new s3-provisioning
|
||||
cd ./.dagger/env/s3-provisioning/plan/ #Personal preference to directly work inside the plan
|
||||
```
|
||||
|
||||
3. Create `main.cue` file with its corresponding `main` package
|
||||
|
||||
```bash
|
||||
touch main.cue
|
||||
|
||||
-- ~/infra-provisioning/.dagger/env/s3-provisioning/plan/main.cue --
|
||||
package main
|
||||
```
|
||||
|
||||
## Cloudformation
|
||||
|
||||
Now that a plan has been set, let's implement the Cloudformation template and convert it to a Cue definition for further flexibility.
|
||||
|
||||
### Template creation
|
||||
|
||||
The idea here is to follow best practices in [<u>S3 buckets</u>](https://docs.aws.amazon.com/AmazonS3/latest/userguide/HostingWebsiteOnS3Setup.html) provisioning. Thanksfully, the AWS documentation contains a working [<u>Cloudformation template</u>](https://docs.aws.amazon.com/fr_fr/AWSCloudFormation/latest/UserGuide/quickref-s3.html#scenario-s3-bucket-website) that fits 95% of our needs.
|
||||
|
||||
1. Tweaking the template: removing some of the ouputs
|
||||
|
||||
The [<u>template</u>](https://docs.aws.amazon.com/fr_fr/AWSCloudFormation/latest/UserGuide/quickref-s3.html#scenario-s3-bucket-website) has far more outputs than necessary, as we just want to retrieve the bucket name:
|
||||
|
||||
import Tabs from "@theme/Tabs";
|
||||
import TabItem from "@theme/TabItem";
|
||||
|
||||
<Tabs
|
||||
defaultValue="nv"
|
||||
values={[
|
||||
{ label: 'Past Output', value: 'pv', },
|
||||
{ label: 'New Output', value: 'nv', },
|
||||
{ label: 'Full Base Template', value: 'ft', },
|
||||
]
|
||||
}>
|
||||
<TabItem value="pv">
|
||||
|
||||
```json
|
||||
"Outputs": {
|
||||
"WebsiteURL": {
|
||||
"Value": {
|
||||
"Fn::GetAtt": [
|
||||
"S3Bucket",
|
||||
"WebsiteURL"
|
||||
]
|
||||
},
|
||||
"Description": "URL for website hosted on S3"
|
||||
},
|
||||
"S3BucketSecureURL": {
|
||||
"Value": {
|
||||
"Fn::Join": [
|
||||
"",
|
||||
[
|
||||
"https://",
|
||||
{
|
||||
"Fn::GetAtt": [
|
||||
"S3Bucket",
|
||||
"DomainName"
|
||||
]
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Description": "Name of S3 bucket to hold website content"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="nv">
|
||||
|
||||
```json
|
||||
"Outputs": {
|
||||
"Name": {
|
||||
"Value": {
|
||||
"Fn::GetAtt": [
|
||||
"S3Bucket",
|
||||
"Arn"
|
||||
]
|
||||
},
|
||||
"Description": "Name S3 Bucket"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="ft">
|
||||
|
||||
```json
|
||||
{
|
||||
"AWSTemplateFormatVersion": "2010-09-09",
|
||||
"Resources": {
|
||||
"S3Bucket": {
|
||||
"Type": "AWS::S3::Bucket",
|
||||
"Properties": {
|
||||
"AccessControl": "PublicRead",
|
||||
"WebsiteConfiguration": {
|
||||
"IndexDocument": "index.html",
|
||||
"ErrorDocument": "error.html"
|
||||
}
|
||||
},
|
||||
"DeletionPolicy": "Retain"
|
||||
},
|
||||
"BucketPolicy": {
|
||||
"Type": "AWS::S3::BucketPolicy",
|
||||
"Properties": {
|
||||
"PolicyDocument": {
|
||||
"Id": "MyPolicy",
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{
|
||||
"Sid": "PublicReadForGetBucketObjects",
|
||||
"Effect": "Allow",
|
||||
"Principal": "*",
|
||||
"Action": "s3:GetObject",
|
||||
"Resource": {
|
||||
"Fn::Join": [
|
||||
"",
|
||||
[
|
||||
"arn:aws:s3:::",
|
||||
{
|
||||
"Ref": "S3Bucket"
|
||||
},
|
||||
"/*"
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"Bucket": {
|
||||
"Ref": "S3Bucket"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Outputs": {
|
||||
"Name": {
|
||||
"Value": {
|
||||
"Fn::GetAtt": ["S3Bucket", "Arn"]
|
||||
},
|
||||
"Description": "Name S3 Bucket"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
2. Some _"Pro tips"_
|
||||
|
||||
Double-checks at the template level can be done with manual uploads on Cloudformation's web interface or by executing the below command locally:
|
||||
|
||||
```bash
|
||||
aws cloudformation validate-template --template-body file://template.json
|
||||
```
|
||||
|
||||
> PS: The _"Full Base Template"_ tab contains the base template used for the following parts of the guide
|
||||
|
||||
### JSON / YAML to Cue conversion
|
||||
|
||||
Once you'll get used to Cue, you might directly write Cloudformation templates in this language. As most of the current examples are either written in JSON or in YAML, let's see how to lazily convert them in Cue (optional but recommended).
|
||||
|
||||
###### 1. Modify main.cue
|
||||
|
||||
We will temporarly modify `main.cue` to process the conversion
|
||||
|
||||
<Tabs
|
||||
defaultValue="sv"
|
||||
values={[
|
||||
{ label: 'JSON Generic Code', value: 'sv', },
|
||||
{ label: 'YAML Generic Code', value: 'yv', },
|
||||
{ label: 'JSON Full example', value: 'fv', },
|
||||
]
|
||||
}>
|
||||
<TabItem value="sv">
|
||||
|
||||
```cue
|
||||
-- ~/infra-provisioning/.dagger/env/s3-provisioning/plan/main.cue --
|
||||
package main
|
||||
import "encoding/json"
|
||||
|
||||
point: json.Unmarshal(data)
|
||||
data: #"""
|
||||
// Paste above final JSON template here
|
||||
"""#
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="fv">
|
||||
|
||||
```cue
|
||||
-- ~/infra-provisioning/.dagger/env/s3-provisioning/plan/main.cue --
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
)
|
||||
|
||||
point: json.Unmarshal(data)
|
||||
data: #"""
|
||||
{
|
||||
"AWSTemplateFormatVersion": "2010-09-09",
|
||||
"Resources": {
|
||||
"S3Bucket": {
|
||||
"Type": "AWS::S3::Bucket",
|
||||
"Properties": {
|
||||
"AccessControl": "PublicRead",
|
||||
"WebsiteConfiguration": {
|
||||
"IndexDocument": "index.html",
|
||||
"ErrorDocument": "error.html"
|
||||
}
|
||||
},
|
||||
"DeletionPolicy": "Retain"
|
||||
},
|
||||
"BucketPolicy": {
|
||||
"Type": "AWS::S3::BucketPolicy",
|
||||
"Properties": {
|
||||
"PolicyDocument": {
|
||||
"Id": "MyPolicy",
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{
|
||||
"Sid": "PublicReadForGetBucketObjects",
|
||||
"Effect": "Allow",
|
||||
"Principal": "*",
|
||||
"Action": "s3:GetObject",
|
||||
"Resource": {
|
||||
"Fn::Join": [
|
||||
"",
|
||||
[
|
||||
"arn:aws:s3:::",
|
||||
{
|
||||
"Ref": "S3Bucket"
|
||||
},
|
||||
"/*"
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"Bucket": {
|
||||
"Ref": "S3Bucket"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Outputs": {
|
||||
"Name": {
|
||||
"Value": {
|
||||
"Fn::GetAtt": [
|
||||
"S3Bucket",
|
||||
"Arn"
|
||||
]
|
||||
},
|
||||
"Description": "Name S3 Bucket"
|
||||
}
|
||||
}
|
||||
}
|
||||
"""#
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="yv">
|
||||
|
||||
```cue
|
||||
-- ~/infra-provisioning/.dagger/env/s3-provisioning/plan/main.cue --
|
||||
package main
|
||||
import "encoding/yaml"
|
||||
|
||||
point: yaml.Unmarshal(data)
|
||||
data: """
|
||||
// Paste YAML here
|
||||
"""
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
###### 2. Retrieve the Unmarshalled JSON
|
||||
|
||||
Then, still in the same folder, query the `point` value to retrieve the Unmarshalled result of `data`:
|
||||
|
||||
<Tabs
|
||||
defaultValue="sc"
|
||||
values={[
|
||||
{ label: 'Short Command', value: 'sc', },
|
||||
{ label: 'Full Base Output', value: 'fo', },
|
||||
]
|
||||
}>
|
||||
<TabItem value="fo">
|
||||
|
||||
```bash
|
||||
-- ~/infra-provisioning/.dagger/env/s3-provisioning/plan/ --
|
||||
dagger query point
|
||||
# Output:
|
||||
# {
|
||||
# "AWSTemplateFormatVersion": "2010-09-09",
|
||||
# "Outputs": {
|
||||
# "Name": {
|
||||
# "Description": "Name S3 Bucket",
|
||||
# "Value": {
|
||||
# "Fn::GetAtt": [
|
||||
# "S3Bucket",
|
||||
# "Arn"
|
||||
# ]
|
||||
# }
|
||||
# }
|
||||
# },
|
||||
# "Resources": {
|
||||
# "BucketPolicy": {
|
||||
# "Properties": {
|
||||
# "Bucket": {
|
||||
# "Ref": "S3Bucket"
|
||||
# },
|
||||
# "PolicyDocument": {
|
||||
# "Id": "MyPolicy",
|
||||
# "Statement": [
|
||||
# {
|
||||
# "Action": "s3:GetObject",
|
||||
# "Effect": "Allow",
|
||||
# "Principal": "*",
|
||||
# "Resource": {
|
||||
# "Fn::Join": [
|
||||
# "",
|
||||
# [
|
||||
# "arn:aws:s3:::",
|
||||
# {
|
||||
# "Ref": "S3Bucket"
|
||||
# },
|
||||
# "/*"
|
||||
# ]
|
||||
# ]
|
||||
# },
|
||||
# "Sid": "PublicReadForGetBucketObjects"
|
||||
# }
|
||||
# ],
|
||||
# "Version": "2012-10-17"
|
||||
# }
|
||||
# },
|
||||
# "Type": "AWS::S3::BucketPolicy"
|
||||
# },
|
||||
# "S3Bucket": {
|
||||
# "DeletionPolicy": "Retain",
|
||||
# "Properties": {
|
||||
# "AccessControl": "PublicRead",
|
||||
# "WebsiteConfiguration": {
|
||||
# "ErrorDocument": "error.html",
|
||||
# "IndexDocument": "index.html"
|
||||
# }
|
||||
# },
|
||||
# "Type": "AWS::S3::Bucket"
|
||||
# }
|
||||
# }
|
||||
# }
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="sc">
|
||||
|
||||
```bash
|
||||
-- ~/infra-provisioning/.dagger/env/s3-provisioning/plan/ --
|
||||
dagger query point
|
||||
# Output:
|
||||
# {
|
||||
#Prints value stored in point key
|
||||
# }
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
###### 3. Store the output
|
||||
|
||||
This Cue version of the JSON template is going to be integrated inside our provisioning plan. Save the output for the next steps of the guide.
|
||||
|
||||
## Personal plan
|
||||
|
||||
With the Cloudformation template now finished, tested and converted in Cue. We can now enter the last part of our guide: piping everything together inside our personal plan.
|
||||
|
||||
Before continuing, don't forget to reset your `main.cue` plan to it's _Setup_ form:
|
||||
|
||||
```cue
|
||||
-- ~/infra-provisioning/.dagger/env/s3-provisioning/plan/main.cue --
|
||||
package main
|
||||
```
|
||||
|
||||
### Cloudformation relay
|
||||
|
||||
As our plan relies on [<u>Cloudformation's relay</u>](https://dagger.io/aws/cloudformation), let's dissect the expected inputs by gradually incorporating them in our plan.
|
||||
|
||||
| Name | Type | Description |
|
||||
| ------------------ | :---------------------------------------: | :------------------------------------------------------------------: |
|
||||
| _config.region_ | `string` | AWS region |
|
||||
| _config.accessKey_ | `dagger.#Secret` | AWS access key |
|
||||
| _config.secretKey_ | `dagger.#Secret` | AWS secret key |
|
||||
| _source_ | `string` | Source is the Cloudformation template (JSON/YAML string) |
|
||||
| _stackName_ | `string` | Stackname is the cloudformation stack |
|
||||
| _onFailure_ | `*"DO_NOTHING" \| "ROLLBACK" \| "DELETE"` | Behavior when failure to create/update the Stack |
|
||||
| _timeout_ | `*10 \| \>=0 & int` | Timeout for waiting for the stack to be created/updated (in minutes) |
|
||||
| _neverUpdate_ | `*false \| bool` | Never update the stack if already exists |
|
||||
|
||||
1. General insights
|
||||
|
||||
As seen before in the documentation, values starting with `*` are default values. However, as a plan developer, we may face the need to add default values to inputs from relays that don't have one : Cue gives you this flexibility (cf. `config` value detailed below).
|
||||
|
||||
> WARNING: All inputs without a default option have to be filled for a proper execution of the relay. In our case:
|
||||
>
|
||||
> - _config.region_
|
||||
> - _config.accessKey_
|
||||
> - _config.secretKey_
|
||||
> - _source_
|
||||
> - _stackName_
|
||||
|
||||
2. The config value
|
||||
|
||||
The config values are all part of the `aws` relay. Regarding this package, as you can see above, none of the 3 required inputs contain default options.
|
||||
|
||||
For the sake of the exercise, let's say that our company's policy is to mainly deploy on the `us-east-2` region. Having this value set as a default option could be a smart and efficient decision for our dev teams. Let's see how to implement it:
|
||||
|
||||
<Tabs
|
||||
defaultValue="av"
|
||||
values={[
|
||||
{ label: 'Before', value: 'bv', },
|
||||
{ label: 'After', value: 'av', },
|
||||
{ label: 'Details', value: 'dv', },
|
||||
]
|
||||
}>
|
||||
<TabItem value="bv">
|
||||
|
||||
```cue
|
||||
-- ~/infra-provisioning/.dagger/env/s3-provisioning/plan/main.cue --
|
||||
package main
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="av">
|
||||
|
||||
```cue
|
||||
-- ~/infra-provisioning/.dagger/env/s3-provisioning/plan/main.cue --
|
||||
package main
|
||||
|
||||
import (
|
||||
"dagger.io/aws"
|
||||
)
|
||||
|
||||
// AWS account: credentials and region
|
||||
awsConfig: aws.#Config & {
|
||||
region: *"us-east-2" | string @dagger(input)
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="dv">
|
||||
|
||||
```cue
|
||||
-- ~/infra-provisioning/.dagger/env/s3-provisioning/plan/main.cue --
|
||||
package main
|
||||
|
||||
import (
|
||||
"dagger.io/aws" // <-- Import AWS relay to instanciate aws.#Config
|
||||
)
|
||||
|
||||
// AWS account: credentials and region
|
||||
awsConfig: aws.#Config & { // Assign an aws.#Config definition to a field named `awsConfig`
|
||||
// awsConfig will be a directly requestable key : `dagger query awsConfig`
|
||||
// awsConfig sets the region to either an input, or a default string: "us-east-2"
|
||||
region: *"us-east-2" | string @dagger(input)
|
||||
// As we declare an aws.#Config, Dagger/Cue will automatically know that some others values inside this definition
|
||||
// are inputs, especially secrets (AccessKey, secretKey). Due to the confidential nature of secrets, we won't declare default values to them
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
_Pro tips: In order to check wether it worked or not, these two commands might help_
|
||||
|
||||
<Tabs
|
||||
defaultValue="fc"
|
||||
values={[
|
||||
{ label: 'First command', value: 'fc', },
|
||||
{ label: 'Second command', value: 'sc', },
|
||||
{ label: 'Failed execution', value: 'fe', },
|
||||
]
|
||||
}>
|
||||
<TabItem value="fc">
|
||||
|
||||
```bash
|
||||
-- ~/infra-provisioning/.dagger/env/s3-provisioning/plan/ --
|
||||
dagger input list # List required input in our personal plan
|
||||
# Output:
|
||||
# Input Value Set by user Description
|
||||
# awsConfig.region *"us-east-2" | string false AWS region
|
||||
# awsConfig.accessKey dagger.#Secret false AWS access key
|
||||
# awsConfig.secretKey dagger.#Secret false AWS secret key
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="sc">
|
||||
|
||||
```bash
|
||||
-- ~/infra-provisioning/.dagger/env/s3-provisioning/plan/ --
|
||||
dagger query # Query values / inspect default values (Very useful in case of conflict)
|
||||
# Output:
|
||||
# {
|
||||
# "awsConfig": {
|
||||
# "region": "us-east-2"
|
||||
# }
|
||||
# }
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="fe">
|
||||
|
||||
```bash
|
||||
-- ~/infra-provisioning/.dagger/env/s3-provisioning/plan/ --
|
||||
dagger up # Try to run the plan. As expected, we encounter a failure
|
||||
# Output:
|
||||
# 9:07PM ERR system | required input is missing input=awsConfig.accessKey
|
||||
# 9:07PM ERR system | required input is missing input=awsConfig.secretKey
|
||||
# 9:07PM FTL system | some required inputs are not set, please re-run with `--force` if you think it's a mistake missing=0s
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
Inside the `firstCommand` tab, we see that the `awsConfig.region` key has a default value set. It wasn't the case when we just imported the base relay.
|
||||
|
||||
Furthemore, in the `Failed execution` tab, the execution of the `dagger up` command fails because of the unspecified secret inputs.
|
||||
|
||||
3. Integrating Cloudformation relay
|
||||
|
||||
Now that we have the `config` definition properly configured, we can now import the Cloudformation one, and properly fill it :
|
||||
|
||||
<Tabs
|
||||
defaultValue="av"
|
||||
values={[
|
||||
{ label: 'Before', value: 'bv', },
|
||||
{ label: 'After', value: 'av', },
|
||||
{ label: 'Details', value: 'dv', },
|
||||
{ label: 'Full Base version', value: 'fv', },
|
||||
]
|
||||
}>
|
||||
<TabItem value="bv">
|
||||
|
||||
```cue
|
||||
-- ~/infra-provisioning/.dagger/env/s3-provisioning/plan/main.cue --
|
||||
package main
|
||||
|
||||
import (
|
||||
"dagger.io/aws"
|
||||
)
|
||||
|
||||
// AWS account: credentials and region
|
||||
awsConfig: aws.#Config & {
|
||||
region: *"us-east-2" | string @dagger(input)
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="av">
|
||||
|
||||
```cue
|
||||
-- ~/infra-provisioning/.dagger/env/s3-provisioning/plan/main.cue --
|
||||
package main
|
||||
|
||||
import (
|
||||
"dagger.io/aws"
|
||||
"dagger.io/random"
|
||||
"dagger.io/aws/cloudformation"
|
||||
)
|
||||
|
||||
// AWS account: credentials and region
|
||||
awsConfig: aws.#Config & {
|
||||
region: *"us-east-2" | string @dagger(input)
|
||||
}
|
||||
|
||||
|
||||
// Create a random suffix
|
||||
suffix: random.#String & {
|
||||
seed: ""
|
||||
}
|
||||
|
||||
// Request the cloudformation stackname as an input, or generated a default one with a random suffix to keep uniqueness
|
||||
cfnStackName: *"stack-\(suffix.out)" | string @dagger(input) // Has to be unique
|
||||
|
||||
// AWS Cloudformation stdlib
|
||||
cfnStack: cloudformation.#Stack & {
|
||||
config: awsConfig
|
||||
stackName: cfnStackName
|
||||
onFailure: "DO_NOTHING"
|
||||
source: json.Marshal(#cfnTemplate)
|
||||
}
|
||||
|
||||
#cfnTemplate: {
|
||||
// Paste Cue Cloudformation template here
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="dv">
|
||||
|
||||
```cue
|
||||
-- ~/infra-provisioning/.dagger/env/s3-provisioning/plan/main.cue --
|
||||
package main
|
||||
|
||||
import (
|
||||
"dagger.io/aws" // <-- Import AWS relay to instanciate aws.#Config
|
||||
"dagger.io/random" // <-- Import Random relay to instanciate random.#String
|
||||
"dagger.io/aws/cloudformation" // <-- Import Cloudformation relay to instanciate aws.#Cloudformation
|
||||
)
|
||||
|
||||
// AWS account: credentials and region
|
||||
awsConfig: aws.#Config & { // Assign an aws.#Config definition to a field named `awsConfig`
|
||||
// awsConfig will be a directly requestable key : `dagger query awsConfig`
|
||||
// awsConfig sets the region to either an input, or a default string: "us-east-2"
|
||||
region: *"us-east-2" | string @dagger(input)
|
||||
// As we declare an aws.#Config, Dagger/Cue will automatically know that some others values inside this definition
|
||||
// are inputs, especially secrets (AccessKey, secretKey). Due to the confidential nature of secrets, we won't declare default values to them
|
||||
}
|
||||
|
||||
// AWS Cloudformation stdlib
|
||||
cfnStack: cloudformation.#Stack & { // Assign an aws.#Cloudformation definition to a field named `cfnStack`
|
||||
// This definition is the stdlib package to use in order to deploy AWS instances programmatically
|
||||
|
||||
config: awsConfig // As seen in the relay doc, 3 config fields have to be provided : `config.region`, `config.accessKey` and `config.secretKey`
|
||||
// As their names contain a `.`, it means that the value `config` expects 3 fields `region`, `accessKey` and `secretKey`, included in a `aws.#Config` parent definition
|
||||
|
||||
stackName: cfnStackName // We assign to the `stackName` the `cfnStackName` declared below.
|
||||
// `stackName` expects a string type. However, as a plan developer, we wanted to give the developer a choice : either a default random value, or an input
|
||||
// The default random value *"stack-\(suffix.out)" uses the random.#String relay to generate a random value. We append it's result inside `"\(append_happening_here)"`
|
||||
|
||||
onFailure: "DO_NOTHING" // As cited in the Cloudformation relay, the `onFailure` key defines Cloudformation's stack behavior on failure
|
||||
|
||||
source: json.Marshal(#cfnTemplate) // source expects a JSON artifact. Here we remarshall the template decaled in Cue
|
||||
}
|
||||
|
||||
// Create a random suffix (cf. random relay)
|
||||
suffix: random.#String & { // Assign a #random definition to a field named `suffix`
|
||||
seed: "" // Set seed to empty string, to generate a new random string every time
|
||||
} // Output -> suffix.out is a random string
|
||||
|
||||
// Request the cloudformation stackname as an input, or generated a default one with a random suffix to keep uniqueness
|
||||
cfnStackName: *"stack-\(suffix.out)" | string @dagger(input) // Has to be unique
|
||||
|
||||
#cfnTemplate: {
|
||||
// Paste Cue Cloudformation template here
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="fv">
|
||||
|
||||
```cue
|
||||
-- ~/infra-provisioning/.dagger/env/s3-provisioning/plan/main.cue --
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"dagger.io/aws"
|
||||
"dagger.io/random"
|
||||
"dagger.io/aws/cloudformation"
|
||||
)
|
||||
|
||||
// AWS account: credentials and region
|
||||
awsConfig: aws.#Config & {
|
||||
region: *"us-east-2" | string @dagger(input)
|
||||
}
|
||||
|
||||
// Create a random suffix
|
||||
suffix: random.#String & {
|
||||
seed: ""
|
||||
}
|
||||
|
||||
// Query the Cloudformation stackname, or create one with a random suffix to keep unicity
|
||||
cfnStackName: *"stack-\(suffix.out)" | string @dagger(input)
|
||||
|
||||
// AWS Cloudformation stdlib
|
||||
cfnStack: cloudformation.#Stack & {
|
||||
config: awsConfig
|
||||
stackName: cfnStackName
|
||||
onFailure: "DO_NOTHING"
|
||||
source: json.Marshal(#cfnTemplate)
|
||||
}
|
||||
|
||||
#cfnTemplate: {
|
||||
"AWSTemplateFormatVersion": "2010-09-09",
|
||||
"Outputs": {
|
||||
"Name": {
|
||||
"Description": "Name S3 Bucket",
|
||||
"Value": {
|
||||
"Fn::GetAtt": [
|
||||
"S3Bucket",
|
||||
"Arn"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"Resources": {
|
||||
"BucketPolicy": {
|
||||
"Properties": {
|
||||
"Bucket": {
|
||||
"Ref": "S3Bucket"
|
||||
},
|
||||
"PolicyDocument": {
|
||||
"Id": "MyPolicy",
|
||||
"Statement": [
|
||||
{
|
||||
"Action": "s3:GetObject",
|
||||
"Effect": "Allow",
|
||||
"Principal": "*",
|
||||
"Resource": {
|
||||
"Fn::Join": [
|
||||
"",
|
||||
[
|
||||
"arn:aws:s3:::",
|
||||
{
|
||||
"Ref": "S3Bucket"
|
||||
},
|
||||
"/*"
|
||||
]
|
||||
]
|
||||
},
|
||||
"Sid": "PublicReadForGetBucketObjects"
|
||||
}
|
||||
],
|
||||
"Version": "2012-10-17"
|
||||
}
|
||||
},
|
||||
"Type": "AWS::S3::BucketPolicy"
|
||||
},
|
||||
"S3Bucket": {
|
||||
"DeletionPolicy": "Retain",
|
||||
"Properties": {
|
||||
"AccessControl": "PublicRead",
|
||||
"WebsiteConfiguration": {
|
||||
"ErrorDocument": "error.html",
|
||||
"IndexDocument": "index.html"
|
||||
}
|
||||
},
|
||||
"Type": "AWS::S3::Bucket"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
### Deploy
|
||||
|
||||
Finally ! We now have a working template ready to be used to provision S3 infrastructures. Let's add the missing inputs (aws credentials) and let's deploy it :
|
||||
|
||||
<Tabs
|
||||
defaultValue="nd"
|
||||
values={[
|
||||
{ label: 'Normal deploy', value: 'nd', },
|
||||
{ label: 'Debug deploy', value: 'dd', },
|
||||
]
|
||||
}>
|
||||
<TabItem value="nd">
|
||||
|
||||
```bash
|
||||
-- ~/infra-provisioning/.dagger/env/s3-provisioning/plan/ --
|
||||
dagger input secret awsConfig.accessKey yourAccessKey
|
||||
|
||||
-- ~/infra-provisioning/.dagger/env/s3-provisioning/plan/ --
|
||||
dagger input secret awsConfig.secretKey yourSecretKey
|
||||
|
||||
-- ~/infra-provisioning/.dagger/env/s3-provisioning/plan/ --
|
||||
dagger input list
|
||||
# Input Value Set by user Description
|
||||
# awsConfig.region *"us-east-2" | string false AWS region
|
||||
# awsConfig.accessKey dagger.#Secret true AWS access key <-- Specified
|
||||
# awsConfig.secretKey dagger.#Secret true AWS secret key <-- Specified
|
||||
# suffix.length *12 | number false length of the string
|
||||
# cfnStack.timeout *10 | >=0 & int false Timeout for waiting for the stack to be created/updated (in minutes)
|
||||
# cfnStack.neverUpdate *false | bool false Never update the stack if already exists
|
||||
|
||||
# All the other inputs have default values, we're good to go !
|
||||
|
||||
-- ~/infra-provisioning/.dagger/env/s3-provisioning/plan/ --
|
||||
dagger up
|
||||
# Output:
|
||||
#2:22PM INF suffix.out | computing
|
||||
#2:22PM INF suffix.out | completed duration=200ms
|
||||
#2:22PM INF cfnStack.outputs | computing
|
||||
#2:22PM INF cfnStack.outputs | #15 1.304 {
|
||||
#2:22PM INF cfnStack.outputs | #15 1.304 "Parameters": []
|
||||
#2:22PM INF cfnStack.outputs | #15 1.304 }
|
||||
#2:22PM INF cfnStack.outputs | #15 2.948 {
|
||||
#2:22PM INF cfnStack.outputs | #15 2.948 "StackId": "arn:aws:cloudformation:us-east-2:817126022176:stack/stack-emktqcfwksng/207d29a0-cd0b-11eb-aafd-0a6bae5481b4"
|
||||
#2:22PM INF cfnStack.outputs | #15 2.948 }
|
||||
#2:22PM INF cfnStack.outputs | completed duration=35s
|
||||
|
||||
-- ~/infra-provisioning/.dagger/env/s3-provisioning/plan/ --
|
||||
dagger output list
|
||||
# Output Value Description
|
||||
# suffix.out "emktqcfwksng" generated random string
|
||||
# cfnStack.outputs.Name "arn:aws:s3:::stack-emktqcfwksng-s3bucket-9eiowjs1jab4" -
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="dd">
|
||||
|
||||
```bash
|
||||
-- ~/infra-provisioning/.dagger/env/s3-provisioning/plan/ --
|
||||
dagger input secret awsConfig.accessKey yourAccessKey
|
||||
|
||||
-- ~/infra-provisioning/.dagger/env/s3-provisioning/plan/ --
|
||||
dagger input secret awsConfig.secretKey yourSecretKey
|
||||
|
||||
-- ~/infra-provisioning/.dagger/env/s3-provisioning/plan/ --
|
||||
dagger input list
|
||||
# Input Value Set by user Description
|
||||
# awsConfig.region *"us-east-2" | string false AWS region
|
||||
# awsConfig.accessKey dagger.#Secret true AWS access key <-- Specified
|
||||
# awsConfig.secretKey dagger.#Secret true AWS secret key <-- Specified
|
||||
# suffix.length *12 | number false length of the string
|
||||
# cfnStack.timeout *10 | >=0 & int false Timeout for waiting for the stack to be created/updated (in minutes)
|
||||
# cfnStack.neverUpdate *false | bool false Never update the stack if already exists
|
||||
|
||||
# All the other inputs have default values, we're good to go !
|
||||
|
||||
dagger up -l debug
|
||||
#Output:
|
||||
# 3:50PM DBG system | detected buildkit version version=v0.8.3
|
||||
# 3:50PM DBG system | spawning buildkit job localdirs={
|
||||
# "/tmp/infra-provisioning/.dagger/env/infra/plan": "/tmp/infra-provisioning/.dagger/env/infra/plan"
|
||||
# } attrs=null
|
||||
# 3:50PM DBG system | loading configuration
|
||||
# ... Lots of logs ... :-D
|
||||
# Output Value Description
|
||||
# suffix.out "abnyiemsoqbm" generated random string
|
||||
# cfnStack.outputs.Name "arn:aws:s3:::stack-abnyiemsoqbm-s3bucket-9eiowjs1jab4" -
|
||||
|
||||
-- ~/infra-provisioning/.dagger/env/s3-provisioning/plan/ --
|
||||
dagger output list
|
||||
# Output Value Description
|
||||
# suffix.out "abnyiemsoqbm" generated random string
|
||||
# cfnStack.outputs.Name "arn:aws:s3:::stack-abnyiemsoqbm-s3bucket-9eiowjs1jab4" -
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
> The deployment went well !
|
||||
|
||||
In case of a failure, the `Debug deploy` tab shows the command to use in order to get more informations.
|
||||
The name of the provisioned S3 instance lies in the `cfnStack.outputs.Name` output key, without `arn:aws:s3:::`
|
||||
|
||||
> With this provisioning infrastructure, your dev team will easily be able to instanciate aws infrastructures : all they need to know is `dagger input list` and `dagger up`, isn't that awesome ? :-D
|
||||
|
||||
PS: This plan could be further extended with the AWS S3 example : it could not only provision an infrastructure but also easily deploy on it.
|
||||
|
||||
PS1: As it could make a nice first exercise for you, this won't be detailed here. However, we're interested in your imagination : let us know your implementations :-)
|
5
docs/learn/_category_.json
Normal file
5
docs/learn/_category_.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"label": "Learn Dagger",
|
||||
"position": 3,
|
||||
"collapsed": false
|
||||
}
|
Reference in New Issue
Block a user