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

Deploy, rollback, retry and cancel deployments from the web console #4177

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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
42 changes: 26 additions & 16 deletions assets/app/scripts/controllers/builds.js
Expand Up @@ -129,7 +129,31 @@ angular.module('openshiftConsole')
{
type: "error",
message: "An error occurred while starting the build.",
details: getErrorDetails(result)
details: $filter('getErrorDetails')(result)
}
];
}
);
};

$scope.cancelBuild = function(build, buildConfigName) {
var canceledBuild = angular.copy(build);
canceledBuild.status.cancelled = true;
DataService.update("builds", canceledBuild.metadata.name, canceledBuild, $scope).then(
function() {
$scope.alerts = [
{
type: "success",
message: "Cancelling build " + build.metadata.name + " of " + buildConfigName + ".",
}
];
},
function(result) {
$scope.alerts = [
{
type: "error",
message: "An error occurred cancelling the build.",
details: $filter('getErrorDetails')(result)
}
];
}
Expand Down Expand Up @@ -159,7 +183,7 @@ angular.module('openshiftConsole')
{
type: "error",
message: "An error occurred while rerunning the build.",
details: getErrorDetails(result)
details: $filter('getErrorDetails')(result)
}
];
}
Expand All @@ -175,20 +199,6 @@ angular.module('openshiftConsole')
});
});

function getErrorDetails(result) {
var error = result.data || {};
if (error.message) {
return error.message;
}

var status = result.status || error.status;
if (status) {
return "Status: " + status;
}

return "";
}

$scope.$on('$destroy', function(){
DataService.unwatchAll(watches);
});
Expand Down
238 changes: 237 additions & 1 deletion assets/app/scripts/controllers/deployments.js
Expand Up @@ -30,7 +30,7 @@ angular.module('openshiftConsole')
});
}

watches.push(DataService.watch("replicationcontrollers", $scope, function(deployments) {
watches.push(DataService.watch("replicationcontrollers", $scope, function(deployments, action, deployment) {
$scope.unfilteredDeployments = deployments.by("metadata.name");
LabelFilter.addLabelSuggestionsFromResources($scope.unfilteredDeployments, $scope.labelSuggestions);
LabelFilter.setLabelSuggestions($scope.labelSuggestions);
Expand All @@ -41,6 +41,27 @@ angular.module('openshiftConsole')
associateDeploymentsToDeploymentConfig();
updateFilterWarning();

var deploymentConfigName;
var deploymentName;
if (deployment) {
deploymentConfigName = $filter('annotation')(deployment, 'deploymentConfig');
deploymentName = deployment.metadata.name;
}
if (!action) {
// Loading of the page that will create deploymentConfigDeploymentsInProgress structure, which will associate running deployment to his deploymentConfig.
$scope.deploymentConfigDeploymentsInProgress = associateRunningDeploymentToDeploymentConfig($scope.deploymentsByDeploymentConfig);
} else if (action === 'ADDED' || (action === 'MODIFIED' && ['New', 'Pending', 'Running'].indexOf($scope.deploymentStatus(deployment)) > -1)) {
// When new deployment id instantiated/cloned, or in case of a retry, associate him to his deploymentConfig and add him into deploymentConfigDeploymentsInProgress structure.
$scope.deploymentConfigDeploymentsInProgress[deploymentConfigName] = $scope.deploymentConfigDeploymentsInProgress[deploymentConfigName] || {};
$scope.deploymentConfigDeploymentsInProgress[deploymentConfigName][deploymentName] = deployment;
} else if (action === 'MODIFIED') {
// After the deployment ends remove him from the deploymentConfigDeploymentsInProgress structure.
var deploymentStatus = $scope.deploymentStatus(deployment);
if (deploymentStatus === "Complete" || deploymentStatus === "Failed"){
delete $scope.deploymentConfigDeploymentsInProgress[deploymentConfigName][deploymentName];
}
}

Logger.log("deployments (subscribe)", $scope.deployments);
}));

Expand Down Expand Up @@ -73,6 +94,20 @@ angular.module('openshiftConsole')
});
}

