Skip to content

Commit

Permalink
Add resource manager apply tests
Browse files Browse the repository at this point in the history
Signed-off-by: Stefan Prodan <stefan.prodan@gmail.com>
  • Loading branch information
stefanprodan committed Sep 2, 2021
1 parent f3dee1c commit 6a47c80
Show file tree
Hide file tree
Showing 5 changed files with 394 additions and 1 deletion.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,5 @@
# Dependency directories (remove the comment below to include it)
# vendor/

bin/
bin/
testbin/
3 changes: 3 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,7 @@ github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4er
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
Expand Down Expand Up @@ -310,6 +311,7 @@ github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3
github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.2.0 h1:qJYtXnJRWmpe7m/3XlyhrsLrEURqHRM2kxzoxXqyUDs=
github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
Expand Down Expand Up @@ -1043,6 +1045,7 @@ k8s.io/client-go v0.22.1/go.mod h1:BquC5A4UOo4qVDUtoc04/+Nxp1MeHcVc1HJm1KmG8kk=
k8s.io/code-generator v0.21.1/go.mod h1:hUlps5+9QaTrKx+jiM4rmq7YmH8wPOIko64uZCHDh6Q=
k8s.io/code-generator v0.22.1/go.mod h1:eV77Y09IopzeXOJzndrDyCI88UBok2h6WxAlBwpxa+o=
k8s.io/component-base v0.21.1/go.mod h1:NgzFZ2qu4m1juby4TnrmpR8adRk6ka62YdH5DkIIyKA=
k8s.io/component-base v0.22.1 h1:SFqIXsEN3v3Kkr1bS6rstrs1wd45StJqbtgbQ4nRQdo=
k8s.io/component-base v0.22.1/go.mod h1:0D+Bl8rrnsPN9v0dyYvkqFfBeAd4u7n77ze+p8CMiPo=
k8s.io/component-helpers v0.21.1/go.mod h1:FtC1flbiQlosHQrLrRUulnKxE4ajgWCGy/67fT2GRlQ=
k8s.io/gengo v0.0.0-20200413195148-3a45101e95ac/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0=
Expand Down
125 changes: 125 additions & 0 deletions pkg/resmgr/main_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
package resmgr

import (
"fmt"
"io"
"os"
"strings"
"sync/atomic"
"testing"

"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
apiruntime "k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
yamlutil "k8s.io/apimachinery/pkg/util/yaml"
"sigs.k8s.io/cli-utils/pkg/kstatus/polling"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/client/apiutil"
"sigs.k8s.io/controller-runtime/pkg/envtest"
)

var manager *ResourceManager

func TestMain(m *testing.M) {
testEnv := &envtest.Environment{}

cfg, err := testEnv.Start()
if err != nil {
panic(err)
}

kubeClient, err := client.NewWithWatch(cfg, client.Options{Scheme: newScheme()})
if err != nil {
panic(err)
}

restMapper, err := apiutil.NewDynamicRESTMapper(cfg)
if err != nil {
panic(err)
}

c, err := client.New(cfg, client.Options{Mapper: restMapper})
if err != nil {
panic(err)
}

poller := polling.NewStatusPoller(c, restMapper)

manager = &ResourceManager{
kubeClient: kubeClient,
kstatusPoller: poller,
fmt: &ResourceFormatter{},
fieldOwner: "resource-manager",
}

code := m.Run()

testEnv.Stop()

os.Exit(code)
}

func readManifest(manifest, namespace string) ([]*unstructured.Unstructured, error) {
data, err := os.ReadFile(manifest)
if err != nil {
return nil, err
}
yml := fmt.Sprintf(string(data), namespace)

reader := yamlutil.NewYAMLOrJSONDecoder(strings.NewReader(yml), 2048)
objects := make([]*unstructured.Unstructured, 0)
for {
obj := &unstructured.Unstructured{}
err := reader.Decode(obj)
if err != nil {
if err == io.EOF {
err = nil
break
}
return objects, err
}

if obj.IsList() {
err = obj.EachListItem(func(item apiruntime.Object) error {
obj := item.(*unstructured.Unstructured)
objects = append(objects, obj)
return nil
})
if err != nil {
return objects, err
}
continue
}

objects = append(objects, obj)
}

return objects, nil

}

func setNamespace(objects []*unstructured.Unstructured, namespace string) {
for _, object := range objects {
object.SetNamespace(namespace)
}

u := &unstructured.Unstructured{}
u.SetGroupVersionKind(schema.GroupVersionKind{
Group: "",
Kind: "Namespace",
Version: "v1",
})
u.SetName(namespace)
objects = append(objects, u)
}

var nextNameId int64

func generateName(prefix string) string {
id := atomic.AddInt64(&nextNameId, 1)
return fmt.Sprintf("%s-%d", prefix, id)
}

func removeObject(s []*unstructured.Unstructured, index int) []*unstructured.Unstructured {
return append(s[:index], s[index+1:]...)
}
211 changes: 211 additions & 0 deletions pkg/resmgr/manager_apply_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
package resmgr

import (
"context"
"encoding/base64"
"fmt"
"sort"
"testing"
"time"

"github.com/google/go-cmp/cmp"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"sigs.k8s.io/controller-runtime/pkg/client"
)

