Skip to content

Commit 098778e

Browse files
authored
add support for handling plain+v0 bundle types (#242)
1 parent 177fad2 commit 098778e

File tree

12 files changed

+467
-168
lines changed

12 files changed

+467
-168
lines changed

Diff for: Makefile

+2
Original file line numberDiff line numberDiff line change
@@ -105,8 +105,10 @@ kind-cluster-cleanup: $(KIND) ## Delete the kind cluster
105105

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

112114
##@ Build

Diff for: internal/controllers/operator_controller.go

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

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

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

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

Diff for: internal/controllers/operator_controller_test.go

+115-1
Original file line numberDiff line numberDiff line change
@@ -904,6 +904,107 @@ var _ = Describe("Operator Controller Test", func() {
904904
Expect(cond.Message).To(Equal("installation has not been attempted as resolution failed"))
905905
})
906906
})
907+
When("the operator specifies a package with a plain+v0 bundle", func() {
908+
var pkgName string
909+
var pkgVer string
910+
var pkgChan string
911+
BeforeEach(func() {
912+
By("initializing cluster state")
913+
pkgName = "plain"
914+
pkgVer = "0.1.0"
915+
pkgChan = "beta"
916+
operator = &operatorsv1alpha1.Operator{
917+
ObjectMeta: metav1.ObjectMeta{Name: opKey.Name},
918+
Spec: operatorsv1alpha1.OperatorSpec{
919+
PackageName: pkgName,
920+
Version: pkgVer,
921+
Channel: pkgChan,
922+
},
923+
}
924+
err := cl.Create(ctx, operator)
925+
Expect(err).NotTo(HaveOccurred())
926+
})
927+
It("sets resolution success status", func() {
928+
By("running reconcile")
929+
res, err := reconciler.Reconcile(ctx, ctrl.Request{NamespacedName: opKey})
930+
Expect(res).To(Equal(ctrl.Result{}))
931+
Expect(err).NotTo(HaveOccurred())
932+
By("fetching updated operator after reconcile")
933+
Expect(cl.Get(ctx, opKey, operator)).NotTo(HaveOccurred())
934+
935+
By("Checking the status fields")
936+
Expect(operator.Status.ResolvedBundleResource).To(Equal("quay.io/operatorhub/plain@sha256:plain"))
937+
Expect(operator.Status.InstalledBundleResource).To(Equal(""))
938+
939+
By("checking the expected conditions")
940+
cond := apimeta.FindStatusCondition(operator.Status.Conditions, operatorsv1alpha1.TypeResolved)
941+
Expect(cond).NotTo(BeNil())
942+
Expect(cond.Status).To(Equal(metav1.ConditionTrue))
943+
Expect(cond.Reason).To(Equal(operatorsv1alpha1.ReasonSuccess))
944+
Expect(cond.Message).To(Equal("resolved to \"quay.io/operatorhub/plain@sha256:plain\""))
945+
cond = apimeta.FindStatusCondition(operator.Status.Conditions, operatorsv1alpha1.TypeInstalled)
946+
Expect(cond).NotTo(BeNil())
947+
Expect(cond.Status).To(Equal(metav1.ConditionUnknown))
948+
Expect(cond.Reason).To(Equal(operatorsv1alpha1.ReasonInstallationStatusUnknown))
949+
Expect(cond.Message).To(Equal("bundledeployment status is unknown"))
950+
951+
By("fetching the bundled deployment")
952+
bd := &rukpakv1alpha1.BundleDeployment{}
953+
Expect(cl.Get(ctx, types.NamespacedName{Name: opKey.Name}, bd)).NotTo(HaveOccurred())
954+
Expect(bd.Spec.ProvisionerClassName).To(Equal("core-rukpak-io-plain"))
955+
Expect(bd.Spec.Template.Spec.ProvisionerClassName).To(Equal("core-rukpak-io-plain"))
956+
Expect(bd.Spec.Template.Spec.Source.Type).To(Equal(rukpakv1alpha1.SourceTypeImage))
957+
Expect(bd.Spec.Template.Spec.Source.Image).NotTo(BeNil())
958+
Expect(bd.Spec.Template.Spec.Source.Image.Ref).To(Equal("quay.io/operatorhub/plain@sha256:plain"))
959+
})
960+
})
961+
When("the operator specifies a package with a bad bundle mediatype", func() {
962+
var pkgName string
963+
var pkgVer string
964+
var pkgChan string
965+
BeforeEach(func() {
966+
By("initializing cluster state")
967+
pkgName = "badmedia"
968+
pkgVer = "0.1.0"
969+
pkgChan = "beta"
970+
operator = &operatorsv1alpha1.Operator{
971+
ObjectMeta: metav1.ObjectMeta{Name: opKey.Name},
972+
Spec: operatorsv1alpha1.OperatorSpec{
973+
PackageName: pkgName,
974+
Version: pkgVer,
975+
Channel: pkgChan,
976+
},
977+
}
978+
err := cl.Create(ctx, operator)
979+
Expect(err).NotTo(HaveOccurred())
980+
})
981+
It("sets resolution success status", func() {
982+
By("running reconcile")
983+
res, err := reconciler.Reconcile(ctx, ctrl.Request{NamespacedName: opKey})
984+
Expect(res).To(Equal(ctrl.Result{}))
985+
Expect(err).To(HaveOccurred())
986+
Expect(err.Error()).To(Equal("unknown bundle mediatype: badmedia+v1"))
987+
988+
By("fetching updated operator after reconcile")
989+
Expect(cl.Get(ctx, opKey, operator)).NotTo(HaveOccurred())
990+
991+
By("Checking the status fields")
992+
Expect(operator.Status.ResolvedBundleResource).To(Equal("quay.io/operatorhub/badmedia@sha256:badmedia"))
993+
Expect(operator.Status.InstalledBundleResource).To(Equal(""))
994+
995+
By("checking the expected conditions")
996+
cond := apimeta.FindStatusCondition(operator.Status.Conditions, operatorsv1alpha1.TypeResolved)
997+
Expect(cond).NotTo(BeNil())
998+
Expect(cond.Status).To(Equal(metav1.ConditionTrue))
999+
Expect(cond.Reason).To(Equal(operatorsv1alpha1.ReasonSuccess))
1000+
Expect(cond.Message).To(Equal("resolved to \"quay.io/operatorhub/badmedia@sha256:badmedia\""))
1001+
cond = apimeta.FindStatusCondition(operator.Status.Conditions, operatorsv1alpha1.TypeInstalled)
1002+
Expect(cond).NotTo(BeNil())
1003+
Expect(cond.Status).To(Equal(metav1.ConditionFalse))
1004+
Expect(cond.Reason).To(Equal(operatorsv1alpha1.ReasonInstallationFailed))
1005+
Expect(cond.Message).To(Equal("unknown bundle mediatype: badmedia+v1"))
1006+
})
1007+
})
9071008
When("an invalid semver is provided that bypasses the regex validation", func() {
9081009
var (
9091010
pkgName string
@@ -955,7 +1056,6 @@ var _ = Describe("Operator Controller Test", func() {
9551056
Expect(cond.Message).To(Equal("installation has not been attempted as spec is invalid"))
9561057
})
9571058
})
958-
9591059
})
9601060
})
9611061

