containerdでcreator-ee v0.5.0+のイメージがpullできずにハマった話

containerdでcreator-eeのv0.5.0+のイメージがpullに失敗するという事象が発生した。(KubernetesのコンテナランタイムがDockerからcontainerdに代わってから、初めてruntimeの挙動の差にハマった気がした)

containerdのコード読むよい機会だったので、コンテナイメージのpull周りを読みつつ、この件についてまとめてみた。今回の問題は既にコミュニティにより [containerd - #7094](https://github.com/containerd/containerd/pull/7094) で修正されている。

# 背景

ローカルのDocker for Desktopでは問題なく動いていたが、Kubernetes上で動いているDrone[^1]でcreator-ee[^2]のv0.6.0のイメージを使うと何のエラーも出ずに停止する問題があった。

```bash
> sudo docker pull quay.io/ansible/creator-ee:v0.5.0
...
ff3ce4e37203: Pull complete
be34f4f093fc: Pull complete
Digest: sha256:4d21770ba05e0373ef6506df50eaebcf1586c4dbfffb728f52dfba08eb0dc222
Status: Downloaded newer image for quay.io/ansible/creator-ee:v0.5.0
quay.io/ansible/creator-ee:v0.5.0

> echo $?
0
```


## 確認したこと

1.  creator-eeのバージョンを二分探索したところ、v0.5.0+のイメージが動かないことが分かった
2.  droneはKubernetes上で動いているので該当pipelineのPodを確認したが、該当コンテナのログはUI上と同じく何も出力されておらず、Exit(2)になっていた
3.  droneを経由させずにKubernetes上で該当イメージを立ち上げたところ、イメージのpull時に権限エラーでレイヤーの展開に失敗していることが分かった
    
    ```bash
    Events:
      Type     Reason     Age                   From               Message
      ----     ------     ----                  ----               -------
      Normal   Scheduled  30m                   default-scheduler  Successfully assigned default/ee50 to demo-ladicle-w-default-a9d89c28-htgqp
      Normal   Pulling    27m (x4 over 30m)     kubelet            Pulling image "quay.io/ansible/creator-ee:v0.5.0"
      Warning  Failed     27m (x4 over 29m)     kubelet            Error: ErrImagePull
      Warning  Failed     27m (x6 over 29m)     kubelet            Error: ImagePullBackOff
      Normal   BackOff    5m17s (x94 over 29m)  kubelet            Back-off pulling image "quay.io/ansible/creator-ee:v0.5.0"
      Warning  Failed     19s (x10 over 29m)    kubelet            Failed to pull image "quay.io/ansible/creator-ee:v0.5.0": rpc error: code = Unknown desc = failed to pull and unpack image "quay.io/ansible/creator-ee:v0.5.0": failed to extract layer sha256:140b571c235813a8eb5ba7f5f474f8852eef0218d4e5efe99801351d4c76aa6b: operation not permitted: unknown
    ```
4.  手元の環境でcontainerd(ctr)単体のイメージpullを試したが、同様のエラーで失敗するためkubernetes & 環境固有の問題ではないことが分かった
    
    ```bash
    $ sudo ctr image pull quay.io/ansible/creator-ee:v0.5.0
    ...
    layer-sha256:1cb646aa85f30788a51b53eda39160008325a7ae05b03050ba8b2726aee91549:    done           |++++++++++++++++++++++++++++++++++++++|
    layer-sha256:764ac4e7ad9bfa3a618b07693be79a7fd44b6a2fd35d497a7256458925219d33:    done           |++++++++++++++++++++++++++++++++++++++|
    elapsed: 56.0s                                                                    total:  540.1  (9.6 MiB/s)
    unpacking linux/amd64 sha256:4d21770ba05e0373ef6506df50eaebcf1586c4dbfffb728f52dfba08eb0dc222...
    INFO[0064] apply failure, attempting cleanup             error="failed to extract layer sha256:140b571c235813a8eb5ba7f5f474f8852eef0218d4e5efe99801351d4c76aa6b: operation not permitted: unknown" key="extract-661548272-faQw sha256:f5544d59a150fa51ce83f2fce9214393ff91001ccaaac6405d892a6dbb3ac4c7"
    ctr: failed to extract layer sha256:140b571c235813a8eb5ba7f5f474f8852eef0218d4e5efe99801351d4c76aa6b: operation not permitted: unknown
    ```
5.  containerdの特定バージョンの問題か確認するため、v1.6.4とv1.4.4で試したが同様のエラーが発生した
6.  Dockerでは動作し、containerdでは複数バージョンで失敗するためcontainerdに報告を上げた

# 原因と解決方法

ref: <https://github.com/containerd/containerd/pull/7094>
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/26683/ce166058-603a-802c-880a-c28034ac99b3.png)

