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

✨ Respect upgrade constraint policy set on Operator #520

Merged
merged 1 commit into from
Nov 7, 2023
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
260 changes: 258 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/prometheus@fake1.2.0"`, 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,262 @@ func TestOperatorUpgrade(t *testing.T) {
assert.Equal(t, operatorsv1alpha1.ReasonSuccess, cond.Reason)
assert.Equal(t, `resolved to "quay.io/operatorhubio/prometheus@fake1.0.1"`, 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/prometheus@fake1.0.0", 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/prometheus@fake1.0.0"`, 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
// when using legacy constraints
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/prometheus@fake2.0.0", 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/prometheus@fake2.0.0"`, 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/prometheus@fake1.0.1", 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/prometheus@fake1.0.1"`, 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/prometheus@fake2.0.0", 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/prometheus@fake2.0.0"`, 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/prometheus@fake1.0.0", 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/prometheus@fake1.0.0"`, 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