• harbor 中 oidc 认证的一些笔记

    使用版本是 harbor v2.2.0。本文只是记录了一些 OIDC 认证相关的东西。需要事先了解一些 SSO 知识和 Docker HTTP V2 API

    Harbor Core 组件分两个比较独立的功能,一个是提供 Token 服务,一个是反向代理后面的 Registry 。两者都有和 OIDC 打交道的地方。

    Harbor 中使用 OIDC 的地方,大的来说有两个。一个是 Web 页面登陆的时候,一个是 docker login/pull/push 时的身份认证。

    数据库里面和 OIDC 相关的一个重要表是 oidc_user,里面有两个重要的列,一个 secret,也就是密码,另外一个是 token,用来做验证(比密码更多一层安全?)。

  • cgroup v2 学习笔记简记

    参考资料

    启用 cgroup-v2

    测试系统环境:

    # cat /etc/centos-release
    CentOS Linux release 7.6.1810 (Core)
    
    # systemctl --version
    systemd 219
    +PAM +AUDIT +SELINUX +IMA -APPARMOR +SMACK +SYSVINIT +UTMP +LIBCRYPTSETUP +GCRYPT +GNUTLS +ACL +XZ +LZ4 -SECCOMP +BLKID +ELFUTILS +KMOD +IDN
    
    # uname -a
    Linux VMS171583 5.10.56-trip20211224.el7.centos.x86_64 #1 SMP Fri Dec 24 02:11:17 EST 2021 x86_64 x86_64 x86_64 GNU/Linux
    

    如果控制器(cpu,memory 等)已经绑定在 cgroup v1,那就没办法再绑定到 cgroup v2 了。

    systemd 又是 pid 1 进程,所以第一步需要让 systemd 使用 cgroup v2。

    grubby --update-kernel=ALL --args=systemd.unified_cgroup_hierarchy=1
    

    使用上面命令添加内核启动参数,然后重启,可以让 systemd 使用 cgroup v2。

    但我的测试环境中,这个版本的 systemd 还不支持 cgroup v2,所以添加了这个参数也没用。https://github.com/systemd/systemd/issues/19760

    所以需要强制关闭 cgroup v1:

    grubby --update-kernel=ALL --args=cgroup_no_v1=all
    

    重启之后,mount | grep cgroup 可以看到 cgroup v1 的 mount 已经没有了。(可能 systemd 不能再对服务做资源控制了?未验证)

    mount cgroup v2

    # mkdir /tmp/abcd
    # mount -t cgroup2 nodev /tmp/abcd/
    # mount | grep cgroup2
    nodev on /tmp/abcd type cgroup2 (rw,relatime)
    

    成功 mount cgroup v2 了,接下来就能使用 v2 做资源控制了。

    开启 CPU 限制

    看一下 v2 里面有哪些可用的控制器

    # cat cgroup.controllers
    cpuset cpu io memory hugetlb pids
    

    创建一个 child cgroup

    # cd /tmp/abcd
    [root@VMS171583 abcd]# mkdir t
    

    开启 CPU 控制器

    cd /tmp/abcd
    
    [root@VMS171583 abcd]# ll t
    total 0
    -r--r--r-- 1 root root 0 Mar 22 15:10 cgroup.controllers
    -r--r--r-- 1 root root 0 Mar 22 15:10 cgroup.events
    -rw-r--r-- 1 root root 0 Mar 22 15:10 cgroup.freeze
    -rw-r--r-- 1 root root 0 Mar 22 15:10 cgroup.max.depth
    -rw-r--r-- 1 root root 0 Mar 22 15:10 cgroup.max.descendants
    -rw-r--r-- 1 root root 0 Mar 22 15:10 cgroup.procs
    -r--r--r-- 1 root root 0 Mar 22 15:10 cgroup.stat
    -rw-r--r-- 1 root root 0 Mar 22 15:10 cgroup.subtree_control
    -rw-r--r-- 1 root root 0 Mar 22 15:10 cgroup.threads
    -rw-r--r-- 1 root root 0 Mar 22 15:10 cgroup.type
    -r--r--r-- 1 root root 0 Mar 22 15:10 cpu.stat
    
    echo "+cpu" > cgroup.subtree_control
    
    [root@VMS171583 abcd]# ll t
    total 0
    -r--r--r-- 1 root root 0 Mar 22 15:10 cgroup.controllers
    -r--r--r-- 1 root root 0 Mar 22 15:10 cgroup.events
    -rw-r--r-- 1 root root 0 Mar 22 15:10 cgroup.freeze
    -rw-r--r-- 1 root root 0 Mar 22 15:10 cgroup.max.depth
    -rw-r--r-- 1 root root 0 Mar 22 15:10 cgroup.max.descendants
    -rw-r--r-- 1 root root 0 Mar 22 15:10 cgroup.procs
    -r--r--r-- 1 root root 0 Mar 22 15:10 cgroup.stat
    -rw-r--r-- 1 root root 0 Mar 22 15:10 cgroup.subtree_control
    -rw-r--r-- 1 root root 0 Mar 22 15:10 cgroup.threads
    -rw-r--r-- 1 root root 0 Mar 22 15:10 cgroup.type
    -rw-r--r-- 1 root root 0 Mar 22 15:14 cpu.max
    -r--r--r-- 1 root root 0 Mar 22 15:10 cpu.stat
    -rw-r--r-- 1 root root 0 Mar 22 15:14 cpu.weight
    -rw-r--r-- 1 root root 0 Mar 22 15:14 cpu.weight.nice
    

    可以看到 CPU 的接口文件就自动创建好了。

    更改一下 CPU 资源控制的参数:

    cd /tmp/abcd/t
    echo 20000 100000 > cpu.max
    

    跑一个 Python 死循环脚本,可以看到 CPU 使用率100%。

    cd /tmp/abcd/t
    echo $pythonPID > cgroup.procs
    

    可以看到 python 进程的 CPU 使用率被限制到 20% 了。

  • Docker Push 503

    记一下刚刚发生的 Docker Push 一直重试,最后报错 received unexpected HTTP status: 503 Service Unavailable 的事。

  • dockerd 里面使用 lz4 解压缩测试小结

    线上 dockerd 版本: Docker version 19.03.12, build 48a66213fe

    实验使用的 dockerd 版本:v20.10.9

    目的

    为了加速容器的启动,docker pull 做为其中的一环,调研一下如何加速 docker pull。

    局域网环境问题下,docker pull 里面的解压的时间占了大头。Lz4 的解压速度比当前 Docker 默认的 gzip 要快不少,我们就在 Docker 里面实际测下看效果。

  • Harbor v2.2.0 中 Sweep 引发的两个问题

    在说这些问题之前,先说一下 Harbor 里面触发镜像复制任务(其他任务类似)的大概流程。

    1. Put Manifest 接口已经是 Docker Push 的最后一步了,里面会生成 Event,并做 Publish。至于需要 Publish 到哪里去,是https://github.com/goharbor/harbor/blob/release-2.4.0/src/pkg/notifier/notifier.go 里面注册的。

    2. controller/replication/controller:Start 里面创建一个 Execution (注意,这里会创建 Sweep Goroutine)

    3. 开一个 Goroutine ,创建 Tasks (一个 Execution 可能对应多个 Tasks)

    4. Task 里面提交 Job,POST /api/v1/jobs

    5. Core 组件里面收到这个请求,调用 LauchJob,会任务 Enqueue 到 Redis 里面注册的。

    以上都是 Core 里面做的。

    接下来 Jobservice 组件收到这个 Job,开始处理。并在任务成功或者失败时,回调一下配置在 Job 里面的 StatusHook。

  • 我们的 Habror 灾备架构

    介绍一下背景,我们使用 harbor 做镜像仓库服务,后端存储使用 Ceph。

    现在 1 需要做两机房的灾备, 2 Ceph 还没有两机房的集群能力。

  • 记一个 Harbor 中的小问题 -- get-manifest header-content-type 变化

    起因

    一个同事 使用 Ruby 调用 harbor GET /v2/<name>/manifests/<reference> 接口,开始的时候没有问题。

    后来,因为我们 harbor 架构的问题,对 harbor 代码做了一个小的改造。导致同事那边 Ruby 拿到的结果不认为是 Json,而是一个 String,需要再次 Json 解析一次。

    同事看起来,认为是 header 里面的 content-type 变了导致的。需要我们查明一下原因。

  • Docker Pull Image 404

    切换 DR 之后,有些镜像 Pull 失败。想对比一下两边的全量的 Tag,看一下有哪些缺失。结果发现全量 Tag 列表是一样的。

    然后就找了一下 Pull 404 的镜像。走 Registry:5000/v2/tag/list 的确返回列表里面有这个 Tag。那为啥docker pull 会报404呢?第一反应是哪里有缓存吧,翻看了一下代码,还真没有缓存。

    直接 curl /v2/{repo}manifest/{tag} ,如果走 Harbor,是404;走 Registry 就是200。看来问题出在 Harbor 这里。

    抓包看了一下,Harbor 居然把 Tag 转成 sha256 再去请求,到了 Registry 的请求是 /v2/{repo}manifest/{sha256},不太明白为啥要这样搞。

    这块代码是2年前的,可能和当时的 Registry 有关系。不过现在的确带来了副作用。

  • 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 配置,用户权限等,都不能同步。