func TestApply(t *testing.T) {
timeout := 10 * time.Second
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()

objects, err := readManifest("testdata/test1.yaml", generateName("ns"))
if err != nil {
t.Fatal(err)
}

// create objects
createdChangeSet, err := manager.ApplyAllStaged(ctx, objects, false, timeout)
if err != nil {
t.Fatal(err)
}

// expected created order
sort.Sort(ApplyOrder(objects))
var expected []string
for _, object := range objects {
expected = append(expected, manager.fmt.Unstructured(object))
}

// verify the change set contains only created actions
var output []string
for _, entry := range createdChangeSet.Entries {
if diff := cmp.Diff(entry.Action, string(CreatedAction)); diff != "" {
t.Errorf("Mismatch from expected value (-want +got):\n%s", diff)
}
output = append(output, entry.Subject)
}

// verify the change set contains all objects in the right order
if diff := cmp.Diff(expected, output); diff != "" {
t.Errorf("Mismatch from expected value (-want +got):\n%s", diff)
}

// no-op apply
unchangedChangeSet, err := manager.ApplyAllStaged(ctx, objects, false, timeout)
if err != nil {
t.Fatal(err)
}

// verify the change set contains only unchanged actions
for _, entry := range unchangedChangeSet.Entries {
if diff := cmp.Diff(string(UnchangedAction), entry.Action); diff != "" {
t.Errorf("Mismatch from expected value (-want +got):\n%s", diff)
}
output = append(output, entry.Subject)
}

// extract configmap
var configMap *unstructured.Unstructured
for _, object := range objects {
if object.GetKind() == "ConfigMap" {
configMap = object
break
}
}
configMapName := manager.fmt.Unstructured(configMap)

// update a value in the configmap
err = unstructured.SetNestedField(configMap.Object, "val", "data", "key")
if err != nil {
t.Fatal(err)
}

// apply changes
configuredChangeSet, err := manager.ApplyAllStaged(ctx, objects, false, timeout)
if err != nil {
t.Fatal(err)
}

// verify the change set contains the configured action only for the configmap
for _, entry := range configuredChangeSet.Entries {
if entry.Subject == configMapName {
if diff := cmp.Diff(string(ConfiguredAction), entry.Action); diff != "" {
t.Errorf("Mismatch from expected value (-want +got):\n%s", diff)
}
} else {
if diff := cmp.Diff(string(UnchangedAction), entry.Action); diff != "" {
t.Errorf("Mismatch from expected value (-want +got):\n%s", diff)
}
}
}

// get the configmap from cluster
configMapClone := configMap.DeepCopy()
err = manager.kubeClient.Get(ctx, client.ObjectKeyFromObject(configMapClone), configMapClone)
if err != nil {
t.Fatal(err)
}

// get data value from the in-cluster configmap
val, _, err := unstructured.NestedFieldCopy(configMapClone.Object, "data", "key")
if err != nil {
t.Fatal(err)
}

// verify the configmap was updated in cluster with the right data value
if diff := cmp.Diff(val, "val"); diff != "" {
t.Errorf("Mismatch from expected value (-want +got):\n%s", diff)
}

// delete the configmap
deletedChangeSet, err := manager.DeleteAll(ctx, []*unstructured.Unstructured{configMap})
for _, entry := range deletedChangeSet.Entries {
if diff := cmp.Diff(string(DeletedAction), entry.Action); diff != "" {
t.Errorf("Mismatch from expected value (-want +got):\n%s", diff)
}
}

// reapply objects
changeSet, err := manager.ApplyAllStaged(ctx, objects, false, timeout)
if err != nil {
t.Fatal(err)
}

// verify the configmap was recreated
for _, entry := range changeSet.Entries {
if entry.Subject == configMapName {
if diff := cmp.Diff(string(CreatedAction), entry.Action); diff != "" {
t.Errorf("Mismatch from expected value (-want +got):\n%s", diff)
}
} else {
if diff := cmp.Diff(string(UnchangedAction), entry.Action); diff != "" {
t.Errorf("Mismatch from expected value (-want +got):\n%s", diff)
}
}
}

// extract secret
var secret *unstructured.Unstructured
for _, object := range objects {
if object.GetKind() == "Secret" {
secret = object
break
}
}
secretName := manager.fmt.Unstructured(secret)

// update a value in the secret
err = unstructured.SetNestedField(secret.Object, "val-secret", "stringData", "key")
if err != nil {
t.Fatal(err)
}

// apply and expect to fail
changeSet, err = manager.ApplyAllStaged(ctx, objects, false, timeout)
if err == nil {
t.Fatal("Expected error got none")
}

// verify that the error message does not contain sensitive information
expectedErr := fmt.Sprintf("%s is invalid, error: secret is is immutable", manager.fmt.Unstructured(secret))
if diff := cmp.Diff(expectedErr, err.Error()); diff != "" {
t.Errorf("Mismatch from expected value (-want +got):\n%s", diff)
}

// force apply
changeSet, err = manager.ApplyAllStaged(ctx, objects, true, timeout)
if err != nil {
t.Fatal(err)
}

// verify the secret was recreated
for _, entry := range changeSet.Entries {
if entry.Subject == secretName {
if diff := cmp.Diff(string(CreatedAction), entry.Action); diff != "" {
t.Errorf("Mismatch from expected value (-want +got):\n%s", diff)
}
} else {
if diff := cmp.Diff(string(UnchangedAction), entry.Action); diff != "" {
t.Errorf("Mismatch from expected value (-want +got):\n%s", diff)
}
}
}

// get the secret from cluster
secretClone := secret.DeepCopy()
err = manager.kubeClient.Get(ctx, client.ObjectKeyFromObject(secretClone), secretClone)
if err != nil {
t.Fatal(err)
}

// get data value from the in-cluster secret
val, _, err = unstructured.NestedFieldCopy(secretClone.Object, "data", "key")
if err != nil {
t.Fatal(err)
}

// verify the secret was updated in cluster with the right data value
if diff := cmp.Diff(val, base64.StdEncoding.EncodeToString([]byte("val-secret"))); diff != "" {
t.Errorf("Mismatch from expected value (-want +got):\n%s", diff)
}
}
Loading

0 comments on commit 6a47c80

Please sign in to comment.