-
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 里面触发镜像复制任务(其他任务类似)的大概流程。
-
Put Manifest 接口已经是 Docker Push 的最后一步了,里面会生成 Event,并做 Publish。至于需要 Publish 到哪里去,是https://github.com/goharbor/harbor/blob/release-2.4.0/src/pkg/notifier/notifier.go 里面注册的。
-
controller/replication/controller:Start 里面创建一个 Execution (注意,这里会创建 Sweep Goroutine)
-
开一个 Goroutine ,创建 Tasks (一个 Execution 可能对应多个 Tasks)
-
Task 里面提交 Job,
POST /api/v1/jobs
-
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 配置,用户权限等,都不能同步。