Skip to content

Commit 09df3ba

Browse files
committed
improve workflow for installing single-namespace Operators from Marketplace
1 parent afb2707 commit 09df3ba

28 files changed

+896
-296
lines changed

frontend/__mocks__/k8sResourcesMocks.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ export const testClusterServiceVersion: ClusterServiceVersionKind = {
5858
'alm-owner-testapp': 'testapp.clusterserviceversions.operators.coreos.com.v1alpha1',
5959
},
6060
},
61+
installModes: [],
6162
install: {
6263
strategy: 'Deployment',
6364
spec: {
@@ -129,6 +130,7 @@ export const localClusterServiceVersion: ClusterServiceVersionKind = {
129130
'alm-owner-local-testapp': 'local-testapp.clusterserviceversions.operators.coreos.com.v1alpha1',
130131
},
131132
},
133+
installModes: [],
132134
install: {
133135
strategy: 'Deployment',
134136
spec: {
@@ -268,6 +270,7 @@ export const testPackageManifest: PackageManifestKind = {
268270
provider: {
269271
name: 'CoreOS, Inc',
270272
},
273+
installModes: [],
271274
},
272275
}],
273276
defaultChannel: 'alpha',

frontend/__mocks__/operatorHubItemsMocks.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ const amqPackageManifest = {
4040
provider: {
4141
name: 'Red Hat',
4242
},
43+
installModes: [],
4344
annotations: {
4445
'alm-examples': '[{"apiVersion":"kafka.strimzi.io/v1alpha1","kind":"Kafka","metadata":{"name":"my-cluster"},"spec":{"kafka":{"replicas":3,"listeners":{"plain":{},"tls":{}},"config":{"offsets.topic.replication.factor":3,"transaction.state.log.replication.factor":3,"transaction.state.log.min.isr":2},"storage":{"type":"ephemeral"}},"zookeeper":{"replicas":3,"storage":{"type":"ephemeral"}},"entityOperator":{"topicOperator":{},"userOperator":{}}}}, {"apiVersion":"kafka.strimzi.io/v1alpha1","kind":"KafkaConnect","metadata":{"name":"my-connect-cluster"},"spec":{"replicas":1,"bootstrapServers":"my-cluster-kafka-bootstrap:9093","tls":{"trustedCertificates":[{"secretName":"my-cluster-cluster-ca-cert","certificate":"ca.crt"}]}}}, {"apiVersion":"kafka.strimzi.io/v1alpha1","kind":"KafkaConnectS2I","metadata":{"name":"my-connect-cluster"},"spec":{"replicas":1,"bootstrapServers":"my-cluster-kafka-bootstrap:9093","tls":{"trustedCertificates":[{"secretName":"my-cluster-cluster-ca-cert","certificate":"ca.crt"}]}}}, {"apiVersion":"kafka.strimzi.io/v1alpha1","kind":"KafkaTopic","metadata":{"name":"my-topic","labels":{"strimzi.io/cluster":"my-cluster"}},"spec":{"partitions":10,"replicas":3,"config":{"retention.ms":604800000,"segment.bytes":1073741824}}}, {"apiVersion":"kafka.strimzi.io/v1alpha1","kind":"KafkaUser","metadata":{"name":"my-user","labels":{"strimzi.io/cluster":"my-cluster"}},"spec":{"authentication":{"type":"tls"},"authorization":{"type":"simple","acls":[{"resource":{"type":"topic","name":"my-topic","patternType":"literal"},"operation":"Read","host":"*"},{"resource":{"type":"topic","name":"my-topic","patternType":"literal"},"operation":"Describe","host":"*"},{"resource":{"type":"group","name":"my-group","patternType":"literal"},"operation":"Read","host":"*"},{"resource":{"type":"topic","name":"my-topic","patternType":"literal"},"operation":"Write","host":"*"},{"resource":{"type":"topic","name":"my-topic","patternType":"literal"},"operation":"Create","host":"*"},{"resource":{"type":"topic","name":"my-topic","patternType":"literal"},"operation":"Describe","host":"*"}]}}}]',
4546
description: '**Red Hat AMQ Streams** is a massively scalable, distributed, and high performance data streaming platform based on the Apache Kafka project. \nAMQ Streams provides an event streaming backbone that allows microservices and other application components to exchange data with extremely high throughput and low latency.\n\n**The core capabilities include**\n* A pub/sub messaging model, similar to a traditional enterprise messaging system, in which application components publish and consume events to/from an ordered stream\n* The long term, fault-tolerant storage of events\n* The ability for a consumer to replay streams of events\n* The ability to partition topics for horizontal scalability\n\n# Before you start\n\n1. Create AMQ Streams Cluster Roles\n```\n$ oc apply -f http://amq.io/amqstreams/rbac.yaml\n```\n2. Create following bindings\n```\n$ oc adm policy add-cluster-role-to-user strimzi-cluster-operator -z strimzi-cluster-operator --namespace <namespace>\n$ oc adm policy add-cluster-role-to-user strimzi-kafka-broker -z strimzi-cluster-operator --namespace <namespace>\n```',
@@ -89,6 +90,7 @@ const etcdPackageManifest = {
8990
provider: {
9091
name: 'CoreOS, Inc',
9192
},
93+
installModes: [],
9294
annotations: {
9395
'alm-examples': '[{"apiVersion":"etcd.database.coreos.com/v1beta2","kind":"EtcdCluster","metadata":{"name":"example","namespace":"default"},"spec":{"size":3,"version":"3.2.13"}},{"apiVersion":"etcd.database.coreos.com/v1beta2","kind":"EtcdRestore","metadata":{"name":"example-etcd-cluster"},"spec":{"etcdCluster":{"name":"example-etcd-cluster"},"backupStorageType":"S3","s3":{"path":"<full-s3-path>","awsSecret":"<aws-secret>"}}},{"apiVersion":"etcd.database.coreos.com/v1beta2","kind":"EtcdBackup","metadata":{"name":"example-etcd-cluster-backup"},"spec":{"etcdEndpoints":["<etcd-cluster-endpoints>"],"storageType":"S3","s3":{"path":"<full-s3-path>","awsSecret":"<aws-secret>"}}}]',
9496
'tectonic-visibility': 'ocs',
@@ -136,6 +138,7 @@ const federationv2PackageManifest = {
136138
provider: {
137139
name: 'Red Hat',
138140
},
141+
installModes: [],
139142
annotations: {
140143
description: 'Kubernetes Federation V2 namespace-scoped installation',
141144
categories: '',
@@ -184,6 +187,7 @@ const prometheusPackageManifest = {
184187
provider: {
185188
name: 'Red Hat',
186189
},
190+
installModes: [],
187191
annotations: {
188192
'alm-examples': '[{"apiVersion":"monitoring.coreos.com/v1","kind":"Prometheus","metadata":{"name":"example","labels":{"prometheus":"k8s"}},"spec":{"replicas":2,"version":"v2.3.2","serviceAccountName":"prometheus-k8s","securityContext": {}, "serviceMonitorSelector":{"matchExpressions":[{"key":"k8s-app","operator":"Exists"}]},"ruleSelector":{"matchLabels":{"role":"prometheus-rulefiles","prometheus":"k8s"}},"alerting":{"alertmanagers":[{"namespace":"monitoring","name":"alertmanager-main","port":"web"}]}}},{"apiVersion":"monitoring.coreos.com/v1","kind":"ServiceMonitor","metadata":{"name":"example","labels":{"k8s-app":"prometheus"}},"spec":{"selector":{"matchLabels":{"k8s-app":"prometheus"}},"endpoints":[{"port":"web","interval":"30s"}]}},{"apiVersion":"monitoring.coreos.com/v1","kind":"Alertmanager","metadata":{"name":"alertmanager-main"},"spec":{"replicas":3, "securityContext": {}}}]',
189193
description: 'The Prometheus Operator for Kubernetes provides easy monitoring definitions for Kubernetes services and deployment and management of Prometheus instances.',
@@ -230,6 +234,7 @@ const svcatPackageManifest = {
230234
provider: {
231235
name: 'Red Hat',
232236
},
237+
installModes: [],
233238
annotations: {
234239
description: 'Service Catalog lets you provision cloud services directly from the comfort of native Kubernetes tooling.',
235240
categories: 'catalog',
@@ -276,6 +281,7 @@ const dummyPackageManifest = {
276281
provider: {
277282
name: 'Dummy',
278283
},
284+
installModes: [],
279285
annotations: {
280286
description: 'Dummy is not a real operator',
281287
categories: 'dummy',

frontend/__tests__/components/modals/subscription-channel-modal.spec.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ describe(SubscriptionChannelModal.name, () => {
3737
provider: {
3838
name: 'CoreOS, Inc',
3939
},
40+
installModes: [],
4041
},
4142
}, {
4243
name: 'nightly',
@@ -48,6 +49,7 @@ describe(SubscriptionChannelModal.name, () => {
4849
provider: {
4950
name: 'CoreOS, Inc',
5051
},
52+
installModes: [],
5153
},
5254
}];
5355

frontend/__tests__/components/operator-lifecycle-manager/operator-group.spec.tsx

Lines changed: 112 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
/* eslint-disable no-undef, no-unused-vars */
22

33
import * as React from 'react';
4+
import * as _ from 'lodash-es';
45
import { shallow } from 'enzyme';
56

6-
import { requireOperatorGroup, NoOperatorGroupMsg } from '../../../public/components/operator-lifecycle-manager/operator-group';
7-
import { testOperatorGroup } from '../../../__mocks__/k8sResourcesMocks';
7+
import { requireOperatorGroup, NoOperatorGroupMsg, supports, InstallModeSet, InstallModeType, installedFor } from '../../../public/components/operator-lifecycle-manager/operator-group';
8+
import { OperatorGroupKind, SubscriptionKind } from '../../../public/components/operator-lifecycle-manager';
9+
import { testOperatorGroup, testSubscription } from '../../../__mocks__/k8sResourcesMocks';
810

911
describe('requireOperatorGroup', () => {
1012
const SomeComponent = () => <div>Requires OperatorGroup</div>;
@@ -33,3 +35,111 @@ describe('requireOperatorGroup', () => {
3335
expect(wrapper.find(NoOperatorGroupMsg).exists()).toBe(false);
3436
});
3537
});
38+
39+
describe('installedFor', () => {
40+
const pkgName = testSubscription.spec.name;
41+
const ns = testSubscription.metadata.namespace;
42+
let subscriptions: SubscriptionKind[];
43+
let operatorGroups: OperatorGroupKind[];
44+
45+
beforeEach(() => {
46+
subscriptions = [];
47+
operatorGroups = [];
48+
});
49+
50+
it('returns false if no `Subscriptions` exist for the given package', () => {
51+
subscriptions = [testSubscription];
52+
operatorGroups = [{...testOperatorGroup, status: {namespaces: [ns], lastUpdated: null}}];
53+
54+
expect(installedFor(subscriptions)(operatorGroups)('new-operator')(ns)).toBe(false);
55+
});
56+
57+
it('returns false if no `OperatorGroups` target the given namespace', () => {
58+
subscriptions = [testSubscription];
59+
operatorGroups = [{...testOperatorGroup, status: {namespaces: ['prod-a', 'prod-b'], lastUpdated: null}}];
60+
61+
expect(installedFor(subscriptions)(operatorGroups)(pkgName)(ns)).toBe(false);
62+
});
63+
64+
it('returns false if checking for `all-namespaces`', () => {
65+
subscriptions = [testSubscription];
66+
operatorGroups = [{...testOperatorGroup, status: {namespaces: [ns], lastUpdated: null}}];
67+
68+
expect(installedFor(subscriptions)(operatorGroups)(pkgName)('')).toBe(false);
69+
});
70+
71+
it('returns true if `Subscription` exists in the "global" `OperatorGroup`', () => {
72+
subscriptions = [testSubscription];
73+
operatorGroups = [{...testOperatorGroup, status: {namespaces: [''], lastUpdated: null}}];
74+
75+
expect(installedFor(subscriptions)(operatorGroups)(pkgName)(ns)).toBe(true);
76+
});
77+
78+
it('returns true if `Subscription` exists in an `OperatorGroup` that targets given namespace', () => {
79+
subscriptions = [testSubscription];
80+
operatorGroups = [{...testOperatorGroup, status: {namespaces: [ns], lastUpdated: null}}];
81+
82+
expect(installedFor(subscriptions)(operatorGroups)(pkgName)(ns)).toBe(true);
83+
});
84+
});
85+
86+
describe('supports', () => {
87+
let set: InstallModeSet;
88+
let ownNamespaceGroup: OperatorGroupKind;
89+
let singleNamespaceGroup: OperatorGroupKind;
90+
let multiNamespaceGroup: OperatorGroupKind;
91+
let allNamespacesGroup: OperatorGroupKind;
92+
93+
beforeEach(() => {
94+
ownNamespaceGroup = _.cloneDeep(testOperatorGroup);
95+
ownNamespaceGroup.status = {namespaces: [ownNamespaceGroup.metadata.namespace], lastUpdated: null};
96+
singleNamespaceGroup = _.cloneDeep(testOperatorGroup);
97+
singleNamespaceGroup.status = {namespaces: ['test-ns'], lastUpdated: null};
98+
multiNamespaceGroup = _.cloneDeep(testOperatorGroup);
99+
multiNamespaceGroup.status = {namespaces: ['test-ns', 'default'], lastUpdated: null};
100+
allNamespacesGroup = _.cloneDeep(testOperatorGroup);
101+
allNamespacesGroup.status = {namespaces: [''], lastUpdated: null};
102+
});
103+
104+
it('correctly returns for an Operator that can only run in its own namespace', () => {
105+
set = [
106+
{type: InstallModeType.InstallModeTypeOwnNamespace, supported: true},
107+
{type: InstallModeType.InstallModeTypeSingleNamespace, supported: true},
108+
{type: InstallModeType.InstallModeTypeMultiNamespace, supported: false},
109+
{type: InstallModeType.InstallModeTypeAllNamespaces, supported: false},
110+
];
111+
112+
expect(supports(set)(ownNamespaceGroup)).toBe(true);
113+
expect(supports(set)(singleNamespaceGroup)).toBe(true);
114+
expect(supports(set)(multiNamespaceGroup)).toBe(false);
115+
expect(supports(set)(allNamespacesGroup)).toBe(false);
116+
});
117+
118+
it('correctly returns for an Operator which can run in several namespaces', () => {
119+
set = [
120+
{type: InstallModeType.InstallModeTypeOwnNamespace, supported: true},
121+
{type: InstallModeType.InstallModeTypeSingleNamespace, supported: true},
122+
{type: InstallModeType.InstallModeTypeMultiNamespace, supported: true},
123+
{type: InstallModeType.InstallModeTypeAllNamespaces, supported: false},
124+
];
125+
126+
expect(supports(set)(ownNamespaceGroup)).toBe(true);
127+
expect(supports(set)(singleNamespaceGroup)).toBe(true);
128+
expect(supports(set)(multiNamespaceGroup)).toBe(true);
129+
expect(supports(set)(allNamespacesGroup)).toBe(false);
130+
});
131+
132+
it('correctly returns for an Operator which can only run in all namespaces', () => {
133+
set = [
134+
{type: InstallModeType.InstallModeTypeOwnNamespace, supported: true},
135+
{type: InstallModeType.InstallModeTypeSingleNamespace, supported: false},
136+
{type: InstallModeType.InstallModeTypeMultiNamespace, supported: false},
137+
{type: InstallModeType.InstallModeTypeAllNamespaces, supported: true},
138+
];
139+
140+
expect(supports(set)(ownNamespaceGroup)).toBe(false);
141+
expect(supports(set)(singleNamespaceGroup)).toBe(false);
142+
expect(supports(set)(multiNamespaceGroup)).toBe(false);
143+
expect(supports(set)(allNamespacesGroup)).toBe(true);
144+
});
145+
});

frontend/__tests__/components/operator-lifecycle-manager/package-manifest.spec.tsx

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ describe(PackageManifestRow.displayName, () => {
3535
let wrapper: ShallowWrapper<PackageManifestRowProps>;
3636

3737
beforeEach(() => {
38-
wrapper = shallow(<PackageManifestRow obj={testPackageManifest} catalogSourceNamespace={testCatalogSource.metadata.namespace} catalogSourceName={testCatalogSource.metadata.name} subscription={testSubscription} defaultNS="default" />);
38+
wrapper = shallow(<PackageManifestRow obj={testPackageManifest} catalogSourceNamespace={testCatalogSource.metadata.namespace} catalogSourceName={testCatalogSource.metadata.name} subscription={testSubscription} defaultNS="default" canSubscribe={true} />);
3939
});
4040

4141
it('renders column for package name and logo', () => {
@@ -58,11 +58,15 @@ describe(PackageManifestRow.displayName, () => {
5858
expect(wrapper.find('.co-resource-list__item').childAt(2).find(Link).at(0).childAt(0).text()).toEqual('View');
5959
});
6060

61-
it('renders button to create new subscription', () => {
62-
wrapper = wrapper.setProps({subscription: null});
63-
61+
it('renders button to create new subscription if `canSubscribe` is true', () => {
6462
expect(wrapper.find('.co-resource-list__item').childAt(2).find('button').text()).toEqual('Create Subscription');
6563
});
64+
65+
it('does not render button to create new subscription if `canSubscribe` is false', () => {
66+
wrapper = wrapper.setProps({canSubscribe: false});
67+
68+
expect(wrapper.find('.co-resource-list__item').childAt(2).find('button').exists()).toBe(false);
69+
});
6670
});
6771

6872
describe(PackageManifestList.displayName, () => {
@@ -74,7 +78,7 @@ describe(PackageManifestList.displayName, () => {
7478
otherPackageManifest.status.catalogSource = 'another-catalog-source';
7579
otherPackageManifest.status.catalogSourceDisplayName = 'Another Catalog Source';
7680
otherPackageManifest.status.catalogSourcePublisher = 'Some Publisher';
77-
packages = [testPackageManifest, otherPackageManifest];
81+
packages = [otherPackageManifest, testPackageManifest];
7882

7983
wrapper = shallow(<PackageManifestList.WrappedComponent loaded={true} data={packages} operatorGroup={null} subscription={null} />);
8084
});
@@ -83,7 +87,6 @@ describe(PackageManifestList.displayName, () => {
8387
expect(wrapper.find('.co-catalogsource-list__section').length).toEqual(2);
8488
packages.forEach(({status}, i) => {
8589
expect(wrapper.find('.co-catalogsource-list__section').at(i).find('h3').text()).toEqual(status.catalogSourceDisplayName);
86-
expect(wrapper.find('.co-catalogsource-list__section').at(i).find('h3').text()).toEqual(status.catalogSourceDisplayName);
8790
});
8891
});
8992

frontend/integration-tests/protractor.conf.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ export const config: Config = {
8989
crud: ['tests/base.scenario.ts', 'tests/crud.scenario.ts', 'tests/secrets.scenario.ts', 'tests/filter.scenario.ts', 'tests/modal-annotations.scenario.ts', 'tests/environment.scenario.ts'],
9090
monitoring: ['tests/base.scenario.ts', 'tests/monitoring.scenario.ts'],
9191
newApp: ['tests/base.scenario.ts', 'tests/overview/overview.scenario.ts', 'tests/source-to-image.scenario.ts', 'tests/deploy-image.scenario.ts'],
92-
olm: ['tests/base.scenario.ts', 'tests/olm/descriptors.scenario.ts', 'tests/olm/catalog.scenario.ts', 'tests/olm/etcd.scenario.ts'],
92+
olm: ['tests/base.scenario.ts', 'tests/olm/descriptors.scenario.ts', 'tests/olm/catalog.scenario.ts', 'tests/olm/prometheus.scenario.ts', 'tests/olm/etcd.scenario.ts'],
9393
olmUpgrade: ['tests/base.scenario.ts', 'tests/olm/update-channel-approval.scenario.ts'],
9494
performance: ['tests/base.scenario.ts', 'tests/performance.scenario.ts'],
9595
serviceCatalog: ['tests/base.scenario.ts', 'tests/service-catalog/service-catalog.scenario.ts', 'tests/service-catalog/service-broker.scenario.ts', 'tests/service-catalog/service-class.scenario.ts', 'tests/service-catalog/service-binding.scenario.ts', 'tests/developer-catalog.scenario.ts'],
@@ -108,6 +108,7 @@ export const config: Config = {
108108
'tests/olm/descriptors.scenario.ts',
109109
'tests/olm/catalog.scenario.ts',
110110
'tests/operator-hub/operator-hub.scenario.ts',
111+
'tests/olm/prometheus.scenario.ts',
111112
'tests/olm/etcd.scenario.ts'],
112113
all: ['tests/base.scenario.ts',
113114
'tests/crud.scenario.ts',

frontend/integration-tests/tests/olm/catalog.scenario.ts

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,29 @@ import * as sidenavView from '../../views/sidenav.view';
99

1010
describe('Installing a service from a Catalog Source', () => {
1111
const openCloudServices = new Set(['etcd', 'Prometheus Operator', 'AMQ Streams', 'Service Catalog', 'FederationV2']);
12+
const operatorGroupName = 'test-operatorgroup';
1213

1314
beforeAll(async() => {
15+
const catalogSource = {
16+
apiVersion: 'operators.coreos.com/v1alpha1',
17+
kind: 'CatalogSource',
18+
metadata: {name: 'console-e2e'},
19+
spec: {
20+
sourceType: 'grpc',
21+
image: 'quay.io/operatorframework/operator-manifests@sha256:ac3140d9f2d2a3cf5446d82048ddf64ad5cd13b31070d1c4b5c689b7272062dc',
22+
displayName: 'Console E2E Operators',
23+
publisher: 'Red Hat, Inc',
24+
},
25+
};
26+
execSync(`echo '${JSON.stringify(catalogSource)}' | kubectl create -n ${testName} -f -`);
27+
// FIXME(alecmerdler): Wait until `PackageManifests` are being served from registry pod
28+
browser.sleep(30000);
29+
1430
const operatorGroup = {
1531
apiVersion: 'operators.coreos.com/v1alpha2',
1632
kind: 'OperatorGroup',
17-
metadata: {name: 'test-operatorgroup'},
18-
spec: {selector: {matchLabels: {'test-name': testName}}},
33+
metadata: {name: operatorGroupName},
34+
spec: {targetNamespaces: [testName]},
1935
};
2036
execSync(`echo '${JSON.stringify(operatorGroup)}' | kubectl create -n ${testName} -f -`);
2137

@@ -28,6 +44,10 @@ describe('Installing a service from a Catalog Source', () => {
2844
checkErrors();
2945
});
3046

47+
afterAll(() => {
48+
execSync(`kubectl delete operatorgroup -n ${testName} ${operatorGroupName}`);
49+
});
50+
3151
it('displays `Catalog` tab in navigation sidebar', async() => {
3252
await browser.wait(until.presenceOf(sidenavView.navSectionFor('Catalog')));
3353

0 commit comments

Comments
 (0)