Skip to content

Commit

Permalink
Improve friendliness of the languages table (#1160)
Browse files Browse the repository at this point in the history
Co-authored-by: Andrew Starr-Bochicchio <[email protected]>
  • Loading branch information
joshuaauerbachwatson and andrewsomething authored May 10, 2022
1 parent d0b5895 commit 6124eb7
Show file tree
Hide file tree
Showing 5 changed files with 155 additions and 14 deletions.
8 changes: 5 additions & 3 deletions commands/sandbox-extra.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,11 @@ func SandboxExtras(cmd *Command) {

create := CmdBuilder(cmd, RunSandboxExtraCreate, "init <path>", "Initialize a local file system directory for the sandbox",
`The `+"`"+`doctl sandbox init`+"`"+` command specifies a directory in your file system which will hold functions and
supporting artifacts while you're developing them. When ready, you can upload these to the cloud for
testing. Later, after the area is committed to a `+"`"+`git`+"`"+` repository, you can create an app from them.
Type `+"`"+`doctl sandbox status --languages`+"`"+` for a list of supported languages.`,
supporting artifacts while you're developing them. When ready, you can upload these to the cloud for testing.
Later, after the area is committed to a `+"`"+`git`+"`"+` repository, you can create an app from them.
Type `+"`"+`doctl sandbox status --languages`+"`"+` for a list of supported languages. Use one of the displayed keywords
to choose your sample language for `+"`"+`doctl sandbox init`+"`"+`.`,
Writer)
AddStringFlag(create, "language", "l", "javascript", "Language for the initial sample code")
AddBoolFlag(create, "overwrite", "", false, "Clears and reuses an existing directory")
Expand Down
59 changes: 50 additions & 9 deletions commands/sandbox.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,10 +63,27 @@ var (
ErrSandboxNeedsUpgrade = errors.New("The sandbox support needs to be upgraded (use `doctl sandbox upgrade`)")
// ErrSandboxNotConnected is the error returned to users when the sandbox is not connected to a namespace
ErrSandboxNotConnected = errors.New("A sandbox is installed but not connected to a function namespace (use `doctl sandbox connect`)")
// ErrUndeployAllAndArgs is the error returned when the --all flag is used along with args on undeploy
// errUndeployAllAndArgs is the error returned when the --all flag is used along with args on undeploy
errUndeployAllAndArgs = errors.New("command line arguments and the `--all` flag are mutually exclusive")
// ErrUndeployTooFewArgs is the error returned when neither --all nor args are specified on undeploy
// errUndeployTooFewArgs is the error returned when neither --all nor args are specified on undeploy
errUndeployTooFewArgs = errors.New("either command line arguments or `--all` must be specified")

// languageKeywords maps the backend's runtime category names to keywords accepted as languages
// Note: this table has all languages for which we possess samples. Only those with currently
// active runtimes will display.
languageKeywords map[string][]string = map[string][]string{
"nodejs": {"javascript", "js"},
"deno": {"deno"},
"go": {"go", "golang"},
"java": {"java"},
"php": {"php"},
"python": {"python", "py"},
"ruby": {"ruby"},
"rust": {"rust"},
"swift": {"swift"},
"dotnet": {"csharp", "cs"},
"typescript": {"typescript", "ts"},
}
)

// Sandbox contains support for 'sandbox' commands provided by a hidden install of the Nimbella CLI
Expand Down Expand Up @@ -226,14 +243,38 @@ func RunSandboxStatus(c *CmdConfig) error {
return errors.New("Could not retrieve information about the connected namespace")
}
mapResult := result.Entity.(map[string]interface{})
fmt.Fprintf(c.Out, "Connected to function namespace '%s' on API host '%s'\n", mapResult["name"], mapResult["apihost"])
apiHost := mapResult["apihost"].(string)
fmt.Fprintf(c.Out, "Connected to function namespace '%s' on API host '%s'\n", mapResult["name"], apiHost)
fmt.Fprintf(c.Out, "Sandbox version is %s\n\n", minSandboxVersion)
displayRuntimes, _ := c.Doit.GetBool(c.NS, "languages")
if displayRuntimes {
result, err = SandboxExec(c, "info", "--runtimes")
if result.Error == "" && err == nil {
fmt.Fprintf(c.Out, "Available runtimes:\n")
c.PrintSandboxTextOutput(result)
languages, _ := c.Doit.GetBool(c.NS, "languages")
if languages {
return showLanguageInfo(c, apiHost)
}
return nil
}

// showLanguageInfo is called by RunSandboxStatus when --languages is specified
func showLanguageInfo(c *CmdConfig, APIHost string) error {
info, err := c.Sandbox().GetHostInfo(APIHost)
if err != nil {
return err
}
fmt.Fprintf(c.Out, "Supported Languages:\n")
for language := range info.Runtimes {
fmt.Fprintf(c.Out, "%s:\n", language)
keywords := strings.Join(languageKeywords[language], ", ")
fmt.Fprintf(c.Out, " Keywords: %s\n", keywords)
fmt.Fprintf(c.Out, " Runtime versions:\n")
runtimes := info.Runtimes[language]
for _, runtime := range runtimes {
tag := ""
if runtime.Default {
tag = fmt.Sprintf(" (%s:default)", language)
}
if runtime.Deprecated {
tag = " (deprecated)"
}
fmt.Fprintf(c.Out, " %s%s\n", runtime.Kind, tag)
}
}
return nil
Expand Down
51 changes: 51 additions & 0 deletions commands/sandbox_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,57 @@ func TestSandboxStatusWhenConnected(t *testing.T) {
})
}

func TestSandboxStatusWithLanguages(t *testing.T) {
withTestClient(t, func(config *CmdConfig, tm *tcMocks) {
buf := &bytes.Buffer{}
config.Out = buf
config.Doit.Set(config.NS, "languages", true)
fakeCmd := &exec.Cmd{
Stdout: config.Out,
}
fakeHostInfo := do.ServerlessHostInfo{
Runtimes: map[string][]do.ServerlessRuntime{
"go": {
{
Kind: "go:1.20",
Deprecated: true,
Default: false,
},
{
Kind: "go:1.21",
Deprecated: false,
Default: false,
},
{
Kind: "go:1.22",
Deprecated: false,
Default: true,
},
},
},
}
expectedDisplay := `go:
Keywords: go, golang
Runtime versions:
go:1.20 (deprecated)
go:1.21
go:1.22 (go:default)
`

tm.sandbox.EXPECT().Cmd("auth/current", []string{"--apihost", "--name"}).Return(fakeCmd, nil)
tm.sandbox.EXPECT().Exec(fakeCmd).Return(do.SandboxOutput{
Entity: map[string]interface{}{
"name": "hello",
"apihost": "https://api.example.com",
},
}, nil)
tm.sandbox.EXPECT().GetHostInfo("https://api.example.com").Return(fakeHostInfo, nil)
err := RunSandboxStatus(config)
require.NoError(t, err)
assert.Contains(t, buf.String(), expectedDisplay)
})
}

func TestSandboxStatusWhenNotConnected(t *testing.T) {
withTestClient(t, func(config *CmdConfig, tm *tcMocks) {
fakeCmd := &exec.Cmd{
Expand Down
15 changes: 15 additions & 0 deletions do/mocks/SandboxService.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

36 changes: 34 additions & 2 deletions do/sandbox.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
"context"
"encoding/json"
"errors"
"io"
"net/http"
"os"
"os/exec"
Expand Down Expand Up @@ -46,13 +47,30 @@ type namespacesResponseBody struct {
Namespace outputNamespace `json:"namespace"`
}

// SandboxService is an interface for interacting with the sandbox plugin
// and with the namespaces service.
// ServerlessRuntime is the type of a runtime entry returned by the API host controller
// of the serverless cluster.
// Only relevant fields unmarshalled
type ServerlessRuntime struct {
Default bool `json:"default"`
Deprecated bool `json:"deprecated"`
Kind string `json:"kind"`
}

// ServerlessHostInfo is the type of the host information return from the API host controller
// of the serverless cluster.
// Only relevant fields unmarshaled.
type ServerlessHostInfo struct {
Runtimes map[string][]ServerlessRuntime `json:"runtimes"`
}

// SandboxService is an interface for interacting with the sandbox plugin,
// with the namespaces service, and with the serverless cluster controller.
type SandboxService interface {
Cmd(string, []string) (*exec.Cmd, error)
Exec(*exec.Cmd) (SandboxOutput, error)
Stream(*exec.Cmd) error
GetSandboxNamespace(context.Context) (SandboxCredentials, error)
GetHostInfo(string) (ServerlessHostInfo, error)
}

type sandboxService struct {
Expand Down Expand Up @@ -150,6 +168,20 @@ func (n *sandboxService) GetSandboxNamespace(ctx context.Context) (SandboxCreden
return ans, nil
}

// GetHostInfo returns the HostInfo structure of the provided API host
func (n *sandboxService) GetHostInfo(APIHost string) (ServerlessHostInfo, error) {
endpoint := APIHost + "/api/v1"
resp, err := http.Get(endpoint)
if err != nil {
return ServerlessHostInfo{}, err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
var result ServerlessHostInfo
err = json.Unmarshal(body, &result)
return result, err
}

// Assign the correct API host based on the namespace name.
// Every serverless cluster has two domain names, one ending in '.io', the other in '.co'.
// By convention, the portal only returns the '.io' one but 'doctl sbx' must start using
Expand Down

0 comments on commit 6124eb7

Please sign in to comment.