Skip to content

Commit fd6cb24

Browse files
Add the ability to add a secret to an application
1 parent 73c7420 commit fd6cb24

File tree

9 files changed

+630
-92
lines changed

9 files changed

+630
-92
lines changed

app/index.html

+1
Original file line numberDiff line numberDiff line change
@@ -328,6 +328,7 @@ <h1>JavaScript Required</h1>
328328
<script src="scripts/directives/labels.js"></script>
329329
<script src="scripts/directives/lifecycleHook.js"></script>
330330
<script src="scripts/directives/actionChip.js"></script>
331+
<script src="scripts/directives/addSecretToApplication.js"></script>
331332
<script src="scripts/directives/templateopt.js"></script>
332333
<script src="scripts/directives/tasks.js"></script>
333334
<script src="scripts/directives/catalog.js"></script>

app/scripts/controllers/secret.js

+10
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,16 @@ angular.module('openshiftConsole')
2727
}
2828
];
2929

30+
$scope.addToApplicationVisible = false;
31+
32+
$scope.addToApplication = function() {
33+
$scope.addToApplicationVisible = true;
34+
};
35+
36+
$scope.closeAddToApplication = function() {
37+
$scope.addToApplicationVisible = false;
38+
};
39+
3040
ProjectsService
3141
.get($routeParams.project)
3242
.then(_.spread(function(project, context) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
"use strict";
2+
(function() {
3+
angular.module("openshiftConsole").component('addSecretToApplication', {
4+
controller: [
5+
'$filter',
6+
'$scope',
7+
'APIService',
8+
'DataService',
9+
'NotificationsService',
10+
'StorageService',
11+
AddSecretToApplication
12+
],
13+
controllerAs: 'ctrl',
14+
bindings: {
15+
project: '<',
16+
secret: '<',
17+
onComplete: '<',
18+
onCancel: '<'
19+
},
20+
templateUrl: 'views/directives/add-secret-to-application.html'
21+
});
22+
23+
function AddSecretToApplication($filter, $scope, APIService, DataService, NotificationsService, StorageService) {
24+
var ctrl = this;
25+
var deploymentConfigs;
26+
var deployments;
27+
var replicationControllers;
28+
var replicaSets;
29+
var statefulSets;
30+
31+
var sortApplications = function() {
32+
// Don't waste time sorting on each data load, just sort when we have them all
33+
if (deploymentConfigs && deployments && replicationControllers && replicaSets && statefulSets) {
34+
var apiObjects = deploymentConfigs.concat(deployments)
35+
.concat(replicationControllers)
36+
.concat(replicaSets)
37+
.concat(statefulSets);
38+
ctrl.applications = _.sortBy(apiObjects, ['metadata.name', 'kind']);
39+
ctrl.updating = false;
40+
}
41+
};
42+
43+
var getApplications = function() {
44+
var hasDeploymentFilter = $filter('hasDeployment');
45+
var hasDeploymentConfigFilter = $filter('hasDeploymentConfig');
46+
47+
ctrl.updating = true;
48+
var context = {
49+
namespace: ctrl.project.metadata.name
50+
};
51+
// Load all the "application" types
52+
DataService.list('deploymentconfigs', context).then(function(deploymentConfigData) {
53+
deploymentConfigs = _.toArray(deploymentConfigData.by('metadata.name'));
54+
sortApplications();
55+
});
56+
DataService.list('replicationcontrollers', context).then(function(replicationControllerData) {
57+
replicationControllers = _.reject(replicationControllerData.by('metadata.name'), hasDeploymentConfigFilter);
58+
sortApplications();
59+
});
60+
DataService.list({
61+
group: 'apps',
62+
resource: 'deployments'
63+
}, context).then(function(deploymentData) {
64+
deployments = _.toArray(deploymentData.by('metadata.name'));
65+
sortApplications();
66+
});
67+
DataService.list({
68+
group: 'extensions',
69+
resource: 'replicasets'
70+
}, context).then(function(replicaSetData) {
71+
replicaSets = _.reject(replicaSetData.by('metadata.name'), hasDeploymentFilter);
72+
sortApplications();
73+
});
74+
DataService.list({
75+
group: 'apps',
76+
resource: 'statefulsets'
77+
}, context).then(function(statefulSetData) {
78+
statefulSets = _.toArray(statefulSetData.by('metadata.name'));
79+
sortApplications();
80+
});
81+
};
82+
83+
ctrl.$onInit = function() {
84+
ctrl.addType = 'env';
85+
ctrl.disableInputs = false;
86+
getApplications();
87+
};
88+
89+
ctrl.$postLink = function() {
90+
$scope.$watch(function() {
91+
return ctrl.application;
92+
}, function() {
93+
// Look at the existing mount paths so that we can warn if the new value is not unique.
94+
var podTemplate = _.get(ctrl.application, 'spec.template');
95+
ctrl.existingMountPaths = StorageService.getMountPaths(podTemplate);
96+
});
97+
};
98+
99+
ctrl.addToApplication = function() {
100+
var podTemplate = _.get(ctrl.application, 'spec.template');
101+
102+
ctrl.disableInputs = true;
103+
104+
if (ctrl.addType === 'env') {
105+
var newEnvFrom = {
106+
secretRef: ctrl.secret.metadata
107+
};
108+
109+
// For each container, add the new volume mount.
110+
_.each(podTemplate.spec.containers, function(container) {
111+
container.envFrom = container.envFrom || [];
112+
container.envFrom.push(newEnvFrom);
113+
});
114+
} else {
115+
var generateName = $filter('generateName');
116+
var name = generateName('volume-');
117+
var newVolumeMount = {
118+
name: name,
119+
mountPath: ctrl.mountVolume,
120+
readOnly: true
121+
};
122+
123+
// For each selected container, add the new volume mount.
124+
_.each(podTemplate.spec.containers, function(container) {
125+
container.volumeMounts = container.volumeMounts || [];
126+
container.volumeMounts.push(newVolumeMount);
127+
});
128+
129+
var newVolume = {
130+
name: name,
131+
secret: {
132+
secretName: ctrl.secret.metadata.name,
133+
}
134+
};
135+
136+
podTemplate.spec.volumes = podTemplate.spec.volumes || [];
137+
podTemplate.spec.volumes.push(newVolume);
138+
}
139+
140+
var humanizeKind = $filter('humanizeKind');
141+
var sourceKind = humanizeKind(ctrl.secret.kind);
142+
var targetKind = humanizeKind(ctrl.application.kind);
143+
var context = {
144+
namespace: ctrl.project.metadata.name
145+
};
146+
147+
DataService.update(APIService.kindToResource(ctrl.application.kind), ctrl.application.metadata.name, ctrl.application, context).then(
148+
function() {
149+
NotificationsService.addNotification({
150+
type: "success",
151+
message: "Successfully added " + sourceKind + " " + ctrl.secret.metadata.name + " to " + targetKind + " " + ctrl.application.metadata.name + "."
152+
});
153+
if (angular.isFunction(ctrl.onComplete)) {
154+
ctrl.onComplete();
155+
}
156+
},
157+
function(result) {
158+
var getErrorDetails = $filter('getErrorDetails');
159+
160+
NotificationsService.addNotification({
161+
type: "error",
162+
message: "An error occurred adding " + sourceKind + " " + ctrl.secret.metadata.name + " to " + targetKind + " " + ctrl.application.metadata.name + ". " +
163+
getErrorDetails(result)
164+
});
165+
}).finally(function() {
166+
ctrl.disableInputs = false;
167+
}
168+
);
169+
};
170+
}
171+
})();

app/styles/_secrets.less

+56-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,58 @@
1+
.add-secret-to-application {
2+
.catalogs-overlay-panel {
3+
max-width: 600px;
4+
}
5+
6+
.dialog-title {
7+
border-bottom: 1px solid @color-pf-black-300;
8+
9+
h3 {
10+
margin: 18px 0;
11+
padding-left: 15px;
12+
}
13+
}
14+
15+
.dialog-body {
16+
padding: 20px;
17+
18+
.add-choice {
19+
margin-bottom: 10px;
20+
}
21+
.button-group {
22+
.btn {
23+
margin-left: 10px;
24+
&:first-of-type {
25+
margin-left: 0;
26+
}
27+
}
28+
}
29+
legend {
30+
border-bottom: 0;
31+
font-size: @font-size-base + 1;
32+
font-weight: 600;
33+
margin-bottom: 10px;
34+
}
35+
.radio {
36+
margin-top: 0;
37+
}
38+
}
39+
40+
.updating {
41+
background-color: @color-pf-white;
42+
bottom: 55px;
43+
left: 0;
44+
padding-top: 60px;
45+
position: absolute;
46+
right: 0;
47+
top: 55px;
48+
z-index: 1000;
49+
}
50+
51+
.volume-options {
52+
margin-left: 20px;
53+
}
54+
}
55+
156
.osc-secrets-form {
257
.advanced-secrets,
358
.basic-secrets {
@@ -80,7 +135,7 @@ dl.secret-data {
80135
}
81136

82137
.create-secret-modal {
83-
background-color: #F5F5F5;
138+
background-color: @color-pf-black-150;
84139
.modal-footer{
85140
margin-top: 0px
86141
}

app/views/browse/secret.html

+13-2
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,21 @@ <h2>The secret details could not be loaded.</h2>
1010
</div>
1111
<div ng-if="loaded && !error">
1212
<h1 class="contains-actions">
13-
<div class="pull-right dropdown" ng-hide="!('secrets' | canIDoAny)">
14-
<button type="button" class="dropdown-toggle btn btn-default actions-dropdown-btn hidden-xs" data-toggle="dropdown">
13+
<div class="pull-right dropdown">
14+
<button type="button" class="btn btn-default hidden-xs" ng-click="addToApplication()">
15+
Add to Application
16+
</button>
17+
<button type="button" class="dropdown-toggle btn btn-default actions-dropdown-btn hidden-xs" data-toggle="dropdown" ng-hide="!('secrets' | canIDoAny)">
1518
Actions
1619
<span class="caret"></span>
1720
</button>
1821
<a href=""
1922
class="dropdown-toggle actions-dropdown-kebab visible-xs-inline"
2023
data-toggle="dropdown"><i class="fa fa-ellipsis-v"></i><span class="sr-only">Actions</span></a>
2124
<ul class="dropdown-menu dropdown-menu-right actions action-button">
25+
<li class="visible-xs">
26+
<a href="" role="button" ng-click="addToApplication()">Add to Application</a>
27+
</li>
2228
<li ng-if="'secrets' | canI : 'update'">
2329
<a ng-href="{{secret | editYamlURL}}" role="button">Edit YAML</a>
2430
</li>
@@ -32,6 +38,8 @@ <h1 class="contains-actions">
3238
</li>
3339
</ul>
3440
</div>
41+
<div class="pull-right" ng-hide="!('secrets' | canIDoAny)">
42+
</div>
3543
{{secret.metadata.name}}
3644
<small class="meta">created <span am-time-ago="secret.metadata.creationTimestamp"></span></small>
3745
</h1>
@@ -79,5 +87,8 @@ <h2 class="mar-top-none">
7987
</div><!-- /col-* -->
8088
</div>
8189
</div>
90+
<overlay-panel class="add-secret-to-application" show-panel="addToApplicationVisible" show-close="true" handle-close="closeAddToApplication">
91+
<add-secret-to-application project="project" secret="secret" on-cancel="closeAddToApplication" on-complete="closeAddToApplication"></add-secret-to-application>
92+
</overlay-panel>
8293
</div><!-- /middle-content -->
8394
</div>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
<div>
2+
<div class="dialog-title">
3+
<h3>Add to Application</h3>
4+
</div>
5+
<div class="dialog-body">
6+
<form name="addToApplicationForm" novalidate>
7+
<fieldset ng-disabled="disableInputs">
8+
<legend>Add this secret to application:</legend>
9+
<div class="form-group">
10+
<div class="application-select">
11+
<ui-select id="application" ng-model="ctrl.application" required="true" ng-disabled="ctrl.disableInputs">
12+
<ui-select-match placeholder="{{ctrl.applications.length ? 'Select an application' : 'There are no applications in this project'}}">
13+
<span>
14+
{{$select.selected.metadata.name}}
15+
<small class="text-muted">&ndash; {{$select.selected.kind | humanizeKind : true}}</small>
16+
</span>
17+
</ui-select-match>
18+
<ui-select-choices
19+
repeat="application in (ctrl.applications) | filter : { metadata: { name: $select.search } } track by (application | uid)"
20+
group-by="ctrl.groupByKind">
21+
<span ng-bind-html="application.metadata.name | highlight : $select.search"></span>
22+
</ui-select-choices>
23+
</ui-select>
24+
</div>
25+
</div>
26+
<legend>Add secret as:</legend>
27+
<div class="form-group">
28+
<div class="radio">
29+
<label class="add-choice" for="envFrom">
30+
<input id="envFrom" type="radio" ng-model="ctrl.addType" value="env" ng-disabled="ctrl.disableInputs">
31+
Environment variables
32+
</label>
33+
<div>
34+
<label class="add-choice" for="mountVolume">
35+
<input type="radio" ng-model="ctrl.addType" value="volume" ng-disabled="ctrl.disableInputs">
36+
Volume
37+
</label>
38+
</div>
39+
<div class="volume-options">
40+
<div ng-class="{'has-error': (addToApplicationForm.mountVolume.$error.pattern && addToApplicationForm.mountVolume.$touched)}">
41+
<input class="form-control"
42+
name="mountVolume"
43+
id="mountVolume"
44+
placeholder="Enter a mount path"
45+
type="text"
46+
required
47+
ng-pattern="/^\/.*$/"
48+
osc-unique="ctrl.existingMountPaths"
49+
aria-describedby="mount-path-help"
50+
ng-disabled="ctrl.addType !== 'volume' || ctrl.disableInputs"
51+
ng-model="ctrl.mountVolume"
52+
autocorrect="off"
53+
autocapitalize="off"
54+
spellcheck="false">
55+
</div>
56+
<div class="help-block bind-description">
57+
Mount Path for the volume. A file will be created in this director for each key from the secret. The file contents will be the value of the key.
58+
</div>
59+
<div class="has-error" ng-show="addToApplicationForm.mountVolume.$error.oscUnique">
60+
<span class="help-block">
61+
The mount path is already used. Please choose another mount path.
62+
</span>
63+
</div>
64+
</div>
65+
</div>
66+
</div>
67+
<div class="button-group pull-right">
68+
<button
69+
class="btn btn-default"
70+
ng-class="{'dialog-btn': isDialog}"
71+
ng-click="ctrl.onCancel()">
72+
Cancel
73+
</button>
74+
<button type="submit"
75+
class="btn btn-primary"
76+
ng-class="{'dialog-btn': isDialog}"
77+
ng-click="ctrl.addToApplication()"
78+
ng-disabled="ctrl.addType === 'volume' && addToApplicationForm.$invalid || !ctrl.application"
79+
value="">
80+
Save
81+
</button>
82+
</div>
83+
</fieldset>
84+
</form>
85+
<div class="updating" ng-if="ctrl.updating">
86+
<div class="spinner spinner-lg" aria-hidden="true"></div>
87+
<h3>
88+
<span class="sr-only">Updating</span>
89+
</h3>
90+
</div>
91+
</div>
92+
</div>

0 commit comments

Comments
 (0)