Skip to content

Commit

Permalink
add playground mgmt commands
Browse files Browse the repository at this point in the history
  • Loading branch information
iximiuz committed Mar 6, 2024
1 parent b79164f commit f489183
Show file tree
Hide file tree
Showing 10 changed files with 370 additions and 11 deletions.
2 changes: 1 addition & 1 deletion cmd/auth/whoami.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ func runWhoAmI(ctx context.Context, cli labcli.CLI) error {
return nil
}

me, err := cli.Client().GetMe(ctx)
me, err := cli.Client().GetAccount(ctx)
if err != nil {
return err
}
Expand Down
149 changes: 149 additions & 0 deletions cmd/playground/list.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
package playground

import (
"context"
"fmt"
"io"
"strings"
"text/tabwriter"
"time"

"github.com/dustin/go-humanize"
"github.com/spf13/cobra"

"github.com/iximiuz/labctl/internal/api"
"github.com/iximiuz/labctl/internal/labcli"
)

type listOptions struct {
all bool
quiet bool
}

func newListCommand(cli labcli.CLI) *cobra.Command {
var opts listOptions

cmd := &cobra.Command{
Use: "list [flags]",
Aliases: []string{"ls"},
Short: `List recent playgrounds (up to 50)`,
RunE: func(cmd *cobra.Command, args []string) error {
return labcli.WrapStatusError(runListPlaygrounds(cmd.Context(), cli, &opts))
},
}

flags := cmd.Flags()

flags.BoolVarP(
&opts.all,
"all",
"a",
false,
`List all playgrounds (including terminated)`,
)
flags.BoolVarP(
&opts.quiet,
"quiet",
"q",
false,
`Only print playground IDs`,
)

return cmd
}

func runListPlaygrounds(ctx context.Context, cli labcli.CLI, opts *listOptions) error {
printer := newListPrinter(cli.OutputStream(), opts.quiet)
defer printer.flush()

printer.printHeader()

plays, err := cli.Client().ListPlays(ctx)
if err != nil {
return fmt.Errorf("couldn't list playgrounds: %w", err)
}

for _, play := range plays {
if opts.all || play.Active {
printer.printOne(play)
}
}

return nil
}

type listPrinter struct {
quiet bool
header []string
writer *tabwriter.Writer
}

func newListPrinter(outStream io.Writer, quiet bool) *listPrinter {
header := []string{
"PLAYGROUND ID",
"NAME",
"CREATED",
"STATUS",
"LINK",
}

return &listPrinter{
quiet: quiet,
header: header,
writer: tabwriter.NewWriter(outStream, 0, 4, 2, ' ', 0),
}
}

func (p *listPrinter) printHeader() {
if !p.quiet {
fmt.Fprintln(p.writer, strings.Join(p.header, "\t"))
}
}

func (p *listPrinter) printOne(play *api.Play) {
if p.quiet {
fmt.Fprintln(p.writer, play.ID)
return
}

var link string
if play.Active {
link = play.PageURL
}

fields := []string{
play.ID,
play.Playground.Name,
humanize.Time(safeParseTime(play.CreatedAt)),
playStatus(play),
link,
}

fmt.Fprintln(p.writer, strings.Join(fields, "\t"))
}

func (p *listPrinter) flush() {
p.writer.Flush()
}

func playStatus(play *api.Play) string {
if play.Running {
return fmt.Sprintf("running (expires in %s)",
humanize.Time(time.Now().Add(time.Duration(play.ExpiresIn)*time.Millisecond)))
}
if play.Destroyed {
return fmt.Sprintf("terminated %s",
humanize.Time(safeParseTime(play.LastStateAt)))
}
if play.Failed {
return fmt.Sprintf("failed %s",
humanize.Time(safeParseTime(play.LastStateAt)))
}

return "unknown"
}

func safeParseTime(s string) time.Time {
t, _ := time.Parse(time.RFC3339, s)
return t
}
23 changes: 23 additions & 0 deletions cmd/playground/playground.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package playground

import (
"github.com/spf13/cobra"

"github.com/iximiuz/labctl/internal/labcli"
)

func NewCommand(cli labcli.CLI) *cobra.Command {
cmd := &cobra.Command{
Use: "playground <list|start|stop> [playground-name]",
Aliases: []string{"p", "playgrounds"},
Short: "List, start and stop playgrounds",
}

cmd.AddCommand(
newListCommand(cli),
newStartCommand(cli),
newStopCommand(cli),
)

return cmd
}
73 changes: 73 additions & 0 deletions cmd/playground/start.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package playground

