From 8ceafff09b5d762147e1b7bbf8fbaffe4d7be3f9 Mon Sep 17 00:00:00 2001 From: Gabriel Adrian Samfira Date: Tue, 3 May 2022 12:40:59 +0000 Subject: [PATCH] Add more CLI commands --- apiserver/controllers/controllers.go | 7 +- apiserver/routers/routers.go | 2 + auth/auth.go | 2 +- cmd/run-cli/client/client.go | 210 +++++++++++++++++- cmd/run-cli/cmd/credentials.go | 63 ++++++ cmd/run-cli/cmd/init.go | 64 ++++-- cmd/run-cli/cmd/login.go | 74 ++++++- cmd/run-cli/cmd/organization.go | 1 + cmd/run-cli/cmd/provider.go | 25 ++- cmd/run-cli/cmd/repo_pool.go | 309 +++++++++++++++++++++++++++ cmd/run-cli/cmd/repository.go | 170 +++++++++++++-- cmd/run-cli/cmd/root.go | 36 +++- cmd/run-cli/common/common.go | 53 +++++ cmd/run-cli/config/config.go | 8 + cmd/run-cli/config/home_nix.go | 9 +- database/sql/sql.go | 46 ++-- go.mod | 2 + go.sum | 6 + params/params.go | 1 + params/requests.go | 2 +- runner/repositories.go | 12 +- 21 files changed, 995 insertions(+), 107 deletions(-) create mode 100644 cmd/run-cli/cmd/credentials.go create mode 100644 cmd/run-cli/cmd/repo_pool.go create mode 100644 cmd/run-cli/common/common.go diff --git a/apiserver/controllers/controllers.go b/apiserver/controllers/controllers.go index 744ce539..b5c42140 100644 --- a/apiserver/controllers/controllers.go +++ b/apiserver/controllers/controllers.go @@ -95,9 +95,7 @@ func (a *APIController) handleWorkflowJobEvent(w http.ResponseWriter, r *http.Re func (a *APIController) CatchAll(w http.ResponseWriter, r *http.Request) { headers := r.Header.Clone() - // for key, val := range headers { - // fmt.Printf("%s --> %v\n", key, val) - // } + event := runnerParams.Event(headers.Get("X-Github-Event")) switch event { case runnerParams.WorkflowJobEvent: @@ -115,6 +113,7 @@ func (a *APIController) NotFoundHandler(w http.ResponseWriter, r *http.Request) Error: "Not found", } w.WriteHeader(http.StatusNotFound) + w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(apiErr) } @@ -180,6 +179,7 @@ func (a *APIController) ListCredentials(w http.ResponseWriter, r *http.Request) return } + w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(creds) } @@ -191,6 +191,7 @@ func (a *APIController) ListProviders(w http.ResponseWriter, r *http.Request) { return } + w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(providers) } diff --git a/apiserver/routers/routers.go b/apiserver/routers/routers.go index b63310c6..516b69ed 100644 --- a/apiserver/routers/routers.go +++ b/apiserver/routers/routers.go @@ -98,7 +98,9 @@ func NewAPIRouter(han *controllers.APIController, logWriter io.Writer, authMiddl apiRouter.Handle("/organizations", log(os.Stdout, http.HandlerFunc(han.CatchAll))).Methods("POST", "OPTIONS") // Credentials and providers + apiRouter.Handle("/credentials/", log(os.Stdout, http.HandlerFunc(han.ListCredentials))).Methods("GET", "OPTIONS") apiRouter.Handle("/credentials", log(os.Stdout, http.HandlerFunc(han.ListCredentials))).Methods("GET", "OPTIONS") + apiRouter.Handle("/providers/", log(os.Stdout, http.HandlerFunc(han.ListProviders))).Methods("GET", "OPTIONS") apiRouter.Handle("/providers", log(os.Stdout, http.HandlerFunc(han.ListProviders))).Methods("GET", "OPTIONS") return router diff --git a/auth/auth.go b/auth/auth.go index 3665fd1b..e400001a 100644 --- a/auth/auth.go +++ b/auth/auth.go @@ -115,7 +115,7 @@ func (a *Authenticator) AuthenticateUser(ctx context.Context, info params.Passwo user, err := a.store.GetUser(ctx, info.Username) if err != nil { - if err == runnerErrors.ErrNotFound { + if errors.Is(err, runnerErrors.ErrNotFound) { return ctx, runnerErrors.ErrUnauthorized } return ctx, errors.Wrap(err, "authenticating") diff --git a/cmd/run-cli/client/client.go b/cmd/run-cli/client/client.go index 075b7843..4a877834 100644 --- a/cmd/run-cli/client/client.go +++ b/cmd/run-cli/client/client.go @@ -12,8 +12,14 @@ import ( "github.com/pkg/errors" ) -func NewClient(name string, cfg config.Manager) *Client { +func NewClient(name string, cfg config.Manager, debug bool) *Client { cli := resty.New() + if cfg.Token != "" { + cli = cli.SetAuthToken(cfg.Token) + } + cli = cli. + SetHeader("Accept", "application/json"). + SetDebug(debug) return &Client{ ManagerName: name, Config: cfg, @@ -45,7 +51,6 @@ func (c *Client) InitManager(url string, param params.NewUserParams) (params.Use var response params.User resp, err := c.client.R(). - SetHeader("Content-Type", "application/json"). SetBody(body). SetResult(&response). Post(url) @@ -69,17 +74,210 @@ func (c *Client) Login(url string, param params.PasswordLoginParams) (string, er var response params.JWTResponse resp, err := c.client.R(). - SetHeader("Content-Type", "application/json"). SetBody(body). SetResult(&response). Post(url) - if err != nil { + if err != nil || resp.IsError() { apiErr, decErr := c.decodeAPIError(resp.Body()) if decErr != nil { - return "", errors.Wrap(err, "sending request") + return "", errors.Wrap(decErr, "sending request") } - return "", fmt.Errorf("error running init: %s", apiErr.Details) + return "", fmt.Errorf("error performing login: %s", apiErr.Details) } return response.Token, nil } + +func (c *Client) ListCredentials() ([]params.GithubCredentials, error) { + var ghCreds []params.GithubCredentials + url := fmt.Sprintf("%s/api/v1/credentials", c.Config.BaseURL) + resp, err := c.client.R(). + SetResult(&ghCreds). + Get(url) + if err != nil || resp.IsError() { + apiErr, decErr := c.decodeAPIError(resp.Body()) + if decErr != nil { + return nil, errors.Wrap(decErr, "sending request") + } + return nil, fmt.Errorf("error fetching credentials: %s", apiErr.Details) + } + return ghCreds, nil +} + +func (c *Client) ListProviders() ([]params.Provider, error) { + var providers []params.Provider + url := fmt.Sprintf("%s/api/v1/providers", c.Config.BaseURL) + resp, err := c.client.R(). + SetResult(&providers). + Get(url) + if err != nil || resp.IsError() { + apiErr, decErr := c.decodeAPIError(resp.Body()) + if decErr != nil { + return nil, errors.Wrap(decErr, "sending request") + } + return nil, fmt.Errorf("error fetching providers: %s", apiErr.Details) + } + return providers, nil +} + +func (c *Client) ListRepositories() ([]params.Repository, error) { + var repos []params.Repository + url := fmt.Sprintf("%s/api/v1/repositories", c.Config.BaseURL) + resp, err := c.client.R(). + SetResult(&repos). + Get(url) + if err != nil || resp.IsError() { + apiErr, decErr := c.decodeAPIError(resp.Body()) + if decErr != nil { + return nil, errors.Wrap(decErr, "sending request") + } + return nil, fmt.Errorf("error fetching repos: %s", apiErr.Details) + } + return repos, nil +} + +func (c *Client) CreateRepository(param params.CreateRepoParams) (params.Repository, error) { + var response params.Repository + url := fmt.Sprintf("%s/api/v1/repositories", c.Config.BaseURL) + + body, err := json.Marshal(param) + if err != nil { + return params.Repository{}, err + } + resp, err := c.client.R(). + SetBody(body). + SetResult(&response). + Post(url) + if err != nil || resp.IsError() { + apiErr, decErr := c.decodeAPIError(resp.Body()) + if decErr != nil { + return response, errors.Wrap(decErr, "sending request") + } + return response, fmt.Errorf("error performing login: %s", apiErr.Details) + } + return response, nil +} + +func (c *Client) GetRepository(repoID string) (params.Repository, error) { + var response params.Repository + url := fmt.Sprintf("%s/api/v1/repositories/%s", c.Config.BaseURL, repoID) + resp, err := c.client.R(). + SetResult(&response). + Get(url) + if err != nil || resp.IsError() { + apiErr, decErr := c.decodeAPIError(resp.Body()) + if decErr != nil { + return response, errors.Wrap(decErr, "sending request") + } + return response, fmt.Errorf("error fetching repos: %s", apiErr.Details) + } + return response, nil +} + +func (c *Client) DeleteRepository(repoID string) error { + url := fmt.Sprintf("%s/api/v1/repositories/%s", c.Config.BaseURL, repoID) + resp, err := c.client.R(). + Delete(url) + if err != nil || resp.IsError() { + apiErr, decErr := c.decodeAPIError(resp.Body()) + if decErr != nil { + return errors.Wrap(decErr, "sending request") + } + return fmt.Errorf("error fetching repos: %s", apiErr.Details) + } + return nil +} + +func (c *Client) CreateRepoPool(repoID string, param params.CreatePoolParams) (params.Pool, error) { + url := fmt.Sprintf("%s/api/v1/repositories/%s/pools", c.Config.BaseURL, repoID) + + var response params.Pool + body, err := json.Marshal(param) + if err != nil { + return response, err + } + resp, err := c.client.R(). + SetBody(body). + SetResult(&response). + Post(url) + if err != nil || resp.IsError() { + apiErr, decErr := c.decodeAPIError(resp.Body()) + if decErr != nil { + return response, errors.Wrap(decErr, "sending request") + } + return response, fmt.Errorf("error performing login: %s", apiErr.Details) + } + return response, nil +} + +func (c *Client) ListRepoPools(repoID string) ([]params.Pool, error) { + url := fmt.Sprintf("%s/api/v1/repositories/%s/pools", c.Config.BaseURL, repoID) + + var response []params.Pool + resp, err := c.client.R(). + SetResult(&response). + Get(url) + if err != nil || resp.IsError() { + apiErr, decErr := c.decodeAPIError(resp.Body()) + if decErr != nil { + return response, errors.Wrap(decErr, "sending request") + } + return response, fmt.Errorf("error performing login: %s", apiErr.Details) + } + return response, nil +} + +func (c *Client) GetRepoPool(repoID, poolID string) (params.Pool, error) { + url := fmt.Sprintf("%s/api/v1/repositories/%s/pools/%s", c.Config.BaseURL, repoID, poolID) + + var response params.Pool + resp, err := c.client.R(). + SetResult(&response). + Get(url) + if err != nil || resp.IsError() { + apiErr, decErr := c.decodeAPIError(resp.Body()) + if decErr != nil { + return response, errors.Wrap(decErr, "sending request") + } + return response, fmt.Errorf("error performing login: %s", apiErr.Details) + } + return response, nil +} + +func (c *Client) DeleteRepoPool(repoID, poolID string) error { + url := fmt.Sprintf("%s/api/v1/repositories/%s/pools/%s", c.Config.BaseURL, repoID, poolID) + + resp, err := c.client.R(). + Delete(url) + + if err != nil || resp.IsError() { + apiErr, decErr := c.decodeAPIError(resp.Body()) + if decErr != nil { + return errors.Wrap(decErr, "sending request") + } + return fmt.Errorf("error performing login: %s", apiErr.Details) + } + return nil +} + +func (c *Client) UpdateRepoPool(repoID, poolID string, param params.UpdatePoolParams) (params.Pool, error) { + url := fmt.Sprintf("%s/api/v1/repositories/%s/pools/%s", c.Config.BaseURL, repoID, poolID) + + var response params.Pool + body, err := json.Marshal(param) + if err != nil { + return response, err + } + resp, err := c.client.R(). + SetBody(body). + SetResult(&response). + Put(url) + if err != nil || resp.IsError() { + apiErr, decErr := c.decodeAPIError(resp.Body()) + if decErr != nil { + return response, errors.Wrap(decErr, "sending request") + } + return response, fmt.Errorf("error performing login: %s", apiErr.Details) + } + return response, nil +} diff --git a/cmd/run-cli/cmd/credentials.go b/cmd/run-cli/cmd/credentials.go new file mode 100644 index 00000000..4f0e7fb6 --- /dev/null +++ b/cmd/run-cli/cmd/credentials.go @@ -0,0 +1,63 @@ +/* +Copyright © 2022 NAME HERE + +*/ +package cmd + +import ( + "fmt" + "runner-manager/params" + + "github.com/jedib0t/go-pretty/v6/table" + "github.com/spf13/cobra" +) + +// credentialsCmd represents the credentials command +var credentialsCmd = &cobra.Command{ + Use: "credentials", + Aliases: []string{"creds"}, + Short: "List configured credentials", + Long: `List all available credentials configured in the service +config file. + +Currently, github personal tokens are configured statically in the config file +of the runner-manager service. This command lists the names of those credentials, +which in turn can be used to define pools of runners withing repositories.`, + Run: nil, +} + +func init() { + credentialsCmd.AddCommand( + &cobra.Command{ + Use: "list", + Aliases: []string{"ls"}, + Short: "List configured github credentials", + Long: `List the names of the github personal access tokens availabe to the runner-manager.`, + SilenceUsage: true, + RunE: func(cmd *cobra.Command, args []string) error { + if needsInit { + return needsInitError + } + + creds, err := cli.ListCredentials() + if err != nil { + return err + } + formatGithubCredentials(creds) + return nil + }, + }) + + rootCmd.AddCommand(credentialsCmd) +} + +func formatGithubCredentials(creds []params.GithubCredentials) { + t := table.NewWriter() + header := table.Row{"Name", "Description"} + t.AppendHeader(header) + for _, val := range creds { + t.AppendRow(table.Row{val.Name, val.Description}) + t.AppendSeparator() + } + fmt.Println(t.Render()) +} diff --git a/cmd/run-cli/cmd/init.go b/cmd/run-cli/cmd/init.go index 957e0ebc..85605aa2 100644 --- a/cmd/run-cli/cmd/init.go +++ b/cmd/run-cli/cmd/init.go @@ -6,7 +6,9 @@ package cmd import ( "fmt" + "strings" + "runner-manager/cmd/run-cli/common" "runner-manager/cmd/run-cli/config" "runner-manager/params" @@ -33,18 +35,24 @@ run-cli login --name=dev --url=https://runner.example.com --username=admin --pas `, RunE: func(cmd *cobra.Command, args []string) error { if cfg != nil { - if cfg.HasManager(loginName) { - return fmt.Errorf("a manager with name %s already exists in your local config", loginName) + if cfg.HasManager(loginProfileName) { + return fmt.Errorf("a manager with name %s already exists in your local config", loginProfileName) } } + if err := promptUnsetInitVariables(); err != nil { + return err + } + newUser := params.NewUserParams{ Username: loginUserName, Password: loginPassword, FullName: loginFullName, Email: loginEmail, } - response, err := cli.InitManager(loginURL, newUser) + + url := strings.TrimSuffix(loginURL, "/") + response, err := cli.InitManager(url, newUser) if err != nil { return errors.Wrap(err, "initializing manager") } @@ -54,25 +62,18 @@ run-cli login --name=dev --url=https://runner.example.com --username=admin --pas Password: loginPassword, } - token, err := cli.Login(loginURL, loginParams) + token, err := cli.Login(url, loginParams) if err != nil { return errors.Wrap(err, "authenticating") } - if cfg == nil { - // we're creating a new config - cfg = &config.Config{ - Managers: []config.Manager{}, - } - } - cfg.Managers = append(cfg.Managers, config.Manager{ - Name: loginName, - BaseURL: loginURL, + Name: loginProfileName, + BaseURL: url, Token: token, }) - cfg.ActiveManager = loginName + cfg.ActiveManager = loginProfileName if err := cfg.SaveConfig(); err != nil { return errors.Wrap(err, "saving config") @@ -83,21 +84,42 @@ run-cli login --name=dev --url=https://runner.example.com --username=admin --pas }, } +func promptUnsetInitVariables() error { + var err error + if loginUserName == "" { + loginUserName, err = common.PromptString("Username") + if err != nil { + return err + } + } + + if loginEmail == "" { + loginEmail, err = common.PromptString("Email") + if err != nil { + return err + } + } + + if loginPassword == "" { + loginPassword, err = common.PromptPassword("Password") + if err != nil { + return err + } + } + return nil +} + func init() { rootCmd.AddCommand(initCmd) - initCmd.Flags().StringVarP(&loginName, "name", "n", "", "A name for this runner manager") + initCmd.Flags().StringVarP(&loginProfileName, "name", "n", "", "A name for this runner manager") initCmd.Flags().StringVarP(&loginURL, "url", "a", "", "The base URL for the runner manager API") initCmd.Flags().StringVarP(&loginUserName, "username", "u", "", "The desired administrative username") - initCmd.Flags().StringVarP(&loginPassword, "password", "p", "", "The admin password") - initCmd.Flags().StringVarP(&loginFullName, "full-name", "f", "", "Full name of the user") initCmd.Flags().StringVarP(&loginEmail, "email", "e", "", "Email address") + initCmd.Flags().StringVarP(&loginFullName, "full-name", "f", "", "Full name of the user") + initCmd.Flags().StringVarP(&loginPassword, "password", "p", "", "The admin password") initCmd.MarkFlagRequired("name") initCmd.MarkFlagRequired("url") - initCmd.MarkFlagRequired("username") - initCmd.MarkFlagRequired("password") - initCmd.MarkFlagRequired("full-name") - initCmd.MarkFlagRequired("email") } func renderUserTable(user params.User) { diff --git a/cmd/run-cli/cmd/login.go b/cmd/run-cli/cmd/login.go index b29031e4..c680bceb 100644 --- a/cmd/run-cli/cmd/login.go +++ b/cmd/run-cli/cmd/login.go @@ -6,17 +6,21 @@ package cmd import ( "fmt" + "runner-manager/cmd/run-cli/common" + "runner-manager/cmd/run-cli/config" + "runner-manager/params" + "strings" "github.com/spf13/cobra" ) var ( - loginName string - loginURL string - loginPassword string - loginUserName string - loginFullName string - loginEmail string + loginProfileName string + loginURL string + loginPassword string + loginUserName string + loginFullName string + loginEmail string ) // loginCmd represents the login command @@ -27,16 +31,68 @@ var loginCmd = &cobra.Command{ Long: `Performs login for this machine on a remote runner-manager: run-cli login --name=dev --url=https://runner.example.com`, - Run: func(cmd *cobra.Command, args []string) { - fmt.Printf("--> %v %v\n", cfg, configErr) + RunE: func(cmd *cobra.Command, args []string) error { + if cfg != nil { + if cfg.HasManager(loginProfileName) { + return fmt.Errorf("a manager with name %s already exists in your local config", loginProfileName) + } + } + + if err := promptUnsetLoginVariables(); err != nil { + return err + } + + url := strings.TrimSuffix(loginURL, "/") + loginParams := params.PasswordLoginParams{ + Username: loginUserName, + Password: loginPassword, + } + + resp, err := cli.Login(url, loginParams) + if err != nil { + return err + } + + cfg.Managers = append(cfg.Managers, config.Manager{ + Name: loginProfileName, + BaseURL: url, + Token: resp, + }) + cfg.ActiveManager = loginProfileName + + if err := cfg.SaveConfig(); err != nil { + return err + } + return nil }, } +func promptUnsetLoginVariables() error { + var err error + if loginUserName == "" { + loginUserName, err = common.PromptString("Username") + if err != nil { + return err + } + } + + if loginPassword == "" { + loginPassword, err = common.PromptPassword("Password") + if err != nil { + return err + } + } + return nil +} + func init() { rootCmd.AddCommand(loginCmd) - loginCmd.Flags().StringVarP(&loginName, "name", "n", "", "A name for this runner manager") + loginCmd.Flags().StringVarP(&loginProfileName, "name", "n", "", "A name for this runner manager") loginCmd.Flags().StringVarP(&loginURL, "url", "a", "", "The base URL for the runner manager API") + loginCmd.Flags().StringVarP(&loginUserName, "username", "u", "", "Username to log in as") + loginCmd.Flags().StringVarP(&loginPassword, "password", "p", "", "The user passowrd") + loginCmd.MarkFlagRequired("name") loginCmd.MarkFlagRequired("url") } diff --git a/cmd/run-cli/cmd/organization.go b/cmd/run-cli/cmd/organization.go index 88031875..f3f6245b 100644 --- a/cmd/run-cli/cmd/organization.go +++ b/cmd/run-cli/cmd/organization.go @@ -14,6 +14,7 @@ import ( var organizationCmd = &cobra.Command{ Use: "organization", SilenceUsage: true, + Aliases: []string{"org"}, Short: "A brief description of your command", Long: `A longer description that spans multiple lines and likely contains examples and usage of using your command. For example: diff --git a/cmd/run-cli/cmd/provider.go b/cmd/run-cli/cmd/provider.go index 36e53e69..c445c42c 100644 --- a/cmd/run-cli/cmd/provider.go +++ b/cmd/run-cli/cmd/provider.go @@ -6,7 +6,9 @@ package cmd import ( "fmt" + "runner-manager/params" + "github.com/jedib0t/go-pretty/v6/table" "github.com/spf13/cobra" ) @@ -32,10 +34,29 @@ func init() { Long: `List all cloud providers configured with the service.`, SilenceUsage: true, RunE: func(cmd *cobra.Command, args []string) error { - fmt.Println("provider list called") - return fmt.Errorf("I failed :(") + if needsInit { + return needsInitError + } + + providers, err := cli.ListProviders() + if err != nil { + return err + } + formatProviders(providers) + return nil }, }) rootCmd.AddCommand(providerCmd) } + +func formatProviders(providers []params.Provider) { + t := table.NewWriter() + header := table.Row{"Name", "Description"} + t.AppendHeader(header) + for _, val := range providers { + t.AppendRow(table.Row{val.Name, val.ProviderType}) + t.AppendSeparator() + } + fmt.Println(t.Render()) +} diff --git a/cmd/run-cli/cmd/repo_pool.go b/cmd/run-cli/cmd/repo_pool.go new file mode 100644 index 00000000..7d73ff2a --- /dev/null +++ b/cmd/run-cli/cmd/repo_pool.go @@ -0,0 +1,309 @@ +/* +Copyright © 2022 NAME HERE + +*/ +package cmd + +import ( + "fmt" + "runner-manager/config" + "runner-manager/params" + "strings" + + "github.com/jedib0t/go-pretty/v6/table" + "github.com/spf13/cobra" +) + +var ( + poolProvider string + poolMaxRunners uint + poolMinIdleRunners uint + poolImage string + poolFlavor string + poolOSType string + poolOSArch string + poolTags string + poolEnabled bool +) + +// repoPoolCmd represents the pool command +var repoPoolCmd = &cobra.Command{ + Use: "pool", + SilenceUsage: true, + Aliases: []string{"pools"}, + Short: "Manage pools", + Long: `Manage pools for a repository. + +Repositories and organizations can define multiple pools with different +characteristics, which in turn will spawn github self hosted runners on +compute instances that reflect those characteristics. + +For example, one pool can define a runner with tags "GPU,ML" which will +spin up instances with access to a GPU, on the desired provider.`, + Run: nil, +} + +var poolAddCmd = &cobra.Command{ + Use: "add", + Aliases: []string{"create"}, + Short: "Add pool", + Long: `Add a new pool repository to the manager.`, + SilenceUsage: true, + RunE: func(cmd *cobra.Command, args []string) error { + if needsInit { + return needsInitError + } + + if len(args) == 0 { + return fmt.Errorf("requires a repository ID") + } + + if len(args) > 1 { + return fmt.Errorf("too many arguments") + } + + tags := strings.Split(poolTags, ",") + newPoolParams := params.CreatePoolParams{ + ProviderName: poolProvider, + MaxRunners: poolMaxRunners, + MinIdleRunners: poolMinIdleRunners, + Image: poolImage, + Flavor: poolFlavor, + OSType: config.OSType(poolOSType), + OSArch: config.OSArch(poolOSArch), + Tags: tags, + Enabled: poolEnabled, + } + if err := newPoolParams.Validate(); err != nil { + return err + } + pool, err := cli.CreateRepoPool(args[0], newPoolParams) + if err != nil { + return err + } + formatOnePool(pool) + return nil + }, +} + +var poolListCmd = &cobra.Command{ + Use: "list", + Aliases: []string{"ls"}, + Short: "List repository pools", + Long: `List all configured pools for a given repository.`, + SilenceUsage: true, + RunE: func(cmd *cobra.Command, args []string) error { + if needsInit { + return needsInitError + } + + if len(args) == 0 { + return fmt.Errorf("requires a repository ID") + } + + if len(args) > 1 { + return fmt.Errorf("too many arguments") + } + + pools, err := cli.ListRepoPools(args[0]) + if err != nil { + return err + } + formatPools(pools) + return nil + }, +} + +var poolShowCmd = &cobra.Command{ + Use: "show", + Short: "Show details for one pool", + Long: `Displays detailed information about a single pool.`, + RunE: func(cmd *cobra.Command, args []string) error { + if needsInit { + return needsInitError + } + + if len(args) < 2 || len(args) > 2 { + return fmt.Errorf("command requires repoID and poolID") + } + + pool, err := cli.GetRepoPool(args[0], args[1]) + if err != nil { + return err + } + + formatOnePool(pool) + return nil + }, +} + +var poolDeleteCmd = &cobra.Command{ + Use: "delete", + Aliases: []string{"remove", "rm", "del"}, + Short: "Removes one pool", + Long: `Delete one repository pool from the manager.`, + SilenceUsage: true, + RunE: func(cmd *cobra.Command, args []string) error { + if needsInit { + return needsInitError + } + if len(args) < 2 || len(args) > 2 { + return fmt.Errorf("command requires repoID and poolID") + } + + if err := cli.DeleteRepoPool(args[0], args[1]); err != nil { + return err + } + return nil + }, +} + +var poolUpdateCmd = &cobra.Command{ + Use: "update", + Short: "Update one pool", + Long: `Updates pool characteristics. + +This command updates the pool characteristics. Runners already created prior to updating +the pool, will not be recreated. IF they no longer suit your needs, you will need to +explicitly remove them using the runner delete command. + `, + SilenceUsage: true, + RunE: func(cmd *cobra.Command, args []string) error { + if needsInit { + return needsInitError + } + + if len(args) < 2 || len(args) > 2 { + return fmt.Errorf("command requires repoID and poolID") + } + + poolUpdateParams := params.UpdatePoolParams{} + + if cmd.Flags().Changed("image") { + poolUpdateParams.Image = poolImage + } + + if cmd.Flags().Changed("flavor") { + poolUpdateParams.Flavor = poolFlavor + } + + if cmd.Flags().Changed("tags") { + poolUpdateParams.Tags = strings.Split(poolTags, ",") + } + + if cmd.Flags().Changed("os-type") { + poolUpdateParams.OSType = config.OSType(poolOSType) + } + + if cmd.Flags().Changed("os-arch") { + poolUpdateParams.OSArch = config.OSArch(poolOSArch) + } + + if cmd.Flags().Changed("max-runners") { + poolUpdateParams.MaxRunners = &poolMaxRunners + } + + if cmd.Flags().Changed("min-idle-runners") { + poolUpdateParams.MinIdleRunners = &poolMinIdleRunners + } + + if cmd.Flags().Changed("enabled") { + poolUpdateParams.Enabled = &poolEnabled + } + + pool, err := cli.UpdateRepoPool(args[0], args[1], poolUpdateParams) + if err != nil { + return err + } + + formatOnePool(pool) + return nil + }, +} + +func init() { + poolAddCmd.Flags().StringVar(&poolProvider, "provider-name", "", "The name of the provider where runners will be created.") + poolAddCmd.Flags().StringVar(&poolImage, "image", "", "The provider-specific image name to use for runners in this pool.") + poolAddCmd.Flags().StringVar(&poolFlavor, "flavor", "", "The flavor to use for this runner.") + poolAddCmd.Flags().StringVar(&poolTags, "tags", "", "A comma separated list of tags to assign to this runner.") + poolAddCmd.Flags().StringVar(&poolOSType, "os-type", "linux", "Operating system type (windows, linux, etc).") + poolAddCmd.Flags().StringVar(&poolOSArch, "os-arch", "amd64", "Operating system architecture (amd64, arm, etc).") + poolAddCmd.Flags().UintVar(&poolMaxRunners, "max-runners", 5, "The maximum number of runner this pool will create.") + poolAddCmd.Flags().UintVar(&poolMinIdleRunners, "min-idle-runners", 1, "Attempt to maintain a minimum of idle self-hosted runners of this type.") + poolAddCmd.Flags().BoolVar(&poolEnabled, "enabled", false, "Enable this pool.") + poolAddCmd.MarkFlagRequired("provider-name") + poolAddCmd.MarkFlagRequired("image") + poolAddCmd.MarkFlagRequired("flavor") + poolAddCmd.MarkFlagRequired("tags") + + poolUpdateCmd.Flags().StringVar(&poolImage, "image", "", "The provider-specific image name to use for runners in this pool.") + poolUpdateCmd.Flags().StringVar(&poolFlavor, "flavor", "", "The flavor to use for this runner.") + poolUpdateCmd.Flags().StringVar(&poolTags, "tags", "", "A comma separated list of tags to assign to this runner.") + poolUpdateCmd.Flags().StringVar(&poolOSType, "os-type", "linux", "Operating system type (windows, linux, etc).") + poolUpdateCmd.Flags().StringVar(&poolOSArch, "os-arch", "amd64", "Operating system architecture (amd64, arm, etc).") + poolUpdateCmd.Flags().UintVar(&poolMaxRunners, "max-runners", 5, "The maximum number of runner this pool will create.") + poolUpdateCmd.Flags().UintVar(&poolMinIdleRunners, "min-idle-runners", 1, "Attempt to maintain a minimum of idle self-hosted runners of this type.") + poolUpdateCmd.Flags().BoolVar(&poolEnabled, "enabled", false, "Enable this pool.") + + repoPoolCmd.AddCommand( + poolListCmd, + poolAddCmd, + poolShowCmd, + poolDeleteCmd, + poolUpdateCmd, + ) + + repositoryCmd.AddCommand(repoPoolCmd) +} + +func formatPools(pools []params.Pool) { + t := table.NewWriter() + header := table.Row{"ID", "Image", "Flavor", "Tags", "Enabled"} + t.AppendHeader(header) + + for _, pool := range pools { + tags := []string{} + for _, tag := range pool.Tags { + tags = append(tags, tag.Name) + } + t.AppendRow(table.Row{pool.ID, pool.Image, pool.Flavor, strings.Join(tags, " "), pool.Enabled}) + t.AppendSeparator() + } + fmt.Println(t.Render()) +} + +func formatOnePool(pool params.Pool) { + t := table.NewWriter() + rowConfigAutoMerge := table.RowConfig{AutoMerge: true} + + header := table.Row{"Field", "Value"} + + tags := []string{} + for _, tag := range pool.Tags { + tags = append(tags, tag.Name) + } + + t.AppendHeader(header) + t.AppendRow(table.Row{"ID", pool.ID}) + t.AppendRow(table.Row{"Provider Name", pool.ProviderName}) + t.AppendRow(table.Row{"Image", pool.Image}) + t.AppendRow(table.Row{"Flavor", pool.Flavor}) + t.AppendRow(table.Row{"OS Type", pool.OSType}) + t.AppendRow(table.Row{"OS Architecture", pool.OSArch}) + t.AppendRow(table.Row{"Max Runners", pool.MaxRunners}) + t.AppendRow(table.Row{"Min Idle Runners", pool.MinIdleRunners}) + t.AppendRow(table.Row{"Tags", strings.Join(tags, ", ")}) + t.AppendRow(table.Row{"Enabled", pool.Enabled}) + + if len(pool.Instances) > 0 { + for _, instance := range pool.Instances { + t.AppendRow(table.Row{"Instances", fmt.Sprintf("%s (%s)", instance.Name, instance.ID)}, rowConfigAutoMerge) + } + } + + t.SetColumnConfigs([]table.ColumnConfig{ + {Number: 1, AutoMerge: true}, + {Number: 2, AutoMerge: true}, + }) + fmt.Println(t.Render()) +} diff --git a/cmd/run-cli/cmd/repository.go b/cmd/run-cli/cmd/repository.go index 7ad3d568..d6b6838e 100644 --- a/cmd/run-cli/cmd/repository.go +++ b/cmd/run-cli/cmd/repository.go @@ -6,36 +6,170 @@ package cmd import ( "fmt" + "runner-manager/params" + "github.com/jedib0t/go-pretty/v6/table" "github.com/spf13/cobra" ) +var ( + repoOwner string + repoName string + repoWebhookSecret string + repoCreds string +) + // repositoryCmd represents the repository command var repositoryCmd = &cobra.Command{ Use: "repository", + Aliases: []string{"repo"}, SilenceUsage: true, - Short: "A brief description of your command", - Long: `A longer description that spans multiple lines and likely contains examples -and usage of using your command. For example: + Short: "Manage repositories", + Long: `Add, remove or update repositories for which we manage +self hosted runners. -Cobra is a CLI library for Go that empowers applications. -This application is a tool to generate the needed files -to quickly create a Cobra application.`, - Run: func(cmd *cobra.Command, args []string) { - fmt.Println("repository called") +This command allows you to define a new repository or manage an existing +repository for which the runner-manager maintains pools of self hosted runners.`, + Run: nil, +} + +var repoAddCmd = &cobra.Command{ + Use: "add", + Aliases: []string{"create"}, + Short: "Add repository", + Long: `Add a new repository to the manager.`, + SilenceUsage: true, + RunE: func(cmd *cobra.Command, args []string) error { + if needsInit { + return needsInitError + } + + newRepoReq := params.CreateRepoParams{ + Owner: repoOwner, + Name: repoName, + WebhookSecret: repoWebhookSecret, + CredentialsName: repoCreds, + } + repo, err := cli.CreateRepository(newRepoReq) + if err != nil { + return err + } + formatOneRepository(repo) + return nil + }, +} + +var repoListCmd = &cobra.Command{ + Use: "list", + Aliases: []string{"ls"}, + Short: "List repositories", + Long: `List all configured respositories that are currently managed.`, + SilenceUsage: true, + RunE: func(cmd *cobra.Command, args []string) error { + if needsInit { + return needsInitError + } + + repos, err := cli.ListRepositories() + if err != nil { + return err + } + formatRepositories(repos) + return nil + }, +} + +var repoShowCmd = &cobra.Command{ + Use: "show", + Short: "Show details for one repository", + Long: `Displays detailed information about a single repository.`, + SilenceUsage: true, + RunE: func(cmd *cobra.Command, args []string) error { + if needsInit { + return needsInitError + } + if len(args) == 0 { + return fmt.Errorf("requires a repository ID") + } + if len(args) > 1 { + return fmt.Errorf("too many arguments") + } + repo, err := cli.GetRepository(args[0]) + if err != nil { + return err + } + formatOneRepository(repo) + return nil + }, +} + +var repoDeleteCmd = &cobra.Command{ + Use: "delete", + Aliases: []string{"remove", "rm", "del"}, + Short: "Removes one repository", + Long: `Delete one repository from the manager.`, + SilenceUsage: true, + RunE: func(cmd *cobra.Command, args []string) error { + if needsInit { + return needsInitError + } + if len(args) == 0 { + return fmt.Errorf("requires a repository ID") + } + if len(args) > 1 { + return fmt.Errorf("too many arguments") + } + if err := cli.DeleteRepository(args[0]); err != nil { + return err + } + return nil }, } func init() { + + repoAddCmd.Flags().StringVar(&repoOwner, "owner", "", "The owner of this repository") + repoAddCmd.Flags().StringVar(&repoName, "name", "", "The name of the repository") + repoAddCmd.Flags().StringVar(&repoWebhookSecret, "webhook-secret", "", "The webhook secret for this repository") + repoAddCmd.Flags().StringVar(&repoCreds, "credentials", "", "Credentials name. See credentials list.") + repoAddCmd.MarkFlagRequired("credentials") + repoAddCmd.MarkFlagRequired("owner") + repoAddCmd.MarkFlagRequired("name") + + repositoryCmd.AddCommand( + repoListCmd, + repoAddCmd, + repoShowCmd, + repoDeleteCmd, + ) + rootCmd.AddCommand(repositoryCmd) - - // Here you will define your flags and configuration settings. - - // Cobra supports Persistent Flags which will work for this command - // and all subcommands, e.g.: - // repositoryCmd.PersistentFlags().String("foo", "", "A help for foo") - - // Cobra supports local flags which will only run when this command - // is called directly, e.g.: - // repositoryCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") +} + +func formatRepositories(repos []params.Repository) { + t := table.NewWriter() + header := table.Row{"ID", "Owner", "Name", "Credentials name"} + t.AppendHeader(header) + for _, val := range repos { + t.AppendRow(table.Row{val.ID, val.Owner, val.Name, val.CredentialsName}) + t.AppendSeparator() + } + fmt.Println(t.Render()) +} + +func formatOneRepository(repo params.Repository) { + t := table.NewWriter() + header := table.Row{"Field", "Value"} + t.AppendHeader(header) + t.AppendRow(table.Row{"ID", repo.ID}) + t.AppendRow(table.Row{"Owner", repo.Owner}) + t.AppendRow(table.Row{"Name", repo.Name}) + t.AppendRow(table.Row{"Credentials", repo.CredentialsName}) + + if len(repo.Pools) > 0 { + for _, pool := range repo.Pools { + t.AppendRow(table.Row{"Pools", pool.ID}) + } + } + fmt.Println(t.Render()) } diff --git a/cmd/run-cli/cmd/root.go b/cmd/run-cli/cmd/root.go index 38c9026e..159163cf 100644 --- a/cmd/run-cli/cmd/root.go +++ b/cmd/run-cli/cmd/root.go @@ -5,6 +5,7 @@ Copyright © 2022 NAME HERE package cmd import ( + "fmt" "os" "runner-manager/cmd/run-cli/client" "runner-manager/cmd/run-cli/config" @@ -12,11 +13,15 @@ import ( "github.com/spf13/cobra" ) -var cfg *config.Config -var mgr config.Manager -var configErr error -var cli *client.Client -var active string +var ( + cfg *config.Config + mgr config.Manager + cli *client.Client + active string + needsInit bool + debug bool + needsInitError = fmt.Errorf("Please log into a runner-manager first") +) // rootCmd represents the base command when called without any subcommands var rootCmd = &cobra.Command{ @@ -28,6 +33,7 @@ var rootCmd = &cobra.Command{ // Execute adds all child commands to the root command and sets flags appropriately. // This is called by main.main(). It only needs to happen once to the rootCmd. func Execute() { + rootCmd.PersistentFlags().BoolVar(&debug, "debug", false, "Enable debug on all API calls") cobra.OnInitialize(initConfig) err := rootCmd.Execute() @@ -37,9 +43,21 @@ func Execute() { } func initConfig() { - cfg, configErr = config.LoadConfig() - if configErr == nil { - mgr, _ = cfg.GetActiveConfig() + var err error + cfg, err = config.LoadConfig() + if err != nil { + fmt.Printf("Failed to load config: %s", err) + os.Exit(1) } - cli = client.NewClient(active, mgr) + if len(cfg.Managers) == 0 { + // config is empty. + needsInit = true + } else { + mgr, err = cfg.GetActiveConfig() + if err != nil { + mgr = cfg.Managers[0] + } + active = mgr.Name + } + cli = client.NewClient(active, mgr, debug) } diff --git a/cmd/run-cli/common/common.go b/cmd/run-cli/common/common.go new file mode 100644 index 00000000..b987ad96 --- /dev/null +++ b/cmd/run-cli/common/common.go @@ -0,0 +1,53 @@ +package common + +import ( + "errors" + + "github.com/manifoldco/promptui" + "github.com/nbutton23/zxcvbn-go" +) + +func PromptPassword(label string) (string, error) { + if label == "" { + label = "Password" + } + validate := func(input string) error { + passwordStenght := zxcvbn.PasswordStrength(input, nil) + if passwordStenght.Score < 4 { + return errors.New("password is too weak") + } + return nil + } + + prompt := promptui.Prompt{ + Label: label, + Validate: validate, + Mask: '*', + } + result, err := prompt.Run() + + if err != nil { + return "", err + } + return result, nil +} + +func PromptString(label string) (string, error) { + validate := func(input string) error { + if len(input) == 0 { + return errors.New("empty input not allowed") + } + return nil + } + + prompt := promptui.Prompt{ + Label: label, + Validate: validate, + } + result, err := prompt.Run() + + if err != nil { + return "", err + } + return result, nil +} diff --git a/cmd/run-cli/config/config.go b/cmd/run-cli/config/config.go index b8b5d372..cd88cb2d 100644 --- a/cmd/run-cli/config/config.go +++ b/cmd/run-cli/config/config.go @@ -35,6 +35,14 @@ func LoadConfig() (*Config, error) { return nil, errors.Wrap(err, "fetching config") } + if _, err := os.Stat(cfgFile); err != nil { + if errors.Is(err, os.ErrNotExist) { + // return empty config + return &Config{}, nil + } + return nil, errors.Wrap(err, "accessing config file") + } + var config Config if _, err := toml.DecodeFile(cfgFile, &config); err != nil { return nil, errors.Wrap(err, "decoding toml") diff --git a/cmd/run-cli/config/home_nix.go b/cmd/run-cli/config/home_nix.go index 4a3c6ad6..92c99bad 100644 --- a/cmd/run-cli/config/home_nix.go +++ b/cmd/run-cli/config/home_nix.go @@ -4,16 +4,17 @@ package config import ( - "fmt" "os" "path/filepath" + + "github.com/pkg/errors" ) func getHomeDir() (string, error) { - home := os.Getenv("HOME") + home, err := os.UserHomeDir() - if home == "" { - return "", fmt.Errorf("failed to get home folder") + if err != nil { + return "", errors.Wrap(err, "fetching home dir") } return filepath.Join(home, ".local", "share", DefaultAppFolder), nil diff --git a/database/sql/sql.go b/database/sql/sql.go index 89fd1fd5..95533cac 100644 --- a/database/sql/sql.go +++ b/database/sql/sql.go @@ -75,12 +75,17 @@ func (s *sqlDatabase) sqlToCommonPool(pool Pool) params.Pool { OSType: pool.OSType, Enabled: pool.Enabled, Tags: make([]params.Tag, len(pool.Tags)), + Instances: make([]params.Instance, len(pool.Instances)), } for idx, val := range pool.Tags { ret.Tags[idx] = s.sqlToCommonTags(*val) } + for idx, inst := range pool.Instances { + ret.Instances[idx] = s.sqlToParamsInstance(inst) + } + return ret } @@ -273,9 +278,6 @@ func (s *sqlDatabase) ListRepositories(ctx context.Context) ([]params.Repository func (s *sqlDatabase) DeleteRepository(ctx context.Context, repoID string, hardDelete bool) error { repo, err := s.getRepoByID(ctx, repoID) if err != nil { - if err == runnerErrors.ErrNotFound { - return nil - } return errors.Wrap(err, "fetching repo") } @@ -391,13 +393,10 @@ func (s *sqlDatabase) ListOrganizations(ctx context.Context) ([]params.Organizat func (s *sqlDatabase) DeleteOrganization(ctx context.Context, name string) error { org, err := s.getOrg(ctx, name, false) if err != nil { - if err == runnerErrors.ErrNotFound { - return nil - } return errors.Wrap(err, "fetching repo") } - q := s.conn.Delete(&org) + q := s.conn.Unscoped().Delete(&org) if q.Error != nil && !errors.Is(q.Error, gorm.ErrRecordNotFound) { return errors.Wrap(q.Error, "deleting org") } @@ -579,7 +578,7 @@ func (s *sqlDatabase) ListOrgPools(ctx context.Context, orgID string) ([]params. return ret, nil } -func (s *sqlDatabase) getRepoPool(ctx context.Context, repoID, poolID string) (Pool, error) { +func (s *sqlDatabase) getRepoPool(ctx context.Context, repoID, poolID string, preload ...string) (Pool, error) { repo, err := s.getRepoByID(ctx, repoID) if err != nil { return Pool{}, errors.Wrap(err, "fetching repo") @@ -589,8 +588,16 @@ func (s *sqlDatabase) getRepoPool(ctx context.Context, repoID, poolID string) (P if err != nil { return Pool{}, errors.Wrap(runnerErrors.ErrBadRequest, "parsing id") } + + q := s.conn + if len(preload) > 0 { + for _, item := range preload { + q = q.Preload(item) + } + } + var pool []Pool - err = s.conn.Model(&repo).Association("Pools").Find(&pool, "id = ?", u) + err = q.Model(&repo).Association("Pools").Find(&pool, "id = ?", u) if err != nil { return Pool{}, errors.Wrap(err, "fetching pool") } @@ -602,7 +609,7 @@ func (s *sqlDatabase) getRepoPool(ctx context.Context, repoID, poolID string) (P } func (s *sqlDatabase) GetRepositoryPool(ctx context.Context, repoID, poolID string) (params.Pool, error) { - pool, err := s.getRepoPool(ctx, repoID, poolID) + pool, err := s.getRepoPool(ctx, repoID, poolID, "Tags", "Instances") if err != nil { return params.Pool{}, errors.Wrap(err, "fetching pool") } @@ -670,12 +677,9 @@ func (s *sqlDatabase) GetOrganizationPool(ctx context.Context, orgID, poolID str func (s *sqlDatabase) DeleteRepositoryPool(ctx context.Context, repoID, poolID string) error { pool, err := s.getRepoPool(ctx, repoID, poolID) if err != nil { - if errors.Is(err, runnerErrors.ErrNotFound) { - return nil - } return errors.Wrap(err, "looking up repo pool") } - q := s.conn.Delete(&pool) + q := s.conn.Unscoped().Delete(&pool) if q.Error != nil && !errors.Is(q.Error, gorm.ErrRecordNotFound) { return errors.Wrap(q.Error, "deleting pool") } @@ -685,12 +689,9 @@ func (s *sqlDatabase) DeleteRepositoryPool(ctx context.Context, repoID, poolID s func (s *sqlDatabase) DeleteOrganizationPool(ctx context.Context, orgID, poolID string) error { pool, err := s.getOrgPool(ctx, orgID, poolID, false) if err != nil { - if errors.Is(err, runnerErrors.ErrNotFound) { - return nil - } return errors.Wrap(err, "looking up repo pool") } - q := s.conn.Delete(&pool) + q := s.conn.Unscoped().Delete(&pool) if q.Error != nil && !errors.Is(q.Error, gorm.ErrRecordNotFound) { return errors.Wrap(q.Error, "deleting pool") } @@ -866,12 +867,9 @@ func (s *sqlDatabase) GetInstanceByName(ctx context.Context, instanceName string func (s *sqlDatabase) DeleteInstance(ctx context.Context, poolID string, instanceName string) error { instance, err := s.getPoolInstanceByName(ctx, poolID, instanceName) if err != nil { - if errors.Is(err, runnerErrors.ErrNotFound) { - return nil - } return errors.Wrap(err, "deleting instance") } - if q := s.conn.Delete(&instance); q.Error != nil { + if q := s.conn.Unscoped().Delete(&instance); q.Error != nil { if errors.Is(q.Error, gorm.ErrRecordNotFound) { return nil } @@ -1005,7 +1003,7 @@ func (s *sqlDatabase) updatePool(pool Pool, param params.UpdatePoolParams) (para tags := make([]Tag, len(param.Tags)) for idx, t := range param.Tags { tags[idx] = Tag{ - Name: t.Name, + Name: t, } } @@ -1018,7 +1016,7 @@ func (s *sqlDatabase) updatePool(pool Pool, param params.UpdatePoolParams) (para } func (s *sqlDatabase) UpdateRepositoryPool(ctx context.Context, repoID, poolID string, param params.UpdatePoolParams) (params.Pool, error) { - pool, err := s.getRepoPool(ctx, repoID, poolID) + pool, err := s.getRepoPool(ctx, repoID, poolID, "Tags") if err != nil { return params.Pool{}, errors.Wrap(err, "fetching pool") } diff --git a/go.mod b/go.mod index 109495fe..f95ff4dc 100644 --- a/go.mod +++ b/go.mod @@ -12,6 +12,7 @@ require ( github.com/gorilla/mux v1.8.0 github.com/jedib0t/go-pretty/v6 v6.3.1 github.com/lxc/lxd v0.0.0-20220415052741-1170f2806124 + github.com/manifoldco/promptui v0.9.0 github.com/nbutton23/zxcvbn-go v0.0.0-20210217022336-fa2cb2858354 github.com/pkg/errors v0.9.1 github.com/satori/go.uuid v1.2.1-0.20181028125025-b2ce2384e17b @@ -26,6 +27,7 @@ require ( ) require ( + github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e // indirect github.com/felixge/httpsnoop v1.0.1 // indirect github.com/flosch/pongo2 v0.0.0-20200913210552-0d938eb266f3 // indirect github.com/go-macaroon-bakery/macaroonpb v1.0.0 // indirect diff --git a/go.sum b/go.sum index 144af30c..008b5279 100644 --- a/go.sum +++ b/go.sum @@ -35,8 +35,11 @@ github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= @@ -181,6 +184,8 @@ github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lxc/lxd v0.0.0-20220415052741-1170f2806124 h1:EmjWCASxSUz+ymsEJfiWN3yx3yTypoKJrnOSSzAWYds= github.com/lxc/lxd v0.0.0-20220415052741-1170f2806124/go.mod h1:T4xjj62BmFg1L5JfY2wRyPZtKbBeTFgo/GLwV8DFZ8M= +github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA= +github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg= github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-sqlite3 v1.14.12 h1:TJ1bhYJPV44phC+IMu1u2K/i5RriLTPe+yc68XDJ1Z0= @@ -319,6 +324,7 @@ golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180816055513-1c9583448a9c/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= diff --git a/params/params.go b/params/params.go index 6992cd5f..7189448c 100644 --- a/params/params.go +++ b/params/params.go @@ -95,6 +95,7 @@ type Pool struct { OSArch config.OSArch `json:"os_arch"` Tags []Tag `json:"tags"` Enabled bool `json:"enabled"` + Instances []Instance `json:"instances"` } type Internal struct { diff --git a/params/requests.go b/params/requests.go index e3730190..4bb277d5 100644 --- a/params/requests.go +++ b/params/requests.go @@ -47,7 +47,7 @@ type NewUserParams struct { } type UpdatePoolParams struct { - Tags []Tag `json:"tags"` + Tags []string `json:"tags"` Enabled *bool `json:"enabled"` MaxRunners *uint `json:"max_runners"` MinIdleRunners *uint `json:"min_idle_runners"` diff --git a/runner/repositories.go b/runner/repositories.go index 6330facc..6c5ff7d1 100644 --- a/runner/repositories.go +++ b/runner/repositories.go @@ -248,10 +248,7 @@ func (r *Runner) DeleteRepoPool(ctx context.Context, repoID, poolID string) erro pool, err := r.store.GetRepositoryPool(ctx, repoID, poolID) if err != nil { - if !errors.Is(err, runnerErrors.ErrNotFound) { - return errors.Wrap(err, "fetching pool") - } - return nil + return errors.Wrap(err, "fetching pool") } instances, err := r.store.ListInstances(ctx, pool.ID) @@ -269,10 +266,7 @@ func (r *Runner) DeleteRepoPool(ctx context.Context, repoID, poolID string) erro } if err := r.store.DeleteRepositoryPool(ctx, repoID, poolID); err != nil { - // deleted by some othe call? - if !errors.Is(err, runnerErrors.ErrNotFound) { - return errors.Wrap(err, "deleting pool") - } + return errors.Wrap(err, "deleting pool") } return nil } @@ -341,7 +335,7 @@ func (r *Runner) UpdateRepoPool(ctx context.Context, repoID, poolID string, para minIdleRunners = *param.MinIdleRunners } - if minIdleRunners < maxRunners { + if minIdleRunners > maxRunners { return params.Pool{}, runnerErrors.NewBadRequestError("min_idle_runners cannot be larger than max_runners") }