diff --git a/cmd/cli/deployment.go b/cmd/cli/deployment.go new file mode 100644 index 00000000..2ae57617 --- /dev/null +++ b/cmd/cli/deployment.go @@ -0,0 +1,15 @@ +package main + +import "github.com/urfave/cli/v2" + +var deploymentCommand = &cli.Command{ + Name: "deployment", + Aliases: []string{"d"}, + Usage: "Manage deployments.", + Subcommands: []*cli.Command{ + deploymentListCommand, + deploymentGetCommand, + deploymentDeployCommand, + deploymentUpdateCommand, + }, +} diff --git a/cmd/cli/deployment_deploy.go b/cmd/cli/deployment_deploy.go new file mode 100644 index 00000000..0f34ab81 --- /dev/null +++ b/cmd/cli/deployment_deploy.go @@ -0,0 +1,48 @@ +package main + +import ( + "github.com/urfave/cli/v2" + + "github.com/gitploy-io/gitploy/pkg/api" +) + +var deploymentDeployCommand = &cli.Command{ + Name: "deploy", + Usage: "Deploy a specific ref(branch, SHA, tag) to the environment.", + ArgsUsage: "/", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "type", + Usage: "The type of the ref: 'commit', 'branch', or 'tag'.", + Required: true, + }, + &cli.StringFlag{ + Name: "env", + Usage: "The name of the environment.", + Required: true, + }, + &cli.StringFlag{ + Name: "ref", + Usage: "The specific ref. It can be any any named branch, tag, or SHA.", + Required: true, + }, + }, + Action: func(cli *cli.Context) error { + ns, n, err := splitFullName(cli.Args().First()) + if err != nil { + return err + } + + c := buildClient(cli) + d, err := c.Deployment.Create(cli.Context, ns, n, api.DeploymentCreateRequest{ + Type: cli.String("type"), + Ref: cli.String("ref"), + Env: cli.String("env"), + }) + if err != nil { + return err + } + + return printJson(d, cli.String("query")) + }, +} diff --git a/cmd/cli/deployment_get.go b/cmd/cli/deployment_get.go new file mode 100644 index 00000000..15dcdd86 --- /dev/null +++ b/cmd/cli/deployment_get.go @@ -0,0 +1,33 @@ +package main + +import ( + "strconv" + + "github.com/urfave/cli/v2" +) + +var deploymentGetCommand = &cli.Command{ + Name: "get", + Usage: "Show the deployment", + ArgsUsage: "/ ", + Action: func(cli *cli.Context) error { + // Validate arguments. + ns, n, err := splitFullName(cli.Args().First()) + if err != nil { + return err + } + + number, err := strconv.Atoi(cli.Args().Get(1)) + if err != nil { + return err + } + + c := buildClient(cli) + d, err := c.Deployment.Get(cli.Context, ns, n, number) + if err != nil { + return err + } + + return printJson(d, cli.String("query")) + }, +} diff --git a/cmd/cli/deployment_list.go b/cmd/cli/deployment_list.go new file mode 100644 index 00000000..541e8d59 --- /dev/null +++ b/cmd/cli/deployment_list.go @@ -0,0 +1,54 @@ +package main + +import ( + "github.com/urfave/cli/v2" + + "github.com/gitploy-io/gitploy/model/ent/deployment" + "github.com/gitploy-io/gitploy/pkg/api" +) + +var deploymentListCommand = &cli.Command{ + Name: "list", + Aliases: []string{"ls"}, + Usage: "Show the deployments under the repository.", + ArgsUsage: "/", + Flags: []cli.Flag{ + &cli.IntFlag{ + Name: "page", + Value: 1, + Usage: "The page of list.", + }, + &cli.IntFlag{ + Name: "per-page", + Value: 30, + Usage: "The item count per page.", + }, + &cli.StringFlag{ + Name: "env", + Usage: "The name of environment. It only shows deployments for the environment.", + }, + &cli.StringFlag{ + Name: "status", + Usage: "The deployment status: 'waiting', 'created', 'queued', 'running', 'success', or 'failure'. It only shows deployments the status is matched. ", + }, + }, + Action: func(cli *cli.Context) error { + c := buildClient(cli) + + ns, n, err := splitFullName(cli.Args().First()) + if err != nil { + return err + } + + ds, err := c.Deployment.List(cli.Context, ns, n, api.DeploymentListOptions{ + ListOptions: api.ListOptions{Page: cli.Int("page"), PerPage: cli.Int("per-page")}, + Env: cli.String("env"), + Status: deployment.Status(cli.String("status")), + }) + if err != nil { + return err + } + + return printJson(ds, cli.String("query")) + }, +} diff --git a/cmd/cli/deployment_update.go b/cmd/cli/deployment_update.go new file mode 100644 index 00000000..08dee100 --- /dev/null +++ b/cmd/cli/deployment_update.go @@ -0,0 +1,32 @@ +package main + +import ( + "strconv" + + "github.com/urfave/cli/v2" +) + +var deploymentUpdateCommand = &cli.Command{ + Name: "update", + Usage: "Trigger the deployment which has approved by reviews.", + ArgsUsage: "/ ", + Action: func(cli *cli.Context) error { + ns, n, err := splitFullName(cli.Args().First()) + if err != nil { + return err + } + + number, err := strconv.Atoi(cli.Args().Get(1)) + if err != nil { + return err + } + + c := buildClient(cli) + d, err := c.Deployment.Update(cli.Context, ns, n, number) + if err != nil { + return err + } + + return printJson(d, cli.String("query")) + }, +} diff --git a/cmd/cli/main.go b/cmd/cli/main.go index addf0060..3aa2d971 100644 --- a/cmd/cli/main.go +++ b/cmd/cli/main.go @@ -33,6 +33,7 @@ func main() { }, Commands: []*cli.Command{ repoCommand, + deploymentCommand, }, } diff --git a/cmd/cli/repo.go b/cmd/cli/repo.go index eed371fb..659981fb 100644 --- a/cmd/cli/repo.go +++ b/cmd/cli/repo.go @@ -6,7 +6,7 @@ import ( var repoCommand *cli.Command = &cli.Command{ Name: "repo", - Usage: "Manage repos.", + Usage: "Manage repositories.", Subcommands: []*cli.Command{ repoListCommand, repoGetCommand, diff --git a/cmd/cli/shared.go b/cmd/cli/shared.go index e54a5464..a861459e 100644 --- a/cmd/cli/shared.go +++ b/cmd/cli/shared.go @@ -1,9 +1,11 @@ package main import ( + "encoding/json" "fmt" "strings" + "github.com/tidwall/gjson" "github.com/urfave/cli/v2" "golang.org/x/oauth2" @@ -20,6 +22,7 @@ func buildClient(cli *cli.Context) *api.Client { return api.NewClient(cli.String("host"), tc) } +// splitFullName splits the full name into namespace, and name. func splitFullName(name string) (string, string, error) { ss := strings.Split(name, "/") if len(ss) != 2 { @@ -28,3 +31,19 @@ func splitFullName(name string) (string, string, error) { return ss[0], ss[1], nil } + +// printJson prints the object as JSON-format. +func printJson(v interface{}, query string) error { + output, err := json.MarshalIndent(v, "", " ") + if err != nil { + return fmt.Errorf("Failed to marshal: %w", err) + } + + if query != "" { + fmt.Println(gjson.GetBytes(output, query)) + return nil + } + + fmt.Println(string(output)) + return nil +} diff --git a/go.mod b/go.mod index ae39890e..183ccff5 100644 --- a/go.mod +++ b/go.mod @@ -61,7 +61,7 @@ require ( github.com/russross/blackfriday/v2 v2.0.1 // indirect github.com/shurcooL/graphql v0.0.0-20200928012149-18c5c3165e3a // indirect github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect - github.com/tidwall/gjson v1.13.0 // indirect + github.com/tidwall/gjson v1.14.0 // indirect github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.0 // indirect github.com/ugorji/go/codec v1.2.6 // indirect diff --git a/go.sum b/go.sum index 346562bc..e277d340 100644 --- a/go.sum +++ b/go.sum @@ -448,6 +448,8 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/tidwall/gjson v1.13.0 h1:3TFY9yxOQShrvmjdM76K+jc66zJeT6D3/VFFYCGQf7M= github.com/tidwall/gjson v1.13.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/gjson v1.14.0 h1:6aeJ0bzojgWLa82gDQHcx3S0Lr/O51I9bJ5nv6JFx5w= +github.com/tidwall/gjson v1.14.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= diff --git a/pkg/api/client.go b/pkg/api/client.go index 02cc47a6..d388bd62 100644 --- a/pkg/api/client.go +++ b/pkg/api/client.go @@ -20,7 +20,8 @@ type ( common *client // Services used for talking to different parts of the Gitploy API. - Repo *RepoService + Repo *RepoService + Deployment *DeploymentService } client struct { @@ -54,6 +55,7 @@ func NewClient(host string, httpClient *http.Client) *Client { } c.Repo = &RepoService{client: c.common} + c.Deployment = &DeploymentService{client: c.common} return c } diff --git a/pkg/api/deployment.go b/pkg/api/deployment.go new file mode 100644 index 00000000..74286b5f --- /dev/null +++ b/pkg/api/deployment.go @@ -0,0 +1,128 @@ +package api + +import ( + "context" + "fmt" + "net/url" + "strconv" + + "github.com/gitploy-io/gitploy/model/ent" + "github.com/gitploy-io/gitploy/model/ent/deployment" +) + +type ( + DeploymentService service + + DeploymentListOptions struct { + ListOptions + + Env string + Status deployment.Status + } + + DeploymentCreateRequest struct { + Type string `json:"type"` + Ref string `json:"ref"` + Env string `json:"env"` + } +) + +// List returns the deployment list. +// It returns an error for a bad request. +func (s *DeploymentService) List(ctx context.Context, namespace, name string, options DeploymentListOptions) ([]*ent.Deployment, error) { + // Build the query. + vals := url.Values{} + + vals.Add("page", strconv.Itoa(options.ListOptions.Page)) + vals.Add("per_page", strconv.Itoa(options.PerPage)) + + if options.Env != "" { + vals.Add("env", options.Env) + } + + if options.Status != "" { + if err := deployment.StatusValidator(options.Status); err != nil { + return nil, err + } + + vals.Add("status", string(options.Status)) + } + + // Request a server. + req, err := s.client.NewRequest( + "GET", + fmt.Sprintf("api/v1/repos/%s/%s/deployments?%s", namespace, name, vals.Encode()), + nil, + ) + if err != nil { + return nil, err + } + + var ds []*ent.Deployment + err = s.client.Do(ctx, req, &ds) + if err != nil { + return nil, err + } + + return ds, nil +} + +// Get returns the deployment. +func (s *DeploymentService) Get(ctx context.Context, namespace, name string, number int) (*ent.Deployment, error) { + req, err := s.client.NewRequest( + "GET", + fmt.Sprintf("api/v1/repos/%s/%s/deployments/%d", namespace, name, number), + nil, + ) + if err != nil { + return nil, err + } + + var d *ent.Deployment + err = s.client.Do(ctx, req, &d) + if err != nil { + return nil, err + } + + return d, nil +} + +// Create requests a server to deploy a specific ref(branch, SHA, tag). +func (s *DeploymentService) Create(ctx context.Context, namespace, name string, body DeploymentCreateRequest) (*ent.Deployment, error) { + req, err := s.client.NewRequest( + "POST", + fmt.Sprintf("api/v1/repos/%s/%s/deployments", namespace, name), + body, + ) + if err != nil { + return nil, err + } + + var d *ent.Deployment + err = s.client.Do(ctx, req, &d) + if err != nil { + return nil, err + } + + return d, nil +} + +// Update requests to trigger the 'waiting' deployment. +func (s *DeploymentService) Update(ctx context.Context, namespace, name string, number int) (*ent.Deployment, error) { + req, err := s.client.NewRequest( + "PUT", + fmt.Sprintf("api/v1/repos/%s/%s/deployments/%d", namespace, name, number), + nil, + ) + if err != nil { + return nil, err + } + + var d *ent.Deployment + err = s.client.Do(ctx, req, &d) + if err != nil { + return nil, err + } + + return d, nil +} diff --git a/pkg/api/deployment_test.go b/pkg/api/deployment_test.go new file mode 100644 index 00000000..bfff7f9d --- /dev/null +++ b/pkg/api/deployment_test.go @@ -0,0 +1,99 @@ +package api + +import ( + "context" + "encoding/json" + "io/ioutil" + "net/http" + "testing" + + "github.com/gitploy-io/gitploy/model/ent" + "github.com/gitploy-io/gitploy/model/ent/deployment" + "gopkg.in/h2non/gock.v1" +) + +func TestDeploymentService_List(t *testing.T) { + t.Run("Verify the query of a request.", func(t *testing.T) { + ds := []*ent.Deployment{ + {ID: 1, Env: "production", Status: deployment.StatusWaiting}, + {ID: 2, Env: "production", Status: deployment.StatusWaiting}, + } + gock.New("https://cloud.gitploy.io"). + Get("/api/v1/repos/gitploy-io/gitploy/deployments"). + MatchParam("env", "production"). + MatchParam("status", "waiting"). + Reply(200). + JSON(ds) + + c := NewClient("https://cloud.gitploy.io/", http.DefaultClient) + + ret, err := c.Deployment.List(context.Background(), "gitploy-io", "gitploy", DeploymentListOptions{ + ListOptions: ListOptions{Page: 1, PerPage: 30}, + Env: "production", + Status: "waiting", + }) + if err != nil { + t.Fatalf("List returns an error: %s", err) + } + + for idx := range ret { + if ret[idx].ID != ds[idx].ID { + t.Fatalf("List = %v, wanted %v", ret, ds) + } + } + }) +} + +func TestDeploymentService_Create(t *testing.T) { + t.Run("Verify the body of a request.", func(t *testing.T) { + d := &ent.Deployment{ + ID: 2, Number: 1, Type: deployment.TypeBranch, Env: "production", Ref: "main", + } + gock.New("https://cloud.gitploy.io"). + Post("/api/v1/repos/gitploy-io/gitploy/deployments"). + AddMatcher(func(req *http.Request, ereq *gock.Request) (bool, error) { + defer req.Body.Close() + output, _ := ioutil.ReadAll(req.Body) + + b := DeploymentCreateRequest{} + if err := json.Unmarshal(output, &b); err != nil { + return false, err + } + + // Verify the fields of the body. + return b.Type == "branch" && b.Env == "production" && b.Ref == "main", nil + }). + Reply(201). + JSON(d) + + c := NewClient("https://cloud.gitploy.io/", http.DefaultClient) + + _, err := c.Deployment.Create(context.Background(), "gitploy-io", "gitploy", DeploymentCreateRequest{ + Type: "branch", + Env: "production", + Ref: "main", + }) + if err != nil { + t.Fatalf("Create returns an error: %s", err) + } + }) +} + +func TestDeploymentService_Update(t *testing.T) { + t.Run("Verify the request.", func(t *testing.T) { + d := &ent.Deployment{ + ID: 2, Number: 1, Type: deployment.TypeBranch, Env: "production", Ref: "main", + } + gock.New("https://cloud.gitploy.io"). + Put("/api/v1/repos/gitploy-io/gitploy/deployments/1"). + Reply(201). + JSON(d) + + c := NewClient("https://cloud.gitploy.io/", http.DefaultClient) + + _, err := c.Deployment.Update(context.Background(), "gitploy-io", "gitploy", 1) + if err != nil { + t.Fatalf("Create returns an error: %s", err) + } + }) +} diff --git a/pkg/api/repo.go b/pkg/api/repo.go index df904005..997979f3 100644 --- a/pkg/api/repo.go +++ b/pkg/api/repo.go @@ -3,6 +3,8 @@ package api import ( "context" "fmt" + "net/url" + "strconv" "github.com/gitploy-io/gitploy/model/ent" ) @@ -54,9 +56,14 @@ func (s *RepoService) ListAll(ctx context.Context) ([]*ent.Repo, error) { // List returns repositories which are on the page. func (s *RepoService) List(ctx context.Context, options RepoListOptions) ([]*ent.Repo, error) { + // Build the query. + vals := url.Values{} + vals.Add("page", strconv.Itoa(options.Page)) + vals.Add("per_page", strconv.Itoa(options.PerPage)) + req, err := s.client.NewRequest( "GET", - fmt.Sprintf("api/v1/repos?page=%d&per_page=%d", options.Page, options.PerPage), + fmt.Sprintf("api/v1/repos?%s", vals.Encode()), nil) if err != nil { return nil, err