diff --git a/apiserver/controllers/instances.go b/apiserver/controllers/instances.go index 970ea88a..ff0d19e1 100644 --- a/apiserver/controllers/instances.go +++ b/apiserver/controllers/instances.go @@ -27,7 +27,7 @@ func (a *APIController) ListPoolInstancesHandler(w http.ResponseWriter, r *http. instances, err := a.r.ListPoolInstances(ctx, poolID) if err != nil { - log.Printf("listing pools: %s", err) + log.Printf("listing pool instances: %s", err) handleError(w, err) return } @@ -60,6 +60,68 @@ func (a *APIController) GetInstanceHandler(w http.ResponseWriter, r *http.Reques json.NewEncoder(w).Encode(instance) } +func (a *APIController) ListRepoInstancesHandler(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + vars := mux.Vars(r) + repoID, ok := vars["repoID"] + if !ok { + w.WriteHeader(http.StatusBadRequest) + json.NewEncoder(w).Encode(params.APIErrorResponse{ + Error: "Bad Request", + Details: "No repo ID specified", + }) + return + } + + instances, err := a.r.ListRepoInstances(ctx, repoID) + if err != nil { + log.Printf("listing pools: %s", err) + handleError(w, err) + return + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(instances) +} + +func (a *APIController) ListOrgInstancesHandler(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + vars := mux.Vars(r) + orgID, ok := vars["orgID"] + if !ok { + w.WriteHeader(http.StatusBadRequest) + json.NewEncoder(w).Encode(params.APIErrorResponse{ + Error: "Bad Request", + Details: "No org ID specified", + }) + return + } + + instances, err := a.r.ListOrgInstances(ctx, orgID) + if err != nil { + log.Printf("listing pools: %s", err) + handleError(w, err) + return + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(instances) +} + +func (a *APIController) ListAllInstancesHandler(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + instances, err := a.r.ListAllInstances(ctx) + if err != nil { + log.Printf("listing instances: %s", err) + handleError(w, err) + return + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(instances) +} + func (a *APIController) InstanceStatusMessageHandler(w http.ResponseWriter, r *http.Request) { ctx := r.Context() diff --git a/apiserver/controllers/organizations.go b/apiserver/controllers/organizations.go index fccb22ea..6c606a22 100644 --- a/apiserver/controllers/organizations.go +++ b/apiserver/controllers/organizations.go @@ -266,27 +266,3 @@ func (a *APIController) UpdateOrgPoolHandler(w http.ResponseWriter, r *http.Requ w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(pool) } - -func (a *APIController) ListOrgInstancesHandler(w http.ResponseWriter, r *http.Request) { - ctx := r.Context() - vars := mux.Vars(r) - orgID, ok := vars["orgID"] - if !ok { - w.WriteHeader(http.StatusBadRequest) - json.NewEncoder(w).Encode(params.APIErrorResponse{ - Error: "Bad Request", - Details: "No org ID specified", - }) - return - } - - instances, err := a.r.ListOrgInstances(ctx, orgID) - if err != nil { - log.Printf("listing pools: %s", err) - handleError(w, err) - return - } - - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(instances) -} diff --git a/apiserver/controllers/pools.go b/apiserver/controllers/pools.go new file mode 100644 index 00000000..1e035150 --- /dev/null +++ b/apiserver/controllers/pools.go @@ -0,0 +1,111 @@ +package controllers + +import ( + "encoding/json" + "log" + "net/http" + + "garm/apiserver/params" + gErrors "garm/errors" + runnerParams "garm/params" + + "github.com/gorilla/mux" +) + +func (a *APIController) ListAllPoolsHandler(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + pools, err := a.r.ListAllPools(ctx) + + if err != nil { + log.Printf("listing pools: %s", err) + handleError(w, err) + return + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(pools) +} + +func (a *APIController) GetPoolByIDHandler(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + vars := mux.Vars(r) + poolID, ok := vars["poolID"] + if !ok { + w.WriteHeader(http.StatusBadRequest) + json.NewEncoder(w).Encode(params.APIErrorResponse{ + Error: "Bad Request", + Details: "No pool ID specified", + }) + return + } + + pool, err := a.r.GetPoolByID(ctx, poolID) + if err != nil { + log.Printf("fetching pool: %s", err) + handleError(w, err) + return + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(pool) +} + +func (a *APIController) DeletePoolByIDHandler(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + vars := mux.Vars(r) + poolID, ok := vars["poolID"] + if !ok { + w.WriteHeader(http.StatusBadRequest) + json.NewEncoder(w).Encode(params.APIErrorResponse{ + Error: "Bad Request", + Details: "No pool ID specified", + }) + return + } + + if err := a.r.DeletePoolByID(ctx, poolID); err != nil { + log.Printf("removing pool: %s", err) + handleError(w, err) + return + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + +} + +func (a *APIController) UpdatePoolByIDHandler(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + vars := mux.Vars(r) + poolID, ok := vars["poolID"] + if !ok { + w.WriteHeader(http.StatusBadRequest) + json.NewEncoder(w).Encode(params.APIErrorResponse{ + Error: "Bad Request", + Details: "No pool ID specified", + }) + return + } + + var poolData runnerParams.UpdatePoolParams + if err := json.NewDecoder(r.Body).Decode(&poolData); err != nil { + log.Printf("failed to decode: %s", err) + handleError(w, gErrors.ErrBadRequest) + return + } + + pool, err := a.r.UpdatePoolByID(ctx, poolID, poolData) + if err != nil { + log.Printf("fetching pool: %s", err) + handleError(w, err) + return + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(pool) + +} diff --git a/apiserver/controllers/repositories.go b/apiserver/controllers/repositories.go index c46c9dd2..7bed8618 100644 --- a/apiserver/controllers/repositories.go +++ b/apiserver/controllers/repositories.go @@ -266,27 +266,3 @@ func (a *APIController) UpdateRepoPoolHandler(w http.ResponseWriter, r *http.Req w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(pool) } - -func (a *APIController) ListRepoInstancesHandler(w http.ResponseWriter, r *http.Request) { - ctx := r.Context() - vars := mux.Vars(r) - repoID, ok := vars["repoID"] - if !ok { - w.WriteHeader(http.StatusBadRequest) - json.NewEncoder(w).Encode(params.APIErrorResponse{ - Error: "Bad Request", - Details: "No repo ID specified", - }) - return - } - - instances, err := a.r.ListRepoInstances(ctx, repoID) - if err != nil { - log.Printf("listing pools: %s", err) - handleError(w, err) - return - } - - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(instances) -} diff --git a/apiserver/routers/routers.go b/apiserver/routers/routers.go index 42fbc28c..b562ba93 100644 --- a/apiserver/routers/routers.go +++ b/apiserver/routers/routers.go @@ -42,16 +42,34 @@ func NewAPIRouter(han *controllers.APIController, logWriter io.Writer, authMiddl apiRouter.Use(initMiddleware.Middleware) apiRouter.Use(authMiddleware.Middleware) - // Runners (instances) + /////////// + // Pools // + /////////// + // List all pools + apiRouter.Handle("/pools/", log(os.Stdout, http.HandlerFunc(han.ListAllPoolsHandler))).Methods("GET", "OPTIONS") + apiRouter.Handle("/pools", log(os.Stdout, http.HandlerFunc(han.ListAllPoolsHandler))).Methods("GET", "OPTIONS") + // Get one pool + apiRouter.Handle("/pools/{poolID}/", log(os.Stdout, http.HandlerFunc(han.GetPoolByIDHandler))).Methods("GET", "OPTIONS") + apiRouter.Handle("/pools/{poolID}", log(os.Stdout, http.HandlerFunc(han.GetPoolByIDHandler))).Methods("GET", "OPTIONS") + // Delete one pool + apiRouter.Handle("/pools/{poolID}/", log(os.Stdout, http.HandlerFunc(han.DeletePoolByIDHandler))).Methods("DELETE", "OPTIONS") + apiRouter.Handle("/pools/{poolID}", log(os.Stdout, http.HandlerFunc(han.DeletePoolByIDHandler))).Methods("DELETE", "OPTIONS") + // Update one pool + apiRouter.Handle("/pools/{poolID}/", log(os.Stdout, http.HandlerFunc(han.UpdatePoolByIDHandler))).Methods("PUT", "OPTIONS") + apiRouter.Handle("/pools/{poolID}", log(os.Stdout, http.HandlerFunc(han.UpdatePoolByIDHandler))).Methods("PUT", "OPTIONS") // List pool instances - apiRouter.Handle("/pools/instances/{poolID}/", log(os.Stdout, http.HandlerFunc(han.ListPoolInstancesHandler))).Methods("GET", "OPTIONS") - apiRouter.Handle("/pools/instances/{poolID}", log(os.Stdout, http.HandlerFunc(han.ListPoolInstancesHandler))).Methods("GET", "OPTIONS") + apiRouter.Handle("/pools/{poolID}/instances/", log(os.Stdout, http.HandlerFunc(han.ListPoolInstancesHandler))).Methods("GET", "OPTIONS") + apiRouter.Handle("/pools/{poolID}/instances", log(os.Stdout, http.HandlerFunc(han.ListPoolInstancesHandler))).Methods("GET", "OPTIONS") + + ///////////// + // Runners // + ///////////// + // List runners + apiRouter.Handle("/instances/", log(os.Stdout, http.HandlerFunc(han.ListAllInstancesHandler))).Methods("GET", "OPTIONS") + apiRouter.Handle("/instances", log(os.Stdout, http.HandlerFunc(han.ListAllInstancesHandler))).Methods("GET", "OPTIONS") // Get instance apiRouter.Handle("/instances/{instanceName}/", log(os.Stdout, http.HandlerFunc(han.GetInstanceHandler))).Methods("GET", "OPTIONS") apiRouter.Handle("/instances/{instanceName}", log(os.Stdout, http.HandlerFunc(han.GetInstanceHandler))).Methods("GET", "OPTIONS") - // Delete instance - // apiRouter.Handle("/instances/{instanceName}/", log(os.Stdout, http.HandlerFunc(han.CatchAll))).Methods("DELETE", "OPTIONS") - // apiRouter.Handle("/instances/{instanceName}", log(os.Stdout, http.HandlerFunc(han.CatchAll))).Methods("DELETE", "OPTIONS") ///////////////////// // Repos and pools // diff --git a/cmd/garm-cli/client/client.go b/cmd/garm-cli/client/client.go index a7c5a3ef..64f69376 100644 --- a/cmd/garm-cli/client/client.go +++ b/cmd/garm-cli/client/client.go @@ -138,7 +138,7 @@ func (c *Client) GetInstanceByName(instanceName string) (params.Instance, error) } func (c *Client) ListPoolInstances(poolID string) ([]params.Instance, error) { - url := fmt.Sprintf("%s/api/v1/pools/instances/%s", c.Config.BaseURL, poolID) + url := fmt.Sprintf("%s/api/v1/pools/%s/instances", c.Config.BaseURL, poolID) var response []params.Instance resp, err := c.client.R(). @@ -153,3 +153,90 @@ func (c *Client) ListPoolInstances(poolID string) ([]params.Instance, error) { } return response, nil } + +func (c *Client) ListAllInstances() ([]params.Instance, error) { + url := fmt.Sprintf("%s/api/v1/instances", c.Config.BaseURL) + + var response []params.Instance + 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) GetPoolByID(poolID string) (params.Pool, error) { + url := fmt.Sprintf("%s/api/v1/pools/%s", c.Config.BaseURL, 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) ListAllPools() ([]params.Pool, error) { + url := fmt.Sprintf("%s/api/v1/pools", c.Config.BaseURL) + + 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) DeletePoolByID(poolID string) error { + url := fmt.Sprintf("%s/api/v1/pools/%s", c.Config.BaseURL, 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 deleting pool by ID: %s", apiErr.Details) + } + return nil +} + +func (c *Client) UpdatePoolByID(poolID string, param params.UpdatePoolParams) (params.Pool, error) { + url := fmt.Sprintf("%s/api/v1/pools/%s", c.Config.BaseURL, 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/garm-cli/cmd/org_pool.go b/cmd/garm-cli/cmd/org_pool.go index 80fe8d19..26720565 100644 --- a/cmd/garm-cli/cmd/org_pool.go +++ b/cmd/garm-cli/cmd/org_pool.go @@ -34,7 +34,7 @@ var orgPoolAddCmd = &cobra.Command{ Use: "add", Aliases: []string{"create"}, Short: "Add pool", - Long: `Add a new pool organization to the manager.`, + Long: `Add a new pool to an organization.`, SilenceUsage: true, RunE: func(cmd *cobra.Command, args []string) error { if needsInit { diff --git a/cmd/garm-cli/cmd/pool.go b/cmd/garm-cli/cmd/pool.go new file mode 100644 index 00000000..17f68f10 --- /dev/null +++ b/cmd/garm-cli/cmd/pool.go @@ -0,0 +1,297 @@ +/* +Copyright © 2022 NAME HERE + +*/ +package cmd + +import ( + "fmt" + "garm/config" + "garm/params" + "os" + "strings" + + "github.com/spf13/cobra" +) + +var ( + poolRepository string + poolOrganization string + poolAll bool +) + +// runnerCmd represents the runner command +var poolCmd = &cobra.Command{ + Use: "pool", + SilenceUsage: true, + Short: "List pools", + Long: `Query information or perform operations on pools.`, + Run: nil, +} + +var poolListCmd = &cobra.Command{ + Use: "list", + Aliases: []string{"ls"}, + Short: "List pools", + Long: `List pools of repositories, orgs or all of the above. + +This command will list pools from one repo, one org or all pools +on the system. The list flags are mutually exclusive. You must however +specify one of them. + +Example: + + List pools from one repo: + garm-cli pool list --repo=05e7eac6-4705-486d-89c9-0170bbb576af + + List pools from one org: + garm-cli pool list --org=5493e51f-3170-4ce3-9f05-3fe690fc6ec6 + + List all pools from all repos and orgs: + garm-cli pool list --all + +`, + SilenceUsage: true, + RunE: func(cmd *cobra.Command, args []string) error { + if needsInit { + return needsInitError + } + + var pools []params.Pool + var err error + + switch len(args) { + case 0: + if cmd.Flags().Changed("repo") { + pools, err = cli.ListRepoPools(poolRepository) + } else if cmd.Flags().Changed("org") { + pools, err = cli.ListOrgPools(poolOrganization) + } else if cmd.Flags().Changed("all") { + pools, err = cli.ListAllPools() + } else { + cmd.Help() + os.Exit(0) + } + default: + cmd.Help() + os.Exit(0) + } + + if err != nil { + return err + } + formatPools(pools) + return nil + }, +} + +var poolShowCmd = &cobra.Command{ + Use: "show", + Short: "Show details for a runner", + Long: `Displays a detailed view of a single runner.`, + SilenceUsage: true, + RunE: func(cmd *cobra.Command, args []string) error { + if needsInit { + return needsInitError + } + + if len(args) == 0 { + return fmt.Errorf("requires a pool ID") + } + + if len(args) > 1 { + return fmt.Errorf("too many arguments") + } + + pool, err := cli.GetPoolByID(args[0]) + if err != nil { + return err + } + formatOnePool(pool) + return nil + }, +} + +var poolDeleteCmd = &cobra.Command{ + Use: "delete", + Aliases: []string{"remove", "rm", "del"}, + Short: "Delete pool by ID", + Long: `Delete one pool by referencing it's ID, regardless of repo or org.`, + SilenceUsage: true, + RunE: func(cmd *cobra.Command, args []string) error { + if needsInit { + return needsInitError + } + + if len(args) == 0 { + return fmt.Errorf("requires a pool ID") + } + + if len(args) > 1 { + return fmt.Errorf("too many arguments") + } + + if err := cli.DeletePoolByID(args[0]); err != nil { + return err + } + return nil + }, +} + +var poolAddCmd = &cobra.Command{ + Use: "add", + Aliases: []string{"create"}, + Short: "Add pool", + Long: `Add a new pool to a repository or organization.`, + SilenceUsage: true, + RunE: func(cmd *cobra.Command, args []string) error { + if needsInit { + return needsInitError + } + + 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 + } + + var pool params.Pool + var err error + + if cmd.Flags().Changed("repo") { + pool, err = cli.CreateRepoPool(poolRepository, newPoolParams) + } else if cmd.Flags().Changed("org") { + pool, err = cli.CreateOrgPool(poolOrganization, newPoolParams) + } else { + cmd.Help() + os.Exit(0) + } + + if err != nil { + return err + } + formatOnePool(pool) + 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) == 0 { + return fmt.Errorf("command requires a poolID") + } + + if len(args) > 1 { + return fmt.Errorf("too many arguments") + } + + 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.UpdatePoolByID(args[0], poolUpdateParams) + if err != nil { + return err + } + + formatOnePool(pool) + return nil + }, +} + +func init() { + poolListCmd.Flags().StringVarP(&poolRepository, "repo", "r", "", "List all pools within this repository.") + poolListCmd.Flags().StringVarP(&poolOrganization, "org", "o", "", "List all pools withing this organization.") + poolListCmd.Flags().BoolVarP(&poolAll, "all", "a", false, "List all pools, regardless of org or repo.") + poolListCmd.MarkFlagsMutuallyExclusive("repo", "org", "all") + + 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.") + + 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") + + poolAddCmd.Flags().StringVarP(&poolRepository, "repo", "r", "", "Add the new pool within this repository.") + poolAddCmd.Flags().StringVarP(&poolOrganization, "org", "o", "", "Add the new pool withing this organization.") + poolAddCmd.MarkFlagsMutuallyExclusive("repo", "org") + + poolCmd.AddCommand( + poolListCmd, + poolShowCmd, + poolDeleteCmd, + poolUpdateCmd, + poolAddCmd, + ) + + rootCmd.AddCommand(poolCmd) +} diff --git a/cmd/garm-cli/cmd/repo_pool.go b/cmd/garm-cli/cmd/repo_pool.go index 2115394d..33c8f2b8 100644 --- a/cmd/garm-cli/cmd/repo_pool.go +++ b/cmd/garm-cli/cmd/repo_pool.go @@ -43,11 +43,11 @@ spin up instances with access to a GPU, on the desired provider.`, Run: nil, } -var poolAddCmd = &cobra.Command{ +var repoPoolAddCmd = &cobra.Command{ Use: "add", Aliases: []string{"create"}, Short: "Add pool", - Long: `Add a new pool repository to the manager.`, + Long: `Add a new pool to a repository.`, SilenceUsage: true, RunE: func(cmd *cobra.Command, args []string) error { if needsInit { @@ -86,7 +86,7 @@ var poolAddCmd = &cobra.Command{ }, } -var poolListCmd = &cobra.Command{ +var repoPoolListCmd = &cobra.Command{ Use: "list", Aliases: []string{"ls"}, Short: "List repository pools", @@ -114,7 +114,7 @@ var poolListCmd = &cobra.Command{ }, } -var poolShowCmd = &cobra.Command{ +var repoPoolShowCmd = &cobra.Command{ Use: "show", Short: "Show details for one pool", Long: `Displays detailed information about a single pool.`, @@ -137,7 +137,7 @@ var poolShowCmd = &cobra.Command{ }, } -var poolDeleteCmd = &cobra.Command{ +var repoPoolDeleteCmd = &cobra.Command{ Use: "delete", Aliases: []string{"remove", "rm", "del"}, Short: "Removes one pool", @@ -158,7 +158,7 @@ var poolDeleteCmd = &cobra.Command{ }, } -var poolUpdateCmd = &cobra.Command{ +var repoPoolUpdateCmd = &cobra.Command{ Use: "update", Short: "Update one pool", Long: `Updates pool characteristics. @@ -222,35 +222,35 @@ explicitly remove them using the runner delete command. } 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") + repoPoolAddCmd.Flags().StringVar(&poolProvider, "provider-name", "", "The name of the provider where runners will be created.") + repoPoolAddCmd.Flags().StringVar(&poolImage, "image", "", "The provider-specific image name to use for runners in this pool.") + repoPoolAddCmd.Flags().StringVar(&poolFlavor, "flavor", "", "The flavor to use for this runner.") + repoPoolAddCmd.Flags().StringVar(&poolTags, "tags", "", "A comma separated list of tags to assign to this runner.") + repoPoolAddCmd.Flags().StringVar(&poolOSType, "os-type", "linux", "Operating system type (windows, linux, etc).") + repoPoolAddCmd.Flags().StringVar(&poolOSArch, "os-arch", "amd64", "Operating system architecture (amd64, arm, etc).") + repoPoolAddCmd.Flags().UintVar(&poolMaxRunners, "max-runners", 5, "The maximum number of runner this pool will create.") + repoPoolAddCmd.Flags().UintVar(&poolMinIdleRunners, "min-idle-runners", 1, "Attempt to maintain a minimum of idle self-hosted runners of this type.") + repoPoolAddCmd.Flags().BoolVar(&poolEnabled, "enabled", false, "Enable this pool.") + repoPoolAddCmd.MarkFlagRequired("provider-name") + repoPoolAddCmd.MarkFlagRequired("image") + repoPoolAddCmd.MarkFlagRequired("flavor") + repoPoolAddCmd.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.") + repoPoolUpdateCmd.Flags().StringVar(&poolImage, "image", "", "The provider-specific image name to use for runners in this pool.") + repoPoolUpdateCmd.Flags().StringVar(&poolFlavor, "flavor", "", "The flavor to use for this runner.") + repoPoolUpdateCmd.Flags().StringVar(&poolTags, "tags", "", "A comma separated list of tags to assign to this runner.") + repoPoolUpdateCmd.Flags().StringVar(&poolOSType, "os-type", "linux", "Operating system type (windows, linux, etc).") + repoPoolUpdateCmd.Flags().StringVar(&poolOSArch, "os-arch", "amd64", "Operating system architecture (amd64, arm, etc).") + repoPoolUpdateCmd.Flags().UintVar(&poolMaxRunners, "max-runners", 5, "The maximum number of runner this pool will create.") + repoPoolUpdateCmd.Flags().UintVar(&poolMinIdleRunners, "min-idle-runners", 1, "Attempt to maintain a minimum of idle self-hosted runners of this type.") + repoPoolUpdateCmd.Flags().BoolVar(&poolEnabled, "enabled", false, "Enable this pool.") repoPoolCmd.AddCommand( poolListCmd, - poolAddCmd, - poolShowCmd, - poolDeleteCmd, - poolUpdateCmd, + repoPoolAddCmd, + repoPoolShowCmd, + repoPoolDeleteCmd, + repoPoolUpdateCmd, ) repositoryCmd.AddCommand(repoPoolCmd) @@ -258,7 +258,7 @@ func init() { func formatPools(pools []params.Pool) { t := table.NewWriter() - header := table.Row{"ID", "Image", "Flavor", "Tags", "Enabled"} + header := table.Row{"ID", "Image", "Flavor", "Tags", "Belongs to", "Level", "Enabled"} t.AppendHeader(header) for _, pool := range pools { @@ -266,7 +266,17 @@ func formatPools(pools []params.Pool) { 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}) + var belongsTo string + var level string + + if pool.RepoID != "" && pool.RepoName != "" { + belongsTo = pool.RepoName + level = "repo" + } else if pool.OrgID != "" && pool.OrgName != "" { + belongsTo = pool.OrgName + level = "org" + } + t.AppendRow(table.Row{pool.ID, pool.Image, pool.Flavor, strings.Join(tags, " "), belongsTo, level, pool.Enabled}) t.AppendSeparator() } fmt.Println(t.Render()) @@ -283,6 +293,17 @@ func formatOnePool(pool params.Pool) { tags = append(tags, tag.Name) } + var belongsTo string + var level string + + if pool.RepoID != "" && pool.RepoName != "" { + belongsTo = pool.RepoName + level = "repo" + } else if pool.OrgID != "" && pool.OrgName != "" { + belongsTo = pool.OrgName + level = "org" + } + t.AppendHeader(header) t.AppendRow(table.Row{"ID", pool.ID}) t.AppendRow(table.Row{"Provider Name", pool.ProviderName}) @@ -293,6 +314,8 @@ func formatOnePool(pool params.Pool) { 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{"Belongs to", belongsTo}) + t.AppendRow(table.Row{"Level", level}) t.AppendRow(table.Row{"Enabled", pool.Enabled}) if len(pool.Instances) > 0 { diff --git a/cmd/garm-cli/cmd/runner.go b/cmd/garm-cli/cmd/runner.go index 466d8fc3..c8a98992 100644 --- a/cmd/garm-cli/cmd/runner.go +++ b/cmd/garm-cli/cmd/runner.go @@ -7,14 +7,22 @@ package cmd import ( "fmt" "garm/params" + "os" "github.com/jedib0t/go-pretty/v6/table" "github.com/spf13/cobra" ) +var ( + runnerRepository string + runnerOrganization string + runnerAll bool +) + // runnerCmd represents the runner command var runnerCmd = &cobra.Command{ Use: "runner", + Aliases: []string{"run"}, SilenceUsage: true, Short: "List runners in a pool", Long: `Given a pool ID, of either a repository or an organization, @@ -23,25 +31,63 @@ list all instances.`, } var runnerListCmd = &cobra.Command{ - Use: "list", - Aliases: []string{"ls"}, - Short: "List pool runners", - Long: `List all configured pools for a given repository.`, + Use: "list", + Aliases: []string{"ls"}, + Short: "List runners", + Long: `List runners of pools, repositories, orgs or all of the above. + +This command expects to get either a pool ID as a positional parameter, or it expects +that one of the supported switches be used to fetch runners of --repo, --org or --all + +Example: + + List runners from one pool: + garm-cli runner list e87e70bd-3d0d-4b25-be9a-86b85e114bcb + + List runners from one repo: + garm-cli runner list --repo=05e7eac6-4705-486d-89c9-0170bbb576af + + List runners from one org: + garm-cli runner list --org=5493e51f-3170-4ce3-9f05-3fe690fc6ec6 + + List all runners from all pools belonging to all repos and orgs: + garm-cli runner list --all + +`, SilenceUsage: true, RunE: func(cmd *cobra.Command, args []string) error { if needsInit { return needsInitError } - if len(args) == 0 { - return fmt.Errorf("requires a pool ID") + var instances []params.Instance + var err error + + switch len(args) { + case 1: + if cmd.Flags().Changed("repo") || + cmd.Flags().Changed("org") || + cmd.Flags().Changed("all") { + + return fmt.Errorf("specifying a pool ID and any of [all org repo] are mutually exclusive") + } + instances, err = cli.ListPoolInstances(args[0]) + case 0: + if cmd.Flags().Changed("repo") { + instances, err = cli.ListRepoInstances(runnerRepository) + } else if cmd.Flags().Changed("org") { + instances, err = cli.ListOrgInstances(runnerOrganization) + } else if cmd.Flags().Changed("all") { + instances, err = cli.ListAllInstances() + } else { + cmd.Help() + os.Exit(0) + } + default: + cmd.Help() + os.Exit(0) } - if len(args) > 1 { - return fmt.Errorf("too many arguments") - } - - instances, err := cli.ListPoolInstances(args[0]) if err != nil { return err } @@ -78,6 +124,11 @@ var runnerShowCmd = &cobra.Command{ } func init() { + runnerListCmd.Flags().StringVarP(&runnerRepository, "repo", "r", "", "List all runners from all pools within this repository.") + runnerListCmd.Flags().StringVarP(&runnerOrganization, "org", "o", "", "List all runners from all pools withing this organization.") + runnerListCmd.Flags().BoolVarP(&runnerAll, "all", "a", false, "List all runners, regardless of org or repo.") + runnerListCmd.MarkFlagsMutuallyExclusive("repo", "org", "all") + runnerCmd.AddCommand( runnerListCmd, runnerShowCmd, diff --git a/database/common/common.go b/database/common/common.go index 27d22c87..2cf38207 100644 --- a/database/common/common.go +++ b/database/common/common.go @@ -28,6 +28,11 @@ type Store interface { ListRepoPools(ctx context.Context, repoID string) ([]params.Pool, error) ListOrgPools(ctx context.Context, orgID string) ([]params.Pool, error) + // Probably a bad idea without some king of filter or at least pagination + // TODO: add filter/pagination + ListAllPools(ctx context.Context) ([]params.Pool, error) + GetPoolByID(ctx context.Context, poolID string) (params.Pool, error) + DeletePoolByID(ctx context.Context, poolID string) error DeleteRepositoryPool(ctx context.Context, repoID, poolID string) error DeleteOrganizationPool(ctx context.Context, orgID, poolID string) error @@ -42,10 +47,14 @@ type Store interface { DeleteInstance(ctx context.Context, poolID string, instanceID string) error UpdateInstance(ctx context.Context, instanceID string, param params.UpdateInstanceParams) (params.Instance, error) - ListInstances(ctx context.Context, poolID string) ([]params.Instance, error) + ListPoolInstances(ctx context.Context, poolID string) ([]params.Instance, error) ListRepoInstances(ctx context.Context, repoID string) ([]params.Instance, error) ListOrgInstances(ctx context.Context, orgID string) ([]params.Instance, error) + // Probably a bad idea without some king of filter or at least pagination + // TODO: add filter/pagination + ListAllInstances(ctx context.Context) ([]params.Instance, error) + GetPoolInstanceByName(ctx context.Context, poolID string, instanceName string) (params.Instance, error) GetInstanceByName(ctx context.Context, instanceName string) (params.Instance, error) AddInstanceStatusMessage(ctx context.Context, instanceID string, statusMessage string) error diff --git a/database/sql/instances.go b/database/sql/instances.go index 761bed74..acfa489d 100644 --- a/database/sql/instances.go +++ b/database/sql/instances.go @@ -182,7 +182,7 @@ func (s *sqlDatabase) UpdateInstance(ctx context.Context, instanceID string, par return s.sqlToParamsInstance(instance), nil } -func (s *sqlDatabase) ListInstances(ctx context.Context, poolID string) ([]params.Instance, error) { +func (s *sqlDatabase) ListPoolInstances(ctx context.Context, poolID string) ([]params.Instance, error) { pool, err := s.getPoolByID(ctx, poolID, "Tags", "Instances") if err != nil { return nil, errors.Wrap(err, "fetching pool") @@ -194,3 +194,17 @@ func (s *sqlDatabase) ListInstances(ctx context.Context, poolID string) ([]param } return ret, nil } + +func (s *sqlDatabase) ListAllInstances(ctx context.Context) ([]params.Instance, error) { + var instances []Instance + + q := s.conn.Model(&Instance{}).Find(&instances) + if q.Error != nil { + return nil, errors.Wrap(q.Error, "fetching instances") + } + ret := make([]params.Instance, len(instances)) + for idx, instance := range instances { + ret[idx] = s.sqlToParamsInstance(instance) + } + return ret, nil +} diff --git a/database/sql/organizations.go b/database/sql/organizations.go index f6c0e425..c639d962 100644 --- a/database/sql/organizations.go +++ b/database/sql/organizations.go @@ -182,7 +182,7 @@ func (s *sqlDatabase) CreateOrganizationPool(ctx context.Context, orgId string, } } - pool, err := s.getPoolByID(ctx, newPool.ID.String(), "Tags") + pool, err := s.getPoolByID(ctx, newPool.ID.String(), "Tags", "Instances", "Organization", "Repository") if err != nil { return params.Pool{}, errors.Wrap(err, "fetching pool") } @@ -247,7 +247,7 @@ func (s *sqlDatabase) ListOrgInstances(ctx context.Context, orgID string) ([]par } func (s *sqlDatabase) UpdateOrganizationPool(ctx context.Context, orgID, poolID string, param params.UpdatePoolParams) (params.Pool, error) { - pool, err := s.getOrgPool(ctx, orgID, poolID, "Tags") + pool, err := s.getOrgPool(ctx, orgID, poolID, "Tags", "Instances", "Organization", "Repository") if err != nil { return params.Pool{}, errors.Wrap(err, "fetching pool") } diff --git a/database/sql/pools.go b/database/sql/pools.go new file mode 100644 index 00000000..7ac6dc2f --- /dev/null +++ b/database/sql/pools.go @@ -0,0 +1,49 @@ +package sql + +import ( + "context" + + "garm/params" + + "github.com/pkg/errors" +) + +func (s *sqlDatabase) ListAllPools(ctx context.Context) ([]params.Pool, error) { + var pools []Pool + + q := s.conn.Model(&Pool{}). + Preload("Tags"). + Preload("Organization"). + Preload("Repository"). + Find(&pools) + if q.Error != nil { + return nil, errors.Wrap(q.Error, "fetching all pools") + } + + ret := make([]params.Pool, len(pools)) + for idx, val := range pools { + ret[idx] = s.sqlToCommonPool(val) + } + return ret, nil +} + +func (s *sqlDatabase) GetPoolByID(ctx context.Context, poolID string) (params.Pool, error) { + pool, err := s.getPoolByID(ctx, poolID, "Tags", "Instances", "Organization", "Repository") + if err != nil { + return params.Pool{}, errors.Wrap(err, "fetching pool by ID") + } + return s.sqlToCommonPool(pool), nil +} + +func (s *sqlDatabase) DeletePoolByID(ctx context.Context, poolID string) error { + pool, err := s.getPoolByID(ctx, poolID) + if err != nil { + return errors.Wrap(err, "fetching pool by ID") + } + + if q := s.conn.Unscoped().Delete(&pool); q.Error != nil { + return errors.Wrap(q.Error, "removing pool") + } + + return nil +} diff --git a/database/sql/repositories.go b/database/sql/repositories.go index 01939d93..78ebe04e 100644 --- a/database/sql/repositories.go +++ b/database/sql/repositories.go @@ -190,7 +190,7 @@ func (s *sqlDatabase) CreateRepositoryPool(ctx context.Context, repoId string, p } } - pool, err := s.getPoolByID(ctx, newPool.ID.String(), "Tags") + pool, err := s.getPoolByID(ctx, newPool.ID.String(), "Tags", "Instances", "Organization", "Repository") if err != nil { return params.Pool{}, errors.Wrap(err, "fetching pool") } @@ -256,7 +256,7 @@ func (s *sqlDatabase) ListRepoInstances(ctx context.Context, repoID string) ([]p } func (s *sqlDatabase) UpdateRepositoryPool(ctx context.Context, repoID, poolID string, param params.UpdatePoolParams) (params.Pool, error) { - pool, err := s.getRepoPool(ctx, repoID, poolID, "Tags") + pool, err := s.getRepoPool(ctx, repoID, poolID, "Tags", "Instances", "Organization", "Repository") if err != nil { return params.Pool{}, errors.Wrap(err, "fetching pool") } diff --git a/database/sql/util.go b/database/sql/util.go index 2a51e0b6..79f96cc9 100644 --- a/database/sql/util.go +++ b/database/sql/util.go @@ -1,9 +1,11 @@ package sql import ( + "fmt" "garm/params" "github.com/pkg/errors" + uuid "github.com/satori/go.uuid" "gorm.io/gorm" ) @@ -73,6 +75,18 @@ func (s *sqlDatabase) sqlToCommonPool(pool Pool) params.Pool { Instances: make([]params.Instance, len(pool.Instances)), } + if pool.RepoID != uuid.Nil { + ret.RepoID = pool.RepoID.String() + if pool.Repository.Owner != "" && pool.Repository.Name != "" { + ret.RepoName = fmt.Sprintf("%s/%s", pool.Repository.Owner, pool.Repository.Name) + } + } + + if pool.OrgID != uuid.Nil && pool.Organization.Name != "" { + ret.OrgID = pool.OrgID.String() + ret.OrgName = pool.Organization.Name + } + for idx, val := range pool.Tags { ret.Tags[idx] = s.sqlToCommonTags(*val) } diff --git a/go.mod b/go.mod index 3abf5c0b..105877dd 100644 --- a/go.mod +++ b/go.mod @@ -16,7 +16,7 @@ require ( 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 - github.com/spf13/cobra v1.4.0 + github.com/spf13/cobra v1.4.1-0.20220504202302-9e88759b19cd golang.org/x/crypto v0.0.0-20220321153916-2c7772ba3064 golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602 gopkg.in/natefinch/lumberjack.v2 v2.0.0 diff --git a/go.sum b/go.sum index 008b5279..49da1471 100644 --- a/go.sum +++ b/go.sum @@ -219,6 +219,8 @@ github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/spf13/cobra v1.4.0 h1:y+wJpx64xcgO1V+RcnwW0LEHxTKRi2ZDPSBjWnrg88Q= github.com/spf13/cobra v1.4.0/go.mod h1:Wo4iy3BUC+X2Fybo0PDqwJIv3dNRiZLHQymsfxlB84g= +github.com/spf13/cobra v1.4.1-0.20220504202302-9e88759b19cd h1:0Hv1DPpsKWp/xjP1sQRfLDIymRDu79mErd9H9+l0uaE= +github.com/spf13/cobra v1.4.1-0.20220504202302-9e88759b19cd/go.mod h1:Wo4iy3BUC+X2Fybo0PDqwJIv3dNRiZLHQymsfxlB84g= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= diff --git a/params/params.go b/params/params.go index 8461200e..a638aff8 100644 --- a/params/params.go +++ b/params/params.go @@ -103,6 +103,10 @@ type Pool struct { Tags []Tag `json:"tags"` Enabled bool `json:"enabled"` Instances []Instance `json:"instances"` + RepoID string `json:"repo_id,omitempty"` + RepoName string `json:"repo_name,omitempty"` + OrgID string `json:"org_id,omitempty"` + OrgName string `json:"org_name,omitempty"` } type Internal struct { diff --git a/runner/organizations.go b/runner/organizations.go index 5edc1d18..9a4af5ce 100644 --- a/runner/organizations.go +++ b/runner/organizations.go @@ -218,7 +218,7 @@ func (r *Runner) DeleteOrgPool(ctx context.Context, orgID, poolID string) error return errors.Wrap(err, "fetching pool") } - instances, err := r.store.ListInstances(ctx, pool.ID) + instances, err := r.store.ListPoolInstances(ctx, pool.ID) if err != nil { return errors.Wrap(err, "fetching instances") } diff --git a/runner/pool/common.go b/runner/pool/common.go index 08f8acf3..f85a6cd6 100644 --- a/runner/pool/common.go +++ b/runner/pool/common.go @@ -146,7 +146,7 @@ func (r *basePool) acquireNewInstance(job params.WorkflowJob) error { } // TODO: implement count - poolInstances, err := r.store.ListInstances(r.ctx, pool.ID) + poolInstances, err := r.store.ListPoolInstances(r.ctx, pool.ID) if err != nil { return errors.Wrap(err, "fetching instances") } @@ -365,7 +365,7 @@ func (r *basePool) ensureIdleRunnersForOnePool(pool params.Pool) { log.Printf("pool %s is disabled, skipping", pool.ID) return } - existingInstances, err := r.store.ListInstances(r.ctx, pool.ID) + existingInstances, err := r.store.ListPoolInstances(r.ctx, pool.ID) if err != nil { log.Printf("failed to ensure minimum idle workers for pool %s: %s", pool.ID, err) return diff --git a/runner/pools.go b/runner/pools.go new file mode 100644 index 00000000..b4e3eaba --- /dev/null +++ b/runner/pools.go @@ -0,0 +1,83 @@ +package runner + +import ( + "context" + + "garm/auth" + runnerErrors "garm/errors" + "garm/params" + + "github.com/pkg/errors" +) + +func (r *Runner) ListAllPools(ctx context.Context) ([]params.Pool, error) { + if !auth.IsAdmin(ctx) { + return []params.Pool{}, runnerErrors.ErrUnauthorized + } + pools, err := r.store.ListAllPools(ctx) + if err != nil { + return nil, errors.Wrap(err, "fetching pools") + } + return pools, nil +} + +func (r *Runner) GetPoolByID(ctx context.Context, poolID string) (params.Pool, error) { + if !auth.IsAdmin(ctx) { + return params.Pool{}, runnerErrors.ErrUnauthorized + } + + pool, err := r.store.GetPoolByID(ctx, poolID) + if err != nil { + return params.Pool{}, errors.Wrap(err, "fetching pool") + } + return pool, nil +} + +func (r *Runner) DeletePoolByID(ctx context.Context, poolID string) error { + if !auth.IsAdmin(ctx) { + return runnerErrors.ErrUnauthorized + } + + if err := r.store.DeletePoolByID(ctx, poolID); err != nil { + return errors.Wrap(err, "fetching pool") + } + return nil +} + +func (r *Runner) UpdatePoolByID(ctx context.Context, poolID string, param params.UpdatePoolParams) (params.Pool, error) { + if !auth.IsAdmin(ctx) { + return params.Pool{}, runnerErrors.ErrUnauthorized + } + + pool, err := r.store.GetPoolByID(ctx, poolID) + if err != nil { + return params.Pool{}, errors.Wrap(err, "fetching pool") + } + + maxRunners := pool.MaxRunners + minIdleRunners := pool.MinIdleRunners + + if param.MaxRunners != nil { + maxRunners = *param.MaxRunners + } + if param.MinIdleRunners != nil { + minIdleRunners = *param.MinIdleRunners + } + + if minIdleRunners > maxRunners { + return params.Pool{}, runnerErrors.NewBadRequestError("min_idle_runners cannot be larger than max_runners") + } + + var newPool params.Pool + + if pool.RepoID != "" { + newPool, err = r.store.UpdateRepositoryPool(ctx, pool.RepoID, poolID, param) + } else if pool.OrgID != "" { + newPool, err = r.store.UpdateOrganizationPool(ctx, pool.OrgID, poolID, param) + } + + if err != nil { + return params.Pool{}, errors.Wrap(err, "updating pool") + } + return newPool, nil +} diff --git a/runner/repositories.go b/runner/repositories.go index d0c0a5a3..6b64bf50 100644 --- a/runner/repositories.go +++ b/runner/repositories.go @@ -217,7 +217,7 @@ func (r *Runner) DeleteRepoPool(ctx context.Context, repoID, poolID string) erro return errors.Wrap(err, "fetching pool") } - instances, err := r.store.ListInstances(ctx, pool.ID) + instances, err := r.store.ListPoolInstances(ctx, pool.ID) if err != nil { return errors.Wrap(err, "fetching instances") } @@ -254,7 +254,7 @@ func (r *Runner) ListPoolInstances(ctx context.Context, poolID string) ([]params return nil, runnerErrors.ErrUnauthorized } - instances, err := r.store.ListInstances(ctx, poolID) + instances, err := r.store.ListPoolInstances(ctx, poolID) if err != nil { return []params.Instance{}, errors.Wrap(err, "fetching instances") } diff --git a/runner/runner.go b/runner/runner.go index 5f388a60..360b2741 100644 --- a/runner/runner.go +++ b/runner/runner.go @@ -483,6 +483,18 @@ func (r *Runner) GetInstance(ctx context.Context, instanceName string) (params.I return instance, nil } +func (r *Runner) ListAllInstances(ctx context.Context) ([]params.Instance, error) { + if !auth.IsAdmin(ctx) { + return nil, runnerErrors.ErrUnauthorized + } + + instances, err := r.store.ListAllInstances(ctx) + if err != nil { + return nil, errors.Wrap(err, "fetcing instances") + } + return instances, nil +} + func (r *Runner) AddInstanceStatusMessage(ctx context.Context, param params.InstanceUpdateMessage) error { instanceID := auth.InstanceID(ctx) if instanceID == "" { @@ -493,9 +505,6 @@ func (r *Runner) AddInstanceStatusMessage(ctx context.Context, param params.Inst return errors.Wrap(err, "adding status update") } - // if param.Status == providerCommon.RunnerIdle { - // } - updateParams := params.UpdateInstanceParams{ RunnerStatus: param.Status, }