From 441f70aee8a74416ce3030960b514689dac00e8b Mon Sep 17 00:00:00 2001 From: Jordan Date: Wed, 27 Oct 2021 07:35:37 -0500 Subject: [PATCH] feat(runtime): add logic from pkg-runtime --- cmd/vela-worker/exec.go | 4 +- cmd/vela-worker/flags.go | 2 +- cmd/vela-worker/run.go | 2 +- cmd/vela-worker/validate.go | 2 +- cmd/vela-worker/worker.go | 2 +- executor/context_test.go | 2 +- executor/executor_test.go | 2 +- executor/linux/build_test.go | 2 +- executor/linux/driver_test.go | 2 +- executor/linux/linux.go | 2 +- executor/linux/linux_test.go | 2 +- executor/linux/opts.go | 2 +- executor/linux/opts_test.go | 4 +- executor/linux/secret_test.go | 2 +- executor/linux/service_test.go | 2 +- executor/linux/stage_test.go | 2 +- executor/linux/step_test.go | 2 +- executor/local/build_test.go | 2 +- executor/local/driver_test.go | 2 +- executor/local/local.go | 2 +- executor/local/local_test.go | 2 +- executor/local/opts.go | 2 +- executor/local/opts_test.go | 4 +- executor/local/service_test.go | 2 +- executor/local/stage_test.go | 2 +- executor/local/step_test.go | 2 +- executor/setup.go | 2 +- executor/setup_test.go | 2 +- go.mod | 12 +- go.sum | 5 - router/middleware/executor/executor_test.go | 2 +- runtime/context.go | 67 +++ runtime/context_test.go | 142 +++++ runtime/doc.go | 17 + runtime/docker/build.go | 45 ++ runtime/docker/build_test.go | 154 +++++ runtime/docker/container.go | 355 ++++++++++++ runtime/docker/container_test.go | 389 +++++++++++++ runtime/docker/doc.go | 11 + runtime/docker/docker.go | 109 ++++ runtime/docker/docker_test.go | 117 ++++ runtime/docker/driver.go | 12 + runtime/docker/driver_test.go | 29 + runtime/docker/image.go | 92 +++ runtime/docker/image_test.go | 64 +++ runtime/docker/network.go | 110 ++++ runtime/docker/network_test.go | 132 +++++ runtime/docker/opts.go | 36 ++ runtime/docker/opts_test.go | 74 +++ runtime/docker/volume.go | 150 +++++ runtime/docker/volume_test.go | 132 +++++ runtime/engine.go | 92 +++ runtime/flags.go | 70 +++ runtime/kubernetes/build.go | 171 ++++++ runtime/kubernetes/build_test.go | 226 ++++++++ runtime/kubernetes/container.go | 382 ++++++++++++ runtime/kubernetes/container_test.go | 336 +++++++++++ runtime/kubernetes/doc.go | 11 + runtime/kubernetes/driver.go | 12 + runtime/kubernetes/driver_test.go | 29 + runtime/kubernetes/image.go | 67 +++ runtime/kubernetes/image_test.go | 48 ++ runtime/kubernetes/kubernetes.go | 130 +++++ runtime/kubernetes/kubernetes_test.go | 316 ++++++++++ runtime/kubernetes/network.go | 124 ++++ runtime/kubernetes/network_test.go | 132 +++++ runtime/kubernetes/opts.go | 67 +++ runtime/kubernetes/opts_test.go | 163 ++++++ runtime/kubernetes/testdata/config | 18 + runtime/kubernetes/testdata/config_empty | 0 runtime/kubernetes/volume.go | 174 ++++++ runtime/kubernetes/volume_test.go | 136 +++++ runtime/runtime.go | 50 ++ runtime/runtime_test.go | 63 ++ runtime/setup.go | 89 +++ runtime/setup_test.go | 91 +++ runtime/testdata/config | 18 + runtime/testdata/large.yml | 605 ++++++++++++++++++++ runtime/testdata/stages.yml | 13 + runtime/testdata/steps.yml | 32 ++ 80 files changed, 5945 insertions(+), 38 deletions(-) create mode 100644 runtime/context.go create mode 100644 runtime/context_test.go create mode 100644 runtime/doc.go create mode 100644 runtime/docker/build.go create mode 100644 runtime/docker/build_test.go create mode 100644 runtime/docker/container.go create mode 100644 runtime/docker/container_test.go create mode 100644 runtime/docker/doc.go create mode 100644 runtime/docker/docker.go create mode 100644 runtime/docker/docker_test.go create mode 100644 runtime/docker/driver.go create mode 100644 runtime/docker/driver_test.go create mode 100644 runtime/docker/image.go create mode 100644 runtime/docker/image_test.go create mode 100644 runtime/docker/network.go create mode 100644 runtime/docker/network_test.go create mode 100644 runtime/docker/opts.go create mode 100644 runtime/docker/opts_test.go create mode 100644 runtime/docker/volume.go create mode 100644 runtime/docker/volume_test.go create mode 100644 runtime/engine.go create mode 100644 runtime/flags.go create mode 100644 runtime/kubernetes/build.go create mode 100644 runtime/kubernetes/build_test.go create mode 100644 runtime/kubernetes/container.go create mode 100644 runtime/kubernetes/container_test.go create mode 100644 runtime/kubernetes/doc.go create mode 100644 runtime/kubernetes/driver.go create mode 100644 runtime/kubernetes/driver_test.go create mode 100644 runtime/kubernetes/image.go create mode 100644 runtime/kubernetes/image_test.go create mode 100644 runtime/kubernetes/kubernetes.go create mode 100644 runtime/kubernetes/kubernetes_test.go create mode 100644 runtime/kubernetes/network.go create mode 100644 runtime/kubernetes/network_test.go create mode 100644 runtime/kubernetes/opts.go create mode 100644 runtime/kubernetes/opts_test.go create mode 100644 runtime/kubernetes/testdata/config create mode 100644 runtime/kubernetes/testdata/config_empty create mode 100644 runtime/kubernetes/volume.go create mode 100644 runtime/kubernetes/volume_test.go create mode 100644 runtime/runtime.go create mode 100644 runtime/runtime_test.go create mode 100644 runtime/setup.go create mode 100644 runtime/setup_test.go create mode 100644 runtime/testdata/config create mode 100644 runtime/testdata/large.yml create mode 100644 runtime/testdata/stages.yml create mode 100644 runtime/testdata/steps.yml diff --git a/cmd/vela-worker/exec.go b/cmd/vela-worker/exec.go index b0986742..c2166e8e 100644 --- a/cmd/vela-worker/exec.go +++ b/cmd/vela-worker/exec.go @@ -8,8 +8,8 @@ import ( "context" "time" - "github.com/go-vela/pkg-runtime/runtime" "github.com/go-vela/worker/executor" + "github.com/go-vela/worker/runtime" "github.com/go-vela/worker/version" "github.com/sirupsen/logrus" @@ -27,7 +27,7 @@ func (w *Worker) exec(index int) error { // setup the runtime // - // https://pkg.go.dev/github.com/go-vela/pkg-runtime/runtime?tab=doc#New + // https://pkg.go.dev/github.com/go-vela/worker/runtime?tab=doc#New w.Runtime, err = runtime.New(w.Config.Runtime) if err != nil { return err diff --git a/cmd/vela-worker/flags.go b/cmd/vela-worker/flags.go index e47d0b30..38696c2e 100644 --- a/cmd/vela-worker/flags.go +++ b/cmd/vela-worker/flags.go @@ -8,8 +8,8 @@ import ( "time" "github.com/go-vela/pkg-queue/queue" - "github.com/go-vela/pkg-runtime/runtime" "github.com/go-vela/worker/executor" + "github.com/go-vela/worker/runtime" "github.com/urfave/cli/v2" ) diff --git a/cmd/vela-worker/run.go b/cmd/vela-worker/run.go index 6574c62c..e81f2bf5 100644 --- a/cmd/vela-worker/run.go +++ b/cmd/vela-worker/run.go @@ -11,8 +11,8 @@ import ( "github.com/gin-gonic/gin" "github.com/go-vela/pkg-queue/queue" - "github.com/go-vela/pkg-runtime/runtime" "github.com/go-vela/worker/executor" + "github.com/go-vela/worker/runtime" "github.com/sirupsen/logrus" diff --git a/cmd/vela-worker/validate.go b/cmd/vela-worker/validate.go index 36116a16..ab42a971 100644 --- a/cmd/vela-worker/validate.go +++ b/cmd/vela-worker/validate.go @@ -72,7 +72,7 @@ func (w *Worker) Validate() error { // verify the runtime configuration // - // https://godoc.org/github.com/go-vela/pkg-runtime/runtime#Setup.Validate + // https://godoc.org/github.com/go-vela/worker/runtime#Setup.Validate err = w.Config.Runtime.Validate() if err != nil { return err diff --git a/cmd/vela-worker/worker.go b/cmd/vela-worker/worker.go index d05af002..20b25e7d 100644 --- a/cmd/vela-worker/worker.go +++ b/cmd/vela-worker/worker.go @@ -9,9 +9,9 @@ import ( "time" "github.com/go-vela/pkg-queue/queue" - "github.com/go-vela/pkg-runtime/runtime" "github.com/go-vela/sdk-go/vela" "github.com/go-vela/worker/executor" + "github.com/go-vela/worker/runtime" ) type ( diff --git a/executor/context_test.go b/executor/context_test.go index cb98e237..4ccee9a6 100644 --- a/executor/context_test.go +++ b/executor/context_test.go @@ -16,7 +16,7 @@ import ( "github.com/go-vela/worker/executor/linux" - "github.com/go-vela/pkg-runtime/runtime/docker" + "github.com/go-vela/worker/runtime/docker" "github.com/go-vela/sdk-go/vela" ) diff --git a/executor/executor_test.go b/executor/executor_test.go index ad55ff88..8995ca6f 100644 --- a/executor/executor_test.go +++ b/executor/executor_test.go @@ -16,7 +16,7 @@ import ( "github.com/go-vela/worker/executor/linux" "github.com/go-vela/worker/executor/local" - "github.com/go-vela/pkg-runtime/runtime/docker" + "github.com/go-vela/worker/runtime/docker" "github.com/go-vela/sdk-go/vela" diff --git a/executor/linux/build_test.go b/executor/linux/build_test.go index ad937865..0cc4a4b0 100644 --- a/executor/linux/build_test.go +++ b/executor/linux/build_test.go @@ -14,7 +14,7 @@ import ( "github.com/go-vela/mock/server" "github.com/urfave/cli/v2" - "github.com/go-vela/pkg-runtime/runtime/docker" + "github.com/go-vela/worker/runtime/docker" "github.com/go-vela/sdk-go/vela" diff --git a/executor/linux/driver_test.go b/executor/linux/driver_test.go index a1e826b3..02b60e25 100644 --- a/executor/linux/driver_test.go +++ b/executor/linux/driver_test.go @@ -11,9 +11,9 @@ import ( "github.com/gin-gonic/gin" "github.com/go-vela/mock/server" - "github.com/go-vela/pkg-runtime/runtime/docker" "github.com/go-vela/sdk-go/vela" "github.com/go-vela/types/constants" + "github.com/go-vela/worker/runtime/docker" ) func TestLinux_Driver(t *testing.T) { diff --git a/executor/linux/linux.go b/executor/linux/linux.go index 80d020e8..fea5eeea 100644 --- a/executor/linux/linux.go +++ b/executor/linux/linux.go @@ -7,7 +7,7 @@ package linux import ( "sync" - "github.com/go-vela/pkg-runtime/runtime" + "github.com/go-vela/worker/runtime" "github.com/go-vela/sdk-go/vela" diff --git a/executor/linux/linux_test.go b/executor/linux/linux_test.go index 073ce616..c6541bde 100644 --- a/executor/linux/linux_test.go +++ b/executor/linux/linux_test.go @@ -13,7 +13,7 @@ import ( "github.com/go-vela/mock/server" "github.com/go-vela/types" - "github.com/go-vela/pkg-runtime/runtime/docker" + "github.com/go-vela/worker/runtime/docker" "github.com/go-vela/sdk-go/vela" diff --git a/executor/linux/opts.go b/executor/linux/opts.go index 179cafdd..8dcd266b 100644 --- a/executor/linux/opts.go +++ b/executor/linux/opts.go @@ -7,7 +7,7 @@ package linux import ( "fmt" - "github.com/go-vela/pkg-runtime/runtime" + "github.com/go-vela/worker/runtime" "github.com/go-vela/sdk-go/vela" diff --git a/executor/linux/opts_test.go b/executor/linux/opts_test.go index 1894e0d6..4e25e2fd 100644 --- a/executor/linux/opts_test.go +++ b/executor/linux/opts_test.go @@ -13,8 +13,8 @@ import ( "github.com/go-vela/mock/server" - "github.com/go-vela/pkg-runtime/runtime" - "github.com/go-vela/pkg-runtime/runtime/docker" + "github.com/go-vela/worker/runtime" + "github.com/go-vela/worker/runtime/docker" "github.com/go-vela/sdk-go/vela" diff --git a/executor/linux/secret_test.go b/executor/linux/secret_test.go index a1bcd3b6..a9eed01e 100644 --- a/executor/linux/secret_test.go +++ b/executor/linux/secret_test.go @@ -17,7 +17,7 @@ import ( "github.com/go-vela/compiler/compiler/native" "github.com/go-vela/mock/server" - "github.com/go-vela/pkg-runtime/runtime/docker" + "github.com/go-vela/worker/runtime/docker" "github.com/go-vela/sdk-go/vela" diff --git a/executor/linux/service_test.go b/executor/linux/service_test.go index 5b39eba7..bec3f492 100644 --- a/executor/linux/service_test.go +++ b/executor/linux/service_test.go @@ -13,7 +13,7 @@ import ( "github.com/go-vela/mock/server" - "github.com/go-vela/pkg-runtime/runtime/docker" + "github.com/go-vela/worker/runtime/docker" "github.com/go-vela/sdk-go/vela" diff --git a/executor/linux/stage_test.go b/executor/linux/stage_test.go index 8b7084b5..469ffc27 100644 --- a/executor/linux/stage_test.go +++ b/executor/linux/stage_test.go @@ -18,7 +18,7 @@ import ( "github.com/go-vela/compiler/compiler/native" "github.com/go-vela/mock/server" - "github.com/go-vela/pkg-runtime/runtime/docker" + "github.com/go-vela/worker/runtime/docker" "github.com/go-vela/sdk-go/vela" diff --git a/executor/linux/step_test.go b/executor/linux/step_test.go index 314ca550..e5dd7a89 100644 --- a/executor/linux/step_test.go +++ b/executor/linux/step_test.go @@ -13,7 +13,7 @@ import ( "github.com/go-vela/mock/server" - "github.com/go-vela/pkg-runtime/runtime/docker" + "github.com/go-vela/worker/runtime/docker" "github.com/go-vela/sdk-go/vela" diff --git a/executor/local/build_test.go b/executor/local/build_test.go index 527f1748..f1249bb8 100644 --- a/executor/local/build_test.go +++ b/executor/local/build_test.go @@ -12,7 +12,7 @@ import ( "github.com/go-vela/compiler/compiler/native" "github.com/urfave/cli/v2" - "github.com/go-vela/pkg-runtime/runtime/docker" + "github.com/go-vela/worker/runtime/docker" ) func TestLocal_CreateBuild(t *testing.T) { diff --git a/executor/local/driver_test.go b/executor/local/driver_test.go index 23a43c92..4f19c178 100644 --- a/executor/local/driver_test.go +++ b/executor/local/driver_test.go @@ -8,8 +8,8 @@ import ( "reflect" "testing" - "github.com/go-vela/pkg-runtime/runtime/docker" "github.com/go-vela/types/constants" + "github.com/go-vela/worker/runtime/docker" ) func TestLocal_Driver(t *testing.T) { diff --git a/executor/local/local.go b/executor/local/local.go index 67668744..801b7ce8 100644 --- a/executor/local/local.go +++ b/executor/local/local.go @@ -7,10 +7,10 @@ package local import ( "sync" - "github.com/go-vela/pkg-runtime/runtime" "github.com/go-vela/sdk-go/vela" "github.com/go-vela/types/library" "github.com/go-vela/types/pipeline" + "github.com/go-vela/worker/runtime" ) type ( diff --git a/executor/local/local_test.go b/executor/local/local_test.go index b21ed525..0d000e1d 100644 --- a/executor/local/local_test.go +++ b/executor/local/local_test.go @@ -12,7 +12,7 @@ import ( "github.com/go-vela/mock/server" - "github.com/go-vela/pkg-runtime/runtime/docker" + "github.com/go-vela/worker/runtime/docker" "github.com/go-vela/sdk-go/vela" diff --git a/executor/local/opts.go b/executor/local/opts.go index b3e69be2..27e63129 100644 --- a/executor/local/opts.go +++ b/executor/local/opts.go @@ -7,7 +7,7 @@ package local import ( "fmt" - "github.com/go-vela/pkg-runtime/runtime" + "github.com/go-vela/worker/runtime" "github.com/go-vela/sdk-go/vela" diff --git a/executor/local/opts_test.go b/executor/local/opts_test.go index 2d5b57ee..6fa5f1d7 100644 --- a/executor/local/opts_test.go +++ b/executor/local/opts_test.go @@ -13,8 +13,8 @@ import ( "github.com/go-vela/mock/server" - "github.com/go-vela/pkg-runtime/runtime" - "github.com/go-vela/pkg-runtime/runtime/docker" + "github.com/go-vela/worker/runtime" + "github.com/go-vela/worker/runtime/docker" "github.com/go-vela/sdk-go/vela" diff --git a/executor/local/service_test.go b/executor/local/service_test.go index 581a1ac2..e3e1c784 100644 --- a/executor/local/service_test.go +++ b/executor/local/service_test.go @@ -8,7 +8,7 @@ import ( "context" "testing" - "github.com/go-vela/pkg-runtime/runtime/docker" + "github.com/go-vela/worker/runtime/docker" "github.com/go-vela/types/library" "github.com/go-vela/types/pipeline" diff --git a/executor/local/stage_test.go b/executor/local/stage_test.go index ec1f04c9..0f10daca 100644 --- a/executor/local/stage_test.go +++ b/executor/local/stage_test.go @@ -15,7 +15,7 @@ import ( "github.com/go-vela/compiler/compiler/native" - "github.com/go-vela/pkg-runtime/runtime/docker" + "github.com/go-vela/worker/runtime/docker" "github.com/go-vela/types/pipeline" ) diff --git a/executor/local/step_test.go b/executor/local/step_test.go index e6f08ae8..6d94dbc2 100644 --- a/executor/local/step_test.go +++ b/executor/local/step_test.go @@ -8,7 +8,7 @@ import ( "context" "testing" - "github.com/go-vela/pkg-runtime/runtime/docker" + "github.com/go-vela/worker/runtime/docker" "github.com/go-vela/types/library" "github.com/go-vela/types/pipeline" diff --git a/executor/setup.go b/executor/setup.go index bedbd415..112b6214 100644 --- a/executor/setup.go +++ b/executor/setup.go @@ -13,7 +13,7 @@ import ( "github.com/go-vela/worker/executor/linux" "github.com/go-vela/worker/executor/local" - "github.com/go-vela/pkg-runtime/runtime" + "github.com/go-vela/worker/runtime" "github.com/go-vela/types/constants" "github.com/go-vela/types/library" diff --git a/executor/setup_test.go b/executor/setup_test.go index c56496cc..c39b5d33 100644 --- a/executor/setup_test.go +++ b/executor/setup_test.go @@ -16,7 +16,7 @@ import ( "github.com/go-vela/worker/executor/linux" "github.com/go-vela/worker/executor/local" - "github.com/go-vela/pkg-runtime/runtime/docker" + "github.com/go-vela/worker/runtime/docker" "github.com/go-vela/sdk-go/vela" diff --git a/go.mod b/go.mod index d3055291..7aeba04d 100644 --- a/go.mod +++ b/go.mod @@ -4,18 +4,28 @@ go 1.16 require ( github.com/Masterminds/semver/v3 v3.1.1 + github.com/buildkite/yaml v0.0.0-20181016232759-0caa5f0796e3 github.com/docker/distribution v2.7.1+incompatible + github.com/docker/docker v20.10.9+incompatible + github.com/docker/go-units v0.4.0 + github.com/fatih/color v1.10.0 // indirect github.com/gin-gonic/gin v1.7.4 github.com/go-vela/compiler v0.10.0 github.com/go-vela/mock v0.10.0 github.com/go-vela/pkg-queue v0.10.0 - github.com/go-vela/pkg-runtime v0.10.1-0.20211025172651-7d29320dd785 github.com/go-vela/sdk-go v0.10.0 github.com/go-vela/types v0.10.0 github.com/google/go-cmp v0.5.6 + github.com/google/gofuzz v1.2.0 // indirect + github.com/google/uuid v1.2.0 // indirect + github.com/hashicorp/go-hclog v0.10.0 // indirect github.com/joho/godotenv v1.4.0 github.com/prometheus/client_golang v1.11.0 github.com/sirupsen/logrus v1.8.1 github.com/urfave/cli/v2 v2.3.0 golang.org/x/sync v0.0.0-20210220032951-036812b2e83c + gotest.tools/v3 v3.0.3 + k8s.io/api v0.22.2 + k8s.io/apimachinery v0.22.2 + k8s.io/client-go v0.22.2 ) diff --git a/go.sum b/go.sum index 86a720f7..cf5d2ff6 100644 --- a/go.sum +++ b/go.sum @@ -167,8 +167,6 @@ github.com/go-vela/mock v0.10.0 h1:ZJs40xElnB4DNiQc+nEEeZS4Z0K/uXl6kGRpPlccuMY= github.com/go-vela/mock v0.10.0/go.mod h1:TihYvb+NBiKXgcsBIpARU9H00rzrLAhFQvsRkzUqDxc= github.com/go-vela/pkg-queue v0.10.0 h1:cxpkyVuX+ZJuF9t7XEQuHOFBa776SNgraEsFpnWI03E= github.com/go-vela/pkg-queue v0.10.0/go.mod h1:ZtkPoazVfpKK/ePdea/2s2LpNWDrc19nqmn1hPI3jxY= -github.com/go-vela/pkg-runtime v0.10.1-0.20211025172651-7d29320dd785 h1:VByvQACNppX/P74CfgSr+E0+HuOCMXnDHZYFiF9HdCs= -github.com/go-vela/pkg-runtime v0.10.1-0.20211025172651-7d29320dd785/go.mod h1:7KV1aAufGNpGbbLLsD7W7z8gc50fXdIK03NPM+SClg8= github.com/go-vela/sdk-go v0.10.0 h1:monESdM738WeY2MKlj0COGK0W/f1PIGwp8K4tClfLlo= github.com/go-vela/sdk-go v0.10.0/go.mod h1:LGHpZezP0+KBb3OX9Mf5rGXK1dS7Ms8kWCHb8bWzOYc= github.com/go-vela/types v0.10.0-rc3/go.mod h1:6taTlivaC0wDwDJVlc8sBaVZToyzkyDMtGUYIAfgA9M= @@ -351,13 +349,11 @@ github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= -github.com/onsi/ginkgo v1.14.2/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc= github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= -github.com/onsi/gomega v1.10.4/go.mod h1:g/HbgYopi++010VEqkFgJHKC09uJiW9UkXvMUuKHUCQ= github.com/onsi/gomega v1.16.0 h1:6gjqkI8iiRHMvdccRJM8rVKjCWk6ZIm6FTm3ddIe4/c= github.com/onsi/gomega v1.16.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= @@ -522,7 +518,6 @@ golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81R golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= golang.org/x/net v0.0.0-20210520170846-37e1c6afe023/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= diff --git a/router/middleware/executor/executor_test.go b/router/middleware/executor/executor_test.go index cf703562..516ebe8f 100644 --- a/router/middleware/executor/executor_test.go +++ b/router/middleware/executor/executor_test.go @@ -12,12 +12,12 @@ import ( "github.com/gin-gonic/gin" - "github.com/go-vela/pkg-runtime/runtime/docker" "github.com/go-vela/sdk-go/vela" "github.com/go-vela/types/constants" "github.com/go-vela/types/library" "github.com/go-vela/types/pipeline" "github.com/go-vela/worker/executor" + "github.com/go-vela/worker/runtime/docker" ) func TestExecutor_Retrieve(t *testing.T) { diff --git a/runtime/context.go b/runtime/context.go new file mode 100644 index 00000000..f85d972d --- /dev/null +++ b/runtime/context.go @@ -0,0 +1,67 @@ +// Copyright (c) 2021 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package runtime + +import ( + "context" + + "github.com/gin-gonic/gin" +) + +// key defines the key type for storing +// the runtime Engine in the context. +const key = "runtime" + +// FromContext retrieves the runtime Engine from the context.Context. +func FromContext(c context.Context) Engine { + // get runtime value from context.Context + v := c.Value(key) + if v == nil { + return nil + } + + // cast runtime value to expected Engine type + e, ok := v.(Engine) + if !ok { + return nil + } + + return e +} + +// FromGinContext retrieves the runtime Engine from the gin.Context. +func FromGinContext(c *gin.Context) Engine { + // get runtime value from gin.Context + // + // https://pkg.go.dev/github.com/gin-gonic/gin?tab=doc#Context.Get + v, ok := c.Get(key) + if !ok { + return nil + } + + // cast runtime value to expected Engine type + e, ok := v.(Engine) + if !ok { + return nil + } + + return e +} + +// WithContext inserts the runtime Engine into the context.Context. +func WithContext(c context.Context, e Engine) context.Context { + // set the runtime Engine in the context.Context + // + // nolint: golint,staticcheck // ignore using string with context value + return context.WithValue(c, key, e) +} + +// WithGinContext inserts the runtime Engine into the gin.Context. +func WithGinContext(c *gin.Context, e Engine) { + // set the runtime Engine in the gin.Context + // + // https://pkg.go.dev/github.com/gin-gonic/gin?tab=doc#Context.Set + c.Set(key, e) +} diff --git a/runtime/context_test.go b/runtime/context_test.go new file mode 100644 index 00000000..1ae77b45 --- /dev/null +++ b/runtime/context_test.go @@ -0,0 +1,142 @@ +// Copyright (c) 2021 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package runtime + +import ( + "context" + "reflect" + "testing" + + "github.com/gin-gonic/gin" + + "github.com/go-vela/types/constants" +) + +func TestRuntime_FromContext(t *testing.T) { + // setup types + _engine, err := New(&Setup{ + Driver: constants.DriverDocker, + }) + if err != nil { + t.Errorf("unable to create runtime engine: %v", err) + } + + // setup tests + tests := []struct { + context context.Context + want Engine + }{ + { + // nolint: golint,staticcheck // ignore using string with context value + context: context.WithValue(context.Background(), key, _engine), + want: _engine, + }, + { + context: context.Background(), + want: nil, + }, + { + // nolint: golint,staticcheck // ignore using string with context value + context: context.WithValue(context.Background(), key, "foo"), + want: nil, + }, + } + + // run tests + for _, test := range tests { + got := FromContext(test.context) + + if !reflect.DeepEqual(got, test.want) { + t.Errorf("FromContext is %v, want %v", got, test.want) + } + } +} + +func TestRuntime_FromGinContext(t *testing.T) { + // setup types + _engine, err := New(&Setup{ + Driver: constants.DriverDocker, + }) + if err != nil { + t.Errorf("unable to create runtime engine: %v", err) + } + + // setup tests + tests := []struct { + context *gin.Context + value interface{} + want Engine + }{ + { + context: new(gin.Context), + value: _engine, + want: _engine, + }, + { + context: new(gin.Context), + value: nil, + want: nil, + }, + { + context: new(gin.Context), + value: "foo", + want: nil, + }, + } + + // run tests + for _, test := range tests { + if test.value != nil { + test.context.Set(key, test.value) + } + + got := FromGinContext(test.context) + + if !reflect.DeepEqual(got, test.want) { + t.Errorf("FromGinContext is %v, want %v", got, test.want) + } + } +} + +func TestRuntime_WithContext(t *testing.T) { + // setup types + _engine, err := New(&Setup{ + Driver: constants.DriverDocker, + }) + if err != nil { + t.Errorf("unable to create runtime engine: %v", err) + } + + // nolint: golint,staticcheck // ignore using string with context value + want := context.WithValue(context.Background(), key, _engine) + + // run test + got := WithContext(context.Background(), _engine) + + if !reflect.DeepEqual(got, want) { + t.Errorf("WithContext is %v, want %v", got, want) + } +} + +func TestRuntime_WithGinContext(t *testing.T) { + // setup types + _engine, err := New(&Setup{ + Driver: constants.DriverDocker, + }) + if err != nil { + t.Errorf("unable to create runtime engine: %v", err) + } + + want := new(gin.Context) + want.Set(key, _engine) + + // run test + got := new(gin.Context) + WithGinContext(got, _engine) + + if !reflect.DeepEqual(got, want) { + t.Errorf("WithGinContext is %v, want %v", got, want) + } +} diff --git a/runtime/doc.go b/runtime/doc.go new file mode 100644 index 00000000..a0024770 --- /dev/null +++ b/runtime/doc.go @@ -0,0 +1,17 @@ +// Copyright (c) 2021 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +// Package runtime provides the ability for Vela to +// integrate with different supported Runtime +// environments. +// +// Currently the following runtimes are supported: +// +// * Docker - https://docker.io/ +// * Kubernetes - https://kubernetes.io/ +// +// Usage: +// +// import "github.com/go-vela/worker/runtime" +package runtime diff --git a/runtime/docker/build.go b/runtime/docker/build.go new file mode 100644 index 00000000..a468bc67 --- /dev/null +++ b/runtime/docker/build.go @@ -0,0 +1,45 @@ +// Copyright (c) 2021 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package docker + +import ( + "context" + + "github.com/go-vela/types/pipeline" + + "github.com/sirupsen/logrus" +) + +// InspectBuild displays details about the pod for the init step. +// This is a no-op for docker. +func (c *client) InspectBuild(ctx context.Context, b *pipeline.Build) ([]byte, error) { + logrus.Tracef("no-op: inspecting build for pipeline %s", b.ID) + + return []byte{}, nil +} + +// SetupBuild prepares the pipeline build. +// This is a no-op for docker. +func (c *client) SetupBuild(ctx context.Context, b *pipeline.Build) error { + logrus.Tracef("no-op: setting up for build %s", b.ID) + + return nil +} + +// AssembleBuild finalizes pipeline build setup. +// This is a no-op for docker. +func (c *client) AssembleBuild(ctx context.Context, b *pipeline.Build) error { + logrus.Tracef("no-op: assembling build %s", b.ID) + + return nil +} + +// RemoveBuild deletes (kill, remove) the pipeline build metadata. +// This is a no-op for docker. +func (c *client) RemoveBuild(ctx context.Context, b *pipeline.Build) error { + logrus.Tracef("no-op: removing build %s", b.ID) + + return nil +} diff --git a/runtime/docker/build_test.go b/runtime/docker/build_test.go new file mode 100644 index 00000000..4ac26f25 --- /dev/null +++ b/runtime/docker/build_test.go @@ -0,0 +1,154 @@ +// Copyright (c) 2021 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package docker + +import ( + "context" + "testing" + + "github.com/go-vela/types/pipeline" +) + +func TestDocker_InspectBuild(t *testing.T) { + // setup Docker + _engine, err := NewMock() + if err != nil { + t.Errorf("unable to create runtime engine: %v", err) + } + + // setup tests + tests := []struct { + failure bool + pipeline *pipeline.Build + }{ + { + failure: false, + pipeline: _pipeline, + }, + } + + // run tests + for _, test := range tests { + _, err = _engine.InspectBuild(context.Background(), test.pipeline) + + if test.failure { + if err == nil { + t.Errorf("InspectBuild should have returned err") + } + + continue + } + + if err != nil { + t.Errorf("InspectBuild returned err: %v", err) + } + } +} + +func TestDocker_SetupBuild(t *testing.T) { + // setup Docker + _engine, err := NewMock() + if err != nil { + t.Errorf("unable to create runtime engine: %v", err) + } + + // setup tests + tests := []struct { + failure bool + pipeline *pipeline.Build + }{ + { + failure: false, + pipeline: _pipeline, + }, + } + + // run tests + for _, test := range tests { + err = _engine.SetupBuild(context.Background(), test.pipeline) + + if test.failure { + if err == nil { + t.Errorf("SetupBuild should have returned err") + } + + continue + } + + if err != nil { + t.Errorf("SetupBuild returned err: %v", err) + } + } +} + +func TestDocker_AssembleBuild(t *testing.T) { + // setup tests + tests := []struct { + failure bool + pipeline *pipeline.Build + }{ + { + failure: false, + pipeline: _pipeline, + }, + } + + // run tests + for _, test := range tests { + _engine, err := NewMock() + if err != nil { + t.Errorf("unable to create runtime engine: %v", err) + } + + err = _engine.AssembleBuild(context.Background(), test.pipeline) + + if test.failure { + if err == nil { + t.Errorf("AssembleBuild should have returned err") + } + + continue + } + + if err != nil { + t.Errorf("AssembleBuild returned err: %v", err) + } + } +} + +func TestDocker_RemoveBuild(t *testing.T) { + // setup tests + tests := []struct { + failure bool + pipeline *pipeline.Build + }{ + { + failure: false, + pipeline: _pipeline, + }, + } + + // run tests + for _, test := range tests { + _engine, err := NewMock() + if err != nil { + t.Errorf("unable to create runtime engine: %v", err) + } + + err = _engine.RemoveBuild(context.Background(), test.pipeline) + + if test.failure { + if err == nil { + t.Errorf("RemoveBuild should have returned err") + } + + continue + } + + if err != nil { + t.Errorf("RemoveBuild returned err: %v", err) + } + } +} diff --git a/runtime/docker/container.go b/runtime/docker/container.go new file mode 100644 index 00000000..fafb53fe --- /dev/null +++ b/runtime/docker/container.go @@ -0,0 +1,355 @@ +// Copyright (c) 2021 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package docker + +import ( + "context" + "fmt" + "io" + "strings" + + "github.com/go-vela/types/constants" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/container" + docker "github.com/docker/docker/client" + "github.com/docker/docker/pkg/stdcopy" + + "github.com/go-vela/types/pipeline" + "github.com/go-vela/worker/internal/image" + + "github.com/sirupsen/logrus" +) + +// InspectContainer inspects the pipeline container. +func (c *client) InspectContainer(ctx context.Context, ctn *pipeline.Container) error { + logrus.Tracef("inspecting container %s", ctn.ID) + + // send API call to inspect the container + // + // https://godoc.org/github.com/docker/docker/client#Client.ContainerInspect + container, err := c.Docker.ContainerInspect(ctx, ctn.ID) + if err != nil { + return err + } + + // capture the container exit code + // + // https://godoc.org/github.com/docker/docker/api/types#ContainerState + ctn.ExitCode = container.State.ExitCode + + return nil +} + +// RemoveContainer deletes (kill, remove) the pipeline container. +func (c *client) RemoveContainer(ctx context.Context, ctn *pipeline.Container) error { + logrus.Tracef("removing container %s", ctn.ID) + + // send API call to inspect the container + // + // https://godoc.org/github.com/docker/docker/client#Client.ContainerInspect + container, err := c.Docker.ContainerInspect(ctx, ctn.ID) + if err != nil { + return err + } + + // if the container is paused, restarting or running + // + // https://godoc.org/github.com/docker/docker/api/types#ContainerState + if container.State.Paused || + container.State.Restarting || + container.State.Running { + // send API call to kill the container + // + // https://godoc.org/github.com/docker/docker/client#Client.ContainerKill + err := c.Docker.ContainerKill(ctx, ctn.ID, "SIGKILL") + if err != nil { + return err + } + } + + // create options for removing container + // + // https://godoc.org/github.com/docker/docker/api/types#ContainerRemoveOptions + opts := types.ContainerRemoveOptions{ + Force: true, + RemoveLinks: false, + RemoveVolumes: true, + } + + // send API call to remove the container + // + // https://godoc.org/github.com/docker/docker/client#Client.ContainerRemove + err = c.Docker.ContainerRemove(ctx, ctn.ID, opts) + if err != nil { + return err + } + + return nil +} + +// RunContainer creates and starts the pipeline container. +// +// nolint: lll // ignore long line length due to variable names +func (c *client) RunContainer(ctx context.Context, ctn *pipeline.Container, b *pipeline.Build) error { + logrus.Tracef("running container %s", ctn.ID) + + // allocate new container config from pipeline container + containerConf := ctnConfig(ctn) + // allocate new host config with volume data + hostConf := hostConfig(b.ID, ctn.Ulimits, c.config.Volumes) + // allocate new network config with container name + networkConf := netConfig(b.ID, ctn.Name) + + // -------------------- Start of TODO: -------------------- + // + // Remove the below code once the mounting issue with Kaniko is + // resolved to allow mounting private cert bundles with Vela. + // + // This code is required due to a known bug in Kaniko: + // + // * https://github.com/go-vela/community/issues/253 + + // check if the pipeline container image contains + // the key words "kaniko" and "vela" + // + // this is a soft check for the Vela Kaniko plugin + if strings.Contains(ctn.Image, "kaniko") && + strings.Contains(ctn.Image, "vela") { + // iterate through the list of host mounts provided + for i, mount := range hostConf.Mounts { + // check if the source path or target path + // for the mount contains "/etc/ssl/certs" + // + // this is a soft check for mounting private cert bundles + if strings.Contains(mount.Source, "/etc/ssl/certs") || + strings.Contains(mount.Target, "/etc/ssl/certs") { + // remove the private cert bundle mount from the host config + hostConf.Mounts = append(hostConf.Mounts[:i], hostConf.Mounts[i+1:]...) + } + } + } + // + // -------------------- End of TODO: -------------------- + + // check if the container pull policy is on_start + if strings.EqualFold(ctn.Pull, constants.PullOnStart) { + // send API call to create the image + err := c.CreateImage(ctx, ctn) + if err != nil { + return err + } + } + + // check if the image is allowed to run privileged + for _, pattern := range c.config.Images { + privileged, err := image.IsPrivilegedImage(ctn.Image, pattern) + if err != nil { + return err + } + + if privileged { + hostConf.Privileged = true + } + } + + // send API call to create the container + // + // https://godoc.org/github.com/docker/docker/client#Client.ContainerCreate + _, err := c.Docker.ContainerCreate( + ctx, + containerConf, + hostConf, + networkConf, + nil, + ctn.ID, + ) + if err != nil { + return err + } + + // create options for starting container + // + // https://godoc.org/github.com/docker/docker/api/types#ContainerStartOptions + opts := types.ContainerStartOptions{} + + // send API call to start the container + // + // https://godoc.org/github.com/docker/docker/client#Client.ContainerStart + err = c.Docker.ContainerStart(ctx, ctn.ID, opts) + if err != nil { + return err + } + + return nil +} + +// SetupContainer prepares the image for the pipeline container. +func (c *client) SetupContainer(ctx context.Context, ctn *pipeline.Container) error { + logrus.Tracef("setting up for container %s", ctn.ID) + + // handle the container pull policy + switch ctn.Pull { + case constants.PullAlways: + // send API call to create the image + return c.CreateImage(ctx, ctn) + case constants.PullNotPresent: + // handled further down in this function + break + case constants.PullNever: + fallthrough + case constants.PullOnStart: + fallthrough + default: + logrus.Tracef("skipping setup for container %s due to pull policy %s", ctn.ID, ctn.Pull) + + return nil + } + + // parse image from container + // + // https://pkg.go.dev/github.com/go-vela/worker/internal/image#ParseWithError + _image, err := image.ParseWithError(ctn.Image) + if err != nil { + return err + } + + // check if the container image exists on the host + // + // https://godoc.org/github.com/docker/docker/client#Client.ImageInspectWithRaw + _, _, err = c.Docker.ImageInspectWithRaw(ctx, _image) + if err == nil { + return nil + } + + // if the container image does not exist on the host + // we attempt to capture it for executing the pipeline + // + // https://godoc.org/github.com/docker/docker/client#IsErrNotFound + if docker.IsErrNotFound(err) { + // send API call to create the image + return c.CreateImage(ctx, ctn) + } + + return err +} + +// TailContainer captures the logs for the pipeline container. +// +// nolint: lll // ignore long line length due to variable names +func (c *client) TailContainer(ctx context.Context, ctn *pipeline.Container) (io.ReadCloser, error) { + logrus.Tracef("tailing output for container %s", ctn.ID) + + // create options for capturing container logs + // + // https://godoc.org/github.com/docker/docker/api/types#ContainerLogsOptions + opts := types.ContainerLogsOptions{ + Follow: true, + ShowStdout: true, + ShowStderr: true, + Details: false, + Timestamps: false, + } + + // send API call to capture the container logs + // + // https://godoc.org/github.com/docker/docker/client#Client.ContainerLogs + logs, err := c.Docker.ContainerLogs(ctx, ctn.ID, opts) + if err != nil { + return nil, err + } + + // create in-memory pipe for capturing logs + rc, wc := io.Pipe() + + // capture all stdout and stderr logs + go func() { + logrus.Tracef("copying logs for container %s", ctn.ID) + + // copy container stdout and stderr logs to our in-memory pipe + // + // https://godoc.org/github.com/docker/docker/pkg/stdcopy#StdCopy + _, err := stdcopy.StdCopy(wc, wc, logs) + if err != nil { + logrus.Errorf("unable to copy logs for container: %v", err) + } + + // close logs buffer + logs.Close() + + // close in-memory pipe write closer + wc.Close() + }() + + return rc, nil +} + +// WaitContainer blocks until the pipeline container completes. +func (c *client) WaitContainer(ctx context.Context, ctn *pipeline.Container) error { + logrus.Tracef("waiting for container %s", ctn.ID) + + // send API call to wait for the container completion + // + // https://godoc.org/github.com/docker/docker/client#Client.ContainerWait + wait, errC := c.Docker.ContainerWait(ctx, ctn.ID, container.WaitConditionNotRunning) + + select { + case <-wait: + case err := <-errC: + return err + } + + return nil +} + +// ctnConfig is a helper function to +// generate the container config. +func ctnConfig(ctn *pipeline.Container) *container.Config { + logrus.Tracef("Creating container configuration for step %s", ctn.ID) + + // create container config object + // + // https://godoc.org/github.com/docker/docker/api/types/container#Config + config := &container.Config{ + Image: image.Parse(ctn.Image), + WorkingDir: ctn.Directory, + AttachStdin: false, + AttachStdout: true, + AttachStderr: true, + Tty: false, + OpenStdin: false, + StdinOnce: false, + ArgsEscaped: false, + } + + // check if the environment is provided + if len(ctn.Environment) > 0 { + // iterate through each element in the container environment + for k, v := range ctn.Environment { + // add key/value environment to container config + config.Env = append(config.Env, fmt.Sprintf("%s=%s", k, v)) + } + } + + // check if the entrypoint is provided + if len(ctn.Entrypoint) > 0 { + // add entrypoint to container config + config.Entrypoint = ctn.Entrypoint + } + + // check if the commands are provided + if len(ctn.Commands) > 0 { + // add commands to container config + config.Cmd = ctn.Commands + } + + // check if the user is present + if len(ctn.User) > 0 { + // add user to container config + config.User = ctn.User + } + + return config +} diff --git a/runtime/docker/container_test.go b/runtime/docker/container_test.go new file mode 100644 index 00000000..24fb4063 --- /dev/null +++ b/runtime/docker/container_test.go @@ -0,0 +1,389 @@ +// Copyright (c) 2021 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package docker + +import ( + "context" + "testing" + + "github.com/go-vela/types/pipeline" +) + +func TestDocker_InspectContainer(t *testing.T) { + // setup Docker + _engine, err := NewMock() + if err != nil { + t.Errorf("unable to create runtime engine: %v", err) + } + + // setup tests + tests := []struct { + failure bool + container *pipeline.Container + }{ + { + failure: false, + container: _container, + }, + { + failure: true, + container: new(pipeline.Container), + }, + } + + // run tests + for _, test := range tests { + err = _engine.InspectContainer(context.Background(), test.container) + + if test.failure { + if err == nil { + t.Errorf("InspectContainer should have returned err") + } + + continue + } + + if err != nil { + t.Errorf("InspectContainer returned err: %v", err) + } + } +} + +func TestDocker_RemoveContainer(t *testing.T) { + // setup Docker + _engine, err := NewMock() + if err != nil { + t.Errorf("unable to create runtime engine: %v", err) + } + + // setup tests + tests := []struct { + failure bool + container *pipeline.Container + }{ + { + failure: false, + container: _container, + }, + { + failure: true, + container: new(pipeline.Container), + }, + { + failure: true, + container: &pipeline.Container{ + ID: "step_github_octocat_1_ignorenotfound", + Directory: "/vela/src/github.com/octocat/helloworld", + Environment: map[string]string{"FOO": "bar"}, + Image: "target/vela-git:v0.4.0", + Name: "ignorenotfound", + Number: 2, + Pull: "always", + }, + }, + } + + // run tests + for _, test := range tests { + err = _engine.RemoveContainer(context.Background(), test.container) + + if test.failure { + if err == nil { + t.Errorf("RemoveContainer should have returned err") + } + + continue + } + + if err != nil { + t.Errorf("RemoveContainer returned err: %v", err) + } + } +} + +func TestDocker_RunContainer(t *testing.T) { + // setup Docker + _engine, err := NewMock() + if err != nil { + t.Errorf("unable to create runtime engine: %v", err) + } + + // setup tests + tests := []struct { + failure bool + pipeline *pipeline.Build + container *pipeline.Container + volumes []string + }{ + { + failure: false, + pipeline: _pipeline, + container: _container, + }, + { + failure: false, + pipeline: _pipeline, + container: &pipeline.Container{ + ID: "step_github_octocat_1_echo", + Commands: []string{"echo", "hello"}, + Directory: "/vela/src/github.com/octocat/helloworld", + Environment: map[string]string{"FOO": "bar"}, + Entrypoint: []string{"/bin/sh", "-c"}, + Image: "alpine:latest", + Name: "echo", + Number: 2, + Pull: "always", + }, + }, + { + failure: false, + pipeline: _pipeline, + container: &pipeline.Container{ + ID: "step_github_octocat_1_echo", + Commands: []string{"echo", "hello"}, + Directory: "/vela/src/github.com/octocat/helloworld", + Environment: map[string]string{"FOO": "bar"}, + Entrypoint: []string{"/bin/sh", "-c"}, + Image: "target/vela-docker:latest", + Name: "echo", + Number: 2, + Pull: "always", + }, + }, + { + failure: false, + pipeline: _pipeline, + container: &pipeline.Container{ + ID: "step_github_octocat_1_echo", + Commands: []string{"echo", "hello"}, + Directory: "/vela/src/github.com/octocat/helloworld", + Environment: map[string]string{"FOO": "bar"}, + Entrypoint: []string{"/bin/sh", "-c"}, + Image: "target/vela-kaniko:latest", + Name: "echo", + Number: 2, + Pull: "always", + }, + volumes: []string{"/etc/ssl/certs/ca-certificates.crt:/etc/ssl/certs/ca-certificates.crt:rw"}, + }, + { + failure: true, + pipeline: _pipeline, + container: new(pipeline.Container), + }, + { + failure: true, + pipeline: _pipeline, + container: &pipeline.Container{ + ID: "step_github_octocat_1_ignorenotfound", + Directory: "/vela/src/github.com/octocat/helloworld", + Environment: map[string]string{"FOO": "bar"}, + Image: "target/vela-git:v0.4.0", + Name: "ignorenotfound", + Number: 2, + Pull: "always", + }, + }, + { + failure: true, + pipeline: _pipeline, + container: &pipeline.Container{ + ID: "step_github_octocat_1_ignorenotfound", + Directory: "/vela/src/github.com/octocat/helloworld", + Environment: map[string]string{"FOO": "bar"}, + Image: "target/vela-git:v0.4.0", + Name: "ignorenotfound", + Number: 2, + Pull: "always", + User: "foo", + }, + }, + } + + // run tests + for _, test := range tests { + if len(test.volumes) > 0 { + _engine.config.Volumes = test.volumes + } + + err = _engine.RunContainer(context.Background(), test.container, test.pipeline) + + if test.failure { + if err == nil { + t.Errorf("RunContainer should have returned err") + } + + continue + } + + if err != nil { + t.Errorf("RunContainer returned err: %v", err) + } + } +} + +func TestDocker_SetupContainer(t *testing.T) { + // setup Docker + _engine, err := NewMock() + if err != nil { + t.Errorf("unable to create runtime engine: %v", err) + } + + // setup tests + tests := []struct { + failure bool + container *pipeline.Container + }{ + { + failure: false, + container: _container, + }, + { + failure: false, + container: &pipeline.Container{ + ID: "step_github_octocat_1_clone", + Directory: "/vela/src/github.com/octocat/helloworld", + Environment: map[string]string{"FOO": "bar"}, + Image: "target/vela-git:v0.4.0", + Name: "clone", + Number: 2, + Pull: "not_present", + }, + }, + { + failure: false, + container: &pipeline.Container{ + ID: "step_github_octocat_1_clone", + Directory: "/vela/src/github.com/octocat/helloworld", + Environment: map[string]string{"FOO": "bar"}, + Image: "target/vela-git:ignorenotfound", + Name: "clone", + Number: 2, + Pull: "not_present", + }, + }, + { + failure: true, + container: &pipeline.Container{ + ID: "step_github_octocat_1_clone", + Directory: "/vela/src/github.com/octocat/helloworld", + Environment: map[string]string{"FOO": "bar"}, + Image: "target/vela-git:notfound", + Name: "clone", + Number: 2, + Pull: "always", + }, + }, + { + failure: true, + container: &pipeline.Container{ + ID: "step_github_octocat_1_clone", + Directory: "/vela/src/github.com/octocat/helloworld", + Environment: map[string]string{"FOO": "bar"}, + Image: "target/vela-git:notfound", + Name: "clone", + Number: 2, + Pull: "not_present", + }, + }, + } + + // run tests + for _, test := range tests { + err = _engine.SetupContainer(context.Background(), test.container) + + if test.failure { + if err == nil { + t.Errorf("SetupContainer should have returned err") + } + + continue + } + + if err != nil { + t.Errorf("SetupContainer returned err: %v", err) + } + } +} + +func TestDocker_TailContainer(t *testing.T) { + // setup Docker + _engine, err := NewMock() + if err != nil { + t.Errorf("unable to create runtime engine: %v", err) + } + + // setup tests + tests := []struct { + failure bool + container *pipeline.Container + }{ + { + failure: false, + container: _container, + }, + { + failure: true, + container: new(pipeline.Container), + }, + } + + // run tests + for _, test := range tests { + _, err = _engine.TailContainer(context.Background(), test.container) + + if test.failure { + if err == nil { + t.Errorf("TailContainer should have returned err") + } + + continue + } + + if err != nil { + t.Errorf("TailContainer returned err: %v", err) + } + } +} + +func TestDocker_WaitContainer(t *testing.T) { + // setup Docker + _engine, err := NewMock() + if err != nil { + t.Errorf("unable to create runtime engine: %v", err) + } + + // setup tests + tests := []struct { + failure bool + container *pipeline.Container + }{ + { + failure: false, + container: _container, + }, + { + failure: true, + container: new(pipeline.Container), + }, + } + + // run tests + for _, test := range tests { + err = _engine.WaitContainer(context.Background(), test.container) + + if test.failure { + if err == nil { + t.Errorf("WaitContainer should have returned err") + } + + continue + } + + if err != nil { + t.Errorf("WaitContainer returned err: %v", err) + } + } +} diff --git a/runtime/docker/doc.go b/runtime/docker/doc.go new file mode 100644 index 00000000..78dfe7b0 --- /dev/null +++ b/runtime/docker/doc.go @@ -0,0 +1,11 @@ +// Copyright (c) 2021 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +// Package docker provides the ability for Vela to +// integrate with Docker as a runtime environment. +// +// Usage: +// +// import "github.com/go-vela/worker/runtime/docker" +package docker diff --git a/runtime/docker/docker.go b/runtime/docker/docker.go new file mode 100644 index 00000000..8f18dc42 --- /dev/null +++ b/runtime/docker/docker.go @@ -0,0 +1,109 @@ +// Copyright (c) 2021 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package docker + +import ( + docker "github.com/docker/docker/client" + + mock "github.com/go-vela/mock/docker" +) + +// nolint: godot // ignore comment ending in a list +// +// Version represents the supported Docker API version for the mock. +// +// The Docker API version is pinned to ensure compatibility between the +// Docker API and client. The goal is to maintain n-1 compatibility. +// +// The maximum supported Docker API version for the client is here: +// +// https://docs.docker.com/engine/api/#api-version-matrix +// +// For example (use the compatibility matrix above for reference): +// +// * the Docker version of v20.10 has a maximum API version of v1.41 +// * to maintain n-1, the API version is pinned to v1.40 +const Version = "v1.40" + +type config struct { + // specifies a list of privileged images to use for the Docker client + Images []string + // specifies a list of host volumes to use for the Docker client + Volumes []string +} + +type client struct { + config *config + // https://godoc.org/github.com/docker/docker/client#CommonAPIClient + Docker docker.CommonAPIClient +} + +// New returns an Engine implementation that +// integrates with a Docker runtime. +// +// nolint: golint // ignore returning unexported client +func New(opts ...ClientOpt) (*client, error) { + // create new Docker client + c := new(client) + + // create new fields + c.config = new(config) + + // apply all provided configuration options + for _, opt := range opts { + err := opt(c) + if err != nil { + return nil, err + } + } + + // create new Docker client from environment + // + // https://godoc.org/github.com/docker/docker/client#NewClientWithOpts + _docker, err := docker.NewClientWithOpts(docker.FromEnv) + if err != nil { + return nil, err + } + + // pin version to ensure we know what Docker API version we're using + // + // typically this would be inherited from the host environment + // but this ensures the version of client being used + // + // https://godoc.org/github.com/docker/docker/client#WithVersion + _ = docker.WithVersion(Version)(_docker) + + // set the Docker client in the runtime client + c.Docker = _docker + + return c, nil +} + +// NewMock returns an Engine implementation that +// integrates with a mock Docker runtime. +// +// This function is intended for running tests only. +// +// nolint: golint // ignore returning unexported client +func NewMock(opts ...ClientOpt) (*client, error) { + // create new Docker runtime client + c, err := New(opts...) + if err != nil { + return nil, err + } + + // create Docker client from the mock client + // + // https://pkg.go.dev/github.com/go-vela/mock/docker#New + _docker, err := mock.New() + if err != nil { + return nil, err + } + + // set the Docker client in the runtime client + c.Docker = _docker + + return c, nil +} diff --git a/runtime/docker/docker_test.go b/runtime/docker/docker_test.go new file mode 100644 index 00000000..ba5d9c89 --- /dev/null +++ b/runtime/docker/docker_test.go @@ -0,0 +1,117 @@ +// Copyright (c) 2021 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package docker + +import ( + "testing" + + "github.com/go-vela/types/pipeline" + + "gotest.tools/v3/env" +) + +func TestDocker_New(t *testing.T) { + // setup tests + tests := []struct { + failure bool + envs map[string]string + }{ + { + failure: false, + envs: map[string]string{}, + }, + { + failure: true, + envs: map[string]string{ + "DOCKER_CERT_PATH": "invalid/path", + }, + }, + } + + // defer env cleanup + defer env.PatchAll(t, nil)() + + // run tests + for _, test := range tests { + // patch environment for tests + env.PatchAll(t, test.envs) + + _, err := New( + WithPrivilegedImages([]string{"alpine"}), + WithHostVolumes([]string{"/foo/bar.txt:/foo/bar.txt"}), + ) + + if test.failure { + if err == nil { + t.Errorf("New should have returned err") + } + + continue + } + + if err != nil { + t.Errorf("New returned err: %v", err) + } + } +} + +// setup global variables used for testing. +var ( + _container = &pipeline.Container{ + ID: "step_github_octocat_1_clone", + Directory: "/vela/src/github.com/octocat/helloworld", + Environment: map[string]string{"FOO": "bar"}, + Image: "target/vela-git:v0.4.0", + Name: "clone", + Number: 2, + Pull: "always", + } + + _pipeline = &pipeline.Build{ + Version: "1", + ID: "github_octocat_1", + Services: pipeline.ContainerSlice{ + { + ID: "service_github_octocat_1_postgres", + Directory: "/vela/src/github.com/octocat/helloworld", + Environment: map[string]string{"FOO": "bar"}, + Image: "postgres:12-alpine", + Name: "postgres", + Number: 1, + Ports: []string{"5432:5432"}, + }, + }, + Steps: pipeline.ContainerSlice{ + { + ID: "step_github_octocat_1_init", + Directory: "/vela/src/github.com/octocat/helloworld", + Environment: map[string]string{"FOO": "bar"}, + Image: "#init", + Name: "init", + Number: 1, + Pull: "always", + }, + { + ID: "step_github_octocat_1_clone", + Directory: "/vela/src/github.com/octocat/helloworld", + Environment: map[string]string{"FOO": "bar"}, + Image: "target/vela-git:v0.4.0", + Name: "clone", + Number: 2, + Pull: "always", + }, + { + ID: "step_github_octocat_1_echo", + Commands: []string{"echo hello"}, + Directory: "/vela/src/github.com/octocat/helloworld", + Environment: map[string]string{"FOO": "bar"}, + Image: "alpine:latest", + Name: "echo", + Number: 3, + Pull: "always", + }, + }, + } +) diff --git a/runtime/docker/driver.go b/runtime/docker/driver.go new file mode 100644 index 00000000..52e037d4 --- /dev/null +++ b/runtime/docker/driver.go @@ -0,0 +1,12 @@ +// Copyright (c) 2021 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package docker + +import "github.com/go-vela/types/constants" + +// Driver outputs the configured runtime driver. +func (c *client) Driver() string { + return constants.DriverDocker +} diff --git a/runtime/docker/driver_test.go b/runtime/docker/driver_test.go new file mode 100644 index 00000000..3ed51c78 --- /dev/null +++ b/runtime/docker/driver_test.go @@ -0,0 +1,29 @@ +// Copyright (c) 2021 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package docker + +import ( + "reflect" + "testing" + + "github.com/go-vela/types/constants" +) + +func TestDocker_Driver(t *testing.T) { + // setup types + want := constants.DriverDocker + + _engine, err := NewMock() + if err != nil { + t.Errorf("unable to create runtime engine: %v", err) + } + + // run tes + got := _engine.Driver() + + if !reflect.DeepEqual(got, want) { + t.Errorf("Driver is %v, want %v", got, want) + } +} diff --git a/runtime/docker/image.go b/runtime/docker/image.go new file mode 100644 index 00000000..43f8d816 --- /dev/null +++ b/runtime/docker/image.go @@ -0,0 +1,92 @@ +// Copyright (c) 2021 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package docker + +import ( + "context" + "fmt" + "io" + "os" + "strings" + + "github.com/docker/docker/api/types" + "github.com/go-vela/types/constants" + "github.com/go-vela/types/pipeline" + "github.com/go-vela/worker/internal/image" + + "github.com/sirupsen/logrus" +) + +// CreateImage creates the pipeline container image. +func (c *client) CreateImage(ctx context.Context, ctn *pipeline.Container) error { + logrus.Tracef("creating image for container %s", ctn.ID) + + // parse image from container + // + // https://pkg.go.dev/github.com/go-vela/worker/internal/image#ParseWithError + _image, err := image.ParseWithError(ctn.Image) + if err != nil { + return err + } + + // create options for pulling image + // + // https://godoc.org/github.com/docker/docker/api/types#ImagePullOptions + opts := types.ImagePullOptions{} + + // send API call to pull the image for the container + // + // https://godoc.org/github.com/docker/docker/client#Client.ImagePull + reader, err := c.Docker.ImagePull(ctx, _image, opts) + if err != nil { + return err + } + + defer reader.Close() + + // copy output from image pull to standard output + _, err = io.Copy(os.Stdout, reader) + if err != nil { + return err + } + + return nil +} + +// InspectImage inspects the pipeline container image. +func (c *client) InspectImage(ctx context.Context, ctn *pipeline.Container) ([]byte, error) { + logrus.Tracef("inspecting image for container %s", ctn.ID) + + // create output for inspecting image + output := []byte( + fmt.Sprintf("$ docker image inspect %s\n", ctn.Image), + ) + + // parse image from container + // + // https://pkg.go.dev/github.com/go-vela/worker/internal/image#ParseWithError + _image, err := image.ParseWithError(ctn.Image) + if err != nil { + return output, err + } + + // check if the container pull policy is on start + if strings.EqualFold(ctn.Pull, constants.PullOnStart) { + return []byte( + fmt.Sprintf("skipped for container %s due to pull policy %s\n", ctn.ID, ctn.Pull), + ), nil + } + + // send API call to inspect the image + // + // https://godoc.org/github.com/docker/docker/client#Client.ImageInspectWithRaw + i, _, err := c.Docker.ImageInspectWithRaw(ctx, _image) + if err != nil { + return output, err + } + + // add new line to end of bytes + return append(output, []byte(i.ID+"\n")...), nil +} diff --git a/runtime/docker/image_test.go b/runtime/docker/image_test.go new file mode 100644 index 00000000..cefa0757 --- /dev/null +++ b/runtime/docker/image_test.go @@ -0,0 +1,64 @@ +// Copyright (c) 2021 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package docker + +import ( + "context" + "testing" + + "github.com/go-vela/types/pipeline" +) + +func TestDocker_InspectImage(t *testing.T) { + // setup types + _engine, err := NewMock() + if err != nil { + t.Errorf("unable to create runtime engine: %v", err) + } + + // setup tests + tests := []struct { + failure bool + container *pipeline.Container + }{ + { + failure: false, + container: _container, + }, + { + failure: true, + container: new(pipeline.Container), + }, + { + failure: true, + container: &pipeline.Container{ + ID: "step_github_octocat_1_clone", + Directory: "/vela/src/github.com/octocat/helloworld", + Environment: map[string]string{"FOO": "bar"}, + Image: "target/vela-git:notfound", + Name: "clone", + Number: 2, + Pull: "always", + }, + }, + } + + // run tests + for _, test := range tests { + _, err = _engine.InspectImage(context.Background(), test.container) + + if test.failure { + if err == nil { + t.Errorf("InspectImage should have returned err") + } + + continue + } + + if err != nil { + t.Errorf("InspectImage returned err: %v", err) + } + } +} diff --git a/runtime/docker/network.go b/runtime/docker/network.go new file mode 100644 index 00000000..090adb28 --- /dev/null +++ b/runtime/docker/network.go @@ -0,0 +1,110 @@ +// Copyright (c) 2021 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package docker + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/network" + + "github.com/go-vela/types/pipeline" + + "github.com/sirupsen/logrus" +) + +// CreateNetwork creates the pipeline network. +func (c *client) CreateNetwork(ctx context.Context, b *pipeline.Build) error { + logrus.Tracef("creating network for pipeline %s", b.ID) + + // create options for creating network + // + // https://godoc.org/github.com/docker/docker/api/types#NetworkCreate + opts := types.NetworkCreate{ + Driver: "bridge", + } + + // send API call to create the network + // + // https://godoc.org/github.com/docker/docker/client#Client.NetworkCreate + _, err := c.Docker.NetworkCreate(ctx, b.ID, opts) + if err != nil { + return err + } + + return nil +} + +// InspectNetwork inspects the pipeline network. +func (c *client) InspectNetwork(ctx context.Context, b *pipeline.Build) ([]byte, error) { + logrus.Tracef("inspecting network for pipeline %s", b.ID) + + // create options for inspecting network + // + // https://godoc.org/github.com/docker/docker/api/types#NetworkInspectOptions + opts := types.NetworkInspectOptions{} + + // create output for inspecting network + output := []byte( + fmt.Sprintf("$ docker network inspect %s\n", b.ID), + ) + + // send API call to inspect the network + // + // https://godoc.org/github.com/docker/docker/client#Client.NetworkInspect + n, err := c.Docker.NetworkInspect(ctx, b.ID, opts) + if err != nil { + return output, err + } + + // convert network type NetworkResource to bytes with pretty print + // + // https://godoc.org/github.com/docker/docker/api/types#NetworkResource + network, err := json.MarshalIndent(n, "", " ") + if err != nil { + return output, err + } + + // add new line to end of bytes + return append(output, append(network, "\n"...)...), nil +} + +// RemoveNetwork deletes the pipeline network. +func (c *client) RemoveNetwork(ctx context.Context, b *pipeline.Build) error { + logrus.Tracef("removing network for pipeline %s", b.ID) + + // send API call to remove the network + // + // https://godoc.org/github.com/docker/docker/client#Client.NetworkRemove + err := c.Docker.NetworkRemove(ctx, b.ID) + if err != nil { + return err + } + + return nil +} + +// netConfig is a helper function to generate +// the network config for a container. +func netConfig(id, alias string) *network.NetworkingConfig { + endpoints := make(map[string]*network.EndpointSettings) + + // set pipeline id for endpoint with alias + // + // https://godoc.org/github.com/docker/docker/api/types/network#EndpointSettings + endpoints[id] = &network.EndpointSettings{ + NetworkID: id, + Aliases: []string{alias}, + } + + // return network config with configured endpoints + // + // https://godoc.org/github.com/docker/docker/api/types/network#NetworkingConfig + return &network.NetworkingConfig{ + EndpointsConfig: endpoints, + } +} diff --git a/runtime/docker/network_test.go b/runtime/docker/network_test.go new file mode 100644 index 00000000..9b8bbe56 --- /dev/null +++ b/runtime/docker/network_test.go @@ -0,0 +1,132 @@ +// Copyright (c) 2021 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package docker + +import ( + "context" + "testing" + + "github.com/go-vela/types/pipeline" +) + +func TestDocker_CreateNetwork(t *testing.T) { + // setup types + _engine, err := NewMock() + if err != nil { + t.Errorf("unable to create runtime engine: %v", err) + } + + // setup tests + tests := []struct { + failure bool + pipeline *pipeline.Build + }{ + { + failure: false, + pipeline: _pipeline, + }, + { + failure: true, + pipeline: new(pipeline.Build), + }, + } + + // run tests + for _, test := range tests { + err = _engine.CreateNetwork(context.Background(), test.pipeline) + + if test.failure { + if err == nil { + t.Errorf("CreateNetwork should have returned err") + } + + continue + } + + if err != nil { + t.Errorf("CreateNetwork returned err: %v", err) + } + } +} + +func TestDocker_InspectNetwork(t *testing.T) { + // setup types + _engine, err := NewMock() + if err != nil { + t.Errorf("unable to create runtime engine: %v", err) + } + + // setup tests + tests := []struct { + failure bool + pipeline *pipeline.Build + }{ + { + failure: false, + pipeline: _pipeline, + }, + { + failure: true, + pipeline: new(pipeline.Build), + }, + } + + // run tests + for _, test := range tests { + _, err = _engine.InspectNetwork(context.Background(), test.pipeline) + + if test.failure { + if err == nil { + t.Errorf("InspectNetwork should have returned err") + } + + continue + } + + if err != nil { + t.Errorf("InspectNetwork returned err: %v", err) + } + } +} + +func TestDocker_RemoveNetwork(t *testing.T) { + // setup types + _engine, err := NewMock() + if err != nil { + t.Errorf("unable to create runtime engine: %v", err) + } + + // setup tests + tests := []struct { + failure bool + pipeline *pipeline.Build + }{ + { + failure: false, + pipeline: _pipeline, + }, + { + failure: true, + pipeline: new(pipeline.Build), + }, + } + + // run tests + for _, test := range tests { + err = _engine.RemoveNetwork(context.Background(), test.pipeline) + + if test.failure { + if err == nil { + t.Errorf("RemoveNetwork should have returned err") + } + + continue + } + + if err != nil { + t.Errorf("RemoveNetwork returned err: %v", err) + } + } +} diff --git a/runtime/docker/opts.go b/runtime/docker/opts.go new file mode 100644 index 00000000..33dad050 --- /dev/null +++ b/runtime/docker/opts.go @@ -0,0 +1,36 @@ +// Copyright (c) 2021 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package docker + +import ( + "github.com/sirupsen/logrus" +) + +// ClientOpt represents a configuration option to initialize the runtime client. +type ClientOpt func(*client) error + +// WithPrivilegedImages sets the Docker privileged images in the runtime client. +func WithPrivilegedImages(images []string) ClientOpt { + logrus.Trace("configuring privileged images in docker runtime client") + + return func(c *client) error { + // set the runtime privileged images in the docker client + c.config.Images = images + + return nil + } +} + +// WithHostVolumes sets the Docker host volumes in the runtime client. +func WithHostVolumes(volumes []string) ClientOpt { + logrus.Trace("configuring host volumes in docker runtime client") + + return func(c *client) error { + // set the runtime host volumes in the docker client + c.config.Volumes = volumes + + return nil + } +} diff --git a/runtime/docker/opts_test.go b/runtime/docker/opts_test.go new file mode 100644 index 00000000..5970a293 --- /dev/null +++ b/runtime/docker/opts_test.go @@ -0,0 +1,74 @@ +// Copyright (c) 2021 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package docker + +import ( + "reflect" + "testing" +) + +func TestDocker_ClientOpt_WithPrivilegedImages(t *testing.T) { + // setup tests + tests := []struct { + images []string + want []string + }{ + { + images: []string{"alpine", "golang"}, + want: []string{"alpine", "golang"}, + }, + { + images: []string{}, + want: []string{}, + }, + } + + // run tests + for _, test := range tests { + _service, err := New( + WithPrivilegedImages(test.images), + ) + + if err != nil { + t.Errorf("WithPrivilegedImages returned err: %v", err) + } + + if !reflect.DeepEqual(_service.config.Images, test.want) { + t.Errorf("WithPrivilegedImages is %v, want %v", _service.config.Images, test.want) + } + } +} + +func TestDocker_ClientOpt_WithHostVolumes(t *testing.T) { + // setup tests + tests := []struct { + volumes []string + want []string + }{ + { + volumes: []string{"/foo/bar.txt:/foo/bar.txt", "/tmp/baz.conf:/tmp/baz.conf"}, + want: []string{"/foo/bar.txt:/foo/bar.txt", "/tmp/baz.conf:/tmp/baz.conf"}, + }, + { + volumes: []string{}, + want: []string{}, + }, + } + + // run tests + for _, test := range tests { + _service, err := New( + WithHostVolumes(test.volumes), + ) + + if err != nil { + t.Errorf("WithHostVolumes returned err: %v", err) + } + + if !reflect.DeepEqual(_service.config.Volumes, test.want) { + t.Errorf("WithHostVolumes is %v, want %v", _service.config.Volumes, test.want) + } + } +} diff --git a/runtime/docker/volume.go b/runtime/docker/volume.go new file mode 100644 index 00000000..357162da --- /dev/null +++ b/runtime/docker/volume.go @@ -0,0 +1,150 @@ +// Copyright (c) 2021 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package docker + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/docker/docker/api/types/container" + "github.com/docker/docker/api/types/mount" + "github.com/docker/docker/api/types/volume" + "github.com/docker/go-units" + + "github.com/go-vela/types/constants" + "github.com/go-vela/types/pipeline" + vol "github.com/go-vela/worker/internal/volume" + + "github.com/sirupsen/logrus" +) + +// CreateVolume creates the pipeline volume. +func (c *client) CreateVolume(ctx context.Context, b *pipeline.Build) error { + logrus.Tracef("creating volume for pipeline %s", b.ID) + + // create options for creating volume + // + // https://godoc.org/github.com/docker/docker/api/types/volume#VolumeCreateBody + opts := volume.VolumeCreateBody{ + Name: b.ID, + Driver: "local", + } + + // send API call to create the volume + // + // https://godoc.org/github.com/docker/docker/client#Client.VolumeCreate + _, err := c.Docker.VolumeCreate(ctx, opts) + if err != nil { + return err + } + + return nil +} + +// InspectVolume inspects the pipeline volume. +func (c *client) InspectVolume(ctx context.Context, b *pipeline.Build) ([]byte, error) { + logrus.Tracef("inspecting volume for pipeline %s", b.ID) + + // create output for inspecting volume + output := []byte( + fmt.Sprintf("$ docker volume inspect %s\n", b.ID), + ) + + // send API call to inspect the volume + // + // https://godoc.org/github.com/docker/docker/client#Client.VolumeInspect + v, err := c.Docker.VolumeInspect(ctx, b.ID) + if err != nil { + return output, err + } + + // convert volume type Volume to bytes with pretty print + // + // https://godoc.org/github.com/docker/docker/api/types#Volume + volume, err := json.MarshalIndent(v, "", " ") + if err != nil { + return output, err + } + + // add new line to end of bytes + return append(output, append(volume, "\n"...)...), nil +} + +// RemoveVolume deletes the pipeline volume. +func (c *client) RemoveVolume(ctx context.Context, b *pipeline.Build) error { + logrus.Tracef("removing volume for pipeline %s", b.ID) + + // send API call to remove the volume + // + // https://godoc.org/github.com/docker/docker/client#Client.VolumeRemove + err := c.Docker.VolumeRemove(ctx, b.ID, true) + if err != nil { + return err + } + + return nil +} + +// hostConfig is a helper function to generate the host config +// with Ulimit and volume specifications for a container. +func hostConfig(id string, ulimits pipeline.UlimitSlice, volumes []string) *container.HostConfig { + logrus.Tracef("creating mount for default volume %s", id) + + // create default mount for pipeline volume + mounts := []mount.Mount{ + { + Type: mount.TypeVolume, + Source: id, + Target: constants.WorkspaceMount, + }, + } + + resources := container.Resources{} + // iterate through all ulimits provided + + for _, v := range ulimits { + resources.Ulimits = append(resources.Ulimits, &units.Ulimit{ + Name: v.Name, + Hard: v.Hard, + Soft: v.Soft, + }) + } + + // check if other volumes were provided + if len(volumes) > 0 { + // iterate through all volumes provided + for _, v := range volumes { + logrus.Tracef("creating mount for volume %s", v) + + // parse the volume provided + _volume, err := vol.ParseWithError(v) + if err != nil { + logrus.Error(err) + } + + // add the volume to the set of mounts + mounts = append(mounts, mount.Mount{ + Type: mount.TypeBind, + Source: _volume.Source, + Target: _volume.Destination, + ReadOnly: _volume.AccessMode == "ro", + }) + } + } + + // https://godoc.org/github.com/docker/docker/api/types/container#HostConfig + return &container.HostConfig{ + // https://godoc.org/github.com/docker/docker/api/types/container#LogConfig + LogConfig: container.LogConfig{ + Type: "json-file", + }, + Privileged: false, + // https://godoc.org/github.com/docker/docker/api/types/mount#Mount + Mounts: mounts, + // https://pkg.go.dev/github.com/docker/docker/api/types/container#Resources.Ulimits + Resources: resources, + } +} diff --git a/runtime/docker/volume_test.go b/runtime/docker/volume_test.go new file mode 100644 index 00000000..6219b651 --- /dev/null +++ b/runtime/docker/volume_test.go @@ -0,0 +1,132 @@ +// Copyright (c) 2021 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package docker + +import ( + "context" + "testing" + + "github.com/go-vela/types/pipeline" +) + +func TestDocker_CreateVolume(t *testing.T) { + // setup types + _engine, err := NewMock() + if err != nil { + t.Errorf("unable to create runtime engine: %v", err) + } + + // setup tests + tests := []struct { + failure bool + pipeline *pipeline.Build + }{ + { + failure: false, + pipeline: _pipeline, + }, + { + failure: true, + pipeline: new(pipeline.Build), + }, + } + + // run tests + for _, test := range tests { + err = _engine.CreateVolume(context.Background(), test.pipeline) + + if test.failure { + if err == nil { + t.Errorf("CreateVolume should have returned err") + } + + continue + } + + if err != nil { + t.Errorf("CreateVolume returned err: %v", err) + } + } +} + +func TestDocker_InspectVolume(t *testing.T) { + // setup types + _engine, err := NewMock() + if err != nil { + t.Errorf("unable to create runtime engine: %v", err) + } + + // setup tests + tests := []struct { + failure bool + pipeline *pipeline.Build + }{ + { + failure: false, + pipeline: _pipeline, + }, + { + failure: true, + pipeline: new(pipeline.Build), + }, + } + + // run tests + for _, test := range tests { + _, err = _engine.InspectVolume(context.Background(), test.pipeline) + + if test.failure { + if err == nil { + t.Errorf("InspectVolume should have returned err") + } + + continue + } + + if err != nil { + t.Errorf("InspectVolume returned err: %v", err) + } + } +} + +func TestDocker_RemoveVolume(t *testing.T) { + // setup types + _engine, err := NewMock() + if err != nil { + t.Errorf("unable to create runtime engine: %v", err) + } + + // setup tests + tests := []struct { + failure bool + pipeline *pipeline.Build + }{ + { + failure: false, + pipeline: _pipeline, + }, + { + failure: true, + pipeline: new(pipeline.Build), + }, + } + + // run tests + for _, test := range tests { + err = _engine.RemoveVolume(context.Background(), test.pipeline) + + if test.failure { + if err == nil { + t.Errorf("RemoveVolume should have returned err") + } + + continue + } + + if err != nil { + t.Errorf("RemoveVolume returned err: %v", err) + } + } +} diff --git a/runtime/engine.go b/runtime/engine.go new file mode 100644 index 00000000..6f57d0e9 --- /dev/null +++ b/runtime/engine.go @@ -0,0 +1,92 @@ +// Copyright (c) 2021 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package runtime + +import ( + "context" + "io" + + "github.com/go-vela/types/pipeline" +) + +// Engine represents the interface for Vela integrating +// with the different supported Runtime environments. +type Engine interface { + + // Engine Interface Functions + + // Driver defines a function that outputs + // the configured runtime driver. + Driver() string + + // Build Engine Interface Functions + + // InspectBuild defines a function that + // displays details about the build for the init step. + InspectBuild(ctx context.Context, b *pipeline.Build) ([]byte, error) + // SetupBuild defines a function that + // prepares the pipeline build. + SetupBuild(context.Context, *pipeline.Build) error + // AssembleBuild defines a function that + // finalizes pipeline build setup. + AssembleBuild(context.Context, *pipeline.Build) error + // RemoveBuild defines a function that deletes + // (kill, remove) the pipeline build metadata. + RemoveBuild(context.Context, *pipeline.Build) error + + // Container Engine Interface Functions + + // InspectContainer defines a function that inspects + // the pipeline container. + InspectContainer(context.Context, *pipeline.Container) error + // RemoveContainer defines a function that deletes + // (kill, remove) the pipeline container. + RemoveContainer(context.Context, *pipeline.Container) error + // RunContainer defines a function that creates + // and starts the pipeline container. + RunContainer(context.Context, *pipeline.Container, *pipeline.Build) error + // SetupContainer defines a function that prepares + // the image for the pipeline container. + SetupContainer(context.Context, *pipeline.Container) error + // TailContainer defines a function that captures + // the logs on the pipeline container. + TailContainer(context.Context, *pipeline.Container) (io.ReadCloser, error) + // WaitContainer defines a function that blocks + // until the pipeline container completes. + WaitContainer(context.Context, *pipeline.Container) error + + // Image Engine Interface Functions + + // CreateImage defines a function that + // creates the pipeline container image. + CreateImage(context.Context, *pipeline.Container) error + // InspectImage defines a function that + // inspects the pipeline container image. + InspectImage(context.Context, *pipeline.Container) ([]byte, error) + + // Network Engine Interface Functions + + // CreateNetwork defines a function that + // creates the pipeline network. + CreateNetwork(context.Context, *pipeline.Build) error + // InspectNetwork defines a function that + // inspects the pipeline network. + InspectNetwork(context.Context, *pipeline.Build) ([]byte, error) + // RemoveNetwork defines a function that + // deletes the pipeline network. + RemoveNetwork(context.Context, *pipeline.Build) error + + // Volume Engine Interface Functions + + // CreateVolume defines a function that + // creates the pipeline volume. + CreateVolume(context.Context, *pipeline.Build) error + // InspectVolume defines a function that + // inspects the pipeline volume. + InspectVolume(context.Context, *pipeline.Build) ([]byte, error) + // RemoveVolume defines a function that + // deletes the pipeline volume. + RemoveVolume(context.Context, *pipeline.Build) error +} diff --git a/runtime/flags.go b/runtime/flags.go new file mode 100644 index 00000000..1565d0ea --- /dev/null +++ b/runtime/flags.go @@ -0,0 +1,70 @@ +// Copyright (c) 2021 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package runtime + +import ( + "github.com/go-vela/types/constants" + + "github.com/urfave/cli/v2" +) + +// Flags represents all supported command line +// interface (CLI) flags for the runtime. +// +// https://pkg.go.dev/github.com/urfave/cli?tab=doc#Flag +var Flags = []cli.Flag{ + + // Logging Flags + + &cli.StringFlag{ + EnvVars: []string{"VELA_LOG_FORMAT", "RUNTIME_LOG_FORMAT"}, + FilePath: "/vela/runtime/log_format", + Name: "runtime.log.format", + Usage: "format of logs to output", + Value: "json", + }, + &cli.StringFlag{ + EnvVars: []string{"VELA_LOG_LEVEL", "RUNTIME_LOG_LEVEL"}, + FilePath: "/vela/runtime/log_level", + Name: "runtime.log.level", + Usage: "level of logs to output", + Value: "info", + }, + + // Runtime Flags + + &cli.StringFlag{ + EnvVars: []string{"VELA_RUNTIME_DRIVER", "RUNTIME_DRIVER"}, + FilePath: "/vela/runtime/driver", + Name: "runtime.driver", + Usage: "driver to be used for the runtime", + Value: constants.DriverDocker, + }, + &cli.StringFlag{ + EnvVars: []string{"VELA_RUNTIME_CONFIG", "RUNTIME_CONFIG"}, + FilePath: "/vela/runtime/config", + Name: "runtime.config", + Usage: "path to configuration file for the runtime", + }, + &cli.StringFlag{ + EnvVars: []string{"VELA_RUNTIME_NAMESPACE", "RUNTIME_NAMESPACE"}, + FilePath: "/vela/runtime/namespace", + Name: "runtime.namespace", + Usage: "namespace to use for the runtime (only used by kubernetes)", + }, + &cli.StringSliceFlag{ + EnvVars: []string{"VELA_RUNTIME_PRIVILEGED_IMAGES", "RUNTIME_PRIVILEGED_IMAGES"}, + FilePath: "/vela/runtime/privileged_images", + Name: "runtime.privileged-images", + Usage: "list of images allowed to run in privileged mode for the runtime", + Value: cli.NewStringSlice("target/vela-docker"), + }, + &cli.StringSliceFlag{ + EnvVars: []string{"VELA_RUNTIME_VOLUMES", "RUNTIME_VOLUMES"}, + FilePath: "/vela/runtime/volumes", + Name: "runtime.volumes", + Usage: "list of host volumes to mount for the runtime", + }, +} diff --git a/runtime/kubernetes/build.go b/runtime/kubernetes/build.go new file mode 100644 index 00000000..42fbd1bc --- /dev/null +++ b/runtime/kubernetes/build.go @@ -0,0 +1,171 @@ +// Copyright (c) 2021 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package kubernetes + +import ( + "context" + "fmt" + + "github.com/go-vela/types/pipeline" + + "github.com/buildkite/yaml" + "github.com/sirupsen/logrus" + + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// InspectBuild displays details about the pod for the init step. +func (c *client) InspectBuild(ctx context.Context, b *pipeline.Build) ([]byte, error) { + logrus.Tracef("inspecting build pod for pipeline %s", b.ID) + + output := []byte(fmt.Sprintf("> Inspecting pod for pipeline %s", b.ID)) + + // TODO: The environment gets populated in AssembleBuild, after InspectBuild runs. + // But, we should make sure that secrets can't be leaked here anyway. + buildOutput, err := yaml.Marshal(c.Pod) + if err != nil { + return []byte{}, fmt.Errorf("unable to serialize pod: %w", err) + } + + output = append(output, buildOutput...) + + // TODO: make other k8s Inspect* funcs no-ops (prefer this method): + // InspectVolume, InspectImage, InspectNetwork + return output, nil +} + +// SetupBuild prepares the pod metadata for the pipeline build. +func (c *client) SetupBuild(ctx context.Context, b *pipeline.Build) error { + logrus.Tracef("setting up for build %s", b.ID) + + // create the object metadata for the pod + // + // https://pkg.go.dev/k8s.io/apimachinery/pkg/apis/meta/v1?tab=doc#ObjectMeta + c.Pod.ObjectMeta = metav1.ObjectMeta{ + Name: b.ID, + Labels: map[string]string{"pipeline": b.ID}, + } + + // TODO: Vela admin defined worker-specific: + // NodeSelector, Tolerations, Affinity, AutomountServiceAccountToken + + // create the restart policy for the pod + // + // https://pkg.go.dev/k8s.io/api/core/v1?tab=doc#RestartPolicy + c.Pod.Spec.RestartPolicy = v1.RestartPolicyNever + + return nil +} + +// AssembleBuild finalizes the pipeline build setup. +// This creates the pod in kubernetes for the pipeline build. +// After creation, image is the only container field we can edit in kubernetes. +// So, all environment, volume, and other container metadata must be setup +// before running AssembleBuild. +func (c *client) AssembleBuild(ctx context.Context, b *pipeline.Build) error { + logrus.Tracef("assembling build %s", b.ID) + var err error + + // last minute Environment setup + for _, _service := range b.Services { + err = c.setupContainerEnvironment(_service) + if err != nil { + return err + } + } + for _, _stage := range b.Stages { + // TODO: remove hardcoded reference + if _stage.Name == "init" { + continue + } + for _, _step := range _stage.Steps { + err = c.setupContainerEnvironment(_step) + if err != nil { + return err + } + } + } + for _, _step := range b.Steps { + // TODO: remove hardcoded reference + if _step.Name == "init" { + continue + } + err = c.setupContainerEnvironment(_step) + if err != nil { + return err + } + } + for _, _secret := range b.Secrets { + if _secret.Origin.Empty() { + continue + } + err = c.setupContainerEnvironment(_secret.Origin) + if err != nil { + return err + } + } + + // If the api call to create the pod fails, the pod might + // partially exist. So, set this first to make sure all + // remnants get deleted. + c.createdPod = true + + logrus.Infof("creating pod %s", c.Pod.ObjectMeta.Name) + // send API call to create the pod + // + // https://pkg.go.dev/k8s.io/client-go/kubernetes/typed/core/v1?tab=doc#PodInterface + _, err = c.Kubernetes.CoreV1(). + Pods(c.config.Namespace). + Create(context.Background(), c.Pod, metav1.CreateOptions{}) + if err != nil { + return err + } + + return nil +} + +// RemoveBuild deletes (kill, remove) the pipeline build metadata. +// This deletes the kubernetes pod. +func (c *client) RemoveBuild(ctx context.Context, b *pipeline.Build) error { + logrus.Tracef("removing build %s", b.ID) + + if !c.createdPod { + // nothing to do + return nil + } + + // create variables for the delete options + // + // This is necessary because the delete options + // expect all values to be passed by reference. + var ( + period = int64(0) + policy = metav1.DeletePropagationForeground + ) + + // create options for removing the pod + // + // https://pkg.go.dev/k8s.io/apimachinery/pkg/apis/meta/v1?tab=doc#DeleteOptions + opts := metav1.DeleteOptions{ + GracePeriodSeconds: &period, + // https://pkg.go.dev/k8s.io/apimachinery/pkg/apis/meta/v1?tab=doc#DeletionPropagation + PropagationPolicy: &policy, + } + + logrus.Infof("removing pod %s", c.Pod.ObjectMeta.Name) + // send API call to delete the pod + err := c.Kubernetes.CoreV1(). + Pods(c.config.Namespace). + Delete(context.Background(), c.Pod.ObjectMeta.Name, opts) + if err != nil { + return err + } + + c.Pod = &v1.Pod{} + c.createdPod = false + + return nil +} diff --git a/runtime/kubernetes/build_test.go b/runtime/kubernetes/build_test.go new file mode 100644 index 00000000..63f6e3f6 --- /dev/null +++ b/runtime/kubernetes/build_test.go @@ -0,0 +1,226 @@ +// Copyright (c) 2021 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package kubernetes + +import ( + "context" + "testing" + + "github.com/go-vela/types/pipeline" + + v1 "k8s.io/api/core/v1" +) + +func TestKubernetes_InspectBuild(t *testing.T) { + // setup types + _engine, err := NewMock(_pod) + if err != nil { + t.Errorf("unable to create runtime engine: %v", err) + } + + // setup tests + tests := []struct { + failure bool + pipeline *pipeline.Build + }{ + { + failure: false, + pipeline: _stages, + }, + { + failure: false, + pipeline: _steps, + }, + } + + // run tests + for _, test := range tests { + _, err = _engine.InspectBuild(context.Background(), test.pipeline) + + if test.failure { + if err == nil { + t.Errorf("InspectBuild should have returned err") + } + + continue + } + + if err != nil { + t.Errorf("InspectBuild returned err: %v", err) + } + } +} + +func TestKubernetes_SetupBuild(t *testing.T) { + // setup types + _engine, err := NewMock(&v1.Pod{}) + if err != nil { + t.Errorf("unable to create runtime engine: %v", err) + } + + // setup tests + tests := []struct { + failure bool + pipeline *pipeline.Build + }{ + { + failure: false, + pipeline: _stages, + }, + { + failure: false, + pipeline: _steps, + }, + } + + // run tests + for _, test := range tests { + err = _engine.SetupBuild(context.Background(), test.pipeline) + + // this does not test the resulting pod spec (ie no tests for ObjectMeta, RestartPolicy) + + if test.failure { + if err == nil { + t.Errorf("SetupBuild should have returned err") + } + + continue + } + + if err != nil { + t.Errorf("SetupBuild returned err: %v", err) + } + } +} + +func TestKubernetes_AssembleBuild(t *testing.T) { + // setup tests + tests := []struct { + failure bool + pipeline *pipeline.Build + // k8sPod is the pod that the mock Kubernetes client will return + k8sPod *v1.Pod + // enginePod is the pod under construction in the Runtime Engine + enginePod *v1.Pod + }{ + { + failure: false, + pipeline: _stages, + k8sPod: &v1.Pod{}, + enginePod: _stagesPod, + }, + { + failure: false, + pipeline: _steps, + k8sPod: &v1.Pod{}, + enginePod: _pod, + }, + { + failure: true, + pipeline: _stages, + k8sPod: _stagesPod, + enginePod: _stagesPod, + }, + { + failure: true, + pipeline: _steps, + k8sPod: _pod, + enginePod: _pod, + }, + } + + // run tests + for _, test := range tests { + _engine, err := NewMock(test.k8sPod) + _engine.Pod = test.enginePod + if err != nil { + t.Errorf("unable to create runtime engine: %v", err) + } + + err = _engine.AssembleBuild(context.Background(), test.pipeline) + + if test.failure { + if err == nil { + t.Errorf("AssembleBuild should have returned err") + } + + continue + } + + if err != nil { + t.Errorf("AssembleBuild returned err: %v", err) + } + } +} + +func TestKubernetes_RemoveBuild(t *testing.T) { + // setup tests + tests := []struct { + failure bool + createdPod bool + pipeline *pipeline.Build + pod *v1.Pod + }{ + { + failure: false, + createdPod: true, + pipeline: _stages, + pod: _pod, + }, + { + failure: false, + createdPod: true, + pipeline: _steps, + pod: _pod, + }, + { + failure: false, + createdPod: false, + pipeline: _stages, + pod: &v1.Pod{}, + }, + { + failure: false, + pipeline: _steps, + pod: &v1.Pod{}, + createdPod: false, + }, + { + failure: true, + pipeline: _stages, + pod: &v1.Pod{}, + createdPod: true, + }, + { + failure: true, + pipeline: _steps, + pod: &v1.Pod{}, + createdPod: true, + }, + } + + // run tests + for _, test := range tests { + _engine, err := NewMock(test.pod) + if err != nil { + t.Errorf("unable to create runtime engine: %v", err) + } + _engine.createdPod = test.createdPod + + err = _engine.RemoveBuild(context.Background(), test.pipeline) + + if test.failure { + if err == nil { + t.Errorf("RemoveBuild should have returned err") + } + + continue + } + + if err != nil { + t.Errorf("RemoveBuild returned err: %v", err) + } + } +} diff --git a/runtime/kubernetes/container.go b/runtime/kubernetes/container.go new file mode 100644 index 00000000..d0e2d8c0 --- /dev/null +++ b/runtime/kubernetes/container.go @@ -0,0 +1,382 @@ +// Copyright (c) 2021 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package kubernetes + +import ( + "bufio" + "context" + "fmt" + "io" + "io/ioutil" + "strings" + "time" + + "github.com/go-vela/types/constants" + "github.com/go-vela/types/pipeline" + "github.com/go-vela/worker/internal/image" + + "github.com/sirupsen/logrus" + + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/wait" +) + +// InspectContainer inspects the pipeline container. +func (c *client) InspectContainer(ctx context.Context, ctn *pipeline.Container) error { + logrus.Tracef("inspecting container %s", ctn.ID) + + // create options for getting the container + opts := metav1.GetOptions{} + + // send API call to capture the container + // + // https://pkg.go.dev/k8s.io/client-go/kubernetes/typed/core/v1?tab=doc#PodInterface + pod, err := c.Kubernetes.CoreV1().Pods(c.config.Namespace).Get( + context.Background(), + c.Pod.ObjectMeta.Name, + opts, + ) + if err != nil { + return err + } + + // iterate through each container in the pod + for _, cst := range pod.Status.ContainerStatuses { + // check if the container has a matching ID + // + // https://pkg.go.dev/k8s.io/api/core/v1?tab=doc#ContainerStatus + if !strings.EqualFold(cst.Name, ctn.ID) { + // skip container if it's not a matching ID + continue + } + + // set the step exit code + ctn.ExitCode = int(cst.State.Terminated.ExitCode) + + break + } + + return nil +} + +// RemoveContainer deletes (kill, remove) the pipeline container. +// This is a no-op for kubernetes. RemoveBuild handles deleting the pod. +func (c *client) RemoveContainer(ctx context.Context, ctn *pipeline.Container) error { + logrus.Tracef("no-op: removing container %s", ctn.ID) + + return nil +} + +// RunContainer creates and starts the pipeline container. +// +// nolint: lll // ignore long line length +func (c *client) RunContainer(ctx context.Context, ctn *pipeline.Container, b *pipeline.Build) error { + logrus.Tracef("running container %s", ctn.ID) + // parse image from step + _image, err := image.ParseWithError(ctn.Image) + if err != nil { + return err + } + + // set the pod container image to the parsed step image + // (-1 to convert to 0-based index, -1 for init which isn't a container) + c.Pod.Spec.Containers[ctn.Number-2].Image = _image + + // send API call to patch the pod with the new container image + // + // https://pkg.go.dev/k8s.io/client-go/kubernetes/typed/core/v1?tab=doc#PodInterface + _, err = c.Kubernetes.CoreV1().Pods(c.config.Namespace).Patch( + context.Background(), + c.Pod.ObjectMeta.Name, + types.StrategicMergePatchType, + []byte(fmt.Sprintf(imagePatch, ctn.ID, _image)), + metav1.PatchOptions{}, + ) + if err != nil { + return err + } + + return nil +} + +// SetupContainer prepares the image for the pipeline container. +func (c *client) SetupContainer(ctx context.Context, ctn *pipeline.Container) error { + logrus.Tracef("setting up for container %s", ctn.ID) + + // create the container object for the pod + // + // https://pkg.go.dev/k8s.io/api/core/v1?tab=doc#Container + container := v1.Container{ + Name: ctn.ID, + // create the container with the kubernetes/pause image + // + // This is done due to the nature of how containers are + // executed inside the pod. Kubernetes will attempt to + // start and run all containers in the pod at once. We + // want to control the execution of the containers + // inside the pod so we use the pause image as the + // default for containers, and then sequentially patch + // the containers with the proper image. + // + // https://hub.docker.com/r/kubernetes/pause + Image: image.Parse("kubernetes/pause:latest"), + Env: []v1.EnvVar{}, + Stdin: false, + StdinOnce: false, + TTY: false, + WorkingDir: ctn.Directory, + } + + // handle the container pull policy (This cannot be updated like the image can) + switch ctn.Pull { + case constants.PullAlways: + // set the pod container pull policy to always + container.ImagePullPolicy = v1.PullAlways + case constants.PullNever: + // set the pod container pull policy to never + container.ImagePullPolicy = v1.PullNever + case constants.PullOnStart: + // set the pod container pull policy to always + // + // if the pipeline container image should be pulled on start, than + // we force Kubernetes to pull the image on start with the always + // pull policy for the pod container + container.ImagePullPolicy = v1.PullAlways + case constants.PullNotPresent: + fallthrough + default: + // default the pod container pull policy to if not present + container.ImagePullPolicy = v1.PullIfNotPresent + } + + // fill in the VolumeMounts including workspaceMount + volumeMounts, err := c.setupVolumeMounts(ctx, ctn) + if err != nil { + return err + } + container.VolumeMounts = volumeMounts + + // check if the image is allowed to run privileged + for _, pattern := range c.config.Images { + privileged, err := image.IsPrivilegedImage(ctn.Image, pattern) + if err != nil { + return err + } + + container.SecurityContext = &v1.SecurityContext{ + Privileged: &privileged, + } + } + + // TODO: add SecurityContext options (runAsUser, runAsNonRoot, sysctls) + + // Executor.CreateBuild extends the environment AFTER calling Runtime.SetupBuild. + // So, configure the environment as late as possible (just before pod creation). + + // check if the entrypoint is provided + if len(ctn.Entrypoint) > 0 { + // add entrypoint to container config + container.Args = ctn.Entrypoint + } + + // check if the commands are provided + if len(ctn.Commands) > 0 { + // add commands to container config + container.Args = append(container.Args, ctn.Commands...) + } + + // add the container definition to the pod spec + // + // https://pkg.go.dev/k8s.io/api/core/v1?tab=doc#PodSpec + c.Pod.Spec.Containers = append(c.Pod.Spec.Containers, container) + + return nil +} + +// setupContainerEnvironment adds env vars to the Pod spec for a container. +// Call this just before pod creation to capture as many env changes as possible. +func (c *client) setupContainerEnvironment(ctn *pipeline.Container) error { + logrus.Tracef("setting up environment for container %s", ctn.ID) + + // get the matching container spec + // (-1 to convert to 0-based index, -1 for injected init container) + container := &c.Pod.Spec.Containers[ctn.Number-2] + if !strings.EqualFold(container.Name, ctn.ID) { + return fmt.Errorf("wrong container! got %s instead of %s", container.Name, ctn.ID) + } + + // check if the environment is provided + if len(ctn.Environment) > 0 { + // iterate through each element in the container environment + for k, v := range ctn.Environment { + // add key/value environment to container config + container.Env = append(container.Env, v1.EnvVar{Name: k, Value: v}) + } + } + return nil +} + +// TailContainer captures the logs for the pipeline container. +// +// nolint: lll // ignore long line length due to variable names +func (c *client) TailContainer(ctx context.Context, ctn *pipeline.Container) (io.ReadCloser, error) { + logrus.Tracef("tailing output for container %s", ctn.ID) + + // create object to store container logs + var logs io.ReadCloser + + // create function for periodically capturing + // the logs from the container with backoff + logsFunc := func() (bool, error) { + // create options for capturing the logs from the container + // + // https://pkg.go.dev/k8s.io/api/core/v1?tab=doc#PodLogOptions + opts := &v1.PodLogOptions{ + Container: ctn.ID, + Follow: true, + // steps can exit quickly, and might be gone before + // log tailing has started, so we need to request + // logs for previously exited containers as well. + // Pods get deleted after job completion, and names for + // pod+container don't get reused. So, previous + // should only retrieve logs for the current build step. + Previous: true, + Timestamps: false, + } + + // send API call to capture stream of container logs + // + // https://pkg.go.dev/k8s.io/client-go/kubernetes/typed/core/v1?tab=doc#PodExpansion + // -> + // https://pkg.go.dev/k8s.io/client-go/rest?tab=doc#Request.Stream + stream, err := c.Kubernetes.CoreV1(). + Pods(c.config.Namespace). + GetLogs(c.Pod.ObjectMeta.Name, opts). + Stream(context.Background()) + if err != nil { + logrus.Errorf("%v", err) + return false, nil + } + + // create temporary reader to ensure logs are available + reader := bufio.NewReader(stream) + + // peek at container logs from the stream + // + // nolint: gomnd // ignore magic number + bytes, err := reader.Peek(5) + if err != nil { + // skip so we resend API call to capture stream + return false, nil + } + + // check if we have container logs from the stream + if len(bytes) > 0 { + // set the logs to the reader + logs = ioutil.NopCloser(reader) + return true, nil + } + + // no logs are available + return false, nil + } + + // create backoff object for capturing the logs + // from the container with periodic backoff + // + // https://pkg.go.dev/k8s.io/apimachinery/pkg/util/wait?tab=doc#Backoff + backoff := wait.Backoff{ + Duration: 1 * time.Second, + Factor: 2.0, + Jitter: 0.25, + Steps: 10, + Cap: 2 * time.Minute, + } + + logrus.Tracef("capturing logs with exponential backoff for container %s", ctn.ID) + // perform the function to capture logs with periodic backoff + // + // https://pkg.go.dev/k8s.io/apimachinery/pkg/util/wait?tab=doc#ExponentialBackoff + err := wait.ExponentialBackoff(backoff, logsFunc) + if err != nil { + return nil, err + } + + return logs, nil +} + +// WaitContainer blocks until the pipeline container completes. +func (c *client) WaitContainer(ctx context.Context, ctn *pipeline.Container) error { + logrus.Tracef("waiting for container %s", ctn.ID) + + // create label selector for watching the pod + selector := fmt.Sprintf("pipeline=%s", c.Pod.ObjectMeta.Name) + + // create options for watching the container + opts := metav1.ListOptions{ + LabelSelector: selector, + Watch: true, + } + + // send API call to capture channel for watching the container + // + // https://pkg.go.dev/k8s.io/client-go/kubernetes/typed/core/v1?tab=doc#PodInterface + // -> + // https://pkg.go.dev/k8s.io/apimachinery/pkg/watch?tab=doc#Interface + watch, err := c.Kubernetes.CoreV1().Pods(c.config.Namespace).Watch(context.Background(), opts) + if err != nil { + return err + } + + for { + // capture new result from the channel + // + // https://pkg.go.dev/k8s.io/apimachinery/pkg/watch?tab=doc#Interface + result := <-watch.ResultChan() + + // convert the object from the result to a pod + pod, ok := result.Object.(*v1.Pod) + if !ok { + return fmt.Errorf("unable to watch pod %s", c.Pod.ObjectMeta.Name) + } + + // check if the pod is in a pending state + // + // https://pkg.go.dev/k8s.io/api/core/v1?tab=doc#PodStatus + if pod.Status.Phase == v1.PodPending { + // skip pod if it's in a pending state + continue + } + + // iterate through each container in the pod + for _, cst := range pod.Status.ContainerStatuses { + // check if the container has a matching ID + // + // https://pkg.go.dev/k8s.io/api/core/v1?tab=doc#ContainerStatus + if !strings.EqualFold(cst.Name, ctn.ID) { + // skip container if it's not a matching ID + continue + } + + // check if the container is in a terminated state + // + // https://pkg.go.dev/k8s.io/api/core/v1?tab=doc#ContainerState + if cst.State.Terminated == nil { + // skip container if it's not in a terminated state + break + } + + // check if the container has a terminated state reason + // + // https://pkg.go.dev/k8s.io/api/core/v1?tab=doc#ContainerStateTerminated + if len(cst.State.Terminated.Reason) > 0 { + // break watching the container as it's complete + return nil + } + } + } +} diff --git a/runtime/kubernetes/container_test.go b/runtime/kubernetes/container_test.go new file mode 100644 index 00000000..aef5249c --- /dev/null +++ b/runtime/kubernetes/container_test.go @@ -0,0 +1,336 @@ +// Copyright (c) 2021 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package kubernetes + +import ( + "context" + "testing" + + "github.com/go-vela/types/pipeline" + + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/watch" + "k8s.io/client-go/kubernetes/fake" + testcore "k8s.io/client-go/testing" +) + +func TestKubernetes_InspectContainer(t *testing.T) { + // setup types + _engine, err := NewMock(_pod) + if err != nil { + t.Errorf("unable to create runtime engine: %v", err) + } + + // setup tests + tests := []struct { + failure bool + container *pipeline.Container + }{ + { + failure: false, + container: _container, + }, + { + failure: false, + container: new(pipeline.Container), + }, + } + + // run tests + for _, test := range tests { + err = _engine.InspectContainer(context.Background(), test.container) + + if test.failure { + if err == nil { + t.Errorf("InspectContainer should have returned err") + } + + continue + } + + if err != nil { + t.Errorf("InspectContainer returned err: %v", err) + } + } +} + +func TestKubernetes_RemoveContainer(t *testing.T) { + // setup types + _engine, err := NewMock(_pod) + if err != nil { + t.Errorf("unable to create runtime engine: %v", err) + } + + // setup tests + tests := []struct { + failure bool + container *pipeline.Container + }{ + { + failure: false, + container: _container, + }, + } + + // run tests + for _, test := range tests { + err = _engine.RemoveContainer(context.Background(), test.container) + + if test.failure { + if err == nil { + t.Errorf("RemoveContainer should have returned err") + } + + continue + } + + if err != nil { + t.Errorf("RemoveContainer returned err: %v", err) + } + } +} + +func TestKubernetes_RunContainer(t *testing.T) { + // TODO: include VolumeMounts? + // setup tests + tests := []struct { + failure bool + container *pipeline.Container + pipeline *pipeline.Build + pod *v1.Pod + volumes []string + }{ + { + failure: false, + container: _container, + pipeline: _stages, + pod: _pod, + }, + { + failure: false, + container: _container, + pipeline: _steps, + pod: _pod, + }, + } + + // run tests + for _, test := range tests { + _engine, err := NewMock(test.pod) + if err != nil { + t.Errorf("unable to create runtime engine: %v", err) + } + + if len(test.volumes) > 0 { + _engine.config.Volumes = test.volumes + } + + err = _engine.RunContainer(context.Background(), test.container, test.pipeline) + + if test.failure { + if err == nil { + t.Errorf("RunContainer should have returned err") + } + + continue + } + + if err != nil { + t.Errorf("RunContainer returned err: %v", err) + } + } +} + +func TestKubernetes_SetupContainer(t *testing.T) { + // setup types + _engine, err := NewMock(_pod) + if err != nil { + t.Errorf("unable to create runtime engine: %v", err) + } + + // setup tests + tests := []struct { + failure bool + container *pipeline.Container + }{ + { + failure: false, + container: _container, + }, + { + failure: false, + container: &pipeline.Container{ + ID: "step_github_octocat_1_echo", + Commands: []string{"echo", "hello"}, + Directory: "/vela/src/github.com/octocat/helloworld", + Environment: map[string]string{"FOO": "bar"}, + Entrypoint: []string{"/bin/sh", "-c"}, + Image: "alpine:latest", + Name: "echo", + Number: 2, + Pull: "always", + }, + }, + { + failure: false, + container: &pipeline.Container{ + ID: "step_github_octocat_1_echo", + Commands: []string{"echo", "hello"}, + Directory: "/vela/src/github.com/octocat/helloworld", + Environment: map[string]string{"FOO": "bar"}, + Entrypoint: []string{"/bin/sh", "-c"}, + Image: "target/vela-docker:latest", + Name: "echo", + Number: 2, + Pull: "always", + }, + }, + } + + // run tests + for _, test := range tests { + err = _engine.SetupContainer(context.Background(), test.container) + + // this does not test the resulting pod spec (ie no tests for ImagePullPolicy, VolumeMounts) + + if test.failure { + if err == nil { + t.Errorf("SetupContainer should have returned err") + } + + continue + } + + if err != nil { + t.Errorf("SetupContainer returned err: %v", err) + } + } +} + +// TODO: implement this once they resolve the bug +// +// https://github.com/kubernetes/kubernetes/issues/84203 +func TestKubernetes_TailContainer(t *testing.T) { + // Unfortunately, we can't implement this test using + // the native Kubernetes fake. This is because there + // is a bug in that code where an "empty" request is + // always returned when calling the GetLogs function. + // + // https://github.com/kubernetes/kubernetes/issues/84203 + // fixed in k8s.io/client-go v0.19.0; we already have v0.22.2 +} + +func TestKubernetes_WaitContainer(t *testing.T) { + // setup types + _engine, err := NewMock(_pod) + if err != nil { + t.Errorf("unable to create runtime engine: %v", err) + } + + // create a new fake kubernetes client + // + // https://pkg.go.dev/k8s.io/client-go/kubernetes/fake?tab=doc#NewSimpleClientset + _kubernetes := fake.NewSimpleClientset(_pod) + + // create a new fake watcher + // + // https://pkg.go.dev/k8s.io/apimachinery/pkg/watch?tab=doc#NewFake + _watch := watch.NewFake() + + // create a new watch reactor with the fake watcher + // + // https://pkg.go.dev/k8s.io/client-go/testing?tab=doc#DefaultWatchReactor + reactor := testcore.DefaultWatchReactor(_watch, nil) + + // add watch reactor to beginning of the client chain + // + // https://pkg.go.dev/k8s.io/client-go/testing?tab=doc#Fake.PrependWatchReactor + _kubernetes.PrependWatchReactor("pods", reactor) + + // overwrite the mock kubernetes client + _engine.Kubernetes = _kubernetes + + // setup tests + tests := []struct { + failure bool + container *pipeline.Container + object runtime.Object + }{ + { + failure: false, + container: _container, + object: _pod, + }, + { + failure: false, + container: _container, + object: &v1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "github-octocat-1", + Namespace: "test", + Labels: map[string]string{ + "pipeline": "github-octocat-1", + }, + }, + TypeMeta: metav1.TypeMeta{ + APIVersion: "v1", + Kind: "Pod", + }, + Status: v1.PodStatus{ + Phase: v1.PodRunning, + ContainerStatuses: []v1.ContainerStatus{ + { + Name: "step-github-octocat-1-echo", + State: v1.ContainerState{ + Terminated: &v1.ContainerStateTerminated{ + Reason: "Completed", + ExitCode: 0, + }, + }, + }, + { + Name: "step-github-octocat-1-clone", + State: v1.ContainerState{ + Terminated: &v1.ContainerStateTerminated{ + Reason: "Completed", + ExitCode: 0, + }, + }, + }, + }, + }, + }, + }, + { + failure: true, + container: _container, + object: new(v1.PodTemplate), + }, + } + + // run tests + for _, test := range tests { + go func() { + // simulate adding a pod to the watcher + _watch.Add(test.object) + }() + + err := _engine.WaitContainer(context.Background(), test.container) + + if test.failure { + if err == nil { + t.Errorf("WaitContainer should have returned err") + } + + continue + } + + if err != nil { + t.Errorf("WaitContainer returned err: %v", err) + } + } +} diff --git a/runtime/kubernetes/doc.go b/runtime/kubernetes/doc.go new file mode 100644 index 00000000..b9bf7b5d --- /dev/null +++ b/runtime/kubernetes/doc.go @@ -0,0 +1,11 @@ +// Copyright (c) 2021 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +// Package kubernetes provides the ability for Vela to +// integrate with Kubernetes as a runtime environment. +// +// Usage: +// +// import "github.com/go-vela/worker/runtime/kubernetes" +package kubernetes diff --git a/runtime/kubernetes/driver.go b/runtime/kubernetes/driver.go new file mode 100644 index 00000000..216e2d55 --- /dev/null +++ b/runtime/kubernetes/driver.go @@ -0,0 +1,12 @@ +// Copyright (c) 2021 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package kubernetes + +import "github.com/go-vela/types/constants" + +// Driver outputs the configured runtime driver. +func (c *client) Driver() string { + return constants.DriverKubernetes +} diff --git a/runtime/kubernetes/driver_test.go b/runtime/kubernetes/driver_test.go new file mode 100644 index 00000000..2010ff45 --- /dev/null +++ b/runtime/kubernetes/driver_test.go @@ -0,0 +1,29 @@ +// Copyright (c) 2021 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package kubernetes + +import ( + "reflect" + "testing" + + "github.com/go-vela/types/constants" +) + +func TestKubernetes_Driver(t *testing.T) { + // setup types + want := constants.DriverKubernetes + + _engine, err := NewMock(_pod) + if err != nil { + t.Errorf("unable to create runtime engine: %v", err) + } + + // run tes + got := _engine.Driver() + + if !reflect.DeepEqual(got, want) { + t.Errorf("Driver is %v, want %v", got, want) + } +} diff --git a/runtime/kubernetes/image.go b/runtime/kubernetes/image.go new file mode 100644 index 00000000..1a123131 --- /dev/null +++ b/runtime/kubernetes/image.go @@ -0,0 +1,67 @@ +// Copyright (c) 2021 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package kubernetes + +import ( + "context" + "encoding/json" + "fmt" + "strings" + + "github.com/go-vela/types/constants" + "github.com/go-vela/types/pipeline" + + "github.com/sirupsen/logrus" +) + +const imagePatch = ` +{ + "spec": { + "containers": [ + { + "name": "%s", + "image": "%s" + } + ] + } +} +` + +// CreateImage creates the pipeline container image. +func (c *client) CreateImage(ctx context.Context, ctn *pipeline.Container) error { + logrus.Tracef("creating image for container %s", ctn.ID) + + return nil +} + +// InspectImage inspects the pipeline container image. +func (c *client) InspectImage(ctx context.Context, ctn *pipeline.Container) ([]byte, error) { + logrus.Tracef("inspecting image for container %s", ctn.ID) + + // TODO: consider updating this command + // + // create output for inspecting image + output := []byte( + // nolint: lll // ignore line length due to string formatting with parameters + fmt.Sprintf("$ kubectl get pod -o=jsonpath='{.spec.containers[%d].image}' %s\n", ctn.Number, ctn.ID), + ) + + // check if the container pull policy is on start + if strings.EqualFold(ctn.Pull, constants.PullOnStart) { + return []byte( + fmt.Sprintf("skipped for container %s due to pull policy %s\n", ctn.ID, ctn.Pull), + ), nil + } + + // marshal the image information from the container + // (-1 to convert to 0-based index, -1 for init which isn't a container) + image, err := json.MarshalIndent(c.Pod.Spec.Containers[ctn.Number-2].Image, "", " ") + if err != nil { + return output, err + } + + // add new line to end of bytes + return append(output, append(image, "\n"...)...), nil +} diff --git a/runtime/kubernetes/image_test.go b/runtime/kubernetes/image_test.go new file mode 100644 index 00000000..78c9cf7a --- /dev/null +++ b/runtime/kubernetes/image_test.go @@ -0,0 +1,48 @@ +// Copyright (c) 2021 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package kubernetes + +import ( + "context" + "testing" + + "github.com/go-vela/types/pipeline" +) + +func TestKubernetes_InspectImage(t *testing.T) { + // setup types + _engine, err := NewMock(_pod) + if err != nil { + t.Errorf("unable to create runtime engine: %v", err) + } + + // setup tests + tests := []struct { + failure bool + container *pipeline.Container + }{ + { + failure: false, + container: _container, + }, + } + + // run tests + for _, test := range tests { + _, err = _engine.InspectImage(context.Background(), test.container) + + if test.failure { + if err == nil { + t.Errorf("InspectImage should have returned err") + } + + continue + } + + if err != nil { + t.Errorf("InspectImage returned err: %v", err) + } + } +} diff --git a/runtime/kubernetes/kubernetes.go b/runtime/kubernetes/kubernetes.go new file mode 100644 index 00000000..d406830f --- /dev/null +++ b/runtime/kubernetes/kubernetes.go @@ -0,0 +1,130 @@ +// Copyright (c) 2021 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package kubernetes + +import ( + "github.com/sirupsen/logrus" + v1 "k8s.io/api/core/v1" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/kubernetes/fake" + "k8s.io/client-go/rest" + "k8s.io/client-go/tools/clientcmd" +) + +type config struct { + // specifies the config file to use for the Kubernetes client + File string + // specifies the namespace to use for the Kubernetes client + Namespace string + // specifies a list of privileged images to use for the Kubernetes client + Images []string + // specifies a list of host volumes to use for the Kubernetes client + Volumes []string +} + +type client struct { + config *config + // https://pkg.go.dev/k8s.io/client-go/kubernetes#Interface + Kubernetes kubernetes.Interface + // https://pkg.go.dev/k8s.io/api/core/v1#Pod + Pod *v1.Pod + // commonVolumeMounts includes workspace mount and any global host mounts (VELA_RUNTIME_VOLUMES) + commonVolumeMounts []v1.VolumeMount + // indicates when the pod has been created in kubernetes + createdPod bool +} + +// New returns an Engine implementation that +// integrates with a Kubernetes runtime. +// +// nolint: golint // ignore returning unexported client +func New(opts ...ClientOpt) (*client, error) { + // create new Kubernetes client + c := &client{} + + // create new fields + c.config = &config{} + c.Pod = &v1.Pod{} + + // apply all provided configuration options + for _, opt := range opts { + err := opt(c) + if err != nil { + return nil, err + } + } + + // use the current context in kubeconfig + // + // when no kube config is provided create InClusterConfig + // else use out of cluster config option + var ( + config *rest.Config + err error + ) + if c.config.File == "" { + // https://pkg.go.dev/k8s.io/client-go/rest?tab=doc#InClusterConfig + config, err = rest.InClusterConfig() + if err != nil { + logrus.Error("VELA_RUNTIME_CONFIG not defined and failed to create kubernetes InClusterConfig!") + return nil, err + } + } else { + // https://pkg.go.dev/k8s.io/client-go/tools/clientcmd?tab=doc#BuildConfigFromFlags + config, err = clientcmd.BuildConfigFromFlags("", c.config.File) + if err != nil { + return nil, err + } + } + + // creates Kubernetes client from configuration + // + // https://pkg.go.dev/k8s.io/client-go/kubernetes?tab=doc#NewForConfig + _kubernetes, err := kubernetes.NewForConfig(config) + if err != nil { + return nil, err + } + + // set the Kubernetes client in the runtime client + c.Kubernetes = _kubernetes + + return c, nil +} + +// NewMock returns an Engine implementation that +// integrates with a Kubernetes runtime. +// +// This function is intended for running tests only. +// +// nolint: golint // ignore returning unexported client +func NewMock(_pod *v1.Pod, opts ...ClientOpt) (*client, error) { + // create new Kubernetes client + c := &client{} + + // create new fields + c.config = &config{} + c.Pod = &v1.Pod{} + + // set the Kubernetes namespace in the runtime client + c.config.Namespace = "test" + + // set the Kubernetes pod in the runtime client + c.Pod = _pod + + // apply all provided configuration options + for _, opt := range opts { + err := opt(c) + if err != nil { + return nil, err + } + } + + // set the Kubernetes fake client in the runtime client + // + // https://pkg.go.dev/k8s.io/client-go/kubernetes/fake?tab=doc#NewSimpleClientset + c.Kubernetes = fake.NewSimpleClientset(c.Pod) + + return c, nil +} diff --git a/runtime/kubernetes/kubernetes_test.go b/runtime/kubernetes/kubernetes_test.go new file mode 100644 index 00000000..9027085b --- /dev/null +++ b/runtime/kubernetes/kubernetes_test.go @@ -0,0 +1,316 @@ +// Copyright (c) 2021 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package kubernetes + +import ( + "testing" + + "github.com/go-vela/types/pipeline" + + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func TestKubernetes_New(t *testing.T) { + // setup tests + tests := []struct { + failure bool + namespace string + path string + }{ + { + failure: false, + namespace: "test", + path: "testdata/config", + }, + { + failure: true, + namespace: "test", + path: "testdata/config_empty", + }, + // An empty path implies that we are running in kubernetes, + // so we should use InClusterConfig. Tests, however, do not + // run in kubernetes, so we would need a way to mock the + // return value of rest.InClusterConfig(), but how? + //{ + // failure: false, + // namespace: "test", + // path: "", + //}, + } + + // run tests + for _, test := range tests { + _, err := New( + WithConfigFile(test.path), + WithNamespace(test.namespace), + ) + + if test.failure { + if err == nil { + t.Errorf("New should have returned err") + } + + continue + } + + if err != nil { + t.Errorf("New returned err: %v", err) + } + } +} + +// setup global variables used for testing. +var ( + _container = &pipeline.Container{ + ID: "step-github-octocat-1-clone", + Directory: "/vela/src/github.com/octocat/helloworld", + Environment: map[string]string{"FOO": "bar"}, + Image: "target/vela-git:v0.4.0", + Name: "clone", + Number: 2, + Pull: "always", + } + + _pod = &v1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "github-octocat-1", + Namespace: "test", + Labels: map[string]string{ + "pipeline": "github-octocat-1", + }, + }, + TypeMeta: metav1.TypeMeta{ + APIVersion: "v1", + Kind: "Pod", + }, + Status: v1.PodStatus{ + Phase: v1.PodRunning, + ContainerStatuses: []v1.ContainerStatus{ + { + Name: "step-github-octocat-1-clone", + State: v1.ContainerState{ + Terminated: &v1.ContainerStateTerminated{ + Reason: "Completed", + ExitCode: 0, + }, + }, + }, + { + Name: "step-github-octocat-1-echo", + State: v1.ContainerState{ + Terminated: &v1.ContainerStateTerminated{ + Reason: "Completed", + ExitCode: 0, + }, + }, + }, + }, + }, + Spec: v1.PodSpec{ + Containers: []v1.Container{ + { + Name: "step-github-octocat-1-clone", + Image: "target/vela-git:v0.4.0", + WorkingDir: "/vela/src/github.com/octocat/helloworld", + ImagePullPolicy: v1.PullAlways, + }, + { + Name: "step-github-octocat-1-echo", + Image: "alpine:latest", + WorkingDir: "/vela/src/github.com/octocat/helloworld", + ImagePullPolicy: v1.PullAlways, + }, + { + Name: "service-github-octocat-1-postgres", + Image: "postgres:12-alpine", + WorkingDir: "/vela/src/github.com/octocat/helloworld", + ImagePullPolicy: v1.PullAlways, + }, + }, + HostAliases: []v1.HostAlias{ + { + IP: "127.0.0.1", + Hostnames: []string{ + "postgres.local", + "echo.local", + }, + }, + }, + Volumes: []v1.Volume{ + { + Name: "github-octocat-1", + VolumeSource: v1.VolumeSource{ + EmptyDir: &v1.EmptyDirVolumeSource{}, + }, + }, + }, + }, + } + + _stages = &pipeline.Build{ + Version: "1", + ID: "github-octocat-1", + Services: pipeline.ContainerSlice{ + { + ID: "service-github-octocat-1-postgres", + Directory: "/vela/src/github.com/octocat/helloworld", + Environment: map[string]string{"FOO": "bar"}, + Image: "postgres:12-alpine", + Name: "postgres", + Number: 4, + Ports: []string{"5432:5432"}, + }, + }, + Stages: pipeline.StageSlice{ + { + Name: "init", + Steps: pipeline.ContainerSlice{ + { + ID: "step-github-octocat-1-init-init", + Directory: "/vela/src/github.com/octocat/helloworld", + Environment: map[string]string{"FOO": "bar"}, + Image: "#init", + Name: "init", + Number: 1, + Pull: "always", + }, + }, + }, + { + Name: "clone", + Needs: []string{"init"}, + Steps: pipeline.ContainerSlice{ + { + ID: "step-github-octocat-1-clone-clone", + Directory: "/vela/src/github.com/octocat/helloworld", + Environment: map[string]string{"FOO": "bar"}, + Image: "target/vela-git:v0.4.0", + Name: "clone", + Number: 2, + Pull: "always", + }, + }, + }, + { + Name: "echo", + Needs: []string{"clone"}, + Steps: pipeline.ContainerSlice{ + { + ID: "step-github-octocat-1-echo-echo", + Commands: []string{"echo hello"}, + Detach: true, + Directory: "/vela/src/github.com/octocat/helloworld", + Environment: map[string]string{"FOO": "bar"}, + Image: "alpine:latest", + Name: "echo", + Number: 3, + Pull: "always", + }, + }, + }, + }, + } + + _steps = &pipeline.Build{ + Version: "1", + ID: "github-octocat-1", + Services: pipeline.ContainerSlice{ + { + ID: "service-github-octocat-1-postgres", + Directory: "/vela/src/github.com/octocat/helloworld", + Environment: map[string]string{"FOO": "bar"}, + Image: "postgres:12-alpine", + Name: "postgres", + Number: 4, + Ports: []string{"5432:5432"}, + }, + }, + Steps: pipeline.ContainerSlice{ + { + ID: "step-github-octocat-1-init", + Directory: "/vela/src/github.com/octocat/helloworld", + Environment: map[string]string{"FOO": "bar"}, + Image: "#init", + Name: "init", + Number: 1, + Pull: "always", + }, + { + ID: "step-github-octocat-1-clone", + Directory: "/vela/src/github.com/octocat/helloworld", + Environment: map[string]string{"FOO": "bar"}, + Image: "target/vela-git:v0.4.0", + Name: "clone", + Number: 2, + Pull: "always", + }, + { + ID: "step-github-octocat-1-echo", + Commands: []string{"echo hello"}, + Detach: true, + Directory: "/vela/src/github.com/octocat/helloworld", + Environment: map[string]string{"FOO": "bar"}, + Image: "alpine:latest", + Name: "echo", + Number: 3, + Pull: "always", + }, + }, + } + + _stagesPod = &v1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "github-octocat-1", + Namespace: "test", + Labels: map[string]string{ + "pipeline": "github-octocat-1", + }, + }, + TypeMeta: metav1.TypeMeta{ + APIVersion: "v1", + Kind: "Pod", + }, + Spec: v1.PodSpec{ + Containers: []v1.Container{ + { + Name: "step-github-octocat-1-clone-clone", + Image: "target/vela-git:v0.4.0", + WorkingDir: "/vela/src/github.com/octocat/helloworld", + ImagePullPolicy: v1.PullAlways, + }, + { + Name: "step-github-octocat-1-echo-echo", + Image: "alpine:latest", + WorkingDir: "/vela/src/github.com/octocat/helloworld", + ImagePullPolicy: v1.PullAlways, + }, + { + Name: "service-github-octocat-1-postgres", + Image: "postgres:12-alpine", + WorkingDir: "/vela/src/github.com/octocat/helloworld", + ImagePullPolicy: v1.PullAlways, + }, + }, + HostAliases: []v1.HostAlias{ + { + IP: "127.0.0.1", + Hostnames: []string{ + "postgres.local", + "echo.local", + }, + }, + }, + Volumes: []v1.Volume{ + { + Name: "github-octocat-1", + VolumeSource: v1.VolumeSource{ + EmptyDir: &v1.EmptyDirVolumeSource{}, + }, + }, + }, + }, + } +) diff --git a/runtime/kubernetes/network.go b/runtime/kubernetes/network.go new file mode 100644 index 00000000..a481aefb --- /dev/null +++ b/runtime/kubernetes/network.go @@ -0,0 +1,124 @@ +// Copyright (c) 2021 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package kubernetes + +import ( + "context" + "encoding/json" + "fmt" + + v1 "k8s.io/api/core/v1" + + "github.com/go-vela/types/pipeline" + + "github.com/sirupsen/logrus" +) + +// CreateNetwork creates the pipeline network. +func (c *client) CreateNetwork(ctx context.Context, b *pipeline.Build) error { + logrus.Tracef("creating network for pipeline %s", b.ID) + + // nolint: lll // ignore long line length due to link + // create the network for the pod + // + // This is done due to the nature of how networking works inside the + // pod. Each container inside the pod shares the same network IP and + // port space. This allows them to communicate with each other via + // localhost. However, to keep the runtime behavior consistent, + // Vela adds DNS entries for each container that requires it. + // + // More info: + // * https://kubernetes.io/docs/concepts/workloads/pods/pod/ + // * https://kubernetes.io/docs/concepts/services-networking/add-entries-to-pod-etc-hosts-with-host-aliases/ + // + // https://pkg.go.dev/k8s.io/api/core/v1?tab=doc#HostAlias + network := v1.HostAlias{ + IP: "127.0.0.1", + Hostnames: []string{}, + } + + // iterate through all services in the pipeline + for _, service := range b.Services { + // create the host entry for the pod container aliases + host := fmt.Sprintf("%s.local", service.Name) + + // add the host entry to the pod container aliases + network.Hostnames = append(network.Hostnames, host) + } + + // iterate through all steps in the pipeline + for _, step := range b.Steps { + // skip all steps not running in detached mode + if !step.Detach { + continue + } + + // create the host entry for the pod container aliases + host := fmt.Sprintf("%s.local", step.Name) + + // add the host entry to the pod container aliases + network.Hostnames = append(network.Hostnames, host) + } + + // iterate through all stages in the pipeline + for _, stage := range b.Stages { + // iterate through all steps in each stage + for _, step := range stage.Steps { + // skip all steps not running in detached mode + if !step.Detach { + continue + } + + // create the host entry for the pod container aliases + host := fmt.Sprintf("%s.local", step.Name) + + // add the host entry to the pod container aliases + network.Hostnames = append(network.Hostnames, host) + } + } + + // add the network definition to the pod spec + // + // https://pkg.go.dev/k8s.io/api/core/v1?tab=doc#PodSpec + c.Pod.Spec.HostAliases = append(c.Pod.Spec.HostAliases, network) + + return nil +} + +// InspectNetwork inspects the pipeline network. +func (c *client) InspectNetwork(ctx context.Context, b *pipeline.Build) ([]byte, error) { + logrus.Tracef("inspecting network for pipeline %s", b.ID) + + // TODO: consider updating this command + // + // create output for inspecting volume + output := []byte( + fmt.Sprintf("$ kubectl get pod -o=jsonpath='{.spec.hostAliases}' %s\n", b.ID), + ) + + // marshal the network information from the pod + network, err := json.MarshalIndent(c.Pod.Spec.HostAliases, "", " ") + if err != nil { + return output, err + } + + return append(output, append(network, "\n"...)...), nil +} + +// RemoveNetwork deletes the pipeline network. +// +// Currently, this is comparable to a no-op because in Kubernetes the +// network lives and dies with the pod it's attached to. However, Vela +// uses it to cleanup the network definition for the pod. +func (c *client) RemoveNetwork(ctx context.Context, b *pipeline.Build) error { + logrus.Tracef("removing network for pipeline %s", b.ID) + + // remove the network definition from the pod spec + // + // https://pkg.go.dev/k8s.io/api/core/v1?tab=doc#PodSpec + c.Pod.Spec.HostAliases = []v1.HostAlias{} + + return nil +} diff --git a/runtime/kubernetes/network_test.go b/runtime/kubernetes/network_test.go new file mode 100644 index 00000000..2428da27 --- /dev/null +++ b/runtime/kubernetes/network_test.go @@ -0,0 +1,132 @@ +// Copyright (c) 2021 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package kubernetes + +import ( + "context" + "testing" + + "github.com/go-vela/types/pipeline" +) + +func TestKubernetes_CreateNetwork(t *testing.T) { + // setup types + _engine, err := NewMock(_pod) + if err != nil { + t.Errorf("unable to create runtime engine: %v", err) + } + + // setup tests + tests := []struct { + failure bool + pipeline *pipeline.Build + }{ + { + failure: false, + pipeline: _stages, + }, + { + failure: false, + pipeline: _steps, + }, + } + + // run tests + for _, test := range tests { + err := _engine.CreateNetwork(context.Background(), test.pipeline) + + if test.failure { + if err == nil { + t.Errorf("CreateNetwork should have returned err") + } + + continue + } + + if err != nil { + t.Errorf("CreateNetwork returned err: %v", err) + } + } +} + +func TestKubernetes_InspectNetwork(t *testing.T) { + // setup types + _engine, err := NewMock(_pod) + if err != nil { + t.Errorf("unable to create runtime engine: %v", err) + } + + // setup tests + tests := []struct { + failure bool + pipeline *pipeline.Build + }{ + { + failure: false, + pipeline: _stages, + }, + { + failure: false, + pipeline: _steps, + }, + } + + // run tests + for _, test := range tests { + _, err = _engine.InspectNetwork(context.Background(), test.pipeline) + + if test.failure { + if err == nil { + t.Errorf("InspectNetwork should have returned err") + } + + continue + } + + if err != nil { + t.Errorf("InspectNetwork returned err: %v", err) + } + } +} + +func TestKubernetes_RemoveNetwork(t *testing.T) { + // setup types + _engine, err := NewMock(_pod) + if err != nil { + t.Errorf("unable to create runtime engine: %v", err) + } + + // setup tests + tests := []struct { + failure bool + pipeline *pipeline.Build + }{ + { + failure: false, + pipeline: _stages, + }, + { + failure: false, + pipeline: _steps, + }, + } + + // run tests + for _, test := range tests { + err = _engine.RemoveNetwork(context.Background(), test.pipeline) + + if test.failure { + if err == nil { + t.Errorf("RemoveNetwork should have returned err") + } + + continue + } + + if err != nil { + t.Errorf("RemoveNetwork returned err: %v", err) + } + } +} diff --git a/runtime/kubernetes/opts.go b/runtime/kubernetes/opts.go new file mode 100644 index 00000000..b30d2f05 --- /dev/null +++ b/runtime/kubernetes/opts.go @@ -0,0 +1,67 @@ +// Copyright (c) 2021 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package kubernetes + +import ( + "fmt" + + "github.com/sirupsen/logrus" +) + +// ClientOpt represents a configuration option to initialize the runtime client. +type ClientOpt func(*client) error + +// WithConfigFile sets the Kubernetes config file in the runtime client. +func WithConfigFile(file string) ClientOpt { + logrus.Trace("configuring config file in kubernetes runtime client") + + return func(c *client) error { + // set the runtime config file in the kubernetes client + c.config.File = file + + return nil + } +} + +// WithNamespace sets the Kubernetes namespace in the runtime client. +func WithNamespace(namespace string) ClientOpt { + logrus.Trace("configuring namespace in kubernetes runtime client") + + return func(c *client) error { + // check if the namespace provided is empty + if len(namespace) == 0 { + return fmt.Errorf("no Kubernetes namespace provided") + } + + // set the runtime namespace in the kubernetes client + c.config.Namespace = namespace + + return nil + } +} + +// WithPrivilegedImages sets the Kubernetes privileged images in the runtime client. +func WithPrivilegedImages(images []string) ClientOpt { + logrus.Trace("configuring privileged images in kubernetes runtime client") + + return func(c *client) error { + // set the runtime privileged images in the kubernetes client + c.config.Images = images + + return nil + } +} + +// WithHostVolumes sets the Kubernetes host volumes in the runtime client. +func WithHostVolumes(volumes []string) ClientOpt { + logrus.Trace("configuring host volumes in kubernetes runtime client") + + return func(c *client) error { + // set the runtime host volumes in the kubernetes client + c.config.Volumes = volumes + + return nil + } +} diff --git a/runtime/kubernetes/opts_test.go b/runtime/kubernetes/opts_test.go new file mode 100644 index 00000000..45a149c1 --- /dev/null +++ b/runtime/kubernetes/opts_test.go @@ -0,0 +1,163 @@ +// Copyright (c) 2021 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package kubernetes + +import ( + "reflect" + "testing" +) + +func TestKubernetes_ClientOpt_WithConfigFile(t *testing.T) { + // setup tests + tests := []struct { + failure bool + file string + want string + }{ + { + failure: false, + file: "testdata/config", + want: "testdata/config", + }, + { + failure: true, + file: "", + want: "", + }, + } + + // run tests + for _, test := range tests { + _engine, err := New( + WithConfigFile(test.file), + ) + + if test.failure { + if err == nil { + t.Errorf("WithConfigFile should have returned err") + } + + continue + } + + if err != nil { + t.Errorf("WithConfigFile returned err: %v", err) + } + + if !reflect.DeepEqual(_engine.config.File, test.want) { + t.Errorf("WithConfigFile is %v, want %v", _engine.config.File, test.want) + } + } +} + +func TestKubernetes_ClientOpt_WithNamespace(t *testing.T) { + // setup tests + tests := []struct { + failure bool + namespace string + want string + }{ + { + failure: false, + namespace: "foo", + want: "foo", + }, + { + failure: true, + namespace: "", + want: "", + }, + } + + // run tests + for _, test := range tests { + _engine, err := New( + WithConfigFile("testdata/config"), + WithNamespace(test.namespace), + ) + + if test.failure { + if err == nil { + t.Errorf("WithNamespace should have returned err") + } + + continue + } + + if err != nil { + t.Errorf("WithNamespace returned err: %v", err) + } + + if !reflect.DeepEqual(_engine.config.Namespace, test.want) { + t.Errorf("WithNamespace is %v, want %v", _engine.config.Namespace, test.want) + } + } +} + +func TestKubernetes_ClientOpt_WithHostVolumes(t *testing.T) { + // setup tests + tests := []struct { + volumes []string + want []string + }{ + { + volumes: []string{"/foo/bar.txt:/foo/bar.txt", "/tmp/baz.conf:/tmp/baz.conf"}, + want: []string{"/foo/bar.txt:/foo/bar.txt", "/tmp/baz.conf:/tmp/baz.conf"}, + }, + { + volumes: []string{}, + want: []string{}, + }, + } + + // run tests + for _, test := range tests { + _engine, err := New( + WithConfigFile("testdata/config"), + WithHostVolumes(test.volumes), + ) + + if err != nil { + t.Errorf("WithHostVolumes returned err: %v", err) + } + + if !reflect.DeepEqual(_engine.config.Volumes, test.want) { + t.Errorf("WithHostVolumes is %v, want %v", _engine.config.Volumes, test.want) + } + } +} + +func TestKubernetes_ClientOpt_WithPrivilegedImages(t *testing.T) { + // setup tests + tests := []struct { + images []string + want []string + }{ + { + images: []string{"alpine", "golang"}, + want: []string{"alpine", "golang"}, + }, + { + images: []string{}, + want: []string{}, + }, + } + + // run tests + for _, test := range tests { + _engine, err := New( + WithConfigFile("testdata/config"), + WithPrivilegedImages(test.images), + ) + + if err != nil { + t.Errorf("WithPrivilegedImages returned err: %v", err) + } + + if !reflect.DeepEqual(_engine.config.Images, test.want) { + t.Errorf("WithPrivilegedImages is %v, want %v", _engine.config.Images, test.want) + } + } +} diff --git a/runtime/kubernetes/testdata/config b/runtime/kubernetes/testdata/config new file mode 100644 index 00000000..ec9cdfd9 --- /dev/null +++ b/runtime/kubernetes/testdata/config @@ -0,0 +1,18 @@ +apiVersion: v1 +clusters: +- cluster: + server: https://localhost:443 + name: foo +contexts: +- context: + cluster: foo + namespace: test + user: foo + name: foo +current-context: foo +kind: Config +preferences: {} +users: +- name: foo + user: + token: somerandomstringqwerty \ No newline at end of file diff --git a/runtime/kubernetes/testdata/config_empty b/runtime/kubernetes/testdata/config_empty new file mode 100644 index 00000000..e69de29b diff --git a/runtime/kubernetes/volume.go b/runtime/kubernetes/volume.go new file mode 100644 index 00000000..974c2a8d --- /dev/null +++ b/runtime/kubernetes/volume.go @@ -0,0 +1,174 @@ +// Copyright (c) 2021 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package kubernetes + +import ( + "context" + "encoding/json" + "fmt" + "strings" + + v1 "k8s.io/api/core/v1" + + "github.com/go-vela/types/constants" + "github.com/go-vela/types/pipeline" + vol "github.com/go-vela/worker/internal/volume" + + "github.com/sirupsen/logrus" +) + +// CreateVolume creates the pipeline volume. +func (c *client) CreateVolume(ctx context.Context, b *pipeline.Build) error { + logrus.Tracef("creating volume for pipeline %s", b.ID) + + // create the workspace volume for the pod + // + // This is done due to the nature of how volumes works inside + // the pod. Each container inside the pod can access and use + // the same volume. This allows them to share this volume + // throughout the life of the pod. However, to keep the + // runtime behavior consistent, Vela uses an emtpyDir volume + // because that volume only exists for the life + // of the pod. + // + // More info: + // * https://kubernetes.io/docs/concepts/workloads/pods/pod/ + // * https://kubernetes.io/docs/concepts/storage/volumes/#emptydir + // + // https://pkg.go.dev/k8s.io/api/core/v1?tab=doc#Volume + workspaceVolume := v1.Volume{ + Name: b.ID, + VolumeSource: v1.VolumeSource{ + EmptyDir: &v1.EmptyDirVolumeSource{}, + }, + } + + // create the workspace volumeMount for the pod + // + // https://pkg.go.dev/k8s.io/api/core/v1?tab=doc#VolumeMount + workspaceVolumeMount := v1.VolumeMount{ + Name: b.ID, + MountPath: constants.WorkspaceMount, + } + + // add the volume definition to the pod spec + // + // https://pkg.go.dev/k8s.io/api/core/v1?tab=doc#PodSpec + c.Pod.Spec.Volumes = append(c.Pod.Spec.Volumes, workspaceVolume) + + // save the volumeMount to add to each of the containers in the pod spec later + c.commonVolumeMounts = append(c.commonVolumeMounts, workspaceVolumeMount) + + // check if global host volumes were provided (VELA_RUNTIME_VOLUMES) + if len(c.config.Volumes) > 0 { + // iterate through all volumes provided + for k, v := range c.config.Volumes { + // parse the volume provided + _volume := vol.Parse(v) + _volumeName := fmt.Sprintf("%s_%d", b.ID, k) + + // add the volume to the set of pod volumes + c.Pod.Spec.Volumes = append(c.Pod.Spec.Volumes, v1.Volume{ + Name: _volumeName, + VolumeSource: v1.VolumeSource{ + HostPath: &v1.HostPathVolumeSource{ + Path: _volume.Source, + }, + }, + }) + + // save the volumeMounts for later addition to each container's mounts + c.commonVolumeMounts = append(c.commonVolumeMounts, v1.VolumeMount{ + Name: _volumeName, + MountPath: _volume.Destination, + }) + } + } + + // TODO: extend c.config.Volumes to include container-specific volumes (container.Volumes) + + return nil +} + +// InspectVolume inspects the pipeline volume. +func (c *client) InspectVolume(ctx context.Context, b *pipeline.Build) ([]byte, error) { + logrus.Tracef("inspecting volume for pipeline %s", b.ID) + + // TODO: consider updating this command + // + // create output for inspecting volume + output := []byte( + fmt.Sprintf("$ kubectl get pod -o=jsonpath='{.spec.volumes}' %s\n", b.ID), + ) + + // marshal the volume information from the pod + volume, err := json.MarshalIndent(c.Pod.Spec.Volumes, "", " ") + if err != nil { + return nil, err + } + + return append(output, append(volume, "\n"...)...), nil +} + +// RemoveVolume deletes the pipeline volume. +// +// Currently, this is comparable to a no-op because in Kubernetes the +// volume lives and dies with the pod it's attached to. However, Vela +// uses it to cleanup the volume definition for the pod. +func (c *client) RemoveVolume(ctx context.Context, b *pipeline.Build) error { + logrus.Tracef("removing volume for pipeline %s", b.ID) + + // remove the volume definition from the pod spec + // + // https://pkg.go.dev/k8s.io/api/core/v1?tab=doc#PodSpec + c.Pod.Spec.Volumes = []v1.Volume{} + + return nil +} + +// setupVolumeMounts generates the VolumeMounts for a given container. +// nolint:unparam // keep signature similar to Engine interface methods despite unused ctx and err +func (c *client) setupVolumeMounts(ctx context.Context, ctn *pipeline.Container) ( + volumeMounts []v1.VolumeMount, + err error, +) { + logrus.Tracef("setting up VolumeMounts for container %s", ctn.ID) + + // add workspace mount and any global host mounts (VELA_RUNTIME_VOLUMES) + volumeMounts = append(volumeMounts, c.commonVolumeMounts...) + + // -------------------- Start of TODO: -------------------- + // + // Remove the below code once the mounting issue with Kaniko is + // resolved to allow mounting private cert bundles with Vela. + // + // This code is required due to a known bug in Kaniko: + // + // * https://github.com/go-vela/community/issues/253 + + // check if the pipeline container image contains + // the key words "kaniko" and "vela" + // + // this is a soft check for the Vela Kaniko plugin + if strings.Contains(ctn.Image, "kaniko") && + strings.Contains(ctn.Image, "vela") { + // iterate through the list of host mounts provided + for i, mount := range volumeMounts { + // check if the path for the mount contains "/etc/ssl/certs" + // + // this is a soft check for mounting private cert bundles + if strings.Contains(mount.MountPath, "/etc/ssl/certs") { + // remove the private cert bundle mount from the host config + volumeMounts = append(volumeMounts[:i], volumeMounts[i+1:]...) + } + } + } + // + // -------------------- End of TODO: -------------------- + + // TODO: extend volumeMounts based on ctn.Volumes + + return volumeMounts, nil +} diff --git a/runtime/kubernetes/volume_test.go b/runtime/kubernetes/volume_test.go new file mode 100644 index 00000000..0821a26b --- /dev/null +++ b/runtime/kubernetes/volume_test.go @@ -0,0 +1,136 @@ +// Copyright (c) 2021 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package kubernetes + +import ( + "context" + "testing" + + "github.com/go-vela/types/pipeline" + + v1 "k8s.io/api/core/v1" +) + +func TestKubernetes_CreateVolume(t *testing.T) { + // setup types + _engine, err := NewMock(_pod) + if err != nil { + t.Errorf("unable to create runtime engine: %v", err) + } + + // setup tests + tests := []struct { + failure bool + pipeline *pipeline.Build + }{ + { + failure: false, + pipeline: _stages, + }, + { + failure: false, + pipeline: _steps, + }, + } + + // run tests + for _, test := range tests { + err = _engine.CreateVolume(context.Background(), test.pipeline) + + if test.failure { + if err == nil { + t.Errorf("CreateVolume should have returned err") + } + + continue + } + + if err != nil { + t.Errorf("CreateVolume returned err: %v", err) + } + } +} + +func TestKubernetes_InspectVolume(t *testing.T) { + // setup tests + tests := []struct { + failure bool + pipeline *pipeline.Build + pod *v1.Pod + }{ + { + failure: false, + pipeline: _stages, + pod: _pod, + }, + { + failure: false, + pipeline: _steps, + pod: _pod, + }, + } + + // run tests + for _, test := range tests { + _engine, err := NewMock(test.pod) + if err != nil { + t.Errorf("unable to create runtime engine: %v", err) + } + + _, err = _engine.InspectVolume(context.Background(), test.pipeline) + + if test.failure { + if err == nil { + t.Errorf("InspectVolume should have returned err") + } + + continue + } + + if err != nil { + t.Errorf("InspectVolume returned err: %v", err) + } + } +} + +func TestKubernetes_RemoveVolume(t *testing.T) { + // setup types + _engine, err := NewMock(_pod) + if err != nil { + t.Errorf("unable to create runtime engine: %v", err) + } + + // setup tests + tests := []struct { + failure bool + pipeline *pipeline.Build + }{ + { + failure: false, + pipeline: _stages, + }, + { + failure: false, + pipeline: _steps, + }, + } + + // run tests + for _, test := range tests { + err = _engine.RemoveVolume(context.Background(), test.pipeline) + + if test.failure { + if err == nil { + t.Errorf("RemoveVolume should have returned err") + } + + continue + } + + if err != nil { + t.Errorf("RemoveVolume returned err: %v", err) + } + } +} diff --git a/runtime/runtime.go b/runtime/runtime.go new file mode 100644 index 00000000..8bcb182e --- /dev/null +++ b/runtime/runtime.go @@ -0,0 +1,50 @@ +// Copyright (c) 2021 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package runtime + +import ( + "fmt" + + "github.com/go-vela/types/constants" + + "github.com/sirupsen/logrus" +) + +// nolint: godot // ignore period at end for comment ending in a list +// +// New creates and returns a Vela engine capable of +// integrating with the configured runtime. +// +// Currently the following runtimes are supported: +// +// * docker +// * kubernetes +func New(s *Setup) (Engine, error) { + // validate the setup being provided + // + // https://pkg.go.dev/github.com/go-vela/worker/runtime?tab=doc#Setup.Validate + err := s.Validate() + if err != nil { + return nil, err + } + + logrus.Debug("creating runtime engine from setup") + // process the runtime driver being provided + switch s.Driver { + case constants.DriverDocker: + // handle the Docker runtime driver being provided + // + // https://pkg.go.dev/github.com/go-vela/worker/runtime?tab=doc#Setup.Docker + return s.Docker() + case constants.DriverKubernetes: + // handle the Kubernetes runtime driver being provided + // + // https://pkg.go.dev/github.com/go-vela/worker/runtime?tab=doc#Setup.Kubernetes + return s.Kubernetes() + default: + // handle an invalid runtime driver being provided + return nil, fmt.Errorf("invalid runtime driver provided: %s", s.Driver) + } +} diff --git a/runtime/runtime_test.go b/runtime/runtime_test.go new file mode 100644 index 00000000..ba0f4a56 --- /dev/null +++ b/runtime/runtime_test.go @@ -0,0 +1,63 @@ +// Copyright (c) 2021 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package runtime + +import ( + "testing" + + "github.com/go-vela/types/constants" +) + +func TestRuntime_New(t *testing.T) { + // setup tests + tests := []struct { + failure bool + setup *Setup + }{ + { + failure: false, + setup: &Setup{ + Driver: constants.DriverDocker, + }, + }, + { + failure: false, + setup: &Setup{ + Driver: constants.DriverKubernetes, + Namespace: "docker", + ConfigFile: "testdata/config", + }, + }, + { + failure: true, + setup: &Setup{ + Driver: "invalid", + }, + }, + { + failure: true, + setup: &Setup{ + Driver: "", + }, + }, + } + + // run tests + for _, test := range tests { + _, err := New(test.setup) + + if test.failure { + if err == nil { + t.Errorf("New should have returned err") + } + + continue + } + + if err != nil { + t.Errorf("New returned err: %v", err) + } + } +} diff --git a/runtime/setup.go b/runtime/setup.go new file mode 100644 index 00000000..e4f6ac0b --- /dev/null +++ b/runtime/setup.go @@ -0,0 +1,89 @@ +// Copyright (c) 2021 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package runtime + +import ( + "fmt" + + "github.com/go-vela/worker/runtime/docker" + "github.com/go-vela/worker/runtime/kubernetes" + + "github.com/go-vela/types/constants" + + "github.com/sirupsen/logrus" +) + +// Setup represents the configuration necessary for +// creating a Vela engine capable of integrating +// with a configured runtime environment. +type Setup struct { + // Runtime Configuration + + // specifies the driver to use for the runtime client + Driver string + // specifies the path to a configuration file to use for the runtime client + ConfigFile string + // specifies a list of host volumes to use for the runtime client + HostVolumes []string + // specifies the namespace to use for the runtime client (only used by kubernetes) + Namespace string + // specifies a list of privileged images to use for the runtime client + PrivilegedImages []string +} + +// Docker creates and returns a Vela engine capable of +// integrating with a Docker runtime environment. +func (s *Setup) Docker() (Engine, error) { + logrus.Trace("creating docker runtime client from setup") + + // create new Docker runtime engine + // + // https://pkg.go.dev/github.com/go-vela/worker/runtime/docker?tab=doc#New + return docker.New( + docker.WithHostVolumes(s.HostVolumes), + docker.WithPrivilegedImages(s.PrivilegedImages), + ) +} + +// Kubernetes creates and returns a Vela engine capable of +// integrating with a Kubernetes runtime environment. +func (s *Setup) Kubernetes() (Engine, error) { + logrus.Trace("creating kubernetes runtime client from setup") + + // create new Kubernetes runtime engine + // + // https://pkg.go.dev/github.com/go-vela/worker/runtime/kubernetes?tab=doc#New + return kubernetes.New( + kubernetes.WithConfigFile(s.ConfigFile), + kubernetes.WithHostVolumes(s.HostVolumes), + kubernetes.WithNamespace(s.Namespace), + kubernetes.WithPrivilegedImages(s.PrivilegedImages), + ) +} + +// Validate verifies the necessary fields for the +// provided configuration are populated correctly. +func (s *Setup) Validate() error { + logrus.Trace("validating runtime setup for client") + + // check if a runtime driver was provided + if len(s.Driver) == 0 { + return fmt.Errorf("no runtime driver provided") + } + + // process the secret driver being provided + switch s.Driver { + case constants.DriverDocker: + break + case constants.DriverKubernetes: + // check if a runtime namespace was provided + if len(s.Namespace) == 0 { + return fmt.Errorf("no runtime namespace provided") + } + } + + // setup is valid + return nil +} diff --git a/runtime/setup_test.go b/runtime/setup_test.go new file mode 100644 index 00000000..3966472a --- /dev/null +++ b/runtime/setup_test.go @@ -0,0 +1,91 @@ +// Copyright (c) 2021 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package runtime + +import ( + "testing" + + "github.com/go-vela/types/constants" +) + +func TestRuntime_Setup_Docker(t *testing.T) { + // setup types + _setup := &Setup{ + Driver: constants.DriverDocker, + } + + // run test + _, err := _setup.Docker() + if err != nil { + t.Errorf("Docker returned err: %v", err) + } +} + +func TestRuntime_Setup_Kubernetes(t *testing.T) { + // setup types + _setup := &Setup{ + Driver: constants.DriverKubernetes, + ConfigFile: "testdata/config", + Namespace: "docker", + } + + // run test + _, err := _setup.Kubernetes() + if err != nil { + t.Errorf("Kubernetes returned err: %v", err) + } +} + +func TestRuntime_Validate(t *testing.T) { + // setup types + tests := []struct { + failure bool + setup *Setup + want error + }{ + { + failure: false, + setup: &Setup{ + Driver: constants.DriverDocker, + }, + }, + { + failure: false, + setup: &Setup{ + Driver: constants.DriverKubernetes, + Namespace: "docker", + }, + }, + { + failure: true, + setup: &Setup{ + Driver: "", + }, + }, + { + failure: true, + setup: &Setup{ + Driver: constants.DriverKubernetes, + }, + }, + } + + // run tests + for _, test := range tests { + err := test.setup.Validate() + + if test.failure { + if err == nil { + t.Errorf("Validate should have returned err") + } + + continue + } + + if err != nil { + t.Errorf("Validate returned err: %v", err) + } + } +} diff --git a/runtime/testdata/config b/runtime/testdata/config new file mode 100644 index 00000000..ec9cdfd9 --- /dev/null +++ b/runtime/testdata/config @@ -0,0 +1,18 @@ +apiVersion: v1 +clusters: +- cluster: + server: https://localhost:443 + name: foo +contexts: +- context: + cluster: foo + namespace: test + user: foo + name: foo +current-context: foo +kind: Config +preferences: {} +users: +- name: foo + user: + token: somerandomstringqwerty \ No newline at end of file diff --git a/runtime/testdata/large.yml b/runtime/testdata/large.yml new file mode 100644 index 00000000..b760122e --- /dev/null +++ b/runtime/testdata/large.yml @@ -0,0 +1,605 @@ +version: "1" + +metadata: + template: false + +steps: + - name: echo_1 + commands: + - echo hello + image: alpine:latest + pull: true + + - name: echo_2 + commands: + - echo hello + image: alpine:latest + pull: true + + - name: echo_3 + commands: + - echo hello + image: alpine:latest + pull: true + + - name: echo_4 + commands: + - echo hello + image: alpine:latest + pull: true + + - name: echo_5 + commands: + - echo hello + image: alpine:latest + pull: true + + - name: echo_6 + commands: + - echo hello + image: alpine:latest + pull: true + + - name: echo_7 + commands: + - echo hello + image: alpine:latest + pull: true + + - name: echo_8 + commands: + - echo hello + image: alpine:latest + pull: true + + - name: echo_9 + commands: + - echo hello + image: alpine:latest + pull: true + + - name: echo_10 + commands: + - echo hello + image: alpine:latest + pull: true + + - name: echo_11 + commands: + - echo hello + image: alpine:latest + pull: true + + - name: echo_12 + commands: + - echo hello + image: alpine:latest + pull: true + + - name: echo_13 + commands: + - echo hello + image: alpine:latest + pull: true + + - name: echo_14 + commands: + - echo hello + image: alpine:latest + pull: true + + - name: echo_15 + commands: + - echo hello + image: alpine:latest + pull: true + + - name: echo_16 + commands: + - echo hello + image: alpine:latest + pull: true + + - name: echo_17 + commands: + - echo hello + image: alpine:latest + pull: true + + - name: echo_18 + commands: + - echo hello + image: alpine:latest + pull: true + + - name: echo_19 + commands: + - echo hello + image: alpine:latest + pull: true + + - name: echo_20 + commands: + - echo hello + image: alpine:latest + pull: true + + - name: echo_21 + commands: + - echo hello + image: alpine:latest + pull: true + + - name: echo_22 + commands: + - echo hello + image: alpine:latest + pull: true + + - name: echo_23 + commands: + - echo hello + image: alpine:latest + pull: true + + - name: echo_24 + commands: + - echo hello + image: alpine:latest + pull: true + + - name: echo_25 + commands: + - echo hello + image: alpine:latest + pull: true + + - name: echo_26 + commands: + - echo hello + image: alpine:latest + pull: true + + - name: echo_27 + commands: + - echo hello + image: alpine:latest + pull: true + + - name: echo_28 + commands: + - echo hello + image: alpine:latest + pull: true + + - name: echo_29 + commands: + - echo hello + image: alpine:latest + pull: true + + - name: echo_30 + commands: + - echo hello + image: alpine:latest + pull: true + + - name: echo_31 + commands: + - echo hello + image: alpine:latest + pull: true + + - name: echo_32 + commands: + - echo hello + image: alpine:latest + pull: true + + - name: echo_33 + commands: + - echo hello + image: alpine:latest + pull: true + + - name: echo_34 + commands: + - echo hello + image: alpine:latest + pull: true + + - name: echo_35 + commands: + - echo hello + image: alpine:latest + pull: true + + - name: echo_36 + commands: + - echo hello + image: alpine:latest + pull: true + + - name: echo_37 + commands: + - echo hello + image: alpine:latest + pull: true + + - name: echo_38 + commands: + - echo hello + image: alpine:latest + pull: true + + - name: echo_39 + commands: + - echo hello + image: alpine:latest + pull: true + + - name: echo_40 + commands: + - echo hello + image: alpine:latest + pull: true + + - name: echo_41 + commands: + - echo hello + image: alpine:latest + pull: true + + - name: echo_42 + commands: + - echo hello + image: alpine:latest + pull: true + + - name: echo_43 + commands: + - echo hello + image: alpine:latest + pull: true + + - name: echo_44 + commands: + - echo hello + image: alpine:latest + pull: true + + - name: echo_45 + commands: + - echo hello + image: alpine:latest + pull: true + + - name: echo_46 + commands: + - echo hello + image: alpine:latest + pull: true + + - name: echo_47 + commands: + - echo hello + image: alpine:latest + pull: true + + - name: echo_48 + commands: + - echo hello + image: alpine:latest + pull: true + + - name: echo_49 + commands: + - echo hello + image: alpine:latest + pull: true + + - name: echo_50 + commands: + - echo hello + image: alpine:latest + pull: true + + - name: echo_51 + commands: + - echo hello + image: alpine:latest + pull: true + + - name: echo_52 + commands: + - echo hello + image: alpine:latest + pull: true + + - name: echo_53 + commands: + - echo hello + image: alpine:latest + pull: true + + - name: echo_54 + commands: + - echo hello + image: alpine:latest + pull: true + + - name: echo_55 + commands: + - echo hello + image: alpine:latest + pull: true + + - name: echo_56 + commands: + - echo hello + image: alpine:latest + pull: true + + - name: echo_57 + commands: + - echo hello + image: alpine:latest + pull: true + + - name: echo_58 + commands: + - echo hello + image: alpine:latest + pull: true + + - name: echo_59 + commands: + - echo hello + image: alpine:latest + pull: true + + - name: echo_60 + commands: + - echo hello + image: alpine:latest + pull: true + + - name: echo_61 + commands: + - echo hello + image: alpine:latest + pull: true + + - name: echo_62 + commands: + - echo hello + image: alpine:latest + pull: true + + - name: echo_63 + commands: + - echo hello + image: alpine:latest + pull: true + + - name: echo_64 + commands: + - echo hello + image: alpine:latest + pull: true + + - name: echo_65 + commands: + - echo hello + image: alpine:latest + pull: true + + - name: echo_66 + commands: + - echo hello + image: alpine:latest + pull: true + + - name: echo_67 + commands: + - echo hello + image: alpine:latest + pull: true + + - name: echo_68 + commands: + - echo hello + image: alpine:latest + pull: true + + - name: echo_69 + commands: + - echo hello + image: alpine:latest + pull: true + + - name: echo_70 + commands: + - echo hello + image: alpine:latest + pull: true + + - name: echo_71 + commands: + - echo hello + image: alpine:latest + pull: true + + - name: echo_72 + commands: + - echo hello + image: alpine:latest + pull: true + + - name: echo_73 + commands: + - echo hello + image: alpine:latest + pull: true + + - name: echo_74 + commands: + - echo hello + image: alpine:latest + pull: true + + - name: echo_75 + commands: + - echo hello + image: alpine:latest + pull: true + + - name: echo_76 + commands: + - echo hello + image: alpine:latest + pull: true + + - name: echo_77 + commands: + - echo hello + image: alpine:latest + pull: true + + - name: echo_78 + commands: + - echo hello + image: alpine:latest + pull: true + + - name: echo_79 + commands: + - echo hello + image: alpine:latest + pull: true + + - name: echo_80 + commands: + - echo hello + image: alpine:latest + pull: true + + - name: echo_81 + commands: + - echo hello + image: alpine:latest + pull: true + + - name: echo_82 + commands: + - echo hello + image: alpine:latest + pull: true + + - name: echo_83 + commands: + - echo hello + image: alpine:latest + pull: true + + - name: echo_84 + commands: + - echo hello + image: alpine:latest + pull: true + + - name: echo_85 + commands: + - echo hello + image: alpine:latest + pull: true + + - name: echo_86 + commands: + - echo hello + image: alpine:latest + pull: true + + - name: echo_87 + commands: + - echo hello + image: alpine:latest + pull: true + + - name: echo_88 + commands: + - echo hello + image: alpine:latest + pull: true + + - name: echo_89 + commands: + - echo hello + image: alpine:latest + pull: true + + - name: echo_90 + commands: + - echo hello + image: alpine:latest + pull: true + + - name: echo_91 + commands: + - echo hello + image: alpine:latest + pull: true + + - name: echo_92 + commands: + - echo hello + image: alpine:latest + pull: true + + - name: echo_93 + commands: + - echo hello + image: alpine:latest + pull: true + + - name: echo_94 + commands: + - echo hello + image: alpine:latest + pull: true + + - name: echo_95 + commands: + - echo hello + image: alpine:latest + pull: true + + - name: echo_96 + commands: + - echo hello + image: alpine:latest + pull: true + + - name: echo_97 + commands: + - echo hello + image: alpine:latest + pull: true + + - name: echo_98 + commands: + - echo hello + image: alpine:latest + pull: true + + - name: echo_99 + commands: + - echo hello + image: alpine:latest + pull: true + + - name: echo_100 + commands: + - echo hello + image: alpine:latest + pull: true diff --git a/runtime/testdata/stages.yml b/runtime/testdata/stages.yml new file mode 100644 index 00000000..75c143c2 --- /dev/null +++ b/runtime/testdata/stages.yml @@ -0,0 +1,13 @@ +--- +version: "1" + +stages: + test: + steps: + - name: test + commands: + - echo ${FOO} + environment: + FOO: bar + image: alpine:latest + pull: true diff --git a/runtime/testdata/steps.yml b/runtime/testdata/steps.yml new file mode 100644 index 00000000..1410a03c --- /dev/null +++ b/runtime/testdata/steps.yml @@ -0,0 +1,32 @@ +--- +version: "1" + +steps: + - name: git-test + image: target/vela-git:latest + pull: true + parameters: + path: hello-world + ref: refs/heads/master + remote: https://github.com/octocat/hello-world.git + sha: 7fd1a60b01f91b314f59955a4e4d4e80d8edf11d + + # sleep testing waiting step + - name: sleep + commands: | + secs=30 + while [ $secs -gt 0 ]; do + echo "$secs" + sleep 1 + : $((secs--)) + done + image: alpine:latest + pull: true + + # exit testing inspect step + - name: exit + commands: + - exit 1 + image: alpine:latest + pull: true +