Skip to content

Commit

Permalink
refactor the ApplySpecifiedModules func (kyma-project#2207)
Browse files Browse the repository at this point in the history
  • Loading branch information
pPrecel authored Aug 20, 2024
1 parent 69608ff commit 6a7c96e
Show file tree
Hide file tree
Showing 5 changed files with 197 additions and 47 deletions.
37 changes: 13 additions & 24 deletions internal/communitymodules/cluster/modules.go
Original file line number Diff line number Diff line change
@@ -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"
)
Expand Down Expand Up @@ -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
}
Expand All @@ -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
}
Expand All @@ -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
}
Expand All @@ -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
Expand Down
151 changes: 144 additions & 7 deletions internal/communitymodules/cluster/modules_test.go
Original file line number Diff line number Diff line change
@@ -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"}
Expand All @@ -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{},
Expand All @@ -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{
Expand All @@ -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])
})
}

Expand Down Expand Up @@ -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
}
8 changes: 5 additions & 3 deletions internal/communitymodules/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down
24 changes: 15 additions & 9 deletions internal/kube/rootlessdynamic/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,15 @@ 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"
"k8s.io/client-go/discovery"
"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
Expand All @@ -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,
}
}

Expand All @@ -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"))
}
Expand All @@ -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
}
Expand Down
Loading

0 comments on commit 6a7c96e

Please sign in to comment.