• Docker Registry Implement

    对照https://docs.docker.com/registry/spec/api/,使用 Flask 做一下实现。但发现有两处和文档不太符合的地方。

    1. Chunked Upload

    文档中说,需要返回格式如下:

    202 Accepted
    Location: /v2/<name>/blobs/uploads/<uuid>
    Range: bytes=0-<offset>
    Content-Length: 0
    Docker-Upload-UUID: <uuid>
    

    这里显示 Range 的格式与Range - HTTP | MDN 中说的一致。

    但实际上,前面不能加 bytes=。否则 docker cli 会报错。

    上面的文档是说 Request 里面的 Range,但 Docker 文档这里用到的其实是 Response,不知道是不是这个区别,感觉前面加单位是合理的。

    2. Completed Upload

    传输完成一个 Layer之后,需要 Put 确认完成。 Server 应该返回如下信息。

    201 Created
    Location: /v2/<name>/blobs/<digest>
    Content-Length: 0
    Docker-Content-Digest: <digest>
    

    文档中说 The Location header will contain the registry URL to access the accepted layer file

    但实际上,docker cli 不会通过这个返回的 Location 来确认 Layer。而是通过 HEAD /v2/liujia/test/blobs/sha256:c74f8866df097496217c9f15efe8f8d3db05d19d678a02d01cc7eaed520bb136 HTTP/1.1 来确认的。就是说,不管Location 返回什么,都是通过之前的 digest 来做 HEAD 请求确认 Layer 信息的。

    3. Get Manifest

    返回的时候,需要解析 manifest 文件里面的 mediaType 作为 content-type 返回。

    代码

    模拟实现 registry 的 python+flask 代码如下。

    # -*- coding: utf-8 -*-
    
    from flask import Flask
    from flask import request
    from flask import make_response
    
    from uuid import uuid1
    import os
    import json
    from hashlib import sha256
    
    
    app = Flask(__name__)
    logger = app.logger
    
    registry_data_root = "./data/"
    
    
    @app.route("/v2/")
    def v2():
        return ""
    
    
    @app.route("/v2/<path:reponame>/manifests/<string:reference>", methods=["HEAD", "GET"])
    def get_manifest(reponame, reference):
        path = os.path.join(
            registry_data_root, "manifests", reponame, *reference.split(":")
        )
        resp = make_response()
        if not os.path.exists(path):
            resp.status = 404
            return resp
    
        resp.status = 200
        if request.method == "GET":
            data = open(path, "rb").read()
            resp.set_data(data)
            sha256_rst = sha256(data).hexdigest()
            resp.headers["Docker-Content-Digest"] = f"sha256:{sha256_rst}"
            resp.headers["Content-Length"] = str(len(data))
            content_type = json.loads(data)["mediaType"]
            resp.headers["Content-Type"] = content_type
        logger.info(resp.headers)
        return resp
    
    
    @app.route("/v2/<path:reponame>/blobs/<string:digest>", methods=["HEAD", "GET"])
    def get_layer(reponame, digest):
        logger.info(request.headers)
    
        path = os.path.join(registry_data_root, "blobs", reponame)
        dst = os.path.join(path, digest)
        resp = make_response()
        if not os.path.exists(dst):
            resp.status = 404
            return resp
    
        resp.status = 200
        if request.method == "GET":
            data = open(dst, "rb").read()
            resp.set_data(data)
            sha256_rst = sha256(data).hexdigest()
            resp.headers["Docker-Content-Digest"] = f"sha256:{sha256_rst}"
            resp.headers["Content-Length"] = str(len(data))
    
        logger.info(resp.headers)
        return resp
    
    
    @app.route("/v2/<path:reponame>/blobs/uploads/", methods=["POST"])
    def upload(reponame):
        logger.info(reponame)
        logger.info(request.headers)
        uuid = str(uuid1())
        resp = make_response()
        resp.headers["Location"] = f"/v2/{reponame}/blobs/uploads/{uuid}"
        resp.headers["Range"] = "bytes=0-0"
        resp.headers["Content-Length"] = "0"
        resp.headers["Docker-Upload-UUID"] = uuid
        resp.status = 202
        return resp
    
    
    @app.route(
        "/v2/<path:reponame>/blobs/uploads/<string:uuid>",
        methods=["PATCH"],
    )
    def patch_upload(reponame, uuid):
        """
        Chunked Upload
        """
        logger.info(request.url)
        logger.info(reponame)
        logger.info(uuid)
        logger.info(request.headers)
    
        r = [e for e in request.headers if e[0].upper() == "RANGE"]
        if r:
            start = int(r[0].split("-")[0])
        else:
            start = 0
    
        path = os.path.join(registry_data_root, "_upload", reponame)
        os.makedirs(path, exist_ok=True)
        f = open(os.path.join(path, uuid), "ab")
        f.seek(start, os.SEEK_SET)
        data = request.stream.read()
        f.write(data)
    
        resp = make_response()
        resp.headers["Location"] = f"/v2/{reponame}/blobs/uploads/{uuid}"
        resp.headers["Range"] = f"0-{len(data)}"
        resp.headers["Content-Length"] = "0"
        resp.headers["Docker-Upload-UUID"] = uuid
        logger.info(f"{resp.headers=}")
        resp.status = 202
        return resp
    
    
    @app.route(
        "/v2/<path:reponame>/blobs/uploads/<string:uuid>",
        methods=["PUT"],
    )
    def put_upload(reponame, uuid):
        """
        Completed Upload
        """
        logger.info(request.url)
        logger.info(reponame)
        logger.info(uuid)
        logger.info(request.headers)
        digest = request.args["digest"]
    
        dst_path = os.path.join(registry_data_root, "blobs", reponame)
        os.makedirs(dst_path, exist_ok=True)
        dst = os.path.join(dst_path, digest)
        src = os.path.join(registry_data_root, "_upload", reponame, uuid)
        logger.info(f"{src=} {dst=}")
        os.rename(src, dst)
    
        resp = make_response()
        resp.headers["Location"] = f"/v2/{reponame}/blobs/{digest}"
        logger.info(resp.headers)
        resp.status = 201
        return resp
    
    
    @app.route(
        "/v2/<path:reponame>/manifests/<string:reference>",
        methods=["PUT"],
    )
    def put_manifest(reponame, reference):
        """
        Completed Upload
        """
        logger.info(request.url)
        logger.info(reponame)
        logger.info(request.headers)
    
        path = os.path.join(registry_data_root, "manifests", reponame)
        os.makedirs(path, exist_ok=True)
        data = request.stream.read()
        with open(os.path.join(path, reference), "wb") as f:
            f.write(data)
    
        sha256_rst = sha256(data).hexdigest()
        path = os.path.join(path, "sha256")
        os.makedirs(path, exist_ok=True)
        with open(os.path.join(path, sha256_rst), "wb") as f:
            f.write(data)
    
        resp = make_response()
        resp.headers["Docker-Content-Digest"] = f"sha256:{sha256_rst}"
        resp.headers["Location"] = f"/v2/{reponame}/manifests/{reference}"
        logger.info(resp.headers)
        resp.status = 201
        return resp
    
  • Harbor DR 方案的一些尝试记录

    本篇博客已经不在适用,我们当前也不再使用这个方案。我们当前的架构一定程度是可以参考 我们的 Habror 灾备架构

    现状

    我们使用 Harbor v2.2.0 做 Docker 的镜像仓库。后端使用 Swift+Ceph。

    因为后端 Ceph 不能做两 IDC 的高可用方案。所以就搞了两套 Harbor,IDC A 和 IDC B 各一个。使用 Harbor 自己带的 Replication 功能做镜像的复制。

    这种方案有个问题:应用层面的数据不一致,用户的信息,项目的配置,包括 ID,Webhook 配置,用户权限等,都不能同步。

  • harbor push image 时偶尔出错的定位

    harbor 后端使用 Ceph 做存在,前端加 Swift 一层(应该是因为 registry 不直接支持 Ceph吧,这块还不是特别了解。)

    最近一段时间发现偶尔有 push image 出错。看 registry.log 是因为 PutContent 失败。 查 Ceph Access 日志,是 401 报错,接着一个 400 报错。

  • logrotate 笔记一二

    1

    logrotate 里面有如下配置:

    prerotate
       sh -c "[[ ! $1 =~ mongodb.log$ ]]"
    endscript
    

    运行时有如下报错:

    error: error running non-shared prerotate script for XXX

    这个其实不是错误,是说 prerotate 脚本 return code 不是0,也就是说会跳过这个日志文件的处理。

    2

    如果有两个 Pattern 有重合,比如说 /var/log/*.log, 和 /var/log/redis.log , Logrotate 不会处理两次,前面那个配置生效。后面的配置会被完全忽略掉。这里要小心。

    补充说明一下,如果一个文件在两个 pattern 里面都匹配到,第二个配置被完全忽略掉,而不仅仅是这个文件被忽略。

    大概翻看了一下 logrotate 的代码,逻辑应该是这样的:

    /var/log/*.log 称为一个 entry,每一个 entry,要遍历符合的所有日志,然后和前面的所有 entry 的所有日志对比,如果有一样的,就报 duplicate log entry 这个错误,并完全忽略这个 entry,也就是说,这个 entry 里面的所有日志都会跳过。

    我这个需求怎么办?

    我有这样一个需求,对 /var/lib/*.log 里面的日志做 rotate,但是

    1. mongodb.log 不需要做
    2. redis.log 不使用copytruncate

    想来想去,只能是“半手工”的来实现,如下:

    /var/lib/*.log {
        copytruncate
        prerotate
           sh -c "[[ $1 =~ redis.log$ ]]" && mv $1 $1-`date +%Y%m%d-%s` && exit 1
           sh -c "[[ ! ($1 =~ mongodb.log$ || $1 =~ redis.log$) ]]"
        endscript
    }
    
  • docker registry 更换后端存储后 Pull/Push 失败

    使用 harbor v1.10 做 docker hub。

    在一次测试过程,换了后端的 Swift 存储配置。 然后 Push 一个之前存在的 Image 时,显示 layers 都存在了,直接跳过,最后显示 Push 成功。但这显然是不可能的,因为后面的存储都是新的,不可能 layers 已经存在。 而且 Pull 的时候果然失败了。

  • Introducing the Journal

    Introducing the Journal Introducing the Journal

    现在觉得 Systemd 比之前的管理方式好。但对于使用 Journal 管理日志还有些不明白,之前的日志方式(Rsyslog 或者直接日志打印到某到文件文件)不行?使用 Journal 的必要性在哪里? 看到 Systemd 作者的这篇文件,翻译学习一下。

  • systemd for Developers I

    原文systemd for Developers I

    systemd not only brings improvements for administrators and users, it also brings a (small) number of new APIs with it. In this blog story (which might become the first of a series) I hope to shed some light on one of the most important new APIs in systemd: Socket Activation

    systemd 不仅为管理员和用户带来了改进,还带来了少量的新 api。在这个博客故事中(可能成为本系列文章的第一篇),我希望能对 systemd 中最重要的一个新 api 做一个分享:Socket Activation

    In the original blog story about systemd I tried to explain why socket activation is a wonderful technology to spawn services. Let’s reiterate the background here a bit.

    在关于 systemd 的最开始的博客故事中,我试图解释为什么 socket activation 是一种神奇的服务创建技术。让我们在这里再介绍一下背景。

  • Coc.nvim

    今天看了一下 coc.nvim 的配置和使用,挺不错的 VIM 插件,记录一下.

    我之前也使用到了这个插件,主要是用来做补全的。今天想使用 git blame 功能的时候,又看到了它。想搞搞明白,COC 到底是个什么东西.

    什么是 COC

    COC 就是一个 VIM 的插件啦。但它是一个管理插件的插件.

    和 Plug 不同的是,它只能管理 COC 系列的插件。严格来说,应该算是 COC 的插件吧,应该不能算是 VIM 插件, 我的感觉是这样。因为只能用 COC 来安装,而且是安装在 COC 自己的目录下面的.

    COC 系列的插件有不少,除了自动补全外,还有比如 coc-go ,coc-python,coc-git 啥的,可能可以取代 vimgo ,python-mode,vim-fugitive 等插件。作者也说了,用不用 COC 系列插件,还是要看你的需求是什么,看 COC 的插件是不是能满足你的需求,如果不行的话,还可以给作者提需求~~ 反正我是全部使用 COC 插件了,替换掉了 vim-go python-mode 等。使用 coc 插件好处多多,下面有提到。

    2021-09-05 补充:

    我现在 Python 和 Go 已经都使用 COC 支持下的能力了。不再使用 python-mode,vim-go 这两个插件。不过觉得好怀念,感谢他们。

    为什么要换成 COC 的支持呢?对我来说有两点,一个是快捷键的统一,比如说 rename,go to definition,go to implementation 等。另外一个是 LSP 的功能的确更强大一些吧。 这两者配合起来,在 VIM 里面的编程更方便了。

  • [译] Why Should We Separate A and AAAA DNS Queries

    看了朋友一篇文章https://leeweir.github.io/posts/wget-curl-does-not-resolve-domain-properly/, 直接拉到最后的根因, 看到里面写:

    curl 不指定协议访问的时候,为什么直接从(DNS)缓存里返回了结果而不做dns解析,还是要具体从libcurl的实现上去分析

    我的第一反应是不太可能, curl 的请求不会因为指定使用 ipv4 就做 DNS 解析, ipv6 不做. libcurl 不可能这么实现.

    然后去翻了 libcurl 的代码, 他是使用另外一个叫 ARES 的库去做的 DNS 解析. 在 ARES 中看到有解析 ipv6 的时候, 使用了多线程(细节有些记不清了). 然后搜索文章, 就看到了下面这篇文章: 为什么我们要分开请求 A 和 AAAA DNS记录?

  • 多线程一起读stdin

    一个进程里面开两个线程读取 stdin , 会是哪一个能读到呢? 来测下看.