diff --git a/pkg/controller/registry/reconciler/reconciler.go b/pkg/controller/registry/reconciler/reconciler.go index 88315b5688..3b74ff1f4b 100644 --- a/pkg/controller/registry/reconciler/reconciler.go +++ b/pkg/controller/registry/reconciler/reconciler.go @@ -121,6 +121,8 @@ func Pod(source *v1alpha1.CatalogSource, name string, image string, saName strin Annotations: annotations, }, Spec: v1.PodSpec{ + // TODO: Remove this before merging + // HostNetwork: true, Containers: []v1.Container{ { Name: name, diff --git a/pkg/controller/registry/resolver/cache/cache.go b/pkg/controller/registry/resolver/cache/cache.go index e719265ff4..59fc22f80c 100644 --- a/pkg/controller/registry/resolver/cache/cache.go +++ b/pkg/controller/registry/resolver/cache/cache.go @@ -266,6 +266,7 @@ func (c *NamespacedOperatorCache) FindPreferred(preferred *SourceKey, preferredN if preferred != nil && preferred.Empty() { preferred = nil } + sorted := newSortableSnapshots(c.existing, preferred, preferredNamespace, c.snapshots) sort.Sort(sorted) for _, snapshot := range sorted.snapshots { diff --git a/pkg/controller/registry/resolver/installabletypes.go b/pkg/controller/registry/resolver/installabletypes.go index 580e3c7a32..80eb3e67ea 100644 --- a/pkg/controller/registry/resolver/installabletypes.go +++ b/pkg/controller/registry/resolver/installabletypes.go @@ -36,6 +36,11 @@ func (i *BundleInstallable) AddConstraint(c solver.Constraint) { i.constraints = append(i.constraints, c) } +func (i *BundleInstallable) AddRuntimeConstraintFailure(message string) { + msg := fmt.Sprintf("%s violates a cluster runtime constraint: %s", i.identifier, message) + i.AddConstraint(PrettyConstraint(solver.Prohibited(), msg)) +} + func (i *BundleInstallable) BundleSourceInfo() (string, string, cache.SourceKey, error) { info := strings.Split(i.identifier.String(), "/") // This should be enforced by Kube naming constraints diff --git a/pkg/controller/registry/resolver/resolver.go b/pkg/controller/registry/resolver/resolver.go index 5f865a6db9..c0aeb7d4c5 100644 --- a/pkg/controller/registry/resolver/resolver.go +++ b/pkg/controller/registry/resolver/resolver.go @@ -15,6 +15,7 @@ import ( v1alpha1listers "github.com/operator-framework/operator-lifecycle-manager/pkg/api/client/listers/operators/v1alpha1" "github.com/operator-framework/operator-lifecycle-manager/pkg/controller/registry/resolver/cache" "github.com/operator-framework/operator-lifecycle-manager/pkg/controller/registry/resolver/projection" + "github.com/operator-framework/operator-lifecycle-manager/pkg/controller/registry/resolver/runtime_constraints" "github.com/operator-framework/operator-lifecycle-manager/pkg/controller/registry/resolver/solver" "github.com/operator-framework/operator-registry/pkg/api" opregistry "github.com/operator-framework/operator-registry/pkg/registry" @@ -25,15 +26,33 @@ type OperatorResolver interface { } type SatResolver struct { - cache cache.OperatorCacheProvider - log logrus.FieldLogger + cache cache.OperatorCacheProvider + log logrus.FieldLogger + runtimeConstraintsProvider *runtime_constraints.RuntimeConstraintsProvider } -func NewDefaultSatResolver(rcp cache.SourceProvider, catsrcLister v1alpha1listers.CatalogSourceLister, logger logrus.FieldLogger) *SatResolver { - return &SatResolver{ +type SatSolverOption func(resolver *SatResolver) + +func WithRuntimeConstraintsProvider(provider *runtime_constraints.RuntimeConstraintsProvider) SatSolverOption { + return func(satSolver *SatResolver) { + if satSolver != nil { + satSolver.runtimeConstraintsProvider = provider + } + } +} + +func NewDefaultSatResolver(rcp cache.SourceProvider, catsrcLister v1alpha1listers.CatalogSourceLister, logger logrus.FieldLogger, opts ...SatSolverOption) *SatResolver { + satSolver := &SatResolver{ cache: cache.New(rcp, cache.WithLogger(logger), cache.WithCatalogSourceLister(catsrcLister)), log: logger, } + + // apply options + for _, opt := range opts { + opt(satSolver) + } + + return satSolver } type debugWriter struct { @@ -568,6 +587,16 @@ func (r *SatResolver) addInvariants(namespacedCache cache.MultiCatalogOperatorFi } packageConflictToInstallable[prop.PackageName] = append(packageConflictToInstallable[prop.PackageName], installable.Identifier()) } + + // apply runtime constraints to packages that aren't already installed + if !catalog.Virtual() && r.runtimeConstraintsProvider != nil { + for _, predicate := range r.runtimeConstraintsProvider.Constraints() { + if !predicate.Test(op) { + bundleInstallable.AddRuntimeConstraintFailure(predicate.String()) + break + } + } + } } for gvk, is := range gvkConflictToInstallable { diff --git a/pkg/controller/registry/resolver/resolver_test.go b/pkg/controller/registry/resolver/resolver_test.go index 30f633f47b..71e4872313 100644 --- a/pkg/controller/registry/resolver/resolver_test.go +++ b/pkg/controller/registry/resolver/resolver_test.go @@ -5,6 +5,8 @@ import ( "fmt" "testing" + "github.com/operator-framework/operator-lifecycle-manager/pkg/controller/registry/resolver/runtime_constraints" + "github.com/blang/semver/v4" "github.com/sirupsen/logrus" "github.com/sirupsen/logrus/hooks/test" @@ -23,6 +25,23 @@ import ( opregistry "github.com/operator-framework/operator-registry/pkg/registry" ) +type fakeSourceProvider struct { +} + +func (f *fakeSourceProvider) Sources(namespaces ...string) map[cache.SourceKey]cache.Source { + return nil +} + +type fakeCatalogSourceLister struct{} + +func (l *fakeCatalogSourceLister) List(selector labels.Selector) (ret []*v1alpha1.CatalogSource, err error) { + return nil, nil +} + +func (l *fakeCatalogSourceLister) CatalogSources(namespace string) listersv1alpha1.CatalogSourceNamespaceLister { + return nil +} + func TestSolveOperators(t *testing.T) { APISet := cache.APISet{opregistry.APIKey{Group: "g", Version: "v", Kind: "k", Plural: "ks"}: struct{}{}} Provides := APISet @@ -82,6 +101,106 @@ func TestDisjointChannelGraph(t *testing.T) { require.Error(t, err, "a unique replacement chain within a channel is required to determine the relative order between channel entries, but 2 replacement chains were found in channel \"alpha\" of package \"packageA\": packageA.side1.v2...packageA.side1.v1, packageA.side2.v2...packageA.side2.v1") } +func TestRuntimeConstraints(t *testing.T) { + const namespace = "test-namespace" + catalog := cache.SourceKey{Name: "test-catalog", Namespace: namespace} + + packageASub := newSub(namespace, "packageA", "alpha", catalog) + packageDSub := existingSub(namespace, "packageD.v1", "packageD", "alpha", catalog) + + APISet := cache.APISet{opregistry.APIKey{Group: "g", Version: "v", Kind: "k", Plural: "ks"}: struct{}{}} + + // packageA requires an API that can be provided by B or C + packageA := genOperator("packageA.v1", "0.0.1", "", "packageA", "alpha", catalog.Name, catalog.Namespace, APISet, nil, nil, "", false) + packageA.Properties = append(packageA.Properties, newLabelProperty("filterOut=yes"), newLabelProperty("theBest=yes")) + + packageB := genOperator("packageB.v1", "1.0.0", "", "packageB", "alpha", catalog.Name, catalog.Namespace, nil, APISet, nil, "", false) + packageB.Properties = append(packageB.Properties, newLabelProperty("filterOut=no"), newLabelProperty("theBest=no")) + + packageC := genOperator("packageC.v1", "1.0.0", "", "packageC", "alpha", catalog.Name, catalog.Namespace, nil, APISet, nil, "", false) + packageC.Properties = append(packageC.Properties, newLabelProperty("filterOut=no"), newLabelProperty("theBest=yes")) + + // Existing operators + packageD := genOperator("packageD.v1", "1.0.0", "", "packageD", "alpha", catalog.Name, catalog.Namespace, nil, nil, nil, "", false) + existingPackageD := existingOperator(namespace, "packageD.v1", "packageD", "alpha", "", nil, nil, nil, nil) + existingPackageD.Annotations = map[string]string{"operatorframework.io/properties": `{"properties":[{"type":"olm.package","value":{"packageName":"packageD","version":"1.0.0"}}]}`} + + testCases := []struct { + title string + runtimeConstraints []cache.Predicate + expectedOperators cache.OperatorSet + csvs []*v1alpha1.ClusterServiceVersion + subs []*v1alpha1.Subscription + snapshotEntries []*cache.Entry + err string + }{ + { + title: "No runtime constraints", + snapshotEntries: []*cache.Entry{packageA, packageB, packageC, packageD}, + runtimeConstraints: []cache.Predicate{}, + expectedOperators: cache.OperatorSet{"packageA.v1": packageA, "packageB.v1": packageB}, + csvs: nil, + subs: []*v1alpha1.Subscription{packageASub}, + err: "", + }, + { + title: "Runtime constraints only accept packages A and C", + snapshotEntries: []*cache.Entry{packageA, packageB, packageC, packageD}, + runtimeConstraints: []cache.Predicate{ + cache.LabelPredicate("theBest=yes"), + }, + expectedOperators: cache.OperatorSet{"packageA.v1": packageA, "packageC.v1": packageC}, + csvs: nil, + subs: []*v1alpha1.Subscription{packageASub}, + err: "", + }, + { + title: "Existing packages are ignored", + snapshotEntries: []*cache.Entry{packageA, packageB, packageC, packageD}, + runtimeConstraints: []cache.Predicate{ + cache.LabelPredicate("theBest=yes"), + }, + expectedOperators: cache.OperatorSet{"packageA.v1": packageA, "packageC.v1": packageC}, + csvs: []*v1alpha1.ClusterServiceVersion{existingPackageD}, + subs: []*v1alpha1.Subscription{packageASub, packageDSub}, + err: "", + }, + { + title: "Runtime constraints don't allow A", + snapshotEntries: []*cache.Entry{packageA, packageB, packageC, packageD}, + runtimeConstraints: []cache.Predicate{ + cache.LabelPredicate("filterOut=no"), + }, + expectedOperators: nil, + csvs: nil, + subs: []*v1alpha1.Subscription{packageASub}, + err: "test-catalog/test-namespace/alpha/packageA.v1 violates a cluster runtime constraint: with label: filterOut=no", + }, + } + + for _, testCase := range testCases { + provider, err := runtime_constraints.New(testCase.runtimeConstraints) + require.Nil(t, err) + satResolver := SatResolver{ + cache: cache.New(cache.StaticSourceProvider{ + catalog: &cache.Snapshot{ + Entries: testCase.snapshotEntries, + }, + }), + log: logrus.New(), + runtimeConstraintsProvider: provider, + } + operators, err := satResolver.SolveOperators([]string{namespace}, testCase.csvs, testCase.subs) + + if testCase.err != "" { + require.Containsf(t, err.Error(), testCase.err, "Test %s failed", testCase.title) + } else { + require.NoErrorf(t, err, "Test %s failed", testCase.title) + } + require.EqualValuesf(t, testCase.expectedOperators, operators, "Test %s failed", testCase.title) + } +} + func TestPropertiesAnnotationHonored(t *testing.T) { const ( namespace = "olm" @@ -2066,3 +2185,10 @@ func TestNewOperatorFromCSV(t *testing.T) { }) } } + +func newLabelProperty(label string) *api.Property { + return &api.Property{ + Type: opregistry.LabelType, + Value: fmt.Sprintf(`{"label": "%s"}`, label), + } +} diff --git a/pkg/controller/registry/resolver/runtime_constraints/provider.go b/pkg/controller/registry/resolver/runtime_constraints/provider.go new file mode 100644 index 0000000000..9454b59797 --- /dev/null +++ b/pkg/controller/registry/resolver/runtime_constraints/provider.go @@ -0,0 +1,91 @@ +package runtime_constraints + +import ( + "encoding/json" + "io/ioutil" + "os" + + "github.com/operator-framework/operator-lifecycle-manager/pkg/controller/registry/resolver/cache" + "github.com/operator-framework/operator-registry/pkg/registry" + "github.com/pkg/errors" +) + +const ( + MaxRuntimeConstraints = 10 + RuntimeConstraintEnvVarName = "RUNTIME_CONSTRAINTS" +) + +type RuntimeConstraintsProvider struct { + runtimeConstraints []cache.Predicate +} + +func (p *RuntimeConstraintsProvider) Constraints() []cache.Predicate { + return p.runtimeConstraints +} + +func New(runtimeConstraints []cache.Predicate) (*RuntimeConstraintsProvider, error) { + if len(runtimeConstraints) >= MaxRuntimeConstraints { + return nil, errors.Errorf("Too many runtime constraints defined (%d/%d)", len(runtimeConstraints), MaxRuntimeConstraints) + } + + return &RuntimeConstraintsProvider{ + runtimeConstraints: runtimeConstraints, + }, nil +} + +func NewFromEnv() (*RuntimeConstraintsProvider, error) { + runtimeConstraintsFilePath, isSet := os.LookupEnv(RuntimeConstraintEnvVarName) + if !isSet { + return nil, nil + } + return NewFromFile(runtimeConstraintsFilePath) +} + +func NewFromFile(runtimeConstraintsFilePath string) (*RuntimeConstraintsProvider, error) { + propertiesFile, err := readRuntimeConstraintsYaml(runtimeConstraintsFilePath) + if err != nil { + return nil, err + } + + // Using package type to test with + // We may only want to allow the generic constraint types once they are readym + var constraints = make([]cache.Predicate, 0) + for _, property := range propertiesFile.Properties { + rawMessage := []byte(property.Value) + switch property.Type { + case registry.PackageType: + dep := registry.PackageDependency{} + err := json.Unmarshal(rawMessage, &dep) + if err != nil { + return nil, err + } + constraints = append(constraints, cache.PkgPredicate(dep.PackageName)) + case registry.LabelType: + dep := registry.LabelDependency{} + err := json.Unmarshal(rawMessage, &dep) + if err != nil { + return nil, err + } + constraints = append(constraints, cache.LabelPredicate(dep.Label)) + } + } + + return New(constraints) +} + +func readRuntimeConstraintsYaml(yamlPath string) (*registry.PropertiesFile, error) { + // Read file + yamlFile, err := ioutil.ReadFile(yamlPath) + if err != nil { + return nil, err + } + + // Parse yaml + var propertiesFile = ®istry.PropertiesFile{} + err = json.Unmarshal(yamlFile, propertiesFile) + if err != nil { + return nil, err + } + + return propertiesFile, nil +} diff --git a/pkg/controller/registry/resolver/runtime_constraints/provider_test.go b/pkg/controller/registry/resolver/runtime_constraints/provider_test.go new file mode 100644 index 0000000000..a6bc879e12 --- /dev/null +++ b/pkg/controller/registry/resolver/runtime_constraints/provider_test.go @@ -0,0 +1,95 @@ +package runtime_constraints + +import ( + "fmt" + "os" + "testing" + + "github.com/operator-framework/operator-lifecycle-manager/pkg/controller/registry/resolver/cache" + "github.com/stretchr/testify/require" +) + +func TestNew_HappyPath(t *testing.T) { + predicates := []cache.Predicate{ + cache.PkgPredicate("etcd"), + cache.LabelPredicate("something"), + } + provider, err := New(predicates) + require.Nil(t, err) + require.Equal(t, predicates, provider.Constraints()) +} + +func TestNew_TooManyConstraints(t *testing.T) { + predicates := make([]cache.Predicate, MaxRuntimeConstraints+1) + for i := 0; i < len(predicates); i++ { + predicates[i] = cache.PkgPredicate(fmt.Sprintf("etcd-%d", i)) + } + provider, err := New(predicates) + require.NotNil(t, err) + require.Nil(t, provider) +} + +func TestNewFromFile_HappyPath(t *testing.T) { + provider, err := NewFromFile("testdata/runtime_constraints.json") + require.Nil(t, err) + require.Len(t, provider.Constraints(), 1) + require.Equal(t, provider.Constraints()[0], cache.PkgPredicate("etcd")) +} + +func TestNewFromFile_ErrorOnNotFound(t *testing.T) { + provider, err := NewFromFile("testdata/not/a/real/path.json") + require.NotNil(t, err) + require.Nil(t, provider) +} + +func TestNewFromFile_TooManyConstraints(t *testing.T) { + provider, err := NewFromFile("testdata/too_many_constraints.json") + require.NotNil(t, err) + require.Nil(t, provider) +} + +func TestNewFromEnv_HappyPath(t *testing.T) { + require.Nil(t, os.Setenv(RuntimeConstraintEnvVarName, "testdata/runtime_constraints.json")) + t.Cleanup(func() { _ = os.Unsetenv(RuntimeConstraintEnvVarName) }) + + provider, err := NewFromEnv() + require.Nil(t, err) + require.Len(t, provider.Constraints(), 1) + require.Equal(t, provider.Constraints()[0], cache.PkgPredicate("etcd")) +} + +func TestNewFromEnv_ErrorOnNotFound(t *testing.T) { + testCases := []struct { + title string + value string + }{ + { + title: "File not found", + value: "testdata/not/a/real/path.json", + }, { + title: "Bad path", + value: "fsdkljflsdk ropweiropw 4434!@#!#", + }, { + title: "No env var set", + value: "nil", + }, + } + + for _, testCase := range testCases { + if testCase.value != "nil" { + require.Nil(t, os.Setenv(RuntimeConstraintEnvVarName, testCase.value)) + t.Cleanup(func() { _ = os.Unsetenv(RuntimeConstraintEnvVarName) }) + } + provider, err := NewFromEnv() + require.NotNil(t, err) + require.Nil(t, provider) + } +} + +func TestNewFromEnv_TooManyConstraints(t *testing.T) { + require.Nil(t, os.Setenv(RuntimeConstraintEnvVarName, "testdata/too_many_constraints.json")) + t.Cleanup(func() { _ = os.Unsetenv(RuntimeConstraintEnvVarName) }) + provider, err := NewFromEnv() + require.NotNil(t, err) + require.Nil(t, provider) +} diff --git a/pkg/controller/registry/resolver/runtime_constraints/testdata/bad_runtime_constraints.json b/pkg/controller/registry/resolver/runtime_constraints/testdata/bad_runtime_constraints.json new file mode 100644 index 0000000000..dbe5d05a11 --- /dev/null +++ b/pkg/controller/registry/resolver/runtime_constraints/testdata/bad_runtime_constraints.json @@ -0,0 +1 @@ +this is a poorly formatted runtime constraints file \ No newline at end of file diff --git a/pkg/controller/registry/resolver/runtime_constraints/testdata/runtime_constraints.json b/pkg/controller/registry/resolver/runtime_constraints/testdata/runtime_constraints.json new file mode 100644 index 0000000000..4b99498386 --- /dev/null +++ b/pkg/controller/registry/resolver/runtime_constraints/testdata/runtime_constraints.json @@ -0,0 +1,11 @@ +{ + "properties": [ + { + "type": "olm.package", + "value": { + "packageName": "etcd", + "version": "1.0.0" + } + } + ] +} diff --git a/pkg/controller/registry/resolver/runtime_constraints/testdata/too_many_constraints.json b/pkg/controller/registry/resolver/runtime_constraints/testdata/too_many_constraints.json new file mode 100644 index 0000000000..265250695e --- /dev/null +++ b/pkg/controller/registry/resolver/runtime_constraints/testdata/too_many_constraints.json @@ -0,0 +1,77 @@ +{ + "properties": [ + { + "type": "olm.package", + "value": { + "packageName": "etcd12", + "version": "1.0.0" + } + }, { + "type": "olm.package", + "value": { + "packageName": "etcd11", + "version": "1.0.0" + } + }, { + "type": "olm.package", + "value": { + "packageName": "etcd10", + "version": "1.0.0" + } + }, { + "type": "olm.package", + "value": { + "packageName": "etcd9", + "version": "1.0.0" + } + }, { + "type": "olm.package", + "value": { + "packageName": "etcd8", + "version": "1.0.0" + } + }, { + "type": "olm.package", + "value": { + "packageName": "etcd7", + "version": "1.0.0" + } + }, { + "type": "olm.package", + "value": { + "packageName": "etcd6", + "version": "1.0.0" + } + }, { + "type": "olm.package", + "value": { + "packageName": "etcd5", + "version": "1.0.0" + } + }, { + "type": "olm.package", + "value": { + "packageName": "etcd4", + "version": "1.0.0" + } + }, { + "type": "olm.package", + "value": { + "packageName": "etcd3", + "version": "1.0.0" + } + }, { + "type": "olm.package", + "value": { + "packageName": "etcd2", + "version": "1.0.0" + } + }, { + "type": "olm.package", + "value": { + "packageName": "etcd1", + "version": "1.0.0" + } + } + ] +} \ No newline at end of file diff --git a/pkg/controller/registry/resolver/source_registry.go b/pkg/controller/registry/resolver/source_registry.go index ebf18a1c1a..996d7b7eb5 100644 --- a/pkg/controller/registry/resolver/source_registry.go +++ b/pkg/controller/registry/resolver/source_registry.go @@ -182,7 +182,7 @@ func legacyDependenciesToProperties(dependencies []*api.Dependency) ([]*api.Prop var result []*api.Property for _, dependency := range dependencies { switch dependency.Type { - case "olm.gvk": + case opregistry.GVKType: type gvk struct { Group string `json:"group"` Version string `json:"version"` @@ -205,7 +205,7 @@ func legacyDependenciesToProperties(dependencies []*api.Dependency) ([]*api.Prop Type: "olm.gvk.required", Value: string(vb), }) - case "olm.package": + case opregistry.PackageType: var vfrom struct { PackageName string `json:"packageName"` VersionRange string `json:"version"` @@ -228,7 +228,7 @@ func legacyDependenciesToProperties(dependencies []*api.Dependency) ([]*api.Prop Type: "olm.package.required", Value: string(vb), }) - case "olm.label": + case opregistry.LabelType: result = append(result, &api.Property{ Type: "olm.label.required", Value: dependency.Value, diff --git a/pkg/controller/registry/resolver/step_resolver.go b/pkg/controller/registry/resolver/step_resolver.go index 079b928065..09b88a1e56 100644 --- a/pkg/controller/registry/resolver/step_resolver.go +++ b/pkg/controller/registry/resolver/step_resolver.go @@ -6,6 +6,8 @@ import ( "fmt" "time" + "github.com/operator-framework/operator-lifecycle-manager/pkg/controller/registry/resolver/runtime_constraints" + "github.com/sirupsen/logrus" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/errors" @@ -48,6 +50,21 @@ var _ StepResolver = &OperatorStepResolver{} func NewOperatorStepResolver(lister operatorlister.OperatorLister, client versioned.Interface, kubeclient kubernetes.Interface, globalCatalogNamespace string, provider RegistryClientProvider, log logrus.FieldLogger) *OperatorStepResolver { + + runtimeConstraintProvider, err := runtime_constraints.NewFromEnv() + if err != nil { + log.Errorf("Error creating runtime constraints from file: %s", err) + panic(err) + } else { + log.Info("Cluster runtime constraints are in effect") + } + + satResolver := NewDefaultSatResolver( + SourceProviderFromRegistryClientProvider(provider, log), + lister.OperatorsV1alpha1().CatalogSourceLister(), + log, + WithRuntimeConstraintsProvider(runtimeConstraintProvider)) + return &OperatorStepResolver{ subLister: lister.OperatorsV1alpha1().SubscriptionLister(), csvLister: lister.OperatorsV1alpha1().ClusterServiceVersionLister(), @@ -55,7 +72,7 @@ func NewOperatorStepResolver(lister operatorlister.OperatorLister, client versio client: client, kubeclient: kubeclient, globalCatalogNamespace: globalCatalogNamespace, - satResolver: NewDefaultSatResolver(SourceProviderFromRegistryClientProvider(provider, log), lister.OperatorsV1alpha1().CatalogSourceLister(), log), + satResolver: satResolver, log: log, } } diff --git a/pkg/controller/registry/resolver/step_resolver_test.go b/pkg/controller/registry/resolver/step_resolver_test.go index f85056192f..e9d0f61a1c 100644 --- a/pkg/controller/registry/resolver/step_resolver_test.go +++ b/pkg/controller/registry/resolver/step_resolver_test.go @@ -2,10 +2,13 @@ package resolver import ( "fmt" + "os" "strings" "testing" "time" + "github.com/operator-framework/operator-lifecycle-manager/pkg/controller/registry/resolver/runtime_constraints" + "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -47,6 +50,74 @@ var ( Requires4 = APISet4 ) +func TestNewOperatorStepResolver_NoClusterRuntimeConstraints(t *testing.T) { + // Ensure no runtime constraints are loaded if the runtime constraints env + // var is not set + logger := logrus.New() + lister := operatorlister.NewLister() + + // Unset the runtime constraints file path environment variable + // signaling that no runtime constraints should be considered by the resolver + require.Nil(t, os.Unsetenv(runtime_constraints.RuntimeConstraintEnvVarName)) + resolver := NewOperatorStepResolver(lister, nil, nil, "", nil, logger) + require.Nil(t, resolver.satResolver.runtimeConstraintsProvider) +} + +func TestNewOperatorStepResolver_BadClusterRuntimeConstraintsEnvVar(t *testing.T) { + // Ensure TestNewDefaultSatResolver panics if the runtime constraints + // environment variable does not point to an existing file or valid path + lister := operatorlister.NewLister() + logger := logrus.New() + t.Cleanup(func() { _ = os.Unsetenv(runtime_constraints.RuntimeConstraintEnvVarName) }) + + // This test expects a panic to happen + defer func() { + if r := recover(); r == nil { + t.Errorf("The code did not panic") + } + }() + + // Set the runtime constraints env var to something that isn't a valid filesystem path + require.Nil(t, os.Setenv(runtime_constraints.RuntimeConstraintEnvVarName, "%#$%#$ %$#%#$%")) + _ = NewOperatorStepResolver(lister, nil, nil, "", nil, logger) +} + +func TestNewOperatorStepResolver_BadClusterRuntimeConstraintsFile(t *testing.T) { + // Ensure TestNewDefaultSatResolver panics if the runtime constraints + // environment variable points to a poorly formatted runtime constraints file + lister := operatorlister.NewLister() + logger := logrus.New() + t.Cleanup(func() { _ = os.Unsetenv(runtime_constraints.RuntimeConstraintEnvVarName) }) + + // This test expects a panic to happen + defer func() { + if r := recover(); r == nil { + t.Errorf("The code did not panic") + } + }() + + runtimeConstraintsFilePath := "runtime_constraints/testdata/bad_runtime_constraints.json" + // set the runtime constraints env var to something that isn't a valid filesystem path + require.Nil(t, os.Setenv(runtime_constraints.RuntimeConstraintEnvVarName, runtimeConstraintsFilePath)) + _ = NewOperatorStepResolver(lister, nil, nil, "", nil, logger) +} + +func TestNewOperatorStepResolver_GoodClusterRuntimeConstraintsFile(t *testing.T) { + // Ensure TestNewDefaultSatResolver loads the runtime constraints + // defined in a well formatted file point to by the runtime constraints env var + lister := operatorlister.NewLister() + logger := logrus.New() + t.Cleanup(func() { _ = os.Unsetenv(runtime_constraints.RuntimeConstraintEnvVarName) }) + + runtimeConstraintsFilePath := "runtime_constraints/testdata/runtime_constraints.json" + // set the runtime constraints env var to something that isn't a valid filesystem path + require.Nil(t, os.Setenv(runtime_constraints.RuntimeConstraintEnvVarName, runtimeConstraintsFilePath)) + resolver := NewOperatorStepResolver(lister, nil, nil, "", nil, logger) + runtimeConstraints := resolver.satResolver.runtimeConstraintsProvider.Constraints() + require.Len(t, runtimeConstraints, 1) + require.Equal(t, "with package: etcd", runtimeConstraints[0].String()) +} + func TestResolver(t *testing.T) { const namespace = "catsrc-namespace" catalog := resolvercache.SourceKey{Name: "catsrc", Namespace: namespace} diff --git a/test/e2e/runtime_constraints_e2e_test.go b/test/e2e/runtime_constraints_e2e_test.go new file mode 100644 index 0000000000..05bb7a5b8c --- /dev/null +++ b/test/e2e/runtime_constraints_e2e_test.go @@ -0,0 +1,255 @@ +package e2e + +import ( + "context" + "errors" + "fmt" + "regexp" + "strings" + "time" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + "github.com/operator-framework/operator-lifecycle-manager/pkg/controller/registry/resolver/runtime_constraints" + "github.com/operator-framework/operator-lifecycle-manager/test/e2e/ctx" + "github.com/stretchr/testify/require" + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + k8serrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + k8scontrollerclient "sigs.k8s.io/controller-runtime/pkg/client" +) + +const ( + runtimeConstraintsVolumeMountName = "runtime-constraints" + runtimeConstraintsConfigMapName = "runtime-constraints" + runtimeConstraintsFileName = "runtime_constraints.json" + defaultOlmNamespace = "operator-lifecycle-manager" + catalogOperatorName = "catalog-operator" + catalogContainerIndex = 0 +) + +var ( + olmOperatorKey = k8scontrollerclient.ObjectKey{ + Namespace: defaultOlmNamespace, + Name: catalogOperatorName, + } +) + +var _ = By + +// This suite describes the e2e tests targeting the cluster runtime constraints +// Currently, cluster runtime constraints can be applied to the resolution process +// by including an environment variable (runtime_constraints.RuntimeConstraintEnvVarName) +// that points to the yaml file with the constraints defined. +// The strategy to modify the olm-operator deployment is: +// Before each test: +// 1. Create a new config map in the operator-lifecycle-manager namespace that contains the +// runtime constraints +// 2. Update the deployment to mount the contents of the config map to /constraints/runtime_constraints.json +// 3. Update the deployment to include the environment variable pointing to the runtime constraints file +// 4. Wait for the deployment to finish updating +// +// After each test: +// 1. Delete the config map +// 2. Revert the changes made to the olm-operator deployment +// 3. Wait for the deployment to finish updating +// +// This process ensures the olm-operator has been started with the runtime constraints as defined in each test +var _ = Describe("Cluster Runtime Constraints", func() { + var ( + generatedNamespace corev1.Namespace + ) + + BeforeEach(func() { + generatedNamespace = SetupGeneratedTestNamespace(genName("runtime-constraints-e2e-")) + setupRuntimeConstraints(ctx.Ctx().Client()) + }) + + AfterEach(func() { + teardownRuntimeConstraints(ctx.Ctx().Client()) + time.Sleep(1 * time.Minute) + TeardownNamespace(generatedNamespace.GetName()) + }) + + It("Runtime", func() { + time.Sleep(2 * time.Minute) + require.Equal(GinkgoT(), true, true) + }) +}) + +func mustDeployRuntimeConstraintsConfigMap(kubeClient k8scontrollerclient.Client) { + runtimeConstraints := stripMargin(` + |{ + | "properties": [ + | { + | "type": "olm.package", + | "value": { + | "packageName": "etcd", + | "version": "1.0.0" + | } + | } + | ] + |}`) + + isImmutable := true + configMap := &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: runtimeConstraintsConfigMapName, + Namespace: defaultOlmNamespace, + }, + Immutable: &isImmutable, + Data: map[string]string{ + runtimeConstraintsFileName: runtimeConstraints, + }, + } + + err := kubeClient.Create(context.TODO(), configMap) + if err != nil { + panic(err) + } +} + +func mustUndeployRuntimeConstraintsConfigMap(kubeClient k8scontrollerclient.Client) { + if err := kubeClient.Delete(context.TODO(), &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "runtime-constraints", + Namespace: "operator-lifecycle-manager", + }, + }); err != nil { + panic(err) + } + + // Wait for config map to be removed + Eventually(func() bool { + configMap := &corev1.ConfigMap{} + err := kubeClient.Get(context.TODO(), k8scontrollerclient.ObjectKey{ + Name: runtimeConstraintsConfigMapName, + Namespace: defaultOlmNamespace, + }, configMap) + return k8serrors.IsNotFound(err) + }).Should(BeTrue()) +} + +func mustPatchCatalogOperatorDeployment(kubeClient k8scontrollerclient.Client) { + catalogDeployment := &appsv1.Deployment{} + err := kubeClient.Get(context.TODO(), olmOperatorKey, catalogDeployment) + + if err != nil { + panic(err) + } + + volumes := catalogDeployment.Spec.Template.Spec.Volumes + olmContainer := catalogDeployment.Spec.Template.Spec.Containers[0] + + newVolume := corev1.Volume{ + Name: runtimeConstraintsVolumeMountName, + VolumeSource: corev1.VolumeSource{ + ConfigMap: &corev1.ConfigMapVolumeSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: runtimeConstraintsConfigMapName, + }, + }, + }, + } + + mountPath := "/constraints" + newVolumeMount := corev1.VolumeMount{ + Name: runtimeConstraintsVolumeMountName, + MountPath: mountPath, + ReadOnly: true, + } + + catalogDeployment.Spec.Template.Spec.Volumes = append(volumes, newVolume) + catalogDeployment.Spec.Template.Spec.Containers[catalogContainerIndex].VolumeMounts = append(olmContainer.VolumeMounts, newVolumeMount) + catalogDeployment.Spec.Template.Spec.Containers[catalogContainerIndex].Env = append(olmContainer.Env, corev1.EnvVar{ + Name: runtime_constraints.RuntimeConstraintEnvVarName, + Value: fmt.Sprintf("%s/%s", mountPath, runtimeConstraintsFileName), + }) + + err = kubeClient.Update(context.TODO(), catalogDeployment) + + if err != nil { + panic(err) + } + + waitForCatalogOperatorDeploymentToUpdate(kubeClient) +} + +func mustUnpatchCatalogOperatorDeployment(kubeClient k8scontrollerclient.Client) { + catalogDeployment := &appsv1.Deployment{} + err := kubeClient.Get(context.TODO(), olmOperatorKey, catalogDeployment) + + if err != nil { + panic(err) + } + + // Remove volume + volumes := catalogDeployment.Spec.Template.Spec.Volumes + for index, volume := range volumes { + if volume.Name == runtimeConstraintsVolumeMountName { + volumes = append(volumes[:index], volumes[index+1:]...) + break + } + } + catalogDeployment.Spec.Template.Spec.Volumes = volumes + + // Remove volume mount + volumeMounts := catalogDeployment.Spec.Template.Spec.Containers[catalogContainerIndex].VolumeMounts + for index, volumeMount := range volumeMounts { + if volumeMount.Name == runtimeConstraintsVolumeMountName { + volumeMounts = append(volumeMounts[:index], volumeMounts[index+1:]...) + } + } + catalogDeployment.Spec.Template.Spec.Containers[catalogContainerIndex].VolumeMounts = volumeMounts + + // Remove environment variable + envVars := catalogDeployment.Spec.Template.Spec.Containers[catalogContainerIndex].Env + for index, envVar := range envVars { + if envVar.Name == runtime_constraints.RuntimeConstraintEnvVarName { + envVars = append(envVars[:index], envVars[index+1:]...) + } + } + catalogDeployment.Spec.Template.Spec.Containers[catalogContainerIndex].Env = envVars + + err = kubeClient.Update(context.TODO(), catalogDeployment) + + if err != nil { + panic(err) + } + + waitForCatalogOperatorDeploymentToUpdate(kubeClient) +} + +func setupRuntimeConstraints(kubeClient k8scontrollerclient.Client) { + mustDeployRuntimeConstraintsConfigMap(kubeClient) + mustPatchCatalogOperatorDeployment(kubeClient) +} + +func teardownRuntimeConstraints(kubeClient k8scontrollerclient.Client) { + mustUnpatchCatalogOperatorDeployment(kubeClient) + mustUndeployRuntimeConstraintsConfigMap(kubeClient) +} + +func stripMargin(text string) string { + regex := regexp.MustCompile(`([ \t]+)\|`) + return strings.TrimSpace(regex.ReplaceAllString(text, "")) +} + +// waitForCatalogOperatorDeploymentToUpdate waits for the olm operator deployment to be ready after an update +func waitForCatalogOperatorDeploymentToUpdate(kubeClient k8scontrollerclient.Client) { + Eventually(func() error { + deployment := &appsv1.Deployment{} + err := kubeClient.Get(context.TODO(), olmOperatorKey, deployment) + if err != nil { + return err + } + // TODO: check that this is the right way to check that a deployment + // has finished being updated + ok := deployment.Status.Replicas == deployment.Status.AvailableReplicas + if !ok { + return errors.New("deployment has not yet finished updating") + } + return nil + }).Should(BeNil()) +}