/* * Copyright 2017-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.cloud.skipper.shell.command; import java.io.File; import java.io.IOException; import java.time.Duration; import java.util.ArrayList; import java.util.Collection; import java.util.LinkedHashMap; import java.util.List; import javax.validation.constraints.NotNull; import org.apache.commons.io.FilenameUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cloud.deployer.spi.app.DeploymentState; import org.springframework.cloud.skipper.ReleaseNotFoundException; import org.springframework.cloud.skipper.SkipperException; import org.springframework.cloud.skipper.client.SkipperClient; import org.springframework.cloud.skipper.domain.CancelRequest; import org.springframework.cloud.skipper.domain.CancelResponse; import org.springframework.cloud.skipper.domain.ConfigValues; import org.springframework.cloud.skipper.domain.Info; import org.springframework.cloud.skipper.domain.PackageIdentifier; import org.springframework.cloud.skipper.domain.Release; import org.springframework.cloud.skipper.domain.RollbackRequest; import org.springframework.cloud.skipper.domain.UpgradeProperties; import org.springframework.cloud.skipper.domain.UpgradeRequest; import org.springframework.cloud.skipper.shell.command.support.DeploymentStateDisplay; import org.springframework.cloud.skipper.shell.command.support.TableUtils; import org.springframework.cloud.skipper.shell.command.support.YmlUtils; import org.springframework.cloud.skipper.support.DurationUtils; import org.springframework.shell.standard.ShellComponent; import org.springframework.shell.standard.ShellMethod; import org.springframework.shell.standard.ShellOption; import org.springframework.shell.table.ArrayTableModel; import org.springframework.shell.table.BeanListTableModel; import org.springframework.shell.table.Table; import org.springframework.shell.table.TableBuilder; import org.springframework.shell.table.TableModel; import org.springframework.util.Assert; import org.springframework.util.StringUtils; /** * The main skipper commands that deal with releases. * @author Ilayaperumal Gopinathan * @author Mark Pollack */ @ShellComponent public class ReleaseCommands extends AbstractSkipperCommand { private static final Logger logger = LoggerFactory.getLogger(ReleaseCommands.class); @Autowired public ReleaseCommands(SkipperClient skipperClient) { this.skipperClient = skipperClient; } /** * Aggregate the set of app states into a single state for a stream. * * @param states set of states for apps of a stream * @return the stream state based on app states */ public static DeploymentState aggregateState(List<DeploymentState> states) { if (states.size() == 1) { DeploymentState state = states.iterator().next(); logger.debug("aggregateState: Deployment State Set Size = 1. Deployment State " + state); // a stream which is known to the stream definition repository // but unknown to deployers is undeployed if (state == DeploymentState.unknown) { logger.debug("aggregateState: Returning " + DeploymentState.undeployed); return DeploymentState.undeployed; } else { logger.debug("aggregateState: Returning " + state); return state; } } if (states.isEmpty() || states.contains(DeploymentState.error)) { logger.debug("aggregateState: Returning " + DeploymentState.error); return DeploymentState.error; } if (states.contains(DeploymentState.failed)) { logger.debug("aggregateState: Returning " + DeploymentState.failed); return DeploymentState.failed; } if (states.contains(DeploymentState.deploying)) { logger.debug("aggregateState: Returning " + DeploymentState.deploying); return DeploymentState.deploying; } if (allAppsDeployed(states)) { return DeploymentState.deployed; } logger.debug("aggregateState: Returning " + DeploymentState.partial); return DeploymentState.partial; } private static boolean allAppsDeployed(List<DeploymentState> deploymentStateList) { boolean allDeployed = true; for (DeploymentState deploymentState : deploymentStateList) { if (deploymentState != DeploymentState.deployed) { allDeployed = false; break; } } return allDeployed; } @ShellMethod(key = "release upgrade", value = "Upgrade a release.") public Object upgrade( @ShellOption(help = "the name of the release to upgrade") String releaseName, @ShellOption(help = "the name of the package to use for the upgrade") String packageName, @ShellOption(help = "the version of the package to use for the upgrade, if not specified latest version will be used", defaultValue = ShellOption.NULL) String packageVersion, @ShellOption(help = "specify values in a YAML file", defaultValue = ShellOption.NULL) File file, @ShellOption(help = "the expression for upgrade timeout", defaultValue = ShellOption.NULL) String timeoutExpression, @ShellOption(help = "the comma separated set of properties to override during upgrade", defaultValue = ShellOption.NULL) String properties, @ShellOption(help = "force upgrade") boolean force, @ShellOption(help = "application names to force upgrade. If no specific list is provided, all the apps in the packages are force upgraded", defaultValue = ShellOption.NULL) String appNames) throws IOException { // Commented out until https://github.com/spring-cloud/spring-cloud-skipper/issues/263 is // addressed // assertMutuallyExclusiveFileAndProperties(file, properties); if (StringUtils.hasText(appNames)) { Assert.isTrue(force, "App names can be used only when the stream update is forced."); } Release release = skipperClient .upgrade(getUpgradeRequest(releaseName, packageName, packageVersion, file, properties, timeoutExpression, force, appNames)); StringBuilder sb = new StringBuilder(); sb.append(release.getName() + " has been upgraded. Now at version v" + release.getVersion() + "."); return sb.toString(); } private void updateStatus(StringBuilder sb, Release release) { sb.append("Release Status: " + release.getInfo().getStatus().getStatusCode() + "\n"); if (StringUtils.hasText(release.getInfo().getStatus().getPlatformStatus())) { sb.append("Platform Status: " + release.getInfo().getStatus().getPlatformStatusPrettyPrint()); } else { sb.append("Platform Status: unknown"); } } private void assertMutuallyExclusiveFileAndProperties(File yamlFile, String properties) { Assert.isTrue(!(yamlFile != null && properties != null), "The options 'file' and 'properties' options " + "are mutually exclusive."); if (yamlFile != null) { String extension = FilenameUtils.getExtension(yamlFile.getName()); Assert.isTrue((extension.equalsIgnoreCase("yml") || extension.equalsIgnoreCase("yaml")), "The file should be YAML file"); } } private UpgradeRequest getUpgradeRequest(String releaseName, String packageName, String packageVersion, File propertiesFile, String propertiesToOverride, String timeoutExpression, boolean forceUpgrade, String appNames) throws IOException { UpgradeRequest upgradeRequest = new UpgradeRequest(); upgradeRequest.setForce(forceUpgrade); upgradeRequest.setAppNames(new ArrayList<>(StringUtils.commaDelimitedListToSet(appNames))); UpgradeProperties upgradeProperties = new UpgradeProperties(); upgradeProperties.setReleaseName(releaseName); String configValuesYML = YmlUtils.getYamlConfigValues(propertiesFile, propertiesToOverride); if (StringUtils.hasText(configValuesYML)) { ConfigValues configValues = new ConfigValues(); configValues.setRaw(configValuesYML); upgradeProperties.setConfigValues(configValues); } upgradeRequest.setUpgradeProperties(upgradeProperties); PackageIdentifier packageIdentifier = new PackageIdentifier(); packageIdentifier.setPackageName(packageName); packageIdentifier.setPackageVersion(packageVersion); upgradeRequest.setPackageIdentifier(packageIdentifier); upgradeRequest.setPackageIdentifier(packageIdentifier); Duration duration = DurationUtils.convert(timeoutExpression); if (duration != null) { upgradeRequest.setTimeout(duration.toMillis()); } return upgradeRequest; } @ShellMethod(key = "release rollback", value = "Rollback the release to a previous or a specific release.") public String rollback( @ShellOption(help = "the name of the release to rollback") String releaseName, @ShellOption(help = "the specific release version to rollback to. " + "Not specifying the value rolls back to the previous release.", defaultValue = "0") int releaseVersion, @ShellOption(help = "the expression for rollback timeout", defaultValue = ShellOption.NULL) String timeoutExpression) { RollbackRequest rollbackRequest = new RollbackRequest(releaseName, releaseVersion); Duration duration = DurationUtils.convert(timeoutExpression); if (duration != null) { rollbackRequest.setTimeout(duration.toMillis()); } Release release = skipperClient.rollback(rollbackRequest); StringBuilder sb = new StringBuilder(); sb.append(release.getName() + " has been rolled back. Now at version v" + release.getVersion() + "."); return sb.toString(); } @ShellMethod(key = "release delete", value = "Delete the release.") public String delete( @ShellOption(help = "the name of the release to delete") String releaseName, @ShellOption(help = "delete the release package", defaultValue = "false") boolean deletePackage) { this.skipperClient.delete(releaseName, deletePackage); StringBuilder sb = new StringBuilder(); sb.append(releaseName + " has been deleted."); return sb.toString(); } @ShellMethod(key = "release cancel", value = "Request a cancellation of current release operation.") public String cancel( @ShellOption(help = "the name of the release to cancel") String releaseName) { CancelResponse cancelResponse = this.skipperClient.cancel(new CancelRequest(releaseName)); if (cancelResponse != null && cancelResponse.getAccepted() != null && cancelResponse.getAccepted()) { return "Cancel request for release " + releaseName + " sent"; } throw new SkipperException("Cancel request for release " + releaseName + " not accepted"); } @ShellMethod(key = "release list", value = "List the latest version of releases with status of deployed or failed.") public Table list( @ShellOption(help = "wildcard expression to search by release name", defaultValue = ShellOption.NULL) String releaseName) { List<Release> releases = this.skipperClient.list(releaseName); LinkedHashMap<String, Object> headers = new LinkedHashMap<>(); headers.put("name", "Name"); headers.put("version", "Version"); headers.put("info.lastDeployed", "Last updated"); headers.put("info.status.statusCode", "Status"); headers.put("pkg.metadata.name", "Package Name"); headers.put("pkg.metadata.version", "Package Version"); headers.put("platformName", "Platform Name"); headers.put("info.status.platformStatusPrettyPrint", "Platform Status"); TableModel model = new BeanListTableModel<>(releases, headers); TableBuilder tableBuilder = new TableBuilder(model); TableUtils.applyStyle(tableBuilder); return tableBuilder.build(); } @ShellMethod(key = "release history", value = "List the history of versions for a given release.") public Table history( @ShellOption(help = "wildcard expression to search by release name") @NotNull String releaseName) { Collection<Release> releases; releases = this.skipperClient.history(releaseName); LinkedHashMap<String, Object> headers = new LinkedHashMap<>(); headers.put("version", "Version"); headers.put("info.lastDeployed", "Last updated"); headers.put("info.status.statusCode", "Status"); headers.put("pkg.metadata.name", "Package Name"); headers.put("pkg.metadata.version", "Package Version"); headers.put("info.description", "Description"); TableModel model = new BeanListTableModel<>(releases, headers); TableBuilder tableBuilder = new TableBuilder(model); TableUtils.applyStyle(tableBuilder); return tableBuilder.build(); } @ShellMethod(key = "release status", value = "Status for a last known release version.") public Object status( @ShellOption(help = "release name") @NotNull String releaseName, @ShellOption(help = "the specific release version.", defaultValue = ShellOption.NULL) Integer releaseVersion) { Info info; try { if (releaseVersion == null) { info = this.skipperClient.status(releaseName); } else { info = this.skipperClient.status(releaseName, releaseVersion); } } catch (ReleaseNotFoundException e) { return "Release with name '" + e.getReleaseName() + "' not found"; } Object[][] data = new Object[3][]; data[0] = new Object[] { "Last Deployed", info.getFirstDeployed() }; data[1] = new Object[] { "Status", info.getStatus().getStatusCode().toString() }; DeploymentState aggregateState = aggregateState(info.getStatus().getDeploymentStateList()); StringBuilder sb = new StringBuilder(); sb.append(DeploymentStateDisplay.fromKey(aggregateState.name()).getDescription() + "\n"); sb.append(info.getStatus().getPlatformStatusPrettyPrint()); data[2] = new Object[] { "Platform Status", sb.toString() }; TableModel model = new ArrayTableModel(data); TableBuilder tableBuilder = new TableBuilder(model); TableUtils.applyStyleNoHeader(tableBuilder); return tableBuilder.build(); } private void assertMaxIsIntegerAndGreaterThanZero(String max) { try { int maxInt = Integer.parseInt(max); Assert.isTrue(maxInt > 0, "The maximum number of revisions should be greater than zero."); } catch (NumberFormatException e) { throw new NumberFormatException("The maximum number of revisions is not an integer. Input string = " + max); } } }