原因は、tarからファイルを展開[^6]する際に、拡張属性を設定できないシンボリックリンクに対してsetxattrを実行し、そのままエラーを返したことだった。

ユーザ拡張属性はレギュラーファイルとディレクトリのみに設定できる[^3]。しかし、公式のcreator-eeのコンテナイメージでは、シンボリックリンクの `/etc/krb5.conf.d/crypto-policies` tarヘッダ[^4]にこの拡張属性情報が入っていた。containerdはこれを設定しようとして `EPERM` で失敗していた[^5]。

解決策として、上記PRではレギュラーファイルとディレクトリ以外に対しては `EPERM` を無視するように修正された。

https://github.com/containerd/containerd/blob/16992a4a2e5c86a91556f4c24e3fcded3a7cfd0d/archive/tar.go#L395-L401 より抜粋
```go
func createTarFile(ctx context.Context, path, extractDir string, hdr *tar.Header, reader io.Reader) error {
...
if strings.HasPrefix(key, paxSchilyXattr) {
	key = key[len(paxSchilyXattr):]
	if err := setxattr(path, key, value); err != nil { // ここでsymlinkに対するsetxattrがEPERMで失敗
		if errors.Is(err, syscall.ENOTSUP) {
			log.G(ctx).WithError(err).Warnf("ignored xattr %s in archive", key)
			continue
		}
		return err
	}
}
```


## NOTE: docker(moby)の場合

全てのストレージドライバが `xattrs` をサポートしているわけではない。ということで、 `EPERM` (& `ENOTSUP`)エラーは全て無視する仕様になっている。
 
https://github.com/moby/moby/blob/b8b1e16fed062eb351608a0d608b7e90c4e1ddf1/pkg/archive/archive.go#L782-L790 より抜粋
```go
var errors []string
for key, value := range hdr.Xattrs {
	if err := system.Lsetxattr(path, key, []byte(value), 0); err != nil {
		if err == syscall.ENOTSUP || err == syscall.EPERM {
			// We ignore errors here because not all graphdrivers support
			// xattrs *cough* old versions of AUFS *cough*. However only
			// ENOTSUP should be emitted in that case, otherwise we still
			// bail.
			// EPERM occurs if modifying xattrs is not allowed. This can
			// happen when running in userns with restrictions (ChromeOS).
			errors = append(errors, err.Error())
			continue
		}
		return err
	}
```

[^1]: [Drone CI – Automate Software Testing and Delivery](https://www.drone.io/)
[^2]: [ansible/creator-ee: Ansible Execution environment targeted for content creators.](https://github.com/ansible/creator-ee)
[^3]: [xattr(7) - Linux manual page](https://man7.org/linux/man-pages/man7/xattr.7.html) のUser extended attributes参照 (実装は[linux/fs/xattr.c#L129](https://github.com/torvalds/linux/blob/de5c208d533a46a074eb46ea17f672cc005a7269/fs/xattr.c#L129))
[^4]: <https://docs.studygolang.com/src/archive/tar/common.go#L191>
[^5]: そもそも何故、拡張属性がシンボリックリンクに付いているのかはまだ分かっていない。creator-eeが提供しているpodman/dockerでそれぞれ自前ビルドしたイメージをpullしてみたが、その場合は問題なく展開できた。creator-eeのビルドオプションの関係なんだろうか。
[^6]: (単純にtarを展開しているのではなく) OCIの [Applying ChangeSets](https://github.com/opencontainers/image-spec/blob/main/layer.md#applying-changesets) の仕様に基づき、ファイルやwhiteoutを展開先のディレクトリに適用している