Skip to content

Commit

Permalink
Refactor and test the cluster pkg (#2204)
Browse files Browse the repository at this point in the history
  • Loading branch information
pPrecel authored Aug 12, 2024
1 parent f61f4ed commit 6826f43
Show file tree
Hide file tree
Showing 8 changed files with 276 additions and 108 deletions.
14 changes: 11 additions & 3 deletions internal/cmd/alpha/add/add.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"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/cluster"
"github.com/kyma-project/cli.v3/internal/kube/resources"
"github.com/spf13/cobra"
)

Expand Down Expand Up @@ -44,10 +45,17 @@ func NewAddCMD(kymaConfig *cmdcommon.KymaConfig) *cobra.Command {
}

func runAdd(cfg *addConfig) clierror.Error {
err := cluster.AssureNamespace(cfg.Ctx, cfg.KubeClient.Static(), "kyma-system")
cliErr := cluster.AssureNamespace(cfg.Ctx, cfg.KubeClient.Static(), "kyma-system")
if cliErr != nil {
return cliErr
}

crs, err := resources.ReadFromFiles(cfg.crs...)
if err != nil {
return err
return clierror.Wrap(err, clierror.New("failed to read CRs from input paths"))
}

return cluster.ApplySpecifiedModules(cfg.Ctx, cfg.KubeClient.RootlessDynamic(), cfg.modules, cfg.crs)
modules := cluster.ParseModules(cfg.modules)

return cluster.ApplySpecifiedModules(cfg.Ctx, cfg.KubeClient.RootlessDynamic(), modules, crs)
}
119 changes: 52 additions & 67 deletions internal/communitymodules/cluster/modules.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,41 +6,72 @@ import (
"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/resources"
"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
type ModuleInfo struct {
Name string
Version string
}

// ParseModules returns ModuleInfo struct based on the string input.
// Can convert 'name' or 'name:version' into struct
func ParseModules(modules []string) []ModuleInfo {
// TODO: I can't find better place for this function (move it)
var moduleInfo []ModuleInfo
for _, module := range modules {
if module == "" {
// skip empty strings
continue
}

elems := strings.Split(module, ":")
info := ModuleInfo{
Name: elems[0],
}

if len(elems) > 1 {
info.Version = elems[1]
}

moduleInfo = append(moduleInfo, info)
}

customConfig, err := readCustomConfig(crs)
return moduleInfo
}

// ApplySpecifiedModules applies modules to the cluster based on the resources from the community module json (Github)
// if module cr is in the given crs list then it will be applied instead of one from the community module json
func ApplySpecifiedModules(ctx context.Context, client rootlessdynamic.Interface, modules []ModuleInfo, crs []unstructured.Unstructured) clierror.Error {
available, err := communitymodules.GetAvailableModules()
if err != nil {
return err
}

for _, rec := range available {
versionedName := containsModule(rec.Name, modules) //TODO move splitting to earlier
if versionedName == nil {
return applySpecifiedModules(ctx, client, modules, crs, available)
}

func applySpecifiedModules(ctx context.Context, client rootlessdynamic.Interface, modules []ModuleInfo, crs []unstructured.Unstructured, availableModules communitymodules.Modules) clierror.Error {
for _, rec := range availableModules {
moduleInfo := containsModule(rec.Name, modules)
if moduleInfo == nil {
continue
}

wantedVersion := verifyVersion(versionedName, rec)
wantedVersion := verifyVersion(*moduleInfo, rec)
fmt.Printf("Applying %s module manifest\n", rec.Name)
err = applyGivenObjects(ctx, client, wantedVersion.DeploymentYaml)
err := applyGivenObjects(ctx, client, wantedVersion.DeploymentYaml)
if err != nil {
return err
}

if applyGivenCustomCR(ctx, client, rec, customConfig) {
if applyGivenCustomCR(ctx, client, rec, crs) {
fmt.Println("Applying custom CR")
continue
}
Expand All @@ -54,39 +85,19 @@ func ApplySpecifiedModules(ctx context.Context, client rootlessdynamic.Interface
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 {
func containsModule(have string, want []ModuleInfo) *ModuleInfo {
for _, rec := range want {
name := strings.Split(rec, ":")
if name[0] == have {
return name
if have == rec.Name {
return &rec
}
}
return nil
}

func verifyVersion(name []string, rec communitymodules.Module) communitymodules.Version {
if len(name) != 1 {
func verifyVersion(moduleInfo ModuleInfo, rec communitymodules.Module) communitymodules.Version {
if moduleInfo.Version != "" {
for _, version := range rec.Versions {
if version.Version == name[1] {
if version.Version == moduleInfo.Version {
fmt.Printf("Version %s found for %s\n", version.Version, rec.Name)
return version
}
Expand All @@ -110,6 +121,7 @@ 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"))
Expand All @@ -121,7 +133,7 @@ func applyGivenObjects(ctx context.Context, client rootlessdynamic.Interface, ur
return clierror.Wrap(err, clierror.New("failed to read YAML"))
}

objects, err := decodeYaml(bytes.NewReader(yamlContent))
objects, err := resources.DecodeYaml(bytes.NewReader(yamlContent))
if err != nil {
return clierror.Wrap(err, clierror.New("failed to decode YAML"))
}
Expand All @@ -133,30 +145,3 @@ func applyGivenObjects(ctx context.Context, client rootlessdynamic.Interface, ur
}
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
}
72 changes: 44 additions & 28 deletions internal/communitymodules/cluster/modules_test.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,34 @@
package cluster

import (
"bytes"
"github.com/kyma-project/cli.v3/internal/communitymodules"
"testing"

"github.com/kyma-project/cli.v3/internal/communitymodules"
"github.com/kyma-project/cli.v3/internal/kube/rootlessdynamic"
"github.com/stretchr/testify/require"
discovery_fake "k8s.io/client-go/discovery/fake"
dynamic_fake "k8s.io/client-go/dynamic/fake"
"k8s.io/client-go/kubernetes/scheme"
)

func TestParseModules(t *testing.T) {
t.Run("parse input", func(t *testing.T) {
input := []string{"test", "", "test2:1.2.3"}

moduleInfoList := ParseModules(input)
require.Len(t, moduleInfoList, 2)
require.Contains(t, moduleInfoList, ModuleInfo{"test", ""})
require.Contains(t, moduleInfoList, ModuleInfo{"test2", "1.2.3"})
})
}

func fixTestRootlessDynamicClient() rootlessdynamic.Interface {
return rootlessdynamic.NewClient(
dynamic_fake.NewSimpleDynamicClient(scheme.Scheme),
&discovery_fake.FakeDiscovery{},
)
}

func Test_verifyVersion(t *testing.T) {
t.Run("Version found", func(t *testing.T) {
rec := communitymodules.Module{
Expand All @@ -19,11 +42,12 @@ func Test_verifyVersion(t *testing.T) {
},
},
}
var versionedName []string
versionedName = append(versionedName, "test")
versionedName = append(versionedName, "1.0.0")
moduleInfo := ModuleInfo{
Name: "test",
Version: "1.0.0",
}

got := verifyVersion(versionedName, rec)
got := verifyVersion(moduleInfo, rec)
if got != rec.Versions[0] {
t.Errorf("verifyVersion() got = %v, want %v", got, rec.Versions[0])
}
Expand All @@ -40,11 +64,12 @@ func Test_verifyVersion(t *testing.T) {
},
},
}
var versionedName []string
versionedName = append(versionedName, "test")
versionedName = append(versionedName, "1.0.2")
moduleInfo := ModuleInfo{
Name: "test",
Version: "1.0.2",
}

got := verifyVersion(versionedName, rec)
got := verifyVersion(moduleInfo, rec)
if got != rec.Versions[1] {
t.Errorf("verifyVersion() got = %v, want %v", got, nil)
}
Expand All @@ -54,35 +79,26 @@ func Test_verifyVersion(t *testing.T) {
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"}
want := []ModuleInfo{
{"serverless", "1.0.0"},
{"keda", "1.0.1"},
}

got := containsModule(have, want)
if got[0] != "serverless" {
if got.Name != "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"}
want := []ModuleInfo{
{"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)
6 changes: 5 additions & 1 deletion internal/communitymodules/modules.go
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,11 @@ func calculateVersion(moduleVersion string, installedVersion string) string {
}

func GetAvailableModules() (Modules, clierror.Error) {
resp, err := http.Get(URL)
return getAvailableModules(URL)
}

func getAvailableModules(url string) (Modules, clierror.Error) {
resp, err := http.Get(url)
if err != nil {
return nil, clierror.Wrap(err, clierror.New("failed to get available modules"))
}
Expand Down
5 changes: 4 additions & 1 deletion internal/kube/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"github.com/kyma-project/cli.v3/internal/kube/btp"
"github.com/kyma-project/cli.v3/internal/kube/kyma"
"github.com/kyma-project/cli.v3/internal/kube/rootlessdynamic"
"k8s.io/client-go/discovery"
"k8s.io/client-go/dynamic"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
Expand Down Expand Up @@ -67,11 +68,13 @@ func newClient(kubeconfig string) (Client, error) {

kymaClient := kyma.NewClient(dynamicClient)

rootlessClient, err := rootlessdynamic.NewClient(dynamicClient, restConfig)
discovery, err := discovery.NewDiscoveryClientForConfig(restConfig)
if err != nil {
return nil, err
}

rootlessClient := rootlessdynamic.NewClient(dynamicClient, discovery)

btpClient := btp.NewClient(dynamicClient)

restClientConfig := *restConfig
Expand Down
Loading

0 comments on commit 6826f43

Please sign in to comment.