实验不能停:我用java下载docker镜像文件还打成了tar包

210 阅读5分钟

在工作中,我遇到了一个需要从系统网页下载 Docker 镜像包的需求,目标是直接将结果下载为 tar 包。我们使用的镜像服务是 Harbor,它提供了丰富的系统管理 API,但遗憾的是,似乎没有找到用于下载或导出镜像的接口。这让我感到非常困惑。

经过反复思考,我决定从 Docker pull 协议入手。值得注意的是,Docker pull 是基于权限的 HTTP 协议,支持镜像各个层的下载,但它并不直接包含 manifest.json 文件。这意味着我需要自己编写代码来组装这些信息。在这个过程中,我还需要深入分析 tar 包的结构,以及 manifest.json 与各个层(layers)之间的关系。同时,我还必须理解加密验证的逻辑,以确保下载的镜像包是完整且有效的。

这个过程并不简单。首先,我需要了解 Docker 镜像的层级结构。每个 Docker 镜像由多个层组成,这些层通过 layers 字段在 manifest.json 中被描述。每一层都是只读的,可以被多个镜像共享,这也使得镜像的存储更加高效。其次,manifest.json 文件包含了关于镜像的元数据,比如标签、架构和层的哈希值等信息,这对下载和验证镜像的完整性至关重要。

此外,还要考虑到安全性和权限管理。Harbor 通常会对访问镜像的请求进行身份验证,确保只有授权用户可以下载镜像。因此,我需要处理相应的身份验证流程,获取所需的访问令牌,以便能够顺利执行下载操作。

目前已经通过这一系列的步骤,成功下载到所需的 Docker 镜像包,并确保它的完整性和可用性。这不仅是一个技术挑战,也让我对 Docker 镜像的内部机制有了更深刻的理解。

以下就是具体代码实现的过程,逻辑因为实验性质,所以并没有做任何抽象,只在Main类里直接过程形式书写,注释较少还请见谅。

java 依赖

dependencies {
    implementation 'org.json:json:20231013'
    implementation 'org.apache.commons:commons-compress:1.26.0'
}

java 代码


package org.example;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream;

import java.io.*;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.*;
import java.util.stream.Collectors;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.nio.charset.StandardCharsets;

public class Main {
    static String projectName = "project-name/";
    public static void main(String[] args) throws Exception {
        String registryUrl = "http://127.0.0.1:8008";
        String baseImageName = "alpine";
        String tag = "v1.0.0";
        String username = "admin";
        String password = "*********";

        try {
            String imageDir = downloadImage(projectName+baseImageName, registryUrl, tag, username, password);
            createTarArchive(imageDir, imageDir + ".tar");
        }catch (Exception e){
            e.printStackTrace();
        }
    }
    private static void createTarArchive(String sourceDir, String tarFile) throws IOException {
        try (TarArchiveOutputStream tarOs = new TarArchiveOutputStream(new FileOutputStream(tarFile))) {
            tarOs.setLongFileMode(TarArchiveOutputStream.LONGFILE_POSIX);
            Path sourcePath = Paths.get(sourceDir);

            Files.walk(sourcePath)
                    .filter(path -> !Files.isDirectory(path))
                    .forEach(path -> {
                        try {
                            String entryName = sourcePath.relativize(path).toString();
                            TarArchiveEntry entry = new TarArchiveEntry(path.toFile(), entryName);
                            tarOs.putArchiveEntry(entry);
                            Files.copy(path, tarOs);
                            tarOs.closeArchiveEntry();
                        } catch (IOException e) {
                            throw new RuntimeException("Failed to add file to tar: " + path, e);
                        }
                    });

            tarOs.finish();
        }
    }
    private static String downloadImage(String imageName, String registryUrl, String tag, String username, String password) throws Exception {
        // 创建目标目录结构
        String imageDir = imageName.replace("/", "_");
        Files.createDirectories(Paths.get(imageDir, "blobs", "sha256"));

        // 下载 manifest
        String manifestJson = downloadManifest(registryUrl, imageName, tag, username, password);
        Map manifest = new Gson().fromJson(manifestJson, Map.class);


        List<Map> layers = (List<Map>) manifest.get("layers");
        for (Map layer : layers) {
            String digest = (String) layer.get("digest");
            String sha256 = digest.replace("sha256:", "");
            downloadBlob(registryUrl, imageName, sha256, username, password,
                    Paths.get(imageDir, "blobs", "sha256", sha256));
        }
        String dockerManifest = createDockerManifest(imageName, tag, manifest);
        Files.write(Paths.get(imageDir, "manifest.json"), dockerManifest.getBytes());
        Map config = (Map) manifest.get("config");
        String configDigest = ((String) config.get("digest")).replace("sha256:", "");
        downloadBlob(registryUrl, imageName, configDigest, username, password,
                Paths.get(imageDir, "blobs", "sha256", configDigest));

        String ociLayout = "{"imageLayoutVersion": "1.0.0"}";
        Files.write(Paths.get(imageDir, "oci-layout"), ociLayout.getBytes());

        String indexJson = createIndexJson(manifest, imageName, tag);
        Files.write(Paths.get(imageDir, "index.json"), indexJson.getBytes());

        String repoJson = createRepositoriesJson(imageName, tag, manifest);
        Files.write(Paths.get(imageDir, "repositories"), repoJson.getBytes());
        return imageDir;
    }