function associateRunningDeploymentToDeploymentConfig(deploymentsByDeploymentConfig) {
var deploymentConfigDeploymentsInProgress = {};
angular.forEach(deploymentsByDeploymentConfig, function(deploymentConfigDeployments, deploymentConfigName) {
deploymentConfigDeploymentsInProgress[deploymentConfigName] = {};
angular.forEach(deploymentConfigDeployments, function(deployment, deploymentName) {
var deploymentStatus = $scope.deploymentStatus(deployment);
if (deploymentStatus === "New" || deploymentStatus === "Pending" || deploymentStatus === "Running") {
deploymentConfigDeploymentsInProgress[deploymentConfigName][deploymentName] = deployment;
}
});
});
return deploymentConfigDeploymentsInProgress;
}

Copy link
Contributor

Choose a reason for hiding this comment

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

Wondering if we may want functions like this moved up into a service. It may be handy to have available in other contexts & keep the controllers light-weight. We don't have many services in front of DataService yet, but this one might be a good conversation starter. DataService provides methods like deployments.by("metadata.name"), but prob shouldn't provide methods specific to a particular object. If we wrapped it we could then also have methods like deployments.associateRunningDeploymentToDeploymentConfig() on the service. We might be able to shorten names then, perhaps something like deployments.byConfig(config).

Conceptually I'm thinking something like this:

// this would likely use the DataService.watch("replicationcontrollers", $scope, function(deployments) {});
replicationControllers.watch(context, function(deployments) {

});

// and the deployments object could look something like this, giving us data & functions:
return {
  data: { /* the request data */ },
  // functions we can share so they don't have to live in controllers
  byConfig: function(config) { return /* */ },
  foo: function() { /* */ }

}

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah, it makes sense to me. I would handle it separately (not as part of this PR) but definitely makes sense.

