JAVA调用harbor接口实现镜像tar包上传

前言

  使用harbor过程中,一直想使用harbor的api实现镜像的上传功能,但是实际上harbor是直接调用了docker registry的api,harbor层只是做了一个透传的功能,这个可以参考《harbor权威指南》这本书,参考官网接口以及网上大佬的思想,实现了一个Java版本,主要是实现了docker daemon上传的逻辑。

docker镜像tar包结构

实现上传首先需要将镜像的tar包解压,读取目录结构,一个典型的docker镜像包(使用docker save命令)结构如下:

.
├── 1ecf8bc84a7c3d60c0a6bbdd294f12a6b0e17a8269616fc9bdbedd926f74f50c
│   ├── VERSION
│   ├── json
│   └── layer.tar
├── 6f4ec1f3d7ea33646d491a705f94442f5a706e9ac9acbca22fa9b117094eb720.json
├── aaac5bde2c2bcb6cc28b1e6d3f29fe13efce6d6b669300cc2c6bfab96b942af4
│   ├── VERSION
│   ├── json
│   └── layer.tar
├── b63363f0d2ac8b3dca6f903bb5a7301bf497b1e5be8dc4f57a14e4dc649ef9bb
│   ├── VERSION
│   ├── json
│   └── layer.tar
├── c453224a84b8318b0a09a83052314dd876899d3a1a1cf2379e74bba410415059
│   ├── VERSION
│   ├── json
│   └── layer.tar
├── dd8ef1d42fbcccc87927eee94e57519c401b84437b98fcf35505fb6b7267a375
│   ├── VERSION
│   ├── json
│   └── layer.tar
├── manifest.json
└── repositories

清单文件manifet.json结构

[
    {
        "Config":"6f4ec1f3d7ea33646d491a705f94442f5a706e9ac9acbca22fa9b117094eb720.json",
        "RepoTags":[
            "alpine:filebeat-6.8.7-arm64"
        ],
        "Layers":[
            "aaac5bde2c2bcb6cc28b1e6d3f29fe13efce6d6b669300cc2c6bfab96b942af4/layer.tar",
            "dd8ef1d42fbcccc87927eee94e57519c401b84437b98fcf35505fb6b7267a375/layer.tar",
            "c453224a84b8318b0a09a83052314dd876899d3a1a1cf2379e74bba410415059/layer.tar",
            "b63363f0d2ac8b3dca6f903bb5a7301bf497b1e5be8dc4f57a14e4dc649ef9bb/layer.tar",
            "1ecf8bc84a7c3d60c0a6bbdd294f12a6b0e17a8269616fc9bdbedd926f74f50c/layer.tar"
        ]
    }
]

manifest.json 包含了对这个tar包的描述信息,比如image config文件地址,tags说明,镜像layer信息,在解析的时候也是根据这个文件去获取关联的文件

Doker Registry api

上传过程主要调用docker registry api

参考https://docs.docker.com/registry/spec/api/#pushing-an-image

上传流程

  1. 获取鉴权信息
  2. 检查layer.tar是否已经存在
  3. 上传layer.tar
  4. 上传image config
  5. 上传manifest(非包中的manifest.json而是Manifest struct)

Java实现

  1. 项目结构如下


    项目结构.png
  2. 核心实现类DockerImageUploadBiz.java

/**
 * @author Administrator
 */
@Service
@Slf4j
public class DockerImageUploadBiz {
    
    //远程仓库
    @Value("${docker.remote.repo}")
    private String targetRepoAddress;
    
    //本地解压路径
    @Value("${docker.upload.extractPath}")
    private String tarPath;
    
    //harbor用户名
    @Value("${docker.harbor.userName}")
    private String userName;
    
    //密码
    @Value("${docker.harbor.password}")
    private String password;
    
