mirror of
https://github.com/sourcegraph/sourcegraph.git
synced 2026-02-06 14:51:44 +00:00
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:
parent
b73d2456dc
commit
333b6c3666
@ -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__"],
|
||||
|
||||
@ -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, "/", "-")
|
||||
}
|
||||
|
||||
68
dev/sg/internal/cloud/delete_command.go
Normal file
68
dev/sg/internal/cloud/delete_command.go
Normal 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
|
||||
}
|
||||
@ -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)
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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...)
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
79
dev/sg/internal/cloud/status_command.go
Normal file
79
dev/sg/internal/cloud/status_command.go
Normal 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
|
||||
}
|
||||
@ -48,6 +48,8 @@ var cloudCommand = &cli.Command{
|
||||
&cloud.DeployEphemeralCommand,
|
||||
&cloud.ListEphemeralCommand,
|
||||
&cloud.ListVersionsEphemeralCommand,
|
||||
&cloud.StatusEphemeralCommand,
|
||||
&cloud.DeleteEphemeralCommand,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user