Skip to content

Commit

Permalink
feat: manage deployments (#929)
Browse files Browse the repository at this point in the history
  • Loading branch information
dbarrosop authored Dec 5, 2024
1 parent 63c19bf commit 183d199
Show file tree
Hide file tree
Showing 7 changed files with 657 additions and 0 deletions.
28 changes: 28 additions & 0 deletions cmd/deployments/deployments.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package deployments

import "github.com/urfave/cli/v2"

const flagSubdomain = "subdomain"

func commonFlags() []cli.Flag {
return []cli.Flag{
&cli.StringFlag{ //nolint:exhaustruct
Name: flagSubdomain,
Usage: "Project's subdomain to operate on, defaults to linked project",
EnvVars: []string{"NHOST_SUBDOMAIN"},
},
}
}

func Command() *cli.Command {
return &cli.Command{ //nolint:exhaustruct
Name: "deployments",
Aliases: []string{},
Usage: "Manage deployments",
Subcommands: []*cli.Command{
CommandList(),
CommandLogs(),
CommandNew(),
},
}
}
106 changes: 106 additions & 0 deletions cmd/deployments/list.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package deployments

import (
"fmt"
"time"

"github.com/nhost/cli/clienv"
"github.com/nhost/cli/nhostclient/graphql"
"github.com/urfave/cli/v2"
)

func CommandList() *cli.Command {
return &cli.Command{ //nolint:exhaustruct
Name: "list",
Aliases: []string{},
Usage: "List deployments in the cloud environment",
Action: commandList,
Flags: commonFlags(),
}
}

func printDeployments(ce *clienv.CliEnv, deployments []*graphql.ListDeployments_Deployments) {
id := clienv.Column{
Header: "ID",
Rows: make([]string, 0),
}

date := clienv.Column{
Header: "Date",
Rows: make([]string, 0),
}

duration := clienv.Column{
Header: "Duration",
Rows: make([]string, 0),
}

status := clienv.Column{
Header: "Status",
Rows: make([]string, 0),
}

user := clienv.Column{
Header: "User",
Rows: make([]string, 0),
}

ref := clienv.Column{
Header: "Ref",
Rows: make([]string, 0),
}

message := clienv.Column{
Header: "Message",
Rows: make([]string, 0),
}

for _, d := range deployments {
var startedAt time.Time
if d.DeploymentStartedAt != nil && !d.DeploymentStartedAt.IsZero() {
startedAt = *d.DeploymentStartedAt
}

var endedAt time.Time
var deplPuration time.Duration
if d.DeploymentEndedAt != nil && !d.DeploymentEndedAt.IsZero() {
endedAt = *d.DeploymentEndedAt
deplPuration = endedAt.Sub(startedAt)
}

id.Rows = append(id.Rows, d.ID)
date.Rows = append(date.Rows, startedAt.Format(time.RFC3339))
duration.Rows = append(duration.Rows, deplPuration.String())
status.Rows = append(status.Rows, *d.DeploymentStatus)
user.Rows = append(user.Rows, *d.CommitUserName)
ref.Rows = append(ref.Rows, d.CommitSha)
message.Rows = append(message.Rows, *d.CommitMessage)
}

ce.Println("%s", clienv.Table(id, date, duration, status, user, ref, message))
}

func commandList(cCtx *cli.Context) error {
ce := clienv.FromCLI(cCtx)

proj, err := ce.GetAppInfo(cCtx.Context, cCtx.String(flagSubdomain))
if err != nil {
return fmt.Errorf("failed to get app info: %w", err)
}

cl, err := ce.GetNhostClient(cCtx.Context)
if err != nil {
return fmt.Errorf("failed to get nhost client: %w", err)
}
deployments, err := cl.ListDeployments(
cCtx.Context,
proj.ID,
)
if err != nil {
return fmt.Errorf("failed to get deployments: %w", err)
}

printDeployments(ce, deployments.GetDeployments())

return nil
}
131 changes: 131 additions & 0 deletions cmd/deployments/logs.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
package deployments

import (
"context"
"errors"
"fmt"
"time"

"github.com/nhost/cli/clienv"
"github.com/nhost/cli/nhostclient"
"github.com/urfave/cli/v2"
)

const (
flagFollow = "follow"
flagTimeout = "timeout"
)

func CommandLogs() *cli.Command {
return &cli.Command{ //nolint:exhaustruct
Name: "logs",
Aliases: []string{},
Usage: "View deployments logs in the cloud environment",
Action: commandLogs,
ArgsUsage: "<deployment_id>",
Flags: append(
commonFlags(),
[]cli.Flag{
&cli.BoolFlag{ //nolint:exhaustruct
Name: flagFollow,
Usage: "Specify if the logs should be streamed",
Value: false,
},
&cli.DurationFlag{ //nolint:exhaustruct
Name: flagTimeout,
Usage: "Specify the timeout for streaming logs",
Value: time.Minute * 5, //nolint:mnd
},
}...,
),
}
}

func showLogsSimple(
ctx context.Context,
ce *clienv.CliEnv,
cl *nhostclient.Client,
deploymentID string,
) error {
resp, err := cl.GetDeploymentLogs(ctx, deploymentID)
if err != nil {
return fmt.Errorf("failed to get deployments: %w", err)
}

for _, log := range resp.GetDeploymentLogs() {
ce.Println(
"%s %s",
log.GetCreatedAt().Format(time.RFC3339),
log.GetMessage(),
)
}

return nil
}

func showLogsFollow(
ctx context.Context,
ce *clienv.CliEnv,
cl *nhostclient.Client,
deploymentID string,
) (string, error) {
ticker := time.NewTicker(time.Second * 2) //nolint:mnd

printed := make(map[string]struct{})

for {
select {
case <-ctx.Done():
return "", nil
case <-ticker.C:
resp, err := cl.GetDeploymentLogs(ctx, deploymentID)
if err != nil {
return "", fmt.Errorf("failed to get deployments: %w", err)
}

for _, log := range resp.GetDeploymentLogs() {
if _, ok := printed[log.GetID()]; !ok {
ce.Println(
"%s %s",
log.GetCreatedAt().Format(time.RFC3339),
log.GetMessage(),
)
printed[log.GetID()] = struct{}{}
}
}

if resp.Deployment.DeploymentEndedAt != nil {
return *resp.Deployment.DeploymentStatus, nil
}
}
}
}

func commandLogs(cCtx *cli.Context) error {
deploymentID := cCtx.Args().First()
if deploymentID == "" {
return errors.New("deployment_id is required") //nolint:goerr113
}

ce := clienv.FromCLI(cCtx)

cl, err := ce.GetNhostClient(cCtx.Context)
if err != nil {
return fmt.Errorf("failed to get nhost client: %w", err)
}

if cCtx.Bool(flagFollow) {
ctx, cancel := context.WithTimeout(cCtx.Context, cCtx.Duration(flagTimeout))
defer cancel()

if _, err := showLogsFollow(ctx, ce, cl, deploymentID); err != nil {
return err
}
} else {
if err := showLogsSimple(cCtx.Context, ce, cl, deploymentID); err != nil {
return err
}
}

return nil
}
117 changes: 117 additions & 0 deletions cmd/deployments/new.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
package deployments

import (
"context"
"fmt"
"time"

"github.com/nhost/cli/clienv"
"github.com/nhost/cli/nhostclient/graphql"
"github.com/urfave/cli/v2"
)

const (
flagRef = "ref"
flagMessage = "message"
flagUser = "user"
flagUserAvatarURL = "user-avatar-url"
)

func CommandNew() *cli.Command {
return &cli.Command{ //nolint:exhaustruct
Name: "new",
Aliases: []string{},
Usage: "[EXPERIMENTAL] Create a new deployment",
ArgsUsage: "<git_ref>",
Action: commandNew,
Flags: append(
commonFlags(),
[]cli.Flag{
&cli.BoolFlag{ //nolint:exhaustruct
Name: flagFollow,
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
Value: false,
},
&cli.DurationFlag{ //nolint:exhaustruct
Name: flagTimeout,
Usage: "Specify the timeout for streaming logs",
Value: time.Minute * 5, //nolint:mnd
},
&cli.StringFlag{ //nolint:exhaustruct
Name: flagRef,
Usage: "Git reference",
EnvVars: []string{"GITHUB_SHA"},
Required: true,
},
&cli.StringFlag{ //nolint:exhaustruct
Name: flagMessage,
Usage: "Commit message",
Required: true,
},
&cli.StringFlag{ //nolint:exhaustruct
Name: flagUser,
Usage: "Commit user name",
EnvVars: []string{"GITHUB_ACTOR"},
Required: true,
},
&cli.StringFlag{ //nolint:exhaustruct
Name: flagUserAvatarURL,
Usage: "Commit user avatar URL",
},
}...,
),
}
}

func ptr[i any](v i) *i {
return &v
}

func commandNew(cCtx *cli.Context) error {
ce := clienv.FromCLI(cCtx)

cl, err := ce.GetNhostClient(cCtx.Context)
if err != nil {
return fmt.Errorf("failed to get nhost client: %w", err)
}

proj, err := ce.GetAppInfo(cCtx.Context, cCtx.String(flagSubdomain))
if err != nil {
return fmt.Errorf("failed to get app info: %w", err)
}

resp, err := cl.InsertDeployment(
cCtx.Context,
graphql.DeploymentsInsertInput{
App: nil,
AppID: ptr(proj.ID),
CommitMessage: ptr(cCtx.String(flagMessage)),
CommitSha: ptr(cCtx.String(flagRef)),
CommitUserAvatarURL: ptr(cCtx.String(flagUserAvatarURL)),
CommitUserName: ptr(cCtx.String(flagUser)),
DeploymentStatus: ptr("SCHEDULED"),
},
)
if err != nil {
return fmt.Errorf("failed to insert deployment: %w", err)
}

ce.Println("Deployment created: %s", resp.InsertDeployment.ID)

if cCtx.Bool(flagFollow) {
ce.Println("")
ctx, cancel := context.WithTimeout(cCtx.Context, cCtx.Duration(flagTimeout))
defer cancel()

status, err := showLogsFollow(ctx, ce, cl, resp.InsertDeployment.ID)
if err != nil {
return fmt.Errorf("error streaming logs: %w", err)
}

if status != "DEPLOYED" {
return fmt.Errorf("deployment failed: %s", status) //nolint:goerr113
}
}

return nil
}
Loading

0 comments on commit 183d199

Please sign in to comment.