Skip to content

Commit

Permalink
improve workflow for installing single-namespace Operators from Marke…
Browse files Browse the repository at this point in the history
…tplace
  • Loading branch information
alecmerdler committed Feb 8, 2019
1 parent 98c97ae commit e4feb72
Show file tree
Hide file tree
Showing 21 changed files with 519 additions and 197 deletions.
3 changes: 3 additions & 0 deletions frontend/__mocks__/k8sResourcesMocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ export const testClusterServiceVersion: ClusterServiceVersionKind = {
'alm-owner-testapp': 'testapp.clusterserviceversions.operators.coreos.com.v1alpha1',
},
},
installModes: [],
install: {
strategy: 'Deployment',
spec: {
Expand Down Expand Up @@ -129,6 +130,7 @@ export const localClusterServiceVersion: ClusterServiceVersionKind = {
'alm-owner-local-testapp': 'local-testapp.clusterserviceversions.operators.coreos.com.v1alpha1',
},
},
installModes: [],
install: {
strategy: 'Deployment',
spec: {
Expand Down Expand Up @@ -268,6 +270,7 @@ export const testPackageManifest: PackageManifestKind = {
provider: {
name: 'CoreOS, Inc',
},
installModes: [],
},
}],
defaultChannel: 'alpha',
Expand Down
6 changes: 6 additions & 0 deletions frontend/__mocks__/operatorHubItemsMocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ const amqPackageManifest = {
provider: {
name: 'Red Hat',
},
installModes: [],
annotations: {
'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":"*"}]}}}]',
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```',
Expand Down Expand Up @@ -89,6 +90,7 @@ const etcdPackageManifest = {
provider: {
name: 'CoreOS, Inc',
},
installModes: [],
annotations: {
'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>"}}}]',
'tectonic-visibility': 'ocs',
Expand Down Expand Up @@ -136,6 +138,7 @@ const federationv2PackageManifest = {
provider: {
name: 'Red Hat',
},
installModes: [],
annotations: {
description: 'Kubernetes Federation V2 namespace-scoped installation',
categories: '',
Expand Down Expand Up @@ -184,6 +187,7 @@ const prometheusPackageManifest = {
provider: {
name: 'Red Hat',
},
installModes: [],
annotations: {
'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": {}}}]',
description: 'The Prometheus Operator for Kubernetes provides easy monitoring definitions for Kubernetes services and deployment and management of Prometheus instances.',
Expand Down Expand Up @@ -230,6 +234,7 @@ const svcatPackageManifest = {
provider: {
name: 'Red Hat',
},
installModes: [],
annotations: {
description: 'Service Catalog lets you provision cloud services directly from the comfort of native Kubernetes tooling.',
categories: 'catalog',
Expand Down Expand Up @@ -276,6 +281,7 @@ const dummyPackageManifest = {
provider: {
name: 'Dummy',
},
installModes: [],
annotations: {
description: 'Dummy is not a real operator',
categories: 'dummy',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ describe(SubscriptionChannelModal.name, () => {
provider: {
name: 'CoreOS, Inc',
},
installModes: [],
},
}, {
name: 'nightly',
Expand All @@ -48,6 +49,7 @@ describe(SubscriptionChannelModal.name, () => {
provider: {
name: 'CoreOS, Inc',
},
installModes: [],
},
}];

Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
/* eslint-disable no-undef, no-unused-vars */

import * as React from 'react';
import * as _ from 'lodash-es';
import { shallow } from 'enzyme';

import { requireOperatorGroup, NoOperatorGroupMsg } from '../../../public/components/operator-lifecycle-manager/operator-group';
import { requireOperatorGroup, NoOperatorGroupMsg, supports, InstallModeSet, InstallModeType } from '../../../public/components/operator-lifecycle-manager/operator-group';
import { OperatorGroupKind } from '../../../public/components/operator-lifecycle-manager';
import { testOperatorGroup } from '../../../__mocks__/k8sResourcesMocks';

describe('requireOperatorGroup', () => {
Expand Down Expand Up @@ -33,3 +35,88 @@ describe('requireOperatorGroup', () => {
expect(wrapper.find(NoOperatorGroupMsg).exists()).toBe(false);
});
});

describe('installedFor', () => {
const pkgName = 'etcd';

it('returns false if no `Subscriptions` exist for the given package', () => {

});

it('returns false if no `OperatorGroups` target the given namespace', () => {

});

it('returns false if given `all-namespaces`', () => {

});

it('returns true if `Subscription` exists in the "global" `OperatorGroup`', () => {

});

it('returns true if `Subscription` exists in an `OperatorGroup` that targets given namespace', () => {

});
});

describe('supports', () => {
let set: InstallModeSet;
let ownNamespaceGroup: OperatorGroupKind;
let singleNamespaceGroup: OperatorGroupKind;
let multiNamespaceGroup: OperatorGroupKind;
let allNamespacesGroup: OperatorGroupKind;

beforeEach(() => {
ownNamespaceGroup = _.cloneDeep(testOperatorGroup);
ownNamespaceGroup.status = {namespaces: [ownNamespaceGroup.metadata.namespace], lastUpdated: null};
singleNamespaceGroup = _.cloneDeep(testOperatorGroup);
singleNamespaceGroup.status = {namespaces: ['test-ns'], lastUpdated: null};
multiNamespaceGroup = _.cloneDeep(testOperatorGroup);
multiNamespaceGroup.status = {namespaces: ['test-ns', 'default'], lastUpdated: null};
allNamespacesGroup = _.cloneDeep(testOperatorGroup);
allNamespacesGroup.status = {namespaces: [''], lastUpdated: null};
});

it('correctly returns for an Operator that can only run in its own namespace', () => {
set = [
{type: InstallModeType.InstallModeTypeOwnNamespace, supported: true},
{type: InstallModeType.InstallModeTypeSingleNamespace, supported: true},
{type: InstallModeType.InstallModeTypeMultiNamespace, supported: false},
{type: InstallModeType.InstallModeTypeAllNamespaces, supported: false},
];

expect(supports(set)(ownNamespaceGroup)).toBe(true);
expect(supports(set)(singleNamespaceGroup)).toBe(true);
expect(supports(set)(multiNamespaceGroup)).toBe(false);
expect(supports(set)(allNamespacesGroup)).toBe(false);
});

it('correctly returns for an Operator which can run in several namespaces', () => {
set = [
{type: InstallModeType.InstallModeTypeOwnNamespace, supported: true},
{type: InstallModeType.InstallModeTypeSingleNamespace, supported: true},
{type: InstallModeType.InstallModeTypeMultiNamespace, supported: true},
{type: InstallModeType.InstallModeTypeAllNamespaces, supported: false},
];

expect(supports(set)(ownNamespaceGroup)).toBe(true);
expect(supports(set)(singleNamespaceGroup)).toBe(true);
expect(supports(set)(multiNamespaceGroup)).toBe(true);
expect(supports(set)(allNamespacesGroup)).toBe(false);
});

it('correctly returns for an Operator which can only run in all namespaces', () => {
set = [
{type: InstallModeType.InstallModeTypeOwnNamespace, supported: true},
{type: InstallModeType.InstallModeTypeSingleNamespace, supported: false},
{type: InstallModeType.InstallModeTypeMultiNamespace, supported: false},
{type: InstallModeType.InstallModeTypeAllNamespaces, supported: true},
];

expect(supports(set)(ownNamespaceGroup)).toBe(false);
expect(supports(set)(singleNamespaceGroup)).toBe(false);
expect(supports(set)(multiNamespaceGroup)).toBe(false);
expect(supports(set)(allNamespacesGroup)).toBe(true);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ describe(PackageManifestRow.displayName, () => {
let wrapper: ShallowWrapper<PackageManifestRowProps>;

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

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

it('renders button to create new subscription', () => {
wrapper = wrapper.setProps({subscription: null});

it('renders button to create new subscription if `canSubscribe` is true', () => {
expect(wrapper.find('.co-resource-list__item').childAt(2).find('button').text()).toEqual('Create Subscription');
});

it('does not render button to create new subscription if `canSubscribe` is false', () => {
wrapper = wrapper.setProps({canSubscribe: false});

expect(wrapper.find('.co-resource-list__item').childAt(2).find('button').exists()).toBe(false);
});
});

describe(PackageManifestList.displayName, () => {
Expand All @@ -74,7 +78,7 @@ describe(PackageManifestList.displayName, () => {
otherPackageManifest.status.catalogSource = 'another-catalog-source';
otherPackageManifest.status.catalogSourceDisplayName = 'Another Catalog Source';
otherPackageManifest.status.catalogSourcePublisher = 'Some Publisher';
packages = [testPackageManifest, otherPackageManifest];
packages = [otherPackageManifest, testPackageManifest];

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

Expand Down
5 changes: 4 additions & 1 deletion frontend/public/components/app.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -193,8 +193,11 @@ class App extends React.PureComponent {
<LazyRoute path="/cluster-health" exact loader={() => import('./cluster-health' /* webpackChunkName: "cluster-health" */).then(m => m.ClusterHealth)} />
<LazyRoute path="/start-guide" exact loader={() => import('./start-guide' /* webpackChunkName: "start-guide" */).then(m => m.StartGuidePage)} />

<LazyRoute path="/operatorhub" exact loader={() => import('./operator-hub/operator-hub-page' /* webpackChunkName: "operator-hub" */).then(m => m.OperatorHubPage)} />
<LazyRoute path="/operatorhub/all-namespaces" exact loader={() => import('./operator-hub/operator-hub-page' /* webpackChunkName: "operator-hub" */).then(m => m.OperatorHubPage)} />
<LazyRoute path="/operatorhub/ns/:ns" exact loader={() => import('./operator-hub/operator-hub-page' /* webpackChunkName: "operator-hub" */).then(m => m.OperatorHubPage)} />
<Route path="/operatorhub" exact component={NamespaceRedirect} />
<LazyRoute path="/operatorhub/subscribe" exact loader={() => import('./operator-hub/operator-hub-subscribe' /* webpackChunkName: "operator-hub-subscribe" */).then(m => m.OperatorHubSubscribePage)} />

<LazyRoute path="/catalog/all-namespaces" exact loader={() => import('./catalog/catalog-page' /* webpackChunkName: "catalog" */).then(m => m.CatalogPage)} />
<LazyRoute path="/catalog/ns/:ns" exact loader={() => import('./catalog/catalog-page' /* webpackChunkName: "catalog" */).then(m => m.CatalogPage)} />
<Route path="/catalog" exact component={NamespaceRedirect} />
Expand Down
4 changes: 3 additions & 1 deletion frontend/public/components/nav.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,9 @@ import {
MachineSetModel,
PackageManifestModel,
SubscriptionModel,
OperatorGroupModel,
} from '../models';
import { referenceForModel } from '../module/k8s';

import { history, stripBasePath } from './utils';

export const matchesPath = (resourcePath, prefix) => resourcePath === prefix || _.startsWith(resourcePath, `${prefix}/`);
Expand Down Expand Up @@ -277,6 +277,8 @@ const operatorManagementStartsWith = [
InstallPlanModel.path,
referenceForModel(CatalogSourceModel),
CatalogSourceModel.path,
referenceForModel(OperatorGroupModel),
OperatorGroupModel.path,
];
const provisionedServicesStartsWith = ['serviceinstances', 'servicebindings'];
const brokerManagementStartsWith = ['clusterservicebrokers', 'clusterserviceclasses'];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { MarkdownView } from '../operator-lifecycle-manager/clusterserviceversio
import { history, ExternalLink } from '../utils';
import { RH_OPERATOR_SUPPORT_POLICY_LINK } from '../../const';

export const OperatorHubItemDetails: React.SFC<OperatorHubItemDetailsProps> = ({item, closeOverlay}) => {
export const OperatorHubItemDetails: React.SFC<OperatorHubItemDetailsProps> = ({item, closeOverlay, namespace}) => {
if (!item) {
return null;
}
Expand Down Expand Up @@ -70,10 +70,8 @@ export const OperatorHubItemDetails: React.SFC<OperatorHubItemDetailsProps> = ({

const onActionClick = () => {
if (!installed) {
history.push(`/operatorhub/subscribe?pkg=${item.obj.metadata.name}&catalog=${catalogSource}&catalogNamespace=${catalogSourceNamespace}`);
return;
return history.push(`/operatorhub/subscribe?pkg=${item.obj.metadata.name}&catalog=${catalogSource}&catalogNamespace=${catalogSourceNamespace}&targetNamespace=${namespace}`);
}

// TODO: Allow for Manage button to navigate to the CSV details for the item
};

Expand Down Expand Up @@ -126,6 +124,7 @@ OperatorHubItemDetails.defaultProps = {
};

export type OperatorHubItemDetailsProps = {
namespace?: string;
item: any;
closeOverlay: () => void;
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -335,6 +335,7 @@ export const OperatorHubTileView = requireOperatorGroup(
const { uid, name, imgUrl, iconClass, provider, description, installed } = item;
const normalizedIconClass = iconClass && `icon ${normalizeIconClass(iconClass)}`;
const vendor = provider ? `provided by ${provider}` : null;

return (
<CatalogTile
id={uid}
Expand Down Expand Up @@ -366,8 +367,8 @@ export const OperatorHubTileView = requireOperatorGroup(
pageDescription={pageDescription}
emptyStateInfo="No Operator Hub items are being shown due to the filters being applied."
/>
<Modal show={!!detailsItem} onHide={this.closeOverlay} bsSize={'lg'} className="co-catalog-page__overlay right-side-modal-pf">
{detailsItem && <OperatorHubItemDetails item={detailsItem} closeOverlay={this.closeOverlay} />}
<Modal show={!!detailsItem} onHide={this.closeOverlay} bsSize="lg" className="co-catalog-page__overlay right-side-modal-pf">
{detailsItem && <OperatorHubItemDetails namespace={this.props.namespace} item={detailsItem} closeOverlay={this.closeOverlay} />}
</Modal>
</React.Fragment>;
}
Expand All @@ -380,6 +381,7 @@ OperatorHubTileView.propTypes = {
};

export type OperatorHubTileViewProps = {
namespace?: string;
items: any[];
catalogSourceConfig: K8sResourceKind;
};
Expand Down
Loading

0 comments on commit e4feb72

Please sign in to comment.