Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support go plugins for forges and agent backends #2751

Merged
merged 51 commits into from
Dec 20, 2023
Merged
Show file tree
Hide file tree
Changes from 37 commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
851fdf8
Some basic work on addons
qwerty287 Nov 1, 2023
205852f
Support agent backends
qwerty287 Nov 1, 2023
9e99f60
improve/fix docs
qwerty287 Nov 3, 2023
0f37be8
Merge branch 'main' into go-addon
qwerty287 Nov 3, 2023
7b82fab
remove old gomod
qwerty287 Nov 5, 2023
b2af545
Merge branch 'main' into go-addon
qwerty287 Nov 5, 2023
cbcd301
[pre-commit.ci] auto fixes from pre-commit.com hooks [CI SKIP]
pre-commit-ci[bot] Nov 5, 2023
7c8a0fa
Merge branch 'main' into go-addon
qwerty287 Nov 7, 2023
b86f8d2
fix imports
qwerty287 Nov 7, 2023
c8527fe
fix imports
qwerty287 Nov 7, 2023
9728118
fix style
qwerty287 Nov 7, 2023
6111df8
add missing newline
qwerty287 Nov 7, 2023
0676ab4
Merge branch 'main' into go-addon
qwerty287 Nov 22, 2023
f5bbbbe
[pre-commit.ci] auto fixes from pre-commit.com hooks [CI SKIP]
pre-commit-ci[bot] Nov 22, 2023
91afda4
some move to hashicorp/go-plugin
qwerty287 Nov 22, 2023
11ac924
some more tests
qwerty287 Nov 23, 2023
b21a445
Revert "some more tests"
qwerty287 Dec 5, 2023
d980db5
Revert "some move to hashicorp/go-plugin"
qwerty287 Dec 5, 2023
af96af4
Merge branch 'main' into go-addon
qwerty287 Dec 5, 2023
cee88b2
fix docs typo
qwerty287 Dec 5, 2023
44e246c
Add warning to docs
qwerty287 Dec 5, 2023
d942783
some logging improvements
qwerty287 Dec 5, 2023
a2aad23
fix link
qwerty287 Dec 5, 2023
897f44c
format
qwerty287 Dec 5, 2023
59ee790
fix link
qwerty287 Dec 5, 2023
fad667b
Merge branch 'main' into go-addon
qwerty287 Dec 7, 2023
a437332
remove test addon
qwerty287 Dec 7, 2023
f2b8420
fix
qwerty287 Dec 7, 2023
7a2b835
fix type
qwerty287 Dec 7, 2023
44c8a50
fix lint
qwerty287 Dec 7, 2023
9f12975
Merge branch 'main' into go-addon
qwerty287 Dec 8, 2023
be45bf7
fix typo
qwerty287 Dec 8, 2023
aba0ab5
fix import
qwerty287 Dec 8, 2023
efa3834
Merge branch 'main' into go-addon
qwerty287 Dec 13, 2023
2a7a675
Update docs/docs/30-administration/75-addons/00-overview.md
qwerty287 Dec 14, 2023
92e9108
Merge branch 'main' into go-addon
qwerty287 Dec 14, 2023
548a85b
as per reviews
qwerty287 Dec 14, 2023
09b4034
Apply suggestions from code review
qwerty287 Dec 14, 2023
10fea7d
[pre-commit.ci] auto fixes from pre-commit.com hooks [CI SKIP]
pre-commit-ci[bot] Dec 14, 2023
94a89eb
Rename new occurences to backend
qwerty287 Dec 14, 2023
81084e4
add trust warning
qwerty287 Dec 14, 2023
4d10d0e
different wording
qwerty287 Dec 14, 2023
893b857
Merge branch 'main' into go-addon
qwerty287 Dec 16, 2023
f458289
Merge branch 'main' into go-addon
qwerty287 Dec 17, 2023
47c0fee
Apply suggestions from code review
6543 Dec 19, 2023
63299c0
Merge branch 'main' into go-addon
6543 Dec 19, 2023
ea53cb2
fix
6543 Dec 19, 2023
0ed6471
Merge branch 'main' into go-addon
qwerty287 Dec 20, 2023
861d9e8
rename to getBackendEngine
qwerty287 Dec 20, 2023
8b9c744
Update docs/docs/30-administration/75-addons/20-creating-addons.md
6543 Dec 20, 2023
2c4546d
fmt
6543 Dec 20, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 28 additions & 5 deletions cmd/agent/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ 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/utils"
"go.woodpecker-ci.org/woodpecker/v2/version"
)
Expand Down Expand Up @@ -149,20 +151,22 @@ func run(c *cli.Context) error {
return err
}

