sg: cloud ephemeral add status and delete commands (#62265)

* minor refactor & impl GetInstance

standardise all method names on CRUD prefix + Instance suffix

* add `sg cloud status` and `sg cloud delete` commands

* bazel

* small refactor and add status formats

* fixup
This commit is contained in:
William Bezuidenhout 2024-05-02 14:53:41 +02:00 committed by GitHub
parent b73d2456dc
commit 333b6c3666
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 237 additions and 26 deletions

View File

@ -4,11 +4,13 @@ go_library(
name = "cloud",
srcs = [
"client.go",
"delete_command.go",
"deploy_command.go",
"instance.go",
"list_command.go",
"list_versions_command.go",
"printers.go",
"status_command.go",
],
importpath = "github.com/sourcegraph/sourcegraph/dev/sg/internal/cloud",
visibility = ["//dev/sg:__subpackages__"],

View File

@ -28,6 +28,15 @@ const DevEnvironment = "dev"
// It is set to internal because in cloud, internal instance types does not have metrics or security enabled.
const EphemeralInstanceType = "internal"
var _ EphemeralClient = &Client{}
type EphemeralClient interface {
CreateInstance(context.Context, *DeploymentSpec) (*Instance, error)
GetInstance(context.Context, string) (*Instance, error)
ListInstances(context.Context) ([]*Instance, error)
DeleteInstance(context.Context, string) error
}
type Client struct {
client cloudapiv1connect.InstanceServiceClient
token string
@ -42,7 +51,7 @@ type DeploymentSpec struct {
func NewDeploymentSpec(name, version string) *DeploymentSpec {
return &DeploymentSpec{
Name: name,
Name: sanitizeInstanceName(name),
Version: version,
InstanceFeatures: map[string]string{
"ephemeral": "true", // need to have this to make the instance ephemeral
@ -88,6 +97,21 @@ func newRequestWithToken[T any](token string, message *T) *connect.Request[T] {
return req
}
func (c *Client) GetInstance(ctx context.Context, name string) (*Instance, error) {
req := newRequestWithToken(c.token, &cloudapiv1.GetInstanceRequest{
Name: name,
Environment: DevEnvironment,
},
)
resp, err := c.client.GetInstance(ctx, req)
if err != nil {
return nil, errors.Wrapf(err, "failed to get instance %q", name)
}
return newInstance(resp.Msg.GetInstance()), nil
}
func (c *Client) ListInstances(ctx context.Context) ([]*Instance, error) {
req := newRequestWithToken(c.token, &cloudapiv1.ListInstancesRequest{
InstanceFilter: &cloudapiv1.InstanceFilter{
@ -105,7 +129,7 @@ func (c *Client) ListInstances(ctx context.Context) ([]*Instance, error) {
return toInstances(resp.Msg.GetInstances()...), nil
}
func (c *Client) DeployVersion(ctx context.Context, spec *DeploymentSpec) (*Instance, error) {
func (c *Client) CreateInstance(ctx context.Context, spec *DeploymentSpec) (*Instance, error) {
// TODO(burmudar): Better method to get LicenseKeys
licenseKey := os.Getenv("EPHEMERAL_LICENSE_KEY")
if licenseKey == "" {
@ -134,3 +158,8 @@ func (c *Client) DeployVersion(ctx context.Context, spec *DeploymentSpec) (*Inst
func (c *Client) DeleteInstance(ctx context.Context, name string) error {
return nil
}
func sanitizeInstanceName(name string) string {
name = strings.ToLower(name)
return strings.ReplaceAll(name, "/", "-")
}

View File

@ -0,0 +1,68 @@
package cloud
import (
"fmt"
"github.com/urfave/cli/v2"
"github.com/sourcegraph/sourcegraph/dev/sg/internal/repo"
"github.com/sourcegraph/sourcegraph/dev/sg/internal/std"
"github.com/sourcegraph/sourcegraph/lib/errors"
"github.com/sourcegraph/sourcegraph/lib/output"
)
var DeleteEphemeralCommand = cli.Command{
Name: "delete",
Usage: "sg could delete <name/slug>",
Description: "delete ephemeral cloud instance identified either by the current branch or provided as a cli arg",
Action: wipAction(deleteCloudEphemeral),
Flags: []cli.Flag{
&cli.StringFlag{
Name: "name",
Usage: "name or slug of the cloud ephemeral instance to delete",
},
},
}
func deleteCloudEphemeral(ctx *cli.Context) error {
email, err := GetGCloudAccount(ctx.Context)
if err != nil {
return err
}
cloudClient, err := NewClient(ctx.Context, email, APIEndpoint)
if err != nil {
return err
}
name := ctx.String("name")
if name == "" {
currentBranch, err := repo.GetCurrentBranch(ctx.Context)
if err != nil {
return errors.Wrap(err, "failed to determine current branch")
}
name = currentBranch
}
name = sanitizeInstanceName(name)
var answ string
_, err = std.PromptAndScan(std.Out, fmt.Sprintf("Are you sure you want to delete ephemeral instance %q? (yes/no)", name), &answ)
if err != nil {
return err
}
if oneOfEquals(answ, "no", "n") {
return ErrUserCancelled
}
cloudEmoji := "☁️"
pending := std.Out.Pending(output.Linef(cloudEmoji, output.StylePending, "Deleting ephemeral instance %q", name))
err = cloudClient.DeleteInstance(ctx.Context, name)
if err != nil {
pending.Complete(output.Linef(output.EmojiFailure, output.StyleFailure, "deleting of %q failed", name))
return err
}
pending.Complete(output.Linef(output.EmojiSuccess, output.StyleBold, "Ephemeral instance %q deleted", name))
return nil
}

View File

@ -27,7 +27,7 @@ var DeployEphemeralCommand = cli.Command{
Name: "deploy",
Usage: "sg could deploy --branch <branch> --tag <tag>",
Description: "Deploy the specified branch or tag to an ephemeral Sourcegraph Cloud environment",
Action: deployCloudEphemeral,
Action: wipAction(deployCloudEphemeral),
Flags: []cli.Flag{
&cli.StringFlag{
Name: "name",
@ -108,6 +108,19 @@ func triggerEphemeralBuild(ctx context.Context, currRepo *repo.GitRepo) (*buildk
return build, nil
}
func wipAction(actionFn cli.ActionFunc) cli.ActionFunc {
if actionFn == nil {
return nil
}
return func(ctx *cli.Context) error {
if err := printWIPNotice(ctx); err != nil {
return err
}
return actionFn(ctx)
}
}
func printWIPNotice(ctx *cli.Context) error {
if ctx.Bool("skip-wip-notice") {
return nil
@ -146,7 +159,7 @@ func createDeploymentForVersion(ctx context.Context, name, version string) error
name,
version,
)
inst, err := cloudClient.DeployVersion(ctx, spec)
inst, err := cloudClient.CreateInstance(ctx, spec)
if err != nil {
pending.Complete(output.Linef(output.EmojiFailure, output.StyleFailure, "deployment failed: %v", err))
return errors.Wrapf(err, "failed to deploy version %v", version)

View File

@ -35,12 +35,10 @@ Environment : %s
Version : %s
Hostname : %s
AdminEmail : %s
CreatedAt : %s
Project : %s
Region : %s
DeletetAt : %s
Status : %s
`, i.ID, i.Name, i.InstanceType, i.Environment, i.Version, i.Hostname, i.AdminEmail,
i.CreatedAt, i.Project, i.Region, i.DeletedAt,

View File

@ -12,7 +12,7 @@ var ListEphemeralCommand = cli.Command{
Name: "list",
Usage: "sg could list",
Description: "list ephemeral cloud instances attached to your GCP account",
Action: listCloudEphemeral,
Action: wipAction(listCloudEphemeral),
Flags: []cli.Flag{
&cli.BoolFlag{
Name: "json",
@ -44,22 +44,8 @@ func listCloudEphemeral(ctx *cli.Context) error {
if ctx.Bool("json") {
printer = &jsonInstancePrinter{w: os.Stdout}
} else {
valueFunc := func(inst *Instance) []any {
name := inst.Name
if len(name) > 20 {
name = name[:20]
}
status := inst.Status
createdAt := inst.CreatedAt.String()
return []any{
name, status, createdAt,
}
}
printer = newTerminalInstancePrinter(valueFunc, "%-20s %-11s %s", "Name", "Status", "Created At")
printer = newDefaultTerminalInstancePrinter()
}
return printer.Print(instances)
return printer.Print(instances...)
}

View File

@ -10,7 +10,11 @@ import (
)
type Printer interface {
Print([]*Instance) error
Print(...*Instance) error
}
type rawInstancePrinter struct {
w io.Writer
}
type terminalInstancePrinter struct {
@ -23,6 +27,24 @@ type jsonInstancePrinter struct {
w io.Writer
}
func newDefaultTerminalInstancePrinter() *terminalInstancePrinter {
valueFunc := func(inst *Instance) []any {
name := inst.Name
if len(name) > 20 {
name = name[:20]
}
status := inst.Status
createdAt := inst.CreatedAt.String()
return []any{
name, status, createdAt,
}
}
return newTerminalInstancePrinter(valueFunc, "%-20s %-11s %s", "Name", "Status", "Created At")
}
func newTerminalInstancePrinter(valueFunc func(i *Instance) []any, headingFmt string, headings ...string) *terminalInstancePrinter {
anyHeadings := make([]any, 0, len(headings))
for _, h := range headings {
@ -36,7 +58,7 @@ func newTerminalInstancePrinter(valueFunc func(i *Instance) []any, headingFmt st
}
}
func (p *terminalInstancePrinter) Print(items []*Instance) error {
func (p *terminalInstancePrinter) Print(items ...*Instance) error {
heading := fmt.Sprintf(p.headingFmt, p.headings...)
std.Out.WriteLine(output.Line("", output.StyleBold, heading))
for _, inst := range items {
@ -51,6 +73,18 @@ func newJSONInstancePrinter(w io.Writer) *jsonInstancePrinter {
return &jsonInstancePrinter{w: w}
}
func (p *jsonInstancePrinter) Print(items []*Instance) error {
func (p *jsonInstancePrinter) Print(items ...*Instance) error {
return json.NewEncoder(p.w).Encode(items)
}
func newRawInstancePrinter(w io.Writer) *rawInstancePrinter {
return &rawInstancePrinter{w: w}
}
func (p *rawInstancePrinter) Print(items ...*Instance) error {
for _, inst := range items {
fmt.Fprintln(p.w, inst.String())
}
return nil
}

View File

@ -0,0 +1,79 @@
package cloud
import (
"os"
"github.com/urfave/cli/v2"
"github.com/sourcegraph/sourcegraph/dev/sg/internal/repo"
"github.com/sourcegraph/sourcegraph/dev/sg/internal/std"
"github.com/sourcegraph/sourcegraph/lib/errors"
"github.com/sourcegraph/sourcegraph/lib/output"
)
var StatusEphemeralCommand = cli.Command{
Name: "status",
Usage: "sg could status",
Description: "get the status of the ephemeral cloud instance for this branch or instance with the provided slug",
Action: wipAction(statusCloudEphemeral),
Flags: []cli.Flag{
&cli.StringFlag{
Name: "name",
Usage: "name of the instance to get the status of",
},
&cli.BoolFlag{
Name: "json",
Usage: "print the instance details in JSON",
},
&cli.BoolFlag{
Name: "raw",
Usage: "print all of the instance details",
},
},
}
func statusCloudEphemeral(ctx *cli.Context) error {
email, err := GetGCloudAccount(ctx.Context)
if err != nil {
return err
}
cloudClient, err := NewClient(ctx.Context, email, APIEndpoint)
if err != nil {
return err
}
name := ctx.String("name")
if name == "" {
currentBranch, err := repo.GetCurrentBranch(ctx.Context)
if err != nil {
return errors.Wrap(err, "failed to determine current branch")
}
name = currentBranch
}
name = sanitizeInstanceName(name)
cloudEmoji := "☁️"
pending := std.Out.Pending(output.Linef(cloudEmoji, output.StylePending, "Getting status of ephemeral instance %q", name))
inst, err := cloudClient.GetInstance(ctx.Context, name)
if err != nil {
pending.Complete(output.Linef(output.EmojiFailure, output.StyleFailure, "getting status of %q failed", name))
return err
}
pending.Complete(output.Linef(output.EmojiSuccess, output.StyleBold, "Ephemeral instance %q status retrieved", name))
var printer Printer
switch {
case ctx.Bool("json"):
printer = newJSONInstancePrinter(os.Stdout)
case ctx.Bool("raw"):
printer = newRawInstancePrinter(os.Stdout)
default:
printer = newDefaultTerminalInstancePrinter()
}
std.Out.Write("Ephemeral instance details:")
printer.Print(inst)
return nil
}

View File

@ -48,6 +48,8 @@ var cloudCommand = &cli.Command{
&cloud.DeployEphemeralCommand,
&cloud.ListEphemeralCommand,
&cloud.ListVersionsEphemeralCommand,
&cloud.StatusEphemeralCommand,
&cloud.DeleteEphemeralCommand,
},
}