From 8dc2221f5f9293a120b9221281f344fbceef0e0a Mon Sep 17 00:00:00 2001 From: Gerrit Date: Mon, 7 Oct 2024 13:14:58 +0200 Subject: [PATCH] Add `project machine-reservations` command. (#318) --- cmd/billing.go | 77 +++++ cmd/common_test.go | 338 ++++++++++++++++++++++ cmd/completion/project-reservations.go | 59 ++++ cmd/output/printer.go | 2 +- cmd/printers.go | 2 +- cmd/project-reservations.go | 217 ++++++++++++++ cmd/project-reservations_test.go | 279 ++++++++++++++++++ cmd/project.go | 1 + cmd/root.go | 44 +-- cmd/sorters/project-reservations.go | 85 ++++++ cmd/tableprinters/printer.go | 35 ++- cmd/tableprinters/project-reservations.go | 158 ++++++++++ cmd/version.go | 3 +- cmd/version_test.go | 68 ++--- go.mod | 36 ++- go.sum | 120 ++++---- pkg/api/version.go | 4 +- 17 files changed, 1384 insertions(+), 144 deletions(-) create mode 100644 cmd/common_test.go create mode 100644 cmd/completion/project-reservations.go create mode 100644 cmd/project-reservations.go create mode 100644 cmd/project-reservations_test.go create mode 100644 cmd/sorters/project-reservations.go create mode 100644 cmd/tableprinters/project-reservations.go diff --git a/cmd/billing.go b/cmd/billing.go index b29a30de..4f384ff6 100644 --- a/cmd/billing.go +++ b/cmd/billing.go @@ -6,6 +6,7 @@ import ( "github.com/fi-ts/cloud-go/api/client/accounting" "github.com/fi-ts/cloud-go/api/models" + "github.com/fi-ts/cloudctl/cmd/sorters" "github.com/go-openapi/strfmt" "github.com/go-playground/validator/v10" "github.com/jinzhu/now" @@ -123,6 +124,24 @@ export CLOUDCTL_COSTS_HOUR=0.01 # costs per hour return c.machineUsage() }, } + machineReservationBillingCmd := &cobra.Command{ + Use: "machine-reservation", + Short: "look at machine reservation bills", + Long: ` +You may want to convert the usage to a price in Euro by using the prices from your contract. You can use the following environment variables: + +export CLOUDCTL_COSTS_HOUR=0.01 # costs per hour + +⚠ Please be aware that any costs calculated in this fashion can still be different from the final bill as it does not include contract specific details like minimum purchase, discounts, etc. +`, + RunE: func(cmd *cobra.Command, args []string) error { + err := initBillingOpts() + if err != nil { + return err + } + return c.machineReservationUsage() + }, + } productOptionBillingCmd := &cobra.Command{ Use: "product-option", Short: "look at product option bills", @@ -227,6 +246,7 @@ export CLOUDCTL_COSTS_STORAGE_GI_HOUR=0.01 # Costs per capacity hour billingCmd.AddCommand(volumeBillingCmd) billingCmd.AddCommand(postgresBillingCmd) billingCmd.AddCommand(machineBillingCmd) + billingCmd.AddCommand(machineReservationBillingCmd) billingCmd.AddCommand(productOptionBillingCmd) billingOpts = &BillingOpts{} @@ -282,6 +302,23 @@ export CLOUDCTL_COSTS_STORAGE_GI_HOUR=0.01 # Costs per capacity hour genericcli.Must(viper.BindPFlags(machineBillingCmd.Flags())) + machineReservationBillingCmd.Flags().StringVarP(&billingOpts.Tenant, "tenant", "t", "", "the tenant to account") + machineReservationBillingCmd.Flags().StringP("time-format", "", "2006-01-02", "the time format used to parse the arguments 'from' and 'to'") + machineReservationBillingCmd.Flags().StringVarP(&billingOpts.FromString, "from", "", "", "the start time in the accounting window to look at (optional, defaults to start of the month") + machineReservationBillingCmd.Flags().StringVarP(&billingOpts.ToString, "to", "", "", "the end time in the accounting window to look at (optional, defaults to current system time)") + machineReservationBillingCmd.Flags().StringVarP(&billingOpts.ProjectID, "project-id", "p", "", "the project to account") + machineReservationBillingCmd.Flags().String("id", "", "the id to account") + machineReservationBillingCmd.Flags().String("size-id", "", "the size-id to account") + machineReservationBillingCmd.Flags().String("partition-id", "", "the partition-id to account") + + genericcli.Must(machineReservationBillingCmd.RegisterFlagCompletionFunc("tenant", c.comp.TenantListCompletion)) + genericcli.Must(machineReservationBillingCmd.RegisterFlagCompletionFunc("project-id", c.comp.ProjectListCompletion)) + genericcli.Must(machineReservationBillingCmd.RegisterFlagCompletionFunc("partition-id", c.comp.PartitionListCompletion)) + genericcli.Must(machineReservationBillingCmd.RegisterFlagCompletionFunc("size-id", c.comp.SizeListCompletion)) + genericcli.AddSortFlag(machineReservationBillingCmd, sorters.MachineReservationsBillingUsageSorter()) + + genericcli.Must(viper.BindPFlags(machineReservationBillingCmd.Flags())) + productOptionBillingCmd.Flags().StringVarP(&billingOpts.Tenant, "tenant", "t", "", "the tenant to account") productOptionBillingCmd.Flags().StringP("time-format", "", "2006-01-02", "the time format used to parse the arguments 'from' and 'to'") productOptionBillingCmd.Flags().StringVarP(&billingOpts.FromString, "from", "", "", "the start time in the accounting window to look at (optional, defaults to start of the month") @@ -495,6 +532,46 @@ func (c *config) machineUsage() error { return c.listPrinter.Print(response.Payload) } +func (c *config) machineReservationUsage() error { + from := strfmt.DateTime(billingOpts.From) + cur := models.V1MachineReservationUsageRequest{ + From: &from, + To: strfmt.DateTime(billingOpts.To), + } + if billingOpts.Tenant != "" { + cur.Tenant = billingOpts.Tenant + } + if billingOpts.ProjectID != "" { + cur.Projectid = billingOpts.ProjectID + } + if viper.IsSet("id") { + cur.ID = viper.GetString("id") + } + if viper.IsSet("size-id") { + cur.Sizeid = viper.GetString("size-id") + } + if viper.IsSet("partition-id") { + cur.Partition = viper.GetString("partition-id") + } + + response, err := c.cloud.Accounting.MachineReservationUsage(accounting.NewMachineReservationUsageParams().WithBody(&cur), nil) + if err != nil { + return err + } + + keys, err := genericcli.ParseSortFlags() + if err != nil { + return err + } + + err = sorters.MachineReservationsBillingUsageSorter().SortBy(response.Payload.Usage, keys...) + if err != nil { + return err + } + + return c.listPrinter.Print(response.Payload) +} + func (c *config) productOptionUsage() error { from := strfmt.DateTime(billingOpts.From) cur := models.V1ProductOptionUsageRequest{ diff --git a/cmd/common_test.go b/cmd/common_test.go new file mode 100644 index 00000000..81826ad2 --- /dev/null +++ b/cmd/common_test.go @@ -0,0 +1,338 @@ +package cmd + +import ( + "bytes" + "encoding/json" + "fmt" + "log/slog" + "os" + "strconv" + "strings" + "testing" + "time" + + "slices" + + "github.com/fi-ts/cloud-go/api/client" + testclient "github.com/fi-ts/cloud-go/test/client" + "github.com/fi-ts/cloudctl/cmd/completion" + "github.com/google/go-cmp/cmp" + "github.com/metal-stack/metal-lib/pkg/pointer" + "github.com/metal-stack/metal-lib/pkg/testcommon" + "github.com/spf13/afero" + "github.com/spf13/pflag" + "github.com/spf13/viper" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/undefinedlabs/go-mpatch" + "gopkg.in/yaml.v3" +) + +var testTime = time.Date(2022, time.May, 19, 1, 2, 3, 4, time.UTC) + +func init() { + _, err := mpatch.PatchMethod(time.Now, func() time.Time { return testTime }) + if err != nil { + panic(err) + } +} + +type test[R any] struct { + name string + + mocks *testclient.CloudMockFns + fsMocks func(fs afero.Fs, want R) + cmd func(want R) []string + + // disableMockClient can switch off mock client creation + // + // BE CAREFUL WITH THIS FLAG! + // the tests will then run with an HTTP client that really communicates with an endpoint. + // + // only use this flag for testing code parts for client creation! + // + // point to a test http server and make sure that environment variables + // that can potentially override values for client creation are cleaned up before running the test + disableMockClient bool + + wantErr error + want R // for json and yaml + wantTable *string // for table printer + wantWideTable *string // for wide table printer + template *string // for template printer + wantTemplate *string // for template printer + wantMarkdown *string // for markdown printer +} + +func (c *test[R]) testCmd(t *testing.T) { + require.NotEmpty(t, c.name, "test name must not be empty") + require.NotEmpty(t, c.cmd, "cmd must not be empty") + + t.Setenv(strings.ToUpper(binaryName)+"_FORCE_COLOR", strconv.FormatBool(false)) + + if c.wantErr != nil { + _, _, config := c.newMockConfig(t) + + cmd := newRootCmd(config) + os.Args = append([]string{binaryName}, c.cmd(c.want)...) + + err := cmd.Execute() + if diff := cmp.Diff(c.wantErr, err, testcommon.ErrorStringComparer()); diff != "" { + t.Errorf("error diff (+got -want):\n %s", diff) + } + + } + + for _, format := range outputFormats(c) { + format := format + t.Run(fmt.Sprintf("%v", format.Args()), func(t *testing.T) { + _, out, config := c.newMockConfig(t) + + viper.Reset() + + cmd := newRootCmd(config) + os.Args = append([]string{binaryName}, c.cmd(c.want)...) + os.Args = append(os.Args, format.Args()...) + + err := cmd.Execute() + require.NoError(t, err) + + format.Validate(t, out.Bytes()) + }) + } +} + +func (c *test[R]) newMockConfig(t *testing.T) (*client.CloudAPI, *bytes.Buffer, *config) { + mock := testclient.NewCloudMockClient(t, c.mocks) + + fs := afero.NewMemMapFs() + if c.fsMocks != nil { + c.fsMocks(fs, c.want) + } + + var ( + out bytes.Buffer + config = &config{ + fs: fs, + cloud: mock, + out: &out, + log: slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{})), + comp: &completion.Completion{}, + } + ) + + if c.disableMockClient { + config.cloud = nil + } + + return mock, &out, config +} + +func assertExhaustiveArgs(t *testing.T, args []string, exclude ...string) { + assertContainsPrefix := func(ss []string, prefix string) error { + for _, s := range ss { + if strings.HasPrefix(s, prefix) { + return nil + } + } + return fmt.Errorf("not exhaustive: does not contain %s", prefix) + } + + root := newRootCmd(&config{comp: &completion.Completion{}}) + cmd, args, err := root.Find(args) + require.NoError(t, err) + + cmd.LocalFlags().VisitAll(func(f *pflag.Flag) { + if slices.Contains(exclude, f.Name) { + return + } + require.NoError(t, assertContainsPrefix(args, "--"+f.Name), "please ensure you all available args are used in order to increase coverage or exclude them explicitly") + }) +} + +func mustMarshal(t *testing.T, d any) []byte { + b, err := json.MarshalIndent(d, "", " ") + require.NoError(t, err) + return b +} + +func mustMarshalToMultiYAML[R any](t *testing.T, data []R) []byte { + var parts []string + for _, elem := range data { + parts = append(parts, string(mustMarshal(t, elem))) + } + return []byte(strings.Join(parts, "\n---\n")) +} + +// might come in handy later: +// func mustJsonDeepCopy[O any](t *testing.T, object O) O { +// raw, err := json.Marshal(&object) +// require.NoError(t, err) +// var copy O +// err = json.Unmarshal(raw, ©) +// require.NoError(t, err) +// return copy +// } + +func outputFormats[R any](c *test[R]) []outputFormat[R] { + var formats []outputFormat[R] + + if !pointer.IsZero(c.want) { + formats = append(formats, &jsonOutputFormat[R]{want: c.want}, &yamlOutputFormat[R]{want: c.want}) + } + + if c.wantTable != nil { + formats = append(formats, &tableOutputFormat[R]{table: *c.wantTable}) + } + + if c.wantWideTable != nil { + formats = append(formats, &wideTableOutputFormat[R]{table: *c.wantWideTable}) + } + + if c.template != nil && c.wantTemplate != nil { + formats = append(formats, &templateOutputFormat[R]{template: *c.template, templateOutput: *c.wantTemplate}) + } + + if c.wantMarkdown != nil { + formats = append(formats, &markdownOutputFormat[R]{table: *c.wantMarkdown}) + } + + return formats +} + +type outputFormat[R any] interface { + Args() []string + Validate(t *testing.T, output []byte) +} + +type jsonOutputFormat[R any] struct { + want R +} + +func (o *jsonOutputFormat[R]) Args() []string { + return []string{"-o", "json"} +} + +func (o *jsonOutputFormat[R]) Validate(t *testing.T, output []byte) { + var got R + err := json.Unmarshal(output, &got) + require.NoError(t, err, string(output)) + + if diff := cmp.Diff(o.want, got, testcommon.StrFmtDateComparer()); diff != "" { + t.Errorf("diff (+got -want):\n %s", diff) + } +} + +type yamlOutputFormat[R any] struct { + want R +} + +func (o *yamlOutputFormat[R]) Args() []string { + return []string{"-o", "yaml"} +} + +func (o *yamlOutputFormat[R]) Validate(t *testing.T, output []byte) { + var got R + err := yaml.Unmarshal(output, &got) + require.NoError(t, err) + + if diff := cmp.Diff(o.want, got, testcommon.StrFmtDateComparer()); diff != "" { + t.Errorf("diff (+got -want):\n %s", diff) + } +} + +type tableOutputFormat[R any] struct { + table string +} + +func (o *tableOutputFormat[R]) Args() []string { + return []string{"-o", "table"} +} + +func (o *tableOutputFormat[R]) Validate(t *testing.T, output []byte) { + validateTableRows(t, o.table, string(output)) +} + +type wideTableOutputFormat[R any] struct { + table string +} + +func (o *wideTableOutputFormat[R]) Args() []string { + return []string{"-o", "wide"} +} + +func (o *wideTableOutputFormat[R]) Validate(t *testing.T, output []byte) { + validateTableRows(t, o.table, string(output)) +} + +type templateOutputFormat[R any] struct { + template string + templateOutput string +} + +func (o *templateOutputFormat[R]) Args() []string { + return []string{"-o", "template", "--template", o.template} +} + +func (o *templateOutputFormat[R]) Validate(t *testing.T, output []byte) { + t.Logf("got following template output:\n\n%s\n\nconsider using this for test comparison if it looks correct.", string(output)) + + if diff := cmp.Diff(strings.TrimSpace(o.templateOutput), strings.TrimSpace(string(output))); diff != "" { + t.Errorf("diff (+got -want):\n %s", diff) + } +} + +type markdownOutputFormat[R any] struct { + table string +} + +func (o *markdownOutputFormat[R]) Args() []string { + return []string{"-o", "markdown"} +} + +func (o *markdownOutputFormat[R]) Validate(t *testing.T, output []byte) { + validateTableRows(t, o.table, string(output)) +} + +func validateTableRows(t *testing.T, want, got string) { + trimAll := func(ss []string) []string { + var res []string + for _, s := range ss { + res = append(res, strings.TrimSpace(s)) + } + return res + } + + var ( + trimmedWant = strings.TrimSpace(want) + trimmedGot = strings.TrimSpace(string(got)) + + wantRows = trimAll(strings.Split(trimmedWant, "\n")) + gotRows = trimAll(strings.Split(trimmedGot, "\n")) + ) + + t.Logf("got following table output:\n\n%s\n\nconsider using this for test comparison if it looks correct.", trimmedGot) + + t.Log(cmp.Diff(trimmedWant, trimmedGot)) + + require.Equal(t, len(wantRows), len(gotRows), "tables have different lengths") + + for i := range wantRows { + wantFields := trimAll(strings.Split(wantRows[i], " ")) + gotFields := trimAll(strings.Split(gotRows[i], " ")) + + require.Equal(t, len(wantFields), len(gotFields), "table fields have different lengths") + + for i := range wantFields { + assert.Equal(t, wantFields[i], gotFields[i]) + } + } +} + +func appendFromFileCommonArgs(args ...string) []string { + return append(args, []string{"-f", "/file.yaml", "--skip-security-prompts", "--bulk-output"}...) +} + +func commonExcludedFileArgs() []string { + return []string{"file", "bulk-output", "skip-security-prompts", "timestamps"} +} diff --git a/cmd/completion/project-reservations.go b/cmd/completion/project-reservations.go new file mode 100644 index 00000000..04d59308 --- /dev/null +++ b/cmd/completion/project-reservations.go @@ -0,0 +1,59 @@ +package completion + +import ( + "sort" + + "github.com/fi-ts/cloud-go/api/client/project" + "github.com/fi-ts/cloud-go/api/models" + "github.com/spf13/cobra" +) + +func (c *Completion) MachineReservationListCompletion(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + if len(args) == 0 { + resp, err := c.cloud.Project.ListMachineReservations(project.NewListMachineReservationsParams().WithBody(&models.V1MachineReservationFindRequest{}), nil) + if err != nil { + return nil, cobra.ShellCompDirectiveError + } + + var projects []string + + for _, rv := range resp.Payload { + if rv.Projectid == nil { + continue + } + + projects = append(projects, *rv.Projectid) + } + + sort.Strings(projects) + + return projects, cobra.ShellCompDirectiveNoFileComp + } + + p := args[0] + + resp, err := c.cloud.Project.ListMachineReservations(project.NewListMachineReservationsParams().WithBody(&models.V1MachineReservationFindRequest{ + Projectid: &p, + }), nil) + if err != nil { + return nil, cobra.ShellCompDirectiveError + } + + var sizes []string + + for _, rv := range resp.Payload { + if rv.Projectid == nil || rv.Sizeid == nil { + continue + } + + if *rv.Projectid != p { + continue + } + + sizes = append(sizes, *rv.Sizeid) + } + + sort.Strings(sizes) + + return sizes, cobra.ShellCompDirectiveNoFileComp +} diff --git a/cmd/output/printer.go b/cmd/output/printer.go index 6dbe4187..f8d66aec 100644 --- a/cmd/output/printer.go +++ b/cmd/output/printer.go @@ -148,7 +148,7 @@ func newPrinter(format, order, tpl string, noHeaders bool, writer io.Writer) (Pr printer = printers.NewYAMLPrinter().WithOut(writer) case "json": printer = printers.NewJSONPrinter().WithOut(writer) - case "table", "wide": + case "table", "wide", "markdown": printer = newTablePrinter(format, order, noHeaders, nil, writer) case "template": tmpl, err := template.New("t").Funcs(sprig.TxtFuncMap()).Parse(tpl) diff --git a/cmd/printers.go b/cmd/printers.go index 3f5ec1b5..15127cc3 100644 --- a/cmd/printers.go +++ b/cmd/printers.go @@ -19,7 +19,7 @@ func newPrinterFromCLI(out io.Writer) printers.Printer { case "json": printer = printers.NewJSONPrinter().WithOut(out) case "table", "wide", "markdown": - tp := tableprinters.New() + tp := tableprinters.New(out) tablePrinter := printers.NewTablePrinter(&printers.TablePrinterConfig{ ToHeaderAndRows: tp.ToHeaderAndRows, diff --git a/cmd/project-reservations.go b/cmd/project-reservations.go new file mode 100644 index 00000000..5d3e12ca --- /dev/null +++ b/cmd/project-reservations.go @@ -0,0 +1,217 @@ +package cmd + +import ( + "errors" + + "github.com/fi-ts/cloud-go/api/client/project" + "github.com/fi-ts/cloud-go/api/models" + "github.com/fi-ts/cloudctl/cmd/sorters" + "github.com/metal-stack/metal-lib/pkg/genericcli" + "github.com/metal-stack/metal-lib/pkg/genericcli/printers" + "github.com/metal-stack/metal-lib/pkg/pointer" + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +type machineReservationsCmd struct { + *config +} + +func newMachineReservationsCmd(c *config) *cobra.Command { + w := machineReservationsCmd{ + config: c, + } + + cmdsConfig := &genericcli.CmdsConfig[*models.V1MachineReservationCreateRequest, *models.V1MachineReservationUpdateRequest, *models.V1MachineReservationResponse]{ + BinaryName: binaryName, + GenericCLI: genericcli.NewGenericCLI(w).WithFS(c.fs), + Singular: "machine-reservation", + Plural: "machine-reservations", + Description: "manage machine reservations, ids must be provided in the form @", + Sorter: sorters.MachineReservationsSorter(), + ValidArgsFn: c.comp.MachineReservationListCompletion, + DescribePrinter: func() printers.Printer { return c.describePrinter }, + ListPrinter: func() printers.Printer { return c.listPrinter }, + ListCmdMutateFn: func(cmd *cobra.Command) { + cmd.Flags().String("id", "", "show reservations of given id") + cmd.Flags().String("project", "", "show reservations of given project") + cmd.Flags().String("size", "", "show reservations of given size") + cmd.Flags().String("tenant", "", "show reservations of given tenant") + genericcli.Must(cmd.RegisterFlagCompletionFunc("id", c.comp.MachineReservationListCompletion)) + genericcli.Must(cmd.RegisterFlagCompletionFunc("tenant", c.comp.TenantListCompletion)) + genericcli.Must(cmd.RegisterFlagCompletionFunc("project", c.comp.ProjectListCompletion)) + genericcli.Must(cmd.RegisterFlagCompletionFunc("size", c.comp.SizeListCompletion)) + }, + UpdateCmdMutateFn: func(cmd *cobra.Command) { + cmd.Flags().Int32("amount", 0, "the amount of machines to reserve") + cmd.Flags().String("description", "", "the description of the reservation") + cmd.Flags().StringSlice("partitions", nil, "the partitions in which this reservation is being made") + cmd.Flags().Bool("force", false, "allows overbooking of a partition") + genericcli.Must(cmd.RegisterFlagCompletionFunc("partitions", c.comp.PartitionListCompletion)) + }, + CreateCmdMutateFn: func(cmd *cobra.Command) { + cmd.Flags().String("project", "", "the project of the reservation") + cmd.Flags().String("size", "", "the size of the reservation") + cmd.Flags().Int32("amount", 0, "the amount of machines to reserve") + cmd.Flags().String("description", "", "the description of the reservation") + cmd.Flags().StringSlice("partitions", nil, "the partitions in which this reservation is being made") + cmd.Flags().Bool("force", false, "allows overbooking of a partition") + genericcli.Must(cmd.RegisterFlagCompletionFunc("project", c.comp.ProjectListCompletion)) + genericcli.Must(cmd.RegisterFlagCompletionFunc("size", c.comp.SizeListCompletion)) + genericcli.Must(cmd.RegisterFlagCompletionFunc("partitions", c.comp.PartitionListCompletion)) + }, + EditCmdMutateFn: func(cmd *cobra.Command) { + cmd.Flags().Bool("force", false, "allows overbooking of a partition") + }, + CreateRequestFromCLI: func() (*models.V1MachineReservationCreateRequest, error) { + return &models.V1MachineReservationCreateRequest{ + Amount: pointer.PointerOrNil(viper.GetInt32("amount")), + Description: pointer.PointerOrNil(viper.GetString("description")), + Partitionids: viper.GetStringSlice("partitions"), + Projectid: pointer.PointerOrNil(viper.GetString("project")), + Sizeid: pointer.PointerOrNil(viper.GetString("size")), + }, nil + }, + UpdateRequestFromCLI: func(args []string) (*models.V1MachineReservationUpdateRequest, error) { + id, err := genericcli.GetExactlyOneArg(args) + if err != nil { + return nil, err + } + + return &models.V1MachineReservationUpdateRequest{ + ID: &id, + Amount: pointer.PointerOrNil(viper.GetInt32("amount")), + Description: pointer.PointerOrNil(viper.GetString("description")), + Partitionids: viper.GetStringSlice("partitions"), + }, nil + }, + } + + usageCmd := &cobra.Command{ + Use: "usage", + Short: "shows the current usage of machine reservations", + ValidArgsFunction: c.comp.MachineReservationListCompletion, + RunE: func(cmd *cobra.Command, _ []string) error { + return w.machineReservationsUsage() + }, + } + + usageCmd.Flags().String("project", "", "show reservations of given project") + usageCmd.Flags().String("size", "", "show reservations of given size") + usageCmd.Flags().String("tenant", "", "show reservations of given tenant") + genericcli.Must(usageCmd.RegisterFlagCompletionFunc("tenant", c.comp.TenantListCompletion)) + genericcli.Must(usageCmd.RegisterFlagCompletionFunc("project", c.comp.ProjectListCompletion)) + genericcli.Must(usageCmd.RegisterFlagCompletionFunc("size", c.comp.SizeListCompletion)) + genericcli.AddSortFlag(usageCmd, sorters.MachineReservationsUsageSorter()) + + return genericcli.NewCmds(cmdsConfig, usageCmd) +} + +func (m machineReservationsCmd) Convert(r *models.V1MachineReservationResponse) (string, *models.V1MachineReservationCreateRequest, *models.V1MachineReservationUpdateRequest, error) { + if r.ID == nil { + return "", nil, nil, errors.New("id is not defined") + } + return *r.ID, toMachineReservationCreateRequest(r), toMachineReservationUpdateRequest(r), nil +} + +func toMachineReservationCreateRequest(r *models.V1MachineReservationResponse) *models.V1MachineReservationCreateRequest { + return &models.V1MachineReservationCreateRequest{ + Amount: r.Amount, + Description: &r.Description, + Partitionids: r.Partitionids, + Projectid: r.Projectid, + Sizeid: r.Sizeid, + } +} + +func toMachineReservationUpdateRequest(r *models.V1MachineReservationResponse) *models.V1MachineReservationUpdateRequest { + return &models.V1MachineReservationUpdateRequest{ + Amount: r.Amount, + Description: &r.Description, + Partitionids: r.Partitionids, + Projectid: r.Projectid, + Sizeid: r.Sizeid, + } +} + +func (m machineReservationsCmd) Create(rq *models.V1MachineReservationCreateRequest) (*models.V1MachineReservationResponse, error) { + resp, err := m.cloud.Project.CreateMachineReservation(project.NewCreateMachineReservationParams(). + WithBody(rq). + WithForce(pointer.Pointer(viper.GetBool("force"))), nil) + if err != nil { + var r *project.CreateMachineReservationConflict + if errors.As(err, &r) { + return nil, genericcli.AlreadyExistsError() + } + return nil, err + } + + return resp.Payload, nil +} + +func (m machineReservationsCmd) Delete(id string) (*models.V1MachineReservationResponse, error) { + resp, err := m.cloud.Project.DeleteMachineReservation(project.NewDeleteMachineReservationParams().WithID(id), nil) + if err != nil { + return nil, err + } + + return resp.Payload, nil +} + +func (m machineReservationsCmd) Get(id string) (*models.V1MachineReservationResponse, error) { + resp, err := m.cloud.Project.GetMachineReservation(project.NewGetMachineReservationParams().WithID(id), nil) + if err != nil { + return nil, err + } + + return resp.Payload, nil +} + +func (m machineReservationsCmd) List() ([]*models.V1MachineReservationResponse, error) { + resp, err := m.cloud.Project.ListMachineReservations(project.NewListMachineReservationsParams(). + WithBody(&models.V1MachineReservationFindRequest{ + Projectid: pointer.PointerOrNil(viper.GetString("project")), + Sizeid: pointer.PointerOrNil(viper.GetString("size")), + Tenant: pointer.PointerOrNil(viper.GetString("tenant")), + ID: pointer.PointerOrNil(viper.GetString("id")), + }), nil) + if err != nil { + return nil, err + } + + return resp.Payload, nil +} + +func (m machineReservationsCmd) Update(rq *models.V1MachineReservationUpdateRequest) (*models.V1MachineReservationResponse, error) { + resp, err := m.cloud.Project.UpdateMachineReservation(project.NewUpdateMachineReservationParams().WithBody(rq). + WithForce(pointer.Pointer(viper.GetBool("force"))), nil) + if err != nil { + return nil, err + } + + return resp.Payload, nil +} + +func (m machineReservationsCmd) machineReservationsUsage() error { + resp, err := m.cloud.Project.MachineReservationsUsage(project.NewMachineReservationsUsageParams(). + WithBody(&models.V1MachineReservationFindRequest{ + Projectid: pointer.PointerOrNil(viper.GetString("project")), + Sizeid: pointer.PointerOrNil(viper.GetString("size")), + Tenant: pointer.PointerOrNil(viper.GetString("tenant")), + }), nil) + if err != nil { + return err + } + + keys, err := genericcli.ParseSortFlags() + if err != nil { + return err + } + + err = sorters.MachineReservationsUsageSorter().SortBy(resp.Payload, keys...) + if err != nil { + return err + } + + return m.listPrinter.Print(resp.Payload) +} diff --git a/cmd/project-reservations_test.go b/cmd/project-reservations_test.go new file mode 100644 index 00000000..3aae7519 --- /dev/null +++ b/cmd/project-reservations_test.go @@ -0,0 +1,279 @@ +package cmd + +import ( + "strconv" + "strings" + "testing" + + "github.com/fi-ts/cloud-go/api/client/project" + "github.com/fi-ts/cloud-go/api/models" + testclient "github.com/fi-ts/cloud-go/test/client" + "github.com/metal-stack/metal-lib/pkg/pointer" + "github.com/metal-stack/metal-lib/pkg/testcommon" + "github.com/spf13/afero" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" +) + +var ( + machineReservation1 = &models.V1MachineReservationResponse{ + ID: pointer.Pointer("1"), + Amount: pointer.Pointer(int32(3)), + Description: "for firewalls", + Labels: map[string]string{ + "size.metal-stack.io/reserved-at": "2024-09-19T08:57:40Z", + "size.metal-stack.io/reserved-by": "fits", + }, + Partitionids: []string{"partition-a"}, + Projectid: pointer.Pointer("project-a"), + Sizeid: pointer.Pointer("size-a"), + Tenant: pointer.Pointer("fits"), + } + machineReservation2 = &models.V1MachineReservationResponse{ + ID: pointer.Pointer("2"), + Amount: pointer.Pointer(int32(3)), + Description: "for machines", + Labels: map[string]string{ + "size.metal-stack.io/reserved-by": "fits", + }, + Partitionids: []string{"partition-a", "partition-b"}, + Projectid: pointer.Pointer("project-b"), + Sizeid: pointer.Pointer("size-b"), + Tenant: pointer.Pointer("fits"), + } +) + +func Test_ProjectMachineReservationsCmd_MultiResult(t *testing.T) { + tests := []*test[[]*models.V1MachineReservationResponse]{ + { + name: "list", + cmd: func(want []*models.V1MachineReservationResponse) []string { + return []string{"project", "machine-reservation", "list"} + }, + mocks: &testclient.CloudMockFns{ + Project: func(mock *mock.Mock) { + mock.On("ListMachineReservations", testcommon.MatchIgnoreContext(t, project.NewListMachineReservationsParams().WithBody(&models.V1MachineReservationFindRequest{})), nil).Return(&project.ListMachineReservationsOK{ + Payload: []*models.V1MachineReservationResponse{ + machineReservation2, + machineReservation1, + }, + }, nil) + }, + }, + want: []*models.V1MachineReservationResponse{ + machineReservation1, + machineReservation2, + }, + wantTable: pointer.Pointer(` +ID TENANT PROJECT SIZE AMOUNT PARTITIONS DESCRIPTION +1 fits project-a size-a 3 partition-a for firewalls +2 fits project-b size-b 3 partition-a,partition-b for machines +`), + wantWideTable: pointer.Pointer(` +ID TENANT PROJECT SIZE AMOUNT PARTITIONS DESCRIPTION LABELS +1 fits project-a size-a 3 partition-a for firewalls for firewalls size.metal-stack.io/reserved-at=2024-09-19T08:57:40Z + size.metal-stack.io/reserved-by=fits +2 fits project-b size-b 3 partition-a,partition-b for machines for machines size.metal-stack.io/reserved-by=fits +`), + template: pointer.Pointer("{{ .sizeid }} {{ .projectid }}"), + wantTemplate: pointer.Pointer(` +size-a project-a +size-b project-b +`), + wantMarkdown: pointer.Pointer(` +| ID | TENANT | PROJECT | SIZE | AMOUNT | PARTITIONS | DESCRIPTION | +|----|--------|-----------|--------|--------|-------------------------|---------------| +| 1 | fits | project-a | size-a | 3 | partition-a | for firewalls | +| 2 | fits | project-b | size-b | 3 | partition-a,partition-b | for machines | +`), + }, + { + name: "list with filters", + cmd: func(want []*models.V1MachineReservationResponse) []string { + args := []string{"project", "machine-reservation", "list", "--tenant", *want[0].Tenant, "--project", *want[0].Projectid, "--size", *want[0].Sizeid, "--id", *want[0].ID} + assertExhaustiveArgs(t, args, "sort-by") + return args + }, + mocks: &testclient.CloudMockFns{ + Project: func(mock *mock.Mock) { + mock.On("ListMachineReservations", testcommon.MatchIgnoreContext(t, project.NewListMachineReservationsParams().WithBody(&models.V1MachineReservationFindRequest{ + Projectid: pointer.Pointer("project-a"), + Sizeid: pointer.Pointer("size-a"), + Tenant: pointer.Pointer("fits"), + ID: pointer.Pointer("1"), + })), nil).Return(&project.ListMachineReservationsOK{ + Payload: []*models.V1MachineReservationResponse{ + machineReservation1, + }, + }, nil) + }, + }, + want: []*models.V1MachineReservationResponse{ + machineReservation1, + }, + }, + { + name: "apply", + cmd: func(want []*models.V1MachineReservationResponse) []string { + return appendFromFileCommonArgs("project", "machine-reservation", "apply") + }, + fsMocks: func(fs afero.Fs, want []*models.V1MachineReservationResponse) { + require.NoError(t, afero.WriteFile(fs, "/file.yaml", mustMarshalToMultiYAML(t, want), 0755)) + }, + mocks: &testclient.CloudMockFns{ + Project: func(mock *mock.Mock) { + mock.On("CreateMachineReservation", testcommon.MatchIgnoreContext(t, project.NewCreateMachineReservationParams(). + WithBody(toMachineReservationCreateRequest(machineReservation1)).WithForce(pointer.Pointer(false))), nil). + Return(nil, &project.CreateMachineReservationConflict{}).Once() + mock.On("UpdateMachineReservation", testcommon.MatchIgnoreContext(t, project.NewUpdateMachineReservationParams(). + WithBody(toMachineReservationUpdateRequest(machineReservation1)).WithForce(pointer.Pointer(false))), nil). + Return(&project.UpdateMachineReservationOK{Payload: machineReservation1}, nil) + + mock.On("CreateMachineReservation", testcommon.MatchIgnoreContext(t, project.NewCreateMachineReservationParams(). + WithBody(toMachineReservationCreateRequest(machineReservation2)).WithForce(pointer.Pointer(false))), nil). + Return(&project.CreateMachineReservationCreated{Payload: machineReservation2}, nil) + }, + }, + want: []*models.V1MachineReservationResponse{ + machineReservation1, + machineReservation2, + }, + }, + { + name: "create from file", + cmd: func(want []*models.V1MachineReservationResponse) []string { + return appendFromFileCommonArgs("project", "machine-reservation", "create") + }, + fsMocks: func(fs afero.Fs, want []*models.V1MachineReservationResponse) { + require.NoError(t, afero.WriteFile(fs, "/file.yaml", mustMarshalToMultiYAML(t, want), 0755)) + }, + mocks: &testclient.CloudMockFns{ + Project: func(mock *mock.Mock) { + mock.On("CreateMachineReservation", testcommon.MatchIgnoreContext(t, project.NewCreateMachineReservationParams(). + WithBody(toMachineReservationCreateRequest(machineReservation1)).WithForce(pointer.Pointer(false))), nil). + Return(&project.CreateMachineReservationCreated{Payload: machineReservation1}, nil) + }, + }, + want: []*models.V1MachineReservationResponse{ + machineReservation1, + }, + }, + { + name: "update from file", + cmd: func(want []*models.V1MachineReservationResponse) []string { + return appendFromFileCommonArgs("project", "machine-reservation", "update") + }, + fsMocks: func(fs afero.Fs, want []*models.V1MachineReservationResponse) { + require.NoError(t, afero.WriteFile(fs, "/file.yaml", mustMarshalToMultiYAML(t, want), 0755)) + }, + mocks: &testclient.CloudMockFns{ + Project: func(mock *mock.Mock) { + mock.On("UpdateMachineReservation", testcommon.MatchIgnoreContext(t, project.NewUpdateMachineReservationParams(). + WithBody(toMachineReservationUpdateRequest(machineReservation1)).WithForce(pointer.Pointer(false))), nil). + Return(&project.UpdateMachineReservationOK{Payload: machineReservation1}, nil) + }, + }, + want: []*models.V1MachineReservationResponse{ + machineReservation1, + }, + }, + { + name: "delete from file", + cmd: func(want []*models.V1MachineReservationResponse) []string { + return appendFromFileCommonArgs("project", "machine-reservation", "delete") + }, + fsMocks: func(fs afero.Fs, want []*models.V1MachineReservationResponse) { + require.NoError(t, afero.WriteFile(fs, "/file.yaml", mustMarshalToMultiYAML(t, want), 0755)) + }, + mocks: &testclient.CloudMockFns{ + Project: func(mock *mock.Mock) { + mock.On("DeleteMachineReservation", testcommon.MatchIgnoreContext(t, project.NewDeleteMachineReservationParams().WithID(*machineReservation1.ID)), nil). + Return(&project.DeleteMachineReservationOK{Payload: machineReservation1}, nil) + }, + }, + want: []*models.V1MachineReservationResponse{ + machineReservation1, + }, + }, + } + for _, tt := range tests { + tt.testCmd(t) + } +} + +func Test_ProjectMachineReservationsCmd_SingleResult(t *testing.T) { + tests := []*test[*models.V1MachineReservationResponse]{ + { + name: "describe", + cmd: func(want *models.V1MachineReservationResponse) []string { + return []string{"project", "machine-reservation", "describe", *want.ID} + }, + mocks: &testclient.CloudMockFns{ + Project: func(mock *mock.Mock) { + mock.On("GetMachineReservation", testcommon.MatchIgnoreContext(t, project.NewGetMachineReservationParams().WithID(*machineReservation1.ID)), nil).Return(&project.GetMachineReservationOK{ + Payload: machineReservation1, + }, nil) + }, + }, + want: machineReservation1, + wantTable: pointer.Pointer(` +ID TENANT PROJECT SIZE AMOUNT PARTITIONS DESCRIPTION +1 fits project-a size-a 3 partition-a for firewalls +`), + wantWideTable: pointer.Pointer(` +ID TENANT PROJECT SIZE AMOUNT PARTITIONS DESCRIPTION LABELS +1 fits project-a size-a 3 partition-a for firewalls for firewalls size.metal-stack.io/reserved-at=2024-09-19T08:57:40Z + size.metal-stack.io/reserved-by=fits +`), + template: pointer.Pointer("{{ .sizeid }} {{ .projectid }}"), + wantTemplate: pointer.Pointer(` +size-a project-a +`), + wantMarkdown: pointer.Pointer(` +| ID | TENANT | PROJECT | SIZE | AMOUNT | PARTITIONS | DESCRIPTION | +|----|--------|-----------|--------|--------|-------------|---------------| +| 1 | fits | project-a | size-a | 3 | partition-a | for firewalls | +`), + }, + { + name: "delete", + cmd: func(want *models.V1MachineReservationResponse) []string { + return []string{"project", "machine-reservation", "rm", *want.ID} + }, + mocks: &testclient.CloudMockFns{ + Project: func(mock *mock.Mock) { + mock.On("DeleteMachineReservation", testcommon.MatchIgnoreContext(t, project.NewDeleteMachineReservationParams().WithID(*machineReservation1.ID)), nil). + Return(&project.DeleteMachineReservationOK{Payload: machineReservation1}, nil) + }, + }, + want: machineReservation1, + }, + { + name: "create", + cmd: func(want *models.V1MachineReservationResponse) []string { + args := []string{"project", "machine-reservation", "create", + "--amount", strconv.Itoa(int(*want.Amount)), //nolint:gosec + "--description", want.Description, + "--project", *want.Projectid, + "--force", + "--partitions", strings.Join(want.Partitionids, ","), + "--size", *want.Sizeid, + } + + assertExhaustiveArgs(t, args, commonExcludedFileArgs()...) + return args + }, + mocks: &testclient.CloudMockFns{ + Project: func(mock *mock.Mock) { + mock.On("CreateMachineReservation", testcommon.MatchIgnoreContext(t, project.NewCreateMachineReservationParams(). + WithBody(toMachineReservationCreateRequest(machineReservation1)).WithForce(pointer.Pointer(true))), nil). + Return(&project.CreateMachineReservationCreated{Payload: machineReservation1}, nil) + }, + }, + want: machineReservation1, + }, + } + for _, tt := range tests { + tt.testCmd(t) + } +} diff --git a/cmd/project.go b/cmd/project.go index a5153ac1..0a6efac0 100644 --- a/cmd/project.go +++ b/cmd/project.go @@ -104,6 +104,7 @@ func newProjectCmd(c *config) *cobra.Command { projectCmd.AddCommand(projectListCmd) projectCmd.AddCommand(projectApplyCmd) projectCmd.AddCommand(projectEditCmd) + projectCmd.AddCommand(newMachineReservationsCmd(c)) return projectCmd } diff --git a/cmd/root.go b/cmd/root.go index dbc0baf5..519edb40 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -36,6 +36,26 @@ type config struct { listPrinter printers.Printer } +// Execute is the entrypoint of the cloudctl application +func Execute() { + // the config will be provided with more values on cobra init + // cobra flags do not work so early in the game + c := &config{ + fs: afero.NewOsFs(), + out: os.Stdout, + comp: &completion.Completion{}, + } + + cmd := newRootCmd(c) + err := cmd.Execute() + if err != nil { + if viper.GetBool("debug") { + panic(err) + } + os.Exit(1) + } +} + func newRootCmd(cfg *config) *cobra.Command { rootCmd := &cobra.Command{ Use: binaryName, @@ -92,26 +112,6 @@ func newRootCmd(cfg *config) *cobra.Command { return rootCmd } -// Execute is the entrypoint of the cloudctl application -func Execute() { - // the config will be provided with more values on cobra init - // cobra flags do not work so early in the game - c := &config{ - fs: afero.NewOsFs(), - out: os.Stdout, - comp: &completion.Completion{}, - } - - cmd := newRootCmd(c) - err := cmd.Execute() - if err != nil { - if viper.GetBool("debug") { - panic(err) - } - os.Exit(1) - } -} - func initConfigWithViperCtx(cfg *config) error { viper.SetEnvPrefix(strings.ToUpper(binaryName)) viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_")) @@ -163,6 +163,10 @@ func initConfigWithViperCtx(cfg *config) error { } cfg.log = slog.New(slog.NewJSONHandler(os.Stdout, opts)) + if cfg.cloud != nil { + return nil + } + driverURL := viper.GetString("url") if driverURL == "" && ctx.ApiURL != "" { driverURL = ctx.ApiURL diff --git a/cmd/sorters/project-reservations.go b/cmd/sorters/project-reservations.go new file mode 100644 index 00000000..3dd3f3b1 --- /dev/null +++ b/cmd/sorters/project-reservations.go @@ -0,0 +1,85 @@ +package sorters + +import ( + "strconv" + + "github.com/fi-ts/cloud-go/api/models" + "github.com/metal-stack/metal-lib/pkg/multisort" + p "github.com/metal-stack/metal-lib/pkg/pointer" +) + +func MachineReservationsSorter() *multisort.Sorter[*models.V1MachineReservationResponse] { + return multisort.New(multisort.FieldMap[*models.V1MachineReservationResponse]{ + "id": func(a, b *models.V1MachineReservationResponse, descending bool) multisort.CompareResult { + return multisort.Compare(p.SafeDeref(a.ID), p.SafeDeref(b.ID), descending) + }, + "tenant": func(a, b *models.V1MachineReservationResponse, descending bool) multisort.CompareResult { + return multisort.Compare(p.SafeDeref(a.Tenant), p.SafeDeref(b.Tenant), descending) + }, + "project": func(a, b *models.V1MachineReservationResponse, descending bool) multisort.CompareResult { + return multisort.Compare(p.SafeDeref(a.Projectid), p.SafeDeref(b.Projectid), descending) + }, + "size": func(a, b *models.V1MachineReservationResponse, descending bool) multisort.CompareResult { + return multisort.Compare(p.SafeDeref(a.Sizeid), p.SafeDeref(b.Sizeid), descending) + }, + "amount": func(a, b *models.V1MachineReservationResponse, descending bool) multisort.CompareResult { + return multisort.Compare(p.SafeDeref(a.Amount), p.SafeDeref(b.Amount), descending) + }, + }, multisort.Keys{{ID: "tenant"}, {ID: "project"}, {ID: "size"}, {ID: "id"}}) +} + +func MachineReservationsUsageSorter() *multisort.Sorter[*models.V1MachineReservationUsageResponse] { + return multisort.New(multisort.FieldMap[*models.V1MachineReservationUsageResponse]{ + "id": func(a, b *models.V1MachineReservationUsageResponse, descending bool) multisort.CompareResult { + return multisort.Compare(p.SafeDeref(a.ID), p.SafeDeref(b.ID), descending) + }, + "tenant": func(a, b *models.V1MachineReservationUsageResponse, descending bool) multisort.CompareResult { + return multisort.Compare(p.SafeDeref(a.Tenant), p.SafeDeref(b.Tenant), descending) + }, + "project": func(a, b *models.V1MachineReservationUsageResponse, descending bool) multisort.CompareResult { + return multisort.Compare(p.SafeDeref(a.Projectid), p.SafeDeref(b.Projectid), descending) + }, + "size": func(a, b *models.V1MachineReservationUsageResponse, descending bool) multisort.CompareResult { + return multisort.Compare(p.SafeDeref(a.Sizeid), p.SafeDeref(b.Sizeid), descending) + }, + "partition": func(a, b *models.V1MachineReservationUsageResponse, descending bool) multisort.CompareResult { + return multisort.Compare(p.SafeDeref(a.Partitionid), p.SafeDeref(b.Partitionid), descending) + }, + "reservations": func(a, b *models.V1MachineReservationUsageResponse, descending bool) multisort.CompareResult { + return multisort.Compare(p.SafeDeref(a.Reservations), p.SafeDeref(b.Reservations), descending) + }, + "used-reservations": func(a, b *models.V1MachineReservationUsageResponse, descending bool) multisort.CompareResult { + return multisort.Compare(p.SafeDeref(a.Usedreservations), p.SafeDeref(b.Usedreservations), descending) + }, + }, multisort.Keys{{ID: "tenant"}, {ID: "project"}, {ID: "partition"}, {ID: "size"}, {ID: "id"}}) +} + +func MachineReservationsBillingUsageSorter() *multisort.Sorter[*models.V1MachineReservationUsage] { + return multisort.New(multisort.FieldMap[*models.V1MachineReservationUsage]{ + "id": func(a, b *models.V1MachineReservationUsage, descending bool) multisort.CompareResult { + return multisort.Compare(p.SafeDeref(a.ID), p.SafeDeref(b.ID), descending) + }, + "tenant": func(a, b *models.V1MachineReservationUsage, descending bool) multisort.CompareResult { + return multisort.Compare(p.SafeDeref(a.Tenant), p.SafeDeref(b.Tenant), descending) + }, + "project": func(a, b *models.V1MachineReservationUsage, descending bool) multisort.CompareResult { + return multisort.Compare(p.SafeDeref(a.Projectid), p.SafeDeref(b.Projectid), descending) + }, + "size": func(a, b *models.V1MachineReservationUsage, descending bool) multisort.CompareResult { + return multisort.Compare(p.SafeDeref(a.Sizeid), p.SafeDeref(b.Sizeid), descending) + }, + "partition": func(a, b *models.V1MachineReservationUsage, descending bool) multisort.CompareResult { + return multisort.Compare(p.SafeDeref(a.Partition), p.SafeDeref(b.Partition), descending) + }, + "reservation-seconds": func(a, b *models.V1MachineReservationUsage, descending bool) multisort.CompareResult { + aSeconds, _ := strconv.ParseInt(p.SafeDeref(a.Reservationseconds), 10, 64) + bSeconds, _ := strconv.ParseInt(p.SafeDeref(b.Reservationseconds), 10, 64) + return multisort.Compare(aSeconds, bSeconds, descending) + }, + "average": func(a, b *models.V1MachineReservationUsage, descending bool) multisort.CompareResult { + aSeconds, _ := strconv.ParseFloat(p.SafeDeref(a.Average), 64) + bSeconds, _ := strconv.ParseFloat(p.SafeDeref(b.Average), 64) + return multisort.Compare(aSeconds, bSeconds, descending) + }, + }, multisort.Keys{{ID: "tenant"}, {ID: "project"}, {ID: "partition"}, {ID: "size"}, {ID: "id"}}) +} diff --git a/cmd/tableprinters/printer.go b/cmd/tableprinters/printer.go index 89bac39e..179f4dc6 100644 --- a/cmd/tableprinters/printer.go +++ b/cmd/tableprinters/printer.go @@ -3,8 +3,10 @@ package tableprinters import ( "io" + "github.com/fi-ts/cloud-go/api/models" "github.com/fi-ts/cloudctl/cmd/output" "github.com/metal-stack/metal-lib/pkg/genericcli/printers" + "github.com/metal-stack/metal-lib/pkg/pointer" ) type TablePrinter struct { @@ -12,11 +14,13 @@ type TablePrinter struct { // TODO: we want to slowly migrate to the genericcli table printer // after everything was shifted to this package we can remove the "oldPrinter" oldPrinter printers.Printer + out io.Writer } -func New() *TablePrinter { +func New(out io.Writer) *TablePrinter { return &TablePrinter{ oldPrinter: output.New(), + out: out, } } @@ -25,13 +29,26 @@ func (t *TablePrinter) SetPrinter(printer *printers.TablePrinter) { } func (t *TablePrinter) ToHeaderAndRows(data any, wide bool) ([]string, [][]string, error) { + t.t.WithOut(t.out) + // TODO: migrate old output package code to here - // switch d := data.(type) { - // default: - // return nil, nil, t.oldPrinter.Print(data) - // } - // - // fallback to old printer for as long as the migration takes: - t.t.WithOut(io.Discard) - return nil, nil, t.oldPrinter.Print(data) + switch d := data.(type) { + + // project machine reservations + case *models.V1MachineReservationResponse: + return t.MachineReservationsTable(pointer.WrapInSlice(d), wide) + case []*models.V1MachineReservationResponse: + return t.MachineReservationsTable(d, wide) + case *models.V1MachineReservationUsageResponse: + return t.MachineReservationsUsageTable(pointer.WrapInSlice(d), wide) + case []*models.V1MachineReservationUsageResponse: + return t.MachineReservationsUsageTable(d, wide) + case *models.V1MachineReservationBillingUsageResponse: + return t.MachineReservationsBillingTable(d, wide) + + default: + // fallback to old printer for as long as the migration takes: + t.t.WithOut(io.Discard) + return nil, nil, t.oldPrinter.Print(data) + } } diff --git a/cmd/tableprinters/project-reservations.go b/cmd/tableprinters/project-reservations.go new file mode 100644 index 00000000..a77ab3ac --- /dev/null +++ b/cmd/tableprinters/project-reservations.go @@ -0,0 +1,158 @@ +package tableprinters + +import ( + "fmt" + "sort" + "strconv" + "strings" + "time" + + "github.com/fi-ts/cloud-go/api/models" + "github.com/fi-ts/cloudctl/cmd/helper" + "github.com/metal-stack/metal-lib/pkg/genericcli" + "github.com/metal-stack/metal-lib/pkg/pointer" + "github.com/olekukonko/tablewriter" + "github.com/spf13/viper" +) + +func (t *TablePrinter) MachineReservationsTable(data []*models.V1MachineReservationResponse, wide bool) ([]string, [][]string, error) { + var ( + header = []string{"ID", "Tenant", "Project", "Size", "Amount", "Partitions", "Description"} + rows [][]string + ) + + if wide { + header = append(header, "Labels") + } + + for _, rv := range data { + sort.Strings(rv.Partitionids) + + row := []string{ + pointer.SafeDeref(rv.ID), + pointer.SafeDeref(rv.Tenant), + pointer.SafeDeref(rv.Projectid), + pointer.SafeDeref(rv.Sizeid), + strconv.Itoa(int(pointer.SafeDeref(rv.Amount))), + strings.Join(rv.Partitionids, ","), + genericcli.TruncateEnd(rv.Description, 50), + } + + if wide { + var labels []string + for k, v := range rv.Labels { + labels = append(labels, k+"="+v) + } + sort.Strings(labels) + + row = append(row, rv.Description, strings.Join(labels, "\n")) + } + + rows = append(rows, row) + } + + t.t.MutateTable(func(table *tablewriter.Table) { + table.SetAutoWrapText(false) + }) + + return header, rows, nil +} + +func (t *TablePrinter) MachineReservationsUsageTable(data []*models.V1MachineReservationUsageResponse, wide bool) ([]string, [][]string, error) { + var ( + header = []string{"ID", "Tenant", "Project", "Partition", "Size", "Reservations"} + rows [][]string + ) + + if wide { + header = append(header, "Allocations", "Labels") + } + + for _, rv := range data { + reservations := "0" + if pointer.SafeDeref(rv.Reservations) > 0 { + unused := pointer.SafeDeref(rv.Reservations) - pointer.SafeDeref(rv.Usedreservations) + reservations = fmt.Sprintf("%d (%d/%d used)", unused, pointer.SafeDeref(rv.Usedreservations), pointer.SafeDeref(rv.Reservations)) + } + + row := []string{ + pointer.SafeDeref(rv.ID), + pointer.SafeDeref(rv.Tenant), + pointer.SafeDeref(rv.Projectid), + pointer.SafeDeref(rv.Partitionid), + pointer.SafeDeref(rv.Sizeid), + reservations, + } + + if wide { + row = append(row, fmt.Sprintf("%d", pointer.SafeDeref(rv.Projectallocations))) + + labels := []string{} + for k, v := range rv.Labels { + labels = append(labels, k+"="+v) + } + lbls := strings.Join(labels, "\n") + + row = append(row, lbls) + } + + rows = append(rows, row) + } + + t.t.MutateTable(func(table *tablewriter.Table) { + table.SetAutoWrapText(false) + }) + + return header, rows, nil +} + +func (t *TablePrinter) MachineReservationsBillingTable(data *models.V1MachineReservationBillingUsageResponse, wide bool) ([]string, [][]string, error) { + var ( + header = []string{"Tenant", "From", "To", "ProjectID", "ProjectName", "Partition", "Size", "ID", "Reservations * Time", "Average"} + rows [][]string + ) + + for _, rv := range data.Usage { + row := []string{ + pointer.SafeDeref(rv.Tenant), + time.Time(pointer.SafeDeref(data.From)).String(), + time.Time(data.To).String(), + pointer.SafeDeref(rv.Projectid), + pointer.SafeDeref(rv.Projectname), + pointer.SafeDeref(rv.Partition), + pointer.SafeDeref(rv.Sizeid), + pointer.SafeDeref(rv.ID), + humanizeSeconds(pointer.SafeDeref(rv.Reservationseconds)), + pointer.SafeDeref(rv.Average), + } + + rows = append(rows, row) + } + + rows = append(rows, []string{"Total", "", "", "", "", "", "", "", + humanizeSeconds(pointer.SafeDeref(data.Accumulatedusage.Reservationseconds)) + secondsCosts(pointer.SafeDeref(data.Accumulatedusage.Reservationseconds)), + pointer.SafeDeref(data.Accumulatedusage.Average), + }) + + return header, rows, nil +} + +func humanizeSeconds(seconds string) string { + duration, err := strconv.ParseInt(seconds, 10, 64) + if err == nil { + return helper.HumanizeDuration(time.Duration(duration) * time.Second) + } + return "" +} + +func secondsCosts(seconds string) string { + costsPerHour := viper.GetFloat64("costs-hour") + if costsPerHour <= 0 { + return "" + } + duration, err := strconv.ParseInt(seconds, 10, 64) + if err == nil { + return fmt.Sprintf(" (%.2f €)", float64(duration/3600)*costsPerHour) + } + return "" +} diff --git a/cmd/version.go b/cmd/version.go index ff2e439b..4bdb98a8 100644 --- a/cmd/version.go +++ b/cmd/version.go @@ -3,6 +3,7 @@ package cmd import ( "fmt" + "github.com/fi-ts/cloud-go/api/client/version" "github.com/fi-ts/cloudctl/pkg/api" "github.com/metal-stack/v" "github.com/spf13/cobra" @@ -18,7 +19,7 @@ func newVersionCmd(c *config) *cobra.Command { Client: v.V.String(), } - resp, err := c.cloud.Version.Info(nil, nil) + resp, err := c.cloud.Version.Info(version.NewInfoParams(), nil) if err == nil { v.Server = resp.Payload } diff --git a/cmd/version_test.go b/cmd/version_test.go index 67f3976e..ff1520ba 100644 --- a/cmd/version_test.go +++ b/cmd/version_test.go @@ -1,57 +1,45 @@ package cmd import ( - "bytes" - "fmt" "runtime" "testing" - "github.com/fi-ts/cloud-go/api/client" "github.com/fi-ts/cloud-go/api/client/version" "github.com/fi-ts/cloud-go/api/models" - mockversion "github.com/fi-ts/cloud-go/test/mocks/version" + testclient "github.com/fi-ts/cloud-go/test/client" + "github.com/fi-ts/cloudctl/pkg/api" "github.com/metal-stack/metal-lib/pkg/pointer" + "github.com/metal-stack/metal-lib/pkg/testcommon" "github.com/metal-stack/v" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" ) func Test_newVersionCmd(t *testing.T) { - v.BuildDate = "1.1.1970" - v.GitSHA1 = "abcdef" - v.Revision = "v0.0.0" - v.Version = "v0.0.0" - - mockVersionService := new(mockversion.ClientService) - cloud := client.CloudAPI{ - Version: mockVersionService, + tests := []*test[*api.Version]{ + { + name: "version", + cmd: func(want *api.Version) []string { + return []string{"version"} + }, + mocks: &testclient.CloudMockFns{ + Version: func(mock *mock.Mock) { + mock.On("Info", testcommon.MatchIgnoreContext(t, version.NewInfoParams()), nil).Return(&version.InfoOK{ + Payload: &models.RestVersion{ + Version: pointer.Pointer("server v1.0.0"), + }, + }, nil) + }, + }, + want: &api.Version{ + Client: "client v1.0.0, " + runtime.Version(), + Server: &models.RestVersion{ + Version: pointer.Pointer("server v1.0.0"), + }, + }, + }, } - - var out bytes.Buffer - - mockVersionService.On("Info", mock.Anything, mock.Anything).Return(&version.InfoOK{Payload: &models.RestVersion{Name: pointer.Pointer("cloudctl")}}, nil) - cfg := &config{ - cloud: &cloud, - describePrinter: defaultToYAMLPrinter(&out), + for _, tt := range tests { + v.Version = "client v1.0.0" + tt.testCmd(t) } - cmd := newVersionCmd(cfg) - - err := cmd.Execute() - if err != nil { - t.Fatal(err) - } - - mockVersionService.AssertExpectations(t) - - expected := fmt.Sprintf(`--- -Client: v0.0.0 (abcdef), v0.0.0, 1.1.1970, %s -Server: - builddate: null - gitsha1: null - min_client_version: null - name: cloudctl - revision: null - version: null -`, runtime.Version()) - assert.Equal(t, expected, string(out.String())) } diff --git a/go.mod b/go.mod index 1a22795d..6229e5c1 100644 --- a/go.mod +++ b/go.mod @@ -20,18 +20,20 @@ require ( github.com/jinzhu/now v1.1.5 github.com/metal-stack/duros-go v0.5.1 github.com/metal-stack/metal-go v0.34.0 - github.com/metal-stack/metal-lib v0.18.2 + github.com/metal-stack/metal-lib v0.18.3 github.com/metal-stack/updater v1.2.2 github.com/metal-stack/v v1.0.3 github.com/olekukonko/tablewriter v0.0.5 github.com/spf13/afero v1.11.0 github.com/spf13/cobra v1.8.1 + github.com/spf13/pflag v1.0.5 github.com/spf13/viper v1.19.0 github.com/stretchr/testify v1.9.0 + github.com/undefinedlabs/go-mpatch v1.0.7 golang.org/x/sync v0.8.0 gopkg.in/yaml.v3 v3.0.1 - k8s.io/api v0.29.8 - k8s.io/apimachinery v0.29.8 + k8s.io/api v0.30.3 + k8s.io/apimachinery v0.31.0 sigs.k8s.io/yaml v1.4.0 ) @@ -56,11 +58,12 @@ require ( github.com/aws/aws-sdk-go-v2/service/ssooidc v1.22.0 // indirect github.com/aws/aws-sdk-go-v2/service/sts v1.27.0 // indirect github.com/aws/smithy-go v1.20.0 // indirect + github.com/bits-and-blooms/bitset v1.13.0 // indirect github.com/blang/semver/v4 v4.0.0 // indirect github.com/cheggaaa/pb/v3 v3.1.5 // indirect - github.com/coreos/go-iptables v0.7.0 // indirect + github.com/coder/websocket v1.8.12 // indirect + github.com/coreos/go-iptables v0.7.1-0.20240112124308-65c67c9f46e6 // indirect github.com/coreos/go-oidc/v3 v3.11.0 // indirect - github.com/coreos/go-systemd/v22 v22.5.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/dblohm7/wingoes v0.0.0-20240801171404-fc12d7c70140 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // indirect @@ -68,7 +71,9 @@ require ( github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/fxamacker/cbor/v2 v2.7.0 // indirect github.com/gabriel-vasile/mimetype v1.4.3 // indirect + github.com/gaissmai/bart v0.11.1 // indirect github.com/go-jose/go-jose/v4 v4.0.4 // indirect + github.com/go-json-experiment/json v0.0.0-20231102232822-2e55bd4e08b0 // indirect github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-ole/go-ole v1.3.0 // indirect @@ -93,7 +98,7 @@ require ( github.com/google/go-github/v56 v56.0.0 // indirect github.com/google/go-querystring v1.1.0 // indirect github.com/google/gofuzz v1.2.0 // indirect - github.com/google/nftables v0.2.0 // indirect + github.com/google/nftables v0.2.1-0.20240414091927-5e242ec57806 // indirect github.com/google/uuid v1.6.0 // indirect github.com/gorilla/csrf v1.7.2 // indirect github.com/gorilla/mux v1.8.1 // indirect @@ -142,6 +147,7 @@ require ( github.com/pelletier/go-toml/v2 v2.2.2 // indirect github.com/pierrec/lz4/v4 v4.1.21 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/prometheus-community/pro-bing v0.4.0 // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/safchain/ethtool v0.3.0 // indirect github.com/sagikazarmark/locafero v0.4.0 // indirect @@ -149,17 +155,17 @@ require ( github.com/segmentio/asm v1.2.0 // indirect github.com/sourcegraph/conc v0.3.0 // indirect github.com/spf13/cast v1.7.0 // indirect - github.com/spf13/pflag v1.0.5 // indirect github.com/stretchr/objx v0.5.2 // indirect github.com/subosito/gotenv v1.6.0 // indirect - github.com/tailscale/certstore v0.1.1-0.20231020161753-77811a65f4ff // indirect + github.com/tailscale/certstore v0.1.1-0.20231202035212-d3fa0460f47e // indirect github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55 // indirect - github.com/tailscale/golang-x-crypto v0.0.0-20240108194725-7ce1f622c780 // indirect + github.com/tailscale/golang-x-crypto v0.0.0-20240604161659-3fde5e568aa4 // indirect github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05 // indirect github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a // indirect github.com/tailscale/netlink v1.1.1-0.20211101221916-cabfb018fe85 // indirect - github.com/tailscale/web-client-prebuilt v0.0.0-20240208184856-443a64766f61 // indirect - github.com/tailscale/wireguard-go v0.0.0-20231101022006-db7604d1aa90 // indirect + github.com/tailscale/peercred v0.0.0-20240214030740-b535050b2aa4 // indirect + github.com/tailscale/web-client-prebuilt v0.0.0-20240226180453-5db17b287bf1 // indirect + github.com/tailscale/wireguard-go v0.0.0-20240731203015-71393c576b98 // indirect github.com/tcnksm/go-httpstat v0.2.0 // indirect github.com/u-root/uio v0.0.0-20240209044354-b3d14b93376a // indirect github.com/vishvananda/netlink v1.2.1-beta.2 // indirect @@ -192,12 +198,10 @@ require ( gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect - gvisor.dev/gvisor v0.0.0-20230928000133-4fe30062272c // indirect - inet.af/peercred v0.0.0-20210906144145-0893ea02156a // indirect - k8s.io/klog/v2 v2.120.1 // indirect + gvisor.dev/gvisor v0.0.0-20240722211153-64c016c92987 // indirect + k8s.io/klog/v2 v2.130.1 // indirect k8s.io/utils v0.0.0-20240902221715-702e33fdd3c3 // indirect - nhooyr.io/websocket v1.8.10 // indirect sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect - tailscale.com v1.54.0 // indirect + tailscale.com v1.72.1 // indirect ) diff --git a/go.sum b/go.sum index 049ed2c2..82119b19 100644 --- a/go.sum +++ b/go.sum @@ -50,24 +50,26 @@ github.com/aws/aws-sdk-go-v2/service/sts v1.27.0 h1:cjTRjh700H36MQ8M0LnDn33W3Jmw github.com/aws/aws-sdk-go-v2/service/sts v1.27.0/go.mod h1:nXfOBMWPokIbOY+Gi7a1psWMSvskUCemZzI+SMB7Akc= github.com/aws/smithy-go v1.20.0 h1:6+kZsCXZwKxZS9RfISnPc4EXlHoyAkm2hPuM8X2BrrQ= github.com/aws/smithy-go v1.20.0/go.mod h1:uo5RKksAl4PzhqaAbjd4rLgFoq5koTsQKYuGe7dklGc= +github.com/bits-and-blooms/bitset v1.13.0 h1:bAQ9OPNFYbGHV6Nez0tmNI0RiEu7/hxlYJRUA0wFAVE= +github.com/bits-and-blooms/bitset v1.13.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cheggaaa/pb/v3 v3.1.5 h1:QuuUzeM2WsAqG2gMqtzaWithDJv0i+i6UlnwSCI4QLk= github.com/cheggaaa/pb/v3 v3.1.5/go.mod h1:CrxkeghYTXi1lQBEI7jSn+3svI3cuc19haAj6jM60XI= -github.com/cilium/ebpf v0.12.3 h1:8ht6F9MquybnY97at+VDZb3eQQr8ev79RueWeVaEcG4= -github.com/cilium/ebpf v0.12.3/go.mod h1:TctK1ivibvI3znr66ljgi4hqOT8EYQjz1KWBfb1UVgM= +github.com/cilium/ebpf v0.15.0 h1:7NxJhNiBT3NG8pZJ3c+yfrVdHY8ScgKD27sScgjLMMk= +github.com/cilium/ebpf v0.15.0/go.mod h1:DHp1WyrLeiBh19Cf/tfiSMhqheEiK8fXFZ4No0P1Hso= 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= -github.com/coreos/go-iptables v0.7.0 h1:XWM3V+MPRr5/q51NuWSgU0fqMad64Zyxs8ZUoMsamr8= -github.com/coreos/go-iptables v0.7.0/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFETJConOQ//Q= +github.com/coder/websocket v1.8.12 h1:5bUXkEPPIbewrnkU8LTCLVaxi4N4J8ahufH2vlo4NAo= +github.com/coder/websocket v1.8.12/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs= +github.com/coreos/go-iptables v0.7.1-0.20240112124308-65c67c9f46e6 h1:8h5+bWd7R6AYUslN6c6iuZWTKsKxUFDlpnmilO6R2n0= +github.com/coreos/go-iptables v0.7.1-0.20240112124308-65c67c9f46e6/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFETJConOQ//Q= github.com/coreos/go-oidc/v3 v3.11.0 h1:Ia3MxdwpSw702YW0xgfmP1GVCMA9aEFWu12XUZ3/OtI= github.com/coreos/go-oidc/v3 v3.11.0/go.mod h1:gE3LgjOgFoHi9a4ce4/tJczr0Ai2/BoDhf0r5lltWI0= -github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= -github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= -github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= -github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= +github.com/creack/pty v1.1.23 h1:4M6+isWdcStXEf15G/RbrMPOQj1dZ7HPZCGwE4kOeP0= +github.com/creack/pty v1.1.23/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= @@ -80,6 +82,10 @@ github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 h1:rpfIENRNNilwHwZeG5+P150SMrnN github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= github.com/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e h1:vUmf0yezR0y7jJ5pceLHthLaYf4bA5T14B6q39S4q2Q= github.com/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e/go.mod h1:YTIHhz/QFSYnu/EhlF2SpU2Uk+32abacUYA5ZPljz1A= +github.com/djherbis/times v1.6.0 h1:w2ctJ92J8fBvWPxugmXIv7Nz7Q3iDMKNx9v5ocVH20c= +github.com/djherbis/times v1.6.0/go.mod h1:gOHeRAz2h+VJNZ5Gmc/o7iD9k4wW7NMVqieYCY99oc0= +github.com/dsnet/try v0.0.3 h1:ptR59SsrcFUYbT/FhAbKTV6iLkeD6O18qfIWRml2fqI= +github.com/dsnet/try v0.0.3/go.mod h1:WBM8tRpUmnXXhY1U6/S8dt6UWdHTQ7y8A5YSkRCkq40= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= @@ -100,6 +106,8 @@ github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= +github.com/gaissmai/bart v0.11.1 h1:5Uv5XwsaFBRo4E5VBcb9TzY8B7zxFf+U7isDxqOrRfc= +github.com/gaissmai/bart v0.11.1/go.mod h1:KHeYECXQiBjTzQz/om2tqn3sZF1J7hw9m6z41ftj3fg= github.com/gardener/gardener v1.91.0 h1:m+0jtmS1ANN+jm+Y+IyhW6ofZbPtRL18gZ+lgAA8mQw= github.com/gardener/gardener v1.91.0/go.mod h1:3h8gSsr05ABuLGnGLB4bEYRn8ot42APkIa2E3f+nGc0= github.com/gardener/machine-controller-manager v0.53.1 h1:4P9qtzoD+989Lhc8XaI6Zo3X2TaQVXgHHrbEpuhJcrI= @@ -109,6 +117,8 @@ github.com/github/fakeca v0.1.0 h1:Km/MVOFvclqxPM9dZBC4+QE564nU4gz4iZ0D9pMw28I= github.com/github/fakeca v0.1.0/go.mod h1:+bormgoGMMuamOscx7N91aOuUST7wdaJ2rNjeohylyo= github.com/go-jose/go-jose/v4 v4.0.4 h1:VsjPI33J0SB9vQM6PLmNjoHqMQNGPiZ0rHL7Ni7Q6/E= github.com/go-jose/go-jose/v4 v4.0.4/go.mod h1:NKb5HO1EZccyMpiZNbdUw/14tiXNyUJh188dfnMCAfc= +github.com/go-json-experiment/json v0.0.0-20231102232822-2e55bd4e08b0 h1:ymLjT4f35nQbASLnvxEde4XOBL+Sn7rFuV+FOJqkljg= +github.com/go-json-experiment/json v0.0.0-20231102232822-2e55bd4e08b0/go.mod h1:6daplAwHHGbUGib4990V3Il26O0OC4aRyvewaaAihaA= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= @@ -144,15 +154,12 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= github.com/go-playground/validator/v10 v10.22.1 h1:40JcKH+bBNGFczGuoBYgX4I6m/i27HYW8P9FDk5PbgA= github.com/go-playground/validator/v10 v10.22.1/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= -github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= -github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA= github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/goccy/go-yaml v1.12.0 h1:/1WHjnMsI1dlIBQutrvSMGZRQufVO3asrHfTwfACoPM= github.com/goccy/go-yaml v1.12.0/go.mod h1:wKnAMd44+9JAAnGQpWVEgBzGt3YuTaQ4uXoHvE4m7WU= -github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/godbus/dbus/v5 v5.1.1-0.20230522191255-76236955d466 h1:sQspH8M4niEijh3PFscJRLDnkL547IeP7kpPe3uUhEg= github.com/godbus/dbus/v5 v5.1.1-0.20230522191255-76236955d466/go.mod h1:ZiQxhyQ+bbbfxUKVvjfO498oPYvtYhZzycal3G/NHmU= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= @@ -183,10 +190,10 @@ github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17 github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/nftables v0.2.0 h1:PbJwaBmbVLzpeldoeUKGkE2RjstrjPKMl6oLrfEJ6/8= -github.com/google/nftables v0.2.0/go.mod h1:Beg6V6zZ3oEn0JuiUQ4wqwuyqqzasOltcoXPtgLbFp4= -github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 h1:K6RDEckDVWvDI9JAJYCmNdQXq6neHJOYx3V6jnqNEec= -github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/nftables v0.2.1-0.20240414091927-5e242ec57806 h1:wG8RYIyctLhdFk6Vl1yPGtSRtwGpVkWyZww1OCil2MI= +github.com/google/nftables v0.2.1-0.20240414091927-5e242ec57806/go.mod h1:Beg6V6zZ3oEn0JuiUQ4wqwuyqqzasOltcoXPtgLbFp4= +github.com/google/pprof v0.0.0-20240525223248-4bfdf5a9a2af h1:kmjWCqn2qkEml422C2Rrd27c3VGxi6a/6HNq8QmHRKM= +github.com/google/pprof v0.0.0-20240525223248-4bfdf5a9a2af/go.mod h1:K1liHPHnj73Fdn/EKuT8nrFqBihUSKXoLYU0BuatOYo= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= @@ -214,6 +221,8 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/insomniacslk/dhcp v0.0.0-20240204152450-ca2dc33955c1 h1:L3pm9Kf2G6gJVYawz2SrI5QnV1wzHYbqmKnSHHXJAb8= github.com/insomniacslk/dhcp v0.0.0-20240204152450-ca2dc33955c1/go.mod h1:izxuNQZeFrbx2nK2fAyN5iNUB34Fe9j0nK4PwLzAkKw= +github.com/jellydator/ttlcache/v3 v3.1.0 h1:0gPFG0IHHP6xyUyXq+JaD8fwkDCqgqwohXNJBcYE71g= +github.com/jellydator/ttlcache/v3 v3.1.0/go.mod h1:hi7MGFdMAwZna5n2tuvh63DvFLzVKySzCVW6+0gA2n4= github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= @@ -279,8 +288,8 @@ github.com/metal-stack/duros-go v0.5.1 h1:baE/c0AKy9sTOztPhILJLaoMmT17Ajsb+xRMfH github.com/metal-stack/duros-go v0.5.1/go.mod h1:Z9mzI9ds2gI8zHC03PUCQvmlWa7WAPukDCUhowtVeOk= github.com/metal-stack/metal-go v0.34.0 h1:X4Wlt2OAhsu3Lq+rHSWnWeASmX6CYvOxnL6DxmjnzbU= github.com/metal-stack/metal-go v0.34.0/go.mod h1:3MJTYCS4YJz8D8oteTKhjpaAKNMMjMKYDrIy9awHGtQ= -github.com/metal-stack/metal-lib v0.18.2 h1:EAmZkZeKpenAvxZRSKsA6gj9Jd8XLR6Z0/QhABFCCDE= -github.com/metal-stack/metal-lib v0.18.2/go.mod h1:GJjipRpHmpd2vjBtsaw9gGk5ZFan7NlShyjIsTdY1x4= +github.com/metal-stack/metal-lib v0.18.3 h1:bovFiJPB9SMvuGLqcXVWz6jFB8HrdzwnCX7TFlen4r0= +github.com/metal-stack/metal-lib v0.18.3/go.mod h1:Ctyi6zaXFr2NVrQZLFsDLnFCzupKnYErTtgRFKAsnbw= github.com/metal-stack/security v0.8.1 h1:4zmVUxZvDWShVvVIxM3XhIv7pTmPe9DvACRIHW6YTsk= github.com/metal-stack/security v0.8.1/go.mod h1:OO8ZilZO6fUV5QEmwc7HP/RAjqYrGQxXoYIddJ9TvqE= github.com/metal-stack/updater v1.2.2 h1:gnUrnQgfT20QFMDtFBY89opKoBAkdeI/8T2iwMHNdxs= @@ -310,8 +319,8 @@ github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= -github.com/onsi/ginkgo/v2 v2.17.0 h1:kdnunFXpBjbzN56hcJHrXZ8M+LOkenKA7NnBzTNigTI= -github.com/onsi/ginkgo/v2 v2.17.0/go.mod h1:llBI3WDLL9Z6taip6f33H76YcWtJv+7R3HigUjbIBOs= +github.com/onsi/ginkgo/v2 v2.19.0 h1:9Cnnf7UHo57Hy3k6/m5k3dRfGTMXGvxhHFvkDTCTpvA= +github.com/onsi/ginkgo/v2 v2.19.0/go.mod h1:rlwLi9PilAFJ8jCg9UE1QP6VBpd6/xj3SRC0d6TU0To= github.com/onsi/gomega v1.32.0 h1:JRYU78fJ1LPxlckP6Txi/EYqJvjtMrDC04/MM5XRHPk= github.com/onsi/gomega v1.32.0/go.mod h1:a4x4gW6Pz2yK1MAmvluYme5lvYTn61afQ2ETw/8n4Lg= github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs= @@ -325,13 +334,15 @@ github.com/pkg/sftp v1.13.6/go.mod h1:tz1ryNURKu77RL+GuCzmoJYxQczL3wLNNpPWagdg4Q github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus-community/pro-bing v0.4.0 h1:YMbv+i08gQz97OZZBwLyvmmQEEzyfyrrjEaAchdy3R4= +github.com/prometheus-community/pro-bing v0.4.0/go.mod h1:b7wRYZtCcPmt4Sz319BykUU241rWLe1VFXyiyWK/dH4= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= -github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= -github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/safchain/ethtool v0.3.0 h1:gimQJpsI6sc1yIqP/y8GYgiXn/NjgvpM0RNoWLVVmP0= github.com/safchain/ethtool v0.3.0/go.mod h1:SA9BwrgyAqNo7M+uaL6IYbxpm5wk3L7Mm6ocLW+CJUs= @@ -367,30 +378,38 @@ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsT github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= -github.com/tailscale/certstore v0.1.1-0.20231020161753-77811a65f4ff h1:vnxdYZUJbsSRcIcduDW3DcQqfqaiv4FUgy25q8X+vfI= -github.com/tailscale/certstore v0.1.1-0.20231020161753-77811a65f4ff/go.mod h1:XrBNfAFN+pwoWuksbFS9Ccxnopa15zJGgXRFN90l3K4= +github.com/tailscale/certstore v0.1.1-0.20231202035212-d3fa0460f47e h1:PtWT87weP5LWHEY//SWsYkSO3RWRZo4OSWagh3YD2vQ= +github.com/tailscale/certstore v0.1.1-0.20231202035212-d3fa0460f47e/go.mod h1:XrBNfAFN+pwoWuksbFS9Ccxnopa15zJGgXRFN90l3K4= github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55 h1:Gzfnfk2TWrk8Jj4P4c1a3CtQyMaTVCznlkLZI++hok4= github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55/go.mod h1:4k4QO+dQ3R5FofL+SanAUZe+/QfeK0+OIuwDIRu2vSg= -github.com/tailscale/golang-x-crypto v0.0.0-20240108194725-7ce1f622c780 h1:U0J2CUrrTcc2wmr9tSLYEo+USfwNikRRsmxVLD4eZ7E= -github.com/tailscale/golang-x-crypto v0.0.0-20240108194725-7ce1f622c780/go.mod h1:ikbF+YT089eInTp9f2vmvy4+ZVnW5hzX1q2WknxSprQ= +github.com/tailscale/golang-x-crypto v0.0.0-20240604161659-3fde5e568aa4 h1:rXZGgEa+k2vJM8xT0PoSKfVXwFGPQ3z3CJfmnHJkZZw= +github.com/tailscale/golang-x-crypto v0.0.0-20240604161659-3fde5e568aa4/go.mod h1:ikbF+YT089eInTp9f2vmvy4+ZVnW5hzX1q2WknxSprQ= github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05 h1:4chzWmimtJPxRs2O36yuGRW3f9SYV+bMTTvMBI0EKio= github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05/go.mod h1:PdCqy9JzfWMJf1H5UJW2ip33/d4YkoKN0r67yKH1mG8= github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a h1:SJy1Pu0eH1C29XwJucQo73FrleVK6t4kYz4NVhp34Yw= github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a/go.mod h1:DFSS3NAGHthKo1gTlmEcSBiZrRJXi28rLNd/1udP1c8= github.com/tailscale/netlink v1.1.1-0.20211101221916-cabfb018fe85 h1:zrsUcqrG2uQSPhaUPjUQwozcRdDdSxxqhNgNZ3drZFk= github.com/tailscale/netlink v1.1.1-0.20211101221916-cabfb018fe85/go.mod h1:NzVQi3Mleb+qzq8VmcWpSkcSYxXIg0DkI6XDzpVkhJ0= -github.com/tailscale/web-client-prebuilt v0.0.0-20240208184856-443a64766f61 h1:G6/VUGQkHbBffO0s3f51DThcHCWrShlWklcS4Zxh5BU= -github.com/tailscale/web-client-prebuilt v0.0.0-20240208184856-443a64766f61/go.mod h1:agQPE6y6ldqCOui2gkIh7ZMztTkIQKH049tv8siLuNQ= -github.com/tailscale/wireguard-go v0.0.0-20231101022006-db7604d1aa90 h1:lMGYrokOq9NKDw1UMBH7AsS4boZ41jcduvYaRIdedhE= -github.com/tailscale/wireguard-go v0.0.0-20231101022006-db7604d1aa90/go.mod h1:BOm5fXUBFM+m9woLNBoxI9TaBXXhGNP50LX/TGIvGb4= +github.com/tailscale/peercred v0.0.0-20240214030740-b535050b2aa4 h1:Gz0rz40FvFVLTBk/K8UNAenb36EbDSnh+q7Z9ldcC8w= +github.com/tailscale/peercred v0.0.0-20240214030740-b535050b2aa4/go.mod h1:phI29ccmHQBc+wvroosENp1IF9195449VDnFDhJ4rJU= +github.com/tailscale/web-client-prebuilt v0.0.0-20240226180453-5db17b287bf1 h1:tdUdyPqJ0C97SJfjB9tW6EylTtreyee9C44de+UBG0g= +github.com/tailscale/web-client-prebuilt v0.0.0-20240226180453-5db17b287bf1/go.mod h1:agQPE6y6ldqCOui2gkIh7ZMztTkIQKH049tv8siLuNQ= +github.com/tailscale/wf v0.0.0-20240214030419-6fbb0a674ee6 h1:l10Gi6w9jxvinoiq15g8OToDdASBni4CyJOdHY1Hr8M= +github.com/tailscale/wf v0.0.0-20240214030419-6fbb0a674ee6/go.mod h1:ZXRML051h7o4OcI0d3AaILDIad/Xw0IkXaHM17dic1Y= +github.com/tailscale/wireguard-go v0.0.0-20240731203015-71393c576b98 h1:RNpJrXfI5u6e+uzyIzvmnXbhmhdRkVf//90sMBH3lso= +github.com/tailscale/wireguard-go v0.0.0-20240731203015-71393c576b98/go.mod h1:BOm5fXUBFM+m9woLNBoxI9TaBXXhGNP50LX/TGIvGb4= +github.com/tailscale/xnet v0.0.0-20240729143630-8497ac4dab2e h1:zOGKqN5D5hHhiYUp091JqK7DPCqSARyUfduhGUY8Bek= +github.com/tailscale/xnet v0.0.0-20240729143630-8497ac4dab2e/go.mod h1:orPd6JZXXRyuDusYilywte7k094d7dycXXU5YnWsrwg= github.com/tc-hib/winres v0.2.1 h1:YDE0FiP0VmtRaDn7+aaChp1KiF4owBiJa5l964l5ujA= github.com/tc-hib/winres v0.2.1/go.mod h1:C/JaNhH3KBvhNKVbvdlDWkbMDO9H4fKKDaN7/07SSuk= github.com/tcnksm/go-httpstat v0.2.0 h1:rP7T5e5U2HfmOBmZzGgGZjBQ5/GluWUylujl0tJ04I0= github.com/tcnksm/go-httpstat v0.2.0/go.mod h1:s3JVJFtQxtBEBC9dwcdTTXS9xFnM3SXAZwPG41aurT8= -github.com/u-root/u-root v0.11.0 h1:6gCZLOeRyevw7gbTwMj3fKxnr9+yHFlgF3N7udUVNO8= -github.com/u-root/u-root v0.11.0/go.mod h1:DBkDtiZyONk9hzVEdB/PWI9B4TxDkElWlVTHseglrZY= +github.com/u-root/u-root v0.12.0 h1:K0AuBFriwr0w/PGS3HawiAw89e3+MU7ks80GpghAsNs= +github.com/u-root/u-root v0.12.0/go.mod h1:FYjTOh4IkIZHhjsd17lb8nYW6udgXdJhG1c0r6u0arI= github.com/u-root/uio v0.0.0-20240209044354-b3d14b93376a h1:BH1SOPEvehD2kVrndDnGJiUF0TrBpNs+iyYocu6h0og= github.com/u-root/uio v0.0.0-20240209044354-b3d14b93376a/go.mod h1:P3a5rG4X7tI17Nn3aOIAYr5HbIMukwXG0urG0WuL8OA= +github.com/undefinedlabs/go-mpatch v1.0.7 h1:943FMskd9oqfbZV0qRVKOUsXQhTLXL0bQTVbQSpzmBs= +github.com/undefinedlabs/go-mpatch v1.0.7/go.mod h1:TyJZDQ/5AgyN7FSLiBJ8RO9u2c6wbtRvK827b6AVqY4= github.com/vishvananda/netlink v1.2.1-beta.2 h1:Llsql0lnQEbHj0I1OuKyp8otXp0r3q0mPkuhwHfStVs= github.com/vishvananda/netlink v1.2.1-beta.2/go.mod h1:twkDnbuQxJYemMlGd4JFIcuhgX83tXhKS2B/PRMpOho= github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0= @@ -424,10 +443,10 @@ golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt7 golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 h1:e66Fs6Z+fZTbFBAxKfP3PALWBtpfqks2bwGcexMxgtk= golang.org/x/exp v0.0.0-20240909161429-701f63a606c0/go.mod h1:2TbTHSBQa924w8M6Xs1QcRcFwyucIwBGpK1p2f1YFFY= -golang.org/x/exp/typeparams v0.0.0-20230905200255-921286631fa9 h1:j3D9DvWRpUfIyFfDPws7LoIZ2MAI1OJHdQXtTnYtN+k= -golang.org/x/exp/typeparams v0.0.0-20230905200255-921286631fa9/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= -golang.org/x/image v0.12.0 h1:w13vZbU4o5rKOFFR8y7M+c4A5jXDC0uXTdHYRP8X2DQ= -golang.org/x/image v0.12.0/go.mod h1:Lu90jvHG7GfemOIcldsh9A2hS01ocl6oNO7ype5mEnk= +golang.org/x/exp/typeparams v0.0.0-20240119083558-1b970713d09a h1:8qmSSA8Gz/1kTrCe0nqR0R3Gb/NDhykzWw2q2mWZydM= +golang.org/x/exp/typeparams v0.0.0-20240119083558-1b970713d09a/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= +golang.org/x/image v0.18.0 h1:jGzIakQa/ZXI1I0Fxvaa9W7yP25TqT6cHIHn+6CqvSQ= +golang.org/x/image v0.18.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= @@ -467,7 +486,6 @@ golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210301091718-77cc2087c03b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220817070843-5a390386f1f2/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -535,35 +553,29 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gvisor.dev/gvisor v0.0.0-20230928000133-4fe30062272c h1:bYb98Ra11fJ8F2xFbZx0zg2VQ28lYqC1JxfaaF53xqY= -gvisor.dev/gvisor v0.0.0-20230928000133-4fe30062272c/go.mod h1:AVgIgHMwK63XvmAzWG9vLQ41YnVHN0du0tEC46fI7yY= +gvisor.dev/gvisor v0.0.0-20240722211153-64c016c92987 h1:TU8z2Lh3Bbq77w0t1eG8yRlLcNHzZu3x6mhoH2Mk0c8= +gvisor.dev/gvisor v0.0.0-20240722211153-64c016c92987/go.mod h1:sxc3Uvk/vHcd3tj7/DHVBoR5wvWT/MmRq2pj7HRJnwU= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.4.6 h1:oFEHCKeID7to/3autwsWfnuv69j3NsfcXbvJKuIcep8= honnef.co/go/tools v0.4.6/go.mod h1:+rnGS1THNh8zMwnd2oVOTL9QF6vmfyG6ZXBULae2uc0= howett.net/plist v1.0.0 h1:7CrbWYbPPO/PyNy38b2EB/+gYbjCe2DXBxgtOOZbSQM= howett.net/plist v1.0.0/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g= -inet.af/peercred v0.0.0-20210906144145-0893ea02156a h1:qdkS8Q5/i10xU2ArJMKYhVa1DORzBfYS/qA2UK2jheg= -inet.af/peercred v0.0.0-20210906144145-0893ea02156a/go.mod h1:FjawnflS/udxX+SvpsMgZfdqx2aykOlkISeAsADi5IU= -inet.af/wf v0.0.0-20221017222439-36129f591884 h1:zg9snq3Cpy50lWuVqDYM7AIRVTtU50y5WXETMFohW/Q= -inet.af/wf v0.0.0-20221017222439-36129f591884/go.mod h1:bSAQ38BYbY68uwpasXOTZo22dKGy9SNvI6PZFeKomZE= -k8s.io/api v0.29.8 h1:ZBKg9clWnIGtQ5yGhNwMw2zyyrsIAQaXhZACcYNflQE= -k8s.io/api v0.29.8/go.mod h1:XlGIpmpzKGrtVca7GlgNryZJ19SvQdI808NN7fy1SgQ= -k8s.io/apimachinery v0.29.8 h1:uBHc9WuKiTHClIspJqtR84WNpG0aOGn45HWqxgXkk8Y= -k8s.io/apimachinery v0.29.8/go.mod h1:i3FJVwhvSp/6n8Fl4K97PJEP8C+MM+aoDq4+ZJBf70Y= -k8s.io/klog/v2 v2.120.1 h1:QXU6cPEOIslTGvZaXvFWiP9VKyeet3sawzTOvdXb4Vw= -k8s.io/klog/v2 v2.120.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= +k8s.io/api v0.30.3 h1:ImHwK9DCsPA9uoU3rVh4QHAHHK5dTSv1nxJUapx8hoQ= +k8s.io/api v0.30.3/go.mod h1:GPc8jlzoe5JG3pb0KJCSLX5oAFIW3/qNJITlDj8BH04= +k8s.io/apimachinery v0.31.0 h1:m9jOiSr3FoSSL5WO9bjm1n6B9KROYYgNZOb4tyZ1lBc= +k8s.io/apimachinery v0.31.0/go.mod h1:rsPdaZJfTfLsNJSQzNHQvYoTmxhoOEofxtOsF3rtsMo= +k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= +k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= k8s.io/utils v0.0.0-20240902221715-702e33fdd3c3 h1:b2FmK8YH+QEwq/Sy2uAEhmqL5nPfGYbJOcaqjeYYZoA= k8s.io/utils v0.0.0-20240902221715-702e33fdd3c3/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= -nhooyr.io/websocket v1.8.10 h1:mv4p+MnGrLDcPlBoWsvPP7XCzTYMXP9F9eIGoKbgx7Q= -nhooyr.io/websocket v1.8.10/go.mod h1:rN9OFWIUwuxg4fR5tELlYC04bXYowCP9GX47ivo2l+c= sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4= sigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08= sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= -software.sslmate.com/src/go-pkcs12 v0.2.1 h1:tbT1jjaeFOF230tzOIRJ6U5S1jNqpsSyNjzDd58H3J8= -software.sslmate.com/src/go-pkcs12 v0.2.1/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI= -tailscale.com v1.54.0 h1:Dri5BTKkHYpl+/t8ofY+tyvoTDbH/FpP7iB4B0cAQOY= -tailscale.com v1.54.0/go.mod h1:MnLFoCRwzFWr3qtkSW2nZdQpK7wQRZEk1KtcEGAuZYw= +software.sslmate.com/src/go-pkcs12 v0.4.0 h1:H2g08FrTvSFKUj+D309j1DPfk5APnIdAQAB8aEykJ5k= +software.sslmate.com/src/go-pkcs12 v0.4.0/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI= +tailscale.com v1.72.1 h1:hk82jek36ph2S3Tfsh57NVWKEm/pZ9nfUonvlowpfaA= +tailscale.com v1.72.1/go.mod h1:v7OHtg0KLAnhOVf81Z8WrjNefj238QbFhgkWJQoKxbs= diff --git a/pkg/api/version.go b/pkg/api/version.go index fd8131c6..e7f9cdc2 100644 --- a/pkg/api/version.go +++ b/pkg/api/version.go @@ -5,6 +5,6 @@ import ( ) type Version struct { - Client string `yaml:"client"` - Server *cloudmodels.RestVersion `yaml:"server,omitempty"` + Client string `json:"client" yaml:"client"` + Server *cloudmodels.RestVersion `json:"server,omitempty" yaml:"server,omitempty"` }