-
Notifications
You must be signed in to change notification settings - Fork 552
Magic catalog #2527
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Magic catalog #2527
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
package e2e | ||
|
||
import ( | ||
"io/ioutil" | ||
) | ||
|
||
type FileBasedCatalogProvider interface { | ||
GetCatalog() string | ||
} | ||
|
||
type fileBasedFileBasedCatalogProvider struct { | ||
fbc string | ||
} | ||
|
||
func NewFileBasedFiledBasedCatalogProvider(path string) (FileBasedCatalogProvider, error) { | ||
data, err := ioutil.ReadFile(path) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
return &fileBasedFileBasedCatalogProvider{ | ||
fbc: string(data), | ||
}, nil | ||
} | ||
|
||
func (f *fileBasedFileBasedCatalogProvider) GetCatalog() string { | ||
return f.fbc | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,280 @@ | ||
package e2e | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
|
||
operatorsv1alpha1 "github.com/operator-framework/api/pkg/operators/v1alpha1" | ||
corev1 "k8s.io/api/core/v1" | ||
k8serror "k8s.io/apimachinery/pkg/api/errors" | ||
"k8s.io/apimachinery/pkg/api/resource" | ||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||
"k8s.io/apimachinery/pkg/util/intstr" | ||
k8scontrollerclient "sigs.k8s.io/controller-runtime/pkg/client" | ||
) | ||
|
||
const ( | ||
olmCatalogLabel string = "olm.catalogSource" | ||
catalogMountPath string = "/opt/olm" | ||
catalogServicePort int32 = 50051 | ||
catalogReadyState string = "READY" | ||
) | ||
|
||
type MagicCatalog interface { | ||
DeployCatalog(ctx context.Context) error | ||
UndeployCatalog(ctx context.Context) []error | ||
} | ||
|
||
type magicCatalog struct { | ||
fileBasedCatalog FileBasedCatalogProvider | ||
kubeClient k8scontrollerclient.Client | ||
namespace string | ||
name string | ||
configMapName string | ||
serviceName string | ||
podName string | ||
} | ||
|
||
// NewMagicCatalog creates an object that can deploy an arbitrary file-based catalog given by the FileBasedCatalogProvider | ||
// Keep in mind that there are limits to the configMaps. So, the catalogs need to be relatively simple | ||
func NewMagicCatalog(kubeClient k8scontrollerclient.Client, namespace string, catalogName string, provider FileBasedCatalogProvider) MagicCatalog { | ||
return &magicCatalog{ | ||
fileBasedCatalog: provider, | ||
kubeClient: kubeClient, | ||
namespace: namespace, | ||
name: catalogName, | ||
configMapName: catalogName + "-configmap", | ||
serviceName: catalogName + "-svc", | ||
podName: catalogName + "-pod", | ||
} | ||
} | ||
|
||
func (c *magicCatalog) DeployCatalog(ctx context.Context) error { | ||
|
||
catalogSource := c.makeCatalogSource() | ||
resourcesInOrderOfDeployment := []k8scontrollerclient.Object{ | ||
c.makeConfigMap(), | ||
c.makeCatalogSourcePod(), | ||
c.makeCatalogService(), | ||
catalogSource, | ||
} | ||
|
||
for _, res := range resourcesInOrderOfDeployment { | ||
err := c.kubeClient.Create(ctx, res) | ||
if err != nil { | ||
return c.cleanUpAfter(ctx, err) | ||
} | ||
} | ||
|
||
// wait for catalog source to become ready | ||
err := waitFor(func() (bool, error) { | ||
err := c.kubeClient.Get(ctx, k8scontrollerclient.ObjectKey{ | ||
Name: catalogSource.GetName(), | ||
Namespace: catalogSource.GetNamespace(), | ||
}, catalogSource) | ||
|
||
if err != nil || catalogSource.Status.GRPCConnectionState == nil { | ||
return false, err | ||
} | ||
|
||
state := catalogSource.Status.GRPCConnectionState.LastObservedState | ||
|
||
if state != catalogReadyState { | ||
return false, nil | ||
} else { | ||
return true, nil | ||
} | ||
}) | ||
|
||
if err != nil { | ||
return c.cleanUpAfter(ctx, err) | ||
} | ||
|
||
return nil | ||
} | ||
|
||
func (c *magicCatalog) UndeployCatalog(ctx context.Context) []error { | ||
var errs []error = nil | ||
|
||
resourcesInOrderOfDeletion := []k8scontrollerclient.Object{ | ||
c.makeCatalogSource(), | ||
c.makeCatalogService(), | ||
c.makeCatalogSourcePod(), | ||
c.makeConfigMap(), | ||
} | ||
|
||
// try to delete all resourcesInOrderOfDeletion even if errors are | ||
// encountered through deletion. | ||
for _, res := range resourcesInOrderOfDeletion { | ||
err := c.kubeClient.Delete(ctx, res) | ||
|
||
// ignore not found errors | ||
if err != nil && !k8serror.IsNotFound(err) { | ||
if errs == nil { | ||
errs = make([]error, 0) | ||
} | ||
errs = append(errs, err) | ||
} | ||
} | ||
|
||
return errs | ||
} | ||
|
||
func (c *magicCatalog) cleanUpAfter(ctx context.Context, err error) error { | ||
cleanupErr := c.UndeployCatalog(ctx) | ||
if cleanupErr != nil { | ||
return fmt.Errorf("the following cleanup errors occurred: '%s' after an error deploying the configmap: '%s' ", cleanupErr, err) | ||
} | ||
return err | ||
} | ||
|
||
func (c *magicCatalog) makeCatalogService() *corev1.Service { | ||
return &corev1.Service{ | ||
ObjectMeta: metav1.ObjectMeta{ | ||
Name: c.serviceName, | ||
Namespace: c.namespace, | ||
}, | ||
Spec: corev1.ServiceSpec{ | ||
Ports: []corev1.ServicePort{ | ||
{ | ||
Name: "grpc", | ||
Port: catalogServicePort, | ||
Protocol: "TCP", | ||
TargetPort: intstr.FromInt(int(catalogServicePort)), | ||
}, | ||
}, | ||
Selector: c.makeCatalogSourcePodLabels(), | ||
}, | ||
} | ||
} | ||
|
||
func (c *magicCatalog) makeConfigMap() *corev1.ConfigMap { | ||
isImmutable := true | ||
return &corev1.ConfigMap{ | ||
ObjectMeta: metav1.ObjectMeta{ | ||
Name: c.configMapName, | ||
Namespace: c.namespace, | ||
}, | ||
Immutable: &isImmutable, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Super unimportant: we likely have a test helper function (or at least we should) that returns a pointer to a bool somewhere. |
||
Data: map[string]string{ | ||
"catalog.json": c.fileBasedCatalog.GetCatalog(), | ||
// due to the way files get mounted to pods from configMaps | ||
// it is important to add _this_ .indexignore | ||
// | ||
// The mount folder will look something like this: | ||
// /opt/olm | ||
// |--> ..2021_12_15_02_01_11.729011450 | ||
// |--> catalog.json | ||
// |--> .indexignore | ||
// |--> ..data -> ..2021_12_15_02_01_11.729011450 | ||
// |--> catalog.json -> ..data/catalog.json | ||
// |--> .indexignore -> ..data/.indexignore | ||
// Adding '**/..*' to the .indexignore ensures the | ||
// '..2021_12_15_02_01_11.729011450' and ' ..data' directories are ignored. | ||
// Otherwise, opm will pick up on both catalog.json files and fail with a conflicts (duplicate packages) | ||
".indexignore": "**/\\.\\.*\n", | ||
}, | ||
} | ||
} | ||
|
||
func (c *magicCatalog) makeCatalogSource() *operatorsv1alpha1.CatalogSource { | ||
return &operatorsv1alpha1.CatalogSource{ | ||
ObjectMeta: metav1.ObjectMeta{ | ||
Name: c.name, | ||
Namespace: c.namespace, | ||
}, | ||
Spec: operatorsv1alpha1.CatalogSourceSpec{ | ||
SourceType: operatorsv1alpha1.SourceTypeGrpc, | ||
Address: fmt.Sprintf("%s.%s.svc:50051", c.serviceName, c.namespace), | ||
}, | ||
} | ||
} | ||
|
||
func (c *magicCatalog) makeCatalogSourcePod() *corev1.Pod { | ||
|
||
const ( | ||
image = "quay.io/operator-framework/upstream-opm-builder" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this might need to be injectable for downstream CI at some point. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. that's a very good point - we could think of using env vars or something like that |
||
readinessDelay int32 = 5 | ||
livenessDelay int32 = 10 | ||
volumeMountName string = "fbc-catalog" | ||
) | ||
|
||
readOnlyRootFilesystem := false | ||
|
||
return &corev1.Pod{ | ||
ObjectMeta: metav1.ObjectMeta{ | ||
Name: c.podName, | ||
Namespace: c.namespace, | ||
Labels: c.makeCatalogSourcePodLabels(), | ||
}, | ||
Spec: corev1.PodSpec{ | ||
Containers: []corev1.Container{ | ||
{ | ||
Name: "catalog", | ||
Image: image, | ||
Command: []string{"opm", "serve", catalogMountPath}, | ||
Ports: []corev1.ContainerPort{ | ||
{ | ||
Name: "grpc", | ||
ContainerPort: 50051, | ||
}, | ||
}, | ||
ReadinessProbe: &corev1.Probe{ | ||
Handler: corev1.Handler{ | ||
Exec: &corev1.ExecAction{ | ||
Command: []string{"grpc_health_probe", "-addr=:50051"}, | ||
}, | ||
}, | ||
InitialDelaySeconds: readinessDelay, | ||
TimeoutSeconds: 5, | ||
}, | ||
LivenessProbe: &corev1.Probe{ | ||
Handler: corev1.Handler{ | ||
Exec: &corev1.ExecAction{ | ||
Command: []string{"grpc_health_probe", "-addr=:50051"}, | ||
}, | ||
}, | ||
InitialDelaySeconds: livenessDelay, | ||
TimeoutSeconds: 5, | ||
}, | ||
Resources: corev1.ResourceRequirements{ | ||
Requests: corev1.ResourceList{ | ||
corev1.ResourceCPU: resource.MustParse("10m"), | ||
corev1.ResourceMemory: resource.MustParse("50Mi"), | ||
}, | ||
}, | ||
SecurityContext: &corev1.SecurityContext{ | ||
ReadOnlyRootFilesystem: &readOnlyRootFilesystem, | ||
}, | ||
ImagePullPolicy: corev1.PullAlways, | ||
TerminationMessagePolicy: corev1.TerminationMessageFallbackToLogsOnError, | ||
VolumeMounts: []corev1.VolumeMount{ | ||
{ | ||
Name: volumeMountName, | ||
MountPath: catalogMountPath, | ||
ReadOnly: true, | ||
}, | ||
}, | ||
}, | ||
}, | ||
Volumes: []corev1.Volume{ | ||
{ | ||
Name: volumeMountName, | ||
VolumeSource: corev1.VolumeSource{ | ||
ConfigMap: &corev1.ConfigMapVolumeSource{ | ||
LocalObjectReference: corev1.LocalObjectReference{ | ||
Name: c.configMapName, | ||
}, | ||
}, | ||
}, | ||
}, | ||
}, | ||
}, | ||
} | ||
} | ||
|
||
func (c *magicCatalog) makeCatalogSourcePodLabels() map[string]string { | ||
return map[string]string{ | ||
olmCatalogLabel: c.name, | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
package e2e | ||
|
||
import ( | ||
"context" | ||
|
||
. "github.com/onsi/ginkgo" | ||
. "github.com/onsi/gomega" | ||
"github.com/operator-framework/operator-lifecycle-manager/test/e2e/ctx" | ||
corev1 "k8s.io/api/core/v1" | ||
) | ||
|
||
var _ = Describe("MagicCatalog", func() { | ||
var ( | ||
generatedNamespace corev1.Namespace | ||
) | ||
|
||
BeforeEach(func() { | ||
generatedNamespace = SetupGeneratedTestNamespace(genName("magic-catalog-e2e-")) | ||
}) | ||
|
||
AfterEach(func() { | ||
TeardownNamespace(generatedNamespace.GetName()) | ||
}) | ||
|
||
It("Deploys and Undeploys a File-based Catalog", func() { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Tests for the test harness 🙂 |
||
// create dependencies | ||
const catalogName = "test" | ||
namespace := generatedNamespace.GetName() | ||
kubeClient := ctx.Ctx().Client() | ||
provider, err := NewFileBasedFiledBasedCatalogProvider("../test/e2e/testdata/fbc_catalog.json") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Personally, I'd rather see the FBC JSON literal in the test case itself than in a golden file, but this works for now. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. tbh - I'm not a huge fan of big string literals, especially when we can't nicely format them due to code formatting or indentation, etc. For small stuff sure, for bigger files it's start to get awkward for me. But this is a taste thing. I've tried to create an abstraction through this provider interface where we can plug-in different fbc sources (file, string, etc.). When I have a little more time, I'd like to have a way to create the fbcs programatically. Might be even more readable that way...idk... |
||
Expect(err).To(BeNil()) | ||
|
||
// create and deploy and undeploy the magic catalog | ||
magicCatalog := NewMagicCatalog(kubeClient, namespace, catalogName, provider) | ||
|
||
// deployment blocks until the catalog source has reached a READY status | ||
Expect(magicCatalog.DeployCatalog(context.TODO())).To(BeNil()) | ||
Expect(magicCatalog.UndeployCatalog(context.TODO())).To(BeNil()) | ||
}) | ||
}) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
{ | ||
"schema": "olm.package", | ||
"name": "packageA", | ||
"defaultChannel": "stable" | ||
} | ||
{ | ||
"schema": "olm.channel", | ||
"name": "stable", | ||
"package": "packageA", | ||
"entries": [ | ||
{ | ||
"name": "packageA.v1.0.0" | ||
} | ||
] | ||
} | ||
{ | ||
"schema": "olm.bundle", | ||
"name": "packageA.v1.0.0", | ||
"package": "packageA", | ||
"image": "packageA:v1.0.0", | ||
"properties": [ | ||
{ | ||
"type": "olm.gvk", | ||
"value": { | ||
"group": "example.com", | ||
"kind": "TestA", | ||
"version": "v1alpha1" | ||
} | ||
}, | ||
{ | ||
"type": "olm.package", | ||
"value": { | ||
"packageName": "packageA", | ||
"version": "1.0.0" | ||
} | ||
} | ||
] | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: this appears to be redundant as
var errs []error
should be equivalent to an empty slice.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
right! old habits hehehe