diff --git a/cmd/plugin/rpaasv2/cmd/metadata.go b/cmd/plugin/rpaasv2/cmd/metadata.go index 0842e8a7..7ce6333e 100644 --- a/cmd/plugin/rpaasv2/cmd/metadata.go +++ b/cmd/plugin/rpaasv2/cmd/metadata.go @@ -128,7 +128,7 @@ func createMetadata(meta []string, metaType string, isSet bool) (*types.Metadata var item types.MetadataItem if isSet { if !strings.Contains(kv, "=") { - return nil, fmt.Errorf("invalid NAME=value pair: %v", kv) + return nil, fmt.Errorf("invalid NAME=value pair: %q", kv) } item.Name = strings.Split(kv, "=")[0] item.Value = strings.Split(kv, "=")[1] @@ -155,7 +155,7 @@ func runSetMetadata(c *cli.Context) error { } if !isValidMetadataType(metaType) { - return fmt.Errorf("invalid metadata type: %v", metaType) + return fmt.Errorf("invalid metadata type: %q", metaType) } metadata, err := createMetadata(keyValues, metaType, true) @@ -216,7 +216,7 @@ func runUnsetMetadata(c *cli.Context) error { } if !isValidMetadataType(metaType) { - return fmt.Errorf("invalid metadata type: %v", metaType) + return fmt.Errorf("invalid metadata type: %q", metaType) } metadata, _ := createMetadata(keys, metaType, false) diff --git a/cmd/plugin/rpaasv2/cmd/metadata_test.go b/cmd/plugin/rpaasv2/cmd/metadata_test.go index 51305542..7ca2135f 100644 --- a/cmd/plugin/rpaasv2/cmd/metadata_test.go +++ b/cmd/plugin/rpaasv2/cmd/metadata_test.go @@ -99,12 +99,12 @@ func TestSetMetadata(t *testing.T) { { name: "invalid metadata type", args: []string{"-i", "my-instance", "-t", "invalid", "key=value"}, - expectedErr: "invalid metadata type: invalid", + expectedErr: "invalid metadata type: \"invalid\"", }, { name: "invalid key value pair", args: []string{"-i", "my-instance", "-t", "annotation", "key"}, - expectedErr: "invalid NAME=value pair: key", + expectedErr: "invalid NAME=value pair: \"key\"", }, { name: "valid metadata", @@ -161,7 +161,7 @@ func TestUnsetMetadata(t *testing.T) { { name: "invalid metadata type", args: []string{"-i", "my-instance", "-t", "invalid", "key=value"}, - expectedErr: "invalid metadata type: invalid", + expectedErr: "invalid metadata type: \"invalid\"", }, { name: "valid metadata", diff --git a/internal/pkg/rpaas/k8s.go b/internal/pkg/rpaas/k8s.go index 15bcf7b5..37de1adf 100644 --- a/internal/pkg/rpaas/k8s.go +++ b/internal/pkg/rpaas/k8s.go @@ -19,7 +19,6 @@ import ( "net" "net/url" "regexp" - "slices" "sort" "strings" "text/template" @@ -1643,25 +1642,6 @@ func setLoadBalancerName(instance *v1alpha1.RpaasInstance, lbName string) { instance.Spec.Service.Annotations[lbNameLabelKey] = lbName } -func filterMetadata(meta map[string]string) map[string]string { - filterAnnotations := make(map[string]string) - for key, val := range meta { - if !strings.HasPrefix(key, defaultKeyLabelPrefix) { - filterAnnotations[key] = val - } - } - return filterAnnotations -} - -func flattenMetadata(meta map[string]string) []string { - var result []string - for k, v := range meta { - result = append(result, fmt.Sprintf("%s=%s", k, v)) - } - slices.Sort(result) - return result -} - func (m *k8sRpaasManager) GetInstanceInfo(ctx context.Context, instanceName string) (*clientTypes.InstanceInfo, error) { instance, err := m.GetInstance(ctx, instanceName) if err != nil { @@ -2468,95 +2448,3 @@ func contains(ss []string, s string) bool { return false } - -func (m *k8sRpaasManager) GetMetadata(ctx context.Context, instanceName string) (*clientTypes.Metadata, error) { - instance, err := m.GetInstance(ctx, instanceName) - if err != nil { - return nil, err - } - - filteredLabels := filterMetadata(instance.Labels) - filteredAnnotations := filterMetadata(instance.Annotations) - - metadata := &clientTypes.Metadata{} - - for k, v := range filteredLabels { - item := clientTypes.MetadataItem{Name: k, Value: v} - metadata.Labels = append(metadata.Labels, item) - } - - for k, v := range filteredAnnotations { - item := clientTypes.MetadataItem{Name: k, Value: v} - metadata.Annotations = append(metadata.Annotations, item) - } - - return metadata, nil -} - -func validateMetadata(items []clientTypes.MetadataItem) error { - for _, item := range items { - if strings.HasPrefix(item.Name, defaultKeyLabelPrefix) { - return &ValidationError{Msg: fmt.Sprintf("metadata key %q is reserved", item.Name)} - } - } - return nil -} - -func (m *k8sRpaasManager) SetMetadata(ctx context.Context, instanceName string, metadata *clientTypes.Metadata) error { - instance, err := m.GetInstance(ctx, instanceName) - if err != nil { - return err - } - - if err = validateMetadata(metadata.Labels); err != nil { - return err - } - - if err = validateMetadata(metadata.Annotations); err != nil { - return err - } - - originalInstance := instance.DeepCopy() - - if metadata.Labels != nil { - for _, item := range metadata.Labels { - instance.Labels[item.Name] = item.Value - } - } - - if metadata.Annotations != nil { - for _, item := range metadata.Annotations { - instance.Annotations[item.Name] = item.Value - } - } - - return m.patchInstance(ctx, originalInstance, instance) -} - -func (m *k8sRpaasManager) UnsetMetadata(ctx context.Context, instanceName string, metadata *clientTypes.Metadata) error { - instance, err := m.GetInstance(ctx, instanceName) - if err != nil { - return err - } - originalInstance := instance.DeepCopy() - - if metadata.Labels != nil { - for _, item := range metadata.Labels { - if _, ok := instance.Labels[item.Name]; !ok { - return &NotFoundError{Msg: fmt.Sprintf("label %q not found in instance %q", item.Name, instanceName)} - } - delete(instance.Labels, item.Name) - } - } - - if metadata.Annotations != nil { - for _, item := range metadata.Annotations { - if _, ok := instance.Annotations[item.Name]; !ok { - return &NotFoundError{Msg: fmt.Sprintf("annotation %q not found in instance %q", item.Name, instanceName)} - } - delete(instance.Annotations, item.Name) - } - } - - return m.patchInstance(ctx, originalInstance, instance) -} diff --git a/internal/pkg/rpaas/k8s_test.go b/internal/pkg/rpaas/k8s_test.go index b4d4d186..72ad2577 100644 --- a/internal/pkg/rpaas/k8s_test.go +++ b/internal/pkg/rpaas/k8s_test.go @@ -5187,39 +5187,3 @@ func Test_k8sRpaasManager_Debug(t *testing.T) { } } - -func Test_k8sRpaasManager_GetMetadata(t *testing.T) { - scheme := newScheme() - - instance := newEmptyRpaasInstance() - instance.ObjectMeta = metav1.ObjectMeta{ - Name: "my-instance", - Namespace: "rpaasv2", - Labels: map[string]string{ - "rpaas.extensions.tsuru.io/cluster-name": "my-cluster", - "rpaas.extensions.tsuru.io/instance-name": "my-instance", - "rpaas.extensions.tsuru.io/service-name": "my-service", - "rpaas.extensions.tsuru.io/team-owner": "my-team", - "rpaas_instance": "my-instance", - "rpaas_service": "my-service", - }, - Annotations: map[string]string{ - "rpaas.extensions.tsuru.io/cluster-name": "my-cluster", - "rpaas.extensions.tsuru.io/description": "my-description", - "rpaas.extensions.tsuru.io/tags": "my-tag=my-value", - "rpaas.extensions.tsuru.io/team-owner": "my-team", - "custom-annotation": "custom-value", - }, - } - - manager := &k8sRpaasManager{cli: fake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(instance).Build()} - meta, err := manager.GetMetadata(context.Background(), "my-instance") - require.NoError(t, err) - - assert.Equal(t, len(meta.Labels), 2) - assert.Contains(t, meta.Labels, clientTypes.MetadataItem{Name: "rpaas_instance", Value: "my-instance"}) - assert.Contains(t, meta.Labels, clientTypes.MetadataItem{Name: "rpaas_service", Value: "my-service"}) - - assert.Equal(t, len(meta.Annotations), 1) - assert.Contains(t, meta.Annotations, clientTypes.MetadataItem{Name: "custom-annotation", Value: "custom-value"}) -} diff --git a/internal/pkg/rpaas/metadata.go b/internal/pkg/rpaas/metadata.go new file mode 100644 index 00000000..af4d0900 --- /dev/null +++ b/internal/pkg/rpaas/metadata.go @@ -0,0 +1,131 @@ +// Copyright 2024 tsuru authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package rpaas + +import ( + "context" + "fmt" + "slices" + "strings" + + clientTypes "github.com/tsuru/rpaas-operator/pkg/rpaas/client/types" +) + +func filterMetadata(meta map[string]string) map[string]string { + filterAnnotations := make(map[string]string) + for key, val := range meta { + if !strings.HasPrefix(key, defaultKeyLabelPrefix) { + filterAnnotations[key] = val + } + } + return filterAnnotations +} + +func flattenMetadata(meta map[string]string) []string { + var result []string + for k, v := range meta { + result = append(result, fmt.Sprintf("%s=%s", k, v)) + } + slices.Sort(result) + return result +} + +func (m *k8sRpaasManager) GetMetadata(ctx context.Context, instanceName string) (*clientTypes.Metadata, error) { + instance, err := m.GetInstance(ctx, instanceName) + if err != nil { + return nil, err + } + + filteredLabels := filterMetadata(instance.Labels) + filteredAnnotations := filterMetadata(instance.Annotations) + + metadata := &clientTypes.Metadata{} + + for k, v := range filteredLabels { + item := clientTypes.MetadataItem{Name: k, Value: v} + metadata.Labels = append(metadata.Labels, item) + } + + for k, v := range filteredAnnotations { + item := clientTypes.MetadataItem{Name: k, Value: v} + metadata.Annotations = append(metadata.Annotations, item) + } + + return metadata, nil +} + +func validateMetadata(items []clientTypes.MetadataItem) error { + for _, item := range items { + if strings.HasPrefix(item.Name, defaultKeyLabelPrefix) { + return &ValidationError{Msg: fmt.Sprintf("metadata key %q is reserved", item.Name)} + } + } + return nil +} + +func (m *k8sRpaasManager) SetMetadata(ctx context.Context, instanceName string, metadata *clientTypes.Metadata) error { + instance, err := m.GetInstance(ctx, instanceName) + if err != nil { + return err + } + + if err = validateMetadata(metadata.Labels); err != nil { + return err + } + + if err = validateMetadata(metadata.Annotations); err != nil { + return err + } + + originalInstance := instance.DeepCopy() + + if metadata.Labels != nil { + if instance.Labels == nil { + instance.Labels = make(map[string]string) + } + for _, item := range metadata.Labels { + instance.Labels[item.Name] = item.Value + } + } + + if metadata.Annotations != nil { + if instance.Annotations == nil { + instance.Annotations = make(map[string]string) + } + for _, item := range metadata.Annotations { + instance.Annotations[item.Name] = item.Value + } + } + + return m.patchInstance(ctx, originalInstance, instance) +} + +func (m *k8sRpaasManager) UnsetMetadata(ctx context.Context, instanceName string, metadata *clientTypes.Metadata) error { + instance, err := m.GetInstance(ctx, instanceName) + if err != nil { + return err + } + originalInstance := instance.DeepCopy() + + if metadata.Labels != nil { + for _, item := range metadata.Labels { + if _, ok := instance.Labels[item.Name]; !ok { + return &NotFoundError{Msg: fmt.Sprintf("label %q not found in instance %q", item.Name, instanceName)} + } + delete(instance.Labels, item.Name) + } + } + + if metadata.Annotations != nil { + for _, item := range metadata.Annotations { + if _, ok := instance.Annotations[item.Name]; !ok { + return &NotFoundError{Msg: fmt.Sprintf("annotation %q not found in instance %q", item.Name, instanceName)} + } + delete(instance.Annotations, item.Name) + } + } + + return m.patchInstance(ctx, originalInstance, instance) +} diff --git a/internal/pkg/rpaas/metadata_test.go b/internal/pkg/rpaas/metadata_test.go new file mode 100644 index 00000000..62ca1d2a --- /dev/null +++ b/internal/pkg/rpaas/metadata_test.go @@ -0,0 +1,222 @@ +// Copyright 2024 tsuru authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package rpaas + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + + clientTypes "github.com/tsuru/rpaas-operator/pkg/rpaas/client/types" +) + +func Test_k8sRpaasManager_GetMetadata(t *testing.T) { + scheme := newScheme() + + instance := newEmptyRpaasInstance() + instance.ObjectMeta = metav1.ObjectMeta{ + Name: "my-instance", + Namespace: "rpaasv2", + Labels: map[string]string{ + "rpaas.extensions.tsuru.io/cluster-name": "my-cluster", + "rpaas.extensions.tsuru.io/instance-name": "my-instance", + "rpaas.extensions.tsuru.io/service-name": "my-service", + "rpaas.extensions.tsuru.io/team-owner": "my-team", + "rpaas_instance": "my-instance", + "rpaas_service": "my-service", + }, + Annotations: map[string]string{ + "rpaas.extensions.tsuru.io/cluster-name": "my-cluster", + "rpaas.extensions.tsuru.io/description": "my-description", + "rpaas.extensions.tsuru.io/tags": "my-tag=my-value", + "rpaas.extensions.tsuru.io/team-owner": "my-team", + "custom-annotation": "custom-value", + }, + } + + manager := &k8sRpaasManager{cli: fake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(instance).Build()} + meta, err := manager.GetMetadata(context.Background(), "my-instance") + require.NoError(t, err) + + assert.Equal(t, len(meta.Labels), 2) + assert.Contains(t, meta.Labels, clientTypes.MetadataItem{Name: "rpaas_instance", Value: "my-instance"}) + assert.Contains(t, meta.Labels, clientTypes.MetadataItem{Name: "rpaas_service", Value: "my-service"}) + + assert.Equal(t, len(meta.Annotations), 1) + assert.Contains(t, meta.Annotations, clientTypes.MetadataItem{Name: "custom-annotation", Value: "custom-value"}) +} + +func Test_k8sRpaasManager_SetMetadata(t *testing.T) { + scheme := newScheme() + testCases := []struct { + name string + meta *clientTypes.Metadata + expectedErr string + }{ + { + name: "set metadata", + meta: &clientTypes.Metadata{ + Labels: []clientTypes.MetadataItem{ + {Name: "rpaas_instance", Value: "my-instance"}, + {Name: "rpaas_service", Value: "my-service"}, + }, + Annotations: []clientTypes.MetadataItem{ + {Name: "custom-annotation", Value: "custom-value"}, + }, + }, + }, + { + name: "set reserved metadata for labels", + meta: &clientTypes.Metadata{ + Labels: []clientTypes.MetadataItem{ + {Name: "rpaas.extensions.tsuru.io/custom-key", Value: "custom-value"}, + }, + }, + expectedErr: "metadata key \"rpaas.extensions.tsuru.io/custom-key\" is reserved", + }, + { + name: "set reserved metadata for annotations", + meta: &clientTypes.Metadata{ + Annotations: []clientTypes.MetadataItem{ + {Name: "rpaas.extensions.tsuru.io/custom-key", Value: "custom-value"}, + }, + }, + expectedErr: "metadata key \"rpaas.extensions.tsuru.io/custom-key\" is reserved", + }, + } + + for _, tt := range testCases { + t.Run(tt.name, func(t *testing.T) { + instance := newEmptyRpaasInstance() + instance.Name = "my-instance" + + manager := &k8sRpaasManager{cli: fake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(instance).Build()} + + err := manager.SetMetadata(context.Background(), "my-instance", tt.meta) + if tt.expectedErr != "" { + assert.EqualError(t, err, tt.expectedErr) + return + } + + require.NoError(t, err) + + instance = newEmptyRpaasInstance() + + err = manager.cli.Get(context.Background(), types.NamespacedName{Name: "my-instance", Namespace: "rpaasv2"}, instance) + require.NoError(t, err) + + for _, item := range tt.meta.Labels { + assert.Equal(t, item.Value, instance.Labels[item.Name]) + } + + for _, item := range tt.meta.Annotations { + assert.Equal(t, item.Value, instance.Annotations[item.Name]) + } + }) + } +} + +func Test_k8sRpaasManager_UnsetMetadata(t *testing.T) { + testCases := []struct { + name string + objMeta metav1.ObjectMeta + meta clientTypes.Metadata + expectedErr string + }{ + { + name: "unset label", + objMeta: metav1.ObjectMeta{ + Name: "my-instance", + Namespace: "rpaasv2", + Labels: map[string]string{ + "my-label": "my-value", + "my-other-label": "my-other-value", + }, + Annotations: map[string]string{ + "my-annotation": "my-value", + "my-other-annotation": "my-other-value", + }, + }, + meta: clientTypes.Metadata{ + Labels: []clientTypes.MetadataItem{ + {Name: "my-label"}, + }, + Annotations: []clientTypes.MetadataItem{ + {Name: "my-other-annotation"}, + }, + }, + }, + { + name: "unset invalid label", + objMeta: metav1.ObjectMeta{ + Name: "my-instance", + Namespace: "rpaasv2", + Labels: map[string]string{ + "my-label": "my-label-value", + }, + }, + meta: clientTypes.Metadata{ + Labels: []clientTypes.MetadataItem{ + {Name: "invalid-label"}, + }, + }, + expectedErr: "label \"invalid-label\" not found in instance \"my-instance\"", + }, + { + name: "unset invalid annotation", + objMeta: metav1.ObjectMeta{ + Name: "my-instance", + Namespace: "rpaasv2", + Annotations: map[string]string{ + "my-annotation": "my-annotation-value", + }, + }, + meta: clientTypes.Metadata{ + Annotations: []clientTypes.MetadataItem{ + {Name: "invalid-annotation"}, + }, + }, + expectedErr: "annotation \"invalid-annotation\" not found in instance \"my-instance\"", + }, + } + + for _, tt := range testCases { + t.Run(tt.name, func(t *testing.T) { + scheme := newScheme() + + instance := newEmptyRpaasInstance() + instance.Name = "my-instance" + instance.ObjectMeta = tt.objMeta + + manager := &k8sRpaasManager{cli: fake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(instance).Build()} + + err := manager.UnsetMetadata(context.Background(), "my-instance", &tt.meta) + if tt.expectedErr != "" { + assert.EqualError(t, err, tt.expectedErr) + return + } + + require.NoError(t, err) + + instance = newEmptyRpaasInstance() + + err = manager.cli.Get(context.Background(), types.NamespacedName{Name: "my-instance", Namespace: "rpaasv2"}, instance) + require.NoError(t, err) + + for _, item := range tt.meta.Labels { + assert.NotContains(t, instance.Labels, item.Name) + } + + for _, item := range tt.meta.Annotations { + assert.NotContains(t, instance.Annotations, item.Name) + } + }) + } +} diff --git a/pkg/rpaas/client/metadata.go b/pkg/rpaas/client/metadata.go index 84035f34..dc1ccd15 100644 --- a/pkg/rpaas/client/metadata.go +++ b/pkg/rpaas/client/metadata.go @@ -5,7 +5,9 @@ package client import ( + "bytes" "context" + "encoding/json" "fmt" "net/http" @@ -46,20 +48,28 @@ func (c *client) SetMetadata(ctx context.Context, instance string, metadata *typ return ErrMissingInstance } - // pathName := fmt.Sprintf("/resources/%s/metadata", instance) - // req, err := c.newRequest("POST", pathName, metadata, instance) - // if err != nil { - // return err - // } - // - // response, err := c.do(ctx, req) - // if err != nil { - // return err - // } - // - // if response.StatusCode != http.StatusOK { - // return newErrUnexpectedStatusCodeFromResponse(response) - // } + b, err := json.Marshal(metadata) + if err != nil { + return err + } + body := bytes.NewReader(b) + + pathName := fmt.Sprintf("/resources/%s/metadata", instance) + req, err := c.newRequest("POST", pathName, body, instance) + if err != nil { + return err + } + req.Header.Set("Content-Type", "application/json") + + response, err := c.do(ctx, req) + if err != nil { + return err + } + defer response.Body.Close() + + if response.StatusCode != http.StatusOK { + return newErrUnexpectedStatusCodeFromResponse(response) + } return nil } @@ -69,20 +79,28 @@ func (c *client) UnsetMetadata(ctx context.Context, instance string, metadata *t return ErrMissingInstance } - // pathName := fmt.Sprintf("/resources/%s/metadata", instance) - // req, err := c.newRequest("POST", pathName, metadata, instance) - // if err != nil { - // return err - // } - // - // response, err := c.do(ctx, req) - // if err != nil { - // return err - // } - // - // if response.StatusCode != http.StatusOK { - // return newErrUnexpectedStatusCodeFromResponse(response) - // } + b, err := json.Marshal(metadata) + if err != nil { + return err + } + body := bytes.NewReader(b) + + pathName := fmt.Sprintf("/resources/%s/metadata", instance) + req, err := c.newRequest("DELETE", pathName, body, instance) + if err != nil { + return err + } + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + + response, err := c.do(ctx, req) + if err != nil { + return err + } + defer response.Body.Close() + + if response.StatusCode != http.StatusOK { + return newErrUnexpectedStatusCodeFromResponse(response) + } return nil } diff --git a/pkg/rpaas/client/metadata_test.go b/pkg/rpaas/client/metadata_test.go new file mode 100644 index 00000000..307d80b6 --- /dev/null +++ b/pkg/rpaas/client/metadata_test.go @@ -0,0 +1,5 @@ +// Copyright 2024 tsuru authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package client