Merge branch 'main' into github-refresh

This commit is contained in:
Anbraten 2024-10-27 17:08:14 +01:00 committed by GitHub
commit 6a4f2f17ad
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
146 changed files with 8322 additions and 5333 deletions

View file

@ -15,6 +15,7 @@
"apimachinery",
"Archlinux",
"autoincr",
"automerge",
"autoscaler",
"backporting",
"backports",

View file

@ -1,6 +1,7 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": ["github>woodpecker-ci/renovate-config"],
"automergeType": "pr",
"customManagers": [
{
"customType": "regex",

1
.lycheeignore Normal file
View file

@ -0,0 +1 @@
https://stackoverflow.com/*

View file

@ -5,16 +5,16 @@ repos:
- id: check-hooks-apply
- id: check-useless-excludes
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.6.0
rev: v5.0.0
hooks:
- id: end-of-file-fixer
- id: trailing-whitespace
- repo: https://github.com/golangci/golangci-lint
rev: v1.59.1
rev: v1.61.0
hooks:
- id: golangci-lint
- repo: https://github.com/igorshubovych/markdownlint-cli
rev: v0.41.0
rev: v0.42.0
hooks:
- id: markdownlint
exclude: '^(docs/versioned_docs/.*|CHANGELOG.md)$'
@ -24,7 +24,7 @@ repos:
- id: checkmake
exclude: '^docker/Dockerfile.make$' # actually a Dockerfile and not a makefile
- repo: https://github.com/hadolint/hadolint
rev: v2.13.0-beta
rev: v2.13.1-beta
hooks:
- id: hadolint
- repo: https://github.com/pre-commit/mirrors-prettier

View file

@ -6,7 +6,7 @@ when:
variables:
- &golang_image 'docker.io/golang:1.23'
- &node_image 'docker.io/node:22-alpine'
- &node_image 'docker.io/node:23-alpine'
- &xgo_image 'docker.io/techknowlogick/xgo:go-1.22.x'
# cspell:words bindata netgo
@ -97,7 +97,7 @@ steps:
release:
depends_on:
- checksums
image: woodpeckerci/plugin-release:0.1.0
image: woodpeckerci/plugin-release:0.2.1
settings:
api_key:
from_secret: github_token

View file

@ -1,8 +1,8 @@
variables:
- &golang_image 'docker.io/golang:1.23'
- &node_image 'docker.io/node:22-alpine'
- &node_image 'docker.io/node:23-alpine'
- &xgo_image 'docker.io/techknowlogick/xgo:go-1.22.x'
- &buildx_plugin 'docker.io/woodpeckerci/plugin-docker-buildx:4.2.0'
- &buildx_plugin 'docker.io/woodpeckerci/plugin-docker-buildx:5.0.0'
- &platforms_release 'linux/arm/v6,linux/arm/v7,linux/arm64/v8,linux/386,linux/amd64,linux/ppc64le,linux/riscv64,linux/s390x,freebsd/arm64,freebsd/amd64,openbsd/arm64,openbsd/amd64'
- &platforms_server 'linux/arm/v7,linux/arm64/v8,linux/amd64,linux/ppc64le,linux/riscv64'
- &platforms_preview 'linux/amd64'

View file

@ -1,7 +1,7 @@
variables:
- &golang_image 'docker.io/golang:1.22'
- &node_image 'docker.io/node:21-alpine'
- &alpine_image 'docker.io/alpine:3.19'
- &golang_image 'docker.io/golang:1.23'
- &node_image 'docker.io/node:23-alpine'
- &alpine_image 'docker.io/alpine:3.20'
- path: &when_path
- 'docs/**'
- '.woodpecker/docs.yaml'
@ -60,7 +60,7 @@ steps:
- event: manual
deploy-preview:
image: docker.io/woodpeckerci/plugin-surge-preview:1.3.0
image: docker.io/woodpeckerci/plugin-surge-preview:1.3.2
settings:
path: 'docs/build/'
surge_token:

View file

@ -1,6 +1,6 @@
steps:
release-helper:
image: woodpeckerci/plugin-ready-release-go:1.2.0
image: woodpeckerci/plugin-ready-release-go:2.0.0
pull: true
settings:
release_branch: ${CI_REPO_DEFAULT_BRANCH}

View file

@ -6,7 +6,7 @@ when:
- renovate/*
variables:
- &trivy_plugin docker.io/woodpeckerci/plugin-trivy:1.1.0
- &trivy_plugin docker.io/woodpeckerci/plugin-trivy:1.1.1
steps:
backend:

View file

@ -13,7 +13,7 @@ steps:
branch: renovate/*
- name: spellcheck
image: docker.io/node:22-alpine
image: docker.io/node:23-alpine
depends_on: []
commands:
- corepack enable

View file

@ -1,5 +1,5 @@
variables:
- &golang_image 'docker.io/golang:1.22'
- &golang_image 'docker.io/golang:1.23'
- &when
- path: &when_path # related config files
- '.woodpecker/test.yaml'
@ -40,7 +40,7 @@ steps:
- go run go.woodpecker-ci.org/woodpecker/v2/cmd/cli lint
environment:
WOODPECKER_DISABLE_UPDATE_CHECK: true
WOODPECKER_PLUGINS_PRIVILEGED: 'docker.io/woodpeckerci/plugin-docker-buildx:4.2.0'
WOODPECKER_PLUGINS_PRIVILEGED: 'docker.io/woodpeckerci/plugin-docker-buildx:5.0.0'
when:
- event: pull_request
path:
@ -129,7 +129,7 @@ steps:
- test
- sqlite
pull: true
image: docker.io/woodpeckerci/plugin-codecov:2.1.2
image: docker.io/woodpeckerci/plugin-codecov:2.1.5
settings:
files:
- agent-coverage.out
@ -145,7 +145,7 @@ steps:
services:
postgres:
image: docker.io/postgres:16
image: docker.io/postgres:17
ports: ['5432']
environment:
POSTGRES_USER: postgres
@ -153,7 +153,7 @@ services:
when: *when
mysql:
image: docker.io/mysql:8.2.0
image: docker.io/mysql:9.1.0
ports: ['3306']
environment:
MYSQL_DATABASE: test

View file

@ -6,7 +6,7 @@ when:
- renovate/*
variables:
- &node_image 'docker.io/node:22-alpine'
- &node_image 'docker.io/node:23-alpine'
- &when
path:
# related config files

View file

@ -480,12 +480,15 @@ func (c *client) sendLogs(ctx context.Context, entries []*proto.LogEntry) error
return nil
}
func (c *client) RegisterAgent(ctx context.Context, platform, backend, version string, capacity int) (int64, error) {
func (c *client) RegisterAgent(ctx context.Context, info rpc.AgentInfo) (int64, error) {
req := new(proto.RegisterAgentRequest)
req.Platform = platform
req.Backend = backend
req.Version = version
req.Capacity = int32(capacity)
req.Info = &proto.AgentInfo{
Platform: info.Platform,
Backend: info.Backend,
Version: info.Version,
Capacity: int32(info.Capacity),
CustomLabels: info.CustomLabels,
}
res, err := c.client.RegisterAgent(ctx, req)
return res.GetAgentId(), err

View file

@ -25,6 +25,7 @@ import (
"strings"
"github.com/drone/envsubst"
"github.com/oklog/ulid/v2"
"github.com/rs/zerolog/log"
"github.com/urfave/cli/v3"
@ -36,6 +37,7 @@ import (
"go.woodpecker-ci.org/woodpecker/v2/pipeline/backend/kubernetes"
"go.woodpecker-ci.org/woodpecker/v2/pipeline/backend/local"
backend_types "go.woodpecker-ci.org/woodpecker/v2/pipeline/backend/types"
"go.woodpecker-ci.org/woodpecker/v2/pipeline/frontend/metadata"
"go.woodpecker-ci.org/woodpecker/v2/pipeline/frontend/yaml"
"go.woodpecker-ci.org/woodpecker/v2/pipeline/frontend/yaml/compiler"
"go.woodpecker-ci.org/woodpecker/v2/pipeline/frontend/yaml/linter"
@ -75,6 +77,7 @@ func execDir(ctx context.Context, c *cli.Command, dir string) error {
if runtime.GOOS == "windows" {
repoPath = convertPathForWindows(repoPath)
}
// TODO: respect depends_on and do parallel runs with output to multiple _windows_ e.g. tmux like
return filepath.Walk(dir, func(path string, info os.FileInfo, e error) error {
if e != nil {
return e
@ -83,7 +86,7 @@ func execDir(ctx context.Context, c *cli.Command, dir string) error {
// check if it is a regular file (not dir)
if info.Mode().IsRegular() && (strings.HasSuffix(info.Name(), ".yaml") || strings.HasSuffix(info.Name(), ".yml")) {
fmt.Println("#", info.Name())
_ = runExec(ctx, c, path, repoPath) // TODO: should we drop errors or store them and report back?
_ = runExec(ctx, c, path, repoPath, false) // TODO: should we drop errors or store them and report back?
fmt.Println("")
return nil
}
@ -102,10 +105,10 @@ func execFile(ctx context.Context, c *cli.Command, file string) error {
if runtime.GOOS == "windows" {
repoPath = convertPathForWindows(repoPath)
}
return runExec(ctx, c, file, repoPath)
return runExec(ctx, c, file, repoPath, true)
}
func runExec(ctx context.Context, c *cli.Command, file, repoPath string) error {
func runExec(ctx context.Context, c *cli.Command, file, repoPath string, singleExec bool) error {
dat, err := os.ReadFile(file)
if err != nil {
return err
@ -120,7 +123,7 @@ func runExec(ctx context.Context, c *cli.Command, file, repoPath string) error {
axes = append(axes, matrix.Axis{})
}
for _, axis := range axes {
err := execWithAxis(ctx, c, file, repoPath, axis)
err := execWithAxis(ctx, c, file, repoPath, axis, singleExec)
if err != nil {
return err
}
@ -128,11 +131,20 @@ func runExec(ctx context.Context, c *cli.Command, file, repoPath string) error {
return nil
}
func execWithAxis(ctx context.Context, c *cli.Command, file, repoPath string, axis matrix.Axis) error {
metadata, err := metadataFromContext(ctx, c, axis)
func execWithAxis(ctx context.Context, c *cli.Command, file, repoPath string, axis matrix.Axis, singleExec bool) error {
metadataWorkflow := &metadata.Workflow{}
if !singleExec {
// TODO: proper try to use the engine to generate the same metadata for workflows
// https://github.com/woodpecker-ci/woodpecker/pull/3967
metadataWorkflow.Name = strings.TrimSuffix(strings.TrimSuffix(file, ".yaml"), ".yml")
}
metadata, err := metadataFromContext(ctx, c, axis, metadataWorkflow)
if err != nil {
return fmt.Errorf("could not create metadata: %w", err)
} else if metadata == nil {
return fmt.Errorf("metadata is nil")
}
environ := metadata.Environ()
var secrets []compiler.Secret
for key, val := range metadata.Workflow.Matrix {
@ -170,6 +182,9 @@ func execWithAxis(ctx context.Context, c *cli.Command, file, repoPath string, ax
return err
}
// emulate server behavior https://github.com/woodpecker-ci/woodpecker/blob/eebaa10d104cbc3fa7ce4c0e344b0b7978405135/server/pipeline/stepbuilder/stepBuilder.go#L289-L295
prefix := "wp_" + ulid.Make().String()
// configure volumes for local execution
volumes := c.StringSlice("volumes")
if c.Bool("local") {
@ -184,7 +199,7 @@ func execWithAxis(ctx context.Context, c *cli.Command, file, repoPath string, ax
workspacePath = c.String("workspace-path")
}
volumes = append(volumes, c.String("prefix")+"_default:"+workspaceBase)
volumes = append(volumes, prefix+"_default:"+workspaceBase)
volumes = append(volumes, repoPath+":"+path.Join(workspaceBase, workspacePath))
}
@ -221,9 +236,7 @@ func execWithAxis(ctx context.Context, c *cli.Command, file, repoPath string, ax
compiler.WithNetworks(
c.StringSlice("network")...,
),
compiler.WithPrefix(
c.String("prefix"),
),
compiler.WithPrefix(prefix),
compiler.WithProxy(compiler.ProxyOptions{
NoProxy: c.String("backend-no-proxy"),
HTTPProxy: c.String("backend-http-proxy"),
@ -237,7 +250,7 @@ func execWithAxis(ctx context.Context, c *cli.Command, file, repoPath string, ax
c.String("netrc-password"),
c.String("netrc-machine"),
),
compiler.WithMetadata(metadata),
compiler.WithMetadata(*metadata),
compiler.WithSecret(secrets...),
compiler.WithEnviron(pipelineEnv),
).Compile(conf)

View file

@ -32,6 +32,11 @@ var flags = []cli.Flag{
Name: "repo-path",
Usage: "path to local repository",
},
&cli.StringFlag{
Sources: cli.EnvVars("WOODPECKER_METADATA_FILE"),
Name: "metadata-file",
Usage: "path to pipeline metadata file (normally downloaded from UI). Parameters can be adjusted by applying additional cli flags",
},
&cli.DurationFlag{
Sources: cli.EnvVars("WOODPECKER_TIMEOUT"),
Name: "timeout",
@ -48,13 +53,6 @@ var flags = []cli.Flag{
Name: "network",
Usage: "external networks",
},
&cli.StringFlag{
Sources: cli.EnvVars("WOODPECKER_PREFIX"),
Name: "prefix",
Value: "woodpecker",
Usage: "prefix used for containers, volumes, networks, ... created by woodpecker",
Hidden: true,
},
&cli.StringSliceFlag{
Sources: cli.EnvVars("WOODPECKER_PLUGINS_PRIVILEGED"),
Name: "plugins-privileged",
@ -235,7 +233,7 @@ var flags = []cli.Flag{
&cli.StringFlag{
Sources: cli.EnvVars("CI_PIPELINE_FILES"),
Usage: "Set the metadata environment variable \"CI_PIPELINE_FILES\", either json formatted list of strings, or comma separated string list.",
Name: "pipeline-files",
Name: "pipeline-changed-files",
},
&cli.StringFlag{
Sources: cli.EnvVars("CI_COMMIT_SHA"),

View file

@ -18,6 +18,7 @@ import (
"context"
"encoding/json"
"fmt"
"os"
"runtime"
"strings"
@ -29,108 +30,131 @@ import (
)
// return the metadata from the cli context.
func metadataFromContext(_ context.Context, c *cli.Command, axis matrix.Axis) (metadata.Metadata, error) {
func metadataFromContext(_ context.Context, c *cli.Command, axis matrix.Axis, w *metadata.Workflow) (*metadata.Metadata, error) {
m := &metadata.Metadata{}
if c.IsSet("metadata-file") {
metadataFile, err := os.Open(c.String("metadata-file"))
if err != nil {
return nil, err
}
defer metadataFile.Close()
if err := json.NewDecoder(metadataFile).Decode(m); err != nil {
return nil, err
}
}
platform := c.String("system-platform")
if platform == "" {
platform = runtime.GOOS + "/" + runtime.GOARCH
}
fullRepoName := c.String("repo-name")
repoOwner := ""
repoName := ""
if idx := strings.LastIndex(fullRepoName, "/"); idx != -1 {
repoOwner = fullRepoName[:idx]
repoName = fullRepoName[idx+1:]
metadataFileAndOverrideOrDefault(c, "repo-name", func(fullRepoName string) {
if idx := strings.LastIndex(fullRepoName, "/"); idx != -1 {
m.Repo.Owner = fullRepoName[:idx]
m.Repo.Name = fullRepoName[idx+1:]
}
}, c.String)
var err error
metadataFileAndOverrideOrDefault(c, "pipeline-changed-files", func(changedFilesRaw string) {
var changedFiles []string
if len(changedFilesRaw) != 0 && changedFilesRaw[0] == '[' {
if jsonErr := json.Unmarshal([]byte(changedFilesRaw), &changedFiles); jsonErr != nil {
err = fmt.Errorf("pipeline-changed-files detected json but could not parse it: %w", jsonErr)
}
} else {
for _, file := range strings.Split(changedFilesRaw, ",") {
changedFiles = append(changedFiles, strings.TrimSpace(file))
}
}
m.Curr.Commit.ChangedFiles = changedFiles
}, c.String)
if err != nil {
return nil, err
}
var changedFiles []string
changedFilesRaw := c.String("pipeline-files")
if len(changedFilesRaw) != 0 && changedFilesRaw[0] == '[' {
if err := json.Unmarshal([]byte(changedFilesRaw), &changedFiles); err != nil {
return metadata.Metadata{}, fmt.Errorf("pipeline-files detected json but could not parse it: %w", err)
}
} else {
for _, file := range strings.Split(changedFilesRaw, ",") {
changedFiles = append(changedFiles, strings.TrimSpace(file))
}
// Repo
metadataFileAndOverrideOrDefault(c, "repo-remote-id", func(s string) { m.Repo.RemoteID = s }, c.String)
metadataFileAndOverrideOrDefault(c, "repo-url", func(s string) { m.Repo.ForgeURL = s }, c.String)
metadataFileAndOverrideOrDefault(c, "repo-scm", func(s string) { m.Repo.SCM = s }, c.String)
metadataFileAndOverrideOrDefault(c, "repo-default-branch", func(s string) { m.Repo.Branch = s }, c.String)
metadataFileAndOverrideOrDefault(c, "repo-clone-url", func(s string) { m.Repo.CloneURL = s }, c.String)
metadataFileAndOverrideOrDefault(c, "repo-clone-ssh-url", func(s string) { m.Repo.CloneSSHURL = s }, c.String)
metadataFileAndOverrideOrDefault(c, "repo-private", func(b bool) { m.Repo.Private = b }, c.Bool)
metadataFileAndOverrideOrDefault(c, "repo-trusted", func(b bool) { m.Repo.Trusted = b }, c.Bool)
// Current Pipeline
metadataFileAndOverrideOrDefault(c, "pipeline-number", func(i int64) { m.Curr.Number = i }, c.Int)
metadataFileAndOverrideOrDefault(c, "pipeline-parent", func(i int64) { m.Curr.Parent = i }, c.Int)
metadataFileAndOverrideOrDefault(c, "pipeline-created", func(i int64) { m.Curr.Created = i }, c.Int)
metadataFileAndOverrideOrDefault(c, "pipeline-started", func(i int64) { m.Curr.Started = i }, c.Int)
metadataFileAndOverrideOrDefault(c, "pipeline-finished", func(i int64) { m.Curr.Finished = i }, c.Int)
metadataFileAndOverrideOrDefault(c, "pipeline-status", func(s string) { m.Curr.Status = s }, c.String)
metadataFileAndOverrideOrDefault(c, "pipeline-event", func(s string) { m.Curr.Event = s }, c.String)
metadataFileAndOverrideOrDefault(c, "pipeline-url", func(s string) { m.Curr.ForgeURL = s }, c.String)
metadataFileAndOverrideOrDefault(c, "pipeline-deploy-to", func(s string) { m.Curr.DeployTo = s }, c.String)
metadataFileAndOverrideOrDefault(c, "pipeline-deploy-task", func(s string) { m.Curr.DeployTask = s }, c.String)
// Current Pipeline Commit
metadataFileAndOverrideOrDefault(c, "commit-sha", func(s string) { m.Curr.Commit.Sha = s }, c.String)
metadataFileAndOverrideOrDefault(c, "commit-ref", func(s string) { m.Curr.Commit.Ref = s }, c.String)
metadataFileAndOverrideOrDefault(c, "commit-refspec", func(s string) { m.Curr.Commit.Refspec = s }, c.String)
metadataFileAndOverrideOrDefault(c, "commit-branch", func(s string) { m.Curr.Commit.Branch = s }, c.String)
metadataFileAndOverrideOrDefault(c, "commit-message", func(s string) { m.Curr.Commit.Message = s }, c.String)
metadataFileAndOverrideOrDefault(c, "commit-author-name", func(s string) { m.Curr.Commit.Author.Name = s }, c.String)
metadataFileAndOverrideOrDefault(c, "commit-author-email", func(s string) { m.Curr.Commit.Author.Email = s }, c.String)
metadataFileAndOverrideOrDefault(c, "commit-author-avatar", func(s string) { m.Curr.Commit.Author.Avatar = s }, c.String)
metadataFileAndOverrideOrDefault(c, "commit-pull-labels", func(sl []string) { m.Curr.Commit.PullRequestLabels = sl }, c.StringSlice)
metadataFileAndOverrideOrDefault(c, "commit-release-is-pre", func(b bool) { m.Curr.Commit.IsPrerelease = b }, c.Bool)
// Previous Pipeline
metadataFileAndOverrideOrDefault(c, "prev-pipeline-number", func(i int64) { m.Prev.Number = i }, c.Int)
metadataFileAndOverrideOrDefault(c, "prev-pipeline-created", func(i int64) { m.Prev.Created = i }, c.Int)
metadataFileAndOverrideOrDefault(c, "prev-pipeline-started", func(i int64) { m.Prev.Started = i }, c.Int)
metadataFileAndOverrideOrDefault(c, "prev-pipeline-finished", func(i int64) { m.Prev.Finished = i }, c.Int)
metadataFileAndOverrideOrDefault(c, "prev-pipeline-status", func(s string) { m.Prev.Status = s }, c.String)
metadataFileAndOverrideOrDefault(c, "prev-pipeline-event", func(s string) { m.Prev.Event = s }, c.String)
metadataFileAndOverrideOrDefault(c, "prev-pipeline-url", func(s string) { m.Prev.ForgeURL = s }, c.String)
// Previous Pipeline Commit
metadataFileAndOverrideOrDefault(c, "prev-commit-sha", func(s string) { m.Prev.Commit.Sha = s }, c.String)
metadataFileAndOverrideOrDefault(c, "prev-commit-ref", func(s string) { m.Prev.Commit.Ref = s }, c.String)
metadataFileAndOverrideOrDefault(c, "prev-commit-refspec", func(s string) { m.Prev.Commit.Refspec = s }, c.String)
metadataFileAndOverrideOrDefault(c, "prev-commit-branch", func(s string) { m.Prev.Commit.Branch = s }, c.String)
metadataFileAndOverrideOrDefault(c, "prev-commit-message", func(s string) { m.Prev.Commit.Message = s }, c.String)
metadataFileAndOverrideOrDefault(c, "prev-commit-author-name", func(s string) { m.Prev.Commit.Author.Name = s }, c.String)
metadataFileAndOverrideOrDefault(c, "prev-commit-author-email", func(s string) { m.Prev.Commit.Author.Email = s }, c.String)
metadataFileAndOverrideOrDefault(c, "prev-commit-author-avatar", func(s string) { m.Prev.Commit.Author.Avatar = s }, c.String)
// Workflow
metadataFileAndOverrideOrDefault(c, "workflow-name", func(s string) { m.Workflow.Name = s }, c.String)
metadataFileAndOverrideOrDefault(c, "workflow-number", func(i int64) { m.Workflow.Number = int(i) }, c.Int)
m.Workflow.Matrix = axis
// System
metadataFileAndOverrideOrDefault(c, "system-name", func(s string) { m.Sys.Name = s }, c.String)
metadataFileAndOverrideOrDefault(c, "system-url", func(s string) { m.Sys.URL = s }, c.String)
metadataFileAndOverrideOrDefault(c, "system-host", func(s string) { m.Sys.Host = s }, c.String)
m.Sys.Platform = platform
m.Sys.Version = version.Version
// Forge
metadataFileAndOverrideOrDefault(c, "forge-type", func(s string) { m.Forge.Type = s }, c.String)
metadataFileAndOverrideOrDefault(c, "forge-url", func(s string) { m.Forge.URL = s }, c.String)
if w != nil {
m.Workflow = *w
}
return metadata.Metadata{
Repo: metadata.Repo{
Name: repoName,
Owner: repoOwner,
RemoteID: c.String("repo-remote-id"),
ForgeURL: c.String("repo-url"),
SCM: c.String("repo-scm"),
Branch: c.String("repo-default-branch"),
CloneURL: c.String("repo-clone-url"),
CloneSSHURL: c.String("repo-clone-ssh-url"),
Private: c.Bool("repo-private"),
Trusted: c.Bool("repo-trusted"),
},
Curr: metadata.Pipeline{
Number: c.Int("pipeline-number"),
Parent: c.Int("pipeline-parent"),
Created: c.Int("pipeline-created"),
Started: c.Int("pipeline-started"),
Finished: c.Int("pipeline-finished"),
Status: c.String("pipeline-status"),
Event: c.String("pipeline-event"),
ForgeURL: c.String("pipeline-url"),
DeployTo: c.String("pipeline-deploy-to"),
DeployTask: c.String("pipeline-deploy-task"),
Commit: metadata.Commit{
Sha: c.String("commit-sha"),
Ref: c.String("commit-ref"),
Refspec: c.String("commit-refspec"),
Branch: c.String("commit-branch"),
Message: c.String("commit-message"),
Author: metadata.Author{
Name: c.String("commit-author-name"),
Email: c.String("commit-author-email"),
Avatar: c.String("commit-author-avatar"),
},
PullRequestLabels: c.StringSlice("commit-pull-labels"),
IsPrerelease: c.Bool("commit-release-is-pre"),
ChangedFiles: changedFiles,
},
},
Prev: metadata.Pipeline{
Number: c.Int("prev-pipeline-number"),
Created: c.Int("prev-pipeline-created"),
Started: c.Int("prev-pipeline-started"),
Finished: c.Int("prev-pipeline-finished"),
Status: c.String("prev-pipeline-status"),
Event: c.String("prev-pipeline-event"),
ForgeURL: c.String("prev-pipeline-url"),
Commit: metadata.Commit{
Sha: c.String("prev-commit-sha"),
Ref: c.String("prev-commit-ref"),
Refspec: c.String("prev-commit-refspec"),
Branch: c.String("prev-commit-branch"),
Message: c.String("prev-commit-message"),
Author: metadata.Author{
Name: c.String("prev-commit-author-name"),
Email: c.String("prev-commit-author-email"),
Avatar: c.String("prev-commit-author-avatar"),
},
},
},
Workflow: metadata.Workflow{
Name: c.String("workflow-name"),
Number: int(c.Int("workflow-number")),
Matrix: axis,
},
Sys: metadata.System{
Name: c.String("system-name"),
URL: c.String("system-url"),
Host: c.String("system-host"),
Platform: platform,
Version: version.Version,
},
Forge: metadata.Forge{
Type: c.String("forge-type"),
URL: c.String("forge-url"),
},
}, nil
return m, nil
}
// metadataFileAndOverrideOrDefault will either use the flag default or if metadata file is set only overload if explicit set.
func metadataFileAndOverrideOrDefault[T any](c *cli.Command, flag string, setter func(T), getter func(string) T) {
if !c.IsSet("metadata-file") || c.IsSet(flag) {
setter(getter(flag))
}
}

142
cli/exec/metadata_test.go Normal file
View file

@ -0,0 +1,142 @@
// 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 exec
import (
"context"
"encoding/json"
"os"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/urfave/cli/v3"
"go.woodpecker-ci.org/woodpecker/v2/pipeline/frontend/metadata"
"go.woodpecker-ci.org/woodpecker/v2/pipeline/frontend/yaml/matrix"
)
func TestMetadataFromContext(t *testing.T) {
sampleMetadata := &metadata.Metadata{
Repo: metadata.Repo{Owner: "test-user", Name: "test-repo"},
Curr: metadata.Pipeline{Number: 5},
}
runCommand := func(flags []cli.Flag, fn func(c *cli.Command)) {
c := &cli.Command{
Flags: flags,
Action: func(_ context.Context, c *cli.Command) error {
fn(c)
return nil
},
}
assert.NoError(t, c.Run(context.Background(), []string{"woodpecker-cli"}))
}
t.Run("LoadFromFile", func(t *testing.T) {
tempFileName := createTempFile(t, sampleMetadata)
flags := []cli.Flag{
&cli.StringFlag{Name: "metadata-file"},
}
runCommand(flags, func(c *cli.Command) {
_ = c.Set("metadata-file", tempFileName)
m, err := metadataFromContext(context.Background(), c, nil, nil)
require.NoError(t, err)
assert.Equal(t, "test-repo", m.Repo.Name)
assert.Equal(t, int64(5), m.Curr.Number)
})
})
t.Run("OverrideFromFlags", func(t *testing.T) {
tempFileName := createTempFile(t, sampleMetadata)
flags := []cli.Flag{
&cli.StringFlag{Name: "metadata-file"},
&cli.StringFlag{Name: "repo-name"},
&cli.IntFlag{Name: "pipeline-number"},
}
runCommand(flags, func(c *cli.Command) {
_ = c.Set("metadata-file", tempFileName)
_ = c.Set("repo-name", "aUser/override-repo")
_ = c.Set("pipeline-number", "10")
m, err := metadataFromContext(context.Background(), c, nil, nil)
require.NoError(t, err)
assert.Equal(t, "override-repo", m.Repo.Name)
assert.Equal(t, int64(10), m.Curr.Number)
})
})
t.Run("InvalidFile", func(t *testing.T) {
tempFile, err := os.CreateTemp("", "invalid.json")
require.NoError(t, err)
t.Cleanup(func() { os.Remove(tempFile.Name()) })
_, err = tempFile.Write([]byte("invalid json"))
require.NoError(t, err)
flags := []cli.Flag{
&cli.StringFlag{Name: "metadata-file"},
}
runCommand(flags, func(c *cli.Command) {
_ = c.Set("metadata-file", tempFile.Name())
_, err = metadataFromContext(context.Background(), c, nil, nil)
assert.Error(t, err)
})
})
t.Run("DefaultValues", func(t *testing.T) {
flags := []cli.Flag{
&cli.StringFlag{Name: "repo-name", Value: "test/default-repo"},
&cli.IntFlag{Name: "pipeline-number", Value: 1},
}
runCommand(flags, func(c *cli.Command) {
m, err := metadataFromContext(context.Background(), c, nil, nil)
require.NoError(t, err)
if assert.NotNil(t, m) {
assert.Equal(t, "test", m.Repo.Owner)
assert.Equal(t, "default-repo", m.Repo.Name)
assert.Equal(t, int64(1), m.Curr.Number)
}
})
})
t.Run("MatrixAxis", func(t *testing.T) {
runCommand([]cli.Flag{}, func(c *cli.Command) {
axis := matrix.Axis{"go": "1.16", "os": "linux"}
m, err := metadataFromContext(context.Background(), c, axis, nil)
require.NoError(t, err)
assert.EqualValues(t, map[string]string{"go": "1.16", "os": "linux"}, m.Workflow.Matrix)
})
})
}
func createTempFile(t *testing.T, content any) string {
t.Helper()
tempFile, err := os.CreateTemp("", "metadata.json")
require.NoError(t, err)
t.Cleanup(func() { os.Remove(tempFile.Name()) })
err = json.NewEncoder(tempFile).Encode(content)
require.NoError(t, err)
return tempFile.Name()
}

View file

@ -20,6 +20,7 @@ import (
"crypto/tls"
"errors"
"fmt"
"maps"
"net/http"
"os"
"strings"
@ -198,7 +199,22 @@ func run(ctx context.Context, c *cli.Command, backends []types.Backend) error {
log.Debug().Msgf("loaded %s backend engine", backendEngine.Name())
maxWorkflows := int(c.Int("max-workflows"))
agentConfig.AgentID, err = client.RegisterAgent(grpcCtx, engInfo.Platform, backendEngine.Name(), version.String(), maxWorkflows) //nolint:contextcheck
customLabels := make(map[string]string)
if err := stringSliceAddToMap(c.StringSlice("labels"), customLabels); err != nil {
return err
}
if len(customLabels) != 0 {
log.Debug().Msgf("custom labels detected: %#v", customLabels)
}
agentConfig.AgentID, err = client.RegisterAgent(grpcCtx, rpc.AgentInfo{ //nolint:contextcheck
Version: version.String(),
Backend: backendEngine.Name(),
Platform: engInfo.Platform,
Capacity: maxWorkflows,
CustomLabels: customLabels,
})
if err != nil {
return err
}
@ -210,7 +226,7 @@ func run(ctx context.Context, c *cli.Command, backends []types.Backend) error {
<-agentCtx.Done()
// Remove stateless agents from server
if !agentConfigPersisted.Load() {
log.Debug().Msg("unregistering agent from server ...")
log.Debug().Msg("unregister agent from server ...")
// we want to run it explicit run when context got canceled so run it in background
err := client.UnregisterAgent(grpcClientCtx)
if err != nil {
@ -228,16 +244,17 @@ func run(ctx context.Context, c *cli.Command, backends []types.Backend) error {
}
}
// set default labels ...
labels := map[string]string{
"hostname": hostname,
"platform": engInfo.Platform,
"backend": backendEngine.Name(),
"repo": "*", // allow all repos by default
}
// ... and let it overwrite by custom ones
maps.Copy(labels, customLabels)
if err := stringSliceAddToMap(c.StringSlice("filter"), labels); err != nil {
return err
}
log.Debug().Any("labels", labels).Msgf("agent configured with labels")
filter := rpc.Filter{
Labels: labels,

View file

@ -60,8 +60,9 @@ var flags = []cli.Flag{
Value: "/etc/woodpecker/agent.conf",
},
&cli.StringSliceFlag{
Sources: cli.EnvVars("WOODPECKER_FILTER_LABELS"),
Name: "filter",
Sources: cli.EnvVars("WOODPECKER_AGENT_LABELS", "WOODPECKER_FILTER_LABELS"), // remove WOODPECKER_FILTER_LABELS in v4.x
Name: "labels",
Aliases: []string{"filter"}, // remove in v4.x
Usage: "List of labels to filter tasks on. An agent must be assigned every tag listed in a task to be selected.",
},
&cli.IntFlag{

View file

@ -101,7 +101,7 @@ const docTemplate = `{
}
}
},
"/agents/{agent}": {
"/agents/{agent_id}": {
"get": {
"produces": [
"application/json"
@ -211,7 +211,7 @@ const docTemplate = `{
}
}
},
"/agents/{agent}/tasks": {
"/agents/{agent_id}/tasks": {
"get": {
"produces": [
"application/json"
@ -1063,6 +1063,193 @@ const docTemplate = `{
}
}
},
"/orgs/{org_id}/agents": {
"get": {
"produces": [
"application/json"
],
"tags": [
"Agents"
],
"summary": "List agents for an organization",
"parameters": [
{
"type": "string",
"default": "Bearer \u003cpersonal access token\u003e",
"description": "Insert your personal access token",
"name": "Authorization",
"in": "header",
"required": true
},
{
"type": "integer",
"description": "the organization's id",
"name": "org_id",
"in": "path",
"required": true
},
{
"type": "integer",
"default": 1,
"description": "for response pagination, page offset number",
"name": "page",
"in": "query"
},
{
"type": "integer",
"default": 50,
"description": "for response pagination, max items per page",
"name": "perPage",
"in": "query"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/Agent"
}
}
}
}
},
"post": {
"description": "Creates a new agent with a random token, scoped to the specified organization",
"produces": [
"application/json"
],
"tags": [
"Agents"
],
"summary": "Create a new organization-scoped agent",
"parameters": [
{
"type": "string",
"default": "Bearer \u003cpersonal access token\u003e",
"description": "Insert your personal access token",
"name": "Authorization",
"in": "header",
"required": true
},
{
"type": "integer",
"description": "the organization's id",
"name": "org_id",
"in": "path",
"required": true
},
{
"description": "the agent's data (only 'name' and 'no_schedule' are read)",
"name": "agent",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/Agent"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/Agent"
}
}
}
}
},
"/orgs/{org_id}/agents/{agent_id}": {
"delete": {
"produces": [
"text/plain"
],
"tags": [
"Agents"
],
"summary": "Delete an organization-scoped agent",
"parameters": [
{
"type": "string",
"default": "Bearer \u003cpersonal access token\u003e",
"description": "Insert your personal access token",
"name": "Authorization",
"in": "header",
"required": true
},
{
"type": "integer",
"description": "the organization's id",
"name": "org_id",
"in": "path",
"required": true
},
{
"type": "integer",
"description": "the agent's id",
"name": "agent_id",
"in": "path",
"required": true
}
],
"responses": {
"204": {
"description": "No Content"
}
}
},
"patch": {
"produces": [
"application/json"
],
"tags": [
"Agents"
],
"summary": "Update an organization-scoped agent",
"parameters": [
{
"type": "string",
"default": "Bearer \u003cpersonal access token\u003e",
"description": "Insert your personal access token",
"name": "Authorization",
"in": "header",
"required": true
},
{
"type": "integer",
"description": "the organization's id",
"name": "org_id",
"in": "path",
"required": true
},
{
"type": "integer",
"description": "the agent's id",
"name": "agent_id",
"in": "path",
"required": true
},
{
"description": "the agent's updated data",
"name": "agent",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/Agent"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/Agent"
}
}
}
}
},
"/orgs/{org_id}/permissions": {
"get": {
"produces": [
@ -3144,6 +3331,49 @@ const docTemplate = `{
}
}
},
"/repos/{repo_id}/pipelines/{number}/metadata": {
"get": {
"produces": [
"application/json"
],
"tags": [
"Pipelines"
],
"summary": "Get metadata for a pipeline or a specific workflow, including previous pipeline info",
"parameters": [
{
"type": "string",
"default": "Bearer \u003cpersonal access token\u003e",
"description": "Insert your personal access token",
"name": "Authorization",
"in": "header",
"required": true
},
{
"type": "integer",
"description": "the repository id",
"name": "repo_id",
"in": "path",
"required": true
},
{
"type": "integer",
"description": "the number of the pipeline",
"name": "number",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/metadata.Metadata"
}
}
}
}
},
"/repos/{repo_id}/pull_requests": {
"get": {
"produces": [
@ -4359,6 +4589,12 @@ const docTemplate = `{
"created": {
"type": "integer"
},
"custom_labels": {
"type": "object",
"additionalProperties": {
"type": "string"
}
},
"id": {
"type": "integer"
},
@ -4375,6 +4611,10 @@ const docTemplate = `{
"no_schedule": {
"type": "boolean"
},
"org_id": {
"description": "OrgID is counted as unset if set to -1, this is done to ensure a new(Agent) still enforce the OrgID check by default",
"type": "integer"
},
"owner_id": {
"type": "integer"
},
@ -5148,6 +5388,225 @@ const docTemplate = `{
"EventManual"
]
},
"metadata.Author": {
"type": "object",
"properties": {
"avatar": {
"type": "string"
},
"email": {
"type": "string"
},
"name": {
"type": "string"
}
}
},
"metadata.Commit": {
"type": "object",
"properties": {
"author": {
"$ref": "#/definitions/metadata.Author"
},
"branch": {
"type": "string"
},
"changed_files": {
"type": "array",
"items": {
"type": "string"
}
},
"is_prerelease": {
"type": "boolean"
},
"labels": {
"type": "array",
"items": {
"type": "string"
}
},
"message": {
"type": "string"
},
"ref": {
"type": "string"
},
"refspec": {
"type": "string"
},
"sha": {
"type": "string"
}
}
},
"metadata.Forge": {
"type": "object",
"properties": {
"type": {
"type": "string"
},
"url": {
"type": "string"
}
}
},
"metadata.Metadata": {
"type": "object",
"properties": {
"curr": {
"$ref": "#/definitions/metadata.Pipeline"
},
"forge": {
"$ref": "#/definitions/metadata.Forge"
},
"id": {
"type": "string"
},
"prev": {
"$ref": "#/definitions/metadata.Pipeline"
},
"repo": {
"$ref": "#/definitions/metadata.Repo"
},
"step": {
"$ref": "#/definitions/metadata.Step"
},
"sys": {
"$ref": "#/definitions/metadata.System"
},
"workflow": {
"$ref": "#/definitions/metadata.Workflow"
}
}
},
"metadata.Pipeline": {
"type": "object",
"properties": {
"commit": {
"$ref": "#/definitions/metadata.Commit"
},
"created": {
"type": "integer"
},
"cron": {
"type": "string"
},
"event": {
"type": "string"
},
"finished": {
"type": "integer"
},
"forge_url": {
"type": "string"
},
"number": {
"type": "integer"
},
"parent": {
"type": "integer"
},
"started": {
"type": "integer"
},
"status": {
"type": "string"
},
"target": {
"type": "string"
},
"task": {
"type": "string"
}
}
},
"metadata.Repo": {
"type": "object",
"properties": {
"clone_url": {
"type": "string"
},
"clone_url_ssh": {
"type": "string"
},
"default_branch": {
"type": "string"
},
"forge_url": {
"type": "string"
},
"id": {
"type": "integer"
},
"name": {
"type": "string"
},
"owner": {
"type": "string"
},
"private": {
"type": "boolean"
},
"remote_id": {
"type": "string"
},
"scm": {
"type": "string"
},
"trusted": {
"type": "boolean"
}
}
},
"metadata.Step": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"number": {
"type": "integer"
}
}
},
"metadata.System": {
"type": "object",
"properties": {
"arch": {
"type": "string"
},
"host": {
"type": "string"
},
"name": {
"type": "string"
},
"url": {
"type": "string"
},
"version": {
"type": "string"
}
}
},
"metadata.Workflow": {
"type": "object",
"properties": {
"matrix": {
"type": "object",
"additionalProperties": {
"type": "string"
}
},
"name": {
"type": "string"
},
"number": {
"type": "integer"
}
}
},
"model.ForgeType": {
"type": "string",
"enum": [

View file

@ -287,7 +287,7 @@ var flags = append([]cli.Flag{
Sources: cli.EnvVars("WOODPECKER_FORGE_TIMEOUT"),
Name: "forge-timeout",
Usage: "how many seconds before timeout when fetching the Woodpecker configuration from a Forge",
Value: time.Second * 3,
Value: time.Second * 5,
},
&cli.UintFlag{
Sources: cli.EnvVars("WOODPECKER_FORGE_RETRY"),
@ -295,36 +295,8 @@ var flags = append([]cli.Flag{
Usage: "How many retries of fetching the Woodpecker configuration from a forge are done before we fail",
Value: 3,
},
&cli.IntFlag{
Sources: cli.EnvVars("WOODPECKER_LIMIT_MEM_SWAP"),
Name: "limit-mem-swap",
Usage: "maximum memory used for swap in bytes",
},
&cli.IntFlag{
Sources: cli.EnvVars("WOODPECKER_LIMIT_MEM"),
Name: "limit-mem",
Usage: "maximum memory allowed in bytes",
},
&cli.IntFlag{
Sources: cli.EnvVars("WOODPECKER_LIMIT_SHM_SIZE"),
Name: "limit-shm-size",
Usage: "docker compose /dev/shm allowed in bytes",
},
&cli.IntFlag{
Sources: cli.EnvVars("WOODPECKER_LIMIT_CPU_QUOTA"),
Name: "limit-cpu-quota",
Usage: "impose a cpu quota",
},
&cli.IntFlag{
Sources: cli.EnvVars("WOODPECKER_LIMIT_CPU_SHARES"),
Name: "limit-cpu-shares",
Usage: "change the cpu shares",
},
&cli.StringFlag{
Sources: cli.EnvVars("WOODPECKER_LIMIT_CPU_SET"),
Name: "limit-cpu-set",
Usage: "set the cpus allowed to execute containers",
},
//
// generic forge settings
//
&cli.StringFlag{
Name: "forge-url",

View file

@ -178,14 +178,6 @@ func setupEvilGlobals(ctx context.Context, c *cli.Command, s store.Store) error
server.Config.Pipeline.DefaultTimeout = c.Int("default-pipeline-timeout")
server.Config.Pipeline.MaxTimeout = c.Int("max-pipeline-timeout")
// limits
server.Config.Pipeline.Limits.MemSwapLimit = c.Int("limit-mem-swap")
server.Config.Pipeline.Limits.MemLimit = c.Int("limit-mem")
server.Config.Pipeline.Limits.ShmSize = c.Int("limit-shm-size")
server.Config.Pipeline.Limits.CPUQuota = c.Int("limit-cpu-quota")
server.Config.Pipeline.Limits.CPUShares = c.Int("limit-cpu-shares")
server.Config.Pipeline.Limits.CPUSet = c.String("limit-cpu-set")
// backend options for pipeline compiler
server.Config.Pipeline.Proxy.No = c.String("backend-no-proxy")
server.Config.Pipeline.Proxy.HTTP = c.String("backend-http-proxy")

View file

@ -3,7 +3,7 @@ version: '3'
services:
gitea-database:
image: postgres:16.3-alpine
image: postgres:17.0-alpine
environment:
POSTGRES_USER: gitea
POSTGRES_PASSWORD: 123456

View file

@ -1,6 +1,6 @@
# docker build --rm -f docker/Dockerfile.make -t woodpecker/make:local .
FROM docker.io/golang:1.23-alpine3.19 as golang_image
FROM docker.io/node:22-alpine3.19
FROM docker.io/node:23-alpine3.19
# renovate: datasource=repology depName=alpine_3_19/make versioning=loose
ENV MAKE_VERSION="4.4.1-r2"

View file

@ -179,12 +179,6 @@ Woodpecker provides the ability to pass environment variables to individual step
For more details, check the [environment docs](./50-environment.md).
### `secrets`
Woodpecker provides the ability to store named parameters external to the YAML configuration file, in a central secret store. These secrets can be passed to individual steps of the workflow at runtime.
For more details, check the [secrets docs](./40-secrets.md).
### `failure`
Some of the steps may be allowed to fail without causing the whole workflow and therefore pipeline to report a failure (e.g., a step executing a linting check). To enable this, add `failure: ignore` to your step. If Woodpecker encounters an error while executing the step, it will report it as failed but still executes the next steps of the workflow, if any, without affecting the status of the workflow.
@ -610,7 +604,7 @@ For more details check the [matrix build docs](./30-matrix-workflows.md).
You can set labels for your workflow to select an agent to execute the workflow on. An agent will pick up and run a workflow when **every** label assigned to it matches the agents labels.
To set additional agent labels, check the [agent configuration options](../30-administration/15-agent-config.md#woodpecker_filter_labels). Agents will have at least four default labels: `platform=agent-os/agent-arch`, `hostname=my-agent`, `backend=docker` (type of the agent backend) and `repo=*`. Agents can use a `*` as a wildcard for a label. For example `repo=*` will match every repo.
To set additional agent labels, check the [agent configuration options](../30-administration/15-agent-config.md#woodpecker_agent_labels). Agents will have at least four default labels: `platform=agent-os/agent-arch`, `hostname=my-agent`, `backend=docker` (type of the agent backend) and `repo=*`. Agents can use a `*` as a wildcard for a label. For example `repo=*` will match every repo.
Workflow labels with an empty value will be ignored.
By default, each workflow has at least the `repo=your-user/your-repo-name` label. If you have set the [platform attribute](#platform) for your workflow it will have a label like `platform=your-os/your-arch` as well.

View file

@ -11,26 +11,7 @@ Woodpecker provides three different levels to add secrets to your pipeline. The
## Usage
### Use secrets in commands
Secrets are exposed to your pipeline steps and plugins as uppercase environment variables and can therefore be referenced in the commands section of your pipeline,
once their usage is declared in the `secrets` section:
```diff
steps:
- name: docker
image: docker
commands:
+ - echo $docker_username
+ - echo $DOCKER_PASSWORD
+ secrets: [ docker_username, DOCKER_PASSWORD ]
```
The case of the environment variables is not changed, but secret matching is done case-insensitively. In the example above, `DOCKER_PASSWORD` would also match if the secret is called `docker_password`.
### Use secrets in settings and environment
You can set an setting or environment value from secrets using the `from_secret` syntax.
You can set a setting or an environment value from secrets using the `from_secret` syntax.
In this example, the secret named `secret_token` would be passed to the setting named `token`,which will be available in the plugin as environment variable named `PLUGIN_TOKEN` (See [plugins](./51-plugins/20-creating-plugins.md#settings) for details), and to the environment variable `TOKEN_ENV`.
@ -55,11 +36,11 @@ Please note parameter expressions are subject to pre-processing. When using secr
- name: docker
image: docker
commands:
- - echo ${docker_username}
- - echo ${DOCKER_PASSWORD}
+ - echo $${docker_username}
+ - echo $${DOCKER_PASSWORD}
secrets: [ docker_username, DOCKER_PASSWORD ]
- - echo ${TOKEN_ENV}
+ - echo $${TOKEN_ENV}
environment:
TOKEN_ENV:
from_secret: secret_token
```
### Use in Pull Requests events

View file

@ -35,10 +35,6 @@ Example registry hostname matching logic:
- Hostname `docker.io` matches `bradrydzewski/golang`
- Hostname `docker.io` matches `bradrydzewski/golang:latest`
:::note
The flow above doesn't work in Kubernetes. There is [workaround](../30-administration/22-backends/40-kubernetes.md#images-from-private-registries).
:::
## Global registry support
To make a private registry globally available, check the [server configuration docs](../30-administration/10-server-config.md#global-registry-setting).

View file

@ -42,7 +42,7 @@ Values like this are converted to JSON and then passed to your plugin. In the ex
### Secrets
Secrets should be passed as settings too. Therefore, users should use [`from_secret`](../40-secrets.md#use-secrets-in-settings-and-environment).
Secrets should be passed as settings too. Therefore, users should use [`from_secret`](../40-secrets.md#usage).
## Plugin library

View file

@ -52,7 +52,7 @@ While normal steps are all about arbitrary code execution, plugins should only a
That's why there are a few limitations. The workspace base is always mounted at `/woodpecker`, but the working directory is dynamically
adjusted accordingly, as user of a plugin you should not have to care about this. Also, you cannot use the plugin together with `commands`
or `entrypoint` which will fail. Using `secrets` or `environment` is possible, but in this case, the plugin is internally not treated as plugin
or `entrypoint` which will fail. Using `environment` is possible, but in this case, the plugin is internally not treated as plugin
anymore. The container then cannot access secrets with plugin filter anymore and the containers won't be privileged without explicit definition.
## Finding Plugins

View file

@ -49,7 +49,7 @@ Woodpecker needs to know its own address. You must therefore provide the public
+ - WOODPECKER_HOST=${WOODPECKER_HOST}
```
Woodpecker can also have its port's configured. It uses a separate port for gRPC and for HTTP. The agent performs gRPC calls and connects to the gRPC port.
Woodpecker can also have its ports configured. It uses a separate port for gRPC and for HTTP. The agent performs gRPC calls and connects to the gRPC port.
They can be configured with `*_ADDR` variables:
```diff title="docker-compose.yaml"

View file

@ -476,44 +476,6 @@ Supported variables:
---
### `WOODPECKER_LIMIT_MEM_SWAP`
> Default: `0`
The maximum amount of memory a single pipeline container is allowed to swap to disk, configured in bytes. There is no limit if `0`.
### `WOODPECKER_LIMIT_MEM`
> Default: `0`
The maximum amount of memory a single pipeline container can use, configured in bytes. There is no limit if `0`.
### `WOODPECKER_LIMIT_SHM_SIZE`
> Default: `0`
The maximum amount of memory of `/dev/shm` allowed in bytes. There is no limit if `0`.
### `WOODPECKER_LIMIT_CPU_QUOTA`
> Default: `0`
The number of microseconds per CPU period that the container is limited to before throttled. There is no limit if `0`.
### `WOODPECKER_LIMIT_CPU_SHARES`
> Default: `0`
The relative weight vs. other containers.
### `WOODPECKER_LIMIT_CPU_SET`
> Default: empty
Comma-separated list to limit the specific CPUs or cores a pipeline container can use.
Example: `WOODPECKER_LIMIT_CPU_SET=1,2`
### `WOODPECKER_CONFIG_SERVICE_ENDPOINT`
> Default: empty
@ -522,7 +484,7 @@ Specify a configuration service endpoint, see [Configuration Extension](./40-adv
### `WOODPECKER_FORGE_TIMEOUT`
> Default: 3s
> Default: 5s
Specify timeout when fetching the Woodpecker configuration from forge. See <https://pkg.go.dev/time#ParseDuration> for syntax reference.

View file

@ -120,11 +120,14 @@ Configures the path of the agent config file.
Configures the number of parallel workflows.
### `WOODPECKER_FILTER_LABELS`
### `WOODPECKER_AGENT_LABELS`
> Default: empty
Configures labels to filter pipeline pick up. Use a list of key-value pairs like `key=value,second-key=*`. `*` can be used as a wildcard. By default, agents provide three additional labels `platform=os/arch`, `hostname=my-agent` and `repo=*` which can be overwritten if needed. To learn how labels work, check out the [pipeline syntax page](../20-usage/20-workflow-syntax.md#labels).
Configures custom labels for the agent, to let workflows filter by it.
Use a list of key-value pairs like `key=value,second-key=*`. `*` can be used as a wildcard.
By default, agents provide three additional labels `platform=os/arch`, `hostname=my-agent` and `repo=*` which can be overwritten if needed.
To learn how labels work, check out the [pipeline syntax page](../20-usage/20-workflow-syntax.md#labels).
### `WOODPECKER_HEALTHCHECK`

View file

@ -64,3 +64,41 @@ Enable IPv6 for the networks used by pipeline containers (steps). Make sure you
List of default volumes separated by comma to be mounted to all pipeline containers (steps). For example to use custom CA
certificates installed on host and host timezone use `/etc/ssl/certs:/etc/ssl/certs:ro,/etc/timezone:/etc/timezone`.
### `WOODPECKER_BACKEND_DOCKER_LIMIT_MEM_SWAP`
> Default: `0`
The maximum amount of memory a single pipeline container is allowed to swap to disk, configured in bytes. There is no limit if `0`.
### `WOODPECKER_BACKEND_DOCKER_LIMIT_MEM`
> Default: `0`
The maximum amount of memory a single pipeline container can use, configured in bytes. There is no limit if `0`.
### `WOODPECKER_BACKEND_DOCKER_LIMIT_SHM_SIZE`
> Default: `0`
The maximum amount of memory of `/dev/shm` allowed in bytes. There is no limit if `0`.
### `WOODPECKER_BACKEND_DOCKER_LIMIT_CPU_QUOTA`
> Default: `0`
The number of microseconds per CPU period that the container is limited to before throttled. There is no limit if `0`.
### `WOODPECKER_BACKEND_DOCKER_LIMIT_CPU_SHARES`
> Default: `0`
The relative weight vs. other containers.
### `WOODPECKER_BACKEND_DOCKER_LIMIT_CPU_SET`
> Default: empty
Comma-separated list to limit the specific CPUs or cores a pipeline container can use.
Example: `WOODPECKER_BACKEND_DOCKER_LIMIT_CPU_SET=1,2`

View file

@ -8,9 +8,9 @@ The Kubernetes backend executes steps inside standalone Pods. A temporary PVC is
## Images from private registries
In order to pull private container images defined in your pipeline YAML you must provide [registry credentials in Kubernetes Secret](https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/).
As the Secret is Agent-wide, it has to be placed in namespace defined by `WOODPECKER_BACKEND_K8S_NAMESPACE`.
Besides, you need to provide the Secret name to Agent via `WOODPECKER_BACKEND_K8S_PULL_SECRET_NAMES`.
In addition to [registries specified in the UI](../../20-usage/41-registries.md), you may provide [registry credentials in Kubernetes Secrets](https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/) to pull private container images defined in your pipeline YAML.
Place these Secrets in namespace defined by `WOODPECKER_BACKEND_K8S_NAMESPACE` and provide the Secret names to Agents via `WOODPECKER_BACKEND_K8S_PULL_SECRET_NAMES`.
## Job specific configuration

View file

@ -4,13 +4,15 @@ Some versions need some changes to the server configuration or the pipeline conf
## `next`
- Set `/woodpecker` as defautl workdir for the **woodpecker-cli** container
- Deprecate `WOODPECKER_FILTER_LABELS` use `WOODPECKER_AGENT_LABELS`
- Removed built-in environment variables:
- `CI_COMMIT_URL` use `CI_PIPELINE_FORGE_URL`
- `CI_STEP_FINISHED` as empty during execution
- `CI_PIPELINE_FINISHED` as empty during execution
- `CI_PIPELINE_STATUS` was always `success`
- `CI_STEP_STATUS` was always `success`
- Set `/woodpecker` as defautl workdir for the **woodpecker-cli** container
- Move docker resource limit settings from server into agent configuration
- Rename server environment variable `WOODPECKER_ESCALATE` to `WOODPECKER_PLUGINS_PRIVILEGED`
- All default privileged plugins (like `woodpeckerci/plugin-docker-buildx`) were removed. Please carefully [re-add those plugins](./30-administration/10-server-config.md#woodpecker_plugins_privileged) you trust and rely on.
- `WOODPECKER_DEFAULT_CLONE_IMAGE` got depricated use `WOODPECKER_DEFAULT_CLONE_PLUGIN`
@ -23,7 +25,7 @@ Some versions need some changes to the server configuration or the pipeline conf
- Pipelines without a config file will now be skipped instead of failing
- Removed implicitly defined `regcred` image pull secret name. Set it explicitly via `WOODPECKER_BACKEND_K8S_PULL_SECRET_NAMES`
- Removed `includes` and `excludes` support from **event** filter
- Removed uppercasing all secret env vars, instead, the value of the `secrets` property is used. [Read more](./20-usage/40-secrets.md#use-secrets-in-commands)
- Removed uppercasing all secret env vars, instead, the value of the `secrets` property is used. [Read more](./20-usage/40-secrets.md#usage)
- Removed alternative names for secrets, use `environment` with `from_secret`
- Removed slice definition for env vars
- Removed `environment` filter, use `when.evaluate`
@ -36,6 +38,7 @@ Some versions need some changes to the server configuration or the pipeline conf
- Removed old API routes: `registry/` -> `registries`, `/authorize/token`
- Replaced `registry` command with `repo registry` in cli
- Disallow upgrades from 1.x, upgrade to 2.x first
- Deprecated `secrets`, use `environment` with `from_secret`
## 2.0.0

View file

@ -147,7 +147,7 @@ const config: Config = {
],
},
],
copyright: `Copyright © ${new Date().getFullYear()} Woodpecker CI. Built with Docusaurus.`,
copyright: `Copyright © ${new Date().getFullYear()} Woodpecker Authors. Built with Docusaurus.`,
},
prism: {
theme: themes.github,

View file

@ -14,19 +14,19 @@
"write-heading-ids": "docusaurus write-heading-ids"
},
"dependencies": {
"@docusaurus/core": "^3.1.0",
"@docusaurus/plugin-content-blog": "^3.1.0",
"@docusaurus/preset-classic": "^3.1.0",
"@easyops-cn/docusaurus-search-local": "^0.44.0",
"@mdx-js/react": "^3.0.0",
"@docusaurus/core": "^3.5.2",
"@docusaurus/plugin-content-blog": "^3.5.2",
"@docusaurus/preset-classic": "^3.5.2",
"@easyops-cn/docusaurus-search-local": "^0.45.0",
"@mdx-js/react": "^3.1.0",
"@svgr/webpack": "^8.1.0",
"clsx": "^2.1.0",
"esbuild-loader": "^4.1.0",
"clsx": "^2.1.1",
"esbuild-loader": "^4.2.2",
"file-loader": "^6.2.0",
"prism-react-renderer": "^2.3.1",
"prism-react-renderer": "^2.4.0",
"react": "^18.3.1",
"react-dom": "^18.2.0",
"redocusaurus": "^2.0.2",
"react-dom": "^18.3.1",
"redocusaurus": "^2.1.2",
"url-loader": "^4.1.1"
},
"browserslist": {
@ -42,18 +42,20 @@
]
},
"devDependencies": {
"@docusaurus/module-type-aliases": "^3.1.0",
"@docusaurus/module-type-aliases": "^3.5.2",
"@docusaurus/tsconfig": "3.5.2",
"@docusaurus/types": "^3.1.0",
"@types/node": "^20.11.30",
"@types/react": "^18.2.67",
"@docusaurus/types": "^3.5.2",
"@types/node": "^20.17.1",
"@types/react": "^18.3.12",
"@types/react-helmet": "^6.1.11",
"@types/react-router-dom": "^5.3.3",
"typescript": "^5.4.3"
"typescript": "^5.6.3"
},
"pnpm": {
"overrides": {
"got": "^14.0.0"
"got": "^14.0.0",
"path-to-regexp": "^3.3.0",
"cookie": "^1.0.0"
}
}
}

View file

@ -16,7 +16,7 @@
"@tsconfig/docusaurus": "^2.0.3",
"@types/node": "^20.12.13",
"axios": "^1.7.2",
"concurrently": "^8.2.2",
"concurrently": "^9.0.0",
"isomorphic-dompurify": "^2.11.0",
"marked": "^14.0.0",
"tslib": "^2.6.2",

View file

@ -40,11 +40,6 @@
"docs": "https://raw.githubusercontent.com/woodpecker-ci/plugin-extend-env/main/docs.md",
"verified": true
},
{
"name": "Block Git changes",
"docs": "https://codeberg.org/qwerty287/woodpecker-block-git-changes/raw/branch/main/docs.md",
"verified": false
},
{
"name": "Regex check",
"docs": "https://codeberg.org/qwerty287/woodpecker-regex-check/raw/branch/main/docs.md",
@ -219,6 +214,11 @@
"name": "Docker Tags",
"docs": "https://raw.githubusercontent.com/dvjn/woodpecker-docker-tags-plugin/main/docs.md",
"verified": false
},
{
"name": "Telegram",
"docs": "https://raw.githubusercontent.com/appleboy/drone-telegram/refs/heads/master/DOCS.md",
"verified": false
}
]
}

File diff suppressed because it is too large Load diff

View file

@ -5,11 +5,11 @@
"systems": "systems"
},
"locked": {
"lastModified": 1710146030,
"narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=",
"lastModified": 1726560853,
"narHash": "sha256-X6rJYSESBVr3hBoH0WbKE5KvhPU5bloyZ2L4K60/fPQ=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a",
"rev": "c1dfcf08411b08f6b8615f7d8971a2bfa81d5e8a",
"type": "github"
},
"original": {
@ -20,11 +20,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1724224976,
"narHash": "sha256-Z/ELQhrSd7bMzTO8r7NZgi9g5emh+aRKoCdaAv5fiO0=",
"lastModified": 1729665710,
"narHash": "sha256-AlcmCXJZPIlO5dmFzV3V2XF6x/OpNWUV8Y/FMPGd8Z4=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "c374d94f1536013ca8e92341b540eba4c22f9c62",
"rev": "2768c7d042a37de65bb1b5b3268fc987e534c49d",
"type": "github"
},
"original": {

View file

@ -34,6 +34,10 @@
go-mockery
protobuf
sqlite
go-swag # for generate-swagger
addlicense
protoc-gen-go
protoc-gen-go-grpc
];
CFLAGS = "-I${pkgs.glibc.dev}/include";
LDFLAGS = "-L${pkgs.glibc}/lib";

76
go.mod
View file

@ -2,24 +2,24 @@ module go.woodpecker-ci.org/woodpecker/v2
go 1.22.0
toolchain go1.23.0
toolchain go1.23.2
require (
al.essio.dev/pkg/shellescape v1.5.0
al.essio.dev/pkg/shellescape v1.5.1
code.gitea.io/sdk/gitea v0.19.0
codeberg.org/6543/go-yaml2json v1.0.0
codeberg.org/6543/xyaml v1.1.0
codeberg.org/mvdkleijn/forgejo-sdk/forgejo v1.1.1
github.com/6543/logfile-open v1.2.1
github.com/adrg/xdg v0.5.0
github.com/bmatcuk/doublestar/v4 v4.6.1
github.com/caddyserver/certmagic v0.21.3
github.com/bmatcuk/doublestar/v4 v4.7.1
github.com/caddyserver/certmagic v0.21.4
github.com/cenkalti/backoff/v4 v4.3.0
github.com/charmbracelet/huh v0.5.3
github.com/charmbracelet/huh v0.6.0
github.com/charmbracelet/huh/spinner v0.0.0-20240327025511-ec643317aa10
github.com/distribution/reference v0.6.0
github.com/docker/cli v27.1.2+incompatible
github.com/docker/docker v27.1.2+incompatible
github.com/docker/cli v27.3.1+incompatible
github.com/docker/docker v27.3.1+incompatible
github.com/docker/go-connections v0.5.0
github.com/docker/go-units v0.5.0
github.com/drone/envsubst v1.0.3
@ -32,48 +32,48 @@ require (
github.com/gitsight/go-vcsurl v1.0.1
github.com/go-sql-driver/mysql v1.8.1
github.com/golang-jwt/jwt/v5 v5.2.1
github.com/google/go-github/v64 v64.0.0
github.com/google/go-github/v66 v66.0.0
github.com/google/tink/go v1.7.0
github.com/gorilla/securecookie v1.1.2
github.com/hashicorp/go-hclog v1.6.3
github.com/hashicorp/go-plugin v1.6.1
github.com/jellydator/ttlcache/v3 v3.2.1
github.com/jellydator/ttlcache/v3 v3.3.0
github.com/joho/godotenv v1.5.1
github.com/kinbiko/jsonassert v1.1.1
github.com/kinbiko/jsonassert v1.2.0
github.com/lib/pq v1.10.9
github.com/mattn/go-sqlite3 v1.14.22
github.com/mattn/go-sqlite3 v1.14.24
github.com/mitchellh/mapstructure v1.5.0
github.com/moby/moby v27.1.2+incompatible
github.com/moby/moby v27.3.1+incompatible
github.com/moby/term v0.5.0
github.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a
github.com/muesli/termenv v0.15.3-0.20240912151726-82936c5ea257
github.com/neticdk/go-bitbucket v1.0.0
github.com/oklog/ulid/v2 v2.1.0
github.com/pkg/errors v0.9.1
github.com/prometheus/client_golang v1.20.2
github.com/prometheus/client_golang v1.20.4
github.com/rs/zerolog v1.33.0
github.com/stretchr/testify v1.9.0
github.com/swaggo/files v1.0.1
github.com/swaggo/gin-swagger v1.6.0
github.com/swaggo/swag v1.16.3
github.com/urfave/cli-docs/v3 v3.0.0-alpha5.0.20240714105325-1da00919bcb4
github.com/urfave/cli/v3 v3.0.0-alpha9.0.20240811205807-fc07a8c3673f
github.com/xanzy/go-gitlab v0.108.0
github.com/urfave/cli/v3 v3.0.0-alpha9.0.20241004184838-20ef97b2155a
github.com/xanzy/go-gitlab v0.111.0
github.com/xeipuuv/gojsonschema v1.2.0
github.com/yaronf/httpsign v0.3.1
github.com/zalando/go-keyring v0.2.5
go.uber.org/multierr v1.11.0
golang.org/x/crypto v0.26.0
golang.org/x/net v0.28.0
golang.org/x/oauth2 v0.22.0
golang.org/x/crypto v0.28.0
golang.org/x/net v0.30.0
golang.org/x/oauth2 v0.23.0
golang.org/x/sync v0.8.0
golang.org/x/term v0.23.0
golang.org/x/text v0.17.0
google.golang.org/grpc v1.65.0
google.golang.org/protobuf v1.34.2
golang.org/x/term v0.25.0
golang.org/x/text v0.19.0
google.golang.org/grpc v1.67.1
google.golang.org/protobuf v1.35.1
gopkg.in/yaml.v3 v3.0.1
k8s.io/api v0.31.0
k8s.io/apimachinery v0.31.0
k8s.io/client-go v0.31.0
k8s.io/api v0.31.1
k8s.io/apimachinery v0.31.1
k8s.io/client-go v0.31.1
src.techknowlogick.com/xormigrate v1.7.1
xorm.io/builder v0.3.13
xorm.io/xorm v1.3.9
@ -93,10 +93,10 @@ require (
github.com/caddyserver/zerossl v0.1.3 // indirect
github.com/catppuccin/go v0.2.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/charmbracelet/bubbles v0.19.0 // indirect
github.com/charmbracelet/bubbletea v0.27.0 // indirect
github.com/charmbracelet/bubbles v0.20.0 // indirect
github.com/charmbracelet/bubbletea v1.1.0 // indirect
github.com/charmbracelet/lipgloss v0.13.0 // indirect
github.com/charmbracelet/x/ansi v0.2.2 // indirect
github.com/charmbracelet/x/ansi v0.2.3 // indirect
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 // indirect
github.com/charmbracelet/x/term v0.2.0 // indirect
github.com/cloudwego/base64x v0.1.4 // indirect
@ -147,7 +147,7 @@ require (
github.com/json-iterator/go v1.1.12 // indirect
github.com/julienschmidt/httprouter v1.3.0 // indirect
github.com/klauspost/compress v1.17.9 // indirect
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
github.com/klauspost/cpuid/v2 v2.2.8 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/lestrrat-go/blackmagic v1.0.2 // indirect
github.com/lestrrat-go/httpcc v1.0.1 // indirect
@ -162,8 +162,8 @@ require (
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-localereader v0.0.1 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/mholt/acmez/v2 v2.0.1 // indirect
github.com/miekg/dns v1.1.59 // indirect
github.com/mholt/acmez/v2 v2.0.3 // indirect
github.com/miekg/dns v1.1.62 // indirect
github.com/mitchellh/go-testing-interface v1.0.0 // indirect
github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect
github.com/moby/docker-image-spec v1.3.1 // indirect
@ -195,7 +195,7 @@ require (
github.com/x448/float16 v0.8.4 // indirect
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
github.com/zeebo/blake3 v0.2.3 // indirect
github.com/zeebo/blake3 v0.2.4 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 // indirect
go.opentelemetry.io/otel v1.28.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.28.0 // indirect
@ -204,12 +204,12 @@ require (
go.opentelemetry.io/otel/trace v1.28.0 // indirect
go.uber.org/zap v1.27.0 // indirect
golang.org/x/arch v0.8.0 // indirect
golang.org/x/mod v0.17.0 // indirect
golang.org/x/sys v0.24.0 // indirect
golang.org/x/mod v0.18.0 // indirect
golang.org/x/sys v0.26.0 // indirect
golang.org/x/time v0.5.0 // indirect
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20240722135656-d784300faade // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240723171418-e6d459c13d2a // indirect
golang.org/x/tools v0.22.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20240814211410-ddb44dafa142 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gotest.tools/v3 v3.4.0 // indirect

149
go.sum
View file

@ -1,5 +1,5 @@
al.essio.dev/pkg/shellescape v1.5.0 h1:7oTvSsQ5kg9WksA9O58y9wjYnY4jP0CL82/Q8WLUGKk=
al.essio.dev/pkg/shellescape v1.5.0/go.mod h1:6sIqp7X2P6mThCQ7twERpZTuigpr6KbZWtls1U8I890=
al.essio.dev/pkg/shellescape v1.5.1 h1:86HrALUujYS/h+GtqoB26SBEdkWfmMI6FubjXlsXyho=
al.essio.dev/pkg/shellescape v1.5.1/go.mod h1:6sIqp7X2P6mThCQ7twERpZTuigpr6KbZWtls1U8I890=
code.gitea.io/sdk/gitea v0.19.0 h1:8I6s1s4RHgzxiPHhOQdgim1RWIRcr0LVMbHBjBFXq4Y=
code.gitea.io/sdk/gitea v0.19.0/go.mod h1:IG9xZJoltDNeDSW0qiF2Vqx5orMWa7OhVWrjvrd5NpI=
codeberg.org/6543/go-yaml2json v1.0.0 h1:heGqo9VEi7gY2yNqjj7X4ADs5nzlFIbGsJtgYDLrnig=
@ -40,16 +40,16 @@ github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiE
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bmatcuk/doublestar/v4 v4.6.1 h1:FH9SifrbvJhnlQpztAx++wlkk70QBf0iBWDwNy7PA4I=
github.com/bmatcuk/doublestar/v4 v4.6.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
github.com/bmatcuk/doublestar/v4 v4.7.1 h1:fdDeAqgT47acgwd9bd9HxJRDmc9UAmPpc+2m0CXv75Q=
github.com/bmatcuk/doublestar/v4 v4.7.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
github.com/bufbuild/protocompile v0.4.0 h1:LbFKd2XowZvQ/kajzguUp2DC9UEIQhIq77fZZlaQsNA=
github.com/bufbuild/protocompile v0.4.0/go.mod h1:3v93+mbWn/v3xzN+31nwkJfrEpAUwp+BagBSZWx+TP8=
github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0=
github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=
github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
github.com/caddyserver/certmagic v0.21.3 h1:pqRRry3yuB4CWBVq9+cUqu+Y6E2z8TswbhNx1AZeYm0=
github.com/caddyserver/certmagic v0.21.3/go.mod h1:Zq6pklO9nVRl3DIFUw9gVUfXKdpc/0qwTUAQMBlfgtI=
github.com/caddyserver/certmagic v0.21.4 h1:e7VobB8rffHv8ZZpSiZtEwnLDHUwLVYLWzWSa1FfKI0=
github.com/caddyserver/certmagic v0.21.4/go.mod h1:swUXjQ1T9ZtMv95qj7/InJvWLXURU85r+CfG0T+ZbDE=
github.com/caddyserver/zerossl v0.1.3 h1:onS+pxp3M8HnHpN5MMbOMyNjmTheJyWRaZYwn+YTAyA=
github.com/caddyserver/zerossl v0.1.3/go.mod h1:CxA0acn7oEGO6//4rtrRjYgEoa4MFw/XofZnrYwGqG4=
github.com/catppuccin/go v0.2.0 h1:ktBeIrIP42b/8FGiScP9sgrWOss3lw0Z5SktRoithGA=
@ -58,18 +58,18 @@ github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK3
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/charmbracelet/bubbles v0.19.0 h1:gKZkKXPP6GlDk6EcfujDK19PCQqRjaJZQ7QRERx1UF0=
github.com/charmbracelet/bubbles v0.19.0/go.mod h1:WILteEqZ+krG5c3ntGEMeG99nCupcuIk7V0/zOP0tOA=
github.com/charmbracelet/bubbletea v0.27.0 h1:Mznj+vvYuYagD9Pn2mY7fuelGvP0HAXtZYGgRBCbHvU=
github.com/charmbracelet/bubbletea v0.27.0/go.mod h1:5MdP9XH6MbQkgGhnlxUqCNmBXf9I74KRQ8HIidRxV1Y=
github.com/charmbracelet/huh v0.5.3 h1:3KLP4a/K1/S4dq4xFMTNMt3XWhgMl/yx8NYtygQ0bmg=
github.com/charmbracelet/huh v0.5.3/go.mod h1:OZC3lshuF+/y8laj//DoZdFSHxC51OrtXLJI8xWVouQ=
github.com/charmbracelet/bubbles v0.20.0 h1:jSZu6qD8cRQ6k9OMfR1WlM+ruM8fkPWkHvQWD9LIutE=
github.com/charmbracelet/bubbles v0.20.0/go.mod h1:39slydyswPy+uVOHZ5x/GjwVAFkCsV8IIVy+4MhzwwU=
github.com/charmbracelet/bubbletea v1.1.0 h1:FjAl9eAL3HBCHenhz/ZPjkKdScmaS5SK69JAK2YJK9c=
github.com/charmbracelet/bubbletea v1.1.0/go.mod h1:9Ogk0HrdbHolIKHdjfFpyXJmiCzGwy+FesYkZr7hYU4=
github.com/charmbracelet/huh v0.6.0 h1:mZM8VvZGuE0hoDXq6XLxRtgfWyTI3b2jZNKh0xWmax8=
github.com/charmbracelet/huh v0.6.0/go.mod h1:GGNKeWCeNzKpEOh/OJD8WBwTQjV3prFAtQPpLv+AVwU=
github.com/charmbracelet/huh/spinner v0.0.0-20240327025511-ec643317aa10 h1:/HZJSyFVH5rB1MlCDfkhQhRbLPD2Er29ngWXiUQ8bik=
github.com/charmbracelet/huh/spinner v0.0.0-20240327025511-ec643317aa10/go.mod h1:nrBG0YEHaxdbqHXW1xvG1hPqkuac9Eg7RTMvogiXuz0=
github.com/charmbracelet/lipgloss v0.13.0 h1:4X3PPeoWEDCMvzDvGmTajSyYPcZM4+y8sCA/SsA3cjw=
github.com/charmbracelet/lipgloss v0.13.0/go.mod h1:nw4zy0SBX/F/eAO1cWdcvy6qnkDUxr8Lw7dvFrAIbbY=
github.com/charmbracelet/x/ansi v0.2.2 h1:BC7xzaVpfWIYZRNE8NhO9zo8KA4eGUL6L/JWXDh3GF0=
github.com/charmbracelet/x/ansi v0.2.2/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw=
github.com/charmbracelet/x/ansi v0.2.3 h1:VfFN0NUpcjBRd4DnKfRaIRo53KRgey/nhOoEqosGDEY=
github.com/charmbracelet/x/ansi v0.2.3/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw=
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 h1:qko3AQ4gK1MTS/de7F5hPGx6/k1u0w4TeYmBFwzYVP4=
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0/go.mod h1:pBhA0ybfXv6hDjQUZ7hk1lVxBiUbupdw5R31yPUViVQ=
github.com/charmbracelet/x/term v0.2.0 h1:cNB9Ot9q8I711MyZ7myUR5HFWL/lc3OpU8jZ4hwm0x0=
@ -107,10 +107,10 @@ github.com/denisenkom/go-mssqldb v0.12.3/go.mod h1:k0mtMFOnU+AihqFxPMiF05rtiDror
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ=
github.com/docker/cli v27.1.2+incompatible h1:nYviRv5Y+YAKx3dFrTvS1ErkyVVunKOhoweCTE1BsnI=
github.com/docker/cli v27.1.2+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/docker v27.1.2+incompatible h1:AhGzR1xaQIy53qCkxARaFluI00WPGtXn0AJuoQsVYTY=
github.com/docker/docker v27.1.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/cli v27.3.1+incompatible h1:qEGdFBF3Xu6SCvCYhc7CzaQTlBmqDuzxPDpigSyeKQQ=
github.com/docker/cli v27.3.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/docker v27.3.1+incompatible h1:KttF0XoteNTicmUtBO0L2tP+J7FGRFTjaEF4k6WdhfI=
github.com/docker/docker v27.3.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker-credential-helpers v0.8.0 h1:YQFtbBQb4VrpoPxhFuzEBPQ9E16qz5SpHLS+uswaCp8=
github.com/docker/docker-credential-helpers v0.8.0/go.mod h1:UGFXcuoQ5TxPiB54nHOZ32AWRqQdECoh/Mg0AlEYb40=
github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
@ -222,8 +222,8 @@ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-github/v64 v64.0.0 h1:4G61sozmY3eiPAjjoOHponXDBONm+utovTKbyUb2Qdg=
github.com/google/go-github/v64 v64.0.0/go.mod h1:xB3vqMQNdHzilXBiO2I+M7iEFtHf+DP/omBOv6tQzVo=
github.com/google/go-github/v66 v66.0.0 h1:ADJsaXj9UotwdgK8/iFZtv7MLc8E8WBl62WLd/D/9+M=
github.com/google/go-github/v66 v66.0.0/go.mod h1:+4SO9Zkuyf8ytMj0csN1NR/5OTR+MfqPp8P8dVlcvY4=
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
@ -302,8 +302,8 @@ github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0f
github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jackc/puddle v1.3.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jellydator/ttlcache/v3 v3.2.1 h1:eS8ljnYY7BllYGkXw/TfczWZrXUu/CH7SIkC6ugn9Js=
github.com/jellydator/ttlcache/v3 v3.2.1/go.mod h1:bj2/e0l4jRnQdrnSTaGTsh4GSXvMjQcy41i7th0GVGw=
github.com/jellydator/ttlcache/v3 v3.3.0 h1:BdoC9cE81qXfrxeb9eoJi9dWrdhSuwXMAnHTbnBm4Wc=
github.com/jellydator/ttlcache/v3 v3.3.0/go.mod h1:bj2/e0l4jRnQdrnSTaGTsh4GSXvMjQcy41i7th0GVGw=
github.com/jhump/protoreflect v1.15.1 h1:HUMERORf3I3ZdX05WaQ6MIpd/NJ434hTp5YiKgfCL6c=
github.com/jhump/protoreflect v1.15.1/go.mod h1:jD/2GMKKE6OqX8qTjhADU1e6DShO+gavG9e0Q693nKo=
github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
@ -317,16 +317,15 @@ github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4d
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
github.com/kinbiko/jsonassert v1.1.1 h1:DB12divY+YB+cVpHULLuKePSi6+ui4M/shHSzJISkSE=
github.com/kinbiko/jsonassert v1.1.1/go.mod h1:NO4lzrogohtIdNUNzx8sdzB55M4R4Q1bsrWVdqQ7C+A=
github.com/kinbiko/jsonassert v1.2.0 h1:+/JthIVXdIrThrOtSN9ry0mNtWKXMWuvxR0nU7gQ+tI=
github.com/kinbiko/jsonassert v1.2.0/go.mod h1:pCc3uudOt+lVAbkji9O0uw8MSVt4s+1ZJ0y8Ux2F1Og=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c=
github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM=
github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
@ -387,12 +386,12 @@ github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6T
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/mholt/acmez/v2 v2.0.1 h1:3/3N0u1pLjMK4sNEAFSI+bcvzbPhRpY383sy1kLHJ6k=
github.com/mholt/acmez/v2 v2.0.1/go.mod h1:fX4c9r5jYwMyMsC+7tkYRxHibkOTgta5DIFGoe67e1U=
github.com/miekg/dns v1.1.59 h1:C9EXc/UToRwKLhK5wKU/I4QVsBUc8kE6MkHBkeypWZs=
github.com/miekg/dns v1.1.59/go.mod h1:nZpewl5p6IvctfgrckopVx2OlSEHPRO/U4SYkRklrEk=
github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM=
github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/mholt/acmez/v2 v2.0.3 h1:CgDBlEwg3QBp6s45tPQmFIBrkRIkBT4rW4orMM6p4sw=
github.com/mholt/acmez/v2 v2.0.3/go.mod h1:pQ1ysaDeGrIMvJ9dfJMk5kJNkn7L2sb3UhyrX6Q91cw=
github.com/miekg/dns v1.1.62 h1:cN8OuEF1/x5Rq6Np+h1epln8OiyPWV+lROx9LxcGgIQ=
github.com/miekg/dns v1.1.62/go.mod h1:mvDlcItzm+br7MToIKqkglaGhlFMHJ9DTNNWONWXbNQ=
github.com/mitchellh/go-testing-interface v1.0.0 h1:fzU/JVNcaqHQEcVFAKeR41fkiLdIPrefOvVG1VZ96U0=
github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4=
@ -401,8 +400,8 @@ github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyua
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
github.com/moby/moby v27.1.2+incompatible h1:vqOs4c7YktTdEBnPQNm0Q+M+IOuxxTCkrYJLBAVsEHQ=
github.com/moby/moby v27.1.2+incompatible/go.mod h1:fDXVQ6+S340veQPv35CzDahGBmHsiclFwfEygB/TWMc=
github.com/moby/moby v27.3.1+incompatible h1:KQbXBjo7PavKpzIl7UkHT31y9lw/e71Uvrqhr4X+zMA=
github.com/moby/moby v27.3.1+incompatible/go.mod h1:fDXVQ6+S340veQPv35CzDahGBmHsiclFwfEygB/TWMc=
github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@ -419,8 +418,8 @@ github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
github.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a h1:2MaM6YC3mGu54x+RKAA6JiFFHlHDY1UbkxqppT7wYOg=
github.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a/go.mod h1:hxSnBBYLK21Vtq/PHd0S2FYCxBXzBua8ov5s1RobyRQ=
github.com/muesli/termenv v0.15.3-0.20240912151726-82936c5ea257 h1:RNw/zu+CJemcRlDFPjElZUbY2UlI/MA2B3I6PM3Isiw=
github.com/muesli/termenv v0.15.3-0.20240912151726-82936c5ea257/go.mod h1:hxSnBBYLK21Vtq/PHd0S2FYCxBXzBua8ov5s1RobyRQ=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/neticdk/go-bitbucket v1.0.0 h1:FPvHEgPHoDwD2VHbpyu2R2gnoWQ867RxZd2FivS4wSw=
@ -453,8 +452,8 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v1.20.2 h1:5ctymQzZlyOON1666svgwn3s6IKWgfbjsejTMiXIyjg=
github.com/prometheus/client_golang v1.20.2/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE=
github.com/prometheus/client_golang v1.20.4 h1:Tgh3Yr67PaOv/uTqloMsCEdeuFTatm5zIq5+qNN23vI=
github.com/prometheus/client_golang v1.20.4/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE=
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc=
@ -526,12 +525,12 @@ github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65E
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
github.com/urfave/cli-docs/v3 v3.0.0-alpha5.0.20240714105325-1da00919bcb4 h1:exFN/ZOxXslYr9t2AjrniP7wPjp/VLLAJhgazj92EBg=
github.com/urfave/cli-docs/v3 v3.0.0-alpha5.0.20240714105325-1da00919bcb4/go.mod h1:AIqom6Q60U4tiqHp41i7+/AB2XHgi1WvQ7jOFlccmZ4=
github.com/urfave/cli/v3 v3.0.0-alpha9.0.20240811205807-fc07a8c3673f h1:C3vgiHDZBVKtNLp3PCQ9//V2NBhGOwUhLILmZhB6/jY=
github.com/urfave/cli/v3 v3.0.0-alpha9.0.20240811205807-fc07a8c3673f/go.mod h1:Z1ItyMma7t6I7zHG9OpbExhHQOSkFf/96n+mAZ9MtVI=
github.com/urfave/cli/v3 v3.0.0-alpha9.0.20241004184838-20ef97b2155a h1:ipFw/N7kumxX+CA9UoKXX86MNfYsfsom8YOdUC+Rsfw=
github.com/urfave/cli/v3 v3.0.0-alpha9.0.20241004184838-20ef97b2155a/go.mod h1:Z1ItyMma7t6I7zHG9OpbExhHQOSkFf/96n+mAZ9MtVI=
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
github.com/xanzy/go-gitlab v0.108.0 h1:IEvEUWFR5G1seslRhJ8gC//INiIUqYXuSUoBd7/gFKE=
github.com/xanzy/go-gitlab v0.108.0/go.mod h1:wKNKh3GkYDMOsGmnfuX+ITCmDuSDWFO0G+C4AygL9RY=
github.com/xanzy/go-gitlab v0.111.0 h1:4zT52QdDVxGYAGxN2VY8upSvZIiuiI+Z4d+c+7D/lII=
github.com/xanzy/go-gitlab v0.111.0/go.mod h1:wKNKh3GkYDMOsGmnfuX+ITCmDuSDWFO0G+C4AygL9RY=
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo=
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
@ -548,8 +547,8 @@ github.com/zalando/go-keyring v0.2.5 h1:Bc2HHpjALryKD62ppdEzaFG6VxL6Bc+5v0LYpN8L
github.com/zalando/go-keyring v0.2.5/go.mod h1:HL4k+OXQfJUWaMnqyuSOc0drfGPX2b51Du6K+MRgZMk=
github.com/zeebo/assert v1.1.0 h1:hU1L1vLTHsnO8x8c9KAR5GmM5QscxHg5RNU5z5qbUWY=
github.com/zeebo/assert v1.1.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
github.com/zeebo/blake3 v0.2.3 h1:TFoLXsjeXqRNFxSbk35Dk4YtszE/MQQGK10BH4ptoTg=
github.com/zeebo/blake3 v0.2.3/go.mod h1:mjJjZpnsyIVtVgTOSpJ9vmRE4wgDeyt2HU3qXvvKCaQ=
github.com/zeebo/blake3 v0.2.4 h1:KYQPkhpRtcqh0ssGYcKLG1JYvddkEA8QwCM/yBqhaZI=
github.com/zeebo/blake3 v0.2.4/go.mod h1:7eeQ6d2iXWRGF6npfaxl2CU+xy2Fjo2gxeyZGCRUjcE=
github.com/zeebo/pcg v1.0.1 h1:lyqfGeWiv4ahac6ttHs+I5hwtH/+1mrhlCtVNQM2kHo=
github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4=
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
@ -604,16 +603,16 @@ golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5y
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw=
golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54=
golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw=
golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0=
golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
@ -627,10 +626,10 @@ golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qx
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE=
golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg=
golang.org/x/oauth2 v0.22.0 h1:BzDx2FehcG7jJwgWLELCdmLuxk2i+x9UDpSiss2u0ZA=
golang.org/x/oauth2 v0.22.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4=
golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU=
golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs=
golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@ -667,14 +666,14 @@ golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg=
golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.23.0 h1:F6D4vR+EHoL9/sWAWgAR1H2DcHr4PareCbAaCo1RpuU=
golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk=
golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24=
golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
@ -682,8 +681,8 @@ golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc=
golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@ -700,22 +699,22 @@ golang.org/x/tools v0.0.0-20201124115921-2c860bdd6e78/go.mod h1:emZCQorbCU4vsT4f
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA=
golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c=
golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/genproto/googleapis/api v0.0.0-20240722135656-d784300faade h1:WxZOF2yayUHpHSbUE6NMzumUzBxYc3YGwo0YHnbzsJY=
google.golang.org/genproto/googleapis/api v0.0.0-20240722135656-d784300faade/go.mod h1:mw8MG/Qz5wfgYr6VqVCiZcHe/GJEfI+oGGDCohaVgB0=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240723171418-e6d459c13d2a h1:hqK4+jJZXCU4pW7jsAdGOVFIfLHQeV7LaizZKnZ84HI=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240723171418-e6d459c13d2a/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY=
google.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc=
google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ=
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
google.golang.org/genproto/googleapis/api v0.0.0-20240814211410-ddb44dafa142 h1:wKguEg1hsxI2/L3hUYrpo1RVi48K+uTyzKqprwLXsb8=
google.golang.org/genproto/googleapis/api v0.0.0-20240814211410-ddb44dafa142/go.mod h1:d6be+8HhtEtucleCbxpPW9PA9XwISACu8nvpPqF0BVo=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142 h1:e7S5W7MGGLaSu8j3YjdezkZ+m1/Nm0uRVRMEMGk26Xs=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU=
google.golang.org/grpc v1.67.1 h1:zWnc1Vrcno+lHZCOofnIMvycFcc0QRGIzm9dhnDX68E=
google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA=
google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA=
google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
@ -740,12 +739,12 @@ gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gotest.tools/v3 v3.4.0 h1:ZazjZUfuVeZGLAmlKKuyv3IKP5orXcwtOwDQH6YVr6o=
gotest.tools/v3 v3.4.0/go.mod h1:CtbdzLSsqVhDgMtKsx03ird5YTGB3ar27v0u/yKBW5g=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
k8s.io/api v0.31.0 h1:b9LiSjR2ym/SzTOlfMHm1tr7/21aD7fSkqgD/CVJBCo=
k8s.io/api v0.31.0/go.mod h1:0YiFF+JfFxMM6+1hQei8FY8M7s1Mth+z/q7eF1aJkTE=
k8s.io/apimachinery v0.31.0 h1:m9jOiSr3FoSSL5WO9bjm1n6B9KROYYgNZOb4tyZ1lBc=
k8s.io/apimachinery v0.31.0/go.mod h1:rsPdaZJfTfLsNJSQzNHQvYoTmxhoOEofxtOsF3rtsMo=
k8s.io/client-go v0.31.0 h1:QqEJzNjbN2Yv1H79SsS+SWnXkBgVu4Pj3CJQgbx0gI8=
k8s.io/client-go v0.31.0/go.mod h1:Y9wvC76g4fLjmU0BA+rV+h2cncoadjvjjkkIGoTLcGU=
k8s.io/api v0.31.1 h1:Xe1hX/fPW3PXYYv8BlozYqw63ytA92snr96zMW9gWTU=
k8s.io/api v0.31.1/go.mod h1:sbN1g6eY6XVLeqNsZGLnI5FwVseTrZX7Fv3O26rhAaI=
k8s.io/apimachinery v0.31.1 h1:mhcUBbj7KUjaVhyXILglcVjuS4nYXiwC+KKFBgIVy7U=
k8s.io/apimachinery v0.31.1/go.mod h1:rsPdaZJfTfLsNJSQzNHQvYoTmxhoOEofxtOsF3rtsMo=
k8s.io/client-go v0.31.1 h1:f0ugtWSbWpxHR7sjVpQwuvw9a3ZKLXX0u0itkFXufb0=
k8s.io/client-go v0.31.1/go.mod h1:sKI8871MJN2OyeqRlmA4W4KM9KBdBUpDLu/43eGemCg=
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 h1:BZqlfIlq5YbRMFko6/PM7FjZpUb45WallggurYhKGag=

View file

@ -10,7 +10,7 @@ section: daemon/system
contents:
- src: ./dist/agent/linux_amd64/woodpecker-agent
dst: /usr/local/bin/woodpecker-agent
- src: ./woodpecker-agent.service
- src: ./nfpm/woodpecker-agent.service
dst: /usr/local/lib/systemd/system/
- src: ./woodpecker-agent.env.example
- src: ./nfpm/woodpecker-agent.env.example
dst: /etc/woodpecker/

View file

@ -10,7 +10,7 @@ section: daemon/system
contents:
- src: ./dist/server/linux_amd64/woodpecker-server
dst: /usr/local/bin/woodpecker-server
- src: ./woodpecker-server.service
- src: ./nfpm/woodpecker-server.service
dst: /usr/local/lib/systemd/system/
- src: ./woodpecker-server.env.example
- src: ./nfpm/woodpecker-server.env.example
dst: /etc/woodpecker/

View file

@ -0,0 +1,71 @@
// 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 docker
import (
"fmt"
"strings"
"github.com/rs/zerolog/log"
"github.com/urfave/cli/v3"
)
type config struct {
enableIPv6 bool
network string
volumes []string
resourceLimit resourceLimit
}
type resourceLimit struct {
MemSwapLimit int64
MemLimit int64
ShmSize int64
CPUQuota int64
CPUShares int64
CPUSet string
}
func configFromCli(c *cli.Command) (config, error) {
conf := config{
enableIPv6: c.Bool("backend-docker-ipv6"),
network: c.String("backend-docker-network"),
resourceLimit: resourceLimit{
MemSwapLimit: c.Int("backend-docker-limit-mem-swap"),
MemLimit: c.Int("backend-docker-limit-mem"),
ShmSize: c.Int("backend-docker-limit-shm-size"),
CPUQuota: c.Int("backend-docker-limit-cpu-quota"),
CPUShares: c.Int("backend-docker-limit-cpu-shares"),
CPUSet: c.String("backend-docker-limit-cpu-set"),
},
}
volumes := strings.Split(c.String("backend-docker-volumes"), ",")
conf.volumes = make([]string, 0, len(volumes))
// Validate provided volume definitions
for _, v := range volumes {
if v == "" {
continue
}
parts, err := splitVolumeParts(v)
if err != nil {
log.Error().Err(err).Msgf("can not parse volume config")
return conf, fmt.Errorf("invalid volume '%s' provided in WOODPECKER_BACKEND_DOCKER_VOLUMES: %w", v, err)
}
conf.volumes = append(conf.volumes, strings.Join(parts, ":"))
}
return conf, nil
}

View file

@ -68,20 +68,20 @@ func toContainerName(step *types.Step) string {
}
// returns a container host configuration.
func toHostConfig(step *types.Step) *container.HostConfig {
func toHostConfig(step *types.Step, conf *config) *container.HostConfig {
config := &container.HostConfig{
Resources: container.Resources{
CPUQuota: step.CPUQuota,
CPUShares: step.CPUShares,
CpusetCpus: step.CPUSet,
Memory: step.MemLimit,
MemorySwap: step.MemSwapLimit,
CPUQuota: conf.resourceLimit.CPUQuota,
CPUShares: conf.resourceLimit.CPUShares,
CpusetCpus: conf.resourceLimit.CPUSet,
Memory: conf.resourceLimit.MemLimit,
MemorySwap: conf.resourceLimit.MemSwapLimit,
},
ShmSize: conf.resourceLimit.ShmSize,
LogConfig: container.LogConfig{
Type: "json-file",
},
Privileged: step.Privileged,
ShmSize: step.ShmSize,
}
if len(step.NetworkMode) != 0 {

View file

@ -196,37 +196,44 @@ func TestToConfigSmall(t *testing.T) {
}
func TestToConfigFull(t *testing.T) {
engine := docker{info: system.Info{OSType: "linux/riscv64"}}
engine := docker{
info: system.Info{OSType: "linux/riscv64"},
config: config{
enableIPv6: true,
resourceLimit: resourceLimit{
MemSwapLimit: 12,
MemLimit: 13,
ShmSize: 14,
CPUQuota: 15,
CPUShares: 16,
},
},
}
conf := engine.toConfig(&backend.Step{
Name: "test",
UUID: "09238932",
Type: backend.StepTypeCommands,
Image: "golang:1.2.3",
Pull: true,
Detached: true,
Privileged: true,
WorkingDir: "/src/abc",
Environment: map[string]string{"TAGS": "sqlite"},
Commands: []string{"go test", "go vet ./..."},
ExtraHosts: []backend.HostAlias{{Name: "t", IP: "1.2.3.4"}},
Volumes: []string{"/cache:/cache"},
Tmpfs: []string{"/tmp"},
Devices: []string{"/dev/sdc"},
Networks: []backend.Conn{{Name: "extra-net", Aliases: []string{"extra.net"}}},
DNS: []string{"9.9.9.9", "8.8.8.8"},
DNSSearch: nil,
MemSwapLimit: 12,
MemLimit: 13,
ShmSize: 14,
CPUQuota: 15,
CPUShares: 16,
OnFailure: true,
OnSuccess: true,
Failure: "fail",
AuthConfig: backend.Auth{Username: "user", Password: "123456"},
NetworkMode: "bridge",
Ports: []backend.Port{{Number: 21}, {Number: 22}},
Name: "test",
UUID: "09238932",
Type: backend.StepTypeCommands,
Image: "golang:1.2.3",
Pull: true,
Detached: true,
Privileged: true,
WorkingDir: "/src/abc",
Environment: map[string]string{"TAGS": "sqlite"},
Commands: []string{"go test", "go vet ./..."},
ExtraHosts: []backend.HostAlias{{Name: "t", IP: "1.2.3.4"}},
Volumes: []string{"/cache:/cache"},
Tmpfs: []string{"/tmp"},
Devices: []string{"/dev/sdc"},
Networks: []backend.Conn{{Name: "extra-net", Aliases: []string{"extra.net"}}},
DNS: []string{"9.9.9.9", "8.8.8.8"},
DNSSearch: nil,
OnFailure: true,
OnSuccess: true,
Failure: "fail",
AuthConfig: backend.Auth{Username: "user", Password: "123456"},
NetworkMode: "bridge",
Ports: []backend.Port{{Number: 21}, {Number: 22}},
})
assert.NotNil(t, conf)

View file

@ -40,11 +40,9 @@ import (
)
type docker struct {
client client.APIClient
enableIPv6 bool
network string
volumes []string
info system.Info
client client.APIClient
info system.Info
config config
}
const (
@ -132,22 +130,9 @@ func (e *docker) Load(ctx context.Context) (*backend.BackendInfo, error) {
return nil, err
}
e.enableIPv6 = c.Bool("backend-docker-ipv6")
e.network = c.String("backend-docker-network")
volumes := strings.Split(c.String("backend-docker-volumes"), ",")
e.volumes = make([]string, 0, len(volumes))
// Validate provided volume definitions
for _, v := range volumes {
if v == "" {
continue
}
parts, err := splitVolumeParts(v)
if err != nil {
log.Error().Err(err).Msgf("invalid volume '%s' provided in WOODPECKER_BACKEND_DOCKER_VOLUMES", v)
continue
}
e.volumes = append(e.volumes, strings.Join(parts, ":"))
e.config, err = configFromCli(c)
if err != nil {
return nil, err
}
return &backend.BackendInfo{
@ -175,7 +160,7 @@ func (e *docker) SetupWorkflow(ctx context.Context, conf *backend.Config, taskUU
for _, n := range conf.Networks {
_, err := e.client.NetworkCreate(ctx, n.Name, network.CreateOptions{
Driver: networkDriver,
EnableIPv6: &e.enableIPv6,
EnableIPv6: &e.config.enableIPv6,
})
if err != nil {
return err
@ -188,7 +173,7 @@ func (e *docker) StartStep(ctx context.Context, step *backend.Step, taskUUID str
log.Trace().Str("taskUUID", taskUUID).Msgf("start step %s", step.Name)
config := e.toConfig(step)
hostConfig := toHostConfig(step)
hostConfig := toHostConfig(step, &e.config)
containerName := toContainerName(step)
// create pull options with encoded authorization credentials.
@ -217,7 +202,7 @@ func (e *docker) StartStep(ctx context.Context, step *backend.Step, taskUUID str
}
// add default volumes to the host configuration
hostConfig.Binds = utils.DeduplicateStrings(append(hostConfig.Binds, e.volumes...))
hostConfig.Binds = utils.DeduplicateStrings(append(hostConfig.Binds, e.config.volumes...))
_, err := e.client.ContainerCreate(ctx, config, hostConfig, nil, nil, containerName)
if client.IsErrNotFound(err) {
@ -251,8 +236,8 @@ func (e *docker) StartStep(ctx context.Context, step *backend.Step, taskUUID str
}
// join the container to an existing network
if e.network != "" {
err = e.client.NetworkConnect(ctx, e.network, containerName, &network.EndpointSettings{})
if e.config.network != "" {
err = e.client.NetworkConnect(ctx, e.config.network, containerName, &network.EndpointSettings{})
if err != nil {
return err
}

View file

@ -56,4 +56,37 @@ var Flags = []cli.Flag{
Name: "backend-docker-volumes",
Usage: "backend docker volumes (comma separated)",
},
//
// resource limit parameters
//
&cli.IntFlag{
Sources: cli.EnvVars("WOODPECKER_BACKEND_DOCKER_LIMIT_MEM_SWAP", "WOODPECKER_LIMIT_MEM_SWAP"),
Name: "backend-docker-limit-mem-swap",
Usage: "maximum memory used for swap in bytes",
},
&cli.IntFlag{
Sources: cli.EnvVars("WOODPECKER_BACKEND_DOCKER_LIMIT_MEM", "WOODPECKER_LIMIT_MEM"),
Name: "backend-docker-limit-mem",
Usage: "maximum memory allowed in bytes",
},
&cli.IntFlag{
Sources: cli.EnvVars("WOODPECKER_BACKEND_DOCKER_LIMIT_SHM_SIZE", "WOODPECKER_LIMIT_SHM_SIZE"),
Name: "backend-docker-limit-shm-size",
Usage: "docker /dev/shm allowed in bytes",
},
&cli.IntFlag{
Sources: cli.EnvVars("WOODPECKER_BACKEND_DOCKER_LIMIT_CPU_QUOTA", "WOODPECKER_LIMIT_CPU_QUOTA"),
Name: "backend-docker-limit-cpu-quota",
Usage: "impose a cpu quota",
},
&cli.IntFlag{
Sources: cli.EnvVars("WOODPECKER_BACKEND_DOCKER_LIMIT_CPU_SHARES", "WOODPECKER_LIMIT_CPU_SHARES"),
Name: "backend-docker-limit-cpu-shares",
Usage: "change the cpu shares",
},
&cli.StringFlag{
Sources: cli.EnvVars("WOODPECKER_BACKEND_DOCKER_LIMIT_CPU_SET", "WOODPECKER_LIMIT_CPU_SET"),
Name: "backend-docker-limit-cpu-set",
Usage: "set the cpus allowed to execute containers",
},
}

View file

@ -16,6 +16,7 @@ package kubernetes
import (
"context"
std_errs "errors"
"fmt"
"io"
"maps"
@ -225,6 +226,13 @@ func (e *kube) StartStep(ctx context.Context, step *types.Step, taskUUID string)
log.Error().Err(err).Msg("could not parse backend options")
}
if needsRegistrySecret(step) {
err = startRegistrySecret(ctx, e, step)
if err != nil {
return err
}
}
log.Trace().Str("taskUUID", taskUUID).Msgf("starting step: %s", step.Name)
_, err = startPod(ctx, e, step, options)
return err
@ -382,9 +390,20 @@ func (e *kube) TailStep(ctx context.Context, step *types.Step, taskUUID string)
}
func (e *kube) DestroyStep(ctx context.Context, step *types.Step, taskUUID string) error {
var errs []error
log.Trace().Str("taskUUID", taskUUID).Msgf("Stopping step: %s", step.Name)
if needsRegistrySecret(step) {
err := stopRegistrySecret(ctx, e, step, defaultDeleteOptions)
if err != nil {
errs = append(errs, err)
}
}
err := stopPod(ctx, e, step, defaultDeleteOptions)
return err
if err != nil {
errs = append(errs, err)
}
return std_errs.Join(errs...)
}
// DestroyWorkflow destroys the pipeline environment.

View file

@ -163,6 +163,14 @@ func podSpec(step *types.Step, config *config, options BackendOptions, nsp nativ
log.Trace().Msgf("using the image pull secrets: %v", config.ImagePullSecretNames)
spec.ImagePullSecrets = secretsReferences(config.ImagePullSecretNames)
if needsRegistrySecret(step) {
log.Trace().Msgf("using an image pull secret from registries")
name, err := registrySecretName(step)
if err != nil {
return spec, err
}
spec.ImagePullSecrets = append(spec.ImagePullSecrets, secretReference(name))
}
spec.Volumes = append(spec.Volumes, nsp.volumes...)
@ -514,6 +522,7 @@ func stopPod(ctx context.Context, engine *kube, step *types.Step, deleteOpts met
if err != nil {
return err
}
log.Trace().Str("name", podName).Msg("deleting pod")
err = engine.client.CoreV1().Pods(engine.config.Namespace).Delete(ctx, podName, deleteOpts)

View file

@ -264,6 +264,9 @@ func TestFullPod(t *testing.T) {
},
{
"name": "another-pull-secret"
},
{
"name": "wp-01he8bebctabr3kgk0qj36d2me-0"
}
],
"tolerations": [
@ -317,6 +320,7 @@ func TestFullPod(t *testing.T) {
},
}
pod, err := mkPod(&types.Step{
UUID: "01he8bebctabr3kgk0qj36d2me-0",
Name: "go-test",
Image: "meltwater/drone-cache",
WorkingDir: "/woodpecker/src",
@ -328,6 +332,10 @@ func TestFullPod(t *testing.T) {
Environment: map[string]string{"CGO": "0"},
ExtraHosts: hostAliases,
Ports: ports,
AuthConfig: types.Auth{
Username: "foo",
Password: "bar",
},
}, &config{
Namespace: "woodpecker",
ImagePullSecretNames: []string{"regcred", "another-pull-secret"},

View file

@ -15,11 +15,21 @@
package kubernetes
import (
"context"
"encoding/json"
"fmt"
"strings"
"github.com/distribution/reference"
config_file "github.com/docker/cli/cli/config/configfile"
config_file_types "github.com/docker/cli/cli/config/types"
"github.com/rs/zerolog/log"
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors"
meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"go.woodpecker-ci.org/woodpecker/v2/pipeline/backend/types"
"go.woodpecker-ci.org/woodpecker/v2/pipeline/frontend/yaml/utils"
)
type nativeSecretsProcessor struct {
@ -189,3 +199,96 @@ func secretReference(name string) v1.LocalObjectReference {
Name: name,
}
}
func needsRegistrySecret(step *types.Step) bool {
return step.AuthConfig.Username != "" && step.AuthConfig.Password != ""
}
func mkRegistrySecret(step *types.Step, config *config) (*v1.Secret, error) {
name, err := registrySecretName(step)
if err != nil {
return nil, err
}
labels, err := registrySecretLabels(step)
if err != nil {
return nil, err
}
named, err := utils.ParseNamed(step.Image)
if err != nil {
return nil, err
}
authConfig := config_file.ConfigFile{
AuthConfigs: map[string]config_file_types.AuthConfig{
reference.Domain(named): {
Username: step.AuthConfig.Username,
Password: step.AuthConfig.Password,
},
},
}
configFileJSON, err := json.Marshal(authConfig)
if err != nil {
return nil, err
}
return &v1.Secret{
ObjectMeta: meta_v1.ObjectMeta{
Namespace: config.Namespace,
Name: name,
Labels: labels,
},
Type: v1.SecretTypeDockerConfigJson,
Data: map[string][]byte{
v1.DockerConfigJsonKey: configFileJSON,
},
}, nil
}
func registrySecretName(step *types.Step) (string, error) {
return podName(step)
}
func registrySecretLabels(step *types.Step) (map[string]string, error) {
var err error
labels := make(map[string]string)
if step.Type == types.StepTypeService {
labels[ServiceLabel], _ = serviceName(step)
}
labels[StepLabel], err = stepLabel(step)
if err != nil {
return labels, err
}
return labels, nil
}
func startRegistrySecret(ctx context.Context, engine *kube, step *types.Step) error {
secret, err := mkRegistrySecret(step, engine.config)
if err != nil {
return err
}
log.Trace().Msgf("creating secret: %s", secret.Name)
_, err = engine.client.CoreV1().Secrets(engine.config.Namespace).Create(ctx, secret, meta_v1.CreateOptions{})
if err != nil {
return err
}
return nil
}
func stopRegistrySecret(ctx context.Context, engine *kube, step *types.Step, deleteOpts meta_v1.DeleteOptions) error {
name, err := registrySecretName(step)
if err != nil {
return err
}
log.Trace().Str("name", name).Msg("deleting secret")
err = engine.client.CoreV1().Secrets(engine.config.Namespace).Delete(ctx, name, deleteOpts)
if errors.IsNotFound(err) {
return nil
}
return err
}

View file

@ -15,10 +15,14 @@
package kubernetes
import (
"encoding/json"
"testing"
"github.com/kinbiko/jsonassert"
"github.com/stretchr/testify/assert"
v1 "k8s.io/api/core/v1"
"go.woodpecker-ci.org/woodpecker/v2/pipeline/backend/types"
)
func TestNativeSecretsEnabled(t *testing.T) {
@ -178,3 +182,61 @@ func TestFileSecret(t *testing.T) {
},
}, nsp.mounts)
}
func TestNoAuthNoSecret(t *testing.T) {
assert.False(t, needsRegistrySecret(&types.Step{}))
}
func TestNoPasswordNoSecret(t *testing.T) {
assert.False(t, needsRegistrySecret(&types.Step{
AuthConfig: types.Auth{Username: "foo"},
}))
}
func TestNoUsernameNoSecret(t *testing.T) {
assert.False(t, needsRegistrySecret(&types.Step{
AuthConfig: types.Auth{Password: "foo"},
}))
}
func TestUsernameAndPasswordNeedsSecret(t *testing.T) {
assert.True(t, needsRegistrySecret(&types.Step{
AuthConfig: types.Auth{Username: "foo", Password: "bar"},
}))
}
func TestRegistrySecret(t *testing.T) {
const expected = `{
"metadata": {
"name": "wp-01he8bebctabr3kgk0qj36d2me-0",
"namespace": "woodpecker",
"creationTimestamp": null,
"labels": {
"step": "go-test"
}
},
"type": "kubernetes.io/dockerconfigjson",
"data": {
".dockerconfigjson": "eyJhdXRocyI6eyJkb2NrZXIuaW8iOnsidXNlcm5hbWUiOiJmb28iLCJwYXNzd29yZCI6ImJhciJ9fX0="
}
}`
secret, err := mkRegistrySecret(&types.Step{
UUID: "01he8bebctabr3kgk0qj36d2me-0",
Name: "go-test",
Image: "meltwater/drone-cache",
AuthConfig: types.Auth{
Username: "foo",
Password: "bar",
},
}, &config{
Namespace: "woodpecker",
})
assert.NoError(t, err)
secretJSON, err := json.Marshal(secret)
assert.NoError(t, err)
ja := jsonassert.New(t)
ja.Assertf(string(secretJSON), expected)
}

View file

@ -34,12 +34,6 @@ type Step struct {
Networks []Conn `json:"networks,omitempty"`
DNS []string `json:"dns,omitempty"`
DNSSearch []string `json:"dns_search,omitempty"`
MemSwapLimit int64 `json:"memswap_limit,omitempty"`
MemLimit int64 `json:"mem_limit,omitempty"`
ShmSize int64 `json:"shm_size,omitempty"`
CPUQuota int64 `json:"cpu_quota,omitempty"`
CPUShares int64 `json:"cpu_shares,omitempty"`
CPUSet string `json:"cpu_set,omitempty"`
OnFailure bool `json:"on_failure,omitempty"`
OnSuccess bool `json:"on_success,omitempty"`
Failure string `json:"failure,omitempty"`

View file

@ -46,7 +46,7 @@ type Secret struct {
func (s *Secret) Available(event string, container *yaml_types.Container) error {
onlyAllowSecretForPlugins := len(s.AllowedPlugins) > 0
if onlyAllowSecretForPlugins && !container.IsPlugin() {
return fmt.Errorf("secret %q only allowed to be used by plugins by step %q", s.Name, container.Name)
return fmt.Errorf("secret %q is only allowed to be used by plugins (a filter has been set on the secret). Note: Image filters do not work for normal steps", s.Name)
}
if onlyAllowSecretForPlugins && !utils.MatchImageDynamic(container.Image, s.AllowedPlugins...) {
@ -81,15 +81,6 @@ func (s *Secret) Match(event string) bool {
return false
}
type ResourceLimit struct {
MemSwapLimit int64
MemLimit int64
ShmSize int64
CPUQuota int64
CPUShares int64
CPUSet string
}
// Compiler compiles the yaml.
type Compiler struct {
local bool
@ -104,7 +95,6 @@ type Compiler struct {
metadata metadata.Metadata
registries []Registry
secrets map[string]Secret
reslimit ResourceLimit
defaultClonePlugin string
trustedClonePlugins []string
trustedPipeline bool

View file

@ -50,7 +50,7 @@ func TestSecretAvailable(t *testing.T) {
assert.ErrorContains(t, secret.Available("push", &yaml_types.Container{
Image: "golang",
Commands: yaml_base_types.StringOrSlice{"echo 'this is not a plugin'"},
}), "only allowed to be used by plugins by step")
}), "is only allowed to be used by plugins (a filter has been set on the secret). Note: Image filters do not work for normal steps")
assert.ErrorContains(t, secret.Available("push", &yaml_types.Container{
Image: "not-golang",
Commands: yaml_base_types.StringOrSlice{},

View file

@ -151,31 +151,6 @@ func (c *Compiler) createProcess(container *yaml_types.Container, stepType backe
}
}
memSwapLimit := int64(container.MemSwapLimit)
if c.reslimit.MemSwapLimit != 0 {
memSwapLimit = c.reslimit.MemSwapLimit
}
memLimit := int64(container.MemLimit)
if c.reslimit.MemLimit != 0 {
memLimit = c.reslimit.MemLimit
}
shmSize := int64(container.ShmSize)
if c.reslimit.ShmSize != 0 {
shmSize = c.reslimit.ShmSize
}
cpuQuota := int64(container.CPUQuota)
if c.reslimit.CPUQuota != 0 {
cpuQuota = c.reslimit.CPUQuota
}
cpuShares := int64(container.CPUShares)
if c.reslimit.CPUShares != 0 {
cpuShares = c.reslimit.CPUShares
}
cpuSet := container.CPUSet
if c.reslimit.CPUSet != "" {
cpuSet = c.reslimit.CPUSet
}
var ports []backend_types.Port
for _, portDef := range container.Ports {
port, err := convertPort(portDef)
@ -214,12 +189,6 @@ func (c *Compiler) createProcess(container *yaml_types.Container, stepType backe
Networks: networks,
DNS: container.DNS,
DNSSearch: container.DNSSearch,
MemSwapLimit: memSwapLimit,
MemLimit: memLimit,
ShmSize: shmSize,
CPUQuota: cpuQuota,
CPUShares: cpuShares,
CPUSet: cpuSet,
AuthConfig: authConfig,
OnSuccess: onSuccess,
OnFailure: onFailure,

View file

@ -157,21 +157,6 @@ func WithNetworks(networks ...string) Option {
}
}
// WithResourceLimit configures the compiler with default resource limits that
// are applied each container in the pipeline.
func WithResourceLimit(swap, mem, shmSize, cpuQuota, cpuShares int64, cpuSet string) Option {
return func(compiler *Compiler) {
compiler.reslimit = ResourceLimit{
MemSwapLimit: swap,
MemLimit: mem,
ShmSize: shmSize,
CPUQuota: cpuQuota,
CPUShares: cpuShares,
CPUSet: cpuSet,
}
}
}
func WithDefaultClonePlugin(cloneImage string) Option {
return func(compiler *Compiler) {
compiler.defaultClonePlugin = cloneImage

View file

@ -67,25 +67,6 @@ func TestWithNetworks(t *testing.T) {
assert.Equal(t, "overlay_bar", compiler.networks[1])
}
func TestWithResourceLimit(t *testing.T) {
compiler := New(
WithResourceLimit(
1,
2,
3,
4,
5,
"0,2-5",
),
)
assert.EqualValues(t, 1, compiler.reslimit.MemSwapLimit)
assert.EqualValues(t, 2, compiler.reslimit.MemLimit)
assert.EqualValues(t, 3, compiler.reslimit.ShmSize)
assert.EqualValues(t, 4, compiler.reslimit.CPUQuota)
assert.EqualValues(t, 5, compiler.reslimit.CPUShares)
assert.Equal(t, "0,2-5", compiler.reslimit.CPUSet)
}
func TestWithPrefix(t *testing.T) {
assert.Equal(t, "someprefix_", New(WithPrefix("someprefix_")).prefix)
}

View file

@ -207,9 +207,6 @@ func (l *Linter) lintTrusted(config *WorkflowConfig, c *types.Container, area st
if c.Privileged {
errors = append(errors, "Insufficient privileges to use privileged mode")
}
if c.ShmSize != 0 {
errors = append(errors, "Insufficient privileges to override shm_size")
}
if len(c.DNS) != 0 {
errors = append(errors, "Insufficient privileges to use custom dns")
}
@ -268,6 +265,21 @@ func (l *Linter) lintDeprecations(config *WorkflowConfig) (err error) {
return err
}
for _, container := range parsed.Steps.ContainerList {
if len(container.Secrets) > 0 {
err = multierr.Append(err, &errorTypes.PipelineError{
Type: errorTypes.PipelineErrorTypeDeprecation,
Message: "Secrets are deprecated, use environment with from_secret",
Data: errors.DeprecationErrorData{
File: config.File,
Field: fmt.Sprintf("steps.%s.secrets", container.Name),
Docs: "https://woodpecker-ci.org/docs/usage/secrets#usage",
},
IsWarning: true,
})
}
}
return nil
}

View file

@ -120,10 +120,6 @@ func TestLintErrors(t *testing.T) {
from: "steps: { build: { image: golang, privileged: true } }",
want: "Insufficient privileges to use privileged mode",
},
{
from: "steps: { build: { image: golang, shm_size: 10gb } }",
want: "Insufficient privileges to override shm_size",
},
{
from: "steps: { build: { image: golang, dns: [ 8.8.8.8 ] } }",
want: "Insufficient privileges to use custom dns",

View file

@ -47,25 +47,21 @@ type (
Ports []string `yaml:"ports,omitempty"`
DependsOn base.StringOrSlice `yaml:"depends_on,omitempty"`
Secrets []string `yaml:"secrets,omitempty"`
Environment map[string]any `yaml:"environment,omitempty"`
// Deprecated
Secrets []string `yaml:"secrets,omitempty"`
// Docker and Kubernetes Specific
Privileged bool `yaml:"privileged,omitempty"`
// Undocumented
CPUQuota base.StringOrInt `yaml:"cpu_quota,omitempty"`
CPUSet string `yaml:"cpuset,omitempty"`
CPUShares base.StringOrInt `yaml:"cpu_shares,omitempty"`
Devices []string `yaml:"devices,omitempty"`
DNSSearch base.StringOrSlice `yaml:"dns_search,omitempty"`
DNS base.StringOrSlice `yaml:"dns,omitempty"`
ExtraHosts []string `yaml:"extra_hosts,omitempty"`
MemLimit base.MemStringOrInt `yaml:"mem_limit,omitempty"`
MemSwapLimit base.MemStringOrInt `yaml:"memswap_limit,omitempty"`
NetworkMode string `yaml:"network_mode,omitempty"`
ShmSize base.MemStringOrInt `yaml:"shm_size,omitempty"`
Tmpfs []string `yaml:"tmpfs,omitempty"`
Devices []string `yaml:"devices,omitempty"`
DNSSearch base.StringOrSlice `yaml:"dns_search,omitempty"`
DNS base.StringOrSlice `yaml:"dns,omitempty"`
ExtraHosts []string `yaml:"extra_hosts,omitempty"`
NetworkMode string `yaml:"network_mode,omitempty"`
Tmpfs []string `yaml:"tmpfs,omitempty"`
}
)

View file

@ -30,9 +30,6 @@ image: golang:latest
commands:
- go build
- go test
cpu_quota: 11
cpuset: 1,2
cpu_shares: 99
detach: true
devices:
- /dev/ttyUSB0:/dev/ttyUSB0
@ -54,9 +51,6 @@ networks:
- other-network
pull: true
privileged: true
shm_size: 1kb
mem_limit: 1kb
memswap_limit: 1kb
volumes:
- /var/lib/mysql
- /opt/data:/var/lib/mysql
@ -78,27 +72,21 @@ ports:
func TestUnmarshalContainer(t *testing.T) {
want := Container{
Commands: base.StringOrSlice{"go build", "go test"},
CPUQuota: base.StringOrInt(11),
CPUSet: "1,2",
CPUShares: base.StringOrInt(99),
Detached: true,
Devices: []string{"/dev/ttyUSB0:/dev/ttyUSB0"},
Directory: "example/",
DNS: base.StringOrSlice{"8.8.8.8"},
DNSSearch: base.StringOrSlice{"example.com"},
Entrypoint: []string{"/bin/sh", "-c"},
Environment: map[string]any{"RACK_ENV": "development", "SHOW": true},
ExtraHosts: []string{"somehost:162.242.195.82", "otherhost:50.31.209.229", "ipv6:2001:db8::10"},
Image: "golang:latest",
MemLimit: base.MemStringOrInt(1024),
MemSwapLimit: base.MemStringOrInt(1024),
Name: "my-build-container",
NetworkMode: "bridge",
Pull: true,
Privileged: true,
ShmSize: base.MemStringOrInt(1024),
Tmpfs: base.StringOrSlice{"/var/lib/test"},
Commands: base.StringOrSlice{"go build", "go test"},
Detached: true,
Devices: []string{"/dev/ttyUSB0:/dev/ttyUSB0"},
Directory: "example/",
DNS: base.StringOrSlice{"8.8.8.8"},
DNSSearch: base.StringOrSlice{"example.com"},
Entrypoint: []string{"/bin/sh", "-c"},
Environment: map[string]any{"RACK_ENV": "development", "SHOW": true},
ExtraHosts: []string{"somehost:162.242.195.82", "otherhost:50.31.209.229", "ipv6:2001:db8::10"},
Image: "golang:latest",
Name: "my-build-container",
NetworkMode: "bridge",
Pull: true,
Privileged: true,
Tmpfs: base.StringOrSlice{"/var/lib/test"},
Volumes: Volumes{
Volumes: []*Volume{
{Source: "", Destination: "/var/lib/mysql"},

View file

@ -84,14 +84,19 @@ func imageHasTag(name string) bool {
return strings.Contains(name, ":")
}
// ParseNamed parses an image as a reference to validate it then parses it as a named reference.
func ParseNamed(image string) (reference.Named, error) {
ref, err := reference.ParseAnyReference(image)
if err != nil {
return nil, err
}
return reference.ParseNamed(ref.String())
}
// MatchHostname returns true if the image hostname
// matches the specified hostname.
func MatchHostname(image, hostname string) bool {
ref, err := reference.ParseAnyReference(image)
if err != nil {
return false
}
named, err := reference.ParseNamed(ref.String())
named, err := ParseNamed(image)
if err != nil {
return false
}

View file

@ -106,9 +106,9 @@ func (_m *Peer) Next(c context.Context, f rpc.Filter) (*rpc.Workflow, error) {
return r0, r1
}
// RegisterAgent provides a mock function with given fields: ctx, platform, backend, version, capacity
func (_m *Peer) RegisterAgent(ctx context.Context, platform string, backend string, version string, capacity int) (int64, error) {
ret := _m.Called(ctx, platform, backend, version, capacity)
// RegisterAgent provides a mock function with given fields: ctx, info
func (_m *Peer) RegisterAgent(ctx context.Context, info rpc.AgentInfo) (int64, error) {
ret := _m.Called(ctx, info)
if len(ret) == 0 {
panic("no return value specified for RegisterAgent")
@ -116,17 +116,17 @@ func (_m *Peer) RegisterAgent(ctx context.Context, platform string, backend stri
var r0 int64
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, string, string, string, int) (int64, error)); ok {
return rf(ctx, platform, backend, version, capacity)
if rf, ok := ret.Get(0).(func(context.Context, rpc.AgentInfo) (int64, error)); ok {
return rf(ctx, info)
}
if rf, ok := ret.Get(0).(func(context.Context, string, string, string, int) int64); ok {
r0 = rf(ctx, platform, backend, version, capacity)
if rf, ok := ret.Get(0).(func(context.Context, rpc.AgentInfo) int64); ok {
r0 = rf(ctx, info)
} else {
r0 = ret.Get(0).(int64)
}
if rf, ok := ret.Get(1).(func(context.Context, string, string, string, int) error); ok {
r1 = rf(ctx, platform, backend, version, capacity)
if rf, ok := ret.Get(1).(func(context.Context, rpc.AgentInfo) error); ok {
r1 = rf(ctx, info)
} else {
r1 = ret.Error(1)
}

View file

@ -55,6 +55,15 @@ type (
GrpcVersion int32 `json:"grpc_version,omitempty"`
ServerVersion string `json:"server_version,omitempty"`
}
// AgentInfo represents all the metadata that should be known about an agent.
AgentInfo struct {
Version string `json:"version"`
Platform string `json:"platform"`
Backend string `json:"backend"`
Capacity int `json:"capacity"`
CustomLabels map[string]string `json:"custom_labels"`
}
)
//go:generate mockery --name Peer --output mocks --case underscore --note "+build test"
@ -86,7 +95,7 @@ type Peer interface {
EnqueueLog(logEntry *LogEntry)
// RegisterAgent register our agent to the server
RegisterAgent(ctx context.Context, platform, backend, version string, capacity int) (int64, error)
RegisterAgent(ctx context.Context, info AgentInfo) (int64, error)
// UnregisterAgent unregister our agent from the server
UnregisterAgent(ctx context.Context) error

View file

@ -16,4 +16,4 @@ package proto
// Version is the version of the woodpecker.proto file,
// IMPORTANT: increased by 1 each time it get changed.
const Version int32 = 10
const Version int32 = 11

View file

@ -812,21 +812,97 @@ func (x *ReportHealthRequest) GetStatus() string {
return ""
}
type AgentInfo struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Platform string `protobuf:"bytes,1,opt,name=platform,proto3" json:"platform,omitempty"`
Capacity int32 `protobuf:"varint,2,opt,name=capacity,proto3" json:"capacity,omitempty"`
Backend string `protobuf:"bytes,3,opt,name=backend,proto3" json:"backend,omitempty"`
Version string `protobuf:"bytes,4,opt,name=version,proto3" json:"version,omitempty"`
CustomLabels map[string]string `protobuf:"bytes,5,rep,name=customLabels,proto3" json:"customLabels,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
}
func (x *AgentInfo) Reset() {
*x = AgentInfo{}
if protoimpl.UnsafeEnabled {
mi := &file_woodpecker_proto_msgTypes[14]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *AgentInfo) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*AgentInfo) ProtoMessage() {}
func (x *AgentInfo) ProtoReflect() protoreflect.Message {
mi := &file_woodpecker_proto_msgTypes[14]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use AgentInfo.ProtoReflect.Descriptor instead.
func (*AgentInfo) Descriptor() ([]byte, []int) {
return file_woodpecker_proto_rawDescGZIP(), []int{14}
}
func (x *AgentInfo) GetPlatform() string {
if x != nil {
return x.Platform
}
return ""
}
func (x *AgentInfo) GetCapacity() int32 {
if x != nil {
return x.Capacity
}
return 0
}
func (x *AgentInfo) GetBackend() string {
if x != nil {
return x.Backend
}
return ""
}
func (x *AgentInfo) GetVersion() string {
if x != nil {
return x.Version
}
return ""
}
func (x *AgentInfo) GetCustomLabels() map[string]string {
if x != nil {
return x.CustomLabels
}
return nil
}
type RegisterAgentRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Platform string `protobuf:"bytes,1,opt,name=platform,proto3" json:"platform,omitempty"`
Capacity int32 `protobuf:"varint,2,opt,name=capacity,proto3" json:"capacity,omitempty"`
Backend string `protobuf:"bytes,3,opt,name=backend,proto3" json:"backend,omitempty"`
Version string `protobuf:"bytes,4,opt,name=version,proto3" json:"version,omitempty"`
Info *AgentInfo `protobuf:"bytes,1,opt,name=info,proto3" json:"info,omitempty"`
}
func (x *RegisterAgentRequest) Reset() {
*x = RegisterAgentRequest{}
if protoimpl.UnsafeEnabled {
mi := &file_woodpecker_proto_msgTypes[14]
mi := &file_woodpecker_proto_msgTypes[15]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -839,7 +915,7 @@ func (x *RegisterAgentRequest) String() string {
func (*RegisterAgentRequest) ProtoMessage() {}
func (x *RegisterAgentRequest) ProtoReflect() protoreflect.Message {
mi := &file_woodpecker_proto_msgTypes[14]
mi := &file_woodpecker_proto_msgTypes[15]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -852,35 +928,14 @@ func (x *RegisterAgentRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use RegisterAgentRequest.ProtoReflect.Descriptor instead.
func (*RegisterAgentRequest) Descriptor() ([]byte, []int) {
return file_woodpecker_proto_rawDescGZIP(), []int{14}
return file_woodpecker_proto_rawDescGZIP(), []int{15}
}
func (x *RegisterAgentRequest) GetPlatform() string {
func (x *RegisterAgentRequest) GetInfo() *AgentInfo {
if x != nil {
return x.Platform
return x.Info
}
return ""
}
func (x *RegisterAgentRequest) GetCapacity() int32 {
if x != nil {
return x.Capacity
}
return 0
}
func (x *RegisterAgentRequest) GetBackend() string {
if x != nil {
return x.Backend
}
return ""
}
func (x *RegisterAgentRequest) GetVersion() string {
if x != nil {
return x.Version
}
return ""
return nil
}
type VersionResponse struct {
@ -895,7 +950,7 @@ type VersionResponse struct {
func (x *VersionResponse) Reset() {
*x = VersionResponse{}
if protoimpl.UnsafeEnabled {
mi := &file_woodpecker_proto_msgTypes[15]
mi := &file_woodpecker_proto_msgTypes[16]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -908,7 +963,7 @@ func (x *VersionResponse) String() string {
func (*VersionResponse) ProtoMessage() {}
func (x *VersionResponse) ProtoReflect() protoreflect.Message {
mi := &file_woodpecker_proto_msgTypes[15]
mi := &file_woodpecker_proto_msgTypes[16]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -921,7 +976,7 @@ func (x *VersionResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use VersionResponse.ProtoReflect.Descriptor instead.
func (*VersionResponse) Descriptor() ([]byte, []int) {
return file_woodpecker_proto_rawDescGZIP(), []int{15}
return file_woodpecker_proto_rawDescGZIP(), []int{16}
}
func (x *VersionResponse) GetGrpcVersion() int32 {
@ -949,7 +1004,7 @@ type NextResponse struct {
func (x *NextResponse) Reset() {
*x = NextResponse{}
if protoimpl.UnsafeEnabled {
mi := &file_woodpecker_proto_msgTypes[16]
mi := &file_woodpecker_proto_msgTypes[17]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -962,7 +1017,7 @@ func (x *NextResponse) String() string {
func (*NextResponse) ProtoMessage() {}
func (x *NextResponse) ProtoReflect() protoreflect.Message {
mi := &file_woodpecker_proto_msgTypes[16]
mi := &file_woodpecker_proto_msgTypes[17]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -975,7 +1030,7 @@ func (x *NextResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use NextResponse.ProtoReflect.Descriptor instead.
func (*NextResponse) Descriptor() ([]byte, []int) {
return file_woodpecker_proto_rawDescGZIP(), []int{16}
return file_woodpecker_proto_rawDescGZIP(), []int{17}
}
func (x *NextResponse) GetWorkflow() *Workflow {
@ -996,7 +1051,7 @@ type RegisterAgentResponse struct {
func (x *RegisterAgentResponse) Reset() {
*x = RegisterAgentResponse{}
if protoimpl.UnsafeEnabled {
mi := &file_woodpecker_proto_msgTypes[17]
mi := &file_woodpecker_proto_msgTypes[18]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -1009,7 +1064,7 @@ func (x *RegisterAgentResponse) String() string {
func (*RegisterAgentResponse) ProtoMessage() {}
func (x *RegisterAgentResponse) ProtoReflect() protoreflect.Message {
mi := &file_woodpecker_proto_msgTypes[17]
mi := &file_woodpecker_proto_msgTypes[18]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -1022,7 +1077,7 @@ func (x *RegisterAgentResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use RegisterAgentResponse.ProtoReflect.Descriptor instead.
func (*RegisterAgentResponse) Descriptor() ([]byte, []int) {
return file_woodpecker_proto_rawDescGZIP(), []int{17}
return file_woodpecker_proto_rawDescGZIP(), []int{18}
}
func (x *RegisterAgentResponse) GetAgentId() int64 {
@ -1044,7 +1099,7 @@ type AuthRequest struct {
func (x *AuthRequest) Reset() {
*x = AuthRequest{}
if protoimpl.UnsafeEnabled {
mi := &file_woodpecker_proto_msgTypes[18]
mi := &file_woodpecker_proto_msgTypes[19]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -1057,7 +1112,7 @@ func (x *AuthRequest) String() string {
func (*AuthRequest) ProtoMessage() {}
func (x *AuthRequest) ProtoReflect() protoreflect.Message {
mi := &file_woodpecker_proto_msgTypes[18]
mi := &file_woodpecker_proto_msgTypes[19]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -1070,7 +1125,7 @@ func (x *AuthRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use AuthRequest.ProtoReflect.Descriptor instead.
func (*AuthRequest) Descriptor() ([]byte, []int) {
return file_woodpecker_proto_rawDescGZIP(), []int{18}
return file_woodpecker_proto_rawDescGZIP(), []int{19}
}
func (x *AuthRequest) GetAgentToken() string {
@ -1100,7 +1155,7 @@ type AuthResponse struct {
func (x *AuthResponse) Reset() {
*x = AuthResponse{}
if protoimpl.UnsafeEnabled {
mi := &file_woodpecker_proto_msgTypes[19]
mi := &file_woodpecker_proto_msgTypes[20]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -1113,7 +1168,7 @@ func (x *AuthResponse) String() string {
func (*AuthResponse) ProtoMessage() {}
func (x *AuthResponse) ProtoReflect() protoreflect.Message {
mi := &file_woodpecker_proto_msgTypes[19]
mi := &file_woodpecker_proto_msgTypes[20]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -1126,7 +1181,7 @@ func (x *AuthResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use AuthResponse.ProtoReflect.Descriptor instead.
func (*AuthResponse) Descriptor() ([]byte, []int) {
return file_woodpecker_proto_rawDescGZIP(), []int{19}
return file_woodpecker_proto_rawDescGZIP(), []int{20}
}
func (x *AuthResponse) GetStatus() string {
@ -1220,83 +1275,95 @@ var file_woodpecker_proto_rawDesc = []byte{
0x22, 0x2d, 0x0a, 0x13, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68,
0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75,
0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22,
0x82, 0x01, 0x0a, 0x14, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x41, 0x67, 0x65, 0x6e,
0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x6c, 0x61, 0x74,
0x66, 0x6f, 0x72, 0x6d, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x6c, 0x61, 0x74,
0x66, 0x6f, 0x72, 0x6d, 0x12, 0x1a, 0x0a, 0x08, 0x63, 0x61, 0x70, 0x61, 0x63, 0x69, 0x74, 0x79,
0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x08, 0x63, 0x61, 0x70, 0x61, 0x63, 0x69, 0x74, 0x79,
0x12, 0x18, 0x0a, 0x07, 0x62, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28,
0x09, 0x52, 0x07, 0x62, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65,
0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x76, 0x65, 0x72,
0x73, 0x69, 0x6f, 0x6e, 0x22, 0x5b, 0x0a, 0x0f, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x52,
0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x21, 0x0a, 0x0c, 0x67, 0x72, 0x70, 0x63, 0x5f,
0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0b, 0x67,
0x72, 0x70, 0x63, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x25, 0x0a, 0x0e, 0x73, 0x65,
0x72, 0x76, 0x65, 0x72, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01,
0x28, 0x09, 0x52, 0x0d, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f,
0x6e, 0x22, 0x3b, 0x0a, 0x0c, 0x4e, 0x65, 0x78, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73,
0x65, 0x12, 0x2b, 0x0a, 0x08, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x18, 0x01, 0x20,
0x01, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x57, 0x6f, 0x72, 0x6b,
0x66, 0x6c, 0x6f, 0x77, 0x52, 0x08, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x22, 0x32,
0x0a, 0x15, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x52,
0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x19, 0x0a, 0x08, 0x61, 0x67, 0x65, 0x6e, 0x74,
0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x07, 0x61, 0x67, 0x65, 0x6e, 0x74,
0x49, 0x64, 0x22, 0x49, 0x0a, 0x0b, 0x41, 0x75, 0x74, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
0x74, 0x12, 0x1f, 0x0a, 0x0b, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e,
0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x54, 0x6f, 0x6b,
0x65, 0x6e, 0x12, 0x19, 0x0a, 0x08, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x02,
0x20, 0x01, 0x28, 0x03, 0x52, 0x07, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x49, 0x64, 0x22, 0x64, 0x0a,
0x0c, 0x41, 0x75, 0x74, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x16, 0x0a,
0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73,
0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x19, 0x0a, 0x08, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x5f, 0x69,
0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x07, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x49, 0x64,
0x12, 0x21, 0x0a, 0x0c, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e,
0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x54, 0x6f,
0x6b, 0x65, 0x6e, 0x32, 0xbb, 0x04, 0x0a, 0x0a, 0x57, 0x6f, 0x6f, 0x64, 0x70, 0x65, 0x63, 0x6b,
0x65, 0x72, 0x12, 0x31, 0x0a, 0x07, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x0c, 0x2e,
0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x16, 0x2e, 0x70, 0x72,
0x6f, 0x74, 0x6f, 0x2e, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f,
0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x31, 0x0a, 0x04, 0x4e, 0x65, 0x78, 0x74, 0x12, 0x12, 0x2e,
0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x4e, 0x65, 0x78, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
0x74, 0x1a, 0x13, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x4e, 0x65, 0x78, 0x74, 0x52, 0x65,
0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x2a, 0x0a, 0x04, 0x49, 0x6e, 0x69, 0x74,
0x12, 0x12, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x49, 0x6e, 0x69, 0x74, 0x52, 0x65, 0x71,
0x75, 0x65, 0x73, 0x74, 0x1a, 0x0c, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x45, 0x6d, 0x70,
0x74, 0x79, 0x22, 0x00, 0x12, 0x2a, 0x0a, 0x04, 0x57, 0x61, 0x69, 0x74, 0x12, 0x12, 0x2e, 0x70,
0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x57, 0x61, 0x69, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74,
0x80, 0x02, 0x0a, 0x09, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x1a, 0x0a,
0x08, 0x70, 0x6c, 0x61, 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52,
0x08, 0x70, 0x6c, 0x61, 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x12, 0x1a, 0x0a, 0x08, 0x63, 0x61, 0x70,
0x61, 0x63, 0x69, 0x74, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x08, 0x63, 0x61, 0x70,
0x61, 0x63, 0x69, 0x74, 0x79, 0x12, 0x18, 0x0a, 0x07, 0x62, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64,
0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x62, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x12,
0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09,
0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x46, 0x0a, 0x0c, 0x63, 0x75, 0x73,
0x74, 0x6f, 0x6d, 0x4c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32,
0x22, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x49, 0x6e, 0x66,
0x6f, 0x2e, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x4c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x45, 0x6e,
0x74, 0x72, 0x79, 0x52, 0x0c, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x4c, 0x61, 0x62, 0x65, 0x6c,
0x73, 0x1a, 0x3f, 0x0a, 0x11, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x4c, 0x61, 0x62, 0x65, 0x6c,
0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20,
0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75,
0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02,
0x38, 0x01, 0x22, 0x3c, 0x0a, 0x14, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x41, 0x67,
0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x24, 0x0a, 0x04, 0x69, 0x6e,
0x66, 0x6f, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f,
0x2e, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x04, 0x69, 0x6e, 0x66, 0x6f,
0x22, 0x5b, 0x0a, 0x0f, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f,
0x6e, 0x73, 0x65, 0x12, 0x21, 0x0a, 0x0c, 0x67, 0x72, 0x70, 0x63, 0x5f, 0x76, 0x65, 0x72, 0x73,
0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0b, 0x67, 0x72, 0x70, 0x63, 0x56,
0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x25, 0x0a, 0x0e, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72,
0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d,
0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x3b, 0x0a,
0x0c, 0x4e, 0x65, 0x78, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2b, 0x0a,
0x08, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32,
0x0f, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x57, 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77,
0x52, 0x08, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x22, 0x32, 0x0a, 0x15, 0x52, 0x65,
0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f,
0x6e, 0x73, 0x65, 0x12, 0x19, 0x0a, 0x08, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18,
0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x07, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x49, 0x64, 0x22, 0x49,
0x0a, 0x0b, 0x41, 0x75, 0x74, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1f, 0x0a,
0x0b, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x01, 0x20, 0x01,
0x28, 0x09, 0x52, 0x0a, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x19,
0x0a, 0x08, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03,
0x52, 0x07, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x49, 0x64, 0x22, 0x64, 0x0a, 0x0c, 0x41, 0x75, 0x74,
0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, 0x61,
0x74, 0x75, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75,
0x73, 0x12, 0x19, 0x0a, 0x08, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20,
0x01, 0x28, 0x03, 0x52, 0x07, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x49, 0x64, 0x12, 0x21, 0x0a, 0x0c,
0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x03, 0x20, 0x01,
0x28, 0x09, 0x52, 0x0b, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x32,
0xbb, 0x04, 0x0a, 0x0a, 0x57, 0x6f, 0x6f, 0x64, 0x70, 0x65, 0x63, 0x6b, 0x65, 0x72, 0x12, 0x31,
0x0a, 0x07, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x0c, 0x2e, 0x70, 0x72, 0x6f, 0x74,
0x6f, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x16, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e,
0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22,
0x00, 0x12, 0x31, 0x0a, 0x04, 0x4e, 0x65, 0x78, 0x74, 0x12, 0x12, 0x2e, 0x70, 0x72, 0x6f, 0x74,
0x6f, 0x2e, 0x4e, 0x65, 0x78, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x13, 0x2e,
0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x4e, 0x65, 0x78, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
0x73, 0x65, 0x22, 0x00, 0x12, 0x2a, 0x0a, 0x04, 0x49, 0x6e, 0x69, 0x74, 0x12, 0x12, 0x2e, 0x70,
0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x49, 0x6e, 0x69, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74,
0x1a, 0x0c, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00,
0x12, 0x2a, 0x0a, 0x04, 0x44, 0x6f, 0x6e, 0x65, 0x12, 0x12, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f,
0x2e, 0x44, 0x6f, 0x6e, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x0c, 0x2e, 0x70,
0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x12, 0x2e, 0x0a, 0x06,
0x45, 0x78, 0x74, 0x65, 0x6e, 0x64, 0x12, 0x14, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x45,
0x78, 0x74, 0x65, 0x6e, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x0c, 0x2e, 0x70,
0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x12, 0x2e, 0x0a, 0x06,
0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x12, 0x14, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x55,
0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x0c, 0x2e, 0x70,
0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x12, 0x28, 0x0a, 0x03,
0x4c, 0x6f, 0x67, 0x12, 0x11, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x4c, 0x6f, 0x67, 0x52,
0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x0c, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x45,
0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x12, 0x4c, 0x0a, 0x0d, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74,
0x65, 0x72, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x12, 0x1b, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e,
0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71,
0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x52, 0x65, 0x67,
0x69, 0x73, 0x74, 0x65, 0x72, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
0x73, 0x65, 0x22, 0x00, 0x12, 0x2f, 0x0a, 0x0f, 0x55, 0x6e, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74,
0x65, 0x72, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x12, 0x0c, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e,
0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x0c, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x45, 0x6d,
0x70, 0x74, 0x79, 0x22, 0x00, 0x12, 0x3a, 0x0a, 0x0c, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x48,
0x65, 0x61, 0x6c, 0x74, 0x68, 0x12, 0x1a, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x52, 0x65,
0x70, 0x6f, 0x72, 0x74, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
0x74, 0x1a, 0x0c, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22,
0x00, 0x32, 0x43, 0x0a, 0x0e, 0x57, 0x6f, 0x6f, 0x64, 0x70, 0x65, 0x63, 0x6b, 0x65, 0x72, 0x41,
0x75, 0x74, 0x68, 0x12, 0x31, 0x0a, 0x04, 0x41, 0x75, 0x74, 0x68, 0x12, 0x12, 0x2e, 0x70, 0x72,
0x6f, 0x74, 0x6f, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a,
0x13, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x52, 0x65, 0x73, 0x70,
0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x42, 0x37, 0x5a, 0x35, 0x67, 0x6f, 0x2e, 0x77, 0x6f, 0x6f,
0x64, 0x70, 0x65, 0x63, 0x6b, 0x65, 0x72, 0x2d, 0x63, 0x69, 0x2e, 0x6f, 0x72, 0x67, 0x2f, 0x77,
0x6f, 0x6f, 0x64, 0x70, 0x65, 0x63, 0x6b, 0x65, 0x72, 0x2f, 0x76, 0x32, 0x2f, 0x70, 0x69, 0x70,
0x65, 0x6c, 0x69, 0x6e, 0x65, 0x2f, 0x72, 0x70, 0x63, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62,
0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
0x12, 0x2a, 0x0a, 0x04, 0x57, 0x61, 0x69, 0x74, 0x12, 0x12, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f,
0x2e, 0x57, 0x61, 0x69, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x0c, 0x2e, 0x70,
0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x12, 0x2a, 0x0a, 0x04,
0x44, 0x6f, 0x6e, 0x65, 0x12, 0x12, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x44, 0x6f, 0x6e,
0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x0c, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f,
0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x12, 0x2e, 0x0a, 0x06, 0x45, 0x78, 0x74, 0x65,
0x6e, 0x64, 0x12, 0x14, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x45, 0x78, 0x74, 0x65, 0x6e,
0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x0c, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f,
0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x12, 0x2e, 0x0a, 0x06, 0x55, 0x70, 0x64, 0x61,
0x74, 0x65, 0x12, 0x14, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74,
0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x0c, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f,
0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x12, 0x28, 0x0a, 0x03, 0x4c, 0x6f, 0x67, 0x12,
0x11, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x4c, 0x6f, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65,
0x73, 0x74, 0x1a, 0x0c, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79,
0x22, 0x00, 0x12, 0x4c, 0x0a, 0x0d, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x41, 0x67,
0x65, 0x6e, 0x74, 0x12, 0x1b, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x52, 0x65, 0x67, 0x69,
0x73, 0x74, 0x65, 0x72, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74,
0x1a, 0x1c, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65,
0x72, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00,
0x12, 0x2f, 0x0a, 0x0f, 0x55, 0x6e, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x41, 0x67,
0x65, 0x6e, 0x74, 0x12, 0x0c, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x45, 0x6d, 0x70, 0x74,
0x79, 0x1a, 0x0c, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22,
0x00, 0x12, 0x3a, 0x0a, 0x0c, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x48, 0x65, 0x61, 0x6c, 0x74,
0x68, 0x12, 0x1a, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74,
0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x0c, 0x2e,
0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x32, 0x43, 0x0a,
0x0e, 0x57, 0x6f, 0x6f, 0x64, 0x70, 0x65, 0x63, 0x6b, 0x65, 0x72, 0x41, 0x75, 0x74, 0x68, 0x12,
0x31, 0x0a, 0x04, 0x41, 0x75, 0x74, 0x68, 0x12, 0x12, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e,
0x41, 0x75, 0x74, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x13, 0x2e, 0x70, 0x72,
0x6f, 0x74, 0x6f, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65,
0x22, 0x00, 0x42, 0x37, 0x5a, 0x35, 0x67, 0x6f, 0x2e, 0x77, 0x6f, 0x6f, 0x64, 0x70, 0x65, 0x63,
0x6b, 0x65, 0x72, 0x2d, 0x63, 0x69, 0x2e, 0x6f, 0x72, 0x67, 0x2f, 0x77, 0x6f, 0x6f, 0x64, 0x70,
0x65, 0x63, 0x6b, 0x65, 0x72, 0x2f, 0x76, 0x32, 0x2f, 0x70, 0x69, 0x70, 0x65, 0x6c, 0x69, 0x6e,
0x65, 0x2f, 0x72, 0x70, 0x63, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f,
0x74, 0x6f, 0x33,
}
var (
@ -1311,7 +1378,7 @@ func file_woodpecker_proto_rawDescGZIP() []byte {
return file_woodpecker_proto_rawDescData
}
var file_woodpecker_proto_msgTypes = make([]protoimpl.MessageInfo, 21)
var file_woodpecker_proto_msgTypes = make([]protoimpl.MessageInfo, 23)
var file_woodpecker_proto_goTypes = []interface{}{
(*StepState)(nil), // 0: proto.StepState
(*WorkflowState)(nil), // 1: proto.WorkflowState
@ -1327,51 +1394,55 @@ var file_woodpecker_proto_goTypes = []interface{}{
(*LogRequest)(nil), // 11: proto.LogRequest
(*Empty)(nil), // 12: proto.Empty
(*ReportHealthRequest)(nil), // 13: proto.ReportHealthRequest
(*RegisterAgentRequest)(nil), // 14: proto.RegisterAgentRequest
(*VersionResponse)(nil), // 15: proto.VersionResponse
(*NextResponse)(nil), // 16: proto.NextResponse
(*RegisterAgentResponse)(nil), // 17: proto.RegisterAgentResponse
(*AuthRequest)(nil), // 18: proto.AuthRequest
(*AuthResponse)(nil), // 19: proto.AuthResponse
nil, // 20: proto.Filter.LabelsEntry
(*AgentInfo)(nil), // 14: proto.AgentInfo
(*RegisterAgentRequest)(nil), // 15: proto.RegisterAgentRequest
(*VersionResponse)(nil), // 16: proto.VersionResponse
(*NextResponse)(nil), // 17: proto.NextResponse
(*RegisterAgentResponse)(nil), // 18: proto.RegisterAgentResponse
(*AuthRequest)(nil), // 19: proto.AuthRequest
(*AuthResponse)(nil), // 20: proto.AuthResponse
nil, // 21: proto.Filter.LabelsEntry
nil, // 22: proto.AgentInfo.CustomLabelsEntry
}
var file_woodpecker_proto_depIdxs = []int32{
20, // 0: proto.Filter.labels:type_name -> proto.Filter.LabelsEntry
21, // 0: proto.Filter.labels:type_name -> proto.Filter.LabelsEntry
3, // 1: proto.NextRequest.filter:type_name -> proto.Filter
1, // 2: proto.InitRequest.state:type_name -> proto.WorkflowState
1, // 3: proto.DoneRequest.state:type_name -> proto.WorkflowState
0, // 4: proto.UpdateRequest.state:type_name -> proto.StepState
2, // 5: proto.LogRequest.logEntries:type_name -> proto.LogEntry
4, // 6: proto.NextResponse.workflow:type_name -> proto.Workflow
12, // 7: proto.Woodpecker.Version:input_type -> proto.Empty
5, // 8: proto.Woodpecker.Next:input_type -> proto.NextRequest
6, // 9: proto.Woodpecker.Init:input_type -> proto.InitRequest
7, // 10: proto.Woodpecker.Wait:input_type -> proto.WaitRequest
8, // 11: proto.Woodpecker.Done:input_type -> proto.DoneRequest
9, // 12: proto.Woodpecker.Extend:input_type -> proto.ExtendRequest
10, // 13: proto.Woodpecker.Update:input_type -> proto.UpdateRequest
11, // 14: proto.Woodpecker.Log:input_type -> proto.LogRequest
14, // 15: proto.Woodpecker.RegisterAgent:input_type -> proto.RegisterAgentRequest
12, // 16: proto.Woodpecker.UnregisterAgent:input_type -> proto.Empty
13, // 17: proto.Woodpecker.ReportHealth:input_type -> proto.ReportHealthRequest
18, // 18: proto.WoodpeckerAuth.Auth:input_type -> proto.AuthRequest
15, // 19: proto.Woodpecker.Version:output_type -> proto.VersionResponse
16, // 20: proto.Woodpecker.Next:output_type -> proto.NextResponse
12, // 21: proto.Woodpecker.Init:output_type -> proto.Empty
12, // 22: proto.Woodpecker.Wait:output_type -> proto.Empty
12, // 23: proto.Woodpecker.Done:output_type -> proto.Empty
12, // 24: proto.Woodpecker.Extend:output_type -> proto.Empty
12, // 25: proto.Woodpecker.Update:output_type -> proto.Empty
12, // 26: proto.Woodpecker.Log:output_type -> proto.Empty
17, // 27: proto.Woodpecker.RegisterAgent:output_type -> proto.RegisterAgentResponse
12, // 28: proto.Woodpecker.UnregisterAgent:output_type -> proto.Empty
12, // 29: proto.Woodpecker.ReportHealth:output_type -> proto.Empty
19, // 30: proto.WoodpeckerAuth.Auth:output_type -> proto.AuthResponse
19, // [19:31] is the sub-list for method output_type
7, // [7:19] is the sub-list for method input_type
7, // [7:7] is the sub-list for extension type_name
7, // [7:7] is the sub-list for extension extendee
0, // [0:7] is the sub-list for field type_name
22, // 6: proto.AgentInfo.customLabels:type_name -> proto.AgentInfo.CustomLabelsEntry
14, // 7: proto.RegisterAgentRequest.info:type_name -> proto.AgentInfo
4, // 8: proto.NextResponse.workflow:type_name -> proto.Workflow
12, // 9: proto.Woodpecker.Version:input_type -> proto.Empty
5, // 10: proto.Woodpecker.Next:input_type -> proto.NextRequest
6, // 11: proto.Woodpecker.Init:input_type -> proto.InitRequest
7, // 12: proto.Woodpecker.Wait:input_type -> proto.WaitRequest
8, // 13: proto.Woodpecker.Done:input_type -> proto.DoneRequest
9, // 14: proto.Woodpecker.Extend:input_type -> proto.ExtendRequest
10, // 15: proto.Woodpecker.Update:input_type -> proto.UpdateRequest
11, // 16: proto.Woodpecker.Log:input_type -> proto.LogRequest
15, // 17: proto.Woodpecker.RegisterAgent:input_type -> proto.RegisterAgentRequest
12, // 18: proto.Woodpecker.UnregisterAgent:input_type -> proto.Empty
13, // 19: proto.Woodpecker.ReportHealth:input_type -> proto.ReportHealthRequest
19, // 20: proto.WoodpeckerAuth.Auth:input_type -> proto.AuthRequest
16, // 21: proto.Woodpecker.Version:output_type -> proto.VersionResponse
17, // 22: proto.Woodpecker.Next:output_type -> proto.NextResponse
12, // 23: proto.Woodpecker.Init:output_type -> proto.Empty
12, // 24: proto.Woodpecker.Wait:output_type -> proto.Empty
12, // 25: proto.Woodpecker.Done:output_type -> proto.Empty
12, // 26: proto.Woodpecker.Extend:output_type -> proto.Empty
12, // 27: proto.Woodpecker.Update:output_type -> proto.Empty
12, // 28: proto.Woodpecker.Log:output_type -> proto.Empty
18, // 29: proto.Woodpecker.RegisterAgent:output_type -> proto.RegisterAgentResponse
12, // 30: proto.Woodpecker.UnregisterAgent:output_type -> proto.Empty
12, // 31: proto.Woodpecker.ReportHealth:output_type -> proto.Empty
20, // 32: proto.WoodpeckerAuth.Auth:output_type -> proto.AuthResponse
21, // [21:33] is the sub-list for method output_type
9, // [9:21] is the sub-list for method input_type
9, // [9:9] is the sub-list for extension type_name
9, // [9:9] is the sub-list for extension extendee
0, // [0:9] is the sub-list for field type_name
}
func init() { file_woodpecker_proto_init() }
@ -1549,7 +1620,7 @@ func file_woodpecker_proto_init() {
}
}
file_woodpecker_proto_msgTypes[14].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*RegisterAgentRequest); i {
switch v := v.(*AgentInfo); i {
case 0:
return &v.state
case 1:
@ -1561,7 +1632,7 @@ func file_woodpecker_proto_init() {
}
}
file_woodpecker_proto_msgTypes[15].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*VersionResponse); i {
switch v := v.(*RegisterAgentRequest); i {
case 0:
return &v.state
case 1:
@ -1573,7 +1644,7 @@ func file_woodpecker_proto_init() {
}
}
file_woodpecker_proto_msgTypes[16].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*NextResponse); i {
switch v := v.(*VersionResponse); i {
case 0:
return &v.state
case 1:
@ -1585,7 +1656,7 @@ func file_woodpecker_proto_init() {
}
}
file_woodpecker_proto_msgTypes[17].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*RegisterAgentResponse); i {
switch v := v.(*NextResponse); i {
case 0:
return &v.state
case 1:
@ -1597,7 +1668,7 @@ func file_woodpecker_proto_init() {
}
}
file_woodpecker_proto_msgTypes[18].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*AuthRequest); i {
switch v := v.(*RegisterAgentResponse); i {
case 0:
return &v.state
case 1:
@ -1609,6 +1680,18 @@ func file_woodpecker_proto_init() {
}
}
file_woodpecker_proto_msgTypes[19].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*AuthRequest); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_woodpecker_proto_msgTypes[20].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*AuthResponse); i {
case 0:
return &v.state
@ -1627,7 +1710,7 @@ func file_woodpecker_proto_init() {
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_woodpecker_proto_rawDesc,
NumEnums: 0,
NumMessages: 21,
NumMessages: 23,
NumExtensions: 0,
NumServices: 2,
},

View file

@ -116,11 +116,16 @@ message ReportHealthRequest {
string status = 1;
}
message RegisterAgentRequest {
message AgentInfo {
string platform = 1;
int32 capacity = 2;
string backend = 3;
string version = 4;
map<string, string> customLabels = 5;
}
message RegisterAgentRequest {
AgentInfo info = 1;
}
//

View file

@ -15,12 +15,10 @@
package api
import (
"encoding/base32"
"net/http"
"strconv"
"github.com/gin-gonic/gin"
"github.com/gorilla/securecookie"
"go.woodpecker-ci.org/woodpecker/v2/server"
"go.woodpecker-ci.org/woodpecker/v2/server/model"
@ -28,6 +26,10 @@ import (
"go.woodpecker-ci.org/woodpecker/v2/server/store"
)
//
// Global Agents.
//
// GetAgents
//
// @Summary List agents
@ -50,14 +52,14 @@ func GetAgents(c *gin.Context) {
// GetAgent
//
// @Summary Get an agent
// @Router /agents/{agent} [get]
// @Router /agents/{agent_id} [get]
// @Produce json
// @Success 200 {object} Agent
// @Tags Agents
// @Param Authorization header string true "Insert your personal access token" default(Bearer <personal access token>)
// @Param agent path int true "the agent's id"
func GetAgent(c *gin.Context) {
agentID, err := strconv.ParseInt(c.Param("agent"), 10, 64)
agentID, err := strconv.ParseInt(c.Param("agent_id"), 10, 64)
if err != nil {
_ = c.AbortWithError(http.StatusBadRequest, err)
return
@ -74,14 +76,14 @@ func GetAgent(c *gin.Context) {
// GetAgentTasks
//
// @Summary List agent tasks
// @Router /agents/{agent}/tasks [get]
// @Router /agents/{agent_id}/tasks [get]
// @Produce json
// @Success 200 {array} Task
// @Tags Agents
// @Param Authorization header string true "Insert your personal access token" default(Bearer <personal access token>)
// @Param agent path int true "the agent's id"
func GetAgentTasks(c *gin.Context) {
agentID, err := strconv.ParseInt(c.Param("agent"), 10, 64)
agentID, err := strconv.ParseInt(c.Param("agent_id"), 10, 64)
if err != nil {
_ = c.AbortWithError(http.StatusBadRequest, err)
return
@ -107,7 +109,7 @@ func GetAgentTasks(c *gin.Context) {
// PatchAgent
//
// @Summary Update an agent
// @Router /agents/{agent} [patch]
// @Router /agents/{agent_id} [patch]
// @Produce json
// @Success 200 {object} Agent
// @Tags Agents
@ -124,7 +126,7 @@ func PatchAgent(c *gin.Context) {
return
}
agentID, err := strconv.ParseInt(c.Param("agent"), 10, 64)
agentID, err := strconv.ParseInt(c.Param("agent_id"), 10, 64)
if err != nil {
_ = c.AbortWithError(http.StatusBadRequest, err)
return
@ -135,6 +137,8 @@ func PatchAgent(c *gin.Context) {
handleDBError(c, err)
return
}
// Update allowed fields
agent.Name = in.Name
agent.NoSchedule = in.NoSchedule
if agent.NoSchedule {
@ -172,11 +176,10 @@ func PostAgent(c *gin.Context) {
agent := &model.Agent{
Name: in.Name,
NoSchedule: in.NoSchedule,
OwnerID: user.ID,
Token: base32.StdEncoding.EncodeToString(
securecookie.GenerateRandomKey(32),
),
OrgID: model.IDNotSet,
NoSchedule: in.NoSchedule,
Token: model.GenerateNewAgentToken(),
}
if err = store.FromContext(c).AgentCreate(agent); err != nil {
c.String(http.StatusInternalServerError, err.Error())
@ -188,7 +191,7 @@ func PostAgent(c *gin.Context) {
// DeleteAgent
//
// @Summary Delete an agent
// @Router /agents/{agent} [delete]
// @Router /agents/{agent_id} [delete]
// @Produce plain
// @Success 200
// @Tags Agents
@ -197,7 +200,7 @@ func PostAgent(c *gin.Context) {
func DeleteAgent(c *gin.Context) {
_store := store.FromContext(c)
agentID, err := strconv.ParseInt(c.Param("agent"), 10, 64)
agentID, err := strconv.ParseInt(c.Param("agent_id"), 10, 64)
if err != nil {
_ = c.AbortWithError(http.StatusBadRequest, err)
return
@ -227,3 +230,179 @@ func DeleteAgent(c *gin.Context) {
}
c.Status(http.StatusNoContent)
}
//
// Org/User Agents.
//
// PostOrgAgent
//
// @Summary Create a new organization-scoped agent
// @Description Creates a new agent with a random token, scoped to the specified organization
// @Router /orgs/{org_id}/agents [post]
// @Produce json
// @Success 200 {object} Agent
// @Tags Agents
// @Param Authorization header string true "Insert your personal access token" default(Bearer <personal access token>)
// @Param org_id path int true "the organization's id"
// @Param agent body Agent true "the agent's data (only 'name' and 'no_schedule' are read)"
func PostOrgAgent(c *gin.Context) {
_store := store.FromContext(c)
user := session.User(c)
orgID, err := strconv.ParseInt(c.Param("org_id"), 10, 64)
if err != nil {
c.String(http.StatusBadRequest, "Invalid organization ID")
return
}
in := new(model.Agent)
err = c.Bind(in)
if err != nil {
c.String(http.StatusBadRequest, err.Error())
return
}
agent := &model.Agent{
Name: in.Name,
OwnerID: user.ID,
OrgID: orgID,
NoSchedule: in.NoSchedule,
Token: model.GenerateNewAgentToken(),
}
if err = _store.AgentCreate(agent); err != nil {
c.String(http.StatusInternalServerError, err.Error())
return
}
c.JSON(http.StatusOK, agent)
}
// GetOrgAgents
//
// @Summary List agents for an organization
// @Router /orgs/{org_id}/agents [get]
// @Produce json
// @Success 200 {array} Agent
// @Tags Agents
// @Param Authorization header string true "Insert your personal access token" default(Bearer <personal access token>)
// @Param org_id path int true "the organization's id"
// @Param page query int false "for response pagination, page offset number" default(1)
// @Param perPage query int false "for response pagination, max items per page" default(50)
func GetOrgAgents(c *gin.Context) {
_store := store.FromContext(c)
org := session.Org(c)
agents, err := _store.AgentListForOrg(org.ID, session.Pagination(c))
if err != nil {
c.String(http.StatusInternalServerError, "Error getting agent list. %s", err)
return
}
c.JSON(http.StatusOK, agents)
}
// PatchOrgAgent
//
// @Summary Update an organization-scoped agent
// @Router /orgs/{org_id}/agents/{agent_id} [patch]
// @Produce json
// @Success 200 {object} Agent
// @Tags Agents
// @Param Authorization header string true "Insert your personal access token" default(Bearer <personal access token>)
// @Param org_id path int true "the organization's id"
// @Param agent_id path int true "the agent's id"
// @Param agent body Agent true "the agent's updated data"
func PatchOrgAgent(c *gin.Context) {
_store := store.FromContext(c)
org := session.Org(c)
agentID, err := strconv.ParseInt(c.Param("agent_id"), 10, 64)
if err != nil {
c.String(http.StatusBadRequest, "Invalid agent ID")
return
}
agent, err := _store.AgentFind(agentID)
if err != nil {
c.String(http.StatusNotFound, "Agent not found")
return
}
if agent.OrgID != org.ID {
c.String(http.StatusBadRequest, "Agent does not belong to this organization")
return
}
in := new(model.Agent)
if err := c.Bind(in); err != nil {
c.String(http.StatusBadRequest, err.Error())
return
}
// Update allowed fields
agent.Name = in.Name
agent.NoSchedule = in.NoSchedule
if agent.NoSchedule {
server.Config.Services.Queue.KickAgentWorkers(agent.ID)
}
if err := _store.AgentUpdate(agent); err != nil {
c.String(http.StatusInternalServerError, err.Error())
return
}
c.JSON(http.StatusOK, agent)
}
// DeleteOrgAgent
//
// @Summary Delete an organization-scoped agent
// @Router /orgs/{org_id}/agents/{agent_id} [delete]
// @Produce plain
// @Success 204
// @Tags Agents
// @Param Authorization header string true "Insert your personal access token" default(Bearer <personal access token>)
// @Param org_id path int true "the organization's id"
// @Param agent_id path int true "the agent's id"
func DeleteOrgAgent(c *gin.Context) {
_store := store.FromContext(c)
org := session.Org(c)
agentID, err := strconv.ParseInt(c.Param("agent_id"), 10, 64)
if err != nil {
c.String(http.StatusBadRequest, "Invalid agent ID")
return
}
agent, err := _store.AgentFind(agentID)
if err != nil {
c.String(http.StatusNotFound, "Agent not found")
return
}
if agent.OrgID != org.ID {
c.String(http.StatusBadRequest, "Agent does not belong to this organization")
return
}
// Check if the agent has any running tasks
info := server.Config.Services.Queue.Info(c)
for _, task := range info.Running {
if task.AgentID == agent.ID {
c.String(http.StatusConflict, "Agent has running tasks")
return
}
}
// Kick workers to remove the agent from the queue
server.Config.Services.Queue.KickAgentWorkers(agent.ID)
if err := _store.AgentDelete(agent); err != nil {
c.String(http.StatusInternalServerError, "Error deleting agent. %s", err)
return
}
c.Status(http.StatusNoContent)
}

View file

@ -16,7 +16,6 @@ package api
import (
"net/http"
"strconv"
"strings"
"github.com/gin-gonic/gin"
@ -58,20 +57,7 @@ func GetOrgs(c *gin.Context) {
// @Param Authorization header string true "Insert your personal access token" default(Bearer <personal access token>)
// @Param org_id path string true "the organization's id"
func GetOrg(c *gin.Context) {
_store := store.FromContext(c)
orgID, err := strconv.ParseInt(c.Param("org_id"), 10, 64)
if err != nil {
c.String(http.StatusBadRequest, "Error parsing org id. %s", err)
return
}
org, err := _store.OrgGet(orgID)
if err != nil {
handleDBError(c, err)
return
}
org := session.Org(c)
c.JSON(http.StatusOK, org)
}
@ -86,7 +72,7 @@ func GetOrg(c *gin.Context) {
// @Param org_id path string true "the organization's id"
func GetOrgPermissions(c *gin.Context) {
user := session.User(c)
_store := store.FromContext(c)
org := session.Org(c)
_forge, err := server.Config.Services.Manager.ForgeFromUser(user)
if err != nil {
@ -95,23 +81,11 @@ func GetOrgPermissions(c *gin.Context) {
return
}
orgID, err := strconv.ParseInt(c.Param("org_id"), 10, 64)
if err != nil {
c.String(http.StatusBadRequest, "Error parsing org id. %s", err)
return
}
if user == nil {
c.JSON(http.StatusOK, &model.OrgPerm{})
return
}
org, err := _store.OrgGet(orgID)
if err != nil {
c.String(http.StatusInternalServerError, "Error getting org %d. %s", orgID, err)
return
}
if (org.IsUser && org.Name == user.Login) || (user.Admin && !org.IsUser) {
c.JSON(http.StatusOK, &model.OrgPerm{
Member: true,
@ -125,7 +99,7 @@ func GetOrgPermissions(c *gin.Context) {
perm, err := server.Config.Services.Membership.Get(c, _forge, user, org.Name)
if err != nil {
c.String(http.StatusInternalServerError, "Error getting membership for %d. %s", orgID, err)
c.String(http.StatusInternalServerError, "Error getting membership for %d. %s", org.ID, err)
return
}
@ -200,15 +174,9 @@ func LookupOrg(c *gin.Context) {
// @Param id path string true "the org's id"
func DeleteOrg(c *gin.Context) {
_store := store.FromContext(c)
org := session.Org(c)
orgID, err := strconv.ParseInt(c.Param("org_id"), 10, 64)
if err != nil {
c.String(http.StatusBadRequest, "Error parsing org id. %s", err)
return
}
err = _store.OrgDelete(orgID)
if err != nil {
if err := _store.OrgDelete(org.ID); err != nil {
handleDBError(c, err)
return
}

View file

@ -16,7 +16,6 @@ package api
import (
"net/http"
"strconv"
"github.com/gin-gonic/gin"
@ -36,16 +35,11 @@ import (
// @Param org_id path string true "the org's id"
// @Param registry path string true "the registry's address"
func GetOrgRegistry(c *gin.Context) {
org := session.Org(c)
addr := c.Param("registry")
orgID, err := strconv.ParseInt(c.Param("org_id"), 10, 64)
if err != nil {
c.String(http.StatusBadRequest, "Error parsing org id. %s", err)
return
}
registryService := server.Config.Services.Manager.RegistryService()
registry, err := registryService.OrgRegistryFind(orgID, addr)
registry, err := registryService.OrgRegistryFind(org.ID, addr)
if err != nil {
handleDBError(c, err)
return
@ -65,16 +59,12 @@ func GetOrgRegistry(c *gin.Context) {
// @Param page query int false "for response pagination, page offset number" default(1)
// @Param perPage query int false "for response pagination, max items per page" default(50)
func GetOrgRegistryList(c *gin.Context) {
orgID, err := strconv.ParseInt(c.Param("org_id"), 10, 64)
if err != nil {
c.String(http.StatusBadRequest, "Error parsing org id. %s", err)
return
}
org := session.Org(c)
registryService := server.Config.Services.Manager.RegistryService()
list, err := registryService.OrgRegistryList(orgID, session.Pagination(c))
list, err := registryService.OrgRegistryList(org.ID, session.Pagination(c))
if err != nil {
c.String(http.StatusInternalServerError, "Error getting registry list for %q. %s", orgID, err)
c.String(http.StatusInternalServerError, "Error getting registry list for %q. %s", org.ID, err)
return
}
// copy the registry detail to remove the sensitive
@ -96,31 +86,27 @@ func GetOrgRegistryList(c *gin.Context) {
// @Param org_id path string true "the org's id"
// @Param registryData body Registry true "the new registry"
func PostOrgRegistry(c *gin.Context) {
orgID, err := strconv.ParseInt(c.Param("org_id"), 10, 64)
if err != nil {
c.String(http.StatusBadRequest, "Error parsing org id. %s", err)
return
}
org := session.Org(c)
in := new(model.Registry)
if err := c.Bind(in); err != nil {
c.String(http.StatusBadRequest, "Error parsing org %q registry. %s", orgID, err)
c.String(http.StatusBadRequest, "Error parsing org %q registry. %s", org.ID, err)
return
}
registry := &model.Registry{
OrgID: orgID,
OrgID: org.ID,
Address: in.Address,
Username: in.Username,
Password: in.Password,
}
if err := registry.Validate(); err != nil {
c.String(http.StatusUnprocessableEntity, "Error inserting org %q registry. %s", orgID, err)
c.String(http.StatusUnprocessableEntity, "Error inserting org %q registry. %s", org.ID, err)
return
}
registryService := server.Config.Services.Manager.RegistryService()
if err := registryService.OrgRegistryCreate(orgID, registry); err != nil {
c.String(http.StatusInternalServerError, "Error inserting org %q registry %q. %s", orgID, in.Address, err)
if err := registryService.OrgRegistryCreate(org.ID, registry); err != nil {
c.String(http.StatusInternalServerError, "Error inserting org %q registry %q. %s", org.ID, in.Address, err)
return
}
c.JSON(http.StatusOK, registry.Copy())
@ -138,22 +124,17 @@ func PostOrgRegistry(c *gin.Context) {
// @Param registry path string true "the registry's name"
// @Param registryData body Registry true "the update registry data"
func PatchOrgRegistry(c *gin.Context) {
org := session.Org(c)
addr := c.Param("registry")
orgID, err := strconv.ParseInt(c.Param("org_id"), 10, 64)
if err != nil {
c.String(http.StatusBadRequest, "Error parsing org id. %s", err)
return
}
in := new(model.Registry)
err = c.Bind(in)
if err != nil {
if err := c.Bind(in); err != nil {
c.String(http.StatusBadRequest, "Error parsing registry. %s", err)
return
}
registryService := server.Config.Services.Manager.RegistryService()
registry, err := registryService.OrgRegistryFind(orgID, addr)
registry, err := registryService.OrgRegistryFind(org.ID, addr)
if err != nil {
handleDBError(c, err)
return
@ -169,12 +150,12 @@ func PatchOrgRegistry(c *gin.Context) {
}
if err := registry.Validate(); err != nil {
c.String(http.StatusUnprocessableEntity, "Error updating org %q registry. %s", orgID, err)
c.String(http.StatusUnprocessableEntity, "Error updating org %q registry. %s", org.ID, err)
return
}
if err := registryService.OrgRegistryUpdate(orgID, registry); err != nil {
c.String(http.StatusInternalServerError, "Error updating org %q registry %q. %s", orgID, in.Address, err)
if err := registryService.OrgRegistryUpdate(org.ID, registry); err != nil {
c.String(http.StatusInternalServerError, "Error updating org %q registry %q. %s", org.ID, in.Address, err)
return
}
c.JSON(http.StatusOK, registry.Copy())
@ -191,15 +172,11 @@ func PatchOrgRegistry(c *gin.Context) {
// @Param org_id path string true "the org's id"
// @Param registry path string true "the registry's name"
func DeleteOrgRegistry(c *gin.Context) {
org := session.Org(c)
addr := c.Param("registry")
orgID, err := strconv.ParseInt(c.Param("org_id"), 10, 64)
if err != nil {
c.String(http.StatusBadRequest, "Error parsing org id. %s", err)
return
}
registryService := server.Config.Services.Manager.RegistryService()
if err := registryService.OrgRegistryDelete(orgID, addr); err != nil {
if err := registryService.OrgRegistryDelete(org.ID, addr); err != nil {
handleDBError(c, err)
return
}

View file

@ -16,7 +16,6 @@ package api
import (
"net/http"
"strconv"
"github.com/gin-gonic/gin"
@ -36,16 +35,11 @@ import (
// @Param org_id path string true "the org's id"
// @Param secret path string true "the secret's name"
func GetOrgSecret(c *gin.Context) {
org := session.Org(c)
name := c.Param("secret")
orgID, err := strconv.ParseInt(c.Param("org_id"), 10, 64)
if err != nil {
c.String(http.StatusBadRequest, "Error parsing org id. %s", err)
return
}
secretService := server.Config.Services.Manager.SecretService()
secret, err := secretService.OrgSecretFind(orgID, name)
secret, err := secretService.OrgSecretFind(org.ID, name)
if err != nil {
handleDBError(c, err)
return
@ -65,16 +59,12 @@ func GetOrgSecret(c *gin.Context) {
// @Param page query int false "for response pagination, page offset number" default(1)
// @Param perPage query int false "for response pagination, max items per page" default(50)
func GetOrgSecretList(c *gin.Context) {
orgID, err := strconv.ParseInt(c.Param("org_id"), 10, 64)
if err != nil {
c.String(http.StatusBadRequest, "Error parsing org id. %s", err)
return
}
org := session.Org(c)
secretService := server.Config.Services.Manager.SecretService()
list, err := secretService.OrgSecretList(orgID, session.Pagination(c))
list, err := secretService.OrgSecretList(org.ID, session.Pagination(c))
if err != nil {
c.String(http.StatusInternalServerError, "Error getting secret list for %q. %s", orgID, err)
c.String(http.StatusInternalServerError, "Error getting secret list for %q. %s", org.ID, err)
return
}
// copy the secret detail to remove the sensitive
@ -96,32 +86,28 @@ func GetOrgSecretList(c *gin.Context) {
// @Param org_id path string true "the org's id"
// @Param secretData body Secret true "the new secret"
func PostOrgSecret(c *gin.Context) {
orgID, err := strconv.ParseInt(c.Param("org_id"), 10, 64)
if err != nil {
c.String(http.StatusBadRequest, "Error parsing org id. %s", err)
return
}
org := session.Org(c)
in := new(model.Secret)
if err := c.Bind(in); err != nil {
c.String(http.StatusBadRequest, "Error parsing org %q secret. %s", orgID, err)
c.String(http.StatusBadRequest, "Error parsing org %q secret. %s", org.ID, err)
return
}
secret := &model.Secret{
OrgID: orgID,
OrgID: org.ID,
Name: in.Name,
Value: in.Value,
Events: in.Events,
Images: in.Images,
}
if err := secret.Validate(); err != nil {
c.String(http.StatusUnprocessableEntity, "Error inserting org %q secret. %s", orgID, err)
c.String(http.StatusUnprocessableEntity, "Error inserting org %q secret. %s", org.ID, err)
return
}
secretService := server.Config.Services.Manager.SecretService()
if err := secretService.OrgSecretCreate(orgID, secret); err != nil {
c.String(http.StatusInternalServerError, "Error inserting org %q secret %q. %s", orgID, in.Name, err)
if err := secretService.OrgSecretCreate(org.ID, secret); err != nil {
c.String(http.StatusInternalServerError, "Error inserting org %q secret %q. %s", org.ID, in.Name, err)
return
}
c.JSON(http.StatusOK, secret.Copy())
@ -139,22 +125,17 @@ func PostOrgSecret(c *gin.Context) {
// @Param secret path string true "the secret's name"
// @Param secretData body Secret true "the update secret data"
func PatchOrgSecret(c *gin.Context) {
org := session.Org(c)
name := c.Param("secret")
orgID, err := strconv.ParseInt(c.Param("org_id"), 10, 64)
if err != nil {
c.String(http.StatusBadRequest, "Error parsing org id. %s", err)
return
}
in := new(model.Secret)
err = c.Bind(in)
if err != nil {
if err := c.Bind(in); err != nil {
c.String(http.StatusBadRequest, "Error parsing secret. %s", err)
return
}
secretService := server.Config.Services.Manager.SecretService()
secret, err := secretService.OrgSecretFind(orgID, name)
secret, err := secretService.OrgSecretFind(org.ID, name)
if err != nil {
handleDBError(c, err)
return
@ -170,12 +151,12 @@ func PatchOrgSecret(c *gin.Context) {
}
if err := secret.Validate(); err != nil {
c.String(http.StatusUnprocessableEntity, "Error updating org %q secret. %s", orgID, err)
c.String(http.StatusUnprocessableEntity, "Error updating org %q secret. %s", org.ID, err)
return
}
if err := secretService.OrgSecretUpdate(orgID, secret); err != nil {
c.String(http.StatusInternalServerError, "Error updating org %q secret %q. %s", orgID, in.Name, err)
if err := secretService.OrgSecretUpdate(org.ID, secret); err != nil {
c.String(http.StatusInternalServerError, "Error updating org %q secret %q. %s", org.ID, in.Name, err)
return
}
c.JSON(http.StatusOK, secret.Copy())
@ -192,15 +173,11 @@ func PatchOrgSecret(c *gin.Context) {
// @Param org_id path string true "the org's id"
// @Param secret path string true "the secret's name"
func DeleteOrgSecret(c *gin.Context) {
org := session.Org(c)
name := c.Param("secret")
orgID, err := strconv.ParseInt(c.Param("org_id"), 10, 64)
if err != nil {
c.String(http.StatusBadRequest, "Error parsing org id. %s", err)
return
}
secretService := server.Config.Services.Manager.SecretService()
if err := secretService.OrgSecretDelete(orgID, name); err != nil {
if err := secretService.OrgSecretDelete(org.ID, name); err != nil {
handleDBError(c, err)
return
}

View file

@ -30,8 +30,10 @@ import (
"go.woodpecker-ci.org/woodpecker/v2/server"
"go.woodpecker-ci.org/woodpecker/v2/server/model"
"go.woodpecker-ci.org/woodpecker/v2/server/pipeline"
"go.woodpecker-ci.org/woodpecker/v2/server/pipeline/stepbuilder"
"go.woodpecker-ci.org/woodpecker/v2/server/router/middleware/session"
"go.woodpecker-ci.org/woodpecker/v2/server/store"
"go.woodpecker-ci.org/woodpecker/v2/server/store/types"
)
// CreatePipeline
@ -392,6 +394,47 @@ func GetPipelineConfig(c *gin.Context) {
c.JSON(http.StatusOK, configs)
}
// GetPipelineMetadata
//
// @Summary Get metadata for a pipeline or a specific workflow, including previous pipeline info
// @Router /repos/{repo_id}/pipelines/{number}/metadata [get]
// @Produce json
// @Success 200 {object} metadata.Metadata
// @Tags Pipelines
// @Param Authorization header string true "Insert your personal access token" default(Bearer <personal access token>)
// @Param repo_id path int true "the repository id"
// @Param number path int true "the number of the pipeline"
func GetPipelineMetadata(c *gin.Context) {
repo := session.Repo(c)
num, err := strconv.ParseInt(c.Param("number"), 10, 64)
if err != nil {
_ = c.AbortWithError(http.StatusBadRequest, err)
return
}
_store := store.FromContext(c)
currentPipeline, err := _store.GetPipelineNumber(repo, num)
if err != nil {
handleDBError(c, err)
return
}
forge, err := server.Config.Services.Manager.ForgeFromRepo(repo)
if err != nil {
c.AbortWithStatus(http.StatusInternalServerError)
return
}
prevPipeline, err := _store.GetPipelineLastBefore(repo, currentPipeline.Branch, currentPipeline.ID)
if err != nil && !errors.Is(err, types.RecordNotExist) {
handleDBError(c, err)
return
}
metadata := stepbuilder.MetadataFromStruct(forge, repo, currentPipeline, prevPipeline, nil, server.Config.Server.Host)
c.JSON(http.StatusOK, metadata)
}
// CancelPipeline
//
// @Summary Cancel a pipeline

View file

@ -1,129 +1,217 @@
// 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 api
import (
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"github.com/franela/goblin"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"go.woodpecker-ci.org/woodpecker/v2/pipeline/frontend/metadata"
"go.woodpecker-ci.org/woodpecker/v2/server"
forge_mocks "go.woodpecker-ci.org/woodpecker/v2/server/forge/mocks"
"go.woodpecker-ci.org/woodpecker/v2/server/model"
"go.woodpecker-ci.org/woodpecker/v2/server/store/mocks"
mocks_manager "go.woodpecker-ci.org/woodpecker/v2/server/services/mocks"
store_mocks "go.woodpecker-ci.org/woodpecker/v2/server/store/mocks"
"go.woodpecker-ci.org/woodpecker/v2/server/store/types"
)
var fakePipeline = &model.Pipeline{
ID: 2,
Number: 2,
Status: model.StatusSuccess,
}
func TestGetPipelines(t *testing.T) {
gin.SetMode(gin.TestMode)
g := goblin.Goblin(t)
g.Describe("Pipeline", func() {
g.It("should get pipelines", func() {
pipelines := []*model.Pipeline{fakePipeline}
t.Run("should get pipelines", func(t *testing.T) {
pipelines := []*model.Pipeline{fakePipeline}
mockStore := mocks.NewStore(t)
mockStore.On("GetPipelineList", mock.Anything, mock.Anything, mock.Anything).Return(pipelines, nil)
mockStore := store_mocks.NewStore(t)
mockStore.On("GetPipelineList", mock.Anything, mock.Anything, mock.Anything).Return(pipelines, nil)
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
c.Set("store", mockStore)
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
c.Set("store", mockStore)
GetPipelines(c)
GetPipelines(c)
mockStore.AssertCalled(t, "GetPipelineList", mock.Anything, mock.Anything, mock.Anything)
assert.Equal(t, http.StatusOK, c.Writer.Status())
})
mockStore.AssertCalled(t, "GetPipelineList", mock.Anything, mock.Anything, mock.Anything)
assert.Equal(t, http.StatusOK, c.Writer.Status())
})
g.It("should not parse pipeline filter", func() {
c, _ := gin.CreateTestContext(httptest.NewRecorder())
c.Request, _ = http.NewRequest(http.MethodDelete, "/?before=2023-01-16&after=2023-01-15", nil)
t.Run("should not parse pipeline filter", func(t *testing.T) {
c, _ := gin.CreateTestContext(httptest.NewRecorder())
c.Request, _ = http.NewRequest(http.MethodDelete, "/?before=2023-01-16&after=2023-01-15", nil)
GetPipelines(c)
GetPipelines(c)
assert.Equal(t, http.StatusBadRequest, c.Writer.Status())
})
assert.Equal(t, http.StatusBadRequest, c.Writer.Status())
})
g.It("should parse pipeline filter", func() {
pipelines := []*model.Pipeline{fakePipeline}
t.Run("should parse pipeline filter", func(t *testing.T) {
pipelines := []*model.Pipeline{fakePipeline}
mockStore := mocks.NewStore(t)
mockStore.On("GetPipelineList", mock.Anything, mock.Anything, mock.Anything).Return(pipelines, nil)
mockStore := store_mocks.NewStore(t)
mockStore.On("GetPipelineList", mock.Anything, mock.Anything, mock.Anything).Return(pipelines, nil)
c, _ := gin.CreateTestContext(httptest.NewRecorder())
c.Set("store", mockStore)
c.Request, _ = http.NewRequest(http.MethodDelete, "/?2023-01-16T15:00:00Z&after=2023-01-15T15:00:00Z", nil)
c, _ := gin.CreateTestContext(httptest.NewRecorder())
c.Set("store", mockStore)
c.Request, _ = http.NewRequest(http.MethodDelete, "/?2023-01-16T15:00:00Z&after=2023-01-15T15:00:00Z", nil)
GetPipelines(c)
GetPipelines(c)
assert.Equal(t, http.StatusOK, c.Writer.Status())
})
assert.Equal(t, http.StatusOK, c.Writer.Status())
})
g.It("should parse pipeline filter with tz offset", func() {
pipelines := []*model.Pipeline{fakePipeline}
t.Run("should parse pipeline filter with tz offset", func(t *testing.T) {
pipelines := []*model.Pipeline{fakePipeline}
mockStore := mocks.NewStore(t)
mockStore.On("GetPipelineList", mock.Anything, mock.Anything, mock.Anything).Return(pipelines, nil)
mockStore := store_mocks.NewStore(t)
mockStore.On("GetPipelineList", mock.Anything, mock.Anything, mock.Anything).Return(pipelines, nil)
c, _ := gin.CreateTestContext(httptest.NewRecorder())
c.Set("store", mockStore)
c.Request, _ = http.NewRequest(http.MethodDelete, "/?before=2023-01-16T15:00:00%2B01:00&after=2023-01-15T15:00:00%2B01:00", nil)
c, _ := gin.CreateTestContext(httptest.NewRecorder())
c.Set("store", mockStore)
c.Request, _ = http.NewRequest(http.MethodDelete, "/?before=2023-01-16T15:00:00%2B01:00&after=2023-01-15T15:00:00%2B01:00", nil)
GetPipelines(c)
GetPipelines(c)
assert.Equal(t, http.StatusOK, c.Writer.Status())
})
assert.Equal(t, http.StatusOK, c.Writer.Status())
})
}
func TestDeletePipeline(t *testing.T) {
gin.SetMode(gin.TestMode)
g := goblin.Goblin(t)
g.Describe("Pipeline", func() {
g.It("should delete pipeline", func() {
mockStore := mocks.NewStore(t)
mockStore.On("GetPipelineNumber", mock.Anything, mock.Anything).Return(fakePipeline, nil)
mockStore.On("DeletePipeline", mock.Anything).Return(nil)
t.Run("should delete pipeline", func(t *testing.T) {
mockStore := store_mocks.NewStore(t)
mockStore.On("GetPipelineNumber", mock.Anything, mock.Anything).Return(fakePipeline, nil)
mockStore.On("DeletePipeline", mock.Anything).Return(nil)
c, _ := gin.CreateTestContext(httptest.NewRecorder())
c, _ := gin.CreateTestContext(httptest.NewRecorder())
c.Set("store", mockStore)
c.Params = gin.Params{{Key: "number", Value: "2"}}
DeletePipeline(c)
mockStore.AssertCalled(t, "GetPipelineNumber", mock.Anything, mock.Anything)
mockStore.AssertCalled(t, "DeletePipeline", mock.Anything)
assert.Equal(t, http.StatusNoContent, c.Writer.Status())
})
t.Run("should not delete without pipeline number", func(t *testing.T) {
c, _ := gin.CreateTestContext(httptest.NewRecorder())
DeletePipeline(c)
assert.Equal(t, http.StatusBadRequest, c.Writer.Status())
})
t.Run("should not delete pending", func(t *testing.T) {
fakePipeline := *fakePipeline
fakePipeline.Status = model.StatusPending
mockStore := store_mocks.NewStore(t)
mockStore.On("GetPipelineNumber", mock.Anything, mock.Anything).Return(&fakePipeline, nil)
c, _ := gin.CreateTestContext(httptest.NewRecorder())
c.Set("store", mockStore)
c.Params = gin.Params{{Key: "number", Value: "2"}}
DeletePipeline(c)
mockStore.AssertCalled(t, "GetPipelineNumber", mock.Anything, mock.Anything)
mockStore.AssertNotCalled(t, "DeletePipeline", mock.Anything)
assert.Equal(t, http.StatusUnprocessableEntity, c.Writer.Status())
})
}
func TestGetPipelineMetadata(t *testing.T) {
gin.SetMode(gin.TestMode)
prevPipeline := &model.Pipeline{
ID: 1,
Number: 1,
Status: model.StatusFailure,
}
fakeRepo := &model.Repo{ID: 1}
mockForge := forge_mocks.NewForge(t)
mockForge.On("Name").Return("mock")
mockForge.On("URL").Return("https://codeberg.org")
mockManager := mocks_manager.NewManager(t)
mockManager.On("ForgeFromRepo", fakeRepo).Return(mockForge, nil)
server.Config.Services.Manager = mockManager
mockStore := store_mocks.NewStore(t)
mockStore.On("GetPipelineNumber", mock.Anything, int64(2)).Return(fakePipeline, nil)
mockStore.On("GetPipelineLastBefore", mock.Anything, mock.Anything, int64(2)).Return(prevPipeline, nil)
t.Run("PipelineMetadata", func(t *testing.T) {
t.Run("should get pipeline metadata", func(t *testing.T) {
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
c.Params = gin.Params{{Key: "number", Value: "2"}}
c.Set("store", mockStore)
c.Params = gin.Params{{Key: "number", Value: "1"}}
c.Set("forge", mockForge)
c.Set("repo", fakeRepo)
DeletePipeline(c)
GetPipelineMetadata(c)
mockStore.AssertCalled(t, "GetPipelineNumber", mock.Anything, mock.Anything)
mockStore.AssertCalled(t, "DeletePipeline", mock.Anything)
assert.Equal(t, http.StatusNoContent, c.Writer.Status())
assert.Equal(t, http.StatusOK, w.Code)
var response metadata.Metadata
err := json.Unmarshal(w.Body.Bytes(), &response)
assert.NoError(t, err)
assert.Equal(t, int64(1), response.Repo.ID)
assert.Equal(t, int64(2), response.Curr.Number)
assert.Equal(t, int64(1), response.Prev.Number)
})
g.It("should not delete without pipeline number", func() {
c, _ := gin.CreateTestContext(httptest.NewRecorder())
t.Run("should return bad request for invalid pipeline number", func(t *testing.T) {
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
c.Params = gin.Params{{Key: "number", Value: "invalid"}}
DeletePipeline(c)
GetPipelineMetadata(c)
assert.Equal(t, http.StatusBadRequest, c.Writer.Status())
assert.Equal(t, http.StatusBadRequest, w.Code)
})
g.It("should not delete pending", func() {
fakePipeline.Status = model.StatusPending
t.Run("should return not found for non-existent pipeline", func(t *testing.T) {
mockStore := store_mocks.NewStore(t)
mockStore.On("GetPipelineNumber", mock.Anything, int64(3)).Return((*model.Pipeline)(nil), types.RecordNotExist)
mockStore := mocks.NewStore(t)
mockStore.On("GetPipelineNumber", mock.Anything, mock.Anything).Return(fakePipeline, nil)
c, _ := gin.CreateTestContext(httptest.NewRecorder())
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
c.Params = gin.Params{{Key: "number", Value: "3"}}
c.Set("store", mockStore)
c.Params = gin.Params{{Key: "number", Value: "1"}}
c.Set("repo", fakeRepo)
DeletePipeline(c)
GetPipelineMetadata(c)
mockStore.AssertCalled(t, "GetPipelineNumber", mock.Anything, mock.Anything)
mockStore.AssertNotCalled(t, "DeletePipeline", mock.Anything)
assert.Equal(t, http.StatusUnprocessableEntity, c.Writer.Status())
assert.Equal(t, http.StatusNotFound, w.Code)
})
})
}

View file

@ -66,7 +66,6 @@ var Config = struct {
DefaultCancelPreviousPipelineEvents []model.WebhookEvent
DefaultClonePlugin string
TrustedClonePlugins []string
Limits model.ResourceLimit
Volumes []string
Networks []string
PrivilegedPlugins []string

View file

@ -18,7 +18,7 @@ package github
import (
"fmt"
"github.com/google/go-github/v64/github"
"github.com/google/go-github/v66/github"
"go.woodpecker-ci.org/woodpecker/v2/server/model"
)

View file

@ -19,7 +19,7 @@ import (
"testing"
"github.com/franela/goblin"
"github.com/google/go-github/v64/github"
"github.com/google/go-github/v66/github"
"go.woodpecker-ci.org/woodpecker/v2/server/model"
)

View file

@ -27,7 +27,7 @@ import (
"strings"
"time"
"github.com/google/go-github/v64/github"
"github.com/google/go-github/v66/github"
"github.com/rs/zerolog/log"
"golang.org/x/oauth2"

View file

@ -22,7 +22,7 @@ import (
"net/http"
"strings"
"github.com/google/go-github/v64/github"
"github.com/google/go-github/v66/github"
"go.woodpecker-ci.org/woodpecker/v2/server/forge/types"
"go.woodpecker-ci.org/woodpecker/v2/server/model"

View file

@ -303,7 +303,9 @@ func extractFromPath(str string) (string, string, error) {
if len(s) < minPathComponents {
return "", "", fmt.Errorf("minimum match not found")
}
return s[0], s[1], nil
owner := strings.Join(s[:len(s)-1], "/")
name := s[len(s)-1]
return owner, name, nil
}
func convertLabels(from []*gitlab.EventLabel) []string {

View file

@ -300,3 +300,68 @@ func Test_GitLab(t *testing.T) {
})
})
}
func TestExtractFromPath(t *testing.T) {
type testCase struct {
name string
input string
wantOwner string
wantName string
errContains string
}
tests := []testCase{
{
name: "basic two components",
input: "owner/repo",
wantOwner: "owner",
wantName: "repo",
},
{
name: "three components",
input: "owner/group/repo",
wantOwner: "owner/group",
wantName: "repo",
},
{
name: "many components",
input: "owner/group/subgroup/deep/repo",
wantOwner: "owner/group/subgroup/deep",
wantName: "repo",
},
{
name: "empty string",
input: "",
errContains: "minimum match not found",
},
{
name: "single component",
input: "onlyrepo",
errContains: "minimum match not found",
},
{
name: "trailing slash",
input: "owner/repo/",
wantOwner: "owner/repo",
wantName: "",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
owner, name, err := extractFromPath(tc.input)
// Check error expectations
if tc.errContains != "" {
if assert.Error(t, err) {
assert.Contains(t, err.Error(), tc.errContains)
}
return
}
assert.NoError(t, err)
assert.EqualValues(t, tc.wantOwner, owner)
assert.EqualValues(t, tc.wantName, name)
})
}
}

View file

@ -60,13 +60,12 @@ func (s *WoodpeckerAuthServer) getAgent(agentID int64, agentToken string) (*mode
// global agent secret auth
if s.agentMasterToken != "" {
if agentToken == s.agentMasterToken && agentID == -1 {
agent := new(model.Agent)
agent.Name = ""
agent.OwnerID = -1 // system agent
agent.Token = s.agentMasterToken
agent.Backend = ""
agent.Platform = ""
agent.Capacity = -1
agent := &model.Agent{
OwnerID: model.IDNotSet,
OrgID: model.IDNotSet,
Token: s.agentMasterToken,
Capacity: -1,
}
err := s.store.AgentCreate(agent)
if err != nil {
log.Error().Err(err).Msg("error creating system agent")

View file

@ -21,28 +21,32 @@ import (
)
func createFilterFunc(agentFilter rpc.Filter) queue.FilterFn {
return func(task *model.Task) bool {
return func(task *model.Task) (bool, int) {
score := 0
for taskLabel, taskLabelValue := range task.Labels {
// if a task label is empty it will be ignored
if taskLabelValue == "" {
continue
}
// all task labels are required to be present for an agent to match
agentLabelValue, ok := agentFilter.Labels[taskLabel]
if !ok {
return false
return false, 0
}
switch {
// if agent label has a wildcard
if agentLabelValue == "*" {
continue
}
if taskLabelValue != agentLabelValue {
return false
case agentLabelValue == "*":
score++
// if agent label has an exact match
case agentLabelValue == taskLabelValue:
score += 10
// agent doesn't match
default:
return false, 0
}
}
return true
return true, score
}
}

View file

@ -24,68 +24,110 @@ import (
)
func TestCreateFilterFunc(t *testing.T) {
t.Parallel()
tests := []struct {
name string
agentLabels map[string]string
task model.Task
exp bool
agentFilter rpc.Filter
task *model.Task
wantMatched bool
wantScore int
}{
{
name: "agent with missing labels",
agentLabels: map[string]string{"repo": "test/woodpecker"},
task: model.Task{
Labels: map[string]string{"platform": "linux/amd64", "repo": "test/woodpecker"},
name: "Two exact matches",
agentFilter: rpc.Filter{
Labels: map[string]string{"org-id": "123", "platform": "linux"},
},
exp: false,
task: &model.Task{
Labels: map[string]string{"org-id": "123", "platform": "linux"},
},
wantMatched: true,
wantScore: 20,
},
{
name: "agent with wrong labels",
agentLabels: map[string]string{"platform": "linux/arm64"},
task: model.Task{
Labels: map[string]string{"platform": "linux/amd64"},
name: "Wildcard and exact match",
agentFilter: rpc.Filter{
Labels: map[string]string{"org-id": "*", "platform": "linux"},
},
exp: false,
task: &model.Task{
Labels: map[string]string{"org-id": "123", "platform": "linux"},
},
wantMatched: true,
wantScore: 11,
},
{
name: "agent with correct labels",
agentLabels: map[string]string{"platform": "linux/amd64", "location": "europe"},
task: model.Task{
Labels: map[string]string{"platform": "linux/amd64", "location": "europe"},
name: "Partial match",
agentFilter: rpc.Filter{
Labels: map[string]string{"org-id": "123", "platform": "linux"},
},
exp: true,
task: &model.Task{
Labels: map[string]string{"org-id": "123", "platform": "windows"},
},
wantMatched: false,
wantScore: 0,
},
{
name: "agent with additional labels",
agentLabels: map[string]string{"platform": "linux/amd64", "location": "europe"},
task: model.Task{
Labels: map[string]string{"platform": "linux/amd64"},
name: "No match",
agentFilter: rpc.Filter{
Labels: map[string]string{"org-id": "456", "platform": "linux"},
},
exp: true,
task: &model.Task{
Labels: map[string]string{"org-id": "123", "platform": "windows"},
},
wantMatched: false,
wantScore: 0,
},
{
name: "agent with wildcard label",
agentLabels: map[string]string{"platform": "linux/amd64", "location": "*"},
task: model.Task{
Labels: map[string]string{"platform": "linux/amd64", "location": "america"},
name: "Missing label",
agentFilter: rpc.Filter{
Labels: map[string]string{"platform": "linux"},
},
exp: true,
task: &model.Task{
Labels: map[string]string{"needed": "some"},
},
wantMatched: false,
wantScore: 0,
},
{
name: "agent with platform label and task without",
agentLabels: map[string]string{"platform": "linux/amd64"},
task: model.Task{
Labels: map[string]string{"platform": ""},
name: "Empty task labels",
agentFilter: rpc.Filter{
Labels: map[string]string{"org-id": "123", "platform": "linux"},
},
exp: true,
task: &model.Task{
Labels: map[string]string{},
},
wantMatched: true,
wantScore: 0,
},
{
name: "Agent with additional label",
agentFilter: rpc.Filter{
Labels: map[string]string{"org-id": "123", "platform": "linux", "extra": "value"},
},
task: &model.Task{
Labels: map[string]string{"org-id": "123", "platform": "linux", "empty": ""},
},
wantMatched: true,
wantScore: 20,
},
{
name: "Two wildcard matches",
agentFilter: rpc.Filter{
Labels: map[string]string{"org-id": "*", "platform": "*"},
},
task: &model.Task{
Labels: map[string]string{"org-id": "123", "platform": "linux"},
},
wantMatched: true,
wantScore: 2,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
fn := createFilterFunc(rpc.Filter{Labels: test.agentLabels})
assert.EqualValues(t, test.exp, fn(&test.task))
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
filterFunc := createFilterFunc(tt.agentFilter)
gotMatched, gotScore := filterFunc(tt.task)
assert.Equal(t, tt.wantMatched, gotMatched, "Matched result")
assert.Equal(t, tt.wantScore, gotScore, "Score")
})
}
}

View file

@ -57,8 +57,6 @@ func (s *RPC) Next(c context.Context, agentFilter rpc.Filter) (*rpc.Workflow, er
log.Debug().Msgf("agent connected: %s: polling", hostname)
}
filterFn := createFilterFunc(agentFilter)
agent, err := s.getAgentFromContext(c)
if err != nil {
return nil, err
@ -69,6 +67,20 @@ func (s *RPC) Next(c context.Context, agentFilter rpc.Filter) (*rpc.Workflow, er
return nil, nil
}
agentServerLabels, err := agent.GetServerLabels()
if err != nil {
return nil, err
}
// enforce labels from server by overwriting agent labels
for k, v := range agentServerLabels {
agentFilter.Labels[k] = v
}
log.Trace().Msgf("Agent %s[%d] tries to pull task with labels: %v", agent.Name, agent.ID, agentFilter.Labels)
filterFn := createFilterFunc(agentFilter)
for {
// poll blocks until a task is available or the context is canceled / worker is kicked
task, err := s.queue.Poll(c, agent.ID, filterFn)
@ -91,6 +103,15 @@ func (s *RPC) Next(c context.Context, agentFilter rpc.Filter) (*rpc.Workflow, er
// Wait blocks until the workflow with the given ID is done.
func (s *RPC) Wait(c context.Context, workflowID string) error {
agent, err := s.getAgentFromContext(c)
if err != nil {
return err
}
if err := s.checkAgentPermissionByWorkflow(c, agent, workflowID, nil, nil); err != nil {
return err
}
return s.queue.Wait(c, workflowID)
}
@ -106,11 +127,15 @@ func (s *RPC) Extend(c context.Context, workflowID string) error {
return err
}
return s.queue.Extend(c, workflowID)
if err := s.checkAgentPermissionByWorkflow(c, agent, workflowID, nil, nil); err != nil {
return err
}
return s.queue.Extend(c, agent.ID, workflowID)
}
// Update updates the state of a step.
func (s *RPC) Update(_ context.Context, strWorkflowID string, state rpc.StepState) error {
func (s *RPC) Update(c context.Context, strWorkflowID string, state rpc.StepState) error {
workflowID, err := strconv.ParseInt(strWorkflowID, 10, 64)
if err != nil {
return err
@ -128,6 +153,11 @@ func (s *RPC) Update(_ context.Context, strWorkflowID string, state rpc.StepStat
return err
}
agent, err := s.getAgentFromContext(c)
if err != nil {
return err
}
step, err := s.store.StepByUUID(state.StepUUID)
if err != nil {
log.Error().Err(err).Msgf("cannot find step with uuid %s", state.StepUUID)
@ -149,6 +179,11 @@ func (s *RPC) Update(_ context.Context, strWorkflowID string, state rpc.StepStat
return err
}
// check before agent can alter some state
if err := s.checkAgentPermissionByWorkflow(c, agent, strWorkflowID, currentPipeline, repo); err != nil {
return err
}
if err := pipeline.UpdateStepStatus(s.store, step, state); err != nil {
log.Error().Err(err).Msg("rpc.update: cannot update step")
}
@ -192,6 +227,7 @@ func (s *RPC) Init(c context.Context, strWorkflowID string, state rpc.WorkflowSt
if err != nil {
return err
}
workflow.AgentID = agent.ID
currentPipeline, err := s.store.GetPipeline(workflow.PipelineID)
@ -206,6 +242,11 @@ func (s *RPC) Init(c context.Context, strWorkflowID string, state rpc.WorkflowSt
return err
}
// check before agent can alter some state
if err := s.checkAgentPermissionByWorkflow(c, agent, strWorkflowID, currentPipeline, repo); err != nil {
return err
}
if currentPipeline.Status == model.StatusPending {
if currentPipeline, err = pipeline.UpdateToStatusRunning(s.store, *currentPipeline, state.Started); err != nil {
log.Error().Err(err).Msgf("init: cannot update pipeline %d state", currentPipeline.ID)
@ -272,6 +313,16 @@ func (s *RPC) Done(c context.Context, strWorkflowID string, state rpc.WorkflowSt
return err
}
agent, err := s.getAgentFromContext(c)
if err != nil {
return err
}
// check before agent can alter some state
if err := s.checkAgentPermissionByWorkflow(c, agent, strWorkflowID, currentPipeline, repo); err != nil {
return err
}
logger := log.With().
Str("repo_id", fmt.Sprint(repo.ID)).
Str("pipeline_id", fmt.Sprint(currentPipeline.ID)).
@ -328,10 +379,6 @@ func (s *RPC) Done(c context.Context, strWorkflowID string, state rpc.WorkflowSt
s.pipelineTime.WithLabelValues(repo.FullName, currentPipeline.Branch, string(workflow.State), workflow.Name).Set(float64(workflow.Finished - workflow.Started))
}
agent, err := s.getAgentFromContext(c)
if err != nil {
return err
}
return s.updateAgentLastWork(agent)
}
@ -348,6 +395,17 @@ func (s *RPC) Log(c context.Context, stepUUID string, rpcLogEntries []*rpc.LogEn
return err
}
currentPipeline, err := s.store.GetPipeline(step.PipelineID)
if err != nil {
log.Error().Err(err).Msgf("cannot find pipeline with id %d", step.PipelineID)
return err
}
// check before agent can alter some state
if err := s.checkAgentPermissionByWorkflow(c, agent, "", currentPipeline, nil); err != nil {
return err
}
err = s.updateAgentLastWork(agent)
if err != nil {
return err
@ -383,7 +441,7 @@ func (s *RPC) Log(c context.Context, stepUUID string, rpcLogEntries []*rpc.LogEn
return nil
}
func (s *RPC) RegisterAgent(ctx context.Context, platform, backend, version string, capacity int32) (int64, error) {
func (s *RPC) RegisterAgent(ctx context.Context, info rpc.AgentInfo) (int64, error) {
agent, err := s.getAgentFromContext(ctx)
if err != nil {
return -1, err
@ -395,10 +453,11 @@ func (s *RPC) RegisterAgent(ctx context.Context, platform, backend, version stri
}
}
agent.Backend = backend
agent.Platform = platform
agent.Capacity = capacity
agent.Version = version
agent.Backend = info.Backend
agent.Platform = info.Platform
agent.Capacity = int32(info.Capacity)
agent.Version = info.Version
agent.CustomLabels = info.CustomLabels
err = s.store.AgentUpdate(agent)
if err != nil {
@ -441,6 +500,44 @@ func (s *RPC) ReportHealth(ctx context.Context, status string) error {
return s.store.AgentUpdate(agent)
}
func (s *RPC) checkAgentPermissionByWorkflow(_ context.Context, agent *model.Agent, strWorkflowID string, pipeline *model.Pipeline, repo *model.Repo) error {
var err error
if repo == nil && pipeline == nil {
workflowID, err := strconv.ParseInt(strWorkflowID, 10, 64)
if err != nil {
return err
}
workflow, err := s.store.WorkflowLoad(workflowID)
if err != nil {
log.Error().Err(err).Msgf("cannot find workflow with id %d", workflowID)
return err
}
pipeline, err = s.store.GetPipeline(workflow.PipelineID)
if err != nil {
log.Error().Err(err).Msgf("cannot find pipeline with id %d", workflow.PipelineID)
return err
}
}
if repo == nil {
repo, err = s.store.GetRepo(pipeline.RepoID)
if err != nil {
log.Error().Err(err).Msgf("cannot find repo with id %d", pipeline.RepoID)
return err
}
}
if agent.CanAccessRepo(repo) {
return nil
}
msg := fmt.Sprintf("agent '%d' is not allowed to interact with repo[%d] '%s'", agent.ID, repo.ID, repo.FullName)
log.Error().Int64("repoId", repo.ID).Msg(msg)
return errors.New(msg)
}
func (s *RPC) completeChildrenIfParentCompleted(completedWorkflow *model.Workflow) {
for _, c := range completedWorkflow.Children {
if c.Running() {

View file

@ -24,6 +24,7 @@ import (
"github.com/stretchr/testify/mock"
"google.golang.org/grpc/metadata"
"go.woodpecker-ci.org/woodpecker/v2/pipeline/rpc"
"go.woodpecker-ci.org/woodpecker/v2/server/model"
mocks_store "go.woodpecker-ci.org/woodpecker/v2/server/store/mocks"
)
@ -52,15 +53,19 @@ func TestRegisterAgent(t *testing.T) {
store.On("AgentFind", int64(1337)).Once().Return(storeAgent, nil)
store.On("AgentUpdate", &updatedAgent).Once().Return(nil)
rpc := RPC{
grpc := RPC{
store: store,
}
ctx := metadata.NewIncomingContext(
context.Background(),
metadata.Pairs("hostname", "hostname", "agent_id", "1337"),
)
capacity := int32(2)
agentID, err := rpc.RegisterAgent(ctx, "platform", "backend", "version", capacity)
agentID, err := grpc.RegisterAgent(ctx, rpc.AgentInfo{
Version: "version",
Platform: "platform",
Backend: "backend",
Capacity: 2,
})
if !assert.NoError(t, err) {
return
}
@ -92,15 +97,19 @@ func TestRegisterAgent(t *testing.T) {
store.On("AgentFind", int64(1337)).Once().Return(storeAgent, nil)
store.On("AgentUpdate", &updatedAgent).Once().Return(nil)
rpc := RPC{
grpc := RPC{
store: store,
}
ctx := metadata.NewIncomingContext(
context.Background(),
metadata.Pairs("hostname", "newHostname", "agent_id", "1337"),
)
capacity := int32(2)
agentID, err := rpc.RegisterAgent(ctx, "platform", "backend", "version", capacity)
agentID, err := grpc.RegisterAgent(ctx, rpc.AgentInfo{
Version: "version",
Platform: "platform",
Backend: "backend",
Capacity: 2,
})
if !assert.NoError(t, err) {
return
}

View file

@ -172,7 +172,14 @@ func (s *WoodpeckerServer) Log(c context.Context, req *proto.LogRequest) (*proto
func (s *WoodpeckerServer) RegisterAgent(c context.Context, req *proto.RegisterAgentRequest) (*proto.RegisterAgentResponse, error) {
res := new(proto.RegisterAgentResponse)
agentID, err := s.peer.RegisterAgent(c, req.GetPlatform(), req.GetBackend(), req.GetVersion(), req.GetCapacity())
agentInfo := req.GetInfo()
agentID, err := s.peer.RegisterAgent(c, rpc.AgentInfo{
Version: agentInfo.GetVersion(),
Platform: agentInfo.GetPlatform(),
Backend: agentInfo.GetBackend(),
Capacity: int(agentInfo.GetCapacity()),
CustomLabels: agentInfo.GetCustomLabels(),
})
res.AgentId = agentID
return res, err
}

View file

@ -14,27 +14,73 @@
package model
import (
"encoding/base32"
"fmt"
"github.com/gorilla/securecookie"
)
type Agent struct {
ID int64 `json:"id" xorm:"pk autoincr 'id'"`
Created int64 `json:"created" xorm:"created"`
Updated int64 `json:"updated" xorm:"updated"`
Name string `json:"name" xorm:"name"`
OwnerID int64 `json:"owner_id" xorm:"'owner_id'"`
Token string `json:"token" xorm:"token"`
LastContact int64 `json:"last_contact" xorm:"last_contact"`
LastWork int64 `json:"last_work" xorm:"last_work"` // last time the agent did something, this value is used to determine if the agent is still doing work used by the autoscaler
Platform string `json:"platform" xorm:"VARCHAR(100) 'platform'"`
Backend string `json:"backend" xorm:"VARCHAR(100) 'backend'"`
Capacity int32 `json:"capacity" xorm:"capacity"`
Version string `json:"version" xorm:"'version'"`
NoSchedule bool `json:"no_schedule" xorm:"no_schedule"`
ID int64 `json:"id" xorm:"pk autoincr 'id'"`
Created int64 `json:"created" xorm:"created"`
Updated int64 `json:"updated" xorm:"updated"`
Name string `json:"name" xorm:"name"`
OwnerID int64 `json:"owner_id" xorm:"'owner_id'"`
Token string `json:"token" xorm:"token"`
LastContact int64 `json:"last_contact" xorm:"last_contact"`
LastWork int64 `json:"last_work" xorm:"last_work"` // last time the agent did something, this value is used to determine if the agent is still doing work used by the autoscaler
Platform string `json:"platform" xorm:"VARCHAR(100) 'platform'"`
Backend string `json:"backend" xorm:"VARCHAR(100) 'backend'"`
Capacity int32 `json:"capacity" xorm:"capacity"`
Version string `json:"version" xorm:"'version'"`
NoSchedule bool `json:"no_schedule" xorm:"no_schedule"`
CustomLabels map[string]string `json:"custom_labels" xorm:"JSON 'custom_labels'"`
// OrgID is counted as unset if set to -1, this is done to ensure a new(Agent) still enforce the OrgID check by default
OrgID int64 `json:"org_id" xorm:"INDEX 'org_id'"`
} // @name Agent
const (
IDNotSet = -1
agentFilterOrgID = "org-id"
)
// TableName return database table name for xorm.
func (Agent) TableName() string {
return "agents"
}
func (a *Agent) IsSystemAgent() bool {
return a.OwnerID == -1
return a.OwnerID == IDNotSet
}
func GenerateNewAgentToken() string {
return base32.StdEncoding.EncodeToString(securecookie.GenerateRandomKey(32))
}
func (a *Agent) GetServerLabels() (map[string]string, error) {
filters := make(map[string]string)
// enforce filters for user and organization agents
if a.OrgID != IDNotSet {
filters[agentFilterOrgID] = fmt.Sprintf("%d", a.OrgID)
} else {
filters[agentFilterOrgID] = "*"
}
return filters, nil
}
func (a *Agent) CanAccessRepo(repo *Repo) bool {
// global agent
if a.OrgID == IDNotSet {
return true
}
// agent has access to the organization
if a.OrgID == repo.OrgID {
return true
}
return false
}

View file

@ -0,0 +1,90 @@
// 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 model
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestGenerateNewAgentToken(t *testing.T) {
token1 := GenerateNewAgentToken()
token2 := GenerateNewAgentToken()
assert.NotEmpty(t, token1)
assert.NotEmpty(t, token2)
assert.NotEqual(t, token1, token2)
assert.Len(t, token1, 56)
}
func TestAgent_GetServerLabels(t *testing.T) {
t.Run("EmptyAgent", func(t *testing.T) {
agent := &Agent{}
filters, err := agent.GetServerLabels()
assert.NoError(t, err)
assert.Equal(t, map[string]string{
agentFilterOrgID: "0",
}, filters)
})
t.Run("GlobalAgent", func(t *testing.T) {
agent := &Agent{
OrgID: IDNotSet,
}
filters, err := agent.GetServerLabels()
assert.NoError(t, err)
assert.Equal(t, map[string]string{
agentFilterOrgID: "*",
}, filters)
})
t.Run("OrgAgent", func(t *testing.T) {
agent := &Agent{
OrgID: 123,
}
filters, err := agent.GetServerLabels()
assert.NoError(t, err)
assert.Equal(t, map[string]string{
agentFilterOrgID: "123",
}, filters)
})
}
func TestAgent_CanAccessRepo(t *testing.T) {
repo := &Repo{ID: 123, OrgID: 12}
otherRepo := &Repo{ID: 456, OrgID: 45}
t.Run("EmptyAgent", func(t *testing.T) {
agent := &Agent{}
assert.False(t, agent.CanAccessRepo(repo))
})
t.Run("GlobalAgent", func(t *testing.T) {
agent := &Agent{
OrgID: IDNotSet,
}
assert.True(t, agent.CanAccessRepo(repo))
})
t.Run("OrgAgent", func(t *testing.T) {
agent := &Agent{
OrgID: 12,
}
assert.True(t, agent.CanAccessRepo(repo))
assert.False(t, agent.CanAccessRepo(otherRepo))
})
}

View file

@ -1,25 +0,0 @@
// Copyright 2018 Drone.IO Inc.
//
// 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 model
// ResourceLimit is the resource limit to set on pipeline steps.
type ResourceLimit struct {
MemSwapLimit int64
MemLimit int64
ShmSize int64
CPUQuota int64
CPUShares int64
CPUSet string
}

View file

@ -41,6 +41,18 @@ func (t *Task) String() string {
return sb.String()
}
func (t *Task) ApplyLabelsFromRepo(r *Repo) error {
if r == nil {
return fmt.Errorf("repo is nil but needed to get task labels")
}
if t.Labels == nil {
t.Labels = make(map[string]string)
}
t.Labels["repo"] = r.FullName
t.Labels[agentFilterOrgID] = fmt.Sprintf("%d", r.OrgID)
return nil
}
// ShouldRun tells if a task should be run or skipped, based on dependencies.
func (t *Task) ShouldRun() bool {
if t.runsOnFailure() && t.runsOnSuccess() {

87
server/model/task_test.go Normal file
View file

@ -0,0 +1,87 @@
// 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 model
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestTask_GetLabels(t *testing.T) {
t.Run("Nil Repo", func(t *testing.T) {
task := &Task{}
err := task.ApplyLabelsFromRepo(nil)
assert.Error(t, err)
assert.Nil(t, task.Labels)
assert.EqualError(t, err, "repo is nil but needed to get task labels")
})
t.Run("Empty Repo", func(t *testing.T) {
task := &Task{}
repo := &Repo{}
err := task.ApplyLabelsFromRepo(repo)
assert.NoError(t, err)
assert.NotNil(t, task.Labels)
assert.Equal(t, map[string]string{
"repo": "",
agentFilterOrgID: "0",
}, task.Labels)
})
t.Run("Empty Labels", func(t *testing.T) {
task := &Task{}
repo := &Repo{
FullName: "test/repo",
ID: 123,
OrgID: 456,
}
err := task.ApplyLabelsFromRepo(repo)
assert.NoError(t, err)
assert.NotNil(t, task.Labels)
assert.Equal(t, map[string]string{
"repo": "test/repo",
agentFilterOrgID: "456",
}, task.Labels)
})
t.Run("Existing Labels", func(t *testing.T) {
task := &Task{
Labels: map[string]string{
"existing": "label",
},
}
repo := &Repo{
FullName: "test/repo",
ID: 123,
OrgID: 456,
}
err := task.ApplyLabelsFromRepo(repo)
assert.NoError(t, err)
assert.NotNil(t, task.Labels)
assert.Equal(t, map[string]string{
"existing": "label",
"repo": "test/repo",
agentFilterOrgID: "456",
}, task.Labels)
})
}

View file

@ -38,7 +38,7 @@ func parsePipeline(forge forge.Forge, store store.Store, currentPipeline *model.
}
// get the previous pipeline so that we can send status change notifications
last, err := store.GetPipelineLastBefore(repo, currentPipeline.Branch, currentPipeline.ID)
prev, err := store.GetPipelineLastBefore(repo, currentPipeline.Branch, currentPipeline.ID)
if err != nil && !errors.Is(err, sql.ErrNoRows) {
log.Error().Err(err).Str("repo", repo.FullName).Msgf("error getting last pipeline before pipeline number '%d'", currentPipeline.Number)
}
@ -74,7 +74,7 @@ func parsePipeline(forge forge.Forge, store store.Store, currentPipeline *model.
b := stepbuilder.StepBuilder{
Repo: repo,
Curr: currentPipeline,
Last: last,
Prev: prev,
Netrc: netrc,
Secs: secs,
Regs: regs,

View file

@ -18,6 +18,7 @@ import (
"context"
"encoding/json"
"fmt"
"maps"
"go.woodpecker-ci.org/woodpecker/v2/pipeline/rpc"
"go.woodpecker-ci.org/woodpecker/v2/server"
@ -31,18 +32,19 @@ func queuePipeline(ctx context.Context, repo *model.Repo, pipelineItems []*stepb
if item.Workflow.State == model.StatusSkipped {
continue
}
task := new(model.Task)
task.ID = fmt.Sprint(item.Workflow.ID)
task.Labels = map[string]string{}
for k, v := range item.Labels {
task.Labels[k] = v
task := &model.Task{
ID: fmt.Sprint(item.Workflow.ID),
Labels: make(map[string]string),
}
maps.Copy(task.Labels, item.Labels)
err := task.ApplyLabelsFromRepo(repo)
if err != nil {
return err
}
task.Labels["repo"] = repo.FullName
task.Dependencies = taskIDs(item.DependsOn, pipelineItems)
task.RunOn = item.RunsOn
task.DepStatus = make(map[string]model.StatusValue)
var err error
task.Data, err = json.Marshal(rpc.Workflow{
ID: fmt.Sprint(item.Workflow.ID),
Config: item.Config,

View file

@ -25,7 +25,7 @@ import (
)
// MetadataFromStruct return the metadata from a pipeline will run with.
func MetadataFromStruct(forge metadata.ServerForge, repo *model.Repo, pipeline, last *model.Pipeline, workflow *model.Workflow, sysURL string) metadata.Metadata {
func MetadataFromStruct(forge metadata.ServerForge, repo *model.Repo, pipeline, prev *model.Pipeline, workflow *model.Workflow, sysURL string) metadata.Metadata {
host := sysURL
uri, err := url.Parse(sysURL)
if err == nil {
@ -78,7 +78,7 @@ func MetadataFromStruct(forge metadata.ServerForge, repo *model.Repo, pipeline,
return metadata.Metadata{
Repo: fRepo,
Curr: metadataPipelineFromModelPipeline(pipeline, true),
Prev: metadataPipelineFromModelPipeline(last, false),
Prev: metadataPipelineFromModelPipeline(prev, false),
Workflow: fWorkflow,
Step: metadata.Step{},
Sys: metadata.System{

View file

@ -33,7 +33,7 @@ func TestMetadataFromStruct(t *testing.T) {
name string
forge metadata.ServerForge
repo *model.Repo
pipeline, last *model.Pipeline
pipeline, prev *model.Pipeline
workflow *model.Workflow
sysURL string
expectedMetadata metadata.Metadata
@ -63,7 +63,7 @@ func TestMetadataFromStruct(t *testing.T) {
forge: forge,
repo: &model.Repo{FullName: "testUser/testRepo", ForgeURL: "https://gitea.com/testUser/testRepo", Clone: "https://gitea.com/testUser/testRepo.git", CloneSSH: "git@gitea.com:testUser/testRepo.git", Branch: "main", IsSCMPrivate: true, SCMKind: "git"},
pipeline: &model.Pipeline{Number: 3, ChangedFiles: []string{"test.go", "markdown file.md"}},
last: &model.Pipeline{Number: 2},
prev: &model.Pipeline{Number: 2},
workflow: &model.Workflow{Name: "hello"},
sysURL: "https://example.com",
expectedMetadata: metadata.Metadata{
@ -98,7 +98,7 @@ func TestMetadataFromStruct(t *testing.T) {
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
result := MetadataFromStruct(testCase.forge, testCase.repo, testCase.pipeline, testCase.last, testCase.workflow, testCase.sysURL)
result := MetadataFromStruct(testCase.forge, testCase.repo, testCase.pipeline, testCase.prev, testCase.workflow, testCase.sysURL)
assert.EqualValues(t, testCase.expectedMetadata, result)
assert.EqualValues(t, testCase.expectedEnviron, result.Environ())
})

Some files were not shown because too many files have changed in this diff Show more