Skip to content

Commit ed47bd7

Browse files
Merge pull request #931 from operator-framework/depupdate
feat(csv): detect changes to a deployment and persist them
2 parents 49690b7 + e974cc6 commit ed47bd7

File tree

8 files changed

+242
-26
lines changed

8 files changed

+242
-26
lines changed

pkg/controller/install/deployment.go

Lines changed: 57 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import (
66
log "github.com/sirupsen/logrus"
77
appsv1 "k8s.io/api/apps/v1"
88
rbac "k8s.io/api/rbac/v1"
9+
"k8s.io/apimachinery/pkg/api/equality"
10+
"k8s.io/apimachinery/pkg/util/diff"
911

1012
"github.com/operator-framework/operator-lifecycle-manager/pkg/api/apis/operators/v1alpha1"
1113
"github.com/operator-framework/operator-lifecycle-manager/pkg/api/wrappers"
@@ -61,32 +63,34 @@ func NewStrategyDeploymentInstaller(strategyClient wrappers.InstallStrategyDeplo
6163

6264
func (i *StrategyDeploymentInstaller) installDeployments(deps []StrategyDeploymentSpec) error {
6365
for _, d := range deps {
64-
dep := &appsv1.Deployment{Spec: d.Spec}
65-
dep.SetName(d.Name)
66-
dep.SetNamespace(i.owner.GetNamespace())
67-
68-
// Merge annotations (to avoid losing info from pod template)
69-
annotations := map[string]string{}
70-
for k, v := range i.templateAnnotations {
71-
annotations[k] = v
72-
}
73-
for k, v := range dep.Spec.Template.GetAnnotations() {
74-
annotations[k] = v
75-
}
76-
dep.Spec.Template.SetAnnotations(annotations)
77-
78-
ownerutil.AddNonBlockingOwner(dep, i.owner)
79-
if err := ownerutil.AddOwnerLabels(dep, i.owner); err != nil {
80-
return err
81-
}
82-
if _, err := i.strategyClient.CreateOrUpdateDeployment(dep); err != nil {
66+
if _, err := i.strategyClient.CreateOrUpdateDeployment(i.deploymentForSpec(d.Name, d.Spec)); err != nil {
8367
return err
8468
}
8569
}
8670

8771
return nil
8872
}
8973

74+
func (i *StrategyDeploymentInstaller) deploymentForSpec(name string, spec appsv1.DeploymentSpec) *appsv1.Deployment {
75+
dep := &appsv1.Deployment{Spec: spec}
76+
dep.SetName(name)
77+
dep.SetNamespace(i.owner.GetNamespace())
78+
79+
// Merge annotations (to avoid losing info from pod template)
80+
annotations := map[string]string{}
81+
for k, v := range i.templateAnnotations {
82+
annotations[k] = v
83+
}
84+
for k, v := range dep.Spec.Template.GetAnnotations() {
85+
annotations[k] = v
86+
}
87+
dep.Spec.Template.SetAnnotations(annotations)
88+
89+
ownerutil.AddNonBlockingOwner(dep, i.owner)
90+
ownerutil.AddOwnerLabelsForKind(dep, i.owner, v1alpha1.ClusterServiceVersionKind)
91+
return dep
92+
}
93+
9094
func (i *StrategyDeploymentInstaller) cleanupPrevious(current *StrategyDetailsDeployment, previous *StrategyDetailsDeployment) error {
9195
previousDeploymentsMap := map[string]struct{}{}
9296
for _, d := range previous.DeploymentSpecs {
@@ -179,10 +183,44 @@ func (i *StrategyDeploymentInstaller) checkForDeployments(deploymentSpecs []Stra
179183
return StrategyError{Reason: StrategyErrReasonAnnotationsMissing, Message: fmt.Sprintf("annotations on deployment don't match. couldn't find %s: %s", key, value)}
180184
}
181185
}
186+
187+
// check equality
188+
calculated := i.deploymentForSpec(spec.Name, spec.Spec)
189+
if !i.equalDeployments(&calculated.Spec, &dep.Spec) {
190+
return StrategyError{Reason: StrategyErrDeploymentUpdated, Message: fmt.Sprintf("deployment changed, rolling update with patch: %s\n%#v\n%#v", diff.ObjectDiff(dep.Spec.Template.Spec, calculated.Spec.Template.Spec), calculated.Spec.Template.Spec, dep.Spec.Template.Spec)}
191+
}
182192
}
183193
return nil
184194
}
185195

196+
func (i *StrategyDeploymentInstaller) equalDeployments(calculated, onCluster *appsv1.DeploymentSpec) bool {
197+
// ignore template annotations, OLM injects these elsewhere
198+
calculated.Template.Annotations = nil
199+
200+
// DeepDerivative doesn't treat `0` ints as unset. Stripping them here means we miss changes to these values,
201+
// but we don't end up getting bitten by the defaulter for deployments.
202+
for i, c := range onCluster.Template.Spec.Containers {
203+
o := calculated.Template.Spec.Containers[i]
204+
if o.ReadinessProbe != nil {
205+
o.ReadinessProbe.InitialDelaySeconds = c.ReadinessProbe.InitialDelaySeconds
206+
o.ReadinessProbe.TimeoutSeconds = c.ReadinessProbe.TimeoutSeconds
207+
o.ReadinessProbe.PeriodSeconds = c.ReadinessProbe.PeriodSeconds
208+
o.ReadinessProbe.SuccessThreshold = c.ReadinessProbe.SuccessThreshold
209+
o.ReadinessProbe.FailureThreshold = c.ReadinessProbe.FailureThreshold
210+
}
211+
if o.LivenessProbe != nil {
212+
o.LivenessProbe.InitialDelaySeconds = c.LivenessProbe.InitialDelaySeconds
213+
o.LivenessProbe.TimeoutSeconds = c.LivenessProbe.TimeoutSeconds
214+
o.LivenessProbe.PeriodSeconds = c.LivenessProbe.PeriodSeconds
215+
o.LivenessProbe.SuccessThreshold = c.LivenessProbe.SuccessThreshold
216+
o.LivenessProbe.FailureThreshold = c.LivenessProbe.FailureThreshold
217+
}
218+
}
219+
220+
// DeepDerivative ensures that, for any non-nil, non-empty value in A, the corresponding value is set in B
221+
return equality.Semantic.DeepDerivative(calculated, onCluster)
222+
}
223+
186224
// Clean up orphaned deployments after reinstalling deployments process
187225
func (i *StrategyDeploymentInstaller) cleanupOrphanedDeployments(deploymentSpecs []StrategyDeploymentSpec) error {
188226
// Map of deployments

pkg/controller/install/errors.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,15 @@ const (
99
StrategyErrReasonInvalidStrategy = "InvalidStrategy"
1010
StrategyErrReasonTimeout = "Timeout"
1111
StrategyErrReasonUnknown = "Unknown"
12+
StrategyErrBadPatch = "PatchUnsuccessful"
13+
StrategyErrDeploymentUpdated = "DeploymentUpdated"
1214
)
1315

1416
// unrecoverableErrors are the set of errors that mean we can't recover an install strategy
1517
var unrecoverableErrors = map[string]struct{}{
1618
StrategyErrReasonInvalidStrategy: {},
1719
StrategyErrReasonTimeout: {},
20+
StrategyErrBadPatch: {},
1821
}
1922

2023
// StrategyError is used to represent error types for install strategies

pkg/controller/operators/olm/operator.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1271,6 +1271,10 @@ func (a *Operator) updateInstallStatus(csv *v1alpha1.ClusterServiceVersion, inst
12711271
strategyInstalled, strategyErr := installer.CheckInstalled(strategy)
12721272
now := a.now()
12731273

1274+
if strategyErr != nil {
1275+
a.logger.WithError(strategyErr).Debug("operator not installed")
1276+
}
1277+
12741278
if strategyInstalled && apiServicesInstalled {
12751279
// if there's no error, we're successfully running
12761280
csv.SetPhaseWithEventIfChanged(v1alpha1.CSVPhaseSucceeded, v1alpha1.CSVReasonInstallSuccessful, "install strategy completed with no errors", now, a.recorder)

pkg/lib/ownerutil/util.go

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -195,21 +195,26 @@ func OwnerLabel(owner Owner, kind string) map[string]string {
195195
}
196196
}
197197

198-
// AddOwnerLabels adds ownerref-like labels to an object
198+
// AddOwnerLabels adds ownerref-like labels to an object by inferring the owner kind
199199
func AddOwnerLabels(object metav1.Object, owner Owner) error {
200200
err := InferGroupVersionKind(owner)
201201
if err != nil {
202202
return err
203203
}
204+
AddOwnerLabelsForKind(object, owner, owner.GetObjectKind().GroupVersionKind().Kind)
205+
return nil
206+
}
207+
208+
// AddOwnerLabels adds ownerref-like labels to an object, with no inference
209+
func AddOwnerLabelsForKind(object metav1.Object, owner Owner, kind string) {
204210
labels := object.GetLabels()
205211
if labels == nil {
206212
labels = map[string]string{}
207213
}
208-
for key, val := range OwnerLabel(owner, owner.GetObjectKind().GroupVersionKind().Kind) {
214+
for key, val := range OwnerLabel(owner, kind) {
209215
labels[key] = val
210216
}
211217
object.SetLabels(labels)
212-
return nil
213218
}
214219

215220
// IsOwnedByKindLabel returns whether or not a label exists on the object pointing to an owner of a particular kind

scripts/build_local.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ fi
1313

1414
docker build -f local.Dockerfile -t quay.io/operator-framework/olm:local -t quay.io/operator-framework/olm-e2e:local ./bin
1515

16-
if [ -x "$(command -v kind)" ] && [ "kubectl config current-context" -eq "kind" ]; then
16+
if [ -x "$(command -v kind)" ] && [ "$(kubectl config current-context)" = "kind" ]; then
1717
kind load docker-image quay.io/operator-framework/olm:local
1818
kind load docker-image quay.io/operator-framework/olm-e2e:local
1919
fi

test/e2e/csv_e2e_test.go

Lines changed: 167 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@ func newNginxDeployment(name string) appsv1.DeploymentSpec {
148148
Containers: []corev1.Container{
149149
{
150150
Name: genName("nginx"),
151-
Image: "bitnami/nginx:latest",
151+
Image: "redis",
152152
Ports: []corev1.ContainerPort{
153153
{
154154
ContainerPort: 80,
@@ -2393,6 +2393,172 @@ func TestUpdateCSVMultipleIntermediates(t *testing.T) {
23932393
require.NoError(t, err)
23942394
}
23952395

2396+
func TestUpdateCSVInPlace(t *testing.T) {
2397+
defer cleaner.NotifyTestComplete(t, true)
2398+
2399+
c := newKubeClient(t)
2400+
crc := newCRClient(t)
2401+
2402+
// Create dependency first (CRD)
2403+
crdPlural := genName("ins")
2404+
crdName := crdPlural + ".cluster.com"
2405+
cleanupCRD, err := createCRD(c, apiextensions.CustomResourceDefinition{
2406+
ObjectMeta: metav1.ObjectMeta{
2407+
Name: crdName,
2408+
},
2409+
Spec: apiextensions.CustomResourceDefinitionSpec{
2410+
Group: "cluster.com",
2411+
Version: "v1alpha1",
2412+
Names: apiextensions.CustomResourceDefinitionNames{
2413+
Plural: crdPlural,
2414+
Singular: crdPlural,
2415+
Kind: crdPlural,
2416+
ListKind: "list" + crdPlural,
2417+
},
2418+
Scope: "Namespaced",
2419+
},
2420+
})
2421+
2422+
// Create "current" CSV
2423+
nginxName := genName("nginx-")
2424+
strategy := install.StrategyDetailsDeployment{
2425+
DeploymentSpecs: []install.StrategyDeploymentSpec{
2426+
{
2427+
Name: genName("dep-"),
2428+
Spec: newNginxDeployment(nginxName),
2429+
},
2430+
},
2431+
}
2432+
strategyRaw, err := json.Marshal(strategy)
2433+
require.NoError(t, err)
2434+
2435+
require.NoError(t, err)
2436+
defer cleanupCRD()
2437+
csv := v1alpha1.ClusterServiceVersion{
2438+
TypeMeta: metav1.TypeMeta{
2439+
Kind: v1alpha1.ClusterServiceVersionKind,
2440+
APIVersion: v1alpha1.ClusterServiceVersionAPIVersion,
2441+
},
2442+
ObjectMeta: metav1.ObjectMeta{
2443+
Name: genName("csv"),
2444+
},
2445+
Spec: v1alpha1.ClusterServiceVersionSpec{
2446+
MinKubeVersion: "0.0.0",
2447+
InstallModes: []v1alpha1.InstallMode{
2448+
{
2449+
Type: v1alpha1.InstallModeTypeOwnNamespace,
2450+
Supported: true,
2451+
},
2452+
{
2453+
Type: v1alpha1.InstallModeTypeSingleNamespace,
2454+
Supported: true,
2455+
},
2456+
{
2457+
Type: v1alpha1.InstallModeTypeMultiNamespace,
2458+
Supported: true,
2459+
},
2460+
{
2461+
Type: v1alpha1.InstallModeTypeAllNamespaces,
2462+
Supported: true,
2463+
},
2464+
},
2465+
InstallStrategy: v1alpha1.NamedInstallStrategy{
2466+
StrategyName: install.InstallStrategyNameDeployment,
2467+
StrategySpecRaw: strategyRaw,
2468+
},
2469+
CustomResourceDefinitions: v1alpha1.CustomResourceDefinitions{
2470+
Owned: []v1alpha1.CRDDescription{
2471+
{
2472+
Name: crdName,
2473+
Version: "v1alpha1",
2474+
Kind: crdPlural,
2475+
DisplayName: crdName,
2476+
Description: "In the cluster",
2477+
},
2478+
},
2479+
},
2480+
},
2481+
}
2482+
2483+
cleanupCSV, err := createCSV(t, c, crc, csv, testNamespace, false, true)
2484+
require.NoError(t, err)
2485+
defer cleanupCSV()
2486+
2487+
// Wait for current CSV to succeed
2488+
fetchedCSV, err := fetchCSV(t, crc, csv.Name, testNamespace, csvSucceededChecker)
2489+
require.NoError(t, err)
2490+
2491+
// Should have created deployment
2492+
dep, err := c.GetDeployment(testNamespace, strategy.DeploymentSpecs[0].Name)
2493+
require.NoError(t, err)
2494+
require.NotNil(t, dep)
2495+
2496+
// Create "updated" CSV with a different image
2497+
strategyNew := strategy
2498+
strategyNew.DeploymentSpecs[0].Spec.Template.Spec.Containers = []corev1.Container{
2499+
{
2500+
Name: genName("hat"),
2501+
Image: "quay.io/coreos/mock-extension-apiserver:master",
2502+
Command: []string{"/bin/mock-extension-apiserver"},
2503+
Args: []string{
2504+
"-v=4",
2505+
"--mock-kinds",
2506+
"fedora",
2507+
"--mock-group-version",
2508+
"group.version",
2509+
"--secure-port",
2510+
"5443",
2511+
"--debug",
2512+
},
2513+
Ports: []corev1.ContainerPort{
2514+
{
2515+
ContainerPort: 5443,
2516+
},
2517+
},
2518+
ImagePullPolicy: corev1.PullIfNotPresent,
2519+
},
2520+
}
2521+
2522+
// Also set something outside the spec template - this should be ignored
2523+
var five int32 = 5
2524+
strategyNew.DeploymentSpecs[0].Spec.Replicas = &five
2525+
2526+
strategyNewRaw, err := json.Marshal(strategyNew)
2527+
require.NoError(t, err)
2528+
2529+
fetchedCSV.Spec.InstallStrategy.StrategySpecRaw = strategyNewRaw
2530+
2531+
// Update CSV directly
2532+
_, err = crc.OperatorsV1alpha1().ClusterServiceVersions(testNamespace).Update(fetchedCSV)
2533+
require.NoError(t, err)
2534+
2535+
// wait for deployment spec to be updated
2536+
err = wait.Poll(pollInterval, pollDuration, func() (bool, error) {
2537+
fetched, err := c.GetDeployment(testNamespace, strategyNew.DeploymentSpecs[0].Name)
2538+
if err != nil {
2539+
return false, err
2540+
}
2541+
fmt.Println("waiting for deployment to update...")
2542+
return fetched.Spec.Template.Spec.Containers[0].Name == strategyNew.DeploymentSpecs[0].Spec.Template.Spec.Containers[0].Name, nil
2543+
})
2544+
require.NoError(t, err)
2545+
2546+
// Wait for updated CSV to succeed
2547+
_, err = fetchCSV(t, crc, csv.Name, testNamespace, csvSucceededChecker)
2548+
require.NoError(t, err)
2549+
2550+
depUpdated, err := c.GetDeployment(testNamespace, strategyNew.DeploymentSpecs[0].Name)
2551+
require.NoError(t, err)
2552+
require.NotNil(t, depUpdated)
2553+
2554+
// Deployment should have changed even though the CSV is otherwise the same
2555+
require.Equal(t, depUpdated.Spec.Template.Spec.Containers[0].Name, strategyNew.DeploymentSpecs[0].Spec.Template.Spec.Containers[0].Name)
2556+
require.Equal(t, depUpdated.Spec.Template.Spec.Containers[0].Image, strategyNew.DeploymentSpecs[0].Spec.Template.Spec.Containers[0].Image)
2557+
2558+
// Field updated even though template spec didn't change, because it was part of a template spec change as well
2559+
require.Equal(t, *depUpdated.Spec.Replicas, *strategyNew.DeploymentSpecs[0].Spec.Replicas)
2560+
}
2561+
23962562
func TestUpdateCSVMultipleVersionCRD(t *testing.T) {
23972563
defer cleaner.NotifyTestComplete(t, true)
23982564

test/e2e/installplan_e2e_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ func newNginxInstallStrategy(name string, permissions []install.StrategyDeployme
101101
Spec: corev1.PodSpec{Containers: []corev1.Container{
102102
{
103103
Name: genName("nginx"),
104-
Image: "bitnami/nginx:latest",
104+
Image: "redis",
105105
Ports: []corev1.ContainerPort{{ContainerPort: 80}},
106106
ImagePullPolicy: corev1.PullIfNotPresent,
107107
},

test/e2e/subscription_e2e_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -210,7 +210,7 @@ var (
210210
Spec: corev1.PodSpec{Containers: []corev1.Container{
211211
{
212212
Name: genName("nginx"),
213-
Image: "bitnami/nginx:latest",
213+
Image: "redis",
214214
Ports: []corev1.ContainerPort{{ContainerPort: 80}},
215215
ImagePullPolicy: corev1.PullIfNotPresent,
216216
},

0 commit comments

Comments
 (0)