diff --git a/README.md b/README.md
index 31de6c0..cf73793 100644
--- a/README.md
+++ b/README.md
@@ -111,6 +111,7 @@ More detailed instructions are in the [`example README.md`](https://github.com/R
| **cap_add** | []string | no | Add individual capabilities. |
| **cap_drop** | []string | no | Drop invidual capabilities. |
| **devices** | []string | no | A list of devices to be exposed to the container. |
+| **auth** | block | no | Provide authentication for a private registry. See [Auth](#auth) for more details. |
| **mounts** | []block | no | A list of mounts to be mounted in the container. Volume, bind and tmpfs type mounts are supported. fstab style [`mount options`](https://github.com/containerd/containerd/blob/master/mount/mount_linux.go#L211-L235) are supported. |
**Mount block**
@@ -162,6 +163,21 @@ config {
}
```
+### auth
+
+If you want to pull from a private repository e.g. docker hub, you can specify `username` and `password` in the `auth` stanza. See example below.
+
+**NOTE**: In the below example, `user` and `pass` are just placeholder values which need to be replaced by actual `username` and `password`, when specifying the credentials.
+
+```
+config {
+ auth {
+ username = "user"
+ password = "pass"
+ }
+}
+```
+
## Networking
`nomad-driver-containerd` supports **host** and **bridge** networks.
diff --git a/containerd/containerd.go b/containerd/containerd.go
index ebdb6ee..8d00c6b 100644
--- a/containerd/containerd.go
+++ b/containerd/containerd.go
@@ -28,6 +28,7 @@ import (
"github.com/containerd/containerd/contrib/seccomp"
"github.com/containerd/containerd/oci"
refdocker "github.com/containerd/containerd/reference/docker"
+ remotesdocker "github.com/containerd/containerd/remotes/docker"
specs "github.com/opencontainers/runtime-spec/specs-go"
)
@@ -61,7 +62,26 @@ func (d *Driver) getContainerdVersion() (containerd.Version, error) {
return d.client.Version(ctxWithTimeout)
}
-func (d *Driver) pullImage(imageName, imagePullTimeout string) (containerd.Image, error) {
+type CredentialsOpt func(string) (string, string, error)
+
+func parshAuth(auth *RegistryAuth) CredentialsOpt {
+ return func(string) (string, string, error) {
+ if auth == nil {
+ return "", "", nil
+ }
+ return auth.Username, auth.Password, nil
+ }
+}
+
+func withResolver(creds CredentialsOpt) containerd.RemoteOpt {
+ resolver := remotesdocker.NewResolver(remotesdocker.ResolverOptions{
+ Hosts: remotesdocker.ConfigureDefaultRegistries(remotesdocker.WithAuthorizer(
+ remotesdocker.NewAuthorizer(nil, creds))),
+ })
+ return containerd.WithResolver(resolver)
+}
+
+func (d *Driver) pullImage(imageName, imagePullTimeout string, auth *RegistryAuth) (containerd.Image, error) {
pullTimeout, err := time.ParseDuration(imagePullTimeout)
if err != nil {
return nil, fmt.Errorf("Failed to parse image_pull_timeout: %v", err)
@@ -75,7 +95,12 @@ func (d *Driver) pullImage(imageName, imagePullTimeout string) (containerd.Image
return nil, err
}
- return d.client.Pull(ctxWithTimeout, named.String(), containerd.WithPullUnpack)
+ pullOpts := []containerd.RemoteOpt{
+ containerd.WithPullUnpack,
+ withResolver(parshAuth(auth)),
+ }
+
+ return d.client.Pull(ctxWithTimeout, named.String(), pullOpts...)
}
func (d *Driver) createContainer(containerConfig *ContainerConfig, config *TaskConfig) (containerd.Container, error) {
diff --git a/containerd/driver.go b/containerd/driver.go
index b84710d..30a48a6 100644
--- a/containerd/driver.go
+++ b/containerd/driver.go
@@ -116,6 +116,10 @@ var (
"sysctl": hclspec.NewAttr("sysctl", "list(map(string))", false),
"readonly_rootfs": hclspec.NewAttr("readonly_rootfs", "bool", false),
"host_network": hclspec.NewAttr("host_network", "bool", false),
+ "auth": hclspec.NewBlock("auth", false, hclspec.NewObject(map[string]*hclspec.Spec{
+ "username": hclspec.NewAttr("username", "string", false),
+ "password": hclspec.NewAttr("password", "string", false),
+ })),
"mounts": hclspec.NewBlockList("mounts", hclspec.NewObject(map[string]*hclspec.Spec{
"type": hclspec.NewDefault(
hclspec.NewAttr("type", "string", false),
@@ -155,6 +159,12 @@ type Mount struct {
Options []string `codec:"options"`
}
+// Auth info to pull image from registry.
+type RegistryAuth struct {
+ Username string `codec:"username"`
+ Password string `codec:"password"`
+}
+
// TaskConfig contains configuration information for a task that runs with
// this plugin
type TaskConfig struct {
@@ -177,6 +187,7 @@ type TaskConfig struct {
Entrypoint []string `codec:"entrypoint"`
ReadOnlyRootfs bool `codec:"readonly_rootfs"`
HostNetwork bool `codec:"host_network"`
+ Auth RegistryAuth `codec:"auth"`
Mounts []Mount `codec:"mounts"`
}
@@ -411,7 +422,7 @@ func (d *Driver) StartTask(cfg *drivers.TaskConfig) (*drivers.TaskHandle, *drive
containerConfig.ContainerName = containerName
var err error
- containerConfig.Image, err = d.pullImage(driverConfig.Image, driverConfig.ImagePullTimeout)
+ containerConfig.Image, err = d.pullImage(driverConfig.Image, driverConfig.ImagePullTimeout, &driverConfig.Auth)
if err != nil {
return nil, nil, fmt.Errorf("Error in pulling image %s: %v", driverConfig.Image, err)
}
diff --git a/example/auth.nomad b/example/auth.nomad
new file mode 100644
index 0000000..f83c74b
--- /dev/null
+++ b/example/auth.nomad
@@ -0,0 +1,24 @@
+job "auth" {
+ datacenters = ["dc1"]
+
+ reschedule {
+ delay = "9s"
+ delay_function = "constant"
+ unlimited = true
+ }
+
+ group "auth-group" {
+ task "auth-task" {
+ driver = "containerd-driver"
+
+ config {
+ image = "shm32/hello-world:private"
+ }
+
+ resources {
+ cpu = 500
+ memory = 256
+ }
+ }
+ }
+}
diff --git a/tests/009-test-auth.sh b/tests/009-test-auth.sh
new file mode 100755
index 0000000..4696054
--- /dev/null
+++ b/tests/009-test-auth.sh
@@ -0,0 +1,30 @@
+#!/bin/bash
+
+source $SRCDIR/utils.sh
+
+job_name=auth
+
+# test auth
+test_auth_nomad_job() {
+ pushd ~/go/src/github.com/Roblox/nomad-driver-containerd/example
+
+ echo "INFO: Starting nomad $job_name job using nomad-driver-containerd."
+ nomad job run $job_name.nomad
+
+ wait_nomad_job_status $job_name failed
+
+ echo "INFO: Checking can not pull image without auth info."
+ local alloc_id
+ alloc_id=$(nomad job status auth|grep Allocations -A2|tail -n 1 |awk '{print $1}')
+ nomad status "$alloc_id"|grep -q "pull access denied, repository does not exist or may require authorization"
+ if [ $? -ne 0 ];then
+ echo "ERROR: Can not found pull access denied in alloc log."
+ exit 1
+ fi
+
+ echo "INFO: purge nomad ${job_name} job."
+ nomad job stop -purge ${job_name}
+ popd
+}
+
+test_auth_nomad_job
diff --git a/tests/009-test-allow-privileged.sh b/tests/010-test-allow-privileged.sh
similarity index 100%
rename from tests/009-test-allow-privileged.sh
rename to tests/010-test-allow-privileged.sh
diff --git a/tests/utils.sh b/tests/utils.sh
index 8c69d3a..0f15e2f 100755
--- a/tests/utils.sh
+++ b/tests/utils.sh
@@ -1,5 +1,24 @@
#!/bin/bash
+wait_nomad_job_status() {
+ local job_name=$1
+ local expected_status="$2"
+
+ local status
+ local i=0
+ while [ $i -lt 5 ]; do
+ status=$(nomad job status $job_name|grep Allocations -A2|tail -n 1 |awk '{print $6}')
+ if [ "$status" == "$expected_status" ]; then
+ return
+ fi
+ sleep 4
+ i=$((i + 1))
+ done
+
+ echo "ERROR: ${job_name} didn't enter $expected_status status. exit 1."
+ exit 1
+}
+
is_container_active() {
local job_name=$1
local is_sleep=$2