Compare commits

...

14 commits

Author SHA1 Message Date
6543 7f09b921f9
Merge 5dc778e80d into 2d66cfcce2 2024-04-26 11:54:37 -03:00
Robert Kaussow 2d66cfcce2
Split client into multiple files and add more tests (#3647)
All the client functions were in a single file, which was already very
long, and the test file gets even longer as more tests are added. I
split it into separate files representing the API path and started
adding some tests.
2024-04-26 13:46:55 +02:00
Robert Kaussow daf673a857
Add make target for spellcheck (#3648) 2024-04-26 07:51:10 +02:00
Anbraten 5dc778e80d Merge remote-tracking branch 'upstream/main' into pr/6543/3539 2024-04-17 13:00:56 +02:00
Anbraten cdb44f7c7f apply and check permission labels 2024-04-17 13:00:13 +02:00
6543 ef2929e3bd
Update server/model/agent.go
Co-authored-by: Anbraten <6918444+anbraten@users.noreply.github.com>
2024-03-29 13:04:40 +01:00
6543 65a821a956 try to have an PoC for an bad agent
to make sure nothing breaks or leaks
2024-03-29 11:33:30 +01:00
6543 b68761f3c1
more todos 2024-03-29 11:32:37 +01:00
6543 ad96a2dbe5 finish-wip 2024-03-29 10:31:29 +01:00
6543 44e026e843 wip 2024-03-26 11:18:26 +01:00
6543 43ada66dde allow glob expresion in pipeline config for labels 2024-03-26 11:02:00 +01:00
6543 5197af3ddc add indixes to db for speedups 2024-03-26 10:54:54 +01:00
6543 0339d58136 Merge branch 'main' into server-side-agent-filters 2024-03-26 10:48:33 +01:00
6543 414ee1d371
start imp. server side set filters for agents
will allow https://github.com/woodpecker-ci/woodpecker/issues/267

TODOs: API to set server-side-agent-filters; WebUI; secure all rpc endpoints; secure set of labels for non-admins
2024-03-23 22:20:24 +01:00
28 changed files with 1710 additions and 685 deletions

2
.gitignore vendored
View file

@ -13,7 +13,7 @@
*.so
*.dylib
vendor/
__debug_bin
__debug_bin*
# Test binary, built with `go test -c`
*.test

View file

@ -306,6 +306,10 @@ bundle-cli: bundle-prepare ## Create bundles for cli
.PHONY: bundle
bundle: bundle-agent bundle-server bundle-cli ## Create all bundles
.PHONY: spellcheck
spellcheck:
pnpx cspell lint --no-progress --gitignore '{**,.*}/{*,.*}'
##@ Docs
.PHONY: docs
docs: ## Generate docs (currently only for the cli)

View file

@ -37,14 +37,11 @@ import (
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/status"
"go.woodpecker-ci.org/woodpecker/v2/agent"
agentRpc "go.woodpecker-ci.org/woodpecker/v2/agent/rpc"
"go.woodpecker-ci.org/woodpecker/v2/pipeline/backend"
"go.woodpecker-ci.org/woodpecker/v2/pipeline/backend/types"
"go.woodpecker-ci.org/woodpecker/v2/pipeline/rpc"
"go.woodpecker-ci.org/woodpecker/v2/shared/logger"
"go.woodpecker-ci.org/woodpecker/v2/shared/utils"
"go.woodpecker-ci.org/woodpecker/v2/version"
)
func run(c *cli.Context, backends []types.Backend) error {
@ -112,9 +109,12 @@ func run(c *cli.Context, backends []types.Backend) error {
client := agentRpc.NewGrpcClient(conn)
sigterm := abool.New()
// HEHE we are an bad agent
ctx := metadata.NewOutgoingContext(
context.Background(),
metadata.Pairs("hostname", hostname),
metadata.Pairs("hostname", hostname, "agent_id", ""),
)
agentConfigPersisted := abool.New()
@ -151,43 +151,12 @@ func run(c *cli.Context, backends []types.Backend) error {
parallel := c.Int("max-workflows")
wg.Add(parallel)
// new engine
backendCtx := context.WithValue(ctx, types.CliContext, c)
backendName := c.String("backend-engine")
backendEngine, err := backend.FindBackend(backendCtx, backends, backendName)
if err != nil {
log.Error().Err(err).Msgf("cannot find backend engine '%s'", backendName)
return err
}
if !backendEngine.IsAvailable(backendCtx) {
log.Error().Str("engine", backendEngine.Name()).Msg("selected backend engine is unavailable")
return fmt.Errorf("selected backend engine %s is unavailable", backendEngine.Name())
}
// load engine (e.g. init api client)
engInfo, err := backendEngine.Load(backendCtx)
if err != nil {
log.Error().Err(err).Msg("cannot load backend engine")
return err
}
log.Debug().Msgf("loaded %s backend engine", backendEngine.Name())
agentConfig.AgentID, err = client.RegisterAgent(ctx, engInfo.Platform, backendEngine.Name(), version.String(), parallel)
if err != nil {
return err
}
if agentConfigPath != "" {
if err := writeAgentConfig(agentConfig, agentConfigPath); err == nil {
agentConfigPersisted.Set()
}
}
// HEHE we are an bad agent
labels := map[string]string{
"hostname": hostname,
"platform": engInfo.Platform,
"backend": backendEngine.Name(),
"repo": "*", // allow all repos by default
"hostname": "*",
"platform": "*",
"backend": "*",
}
if err := stringSliceAddToMap(c.StringSlice("filter"), labels); err != nil {
@ -198,54 +167,20 @@ func run(c *cli.Context, backends []types.Backend) error {
Labels: labels,
}
log.Debug().Msgf("agent registered with ID %d", agentConfig.AgentID)
go func() {
for {
if sigterm.IsSet() {
log.Debug().Msg("terminating health reporting")
return
for {
fmt.Println("I'm a bad agent hehe")
work, err := client.Next(ctx, filter)
if err != nil {
fmt.Printf("ERROR: %s", err)
} else {
fmt.Println("sweet sweet secrets")
for _, s := range work.Config.Secrets {
fmt.Printf("name: '%s' value: '%s'\n", s.Name, s.Value)
}
err := client.ReportHealth(ctx)
if err != nil {
log.Err(err).Msg("failed to report health")
return
}
<-time.After(time.Second * 10)
}
}()
for i := 0; i < parallel; i++ {
i := i
go func() {
defer wg.Done()
r := agent.NewRunner(client, filter, hostname, counter, &backendEngine)
log.Debug().Msgf("created new runner %d", i)
for {
if sigterm.IsSet() {
log.Debug().Msgf("terminating runner %d", i)
return
}
log.Debug().Msg("polling new steps")
if err := r.Run(ctx); err != nil {
log.Error().Err(err).Msg("pipeline done with error")
return
}
}
}()
time.Sleep(time.Second)
}
log.Info().Msgf(
"starting Woodpecker agent with version '%s' and backend '%s' using platform '%s' running up to %d pipelines in parallel",
version.String(), backendEngine.Name(), engInfo.Platform, parallel)
wg.Wait()
return nil
}
func runWithRetry(backendEngines []types.Backend) func(context *cli.Context) error {

View file

@ -20,6 +20,7 @@ RUN apk add --no-cache --update make=${MAKE_VERSION} gcc=${GCC_VERSION} binutils
COPY --from=golang_image /usr/local/go /usr/local/go
COPY Makefile /
ENV PATH=$PATH:/usr/local/go/bin
ENV COREPACK_ENABLE_DOWNLOAD_PROMPT=0
# Cache tools
RUN GOBIN=/usr/local/go/bin make install-tools && \

View file

@ -62,7 +62,7 @@ func (s *WoodpeckerAuthServer) getAgent(agentID int64, agentToken string) (*mode
if agentToken == s.agentMasterToken && agentID == -1 {
agent := new(model.Agent)
agent.Name = ""
agent.OwnerID = -1 // system agent
agent.OrgID = -1 // system agent
agent.Token = s.agentMasterToken
agent.Backend = ""
agent.Platform = ""
@ -89,5 +89,8 @@ func (s *WoodpeckerAuthServer) getAgent(agentID int64, agentToken string) (*mode
if err != nil && errors.Is(err, types.RecordNotExist) {
return nil, fmt.Errorf("individual agent not found by token: %w", err)
}
// TODO: check if an agent can still pretend to be an other one
return agent, err
}

View file

@ -15,6 +15,9 @@
package grpc
import (
"github.com/bmatcuk/doublestar/v4"
"github.com/rs/zerolog/log"
"go.woodpecker-ci.org/woodpecker/v2/pipeline/rpc"
"go.woodpecker-ci.org/woodpecker/v2/server/model"
"go.woodpecker-ci.org/woodpecker/v2/server/queue"
@ -39,9 +42,11 @@ func createFilterFunc(agentFilter rpc.Filter) queue.FilterFn {
continue
}
if taskLabelValue != agentLabelValue {
return false
match, err := doublestar.Match(taskLabelValue, agentLabelValue)
if err != nil {
log.Error().Err(err).Msg("got unexpected error while try to match task and agent lable value")
}
return match
}
return true
}

View file

@ -56,8 +56,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
@ -68,6 +66,18 @@ func (s *RPC) Next(c context.Context, agentFilter rpc.Filter) (*rpc.Workflow, er
return nil, nil
}
// enforce server set agent filters
agentFilters, err := agent.GetFilters()
if err != nil {
return nil, err
}
for k, v := range agentFilters {
agentFilter.Labels[k] = v
}
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)
@ -95,11 +105,11 @@ func (s *RPC) Wait(c context.Context, id string) error {
// Extend implements the rpc.Extend function
func (s *RPC) Extend(c context.Context, id string) error {
return s.queue.Extend(c, id)
}
agent, err := s.getAgentFromContext(c)
if err != nil {
return err
}
// Update implements the rpc.Update function
func (s *RPC) Update(_ context.Context, id string, state rpc.State) error {
workflowID, err := strconv.ParseInt(id, 10, 64)
if err != nil {
return err
@ -117,6 +127,47 @@ func (s *RPC) Update(_ context.Context, id string, state rpc.State) error {
return err
}
repo, err := s.store.GetRepo(currentPipeline.RepoID)
if err != nil {
log.Error().Err(err).Msgf("cannot find repo with id %d", currentPipeline.RepoID)
return err
}
if !agent.CanAccessRepo(repo) {
msg := fmt.Sprintf("agent '%d' is not allowed to interact with repo '%s'", agent.ID, repo.FullName)
log.Error().
Int64("repoId", repo.ID).
Msg(msg)
return fmt.Errorf(msg)
}
return s.queue.Extend(c, agent.ID, id)
}
// Update implements the rpc.Update function
func (s *RPC) Update(c context.Context, id string, state rpc.State) error {
workflowID, err := strconv.ParseInt(id, 10, 64)
if err != nil {
return err
}
workflow, err := s.store.WorkflowLoad(workflowID)
if err != nil {
log.Error().Err(err).Msgf("rpc.update: cannot find workflow with id %d", workflowID)
return err
}
currentPipeline, err := s.store.GetPipeline(workflow.PipelineID)
if err != nil {
log.Error().Err(err).Msgf("cannot find pipeline with id %d", workflow.PipelineID)
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)
@ -138,6 +189,14 @@ func (s *RPC) Update(_ context.Context, id string, state rpc.State) error {
return err
}
if !agent.CanAccessRepo(repo) {
msg := fmt.Sprintf("agent '%d' is not allowed to interact with repo '%s'", agent.ID, repo.FullName)
log.Error().
Int64("repoId", repo.ID).
Msg(msg)
return fmt.Errorf(msg)
}
if err := pipeline.UpdateStepStatus(s.store, step, state); err != nil {
log.Error().Err(err).Msg("rpc.update: cannot update step")
}
@ -181,6 +240,7 @@ func (s *RPC) Init(c context.Context, id string, state rpc.State) error {
if err != nil {
return err
}
workflow.AgentID = agent.ID
currentPipeline, err := s.store.GetPipeline(workflow.PipelineID)
@ -195,6 +255,14 @@ func (s *RPC) Init(c context.Context, id string, state rpc.State) error {
return err
}
if !agent.CanAccessRepo(repo) {
msg := fmt.Sprintf("agent '%d' is not allowed to interact with repo '%s'", agent.ID, repo.FullName)
log.Error().
Int64("repoId", repo.ID).
Msg(msg)
return fmt.Errorf(msg)
}
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 build_id %d state", currentPipeline.ID)
@ -260,6 +328,19 @@ func (s *RPC) Done(c context.Context, id string, state rpc.State) error {
return err
}
agent, err := s.getAgentFromContext(c)
if err != nil {
return err
}
if !agent.CanAccessRepo(repo) {
msg := fmt.Sprintf("agent '%d' is not allowed to interact with repo '%s'", agent.ID, repo.FullName)
log.Error().
Int64("repoId", repo.ID).
Msg(msg)
return fmt.Errorf(msg)
}
logger := log.With().
Str("repo_id", fmt.Sprint(repo.ID)).
Str("pipeline_id", fmt.Sprint(currentPipeline.ID)).
@ -326,6 +407,32 @@ func (s *RPC) Log(c context.Context, _logEntry *rpc.LogEntry) error {
if err != nil {
return fmt.Errorf("could not find step with uuid %s in store: %w", _logEntry.StepUUID, err)
}
agent, err := s.getAgentFromContext(c)
if err != nil {
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
}
repo, err := s.store.GetRepo(currentPipeline.RepoID)
if err != nil {
log.Error().Err(err).Msgf("cannot find repo with id %d", currentPipeline.RepoID)
return err
}
if !agent.CanAccessRepo(repo) {
msg := fmt.Sprintf("agent '%d' is not allowed to interact with repo '%s'", agent.ID, repo.FullName)
log.Error().
Int64("repoId", repo.ID).
Msg(msg)
return fmt.Errorf(msg)
}
logEntry := &model.LogEntry{
StepID: step.ID,
Time: _logEntry.Time,

View file

@ -38,7 +38,7 @@ func TestRegisterAgent(t *testing.T) {
Created: 0,
Updated: 0,
Name: "hostname",
OwnerID: 0,
OrgID: 0,
Token: "",
LastContact: 0,
Platform: "platform",
@ -78,7 +78,7 @@ func TestRegisterAgent(t *testing.T) {
Created: 0,
Updated: 0,
Name: "originalHostname",
OwnerID: 0,
OrgID: 0,
Token: "",
LastContact: 0,
Platform: "platform",

View file

@ -14,12 +14,17 @@
package model
import (
"errors"
"fmt"
)
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'"`
OrgID int64 `json:"owner_id" xorm:"'owner_id'"` // TODO: rename to org_id
Token string `json:"token" xorm:"token"`
LastContact int64 `json:"last_contact" xorm:"last_contact"`
Platform string `json:"platform" xorm:"VARCHAR(100) 'platform'"`
@ -27,6 +32,8 @@ type Agent struct {
Capacity int32 `json:"capacity" xorm:"capacity"`
Version string `json:"version" xorm:"'version'"`
NoSchedule bool `json:"no_schedule" xorm:"no_schedule"`
// Server side enforced agent filters
Filters map[string]string `json:"filters" xorm:"'filters' json"`
} // @name Agent
// TableName return database table name for xorm
@ -35,5 +42,37 @@ func (Agent) TableName() string {
}
func (a *Agent) IsSystemAgent() bool {
return a.OwnerID == -1
return a.OrgID == -1
}
var ErrFiltersBroken = errors.New("while creating filters map error ocured")
func (a *Agent) GetFilters() (map[string]string, error) {
filters := a.Filters
if filters == nil {
filters = make(map[string]string)
}
// enforce filters for user and organization agents
if a.IsSystemAgent() {
filters["org-id"] = "*" // allow all orgs
filters["repo-id"] = "*" // allow all repos
} else if a.OrgID > 0 {
filters["org-id"] = fmt.Sprintf("%d", a.OrgID)
filters["repo-id"] = "*" // allow all repos of the org
}
return filters, nil
}
func (a *Agent) CanAccessRepo(repo *Repo) bool {
if a.IsSystemAgent() {
return true
}
if a.OrgID == repo.OrgID {
return true
}
return false
}

View file

@ -23,13 +23,13 @@ import (
// Repo represents a repository.
type Repo struct {
ID int64 `json:"id,omitempty" xorm:"pk autoincr 'repo_id'"`
UserID int64 `json:"-" xorm:"repo_user_id"`
UserID int64 `json:"-" xorm:"INDEX repo_user_id"`
ForgeID int64 `json:"forge_id,omitempty" xorm:"forge_id"`
// ForgeRemoteID is the unique identifier for the repository on the forge.
ForgeRemoteID ForgeRemoteID `json:"forge_remote_id" xorm:"forge_remote_id"`
OrgID int64 `json:"org_id" xorm:"repo_org_id"`
Owner string `json:"owner" xorm:"UNIQUE(name) 'repo_owner'"`
Name string `json:"name" xorm:"UNIQUE(name) 'repo_name'"`
OrgID int64 `json:"org_id" xorm:"INDEX repo_org_id"`
Owner string `json:"owner" xorm:"INDEX UNIQUE(name) 'repo_owner'"`
Name string `json:"name" xorm:"INDEX UNIQUE(name) 'repo_name'"`
FullName string `json:"full_name" xorm:"UNIQUE 'repo_full_name'"`
Avatar string `json:"avatar_url,omitempty" xorm:"varchar(500) 'repo_avatar'"`
ForgeURL string `json:"forge_url,omitempty" xorm:"varchar(1000) 'repo_forge_url'"`

View file

@ -37,7 +37,7 @@ func queuePipeline(ctx context.Context, repo *model.Repo, pipelineItems []*stepb
for k, v := range item.Labels {
task.Labels[k] = v
}
task.Labels["repo"] = repo.FullName
enforcedLabels(task, repo)
task.Dependencies = taskIDs(item.DependsOn, pipelineItems)
task.RunOn = item.RunsOn
task.DepStatus = make(map[string]model.StatusValue)
@ -57,6 +57,13 @@ func queuePipeline(ctx context.Context, repo *model.Repo, pipelineItems []*stepb
return server.Config.Services.Queue.PushAtOnce(ctx, tasks)
}
func enforcedLabels(task *model.Task, repo *model.Repo) {
task.Labels["repo"] = repo.FullName
task.Labels["owner"] = repo.Owner
task.Labels["repo-id"] = fmt.Sprint(repo.ID)
task.Labels["org-id"] = fmt.Sprint(repo.OrgID)
}
func taskIDs(dependsOn []string, pipelineItems []*stepbuilder.Item) (taskIDs []string) {
for _, dep := range dependsOn {
for _, pipelineItem := range pipelineItems {

View file

@ -187,12 +187,16 @@ func (q *fifo) Wait(c context.Context, id string) error {
}
// Extend extends the task execution deadline.
func (q *fifo) Extend(_ context.Context, id string) error {
func (q *fifo) Extend(_ context.Context, agentID int64, id string) error {
q.Lock()
defer q.Unlock()
state, ok := q.running[id]
if ok {
if state.item.AgentID != agentID {
return ErrAgentMissmatch
}
state.deadline = time.Now().Add(q.extension)
return nil
}

View file

@ -28,6 +28,9 @@ var (
// ErrNotFound indicates the task was not found in the queue.
ErrNotFound = errors.New("queue: task not found")
// ErrAgentMissmatch indicates an agent does not mat to the task
ErrAgentMissmatch = errors.New("task has not expected agent id")
)
// InfoT provides runtime information.
@ -79,7 +82,7 @@ type Queue interface {
Poll(c context.Context, agentID int64, f FilterFn) (*model.Task, error)
// Extend extends the deadline for a task.
Extend(c context.Context, id string) error
Extend(c context.Context, agentID int64, id string) error
// Done signals the task is complete.
Done(c context.Context, id string, exitStatus model.StatusValue) error

View file

@ -0,0 +1,50 @@
package woodpecker
import "fmt"
const (
pathAgents = "%s/api/agents"
pathAgent = "%s/api/agents/%d"
pathAgentTasks = "%s/api/agents/%d/tasks"
)
// AgentCreate creates a new agent.
func (c *client) AgentCreate(in *Agent) (*Agent, error) {
out := new(Agent)
uri := fmt.Sprintf(pathAgents, c.addr)
return out, c.post(uri, in, out)
}
// AgentList returns a list of all registered agents.
func (c *client) AgentList() ([]*Agent, error) {
out := make([]*Agent, 0, 5)
uri := fmt.Sprintf(pathAgents, c.addr)
return out, c.get(uri, &out)
}
// Agent returns an agent by id.
func (c *client) Agent(agentID int64) (*Agent, error) {
out := new(Agent)
uri := fmt.Sprintf(pathAgent, c.addr, agentID)
return out, c.get(uri, out)
}
// AgentUpdate updates the agent with the provided Agent struct.
func (c *client) AgentUpdate(in *Agent) (*Agent, error) {
out := new(Agent)
uri := fmt.Sprintf(pathAgent, c.addr, in.ID)
return out, c.patch(uri, in, out)
}
// AgentDelete deletes the agent with the given id.
func (c *client) AgentDelete(agentID int64) error {
uri := fmt.Sprintf(pathAgent, c.addr, agentID)
return c.delete(uri)
}
// AgentTasksList returns a list of all tasks for the agent with the given id.
func (c *client) AgentTasksList(agentID int64) ([]*Task, error) {
out := make([]*Task, 0, 5)
uri := fmt.Sprintf(pathAgentTasks, c.addr, agentID)
return out, c.get(uri, &out)
}

View file

@ -0,0 +1,511 @@
package woodpecker
import (
"fmt"
"net/http"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/assert"
)
func TestClient_AgentCreate(t *testing.T) {
tests := []struct {
name string
handler http.HandlerFunc
input *Agent
expected *Agent
wantErr bool
}{
{
name: "success",
handler: func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
w.WriteHeader(http.StatusCreated)
_, err := fmt.Fprint(w, `{"id":1,"name":"new_agent","backend":"local","capacity":2,"version":"1.0.0"}`)
assert.NoError(t, err)
},
input: &Agent{Name: "new_agent", Backend: "local", Capacity: 2, Version: "1.0.0"},
expected: &Agent{ID: 1, Name: "new_agent", Backend: "local", Capacity: 2, Version: "1.0.0"},
wantErr: false,
},
{
name: "invalid input",
handler: func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
w.WriteHeader(http.StatusBadRequest)
},
input: &Agent{},
expected: nil,
wantErr: true,
},
{
name: "server error",
handler: func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
w.WriteHeader(http.StatusInternalServerError)
},
input: &Agent{Name: "new_agent", Backend: "local", Capacity: 2, Version: "1.0.0"},
expected: nil,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ts := httptest.NewServer(tt.handler)
defer ts.Close()
client := NewClient(ts.URL, http.DefaultClient)
agent, err := client.AgentCreate(tt.input)
if tt.wantErr {
assert.Error(t, err)
return
}
assert.NoError(t, err)
assert.Equal(t, agent, tt.expected)
})
}
}
func TestClient_AgentList(t *testing.T) {
tests := []struct {
name string
handler http.HandlerFunc
expected []*Agent
wantErr bool
}{
{
name: "success",
handler: func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK)
_, err := fmt.Fprint(w, `[
{
"id": 1,
"name": "agent-1",
"backend": "local",
"capacity": 2,
"version": "1.0.0"
},
{
"id": 2,
"name": "agent-2",
"backend": "kubernetes",
"capacity": 4,
"version": "1.0.0"
}
]`)
assert.NoError(t, err)
},
expected: []*Agent{
{
ID: 1,
Name: "agent-1",
Backend: "local",
Capacity: 2,
Version: "1.0.0",
},
{
ID: 2,
Name: "agent-2",
Backend: "kubernetes",
Capacity: 4,
Version: "1.0.0",
},
},
wantErr: false,
},
{
name: "server error",
handler: func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusInternalServerError)
},
expected: nil,
wantErr: true,
},
{
name: "invalid response",
handler: func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK)
_, err := fmt.Fprint(w, `invalid json`)
assert.NoError(t, err)
},
expected: nil,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ts := httptest.NewServer(tt.handler)
defer ts.Close()
client := NewClient(ts.URL, http.DefaultClient)
agents, err := client.AgentList()
if tt.wantErr {
assert.Error(t, err)
return
}
assert.NoError(t, err)
assert.Equal(t, tt.expected, agents)
})
}
}
func TestClient_Agent(t *testing.T) {
tests := []struct {
name string
handler http.HandlerFunc
agentID int64
expected *Agent
wantErr bool
}{
{
name: "success",
handler: func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
w.WriteHeader(http.StatusOK)
_, err := fmt.Fprint(w, `{"id":1,"name":"agent-1","backend":"local","capacity":2,"version":"1.0.0"}`)
assert.NoError(t, err)
},
agentID: 1,
expected: &Agent{ID: 1, Name: "agent-1", Backend: "local", Capacity: 2, Version: "1.0.0"},
wantErr: false,
},
{
name: "not found",
handler: func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
w.WriteHeader(http.StatusNotFound)
},
agentID: 999,
expected: nil,
wantErr: true,
},
{
name: "server error",
handler: func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
w.WriteHeader(http.StatusInternalServerError)
},
agentID: 1,
expected: nil,
wantErr: true,
},
{
name: "invalid response",
handler: func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
w.WriteHeader(http.StatusOK)
_, err := fmt.Fprint(w, `invalid json`)
assert.NoError(t, err)
},
agentID: 1,
expected: nil,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ts := httptest.NewServer(tt.handler)
defer ts.Close()
client := NewClient(ts.URL, http.DefaultClient)
agent, err := client.Agent(tt.agentID)
if tt.wantErr {
assert.Error(t, err)
return
}
assert.NoError(t, err)
assert.Equal(t, tt.expected, agent)
})
}
}
func TestClient_AgentUpdate(t *testing.T) {
tests := []struct {
name string
handler http.HandlerFunc
input *Agent
expected *Agent
wantErr bool
}{
{
name: "success",
handler: func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPatch {
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
w.WriteHeader(http.StatusOK)
_, err := fmt.Fprint(w, `{"id":1,"name":"updated_agent"}`)
assert.NoError(t, err)
},
input: &Agent{ID: 1, Name: "existing_agent"},
expected: &Agent{ID: 1, Name: "updated_agent"},
wantErr: false,
},
{
name: "not found",
handler: func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPatch {
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
w.WriteHeader(http.StatusNotFound)
},
input: &Agent{ID: 999, Name: "nonexistent_agent"},
expected: nil,
wantErr: true,
},
{
name: "invalid input",
handler: func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPatch {
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
w.WriteHeader(http.StatusBadRequest)
},
input: &Agent{},
expected: nil,
wantErr: true,
},
{
name: "server error",
handler: func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPatch {
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
w.WriteHeader(http.StatusInternalServerError)
},
input: &Agent{ID: 1, Name: "existing_agent"},
expected: nil,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ts := httptest.NewServer(tt.handler)
defer ts.Close()
client := NewClient(ts.URL, http.DefaultClient)
agent, err := client.AgentUpdate(tt.input)
if tt.wantErr {
assert.Error(t, err)
return
}
assert.NoError(t, err)
assert.Equal(t, agent, tt.expected)
})
}
}
func TestClient_AgentDelete(t *testing.T) {
tests := []struct {
name string
handler http.HandlerFunc
agentID int64
wantErr bool
}{
{
name: "success",
handler: func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodDelete {
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
w.WriteHeader(http.StatusOK)
},
agentID: 1,
wantErr: false,
},
{
name: "not found",
handler: func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodDelete {
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
w.WriteHeader(http.StatusNotFound)
},
agentID: 999,
wantErr: true,
},
{
name: "server error",
handler: func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodDelete {
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
w.WriteHeader(http.StatusInternalServerError)
},
agentID: 1,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ts := httptest.NewServer(tt.handler)
defer ts.Close()
client := NewClient(ts.URL, http.DefaultClient)
err := client.AgentDelete(tt.agentID)
if tt.wantErr {
assert.Error(t, err)
return
}
assert.NoError(t, err)
})
}
}
func TestClient_AgentTasksList(t *testing.T) {
tests := []struct {
name string
handler http.HandlerFunc
agentID int64
expected []*Task
wantErr bool
}{
{
name: "success",
handler: func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
w.WriteHeader(http.StatusOK)
_, err := fmt.Fprint(w, `[
{
"id": "4696",
"data": "",
"labels": {
"platform": "linux/amd64",
"repo": "woodpecker-ci/woodpecker"
}
},
{
"id": "4697",
"data": "",
"labels": {
"platform": "linux/arm64",
"repo": "woodpecker-ci/woodpecker"
}
}
]`)
assert.NoError(t, err)
},
agentID: 1,
expected: []*Task{
{
ID: "4696",
Data: []byte{},
Labels: map[string]string{
"platform": "linux/amd64",
"repo": "woodpecker-ci/woodpecker",
},
},
{
ID: "4697",
Data: []byte{},
Labels: map[string]string{
"platform": "linux/arm64",
"repo": "woodpecker-ci/woodpecker",
},
},
},
wantErr: false,
},
{
name: "not found",
handler: func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
w.WriteHeader(http.StatusNotFound)
},
agentID: 999,
expected: nil,
wantErr: true,
},
{
name: "server error",
handler: func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
w.WriteHeader(http.StatusInternalServerError)
},
agentID: 1,
expected: nil,
wantErr: true,
},
{
name: "invalid response",
handler: func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
w.WriteHeader(http.StatusOK)
_, err := fmt.Fprint(w, `invalid json`)
assert.NoError(t, err)
},
agentID: 1,
expected: nil,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ts := httptest.NewServer(tt.handler)
defer ts.Close()
client := NewClient(ts.URL, http.DefaultClient)
tasks, err := client.AgentTasksList(tt.agentID)
if tt.wantErr {
assert.Error(t, err)
return
}
assert.NoError(t, err)
assert.Equal(t, tt.expected, tasks)
})
}
}

View file

@ -26,41 +26,8 @@ import (
)
const (
pathSelf = "%s/api/user"
pathRepos = "%s/api/user/repos"
pathRepoPost = "%s/api/repos?forge_remote_id=%d"
pathRepo = "%s/api/repos/%d"
pathRepoLookup = "%s/api/repos/lookup/%s"
pathRepoMove = "%s/api/repos/%d/move?to=%s"
pathChown = "%s/api/repos/%d/chown"
pathRepair = "%s/api/repos/%d/repair"
pathPipelines = "%s/api/repos/%d/pipelines"
pathPipeline = "%s/api/repos/%d/pipelines/%v"
pathPipelineLogs = "%s/api/repos/%d/logs/%d"
pathStepLogs = "%s/api/repos/%d/logs/%d/%d"
pathApprove = "%s/api/repos/%d/pipelines/%d/approve"
pathDecline = "%s/api/repos/%d/pipelines/%d/decline"
pathStop = "%s/api/repos/%d/pipelines/%d/cancel"
pathRepoSecrets = "%s/api/repos/%d/secrets"
pathRepoSecret = "%s/api/repos/%d/secrets/%s"
pathRepoRegistries = "%s/api/repos/%d/registry"
pathRepoRegistry = "%s/api/repos/%d/registry/%s"
pathRepoCrons = "%s/api/repos/%d/cron"
pathRepoCron = "%s/api/repos/%d/cron/%d"
pathOrg = "%s/api/orgs/%d"
pathOrgLookup = "%s/api/orgs/lookup/%s"
pathOrgSecrets = "%s/api/orgs/%d/secrets"
pathOrgSecret = "%s/api/orgs/%d/secrets/%s"
pathGlobalSecrets = "%s/api/secrets"
pathGlobalSecret = "%s/api/secrets/%s"
pathUsers = "%s/api/users"
pathUser = "%s/api/users/%s"
pathPipelineQueue = "%s/api/pipelines"
pathQueue = "%s/api/queue"
pathLogLevel = "%s/api/log-level"
pathAgents = "%s/api/agents"
pathAgent = "%s/api/agents/%d"
pathAgentTasks = "%s/api/agents/%d/tasks"
pathLogLevel = "%s/api/log-level"
// TODO: implement endpoints
// pathFeed = "%s/api/user/feed"
// pathVersion = "%s/version"
@ -91,422 +58,6 @@ func (c *client) SetAddress(addr string) {
c.addr = addr
}
// Self returns the currently authenticated user.
func (c *client) Self() (*User, error) {
out := new(User)
uri := fmt.Sprintf(pathSelf, c.addr)
err := c.get(uri, out)
return out, err
}
// User returns a user by login.
func (c *client) User(login string) (*User, error) {
out := new(User)
uri := fmt.Sprintf(pathUser, c.addr, login)
err := c.get(uri, out)
return out, err
}
// UserList returns a list of all registered users.
func (c *client) UserList() ([]*User, error) {
var out []*User
uri := fmt.Sprintf(pathUsers, c.addr)
err := c.get(uri, &out)
return out, err
}
// UserPost creates a new user account.
func (c *client) UserPost(in *User) (*User, error) {
out := new(User)
uri := fmt.Sprintf(pathUsers, c.addr)
err := c.post(uri, in, out)
return out, err
}
// UserPatch updates a user account.
func (c *client) UserPatch(in *User) (*User, error) {
out := new(User)
uri := fmt.Sprintf(pathUser, c.addr, in.Login)
err := c.patch(uri, in, out)
return out, err
}
// UserDel deletes a user account.
func (c *client) UserDel(login string) error {
uri := fmt.Sprintf(pathUser, c.addr, login)
err := c.delete(uri)
return err
}
// Repo returns a repository by id.
func (c *client) Repo(repoID int64) (*Repo, error) {
out := new(Repo)
uri := fmt.Sprintf(pathRepo, c.addr, repoID)
err := c.get(uri, out)
return out, err
}
// RepoLookup returns a repository by name.
func (c *client) RepoLookup(fullName string) (*Repo, error) {
out := new(Repo)
uri := fmt.Sprintf(pathRepoLookup, c.addr, fullName)
err := c.get(uri, out)
return out, err
}
// RepoList returns a list of all repositories to which
// the user has explicit access in the host system.
func (c *client) RepoList() ([]*Repo, error) {
var out []*Repo
uri := fmt.Sprintf(pathRepos, c.addr)
err := c.get(uri, &out)
return out, err
}
// RepoListOpts returns a list of all repositories to which
// the user has explicit access in the host system.
func (c *client) RepoListOpts(all bool) ([]*Repo, error) {
var out []*Repo
uri := fmt.Sprintf(pathRepos+"?all=%v", c.addr, all)
err := c.get(uri, &out)
return out, err
}
// RepoPost activates a repository.
func (c *client) RepoPost(forgeRemoteID int64) (*Repo, error) {
out := new(Repo)
uri := fmt.Sprintf(pathRepoPost, c.addr, forgeRemoteID)
err := c.post(uri, nil, out)
return out, err
}
// RepoChown updates a repository owner.
func (c *client) RepoChown(repoID int64) (*Repo, error) {
out := new(Repo)
uri := fmt.Sprintf(pathChown, c.addr, repoID)
err := c.post(uri, nil, out)
return out, err
}
// RepoRepair repairs the repository hooks.
func (c *client) RepoRepair(repoID int64) error {
uri := fmt.Sprintf(pathRepair, c.addr, repoID)
return c.post(uri, nil, nil)
}
// RepoPatch updates a repository.
func (c *client) RepoPatch(repoID int64, in *RepoPatch) (*Repo, error) {
out := new(Repo)
uri := fmt.Sprintf(pathRepo, c.addr, repoID)
err := c.patch(uri, in, out)
return out, err
}
// RepoDel deletes a repository.
func (c *client) RepoDel(repoID int64) error {
uri := fmt.Sprintf(pathRepo, c.addr, repoID)
err := c.delete(uri)
return err
}
// RepoMove moves a repository
func (c *client) RepoMove(repoID int64, newFullName string) error {
uri := fmt.Sprintf(pathRepoMove, c.addr, repoID, newFullName)
return c.post(uri, nil, nil)
}
// Pipeline returns a repository pipeline by pipeline-id.
func (c *client) Pipeline(repoID, pipeline int64) (*Pipeline, error) {
out := new(Pipeline)
uri := fmt.Sprintf(pathPipeline, c.addr, repoID, pipeline)
err := c.get(uri, out)
return out, err
}
// Pipeline returns the latest repository pipeline by branch.
func (c *client) PipelineLast(repoID int64, branch string) (*Pipeline, error) {
out := new(Pipeline)
uri := fmt.Sprintf(pathPipeline, c.addr, repoID, "latest")
if len(branch) != 0 {
uri += "?branch=" + branch
}
err := c.get(uri, out)
return out, err
}
// PipelineList returns a list of recent pipelines for the
// the specified repository.
func (c *client) PipelineList(repoID int64) ([]*Pipeline, error) {
var out []*Pipeline
uri := fmt.Sprintf(pathPipelines, c.addr, repoID)
err := c.get(uri, &out)
return out, err
}
func (c *client) PipelineCreate(repoID int64, options *PipelineOptions) (*Pipeline, error) {
var out *Pipeline
uri := fmt.Sprintf(pathPipelines, c.addr, repoID)
err := c.post(uri, options, &out)
return out, err
}
// PipelineQueue returns a list of enqueued pipelines.
func (c *client) PipelineQueue() ([]*Feed, error) {
var out []*Feed
uri := fmt.Sprintf(pathPipelineQueue, c.addr)
err := c.get(uri, &out)
return out, err
}
// PipelineStart re-starts a stopped pipeline.
func (c *client) PipelineStart(repoID, pipeline int64, params map[string]string) (*Pipeline, error) {
out := new(Pipeline)
val := mapValues(params)
uri := fmt.Sprintf(pathPipeline, c.addr, repoID, pipeline)
err := c.post(uri+"?"+val.Encode(), nil, out)
return out, err
}
// PipelineStop cancels the running step.
func (c *client) PipelineStop(repoID, pipeline int64) error {
uri := fmt.Sprintf(pathStop, c.addr, repoID, pipeline)
err := c.post(uri, nil, nil)
return err
}
// PipelineApprove approves a blocked pipeline.
func (c *client) PipelineApprove(repoID, pipeline int64) (*Pipeline, error) {
out := new(Pipeline)
uri := fmt.Sprintf(pathApprove, c.addr, repoID, pipeline)
err := c.post(uri, nil, out)
return out, err
}
// PipelineDecline declines a blocked pipeline.
func (c *client) PipelineDecline(repoID, pipeline int64) (*Pipeline, error) {
out := new(Pipeline)
uri := fmt.Sprintf(pathDecline, c.addr, repoID, pipeline)
err := c.post(uri, nil, out)
return out, err
}
// PipelineKill force kills the running pipeline.
func (c *client) PipelineKill(repoID, pipeline int64) error {
uri := fmt.Sprintf(pathPipeline, c.addr, repoID, pipeline)
err := c.delete(uri)
return err
}
// LogsPurge purges the pipeline all steps logs for the specified pipeline.
func (c *client) LogsPurge(repoID, pipeline int64) error {
uri := fmt.Sprintf(pathPipelineLogs, c.addr, repoID, pipeline)
err := c.delete(uri)
return err
}
// StepLogEntries returns the pipeline logs for the specified step.
func (c *client) StepLogEntries(repoID, num, step int64) ([]*LogEntry, error) {
uri := fmt.Sprintf(pathStepLogs, c.addr, repoID, num, step)
var out []*LogEntry
err := c.get(uri, &out)
return out, err
}
// StepLogsPurge purges the pipeline logs for the specified step.
func (c *client) StepLogsPurge(repoID, pipelineNumber, stepID int64) error {
uri := fmt.Sprintf(pathStepLogs, c.addr, repoID, pipelineNumber, stepID)
err := c.delete(uri)
return err
}
// Deploy triggers a deployment for an existing pipeline using the
// specified target environment.
func (c *client) Deploy(repoID, pipeline int64, env string, params map[string]string) (*Pipeline, error) {
out := new(Pipeline)
val := mapValues(params)
val.Set("event", EventDeploy)
val.Set("deploy_to", env)
uri := fmt.Sprintf(pathPipeline, c.addr, repoID, pipeline)
err := c.post(uri+"?"+val.Encode(), nil, out)
return out, err
}
// Registry returns a registry by hostname.
func (c *client) Registry(repoID int64, hostname string) (*Registry, error) {
out := new(Registry)
uri := fmt.Sprintf(pathRepoRegistry, c.addr, repoID, hostname)
err := c.get(uri, out)
return out, err
}
// RegistryList returns a list of all repository registries.
func (c *client) RegistryList(repoID int64) ([]*Registry, error) {
var out []*Registry
uri := fmt.Sprintf(pathRepoRegistries, c.addr, repoID)
err := c.get(uri, &out)
return out, err
}
// RegistryCreate creates a registry.
func (c *client) RegistryCreate(repoID int64, in *Registry) (*Registry, error) {
out := new(Registry)
uri := fmt.Sprintf(pathRepoRegistries, c.addr, repoID)
err := c.post(uri, in, out)
return out, err
}
// RegistryUpdate updates a registry.
func (c *client) RegistryUpdate(repoID int64, in *Registry) (*Registry, error) {
out := new(Registry)
uri := fmt.Sprintf(pathRepoRegistry, c.addr, repoID, in.Address)
err := c.patch(uri, in, out)
return out, err
}
// RegistryDelete deletes a registry.
func (c *client) RegistryDelete(repoID int64, hostname string) error {
uri := fmt.Sprintf(pathRepoRegistry, c.addr, repoID, hostname)
return c.delete(uri)
}
// Secret returns a secret by name.
func (c *client) Secret(repoID int64, secret string) (*Secret, error) {
out := new(Secret)
uri := fmt.Sprintf(pathRepoSecret, c.addr, repoID, secret)
err := c.get(uri, out)
return out, err
}
// SecretList returns a list of all repository secrets.
func (c *client) SecretList(repoID int64) ([]*Secret, error) {
var out []*Secret
uri := fmt.Sprintf(pathRepoSecrets, c.addr, repoID)
err := c.get(uri, &out)
return out, err
}
// SecretCreate creates a secret.
func (c *client) SecretCreate(repoID int64, in *Secret) (*Secret, error) {
out := new(Secret)
uri := fmt.Sprintf(pathRepoSecrets, c.addr, repoID)
err := c.post(uri, in, out)
return out, err
}
// SecretUpdate updates a secret.
func (c *client) SecretUpdate(repoID int64, in *Secret) (*Secret, error) {
out := new(Secret)
uri := fmt.Sprintf(pathRepoSecret, c.addr, repoID, in.Name)
err := c.patch(uri, in, out)
return out, err
}
// SecretDelete deletes a secret.
func (c *client) SecretDelete(repoID int64, secret string) error {
uri := fmt.Sprintf(pathRepoSecret, c.addr, repoID, secret)
return c.delete(uri)
}
// Org returns an organization by id.
func (c *client) Org(orgID int64) (*Org, error) {
out := new(Org)
uri := fmt.Sprintf(pathOrg, c.addr, orgID)
err := c.get(uri, out)
return out, err
}
// OrgLookup returns a organization by its name.
func (c *client) OrgLookup(name string) (*Org, error) {
out := new(Org)
uri := fmt.Sprintf(pathOrgLookup, c.addr, name)
err := c.get(uri, out)
return out, err
}
// OrgSecret returns an organization secret by name.
func (c *client) OrgSecret(orgID int64, secret string) (*Secret, error) {
out := new(Secret)
uri := fmt.Sprintf(pathOrgSecret, c.addr, orgID, secret)
err := c.get(uri, out)
return out, err
}
// OrgSecretList returns a list of all organization secrets.
func (c *client) OrgSecretList(orgID int64) ([]*Secret, error) {
var out []*Secret
uri := fmt.Sprintf(pathOrgSecrets, c.addr, orgID)
err := c.get(uri, &out)
return out, err
}
// OrgSecretCreate creates an organization secret.
func (c *client) OrgSecretCreate(orgID int64, in *Secret) (*Secret, error) {
out := new(Secret)
uri := fmt.Sprintf(pathOrgSecrets, c.addr, orgID)
err := c.post(uri, in, out)
return out, err
}
// OrgSecretUpdate updates an organization secret.
func (c *client) OrgSecretUpdate(orgID int64, in *Secret) (*Secret, error) {
out := new(Secret)
uri := fmt.Sprintf(pathOrgSecret, c.addr, orgID, in.Name)
err := c.patch(uri, in, out)
return out, err
}
// OrgSecretDelete deletes an organization secret.
func (c *client) OrgSecretDelete(orgID int64, secret string) error {
uri := fmt.Sprintf(pathOrgSecret, c.addr, orgID, secret)
return c.delete(uri)
}
// GlobalOrgSecret returns an global secret by name.
func (c *client) GlobalSecret(secret string) (*Secret, error) {
out := new(Secret)
uri := fmt.Sprintf(pathGlobalSecret, c.addr, secret)
err := c.get(uri, out)
return out, err
}
// GlobalSecretList returns a list of all global secrets.
func (c *client) GlobalSecretList() ([]*Secret, error) {
var out []*Secret
uri := fmt.Sprintf(pathGlobalSecrets, c.addr)
err := c.get(uri, &out)
return out, err
}
// GlobalSecretCreate creates a global secret.
func (c *client) GlobalSecretCreate(in *Secret) (*Secret, error) {
out := new(Secret)
uri := fmt.Sprintf(pathGlobalSecrets, c.addr)
err := c.post(uri, in, out)
return out, err
}
// GlobalSecretUpdate updates a global secret.
func (c *client) GlobalSecretUpdate(in *Secret) (*Secret, error) {
out := new(Secret)
uri := fmt.Sprintf(pathGlobalSecret, c.addr, in.Name)
err := c.patch(uri, in, out)
return out, err
}
// GlobalSecretDelete deletes a global secret.
func (c *client) GlobalSecretDelete(secret string) error {
uri := fmt.Sprintf(pathGlobalSecret, c.addr, secret)
return c.delete(uri)
}
// QueueInfo returns queue info
func (c *client) QueueInfo() (*Info, error) {
out := new(Info)
uri := fmt.Sprintf(pathQueue+"/info", c.addr)
err := c.get(uri, out)
return out, err
}
// LogLevel returns the current logging level
func (c *client) LogLevel() (*LogLevel, error) {
out := new(LogLevel)
@ -523,96 +74,31 @@ func (c *client) SetLogLevel(in *LogLevel) (*LogLevel, error) {
return out, err
}
func (c *client) CronList(repoID int64) ([]*Cron, error) {
out := make([]*Cron, 0, 5)
uri := fmt.Sprintf(pathRepoCrons, c.addr, repoID)
return out, c.get(uri, &out)
}
func (c *client) CronCreate(repoID int64, in *Cron) (*Cron, error) {
out := new(Cron)
uri := fmt.Sprintf(pathRepoCrons, c.addr, repoID)
return out, c.post(uri, in, out)
}
func (c *client) CronUpdate(repoID int64, in *Cron) (*Cron, error) {
out := new(Cron)
uri := fmt.Sprintf(pathRepoCron, c.addr, repoID, in.ID)
err := c.patch(uri, in, out)
return out, err
}
func (c *client) CronDelete(repoID, cronID int64) error {
uri := fmt.Sprintf(pathRepoCron, c.addr, repoID, cronID)
return c.delete(uri)
}
func (c *client) CronGet(repoID, cronID int64) (*Cron, error) {
out := new(Cron)
uri := fmt.Sprintf(pathRepoCron, c.addr, repoID, cronID)
return out, c.get(uri, out)
}
func (c *client) AgentList() ([]*Agent, error) {
out := make([]*Agent, 0, 5)
uri := fmt.Sprintf(pathAgents, c.addr)
return out, c.get(uri, &out)
}
func (c *client) Agent(agentID int64) (*Agent, error) {
out := new(Agent)
uri := fmt.Sprintf(pathAgent, c.addr, agentID)
return out, c.get(uri, out)
}
func (c *client) AgentCreate(in *Agent) (*Agent, error) {
out := new(Agent)
uri := fmt.Sprintf(pathAgents, c.addr)
return out, c.post(uri, in, out)
}
func (c *client) AgentUpdate(in *Agent) (*Agent, error) {
out := new(Agent)
uri := fmt.Sprintf(pathAgent, c.addr, in.ID)
return out, c.patch(uri, in, out)
}
func (c *client) AgentDelete(agentID int64) error {
uri := fmt.Sprintf(pathAgent, c.addr, agentID)
return c.delete(uri)
}
func (c *client) AgentTasksList(agentID int64) ([]*Task, error) {
out := make([]*Task, 0, 5)
uri := fmt.Sprintf(pathAgentTasks, c.addr, agentID)
return out, c.get(uri, &out)
}
//
// http request helper functions
//
// helper function for making an http GET request.
// Helper function for making an http GET request.
func (c *client) get(rawurl string, out any) error {
return c.do(rawurl, http.MethodGet, nil, out)
}
// helper function for making an http POST request.
// Helper function for making an http POST request.
func (c *client) post(rawurl string, in, out any) error {
return c.do(rawurl, http.MethodPost, in, out)
}
// helper function for making an http PATCH request.
// Helper function for making an http PATCH request.
func (c *client) patch(rawurl string, in, out any) error {
return c.do(rawurl, http.MethodPatch, in, out)
}
// helper function for making an http DELETE request.
// Helper function for making an http DELETE request.
func (c *client) delete(rawurl string) error {
return c.do(rawurl, http.MethodDelete, nil, nil)
}
// helper function to make an http request
// Helper function to make an http request.
func (c *client) do(rawurl, method string, in, out any) error {
body, err := c.open(rawurl, method, in)
if err != nil {
@ -625,7 +111,7 @@ func (c *client) do(rawurl, method string, in, out any) error {
return nil
}
// helper function to open an http request
// Helper function to open an http request.
func (c *client) open(rawurl, method string, in any) (io.ReadCloser, error) {
uri, err := url.Parse(rawurl)
if err != nil {

View file

@ -25,44 +25,6 @@ import (
"github.com/stretchr/testify/assert"
)
func Test_QueueInfo(t *testing.T) {
fixtureHandler := func(w http.ResponseWriter, _ *http.Request) {
fmt.Fprint(w, `{
"pending": null,
"running": [
{
"id": "4696",
"data": "",
"labels": {
"platform": "linux/amd64",
"repo": "woodpecker-ci/woodpecker"
},
"Dependencies": [],
"DepStatus": {},
"RunOn": null
}
],
"stats": {
"worker_count": 3,
"pending_count": 0,
"waiting_on_deps_count": 0,
"running_count": 1,
"completed_count": 0
},
"Paused": false
}`)
}
ts := httptest.NewServer(http.HandlerFunc(fixtureHandler))
defer ts.Close()
client := NewClient(ts.URL, http.DefaultClient)
info, err := client.QueueInfo()
assert.NoError(t, err)
assert.Equal(t, 3, info.Stats.Workers)
}
func Test_LogLevel(t *testing.T) {
logLevel := "warn"
fixtureHandler := func(w http.ResponseWriter, r *http.Request) {

View file

@ -49,7 +49,7 @@ const (
LogEntryProgress
)
// StepType identifies the type of step
// StepType identifies the type of step.
type StepType string
const (

View file

@ -0,0 +1,46 @@
package woodpecker
import "fmt"
const (
pathGlobalSecrets = "%s/api/secrets"
pathGlobalSecret = "%s/api/secrets/%s"
)
// GlobalOrgSecret returns an global secret by name.
func (c *client) GlobalSecret(secret string) (*Secret, error) {
out := new(Secret)
uri := fmt.Sprintf(pathGlobalSecret, c.addr, secret)
err := c.get(uri, out)
return out, err
}
// GlobalSecretList returns a list of all global secrets.
func (c *client) GlobalSecretList() ([]*Secret, error) {
var out []*Secret
uri := fmt.Sprintf(pathGlobalSecrets, c.addr)
err := c.get(uri, &out)
return out, err
}
// GlobalSecretCreate creates a global secret.
func (c *client) GlobalSecretCreate(in *Secret) (*Secret, error) {
out := new(Secret)
uri := fmt.Sprintf(pathGlobalSecrets, c.addr)
err := c.post(uri, in, out)
return out, err
}
// GlobalSecretUpdate updates a global secret.
func (c *client) GlobalSecretUpdate(in *Secret) (*Secret, error) {
out := new(Secret)
uri := fmt.Sprintf(pathGlobalSecret, c.addr, in.Name)
err := c.patch(uri, in, out)
return out, err
}
// GlobalSecretDelete deletes a global secret.
func (c *client) GlobalSecretDelete(secret string) error {
uri := fmt.Sprintf(pathGlobalSecret, c.addr, secret)
return c.delete(uri)
}

View file

@ -190,42 +190,42 @@ type Client interface {
// QueueInfo returns the queue state.
QueueInfo() (*Info, error)
// LogLevel returns the current logging level
// LogLevel returns the current logging level.
LogLevel() (*LogLevel, error)
// SetLogLevel sets the server's logging level
// SetLogLevel sets the server's logging level.
SetLogLevel(logLevel *LogLevel) (*LogLevel, error)
// CronList list all cron jobs of a repo
// CronList list all cron jobs of a repo.
CronList(repoID int64) ([]*Cron, error)
// CronGet get a specific cron job of a repo by id
// CronGet get a specific cron job of a repo by id.
CronGet(repoID, cronID int64) (*Cron, error)
// CronDelete delete a specific cron job of a repo by id
// CronDelete delete a specific cron job of a repo by id.
CronDelete(repoID, cronID int64) error
// CronCreate create a new cron job in a repo
// CronCreate create a new cron job in a repo.
CronCreate(repoID int64, cron *Cron) (*Cron, error)
// CronUpdate update an existing cron job of a repo
// CronUpdate update an existing cron job of a repo.
CronUpdate(repoID int64, cron *Cron) (*Cron, error)
// AgentList returns a list of all registered agents
// AgentList returns a list of all registered agents.
AgentList() ([]*Agent, error)
// Agent returns an agent by id
// Agent returns an agent by id.
Agent(int64) (*Agent, error)
// AgentCreate creates a new agent
// AgentCreate creates a new agent.
AgentCreate(*Agent) (*Agent, error)
// AgentUpdate updates an existing agent
// AgentUpdate updates an existing agent.
AgentUpdate(*Agent) (*Agent, error)
// AgentDelete deletes an agent
// AgentDelete deletes an agent.
AgentDelete(int64) error
// AgentTasksList returns a list of all tasks executed by an agent
// AgentTasksList returns a list of all tasks executed by an agent.
AgentTasksList(int64) ([]*Task, error)
}

View file

@ -0,0 +1,64 @@
package woodpecker
import "fmt"
const (
pathOrg = "%s/api/orgs/%d"
pathOrgLookup = "%s/api/orgs/lookup/%s"
pathOrgSecrets = "%s/api/orgs/%d/secrets"
pathOrgSecret = "%s/api/orgs/%d/secrets/%s"
)
// Org returns an organization by id.
func (c *client) Org(orgID int64) (*Org, error) {
out := new(Org)
uri := fmt.Sprintf(pathOrg, c.addr, orgID)
err := c.get(uri, out)
return out, err
}
// OrgLookup returns a organization by its name.
func (c *client) OrgLookup(name string) (*Org, error) {
out := new(Org)
uri := fmt.Sprintf(pathOrgLookup, c.addr, name)
err := c.get(uri, out)
return out, err
}
// OrgSecret returns an organization secret by name.
func (c *client) OrgSecret(orgID int64, secret string) (*Secret, error) {
out := new(Secret)
uri := fmt.Sprintf(pathOrgSecret, c.addr, orgID, secret)
err := c.get(uri, out)
return out, err
}
// OrgSecretList returns a list of all organization secrets.
func (c *client) OrgSecretList(orgID int64) ([]*Secret, error) {
var out []*Secret
uri := fmt.Sprintf(pathOrgSecrets, c.addr, orgID)
err := c.get(uri, &out)
return out, err
}
// OrgSecretCreate creates an organization secret.
func (c *client) OrgSecretCreate(orgID int64, in *Secret) (*Secret, error) {
out := new(Secret)
uri := fmt.Sprintf(pathOrgSecrets, c.addr, orgID)
err := c.post(uri, in, out)
return out, err
}
// OrgSecretUpdate updates an organization secret.
func (c *client) OrgSecretUpdate(orgID int64, in *Secret) (*Secret, error) {
out := new(Secret)
uri := fmt.Sprintf(pathOrgSecret, c.addr, orgID, in.Name)
err := c.patch(uri, in, out)
return out, err
}
// OrgSecretDelete deletes an organization secret.
func (c *client) OrgSecretDelete(orgID int64, secret string) error {
uri := fmt.Sprintf(pathOrgSecret, c.addr, orgID, secret)
return c.delete(uri)
}

View file

@ -0,0 +1,13 @@
package woodpecker
import "fmt"
const pathPipelineQueue = "%s/api/pipelines"
// PipelineQueue returns a list of enqueued pipelines.
func (c *client) PipelineQueue() ([]*Feed, error) {
var out []*Feed
uri := fmt.Sprintf(pathPipelineQueue, c.addr)
err := c.get(uri, &out)
return out, err
}

View file

@ -0,0 +1,13 @@
package woodpecker
import "fmt"
const pathQueue = "%s/api/queue"
// QueueInfo returns queue info.
func (c *client) QueueInfo() (*Info, error) {
out := new(Info)
uri := fmt.Sprintf(pathQueue+"/info", c.addr)
err := c.get(uri, out)
return out, err
}

View file

@ -0,0 +1,116 @@
package woodpecker
import (
"fmt"
"net/http"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/assert"
)
func TestClient_QueueInfo(t *testing.T) {
tests := []struct {
name string
handler http.HandlerFunc
expected *Info
wantErr bool
}{
{
name: "success",
handler: func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK)
_, err := fmt.Fprint(w, `{
"pending": null,
"running": [
{
"id": "4696",
"data": "",
"labels": {
"platform": "linux/amd64",
"repo": "woodpecker-ci/woodpecker"
},
"Dependencies": [],
"DepStatus": {},
"RunOn": null
}
],
"stats": {
"worker_count": 2,
"pending_count": 0,
"waiting_on_deps_count": 0,
"running_count": 0,
"completed_count": 0
},
"Paused": false
}`)
assert.NoError(t, err)
},
expected: &Info{
Running: []Task{
{
ID: "4696",
Data: []byte{},
Labels: map[string]string{
"platform": "linux/amd64",
"repo": "woodpecker-ci/woodpecker",
},
Dependencies: []string{},
DepStatus: nil,
RunOn: nil,
},
},
Stats: struct {
Workers int `json:"worker_count"`
Pending int `json:"pending_count"`
WaitingOnDeps int `json:"waiting_on_deps_count"`
Running int `json:"running_count"`
Complete int `json:"completed_count"`
}{
Workers: 2,
Pending: 0,
WaitingOnDeps: 0,
Running: 0,
Complete: 0,
},
},
wantErr: false,
},
{
name: "server error",
handler: func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusInternalServerError)
},
expected: nil,
wantErr: true,
},
{
name: "invalid response",
handler: func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK)
_, err := fmt.Fprint(w, `invalid json`)
assert.NoError(t, err)
},
expected: nil,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ts := httptest.NewServer(tt.handler)
defer ts.Close()
client := NewClient(ts.URL, http.DefaultClient)
info, err := client.QueueInfo()
if tt.wantErr {
assert.Error(t, err)
return
}
assert.NoError(t, err)
assert.Equal(t, tt.expected, info)
})
}
}

View file

@ -0,0 +1,304 @@
package woodpecker
import "fmt"
const (
pathRepoPost = "%s/api/repos?forge_remote_id=%d"
pathRepo = "%s/api/repos/%d"
pathRepoLookup = "%s/api/repos/lookup/%s"
pathRepoMove = "%s/api/repos/%d/move?to=%s"
pathChown = "%s/api/repos/%d/chown"
pathRepair = "%s/api/repos/%d/repair"
pathPipelines = "%s/api/repos/%d/pipelines"
pathPipeline = "%s/api/repos/%d/pipelines/%v"
pathPipelineLogs = "%s/api/repos/%d/logs/%d"
pathStepLogs = "%s/api/repos/%d/logs/%d/%d"
pathApprove = "%s/api/repos/%d/pipelines/%d/approve"
pathDecline = "%s/api/repos/%d/pipelines/%d/decline"
pathStop = "%s/api/repos/%d/pipelines/%d/cancel"
pathRepoSecrets = "%s/api/repos/%d/secrets"
pathRepoSecret = "%s/api/repos/%d/secrets/%s"
pathRepoRegistries = "%s/api/repos/%d/registry"
pathRepoRegistry = "%s/api/repos/%d/registry/%s"
pathRepoCrons = "%s/api/repos/%d/cron"
pathRepoCron = "%s/api/repos/%d/cron/%d"
)
// Repo returns a repository by id.
func (c *client) Repo(repoID int64) (*Repo, error) {
out := new(Repo)
uri := fmt.Sprintf(pathRepo, c.addr, repoID)
err := c.get(uri, out)
return out, err
}
// RepoLookup returns a repository by name.
func (c *client) RepoLookup(fullName string) (*Repo, error) {
out := new(Repo)
uri := fmt.Sprintf(pathRepoLookup, c.addr, fullName)
err := c.get(uri, out)
return out, err
}
// RepoPost activates a repository.
func (c *client) RepoPost(forgeRemoteID int64) (*Repo, error) {
out := new(Repo)
uri := fmt.Sprintf(pathRepoPost, c.addr, forgeRemoteID)
err := c.post(uri, nil, out)
return out, err
}
// RepoChown updates a repository owner.
func (c *client) RepoChown(repoID int64) (*Repo, error) {
out := new(Repo)
uri := fmt.Sprintf(pathChown, c.addr, repoID)
err := c.post(uri, nil, out)
return out, err
}
// RepoRepair repairs the repository hooks.
func (c *client) RepoRepair(repoID int64) error {
uri := fmt.Sprintf(pathRepair, c.addr, repoID)
return c.post(uri, nil, nil)
}
// RepoPatch updates a repository.
func (c *client) RepoPatch(repoID int64, in *RepoPatch) (*Repo, error) {
out := new(Repo)
uri := fmt.Sprintf(pathRepo, c.addr, repoID)
err := c.patch(uri, in, out)
return out, err
}
// RepoDel deletes a repository.
func (c *client) RepoDel(repoID int64) error {
uri := fmt.Sprintf(pathRepo, c.addr, repoID)
err := c.delete(uri)
return err
}
// RepoMove moves a repository.
func (c *client) RepoMove(repoID int64, newFullName string) error {
uri := fmt.Sprintf(pathRepoMove, c.addr, repoID, newFullName)
return c.post(uri, nil, nil)
}
// Registry returns a registry by hostname.
func (c *client) Registry(repoID int64, hostname string) (*Registry, error) {
out := new(Registry)
uri := fmt.Sprintf(pathRepoRegistry, c.addr, repoID, hostname)
err := c.get(uri, out)
return out, err
}
// RegistryList returns a list of all repository registries.
func (c *client) RegistryList(repoID int64) ([]*Registry, error) {
var out []*Registry
uri := fmt.Sprintf(pathRepoRegistries, c.addr, repoID)
err := c.get(uri, &out)
return out, err
}
// RegistryCreate creates a registry.
func (c *client) RegistryCreate(repoID int64, in *Registry) (*Registry, error) {
out := new(Registry)
uri := fmt.Sprintf(pathRepoRegistries, c.addr, repoID)
err := c.post(uri, in, out)
return out, err
}
// RegistryUpdate updates a registry.
func (c *client) RegistryUpdate(repoID int64, in *Registry) (*Registry, error) {
out := new(Registry)
uri := fmt.Sprintf(pathRepoRegistry, c.addr, repoID, in.Address)
err := c.patch(uri, in, out)
return out, err
}
// RegistryDelete deletes a registry.
func (c *client) RegistryDelete(repoID int64, hostname string) error {
uri := fmt.Sprintf(pathRepoRegistry, c.addr, repoID, hostname)
return c.delete(uri)
}
// Secret returns a secret by name.
func (c *client) Secret(repoID int64, secret string) (*Secret, error) {
out := new(Secret)
uri := fmt.Sprintf(pathRepoSecret, c.addr, repoID, secret)
err := c.get(uri, out)
return out, err
}
// SecretList returns a list of all repository secrets.
func (c *client) SecretList(repoID int64) ([]*Secret, error) {
var out []*Secret
uri := fmt.Sprintf(pathRepoSecrets, c.addr, repoID)
err := c.get(uri, &out)
return out, err
}
// SecretCreate creates a secret.
func (c *client) SecretCreate(repoID int64, in *Secret) (*Secret, error) {
out := new(Secret)
uri := fmt.Sprintf(pathRepoSecrets, c.addr, repoID)
err := c.post(uri, in, out)
return out, err
}
// SecretUpdate updates a secret.
func (c *client) SecretUpdate(repoID int64, in *Secret) (*Secret, error) {
out := new(Secret)
uri := fmt.Sprintf(pathRepoSecret, c.addr, repoID, in.Name)
err := c.patch(uri, in, out)
return out, err
}
// SecretDelete deletes a secret.
func (c *client) SecretDelete(repoID int64, secret string) error {
uri := fmt.Sprintf(pathRepoSecret, c.addr, repoID, secret)
return c.delete(uri)
}
// CronList returns a list of cronjobs for the specified repository.
func (c *client) CronList(repoID int64) ([]*Cron, error) {
out := make([]*Cron, 0, 5)
uri := fmt.Sprintf(pathRepoCrons, c.addr, repoID)
return out, c.get(uri, &out)
}
// CronCreate creates a new cron job for the specified repository.
func (c *client) CronCreate(repoID int64, in *Cron) (*Cron, error) {
out := new(Cron)
uri := fmt.Sprintf(pathRepoCrons, c.addr, repoID)
return out, c.post(uri, in, out)
}
// CronUpdate updates an existing cron job for the specified repository.
func (c *client) CronUpdate(repoID int64, in *Cron) (*Cron, error) {
out := new(Cron)
uri := fmt.Sprintf(pathRepoCron, c.addr, repoID, in.ID)
err := c.patch(uri, in, out)
return out, err
}
// CronDelete deletes a cron job by cron-id for the specified repository.
func (c *client) CronDelete(repoID, cronID int64) error {
uri := fmt.Sprintf(pathRepoCron, c.addr, repoID, cronID)
return c.delete(uri)
}
// CronGet returns a cron job by cron-id for the specified repository.
func (c *client) CronGet(repoID, cronID int64) (*Cron, error) {
out := new(Cron)
uri := fmt.Sprintf(pathRepoCron, c.addr, repoID, cronID)
return out, c.get(uri, out)
}
// Pipeline returns a repository pipeline by pipeline-id.
func (c *client) Pipeline(repoID, pipeline int64) (*Pipeline, error) {
out := new(Pipeline)
uri := fmt.Sprintf(pathPipeline, c.addr, repoID, pipeline)
err := c.get(uri, out)
return out, err
}
// Pipeline returns the latest repository pipeline by branch.
func (c *client) PipelineLast(repoID int64, branch string) (*Pipeline, error) {
out := new(Pipeline)
uri := fmt.Sprintf(pathPipeline, c.addr, repoID, "latest")
if len(branch) != 0 {
uri += "?branch=" + branch
}
err := c.get(uri, out)
return out, err
}
// PipelineList returns a list of recent pipelines for the
// the specified repository.
func (c *client) PipelineList(repoID int64) ([]*Pipeline, error) {
var out []*Pipeline
uri := fmt.Sprintf(pathPipelines, c.addr, repoID)
err := c.get(uri, &out)
return out, err
}
// PipelineCreate creates a new pipeline for the specified repository.
func (c *client) PipelineCreate(repoID int64, options *PipelineOptions) (*Pipeline, error) {
var out *Pipeline
uri := fmt.Sprintf(pathPipelines, c.addr, repoID)
err := c.post(uri, options, &out)
return out, err
}
// PipelineStart re-starts a stopped pipeline.
func (c *client) PipelineStart(repoID, pipeline int64, params map[string]string) (*Pipeline, error) {
out := new(Pipeline)
val := mapValues(params)
uri := fmt.Sprintf(pathPipeline, c.addr, repoID, pipeline)
err := c.post(uri+"?"+val.Encode(), nil, out)
return out, err
}
// PipelineStop cancels the running step.
func (c *client) PipelineStop(repoID, pipeline int64) error {
uri := fmt.Sprintf(pathStop, c.addr, repoID, pipeline)
err := c.post(uri, nil, nil)
return err
}
// PipelineApprove approves a blocked pipeline.
func (c *client) PipelineApprove(repoID, pipeline int64) (*Pipeline, error) {
out := new(Pipeline)
uri := fmt.Sprintf(pathApprove, c.addr, repoID, pipeline)
err := c.post(uri, nil, out)
return out, err
}
// PipelineDecline declines a blocked pipeline.
func (c *client) PipelineDecline(repoID, pipeline int64) (*Pipeline, error) {
out := new(Pipeline)
uri := fmt.Sprintf(pathDecline, c.addr, repoID, pipeline)
err := c.post(uri, nil, out)
return out, err
}
// PipelineKill force kills the running pipeline.
func (c *client) PipelineKill(repoID, pipeline int64) error {
uri := fmt.Sprintf(pathPipeline, c.addr, repoID, pipeline)
err := c.delete(uri)
return err
}
// LogsPurge purges the pipeline all steps logs for the specified pipeline.
func (c *client) LogsPurge(repoID, pipeline int64) error {
uri := fmt.Sprintf(pathPipelineLogs, c.addr, repoID, pipeline)
err := c.delete(uri)
return err
}
// Deploy triggers a deployment for an existing pipeline using the
// specified target environment.
func (c *client) Deploy(repoID, pipeline int64, env string, params map[string]string) (*Pipeline, error) {
out := new(Pipeline)
val := mapValues(params)
val.Set("event", EventDeploy)
val.Set("deploy_to", env)
uri := fmt.Sprintf(pathPipeline, c.addr, repoID, pipeline)
err := c.post(uri+"?"+val.Encode(), nil, out)
return out, err
}
// StepLogEntries returns the pipeline logs for the specified step.
func (c *client) StepLogEntries(repoID, num, step int64) ([]*LogEntry, error) {
uri := fmt.Sprintf(pathStepLogs, c.addr, repoID, num, step)
var out []*LogEntry
err := c.get(uri, &out)
return out, err
}
// StepLogsPurge purges the pipeline logs for the specified step.
func (c *client) StepLogsPurge(repoID, pipelineNumber, stepID int64) error {
uri := fmt.Sprintf(pathStepLogs, c.addr, repoID, pipelineNumber, stepID)
err := c.delete(uri)
return err
}

View file

@ -181,12 +181,22 @@ type (
Commit string `json:"commit,omitempty"`
}
// QueueStats struct {
// Workers int `json:"worker_count"`
// Pending int `json:"pending_count"`
// WaitingOnDeps int `json:"waiting_on_deps_count"`
// Running int `json:"running_count"`
// Complete int `json:"completed_count"`
// }
// Info provides queue stats.
Info struct {
Pending []Task `json:"pending"`
WaitingOnDeps []Task `json:"waiting_on_deps"`
Running []Task `json:"running"`
Stats struct {
// TODO use dedicated struct in 3.x
// Stats QueueStats `json:"stats"`
Stats struct {
Workers int `json:"worker_count"`
Pending int `json:"pending_count"`
WaitingOnDeps int `json:"waiting_on_deps_count"`
@ -196,7 +206,7 @@ type (
Paused bool `json:"paused,omitempty"`
}
// LogLevel is for checking/setting logging level
// LogLevel is for checking/setting logging level.
LogLevel struct {
Level string `json:"log-level"`
}
@ -211,7 +221,7 @@ type (
Type LogEntryType `json:"type"`
}
// Cron is the JSON data of a cron job
// Cron is the JSON data of a cron job.
Cron struct {
ID int64 `json:"id"`
Name string `json:"name"`
@ -223,13 +233,13 @@ type (
Branch string `json:"branch"`
}
// PipelineOptions is the JSON data for creating a new pipeline
// PipelineOptions is the JSON data for creating a new pipeline.
PipelineOptions struct {
Branch string `json:"branch"`
Variables map[string]string `json:"variables"`
}
// Agent is the JSON data for an agent
// Agent is the JSON data for an agent.
Agent struct {
ID int64 `json:"id"`
Created int64 `json:"created"`
@ -245,7 +255,7 @@ type (
NoSchedule bool `json:"no_schedule"`
}
// Task is the JSON data for a task
// Task is the JSON data for a task.
Task struct {
ID string `json:"id"`
Data []byte `json:"data"`
@ -256,7 +266,7 @@ type (
AgentID int64 `json:"agent_id"`
}
// Org is the JSON data for an organization
// Org is the JSON data for an organization.
Org struct {
ID int64 `json:"id"`
Name string `json:"name"`

View file

@ -0,0 +1,75 @@
package woodpecker
import "fmt"
const (
pathSelf = "%s/api/user"
pathRepos = "%s/api/user/repos"
pathUsers = "%s/api/users"
pathUser = "%s/api/users/%s"
)
// Self returns the currently authenticated user.
func (c *client) Self() (*User, error) {
out := new(User)
uri := fmt.Sprintf(pathSelf, c.addr)
err := c.get(uri, out)
return out, err
}
// User returns a user by login.
func (c *client) User(login string) (*User, error) {
out := new(User)
uri := fmt.Sprintf(pathUser, c.addr, login)
err := c.get(uri, out)
return out, err
}
// UserList returns a list of all registered users.
func (c *client) UserList() ([]*User, error) {
var out []*User
uri := fmt.Sprintf(pathUsers, c.addr)
err := c.get(uri, &out)
return out, err
}
// UserPost creates a new user account.
func (c *client) UserPost(in *User) (*User, error) {
out := new(User)
uri := fmt.Sprintf(pathUsers, c.addr)
err := c.post(uri, in, out)
return out, err
}
// UserPatch updates a user account.
func (c *client) UserPatch(in *User) (*User, error) {
out := new(User)
uri := fmt.Sprintf(pathUser, c.addr, in.Login)
err := c.patch(uri, in, out)
return out, err
}
// UserDel deletes a user account.
func (c *client) UserDel(login string) error {
uri := fmt.Sprintf(pathUser, c.addr, login)
err := c.delete(uri)
return err
}
// RepoList returns a list of all repositories to which
// the user has explicit access in the host system.
func (c *client) RepoList() ([]*Repo, error) {
var out []*Repo
uri := fmt.Sprintf(pathRepos, c.addr)
err := c.get(uri, &out)
return out, err
}
// RepoListOpts returns a list of all repositories to which
// the user has explicit access in the host system.
func (c *client) RepoListOpts(all bool) ([]*Repo, error) {
var out []*Repo
uri := fmt.Sprintf(pathRepos+"?all=%v", c.addr, all)
err := c.get(uri, &out)
return out, err
}

View file

@ -0,0 +1,267 @@
package woodpecker
import (
"fmt"
"net/http"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/assert"
)
func TestClient_UserList(t *testing.T) {
tests := []struct {
name string
handler http.HandlerFunc
expected []*User
wantErr bool
}{
{
name: "success",
handler: func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK)
_, err := fmt.Fprint(w, `[{"id":1,"login":"user1"},{"id":2,"login":"user2"}]`)
assert.NoError(t, err)
},
expected: []*User{{ID: 1, Login: "user1"}, {ID: 2, Login: "user2"}},
wantErr: false,
},
{
name: "empty response",
handler: func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK)
_, err := fmt.Fprint(w, `[]`)
assert.NoError(t, err)
},
expected: []*User{},
wantErr: false,
},
{
name: "server error",
handler: func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusInternalServerError)
},
expected: nil,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ts := httptest.NewServer(tt.handler)
defer ts.Close()
client := NewClient(ts.URL, http.DefaultClient)
users, err := client.UserList()
if tt.wantErr {
assert.Error(t, err)
return
}
assert.NoError(t, err)
assert.Equal(t, users, tt.expected)
})
}
}
func TestClient_UserPost(t *testing.T) {
tests := []struct {
name string
handler http.HandlerFunc
input *User
expected *User
wantErr bool
}{
{
name: "success",
handler: func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusCreated)
_, err := fmt.Fprint(w, `{"id":1,"login":"new_user"}`)
assert.NoError(t, err)
},
input: &User{Login: "new_user"},
expected: &User{ID: 1, Login: "new_user"},
wantErr: false,
},
{
name: "invalid input",
handler: func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusBadRequest)
},
input: &User{},
expected: nil,
wantErr: true,
},
{
name: "server error",
handler: func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusInternalServerError)
},
input: &User{Login: "new_user"},
expected: nil,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ts := httptest.NewServer(tt.handler)
defer ts.Close()
client := NewClient(ts.URL, http.DefaultClient)
user, err := client.UserPost(tt.input)
if tt.wantErr {
assert.Error(t, err)
return
}
assert.NoError(t, err)
assert.Equal(t, user, tt.expected)
})
}
}
func TestClient_UserPatch(t *testing.T) {
tests := []struct {
name string
handler http.HandlerFunc
input *User
expected *User
wantErr bool
}{
{
name: "success",
handler: func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPatch {
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
w.WriteHeader(http.StatusOK)
_, err := fmt.Fprint(w, `{"id":1,"login":"updated_user"}`)
assert.NoError(t, err)
},
input: &User{ID: 1, Login: "existing_user"},
expected: &User{ID: 1, Login: "updated_user"},
wantErr: false,
},
{
name: "not found",
handler: func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPatch {
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
w.WriteHeader(http.StatusNotFound)
},
input: &User{ID: 999, Login: "nonexistent_user"},
expected: nil,
wantErr: true,
},
{
name: "invalid input",
handler: func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPatch {
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
w.WriteHeader(http.StatusBadRequest)
},
input: &User{},
expected: nil,
wantErr: true,
},
{
name: "server error",
handler: func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPatch {
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
w.WriteHeader(http.StatusInternalServerError)
},
input: &User{ID: 1, Login: "existing_user"},
expected: nil,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ts := httptest.NewServer(tt.handler)
defer ts.Close()
client := NewClient(ts.URL, http.DefaultClient)
user, err := client.UserPatch(tt.input)
if tt.wantErr {
assert.Error(t, err)
return
}
assert.NoError(t, err)
assert.Equal(t, user, tt.expected)
})
}
}
func TestClient_UserDel(t *testing.T) {
tests := []struct {
name string
handler http.HandlerFunc
login string
wantErr bool
}{
{
name: "success",
handler: func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodDelete {
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
w.WriteHeader(http.StatusOK)
},
login: "existing_user",
wantErr: false,
},
{
name: "not found",
handler: func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodDelete {
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
w.WriteHeader(http.StatusNotFound)
},
login: "nonexistent_user",
wantErr: true,
},
{
name: "server error",
handler: func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodDelete {
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
w.WriteHeader(http.StatusInternalServerError)
},
login: "existing_user",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ts := httptest.NewServer(tt.handler)
defer ts.Close()
client := NewClient(ts.URL, http.DefaultClient)
err := client.UserDel(tt.login)
if tt.wantErr {
assert.Error(t, err)
return
}
assert.NoError(t, err)
})
}
}