diff --git a/cmd/admin/v2/image.go b/cmd/admin/v2/image.go index a554238..4aaf53d 100644 --- a/cmd/admin/v2/image.go +++ b/cmd/admin/v2/image.go @@ -48,6 +48,7 @@ func newImageCmd(c *config.Config) *cobra.Command { cmd.Flags().String("expires-in", "", "expires-in duration") cmd.Flags().String("description", "", "image description") cmd.Flags().StringSlice("features", nil, "image features can be machine and/or firewall") + cmd.Flags().StringSlice("labels", nil, "labels to add to the image") }, CreateRequestFromCLI: w.createFromCLI, UpdateCmdMutateFn: func(cmd *cobra.Command) { @@ -97,6 +98,17 @@ func (c *image) createFromCLI() (*adminv2.ImageServiceCreateRequest, error) { expiresAt = timestamppb.New(time.Now().Add(viper.GetDuration("expires-in"))) } + var labels *apiv2.Labels = nil + + labelSlice := viper.GetStringSlice("labels") + if len(labelSlice) > 0 { + labelsMap, err := genericcli.LabelsToMap(labelSlice) + if err != nil { + return nil, err + } + labels = &apiv2.Labels{Labels: labelsMap} + } + return &adminv2.ImageServiceCreateRequest{ Image: &apiv2.Image{ Id: viper.GetString("id"), @@ -106,8 +118,8 @@ func (c *image) createFromCLI() (*adminv2.ImageServiceCreateRequest, error) { Description: pointer.PointerOrNil(viper.GetString("description")), ExpiresAt: expiresAt, Features: imageFeaturesFromString(viper.GetStringSlice("features")), - Meta: &apiv2.Meta{ - // TODO labels + Meta: &apiv2.Meta{ + Labels: labels, }, }, }, nil @@ -152,6 +164,7 @@ func (c *image) Convert(r *apiv2.Image) (string, *adminv2.ImageServiceCreateRequ LockingStrategy: apiv2.OptimisticLockingStrategy_OPTIMISTIC_LOCKING_STRATEGY_CLIENT, UpdatedAt: r.Meta.UpdatedAt, }, + // FIXME: Labels cannot be updated? Classification: r.Classification, ExpiresAt: r.ExpiresAt, }, nil diff --git a/cmd/admin/v2/size.go b/cmd/admin/v2/size.go index b51a91d..634fe08 100644 --- a/cmd/admin/v2/size.go +++ b/cmd/admin/v2/size.go @@ -138,6 +138,12 @@ func (c *size) Convert(r *apiv2.Size) (string, *adminv2.SizeServiceCreateRequest LockingStrategy: apiv2.OptimisticLockingStrategy_OPTIMISTIC_LOCKING_STRATEGY_CLIENT, UpdatedAt: r.Meta.UpdatedAt, }, - Labels: &apiv2.UpdateLabels{}, + Labels: &apiv2.UpdateLabels{ + Strategy: &apiv2.UpdateLabels_Replace{ + Replace: &apiv2.Labels{ + Labels: pointer.SafeDeref(pointer.SafeDeref(r.Meta).Labels).Labels, + }, + }, + }, }, nil } diff --git a/cmd/api/v2/ip.go b/cmd/api/v2/ip.go index bcc8e40..0f89a05 100644 --- a/cmd/api/v2/ip.go +++ b/cmd/api/v2/ip.go @@ -40,7 +40,7 @@ func newIPCmd(c *config.Config) *cobra.Command { cmd.Flags().StringP("network", "n", "", "network from which the ip should get created") cmd.Flags().StringP("name", "", "", "name of the ip") cmd.Flags().StringP("description", "", "", "description of the ip") - cmd.Flags().StringSliceP("labels", "", nil, "labels to add to the ip") + cmd.Flags().StringSlice("labels", nil, "labels to add to the ip") cmd.Flags().BoolP("static", "", false, "make this ip static") cmd.Flags().StringP("addressfamily", "", "", "addressfamily, can be either IPv4|IPv6, defaults to IPv4 (optional)") @@ -85,6 +85,7 @@ func (c *ip) createFromCLI() (*apiv2.IPServiceCreateRequest, error) { } labels = &apiv2.Labels{Labels: labelsMap} } + return &apiv2.IPServiceCreateRequest{ Project: c.c.GetProject(), Network: viper.GetString("network"), @@ -120,10 +121,10 @@ func (c *ip) updateFromCLI(args []string) (*apiv2.IPServiceUpdateRequest, error) req.Type = pointer.PointerOrNil(ipStaticToType(viper.GetBool("static"))) } if viper.IsSet("remove-labels") || viper.IsSet("labels") { - labelsUpdate := &apiv2.UpdateLabels{} + updates := &apiv2.UpdateLabelsIndividually{} if viper.IsSet("remove-labels") { - labelsUpdate.Remove = viper.GetStringSlice("remove-labels") + updates.Remove = viper.GetStringSlice("remove-labels") } if viper.IsSet("labels") { @@ -131,9 +132,14 @@ func (c *ip) updateFromCLI(args []string) (*apiv2.IPServiceUpdateRequest, error) if err != nil { return nil, err } - labelsUpdate.Update = &apiv2.Labels{Labels: labels} + updates.Update = &apiv2.Labels{Labels: labels} + } + + req.Labels = &apiv2.UpdateLabels{ + Strategy: &apiv2.UpdateLabels_Individual{ + Individual: updates, + }, } - req.Labels = labelsUpdate } return req, nil @@ -226,60 +232,34 @@ func (c *ip) Convert(r *apiv2.IP) (string, *apiv2.IPServiceCreateRequest, *apiv2 return helpers.EncodeProject(r.Ip, r.Project), IpResponseToCreate(r), responseToUpdate, err } -func IpResponseToCreate(r *apiv2.IP) *apiv2.IPServiceCreateRequest { +func IpResponseToCreate(ip *apiv2.IP) *apiv2.IPServiceCreateRequest { return &apiv2.IPServiceCreateRequest{ - Ip: &r.Ip, - Project: r.Project, - Network: r.Network, - Name: &r.Name, - Description: &r.Description, - Labels: r.Meta.Labels, - Type: &r.Type, + Ip: &ip.Ip, + Project: ip.Project, + Network: ip.Network, + Name: &ip.Name, + Description: &ip.Description, + Labels: pointer.SafeDeref(ip.Meta).Labels, + Type: &ip.Type, } } -func (c *ip) IpResponseToUpdate(r *apiv2.IP) (*apiv2.IPServiceUpdateRequest, error) { - ctx, cancel := c.c.NewRequestContext() - defer cancel() - - current, err := c.c.Client.Apiv2().IP().Get(ctx, &apiv2.IPServiceGetRequest{ - Ip: r.Ip, - Project: r.Project, - }) - if err != nil { - return nil, err - } - - updateLabels := &apiv2.UpdateLabels{ - Remove: []string{}, - Update: &apiv2.Labels{}, - } - - for key, currentValue := range current.Ip.Meta.Labels.Labels { - value, ok := r.Meta.Labels.Labels[key] - - if !ok { - updateLabels.Remove = append(updateLabels.Remove, key) - continue - } - - if currentValue != value { - if updateLabels.Update.Labels == nil { - updateLabels.Update.Labels = map[string]string{} - } - updateLabels.Update.Labels[key] = value - } - } - +func (c *ip) IpResponseToUpdate(ip *apiv2.IP) (*apiv2.IPServiceUpdateRequest, error) { return &apiv2.IPServiceUpdateRequest{ - Project: r.Project, - Ip: r.Ip, - Name: &r.Name, - Description: &r.Description, - Type: &r.Type, - Labels: updateLabels, + Project: ip.Project, + Ip: ip.Ip, + Name: &ip.Name, + Description: &ip.Description, + Type: &ip.Type, + Labels: &apiv2.UpdateLabels{ + Strategy: &apiv2.UpdateLabels_Replace{ + Replace: &apiv2.Labels{ + Labels: pointer.SafeDeref(pointer.SafeDeref(ip.Meta).Labels).Labels, + }, + }, + }, UpdateMeta: &apiv2.UpdateMeta{ - UpdatedAt: current.Ip.Meta.UpdatedAt, + UpdatedAt: pointer.SafeDeref(ip.Meta).UpdatedAt, LockingStrategy: apiv2.OptimisticLockingStrategy_OPTIMISTIC_LOCKING_STRATEGY_CLIENT, }, }, nil diff --git a/cmd/tableprinters/ip.go b/cmd/tableprinters/ip.go index d60a149..65d66e9 100644 --- a/cmd/tableprinters/ip.go +++ b/cmd/tableprinters/ip.go @@ -1,10 +1,10 @@ package tableprinters import ( - "fmt" "strings" apiv2 "github.com/metal-stack/api/go/metalstack/api/v2" + "github.com/metal-stack/metal-lib/pkg/genericcli" ) func (t *TablePrinter) IPTable(data []*apiv2.IP, wide bool) ([]string, [][]string, error) { @@ -36,9 +36,7 @@ func (t *TablePrinter) IPTable(data []*apiv2.IP, wide bool) ([]string, [][]strin var labels []string if ip.Meta != nil && ip.Meta.Labels != nil { - for k, v := range ip.Meta.Labels.Labels { - labels = append(labels, fmt.Sprintf("%s=%s", k, v)) - } + labels = genericcli.MapToLabels(ip.Meta.Labels.Labels) } if wide { diff --git a/go.mod b/go.mod index 8a2f860..229d67d 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,7 @@ require ( github.com/fatih/color v1.19.0 github.com/google/go-cmp v0.7.0 github.com/google/uuid v1.6.0 - github.com/metal-stack/api v0.2.0 + github.com/metal-stack/api v0.2.2 github.com/metal-stack/metal-lib v0.25.2 github.com/metal-stack/v v1.0.3 github.com/spf13/afero v1.15.0 diff --git a/go.sum b/go.sum index 0d71dce..7a487ae 100644 --- a/go.sum +++ b/go.sum @@ -71,8 +71,8 @@ github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4= github.com/mattn/go-runewidth v0.0.24 h1:cpokDiIn0MGnhdHwuWnJBITySJ20QyNGnY2kR/ay2DU= github.com/mattn/go-runewidth v0.0.24/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= -github.com/metal-stack/api v0.2.0 h1:QfiQ4dWjd2XXKiClciHrTsDzZmspFYK4xuYA7t7X4TA= -github.com/metal-stack/api v0.2.0/go.mod h1:+WrGqA7QpQ2O60vakm3tjDbo70Q4c92pdco4R5MRcL8= +github.com/metal-stack/api v0.2.2 h1:z6IHXtvLFP5Fa2jYw4ajC0SZWD8nrhj/o8yTrTpjaaw= +github.com/metal-stack/api v0.2.2/go.mod h1:+WrGqA7QpQ2O60vakm3tjDbo70Q4c92pdco4R5MRcL8= github.com/metal-stack/metal-lib v0.25.2 h1:OW8y6PtlV5lv3bXSDU1MaNGvtbroiDYjuhSxqKHKAgw= github.com/metal-stack/metal-lib v0.25.2/go.mod h1:tnx4MM5oml10EMN6Nq6oFSbjOKB9B27WUZQUyZ4MywA= github.com/metal-stack/v v1.0.3 h1:Sh2oBlnxrCUD+mVpzfC8HiqL045YWkxs0gpTvkjppqs= diff --git a/tests/e2e/admin/size_test.go b/tests/e2e/admin/size_test.go index 3b26c51..de25e98 100644 --- a/tests/e2e/admin/size_test.go +++ b/tests/e2e/admin/size_test.go @@ -179,7 +179,11 @@ func Test_AdminSizeCmd_Update(t *testing.T) { UpdateMeta: &apiv2.UpdateMeta{ LockingStrategy: apiv2.OptimisticLockingStrategy_OPTIMISTIC_LOCKING_STRATEGY_CLIENT, }, - Labels: &apiv2.UpdateLabels{}, + Labels: &apiv2.UpdateLabels{ + Strategy: &apiv2.UpdateLabels_Replace{ + Replace: testresources.Size1().Meta.Labels, + }, + }, Constraints: testresources.Size1().Constraints, }, WantResponse: func() connect.AnyResponse { diff --git a/tests/e2e/api/ip_test.go b/tests/e2e/api/ip_test.go index be9fdcd..b3d869b 100644 --- a/tests/e2e/api/ip_test.go +++ b/tests/e2e/api/ip_test.go @@ -142,17 +142,6 @@ func Test_IPCmd_Create(t *testing.T) { require.NoError(t, fs.WriteFile(e2e.InputFilePath, e2e.MustMarshal(t, testresources.IP1()), 0755)) }, ClientCalls: []client.ClientCall{ - { - WantRequest: &apiv2.IPServiceGetRequest{ - Ip: testresources.IP1().Ip, - Project: testresources.IP1().Project, - }, - WantResponse: func() connect.AnyResponse { - return connect.NewResponse(&apiv2.IPServiceGetResponse{ - Ip: testresources.IP1(), - }) - }, - }, { WantRequest: &apiv2.IPServiceCreateRequest{ Ip: &testresources.IP1().Ip, @@ -214,17 +203,6 @@ func Test_IPCmd_Delete(t *testing.T) { require.NoError(t, fs.WriteFile(e2e.InputFilePath, e2e.MustMarshal(t, testresources.IP1()), 0755)) }, ClientCalls: []client.ClientCall{ - { - WantRequest: &apiv2.IPServiceGetRequest{ - Ip: testresources.IP1().Ip, - Project: testresources.IP1().Project, - }, - WantResponse: func() connect.AnyResponse { - return connect.NewResponse(&apiv2.IPServiceGetResponse{ - Ip: testresources.IP1(), - }) - }, - }, { WantRequest: &apiv2.IPServiceDeleteRequest{ Ip: testresources.IP1().Ip, @@ -278,7 +256,7 @@ func Test_IPCmd_Update(t *testing.T) { ), WantObject: testresources.IP1(), WantTable: new(` - IP PROJECT ID TYPE NAME ATTACHED SERVICE + IP PROJECT ID TYPE NAME ATTACHED SERVICE 1.1.1.1 ce19a655-7933-4745-8f3e-9592b4a90488 2e0144a2-09ef-42b7-b629-4263295db6e8 static a `), WantWideTable: new(` @@ -295,24 +273,17 @@ func Test_IPCmd_Update(t *testing.T) { require.NoError(t, fs.WriteFile(e2e.InputFilePath, e2e.MustMarshal(t, testresources.IP1()), 0755)) }, ClientCalls: []client.ClientCall{ - { - WantRequest: &apiv2.IPServiceGetRequest{ - Ip: testresources.IP1().Ip, - Project: testresources.IP1().Project, - }, - WantResponse: func() connect.AnyResponse { - return connect.NewResponse(&apiv2.IPServiceGetResponse{ - Ip: testresources.IP1(), - }) - }, - }, { WantRequest: &apiv2.IPServiceUpdateRequest{ Ip: testresources.IP1().Ip, Project: testresources.IP1().Project, Description: &testresources.IP1().Description, Labels: &apiv2.UpdateLabels{ - Update: &apiv2.Labels{}, + Strategy: &apiv2.UpdateLabels_Replace{ + Replace: &apiv2.Labels{ + Labels: testresources.IP1().Meta.Labels.Labels, + }, + }, }, Name: &testresources.IP1().Name, Type: &testresources.IP1().Type, @@ -351,17 +322,6 @@ func Test_IPCmd_Apply(t *testing.T) { require.NoError(t, fs.WriteFile(e2e.InputFilePath, e2e.MustMarshal(t, testresources.IP1()), 0755)) }, ClientCalls: []client.ClientCall{ - { - WantRequest: &apiv2.IPServiceGetRequest{ - Ip: testresources.IP1().Ip, - Project: testresources.IP1().Project, - }, - WantResponse: func() connect.AnyResponse { - return connect.NewResponse(&apiv2.IPServiceGetResponse{ - Ip: testresources.IP1(), - }) - }, - }, { WantRequest: &apiv2.IPServiceCreateRequest{ Ip: &testresources.IP1().Ip, @@ -396,17 +356,6 @@ func Test_IPCmd_Apply(t *testing.T) { require.NoError(t, fs.WriteFile(e2e.InputFilePath, e2e.MustMarshal(t, testresources.IP1()), 0755)) }, ClientCalls: []client.ClientCall{ - { - WantRequest: &apiv2.IPServiceGetRequest{ - Ip: testresources.IP1().Ip, - Project: testresources.IP1().Project, - }, - WantResponse: func() connect.AnyResponse { - return connect.NewResponse(&apiv2.IPServiceGetResponse{ - Ip: testresources.IP1(), - }) - }, - }, { WantRequest: &apiv2.IPServiceCreateRequest{ Ip: &testresources.IP1().Ip, @@ -420,25 +369,17 @@ func Test_IPCmd_Apply(t *testing.T) { }, WantError: connect.NewError(connect.CodeAlreadyExists, fmt.Errorf("already exists")), }, - { - WantRequest: &apiv2.IPServiceGetRequest{ - Ip: testresources.IP1().Ip, - Project: testresources.IP1().Project, - }, - WantResponse: func() connect.AnyResponse { - return connect.NewResponse(&apiv2.IPServiceGetResponse{ - Ip: testresources.IP1(), - }) - }, - }, { WantRequest: &apiv2.IPServiceUpdateRequest{ Ip: testresources.IP1().Ip, Project: testresources.IP1().Project, Description: &testresources.IP1().Description, Labels: &apiv2.UpdateLabels{ - Update: &apiv2.Labels{}, - }, + Strategy: &apiv2.UpdateLabels_Replace{ + Replace: &apiv2.Labels{ + Labels: testresources.IP1().Meta.Labels.Labels, + }, + }}, Name: &testresources.IP1().Name, Type: &testresources.IP1().Type, UpdateMeta: &apiv2.UpdateMeta{