Добавил фичу сборки образа
This commit is contained in:
parent
27ffa6ef6d
commit
0fae695941
|
|
@ -110,70 +110,21 @@ public class App {
|
||||||
SystemLogger.warning("/!\\ We are going to make a backup. You have to stop all services in this container to prevent data corruption.", LOG_CONTEXT);
|
SystemLogger.warning("/!\\ We are going to make a backup. You have to stop all services in this container to prevent data corruption.", LOG_CONTEXT);
|
||||||
System.out.print("Type yes to continue or Ctrl+C to abort: ");
|
System.out.print("Type yes to continue or Ctrl+C to abort: ");
|
||||||
while (!"yes".equals(cin())) ;
|
while (!"yes".equals(cin())) ;
|
||||||
SystemLogger.message("Reading container filesystem metadata...", LOG_CONTEXT);
|
|
||||||
var filterContents = List.of("/sys/", "/proc/", "/dev/", "/mnt/", "/run/", "/tmp/");
|
|
||||||
var files = deviceContext.listFilesystem(container).stream().filter(f -> filterContents.contains(f.getPath()) || filterContents.stream().noneMatch(s -> f.getPath().startsWith(s))).toList();
|
|
||||||
SystemLogger.message("Found " + files.size() + " files", LOG_CONTEXT);
|
|
||||||
|
|
||||||
var sftp = deviceContext.getSftpClient();
|
|
||||||
final var localTmpFile = "./transfer.tmp";
|
|
||||||
|
|
||||||
try (var outputStream = new FileOutputStream(container.getGuid())) {
|
|
||||||
var count = 0;
|
|
||||||
|
|
||||||
try (var tar = new TarArchiveOutputStream(outputStream)) {
|
|
||||||
tar.setLongFileMode(TarArchiveOutputStream.LONGFILE_GNU);
|
|
||||||
var status = new DownloadStatus("Downloading", files.size());
|
|
||||||
for (var file : files) {
|
|
||||||
try {
|
|
||||||
if (file.isDirectory()) {
|
|
||||||
var entry = new TarArchiveEntry(file.getPath(), TarConstants.LF_DIR);
|
|
||||||
entry.setGroupId(file.getGroupId());
|
|
||||||
entry.setUserId(file.getUserId());
|
|
||||||
entry.setMode(file.getMode());
|
|
||||||
tar.putArchiveEntry(entry);
|
|
||||||
tar.closeArchiveEntry();
|
|
||||||
status.addCopiedFile(0);
|
|
||||||
} else if (file.isSymlink()) {
|
|
||||||
var entry = new TarArchiveEntry(file.getPath(), TarConstants.LF_SYMLINK);
|
|
||||||
entry.setLinkName(file.getTarget());
|
|
||||||
tar.putArchiveEntry(entry);
|
|
||||||
tar.closeArchiveEntry();
|
|
||||||
status.addCopiedFile(0);
|
|
||||||
} else {
|
|
||||||
var entry = new TarArchiveEntry(file.getPath());
|
|
||||||
entry.setGroupId(file.getGroupId());
|
|
||||||
entry.setUserId(file.getUserId());
|
|
||||||
entry.setMode(file.getMode());
|
|
||||||
var remotePath = "/" + container.getGuid() + file.getPath();
|
|
||||||
var size = sftp.size(remotePath);
|
|
||||||
|
|
||||||
|
|
||||||
entry.setSize(size);
|
var layerFile = new File(container.getGuid());
|
||||||
tar.putArchiveEntry(entry);
|
var outputFile = new File("backup_of_" + container.getComment() + ".tar");
|
||||||
|
|
||||||
|
if (!layerFile.exists()) {
|
||||||
try {
|
var files = findFiles(container);
|
||||||
sftp.get(remotePath, new SFTPFileStream(size, tar));
|
downloadFiles(container, files, layerFile);
|
||||||
}catch (AssertionError ae){
|
|
||||||
throw new RuntimeException(ae);
|
|
||||||
}
|
|
||||||
|
|
||||||
tar.closeArchiveEntry();
|
|
||||||
status.addCopiedFile(size);
|
|
||||||
}
|
|
||||||
} catch (IOException e) {
|
|
||||||
status.clear();
|
|
||||||
SystemLogger.error("Unable to copy file " + file.getPath(), LOG_CONTEXT, e);
|
|
||||||
pause();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
tar.finish();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
System.out.println(files.size());
|
SystemLogger.message("Building docker image " + outputFile.getName(), LOG_CONTEXT);
|
||||||
|
var builder = new DockerImageBuilder(container);
|
||||||
|
builder.build(layerFile, outputFile);
|
||||||
|
SystemLogger.message("Backup is done ", LOG_CONTEXT);
|
||||||
|
pause();
|
||||||
|
|
||||||
} finally {
|
} finally {
|
||||||
if (reader != null) {
|
if (reader != null) {
|
||||||
|
|
@ -189,6 +140,62 @@ public class App {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void downloadFiles(ContainerMetadata container, List<FileMetadata> files, File outputFile) throws IOException {
|
||||||
|
var sftp = deviceContext.getSftpClient();
|
||||||
|
try (var outputStream = new FileOutputStream(outputFile)) {
|
||||||
|
try (var tar = new TarArchiveOutputStream(outputStream)) {
|
||||||
|
tar.setLongFileMode(TarArchiveOutputStream.LONGFILE_GNU);
|
||||||
|
var status = new DownloadStatus("Downloading", files.size());
|
||||||
|
for (var file : files) {
|
||||||
|
try {
|
||||||
|
if (file.isDirectory()) {
|
||||||
|
var entry = new TarArchiveEntry(file.getPath(), TarConstants.LF_DIR);
|
||||||
|
entry.setGroupId(file.getGroupId());
|
||||||
|
entry.setUserId(file.getUserId());
|
||||||
|
entry.setMode(file.getMode());
|
||||||
|
tar.putArchiveEntry(entry);
|
||||||
|
tar.closeArchiveEntry();
|
||||||
|
status.addCopiedFile(0);
|
||||||
|
} else if (file.isSymlink()) {
|
||||||
|
var entry = new TarArchiveEntry(file.getPath(), TarConstants.LF_SYMLINK);
|
||||||
|
entry.setLinkName(file.getTarget());
|
||||||
|
tar.putArchiveEntry(entry);
|
||||||
|
tar.closeArchiveEntry();
|
||||||
|
status.addCopiedFile(0);
|
||||||
|
} else {
|
||||||
|
var entry = new TarArchiveEntry(file.getPath());
|
||||||
|
entry.setGroupId(file.getGroupId());
|
||||||
|
entry.setUserId(file.getUserId());
|
||||||
|
entry.setMode(file.getMode());
|
||||||
|
var remotePath = "/" + container.getGuid() + file.getPath();
|
||||||
|
var size = sftp.size(remotePath);
|
||||||
|
entry.setSize(size);
|
||||||
|
tar.putArchiveEntry(entry);
|
||||||
|
sftp.get(remotePath, new SFTPFileStream(size, tar));
|
||||||
|
tar.closeArchiveEntry();
|
||||||
|
status.addCopiedFile(size);
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
status.clear();
|
||||||
|
SystemLogger.error("Unable to copy file " + file.getPath(), LOG_CONTEXT, e);
|
||||||
|
pause();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tar.finish();
|
||||||
|
status.finish();
|
||||||
|
SystemLogger.message("Download of " + files.size() + " has been completed", LOG_CONTEXT);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<FileMetadata> findFiles(ContainerMetadata container) throws IOException, InterruptedException {
|
||||||
|
SystemLogger.message("Reading container filesystem metadata...", LOG_CONTEXT);
|
||||||
|
var filterContents = List.of("/sys/", "/proc/", "/dev/", "/mnt/", "/run/", "/tmp/");
|
||||||
|
var files = deviceContext.listFilesystem(container).stream().filter(f -> filterContents.contains(f.getPath()) || filterContents.stream().noneMatch(s -> f.getPath().startsWith(s))).toList();
|
||||||
|
SystemLogger.message("Found " + files.size() + " files", LOG_CONTEXT);
|
||||||
|
return files;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// private void openSSH() throws TransportException, ConnectionException {
|
// private void openSSH() throws TransportException, ConnectionException {
|
||||||
// session = client.startSession();
|
// session = client.startSession();
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ package ru.kirillius.mktbk;
|
||||||
|
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.NoArgsConstructor;
|
import lombok.NoArgsConstructor;
|
||||||
|
import ru.kirillius.json.JSONProperty;
|
||||||
import ru.kirillius.json.JSONSerializable;
|
import ru.kirillius.json.JSONSerializable;
|
||||||
|
|
||||||
import java.lang.annotation.ElementType;
|
import java.lang.annotation.ElementType;
|
||||||
|
|
@ -72,32 +73,52 @@ public class ContainerMetadata {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@JSONProperty
|
||||||
@FieldAlias(".id")
|
@FieldAlias(".id")
|
||||||
private String id;
|
private String id;
|
||||||
|
@JSONProperty
|
||||||
@FieldAlias("name")
|
@FieldAlias("name")
|
||||||
private String guid;
|
private String guid;
|
||||||
|
@JSONProperty
|
||||||
private String tag;
|
private String tag;
|
||||||
|
@JSONProperty
|
||||||
private String comment;
|
private String comment;
|
||||||
|
@JSONProperty
|
||||||
private String os;
|
private String os;
|
||||||
|
@JSONProperty
|
||||||
private String arch;
|
private String arch;
|
||||||
@FieldAlias("interface")
|
@FieldAlias("interface")
|
||||||
|
@JSONProperty
|
||||||
private String ifname;
|
private String ifname;
|
||||||
|
@JSONProperty
|
||||||
private String entrypoint;
|
private String entrypoint;
|
||||||
|
@JSONProperty
|
||||||
private String mounts;
|
private String mounts;
|
||||||
|
@JSONProperty
|
||||||
private String dns;
|
private String dns;
|
||||||
|
@JSONProperty
|
||||||
private String hostname;
|
private String hostname;
|
||||||
|
@JSONProperty
|
||||||
private String workdir;
|
private String workdir;
|
||||||
|
@JSONProperty
|
||||||
@FieldAlias("start-on-boot")
|
@FieldAlias("start-on-boot")
|
||||||
private String autostart;
|
private String autostart;
|
||||||
|
@JSONProperty
|
||||||
@FieldAlias("domain-name")
|
@FieldAlias("domain-name")
|
||||||
private String domainname;
|
private String domainname;
|
||||||
|
@JSONProperty
|
||||||
private String envlist;
|
private String envlist;
|
||||||
|
@JSONProperty
|
||||||
private String logging;
|
private String logging;
|
||||||
|
@JSONProperty
|
||||||
@FieldAlias("root-dir")
|
@FieldAlias("root-dir")
|
||||||
private String rootdir;
|
private String rootdir;
|
||||||
|
@JSONProperty
|
||||||
@FieldAlias("stop-signal")
|
@FieldAlias("stop-signal")
|
||||||
private String stopsignal;
|
private String stopsignal;
|
||||||
|
@JSONProperty
|
||||||
private String user;
|
private String user;
|
||||||
|
@JSONProperty
|
||||||
private String status;
|
private String status;
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,150 @@
|
||||||
|
package ru.kirillius.mktbk;
|
||||||
|
|
||||||
|
import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
|
||||||
|
import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream;
|
||||||
|
import org.apache.commons.compress.archivers.tar.TarConstants;
|
||||||
|
import org.json.JSONArray;
|
||||||
|
import org.json.JSONObject;
|
||||||
|
import org.json.JSONTokener;
|
||||||
|
import ru.kirillius.json.JSONUtility;
|
||||||
|
|
||||||
|
import java.io.*;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.security.MessageDigest;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
public class DockerImageBuilder {
|
||||||
|
private final MessageDigest digest;
|
||||||
|
private final ContainerMetadata containerMetadata;
|
||||||
|
|
||||||
|
|
||||||
|
private JSONObject loadJsonTemplate(String template) {
|
||||||
|
try (var resource = getClass().getClassLoader().getResourceAsStream("template/" + template + ".json")) {
|
||||||
|
if (resource == null) {
|
||||||
|
throw new FileNotFoundException(template + ".json");
|
||||||
|
}
|
||||||
|
return new JSONObject(new JSONTokener(resource));
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public DockerImageBuilder(ContainerMetadata containerMetadata) {
|
||||||
|
try {
|
||||||
|
digest = MessageDigest.getInstance("SHA-256");
|
||||||
|
} catch (NoSuchAlgorithmException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
this.containerMetadata = containerMetadata;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public void build(File layerFile, File outputFile) throws IOException {
|
||||||
|
try (var gzip = (new FileOutputStream(outputFile))) {
|
||||||
|
try (var tar = new TarArchiveOutputStream(gzip)) {
|
||||||
|
var layer = addLayer(layerFile, tar);
|
||||||
|
var repos = new JSONObject();
|
||||||
|
var imageInfo = containerMetadata.getTag().split(Pattern.quote(":"));
|
||||||
|
if (imageInfo.length > 1) {
|
||||||
|
var repo = new JSONObject();
|
||||||
|
repo.put(imageInfo[1], layer);
|
||||||
|
repos.put(imageInfo[0], repo);
|
||||||
|
}
|
||||||
|
createStringEntry("repositories", repos.toString(), tar);
|
||||||
|
var manifest = loadJsonTemplate("manifest");
|
||||||
|
var config = loadJsonTemplate("config");
|
||||||
|
|
||||||
|
config.put("os", containerMetadata.getOs());
|
||||||
|
config.put("architecture", containerMetadata.getArch());
|
||||||
|
config.getJSONObject("rootfs").getJSONArray("diff_ids").put("sha256:" + layer);
|
||||||
|
var configHash = hash(config.toString()) + ".json";
|
||||||
|
|
||||||
|
createStringEntry(configHash, config.toString(), tar);
|
||||||
|
|
||||||
|
manifest.getJSONArray("RepoTags").put(containerMetadata.getTag());
|
||||||
|
manifest.getJSONArray("Layers").put(layer + "/layer.tar");
|
||||||
|
manifest.put("Config", configHash);
|
||||||
|
|
||||||
|
|
||||||
|
var manifsetArray = new JSONArray();
|
||||||
|
manifsetArray.put(manifest);
|
||||||
|
createStringEntry("manifest.json", manifsetArray.toString(), tar);
|
||||||
|
|
||||||
|
tar.finish();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var metafile = new File(outputFile.getPath() + ".meta.json");
|
||||||
|
try (var writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(metafile)))) {
|
||||||
|
writer.write(JSONUtility.serializeStructure(containerMetadata).toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String addLayer(File layer, TarArchiveOutputStream tar) throws IOException {
|
||||||
|
|
||||||
|
var layerHash = hashFile(layer);
|
||||||
|
createFolder(layerHash, tar);
|
||||||
|
createFile(layer, layerHash + "/layer.tar", tar);
|
||||||
|
createStringEntry(layerHash + "/VERSION", "1.0", tar);
|
||||||
|
var json = loadJsonTemplate("layer");
|
||||||
|
json.put("id", layerHash);
|
||||||
|
json.put("os", containerMetadata.getOs());
|
||||||
|
createStringEntry(layerHash + "/json", json.toString(), tar);
|
||||||
|
return layerHash;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void createFile(File file, String path, TarArchiveOutputStream tar) throws IOException {
|
||||||
|
var entry = new TarArchiveEntry(path);
|
||||||
|
entry.setSize(file.length());
|
||||||
|
tar.putArchiveEntry(entry);
|
||||||
|
Files.copy(file.toPath(), tar);
|
||||||
|
tar.closeArchiveEntry();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void createFolder(String folder, TarArchiveOutputStream tar) throws IOException {
|
||||||
|
var entry = new TarArchiveEntry(folder, TarConstants.LF_DIR);
|
||||||
|
tar.putArchiveEntry(entry);
|
||||||
|
tar.closeArchiveEntry();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void createStringEntry(String path, String data, TarArchiveOutputStream tar) throws IOException {
|
||||||
|
var bytes = data.getBytes(StandardCharsets.UTF_8);
|
||||||
|
var entry = new TarArchiveEntry(path);
|
||||||
|
entry.setSize(bytes.length);
|
||||||
|
tar.putArchiveEntry(entry);
|
||||||
|
try (var inputStream = new ByteArrayInputStream(bytes)) {
|
||||||
|
inputStream.transferTo(tar);
|
||||||
|
}
|
||||||
|
tar.closeArchiveEntry();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String hash(String something) {
|
||||||
|
return hashToString(digest.digest(something.getBytes(StandardCharsets.UTF_8)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private String hashFile(File file) throws IOException {
|
||||||
|
|
||||||
|
byte[] buffer = new byte[8192];
|
||||||
|
int count;
|
||||||
|
try (var inputStream = new BufferedInputStream(new FileInputStream(file))) {
|
||||||
|
while ((count = inputStream.read(buffer)) > 0) {
|
||||||
|
digest.update(buffer, 0, count);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return hashToString(digest.digest());
|
||||||
|
}
|
||||||
|
|
||||||
|
private String hashToString(byte[] hash) {
|
||||||
|
var hexString = new StringBuilder(2 * hash.length);
|
||||||
|
for (byte b : hash) {
|
||||||
|
var hex = Integer.toHexString(0xff & b);
|
||||||
|
if (hex.length() == 1) {
|
||||||
|
hexString.append('0');
|
||||||
|
}
|
||||||
|
hexString.append(hex);
|
||||||
|
}
|
||||||
|
return hexString.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -69,4 +69,9 @@ public class DownloadStatus {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void finish(){
|
||||||
|
lastUpdate = 0;
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
{
|
||||||
|
"architecture": "amd64",
|
||||||
|
"author": "Container backup utility",
|
||||||
|
"config": {
|
||||||
|
"Env": [
|
||||||
|
],
|
||||||
|
"Cmd": [
|
||||||
|
],
|
||||||
|
"Labels": {
|
||||||
|
},
|
||||||
|
"ArgsEscaped": true,
|
||||||
|
"OnBuild": null
|
||||||
|
},
|
||||||
|
"created": "1970-01-01T03:00:00+03:00",
|
||||||
|
"history": [
|
||||||
|
],
|
||||||
|
"os": "linux",
|
||||||
|
"rootfs": {
|
||||||
|
"type": "layers",
|
||||||
|
"diff_ids": [
|
||||||
|
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
{
|
||||||
|
"id": "f407f64ec36e5c33a709060448ba6e4f955cd19e60543b84bf059ae6c5a09113",
|
||||||
|
"created": "1970-01-01T03:00:00+03:00",
|
||||||
|
"container_config": {
|
||||||
|
"Hostname": "",
|
||||||
|
"Domainname": "",
|
||||||
|
"User": "",
|
||||||
|
"AttachStdin": false,
|
||||||
|
"AttachStdout": false,
|
||||||
|
"AttachStderr": false,
|
||||||
|
"Tty": false,
|
||||||
|
"OpenStdin": false,
|
||||||
|
"StdinOnce": false,
|
||||||
|
"Env": null,
|
||||||
|
"Cmd": null,
|
||||||
|
"Image": "",
|
||||||
|
"Volumes": null,
|
||||||
|
"WorkingDir": "",
|
||||||
|
"Entrypoint": null,
|
||||||
|
"OnBuild": null,
|
||||||
|
"Labels": null
|
||||||
|
},
|
||||||
|
"os": "linux"
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"Config": "8550fec822c523351b294d5726da0dd8635879e5ae03c9abd3ae66f85f0ca97a.json",
|
||||||
|
"RepoTags": [
|
||||||
|
],
|
||||||
|
"Layers": [
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"opensuse-ssh-supervisord": {
|
||||||
|
"latest": "a7ab6e7cbb92df484a7302d3ecc084e3e5e45c1f7aac6a1e69110bf196f65b3a"
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue