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 13, 2023
1 parent 0dfb4e6 commit bbeda78
Show file tree
Hide file tree
Showing 8 changed files with 691 additions and 1 deletion.
6 changes: 5 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.4
github.com/onsi/gomega v1.27.6
github.com/opencontainers/image-spec v1.1.0-rc.3
github.com/pkg/errors v0.9.1
github.com/prometheus/client_golang v1.15.0
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.2
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.41.1
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 Down Expand Up @@ -76,6 +79,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/moby/term v0.0.0-20221205130635-1aeaba878587 // indirect
Expand Down Expand Up @@ -103,7 +108,6 @@ require (
go.opentelemetry.io/otel/trace v1.15.1 // 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.2.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/genproto v0.0.0-20230306155012-7f2fa6fef1f4 // indirect
Expand Down
13 changes: 13 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,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 +391,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 +415,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 @@ -609,8 +613,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 @@ -732,6 +740,9 @@ github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6L
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
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 @@ -1031,10 +1042,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"
)
177 changes: 177 additions & 0 deletions internal/agent/runtime/docker.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
package runtime

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

"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{}

// DockerOptions defines the options for configuring a Docker instance.
type DockerOptions struct {
// Logger defines the logger to be used by the Docker instance.
// Defaults to logr.Discard().
Logger logr.Logger

// Client is the client to be used by the Docker instance.
// Defaults to client.NewClientWithOpts(client.FromEnv).
Client *client.Client
}

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

// NewDocker creates a new Docker instance.
func NewDocker(opts DockerOptions) (*Docker, error) {
if opts.Client == nil {
clnt, err := client.NewClientWithOpts(client.FromEnv)
if err != nil {
return nil, err
}
clnt.NegotiateAPIVersion(context.Background())
opts.Client = clnt
}

if opts.Logger.GetSink() == nil {
opts.Logger = logr.Discard()
}

return &Docker{
log: opts.Logger,
client: opts.Client,
}, nil
}

// Run satisfies agent.ContainerRuntime.
func (d *Docker) Run(ctx context.Context, a workflow.Action) 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)
}
// Close should be idempotent and we don't need the handle beyond this point.
image.Close()

// 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 := toValidContainerName(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.

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() {
// We can't use the context passed to Run() as it may have been cancelled.
err := d.client.ContainerRemove(context.Background(), create.ID, types.ContainerRemoveOptions{})
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())
}
}

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
}

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

// toValidContainerName returns a valid container name for docker. Container names must satisfy
// [a-zA-Z0-9][a-zA-Z0-9_.-].
func toValidContainerName(name string) string {
// Prepend 'action_' so we guarantee the additional constraints on the first character.
return "action_" + validContainerNameRegex.ReplaceAllString(name, "_")
}
Loading

0 comments on commit bbeda78

Please sign in to comment.