    /**
    * @param sourceTar 上传的镜像文件
    * @param project 项目
    */
    public void push(File sourceTar, String project) {
        if (!sourceTar.exists()) {
            log.warn("Error!file is not exist!path:{}", sourceTar);
            return;
        }
        try {
            String unTarPath = FileUtil.doUnArchiver(sourceTar, tarPath);
            String manifest = FileUtil.readJsonFile(unTarPath + File.separator + "manifest.json");
            JSONArray jsonArray = JSONObject.parseArray(manifest);
            if (Objects.isNull(jsonArray)) {
                log.warn("manifest convert error!path:{},content:{}", unTarPath + File.separator + "manifest.json", manifest);
                return;
            }
            for (Object arr : jsonArray) {
                JSONObject jsonObject = (JSONObject) arr;
                JSONArray repoTags = jsonObject.getJSONArray("RepoTags");
                for (Object repoTag : repoTags) {
                    String repo = repoTag.toString();
                    String substring = repo.substring(repo.lastIndexOf('/') + 1);
                    String[] split = substring.split(":");
                    String imageName = split[0];
                    String tag = split[1];
                    log.info("imageName:{},tag:{}", imageName, tag);
                    JSONArray layers = jsonObject.getJSONArray("Layers");
                    //1.上传layer
                    log.info("========================STEP:1/3===============================");
                    log.info("PUSHING LAYERS STARTING...");
                    List layerPathList = new ArrayList<>(layers.size());
                    int i = 1;
                    for (Object layer : layers) {
                        String layerPath = unTarPath + File.separator + layer.toString();
                        log.info("PUSHING LAYER:{}-{} ...", i, layers.size());
                        layerPathList.add(layerPath);
                        pushLayer(project, layerPath, imageName);
                        i++;
                    }
                    log.info("PUSHING LAYERS ENDED...");

                    log.info("========================STEP:2/3===============================");
                    //2.上传config
                    log.info("PUSHING CONFIG STARTING...");
                    String config = jsonObject.getString("Config");
                    String configPath = unTarPath + File.separator + config;
                    pushingConfig(project, configPath, imageName);
                    log.info("PUSHING CONFIG ENDED...");

                    log.info("========================STEP:3/3===============================");
                    //3.上传manifest
                    log.info("PUSHING MANIFEST STARTING...");
                    pushingManifest(project, layerPathList, configPath, imageName, tag);
                    log.info("PUSHING MANIFEST ENDED...");
                    log.info("PUSHING {} COMPLETED!", repo);
                }
            }

        } catch (Exception e) {
            log.error("", e);
        }
    }

    /**
     * 上传镜像层
     *
     * @param layerPath 层路径
     * @param imageName 镜像名称
     */
    private void pushLayer(String project, String layerPath, String imageName) throws Exception {
        File layerFile = new File(layerPath);
        boolean layerExist = checkLayerExist(project, layerFile, imageName);
        if (layerExist) {
            log.info("LAYER ALREADY EXISTS! LAYER PATH:{}", layerPath);
            return;
        }
        String location = startingPush(project, imageName);
        chunkPush(layerFile, location);
//        monolithicPush(layerFile,location);
    }

    /**
     * 判断层是否存在
     *
     * @param layer     层
     * @param imageName 镜像名称
     * @return true:存在,false:不存在
     */
    private boolean checkLayerExist(String project, File layer, String imageName) throws Exception {
        String hash256 = FileUtil.hash256(layer);
        String url = String
                .format("%s/v2/%s/blobs/%s", targetRepoAddress, project + "/" + imageName, "sha256:" + hash256);
        Response response = OkHttpClientUtil.headOkHttp(url);
        return response.code() == HttpStatus.OK.value();
    }

    /**
     * 开始上传
     *
     * @param imageName 镜像名称
     */
    private String startingPush(String project, String imageName) throws IOException {
        String url = String.format("%s/v2/%s/blobs/uploads/", targetRepoAddress, project + "/" + imageName);
        Response response = OkHttpClientUtil.postOkHttp(url, RequestBody.create(null, ""));
        if (response.code() == HttpStatus.ACCEPTED.value()) {
            return response.header("location");
        }
        return "";
    }

