Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bug 1933779: Support InstallPlan steps upgrading existing ClusterIP Services. #2021

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
15 changes: 13 additions & 2 deletions pkg/lib/operatorclient/service.go
Expand Up @@ -28,11 +28,22 @@ func (c *Client) DeleteService(namespace, name string, options *metav1.DeleteOpt
// UpdateService will update the given Service resource.
func (c *Client) UpdateService(service *v1.Service) (*v1.Service, error) {
klog.V(4).Infof("[UPDATE Service]: %s", service.GetName())
oldSa, err := c.GetService(service.GetNamespace(), service.GetName())
old, err := c.GetService(service.GetNamespace(), service.GetName())
if err != nil {
return nil, err
}
patchBytes, err := createPatch(oldSa, service)
normalized, err := cloneAndNormalizeObject(old)
if err != nil {
return nil, fmt.Errorf("failed to normalize existing Service resource for patch: %w", err)
}
if service.Spec.ClusterIP == old.Spec.ClusterIP {
// Support updating to manifests that specify a
// ClusterIP when its value is the same as that of the
// existing Service.
service = service.DeepCopy()
service.Spec.ClusterIP = ""
}
patchBytes, err := createPatch(normalized, service)
if err != nil {
return nil, fmt.Errorf("error creating patch for Service: %v", err)
}
Expand Down
187 changes: 187 additions & 0 deletions pkg/lib/operatorclient/service_test.go
@@ -0,0 +1,187 @@
package operatorclient

import (
"testing"

corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/kubernetes/fake"
clienttesting "k8s.io/client-go/testing"

"github.com/stretchr/testify/require"
)

func TestUpdateService(t *testing.T) {
gvr := schema.GroupVersionResource{Group: "", Version: "v1", Resource: "services"}

// In a test expectation, matches any single fake client action.
var wildcard clienttesting.Action = clienttesting.ActionImpl{Verb: "wildcard!"}

for _, tc := range []struct {
Name string
Old *corev1.Service
New *corev1.Service
Expected []clienttesting.Action
}{
{
Name: "no changes",
Old: &corev1.Service{
ObjectMeta: metav1.ObjectMeta{
Name: "name",
Namespace: "namespace",
},
},
New: &corev1.Service{
ObjectMeta: metav1.ObjectMeta{
Name: "name",
Namespace: "namespace",
},
},
Expected: []clienttesting.Action{
wildcard,
clienttesting.NewPatchAction(gvr, "namespace", "name", types.StrategicMergePatchType, []byte(`{}`)),
},
},
{
Name: "resourceversion not patched",
Old: &corev1.Service{
ObjectMeta: metav1.ObjectMeta{
Name: "name",
Namespace: "namespace",
ResourceVersion: "42",
},
},
New: &corev1.Service{
ObjectMeta: metav1.ObjectMeta{
Name: "name",
Namespace: "namespace",
},
},
Expected: []clienttesting.Action{
wildcard,
clienttesting.NewPatchAction(gvr, "namespace", "name", types.StrategicMergePatchType, []byte(`{}`)),
},
},
{
Name: "clusterip not patched if omitted",
Old: &corev1.Service{
ObjectMeta: metav1.ObjectMeta{
Name: "name",
Namespace: "namespace",
},
Spec: corev1.ServiceSpec{
ClusterIP: "1.2.3.4",
},
},
New: &corev1.Service{
ObjectMeta: metav1.ObjectMeta{
Name: "name",
Namespace: "namespace",
},
},
Expected: []clienttesting.Action{
wildcard,
clienttesting.NewPatchAction(gvr, "namespace", "name", types.StrategicMergePatchType, []byte(`{}`)),
},
},
{
Name: "clusterip not patched if unchanged",
Old: &corev1.Service{
ObjectMeta: metav1.ObjectMeta{
Name: "name",
Namespace: "namespace",
},
Spec: corev1.ServiceSpec{
ClusterIP: "1.2.3.4",
},
},
New: &corev1.Service{
ObjectMeta: metav1.ObjectMeta{
Name: "name",
Namespace: "namespace",
},
Spec: corev1.ServiceSpec{
ClusterIP: "1.2.3.4",
},
},
Expected: []clienttesting.Action{
wildcard,
clienttesting.NewPatchAction(gvr, "namespace", "name", types.StrategicMergePatchType, []byte(`{}`)),
},
},
{
Name: "clusterip patched if changed", // even though the patch will be rejected due to field immutability
Old: &corev1.Service{
ObjectMeta: metav1.ObjectMeta{
Name: "name",
Namespace: "namespace",
},
Spec: corev1.ServiceSpec{
ClusterIP: "1.2.3.4",
},
},
New: &corev1.Service{
ObjectMeta: metav1.ObjectMeta{
Name: "name",
Namespace: "namespace",
},
Spec: corev1.ServiceSpec{
ClusterIP: "4.3.2.1",
},
},
Expected: []clienttesting.Action{
wildcard,
clienttesting.NewPatchAction(gvr, "namespace", "name", types.StrategicMergePatchType, []byte(`{"spec":{"clusterIP":"4.3.2.1"}}`)),
},
},
{
Name: "spec modified",
Old: &corev1.Service{
ObjectMeta: metav1.ObjectMeta{
Name: "name",
Namespace: "namespace",
},
Spec: corev1.ServiceSpec{
SessionAffinity: "None",
},
},
New: &corev1.Service{
ObjectMeta: metav1.ObjectMeta{
Name: "name",
Namespace: "namespace",
},
Spec: corev1.ServiceSpec{
SessionAffinity: "ClientIP",
},
},
Expected: []clienttesting.Action{
wildcard,
clienttesting.NewPatchAction(gvr, "namespace", "name", types.StrategicMergePatchType, []byte(`{"spec":{"sessionAffinity":"ClientIP"}}`)),
},
},
} {
t.Run(tc.Name, func(t *testing.T) {
require := require.New(t)

kube := fake.NewSimpleClientset(tc.Old)
c := &Client{
Interface: kube,
}

_, err := c.UpdateService(tc.New)
require.NoError(err)

actual := kube.Actions()
require.Len(actual, len(tc.Expected))

for i, action := range kube.Actions() {
if tc.Expected[i] == wildcard {
continue
}
require.Equal(tc.Expected[i], action)
}
})
}
}
89 changes: 89 additions & 0 deletions test/e2e/installplan_e2e_test.go
Expand Up @@ -46,6 +46,95 @@ import (
)

var _ = Describe("Install Plan", func() {
When("a ClusterIP service exists", func() {
var (
service *corev1.Service
)

BeforeEach(func() {
service = &corev1.Service{
TypeMeta: metav1.TypeMeta{
Kind: "Service",
APIVersion: "v1",
},
ObjectMeta: metav1.ObjectMeta{
Namespace: testNamespace,
Name: "test-service",
},
Spec: corev1.ServiceSpec{
Type: corev1.ServiceTypeClusterIP,
Ports: []corev1.ServicePort{
{
Port: 12345,
},
},
},
}

Expect(ctx.Ctx().Client().Create(context.Background(), service.DeepCopy())).To(Succeed())
})

AfterEach(func() {
Expect(ctx.Ctx().Client().Delete(context.Background(), service)).To(Succeed())
})

It("it can be updated by an InstallPlan step", func() {
defer cleaner.NotifyTestComplete(true)

scheme := runtime.NewScheme()
Expect(corev1.AddToScheme(scheme)).To(Succeed())
var manifest bytes.Buffer
Expect(k8sjson.NewSerializer(k8sjson.DefaultMetaFactory, scheme, scheme, false).Encode(service, &manifest)).To(Succeed())

plan := &operatorsv1alpha1.InstallPlan{
TypeMeta: metav1.TypeMeta{
Kind: operatorsv1alpha1.InstallPlanKind,
APIVersion: operatorsv1alpha1.SchemeGroupVersion.String(),
},
ObjectMeta: metav1.ObjectMeta{
Namespace: testNamespace,
Name: "test-plan",
},
Spec: operatorsv1alpha1.InstallPlanSpec{
Approval: operatorsv1alpha1.ApprovalAutomatic,
Approved: true,
ClusterServiceVersionNames: []string{},
},
Status: operatorsv1alpha1.InstallPlanStatus{
Phase: operatorsv1alpha1.InstallPlanPhaseInstalling,
CatalogSources: []string{},
Plan: []*operatorsv1alpha1.Step{
{
Status: operatorsv1alpha1.StepStatusUnknown,
Resource: operatorsv1alpha1.StepResource{
Name: service.Name,
Version: "v1",
Kind: "Service",
Manifest: manifest.String(),
},
},
},
},
}

Expect(ctx.Ctx().Client().Create(context.Background(), plan)).To(Succeed())
Expect(ctx.Ctx().Client().Status().Update(context.Background(), plan)).To(Succeed())

key, err := runtimeclient.ObjectKeyFromObject(plan)
Expect(err).ToNot(HaveOccurred())

HavePhase := func(goal operatorsv1alpha1.InstallPlanPhase) types.GomegaMatcher {
return WithTransform(func(plan *operatorsv1alpha1.InstallPlan) operatorsv1alpha1.InstallPlanPhase {
return plan.Status.Phase
}, Equal(goal))
}

Eventually(func() (*operatorsv1alpha1.InstallPlan, error) {
return plan, ctx.Ctx().Client().Get(context.Background(), key, plan)
}).Should(HavePhase(operatorsv1alpha1.InstallPlanPhaseComplete))
})
})

It("with CSVs across multiple catalog sources", func() {

defer cleaner.NotifyTestComplete(true)
Expand Down