Skip to content

Commit 599edb8

Browse files
committed
Create secret page
1 parent 4d46f3e commit 599edb8

File tree

7 files changed

+265
-4
lines changed

7 files changed

+265
-4
lines changed

frontend/public/components/app.jsx

Lines changed: 8 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, CopySecret } 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,13 @@ 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/cluster/secrets/new/:type" exact component={props => <CreateSecret {...props} kind="Secret" />} />
180+
<Route path="/k8s/ns/:ns/secrets/new/:type" exact component={props => <CreateSecret {...props} kind="Secret" />} />
181+
<Route path="/k8s/cluster/secrets/:name/edit" exact component={props => <EditSecret {...props} kind="Secret" />} />
182+
<Route path="/k8s/ns/:ns/secrets/:name/edit" exact component={props => <EditSecret {...props} kind="Secret" />} />
183+
<Route path="/k8s/cluster/secrets/:name/copy" exact component={props => <CopySecret {...props} kind="Secret" />} />
184+
<Route path="/k8s/ns/:ns/secrets/:name/copy" exact component={props => <CopySecret {...props} kind="Secret" />} />
185+
178186
<Route path="/k8s/cluster/rolebindings/new" exact component={props => <CreateRoleBinding {...props} kind="RoleBinding" />} />
179187
<Route path="/k8s/ns/:ns/rolebindings/new" exact component={props => <CreateRoleBinding {...props} kind="RoleBinding" />} />
180188
<Route path="/k8s/ns/:ns/rolebindings/:name/copy" exact component={props => <CopyRoleBinding {...props} kind="RoleBinding" />} />

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

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -246,7 +246,19 @@ export const ListPage = props => {
246246
href = namespaced ? `/k8s/ns/${namespace || 'default'}/${ref}/new` : `/k8s/cluster/${ref}/new`;
247247
} catch (unused) { /**/ }
248248
}
249-
const createProps = createHandler ? {onClick: createHandler} : {to: href};
249+
let createProps;
250+
if (_.isFunction(createHandler)) {
251+
createProps = {onClick: createHandler};
252+
} else if (_.isObject(createHandler)) {
253+
createProps = {
254+
items: createHandler,
255+
createLink(param) {
256+
return param === 'yaml' ? href : `${href}/${param}`;
257+
}
258+
};
259+
} else {
260+
createProps = {to: href};
261+
}
250262
const resources = [{ kind, name, namespaced, selector, fieldSelector, filters, limit }];
251263