backendCtx := context.WithValue(ctx, types.CliContext, c)
backend.Init(backendCtx)

var wg sync.WaitGroup
parallel := c.Int("max-workflows")
wg.Add(parallel)

// new engine
engine, err := backend.FindEngine(backendCtx, c.String("backend-engine"))
backendCtx := context.WithValue(ctx, types.CliContext, c)
engine, err := getEngine(backendCtx, c.String("backend-engine"), c.StringSlice("addons"))
if err != nil {
log.Error().Err(err).Msgf("cannot find backend engine '%s'", c.String("backend-engine"))
return err
}

if !engine.IsAvailable(backendCtx) {
log.Error().Str("engine", engine.Name()).Msg("selected backend engine unavailable")
return fmt.Errorf("selected backend engine %s unavailable", engine.Name())
qwerty287 marked this conversation as resolved.
Show resolved Hide resolved
}

// load engine (e.g. init api client)
engInfo, err := engine.Load(backendCtx)
if err != nil {
Expand Down Expand Up @@ -247,6 +251,25 @@ func run(c *cli.Context) error {
return nil
}

func getEngine(backendCtx context.Context, engineName string, addons []string) (types.Engine, error) {
addonEngine, err := addon.Load[types.Engine](addons, addonTypes.TypeEngine)
qwerty287 marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
log.Error().Err(err).Msg("cannot load addon")
qwerty287 marked this conversation as resolved.
Show resolved Hide resolved
return nil, err
}
if addonEngine != nil {
return addonEngine.Value, nil
}

backend.Init(backendCtx)
engine, err := backend.FindEngine(backendCtx, engineName)
if err != nil {
log.Error().Err(err).Msgf("cannot find backend engine '%s'", engineName)
return nil, err
}
return engine, nil
}

