前言

  使用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信息,在解析的时候也是根据这个文件去获取关联的文件

上传流程

  1. 获取鉴权信息
  2. 检查layer.tar是否已经存在
  3. 上传layer.tar
  4. 上传image config
  5. 上传manifest(非包中的manifest.json而是Manifest struct)
  1. 核心实现类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<String> 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<String> 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<Layer> 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等方法
  2. /**
     * @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;
        }
    }
    

  3. OkHttpClientUtil.java实现了http相关方法
  4. /**
     * @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];
        }
    }
    

  5. 清单文件结构ManifestV2.java
  6. @Data
    @JsonIgnoreProperties(ignoreUnknown = true)
    @Accessors(chain = true)
    public class ManifestV2 {
        private Integer schemaVersion;
        private String mediaType;
        private Config config;
        private List<Layer> 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层
  2. @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";
        }
GitHub 加速计划 / ha / harbor
23.24 K
4.68 K
下载
Harbor 是一个开源的容器镜像仓库,用于存储和管理 Docker 镜像和其他容器镜像。 * 容器镜像仓库、存储和管理 Docker 镜像和其他容器镜像 * 有什么特点:支持多种镜像格式、易于使用、安全性和访问控制
最近提交(Master分支:3 个月前 )
969384cd enable job service to set MAX_JOB_DURATION_SECONDS in the jobservice container to customize max job duration fork gocraft/work to goharbor/work Signed-off-by: stonezdj <stone.zhang@broadcom.com> 13 小时前
66c98c81 Some developers are no longer working on Harbor. I'm removing them from assignees list. Signed-off-by: Daniel Jiang <daniel.jiang@broadcom.com> 4 天前
Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