08961919b5
- Previously the golang code would create a Service and an APIService. The APIService would be given an owner reference which pointed to the namespace in which the app was installed. - This prevented the app from being uninstalled. The namespace would refuse to delete, so `kapp delete` or `kubectl delete` would fail. - The new approach is to statically define the Service and an APIService in the deployment.yaml, except for the caBundle of the APIService. Then the golang code will perform an update to add the caBundle at runtime. - When the user uses `kapp deploy` or `kubectl apply` either tool will notice that the caBundle is not declared in the yaml and will therefore avoid editing that field. - When the user uses `kapp delete` or `kubectl delete` either tool will destroy the objects because they are statically declared with names in the yaml, just like all of the other objects. There are no ownerReferences used, so nothing should prevent the namespace from being deleted. - This approach also allows us to have less golang code to maintain. - In the future, if our golang controllers want to dynamically add an Ingress or other objects, they can still do that. An Ingress would point to our statically defined Service as its backend. Signed-off-by: Andrew Keesler <akeesler@vmware.com>
170 lines
5.6 KiB
Go
170 lines
5.6 KiB
Go
/*
|
|
Copyright 2020 VMware, Inc.
|
|
SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
package autoregistration
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/apimachinery/pkg/runtime"
|
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
|
kubetesting "k8s.io/client-go/testing"
|
|
apiregistrationv1 "k8s.io/kube-aggregator/pkg/apis/apiregistration/v1"
|
|
aggregationv1fake "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset/fake"
|
|
)
|
|
|
|
func TestUpdateAPIService(t *testing.T) {
|
|
const apiServiceName = "v1alpha1.placeholder.suzerain-io.github.io"
|
|
|
|
tests := []struct {
|
|
name string
|
|
mocks func(*aggregationv1fake.Clientset)
|
|
caInput []byte
|
|
wantObjects []apiregistrationv1.APIService
|
|
wantErr string
|
|
}{
|
|
{
|
|
name: "happy path update when the pre-existing APIService did not already have a CA bundle",
|
|
mocks: func(c *aggregationv1fake.Clientset) {
|
|
_ = c.Tracker().Add(&apiregistrationv1.APIService{
|
|
ObjectMeta: metav1.ObjectMeta{Name: apiServiceName},
|
|
Spec: apiregistrationv1.APIServiceSpec{
|
|
GroupPriorityMinimum: 999,
|
|
CABundle: nil,
|
|
},
|
|
})
|
|
},
|
|
caInput: []byte("some-ca-bundle"),
|
|
wantObjects: []apiregistrationv1.APIService{{
|
|
ObjectMeta: metav1.ObjectMeta{Name: apiServiceName},
|
|
Spec: apiregistrationv1.APIServiceSpec{
|
|
GroupPriorityMinimum: 999,
|
|
CABundle: []byte("some-ca-bundle"),
|
|
},
|
|
}},
|
|
},
|
|
{
|
|
name: "happy path update when the pre-existing APIService already had a CA bundle",
|
|
mocks: func(c *aggregationv1fake.Clientset) {
|
|
_ = c.Tracker().Add(&apiregistrationv1.APIService{
|
|
ObjectMeta: metav1.ObjectMeta{Name: apiServiceName},
|
|
Spec: apiregistrationv1.APIServiceSpec{
|
|
GroupPriorityMinimum: 999,
|
|
CABundle: []byte("some-other-different-ca-bundle"),
|
|
},
|
|
})
|
|
},
|
|
caInput: []byte("some-ca-bundle"),
|
|
wantObjects: []apiregistrationv1.APIService{{
|
|
ObjectMeta: metav1.ObjectMeta{Name: apiServiceName},
|
|
Spec: apiregistrationv1.APIServiceSpec{
|
|
GroupPriorityMinimum: 999,
|
|
CABundle: []byte("some-ca-bundle"),
|
|
},
|
|
}},
|
|
},
|
|
{
|
|
name: "error on update",
|
|
mocks: func(c *aggregationv1fake.Clientset) {
|
|
_ = c.Tracker().Add(&apiregistrationv1.APIService{
|
|
ObjectMeta: metav1.ObjectMeta{Name: apiServiceName},
|
|
Spec: apiregistrationv1.APIServiceSpec{},
|
|
})
|
|
c.PrependReactor("update", "apiservices", func(_ kubetesting.Action) (bool, runtime.Object, error) {
|
|
return true, nil, fmt.Errorf("error on update")
|
|
})
|
|
},
|
|
wantErr: "could not update API service: error on update",
|
|
},
|
|
{
|
|
name: "error on get",
|
|
mocks: func(c *aggregationv1fake.Clientset) {
|
|
_ = c.Tracker().Add(&apiregistrationv1.APIService{
|
|
ObjectMeta: metav1.ObjectMeta{Name: apiServiceName},
|
|
Spec: apiregistrationv1.APIServiceSpec{},
|
|
})
|
|
c.PrependReactor("get", "apiservices", func(_ kubetesting.Action) (bool, runtime.Object, error) {
|
|
return true, nil, fmt.Errorf("error on get")
|
|
})
|
|
},
|
|
caInput: []byte("some-ca-bundle"),
|
|
wantErr: "could not update API service: could not get existing version of API service: error on get",
|
|
},
|
|
{
|
|
name: "conflict error on update, followed by successful retry",
|
|
mocks: func(c *aggregationv1fake.Clientset) {
|
|
_ = c.Tracker().Add(&apiregistrationv1.APIService{
|
|
ObjectMeta: metav1.ObjectMeta{Name: apiServiceName},
|
|
Spec: apiregistrationv1.APIServiceSpec{
|
|
GroupPriorityMinimum: 111,
|
|
CABundle: nil,
|
|
},
|
|
})
|
|
hit := false
|
|
c.PrependReactor("update", "apiservices", func(_ kubetesting.Action) (bool, runtime.Object, error) {
|
|
// Return an error on the first call, then fall through to the default (successful) response.
|
|
if !hit {
|
|
// Before the update fails, also change the object that will be returned by the next Get(),
|
|
// to make sure that the production code does a fresh Get() after detecting a conflict.
|
|
_ = c.Tracker().Update(schema.GroupVersionResource{
|
|
Group: apiregistrationv1.GroupName,
|
|
Version: apiregistrationv1.SchemeGroupVersion.Version,
|
|
Resource: "apiservices",
|
|
}, &apiregistrationv1.APIService{
|
|
ObjectMeta: metav1.ObjectMeta{Name: apiServiceName},
|
|
Spec: apiregistrationv1.APIServiceSpec{
|
|
GroupPriorityMinimum: 222,
|
|
CABundle: nil,
|
|
},
|
|
}, "")
|
|
hit = true
|
|
return true, nil, apierrors.NewConflict(schema.GroupResource{
|
|
Group: apiregistrationv1.GroupName,
|
|
Resource: "apiservices",
|
|
}, apiServiceName, fmt.Errorf("there was a conflict"))
|
|
}
|
|
return false, nil, nil
|
|
})
|
|
},
|
|
caInput: []byte("some-ca-bundle"),
|
|
wantObjects: []apiregistrationv1.APIService{{
|
|
ObjectMeta: metav1.ObjectMeta{Name: apiServiceName},
|
|
Spec: apiregistrationv1.APIServiceSpec{
|
|
GroupPriorityMinimum: 222,
|
|
CABundle: []byte("some-ca-bundle"),
|
|
},
|
|
}},
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
tt := tt
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
ctx := context.Background()
|
|
|
|
client := aggregationv1fake.NewSimpleClientset()
|
|
if tt.mocks != nil {
|
|
tt.mocks(client)
|
|
}
|
|
|
|
err := UpdateAPIService(ctx, client, tt.caInput)
|
|
if tt.wantErr != "" {
|
|
require.EqualError(t, err, tt.wantErr)
|
|
return
|
|
}
|
|
require.NoError(t, err)
|
|
if tt.wantObjects != nil {
|
|
objects, err := client.ApiregistrationV1().APIServices().List(ctx, metav1.ListOptions{})
|
|
require.NoError(t, err)
|
|
require.Equal(t, tt.wantObjects, objects.Items)
|
|
}
|
|
})
|
|
}
|
|
}
|