func runWithRetry(context *cli.Context) error {
retryCount := context.Int("connect-retry-count")
retryDelay := context.Duration("connect-retry-delay")
Expand Down
4 changes: 4 additions & 0 deletions cmd/agent/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,4 +97,8 @@ var flags = []cli.Flag{
Usage: "backend engine to run pipelines on",
Value: "auto-detect",
},
&cli.StringSliceFlag{
EnvVars: []string{"WOODPECKER_ADDONS"},
Name: "addons",
6543 marked this conversation as resolved.
Show resolved Hide resolved
6543 marked this conversation as resolved.
Show resolved Hide resolved
},
}
5 changes: 5 additions & 0 deletions cmd/server/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,11 @@ var flags = append([]cli.Flag{
Name: "enable-swagger",
Value: true,
},
&cli.StringSliceFlag{
EnvVars: []string{"WOODPECKER_ADDONS"},
Name: "addons",
qwerty287 marked this conversation as resolved.
Show resolved Hide resolved
Usage: "list of addon files",
},
//
// backend options for pipeline compiler
//
Expand Down
12 changes: 11 additions & 1 deletion cmd/server/setup.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ import (
"go.woodpecker-ci.org/woodpecker/v2/server/store"
"go.woodpecker-ci.org/woodpecker/v2/server/store/datastore"
"go.woodpecker-ci.org/woodpecker/v2/server/store/types"
"go.woodpecker-ci.org/woodpecker/v2/shared/addon"
addonTypes "go.woodpecker-ci.org/woodpecker/v2/shared/addon/types"
)

func setupStore(c *cli.Context) (store.Store, error) {
Expand Down Expand Up @@ -130,8 +132,16 @@ func setupMembershipService(_ *cli.Context, r forge.Forge) cache.MembershipServi
return cache.NewMembershipService(r)
}

// setupForge helper function to setup the forge from the CLI arguments.
// setupForge helper function to set up the forge from the CLI arguments.
func setupForge(c *cli.Context) (forge.Forge, error) {
add, err := addon.Load[forge.Forge](c.StringSlice("addons"), addonTypes.TypeForge)
if err != nil {
return nil, err
}
if add != nil {
return add.Value, nil
}
qwerty287 marked this conversation as resolved.
Show resolved Hide resolved

switch {
case c.Bool("github"):
return setupGitHub(c)
Expand Down
6 changes: 6 additions & 0 deletions docs/docs/30-administration/10-server-config.md
Original file line number Diff line number Diff line change
Expand Up @@ -521,6 +521,12 @@ Supported variables:
- `owner`: the repo's owner
- `repo`: the repo's name

### WOODPECKER_ADDONS

> Default: empty

List of addon files. See [addons](./75-addons/00-overview.md).

---

### `WOODPECKER_LIMIT_MEM_SWAP`
Expand Down
6 changes: 6 additions & 0 deletions docs/docs/30-administration/15-agent-config.md
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,12 @@ 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)
Expand Down
44 changes: 44 additions & 0 deletions docs/docs/30-administration/75-addons/00-overview.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Addons

:::warning
Addons are still experimental. Their implementation can change and break at any time.
:::

To adapt Woodpecker to your needs beyond the [configuration](../10-server-config.md), Woodpecker has its own **addon** system, built ontop of [Go's internal plugin system](https://go.dev/pkg/plugin).
qwerty287 marked this conversation as resolved.
Show resolved Hide resolved

Addons can be used for:

- Forges
- Agent backends

## Restrictions

Addons are restricted by how Go plugins work. This includes the following restrictions:

- only supported on Linux, FreeBSD and macOS
- addons must have been built for the correct Woodpecker version. If an addon is not provided specifically for this version, you likely won't be able to use it.

## Usage

To use an addon, download the addon version built for your Woodpecker version. Then, you can add the following to your configuration:

```diff
# docker-compose.yml
version: '3'

services:
woodpecker-server:
[...]
environment:
+ - WOODPECKER_ADDONS=/path/to/your/addon/file.so
```

You may need to [mount the addon file as volume](https://docs.docker.com/storage/volumes/#create-and-manage-volumes) to access it from inside the Docker container.
qwerty287 marked this conversation as resolved.
Show resolved Hide resolved

You can list multiple addons, Woodpecker will automatically determine their type. If you specify multiple addons with the same type, only the first one will be used.
qwerty287 marked this conversation as resolved.
Show resolved Hide resolved

Using an addon always overwrites Woodpecker's internal setup. This means, that a forge addon will be used if specified, no matter what's configured for the forges natively supported by Woodpecker.
anbraten marked this conversation as resolved.
Show resolved Hide resolved

### Bug reports

If you experience bugs, please check which component has the issue. If it's the addon, **do not raise an issue in the main repository**, but rather use the separate addon repositories. To check which component is responsible for the bug, look at the logs. Logs from addons are marked with a special field `addon` containing their addon file name.
88 changes: 88 additions & 0 deletions docs/docs/30-administration/75-addons/20-creating-addons.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# Creating addons

Addons are written in Go.

## Writing your code

An addon consists of two variables/functions in Go.

1. The `Type` variable. Specifies the type of the addon and must be directly accessed from `shared/addons/types/types.go`.
2. The `Addon` function which is the main point of your addon.
This function takes two arguments:

1. The zerolog logger you should use to log errors, warnings etc.
2. A slice of strings with the environment variables used as configuration.

It returns two values:

1. The actual addon. For type reference see [table below](#return-types).
2. An error. If this error is not `nil`, Woodpecker exits.

Directly import Woodpecker's Go package (`go.woodpecker-ci.org/woodpecker/woodpecker`) and use the interfaces and types defined there.
qwerty287 marked this conversation as resolved.
Show resolved Hide resolved

### Return types

| Addon type | Return type |
| ---------- | ---------------------------------------------------------------------------- |
| `Forge` | `"go.woodpecker-ci.org/woodpecker/woodpecker/server/forge".Forge` |
| `Engine` | `"go.woodpecker-ci.org/woodpecker/woodpecker/pipeline/backend/types".Engine` |
qwerty287 marked this conversation as resolved.
Show resolved Hide resolved

## Compiling

After you wrote your addon code, compile your addon:
6543 marked this conversation as resolved.
Show resolved Hide resolved

```sh
go build -buildmode plugin
```

The output file is your addon which is now ready to use.
qwerty287 marked this conversation as resolved.
Show resolved Hide resolved

## Restrictions

Addons must directly depend on Woodpecker's core (`go.woodpecker-ci.org/woodpecker/woodpecker`).
qwerty287 marked this conversation as resolved.
Show resolved Hide resolved
The addon must have been built with **excatly the same code** as the Woodpecker instance you'd like to use it on. This means: If you build your addon with a specific commit from Woodpecker `next`, you can likely only use it with the Woodpecker version compiled from this commit.
Also, if you change something inside of Woodpecker without commiting, it might fail because you need to recompile your addon with this code.
qwerty287 marked this conversation as resolved.
Show resolved Hide resolved

In addition to this, addons are only supported on Linux, FreeBSD and macOS.

:::info
It is recommended to at least support the latest released version of Woodpecker.
:::

### Compile for different versions

As long as there were no changes to Woodpecker's interfaces or they are backwards-compatible, you can easily compile the addon for multiple version by changing the version of `go.woodpecker-ci.org/woodpecker/woodpecker` using `go get` before compiling.
qwerty287 marked this conversation as resolved.
Show resolved Hide resolved

## Logging

The entrypoint receives a `zerolog.Logger` as input. **Do not use any other logging solution.** This logger follows the configuration of the Woodpecker instance and adds a special field `addon` to the log entries which allows users to find out which component is writing the log messages.

## Example structure

```go
package main

import (
"context"
"net/http"

"github.com/rs/zerolog"
"go.woodpecker-ci.org/woodpecker/woodpecker/server/forge"
forge_types "go.woodpecker-ci.org/woodpecker/woodpecker/server/forge/types"
"go.woodpecker-ci.org/woodpecker/woodpecker/server/model"
addon_types "go.woodpecker-ci.org/woodpecker/woodpecker/shared/addon/types"
qwerty287 marked this conversation as resolved.
Show resolved Hide resolved
)

var Type = addon_types.TypeForge

func Addon(logger zerolog.Logger, env []string) (forge.Forge, error) {
logger.Info().Msg("hello world from addon")
return &config{l: logger}, nil
}

type config struct {
l zerolog.Logger
}

// ... in this case, `config` must implement `forge.Forge`. You must directly use Woodpecker's packages - see imports above.
```
6 changes: 6 additions & 0 deletions docs/docs/30-administration/75-addons/_category_.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
label: 'Addons'
collapsible: true
collapsed: true
link:
type: 'doc'
id: 'overview'
66 changes: 66 additions & 0 deletions shared/addon/addon.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package addon

import (
"errors"
"fmt"
"os"
"plugin"
"reflect"

"github.com/rs/zerolog"
"github.com/rs/zerolog/log"

"go.woodpecker-ci.org/woodpecker/v2/shared/addon/types"
)

var pluginCache = map[string]*plugin.Plugin{}

type Addon[T any] struct {
Type types.Type
Value T
}

func Load[T any](files []string, t types.Type) (*Addon[T], error) {
for _, file := range files {
if _, has := pluginCache[file]; !has {
p, err := plugin.Open(file)
if err != nil {
return nil, err
}
pluginCache[file] = p
}

typeLookup, err := pluginCache[file].Lookup("Type")
if err != nil {
return nil, err
}
if addonType, is := typeLookup.(*types.Type); !is {
return nil, errors.New("addon type has incorrect type")
qwerty287 marked this conversation as resolved.
Show resolved Hide resolved
} else if *addonType != t {
continue
}

mainLookup, err := pluginCache[file].Lookup("Addon")
if err != nil {
return nil, err
}
fmt.Println(reflect.TypeOf(mainLookup))
qwerty287 marked this conversation as resolved.
Show resolved Hide resolved
main, is := mainLookup.(func(zerolog.Logger, []string) (T, error))
if !is {
return nil, errors.New("addon main has incorrect type")
qwerty287 marked this conversation as resolved.
Show resolved Hide resolved
}

logger := log.Logger.With().Str("addon", file).Logger()

mainOut, err := main(logger, os.Environ())
if err != nil {
return nil, err
}
return &Addon[T]{
Type: t,
Value: mainOut,
}, nil
}

return nil, nil
}
8 changes: 8 additions & 0 deletions shared/addon/types/types.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package types

type Type string

const (
TypeForge Type = "forge"
TypeEngine Type = "engine"
)