Skip to content

Commit

Permalink
feat(backup): Complete (KMS-less) backups (#551)
Browse files Browse the repository at this point in the history
  • Loading branch information
lwander authored Jun 12, 2017
1 parent 218ee95 commit 94aeab7
Show file tree
Hide file tree
Showing 13 changed files with 326 additions and 59 deletions.
24 changes: 20 additions & 4 deletions docs/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
* [**hal admin publish version**](#hal-admin-publish-version)
* [**hal backup**](#hal-backup)
* [**hal backup create**](#hal-backup-create)
* [**hal backup restore**](#hal-backup-restore)
* [**hal config**](#hal-config)
* [**hal config ci**](#hal-config-ci)
* [**hal config ci jenkins**](#hal-config-ci-jenkins)
Expand Down Expand Up @@ -227,7 +228,7 @@ hal [parameters] [subcommands]
* `-h, --help`: (*Default*: `false`) Display help text about this command.
* `-l, --log`: Set the log level of the CLI.
* `-o, --output`: Format the CLIs output.
* `-q, --quiet`: Show no task information or messages. When disabled, ANSI formatting will be disabled too.
* `-q, --quiet`: Show no task information or messages. When set, ANSI formatting will be disabled, and all prompts will be accepted.

#### Parameters
* `--docs`: (*Default*: `false`) Print markdown docs for the hal CLI.
Expand Down Expand Up @@ -385,27 +386,42 @@ hal admin publish version [parameters]
---
## hal backup

This is used to periodically checkpoint your configured Spinnaker installation as well as allow you to remotely store all aspects of your configured Spinnaker installation.
This is used to periodically checkpoint your configured Spinnaker installation as well as allow you to store all aspects of your configured Spinnaker installation, to be picked up by an installation of Halyard on another machine.

#### Usage
```
hal backup [subcommands]
```

#### Subcommands
* `create`: Create a backup.
* `create`: Create a backup of Halyard's state.
* `restore`: Restore an existing backup.

---
## hal backup create

Create a backup.
This will create a tarball of your halconfig directory, being careful to rewrite file paths, so when the tarball is expanded by Halyard on another machine it will still be able to reference any files you have explicitly linked with your halconfig - e.g. --kubeconfig-file for Kubernetes, or --json-path for GCE.

#### Usage
```
hal backup create
```


---
## hal backup restore

Restore an existing backup. This backup does _not_ necessarily have to come from the machine it is being restored on - since all files referenced by your halconfig are included in the halconfig backup. As a result of this, keep in mind that after restoring a backup, all your required files are now in $halconfig/.backup/required-files.

#### Usage
```
hal backup restore [parameters]
```

#### Parameters
* `--backup-path`: (*Required*) This is the path to the .tar file created by running `hal backup create`.


---
## hal config

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,38 +17,173 @@

package com.netflix.spinnaker.halyard.backup.services.v1;

import com.netflix.spinnaker.halyard.backup.kms.v1.SecureStorage;
import com.netflix.spinnaker.halyard.config.config.v1.HalconfigDirectoryStructure;
import com.netflix.spinnaker.halyard.config.config.v1.HalconfigParser;
import com.netflix.spinnaker.halyard.config.model.v1.node.Halconfig;
import com.netflix.spinnaker.halyard.core.error.v1.HalException;
import com.netflix.spinnaker.halyard.core.problem.v1.Problem;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.compress.archivers.ArchiveEntry;
import org.apache.commons.compress.archivers.ArchiveException;
import org.apache.commons.compress.archivers.ArchiveStreamFactory;
import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
import org.apache.commons.compress.archivers.tar.TarArchiveInputStream;
import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream;
import org.apache.commons.io.IOUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.io.File;
import java.io.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.Date;
import java.util.Objects;

import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;

@Component
@Slf4j
public class BackupService {
@Autowired
HalconfigParser halconfigParser;

@Autowired(required = false)
SecureStorage secureStorage;

@Autowired
HalconfigDirectoryStructure directoryStructure;

public void create() {
if (secureStorage == null) {
// TODO(lwander): point to docs here.
throw new HalException(Problem.Severity.FATAL, "You must enable secure storage before proceeding.");
}
static String[] omitPaths = {"service-logs"};

public void restore(String backupTar) {
String halconfigDir = directoryStructure.getHalconfigDirectory();
untarHalconfig(halconfigDir, backupTar);

Halconfig halconfig = halconfigParser.getHalconfig();
halconfig.backupLocalFiles(directoryStructure.getBackupConfigDependenciesPath().toString());
halconfig.makeLocalFilesAbsolute(halconfigDir);
halconfigParser.saveConfig();
}

public String create() {
String halconfigDir = directoryStructure.getHalconfigDirectory();
halconfigParser.backupConfig();
Halconfig halconfig = halconfigParser.getHalconfig();
halconfig.backupLocalFiles(directoryStructure.getBackupConfigDependenciesPath().toString());
halconfig.makeLocalFilesRelative(halconfigDir);
halconfigParser.saveConfig();

String tarOutputName = String.format("halbackup-%s.tar", new Date()).replace(" ", "_").replace(":", "-");
String halconfigTar = Paths.get(System.getProperty("user.home"), tarOutputName).toString();
try {
tarHalconfig(halconfigDir, halconfigTar);
} catch (IOException e) {
throw new HalException(Problem.Severity.FATAL, "Unable to safely backup halconfig " + e.getMessage(), e);
} finally {
halconfigParser.switchToBackupConfig();
halconfigParser.getHalconfig();
halconfigParser.saveConfig();
halconfigParser.switchToPrimaryConfig();
}

return halconfigTar;
}


private void untarHalconfig(String halconfigDir, String halconfigTar) {
FileInputStream tarInput = null;
TarArchiveInputStream tarArchiveInputStream = null;

try {
tarInput = new FileInputStream(new File(halconfigTar));
tarArchiveInputStream = (TarArchiveInputStream) new ArchiveStreamFactory()
.createArchiveInputStream("tar", tarInput);

} catch (IOException | ArchiveException e) {
throw new HalException(Problem.Severity.FATAL, "Failed to open backup: " + e.getMessage(), e);
}

try {
ArchiveEntry archiveEntry = tarArchiveInputStream.getNextEntry();
while (archiveEntry != null) {
String entryName = archiveEntry.getName();
Path outputPath = Paths.get(halconfigDir, entryName);
File outputFile = outputPath.toFile();
if (!outputFile.getParentFile().exists()) {
outputFile.getParentFile().mkdirs();
}

if (archiveEntry.isDirectory()) {
outputFile.mkdir();
} else {
Files.copy(tarArchiveInputStream, outputPath, REPLACE_EXISTING);
}

secureStorage.backupFile("config", directoryStructure.getBackupConfigPath().toFile());
archiveEntry = tarArchiveInputStream.getNextEntry();
}
} catch (IOException e) {
throw new HalException(Problem.Severity.FATAL, "Failed to read archive entry: " + e.getMessage(), e);
}
}

private void tarHalconfig(String halconfigDir, String halconfigTar) throws IOException {
FileOutputStream tarOutput = null;
BufferedOutputStream bufferedTarOutput = null;
TarArchiveOutputStream tarArchiveOutputStream = null;
try {
tarOutput = new FileOutputStream(new File(halconfigTar));
bufferedTarOutput = new BufferedOutputStream(tarOutput);
tarArchiveOutputStream = new TarArchiveOutputStream(bufferedTarOutput);
TarArchiveOutputStream finalTarArchiveOutputStream = tarArchiveOutputStream;
Arrays.stream(new File(halconfigDir).listFiles())
.filter(Objects::nonNull)
.forEach(f -> addFileToTar(finalTarArchiveOutputStream, f.getAbsolutePath(), ""));
} catch (IOException e) {
throw new HalException(Problem.Severity.FATAL, "Failed to backup halconfig: " + e.getMessage(), e);
} finally {
if (tarArchiveOutputStream != null) {
tarArchiveOutputStream.finish();
tarArchiveOutputStream.close();
}

if (bufferedTarOutput != null) {
bufferedTarOutput.close();
}

if (tarOutput != null) {
tarOutput.close();
}
}
}

private void addFileToTar(TarArchiveOutputStream tarArchiveOutputStream, String path, String base) {
File file = new File(path);
String fileName = file.getName();

if (Arrays.stream(omitPaths).anyMatch(s -> s.equals(fileName))) {
return;
}

String tarEntryName = String.join("/", base, fileName);
TarArchiveEntry tarEntry = new TarArchiveEntry(file, tarEntryName);
try {
tarArchiveOutputStream.putArchiveEntry(tarEntry);
} catch (IOException e) {
throw new HalException(Problem.Severity.FATAL, "Unable to add archive entry: " + tarEntry + " " + e.getMessage(), e);
}

try {
if (file.isFile()) {
IOUtils.copy(new FileInputStream(file), tarArchiveOutputStream);
tarArchiveOutputStream.closeArchiveEntry();
} else if (file.isDirectory()) {
tarArchiveOutputStream.closeArchiveEntry();
Arrays.stream(file.listFiles())
.filter(Objects::nonNull)
.forEach(f -> addFileToTar(tarArchiveOutputStream, f.getAbsolutePath(), tarEntryName));
} else {
log.warn("Unknown file type: " + file + " - skipping addition to tar archive");
}
} catch (IOException e) {
throw new HalException(Problem.Severity.FATAL, "Unable to file " + file.getName() + " to archive entry: " + tarEntry + " " + e.getMessage(), e);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@

import com.beust.jcommander.Parameters;
import com.netflix.spinnaker.halyard.cli.command.v1.backup.CreateBackupCommand;
import com.netflix.spinnaker.halyard.cli.command.v1.backup.RestoreBackupCommand;
import lombok.AccessLevel;
import lombok.Getter;

Expand All @@ -33,11 +34,12 @@ public class BackupCommand extends NestableCommand {

@Getter(AccessLevel.PUBLIC)
private String longDescription = String.join(" ", "This is used to periodically checkpoint your configured Spinnaker installation as well as",
"allow you to remotely store all aspects of your configured Spinnaker installation.");
"allow you to store all aspects of your configured Spinnaker installation, to be picked up by an installation of Halyard on another machine.");


public BackupCommand() {
registerSubcommand(new CreateBackupCommand());
registerSubcommand(new RestoreBackupCommand());
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ public void setDebug(boolean debug) {
GlobalOptions.getGlobalOptions().setDebug(debug);
}

@Parameter(names = {"-q", "--quiet"}, description = "Show no task information or messages. When disabled, ANSI formatting will be disabled too.")
@Parameter(names = {"-q", "--quiet"}, description = "Show no task information or messages. When set, ANSI formatting will be disabled, and all prompts will be accepted.")
public void setQuiet(boolean quiet) {
GlobalOptions.getGlobalOptions().setQuiet(quiet);
GlobalOptions.getGlobalOptions().setColor(!quiet);
Expand Down Expand Up @@ -117,10 +117,10 @@ public void execute() {
AnsiUi.warning(((DeprecatedCommand) this).getDeprecatedWarning());
}

if (this instanceof ProtectedCommand) {
if (this instanceof ProtectedCommand && !GlobalOptions.getGlobalOptions().isQuiet()) {
String prompt = ((ProtectedCommand) this).getPrompt();
Console console = System.console();
String input = console.readLine(prompt+ " Do you want to continue? (Y/n) ");
String input = console.readLine(prompt + " Do you want to continue? (Y/n) ");
if (!input.equalsIgnoreCase("y")) {
AnsiUi.raw("Aborted.");
return;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import com.netflix.spinnaker.halyard.cli.command.v1.NestableCommand;
import com.netflix.spinnaker.halyard.cli.services.v1.Daemon;
import com.netflix.spinnaker.halyard.cli.services.v1.OperationHandler;
import com.netflix.spinnaker.halyard.cli.ui.v1.AnsiFormatUtils;
import lombok.AccessLevel;
import lombok.Getter;

Expand All @@ -31,14 +32,21 @@ public class CreateBackupCommand extends NestableCommand {
private String commandName = "create";

@Getter(AccessLevel.PUBLIC)
private String description = "Create a backup.";
private String shortDescription = "Create a backup of Halyard's state.";

@Getter(AccessLevel.PUBLIC)
private String longDescription = "This will create a tarball of your halconfig directory, being careful to rewrite "
+ "file paths, so when the tarball is expanded by Halyard on another machine it will still be able to reference "
+ "any files you have explicitly linked with your halconfig - e.g. --kubeconfig-file for Kubernetes, or --json-path "
+ "for GCE.";

@Override
protected void executeThis() {
new OperationHandler<Void>()
new OperationHandler<String>()
.setFailureMesssage("Failed to create a backup.")
.setSuccessMessage("Successfully created a backup.")
.setSuccessMessage("Successfully created a backup at location: ")
.setOperation(Daemon.createBackup())
.setFormat(AnsiFormatUtils.Format.STRING)
.get();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/*
* Copyright 2017 Google, 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.netflix.spinnaker.halyard.cli.command.v1.backup;

import com.beust.jcommander.Parameter;
import com.beust.jcommander.Parameters;
import com.netflix.spinnaker.halyard.cli.command.v1.NestableCommand;
import com.netflix.spinnaker.halyard.cli.command.v1.ProtectedCommand;
import com.netflix.spinnaker.halyard.cli.command.v1.converter.PathExpandingConverter;
import com.netflix.spinnaker.halyard.cli.services.v1.Daemon;
import com.netflix.spinnaker.halyard.cli.services.v1.OperationHandler;
import lombok.AccessLevel;
import lombok.Getter;

@Parameters(separators = "=")
public class RestoreBackupCommand extends NestableCommand implements ProtectedCommand {
@Getter(AccessLevel.PUBLIC)
private String commandName = "restore";

@Getter(AccessLevel.PUBLIC)
private String shortDescription = "Restore an existing backup.";

@Getter(AccessLevel.PUBLIC)
private String longDescription = "Restore an existing backup. This backup does _not_ necessarily have to come from "
+ "the machine it is being restored on - since all files referenced by your halconfig are included in the halconfig backup. "
+ "As a result of this, keep in mind that after restoring a backup, all your required files are now in $halconfig/.backup/required-files.";

@Parameter(
names = "--backup-path",
converter = PathExpandingConverter.class,
required = true,
description = "This is the path to the .tar file created by running `hal backup create`."
)
private String backupPath;

@Override
protected void executeThis() {
new OperationHandler<Void>()
.setFailureMesssage("Failed to restore the backup.")
.setSuccessMessage("Successfully restored your backup.")
.setOperation(Daemon.restoreBackup(backupPath))
.get();
}

@Override
public String getPrompt() {
return "This will override your entire current halconfig directory.";
}
}
Loading

0 comments on commit 94aeab7

Please sign in to comment.