garm/cmd/garm-cli/cmd/scalesets.go
Gabriel Adrian Samfira f805123a85 Remove the --all flag for scalesets
Display all scalesets by default, similar to runners and pools.

Signed-off-by: Gabriel Adrian Samfira <gsamfira@cloudbasesolutions.com>
2025-08-16 23:13:47 +00:00

539 lines
20 KiB
Go

// Copyright 2022 Cloudbase Solutions SRL
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations
// under the License.
package cmd
import (
"fmt"
"os"
"github.com/jedib0t/go-pretty/v6/table"
"github.com/spf13/cobra"
commonParams "github.com/cloudbase/garm-provider-common/params"
apiClientEnterprises "github.com/cloudbase/garm/client/enterprises"
apiClientOrgs "github.com/cloudbase/garm/client/organizations"
apiClientRepos "github.com/cloudbase/garm/client/repositories"
apiClientScaleSets "github.com/cloudbase/garm/client/scalesets"
"github.com/cloudbase/garm/cmd/garm-cli/common"
"github.com/cloudbase/garm/params"
)
var (
scalesetProvider string
scalesetMaxRunners uint
scalesetMinIdleRunners uint
scalesetRunnerPrefix string
scalesetName string
scalesetImage string
scalesetFlavor string
scalesetOSType string
scalesetOSArch string
scalesetEnabled bool
scalesetRunnerBootstrapTimeout uint
scalesetRepository string
scalesetOrganization string
scalesetEnterprise string
scalesetExtraSpecsFile string
scalesetExtraSpecs string
scalesetGitHubRunnerGroup string
)
type scalesetPayloadGetter interface {
GetPayload() params.ScaleSet
}
type scalesetsPayloadGetter interface {
GetPayload() params.ScaleSets
}
// scalesetCmd represents the scale set command
var scalesetCmd = &cobra.Command{
Use: "scaleset",
SilenceUsage: true,
Short: "List scale sets",
Long: `Query information or perform operations on scale sets.`,
Run: nil,
}
var scalesetListCmd = &cobra.Command{
Use: "list",
Aliases: []string{"ls"},
Short: "List scale sets",
Long: `List scale sets of repositories, orgs or all of the above.
This command will list scale sets from one repo, one org or all scale sets
on the system. The list flags are mutually exclusive. You must however
specify one of them.
Example:
List scalesets from one repo:
garm-cli scaleset list --repo=05e7eac6-4705-486d-89c9-0170bbb576af
List scalesets from one org:
garm-cli scaleset list --org=5493e51f-3170-4ce3-9f05-3fe690fc6ec6
List scalesets from one enterprise:
garm-cli scaleset list --enterprise=a8ee4c66-e762-4cbe-a35d-175dba2c9e62
List all scalesets from all repos, orgs and enterprises:
garm-cli scaleset list --all
`,
SilenceUsage: true,
RunE: func(cmd *cobra.Command, args []string) error {
if needsInit {
return errNeedsInitError
}
var response scalesetsPayloadGetter
var err error
switch len(args) {
case 0:
if cmd.Flags().Changed("repo") {
scalesetRepository, err = resolveRepository(scalesetRepository, endpointName)
if err != nil {
return err
}
listRepoScaleSetsReq := apiClientRepos.NewListRepoScaleSetsParams()
listRepoScaleSetsReq.RepoID = scalesetRepository
response, err = apiCli.Repositories.ListRepoScaleSets(listRepoScaleSetsReq, authToken)
} else if cmd.Flags().Changed("org") {
scalesetOrganization, err = resolveOrganization(scalesetOrganization, endpointName)
if err != nil {
return err
}
listOrgScaleSetsReq := apiClientOrgs.NewListOrgScaleSetsParams()
listOrgScaleSetsReq.OrgID = scalesetOrganization
response, err = apiCli.Organizations.ListOrgScaleSets(listOrgScaleSetsReq, authToken)
} else if cmd.Flags().Changed("enterprise") {
scalesetEnterprise, err = resolveEnterprise(scalesetEnterprise, endpointName)
if err != nil {
return err
}
listEnterpriseScaleSetsReq := apiClientEnterprises.NewListEnterpriseScaleSetsParams()
listEnterpriseScaleSetsReq.EnterpriseID = scalesetEnterprise
response, err = apiCli.Enterprises.ListEnterpriseScaleSets(listEnterpriseScaleSetsReq, authToken)
} else {
listScaleSetsReq := apiClientScaleSets.NewListScalesetsParams()
response, err = apiCli.Scalesets.ListScalesets(listScaleSetsReq, authToken)
}
default:
cmd.Help() //nolint
os.Exit(0)
}
if err != nil {
return err
}
formatScaleSets(response.GetPayload())
return nil
},
}
var scaleSetShowCmd = &cobra.Command{
Use: "show",
Short: "Show details for a scale set",
Long: `Displays a detailed view of a single scale set.`,
SilenceUsage: true,
RunE: func(_ *cobra.Command, args []string) error {
if needsInit {
return errNeedsInitError
}
if len(args) == 0 {
return fmt.Errorf("requires a scale set ID")
}
if len(args) > 1 {
return fmt.Errorf("too many arguments")
}
getScaleSetReq := apiClientScaleSets.NewGetScaleSetParams()
getScaleSetReq.ScalesetID = args[0]
response, err := apiCli.Scalesets.GetScaleSet(getScaleSetReq, authToken)
if err != nil {
return err
}
formatOneScaleSet(response.Payload)
return nil
},
}
var scaleSetDeleteCmd = &cobra.Command{
Use: "delete",
Aliases: []string{"remove", "rm", "del"},
Short: "Delete scale set by ID",
Long: `Delete one scale set by referencing it's ID, regardless of repo or org.`,
SilenceUsage: true,
RunE: func(_ *cobra.Command, args []string) error {
if needsInit {
return errNeedsInitError
}
if len(args) == 0 {
return fmt.Errorf("requires a scale set ID")
}
if len(args) > 1 {
return fmt.Errorf("too many arguments")
}
deleteScaleSetReq := apiClientScaleSets.NewDeleteScaleSetParams()
deleteScaleSetReq.ScalesetID = args[0]
if err := apiCli.Scalesets.DeleteScaleSet(deleteScaleSetReq, authToken); err != nil {
return err
}
return nil
},
}
var scaleSetAddCmd = &cobra.Command{
Use: "add",
Aliases: []string{"create"},
Short: "Add scale set",
Long: `Add a new scale set.`,
SilenceUsage: true,
RunE: func(cmd *cobra.Command, _ []string) error {
if needsInit {
return errNeedsInitError
}
newScaleSetParams := params.CreateScaleSetParams{
RunnerPrefix: params.RunnerPrefix{
Prefix: scalesetRunnerPrefix,
},
ProviderName: scalesetProvider,
Name: scalesetName,
MaxRunners: scalesetMaxRunners,
MinIdleRunners: scalesetMinIdleRunners,
Image: scalesetImage,
Flavor: scalesetFlavor,
OSType: commonParams.OSType(scalesetOSType),
OSArch: commonParams.OSArch(scalesetOSArch),
Enabled: scalesetEnabled,
RunnerBootstrapTimeout: scalesetRunnerBootstrapTimeout,
GitHubRunnerGroup: scalesetGitHubRunnerGroup,
}
if cmd.Flags().Changed("extra-specs") {
data, err := asRawMessage([]byte(scalesetExtraSpecs))
if err != nil {
return err
}
newScaleSetParams.ExtraSpecs = data
}
if scalesetExtraSpecsFile != "" {
data, err := extraSpecsFromFile(scalesetExtraSpecsFile)
if err != nil {
return err
}
newScaleSetParams.ExtraSpecs = data
}
if err := newScaleSetParams.Validate(); err != nil {
return err
}
var err error
var response scalesetPayloadGetter
if cmd.Flags().Changed("repo") {
scalesetRepository, err = resolveRepository(scalesetRepository, endpointName)
if err != nil {
return err
}
newRepoScaleSetReq := apiClientRepos.NewCreateRepoScaleSetParams()
newRepoScaleSetReq.RepoID = scalesetRepository
newRepoScaleSetReq.Body = newScaleSetParams
response, err = apiCli.Repositories.CreateRepoScaleSet(newRepoScaleSetReq, authToken)
} else if cmd.Flags().Changed("org") {
scalesetOrganization, err = resolveOrganization(scalesetOrganization, endpointName)
if err != nil {
return err
}
newOrgScaleSetReq := apiClientOrgs.NewCreateOrgScaleSetParams()
newOrgScaleSetReq.OrgID = scalesetOrganization
newOrgScaleSetReq.Body = newScaleSetParams
response, err = apiCli.Organizations.CreateOrgScaleSet(newOrgScaleSetReq, authToken)
} else if cmd.Flags().Changed("enterprise") {
scalesetEnterprise, err = resolveEnterprise(scalesetEnterprise, endpointName)
if err != nil {
return err
}
newEnterpriseScaleSetReq := apiClientEnterprises.NewCreateEnterpriseScaleSetParams()
newEnterpriseScaleSetReq.EnterpriseID = scalesetEnterprise
newEnterpriseScaleSetReq.Body = newScaleSetParams
response, err = apiCli.Enterprises.CreateEnterpriseScaleSet(newEnterpriseScaleSetReq, authToken)
} else {
cmd.Help() //nolint
os.Exit(0)
}
if err != nil {
return err
}
formatOneScaleSet(response.GetPayload())
return nil
},
}
var scaleSetUpdateCmd = &cobra.Command{
Use: "update",
Short: "Update one scale set",
Long: `Updates scale set characteristics.
This command updates the scale set characteristics. Runners already created prior to updating
the scale set, 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 errNeedsInitError
}
if len(args) == 0 {
return fmt.Errorf("command requires a scale set ID")
}
if len(args) > 1 {
return fmt.Errorf("too many arguments")
}
updateScaleSetReq := apiClientScaleSets.NewUpdateScaleSetParams()
scaleSetUpdateParams := params.UpdateScaleSetParams{}
if cmd.Flags().Changed("image") {
scaleSetUpdateParams.Image = scalesetImage
}
if cmd.Flags().Changed("name") {
scaleSetUpdateParams.Name = scalesetName
}
if cmd.Flags().Changed("flavor") {
scaleSetUpdateParams.Flavor = scalesetFlavor
}
if cmd.Flags().Changed("os-type") {
scaleSetUpdateParams.OSType = commonParams.OSType(scalesetOSType)
}
if cmd.Flags().Changed("os-arch") {
scaleSetUpdateParams.OSArch = commonParams.OSArch(scalesetOSArch)
}
if cmd.Flags().Changed("max-runners") {
scaleSetUpdateParams.MaxRunners = &scalesetMaxRunners
}
if cmd.Flags().Changed("min-idle-runners") {
scaleSetUpdateParams.MinIdleRunners = &scalesetMinIdleRunners
}
if cmd.Flags().Changed("runner-prefix") {
scaleSetUpdateParams.RunnerPrefix = params.RunnerPrefix{
Prefix: scalesetRunnerPrefix,
}
}
if cmd.Flags().Changed("runner-group") {
scaleSetUpdateParams.GitHubRunnerGroup = &scalesetGitHubRunnerGroup
}
if cmd.Flags().Changed("enabled") {
scaleSetUpdateParams.Enabled = &scalesetEnabled
}
if cmd.Flags().Changed("runner-bootstrap-timeout") {
scaleSetUpdateParams.RunnerBootstrapTimeout = &scalesetRunnerBootstrapTimeout
}
if cmd.Flags().Changed("extra-specs") {
data, err := asRawMessage([]byte(scalesetExtraSpecs))
if err != nil {
return err
}
scaleSetUpdateParams.ExtraSpecs = data
}
if scalesetExtraSpecsFile != "" {
data, err := extraSpecsFromFile(scalesetExtraSpecsFile)
if err != nil {
return err
}
scaleSetUpdateParams.ExtraSpecs = data
}
updateScaleSetReq.ScalesetID = args[0]
updateScaleSetReq.Body = scaleSetUpdateParams
response, err := apiCli.Scalesets.UpdateScaleSet(updateScaleSetReq, authToken)
if err != nil {
return err
}
formatOneScaleSet(response.Payload)
return nil
},
}
func init() {
scalesetListCmd.Flags().StringVarP(&scalesetRepository, "repo", "r", "", "List all scale sets within this repository.")
scalesetListCmd.Flags().StringVarP(&scalesetOrganization, "org", "o", "", "List all scale sets within this organization.")
scalesetListCmd.Flags().StringVarP(&scalesetEnterprise, "enterprise", "e", "", "List all scale sets within this enterprise.")
scalesetListCmd.MarkFlagsMutuallyExclusive("repo", "org", "enterprise")
scalesetListCmd.Flags().StringVar(&endpointName, "endpoint", "", "When using the name of an entity, the endpoint must be specified when multiple entities with the same name exist.")
scaleSetUpdateCmd.Flags().StringVar(&scalesetImage, "image", "", "The provider-specific image name to use for runners in this scale set.")
scaleSetUpdateCmd.Flags().StringVar(&scalesetFlavor, "flavor", "", "The flavor to use for the runners in this scale set.")
scaleSetUpdateCmd.Flags().StringVar(&scalesetName, "name", "", "The name of the scale set. This option is mandatory.")
scaleSetUpdateCmd.Flags().StringVar(&scalesetOSType, "os-type", "linux", "Operating system type (windows, linux, etc).")
scaleSetUpdateCmd.Flags().StringVar(&scalesetOSArch, "os-arch", "amd64", "Operating system architecture (amd64, arm, etc).")
scaleSetUpdateCmd.Flags().StringVar(&scalesetRunnerPrefix, "runner-prefix", "", "The name prefix to use for runners in this scale set.")
scaleSetUpdateCmd.Flags().UintVar(&scalesetMaxRunners, "max-runners", 5, "The maximum number of runner this scale set will create.")
scaleSetUpdateCmd.Flags().UintVar(&scalesetMinIdleRunners, "min-idle-runners", 1, "Attempt to maintain a minimum of idle self-hosted runners of this type.")
scaleSetUpdateCmd.Flags().StringVar(&scalesetGitHubRunnerGroup, "runner-group", "", "The GitHub runner group in which all runners of this scale set will be added.")
scaleSetUpdateCmd.Flags().BoolVar(&scalesetEnabled, "enabled", false, "Enable this scale set.")
scaleSetUpdateCmd.Flags().UintVar(&scalesetRunnerBootstrapTimeout, "runner-bootstrap-timeout", 20, "Duration in minutes after which a runner is considered failed if it does not join Github.")
scaleSetUpdateCmd.Flags().StringVar(&scalesetExtraSpecsFile, "extra-specs-file", "", "A file containing a valid json which will be passed to the IaaS provider managing the scale set.")
scaleSetUpdateCmd.Flags().StringVar(&scalesetExtraSpecs, "extra-specs", "", "A valid json which will be passed to the IaaS provider managing the scale set.")
scaleSetUpdateCmd.MarkFlagsMutuallyExclusive("extra-specs-file", "extra-specs")
scaleSetAddCmd.Flags().StringVar(&scalesetProvider, "provider-name", "", "The name of the provider where runners will be created.")
scaleSetAddCmd.Flags().StringVar(&scalesetImage, "image", "", "The provider-specific image name to use for runners in this scale set.")
scaleSetAddCmd.Flags().StringVar(&scalesetName, "name", "", "The name of the scale set. This option is mandatory.")
scaleSetAddCmd.Flags().StringVar(&scalesetFlavor, "flavor", "", "The flavor to use for this runner.")
scaleSetAddCmd.Flags().StringVar(&scalesetRunnerPrefix, "runner-prefix", "", "The name prefix to use for runners in this scale set.")
scaleSetAddCmd.Flags().StringVar(&scalesetOSType, "os-type", "linux", "Operating system type (windows, linux, etc).")
scaleSetAddCmd.Flags().StringVar(&scalesetOSArch, "os-arch", "amd64", "Operating system architecture (amd64, arm, etc).")
scaleSetAddCmd.Flags().StringVar(&scalesetExtraSpecsFile, "extra-specs-file", "", "A file containing a valid json which will be passed to the IaaS provider managing the scale set.")
scaleSetAddCmd.Flags().StringVar(&scalesetExtraSpecs, "extra-specs", "", "A valid json which will be passed to the IaaS provider managing the scale set.")
scaleSetAddCmd.Flags().StringVar(&scalesetGitHubRunnerGroup, "runner-group", "", "The GitHub runner group in which all runners of this scale set will be added.")
scaleSetAddCmd.Flags().UintVar(&scalesetMaxRunners, "max-runners", 5, "The maximum number of runner this scale set will create.")
scaleSetAddCmd.Flags().UintVar(&scalesetRunnerBootstrapTimeout, "runner-bootstrap-timeout", 20, "Duration in minutes after which a runner is considered failed if it does not join Github.")
scaleSetAddCmd.Flags().UintVar(&scalesetMinIdleRunners, "min-idle-runners", 1, "Attempt to maintain a minimum of idle self-hosted runners of this type.")
scaleSetAddCmd.Flags().BoolVar(&scalesetEnabled, "enabled", false, "Enable this scale set.")
scaleSetAddCmd.Flags().StringVar(&endpointName, "endpoint", "", "When using the name of an entity, the endpoint must be specified when multiple entities with the same name exist.")
scaleSetAddCmd.MarkFlagRequired("provider-name") //nolint
scaleSetAddCmd.MarkFlagRequired("name") //nolint
scaleSetAddCmd.MarkFlagRequired("image") //nolint
scaleSetAddCmd.MarkFlagRequired("flavor") //nolint
scaleSetAddCmd.Flags().StringVarP(&scalesetRepository, "repo", "r", "", "Add the new scale set within this repository.")
scaleSetAddCmd.Flags().StringVarP(&scalesetOrganization, "org", "o", "", "Add the new scale set within this organization.")
scaleSetAddCmd.Flags().StringVarP(&scalesetEnterprise, "enterprise", "e", "", "Add the new scale set within this enterprise.")
scaleSetAddCmd.MarkFlagsMutuallyExclusive("repo", "org", "enterprise")
scaleSetAddCmd.MarkFlagsMutuallyExclusive("extra-specs-file", "extra-specs")
scalesetCmd.AddCommand(
scalesetListCmd,
scaleSetShowCmd,
scaleSetDeleteCmd,
scaleSetUpdateCmd,
scaleSetAddCmd,
)
rootCmd.AddCommand(scalesetCmd)
}
func formatScaleSets(scaleSets []params.ScaleSet) {
if outputFormat == common.OutputFormatJSON {
printAsJSON(scaleSets)
return
}
t := table.NewWriter()
header := table.Row{"ID", "Scale Set Name", "Image", "Flavor", "Belongs to", "Level", "Runner Group", "Enabled", "Runner Prefix", "Provider"}
t.AppendHeader(header)
for _, scaleSet := range scaleSets {
var belongsTo string
var level string
switch {
case scaleSet.RepoID != "" && scaleSet.RepoName != "":
belongsTo = scaleSet.RepoName
level = entityTypeRepo
case scaleSet.OrgID != "" && scaleSet.OrgName != "":
belongsTo = scaleSet.OrgName
level = entityTypeOrg
case scaleSet.EnterpriseID != "" && scaleSet.EnterpriseName != "":
belongsTo = scaleSet.EnterpriseName
level = entityTypeEnterprise
}
t.AppendRow(table.Row{scaleSet.ID, scaleSet.Name, scaleSet.Image, scaleSet.Flavor, belongsTo, level, scaleSet.GitHubRunnerGroup, scaleSet.Enabled, scaleSet.GetRunnerPrefix(), scaleSet.ProviderName})
t.AppendSeparator()
}
fmt.Println(t.Render())
}
func formatOneScaleSet(scaleSet params.ScaleSet) {
if outputFormat == common.OutputFormatJSON {
printAsJSON(scaleSet)
return
}
t := table.NewWriter()
rowConfigAutoMerge := table.RowConfig{AutoMerge: true}
header := table.Row{"Field", "Value"}
var belongsTo string
var level string
switch {
case scaleSet.RepoID != "" && scaleSet.RepoName != "":
belongsTo = scaleSet.RepoName
level = entityTypeRepo
case scaleSet.OrgID != "" && scaleSet.OrgName != "":
belongsTo = scaleSet.OrgName
level = entityTypeOrg
case scaleSet.EnterpriseID != "" && scaleSet.EnterpriseName != "":
belongsTo = scaleSet.EnterpriseName
level = entityTypeEnterprise
}
t.AppendHeader(header)
t.AppendRow(table.Row{"ID", scaleSet.ID})
t.AppendRow(table.Row{"Scale Set ID", scaleSet.ScaleSetID})
t.AppendRow(table.Row{"Scale Name", scaleSet.Name})
t.AppendRow(table.Row{"Provider Name", scaleSet.ProviderName})
t.AppendRow(table.Row{"Image", scaleSet.Image})
t.AppendRow(table.Row{"Flavor", scaleSet.Flavor})
t.AppendRow(table.Row{"OS Type", scaleSet.OSType})
t.AppendRow(table.Row{"OS Architecture", scaleSet.OSArch})
t.AppendRow(table.Row{"Max Runners", scaleSet.MaxRunners})
t.AppendRow(table.Row{"Min Idle Runners", scaleSet.MinIdleRunners})
t.AppendRow(table.Row{"Runner Bootstrap Timeout", scaleSet.RunnerBootstrapTimeout})
t.AppendRow(table.Row{"Belongs to", belongsTo})
t.AppendRow(table.Row{"Level", level})
t.AppendRow(table.Row{"Enabled", scaleSet.Enabled})
t.AppendRow(table.Row{"Runner Prefix", scaleSet.GetRunnerPrefix()})
t.AppendRow(table.Row{"Extra specs", string(scaleSet.ExtraSpecs)})
t.AppendRow(table.Row{"GitHub Runner Group", scaleSet.GitHubRunnerGroup})
if len(scaleSet.Instances) > 0 {
for _, instance := range scaleSet.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: false, WidthMax: 100},
})
fmt.Println(t.Render())
}