Skip to content
2 changes: 2 additions & 0 deletions pkg/controller/registry/reconciler/reconciler.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions pkg/controller/registry/resolver/cache/cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
5 changes: 5 additions & 0 deletions pkg/controller/registry/resolver/installabletypes.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
37 changes: 33 additions & 4 deletions pkg/controller/registry/resolver/resolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down
126 changes: 126 additions & 0 deletions pkg/controller/registry/resolver/resolver_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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),
}
}
91 changes: 91 additions & 0 deletions pkg/controller/registry/resolver/runtime_constraints/provider.go
Original file line number Diff line number Diff line change
@@ -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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you test how this file-reading code will work in real life? How is this file gonna be "injected" into the process?

Copy link
Collaborator Author

@perdasilva perdasilva Nov 30, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've tested it manually. I still need to add unit tests for this. I'm thinking the way to side-load it should be we have an environment variable that points to the path of the constraints file. Downstream, the file will be baked in to the olm operator image and we'll update the deployment to include the env var. This way, if people want to turn it off, it's doable. I'm not sure how this strategy fits with support, etc. E.g. if they turn it off would that mean the customer is in an unmanaged state?

Copy link
Member

@njhale njhale Dec 3, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if they turn it off would that mean the customer is in an unmanaged state?

it does

if err != nil {
return nil, err
}

// Parse yaml
var propertiesFile = &registry.PropertiesFile{}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to decide if this property or a dependency so we can use either PropertiesFile or DependenciesFile. Honestly, it is just wording but it is a decision to be made for syntax sake.

err = json.Unmarshal(yamlFile, propertiesFile)
if err != nil {
return nil, err
}

return propertiesFile, nil
}
Loading