Skip to content

Commit

Permalink
Respect .spec.upgradeConstraintPolicy
Browse files Browse the repository at this point in the history
This commit makes OLM respect `.spec.upgradeConstraintPolicy`
set on an `Operator` CR when chosing upgrade edges.

Signed-off-by: Mikalai Radchuk <[email protected]>
  • Loading branch information
m1kola committed Nov 7, 2023
1 parent 774ab74 commit 7c27170
Show file tree
Hide file tree
Showing 6 changed files with 403 additions and 20 deletions.
259 changes: 257 additions & 2 deletions internal/controllers/operator_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1062,7 +1062,7 @@ func TestOperatorUpgrade(t *testing.T) {
Resolver: solver.NewDeppySolver(controllers.NewVariableSource(cl, &fakeCatalogClient)),
}

t.Run("semver upgrade constraints", func(t *testing.T) {
t.Run("semver upgrade constraints enforcement of upgrades within major version", func(t *testing.T) {
defer featuregatetesting.SetFeatureGateDuringTest(t, features.OperatorControllerFeatureGate, features.ForceSemverUpgradeConstraints, true)()
defer func() {
require.NoError(t, cl.DeleteAllOf(ctx, &operatorsv1alpha1.Operator{}))
Expand Down Expand Up @@ -1155,7 +1155,7 @@ func TestOperatorUpgrade(t *testing.T) {
assert.Equal(t, `resolved to "quay.io/operatorhubio/[email protected]"`, cond.Message)
})

t.Run("legacy semantics upgrade constraints", func(t *testing.T) {
t.Run("legacy semantics upgrade constraints enforcement", func(t *testing.T) {
defer featuregatetesting.SetFeatureGateDuringTest(t, features.OperatorControllerFeatureGate, features.ForceSemverUpgradeConstraints, false)()
defer func() {
require.NoError(t, cl.DeleteAllOf(ctx, &operatorsv1alpha1.Operator{}))
Expand Down Expand Up @@ -1247,6 +1247,261 @@ func TestOperatorUpgrade(t *testing.T) {
assert.Equal(t, operatorsv1alpha1.ReasonSuccess, cond.Reason)
assert.Equal(t, `resolved to "quay.io/operatorhubio/[email protected]"`, cond.Message)
})

t.Run("ignore upgrade constraints", func(t *testing.T) {
for _, tt := range []struct {
name string
flagState bool
}{
{
name: "ForceSemverUpgradeConstraints feature gate enabled",
flagState: true,
},
{
name: "ForceSemverUpgradeConstraints feature gate disabled",
flagState: false,
},
} {
t.Run(tt.name, func(t *testing.T) {
defer featuregatetesting.SetFeatureGateDuringTest(t, features.OperatorControllerFeatureGate, features.ForceSemverUpgradeConstraints, tt.flagState)()
defer func() {
require.NoError(t, cl.DeleteAllOf(ctx, &operatorsv1alpha1.Operator{}))
require.NoError(t, cl.DeleteAllOf(ctx, &rukpakv1alpha1.BundleDeployment{}))
}()

opKey := types.NamespacedName{Name: fmt.Sprintf("operator-test-%s", rand.String(8))}
operator := &operatorsv1alpha1.Operator{
ObjectMeta: metav1.ObjectMeta{Name: opKey.Name},
Spec: operatorsv1alpha1.OperatorSpec{
PackageName: "prometheus",
Version: "1.0.0",
Channel: "beta",
UpgradeConstraintPolicy: operatorsv1alpha1.UpgradeConstraintPolicyIgnore,
},
}
// Create an operator
err := cl.Create(ctx, operator)
require.NoError(t, err)

// Run reconcile
res, err := reconciler.Reconcile(ctx, ctrl.Request{NamespacedName: opKey})
require.NoError(t, err)
assert.Equal(t, ctrl.Result{}, res)

// Refresh the operator after reconcile
err = cl.Get(ctx, opKey, operator)
require.NoError(t, err)

// Checking the status fields
assert.Equal(t, "quay.io/operatorhubio/[email protected]", operator.Status.ResolvedBundleResource)

// checking the expected conditions
cond := apimeta.FindStatusCondition(operator.Status.Conditions, operatorsv1alpha1.TypeResolved)
require.NotNil(t, cond)
assert.Equal(t, metav1.ConditionTrue, cond.Status)
assert.Equal(t, operatorsv1alpha1.ReasonSuccess, cond.Reason)
assert.Equal(t, `resolved to "quay.io/operatorhubio/[email protected]"`, cond.Message)

// We can go to the next major version when using semver
// as well as to the version which is not next in the channel
operator.Spec.Version = "2.0.0"
err = cl.Update(ctx, operator)
require.NoError(t, err)

// Run reconcile again
res, err = reconciler.Reconcile(ctx, ctrl.Request{NamespacedName: opKey})
require.NoError(t, err)
assert.Equal(t, ctrl.Result{}, res)

// Refresh the operator after reconcile
err = cl.Get(ctx, opKey, operator)
require.NoError(t, err)

// Checking the status fields
assert.Equal(t, "quay.io/operatorhubio/[email protected]", operator.Status.ResolvedBundleResource)

// checking the expected conditions
cond = apimeta.FindStatusCondition(operator.Status.Conditions, operatorsv1alpha1.TypeResolved)
require.NotNil(t, cond)
assert.Equal(t, metav1.ConditionTrue, cond.Status)
assert.Equal(t, operatorsv1alpha1.ReasonSuccess, cond.Reason)
assert.Equal(t, `resolved to "quay.io/operatorhubio/[email protected]"`, cond.Message)
})
}
})
}

func TestOperatorDowngrade(t *testing.T) {
ctx := context.Background()
fakeCatalogClient := testutil.NewFakeCatalogClient(testBundleList)
reconciler := &controllers.OperatorReconciler{
Client: cl,
Scheme: sch,
Resolver: solver.NewDeppySolver(controllers.NewVariableSource(cl, &fakeCatalogClient)),
}

t.Run("enforce upgrade constraints", func(t *testing.T) {
for _, tt := range []struct {
name string
flagState bool
}{
{
name: "ForceSemverUpgradeConstraints feature gate enabled",
flagState: true,
},
{
name: "ForceSemverUpgradeConstraints feature gate disabled",
flagState: false,
},
} {
t.Run(tt.name, func(t *testing.T) {
defer featuregatetesting.SetFeatureGateDuringTest(t, features.OperatorControllerFeatureGate, features.ForceSemverUpgradeConstraints, tt.flagState)()
defer func() {
require.NoError(t, cl.DeleteAllOf(ctx, &operatorsv1alpha1.Operator{}))
require.NoError(t, cl.DeleteAllOf(ctx, &rukpakv1alpha1.BundleDeployment{}))
}()

opKey := types.NamespacedName{Name: fmt.Sprintf("operator-test-%s", rand.String(8))}
operator := &operatorsv1alpha1.Operator{
ObjectMeta: metav1.ObjectMeta{Name: opKey.Name},
Spec: operatorsv1alpha1.OperatorSpec{
PackageName: "prometheus",
Version: "1.0.1",
Channel: "beta",
},
}
// Create an operator
err := cl.Create(ctx, operator)
require.NoError(t, err)

// Run reconcile
res, err := reconciler.Reconcile(ctx, ctrl.Request{NamespacedName: opKey})
require.NoError(t, err)
assert.Equal(t, ctrl.Result{}, res)

// Refresh the operator after reconcile
err = cl.Get(ctx, opKey, operator)
require.NoError(t, err)

// Checking the status fields
assert.Equal(t, "quay.io/operatorhubio/[email protected]", operator.Status.ResolvedBundleResource)

// checking the expected conditions
cond := apimeta.FindStatusCondition(operator.Status.Conditions, operatorsv1alpha1.TypeResolved)
require.NotNil(t, cond)
assert.Equal(t, metav1.ConditionTrue, cond.Status)
assert.Equal(t, operatorsv1alpha1.ReasonSuccess, cond.Reason)
assert.Equal(t, `resolved to "quay.io/operatorhubio/[email protected]"`, cond.Message)

// Invalid operation: can not downgrade
operator.Spec.Version = "1.0.0"
err = cl.Update(ctx, operator)
require.NoError(t, err)

// Run reconcile again
res, err = reconciler.Reconcile(ctx, ctrl.Request{NamespacedName: opKey})
require.Error(t, err)
assert.Equal(t, ctrl.Result{}, res)

// Refresh the operator after reconcile
err = cl.Get(ctx, opKey, operator)
require.NoError(t, err)

// Checking the status fields
// TODO: https://github.com/operator-framework/operator-controller/issues/320
assert.Equal(t, "", operator.Status.ResolvedBundleResource)

// checking the expected conditions
cond = apimeta.FindStatusCondition(operator.Status.Conditions, operatorsv1alpha1.TypeResolved)
require.NotNil(t, cond)
assert.Equal(t, metav1.ConditionFalse, cond.Status)
assert.Equal(t, operatorsv1alpha1.ReasonResolutionFailed, cond.Reason)
assert.Contains(t, cond.Message, "constraints not satisfiable")
assert.Contains(t, cond.Message, "installed package prometheus requires at least one of fake-catalog-prometheus-operatorhub/prometheus/beta/1.2.0, fake-catalog-prometheus-operatorhub/prometheus/beta/1.0.1;")
})
}
})

t.Run("ignore upgrade constraints", func(t *testing.T) {
for _, tt := range []struct {
name string
flagState bool
}{
{
name: "ForceSemverUpgradeConstraints feature gate enabled",
flagState: true,
},
{
name: "ForceSemverUpgradeConstraints feature gate disabled",
flagState: false,
},
} {
t.Run(tt.name, func(t *testing.T) {
defer featuregatetesting.SetFeatureGateDuringTest(t, features.OperatorControllerFeatureGate, features.ForceSemverUpgradeConstraints, tt.flagState)()
defer func() {
require.NoError(t, cl.DeleteAllOf(ctx, &operatorsv1alpha1.Operator{}))
require.NoError(t, cl.DeleteAllOf(ctx, &rukpakv1alpha1.BundleDeployment{}))
}()

opKey := types.NamespacedName{Name: fmt.Sprintf("operator-test-%s", rand.String(8))}
operator := &operatorsv1alpha1.Operator{
ObjectMeta: metav1.ObjectMeta{Name: opKey.Name},
Spec: operatorsv1alpha1.OperatorSpec{
PackageName: "prometheus",
Version: "2.0.0",
Channel: "beta",
UpgradeConstraintPolicy: operatorsv1alpha1.UpgradeConstraintPolicyIgnore,
},
}
// Create an operator
err := cl.Create(ctx, operator)
require.NoError(t, err)

// Run reconcile
res, err := reconciler.Reconcile(ctx, ctrl.Request{NamespacedName: opKey})
require.NoError(t, err)
assert.Equal(t, ctrl.Result{}, res)

// Refresh the operator after reconcile
err = cl.Get(ctx, opKey, operator)
require.NoError(t, err)

// Checking the status fields
assert.Equal(t, "quay.io/operatorhubio/[email protected]", operator.Status.ResolvedBundleResource)

// checking the expected conditions
cond := apimeta.FindStatusCondition(operator.Status.Conditions, operatorsv1alpha1.TypeResolved)
require.NotNil(t, cond)
assert.Equal(t, metav1.ConditionTrue, cond.Status)
assert.Equal(t, operatorsv1alpha1.ReasonSuccess, cond.Reason)
assert.Equal(t, `resolved to "quay.io/operatorhubio/[email protected]"`, cond.Message)

// We downgrade
operator.Spec.Version = "1.0.0"
err = cl.Update(ctx, operator)
require.NoError(t, err)

// Run reconcile again
res, err = reconciler.Reconcile(ctx, ctrl.Request{NamespacedName: opKey})
require.NoError(t, err)
assert.Equal(t, ctrl.Result{}, res)

// Refresh the operator after reconcile
err = cl.Get(ctx, opKey, operator)
require.NoError(t, err)

// Checking the status fields
assert.Equal(t, "quay.io/operatorhubio/[email protected]", operator.Status.ResolvedBundleResource)

// checking the expected conditions
cond = apimeta.FindStatusCondition(operator.Status.Conditions, operatorsv1alpha1.TypeResolved)
require.NotNil(t, cond)
assert.Equal(t, metav1.ConditionTrue, cond.Status)
assert.Equal(t, operatorsv1alpha1.ReasonSuccess, cond.Reason)
assert.Equal(t, `resolved to "quay.io/operatorhubio/[email protected]"`, cond.Message)
})
}
})
}

var (
Expand Down
2 changes: 1 addition & 1 deletion internal/controllers/variable_source.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ func (v *VariableSource) GetVariables(ctx context.Context) ([]deppy.Variable, er
return variablesources.NewOperatorVariableSource(operatorList.Items, allBundles, inputVariableSource), nil
},
func(inputVariableSource input.VariableSource) (input.VariableSource, error) {
return variablesources.NewBundleDeploymentVariableSource(bundleDeploymentList.Items, allBundles, inputVariableSource), nil
return variablesources.NewBundleDeploymentVariableSource(operatorList.Items, bundleDeploymentList.Items, allBundles, inputVariableSource), nil
},
func(inputVariableSource input.VariableSource) (input.VariableSource, error) {
return variablesources.NewBundlesAndDepsVariableSource(allBundles, inputVariableSource), nil
Expand Down
7 changes: 5 additions & 2 deletions internal/resolution/variablesources/bundle_deployment.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,22 @@ import (
"github.com/operator-framework/deppy/pkg/deppy/input"
rukpakv1alpha1 "github.com/operator-framework/rukpak/api/v1alpha1"

operatorsv1alpha1 "github.com/operator-framework/operator-controller/api/v1alpha1"
"github.com/operator-framework/operator-controller/internal/catalogmetadata"
)

var _ input.VariableSource = &BundleDeploymentVariableSource{}

type BundleDeploymentVariableSource struct {
operators []operatorsv1alpha1.Operator
bundleDeployments []rukpakv1alpha1.BundleDeployment
allBundles []*catalogmetadata.Bundle
inputVariableSource input.VariableSource
}

func NewBundleDeploymentVariableSource(bundleDeployments []rukpakv1alpha1.BundleDeployment, allBundles []*catalogmetadata.Bundle, inputVariableSource input.VariableSource) *BundleDeploymentVariableSource {
func NewBundleDeploymentVariableSource(operators []operatorsv1alpha1.Operator, bundleDeployments []rukpakv1alpha1.BundleDeployment, allBundles []*catalogmetadata.Bundle, inputVariableSource input.VariableSource) *BundleDeploymentVariableSource {
return &BundleDeploymentVariableSource{
operators: operators,
bundleDeployments: bundleDeployments,
allBundles: allBundles,
inputVariableSource: inputVariableSource,
Expand All @@ -37,7 +40,7 @@ func (o *BundleDeploymentVariableSource) GetVariables(ctx context.Context) ([]de
return nil, err
}

installedPackages, err := MakeInstalledPackageVariables(o.allBundles, o.bundleDeployments)
installedPackages, err := MakeInstalledPackageVariables(o.allBundles, o.operators, o.bundleDeployments)
if err != nil {
return nil, err
}
Expand Down
Loading

0 comments on commit 7c27170

Please sign in to comment.