    /**
     * 分块上传
     */
    private void chunkPush(File layerFile, String url) throws Exception {
        long length = layerFile.length();
        log.info("file size:{}", length);
        //10M
        int len = 1024 * 1024 * 5;
        byte[] chunk = new byte[len];
        int offset = 0;
        int index = 0;
        MessageDigest messageDigest = MessageDigest.getInstance("SHA-256");
        while (true) {
            byte[] blocks = FileUtil.getBlock(offset, layerFile, chunk.length);
            if (Objects.isNull(blocks)) {
                log.warn("File block is null!");
                break;
            }
            offset += blocks.length;
            messageDigest.update(blocks);
            log.info("pushing range:[{}-{}]... {}%", index, offset, String.format("%.2f", (float) offset / (float) length * 100));
            if (offset == length) {
                String hash256 = FileUtil.byte2Hex(messageDigest.digest());
                url = String.format("%s&digest=sha256:%s", url, hash256);
                Response response = OkHttpClientUtil.putOkHttp(url, index, offset, blocks);
                if (response.code() != HttpStatus.CREATED.value()) {
                    log.error("chunk push error!code:{},digest:{},{}", response.code(), hash256, response.body().string());
                    throw new RuntimeException("chunk push error");
                }
                response.close();
                break;
            } else {
                Response response = OkHttpClientUtil.patchOkHttp(url, index, offset, blocks);
                if (response.code() != HttpStatus.ACCEPTED.value()) {
                    log.error("patch error!code:{},response:{}", response.code(), response.body().string());
                    throw new RuntimeException("patch error!");
                }
                url = response.header("location");
            }
            index = offset;
        }
    }

    /**
     * 整块上传
     *
     * @param layer
     */
    private void monolithicPush(File layer, String url) throws Exception {
        byte[] contents = FileUtils.readFileToByteArray(layer);
        String hash256 = FileUtil.hash256(layer);
        url = url + "&digest=sha256:" + hash256;
        Response response = OkHttpClientUtil.putOkHttp(url, contents);
        if (response.code() != HttpStatus.CREATED.value()) {
            log.error("monolithicPush error!code:{},{}", response.code(), response.body().string());
            throw new RuntimeException("monolithicPush error!");
        }
    }

    /**
     * 上传config
     *
     * @param configPath 路径
     * @param imageName  镜像名称
     * @throws Exception 异常
     */
    private void pushingConfig(String project, String configPath, String imageName) throws Exception {
        File file = new File(configPath);
        if (checkLayerExist(project, file, imageName)) {
            log.warn("{} exists!", configPath);
            return;
        }
        log.info("start pushing config...");
        String url = startingPush(project, imageName);
        monolithicPush(file, url);
        log.info("config:{} upload success!", configPath);
    }

    /**
     * 上传manifest清单
     *
     * @param layerArrays
     * @param configPath
     * @param tag
     * @throws Exception
     */
    private void pushingManifest(String project, List layerArrays, String configPath, String imageName, String tag) throws Exception {
        ManifestV2 manifestV2 = new ManifestV2()
                .setMediaType("application/vnd.docker.distribution.manifest.v2+json")
                .setSchemaVersion(2);
        File configFile = new File(configPath);
        String hash256 = FileUtil.hash256(configFile);
        Config config = new Config()
                .setMediaType("application/vnd.docker.container.image.v1+json")
                .setDigest("sha256:" + hash256)
                .setSize((int) configFile.length());
        manifestV2.setConfig(config);
        List layers = layerArrays.stream()
                .map(layerPath -> {
                    File layerFile = new File(layerPath);
                    Layer layer = new Layer();
                    String hash2561 = FileUtil.hash256(layerFile);
                    layer.setDigest("sha256:" + hash2561);
                    layer.setMediaType("application/vnd.docker.image.rootfs.diff.tar");
                    layer.setSize((int) layerFile.length());
                    return layer;
                }).collect(Collectors.toList());
        manifestV2.setLayers(layers);
        String manifestStr = JSON.toJSONString(manifestV2);
//        System.out.println(manifestStr);
        String url = String.format("%s/v2/%s/manifests/%s", targetRepoAddress, project + "/" + imageName, tag);
        Response response = OkHttpClientUtil.putManifestOkHttp(url, manifestStr.getBytes(StandardCharsets.UTF_8));
        if (response.code() != HttpStatus.CREATED.value()) {
            log.error("upload manifest error!,code:{},response:{}", response.code(), response.body().string());
            return;
        }
//        response.close();
        log.info("manifest upload success!");
    }
}

  1. FileUtil.java实现了文件的解压以及获取文件sha256等方法