252264
if (!namespaced && namespace) {

frontend/public/components/secret.jsx

Lines changed: 23 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,19 @@ data:
1616
username: YWRtaW4=
1717
password: MWYyZDFlMmU2N2Rm`);
1818

19-
const menuActions = Cog.factory.common;
19+
const menuActions = [
20+
Cog.factory.ModifyLabels,
21+
Cog.factory.ModifyAnnotations,
22+
(kind, obj) => ({
23+
label: `Duplicate ${kind.label}...`,
24+
href: `${resourceObjPath(obj, kind.kind)}/copy`,
25+
}),
26+
(kind, obj) => ({
27+
label: `Edit ${kind.label}...`,
28+
href: `${resourceObjPath(obj, kind.kind)}/edit`,
29+
}),
30+
Cog.factory.Delete,
31+
];
2032

2133
const SecretHeader = props => <ListHeader>
2234
<ColHead {...props} className="col-md-3 col-sm-4 col-xs-6" sortField="metadata.name">Name</ColHead>
@@ -75,7 +87,15 @@ const filters = [{
7587
],
7688
}];
7789

78-
const SecretsPage = props => <ListPage ListComponent={SecretsList} rowFilters={filters} canCreate={true} {...props} />;
90+
const createItems = {
91+
// source: 'Create Source Secret',
92+
// image: 'Create Image Pull Secret',
93+
// generic: 'Create Key/Value Secret',
94+
webhook: 'Create Webhook Secret',
95+
yaml: 'Create via YAML',
96+
};
97+
98+
const SecretsPage = props => <ListPage ListComponent={SecretsList} canCreate={true} rowFilters={filters} createButtonText="Create New" createHandler={createItems} {...props} />;
7999

80100
const SecretsDetailsPage = props => <DetailsPage
81101
{...props}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
.separator {
2+
border-top: solid 1px #eee;
3+
margin: 30px 0;
4+
}
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
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 } 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 { type, data } = secret;
15+
let determinedType = null;
16+
switch (type) {
17+
case 'kubernetes.io/dockerconfigjson':
18+
case 'kubernetes.io/dockercfg':
19+
determinedType = 'image';
20+
break;
21+
case 'kubernetes.io/basic-auth':
22+
case 'kubernetes.io/ssh-auth':
23+
determinedType = 'source';
24+
break;
25+
case 'Opaque':
26+
case 'kubernetes.io/tls':
27+
case 'kubernetes.io/service-account-token':
28+
if (_.includes(_.keys(data), 'WebhookSecretKey')) {
29+
determinedType = 'webhook';
30+
} else {
31+
determinedType = 'generic';
32+
}
33+
break;
34+
default:
35+
break;
36+
}
37+
return determinedType;
38+
};
39+
40+
const BaseEditSecret = connect(null, {setActiveNamespace: UIActions.setActiveNamespace})(
41+
class BaseEditSecret_ extends SafetyFirst {
42+
constructor (props) {
43+
super(props);
44+
const kind = 'Secret';
45+
const existingData = _.pick(props.obj, ['metadata.name', 'metadata.namespace', 'data']);
46+
existingData.kind = kind;
47+
const secret = _.defaultsDeep({}, props.fixed, existingData, {
48+
apiVersion: 'v1',
49+
data: {},
50+
kind: 'Secret',
51+
metadata: {
52+
name: '',
53+
},
54+
type: 'Opaque',
55+
});
56+
57+
this.state = {
58+
secretType: this.props.secretType || determineSecretTypeAbstraction(secret),
59+
secret: secret,
60+
inProgress: false,
61+
type: secret.type,
62+
stringData: _.mapValues(secret.data, (v) => window.atob(v)),
63+
};
64+
this.changeSecretName = this.changeSecretName.bind(this);
65+
this.save = this.save.bind(this);
66+
}
67+
dataCallback (secretsData) {
68+
let stringData = {...this.state.stringData};
69+
stringData = secretsData;
70+
this.setState({stringData});
71+
}
72+
changeSecretName (event) {
73+
let secret = {...this.state.secret};
74+
secret.metadata.name = event.target.value;
75+
this.setState({secret});
76+
}
77+
save (e) {
78+
e.preventDefault();
79+
const { kind, metadata } = this.state.secret;
80+
this.setState({ inProgress: true });
81+
const newSecretObject = _.assign(this.state.secret, {stringData: this.state.stringData});
82+
83+
const ko = kindObj(kind);
84+
(this.props.isCreate
85+
? k8sCreate(ko, newSecretObject)
86+
: k8sUpdate(ko, newSecretObject, metadata.namespace, newSecretObject.metadata.name)
87+
).then(
88+
() => {
89+
this.setState({inProgress: false});
90+
if (metadata.namespace) {
91+
this.props.setActiveNamespace(metadata.namespace);
92+
}
93+
history.push(formatNamespacedRouteForResource('secrets'));
94+
},
95+
err => this.setState({error: err.message, inProgress: false})
96+
);
97+
}
98+
render () {
99+
const title = `${this.props.titleVerb} ${_.upperFirst(this.state.secretType)} Secret`;
100+
const { saveButtonText } = this.props;
101+
let subform, explanation = null;
102+
switch (this.state.secretType) {
103+
case 'source':
104+
case 'image':
105+
case 'generic':
106+
case 'webhook':
107+
explanation = 'Webhook secret allow you to authenticate a webhook trigger.';
108+
subform = <WebHookSecretSubform callbackForMetadata={this.dataCallback.bind(this)} stringData={this.state.stringData} />;
109+
break;
110+
default:
111+
break;
112+
}
113+
114+
return <div className="co-m-pane__body">
115+
<Helmet>
116+
<title>{title}</title>
117+
</Helmet>
118+
<form className="co-m-pane__body-group" onSubmit={this.save}>
119+
<h1 className="co-m-pane__heading">{title}</h1>
120+
<p className="co-m-pane__explanation">{explanation}</p>
121+
122+
<fieldset disabled={!this.props.isCreate}>
123+
<div className="form-group">
124+
<label className="control-label">Secret Name</label>
125+
<div className="modal-body__field">
126+
<input className="form-control" type="text" onChange={this.changeSecretName} value={this.state.secret.metadata.name} required id="test--subject-name" />
127+
<p className="help-block text-muted">Unique name of the new secret.</p>
128+
</div>
129+
</div>
130+
</fieldset>
131+
{subform}
132+
<div className="separator"></div>
133+
<ButtonBar errorMessage={this.state.error} inProgress={this.state.inProgress}>
134+
<button type="submit" className="btn btn-primary" id="create-secret">{saveButtonText || 'Create Secret'}</button>
135+
<Link to={formatNamespacedRouteForResource('secrets')} className="btn btn-default">Cancel</Link>
136+
</ButtonBar>
137+
</form>
138+
</div>;
139+
}
140+
}
141+
);
142+
143+
const BindingLoadingWrapper = props => {
144+
const fixed = {};
145+
_.each(props.fixedKeys, k => fixed[k] = _.get(props.obj.data, k));
146+
return <StatusBox {...props.obj}>
147+
<BaseEditSecret {...props} obj={props.obj.data} fixed={fixed} />
148+
</StatusBox>;
149+
};
150+
151+
export const CreateSecret = ({match: {params}}) => <BaseEditSecret
152+
metadata={{
153+
namespace: getActiveNamespace(),
154+
}}
155+
fixed={{
156+
kind: 'Secret',
157+
metadata: {namespace: params.ns},
158+
}}
159+
secretType={params.type}
160+
titleVerb="Create"
161+
isCreate={true}
162+
/>;
163+
164+
export const EditSecret = ({match: {params}, kind}) => <Firehose resources={[{kind: kind, name: params.name, namespace: params.ns, isList: false, prop: 'obj'}]}>
165+
<BindingLoadingWrapper fixedKeys={['kind', 'metadata']} titleVerb="Edit" saveButtonText="Save Changes" />
166+
</Firehose>;
167+
168+
export const CopySecret = ({match: {params}, kind}) => <Firehose resources={[{kind: kind, name: params.name, namespace: params.ns, isList: false, prop: 'obj'}]}>
169+
<BindingLoadingWrapper isCreate={true} titleVerb="Duplicate" />
170+
</Firehose>;
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import * as React from 'react';
2+
3+
const generateSecret = () => {
4+
//http://stackoverflow.com/questions/105034/create-guid-uuid-in-javascript
5+
function s4() {
6+
return Math.floor((1 + Math.random()) * 0x10000)
7+
.toString(16)
8+
.substring(1);
9+
}
10+
return s4()+s4()+s4()+s4();
11+
};
12+
13+
export class WebHookSecretSubform extends React.PureComponent {
14+
constructor (props) {
15+
super(props);
16+
this.state = {
17+
WebHookSecretKey: this.props.stringData.WebHookSecretKey || '' ,
18+
};
19+
this.changeWebHookSecretkey = this.changeWebHookSecretkey.bind(this);
20+
this.generateWebHookSecret = this.generateWebHookSecret.bind(this);
21+
}
22+
changeWebHookSecretkey(event) {
23+
this.setState({
24+
WebHookSecretKey: event.target.value
25+
}, () => this.props.callbackForMetadata(this.state));
26+
}
27+
generateWebHookSecret() {
28+
this.setState({
29+
WebHookSecretKey: generateSecret()
30+
}, () => this.props.callbackForMetadata(this.state));
31+
}
32+
render () {
33+
return <React.Fragment>
34+
<div className="form-group">
35+
<label className="control-label" htmlFor="webhook-secret-key">Webhook Secret Key</label>
36+
<div className="input-group modal-body__field">
37+
<input className="form-control" id="webhook-secret-key" type="text" name="webhookSecretKey" onChange={this.changeWebHookSecretkey} value={this.state.WebHookSecretKey} required/>
38+
<span className="input-group-btn">
39+
<button type="button" onClick={this.generateWebHookSecret} className="btn btn-default">Generate</button>
40+
</span>
41+
</div>
42+
<p className="help-block text-muted">Value of the secret will be supplied when invoking the webhook. </p>
43+
</div>
44+
</React.Fragment>;
45+
}
46+
}

frontend/public/style.scss

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@
5656
@import "components/resource-dropdown";
5757
@import "components/row-filter";
5858
@import "components/route";
59+
@import "components/secret";
5960
@import "components/service";
6061
@import "components/sysevent-icon";
6162
@import "components/sysevent-stream";

0 commit comments

Comments
 (0)