Skip to content

Commit 7a17600

Browse files
justinsbtomasaschan
andcommitted
mockkubeapiserver: Support stringData when creating a secret
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 <[email protected]>
1 parent a248ed1 commit 7a17600

File tree

9 files changed

+278
-1
lines changed

9 files changed

+278
-1
lines changed

dev/update-golden

+4-1
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,7 @@ chmod +x bin/kubectl
1919
export PATH="${REPO_ROOT}/bin:$PATH"
2020
echo "kubectl version is $(kubectl version --client)"
2121

22-
WRITE_GOLDEN_OUTPUT=1 go test -count=1 -v ./...
22+
WRITE_GOLDEN_OUTPUT=1 go test -count=1 -v ./...
23+
24+
cd "${REPO_ROOT}/mockkubeapiserver"
25+
WRITE_GOLDEN_OUTPUT=1 go test -count=1 -v ./...

mockkubeapiserver/patchresource.go

+8
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,10 @@ func (req *patchResource) Run(ctx context.Context, s *MockKubeAPIServer) error {
8585
patched.SetGeneration(1)
8686
}
8787

88+
if err := beforeObjectCreation(ctx, patched); err != nil {
89+
return err
90+
}
91+
8892
if err := resource.CreateObject(ctx, id, patched); err != nil {
8993
return err
9094
}
@@ -130,6 +134,10 @@ func (req *patchResource) Run(ctx context.Context, s *MockKubeAPIServer) error {
130134
klog.Infof("skipping write, object not changed")
131135
return req.writeResponse(existingObj)
132136
} else {
137+
if err := beforeObjectCreation(ctx, updated); err != nil {
138+
return err
139+
}
140+
133141
if resource.SetsGeneration() {
134142
specIsSame := reflect.DeepEqual(existingObj.Object["spec"], updated.Object["spec"])
135143
if !specIsSame {

mockkubeapiserver/postresource.go

+48
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package mockkubeapiserver
1818

1919
import (
2020
"context"
21+
"encoding/base64"
2122
"fmt"
2223
"io"
2324
"net/http"
@@ -73,8 +74,55 @@ func (req *postResource) Run(ctx context.Context, s *MockKubeAPIServer) error {
7374
obj.SetGeneration(1)
7475
}
7576

77+
if err := beforeObjectCreation(ctx, obj); err != nil {
78+
return err
79+
}
80+
7681
if err := resource.CreateObject(ctx, id, obj); err != nil {
7782
return err
7883
}
7984
return req.writeResponse(obj)
8085
}
86+
87+
var secretGVK = schema.GroupVersionKind{
88+
Group: "",
89+
Version: "v1",
90+
Kind: "Secret",
91+
}
92+
93+
func beforeObjectCreation(ctx context.Context, obj *unstructured.Unstructured) error {
94+
gvk := obj.GroupVersionKind()
95+
if gvk == secretGVK {
96+
return beforeSecretCreation(ctx, obj)
97+
}
98+
return nil
99+
}
100+
101+
func beforeSecretCreation(ctx context.Context, obj *unstructured.Unstructured) error {
102+
// If there is any stringData, merge it into data
103+
stringData, _, err := unstructured.NestedStringMap(obj.Object, "stringData")
104+
if err != nil {
105+
return fmt.Errorf("getting Secret stringData: %w", err)
106+
}
107+
if len(stringData) == 0 {
108+
return nil
109+
}
110+
111+
// Get a copy of data
112+
data, _, err := unstructured.NestedStringMap(obj.Object, "data")
113+
if err != nil {
114+
return fmt.Errorf("getting Secret data: %w", err)
115+
}
116+
if data == nil {
117+
data = make(map[string]string)
118+
}
119+
for k, v := range stringData {
120+
data[k] = base64.StdEncoding.EncodeToString([]byte(v))
121+
}
122+
if err := unstructured.SetNestedStringMap(obj.Object, data, "data"); err != nil {
123+
return fmt.Errorf("setting Secret data: %w", err)
124+
}
125+
unstructured.RemoveNestedField(obj.Object, "stringData")
126+
127+
return nil
128+
}

mockkubeapiserver/storage/hook.go

+1
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,6 @@ package storage
22

33
// A Hook implements a lightweight watch on all objects, intended for use to mock controller behaviour.
44
type Hook interface {
5+
// OnWatchEvent is called whenever a watch event is created
56
OnWatchEvent(ev *WatchEvent)
67
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
package applier
2+
3+
import (
4+
"context"
5+
"net/http"
6+
"path/filepath"
7+
"testing"
8+
9+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
10+
"k8s.io/client-go/dynamic"
11+
"k8s.io/client-go/rest"
12+
"k8s.io/klog/v2"
13+
"sigs.k8s.io/kubebuilder-declarative-pattern/mockkubeapiserver"
14+
"sigs.k8s.io/kubebuilder-declarative-pattern/pkg/patterns/declarative/pkg/manifest"
15+
"sigs.k8s.io/kubebuilder-declarative-pattern/pkg/restmapper"
16+
"sigs.k8s.io/kubebuilder-declarative-pattern/pkg/test/httprecorder"
17+
"sigs.k8s.io/kubebuilder-declarative-pattern/pkg/test/testharness"
18+
)
19+
20+
func TestGoldenTests(t *testing.T) {
21+
testharness.RunGoldenTests(t, "testdata", func(h *testharness.Harness, testdir string) {
22+
ctx := context.Background()
23+
24+
k8s, err := mockkubeapiserver.NewMockKubeAPIServer(":0")
25+
if err != nil {
26+
t.Fatalf("error building mock kube-apiserver: %v", err)
27+
}
28+
defer func() {
29+
if err := k8s.Stop(); err != nil {
30+
t.Fatalf("error closing mock kube-apiserver: %v", err)
31+
}
32+
}()
33+
34+
addr, err := k8s.StartServing()
35+
if err != nil {
36+
t.Errorf("error starting mock kube-apiserver: %v", err)
37+
}
38+
39+
klog.Infof("mock kubeapiserver will listen on %v", addr)
40+
41+
var requestLog httprecorder.RequestLog
42+
wrapTransport := func(rt http.RoundTripper) http.RoundTripper {
43+
return httprecorder.NewRecorder(rt, &requestLog)
44+
}
45+
restConfig := &rest.Config{
46+
Host: addr.String(),
47+
WrapTransport: wrapTransport,
48+
}
49+
50+
httpClient := &http.Client{}
51+
52+
httpClient.Transport = wrapTransport(http.DefaultTransport)
53+
54+
// var apiserverRequestLog httprecorder.RequestLog
55+
// if interceptHTTPServer {
56+
// k8s.AddHook(&logKubeRequestsHook{log: &apiserverRequestLog})
57+
// }
58+
59+
p := filepath.Join(testdir, "manifest.yaml")
60+
manifestYAML := string(h.MustReadFile(p))
61+
objects, err := manifest.ParseObjects(ctx, manifestYAML)
62+
if err != nil {
63+
t.Errorf("error parsing manifest %q: %v", p, err)
64+
}
65+
66+
restMapper, err := restmapper.NewForTest(restConfig)
67+
if err != nil {
68+
t.Fatalf("error from controllerrestmapper.NewForTest: %v", err)
69+
}
70+
71+
dynamicClient, err := dynamic.NewForConfigAndClient(restConfig, httpClient)
72+
if err != nil {
73+
t.Fatalf("building dynamic client: %v", err)
74+
}
75+
for _, obj := range objects.GetItems() {
76+
gvk := obj.GroupVersionKind()
77+
restMapping, err := restMapper.RESTMapping(gvk.GroupKind(), gvk.Version)
78+
if err != nil {
79+
t.Errorf("error getting restmapping for %v: %v", gvk, err)
80+
}
81+
82+
var applyOptions metav1.ApplyOptions
83+
applyOptions.FieldManager = "test"
84+
resource := dynamicClient.Resource(restMapping.Resource).Namespace(obj.GetNamespace())
85+
86+
if _, err := resource.Apply(ctx, obj.GetName(), obj.UnstructuredObject(), applyOptions); err != nil {
87+
t.Fatalf("error applying resource %v: %v", gvk, err)
88+
}
89+
}
90+
91+
t.Logf("replacing old url prefix %q", "http://"+restConfig.Host)
92+
requestLog.ReplaceURLPrefix("http://"+restConfig.Host, "http://kube-apiserver")
93+
requestLog.RemoveUserAgent()
94+
requestLog.SortGETs()
95+
96+
requests := requestLog.FormatHTTP()
97+
h.CompareGoldenFile(filepath.Join(testdir, "expected.yaml"), requests)
98+
})
99+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
GET http://kube-apiserver/api/v1
2+
Accept: application/json, */*
3+
4+
200 OK
5+
Cache-Control: no-cache, private
6+
Content-Length: 1820
7+
Content-Type: application/json
8+
Date: (removed)
9+
10+
{"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}]}
11+
12+
---
13+
14+
PATCH http://kube-apiserver/api/v1/namespaces/default?fieldManager=test&force=false
15+
Accept: application/json
16+
Content-Type: application/apply-patch+yaml
17+
18+
{"apiVersion":"v1","kind":"Namespace","metadata":{"name":"default"}}
19+
20+
21+
200 OK
22+
Cache-Control: no-cache, private
23+
Content-Length: 294
24+
Content-Type: application/json
25+
Date: (removed)
26+
27+
{"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"}}
28+
29+
---
30+
31+
PATCH http://kube-apiserver/api/v1/namespaces/default/configmaps/config?fieldManager=test&force=false
32+
Accept: application/json
33+
Content-Type: application/apply-patch+yaml
34+
35+
{"apiVersion":"v1","data":{"foo":"bar"},"kind":"ConfigMap","metadata":{"name":"config","namespace":"default"}}
36+
37+
38+
200 OK
39+
Cache-Control: no-cache, private
40+
Content-Length: 220
41+
Content-Type: application/json
42+
Date: (removed)
43+
44+
{"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"}}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
kind: Namespace
2+
apiVersion: v1
3+
metadata:
4+
name: default
5+
6+
---
7+
8+
kind: ConfigMap
9+
apiVersion: v1
10+
metadata:
11+
name: config
12+
namespace: default
13+
data:
14+
foo: bar
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
GET http://kube-apiserver/api/v1
2+
Accept: application/json, */*
3+
4+
200 OK
5+
Cache-Control: no-cache, private
6+
Content-Length: 1820
7+
Content-Type: application/json
8+
Date: (removed)
9+
10+
{"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}]}
11+
12+
---
13+
14+
PATCH http://kube-apiserver/api/v1/namespaces/default?fieldManager=test&force=false
15+
Accept: application/json
16+
Content-Type: application/apply-patch+yaml
17+
18+
{"apiVersion":"v1","kind":"Namespace","metadata":{"name":"default"}}
19+
20+
21+
200 OK
22+
Cache-Control: no-cache, private
23+
Content-Length: 294
24+
Content-Type: application/json
25+
Date: (removed)
26+
27+
{"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"}}
28+
29+
---
30+
31+
PATCH http://kube-apiserver/api/v1/namespaces/default/secrets/secret?fieldManager=test&force=false
32+
Accept: application/json
33+
Content-Type: application/apply-patch+yaml
34+
35+
{"apiVersion":"v1","kind":"Secret","metadata":{"name":"secret","namespace":"default"},"stringData":{"foo":"bar","foo2":"bar2"},"type":"Opaque"}
36+
37+
38+
200 OK
39+
Cache-Control: no-cache, private
40+
Content-Length: 252
41+
Content-Type: application/json
42+
Date: (removed)
43+
44+
{"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"}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
kind: Namespace
2+
apiVersion: v1
3+
metadata:
4+
name: default
5+
6+
---
7+
8+
kind: Secret
9+
apiVersion: v1
10+
metadata:
11+
name: secret
12+
namespace: default
13+
type: Opaque
14+
stringData:
15+
foo: bar
16+
foo2: bar2

0 commit comments

Comments
 (0)