Skip to content

Commit 5eb8b1e

Browse files
committed
implement support for plain+v0 bundles
Signed-off-by: Bryce Palmer <[email protected]>
1 parent 22afecf commit 5eb8b1e

File tree

12 files changed

+426
-108
lines changed

12 files changed

+426
-108
lines changed

Makefile

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,8 +106,10 @@ kind-cluster-cleanup: $(KIND) ## Delete the kind cluster
106106

107107
kind-load-test-artifacts: $(KIND) ## Load the e2e testdata container images into a kind cluster
108108
$(CONTAINER_RUNTIME) build $(TESTDATA_DIR)/bundles/registry-v1/prometheus-operator.v0.47.0 -t localhost/testdata/bundles/registry-v1/prometheus-operator:v0.47.0
109+
$(CONTAINER_RUNTIME) build $(TESTDATA_DIR)/bundles/plain-v0/plain.v0.1.0 -t localhost/testdata/bundles/plain-v0/plain:v0.1.0
109110
$(CONTAINER_RUNTIME) build $(TESTDATA_DIR)/catalogs -f $(TESTDATA_DIR)/catalogs/test-catalog.Dockerfile -t localhost/testdata/catalogs/test-catalog:e2e
110111
$(KIND) load docker-image localhost/testdata/bundles/registry-v1/prometheus-operator:v0.47.0 --name $(KIND_CLUSTER_NAME)
112+
$(KIND) load docker-image localhost/testdata/bundles/plain-v0/plain:v0.1.0 --name $(KIND_CLUSTER_NAME)
111113
$(KIND) load docker-image localhost/testdata/catalogs/test-catalog:e2e --name $(KIND_CLUSTER_NAME)
112114

113115
##@ Build

