Skip to content

Commit

Permalink
Add docker runtime for agent
Browse files Browse the repository at this point in the history
The new agent requires a container runtime to execute workflow actions.
This commit introduces a Docker runtime.

Signed-off-by: Chris Doherty <chris.doherty4@gmail.com>
  • Loading branch information
chrisdoherty4 committed May 30, 2023
1 parent 7eebdf4 commit 6fbb52e
Show file tree
Hide file tree
Showing 8 changed files with 707 additions and 1 deletion.
7 changes: 6 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,21 @@ require (
github.com/equinix-labs/otel-init-go v0.0.7
github.com/go-logr/logr v1.2.4
github.com/go-logr/zapr v1.2.4
github.com/go-logr/zerologr v1.2.3
github.com/google/go-cmp v0.5.9
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0
github.com/onsi/ginkgo/v2 v2.9.5
github.com/onsi/gomega v1.27.7
github.com/opencontainers/image-spec v1.1.0-rc.3
github.com/pkg/errors v0.9.1
github.com/prometheus/client_golang v1.15.1
github.com/rs/zerolog v1.29.1
github.com/spf13/cobra v1.7.0
github.com/spf13/pflag v1.0.5
github.com/spf13/viper v1.15.0
github.com/stretchr/testify v1.8.3
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.42.0
go.uber.org/multierr v1.9.0
go.uber.org/zap v1.24.0
google.golang.org/grpc v1.55.0
google.golang.org/protobuf v1.30.0
Expand All @@ -45,6 +48,7 @@ require (

require (
github.com/Microsoft/go-winio v0.6.0 // indirect
github.com/avast/retry-go v3.0.0+incompatible // indirect
github.com/benbjohnson/clock v1.3.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/cenkalti/backoff/v4 v4.2.1 // indirect
Expand Down Expand Up @@ -76,6 +80,8 @@ require (
github.com/json-iterator/go v1.1.12 // indirect
github.com/magiconair/properties v1.8.7 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-colorable v0.1.12 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
Expand All @@ -101,7 +107,6 @@ require (
go.opentelemetry.io/otel/trace v1.16.0 // indirect
go.opentelemetry.io/proto/otlp v0.19.0 // indirect
go.uber.org/atomic v1.10.0 // indirect
go.uber.org/multierr v1.9.0 // indirect
gomodules.xyz/jsonpatch/v2 v2.3.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/genproto v0.0.0-20230306155012-7f2fa6fef1f4 // indirect
Expand Down
15 changes: 15 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,8 @@ github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hC
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY=
github.com/avast/retry-go v3.0.0+incompatible h1:4SOWQ7Qs+oroOTQOYnAHqelpCO0biHSxpiH9JdtuBj0=
github.com/avast/retry-go v3.0.0+incompatible/go.mod h1:XtSnn+n/sHqQIpZ10K1qAevBhOOCWBLXXy3hyiqqBrY=
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A=
github.com/benbjohnson/clock v1.3.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
Expand Down Expand Up @@ -307,6 +309,7 @@ github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3Ee
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
Expand Down Expand Up @@ -390,6 +393,8 @@ github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-logr/zapr v1.2.4 h1:QHVo+6stLbfJmYGkQ7uGHUCu5hnAFAj6mDe6Ea0SeOo=
github.com/go-logr/zapr v1.2.4/go.mod h1:FyHWQIzQORZ0QVE1BtVHv3cKtNLuXsbNLtpuhNapBOA=
github.com/go-logr/zerologr v1.2.3 h1:up5N9vcH9Xck3jJkXzgyOxozT14R47IyDODz8LM1KSs=
github.com/go-logr/zerologr v1.2.3/go.mod h1:BxwGo7y5zgSHYR1BjbnHPyF/5ZjVKfKxAZANVu6E8Ho=
github.com/go-openapi/jsonpointer v0.19.2/go.mod h1:3akKfEdA7DF1sugOqz1dVQHBcuDBPKZGEoHC/NkiQRg=
github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
Expand All @@ -412,6 +417,7 @@ github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg78
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
github.com/gobuffalo/flect v0.2.4/go.mod h1:1ZyCLIbg0YD7sDkzvFdPoOydPtD8y9JQnrOROolUcM8=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gogo/googleapis v1.1.0/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
Expand Down Expand Up @@ -608,8 +614,12 @@ github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJ
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40=
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
Expand Down Expand Up @@ -729,6 +739,9 @@ github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6So
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/zerolog v1.29.1 h1:cO+d60CHkknCbvzEWxP0S9K6KqyTjrCNUy1LdQLCGPc=
github.com/rs/zerolog v1.29.1/go.mod h1:Le6ESbR7hc+DP6Lt1THiV8CQSdkkNrd3R0XbEgp3ZBU=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
Expand Down Expand Up @@ -1028,10 +1041,12 @@ golang.org/x/sys v0.0.0-20210426230700-d19ff857e887/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210917161153-d61c044b1678/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211210111614-af8b64212486/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
Expand Down
9 changes: 9 additions & 0 deletions internal/agent/runtime/action_failure.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package runtime

const (
// ReasonMountPath is the path used by Actions to write their failure reasons.
ReasonMountPath = "/tinkerbell/failure-reason"

// MessageMountPath is the path used by Actions to write their failure message.
MessageMountPath = "/tinkerbell/failure-message"
)
202 changes: 202 additions & 0 deletions internal/agent/runtime/docker.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
package runtime

import (
"context"
"fmt"
"io"
"regexp"

retry "github.com/avast/retry-go"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/mount"
"github.com/docker/docker/client"
"github.com/go-logr/logr"
"github.com/tinkerbell/tink/internal/agent"
"github.com/tinkerbell/tink/internal/agent/runtime/internal"
"github.com/tinkerbell/tink/internal/agent/workflow"
"github.com/tinkerbell/tink/internal/ptr"
)

var _ agent.ContainerRuntime = &Docker{}

// Docker is a docker runtime that satisfies agent.ContainerRuntime.
type Docker struct {
log logr.Logger
client *client.Client
}

// Run satisfies agent.ContainerRuntime.
func (d *Docker) Run(ctx context.Context, a workflow.Action) error {
pullImage := func() error {
// We need the image to be available before we can create a container.
image, err := d.client.ImagePull(ctx, a.Image, types.ImagePullOptions{})
if err != nil {
return fmt.Errorf("docker: %w", err)
}
defer image.Close()

// Docker requires everything to be read from the images ReadCloser for the image to actually
// be pulled. We may want to log image pulls in a circular buffer somewhere for debugability.
if _, err = io.Copy(io.Discard, image); err != nil {
return fmt.Errorf("docker: %w", err)
}

return nil
}

err := retry.Do(pullImage, retry.Attempts(5), retry.DelayType(retry.BackOffDelay))
if err != nil {
return err
}

// TODO: Support all the other things on the action such as volumes.
cfg := container.Config{
Image: a.Image,
Env: toDockerEnv(a.Env),
}

failureFiles, err := internal.NewFailureFiles()
if err != nil {
return fmt.Errorf("create action failure files: %w", err)
}
defer failureFiles.Close()

hostCfg := container.HostConfig{
Mounts: []mount.Mount{
{
Type: mount.TypeBind,
Source: failureFiles.ReasonPath(),
Target: ReasonMountPath,
},
{
Type: mount.TypeBind,
Source: failureFiles.MessagePath(),
Target: MessageMountPath,
},
},
}

containerName := toContainerName(a.ID)

// Docker uses the entrypoint as the default command. The Tink Action Cmd property is modeled
// as being the command launched in the container hence it is used as the entrypoint. Args
// on the action are therefore the command portion in Docker.
if a.Cmd != "" {
cfg.Entrypoint = append(cfg.Entrypoint, a.Cmd)
}
if len(a.Args) > 0 {
cfg.Cmd = append(cfg.Cmd, a.Args...)
}

// TODO: Figure out container logging. We probably want to save it somewhere for debugability.

create, err := d.client.ContainerCreate(ctx, &cfg, &hostCfg, nil, nil, containerName)
if err != nil {
return fmt.Errorf("docker: %w", err)
}

// Always try to remove the container on exit.
defer func() {
// Force remove containers in an attempt to preserve space in memory constraints environments.
// In rare cases this may create orphaned volumes that the Docker CLI won't clean up.
opts := types.ContainerRemoveOptions{
Force: true,
RemoveLinks: true,
}

// We can't use the context passed to Run() as it may have been cancelled so we use Background()
// instead.
err := d.client.ContainerRemove(context.Background(), create.ID, opts)
if err != nil {
d.log.Info("Couldn't remove container", "container_name", containerName, "error", err)
}
}()

// Issue the wait with a 'next-exit' condition so we can await a response originating from
// ContainerStart().
waitBody, waitErr := d.client.ContainerWait(ctx, create.ID, container.WaitConditionNextExit)

if err := d.client.ContainerStart(ctx, create.ID, types.ContainerStartOptions{}); err != nil {
return fmt.Errorf("docker: %w", err)
}

select {
case result := <-waitBody:
if result.StatusCode == 0 {
return nil
}
return failureFiles.ToError()

case err := <-waitErr:
return fmt.Errorf("docker: %w", err)

case <-ctx.Done():
// We can't use the context passed to Run() as its been cancelled.
err := d.client.ContainerStop(context.Background(), create.ID, container.StopOptions{
Timeout: ptr.Int(5),
})
if err != nil {
d.log.Info("Failed to gracefully stop container", "error", err)
}
return fmt.Errorf("docker: %w", ctx.Err())
}
}

var validContainerName = regexp.MustCompile(`[^a-zA-Z0-9_.-]`)

// toContainerName converts an action ID into a usable container name.
func toContainerName(actionID string) string {
// Prepend 'tinkerbell_' so we guarantee the additional constraints on the first character.
return "tinkerbell_" + validContainerName.ReplaceAllString(actionID, "_")
}

func toDockerEnv(env map[string]string) []string {
var de []string
for k, v := range env {
de = append(de, fmt.Sprintf("%v=%v", k, v))
}
return de
}

// NewDocker creates a new Docker instance.
func NewDocker(opts ...DockerOption) (*Docker, error) {
o := &Docker{
log: logr.Discard(),
}

var err error
o.client, err = client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
if err != nil {
return nil, err
}

for _, fn := range opts {
fn(o)
}

return o, nil
}

// DockerOption defines optional configuration for a Docker instance.
type DockerOption func(*Docker)

// WithLogger returns an option to configure the logger on a Docker instance.
func WithLogger(log logr.Logger) DockerOption {
return func(o *Docker) {
if log.GetSink() == nil {
return
}
o.log = log
}
}

// WithClient returns an option to configure a Docker client on a Docker instance.
func WithClient(clnt *client.Client) DockerOption {
return func(o *Docker) {
if clnt == nil {
return
}
o.client = clnt
}
}
Loading

0 comments on commit 6fbb52e

Please sign in to comment.