Skip to content

Commit a6383dd

Browse files
committed
Create secret page
1 parent 4d46f3e commit a6383dd

File tree

9 files changed

+263
-13
lines changed

9 files changed

+263
-13
lines changed

frontend/integration-tests/tests/crud.scenario.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,12 @@ describe('Kubernetes resource CRUD operations', () => {
8585
}
8686

8787
it('displays a YAML editor for creating a new resource instance', async() => {
88-
await crudView.createYAMLButton.click();
88+
if (kind === 'Secret') {
89+
await crudView.createItemButton.click();
90+
await crudView.createYAMLLink.click();
91+
} else {
92+
await crudView.createYAMLButton.click();
93+
}
8994
await yamlView.isLoaded();
9095

9196
const content = await yamlView.editorContent.getText();

frontend/integration-tests/views/crud.view.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { $, $$, browser, ExpectedConditions as until } from 'protractor';
22

33
export const createYAMLButton = $('#yaml-create');
4+
export const createItemButton = $('#item-create');
5+
export const createYAMLLink = $('#yaml-link');
46

57
/**
68
* Returns a promise that resolves after the loading spinner is not present.

frontend/public/components/app.jsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import { Nav } from './nav';
2424
import { ProfilePage } from './profile';
2525
import { ResourceDetailsPage, ResourceListPage } from './resource-list';
2626
import { CopyRoleBinding, CreateRoleBinding, EditRoleBinding, EditRulePage } from './RBAC';
27+
import { CreateSecret, EditSecret } from './secrets/create-secret';
2728
import { StartGuidePage } from './start-guide';
2829
import { SearchPage } from './search';
2930
import { history, AsyncComponent, Loading } from './utils';
@@ -175,6 +176,10 @@ class App extends React.PureComponent {
175176
<Route path="/k8s/ns/:ns/roles/:name/:rule/edit" exact component={EditRulePage} />
176177
<Route path="/k8s/ns/:ns/roles" exact component={rolesListPage} />
177178

179+
<Route path="/k8s/ns/:ns/secrets/new/:type" exact component={props => <CreateSecret {...props} kind="Secret" />} />
180+
<Route path="/k8s/ns/:ns/secrets/:name/edit" exact component={props => <EditSecret {...props} kind="Secret" />} />
181+
<Route path="/k8s/ns/:ns/secrets/:name/edit-yaml" exact component={props => <EditYAMLPage {...props} kind="Secret" />} />
182+
178183
<Route path="/k8s/cluster/rolebindings/new" exact component={props => <CreateRoleBinding {...props} kind="RoleBinding" />} />
179184
<Route path="/k8s/ns/:ns/rolebindings/new" exact component={props => <CreateRoleBinding {...props} kind="RoleBinding" />} />
180185
<Route path="/k8s/ns/:ns/rolebindings/:name/copy" exact component={props => <CopyRoleBinding {...props} kind="RoleBinding" />} />

frontend/public/components/factory/list-page.jsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,7 @@ export const FireMan_ = connect(null, {filterList: k8sActions.filterList})(
162162
</Link>;
163163
} else if (createProps.items) {
164164
createLink = <div className="co-m-primary-action">
165-
<Dropdown noButton={true} className="btn btn-primary" id="yaml-create" title={createButtonText} items={createProps.items} onChange={(name) => history.push(createProps.createLink(name))} />
165+
<Dropdown noButton={true} className="btn btn-primary" id="item-create" title={createButtonText} items={createProps.items} onChange={(name) => history.push(createProps.createLink(name))} />
166166
</div>;
167167
} else {
168168
createLink = <div className="co-m-primary-action">
@@ -236,6 +236,7 @@ FireMan_.propTypes = {
236236
/** @type {React.SFC<{ListComponent: React.ComponentType<any>, kind: string, namespace?: string, filterLabel?: string, title?: string, showTitle?: boolean, dropdownFilters?: any[], rowFilters?: any[], selector?: string, fieldSelector?: string, canCreate?: boolean, createButtonText?: string, createProps?: any, fake?: boolean}>} */
237237
export const ListPage = props => {
238238
const {createButtonText, createHandler, filterLabel, kind, namespace, selector, name, fieldSelector, filters, limit, showTitle = true, fake} = props;
239+
let { createProps } = props;
239240
const ko = kindObj(kind);
240241
const {labelPlural, plural, namespaced, label} = ko;
241242
const title = props.title || labelPlural;
@@ -246,7 +247,8 @@ export const ListPage = props => {
246247
href = namespaced ? `/k8s/ns/${namespace || 'default'}/${ref}/new` : `/k8s/cluster/${ref}/new`;
247248
} catch (unused) { /**/ }
248249
}
249-
const createProps = createHandler ? {onClick: createHandler} : {to: href};
250+
251+
createProps = createProps || (createHandler ? {onClick: createHandler} : {to: href});
250252
const resources = [{ kind, name, namespaced, selector, fieldSelector, filters, limit }];
251253

252254
if (!namespaced && namespace) {

frontend/public/components/secret.jsx

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import * as React from 'react';
33

44
import { ColHead, DetailsPage, List, ListHeader, ListPage, ResourceRow } from './factory';
55
import { SecretData } from './configmap-and-secret-data';
6-
import { Cog, ResourceCog, ResourceLink, ResourceSummary, detailsPage, navFactory } from './utils';
6+
import { Cog, ResourceCog, ResourceLink, ResourceSummary, detailsPage, navFactory, resourceObjPath } from './utils';
77
import { fromNow } from './utils/datetime';
88
import { registerTemplate } from '../yaml-templates';
99

@@ -16,7 +16,22 @@ data:
1616
username: YWRtaW4=
1717
password: MWYyZDFlMmU2N2Rm`);
1818

19-
const menuActions = Cog.factory.common;
19+
const editInYaml = obj => {
20+
if (obj.type === 'Opaque' && _.has(obj.data, 'WebHookSecretKey') && Object.keys(obj.data).length === 1) {
21+
return false;
22+
}
23+
return true;
24+
};
25+
26+
const menuActions = [
27+
Cog.factory.ModifyLabels,
28+
Cog.factory.ModifyAnnotations,
29+
(kind, obj) => ({
30+
label: `Edit ${kind.label}...`,
31+
href: editInYaml(obj) ? `${resourceObjPath(obj, kind.kind)}/edit-yaml` : `${resourceObjPath(obj, kind.kind)}/edit`,
32+
}),
33+
Cog.factory.Delete,
34+
];
2035

2136
const SecretHeader = props => <ListHeader>
2237
<ColHead {...props} className="col-md-3 col-sm-4 col-xs-6" sortField="metadata.name">Name</ColHead>
@@ -75,7 +90,23 @@ const filters = [{
7590
],
7691
}];
7792

78-
const SecretsPage = props => <ListPage ListComponent={SecretsList} rowFilters={filters} canCreate={true} {...props} />;
93+
const SecretsPage = props => {
94+
const createItems = {
95+
// source: 'Create Source Secret',
96+
// image: 'Create Image Pull Secret',
97+
// generic: 'Create Key/Value Secret',
98+
webhook: 'Webhook Secret',
99+
yaml: 'Secret from YAML',
100+
};
101+
102+
const createProps = {
103+
items: createItems,
104+
createLink: (type) => `/k8s/ns/${props.namespace}/secrets/new/${type !== 'yaml' ? type : ''}`
105+
};
106+
107+
return <ListPage ListComponent={SecretsList} canCreate={true} rowFilters={filters} createButtonText="Create" createProps={createProps} {...props} />;
108+
};
109+
79110

80111
const SecretsDetailsPage = props => <DetailsPage
81112
{...props}
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
import * as _ from 'lodash-es';
2+
import * as React from 'react';
3+
import { Helmet } from 'react-helmet';
4+
import { connect } from 'react-redux';
5+
import { Link } from 'react-router-dom';
6+
7+
import { k8sCreate, k8sUpdate, K8sResourceKind } from '../../module/k8s';
8+
import { ButtonBar, Firehose, history, kindObj, StatusBox } from '../utils';
9+
import { getActiveNamespace, formatNamespacedRouteForResource, UIActions } from '../../ui/ui-actions';
10+
import { SafetyFirst } from '../safety-first';
11+
import { WebHookSecretSubform } from './subforms';
12+
13+
const determineSecretTypeAbstraction = (secret) => {
14+
const { data } = secret;
15+
if (_.has(data, 'WebHookSecretKey')) {
16+
return 'webhook';
17+
}
18+
return 'generic';
19+
};
20+
21+
class BaseEditSecret_ extends SafetyFirst<BaseEditSecretProps_, BaseEditSecretState_> {
22+
constructor (props) {
23+
super(props);
24+
let existingData = _.pick(props.obj, ['metadata', 'data']);
25+
const secret = _.defaultsDeep({}, props.fixed, existingData, {
26+
apiVersion: 'v1',
27+
data: {},
28+
kind: 'Secret',
29+
metadata: {
30+
name: '',
31+
},
32+
type: 'Opaque',
33+
});
34+
35+
this.state = {
36+
secretType: this.props.secretType || determineSecretTypeAbstraction(secret),
37+
secret: secret,
38+
inProgress: false,
39+
type: secret.type,
40+
stringData: _.mapValues(secret.data, (v) => window.atob(v)),
41+
};
42+
this.onNameChanged = this.onNameChanged.bind(this);
43+
this.save = this.save.bind(this);
44+
}
45+
onDataChanged (secretsData) {
46+
let stringData = {...this.state.stringData};
47+
stringData = secretsData;
48+
this.setState({stringData});
49+
}
50+
onNameChanged (event) {
51+
let secret = {...this.state.secret};
52+
secret.metadata.name = event.target.value;
53+
this.setState({secret});
54+
}
55+
save (e) {
56+
e.preventDefault();
57+
const { kind, metadata } = this.state.secret;
58+
this.setState({ inProgress: true });
59+
const newSecret = _.assign({}, this.state.secret, {stringData: this.state.stringData});
60+
const ko = kindObj(kind);
61+
(this.props.isCreate
62+
? k8sCreate(ko, newSecret)
63+
: k8sUpdate(ko, newSecret, metadata.namespace, newSecret.metadata.name)
64+
).then(
65+
() => {
66+
this.setState({inProgress: false});
67+
if (metadata.namespace) {
68+
this.props.setActiveNamespace(metadata.namespace);
69+
}
70+
history.push(formatNamespacedRouteForResource('secrets'));
71+
},
72+
err => this.setState({error: err.message, inProgress: false})
73+
);
74+
}
75+
render () {
76+
const title = `${this.props.titleVerb} ${_.upperFirst(this.state.secretType)} Secret`;
77+
const { saveButtonText } = this.props;
78+
79+
let explanation = 'Webhook secret allow you to authenticate a webhook trigger.';
80+
let subform = <WebHookSecretSubform onChange={this.onDataChanged.bind(this)} stringData={this.state.stringData} />;
81+
82+
return <div className="co-m-pane__body">
83+
<Helmet>
84+
<title>{title}</title>
85+
</Helmet>
86+
<form className="co-m-pane__body-group" onSubmit={this.save}>
87+
<h1 className="co-m-pane__heading">{title}</h1>
88+
<p className="co-m-pane__explanation">{explanation}</p>
89+
90+
<fieldset disabled={!this.props.isCreate}>
91+
<div className="form-group">
92+
<label className="control-label">Secret Name</label>
93+
<div>
94+
<input className="form-control" type="text" onChange={this.onNameChanged} value={this.state.secret.metadata.name} required id="test--subject-name" />
95+
<p className="help-block">Unique name of the new secret.</p>
96+
</div>
97+
</div>
98+
</fieldset>
99+
{subform}
100+
<ButtonBar errorMessage={this.state.error} inProgress={this.state.inProgress} >
101+
<button type="submit" className="btn btn-primary" id="create-secret">{saveButtonText || 'Create'}</button>
102+
<Link to={formatNamespacedRouteForResource('secrets')} className="btn btn-default">Cancel</Link>
103+
</ButtonBar>
104+
</form>
105+
</div>;
106+
}
107+
}
108+
109+
const BaseEditSecret = connect(null, {setActiveNamespace: UIActions.setActiveNamespace})(
110+
(props: BaseEditSecretProps_) => <BaseEditSecret_ {...props} />
111+
);
112+
113+
const BindingLoadingWrapper = props => {
114+
const fixed = _.reduce(props.fixedKeys, (acc, k) => ({...acc, k: _.get(props.obj.data, k)}), {});
115+
return <StatusBox {...props.obj}>
116+
<BaseEditSecret {...props} obj={props.obj.data} fixed={fixed} />
117+
</StatusBox>;
118+
};
119+
120+
export const CreateSecret = ({match: {params}}) => {
121+
return <BaseEditSecret
122+
fixed={{ metadata: {namespace: params.ns} }}
123+
metadata={{ namespace: getActiveNamespace() }}
124+
secretType={params.type}
125+
titleVerb="Create"
126+
isCreate={true}
127+
/>;
128+
};
129+
130+
export const EditSecret = ({match: {params}, kind}) => <Firehose resources={[{kind: kind, name: params.name, namespace: params.ns, isList: false, prop: 'obj'}]}>
131+
<BindingLoadingWrapper fixedKeys={['kind', 'metadata']} titleVerb="Edit" saveButtonText="Save Changes" />
132+
</Firehose>;
133+
134+
/* eslint-disable no-undef */
135+
export type BaseEditSecretState_ = {
136+
secretType?: string,
137+
secret: K8sResourceKind,
138+
inProgress: boolean,
139+
type: string,
140+
stringData: {[key: string]: string},
141+
error?: any,
142+
};
143+
144+
export type BaseEditSecretProps_ = {
145+
obj?: any,
146+
fixed: any,
147+
kind?: string,
148+
isCreate: boolean,
149+
titleVerb: string,
150+
setActiveNamespace: any,
151+
secretType?: string,
152+
saveButtonText?: string,
153+
metadata: any,
154+
};
155+
/* eslint-enable no-undef */
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import * as React from 'react';
2+
3+
const generateSecret = () => {
4+
// http://stackoverflow.com/questions/105034/create-guid-uuid-in-javascript
5+
const s4 = () => Math.floor((1 + Math.random()) * 0x10000).toString(16).substring(1);
6+
return s4()+s4()+s4()+s4();
7+
};
8+
9+
export class WebHookSecretSubform extends React.Component<WebHookSecretSubformProps, WebHookSecretSubformState> {
10+
constructor(props) {
11+
super(props);
12+
this.state = {WebHookSecretKey: this.props.stringData.WebHookSecretKey || ''};
13+
this.changeWebHookSecretkey = this.changeWebHookSecretkey.bind(this);
14+
this.generateWebHookSecret = this.generateWebHookSecret.bind(this);
15+
}
16+
changeWebHookSecretkey(event) {
17+
this.setState({
18+
WebHookSecretKey: event.target.value
19+
}, () => this.props.onChange(this.state));
20+
}
21+
generateWebHookSecret() {
22+
this.setState({
23+
WebHookSecretKey: generateSecret()
24+
}, () => this.props.onChange(this.state));
25+
}
26+
render () {
27+
return <div className="form-group">
28+
<label className="control-label" htmlFor="webhook-secret-key">Webhook Secret Key</label>
29+
<div className="input-group">
30+
<input className="form-control" id="webhook-secret-key" type="text" name="webhookSecretKey" onChange={this.changeWebHookSecretkey} value={this.state.WebHookSecretKey} required/>
31+
<span className="input-group-btn">
32+
<button type="button" onClick={this.generateWebHookSecret} className="btn btn-default">Generate</button>
33+
</span>
34+
</div>
35+
<p className="help-block">Value of the secret will be supplied when invoking the webhook. </p>
36+
</div>;
37+
}
38+
}
39+
40+
/* eslint-disable no-undef */
41+
export type WebHookSecretSubformState = {
42+
WebHookSecretKey: string;
43+
};
44+
45+
export type WebHookSecretSubformProps = {
46+
onChange: Function;
47+
stringData: {[WebHookSecretKey: string]: string};
48+
};
49+
/* eslint-enable no-undef */

frontend/public/components/utils/button-bar.jsx

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,12 @@ const InfoMessage = ({message}) => <div className="alert alert-info"><span class
2121
// NOTE: DO NOT use <a> elements within a ButtonBar.
2222
// They don't support the disabled attribute, and therefore
2323
// can't be disabled during a pending promise/request.
24-
export const ButtonBar = ({children, className, errorMessage, infoMessage, inProgress}) => {
25-
return <div className={classNames(className, 'co-m-btn-bar')}>
26-
{errorMessage && <ErrorMessage message={errorMessage} />}
27-
{injectDisabled(children, inProgress)}
28-
{inProgress && <LoadingInline />}
29-
{infoMessage && <InfoMessage message={infoMessage} />}
24+
export const ButtonBar = (props) => {
25+
return <div className={classNames(props.className, 'co-m-btn-bar')}>
26+
{props.errorMessage && <ErrorMessage message={props.errorMessage} />}
27+
{injectDisabled(props.children, props.inProgress)}
28+
{props.inProgress && <LoadingInline />}
29+
{props.infoMessage && <InfoMessage message={props.infoMessage} />}
3030
</div>;
3131
};
3232

@@ -35,4 +35,5 @@ ButtonBar.propTypes = {
3535
errorMessage: PropTypes.string,
3636
infoMessage: PropTypes.string,
3737
inProgress: PropTypes.bool.isRequired,
38+
className: PropTypes.string,
3839
};

frontend/public/components/utils/dropdown.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ class DropDownRow extends React.PureComponent {
103103
}
104104
return <li className={className} key={itemKey}>
105105
{prefix}
106-
<a ref={ref => this.ref=ref} className={classNames({'next-to-bookmark': !!prefix, focus: selected, hover})} onClick={e => onclick(itemKey, e)}>{content}</a>
106+
<a ref={ref => this.ref=ref} id={`${itemKey}-link`} className={classNames({'next-to-bookmark': !!prefix, focus: selected, hover})} onClick={e => onclick(itemKey, e)}>{content}</a>
107107
{suffix}
108108
</li>;
109109
}

0 commit comments

Comments
 (0)