From 6f2190597608a56a3ebcced029d4f9595d78888d Mon Sep 17 00:00:00 2001 From: Guilherme Vicentin <79584418+gvicentin@users.noreply.github.com> Date: Mon, 10 Jun 2024 14:20:37 -0300 Subject: [PATCH] feat: API and commands for managing metadata information (#153) * feat: cli metadata command * feat: commands for set and unset annotations * getmetadata api and manager * metadata get, set and unset api methods * test: cases or metadata set and unset commands * metadata client and rpaasv2 manager * test: implementing metadata client tests * no metadata response * test: web metadatada * validate reserved keys --- cmd/plugin/rpaasv2/cmd/app.go | 1 + cmd/plugin/rpaasv2/cmd/metadata.go | 241 ++++++++++++++++++++++++ cmd/plugin/rpaasv2/cmd/metadata_test.go | 210 +++++++++++++++++++++ internal/pkg/rpaas/fake/manager.go | 24 +++ internal/pkg/rpaas/k8s.go | 27 +-- internal/pkg/rpaas/manager.go | 4 + internal/pkg/rpaas/metadata.go | 136 +++++++++++++ internal/pkg/rpaas/metadata_test.go | 223 ++++++++++++++++++++++ pkg/rpaas/client/client.go | 4 + pkg/rpaas/client/fake/client.go | 27 +++ pkg/rpaas/client/metadata.go | 106 +++++++++++ pkg/rpaas/client/metadata_test.go | 180 ++++++++++++++++++ pkg/rpaas/client/types/types.go | 10 + pkg/web/api.go | 3 + pkg/web/metadata.go | 68 +++++++ pkg/web/metadata_test.go | 190 +++++++++++++++++++ 16 files changed, 1437 insertions(+), 17 deletions(-) create mode 100644 cmd/plugin/rpaasv2/cmd/metadata.go create mode 100644 cmd/plugin/rpaasv2/cmd/metadata_test.go create mode 100644 internal/pkg/rpaas/metadata.go create mode 100644 internal/pkg/rpaas/metadata_test.go create mode 100644 pkg/rpaas/client/metadata.go create mode 100644 pkg/rpaas/client/metadata_test.go create mode 100644 pkg/web/metadata.go create mode 100644 pkg/web/metadata_test.go diff --git a/cmd/plugin/rpaasv2/cmd/app.go b/cmd/plugin/rpaasv2/cmd/app.go index 61cf2068..6a01c4bc 100644 --- a/cmd/plugin/rpaasv2/cmd/app.go +++ b/cmd/plugin/rpaasv2/cmd/app.go @@ -47,6 +47,7 @@ func NewApp(o, e io.Writer, client rpaasclient.Client) (app *cli.App) { NewCmdShell(), NewCmdLogs(), NewCmdExtraFiles(), + NewCmdMetadata(), } app.Flags = []cli.Flag{ &cli.StringFlag{ diff --git a/cmd/plugin/rpaasv2/cmd/metadata.go b/cmd/plugin/rpaasv2/cmd/metadata.go new file mode 100644 index 00000000..45227874 --- /dev/null +++ b/cmd/plugin/rpaasv2/cmd/metadata.go @@ -0,0 +1,241 @@ +// 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 cmd + +import ( + "fmt" + "io" + "strings" + + "github.com/urfave/cli/v2" + + "github.com/tsuru/rpaas-operator/pkg/rpaas/client/types" +) + +func NewCmdMetadata() *cli.Command { + return &cli.Command{ + Name: "metadata", + Usage: "Manages metadata information of rpaasv2 instances", + Subcommands: []*cli.Command{ + NewCmdGetMetadata(), + NewCmdSetMetadata(), + NewCmdUnsetMetadata(), + }, + } +} + +func NewCmdGetMetadata() *cli.Command { + return &cli.Command{ + Name: "get", + Usage: "Shows metadata information of an instance", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "service", + Aliases: []string{"tsuru-service", "s"}, + Usage: "the Tsuru service name", + }, + &cli.StringFlag{ + Name: "instance", + Aliases: []string{"tsuru-service-instance", "i"}, + Usage: "the reverse proxy instance name", + Required: true, + }, + &cli.BoolFlag{ + Name: "json", + Usage: "show as JSON instead of go template format", + Value: false, + }, + }, + Before: setupClient, + Action: runGetMetadata, + } +} + +func writeMetadata(w io.Writer, metadata *types.Metadata) { + if len(metadata.Labels) > 0 { + fmt.Fprintf(w, "Labels:\n") + for _, v := range metadata.Labels { + fmt.Fprintf(w, " %s: %s\n", v.Name, v.Value) + } + } + + if len(metadata.Annotations) > 0 { + fmt.Fprintf(w, "Annotations:\n") + for _, v := range metadata.Annotations { + fmt.Fprintf(w, " %s: %s\n", v.Name, v.Value) + } + } + + if len(metadata.Labels) == 0 && len(metadata.Annotations) == 0 { + fmt.Fprintf(w, "No metadata found\n") + } +} + +func runGetMetadata(c *cli.Context) error { + client, err := getClient(c) + if err != nil { + return err + } + + metadata, err := client.GetMetadata(c.Context, c.String("instance")) + if err != nil { + return err + } + + if outputAsJSON := c.Bool("json"); outputAsJSON { + return writeJSON(c.App.Writer, metadata) + } + + writeMetadata(c.App.Writer, metadata) + return nil +} + +func NewCmdSetMetadata() *cli.Command { + return &cli.Command{ + Name: "set", + Usage: "Sets metadata information of an instance", + ArgsUsage: " [NAME=value] ...", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "service", + Aliases: []string{"tsuru-service", "s"}, + Usage: "the Tsuru service name", + }, + &cli.StringFlag{ + Name: "instance", + Aliases: []string{"tsuru-service-instance", "i"}, + Usage: "the reverse proxy instance name", + Required: true, + }, + &cli.StringFlag{ + Name: "type", + Aliases: []string{"t"}, + Usage: "the type of metadata (label or annotation)", + Required: true, + }, + }, + Before: setupClient, + Action: runSetMetadata, + } +} + +func isValidMetadataType(metaType string) bool { + return metaType == "label" || metaType == "annotation" +} + +func createMetadata(meta []string, metaType string, isSet bool) (*types.Metadata, error) { + metadata := &types.Metadata{} + + for _, kv := range meta { + var item types.MetadataItem + if isSet { + if !strings.Contains(kv, "=") { + return nil, fmt.Errorf("invalid NAME=value pair: %q", kv) + } + item.Name = strings.Split(kv, "=")[0] + item.Value = strings.Split(kv, "=")[1] + } else { + item.Name = kv + } + + if metaType == "label" { + metadata.Labels = append(metadata.Labels, item) + } else { + metadata.Annotations = append(metadata.Annotations, item) + } + } + + return metadata, nil +} + +func runSetMetadata(c *cli.Context) error { + keyValues := c.Args().Slice() + metaType := c.String("type") + + if len(keyValues) == 0 { + return fmt.Errorf("at least one NAME=value pair is required") + } + + if !isValidMetadataType(metaType) { + return fmt.Errorf("invalid metadata type: %q", metaType) + } + + metadata, err := createMetadata(keyValues, metaType, true) + if err != nil { + return err + } + + client, err := getClient(c) + if err != nil { + return err + } + + err = client.SetMetadata(c.Context, c.String("instance"), metadata) + if err != nil { + return err + } + + fmt.Fprintln(c.App.Writer, "Metadata updated successfully") + + return nil +} + +func NewCmdUnsetMetadata() *cli.Command { + return &cli.Command{ + Name: "unset", + Usage: "Unsets metadata information of an instance", + ArgsUsage: "NAME [NAME] ...", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "service", + Aliases: []string{"tsuru-service", "s"}, + Usage: "the Tsuru service name", + }, + &cli.StringFlag{ + Name: "instance", + Aliases: []string{"tsuru-service-instance", "i"}, + Usage: "the reverse proxy instance name", + Required: true, + }, + &cli.StringFlag{ + Name: "type", + Aliases: []string{"t"}, + Usage: "the type of metadata (label or annotation)", + Required: true, + }, + }, + Before: setupClient, + Action: runUnsetMetadata, + } +} + +func runUnsetMetadata(c *cli.Context) error { + keys := c.Args().Slice() + metaType := c.String("type") + + if len(keys) == 0 { + return fmt.Errorf("at least one NAME is required") + } + + if !isValidMetadataType(metaType) { + return fmt.Errorf("invalid metadata type: %q", metaType) + } + + metadata, _ := createMetadata(keys, metaType, false) + + client, err := getClient(c) + if err != nil { + return err + } + + err = client.UnsetMetadata(c.Context, c.String("instance"), metadata) + if err != nil { + return err + } + + fmt.Fprintln(c.App.Writer, "Metadata removed successfully") + + return nil +} diff --git a/cmd/plugin/rpaasv2/cmd/metadata_test.go b/cmd/plugin/rpaasv2/cmd/metadata_test.go new file mode 100644 index 00000000..521d1a94 --- /dev/null +++ b/cmd/plugin/rpaasv2/cmd/metadata_test.go @@ -0,0 +1,210 @@ +// 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 cmd + +import ( + "bytes" + "errors" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/tsuru/rpaas-operator/pkg/rpaas/client/fake" + "github.com/tsuru/rpaas-operator/pkg/rpaas/client/types" +) + +func TestGetMetadata(t *testing.T) { + baseArgs := []string{"./rpaasv2", "metadata", "get", "-s", "my-service"} + + client := &fake.FakeClient{ + FakeGetMetadata: func(instance string) (*types.Metadata, error) { + if instance == "my-instance" { + return &types.Metadata{ + Labels: []types.MetadataItem{ + {Name: "label1", Value: "value1"}, + }, + Annotations: []types.MetadataItem{ + {Name: "annotation1", Value: "value1"}, + {Name: "annotation2", Value: "value2"}, + }, + }, nil + } else if instance == "empty-instance" { + return &types.Metadata{ + Labels: []types.MetadataItem{}, + Annotations: []types.MetadataItem{}, + }, nil + } else { + return nil, errors.New("could not find instance") + } + }, + } + + testCases := []struct { + name string + instance string + expected string + expectedErr string + }{ + { + name: "get metadata", + instance: "my-instance", + expected: `Labels: + label1: value1 +Annotations: + annotation1: value1 + annotation2: value2 +`, + }, + { + name: "get metadata with invalid instance", + instance: "invalid-instance", + expectedErr: "could not find instance", + }, + { + name: "get metadata with no content", + instance: "empty-instance", + expected: "No metadata found\n", + }, + } + + for _, tt := range testCases { + t.Run(tt.name, func(t *testing.T) { + stdout := &bytes.Buffer{} + stderr := &bytes.Buffer{} + + args := append(baseArgs, "-i", tt.instance) + + app := NewApp(stdout, stderr, client) + err := app.Run(args) + + if tt.expectedErr != "" { + assert.EqualError(t, err, tt.expectedErr) + return + } + + assert.NoError(t, err) + assert.Equal(t, tt.expected, stdout.String()) + }) + } +} + +func TestSetMetadata(t *testing.T) { + baseArgs := []string{"./rpaasv2", "metadata", "set", "-s", "my-service"} + testCases := []struct { + name string + args []string + expectedErr string + }{ + { + name: "instance not found", + args: []string{"-i", "invalid-instance", "-t", "label", "key1=value1"}, + expectedErr: "could not find instance", + }, + { + name: "no key-values provided", + args: []string{"-i", "my-instance", "-t", "label"}, + expectedErr: "at least one NAME=value pair is required", + }, + { + name: "invalid metadata type", + args: []string{"-i", "my-instance", "-t", "invalid", "key=value"}, + expectedErr: "invalid metadata type: \"invalid\"", + }, + { + name: "invalid key value pair", + args: []string{"-i", "my-instance", "-t", "annotation", "key"}, + expectedErr: "invalid NAME=value pair: \"key\"", + }, + { + name: "valid metadata", + args: []string{"-i", "my-instance", "-t", "label", "key1=value1", "key2=value2"}, + }, + } + + client := &fake.FakeClient{ + FakeSetMetadata: func(instance string, metadata *types.Metadata) error { + if instance != "my-instance" { + return errors.New("could not find instance") + } + return nil + }, + } + + for _, tt := range testCases { + t.Run(tt.name, func(t *testing.T) { + stdout := &bytes.Buffer{} + stderr := &bytes.Buffer{} + + args := append(baseArgs, tt.args...) + + app := NewApp(stdout, stderr, client) + err := app.Run(args) + + if tt.expectedErr != "" { + assert.EqualError(t, err, tt.expectedErr) + return + } + + assert.NoError(t, err) + }) + } +} + +func TestUnsetMetadata(t *testing.T) { + baseArgs := []string{"./rpaasv2", "metadata", "unset", "-s", "my-service"} + testCases := []struct { + name string + args []string + expectedErr string + }{ + { + name: "instance not found", + args: []string{"-i", "invalid-instance", "-t", "label", "key1"}, + expectedErr: "could not find instance", + }, + { + name: "no key-values provided", + args: []string{"-i", "my-instance", "-t", "label"}, + expectedErr: "at least one NAME is required", + }, + { + name: "invalid metadata type", + args: []string{"-i", "my-instance", "-t", "invalid", "key=value"}, + expectedErr: "invalid metadata type: \"invalid\"", + }, + { + name: "valid metadata", + args: []string{"-i", "my-instance", "-t", "label", "key1", "key2"}, + }, + } + + client := &fake.FakeClient{ + FakeUnsetMetadata: func(instance string, metadata *types.Metadata) error { + if instance != "my-instance" { + return errors.New("could not find instance") + } + return nil + }, + } + + for _, tt := range testCases { + t.Run(tt.name, func(t *testing.T) { + stdout := &bytes.Buffer{} + stderr := &bytes.Buffer{} + + args := append(baseArgs, tt.args...) + + app := NewApp(stdout, stderr, client) + err := app.Run(args) + + if tt.expectedErr != "" { + assert.EqualError(t, err, tt.expectedErr) + return + } + + assert.NoError(t, err) + }) + } +} diff --git a/internal/pkg/rpaas/fake/manager.go b/internal/pkg/rpaas/fake/manager.go index 23007c1b..e66a3b17 100644 --- a/internal/pkg/rpaas/fake/manager.go +++ b/internal/pkg/rpaas/fake/manager.go @@ -60,6 +60,9 @@ type RpaasManager struct { FakeGetCertManagerRequests func(instanceName string) ([]clientTypes.CertManager, error) FakeUpdateCertManagerRequest func(instanceName string, in clientTypes.CertManager) error FakeDeleteCertManagerRequest func(instanceName, issuer string) error + FakeGetMetadata func(instanceName string) (*clientTypes.Metadata, error) + FakeSetMetadata func(instanceName string, metadata *clientTypes.Metadata) error + FakeUnsetMetadata func(instanceName string, metadata *clientTypes.Metadata) error } func (m *RpaasManager) Log(ctx context.Context, instanceName string, args rpaas.LogArgs) error { @@ -349,3 +352,24 @@ func (m *RpaasManager) DeleteCertManagerRequest(ctx context.Context, instance, i } return nil } + +func (m *RpaasManager) GetMetadata(ctx context.Context, instance string) (*clientTypes.Metadata, error) { + if m.FakeGetMetadata != nil { + return m.FakeGetMetadata(instance) + } + return nil, nil +} + +func (m *RpaasManager) SetMetadata(ctx context.Context, instance string, metadata *clientTypes.Metadata) error { + if m.FakeSetMetadata != nil { + return m.FakeSetMetadata(instance, metadata) + } + return nil +} + +func (m *RpaasManager) UnsetMetadata(ctx context.Context, instance string, metadata *clientTypes.Metadata) error { + if m.FakeUnsetMetadata != nil { + return m.FakeUnsetMetadata(instance, metadata) + } + return nil +} diff --git a/internal/pkg/rpaas/k8s.go b/internal/pkg/rpaas/k8s.go index 99b18459..12c9fb08 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" @@ -66,8 +65,10 @@ import ( ) const ( - defaultNamespace = "rpaasv2" - defaultKeyLabelPrefix = "rpaas.extensions.tsuru.io" + defaultNamespace = "rpaasv2" + defaultKeyLabelPrefix = "rpaas.extensions.tsuru.io" + defaultKeyRpaasInstance = "rpaas_instance" + defaultKeyRpaasService = "rpaas_service" externalDNSHostnameLabel = "external-dns.alpha.kubernetes.io/hostname" allowedDNSZonesAnnotation = "rpaas.extensions.tsuru.io/allowed-dns-zones" @@ -1365,8 +1366,8 @@ func labelsForRpaasInstance(name string) map[string]string { return map[string]string{ labelKey("service-name"): getServiceName(), labelKey("instance-name"): name, - "rpaas_service": getServiceName(), - "rpaas_instance": name, + defaultKeyRpaasService: getServiceName(), + defaultKeyRpaasInstance: name, } } @@ -1507,23 +1508,15 @@ func setLoadBalancerName(instance *v1alpha1.RpaasInstance, lbName string) { instance.Spec.Service.Annotations[lbNameLabelKey] = lbName } -func filterAnnotations(annotations map[string]string) []string { - var filterAnnotations []string - for key, val := range annotations { - if !strings.HasPrefix(key, defaultKeyLabelPrefix) { - filterAnnotations = append(filterAnnotations, fmt.Sprintf("%s=%s", key, val)) - } - } - slices.Sort(filterAnnotations) - return filterAnnotations -} - func (m *k8sRpaasManager) GetInstanceInfo(ctx context.Context, instanceName string) (*clientTypes.InstanceInfo, error) { instance, err := m.GetInstance(ctx, instanceName) if err != nil { return nil, err } + filteredAnnotations := filterMetadata(instance.Annotations) + flatAnnotations := flattenMetadata(filteredAnnotations) + info := &clientTypes.InstanceInfo{ Name: instance.Name, Service: instance.Labels[labelKey("service-name")], @@ -1532,7 +1525,7 @@ func (m *k8sRpaasManager) GetInstanceInfo(ctx context.Context, instanceName stri Description: instance.Annotations[labelKey("description")], Team: instance.Annotations[labelKey("team-owner")], Tags: strings.Split(instance.Annotations[labelKey("tags")], ","), - Annotations: filterAnnotations(instance.Annotations), + Annotations: flatAnnotations, Replicas: instance.Spec.Replicas, Plan: instance.Spec.PlanName, Binds: instance.Spec.Binds, diff --git a/internal/pkg/rpaas/manager.go b/internal/pkg/rpaas/manager.go index 3900825a..4e6ce82d 100644 --- a/internal/pkg/rpaas/manager.go +++ b/internal/pkg/rpaas/manager.go @@ -283,6 +283,10 @@ type RpaasManager interface { GetCertManagerRequests(ctx context.Context, instanceName string) ([]clientTypes.CertManager, error) UpdateCertManagerRequest(ctx context.Context, instanceName string, in clientTypes.CertManager) error DeleteCertManagerRequest(ctx context.Context, instanceName, issuer string) error + + GetMetadata(ctx context.Context, instanceName string) (*clientTypes.Metadata, error) + SetMetadata(ctx context.Context, instanceName string, metadata *clientTypes.Metadata) error + UnsetMetadata(ctx context.Context, instanceName string, metadata *clientTypes.Metadata) error } type CertificateData struct { diff --git a/internal/pkg/rpaas/metadata.go b/internal/pkg/rpaas/metadata.go new file mode 100644 index 00000000..781e7874 --- /dev/null +++ b/internal/pkg/rpaas/metadata.go @@ -0,0 +1,136 @@ +// 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 isValidMetadataKey(key string) bool { + return !strings.HasPrefix(key, defaultKeyLabelPrefix) && + key != defaultKeyRpaasInstance && key != defaultKeyRpaasService +} + +func validateMetadata(items []clientTypes.MetadataItem) error { + for _, item := range items { + if !isValidMetadataKey(item.Name) { + 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..4876ea96 --- /dev/null +++ b/internal/pkg/rpaas/metadata_test.go @@ -0,0 +1,223 @@ +// 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: "custom_label1", Value: "custom-value1"}, + {Name: "custom_label2", Value: "custom-value2"}, + }, + 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_instance", Value: "my-instance"}, + {Name: "rpaas_service", Value: "my-instance"}, + }, + }, + expectedErr: "metadata key \"rpaas_instance\" 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/client.go b/pkg/rpaas/client/client.go index f9fe3531..5a63ef1d 100644 --- a/pkg/rpaas/client/client.go +++ b/pkg/rpaas/client/client.go @@ -181,6 +181,10 @@ type Client interface { ListCertManagerRequests(ctx context.Context, instance string) ([]types.CertManager, error) UpdateCertManager(ctx context.Context, args UpdateCertManagerArgs) error DeleteCertManager(ctx context.Context, instance, issuer string) error + + GetMetadata(ctx context.Context, instance string) (*types.Metadata, error) + SetMetadata(ctx context.Context, instance string, metadata *types.Metadata) error + UnsetMetadata(ctx context.Context, instance string, metadata *types.Metadata) error } type wsWriter struct { diff --git a/pkg/rpaas/client/fake/client.go b/pkg/rpaas/client/fake/client.go index 948074fe..33ed37f8 100644 --- a/pkg/rpaas/client/fake/client.go +++ b/pkg/rpaas/client/fake/client.go @@ -45,6 +45,9 @@ type FakeClient struct { FakeDeleteExtraFiles func(args client.DeleteExtraFilesArgs) error FakeListExtraFiles func(args client.ListExtraFilesArgs) ([]types.RpaasFile, error) FakeGetExtraFile func(args client.GetExtraFileArgs) (types.RpaasFile, error) + FakeGetMetadata func(instance string) (*types.Metadata, error) + FakeSetMetadata func(instance string, metadata *types.Metadata) error + FakeUnsetMetadata func(instance string, metadata *types.Metadata) error } func (f *FakeClient) Info(ctx context.Context, args client.InfoArgs) (*types.InstanceInfo, error) { @@ -273,3 +276,27 @@ func (f *FakeClient) GetExtraFile(ctx context.Context, args client.GetExtraFileA return types.RpaasFile{}, nil } + +func (f *FakeClient) GetMetadata(ctx context.Context, instance string) (*types.Metadata, error) { + if f.FakeGetMetadata != nil { + return f.FakeGetMetadata(instance) + } + + return nil, nil +} + +func (f *FakeClient) SetMetadata(ctx context.Context, instance string, metadata *types.Metadata) error { + if f.FakeSetMetadata != nil { + return f.FakeSetMetadata(instance, metadata) + } + + return nil +} + +func (f *FakeClient) UnsetMetadata(tx context.Context, instance string, metadata *types.Metadata) error { + if f.FakeUnsetMetadata != nil { + return f.FakeUnsetMetadata(instance, metadata) + } + + return nil +} diff --git a/pkg/rpaas/client/metadata.go b/pkg/rpaas/client/metadata.go new file mode 100644 index 00000000..87624101 --- /dev/null +++ b/pkg/rpaas/client/metadata.go @@ -0,0 +1,106 @@ +// 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 + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "net/http" + + "github.com/tsuru/rpaas-operator/pkg/rpaas/client/types" +) + +func (c *client) GetMetadata(ctx context.Context, instance string) (*types.Metadata, error) { + if instance == "" { + return nil, ErrMissingInstance + } + + pathName := fmt.Sprintf("/resources/%s/metadata", instance) + req, err := c.newRequest("GET", pathName, nil, instance) + if err != nil { + return nil, err + } + + response, err := c.do(ctx, req) + if err != nil { + return nil, err + } + defer response.Body.Close() + + if response.StatusCode != http.StatusOK { + return nil, newErrUnexpectedStatusCodeFromResponse(response) + } + + var metadata types.Metadata + if err = unmarshalBody(response, &metadata); err != nil { + return nil, err + } + + return &metadata, nil +} + +func (c *client) SetMetadata(ctx context.Context, instance string, metadata *types.Metadata) error { + if instance == "" { + return ErrMissingInstance + } + + 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 +} + +func (c *client) UnsetMetadata(ctx context.Context, instance string, metadata *types.Metadata) error { + if instance == "" { + return ErrMissingInstance + } + + 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/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 +} diff --git a/pkg/rpaas/client/metadata_test.go b/pkg/rpaas/client/metadata_test.go new file mode 100644 index 00000000..03899d56 --- /dev/null +++ b/pkg/rpaas/client/metadata_test.go @@ -0,0 +1,180 @@ +// 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 + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/tsuru/rpaas-operator/pkg/rpaas/client/types" +) + +func TestClientThroughTsuru_GetMetadata(t *testing.T) { + tests := []struct { + name string + instance string + expectedError string + handler http.HandlerFunc + }{ + { + name: "missing instance", + instance: "", + expectedError: "rpaasv2: instance cannot be empty", + handler: func(w http.ResponseWriter, r *http.Request) {}, + }, + { + name: "unexpected status code", + instance: "my-instance", + expectedError: "rpaasv2: unexpected status code: 404 Not Found, detail: instance not found", + handler: func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNotFound) + w.Write([]byte("instance not found")) + }, + }, + { + name: "success", + instance: "my-instance", + handler: func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, r.Method, "GET") + assert.Equal(t, fmt.Sprintf("/services/%s/proxy/%s?callback=%s", FakeTsuruService, "my-instance", "/resources/my-instance/metadata"), r.URL.RequestURI()) + assert.Equal(t, "Bearer f4k3t0k3n", r.Header.Get("Authorization")) + + metadata := types.Metadata{ + Labels: []types.MetadataItem{}, + Annotations: []types.MetadataItem{}, + } + + m, _ := json.Marshal(metadata) + w.WriteHeader(http.StatusOK) + w.Write(m) + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + client, server := newClientThroughTsuru(t, tt.handler) + defer server.Close() + + metadata, err := client.GetMetadata(context.TODO(), tt.instance) + if tt.expectedError != "" { + assert.EqualError(t, err, tt.expectedError) + return + } + + assert.NoError(t, err) + assert.NotNil(t, metadata) + }) + } +} + +func TestClientThroughTsuru_SetMetadata(t *testing.T) { + tests := []struct { + name string + instance string + expectedError string + handler http.HandlerFunc + }{ + { + name: "missing instance", + instance: "", + expectedError: "rpaasv2: instance cannot be empty", + handler: func(w http.ResponseWriter, r *http.Request) {}, + }, + { + name: "unexpected status code", + instance: "my-instance", + expectedError: "rpaasv2: unexpected status code: 404 Not Found, detail: instance not found", + handler: func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNotFound) + w.Write([]byte("instance not found")) + }, + }, + { + name: "success", + instance: "my-instance", + handler: func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, r.Method, "POST") + assert.Equal(t, fmt.Sprintf("/services/%s/proxy/%s?callback=%s", FakeTsuruService, "my-instance", "/resources/my-instance/metadata"), r.URL.RequestURI()) + assert.Equal(t, "Bearer f4k3t0k3n", r.Header.Get("Authorization")) + assert.Equal(t, "application/json", r.Header.Get("Content-Type")) + assert.NotNil(t, r.Body) + w.WriteHeader(http.StatusOK) + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + client, server := newClientThroughTsuru(t, tt.handler) + defer server.Close() + + err := client.SetMetadata(context.TODO(), tt.instance, &types.Metadata{}) + if tt.expectedError != "" { + assert.EqualError(t, err, tt.expectedError) + return + } + + assert.NoError(t, err) + }) + } +} + +func TestClientThroughTsuru_UnsetMetadata(t *testing.T) { + tests := []struct { + name string + instance string + expectedError string + handler http.HandlerFunc + }{ + { + name: "missing instance", + instance: "", + expectedError: "rpaasv2: instance cannot be empty", + handler: func(w http.ResponseWriter, r *http.Request) {}, + }, + { + name: "unexpected status code", + instance: "my-instance", + expectedError: "rpaasv2: unexpected status code: 404 Not Found, detail: instance not found", + handler: func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNotFound) + w.Write([]byte("instance not found")) + }, + }, + { + name: "success", + instance: "my-instance", + handler: func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, r.Method, "DELETE") + assert.Equal(t, fmt.Sprintf("/services/%s/proxy/%s?callback=%s", FakeTsuruService, "my-instance", "/resources/my-instance/metadata"), r.URL.RequestURI()) + assert.Equal(t, "Bearer f4k3t0k3n", r.Header.Get("Authorization")) + assert.Equal(t, "application/json", r.Header.Get("Content-Type")) + assert.NotNil(t, r.Body) + w.WriteHeader(http.StatusOK) + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + client, server := newClientThroughTsuru(t, tt.handler) + defer server.Close() + + err := client.UnsetMetadata(context.TODO(), tt.instance, &types.Metadata{}) + if tt.expectedError != "" { + assert.EqualError(t, err, tt.expectedError) + return + } + + assert.NoError(t, err) + }) + } +} diff --git a/pkg/rpaas/client/types/types.go b/pkg/rpaas/client/types/types.go index 3e843ac2..e1e01124 100644 --- a/pkg/rpaas/client/types/types.go +++ b/pkg/rpaas/client/types/types.go @@ -163,3 +163,13 @@ type CertManager struct { DNSNames []string `json:"dnsNames,omitempty"` IPAddresses []string `json:"ipAddresses,omitempty"` } + +type Metadata struct { + Labels []MetadataItem `json:"labels"` + Annotations []MetadataItem `json:"annotations"` +} + +type MetadataItem struct { + Name string `json:"name"` + Value string `json:"value,omitempty"` +} diff --git a/pkg/web/api.go b/pkg/web/api.go index cea1b6a9..a8a60cf9 100644 --- a/pkg/web/api.go +++ b/pkg/web/api.go @@ -256,6 +256,9 @@ func newEcho(targetFactory target.Factory) *echo.Echo { group.POST("/:instance/acl", addUpstream) group.DELETE("/:instance/acl", deleteUpstream) group.GET("/:instance/log", log) + group.GET("/:instance/metadata", getMetadata) + group.POST("/:instance/metadata", setMetadata) + group.DELETE("/:instance/metadata", unsetMetadata) return e } diff --git a/pkg/web/metadata.go b/pkg/web/metadata.go new file mode 100644 index 00000000..8b543ca9 --- /dev/null +++ b/pkg/web/metadata.go @@ -0,0 +1,68 @@ +// 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 web + +import ( + "net/http" + + "github.com/labstack/echo/v4" + + clientTypes "github.com/tsuru/rpaas-operator/pkg/rpaas/client/types" +) + +func getMetadata(c echo.Context) error { + ctx := c.Request().Context() + manager, err := getManager(ctx) + if err != nil { + return err + } + + metadata, err := manager.GetMetadata(ctx, c.Param("instance")) + if err != nil { + return err + } + + return c.JSON(http.StatusOK, metadata) +} + +func setMetadata(c echo.Context) error { + ctx := c.Request().Context() + manager, err := getManager(ctx) + if err != nil { + return err + } + + var metadata clientTypes.Metadata + if err = c.Bind(&metadata); err != nil { + return err + } + + err = manager.SetMetadata(ctx, c.Param("instance"), &metadata) + if err != nil { + return err + } + + return c.NoContent(http.StatusOK) +} + +func unsetMetadata(c echo.Context) error { + ctx := c.Request().Context() + manager, err := getManager(ctx) + if err != nil { + return err + } + + var metadata clientTypes.Metadata + if err = c.Bind(&metadata); err != nil { + return err + } + + err = manager.UnsetMetadata(ctx, c.Param("instance"), &metadata) + if err != nil { + return err + } + + return c.NoContent(http.StatusOK) +} diff --git a/pkg/web/metadata_test.go b/pkg/web/metadata_test.go new file mode 100644 index 00000000..7cc2916c --- /dev/null +++ b/pkg/web/metadata_test.go @@ -0,0 +1,190 @@ +// 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 web + +import ( + "fmt" + "net/http" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/tsuru/rpaas-operator/internal/pkg/rpaas" + "github.com/tsuru/rpaas-operator/internal/pkg/rpaas/fake" + clientTypes "github.com/tsuru/rpaas-operator/pkg/rpaas/client/types" +) + +func Test_getMetadata(t *testing.T) { + testCases := []struct { + name string + instance string + expectedCode int + manager rpaas.RpaasManager + }{ + { + name: "when successfully getting metadata", + instance: "my-instance", + expectedCode: http.StatusOK, + manager: &fake.RpaasManager{ + FakeGetMetadata: func(instance string) (*clientTypes.Metadata, error) { + assert.Equal(t, "my-instance", instance) + return &clientTypes.Metadata{ + Labels: []clientTypes.MetadataItem{ + {Name: "rpaas_instance", Value: "my-instance"}, + }, + Annotations: []clientTypes.MetadataItem{ + {Name: "custom-annotation", Value: "my-annotation"}, + }, + }, nil + }, + }, + }, + { + name: "when get metadata returns an error", + instance: "my-instance", + expectedCode: http.StatusNotFound, + manager: &fake.RpaasManager{ + FakeGetMetadata: func(instance string) (*clientTypes.Metadata, error) { + return nil, rpaas.NotFoundError{Msg: "instance not found"} + }, + }, + }, + } + + for _, tt := range testCases { + t.Run(tt.name, func(t *testing.T) { + srv := newTestingServer(t, tt.manager) + defer srv.Close() + + path := fmt.Sprintf("%s/resources/%s/metadata", srv.URL, tt.instance) + + req, err := http.NewRequest(http.MethodGet, path, nil) + assert.NoError(t, err) + + rsp, err := srv.Client().Do(req) + assert.NoError(t, err) + + assert.Equal(t, tt.expectedCode, rsp.StatusCode) + }) + } +} + +func Test_setMetadata(t *testing.T) { + testCases := []struct { + name string + instance string + expectedCode int + manager rpaas.RpaasManager + }{ + { + name: "when successfully setting metadata", + instance: "my-instance", + expectedCode: http.StatusOK, + manager: &fake.RpaasManager{ + FakeSetMetadata: func(instance string, metadata *clientTypes.Metadata) error { + return nil + }, + }, + }, + { + name: "when set metadata instance not found", + instance: "my-instance", + expectedCode: http.StatusNotFound, + manager: &fake.RpaasManager{ + FakeSetMetadata: func(instance string, metadata *clientTypes.Metadata) error { + return rpaas.NotFoundError{Msg: "instance not found"} + }, + }, + }, + { + name: "when set metadata returns an error", + instance: "my-instance", + expectedCode: http.StatusBadRequest, + manager: &fake.RpaasManager{ + FakeSetMetadata: func(instance string, metadata *clientTypes.Metadata) error { + return rpaas.ValidationError{Msg: "invalid metadata"} + }, + }, + }, + } + + for _, tt := range testCases { + t.Run(tt.name, func(t *testing.T) { + srv := newTestingServer(t, tt.manager) + defer srv.Close() + + path := fmt.Sprintf("%s/resources/%s/metadata", srv.URL, tt.instance) + + req, err := http.NewRequest(http.MethodPost, path, nil) + assert.NoError(t, err) + + req.Header.Set("Content-Type", "application/json") + + rsp, err := srv.Client().Do(req) + assert.NoError(t, err) + + assert.Equal(t, tt.expectedCode, rsp.StatusCode) + }) + } +} + +func Test_unsetMetadata(t *testing.T) { + testCases := []struct { + name string + instance string + expectedCode int + manager rpaas.RpaasManager + }{ + { + name: "when successfully unsetting metadata", + instance: "my-instance", + expectedCode: http.StatusOK, + manager: &fake.RpaasManager{ + FakeUnsetMetadata: func(instance string, metadata *clientTypes.Metadata) error { + return nil + }, + }, + }, + { + name: "when unset metadata instance not found", + instance: "my-instance", + expectedCode: http.StatusNotFound, + manager: &fake.RpaasManager{ + FakeUnsetMetadata: func(instance string, metadata *clientTypes.Metadata) error { + return rpaas.NotFoundError{Msg: "instance not found"} + }, + }, + }, + { + name: "when unset metadata returns an error", + instance: "my-instance", + expectedCode: http.StatusBadRequest, + manager: &fake.RpaasManager{ + FakeUnsetMetadata: func(instance string, metadata *clientTypes.Metadata) error { + return rpaas.ValidationError{Msg: "invalid metadata"} + }, + }, + }, + } + + for _, tt := range testCases { + t.Run(tt.name, func(t *testing.T) { + srv := newTestingServer(t, tt.manager) + defer srv.Close() + + path := fmt.Sprintf("%s/resources/%s/metadata", srv.URL, tt.instance) + + req, err := http.NewRequest(http.MethodDelete, path, nil) + assert.NoError(t, err) + + req.Header.Set("Content-Type", "application/json") + + rsp, err := srv.Client().Do(req) + assert.NoError(t, err) + + assert.Equal(t, tt.expectedCode, rsp.StatusCode) + }) + } +}