Skip to content

Commit

Permalink
Operator Hub is now namespaced, need to test end-to-end
Browse files Browse the repository at this point in the history
  • Loading branch information
alecmerdler committed Feb 6, 2019
1 parent 6165533 commit d989382
Show file tree
Hide file tree
Showing 19 changed files with 396 additions and 120 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,64 @@ describe('requireOperatorGroup', () => {
expect(wrapper.find(NoOperatorGroupMsg).exists()).toBe(false);
});
});

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);
});
});
5 changes: 4 additions & 1 deletion frontend/public/components/app.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -175,8 +175,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 @@ -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 All @@ -345,7 +346,7 @@ export const OperatorHubTileView = requireOperatorGroup(
vendor={vendor}
description={description}
onClick={() => this.openOverlay(item)}
footer={installed ? <span><Icon type="pf" name="ok" /> Installed</span> : null}
footer={installed && !_.isEmpty(this.props.namespace) ? <span><Icon type="pf" name="ok" /> Installed</span> : null}
/>
);
}
Expand Down Expand Up @@ -380,6 +381,7 @@ OperatorHubTileView.propTypes = {
};

export type OperatorHubTileViewProps = {
namespace?: string;
items: any[];
catalogSourceConfig: K8sResourceKind;
};
Expand Down
79 changes: 30 additions & 49 deletions frontend/public/components/operator-hub/operator-hub-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@

import * as React from 'react';
import * as _ from 'lodash-es';
import {Helmet} from 'react-helmet';
import { Helmet } from 'react-helmet';
import { match } from 'react-router';

import { Firehose, PageHeading, StatusBox, MsgBox, ExternalLink } from '../utils';
import { referenceForModel, K8sResourceKind } from '../../module/k8s';
Expand Down Expand Up @@ -52,51 +53,27 @@ const normalizePackageManifests = (packageManifests: PackageManifestKind[] = [],
});
};

export class OperatorHubList extends React.Component<OperatorHubListProps, OperatorHubListState> {
state = {items: []};

componentDidMount() {
const {packageManifest, subscription, loaded} = this.props;

if (loaded) {
const items = _.sortBy(normalizePackageManifests(_.get(packageManifest, 'data'), subscription.data), 'name');
this.setState({items});
}
}

componentDidUpdate(prevProps: OperatorHubListProps) {
const {packageManifest, subscription, loaded} = this.props;

if (loaded && !prevProps.loaded ||
!_.isEqual(subscription.data, prevProps.subscription.data) ||
!_.isEqual(_.get(packageManifest, 'data'), _.get(prevProps.packageManifest, 'data'))) {
const items = _.sortBy(normalizePackageManifests(_.get(packageManifest, 'data'), subscription.data), 'name');
this.setState({items});
}
}

render() {
const {catalogSourceConfig, loaded, loadError} = this.props;
const {items} = this.state;
const sourceConfigs = _.find(_.get(catalogSourceConfig, 'data'), csc => _.startsWith(csc.metadata.name, OPERATOR_HUB_CSC_BASE));

return (
<StatusBox
data={items}
loaded={loaded}
loadError={loadError}
label="Resources"
EmptyMsg={() => (
<MsgBox
title="No Operator Hub Items Found"
detail={<span>Please check that the OperatorHub is running and that you have created a valid OperatorSource. For more information about Operator Hub, please click <ExternalLink href="https://github.com/operator-framework/operator-marketplace" text="here" />.</span>}
/>
)}>
<OperatorHubTileView items={items} catalogSourceConfig={sourceConfigs} />
</StatusBox>
);
}
}
export const OperatorHubList: React.SFC<OperatorHubListProps> = (props) => {
const {packageManifest, catalogSourceConfig, subscription, loaded, loadError, namespace} = props;
const sourceConfigs = _.find(_.get(catalogSourceConfig, 'data'), csc => _.startsWith(csc.metadata.name, OPERATOR_HUB_CSC_BASE));
const items = loaded
? _.sortBy(normalizePackageManifests(_.get(packageManifest, 'data'), subscription.data), 'name')
: [];

return <StatusBox
data={items}
loaded={loaded}
loadError={loadError}
label="Resources"
EmptyMsg={() => (
<MsgBox
title="No Operator Hub Items Found"
detail={<span>Please check that the OperatorHub is running and that you have created a valid OperatorSource. For more information about Operator Hub, please click <ExternalLink href="https://github.com/operator-framework/operator-marketplace" text="here" />.</span>}
/>
)}>
<OperatorHubTileView items={items} catalogSourceConfig={sourceConfigs} namespace={namespace} />
</StatusBox>;
};

export const OperatorHubPage: React.SFC<OperatorHubPageProps> = (props) => {
return <React.Fragment>
Expand All @@ -120,25 +97,28 @@ export const OperatorHubPage: React.SFC<OperatorHubPageProps> = (props) => {
kind: referenceForModel(PackageManifestModel),
namespace: 'openshift-marketplace',
prop: 'packageManifest',
selector: {matchLabels: {'openshift-marketplace': 'true'}},
// FIXME(alecmerdler): Commenting this out for dev
// selector: {matchLabels: {'openshift-marketplace': 'true'}},
}, {
isList: true,
kind: referenceForModel(SubscriptionModel),
namespace: props.match.params.ns,
prop: 'subscription',
}]}>
{/* FIXME(alecmerdler): Hack because `Firehose` injects props without TypeScript knowing about it */}
<OperatorHubList {...props as any} />
<OperatorHubList {...props as any} namespace={props.match.params.ns} />
</Firehose>
</div>
</div>
</React.Fragment>;
};

export type OperatorHubPageProps = {

match: match<{ns?: string}>;
};

export type OperatorHubListProps = {
namespace?: string;
catalogSourceConfig: {loaded: boolean, data?: K8sResourceKind[]};
operatorGroup: {loaded: boolean, data?: OperatorGroupKind[]};
packageManifest: {loaded: boolean, data?: PackageManifestKind[]};
Expand All @@ -151,4 +131,5 @@ export type OperatorHubListState = {
items: any[];
};

OperatorHubList.displayName = 'OperatorHubList';
OperatorHubPage.displayName = 'OperatorHubPage';
Loading

0 comments on commit d989382

Please sign in to comment.