@@ -1000,4 +1100,18 @@ var testEntitySource = input.NewCacheQuerier(map[deppy.Identifier]input.Entity{
10001100
"olm.package": `{"packageName":"badimage","version":"0.1.0"}`,
10011101
"olm.gvk": `[]`,
10021102
}),
1103+
"operatorhub/plain/0.1.0": *input.NewEntity("operatorhub/plain/0.1.0", map[string]string{
1104+
"olm.bundle.path": `"quay.io/operatorhub/plain@sha256:plain"`,
1105+
"olm.channel": `{"channelName":"beta","priority":0}`,
1106+
"olm.package": `{"packageName":"plain","version":"0.1.0"}`,
1107+
"olm.gvk": `[]`,
1108+
"olm.bundle.mediatype": `"plain+v0"`,
1109+
}),
1110+
"operatorhub/badmedia/0.1.0": *input.NewEntity("operatorhub/badmedia/0.1.0", map[string]string{
1111+
"olm.bundle.path": `"quay.io/operatorhub/badmedia@sha256:badmedia"`,
1112+
"olm.channel": `{"channelName":"beta","priority":0}`,
1113+
"olm.package": `{"packageName":"badmedia","version":"0.1.0"}`,
1114+
"olm.gvk": `[]`,
1115+
"olm.bundle.mediatype": `"badmedia+v1"`,
1116+
}),
10031117
})

Diff for: internal/resolution/entitysources/catalogdsource.go

+5
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

Diff for: internal/resolution/variable_sources/entity/bundle_entity.go

+35
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()

Diff for: internal/resolution/variable_sources/entity/bundle_entity_test.go

+29
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)