/**
 * @author Administrator
 */
public class FileUtil {

    /**
     * 解压tar
     *
     * @param sourceFile
     * @param destPath
     * @throws Exception
     */
    public static String doUnArchiver(File sourceFile, String destPath)
            throws Exception {
        byte[] buf = new byte[1024];
        FileInputStream fis = new FileInputStream(sourceFile);
        BufferedInputStream bis = new BufferedInputStream(fis);
        TarArchiveInputStream tais = new TarArchiveInputStream(bis);
        TarArchiveEntry tae = null;
        destPath = createTempDirIfNotExist(sourceFile.getName(), destPath);
        while ((tae = tais.getNextTarEntry()) != null) {
            File f = new File(destPath + "/" + tae.getName());
            if (tae.isDirectory()) {
                f.mkdirs();
            } else {
                /*
                 * 父目录不存在则创建
                 */
                File parent = f.getParentFile();
                if (!parent.exists()) {
                    parent.mkdirs();
                }

                FileOutputStream fos = new FileOutputStream(f);
                BufferedOutputStream bos = new BufferedOutputStream(fos);
                int len;
                while ((len = tais.read(buf)) != -1) {
                    bos.write(buf, 0, len);
                }
                bos.flush();
                bos.close();
            }
        }
        tais.close();
        return destPath;
    }

    /**
     * 创建临时目录
     *
     * @param pathName
     * @param basePath
     */
    private static synchronized String createTempDirIfNotExist(String pathName, String basePath) {
        String dir;
        if (pathName.contains(".")) {
            String[] split = pathName.split("\\.");
            dir = basePath + File.separator + split[0];
        } else {
            dir = basePath + File.separator + pathName;
        }
        File file = new File(dir);
        if (!file.exists()) {
            file.mkdirs();
        }
        return dir;
    }

    /**
     * 读取json文件
     *
     * @param fileName
     * @return
     */
    public static String readJsonFile(String fileName) {
        try {
            File jsonFile = new File(fileName);
            Reader reader = new InputStreamReader(new FileInputStream(jsonFile), StandardCharsets.UTF_8);
            return IOUtils.toString(reader);
        } catch (IOException e) {
            e.printStackTrace();
            return null;
        }
    }

    public static String hash256(File file) {

        try (InputStream fis = new FileInputStream(file)) {
            byte[] buffer = new byte[4096];
            MessageDigest md5 = MessageDigest.getInstance("SHA-256");
            for (int numRead = 0; (numRead = fis.read(buffer)) > 0; ) {
                md5.update(buffer, 0, numRead);
            }
            return byte2Hex(md5.digest());
        } catch (Exception e) {
            e.printStackTrace();
        }
        return "";
    }

    /**
     * 将byte转为16进制
     *
     * @param bytes 要转换的bytes
     * @return 16进制String
     */
    public static String byte2Hex(byte[] bytes) {
        StringBuilder stringBuffer = new StringBuilder();
        String temp;
        for (byte b : bytes) {
            temp = Integer.toHexString(b & 0xFF);
            if (temp.length() == 1) {
                // 1得到一位的进行补0操作
                stringBuffer.append("0");
            }
            stringBuffer.append(temp);
        }
        return stringBuffer.toString();
    }

