Skip to content

Commit

Permalink
mockkubeapiserver: Support stringData when creating a secret
Browse files Browse the repository at this point in the history
This is an edge case in the kube apiserver, but there is special
handling for the stringData field of a secret, that is mapped to
base64 data.

Co-authored-by: Tomas Aschan <1550920+tomasaschan@users.noreply.github.com>
  • Loading branch information
justinsb and tomasaschan committed Jun 15, 2024
1 parent a248ed1 commit 7c2a843
Show file tree
Hide file tree
Showing 10 changed files with 282 additions and 1 deletion.
5 changes: 4 additions & 1 deletion dev/update-golden
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,7 @@ chmod +x bin/kubectl
export PATH="${REPO_ROOT}/bin:$PATH"
echo "kubectl version is $(kubectl version --client)"

WRITE_GOLDEN_OUTPUT=1 go test -count=1 -v ./...
WRITE_GOLDEN_OUTPUT=1 go test -count=1 -v ./...

cd "${REPO_ROOT}/mockkubeapiserver"
WRITE_GOLDEN_OUTPUT=1 go test -count=1 -v ./...
8 changes: 8 additions & 0 deletions mockkubeapiserver/patchresource.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,10 @@ func (req *patchResource) Run(ctx context.Context, s *MockKubeAPIServer) error {
patched.SetGeneration(1)
}

if err := beforeObjectCreation(ctx, patched); err != nil {
return err
}

