diff --git a/api/api.go b/api/api.go index c86de79f7..a8f6ea724 100644 --- a/api/api.go +++ b/api/api.go @@ -1,3 +1,4 @@ +// TODO(adonovan): rename to package codespaces, and codespaces.Client. package api // For descriptions of service interfaces, see: @@ -15,7 +16,6 @@ import ( "fmt" "io/ioutil" "net/http" - "sort" "strconv" "strings" ) @@ -110,14 +110,6 @@ func (a *API) GetRepository(ctx context.Context, nwo string) (*Repository, error return &response, nil } -type Codespaces []*Codespace - -func (c Codespaces) SortByCreatedAt() { - sort.Slice(c, func(i, j int) bool { - return c[i].CreatedAt > c[j].CreatedAt - }) -} - type Codespace struct { Name string `json:"name"` GUID string `json:"guid"` @@ -145,7 +137,7 @@ type CodespaceEnvironmentConnection struct { RelaySAS string `json:"relaySas"` } -func (a *API) ListCodespaces(ctx context.Context, user *User) (Codespaces, error) { +func (a *API) ListCodespaces(ctx context.Context, user *User) ([]*Codespace, error) { req, err := http.NewRequest( http.MethodGet, githubAPI+"/vscs_internal/user/"+user.Login+"/codespaces", nil, ) @@ -169,9 +161,9 @@ func (a *API) ListCodespaces(ctx context.Context, user *User) (Codespaces, error return nil, jsonErrorResponse(b) } - response := struct { - Codespaces Codespaces `json:"codespaces"` - }{} + var response struct { + Codespaces []*Codespace `json:"codespaces"` + } if err := json.Unmarshal(b, &response); err != nil { return nil, fmt.Errorf("error unmarshaling response: %v", err) } @@ -325,14 +317,12 @@ func (a *API) GetCodespaceRegionLocation(ctx context.Context) (string, error) { return response.Current, nil } -type Skus []*Sku - -type Sku struct { +type SKU struct { Name string `json:"name"` DisplayName string `json:"display_name"` } -func (a *API) GetCodespacesSkus(ctx context.Context, user *User, repository *Repository, location string) (Skus, error) { +func (a *API) GetCodespacesSKUs(ctx context.Context, user *User, repository *Repository, location string) ([]*SKU, error) { req, err := http.NewRequest(http.MethodGet, githubAPI+"/vscs_internal/user/"+user.Login+"/skus", nil) if err != nil { return nil, fmt.Errorf("err creating request: %v", err) @@ -359,14 +349,14 @@ func (a *API) GetCodespacesSkus(ctx context.Context, user *User, repository *Rep return nil, jsonErrorResponse(b) } - response := struct { - Skus Skus `json:"skus"` - }{} + var response struct { + SKUs []*SKU `json:"skus"` + } if err := json.Unmarshal(b, &response); err != nil { return nil, fmt.Errorf("error unmarshaling response: %v", err) } - return response.Skus, nil + return response.SKUs, nil } type createCodespaceRequest struct { diff --git a/cmd/ghcs/code.go b/cmd/ghcs/code.go index fd71da74f..19d76fadb 100644 --- a/cmd/ghcs/code.go +++ b/cmd/ghcs/code.go @@ -12,7 +12,7 @@ import ( "github.com/spf13/cobra" ) -func NewCodeCmd() *cobra.Command { +func newCodeCmd() *cobra.Command { useInsiders := false codeCmd := &cobra.Command{ @@ -24,7 +24,7 @@ func NewCodeCmd() *cobra.Command { if len(args) > 0 { codespaceName = args[0] } - return Code(codespaceName, useInsiders) + return code(codespaceName, useInsiders) }, } @@ -34,10 +34,10 @@ func NewCodeCmd() *cobra.Command { } func init() { - rootCmd.AddCommand(NewCodeCmd()) + rootCmd.AddCommand(newCodeCmd()) } -func Code(codespaceName string, useInsiders bool) error { +func code(codespaceName string, useInsiders bool) error { apiClient := api.New(os.Getenv("GITHUB_TOKEN")) ctx := context.Background() diff --git a/cmd/ghcs/create.go b/cmd/ghcs/create.go index 7b33322be..221f03079 100644 --- a/cmd/ghcs/create.go +++ b/cmd/ghcs/create.go @@ -22,7 +22,7 @@ func newCreateCmd() *cobra.Command { Short: "Create a Codespace", Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { - return Create() + return create() }, } @@ -37,7 +37,7 @@ func init() { rootCmd.AddCommand(newCreateCmd()) } -func Create() error { +func create() error { ctx := context.Background() apiClient := api.New(os.Getenv("GITHUB_TOKEN")) locationCh := getLocation(ctx, apiClient) @@ -151,9 +151,9 @@ func getBranchName() (string, error) { } func getMachineName(ctx context.Context, user *api.User, repo *api.Repository, location string, apiClient *api.API) (string, error) { - skus, err := apiClient.GetCodespacesSkus(ctx, user, repo, location) + skus, err := apiClient.GetCodespacesSKUs(ctx, user, repo, location) if err != nil { - return "", fmt.Errorf("error getting codespace skus: %v", err) + return "", fmt.Errorf("error getting codespace SKUs: %v", err) } // if user supplied a machine type, it must be valid @@ -165,18 +165,18 @@ func getMachineName(ctx context.Context, user *api.User, repo *api.Repository, l } } - availableSkus := make([]string, len(skus)) + availableSKUs := make([]string, len(skus)) for i := 0; i < len(skus); i++ { - availableSkus[i] = skus[i].Name + availableSKUs[i] = skus[i].Name } - return "", fmt.Errorf("there are is no such machine for the repository: %s\nAvailable machines: %v", machine, availableSkus) + return "", fmt.Errorf("there is no such machine for the repository: %s\nAvailable machines: %v", machine, availableSKUs) } else if len(skus) == 0 { return "", nil } skuNames := make([]string, 0, len(skus)) - skuByName := make(map[string]*api.Sku) + skuByName := make(map[string]*api.SKU) for _, sku := range skus { nameParts := camelcase.Split(sku.Name) machineName := strings.Title(strings.ToLower(nameParts[0])) diff --git a/cmd/ghcs/delete.go b/cmd/ghcs/delete.go index c357171d1..d37029753 100644 --- a/cmd/ghcs/delete.go +++ b/cmd/ghcs/delete.go @@ -12,7 +12,7 @@ import ( "github.com/spf13/cobra" ) -func NewDeleteCmd() *cobra.Command { +func newDeleteCmd() *cobra.Command { deleteCmd := &cobra.Command{ Use: "delete []", Short: "Delete a Codespace", @@ -22,7 +22,7 @@ func NewDeleteCmd() *cobra.Command { if len(args) > 0 { codespaceName = args[0] } - return Delete(codespaceName) + return delete_(codespaceName) }, } @@ -31,7 +31,7 @@ func NewDeleteCmd() *cobra.Command { Short: "Delete all Codespaces for the current user", Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { - return DeleteAll() + return deleteAll() }, } @@ -40,7 +40,7 @@ func NewDeleteCmd() *cobra.Command { Short: "Delete all Codespaces for a repository", Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - return DeleteByRepo(args[0]) + return deleteByRepo(args[0]) }, } @@ -50,10 +50,10 @@ func NewDeleteCmd() *cobra.Command { } func init() { - rootCmd.AddCommand(NewDeleteCmd()) + rootCmd.AddCommand(newDeleteCmd()) } -func Delete(codespaceName string) error { +func delete_(codespaceName string) error { apiClient := api.New(os.Getenv("GITHUB_TOKEN")) ctx := context.Background() log := output.NewLogger(os.Stdout, os.Stderr, false) @@ -74,10 +74,10 @@ func Delete(codespaceName string) error { log.Println("Codespace deleted.") - return List(&ListOptions{}) + return list(&listOptions{}) } -func DeleteAll() error { +func deleteAll() error { apiClient := api.New(os.Getenv("GITHUB_TOKEN")) ctx := context.Background() log := output.NewLogger(os.Stdout, os.Stderr, false) @@ -105,10 +105,10 @@ func DeleteAll() error { log.Printf("Codespace deleted: %s\n", c.Name) } - return List(&ListOptions{}) + return list(&listOptions{}) } -func DeleteByRepo(repo string) error { +func deleteByRepo(repo string) error { apiClient := api.New(os.Getenv("GITHUB_TOKEN")) ctx := context.Background() log := output.NewLogger(os.Stdout, os.Stderr, false) @@ -146,5 +146,5 @@ func DeleteByRepo(repo string) error { return fmt.Errorf("No codespace was found for repository: %s", repo) } - return List(&ListOptions{}) + return list(&listOptions{}) } diff --git a/cmd/ghcs/list.go b/cmd/ghcs/list.go index 27b11d4fd..a19439296 100644 --- a/cmd/ghcs/list.go +++ b/cmd/ghcs/list.go @@ -10,32 +10,32 @@ import ( "github.com/spf13/cobra" ) -type ListOptions struct { - AsJSON bool +type listOptions struct { + asJSON bool } -func NewListCmd() *cobra.Command { - opts := &ListOptions{} +func newListCmd() *cobra.Command { + opts := &listOptions{} listCmd := &cobra.Command{ Use: "list", Short: "List your Codespaces", Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { - return List(opts) + return list(opts) }, } - listCmd.Flags().BoolVar(&opts.AsJSON, "json", false, "Output as JSON") + listCmd.Flags().BoolVar(&opts.asJSON, "json", false, "Output as JSON") return listCmd } func init() { - rootCmd.AddCommand(NewListCmd()) + rootCmd.AddCommand(newListCmd()) } -func List(opts *ListOptions) error { +func list(opts *listOptions) error { apiClient := api.New(os.Getenv("GITHUB_TOKEN")) ctx := context.Background() @@ -49,7 +49,7 @@ func List(opts *ListOptions) error { return fmt.Errorf("error getting codespaces: %v", err) } - table := output.NewTable(os.Stdout, opts.AsJSON) + table := output.NewTable(os.Stdout, opts.asJSON) table.SetHeader([]string{"Name", "Repository", "Branch", "State", "Created At"}) for _, codespace := range codespaces { table.Append([]string{ diff --git a/cmd/ghcs/logs.go b/cmd/ghcs/logs.go index dd8664597..006f9c477 100644 --- a/cmd/ghcs/logs.go +++ b/cmd/ghcs/logs.go @@ -12,7 +12,7 @@ import ( "github.com/spf13/cobra" ) -func NewLogsCmd() *cobra.Command { +func newLogsCmd() *cobra.Command { var tail bool logsCmd := &cobra.Command{ @@ -24,7 +24,7 @@ func NewLogsCmd() *cobra.Command { if len(args) > 0 { codespaceName = args[0] } - return Logs(tail, codespaceName) + return logs(tail, codespaceName) }, } @@ -34,10 +34,10 @@ func NewLogsCmd() *cobra.Command { } func init() { - rootCmd.AddCommand(NewLogsCmd()) + rootCmd.AddCommand(newLogsCmd()) } -func Logs(tail bool, codespaceName string) error { +func logs(tail bool, codespaceName string) error { apiClient := api.New(os.Getenv("GITHUB_TOKEN")) ctx := context.Background() log := output.NewLogger(os.Stdout, os.Stderr, false) diff --git a/cmd/ghcs/main.go b/cmd/ghcs/main.go index 58037437a..bc9bc2c6b 100644 --- a/cmd/ghcs/main.go +++ b/cmd/ghcs/main.go @@ -16,7 +16,7 @@ func main() { } } -var Version = "DEV" +var version = "DEV" var rootCmd = &cobra.Command{ Use: "ghcs", @@ -24,7 +24,7 @@ var rootCmd = &cobra.Command{ Running commands requires the GITHUB_TOKEN environment variable to be set to a token to access the GitHub API with.`, - Version: Version, + Version: version, PersistentPreRunE: func(cmd *cobra.Command, args []string) error { if os.Getenv("GITHUB_TOKEN") == "" { @@ -42,5 +42,5 @@ func explainError(w io.Writer, err error) { fmt.Fprintln(w, "Make sure to enable SSO for your organizations after creating the token.") return } - // fmt.Fprintf(w, "%v\n", err) + fmt.Fprintf(w, "%v\n", err) } diff --git a/cmd/ghcs/ports.go b/cmd/ghcs/ports.go index ea501b73e..918b27948 100644 --- a/cmd/ghcs/ports.go +++ b/cmd/ghcs/ports.go @@ -19,19 +19,19 @@ import ( "golang.org/x/sync/errgroup" ) -// PortOptions represents the options accepted by the ports command. -type PortsOptions struct { +// portOptions represents the options accepted by the ports command. +type portsOptions struct { // CodespaceName is the name of the codespace, optional. - CodespaceName string + codespaceName string // AsJSON dictates whether the command returns a json output or not, optional. - AsJSON bool + asJSON bool } -// NewPortsCmd returns a Cobra "ports" command that displays a table of available ports, +// newPortsCmd returns a Cobra "ports" command that displays a table of available ports, // according to the specified flags. -func NewPortsCmd() *cobra.Command { - opts := &PortsOptions{} +func newPortsCmd() *cobra.Command { + opts := &portsOptions{} portsCmd := &cobra.Command{ Use: "ports", @@ -42,31 +42,31 @@ func NewPortsCmd() *cobra.Command { }, } - portsCmd.Flags().StringVarP(&opts.CodespaceName, "codespace", "c", "", "The `name` of the Codespace to use") - portsCmd.Flags().BoolVar(&opts.AsJSON, "json", false, "Output as JSON") + portsCmd.Flags().StringVarP(&opts.codespaceName, "codespace", "c", "", "The `name` of the Codespace to use") + portsCmd.Flags().BoolVar(&opts.asJSON, "json", false, "Output as JSON") - portsCmd.AddCommand(NewPortsPublicCmd()) - portsCmd.AddCommand(NewPortsPrivateCmd()) - portsCmd.AddCommand(NewPortsForwardCmd()) + portsCmd.AddCommand(newPortsPublicCmd()) + portsCmd.AddCommand(newPortsPrivateCmd()) + portsCmd.AddCommand(newPortsForwardCmd()) return portsCmd } func init() { - rootCmd.AddCommand(NewPortsCmd()) + rootCmd.AddCommand(newPortsCmd()) } -func ports(opts *PortsOptions) error { +func ports(opts *portsOptions) error { apiClient := api.New(os.Getenv("GITHUB_TOKEN")) ctx := context.Background() - log := output.NewLogger(os.Stdout, os.Stderr, opts.AsJSON) + log := output.NewLogger(os.Stdout, os.Stderr, opts.asJSON) user, err := apiClient.GetUser(ctx) if err != nil { return fmt.Errorf("error getting user: %v", err) } - codespace, token, err := codespaces.GetOrChooseCodespace(ctx, apiClient, user, opts.CodespaceName) + codespace, token, err := codespaces.GetOrChooseCodespace(ctx, apiClient, user, opts.codespaceName) if err != nil { if err == codespaces.ErrNoCodespaces { return err @@ -88,17 +88,18 @@ func ports(opts *PortsOptions) error { } devContainerResult := <-devContainerCh - if devContainerResult.Err != nil { - _, _ = log.Errorf("Failed to get port names: %v\n", devContainerResult.Err.Error()) + if devContainerResult.err != nil { + // Warn about failure to read the devcontainer file. Not a ghcs command error. + _, _ = log.Errorf("Failed to get port names: %v\n", devContainerResult.err.Error()) } - table := output.NewTable(os.Stdout, opts.AsJSON) + table := output.NewTable(os.Stdout, opts.asJSON) table.SetHeader([]string{"Label", "Port", "Public", "Browse URL"}) for _, port := range ports { sourcePort := strconv.Itoa(port.SourcePort) var portName string - if devContainerResult.DevContainer != nil { - if attributes, ok := devContainerResult.DevContainer.PortAttributes[sourcePort]; ok { + if devContainerResult.devContainer != nil { + if attributes, ok := devContainerResult.devContainer.PortAttributes[sourcePort]; ok { portName = attributes.Label } } @@ -130,8 +131,8 @@ func getPorts(ctx context.Context, lsclient *liveshare.Client) (liveshare.Ports, } type devContainerResult struct { - DevContainer *devContainer - Err error + devContainer *devContainer + err error } type devContainer struct { @@ -173,9 +174,8 @@ func getDevContainer(ctx context.Context, apiClient *api.API, codespace *api.Cod return ch } -// NewPortsPublicCmd returns a Cobra "ports public" subcommand, which makes a given port public. -// to make a given port public. -func NewPortsPublicCmd() *cobra.Command { +// newPortsPublicCmd returns a Cobra "ports public" subcommand, which makes a given port public. +func newPortsPublicCmd() *cobra.Command { return &cobra.Command{ Use: "public ", Short: "Mark port as public", @@ -187,8 +187,8 @@ func NewPortsPublicCmd() *cobra.Command { } } -// NewPortsPrivateCmd returns a Cobra "ports private" subcommand, which makes a given port private. -func NewPortsPrivateCmd() *cobra.Command { +// newPortsPrivateCmd returns a Cobra "ports private" subcommand, which makes a given port private. +func newPortsPrivateCmd() *cobra.Command { return &cobra.Command{ Use: "private ", Short: "Mark port as private", @@ -249,7 +249,7 @@ func updatePortVisibility(log *output.Logger, codespaceName, sourcePort string, // NewPortsForwardCmd returns a Cobra "ports forward" subcommand, which forwards a set of // port pairs from the codespace to localhost. -func NewPortsForwardCmd() *cobra.Command { +func newPortsForwardCmd() *cobra.Command { return &cobra.Command{ Use: "forward :", Short: "Forward ports", @@ -299,14 +299,14 @@ func forwardPorts(log *output.Logger, codespaceName string, ports []string) erro for _, portPair := range portPairs { pp := portPair - srcstr := strconv.Itoa(portPair.Src) - if err := server.StartSharing(gctx, "share-"+srcstr, pp.Src); err != nil { + srcstr := strconv.Itoa(portPair.src) + if err := server.StartSharing(gctx, "share-"+srcstr, pp.src); err != nil { return fmt.Errorf("start sharing port: %v", err) } g.Go(func() error { - log.Println("Forwarding port: " + srcstr + " ==> " + strconv.Itoa(pp.Dst)) - portForwarder := liveshare.NewPortForwarder(lsclient, server, pp.Dst) + log.Println("Forwarding port: " + srcstr + " ==> " + strconv.Itoa(pp.dst)) + portForwarder := liveshare.NewPortForwarder(lsclient, server, pp.dst) if err := portForwarder.Start(gctx); err != nil { return fmt.Errorf("error forwarding port: %v", err) } @@ -323,16 +323,17 @@ func forwardPorts(log *output.Logger, codespaceName string, ports []string) erro } type portPair struct { - Src, Dst int + src, dst int } +// getPortPairs parses a list of strings of form "%d:%d" into pairs of numbers. func getPortPairs(ports []string) ([]portPair, error) { pp := make([]portPair, 0, len(ports)) for _, portString := range ports { parts := strings.Split(portString, ":") if len(parts) < 2 { - return pp, fmt.Errorf("port pair: '%v' is not valid", portString) + return nil, fmt.Errorf("port pair: '%v' is not valid", portString) } srcp, err := strconv.Atoi(parts[0]) diff --git a/cmd/ghcs/ssh.go b/cmd/ghcs/ssh.go index 1754f968a..a02dd557e 100644 --- a/cmd/ghcs/ssh.go +++ b/cmd/ghcs/ssh.go @@ -15,7 +15,7 @@ import ( "github.com/spf13/cobra" ) -func NewSSHCmd() *cobra.Command { +func newSSHCmd() *cobra.Command { var sshProfile, codespaceName string var sshServerPort int @@ -24,7 +24,7 @@ func NewSSHCmd() *cobra.Command { Short: "SSH into a Codespace", Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { - return SSH(sshProfile, codespaceName, sshServerPort) + return ssh(sshProfile, codespaceName, sshServerPort) }, } @@ -36,10 +36,10 @@ func NewSSHCmd() *cobra.Command { } func init() { - rootCmd.AddCommand(NewSSHCmd()) + rootCmd.AddCommand(newSSHCmd()) } -func SSH(sshProfile, codespaceName string, sshServerPort int) error { +func ssh(sshProfile, codespaceName string, sshServerPort int) error { apiClient := api.New(os.Getenv("GITHUB_TOKEN")) ctx := context.Background() log := output.NewLogger(os.Stdout, os.Stderr, false) diff --git a/internal/codespaces/codespaces.go b/internal/codespaces/codespaces.go index 48369cfa0..a346c06d7 100644 --- a/internal/codespaces/codespaces.go +++ b/internal/codespaces/codespaces.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "sort" "time" "github.com/AlecAivazis/survey/v2" @@ -25,7 +26,9 @@ func ChooseCodespace(ctx context.Context, apiClient *api.API, user *api.User) (* return nil, ErrNoCodespaces } - codespaces.SortByCreatedAt() + sort.Slice(codespaces, func(i, j int) bool { + return codespaces[i].CreatedAt > codespaces[j].CreatedAt + }) codespacesByName := make(map[string]*api.Codespace) codespacesNames := make([]string, 0, len(codespaces))