diff --git a/.woodpecker.star b/.woodpecker.star
index 9387377641..5ac1748751 100644
--- a/.woodpecker.star
+++ b/.woodpecker.star
@@ -11,6 +11,7 @@ ALPINE_GIT = "alpine/git:latest"
APACHE_TIKA = "apache/tika:2.8.0.0"
CHKO_DOCKER_PUSHRM = "chko/docker-pushrm:1"
COLLABORA_CODE = "collabora/code:24.04.5.1.1"
+OPEN_SEARCH = "opensearchproject/opensearch:2"
INBUCKET_INBUCKET = "inbucket/inbucket"
MINIO_MC = "minio/mc:RELEASE.2021-10-07T04-19-58Z"
OC_CI_ALPINE = "owncloudci/alpine:latest"
@@ -698,10 +699,36 @@ def testOpencloud(ctx):
],
"environment": CI_HTTP_PROXY_ENV,
},
+ {
+ "name": "open-search",
+ "image": OPEN_SEARCH,
+ "detach": True,
+ "environment": {
+ "discovery.type": "single-node",
+ "DISABLE_INSTALL_DEMO_CONFIG": True,
+ "DISABLE_SECURITY_PLUGIN": True,
+ },
+ "entrypoint": ["/usr/share/opensearch/opensearch-docker-entrypoint.sh", "opensearch"],
+ },
+ {
+ "name": "wait-for-open-search",
+ "image": OC_CI_ALPINE,
+ "commands": [
+ "bash -c '" +
+ "until curl -sS \"http://open-search:9200/_cat/health?h=status\" | grep \"green\\|yellow\"; do\n" +
+ " echo \"Waiting for http://open-search:9200 to be healthy...\"\n" +
+ " sleep 5\n" +
+ "done\n" +
+ "echo \"http://open-search:9200 healthy...\"\n" +
+ "'",
+ ],
+ },
{
"name": "test",
"image": OC_CI_GOLANG,
- "environment": CI_HTTP_PROXY_ENV,
+ "environment": {
+ "SEARCH_ENGINE_OPEN_SEARCH_CLIENT_ADDRESSES": "http://open-search:9200",
+ },
"commands": [
"mkdir -p cache/coverage",
"make test",
diff --git a/go.mod b/go.mod
index 67e1a97f63..7f6ec75c49 100644
--- a/go.mod
+++ b/go.mod
@@ -23,6 +23,7 @@ require (
github.com/ggwhite/go-masker v1.1.0
github.com/go-chi/chi/v5 v5.2.2
github.com/go-chi/render v1.0.3
+ github.com/go-jose/go-jose/v3 v3.0.4
github.com/go-ldap/ldap/v3 v3.4.11
github.com/go-ldap/ldif v0.0.0-20200320164324-fd88d9b715b3
github.com/go-micro/plugins/v4/client/grpc v1.2.1
@@ -65,6 +66,7 @@ require (
github.com/open-policy-agent/opa v1.6.0
github.com/opencloud-eu/libre-graph-api-go v1.0.8-0.20250724122329-41ba6b191e76
github.com/opencloud-eu/reva/v2 v2.36.1-0.20250827073033-ede93b948fc8
+ github.com/opensearch-project/opensearch-go/v4 v4.5.0
github.com/orcaman/concurrent-map v1.0.0
github.com/pkg/errors v0.9.1
github.com/pkg/xattr v0.4.12
@@ -79,9 +81,12 @@ require (
github.com/spf13/cobra v1.9.1
github.com/stretchr/testify v1.11.0
github.com/test-go/testify v1.1.4
+ github.com/testcontainers/testcontainers-go v0.38.0
+ github.com/testcontainers/testcontainers-go/modules/opensearch v0.38.0
github.com/theckman/yacspin v0.13.12
github.com/thejerf/suture/v4 v4.0.6
github.com/tidwall/gjson v1.18.0
+ github.com/tidwall/sjson v1.2.5
github.com/tus/tusd/v2 v2.8.0
github.com/unrolled/secure v1.16.0
github.com/urfave/cli/v2 v2.27.7
@@ -116,6 +121,7 @@ require (
require (
contrib.go.opencensus.io/exporter/prometheus v0.4.2 // indirect
filippo.io/edwards25519 v1.1.0 // indirect
+ github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect
github.com/BurntSushi/toml v1.5.0 // indirect
github.com/Masterminds/goutils v1.1.1 // indirect
@@ -152,14 +158,20 @@ require (
github.com/blevesearch/zapx/v16 v16.2.4 // indirect
github.com/bluele/gcache v0.0.2 // indirect
github.com/bombsimon/logrusr/v3 v3.1.0 // indirect
+ github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/cenkalti/backoff/v5 v5.0.2 // indirect
github.com/ceph/go-ceph v0.35.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/cevaris/ordered_map v0.0.0-20190319150403-3adeae072e73 // indirect
github.com/cloudflare/circl v1.6.1 // indirect
+ github.com/containerd/errdefs v1.0.0 // indirect
+ github.com/containerd/errdefs/pkg v0.3.0 // indirect
+ github.com/containerd/log v0.1.0 // indirect
+ github.com/containerd/platforms v1.0.0-rc.1 // indirect
github.com/coreos/go-semver v0.3.1 // indirect
github.com/coreos/go-systemd/v22 v22.5.0 // indirect
github.com/cornelk/hashmap v1.0.8 // indirect
+ github.com/cpuguy83/dockercfg v0.3.2 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect
github.com/crewjam/httperr v0.2.0 // indirect
github.com/crewjam/saml v0.4.14 // indirect
@@ -170,8 +182,13 @@ require (
github.com/dgraph-io/ristretto v0.2.0 // indirect
github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
+ github.com/distribution/reference v0.6.0 // indirect
github.com/dlclark/regexp2 v1.4.0 // indirect
+ github.com/docker/docker v28.2.2+incompatible // indirect
+ github.com/docker/go-connections v0.5.0 // indirect
+ github.com/docker/go-units v0.5.0 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
+ github.com/ebitengine/purego v0.8.4 // indirect
github.com/egirna/icap v0.0.0-20181108071049-d5ee18bd70bc // indirect
github.com/emirpasic/gods v1.18.1 // indirect
github.com/emvi/iso-639-1 v1.1.1 // indirect
@@ -186,7 +203,6 @@ require (
github.com/go-git/go-billy/v5 v5.6.2 // indirect
github.com/go-git/go-git/v5 v5.13.2 // indirect
github.com/go-ini/ini v1.67.0 // indirect
- github.com/go-jose/go-jose/v3 v3.0.4 // indirect
github.com/go-jose/go-jose/v4 v4.1.1 // indirect
github.com/go-kit/log v0.2.1 // indirect
github.com/go-logfmt/logfmt v0.5.1 // indirect
@@ -195,6 +211,7 @@ require (
github.com/go-micro/plugins/v4/events/natsjs v1.2.2 // indirect
github.com/go-micro/plugins/v4/store/nats-js v1.2.1 // indirect
github.com/go-micro/plugins/v4/store/redis v1.2.1 // indirect
+ github.com/go-ole/go-ole v1.2.6 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-redis/redis/v8 v8.11.5 // indirect
@@ -242,6 +259,8 @@ require (
github.com/libregraph/oidc-go v1.1.0 // indirect
github.com/longsleep/go-metrics v1.0.0 // indirect
github.com/longsleep/rndm v1.2.0 // indirect
+ github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
+ github.com/magiconair/properties v1.8.10 // indirect
github.com/mattermost/xml-roundtrip-validator v0.1.0 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
@@ -257,8 +276,16 @@ require (
github.com/minio/minio-go/v7 v7.0.95 // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect
+ github.com/moby/docker-image-spec v1.3.1 // indirect
+ github.com/moby/go-archive v0.1.0 // indirect
+ github.com/moby/patternmatcher v0.6.0 // indirect
+ github.com/moby/sys/sequential v0.6.0 // indirect
+ github.com/moby/sys/user v0.4.0 // indirect
+ github.com/moby/sys/userns v0.1.0 // indirect
+ github.com/moby/term v0.5.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
+ github.com/morikuni/aec v1.0.0 // indirect
github.com/mschoch/smat v0.2.0 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/nats-io/jwt/v2 v2.7.4 // indirect
@@ -267,6 +294,8 @@ require (
github.com/nxadm/tail v1.4.8 // indirect
github.com/olekukonko/errors v1.1.0 // indirect
github.com/olekukonko/ll v0.0.9 // indirect
+ github.com/opencontainers/go-digest v1.0.0 // indirect
+ github.com/opencontainers/image-spec v1.1.1 // indirect
github.com/opentracing/opentracing-go v1.2.0 // indirect
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c // indirect
github.com/pablodz/inotifywaitgo v0.0.9 // indirect
@@ -276,6 +305,7 @@ require (
github.com/pierrec/lz4/v4 v4.1.15 // indirect
github.com/pjbgf/sha1cd v0.3.2 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
+ github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
github.com/pquerna/cachecontrol v0.2.0 // indirect
github.com/prometheus/alertmanager v0.28.1 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
@@ -294,6 +324,7 @@ require (
github.com/sethvargo/go-diceware v0.5.0 // indirect
github.com/sethvargo/go-password v0.3.1 // indirect
github.com/shamaton/msgpack/v2 v2.2.3 // indirect
+ github.com/shirou/gopsutil/v4 v4.25.5 // indirect
github.com/shurcooL/httpfs v0.0.0-20230704072500-f1e31cf0ba5c // indirect
github.com/shurcooL/vfsgen v0.0.0-20230704071429-0000e147ea92 // indirect
github.com/skeema/knownhosts v1.3.0 // indirect
@@ -305,6 +336,8 @@ require (
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.1 // indirect
github.com/tinylib/msgp v1.3.0 // indirect
+ github.com/tklauser/go-sysconf v0.3.14 // indirect
+ github.com/tklauser/numcpus v0.8.0 // indirect
github.com/toorop/go-dkim v0.0.0-20201103131630-e1cd1a0a5208 // indirect
github.com/trustelem/zxcvbn v1.0.1 // indirect
github.com/vektah/gqlparser/v2 v2.5.28 // indirect
@@ -315,6 +348,7 @@ require (
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect
github.com/yashtewari/glob-intersection v0.2.0 // indirect
+ github.com/yusufpapurcu/wmi v1.2.4 // indirect
go.etcd.io/etcd/api/v3 v3.6.4 // indirect
go.etcd.io/etcd/client/pkg/v3 v3.6.4 // indirect
go.etcd.io/etcd/client/v3 v3.6.4 // indirect
diff --git a/go.sum b/go.sum
index 8df33a37b2..be84a718ef 100644
--- a/go.sum
+++ b/go.sum
@@ -43,7 +43,11 @@ filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/Acconut/go-httptest-recorder v1.0.0 h1:TAv2dfnqp/l+SUvIaMAUK4GeN4+wqb6KZsFFFTGhoJg=
github.com/Acconut/go-httptest-recorder v1.0.0/go.mod h1:CwQyhTH1kq/gLyWiRieo7c0uokpu3PXeyF/nZjUNtmM=
+github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk=
+github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8=
github.com/Azure/azure-sdk-for-go v32.4.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc=
+github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8=
+github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/Azure/go-autorest/autorest v0.1.0/go.mod h1:AKyIcETwSUFxIcs/Wnq/C+kwCtlEYGUVd7FPNb2slmg=
github.com/Azure/go-autorest/autorest v0.9.0/go.mod h1:xyHB1BMZT0cuDHU7I0+g046+BFDTQ8rEZB0s4Yfa6bI=
github.com/Azure/go-autorest/autorest/adal v0.1.0/go.mod h1:MeS4XhScH55IST095THyTxElntu7WqB7pNbZo8Q5G3E=
@@ -200,6 +204,8 @@ github.com/c-bata/go-prompt v0.2.5/go.mod h1:vFnjEGDIIA/Lib7giyE4E9c50Lvl8j0S+7F
github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4=
github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
github.com/cenkalti/backoff/v4 v4.1.0/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw=
+github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
+github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/cenkalti/backoff/v5 v5.0.2 h1:rIfFVxEf1QsI7E1ZHfp/B4DF/6QBAUhmgkxc0H7Zss8=
github.com/cenkalti/backoff/v5 v5.0.2/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
github.com/census-instrumentation/opencensus-proto v0.2.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
@@ -221,6 +227,14 @@ github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ
github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
github.com/cloudflare/cloudflare-go v0.14.0/go.mod h1:EnwdgGMaFOruiPZRFSgn+TsQ3hQ7C/YWzIGLeu5c304=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
+github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
+github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
+github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
+github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=
+github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
+github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
+github.com/containerd/platforms v1.0.0-rc.1 h1:83KIq4yy1erSRgOVHNk1HYdPvzdJ5CnsWaRoJX4C41E=
+github.com/containerd/platforms v1.0.0-rc.1/go.mod h1:J71L7B+aiM5SdIEqmd9wp6THLVRzJGXfNuWCZCllLA4=
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/go-oidc/v3 v3.15.0 h1:R6Oz8Z4bqWR7VFQ+sPSvZPQv4x8M+sJkDO5ojgwlyAg=
@@ -235,11 +249,15 @@ github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfc
github.com/cornelk/hashmap v1.0.8 h1:nv0AWgw02n+iDcawr5It4CjQIAcdMMKRrs10HOJYlrc=
github.com/cornelk/hashmap v1.0.8/go.mod h1:RfZb7JO3RviW/rT6emczVuC/oxpdz4UsSB2LJSclR1k=
github.com/cpu/goacmedns v0.1.1/go.mod h1:MuaouqEhPAHxsbqjgnck5zeghuwBP1dLnPoobeGqugQ=
+github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA=
+github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo=
github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
+github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=
+github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
github.com/crewjam/httperr v0.2.0 h1:b2BfXR8U3AlIHwNeFFvZ+BV1LFvKLlzMjzaTnZMybNo=
github.com/crewjam/httperr v0.2.0/go.mod h1:Jlz+Sg/XqBQhyMjdDiC+GNNRzZTD7x39Gu3pglZ5oH4=
github.com/crewjam/saml v0.4.14 h1:g9FBNx62osKusnFzs3QTN5L9CVA/Egfgm+stJShzw/c=
@@ -277,10 +295,18 @@ github.com/dgryski/trifles v0.0.0-20230903005119-f50d829f2e54/go.mod h1:if7Fbed8
github.com/dhowden/tag v0.0.0-20240417053706-3d75831295e8 h1:OtSeLS5y0Uy01jaKK4mA/WVIYtpzVm63vLVAPzJXigg=
github.com/dhowden/tag v0.0.0-20240417053706-3d75831295e8/go.mod h1:apkPC/CR3s48O2D7Y++n1XWEpgPNNCjXYga3PPbJe2E=
github.com/dimchansky/utfbom v1.1.0/go.mod h1:rO41eb7gLfo8SF1jd9F8HplJm1Fewwi4mQvIirEdv+8=
+github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
+github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/dlclark/regexp2 v1.4.0 h1:F1rxgk7p4uKjwIQxBs9oAXe5CqrXlCduYEJvrF4u93E=
github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
github.com/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E=
github.com/dnsimple/dnsimple-go v0.63.0/go.mod h1:O5TJ0/U6r7AfT8niYNlmohpLbCSG+c71tQlGr9SeGrg=
+github.com/docker/docker v28.2.2+incompatible h1:CjwRSksz8Yo4+RmQ339Dp/D2tGO5JxwYeqtMOEe0LDw=
+github.com/docker/docker v28.2.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
+github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
+github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
+github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
+github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/dutchcoders/go-clamd v0.0.0-20170520113014-b970184f4d9e h1:rcHHSQqzCgvlwP0I/fQ8rQMn/MpHE5gWSLdtpxtP6KQ=
@@ -288,6 +314,8 @@ github.com/dutchcoders/go-clamd v0.0.0-20170520113014-b970184f4d9e/go.mod h1:Byz
github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs=
github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU=
github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
+github.com/ebitengine/purego v0.8.4 h1:CF7LEKg5FFOsASUj0+QwaXf8Ht6TlFxg09+S9wz0omw=
+github.com/ebitengine/purego v0.8.4/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/egirna/icap v0.0.0-20181108071049-d5ee18bd70bc h1:6IxmRbXV8WXVkcYcTzkU219A3UZeNMX/e6X2sve1wXA=
github.com/egirna/icap v0.0.0-20181108071049-d5ee18bd70bc/go.mod h1:FdVN2WHg7zOHhJ7kZQdDorfFhIfqZaHttjAzDDvAXHE=
github.com/elazarl/goproxy v1.4.0 h1:4GyuSbFa+s26+3rmYNSuUVsx+HgPrV1bk1jXI0l9wjM=
@@ -519,6 +547,7 @@ github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
@@ -730,8 +759,12 @@ github.com/longsleep/go-metrics v1.0.0 h1:o2A6Dbu4MhLpZuL444WFoZzM7X7igewrj2Mouw
github.com/longsleep/go-metrics v1.0.0/go.mod h1:w6QO1LBkVla70FZrrF6XcB0YN+jTEYugjkn3+6RYTSM=
github.com/longsleep/rndm v1.2.0 h1:wPl+kIMyIUTUFW5+2b327DmM1Rlj+gmexsiyOTB7rzM=
github.com/longsleep/rndm v1.2.0/go.mod h1:5qyvM6CXNteKgz6djqqwZOP4+KcPsewrfKyLWd1dCFY=
+github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4=
+github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/magiconair/properties v1.8.4/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=
+github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE=
+github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/matryer/moq v0.0.0-20190312154309-6cfb0558e1bd/go.mod h1:9ELz6aaclSIGnZBoaSLZ3NAl1VTufbOrXBPvtcy6WiQ=
github.com/mattermost/xml-roundtrip-validator v0.1.0 h1:RXbVD2UAl7A7nOTR4u7E3ILa4IbtvKBHw64LDsmu9hU=
github.com/mattermost/xml-roundtrip-validator v0.1.0/go.mod h1:qccnGMcpgwcNaBnxqpJpWWUiPNr5H3O8eDgGV9gT5To=
@@ -801,6 +834,22 @@ github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zx
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
github.com/mna/pigeon v1.3.0 h1:/3fzVrl1C2RK3x04tyL+ribn+3S3VSEFFbCFLmRPAoc=
github.com/mna/pigeon v1.3.0/go.mod h1:SKQNHonx2q9U2QSSoPtMigExj+vQ1mOpL7UVFQF/IA0=
+github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
+github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
+github.com/moby/go-archive v0.1.0 h1:Kk/5rdW/g+H8NHdJW2gsXyZ7UnzvJNOy6VKJqueWdcQ=
+github.com/moby/go-archive v0.1.0/go.mod h1:G9B+YoujNohJmrIYFBpSd54GTUB4lt9S+xVQvsJyFuo=
+github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk=
+github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc=
+github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw=
+github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs=
+github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU=
+github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko=
+github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs=
+github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs=
+github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g=
+github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28=
+github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
+github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@@ -810,6 +859,8 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw=
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8=
+github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
+github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/mschoch/smat v0.2.0 h1:8imxQsjDm8yFEAVBe7azKmKSgzSkZXDuKkSq9374khM=
github.com/mschoch/smat v0.2.0/go.mod h1:kc9mz7DoBKqDyiRL7VZN8KvXQMWeTaVnttLRXOlotKw=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
@@ -868,6 +919,12 @@ github.com/opencloud-eu/libre-graph-api-go v1.0.8-0.20250724122329-41ba6b191e76
github.com/opencloud-eu/libre-graph-api-go v1.0.8-0.20250724122329-41ba6b191e76/go.mod h1:pzatilMEHZFT3qV7C/X3MqOa3NlRQuYhlRhZTL+hN6Q=
github.com/opencloud-eu/reva/v2 v2.36.1-0.20250827073033-ede93b948fc8 h1:mvHvhSHJNjoMyvOPf07cUFi7oDe7qpKdJQg/BrEg/fI=
github.com/opencloud-eu/reva/v2 v2.36.1-0.20250827073033-ede93b948fc8/go.mod h1:Gcpjslh5OnxS4zJ3HxeLOy7Hz2WOjn0eVemvBj17nlk=
+github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
+github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
+github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
+github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
+github.com/opensearch-project/opensearch-go/v4 v4.5.0 h1:26XckmmF6MhlXt91Bu1yY6R51jy1Ns/C3XgIfvyeTRo=
+github.com/opensearch-project/opensearch-go/v4 v4.5.0/go.mod h1:VmFc7dqOEM3ZtLhrpleOzeq+cqUgNabqQG5gX0xId64=
github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs=
github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc=
@@ -908,6 +965,8 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
+github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw=
+github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/pquerna/cachecontrol v0.2.0 h1:vBXSNuE5MYP9IJ5kjsdo8uq+w41jSPgvba2DEnkRx9k=
github.com/pquerna/cachecontrol v0.2.0/go.mod h1:NrUG3Z7Rdu85UNR3vm7SOsl1nFIeSiQnrHV5K9mBcUI=
github.com/pquerna/otp v1.3.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg=
@@ -1011,6 +1070,8 @@ github.com/shamaton/msgpack/v2 v2.2.3 h1:uDOHmxQySlvlUYfQwdjxyybAOzjlQsD1Vjy+4jm
github.com/shamaton/msgpack/v2 v2.2.3/go.mod h1:6khjYnkx73f7VQU7wjcFS9DFjs+59naVWJv1TB7qdOI=
github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI=
github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
+github.com/shirou/gopsutil/v4 v4.25.5 h1:rtd9piuSMGeU8g1RMXjZs9y9luK5BwtnG7dZaQUJAsc=
+github.com/shirou/gopsutil/v4 v4.25.5/go.mod h1:PfybzyydfZcN+JMMjkF6Zb8Mq1A/VcogFFg7hj50W9c=
github.com/shurcooL/httpfs v0.0.0-20230704072500-f1e31cf0ba5c h1:aqg5Vm5dwtvL+YgDpBcK1ITf3o96N/K7/wsRXQnUTEs=
github.com/shurcooL/httpfs v0.0.0-20230704072500-f1e31cf0ba5c/go.mod h1:owqhoLW1qZoYLZzLnBw+QkPP9WZnjlSWihhxAJC1+/M=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
@@ -1077,12 +1138,17 @@ github.com/tchap/go-patricia/v2 v2.3.2 h1:xTHFutuitO2zqKAQ5rCROYgUb7Or/+IC3fts9/
github.com/tchap/go-patricia/v2 v2.3.2/go.mod h1:VZRHKAb53DLaG+nA9EaYYiaEx6YztwDlLElMsnSHD4k=
github.com/test-go/testify v1.1.4 h1:Tf9lntrKUMHiXQ07qBScBTSA0dhYQlu83hswqelv1iE=
github.com/test-go/testify v1.1.4/go.mod h1:rH7cfJo/47vWGdi4GPj16x3/t1xGOj2YxzmNQzk2ghU=
+github.com/testcontainers/testcontainers-go v0.38.0 h1:d7uEapLcv2P8AvH8ahLqDMMxda2W9gQN1nRbHS28HBw=
+github.com/testcontainers/testcontainers-go v0.38.0/go.mod h1:C52c9MoHpWO+C4aqmgSU+hxlR5jlEayWtgYrb8Pzz1w=
+github.com/testcontainers/testcontainers-go/modules/opensearch v0.38.0 h1:+ndHb4j4SxJYSflYJZQen/8Cj4rjNT96toYFMCTQgd8=
+github.com/testcontainers/testcontainers-go/modules/opensearch v0.38.0/go.mod h1:IhutRBtJkqtEG9bTp4dYbaOuHkBqilBNGfVujlFo7/0=
github.com/thanhpk/randstr v1.0.6 h1:psAOktJFD4vV9NEVb3qkhRSMvYh4ORRaj1+w/hn4B+o=
github.com/thanhpk/randstr v1.0.6/go.mod h1:M/H2P1eNLZzlDwAzpkkkUvoyNNMbzRGhESZuEQk3r0U=
github.com/theckman/yacspin v0.13.12 h1:CdZ57+n0U6JMuh2xqjnjRq5Haj6v1ner2djtLQRzJr4=
github.com/theckman/yacspin v0.13.12/go.mod h1:Rd2+oG2LmQi5f3zC3yeZAOl245z8QOvrH4OPOJNZxLg=
github.com/thejerf/suture/v4 v4.0.6 h1:QsuCEsCqb03xF9tPAsWAj8QOAJBgQI1c0VqJNaingg8=
github.com/thejerf/suture/v4 v4.0.6/go.mod h1:gu9Y4dXNUWFrByqRt30Rm9/UZ0wzRSt9AJS6xu/ZGxU=
+github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
@@ -1090,6 +1156,8 @@ github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JT
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
+github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
+github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
github.com/tinylib/msgp v1.3.0 h1:ULuf7GPooDaIlbyvgAxBV/FI7ynli6LZ1/nVUNu+0ww=
github.com/tinylib/msgp v1.3.0/go.mod h1:ykjzy2wzgrlvpDCRc4LA8UXy6D8bzMSuAF3WD57Gok0=
github.com/tklauser/go-sysconf v0.3.14 h1:g5vzr9iPFFz24v2KZXs/pvpvh8/V9Fw6vQK5ZZb78yU=
@@ -1120,6 +1188,8 @@ github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21
github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
github.com/vultr/govultr/v2 v2.0.0/go.mod h1:2PsEeg+gs3p/Fo5Pw8F9mv+DUBEOlrNZ8GmCTGmhOhs=
+github.com/wI2L/jsondiff v0.7.0 h1:1lH1G37GhBPqCfp/lrs91rf/2j3DktX6qYAKZkLuCQQ=
+github.com/wI2L/jsondiff v0.7.0/go.mod h1:KAEIojdQq66oJiHhDyQez2x+sRit0vIzC9KeK0yizxM=
github.com/wk8/go-ordered-map v1.0.0 h1:BV7z+2PaK8LTSd/mWgY12HyMAo5CEgkHqbkVq2thqr8=
github.com/wk8/go-ordered-map v1.0.0/go.mod h1:9ZIbRunKbuvfPKyBP1SIKLcXNlv74YCOZ3t3VTS6gRk=
github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
@@ -1387,6 +1457,7 @@ golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -1418,11 +1489,13 @@ golang.org/x/sys v0.0.0-20200918174421-af09f7315aff/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201110211018-35f3e6cf4a65/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
diff --git a/pkg/conversions/conversions.go b/pkg/conversions/conversions.go
new file mode 100644
index 0000000000..6931f6e723
--- /dev/null
+++ b/pkg/conversions/conversions.go
@@ -0,0 +1,24 @@
+package conversions
+
+import (
+ "encoding/json"
+)
+
+func To[T any](v any) (T, error) {
+ var t T
+
+ if v == nil {
+ return t, nil
+ }
+
+ j, err := json.Marshal(v)
+ if err != nil {
+ return t, err
+ }
+
+ if err := json.Unmarshal(j, &t); err != nil {
+ return t, err
+ }
+
+ return t, nil
+}
diff --git a/services/search/pkg/config/defaults/defaultconfig.go b/services/search/pkg/config/defaults/defaultconfig.go
index ff3bc07113..222eb273d0 100644
--- a/services/search/pkg/config/defaults/defaultconfig.go
+++ b/services/search/pkg/config/defaults/defaultconfig.go
@@ -39,6 +39,11 @@ func DefaultConfig() *config.Config {
Bleve: config.EngineBleve{
Datapath: filepath.Join(defaults.BaseDataPath(), "search"),
},
+ OpenSearch: config.EngineOpenSearch{
+ ResourceIndex: config.EngineOpenSearchResourceIndex{
+ Name: "opencloud-resource",
+ },
+ },
},
Extractor: config.Extractor{
Type: "basic",
diff --git a/services/search/pkg/config/engine.go b/services/search/pkg/config/engine.go
index 41f2de3415..364be26cce 100644
--- a/services/search/pkg/config/engine.go
+++ b/services/search/pkg/config/engine.go
@@ -1,12 +1,47 @@
package config
+import (
+ "net/http"
+ "time"
+)
+
// Engine defines which search engine to use
type Engine struct {
- Type string `yaml:"type" env:"SEARCH_ENGINE_TYPE" desc:"Defines which search engine to use. Defaults to 'bleve'. Supported values are: 'bleve'." introductionVersion:"1.0.0"`
- Bleve EngineBleve `yaml:"bleve"`
+ Type string `yaml:"type" env:"SEARCH_ENGINE_TYPE" desc:"Defines which search engine to use. Defaults to 'bleve'. Supported values are: 'bleve'." introductionVersion:"1.0.0"`
+ Bleve EngineBleve `yaml:"bleve"`
+ OpenSearch EngineOpenSearch `yaml:"open_search"`
}
// EngineBleve configures the bleve engine
type EngineBleve struct {
Datapath string `yaml:"data_path" env:"SEARCH_ENGINE_BLEVE_DATA_PATH" desc:"The directory where the filesystem will store search data. If not defined, the root directory derives from $OC_BASE_DATA_PATH/search." introductionVersion:"1.0.0"`
}
+
+// EngineOpenSearch configures the OpenSearch engine
+type EngineOpenSearch struct {
+ Client EngineOpenSearchClient `yaml:"client"`
+ ResourceIndex EngineOpenSearchResourceIndex `yaml:"resource_index"`
+}
+
+// EngineOpenSearchResourceIndex defines the OpenSearch index for resources
+type EngineOpenSearchResourceIndex struct {
+ Name string `yaml:"name" env:"SEARCH_ENGINE_OPEN_SEARCH_RESOURCE_INDEX_NAME" desc:"The name of the OpenSearch index for resources." introductionVersion:"%%NEXT%%"`
+}
+
+// EngineOpenSearchClient configures the OpenSearch client
+type EngineOpenSearchClient struct {
+ Addresses []string `yaml:"addresses" env:"SEARCH_ENGINE_OPEN_SEARCH_CLIENT_ADDRESSES" desc:"The addresses of the OpenSearch nodes.." introductionVersion:"%%NEXT%%"`
+ Username string `yaml:"username" env:"SEARCH_ENGINE_OPEN_SEARCH_CLIENT_USERNAME" desc:"Username for HTTP Basic Authentication." introductionVersion:"%%NEXT%%"`
+ Password string `yaml:"password" env:"SEARCH_ENGINE_OPEN_SEARCH_CLIENT_PASSWORD" desc:"Password for HTTP Basic Authentication." introductionVersion:"%%NEXT%%"`
+ Header http.Header `yaml:"header" env:"SEARCH_ENGINE_OPEN_SEARCH_CLIENT_HEADER" desc:"HTTP headers to include in requests." introductionVersion:"%%NEXT%%"`
+ CACert []byte `yaml:"ca_cert" env:"SEARCH_ENGINE_OPEN_SEARCH_CLIENT_CA_CERT" desc:"CA certificate for TLS connections." introductionVersion:"%%NEXT%%"`
+ RetryOnStatus []int `yaml:"retry_on_status" env:"SEARCH_ENGINE_OPEN_SEARCH_CLIENT_RETRY_ON_STATUS" desc:"HTTP status codes that trigger a retry." introductionVersion:"%%NEXT%%"`
+ DisableRetry bool `yaml:"disable_retry" env:"SEARCH_ENGINE_OPEN_SEARCH_CLIENT_DISABLE_RETRY" desc:"Disable retries on errors." introductionVersion:"%%NEXT%%"`
+ EnableRetryOnTimeout bool `yaml:"enable_retry_on_timeout" env:"SEARCH_ENGINE_OPEN_SEARCH_CLIENT_ENABLE_RETRY_ON_TIMEOUT" desc:"Enable retries on timeout." introductionVersion:"%%NEXT%%"`
+ MaxRetries int `yaml:"max_retries" env:"SEARCH_ENGINE_OPEN_SEARCH_CLIENT_MAX_RETRIES" desc:"Maximum number of retries for requests." introductionVersion:"%%NEXT%%"`
+ CompressRequestBody bool `yaml:"compress_request_body" env:"SEARCH_ENGINE_OPEN_SEARCH_CLIENT_COMPRESS_REQUEST_BODY" desc:"Compress request bodies." introductionVersion:"%%NEXT%%"`
+ DiscoverNodesOnStart bool `yaml:"discover_nodes_on_start" env:"SEARCH_ENGINE_OPEN_SEARCH_CLIENT_DISCOVER_NODES_ON_START" desc:"Discover nodes on service start." introductionVersion:"%%NEXT%%"`
+ DiscoverNodesInterval time.Duration `yaml:"discover_nodes_interval" env:"SEARCH_ENGINE_OPEN_SEARCH_CLIENT_DISCOVER_NODES_INTERVAL" desc:"Interval for discovering nodes." introductionVersion:"%%NEXT%%"`
+ EnableMetrics bool `yaml:"enable_metrics" env:"SEARCH_ENGINE_OPEN_SEARCH_CLIENT_ENABLE_METRICS" desc:"Enable metrics collection." introductionVersion:"%%NEXT%%"`
+ EnableDebugLogger bool `yaml:"enable_debug_logger" env:"SEARCH_ENGINE_OPEN_SEARCH_CLIENT_ENABLE_DEBUG_LOGGER" desc:"Enable debug logging." introductionVersion:"%%NEXT%%"`
+}
diff --git a/services/search/pkg/opensearch/backend.go b/services/search/pkg/opensearch/backend.go
new file mode 100644
index 0000000000..2ffb72571d
--- /dev/null
+++ b/services/search/pkg/opensearch/backend.go
@@ -0,0 +1,338 @@
+package opensearch
+
+import (
+ "bytes"
+ "context"
+ "encoding/json"
+ "fmt"
+ "path"
+ "strings"
+ "time"
+
+ storageProvider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
+ "github.com/opencloud-eu/reva/v2/pkg/storagespace"
+ "github.com/opencloud-eu/reva/v2/pkg/utils"
+ opensearchgoAPI "github.com/opensearch-project/opensearch-go/v4/opensearchapi"
+
+ "github.com/opencloud-eu/opencloud/pkg/conversions"
+ searchMessage "github.com/opencloud-eu/opencloud/protogen/gen/opencloud/messages/search/v0"
+ searchService "github.com/opencloud-eu/opencloud/protogen/gen/opencloud/services/search/v0"
+ "github.com/opencloud-eu/opencloud/services/search/pkg/engine"
+ "github.com/opencloud-eu/opencloud/services/search/pkg/opensearch/internal/convert"
+ "github.com/opencloud-eu/opencloud/services/search/pkg/opensearch/internal/osu"
+)
+
+var (
+ ErrUnhealthyCluster = fmt.Errorf("cluster is not healthy")
+)
+
+type Backend struct {
+ index string
+ client *opensearchgoAPI.Client
+}
+
+func NewBackend(index string, client *opensearchgoAPI.Client) (*Backend, error) {
+ pingResp, err := client.Ping(context.TODO(), &opensearchgoAPI.PingReq{})
+ switch {
+ case err != nil:
+ return nil, fmt.Errorf("%w, failed to ping opensearch: %w", ErrUnhealthyCluster, err)
+ case pingResp.IsError():
+ return nil, fmt.Errorf("%w, failed to ping opensearch", ErrUnhealthyCluster)
+ }
+
+ // apply the index template
+ if err := IndexManagerLatest.Apply(context.TODO(), index, client); err != nil {
+ return nil, fmt.Errorf("failed to apply index template: %w", err)
+ }
+
+ // first check if the cluster is healthy
+
+ resp, err := client.Cluster.Health(context.TODO(), &opensearchgoAPI.ClusterHealthReq{
+ Indices: []string{index},
+ Params: opensearchgoAPI.ClusterHealthParams{
+ Local: opensearchgoAPI.ToPointer(true),
+ Timeout: 5 * time.Second,
+ },
+ })
+ switch {
+ case err != nil:
+ return nil, fmt.Errorf("%w, failed to get cluster health: %w", ErrUnhealthyCluster, err)
+ case resp.TimedOut:
+ return nil, fmt.Errorf("%w, cluster health request timed out", ErrUnhealthyCluster)
+ case resp.Status != "green" && resp.Status != "yellow":
+ return nil, fmt.Errorf("%w, cluster health is not green or yellow: %s", ErrUnhealthyCluster, resp.Status)
+ }
+
+ return &Backend{index: index, client: client}, nil
+}
+
+func (be *Backend) Search(ctx context.Context, sir *searchService.SearchIndexRequest) (*searchService.SearchIndexResponse, error) {
+ boolQuery, err := convert.KQLToOpenSearchBoolQuery(sir.Query)
+ if err != nil {
+ return nil, fmt.Errorf("failed to convert KQL query to OpenSearch bool query: %w", err)
+ }
+
+ // filter out deleted resources
+ boolQuery.Filter(
+ osu.NewTermQuery[bool]("Deleted").Value(false),
+ )
+
+ if sir.Ref != nil {
+ // if a reference is provided, filter by the root ID
+ boolQuery.Filter(
+ osu.NewTermQuery[string]("RootID").Value(
+ storagespace.FormatResourceID(
+ &storageProvider.ResourceId{
+ StorageId: sir.Ref.GetResourceId().GetStorageId(),
+ SpaceId: sir.Ref.GetResourceId().GetSpaceId(),
+ OpaqueId: sir.Ref.GetResourceId().GetOpaqueId(),
+ },
+ ),
+ ),
+ )
+ }
+
+ searchParams := opensearchgoAPI.SearchParams{}
+
+ switch {
+ case sir.PageSize == -1:
+ searchParams.Size = conversions.ToPointer(1000)
+ case sir.PageSize == 0:
+ searchParams.Size = conversions.ToPointer(200)
+ default:
+ searchParams.Size = conversions.ToPointer(int(sir.PageSize))
+ }
+
+ req, err := osu.BuildSearchReq(&opensearchgoAPI.SearchReq{
+ Indices: []string{be.index},
+ Params: searchParams,
+ },
+ boolQuery,
+ osu.SearchBodyParams{
+ Highlight: &osu.BodyParamHighlight{
+ PreTags: []string{""},
+ PostTags: []string{""},
+ Fields: map[string]osu.BodyParamHighlight{
+ "Content": {},
+ },
+ },
+ },
+ )
+ if err != nil {
+ return nil, fmt.Errorf("failed to build search request: %w", err)
+ }
+
+ resp, err := be.client.Search(ctx, req)
+ if err != nil {
+ return nil, fmt.Errorf("failed to search: %w", err)
+ }
+
+ matches := make([]*searchMessage.Match, 0, len(resp.Hits.Hits))
+ totalMatches := resp.Hits.Total.Value
+ for _, hit := range resp.Hits.Hits {
+ match, err := convert.OpenSearchHitToMatch(hit)
+ if err != nil {
+ return nil, fmt.Errorf("failed to convert hit to match: %w", err)
+ }
+
+ if sir.Ref != nil {
+ hitPath := strings.TrimSuffix(match.GetEntity().GetRef().GetPath(), "/")
+ requestedPath := utils.MakeRelativePath(sir.Ref.Path)
+ isRoot := hitPath == requestedPath
+
+ if !isRoot && requestedPath != "." && !strings.HasPrefix(hitPath, requestedPath+"/") {
+ totalMatches--
+ continue
+ }
+ }
+
+ matches = append(matches, match)
+ }
+
+ return &searchService.SearchIndexResponse{
+ Matches: matches,
+ TotalMatches: int32(totalMatches),
+ }, nil
+}
+
+func (be *Backend) Upsert(id string, r engine.Resource) error {
+ body, err := json.Marshal(r)
+ if err != nil {
+ return fmt.Errorf("failed to marshal resource: %w", err)
+ }
+
+ _, err = be.client.Index(context.TODO(), opensearchgoAPI.IndexReq{
+ Index: be.index,
+ DocumentID: id,
+ Body: bytes.NewReader(body),
+ })
+ if err != nil {
+ return fmt.Errorf("failed to index document: %w", err)
+ }
+
+ return nil
+}
+
+func (be *Backend) Move(id string, parentID string, target string) error {
+ return be.updateSelfAndDescendants(id, func(rootResource engine.Resource) *osu.BodyParamScript {
+ return &osu.BodyParamScript{
+ Source: `
+ if (ctx._source.ID == params.id ) { ctx._source.Name = params.newName; ctx._source.ParentID = params.parentID; }
+ ctx._source.Path = ctx._source.Path.replace(params.oldPath, params.newPath)
+ `,
+ Lang: "painless",
+ Params: map[string]any{
+ "id": id,
+ "parentID": parentID,
+ "oldPath": rootResource.Path,
+ "newPath": utils.MakeRelativePath(target),
+ "newName": path.Base(utils.MakeRelativePath(target)),
+ },
+ }
+ })
+}
+
+func (be *Backend) Delete(id string) error {
+ return be.updateSelfAndDescendants(id, func(_ engine.Resource) *osu.BodyParamScript {
+ return &osu.BodyParamScript{
+ Source: "ctx._source.Deleted = params.deleted",
+ Lang: "painless",
+ Params: map[string]any{
+ "deleted": true,
+ },
+ }
+ })
+}
+
+func (be *Backend) Restore(id string) error {
+ return be.updateSelfAndDescendants(id, func(_ engine.Resource) *osu.BodyParamScript {
+ return &osu.BodyParamScript{
+ Source: "ctx._source.Deleted = params.deleted",
+ Lang: "painless",
+ Params: map[string]any{
+ "deleted": false,
+ },
+ }
+ })
+}
+
+func (be *Backend) Purge(id string) error {
+ resource, err := be.getResource(id)
+ if err != nil {
+ return fmt.Errorf("failed to get resource: %w", err)
+ }
+
+ req, err := osu.BuildDocumentDeleteByQueryReq(
+ opensearchgoAPI.DocumentDeleteByQueryReq{
+ Indices: []string{be.index},
+ Params: opensearchgoAPI.DocumentDeleteByQueryParams{
+ WaitForCompletion: conversions.ToPointer(true),
+ },
+ },
+ osu.NewTermQuery[string]("Path").Value(resource.Path),
+ )
+ if err != nil {
+ return fmt.Errorf("failed to build delete by query request: %w", err)
+ }
+
+ resp, err := be.client.Document.DeleteByQuery(context.TODO(), req)
+ switch {
+ case err != nil:
+ return fmt.Errorf("failed to delete by query: %w", err)
+ case len(resp.Failures) != 0:
+ return fmt.Errorf("failed to delete by query, failures: %v", resp.Failures)
+ }
+
+ return nil
+}
+
+func (be *Backend) DocCount() (uint64, error) {
+ req, err := osu.BuildIndicesCountReq(
+ &opensearchgoAPI.IndicesCountReq{
+ Indices: []string{be.index},
+ },
+ osu.NewTermQuery[bool]("Deleted").Value(false),
+ )
+ if err != nil {
+ return 0, fmt.Errorf("failed to build count request: %w", err)
+ }
+
+ resp, err := be.client.Indices.Count(context.TODO(), req)
+ if err != nil {
+ return 0, fmt.Errorf("failed to count documents: %w", err)
+ }
+
+ return uint64(resp.Count), nil
+}
+
+func (be *Backend) updateSelfAndDescendants(id string, scriptProvider func(engine.Resource) *osu.BodyParamScript) error {
+ if scriptProvider == nil {
+ return fmt.Errorf("script cannot be nil")
+ }
+
+ resource, err := be.getResource(id)
+ if err != nil {
+ return fmt.Errorf("failed to get resource: %w", err)
+ }
+
+ req, err := osu.BuildUpdateByQueryReq(
+ opensearchgoAPI.UpdateByQueryReq{
+ Indices: []string{be.index},
+ Params: opensearchgoAPI.UpdateByQueryParams{
+ WaitForCompletion: conversions.ToPointer(true),
+ },
+ },
+ osu.NewTermQuery[string]("Path").Value(resource.Path),
+ osu.UpdateByQueryBodyParams{
+ Script: scriptProvider(resource),
+ },
+ )
+ if err != nil {
+ return fmt.Errorf("failed to build update by query request: %w", err)
+ }
+
+ resp, err := be.client.UpdateByQuery(context.TODO(), req)
+ switch {
+ case err != nil:
+ return fmt.Errorf("failed to update by query: %w", err)
+ case len(resp.Failures) != 0:
+ return fmt.Errorf("failed to update by query, failures: %v", resp.Failures)
+ }
+
+ return nil
+}
+
+func (be *Backend) getResource(id string) (engine.Resource, error) {
+ req, err := osu.BuildSearchReq(
+ &opensearchgoAPI.SearchReq{
+ Indices: []string{be.index},
+ },
+ osu.NewIDsQuery(id),
+ )
+ if err != nil {
+ return engine.Resource{}, fmt.Errorf("failed to build search request: %w", err)
+ }
+
+ resp, err := be.client.Search(context.TODO(), req)
+ switch {
+ case err != nil:
+ return engine.Resource{}, fmt.Errorf("failed to search for resource: %w", err)
+ case resp.Hits.Total.Value == 0 || len(resp.Hits.Hits) == 0:
+ return engine.Resource{}, fmt.Errorf("document with id %s not found", id)
+ }
+
+ resource, err := conversions.To[engine.Resource](resp.Hits.Hits[0].Source)
+ if err != nil {
+ return engine.Resource{}, fmt.Errorf("failed to convert hit source: %w", err)
+ }
+
+ return resource, nil
+}
+
+func (be *Backend) StartBatch(_ int) error {
+ return nil // todo: implement batch processing
+}
+
+func (be *Backend) EndBatch() error {
+ return nil // todo: implement batch processing
+}
diff --git a/services/search/pkg/opensearch/backend_test.go b/services/search/pkg/opensearch/backend_test.go
new file mode 100644
index 0000000000..6b653c7dff
--- /dev/null
+++ b/services/search/pkg/opensearch/backend_test.go
@@ -0,0 +1,253 @@
+package opensearch_test
+
+import (
+ "fmt"
+ "strings"
+ "testing"
+
+ opensearchgo "github.com/opensearch-project/opensearch-go/v4"
+ opensearchgoAPI "github.com/opensearch-project/opensearch-go/v4/opensearchapi"
+ "github.com/stretchr/testify/require"
+
+ searchService "github.com/opencloud-eu/opencloud/protogen/gen/opencloud/services/search/v0"
+ "github.com/opencloud-eu/opencloud/services/search/pkg/engine"
+ "github.com/opencloud-eu/opencloud/services/search/pkg/opensearch"
+ "github.com/opencloud-eu/opencloud/services/search/pkg/opensearch/internal/test"
+)
+
+func TestNewBackend(t *testing.T) {
+ t.Run("fails to create if the cluster is not healthy", func(t *testing.T) {
+ client, err := opensearchgoAPI.NewClient(opensearchgoAPI.Config{
+ Client: opensearchgo.Config{
+ Addresses: []string{"http://localhost:1025"},
+ },
+ })
+ require.NoError(t, err, "failed to create OpenSearch client")
+
+ backend, err := opensearch.NewBackend("test-engine-new-engine", client)
+ require.Nil(t, backend)
+ require.ErrorIs(t, err, opensearch.ErrUnhealthyCluster)
+ })
+}
+
+func TestEngine_Search(t *testing.T) {
+ indexName := "opencloud-test-engine-search"
+ tc := opensearchtest.NewDefaultTestClient(t, defaultConfig.Engine.OpenSearch.Client)
+ tc.Require.IndicesReset([]string{indexName})
+ tc.Require.IndicesCount([]string{indexName}, nil, 0)
+
+ defer tc.Require.IndicesDelete([]string{indexName})
+
+ backend, err := opensearch.NewBackend(indexName, tc.Client())
+ require.NoError(t, err)
+
+ document := opensearchtest.Testdata.Resources.File
+ tc.Require.DocumentCreate(indexName, document.ID, strings.NewReader(opensearchtest.JSONMustMarshal(t, document)))
+ tc.Require.IndicesCount([]string{indexName}, nil, 1)
+
+ t.Run("most simple search", func(t *testing.T) {
+ resp, err := backend.Search(t.Context(), &searchService.SearchIndexRequest{
+ Query: fmt.Sprintf(`"%s"`, document.Name),
+ })
+ require.NoError(t, err)
+ require.Len(t, resp.Matches, 1)
+ require.Equal(t, int32(1), resp.TotalMatches)
+ require.Equal(t, document.ID, fmt.Sprintf("%s$%s!%s", resp.Matches[0].Entity.Id.StorageId, resp.Matches[0].Entity.Id.SpaceId, resp.Matches[0].Entity.Id.OpaqueId))
+ })
+
+ t.Run("ignores files that are marked as deleted", func(t *testing.T) {
+ deletedDocument := opensearchtest.Testdata.Resources.File
+ deletedDocument.ID = "1$2!4"
+ deletedDocument.Deleted = true
+
+ tc.Require.DocumentCreate(indexName, deletedDocument.ID, strings.NewReader(opensearchtest.JSONMustMarshal(t, deletedDocument)))
+ tc.Require.IndicesCount([]string{indexName}, nil, 2)
+
+ resp, err := backend.Search(t.Context(), &searchService.SearchIndexRequest{
+ Query: fmt.Sprintf(`"%s"`, document.Name),
+ })
+ require.NoError(t, err)
+ require.Len(t, resp.Matches, 1)
+ require.Equal(t, int32(1), resp.TotalMatches)
+ require.Equal(t, document.ID, fmt.Sprintf("%s$%s!%s", resp.Matches[0].Entity.Id.StorageId, resp.Matches[0].Entity.Id.SpaceId, resp.Matches[0].Entity.Id.OpaqueId))
+ })
+}
+
+func TestEngine_Upsert(t *testing.T) {
+ indexName := "opencloud-test-engine-upsert"
+ tc := opensearchtest.NewDefaultTestClient(t, defaultConfig.Engine.OpenSearch.Client)
+ tc.Require.IndicesReset([]string{indexName})
+ tc.Require.IndicesCount([]string{indexName}, nil, 0)
+
+ defer tc.Require.IndicesDelete([]string{indexName})
+
+ backend, err := opensearch.NewBackend(indexName, tc.Client())
+ require.NoError(t, err)
+
+ t.Run("upsert with full document", func(t *testing.T) {
+ document := opensearchtest.Testdata.Resources.File
+ require.NoError(t, backend.Upsert(document.ID, document))
+
+ tc.Require.IndicesCount([]string{indexName}, nil, 1)
+ })
+}
+
+func TestEngine_Move(t *testing.T) {
+ indexName := "opencloud-test-engine-move"
+ tc := opensearchtest.NewDefaultTestClient(t, defaultConfig.Engine.OpenSearch.Client)
+ tc.Require.IndicesReset([]string{indexName})
+ tc.Require.IndicesCount([]string{indexName}, nil, 0)
+
+ defer tc.Require.IndicesDelete([]string{indexName})
+
+ backend, err := opensearch.NewBackend(indexName, tc.Client())
+ require.NoError(t, err)
+
+ t.Run("moves the document to a new path", func(t *testing.T) {
+ document := opensearchtest.Testdata.Resources.File
+ tc.Require.DocumentCreate(indexName, document.ID, strings.NewReader(opensearchtest.JSONMustMarshal(t, document)))
+ tc.Require.IndicesCount([]string{indexName}, nil, 1)
+
+ body := opensearchtest.JSONMustMarshal(t, map[string]any{
+ "query": map[string]any{
+ "ids": map[string]any{
+ "values": []string{document.ID},
+ },
+ },
+ })
+
+ resources := opensearchtest.SearchHitsMustBeConverted[engine.Resource](t, tc.Require.Search(indexName, strings.NewReader(body)).Hits)
+ require.Len(t, resources, 1)
+ require.Equal(t, document.Path, resources[0].Path)
+
+ document.Path = "./new/path/to/resource"
+ require.NoError(t, backend.Move(document.ID, document.ParentID, document.Path))
+
+ resources = opensearchtest.SearchHitsMustBeConverted[engine.Resource](t, tc.Require.Search(indexName, strings.NewReader(body)).Hits)
+ require.Len(t, resources, 1)
+ require.Equal(t, document.Path, resources[0].Path)
+ })
+}
+
+func TestEngine_Delete(t *testing.T) {
+ indexName := "opencloud-test-engine-delete"
+ tc := opensearchtest.NewDefaultTestClient(t, defaultConfig.Engine.OpenSearch.Client)
+ tc.Require.IndicesReset([]string{indexName})
+ tc.Require.IndicesCount([]string{indexName}, nil, 0)
+
+ defer tc.Require.IndicesDelete([]string{indexName})
+
+ backend, err := opensearch.NewBackend(indexName, tc.Client())
+ require.NoError(t, err)
+
+ t.Run("mark document as deleted", func(t *testing.T) {
+ document := opensearchtest.Testdata.Resources.File
+ tc.Require.DocumentCreate(indexName, document.ID, strings.NewReader(opensearchtest.JSONMustMarshal(t, document)))
+ tc.Require.IndicesCount([]string{indexName}, nil, 1)
+
+ body := opensearchtest.JSONMustMarshal(t, map[string]any{
+ "query": map[string]any{
+ "term": map[string]any{
+ "Deleted": map[string]any{
+ "value": true,
+ },
+ },
+ },
+ })
+
+ tc.Require.IndicesCount([]string{indexName}, strings.NewReader(body), 0)
+
+ require.NoError(t, backend.Delete(document.ID))
+ tc.Require.IndicesCount([]string{indexName}, strings.NewReader(body), 1)
+ })
+}
+
+func TestEngine_Restore(t *testing.T) {
+ indexName := "opencloud-test-engine-restore"
+ tc := opensearchtest.NewDefaultTestClient(t, defaultConfig.Engine.OpenSearch.Client)
+ tc.Require.IndicesReset([]string{indexName})
+ tc.Require.IndicesCount([]string{indexName}, nil, 0)
+
+ defer tc.Require.IndicesDelete([]string{indexName})
+
+ backend, err := opensearch.NewBackend(indexName, tc.Client())
+ require.NoError(t, err)
+
+ t.Run("mark document as not deleted", func(t *testing.T) {
+ document := opensearchtest.Testdata.Resources.File
+ document.Deleted = true
+ tc.Require.DocumentCreate(indexName, document.ID, strings.NewReader(opensearchtest.JSONMustMarshal(t, document)))
+ tc.Require.IndicesCount([]string{indexName}, nil, 1)
+
+ body := opensearchtest.JSONMustMarshal(t, map[string]any{
+ "query": map[string]any{
+ "term": map[string]any{
+ "Deleted": map[string]any{
+ "value": true,
+ },
+ },
+ },
+ })
+
+ tc.Require.IndicesCount([]string{indexName}, strings.NewReader(body), 1)
+
+ require.NoError(t, backend.Restore(document.ID))
+ tc.Require.IndicesCount([]string{indexName}, strings.NewReader(body), 0)
+ })
+}
+
+func TestEngine_Purge(t *testing.T) {
+ indexName := "opencloud-test-engine-purge"
+ tc := opensearchtest.NewDefaultTestClient(t, defaultConfig.Engine.OpenSearch.Client)
+ tc.Require.IndicesReset([]string{indexName})
+ tc.Require.IndicesCount([]string{indexName}, nil, 0)
+
+ defer tc.Require.IndicesDelete([]string{indexName})
+
+ backend, err := opensearch.NewBackend(indexName, tc.Client())
+ require.NoError(t, err)
+
+ t.Run("purge with full document", func(t *testing.T) {
+ document := opensearchtest.Testdata.Resources.File
+ tc.Require.DocumentCreate(indexName, document.ID, strings.NewReader(opensearchtest.JSONMustMarshal(t, document)))
+ tc.Require.IndicesCount([]string{indexName}, nil, 1)
+
+ require.NoError(t, backend.Purge(document.ID))
+
+ tc.Require.IndicesCount([]string{indexName}, nil, 0)
+ })
+}
+
+func TestEngine_DocCount(t *testing.T) {
+ indexName := "opencloud-test-engine-doc-count"
+ tc := opensearchtest.NewDefaultTestClient(t, defaultConfig.Engine.OpenSearch.Client)
+ tc.Require.IndicesReset([]string{indexName})
+ tc.Require.IndicesCount([]string{indexName}, nil, 0)
+
+ defer tc.Require.IndicesDelete([]string{indexName})
+
+ backend, err := opensearch.NewBackend(indexName, tc.Client())
+ require.NoError(t, err)
+
+ t.Run("ignore deleted documents", func(t *testing.T) {
+ document := opensearchtest.Testdata.Resources.File
+ tc.Require.DocumentCreate(indexName, document.ID, strings.NewReader(opensearchtest.JSONMustMarshal(t, document)))
+ tc.Require.IndicesCount([]string{indexName}, nil, 1)
+
+ count, err := backend.DocCount()
+ require.NoError(t, err)
+ require.Equal(t, uint64(1), count)
+
+ tc.Require.Update(indexName, document.ID, strings.NewReader(opensearchtest.JSONMustMarshal(t, map[string]any{
+ "doc": map[string]any{
+ "Deleted": true,
+ },
+ })))
+
+ tc.Require.IndicesCount([]string{indexName}, nil, 1)
+
+ count, err = backend.DocCount()
+ require.NoError(t, err)
+ require.Equal(t, uint64(0), count)
+ })
+}
diff --git a/services/search/pkg/opensearch/index.go b/services/search/pkg/opensearch/index.go
new file mode 100644
index 0000000000..ef69cf9041
--- /dev/null
+++ b/services/search/pkg/opensearch/index.go
@@ -0,0 +1,142 @@
+package opensearch
+
+import (
+ "bytes"
+ "context"
+ "embed"
+ "errors"
+ "fmt"
+ "path"
+ "reflect"
+
+ "github.com/go-jose/go-jose/v3/json"
+ opensearchgoAPI "github.com/opensearch-project/opensearch-go/v4/opensearchapi"
+ "github.com/tidwall/gjson"
+)
+
+var (
+ ErrManualActionRequired = errors.New("manual action required")
+ IndexManagerLatest = IndexIndexManagerResourceV1
+ IndexIndexManagerResourceV1 IndexManager = "resource_v1.json"
+)
+
+//go:embed internal/indexes/*.json
+var indexes embed.FS
+
+type IndexManager string
+
+func (m IndexManager) String() string {
+ b, err := m.MarshalJSON()
+ if err != nil {
+ return ""
+ }
+
+ return string(b)
+}
+
+func (m IndexManager) MarshalJSON() ([]byte, error) {
+ filePath := string(m)
+ body, err := indexes.ReadFile(path.Join("./internal/indexes", filePath))
+ switch {
+ case err != nil:
+ return nil, fmt.Errorf("failed to read index file %s: %w", filePath, err)
+ case len(body) <= 0:
+ return nil, fmt.Errorf("index file %s is empty", filePath)
+ }
+
+ return body, nil
+}
+
+func (m IndexManager) Apply(ctx context.Context, name string, client *opensearchgoAPI.Client) error {
+ localIndexB, err := m.MarshalJSON()
+ if err != nil {
+ return fmt.Errorf("failed to marshal index %s: %w", name, err)
+ }
+
+ indicesExistsResp, err := client.Indices.Exists(ctx, opensearchgoAPI.IndicesExistsReq{
+ Indices: []string{name},
+ })
+ switch {
+ case indicesExistsResp != nil && indicesExistsResp.StatusCode == 404:
+ break
+ case err != nil:
+ return fmt.Errorf("failed to check if index %s exists: %w", name, err)
+ case indicesExistsResp == nil:
+ return fmt.Errorf("indicesExistsResp is nil for index %s", name)
+ }
+
+ if indicesExistsResp.StatusCode == 200 {
+ resp, err := client.Indices.Get(ctx, opensearchgoAPI.IndicesGetReq{
+ Indices: []string{name},
+ })
+ if err != nil {
+ return fmt.Errorf("failed to get index %s: %w", name, err)
+ }
+
+ remoteIndex, ok := resp.Indices[name]
+ if !ok {
+ return fmt.Errorf("index %s not found in response", name)
+ }
+ remoteIndexB, err := json.Marshal(remoteIndex)
+ if err != nil {
+ return fmt.Errorf("failed to marshal index %s: %w", name, err)
+ }
+
+ localIndexJson := gjson.ParseBytes(localIndexB)
+ remoteIndexJson := gjson.ParseBytes(remoteIndexB)
+
+ compare := func(lvPath, rvPath string) (any, any, bool) {
+ lv := localIndexJson.Get(lvPath).Raw
+ rv := remoteIndexJson.Get(rvPath).Raw
+
+ var lvv, rvv interface{}
+ if err := json.Unmarshal([]byte(lv), &lvv); err != nil {
+ return nil, nil, false
+ }
+
+ if err := json.Unmarshal([]byte(rv), &rvv); err != nil {
+ return nil, nil, false
+ }
+
+ return lv, rv, reflect.DeepEqual(lvv, rvv)
+ }
+
+ var errs []error
+
+ for k := range localIndexJson.Get("settings").Map() {
+ if lv, rv, ok := compare("settings."+k, "settings.index."+k); !ok {
+ errs = append(errs, fmt.Errorf("settings.%s local %s, remote %s", k, lv, rv))
+ }
+ }
+
+ for k := range localIndexJson.Get("mappings.properties").Map() {
+ if _, _, ok := compare("mappings.properties."+k, "mappings.properties."+k); !ok {
+ errs = append(errs, fmt.Errorf("mappings.properties.%s", k))
+ }
+ }
+
+ if errs != nil {
+ return fmt.Errorf(
+ "index %s allready exists and is different from the requested version, %w: %w",
+ name,
+ ErrManualActionRequired,
+ errors.Join(errs...),
+ )
+ }
+
+ return nil // Index is already up to date, no action needed
+ }
+
+ createResp, err := client.Indices.Create(ctx, opensearchgoAPI.IndicesCreateReq{
+ Index: name,
+ Body: bytes.NewReader(localIndexB),
+ })
+ switch {
+ case err != nil:
+ return fmt.Errorf("failed to create index %s: %w", name, err)
+ case !createResp.Acknowledged:
+ return fmt.Errorf("failed to create index %s: not acknowledged", name)
+ }
+
+ return nil
+}
diff --git a/services/search/pkg/opensearch/index_test.go b/services/search/pkg/opensearch/index_test.go
new file mode 100644
index 0000000000..f4b5451a9d
--- /dev/null
+++ b/services/search/pkg/opensearch/index_test.go
@@ -0,0 +1,63 @@
+package opensearch_test
+
+import (
+ "strings"
+ "testing"
+
+ "github.com/stretchr/testify/require"
+ "github.com/tidwall/sjson"
+
+ "github.com/opencloud-eu/opencloud/services/search/pkg/opensearch"
+ "github.com/opencloud-eu/opencloud/services/search/pkg/opensearch/internal/test"
+)
+
+func TestIndexManager(t *testing.T) {
+ t.Run("index plausibility", func(t *testing.T) {
+ tests := []opensearchtest.TableTest[opensearch.IndexManager, struct{}]{
+ {
+ Name: "empty",
+ Got: opensearch.IndexManagerLatest,
+ },
+ }
+ tc := opensearchtest.NewDefaultTestClient(t, defaultConfig.Engine.OpenSearch.Client)
+
+ for _, test := range tests {
+ t.Run(test.Name, func(t *testing.T) {
+ indexName := "opencloud-test-resource"
+ tc.Require.IndicesReset([]string{indexName})
+
+ body, err := test.Got.MarshalJSON()
+ require.NoError(t, err)
+ require.NotEmpty(t, body)
+ require.NotEmpty(t, test.Got.String())
+ require.JSONEq(t, test.Got.String(), string(body))
+ require.NoError(t, test.Got.Apply(t.Context(), indexName, tc.Client()))
+ })
+ }
+ })
+
+ t.Run("does not create index if it already exists and is up to date", func(t *testing.T) {
+ indexManager := opensearch.IndexManagerLatest
+ indexName := "opencloud-test-resource"
+
+ tc := opensearchtest.NewDefaultTestClient(t, defaultConfig.Engine.OpenSearch.Client)
+ tc.Require.IndicesReset([]string{indexName})
+ tc.Require.IndicesCreate(indexName, strings.NewReader(indexManager.String()))
+
+ require.NoError(t, indexManager.Apply(t.Context(), indexName, tc.Client()))
+ })
+
+ t.Run("fails to create index if it already exists but is not up to date", func(t *testing.T) {
+ indexManager := opensearch.IndexManagerLatest
+ indexName := "opencloud-test-resource"
+
+ tc := opensearchtest.NewDefaultTestClient(t, defaultConfig.Engine.OpenSearch.Client)
+ tc.Require.IndicesReset([]string{indexName})
+
+ body, err := sjson.Set(indexManager.String(), "settings.number_of_shards", "2")
+ require.NoError(t, err)
+ tc.Require.IndicesCreate(indexName, strings.NewReader(body))
+
+ require.ErrorIs(t, indexManager.Apply(t.Context(), indexName, tc.Client()), opensearch.ErrManualActionRequired)
+ })
+}
diff --git a/services/search/pkg/opensearch/internal/convert/kql_expand.go b/services/search/pkg/opensearch/internal/convert/kql_expand.go
new file mode 100644
index 0000000000..5688f6dfe6
--- /dev/null
+++ b/services/search/pkg/opensearch/internal/convert/kql_expand.go
@@ -0,0 +1,197 @@
+package convert
+
+import (
+ "fmt"
+ "reflect"
+ "slices"
+ "strings"
+
+ "github.com/opencloud-eu/opencloud/pkg/ast"
+)
+
+func ExpandKQL(nodes []ast.Node) ([]ast.Node, error) {
+ return kqlExpander{}.expand(nodes, "")
+}
+
+type kqlExpander struct{}
+
+func (e kqlExpander) expand(nodes []ast.Node, defaultKey string) ([]ast.Node, error) {
+ for i, node := range nodes {
+ rnode := reflect.ValueOf(node)
+
+ // we need to ensure that the node is a pointer to an ast.Node in every case
+ if rnode.Kind() != reflect.Ptr {
+ ptr := reflect.New(rnode.Type())
+ ptr.Elem().Set(rnode)
+ rnode = ptr
+ cnode, ok := rnode.Interface().(ast.Node)
+ if !ok {
+ return nil, fmt.Errorf("expected node to be of type ast.Node, got %T", rnode.Interface())
+ }
+
+ node = cnode // Update the original node to the pointer
+ nodes[i] = node // Update the original slice with the pointer
+ }
+
+ var unfoldedNodes []ast.Node
+ switch cnode := node.(type) {
+ case *ast.GroupNode:
+ if cnode.Key != "" { // group nodes should not get a default key
+ cnode.Key = e.remapKey(cnode.Key, defaultKey)
+ }
+
+ groupNodes, err := e.expand(cnode.Nodes, cnode.Key)
+ if err != nil {
+ return nil, err
+ }
+ cnode.Nodes = groupNodes
+ case *ast.StringNode:
+ cnode.Key = e.remapKey(cnode.Key, defaultKey)
+ cnode.Value = e.lowerValue(cnode.Key, cnode.Value)
+ unfoldedNodes = e.unfoldValue(cnode.Key, cnode.Value)
+ case *ast.DateTimeNode:
+ cnode.Key = e.remapKey(cnode.Key, defaultKey)
+ case *ast.BooleanNode:
+ cnode.Key = e.remapKey(cnode.Key, defaultKey)
+ }
+
+ if unfoldedNodes != nil {
+ // Insert unfolded nodes at the current index
+ nodes = append(nodes[:i], append(unfoldedNodes, nodes[i+1:]...)...)
+ // Adjust index to account for new nodes
+ i += len(unfoldedNodes) - 1
+ }
+ }
+
+ return nodes, nil
+}
+
+func (_ kqlExpander) remapKey(current string, defaultKey string) string {
+ if defaultKey == "" {
+ defaultKey = "Name" // Set a default key if none is provided
+ }
+
+ key, ok := map[string]string{
+ "": defaultKey, // Default case if current is empty
+ "rootid": "RootID",
+ "path": "Path",
+ "id": "ID",
+ "name": "Name",
+ "size": "Size",
+ "mtime": "Mtime",
+ "mediatype": "MimeType",
+ "type": "Type",
+ "tag": "Tags",
+ "tags": "Tags",
+ "content": "Content",
+ "hidden": "Hidden",
+ }[current]
+ if !ok {
+ return current // Return the original key if not found
+ }
+
+ return key
+}
+
+func (_ kqlExpander) lowerValue(key, value string) string {
+ if slices.Contains([]string{"Hidden"}, key) {
+ return value // ignore certain keys and return the original value
+ }
+
+ return strings.ToLower(value)
+}
+
+func (_ kqlExpander) unfoldValue(key, value string) []ast.Node {
+ result, ok := map[string][]ast.Node{
+ "MimeType:file": {
+ &ast.OperatorNode{Value: "NOT"},
+ &ast.StringNode{Key: key, Value: "httpd/unix-directory"},
+ },
+ "MimeType:folder": {
+ &ast.StringNode{Key: key, Value: "httpd/unix-directory"},
+ },
+ "MimeType:document": {
+ &ast.GroupNode{Nodes: []ast.Node{
+ &ast.StringNode{Key: key, Value: "application/msword"},
+ &ast.OperatorNode{Value: "OR"},
+ &ast.StringNode{Key: key, Value: "application/vnd.openxmlformats-officedocument.wordprocessingml.document"},
+ &ast.OperatorNode{Value: "OR"},
+ &ast.StringNode{Key: key, Value: "application/vnd.openxmlformats-officedocument.wordprocessingml.form"},
+ &ast.OperatorNode{Value: "OR"},
+ &ast.StringNode{Key: key, Value: "application/vnd.oasis.opendocument.text"},
+ &ast.OperatorNode{Value: "OR"},
+ &ast.StringNode{Key: key, Value: "text/plain"},
+ &ast.OperatorNode{Value: "OR"},
+ &ast.StringNode{Key: key, Value: "text/markdown"},
+ &ast.OperatorNode{Value: "OR"},
+ &ast.StringNode{Key: key, Value: "application/rtf"},
+ &ast.OperatorNode{Value: "OR"},
+ &ast.StringNode{Key: key, Value: "application/vnd.apple.pages"},
+ }},
+ },
+ "MimeType:spreadsheet": {
+ &ast.GroupNode{Nodes: []ast.Node{
+ &ast.StringNode{Key: key, Value: "application/vnd.ms-excel"},
+ &ast.OperatorNode{Value: "OR"},
+ &ast.StringNode{Key: key, Value: "application/vnd.oasis.opendocument.spreadsheet"},
+ &ast.OperatorNode{Value: "OR"},
+ &ast.StringNode{Key: key, Value: "text/csv"},
+ &ast.OperatorNode{Value: "OR"},
+ &ast.StringNode{Key: key, Value: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"},
+ &ast.OperatorNode{Value: "OR"},
+ &ast.StringNode{Key: key, Value: "application/vnd.oasis.opendocument.spreadshee"},
+ &ast.OperatorNode{Value: "OR"},
+ &ast.StringNode{Key: key, Value: "application/vnd.apple.numbers"},
+ }},
+ },
+ "MimeType:presentation": {
+ &ast.GroupNode{Nodes: []ast.Node{
+ &ast.StringNode{Key: key, Value: "application/vnd.openxmlformats-officedocument.presentationml.presentation"},
+ &ast.OperatorNode{Value: "OR"},
+ &ast.StringNode{Key: key, Value: "application/vnd.oasis.opendocument.presentation"},
+ &ast.OperatorNode{Value: "OR"},
+ &ast.StringNode{Key: key, Value: "application/vnd.ms-powerpoint"},
+ &ast.OperatorNode{Value: "OR"},
+ &ast.StringNode{Key: key, Value: "application/vnd.apple.keynote"},
+ }},
+ },
+ "MimeType:pdf": {
+ &ast.StringNode{Key: key, Value: "application/pdf"},
+ },
+ "MimeType:image": {
+ &ast.StringNode{Key: key, Value: "image/*"},
+ },
+ "MimeType:video": {
+ &ast.StringNode{Key: key, Value: "video/*"},
+ },
+ "MimeType:audio": {
+ &ast.StringNode{Key: key, Value: "audio/*"},
+ },
+ "MimeType:archive": {
+ &ast.GroupNode{Nodes: []ast.Node{
+ &ast.StringNode{Key: key, Value: "application/zip"},
+ &ast.OperatorNode{Value: "OR"},
+ &ast.StringNode{Key: key, Value: "application/gzip"},
+ &ast.OperatorNode{Value: "OR"},
+ &ast.StringNode{Key: key, Value: "application/x-gzip"},
+ &ast.OperatorNode{Value: "OR"},
+ &ast.StringNode{Key: key, Value: "application/x-7z-compressed"},
+ &ast.OperatorNode{Value: "OR"},
+ &ast.StringNode{Key: key, Value: "application/x-rar-compressed"},
+ &ast.OperatorNode{Value: "OR"},
+ &ast.StringNode{Key: key, Value: "application/x-tar"},
+ &ast.OperatorNode{Value: "OR"},
+ &ast.StringNode{Key: key, Value: "application/x-bzip2"},
+ &ast.OperatorNode{Value: "OR"},
+ &ast.StringNode{Key: key, Value: "application/x-bzip"},
+ &ast.OperatorNode{Value: "OR"},
+ &ast.StringNode{Key: key, Value: "application/x-tgz"},
+ }},
+ },
+ }[fmt.Sprintf("%s:%s", key, value)]
+ if !ok {
+ return nil
+ }
+
+ return result
+}
diff --git a/services/search/pkg/opensearch/internal/convert/kql_expand_test.go b/services/search/pkg/opensearch/internal/convert/kql_expand_test.go
new file mode 100644
index 0000000000..49218d5aa5
--- /dev/null
+++ b/services/search/pkg/opensearch/internal/convert/kql_expand_test.go
@@ -0,0 +1,607 @@
+package convert_test
+
+import (
+ "fmt"
+ "testing"
+
+ "github.com/stretchr/testify/require"
+
+ "github.com/opencloud-eu/opencloud/services/search/pkg/opensearch/internal/convert"
+
+ "github.com/opencloud-eu/opencloud/pkg/ast"
+ "github.com/opencloud-eu/opencloud/services/search/pkg/opensearch/internal/test"
+)
+
+func TestExpandKQLAST(t *testing.T) {
+ t.Run("always converts a value node to a pointer node", func(t *testing.T) {
+ tests := []opensearchtest.TableTest[[]ast.Node, []ast.Node]{
+ {
+ Name: "ast.node.V -> ast.node.PTR",
+ Got: []ast.Node{
+ &ast.StringNode{Key: "a"},
+ &ast.OperatorNode{Value: "AND"},
+ ast.StringNode{Key: "b"},
+ ast.OperatorNode{Value: "AND"},
+ &ast.DateTimeNode{Key: "c"},
+ &ast.OperatorNode{Value: "OR"},
+ ast.DateTimeNode{Key: "d"},
+ ast.OperatorNode{Value: "OR"},
+ &ast.BooleanNode{Key: "f"},
+ &ast.OperatorNode{Value: "NOT"},
+ ast.BooleanNode{Key: "g"},
+ ast.OperatorNode{Value: "NOT"},
+ &ast.GroupNode{Key: "h", Nodes: []ast.Node{
+ &ast.StringNode{Key: "a"},
+ &ast.OperatorNode{Value: "AND"},
+ ast.StringNode{Key: "b"},
+ &ast.OperatorNode{Value: "OR"},
+ &ast.GroupNode{Key: "h", Nodes: []ast.Node{
+ &ast.StringNode{Key: "a"},
+ &ast.OperatorNode{Value: "AND"},
+ ast.StringNode{Key: "b"},
+ &ast.OperatorNode{Value: "OR"},
+ &ast.GroupNode{Key: "h", Nodes: []ast.Node{
+ &ast.StringNode{Key: "a"},
+ &ast.OperatorNode{Value: "AND"},
+ ast.StringNode{Key: "b"},
+ }},
+ }},
+ }},
+ ast.GroupNode{Key: "i", Nodes: []ast.Node{
+ ast.StringNode{Key: "a"},
+ ast.OperatorNode{Value: "AND"},
+ ast.StringNode{Key: "b"},
+ ast.OperatorNode{Value: "OR"},
+ ast.GroupNode{Key: "h", Nodes: []ast.Node{
+ ast.StringNode{Key: "a"},
+ ast.OperatorNode{Value: "AND"},
+ ast.StringNode{Key: "b"},
+ ast.OperatorNode{Value: "OR"},
+ ast.GroupNode{Key: "h", Nodes: []ast.Node{
+ ast.StringNode{Key: "a"},
+ ast.OperatorNode{Value: "AND"},
+ ast.StringNode{Key: "b"},
+ }},
+ }},
+ }},
+ },
+ Want: []ast.Node{
+ &ast.StringNode{Key: "a"},
+ &ast.OperatorNode{Value: "AND"},
+ &ast.StringNode{Key: "b"},
+ &ast.OperatorNode{Value: "AND"},
+ &ast.DateTimeNode{Key: "c"},
+ &ast.OperatorNode{Value: "OR"},
+ &ast.DateTimeNode{Key: "d"},
+ &ast.OperatorNode{Value: "OR"},
+ &ast.BooleanNode{Key: "f"},
+ &ast.OperatorNode{Value: "NOT"},
+ &ast.BooleanNode{Key: "g"},
+ &ast.OperatorNode{Value: "NOT"},
+ &ast.GroupNode{Key: "h", Nodes: []ast.Node{
+ &ast.StringNode{Key: "a"},
+ &ast.OperatorNode{Value: "AND"},
+ &ast.StringNode{Key: "b"},
+ &ast.OperatorNode{Value: "OR"},
+ &ast.GroupNode{Key: "h", Nodes: []ast.Node{
+ &ast.StringNode{Key: "a"},
+ &ast.OperatorNode{Value: "AND"},
+ &ast.StringNode{Key: "b"},
+ &ast.OperatorNode{Value: "OR"},
+ &ast.GroupNode{Key: "h", Nodes: []ast.Node{
+ &ast.StringNode{Key: "a"},
+ &ast.OperatorNode{Value: "AND"},
+ &ast.StringNode{Key: "b"},
+ }},
+ }},
+ }},
+ &ast.GroupNode{Key: "i", Nodes: []ast.Node{
+ &ast.StringNode{Key: "a"},
+ &ast.OperatorNode{Value: "AND"},
+ &ast.StringNode{Key: "b"},
+ &ast.OperatorNode{Value: "OR"},
+ &ast.GroupNode{Key: "h", Nodes: []ast.Node{
+ &ast.StringNode{Key: "a"},
+ &ast.OperatorNode{Value: "AND"},
+ &ast.StringNode{Key: "b"},
+ &ast.OperatorNode{Value: "OR"},
+ &ast.GroupNode{Key: "h", Nodes: []ast.Node{
+ &ast.StringNode{Key: "a"},
+ &ast.OperatorNode{Value: "AND"},
+ &ast.StringNode{Key: "b"},
+ }},
+ }},
+ }},
+ },
+ },
+ }
+
+ for _, test := range tests {
+ t.Run(test.Name, func(t *testing.T) {
+ result, err := convert.ExpandKQL(test.Got)
+ require.NoError(t, err)
+ require.Equal(t, test.Want, result)
+ })
+ }
+ })
+
+ t.Run("remaps some keys", func(t *testing.T) {
+ var tests []opensearchtest.TableTest[[]ast.Node, []ast.Node]
+
+ for k, v := range map[string]string{
+ "": "Name", // Default to "Name" if no key is provided
+ "rootid": "RootID",
+ "path": "Path",
+ "id": "ID",
+ "name": "Name",
+ "size": "Size",
+ "mtime": "Mtime",
+ "mediatype": "MimeType",
+ "type": "Type",
+ "tag": "Tags",
+ "tags": "Tags",
+ "content": "Content",
+ "hidden": "Hidden",
+ "any": "any", // Example of an unknown key that should remain unchanged
+ } {
+ tests = append(tests, opensearchtest.TableTest[[]ast.Node, []ast.Node]{
+ Name: fmt.Sprintf("%s -> %s", k, v),
+ Got: []ast.Node{
+ &ast.StringNode{Key: k},
+ &ast.OperatorNode{Value: "AND"},
+ ast.StringNode{Key: k},
+ ast.OperatorNode{Value: "AND"},
+ &ast.DateTimeNode{Key: k},
+ &ast.OperatorNode{Value: "OR"},
+ ast.DateTimeNode{Key: k},
+ ast.OperatorNode{Value: "OR"},
+ &ast.BooleanNode{Key: k},
+ &ast.OperatorNode{Value: "NOT"},
+ ast.BooleanNode{Key: k},
+ ast.OperatorNode{Value: "NOT"},
+ &ast.GroupNode{Key: k, Nodes: []ast.Node{
+ &ast.StringNode{Key: k},
+ &ast.OperatorNode{Value: "AND"},
+ ast.StringNode{Key: k},
+ &ast.OperatorNode{Value: "OR"},
+ &ast.GroupNode{Key: k, Nodes: []ast.Node{
+ &ast.StringNode{Key: k},
+ &ast.OperatorNode{Value: "AND"},
+ ast.StringNode{Key: k},
+ &ast.OperatorNode{Value: "OR"},
+ &ast.GroupNode{Key: k, Nodes: []ast.Node{
+ &ast.StringNode{Key: k},
+ &ast.OperatorNode{Value: "AND"},
+ ast.StringNode{Key: k},
+ }},
+ }},
+ }},
+ },
+ Want: []ast.Node{
+ &ast.StringNode{Key: v},
+ &ast.OperatorNode{Value: "AND"},
+ &ast.StringNode{Key: v},
+ &ast.OperatorNode{Value: "AND"},
+ &ast.DateTimeNode{Key: v},
+ &ast.OperatorNode{Value: "OR"},
+ &ast.DateTimeNode{Key: v},
+ &ast.OperatorNode{Value: "OR"},
+ &ast.BooleanNode{Key: v},
+ &ast.OperatorNode{Value: "NOT"},
+ &ast.BooleanNode{Key: v},
+ &ast.OperatorNode{Value: "NOT"},
+ &ast.GroupNode{Key: func() string {
+ switch {
+ case k == "":
+ return k
+ default:
+ return v
+ }
+ }(), Nodes: []ast.Node{
+ &ast.StringNode{Key: v},
+ &ast.OperatorNode{Value: "AND"},
+ &ast.StringNode{Key: v},
+ &ast.OperatorNode{Value: "OR"},
+ &ast.GroupNode{Key: func() string {
+ switch {
+ case k == "":
+ return k
+ default:
+ return v
+ }
+ }(), Nodes: []ast.Node{
+ &ast.StringNode{Key: v},
+ &ast.OperatorNode{Value: "AND"},
+ &ast.StringNode{Key: v},
+ &ast.OperatorNode{Value: "OR"},
+ &ast.GroupNode{Key: func() string {
+ switch {
+ case k == "":
+ return k
+ default:
+ return v
+ }
+ }(), Nodes: []ast.Node{
+ &ast.StringNode{Key: v},
+ &ast.OperatorNode{Value: "AND"},
+ &ast.StringNode{Key: v},
+ }},
+ }},
+ }},
+ },
+ })
+ }
+
+ for _, test := range tests {
+ t.Run(test.Name, func(t *testing.T) {
+ result, err := convert.ExpandKQL(test.Got)
+ require.NoError(t, err)
+ require.Equal(t, test.Want, result)
+ })
+ }
+ })
+
+ t.Run("lowercases some values", func(t *testing.T) {
+ tests := []opensearchtest.TableTest[[]ast.Node, []ast.Node]{
+ {
+ Name: "!Hidden: StringNode -> stringnode",
+ Got: []ast.Node{
+ ast.StringNode{Key: "aBc", Value: "StringNode"},
+ ast.GroupNode{Key: "GroupNode", Nodes: []ast.Node{
+ ast.StringNode{Key: "aBc", Value: "StringNode"},
+ }},
+ },
+ Want: []ast.Node{
+ &ast.StringNode{Key: "aBc", Value: "stringnode"},
+ &ast.GroupNode{Key: "GroupNode", Nodes: []ast.Node{
+ &ast.StringNode{Key: "aBc", Value: "stringnode"},
+ }},
+ },
+ },
+ {
+ Name: "Hidden: StringNode -> StringNode",
+ Got: []ast.Node{
+ ast.StringNode{Key: "Hidden", Value: "StringNode"},
+ ast.GroupNode{Key: "GroupNode", Nodes: []ast.Node{
+ ast.StringNode{Key: "Hidden", Value: "StringNode"},
+ }},
+ },
+ Want: []ast.Node{
+ &ast.StringNode{Key: "Hidden", Value: "StringNode"},
+ &ast.GroupNode{Key: "GroupNode", Nodes: []ast.Node{
+ &ast.StringNode{Key: "Hidden", Value: "StringNode"},
+ }},
+ },
+ },
+ }
+
+ for _, test := range tests {
+ t.Run(test.Name, func(t *testing.T) {
+ result, err := convert.ExpandKQL(test.Got)
+ require.NoError(t, err)
+ require.Equal(t, test.Want, result)
+ })
+ }
+ })
+
+ t.Run("unfolds some values", func(t *testing.T) {
+ tests := []opensearchtest.TableTest[[]ast.Node, []ast.Node]{
+ {
+ Name: "MimeType:unknown",
+ Got: []ast.Node{
+ &ast.StringNode{Key: "MimeType", Value: "unknown"},
+ &ast.OperatorNode{Value: "AND"},
+ &ast.StringNode{Key: "Name", Value: "some-name"},
+ },
+ Want: []ast.Node{
+ &ast.StringNode{Key: "MimeType", Value: "unknown"},
+ &ast.OperatorNode{Value: "AND"},
+ &ast.StringNode{Key: "Name", Value: `some-name`},
+ },
+ },
+ {
+ Name: "MimeType:file",
+ Got: []ast.Node{
+ &ast.StringNode{Key: "MimeType", Value: "file"},
+ &ast.OperatorNode{Value: "AND"},
+ &ast.StringNode{Key: "Name", Value: "some-name"},
+ },
+ Want: []ast.Node{
+ &ast.OperatorNode{Value: "NOT"},
+ &ast.StringNode{Key: "MimeType", Value: "httpd/unix-directory"},
+ &ast.OperatorNode{Value: "AND"},
+ &ast.StringNode{Key: "Name", Value: `some-name`},
+ },
+ },
+ {
+ Name: "MimeType:folder",
+ Got: []ast.Node{
+ ast.BooleanNode{Key: "Deleted", Value: false},
+ ast.OperatorNode{Value: "AND"},
+ ast.StringNode{Key: "MimeType", Value: "folder"},
+ ast.OperatorNode{Value: "AND"},
+ ast.StringNode{Value: "some-name"},
+ },
+ Want: []ast.Node{
+ &ast.BooleanNode{Key: "Deleted", Value: false},
+ &ast.OperatorNode{Value: "AND"},
+ &ast.StringNode{Key: "MimeType", Value: "httpd/unix-directory"},
+ &ast.OperatorNode{Value: "AND"},
+ &ast.StringNode{Key: "Name", Value: `some-name`},
+ },
+ },
+ {
+ Name: "MimeType:document",
+ Got: []ast.Node{
+ ast.BooleanNode{Key: "Deleted", Value: false},
+ ast.OperatorNode{Value: "AND"},
+ ast.StringNode{Key: "MimeType", Value: "document"},
+ ast.OperatorNode{Value: "AND"},
+ ast.StringNode{Value: "some-name"},
+ },
+ Want: []ast.Node{
+ &ast.BooleanNode{Key: "Deleted", Value: false},
+ &ast.OperatorNode{Value: "AND"},
+ &ast.GroupNode{Nodes: []ast.Node{
+ &ast.StringNode{Key: "MimeType", Value: "application/msword"},
+ &ast.OperatorNode{Value: "OR"},
+ &ast.StringNode{Key: "MimeType", Value: "application/vnd.openxmlformats-officedocument.wordprocessingml.document"},
+ &ast.OperatorNode{Value: "OR"},
+ &ast.StringNode{Key: "MimeType", Value: "application/vnd.openxmlformats-officedocument.wordprocessingml.form"},
+ &ast.OperatorNode{Value: "OR"},
+ &ast.StringNode{Key: "MimeType", Value: "application/vnd.oasis.opendocument.text"},
+ &ast.OperatorNode{Value: "OR"},
+ &ast.StringNode{Key: "MimeType", Value: "text/plain"},
+ &ast.OperatorNode{Value: "OR"},
+ &ast.StringNode{Key: "MimeType", Value: "text/markdown"},
+ &ast.OperatorNode{Value: "OR"},
+ &ast.StringNode{Key: "MimeType", Value: "application/rtf"},
+ &ast.OperatorNode{Value: "OR"},
+ &ast.StringNode{Key: "MimeType", Value: "application/vnd.apple.pages"},
+ }},
+ &ast.OperatorNode{Value: "AND"},
+ &ast.StringNode{Key: "Name", Value: `some-name`},
+ },
+ },
+ {
+ Name: "MimeType:spreadsheet",
+ Got: []ast.Node{
+ ast.BooleanNode{Key: "Deleted", Value: false},
+ ast.OperatorNode{Value: "AND"},
+ ast.StringNode{Key: "MimeType", Value: "spreadsheet"},
+ ast.OperatorNode{Value: "AND"},
+ ast.StringNode{Value: "some-name"},
+ },
+ Want: []ast.Node{
+ &ast.BooleanNode{Key: "Deleted", Value: false},
+ &ast.OperatorNode{Value: "AND"},
+ &ast.GroupNode{Nodes: []ast.Node{
+ &ast.StringNode{Key: "MimeType", Value: "application/vnd.ms-excel"},
+ &ast.OperatorNode{Value: "OR"},
+ &ast.StringNode{Key: "MimeType", Value: "application/vnd.oasis.opendocument.spreadsheet"},
+ &ast.OperatorNode{Value: "OR"},
+ &ast.StringNode{Key: "MimeType", Value: "text/csv"},
+ &ast.OperatorNode{Value: "OR"},
+ &ast.StringNode{Key: "MimeType", Value: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"},
+ &ast.OperatorNode{Value: "OR"},
+ &ast.StringNode{Key: "MimeType", Value: "application/vnd.oasis.opendocument.spreadshee"},
+ &ast.OperatorNode{Value: "OR"},
+ &ast.StringNode{Key: "MimeType", Value: "application/vnd.apple.numbers"},
+ }},
+ &ast.OperatorNode{Value: "AND"},
+ &ast.StringNode{Key: "Name", Value: `some-name`},
+ },
+ },
+ {
+ Name: "MimeType:presentation",
+ Got: []ast.Node{
+ ast.BooleanNode{Key: "Deleted", Value: false},
+ ast.OperatorNode{Value: "AND"},
+ ast.StringNode{Key: "MimeType", Value: "presentation"},
+ ast.OperatorNode{Value: "AND"},
+ ast.StringNode{Value: "some-name"},
+ },
+ Want: []ast.Node{
+ &ast.BooleanNode{Key: "Deleted", Value: false},
+ &ast.OperatorNode{Value: "AND"},
+ &ast.GroupNode{Nodes: []ast.Node{
+ &ast.StringNode{Key: "MimeType", Value: "application/vnd.openxmlformats-officedocument.presentationml.presentation"},
+ &ast.OperatorNode{Value: "OR"},
+ &ast.StringNode{Key: "MimeType", Value: "application/vnd.oasis.opendocument.presentation"},
+ &ast.OperatorNode{Value: "OR"},
+ &ast.StringNode{Key: "MimeType", Value: "application/vnd.ms-powerpoint"},
+ &ast.OperatorNode{Value: "OR"},
+ &ast.StringNode{Key: "MimeType", Value: "application/vnd.apple.keynote"},
+ }},
+ &ast.OperatorNode{Value: "AND"},
+ &ast.StringNode{Key: "Name", Value: `some-name`},
+ },
+ },
+ {
+ Name: "MimeType:pdf",
+ Got: []ast.Node{
+ ast.BooleanNode{Key: "Deleted", Value: false},
+ ast.OperatorNode{Value: "AND"},
+ ast.StringNode{Key: "MimeType", Value: "pdf"},
+ ast.OperatorNode{Value: "AND"},
+ ast.StringNode{Value: "some-name"},
+ },
+ Want: []ast.Node{
+ &ast.BooleanNode{Key: "Deleted", Value: false},
+ &ast.OperatorNode{Value: "AND"},
+ &ast.StringNode{Key: "MimeType", Value: "application/pdf"},
+ &ast.OperatorNode{Value: "AND"},
+ &ast.StringNode{Key: "Name", Value: `some-name`},
+ },
+ },
+ {
+ Name: "MimeType:image",
+ Got: []ast.Node{
+ ast.BooleanNode{Key: "Deleted", Value: false},
+ ast.OperatorNode{Value: "AND"},
+ ast.StringNode{Key: "MimeType", Value: "image"},
+ ast.OperatorNode{Value: "AND"},
+ ast.StringNode{Value: "some-name"},
+ },
+ Want: []ast.Node{
+ &ast.BooleanNode{Key: "Deleted", Value: false},
+ &ast.OperatorNode{Value: "AND"},
+ &ast.StringNode{Key: "MimeType", Value: "image/*"},
+ &ast.OperatorNode{Value: "AND"},
+ &ast.StringNode{Key: "Name", Value: `some-name`},
+ },
+ },
+ {
+ Name: "MimeType:video",
+ Got: []ast.Node{
+ ast.BooleanNode{Key: "Deleted", Value: false},
+ ast.OperatorNode{Value: "AND"},
+ ast.StringNode{Key: "MimeType", Value: "video"},
+ ast.OperatorNode{Value: "AND"},
+ ast.StringNode{Value: "some-name"},
+ },
+ Want: []ast.Node{
+ &ast.BooleanNode{Key: "Deleted", Value: false},
+ &ast.OperatorNode{Value: "AND"},
+ &ast.StringNode{Key: "MimeType", Value: "video/*"},
+ &ast.OperatorNode{Value: "AND"},
+ &ast.StringNode{Key: "Name", Value: `some-name`},
+ },
+ },
+ {
+ Name: "MimeType:audio",
+ Got: []ast.Node{
+ ast.BooleanNode{Key: "Deleted", Value: false},
+ ast.OperatorNode{Value: "AND"},
+ ast.StringNode{Key: "MimeType", Value: "audio"},
+ ast.OperatorNode{Value: "AND"},
+ ast.StringNode{Value: "some-name"},
+ },
+ Want: []ast.Node{
+ &ast.BooleanNode{Key: "Deleted", Value: false},
+ &ast.OperatorNode{Value: "AND"},
+ &ast.StringNode{Key: "MimeType", Value: "audio/*"},
+ &ast.OperatorNode{Value: "AND"},
+ &ast.StringNode{Key: "Name", Value: `some-name`},
+ },
+ },
+ {
+ Name: "MimeType:archive",
+ Got: []ast.Node{
+ ast.BooleanNode{Key: "Deleted", Value: false},
+ ast.OperatorNode{Value: "AND"},
+ ast.StringNode{Key: "MimeType", Value: "archive"},
+ ast.OperatorNode{Value: "AND"},
+ ast.StringNode{Value: "some-name"},
+ },
+ Want: []ast.Node{
+ &ast.BooleanNode{Key: "Deleted", Value: false},
+ &ast.OperatorNode{Value: "AND"},
+ &ast.GroupNode{Nodes: []ast.Node{
+ &ast.StringNode{Key: "MimeType", Value: "application/zip"},
+ &ast.OperatorNode{Value: "OR"},
+ &ast.StringNode{Key: "MimeType", Value: "application/gzip"},
+ &ast.OperatorNode{Value: "OR"},
+ &ast.StringNode{Key: "MimeType", Value: "application/x-gzip"},
+ &ast.OperatorNode{Value: "OR"},
+ &ast.StringNode{Key: "MimeType", Value: "application/x-7z-compressed"},
+ &ast.OperatorNode{Value: "OR"},
+ &ast.StringNode{Key: "MimeType", Value: "application/x-rar-compressed"},
+ &ast.OperatorNode{Value: "OR"},
+ &ast.StringNode{Key: "MimeType", Value: "application/x-tar"},
+ &ast.OperatorNode{Value: "OR"},
+ &ast.StringNode{Key: "MimeType", Value: "application/x-bzip2"},
+ &ast.OperatorNode{Value: "OR"},
+ &ast.StringNode{Key: "MimeType", Value: "application/x-bzip"},
+ &ast.OperatorNode{Value: "OR"},
+ &ast.StringNode{Key: "MimeType", Value: "application/x-tgz"},
+ }},
+ &ast.OperatorNode{Value: "AND"},
+ &ast.StringNode{Key: "Name", Value: `some-name`},
+ },
+ },
+ }
+
+ for _, test := range tests {
+ t.Run(test.Name, func(t *testing.T) {
+ if test.Skip {
+ t.Skip("Skipping test due to known issue")
+ }
+ result, err := convert.ExpandKQL(test.Got)
+ require.NoError(t, err)
+ require.EqualValues(t, test.Want, result)
+ })
+ }
+ })
+
+ t.Run("different cases", func(t *testing.T) {
+ tests := []opensearchtest.TableTest[[]ast.Node, []ast.Node]{
+ {
+ Name: "use the group node key as default key",
+ Got: []ast.Node{
+ &ast.GroupNode{Nodes: []ast.Node{
+ &ast.StringNode{Value: "b"},
+ &ast.OperatorNode{Value: "OR"},
+ &ast.StringNode{Key: "c", Value: "d"},
+ }},
+ &ast.OperatorNode{Value: "AND"},
+ &ast.GroupNode{Key: "a", Nodes: []ast.Node{
+ &ast.StringNode{Value: "b"},
+ &ast.OperatorNode{Value: "OR"},
+ &ast.StringNode{Key: "c", Value: "d"},
+ }},
+ &ast.OperatorNode{Value: "AND"},
+ &ast.GroupNode{Key: "mediatype", Nodes: []ast.Node{
+ &ast.StringNode{Value: "file"},
+ &ast.OperatorNode{Value: "OR"},
+ &ast.StringNode{Key: "c", Value: "d"},
+ }},
+ &ast.OperatorNode{Value: "AND"},
+ &ast.GroupNode{Nodes: []ast.Node{
+ &ast.StringNode{Key: "mediatype", Value: "file"},
+ &ast.OperatorNode{Value: "OR"},
+ &ast.StringNode{Key: "c", Value: "d"},
+ }},
+ },
+ Want: []ast.Node{
+ &ast.GroupNode{Nodes: []ast.Node{
+ &ast.StringNode{Key: "Name", Value: "b"},
+ &ast.OperatorNode{Value: "OR"},
+ &ast.StringNode{Key: "c", Value: "d"},
+ }},
+ &ast.OperatorNode{Value: "AND"},
+ &ast.GroupNode{Key: "a", Nodes: []ast.Node{
+ &ast.StringNode{Key: "a", Value: "b"},
+ &ast.OperatorNode{Value: "OR"},
+ &ast.StringNode{Key: "c", Value: "d"},
+ }},
+ &ast.OperatorNode{Value: "AND"},
+ &ast.GroupNode{Key: "MimeType", Nodes: []ast.Node{
+ &ast.OperatorNode{Value: "NOT"},
+ &ast.StringNode{Key: "MimeType", Value: "httpd/unix-directory"},
+ &ast.OperatorNode{Value: "OR"},
+ &ast.StringNode{Key: "c", Value: "d"},
+ }},
+ &ast.OperatorNode{Value: "AND"},
+ &ast.GroupNode{Nodes: []ast.Node{
+ &ast.OperatorNode{Value: "NOT"},
+ &ast.StringNode{Key: "MimeType", Value: "httpd/unix-directory"},
+ &ast.OperatorNode{Value: "OR"},
+ &ast.StringNode{Key: "c", Value: "d"},
+ }},
+ },
+ },
+ }
+
+ for _, test := range tests {
+ t.Run(test.Name, func(t *testing.T) {
+ if test.Skip {
+ t.Skip("Skipping test due to known issue")
+ }
+ result, err := convert.ExpandKQL(test.Got)
+ require.NoError(t, err)
+ require.EqualValues(t, test.Want, result)
+ })
+ }
+ })
+}
diff --git a/services/search/pkg/opensearch/internal/convert/kql_query.go b/services/search/pkg/opensearch/internal/convert/kql_query.go
new file mode 100644
index 0000000000..f04e577077
--- /dev/null
+++ b/services/search/pkg/opensearch/internal/convert/kql_query.go
@@ -0,0 +1,35 @@
+package convert
+
+import (
+ "fmt"
+
+ "github.com/opencloud-eu/opencloud/pkg/kql"
+ "github.com/opencloud-eu/opencloud/services/search/pkg/opensearch/internal/osu"
+)
+
+var (
+ ErrUnsupportedNodeType = fmt.Errorf("unsupported node type")
+)
+
+func KQLToOpenSearchBoolQuery(kqlQuery string) (*osu.BoolQuery, error) {
+ kqlAst, err := kql.Builder{}.Build(kqlQuery)
+ if err != nil {
+ return nil, fmt.Errorf("failed to build query: %w", err)
+ }
+
+ kqlNodes, err := ExpandKQL(kqlAst.Nodes)
+ if err != nil {
+ return nil, fmt.Errorf("failed to expand KQL AST nodes: %w", err)
+ }
+
+ builder, err := TranspileKQLToOpenSearch(kqlNodes)
+ if err != nil {
+ return nil, fmt.Errorf("failed to compile query: %w", err)
+ }
+
+ if q, ok := builder.(*osu.BoolQuery); !ok {
+ return osu.NewBoolQuery().Must(builder), nil
+ } else {
+ return q, nil
+ }
+}
diff --git a/services/search/pkg/opensearch/internal/convert/kql_query_test.go b/services/search/pkg/opensearch/internal/convert/kql_query_test.go
new file mode 100644
index 0000000000..3c03b5e0cd
--- /dev/null
+++ b/services/search/pkg/opensearch/internal/convert/kql_query_test.go
@@ -0,0 +1 @@
+package convert_test
diff --git a/services/search/pkg/opensearch/internal/convert/kql_transpile.go b/services/search/pkg/opensearch/internal/convert/kql_transpile.go
new file mode 100644
index 0000000000..e06df753c6
--- /dev/null
+++ b/services/search/pkg/opensearch/internal/convert/kql_transpile.go
@@ -0,0 +1,147 @@
+package convert
+
+import (
+ "errors"
+ "fmt"
+ "strings"
+ "time"
+
+ "github.com/opencloud-eu/opencloud/pkg/ast"
+ "github.com/opencloud-eu/opencloud/pkg/kql"
+ "github.com/opencloud-eu/opencloud/services/search/pkg/opensearch/internal/osu"
+)
+
+func TranspileKQLToOpenSearch(nodes []ast.Node) (osu.Builder, error) {
+ return kqlOpensearchTranspiler{}.Transpile(nodes)
+}
+
+type kqlOpensearchTranspiler struct{}
+
+func (t kqlOpensearchTranspiler) Transpile(nodes []ast.Node) (osu.Builder, error) {
+ q, err := t.transpile(nodes)
+ if err != nil {
+ return nil, err
+ }
+
+ return q, nil
+}
+
+func (t kqlOpensearchTranspiler) transpile(nodes []ast.Node) (osu.Builder, error) {
+ if len(nodes) == 0 {
+ return nil, fmt.Errorf("no nodes to compile")
+ }
+
+ if len(nodes) == 1 {
+ builder, err := t.toBuilder(nodes[0])
+ if err != nil {
+ return nil, fmt.Errorf("failed to get builder for single node: %w", err)
+ }
+ return builder, nil
+ }
+
+ boolQueryParams := &osu.BoolQueryParams{}
+ boolQuery := osu.NewBoolQuery().Params(boolQueryParams)
+ boolQueryAdd := boolQuery.Must
+ for i, node := range nodes {
+ nextOp := t.getOperatorValueAt(nodes, i+1)
+ prevOp := t.getOperatorValueAt(nodes, i-1)
+
+ switch {
+ case nextOp == kql.BoolOR:
+ boolQueryAdd = boolQuery.Should
+ case nextOp == kql.BoolAND:
+ boolQueryAdd = boolQuery.Must
+ case prevOp == kql.BoolNOT:
+ boolQueryAdd = boolQuery.MustNot
+ }
+
+ builder, err := t.toBuilder(node)
+ switch {
+ // if the node is not known, we skip it, such as an operator node
+ case errors.Is(err, ErrUnsupportedNodeType):
+ continue
+ case err != nil:
+ return nil, fmt.Errorf("failed to get builder for node %T: %w", node, err)
+ }
+
+ if _, ok := node.(*ast.OperatorNode); ok {
+ // operatorNodes are not builders, so we skip them
+ continue
+ }
+
+ if nextOp == kql.BoolOR {
+ // if there are should clauses, we set the minimum should match to 1
+ boolQueryParams.MinimumShouldMatch = 1
+ }
+
+ boolQueryAdd(builder)
+ }
+
+ return boolQuery, nil
+}
+
+func (t kqlOpensearchTranspiler) getOperatorValueAt(nodes []ast.Node, i int) string {
+ if i < 0 || i >= len(nodes) {
+ return ""
+ }
+
+ if opn, ok := nodes[i].(*ast.OperatorNode); ok {
+ return opn.Value
+ }
+
+ return ""
+}
+
+func (t kqlOpensearchTranspiler) toBuilder(node ast.Node) (osu.Builder, error) {
+ var builder osu.Builder
+
+ switch node := node.(type) {
+ case *ast.BooleanNode:
+ return osu.NewTermQuery[bool](node.Key).Value(node.Value), nil
+ case *ast.StringNode:
+ isWildcard := strings.Contains(node.Value, "*")
+ if isWildcard {
+ return osu.NewWildcardQuery(node.Key).Value(node.Value), nil
+ }
+
+ totalTerms := strings.Split(node.Value, " ")
+ isSingleTerm := len(totalTerms) == 1
+ isMultiTerm := len(totalTerms) >= 1
+ switch {
+ case isSingleTerm:
+ return osu.NewTermQuery[string](node.Key).Value(node.Value), nil
+ case isMultiTerm:
+ return osu.NewMatchPhraseQuery(node.Key).Query(node.Value), nil
+ }
+
+ return nil, fmt.Errorf("unsupported string node value: %s", node.Value)
+ case *ast.DateTimeNode:
+ if node.Operator == nil {
+ return builder, fmt.Errorf("date time node without operator: %w", ErrUnsupportedNodeType)
+ }
+
+ query := osu.NewRangeQuery[time.Time](node.Key)
+
+ switch node.Operator.Value {
+ case ">":
+ return query.Gt(node.Value), nil
+ case ">=":
+ return query.Gte(node.Value), nil
+ case "<":
+ return query.Lt(node.Value), nil
+ case "<=":
+ return query.Lte(node.Value), nil
+ }
+
+ return nil, fmt.Errorf("unsupported operator %s for date time node: %w", node.Operator.Value, ErrUnsupportedNodeType)
+ case *ast.GroupNode:
+ group, err := t.transpile(node.Nodes)
+ if err != nil {
+ return nil, fmt.Errorf("failed to build group: %w", err)
+ }
+
+ return group, nil
+ }
+
+ return nil, fmt.Errorf("%w: %T", ErrUnsupportedNodeType, node)
+}
diff --git a/services/search/pkg/opensearch/internal/convert/kql_transpile_test.go b/services/search/pkg/opensearch/internal/convert/kql_transpile_test.go
new file mode 100644
index 0000000000..84d3905175
--- /dev/null
+++ b/services/search/pkg/opensearch/internal/convert/kql_transpile_test.go
@@ -0,0 +1,378 @@
+package convert_test
+
+import (
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/assert"
+
+ "github.com/opencloud-eu/opencloud/pkg/ast"
+ "github.com/opencloud-eu/opencloud/services/search/pkg/opensearch/internal/convert"
+ "github.com/opencloud-eu/opencloud/services/search/pkg/opensearch/internal/osu"
+ "github.com/opencloud-eu/opencloud/services/search/pkg/opensearch/internal/test"
+)
+
+func TestTranspileKQLToOpenSearch(t *testing.T) {
+ tests := []opensearchtest.TableTest[*ast.Ast, osu.Builder]{
+ // kql to os dsl - type tests
+ {
+ Name: "term query - string node",
+ Got: &ast.Ast{
+ Nodes: []ast.Node{
+ &ast.StringNode{Key: "Name", Value: "openCloud"},
+ },
+ },
+ Want: osu.NewTermQuery[string]("Name").Value("openCloud"),
+ },
+ {
+ Name: "term query - boolean node - true",
+ Got: &ast.Ast{
+ Nodes: []ast.Node{
+ &ast.BooleanNode{Key: "Deleted", Value: true},
+ },
+ },
+ Want: osu.NewTermQuery[bool]("Deleted").Value(true),
+ },
+ {
+ Name: "term query - boolean node - false",
+ Got: &ast.Ast{
+ Nodes: []ast.Node{
+ &ast.BooleanNode{Key: "Deleted", Value: false},
+ },
+ },
+ Want: osu.NewTermQuery[bool]("Deleted").Value(false),
+ },
+ {
+ Name: "match-phrase query - string node",
+ Got: &ast.Ast{
+ Nodes: []ast.Node{
+ &ast.StringNode{Key: "Name", Value: "open cloud"},
+ },
+ },
+ Want: osu.NewMatchPhraseQuery("Name").Query(`open cloud`),
+ },
+ {
+ Name: "wildcard query - string node",
+ Got: &ast.Ast{
+ Nodes: []ast.Node{
+ &ast.StringNode{Key: "Name", Value: "open*"},
+ },
+ },
+ Want: osu.NewWildcardQuery("Name").Value("open*"),
+ },
+ {
+ Name: "bool query",
+ Got: &ast.Ast{
+ Nodes: []ast.Node{
+ &ast.GroupNode{Nodes: []ast.Node{
+ &ast.StringNode{Key: "Name", Value: "a"},
+ &ast.StringNode{Key: "Name", Value: "b"},
+ }},
+ },
+ },
+ Want: osu.NewBoolQuery().Must(
+ osu.NewTermQuery[string]("Name").Value("a"),
+ osu.NewTermQuery[string]("Name").Value("b"),
+ ),
+ },
+ {
+ Name: "no bool query for single term",
+ Got: &ast.Ast{
+ Nodes: []ast.Node{
+ &ast.GroupNode{Nodes: []ast.Node{
+ &ast.StringNode{Key: "Name", Value: "any"},
+ }},
+ },
+ },
+ Want: osu.NewTermQuery[string]("Name").Value("any"),
+ },
+ {
+ Name: "range query >",
+ Got: &ast.Ast{
+ Nodes: []ast.Node{
+ &ast.DateTimeNode{
+ Key: "Mtime",
+ Operator: &ast.OperatorNode{Value: ">"},
+ Value: opensearchtest.TimeMustParse(t, "2023-09-05T08:42:11.23554+02:00"),
+ },
+ },
+ },
+ Want: osu.NewRangeQuery[time.Time]("Mtime").Gt(opensearchtest.TimeMustParse(t, "2023-09-05T08:42:11.23554+02:00")),
+ },
+ {
+ Name: "range query >=",
+ Got: &ast.Ast{
+ Nodes: []ast.Node{
+ &ast.DateTimeNode{
+ Key: "Mtime",
+ Operator: &ast.OperatorNode{Value: ">="},
+ Value: opensearchtest.TimeMustParse(t, "2023-09-05T08:42:11.23554+02:00"),
+ },
+ },
+ },
+ Want: osu.NewRangeQuery[time.Time]("Mtime").Gte(opensearchtest.TimeMustParse(t, "2023-09-05T08:42:11.23554+02:00")),
+ },
+ {
+ Name: "range query <",
+ Got: &ast.Ast{
+ Nodes: []ast.Node{
+ &ast.DateTimeNode{
+ Key: "Mtime",
+ Operator: &ast.OperatorNode{Value: "<"},
+ Value: opensearchtest.TimeMustParse(t, "2023-09-05T08:42:11.23554+02:00"),
+ },
+ },
+ },
+ Want: osu.NewRangeQuery[time.Time]("Mtime").Lt(opensearchtest.TimeMustParse(t, "2023-09-05T08:42:11.23554+02:00")),
+ },
+ {
+ Name: "range query <=",
+ Got: &ast.Ast{
+ Nodes: []ast.Node{
+ &ast.DateTimeNode{
+ Key: "Mtime",
+ Operator: &ast.OperatorNode{Value: "<="},
+ Value: opensearchtest.TimeMustParse(t, "2023-09-05T08:42:11.23554+02:00"),
+ },
+ },
+ },
+ Want: osu.NewRangeQuery[time.Time]("Mtime").Lte(opensearchtest.TimeMustParse(t, "2023-09-05T08:42:11.23554+02:00")),
+ },
+ // kql to os dsl - structure tests
+ {
+ Name: "[*]",
+ Got: &ast.Ast{
+ Nodes: []ast.Node{
+ &ast.StringNode{Key: "Name", Value: "openCloud"},
+ },
+ },
+ Want: osu.NewTermQuery[string]("Name").Value("openCloud"),
+ },
+ {
+ Name: "[* *]",
+ Got: &ast.Ast{
+ Nodes: []ast.Node{
+ &ast.StringNode{Key: "Name", Value: "openCloud"},
+ &ast.StringNode{Key: "age", Value: "32"},
+ },
+ },
+ Want: osu.NewBoolQuery().
+ Must(
+ osu.NewTermQuery[string]("Name").Value("openCloud"),
+ osu.NewTermQuery[string]("age").Value("32"),
+ ),
+ },
+ {
+ Name: "[* AND *]",
+ Got: &ast.Ast{
+ Nodes: []ast.Node{
+ &ast.StringNode{Key: "Name", Value: "openCloud"},
+ &ast.OperatorNode{Value: "AND"},
+ &ast.StringNode{Key: "age", Value: "32"},
+ },
+ },
+ Want: osu.NewBoolQuery().
+ Must(
+ osu.NewTermQuery[string]("Name").Value("openCloud"),
+ osu.NewTermQuery[string]("age").Value("32"),
+ ),
+ },
+ {
+ Name: "[* OR *]",
+ Got: &ast.Ast{
+ Nodes: []ast.Node{
+ &ast.StringNode{Key: "Name", Value: "openCloud"},
+ &ast.OperatorNode{Value: "OR"},
+ &ast.StringNode{Key: "age", Value: "32"},
+ },
+ },
+ Want: osu.NewBoolQuery().
+ Params(&osu.BoolQueryParams{MinimumShouldMatch: 1}).
+ Should(
+ osu.NewTermQuery[string]("Name").Value("openCloud"),
+ osu.NewTermQuery[string]("age").Value("32"),
+ ),
+ },
+ {
+ Name: "[NOT *]",
+ Got: &ast.Ast{
+ Nodes: []ast.Node{
+ &ast.OperatorNode{Value: "NOT"},
+ &ast.StringNode{Key: "age", Value: "32"},
+ },
+ },
+ Want: osu.NewBoolQuery().
+ MustNot(
+ osu.NewTermQuery[string]("age").Value("32"),
+ ),
+ },
+ {
+ Name: "[* NOT *]",
+ Got: &ast.Ast{
+ Nodes: []ast.Node{
+ &ast.StringNode{Key: "Name", Value: "openCloud"},
+ &ast.OperatorNode{Value: "NOT"},
+ &ast.StringNode{Key: "age", Value: "32"},
+ },
+ },
+ Want: osu.NewBoolQuery().
+ Must(
+ osu.NewTermQuery[string]("Name").Value("openCloud"),
+ ).
+ MustNot(
+ osu.NewTermQuery[string]("age").Value("32"),
+ ),
+ },
+ {
+ Name: "[* OR * OR *]",
+ Got: &ast.Ast{
+ Nodes: []ast.Node{
+ &ast.StringNode{Key: "Name", Value: "openCloud"},
+ &ast.OperatorNode{Value: "OR"},
+ &ast.StringNode{Key: "age", Value: "32"},
+ &ast.OperatorNode{Value: "OR"},
+ &ast.StringNode{Key: "age", Value: "44"},
+ },
+ },
+ Want: osu.NewBoolQuery().
+ Params(&osu.BoolQueryParams{MinimumShouldMatch: 1}).
+ Should(
+ osu.NewTermQuery[string]("Name").Value("openCloud"),
+ osu.NewTermQuery[string]("age").Value("32"),
+ osu.NewTermQuery[string]("age").Value("44"),
+ ),
+ },
+ {
+ Name: "[* AND * OR *]",
+ Got: &ast.Ast{
+ Nodes: []ast.Node{
+ &ast.StringNode{Key: "a", Value: "a"},
+ &ast.OperatorNode{Value: "AND"},
+ &ast.StringNode{Key: "b", Value: "b"},
+ &ast.OperatorNode{Value: "OR"},
+ &ast.StringNode{Key: "c", Value: "c"},
+ },
+ },
+ Want: osu.NewBoolQuery().
+ Params(&osu.BoolQueryParams{MinimumShouldMatch: 1}).
+ Must(
+ osu.NewTermQuery[string]("a").Value("a"),
+ ).
+ Should(
+ osu.NewTermQuery[string]("b").Value("b"),
+ osu.NewTermQuery[string]("c").Value("c"),
+ ),
+ },
+ {
+ Name: "[* OR * AND *]",
+ Got: &ast.Ast{
+ Nodes: []ast.Node{
+ &ast.StringNode{Key: "a", Value: "a"},
+ &ast.OperatorNode{Value: "OR"},
+ &ast.StringNode{Key: "b", Value: "b"},
+ &ast.OperatorNode{Value: "AND"},
+ &ast.StringNode{Key: "c", Value: "c"},
+ },
+ },
+ Want: osu.NewBoolQuery().
+ Params(&osu.BoolQueryParams{MinimumShouldMatch: 1}).
+ Must(
+ osu.NewTermQuery[string]("b").Value("b"),
+ osu.NewTermQuery[string]("c").Value("c"),
+ ).
+ Should(
+ osu.NewTermQuery[string]("a").Value("a"),
+ ),
+ },
+ {
+ Name: "[* OR * AND *]",
+ Got: &ast.Ast{
+ Nodes: []ast.Node{
+ &ast.StringNode{Key: "a", Value: "a"},
+ &ast.OperatorNode{Value: "OR"},
+ &ast.StringNode{Key: "b", Value: "b"},
+ &ast.OperatorNode{Value: "AND"},
+ &ast.StringNode{Key: "c", Value: "c"},
+ },
+ },
+ Want: osu.NewBoolQuery().
+ Params(&osu.BoolQueryParams{MinimumShouldMatch: 1}).
+ Should(
+ osu.NewTermQuery[string]("a").Value("a"),
+ ).
+ Must(
+ osu.NewTermQuery[string]("b").Value("b"),
+ osu.NewTermQuery[string]("c").Value("c"),
+ ),
+ },
+ {
+ Name: "[[* OR * OR *] AND *]",
+ Got: &ast.Ast{
+ Nodes: []ast.Node{
+ &ast.GroupNode{Nodes: []ast.Node{
+ &ast.StringNode{Key: "a", Value: "a"},
+ &ast.OperatorNode{Value: "OR"},
+ &ast.StringNode{Key: "b", Value: "b"},
+ &ast.OperatorNode{Value: "OR"},
+ &ast.StringNode{Key: "c", Value: "c"},
+ }},
+ &ast.OperatorNode{Value: "AND"},
+ &ast.StringNode{Key: "d", Value: "d"},
+ },
+ },
+ Want: osu.NewBoolQuery().
+ Must(
+ osu.NewBoolQuery().
+ Params(&osu.BoolQueryParams{MinimumShouldMatch: 1}).
+ Should(
+ osu.NewTermQuery[string]("a").Value("a"),
+ osu.NewTermQuery[string]("b").Value("b"),
+ osu.NewTermQuery[string]("c").Value("c"),
+ ),
+ osu.NewTermQuery[string]("d").Value("d"),
+ ),
+ },
+ {
+ Name: "[[* OR * OR *] AND NOT *]",
+ Got: &ast.Ast{
+ Nodes: []ast.Node{
+ &ast.GroupNode{Nodes: []ast.Node{
+ &ast.StringNode{Key: "a", Value: "a"},
+ &ast.OperatorNode{Value: "OR"},
+ &ast.StringNode{Key: "b", Value: "b"},
+ &ast.OperatorNode{Value: "OR"},
+ &ast.StringNode{Key: "c", Value: "c"},
+ }},
+ &ast.OperatorNode{Value: "AND"},
+ &ast.OperatorNode{Value: "NOT"},
+ &ast.StringNode{Key: "d", Value: "d"},
+ },
+ },
+ Want: osu.NewBoolQuery().
+ Must(
+ osu.NewBoolQuery().
+ Params(&osu.BoolQueryParams{MinimumShouldMatch: 1}).
+ Should(
+ osu.NewTermQuery[string]("a").Value("a"),
+ osu.NewTermQuery[string]("b").Value("b"),
+ osu.NewTermQuery[string]("c").Value("c"),
+ ),
+ ).MustNot(
+ osu.NewTermQuery[string]("d").Value("d"),
+ ),
+ },
+ }
+
+ for _, test := range tests {
+ t.Run(test.Name, func(t *testing.T) {
+ if test.Skip {
+ t.Skip("skipping test: " + test.Name)
+ }
+
+ dsl, err := convert.TranspileKQLToOpenSearch(test.Got.Nodes)
+ assert.NoError(t, err)
+
+ assert.JSONEq(t, opensearchtest.JSONMustMarshal(t, test.Want), opensearchtest.JSONMustMarshal(t, dsl))
+ })
+ }
+}
diff --git a/services/search/pkg/opensearch/internal/convert/opensearch.go b/services/search/pkg/opensearch/internal/convert/opensearch.go
new file mode 100644
index 0000000000..8febaaf7ec
--- /dev/null
+++ b/services/search/pkg/opensearch/internal/convert/opensearch.go
@@ -0,0 +1,98 @@
+package convert
+
+import (
+ "fmt"
+ "strings"
+ "time"
+
+ "github.com/opencloud-eu/reva/v2/pkg/storagespace"
+ opensearchgoAPI "github.com/opensearch-project/opensearch-go/v4/opensearchapi"
+ "google.golang.org/protobuf/types/known/timestamppb"
+
+ "github.com/opencloud-eu/opencloud/pkg/conversions"
+ searchMessage "github.com/opencloud-eu/opencloud/protogen/gen/opencloud/messages/search/v0"
+ "github.com/opencloud-eu/opencloud/services/search/pkg/engine"
+)
+
+func OpenSearchHitToMatch(hit opensearchgoAPI.SearchHit) (*searchMessage.Match, error) {
+ resource, err := conversions.To[engine.Resource](hit.Source)
+ if err != nil {
+ return nil, fmt.Errorf("failed to convert hit source: %w", err)
+ }
+
+ resourceRootID, err := storagespace.ParseID(resource.RootID)
+ if err != nil {
+ return nil, err
+ }
+
+ resourceID, err := storagespace.ParseID(resource.ID)
+ if err != nil {
+ return nil, err
+ }
+
+ resourceParentID, _ := storagespace.ParseID(resource.ParentID)
+
+ match := &searchMessage.Match{
+ Score: hit.Score,
+ Entity: &searchMessage.Entity{
+ Ref: &searchMessage.Reference{
+ ResourceId: &searchMessage.ResourceID{
+ StorageId: resourceRootID.GetStorageId(),
+ SpaceId: resourceRootID.GetSpaceId(),
+ OpaqueId: resourceRootID.GetOpaqueId(),
+ },
+ Path: resource.Path,
+ },
+ Id: &searchMessage.ResourceID{
+ StorageId: resourceID.GetStorageId(),
+ SpaceId: resourceID.GetSpaceId(),
+ OpaqueId: resourceID.GetOpaqueId(),
+ },
+ Name: resource.Name,
+ ParentId: &searchMessage.ResourceID{
+ StorageId: resourceParentID.GetStorageId(),
+ SpaceId: resourceParentID.GetSpaceId(),
+ OpaqueId: resourceParentID.GetOpaqueId(),
+ },
+ Size: resource.Size,
+ Type: resource.Type,
+ MimeType: resource.MimeType,
+ Deleted: resource.Deleted,
+ Tags: resource.Tags,
+ Highlights: func() string {
+ contentHighlights, ok := hit.Highlight["Content"]
+ if !ok {
+ return ""
+ }
+
+ return strings.Join(contentHighlights[:], "; ")
+ }(),
+ Audio: func() *searchMessage.Audio {
+ if !strings.HasPrefix(resource.MimeType, "audio/") {
+ return nil
+ }
+
+ audio, _ := conversions.To[*searchMessage.Audio](resource.Audio)
+ return audio
+ }(),
+ Image: func() *searchMessage.Image {
+ image, _ := conversions.To[*searchMessage.Image](resource.Image)
+ return image
+ }(),
+ Location: func() *searchMessage.GeoCoordinates {
+ geoCoordinates, _ := conversions.To[*searchMessage.GeoCoordinates](resource.Location)
+ return geoCoordinates
+ }(),
+ Photo: func() *searchMessage.Photo {
+ photo, _ := conversions.To[*searchMessage.Photo](resource.Photo)
+ return photo
+ }(),
+ },
+ }
+
+ if mtime, err := time.Parse(time.RFC3339, resource.Mtime); err == nil {
+ match.Entity.LastModifiedTime = ×tamppb.Timestamp{Seconds: mtime.Unix(), Nanos: int32(mtime.Nanosecond())}
+ }
+
+ return match, nil
+}
diff --git a/services/search/pkg/opensearch/internal/convert/opensearch_test.go b/services/search/pkg/opensearch/internal/convert/opensearch_test.go
new file mode 100644
index 0000000000..afff90411e
--- /dev/null
+++ b/services/search/pkg/opensearch/internal/convert/opensearch_test.go
@@ -0,0 +1,38 @@
+package convert_test
+
+import (
+ "encoding/json"
+ "testing"
+
+ opensearchgoAPI "github.com/opensearch-project/opensearch-go/v4/opensearchapi"
+ "github.com/stretchr/testify/assert"
+
+ "github.com/opencloud-eu/opencloud/pkg/conversions"
+ searchMessage "github.com/opencloud-eu/opencloud/protogen/gen/opencloud/messages/search/v0"
+ "github.com/opencloud-eu/opencloud/services/search/pkg/opensearch/internal/convert"
+ "github.com/opencloud-eu/opencloud/services/search/pkg/opensearch/internal/test"
+)
+
+func TestOpenSearchHitToMatch(t *testing.T) {
+ resource := opensearchtest.Testdata.Resources.File
+ resource.MimeType = "audio/anything"
+
+ hit := opensearchgoAPI.SearchHit{
+ Score: 1.1,
+ Source: json.RawMessage(opensearchtest.JSONMustMarshal(t, resource)),
+ }
+ match, err := convert.OpenSearchHitToMatch(hit)
+ assert.NoError(t, err)
+ assert.Equal(t, hit.Score, match.Score)
+ assert.Equal(t, resource.Name, match.Entity.Name)
+ t.Parallel()
+ t.Run("converts the audio field to the expected type", func(t *testing.T) {
+ // searchMessage.Audio contains int64, int32 ... values that are converted to strings by the JSON marshaler,
+ // so we need to convert the resource.Audio to align the expectations for the JSON comparison.
+ audio, err := conversions.To[*searchMessage.Audio](resource.Audio)
+ assert.NoError(t, err)
+
+ assert.Equal(t, resource.Audio.Bitrate, match.Entity.Audio.Bitrate)
+ assert.JSONEq(t, opensearchtest.JSONMustMarshal(t, audio), opensearchtest.JSONMustMarshal(t, match.Entity.Audio))
+ })
+}
diff --git a/services/search/pkg/opensearch/internal/indexes/resource_v1.json b/services/search/pkg/opensearch/internal/indexes/resource_v1.json
new file mode 100644
index 0000000000..f0f719c4c5
--- /dev/null
+++ b/services/search/pkg/opensearch/internal/indexes/resource_v1.json
@@ -0,0 +1,49 @@
+{
+ "settings": {
+ "number_of_shards": "1",
+ "number_of_replicas": "1",
+ "analysis": {
+ "analyzer": {
+ "path_hierarchy": {
+ "filter": [
+ "lowercase"
+ ],
+ "tokenizer": "path_hierarchy",
+ "type": "custom"
+ }
+ },
+ "tokenizer": {
+ "path_hierarchy": {
+ "type": "path_hierarchy"
+ }
+ }
+ }
+ },
+ "mappings": {
+ "properties": {
+ "ID": {
+ "type": "keyword"
+ },
+ "ParentID": {
+ "type": "keyword"
+ },
+ "RootID": {
+ "type": "keyword"
+ },
+ "MimeType": {
+ "type": "wildcard",
+ "doc_values": false
+ },
+ "Path": {
+ "type": "text",
+ "analyzer": "path_hierarchy"
+ },
+ "Deleted": {
+ "type": "boolean"
+ },
+ "Hidden": {
+ "type": "boolean"
+ }
+ }
+ }
+}
diff --git a/services/search/pkg/opensearch/internal/osu/osu.go b/services/search/pkg/opensearch/internal/osu/osu.go
new file mode 100644
index 0000000000..03aa169bf3
--- /dev/null
+++ b/services/search/pkg/opensearch/internal/osu/osu.go
@@ -0,0 +1,128 @@
+package osu
+
+import (
+ "encoding/json"
+ "fmt"
+ "reflect"
+
+ "dario.cat/mergo"
+
+ "github.com/opencloud-eu/opencloud/pkg/conversions"
+)
+
+type Builder interface {
+ json.Marshaler
+ Map() (map[string]any, error)
+}
+
+func newBase(v ...any) (map[string]any, error) {
+ base := make(map[string]any)
+ for _, value := range v {
+ data, err := conversions.To[map[string]any](value)
+ if err != nil {
+ return nil, fmt.Errorf("failed to convert value to map: %w", err)
+ }
+
+ if isEmpty(data) {
+ continue
+ }
+
+ if err := mergo.Merge(&base, data); err != nil {
+ return nil, fmt.Errorf("failed to merge value into base: %w", err)
+ }
+ }
+
+ return base, nil
+}
+
+func applyValue[T any](target map[string]any, key string, v T) {
+ if target == nil || isEmpty(key) || isEmpty(v) {
+ return
+ }
+
+ target[key] = v
+}
+
+func applyValues[T any](target map[string]any, values map[string]T) {
+ if target == nil || isEmpty(values) {
+ return
+ }
+
+ for k, v := range values {
+ applyValue[T](target, k, v)
+ }
+}
+
+func applyBuilder(target map[string]any, key string, builder Builder) error {
+ if target == nil || isEmpty(key) || isEmpty(builder) {
+ return nil
+ }
+
+ data, err := builder.Map()
+ if err != nil {
+ return fmt.Errorf("failed to map builder %s: %w", key, err)
+ }
+
+ if !isEmpty(data) {
+ target[key] = data
+ }
+
+ return nil
+}
+
+func applyBuilders(target map[string]any, key string, bs ...Builder) error {
+ if target == nil || isEmpty(key) || isEmpty(bs) {
+ return nil
+ }
+
+ builders := make([]map[string]any, 0, len(bs))
+ for _, builder := range bs {
+ data, err := builder.Map()
+ switch {
+ case err != nil:
+ return fmt.Errorf("failed to map builder %s: %w", key, err)
+ case isEmpty(data):
+ continue
+ default:
+ builders = append(builders, data)
+ }
+ }
+
+ if len(builders) > 0 {
+ target[key] = builders
+ }
+
+ return nil
+}
+
+func isEmpty(x any) bool {
+ switch {
+ case x == nil:
+ return true
+ case reflect.ValueOf(x).Kind() == reflect.Bool:
+ return false
+ case reflect.DeepEqual(x, reflect.Zero(reflect.TypeOf(x)).Interface()):
+ return true
+ case reflect.ValueOf(x).Kind() == reflect.Map && reflect.ValueOf(x).Len() == 0:
+ return true
+ default:
+ return false
+ }
+}
+
+func merge[T any](vals ...T) T {
+ base := make(map[string]any)
+
+ for _, val := range vals {
+ data, err := conversions.To[map[string]any](val)
+ if err != nil {
+ continue
+ }
+
+ _ = mergo.Merge(&base, data)
+ }
+
+ data, _ := conversions.To[T](base)
+
+ return data
+}
diff --git a/services/search/pkg/opensearch/internal/osu/query_bool.go b/services/search/pkg/opensearch/internal/osu/query_bool.go
new file mode 100644
index 0000000000..abccd67d2c
--- /dev/null
+++ b/services/search/pkg/opensearch/internal/osu/query_bool.go
@@ -0,0 +1,87 @@
+package osu
+
+import (
+ "encoding/json"
+)
+
+type BoolQuery struct {
+ must []Builder
+ mustNot []Builder
+ should []Builder
+ filter []Builder
+ params *BoolQueryParams
+}
+
+type BoolQueryParams struct {
+ MinimumShouldMatch int16 `json:"minimum_should_match,omitempty"`
+ Boost float32 `json:"boost,omitempty"`
+ Name string `json:"_name,omitempty"`
+}
+
+func NewBoolQuery() *BoolQuery {
+ return &BoolQuery{}
+}
+
+func (q *BoolQuery) Params(v *BoolQueryParams) *BoolQuery {
+ q.params = v
+ return q
+}
+
+func (q *BoolQuery) Must(v ...Builder) *BoolQuery {
+ q.must = append(q.must, v...)
+ return q
+}
+
+func (q *BoolQuery) MustNot(v ...Builder) *BoolQuery {
+ q.mustNot = append(q.mustNot, v...)
+ return q
+}
+
+func (q *BoolQuery) Should(v ...Builder) *BoolQuery {
+ q.should = append(q.should, v...)
+ return q
+}
+
+func (q *BoolQuery) Filter(v ...Builder) *BoolQuery {
+ q.filter = append(q.filter, v...)
+ return q
+}
+
+func (q *BoolQuery) Map() (map[string]any, error) {
+ base, err := newBase(q.params)
+ if err != nil {
+ return nil, err
+ }
+
+ if err := applyBuilders(base, "must", q.must...); err != nil {
+ return nil, err
+ }
+
+ if err := applyBuilders(base, "must_not", q.mustNot...); err != nil {
+ return nil, err
+ }
+
+ if err := applyBuilders(base, "should", q.should...); err != nil {
+ return nil, err
+ }
+
+ if err := applyBuilders(base, "filter", q.filter...); err != nil {
+ return nil, err
+ }
+
+ if isEmpty(base) {
+ return nil, nil
+ }
+
+ return map[string]any{
+ "bool": base,
+ }, nil
+}
+
+func (q *BoolQuery) MarshalJSON() ([]byte, error) {
+ data, err := q.Map()
+ if err != nil {
+ return nil, err
+ }
+ return json.Marshal(data)
+}
diff --git a/services/search/pkg/opensearch/internal/osu/query_bool_test.go b/services/search/pkg/opensearch/internal/osu/query_bool_test.go
new file mode 100644
index 0000000000..3f33d4c1f4
--- /dev/null
+++ b/services/search/pkg/opensearch/internal/osu/query_bool_test.go
@@ -0,0 +1,158 @@
+package osu_test
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+
+ "github.com/opencloud-eu/opencloud/services/search/pkg/opensearch/internal/osu"
+ "github.com/opencloud-eu/opencloud/services/search/pkg/opensearch/internal/test"
+)
+
+func TestBoolQuery(t *testing.T) {
+ tests := []opensearchtest.TableTest[osu.Builder, map[string]any]{
+ {
+ Name: "empty",
+ Got: osu.NewBoolQuery(),
+ Want: nil,
+ },
+ {
+ Name: "with params",
+ Got: osu.NewBoolQuery().Params(&osu.BoolQueryParams{
+ MinimumShouldMatch: 10,
+ Boost: 10,
+ Name: "some-name",
+ }),
+ Want: map[string]any{
+ "bool": map[string]any{
+ "minimum_should_match": 10,
+ "boost": 10,
+ "_name": "some-name",
+ },
+ },
+ },
+ {
+ Name: "must",
+ Got: osu.NewBoolQuery().Must(osu.NewTermQuery[string]("name").Value("tom")),
+ Want: map[string]any{
+ "bool": map[string]any{
+ "must": []map[string]any{
+ {
+ "term": map[string]any{
+ "name": map[string]any{
+ "value": "tom",
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ {
+ Name: "must_not",
+ Got: osu.NewBoolQuery().MustNot(osu.NewTermQuery[string]("name").Value("tom")),
+ Want: map[string]any{
+ "bool": map[string]any{
+ "must_not": []map[string]any{
+ {
+ "term": map[string]any{
+ "name": map[string]any{
+ "value": "tom",
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ {
+ Name: "should",
+ Got: osu.NewBoolQuery().Should(osu.NewTermQuery[string]("name").Value("tom")),
+ Want: map[string]any{
+ "bool": map[string]any{
+ "should": []map[string]any{
+ {
+ "term": map[string]any{
+ "name": map[string]any{
+ "value": "tom",
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ {
+ Name: "filter",
+ Got: osu.NewBoolQuery().Filter(osu.NewTermQuery[string]("name").Value("tom")),
+ Want: map[string]any{
+ "bool": map[string]any{
+ "filter": []map[string]any{
+ {
+ "term": map[string]any{
+ "name": map[string]any{
+ "value": "tom",
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ {
+ Name: "full",
+ Got: osu.NewBoolQuery().
+ Must(osu.NewTermQuery[string]("name").Value("tom")).
+ MustNot(osu.NewTermQuery[bool]("deleted").Value(true)).
+ Should(osu.NewTermQuery[string]("gender").Value("male")).
+ Filter(osu.NewTermQuery[int]("age").Value(42)),
+ Want: map[string]any{
+ "bool": map[string]any{
+ "must": []map[string]any{
+ {
+ "term": map[string]any{
+ "name": map[string]any{
+ "value": "tom",
+ },
+ },
+ },
+ },
+ "must_not": []map[string]any{
+ {
+ "term": map[string]any{
+ "deleted": map[string]any{
+ "value": true,
+ },
+ },
+ },
+ },
+ "should": []map[string]any{
+ {
+ "term": map[string]any{
+ "gender": map[string]any{
+ "value": "male",
+ },
+ },
+ },
+ },
+ "filter": []map[string]any{
+ {
+ "term": map[string]any{
+ "age": map[string]any{
+ "value": 42,
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ }
+
+ for _, test := range tests {
+ t.Run(test.Name, func(t *testing.T) {
+
+ assert.JSONEq(t, opensearchtest.JSONMustMarshal(t, test.Want), opensearchtest.JSONMustMarshal(t, test.Got))
+ })
+ }
+}
diff --git a/services/search/pkg/opensearch/internal/osu/query_full_text_match_phrase.go b/services/search/pkg/opensearch/internal/osu/query_full_text_match_phrase.go
new file mode 100644
index 0000000000..3ec92d258c
--- /dev/null
+++ b/services/search/pkg/opensearch/internal/osu/query_full_text_match_phrase.go
@@ -0,0 +1,58 @@
+package osu
+
+import (
+ "encoding/json"
+)
+
+type MatchPhraseQuery struct {
+ field string
+ query string
+ params *MatchPhraseQueryParams
+}
+
+type MatchPhraseQueryParams struct {
+ Analyzer string `json:"analyzer,omitempty"`
+ Slop int `json:"slop,omitempty"`
+ ZeroTermsQuery string `json:"zero_terms_query,omitempty"`
+}
+
+func NewMatchPhraseQuery(field string) *MatchPhraseQuery {
+ return &MatchPhraseQuery{field: field}
+}
+
+func (q *MatchPhraseQuery) Params(v *MatchPhraseQueryParams) *MatchPhraseQuery {
+ q.params = v
+ return q
+}
+
+func (q *MatchPhraseQuery) Query(v string) *MatchPhraseQuery {
+ q.query = v
+ return q
+}
+
+func (q *MatchPhraseQuery) Map() (map[string]any, error) {
+ base, err := newBase(q.params)
+ if err != nil {
+ return nil, err
+ }
+
+ applyValue(base, "query", q.query)
+
+ if isEmpty(base) {
+ return nil, nil
+ }
+
+ return map[string]any{
+ "match_phrase": map[string]any{
+ q.field: base,
+ },
+ }, nil
+}
+
+func (q *MatchPhraseQuery) MarshalJSON() ([]byte, error) {
+ data, err := q.Map()
+ if err != nil {
+ return nil, err
+ }
+ return json.Marshal(data)
+}
diff --git a/services/search/pkg/opensearch/internal/osu/query_full_text_match_phrase_test.go b/services/search/pkg/opensearch/internal/osu/query_full_text_match_phrase_test.go
new file mode 100644
index 0000000000..2fa712d039
--- /dev/null
+++ b/services/search/pkg/opensearch/internal/osu/query_full_text_match_phrase_test.go
@@ -0,0 +1,72 @@
+package osu_test
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+
+ "github.com/opencloud-eu/opencloud/services/search/pkg/opensearch/internal/osu"
+ "github.com/opencloud-eu/opencloud/services/search/pkg/opensearch/internal/test"
+)
+
+func TestNewMatchPhraseQuery(t *testing.T) {
+ tests := []opensearchtest.TableTest[osu.Builder, map[string]any]{
+ {
+ Name: "empty",
+ Got: osu.NewMatchPhraseQuery("empty"),
+ Want: nil,
+ },
+ {
+ Name: "with params",
+ Got: osu.NewMatchPhraseQuery("name").Params(&osu.MatchPhraseQueryParams{
+ Analyzer: "analyzer",
+ Slop: 2,
+ ZeroTermsQuery: "all",
+ }),
+ Want: map[string]any{
+ "match_phrase": map[string]any{
+ "name": map[string]any{
+ "analyzer": "analyzer",
+ "slop": 2,
+ "zero_terms_query": "all",
+ },
+ },
+ },
+ },
+ {
+ Name: "query",
+ Got: osu.NewMatchPhraseQuery("name").Query("some match query"),
+ Want: map[string]any{
+ "match_phrase": map[string]any{
+ "name": map[string]any{
+ "query": "some match query",
+ },
+ },
+ },
+ },
+ {
+ Name: "full",
+ Got: osu.NewMatchPhraseQuery("name").Params(&osu.MatchPhraseQueryParams{
+ Analyzer: "analyzer",
+ Slop: 2,
+ ZeroTermsQuery: "all",
+ }).Query("some match query"),
+ Want: map[string]any{
+ "match_phrase": map[string]any{
+ "name": map[string]any{
+ "query": "some match query",
+ "analyzer": "analyzer",
+ "slop": 2,
+ "zero_terms_query": "all",
+ },
+ },
+ },
+ },
+ }
+
+ for _, test := range tests {
+ t.Run(test.Name, func(t *testing.T) {
+ assert.JSONEq(t, opensearchtest.JSONMustMarshal(t, test.Want), opensearchtest.JSONMustMarshal(t, test.Got))
+ })
+ }
+}
diff --git a/services/search/pkg/opensearch/internal/osu/query_term_level_ids.go b/services/search/pkg/opensearch/internal/osu/query_term_level_ids.go
new file mode 100644
index 0000000000..a0c0d02383
--- /dev/null
+++ b/services/search/pkg/opensearch/internal/osu/query_term_level_ids.go
@@ -0,0 +1,49 @@
+package osu
+
+import (
+ "encoding/json"
+ "slices"
+)
+
+type IDsQuery struct {
+ values []string
+ params *IDsQueryParams
+}
+
+type IDsQueryParams struct {
+ Boost float32 `json:"boost,omitempty"`
+}
+
+func NewIDsQuery(v ...string) *IDsQuery {
+ return &IDsQuery{values: slices.Compact(v)}
+}
+
+func (q *IDsQuery) Params(v *IDsQueryParams) *IDsQuery {
+ q.params = v
+ return q
+}
+
+func (q *IDsQuery) Map() (map[string]any, error) {
+ base, err := newBase(q.params)
+ if err != nil {
+ return nil, err
+ }
+
+ applyValue(base, "values", q.values)
+
+ if isEmpty(base) {
+ return nil, nil
+ }
+
+ return map[string]any{
+ "ids": base,
+ }, nil
+}
+
+func (q *IDsQuery) MarshalJSON() ([]byte, error) {
+ data, err := q.Map()
+ if err != nil {
+ return nil, err
+ }
+ return json.Marshal(data)
+}
diff --git a/services/search/pkg/opensearch/internal/osu/query_term_level_ids_test.go b/services/search/pkg/opensearch/internal/osu/query_term_level_ids_test.go
new file mode 100644
index 0000000000..1a15777649
--- /dev/null
+++ b/services/search/pkg/opensearch/internal/osu/query_term_level_ids_test.go
@@ -0,0 +1,45 @@
+package osu_test
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+
+ "github.com/opencloud-eu/opencloud/services/search/pkg/opensearch/internal/osu"
+ "github.com/opencloud-eu/opencloud/services/search/pkg/opensearch/internal/test"
+)
+
+func TestIDsQuery(t *testing.T) {
+ tests := []opensearchtest.TableTest[osu.Builder, map[string]any]{
+ {
+ Name: "empty",
+ Got: osu.NewIDsQuery(),
+ Want: nil,
+ },
+ {
+ Name: "no params",
+ Got: osu.NewIDsQuery("1", "2", "3", "3"),
+ Want: map[string]any{
+ "ids": map[string]any{
+ "values": []string{"1", "2", "3"},
+ },
+ },
+ },
+ {
+ Name: "ids",
+ Got: osu.NewIDsQuery("1", "2", "3", "3").Params(&osu.IDsQueryParams{Boost: 1.0}),
+ Want: map[string]any{
+ "ids": map[string]any{
+ "values": []string{"1", "2", "3"},
+ "boost": 1.0,
+ },
+ },
+ },
+ }
+
+ for _, test := range tests {
+ t.Run(test.Name, func(t *testing.T) {
+ assert.JSONEq(t, opensearchtest.JSONMustMarshal(t, test.Want), opensearchtest.JSONMustMarshal(t, test.Got))
+ })
+ }
+}
diff --git a/services/search/pkg/opensearch/internal/osu/query_term_level_range.go b/services/search/pkg/opensearch/internal/osu/query_term_level_range.go
new file mode 100644
index 0000000000..3e81bb00f1
--- /dev/null
+++ b/services/search/pkg/opensearch/internal/osu/query_term_level_range.go
@@ -0,0 +1,92 @@
+package osu
+
+import (
+ "encoding/json"
+ "errors"
+ "time"
+)
+
+type RangeQuery[T time.Time | string] struct {
+ field string
+ gt T
+ gte T
+ lt T
+ lte T
+ params *RangeQueryParams
+}
+
+type RangeQueryParams struct {
+ Format string `json:"format,omitempty"`
+ Relation string `json:"relation,omitempty"`
+ Boost float32 `json:"boost,omitempty"`
+ TimeZone string `json:"time_zone,omitempty"`
+}
+
+func NewRangeQuery[T time.Time | string](field string) *RangeQuery[T] {
+ return &RangeQuery[T]{field: field}
+}
+
+func (q *RangeQuery[T]) Params(v *RangeQueryParams) *RangeQuery[T] {
+ q.params = v
+ return q
+}
+
+func (q *RangeQuery[T]) Gt(v T) *RangeQuery[T] {
+ q.gt = v
+ return q
+}
+
+func (q *RangeQuery[T]) Gte(v T) *RangeQuery[T] {
+ q.gte = v
+ return q
+}
+
+func (q *RangeQuery[T]) Lt(v T) *RangeQuery[T] {
+ q.lt = v
+ return q
+}
+
+func (q *RangeQuery[T]) Lte(v T) *RangeQuery[T] {
+ q.lte = v
+ return q
+}
+
+func (q *RangeQuery[T]) Map() (map[string]any, error) {
+ if !isEmpty(q.gt) && !isEmpty(q.gte) {
+ return nil, errors.New("cannot set both gt and gte in RangeQuery")
+ }
+
+ if !isEmpty(q.lt) && !isEmpty(q.lte) {
+ return nil, errors.New("cannot set both lt and lte in RangeQuery")
+ }
+
+ base, err := newBase(q.params)
+ if err != nil {
+ return nil, err
+ }
+
+ applyValues(base, map[string]T{
+ "gt": q.gt,
+ "gte": q.gte,
+ "lt": q.lt,
+ "lte": q.lte,
+ })
+
+ if isEmpty(base) {
+ return nil, nil
+ }
+
+ return map[string]any{
+ "range": map[string]any{
+ q.field: base,
+ },
+ }, nil
+}
+
+func (q *RangeQuery[T]) MarshalJSON() ([]byte, error) {
+ data, err := q.Map()
+ if err != nil {
+ return nil, err
+ }
+ return json.Marshal(data)
+}
diff --git a/services/search/pkg/opensearch/internal/osu/query_term_level_range_test.go b/services/search/pkg/opensearch/internal/osu/query_term_level_range_test.go
new file mode 100644
index 0000000000..0bb65fca73
--- /dev/null
+++ b/services/search/pkg/opensearch/internal/osu/query_term_level_range_test.go
@@ -0,0 +1,164 @@
+package osu_test
+
+import (
+ "errors"
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+
+ "github.com/opencloud-eu/opencloud/services/search/pkg/opensearch/internal/osu"
+ "github.com/opencloud-eu/opencloud/services/search/pkg/opensearch/internal/test"
+)
+
+func TestRangeQuery(t *testing.T) {
+ now := time.Now()
+ tests := []opensearchtest.TableTest[osu.Builder, map[string]any]{
+ {
+ Name: "empty",
+ Got: osu.NewRangeQuery[string]("empty"),
+ Want: nil,
+ },
+ {
+ Name: "gt string",
+ Got: osu.NewRangeQuery[string]("created").Gt("2023-01-01T00:00:00Z"),
+ Want: map[string]any{
+ "range": map[string]any{
+ "created": map[string]any{
+ "gt": "2023-01-01T00:00:00Z",
+ },
+ },
+ },
+ },
+ {
+ Name: "gt time",
+ Got: osu.NewRangeQuery[time.Time]("created").Gt(now),
+ Want: map[string]any{
+ "range": map[string]any{
+ "created": map[string]any{
+ "gt": now,
+ },
+ },
+ },
+ },
+ {
+ Name: "gte string",
+ Got: osu.NewRangeQuery[string]("created").Gte("2023-01-01T00:00:00Z"),
+ Want: map[string]any{
+ "range": map[string]any{
+ "created": map[string]any{
+ "gte": "2023-01-01T00:00:00Z",
+ },
+ },
+ },
+ },
+ {
+ Name: "gte time",
+ Got: osu.NewRangeQuery[time.Time]("created").Gte(now),
+ Want: map[string]any{
+ "range": map[string]any{
+ "created": map[string]any{
+ "gte": now,
+ },
+ },
+ },
+ },
+ {
+ Name: "gt & gte",
+ Got: osu.NewRangeQuery[time.Time]("created").Gt(now).Gte(now),
+ Want: nil,
+ Err: errors.New(""),
+ },
+ {
+ Name: "gt string",
+ Got: osu.NewRangeQuery[string]("created").Lt("2023-01-01T00:00:00Z"),
+ Want: map[string]any{
+ "range": map[string]any{
+ "created": map[string]any{
+ "lt": "2023-01-01T00:00:00Z",
+ },
+ },
+ },
+ },
+ {
+ Name: "lt time",
+ Got: osu.NewRangeQuery[time.Time]("created").Lt(now),
+ Want: map[string]any{
+ "range": map[string]any{
+ "created": map[string]any{
+ "lt": now,
+ },
+ },
+ },
+ },
+ {
+ Name: "lte string",
+ Got: osu.NewRangeQuery[string]("created").Lte("2023-01-01T00:00:00Z"),
+ Want: map[string]any{
+ "range": map[string]any{
+ "created": map[string]any{
+ "lte": "2023-01-01T00:00:00Z",
+ },
+ },
+ },
+ },
+ {
+ Name: "lte time",
+ Got: osu.NewRangeQuery[time.Time]("created").Lte(now),
+ Want: map[string]any{
+ "range": map[string]any{
+ "created": map[string]any{
+ "lte": now,
+ },
+ },
+ },
+ },
+ {
+ Name: "lt & lte",
+ Got: osu.NewRangeQuery[time.Time]("created").Lt(now).Lte(now),
+ Want: nil,
+ Err: errors.New(""),
+ },
+ {
+ Name: "with params",
+ Got: osu.NewRangeQuery[time.Time]("created").Params(&osu.RangeQueryParams{
+ Format: "strict_date_optional_time",
+ Relation: "within",
+ Boost: 1.0,
+ TimeZone: "UTC",
+ }).Lte(now).Gte(now),
+ Want: map[string]any{
+ "range": map[string]any{
+ "created": map[string]any{
+ "lte": now,
+ "gte": now,
+ "format": "strict_date_optional_time",
+ "relation": "within",
+ "boost": 1.0,
+ "time_zone": "UTC",
+ },
+ },
+ },
+ },
+ }
+
+ for _, test := range tests {
+ t.Run(test.Name, func(t *testing.T) {
+ got, err := test.Got.MarshalJSON()
+ switch {
+ case test.Err != nil && test.Err.Error() == "": // Expecting any error
+ assert.Error(t, err)
+ assert.Nil(t, got)
+ return
+ case test.Err != nil && test.Err.Error() != "": // Expecting a specific error
+ assert.ErrorIs(t, test.Err, err)
+ assert.Nil(t, got)
+ return
+ }
+
+ require.NoError(t, err)
+ assert.JSONEq(t, opensearchtest.JSONMustMarshal(t, test.Want), string(got))
+ })
+ }
+}
diff --git a/services/search/pkg/opensearch/internal/osu/query_term_level_term.go b/services/search/pkg/opensearch/internal/osu/query_term_level_term.go
new file mode 100644
index 0000000000..0eca228607
--- /dev/null
+++ b/services/search/pkg/opensearch/internal/osu/query_term_level_term.go
@@ -0,0 +1,58 @@
+package osu
+
+import (
+ "encoding/json"
+)
+
+type TermQuery[T comparable] struct {
+ field string
+ value T
+ params *TermQueryParams
+}
+
+type TermQueryParams struct {
+ Boost float32 `json:"boost,omitempty"`
+ CaseInsensitive bool `json:"case_insensitive,omitempty"`
+ Name string `json:"_name,omitempty"`
+}
+
+func NewTermQuery[T comparable](field string) *TermQuery[T] {
+ return &TermQuery[T]{field: field}
+}
+
+func (q *TermQuery[T]) Params(v *TermQueryParams) *TermQuery[T] {
+ q.params = v
+ return q
+}
+
+func (q *TermQuery[T]) Value(v T) *TermQuery[T] {
+ q.value = v
+ return q
+}
+
+func (q *TermQuery[T]) Map() (map[string]any, error) {
+ base, err := newBase(q.params)
+ if err != nil {
+ return nil, err
+ }
+
+ applyValue(base, "value", q.value)
+
+ if isEmpty(base) {
+ return nil, nil
+ }
+
+ return map[string]any{
+ "term": map[string]any{
+ q.field: base,
+ },
+ }, nil
+}
+
+func (q *TermQuery[T]) MarshalJSON() ([]byte, error) {
+ data, err := q.Map()
+ if err != nil {
+ return nil, err
+ }
+ return json.Marshal(data)
+}
diff --git a/services/search/pkg/opensearch/internal/osu/query_term_level_term_test.go b/services/search/pkg/opensearch/internal/osu/query_term_level_term_test.go
new file mode 100644
index 0000000000..c4e9e8f325
--- /dev/null
+++ b/services/search/pkg/opensearch/internal/osu/query_term_level_term_test.go
@@ -0,0 +1,55 @@
+package osu_test
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+
+ "github.com/opencloud-eu/opencloud/services/search/pkg/opensearch/internal/osu"
+ "github.com/opencloud-eu/opencloud/services/search/pkg/opensearch/internal/test"
+)
+
+func TestTermQuery(t *testing.T) {
+ tests := []opensearchtest.TableTest[osu.Builder, map[string]any]{
+ {
+ Name: "empty",
+ Got: osu.NewTermQuery[string]("empty"),
+ Want: nil,
+ },
+ {
+ Name: "no params",
+ Got: osu.NewTermQuery[bool]("deleted").Value(false),
+ Want: map[string]any{
+ "term": map[string]any{
+ "deleted": map[string]any{
+ "value": false,
+ },
+ },
+ },
+ },
+ {
+ Name: "with params",
+ Got: osu.NewTermQuery[bool]("deleted").Params(&osu.TermQueryParams{
+ Boost: 1.0,
+ CaseInsensitive: true,
+ Name: "is-deleted",
+ }).Value(true),
+ Want: map[string]any{
+ "term": map[string]any{
+ "deleted": map[string]any{
+ "value": true,
+ "boost": 1.0,
+ "case_insensitive": true,
+ "_name": "is-deleted",
+ },
+ },
+ },
+ },
+ }
+
+ for _, test := range tests {
+ t.Run(test.Name, func(t *testing.T) {
+ assert.JSONEq(t, opensearchtest.JSONMustMarshal(t, test.Want), opensearchtest.JSONMustMarshal(t, test.Got))
+ })
+ }
+}
diff --git a/services/search/pkg/opensearch/internal/osu/query_term_level_wildcard.go b/services/search/pkg/opensearch/internal/osu/query_term_level_wildcard.go
new file mode 100644
index 0000000000..48274ba9cc
--- /dev/null
+++ b/services/search/pkg/opensearch/internal/osu/query_term_level_wildcard.go
@@ -0,0 +1,58 @@
+package osu
+
+import (
+ "encoding/json"
+)
+
+type WildcardQuery struct {
+ field string
+ value string
+ params *WildcardQueryParams
+}
+
+type WildcardQueryParams struct {
+ Boost float32 `json:"boost,omitempty"`
+ CaseInsensitive bool `json:"case_insensitive,omitempty"`
+ Rewrite string `json:"rewrite,omitempty"`
+}
+
+func NewWildcardQuery(field string) *WildcardQuery {
+ return &WildcardQuery{field: field}
+}
+
+func (q *WildcardQuery) Params(v *WildcardQueryParams) *WildcardQuery {
+ q.params = v
+ return q
+}
+
+func (q *WildcardQuery) Value(v string) *WildcardQuery {
+ q.value = v
+ return q
+}
+
+func (q *WildcardQuery) Map() (map[string]any, error) {
+ base, err := newBase(q.params)
+ if err != nil {
+ return nil, err
+ }
+
+ applyValue(base, "value", q.value)
+
+ if isEmpty(base) {
+ return nil, nil
+ }
+
+ return map[string]any{
+ "wildcard": map[string]any{
+ q.field: base,
+ },
+ }, nil
+}
+
+func (q *WildcardQuery) MarshalJSON() ([]byte, error) {
+ data, err := q.Map()
+ if err != nil {
+ return nil, err
+ }
+ return json.Marshal(data)
+}
diff --git a/services/search/pkg/opensearch/internal/osu/query_term_level_wildcard_test.go b/services/search/pkg/opensearch/internal/osu/query_term_level_wildcard_test.go
new file mode 100644
index 0000000000..afc810e36d
--- /dev/null
+++ b/services/search/pkg/opensearch/internal/osu/query_term_level_wildcard_test.go
@@ -0,0 +1,44 @@
+package osu_test
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+
+ "github.com/opencloud-eu/opencloud/services/search/pkg/opensearch/internal/osu"
+ "github.com/opencloud-eu/opencloud/services/search/pkg/opensearch/internal/test"
+)
+
+func TestWildcardQuery(t *testing.T) {
+ tests := []opensearchtest.TableTest[osu.Builder, map[string]any]{
+ {
+ Name: "empty",
+ Got: osu.NewWildcardQuery("empty"),
+ Want: nil,
+ },
+ {
+ Name: "wildcard",
+ Got: osu.NewWildcardQuery("name").Params(&osu.WildcardQueryParams{
+ Boost: 1.0,
+ CaseInsensitive: true,
+ Rewrite: "top_terms_blended_freqs_N",
+ }).Value("opencl*"),
+ Want: map[string]any{
+ "wildcard": map[string]any{
+ "name": map[string]any{
+ "value": "opencl*",
+ "boost": 1.0,
+ "case_insensitive": true,
+ "rewrite": "top_terms_blended_freqs_N",
+ },
+ },
+ },
+ },
+ }
+
+ for _, test := range tests {
+ t.Run(test.Name, func(t *testing.T) {
+ assert.JSONEq(t, opensearchtest.JSONMustMarshal(t, test.Want), opensearchtest.JSONMustMarshal(t, test.Got))
+ })
+ }
+}
diff --git a/services/search/pkg/opensearch/internal/osu/request.go b/services/search/pkg/opensearch/internal/osu/request.go
new file mode 100644
index 0000000000..8e845cf0af
--- /dev/null
+++ b/services/search/pkg/opensearch/internal/osu/request.go
@@ -0,0 +1,105 @@
+package osu
+
+import (
+ "bytes"
+ "encoding/json"
+
+ opensearchgoAPI "github.com/opensearch-project/opensearch-go/v4/opensearchapi"
+)
+
+type QueryReqBody[P any] struct {
+ query Builder
+ params P
+}
+
+func NewQueryReqBody[P any](q Builder, p ...P) *QueryReqBody[P] {
+ return &QueryReqBody[P]{query: q, params: merge(p...)}
+}
+
+func (q QueryReqBody[O]) Map() (map[string]any, error) {
+ base, err := newBase(q.params)
+ if err != nil {
+ return nil, err
+ }
+
+ if err := applyBuilder(base, "query", q.query); err != nil {
+ return nil, err
+ }
+
+ return base, nil
+}
+
+func (q QueryReqBody[O]) MarshalJSON() ([]byte, error) {
+ data, err := q.Map()
+ if err != nil {
+ return nil, err
+ }
+
+ return json.Marshal(data)
+}
+
+//----------------------------------------------------------------------------//
+
+type BodyParamHighlight struct {
+ PreTags []string `json:"pre_tags,omitempty"`
+ PostTags []string `json:"post_tags,omitempty"`
+ Fields map[string]BodyParamHighlight `json:"fields,omitempty"`
+}
+
+type BodyParamScript struct {
+ Source string `json:"source,omitempty"`
+ Lang string `json:"lang,omitempty"`
+ Params map[string]any `json:"params,omitempty"`
+}
+
+//----------------------------------------------------------------------------//
+
+func BuildSearchReq(req *opensearchgoAPI.SearchReq, q Builder, p ...SearchBodyParams) (*opensearchgoAPI.SearchReq, error) {
+ body, err := json.Marshal(NewQueryReqBody(q, p...))
+ if err != nil {
+ return nil, err
+ }
+ req.Body = bytes.NewReader(body)
+ return req, nil
+}
+
+type SearchBodyParams struct {
+ Highlight *BodyParamHighlight `json:"highlight,omitempty"`
+}
+
+//----------------------------------------------------------------------------//
+
+func BuildDocumentDeleteByQueryReq(req opensearchgoAPI.DocumentDeleteByQueryReq, q Builder) (opensearchgoAPI.DocumentDeleteByQueryReq, error) {
+ body, err := json.Marshal(NewQueryReqBody[any](q))
+ if err != nil {
+ return req, err
+ }
+ req.Body = bytes.NewReader(body)
+ return req, nil
+}
+
+//----------------------------------------------------------------------------//
+
+func BuildUpdateByQueryReq(req opensearchgoAPI.UpdateByQueryReq, q Builder, o ...UpdateByQueryBodyParams) (opensearchgoAPI.UpdateByQueryReq, error) {
+ body, err := json.Marshal(NewQueryReqBody(q, o...))
+ if err != nil {
+ return req, err
+ }
+ req.Body = bytes.NewReader(body)
+ return req, nil
+}
+
+type UpdateByQueryBodyParams struct {
+ Script *BodyParamScript `json:"script,omitempty"`
+}
+
+//----------------------------------------------------------------------------//
+
+func BuildIndicesCountReq(req *opensearchgoAPI.IndicesCountReq, q Builder) (*opensearchgoAPI.IndicesCountReq, error) {
+ body, err := json.Marshal(NewQueryReqBody[any](q))
+ if err != nil {
+ return nil, err
+ }
+ req.Body = bytes.NewReader(body)
+ return req, nil
+}
diff --git a/services/search/pkg/opensearch/internal/osu/request_test.go b/services/search/pkg/opensearch/internal/osu/request_test.go
new file mode 100644
index 0000000000..6a48cd398e
--- /dev/null
+++ b/services/search/pkg/opensearch/internal/osu/request_test.go
@@ -0,0 +1,86 @@
+package osu_test
+
+import (
+ "io"
+ "testing"
+
+ opensearchgoAPI "github.com/opensearch-project/opensearch-go/v4/opensearchapi"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+
+ "github.com/opencloud-eu/opencloud/services/search/pkg/opensearch/internal/osu"
+ "github.com/opencloud-eu/opencloud/services/search/pkg/opensearch/internal/test"
+)
+
+func TestRequestBody(t *testing.T) {
+ tests := []opensearchtest.TableTest[osu.Builder, map[string]any]{
+ {
+ Name: "simple",
+ Got: osu.NewQueryReqBody[any](osu.NewTermQuery[string]("name").Value("tom")),
+ Want: map[string]any{
+ "query": map[string]any{
+ "term": map[string]any{
+ "name": map[string]any{
+ "value": "tom",
+ },
+ },
+ },
+ },
+ },
+ }
+
+ for _, test := range tests {
+ t.Run(test.Name, func(t *testing.T) {
+ assert.JSONEq(t, opensearchtest.JSONMustMarshal(t, test.Want), opensearchtest.JSONMustMarshal(t, test.Got))
+ })
+ }
+}
+
+func TestBuildSearchReq(t *testing.T) {
+ tests := []opensearchtest.TableTest[io.Reader, map[string]any]{
+ {
+ Name: "highlight",
+ Got: func() io.Reader {
+ req, _ := osu.BuildSearchReq(
+ &opensearchgoAPI.SearchReq{},
+ osu.NewTermQuery[string]("content").Value("content"),
+ osu.SearchBodyParams{
+ Highlight: &osu.BodyParamHighlight{
+ PreTags: []string{""},
+ PostTags: []string{""},
+ Fields: map[string]osu.BodyParamHighlight{
+ "content": {},
+ },
+ },
+ },
+ )
+
+ return req.Body
+ }(),
+ Want: map[string]any{
+ "query": map[string]any{
+ "term": map[string]any{
+ "content": map[string]any{
+ "value": "content",
+ },
+ },
+ },
+ "highlight": map[string]any{
+ "pre_tags": []string{""},
+ "post_tags": []string{""},
+ "fields": map[string]any{
+ "content": map[string]any{},
+ },
+ },
+ },
+ },
+ }
+
+ for _, test := range tests {
+ t.Run(test.Name, func(t *testing.T) {
+ body, err := io.ReadAll(test.Got)
+ require.NoError(t, err)
+ assert.JSONEq(t, opensearchtest.JSONMustMarshal(t, test.Want), string(body))
+ })
+ }
+}
diff --git a/services/search/pkg/opensearch/internal/test/helper.go b/services/search/pkg/opensearch/internal/test/helper.go
new file mode 100644
index 0000000000..e032ffe295
--- /dev/null
+++ b/services/search/pkg/opensearch/internal/test/helper.go
@@ -0,0 +1,36 @@
+package opensearchtest
+
+import (
+ "encoding/json"
+ "testing"
+ "time"
+
+ opensearchgoAPI "github.com/opensearch-project/opensearch-go/v4/opensearchapi"
+ "github.com/stretchr/testify/require"
+
+ "github.com/opencloud-eu/opencloud/pkg/conversions"
+)
+
+var TimeMustParse = func(t *testing.T, ts string) time.Time {
+ tp, err := time.Parse(time.RFC3339Nano, ts)
+ require.NoError(t, err, "failed to parse time %s", ts)
+
+ return tp
+}
+
+func JSONMustMarshal(t *testing.T, data any) string {
+ jsonData, err := json.Marshal(data)
+ require.NoError(t, err, "failed to marshal data to JSON")
+ return string(jsonData)
+}
+
+func SearchHitsMustBeConverted[T any](t *testing.T, hits []opensearchgoAPI.SearchHit) []T {
+ ts := make([]T, len(hits))
+ for i, hit := range hits {
+ resource, err := conversions.To[T](hit.Source)
+ require.NoError(t, err)
+ ts[i] = resource
+ }
+
+ return ts
+}
diff --git a/services/search/pkg/opensearch/internal/test/os.go b/services/search/pkg/opensearch/internal/test/os.go
new file mode 100644
index 0000000000..808ae0c3a6
--- /dev/null
+++ b/services/search/pkg/opensearch/internal/test/os.go
@@ -0,0 +1,256 @@
+package opensearchtest
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "io"
+ "slices"
+ "testing"
+
+ opensearchgo "github.com/opensearch-project/opensearch-go/v4"
+ opensearchgoAPI "github.com/opensearch-project/opensearch-go/v4/opensearchapi"
+ "github.com/stretchr/testify/require"
+
+ "github.com/opencloud-eu/opencloud/services/search/pkg/config"
+)
+
+type TestClient struct {
+ c *opensearchgoAPI.Client
+ Require *testRequireClient
+}
+
+func NewDefaultTestClient(t *testing.T, cfg config.EngineOpenSearchClient) *TestClient {
+ client, err := opensearchgoAPI.NewClient(opensearchgoAPI.Config{
+ Client: opensearchgo.Config{
+ Addresses: cfg.Addresses,
+ Username: cfg.Username,
+ Password: cfg.Password,
+ },
+ })
+ require.NoError(t, err, "failed to create OpenSearch client")
+
+ return NewTestClient(t, client)
+}
+
+func NewTestClient(t *testing.T, client *opensearchgoAPI.Client) *TestClient {
+ tc := &TestClient{c: client}
+ trc := &testRequireClient{tc: tc, t: t}
+ tc.Require = trc
+
+ return tc
+}
+
+func (tc *TestClient) Client() *opensearchgoAPI.Client {
+ return tc.c
+}
+
+func (tc *TestClient) IndicesReset(ctx context.Context, indices []string) error {
+ indicesToDelete := make([]string, 0, len(indices))
+ for _, index := range indices {
+ exist, err := tc.IndicesExists(ctx, []string{index})
+ if err != nil {
+ return fmt.Errorf("failed to check if index %s exists: %w", index, err)
+ }
+
+ if !exist {
+ continue
+ }
+
+ indicesToDelete = append(indicesToDelete, index)
+ }
+
+ if len(indicesToDelete) == 0 {
+ // If no indices to delete, return nil
+ return nil
+ }
+
+ return tc.IndicesDelete(ctx, indicesToDelete)
+}
+
+func (tc *TestClient) IndicesExists(ctx context.Context, indices []string) (bool, error) {
+ if err := tc.IndicesRefresh(ctx, indices, []int{404}); err != nil {
+ return false, err
+ }
+
+ resp, err := tc.c.Indices.Exists(ctx, opensearchgoAPI.IndicesExistsReq{
+ Indices: indices,
+ })
+ switch {
+ case resp != nil && resp.StatusCode == 404:
+ return false, nil
+ case err != nil:
+ return false, fmt.Errorf("failed to check if indices exist: %w", err)
+ case resp != nil && resp.IsError():
+ return false, fmt.Errorf("failed to check if indices exist: %s", resp.String())
+ default:
+ return true, nil
+ }
+}
+
+func (tc *TestClient) IndicesRefresh(ctx context.Context, indices []string, allow []int) error {
+ resp, err := tc.c.Indices.Refresh(ctx, &opensearchgoAPI.IndicesRefreshReq{
+ Indices: indices,
+ })
+
+ isAllowed := resp != nil
+ isAllowed = isAllowed && resp.Inspect().Response != nil
+ isAllowed = isAllowed && slices.Contains(allow, resp.Inspect().Response.StatusCode)
+
+ if err != nil && !isAllowed {
+ return fmt.Errorf("failed to refresh indices %v: %w", indices, err)
+ }
+
+ return nil
+}
+
+func (tc *TestClient) IndicesDelete(ctx context.Context, indices []string) error {
+ if err := tc.IndicesRefresh(ctx, indices, []int{}); err != nil {
+ return err
+ }
+
+ resp, err := tc.c.Indices.Delete(ctx, opensearchgoAPI.IndicesDeleteReq{
+ Indices: indices,
+ })
+ switch {
+ case err != nil:
+ return fmt.Errorf("failed to delete indices: %w", err)
+ case !resp.Acknowledged:
+ return errors.New("indices deletion not acknowledged")
+ default:
+ return nil
+ }
+}
+
+func (tc *TestClient) IndicesCreate(ctx context.Context, index string, body io.Reader) error {
+ resp, err := tc.c.Indices.Create(ctx, opensearchgoAPI.IndicesCreateReq{
+ Index: index,
+ Body: body,
+ })
+
+ switch {
+ case err != nil:
+ return fmt.Errorf("failed to create index %s: %w", index, err)
+ case !resp.Acknowledged:
+ return fmt.Errorf("index creation not acknowledged for %s", index)
+ default:
+ return nil
+ }
+}
+
+func (tc *TestClient) IndicesCount(ctx context.Context, indices []string, body io.Reader) (int, error) {
+ if err := tc.IndicesRefresh(ctx, indices, []int{404}); err != nil {
+ return 0, err
+ }
+
+ resp, err := tc.c.Indices.Count(ctx, &opensearchgoAPI.IndicesCountReq{
+ Indices: indices,
+ Body: body,
+ })
+
+ switch {
+ case err != nil:
+ return 0, fmt.Errorf("failed to count documents in indices: %w", err)
+ default:
+ return resp.Count, nil
+ }
+}
+
+func (tc *TestClient) DocumentCreate(ctx context.Context, index, id string, body io.Reader) error {
+ if err := tc.IndicesRefresh(ctx, []string{index}, []int{404}); err != nil {
+ return err
+ }
+
+ _, err := tc.c.Document.Create(ctx, opensearchgoAPI.DocumentCreateReq{
+ Index: index,
+ DocumentID: id,
+ Body: body,
+ })
+ switch {
+ case err != nil:
+ return fmt.Errorf("failed to create document in index %s: %w", index, err)
+ default:
+ return nil
+ }
+}
+
+func (tc *TestClient) Update(ctx context.Context, index, id string, body io.Reader) error {
+ if err := tc.IndicesRefresh(ctx, []string{index}, []int{404}); err != nil {
+ return err
+ }
+
+ _, err := tc.c.Update(ctx, opensearchgoAPI.UpdateReq{
+ Index: index,
+ DocumentID: id,
+ Body: body,
+ })
+ switch {
+ case err != nil:
+ return fmt.Errorf("failed to update document in index %s: %w", index, err)
+ default:
+ return nil
+ }
+}
+
+func (tc *TestClient) Search(ctx context.Context, index string, body io.Reader) (opensearchgoAPI.SearchHits, error) {
+ if err := tc.IndicesRefresh(ctx, []string{index}, []int{404}); err != nil {
+ return opensearchgoAPI.SearchHits{}, err
+ }
+
+ resp, err := tc.c.Search(ctx, &opensearchgoAPI.SearchReq{
+ Indices: []string{index},
+ Body: body,
+ })
+ if err != nil {
+ return opensearchgoAPI.SearchHits{}, fmt.Errorf("failed to search in index %s: %w", index, err)
+ }
+
+ return resp.Hits, nil
+}
+
+type testRequireClient struct {
+ tc *TestClient
+ t *testing.T
+}
+
+func (trc *testRequireClient) IndicesReset(indices []string) {
+ require.NoError(trc.t, trc.tc.IndicesReset(trc.t.Context(), indices))
+}
+
+func (trc *testRequireClient) IndicesRefresh(indices []string, ignore []int) {
+ require.NoError(trc.t, trc.tc.IndicesRefresh(trc.t.Context(), indices, ignore))
+}
+
+func (trc *testRequireClient) IndicesCreate(index string, body io.Reader) {
+ require.NoError(trc.t, trc.tc.IndicesCreate(trc.t.Context(), index, body))
+}
+
+func (trc *testRequireClient) IndicesDelete(indices []string) {
+ require.NoError(trc.t, trc.tc.IndicesDelete(trc.t.Context(), indices))
+}
+
+func (trc *testRequireClient) IndicesCount(indices []string, body io.Reader, expected int) {
+ count, err := trc.tc.IndicesCount(trc.t.Context(), indices, body)
+
+ switch {
+ case expected <= 0:
+ require.True(trc.t, count <= 0, "expected indices to have no documents, but got a count of %d", count)
+ default:
+ require.Equal(trc.t, expected, count, "expected indices to have %d documents, but got %d", expected, count)
+ require.NoError(trc.t, err, "expected indices to have documents, but got an error")
+ }
+}
+
+func (trc *testRequireClient) DocumentCreate(index, id string, body io.Reader) {
+ require.NoError(trc.t, trc.tc.DocumentCreate(trc.t.Context(), index, id, body))
+}
+
+func (trc *testRequireClient) Update(index, id string, body io.Reader) {
+ require.NoError(trc.t, trc.tc.Update(trc.t.Context(), index, id, body))
+}
+
+func (trc *testRequireClient) Search(index string, body io.Reader) opensearchgoAPI.SearchHits {
+ hits, err := trc.tc.Search(trc.t.Context(), index, body)
+ require.NoError(trc.t, err)
+ return hits
+}
diff --git a/services/search/pkg/opensearch/internal/test/suite.go b/services/search/pkg/opensearch/internal/test/suite.go
new file mode 100644
index 0000000000..e565a399e7
--- /dev/null
+++ b/services/search/pkg/opensearch/internal/test/suite.go
@@ -0,0 +1,99 @@
+package opensearchtest
+
+import (
+ "context"
+ "fmt"
+ "os"
+ "slices"
+ "strings"
+ "time"
+
+ "github.com/testcontainers/testcontainers-go"
+ "github.com/testcontainers/testcontainers-go/modules/opensearch"
+ "github.com/testcontainers/testcontainers-go/wait"
+
+ "github.com/opencloud-eu/opencloud/services/search/pkg/config"
+ "github.com/opencloud-eu/opencloud/services/search/pkg/config/defaults"
+ "github.com/opencloud-eu/opencloud/services/search/pkg/config/parser"
+)
+
+const (
+ openSearchImage = "opensearchproject/opensearch:2"
+)
+
+func SetupTests(ctx context.Context) (*config.Config, func(), error) {
+ cfg, err := setupDefaultConfig()
+ if err != nil {
+ return nil, nil, fmt.Errorf("failed to setup default configuration: %w", err)
+ }
+
+ cleanupOpenSearch, err := setupOpenSearchTestContainer(ctx, cfg)
+ if err != nil {
+ return nil, nil, fmt.Errorf("failed to setup OpenSearch test container: %w", err)
+ }
+
+ return cfg, cleanupOpenSearch, nil
+}
+
+func setupDefaultConfig() (*config.Config, error) {
+ cfg := defaults.DefaultConfig()
+ defaults.EnsureDefaults(cfg)
+
+ { // parser.Validate requires these fields to be set even if they are not used in these tests.
+ cfg.TokenManager.JWTSecret = "any-jwt"
+ cfg.ServiceAccount.ServiceAccountID = "any-service-account-id"
+ cfg.ServiceAccount.ServiceAccountSecret = "any-service-account-secret"
+ }
+
+ if err := parser.ParseConfig(cfg); err != nil {
+ return nil, fmt.Errorf("failed to parse default configuration: %w", err)
+ }
+
+ return cfg, nil
+}
+
+func setupOpenSearchTestContainer(ctx context.Context, cfg *config.Config) (func(), error) {
+ usesExternalOpensearch := slices.Contains([]bool{
+ os.Getenv("CI") == "woodpecker",
+ os.Getenv("CI_SYSTEM_NAME") == "woodpecker",
+ os.Getenv("USE_TESTCONTAINERS") == "false",
+ }, true)
+ if usesExternalOpensearch {
+ return func() {}, nil
+ }
+
+ containerName := fmt.Sprintf("opencloud/test/%s", openSearchImage)
+ containerName = strings.Replace(containerName, "/", "__", -1)
+ containerName = strings.Replace(containerName, ":", "_", -1)
+ container, err := opensearch.Run(
+ ctx,
+ openSearchImage,
+ opensearch.WithUsername(cfg.Engine.OpenSearch.Client.Username),
+ opensearch.WithPassword(cfg.Engine.OpenSearch.Client.Password),
+ testcontainers.WithName(containerName),
+ testcontainers.WithReuseByName(containerName),
+ testcontainers.WithWaitStrategy(
+ wait.ForLog("ML configuration initialized successfully").
+ WithStartupTimeout(5*time.Second),
+ ),
+ )
+ if err != nil {
+ return nil, fmt.Errorf("failed to start OpenSearch container: %w", err)
+ }
+
+ address, err := container.Address(ctx)
+ if err != nil {
+ _ = container.Terminate(ctx) // attempt to clean up the container
+ return nil, fmt.Errorf("failed to get OpenSearch container address: %w", err)
+ }
+
+ // Ensure the address is set in the default configuration.
+ cfg.Engine.OpenSearch.Client.Addresses = []string{address}
+
+ return func() {
+ err := container.Terminate(ctx)
+ if err != nil {
+ _, _ = fmt.Fprintf(os.Stderr, "failed to terminate OpenSearch container: %v\n", err)
+ }
+ }, nil
+}
diff --git a/services/search/pkg/opensearch/internal/test/test.go b/services/search/pkg/opensearch/internal/test/test.go
new file mode 100644
index 0000000000..b3eb83e026
--- /dev/null
+++ b/services/search/pkg/opensearch/internal/test/test.go
@@ -0,0 +1,9 @@
+package opensearchtest
+
+type TableTest[G any, W any] struct {
+ Name string
+ Got G
+ Want W
+ Err error
+ Skip bool
+}
diff --git a/services/search/pkg/opensearch/internal/test/testdata.go b/services/search/pkg/opensearch/internal/test/testdata.go
new file mode 100644
index 0000000000..d46b0b8aec
--- /dev/null
+++ b/services/search/pkg/opensearch/internal/test/testdata.go
@@ -0,0 +1,40 @@
+package opensearchtest
+
+import (
+ "embed"
+ "encoding/json"
+ "fmt"
+ "path"
+
+ "github.com/opencloud-eu/opencloud/services/search/pkg/engine"
+)
+
+//go:embed testdata/*.json
+var testdata embed.FS
+
+var Testdata = struct {
+ Resources resourceTestdata
+}{
+ Resources: resourceTestdata{
+ File: fromTestData[engine.Resource]("resource_file.json"),
+ },
+}
+
+type resourceTestdata struct {
+ File engine.Resource
+}
+
+func fromTestData[D any](name string) D {
+ name = path.Join("./testdata", name)
+ data, err := testdata.ReadFile(name)
+ if err != nil {
+ panic(fmt.Sprintf("failed to read testdata file %s: %v", name, err))
+ }
+
+ var d D
+ if json.Unmarshal(data, &d) != nil {
+ panic(fmt.Sprintf("failed to unmarshal testdata %s: %v", name, err))
+ }
+
+ return d
+}
diff --git a/services/search/pkg/opensearch/internal/test/testdata/resource_file.json b/services/search/pkg/opensearch/internal/test/testdata/resource_file.json
new file mode 100644
index 0000000000..d3a5c269bd
--- /dev/null
+++ b/services/search/pkg/opensearch/internal/test/testdata/resource_file.json
@@ -0,0 +1,55 @@
+{
+ "ID" : "1$2!3",
+ "Title" : "dumme title",
+ "Name" : "dummy name",
+ "Content" : "dummy content",
+ "Size" : 42,
+ "Mtime" : "2025-07-24 15:15:01.324093 +0200 CEST m=+0.000056251",
+ "MimeType" : "image/jpeg",
+ "Tags" : [ "dummy" ],
+ "audio" : {
+ "album" : "Some Album",
+ "albumArtist" : "Some AlbumArtist",
+ "artist" : "Some Artist",
+ "bitrate" : 192,
+ "composers" : "Some Composers",
+ "copyright" : "",
+ "disc" : 2,
+ "discCount" : 5,
+ "duration" : 225000,
+ "genre" : "Some Genre",
+ "hasDrm" : false,
+ "isVariableBitrate" : true,
+ "title" : "Some Title",
+ "track" : 34,
+ "trackCount" : 99,
+ "year" : 2004
+ },
+ "image" : {
+ "height" : 100,
+ "width" : 100
+ },
+ "location" : {
+ "altitude" : 1047.7,
+ "latitude" : 49.48675890884328,
+ "longitude" : 11.103870357204285
+ },
+ "photo" : {
+ "cameraMake" : "CameraMake",
+ "cameraModel" : "CameraMake",
+ "exposureDenominator" : 1,
+ "exposureNumerator" : 1,
+ "fNumber" : 1,
+ "focalLength" : 1,
+ "iso" : 1,
+ "orientation" : 1,
+ "takenDateTime" : "2018-01-01T12:34:56Z"
+ },
+ "omitempty" : "1$2!3",
+ "RootID" : "1$2!1",
+ "Path" : "./doc",
+ "ParentID" : "1$2!2",
+ "Type" : 1,
+ "Deleted" : false,
+ "Hidden" : false
+}
diff --git a/services/search/pkg/opensearch/opensearch_test.go b/services/search/pkg/opensearch/opensearch_test.go
new file mode 100644
index 0000000000..6278ce6d66
--- /dev/null
+++ b/services/search/pkg/opensearch/opensearch_test.go
@@ -0,0 +1,26 @@
+package opensearch_test
+
+import (
+ "context"
+ "fmt"
+ "os"
+ "testing"
+
+ "github.com/opencloud-eu/opencloud/services/search/pkg/config"
+ opensearchtest "github.com/opencloud-eu/opencloud/services/search/pkg/opensearch/internal/test"
+)
+
+var defaultConfig *config.Config
+
+func TestMain(m *testing.M) {
+ cfg, done, err := opensearchtest.SetupTests(context.Background())
+ if err != nil {
+ _, _ = fmt.Fprintln(os.Stderr, "Failed to setup tests:", err)
+ os.Exit(1)
+ return
+ }
+ defaultConfig = cfg
+ code := m.Run()
+ done()
+ os.Exit(code)
+}
diff --git a/services/search/pkg/service/grpc/v0/service.go b/services/search/pkg/service/grpc/v0/service.go
index b12ca383fd..3b53ba9fc4 100644
--- a/services/search/pkg/service/grpc/v0/service.go
+++ b/services/search/pkg/service/grpc/v0/service.go
@@ -18,6 +18,8 @@ import (
"github.com/opencloud-eu/reva/v2/pkg/token"
"github.com/opencloud-eu/reva/v2/pkg/token/manager/jwt"
"github.com/opencloud-eu/reva/v2/pkg/utils"
+ opensearchgo "github.com/opensearch-project/opensearch-go/v4"
+ opensearchgoAPI "github.com/opensearch-project/opensearch-go/v4/opensearchapi"
merrors "go-micro.dev/v4/errors"
"go-micro.dev/v4/metadata"
grpcmetadata "google.golang.org/grpc/metadata"
@@ -29,6 +31,7 @@ import (
"github.com/opencloud-eu/opencloud/services/search/pkg/config"
"github.com/opencloud-eu/opencloud/services/search/pkg/content"
"github.com/opencloud-eu/opencloud/services/search/pkg/engine"
+ "github.com/opencloud-eu/opencloud/services/search/pkg/opensearch"
"github.com/opencloud-eu/opencloud/services/search/pkg/query/bleve"
"github.com/opencloud-eu/opencloud/services/search/pkg/search"
)
@@ -54,6 +57,35 @@ func NewHandler(opts ...Option) (searchsvc.SearchProviderHandler, func(), error)
}
eng = engine.NewBleveEngine(idx, bleve.DefaultCreator, logger)
+ case "open-search":
+ client, err := opensearchgoAPI.NewClient(opensearchgoAPI.Config{
+ Client: opensearchgo.Config{
+ Addresses: cfg.Engine.OpenSearch.Client.Addresses,
+ Username: cfg.Engine.OpenSearch.Client.Username,
+ Password: cfg.Engine.OpenSearch.Client.Password,
+ Header: cfg.Engine.OpenSearch.Client.Header,
+ CACert: cfg.Engine.OpenSearch.Client.CACert,
+ RetryOnStatus: cfg.Engine.OpenSearch.Client.RetryOnStatus,
+ DisableRetry: cfg.Engine.OpenSearch.Client.DisableRetry,
+ EnableRetryOnTimeout: cfg.Engine.OpenSearch.Client.EnableRetryOnTimeout,
+ MaxRetries: cfg.Engine.OpenSearch.Client.MaxRetries,
+ CompressRequestBody: cfg.Engine.OpenSearch.Client.CompressRequestBody,
+ DiscoverNodesOnStart: cfg.Engine.OpenSearch.Client.DiscoverNodesOnStart,
+ DiscoverNodesInterval: cfg.Engine.OpenSearch.Client.DiscoverNodesInterval,
+ EnableMetrics: cfg.Engine.OpenSearch.Client.EnableMetrics,
+ EnableDebugLogger: cfg.Engine.OpenSearch.Client.EnableDebugLogger,
+ },
+ })
+ if err != nil {
+ return nil, teardown, fmt.Errorf("failed to create OpenSearch client: %w", err)
+ }
+
+ openSearchBackend, err := opensearch.NewBackend(cfg.Engine.OpenSearch.ResourceIndex.Name, client)
+ if err != nil {
+ return nil, teardown, fmt.Errorf("failed to create OpenSearch backend: %w", err)
+ }
+
+ eng = openSearchBackend
default:
return nil, teardown, fmt.Errorf("unknown search engine: %s", cfg.Engine.Type)
}
diff --git a/vendor/github.com/Azure/go-ansiterm/LICENSE b/vendor/github.com/Azure/go-ansiterm/LICENSE
new file mode 100644
index 0000000000..e3d9a64d1d
--- /dev/null
+++ b/vendor/github.com/Azure/go-ansiterm/LICENSE
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2015 Microsoft Corporation
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/vendor/github.com/Azure/go-ansiterm/README.md b/vendor/github.com/Azure/go-ansiterm/README.md
new file mode 100644
index 0000000000..261c041e7a
--- /dev/null
+++ b/vendor/github.com/Azure/go-ansiterm/README.md
@@ -0,0 +1,12 @@
+# go-ansiterm
+
+This is a cross platform Ansi Terminal Emulation library. It reads a stream of Ansi characters and produces the appropriate function calls. The results of the function calls are platform dependent.
+
+For example the parser might receive "ESC, [, A" as a stream of three characters. This is the code for Cursor Up (http://www.vt100.net/docs/vt510-rm/CUU). The parser then calls the cursor up function (CUU()) on an event handler. The event handler determines what platform specific work must be done to cause the cursor to move up one position.
+
+The parser (parser.go) is a partial implementation of this state machine (http://vt100.net/emu/vt500_parser.png). There are also two event handler implementations, one for tests (test_event_handler.go) to validate that the expected events are being produced and called, the other is a Windows implementation (winterm/win_event_handler.go).
+
+See parser_test.go for examples exercising the state machine and generating appropriate function calls.
+
+-----
+This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments.
diff --git a/vendor/github.com/Azure/go-ansiterm/constants.go b/vendor/github.com/Azure/go-ansiterm/constants.go
new file mode 100644
index 0000000000..96504a33bc
--- /dev/null
+++ b/vendor/github.com/Azure/go-ansiterm/constants.go
@@ -0,0 +1,188 @@
+package ansiterm
+
+const LogEnv = "DEBUG_TERMINAL"
+
+// ANSI constants
+// References:
+// -- http://www.ecma-international.org/publications/standards/Ecma-048.htm
+// -- http://man7.org/linux/man-pages/man4/console_codes.4.html
+// -- http://manpages.ubuntu.com/manpages/intrepid/man4/console_codes.4.html
+// -- http://en.wikipedia.org/wiki/ANSI_escape_code
+// -- http://vt100.net/emu/dec_ansi_parser
+// -- http://vt100.net/emu/vt500_parser.svg
+// -- http://invisible-island.net/xterm/ctlseqs/ctlseqs.html
+// -- http://www.inwap.com/pdp10/ansicode.txt
+const (
+ // ECMA-48 Set Graphics Rendition
+ // Note:
+ // -- Constants leading with an underscore (e.g., _ANSI_xxx) are unsupported or reserved
+ // -- Fonts could possibly be supported via SetCurrentConsoleFontEx
+ // -- Windows does not expose the per-window cursor (i.e., caret) blink times
+ ANSI_SGR_RESET = 0
+ ANSI_SGR_BOLD = 1
+ ANSI_SGR_DIM = 2
+ _ANSI_SGR_ITALIC = 3
+ ANSI_SGR_UNDERLINE = 4
+ _ANSI_SGR_BLINKSLOW = 5
+ _ANSI_SGR_BLINKFAST = 6
+ ANSI_SGR_REVERSE = 7
+ _ANSI_SGR_INVISIBLE = 8
+ _ANSI_SGR_LINETHROUGH = 9
+ _ANSI_SGR_FONT_00 = 10
+ _ANSI_SGR_FONT_01 = 11
+ _ANSI_SGR_FONT_02 = 12
+ _ANSI_SGR_FONT_03 = 13
+ _ANSI_SGR_FONT_04 = 14
+ _ANSI_SGR_FONT_05 = 15
+ _ANSI_SGR_FONT_06 = 16
+ _ANSI_SGR_FONT_07 = 17
+ _ANSI_SGR_FONT_08 = 18
+ _ANSI_SGR_FONT_09 = 19
+ _ANSI_SGR_FONT_10 = 20
+ _ANSI_SGR_DOUBLEUNDERLINE = 21
+ ANSI_SGR_BOLD_DIM_OFF = 22
+ _ANSI_SGR_ITALIC_OFF = 23
+ ANSI_SGR_UNDERLINE_OFF = 24
+ _ANSI_SGR_BLINK_OFF = 25
+ _ANSI_SGR_RESERVED_00 = 26
+ ANSI_SGR_REVERSE_OFF = 27
+ _ANSI_SGR_INVISIBLE_OFF = 28
+ _ANSI_SGR_LINETHROUGH_OFF = 29
+ ANSI_SGR_FOREGROUND_BLACK = 30
+ ANSI_SGR_FOREGROUND_RED = 31
+ ANSI_SGR_FOREGROUND_GREEN = 32
+ ANSI_SGR_FOREGROUND_YELLOW = 33
+ ANSI_SGR_FOREGROUND_BLUE = 34
+ ANSI_SGR_FOREGROUND_MAGENTA = 35
+ ANSI_SGR_FOREGROUND_CYAN = 36
+ ANSI_SGR_FOREGROUND_WHITE = 37
+ _ANSI_SGR_RESERVED_01 = 38
+ ANSI_SGR_FOREGROUND_DEFAULT = 39
+ ANSI_SGR_BACKGROUND_BLACK = 40
+ ANSI_SGR_BACKGROUND_RED = 41
+ ANSI_SGR_BACKGROUND_GREEN = 42
+ ANSI_SGR_BACKGROUND_YELLOW = 43
+ ANSI_SGR_BACKGROUND_BLUE = 44
+ ANSI_SGR_BACKGROUND_MAGENTA = 45
+ ANSI_SGR_BACKGROUND_CYAN = 46
+ ANSI_SGR_BACKGROUND_WHITE = 47
+ _ANSI_SGR_RESERVED_02 = 48
+ ANSI_SGR_BACKGROUND_DEFAULT = 49
+ // 50 - 65: Unsupported
+
+ ANSI_MAX_CMD_LENGTH = 4096
+
+ MAX_INPUT_EVENTS = 128
+ DEFAULT_WIDTH = 80
+ DEFAULT_HEIGHT = 24
+
+ ANSI_BEL = 0x07
+ ANSI_BACKSPACE = 0x08
+ ANSI_TAB = 0x09
+ ANSI_LINE_FEED = 0x0A
+ ANSI_VERTICAL_TAB = 0x0B
+ ANSI_FORM_FEED = 0x0C
+ ANSI_CARRIAGE_RETURN = 0x0D
+ ANSI_ESCAPE_PRIMARY = 0x1B
+ ANSI_ESCAPE_SECONDARY = 0x5B
+ ANSI_OSC_STRING_ENTRY = 0x5D
+ ANSI_COMMAND_FIRST = 0x40
+ ANSI_COMMAND_LAST = 0x7E
+ DCS_ENTRY = 0x90
+ CSI_ENTRY = 0x9B
+ OSC_STRING = 0x9D
+ ANSI_PARAMETER_SEP = ";"
+ ANSI_CMD_G0 = '('
+ ANSI_CMD_G1 = ')'
+ ANSI_CMD_G2 = '*'
+ ANSI_CMD_G3 = '+'
+ ANSI_CMD_DECPNM = '>'
+ ANSI_CMD_DECPAM = '='
+ ANSI_CMD_OSC = ']'
+ ANSI_CMD_STR_TERM = '\\'
+
+ KEY_CONTROL_PARAM_2 = ";2"
+ KEY_CONTROL_PARAM_3 = ";3"
+ KEY_CONTROL_PARAM_4 = ";4"
+ KEY_CONTROL_PARAM_5 = ";5"
+ KEY_CONTROL_PARAM_6 = ";6"
+ KEY_CONTROL_PARAM_7 = ";7"
+ KEY_CONTROL_PARAM_8 = ";8"
+ KEY_ESC_CSI = "\x1B["
+ KEY_ESC_N = "\x1BN"
+ KEY_ESC_O = "\x1BO"
+
+ FILL_CHARACTER = ' '
+)
+
+func getByteRange(start byte, end byte) []byte {
+ bytes := make([]byte, 0, 32)
+ for i := start; i <= end; i++ {
+ bytes = append(bytes, byte(i))
+ }
+
+ return bytes
+}
+
+var toGroundBytes = getToGroundBytes()
+var executors = getExecuteBytes()
+
+// SPACE 20+A0 hex Always and everywhere a blank space
+// Intermediate 20-2F hex !"#$%&'()*+,-./
+var intermeds = getByteRange(0x20, 0x2F)
+
+// Parameters 30-3F hex 0123456789:;<=>?
+// CSI Parameters 30-39, 3B hex 0123456789;
+var csiParams = getByteRange(0x30, 0x3F)
+
+var csiCollectables = append(getByteRange(0x30, 0x39), getByteRange(0x3B, 0x3F)...)
+
+// Uppercase 40-5F hex @ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_
+var upperCase = getByteRange(0x40, 0x5F)
+
+// Lowercase 60-7E hex `abcdefghijlkmnopqrstuvwxyz{|}~
+var lowerCase = getByteRange(0x60, 0x7E)
+
+// Alphabetics 40-7E hex (all of upper and lower case)
+var alphabetics = append(upperCase, lowerCase...)
+
+var printables = getByteRange(0x20, 0x7F)
+
+var escapeIntermediateToGroundBytes = getByteRange(0x30, 0x7E)
+var escapeToGroundBytes = getEscapeToGroundBytes()
+
+// See http://www.vt100.net/emu/vt500_parser.png for description of the complex
+// byte ranges below
+
+func getEscapeToGroundBytes() []byte {
+ escapeToGroundBytes := getByteRange(0x30, 0x4F)
+ escapeToGroundBytes = append(escapeToGroundBytes, getByteRange(0x51, 0x57)...)
+ escapeToGroundBytes = append(escapeToGroundBytes, 0x59)
+ escapeToGroundBytes = append(escapeToGroundBytes, 0x5A)
+ escapeToGroundBytes = append(escapeToGroundBytes, 0x5C)
+ escapeToGroundBytes = append(escapeToGroundBytes, getByteRange(0x60, 0x7E)...)
+ return escapeToGroundBytes
+}
+
+func getExecuteBytes() []byte {
+ executeBytes := getByteRange(0x00, 0x17)
+ executeBytes = append(executeBytes, 0x19)
+ executeBytes = append(executeBytes, getByteRange(0x1C, 0x1F)...)
+ return executeBytes
+}
+
+func getToGroundBytes() []byte {
+ groundBytes := []byte{0x18}
+ groundBytes = append(groundBytes, 0x1A)
+ groundBytes = append(groundBytes, getByteRange(0x80, 0x8F)...)
+ groundBytes = append(groundBytes, getByteRange(0x91, 0x97)...)
+ groundBytes = append(groundBytes, 0x99)
+ groundBytes = append(groundBytes, 0x9A)
+ groundBytes = append(groundBytes, 0x9C)
+ return groundBytes
+}
+
+// Delete 7F hex Always and everywhere ignored
+// C1 Control 80-9F hex 32 additional control characters
+// G1 Displayable A1-FE hex 94 additional displayable characters
+// Special A0+FF hex Same as SPACE and DELETE
diff --git a/vendor/github.com/Azure/go-ansiterm/context.go b/vendor/github.com/Azure/go-ansiterm/context.go
new file mode 100644
index 0000000000..8d66e777c0
--- /dev/null
+++ b/vendor/github.com/Azure/go-ansiterm/context.go
@@ -0,0 +1,7 @@
+package ansiterm
+
+type ansiContext struct {
+ currentChar byte
+ paramBuffer []byte
+ interBuffer []byte
+}
diff --git a/vendor/github.com/Azure/go-ansiterm/csi_entry_state.go b/vendor/github.com/Azure/go-ansiterm/csi_entry_state.go
new file mode 100644
index 0000000000..bcbe00d0c5
--- /dev/null
+++ b/vendor/github.com/Azure/go-ansiterm/csi_entry_state.go
@@ -0,0 +1,49 @@
+package ansiterm
+
+type csiEntryState struct {
+ baseState
+}
+
+func (csiState csiEntryState) Handle(b byte) (s state, e error) {
+ csiState.parser.logf("CsiEntry::Handle %#x", b)
+
+ nextState, err := csiState.baseState.Handle(b)
+ if nextState != nil || err != nil {
+ return nextState, err
+ }
+
+ switch {
+ case sliceContains(alphabetics, b):
+ return csiState.parser.ground, nil
+ case sliceContains(csiCollectables, b):
+ return csiState.parser.csiParam, nil
+ case sliceContains(executors, b):
+ return csiState, csiState.parser.execute()
+ }
+
+ return csiState, nil
+}
+
+func (csiState csiEntryState) Transition(s state) error {
+ csiState.parser.logf("CsiEntry::Transition %s --> %s", csiState.Name(), s.Name())
+ csiState.baseState.Transition(s)
+
+ switch s {
+ case csiState.parser.ground:
+ return csiState.parser.csiDispatch()
+ case csiState.parser.csiParam:
+ switch {
+ case sliceContains(csiParams, csiState.parser.context.currentChar):
+ csiState.parser.collectParam()
+ case sliceContains(intermeds, csiState.parser.context.currentChar):
+ csiState.parser.collectInter()
+ }
+ }
+
+ return nil
+}
+
+func (csiState csiEntryState) Enter() error {
+ csiState.parser.clear()
+ return nil
+}
diff --git a/vendor/github.com/Azure/go-ansiterm/csi_param_state.go b/vendor/github.com/Azure/go-ansiterm/csi_param_state.go
new file mode 100644
index 0000000000..7ed5e01c34
--- /dev/null
+++ b/vendor/github.com/Azure/go-ansiterm/csi_param_state.go
@@ -0,0 +1,38 @@
+package ansiterm
+
+type csiParamState struct {
+ baseState
+}
+
+func (csiState csiParamState) Handle(b byte) (s state, e error) {
+ csiState.parser.logf("CsiParam::Handle %#x", b)
+
+ nextState, err := csiState.baseState.Handle(b)
+ if nextState != nil || err != nil {
+ return nextState, err
+ }
+
+ switch {
+ case sliceContains(alphabetics, b):
+ return csiState.parser.ground, nil
+ case sliceContains(csiCollectables, b):
+ csiState.parser.collectParam()
+ return csiState, nil
+ case sliceContains(executors, b):
+ return csiState, csiState.parser.execute()
+ }
+
+ return csiState, nil
+}
+
+func (csiState csiParamState) Transition(s state) error {
+ csiState.parser.logf("CsiParam::Transition %s --> %s", csiState.Name(), s.Name())
+ csiState.baseState.Transition(s)
+
+ switch s {
+ case csiState.parser.ground:
+ return csiState.parser.csiDispatch()
+ }
+
+ return nil
+}
diff --git a/vendor/github.com/Azure/go-ansiterm/escape_intermediate_state.go b/vendor/github.com/Azure/go-ansiterm/escape_intermediate_state.go
new file mode 100644
index 0000000000..1c719db9e4
--- /dev/null
+++ b/vendor/github.com/Azure/go-ansiterm/escape_intermediate_state.go
@@ -0,0 +1,36 @@
+package ansiterm
+
+type escapeIntermediateState struct {
+ baseState
+}
+
+func (escState escapeIntermediateState) Handle(b byte) (s state, e error) {
+ escState.parser.logf("escapeIntermediateState::Handle %#x", b)
+ nextState, err := escState.baseState.Handle(b)
+ if nextState != nil || err != nil {
+ return nextState, err
+ }
+
+ switch {
+ case sliceContains(intermeds, b):
+ return escState, escState.parser.collectInter()
+ case sliceContains(executors, b):
+ return escState, escState.parser.execute()
+ case sliceContains(escapeIntermediateToGroundBytes, b):
+ return escState.parser.ground, nil
+ }
+
+ return escState, nil
+}
+
+func (escState escapeIntermediateState) Transition(s state) error {
+ escState.parser.logf("escapeIntermediateState::Transition %s --> %s", escState.Name(), s.Name())
+ escState.baseState.Transition(s)
+
+ switch s {
+ case escState.parser.ground:
+ return escState.parser.escDispatch()
+ }
+
+ return nil
+}
diff --git a/vendor/github.com/Azure/go-ansiterm/escape_state.go b/vendor/github.com/Azure/go-ansiterm/escape_state.go
new file mode 100644
index 0000000000..6390abd231
--- /dev/null
+++ b/vendor/github.com/Azure/go-ansiterm/escape_state.go
@@ -0,0 +1,47 @@
+package ansiterm
+
+type escapeState struct {
+ baseState
+}
+
+func (escState escapeState) Handle(b byte) (s state, e error) {
+ escState.parser.logf("escapeState::Handle %#x", b)
+ nextState, err := escState.baseState.Handle(b)
+ if nextState != nil || err != nil {
+ return nextState, err
+ }
+
+ switch {
+ case b == ANSI_ESCAPE_SECONDARY:
+ return escState.parser.csiEntry, nil
+ case b == ANSI_OSC_STRING_ENTRY:
+ return escState.parser.oscString, nil
+ case sliceContains(executors, b):
+ return escState, escState.parser.execute()
+ case sliceContains(escapeToGroundBytes, b):
+ return escState.parser.ground, nil
+ case sliceContains(intermeds, b):
+ return escState.parser.escapeIntermediate, nil
+ }
+
+ return escState, nil
+}
+
+func (escState escapeState) Transition(s state) error {
+ escState.parser.logf("Escape::Transition %s --> %s", escState.Name(), s.Name())
+ escState.baseState.Transition(s)
+
+ switch s {
+ case escState.parser.ground:
+ return escState.parser.escDispatch()
+ case escState.parser.escapeIntermediate:
+ return escState.parser.collectInter()
+ }
+
+ return nil
+}
+
+func (escState escapeState) Enter() error {
+ escState.parser.clear()
+ return nil
+}
diff --git a/vendor/github.com/Azure/go-ansiterm/event_handler.go b/vendor/github.com/Azure/go-ansiterm/event_handler.go
new file mode 100644
index 0000000000..98087b38c2
--- /dev/null
+++ b/vendor/github.com/Azure/go-ansiterm/event_handler.go
@@ -0,0 +1,90 @@
+package ansiterm
+
+type AnsiEventHandler interface {
+ // Print
+ Print(b byte) error
+
+ // Execute C0 commands
+ Execute(b byte) error
+
+ // CUrsor Up
+ CUU(int) error
+
+ // CUrsor Down
+ CUD(int) error
+
+ // CUrsor Forward
+ CUF(int) error
+
+ // CUrsor Backward
+ CUB(int) error
+
+ // Cursor to Next Line
+ CNL(int) error
+
+ // Cursor to Previous Line
+ CPL(int) error
+
+ // Cursor Horizontal position Absolute
+ CHA(int) error
+
+ // Vertical line Position Absolute
+ VPA(int) error
+
+ // CUrsor Position
+ CUP(int, int) error
+
+ // Horizontal and Vertical Position (depends on PUM)
+ HVP(int, int) error
+
+ // Text Cursor Enable Mode
+ DECTCEM(bool) error
+
+ // Origin Mode
+ DECOM(bool) error
+
+ // 132 Column Mode
+ DECCOLM(bool) error
+
+ // Erase in Display
+ ED(int) error
+
+ // Erase in Line
+ EL(int) error
+
+ // Insert Line
+ IL(int) error
+
+ // Delete Line
+ DL(int) error
+
+ // Insert Character
+ ICH(int) error
+
+ // Delete Character
+ DCH(int) error
+
+ // Set Graphics Rendition
+ SGR([]int) error
+
+ // Pan Down
+ SU(int) error
+
+ // Pan Up
+ SD(int) error
+
+ // Device Attributes
+ DA([]string) error
+
+ // Set Top and Bottom Margins
+ DECSTBM(int, int) error
+
+ // Index
+ IND() error
+
+ // Reverse Index
+ RI() error
+
+ // Flush updates from previous commands
+ Flush() error
+}
diff --git a/vendor/github.com/Azure/go-ansiterm/ground_state.go b/vendor/github.com/Azure/go-ansiterm/ground_state.go
new file mode 100644
index 0000000000..52451e9469
--- /dev/null
+++ b/vendor/github.com/Azure/go-ansiterm/ground_state.go
@@ -0,0 +1,24 @@
+package ansiterm
+
+type groundState struct {
+ baseState
+}
+
+func (gs groundState) Handle(b byte) (s state, e error) {
+ gs.parser.context.currentChar = b
+
+ nextState, err := gs.baseState.Handle(b)
+ if nextState != nil || err != nil {
+ return nextState, err
+ }
+
+ switch {
+ case sliceContains(printables, b):
+ return gs, gs.parser.print()
+
+ case sliceContains(executors, b):
+ return gs, gs.parser.execute()
+ }
+
+ return gs, nil
+}
diff --git a/vendor/github.com/Azure/go-ansiterm/osc_string_state.go b/vendor/github.com/Azure/go-ansiterm/osc_string_state.go
new file mode 100644
index 0000000000..593b10ab69
--- /dev/null
+++ b/vendor/github.com/Azure/go-ansiterm/osc_string_state.go
@@ -0,0 +1,31 @@
+package ansiterm
+
+type oscStringState struct {
+ baseState
+}
+
+func (oscState oscStringState) Handle(b byte) (s state, e error) {
+ oscState.parser.logf("OscString::Handle %#x", b)
+ nextState, err := oscState.baseState.Handle(b)
+ if nextState != nil || err != nil {
+ return nextState, err
+ }
+
+ switch {
+ case isOscStringTerminator(b):
+ return oscState.parser.ground, nil
+ }
+
+ return oscState, nil
+}
+
+// See below for OSC string terminators for linux
+// http://man7.org/linux/man-pages/man4/console_codes.4.html
+func isOscStringTerminator(b byte) bool {
+
+ if b == ANSI_BEL || b == 0x5C {
+ return true
+ }
+
+ return false
+}
diff --git a/vendor/github.com/Azure/go-ansiterm/parser.go b/vendor/github.com/Azure/go-ansiterm/parser.go
new file mode 100644
index 0000000000..03cec7ada6
--- /dev/null
+++ b/vendor/github.com/Azure/go-ansiterm/parser.go
@@ -0,0 +1,151 @@
+package ansiterm
+
+import (
+ "errors"
+ "log"
+ "os"
+)
+
+type AnsiParser struct {
+ currState state
+ eventHandler AnsiEventHandler
+ context *ansiContext
+ csiEntry state
+ csiParam state
+ dcsEntry state
+ escape state
+ escapeIntermediate state
+ error state
+ ground state
+ oscString state
+ stateMap []state
+
+ logf func(string, ...interface{})
+}
+
+type Option func(*AnsiParser)
+
+func WithLogf(f func(string, ...interface{})) Option {
+ return func(ap *AnsiParser) {
+ ap.logf = f
+ }
+}
+
+func CreateParser(initialState string, evtHandler AnsiEventHandler, opts ...Option) *AnsiParser {
+ ap := &AnsiParser{
+ eventHandler: evtHandler,
+ context: &ansiContext{},
+ }
+ for _, o := range opts {
+ o(ap)
+ }
+
+ if isDebugEnv := os.Getenv(LogEnv); isDebugEnv == "1" {
+ logFile, _ := os.Create("ansiParser.log")
+ logger := log.New(logFile, "", log.LstdFlags)
+ if ap.logf != nil {
+ l := ap.logf
+ ap.logf = func(s string, v ...interface{}) {
+ l(s, v...)
+ logger.Printf(s, v...)
+ }
+ } else {
+ ap.logf = logger.Printf
+ }
+ }
+
+ if ap.logf == nil {
+ ap.logf = func(string, ...interface{}) {}
+ }
+
+ ap.csiEntry = csiEntryState{baseState{name: "CsiEntry", parser: ap}}
+ ap.csiParam = csiParamState{baseState{name: "CsiParam", parser: ap}}
+ ap.dcsEntry = dcsEntryState{baseState{name: "DcsEntry", parser: ap}}
+ ap.escape = escapeState{baseState{name: "Escape", parser: ap}}
+ ap.escapeIntermediate = escapeIntermediateState{baseState{name: "EscapeIntermediate", parser: ap}}
+ ap.error = errorState{baseState{name: "Error", parser: ap}}
+ ap.ground = groundState{baseState{name: "Ground", parser: ap}}
+ ap.oscString = oscStringState{baseState{name: "OscString", parser: ap}}
+
+ ap.stateMap = []state{
+ ap.csiEntry,
+ ap.csiParam,
+ ap.dcsEntry,
+ ap.escape,
+ ap.escapeIntermediate,
+ ap.error,
+ ap.ground,
+ ap.oscString,
+ }
+
+ ap.currState = getState(initialState, ap.stateMap)
+
+ ap.logf("CreateParser: parser %p", ap)
+ return ap
+}
+
+func getState(name string, states []state) state {
+ for _, el := range states {
+ if el.Name() == name {
+ return el
+ }
+ }
+
+ return nil
+}
+
+func (ap *AnsiParser) Parse(bytes []byte) (int, error) {
+ for i, b := range bytes {
+ if err := ap.handle(b); err != nil {
+ return i, err
+ }
+ }
+
+ return len(bytes), ap.eventHandler.Flush()
+}
+
+func (ap *AnsiParser) handle(b byte) error {
+ ap.context.currentChar = b
+ newState, err := ap.currState.Handle(b)
+ if err != nil {
+ return err
+ }
+
+ if newState == nil {
+ ap.logf("WARNING: newState is nil")
+ return errors.New("New state of 'nil' is invalid.")
+ }
+
+ if newState != ap.currState {
+ if err := ap.changeState(newState); err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+func (ap *AnsiParser) changeState(newState state) error {
+ ap.logf("ChangeState %s --> %s", ap.currState.Name(), newState.Name())
+
+ // Exit old state
+ if err := ap.currState.Exit(); err != nil {
+ ap.logf("Exit state '%s' failed with : '%v'", ap.currState.Name(), err)
+ return err
+ }
+
+ // Perform transition action
+ if err := ap.currState.Transition(newState); err != nil {
+ ap.logf("Transition from '%s' to '%s' failed with: '%v'", ap.currState.Name(), newState.Name, err)
+ return err
+ }
+
+ // Enter new state
+ if err := newState.Enter(); err != nil {
+ ap.logf("Enter state '%s' failed with: '%v'", newState.Name(), err)
+ return err
+ }
+
+ ap.currState = newState
+ return nil
+}
diff --git a/vendor/github.com/Azure/go-ansiterm/parser_action_helpers.go b/vendor/github.com/Azure/go-ansiterm/parser_action_helpers.go
new file mode 100644
index 0000000000..de0a1f9cde
--- /dev/null
+++ b/vendor/github.com/Azure/go-ansiterm/parser_action_helpers.go
@@ -0,0 +1,99 @@
+package ansiterm
+
+import (
+ "strconv"
+)
+
+func parseParams(bytes []byte) ([]string, error) {
+ paramBuff := make([]byte, 0, 0)
+ params := []string{}
+
+ for _, v := range bytes {
+ if v == ';' {
+ if len(paramBuff) > 0 {
+ // Completed parameter, append it to the list
+ s := string(paramBuff)
+ params = append(params, s)
+ paramBuff = make([]byte, 0, 0)
+ }
+ } else {
+ paramBuff = append(paramBuff, v)
+ }
+ }
+
+ // Last parameter may not be terminated with ';'
+ if len(paramBuff) > 0 {
+ s := string(paramBuff)
+ params = append(params, s)
+ }
+
+ return params, nil
+}
+
+func parseCmd(context ansiContext) (string, error) {
+ return string(context.currentChar), nil
+}
+
+func getInt(params []string, dflt int) int {
+ i := getInts(params, 1, dflt)[0]
+ return i
+}
+
+func getInts(params []string, minCount int, dflt int) []int {
+ ints := []int{}
+
+ for _, v := range params {
+ i, _ := strconv.Atoi(v)
+ // Zero is mapped to the default value in VT100.
+ if i == 0 {
+ i = dflt
+ }
+ ints = append(ints, i)
+ }
+
+ if len(ints) < minCount {
+ remaining := minCount - len(ints)
+ for i := 0; i < remaining; i++ {
+ ints = append(ints, dflt)
+ }
+ }
+
+ return ints
+}
+
+func (ap *AnsiParser) modeDispatch(param string, set bool) error {
+ switch param {
+ case "?3":
+ return ap.eventHandler.DECCOLM(set)
+ case "?6":
+ return ap.eventHandler.DECOM(set)
+ case "?25":
+ return ap.eventHandler.DECTCEM(set)
+ }
+ return nil
+}
+
+func (ap *AnsiParser) hDispatch(params []string) error {
+ if len(params) == 1 {
+ return ap.modeDispatch(params[0], true)
+ }
+
+ return nil
+}
+
+func (ap *AnsiParser) lDispatch(params []string) error {
+ if len(params) == 1 {
+ return ap.modeDispatch(params[0], false)
+ }
+
+ return nil
+}
+
+func getEraseParam(params []string) int {
+ param := getInt(params, 0)
+ if param < 0 || 3 < param {
+ param = 0
+ }
+
+ return param
+}
diff --git a/vendor/github.com/Azure/go-ansiterm/parser_actions.go b/vendor/github.com/Azure/go-ansiterm/parser_actions.go
new file mode 100644
index 0000000000..0bb5e51e9a
--- /dev/null
+++ b/vendor/github.com/Azure/go-ansiterm/parser_actions.go
@@ -0,0 +1,119 @@
+package ansiterm
+
+func (ap *AnsiParser) collectParam() error {
+ currChar := ap.context.currentChar
+ ap.logf("collectParam %#x", currChar)
+ ap.context.paramBuffer = append(ap.context.paramBuffer, currChar)
+ return nil
+}
+
+func (ap *AnsiParser) collectInter() error {
+ currChar := ap.context.currentChar
+ ap.logf("collectInter %#x", currChar)
+ ap.context.paramBuffer = append(ap.context.interBuffer, currChar)
+ return nil
+}
+
+func (ap *AnsiParser) escDispatch() error {
+ cmd, _ := parseCmd(*ap.context)
+ intermeds := ap.context.interBuffer
+ ap.logf("escDispatch currentChar: %#x", ap.context.currentChar)
+ ap.logf("escDispatch: %v(%v)", cmd, intermeds)
+
+ switch cmd {
+ case "D": // IND
+ return ap.eventHandler.IND()
+ case "E": // NEL, equivalent to CRLF
+ err := ap.eventHandler.Execute(ANSI_CARRIAGE_RETURN)
+ if err == nil {
+ err = ap.eventHandler.Execute(ANSI_LINE_FEED)
+ }
+ return err
+ case "M": // RI
+ return ap.eventHandler.RI()
+ }
+
+ return nil
+}
+
+func (ap *AnsiParser) csiDispatch() error {
+ cmd, _ := parseCmd(*ap.context)
+ params, _ := parseParams(ap.context.paramBuffer)
+ ap.logf("Parsed params: %v with length: %d", params, len(params))
+
+ ap.logf("csiDispatch: %v(%v)", cmd, params)
+
+ switch cmd {
+ case "@":
+ return ap.eventHandler.ICH(getInt(params, 1))
+ case "A":
+ return ap.eventHandler.CUU(getInt(params, 1))
+ case "B":
+ return ap.eventHandler.CUD(getInt(params, 1))
+ case "C":
+ return ap.eventHandler.CUF(getInt(params, 1))
+ case "D":
+ return ap.eventHandler.CUB(getInt(params, 1))
+ case "E":
+ return ap.eventHandler.CNL(getInt(params, 1))
+ case "F":
+ return ap.eventHandler.CPL(getInt(params, 1))
+ case "G":
+ return ap.eventHandler.CHA(getInt(params, 1))
+ case "H":
+ ints := getInts(params, 2, 1)
+ x, y := ints[0], ints[1]
+ return ap.eventHandler.CUP(x, y)
+ case "J":
+ param := getEraseParam(params)
+ return ap.eventHandler.ED(param)
+ case "K":
+ param := getEraseParam(params)
+ return ap.eventHandler.EL(param)
+ case "L":
+ return ap.eventHandler.IL(getInt(params, 1))
+ case "M":
+ return ap.eventHandler.DL(getInt(params, 1))
+ case "P":
+ return ap.eventHandler.DCH(getInt(params, 1))
+ case "S":
+ return ap.eventHandler.SU(getInt(params, 1))
+ case "T":
+ return ap.eventHandler.SD(getInt(params, 1))
+ case "c":
+ return ap.eventHandler.DA(params)
+ case "d":
+ return ap.eventHandler.VPA(getInt(params, 1))
+ case "f":
+ ints := getInts(params, 2, 1)
+ x, y := ints[0], ints[1]
+ return ap.eventHandler.HVP(x, y)
+ case "h":
+ return ap.hDispatch(params)
+ case "l":
+ return ap.lDispatch(params)
+ case "m":
+ return ap.eventHandler.SGR(getInts(params, 1, 0))
+ case "r":
+ ints := getInts(params, 2, 1)
+ top, bottom := ints[0], ints[1]
+ return ap.eventHandler.DECSTBM(top, bottom)
+ default:
+ ap.logf("ERROR: Unsupported CSI command: '%s', with full context: %v", cmd, ap.context)
+ return nil
+ }
+
+}
+
+func (ap *AnsiParser) print() error {
+ return ap.eventHandler.Print(ap.context.currentChar)
+}
+
+func (ap *AnsiParser) clear() error {
+ ap.context = &ansiContext{}
+ return nil
+}
+
+func (ap *AnsiParser) execute() error {
+ return ap.eventHandler.Execute(ap.context.currentChar)
+}
diff --git a/vendor/github.com/Azure/go-ansiterm/states.go b/vendor/github.com/Azure/go-ansiterm/states.go
new file mode 100644
index 0000000000..f2ea1fcd12
--- /dev/null
+++ b/vendor/github.com/Azure/go-ansiterm/states.go
@@ -0,0 +1,71 @@
+package ansiterm
+
+type stateID int
+
+type state interface {
+ Enter() error
+ Exit() error
+ Handle(byte) (state, error)
+ Name() string
+ Transition(state) error
+}
+
+type baseState struct {
+ name string
+ parser *AnsiParser
+}
+
+func (base baseState) Enter() error {
+ return nil
+}
+
+func (base baseState) Exit() error {
+ return nil
+}
+
+func (base baseState) Handle(b byte) (s state, e error) {
+
+ switch {
+ case b == CSI_ENTRY:
+ return base.parser.csiEntry, nil
+ case b == DCS_ENTRY:
+ return base.parser.dcsEntry, nil
+ case b == ANSI_ESCAPE_PRIMARY:
+ return base.parser.escape, nil
+ case b == OSC_STRING:
+ return base.parser.oscString, nil
+ case sliceContains(toGroundBytes, b):
+ return base.parser.ground, nil
+ }
+
+ return nil, nil
+}
+
+func (base baseState) Name() string {
+ return base.name
+}
+
+func (base baseState) Transition(s state) error {
+ if s == base.parser.ground {
+ execBytes := []byte{0x18}
+ execBytes = append(execBytes, 0x1A)
+ execBytes = append(execBytes, getByteRange(0x80, 0x8F)...)
+ execBytes = append(execBytes, getByteRange(0x91, 0x97)...)
+ execBytes = append(execBytes, 0x99)
+ execBytes = append(execBytes, 0x9A)
+
+ if sliceContains(execBytes, base.parser.context.currentChar) {
+ return base.parser.execute()
+ }
+ }
+
+ return nil
+}
+
+type dcsEntryState struct {
+ baseState
+}
+
+type errorState struct {
+ baseState
+}
diff --git a/vendor/github.com/Azure/go-ansiterm/utilities.go b/vendor/github.com/Azure/go-ansiterm/utilities.go
new file mode 100644
index 0000000000..392114493a
--- /dev/null
+++ b/vendor/github.com/Azure/go-ansiterm/utilities.go
@@ -0,0 +1,21 @@
+package ansiterm
+
+import (
+ "strconv"
+)
+
+func sliceContains(bytes []byte, b byte) bool {
+ for _, v := range bytes {
+ if v == b {
+ return true
+ }
+ }
+
+ return false
+}
+
+func convertBytesToInteger(bytes []byte) int {
+ s := string(bytes)
+ i, _ := strconv.Atoi(s)
+ return i
+}
diff --git a/vendor/github.com/Azure/go-ansiterm/winterm/ansi.go b/vendor/github.com/Azure/go-ansiterm/winterm/ansi.go
new file mode 100644
index 0000000000..5599082ae9
--- /dev/null
+++ b/vendor/github.com/Azure/go-ansiterm/winterm/ansi.go
@@ -0,0 +1,196 @@
+// +build windows
+
+package winterm
+
+import (
+ "fmt"
+ "os"
+ "strconv"
+ "strings"
+ "syscall"
+
+ "github.com/Azure/go-ansiterm"
+ windows "golang.org/x/sys/windows"
+)
+
+// Windows keyboard constants
+// See https://msdn.microsoft.com/en-us/library/windows/desktop/dd375731(v=vs.85).aspx.
+const (
+ VK_PRIOR = 0x21 // PAGE UP key
+ VK_NEXT = 0x22 // PAGE DOWN key
+ VK_END = 0x23 // END key
+ VK_HOME = 0x24 // HOME key
+ VK_LEFT = 0x25 // LEFT ARROW key
+ VK_UP = 0x26 // UP ARROW key
+ VK_RIGHT = 0x27 // RIGHT ARROW key
+ VK_DOWN = 0x28 // DOWN ARROW key
+ VK_SELECT = 0x29 // SELECT key
+ VK_PRINT = 0x2A // PRINT key
+ VK_EXECUTE = 0x2B // EXECUTE key
+ VK_SNAPSHOT = 0x2C // PRINT SCREEN key
+ VK_INSERT = 0x2D // INS key
+ VK_DELETE = 0x2E // DEL key
+ VK_HELP = 0x2F // HELP key
+ VK_F1 = 0x70 // F1 key
+ VK_F2 = 0x71 // F2 key
+ VK_F3 = 0x72 // F3 key
+ VK_F4 = 0x73 // F4 key
+ VK_F5 = 0x74 // F5 key
+ VK_F6 = 0x75 // F6 key
+ VK_F7 = 0x76 // F7 key
+ VK_F8 = 0x77 // F8 key
+ VK_F9 = 0x78 // F9 key
+ VK_F10 = 0x79 // F10 key
+ VK_F11 = 0x7A // F11 key
+ VK_F12 = 0x7B // F12 key
+
+ RIGHT_ALT_PRESSED = 0x0001
+ LEFT_ALT_PRESSED = 0x0002
+ RIGHT_CTRL_PRESSED = 0x0004
+ LEFT_CTRL_PRESSED = 0x0008
+ SHIFT_PRESSED = 0x0010
+ NUMLOCK_ON = 0x0020
+ SCROLLLOCK_ON = 0x0040
+ CAPSLOCK_ON = 0x0080
+ ENHANCED_KEY = 0x0100
+)
+
+type ansiCommand struct {
+ CommandBytes []byte
+ Command string
+ Parameters []string
+ IsSpecial bool
+}
+
+func newAnsiCommand(command []byte) *ansiCommand {
+
+ if isCharacterSelectionCmdChar(command[1]) {
+ // Is Character Set Selection commands
+ return &ansiCommand{
+ CommandBytes: command,
+ Command: string(command),
+ IsSpecial: true,
+ }
+ }
+
+ // last char is command character
+ lastCharIndex := len(command) - 1
+
+ ac := &ansiCommand{
+ CommandBytes: command,
+ Command: string(command[lastCharIndex]),
+ IsSpecial: false,
+ }
+
+ // more than a single escape
+ if lastCharIndex != 0 {
+ start := 1
+ // skip if double char escape sequence
+ if command[0] == ansiterm.ANSI_ESCAPE_PRIMARY && command[1] == ansiterm.ANSI_ESCAPE_SECONDARY {
+ start++
+ }
+ // convert this to GetNextParam method
+ ac.Parameters = strings.Split(string(command[start:lastCharIndex]), ansiterm.ANSI_PARAMETER_SEP)
+ }
+
+ return ac
+}
+
+func (ac *ansiCommand) paramAsSHORT(index int, defaultValue int16) int16 {
+ if index < 0 || index >= len(ac.Parameters) {
+ return defaultValue
+ }
+
+ param, err := strconv.ParseInt(ac.Parameters[index], 10, 16)
+ if err != nil {
+ return defaultValue
+ }
+
+ return int16(param)
+}
+
+func (ac *ansiCommand) String() string {
+ return fmt.Sprintf("0x%v \"%v\" (\"%v\")",
+ bytesToHex(ac.CommandBytes),
+ ac.Command,
+ strings.Join(ac.Parameters, "\",\""))
+}
+
+// isAnsiCommandChar returns true if the passed byte falls within the range of ANSI commands.
+// See http://manpages.ubuntu.com/manpages/intrepid/man4/console_codes.4.html.
+func isAnsiCommandChar(b byte) bool {
+ switch {
+ case ansiterm.ANSI_COMMAND_FIRST <= b && b <= ansiterm.ANSI_COMMAND_LAST && b != ansiterm.ANSI_ESCAPE_SECONDARY:
+ return true
+ case b == ansiterm.ANSI_CMD_G1 || b == ansiterm.ANSI_CMD_OSC || b == ansiterm.ANSI_CMD_DECPAM || b == ansiterm.ANSI_CMD_DECPNM:
+ // non-CSI escape sequence terminator
+ return true
+ case b == ansiterm.ANSI_CMD_STR_TERM || b == ansiterm.ANSI_BEL:
+ // String escape sequence terminator
+ return true
+ }
+ return false
+}
+
+func isXtermOscSequence(command []byte, current byte) bool {
+ return (len(command) >= 2 && command[0] == ansiterm.ANSI_ESCAPE_PRIMARY && command[1] == ansiterm.ANSI_CMD_OSC && current != ansiterm.ANSI_BEL)
+}
+
+func isCharacterSelectionCmdChar(b byte) bool {
+ return (b == ansiterm.ANSI_CMD_G0 || b == ansiterm.ANSI_CMD_G1 || b == ansiterm.ANSI_CMD_G2 || b == ansiterm.ANSI_CMD_G3)
+}
+
+// bytesToHex converts a slice of bytes to a human-readable string.
+func bytesToHex(b []byte) string {
+ hex := make([]string, len(b))
+ for i, ch := range b {
+ hex[i] = fmt.Sprintf("%X", ch)
+ }
+ return strings.Join(hex, "")
+}
+
+// ensureInRange adjusts the passed value, if necessary, to ensure it is within
+// the passed min / max range.
+func ensureInRange(n int16, min int16, max int16) int16 {
+ if n < min {
+ return min
+ } else if n > max {
+ return max
+ } else {
+ return n
+ }
+}
+
+func GetStdFile(nFile int) (*os.File, uintptr) {
+ var file *os.File
+
+ // syscall uses negative numbers
+ // windows package uses very big uint32
+ // Keep these switches split so we don't have to convert ints too much.
+ switch uint32(nFile) {
+ case windows.STD_INPUT_HANDLE:
+ file = os.Stdin
+ case windows.STD_OUTPUT_HANDLE:
+ file = os.Stdout
+ case windows.STD_ERROR_HANDLE:
+ file = os.Stderr
+ default:
+ switch nFile {
+ case syscall.STD_INPUT_HANDLE:
+ file = os.Stdin
+ case syscall.STD_OUTPUT_HANDLE:
+ file = os.Stdout
+ case syscall.STD_ERROR_HANDLE:
+ file = os.Stderr
+ default:
+ panic(fmt.Errorf("Invalid standard handle identifier: %v", nFile))
+ }
+ }
+
+ fd, err := syscall.GetStdHandle(nFile)
+ if err != nil {
+ panic(fmt.Errorf("Invalid standard handle identifier: %v -- %v", nFile, err))
+ }
+
+ return file, uintptr(fd)
+}
diff --git a/vendor/github.com/Azure/go-ansiterm/winterm/api.go b/vendor/github.com/Azure/go-ansiterm/winterm/api.go
new file mode 100644
index 0000000000..6055e33b91
--- /dev/null
+++ b/vendor/github.com/Azure/go-ansiterm/winterm/api.go
@@ -0,0 +1,327 @@
+// +build windows
+
+package winterm
+
+import (
+ "fmt"
+ "syscall"
+ "unsafe"
+)
+
+//===========================================================================================================
+// IMPORTANT NOTE:
+//
+// The methods below make extensive use of the "unsafe" package to obtain the required pointers.
+// Beginning in Go 1.3, the garbage collector may release local variables (e.g., incoming arguments, stack
+// variables) the pointers reference *before* the API completes.
+//
+// As a result, in those cases, the code must hint that the variables remain in active by invoking the
+// dummy method "use" (see below). Newer versions of Go are planned to change the mechanism to no longer
+// require unsafe pointers.
+//
+// If you add or modify methods, ENSURE protection of local variables through the "use" builtin to inform
+// the garbage collector the variables remain in use if:
+//
+// -- The value is not a pointer (e.g., int32, struct)
+// -- The value is not referenced by the method after passing the pointer to Windows
+//
+// See http://golang.org/doc/go1.3.
+//===========================================================================================================
+
+var (
+ kernel32DLL = syscall.NewLazyDLL("kernel32.dll")
+
+ getConsoleCursorInfoProc = kernel32DLL.NewProc("GetConsoleCursorInfo")
+ setConsoleCursorInfoProc = kernel32DLL.NewProc("SetConsoleCursorInfo")
+ setConsoleCursorPositionProc = kernel32DLL.NewProc("SetConsoleCursorPosition")
+ setConsoleModeProc = kernel32DLL.NewProc("SetConsoleMode")
+ getConsoleScreenBufferInfoProc = kernel32DLL.NewProc("GetConsoleScreenBufferInfo")
+ setConsoleScreenBufferSizeProc = kernel32DLL.NewProc("SetConsoleScreenBufferSize")
+ scrollConsoleScreenBufferProc = kernel32DLL.NewProc("ScrollConsoleScreenBufferA")
+ setConsoleTextAttributeProc = kernel32DLL.NewProc("SetConsoleTextAttribute")
+ setConsoleWindowInfoProc = kernel32DLL.NewProc("SetConsoleWindowInfo")
+ writeConsoleOutputProc = kernel32DLL.NewProc("WriteConsoleOutputW")
+ readConsoleInputProc = kernel32DLL.NewProc("ReadConsoleInputW")
+ waitForSingleObjectProc = kernel32DLL.NewProc("WaitForSingleObject")
+)
+
+// Windows Console constants
+const (
+ // Console modes
+ // See https://msdn.microsoft.com/en-us/library/windows/desktop/ms686033(v=vs.85).aspx.
+ ENABLE_PROCESSED_INPUT = 0x0001
+ ENABLE_LINE_INPUT = 0x0002
+ ENABLE_ECHO_INPUT = 0x0004
+ ENABLE_WINDOW_INPUT = 0x0008
+ ENABLE_MOUSE_INPUT = 0x0010
+ ENABLE_INSERT_MODE = 0x0020
+ ENABLE_QUICK_EDIT_MODE = 0x0040
+ ENABLE_EXTENDED_FLAGS = 0x0080
+ ENABLE_AUTO_POSITION = 0x0100
+ ENABLE_VIRTUAL_TERMINAL_INPUT = 0x0200
+
+ ENABLE_PROCESSED_OUTPUT = 0x0001
+ ENABLE_WRAP_AT_EOL_OUTPUT = 0x0002
+ ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004
+ DISABLE_NEWLINE_AUTO_RETURN = 0x0008
+ ENABLE_LVB_GRID_WORLDWIDE = 0x0010
+
+ // Character attributes
+ // Note:
+ // -- The attributes are combined to produce various colors (e.g., Blue + Green will create Cyan).
+ // Clearing all foreground or background colors results in black; setting all creates white.
+ // See https://msdn.microsoft.com/en-us/library/windows/desktop/ms682088(v=vs.85).aspx#_win32_character_attributes.
+ FOREGROUND_BLUE uint16 = 0x0001
+ FOREGROUND_GREEN uint16 = 0x0002
+ FOREGROUND_RED uint16 = 0x0004
+ FOREGROUND_INTENSITY uint16 = 0x0008
+ FOREGROUND_MASK uint16 = 0x000F
+
+ BACKGROUND_BLUE uint16 = 0x0010
+ BACKGROUND_GREEN uint16 = 0x0020
+ BACKGROUND_RED uint16 = 0x0040
+ BACKGROUND_INTENSITY uint16 = 0x0080
+ BACKGROUND_MASK uint16 = 0x00F0
+
+ COMMON_LVB_MASK uint16 = 0xFF00
+ COMMON_LVB_REVERSE_VIDEO uint16 = 0x4000
+ COMMON_LVB_UNDERSCORE uint16 = 0x8000
+
+ // Input event types
+ // See https://msdn.microsoft.com/en-us/library/windows/desktop/ms683499(v=vs.85).aspx.
+ KEY_EVENT = 0x0001
+ MOUSE_EVENT = 0x0002
+ WINDOW_BUFFER_SIZE_EVENT = 0x0004
+ MENU_EVENT = 0x0008
+ FOCUS_EVENT = 0x0010
+
+ // WaitForSingleObject return codes
+ WAIT_ABANDONED = 0x00000080
+ WAIT_FAILED = 0xFFFFFFFF
+ WAIT_SIGNALED = 0x0000000
+ WAIT_TIMEOUT = 0x00000102
+
+ // WaitForSingleObject wait duration
+ WAIT_INFINITE = 0xFFFFFFFF
+ WAIT_ONE_SECOND = 1000
+ WAIT_HALF_SECOND = 500
+ WAIT_QUARTER_SECOND = 250
+)
+
+// Windows API Console types
+// -- See https://msdn.microsoft.com/en-us/library/windows/desktop/ms682101(v=vs.85).aspx for Console specific types (e.g., COORD)
+// -- See https://msdn.microsoft.com/en-us/library/aa296569(v=vs.60).aspx for comments on alignment
+type (
+ CHAR_INFO struct {
+ UnicodeChar uint16
+ Attributes uint16
+ }
+
+ CONSOLE_CURSOR_INFO struct {
+ Size uint32
+ Visible int32
+ }
+
+ CONSOLE_SCREEN_BUFFER_INFO struct {
+ Size COORD
+ CursorPosition COORD
+ Attributes uint16
+ Window SMALL_RECT
+ MaximumWindowSize COORD
+ }
+
+ COORD struct {
+ X int16
+ Y int16
+ }
+
+ SMALL_RECT struct {
+ Left int16
+ Top int16
+ Right int16
+ Bottom int16
+ }
+
+ // INPUT_RECORD is a C/C++ union of which KEY_EVENT_RECORD is one case, it is also the largest
+ // See https://msdn.microsoft.com/en-us/library/windows/desktop/ms683499(v=vs.85).aspx.
+ INPUT_RECORD struct {
+ EventType uint16
+ KeyEvent KEY_EVENT_RECORD
+ }
+
+ KEY_EVENT_RECORD struct {
+ KeyDown int32
+ RepeatCount uint16
+ VirtualKeyCode uint16
+ VirtualScanCode uint16
+ UnicodeChar uint16
+ ControlKeyState uint32
+ }
+
+ WINDOW_BUFFER_SIZE struct {
+ Size COORD
+ }
+)
+
+// boolToBOOL converts a Go bool into a Windows int32.
+func boolToBOOL(f bool) int32 {
+ if f {
+ return int32(1)
+ } else {
+ return int32(0)
+ }
+}
+
+// GetConsoleCursorInfo retrieves information about the size and visiblity of the console cursor.
+// See https://msdn.microsoft.com/en-us/library/windows/desktop/ms683163(v=vs.85).aspx.
+func GetConsoleCursorInfo(handle uintptr, cursorInfo *CONSOLE_CURSOR_INFO) error {
+ r1, r2, err := getConsoleCursorInfoProc.Call(handle, uintptr(unsafe.Pointer(cursorInfo)), 0)
+ return checkError(r1, r2, err)
+}
+
+// SetConsoleCursorInfo sets the size and visiblity of the console cursor.
+// See https://msdn.microsoft.com/en-us/library/windows/desktop/ms686019(v=vs.85).aspx.
+func SetConsoleCursorInfo(handle uintptr, cursorInfo *CONSOLE_CURSOR_INFO) error {
+ r1, r2, err := setConsoleCursorInfoProc.Call(handle, uintptr(unsafe.Pointer(cursorInfo)), 0)
+ return checkError(r1, r2, err)
+}
+
+// SetConsoleCursorPosition location of the console cursor.
+// See https://msdn.microsoft.com/en-us/library/windows/desktop/ms686025(v=vs.85).aspx.
+func SetConsoleCursorPosition(handle uintptr, coord COORD) error {
+ r1, r2, err := setConsoleCursorPositionProc.Call(handle, coordToPointer(coord))
+ use(coord)
+ return checkError(r1, r2, err)
+}
+
+// GetConsoleMode gets the console mode for given file descriptor
+// See http://msdn.microsoft.com/en-us/library/windows/desktop/ms683167(v=vs.85).aspx.
+func GetConsoleMode(handle uintptr) (mode uint32, err error) {
+ err = syscall.GetConsoleMode(syscall.Handle(handle), &mode)
+ return mode, err
+}
+
+// SetConsoleMode sets the console mode for given file descriptor
+// See http://msdn.microsoft.com/en-us/library/windows/desktop/ms686033(v=vs.85).aspx.
+func SetConsoleMode(handle uintptr, mode uint32) error {
+ r1, r2, err := setConsoleModeProc.Call(handle, uintptr(mode), 0)
+ use(mode)
+ return checkError(r1, r2, err)
+}
+
+// GetConsoleScreenBufferInfo retrieves information about the specified console screen buffer.
+// See http://msdn.microsoft.com/en-us/library/windows/desktop/ms683171(v=vs.85).aspx.
+func GetConsoleScreenBufferInfo(handle uintptr) (*CONSOLE_SCREEN_BUFFER_INFO, error) {
+ info := CONSOLE_SCREEN_BUFFER_INFO{}
+ err := checkError(getConsoleScreenBufferInfoProc.Call(handle, uintptr(unsafe.Pointer(&info)), 0))
+ if err != nil {
+ return nil, err
+ }
+ return &info, nil
+}
+
+func ScrollConsoleScreenBuffer(handle uintptr, scrollRect SMALL_RECT, clipRect SMALL_RECT, destOrigin COORD, char CHAR_INFO) error {
+ r1, r2, err := scrollConsoleScreenBufferProc.Call(handle, uintptr(unsafe.Pointer(&scrollRect)), uintptr(unsafe.Pointer(&clipRect)), coordToPointer(destOrigin), uintptr(unsafe.Pointer(&char)))
+ use(scrollRect)
+ use(clipRect)
+ use(destOrigin)
+ use(char)
+ return checkError(r1, r2, err)
+}
+
+// SetConsoleScreenBufferSize sets the size of the console screen buffer.
+// See https://msdn.microsoft.com/en-us/library/windows/desktop/ms686044(v=vs.85).aspx.
+func SetConsoleScreenBufferSize(handle uintptr, coord COORD) error {
+ r1, r2, err := setConsoleScreenBufferSizeProc.Call(handle, coordToPointer(coord))
+ use(coord)
+ return checkError(r1, r2, err)
+}
+
+// SetConsoleTextAttribute sets the attributes of characters written to the
+// console screen buffer by the WriteFile or WriteConsole function.
+// See http://msdn.microsoft.com/en-us/library/windows/desktop/ms686047(v=vs.85).aspx.
+func SetConsoleTextAttribute(handle uintptr, attribute uint16) error {
+ r1, r2, err := setConsoleTextAttributeProc.Call(handle, uintptr(attribute), 0)
+ use(attribute)
+ return checkError(r1, r2, err)
+}
+
+// SetConsoleWindowInfo sets the size and position of the console screen buffer's window.
+// Note that the size and location must be within and no larger than the backing console screen buffer.
+// See https://msdn.microsoft.com/en-us/library/windows/desktop/ms686125(v=vs.85).aspx.
+func SetConsoleWindowInfo(handle uintptr, isAbsolute bool, rect SMALL_RECT) error {
+ r1, r2, err := setConsoleWindowInfoProc.Call(handle, uintptr(boolToBOOL(isAbsolute)), uintptr(unsafe.Pointer(&rect)))
+ use(isAbsolute)
+ use(rect)
+ return checkError(r1, r2, err)
+}
+
+// WriteConsoleOutput writes the CHAR_INFOs from the provided buffer to the active console buffer.
+// See https://msdn.microsoft.com/en-us/library/windows/desktop/ms687404(v=vs.85).aspx.
+func WriteConsoleOutput(handle uintptr, buffer []CHAR_INFO, bufferSize COORD, bufferCoord COORD, writeRegion *SMALL_RECT) error {
+ r1, r2, err := writeConsoleOutputProc.Call(handle, uintptr(unsafe.Pointer(&buffer[0])), coordToPointer(bufferSize), coordToPointer(bufferCoord), uintptr(unsafe.Pointer(writeRegion)))
+ use(buffer)
+ use(bufferSize)
+ use(bufferCoord)
+ return checkError(r1, r2, err)
+}
+
+// ReadConsoleInput reads (and removes) data from the console input buffer.
+// See https://msdn.microsoft.com/en-us/library/windows/desktop/ms684961(v=vs.85).aspx.
+func ReadConsoleInput(handle uintptr, buffer []INPUT_RECORD, count *uint32) error {
+ r1, r2, err := readConsoleInputProc.Call(handle, uintptr(unsafe.Pointer(&buffer[0])), uintptr(len(buffer)), uintptr(unsafe.Pointer(count)))
+ use(buffer)
+ return checkError(r1, r2, err)
+}
+
+// WaitForSingleObject waits for the passed handle to be signaled.
+// It returns true if the handle was signaled; false otherwise.
+// See https://msdn.microsoft.com/en-us/library/windows/desktop/ms687032(v=vs.85).aspx.
+func WaitForSingleObject(handle uintptr, msWait uint32) (bool, error) {
+ r1, _, err := waitForSingleObjectProc.Call(handle, uintptr(uint32(msWait)))
+ switch r1 {
+ case WAIT_ABANDONED, WAIT_TIMEOUT:
+ return false, nil
+ case WAIT_SIGNALED:
+ return true, nil
+ }
+ use(msWait)
+ return false, err
+}
+
+// String helpers
+func (info CONSOLE_SCREEN_BUFFER_INFO) String() string {
+ return fmt.Sprintf("Size(%v) Cursor(%v) Window(%v) Max(%v)", info.Size, info.CursorPosition, info.Window, info.MaximumWindowSize)
+}
+
+func (coord COORD) String() string {
+ return fmt.Sprintf("%v,%v", coord.X, coord.Y)
+}
+
+func (rect SMALL_RECT) String() string {
+ return fmt.Sprintf("(%v,%v),(%v,%v)", rect.Left, rect.Top, rect.Right, rect.Bottom)
+}
+
+// checkError evaluates the results of a Windows API call and returns the error if it failed.
+func checkError(r1, r2 uintptr, err error) error {
+ // Windows APIs return non-zero to indicate success
+ if r1 != 0 {
+ return nil
+ }
+
+ // Return the error if provided, otherwise default to EINVAL
+ if err != nil {
+ return err
+ }
+ return syscall.EINVAL
+}
+
+// coordToPointer converts a COORD into a uintptr (by fooling the type system).
+func coordToPointer(c COORD) uintptr {
+ // Note: This code assumes the two SHORTs are correctly laid out; the "cast" to uint32 is just to get a pointer to pass.
+ return uintptr(*((*uint32)(unsafe.Pointer(&c))))
+}
+
+// use is a no-op, but the compiler cannot see that it is.
+// Calling use(p) ensures that p is kept live until that point.
+func use(p interface{}) {}
diff --git a/vendor/github.com/Azure/go-ansiterm/winterm/attr_translation.go b/vendor/github.com/Azure/go-ansiterm/winterm/attr_translation.go
new file mode 100644
index 0000000000..cbec8f728f
--- /dev/null
+++ b/vendor/github.com/Azure/go-ansiterm/winterm/attr_translation.go
@@ -0,0 +1,100 @@
+// +build windows
+
+package winterm
+
+import "github.com/Azure/go-ansiterm"
+
+const (
+ FOREGROUND_COLOR_MASK = FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE
+ BACKGROUND_COLOR_MASK = BACKGROUND_RED | BACKGROUND_GREEN | BACKGROUND_BLUE
+)
+
+// collectAnsiIntoWindowsAttributes modifies the passed Windows text mode flags to reflect the
+// request represented by the passed ANSI mode.
+func collectAnsiIntoWindowsAttributes(windowsMode uint16, inverted bool, baseMode uint16, ansiMode int16) (uint16, bool) {
+ switch ansiMode {
+
+ // Mode styles
+ case ansiterm.ANSI_SGR_BOLD:
+ windowsMode = windowsMode | FOREGROUND_INTENSITY
+
+ case ansiterm.ANSI_SGR_DIM, ansiterm.ANSI_SGR_BOLD_DIM_OFF:
+ windowsMode &^= FOREGROUND_INTENSITY
+
+ case ansiterm.ANSI_SGR_UNDERLINE:
+ windowsMode = windowsMode | COMMON_LVB_UNDERSCORE
+
+ case ansiterm.ANSI_SGR_REVERSE:
+ inverted = true
+
+ case ansiterm.ANSI_SGR_REVERSE_OFF:
+ inverted = false
+
+ case ansiterm.ANSI_SGR_UNDERLINE_OFF:
+ windowsMode &^= COMMON_LVB_UNDERSCORE
+
+ // Foreground colors
+ case ansiterm.ANSI_SGR_FOREGROUND_DEFAULT:
+ windowsMode = (windowsMode &^ FOREGROUND_MASK) | (baseMode & FOREGROUND_MASK)
+
+ case ansiterm.ANSI_SGR_FOREGROUND_BLACK:
+ windowsMode = (windowsMode &^ FOREGROUND_COLOR_MASK)
+
+ case ansiterm.ANSI_SGR_FOREGROUND_RED:
+ windowsMode = (windowsMode &^ FOREGROUND_COLOR_MASK) | FOREGROUND_RED
+
+ case ansiterm.ANSI_SGR_FOREGROUND_GREEN:
+ windowsMode = (windowsMode &^ FOREGROUND_COLOR_MASK) | FOREGROUND_GREEN
+
+ case ansiterm.ANSI_SGR_FOREGROUND_YELLOW:
+ windowsMode = (windowsMode &^ FOREGROUND_COLOR_MASK) | FOREGROUND_RED | FOREGROUND_GREEN
+
+ case ansiterm.ANSI_SGR_FOREGROUND_BLUE:
+ windowsMode = (windowsMode &^ FOREGROUND_COLOR_MASK) | FOREGROUND_BLUE
+
+ case ansiterm.ANSI_SGR_FOREGROUND_MAGENTA:
+ windowsMode = (windowsMode &^ FOREGROUND_COLOR_MASK) | FOREGROUND_RED | FOREGROUND_BLUE
+
+ case ansiterm.ANSI_SGR_FOREGROUND_CYAN:
+ windowsMode = (windowsMode &^ FOREGROUND_COLOR_MASK) | FOREGROUND_GREEN | FOREGROUND_BLUE
+
+ case ansiterm.ANSI_SGR_FOREGROUND_WHITE:
+ windowsMode = (windowsMode &^ FOREGROUND_COLOR_MASK) | FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE
+
+ // Background colors
+ case ansiterm.ANSI_SGR_BACKGROUND_DEFAULT:
+ // Black with no intensity
+ windowsMode = (windowsMode &^ BACKGROUND_MASK) | (baseMode & BACKGROUND_MASK)
+
+ case ansiterm.ANSI_SGR_BACKGROUND_BLACK:
+ windowsMode = (windowsMode &^ BACKGROUND_COLOR_MASK)
+
+ case ansiterm.ANSI_SGR_BACKGROUND_RED:
+ windowsMode = (windowsMode &^ BACKGROUND_COLOR_MASK) | BACKGROUND_RED
+
+ case ansiterm.ANSI_SGR_BACKGROUND_GREEN:
+ windowsMode = (windowsMode &^ BACKGROUND_COLOR_MASK) | BACKGROUND_GREEN
+
+ case ansiterm.ANSI_SGR_BACKGROUND_YELLOW:
+ windowsMode = (windowsMode &^ BACKGROUND_COLOR_MASK) | BACKGROUND_RED | BACKGROUND_GREEN
+
+ case ansiterm.ANSI_SGR_BACKGROUND_BLUE:
+ windowsMode = (windowsMode &^ BACKGROUND_COLOR_MASK) | BACKGROUND_BLUE
+
+ case ansiterm.ANSI_SGR_BACKGROUND_MAGENTA:
+ windowsMode = (windowsMode &^ BACKGROUND_COLOR_MASK) | BACKGROUND_RED | BACKGROUND_BLUE
+
+ case ansiterm.ANSI_SGR_BACKGROUND_CYAN:
+ windowsMode = (windowsMode &^ BACKGROUND_COLOR_MASK) | BACKGROUND_GREEN | BACKGROUND_BLUE
+
+ case ansiterm.ANSI_SGR_BACKGROUND_WHITE:
+ windowsMode = (windowsMode &^ BACKGROUND_COLOR_MASK) | BACKGROUND_RED | BACKGROUND_GREEN | BACKGROUND_BLUE
+ }
+
+ return windowsMode, inverted
+}
+
+// invertAttributes inverts the foreground and background colors of a Windows attributes value
+func invertAttributes(windowsMode uint16) uint16 {
+ return (COMMON_LVB_MASK & windowsMode) | ((FOREGROUND_MASK & windowsMode) << 4) | ((BACKGROUND_MASK & windowsMode) >> 4)
+}
diff --git a/vendor/github.com/Azure/go-ansiterm/winterm/cursor_helpers.go b/vendor/github.com/Azure/go-ansiterm/winterm/cursor_helpers.go
new file mode 100644
index 0000000000..3ee06ea728
--- /dev/null
+++ b/vendor/github.com/Azure/go-ansiterm/winterm/cursor_helpers.go
@@ -0,0 +1,101 @@
+// +build windows
+
+package winterm
+
+const (
+ horizontal = iota
+ vertical
+)
+
+func (h *windowsAnsiEventHandler) getCursorWindow(info *CONSOLE_SCREEN_BUFFER_INFO) SMALL_RECT {
+ if h.originMode {
+ sr := h.effectiveSr(info.Window)
+ return SMALL_RECT{
+ Top: sr.top,
+ Bottom: sr.bottom,
+ Left: 0,
+ Right: info.Size.X - 1,
+ }
+ } else {
+ return SMALL_RECT{
+ Top: info.Window.Top,
+ Bottom: info.Window.Bottom,
+ Left: 0,
+ Right: info.Size.X - 1,
+ }
+ }
+}
+
+// setCursorPosition sets the cursor to the specified position, bounded to the screen size
+func (h *windowsAnsiEventHandler) setCursorPosition(position COORD, window SMALL_RECT) error {
+ position.X = ensureInRange(position.X, window.Left, window.Right)
+ position.Y = ensureInRange(position.Y, window.Top, window.Bottom)
+ err := SetConsoleCursorPosition(h.fd, position)
+ if err != nil {
+ return err
+ }
+ h.logf("Cursor position set: (%d, %d)", position.X, position.Y)
+ return err
+}
+
+func (h *windowsAnsiEventHandler) moveCursorVertical(param int) error {
+ return h.moveCursor(vertical, param)
+}
+
+func (h *windowsAnsiEventHandler) moveCursorHorizontal(param int) error {
+ return h.moveCursor(horizontal, param)
+}
+
+func (h *windowsAnsiEventHandler) moveCursor(moveMode int, param int) error {
+ info, err := GetConsoleScreenBufferInfo(h.fd)
+ if err != nil {
+ return err
+ }
+
+ position := info.CursorPosition
+ switch moveMode {
+ case horizontal:
+ position.X += int16(param)
+ case vertical:
+ position.Y += int16(param)
+ }
+
+ if err = h.setCursorPosition(position, h.getCursorWindow(info)); err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func (h *windowsAnsiEventHandler) moveCursorLine(param int) error {
+ info, err := GetConsoleScreenBufferInfo(h.fd)
+ if err != nil {
+ return err
+ }
+
+ position := info.CursorPosition
+ position.X = 0
+ position.Y += int16(param)
+
+ if err = h.setCursorPosition(position, h.getCursorWindow(info)); err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func (h *windowsAnsiEventHandler) moveCursorColumn(param int) error {
+ info, err := GetConsoleScreenBufferInfo(h.fd)
+ if err != nil {
+ return err
+ }
+
+ position := info.CursorPosition
+ position.X = int16(param) - 1
+
+ if err = h.setCursorPosition(position, h.getCursorWindow(info)); err != nil {
+ return err
+ }
+
+ return nil
+}
diff --git a/vendor/github.com/Azure/go-ansiterm/winterm/erase_helpers.go b/vendor/github.com/Azure/go-ansiterm/winterm/erase_helpers.go
new file mode 100644
index 0000000000..244b5fa25e
--- /dev/null
+++ b/vendor/github.com/Azure/go-ansiterm/winterm/erase_helpers.go
@@ -0,0 +1,84 @@
+// +build windows
+
+package winterm
+
+import "github.com/Azure/go-ansiterm"
+
+func (h *windowsAnsiEventHandler) clearRange(attributes uint16, fromCoord COORD, toCoord COORD) error {
+ // Ignore an invalid (negative area) request
+ if toCoord.Y < fromCoord.Y {
+ return nil
+ }
+
+ var err error
+
+ var coordStart = COORD{}
+ var coordEnd = COORD{}
+
+ xCurrent, yCurrent := fromCoord.X, fromCoord.Y
+ xEnd, yEnd := toCoord.X, toCoord.Y
+
+ // Clear any partial initial line
+ if xCurrent > 0 {
+ coordStart.X, coordStart.Y = xCurrent, yCurrent
+ coordEnd.X, coordEnd.Y = xEnd, yCurrent
+
+ err = h.clearRect(attributes, coordStart, coordEnd)
+ if err != nil {
+ return err
+ }
+
+ xCurrent = 0
+ yCurrent += 1
+ }
+
+ // Clear intervening rectangular section
+ if yCurrent < yEnd {
+ coordStart.X, coordStart.Y = xCurrent, yCurrent
+ coordEnd.X, coordEnd.Y = xEnd, yEnd-1
+
+ err = h.clearRect(attributes, coordStart, coordEnd)
+ if err != nil {
+ return err
+ }
+
+ xCurrent = 0
+ yCurrent = yEnd
+ }
+
+ // Clear remaining partial ending line
+ coordStart.X, coordStart.Y = xCurrent, yCurrent
+ coordEnd.X, coordEnd.Y = xEnd, yEnd
+
+ err = h.clearRect(attributes, coordStart, coordEnd)
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func (h *windowsAnsiEventHandler) clearRect(attributes uint16, fromCoord COORD, toCoord COORD) error {
+ region := SMALL_RECT{Top: fromCoord.Y, Left: fromCoord.X, Bottom: toCoord.Y, Right: toCoord.X}
+ width := toCoord.X - fromCoord.X + 1
+ height := toCoord.Y - fromCoord.Y + 1
+ size := uint32(width) * uint32(height)
+
+ if size <= 0 {
+ return nil
+ }
+
+ buffer := make([]CHAR_INFO, size)
+
+ char := CHAR_INFO{ansiterm.FILL_CHARACTER, attributes}
+ for i := 0; i < int(size); i++ {
+ buffer[i] = char
+ }
+
+ err := WriteConsoleOutput(h.fd, buffer, COORD{X: width, Y: height}, COORD{X: 0, Y: 0}, ®ion)
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
diff --git a/vendor/github.com/Azure/go-ansiterm/winterm/scroll_helper.go b/vendor/github.com/Azure/go-ansiterm/winterm/scroll_helper.go
new file mode 100644
index 0000000000..2d27fa1d02
--- /dev/null
+++ b/vendor/github.com/Azure/go-ansiterm/winterm/scroll_helper.go
@@ -0,0 +1,118 @@
+// +build windows
+
+package winterm
+
+// effectiveSr gets the current effective scroll region in buffer coordinates
+func (h *windowsAnsiEventHandler) effectiveSr(window SMALL_RECT) scrollRegion {
+ top := addInRange(window.Top, h.sr.top, window.Top, window.Bottom)
+ bottom := addInRange(window.Top, h.sr.bottom, window.Top, window.Bottom)
+ if top >= bottom {
+ top = window.Top
+ bottom = window.Bottom
+ }
+ return scrollRegion{top: top, bottom: bottom}
+}
+
+func (h *windowsAnsiEventHandler) scrollUp(param int) error {
+ info, err := GetConsoleScreenBufferInfo(h.fd)
+ if err != nil {
+ return err
+ }
+
+ sr := h.effectiveSr(info.Window)
+ return h.scroll(param, sr, info)
+}
+
+func (h *windowsAnsiEventHandler) scrollDown(param int) error {
+ return h.scrollUp(-param)
+}
+
+func (h *windowsAnsiEventHandler) deleteLines(param int) error {
+ info, err := GetConsoleScreenBufferInfo(h.fd)
+ if err != nil {
+ return err
+ }
+
+ start := info.CursorPosition.Y
+ sr := h.effectiveSr(info.Window)
+ // Lines cannot be inserted or deleted outside the scrolling region.
+ if start >= sr.top && start <= sr.bottom {
+ sr.top = start
+ return h.scroll(param, sr, info)
+ } else {
+ return nil
+ }
+}
+
+func (h *windowsAnsiEventHandler) insertLines(param int) error {
+ return h.deleteLines(-param)
+}
+
+// scroll scrolls the provided scroll region by param lines. The scroll region is in buffer coordinates.
+func (h *windowsAnsiEventHandler) scroll(param int, sr scrollRegion, info *CONSOLE_SCREEN_BUFFER_INFO) error {
+ h.logf("scroll: scrollTop: %d, scrollBottom: %d", sr.top, sr.bottom)
+ h.logf("scroll: windowTop: %d, windowBottom: %d", info.Window.Top, info.Window.Bottom)
+
+ // Copy from and clip to the scroll region (full buffer width)
+ scrollRect := SMALL_RECT{
+ Top: sr.top,
+ Bottom: sr.bottom,
+ Left: 0,
+ Right: info.Size.X - 1,
+ }
+
+ // Origin to which area should be copied
+ destOrigin := COORD{
+ X: 0,
+ Y: sr.top - int16(param),
+ }
+
+ char := CHAR_INFO{
+ UnicodeChar: ' ',
+ Attributes: h.attributes,
+ }
+
+ if err := ScrollConsoleScreenBuffer(h.fd, scrollRect, scrollRect, destOrigin, char); err != nil {
+ return err
+ }
+ return nil
+}
+
+func (h *windowsAnsiEventHandler) deleteCharacters(param int) error {
+ info, err := GetConsoleScreenBufferInfo(h.fd)
+ if err != nil {
+ return err
+ }
+ return h.scrollLine(param, info.CursorPosition, info)
+}
+
+func (h *windowsAnsiEventHandler) insertCharacters(param int) error {
+ return h.deleteCharacters(-param)
+}
+
+// scrollLine scrolls a line horizontally starting at the provided position by a number of columns.
+func (h *windowsAnsiEventHandler) scrollLine(columns int, position COORD, info *CONSOLE_SCREEN_BUFFER_INFO) error {
+ // Copy from and clip to the scroll region (full buffer width)
+ scrollRect := SMALL_RECT{
+ Top: position.Y,
+ Bottom: position.Y,
+ Left: position.X,
+ Right: info.Size.X - 1,
+ }
+
+ // Origin to which area should be copied
+ destOrigin := COORD{
+ X: position.X - int16(columns),
+ Y: position.Y,
+ }
+
+ char := CHAR_INFO{
+ UnicodeChar: ' ',
+ Attributes: h.attributes,
+ }
+
+ if err := ScrollConsoleScreenBuffer(h.fd, scrollRect, scrollRect, destOrigin, char); err != nil {
+ return err
+ }
+ return nil
+}
diff --git a/vendor/github.com/Azure/go-ansiterm/winterm/utilities.go b/vendor/github.com/Azure/go-ansiterm/winterm/utilities.go
new file mode 100644
index 0000000000..afa7635d77
--- /dev/null
+++ b/vendor/github.com/Azure/go-ansiterm/winterm/utilities.go
@@ -0,0 +1,9 @@
+// +build windows
+
+package winterm
+
+// AddInRange increments a value by the passed quantity while ensuring the values
+// always remain within the supplied min / max range.
+func addInRange(n int16, increment int16, min int16, max int16) int16 {
+ return ensureInRange(n+increment, min, max)
+}
diff --git a/vendor/github.com/Azure/go-ansiterm/winterm/win_event_handler.go b/vendor/github.com/Azure/go-ansiterm/winterm/win_event_handler.go
new file mode 100644
index 0000000000..2d40fb75ad
--- /dev/null
+++ b/vendor/github.com/Azure/go-ansiterm/winterm/win_event_handler.go
@@ -0,0 +1,743 @@
+// +build windows
+
+package winterm
+
+import (
+ "bytes"
+ "log"
+ "os"
+ "strconv"
+
+ "github.com/Azure/go-ansiterm"
+)
+
+type windowsAnsiEventHandler struct {
+ fd uintptr
+ file *os.File
+ infoReset *CONSOLE_SCREEN_BUFFER_INFO
+ sr scrollRegion
+ buffer bytes.Buffer
+ attributes uint16
+ inverted bool
+ wrapNext bool
+ drewMarginByte bool
+ originMode bool
+ marginByte byte
+ curInfo *CONSOLE_SCREEN_BUFFER_INFO
+ curPos COORD
+ logf func(string, ...interface{})
+}
+
+type Option func(*windowsAnsiEventHandler)
+
+func WithLogf(f func(string, ...interface{})) Option {
+ return func(w *windowsAnsiEventHandler) {
+ w.logf = f
+ }
+}
+
+func CreateWinEventHandler(fd uintptr, file *os.File, opts ...Option) ansiterm.AnsiEventHandler {
+ infoReset, err := GetConsoleScreenBufferInfo(fd)
+ if err != nil {
+ return nil
+ }
+
+ h := &windowsAnsiEventHandler{
+ fd: fd,
+ file: file,
+ infoReset: infoReset,
+ attributes: infoReset.Attributes,
+ }
+ for _, o := range opts {
+ o(h)
+ }
+
+ if isDebugEnv := os.Getenv(ansiterm.LogEnv); isDebugEnv == "1" {
+ logFile, _ := os.Create("winEventHandler.log")
+ logger := log.New(logFile, "", log.LstdFlags)
+ if h.logf != nil {
+ l := h.logf
+ h.logf = func(s string, v ...interface{}) {
+ l(s, v...)
+ logger.Printf(s, v...)
+ }
+ } else {
+ h.logf = logger.Printf
+ }
+ }
+
+ if h.logf == nil {
+ h.logf = func(string, ...interface{}) {}
+ }
+
+ return h
+}
+
+type scrollRegion struct {
+ top int16
+ bottom int16
+}
+
+// simulateLF simulates a LF or CR+LF by scrolling if necessary to handle the
+// current cursor position and scroll region settings, in which case it returns
+// true. If no special handling is necessary, then it does nothing and returns
+// false.
+//
+// In the false case, the caller should ensure that a carriage return
+// and line feed are inserted or that the text is otherwise wrapped.
+func (h *windowsAnsiEventHandler) simulateLF(includeCR bool) (bool, error) {
+ if h.wrapNext {
+ if err := h.Flush(); err != nil {
+ return false, err
+ }
+ h.clearWrap()
+ }
+ pos, info, err := h.getCurrentInfo()
+ if err != nil {
+ return false, err
+ }
+ sr := h.effectiveSr(info.Window)
+ if pos.Y == sr.bottom {
+ // Scrolling is necessary. Let Windows automatically scroll if the scrolling region
+ // is the full window.
+ if sr.top == info.Window.Top && sr.bottom == info.Window.Bottom {
+ if includeCR {
+ pos.X = 0
+ h.updatePos(pos)
+ }
+ return false, nil
+ }
+
+ // A custom scroll region is active. Scroll the window manually to simulate
+ // the LF.
+ if err := h.Flush(); err != nil {
+ return false, err
+ }
+ h.logf("Simulating LF inside scroll region")
+ if err := h.scrollUp(1); err != nil {
+ return false, err
+ }
+ if includeCR {
+ pos.X = 0
+ if err := SetConsoleCursorPosition(h.fd, pos); err != nil {
+ return false, err
+ }
+ }
+ return true, nil
+
+ } else if pos.Y < info.Window.Bottom {
+ // Let Windows handle the LF.
+ pos.Y++
+ if includeCR {
+ pos.X = 0
+ }
+ h.updatePos(pos)
+ return false, nil
+ } else {
+ // The cursor is at the bottom of the screen but outside the scroll
+ // region. Skip the LF.
+ h.logf("Simulating LF outside scroll region")
+ if includeCR {
+ if err := h.Flush(); err != nil {
+ return false, err
+ }
+ pos.X = 0
+ if err := SetConsoleCursorPosition(h.fd, pos); err != nil {
+ return false, err
+ }
+ }
+ return true, nil
+ }
+}
+
+// executeLF executes a LF without a CR.
+func (h *windowsAnsiEventHandler) executeLF() error {
+ handled, err := h.simulateLF(false)
+ if err != nil {
+ return err
+ }
+ if !handled {
+ // Windows LF will reset the cursor column position. Write the LF
+ // and restore the cursor position.
+ pos, _, err := h.getCurrentInfo()
+ if err != nil {
+ return err
+ }
+ h.buffer.WriteByte(ansiterm.ANSI_LINE_FEED)
+ if pos.X != 0 {
+ if err := h.Flush(); err != nil {
+ return err
+ }
+ h.logf("Resetting cursor position for LF without CR")
+ if err := SetConsoleCursorPosition(h.fd, pos); err != nil {
+ return err
+ }
+ }
+ }
+ return nil
+}
+
+func (h *windowsAnsiEventHandler) Print(b byte) error {
+ if h.wrapNext {
+ h.buffer.WriteByte(h.marginByte)
+ h.clearWrap()
+ if _, err := h.simulateLF(true); err != nil {
+ return err
+ }
+ }
+ pos, info, err := h.getCurrentInfo()
+ if err != nil {
+ return err
+ }
+ if pos.X == info.Size.X-1 {
+ h.wrapNext = true
+ h.marginByte = b
+ } else {
+ pos.X++
+ h.updatePos(pos)
+ h.buffer.WriteByte(b)
+ }
+ return nil
+}
+
+func (h *windowsAnsiEventHandler) Execute(b byte) error {
+ switch b {
+ case ansiterm.ANSI_TAB:
+ h.logf("Execute(TAB)")
+ // Move to the next tab stop, but preserve auto-wrap if already set.
+ if !h.wrapNext {
+ pos, info, err := h.getCurrentInfo()
+ if err != nil {
+ return err
+ }
+ pos.X = (pos.X + 8) - pos.X%8
+ if pos.X >= info.Size.X {
+ pos.X = info.Size.X - 1
+ }
+ if err := h.Flush(); err != nil {
+ return err
+ }
+ if err := SetConsoleCursorPosition(h.fd, pos); err != nil {
+ return err
+ }
+ }
+ return nil
+
+ case ansiterm.ANSI_BEL:
+ h.buffer.WriteByte(ansiterm.ANSI_BEL)
+ return nil
+
+ case ansiterm.ANSI_BACKSPACE:
+ if h.wrapNext {
+ if err := h.Flush(); err != nil {
+ return err
+ }
+ h.clearWrap()
+ }
+ pos, _, err := h.getCurrentInfo()
+ if err != nil {
+ return err
+ }
+ if pos.X > 0 {
+ pos.X--
+ h.updatePos(pos)
+ h.buffer.WriteByte(ansiterm.ANSI_BACKSPACE)
+ }
+ return nil
+
+ case ansiterm.ANSI_VERTICAL_TAB, ansiterm.ANSI_FORM_FEED:
+ // Treat as true LF.
+ return h.executeLF()
+
+ case ansiterm.ANSI_LINE_FEED:
+ // Simulate a CR and LF for now since there is no way in go-ansiterm
+ // to tell if the LF should include CR (and more things break when it's
+ // missing than when it's incorrectly added).
+ handled, err := h.simulateLF(true)
+ if handled || err != nil {
+ return err
+ }
+ return h.buffer.WriteByte(ansiterm.ANSI_LINE_FEED)
+
+ case ansiterm.ANSI_CARRIAGE_RETURN:
+ if h.wrapNext {
+ if err := h.Flush(); err != nil {
+ return err
+ }
+ h.clearWrap()
+ }
+ pos, _, err := h.getCurrentInfo()
+ if err != nil {
+ return err
+ }
+ if pos.X != 0 {
+ pos.X = 0
+ h.updatePos(pos)
+ h.buffer.WriteByte(ansiterm.ANSI_CARRIAGE_RETURN)
+ }
+ return nil
+
+ default:
+ return nil
+ }
+}
+
+func (h *windowsAnsiEventHandler) CUU(param int) error {
+ if err := h.Flush(); err != nil {
+ return err
+ }
+ h.logf("CUU: [%v]", []string{strconv.Itoa(param)})
+ h.clearWrap()
+ return h.moveCursorVertical(-param)
+}
+
+func (h *windowsAnsiEventHandler) CUD(param int) error {
+ if err := h.Flush(); err != nil {
+ return err
+ }
+ h.logf("CUD: [%v]", []string{strconv.Itoa(param)})
+ h.clearWrap()
+ return h.moveCursorVertical(param)
+}
+
+func (h *windowsAnsiEventHandler) CUF(param int) error {
+ if err := h.Flush(); err != nil {
+ return err
+ }
+ h.logf("CUF: [%v]", []string{strconv.Itoa(param)})
+ h.clearWrap()
+ return h.moveCursorHorizontal(param)
+}
+
+func (h *windowsAnsiEventHandler) CUB(param int) error {
+ if err := h.Flush(); err != nil {
+ return err
+ }
+ h.logf("CUB: [%v]", []string{strconv.Itoa(param)})
+ h.clearWrap()
+ return h.moveCursorHorizontal(-param)
+}
+
+func (h *windowsAnsiEventHandler) CNL(param int) error {
+ if err := h.Flush(); err != nil {
+ return err
+ }
+ h.logf("CNL: [%v]", []string{strconv.Itoa(param)})
+ h.clearWrap()
+ return h.moveCursorLine(param)
+}
+
+func (h *windowsAnsiEventHandler) CPL(param int) error {
+ if err := h.Flush(); err != nil {
+ return err
+ }
+ h.logf("CPL: [%v]", []string{strconv.Itoa(param)})
+ h.clearWrap()
+ return h.moveCursorLine(-param)
+}
+
+func (h *windowsAnsiEventHandler) CHA(param int) error {
+ if err := h.Flush(); err != nil {
+ return err
+ }
+ h.logf("CHA: [%v]", []string{strconv.Itoa(param)})
+ h.clearWrap()
+ return h.moveCursorColumn(param)
+}
+
+func (h *windowsAnsiEventHandler) VPA(param int) error {
+ if err := h.Flush(); err != nil {
+ return err
+ }
+ h.logf("VPA: [[%d]]", param)
+ h.clearWrap()
+ info, err := GetConsoleScreenBufferInfo(h.fd)
+ if err != nil {
+ return err
+ }
+ window := h.getCursorWindow(info)
+ position := info.CursorPosition
+ position.Y = window.Top + int16(param) - 1
+ return h.setCursorPosition(position, window)
+}
+
+func (h *windowsAnsiEventHandler) CUP(row int, col int) error {
+ if err := h.Flush(); err != nil {
+ return err
+ }
+ h.logf("CUP: [[%d %d]]", row, col)
+ h.clearWrap()
+ info, err := GetConsoleScreenBufferInfo(h.fd)
+ if err != nil {
+ return err
+ }
+
+ window := h.getCursorWindow(info)
+ position := COORD{window.Left + int16(col) - 1, window.Top + int16(row) - 1}
+ return h.setCursorPosition(position, window)
+}
+
+func (h *windowsAnsiEventHandler) HVP(row int, col int) error {
+ if err := h.Flush(); err != nil {
+ return err
+ }
+ h.logf("HVP: [[%d %d]]", row, col)
+ h.clearWrap()
+ return h.CUP(row, col)
+}
+
+func (h *windowsAnsiEventHandler) DECTCEM(visible bool) error {
+ if err := h.Flush(); err != nil {
+ return err
+ }
+ h.logf("DECTCEM: [%v]", []string{strconv.FormatBool(visible)})
+ h.clearWrap()
+ return nil
+}
+
+func (h *windowsAnsiEventHandler) DECOM(enable bool) error {
+ if err := h.Flush(); err != nil {
+ return err
+ }
+ h.logf("DECOM: [%v]", []string{strconv.FormatBool(enable)})
+ h.clearWrap()
+ h.originMode = enable
+ return h.CUP(1, 1)
+}
+
+func (h *windowsAnsiEventHandler) DECCOLM(use132 bool) error {
+ if err := h.Flush(); err != nil {
+ return err
+ }
+ h.logf("DECCOLM: [%v]", []string{strconv.FormatBool(use132)})
+ h.clearWrap()
+ if err := h.ED(2); err != nil {
+ return err
+ }
+ info, err := GetConsoleScreenBufferInfo(h.fd)
+ if err != nil {
+ return err
+ }
+ targetWidth := int16(80)
+ if use132 {
+ targetWidth = 132
+ }
+ if info.Size.X < targetWidth {
+ if err := SetConsoleScreenBufferSize(h.fd, COORD{targetWidth, info.Size.Y}); err != nil {
+ h.logf("set buffer failed: %v", err)
+ return err
+ }
+ }
+ window := info.Window
+ window.Left = 0
+ window.Right = targetWidth - 1
+ if err := SetConsoleWindowInfo(h.fd, true, window); err != nil {
+ h.logf("set window failed: %v", err)
+ return err
+ }
+ if info.Size.X > targetWidth {
+ if err := SetConsoleScreenBufferSize(h.fd, COORD{targetWidth, info.Size.Y}); err != nil {
+ h.logf("set buffer failed: %v", err)
+ return err
+ }
+ }
+ return SetConsoleCursorPosition(h.fd, COORD{0, 0})
+}
+
+func (h *windowsAnsiEventHandler) ED(param int) error {
+ if err := h.Flush(); err != nil {
+ return err
+ }
+ h.logf("ED: [%v]", []string{strconv.Itoa(param)})
+ h.clearWrap()
+
+ // [J -- Erases from the cursor to the end of the screen, including the cursor position.
+ // [1J -- Erases from the beginning of the screen to the cursor, including the cursor position.
+ // [2J -- Erases the complete display. The cursor does not move.
+ // Notes:
+ // -- Clearing the entire buffer, versus just the Window, works best for Windows Consoles
+
+ info, err := GetConsoleScreenBufferInfo(h.fd)
+ if err != nil {
+ return err
+ }
+
+ var start COORD
+ var end COORD
+
+ switch param {
+ case 0:
+ start = info.CursorPosition
+ end = COORD{info.Size.X - 1, info.Size.Y - 1}
+
+ case 1:
+ start = COORD{0, 0}
+ end = info.CursorPosition
+
+ case 2:
+ start = COORD{0, 0}
+ end = COORD{info.Size.X - 1, info.Size.Y - 1}
+ }
+
+ err = h.clearRange(h.attributes, start, end)
+ if err != nil {
+ return err
+ }
+
+ // If the whole buffer was cleared, move the window to the top while preserving
+ // the window-relative cursor position.
+ if param == 2 {
+ pos := info.CursorPosition
+ window := info.Window
+ pos.Y -= window.Top
+ window.Bottom -= window.Top
+ window.Top = 0
+ if err := SetConsoleCursorPosition(h.fd, pos); err != nil {
+ return err
+ }
+ if err := SetConsoleWindowInfo(h.fd, true, window); err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+func (h *windowsAnsiEventHandler) EL(param int) error {
+ if err := h.Flush(); err != nil {
+ return err
+ }
+ h.logf("EL: [%v]", strconv.Itoa(param))
+ h.clearWrap()
+
+ // [K -- Erases from the cursor to the end of the line, including the cursor position.
+ // [1K -- Erases from the beginning of the line to the cursor, including the cursor position.
+ // [2K -- Erases the complete line.
+
+ info, err := GetConsoleScreenBufferInfo(h.fd)
+ if err != nil {
+ return err
+ }
+
+ var start COORD
+ var end COORD
+
+ switch param {
+ case 0:
+ start = info.CursorPosition
+ end = COORD{info.Size.X, info.CursorPosition.Y}
+
+ case 1:
+ start = COORD{0, info.CursorPosition.Y}
+ end = info.CursorPosition
+
+ case 2:
+ start = COORD{0, info.CursorPosition.Y}
+ end = COORD{info.Size.X, info.CursorPosition.Y}
+ }
+
+ err = h.clearRange(h.attributes, start, end)
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func (h *windowsAnsiEventHandler) IL(param int) error {
+ if err := h.Flush(); err != nil {
+ return err
+ }
+ h.logf("IL: [%v]", strconv.Itoa(param))
+ h.clearWrap()
+ return h.insertLines(param)
+}
+
+func (h *windowsAnsiEventHandler) DL(param int) error {
+ if err := h.Flush(); err != nil {
+ return err
+ }
+ h.logf("DL: [%v]", strconv.Itoa(param))
+ h.clearWrap()
+ return h.deleteLines(param)
+}
+
+func (h *windowsAnsiEventHandler) ICH(param int) error {
+ if err := h.Flush(); err != nil {
+ return err
+ }
+ h.logf("ICH: [%v]", strconv.Itoa(param))
+ h.clearWrap()
+ return h.insertCharacters(param)
+}
+
+func (h *windowsAnsiEventHandler) DCH(param int) error {
+ if err := h.Flush(); err != nil {
+ return err
+ }
+ h.logf("DCH: [%v]", strconv.Itoa(param))
+ h.clearWrap()
+ return h.deleteCharacters(param)
+}
+
+func (h *windowsAnsiEventHandler) SGR(params []int) error {
+ if err := h.Flush(); err != nil {
+ return err
+ }
+ strings := []string{}
+ for _, v := range params {
+ strings = append(strings, strconv.Itoa(v))
+ }
+
+ h.logf("SGR: [%v]", strings)
+
+ if len(params) <= 0 {
+ h.attributes = h.infoReset.Attributes
+ h.inverted = false
+ } else {
+ for _, attr := range params {
+
+ if attr == ansiterm.ANSI_SGR_RESET {
+ h.attributes = h.infoReset.Attributes
+ h.inverted = false
+ continue
+ }
+
+ h.attributes, h.inverted = collectAnsiIntoWindowsAttributes(h.attributes, h.inverted, h.infoReset.Attributes, int16(attr))
+ }
+ }
+
+ attributes := h.attributes
+ if h.inverted {
+ attributes = invertAttributes(attributes)
+ }
+ err := SetConsoleTextAttribute(h.fd, attributes)
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func (h *windowsAnsiEventHandler) SU(param int) error {
+ if err := h.Flush(); err != nil {
+ return err
+ }
+ h.logf("SU: [%v]", []string{strconv.Itoa(param)})
+ h.clearWrap()
+ return h.scrollUp(param)
+}
+
+func (h *windowsAnsiEventHandler) SD(param int) error {
+ if err := h.Flush(); err != nil {
+ return err
+ }
+ h.logf("SD: [%v]", []string{strconv.Itoa(param)})
+ h.clearWrap()
+ return h.scrollDown(param)
+}
+
+func (h *windowsAnsiEventHandler) DA(params []string) error {
+ h.logf("DA: [%v]", params)
+ // DA cannot be implemented because it must send data on the VT100 input stream,
+ // which is not available to go-ansiterm.
+ return nil
+}
+
+func (h *windowsAnsiEventHandler) DECSTBM(top int, bottom int) error {
+ if err := h.Flush(); err != nil {
+ return err
+ }
+ h.logf("DECSTBM: [%d, %d]", top, bottom)
+
+ // Windows is 0 indexed, Linux is 1 indexed
+ h.sr.top = int16(top - 1)
+ h.sr.bottom = int16(bottom - 1)
+
+ // This command also moves the cursor to the origin.
+ h.clearWrap()
+ return h.CUP(1, 1)
+}
+
+func (h *windowsAnsiEventHandler) RI() error {
+ if err := h.Flush(); err != nil {
+ return err
+ }
+ h.logf("RI: []")
+ h.clearWrap()
+
+ info, err := GetConsoleScreenBufferInfo(h.fd)
+ if err != nil {
+ return err
+ }
+
+ sr := h.effectiveSr(info.Window)
+ if info.CursorPosition.Y == sr.top {
+ return h.scrollDown(1)
+ }
+
+ return h.moveCursorVertical(-1)
+}
+
+func (h *windowsAnsiEventHandler) IND() error {
+ h.logf("IND: []")
+ return h.executeLF()
+}
+
+func (h *windowsAnsiEventHandler) Flush() error {
+ h.curInfo = nil
+ if h.buffer.Len() > 0 {
+ h.logf("Flush: [%s]", h.buffer.Bytes())
+ if _, err := h.buffer.WriteTo(h.file); err != nil {
+ return err
+ }
+ }
+
+ if h.wrapNext && !h.drewMarginByte {
+ h.logf("Flush: drawing margin byte '%c'", h.marginByte)
+
+ info, err := GetConsoleScreenBufferInfo(h.fd)
+ if err != nil {
+ return err
+ }
+
+ charInfo := []CHAR_INFO{{UnicodeChar: uint16(h.marginByte), Attributes: info.Attributes}}
+ size := COORD{1, 1}
+ position := COORD{0, 0}
+ region := SMALL_RECT{Left: info.CursorPosition.X, Top: info.CursorPosition.Y, Right: info.CursorPosition.X, Bottom: info.CursorPosition.Y}
+ if err := WriteConsoleOutput(h.fd, charInfo, size, position, ®ion); err != nil {
+ return err
+ }
+ h.drewMarginByte = true
+ }
+ return nil
+}
+
+// cacheConsoleInfo ensures that the current console screen information has been queried
+// since the last call to Flush(). It must be called before accessing h.curInfo or h.curPos.
+func (h *windowsAnsiEventHandler) getCurrentInfo() (COORD, *CONSOLE_SCREEN_BUFFER_INFO, error) {
+ if h.curInfo == nil {
+ info, err := GetConsoleScreenBufferInfo(h.fd)
+ if err != nil {
+ return COORD{}, nil, err
+ }
+ h.curInfo = info
+ h.curPos = info.CursorPosition
+ }
+ return h.curPos, h.curInfo, nil
+}
+
+func (h *windowsAnsiEventHandler) updatePos(pos COORD) {
+ if h.curInfo == nil {
+ panic("failed to call getCurrentInfo before calling updatePos")
+ }
+ h.curPos = pos
+}
+
+// clearWrap clears the state where the cursor is in the margin
+// waiting for the next character before wrapping the line. This must
+// be done before most operations that act on the cursor.
+func (h *windowsAnsiEventHandler) clearWrap() {
+ h.wrapNext = false
+ h.drewMarginByte = false
+}
diff --git a/vendor/github.com/cenkalti/backoff/v4/.gitignore b/vendor/github.com/cenkalti/backoff/v4/.gitignore
new file mode 100644
index 0000000000..50d95c548b
--- /dev/null
+++ b/vendor/github.com/cenkalti/backoff/v4/.gitignore
@@ -0,0 +1,25 @@
+# Compiled Object files, Static and Dynamic libs (Shared Objects)
+*.o
+*.a
+*.so
+
+# Folders
+_obj
+_test
+
+# Architecture specific extensions/prefixes
+*.[568vq]
+[568vq].out
+
+*.cgo1.go
+*.cgo2.c
+_cgo_defun.c
+_cgo_gotypes.go
+_cgo_export.*
+
+_testmain.go
+
+*.exe
+
+# IDEs
+.idea/
diff --git a/vendor/github.com/cenkalti/backoff/v4/LICENSE b/vendor/github.com/cenkalti/backoff/v4/LICENSE
new file mode 100644
index 0000000000..89b8179965
--- /dev/null
+++ b/vendor/github.com/cenkalti/backoff/v4/LICENSE
@@ -0,0 +1,20 @@
+The MIT License (MIT)
+
+Copyright (c) 2014 Cenk Altı
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/vendor/github.com/cenkalti/backoff/v4/README.md b/vendor/github.com/cenkalti/backoff/v4/README.md
new file mode 100644
index 0000000000..9433004a28
--- /dev/null
+++ b/vendor/github.com/cenkalti/backoff/v4/README.md
@@ -0,0 +1,30 @@
+# Exponential Backoff [![GoDoc][godoc image]][godoc] [![Coverage Status][coveralls image]][coveralls]
+
+This is a Go port of the exponential backoff algorithm from [Google's HTTP Client Library for Java][google-http-java-client].
+
+[Exponential backoff][exponential backoff wiki]
+is an algorithm that uses feedback to multiplicatively decrease the rate of some process,
+in order to gradually find an acceptable rate.
+The retries exponentially increase and stop increasing when a certain threshold is met.
+
+## Usage
+
+Import path is `github.com/cenkalti/backoff/v4`. Please note the version part at the end.
+
+Use https://pkg.go.dev/github.com/cenkalti/backoff/v4 to view the documentation.
+
+## Contributing
+
+* I would like to keep this library as small as possible.
+* Please don't send a PR without opening an issue and discussing it first.
+* If proposed change is not a common use case, I will probably not accept it.
+
+[godoc]: https://pkg.go.dev/github.com/cenkalti/backoff/v4
+[godoc image]: https://godoc.org/github.com/cenkalti/backoff?status.png
+[coveralls]: https://coveralls.io/github/cenkalti/backoff?branch=master
+[coveralls image]: https://coveralls.io/repos/github/cenkalti/backoff/badge.svg?branch=master
+
+[google-http-java-client]: https://github.com/google/google-http-java-client/blob/da1aa993e90285ec18579f1553339b00e19b3ab5/google-http-client/src/main/java/com/google/api/client/util/ExponentialBackOff.java
+[exponential backoff wiki]: http://en.wikipedia.org/wiki/Exponential_backoff
+
+[advanced example]: https://pkg.go.dev/github.com/cenkalti/backoff/v4?tab=doc#pkg-examples
diff --git a/vendor/github.com/cenkalti/backoff/v4/backoff.go b/vendor/github.com/cenkalti/backoff/v4/backoff.go
new file mode 100644
index 0000000000..3676ee405d
--- /dev/null
+++ b/vendor/github.com/cenkalti/backoff/v4/backoff.go
@@ -0,0 +1,66 @@
+// Package backoff implements backoff algorithms for retrying operations.
+//
+// Use Retry function for retrying operations that may fail.
+// If Retry does not meet your needs,
+// copy/paste the function into your project and modify as you wish.
+//
+// There is also Ticker type similar to time.Ticker.
+// You can use it if you need to work with channels.
+//
+// See Examples section below for usage examples.
+package backoff
+
+import "time"
+
+// BackOff is a backoff policy for retrying an operation.
+type BackOff interface {
+ // NextBackOff returns the duration to wait before retrying the operation,
+ // or backoff. Stop to indicate that no more retries should be made.
+ //
+ // Example usage:
+ //
+ // duration := backoff.NextBackOff();
+ // if (duration == backoff.Stop) {
+ // // Do not retry operation.
+ // } else {
+ // // Sleep for duration and retry operation.
+ // }
+ //
+ NextBackOff() time.Duration
+
+ // Reset to initial state.
+ Reset()
+}
+
+// Stop indicates that no more retries should be made for use in NextBackOff().
+const Stop time.Duration = -1
+
+// ZeroBackOff is a fixed backoff policy whose backoff time is always zero,
+// meaning that the operation is retried immediately without waiting, indefinitely.
+type ZeroBackOff struct{}
+
+func (b *ZeroBackOff) Reset() {}
+
+func (b *ZeroBackOff) NextBackOff() time.Duration { return 0 }
+
+// StopBackOff is a fixed backoff policy that always returns backoff.Stop for
+// NextBackOff(), meaning that the operation should never be retried.
+type StopBackOff struct{}
+
+func (b *StopBackOff) Reset() {}
+
+func (b *StopBackOff) NextBackOff() time.Duration { return Stop }
+
+// ConstantBackOff is a backoff policy that always returns the same backoff delay.
+// This is in contrast to an exponential backoff policy,
+// which returns a delay that grows longer as you call NextBackOff() over and over again.
+type ConstantBackOff struct {
+ Interval time.Duration
+}
+
+func (b *ConstantBackOff) Reset() {}
+func (b *ConstantBackOff) NextBackOff() time.Duration { return b.Interval }
+
+func NewConstantBackOff(d time.Duration) *ConstantBackOff {
+ return &ConstantBackOff{Interval: d}
+}
diff --git a/vendor/github.com/cenkalti/backoff/v4/context.go b/vendor/github.com/cenkalti/backoff/v4/context.go
new file mode 100644
index 0000000000..48482330eb
--- /dev/null
+++ b/vendor/github.com/cenkalti/backoff/v4/context.go
@@ -0,0 +1,62 @@
+package backoff
+
+import (
+ "context"
+ "time"
+)
+
+// BackOffContext is a backoff policy that stops retrying after the context
+// is canceled.
+type BackOffContext interface { // nolint: golint
+ BackOff
+ Context() context.Context
+}
+
+type backOffContext struct {
+ BackOff
+ ctx context.Context
+}
+
+// WithContext returns a BackOffContext with context ctx
+//
+// ctx must not be nil
+func WithContext(b BackOff, ctx context.Context) BackOffContext { // nolint: golint
+ if ctx == nil {
+ panic("nil context")
+ }
+
+ if b, ok := b.(*backOffContext); ok {
+ return &backOffContext{
+ BackOff: b.BackOff,
+ ctx: ctx,
+ }
+ }
+
+ return &backOffContext{
+ BackOff: b,
+ ctx: ctx,
+ }
+}
+
+func getContext(b BackOff) context.Context {
+ if cb, ok := b.(BackOffContext); ok {
+ return cb.Context()
+ }
+ if tb, ok := b.(*backOffTries); ok {
+ return getContext(tb.delegate)
+ }
+ return context.Background()
+}
+
+func (b *backOffContext) Context() context.Context {
+ return b.ctx
+}
+
+func (b *backOffContext) NextBackOff() time.Duration {
+ select {
+ case <-b.ctx.Done():
+ return Stop
+ default:
+ return b.BackOff.NextBackOff()
+ }
+}
diff --git a/vendor/github.com/cenkalti/backoff/v4/exponential.go b/vendor/github.com/cenkalti/backoff/v4/exponential.go
new file mode 100644
index 0000000000..aac99f196a
--- /dev/null
+++ b/vendor/github.com/cenkalti/backoff/v4/exponential.go
@@ -0,0 +1,216 @@
+package backoff
+
+import (
+ "math/rand"
+ "time"
+)
+
+/*
+ExponentialBackOff is a backoff implementation that increases the backoff
+period for each retry attempt using a randomization function that grows exponentially.
+
+NextBackOff() is calculated using the following formula:
+
+ randomized interval =
+ RetryInterval * (random value in range [1 - RandomizationFactor, 1 + RandomizationFactor])
+
+In other words NextBackOff() will range between the randomization factor
+percentage below and above the retry interval.
+
+For example, given the following parameters:
+
+ RetryInterval = 2
+ RandomizationFactor = 0.5
+ Multiplier = 2
+
+the actual backoff period used in the next retry attempt will range between 1 and 3 seconds,
+multiplied by the exponential, that is, between 2 and 6 seconds.
+
+Note: MaxInterval caps the RetryInterval and not the randomized interval.
+
+If the time elapsed since an ExponentialBackOff instance is created goes past the
+MaxElapsedTime, then the method NextBackOff() starts returning backoff.Stop.
+
+The elapsed time can be reset by calling Reset().
+
+Example: Given the following default arguments, for 10 tries the sequence will be,
+and assuming we go over the MaxElapsedTime on the 10th try:
+
+ Request # RetryInterval (seconds) Randomized Interval (seconds)
+
+ 1 0.5 [0.25, 0.75]
+ 2 0.75 [0.375, 1.125]
+ 3 1.125 [0.562, 1.687]
+ 4 1.687 [0.8435, 2.53]
+ 5 2.53 [1.265, 3.795]
+ 6 3.795 [1.897, 5.692]
+ 7 5.692 [2.846, 8.538]
+ 8 8.538 [4.269, 12.807]
+ 9 12.807 [6.403, 19.210]
+ 10 19.210 backoff.Stop
+
+Note: Implementation is not thread-safe.
+*/
+type ExponentialBackOff struct {
+ InitialInterval time.Duration
+ RandomizationFactor float64
+ Multiplier float64
+ MaxInterval time.Duration
+ // After MaxElapsedTime the ExponentialBackOff returns Stop.
+ // It never stops if MaxElapsedTime == 0.
+ MaxElapsedTime time.Duration
+ Stop time.Duration
+ Clock Clock
+
+ currentInterval time.Duration
+ startTime time.Time
+}
+
+// Clock is an interface that returns current time for BackOff.
+type Clock interface {
+ Now() time.Time
+}
+
+// ExponentialBackOffOpts is a function type used to configure ExponentialBackOff options.
+type ExponentialBackOffOpts func(*ExponentialBackOff)
+
+// Default values for ExponentialBackOff.
+const (
+ DefaultInitialInterval = 500 * time.Millisecond
+ DefaultRandomizationFactor = 0.5
+ DefaultMultiplier = 1.5
+ DefaultMaxInterval = 60 * time.Second
+ DefaultMaxElapsedTime = 15 * time.Minute
+)
+
+// NewExponentialBackOff creates an instance of ExponentialBackOff using default values.
+func NewExponentialBackOff(opts ...ExponentialBackOffOpts) *ExponentialBackOff {
+ b := &ExponentialBackOff{
+ InitialInterval: DefaultInitialInterval,
+ RandomizationFactor: DefaultRandomizationFactor,
+ Multiplier: DefaultMultiplier,
+ MaxInterval: DefaultMaxInterval,
+ MaxElapsedTime: DefaultMaxElapsedTime,
+ Stop: Stop,
+ Clock: SystemClock,
+ }
+ for _, fn := range opts {
+ fn(b)
+ }
+ b.Reset()
+ return b
+}
+
+// WithInitialInterval sets the initial interval between retries.
+func WithInitialInterval(duration time.Duration) ExponentialBackOffOpts {
+ return func(ebo *ExponentialBackOff) {
+ ebo.InitialInterval = duration
+ }
+}
+
+// WithRandomizationFactor sets the randomization factor to add jitter to intervals.
+func WithRandomizationFactor(randomizationFactor float64) ExponentialBackOffOpts {
+ return func(ebo *ExponentialBackOff) {
+ ebo.RandomizationFactor = randomizationFactor
+ }
+}
+
+// WithMultiplier sets the multiplier for increasing the interval after each retry.
+func WithMultiplier(multiplier float64) ExponentialBackOffOpts {
+ return func(ebo *ExponentialBackOff) {
+ ebo.Multiplier = multiplier
+ }
+}
+
+// WithMaxInterval sets the maximum interval between retries.
+func WithMaxInterval(duration time.Duration) ExponentialBackOffOpts {
+ return func(ebo *ExponentialBackOff) {
+ ebo.MaxInterval = duration
+ }
+}
+
+// WithMaxElapsedTime sets the maximum total time for retries.
+func WithMaxElapsedTime(duration time.Duration) ExponentialBackOffOpts {
+ return func(ebo *ExponentialBackOff) {
+ ebo.MaxElapsedTime = duration
+ }
+}
+
+// WithRetryStopDuration sets the duration after which retries should stop.
+func WithRetryStopDuration(duration time.Duration) ExponentialBackOffOpts {
+ return func(ebo *ExponentialBackOff) {
+ ebo.Stop = duration
+ }
+}
+
+// WithClockProvider sets the clock used to measure time.
+func WithClockProvider(clock Clock) ExponentialBackOffOpts {
+ return func(ebo *ExponentialBackOff) {
+ ebo.Clock = clock
+ }
+}
+
+type systemClock struct{}
+
+func (t systemClock) Now() time.Time {
+ return time.Now()
+}
+
+// SystemClock implements Clock interface that uses time.Now().
+var SystemClock = systemClock{}
+
+// Reset the interval back to the initial retry interval and restarts the timer.
+// Reset must be called before using b.
+func (b *ExponentialBackOff) Reset() {
+ b.currentInterval = b.InitialInterval
+ b.startTime = b.Clock.Now()
+}
+
+// NextBackOff calculates the next backoff interval using the formula:
+// Randomized interval = RetryInterval * (1 ± RandomizationFactor)
+func (b *ExponentialBackOff) NextBackOff() time.Duration {
+ // Make sure we have not gone over the maximum elapsed time.
+ elapsed := b.GetElapsedTime()
+ next := getRandomValueFromInterval(b.RandomizationFactor, rand.Float64(), b.currentInterval)
+ b.incrementCurrentInterval()
+ if b.MaxElapsedTime != 0 && elapsed+next > b.MaxElapsedTime {
+ return b.Stop
+ }
+ return next
+}
+
+// GetElapsedTime returns the elapsed time since an ExponentialBackOff instance
+// is created and is reset when Reset() is called.
+//
+// The elapsed time is computed using time.Now().UnixNano(). It is
+// safe to call even while the backoff policy is used by a running
+// ticker.
+func (b *ExponentialBackOff) GetElapsedTime() time.Duration {
+ return b.Clock.Now().Sub(b.startTime)
+}
+
+// Increments the current interval by multiplying it with the multiplier.
+func (b *ExponentialBackOff) incrementCurrentInterval() {
+ // Check for overflow, if overflow is detected set the current interval to the max interval.
+ if float64(b.currentInterval) >= float64(b.MaxInterval)/b.Multiplier {
+ b.currentInterval = b.MaxInterval
+ } else {
+ b.currentInterval = time.Duration(float64(b.currentInterval) * b.Multiplier)
+ }
+}
+
+// Returns a random value from the following interval:
+// [currentInterval - randomizationFactor * currentInterval, currentInterval + randomizationFactor * currentInterval].
+func getRandomValueFromInterval(randomizationFactor, random float64, currentInterval time.Duration) time.Duration {
+ if randomizationFactor == 0 {
+ return currentInterval // make sure no randomness is used when randomizationFactor is 0.
+ }
+ var delta = randomizationFactor * float64(currentInterval)
+ var minInterval = float64(currentInterval) - delta
+ var maxInterval = float64(currentInterval) + delta
+
+ // Get a random value from the range [minInterval, maxInterval].
+ // The formula used below has a +1 because if the minInterval is 1 and the maxInterval is 3 then
+ // we want a 33% chance for selecting either 1, 2 or 3.
+ return time.Duration(minInterval + (random * (maxInterval - minInterval + 1)))
+}
diff --git a/vendor/github.com/cenkalti/backoff/v4/retry.go b/vendor/github.com/cenkalti/backoff/v4/retry.go
new file mode 100644
index 0000000000..b9c0c51cd7
--- /dev/null
+++ b/vendor/github.com/cenkalti/backoff/v4/retry.go
@@ -0,0 +1,146 @@
+package backoff
+
+import (
+ "errors"
+ "time"
+)
+
+// An OperationWithData is executing by RetryWithData() or RetryNotifyWithData().
+// The operation will be retried using a backoff policy if it returns an error.
+type OperationWithData[T any] func() (T, error)
+
+// An Operation is executing by Retry() or RetryNotify().
+// The operation will be retried using a backoff policy if it returns an error.
+type Operation func() error
+
+func (o Operation) withEmptyData() OperationWithData[struct{}] {
+ return func() (struct{}, error) {
+ return struct{}{}, o()
+ }
+}
+
+// Notify is a notify-on-error function. It receives an operation error and
+// backoff delay if the operation failed (with an error).
+//
+// NOTE that if the backoff policy stated to stop retrying,
+// the notify function isn't called.
+type Notify func(error, time.Duration)
+
+// Retry the operation o until it does not return error or BackOff stops.
+// o is guaranteed to be run at least once.
+//
+// If o returns a *PermanentError, the operation is not retried, and the
+// wrapped error is returned.
+//
+// Retry sleeps the goroutine for the duration returned by BackOff after a
+// failed operation returns.
+func Retry(o Operation, b BackOff) error {
+ return RetryNotify(o, b, nil)
+}
+
+// RetryWithData is like Retry but returns data in the response too.
+func RetryWithData[T any](o OperationWithData[T], b BackOff) (T, error) {
+ return RetryNotifyWithData(o, b, nil)
+}
+
+// RetryNotify calls notify function with the error and wait duration
+// for each failed attempt before sleep.
+func RetryNotify(operation Operation, b BackOff, notify Notify) error {
+ return RetryNotifyWithTimer(operation, b, notify, nil)
+}
+
+// RetryNotifyWithData is like RetryNotify but returns data in the response too.
+func RetryNotifyWithData[T any](operation OperationWithData[T], b BackOff, notify Notify) (T, error) {
+ return doRetryNotify(operation, b, notify, nil)
+}
+
+// RetryNotifyWithTimer calls notify function with the error and wait duration using the given Timer
+// for each failed attempt before sleep.
+// A default timer that uses system timer is used when nil is passed.
+func RetryNotifyWithTimer(operation Operation, b BackOff, notify Notify, t Timer) error {
+ _, err := doRetryNotify(operation.withEmptyData(), b, notify, t)
+ return err
+}
+
+// RetryNotifyWithTimerAndData is like RetryNotifyWithTimer but returns data in the response too.
+func RetryNotifyWithTimerAndData[T any](operation OperationWithData[T], b BackOff, notify Notify, t Timer) (T, error) {
+ return doRetryNotify(operation, b, notify, t)
+}
+
+func doRetryNotify[T any](operation OperationWithData[T], b BackOff, notify Notify, t Timer) (T, error) {
+ var (
+ err error
+ next time.Duration
+ res T
+ )
+ if t == nil {
+ t = &defaultTimer{}
+ }
+
+ defer func() {
+ t.Stop()
+ }()
+
+ ctx := getContext(b)
+
+ b.Reset()
+ for {
+ res, err = operation()
+ if err == nil {
+ return res, nil
+ }
+
+ var permanent *PermanentError
+ if errors.As(err, &permanent) {
+ return res, permanent.Err
+ }
+
+ if next = b.NextBackOff(); next == Stop {
+ if cerr := ctx.Err(); cerr != nil {
+ return res, cerr
+ }
+
+ return res, err
+ }
+
+ if notify != nil {
+ notify(err, next)
+ }
+
+ t.Start(next)
+
+ select {
+ case <-ctx.Done():
+ return res, ctx.Err()
+ case <-t.C():
+ }
+ }
+}
+
+// PermanentError signals that the operation should not be retried.
+type PermanentError struct {
+ Err error
+}
+
+func (e *PermanentError) Error() string {
+ return e.Err.Error()
+}
+
+func (e *PermanentError) Unwrap() error {
+ return e.Err
+}
+
+func (e *PermanentError) Is(target error) bool {
+ _, ok := target.(*PermanentError)
+ return ok
+}
+
+// Permanent wraps the given err in a *PermanentError.
+func Permanent(err error) error {
+ if err == nil {
+ return nil
+ }
+ return &PermanentError{
+ Err: err,
+ }
+}
diff --git a/vendor/github.com/cenkalti/backoff/v4/ticker.go b/vendor/github.com/cenkalti/backoff/v4/ticker.go
new file mode 100644
index 0000000000..df9d68bce5
--- /dev/null
+++ b/vendor/github.com/cenkalti/backoff/v4/ticker.go
@@ -0,0 +1,97 @@
+package backoff
+
+import (
+ "context"
+ "sync"
+ "time"
+)
+
+// Ticker holds a channel that delivers `ticks' of a clock at times reported by a BackOff.
+//
+// Ticks will continue to arrive when the previous operation is still running,
+// so operations that take a while to fail could run in quick succession.
+type Ticker struct {
+ C <-chan time.Time
+ c chan time.Time
+ b BackOff
+ ctx context.Context
+ timer Timer
+ stop chan struct{}
+ stopOnce sync.Once
+}
+
+// NewTicker returns a new Ticker containing a channel that will send
+// the time at times specified by the BackOff argument. Ticker is
+// guaranteed to tick at least once. The channel is closed when Stop
+// method is called or BackOff stops. It is not safe to manipulate the
+// provided backoff policy (notably calling NextBackOff or Reset)
+// while the ticker is running.
+func NewTicker(b BackOff) *Ticker {
+ return NewTickerWithTimer(b, &defaultTimer{})
+}
+
+// NewTickerWithTimer returns a new Ticker with a custom timer.
+// A default timer that uses system timer is used when nil is passed.
+func NewTickerWithTimer(b BackOff, timer Timer) *Ticker {
+ if timer == nil {
+ timer = &defaultTimer{}
+ }
+ c := make(chan time.Time)
+ t := &Ticker{
+ C: c,
+ c: c,
+ b: b,
+ ctx: getContext(b),
+ timer: timer,
+ stop: make(chan struct{}),
+ }
+ t.b.Reset()
+ go t.run()
+ return t
+}
+
+// Stop turns off a ticker. After Stop, no more ticks will be sent.
+func (t *Ticker) Stop() {
+ t.stopOnce.Do(func() { close(t.stop) })
+}
+
+func (t *Ticker) run() {
+ c := t.c
+ defer close(c)
+
+ // Ticker is guaranteed to tick at least once.
+ afterC := t.send(time.Now())
+
+ for {
+ if afterC == nil {
+ return
+ }
+
+ select {
+ case tick := <-afterC:
+ afterC = t.send(tick)
+ case <-t.stop:
+ t.c = nil // Prevent future ticks from being sent to the channel.
+ return
+ case <-t.ctx.Done():
+ return
+ }
+ }
+}
+
+func (t *Ticker) send(tick time.Time) <-chan time.Time {
+ select {
+ case t.c <- tick:
+ case <-t.stop:
+ return nil
+ }
+
+ next := t.b.NextBackOff()
+ if next == Stop {
+ t.Stop()
+ return nil
+ }
+
+ t.timer.Start(next)
+ return t.timer.C()
+}
diff --git a/vendor/github.com/cenkalti/backoff/v4/timer.go b/vendor/github.com/cenkalti/backoff/v4/timer.go
new file mode 100644
index 0000000000..8120d0213c
--- /dev/null
+++ b/vendor/github.com/cenkalti/backoff/v4/timer.go
@@ -0,0 +1,35 @@
+package backoff
+
+import "time"
+
+type Timer interface {
+ Start(duration time.Duration)
+ Stop()
+ C() <-chan time.Time
+}
+
+// defaultTimer implements Timer interface using time.Timer
+type defaultTimer struct {
+ timer *time.Timer
+}
+
+// C returns the timers channel which receives the current time when the timer fires.
+func (t *defaultTimer) C() <-chan time.Time {
+ return t.timer.C
+}
+
+// Start starts the timer to fire after the given duration
+func (t *defaultTimer) Start(duration time.Duration) {
+ if t.timer == nil {
+ t.timer = time.NewTimer(duration)
+ } else {
+ t.timer.Reset(duration)
+ }
+}
+
+// Stop is called when the timer is not used anymore and resources may be freed.
+func (t *defaultTimer) Stop() {
+ if t.timer != nil {
+ t.timer.Stop()
+ }
+}
diff --git a/vendor/github.com/cenkalti/backoff/v4/tries.go b/vendor/github.com/cenkalti/backoff/v4/tries.go
new file mode 100644
index 0000000000..28d58ca37c
--- /dev/null
+++ b/vendor/github.com/cenkalti/backoff/v4/tries.go
@@ -0,0 +1,38 @@
+package backoff
+
+import "time"
+
+/*
+WithMaxRetries creates a wrapper around another BackOff, which will
+return Stop if NextBackOff() has been called too many times since
+the last time Reset() was called
+
+Note: Implementation is not thread-safe.
+*/
+func WithMaxRetries(b BackOff, max uint64) BackOff {
+ return &backOffTries{delegate: b, maxTries: max}
+}
+
+type backOffTries struct {
+ delegate BackOff
+ maxTries uint64
+ numTries uint64
+}
+
+func (b *backOffTries) NextBackOff() time.Duration {
+ if b.maxTries == 0 {
+ return Stop
+ }
+ if b.maxTries > 0 {
+ if b.maxTries <= b.numTries {
+ return Stop
+ }
+ b.numTries++
+ }
+ return b.delegate.NextBackOff()
+}
+
+func (b *backOffTries) Reset() {
+ b.numTries = 0
+ b.delegate.Reset()
+}
diff --git a/vendor/github.com/containerd/errdefs/LICENSE b/vendor/github.com/containerd/errdefs/LICENSE
new file mode 100644
index 0000000000..584149b6ee
--- /dev/null
+++ b/vendor/github.com/containerd/errdefs/LICENSE
@@ -0,0 +1,191 @@
+
+ Apache License
+ Version 2.0, January 2004
+ https://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ Copyright The containerd Authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/vendor/github.com/containerd/errdefs/README.md b/vendor/github.com/containerd/errdefs/README.md
new file mode 100644
index 0000000000..bd418c63f9
--- /dev/null
+++ b/vendor/github.com/containerd/errdefs/README.md
@@ -0,0 +1,13 @@
+# errdefs
+
+A Go package for defining and checking common containerd errors.
+
+## Project details
+
+**errdefs** is a containerd sub-project, licensed under the [Apache 2.0 license](./LICENSE).
+As a containerd sub-project, you will find the:
+ * [Project governance](https://github.com/containerd/project/blob/main/GOVERNANCE.md),
+ * [Maintainers](https://github.com/containerd/project/blob/main/MAINTAINERS),
+ * and [Contributing guidelines](https://github.com/containerd/project/blob/main/CONTRIBUTING.md)
+
+information in our [`containerd/project`](https://github.com/containerd/project) repository.
diff --git a/vendor/github.com/containerd/errdefs/errors.go b/vendor/github.com/containerd/errdefs/errors.go
new file mode 100644
index 0000000000..f654d19649
--- /dev/null
+++ b/vendor/github.com/containerd/errdefs/errors.go
@@ -0,0 +1,443 @@
+/*
+ Copyright The containerd Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+// Package errdefs defines the common errors used throughout containerd
+// packages.
+//
+// Use with fmt.Errorf to add context to an error.
+//
+// To detect an error class, use the IsXXX functions to tell whether an error
+// is of a certain type.
+package errdefs
+
+import (
+ "context"
+ "errors"
+)
+
+// Definitions of common error types used throughout containerd. All containerd
+// errors returned by most packages will map into one of these errors classes.
+// Packages should return errors of these types when they want to instruct a
+// client to take a particular action.
+//
+// These errors map closely to grpc errors.
+var (
+ ErrUnknown = errUnknown{}
+ ErrInvalidArgument = errInvalidArgument{}
+ ErrNotFound = errNotFound{}
+ ErrAlreadyExists = errAlreadyExists{}
+ ErrPermissionDenied = errPermissionDenied{}
+ ErrResourceExhausted = errResourceExhausted{}
+ ErrFailedPrecondition = errFailedPrecondition{}
+ ErrConflict = errConflict{}
+ ErrNotModified = errNotModified{}
+ ErrAborted = errAborted{}
+ ErrOutOfRange = errOutOfRange{}
+ ErrNotImplemented = errNotImplemented{}
+ ErrInternal = errInternal{}
+ ErrUnavailable = errUnavailable{}
+ ErrDataLoss = errDataLoss{}
+ ErrUnauthenticated = errUnauthorized{}
+)
+
+// cancelled maps to Moby's "ErrCancelled"
+type cancelled interface {
+ Cancelled()
+}
+
+// IsCanceled returns true if the error is due to `context.Canceled`.
+func IsCanceled(err error) bool {
+ return errors.Is(err, context.Canceled) || isInterface[cancelled](err)
+}
+
+type errUnknown struct{}
+
+func (errUnknown) Error() string { return "unknown" }
+
+func (errUnknown) Unknown() {}
+
+func (e errUnknown) WithMessage(msg string) error {
+ return customMessage{e, msg}
+}
+
+// unknown maps to Moby's "ErrUnknown"
+type unknown interface {
+ Unknown()
+}
+
+// IsUnknown returns true if the error is due to an unknown error,
+// unhandled condition or unexpected response.
+func IsUnknown(err error) bool {
+ return errors.Is(err, errUnknown{}) || isInterface[unknown](err)
+}
+
+type errInvalidArgument struct{}
+
+func (errInvalidArgument) Error() string { return "invalid argument" }
+
+func (errInvalidArgument) InvalidParameter() {}
+
+func (e errInvalidArgument) WithMessage(msg string) error {
+ return customMessage{e, msg}
+}
+
+// invalidParameter maps to Moby's "ErrInvalidParameter"
+type invalidParameter interface {
+ InvalidParameter()
+}
+
+// IsInvalidArgument returns true if the error is due to an invalid argument
+func IsInvalidArgument(err error) bool {
+ return errors.Is(err, ErrInvalidArgument) || isInterface[invalidParameter](err)
+}
+
+// deadlineExceed maps to Moby's "ErrDeadline"
+type deadlineExceeded interface {
+ DeadlineExceeded()
+}
+
+// IsDeadlineExceeded returns true if the error is due to
+// `context.DeadlineExceeded`.
+func IsDeadlineExceeded(err error) bool {
+ return errors.Is(err, context.DeadlineExceeded) || isInterface[deadlineExceeded](err)
+}
+
+type errNotFound struct{}
+
+func (errNotFound) Error() string { return "not found" }
+
+func (errNotFound) NotFound() {}
+
+func (e errNotFound) WithMessage(msg string) error {
+ return customMessage{e, msg}
+}
+
+// notFound maps to Moby's "ErrNotFound"
+type notFound interface {
+ NotFound()
+}
+
+// IsNotFound returns true if the error is due to a missing object
+func IsNotFound(err error) bool {
+ return errors.Is(err, ErrNotFound) || isInterface[notFound](err)
+}
+
+type errAlreadyExists struct{}
+
+func (errAlreadyExists) Error() string { return "already exists" }
+
+func (errAlreadyExists) AlreadyExists() {}
+
+func (e errAlreadyExists) WithMessage(msg string) error {
+ return customMessage{e, msg}
+}
+
+type alreadyExists interface {
+ AlreadyExists()
+}
+
+// IsAlreadyExists returns true if the error is due to an already existing
+// metadata item
+func IsAlreadyExists(err error) bool {
+ return errors.Is(err, ErrAlreadyExists) || isInterface[alreadyExists](err)
+}
+
+type errPermissionDenied struct{}
+
+func (errPermissionDenied) Error() string { return "permission denied" }
+
+func (errPermissionDenied) Forbidden() {}
+
+func (e errPermissionDenied) WithMessage(msg string) error {
+ return customMessage{e, msg}
+}
+
+// forbidden maps to Moby's "ErrForbidden"
+type forbidden interface {
+ Forbidden()
+}
+
+// IsPermissionDenied returns true if the error is due to permission denied
+// or forbidden (403) response
+func IsPermissionDenied(err error) bool {
+ return errors.Is(err, ErrPermissionDenied) || isInterface[forbidden](err)
+}
+
+type errResourceExhausted struct{}
+
+func (errResourceExhausted) Error() string { return "resource exhausted" }
+
+func (errResourceExhausted) ResourceExhausted() {}
+
+func (e errResourceExhausted) WithMessage(msg string) error {
+ return customMessage{e, msg}
+}
+
+type resourceExhausted interface {
+ ResourceExhausted()
+}
+
+// IsResourceExhausted returns true if the error is due to
+// a lack of resources or too many attempts.
+func IsResourceExhausted(err error) bool {
+ return errors.Is(err, errResourceExhausted{}) || isInterface[resourceExhausted](err)
+}
+
+type errFailedPrecondition struct{}
+
+func (e errFailedPrecondition) Error() string { return "failed precondition" }
+
+func (errFailedPrecondition) FailedPrecondition() {}
+
+func (e errFailedPrecondition) WithMessage(msg string) error {
+ return customMessage{e, msg}
+}
+
+type failedPrecondition interface {
+ FailedPrecondition()
+}
+
+// IsFailedPrecondition returns true if an operation could not proceed due to
+// the lack of a particular condition
+func IsFailedPrecondition(err error) bool {
+ return errors.Is(err, errFailedPrecondition{}) || isInterface[failedPrecondition](err)
+}
+
+type errConflict struct{}
+
+func (errConflict) Error() string { return "conflict" }
+
+func (errConflict) Conflict() {}
+
+func (e errConflict) WithMessage(msg string) error {
+ return customMessage{e, msg}
+}
+
+// conflict maps to Moby's "ErrConflict"
+type conflict interface {
+ Conflict()
+}
+
+// IsConflict returns true if an operation could not proceed due to
+// a conflict.
+func IsConflict(err error) bool {
+ return errors.Is(err, errConflict{}) || isInterface[conflict](err)
+}
+
+type errNotModified struct{}
+
+func (errNotModified) Error() string { return "not modified" }
+
+func (errNotModified) NotModified() {}
+
+func (e errNotModified) WithMessage(msg string) error {
+ return customMessage{e, msg}
+}
+
+// notModified maps to Moby's "ErrNotModified"
+type notModified interface {
+ NotModified()
+}
+
+// IsNotModified returns true if an operation could not proceed due
+// to an object not modified from a previous state.
+func IsNotModified(err error) bool {
+ return errors.Is(err, errNotModified{}) || isInterface[notModified](err)
+}
+
+type errAborted struct{}
+
+func (errAborted) Error() string { return "aborted" }
+
+func (errAborted) Aborted() {}
+
+func (e errAborted) WithMessage(msg string) error {
+ return customMessage{e, msg}
+}
+
+type aborted interface {
+ Aborted()
+}
+
+// IsAborted returns true if an operation was aborted.
+func IsAborted(err error) bool {
+ return errors.Is(err, errAborted{}) || isInterface[aborted](err)
+}
+
+type errOutOfRange struct{}
+
+func (errOutOfRange) Error() string { return "out of range" }
+
+func (errOutOfRange) OutOfRange() {}
+
+func (e errOutOfRange) WithMessage(msg string) error {
+ return customMessage{e, msg}
+}
+
+type outOfRange interface {
+ OutOfRange()
+}
+
+// IsOutOfRange returns true if an operation could not proceed due
+// to data being out of the expected range.
+func IsOutOfRange(err error) bool {
+ return errors.Is(err, errOutOfRange{}) || isInterface[outOfRange](err)
+}
+
+type errNotImplemented struct{}
+
+func (errNotImplemented) Error() string { return "not implemented" }
+
+func (errNotImplemented) NotImplemented() {}
+
+func (e errNotImplemented) WithMessage(msg string) error {
+ return customMessage{e, msg}
+}
+
+// notImplemented maps to Moby's "ErrNotImplemented"
+type notImplemented interface {
+ NotImplemented()
+}
+
+// IsNotImplemented returns true if the error is due to not being implemented
+func IsNotImplemented(err error) bool {
+ return errors.Is(err, errNotImplemented{}) || isInterface[notImplemented](err)
+}
+
+type errInternal struct{}
+
+func (errInternal) Error() string { return "internal" }
+
+func (errInternal) System() {}
+
+func (e errInternal) WithMessage(msg string) error {
+ return customMessage{e, msg}
+}
+
+// system maps to Moby's "ErrSystem"
+type system interface {
+ System()
+}
+
+// IsInternal returns true if the error returns to an internal or system error
+func IsInternal(err error) bool {
+ return errors.Is(err, errInternal{}) || isInterface[system](err)
+}
+
+type errUnavailable struct{}
+
+func (errUnavailable) Error() string { return "unavailable" }
+
+func (errUnavailable) Unavailable() {}
+
+func (e errUnavailable) WithMessage(msg string) error {
+ return customMessage{e, msg}
+}
+
+// unavailable maps to Moby's "ErrUnavailable"
+type unavailable interface {
+ Unavailable()
+}
+
+// IsUnavailable returns true if the error is due to a resource being unavailable
+func IsUnavailable(err error) bool {
+ return errors.Is(err, errUnavailable{}) || isInterface[unavailable](err)
+}
+
+type errDataLoss struct{}
+
+func (errDataLoss) Error() string { return "data loss" }
+
+func (errDataLoss) DataLoss() {}
+
+func (e errDataLoss) WithMessage(msg string) error {
+ return customMessage{e, msg}
+}
+
+// dataLoss maps to Moby's "ErrDataLoss"
+type dataLoss interface {
+ DataLoss()
+}
+
+// IsDataLoss returns true if data during an operation was lost or corrupted
+func IsDataLoss(err error) bool {
+ return errors.Is(err, errDataLoss{}) || isInterface[dataLoss](err)
+}
+
+type errUnauthorized struct{}
+
+func (errUnauthorized) Error() string { return "unauthorized" }
+
+func (errUnauthorized) Unauthorized() {}
+
+func (e errUnauthorized) WithMessage(msg string) error {
+ return customMessage{e, msg}
+}
+
+// unauthorized maps to Moby's "ErrUnauthorized"
+type unauthorized interface {
+ Unauthorized()
+}
+
+// IsUnauthorized returns true if the error indicates that the user was
+// unauthenticated or unauthorized.
+func IsUnauthorized(err error) bool {
+ return errors.Is(err, errUnauthorized{}) || isInterface[unauthorized](err)
+}
+
+func isInterface[T any](err error) bool {
+ for {
+ switch x := err.(type) {
+ case T:
+ return true
+ case customMessage:
+ err = x.err
+ case interface{ Unwrap() error }:
+ err = x.Unwrap()
+ if err == nil {
+ return false
+ }
+ case interface{ Unwrap() []error }:
+ for _, err := range x.Unwrap() {
+ if isInterface[T](err) {
+ return true
+ }
+ }
+ return false
+ default:
+ return false
+ }
+ }
+}
+
+// customMessage is used to provide a defined error with a custom message.
+// The message is not wrapped but can be compared by the `Is(error) bool` interface.
+type customMessage struct {
+ err error
+ msg string
+}
+
+func (c customMessage) Is(err error) bool {
+ return c.err == err
+}
+
+func (c customMessage) As(target any) bool {
+ return errors.As(c.err, target)
+}
+
+func (c customMessage) Error() string {
+ return c.msg
+}
diff --git a/vendor/github.com/containerd/errdefs/pkg/LICENSE b/vendor/github.com/containerd/errdefs/pkg/LICENSE
new file mode 100644
index 0000000000..584149b6ee
--- /dev/null
+++ b/vendor/github.com/containerd/errdefs/pkg/LICENSE
@@ -0,0 +1,191 @@
+
+ Apache License
+ Version 2.0, January 2004
+ https://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ Copyright The containerd Authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/vendor/github.com/containerd/errdefs/pkg/errhttp/http.go b/vendor/github.com/containerd/errdefs/pkg/errhttp/http.go
new file mode 100644
index 0000000000..d7cd2b8c1c
--- /dev/null
+++ b/vendor/github.com/containerd/errdefs/pkg/errhttp/http.go
@@ -0,0 +1,96 @@
+/*
+ Copyright The containerd Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+// Package errhttp provides utility functions for translating errors to
+// and from a HTTP context.
+//
+// The functions ToHTTP and ToNative can be used to map server-side and
+// client-side errors to the correct types.
+package errhttp
+
+import (
+ "errors"
+ "net/http"
+
+ "github.com/containerd/errdefs"
+ "github.com/containerd/errdefs/pkg/internal/cause"
+)
+
+// ToHTTP returns the best status code for the given error
+func ToHTTP(err error) int {
+ switch {
+ case errdefs.IsNotFound(err):
+ return http.StatusNotFound
+ case errdefs.IsInvalidArgument(err):
+ return http.StatusBadRequest
+ case errdefs.IsConflict(err):
+ return http.StatusConflict
+ case errdefs.IsNotModified(err):
+ return http.StatusNotModified
+ case errdefs.IsFailedPrecondition(err):
+ return http.StatusPreconditionFailed
+ case errdefs.IsUnauthorized(err):
+ return http.StatusUnauthorized
+ case errdefs.IsPermissionDenied(err):
+ return http.StatusForbidden
+ case errdefs.IsResourceExhausted(err):
+ return http.StatusTooManyRequests
+ case errdefs.IsInternal(err):
+ return http.StatusInternalServerError
+ case errdefs.IsNotImplemented(err):
+ return http.StatusNotImplemented
+ case errdefs.IsUnavailable(err):
+ return http.StatusServiceUnavailable
+ case errdefs.IsUnknown(err):
+ var unexpected cause.ErrUnexpectedStatus
+ if errors.As(err, &unexpected) && unexpected.Status >= 200 && unexpected.Status < 600 {
+ return unexpected.Status
+ }
+ return http.StatusInternalServerError
+ default:
+ return http.StatusInternalServerError
+ }
+}
+
+// ToNative returns the error best matching the HTTP status code
+func ToNative(statusCode int) error {
+ switch statusCode {
+ case http.StatusNotFound:
+ return errdefs.ErrNotFound
+ case http.StatusBadRequest:
+ return errdefs.ErrInvalidArgument
+ case http.StatusConflict:
+ return errdefs.ErrConflict
+ case http.StatusPreconditionFailed:
+ return errdefs.ErrFailedPrecondition
+ case http.StatusUnauthorized:
+ return errdefs.ErrUnauthenticated
+ case http.StatusForbidden:
+ return errdefs.ErrPermissionDenied
+ case http.StatusNotModified:
+ return errdefs.ErrNotModified
+ case http.StatusTooManyRequests:
+ return errdefs.ErrResourceExhausted
+ case http.StatusInternalServerError:
+ return errdefs.ErrInternal
+ case http.StatusNotImplemented:
+ return errdefs.ErrNotImplemented
+ case http.StatusServiceUnavailable:
+ return errdefs.ErrUnavailable
+ default:
+ return cause.ErrUnexpectedStatus{Status: statusCode}
+ }
+}
diff --git a/vendor/github.com/containerd/errdefs/pkg/internal/cause/cause.go b/vendor/github.com/containerd/errdefs/pkg/internal/cause/cause.go
new file mode 100644
index 0000000000..d88756bb06
--- /dev/null
+++ b/vendor/github.com/containerd/errdefs/pkg/internal/cause/cause.go
@@ -0,0 +1,33 @@
+/*
+ Copyright The containerd Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+// Package cause is used to define root causes for errors
+// common to errors packages like grpc and http.
+package cause
+
+import "fmt"
+
+type ErrUnexpectedStatus struct {
+ Status int
+}
+
+const UnexpectedStatusPrefix = "unexpected status "
+
+func (e ErrUnexpectedStatus) Error() string {
+ return fmt.Sprintf("%s%d", UnexpectedStatusPrefix, e.Status)
+}
+
+func (ErrUnexpectedStatus) Unknown() {}
diff --git a/vendor/github.com/containerd/errdefs/resolve.go b/vendor/github.com/containerd/errdefs/resolve.go
new file mode 100644
index 0000000000..c02d4a73f4
--- /dev/null
+++ b/vendor/github.com/containerd/errdefs/resolve.go
@@ -0,0 +1,147 @@
+/*
+ Copyright The containerd Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package errdefs
+
+import "context"
+
+// Resolve returns the first error found in the error chain which matches an
+// error defined in this package or context error. A raw, unwrapped error is
+// returned or ErrUnknown if no matching error is found.
+//
+// This is useful for determining a response code based on the outermost wrapped
+// error rather than the original cause. For example, a not found error deep
+// in the code may be wrapped as an invalid argument. When determining status
+// code from Is* functions, the depth or ordering of the error is not
+// considered.
+//
+// The search order is depth first, a wrapped error returned from any part of
+// the chain from `Unwrap() error` will be returned before any joined errors
+// as returned by `Unwrap() []error`.
+func Resolve(err error) error {
+ if err == nil {
+ return nil
+ }
+ err = firstError(err)
+ if err == nil {
+ err = ErrUnknown
+ }
+ return err
+}
+
+func firstError(err error) error {
+ for {
+ switch err {
+ case ErrUnknown,
+ ErrInvalidArgument,
+ ErrNotFound,
+ ErrAlreadyExists,
+ ErrPermissionDenied,
+ ErrResourceExhausted,
+ ErrFailedPrecondition,
+ ErrConflict,
+ ErrNotModified,
+ ErrAborted,
+ ErrOutOfRange,
+ ErrNotImplemented,
+ ErrInternal,
+ ErrUnavailable,
+ ErrDataLoss,
+ ErrUnauthenticated,
+ context.DeadlineExceeded,
+ context.Canceled:
+ return err
+ }
+ switch e := err.(type) {
+ case customMessage:
+ err = e.err
+ case unknown:
+ return ErrUnknown
+ case invalidParameter:
+ return ErrInvalidArgument
+ case notFound:
+ return ErrNotFound
+ case alreadyExists:
+ return ErrAlreadyExists
+ case forbidden:
+ return ErrPermissionDenied
+ case resourceExhausted:
+ return ErrResourceExhausted
+ case failedPrecondition:
+ return ErrFailedPrecondition
+ case conflict:
+ return ErrConflict
+ case notModified:
+ return ErrNotModified
+ case aborted:
+ return ErrAborted
+ case errOutOfRange:
+ return ErrOutOfRange
+ case notImplemented:
+ return ErrNotImplemented
+ case system:
+ return ErrInternal
+ case unavailable:
+ return ErrUnavailable
+ case dataLoss:
+ return ErrDataLoss
+ case unauthorized:
+ return ErrUnauthenticated
+ case deadlineExceeded:
+ return context.DeadlineExceeded
+ case cancelled:
+ return context.Canceled
+ case interface{ Unwrap() error }:
+ err = e.Unwrap()
+ if err == nil {
+ return nil
+ }
+ case interface{ Unwrap() []error }:
+ for _, ue := range e.Unwrap() {
+ if fe := firstError(ue); fe != nil {
+ return fe
+ }
+ }
+ return nil
+ case interface{ Is(error) bool }:
+ for _, target := range []error{ErrUnknown,
+ ErrInvalidArgument,
+ ErrNotFound,
+ ErrAlreadyExists,
+ ErrPermissionDenied,
+ ErrResourceExhausted,
+ ErrFailedPrecondition,
+ ErrConflict,
+ ErrNotModified,
+ ErrAborted,
+ ErrOutOfRange,
+ ErrNotImplemented,
+ ErrInternal,
+ ErrUnavailable,
+ ErrDataLoss,
+ ErrUnauthenticated,
+ context.DeadlineExceeded,
+ context.Canceled} {
+ if e.Is(target) {
+ return target
+ }
+ }
+ return nil
+ default:
+ return nil
+ }
+ }
+}
diff --git a/vendor/github.com/containerd/log/.golangci.yml b/vendor/github.com/containerd/log/.golangci.yml
new file mode 100644
index 0000000000..a695775df4
--- /dev/null
+++ b/vendor/github.com/containerd/log/.golangci.yml
@@ -0,0 +1,30 @@
+linters:
+ enable:
+ - exportloopref # Checks for pointers to enclosing loop variables
+ - gofmt
+ - goimports
+ - gosec
+ - ineffassign
+ - misspell
+ - nolintlint
+ - revive
+ - staticcheck
+ - tenv # Detects using os.Setenv instead of t.Setenv since Go 1.17
+ - unconvert
+ - unused
+ - vet
+ - dupword # Checks for duplicate words in the source code
+ disable:
+ - errcheck
+
+run:
+ timeout: 5m
+ skip-dirs:
+ - api
+ - cluster
+ - design
+ - docs
+ - docs/man
+ - releases
+ - reports
+ - test # e2e scripts
diff --git a/vendor/github.com/containerd/log/LICENSE b/vendor/github.com/containerd/log/LICENSE
new file mode 100644
index 0000000000..584149b6ee
--- /dev/null
+++ b/vendor/github.com/containerd/log/LICENSE
@@ -0,0 +1,191 @@
+
+ Apache License
+ Version 2.0, January 2004
+ https://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ Copyright The containerd Authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/vendor/github.com/containerd/log/README.md b/vendor/github.com/containerd/log/README.md
new file mode 100644
index 0000000000..00e0849880
--- /dev/null
+++ b/vendor/github.com/containerd/log/README.md
@@ -0,0 +1,17 @@
+# log
+
+A Go package providing a common logging interface across containerd repositories and a way for clients to use and configure logging in containerd packages.
+
+This package is not intended to be used as a standalone logging package outside of the containerd ecosystem and is intended as an interface wrapper around a logging implementation.
+In the future this package may be replaced with a common go logging interface.
+
+## Project details
+
+**log** is a containerd sub-project, licensed under the [Apache 2.0 license](./LICENSE).
+As a containerd sub-project, you will find the:
+ * [Project governance](https://github.com/containerd/project/blob/main/GOVERNANCE.md),
+ * [Maintainers](https://github.com/containerd/project/blob/main/MAINTAINERS),
+ * and [Contributing guidelines](https://github.com/containerd/project/blob/main/CONTRIBUTING.md)
+
+information in our [`containerd/project`](https://github.com/containerd/project) repository.
+
diff --git a/vendor/github.com/containerd/log/context.go b/vendor/github.com/containerd/log/context.go
new file mode 100644
index 0000000000..20153066f3
--- /dev/null
+++ b/vendor/github.com/containerd/log/context.go
@@ -0,0 +1,182 @@
+/*
+ Copyright The containerd Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+// Package log provides types and functions related to logging, passing
+// loggers through a context, and attaching context to the logger.
+//
+// # Transitional types
+//
+// This package contains various types that are aliases for types in [logrus].
+// These aliases are intended for transitioning away from hard-coding logrus
+// as logging implementation. Consumers of this package are encouraged to use
+// the type-aliases from this package instead of directly using their logrus
+// equivalent.
+//
+// The intent is to replace these aliases with locally defined types and
+// interfaces once all consumers are no longer directly importing logrus
+// types.
+//
+// IMPORTANT: due to the transitional purpose of this package, it is not
+// guaranteed for the full logrus API to be provided in the future. As
+// outlined, these aliases are provided as a step to transition away from
+// a specific implementation which, as a result, exposes the full logrus API.
+// While no decisions have been made on the ultimate design and interface
+// provided by this package, we do not expect carrying "less common" features.
+package log
+
+import (
+ "context"
+ "fmt"
+
+ "github.com/sirupsen/logrus"
+)
+
+// G is a shorthand for [GetLogger].
+//
+// We may want to define this locally to a package to get package tagged log
+// messages.
+var G = GetLogger
+
+// L is an alias for the standard logger.
+var L = &Entry{
+ Logger: logrus.StandardLogger(),
+ // Default is three fields plus a little extra room.
+ Data: make(Fields, 6),
+}
+
+type loggerKey struct{}
+
+// Fields type to pass to "WithFields".
+type Fields = map[string]any
+
+// Entry is a logging entry. It contains all the fields passed with
+// [Entry.WithFields]. It's finally logged when Trace, Debug, Info, Warn,
+// Error, Fatal or Panic is called on it. These objects can be reused and
+// passed around as much as you wish to avoid field duplication.
+//
+// Entry is a transitional type, and currently an alias for [logrus.Entry].
+type Entry = logrus.Entry
+
+// RFC3339NanoFixed is [time.RFC3339Nano] with nanoseconds padded using
+// zeros to ensure the formatted time is always the same number of
+// characters.
+const RFC3339NanoFixed = "2006-01-02T15:04:05.000000000Z07:00"
+
+// Level is a logging level.
+type Level = logrus.Level
+
+// Supported log levels.
+const (
+ // TraceLevel level. Designates finer-grained informational events
+ // than [DebugLevel].
+ TraceLevel Level = logrus.TraceLevel
+
+ // DebugLevel level. Usually only enabled when debugging. Very verbose
+ // logging.
+ DebugLevel Level = logrus.DebugLevel
+
+ // InfoLevel level. General operational entries about what's going on
+ // inside the application.
+ InfoLevel Level = logrus.InfoLevel
+
+ // WarnLevel level. Non-critical entries that deserve eyes.
+ WarnLevel Level = logrus.WarnLevel
+
+ // ErrorLevel level. Logs errors that should definitely be noted.
+ // Commonly used for hooks to send errors to an error tracking service.
+ ErrorLevel Level = logrus.ErrorLevel
+
+ // FatalLevel level. Logs and then calls "logger.Exit(1)". It exits
+ // even if the logging level is set to Panic.
+ FatalLevel Level = logrus.FatalLevel
+
+ // PanicLevel level. This is the highest level of severity. Logs and
+ // then calls panic with the message passed to Debug, Info, ...
+ PanicLevel Level = logrus.PanicLevel
+)
+
+// SetLevel sets log level globally. It returns an error if the given
+// level is not supported.
+//
+// level can be one of:
+//
+// - "trace" ([TraceLevel])
+// - "debug" ([DebugLevel])
+// - "info" ([InfoLevel])
+// - "warn" ([WarnLevel])
+// - "error" ([ErrorLevel])
+// - "fatal" ([FatalLevel])
+// - "panic" ([PanicLevel])
+func SetLevel(level string) error {
+ lvl, err := logrus.ParseLevel(level)
+ if err != nil {
+ return err
+ }
+
+ L.Logger.SetLevel(lvl)
+ return nil
+}
+
+// GetLevel returns the current log level.
+func GetLevel() Level {
+ return L.Logger.GetLevel()
+}
+
+// OutputFormat specifies a log output format.
+type OutputFormat string
+
+// Supported log output formats.
+const (
+ // TextFormat represents the text logging format.
+ TextFormat OutputFormat = "text"
+
+ // JSONFormat represents the JSON logging format.
+ JSONFormat OutputFormat = "json"
+)
+
+// SetFormat sets the log output format ([TextFormat] or [JSONFormat]).
+func SetFormat(format OutputFormat) error {
+ switch format {
+ case TextFormat:
+ L.Logger.SetFormatter(&logrus.TextFormatter{
+ TimestampFormat: RFC3339NanoFixed,
+ FullTimestamp: true,
+ })
+ return nil
+ case JSONFormat:
+ L.Logger.SetFormatter(&logrus.JSONFormatter{
+ TimestampFormat: RFC3339NanoFixed,
+ })
+ return nil
+ default:
+ return fmt.Errorf("unknown log format: %s", format)
+ }
+}
+
+// WithLogger returns a new context with the provided logger. Use in
+// combination with logger.WithField(s) for great effect.
+func WithLogger(ctx context.Context, logger *Entry) context.Context {
+ return context.WithValue(ctx, loggerKey{}, logger.WithContext(ctx))
+}
+
+// GetLogger retrieves the current logger from the context. If no logger is
+// available, the default logger is returned.
+func GetLogger(ctx context.Context) *Entry {
+ if logger := ctx.Value(loggerKey{}); logger != nil {
+ return logger.(*Entry)
+ }
+ return L.WithContext(ctx)
+}
diff --git a/vendor/github.com/containerd/platforms/.gitattributes b/vendor/github.com/containerd/platforms/.gitattributes
new file mode 100644
index 0000000000..a0717e4b3b
--- /dev/null
+++ b/vendor/github.com/containerd/platforms/.gitattributes
@@ -0,0 +1 @@
+*.go text eol=lf
\ No newline at end of file
diff --git a/vendor/github.com/containerd/platforms/.golangci.yml b/vendor/github.com/containerd/platforms/.golangci.yml
new file mode 100644
index 0000000000..d574fe11d7
--- /dev/null
+++ b/vendor/github.com/containerd/platforms/.golangci.yml
@@ -0,0 +1,32 @@
+linters:
+ enable:
+ - copyloopvar
+ - gofmt
+ - goimports
+ - gosec
+ - ineffassign
+ - misspell
+ - nolintlint
+ - revive
+ - staticcheck
+ - tenv # Detects using os.Setenv instead of t.Setenv since Go 1.17
+ - unconvert
+ - unused
+ - govet
+ - dupword # Checks for duplicate words in the source code
+ disable:
+ - errcheck
+
+run:
+ timeout: 5m
+
+issues:
+ exclude-dirs:
+ - api
+ - cluster
+ - design
+ - docs
+ - docs/man
+ - releases
+ - reports
+ - test # e2e scripts
diff --git a/vendor/github.com/containerd/platforms/LICENSE b/vendor/github.com/containerd/platforms/LICENSE
new file mode 100644
index 0000000000..584149b6ee
--- /dev/null
+++ b/vendor/github.com/containerd/platforms/LICENSE
@@ -0,0 +1,191 @@
+
+ Apache License
+ Version 2.0, January 2004
+ https://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ Copyright The containerd Authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/vendor/github.com/containerd/platforms/README.md b/vendor/github.com/containerd/platforms/README.md
new file mode 100644
index 0000000000..2059de771c
--- /dev/null
+++ b/vendor/github.com/containerd/platforms/README.md
@@ -0,0 +1,32 @@
+# platforms
+
+A Go package for formatting, normalizing and matching container platforms.
+
+This package is based on the Open Containers Image Spec definition of a [platform](https://github.com/opencontainers/image-spec/blob/main/specs-go/v1/descriptor.go#L52).
+
+## Platform Specifier
+
+While the OCI platform specifications provide a tool for components to
+specify structured information, user input typically doesn't need the full
+context and much can be inferred. To solve this problem, this package introduces
+"specifiers". A specifier has the format
+`||/[/]`. The user can provide either the
+operating system or the architecture or both.
+
+An example of a common specifier is `linux/amd64`. If the host has a default
+runtime that matches this, the user can simply provide the component that
+matters. For example, if an image provides `amd64` and `arm64` support, the
+operating system, `linux` can be inferred, so they only have to provide
+`arm64` or `amd64`. Similar behavior is implemented for operating systems,
+where the architecture may be known but a runtime may support images from
+different operating systems.
+
+## Project details
+
+**platforms** is a containerd sub-project, licensed under the [Apache 2.0 license](./LICENSE).
+As a containerd sub-project, you will find the:
+ * [Project governance](https://github.com/containerd/project/blob/main/GOVERNANCE.md),
+ * [Maintainers](https://github.com/containerd/project/blob/main/MAINTAINERS),
+ * and [Contributing guidelines](https://github.com/containerd/project/blob/main/CONTRIBUTING.md)
+
+information in our [`containerd/project`](https://github.com/containerd/project) repository.
\ No newline at end of file
diff --git a/vendor/github.com/containerd/platforms/compare.go b/vendor/github.com/containerd/platforms/compare.go
new file mode 100644
index 0000000000..24403f3b3d
--- /dev/null
+++ b/vendor/github.com/containerd/platforms/compare.go
@@ -0,0 +1,260 @@
+/*
+ Copyright The containerd Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package platforms
+
+import (
+ "strconv"
+ "strings"
+
+ specs "github.com/opencontainers/image-spec/specs-go/v1"
+)
+
+// MatchComparer is able to match and compare platforms to
+// filter and sort platforms.
+type MatchComparer interface {
+ Matcher
+
+ Less(specs.Platform, specs.Platform) bool
+}
+
+type platformVersions struct {
+ major []int
+ minor []int
+}
+
+var arm64variantToVersion = map[string]platformVersions{
+ "v8": {[]int{8}, []int{0}},
+ "v8.0": {[]int{8}, []int{0}},
+ "v8.1": {[]int{8}, []int{1}},
+ "v8.2": {[]int{8}, []int{2}},
+ "v8.3": {[]int{8}, []int{3}},
+ "v8.4": {[]int{8}, []int{4}},
+ "v8.5": {[]int{8}, []int{5}},
+ "v8.6": {[]int{8}, []int{6}},
+ "v8.7": {[]int{8}, []int{7}},
+ "v8.8": {[]int{8}, []int{8}},
+ "v8.9": {[]int{8}, []int{9}},
+ "v9": {[]int{9, 8}, []int{0, 5}},
+ "v9.0": {[]int{9, 8}, []int{0, 5}},
+ "v9.1": {[]int{9, 8}, []int{1, 6}},
+ "v9.2": {[]int{9, 8}, []int{2, 7}},
+ "v9.3": {[]int{9, 8}, []int{3, 8}},
+ "v9.4": {[]int{9, 8}, []int{4, 9}},
+ "v9.5": {[]int{9, 8}, []int{5, 9}},
+ "v9.6": {[]int{9, 8}, []int{6, 9}},
+ "v9.7": {[]int{9, 8}, []int{7, 9}},
+}
+
+// platformVector returns an (ordered) vector of appropriate specs.Platform
+// objects to try matching for the given platform object (see platforms.Only).
+func platformVector(platform specs.Platform) []specs.Platform {
+ vector := []specs.Platform{platform}
+
+ switch platform.Architecture {
+ case "amd64":
+ if amd64Version, err := strconv.Atoi(strings.TrimPrefix(platform.Variant, "v")); err == nil && amd64Version > 1 {
+ for amd64Version--; amd64Version >= 1; amd64Version-- {
+ vector = append(vector, specs.Platform{
+ Architecture: platform.Architecture,
+ OS: platform.OS,
+ OSVersion: platform.OSVersion,
+ OSFeatures: platform.OSFeatures,
+ Variant: "v" + strconv.Itoa(amd64Version),
+ })
+ }
+ }
+ vector = append(vector, specs.Platform{
+ Architecture: "386",
+ OS: platform.OS,
+ OSVersion: platform.OSVersion,
+ OSFeatures: platform.OSFeatures,
+ })
+ case "arm":
+ if armVersion, err := strconv.Atoi(strings.TrimPrefix(platform.Variant, "v")); err == nil && armVersion > 5 {
+ for armVersion--; armVersion >= 5; armVersion-- {
+ vector = append(vector, specs.Platform{
+ Architecture: platform.Architecture,
+ OS: platform.OS,
+ OSVersion: platform.OSVersion,
+ OSFeatures: platform.OSFeatures,
+ Variant: "v" + strconv.Itoa(armVersion),
+ })
+ }
+ }
+ case "arm64":
+ variant := platform.Variant
+ if variant == "" {
+ variant = "v8"
+ }
+
+ vector = []specs.Platform{} // Reset vector, the first variant will be added in loop.
+ arm64Versions, ok := arm64variantToVersion[variant]
+ if !ok {
+ break
+ }
+ for i, major := range arm64Versions.major {
+ for minor := arm64Versions.minor[i]; minor >= 0; minor-- {
+ arm64Variant := "v" + strconv.Itoa(major) + "." + strconv.Itoa(minor)
+ if minor == 0 {
+ arm64Variant = "v" + strconv.Itoa(major)
+ }
+ vector = append(vector, specs.Platform{
+ Architecture: "arm64",
+ OS: platform.OS,
+ OSVersion: platform.OSVersion,
+ OSFeatures: platform.OSFeatures,
+ Variant: arm64Variant,
+ })
+ }
+ }
+
+ // All arm64/v8.x and arm64/v9.x are compatible with arm/v8 (32-bits) and below.
+ // There's no arm64 v9 variant, so it's normalized to v8.
+ if strings.HasPrefix(variant, "v8") || strings.HasPrefix(variant, "v9") {
+ variant = "v8"
+ }
+ vector = append(vector, platformVector(specs.Platform{
+ Architecture: "arm",
+ OS: platform.OS,
+ OSVersion: platform.OSVersion,
+ OSFeatures: platform.OSFeatures,
+ Variant: variant,
+ })...)
+ }
+
+ return vector
+}
+
+// Only returns a match comparer for a single platform
+// using default resolution logic for the platform.
+//
+// For arm64/v9.x, will also match arm64/v9.{0..x-1} and arm64/v8.{0..x+5}
+// For arm64/v8.x, will also match arm64/v8.{0..x-1}
+// For arm/v8, will also match arm/v7, arm/v6 and arm/v5
+// For arm/v7, will also match arm/v6 and arm/v5
+// For arm/v6, will also match arm/v5
+// For amd64, will also match 386
+func Only(platform specs.Platform) MatchComparer {
+ return Ordered(platformVector(Normalize(platform))...)
+}
+
+// OnlyStrict returns a match comparer for a single platform.
+//
+// Unlike Only, OnlyStrict does not match sub platforms.
+// So, "arm/vN" will not match "arm/vM" where M < N,
+// and "amd64" will not also match "386".
+//
+// OnlyStrict matches non-canonical forms.
+// So, "arm64" matches "arm/64/v8".
+func OnlyStrict(platform specs.Platform) MatchComparer {
+ return Ordered(Normalize(platform))
+}
+
+// Ordered returns a platform MatchComparer which matches any of the platforms
+// but orders them in order they are provided.
+func Ordered(platforms ...specs.Platform) MatchComparer {
+ matchers := make([]Matcher, len(platforms))
+ for i := range platforms {
+ matchers[i] = NewMatcher(platforms[i])
+ }
+ return orderedPlatformComparer{
+ matchers: matchers,
+ }
+}
+
+// Any returns a platform MatchComparer which matches any of the platforms
+// with no preference for ordering.
+func Any(platforms ...specs.Platform) MatchComparer {
+ matchers := make([]Matcher, len(platforms))
+ for i := range platforms {
+ matchers[i] = NewMatcher(platforms[i])
+ }
+ return anyPlatformComparer{
+ matchers: matchers,
+ }
+}
+
+// All is a platform MatchComparer which matches all platforms
+// with preference for ordering.
+var All MatchComparer = allPlatformComparer{}
+
+type orderedPlatformComparer struct {
+ matchers []Matcher
+}
+
+func (c orderedPlatformComparer) Match(platform specs.Platform) bool {
+ for _, m := range c.matchers {
+ if m.Match(platform) {
+ return true
+ }
+ }
+ return false
+}
+
+func (c orderedPlatformComparer) Less(p1 specs.Platform, p2 specs.Platform) bool {
+ for _, m := range c.matchers {
+ p1m := m.Match(p1)
+ p2m := m.Match(p2)
+ if p1m && !p2m {
+ return true
+ }
+ if p1m || p2m {
+ return false
+ }
+ }
+ return false
+}
+
+type anyPlatformComparer struct {
+ matchers []Matcher
+}
+
+func (c anyPlatformComparer) Match(platform specs.Platform) bool {
+ for _, m := range c.matchers {
+ if m.Match(platform) {
+ return true
+ }
+ }
+ return false
+}
+
+func (c anyPlatformComparer) Less(p1, p2 specs.Platform) bool {
+ var p1m, p2m bool
+ for _, m := range c.matchers {
+ if !p1m && m.Match(p1) {
+ p1m = true
+ }
+ if !p2m && m.Match(p2) {
+ p2m = true
+ }
+ if p1m && p2m {
+ return false
+ }
+ }
+ // If one matches, and the other does, sort match first
+ return p1m && !p2m
+}
+
+type allPlatformComparer struct{}
+
+func (allPlatformComparer) Match(specs.Platform) bool {
+ return true
+}
+
+func (allPlatformComparer) Less(specs.Platform, specs.Platform) bool {
+ return false
+}
diff --git a/vendor/github.com/containerd/platforms/cpuinfo.go b/vendor/github.com/containerd/platforms/cpuinfo.go
new file mode 100644
index 0000000000..91f50e8c88
--- /dev/null
+++ b/vendor/github.com/containerd/platforms/cpuinfo.go
@@ -0,0 +1,43 @@
+/*
+ Copyright The containerd Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package platforms
+
+import (
+ "runtime"
+ "sync"
+
+ "github.com/containerd/log"
+)
+
+// Present the ARM instruction set architecture, eg: v7, v8
+// Don't use this value directly; call cpuVariant() instead.
+var cpuVariantValue string
+
+var cpuVariantOnce sync.Once
+
+func cpuVariant() string {
+ cpuVariantOnce.Do(func() {
+ if isArmArch(runtime.GOARCH) {
+ var err error
+ cpuVariantValue, err = getCPUVariant()
+ if err != nil {
+ log.L.Errorf("Error getCPUVariant for OS %s: %v", runtime.GOOS, err)
+ }
+ }
+ })
+ return cpuVariantValue
+}
diff --git a/vendor/github.com/containerd/platforms/cpuinfo_linux.go b/vendor/github.com/containerd/platforms/cpuinfo_linux.go
new file mode 100644
index 0000000000..98c7001f93
--- /dev/null
+++ b/vendor/github.com/containerd/platforms/cpuinfo_linux.go
@@ -0,0 +1,160 @@
+/*
+ Copyright The containerd Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package platforms
+
+import (
+ "bufio"
+ "bytes"
+ "errors"
+ "fmt"
+ "os"
+ "runtime"
+ "strings"
+
+ "golang.org/x/sys/unix"
+)
+
+// getMachineArch retrieves the machine architecture through system call
+func getMachineArch() (string, error) {
+ var uname unix.Utsname
+ err := unix.Uname(&uname)
+ if err != nil {
+ return "", err
+ }
+
+ arch := string(uname.Machine[:bytes.IndexByte(uname.Machine[:], 0)])
+
+ return arch, nil
+}
+
+// For Linux, the kernel has already detected the ABI, ISA and Features.
+// So we don't need to access the ARM registers to detect platform information
+// by ourselves. We can just parse these information from /proc/cpuinfo
+func getCPUInfo(pattern string) (info string, err error) {
+
+ cpuinfo, err := os.Open("/proc/cpuinfo")
+ if err != nil {
+ return "", err
+ }
+ defer cpuinfo.Close()
+
+ // Start to Parse the Cpuinfo line by line. For SMP SoC, we parse
+ // the first core is enough.
+ scanner := bufio.NewScanner(cpuinfo)
+ for scanner.Scan() {
+ newline := scanner.Text()
+ list := strings.Split(newline, ":")
+
+ if len(list) > 1 && strings.EqualFold(strings.TrimSpace(list[0]), pattern) {
+ return strings.TrimSpace(list[1]), nil
+ }
+ }
+
+ // Check whether the scanner encountered errors
+ err = scanner.Err()
+ if err != nil {
+ return "", err
+ }
+
+ return "", fmt.Errorf("getCPUInfo for pattern %s: %w", pattern, errNotFound)
+}
+
+// getCPUVariantFromArch get CPU variant from arch through a system call
+func getCPUVariantFromArch(arch string) (string, error) {
+
+ var variant string
+
+ arch = strings.ToLower(arch)
+
+ if arch == "aarch64" {
+ variant = "8"
+ } else if arch[0:4] == "armv" && len(arch) >= 5 {
+ // Valid arch format is in form of armvXx
+ switch arch[3:5] {
+ case "v8":
+ variant = "8"
+ case "v7":
+ variant = "7"
+ case "v6":
+ variant = "6"
+ case "v5":
+ variant = "5"
+ case "v4":
+ variant = "4"
+ case "v3":
+ variant = "3"
+ default:
+ variant = "unknown"
+ }
+ } else {
+ return "", fmt.Errorf("getCPUVariantFromArch invalid arch: %s, %w", arch, errInvalidArgument)
+ }
+ return variant, nil
+}
+
+// getCPUVariant returns cpu variant for ARM
+// We first try reading "Cpu architecture" field from /proc/cpuinfo
+// If we can't find it, then fall back using a system call
+// This is to cover running ARM in emulated environment on x86 host as this field in /proc/cpuinfo
+// was not present.
+func getCPUVariant() (string, error) {
+ variant, err := getCPUInfo("Cpu architecture")
+ if err != nil {
+ if errors.Is(err, errNotFound) {
+ // Let's try getting CPU variant from machine architecture
+ arch, err := getMachineArch()
+ if err != nil {
+ return "", fmt.Errorf("failure getting machine architecture: %v", err)
+ }
+
+ variant, err = getCPUVariantFromArch(arch)
+ if err != nil {
+ return "", fmt.Errorf("failure getting CPU variant from machine architecture: %v", err)
+ }
+ } else {
+ return "", fmt.Errorf("failure getting CPU variant: %v", err)
+ }
+ }
+
+ // handle edge case for Raspberry Pi ARMv6 devices (which due to a kernel quirk, report "CPU architecture: 7")
+ // https://www.raspberrypi.org/forums/viewtopic.php?t=12614
+ if runtime.GOARCH == "arm" && variant == "7" {
+ model, err := getCPUInfo("model name")
+ if err == nil && strings.HasPrefix(strings.ToLower(model), "armv6-compatible") {
+ variant = "6"
+ }
+ }
+
+ switch strings.ToLower(variant) {
+ case "8", "aarch64":
+ variant = "v8"
+ case "7", "7m", "?(12)", "?(13)", "?(14)", "?(15)", "?(16)", "?(17)":
+ variant = "v7"
+ case "6", "6tej":
+ variant = "v6"
+ case "5", "5t", "5te", "5tej":
+ variant = "v5"
+ case "4", "4t":
+ variant = "v4"
+ case "3":
+ variant = "v3"
+ default:
+ variant = "unknown"
+ }
+
+ return variant, nil
+}
diff --git a/vendor/github.com/containerd/platforms/cpuinfo_other.go b/vendor/github.com/containerd/platforms/cpuinfo_other.go
new file mode 100644
index 0000000000..97a1fe8a3e
--- /dev/null
+++ b/vendor/github.com/containerd/platforms/cpuinfo_other.go
@@ -0,0 +1,55 @@
+//go:build !linux
+
+/*
+ Copyright The containerd Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package platforms
+
+import (
+ "fmt"
+ "runtime"
+)
+
+func getCPUVariant() (string, error) {
+
+ var variant string
+
+ if runtime.GOOS == "windows" || runtime.GOOS == "darwin" {
+ // Windows/Darwin only supports v7 for ARM32 and v8 for ARM64 and so we can use
+ // runtime.GOARCH to determine the variants
+ switch runtime.GOARCH {
+ case "arm64":
+ variant = "v8"
+ case "arm":
+ variant = "v7"
+ default:
+ variant = "unknown"
+ }
+ } else if runtime.GOOS == "freebsd" {
+ // FreeBSD supports ARMv6 and ARMv7 as well as ARMv4 and ARMv5 (though deprecated)
+ // detecting those variants is currently unimplemented
+ switch runtime.GOARCH {
+ case "arm64":
+ variant = "v8"
+ default:
+ variant = "unknown"
+ }
+ } else {
+ return "", fmt.Errorf("getCPUVariant for OS %s: %v", runtime.GOOS, errNotImplemented)
+ }
+
+ return variant, nil
+}
diff --git a/vendor/github.com/containerd/platforms/database.go b/vendor/github.com/containerd/platforms/database.go
new file mode 100644
index 0000000000..7a6f0d98cd
--- /dev/null
+++ b/vendor/github.com/containerd/platforms/database.go
@@ -0,0 +1,111 @@
+/*
+ Copyright The containerd Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package platforms
+
+import (
+ "runtime"
+ "strings"
+)
+
+// These function are generated from https://golang.org/src/go/build/syslist.go.
+//
+// We use switch statements because they are slightly faster than map lookups
+// and use a little less memory.
+
+// isKnownOS returns true if we know about the operating system.
+//
+// The OS value should be normalized before calling this function.
+func isKnownOS(os string) bool {
+ switch os {
+ case "aix", "android", "darwin", "dragonfly", "freebsd", "hurd", "illumos", "ios", "js", "linux", "nacl", "netbsd", "openbsd", "plan9", "solaris", "windows", "zos":
+ return true
+ }
+ return false
+}
+
+// isArmArch returns true if the architecture is ARM.
+//
+// The arch value should be normalized before being passed to this function.
+func isArmArch(arch string) bool {
+ switch arch {
+ case "arm", "arm64":
+ return true
+ }
+ return false
+}
+
+// isKnownArch returns true if we know about the architecture.
+//
+// The arch value should be normalized before being passed to this function.
+func isKnownArch(arch string) bool {
+ switch arch {
+ case "386", "amd64", "amd64p32", "arm", "armbe", "arm64", "arm64be", "ppc64", "ppc64le", "loong64", "mips", "mipsle", "mips64", "mips64le", "mips64p32", "mips64p32le", "ppc", "riscv", "riscv64", "s390", "s390x", "sparc", "sparc64", "wasm":
+ return true
+ }
+ return false
+}
+
+func normalizeOS(os string) string {
+ if os == "" {
+ return runtime.GOOS
+ }
+ os = strings.ToLower(os)
+
+ switch os {
+ case "macos":
+ os = "darwin"
+ }
+ return os
+}
+
+// normalizeArch normalizes the architecture.
+func normalizeArch(arch, variant string) (string, string) {
+ arch, variant = strings.ToLower(arch), strings.ToLower(variant)
+ switch arch {
+ case "i386":
+ arch = "386"
+ variant = ""
+ case "x86_64", "x86-64", "amd64":
+ arch = "amd64"
+ if variant == "v1" {
+ variant = ""
+ }
+ case "aarch64", "arm64":
+ arch = "arm64"
+ switch variant {
+ case "8", "v8", "v8.0":
+ variant = ""
+ case "9", "9.0", "v9.0":
+ variant = "v9"
+ }
+ case "armhf":
+ arch = "arm"
+ variant = "v7"
+ case "armel":
+ arch = "arm"
+ variant = "v6"
+ case "arm":
+ switch variant {
+ case "", "7":
+ variant = "v7"
+ case "5", "6", "8":
+ variant = "v" + variant
+ }
+ }
+
+ return arch, variant
+}
diff --git a/vendor/github.com/containerd/platforms/defaults.go b/vendor/github.com/containerd/platforms/defaults.go
new file mode 100644
index 0000000000..9d898d60e6
--- /dev/null
+++ b/vendor/github.com/containerd/platforms/defaults.go
@@ -0,0 +1,29 @@
+/*
+ Copyright The containerd Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package platforms
+
+// DefaultString returns the default string specifier for the platform,
+// with [PR#6](https://github.com/containerd/platforms/pull/6) the result
+// may now also include the OSVersion from the provided platform specification.
+func DefaultString() string {
+ return FormatAll(DefaultSpec())
+}
+
+// DefaultStrict returns strict form of Default.
+func DefaultStrict() MatchComparer {
+ return OnlyStrict(DefaultSpec())
+}
diff --git a/vendor/github.com/containerd/platforms/defaults_darwin.go b/vendor/github.com/containerd/platforms/defaults_darwin.go
new file mode 100644
index 0000000000..72355ca85f
--- /dev/null
+++ b/vendor/github.com/containerd/platforms/defaults_darwin.go
@@ -0,0 +1,44 @@
+//go:build darwin
+
+/*
+ Copyright The containerd Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package platforms
+
+import (
+ "runtime"
+
+ specs "github.com/opencontainers/image-spec/specs-go/v1"
+)
+
+// DefaultSpec returns the current platform's default platform specification.
+func DefaultSpec() specs.Platform {
+ return specs.Platform{
+ OS: runtime.GOOS,
+ Architecture: runtime.GOARCH,
+ // The Variant field will be empty if arch != ARM.
+ Variant: cpuVariant(),
+ }
+}
+
+// Default returns the default matcher for the platform.
+func Default() MatchComparer {
+ return Ordered(DefaultSpec(), specs.Platform{
+ // darwin runtime also supports Linux binary via runu/LKL
+ OS: "linux",
+ Architecture: runtime.GOARCH,
+ })
+}
diff --git a/vendor/github.com/containerd/platforms/defaults_freebsd.go b/vendor/github.com/containerd/platforms/defaults_freebsd.go
new file mode 100644
index 0000000000..d3fe89e076
--- /dev/null
+++ b/vendor/github.com/containerd/platforms/defaults_freebsd.go
@@ -0,0 +1,43 @@
+/*
+ Copyright The containerd Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package platforms
+
+import (
+ "runtime"
+
+ specs "github.com/opencontainers/image-spec/specs-go/v1"
+)
+
+// DefaultSpec returns the current platform's default platform specification.
+func DefaultSpec() specs.Platform {
+ return specs.Platform{
+ OS: runtime.GOOS,
+ Architecture: runtime.GOARCH,
+ // The Variant field will be empty if arch != ARM.
+ Variant: cpuVariant(),
+ }
+}
+
+// Default returns the default matcher for the platform.
+func Default() MatchComparer {
+ return Ordered(DefaultSpec(), specs.Platform{
+ OS: "linux",
+ Architecture: runtime.GOARCH,
+ // The Variant field will be empty if arch != ARM.
+ Variant: cpuVariant(),
+ })
+}
diff --git a/vendor/github.com/containerd/platforms/defaults_unix.go b/vendor/github.com/containerd/platforms/defaults_unix.go
new file mode 100644
index 0000000000..44acc47eb3
--- /dev/null
+++ b/vendor/github.com/containerd/platforms/defaults_unix.go
@@ -0,0 +1,40 @@
+//go:build !windows && !darwin && !freebsd
+
+/*
+ Copyright The containerd Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package platforms
+
+import (
+ "runtime"
+
+ specs "github.com/opencontainers/image-spec/specs-go/v1"
+)
+
+// DefaultSpec returns the current platform's default platform specification.
+func DefaultSpec() specs.Platform {
+ return specs.Platform{
+ OS: runtime.GOOS,
+ Architecture: runtime.GOARCH,
+ // The Variant field will be empty if arch != ARM.
+ Variant: cpuVariant(),
+ }
+}
+
+// Default returns the default matcher for the platform.
+func Default() MatchComparer {
+ return Only(DefaultSpec())
+}
diff --git a/vendor/github.com/containerd/platforms/defaults_windows.go b/vendor/github.com/containerd/platforms/defaults_windows.go
new file mode 100644
index 0000000000..0165adea7e
--- /dev/null
+++ b/vendor/github.com/containerd/platforms/defaults_windows.go
@@ -0,0 +1,42 @@
+/*
+ Copyright The containerd Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package platforms
+
+import (
+ "fmt"
+ "runtime"
+
+ specs "github.com/opencontainers/image-spec/specs-go/v1"
+ "golang.org/x/sys/windows"
+)
+
+// DefaultSpec returns the current platform's default platform specification.
+func DefaultSpec() specs.Platform {
+ major, minor, build := windows.RtlGetNtVersionNumbers()
+ return specs.Platform{
+ OS: runtime.GOOS,
+ Architecture: runtime.GOARCH,
+ OSVersion: fmt.Sprintf("%d.%d.%d", major, minor, build),
+ // The Variant field will be empty if arch != ARM.
+ Variant: cpuVariant(),
+ }
+}
+
+// Default returns the current platform's default platform specification.
+func Default() MatchComparer {
+ return Only(DefaultSpec())
+}
diff --git a/vendor/github.com/containerd/platforms/errors.go b/vendor/github.com/containerd/platforms/errors.go
new file mode 100644
index 0000000000..5ad721e779
--- /dev/null
+++ b/vendor/github.com/containerd/platforms/errors.go
@@ -0,0 +1,30 @@
+/*
+ Copyright The containerd Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package platforms
+
+import "errors"
+
+// These errors mirror the errors defined in [github.com/containerd/containerd/errdefs],
+// however, they are not exported as they are not expected to be used as sentinel
+// errors by consumers of this package.
+//
+//nolint:unused // not all errors are used on all platforms.
+var (
+ errNotFound = errors.New("not found")
+ errInvalidArgument = errors.New("invalid argument")
+ errNotImplemented = errors.New("not implemented")
+)
diff --git a/vendor/github.com/containerd/platforms/platform_windows_compat.go b/vendor/github.com/containerd/platforms/platform_windows_compat.go
new file mode 100644
index 0000000000..7f3d9966bc
--- /dev/null
+++ b/vendor/github.com/containerd/platforms/platform_windows_compat.go
@@ -0,0 +1,156 @@
+/*
+ Copyright The containerd Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package platforms
+
+import (
+ "strconv"
+ "strings"
+
+ specs "github.com/opencontainers/image-spec/specs-go/v1"
+)
+
+// windowsOSVersion is a wrapper for Windows version information
+// https://msdn.microsoft.com/en-us/library/windows/desktop/ms724439(v=vs.85).aspx
+type windowsOSVersion struct {
+ Version uint32
+ MajorVersion uint8
+ MinorVersion uint8
+ Build uint16
+}
+
+// Windows Client and Server build numbers.
+//
+// See:
+// https://learn.microsoft.com/en-us/windows/release-health/release-information
+// https://learn.microsoft.com/en-us/windows/release-health/windows-server-release-info
+// https://learn.microsoft.com/en-us/windows/release-health/windows11-release-information
+const (
+ // rs5 (version 1809, codename "Redstone 5") corresponds to Windows Server
+ // 2019 (ltsc2019), and Windows 10 (October 2018 Update).
+ rs5 = 17763
+
+ // v21H2Server corresponds to Windows Server 2022 (ltsc2022).
+ v21H2Server = 20348
+
+ // v22H2Win11 corresponds to Windows 11 (2022 Update).
+ v22H2Win11 = 22621
+)
+
+// List of stable ABI compliant ltsc releases
+// Note: List must be sorted in ascending order
+var compatLTSCReleases = []uint16{
+ v21H2Server,
+}
+
+// CheckHostAndContainerCompat checks if given host and container
+// OS versions are compatible.
+// It includes support for stable ABI compliant versions as well.
+// Every release after WS 2022 will support the previous ltsc
+// container image. Stable ABI is in preview mode for windows 11 client.
+// Refer: https://learn.microsoft.com/en-us/virtualization/windowscontainers/deploy-containers/version-compatibility?tabs=windows-server-2022%2Cwindows-10#windows-server-host-os-compatibility
+func checkWindowsHostAndContainerCompat(host, ctr windowsOSVersion) bool {
+ // check major minor versions of host and guest
+ if host.MajorVersion != ctr.MajorVersion ||
+ host.MinorVersion != ctr.MinorVersion {
+ return false
+ }
+
+ // If host is < WS 2022, exact version match is required
+ if host.Build < v21H2Server {
+ return host.Build == ctr.Build
+ }
+
+ var supportedLtscRelease uint16
+ for i := len(compatLTSCReleases) - 1; i >= 0; i-- {
+ if host.Build >= compatLTSCReleases[i] {
+ supportedLtscRelease = compatLTSCReleases[i]
+ break
+ }
+ }
+ return ctr.Build >= supportedLtscRelease && ctr.Build <= host.Build
+}
+
+func getWindowsOSVersion(osVersionPrefix string) windowsOSVersion {
+ if strings.Count(osVersionPrefix, ".") < 2 {
+ return windowsOSVersion{}
+ }
+
+ major, extra, _ := strings.Cut(osVersionPrefix, ".")
+ minor, extra, _ := strings.Cut(extra, ".")
+ build, _, _ := strings.Cut(extra, ".")
+
+ majorVersion, err := strconv.ParseUint(major, 10, 8)
+ if err != nil {
+ return windowsOSVersion{}
+ }
+
+ minorVersion, err := strconv.ParseUint(minor, 10, 8)
+ if err != nil {
+ return windowsOSVersion{}
+ }
+ buildNumber, err := strconv.ParseUint(build, 10, 16)
+ if err != nil {
+ return windowsOSVersion{}
+ }
+
+ return windowsOSVersion{
+ MajorVersion: uint8(majorVersion),
+ MinorVersion: uint8(minorVersion),
+ Build: uint16(buildNumber),
+ }
+}
+
+func winRevision(v string) int {
+ parts := strings.Split(v, ".")
+ if len(parts) < 4 {
+ return 0
+ }
+ r, err := strconv.Atoi(parts[3])
+ if err != nil {
+ return 0
+ }
+ return r
+}
+
+type windowsVersionMatcher struct {
+ windowsOSVersion
+}
+
+func (m windowsVersionMatcher) Match(v string) bool {
+ if m.isEmpty() || v == "" {
+ return true
+ }
+ osv := getWindowsOSVersion(v)
+ return checkWindowsHostAndContainerCompat(m.windowsOSVersion, osv)
+}
+
+func (m windowsVersionMatcher) isEmpty() bool {
+ return m.MajorVersion == 0 && m.MinorVersion == 0 && m.Build == 0
+}
+
+type windowsMatchComparer struct {
+ Matcher
+}
+
+func (c *windowsMatchComparer) Less(p1, p2 specs.Platform) bool {
+ m1, m2 := c.Match(p1), c.Match(p2)
+ if m1 && m2 {
+ r1, r2 := winRevision(p1.OSVersion), winRevision(p2.OSVersion)
+ return r1 > r2
+ }
+ return m1 && !m2
+}
diff --git a/vendor/github.com/containerd/platforms/platforms.go b/vendor/github.com/containerd/platforms/platforms.go
new file mode 100644
index 0000000000..14d65abd4f
--- /dev/null
+++ b/vendor/github.com/containerd/platforms/platforms.go
@@ -0,0 +1,341 @@
+/*
+ Copyright The containerd Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+// Package platforms provides a toolkit for normalizing, matching and
+// specifying container platforms.
+//
+// Centered around OCI platform specifications, we define a string-based
+// specifier syntax that can be used for user input. With a specifier, users
+// only need to specify the parts of the platform that are relevant to their
+// context, providing an operating system or architecture or both.
+//
+// How do I use this package?
+//
+// The vast majority of use cases should simply use the match function with
+// user input. The first step is to parse a specifier into a matcher:
+//
+// m, err := Parse("linux")
+// if err != nil { ... }
+//
+// Once you have a matcher, use it to match against the platform declared by a
+// component, typically from an image or runtime. Since extracting an images
+// platform is a little more involved, we'll use an example against the
+// platform default:
+//
+// if ok := m.Match(Default()); !ok { /* doesn't match */ }
+//
+// This can be composed in loops for resolving runtimes or used as a filter for
+// fetch and select images.
+//
+// More details of the specifier syntax and platform spec follow.
+//
+// # Declaring Platform Support
+//
+// Components that have strict platform requirements should use the OCI
+// platform specification to declare their support. Typically, this will be
+// images and runtimes that should make these declaring which platform they
+// support specifically. This looks roughly as follows:
+//
+// type Platform struct {
+// Architecture string
+// OS string
+// Variant string
+// }
+//
+// Most images and runtimes should at least set Architecture and OS, according
+// to their GOARCH and GOOS values, respectively (follow the OCI image
+// specification when in doubt). ARM should set variant under certain
+// discussions, which are outlined below.
+//
+// # Platform Specifiers
+//
+// While the OCI platform specifications provide a tool for components to
+// specify structured information, user input typically doesn't need the full
+// context and much can be inferred. To solve this problem, we introduced
+// "specifiers". A specifier has the format
+// `||/[/]`. The user can provide either the
+// operating system or the architecture or both.
+//
+// An example of a common specifier is `linux/amd64`. If the host has a default
+// of runtime that matches this, the user can simply provide the component that
+// matters. For example, if a image provides amd64 and arm64 support, the
+// operating system, `linux` can be inferred, so they only have to provide
+// `arm64` or `amd64`. Similar behavior is implemented for operating systems,
+// where the architecture may be known but a runtime may support images from
+// different operating systems.
+//
+// # Normalization
+//
+// Because not all users are familiar with the way the Go runtime represents
+// platforms, several normalizations have been provided to make this package
+// easier to user.
+//
+// The following are performed for architectures:
+//
+// Value Normalized
+// aarch64 arm64
+// armhf arm
+// armel arm/v6
+// i386 386
+// x86_64 amd64
+// x86-64 amd64
+//
+// We also normalize the operating system `macos` to `darwin`.
+//
+// # ARM Support
+//
+// To qualify ARM architecture, the Variant field is used to qualify the arm
+// version. The most common arm version, v7, is represented without the variant
+// unless it is explicitly provided. This is treated as equivalent to armhf. A
+// previous architecture, armel, will be normalized to arm/v6.
+//
+// Similarly, the most common arm64 version v8, and most common amd64 version v1
+// are represented without the variant.
+//
+// While these normalizations are provided, their support on arm platforms has
+// not yet been fully implemented and tested.
+package platforms
+
+import (
+ "fmt"
+ "path"
+ "regexp"
+ "runtime"
+ "strconv"
+ "strings"
+
+ specs "github.com/opencontainers/image-spec/specs-go/v1"
+)
+
+var (
+ specifierRe = regexp.MustCompile(`^[A-Za-z0-9_.-]+$`)
+ osAndVersionRe = regexp.MustCompile(`^([A-Za-z0-9_-]+)(?:\(([A-Za-z0-9_.-]*)\))?$`)
+)
+
+const osAndVersionFormat = "%s(%s)"
+
+// Platform is a type alias for convenience, so there is no need to import image-spec package everywhere.
+type Platform = specs.Platform
+
+// Matcher matches platforms specifications, provided by an image or runtime.
+type Matcher interface {
+ Match(platform specs.Platform) bool
+}
+
+// NewMatcher returns a simple matcher based on the provided platform
+// specification. The returned matcher only looks for equality based on os,
+// architecture and variant.
+//
+// One may implement their own matcher if this doesn't provide the required
+// functionality.
+//
+// Applications should opt to use `Match` over directly parsing specifiers.
+func NewMatcher(platform specs.Platform) Matcher {
+ m := &matcher{
+ Platform: Normalize(platform),
+ }
+
+ if platform.OS == "windows" {
+ m.osvM = &windowsVersionMatcher{
+ windowsOSVersion: getWindowsOSVersion(platform.OSVersion),
+ }
+ // In prior versions, on windows, the returned matcher implements a
+ // MatchComprarer interface.
+ // This preserves that behavior for backwards compatibility.
+ //
+ // TODO: This isn't actually used in this package, except for a test case,
+ // which may have been an unintended side of some refactor.
+ // It was likely intended to be used in `Ordered` but it is not since
+ // `Less` that is implemented here ends up getting masked due to wrapping.
+ if runtime.GOOS == "windows" {
+ return &windowsMatchComparer{m}
+ }
+ }
+ return m
+}
+
+type osVerMatcher interface {
+ Match(string) bool
+}
+
+type matcher struct {
+ specs.Platform
+ osvM osVerMatcher
+}
+
+func (m *matcher) Match(platform specs.Platform) bool {
+ normalized := Normalize(platform)
+ return m.OS == normalized.OS &&
+ m.Architecture == normalized.Architecture &&
+ m.Variant == normalized.Variant &&
+ m.matchOSVersion(platform)
+}
+
+func (m *matcher) matchOSVersion(platform specs.Platform) bool {
+ if m.osvM != nil {
+ return m.osvM.Match(platform.OSVersion)
+ }
+ return true
+}
+
+func (m *matcher) String() string {
+ return FormatAll(m.Platform)
+}
+
+// ParseAll parses a list of platform specifiers into a list of platform.
+func ParseAll(specifiers []string) ([]specs.Platform, error) {
+ platforms := make([]specs.Platform, len(specifiers))
+ for i, s := range specifiers {
+ p, err := Parse(s)
+ if err != nil {
+ return nil, fmt.Errorf("invalid platform %s: %w", s, err)
+ }
+ platforms[i] = p
+ }
+ return platforms, nil
+}
+
+// Parse parses the platform specifier syntax into a platform declaration.
+//
+// Platform specifiers are in the format `[()]||[()]/[/]`.
+// The minimum required information for a platform specifier is the operating
+// system or architecture. The OSVersion can be part of the OS like `windows(10.0.17763)`
+// When an OSVersion is specified, then specs.Platform.OSVersion is populated with that value,
+// and an empty string otherwise.
+// If there is only a single string (no slashes), the
+// value will be matched against the known set of operating systems, then fall
+// back to the known set of architectures. The missing component will be
+// inferred based on the local environment.
+func Parse(specifier string) (specs.Platform, error) {
+ if strings.Contains(specifier, "*") {
+ // TODO(stevvooe): need to work out exact wildcard handling
+ return specs.Platform{}, fmt.Errorf("%q: wildcards not yet supported: %w", specifier, errInvalidArgument)
+ }
+
+ // Limit to 4 elements to prevent unbounded split
+ parts := strings.SplitN(specifier, "/", 4)
+
+ var p specs.Platform
+ for i, part := range parts {
+ if i == 0 {
+ // First element is [()]
+ osVer := osAndVersionRe.FindStringSubmatch(part)
+ if osVer == nil {
+ return specs.Platform{}, fmt.Errorf("%q is an invalid OS component of %q: OSAndVersion specifier component must match %q: %w", part, specifier, osAndVersionRe.String(), errInvalidArgument)
+ }
+
+ p.OS = normalizeOS(osVer[1])
+ p.OSVersion = osVer[2]
+ } else {
+ if !specifierRe.MatchString(part) {
+ return specs.Platform{}, fmt.Errorf("%q is an invalid component of %q: platform specifier component must match %q: %w", part, specifier, specifierRe.String(), errInvalidArgument)
+ }
+ }
+ }
+
+ switch len(parts) {
+ case 1:
+ // in this case, we will test that the value might be an OS (with or
+ // without the optional OSVersion specified) and look it up.
+ // If it is not known, we'll treat it as an architecture. Since
+ // we have very little information about the platform here, we are
+ // going to be a little more strict if we don't know about the argument
+ // value.
+ if isKnownOS(p.OS) {
+ // picks a default architecture
+ p.Architecture = runtime.GOARCH
+ if p.Architecture == "arm" && cpuVariant() != "v7" {
+ p.Variant = cpuVariant()
+ }
+
+ return p, nil
+ }
+
+ p.Architecture, p.Variant = normalizeArch(parts[0], "")
+ if p.Architecture == "arm" && p.Variant == "v7" {
+ p.Variant = ""
+ }
+ if isKnownArch(p.Architecture) {
+ p.OS = runtime.GOOS
+ return p, nil
+ }
+
+ return specs.Platform{}, fmt.Errorf("%q: unknown operating system or architecture: %w", specifier, errInvalidArgument)
+ case 2:
+ // In this case, we treat as a regular OS[(OSVersion)]/arch pair. We don't care
+ // about whether or not we know of the platform.
+ p.Architecture, p.Variant = normalizeArch(parts[1], "")
+ if p.Architecture == "arm" && p.Variant == "v7" {
+ p.Variant = ""
+ }
+
+ return p, nil
+ case 3:
+ // we have a fully specified variant, this is rare
+ p.Architecture, p.Variant = normalizeArch(parts[1], parts[2])
+ if p.Architecture == "arm64" && p.Variant == "" {
+ p.Variant = "v8"
+ }
+
+ return p, nil
+ }
+
+ return specs.Platform{}, fmt.Errorf("%q: cannot parse platform specifier: %w", specifier, errInvalidArgument)
+}
+
+// MustParse is like Parses but panics if the specifier cannot be parsed.
+// Simplifies initialization of global variables.
+func MustParse(specifier string) specs.Platform {
+ p, err := Parse(specifier)
+ if err != nil {
+ panic("platform: Parse(" + strconv.Quote(specifier) + "): " + err.Error())
+ }
+ return p
+}
+
+// Format returns a string specifier from the provided platform specification.
+func Format(platform specs.Platform) string {
+ if platform.OS == "" {
+ return "unknown"
+ }
+
+ return path.Join(platform.OS, platform.Architecture, platform.Variant)
+}
+
+// FormatAll returns a string specifier that also includes the OSVersion from the
+// provided platform specification.
+func FormatAll(platform specs.Platform) string {
+ if platform.OS == "" {
+ return "unknown"
+ }
+
+ if platform.OSVersion != "" {
+ OSAndVersion := fmt.Sprintf(osAndVersionFormat, platform.OS, platform.OSVersion)
+ return path.Join(OSAndVersion, platform.Architecture, platform.Variant)
+ }
+ return path.Join(platform.OS, platform.Architecture, platform.Variant)
+}
+
+// Normalize validates and translate the platform to the canonical value.
+//
+// For example, if "Aarch64" is encountered, we change it to "arm64" or if
+// "x86_64" is encountered, it becomes "amd64".
+func Normalize(platform specs.Platform) specs.Platform {
+ platform.OS = normalizeOS(platform.OS)
+ platform.Architecture, platform.Variant = normalizeArch(platform.Architecture, platform.Variant)
+
+ return platform
+}
diff --git a/vendor/github.com/cpuguy83/dockercfg/LICENSE b/vendor/github.com/cpuguy83/dockercfg/LICENSE
new file mode 100644
index 0000000000..8ed6818044
--- /dev/null
+++ b/vendor/github.com/cpuguy83/dockercfg/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2020 Brian Goff
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
\ No newline at end of file
diff --git a/vendor/github.com/cpuguy83/dockercfg/README.md b/vendor/github.com/cpuguy83/dockercfg/README.md
new file mode 100644
index 0000000000..880ab80109
--- /dev/null
+++ b/vendor/github.com/cpuguy83/dockercfg/README.md
@@ -0,0 +1,8 @@
+### github.com/cpuguy83/dockercfg
+Go library to load docker CLI configs, auths, etc. with minimal deps.
+So far the only deps are on the stdlib.
+
+### Usage
+See the [godoc](https://godoc.org/github.com/cpuguy83/dockercfg) for API details.
+
+I'm currently using this in [zapp](https://github.com/cpuguy83/zapp/blob/d25c43d4cd7ccf29fba184aafbc720a753e1a15d/main.go#L58-L83) to handle registry auth instead of always asking the user to enter it.
\ No newline at end of file
diff --git a/vendor/github.com/cpuguy83/dockercfg/auth.go b/vendor/github.com/cpuguy83/dockercfg/auth.go
new file mode 100644
index 0000000000..106ab8479b
--- /dev/null
+++ b/vendor/github.com/cpuguy83/dockercfg/auth.go
@@ -0,0 +1,215 @@
+package dockercfg
+
+import (
+ "bytes"
+ "encoding/base64"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "io/fs"
+ "os/exec"
+ "runtime"
+ "strings"
+)
+
+// This is used by the docker CLI in cases where an oauth identity token is used.
+// In that case the username is stored literally as ``
+// When fetching the credentials we check for this value to determine if.
+const tokenUsername = ""
+
+// GetRegistryCredentials gets registry credentials for the passed in registry host.
+//
+// This will use [LoadDefaultConfig] to read registry auth details from the config.
+// If the config doesn't exist, it will attempt to load registry credentials using the default credential helper for the platform.
+func GetRegistryCredentials(hostname string) (string, string, error) {
+ cfg, err := LoadDefaultConfig()
+ if err != nil {
+ if !errors.Is(err, fs.ErrNotExist) {
+ return "", "", fmt.Errorf("load default config: %w", err)
+ }
+
+ return GetCredentialsFromHelper("", hostname)
+ }
+
+ return cfg.GetRegistryCredentials(hostname)
+}
+
+// ResolveRegistryHost can be used to transform a docker registry host name into what is used for the docker config/cred helpers
+//
+// This is useful for using with containerd authorizers.
+// Naturally this only transforms docker hub URLs.
+func ResolveRegistryHost(host string) string {
+ switch host {
+ case "index.docker.io", "docker.io", "https://index.docker.io/v1/", "registry-1.docker.io":
+ return "https://index.docker.io/v1/"
+ }
+ return host
+}
+
+// GetRegistryCredentials gets credentials, if any, for the provided hostname.
+//
+// Hostnames should already be resolved using [ResolveRegistryHost].
+//
+// If the returned username string is empty, the password is an identity token.
+func (c *Config) GetRegistryCredentials(hostname string) (string, string, error) {
+ h, ok := c.CredentialHelpers[hostname]
+ if ok {
+ return GetCredentialsFromHelper(h, hostname)
+ }
+
+ if c.CredentialsStore != "" {
+ username, password, err := GetCredentialsFromHelper(c.CredentialsStore, hostname)
+ if err != nil {
+ return "", "", fmt.Errorf("get credentials from store: %w", err)
+ }
+
+ if username != "" || password != "" {
+ return username, password, nil
+ }
+ }
+
+ auth, ok := c.AuthConfigs[hostname]
+ if !ok {
+ return GetCredentialsFromHelper("", hostname)
+ }
+
+ if auth.IdentityToken != "" {
+ return "", auth.IdentityToken, nil
+ }
+
+ if auth.Username != "" && auth.Password != "" {
+ return auth.Username, auth.Password, nil
+ }
+
+ return DecodeBase64Auth(auth)
+}
+
+// DecodeBase64Auth decodes the legacy file-based auth storage from the docker CLI.
+// It takes the "Auth" filed from AuthConfig and decodes that into a username and password.
+//
+// If "Auth" is empty, an empty user/pass will be returned, but not an error.
+func DecodeBase64Auth(auth AuthConfig) (string, string, error) {
+ if auth.Auth == "" {
+ return "", "", nil
+ }
+
+ decLen := base64.StdEncoding.DecodedLen(len(auth.Auth))
+ decoded := make([]byte, decLen)
+ n, err := base64.StdEncoding.Decode(decoded, []byte(auth.Auth))
+ if err != nil {
+ return "", "", fmt.Errorf("decode auth: %w", err)
+ }
+
+ decoded = decoded[:n]
+
+ const sep = ":"
+ user, pass, found := strings.Cut(string(decoded), sep)
+ if !found {
+ return "", "", fmt.Errorf("invalid auth: missing %q separator", sep)
+ }
+
+ return user, pass, nil
+}
+
+// Errors from credential helpers.
+var (
+ ErrCredentialsNotFound = errors.New("credentials not found in native keychain")
+ ErrCredentialsMissingServerURL = errors.New("no credentials server URL")
+)
+
+//nolint:gochecknoglobals // These are used to mock exec in tests.
+var (
+ // execLookPath is a variable that can be used to mock exec.LookPath in tests.
+ execLookPath = exec.LookPath
+ // execCommand is a variable that can be used to mock exec.Command in tests.
+ execCommand = exec.Command
+)
+
+// GetCredentialsFromHelper attempts to lookup credentials from the passed in docker credential helper.
+//
+// The credential helper should just be the suffix name (no "docker-credential-").
+// If the passed in helper program is empty this will look up the default helper for the platform.
+//
+// If the credentials are not found, no error is returned, only empty credentials.
+//
+// Hostnames should already be resolved using [ResolveRegistryHost]
+//
+// If the username string is empty, the password string is an identity token.
+func GetCredentialsFromHelper(helper, hostname string) (string, string, error) {
+ if helper == "" {
+ helper, helperErr := getCredentialHelper()
+ if helperErr != nil {
+ return "", "", fmt.Errorf("get credential helper: %w", helperErr)
+ }
+
+ if helper == "" {
+ return "", "", nil
+ }
+ }
+
+ helper = "docker-credential-" + helper
+ p, err := execLookPath(helper)
+ if err != nil {
+ if !errors.Is(err, exec.ErrNotFound) {
+ return "", "", fmt.Errorf("look up %q: %w", helper, err)
+ }
+
+ return "", "", nil
+ }
+
+ var outBuf, errBuf bytes.Buffer
+ cmd := execCommand(p, "get")
+ cmd.Stdin = strings.NewReader(hostname)
+ cmd.Stdout = &outBuf
+ cmd.Stderr = &errBuf
+
+ if err = cmd.Run(); err != nil {
+ out := strings.TrimSpace(outBuf.String())
+ switch out {
+ case ErrCredentialsNotFound.Error():
+ return "", "", nil
+ case ErrCredentialsMissingServerURL.Error():
+ return "", "", ErrCredentialsMissingServerURL
+ default:
+ return "", "", fmt.Errorf("execute %q stdout: %q stderr: %q: %w",
+ helper, out, strings.TrimSpace(errBuf.String()), err,
+ )
+ }
+ }
+
+ var creds struct {
+ Username string `json:"Username"`
+ Secret string `json:"Secret"`
+ }
+
+ if err = json.Unmarshal(outBuf.Bytes(), &creds); err != nil {
+ return "", "", fmt.Errorf("unmarshal credentials from: %q: %w", helper, err)
+ }
+
+ // When tokenUsername is used, the output is an identity token and the username is garbage.
+ if creds.Username == tokenUsername {
+ creds.Username = ""
+ }
+
+ return creds.Username, creds.Secret, nil
+}
+
+// getCredentialHelper gets the default credential helper name for the current platform.
+func getCredentialHelper() (string, error) {
+ switch runtime.GOOS {
+ case "linux":
+ if _, err := exec.LookPath("pass"); err != nil {
+ if errors.Is(err, exec.ErrNotFound) {
+ return "secretservice", nil
+ }
+ return "", fmt.Errorf(`look up "pass": %w`, err)
+ }
+ return "pass", nil
+ case "darwin":
+ return "osxkeychain", nil
+ case "windows":
+ return "wincred", nil
+ default:
+ return "", nil
+ }
+}
diff --git a/vendor/github.com/cpuguy83/dockercfg/config.go b/vendor/github.com/cpuguy83/dockercfg/config.go
new file mode 100644
index 0000000000..5e5390794b
--- /dev/null
+++ b/vendor/github.com/cpuguy83/dockercfg/config.go
@@ -0,0 +1,65 @@
+package dockercfg
+
+// Config represents the on disk format of the docker CLI's config file.
+type Config struct {
+ AuthConfigs map[string]AuthConfig `json:"auths"`
+ HTTPHeaders map[string]string `json:"HttpHeaders,omitempty"`
+ PsFormat string `json:"psFormat,omitempty"`
+ ImagesFormat string `json:"imagesFormat,omitempty"`
+ NetworksFormat string `json:"networksFormat,omitempty"`
+ PluginsFormat string `json:"pluginsFormat,omitempty"`
+ VolumesFormat string `json:"volumesFormat,omitempty"`
+ StatsFormat string `json:"statsFormat,omitempty"`
+ DetachKeys string `json:"detachKeys,omitempty"`
+ CredentialsStore string `json:"credsStore,omitempty"`
+ CredentialHelpers map[string]string `json:"credHelpers,omitempty"`
+ Filename string `json:"-"` // Note: for internal use only.
+ ServiceInspectFormat string `json:"serviceInspectFormat,omitempty"`
+ ServicesFormat string `json:"servicesFormat,omitempty"`
+ TasksFormat string `json:"tasksFormat,omitempty"`
+ SecretFormat string `json:"secretFormat,omitempty"`
+ ConfigFormat string `json:"configFormat,omitempty"`
+ NodesFormat string `json:"nodesFormat,omitempty"`
+ PruneFilters []string `json:"pruneFilters,omitempty"`
+ Proxies map[string]ProxyConfig `json:"proxies,omitempty"`
+ Experimental string `json:"experimental,omitempty"`
+ StackOrchestrator string `json:"stackOrchestrator,omitempty"`
+ Kubernetes *KubernetesConfig `json:"kubernetes,omitempty"`
+ CurrentContext string `json:"currentContext,omitempty"`
+ CLIPluginsExtraDirs []string `json:"cliPluginsExtraDirs,omitempty"`
+ Aliases map[string]string `json:"aliases,omitempty"`
+}
+
+// ProxyConfig contains proxy configuration settings.
+type ProxyConfig struct {
+ HTTPProxy string `json:"httpProxy,omitempty"`
+ HTTPSProxy string `json:"httpsProxy,omitempty"`
+ NoProxy string `json:"noProxy,omitempty"`
+ FTPProxy string `json:"ftpProxy,omitempty"`
+}
+
+// AuthConfig contains authorization information for connecting to a Registry.
+type AuthConfig struct {
+ Username string `json:"username,omitempty"`
+ Password string `json:"password,omitempty"`
+ Auth string `json:"auth,omitempty"`
+
+ // Email is an optional value associated with the username.
+ // This field is deprecated and will be removed in a later
+ // version of docker.
+ Email string `json:"email,omitempty"`
+
+ ServerAddress string `json:"serveraddress,omitempty"`
+
+ // IdentityToken is used to authenticate the user and get
+ // an access token for the registry.
+ IdentityToken string `json:"identitytoken,omitempty"`
+
+ // RegistryToken is a bearer token to be sent to a registry.
+ RegistryToken string `json:"registrytoken,omitempty"`
+}
+
+// KubernetesConfig contains Kubernetes orchestrator settings.
+type KubernetesConfig struct {
+ AllNamespaces string `json:"allNamespaces,omitempty"`
+}
diff --git a/vendor/github.com/cpuguy83/dockercfg/load.go b/vendor/github.com/cpuguy83/dockercfg/load.go
new file mode 100644
index 0000000000..a1c4dca021
--- /dev/null
+++ b/vendor/github.com/cpuguy83/dockercfg/load.go
@@ -0,0 +1,55 @@
+package dockercfg
+
+import (
+ "encoding/json"
+ "fmt"
+ "os"
+ "path/filepath"
+)
+
+// UserHomeConfigPath returns the path to the docker config in the current user's home dir.
+func UserHomeConfigPath() (string, error) {
+ home, err := os.UserHomeDir()
+ if err != nil {
+ return "", fmt.Errorf("user home dir: %w", err)
+ }
+
+ return filepath.Join(home, ".docker", "config.json"), nil
+}
+
+// ConfigPath returns the path to the docker cli config.
+//
+// It will either use the DOCKER_CONFIG env var if set, or the value from [UserHomeConfigPath]
+// DOCKER_CONFIG would be the dir path where `config.json` is stored, this returns the path to config.json.
+func ConfigPath() (string, error) {
+ if p := os.Getenv("DOCKER_CONFIG"); p != "" {
+ return filepath.Join(p, "config.json"), nil
+ }
+ return UserHomeConfigPath()
+}
+
+// LoadDefaultConfig loads the docker cli config from the path returned from [ConfigPath].
+func LoadDefaultConfig() (Config, error) {
+ var cfg Config
+ p, err := ConfigPath()
+ if err != nil {
+ return cfg, fmt.Errorf("config path: %w", err)
+ }
+
+ return cfg, FromFile(p, &cfg)
+}
+
+// FromFile loads config from the specified path into cfg.
+func FromFile(configPath string, cfg *Config) error {
+ f, err := os.Open(configPath)
+ if err != nil {
+ return fmt.Errorf("open config: %w", err)
+ }
+ defer f.Close()
+
+ if err = json.NewDecoder(f).Decode(&cfg); err != nil {
+ return fmt.Errorf("decode config: %w", err)
+ }
+
+ return nil
+}
diff --git a/vendor/github.com/distribution/reference/.gitattributes b/vendor/github.com/distribution/reference/.gitattributes
new file mode 100644
index 0000000000..d207b1802b
--- /dev/null
+++ b/vendor/github.com/distribution/reference/.gitattributes
@@ -0,0 +1 @@
+*.go text eol=lf
diff --git a/vendor/github.com/distribution/reference/.gitignore b/vendor/github.com/distribution/reference/.gitignore
new file mode 100644
index 0000000000..dc07e6b04a
--- /dev/null
+++ b/vendor/github.com/distribution/reference/.gitignore
@@ -0,0 +1,2 @@
+# Cover profiles
+*.out
diff --git a/vendor/github.com/distribution/reference/.golangci.yml b/vendor/github.com/distribution/reference/.golangci.yml
new file mode 100644
index 0000000000..793f0bb7ec
--- /dev/null
+++ b/vendor/github.com/distribution/reference/.golangci.yml
@@ -0,0 +1,18 @@
+linters:
+ enable:
+ - bodyclose
+ - dupword # Checks for duplicate words in the source code
+ - gofmt
+ - goimports
+ - ineffassign
+ - misspell
+ - revive
+ - staticcheck
+ - unconvert
+ - unused
+ - vet
+ disable:
+ - errcheck
+
+run:
+ deadline: 2m
diff --git a/vendor/github.com/distribution/reference/CODE-OF-CONDUCT.md b/vendor/github.com/distribution/reference/CODE-OF-CONDUCT.md
new file mode 100644
index 0000000000..48f6704c6d
--- /dev/null
+++ b/vendor/github.com/distribution/reference/CODE-OF-CONDUCT.md
@@ -0,0 +1,5 @@
+# Code of Conduct
+
+We follow the [CNCF Code of Conduct](https://github.com/cncf/foundation/blob/main/code-of-conduct.md).
+
+Please contact the [CNCF Code of Conduct Committee](mailto:conduct@cncf.io) in order to report violations of the Code of Conduct.
diff --git a/vendor/github.com/distribution/reference/CONTRIBUTING.md b/vendor/github.com/distribution/reference/CONTRIBUTING.md
new file mode 100644
index 0000000000..ab21946656
--- /dev/null
+++ b/vendor/github.com/distribution/reference/CONTRIBUTING.md
@@ -0,0 +1,114 @@
+# Contributing to the reference library
+
+## Community help
+
+If you need help, please ask in the [#distribution](https://cloud-native.slack.com/archives/C01GVR8SY4R) channel on CNCF community slack.
+[Click here for an invite to the CNCF community slack](https://slack.cncf.io/)
+
+## Reporting security issues
+
+The maintainers take security seriously. If you discover a security
+issue, please bring it to their attention right away!
+
+Please **DO NOT** file a public issue, instead send your report privately to
+[cncf-distribution-security@lists.cncf.io](mailto:cncf-distribution-security@lists.cncf.io).
+
+## Reporting an issue properly
+
+By following these simple rules you will get better and faster feedback on your issue.
+
+ - search the bugtracker for an already reported issue
+
+### If you found an issue that describes your problem:
+
+ - please read other user comments first, and confirm this is the same issue: a given error condition might be indicative of different problems - you may also find a workaround in the comments
+ - please refrain from adding "same thing here" or "+1" comments
+ - you don't need to comment on an issue to get notified of updates: just hit the "subscribe" button
+ - comment if you have some new, technical and relevant information to add to the case
+ - __DO NOT__ comment on closed issues or merged PRs. If you think you have a related problem, open up a new issue and reference the PR or issue.
+
+### If you have not found an existing issue that describes your problem:
+
+ 1. create a new issue, with a succinct title that describes your issue:
+ - bad title: "It doesn't work with my docker"
+ - good title: "Private registry push fail: 400 error with E_INVALID_DIGEST"
+ 2. copy the output of (or similar for other container tools):
+ - `docker version`
+ - `docker info`
+ - `docker exec registry --version`
+ 3. copy the command line you used to launch your Registry
+ 4. restart your docker daemon in debug mode (add `-D` to the daemon launch arguments)
+ 5. reproduce your problem and get your docker daemon logs showing the error
+ 6. if relevant, copy your registry logs that show the error
+ 7. provide any relevant detail about your specific Registry configuration (e.g., storage backend used)
+ 8. indicate if you are using an enterprise proxy, Nginx, or anything else between you and your Registry
+
+## Contributing Code
+
+Contributions should be made via pull requests. Pull requests will be reviewed
+by one or more maintainers or reviewers and merged when acceptable.
+
+You should follow the basic GitHub workflow:
+
+ 1. Use your own [fork](https://help.github.com/en/articles/about-forks)
+ 2. Create your [change](https://github.com/containerd/project/blob/master/CONTRIBUTING.md#successful-changes)
+ 3. Test your code
+ 4. [Commit](https://github.com/containerd/project/blob/master/CONTRIBUTING.md#commit-messages) your work, always [sign your commits](https://github.com/containerd/project/blob/master/CONTRIBUTING.md#commit-messages)
+ 5. Push your change to your fork and create a [Pull Request](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/creating-a-pull-request-from-a-fork)
+
+Refer to [containerd's contribution guide](https://github.com/containerd/project/blob/master/CONTRIBUTING.md#successful-changes)
+for tips on creating a successful contribution.
+
+## Sign your work
+
+The sign-off is a simple line at the end of the explanation for the patch. Your
+signature certifies that you wrote the patch or otherwise have the right to pass
+it on as an open-source patch. The rules are pretty simple: if you can certify
+the below (from [developercertificate.org](http://developercertificate.org/)):
+
+```
+Developer Certificate of Origin
+Version 1.1
+
+Copyright (C) 2004, 2006 The Linux Foundation and its contributors.
+660 York Street, Suite 102,
+San Francisco, CA 94110 USA
+
+Everyone is permitted to copy and distribute verbatim copies of this
+license document, but changing it is not allowed.
+
+Developer's Certificate of Origin 1.1
+
+By making a contribution to this project, I certify that:
+
+(a) The contribution was created in whole or in part by me and I
+ have the right to submit it under the open source license
+ indicated in the file; or
+
+(b) The contribution is based upon previous work that, to the best
+ of my knowledge, is covered under an appropriate open source
+ license and I have the right under that license to submit that
+ work with modifications, whether created in whole or in part
+ by me, under the same open source license (unless I am
+ permitted to submit under a different license), as indicated
+ in the file; or
+
+(c) The contribution was provided directly to me by some other
+ person who certified (a), (b) or (c) and I have not modified
+ it.
+
+(d) I understand and agree that this project and the contribution
+ are public and that a record of the contribution (including all
+ personal information I submit with it, including my sign-off) is
+ maintained indefinitely and may be redistributed consistent with
+ this project or the open source license(s) involved.
+```
+
+Then you just add a line to every git commit message:
+
+ Signed-off-by: Joe Smith
+
+Use your real name (sorry, no pseudonyms or anonymous contributions.)
+
+If you set your `user.name` and `user.email` git configs, you can sign your
+commit automatically with `git commit -s`.
diff --git a/vendor/github.com/distribution/reference/GOVERNANCE.md b/vendor/github.com/distribution/reference/GOVERNANCE.md
new file mode 100644
index 0000000000..200045b050
--- /dev/null
+++ b/vendor/github.com/distribution/reference/GOVERNANCE.md
@@ -0,0 +1,144 @@
+# distribution/reference Project Governance
+
+Distribution [Code of Conduct](./CODE-OF-CONDUCT.md) can be found here.
+
+For specific guidance on practical contribution steps please
+see our [CONTRIBUTING.md](./CONTRIBUTING.md) guide.
+
+## Maintainership
+
+There are different types of maintainers, with different responsibilities, but
+all maintainers have 3 things in common:
+
+1) They share responsibility in the project's success.
+2) They have made a long-term, recurring time investment to improve the project.
+3) They spend that time doing whatever needs to be done, not necessarily what
+is the most interesting or fun.
+
+Maintainers are often under-appreciated, because their work is harder to appreciate.
+It's easy to appreciate a really cool and technically advanced feature. It's harder
+to appreciate the absence of bugs, the slow but steady improvement in stability,
+or the reliability of a release process. But those things distinguish a good
+project from a great one.
+
+## Reviewers
+
+A reviewer is a core role within the project.
+They share in reviewing issues and pull requests and their LGTM counts towards the
+required LGTM count to merge a code change into the project.
+
+Reviewers are part of the organization but do not have write access.
+Becoming a reviewer is a core aspect in the journey to becoming a maintainer.
+
+## Adding maintainers
+
+Maintainers are first and foremost contributors that have shown they are
+committed to the long term success of a project. Contributors wanting to become
+maintainers are expected to be deeply involved in contributing code, pull
+request review, and triage of issues in the project for more than three months.
+
+Just contributing does not make you a maintainer, it is about building trust
+with the current maintainers of the project and being a person that they can
+depend on and trust to make decisions in the best interest of the project.
+
+Periodically, the existing maintainers curate a list of contributors that have
+shown regular activity on the project over the prior months. From this list,
+maintainer candidates are selected and proposed in a pull request or a
+maintainers communication channel.
+
+After a candidate has been announced to the maintainers, the existing
+maintainers are given five business days to discuss the candidate, raise
+objections and cast their vote. Votes may take place on the communication
+channel or via pull request comment. Candidates must be approved by at least 66%
+of the current maintainers by adding their vote on the mailing list. The
+reviewer role has the same process but only requires 33% of current maintainers.
+Only maintainers of the repository that the candidate is proposed for are
+allowed to vote.
+
+If a candidate is approved, a maintainer will contact the candidate to invite
+the candidate to open a pull request that adds the contributor to the
+MAINTAINERS file. The voting process may take place inside a pull request if a
+maintainer has already discussed the candidacy with the candidate and a
+maintainer is willing to be a sponsor by opening the pull request. The candidate
+becomes a maintainer once the pull request is merged.
+
+## Stepping down policy
+
+Life priorities, interests, and passions can change. If you're a maintainer but
+feel you must remove yourself from the list, inform other maintainers that you
+intend to step down, and if possible, help find someone to pick up your work.
+At the very least, ensure your work can be continued where you left off.
+
+After you've informed other maintainers, create a pull request to remove
+yourself from the MAINTAINERS file.
+
+## Removal of inactive maintainers
+
+Similar to the procedure for adding new maintainers, existing maintainers can
+be removed from the list if they do not show significant activity on the
+project. Periodically, the maintainers review the list of maintainers and their
+activity over the last three months.
+
+If a maintainer has shown insufficient activity over this period, a neutral
+person will contact the maintainer to ask if they want to continue being
+a maintainer. If the maintainer decides to step down as a maintainer, they
+open a pull request to be removed from the MAINTAINERS file.
+
+If the maintainer wants to remain a maintainer, but is unable to perform the
+required duties they can be removed with a vote of at least 66% of the current
+maintainers. In this case, maintainers should first propose the change to
+maintainers via the maintainers communication channel, then open a pull request
+for voting. The voting period is five business days. The voting pull request
+should not come as a surpise to any maintainer and any discussion related to
+performance must not be discussed on the pull request.
+
+## How are decisions made?
+
+Docker distribution is an open-source project with an open design philosophy.
+This means that the repository is the source of truth for EVERY aspect of the
+project, including its philosophy, design, road map, and APIs. *If it's part of
+the project, it's in the repo. If it's in the repo, it's part of the project.*
+
+As a result, all decisions can be expressed as changes to the repository. An
+implementation change is a change to the source code. An API change is a change
+to the API specification. A philosophy change is a change to the philosophy
+manifesto, and so on.
+
+All decisions affecting distribution, big and small, follow the same 3 steps:
+
+* Step 1: Open a pull request. Anyone can do this.
+
+* Step 2: Discuss the pull request. Anyone can do this.
+
+* Step 3: Merge or refuse the pull request. Who does this depends on the nature
+of the pull request and which areas of the project it affects.
+
+## Helping contributors with the DCO
+
+The [DCO or `Sign your work`](./CONTRIBUTING.md#sign-your-work)
+requirement is not intended as a roadblock or speed bump.
+
+Some contributors are not as familiar with `git`, or have used a web
+based editor, and thus asking them to `git commit --amend -s` is not the best
+way forward.
+
+In this case, maintainers can update the commits based on clause (c) of the DCO.
+The most trivial way for a contributor to allow the maintainer to do this, is to
+add a DCO signature in a pull requests's comment, or a maintainer can simply
+note that the change is sufficiently trivial that it does not substantially
+change the existing contribution - i.e., a spelling change.
+
+When you add someone's DCO, please also add your own to keep a log.
+
+## I'm a maintainer. Should I make pull requests too?
+
+Yes. Nobody should ever push to master directly. All changes should be
+made through a pull request.
+
+## Conflict Resolution
+
+If you have a technical dispute that you feel has reached an impasse with a
+subset of the community, any contributor may open an issue, specifically
+calling for a resolution vote of the current core maintainers to resolve the
+dispute. The same voting quorums required (2/3) for adding and removing
+maintainers will apply to conflict resolution.
diff --git a/vendor/github.com/distribution/reference/LICENSE b/vendor/github.com/distribution/reference/LICENSE
new file mode 100644
index 0000000000..e06d208186
--- /dev/null
+++ b/vendor/github.com/distribution/reference/LICENSE
@@ -0,0 +1,202 @@
+Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "{}"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright {yyyy} {name of copyright owner}
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
diff --git a/vendor/github.com/distribution/reference/MAINTAINERS b/vendor/github.com/distribution/reference/MAINTAINERS
new file mode 100644
index 0000000000..9e0a60c8bd
--- /dev/null
+++ b/vendor/github.com/distribution/reference/MAINTAINERS
@@ -0,0 +1,26 @@
+# Distribution project maintainers & reviewers
+#
+# See GOVERNANCE.md for maintainer versus reviewer roles
+#
+# MAINTAINERS (cncf-distribution-maintainers@lists.cncf.io)
+# GitHub ID, Name, Email address
+"chrispat","Chris Patterson","chrispat@github.com"
+"clarkbw","Bryan Clark","clarkbw@github.com"
+"corhere","Cory Snider","csnider@mirantis.com"
+"deleteriousEffect","Hayley Swimelar","hswimelar@gitlab.com"
+"heww","He Weiwei","hweiwei@vmware.com"
+"joaodrp","João Pereira","jpereira@gitlab.com"
+"justincormack","Justin Cormack","justin.cormack@docker.com"
+"squizzi","Kyle Squizzato","ksquizzato@mirantis.com"
+"milosgajdos","Milos Gajdos","milosthegajdos@gmail.com"
+"sargun","Sargun Dhillon","sargun@sargun.me"
+"wy65701436","Wang Yan","wangyan@vmware.com"
+"stevelasker","Steve Lasker","steve.lasker@microsoft.com"
+#
+# REVIEWERS
+# GitHub ID, Name, Email address
+"dmcgowan","Derek McGowan","derek@mcgstyle.net"
+"stevvooe","Stephen Day","stevvooe@gmail.com"
+"thajeztah","Sebastiaan van Stijn","github@gone.nl"
+"DavidSpek", "David van der Spek", "vanderspek.david@gmail.com"
+"Jamstah", "James Hewitt", "james.hewitt@gmail.com"
diff --git a/vendor/github.com/distribution/reference/Makefile b/vendor/github.com/distribution/reference/Makefile
new file mode 100644
index 0000000000..c78576b75d
--- /dev/null
+++ b/vendor/github.com/distribution/reference/Makefile
@@ -0,0 +1,25 @@
+# Project packages.
+PACKAGES=$(shell go list ./...)
+
+# Flags passed to `go test`
+BUILDFLAGS ?=
+TESTFLAGS ?=
+
+.PHONY: all build test coverage
+.DEFAULT: all
+
+all: build
+
+build: ## no binaries to build, so just check compilation suceeds
+ go build ${BUILDFLAGS} ./...
+
+test: ## run tests
+ go test ${TESTFLAGS} ./...
+
+coverage: ## generate coverprofiles from the unit tests
+ rm -f coverage.txt
+ go test ${TESTFLAGS} -cover -coverprofile=cover.out ./...
+
+.PHONY: help
+help:
+ @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n"} /^[a-zA-Z_\/%-]+:.*?##/ { printf " \033[36m%-27s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST)
diff --git a/vendor/github.com/distribution/reference/README.md b/vendor/github.com/distribution/reference/README.md
new file mode 100644
index 0000000000..172a02e0b3
--- /dev/null
+++ b/vendor/github.com/distribution/reference/README.md
@@ -0,0 +1,30 @@
+# Distribution reference
+
+Go library to handle references to container images.
+
+
+
+[](https://github.com/distribution/reference/actions?query=workflow%3ACI)
+[](https://pkg.go.dev/github.com/distribution/reference)
+[](LICENSE)
+[](https://codecov.io/gh/distribution/reference)
+[](https://app.fossa.com/projects/custom%2B162%2Fgithub.com%2Fdistribution%2Freference?ref=badge_shield)
+
+This repository contains a library for handling references to container images held in container registries. Please see [godoc](https://pkg.go.dev/github.com/distribution/reference) for details.
+
+## Contribution
+
+Please see [CONTRIBUTING.md](CONTRIBUTING.md) for details on how to contribute
+issues, fixes, and patches to this project.
+
+## Communication
+
+For async communication and long running discussions please use issues and pull requests on the github repo.
+This will be the best place to discuss design and implementation.
+
+For sync communication we have a #distribution channel in the [CNCF Slack](https://slack.cncf.io/)
+that everyone is welcome to join and chat about development.
+
+## Licenses
+
+The distribution codebase is released under the [Apache 2.0 license](LICENSE).
diff --git a/vendor/github.com/distribution/reference/SECURITY.md b/vendor/github.com/distribution/reference/SECURITY.md
new file mode 100644
index 0000000000..aaf983c0f0
--- /dev/null
+++ b/vendor/github.com/distribution/reference/SECURITY.md
@@ -0,0 +1,7 @@
+# Security Policy
+
+## Reporting a Vulnerability
+
+The maintainers take security seriously. If you discover a security issue, please bring it to their attention right away!
+
+Please DO NOT file a public issue, instead send your report privately to cncf-distribution-security@lists.cncf.io.
diff --git a/vendor/github.com/distribution/reference/distribution-logo.svg b/vendor/github.com/distribution/reference/distribution-logo.svg
new file mode 100644
index 0000000000..cc9f4073b9
--- /dev/null
+++ b/vendor/github.com/distribution/reference/distribution-logo.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/vendor/github.com/distribution/reference/helpers.go b/vendor/github.com/distribution/reference/helpers.go
new file mode 100644
index 0000000000..d10c7ef838
--- /dev/null
+++ b/vendor/github.com/distribution/reference/helpers.go
@@ -0,0 +1,42 @@
+package reference
+
+import "path"
+
+// IsNameOnly returns true if reference only contains a repo name.
+func IsNameOnly(ref Named) bool {
+ if _, ok := ref.(NamedTagged); ok {
+ return false
+ }
+ if _, ok := ref.(Canonical); ok {
+ return false
+ }
+ return true
+}
+
+// FamiliarName returns the familiar name string
+// for the given named, familiarizing if needed.
+func FamiliarName(ref Named) string {
+ if nn, ok := ref.(normalizedNamed); ok {
+ return nn.Familiar().Name()
+ }
+ return ref.Name()
+}
+
+// FamiliarString returns the familiar string representation
+// for the given reference, familiarizing if needed.
+func FamiliarString(ref Reference) string {
+ if nn, ok := ref.(normalizedNamed); ok {
+ return nn.Familiar().String()
+ }
+ return ref.String()
+}
+
+// FamiliarMatch reports whether ref matches the specified pattern.
+// See [path.Match] for supported patterns.
+func FamiliarMatch(pattern string, ref Reference) (bool, error) {
+ matched, err := path.Match(pattern, FamiliarString(ref))
+ if namedRef, isNamed := ref.(Named); isNamed && !matched {
+ matched, _ = path.Match(pattern, FamiliarName(namedRef))
+ }
+ return matched, err
+}
diff --git a/vendor/github.com/distribution/reference/normalize.go b/vendor/github.com/distribution/reference/normalize.go
new file mode 100644
index 0000000000..f4128314c1
--- /dev/null
+++ b/vendor/github.com/distribution/reference/normalize.go
@@ -0,0 +1,255 @@
+package reference
+
+import (
+ "fmt"
+ "strings"
+
+ "github.com/opencontainers/go-digest"
+)
+
+const (
+ // legacyDefaultDomain is the legacy domain for Docker Hub (which was
+ // originally named "the Docker Index"). This domain is still used for
+ // authentication and image search, which were part of the "v1" Docker
+ // registry specification.
+ //
+ // This domain will continue to be supported, but there are plans to consolidate
+ // legacy domains to new "canonical" domains. Once those domains are decided
+ // on, we must update the normalization functions, but preserve compatibility
+ // with existing installs, clients, and user configuration.
+ legacyDefaultDomain = "index.docker.io"
+
+ // defaultDomain is the default domain used for images on Docker Hub.
+ // It is used to normalize "familiar" names to canonical names, for example,
+ // to convert "ubuntu" to "docker.io/library/ubuntu:latest".
+ //
+ // Note that actual domain of Docker Hub's registry is registry-1.docker.io.
+ // This domain will continue to be supported, but there are plans to consolidate
+ // legacy domains to new "canonical" domains. Once those domains are decided
+ // on, we must update the normalization functions, but preserve compatibility
+ // with existing installs, clients, and user configuration.
+ defaultDomain = "docker.io"
+
+ // officialRepoPrefix is the namespace used for official images on Docker Hub.
+ // It is used to normalize "familiar" names to canonical names, for example,
+ // to convert "ubuntu" to "docker.io/library/ubuntu:latest".
+ officialRepoPrefix = "library/"
+
+ // defaultTag is the default tag if no tag is provided.
+ defaultTag = "latest"
+)
+
+// normalizedNamed represents a name which has been
+// normalized and has a familiar form. A familiar name
+// is what is used in Docker UI. An example normalized
+// name is "docker.io/library/ubuntu" and corresponding
+// familiar name of "ubuntu".
+type normalizedNamed interface {
+ Named
+ Familiar() Named
+}
+
+// ParseNormalizedNamed parses a string into a named reference
+// transforming a familiar name from Docker UI to a fully
+// qualified reference. If the value may be an identifier
+// use ParseAnyReference.
+func ParseNormalizedNamed(s string) (Named, error) {
+ if ok := anchoredIdentifierRegexp.MatchString(s); ok {
+ return nil, fmt.Errorf("invalid repository name (%s), cannot specify 64-byte hexadecimal strings", s)
+ }
+ domain, remainder := splitDockerDomain(s)
+ var remote string
+ if tagSep := strings.IndexRune(remainder, ':'); tagSep > -1 {
+ remote = remainder[:tagSep]
+ } else {
+ remote = remainder
+ }
+ if strings.ToLower(remote) != remote {
+ return nil, fmt.Errorf("invalid reference format: repository name (%s) must be lowercase", remote)
+ }
+
+ ref, err := Parse(domain + "/" + remainder)
+ if err != nil {
+ return nil, err
+ }
+ named, isNamed := ref.(Named)
+ if !isNamed {
+ return nil, fmt.Errorf("reference %s has no name", ref.String())
+ }
+ return named, nil
+}
+
+// namedTaggedDigested is a reference that has both a tag and a digest.
+type namedTaggedDigested interface {
+ NamedTagged
+ Digested
+}
+
+// ParseDockerRef normalizes the image reference following the docker convention,
+// which allows for references to contain both a tag and a digest. It returns a
+// reference that is either tagged or digested. For references containing both
+// a tag and a digest, it returns a digested reference. For example, the following
+// reference:
+//
+// docker.io/library/busybox:latest@sha256:7cc4b5aefd1d0cadf8d97d4350462ba51c694ebca145b08d7d41b41acc8db5aa
+//
+// Is returned as a digested reference (with the ":latest" tag removed):
+//
+// docker.io/library/busybox@sha256:7cc4b5aefd1d0cadf8d97d4350462ba51c694ebca145b08d7d41b41acc8db5aa
+//
+// References that are already "tagged" or "digested" are returned unmodified:
+//
+// // Already a digested reference
+// docker.io/library/busybox@sha256:7cc4b5aefd1d0cadf8d97d4350462ba51c694ebca145b08d7d41b41acc8db5aa
+//
+// // Already a named reference
+// docker.io/library/busybox:latest
+func ParseDockerRef(ref string) (Named, error) {
+ named, err := ParseNormalizedNamed(ref)
+ if err != nil {
+ return nil, err
+ }
+ if canonical, ok := named.(namedTaggedDigested); ok {
+ // The reference is both tagged and digested; only return digested.
+ newNamed, err := WithName(canonical.Name())
+ if err != nil {
+ return nil, err
+ }
+ return WithDigest(newNamed, canonical.Digest())
+ }
+ return TagNameOnly(named), nil
+}
+
+// splitDockerDomain splits a repository name to domain and remote-name.
+// If no valid domain is found, the default domain is used. Repository name
+// needs to be already validated before.
+func splitDockerDomain(name string) (domain, remoteName string) {
+ maybeDomain, maybeRemoteName, ok := strings.Cut(name, "/")
+ if !ok {
+ // Fast-path for single element ("familiar" names), such as "ubuntu"
+ // or "ubuntu:latest". Familiar names must be handled separately, to
+ // prevent them from being handled as "hostname:port".
+ //
+ // Canonicalize them as "docker.io/library/name[:tag]"
+
+ // FIXME(thaJeztah): account for bare "localhost" or "example.com" names, which SHOULD be considered a domain.
+ return defaultDomain, officialRepoPrefix + name
+ }
+
+ switch {
+ case maybeDomain == localhost:
+ // localhost is a reserved namespace and always considered a domain.
+ domain, remoteName = maybeDomain, maybeRemoteName
+ case maybeDomain == legacyDefaultDomain:
+ // canonicalize the Docker Hub and legacy "Docker Index" domains.
+ domain, remoteName = defaultDomain, maybeRemoteName
+ case strings.ContainsAny(maybeDomain, ".:"):
+ // Likely a domain or IP-address:
+ //
+ // - contains a "." (e.g., "example.com" or "127.0.0.1")
+ // - contains a ":" (e.g., "example:5000", "::1", or "[::1]:5000")
+ domain, remoteName = maybeDomain, maybeRemoteName
+ case strings.ToLower(maybeDomain) != maybeDomain:
+ // Uppercase namespaces are not allowed, so if the first element
+ // is not lowercase, we assume it to be a domain-name.
+ domain, remoteName = maybeDomain, maybeRemoteName
+ default:
+ // None of the above: it's not a domain, so use the default, and
+ // use the name input the remote-name.
+ domain, remoteName = defaultDomain, name
+ }
+
+ if domain == defaultDomain && !strings.ContainsRune(remoteName, '/') {
+ // Canonicalize "familiar" names, but only on Docker Hub, not
+ // on other domains:
+ //
+ // "docker.io/ubuntu[:tag]" => "docker.io/library/ubuntu[:tag]"
+ remoteName = officialRepoPrefix + remoteName
+ }
+
+ return domain, remoteName
+}
+
+// familiarizeName returns a shortened version of the name familiar
+// to the Docker UI. Familiar names have the default domain
+// "docker.io" and "library/" repository prefix removed.
+// For example, "docker.io/library/redis" will have the familiar
+// name "redis" and "docker.io/dmcgowan/myapp" will be "dmcgowan/myapp".
+// Returns a familiarized named only reference.
+func familiarizeName(named namedRepository) repository {
+ repo := repository{
+ domain: named.Domain(),
+ path: named.Path(),
+ }
+
+ if repo.domain == defaultDomain {
+ repo.domain = ""
+ // Handle official repositories which have the pattern "library/"
+ if strings.HasPrefix(repo.path, officialRepoPrefix) {
+ // TODO(thaJeztah): this check may be too strict, as it assumes the
+ // "library/" namespace does not have nested namespaces. While this
+ // is true (currently), technically it would be possible for Docker
+ // Hub to use those (e.g. "library/distros/ubuntu:latest").
+ // See https://github.com/distribution/distribution/pull/3769#issuecomment-1302031785.
+ if remainder := strings.TrimPrefix(repo.path, officialRepoPrefix); !strings.ContainsRune(remainder, '/') {
+ repo.path = remainder
+ }
+ }
+ }
+ return repo
+}
+
+func (r reference) Familiar() Named {
+ return reference{
+ namedRepository: familiarizeName(r.namedRepository),
+ tag: r.tag,
+ digest: r.digest,
+ }
+}
+
+func (r repository) Familiar() Named {
+ return familiarizeName(r)
+}
+
+func (t taggedReference) Familiar() Named {
+ return taggedReference{
+ namedRepository: familiarizeName(t.namedRepository),
+ tag: t.tag,
+ }
+}
+
+func (c canonicalReference) Familiar() Named {
+ return canonicalReference{
+ namedRepository: familiarizeName(c.namedRepository),
+ digest: c.digest,
+ }
+}
+
+// TagNameOnly adds the default tag "latest" to a reference if it only has
+// a repo name.
+func TagNameOnly(ref Named) Named {
+ if IsNameOnly(ref) {
+ namedTagged, err := WithTag(ref, defaultTag)
+ if err != nil {
+ // Default tag must be valid, to create a NamedTagged
+ // type with non-validated input the WithTag function
+ // should be used instead
+ panic(err)
+ }
+ return namedTagged
+ }
+ return ref
+}
+
+// ParseAnyReference parses a reference string as a possible identifier,
+// full digest, or familiar name.
+func ParseAnyReference(ref string) (Reference, error) {
+ if ok := anchoredIdentifierRegexp.MatchString(ref); ok {
+ return digestReference("sha256:" + ref), nil
+ }
+ if dgst, err := digest.Parse(ref); err == nil {
+ return digestReference(dgst), nil
+ }
+
+ return ParseNormalizedNamed(ref)
+}
diff --git a/vendor/github.com/distribution/reference/reference.go b/vendor/github.com/distribution/reference/reference.go
new file mode 100644
index 0000000000..900398bde7
--- /dev/null
+++ b/vendor/github.com/distribution/reference/reference.go
@@ -0,0 +1,432 @@
+// Package reference provides a general type to represent any way of referencing images within the registry.
+// Its main purpose is to abstract tags and digests (content-addressable hash).
+//
+// Grammar
+//
+// reference := name [ ":" tag ] [ "@" digest ]
+// name := [domain '/'] remote-name
+// domain := host [':' port-number]
+// host := domain-name | IPv4address | \[ IPv6address \] ; rfc3986 appendix-A
+// domain-name := domain-component ['.' domain-component]*
+// domain-component := /([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])/
+// port-number := /[0-9]+/
+// path-component := alpha-numeric [separator alpha-numeric]*
+// path (or "remote-name") := path-component ['/' path-component]*
+// alpha-numeric := /[a-z0-9]+/
+// separator := /[_.]|__|[-]*/
+//
+// tag := /[\w][\w.-]{0,127}/
+//
+// digest := digest-algorithm ":" digest-hex
+// digest-algorithm := digest-algorithm-component [ digest-algorithm-separator digest-algorithm-component ]*
+// digest-algorithm-separator := /[+.-_]/
+// digest-algorithm-component := /[A-Za-z][A-Za-z0-9]*/
+// digest-hex := /[0-9a-fA-F]{32,}/ ; At least 128 bit digest value
+//
+// identifier := /[a-f0-9]{64}/
+package reference
+
+import (
+ "errors"
+ "fmt"
+ "strings"
+
+ "github.com/opencontainers/go-digest"
+)
+
+const (
+ // RepositoryNameTotalLengthMax is the maximum total number of characters in a repository name.
+ RepositoryNameTotalLengthMax = 255
+
+ // NameTotalLengthMax is the maximum total number of characters in a repository name.
+ //
+ // Deprecated: use [RepositoryNameTotalLengthMax] instead.
+ NameTotalLengthMax = RepositoryNameTotalLengthMax
+)
+
+var (
+ // ErrReferenceInvalidFormat represents an error while trying to parse a string as a reference.
+ ErrReferenceInvalidFormat = errors.New("invalid reference format")
+
+ // ErrTagInvalidFormat represents an error while trying to parse a string as a tag.
+ ErrTagInvalidFormat = errors.New("invalid tag format")
+
+ // ErrDigestInvalidFormat represents an error while trying to parse a string as a tag.
+ ErrDigestInvalidFormat = errors.New("invalid digest format")
+
+ // ErrNameContainsUppercase is returned for invalid repository names that contain uppercase characters.
+ ErrNameContainsUppercase = errors.New("repository name must be lowercase")
+
+ // ErrNameEmpty is returned for empty, invalid repository names.
+ ErrNameEmpty = errors.New("repository name must have at least one component")
+
+ // ErrNameTooLong is returned when a repository name is longer than RepositoryNameTotalLengthMax.
+ ErrNameTooLong = fmt.Errorf("repository name must not be more than %v characters", RepositoryNameTotalLengthMax)
+
+ // ErrNameNotCanonical is returned when a name is not canonical.
+ ErrNameNotCanonical = errors.New("repository name must be canonical")
+)
+
+// Reference is an opaque object reference identifier that may include
+// modifiers such as a hostname, name, tag, and digest.
+type Reference interface {
+ // String returns the full reference
+ String() string
+}
+
+// Field provides a wrapper type for resolving correct reference types when
+// working with encoding.
+type Field struct {
+ reference Reference
+}
+
+// AsField wraps a reference in a Field for encoding.
+func AsField(reference Reference) Field {
+ return Field{reference}
+}
+
+// Reference unwraps the reference type from the field to
+// return the Reference object. This object should be
+// of the appropriate type to further check for different
+// reference types.
+func (f Field) Reference() Reference {
+ return f.reference
+}
+
+// MarshalText serializes the field to byte text which
+// is the string of the reference.
+func (f Field) MarshalText() (p []byte, err error) {
+ return []byte(f.reference.String()), nil
+}
+
+// UnmarshalText parses text bytes by invoking the
+// reference parser to ensure the appropriately
+// typed reference object is wrapped by field.
+func (f *Field) UnmarshalText(p []byte) error {
+ r, err := Parse(string(p))
+ if err != nil {
+ return err
+ }
+
+ f.reference = r
+ return nil
+}
+
+// Named is an object with a full name
+type Named interface {
+ Reference
+ Name() string
+}
+
+// Tagged is an object which has a tag
+type Tagged interface {
+ Reference
+ Tag() string
+}
+
+// NamedTagged is an object including a name and tag.
+type NamedTagged interface {
+ Named
+ Tag() string
+}
+
+// Digested is an object which has a digest
+// in which it can be referenced by
+type Digested interface {
+ Reference
+ Digest() digest.Digest
+}
+
+// Canonical reference is an object with a fully unique
+// name including a name with domain and digest
+type Canonical interface {
+ Named
+ Digest() digest.Digest
+}
+
+// namedRepository is a reference to a repository with a name.
+// A namedRepository has both domain and path components.
+type namedRepository interface {
+ Named
+ Domain() string
+ Path() string
+}
+
+// Domain returns the domain part of the [Named] reference.
+func Domain(named Named) string {
+ if r, ok := named.(namedRepository); ok {
+ return r.Domain()
+ }
+ domain, _ := splitDomain(named.Name())
+ return domain
+}
+
+// Path returns the name without the domain part of the [Named] reference.
+func Path(named Named) (name string) {
+ if r, ok := named.(namedRepository); ok {
+ return r.Path()
+ }
+ _, path := splitDomain(named.Name())
+ return path
+}
+
+// splitDomain splits a named reference into a hostname and path string.
+// If no valid hostname is found, the hostname is empty and the full value
+// is returned as name
+func splitDomain(name string) (string, string) {
+ match := anchoredNameRegexp.FindStringSubmatch(name)
+ if len(match) != 3 {
+ return "", name
+ }
+ return match[1], match[2]
+}
+
+// Parse parses s and returns a syntactically valid Reference.
+// If an error was encountered it is returned, along with a nil Reference.
+func Parse(s string) (Reference, error) {
+ matches := ReferenceRegexp.FindStringSubmatch(s)
+ if matches == nil {
+ if s == "" {
+ return nil, ErrNameEmpty
+ }
+ if ReferenceRegexp.FindStringSubmatch(strings.ToLower(s)) != nil {
+ return nil, ErrNameContainsUppercase
+ }
+ return nil, ErrReferenceInvalidFormat
+ }
+
+ var repo repository
+
+ nameMatch := anchoredNameRegexp.FindStringSubmatch(matches[1])
+ if len(nameMatch) == 3 {
+ repo.domain = nameMatch[1]
+ repo.path = nameMatch[2]
+ } else {
+ repo.domain = ""
+ repo.path = matches[1]
+ }
+
+ if len(repo.path) > RepositoryNameTotalLengthMax {
+ return nil, ErrNameTooLong
+ }
+
+ ref := reference{
+ namedRepository: repo,
+ tag: matches[2],
+ }
+ if matches[3] != "" {
+ var err error
+ ref.digest, err = digest.Parse(matches[3])
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ r := getBestReferenceType(ref)
+ if r == nil {
+ return nil, ErrNameEmpty
+ }
+
+ return r, nil
+}
+
+// ParseNamed parses s and returns a syntactically valid reference implementing
+// the Named interface. The reference must have a name and be in the canonical
+// form, otherwise an error is returned.
+// If an error was encountered it is returned, along with a nil Reference.
+func ParseNamed(s string) (Named, error) {
+ named, err := ParseNormalizedNamed(s)
+ if err != nil {
+ return nil, err
+ }
+ if named.String() != s {
+ return nil, ErrNameNotCanonical
+ }
+ return named, nil
+}
+
+// WithName returns a named object representing the given string. If the input
+// is invalid ErrReferenceInvalidFormat will be returned.
+func WithName(name string) (Named, error) {
+ match := anchoredNameRegexp.FindStringSubmatch(name)
+ if match == nil || len(match) != 3 {
+ return nil, ErrReferenceInvalidFormat
+ }
+
+ if len(match[2]) > RepositoryNameTotalLengthMax {
+ return nil, ErrNameTooLong
+ }
+
+ return repository{
+ domain: match[1],
+ path: match[2],
+ }, nil
+}
+
+// WithTag combines the name from "name" and the tag from "tag" to form a
+// reference incorporating both the name and the tag.
+func WithTag(name Named, tag string) (NamedTagged, error) {
+ if !anchoredTagRegexp.MatchString(tag) {
+ return nil, ErrTagInvalidFormat
+ }
+ var repo repository
+ if r, ok := name.(namedRepository); ok {
+ repo.domain = r.Domain()
+ repo.path = r.Path()
+ } else {
+ repo.path = name.Name()
+ }
+ if canonical, ok := name.(Canonical); ok {
+ return reference{
+ namedRepository: repo,
+ tag: tag,
+ digest: canonical.Digest(),
+ }, nil
+ }
+ return taggedReference{
+ namedRepository: repo,
+ tag: tag,
+ }, nil
+}
+
+// WithDigest combines the name from "name" and the digest from "digest" to form
+// a reference incorporating both the name and the digest.
+func WithDigest(name Named, digest digest.Digest) (Canonical, error) {
+ if !anchoredDigestRegexp.MatchString(digest.String()) {
+ return nil, ErrDigestInvalidFormat
+ }
+ var repo repository
+ if r, ok := name.(namedRepository); ok {
+ repo.domain = r.Domain()
+ repo.path = r.Path()
+ } else {
+ repo.path = name.Name()
+ }
+ if tagged, ok := name.(Tagged); ok {
+ return reference{
+ namedRepository: repo,
+ tag: tagged.Tag(),
+ digest: digest,
+ }, nil
+ }
+ return canonicalReference{
+ namedRepository: repo,
+ digest: digest,
+ }, nil
+}
+
+// TrimNamed removes any tag or digest from the named reference.
+func TrimNamed(ref Named) Named {
+ repo := repository{}
+ if r, ok := ref.(namedRepository); ok {
+ repo.domain, repo.path = r.Domain(), r.Path()
+ } else {
+ repo.domain, repo.path = splitDomain(ref.Name())
+ }
+ return repo
+}
+
+func getBestReferenceType(ref reference) Reference {
+ if ref.Name() == "" {
+ // Allow digest only references
+ if ref.digest != "" {
+ return digestReference(ref.digest)
+ }
+ return nil
+ }
+ if ref.tag == "" {
+ if ref.digest != "" {
+ return canonicalReference{
+ namedRepository: ref.namedRepository,
+ digest: ref.digest,
+ }
+ }
+ return ref.namedRepository
+ }
+ if ref.digest == "" {
+ return taggedReference{
+ namedRepository: ref.namedRepository,
+ tag: ref.tag,
+ }
+ }
+
+ return ref
+}
+
+type reference struct {
+ namedRepository
+ tag string
+ digest digest.Digest
+}
+
+func (r reference) String() string {
+ return r.Name() + ":" + r.tag + "@" + r.digest.String()
+}
+
+func (r reference) Tag() string {
+ return r.tag
+}
+
+func (r reference) Digest() digest.Digest {
+ return r.digest
+}
+
+type repository struct {
+ domain string
+ path string
+}
+
+func (r repository) String() string {
+ return r.Name()
+}
+
+func (r repository) Name() string {
+ if r.domain == "" {
+ return r.path
+ }
+ return r.domain + "/" + r.path
+}
+
+func (r repository) Domain() string {
+ return r.domain
+}
+
+func (r repository) Path() string {
+ return r.path
+}
+
+type digestReference digest.Digest
+
+func (d digestReference) String() string {
+ return digest.Digest(d).String()
+}
+
+func (d digestReference) Digest() digest.Digest {
+ return digest.Digest(d)
+}
+
+type taggedReference struct {
+ namedRepository
+ tag string
+}
+
+func (t taggedReference) String() string {
+ return t.Name() + ":" + t.tag
+}
+
+func (t taggedReference) Tag() string {
+ return t.tag
+}
+
+type canonicalReference struct {
+ namedRepository
+ digest digest.Digest
+}
+
+func (c canonicalReference) String() string {
+ return c.Name() + "@" + c.digest.String()
+}
+
+func (c canonicalReference) Digest() digest.Digest {
+ return c.digest
+}
diff --git a/vendor/github.com/distribution/reference/regexp.go b/vendor/github.com/distribution/reference/regexp.go
new file mode 100644
index 0000000000..65bc49d79b
--- /dev/null
+++ b/vendor/github.com/distribution/reference/regexp.go
@@ -0,0 +1,163 @@
+package reference
+
+import (
+ "regexp"
+ "strings"
+)
+
+// DigestRegexp matches well-formed digests, including algorithm (e.g. "sha256:").
+var DigestRegexp = regexp.MustCompile(digestPat)
+
+// DomainRegexp matches hostname or IP-addresses, optionally including a port
+// number. It defines the structure of potential domain components that may be
+// part of image names. This is purposely a subset of what is allowed by DNS to
+// ensure backwards compatibility with Docker image names. It may be a subset of
+// DNS domain name, an IPv4 address in decimal format, or an IPv6 address between
+// square brackets (excluding zone identifiers as defined by [RFC 6874] or special
+// addresses such as IPv4-Mapped).
+//
+// [RFC 6874]: https://www.rfc-editor.org/rfc/rfc6874.
+var DomainRegexp = regexp.MustCompile(domainAndPort)
+
+// IdentifierRegexp is the format for string identifier used as a
+// content addressable identifier using sha256. These identifiers
+// are like digests without the algorithm, since sha256 is used.
+var IdentifierRegexp = regexp.MustCompile(identifier)
+
+// NameRegexp is the format for the name component of references, including
+// an optional domain and port, but without tag or digest suffix.
+var NameRegexp = regexp.MustCompile(namePat)
+
+// ReferenceRegexp is the full supported format of a reference. The regexp
+// is anchored and has capturing groups for name, tag, and digest
+// components.
+var ReferenceRegexp = regexp.MustCompile(referencePat)
+
+// TagRegexp matches valid tag names. From [docker/docker:graph/tags.go].
+//
+// [docker/docker:graph/tags.go]: https://github.com/moby/moby/blob/v1.6.0/graph/tags.go#L26-L28
+var TagRegexp = regexp.MustCompile(tag)
+
+const (
+ // alphanumeric defines the alphanumeric atom, typically a
+ // component of names. This only allows lower case characters and digits.
+ alphanumeric = `[a-z0-9]+`
+
+ // separator defines the separators allowed to be embedded in name
+ // components. This allows one period, one or two underscore and multiple
+ // dashes. Repeated dashes and underscores are intentionally treated
+ // differently. In order to support valid hostnames as name components,
+ // supporting repeated dash was added. Additionally double underscore is
+ // now allowed as a separator to loosen the restriction for previously
+ // supported names.
+ separator = `(?:[._]|__|[-]+)`
+
+ // localhost is treated as a special value for domain-name. Any other
+ // domain-name without a "." or a ":port" are considered a path component.
+ localhost = `localhost`
+
+ // domainNameComponent restricts the registry domain component of a
+ // repository name to start with a component as defined by DomainRegexp.
+ domainNameComponent = `(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])`
+
+ // optionalPort matches an optional port-number including the port separator
+ // (e.g. ":80").
+ optionalPort = `(?::[0-9]+)?`
+
+ // tag matches valid tag names. From docker/docker:graph/tags.go.
+ tag = `[\w][\w.-]{0,127}`
+
+ // digestPat matches well-formed digests, including algorithm (e.g. "sha256:").
+ //
+ // TODO(thaJeztah): this should follow the same rules as https://pkg.go.dev/github.com/opencontainers/go-digest@v1.0.0#DigestRegexp
+ // so that go-digest defines the canonical format. Note that the go-digest is
+ // more relaxed:
+ // - it allows multiple algorithms (e.g. "sha256+b64:") to allow
+ // future expansion of supported algorithms.
+ // - it allows the "" value to use urlsafe base64 encoding as defined
+ // in [rfc4648, section 5].
+ //
+ // [rfc4648, section 5]: https://www.rfc-editor.org/rfc/rfc4648#section-5.
+ digestPat = `[A-Za-z][A-Za-z0-9]*(?:[-_+.][A-Za-z][A-Za-z0-9]*)*[:][[:xdigit:]]{32,}`
+
+ // identifier is the format for a content addressable identifier using sha256.
+ // These identifiers are like digests without the algorithm, since sha256 is used.
+ identifier = `([a-f0-9]{64})`
+
+ // ipv6address are enclosed between square brackets and may be represented
+ // in many ways, see rfc5952. Only IPv6 in compressed or uncompressed format
+ // are allowed, IPv6 zone identifiers (rfc6874) or Special addresses such as
+ // IPv4-Mapped are deliberately excluded.
+ ipv6address = `\[(?:[a-fA-F0-9:]+)\]`
+)
+
+var (
+ // domainName defines the structure of potential domain components
+ // that may be part of image names. This is purposely a subset of what is
+ // allowed by DNS to ensure backwards compatibility with Docker image
+ // names. This includes IPv4 addresses on decimal format.
+ domainName = domainNameComponent + anyTimes(`\.`+domainNameComponent)
+
+ // host defines the structure of potential domains based on the URI
+ // Host subcomponent on rfc3986. It may be a subset of DNS domain name,
+ // or an IPv4 address in decimal format, or an IPv6 address between square
+ // brackets (excluding zone identifiers as defined by rfc6874 or special
+ // addresses such as IPv4-Mapped).
+ host = `(?:` + domainName + `|` + ipv6address + `)`
+
+ // allowed by the URI Host subcomponent on rfc3986 to ensure backwards
+ // compatibility with Docker image names.
+ domainAndPort = host + optionalPort
+
+ // anchoredTagRegexp matches valid tag names, anchored at the start and
+ // end of the matched string.
+ anchoredTagRegexp = regexp.MustCompile(anchored(tag))
+
+ // anchoredDigestRegexp matches valid digests, anchored at the start and
+ // end of the matched string.
+ anchoredDigestRegexp = regexp.MustCompile(anchored(digestPat))
+
+ // pathComponent restricts path-components to start with an alphanumeric
+ // character, with following parts able to be separated by a separator
+ // (one period, one or two underscore and multiple dashes).
+ pathComponent = alphanumeric + anyTimes(separator+alphanumeric)
+
+ // remoteName matches the remote-name of a repository. It consists of one
+ // or more forward slash (/) delimited path-components:
+ //
+ // pathComponent[[/pathComponent] ...] // e.g., "library/ubuntu"
+ remoteName = pathComponent + anyTimes(`/`+pathComponent)
+ namePat = optional(domainAndPort+`/`) + remoteName
+
+ // anchoredNameRegexp is used to parse a name value, capturing the
+ // domain and trailing components.
+ anchoredNameRegexp = regexp.MustCompile(anchored(optional(capture(domainAndPort), `/`), capture(remoteName)))
+
+ referencePat = anchored(capture(namePat), optional(`:`, capture(tag)), optional(`@`, capture(digestPat)))
+
+ // anchoredIdentifierRegexp is used to check or match an
+ // identifier value, anchored at start and end of string.
+ anchoredIdentifierRegexp = regexp.MustCompile(anchored(identifier))
+)
+
+// optional wraps the expression in a non-capturing group and makes the
+// production optional.
+func optional(res ...string) string {
+ return `(?:` + strings.Join(res, "") + `)?`
+}
+
+// anyTimes wraps the expression in a non-capturing group that can occur
+// any number of times.
+func anyTimes(res ...string) string {
+ return `(?:` + strings.Join(res, "") + `)*`
+}
+
+// capture wraps the expression in a capturing group.
+func capture(res ...string) string {
+ return `(` + strings.Join(res, "") + `)`
+}
+
+// anchored anchors the regular expression by adding start and end delimiters.
+func anchored(res ...string) string {
+ return `^` + strings.Join(res, "") + `$`
+}
diff --git a/vendor/github.com/distribution/reference/sort.go b/vendor/github.com/distribution/reference/sort.go
new file mode 100644
index 0000000000..416c37b076
--- /dev/null
+++ b/vendor/github.com/distribution/reference/sort.go
@@ -0,0 +1,75 @@
+/*
+ Copyright The containerd Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package reference
+
+import (
+ "sort"
+)
+
+// Sort sorts string references preferring higher information references.
+//
+// The precedence is as follows:
+//
+// 1. [Named] + [Tagged] + [Digested] (e.g., "docker.io/library/busybox:latest@sha256:")
+// 2. [Named] + [Tagged] (e.g., "docker.io/library/busybox:latest")
+// 3. [Named] + [Digested] (e.g., "docker.io/library/busybo@sha256:")
+// 4. [Named] (e.g., "docker.io/library/busybox")
+// 5. [Digested] (e.g., "docker.io@sha256:")
+// 6. Parse error
+func Sort(references []string) []string {
+ var prefs []Reference
+ var bad []string
+
+ for _, ref := range references {
+ pref, err := ParseAnyReference(ref)
+ if err != nil {
+ bad = append(bad, ref)
+ } else {
+ prefs = append(prefs, pref)
+ }
+ }
+ sort.Slice(prefs, func(a, b int) bool {
+ ar := refRank(prefs[a])
+ br := refRank(prefs[b])
+ if ar == br {
+ return prefs[a].String() < prefs[b].String()
+ }
+ return ar < br
+ })
+ sort.Strings(bad)
+ var refs []string
+ for _, pref := range prefs {
+ refs = append(refs, pref.String())
+ }
+ return append(refs, bad...)
+}
+
+func refRank(ref Reference) uint8 {
+ if _, ok := ref.(Named); ok {
+ if _, ok = ref.(Tagged); ok {
+ if _, ok = ref.(Digested); ok {
+ return 1
+ }
+ return 2
+ }
+ if _, ok = ref.(Digested); ok {
+ return 3
+ }
+ return 4
+ }
+ return 5
+}
diff --git a/vendor/github.com/docker/docker/AUTHORS b/vendor/github.com/docker/docker/AUTHORS
new file mode 100644
index 0000000000..c7c649471c
--- /dev/null
+++ b/vendor/github.com/docker/docker/AUTHORS
@@ -0,0 +1,2496 @@
+# File @generated by hack/generate-authors.sh. DO NOT EDIT.
+# This file lists all contributors to the repository.
+# See hack/generate-authors.sh to make modifications.
+
+17neverends
+7sunarni <710720732@qq.com>
+Aanand Prasad
+Aarni Koskela
+Aaron Davidson
+Aaron Feng
+Aaron Hnatiw
+Aaron Huslage
+Aaron L. Xu
+Aaron Lehmann
+Aaron Welch
+Aaron Yoshitake
+Abdur Rehman
+Abel Muiño
+Abhijeet Kasurde
+Abhinandan Prativadi
+Abhinav Ajgaonkar
+Abhishek Chanda
+Abhishek Sharma
+Abin Shahab
+Abirdcfly
+Ada Mancini
+Adam Avilla
+Adam Dobrawy
+Adam Eijdenberg
+Adam Kunk
+Adam Lamers
+Adam Miller
+Adam Mills
+Adam Pointer
+Adam Simon
+Adam Singer
+Adam Thornton
+Adam Walz
+Adam Williams
+AdamKorcz
+Addam Hardy
+Aditi Rajagopal
+Aditya
+Adnan Khan
+Adolfo Ochagavía
+Adria Casas
+Adrian Moisey
+Adrian Mouat
+Adrian Oprea
+Adrien Folie
+Adrien Gallouët
+Ahmed Kamal
+Ahmet Alp Balkan
+Aidan Feldman
+Aidan Hobson Sayers
+AJ Bowen
+Ajey Charantimath
+ajneu
+Akash Gupta
+Akhil Mohan
+Akihiro Matsushima
+Akihiro Suda
+Akim Demaille
+Akira Koyasu
+Akshay Karle
+Akshay Moghe
+Al Tobey
+alambike
+Alan Hoyle
+Alan Scherger
+Alan Thompson
+Alano Terblanche
+Albert Callarisa
+Albert Zhang
+Albin Kerouanton
+Alec Benson
+Alejandro González Hevia
+Aleksa Sarai
+Aleksandr Chebotov
+Aleksandrs Fadins
+Alena Prokharchyk
+Alessandro Boch
+Alessio Biancalana
+Alex Chan
+Alex Chen
+Alex Coventry
+Alex Crawford
+Alex Ellis
+Alex Gaynor
+Alex Goodman
+Alex Nordlund
+Alex Olshansky
+Alex Samorukov
+Alex Stockinger
+Alex Warhawk
+Alexander Artemenko
+Alexander Boyd
+Alexander Larsson
+Alexander Midlash
+Alexander Morozov
+Alexander Polakov
+Alexander Shopov
+Alexandre Beslic
+Alexandre Garnier
+Alexandre González
+Alexandre Jomin
+Alexandru Sfirlogea
+Alexei Margasov
+Alexey Guskov
+Alexey Kotlyarov
+Alexey Shamrin
+Alexis Ries
+Alexis Thomas
+Alfred Landrum
+Ali Dehghani
+Alicia Lauerman
+Alihan Demir
+Allen Madsen
+Allen Sun
+almoehi
+Alvaro Saurin
+Alvin Deng
+Alvin Richards
+amangoel
+Amen Belayneh
+Ameya Gawde
+Amir Goldstein
+AmirBuddy
+Amit Bakshi
+Amit Krishnan
+Amit Shukla
+Amr Gawish
+Amy Lindburg
+Anand Patil
+AnandkumarPatel
+Anatoly Borodin
+Anca Iordache
+Anchal Agrawal
+Anda Xu
+Anders Janmyr
+Andre Dublin <81dublin@gmail.com>
+Andre Granovsky
+Andrea Denisse Gómez
+Andrea Luzzardi
+Andrea Turli
+Andreas Elvers
+Andreas Köhler
+Andreas Savvides
+Andreas Tiefenthaler
+Andrei Gherzan
+Andrei Ushakov
+Andrei Vagin
+Andrew Baxter <423qpsxzhh8k3h@s.rendaw.me>
+Andrew C. Bodine
+Andrew Clay Shafer
+Andrew Duckworth
+Andrew France
+Andrew Gerrand
+Andrew Guenther
+Andrew He
+Andrew Hsu
+Andrew Kim
+Andrew Kuklewicz
+Andrew Macgregor
+Andrew Macpherson
+Andrew Martin
+Andrew McDonnell
+Andrew Munsell
+Andrew Pennebaker
+Andrew Po
+Andrew Weiss
+Andrew Williams
+Andrews Medina
+Andrey Kolomentsev
+Andrey Petrov
+Andrey Stolbovsky
+André Martins
+Andrés Maldonado
+Andy Chambers
+andy diller
+Andy Goldstein
+Andy Kipp
+Andy Lindeman
+Andy Rothfusz
+Andy Smith
+Andy Wilson
+Andy Zhang
+Aneesh Kulkarni
+Anes Hasicic
+Angel Velazquez
+Anil Belur
+Anil Madhavapeddy
+Anirudh Aithal
+Ankit Jain
+Ankush Agarwal
+Anonmily
+Anran Qiao
+Anshul Pundir
+Anthon van der Neut
+Anthony Baire
+Anthony Bishopric
+Anthony Dahanne
+Anthony Sottile
+Anton Löfgren
+Anton Nikitin
+Anton Polonskiy
+Anton Tiurin
+Antonio Aguilar
+Antonio Murdaca
+Antonis Kalipetis
+Antony Messerli
+Anuj Bahuguna
+Anuj Varma
+Anusha Ragunathan
+Anyu Wang
+apocas
+Arash Deshmeh
+arcosx
+ArikaChen
+Arko Dasgupta
+Arnaud Lefebvre
+Arnaud Porterie
+Arnaud Rebillout
+Artem Khramov
+Arthur Barr
+Arthur Gautier
+Artur Meyster
+Arun Gupta
+Asad Saeeduddin
+Asbjørn Enge
+Ashly Mathew