diff --git a/internal/communitymodules/cluster/modules.go b/internal/communitymodules/cluster/modules.go index 8cbcaeadd..1ffe95dba 100644 --- a/internal/communitymodules/cluster/modules.go +++ b/internal/communitymodules/cluster/modules.go @@ -1,16 +1,12 @@ package cluster import ( - "bytes" "context" "fmt" - "io" - "net/http" "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/resources" "github.com/kyma-project/cli.v3/internal/kube/rootlessdynamic" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" ) @@ -66,7 +62,8 @@ func applySpecifiedModules(ctx context.Context, client rootlessdynamic.Interface wantedVersion := verifyVersion(*moduleInfo, rec) fmt.Printf("Applying %s module manifest\n", rec.Name) - err := applyGivenObjects(ctx, client, wantedVersion.DeploymentYaml) + + err := applyGivenObjects(ctx, client, wantedVersion.Resources...) if err != nil { return err } @@ -77,7 +74,7 @@ func applySpecifiedModules(ctx context.Context, client rootlessdynamic.Interface } fmt.Println("Applying CR") - err = applyGivenObjects(ctx, client, wantedVersion.CrYaml) + err = applyGivenObjects(ctx, client, wantedVersion.CR) if err != nil { return err } @@ -98,6 +95,8 @@ func verifyVersion(moduleInfo ModuleInfo, rec communitymodules.Module) community if moduleInfo.Version != "" { for _, version := range rec.Versions { if version.Version == moduleInfo.Version { + // TODO: what if the user passes a version that does not exist? + // shall we for sure install the latest version? fmt.Printf("Version %s found for %s\n", version.Version, rec.Name) return version } @@ -120,27 +119,17 @@ func applyGivenCustomCR(ctx context.Context, client rootlessdynamic.Interface, r } -func applyGivenObjects(ctx context.Context, client rootlessdynamic.Interface, url string) clierror.Error { - // TODO: do we really need to call github to get module resources? community modules json contains resources - maybe we can apply them? - givenYaml, err := http.Get(url) - if err != nil { - return clierror.Wrap(err, clierror.New("failed to get YAML from URL")) +func applyGivenObjects(ctx context.Context, client rootlessdynamic.Interface, resources ...communitymodules.Resource) clierror.Error { + objects := []unstructured.Unstructured{} + for _, res := range resources { + objects = append(objects, unstructured.Unstructured{ + Object: res, + }) } - defer givenYaml.Body.Close() - yamlContent, err := io.ReadAll(givenYaml.Body) + err := client.ApplyMany(ctx, objects) if err != nil { - return clierror.Wrap(err, clierror.New("failed to read YAML")) - } - - objects, err := resources.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 clierror.WrapE(err, clierror.New("failed to apply module resources")) } return nil diff --git a/internal/communitymodules/cluster/modules_test.go b/internal/communitymodules/cluster/modules_test.go index 1d159d826..97b60f1d7 100644 --- a/internal/communitymodules/cluster/modules_test.go +++ b/internal/communitymodules/cluster/modules_test.go @@ -1,16 +1,142 @@ package cluster import ( + "context" + "errors" "testing" + "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" "github.com/stretchr/testify/require" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" discovery_fake "k8s.io/client-go/discovery/fake" dynamic_fake "k8s.io/client-go/dynamic/fake" "k8s.io/client-go/kubernetes/scheme" ) +var ( + fakeIstioCR = unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "fakegroup/v1", + "kind": "istio", + "metadata": map[string]interface{}{ + "name": "fake-istio", + "namespace": "kyma-system", + }, + }, + } + + fakeServerlessCR = unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "fakegroup/v1", + "kind": "istio", + "metadata": map[string]interface{}{ + "name": "fake-istio", + "namespace": "kyma-system", + }, + }, + } + + fakeAvailableModules = communitymodules.Modules{ + { + Name: "serverless", + Versions: []communitymodules.Version{ + { + Version: "0.0.2", + }, + { + Version: "0.0.1", + CR: fakeIstioCR.Object, + Resources: []communitymodules.Resource{ + { + //empty + }, + { + //empty + }, + }, + }, + }, + }, + { + Name: "eventing", + Versions: []communitymodules.Version{ + { + Version: "1.0.0", + }, + }, + }, + { + Name: "istio", + Versions: []communitymodules.Version{ + { + Version: "0.1.0", + CR: fakeServerlessCR.Object, + Resources: []communitymodules.Resource{ + { + //empty + }, + { + //empty + }, + { + //empty + }, + }, + }, + }, + }, + } +) + +func Test_applySpecifiedModules(t *testing.T) { + t.Run("Apply fake serverless and istio modules", func(t *testing.T) { + fakerootlessdynamic := &rootlessdynamicMock{} + + err := applySpecifiedModules(context.Background(), fakerootlessdynamic, []ModuleInfo{ + {"serverless", "0.0.1"}, + {"istio", "0.1.0"}, + }, []unstructured.Unstructured{}, fakeAvailableModules) + require.Nil(t, err) + + require.Len(t, fakerootlessdynamic.appliedObjects, 7) + require.Contains(t, fakerootlessdynamic.appliedObjects, fakeServerlessCR) + require.Contains(t, fakerootlessdynamic.appliedObjects, fakeIstioCR) + }) + + t.Run("Apply fake serverless and istio with CRs", func(t *testing.T) { + fakerootlessdynamic := &rootlessdynamicMock{} + + istioCR := fakeIstioCR + istioCR.Object["spec"] = "test" + + err := applySpecifiedModules(context.Background(), fakerootlessdynamic, []ModuleInfo{ + {Name: "istio"}, + }, []unstructured.Unstructured{ + istioCR, + }, fakeAvailableModules) + require.Nil(t, err) + + require.Len(t, fakerootlessdynamic.appliedObjects, 4) + require.Contains(t, fakerootlessdynamic.appliedObjects, istioCR) + }) + + t.Run("Apply client error", func(t *testing.T) { + fakerootlessdynamic := &rootlessdynamicMock{ + returnErr: clierror.New("test error"), + } + + err := applySpecifiedModules(context.Background(), fakerootlessdynamic, []ModuleInfo{ + {"serverless", "0.0.1"}, + }, []unstructured.Unstructured{}, fakeAvailableModules) + require.Equal(t, clierror.Wrap( + errors.New("test error"), + clierror.New("failed to apply module resources"), + ), err) + }) +} + func TestParseModules(t *testing.T) { t.Run("parse input", func(t *testing.T) { input := []string{"test", "", "test2:1.2.3"} @@ -22,7 +148,7 @@ func TestParseModules(t *testing.T) { }) } -func fixTestRootlessDynamicClient() rootlessdynamic.Interface { +func fixFakeRootlessDynamicClient() rootlessdynamic.Interface { return rootlessdynamic.NewClient( dynamic_fake.NewSimpleDynamicClient(scheme.Scheme), &discovery_fake.FakeDiscovery{}, @@ -48,9 +174,7 @@ func Test_verifyVersion(t *testing.T) { } got := verifyVersion(moduleInfo, rec) - if got != rec.Versions[0] { - t.Errorf("verifyVersion() got = %v, want %v", got, rec.Versions[0]) - } + require.Equal(t, got, rec.Versions[0]) }) t.Run("Version not found", func(t *testing.T) { rec := communitymodules.Module{ @@ -70,9 +194,7 @@ func Test_verifyVersion(t *testing.T) { } got := verifyVersion(moduleInfo, rec) - if got != rec.Versions[1] { - t.Errorf("verifyVersion() got = %v, want %v", got, nil) - } + require.Equal(t, got, rec.Versions[1]) }) } @@ -102,3 +224,18 @@ func Test_containsModule(t *testing.T) { } }) } + +type rootlessdynamicMock struct { + returnErr clierror.Error + appliedObjects []unstructured.Unstructured +} + +func (m *rootlessdynamicMock) Apply(_ context.Context, obj *unstructured.Unstructured) clierror.Error { + m.appliedObjects = append(m.appliedObjects, *obj) + return m.returnErr +} + +func (m *rootlessdynamicMock) ApplyMany(_ context.Context, objs []unstructured.Unstructured) clierror.Error { + m.appliedObjects = append(m.appliedObjects, objs...) + return m.returnErr +} diff --git a/internal/communitymodules/types.go b/internal/communitymodules/types.go index 085658ee4..7d0ee2211 100644 --- a/internal/communitymodules/types.go +++ b/internal/communitymodules/types.go @@ -12,13 +12,15 @@ type Module struct { ManagedResources []string `json:"managedResources,omitempty"` } +type Resource map[string]interface{} + type Version struct { Version string `json:"version,omitempty"` ManagerPath string `json:"managerPath,omitempty"` Repository string `json:"repository,omitempty"` - //CrPath string `json:"crPath,omitempty"` - DeploymentYaml string `json:"deploymentYaml,omitempty"` - CrYaml string `json:"crYaml,omitempty"` + + Resources []Resource `json:"resources,omitempty"` + CR Resource `json:"cr,omitempty"` } var ( diff --git a/internal/kube/rootlessdynamic/client.go b/internal/kube/rootlessdynamic/client.go index 6be81aa32..f1102fa8a 100644 --- a/internal/kube/rootlessdynamic/client.go +++ b/internal/kube/rootlessdynamic/client.go @@ -6,7 +6,6 @@ import ( "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/schema" @@ -14,6 +13,8 @@ import ( "k8s.io/client-go/dynamic" ) +type applyFunc func(context.Context, dynamic.ResourceInterface, *unstructured.Unstructured) error + type Interface interface { Apply(context.Context, *unstructured.Unstructured) clierror.Error ApplyMany(context.Context, []unstructured.Unstructured) clierror.Error @@ -22,12 +23,20 @@ type Interface interface { type client struct { dynamic dynamic.Interface discovery discovery.DiscoveryInterface + + // for testing purposes + applyFunc applyFunc } func NewClient(dynamic dynamic.Interface, discovery discovery.DiscoveryInterface) Interface { + return NewClientWithApplyFunc(dynamic, discovery, applyResource) +} + +func NewClientWithApplyFunc(dynamic dynamic.Interface, discovery discovery.DiscoveryInterface, applyFunc applyFunc) Interface { return &client{ dynamic: dynamic, discovery: discovery, + applyFunc: applyFunc, } } @@ -46,12 +55,12 @@ func (c *client) Apply(ctx context.Context, resource *unstructured.Unstructured) 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) + err = c.applyFunc(ctx, c.dynamic.Resource(*gvr).Namespace("kyma-system"), resource) if err != nil { return clierror.Wrap(err, clierror.New("failed to apply namespaced resource")) } } else { - err = applyResource(ctx, c.dynamic.Resource(*gvr), resource) + err = c.applyFunc(ctx, c.dynamic.Resource(*gvr), resource) if err != nil { return clierror.Wrap(err, clierror.New("failed to apply cluster-scoped resource")) } @@ -71,14 +80,11 @@ func (c *client) ApplyMany(ctx context.Context, objs []unstructured.Unstructured // 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{ + // this function can't be tested because of dynamic.FakeDynamicClient limitations + _, err := resourceInterface.Apply(ctx, resource.GetName(), resource, metav1.ApplyOptions{ FieldManager: "cli", + Force: true, }) - if apimachinery_errors.IsAlreadyExists(err) { - _, err = resourceInterface.Update(ctx, resource, metav1.UpdateOptions{ - FieldManager: "cli", - }) - } return err } diff --git a/internal/kube/rootlessdynamic/client_test.go b/internal/kube/rootlessdynamic/client_test.go index c6d482a4e..30038d091 100644 --- a/internal/kube/rootlessdynamic/client_test.go +++ b/internal/kube/rootlessdynamic/client_test.go @@ -7,6 +7,7 @@ import ( "github.com/kyma-project/cli.v3/internal/clierror" "github.com/stretchr/testify/require" + 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/schema" @@ -238,13 +239,28 @@ func fixClusterRoleObjectAndApiResource() (*unstructured.Unstructured, *metav1.A } } -func fixRootlessDynamic(dynamic dynamic.Interface, apiResources []*metav1.APIResourceList) *client { - return &client{ - dynamic: dynamic, - discovery: &clientgo_fake.FakeDiscovery{ +func fixRootlessDynamic(dynamic dynamic.Interface, apiResources []*metav1.APIResourceList) Interface { + return NewClientWithApplyFunc( + dynamic, + &clientgo_fake.FakeDiscovery{ Fake: &clientgo_testing.Fake{ Resources: apiResources, }, }, + fixApplyFunc, + ) +} + +// this func is a testing version of the Apply func that can't be used in tests because of dynamic.FakeDynamicClient limitations +func fixApplyFunc(ctx context.Context, ri dynamic.ResourceInterface, u *unstructured.Unstructured) error { + _, err := ri.Create(ctx, u, metav1.CreateOptions{ + FieldManager: "cli", + }) + if apimachinery_errors.IsAlreadyExists(err) { + _, err = ri.Update(ctx, u, metav1.UpdateOptions{ + FieldManager: "cli", + }) } + + return err }