import (
"context"
"fmt"

"github.com/skratchdot/open-golang/open"
"github.com/spf13/cobra"

"github.com/iximiuz/labctl/internal/api"
"github.com/iximiuz/labctl/internal/labcli"
)

type startOptions struct {
playground string

open bool
quiet bool
}

func newStartCommand(cli labcli.CLI) *cobra.Command {
var opts startOptions

cmd := &cobra.Command{
Use: "start [flags] <playground-name>",
Short: `Start a new playground, possibly open it in a browser`,
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
cli.SetQuiet(opts.quiet)

opts.playground = args[0]

return labcli.WrapStatusError(runStartPlayground(cmd.Context(), cli, &opts))
},
}

flags := cmd.Flags()

flags.BoolVar(
&opts.open,
"open",
false,
`Open the playground page in a browser`,
)
flags.BoolVarP(
&opts.quiet,
"quiet",
"q",
false,
`Only print playground's ID`,
)

return cmd
}

func runStartPlayground(ctx context.Context, cli labcli.CLI, opts *startOptions) error {
play, err := cli.Client().CreatePlay(ctx, api.CreatePlayRequest{
Playground: opts.playground,
})
if err != nil {
return fmt.Errorf("couldn't create a new playground: %w", err)
}

cli.PrintAux("Opening %s in your browser...\n", play.PageURL)

if err := open.Run(play.PageURL); err != nil {
cli.PrintAux("Couldn't open the browser. Copy the above URL into a browser manually to access the playground.\n")
}

cli.PrintOut("%s\n", play.ID)

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

import (
"context"
"fmt"
"time"

"github.com/briandowns/spinner"
"github.com/spf13/cobra"

"github.com/iximiuz/labctl/internal/labcli"
)

const stopCommandTimeout = 5 * time.Minute

type stopOptions struct {
playID string

quiet bool
}

func newStopCommand(cli labcli.CLI) *cobra.Command {
var opts stopOptions

cmd := &cobra.Command{
Use: "stop [flags] <playground-id>",
Short: `Start a new playground, possibly open it in a browser`,
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
cli.SetQuiet(opts.quiet)

opts.playID = args[0]

return labcli.WrapStatusError(runStopPlayground(cmd.Context(), cli, &opts))
},
}

flags := cmd.Flags()

flags.BoolVarP(
&opts.quiet,
"quiet",
"q",
false,
`Do not print any diagnostic messages`,
)

return cmd
}

func runStopPlayground(ctx context.Context, cli labcli.CLI, opts *stopOptions) error {
cli.PrintAux("Stopping playground %s...\n", opts.playID)

if err := cli.Client().DeletePlay(ctx, opts.playID); err != nil {
return fmt.Errorf("couldn't delete the playground: %w", err)
}

s := spinner.New(spinner.CharSets[38], 300*time.Millisecond)
s.Writer = cli.AuxStream()
s.Prefix = "Waiting for playground to terminate... "
s.Start()

ctx, cancel := context.WithTimeout(ctx, stopCommandTimeout)
defer cancel()

for ctx.Err() == nil {
if play, err := cli.Client().GetPlay(ctx, opts.playID); err == nil && !play.Active {
s.FinalMSG = "Waiting for playground to terminate... Done.\n"
s.Stop()

return nil
}

time.Sleep(2 * time.Second)
}

cli.PrintAux("Playground has been stopped.\n")

return nil
}
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ go 1.22.0
require (
github.com/briandowns/spinner v1.23.0
github.com/docker/cli v25.0.3+incompatible
github.com/dustin/go-humanize v1.0.1
github.com/google/uuid v1.6.0
github.com/iximiuz/wsmux v0.0.2
github.com/mikesmitty/edkey v0.0.0-20170222072505-3356ea4e686a
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/docker/cli v25.0.3+incompatible h1:KLeNs7zws74oFuVhgZQ5ONGZiXUUdgsdy6/EsX/6284=
github.com/docker/cli v25.0.3+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
Expand Down
2 changes: 1 addition & 1 deletion internal/api/account.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ type Me struct {
GithubProfileId string `json:"githubProfileId" yaml:"githubProfileId"`
}

func (c *Client) GetMe(ctx context.Context) (*Me, error) {
func (c *Client) GetAccount(ctx context.Context) (*Me, error) {
var me Me
return &me, c.GetInto(ctx, "/account", nil, nil, &me)
}
Loading

0 comments on commit f489183

Please sign in to comment.