From 8440e8d2d70a156b96587d6f895cb500edec4e07 Mon Sep 17 00:00:00 2001 From: gvicentin Date: Wed, 29 May 2024 16:07:50 -0300 Subject: [PATCH 01/10] feat: cli metadata command --- cmd/plugin/rpaasv2/cmd/app.go | 1 + cmd/plugin/rpaasv2/cmd/metadata.go | 150 +++++++++++++++++++++++++++++ pkg/rpaas/client/client.go | 3 + pkg/rpaas/client/metadata.go | 67 +++++++++++++ pkg/rpaas/client/types/types.go | 11 +++ 5 files changed, 232 insertions(+) create mode 100644 cmd/plugin/rpaasv2/cmd/metadata.go create mode 100644 pkg/rpaas/client/metadata.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..ed50898f --- /dev/null +++ b/cmd/plugin/rpaasv2/cmd/metadata.go @@ -0,0 +1,150 @@ +// 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" + + "github.com/tsuru/rpaas-operator/pkg/rpaas/client/types" + "github.com/urfave/cli/v2" +) + +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) + } + } +} + +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", + 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: runSetMetadata, + } +} + +func NewCmdUnsetMetadata() *cli.Command { + return &cli.Command{ + Name: "unset", + Usage: "Unsets 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: runSetMetadata, + } +} + +func runSetMetadata(c *cli.Context) error { + _, err := getClient(c) + if err != nil { + return err + } + + return nil +} diff --git a/pkg/rpaas/client/client.go b/pkg/rpaas/client/client.go index f9fe3531..ecf821a9 100644 --- a/pkg/rpaas/client/client.go +++ b/pkg/rpaas/client/client.go @@ -181,6 +181,9 @@ 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 } type wsWriter struct { diff --git a/pkg/rpaas/client/metadata.go b/pkg/rpaas/client/metadata.go new file mode 100644 index 00000000..a3c91d23 --- /dev/null +++ b/pkg/rpaas/client/metadata.go @@ -0,0 +1,67 @@ +// 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" + + "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 + // } + // + // if response.StatusCode != http.StatusOK { + // return nil, newErrUnexpectedStatusCodeFromResponse(response) + // } + + metadata := &types.Metadata{ + Labels: []types.MetadataItem{ + { Name: "label1", Value: "value1" }, + }, + Annotations: []types.MetadataItem{ + { Name: "annotation1", Value: "value1" }, + { Name: "annotation2", Value: "value2" }, + }, + } + + return metadata, nil +} + +func (c *client) SetMetadata(ctx context.Context, instance string, metadata *types.Metadata) error { + if instance == "" { + 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) + // } + + return nil +} diff --git a/pkg/rpaas/client/types/types.go b/pkg/rpaas/client/types/types.go index 3e843ac2..8f77e4a3 100644 --- a/pkg/rpaas/client/types/types.go +++ b/pkg/rpaas/client/types/types.go @@ -163,3 +163,14 @@ 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"` + Delete bool `json:"delete,omitempty" bson:"-"` +} From fbbdea749192c8ba7e0329665f460ae18002c57e Mon Sep 17 00:00:00 2001 From: gvicentin Date: Mon, 3 Jun 2024 10:42:29 -0300 Subject: [PATCH 02/10] feat: commands for set and unset annotations --- cmd/plugin/rpaasv2/cmd/metadata.go | 128 +++++++++++++++++++++--- cmd/plugin/rpaasv2/cmd/metadata_test.go | 80 +++++++++++++++ pkg/rpaas/client/client.go | 1 + pkg/rpaas/client/fake/client.go | 18 ++++ pkg/rpaas/client/metadata.go | 29 +++++- 5 files changed, 237 insertions(+), 19 deletions(-) create mode 100644 cmd/plugin/rpaasv2/cmd/metadata_test.go diff --git a/cmd/plugin/rpaasv2/cmd/metadata.go b/cmd/plugin/rpaasv2/cmd/metadata.go index ed50898f..56d30634 100644 --- a/cmd/plugin/rpaasv2/cmd/metadata.go +++ b/cmd/plugin/rpaasv2/cmd/metadata.go @@ -7,9 +7,11 @@ package cmd import ( "fmt" "io" + "strings" - "github.com/tsuru/rpaas-operator/pkg/rpaas/client/types" "github.com/urfave/cli/v2" + + "github.com/tsuru/rpaas-operator/pkg/rpaas/client/types" ) func NewCmdMetadata() *cli.Command { @@ -88,8 +90,9 @@ func runGetMetadata(c *cli.Context) error { func NewCmdSetMetadata() *cli.Command { return &cli.Command{ - Name: "set", - Usage: "Sets metadata information of an instance", + Name: "set", + Usage: "Sets metadata information of an instance", + ArgsUsage: " [NAME=value] ...", Flags: []cli.Flag{ &cli.StringFlag{ Name: "service", @@ -102,10 +105,11 @@ func NewCmdSetMetadata() *cli.Command { Usage: "the reverse proxy instance name", Required: true, }, - &cli.BoolFlag{ - Name: "json", - Usage: "show as JSON instead of go template format", - Value: false, + &cli.StringFlag{ + Name: "type", + Aliases: []string{"t"}, + Usage: "the type of metadata (label or annotation)", + Required: true, }, }, Before: setupClient, @@ -113,10 +117,77 @@ func NewCmdSetMetadata() *cli.Command { } } +func isValidMetadataType(metaType string) bool { + return metaType == "label" || metaType == "annotation" +} + +func createMetadata(meta []string, metaType string, isSet bool) (*types.Metadata, error) { + metadata := &types.Metadata{ + Labels: []types.MetadataItem{}, + Annotations: []types.MetadataItem{}, + } + + 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 + item.Delete = true + } + + 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) + 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", + Name: "unset", + Usage: "Unsets metadata information of an instance", + ArgsUsage: "NAME [NAME] ...", Flags: []cli.Flag{ &cli.StringFlag{ Name: "service", @@ -129,22 +200,47 @@ func NewCmdUnsetMetadata() *cli.Command { Usage: "the reverse proxy instance name", Required: true, }, - &cli.BoolFlag{ - Name: "json", - Usage: "show as JSON instead of go template format", - Value: false, + &cli.StringFlag{ + Name: "type", + Aliases: []string{"t"}, + Usage: "the type of metadata (label or annotation)", + Required: true, }, }, Before: setupClient, - Action: runSetMetadata, + Action: runUnsetMetadata, } } -func runSetMetadata(c *cli.Context) error { - _, err := getClient(c) +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, err := createMetadata(keys, metaType, false) + if err != nil { + return err + } + + 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) + 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..f5c3b3b2 --- /dev/null +++ b/cmd/plugin/rpaasv2/cmd/metadata_test.go @@ -0,0 +1,80 @@ +// 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 nil, errors.New("could not find instance") + } + return &types.Metadata{ + Labels: []types.MetadataItem{ + {Name: "label1", Value: "value1"}, + }, + Annotations: []types.MetadataItem{ + {Name: "annotation1", Value: "value1"}, + {Name: "annotation2", Value: "value2"}, + }, + }, nil + }, + } + + 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", + }, + } + + 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()) + }) + } +} diff --git a/pkg/rpaas/client/client.go b/pkg/rpaas/client/client.go index ecf821a9..5a63ef1d 100644 --- a/pkg/rpaas/client/client.go +++ b/pkg/rpaas/client/client.go @@ -184,6 +184,7 @@ type Client interface { 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..67539783 100644 --- a/pkg/rpaas/client/fake/client.go +++ b/pkg/rpaas/client/fake/client.go @@ -45,6 +45,8 @@ 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 } func (f *FakeClient) Info(ctx context.Context, args client.InfoArgs) (*types.InstanceInfo, error) { @@ -273,3 +275,19 @@ 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 +} diff --git a/pkg/rpaas/client/metadata.go b/pkg/rpaas/client/metadata.go index a3c91d23..8ab96fa8 100644 --- a/pkg/rpaas/client/metadata.go +++ b/pkg/rpaas/client/metadata.go @@ -32,11 +32,11 @@ func (c *client) GetMetadata(ctx context.Context, instance string) (*types.Metad metadata := &types.Metadata{ Labels: []types.MetadataItem{ - { Name: "label1", Value: "value1" }, + {Name: "label1", Value: "value1"}, }, Annotations: []types.MetadataItem{ - { Name: "annotation1", Value: "value1" }, - { Name: "annotation2", Value: "value2" }, + {Name: "annotation1", Value: "value1"}, + {Name: "annotation2", Value: "value2"}, }, } @@ -65,3 +65,26 @@ func (c *client) SetMetadata(ctx context.Context, instance string, metadata *typ return nil } + +func (c *client) UnsetMetadata(ctx context.Context, instance string, metadata *types.Metadata) error { + if instance == "" { + 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) + // } + + return nil +} From 013f46a95526784f988962b5cdf9c4a0d0e551cd Mon Sep 17 00:00:00 2001 From: gvicentin Date: Mon, 3 Jun 2024 12:19:18 -0300 Subject: [PATCH 03/10] getmetadata api and manager --- cmd/plugin/rpaasv2/cmd/metadata.go | 5 +--- internal/pkg/rpaas/fake/manager.go | 8 +++++ internal/pkg/rpaas/k8s.go | 47 ++++++++++++++++++++++++++---- internal/pkg/rpaas/k8s_test.go | 36 +++++++++++++++++++++++ internal/pkg/rpaas/manager.go | 2 ++ pkg/rpaas/client/fake/client.go | 9 ++++++ pkg/rpaas/client/metadata.go | 44 +++++++++++++--------------- pkg/web/api.go | 1 + pkg/web/metadata.go | 26 +++++++++++++++++ 9 files changed, 145 insertions(+), 33 deletions(-) create mode 100644 pkg/web/metadata.go diff --git a/cmd/plugin/rpaasv2/cmd/metadata.go b/cmd/plugin/rpaasv2/cmd/metadata.go index 56d30634..cdc39499 100644 --- a/cmd/plugin/rpaasv2/cmd/metadata.go +++ b/cmd/plugin/rpaasv2/cmd/metadata.go @@ -122,10 +122,7 @@ func isValidMetadataType(metaType string) bool { } func createMetadata(meta []string, metaType string, isSet bool) (*types.Metadata, error) { - metadata := &types.Metadata{ - Labels: []types.MetadataItem{}, - Annotations: []types.MetadataItem{}, - } + metadata := &types.Metadata{} for _, kv := range meta { var item types.MetadataItem diff --git a/internal/pkg/rpaas/fake/manager.go b/internal/pkg/rpaas/fake/manager.go index 23007c1b..d4e075f0 100644 --- a/internal/pkg/rpaas/fake/manager.go +++ b/internal/pkg/rpaas/fake/manager.go @@ -60,6 +60,7 @@ 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) } func (m *RpaasManager) Log(ctx context.Context, instanceName string, args rpaas.LogArgs) error { @@ -349,3 +350,10 @@ 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 +} diff --git a/internal/pkg/rpaas/k8s.go b/internal/pkg/rpaas/k8s.go index 64d5afa0..0119a923 100644 --- a/internal/pkg/rpaas/k8s.go +++ b/internal/pkg/rpaas/k8s.go @@ -1643,23 +1643,34 @@ 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 { +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 = append(filterAnnotations, fmt.Sprintf("%s=%s", key, val)) + filterAnnotations[key] = val } } - slices.Sort(filterAnnotations) 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 { return nil, err } + filteredAnnotations := filterMetadata(instance.Annotations) + flatAnnotations := flattenMetadata(filteredAnnotations) + info := &clientTypes.InstanceInfo{ Name: instance.Name, Service: instance.Labels[labelKey("service-name")], @@ -1668,7 +1679,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, @@ -2457,3 +2468,27 @@ 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 +} diff --git a/internal/pkg/rpaas/k8s_test.go b/internal/pkg/rpaas/k8s_test.go index 72ad2577..b4d4d186 100644 --- a/internal/pkg/rpaas/k8s_test.go +++ b/internal/pkg/rpaas/k8s_test.go @@ -5187,3 +5187,39 @@ 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/manager.go b/internal/pkg/rpaas/manager.go index 3900825a..1884ee58 100644 --- a/internal/pkg/rpaas/manager.go +++ b/internal/pkg/rpaas/manager.go @@ -283,6 +283,8 @@ 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) } type CertificateData struct { diff --git a/pkg/rpaas/client/fake/client.go b/pkg/rpaas/client/fake/client.go index 67539783..7aeaadd0 100644 --- a/pkg/rpaas/client/fake/client.go +++ b/pkg/rpaas/client/fake/client.go @@ -47,6 +47,7 @@ type FakeClient struct { 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) { @@ -291,3 +292,11 @@ func (f *FakeClient) SetMetadata(ctx context.Context, instance string, metadata return nil } + +func (f *FakeClient) UnsetMetadata(tx context.Context, instance string, metadata *types.Metadata) error { + if f.FakeUnsetMetadata != nil { + return f.FakeSetMetadata(instance, metadata) + } + + return nil +} diff --git a/pkg/rpaas/client/metadata.go b/pkg/rpaas/client/metadata.go index 8ab96fa8..84035f34 100644 --- a/pkg/rpaas/client/metadata.go +++ b/pkg/rpaas/client/metadata.go @@ -6,6 +6,8 @@ package client import ( "context" + "fmt" + "net/http" "github.com/tsuru/rpaas-operator/pkg/rpaas/client/types" ) @@ -15,32 +17,28 @@ func (c *client) GetMetadata(ctx context.Context, instance string) (*types.Metad 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 - // } - // - // if response.StatusCode != http.StatusOK { - // return nil, newErrUnexpectedStatusCodeFromResponse(response) - // } + 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) + } - metadata := &types.Metadata{ - Labels: []types.MetadataItem{ - {Name: "label1", Value: "value1"}, - }, - Annotations: []types.MetadataItem{ - {Name: "annotation1", Value: "value1"}, - {Name: "annotation2", Value: "value2"}, - }, + var metadata types.Metadata + if err = unmarshalBody(response, &metadata); err != nil { + return nil, err } - return metadata, nil + return &metadata, nil } func (c *client) SetMetadata(ctx context.Context, instance string, metadata *types.Metadata) error { diff --git a/pkg/web/api.go b/pkg/web/api.go index cea1b6a9..09b4f712 100644 --- a/pkg/web/api.go +++ b/pkg/web/api.go @@ -256,6 +256,7 @@ 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) return e } diff --git a/pkg/web/metadata.go b/pkg/web/metadata.go new file mode 100644 index 00000000..f5be9d1d --- /dev/null +++ b/pkg/web/metadata.go @@ -0,0 +1,26 @@ +// 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" +) + +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) +} From 1653e6b9aaa88fe11a3d1a02d06afed2985a2e3b Mon Sep 17 00:00:00 2001 From: gvicentin Date: Tue, 4 Jun 2024 10:39:55 -0300 Subject: [PATCH 04/10] metadata get, set and unset api methods --- cmd/plugin/rpaasv2/cmd/metadata.go | 1 - internal/pkg/rpaas/fake/manager.go | 16 +++++++ internal/pkg/rpaas/k8s.go | 68 ++++++++++++++++++++++++++++++ internal/pkg/rpaas/manager.go | 2 + pkg/rpaas/client/types/types.go | 5 +-- pkg/web/api.go | 2 + pkg/web/metadata.go | 42 ++++++++++++++++++ 7 files changed, 132 insertions(+), 4 deletions(-) diff --git a/cmd/plugin/rpaasv2/cmd/metadata.go b/cmd/plugin/rpaasv2/cmd/metadata.go index cdc39499..587d28a6 100644 --- a/cmd/plugin/rpaasv2/cmd/metadata.go +++ b/cmd/plugin/rpaasv2/cmd/metadata.go @@ -134,7 +134,6 @@ func createMetadata(meta []string, metaType string, isSet bool) (*types.Metadata item.Value = strings.Split(kv, "=")[1] } else { item.Name = kv - item.Delete = true } if metaType == "label" { diff --git a/internal/pkg/rpaas/fake/manager.go b/internal/pkg/rpaas/fake/manager.go index d4e075f0..e66a3b17 100644 --- a/internal/pkg/rpaas/fake/manager.go +++ b/internal/pkg/rpaas/fake/manager.go @@ -61,6 +61,8 @@ type RpaasManager struct { 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 { @@ -357,3 +359,17 @@ func (m *RpaasManager) GetMetadata(ctx context.Context, instance string) (*clien } 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 0119a923..15bcf7b5 100644 --- a/internal/pkg/rpaas/k8s.go +++ b/internal/pkg/rpaas/k8s.go @@ -2492,3 +2492,71 @@ func (m *k8sRpaasManager) GetMetadata(ctx context.Context, instanceName string) 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/manager.go b/internal/pkg/rpaas/manager.go index 1884ee58..4e6ce82d 100644 --- a/internal/pkg/rpaas/manager.go +++ b/internal/pkg/rpaas/manager.go @@ -285,6 +285,8 @@ type RpaasManager interface { 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/pkg/rpaas/client/types/types.go b/pkg/rpaas/client/types/types.go index 8f77e4a3..e1e01124 100644 --- a/pkg/rpaas/client/types/types.go +++ b/pkg/rpaas/client/types/types.go @@ -170,7 +170,6 @@ type Metadata struct { } type MetadataItem struct { - Name string `json:"name"` - Value string `json:"value,omitempty"` - Delete bool `json:"delete,omitempty" bson:"-"` + Name string `json:"name"` + Value string `json:"value,omitempty"` } diff --git a/pkg/web/api.go b/pkg/web/api.go index 09b4f712..a8a60cf9 100644 --- a/pkg/web/api.go +++ b/pkg/web/api.go @@ -257,6 +257,8 @@ func newEcho(targetFactory target.Factory) *echo.Echo { 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 index f5be9d1d..8b543ca9 100644 --- a/pkg/web/metadata.go +++ b/pkg/web/metadata.go @@ -8,6 +8,8 @@ import ( "net/http" "github.com/labstack/echo/v4" + + clientTypes "github.com/tsuru/rpaas-operator/pkg/rpaas/client/types" ) func getMetadata(c echo.Context) error { @@ -24,3 +26,43 @@ func getMetadata(c echo.Context) error { 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) +} From 2c6e24b35f50130d162a73d24a7b68ead661c660 Mon Sep 17 00:00:00 2001 From: gvicentin Date: Wed, 5 Jun 2024 11:08:56 -0300 Subject: [PATCH 05/10] test: cases or metadata set and unset commands --- cmd/plugin/rpaasv2/cmd/metadata.go | 12 +-- cmd/plugin/rpaasv2/cmd/metadata_test.go | 119 ++++++++++++++++++++++++ pkg/rpaas/client/fake/client.go | 2 +- 3 files changed, 124 insertions(+), 9 deletions(-) diff --git a/cmd/plugin/rpaasv2/cmd/metadata.go b/cmd/plugin/rpaasv2/cmd/metadata.go index 587d28a6..0842e8a7 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: %q", kv) + return nil, fmt.Errorf("invalid NAME=value pair: %v", 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: %q", metaType) + return fmt.Errorf("invalid metadata type: %v", metaType) } metadata, err := createMetadata(keyValues, metaType, true) @@ -173,7 +173,6 @@ func runSetMetadata(c *cli.Context) error { return err } - fmt.Fprintln(c.App.Writer, metadata) fmt.Fprintln(c.App.Writer, "Metadata updated successfully") return nil @@ -217,13 +216,10 @@ func runUnsetMetadata(c *cli.Context) error { } if !isValidMetadataType(metaType) { - return fmt.Errorf("invalid metadata type: %q", metaType) + return fmt.Errorf("invalid metadata type: %v", metaType) } - metadata, err := createMetadata(keys, metaType, false) - if err != nil { - return err - } + metadata, _ := createMetadata(keys, metaType, false) client, err := getClient(c) if err != nil { diff --git a/cmd/plugin/rpaasv2/cmd/metadata_test.go b/cmd/plugin/rpaasv2/cmd/metadata_test.go index f5c3b3b2..51305542 100644 --- a/cmd/plugin/rpaasv2/cmd/metadata_test.go +++ b/cmd/plugin/rpaasv2/cmd/metadata_test.go @@ -78,3 +78,122 @@ Annotations: }) } } + +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/pkg/rpaas/client/fake/client.go b/pkg/rpaas/client/fake/client.go index 7aeaadd0..33ed37f8 100644 --- a/pkg/rpaas/client/fake/client.go +++ b/pkg/rpaas/client/fake/client.go @@ -295,7 +295,7 @@ func (f *FakeClient) SetMetadata(ctx context.Context, instance string, metadata func (f *FakeClient) UnsetMetadata(tx context.Context, instance string, metadata *types.Metadata) error { if f.FakeUnsetMetadata != nil { - return f.FakeSetMetadata(instance, metadata) + return f.FakeUnsetMetadata(instance, metadata) } return nil From d4adddc318b88cb8007a8322e9ed8f81b9d0cf04 Mon Sep 17 00:00:00 2001 From: gvicentin Date: Wed, 5 Jun 2024 17:26:38 -0300 Subject: [PATCH 06/10] metadata client and rpaasv2 manager --- cmd/plugin/rpaasv2/cmd/metadata.go | 6 +- cmd/plugin/rpaasv2/cmd/metadata_test.go | 6 +- internal/pkg/rpaas/k8s.go | 112 ------------ internal/pkg/rpaas/k8s_test.go | 36 ---- internal/pkg/rpaas/metadata.go | 131 ++++++++++++++ internal/pkg/rpaas/metadata_test.go | 222 ++++++++++++++++++++++++ pkg/rpaas/client/metadata.go | 74 +++++--- pkg/rpaas/client/metadata_test.go | 5 + 8 files changed, 410 insertions(+), 182 deletions(-) create mode 100644 internal/pkg/rpaas/metadata.go create mode 100644 internal/pkg/rpaas/metadata_test.go create mode 100644 pkg/rpaas/client/metadata_test.go 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 From eec9a21e87f23602fe07cd7d9cb5fc01a22ac61c Mon Sep 17 00:00:00 2001 From: gvicentin Date: Thu, 6 Jun 2024 10:13:52 -0300 Subject: [PATCH 07/10] test: implementing metadata client tests --- cmd/plugin/rpaasv2/cmd/metadata.go | 1 - pkg/rpaas/client/metadata.go | 2 +- pkg/rpaas/client/metadata_test.go | 175 +++++++++++++++++++++++++++++ pkg/web/metadata_test.go | 19 ++++ 4 files changed, 195 insertions(+), 2 deletions(-) create mode 100644 pkg/web/metadata_test.go diff --git a/cmd/plugin/rpaasv2/cmd/metadata.go b/cmd/plugin/rpaasv2/cmd/metadata.go index 7ce6333e..74fd881d 100644 --- a/cmd/plugin/rpaasv2/cmd/metadata.go +++ b/cmd/plugin/rpaasv2/cmd/metadata.go @@ -231,7 +231,6 @@ func runUnsetMetadata(c *cli.Context) error { return err } - fmt.Fprintln(c.App.Writer, metadata) fmt.Fprintln(c.App.Writer, "Metadata removed successfully") return nil diff --git a/pkg/rpaas/client/metadata.go b/pkg/rpaas/client/metadata.go index dc1ccd15..87624101 100644 --- a/pkg/rpaas/client/metadata.go +++ b/pkg/rpaas/client/metadata.go @@ -90,7 +90,7 @@ func (c *client) UnsetMetadata(ctx context.Context, instance string, metadata *t if err != nil { return err } - req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + req.Header.Set("Content-Type", "application/json") response, err := c.do(ctx, req) if err != nil { diff --git a/pkg/rpaas/client/metadata_test.go b/pkg/rpaas/client/metadata_test.go index 307d80b6..03899d56 100644 --- a/pkg/rpaas/client/metadata_test.go +++ b/pkg/rpaas/client/metadata_test.go @@ -3,3 +3,178 @@ // 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/web/metadata_test.go b/pkg/web/metadata_test.go new file mode 100644 index 00000000..f2b0b09c --- /dev/null +++ b/pkg/web/metadata_test.go @@ -0,0 +1,19 @@ +// 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 "testing" + +func Test_getMetadata(t *testing.T) { + t.Skip("Not implemented.") +} + +func Test_setMetadata(t *testing.T) { + t.Skip("Not implemented.") +} + +func Test_unsetMetadata(t *testing.T) { + t.Skip("Not implemented.") +} From bcfed935140614d32d8b6634e0be9e6857de0054 Mon Sep 17 00:00:00 2001 From: gvicentin Date: Mon, 10 Jun 2024 09:37:16 -0300 Subject: [PATCH 08/10] no metadata response --- cmd/plugin/rpaasv2/cmd/metadata.go | 4 ++++ cmd/plugin/rpaasv2/cmd/metadata_test.go | 31 +++++++++++++++++-------- 2 files changed, 25 insertions(+), 10 deletions(-) diff --git a/cmd/plugin/rpaasv2/cmd/metadata.go b/cmd/plugin/rpaasv2/cmd/metadata.go index 74fd881d..45227874 100644 --- a/cmd/plugin/rpaasv2/cmd/metadata.go +++ b/cmd/plugin/rpaasv2/cmd/metadata.go @@ -67,6 +67,10 @@ func writeMetadata(w io.Writer, metadata *types.Metadata) { 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 { diff --git a/cmd/plugin/rpaasv2/cmd/metadata_test.go b/cmd/plugin/rpaasv2/cmd/metadata_test.go index 7ca2135f..521d1a94 100644 --- a/cmd/plugin/rpaasv2/cmd/metadata_test.go +++ b/cmd/plugin/rpaasv2/cmd/metadata_test.go @@ -20,18 +20,24 @@ func TestGetMetadata(t *testing.T) { client := &fake.FakeClient{ FakeGetMetadata: func(instance string) (*types.Metadata, error) { - if instance != "my-instance" { + 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") } - return &types.Metadata{ - Labels: []types.MetadataItem{ - {Name: "label1", Value: "value1"}, - }, - Annotations: []types.MetadataItem{ - {Name: "annotation1", Value: "value1"}, - {Name: "annotation2", Value: "value2"}, - }, - }, nil }, } @@ -56,6 +62,11 @@ Annotations: 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 { From a14f25a31041b591f0e26771107cc9fdb8dc329a Mon Sep 17 00:00:00 2001 From: gvicentin Date: Mon, 10 Jun 2024 10:00:51 -0300 Subject: [PATCH 09/10] test: web metadatada --- pkg/web/metadata_test.go | 179 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 175 insertions(+), 4 deletions(-) diff --git a/pkg/web/metadata_test.go b/pkg/web/metadata_test.go index f2b0b09c..7cc2916c 100644 --- a/pkg/web/metadata_test.go +++ b/pkg/web/metadata_test.go @@ -4,16 +4,187 @@ package web -import "testing" +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) { - t.Skip("Not implemented.") + 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) { - t.Skip("Not implemented.") + 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) { - t.Skip("Not implemented.") + 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) + }) + } } From f4963848ffa9fbed66672b6e2128b5cd2583b6d0 Mon Sep 17 00:00:00 2001 From: gvicentin Date: Mon, 10 Jun 2024 10:37:35 -0300 Subject: [PATCH 10/10] validate reserved keys --- internal/pkg/rpaas/k8s.go | 10 ++++++---- internal/pkg/rpaas/metadata.go | 7 ++++++- internal/pkg/rpaas/metadata_test.go | 9 +++++---- 3 files changed, 17 insertions(+), 9 deletions(-) diff --git a/internal/pkg/rpaas/k8s.go b/internal/pkg/rpaas/k8s.go index 4d02a5d4..12c9fb08 100644 --- a/internal/pkg/rpaas/k8s.go +++ b/internal/pkg/rpaas/k8s.go @@ -65,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" @@ -1364,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, } } diff --git a/internal/pkg/rpaas/metadata.go b/internal/pkg/rpaas/metadata.go index af4d0900..781e7874 100644 --- a/internal/pkg/rpaas/metadata.go +++ b/internal/pkg/rpaas/metadata.go @@ -56,9 +56,14 @@ func (m *k8sRpaasManager) GetMetadata(ctx context.Context, instanceName string) 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 strings.HasPrefix(item.Name, defaultKeyLabelPrefix) { + if !isValidMetadataKey(item.Name) { return &ValidationError{Msg: fmt.Sprintf("metadata key %q is reserved", item.Name)} } } diff --git a/internal/pkg/rpaas/metadata_test.go b/internal/pkg/rpaas/metadata_test.go index 62ca1d2a..4876ea96 100644 --- a/internal/pkg/rpaas/metadata_test.go +++ b/internal/pkg/rpaas/metadata_test.go @@ -64,8 +64,8 @@ func Test_k8sRpaasManager_SetMetadata(t *testing.T) { name: "set metadata", meta: &clientTypes.Metadata{ Labels: []clientTypes.MetadataItem{ - {Name: "rpaas_instance", Value: "my-instance"}, - {Name: "rpaas_service", Value: "my-service"}, + {Name: "custom_label1", Value: "custom-value1"}, + {Name: "custom_label2", Value: "custom-value2"}, }, Annotations: []clientTypes.MetadataItem{ {Name: "custom-annotation", Value: "custom-value"}, @@ -85,10 +85,11 @@ func Test_k8sRpaasManager_SetMetadata(t *testing.T) { name: "set reserved metadata for annotations", meta: &clientTypes.Metadata{ Annotations: []clientTypes.MetadataItem{ - {Name: "rpaas.extensions.tsuru.io/custom-key", Value: "custom-value"}, + {Name: "rpaas_instance", Value: "my-instance"}, + {Name: "rpaas_service", Value: "my-instance"}, }, }, - expectedErr: "metadata key \"rpaas.extensions.tsuru.io/custom-key\" is reserved", + expectedErr: "metadata key \"rpaas_instance\" is reserved", }, }