mirror of
https://github.com/edgelesssys/constellation.git
synced 2024-12-12 09:24:24 -05:00
490 lines
12 KiB
Go
490 lines
12 KiB
Go
/*
|
|
Copyright (c) Edgeless Systems GmbH
|
|
|
|
SPDX-License-Identifier: AGPL-3.0-only
|
|
*/
|
|
|
|
package client
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"errors"
|
|
"io"
|
|
"net/http"
|
|
"testing"
|
|
|
|
"github.com/edgelesssys/constellation/v2/bootstrapper/internal/kubernetes/k8sapi/resources"
|
|
"github.com/edgelesssys/constellation/v2/internal/kubernetes"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"go.uber.org/goleak"
|
|
"google.golang.org/protobuf/proto"
|
|
appsv1 "k8s.io/api/apps/v1"
|
|
corev1 "k8s.io/api/core/v1"
|
|
k8s "k8s.io/api/core/v1"
|
|
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
|
|
apiextensionsclientv1 "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1"
|
|
"k8s.io/apimachinery/pkg/api/meta"
|
|
"k8s.io/apimachinery/pkg/api/meta/testrestmapper"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/apimachinery/pkg/runtime"
|
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
|
"k8s.io/apimachinery/pkg/runtime/serializer/json"
|
|
"k8s.io/apimachinery/pkg/watch"
|
|
"k8s.io/cli-runtime/pkg/resource"
|
|
"k8s.io/client-go/kubernetes/fake"
|
|
"k8s.io/client-go/kubernetes/scheme"
|
|
restfake "k8s.io/client-go/rest/fake"
|
|
"k8s.io/client-go/restmapper"
|
|
)
|
|
|
|
func TestMain(m *testing.M) {
|
|
goleak.VerifyTestMain(m)
|
|
}
|
|
|
|
var (
|
|
corev1GV = schema.GroupVersion{Version: "v1"}
|
|
nginxDeployment = &appsv1.Deployment{
|
|
TypeMeta: metav1.TypeMeta{
|
|
APIVersion: "apps/v1",
|
|
Kind: "Deployment",
|
|
},
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Labels: map[string]string{
|
|
"app": "nginx",
|
|
},
|
|
Name: "my-nginx",
|
|
},
|
|
Spec: appsv1.DeploymentSpec{
|
|
Replicas: proto.Int32(3),
|
|
Selector: &metav1.LabelSelector{
|
|
MatchLabels: map[string]string{
|
|
"app": "nginx",
|
|
},
|
|
},
|
|
Template: k8s.PodTemplateSpec{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Labels: map[string]string{
|
|
"app": "nginx",
|
|
},
|
|
},
|
|
Spec: k8s.PodSpec{
|
|
Containers: []k8s.Container{
|
|
{
|
|
Name: "nginx",
|
|
Image: "nginx:1.14.2",
|
|
Ports: []k8s.ContainerPort{
|
|
{
|
|
ContainerPort: 80,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
tolerationsDeployment = appsv1.Deployment{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Namespace: "test-ns",
|
|
Name: "test-deployment",
|
|
},
|
|
}
|
|
selectorsDeployment = appsv1.Deployment{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Namespace: "test-ns",
|
|
Name: "test-deployment",
|
|
},
|
|
Spec: appsv1.DeploymentSpec{
|
|
Template: k8s.PodTemplateSpec{
|
|
Spec: k8s.PodSpec{
|
|
NodeSelector: map[string]string{},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
nginxDeplJSON, _ = marshalJSON(nginxDeployment)
|
|
nginxDeplYAML, _ = marshalYAML(nginxDeployment)
|
|
)
|
|
|
|
type unmarshableResource struct{}
|
|
|
|
func (*unmarshableResource) Marshal() ([]byte, error) {
|
|
return nil, errors.New("someErr")
|
|
}
|
|
|
|
func stringBody(body string) io.ReadCloser {
|
|
return io.NopCloser(bytes.NewReader([]byte(body)))
|
|
}
|
|
|
|
func fakeClientWith(t *testing.T, testName string, data map[string]string) resource.FakeClientFunc {
|
|
return func(version schema.GroupVersion) (resource.RESTClient, error) {
|
|
return &restfake.RESTClient{
|
|
GroupVersion: corev1GV,
|
|
NegotiatedSerializer: scheme.Codecs.WithoutConversion(),
|
|
Client: restfake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
|
|
p := req.URL.Path
|
|
q := req.URL.RawQuery
|
|
if len(q) != 0 {
|
|
p = p + "?" + q
|
|
}
|
|
body, ok := data[p]
|
|
if !ok {
|
|
t.Fatalf("%s: unexpected request: %s (%s)\n%#v", testName, p, req.URL, req)
|
|
}
|
|
header := http.Header{}
|
|
header.Set("Content-Type", runtime.ContentTypeJSON)
|
|
return &http.Response{
|
|
StatusCode: http.StatusOK,
|
|
Header: header,
|
|
Body: stringBody(body),
|
|
}, nil
|
|
}),
|
|
}, nil
|
|
}
|
|
}
|
|
|
|
func newClientWithFakes(t *testing.T, data map[string]string, objects ...runtime.Object) Client {
|
|
clientset := fake.NewSimpleClientset(objects...)
|
|
builder := resource.NewFakeBuilder(
|
|
fakeClientWith(t, "", data),
|
|
func() (meta.RESTMapper, error) {
|
|
return testrestmapper.TestOnlyStaticRESTMapper(scheme.Scheme), nil
|
|
},
|
|
func() (restmapper.CategoryExpander, error) {
|
|
return resource.FakeCategoryExpander, nil
|
|
}).
|
|
Unstructured()
|
|
client := Client{
|
|
clientset: clientset,
|
|
builder: builder,
|
|
}
|
|
return client
|
|
}
|
|
|
|
func failingClient() resource.FakeClientFunc {
|
|
return func(version schema.GroupVersion) (resource.RESTClient, error) {
|
|
return &restfake.RESTClient{
|
|
GroupVersion: corev1GV,
|
|
NegotiatedSerializer: scheme.Codecs.WithoutConversion(),
|
|
Resp: &http.Response{StatusCode: 501},
|
|
}, nil
|
|
}
|
|
}
|
|
|
|
func newFailingClient(objects ...runtime.Object) Client {
|
|
clientset := fake.NewSimpleClientset(objects...)
|
|
builder := resource.NewFakeBuilder(
|
|
failingClient(),
|
|
func() (meta.RESTMapper, error) {
|
|
return testrestmapper.TestOnlyStaticRESTMapper(scheme.Scheme), nil
|
|
},
|
|
func() (restmapper.CategoryExpander, error) {
|
|
return resource.FakeCategoryExpander, nil
|
|
}).
|
|
WithScheme(scheme.Scheme, scheme.Scheme.PrioritizedVersionsAllGroups()...)
|
|
client := Client{
|
|
clientset: clientset,
|
|
builder: builder,
|
|
}
|
|
return client
|
|
}
|
|
|
|
func marshalJSON(obj runtime.Object) ([]byte, error) {
|
|
serializer := json.NewSerializer(json.DefaultMetaFactory, nil, nil, false)
|
|
var buf bytes.Buffer
|
|
if err := serializer.Encode(obj, &buf); err != nil {
|
|
return nil, err
|
|
}
|
|
return buf.Bytes(), nil
|
|
}
|
|
|
|
func marshalYAML(obj runtime.Object) ([]byte, error) {
|
|
serializer := json.NewYAMLSerializer(json.DefaultMetaFactory, nil, nil)
|
|
var buf bytes.Buffer
|
|
if err := serializer.Encode(obj, &buf); err != nil {
|
|
return nil, err
|
|
}
|
|
return buf.Bytes(), nil
|
|
}
|
|
|
|
func TestApplyOneObject(t *testing.T) {
|
|
testCases := map[string]struct {
|
|
httpResponseData map[string]string
|
|
wantObj runtime.Object
|
|
resourcesYAML string
|
|
failingClient bool
|
|
wantErr bool
|
|
}{
|
|
"apply works": {
|
|
httpResponseData: map[string]string{
|
|
"/deployments/my-nginx?fieldManager=constellation-bootstrapper&force=true": string(nginxDeplJSON),
|
|
},
|
|
wantObj: nginxDeployment,
|
|
resourcesYAML: string(nginxDeplYAML),
|
|
wantErr: false,
|
|
},
|
|
"apply fails": {
|
|
httpResponseData: map[string]string{},
|
|
wantObj: nginxDeployment,
|
|
resourcesYAML: string(nginxDeplYAML),
|
|
failingClient: true,
|
|
wantErr: true,
|
|
},
|
|
}
|
|
|
|
for name, tc := range testCases {
|
|
t.Run(name, func(t *testing.T) {
|
|
assert := assert.New(t)
|
|
require := require.New(t)
|
|
|
|
var client Client
|
|
if tc.failingClient {
|
|
client = newFailingClient(tc.wantObj)
|
|
} else {
|
|
client = newClientWithFakes(t, tc.httpResponseData, tc.wantObj)
|
|
}
|
|
|
|
reader := bytes.NewReader([]byte(tc.resourcesYAML))
|
|
res := client.builder.
|
|
ContinueOnError().
|
|
Stream(reader, "yaml").
|
|
Flatten().
|
|
Do()
|
|
assert.NoError(res.Err())
|
|
infos, err := res.Infos()
|
|
assert.NoError(err)
|
|
require.Len(infos, 1)
|
|
|
|
err = client.ApplyOneObject(infos[0], true)
|
|
|
|
if tc.wantErr {
|
|
assert.Error(err)
|
|
return
|
|
}
|
|
require.NoError(err)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGetObjects(t *testing.T) {
|
|
testCases := map[string]struct {
|
|
wantResources kubernetes.Marshaler
|
|
httpResponseData map[string]string
|
|
resourcesYAML string
|
|
wantErr bool
|
|
}{
|
|
"GetObjects works on cluster-autoscaler deployment": {
|
|
wantResources: resources.NewDefaultAutoscalerDeployment(nil, nil, nil, ""),
|
|
resourcesYAML: string(nginxDeplYAML),
|
|
wantErr: false,
|
|
},
|
|
"GetObjects works on cloud-controller-manager deployment": {
|
|
wantResources: resources.NewDefaultCloudControllerManagerDeployment("someProvider", "someImage", "somePath", "someCIDR", nil, nil, nil, nil),
|
|
resourcesYAML: string(nginxDeplYAML),
|
|
wantErr: false,
|
|
},
|
|
"GetObjects Marshal failure detected": {
|
|
wantResources: &unmarshableResource{},
|
|
resourcesYAML: string(nginxDeplYAML),
|
|
wantErr: true,
|
|
},
|
|
}
|
|
|
|
for name, tc := range testCases {
|
|
t.Run(name, func(t *testing.T) {
|
|
assert := assert.New(t)
|
|
require := require.New(t)
|
|
|
|
client := newClientWithFakes(t, tc.httpResponseData)
|
|
infos, err := client.GetObjects(tc.wantResources)
|
|
|
|
if tc.wantErr {
|
|
assert.Error(err)
|
|
return
|
|
}
|
|
require.NoError(err)
|
|
assert.NotNil(infos)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestAddTolerationsToDeployment(t *testing.T) {
|
|
testCases := map[string]struct {
|
|
namespace string
|
|
name string
|
|
tolerations []corev1.Toleration
|
|
wantErr bool
|
|
}{
|
|
"Success": {
|
|
namespace: "test-ns",
|
|
name: "test-deployment",
|
|
},
|
|
"Specifying non-existent deployment fails": {
|
|
namespace: "test-ns",
|
|
name: "wrong-name",
|
|
wantErr: true,
|
|
},
|
|
"Wrong namespace": {
|
|
name: "test-deployment",
|
|
wantErr: true,
|
|
},
|
|
}
|
|
|
|
for name, tc := range testCases {
|
|
t.Run(name, func(t *testing.T) {
|
|
assert := assert.New(t)
|
|
require := require.New(t)
|
|
|
|
client := newClientWithFakes(t, map[string]string{}, &tolerationsDeployment)
|
|
err := client.AddTolerationsToDeployment(context.Background(), tc.tolerations, tc.name, tc.namespace)
|
|
if tc.wantErr {
|
|
assert.Error(err)
|
|
return
|
|
}
|
|
require.NoError(err)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestAddNodeSelectorsToDeployment(t *testing.T) {
|
|
testCases := map[string]struct {
|
|
namespace string
|
|
name string
|
|
selectors map[string]string
|
|
wantErr bool
|
|
}{
|
|
"Success": {
|
|
namespace: "test-ns",
|
|
name: "test-deployment",
|
|
selectors: map[string]string{"some-key": "some-value"},
|
|
},
|
|
"Specifying non-existent deployment fails": {
|
|
namespace: "test-ns",
|
|
name: "wrong-name",
|
|
wantErr: true,
|
|
},
|
|
"Wrong namespace": {
|
|
name: "test-deployment",
|
|
wantErr: true,
|
|
},
|
|
}
|
|
|
|
for name, tc := range testCases {
|
|
t.Run(name, func(t *testing.T) {
|
|
assert := assert.New(t)
|
|
require := require.New(t)
|
|
|
|
client := newClientWithFakes(t, map[string]string{}, &selectorsDeployment)
|
|
err := client.AddNodeSelectorsToDeployment(context.Background(), tc.selectors, tc.name, tc.namespace)
|
|
if tc.wantErr {
|
|
assert.Error(err)
|
|
return
|
|
}
|
|
require.NoError(err)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestWaitForCRD(t *testing.T) {
|
|
testCases := map[string]struct {
|
|
crd string
|
|
events []watch.Event
|
|
watchErr error
|
|
wantErr bool
|
|
}{
|
|
"Success": {
|
|
crd: "test-crd",
|
|
events: []watch.Event{
|
|
{
|
|
Type: watch.Added,
|
|
Object: &apiextensionsv1.CustomResourceDefinition{
|
|
Status: apiextensionsv1.CustomResourceDefinitionStatus{
|
|
Conditions: []apiextensionsv1.CustomResourceDefinitionCondition{
|
|
{
|
|
Type: apiextensionsv1.Established,
|
|
Status: apiextensionsv1.ConditionTrue,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
"watch error": {
|
|
crd: "test-crd",
|
|
watchErr: errors.New("watch error"),
|
|
wantErr: true,
|
|
},
|
|
"crd deleted": {
|
|
crd: "test-crd",
|
|
events: []watch.Event{{Type: watch.Deleted}},
|
|
wantErr: true,
|
|
},
|
|
"other error": {
|
|
crd: "test-crd",
|
|
events: []watch.Event{{Type: watch.Error}},
|
|
wantErr: true,
|
|
},
|
|
}
|
|
|
|
for name, tc := range testCases {
|
|
t.Run(name, func(t *testing.T) {
|
|
assert := assert.New(t)
|
|
require := require.New(t)
|
|
|
|
client := Client{
|
|
apiextensionClient: &stubCRDWatcher{events: tc.events, watchErr: tc.watchErr},
|
|
}
|
|
err := client.WaitForCRD(context.Background(), tc.crd)
|
|
if tc.wantErr {
|
|
assert.Error(err)
|
|
return
|
|
}
|
|
require.NoError(err)
|
|
})
|
|
}
|
|
}
|
|
|
|
type stubCRDWatcher struct {
|
|
events []watch.Event
|
|
watchErr error
|
|
|
|
apiextensionsclientv1.ApiextensionsV1Interface
|
|
}
|
|
|
|
func (w *stubCRDWatcher) CustomResourceDefinitions() apiextensionsclientv1.CustomResourceDefinitionInterface {
|
|
return &stubCustomResourceDefinitions{
|
|
events: w.events,
|
|
watchErr: w.watchErr,
|
|
}
|
|
}
|
|
|
|
type stubCustomResourceDefinitions struct {
|
|
events []watch.Event
|
|
watchErr error
|
|
|
|
apiextensionsclientv1.CustomResourceDefinitionInterface
|
|
}
|
|
|
|
func (c *stubCustomResourceDefinitions) Watch(ctx context.Context, opts metav1.ListOptions) (watch.Interface, error) {
|
|
eventChan := make(chan watch.Event, len(c.events))
|
|
for _, event := range c.events {
|
|
eventChan <- event
|
|
}
|
|
return &stubCRDWatch{events: eventChan}, c.watchErr
|
|
}
|
|
|
|
type stubCRDWatch struct {
|
|
events chan watch.Event
|
|
}
|
|
|
|
func (w *stubCRDWatch) Stop() {
|
|
close(w.events)
|
|
}
|
|
|
|
func (w *stubCRDWatch) ResultChan() <-chan watch.Event {
|
|
return w.events
|
|
}
|