diff --git a/.gitignore b/.gitignore index 90d70c085..4cbc9626d 100644 --- a/.gitignore +++ b/.gitignore @@ -13,7 +13,7 @@ *.so *.dylib vendor/ -__debug_bin +__debug_bin* # Test binary, built with `go test -c` *.test diff --git a/woodpecker-go/woodpecker/agent.go b/woodpecker-go/woodpecker/agent.go new file mode 100644 index 000000000..81ff8883c --- /dev/null +++ b/woodpecker-go/woodpecker/agent.go @@ -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) +} diff --git a/woodpecker-go/woodpecker/agent_test.go b/woodpecker-go/woodpecker/agent_test.go new file mode 100644 index 000000000..d424336d8 --- /dev/null +++ b/woodpecker-go/woodpecker/agent_test.go @@ -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) + }) + } +} diff --git a/woodpecker-go/woodpecker/client.go b/woodpecker-go/woodpecker/client.go index d22622d9e..f2c385ebe 100644 --- a/woodpecker-go/woodpecker/client.go +++ b/woodpecker-go/woodpecker/client.go @@ -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 { diff --git a/woodpecker-go/woodpecker/client_test.go b/woodpecker-go/woodpecker/client_test.go index a5ad5a2b7..4bdad1dc4 100644 --- a/woodpecker-go/woodpecker/client_test.go +++ b/woodpecker-go/woodpecker/client_test.go @@ -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) { diff --git a/woodpecker-go/woodpecker/const.go b/woodpecker-go/woodpecker/const.go index cf0792646..06dfe324a 100644 --- a/woodpecker-go/woodpecker/const.go +++ b/woodpecker-go/woodpecker/const.go @@ -49,7 +49,7 @@ const ( LogEntryProgress ) -// StepType identifies the type of step +// StepType identifies the type of step. type StepType string const ( diff --git a/woodpecker-go/woodpecker/global_secret.go b/woodpecker-go/woodpecker/global_secret.go new file mode 100644 index 000000000..7befc97e5 --- /dev/null +++ b/woodpecker-go/woodpecker/global_secret.go @@ -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) +} diff --git a/woodpecker-go/woodpecker/interface.go b/woodpecker-go/woodpecker/interface.go index 56edc8f6f..9ad2ee33e 100644 --- a/woodpecker-go/woodpecker/interface.go +++ b/woodpecker-go/woodpecker/interface.go @@ -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) } diff --git a/woodpecker-go/woodpecker/org.go b/woodpecker-go/woodpecker/org.go new file mode 100644 index 000000000..885232c05 --- /dev/null +++ b/woodpecker-go/woodpecker/org.go @@ -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) +} diff --git a/woodpecker-go/woodpecker/pipeline.go b/woodpecker-go/woodpecker/pipeline.go new file mode 100644 index 000000000..8ed4d6d20 --- /dev/null +++ b/woodpecker-go/woodpecker/pipeline.go @@ -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 +} diff --git a/woodpecker-go/woodpecker/queue.go b/woodpecker-go/woodpecker/queue.go new file mode 100644 index 000000000..1e0f27e15 --- /dev/null +++ b/woodpecker-go/woodpecker/queue.go @@ -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 +} diff --git a/woodpecker-go/woodpecker/queue_test.go b/woodpecker-go/woodpecker/queue_test.go new file mode 100644 index 000000000..099169d3a --- /dev/null +++ b/woodpecker-go/woodpecker/queue_test.go @@ -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) + }) + } +} diff --git a/woodpecker-go/woodpecker/repo.go b/woodpecker-go/woodpecker/repo.go new file mode 100644 index 000000000..91f488f9e --- /dev/null +++ b/woodpecker-go/woodpecker/repo.go @@ -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 +} diff --git a/woodpecker-go/woodpecker/types.go b/woodpecker-go/woodpecker/types.go index cf84e01b9..f8054e66d 100644 --- a/woodpecker-go/woodpecker/types.go +++ b/woodpecker-go/woodpecker/types.go @@ -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"` diff --git a/woodpecker-go/woodpecker/user.go b/woodpecker-go/woodpecker/user.go new file mode 100644 index 000000000..6cba11e53 --- /dev/null +++ b/woodpecker-go/woodpecker/user.go @@ -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 +} diff --git a/woodpecker-go/woodpecker/user_test.go b/woodpecker-go/woodpecker/user_test.go new file mode 100644 index 000000000..4d99226c1 --- /dev/null +++ b/woodpecker-go/woodpecker/user_test.go @@ -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) + }) + } +}