Skip to content
Closed
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
160 changes: 98 additions & 62 deletions test/e2e/install_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ import (
rukpakv1alpha1 "github.com/operator-framework/rukpak/api/v1alpha1"
apimeta "k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/rand"
"sigs.k8s.io/controller-runtime/pkg/client"
)

const (
Expand All @@ -24,90 +24,126 @@ const (
var _ = Describe("Operator Install", func() {
var (
ctx context.Context
pkgName string
operatorName string
operator *operatorv1alpha1.Operator
operatorCatalog *catalogd.Catalog
operator *operatorv1alpha1.Operator
)
When("An operator is installed from an operator catalog", func() {
BeforeEach(func() {
ctx = context.Background()
pkgName = "argocd-operator"
operatorName = fmt.Sprintf("operator-%s", rand.String(8))
operator = &operatorv1alpha1.Operator{
ObjectMeta: metav1.ObjectMeta{
Name: operatorName,
},
Spec: operatorv1alpha1.OperatorSpec{
PackageName: pkgName,
},
}
operatorCatalog = &catalogd.Catalog{
ObjectMeta: metav1.ObjectMeta{
Name: "test-catalog",
Name: fmt.Sprintf("test-catalog-%s", rand.String(5)),
},
Spec: catalogd.CatalogSpec{
Source: catalogd.CatalogSource{
Type: catalogd.SourceTypeImage,
Image: &catalogd.ImageSource{
// (TODO): Set up a local image registry, and build and store a test catalog in it
// to use in the test suite
Ref: "quay.io/operatorhubio/catalog:latest",
Ref: "quay.io/olmtest/e2e-index:single-package-fbc",
},
},
},
}
err := c.Create(ctx, operatorCatalog)
Expect(err).ToNot(HaveOccurred())
Eventually(func(g Gomega) {
err = c.Get(ctx, types.NamespacedName{Name: "test-catalog"}, operatorCatalog)
g.Expect(err).ToNot(HaveOccurred())
g.Expect(len(operatorCatalog.Status.Conditions)).To(Equal(1))
g.Expect(operatorCatalog.Status.Conditions[0].Message).To(ContainSubstring("successfully unpacked the catalog image"))
}).WithTimeout(5 * time.Minute).WithPolling(defaultPoll).Should(Succeed())
})
It("resolves the specified package with correct bundle path", func() {
By("creating the Operator resource")
err := c.Create(ctx, operator)
Expect(err).ToNot(HaveOccurred())
Expect(c.Create(ctx, operatorCatalog)).To(Succeed())
Eventually(checkCatalogUnpacked(ctx, operatorCatalog)).WithTimeout(5 * time.Minute).WithPolling(defaultPoll).Should(BeNil())

By("eventually reporting a successful resolution and bundle path")
Eventually(func(g Gomega) {
err = c.Get(ctx, types.NamespacedName{Name: operator.Name}, operator)
g.Expect(err).ToNot(HaveOccurred())
g.Expect(len(operator.Status.Conditions)).To(Equal(2))
cond := apimeta.FindStatusCondition(operator.Status.Conditions, operatorv1alpha1.TypeResolved)
g.Expect(cond).ToNot(BeNil())
g.Expect(cond.Status).To(Equal(metav1.ConditionTrue))
g.Expect(cond.Reason).To(Equal(operatorv1alpha1.ReasonSuccess))
g.Expect(cond.Message).To(ContainSubstring("resolved to"))
g.Expect(operator.Status.ResolvedBundleResource).ToNot(BeEmpty())
}).WithTimeout(defaultTimeout).WithPolling(defaultPoll).Should(Succeed())
By("creating the Operator resource")
pkgName := "argocd-operator"
operatorName := fmt.Sprintf("%s-%s", pkgName, rand.String(8))
operator = &operatorv1alpha1.Operator{
ObjectMeta: metav1.ObjectMeta{
Name: operatorName,
},
Spec: operatorv1alpha1.OperatorSpec{
PackageName: pkgName,
Version: "0.6.0",
},
}
Expect(c.Create(ctx, operator)).To(Succeed())
})
It("installs the specified package with correct bundle path", func() {
By("eventually reporting a successful resolution")
Eventually(checkOperatorResolved(ctx, operator)).WithTimeout(defaultTimeout).WithPolling(defaultPoll).Should(Succeed())

By("eventually installing the package successfully")
Eventually(func(g Gomega) {
err = c.Get(ctx, types.NamespacedName{Name: operator.Name}, operator)
g.Expect(err).ToNot(HaveOccurred())
cond := apimeta.FindStatusCondition(operator.Status.Conditions, operatorv1alpha1.TypeInstalled)
g.Expect(cond).ToNot(BeNil())
g.Expect(cond.Status).To(Equal(metav1.ConditionTrue))
g.Expect(cond.Reason).To(Equal(operatorv1alpha1.ReasonSuccess))
g.Expect(cond.Message).To(ContainSubstring("installed from"))
g.Expect(operator.Status.InstalledBundleResource).ToNot(BeEmpty())
bd := rukpakv1alpha1.BundleDeployment{}
err = c.Get(ctx, types.NamespacedName{Name: operatorName}, &bd)
g.Expect(err).ToNot(HaveOccurred())
g.Expect(len(bd.Status.Conditions)).To(Equal(2))
g.Expect(bd.Status.Conditions[0].Reason).To(Equal("UnpackSuccessful"))
g.Expect(bd.Status.Conditions[1].Reason).To(Equal("InstallationSucceeded"))
}).WithTimeout(defaultTimeout).WithPolling(defaultPoll).Should(Succeed())
By("eventually reporting a successful installation")
Eventually(checkOperatorInstalled(ctx, operator)).WithTimeout(defaultTimeout).WithPolling(defaultPoll).Should(Succeed())

By("verifying the expected bundle path")
bd := rukpakv1alpha1.BundleDeployment{}
Expect(c.Get(ctx, client.ObjectKeyFromObject(operator), &bd)).To(Succeed())
Expect(bd.Spec.Template.Spec.Source.Type).To(Equal(rukpakv1alpha1.SourceTypeImage))
Expect(bd.Spec.Template.Spec.Source.Image.Ref).To(Equal("quay.io/operatorhubio/argocd-operator@sha256:1a9b3c8072f2d7f4d6528fa32905634d97b7b4c239ef9887e3fb821ff033fef6"))
Expect(operator.Status.ResolvedBundleResource).To(Equal(bd.Spec.Template.Spec.Source.Image.Ref))
Expect(operator.Status.InstalledBundleResource).To(Equal(bd.Spec.Template.Spec.Source.Image.Ref))
})
AfterEach(func() {
err := c.Delete(ctx, operatorCatalog)
Expect(err).ToNot(HaveOccurred())
err = c.Delete(ctx, operator)
Expect(err).ToNot(HaveOccurred())
Expect(c.Delete(ctx, operator)).To(Succeed())
Expect(c.Delete(ctx, operatorCatalog)).To(Succeed())
})
})
})

func checkCatalogUnpacked(ctx context.Context, catalog *catalogd.Catalog) func() error {
return func() error {
return checkCondition(ctx, catalog, catalogd.TypeUnpacked,
statusReason{metav1.ConditionTrue, catalogd.ReasonUnpackSuccessful},
statusReason{metav1.ConditionFalse, catalogd.ReasonUnpackFailed},
)
}
}

func checkOperatorResolved(ctx context.Context, op *operatorv1alpha1.Operator) func() error {
return func() error {
return checkCondition(ctx, op, operatorv1alpha1.TypeResolved,
statusReason{metav1.ConditionTrue, operatorv1alpha1.ReasonSuccess},
statusReason{metav1.ConditionFalse, operatorv1alpha1.ReasonResolutionFailed},
)
}
}

func checkOperatorInstalled(ctx context.Context, op *operatorv1alpha1.Operator) func() error {
return func() error {
return checkCondition(ctx, op, operatorv1alpha1.TypeInstalled,
statusReason{metav1.ConditionTrue, operatorv1alpha1.ReasonSuccess},
statusReason{metav1.ConditionFalse, operatorv1alpha1.ReasonInstallationFailed},
)
}
}

type statusReason struct {
status metav1.ConditionStatus
reason string
}

func checkCondition(ctx context.Context, obj client.Object, condType string, successStatusReason statusReason, failureStatusReasons ...statusReason) error {
if err := c.Get(ctx, client.ObjectKeyFromObject(obj), obj); err != nil {
return err
}

var conditions []metav1.Condition
switch v := obj.(type) {
case *catalogd.Catalog:
conditions = v.Status.Conditions
case *operatorv1alpha1.Operator:
conditions = v.Status.Conditions
default:
return StopTrying(fmt.Sprintf("cannot get conditions for unknown object type %T", obj))
}
Comment on lines +124 to +132
Copy link
Member Author

Choose a reason for hiding this comment

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

An earlier iteration of this section of code called a separate function I had written that generically:

  • converted to unstructured
  • extracted "status.conditions" into []interface{}
  • marshaled that to JSON
  • unmarshaled the JSON into []metav1.Condition

That code generically handled anything that had status.conditions with type []metav1.Condition, but it seemed a bit hacky/overkill. I'm happy to go back to that, or entertain other options as well.


cond := apimeta.FindStatusCondition(conditions, condType)
if cond == nil {
return fmt.Errorf("condition %q is not set; status: %#v", condType, conditions)
}
if successStatusReason == (statusReason{cond.Status, cond.Reason}) {
return nil
}

err := fmt.Errorf("condition %q is %s/%s; message: %q", cond.Type, cond.Status, cond.Reason, cond.Message)
for _, failureStatusReason := range failureStatusReasons {
if cond.Status == failureStatusReason.status && cond.Reason == failureStatusReason.reason {
return StopTrying(err.Error())
}
}
return err
}