Skip to content

Commit bee0b68

Browse files
authored
Add MagicCatalog for creating arbitrary file-based catalogs (#2527)
Signed-off-by: Per G. da Silva <[email protected]>
1 parent b3cded1 commit bee0b68

File tree

6 files changed

+395
-2
lines changed

6 files changed

+395
-2
lines changed

test/e2e/csv_e2e_test.go

-2
Original file line numberDiff line numberDiff line change
@@ -4423,8 +4423,6 @@ var _ = Describe("Disabling copied CSVs", func() {
44234423

44244424
var singleInstance = int32(1)
44254425

4426-
type cleanupFunc func()
4427-
44284426
var immediateDeleteGracePeriod int64 = 0
44294427

44304428
func findLastEvent(events *corev1.EventList) (event corev1.Event) {

test/e2e/fbc_provider.go

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package e2e
2+
3+
import (
4+
"io/ioutil"
5+
)
6+
7+
type FileBasedCatalogProvider interface {
8+
GetCatalog() string
9+
}
10+
11+
type fileBasedFileBasedCatalogProvider struct {
12+
fbc string
13+
}
14+
15+
func NewFileBasedFiledBasedCatalogProvider(path string) (FileBasedCatalogProvider, error) {
16+
data, err := ioutil.ReadFile(path)
17+
if err != nil {
18+
return nil, err
19+
}
20+
21+
return &fileBasedFileBasedCatalogProvider{
22+
fbc: string(data),
23+
}, nil
24+
}
25+
26+
func (f *fileBasedFileBasedCatalogProvider) GetCatalog() string {
27+
return f.fbc
28+
}

test/e2e/magic_catalog.go

+280
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,280 @@
1+
package e2e
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
operatorsv1alpha1 "github.com/operator-framework/api/pkg/operators/v1alpha1"
8+
corev1 "k8s.io/api/core/v1"
9+
k8serror "k8s.io/apimachinery/pkg/api/errors"
10+
"k8s.io/apimachinery/pkg/api/resource"
11+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
12+
"k8s.io/apimachinery/pkg/util/intstr"
13+
k8scontrollerclient "sigs.k8s.io/controller-runtime/pkg/client"
14+
)
15+
16+
const (
17+
olmCatalogLabel string = "olm.catalogSource"
18+
catalogMountPath string = "/opt/olm"
19+
catalogServicePort int32 = 50051
20+
catalogReadyState string = "READY"
21+
)
22+
23+
type MagicCatalog interface {
24+
DeployCatalog(ctx context.Context) error
25+
UndeployCatalog(ctx context.Context) []error
26+
}
27+
28+
type magicCatalog struct {
29+
fileBasedCatalog FileBasedCatalogProvider
30+
kubeClient k8scontrollerclient.Client
31+
namespace string
32+
name string
33+
configMapName string
34+
serviceName string
35+
podName string
36+
}
37+
38+
// NewMagicCatalog creates an object that can deploy an arbitrary file-based catalog given by the FileBasedCatalogProvider
39+
// Keep in mind that there are limits to the configMaps. So, the catalogs need to be relatively simple
40+
func NewMagicCatalog(kubeClient k8scontrollerclient.Client, namespace string, catalogName string, provider FileBasedCatalogProvider) MagicCatalog {
41+
return &magicCatalog{
42+
fileBasedCatalog: provider,
43+
kubeClient: kubeClient,
44+
namespace: namespace,
45+
name: catalogName,
46+
configMapName: catalogName + "-configmap",
47+
serviceName: catalogName + "-svc",
48+
podName: catalogName + "-pod",
49+
}
50+
}
51+
52+
func (c *magicCatalog) DeployCatalog(ctx context.Context) error {
53+
54+
catalogSource := c.makeCatalogSource()
55+
resourcesInOrderOfDeployment := []k8scontrollerclient.Object{
56+
c.makeConfigMap(),
57+
c.makeCatalogSourcePod(),
58+
c.makeCatalogService(),
59+
catalogSource,
60+
}
61+
62+
for _, res := range resourcesInOrderOfDeployment {
63+
err := c.kubeClient.Create(ctx, res)
64+
if err != nil {
65+
return c.cleanUpAfter(ctx, err)
66+
}
67+
}
68+
69+
// wait for catalog source to become ready
70+
err := waitFor(func() (bool, error) {
71+
err := c.kubeClient.Get(ctx, k8scontrollerclient.ObjectKey{
72+
Name: catalogSource.GetName(),
73+
Namespace: catalogSource.GetNamespace(),
74+
}, catalogSource)
75+
76+
if err != nil || catalogSource.Status.GRPCConnectionState == nil {
77+
return false, err
78+
}
79+
80+
state := catalogSource.Status.GRPCConnectionState.LastObservedState
81+
82+
if state != catalogReadyState {
83+
return false, nil
84+
} else {
85+
return true, nil
86+
}
87+
})
88+
89+
if err != nil {
90+
return c.cleanUpAfter(ctx, err)
91+
}
92+
93+
return nil
94+
}
95+
96+
func (c *magicCatalog) UndeployCatalog(ctx context.Context) []error {
97+
var errs []error = nil
98+
99+
resourcesInOrderOfDeletion := []k8scontrollerclient.Object{
100+
c.makeCatalogSource(),
101+
c.makeCatalogService(),
102+
c.makeCatalogSourcePod(),
103+
c.makeConfigMap(),
104+
}
105+
106+
// try to delete all resourcesInOrderOfDeletion even if errors are
107+
// encountered through deletion.
108+
for _, res := range resourcesInOrderOfDeletion {
109+
err := c.kubeClient.Delete(ctx, res)
110+
111+
// ignore not found errors
112+
if err != nil && !k8serror.IsNotFound(err) {
113+
if errs == nil {
114+
errs = make([]error, 0)
115+
}
116+
errs = append(errs, err)
117+
}
118+
}
119+
120+
return errs
121+
}
122+
123+
func (c *magicCatalog) cleanUpAfter(ctx context.Context, err error) error {
124+
cleanupErr := c.UndeployCatalog(ctx)
125+
if cleanupErr != nil {
126+
return fmt.Errorf("the following cleanup errors occurred: '%s' after an error deploying the configmap: '%s' ", cleanupErr, err)
127+
}
128+
return err
129+
}
130+
131+
func (c *magicCatalog) makeCatalogService() *corev1.Service {
132+
return &corev1.Service{
133+
ObjectMeta: metav1.ObjectMeta{
134+
Name: c.serviceName,
135+
Namespace: c.namespace,
136+
},
137+
Spec: corev1.ServiceSpec{
138+
Ports: []corev1.ServicePort{
139+
{
140+
Name: "grpc",
141+
Port: catalogServicePort,
142+
Protocol: "TCP",
143+
TargetPort: intstr.FromInt(int(catalogServicePort)),
144+
},
145+
},
146+
Selector: c.makeCatalogSourcePodLabels(),
147+
},
148+
}
149+
}
150+
151+
func (c *magicCatalog) makeConfigMap() *corev1.ConfigMap {
152+
isImmutable := true
153+
return &corev1.ConfigMap{
154+
ObjectMeta: metav1.ObjectMeta{
155+
Name: c.configMapName,
156+
Namespace: c.namespace,
157+
},
158+
Immutable: &isImmutable,
159+
Data: map[string]string{
160+
"catalog.json": c.fileBasedCatalog.GetCatalog(),
161+
// due to the way files get mounted to pods from configMaps
162+
// it is important to add _this_ .indexignore
163+
//
164+
// The mount folder will look something like this:
165+
// /opt/olm
166+
// |--> ..2021_12_15_02_01_11.729011450
167+
// |--> catalog.json
168+
// |--> .indexignore
169+
// |--> ..data -> ..2021_12_15_02_01_11.729011450
170+
// |--> catalog.json -> ..data/catalog.json
171+
// |--> .indexignore -> ..data/.indexignore
172+
// Adding '**/..*' to the .indexignore ensures the
173+
// '..2021_12_15_02_01_11.729011450' and ' ..data' directories are ignored.
174+
// Otherwise, opm will pick up on both catalog.json files and fail with a conflicts (duplicate packages)
175+
".indexignore": "**/\\.\\.*\n",
176+
},
177+
}
178+
}
179+
180+
func (c *magicCatalog) makeCatalogSource() *operatorsv1alpha1.CatalogSource {
181+
return &operatorsv1alpha1.CatalogSource{
182+
ObjectMeta: metav1.ObjectMeta{
183+
Name: c.name,
184+
Namespace: c.namespace,
185+
},
186+
Spec: operatorsv1alpha1.CatalogSourceSpec{
187+
SourceType: operatorsv1alpha1.SourceTypeGrpc,
188+
Address: fmt.Sprintf("%s.%s.svc:50051", c.serviceName, c.namespace),
189+
},
190+
}
191+
}
192+
193+
func (c *magicCatalog) makeCatalogSourcePod() *corev1.Pod {
194+
195+
const (
196+
image = "quay.io/operator-framework/upstream-opm-builder"
197+
readinessDelay int32 = 5
198+
livenessDelay int32 = 10
199+
volumeMountName string = "fbc-catalog"
200+
)
201+
202+
readOnlyRootFilesystem := false
203+
204+
return &corev1.Pod{
205+
ObjectMeta: metav1.ObjectMeta{
206+
Name: c.podName,
207+
Namespace: c.namespace,
208+
Labels: c.makeCatalogSourcePodLabels(),
209+
},
210+
Spec: corev1.PodSpec{
211+
Containers: []corev1.Container{
212+
{
213+
Name: "catalog",
214+
Image: image,
215+
Command: []string{"opm", "serve", catalogMountPath},
216+
Ports: []corev1.ContainerPort{
217+
{
218+
Name: "grpc",
219+
ContainerPort: 50051,
220+
},
221+
},
222+
ReadinessProbe: &corev1.Probe{
223+
Handler: corev1.Handler{
224+
Exec: &corev1.ExecAction{
225+
Command: []string{"grpc_health_probe", "-addr=:50051"},
226+
},
227+
},
228+
InitialDelaySeconds: readinessDelay,
229+
TimeoutSeconds: 5,
230+
},
231+
LivenessProbe: &corev1.Probe{
232+
Handler: corev1.Handler{
233+
Exec: &corev1.ExecAction{
234+
Command: []string{"grpc_health_probe", "-addr=:50051"},
235+
},
236+
},
237+
InitialDelaySeconds: livenessDelay,
238+
TimeoutSeconds: 5,
239+
},
240+
Resources: corev1.ResourceRequirements{
241+
Requests: corev1.ResourceList{
242+
corev1.ResourceCPU: resource.MustParse("10m"),
243+
corev1.ResourceMemory: resource.MustParse("50Mi"),
244+
},
245+
},
246+
SecurityContext: &corev1.SecurityContext{
247+
ReadOnlyRootFilesystem: &readOnlyRootFilesystem,
248+
},
249+
ImagePullPolicy: corev1.PullAlways,
250+
TerminationMessagePolicy: corev1.TerminationMessageFallbackToLogsOnError,
251+
VolumeMounts: []corev1.VolumeMount{
252+
{
253+
Name: volumeMountName,
254+
MountPath: catalogMountPath,
255+
ReadOnly: true,
256+
},
257+
},
258+
},
259+
},
260+
Volumes: []corev1.Volume{
261+
{
262+
Name: volumeMountName,
263+
VolumeSource: corev1.VolumeSource{
264+
ConfigMap: &corev1.ConfigMapVolumeSource{
265+
LocalObjectReference: corev1.LocalObjectReference{
266+
Name: c.configMapName,
267+
},
268+
},
269+
},
270+
},
271+
},
272+
},
273+
}
274+
}
275+
276+
func (c *magicCatalog) makeCatalogSourcePodLabels() map[string]string {
277+
return map[string]string{
278+
olmCatalogLabel: c.name,
279+
}
280+
}

test/e2e/magic_catalog_test.go

+40
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package e2e
2+
3+
import (
4+
"context"
5+
6+
. "github.com/onsi/ginkgo"
7+
. "github.com/onsi/gomega"
8+
"github.com/operator-framework/operator-lifecycle-manager/test/e2e/ctx"
9+
corev1 "k8s.io/api/core/v1"
10+
)
11+
12+
var _ = Describe("MagicCatalog", func() {
13+
var (
14+
generatedNamespace corev1.Namespace
15+
)
16+
17+
BeforeEach(func() {
18+
generatedNamespace = SetupGeneratedTestNamespace(genName("magic-catalog-e2e-"))
19+
})
20+
21+
AfterEach(func() {
22+
TeardownNamespace(generatedNamespace.GetName())
23+
})
24+
25+
It("Deploys and Undeploys a File-based Catalog", func() {
26+
// create dependencies
27+
const catalogName = "test"
28+
namespace := generatedNamespace.GetName()
29+
kubeClient := ctx.Ctx().Client()
30+
provider, err := NewFileBasedFiledBasedCatalogProvider("../test/e2e/testdata/fbc_catalog.json")
31+
Expect(err).To(BeNil())
32+
33+
// create and deploy and undeploy the magic catalog
34+
magicCatalog := NewMagicCatalog(kubeClient, namespace, catalogName, provider)
35+
36+
// deployment blocks until the catalog source has reached a READY status
37+
Expect(magicCatalog.DeployCatalog(context.TODO())).To(BeNil())
38+
Expect(magicCatalog.UndeployCatalog(context.TODO())).To(BeNil())
39+
})
40+
})

test/e2e/testdata/fbc_catalog.json

+38
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
{
2+
"schema": "olm.package",
3+
"name": "packageA",
4+
"defaultChannel": "stable"
5+
}
6+
{
7+
"schema": "olm.channel",
8+
"name": "stable",
9+
"package": "packageA",
10+
"entries": [
11+
{
12+
"name": "packageA.v1.0.0"
13+
}
14+
]
15+
}
16+
{
17+
"schema": "olm.bundle",
18+
"name": "packageA.v1.0.0",
19+
"package": "packageA",
20+
"image": "packageA:v1.0.0",
21+
"properties": [
22+
{
23+
"type": "olm.gvk",
24+
"value": {
25+
"group": "example.com",
26+
"kind": "TestA",
27+
"version": "v1alpha1"
28+
}
29+
},
30+
{
31+
"type": "olm.package",
32+
"value": {
33+
"packageName": "packageA",
34+
"version": "1.0.0"
35+
}
36+
}
37+
]
38+
}

0 commit comments

Comments
 (0)