diff --git a/internal/cmd/alpha/add/add.go b/internal/cmd/alpha/add/add.go index 32437990b..7b497bb06 100644 --- a/internal/cmd/alpha/add/add.go +++ b/internal/cmd/alpha/add/add.go @@ -1,29 +1,19 @@ package add import ( - "bytes" - "fmt" - "io" - "net/http" - "github.com/kyma-project/cli.v3/internal/clierror" "github.com/kyma-project/cli.v3/internal/cmd/alpha/remove/managed" "github.com/kyma-project/cli.v3/internal/cmdcommon" - "github.com/kyma-project/cli.v3/internal/communitymodules" + "github.com/kyma-project/cli.v3/internal/communitymodules/cluster" "github.com/spf13/cobra" - v1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/api/errors" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - yaml "sigs.k8s.io/yaml/goyaml.v3" ) type addConfig struct { *cmdcommon.KymaConfig cmdcommon.KubeClientConfig - wantedModules []string - //custom string + modules []string + crs []string } func NewAddCMD(kymaConfig *cmdcommon.KymaConfig) *cobra.Command { @@ -47,141 +37,17 @@ func NewAddCMD(kymaConfig *cmdcommon.KymaConfig) *cobra.Command { cmd.AddCommand(managed.NewManagedCMD(kymaConfig)) cfg.KubeClientConfig.AddFlag(cmd) - cmd.Flags().StringSliceVar(&cfg.wantedModules, "module", []string{}, "Name and version of the modules to add. Example: --module serverless,keda:1.1.1,etc...") - //cmd.Flags().StringVar(&cfg.custom, "custom", "", "Path to the custom file") + cmd.Flags().StringSliceVar(&cfg.modules, "module", []string{}, "Name and version of the modules to add. Example: --module serverless,keda:1.1.1,etc...") + cmd.Flags().StringSliceVar(&cfg.crs, "cr", []string{}, "Path to the custom CR file") return cmd } func runAdd(cfg *addConfig) clierror.Error { - err := assureNamespace("kyma-system", cfg) + err := cluster.AssureNamespace(cfg.Ctx, cfg.KubeClient.Static(), "kyma-system") if err != nil { return err } - return applySpecifiedModules(cfg) -} - -func applySpecifiedModules(cfg *addConfig) clierror.Error { - modules, err := communitymodules.GetAvailableModules() - if err != nil { - return err - } - for _, rec := range modules { - if !containsModule(rec.Name, cfg.wantedModules) { - continue - } - - fmt.Printf("Found matching module for %s\n", rec.Name) - latestVersion := communitymodules.GetLatestVersion(rec.Versions) - - err = applyGivenObjects(cfg, latestVersion.DeploymentYaml) - if err != nil { - return err - } - err = applyGivenObjects(cfg, latestVersion.CrYaml) - if err != nil { - return err - } - } - - return nil -} - -func applyGivenObjects(cfg *addConfig, url string) clierror.Error { - givenYaml, err := http.Get(url) - if err != nil { - return clierror.Wrap(err, clierror.New("failed to get YAML from URL")) - } - defer givenYaml.Body.Close() - - yamlContent, err := io.ReadAll(givenYaml.Body) - if err != nil { - return clierror.Wrap(err, clierror.New("failed to read YAML")) - } - - objects, err := decodeYaml(bytes.NewReader(yamlContent)) - if err != nil { - return clierror.Wrap(err, clierror.New("failed to decode YAML")) - } - - err = cfg.KubeClient.RootlessDynamic().ApplyMany(cfg.Ctx, objects) - if err != nil { - return clierror.Wrap(err, clierror.New("failed to apply module resources")) - - } - return nil -} - -//func applyCustomConfiguration(cfg *addConfig) clierror.Error { -// fmt.Println("Applying custom configuration from " + cfg.custom) -// -// customYaml, err := os.ReadFile(cfg.custom) -// if err != nil { -// return clierror.Wrap(err, clierror.New("failed to read custom file")) -// } -// -// objects, err := decodeYaml(bytes.NewReader(customYaml)) -// if err != nil { -// return clierror.Wrap(err, clierror.New("failed to decode YAML")) -// } -// -// err = cfg.KubeClient.RootlessDynamic().ApplyMany(cfg.Ctx, objects) -// if err != nil { -// return clierror.Wrap(err, clierror.New("failed to apply module resources")) -// } -// -// return nil -//} - -func assureNamespace(namespace string, cfg *addConfig) clierror.Error { - _, err := cfg.KubeClientConfig.KubeClient.Static().CoreV1().Namespaces().Get(cfg.Ctx, namespace, metav1.GetOptions{}) - if !errors.IsNotFound(err) { - return nil - } - _, err = cfg.KubeClientConfig.KubeClient.Static().CoreV1().Namespaces().Create(cfg.Ctx, &v1.Namespace{ - ObjectMeta: metav1.ObjectMeta{ - Name: namespace, - }, - }, metav1.CreateOptions{}) - if err != nil { - return clierror.New("failed to create namespace") - } - return nil -} - -func containsModule(have string, want []string) bool { - for _, rec := range want { - if rec == have { - return true - } - } - return false -} - -func decodeYaml(r io.Reader) ([]unstructured.Unstructured, error) { - results := make([]unstructured.Unstructured, 0) - decoder := yaml.NewDecoder(r) - - for { - var obj map[string]interface{} - err := decoder.Decode(&obj) - - if err == io.EOF { - break - } - - if err != nil { - return nil, err - } - - u := unstructured.Unstructured{Object: obj} - if u.GetObjectKind().GroupVersionKind().Kind == "CustomResourceDefinition" { - results = append([]unstructured.Unstructured{u}, results...) - continue - } - results = append(results, u) - } - - return results, nil + return cluster.ApplySpecifiedModules(cfg.Ctx, cfg.KubeClient.RootlessDynamic(), cfg.modules, cfg.crs) } diff --git a/internal/communitymodules/cluster/modules.go b/internal/communitymodules/cluster/modules.go new file mode 100644 index 000000000..c73bdc853 --- /dev/null +++ b/internal/communitymodules/cluster/modules.go @@ -0,0 +1,162 @@ +package cluster + +import ( + "bytes" + "context" + "fmt" + "io" + "net/http" + "os" + "strings" + + "github.com/kyma-project/cli.v3/internal/clierror" + "github.com/kyma-project/cli.v3/internal/communitymodules" + "github.com/kyma-project/cli.v3/internal/kube/rootlessdynamic" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + yaml "sigs.k8s.io/yaml/goyaml.v3" +) + +func ApplySpecifiedModules(ctx context.Context, client rootlessdynamic.Interface, modules, crs []string) clierror.Error { + available, err := communitymodules.GetAvailableModules() + if err != nil { + return err + } + + customConfig, err := readCustomConfig(crs) + if err != nil { + return err + } + + for _, rec := range available { + versionedName := containsModule(rec.Name, modules) //TODO move splitting to earlier + if versionedName == nil { + continue + } + + wantedVersion := verifyVersion(versionedName, rec) + fmt.Printf("Applying %s module manifest\n", rec.Name) + err = applyGivenObjects(ctx, client, wantedVersion.DeploymentYaml) + if err != nil { + return err + } + + if applyGivenCustomCR(ctx, client, rec, customConfig) { + fmt.Println("Applying custom CR") + continue + } + + fmt.Println("Applying CR") + err = applyGivenObjects(ctx, client, wantedVersion.CrYaml) + if err != nil { + return err + } + } + return nil +} + +func readCustomConfig(cr []string) ([]unstructured.Unstructured, clierror.Error) { + if len(cr) == 0 { + return nil, nil + } + var objects []unstructured.Unstructured + for _, rec := range cr { + yaml, err := os.ReadFile(rec) + if err != nil { + return nil, clierror.Wrap(err, clierror.New("failed to read custom file")) + } + currentObjects, err := decodeYaml(bytes.NewReader(yaml)) + if err != nil { + return nil, clierror.Wrap(err, clierror.New("failed to decode custom YAML")) + } + objects = append(objects, currentObjects...) + } + return objects, nil +} + +func containsModule(have string, want []string) []string { + for _, rec := range want { + name := strings.Split(rec, ":") + if name[0] == have { + return name + } + } + return nil +} + +func verifyVersion(name []string, rec communitymodules.Module) communitymodules.Version { + if len(name) != 1 { + for _, version := range rec.Versions { + if version.Version == name[1] { + fmt.Printf("Version %s found for %s\n", version.Version, rec.Name) + return version + } + } + } + + fmt.Printf("Using latest version for %s\n", rec.Name) + return communitymodules.GetLatestVersion(rec.Versions) +} + +// applyGivenCustomCR applies custom CR if it exists +func applyGivenCustomCR(ctx context.Context, client rootlessdynamic.Interface, rec communitymodules.Module, config []unstructured.Unstructured) bool { + for _, obj := range config { + if strings.EqualFold(obj.GetKind(), strings.ToLower(rec.Name)) { + client.Apply(ctx, &obj) + return true + } + } + return false + +} + +func applyGivenObjects(ctx context.Context, client rootlessdynamic.Interface, url string) clierror.Error { + givenYaml, err := http.Get(url) + if err != nil { + return clierror.Wrap(err, clierror.New("failed to get YAML from URL")) + } + defer givenYaml.Body.Close() + + yamlContent, err := io.ReadAll(givenYaml.Body) + if err != nil { + return clierror.Wrap(err, clierror.New("failed to read YAML")) + } + + objects, err := decodeYaml(bytes.NewReader(yamlContent)) + if err != nil { + return clierror.Wrap(err, clierror.New("failed to decode YAML")) + } + + cliErr := client.ApplyMany(ctx, objects) + if cliErr != nil { + return clierror.WrapE(cliErr, clierror.New("failed to apply module resources")) + + } + return nil +} + +func decodeYaml(r io.Reader) ([]unstructured.Unstructured, error) { + results := make([]unstructured.Unstructured, 0) + decoder := yaml.NewDecoder(r) + + for { + var obj map[string]interface{} + err := decoder.Decode(&obj) + + if err == io.EOF { + break + } + + if err != nil { + return nil, err + } + + u := unstructured.Unstructured{Object: obj} + if u.GetObjectKind().GroupVersionKind().Kind == "CustomResourceDefinition" { + results = append([]unstructured.Unstructured{u}, results...) + continue + } + results = append(results, u) + } + + return results, nil +} diff --git a/internal/communitymodules/cluster/modules_test.go b/internal/communitymodules/cluster/modules_test.go new file mode 100644 index 000000000..992d58c47 --- /dev/null +++ b/internal/communitymodules/cluster/modules_test.go @@ -0,0 +1,88 @@ +package cluster + +import ( + "bytes" + "github.com/kyma-project/cli.v3/internal/communitymodules" + "testing" +) + +func Test_verifyVersion(t *testing.T) { + t.Run("Version found", func(t *testing.T) { + rec := communitymodules.Module{ + Name: "test", + Versions: []communitymodules.Version{ + { + Version: "1.0.0", + }, + { + Version: "1.0.1", + }, + }, + } + var versionedName []string + versionedName = append(versionedName, "test") + versionedName = append(versionedName, "1.0.0") + + got := verifyVersion(versionedName, rec) + if got != rec.Versions[0] { + t.Errorf("verifyVersion() got = %v, want %v", got, rec.Versions[0]) + } + }) + t.Run("Version not found", func(t *testing.T) { + rec := communitymodules.Module{ + Name: "test", + Versions: []communitymodules.Version{ + { + Version: "1.0.0", + }, + { + Version: "1.0.1", + }, + }, + } + var versionedName []string + versionedName = append(versionedName, "test") + versionedName = append(versionedName, "1.0.2") + + got := verifyVersion(versionedName, rec) + if got != rec.Versions[1] { + t.Errorf("verifyVersion() got = %v, want %v", got, nil) + } + }) +} + +func Test_containsModule(t *testing.T) { + t.Run("Module found", func(t *testing.T) { + have := "serverless" + want := []string{"serverless:1.0.0", "keda:1.0.1"} + + got := containsModule(have, want) + if got[0] != "serverless" { + t.Errorf("containsModule() got = %v, want %v", got, "test:1.0.0") + } + }) + t.Run("Module not found", func(t *testing.T) { + have := "test" + want := []string{"serverless:1.0.0", "keda:1.0.1"} + + got := containsModule(have, want) + if got != nil { + t.Errorf("containsModule() got = %v, want %v", got, nil) + } + }) +} + +func Test_decodeYaml(t *testing.T) { + t.Run("Decode YAML", func(t *testing.T) { + yaml := []byte("apiVersion: v1\nkind: Pod\nmetadata:\n name: test\nspec:\n containers:\n - name: test\n image: test") + unstructured, err := decodeYaml(bytes.NewReader(yaml)) + if unstructured[0].GetKind() != "Pod" { + t.Errorf("decodeYaml() got = %v, want %v", unstructured[0].GetKind(), "Pod") + } + if err != nil { + t.Errorf("decodeYaml() got = %v, want %v", err, nil) + } + }) +} + +// func Test_readCustomConfig(t *testing.T) diff --git a/internal/communitymodules/cluster/namespace.go b/internal/communitymodules/cluster/namespace.go new file mode 100644 index 000000000..7be2fbf99 --- /dev/null +++ b/internal/communitymodules/cluster/namespace.go @@ -0,0 +1,26 @@ +package cluster + +import ( + "context" + "github.com/kyma-project/cli.v3/internal/clierror" + v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" +) + +func AssureNamespace(ctx context.Context, client kubernetes.Interface, namespace string) clierror.Error { + _, err := client.CoreV1().Namespaces().Get(ctx, namespace, metav1.GetOptions{}) + if !errors.IsNotFound(err) { + return nil + } + _, err = client.CoreV1().Namespaces().Create(ctx, &v1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: namespace, + }, + }, metav1.CreateOptions{}) + if err != nil { + return clierror.New("failed to create namespace") + } + return nil +} diff --git a/internal/communitymodules/cluster/namespace_test.go b/internal/communitymodules/cluster/namespace_test.go new file mode 100644 index 000000000..06b9a52be --- /dev/null +++ b/internal/communitymodules/cluster/namespace_test.go @@ -0,0 +1,39 @@ +package cluster + +import ( + "context" + "github.com/stretchr/testify/require" + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + k8s_fake "k8s.io/client-go/kubernetes/fake" + "testing" +) + +func Test_AssureNamespace(t *testing.T) { + t.Run("Should do nothing when namespace exists", func(t *testing.T) { + staticClient := k8s_fake.NewSimpleClientset( + &v1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: "kyma-system", + }, + }, + ) + + cliErr := AssureNamespace(context.Background(), staticClient, "kyma-system") + require.Nil(t, cliErr) + + ns, err := staticClient.CoreV1().Namespaces().Get(context.Background(), "kyma-system", metav1.GetOptions{}) + require.NoError(t, err) + require.NotNil(t, ns) + }) + t.Run("Should create namespace when it does not exist", func(t *testing.T) { + staticClient := k8s_fake.NewSimpleClientset() + + cliErr := AssureNamespace(context.Background(), staticClient, "kyma-system") + require.Nil(t, cliErr) + + ns, err := staticClient.CoreV1().Namespaces().Get(context.Background(), "kyma-system", metav1.GetOptions{}) + require.NoError(t, err) + require.NotNil(t, ns) + }) +} diff --git a/internal/kube/rootlessdynamic/client.go b/internal/kube/rootlessdynamic/client.go index a4f2403a8..9baa69cef 100644 --- a/internal/kube/rootlessdynamic/client.go +++ b/internal/kube/rootlessdynamic/client.go @@ -2,22 +2,22 @@ package rootlessdynamic import ( "context" - "errors" + "fmt" "strings" + "github.com/kyma-project/cli.v3/internal/clierror" + apimachinery_errors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/apimachinery/pkg/types" "k8s.io/client-go/discovery" - "k8s.io/client-go/discovery/cached/memory" "k8s.io/client-go/dynamic" "k8s.io/client-go/rest" ) type Interface interface { - ApplyMany(context.Context, []unstructured.Unstructured) error + Apply(context.Context, *unstructured.Unstructured) clierror.Error + ApplyMany(context.Context, []unstructured.Unstructured) clierror.Error } type client struct { @@ -33,49 +33,62 @@ func NewClient(dynamic dynamic.Interface, restConfig *rest.Config) (Interface, e return &client{ dynamic: dynamic, - discovery: memory.NewMemCacheClient(discovery), + discovery: discovery, }, nil } -// TODO: Add a script to test applying default resources -func (c *client) ApplyMany(ctx context.Context, objs []unstructured.Unstructured) error { - for _, resource := range objs { - group, version := groupVersion(resource.GetAPIVersion()) - apiResource, err := c.discoverAPIResource(group, version, resource.GetKind()) +func (c *client) Apply(ctx context.Context, resource *unstructured.Unstructured) clierror.Error { + group, version := groupVersion(resource.GetAPIVersion()) + apiResource, err := c.discoverAPIResource(group, version, resource.GetKind()) + if err != nil { + return clierror.Wrap(err, clierror.New("failed to discover API resource using discovery client")) + } + + gvr := &schema.GroupVersionResource{ + Group: group, + Version: version, + Resource: apiResource.Name, + } + + if apiResource.Namespaced { + // we should not expect here for all resources to be installed in the kyma-system namespace. passed resources should be defaulted and validated out of the Apply func + err = applyResource(ctx, c.dynamic.Resource(*gvr).Namespace("kyma-system"), resource) if err != nil { - return err + return clierror.Wrap(err, clierror.New("failed to apply namespaced resource")) } - - gvr := &schema.GroupVersionResource{ - Group: group, - Version: version, - Resource: apiResource.Name, + } else { + err = applyResource(ctx, c.dynamic.Resource(*gvr), resource) + if err != nil { + return clierror.Wrap(err, clierror.New("failed to apply cluster-scoped resource")) } + } + return nil +} - data, err := runtime.Encode(unstructured.UnstructuredJSONScheme, &resource) +func (c *client) ApplyMany(ctx context.Context, objs []unstructured.Unstructured) clierror.Error { + for _, resource := range objs { + err := c.Apply(ctx, &resource) if err != nil { return err } - - if apiResource.Namespaced { - _, err = c.dynamic.Resource(*gvr).Namespace("kyma-system").Patch(ctx, resource.GetName(), types.ApplyPatchType, data, metav1.PatchOptions{ - FieldManager: "cli", - }) - if err != nil { - return err - } - } else { - _, err = c.dynamic.Resource(*gvr).Patch(ctx, resource.GetName(), types.ApplyPatchType, data, metav1.PatchOptions{ - FieldManager: "cli", - }) - if err != nil { - return err - } - } } return nil } +// applyResource creates or updates given object +func applyResource(ctx context.Context, resourceInterface dynamic.ResourceInterface, resource *unstructured.Unstructured) error { + _, err := resourceInterface.Create(ctx, resource, metav1.CreateOptions{ + FieldManager: "cli", + }) + if apimachinery_errors.IsAlreadyExists(err) { + _, err = resourceInterface.Update(ctx, resource, metav1.UpdateOptions{ + FieldManager: "cli", + }) + } + + return err +} + func (c *client) discoverAPIResource(group, version, kind string) (*metav1.APIResource, error) { groupVersion := schema.GroupVersion{Group: group, Version: version} @@ -89,7 +102,7 @@ func (c *client) discoverAPIResource(group, version, kind string) (*metav1.APIRe return &apiResource, nil } } - return nil, errors.New("Resource " + kind + " in group " + group + " and version " + version + " not registered on cluster") + return nil, fmt.Errorf("resource '%s' in group '%s', and version '%s' not registered on cluster", kind, group, version) } func groupVersion(version string) (string, string) { diff --git a/internal/kube/rootlessdynamic/client_test.go b/internal/kube/rootlessdynamic/client_test.go new file mode 100644 index 000000000..c6d482a4e --- /dev/null +++ b/internal/kube/rootlessdynamic/client_test.go @@ -0,0 +1,250 @@ +package rootlessdynamic + +import ( + "context" + "errors" + "testing" + + "github.com/kyma-project/cli.v3/internal/clierror" + "github.com/stretchr/testify/require" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime/schema" + clientgo_fake "k8s.io/client-go/discovery/fake" + "k8s.io/client-go/dynamic" + dynamic_fake "k8s.io/client-go/dynamic/fake" + "k8s.io/client-go/kubernetes/scheme" + clientgo_testing "k8s.io/client-go/testing" +) + +func Test_Apply(t *testing.T) { + t.Run("create namespaced resource", func(t *testing.T) { + obj, apiResource := fixSecretObjectAndApiResource() + ctx := context.Background() + dynamic := dynamic_fake.NewSimpleDynamicClient(scheme.Scheme) + client := fixRootlessDynamic(dynamic, []*metav1.APIResourceList{apiResource}) + + err := client.Apply(ctx, obj) + require.Nil(t, err) + + clusterObj, clusterErr := dynamic.Resource(schema.GroupVersionResource{ + Group: "", + Version: "v1", + Resource: "secrets", + }).Namespace("kyma-system").Get(ctx, "test", metav1.GetOptions{}) + require.NoError(t, clusterErr) + require.Equal(t, obj, clusterObj) + }) + + t.Run("create cluster-scope resource", func(t *testing.T) { + obj, apiResource := fixClusterRoleObjectAndApiResource() + ctx := context.Background() + dynamic := dynamic_fake.NewSimpleDynamicClient(scheme.Scheme) + client := fixRootlessDynamic(dynamic, []*metav1.APIResourceList{apiResource}) + + err := client.Apply(ctx, obj) + require.Nil(t, err) + + clusterObj, clusterErr := dynamic.Resource(schema.GroupVersionResource{ + Group: "rbac.authorization.k8s.io", + Version: "v1", + Resource: "clusterroles", + }).Get(ctx, "test", metav1.GetOptions{}) + require.NoError(t, clusterErr) + require.Equal(t, obj, clusterObj) + }) + + t.Run("update namespaced resource", func(t *testing.T) { + obj, apiResource := fixSecretObjectAndApiResource() + ctx := context.Background() + dynamic := dynamic_fake.NewSimpleDynamicClient(scheme.Scheme, obj) + client := fixRootlessDynamic(dynamic, []*metav1.APIResourceList{apiResource}) + + obj.Object["data"] = map[string]interface{}{ + "key": "value2", + } + + err := client.Apply(ctx, obj) + require.Nil(t, err) + + clusterObj, clusterErr := dynamic.Resource(schema.GroupVersionResource{ + Group: "", + Version: "v1", + Resource: "secrets", + }).Namespace("kyma-system").Get(ctx, "test", metav1.GetOptions{}) + require.NoError(t, clusterErr) + require.Equal(t, obj, clusterObj) + }) + + t.Run("update cluster-scope resource", func(t *testing.T) { + obj, apiResource := fixClusterRoleObjectAndApiResource() + ctx := context.Background() + dynamic := dynamic_fake.NewSimpleDynamicClient(scheme.Scheme, obj) + client := fixRootlessDynamic(dynamic, []*metav1.APIResourceList{apiResource}) + + obj.Object["rules"] = map[string]interface{}{ + "apiGroups": []interface{}{ + "", + }, + } + + err := client.Apply(ctx, obj) + require.Nil(t, err) + + clusterObj, clusterErr := dynamic.Resource(schema.GroupVersionResource{ + Group: "rbac.authorization.k8s.io", + Version: "v1", + Resource: "clusterroles", + }).Get(ctx, "test", metav1.GetOptions{}) + require.NoError(t, clusterErr) + require.Equal(t, obj, clusterObj) + }) + + t.Run("create namespaced resource error because can't be discovered", func(t *testing.T) { + obj, _ := fixSecretObjectAndApiResource() + ctx := context.Background() + dynamic := dynamic_fake.NewSimpleDynamicClient(scheme.Scheme) + client := fixRootlessDynamic(dynamic, []*metav1.APIResourceList{ + { + GroupVersion: "v1", + }, + }) + + err := client.Apply(ctx, obj) + expectErr := clierror.Wrap( + errors.New("resource 'Secret' in group '', and version 'v1' not registered on cluster"), + clierror.New("failed to discover API resource using discovery client")) + require.Equal(t, expectErr, err) + }) +} + +func Test_client_ApplyMany(t *testing.T) { + t.Run("Apply many resources", func(t *testing.T) { + clusterRole, clusterRoleApiResource := fixClusterRoleObjectAndApiResource() + secret, secretApiResource := fixSecretObjectAndApiResource() + ctx := context.Background() + dynamic := dynamic_fake.NewSimpleDynamicClient(scheme.Scheme) + client := fixRootlessDynamic(dynamic, []*metav1.APIResourceList{ + clusterRoleApiResource, secretApiResource, + }) + + err := client.ApplyMany(ctx, []unstructured.Unstructured{ + *clusterRole, *secret, + }) + require.Nil(t, err) + + createdClusterRole, getErr := dynamic.Resource(schema.GroupVersionResource{ + Group: "rbac.authorization.k8s.io", + Version: "v1", + Resource: "clusterroles", + }).Get(ctx, "test", metav1.GetOptions{}) + require.NoError(t, getErr) + require.Equal(t, clusterRole, createdClusterRole) + + createdSecret, getErr := dynamic.Resource(schema.GroupVersionResource{ + Group: "", + Version: "v1", + Resource: "secrets", + }).Namespace("kyma-system").Get(ctx, "test", metav1.GetOptions{}) + require.NoError(t, getErr) + require.Equal(t, secret, createdSecret) + }) + + t.Run("failed to discover second resource", func(t *testing.T) { + clusterRole, clusterRoleApiResource := fixClusterRoleObjectAndApiResource() + secret, _ := fixSecretObjectAndApiResource() + ctx := context.Background() + dynamic := dynamic_fake.NewSimpleDynamicClient(scheme.Scheme) + client := fixRootlessDynamic(dynamic, []*metav1.APIResourceList{ + clusterRoleApiResource, + }) + + expectedErr := clierror.Wrap(errors.New( + "the server could not find the requested resource, GroupVersion \"v1\" not found"), + clierror.New("failed to discover API resource using discovery client")) + + err := client.ApplyMany(ctx, []unstructured.Unstructured{ + *clusterRole, *secret, + }) + require.Equal(t, expectedErr, err) + + createdClusterRole, getErr := dynamic.Resource(schema.GroupVersionResource{ + Group: "rbac.authorization.k8s.io", + Version: "v1", + Resource: "clusterroles", + }).Get(ctx, "test", metav1.GetOptions{}) + require.NoError(t, getErr) + require.Equal(t, clusterRole, createdClusterRole) + + createdSecret, getErr := dynamic.Resource(schema.GroupVersionResource{ + Group: "", + Version: "v1", + Resource: "secrets", + }).Namespace("kyma-system").Get(ctx, "test", metav1.GetOptions{}) + require.Error(t, getErr) + require.Nil(t, createdSecret) + }) +} + +func fixSecretObjectAndApiResource() (*unstructured.Unstructured, *metav1.APIResourceList) { + return &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "v1", + "kind": "Secret", + "metadata": map[string]interface{}{ + "name": "test", + "namespace": "kyma-system", + }, + "data": map[string]interface{}{ + "key": "value", + }, + }, + }, &metav1.APIResourceList{ + GroupVersion: "v1", + APIResources: []metav1.APIResource{ + { + Group: "", + Version: "v1", + Kind: "Secret", + SingularName: "secret", + Name: "secrets", + Namespaced: true, + }, + }, + } +} + +func fixClusterRoleObjectAndApiResource() (*unstructured.Unstructured, *metav1.APIResourceList) { + return &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "rbac.authorization.k8s.io/v1", + "kind": "ClusterRole", + "metadata": map[string]interface{}{ + "name": "test", + }, + }, + }, &metav1.APIResourceList{ + GroupVersion: "rbac.authorization.k8s.io/v1", + APIResources: []metav1.APIResource{ + { + Group: "rbac.authorization.k8s.io", + Version: "v1", + Kind: "ClusterRole", + SingularName: "clusterrole", + Name: "clusterroles", + Namespaced: false, + }, + }, + } +} + +func fixRootlessDynamic(dynamic dynamic.Interface, apiResources []*metav1.APIResourceList) *client { + return &client{ + dynamic: dynamic, + discovery: &clientgo_fake.FakeDiscovery{ + Fake: &clientgo_testing.Fake{ + Resources: apiResources, + }, + }, + } +}