    private static String downloadManifest(String registryUrl, String imageName, String tag,
                                           String username, String password) throws Exception {
        String auth = Base64.encodeBase64String((username + ":" + password).getBytes());
        String manifestUrl = registryUrl + "/v2/" + imageName + "/manifests/" + tag;

        URL url = new URL(manifestUrl);
        HttpURLConnection conn = (HttpURLConnection) url.openConnection();
        conn.setRequestMethod("GET");
        conn.setRequestProperty("Authorization", "Basic " + auth);
        conn.setRequestProperty("Accept", "application/vnd.docker.distribution.manifest.v2+json");

        try (BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream()))) {
            return reader.lines().collect(Collectors.joining("\n"));
        }
    }

    private static void downloadBlob(String registryUrl, String imageName, String digest,
                                     String username, String password, Path destPath) throws Exception {
        String auth = Base64.encodeBase64String((username + ":" + password).getBytes());
        String blobUrl = registryUrl + "/v2/" + imageName + "/blobs/sha256:" + digest;

        URL url = new URL(blobUrl);
        HttpURLConnection conn = (HttpURLConnection) url.openConnection();
        conn.setRequestMethod("GET");
        conn.setRequestProperty("Authorization", "Basic " + auth);

        try (InputStream in = conn.getInputStream();
             OutputStream out = Files.newOutputStream(destPath)) {
            byte[] buffer = new byte[8192];
            int bytesRead;
            while ((bytesRead = in.read(buffer)) != -1) {
                out.write(buffer, 0, bytesRead);
            }
        }
    }
    private static String createDockerManifest(String imageName, String tag, Map<String, Object> manifest) {
        JsonArray manifestArray = new JsonArray();
        JsonObject manifestEntry = new JsonObject();

        // 设置 Config
        Map<String, Object> config = (Map<String, Object>) manifest.get("config");
        String configDigest = (String) config.get("digest");
        manifestEntry.addProperty("Config", "blobs/sha256/" + configDigest.replace("sha256:", ""));

        // 设置 RepoTags
        JsonArray repoTags = new JsonArray();
        repoTags.add(imageName.replace(Main.projectName,"") + ":" + tag);
        manifestEntry.add("RepoTags", repoTags);

        // 设置 Layers
        JsonArray layerPaths = new JsonArray();
        List<Map<String, Object>> layers = (List<Map<String, Object>>) manifest.get("layers");
        for (Map<String, Object> layer : layers) {
            String digest = (String) layer.get("digest");
            layerPaths.add("blobs/sha256/" + digest.replace("sha256:", ""));
        }
        manifestEntry.add("Layers", layerPaths);

        // 设置 LayerSources
        JsonObject layerSources = new JsonObject();
        for (Map<String, Object> layer : layers) {
            String digest = (String) layer.get("digest");
            JsonObject layerInfo = new JsonObject();
            layerInfo.addProperty("mediaType", (String) layer.get("mediaType"));
            layerInfo.addProperty("size", ((Number) layer.get("size")).longValue());
            layerInfo.addProperty("digest", digest);
            layerSources.add(digest, layerInfo);
        }
        manifestEntry.add("LayerSources", layerSources);

        // 添加到数组中
        manifestArray.add(manifestEntry);

        return new GsonBuilder().setPrettyPrinting().create().toJson(manifestArray);
    }

    // 添加计算 SHA256 的辅助方法
    private static String calculateSha256(String input) {
        try {
            MessageDigest digest = MessageDigest.getInstance("SHA-256");
            byte[] hash = digest.digest(input.getBytes(StandardCharsets.UTF_8));
            StringBuilder hexString = new StringBuilder();
            for (byte b : hash) {
                String hex = Integer.toHexString(0xff & b);
                if (hex.length() == 1) hexString.append('0');
                hexString.append(hex);
            }
            return hexString.toString();
        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException("Failed to calculate SHA-256", e);
        }
    }
    private static String createIndexJson(Map<String, Object> manifest, String imageName, String tag) {
        JsonObject index = new JsonObject();

        // 设置基本属性
        index.addProperty("schemaVersion", 2);
        index.addProperty("mediaType", "application/vnd.oci.image.index.v1+json");

        // 创建 manifests 数组
        JsonArray manifests = new JsonArray();
        JsonObject manifestEntry = new JsonObject();

        // 设置 manifest 条目的属性
        manifestEntry.addProperty("mediaType", "application/vnd.oci.image.manifest.v1+json");

        // 计算 manifest 的 digest
        String manifestJson = new Gson().toJson(manifest);
        String manifestDigest = calculateSha256(manifestJson);
        manifestEntry.addProperty("digest", "sha256:" + manifestDigest);

        // 设置 size
        manifestEntry.addProperty("size", manifestJson.length());

        // 添加 annotations
        JsonObject annotations = new JsonObject();
        annotations.addProperty("io.containerd.image.name", imageName.replace(Main.projectName,"") + ":" + tag);
        annotations.addProperty("org.opencontainers.image.ref.name", tag);
        manifestEntry.add("annotations", annotations);

        manifests.add(manifestEntry);
        index.add("manifests", manifests);

        return new GsonBuilder().setPrettyPrinting().create().toJson(index);
    }


    private static String createRepositoriesJson(String imageName, String tag, Map manifest) {
        Map<String, Object> repositories = new HashMap<>();
        Map<String, Object> tags = new HashMap<>();

        Map config = (Map) manifest.get("config");
        String configDigest = (String) config.get("digest");

        tags.put(tag, configDigest);
        repositories.put(imageName, tags);

        return new GsonBuilder().setPrettyPrinting().create().toJson(repositories);
    }
}

运行结果

09:04:00: 正在执行 ':org.example.Main.main()'…

> Task :compileJava UP-TO-DATE
> Task :processResources NO-SOURCE
> Task :classes UP-TO-DATE
> Task :org.example.Main.main()

BUILD SUCCESSFUL in 612ms
2 actionable tasks: 1 executed, 1 up-to-date
09:04:01: 执行完成 ':org.example.Main.main()'。

拉取组装后的目录结构

与初始创建的目录结构有一些区别,但可以正常导出到docker里

image.png

导入本地 docker hub测试

image.png

PS:本文的内容是本人实验性的测试,毫无保留的分享给大家,但这不是标准答案,请慎用,我们共同研究