    /**
     * 文件分块工具
     *
     * @param offset    起始偏移位置
     * @param file      文件
     * @param blockSize 分块大小
     * @return 分块数据
     */

    public static byte[] getBlock(long offset, File file, int blockSize) {
        byte[] result = new byte[blockSize];
        try (RandomAccessFile accessFile = new RandomAccessFile(file, "r")) {
            accessFile.seek(offset);
            int readSize = accessFile.read(result);
            if (readSize == -1) {
                return null;
            } else if (readSize == blockSize) {
                return result;
            } else {
                byte[] tmpByte = new byte[readSize];
                System.arraycopy(result, 0, tmpByte, 0, readSize);
                return tmpByte;
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }
}

  1. OkHttpClientUtil.java实现了http相关方法
/**
 * @author Administrator
 */
@Slf4j
public class OkHttpClientUtil {

    private static final TrustAllManager trustAllManager = new TrustAllManager();

    private static final OkHttpClient okHttpClient = new OkHttpClient.Builder()
            .authenticator((route, response) -> {
                //用户名、密码
                String credential = Credentials.basic("admin", "password", StandardCharsets.UTF_8);
                return response.request().newBuilder()
                        .header("Authorization", credential)
                        .build();
            })
            .connectionPool(new ConnectionPool(10,1, TimeUnit.MINUTES))
            .sslSocketFactory(createTrustAllSSLFactory(trustAllManager),trustAllManager)
//            .proxy(new Proxy(Proxy.Type.HTTP,new InetSocketAddress("127.0.0.1",8888)))
            .writeTimeout(60,TimeUnit.MINUTES)
            .build();

    protected static SSLSocketFactory createTrustAllSSLFactory(TrustAllManager trustAllManager) {
        SSLSocketFactory ssfFactory = null;
        try {
            SSLContext sc = SSLContext.getInstance("TLS");
            sc.init(null, new TrustManager[]{trustAllManager}, new SecureRandom());
            ssfFactory = sc.getSocketFactory();
        } catch (Exception ignored) {
            ignored.printStackTrace();
        }

        return ssfFactory;
    }

    /**
     * get请求
     *
     * @param url
     */
    public static Response getOkHttp(String url) throws IOException {
        Request request = new Request.Builder()
                .url(url)
                .get()
                .build();
        return okHttpClient.newCall(request).execute();
    }

    /**
     * get请求
     *
     * @param url
     */
    public static Response headOkHttp(String url) throws IOException {
        Request request = new Request.Builder()
                .url(url)
                .head()
                .build();
        return okHttpClient.newCall(request).execute();
    }

    /**
     * post请求
     *
     * @param url
     * @param body
     */
    public static Response postOkHttp(String url, RequestBody body)
            throws IOException {
        Request request = new Request.Builder()
                .url(url)
                .post(body)
                .build();
        return okHttpClient.newCall(request).execute();
    }

    /**
     * patch方式
     *
     * @param url
     * @return
     * @throws IOException
     */
    public static Response patchOkHttp(String url, int index, int offset, byte[] buffer)
            throws IOException {
        MediaType mediaType = MediaType.parse("application/octet-stream");
        RequestBody body = RequestBody.create(mediaType, buffer);
        Request request = new Builder()
                .url(url)
                .patch(body)
                .header("Content-Type", "application/octet-stream")
                .header("Content-Length", String.valueOf(buffer.length))
                .header("Content-Range", String.format("%s-%s", index, offset))
                .build();
        return okHttpClient.newCall(request).execute();
    }

    /**
     * put方式
     *
     * @param url
     * @param index
     * @param offset
     * @param buffer
     * @return
     * @throws IOException
     */
    public static Response putOkHttp(String url, int index, int offset, byte[] buffer)
            throws IOException {
        MediaType mediaType = MediaType.parse("application/octet-stream");
        RequestBody body = RequestBody.create(mediaType, buffer);
        Request request = new Builder()
                .url(url)
                .put(body)
                .header("Content-Type", "application/octet-stream")
                .header("Content-Length", String.valueOf(buffer.length))
                .header("Content-Range", String.format("%s-%s", index, offset))
                .build();
        return okHttpClient.newCall(request).execute();
    }

    /**
     * put方式
     *
     * @param url
     * @param buffer
     * @return
     * @throws IOException
     */
    public static Response putOkHttp(String url, byte[] buffer)
            throws IOException {
        MediaType mediaType = MediaType.parse("application/octet-stream");
        RequestBody body = RequestBody.create(mediaType, buffer);
        Request request = new Builder()
                .url(url)
                .put(body)
                .header("Content-Type", "application/octet-stream")
                .header("Content-Length", String.valueOf(buffer.length))
                .build();
        return okHttpClient.newCall(request).execute();
    }

    /**
     * put方式
     *
     * @param url
     * @param buffers
     * @return
     * @throws IOException
     */
    public static Response putManifestOkHttp(String url, byte[] buffers)
            throws IOException {
        MediaType mediaType = MediaType.parse("application/vnd.docker.distribution.manifest.v2+json");
        RequestBody body = RequestBody.create(mediaType, buffers);
        Request request = new Builder()
                .url(url)
                .put(body)
                .header("Content-Type", "application/vnd.docker.distribution.manifest.v2+json")
                .build();
        return okHttpClient.newCall(request).execute();
    }
}

class TrustAllManager implements X509TrustManager{
    @Override
    public void checkClientTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException {

    }

    @Override
    public void checkServerTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException {

    }

    @Override
    public X509Certificate[] getAcceptedIssuers() {
        return new X509Certificate[0];
    }
}

  1. 清单文件结构ManifestV2.java
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
@Accessors(chain = true)
public class ManifestV2 {
    private Integer schemaVersion;
    private String mediaType;
    private Config config;
    private List layers;
}

@Data
@JsonIgnoreProperties(ignoreUnknown = true)
@Accessors(chain = true)
public class Config {
    private String mediaType;
    private Integer size;
    private String digest;
}

@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class Layer {
    private String mediaType;
    private Integer size;
    private String digest;
}
  1. controller层
@PostMapping("fileUpload")
    public String fileUpload2(@RequestParam("file") MultipartFile file, String project) throws IOException {
        long startTime = System.currentTimeMillis();
        String path = "C:\\test" + File.separator + file.getOriginalFilename();

        File newFile = new File(path);
        //通过CommonsMultipartFile的方法直接写文件(注意这个时候)
        file.transferTo(newFile);
        long endTime = System.currentTimeMillis();
        log.info("file:{} upload success,size:{} byte,spend time:{} ms", newFile.getName(), newFile.length(), endTime - startTime);
        log.info("start to push registry...");
        dockerImageUploadBiz.push(newFile, project);
        return "/success";
    }
  1. 调用方式--postman


    postman.png
  2. pom文件



  4.0.0

  org.example
  docker_images_push
  1.0-SNAPSHOT

  
    8
    8
  

  
    org.springframework.boot
    spring-boot-starter-parent
    2.1.6.RELEASE
    
  
  
    
      org.springframework.boot
      spring-boot-starter-web
    
    
      org.apache.commons
      commons-compress
      1.20
    
    
      com.alibaba
      fastjson
      1.2.54
    
    
      com.squareup.okhttp3
      okhttp
      3.8.1
    
    
      org.projectlombok
      lombok
    
    
      commons-io
      commons-io
      2.6
    
    
      commons-fileupload
      commons-fileupload
      1.4
    
  
  
    
      
        org.springframework.boot
        spring-boot-maven-plugin
        2.1.6.RELEASE
      
    
  


以上。
引用:

  1. https://jishuin.proginn.com/p/763bfbd29bd2
  2. https://docs.docker.com/registry/spec/api/#pushing-an-image
  3. https://github.com/silenceper/docker-tar-push

你可能感兴趣的:(JAVA调用harbor接口实现镜像tar包上传)