Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add the ability to add a secret to an application #2021

Merged
merged 1 commit into from
Sep 7, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions app/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,7 @@ <h1>JavaScript Required</h1>
<script src="scripts/directives/labels.js"></script>
<script src="scripts/directives/lifecycleHook.js"></script>
<script src="scripts/directives/actionChip.js"></script>
<script src="scripts/directives/addSecretToApplication.js"></script>
<script src="scripts/directives/templateopt.js"></script>
<script src="scripts/directives/tasks.js"></script>
<script src="scripts/directives/catalog.js"></script>
Expand Down
10 changes: 10 additions & 0 deletions app/scripts/controllers/secret.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,16 @@ angular.module('openshiftConsole')
}
];

$scope.addToApplicationVisible = false;

$scope.addToApplication = function() {
$scope.addToApplicationVisible = true;
};

$scope.closeAddToApplication = function() {
$scope.addToApplicationVisible = false;
};

ProjectsService
.get($routeParams.project)
.then(_.spread(function(project, context) {
Expand Down
180 changes: 180 additions & 0 deletions app/scripts/directives/addSecretToApplication.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
"use strict";
(function() {
angular.module("openshiftConsole").component('addSecretToApplication', {
controller: [
'$filter',
'$scope',
'APIService',
'DataService',
'Navigate',
'NotificationsService',
'StorageService',
AddSecretToApplication
],
controllerAs: 'ctrl',
bindings: {
project: '<',
secret: '<',
onComplete: '<',
onCancel: '<'
},
templateUrl: 'views/directives/add-secret-to-application.html'
});

function AddSecretToApplication($filter, $scope, APIService, DataService, Navigate, NotificationsService, StorageService) {
var ctrl = this;
var deploymentConfigs;
var deployments;
var replicationControllers;
var replicaSets;
var statefulSets;

var sortApplications = function() {
// Don't waste time sorting on each data load, just sort when we have them all
if (deploymentConfigs && deployments && replicationControllers && replicaSets && statefulSets) {
var apiObjects = deploymentConfigs.concat(deployments)
.concat(replicationControllers)
.concat(replicaSets)
.concat(statefulSets);
ctrl.applications = _.sortBy(apiObjects, ['metadata.name', 'kind']);
ctrl.updating = false;
}
};

var getApplications = function() {
var hasDeploymentFilter = $filter('hasDeployment');
var hasDeploymentConfigFilter = $filter('hasDeploymentConfig');

ctrl.updating = true;
var context = {
namespace: ctrl.project.metadata.name
};
// Load all the "application" types
DataService.list('deploymentconfigs', context).then(function(deploymentConfigData) {
deploymentConfigs = _.toArray(deploymentConfigData.by('metadata.name'));
sortApplications();
});
DataService.list('replicationcontrollers', context).then(function(replicationControllerData) {
replicationControllers = _.reject(replicationControllerData.by('metadata.name'), hasDeploymentConfigFilter);
sortApplications();
});
DataService.list({
group: 'apps',
resource: 'deployments'
}, context).then(function(deploymentData) {
deployments = _.toArray(deploymentData.by('metadata.name'));
sortApplications();
});
DataService.list({
group: 'extensions',
resource: 'replicasets'
}, context).then(function(replicaSetData) {
replicaSets = _.reject(replicaSetData.by('metadata.name'), hasDeploymentFilter);
sortApplications();
});
DataService.list({
group: 'apps',
resource: 'statefulsets'
}, context).then(function(statefulSetData) {
statefulSets = _.toArray(statefulSetData.by('metadata.name'));
sortApplications();
});
};
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We're doing this now in enough places now that it would be good to put this logic into a separate service (ApplicationsService?).

Not for this PR, though.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Heh, I thought the same and started working on it but I ran out of time to make the change.

Added issue: openshift/origin-web-common#175


ctrl.$onInit = function() {
ctrl.addType = 'env';
ctrl.disableInputs = false;
getApplications();
};

ctrl.$postLink = function() {
$scope.$watch(function() {
return ctrl.application;
}, function() {
// Look at the existing mount paths so that we can warn if the new value is not unique.
var podTemplate = _.get(ctrl.application, 'spec.template');
ctrl.existingMountPaths = StorageService.getMountPaths(podTemplate);
});
};

ctrl.addToApplication = function() {
var applicationToUpdate = angular.copy(ctrl.application);

var podTemplate = _.get(applicationToUpdate, 'spec.template');

ctrl.disableInputs = true;

if (ctrl.addType === 'env') {
var newEnvFrom = {
secretRef: {
name: ctrl.secret.metadata.name
}
};

// For each container, add the new volume mount.
_.each(podTemplate.spec.containers, function(container) {
container.envFrom = container.envFrom || [];
container.envFrom.push(newEnvFrom);
Copy link
Member

@spadgett spadgett Sep 6, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should call angular.copy before we start changing the resource. Otherwise it mutates an object that's in the DataService cache

});
} else {
var generateName = $filter('generateName');
var name = generateName(ctrl.secret.metadata.name + '-');
var newVolumeMount = {
name: name,
mountPath: ctrl.mountVolume,
readOnly: true
};

// For each selected container, add the new volume mount.
_.each(podTemplate.spec.containers, function(container) {
container.volumeMounts = container.volumeMounts || [];
container.volumeMounts.push(newVolumeMount);
});

var newVolume = {
name: name,
secret: {
secretName: ctrl.secret.metadata.name
}
};

podTemplate.spec.volumes = podTemplate.spec.volumes || [];
podTemplate.spec.volumes.push(newVolume);
}

var humanizeKind = $filter('humanizeKind');
var sourceKind = humanizeKind(ctrl.secret.kind);
var targetKind = humanizeKind(applicationToUpdate.kind);
var context = {
namespace: ctrl.project.metadata.name
};

DataService.update(APIService.kindToResource(applicationToUpdate.kind), applicationToUpdate.metadata.name, applicationToUpdate, context).then(
function() {
NotificationsService.addNotification({
type: "success",
message: "Successfully added " + sourceKind + " " + ctrl.secret.metadata.name + " to " + targetKind + " " + applicationToUpdate.metadata.name + ".",
links: [{
href: Navigate.resourceURL(applicationToUpdate),
label: "View " + humanizeKind(applicationToUpdate.kind, true)
}]
});
if (angular.isFunction(ctrl.onComplete)) {
ctrl.onComplete();
}
},
function(result) {
var getErrorDetails = $filter('getErrorDetails');

NotificationsService.addNotification({
type: "error",
message: "An error occurred adding " + sourceKind + " " + ctrl.secret.metadata.name + " to " + targetKind + " " + applicationToUpdate.metadata.name + ". " +
getErrorDetails(result)
});
}).finally(function() {
ctrl.disableInputs = false;
}
);
};
}
})();
57 changes: 56 additions & 1 deletion app/styles/_secrets.less
Original file line number Diff line number Diff line change
@@ -1,3 +1,58 @@
.add-secret-to-application {
.catalogs-overlay-panel {
max-width: 600px;
}

.dialog-title {
border-bottom: 1px solid @color-pf-black-300;

h3 {
margin: 18px 0;
padding-left: 15px;
}
}

.dialog-body {
padding: 20px;

.add-choice {
margin-bottom: 10px;
}
.button-group {
.btn {
margin-left: 10px;
&:first-of-type {
margin-left: 0;
}
}
}
legend {
border-bottom: 0;
font-size: @font-size-base + 1;
font-weight: 600;
margin-bottom: 10px;
}
.radio {
margin-top: 0;
}
}

.updating {
background-color: @color-pf-white;
bottom: 55px;
left: 0;
padding-top: 60px;
position: absolute;
right: 0;
top: 55px;
z-index: 1000;
}

.volume-options {
margin-left: 20px;
}
}

.osc-secrets-form {
.advanced-secrets,
.basic-secrets {
Expand Down Expand Up @@ -80,7 +135,7 @@ dl.secret-data {
}

.create-secret-modal {
background-color: #F5F5F5;
background-color: @color-pf-black-150;
.modal-footer{
margin-top: 0px
}
Expand Down
13 changes: 11 additions & 2 deletions app/views/browse/secret.html
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,21 @@ <h2>The secret details could not be loaded.</h2>
</div>
<div ng-if="loaded && !error">
<h1 class="contains-actions">
<div class="pull-right dropdown" ng-hide="!('secrets' | canIDoAny)">
<button type="button" class="dropdown-toggle btn btn-default actions-dropdown-btn hidden-xs" data-toggle="dropdown">
<div class="pull-right dropdown">
<button type="button" class="btn btn-default hidden-xs" ng-click="addToApplication()">
Add to Application
</button>
<button type="button" class="dropdown-toggle btn btn-default actions-dropdown-btn hidden-xs" data-toggle="dropdown" ng-hide="!('secrets' | canIDoAny)">
Actions
<span class="caret"></span>
</button>
<a href=""
class="dropdown-toggle actions-dropdown-kebab visible-xs-inline"
data-toggle="dropdown"><i class="fa fa-ellipsis-v"></i><span class="sr-only">Actions</span></a>
<ul class="dropdown-menu dropdown-menu-right actions action-button">
<li class="visible-xs">
<a href="" role="button" ng-click="addToApplication()">Add to Application</a>
</li>
<li ng-if="'secrets' | canI : 'update'">
<a ng-href="{{secret | editYamlURL}}" role="button">Edit YAML</a>
</li>
Expand Down Expand Up @@ -79,5 +85,8 @@ <h2 class="mar-top-none">
</div><!-- /col-* -->
</div>
</div>
<overlay-panel class="add-secret-to-application" show-panel="addToApplicationVisible" show-close="true" handle-close="closeAddToApplication">
<add-secret-to-application project="project" secret="secret" on-cancel="closeAddToApplication" on-complete="closeAddToApplication"></add-secret-to-application>
</overlay-panel>
</div><!-- /middle-content -->
</div>
92 changes: 92 additions & 0 deletions app/views/directives/add-secret-to-application.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
<div>
<div class="dialog-title">
<h3>Add to Application</h3>
</div>
<div class="dialog-body">
<form name="addToApplicationForm" novalidate>
<fieldset ng-disabled="disableInputs">
<legend>Add this secret to application:</legend>
<div class="form-group">
<div class="application-select">
<ui-select id="application" ng-model="ctrl.application" required="true" ng-disabled="ctrl.disableInputs">
<ui-select-match placeholder="{{ctrl.applications.length ? 'Select an application' : 'There are no applications in this project'}}">
<span>
{{$select.selected.metadata.name}}
<small class="text-muted">&ndash; {{$select.selected.kind | humanizeKind : true}}</small>
</span>
</ui-select-match>
<ui-select-choices
repeat="application in (ctrl.applications) | filter : { metadata: { name: $select.search } } track by (application | uid)"
group-by="ctrl.groupByKind">
<span ng-bind-html="application.metadata.name | highlight : $select.search"></span>
</ui-select-choices>
</ui-select>
</div>
</div>
<legend>Add secret as:</legend>
<div class="form-group">
<div class="radio">
<label class="add-choice" for="envFrom">
<input id="envFrom" type="radio" ng-model="ctrl.addType" value="env" ng-disabled="ctrl.disableInputs">
Environment variables
</label>
<div>
<label class="add-choice" for="mountVolume">
<input type="radio" ng-model="ctrl.addType" value="volume" ng-disabled="ctrl.disableInputs">
Volume
</label>
</div>
<div class="volume-options">
<div ng-class="{'has-error': (addToApplicationForm.mountVolume.$error.pattern && addToApplicationForm.mountVolume.$touched)}">
<input class="form-control"
name="mountVolume"
id="mountVolume"
placeholder="Enter a mount path"
type="text"
required
ng-pattern="/^\/.*$/"
osc-unique="ctrl.existingMountPaths"
aria-describedby="mount-path-help"
ng-disabled="ctrl.addType !== 'volume' || ctrl.disableInputs"
ng-model="ctrl.mountVolume"
autocorrect="off"
autocapitalize="off"
spellcheck="false">
</div>
<div class="help-block bind-description">
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.
</div>
<div class="has-error" ng-show="addToApplicationForm.mountVolume.$error.oscUnique">
<span class="help-block">
The mount path is already used. Please choose another mount path.
</span>
</div>
</div>
</div>
</div>
<div class="button-group pull-right">
<button
class="btn btn-default"
ng-class="{'dialog-btn': isDialog}"
ng-click="ctrl.onCancel()">
Cancel
</button>
<button type="submit"
class="btn btn-primary"
ng-class="{'dialog-btn': isDialog}"
ng-click="ctrl.addToApplication()"
ng-disabled="ctrl.addType === 'volume' && addToApplicationForm.$invalid || !ctrl.application"
value="">
Save
</button>
</div>
</fieldset>
</form>
<div class="updating" ng-if="ctrl.updating">
<div class="spinner spinner-lg" aria-hidden="true"></div>
<h3>
<span class="sr-only">Updating</span>
</h3>
</div>
</div>
</div>
Loading