Skip to content

Commit ea91130

Browse files
committed
improve workflow for installing single-namespace Operators from Marketplace
1 parent 53fface commit ea91130

20 files changed

+470
-184
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: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
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 { requireOperatorGroup, NoOperatorGroupMsg, supports, InstallModeSet, InstallModeType } from '../../../public/components/operator-lifecycle-manager/operator-group';
8+
import { OperatorGroupKind } from '../../../public/components/operator-lifecycle-manager';
79
import { testOperatorGroup } from '../../../__mocks__/k8sResourcesMocks';
810

911
describe('requireOperatorGroup', () => {
@@ -33,3 +35,64 @@ describe('requireOperatorGroup', () => {
3335
expect(wrapper.find(NoOperatorGroupMsg).exists()).toBe(false);
3436
});
3537
});
38+
39+
describe('supports', () => {
40+
let set: InstallModeSet;
41+
let ownNamespaceGroup: OperatorGroupKind;
42+
let singleNamespaceGroup: OperatorGroupKind;
43+
let multiNamespaceGroup: OperatorGroupKind;
44+
let allNamespacesGroup: OperatorGroupKind;
45+
46+
beforeEach(() => {
47+
ownNamespaceGroup = _.cloneDeep(testOperatorGroup);
48+
ownNamespaceGroup.status = {namespaces: [ownNamespaceGroup.metadata.namespace], lastUpdated: null};
49+
singleNamespaceGroup = _.cloneDeep(testOperatorGroup);
50+
singleNamespaceGroup.status = {namespaces: ['test-ns'], lastUpdated: null};
51+
multiNamespaceGroup = _.cloneDeep(testOperatorGroup);
52+
multiNamespaceGroup.status = {namespaces: ['test-ns', 'default'], lastUpdated: null};
53+
allNamespacesGroup = _.cloneDeep(testOperatorGroup);
54+
allNamespacesGroup.status = {namespaces: [''], lastUpdated: null};
55+
});
56+
57+
it('correctly returns for an Operator that can only run in its own namespace', () => {
58+
set = [
59+
{type: InstallModeType.InstallModeTypeOwnNamespace, supported: true},
60+
{type: InstallModeType.InstallModeTypeSingleNamespace, supported: true},
61+
{type: InstallModeType.InstallModeTypeMultiNamespace, supported: false},
62+
{type: InstallModeType.InstallModeTypeAllNamespaces, supported: false},
63+
];
64+
65+
expect(supports(set)(ownNamespaceGroup)).toBe(true);
66+
expect(supports(set)(singleNamespaceGroup)).toBe(true);
67+
expect(supports(set)(multiNamespaceGroup)).toBe(false);
68+
expect(supports(set)(allNamespacesGroup)).toBe(false);
69+
});
70+
71+
it('correctly returns for an Operator which can run in several namespaces', () => {
72+
set = [
73+
{type: InstallModeType.InstallModeTypeOwnNamespace, supported: true},
74+
{type: InstallModeType.InstallModeTypeSingleNamespace, supported: true},
75+
{type: InstallModeType.InstallModeTypeMultiNamespace, supported: true},
76+
{type: InstallModeType.InstallModeTypeAllNamespaces, supported: false},
77+
];
78+
79+
expect(supports(set)(ownNamespaceGroup)).toBe(true);
80+
expect(supports(set)(singleNamespaceGroup)).toBe(true);
81+
expect(supports(set)(multiNamespaceGroup)).toBe(true);
82+
expect(supports(set)(allNamespacesGroup)).toBe(false);
83+
});
84+
85+
it('correctly returns for an Operator which can only run in all namespaces', () => {
86+
set = [
87+
{type: InstallModeType.InstallModeTypeOwnNamespace, supported: true},
88+
{type: InstallModeType.InstallModeTypeSingleNamespace, supported: false},
89+
{type: InstallModeType.InstallModeTypeMultiNamespace, supported: false},
90+
{type: InstallModeType.InstallModeTypeAllNamespaces, supported: true},
91+
];
92+
93+
expect(supports(set)(ownNamespaceGroup)).toBe(false);
94+
expect(supports(set)(singleNamespaceGroup)).toBe(false);
95+
expect(supports(set)(multiNamespaceGroup)).toBe(false);
96+
expect(supports(set)(allNamespacesGroup)).toBe(true);
97+
});
98+
});

