/*
* Copyright 2016 Fizzed, Inc.
*
* 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
*
* http://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 com.fizzed.stork.deploy;
import com.fizzed.blaze.Contexts;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.Optional;
import java.util.stream.Collectors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class Deployer {
private static final Logger log = LoggerFactory.getLogger(Deployer.class);
public Deployer() {
// do nothing
}
public void verify(Assembly assembly, DeployOptions options, String uri) throws IOException, DeployerException {
try (Target target = Targets.connect(uri)) {
deploy(assembly, options, target, false);
}
}
public void verify(Assembly assembly, DeployOptions options, Target target) throws IOException, DeployerException {
deploy(assembly, options, target, false);
}
public void deploy(Assembly assembly, DeployOptions options, String uri) throws IOException, DeployerException {
try (Target target = Targets.connect(uri)) {
deploy(assembly, options, target, true);
}
}
public void deploy(Assembly assembly, DeployOptions options, Target target) throws IOException, DeployerException {
deploy(assembly, options, target, true);
}
private void deploy(Assembly assembly, DeployOptions options, Target target, boolean includeDeploy) throws IOException, DeployerException {
// use assembly + target to prepare for what to install
Deployment install = Deployments.install(assembly, target, options);
// existing deployment
ExistingDeployment existing = Deployments.existing(install, target);
log.info("");
logAssembly(assembly);
log.info("");
logTarget(target);
log.info("");
logExistingDeployment(existing);
log.info("");
logInstallDeployment(install, existing);
log.info("");
/**
if (!options.getYes()) {
String answer = Contexts.prompt("Do you want to continue?");
}
*/
//
// verify install would succeed as much as we can
//
// default the initType to what was detected on the target
InitType initType = target.getInitType();
assembly.verify();
// if we have daemons do we have support for the target platform?
if (assembly.hasDaemons()) {
if (!assembly.hasDaemons(initType)) {
throw new DeployerException("Assembly has daemons but none matching target init type " + initType);
}
}
// verify target user exists
if (install.getUser().isPresent() && !target.hasUser(install.getUser().get())) {
throw new DeployerException(
"User '" + install.getUser().get() + "' does not exist on target. "
+ "Run something like 'sudo useradd -r " + install.getUser().get() + "' then re-run.");
}
// verify target group exists
if (install.getGroup().isPresent() && !target.hasGroup(install.getGroup().get())) {
throw new DeployerException(
"Group '" + install.getGroup().get() + "' does not exist on target. "
+ "Run something like 'sudo groupadd -r " + install.getGroup().get() + "' then re-run.");
}
// TODO: verify commands we need exist?
// tar if (.tar.gz), gunzip if (.zip)
// systemd daemons need their .service files modified to include adjusted
// paths, users, and groups
// TODO: perhaps this is a good "hook" for other last minute customizations
// before the deploy is packaged backup and copied upstream
SystemdHelper.modifyForInstall(log, assembly, install);
if (!includeDeploy) {
return;
}
//
// do deploy
//
// repackage into a new archive using same format as source
Path storkPackageFile = assembly.getUnpackedDir().resolveSibling("stork-package." + assembly.getArchive().getFormat());
Archive.pack(assembly.getUnpackedDir(), storkPackageFile, assembly.getArchive().getFormat());
// create version directory (where we will install to)
target.createDirectories(true, install.getVersionDir());
// for either fresh or upgrade installs, we need a work dir
// by appending a uuid - each install gets their own unique work dir
// which prevents conflicts between different users doing deploys
String targetWorkDir = target.getTempDir() + "/stork-deploy." + install.getUuid();
// create clean work directory
target.remove(false, targetWorkDir);
target.createDirectories(false, targetWorkDir);
// always a .zip since we're re-packaging so that customizations can
// be made after exploding package, modifying it, then uploading
String targetArchiveFile = targetWorkDir + "/" + assembly.getArchive().getName();
// upload assembly
target.put(storkPackageFile, targetArchiveFile);
// unpack archive
target.unpack(targetArchiveFile, targetWorkDir);
if (existing.isFresh()) {
// copy all known files to versioned dir
Files.list(assembly.getUnpackedDir()).forEach((file) -> {
target.copyFiles(true,
targetWorkDir + "/" + assembly.getUnpackedDir().getFileName() + "/" + file.getFileName(),
install.getVersionDir() + "/");
});
} else {
// stop daemons
if (assembly.hasDaemons()) {
for (Daemon daemon : assembly.getDaemons(initType)) {
target.stopDaemon(daemon);
}
}
// copy dirs overwritten on upgrades
// from uploaded assembly -> versioned dir
Arrays.asList("bin", "lib", "share").stream().forEach((dir) -> {
target.copyFiles(true,
targetWorkDir + "/" + assembly.getUnpackedDir().getFileName() + "/" + dir,
install.getVersionDir() + "/");
});
// move dirs retained on upgrades
// from current dir -> versioned dir
Arrays.asList("conf", "log", "data", "run").stream().forEach((dir) -> {
target.moveFiles(true,
install.getCurrentDir() + "/" + dir,
install.getVersionDir() + "/");
});
}
//
// strict ownership & permissions
//
Optional<String> owner = install.getOwner();
if (owner.isPresent()) {
target.chown(true, false, owner.get(), install.getBaseDir());
target.chown(true, true, owner.get(), install.getVersionDir());
}
// NOTE: why 774?
// only user & group can get into the directory, while anyone will at
// least be able to know there is a directory with that name
if (assembly.hasDirectory("bin")) {
target.chmod(true, true, "774", install.getVersionDir() + "/bin");
}
if (assembly.hasDirectory("conf")) {
target.chmod(true, true, "774", install.getVersionDir() + "/conf");
}
if (assembly.hasDirectory("lib")) {
target.chmod(true, true, "774", install.getVersionDir() + "/lib");
}
if (assembly.hasDirectory("share/init.d")) {
target.chmod(true, true, "774", install.getVersionDir() + "/share/init.d");
}
//
// create symlink from current dir -> version dir
//
target.symlink(true, install.getVersionDir(), install.getCurrentDir());
if (assembly.hasDaemons()) {
//
// install daemons on both fresh/upgrade
//
for (Daemon daemon : assembly.getDaemons(initType)) {
target.installDaemon(install, daemon, true);
}
//
// start daemons (ask user though on fresh installs)
//
boolean tryDaemonStart = true;
// guard against new installs w/ conf directorys and failing to start
// simply due to conf files requiring an edit the first time
if (existing.isFresh()) {
if (assembly.hasDirectory("conf")) {
log.warn("Your app has a 'conf' directory and this is a fresh install");
log.warn("Its possible you need to edit your conf files before it can start");
while (!options.safeUnattended()) {
String answer = Contexts.prompt("Do you want to try and start your daemons now [yes/no]? ");
if (answer.equalsIgnoreCase("yes")) {
break;
} else if (answer.equalsIgnoreCase("no")) {
tryDaemonStart = false;
break;
}
}
}
}
if (tryDaemonStart) {
for (Daemon daemon : assembly.getDaemons(initType)) {
target.startDaemon(daemon);
}
}
}
// cleanup after ourselves
target.remove(false, targetWorkDir);
log.info("Deployed {} to {}", assembly, target);
}
private void logAssembly(Assembly assembly) {
log.info(" Assembly>");
log.info(" name: {}", assembly.getName());
log.info(" version: v{}", (assembly.isSnapshot() ? assembly.getVersion() + " (snapshot)" : assembly.getVersion()));
log.info(" archive: {}", assembly.getArchiveFile());
log.info(" unpacked: {}", assembly.getUnpackedDir());
if (assembly.hasDaemons()) {
log.info(" init types: {}", assembly.getDaemons().keySet().stream().map((i) -> i.name()).collect(Collectors.joining(", ")));
log.info(" daemons: {}", assembly.getDaemons().values().iterator().next().stream().map((d) -> d.getName()).collect(Collectors.joining(", ")));
} else {
log.info(" daemons: <none>");
}
}
private void logTarget(Target target) {
log.info(" Target>");
log.info(" uri: {}", target.getUri());
log.info(" system: {}", target.getUname());
log.info(" init type: {}", target.getInitType());
log.info(" impl: {}", target.getClass().getCanonicalName());
}
private void logExistingDeployment(ExistingDeployment existing) {
log.info(" Existing>");
if (existing.isFresh()) {
log.info(" no install found");
} else {
log.info(" base dir: {}", existing.getBaseDir());
log.info("current dir: {}", existing.getCurrentDir());
log.info("version dir: {}", existing.getVersionDir());
log.info("deployed at: {}", (existing.getDeployedAt() == null ? "<none>" : DeployHelper.toFriendlyDateTime(existing.getDeployedAt())));
int i = 0;
for (String versionDir : existing.getVersionDirs()) {
if (i == 0) {
log.info(" all vers: {}", versionDir);
} else {
log.info(" {}", versionDir);
}
i++;
}
}
}
private void logInstallDeployment(Deployment install, ExistingDeployment existing) {
log.info(" Install>");
log.info(" uuid: {}", install.getUuid());
log.info(" type: {}", (existing.isFresh() ? "fresh" : "upgrade"));
log.info(" base dir: {}", install.getBaseDir());
log.info("current dir: {}", install.getCurrentDir());
log.info("version dir: {}", install.getVersionDir());
log.info(" as user: {}", install.getUser().orElse("<null>"));
log.info(" as group: {}", install.getGroup().orElse("<null>"));
}
}