diff --git a/cmd/vela-worker/exec.go b/cmd/vela-worker/exec.go index 62aa1198..76ec0ac5 100644 --- a/cmd/vela-worker/exec.go +++ b/cmd/vela-worker/exec.go @@ -52,6 +52,7 @@ func (w *Worker) exec(index int) error { // https://pkg.go.dev/github.com/go-vela/worker/runtime?tab=doc#New w.Runtime, err = runtime.New(&runtime.Setup{ Logger: logger, + Mock: w.Config.Runtime.Mock, Driver: w.Config.Runtime.Driver, ConfigFile: w.Config.Runtime.ConfigFile, HostVolumes: w.Config.Runtime.HostVolumes, diff --git a/cmd/vela-worker/exec_test.go b/cmd/vela-worker/exec_test.go new file mode 100644 index 00000000..658191ad --- /dev/null +++ b/cmd/vela-worker/exec_test.go @@ -0,0 +1,483 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package main + +import ( + "context" + "encoding/json" + "fmt" + "net/http/httptest" + "net/url" + "testing" + "time" + + "github.com/go-vela/sdk-go/vela" + "github.com/go-vela/server/mock/server" + "github.com/go-vela/server/queue/redis" + "github.com/go-vela/types" + "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/mock/worker" + "github.com/go-vela/worker/runtime" +) + +func TestWorker_exec(t *testing.T) { + // setup types for tests + _repo := testRepo() + _user := testUser() + + type testStruct struct { + name string + config *workerTestConfig + pipeline *pipeline.Build + wantErr bool + } + + // this gets expanded into the product of runtime-executor-baseTest + baseTests := []testStruct{ + { + name: "steps", + pipeline: _steps, + config: &workerTestConfig{}, + }, + { + name: "stages", + pipeline: _stages, + config: &workerTestConfig{}, + }, + } + executors := []workerTestConfig{ + { + name: constants.DriverLinux + "_log_byte_chunks", + executorDriver: constants.DriverLinux, + executorLogMethod: "byte-chunks", + }, + { + name: constants.DriverLocal + "_log_byte_chunks", + executorDriver: constants.DriverLocal, + executorLogMethod: "byte-chunks", + }, + { + name: constants.DriverLinux + "_log_time_chunks", + executorDriver: constants.DriverLinux, + executorLogMethod: "time-chunks", + }, + { + name: constants.DriverLocal + "_log_time_chunks", + executorDriver: constants.DriverLocal, + executorLogMethod: "time-chunks", + }, + } + runtimes := []workerTestConfig{ + { + name: constants.DriverDocker, + runtimeDriver: constants.DriverDocker, + }, + // TODO: kubernetes tests are hanging. Fix in a follow-up. + //{ + // name: constants.DriverKubernetes, + // runtimeDriver: constants.DriverKubernetes, + // runtimeNamespace: "test", + // runtimeConfigFile: "../../runtime/kubernetes/testdata/config", + //}, + } + + // if tests are needed beyond the matrix of tests, they can be added explicitly here. + var tests []testStruct + + for _, r := range runtimes { + for _, e := range executors { + for _, test := range baseTests { + tests = append(tests, testStruct{ + // matrix name format: runtime-executor-subtest + name: fmt.Sprintf("%s-%s-%s", r.name, e.name, test.name), + pipeline: test.pipeline, + config: (&workerTestConfig{ + // executor + executorDriver: e.executorDriver, + executorLogMethod: e.executorLogMethod, + executorMaxLogSize: e.executorMaxLogSize, + // runtime + runtimeDriver: r.runtimeDriver, + runtimeConfigFile: r.runtimeConfigFile, + runtimeNamespace: r.runtimeNamespace, + runtimePodsTemplateName: r.runtimePodsTemplateName, + runtimePodsTemplateFile: r.runtimePodsTemplateFile, + runtimePrivilegedImages: r.runtimePrivilegedImages, + runtimeHostVolumes: r.runtimeHostVolumes, + // etc + buildTimeout: test.config.buildTimeout, + logFormat: test.config.logFormat, + logLevel: test.config.logLevel, + route: test.config.route, + queueRoutes: test.config.queueRoutes, + queuePopTimeout: test.config.queuePopTimeout, + }).applyDefaults(), + }) + } + } + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + var ( + err error + w *Worker + byteItem []byte + ) + + // make the worker with mocked + w, err = mockWorker(test.config) + if err != nil { + t.Errorf("mockWorker error = %v", err) + } + + _build := testBuild(w.Config) + + byteItem, err = json.Marshal(types.Item{ + Build: _build, + Pipeline: test.pipeline, + Repo: _repo, + User: _user, + }) + if err != nil { + t.Errorf("queue Item marshall error = %v", err) + } + + // add Item to the queue + err = w.Queue.Push(context.Background(), test.config.route, byteItem) + if err != nil { + t.Errorf("queue push error = %v", err) + } + + // actually run our test + if err = w.exec(0); (err != nil) != test.wantErr { + t.Errorf("exec() error = %v, wantErr %v", err, test.wantErr) + } + }) + } +} + +type workerTestConfig struct { + name string + buildTimeout time.Duration + executorDriver string + executorLogMethod string + executorMaxLogSize uint + logFormat string + logLevel string + runtimeDriver string + runtimeConfigFile string // only for k8s + runtimeNamespace string // only for k8s + runtimePodsTemplateName string // only for k8s + runtimePodsTemplateFile string // only for k8s + runtimePrivilegedImages []string + runtimeHostVolumes []string + route string + queueRoutes []string + queuePopTimeout time.Duration +} + +// applyDefaults applies defaults for tests +// nolint:wsl // wsl prevents visual grouping of defaults +func (c *workerTestConfig) applyDefaults() *workerTestConfig { + // defaults from flags.go (might need to lower for tests) + if c.buildTimeout == 0 { + c.buildTimeout = 30 * time.Minute + } + if c.logFormat == "" { + c.logFormat = "json" + } + if c.logLevel == "" { + c.logLevel = "info" + } + + // defaults from executor.Flags + if c.executorLogMethod == "" { + c.executorLogMethod = "byte-chunks" + } + + // defaults from runtime.Flags + if c.runtimeDriver == "" { + c.runtimeDriver = constants.DriverDocker + } + if c.runtimePrivilegedImages == nil { + c.runtimePrivilegedImages = []string{"target/vela-docker"} + } + + // defaults from runtime.Flags + if c.queueRoutes == nil { + c.queueRoutes = []string{constants.DefaultRoute} + } + if c.queuePopTimeout == 0 { + c.queuePopTimeout = 60 * time.Second + } + + // convenient default (not based on anything) + if c.route == "" { + c.route = constants.DefaultRoute + } + + return c +} + +// mockWorker creates a Worker with mocks for the Vela server, +// the queue, and the runtime client(s). +func mockWorker(cfg *workerTestConfig) (*Worker, error) { + var err error + + // Worker initialized in run() + w := &Worker{ + // worker configuration (skipping fields unused by exec()) + Config: &Config{ + // api configuration + API: &API{}, + // build configuration + Build: &Build{ + Limit: 1, + Timeout: cfg.buildTimeout, + }, + // executor configuration + Executor: &executor.Setup{ + Driver: cfg.executorDriver, + LogMethod: cfg.executorLogMethod, + MaxLogSize: cfg.executorMaxLogSize, + }, + // logger configuration + Logger: &Logger{ + Format: cfg.logFormat, + Level: cfg.logLevel, + }, + // runtime configuration + Runtime: &runtime.Setup{ + Mock: true, + Driver: cfg.runtimeDriver, + ConfigFile: cfg.runtimeConfigFile, + Namespace: cfg.runtimeNamespace, + PodsTemplateName: cfg.runtimePodsTemplateName, + PodsTemplateFile: cfg.runtimePodsTemplateFile, + HostVolumes: cfg.runtimeHostVolumes, + PrivilegedImages: cfg.runtimePrivilegedImages, + }, + // server configuration + Server: &Server{ + // address is mocked below + Secret: "server.secret", + }, + }, + // exec() creates the runtime (including the mocked runtime). + // exec() creates the executor and adds it here. + Executors: make(map[int]executor.Engine), + } + + // setup mock vela server + s := httptest.NewServer(server.FakeHandler()) + w.Config.Server.Address = s.URL + + // setup mock vela worker + api := httptest.NewServer(worker.FakeHandler()) + + w.Config.API.Address, err = url.Parse(api.URL) + if err != nil { + return nil, fmt.Errorf("unable to parse mock address: %w", err) + } + + // set up VelaClient (setup happens in operate()) + w.VelaClient, err = setupClient(w.Config.Server) + if err != nil { + return nil, fmt.Errorf("vela client setup error = %w", err) + } + + // set up mock Redis client (setup happens in operate()) + w.Queue, err = redis.NewTest(cfg.queueRoutes...) + if err != nil { + return nil, fmt.Errorf("queue setup error = %w", err) + } + + return w, nil +} + +var ( + _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", + }, + }, + } +) + +// testBuild is a test helper function to create a Build +// type with all fields set to a fake value. +func testBuild(cfg *Config) *library.Build { + return &library.Build{ + ID: vela.Int64(1), + Number: vela.Int(1), + Parent: vela.Int(1), + Event: vela.String("push"), + Status: vela.String("success"), + Error: vela.String(""), + Enqueued: vela.Int64(1563474077), + Created: vela.Int64(1563474076), + Started: vela.Int64(1563474077), + Finished: vela.Int64(0), + Deploy: vela.String(""), + Clone: vela.String("https://github.com/github/octocat.git"), + Source: vela.String("https://github.com/github/octocat/abcdefghi123456789"), + Title: vela.String("push received from https://github.com/github/octocat"), + Message: vela.String("First commit..."), + Commit: vela.String("48afb5bdc41ad69bf22588491333f7cf71135163"), + Sender: vela.String("OctoKitty"), + Author: vela.String("OctoKitty"), + Branch: vela.String("master"), + Ref: vela.String("refs/heads/master"), + BaseRef: vela.String(""), + Host: vela.String(cfg.API.Address.Host), + Runtime: vela.String(cfg.Runtime.Driver), + Distribution: vela.String(cfg.Executor.Driver), + } +} + +// testRepo is a test helper function to create a Repo +// type with all fields set to a fake value. +func testRepo() *library.Repo { + return &library.Repo{ + ID: vela.Int64(1), + Org: vela.String("github"), + Name: vela.String("octocat"), + FullName: vela.String("github/octocat"), + Link: vela.String("https://github.com/github/octocat"), + Clone: vela.String("https://github.com/github/octocat.git"), + Branch: vela.String("master"), + Timeout: vela.Int64(60), + Visibility: vela.String("public"), + Private: vela.Bool(false), + Trusted: vela.Bool(false), + Active: vela.Bool(true), + AllowPull: vela.Bool(false), + AllowPush: vela.Bool(true), + AllowDeploy: vela.Bool(false), + AllowTag: vela.Bool(false), + } +} + +// testUser is a test helper function to create a User +// type with all fields set to a fake value. +func testUser() *library.User { + return &library.User{ + ID: vela.Int64(1), + Name: vela.String("octocat"), + Token: vela.String("superSecretToken"), + Hash: vela.String("MzM4N2MzMDAtNmY4Mi00OTA5LWFhZDAtNWIzMTlkNTJkODMy"), + Favorites: vela.Strings([]string{"github/octocat"}), + Active: vela.Bool(true), + Admin: vela.Bool(false), + } +} diff --git a/codecov.yml b/codecov.yml index e6503ede..8575c101 100644 --- a/codecov.yml +++ b/codecov.yml @@ -55,3 +55,17 @@ comment: layout: "reach, diff, flags, files" behavior: default require_changes: no + +# This section tells codecov to ignore certain paths +# that match this list of path globs. +# +# https://docs.codecov.com/docs/codecovyml-reference#ignore +ignore: + # ignore all but client.go and exec.go + - "cmd/vela-worker/flags.go" + - "cmd/vela-worker/main.go" + - "cmd/vela-worker/operate.go" + - "cmd/vela-worker/r*.go" + - "cmd/vela-worker/s*.go" + - "cmd/vela-worker/validate.go" + - "cmd/vela-worker/worker.go" diff --git a/runtime/setup.go b/runtime/setup.go index 3a88c75a..7207f39c 100644 --- a/runtime/setup.go +++ b/runtime/setup.go @@ -13,6 +13,7 @@ import ( "github.com/go-vela/types/constants" "github.com/sirupsen/logrus" + v1 "k8s.io/api/core/v1" ) // Setup represents the configuration necessary for @@ -24,6 +25,9 @@ type Setup struct { // Runtime Configuration + // Mock should only be true for tests. + Mock bool + // specifies the driver to use for the runtime client Driver string // specifies the path to a configuration file to use for the runtime client @@ -45,14 +49,23 @@ type Setup struct { 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( + opts := []docker.ClientOpt{ docker.WithHostVolumes(s.HostVolumes), docker.WithPrivilegedImages(s.PrivilegedImages), docker.WithLogger(s.Logger), - ) + } + + if s.Mock { + // create new mock Docker runtime engine + // + // https://pkg.go.dev/github.com/go-vela/worker/runtime/docker?tab=doc#NewMock + return docker.NewMock(opts...) + } + + // create new Docker runtime engine + // + // https://pkg.go.dev/github.com/go-vela/worker/runtime/docker?tab=doc#New + return docker.New(opts...) } // Kubernetes creates and returns a Vela engine capable of @@ -60,17 +73,26 @@ func (s *Setup) Docker() (Engine, error) { 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( + opts := []kubernetes.ClientOpt{ kubernetes.WithConfigFile(s.ConfigFile), kubernetes.WithHostVolumes(s.HostVolumes), kubernetes.WithNamespace(s.Namespace), kubernetes.WithPodsTemplate(s.PodsTemplateName, s.PodsTemplateFile), kubernetes.WithPrivilegedImages(s.PrivilegedImages), kubernetes.WithLogger(s.Logger), - ) + } + + if s.Mock { + // create new mock Kubernetes runtime engine + // + // https://pkg.go.dev/github.com/go-vela/worker/runtime/kubernetes?tab=doc#NewMock + return kubernetes.NewMock(&v1.Pod{}, opts...) + } + + // create new Kubernetes runtime engine + // + // https://pkg.go.dev/github.com/go-vela/worker/runtime/kubernetes?tab=doc#New + return kubernetes.New(opts...) } // Validate verifies the necessary fields for the diff --git a/runtime/setup_test.go b/runtime/setup_test.go index 8b87fd2b..2484fe2d 100644 --- a/runtime/setup_test.go +++ b/runtime/setup_test.go @@ -11,30 +11,56 @@ import ( ) func TestRuntime_Setup_Docker(t *testing.T) { - // setup types - _setup := &Setup{ - Driver: constants.DriverDocker, + tests := []struct { + name string + mock bool + }{ + {name: "standard", mock: false}, + {name: "mocked", mock: true}, } - // run test - _, err := _setup.Docker() - if err != nil { - t.Errorf("Docker returned err: %v", err) + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + // setup types + _setup := &Setup{ + Mock: test.mock, + Driver: constants.DriverDocker, + } + + _, 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", + tests := []struct { + name string + mock bool + }{ + {name: "standard", mock: false}, + {name: "mocked", mock: true}, } - // run test - _, err := _setup.Kubernetes() - if err != nil { - t.Errorf("Kubernetes returned err: %v", err) + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + // setup types + _setup := &Setup{ + Mock: test.mock, + Driver: constants.DriverKubernetes, + ConfigFile: "testdata/config", + Namespace: "docker", + } + + _, err := _setup.Kubernetes() + if err != nil { + t.Errorf("Kubernetes returned err: %v", err) + } + }) } }