Skip to content

Commit 55ac82b

Browse files
committed
improve workflow for installing single-namespace Operators from Marketplace
1 parent 361814c commit 55ac82b

21 files changed

+542
-198
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/public/components/app.jsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -193,8 +193,11 @@ class App extends React.PureComponent {
193193
<LazyRoute path="/cluster-health" exact loader={() => import('./cluster-health' /* webpackChunkName: "cluster-health" */).then(m => m.ClusterHealth)} />
194194
<LazyRoute path="/start-guide" exact loader={() => import('./start-guide' /* webpackChunkName: "start-guide" */).then(m => m.StartGuidePage)} />
195195

196-
<LazyRoute path="/operatorhub" exact loader={() => import('./operator-hub/operator-hub-page' /* webpackChunkName: "operator-hub" */).then(m => m.OperatorHubPage)} />
196+
<LazyRoute path="/operatorhub/all-namespaces" exact loader={() => import('./operator-hub/operator-hub-page' /* webpackChunkName: "operator-hub" */).then(m => m.OperatorHubPage)} />
197+
<LazyRoute path="/operatorhub/ns/:ns" exact loader={() => import('./operator-hub/operator-hub-page' /* webpackChunkName: "operator-hub" */).then(m => m.OperatorHubPage)} />
198+
<Route path="/operatorhub" exact component={NamespaceRedirect} />
197199
<LazyRoute path="/operatorhub/subscribe" exact loader={() => import('./operator-hub/operator-hub-subscribe' /* webpackChunkName: "operator-hub-subscribe" */).then(m => m.OperatorHubSubscribePage)} />
200+
198201
<LazyRoute path="/catalog/all-namespaces" exact loader={() => import('./catalog/catalog-page' /* webpackChunkName: "catalog" */).then(m => m.CatalogPage)} />
199202
<LazyRoute path="/catalog/ns/:ns" exact loader={() => import('./catalog/catalog-page' /* webpackChunkName: "catalog" */).then(m => m.CatalogPage)} />
200203
<Route path="/catalog" exact component={NamespaceRedirect} />

frontend/public/components/nav.jsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,9 @@ import {
2323
MachineSetModel,
2424
PackageManifestModel,
2525
SubscriptionModel,
26+
OperatorGroupModel,
2627
} from '../models';
2728
import { referenceForModel } from '../module/k8s';
28-
2929
import { history, stripBasePath } from './utils';
3030

3131
export const matchesPath = (resourcePath, prefix) => resourcePath === prefix || _.startsWith(resourcePath, `${prefix}/`);
@@ -279,6 +279,8 @@ const operatorManagementStartsWith = [
279279
InstallPlanModel.path,
280280
referenceForModel(CatalogSourceModel),
281281
CatalogSourceModel.path,
282+
referenceForModel(OperatorGroupModel),
283+
OperatorGroupModel.path,
282284
];
283285
const provisionedServicesStartsWith = ['serviceinstances', 'servicebindings'];
284286
const brokerManagementStartsWith = ['clusterservicebrokers', 'clusterserviceclasses'];

frontend/public/components/operator-hub/operator-hub-item-details.tsx

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { MarkdownView } from '../operator-lifecycle-manager/clusterserviceversio
88
import { history, ExternalLink } from '../utils';
99
import { RH_OPERATOR_SUPPORT_POLICY_LINK } from '../../const';
1010

11-
export const OperatorHubItemDetails: React.SFC<OperatorHubItemDetailsProps> = ({item, closeOverlay}) => {
11+
export const OperatorHubItemDetails: React.SFC<OperatorHubItemDetailsProps> = ({item, closeOverlay, namespace}) => {
1212
if (!item) {
1313
return null;
1414
}
@@ -70,10 +70,8 @@ export const OperatorHubItemDetails: React.SFC<OperatorHubItemDetailsProps> = ({
7070

7171
const onActionClick = () => {
7272
if (!installed) {
73-
history.push(`/operatorhub/subscribe?pkg=${item.obj.metadata.name}&catalog=${catalogSource}&catalogNamespace=${catalogSourceNamespace}`);
74-
return;
73+
return history.push(`/operatorhub/subscribe?pkg=${item.obj.metadata.name}&catalog=${catalogSource}&catalogNamespace=${catalogSourceNamespace}&targetNamespace=${namespace}`);
7574
}
76-
7775
// TODO: Allow for Manage button to navigate to the CSV details for the item
7876
};
7977

@@ -126,6 +124,7 @@ OperatorHubItemDetails.defaultProps = {
126124
};
127125

128126
export type OperatorHubItemDetailsProps = {
127+
namespace?: string;
129128
item: any;
130129
closeOverlay: () => void;
131130
};

0 commit comments

Comments
 (0)