frontend/public/components/app.jsx

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

178-
<LazyRoute path="/operatorhub" exact loader={() => import('./operator-hub/operator-hub-page' /* webpackChunkName: "operator-hub" */).then(m => m.OperatorHubPage)} />
178+
<LazyRoute path="/operatorhub/all-namespaces" exact loader={() => import('./operator-hub/operator-hub-page' /* webpackChunkName: "operator-hub" */).then(m => m.OperatorHubPage)} />
179+
<LazyRoute path="/operatorhub/ns/:ns" exact loader={() => import('./operator-hub/operator-hub-page' /* webpackChunkName: "operator-hub" */).then(m => m.OperatorHubPage)} />
180+
<Route path="/operatorhub" exact component={NamespaceRedirect} />
179181
<LazyRoute path="/operatorhub/subscribe" exact loader={() => import('./operator-hub/operator-hub-subscribe' /* webpackChunkName: "operator-hub-subscribe" */).then(m => m.OperatorHubSubscribePage)} />
182+
180183
<LazyRoute path="/catalog/all-namespaces" exact loader={() => import('./catalog/catalog-page' /* webpackChunkName: "catalog" */).then(m => m.CatalogPage)} />
181184
<LazyRoute path="/catalog/ns/:ns" exact loader={() => import('./catalog/catalog-page' /* webpackChunkName: "catalog" */).then(m => m.CatalogPage)} />
182185
<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}/`);
@@ -277,6 +277,8 @@ const operatorManagementStartsWith = [
277277
InstallPlanModel.path,
278278
referenceForModel(CatalogSourceModel),
279279
CatalogSourceModel.path,
280+
referenceForModel(OperatorGroupModel),
281+
OperatorGroupModel.path,
280282
];
281283
const provisionedServicesStartsWith = ['serviceinstances', 'servicebindings'];
282284
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
};

frontend/public/components/operator-hub/operator-hub-items.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -335,6 +335,7 @@ export const OperatorHubTileView = requireOperatorGroup(
335335
const { uid, name, imgUrl, iconClass, provider, description, installed } = item;
336336
const normalizedIconClass = iconClass && `icon ${normalizeIconClass(iconClass)}`;
337337
const vendor = provider ? `provided by ${provider}` : null;
338+
338339
return (
339340
<CatalogTile
340341
id={uid}
@@ -366,8 +367,8 @@ export const OperatorHubTileView = requireOperatorGroup(
366367
pageDescription={pageDescription}
367368
emptyStateInfo="No Operator Hub items are being shown due to the filters being applied."
368369
/>
369-
<Modal show={!!detailsItem} onHide={this.closeOverlay} bsSize={'lg'} className="co-catalog-page__overlay right-side-modal-pf">
370-
{detailsItem && <OperatorHubItemDetails item={detailsItem} closeOverlay={this.closeOverlay} />}
370+
<Modal show={!!detailsItem} onHide={this.closeOverlay} bsSize="lg" className="co-catalog-page__overlay right-side-modal-pf">
371+
{detailsItem && <OperatorHubItemDetails namespace={this.props.namespace} item={detailsItem} closeOverlay={this.closeOverlay} />}
371372
</Modal>
372373
</React.Fragment>;
373374
}
@@ -380,6 +381,7 @@ OperatorHubTileView.propTypes = {
380381
};
381382

382383
export type OperatorHubTileViewProps = {
384+
namespace?: string;
383385
items: any[];
384386
catalogSourceConfig: K8sResourceKind;
385387
};

0 commit comments

Comments
 (0)