Skip to content

Commit 183d199

Browse files
authored
feat: manage deployments (#929)
1 parent 63c19bf commit 183d199

File tree

7 files changed

+657
-0
lines changed

7 files changed

+657
-0
lines changed

cmd/deployments/deployments.go

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package deployments
2+
3+
import "github.com/urfave/cli/v2"
4+
5+
const flagSubdomain = "subdomain"
6+
7+
func commonFlags() []cli.Flag {
8+
return []cli.Flag{
9+
&cli.StringFlag{ //nolint:exhaustruct
10+
Name: flagSubdomain,
11+
Usage: "Project's subdomain to operate on, defaults to linked project",
12+
EnvVars: []string{"NHOST_SUBDOMAIN"},
13+
},
14+
}
15+
}
16+
17+
func Command() *cli.Command {
18+
return &cli.Command{ //nolint:exhaustruct
19+
Name: "deployments",
20+
Aliases: []string{},
21+
Usage: "Manage deployments",
22+
Subcommands: []*cli.Command{
23+
CommandList(),
24+
CommandLogs(),
25+
CommandNew(),
26+
},
27+
}
28+
}

cmd/deployments/list.go

+106
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
package deployments
2+
3+
import (
4+
"fmt"
5+
"time"
6+
7+
"github.com/nhost/cli/clienv"
8+
"github.com/nhost/cli/nhostclient/graphql"
9+
"github.com/urfave/cli/v2"
10+
)
11+
12+
func CommandList() *cli.Command {
13+
return &cli.Command{ //nolint:exhaustruct
14+
Name: "list",
15+
Aliases: []string{},
16+
Usage: "List deployments in the cloud environment",
17+
Action: commandList,
18+
Flags: commonFlags(),
19+
}
20+
}
21+
22+
func printDeployments(ce *clienv.CliEnv, deployments []*graphql.ListDeployments_Deployments) {
23+
id := clienv.Column{
24+
Header: "ID",
25+
Rows: make([]string, 0),
26+
}
27+
28+
date := clienv.Column{
29+
Header: "Date",
30+
Rows: make([]string, 0),
31+
}
32+
33+
duration := clienv.Column{
34+
Header: "Duration",
35+
Rows: make([]string, 0),
36+
}
37+
38+
status := clienv.Column{
39+
Header: "Status",
40+
Rows: make([]string, 0),
41+
}
42+
43+
user := clienv.Column{
44+
Header: "User",
45+
Rows: make([]string, 0),
46+
}
47+
48+
ref := clienv.Column{
49+
Header: "Ref",
50+
Rows: make([]string, 0),
51+
}
52+
53+
message := clienv.Column{
54+
Header: "Message",
55+
Rows: make([]string, 0),
56+
}
57+
58+
for _, d := range deployments {
59+
var startedAt time.Time
60+
if d.DeploymentStartedAt != nil && !d.DeploymentStartedAt.IsZero() {
61+
startedAt = *d.DeploymentStartedAt
62+
}
63+
64+
var endedAt time.Time
65+
var deplPuration time.Duration
66+
if d.DeploymentEndedAt != nil && !d.DeploymentEndedAt.IsZero() {
67+
endedAt = *d.DeploymentEndedAt
68+
deplPuration = endedAt.Sub(startedAt)
69+
}
70+
71+
id.Rows = append(id.Rows, d.ID)
72+
date.Rows = append(date.Rows, startedAt.Format(time.RFC3339))
73+
duration.Rows = append(duration.Rows, deplPuration.String())
74+
status.Rows = append(status.Rows, *d.DeploymentStatus)
75+
user.Rows = append(user.Rows, *d.CommitUserName)
76+
ref.Rows = append(ref.Rows, d.CommitSha)
77+
message.Rows = append(message.Rows, *d.CommitMessage)
78+
}
79+
80+
ce.Println("%s", clienv.Table(id, date, duration, status, user, ref, message))
81+
}
82+
83+
func commandList(cCtx *cli.Context) error {
84+
ce := clienv.FromCLI(cCtx)
85+
86+
proj, err := ce.GetAppInfo(cCtx.Context, cCtx.String(flagSubdomain))
87+
if err != nil {
88+
return fmt.Errorf("failed to get app info: %w", err)
89+
}
90+
91+
cl, err := ce.GetNhostClient(cCtx.Context)
92+
if err != nil {
93+
return fmt.Errorf("failed to get nhost client: %w", err)
94+
}
95+
deployments, err := cl.ListDeployments(
96+
cCtx.Context,
97+
proj.ID,
98+
)
99+
if err != nil {
100+
return fmt.Errorf("failed to get deployments: %w", err)
101+
}
102+
103+
printDeployments(ce, deployments.GetDeployments())
104+
105+
return nil
106+
}

cmd/deployments/logs.go

+131
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
package deployments
2+
3+
import (
4+
"context"
5+
"errors"
6+
"fmt"
7+
"time"
8+
9+
"github.com/nhost/cli/clienv"
10+
"github.com/nhost/cli/nhostclient"
11+
"github.com/urfave/cli/v2"
12+
)
13+
14+
const (
15+
flagFollow = "follow"
16+
flagTimeout = "timeout"
17+
)
18+
19+
func CommandLogs() *cli.Command {
20+
return &cli.Command{ //nolint:exhaustruct
21+
Name: "logs",
22+
Aliases: []string{},
23+
Usage: "View deployments logs in the cloud environment",
24+
Action: commandLogs,
25+
ArgsUsage: "<deployment_id>",
26+
Flags: append(
27+
commonFlags(),
28+
[]cli.Flag{
29+
&cli.BoolFlag{ //nolint:exhaustruct
30+
Name: flagFollow,
31+
Usage: "Specify if the logs should be streamed",
32+
Value: false,
33+
},
34+
&cli.DurationFlag{ //nolint:exhaustruct
35+
Name: flagTimeout,
36+
Usage: "Specify the timeout for streaming logs",
37+
Value: time.Minute * 5, //nolint:mnd
38+
},
39+
}...,
40+
),
41+
}
42+
}
43+
44+
func showLogsSimple(
45+
ctx context.Context,
46+
ce *clienv.CliEnv,
47+
cl *nhostclient.Client,
48+
deploymentID string,
49+
) error {
50+
resp, err := cl.GetDeploymentLogs(ctx, deploymentID)
51+
if err != nil {
52+
return fmt.Errorf("failed to get deployments: %w", err)
53+
}
54+
55+
for _, log := range resp.GetDeploymentLogs() {
56+
ce.Println(
57+
"%s %s",
58+
log.GetCreatedAt().Format(time.RFC3339),
59+
log.GetMessage(),
60+
)
61+
}
62+
63+
return nil
64+
}
65+
66+
func showLogsFollow(
67+
ctx context.Context,
68+
ce *clienv.CliEnv,
69+
cl *nhostclient.Client,
70+
deploymentID string,
71+
) (string, error) {
72+
ticker := time.NewTicker(time.Second * 2) //nolint:mnd
73+
74+
printed := make(map[string]struct{})
75+
76+
for {
77+
select {
78+
case <-ctx.Done():
79+
return "", nil
80+
case <-ticker.C:
81+
resp, err := cl.GetDeploymentLogs(ctx, deploymentID)
82+
if err != nil {
83+
return "", fmt.Errorf("failed to get deployments: %w", err)
84+
}
85+
86+
for _, log := range resp.GetDeploymentLogs() {
87+
if _, ok := printed[log.GetID()]; !ok {
88+
ce.Println(
89+
"%s %s",
90+
log.GetCreatedAt().Format(time.RFC3339),
91+
log.GetMessage(),
92+
)
93+
printed[log.GetID()] = struct{}{}
94+
}
95+
}
96+
97+
if resp.Deployment.DeploymentEndedAt != nil {
98+
return *resp.Deployment.DeploymentStatus, nil
99+
}
100+
}
101+
}
102+
}
103+
104+
func commandLogs(cCtx *cli.Context) error {
105+
deploymentID := cCtx.Args().First()
106+
if deploymentID == "" {
107+
return errors.New("deployment_id is required") //nolint:goerr113
108+
}
109+
110+
ce := clienv.FromCLI(cCtx)
111+
112+
cl, err := ce.GetNhostClient(cCtx.Context)
113+
if err != nil {
114+
return fmt.Errorf("failed to get nhost client: %w", err)
115+
}
116+
117+
if cCtx.Bool(flagFollow) {
118+
ctx, cancel := context.WithTimeout(cCtx.Context, cCtx.Duration(flagTimeout))
119+
defer cancel()
120+
121+
if _, err := showLogsFollow(ctx, ce, cl, deploymentID); err != nil {
122+
return err
123+
}
124+
} else {
125+
if err := showLogsSimple(cCtx.Context, ce, cl, deploymentID); err != nil {
126+
return err
127+
}
128+
}
129+
130+
return nil
131+
}

cmd/deployments/new.go

+117
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
package deployments
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"time"
7+
8+
"github.com/nhost/cli/clienv"
9+
"github.com/nhost/cli/nhostclient/graphql"
10+
"github.com/urfave/cli/v2"
11+
)
12+
13+
const (
14+
flagRef = "ref"
15+
flagMessage = "message"
16+
flagUser = "user"
17+
flagUserAvatarURL = "user-avatar-url"
18+
)
19+
20+
func CommandNew() *cli.Command {
21+
return &cli.Command{ //nolint:exhaustruct
22+
Name: "new",
23+
Aliases: []string{},
24+
Usage: "[EXPERIMENTAL] Create a new deployment",
25+
ArgsUsage: "<git_ref>",
26+
Action: commandNew,
27+
Flags: append(
28+
commonFlags(),
29+
[]cli.Flag{
30+
&cli.BoolFlag{ //nolint:exhaustruct
31+
Name: flagFollow,
32+
Usage: "Specify if the logs should be streamed. If set, the command will wait for the deployment to finish and stream the logs. If the deployment fails the command will return an error.", //nolint:lll
33+
Value: false,
34+
},
35+
&cli.DurationFlag{ //nolint:exhaustruct
36+
Name: flagTimeout,
37+
Usage: "Specify the timeout for streaming logs",
38+
Value: time.Minute * 5, //nolint:mnd
39+
},
40+
&cli.StringFlag{ //nolint:exhaustruct
41+
Name: flagRef,
42+
Usage: "Git reference",
43+
EnvVars: []string{"GITHUB_SHA"},
44+
Required: true,
45+
},
46+
&cli.StringFlag{ //nolint:exhaustruct
47+
Name: flagMessage,
48+
Usage: "Commit message",
49+
Required: true,
50+
},
51+
&cli.StringFlag{ //nolint:exhaustruct
52+
Name: flagUser,
53+
Usage: "Commit user name",
54+
EnvVars: []string{"GITHUB_ACTOR"},
55+
Required: true,
56+
},
57+
&cli.StringFlag{ //nolint:exhaustruct
58+
Name: flagUserAvatarURL,
59+
Usage: "Commit user avatar URL",
60+
},
61+
}...,
62+
),
63+
}
64+
}
65+
66+
func ptr[i any](v i) *i {
67+
return &v
68+
}
69+
70+
func commandNew(cCtx *cli.Context) error {
71+
ce := clienv.FromCLI(cCtx)
72+
73+
cl, err := ce.GetNhostClient(cCtx.Context)
74+
if err != nil {
75+
return fmt.Errorf("failed to get nhost client: %w", err)
76+
}
77+
78+
proj, err := ce.GetAppInfo(cCtx.Context, cCtx.String(flagSubdomain))
79+
if err != nil {
80+
return fmt.Errorf("failed to get app info: %w", err)
81+
}
82+
83+
resp, err := cl.InsertDeployment(
84+
cCtx.Context,
85+
graphql.DeploymentsInsertInput{
86+
App: nil,
87+
AppID: ptr(proj.ID),
88+
CommitMessage: ptr(cCtx.String(flagMessage)),
89+
CommitSha: ptr(cCtx.String(flagRef)),
90+
CommitUserAvatarURL: ptr(cCtx.String(flagUserAvatarURL)),
91+
CommitUserName: ptr(cCtx.String(flagUser)),
92+
DeploymentStatus: ptr("SCHEDULED"),
93+
},
94+
)
95+
if err != nil {
96+
return fmt.Errorf("failed to insert deployment: %w", err)
97+
}
98+
99+
ce.Println("Deployment created: %s", resp.InsertDeployment.ID)
100+
101+
if cCtx.Bool(flagFollow) {
102+
ce.Println("")
103+
ctx, cancel := context.WithTimeout(cCtx.Context, cCtx.Duration(flagTimeout))
104+
defer cancel()
105+
106+
status, err := showLogsFollow(ctx, ce, cl, resp.InsertDeployment.ID)
107+
if err != nil {
108+
return fmt.Errorf("error streaming logs: %w", err)
109+
}
110+
111+
if status != "DEPLOYED" {
112+
return fmt.Errorf("deployment failed: %s", status) //nolint:goerr113
113+
}
114+
}
115+
116+
return nil
117+
}

0 commit comments

Comments
 (0)