internal/controllers/operator_controller.go

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -157,9 +157,19 @@ func (r *OperatorReconciler) reconcile(ctx context.Context, op *operatorsv1alpha
157157
op.Status.ResolvedBundleResource = bundleImage
158158
setResolvedStatusConditionSuccess(&op.Status.Conditions, fmt.Sprintf("resolved to %q", bundleImage), op.GetGeneration())
159159

160+
mediaType, err := bundleEntity.MediaType()
161+
if err != nil {
162+
setInstalledStatusConditionFailed(&op.Status.Conditions, err.Error(), op.GetGeneration())
163+
return ctrl.Result{}, err
164+
}
165+
bundleProvisioner, err := mapBundleMediaTypeToBundleProvisioner(mediaType)
166+
if err != nil {
167+
setInstalledStatusConditionFailed(&op.Status.Conditions, err.Error(), op.GetGeneration())
168+
return ctrl.Result{}, err
169+
}
160170
// Ensure a BundleDeployment exists with its bundle source from the bundle
161171
// image we just looked up in the solution.
162-
dep := r.generateExpectedBundleDeployment(*op, bundleImage)
172+
dep := r.generateExpectedBundleDeployment(*op, bundleImage, bundleProvisioner)
163173
if err := r.ensureBundleDeployment(ctx, dep); err != nil {
164174
// originally Reason: operatorsv1alpha1.ReasonInstallationFailed
165175
op.Status.InstalledBundleResource = ""
@@ -245,12 +255,13 @@ func (r *OperatorReconciler) getBundleEntityFromSolution(solution *solver.Soluti
245255
return nil, fmt.Errorf("entity for package %q not found in solution", packageName)
246256
}
247257

248-
func (r *OperatorReconciler) generateExpectedBundleDeployment(o operatorsv1alpha1.Operator, bundlePath string) *unstructured.Unstructured {
258+
func (r *OperatorReconciler) generateExpectedBundleDeployment(o operatorsv1alpha1.Operator, bundlePath string, bundleProvisioner string) *unstructured.Unstructured {
249259
// We use unstructured here to avoid problems of serializing default values when sending patches to the apiserver.
250260
// If you use a typed object, any default values from that struct get serialized into the JSON patch, which could
251261
// cause unrelated fields to be patched back to the default value even though that isn't the intention. Using an
252262
// unstructured ensures that the patch contains only what is specified. Using unstructured like this is basically
253263
// identical to "kubectl apply -f"
264+
254265
bd := &unstructured.Unstructured{Object: map[string]interface{}{
255266
"apiVersion": rukpakv1alpha1.GroupVersion.String(),
256267
"kind": rukpakv1alpha1.BundleDeploymentKind,
@@ -262,8 +273,7 @@ func (r *OperatorReconciler) generateExpectedBundleDeployment(o operatorsv1alpha
262273
"provisionerClassName": "core-rukpak-io-plain",
263274
"template": map[string]interface{}{
264275
"spec": map[string]interface{}{
265-
// TODO: Don't assume registry provisioner
266-
"provisionerClassName": "core-rukpak-io-registry",
276+
"provisionerClassName": bundleProvisioner,
267277
"source": map[string]interface{}{
268278
// TODO: Don't assume image type
269279
"type": string(rukpakv1alpha1.SourceTypeImage),
@@ -364,6 +374,23 @@ func isBundleDepStale(bd *rukpakv1alpha1.BundleDeployment) bool {
364374
return bd != nil && bd.Status.ObservedGeneration != bd.GetGeneration()
365375
}
366376

377+
// mapBundleMediaTypeToBundleProvisioner maps an olm.bundle.mediatype property to a
378+
// rukpak bundle provisioner class name that is capable of unpacking the bundle type
379+
func mapBundleMediaTypeToBundleProvisioner(mediaType string) (string, error) {
380+
switch mediaType {
381+
case entity.MediaTypePlain:
382+
return "core-rukpak-io-plain", nil
383+
// To ensure compatibility with bundles created with OLMv0 where the
384+
// olm.bundle.mediatype property doesn't exist, we assume that if the
385+
// property is empty (i.e doesn't exist) that the bundle is one created
386+
// with OLMv0 and therefore should use the registry provisioner
387+
case entity.MediaTypeRegistry, "":
388+
return "core-rukpak-io-registry", nil
389+
default:
390+
return "", fmt.Errorf("unknown bundle mediatype: %s", mediaType)
391+
}
392+
}
393+
367394
// setResolvedStatusConditionSuccess sets the resolved status condition to success.
368395
func setResolvedStatusConditionSuccess(conditions *[]metav1.Condition, message string, generation int64) {
369396
apimeta.SetStatusCondition(conditions, metav1.Condition{

internal/controllers/operator_controller_test.go

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -960,6 +960,108 @@ var _ = Describe("Operator Controller Test", func() {
960960
err := cl.Delete(ctx, operator)
961961
Expect(err).To(Not(HaveOccurred()))
962962
})
963+
When("the operator specifies a package with a plain+v0 bundle", func() {
964+
var pkgName string
965+
var pkgVer string
966+
var pkgChan string
967+
BeforeEach(func() {
968+
By("initializing cluster state")
969+
pkgName = "plain"
970+
pkgVer = "0.1.0"
971+
pkgChan = "beta"
972+
operator = &operatorsv1alpha1.Operator{
973+
ObjectMeta: metav1.ObjectMeta{Name: opKey.Name},
974+
Spec: operatorsv1alpha1.OperatorSpec{
975+
PackageName: pkgName,
976+
Version: pkgVer,
977+
Channel: pkgChan,
978+
},
979+
}
980+
err := cl.Create(ctx, operator)
981+
Expect(err).NotTo(HaveOccurred())
982+
})
983+
It("sets resolution success status", func() {
984+
By("running reconcile")
985+
res, err := reconciler.Reconcile(ctx, ctrl.Request{NamespacedName: opKey})
986+
Expect(res).To(Equal(ctrl.Result{}))
987+
Expect(err).NotTo(HaveOccurred())
988+
989+
By("fetching updated operator after reconcile")
990+
Expect(cl.Get(ctx, opKey, operator)).NotTo(HaveOccurred())
991+
992+
By("Checking the status fields")
993+
Expect(operator.Status.ResolvedBundleResource).To(Equal("quay.io/operatorhub/plain@sha256:plain"))
994+
Expect(operator.Status.InstalledBundleResource).To(Equal(""))
995+
996+
By("checking the expected conditions")
997+
cond := apimeta.FindStatusCondition(operator.Status.Conditions, operatorsv1alpha1.TypeResolved)
998+
Expect(cond).NotTo(BeNil())
999+
Expect(cond.Status).To(Equal(metav1.ConditionTrue))
1000+
Expect(cond.Reason).To(Equal(operatorsv1alpha1.ReasonSuccess))
1001+
Expect(cond.Message).To(Equal("resolved to \"quay.io/operatorhub/plain@sha256:plain\""))
1002+
cond = apimeta.FindStatusCondition(operator.Status.Conditions, operatorsv1alpha1.TypeInstalled)
1003+
Expect(cond).NotTo(BeNil())
1004+
Expect(cond.Status).To(Equal(metav1.ConditionUnknown))
1005+
Expect(cond.Reason).To(Equal(operatorsv1alpha1.ReasonInstallationStatusUnknown))
1006+
Expect(cond.Message).To(Equal("bundledeployment status is unknown"))
1007+
1008+
By("fetching the bundled deployment")
1009+
bd := &rukpakv1alpha1.BundleDeployment{}
1010+
Expect(cl.Get(ctx, types.NamespacedName{Name: opKey.Name}, bd)).NotTo(HaveOccurred())
1011+
Expect(bd.Spec.ProvisionerClassName).To(Equal("core-rukpak-io-plain"))
1012+
Expect(bd.Spec.Template.Spec.ProvisionerClassName).To(Equal("core-rukpak-io-plain"))
1013+
Expect(bd.Spec.Template.Spec.Source.Type).To(Equal(rukpakv1alpha1.SourceTypeImage))
1014+
Expect(bd.Spec.Template.Spec.Source.Image).NotTo(BeNil())
1015+
Expect(bd.Spec.Template.Spec.Source.Image.Ref).To(Equal("quay.io/operatorhub/plain@sha256:plain"))
1016+
})
1017+
})
1018+
When("the operator specifies a package with a bade bundle mediatype", func() {
1019+
var pkgName string
1020+
var pkgVer string
1021+
var pkgChan string
1022+
BeforeEach(func() {
1023+
By("initializing cluster state")
1024+
pkgName = "badmedia"
1025+
pkgVer = "0.1.0"
1026+
pkgChan = "beta"
1027+
operator = &operatorsv1alpha1.Operator{
1028+
ObjectMeta: metav1.ObjectMeta{Name: opKey.Name},
1029+
Spec: operatorsv1alpha1.OperatorSpec{
1030+
PackageName: pkgName,
1031+
Version: pkgVer,
1032+
Channel: pkgChan,
1033+
},
1034+
}
1035+
err := cl.Create(ctx, operator)
1036+
Expect(err).NotTo(HaveOccurred())
1037+
})
1038+
It("sets resolution success status", func() {
1039+
By("running reconcile")
1040+
res, err := reconciler.Reconcile(ctx, ctrl.Request{NamespacedName: opKey})
1041+
Expect(res).To(Equal(ctrl.Result{}))
1042+
Expect(err).To(HaveOccurred())
1043+
Expect(err.Error()).To(Equal("unknown bundle mediatype: badmedia+v1"))
1044+
1045+
By("fetching updated operator after reconcile")
1046+
Expect(cl.Get(ctx, opKey, operator)).NotTo(HaveOccurred())
1047+
1048+
By("Checking the status fields")
1049+
Expect(operator.Status.ResolvedBundleResource).To(Equal("quay.io/operatorhub/badmedia@sha256:badmedia"))
1050+
Expect(operator.Status.InstalledBundleResource).To(Equal(""))
1051+
1052+
By("checking the expected conditions")
1053+
cond := apimeta.FindStatusCondition(operator.Status.Conditions, operatorsv1alpha1.TypeResolved)
1054+
Expect(cond).NotTo(BeNil())
1055+
Expect(cond.Status).To(Equal(metav1.ConditionTrue))
1056+
Expect(cond.Reason).To(Equal(operatorsv1alpha1.ReasonSuccess))
1057+
Expect(cond.Message).To(Equal("resolved to \"quay.io/operatorhub/badmedia@sha256:badmedia\""))
1058+
cond = apimeta.FindStatusCondition(operator.Status.Conditions, operatorsv1alpha1.TypeInstalled)
1059+
Expect(cond).NotTo(BeNil())
1060+
Expect(cond.Status).To(Equal(metav1.ConditionFalse))
1061+
Expect(cond.Reason).To(Equal(operatorsv1alpha1.ReasonInstallationFailed))
1062+
Expect(cond.Message).To(Equal("unknown bundle mediatype: badmedia+v1"))
1063+
})
1064+
})
9631065
})
9641066
When("an invalid semver is provided that bypasses the regex validation", func() {
9651067
var (
@@ -1061,4 +1163,18 @@ var testEntitySource = input.NewCacheQuerier(map[deppy.Identifier]input.Entity{
10611163
"olm.package": `{"packageName":"badimage","version":"0.1.0"}`,
10621164
"olm.gvk": `[]`,
10631165
}),
1166+
"operatorhub/plain/0.1.0": *input.NewEntity("operatorhub/plain/0.1.0", map[string]string{
1167+
"olm.bundle.path": `"quay.io/operatorhub/plain@sha256:plain"`,
1168+
"olm.channel": `{"channelName":"beta","priority":0}`,
1169+
"olm.package": `{"packageName":"plain","version":"0.1.0"}`,
1170+
"olm.gvk": `[]`,
1171+
"olm.bundle.mediatype": `"plain+v0"`,
1172+
}),
1173+
"operatorhub/badmedia/0.1.0": *input.NewEntity("operatorhub/badmedia/0.1.0", map[string]string{
1174+
"olm.bundle.path": `"quay.io/operatorhub/badmedia@sha256:badmedia"`,
1175+
"olm.channel": `{"channelName":"beta","priority":0}`,
1176+
"olm.package": `{"packageName":"badmedia","version":"0.1.0"}`,
1177+
"olm.gvk": `[]`,
1178+
"olm.bundle.mediatype": `"badmedia+v1"`,
1179+
}),
10641180
})

internal/resolution/entitysources/catalogdsource.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77

88
"github.com/operator-framework/deppy/pkg/deppy"
99
"github.com/operator-framework/deppy/pkg/deppy/input"
10+
"github.com/operator-framework/operator-controller/internal/resolution/variable_sources/entity"
1011
"github.com/operator-framework/operator-registry/alpha/property"
1112
"sigs.k8s.io/controller-runtime/pkg/client"
1213

@@ -80,12 +81,16 @@ func getEntities(ctx context.Context, client client.Client) (input.EntityList, e
8081
for _, bundle := range bundleMetadatas.Items {
8182
props := map[string]string{}
8283

84+
// TODO: We should make sure all properties are forwarded
85+
// through and avoid a lossy translation from FBC --> entity
8386
for _, prop := range bundle.Spec.Properties {
8487
switch prop.Type {
8588
case property.TypePackage:
8689
// this is already a json marshalled object, so it doesn't need to be marshalled
8790
// like the other ones
8891
props[property.TypePackage] = string(prop.Value)
92+
case entity.PropertyBundleMediaType:
93+
props[entity.PropertyBundleMediaType] = string(prop.Value)
8994
}
9095
}
9196

internal/resolution/variable_sources/entity/bundle_entity.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,19 @@ import (
1212

1313
const PropertyBundlePath = "olm.bundle.path"
1414

15+
// TODO: Is this the right place for these?
16+
// ----
17+
const PropertyBundleMediaType = "olm.bundle.mediatype"
18+
19+
type MediaType string
20+
21+
const (
22+
MediaTypePlain = "plain+v0"
23+
MediaTypeRegistry = "registry+v1"
24+
)
25+
26+
// ----
27+
1528
type ChannelProperties struct {
1629
property.Channel
1730
Replaces string `json:"replaces,omitempty"`
@@ -58,6 +71,7 @@ type BundleEntity struct {
5871
channelProperties *ChannelProperties
5972
semVersion *semver.Version
6073
bundlePath string
74+
mediaType string
6175
mu sync.RWMutex
6276
}
6377

@@ -124,6 +138,27 @@ func (b *BundleEntity) BundlePath() (string, error) {
124138
return b.bundlePath, nil
125139
}
126140

141+
func (b *BundleEntity) MediaType() (string, error) {
142+
if err := b.loadMediaType(); err != nil {
143+
return "", err
144+
}
145+
146+
return b.mediaType, nil
147+
}
148+
149+
func (b *BundleEntity) loadMediaType() error {
150+
b.mu.Lock()
151+
defer b.mu.Unlock()
152+
if b.mediaType == "" {
153+
mediaType, err := loadFromEntity[string](b.Entity, PropertyBundleMediaType, optional)
154+
if err != nil {
155+
return fmt.Errorf("error determining bundle mediatype for entity '%s': %w", b.ID, err)
156+
}
157+
b.mediaType = mediaType
158+
}
159+
return nil
160+
}
161+
127162
func (b *BundleEntity) loadPackage() error {
128163
b.mu.Lock()
129164
defer b.mu.Unlock()

internal/resolution/variable_sources/entity/bundle_entity_test.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package entity_test
22

33
import (
4+
"fmt"
45
"testing"
56

67
"github.com/blang/semver/v4"
@@ -267,4 +268,32 @@ var _ = Describe("BundleEntity", func() {
267268
Expect(err.Error()).To(Equal("error determining bundle path for entity 'operatorhub/prometheus/0.14.0': property 'olm.bundle.path' ('badBundlePath') could not be parsed: invalid character 'b' looking for beginning of value"))
268269
})
269270
})
271+
272+
Describe("MediaType", func() {
273+
It("should return the bundle mediatype property if present", func() {
274+
entity := input.NewEntity("operatorhub/prometheus/0.14.0", map[string]string{
275+
olmentity.PropertyBundleMediaType: fmt.Sprintf(`"%s"`, olmentity.MediaTypePlain),
276+
})
277+
bundleEntity := olmentity.NewBundleEntity(entity)
278+
mediaType, err := bundleEntity.MediaType()
279+
Expect(err).ToNot(HaveOccurred())
280+
Expect(mediaType).To(Equal(olmentity.MediaTypePlain))
281+
})
282+
It("should not return an error if the property is not found", func() {
283+
entity := input.NewEntity("operatorhub/prometheus/0.14.0", map[string]string{})
284+
bundleEntity := olmentity.NewBundleEntity(entity)
285+
mediaType, err := bundleEntity.MediaType()
286+
Expect(mediaType).To(BeEmpty())
287+
Expect(err).To(BeNil())
288+
})
289+
It("should return error if the property is malformed", func() {
290+
entity := input.NewEntity("operatorhub/prometheus/0.14.0", map[string]string{
291+
olmentity.PropertyBundleMediaType: "badtype",
292+
})
293+
bundleEntity := olmentity.NewBundleEntity(entity)
294+
mediaType, err := bundleEntity.MediaType()
295+
Expect(mediaType).To(BeEmpty())
296+
Expect(err.Error()).To(Equal("error determining bundle mediatype for entity 'operatorhub/prometheus/0.14.0': property 'olm.bundle.mediatype' ('badtype') could not be parsed: invalid character 'b' looking for beginning of value"))
297+
})
298+
})
270299
})

0 commit comments

Comments
 (0)