function updateFilterWarning() {
if (!LabelFilter.getLabelSelector().isEmpty() && $.isEmptyObject($scope.deployments) && !$.isEmptyObject($scope.unfilteredDeployments)) {
$scope.alerts["deployments"] = {
Expand All @@ -85,6 +120,207 @@ angular.module('openshiftConsole')
}
}

$scope.startLatestDeployment = function(deploymentConfigName) {
var deploymentConfig = $scope.deploymentConfigs[deploymentConfigName];

// increase latest version by one so starts new deployment based on latest
var req = {
kind: "DeploymentConfig",
apiVersion: "v1",
metadata: deploymentConfig.metadata,
spec: deploymentConfig.spec,
status: deploymentConfig.status
};
if (!req.status.latestVersion) {
req.status.latestVersion = 0;
}
req.status.latestVersion++;

// update the deployment config
DataService.update("deploymentconfigs", deploymentConfigName, req, $scope).then(
function() {
$scope.alerts = [
{
type: "success",
message: "Deployment #" + req.status.latestVersion + " of " + deploymentConfigName + " has started.",
}
];
},
function(result) {
$scope.alerts = [
{
type: "error",
message: "An error occurred while starting the deployment.",
details: $filter('getErrorDetails')(result)
}
];
}
);
};

$scope.retryFailedDeployment = function(deploymentConfigName, deploymentName) {
var deployment = $scope.deploymentsByDeploymentConfig[deploymentConfigName][deploymentName];
var req = deployment;

// TODO: we need a "retry" api endpoint so we don't have to do this manually

// delete the deployer pod as well as the deployment hooks pods, if any
DataService.list("pods", $scope, function(list) {
Copy link
Member

Choose a reason for hiding this comment

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

i hate that we have to get the whole list of pods and then find the ones that match up :-
@ironcladlou is it possible for the RC generated by the deployment to have an annotation for who its deployer pod was?

Copy link
Contributor

Choose a reason for hiding this comment

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

Wait... You guys shouldn't be deleting the deployed pod... That's the
deployment controllers responsibility.

On Aug 26, 2015, at 10:47 AM, Jessica Forrester notifications@github.com
wrote:

In assets/app/scripts/controllers/deployments.js
#4177 (comment):

  •        {
    
  •          type: "error",
    
  •          message: "An error occurred while starting the deployment.",
    
  •          details: getErrorDetails(result)
    
  •        }
    
  •      ];
    
  •    }
    
  •  );
    
  • };
  • $scope.retryFailedDeployment = function(deploymentConfigName, deploymentName) {
  •  var deployment = $scope.deploymentsByDeploymentConfig[deploymentConfigName][deploymentName];
    
  •  var req = deployment;
    
  •  // delete the deployer pod as well as the deployment hooks pods, if any
    
  •  DataService.list("pods", $scope, function(list) {
    

i hate that we have to get the whole list of pods and then find the ones
that match up :-
@ironcladlou https://github.com/ironcladlou is it possible for the RC
generated by the deployment to have an annotation for who its deployer pod
was?


Reply to this email directly or view it on GitHub
https://github.com/openshift/origin/pull/4177/files#r37982680.

Copy link
Contributor

Choose a reason for hiding this comment

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

Deployment is cancelled by setting the cancel annotation on an RC - it's
out of your hands then.

On Aug 26, 2015, at 10:47 AM, Jessica Forrester notifications@github.com
wrote:

In assets/app/scripts/controllers/deployments.js
#4177 (comment):

  •        {
    
  •          type: "error",
    
  •          message: "An error occurred while starting the deployment.",
    
  •          details: getErrorDetails(result)
    
  •        }
    
  •      ];
    
  •    }
    
  •  );
    
  • };
  • $scope.retryFailedDeployment = function(deploymentConfigName, deploymentName) {
  •  var deployment = $scope.deploymentsByDeploymentConfig[deploymentConfigName][deploymentName];
    
  •  var req = deployment;
    
  •  // delete the deployer pod as well as the deployment hooks pods, if any
    
  •  DataService.list("pods", $scope, function(list) {
    

i hate that we have to get the whole list of pods and then find the ones
that match up :-
@ironcladlou https://github.com/ironcladlou is it possible for the RC
generated by the deployment to have an annotation for who its deployer pod
was?


Reply to this email directly or view it on GitHub
https://github.com/openshift/origin/pull/4177/files#r37982680.

Copy link
Member Author

Choose a reason for hiding this comment

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

I definitely agree, but this is exactly what the command line tools are doing - should be fixed on the server and then we remove it from both the cli and web console, but having both doing different things will quickly lead to inconsistencies.

Copy link
Contributor

Choose a reason for hiding this comment

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

Not only that, we can't fix bugs in one place, and users who also want to
cancel deployments hate us.

On Wed, Aug 26, 2015 at 11:55 AM, Fabiano Franz notifications@github.com
wrote:

In assets/app/scripts/controllers/deployments.js
#4177 (comment):

  •        {
    
  •          type: "error",
    
  •          message: "An error occurred while starting the deployment.",
    
  •          details: getErrorDetails(result)
    
  •        }
    
  •      ];
    
  •    }
    
  •  );
    
  • };
  • $scope.retryFailedDeployment = function(deploymentConfigName, deploymentName) {
  •  var deployment = $scope.deploymentsByDeploymentConfig[deploymentConfigName][deploymentName];
    
  •  var req = deployment;
    
  •  // delete the deployer pod as well as the deployment hooks pods, if any
    
  •  DataService.list("pods", $scope, function(list) {
    

I definitely agree, but this is exactly what the command line tools are
doing - should be fixed on the server and then we remove it from both the
cli and web console, but having both doing different things will quickly
lead to inconsistencies.


Reply to this email directly or view it on GitHub
https://github.com/openshift/origin/pull/4177/files#r37991603.

Clayton Coleman | Lead Engineer, OpenShift

Copy link
Member Author

Choose a reason for hiding this comment

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

Sorry, just saw the other comments.

var pods = list.by("metadata.name");
var deleteDeployerPod = function(pod) {
var deployerPodForAnnotation = $filter('annotationName')('deployerPodFor');
if (pod.metadata.labels[deployerPodForAnnotation] === deploymentName) {
DataService.delete("pods", pod.metadata.name, $scope).then(
function() {
Logger.info("Deployer pod " + pod.metadata.name + " deleted");
},
function(result) {
$scope.alerts = [
{
type: "error",
message: "An error occurred while deleting the deployer pod.",
Copy link
Member

Choose a reason for hiding this comment

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

Isn't it possible that someone just went and cleaned up all their old deployer pods. Should we be popping up an error if the delete failed from a 404? It doesn't stop us from retrying the deployment.

On a related note, if we fail to delete the deployer pod and it still exists, non 404 error, should we still be attempting to retry the deployment?

Copy link
Contributor

Choose a reason for hiding this comment

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

Unfortunately, you do actually need to clean up deployers before resetting states for a retry (duplicating logic currently all bound up in the deploy CLI command). We have a cart to introduce API endpoints for deploy/retry/cancel in this trello card.

You can select all deployer pods using a label selector:

openshift.io/deployer-pod-for.name=<deployment-name>

Copy link
Member

Choose a reason for hiding this comment

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

Using the selector would be better at least, then we arent getting ALL the
pods. Is there a timeframe for the API endpoints?

On Wed, Aug 26, 2015 at 10:26 AM, Dan Mace notifications@github.com wrote:

In assets/app/scripts/controllers/deployments.js
#4177 (comment):

  •  // delete the deployer pod as well as the deployment hooks pods, if any
    
  •  DataService.list("pods", $scope, function(list) {
    
  •    var pods = list.by("metadata.name");
    
  •    var deleteDeployerPod = function(pod) {
    
  •      var deployerPodForAnnotation = $filter('annotationName')('deployerPodFor');
    
  •      if (pod.metadata.labels[deployerPodForAnnotation] === deploymentName) {
    
  •        DataService.delete("pods", pod.metadata.name, $scope).then(
    
  •          function() {
    
  •            Logger.info("Deployer pod " + pod.metadata.name + " deleted");
    
  •          },
    
  •          function(result) {
    
  •            $scope.alerts = [
    
  •              {
    
  •                type: "error",
    
  •                message: "An error occurred while deleting the deployer pod.",
    

Unfortunately, you do actually need to clean up deployers before resetting
states for a retry (duplicating logic currently all bound up in the deploy
CLI command). We have a cart to introduce API endpoints for
deploy/retry/cancel in this trello card https://trello.com/c/qCzTBO0h.

You can select all deployer pods using a label selector:

openshift.io/deployer-pod-for.name=


Reply to this email directly or view it on GitHub
https://github.com/openshift/origin/pull/4177/files#r37987743.

Copy link
Contributor

Choose a reason for hiding this comment

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

On Aug 26, 2015, at 11:26 AM, Dan Mace notifications@github.com wrote:

In assets/app/scripts/controllers/deployments.js
#4177 (comment):

  •  // delete the deployer pod as well as the deployment hooks pods, if any
    
  •  DataService.list("pods", $scope, function(list) {
    
  •    var pods = list.by("metadata.name");
    
  •    var deleteDeployerPod = function(pod) {
    
  •      var deployerPodForAnnotation = $filter('annotationName')('deployerPodFor');
    
  •      if (pod.metadata.labels[deployerPodForAnnotation] === deploymentName) {
    
  •        DataService.delete("pods", pod.metadata.name, $scope).then(
    
  •          function() {
    
  •            Logger.info("Deployer pod " + pod.metadata.name + " deleted");
    
  •          },
    
  •          function(result) {
    
  •            $scope.alerts = [
    
  •              {
    
  •                type: "error",
    
  •                message: "An error occurred while deleting the deployer pod.",
    

Unfortunately, you do actually need to clean up deployers before resetting
states for a retry (duplicating logic currently all bound up in the deploy
CLI command). We have a cart to introduce API endpoints for
deploy/retry/cancel in this trello card https://trello.com/c/qCzTBO0h.

I don't want to introduce the UI for this until an API exists. It's too
fragile. If we need to prioritize the API on your team and get someone to
help complete this, let's get that sorted out with @pweil-. People doing
this fiddling scares me quite a lot.

You can select all deployer pods using a label selector:

openshift.io/deployer-pod-for.name=


Reply to this email directly or view it on GitHub
https://github.com/openshift/origin/pull/4177/files#r37987743.

Copy link
Contributor

Choose a reason for hiding this comment

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

Fine by me. The card is currently sitting at the bottom of our New list, so there's currently no timeframe for it.

details: $filter('getErrorDetails')(result)
}
];
}
);
}
};
angular.forEach(pods, deleteDeployerPod);
});

// set deployment to "New" and remove statuses so we can retry
var deploymentStatusAnnotation = $filter('annotationName')('deploymentStatus');
var deploymentStatusReasonAnnotation = $filter('annotationName')('deploymentStatusReason');
var deploymentCancelledAnnotation = $filter('annotationName')('deploymentCancelled');
req.metadata.annotations[deploymentStatusAnnotation] = "New";
delete req.metadata.annotations[deploymentStatusReasonAnnotation];
delete req.metadata.annotations[deploymentCancelledAnnotation];

// update the deployment
DataService.update("replicationcontrollers", deploymentName, req, $scope).then(
function() {
$scope.alerts = [
{
type: "success",
message: "Retrying deployment " + deploymentName + " of " + deploymentConfigName + ".",
}
];
},
function(result) {
$scope.alerts = [
{
type: "error",
message: "An error occurred while retrying the deployment.",
details: $filter('getErrorDetails')(result)
}
];
}
);
};

$scope.rollbackToDeployment = function(deploymentConfigName, deploymentName, changeScaleSettings, changeStrategy, changeTriggers) {
// put together a new rollback request
var req = {
kind: "DeploymentConfigRollback",
apiVersion: "v1",
spec: {
from: {
name: deploymentName
},
includeTemplate: true,
includeReplicationMeta: changeScaleSettings,
includeStrategy: changeStrategy,
includeTriggers: changeTriggers
}
};

// TODO: we need a "rollback" api endpoint so we don't have to do this manually

// create the deployment config rollback
DataService.create("deploymentconfigrollbacks", null, req, $scope).then(
function(newDeploymentConfig) {
// update the deployment config based on the one returned by the rollback
DataService.update("deploymentconfigs", deploymentConfigName, newDeploymentConfig, $scope).then(
function(rolledBackDeploymentConfig) {
$scope.alerts = [
{
type: "success",
message: "Deployment #" + rolledBackDeploymentConfig.status.latestVersion + " is rolling back " + deploymentConfigName + " to " + deploymentName + ".",
}
];
},
function(result) {
$scope.alerts = [
{
type: "error",
message: "An error occurred while rolling back the deployment.",
details: $filter('getErrorDetails')(result)
}
];
}
);
},
function(result) {
$scope.alerts = [
{
type: "error",
message: "An error occurred while rolling back the deployment.",
details: $filter('getErrorDetails')(result)
}
];
}
);
};

$scope.cancelRunningDeployment = function(deploymentConfigName, deploymentName) {
var deployment = $scope.deploymentsByDeploymentConfig[deploymentConfigName][deploymentName];
var req = deployment;

// TODO: we need a "cancel" api endpoint so we don't have to do this manually

// set the cancellation annotations
var deploymentCancelledAnnotation = $filter('annotationName')('deploymentCancelled');
var deploymentStatusReasonAnnotation = $filter('annotationName')('deploymentStatusReason');
req.metadata.annotations[deploymentCancelledAnnotation] = "true";
req.metadata.annotations[deploymentStatusReasonAnnotation] = "The deployment was cancelled by the user";

// update the deployment with cancellation annotations
DataService.update("replicationcontrollers", deploymentName, req, $scope).then(
function() {
$scope.alerts = [
{
type: "success",
message: "Cancelling deployment " + deploymentName + " of " + deploymentConfigName + ".",
}
];
},
function(result) {
$scope.alerts = [
{
type: "error",
message: "An error occurred while cancelling the deployment.",
details: $filter('getErrorDetails')(result)
}
];
}
);
};

$scope.deploymentIsLatest = function(deploymentConfig, deployment) {
var deploymentVersion = parseInt($filter('annotation')(deployment, 'deploymentVersion'));
var deploymentConfigVersion = deploymentConfig.status.latestVersion;
return deploymentVersion === deploymentConfigVersion;
};

$scope.deploymentStatus = function(deployment) {
Copy link
Member

Choose a reason for hiding this comment

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

dont add annotation filter functions to the controllers, either add this to filters/resources.js if you think its common enough, or just use deployment | annotation : 'deploymentStatus' in the view

Copy link
Member

Choose a reason for hiding this comment

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

actually i will make an exception in this case given how much you are having to use it :)

Copy link
Member Author

Choose a reason for hiding this comment

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

👍

return $filter('annotation')(deployment, 'deploymentStatus');
};

$scope.deploymentIsInProgress = function(deployment) {
return ['New', 'Pending', 'Running'].indexOf($scope.deploymentStatus(deployment)) > -1;
};

LabelFilter.onActiveFiltersChanged(function(labelSelector) {
// trigger a digest loop
$scope.$apply(function() {
Expand Down
38 changes: 23 additions & 15 deletions assets/app/scripts/filters/resources.js
Expand Up @@ -14,28 +14,36 @@ angular.module('openshiftConsole')
}
};
})
.filter('annotation', function() {
// This maps an annotation key to all known synonymous keys to insulate
// the referring code from key renames across API versions.
var annotationMap = {
"deploymentConfig": ["openshift.io/deployment-config.name"],
"deployment": ["openshift.io/deployment.name"],
"pod": ["openshift.io/deployer-pod.name"],
"deploymentStatus": ["openshift.io/deployment.phase"],
"encodedDeploymentConfig": ["openshift.io/encoded-deployment-config"],
"deploymentVersion": ["openshift.io/deployment-config.latest-version"],
"displayName": ["openshift.io/display-name"],
"description": ["openshift.io/description"],
"buildNumber": ["openshift.io/build.number"]
};
.filter('annotationName', function() {
return function(annotationKey) {
// This maps an annotation key to all known synonymous keys to insulate
// the referring code from key renames across API versions.
var annotationMap = {
"deploymentConfig": ["openshift.io/deployment-config.name"],
"deployment": ["openshift.io/deployment.name"],
"pod": ["openshift.io/deployer-pod.name"],
"deployerPodFor": ["openshift.io/deployer-pod-for.name"],
"deploymentStatus": ["openshift.io/deployment.phase"],
"deploymentStatusReason": ["openshift.io/deployment.status-reason"],
"deploymentCancelled": ["openshift.io/deployment.cancelled"],
"encodedDeploymentConfig": ["openshift.io/encoded-deployment-config"],
"deploymentVersion": ["openshift.io/deployment-config.latest-version"],
"displayName": ["openshift.io/display-name"],
"description": ["openshift.io/description"],
"buildNumber": ["openshift.io/build.number"]
};
return annotationMap[annotationKey] || null;
};
})
.filter('annotation', function(annotationNameFilter) {
return function(resource, key) {
if (resource && resource.metadata && resource.metadata.annotations) {
// If the key's already in the annotation map, return it.
if (resource.metadata.annotations[key] !== undefined) {
return resource.metadata.annotations[key];
}
// Try and return a value for a mapped key.
var mappings = annotationMap[key] || [];
var mappings = annotationNameFilter(key) || [];
for (var i=0; i < mappings.length; i++) {
var mappedKey = mappings[i];
if (resource.metadata.annotations[mappedKey] !== undefined) {
Expand Down