From f92f8b17a30f4a1a1a9a37b125d6444bc0b2c134 Mon Sep 17 00:00:00 2001 From: qwerty287 <80460567+qwerty287@users.noreply.github.com> Date: Thu, 8 Feb 2024 16:33:22 +0100 Subject: [PATCH] Make agent usable for external backends (#3270) --- cli/exec/exec.go | 6 +- cmd/agent/{ => core}/agent.go | 63 +++++++------------ cmd/agent/{ => core}/agent_test.go | 2 +- cmd/agent/{ => core}/config.go | 2 +- cmd/agent/{ => core}/config_test.go | 2 +- cmd/agent/{ => core}/flags.go | 7 +-- cmd/agent/{ => core}/health.go | 2 +- cmd/agent/{ => core}/health_test.go | 2 +- cmd/agent/core/run.go | 53 ++++++++++++++++ cmd/agent/main.go | 33 +++------- .../docs/30-administration/15-agent-config.md | 6 -- .../75-addons/00-overview.md | 1 - .../75-addons/20-creating-addons.md | 15 +++-- .../10-custom-agent-backends.md | 23 +++++++ pipeline/backend/backend.go | 11 +--- pipeline/backend/docker/docker.go | 4 ++ pipeline/backend/kubernetes/kubernetes.go | 4 ++ pipeline/backend/local/local.go | 4 ++ pipeline/backend/types/backend.go | 5 ++ shared/addon/types/types.go | 1 - 20 files changed, 142 insertions(+), 104 deletions(-) rename cmd/agent/{ => core}/agent.go (84%) rename cmd/agent/{ => core}/agent_test.go (99%) rename cmd/agent/{ => core}/config.go (99%) rename cmd/agent/{ => core}/config_test.go (99%) rename cmd/agent/{ => core}/flags.go (95%) rename cmd/agent/{ => core}/health.go (99%) rename cmd/agent/{ => core}/health_test.go (99%) create mode 100644 cmd/agent/core/run.go create mode 100644 docs/docs/92-development/10-custom-agent-backends.md diff --git a/cli/exec/exec.go b/cli/exec/exec.go index 7a2859316..9fc374a39 100644 --- a/cli/exec/exec.go +++ b/cli/exec/exec.go @@ -213,7 +213,11 @@ func execWithAxis(c *cli.Context, file, repoPath string, axis matrix.Axis) error } backendCtx := context.WithValue(c.Context, backendTypes.CliContext, c) - backend.Init() + backend.Init([]backendTypes.Backend{ + docker.New(), + local.New(), + kubernetes.New(), + }) backendEngine, err := backend.FindBackend(backendCtx, c.String("backend-engine")) if err != nil { diff --git a/cmd/agent/agent.go b/cmd/agent/core/agent.go similarity index 84% rename from cmd/agent/agent.go rename to cmd/agent/core/agent.go index 4ee9ca007..98fa21516 100644 --- a/cmd/agent/agent.go +++ b/cmd/agent/core/agent.go @@ -13,7 +13,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package main +package core import ( "context" @@ -42,14 +42,12 @@ import ( "go.woodpecker-ci.org/woodpecker/v2/pipeline/backend" "go.woodpecker-ci.org/woodpecker/v2/pipeline/backend/types" "go.woodpecker-ci.org/woodpecker/v2/pipeline/rpc" - "go.woodpecker-ci.org/woodpecker/v2/shared/addon" - addonTypes "go.woodpecker-ci.org/woodpecker/v2/shared/addon/types" "go.woodpecker-ci.org/woodpecker/v2/shared/logger" "go.woodpecker-ci.org/woodpecker/v2/shared/utils" "go.woodpecker-ci.org/woodpecker/v2/version" ) -func run(c *cli.Context) error { +func run(c *cli.Context, backendEngines []types.Backend) error { agentConfigPath := c.String("agent-config") hostname := c.String("hostname") if len(hostname) == 0 { @@ -155,11 +153,13 @@ func run(c *cli.Context) error { // new engine backendCtx := context.WithValue(ctx, types.CliContext, c) - backendEngine, err := getBackendEngine(backendCtx, c.String("backend-engine"), c.StringSlice("addons")) + backend.Init(backendEngines) + backendName := c.String("backend-engine") + backendEngine, err := backend.FindBackend(backendCtx, backendName) if err != nil { + log.Error().Err(err).Msgf("cannot find backend engine '%s'", backendName) return err } - if !backendEngine.IsAvailable(backendCtx) { log.Error().Str("engine", backendEngine.Name()).Msg("selected backend engine is unavailable") return fmt.Errorf("selected backend engine %s is unavailable", backendEngine.Name()) @@ -249,44 +249,27 @@ func run(c *cli.Context) error { return nil } -func getBackendEngine(backendCtx context.Context, backendName string, addons []string) (types.Backend, error) { - addonBackend, err := addon.Load[types.Backend](addons, addonTypes.TypeBackend) - if err != nil { - log.Error().Err(err).Msg("cannot load backend addon") - return nil, err - } - if addonBackend != nil { - return addonBackend.Value, nil - } +func runWithRetry(backendEngines []types.Backend) func(context *cli.Context) error { + return func(context *cli.Context) error { + if err := logger.SetupGlobalLogger(context, true); err != nil { + return err + } - backend.Init() - engine, err := backend.FindBackend(backendCtx, backendName) - if err != nil { - log.Error().Err(err).Msgf("cannot find backend engine '%s'", backendName) - return nil, err - } - return engine, nil -} + initHealth() -func runWithRetry(context *cli.Context) error { - if err := logger.SetupGlobalLogger(context, true); err != nil { + retryCount := context.Int("connect-retry-count") + retryDelay := context.Duration("connect-retry-delay") + var err error + for i := 0; i < retryCount; i++ { + if err = run(context, backendEngines); status.Code(err) == codes.Unavailable { + log.Warn().Err(err).Msg(fmt.Sprintf("cannot connect to server, retrying in %v", retryDelay)) + time.Sleep(retryDelay) + } else { + break + } + } return err } - - initHealth() - - retryCount := context.Int("connect-retry-count") - retryDelay := context.Duration("connect-retry-delay") - var err error - for i := 0; i < retryCount; i++ { - if err = run(context); status.Code(err) == codes.Unavailable { - log.Warn().Err(err).Msg(fmt.Sprintf("cannot connect to server, retrying in %v", retryDelay)) - time.Sleep(retryDelay) - } else { - break - } - } - return err } func stringSliceAddToMap(sl []string, m map[string]string) error { diff --git a/cmd/agent/agent_test.go b/cmd/agent/core/agent_test.go similarity index 99% rename from cmd/agent/agent_test.go rename to cmd/agent/core/agent_test.go index 1e645e68d..8c3c92db4 100644 --- a/cmd/agent/agent_test.go +++ b/cmd/agent/core/agent_test.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package main +package core import ( "testing" diff --git a/cmd/agent/config.go b/cmd/agent/core/config.go similarity index 99% rename from cmd/agent/config.go rename to cmd/agent/core/config.go index 61e700e1b..f2bde3051 100644 --- a/cmd/agent/config.go +++ b/cmd/agent/core/config.go @@ -13,7 +13,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package main +package core import ( "bytes" diff --git a/cmd/agent/config_test.go b/cmd/agent/core/config_test.go similarity index 99% rename from cmd/agent/config_test.go rename to cmd/agent/core/config_test.go index fa21ca6b1..c3c94fd0b 100644 --- a/cmd/agent/config_test.go +++ b/cmd/agent/core/config_test.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package main +package core import ( "os" diff --git a/cmd/agent/flags.go b/cmd/agent/core/flags.go similarity index 95% rename from cmd/agent/flags.go rename to cmd/agent/core/flags.go index 0020d3ab0..5e92f74be 100644 --- a/cmd/agent/flags.go +++ b/cmd/agent/core/flags.go @@ -13,7 +13,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package main +package core import ( "os" @@ -97,9 +97,4 @@ var flags = []cli.Flag{ Usage: "backend to run pipelines on", Value: "auto-detect", }, - &cli.StringSliceFlag{ - EnvVars: []string{"WOODPECKER_ADDONS"}, - Name: "addons", - Usage: "list of addon files", - }, } diff --git a/cmd/agent/health.go b/cmd/agent/core/health.go similarity index 99% rename from cmd/agent/health.go rename to cmd/agent/core/health.go index 8c11589b6..80d5135ea 100644 --- a/cmd/agent/health.go +++ b/cmd/agent/core/health.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package main +package core import ( "encoding/json" diff --git a/cmd/agent/health_test.go b/cmd/agent/core/health_test.go similarity index 99% rename from cmd/agent/health_test.go rename to cmd/agent/core/health_test.go index 463928f76..326d37536 100644 --- a/cmd/agent/health_test.go +++ b/cmd/agent/core/health_test.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package main +package core import ( "testing" diff --git a/cmd/agent/core/run.go b/cmd/agent/core/run.go new file mode 100644 index 000000000..b485f5019 --- /dev/null +++ b/cmd/agent/core/run.go @@ -0,0 +1,53 @@ +// Copyright 2024 Woodpecker Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package core + +import ( + "os" + + // Load config from .env + _ "github.com/joho/godotenv/autoload" + "github.com/rs/zerolog/log" + "github.com/urfave/cli/v2" + + backend "go.woodpecker-ci.org/woodpecker/v2/pipeline/backend/types" + "go.woodpecker-ci.org/woodpecker/v2/shared/logger" + "go.woodpecker-ci.org/woodpecker/v2/shared/utils" + "go.woodpecker-ci.org/woodpecker/v2/version" +) + +func RunAgent(backends []backend.Backend) { + app := cli.NewApp() + app.Name = "woodpecker-agent" + app.Version = version.String() + app.Usage = "woodpecker agent" + app.Action = runWithRetry(backends) + app.Commands = []*cli.Command{ + { + Name: "ping", + Usage: "ping the agent", + Action: pinger, + }, + } + agentFlags := utils.MergeSlices(flags, logger.GlobalLoggerFlags) + for _, b := range backends { + agentFlags = utils.MergeSlices(agentFlags, b.Flags()) + } + app.Flags = agentFlags + + if err := app.Run(os.Args); err != nil { + log.Fatal().Err(err).Msg("error running agent") //nolint:forbidigo + } +} diff --git a/cmd/agent/main.go b/cmd/agent/main.go index 01bb60246..d83ba1400 100644 --- a/cmd/agent/main.go +++ b/cmd/agent/main.go @@ -15,36 +15,17 @@ package main import ( - "os" - - _ "github.com/joho/godotenv/autoload" - "github.com/rs/zerolog/log" - "github.com/urfave/cli/v2" - + "go.woodpecker-ci.org/woodpecker/v2/cmd/agent/core" "go.woodpecker-ci.org/woodpecker/v2/pipeline/backend/docker" "go.woodpecker-ci.org/woodpecker/v2/pipeline/backend/kubernetes" "go.woodpecker-ci.org/woodpecker/v2/pipeline/backend/local" - "go.woodpecker-ci.org/woodpecker/v2/shared/logger" - "go.woodpecker-ci.org/woodpecker/v2/shared/utils" - "go.woodpecker-ci.org/woodpecker/v2/version" + backendTypes "go.woodpecker-ci.org/woodpecker/v2/pipeline/backend/types" ) func main() { - app := cli.NewApp() - app.Name = "woodpecker-agent" - app.Version = version.String() - app.Usage = "woodpecker agent" - app.Action = runWithRetry - app.Commands = []*cli.Command{ - { - Name: "ping", - Usage: "ping the agent", - Action: pinger, - }, - } - app.Flags = utils.MergeSlices(flags, logger.GlobalLoggerFlags, docker.Flags, kubernetes.Flags, local.Flags) - - if err := app.Run(os.Args); err != nil { - log.Fatal().Err(err).Msg("error running agent") //nolint:forbidigo - } + core.RunAgent([]backendTypes.Backend{ + docker.New(), + local.New(), + kubernetes.New(), + }) } diff --git a/docs/docs/30-administration/15-agent-config.md b/docs/docs/30-administration/15-agent-config.md index 26005eaf5..31a37ab1c 100644 --- a/docs/docs/30-administration/15-agent-config.md +++ b/docs/docs/30-administration/15-agent-config.md @@ -168,12 +168,6 @@ Configures if the gRPC server certificate should be verified, only valid when `W Configures the backend engine to run pipelines on. Possible values are `auto-detect`, `docker`, `local` or `kubernetes`. -### `WOODPECKER_ADDONS` - -> Default: empty - -List of addon files. See [addons](./75-addons/00-overview.md). - ### `WOODPECKER_BACKEND_DOCKER_*` See [Docker backend configuration](./22-backends/10-docker.md#configuration) diff --git a/docs/docs/30-administration/75-addons/00-overview.md b/docs/docs/30-administration/75-addons/00-overview.md index 485f9ce84..31c93e782 100644 --- a/docs/docs/30-administration/75-addons/00-overview.md +++ b/docs/docs/30-administration/75-addons/00-overview.md @@ -13,7 +13,6 @@ To adapt Woodpecker to your needs beyond the [configuration](../10-server-config Addons can be used for: - Forges -- Agent backends - Config services - Secret services - Environment services diff --git a/docs/docs/30-administration/75-addons/20-creating-addons.md b/docs/docs/30-administration/75-addons/20-creating-addons.md index 0fd6d2fe0..8ad4fe3e9 100644 --- a/docs/docs/30-administration/75-addons/20-creating-addons.md +++ b/docs/docs/30-administration/75-addons/20-creating-addons.md @@ -19,14 +19,13 @@ Directly import Woodpecker's Go package (`go.woodpecker-ci.org/woodpecker/woodpe ### Return types -| Addon type | Return type | -| -------------------- | -------------------------------------------------------------------------------- | -| `Forge` | `"go.woodpecker-ci.org/woodpecker/woodpecker/v2/server/forge".Forge` | -| `Backend` | `"go.woodpecker-ci.org/woodpecker/woodpecker/v2/pipeline/backend/types".Backend` | -| `ConfigService` | `"go.woodpecker-ci.org/woodpecker/v2/server/plugins/config".Extension` | -| `SecretService` | `"go.woodpecker-ci.org/woodpecker/v2/server/model".SecretService` | -| `EnvironmentService` | `"go.woodpecker-ci.org/woodpecker/v2/server/model".EnvironmentService` | -| `RegistryService` | `"go.woodpecker-ci.org/woodpecker/v2/server/model".RegistryService` | +| Addon type | Return type | +| -------------------- | ---------------------------------------------------------------------- | +| `Forge` | `"go.woodpecker-ci.org/woodpecker/woodpecker/v2/server/forge".Forge` | +| `ConfigService` | `"go.woodpecker-ci.org/woodpecker/v2/server/plugins/config".Extension` | +| `SecretService` | `"go.woodpecker-ci.org/woodpecker/v2/server/model".SecretService` | +| `EnvironmentService` | `"go.woodpecker-ci.org/woodpecker/v2/server/model".EnvironmentService` | +| `RegistryService` | `"go.woodpecker-ci.org/woodpecker/v2/server/model".RegistryService` | ### Using configurations diff --git a/docs/docs/92-development/10-custom-agent-backends.md b/docs/docs/92-development/10-custom-agent-backends.md new file mode 100644 index 000000000..3514e73d9 --- /dev/null +++ b/docs/docs/92-development/10-custom-agent-backends.md @@ -0,0 +1,23 @@ +# Custom agent backends + +If none of our backends fits your usecases, you can write your own. + +Therefore, implement the interface `"go.woodpecker-ci.org/woodpecker/woodpecker/v2/pipeline/backend/types".Backend` and +build a custom agent using your backend with this `main.go`: + +```go +package main + +import ( + "go.woodpecker-ci.org/woodpecker/v2/cmd/agent/core" + backendTypes "go.woodpecker-ci.org/woodpecker/v2/pipeline/backend/types" +) + +func main() { + core.RunAgent([]backendTypes.Backend{ + yourBackend, + }) +} +``` + +It is also possible to use multiple backends, you can select with [`WOODPECKER_BACKEND`](../30-administration/15-agent-config.md#woodpecker_backend) between them. diff --git a/pipeline/backend/backend.go b/pipeline/backend/backend.go index 99ecb6913..16688e9a3 100644 --- a/pipeline/backend/backend.go +++ b/pipeline/backend/backend.go @@ -18,9 +18,6 @@ import ( "context" "fmt" - "go.woodpecker-ci.org/woodpecker/v2/pipeline/backend/docker" - "go.woodpecker-ci.org/woodpecker/v2/pipeline/backend/kubernetes" - "go.woodpecker-ci.org/woodpecker/v2/pipeline/backend/local" "go.woodpecker-ci.org/woodpecker/v2/pipeline/backend/types" ) @@ -29,13 +26,7 @@ var ( backends []types.Backend ) -func Init() { - backends = []types.Backend{ - docker.New(), - local.New(), - kubernetes.New(), - } - +func Init(backends []types.Backend) { backendsByName = make(map[string]types.Backend) for _, engine := range backends { backendsByName[engine.Name()] = engine diff --git a/pipeline/backend/docker/docker.go b/pipeline/backend/docker/docker.go index 240691062..8a92deea5 100644 --- a/pipeline/backend/docker/docker.go +++ b/pipeline/backend/docker/docker.go @@ -93,6 +93,10 @@ func httpClientOfOpts(dockerCertPath string, verifyTLS bool) *http.Client { } } +func (e *docker) Flags() []cli.Flag { + return Flags +} + // Load new client for Docker Backend using environment variables. func (e *docker) Load(ctx context.Context) (*backend.BackendInfo, error) { c, ok := ctx.Value(backend.CliContext).(*cli.Context) diff --git a/pipeline/backend/kubernetes/kubernetes.go b/pipeline/backend/kubernetes/kubernetes.go index cf24c84b3..805c2643e 100644 --- a/pipeline/backend/kubernetes/kubernetes.go +++ b/pipeline/backend/kubernetes/kubernetes.go @@ -129,6 +129,10 @@ func (e *kube) IsAvailable(context.Context) bool { return len(host) > 0 } +func (e *kube) Flags() []cli.Flag { + return Flags +} + func (e *kube) Load(ctx context.Context) (*types.BackendInfo, error) { config, err := configFromCliContext(ctx) if err != nil { diff --git a/pipeline/backend/local/local.go b/pipeline/backend/local/local.go index aee231c65..cd96b470c 100644 --- a/pipeline/backend/local/local.go +++ b/pipeline/backend/local/local.go @@ -66,6 +66,10 @@ func (e *local) IsAvailable(context.Context) bool { return true } +func (e *local) Flags() []cli.Flag { + return Flags +} + func (e *local) Load(ctx context.Context) (*types.BackendInfo, error) { c, ok := ctx.Value(types.CliContext).(*cli.Context) if ok { diff --git a/pipeline/backend/types/backend.go b/pipeline/backend/types/backend.go index d6cfd0990..5bb765f5a 100644 --- a/pipeline/backend/types/backend.go +++ b/pipeline/backend/types/backend.go @@ -17,6 +17,8 @@ package types import ( "context" "io" + + "github.com/urfave/cli/v2" ) // Backend defines a container orchestration backend and is used @@ -28,6 +30,9 @@ type Backend interface { // IsAvailable check if the backend is available. IsAvailable(ctx context.Context) bool + // Flags return the configuration flags of the backend. + Flags() []cli.Flag + // Load loads the backend engine. Load(ctx context.Context) (*BackendInfo, error) diff --git a/shared/addon/types/types.go b/shared/addon/types/types.go index 158df423b..3287efc57 100644 --- a/shared/addon/types/types.go +++ b/shared/addon/types/types.go @@ -4,7 +4,6 @@ type Type string const ( TypeForge Type = "forge" - TypeBackend Type = "backend" TypeConfigService Type = "config_service" TypeSecretService Type = "secret_service" TypeEnvironmentService Type = "environment_service"