if err := resource.CreateObject(ctx, id, patched); err != nil {
return err
}
Expand Down Expand Up @@ -130,6 +134,10 @@ func (req *patchResource) Run(ctx context.Context, s *MockKubeAPIServer) error {
klog.Infof("skipping write, object not changed")
return req.writeResponse(existingObj)
} else {
if err := beforeObjectCreation(ctx, updated); err != nil {
return err
}

if resource.SetsGeneration() {
specIsSame := reflect.DeepEqual(existingObj.Object["spec"], updated.Object["spec"])
if !specIsSame {
Expand Down
48 changes: 48 additions & 0 deletions mockkubeapiserver/postresource.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package mockkubeapiserver

import (
"context"
"encoding/base64"
"fmt"
"io"
"net/http"
Expand Down Expand Up @@ -73,8 +74,55 @@ func (req *postResource) Run(ctx context.Context, s *MockKubeAPIServer) error {
obj.SetGeneration(1)
}

if err := beforeObjectCreation(ctx, obj); err != nil {
return err
}

if err := resource.CreateObject(ctx, id, obj); err != nil {
return err
}
return req.writeResponse(obj)
}

var secretGVK = schema.GroupVersionKind{
Group: "",
Version: "v1",
Kind: "Secret",
}

func beforeObjectCreation(ctx context.Context, obj *unstructured.Unstructured) error {
gvk := obj.GroupVersionKind()
if gvk == secretGVK {
return beforeSecretCreation(ctx, obj)
}
return nil
}

func beforeSecretCreation(ctx context.Context, obj *unstructured.Unstructured) error {
// If there is any stringData, merge it into data
stringData, _, err := unstructured.NestedStringMap(obj.Object, "stringData")
if err != nil {
return fmt.Errorf("getting Secret stringData: %w", err)
}
if len(stringData) == 0 {
return nil
}

// Get a copy of data
data, _, err := unstructured.NestedStringMap(obj.Object, "data")
if err != nil {
return fmt.Errorf("getting Secret data: %w", err)
}
if data == nil {
data = make(map[string]string)
}
for k, v := range stringData {
data[k] = base64.StdEncoding.EncodeToString([]byte(v))
}
if err := unstructured.SetNestedStringMap(obj.Object, data, "data"); err != nil {
return fmt.Errorf("setting Secret data: %w", err)
}
unstructured.RemoveNestedField(obj.Object, "stringData")

return nil
}
4 changes: 4 additions & 0 deletions mockkubeapiserver/putresource.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,10 @@ func (req *putResource) Run(ctx context.Context, s *MockKubeAPIServer) error {
}
}

if err := beforeObjectCreation(ctx, updated); err != nil {
return err
}

if err := resource.UpdateObject(ctx, id, updated); err != nil {
return err
}
Expand Down
1 change: 1 addition & 0 deletions mockkubeapiserver/storage/hook.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@ package storage

// A Hook implements a lightweight watch on all objects, intended for use to mock controller behaviour.
type Hook interface {
// OnWatchEvent is called whenever a watch event is created
OnWatchEvent(ev *WatchEvent)
}
99 changes: 99 additions & 0 deletions mockkubeapiserver/tests/kubeapiserver_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package applier

import (
"context"
"net/http"
"path/filepath"
"testing"

metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/dynamic"
"k8s.io/client-go/rest"
"k8s.io/klog/v2"
"sigs.k8s.io/kubebuilder-declarative-pattern/mockkubeapiserver"
"sigs.k8s.io/kubebuilder-declarative-pattern/pkg/patterns/declarative/pkg/manifest"
"sigs.k8s.io/kubebuilder-declarative-pattern/pkg/restmapper"
"sigs.k8s.io/kubebuilder-declarative-pattern/pkg/test/httprecorder"
"sigs.k8s.io/kubebuilder-declarative-pattern/pkg/test/testharness"
)

func TestGoldenTests(t *testing.T) {
testharness.RunGoldenTests(t, "testdata", func(h *testharness.Harness, testdir string) {
ctx := context.Background()

k8s, err := mockkubeapiserver.NewMockKubeAPIServer(":0")
if err != nil {
t.Fatalf("error building mock kube-apiserver: %v", err)
}
defer func() {
if err := k8s.Stop(); err != nil {
t.Fatalf("error closing mock kube-apiserver: %v", err)
}
}()

addr, err := k8s.StartServing()
if err != nil {
t.Errorf("error starting mock kube-apiserver: %v", err)
}

klog.Infof("mock kubeapiserver will listen on %v", addr)

var requestLog httprecorder.RequestLog
wrapTransport := func(rt http.RoundTripper) http.RoundTripper {
return httprecorder.NewRecorder(rt, &requestLog)
}
restConfig := &rest.Config{
Host: addr.String(),
WrapTransport: wrapTransport,
}

httpClient := &http.Client{}

httpClient.Transport = wrapTransport(http.DefaultTransport)

// var apiserverRequestLog httprecorder.RequestLog
// if interceptHTTPServer {
// k8s.AddHook(&logKubeRequestsHook{log: &apiserverRequestLog})
// }

p := filepath.Join(testdir, "manifest.yaml")
manifestYAML := string(h.MustReadFile(p))
objects, err := manifest.ParseObjects(ctx, manifestYAML)
if err != nil {
t.Errorf("error parsing manifest %q: %v", p, err)
}

restMapper, err := restmapper.NewForTest(restConfig)
if err != nil {
t.Fatalf("error from controllerrestmapper.NewForTest: %v", err)
}

dynamicClient, err := dynamic.NewForConfigAndClient(restConfig, httpClient)
if err != nil {
t.Fatalf("building dynamic client: %v", err)
}
for _, obj := range objects.GetItems() {
gvk := obj.GroupVersionKind()
restMapping, err := restMapper.RESTMapping(gvk.GroupKind(), gvk.Version)
if err != nil {
t.Errorf("error getting restmapping for %v: %v", gvk, err)
}

var applyOptions metav1.ApplyOptions
applyOptions.FieldManager = "test"
resource := dynamicClient.Resource(restMapping.Resource).Namespace(obj.GetNamespace())

if _, err := resource.Apply(ctx, obj.GetName(), obj.UnstructuredObject(), applyOptions); err != nil {
t.Fatalf("error applying resource %v: %v", gvk, err)
}
}

t.Logf("replacing old url prefix %q", "http://"+restConfig.Host)
requestLog.ReplaceURLPrefix("http://"+restConfig.Host, "http://kube-apiserver")
requestLog.RemoveUserAgent()
requestLog.SortGETs()

requests := requestLog.FormatHTTP()
h.CompareGoldenFile(filepath.Join(testdir, "expected.yaml"), requests)
})
}
44 changes: 44 additions & 0 deletions mockkubeapiserver/tests/testdata/configmap/expected.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
GET http://kube-apiserver/api/v1
Accept: application/json, */*

200 OK
Cache-Control: no-cache, private
Content-Length: 1820
Content-Type: application/json
Date: (removed)

{"kind":"APIResourceList","apiVersion":"v1","groupVersion":"v1","resources":[{"name":"componentstatuses","singularName":"","namespaced":false,"version":"v1","kind":"ComponentStatus","verbs":null},{"name":"configmaps","singularName":"","namespaced":true,"version":"v1","kind":"ConfigMap","verbs":null},{"name":"endpoints","singularName":"","namespaced":true,"version":"v1","kind":"Endpoints","verbs":null},{"name":"events","singularName":"","namespaced":true,"version":"v1","kind":"Event","verbs":null},{"name":"limitranges","singularName":"","namespaced":true,"version":"v1","kind":"LimitRange","verbs":null},{"name":"namespaces","singularName":"","namespaced":false,"version":"v1","kind":"Namespace","verbs":null},{"name":"nodes","singularName":"","namespaced":false,"version":"v1","kind":"Node","verbs":null},{"name":"persistentvolumes","singularName":"","namespaced":false,"version":"v1","kind":"PersistentVolume","verbs":null},{"name":"persistentvolumeclaims","singularName":"","namespaced":true,"version":"v1","kind":"PersistentVolumeClaim","verbs":null},{"name":"pods","singularName":"","namespaced":true,"version":"v1","kind":"Pod","verbs":null},{"name":"podtemplates","singularName":"","namespaced":true,"version":"v1","kind":"PodTemplate","verbs":null},{"name":"replicationcontrollers","singularName":"","namespaced":true,"version":"v1","kind":"ReplicationController","verbs":null},{"name":"resourcequotas","singularName":"","namespaced":true,"version":"v1","kind":"ResourceQuota","verbs":null},{"name":"secrets","singularName":"","namespaced":true,"version":"v1","kind":"Secret","verbs":null},{"name":"services","singularName":"","namespaced":true,"version":"v1","kind":"Service","verbs":null},{"name":"serviceaccounts","singularName":"","namespaced":true,"version":"v1","kind":"ServiceAccount","verbs":null}]}

---

PATCH http://kube-apiserver/api/v1/namespaces/default?fieldManager=test&force=false
Accept: application/json
Content-Type: application/apply-patch+yaml

{"apiVersion":"v1","kind":"Namespace","metadata":{"name":"default"}}


200 OK
Cache-Control: no-cache, private
Content-Length: 294
Content-Type: application/json
Date: (removed)

{"apiVersion":"v1","kind":"Namespace","metadata":{"creationTimestamp":"2022-01-01T00:00:00Z","labels":{"kubernetes.io/metadata.name":"default"},"name":"default","resourceVersion":"1","uid":"00000000-0000-0000-0000-000000000001"},"spec":{"finalizers":["kubernetes"]},"status":{"phase":"Active"}}

---

PATCH http://kube-apiserver/api/v1/namespaces/default/configmaps/config?fieldManager=test&force=false
Accept: application/json
Content-Type: application/apply-patch+yaml

{"apiVersion":"v1","data":{"foo":"bar"},"kind":"ConfigMap","metadata":{"name":"config","namespace":"default"}}


200 OK
Cache-Control: no-cache, private
Content-Length: 220
Content-Type: application/json
Date: (removed)

{"apiVersion":"v1","data":{"foo":"bar"},"kind":"ConfigMap","metadata":{"creationTimestamp":"2022-01-01T00:00:01Z","name":"config","namespace":"default","resourceVersion":"2","uid":"00000000-0000-0000-0000-000000000002"}}
14 changes: 14 additions & 0 deletions mockkubeapiserver/tests/testdata/configmap/manifest.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
kind: Namespace
apiVersion: v1
metadata:
name: default

---

kind: ConfigMap
apiVersion: v1
metadata:
name: config
namespace: default
data:
foo: bar
44 changes: 44 additions & 0 deletions mockkubeapiserver/tests/testdata/secrets_stringdata/expected.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
GET http://kube-apiserver/api/v1
Accept: application/json, */*

200 OK
Cache-Control: no-cache, private
Content-Length: 1820
Content-Type: application/json
Date: (removed)

{"kind":"APIResourceList","apiVersion":"v1","groupVersion":"v1","resources":[{"name":"componentstatuses","singularName":"","namespaced":false,"version":"v1","kind":"ComponentStatus","verbs":null},{"name":"configmaps","singularName":"","namespaced":true,"version":"v1","kind":"ConfigMap","verbs":null},{"name":"endpoints","singularName":"","namespaced":true,"version":"v1","kind":"Endpoints","verbs":null},{"name":"events","singularName":"","namespaced":true,"version":"v1","kind":"Event","verbs":null},{"name":"limitranges","singularName":"","namespaced":true,"version":"v1","kind":"LimitRange","verbs":null},{"name":"namespaces","singularName":"","namespaced":false,"version":"v1","kind":"Namespace","verbs":null},{"name":"nodes","singularName":"","namespaced":false,"version":"v1","kind":"Node","verbs":null},{"name":"persistentvolumes","singularName":"","namespaced":false,"version":"v1","kind":"PersistentVolume","verbs":null},{"name":"persistentvolumeclaims","singularName":"","namespaced":true,"version":"v1","kind":"PersistentVolumeClaim","verbs":null},{"name":"pods","singularName":"","namespaced":true,"version":"v1","kind":"Pod","verbs":null},{"name":"podtemplates","singularName":"","namespaced":true,"version":"v1","kind":"PodTemplate","verbs":null},{"name":"replicationcontrollers","singularName":"","namespaced":true,"version":"v1","kind":"ReplicationController","verbs":null},{"name":"resourcequotas","singularName":"","namespaced":true,"version":"v1","kind":"ResourceQuota","verbs":null},{"name":"secrets","singularName":"","namespaced":true,"version":"v1","kind":"Secret","verbs":null},{"name":"services","singularName":"","namespaced":true,"version":"v1","kind":"Service","verbs":null},{"name":"serviceaccounts","singularName":"","namespaced":true,"version":"v1","kind":"ServiceAccount","verbs":null}]}

---

PATCH http://kube-apiserver/api/v1/namespaces/default?fieldManager=test&force=false
Accept: application/json
Content-Type: application/apply-patch+yaml

{"apiVersion":"v1","kind":"Namespace","metadata":{"name":"default"}}


200 OK
Cache-Control: no-cache, private
Content-Length: 294
Content-Type: application/json
Date: (removed)

{"apiVersion":"v1","kind":"Namespace","metadata":{"creationTimestamp":"2022-01-01T00:00:00Z","labels":{"kubernetes.io/metadata.name":"default"},"name":"default","resourceVersion":"1","uid":"00000000-0000-0000-0000-000000000001"},"spec":{"finalizers":["kubernetes"]},"status":{"phase":"Active"}}

---

PATCH http://kube-apiserver/api/v1/namespaces/default/secrets/secret?fieldManager=test&force=false
Accept: application/json
Content-Type: application/apply-patch+yaml

{"apiVersion":"v1","kind":"Secret","metadata":{"name":"secret","namespace":"default"},"stringData":{"foo":"bar","foo2":"bar2"},"type":"Opaque"}


200 OK
Cache-Control: no-cache, private
Content-Length: 252
Content-Type: application/json
Date: (removed)

{"apiVersion":"v1","data":{"foo":"YmFy","foo2":"YmFyMg=="},"kind":"Secret","metadata":{"creationTimestamp":"2022-01-01T00:00:01Z","name":"secret","namespace":"default","resourceVersion":"2","uid":"00000000-0000-0000-0000-000000000002"},"type":"Opaque"}
16 changes: 16 additions & 0 deletions mockkubeapiserver/tests/testdata/secrets_stringdata/manifest.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
kind: Namespace
apiVersion: v1
metadata:
name: default

---

kind: Secret
apiVersion: v1
metadata:
name: secret
namespace: default
type: Opaque
stringData:
foo: bar
foo2: bar2

0 comments on commit 7c2a843

Please sign in to comment.