diff --git a/.mockery.yml b/.mockery.yml index bffc890..569b57e 100644 --- a/.mockery.yml +++ b/.mockery.yml @@ -20,3 +20,7 @@ packages: structname: MockUploadsClient interfaces: UploadsClient: + CatalogClient: + config: + filename: catalog_client.go + structname: MockCatalogClient diff --git a/go.mod b/go.mod index 74b80b8..85739ee 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,7 @@ require ( github.com/gofrs/flock v0.13.0 github.com/lxc/incus-os/incus-osd v0.0.0-20260505023852-d32ba1f13f6f github.com/meigma/imgcli/schemas v0.0.0-20260505154605-5bbbe47a1e06 - github.com/meigma/imgsrv v0.0.0-20260505181350-0de592b46f88 + github.com/meigma/imgsrv v0.0.0-20260507005312-4a67655d031d github.com/spf13/cobra v1.10.2 github.com/spf13/pflag v1.0.10 github.com/spf13/viper v1.21.0 @@ -20,8 +20,14 @@ require ( require ( charm.land/lipgloss/v2 v2.0.1 // indirect cuelabs.dev/go/oci/ociregistry v0.0.0-20251212221603-3adeb8663819 // indirect + dario.cat/mergo v1.0.2 // indirect + github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect github.com/FuturFusion/migration-manager v0.6.9 // indirect github.com/FuturFusion/operations-center v0.5.8 // indirect + github.com/Microsoft/go-winio v0.6.2 // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/cenkalti/backoff/v4 v4.3.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/charmbracelet/ultraviolet v0.0.0-20251205161215-1948445e3318 // indirect github.com/charmbracelet/x/ansi v0.11.6 // indirect github.com/charmbracelet/x/term v0.2.2 // indirect @@ -30,36 +36,104 @@ require ( github.com/clipperhouse/displaywidth v0.11.0 // indirect github.com/clipperhouse/uax29/v2 v2.7.0 // indirect github.com/cockroachdb/apd/v3 v3.2.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.4 // indirect + github.com/cpuguy83/dockercfg v0.3.2 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/distribution/reference v0.6.0 // indirect + github.com/docker/go-connections v0.7.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.10.0 // indirect github.com/emicklei/proto v1.14.3 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fsnotify/fsnotify v1.10.0 // indirect + github.com/go-ini/ini v1.67.0 // indirect github.com/go-jose/go-jose/v4 v4.1.4 // indirect github.com/go-logfmt/logfmt v0.6.0 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-ole/go-ole v1.3.0 // indirect github.com/go-viper/mapstructure/v2 v2.5.0 // indirect github.com/google/uuid v1.6.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect + github.com/jackc/pgx/v5 v5.9.2 // indirect + github.com/jackc/puddle/v2 v2.2.2 // indirect + github.com/klauspost/compress v1.18.6 // indirect + github.com/klauspost/cpuid/v2 v2.3.0 // indirect + github.com/klauspost/crc32 v1.3.0 // indirect github.com/lucasb-eyer/go-colorful v1.4.0 // indirect + github.com/lufia/plan9stats v0.0.0-20260330125221-c963978e514e // indirect github.com/lxc/incus/v7 v7.0.0 // indirect + github.com/magiconair/properties v1.8.10 // indirect github.com/mattn/go-runewidth v0.0.23 // indirect + github.com/mfridman/interpolate v0.0.2 // indirect + github.com/minio/crc64nvme v1.1.1 // indirect + github.com/minio/md5-simd v1.1.2 // indirect + github.com/minio/minio-go/v7 v7.1.0 // indirect github.com/mitchellh/go-wordwrap v1.0.1 // indirect + github.com/moby/docker-image-spec v1.3.1 // indirect + github.com/moby/go-archive v0.2.0 // indirect + github.com/moby/moby/api v1.54.2 // indirect + github.com/moby/moby/client v0.4.1 // indirect + github.com/moby/patternmatcher v0.6.1 // 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.2 // indirect github.com/muesli/cancelreader v0.2.2 // indirect github.com/muhlemmer/gu v0.3.1 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.1 // indirect github.com/pelletier/go-toml/v2 v2.3.0 // indirect + github.com/philhofer/fwd v1.2.0 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect + github.com/pressly/goose/v3 v3.27.1 // indirect + github.com/prometheus/client_golang v1.23.2 // indirect + github.com/prometheus/client_model v0.6.2 // indirect + github.com/prometheus/common v0.67.5 // indirect + github.com/prometheus/otlptranslator v1.0.0 // indirect + github.com/prometheus/procfs v0.20.1 // indirect github.com/protocolbuffers/txtpbfmt v0.0.0-20260217160748-a481f6a22f94 // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/rogpeppe/go-internal v1.14.1 // indirect + github.com/rs/xid v1.6.0 // indirect github.com/sagikazarmark/locafero v0.12.0 // indirect + github.com/sethvargo/go-retry v0.3.0 // indirect + github.com/shirou/gopsutil/v4 v4.26.4 // indirect + github.com/sirupsen/logrus v1.9.4 // indirect github.com/spf13/afero v1.15.0 // indirect github.com/spf13/cast v1.10.0 // indirect github.com/stretchr/objx v0.5.3 // indirect github.com/subosito/gotenv v1.6.0 // indirect + github.com/testcontainers/testcontainers-go v0.42.0 // indirect + github.com/testcontainers/testcontainers-go/modules/postgres v0.42.0 // indirect + github.com/tinylib/msgp v1.6.1 // indirect + github.com/tklauser/go-sysconf v0.3.16 // indirect + github.com/tklauser/numcpus v0.11.0 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect + github.com/yusufpapurcu/wmi v1.2.4 // indirect + github.com/zeebo/xxh3 v1.1.0 // indirect github.com/zitadel/oidc/v3 v3.47.5 // indirect github.com/zitadel/schema v1.3.2 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 // indirect + go.opentelemetry.io/otel v1.43.0 // indirect + go.opentelemetry.io/otel/exporters/prometheus v0.65.0 // indirect + go.opentelemetry.io/otel/metric v1.43.0 // indirect + go.opentelemetry.io/otel/sdk v1.43.0 // indirect + go.opentelemetry.io/otel/sdk/metric v1.43.0 // indirect + go.opentelemetry.io/otel/trace v1.43.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.yaml.in/yaml/v2 v2.4.4 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect + golang.org/x/crypto v0.50.0 // indirect golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f // indirect golang.org/x/net v0.53.0 // indirect golang.org/x/oauth2 v0.36.0 // indirect diff --git a/go.sum b/go.sum index d6ae3a0..941dba4 100644 --- a/go.sum +++ b/go.sum @@ -6,10 +6,24 @@ cuelabs.dev/go/oci/ociregistry v0.0.0-20251212221603-3adeb8663819 h1:Zh+Ur3OsoWp cuelabs.dev/go/oci/ociregistry v0.0.0-20251212221603-3adeb8663819/go.mod h1:WjmQxb+W6nVNCgj8nXrF24lIz95AHwnSl36tpjDZSU8= cuelang.org/go v0.16.1 h1:iPN1lHZd2J0hjcr8hfq9PnIGk7VfPkKFfxH4de+m9sE= cuelang.org/go v0.16.1/go.mod h1:/aW3967FeWC5Hc1cDrN4Z4ICVApdMi83wO5L3uF/1hM= +dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= +dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= +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/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg= +github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/FuturFusion/migration-manager v0.6.9 h1:qf/EtxaasNhSNgM2yv6R53h4yJbA7Gd/Pjlvc3s642A= github.com/FuturFusion/migration-manager v0.6.9/go.mod h1:Si7dBSilhF7FIbaYMG+V804C0ESIm7t6pF2x8es3IOk= github.com/FuturFusion/operations-center v0.5.8 h1:Wf5B3ULMzSqju1u7wVgZbyJWHGd/QYOjNygr//hvPto= github.com/FuturFusion/operations-center v0.5.8/go.mod h1:jmh2YszsUl3Mi5pyZnzndZZykImGJEUgtK+dzAiFFos= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +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/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/charmbracelet/colorprofile v0.4.2 h1:BdSNuMjRbotnxHSfxy+PCSa4xAmz7szw70ktAtWRYrY= github.com/charmbracelet/colorprofile v0.4.2/go.mod h1:0rTi81QpwDElInthtrQ6Ni7cG0sDtwAd4C4le060fT8= github.com/charmbracelet/ultraviolet v0.0.0-20251205161215-1948445e3318 h1:OqDqxQZliC7C8adA7KjelW3OjtAxREfeHkNcd66wpeI= @@ -28,19 +42,54 @@ github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJ github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= github.com/cockroachdb/apd/v3 v3.2.1 h1:U+8j7t0axsIgvQUqthuNm82HIrYXodOV2iWLWtEaIwg= github.com/cockroachdb/apd/v3 v3.2.1/go.mod h1:klXJcjp+FffLTHlhIG69tezTDvdP065naDsHzKhYSqc= +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.4 h1:M42JrUT4zfZTqtkUwkr0GzmUWbfyO5VO0Q5b3op97T4= +github.com/containerd/platforms v1.0.0-rc.4/go.mod h1:lKlMXyLybmBedS/JJm11uDofzI8L2v0J2ZbYvNsbq1A= +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.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s= +github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +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/docker/go-connections v0.7.0 h1:6SsRfJddP22WMrCkj19x9WKjEDTB+ahsdiGYf0mN39c= +github.com/docker/go-connections v0.7.0/go.mod h1:no1qkHdjq7kLMGUXYAduOhYPSJxxvgWBh7ogVvptn3Q= +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/ebitengine/purego v0.10.0 h1:QIw4xfpWT6GWTzaW5XEKy3HXoqrJGx1ijYHzTF0/ISU= +github.com/ebitengine/purego v0.10.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= github.com/emicklei/proto v1.14.3 h1:zEhlzNkpP8kN6utonKMzlPfIvy82t5Kb9mufaJxSe1Q= github.com/emicklei/proto v1.14.3/go.mod h1:rn1FgRS/FANiZdD2djyH7TMA9jdRDcYQ9IEN9yvjX0A= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.10.0 h1:Xx/5Ydg9CeBDX/wi4VJqStNtohYjitZhhlHt4h3St1M= github.com/fsnotify/fsnotify v1.10.0/go.mod h1:TLheqan6HD6GBK6PrDWyDPBaEV8LspOxvPSjC+bVfgo= +github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A= +github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA= github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4= github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= +github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI= github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow= github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro= @@ -53,49 +102,129 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.9.2 h1:3ZhOzMWnR4yJ+RW1XImIPsD1aNSz4T4fyP7zlQb56hw= +github.com/jackc/pgx/v5 v5.9.2/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4= +github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= +github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/klauspost/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao= +github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= +github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= +github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/klauspost/crc32 v1.3.0 h1:sSmTt3gUt81RP655XGZPElI0PelVTZ6YwCRnPSupoFM= +github.com/klauspost/crc32 v1.3.0/go.mod h1:D7kQaZhnkX/Y0tstFGf8VUzv2UofNGqCjnC3zdHB0Hw= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= -github.com/lib/pq v1.10.7 h1:p7ZhMD+KsSRozJr34udlUrhboJwWAgCg34+/ZZNvZZw= -github.com/lib/pq v1.10.7/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lucasb-eyer/go-colorful v1.4.0 h1:UtrWVfLdarDgc44HcS7pYloGHJUjHV/4FwW4TvVgFr4= github.com/lucasb-eyer/go-colorful v1.4.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/lufia/plan9stats v0.0.0-20260330125221-c963978e514e h1:Q6MvJtQK/iRcRtzAscm/zF23XxJlbECiGPyRicsX+Ak= +github.com/lufia/plan9stats v0.0.0-20260330125221-c963978e514e/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg= github.com/lxc/incus-os/incus-osd v0.0.0-20260505023852-d32ba1f13f6f h1:2wNTDCa/sf8xUOjfDHglPUpj5W4UncvuGKH3w6w/QSU= github.com/lxc/incus-os/incus-osd v0.0.0-20260505023852-d32ba1f13f6f/go.mod h1:0/gjLA2CMoYq0N93elIlQ6+tBMwtBtE8Rr2BWgQRB/c= github.com/lxc/incus/v7 v7.0.0 h1:xLz1Q1Xk+yCNL148MFBOSWWrzJVOS1N6PcS0zd8usSc= github.com/lxc/incus/v7 v7.0.0/go.mod h1:Dxu4id/fVr+OmFPQt9tU3fu4E8LhW89NeFxCtjPLCdo= +github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE= +github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4= +github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4= github.com/mattn/go-runewidth v0.0.23 h1:7ykA0T0jkPpzSvMS5i9uoNn2Xy3R383f9HDx3RybWcw= github.com/mattn/go-runewidth v0.0.23/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= +github.com/mdelapenya/tlscert v0.2.0 h1:7H81W6Z/4weDvZBNOfQte5GpIMo0lGYEeWbkGp5LJHI= +github.com/mdelapenya/tlscert v0.2.0/go.mod h1:O4njj3ELLnJjGdkN7M/vIVCpZ+Cf0L6muqOG4tLSl8o= github.com/meigma/imgcli/schemas v0.0.0-20260505154605-5bbbe47a1e06 h1:v/8R2UInUH6gsz099SZQwPapXkFRh+Q5p3fSplImoTw= github.com/meigma/imgcli/schemas v0.0.0-20260505154605-5bbbe47a1e06/go.mod h1:vgdSiTx7yikg0x4QmozD0dh4AYM90ZVALn0k45amZuQ= -github.com/meigma/imgsrv v0.0.0-20260505181350-0de592b46f88 h1:ebNLYGiyABFvk7LS43DkkSonxcolqqPqkAjSMaRqodM= -github.com/meigma/imgsrv v0.0.0-20260505181350-0de592b46f88/go.mod h1:aRf/9hRpxhb53jgUmZdo7XVOqQkEn2FEzVqmGEZtx3I= +github.com/meigma/imgsrv v0.0.0-20260507005312-4a67655d031d h1:YDvuNrpeEj810COQoVUaY3puq3vCC0IKSC9WuLJvy58= +github.com/meigma/imgsrv v0.0.0-20260507005312-4a67655d031d/go.mod h1:aRf/9hRpxhb53jgUmZdo7XVOqQkEn2FEzVqmGEZtx3I= +github.com/mfridman/interpolate v0.0.2 h1:pnuTK7MQIxxFz1Gr+rjSIx9u7qVjf5VOoM/u6BbAxPY= +github.com/mfridman/interpolate v0.0.2/go.mod h1:p+7uk6oE07mpE/Ik1b8EckO0O4ZXiGAfshKBWLUM9Xg= +github.com/minio/crc64nvme v1.1.1 h1:8dwx/Pz49suywbO+auHCBpCtlW1OfpcLN7wYgVR6wAI= +github.com/minio/crc64nvme v1.1.1/go.mod h1:eVfm2fAzLlxMdUGc0EEBGSMmPwmXD5XiNRpnu9J3bvg= +github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34= +github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM= +github.com/minio/minio-go/v7 v7.1.0 h1:QEt5IStDpxgGjEdtOgpiZ5QhmSl3ax7qy61vi2SwHO8= +github.com/minio/minio-go/v7 v7.1.0/go.mod h1:Dm7WS1AgLmBa0NcQD6SeJnJf+K/EUW3GR7Ks6olB3OA= github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= +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.2.0 h1:zg5QDUM2mi0JIM9fdQZWC7U8+2ZfixfTYoHL7rWUcP8= +github.com/moby/go-archive v0.2.0/go.mod h1:mNeivT14o8xU+5q1YnNrkQVpK+dnNe/K6fHqnTg4qPU= +github.com/moby/moby/api v1.54.2 h1:wiat9QAhnDQjA7wk1kh/TqHz2I1uUA7M7t9SAl/JNXg= +github.com/moby/moby/api v1.54.2/go.mod h1:+RQ6wluLwtYaTd1WnPLykIDPekkuyD/ROWQClE83pzs= +github.com/moby/moby/client v0.4.1 h1:DMQgisVoMkmMs7fp3ROSdiBnoAu8+vo3GggFl06M/wY= +github.com/moby/moby/client v0.4.1/go.mod h1:z52C9O2POPOsnxZAy//WtKcQ32P+jT/NGeXu/7nfjGQ= +github.com/moby/patternmatcher v0.6.1 h1:qlhtafmr6kgMIJjKJMDmMWq7WLkKIo23hsrpR3x084U= +github.com/moby/patternmatcher v0.6.1/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= +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.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ= +github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc= github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= github.com/muhlemmer/gu v0.3.1 h1:7EAqmFrW7n3hETvuAdmFmn4hS8W+z3LgKtrnow+YzNM= github.com/muhlemmer/gu v0.3.1/go.mod h1:YHtHR+gxM+bKEIIs7Hmi9sPT3ZDUvTN/i88wQpZkrdM= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w= +github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= 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/pelletier/go-toml/v2 v2.3.0 h1:k59bC/lIZREW0/iVaQR8nDHxVq8OVlIzYCOJf421CaM= github.com/pelletier/go-toml/v2 v2.3.0/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM= +github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 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/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU= +github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= +github.com/pressly/goose/v3 v3.27.1 h1:6uEvcprBybDmW4hcz3gYujhARhye+GoWKhEWyzD5sh4= +github.com/pressly/goose/v3 v3.27.1/go.mod h1:maruOxsPnIG2yHHyo8UqKWXYKFcH7Q76csUV7+7KYoM= +github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= +github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= +github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4= +github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw= +github.com/prometheus/otlptranslator v1.0.0 h1:s0LJW/iN9dkIH+EnhiD3BlkkP5QVIUVEoIwkU+A6qos= +github.com/prometheus/otlptranslator v1.0.0/go.mod h1:vRYWnXvI6aWGpsdY/mOT/cbeVRBlPWtBNDb7kGR3uKM= +github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEycfc= +github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo= github.com/protocolbuffers/txtpbfmt v0.0.0-20260217160748-a481f6a22f94 h1:2PC6Ql3jipz1KvBlqUHjjk6v4aMwE86mfDu1XMH0LR8= github.com/protocolbuffers/txtpbfmt v0.0.0-20260217160748-a481f6a22f94/go.mod h1:JSbkp0BviKovYYt9XunS95M3mLPibE9bGg+Y95DsEEY= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU= +github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sagikazarmark/locafero v0.12.0 h1:/NQhBAkUb4+fH1jivKHWusDYFjMOOKU88eegjfxfHb4= github.com/sagikazarmark/locafero v0.12.0/go.mod h1:sZh36u/YSZ918v0Io+U9ogLYQJ9tLLBmM4eneO6WwsI= +github.com/sethvargo/go-retry v0.3.0 h1:EEt31A35QhrcRZtrYFDTBg91cqZVnFL2navjDrah2SE= +github.com/sethvargo/go-retry v0.3.0/go.mod h1:mNX17F0C/HguQMyMyJxcnU471gOZGxCLyYaFyAZraas= +github.com/shirou/gopsutil/v4 v4.26.4 h1:B4SXVbcwTyrocPHEmWBC4uCYr4Xcu3MK1TXqbprAOWY= +github.com/shirou/gopsutil/v4 v4.26.4/go.mod h1:LZ6ewCSkBqUpvSOf+LsTGnRinC6iaNUNMGBtDkJBaLQ= +github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w= +github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg= github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= @@ -107,22 +236,65 @@ github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU= github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4= github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +github.com/testcontainers/testcontainers-go v0.42.0 h1:He3IhTzTZOygSXLJPMX7n44XtK+qhjat1nI9cneBbUY= +github.com/testcontainers/testcontainers-go v0.42.0/go.mod h1:vZjdY1YmUA1qEForxOIOazfsrdyORJAbhi0bp8plN30= +github.com/testcontainers/testcontainers-go/modules/postgres v0.42.0 h1:GCbb1ndrF7OTDiIvxXyItaDab4qkzTFJ48LKFdM7EIo= +github.com/testcontainers/testcontainers-go/modules/postgres v0.42.0/go.mod h1:IRPBaI8jXdrNfD0e4Zm7Fbcgaz5shKxOQv4axiL09xs= +github.com/tinylib/msgp v1.6.1 h1:ESRv8eL3u+DNHUoSAAQRE50Hm162zqAnBoGv9PzScPY= +github.com/tinylib/msgp v1.6.1/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA= +github.com/tklauser/go-sysconf v0.3.16 h1:frioLaCQSsF5Cy1jgRBrzr6t502KIIwQ0MArYICU0nA= +github.com/tklauser/go-sysconf v0.3.16/go.mod h1:/qNL9xxDhc7tx3HSRsLWNnuzbVfh3e7gh/BmM179nYI= +github.com/tklauser/numcpus v0.11.0 h1:nSTwhKH5e1dMNsCdVBukSZrURJRoHbSEQjdEbY+9RXw= +github.com/tklauser/numcpus v0.11.0/go.mod h1:z+LwcLq54uWZTX0u/bGobaV34u6V7KNlTZejzM6/3MQ= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= +github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= +github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ= +github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= +github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs= +github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s= github.com/zitadel/oidc/v3 v3.47.5 h1:cR2z0oqa5XZkwpXQiPCUGqKtndrjHgEXb81y3oXocK4= github.com/zitadel/oidc/v3 v3.47.5/go.mod h1:XxFh0666HRXycyrKmono+3gY0RACpYJLgy4r/+kliKY= github.com/zitadel/schema v1.3.2 h1:gfJvt7dOMfTmxzhscZ9KkapKo3Nei3B6cAxjav+lyjI= github.com/zitadel/schema v1.3.2/go.mod h1:IZmdfF9Wu62Zu6tJJTH3UsArevs3Y4smfJIj3L8fzxw= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 h1:CqXxU8VOmDefoh0+ztfGaymYbhdB/tT3zs79QaZTNGY= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0/go.mod h1:BuhAPThV8PBHBvg8ZzZ/Ok3idOdhWIodywz2xEcRbJo= +go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= +go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0= +go.opentelemetry.io/otel/exporters/prometheus v0.65.0 h1:jOveH/b4lU9HT7y+Gfamf18BqlOuz2PWEvs8yM7Q6XE= +go.opentelemetry.io/otel/exporters/prometheus v0.65.0/go.mod h1:i1P8pcumauPtUI4YNopea1dhzEMuEqWP1xoUZDylLHo= +go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM= +go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY= +go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg= +go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg= +go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw= +go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A= +go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A= +go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ= +go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= go.yaml.in/yaml/v4 v4.0.0-rc.4 h1:UP4+v6fFrBIb1l934bDl//mmnoIZEDK0idg1+AIvX5U= go.yaml.in/yaml/v4 v4.0.0-rc.4/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0= +golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI= +golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q= golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f h1:W3F4c+6OLc6H2lb//N1q4WpJkhzJCK5J6kUi1NTVXfM= golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f/go.mod h1:J1xhfL/vlindoeF/aINzNzt2Bket5bjo9sdOYzOsU80= golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM= @@ -133,8 +305,14 @@ golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY= +golang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY= golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg= golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164= golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c= @@ -144,5 +322,18 @@ google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= +gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= +modernc.org/libc v1.72.1 h1:db1xwJ6u1kE3KHTFTTbe2GCrczHPKzlURP0aDC4NGD0= +modernc.org/libc v1.72.1/go.mod h1:HRMiC/PhPGLIPM7GzAFCbI+oSgE3dhZ8FWftmRrHVlY= +modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= +modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= +modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= +modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= +modernc.org/sqlite v1.49.1 h1:dYGHTKcX1sJ+EQDnUzvz4TJ5GbuvhNJa8Fg6ElGx73U= +modernc.org/sqlite v1.49.1/go.mod h1:m0w8xhwYUVY3H6pSDwc3gkJ/irZT/0YEXwBlhaxQEew= +pgregory.net/rapid v1.2.0 h1:keKAYRcjm+e1F0oAuU5F5+YPAWcyxNNRK2wud503Gnk= +pgregory.net/rapid v1.2.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04= diff --git a/go.work.sum b/go.work.sum index fec80be..d1fc3d0 100644 --- a/go.work.sum +++ b/go.work.sum @@ -1,5 +1,6 @@ cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4= cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= +dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc= github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= @@ -31,6 +32,7 @@ github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.23/go.mod h1:M8l3mw github.com/aws/aws-sdk-go-v2/service/s3 v1.100.1/go.mod h1:L2dcoOgS2VSgbPLvpak2NyUPsO1TBN7M45Z4H7DlRc4= github.com/aws/smithy-go v1.25.1/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= github.com/aymanbagabas/go-udiff v0.4.1/go.mod h1:0L9PGwj20lrtmEMeyw4WKJ/TMyDtvAoK9bf2u/mNo3w= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bits-and-blooms/bitset v1.24.4/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= @@ -39,36 +41,49 @@ github.com/breml/newline-after-block v0.0.0-20251225141726-84337171eef7/go.mod h github.com/brianvoe/gofakeit/v7 v7.14.0/go.mod h1:QXuPeBw164PJCzCUZVmgpgHJ3Llj49jSLVkKPMtxtxA= github.com/brunoga/deep v1.3.1/go.mod h1:GDV6dnXqn80ezsLSZ5Wlv1PdKAWAO4L5PnKYtv2dgaI= github.com/cavaliergopher/cpio v1.0.1/go.mod h1:pBdaqQjnvXxdS/6CvNDwIANIFSP0xRKI16PX4xejRQc= +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.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= github.com/cenkalti/hub v1.0.2/go.mod h1:8LAFAZcCasb83vfxatMUnZHRoQcffho2ELpHb+kaTJU= github.com/cenkalti/rpc2 v1.0.5/go.mod h1:2yfU5b86vOr16+iY1jN3MvT6Kxc9Nf8j5iZWwUf7iaw= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f/go.mod h1:IfZAMTHB6XkZSeXUqriemErjAWCCzT0LwjKFYCZyw0I= github.com/checkpoint-restore/go-criu/v8 v8.2.0/go.mod h1:HVKJ1dK+bowJcFI1MtdL2ECIuY+/AtRMHzD9Lqa4uA4= github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA= github.com/clipperhouse/uax29/v2 v2.2.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= +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.4 h1:M42JrUT4zfZTqtkUwkr0GzmUWbfyO5VO0Q5b3op97T4= github.com/containerd/platforms v1.0.0-rc.4/go.mod h1:lKlMXyLybmBedS/JJm11uDofzI8L2v0J2ZbYvNsbq1A= github.com/cowsql/go-cowsql v1.22.0/go.mod h1:+QzPcM7QRPIBI8XhsKJ47iUtxGY53lsYGX51G1WQ/4s= +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.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/cyphar/filepath-securejoin v0.6.1/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc= github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= github.com/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e/go.mod h1:YTIHhz/QFSYnu/EhlF2SpU2Uk+32abacUYA5ZPljz1A= +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/docker/docker v28.5.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE= +github.com/docker/go-connections v0.7.0 h1:6SsRfJddP22WMrCkj19x9WKjEDTB+ahsdiGYf0mN39c= +github.com/docker/go-connections v0.7.0/go.mod h1:no1qkHdjq7kLMGUXYAduOhYPSJxxvgWBh7ogVvptn3Q= +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/donovanhide/eventsource v0.0.0-20210830082556-c59027999da0/go.mod h1:56wL82FO0bfMU5RvfXoIwSOP2ggqqxT+tAfNEIyxuHw= github.com/dsnet/golib/memfile v1.0.0/go.mod h1:tXGNW9q3RwvWt1VV2qrRKlSSz0npnh12yftCSCy2T64= +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/dustinkirkland/golang-petname v0.0.0-20260215035315-f0c533e9ce9b/go.mod h1:8AuBTZBRSFqEYBPYULd+NN474/zZBLP+6WeT5S9xlAc= github.com/eapache/channels v1.1.0/go.mod h1:jMm2qB5Ubtg9zLd+inMZd2/NUvXgzmWXsDaLyQIGfH0= github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= +github.com/ebitengine/purego v0.10.0 h1:QIw4xfpWT6GWTzaW5XEKy3HXoqrJGx1ijYHzTF0/ISU= github.com/ebitengine/purego v0.10.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= github.com/envoyproxy/protoc-gen-validate v1.3.0/go.mod h1:HvYl7zwPa5mffgyeTUHA9zHIH36nmrm7oCbo4YKoSWA= @@ -77,6 +92,7 @@ github.com/fabien-marty/slog-helpers v0.0.0-20240624063600-773d61849b89/go.mod h github.com/fabien-marty/tracerr v0.0.0-20240624051446-7f090eca46ee/go.mod h1:eqKGnFoVPY3Ng/iRRYfMxOum60YwQh7ZzYILKbYI7UY= github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/flosch/pongo2/v4 v4.0.2/go.mod h1:B5ObFANs/36VwxxlgKpdchIJHMvHB562PW+BWPhwZD8= github.com/flosch/pongo2/v6 v6.0.0/go.mod h1:CuDpFm47R0uGGE7z13/tTlt1Y6zdxvr2RLT5LJhsHEU= @@ -89,9 +105,13 @@ github.com/gdamore/encoding v1.0.1/go.mod h1:0Z0cMFinngz9kS1QfMjCP8TY7em3bZYeekl github.com/gdamore/tcell/v2 v2.13.9/go.mod h1:+Wfe208WDdB7INEtCsNrAN6O2m+wsTPk1RAovjaILlo= github.com/go-chi/chi/v5 v5.2.5/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0= github.com/go-co-op/gocron/v2 v2.21.1/go.mod h1:5lEiCKk1oVJV39Zg7/YG10OnaVrDAV5GGR6O0663k6U= +github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A= +github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= github.com/go-jose/go-jose/v3 v3.0.4/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ= github.com/go-json-experiment/json v0.0.0-20250813024750-ebf49471dced/go.mod h1:TiCD2a1pcmjd7YnhGH0f/zKNcCD06B029pHhzV23c2M= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= @@ -131,6 +151,14 @@ github.com/hexdigest/gowrap v1.4.1/go.mod h1:s+1hE6qakgdaaLqgdwPAj5qKYVBCSbPJhEb github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= github.com/insomniacslk/dhcp v0.0.0-20260407060928-11b94ed970f2/go.mod h1:qfvBmyDNp+/liLEYWRvqny/PEz9hGe2Dz833eXILSmo= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.9.2 h1:3ZhOzMWnR4yJ+RW1XImIPsD1aNSz4T4fyP7zlQb56hw= +github.com/jackc/pgx/v5 v5.9.2/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4= +github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= +github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/jaypipes/pcidb v1.1.1/go.mod h1:x27LT2krrUgjf875KxQXKB0Ha/YXLdZRVmw6hH0G7g8= github.com/jedib0t/go-pretty/v6 v6.7.8/go.mod h1:YwC5CE4fJ1HFUDeivSV1r//AmANFHyqczZk+U6BDALU= github.com/jeremija/gosubmit v0.2.8/go.mod h1:Ui+HS073lCFREXBbdfrJzMB57OI/bdxTiLtrDHHhFPI= @@ -142,8 +170,12 @@ github.com/k-sone/critbitgo v1.4.0/go.mod h1:7E6pyoyADnFxlUBEKcnfS49b7SUAQGMK+OA github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213/go.mod h1:vNUNkEQ1e29fT/6vq2aBdFsgNPmy8qMdSay1npru+Sw= github.com/karlseguin/ccache/v3 v3.0.6/go.mod h1:b0qfdUOHl4vJgKFQN41paXIdBb3acAtyX2uWrBAZs1w= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= +github.com/klauspost/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao= github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= +github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/klauspost/crc32 v1.3.0 h1:sSmTt3gUt81RP655XGZPElI0PelVTZ6YwCRnPSupoFM= +github.com/klauspost/crc32 v1.3.0/go.mod h1:D7kQaZhnkX/Y0tstFGf8VUzv2UofNGqCjnC3zdHB0Hw= github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs= github.com/knadh/koanf/maps v0.1.2/go.mod h1:npD/QZY3V6ghQDdcQzl1W4ICNVTkohC8E73eI2xW4yI= github.com/knadh/koanf/parsers/yaml v1.1.0/go.mod h1:HHmcHXUrp9cOPcuC+2wrr44GTUB0EC+PyfN3HZD9tFg= @@ -162,6 +194,7 @@ github.com/lufia/plan9stats v0.0.0-20260330125221-c963978e514e/go.mod h1:autxFIv github.com/lxc/distrobuilder/v3 v3.3.2-0.20260410004922-5828176a7f61/go.mod h1:ZcIe6WsvX5vt7cdzu7a5tqRi8v6DX1+9+EmsREbW7L4= github.com/lxc/go-lxc v0.0.0-20260316180011-3af4ce000ed7/go.mod h1:3UTWXVcHfgxE7JM4ZUnsy6bDA8L1vuzwJbJRF6dlB90= github.com/lxc/incus/v6 v6.23.1-0.20260327174201-6acde8bd711a/go.mod h1:efEbxmSexfg8VyYQnBgNQz0dZZLci3s90xcU+VXoCYc= +github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE= github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/maniartech/signals v1.3.1/go.mod h1:AbE8Yy9ZjKCWNU/VhQ+0Ea9KOaTWHp6aOfdLBe5m1iM= github.com/matryer/moq v0.4.0/go.mod h1:kUfalaLk7TcyXhrhonBYQ2Ewun63+/xGbZ7/MzzzC4Y= @@ -176,17 +209,36 @@ github.com/mdlayher/netx v0.0.0-20230430222610-7e21880baee8/go.mod h1:qhZhwMDNWw github.com/mdlayher/packet v1.1.2/go.mod h1:GEu1+n9sG5VtiRE4SydOmX5GTwyyYlteZiFU+x0kew4= github.com/mdlayher/socket v0.6.0/go.mod h1:q7vozUAnxSqnjHc12Fik5yUKIzfZ8ITCfMkhOtE9z18= github.com/mdlayher/vsock v1.2.1/go.mod h1:NRfCibel++DgeMD8z/hP+PPTjlNJsdPOmxcnENvE+SE= +github.com/mfridman/interpolate v0.0.2 h1:pnuTK7MQIxxFz1Gr+rjSIx9u7qVjf5VOoM/u6BbAxPY= +github.com/mfridman/interpolate v0.0.2/go.mod h1:p+7uk6oE07mpE/Ik1b8EckO0O4ZXiGAfshKBWLUM9Xg= github.com/miekg/dns v1.1.72/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs= +github.com/minio/crc64nvme v1.1.1 h1:8dwx/Pz49suywbO+auHCBpCtlW1OfpcLN7wYgVR6wAI= +github.com/minio/crc64nvme v1.1.1/go.mod h1:eVfm2fAzLlxMdUGc0EEBGSMmPwmXD5XiNRpnu9J3bvg= +github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34= +github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM= +github.com/minio/minio-go/v7 v7.1.0 h1:QEt5IStDpxgGjEdtOgpiZ5QhmSl3ax7qy61vi2SwHO8= +github.com/minio/minio-go/v7 v7.1.0/go.mod h1:Dm7WS1AgLmBa0NcQD6SeJnJf+K/EUW3GR7Ks6olB3OA= github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw= github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +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.2.0 h1:zg5QDUM2mi0JIM9fdQZWC7U8+2ZfixfTYoHL7rWUcP8= github.com/moby/go-archive v0.2.0/go.mod h1:mNeivT14o8xU+5q1YnNrkQVpK+dnNe/K6fHqnTg4qPU= +github.com/moby/moby/api v1.54.2 h1:wiat9QAhnDQjA7wk1kh/TqHz2I1uUA7M7t9SAl/JNXg= +github.com/moby/moby/api v1.54.2/go.mod h1:+RQ6wluLwtYaTd1WnPLykIDPekkuyD/ROWQClE83pzs= +github.com/moby/moby/client v0.4.1 h1:DMQgisVoMkmMs7fp3ROSdiBnoAu8+vo3GggFl06M/wY= +github.com/moby/moby/client v0.4.1/go.mod h1:z52C9O2POPOsnxZAy//WtKcQ32P+jT/NGeXu/7nfjGQ= github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= +github.com/moby/patternmatcher v0.6.1 h1:qlhtafmr6kgMIJjKJMDmMWq7WLkKIo23hsrpR3x084U= +github.com/moby/patternmatcher v0.6.1/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= +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/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28= +github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ= github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc= github.com/morikuni/aec v1.1.0/go.mod h1:xDRgiq/iw5l+zkao76YTKzKttOp2cwPEne25HDkJnBw= github.com/muesli/crunchy v0.4.1-0.20210519044311-9cd68953298f/go.mod h1:9k4x6xdSbb7WwtAVy0iDjaiDjIk6Wa5AgUIqp+HqOpU= @@ -195,6 +247,7 @@ github.com/muesli/mango-cobra v1.3.0/go.mod h1:Cj1ZrBu3806Qw7UjxnAUgE+7tllUBj1NC github.com/muesli/mango-pflag v0.2.0/go.mod h1:X9LT1p/pbGA1wjvEbtwnixujKErkP0jVmrxwrw3fL0Y= github.com/muesli/roff v0.1.0/go.mod h1:pjAHQM9hdUUwm/krAfrLGgJkXJ+YuhtsfZ42kieB2Ig= github.com/muhlemmer/httpforwarded v0.1.0/go.mod h1:yo9czKedo2pdZhoXe+yDkGVbU0TJ0q9oQ90BVoDEtw0= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/natefinch/wrap v0.2.0/go.mod h1:6gMHlAl12DwYEfKP3TkuykYUfLSEAvHw67itm4/KAS8= github.com/nwidger/jsoncolor v0.3.2/go.mod h1:Cs34umxLbJvgBMnVNVqhji9BhoT/N/KinHqZptQ7cf4= @@ -214,6 +267,8 @@ github.com/openfga/openfga v1.11.5/go.mod h1:dqouURLTeDwoHxpiVHp/z9Ak4sfoVJ+24YE github.com/orcaman/concurrent-map/v2 v2.0.1/go.mod h1:9Eq3TG2oBe5FirmYWQfYO5iH1q0Jv47PLaNK++uCdOM= github.com/osrg/gobgp/v4 v4.5.0/go.mod h1:pgu8waqTvZUYl4eQuPrKNOaVwhHv7Zt9YymuzCaX7f8= github.com/ovn-kubernetes/libovsdb v0.8.1/go.mod h1:ZlnHLzagmLOSvyd9qfxBIZp6wOSOw0IsRsc+6lNUGbU= +github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM= +github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM= github.com/pierrec/lz4/v4 v4.1.26/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4= github.com/pires/go-proxyproto v0.12.0/go.mod h1:qUvfqUMEoX7T8g0q7TQLDnhMjdTrxnG0hvpMn+7ePNI= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= @@ -221,9 +276,16 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE github.com/pkg/sftp v1.13.10/go.mod h1:bJ1a7uDhrX/4OII+agvy28lzRvQrmIQuaHrcI1HbeGA= github.com/pkg/xattr v0.4.12/go.mod h1:di8WF84zAKk8jzR1UBTEWh9AUlIZZ7M/JNt8e9B6ktU= github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= +github.com/pressly/goose/v3 v3.27.1 h1:6uEvcprBybDmW4hcz3gYujhARhye+GoWKhEWyzD5sh4= +github.com/pressly/goose/v3 v3.27.1/go.mod h1:maruOxsPnIG2yHHyo8UqKWXYKFcH7Q76csUV7+7KYoM= +github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= +github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4= github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw= +github.com/prometheus/otlptranslator v1.0.0 h1:s0LJW/iN9dkIH+EnhiD3BlkkP5QVIUVEoIwkU+A6qos= +github.com/prometheus/otlptranslator v1.0.0/go.mod h1:vRYWnXvI6aWGpsdY/mOT/cbeVRBlPWtBNDb7kGR3uKM= github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo= github.com/quasilyte/go-ruleguard/dsl v0.3.23/go.mod h1:KeCP03KrjuSO0H1kTuZQCWlQPulDV6YMIXmpQss17rU= github.com/rivo/tview v0.42.0/go.mod h1:cSfIYfhpSGCjp3r/ECJb+GKS7cGJnqV8vfjQPwoXyfY= @@ -231,26 +293,39 @@ github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzG github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rootless-containers/proto/go-proto v0.0.0-20260207013450-f6ee952d53d9/go.mod h1:LLjEAc6zmycfeN7/1fxIphWQPjHpTt7ElqT7eVf8e4A= github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= +github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU= +github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ= github.com/rung/go-safecast v1.0.1/go.mod h1:dzUcUS2UMtbfVc7w6mx/Ur3UYcpXEZC+WilISksJ4P8= github.com/russross/blackfriday v1.6.0/go.mod h1:ti0ldHuxg49ri4ksnFxlkCfN+hvslNlmVHqNRXXJNAY= github.com/santhosh-tekuri/jsonschema/v5 v5.3.1/go.mod h1:uToXkOrWAZ6/Oc07xWQrPOhJotwFIyu2bBVN41fcDUY= github.com/schollz/progressbar/v3 v3.19.0/go.mod h1:IsO3lpbaGuzh8zIMzgY3+J8l4C8GjO0Y9S69eFvNsec= github.com/segmentio/fasthash v1.0.3/go.mod h1:waKX8l2N8yckOgmSsXJi7x1ZfdKZ4x7KRMzBtS3oedY= +github.com/sethvargo/go-retry v0.3.0 h1:EEt31A35QhrcRZtrYFDTBg91cqZVnFL2navjDrah2SE= +github.com/sethvargo/go-retry v0.3.0/go.mod h1:mNX17F0C/HguQMyMyJxcnU471gOZGxCLyYaFyAZraas= +github.com/shirou/gopsutil/v4 v4.26.4 h1:B4SXVbcwTyrocPHEmWBC4uCYr4Xcu3MK1TXqbprAOWY= github.com/shirou/gopsutil/v4 v4.26.4/go.mod h1:LZ6ewCSkBqUpvSOf+LsTGnRinC6iaNUNMGBtDkJBaLQ= github.com/shogo82148/logrus-slog-hook v0.1.0/go.mod h1:D2Ge8IJO5/lN5LXQ4Aq5NezL1iajb08s/xNCUru8+0Y= github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= +github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w= github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= github.com/smallstep/pkcs7 v0.2.1/go.mod h1:RcXHsMfL+BzH8tRhmrF1NkkpebKpq3JEM66cOFxanf0= github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U= github.com/stoewer/go-strcase v1.3.0/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo= github.com/testcontainers/testcontainers-go v0.40.0/go.mod h1:FSXV5KQtX2HAMlm7U3APNyLkkap35zNLxukw9oBi/MY= +github.com/testcontainers/testcontainers-go v0.42.0 h1:He3IhTzTZOygSXLJPMX7n44XtK+qhjat1nI9cneBbUY= +github.com/testcontainers/testcontainers-go v0.42.0/go.mod h1:vZjdY1YmUA1qEForxOIOazfsrdyORJAbhi0bp8plN30= github.com/testcontainers/testcontainers-go/modules/openfga v0.40.0/go.mod h1:zwLB9Af9gw2ViufG7oZT/DpmubgKhnpleOWItgcnLVY= +github.com/testcontainers/testcontainers-go/modules/postgres v0.42.0 h1:GCbb1ndrF7OTDiIvxXyItaDab4qkzTFJ48LKFdM7EIo= +github.com/testcontainers/testcontainers-go/modules/postgres v0.42.0/go.mod h1:IRPBaI8jXdrNfD0e4Zm7Fbcgaz5shKxOQv4axiL09xs= github.com/tetratelabs/wazero v1.11.0/go.mod h1:eV28rsN8Q+xwjogd7f4/Pp4xFxO7uOGbLcD/LzB1wiU= github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/match v1.2.0/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/timpalpant/gzran v0.0.0-20201127163450-7b631e56f57b/go.mod h1:yTxMuBKYLrj6gYYtK3gK0ifBhjiBYtD3URZiNK7vBt0= +github.com/tinylib/msgp v1.6.1 h1:ESRv8eL3u+DNHUoSAAQRE50Hm162zqAnBoGv9PzScPY= +github.com/tinylib/msgp v1.6.1/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA= +github.com/tklauser/go-sysconf v0.3.16 h1:frioLaCQSsF5Cy1jgRBrzr6t502KIIwQ0MArYICU0nA= github.com/tklauser/go-sysconf v0.3.16/go.mod h1:/qNL9xxDhc7tx3HSRsLWNnuzbVfh3e7gh/BmM179nYI= github.com/tklauser/numcpus v0.11.0/go.mod h1:z+LwcLq54uWZTX0u/bGobaV34u6V7KNlTZejzM6/3MQ= github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701/go.mod h1:P3a5rG4X7tI17Nn3aOIAYr5HbIMukwXG0urG0WuL8OA= @@ -268,25 +343,41 @@ github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342/go.mod h1:Ohn+xnUBi github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= github.com/zclconf/go-cty v1.17.0/go.mod h1:wqFzcImaLTI6A5HfsRwB0nj5n0MRZFwmey8YoFPPs3U= +github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs= github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s= github.com/zitadel/logging v0.7.0/go.mod h1:9A6h9feBF/3u0IhA4uffdzSDY7mBaf7RE78H5sFMINQ= github.com/ztrue/tracerr v0.4.0/go.mod h1:PaFfYlas0DfmXNpo7Eay4MFhZUONqvXM+T2HyGPpngk= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0/go.mod h1:c7hN3ddxs/z6q9xwvfLPk+UHlWRQyaeR1LdgfL/66l0= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 h1:CqXxU8VOmDefoh0+ztfGaymYbhdB/tT3zs79QaZTNGY= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0/go.mod h1:BuhAPThV8PBHBvg8ZzZ/Ok3idOdhWIodywz2xEcRbJo= +go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0/go.mod h1:bTdK1nhqF76qiPoCCdyFIV+N/sRHYXYCTQc+3VCi3MI= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.40.0/go.mod h1:EtekO9DEJb4/jRyN4v4Qjc2yA7AtfCBuz2FynRUWTXs= +go.opentelemetry.io/otel/exporters/prometheus v0.65.0 h1:jOveH/b4lU9HT7y+Gfamf18BqlOuz2PWEvs8yM7Q6XE= +go.opentelemetry.io/otel/exporters/prometheus v0.65.0/go.mod h1:i1P8pcumauPtUI4YNopea1dhzEMuEqWP1xoUZDylLHo= +go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM= go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY= go.opentelemetry.io/otel/sdk v1.40.0/go.mod h1:Ph7EFdYvxq72Y8Li9q8KebuYUr2KoeyHx0DRMKrYBUE= +go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg= +go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg= +go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw= +go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A= +go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A= go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4= go.starlark.net v0.0.0-20260326113308-fadfc96def35/go.mod h1:Iue6g6iirlfLoVi/DYCi5/x0h/bAOuWF3dULTKpt2Vo= go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ= go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ= go4.org/mem v0.0.0-20240501181205-ae6ca9944745/go.mod h1:reUoABIJ9ikfM5sgtSF3Wushcza7+WeD01VB9Lirh3g= golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA= +golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI= golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q= golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -307,7 +398,6 @@ google.golang.org/genproto/googleapis/api v0.0.0-20260223185530-2f722ef697dc/go. google.golang.org/genproto/googleapis/rpc v0.0.0-20260427160629-7cedc36a6bc4/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= k8s.io/utils v0.0.0-20260319190234-28399d86e0b5/go.mod h1:xDxuJ0whA3d0I4mf/C4ppKHxXynQ+fxnkmQH0vTHnuk= libguestfs.org/libnbd v1.20.0/go.mod h1:pSICAuDOpSGplmGmaZ8QettnBAT3IUJFcBU1bVgWgk4= diff --git a/internal/cli/config.go b/internal/cli/config.go index 90f1688..87e8596 100644 --- a/internal/cli/config.go +++ b/internal/cli/config.go @@ -34,6 +34,8 @@ const ( KeyImgsrvURL = "imgsrv.url" // KeyImgsrvToken is the Viper key for the optional imgsrv bearer token used by publish. KeyImgsrvToken = "imgsrv.token" // #nosec G101 -- config key name, not a credential value. + // KeyPublishVersion is the Viper key for the imgsrv image version created by publish. + KeyPublishVersion = "publish.version" // KeyPublishPartSize is the Viper key for the publish multipart upload part size. KeyPublishPartSize = "publish.part-size" // KeyPublishWait is the Viper key for waiting until uploaded blobs become CAS-ready. @@ -54,6 +56,8 @@ const ( flagImgsrvURL = "imgsrv-url" flagImgsrvToken = "imgsrv-token" // #nosec G101 -- flag name, not a credential value. + flagReleaseVersion = "release-version" + flagAlias = "alias" flagPublishPartSize = "publish-part-size" flagPublishWait = "publish-wait" flagPublishTimeout = "publish-timeout" @@ -102,6 +106,7 @@ type Config struct { type publishConfig struct { imgsrvURL string imgsrvToken string + version string partSizeBytes int64 wait bool timeout time.Duration @@ -120,6 +125,7 @@ func configureViper(vp *viper.Viper) { vp.SetDefault(KeyCacheMaxSize, defaultCacheMaxSize) vp.SetDefault(KeyImgsrvURL, "") vp.SetDefault(KeyImgsrvToken, "") + vp.SetDefault(KeyPublishVersion, "") vp.SetDefault(KeyPublishPartSize, defaultPublishPartSize) vp.SetDefault(KeyPublishWait, true) vp.SetDefault(KeyPublishTimeout, defaultPublishTimeout) @@ -290,6 +296,7 @@ func loadPublishConfig(vp *viper.Viper) (publishConfig, error) { cfg := publishConfig{ imgsrvURL: strings.TrimSpace(vp.GetString(KeyImgsrvURL)), imgsrvToken: strings.TrimSpace(vp.GetString(KeyImgsrvToken)), + version: strings.TrimSpace(vp.GetString(KeyPublishVersion)), partSizeBytes: partSizeBytes, wait: vp.GetBool(KeyPublishWait), timeout: timeout, @@ -371,6 +378,19 @@ func validatePublishConfig(cfg publishConfig) error { if cfg.imgsrvURL == "" { return errors.New("publish requires imgsrv.url: set --imgsrv-url, IMGCLI_IMGSRV_URL, or config imgsrv.url") } + if cfg.imgsrvToken == "" { + return errors.New( + "publish requires imgsrv.token: set --imgsrv-token, IMGCLI_IMGSRV_TOKEN, or config imgsrv.token", + ) + } + if cfg.version == "" { + return errors.New( + "publish requires publish.version: set --release-version, IMGCLI_PUBLISH_VERSION, or config publish.version", + ) + } + if !cfg.wait { + return errors.New("publish requires CAS-ready uploads: --publish-wait=false is not supported") + } if cfg.partSizeBytes < minPublishPartSizeBytes { return fmt.Errorf( "invalid %s %q: must be at least %s", diff --git a/internal/cli/options.go b/internal/cli/options.go index e6f6e77..c077ad6 100644 --- a/internal/cli/options.go +++ b/internal/cli/options.go @@ -39,6 +39,9 @@ type Options struct { // ImgsrvUploadsClient uploads artifacts to imgsrv. Nil selects the HTTP SDK client. ImgsrvUploadsClient publish.UploadsClient + + // ImgsrvCatalogClient publishes artifacts into the imgsrv catalog. Nil selects the HTTP SDK client. + ImgsrvCatalogClient publish.CatalogClient } func (o Options) version() string { diff --git a/internal/cli/publish.go b/internal/cli/publish.go index ff7c8b1..9a53ad2 100644 --- a/internal/cli/publish.go +++ b/internal/cli/publish.go @@ -1,7 +1,11 @@ package cli import ( + "encoding/json" + "errors" "fmt" + "io" + "strings" imgsrv "github.com/meigma/imgsrv/client" "github.com/spf13/cobra" @@ -9,6 +13,8 @@ import ( "github.com/meigma/imgcli/internal/providers" "github.com/meigma/imgcli/internal/publish" + imgschemas "github.com/meigma/imgcli/schemas" + "github.com/meigma/imgcli/schemas/core" ) func newPublishCommand(rt *runtime) *cobra.Command { @@ -21,18 +27,22 @@ func newPublishCommand(rt *runtime) *cobra.Command { if err != nil { return err } + aliases, err := publishAliases(cmd) + if err != nil { + return err + } config, err := loadImageConfig(args[0]) if err != nil { return err } - result, err := rt.runIncusOSBuild(cmd.Context(), config) + buildResult, err := rt.runIncusOSBuild(cmd.Context(), config) if err != nil { return err } - uploadsClient, err := rt.imgsrvUploadsClient(pubConfig) + uploadsClient, catalogClient, err := rt.imgsrvPublishClients(pubConfig) if err != nil { return err } @@ -47,28 +57,29 @@ func newPublishCommand(rt *runtime) *cobra.Command { return err } - digests := make([]string, 0, len(result.Artifacts)) - for _, artifact := range result.Artifacts { - uploadResult, err := uploader.UploadArtifact(cmd.Context(), publishArtifact(artifact)) - if err != nil { - return err - } - digests = append(digests, uploadResult.Digest.String()) + publisher, err := publish.NewPublisher(catalogClient, uploader) + if err != nil { + return err } - for _, digest := range digests { - if _, err := fmt.Fprintln(rt.opts.stdout(), digest); err != nil { - return fmt.Errorf("write published artifact digest: %w", err) - } + request, err := publishReleaseRequest(config, buildResult, pubConfig.version, aliases) + if err != nil { + return err + } + result, err := publisher.PublishRelease(cmd.Context(), request) + if err != nil { + return err } - return nil + return printPublishResult(rt.opts.stdout(), result) }, } flags := cmd.Flags() flags.String(flagImgsrvURL, "", "imgsrv API base URL") flags.String(flagImgsrvToken, "", "imgsrv bearer token") + flags.String(flagReleaseVersion, "", "imgsrv image version to publish") + flags.StringArray(flagAlias, nil, "imgsrv alias to point at the published version") flags.String(flagPublishPartSize, defaultPublishPartSize, "Multipart upload part size") flags.Bool(flagPublishWait, true, "Wait until imgsrv marks the upload ready") flags.String(flagPublishTimeout, defaultPublishTimeout, "Maximum time to wait for imgsrv readiness") @@ -76,6 +87,7 @@ func newPublishCommand(rt *runtime) *cobra.Command { mustBindPublishFlag(rt, flags, KeyImgsrvURL, flagImgsrvURL) mustBindPublishFlag(rt, flags, KeyImgsrvToken, flagImgsrvToken) + mustBindPublishFlag(rt, flags, KeyPublishVersion, flagReleaseVersion) mustBindPublishFlag(rt, flags, KeyPublishPartSize, flagPublishPartSize) mustBindPublishFlag(rt, flags, KeyPublishWait, flagPublishWait) mustBindPublishFlag(rt, flags, KeyPublishTimeout, flagPublishTimeout) @@ -90,9 +102,13 @@ func mustBindPublishFlag(rt *runtime, flags *pflag.FlagSet, key string, flagName } } -func (rt *runtime) imgsrvUploadsClient(cfg publishConfig) (publish.UploadsClient, error) { - if rt.opts.ImgsrvUploadsClient != nil { - return rt.opts.ImgsrvUploadsClient, nil +func (rt *runtime) imgsrvPublishClients( + cfg publishConfig, +) (publish.UploadsClient, publish.CatalogClient, error) { + uploads := rt.opts.ImgsrvUploadsClient + catalog := rt.opts.ImgsrvCatalogClient + if uploads != nil && catalog != nil { + return uploads, catalog, nil } client, err := imgsrv.New(imgsrv.Options{ @@ -101,17 +117,111 @@ func (rt *runtime) imgsrvUploadsClient(cfg publishConfig) (publish.UploadsClient UserAgent: "imgcli/" + rt.opts.version(), }) if err != nil { - return nil, fmt.Errorf("configure imgsrv client: %w", err) + return nil, nil, fmt.Errorf("configure imgsrv client: %w", err) } - return client.Uploads(), nil + if uploads == nil { + uploads = client.Uploads() + } + if catalog == nil { + catalog = client.Catalog() + } + + return uploads, catalog, nil } -func publishArtifact(artifact providers.BuiltArtifact) publish.Artifact { - return publish.Artifact{ - Path: artifact.Path, - Size: artifact.Size, - SHA256: artifact.SHA256, - MediaType: artifact.Plan.MediaType, +func publishAliases(cmd *cobra.Command) ([]string, error) { + values, err := cmd.Flags().GetStringArray(flagAlias) + if err != nil { + return nil, fmt.Errorf("read publish aliases: %w", err) } + + aliases := make([]string, 0, len(values)) + for _, value := range values { + alias := strings.TrimSpace(value) + if alias == "" { + return nil, errors.New("publish alias must not be empty") + } + aliases = append(aliases, alias) + } + + return aliases, nil +} + +func publishReleaseRequest( + config imgschemas.Config, + buildResult providers.BuildResult, + version string, + aliases []string, +) (publish.ReleaseRequest, error) { + request := publish.ReleaseRequest{ + ImageName: publishImageName(config), + ImageDescription: config.Image.Description, + Version: version, + Aliases: aliases, + Artifacts: make([]publish.ReleaseArtifact, 0, len(buildResult.Artifacts)), + } + + for _, artifact := range buildResult.Artifacts { + format, err := imgsrvArtifactFormat(artifact.Plan.Format) + if err != nil { + return publish.ReleaseRequest{}, err + } + + request.Artifacts = append(request.Artifacts, publish.ReleaseArtifact{ + Key: string(artifact.Plan.Key), + Variant: string(artifact.Plan.Variant), + LocalPath: artifact.Path, + OperatingSystem: artifact.Plan.OperatingSystem, + Architecture: imgsrvArchitecture(artifact.Plan.Architecture), + Format: format, + Digest: artifact.SHA256, + Size: artifact.Size, + MediaType: artifact.Plan.MediaType, + }) + } + + return request, nil +} + +func publishImageName(config imgschemas.Config) string { + if config.Publish != nil { + imageName := strings.TrimSpace(string(config.Publish.ImageName)) + if imageName != "" { + return imageName + } + } + + return string(config.Image.Name) +} + +func imgsrvArchitecture(architecture core.Architecture) string { + switch architecture { + case "amd64": + return "x86_64" + case "arm64": + return "aarch64" + default: + return string(architecture) + } +} + +func imgsrvArtifactFormat(format core.ArtifactFormat) (imgsrv.ArtifactFormat, error) { + switch format { + case "raw": + return imgsrv.ArtifactFormatRaw, nil + case "raw.gz": + return imgsrv.ArtifactFormatRawGZ, nil + default: + return "", fmt.Errorf("unsupported imgsrv artifact format %q", format) + } +} + +func printPublishResult(output io.Writer, result publish.ReleaseResult) error { + encoder := json.NewEncoder(output) + if err := encoder.Encode(result); err != nil { + return fmt.Errorf("write published release manifest: %w", err) + } + + return nil } diff --git a/internal/cli/root_test.go b/internal/cli/root_test.go index 91de871..5b7c686 100644 --- a/internal/cli/root_test.go +++ b/internal/cli/root_test.go @@ -3,6 +3,7 @@ package cli import ( "bytes" "context" + "encoding/json" "os" "path/filepath" "strings" @@ -14,6 +15,7 @@ import ( "github.com/stretchr/testify/require" "github.com/meigma/imgcli/internal/providers/incusos" + "github.com/meigma/imgcli/internal/publish" publishmocks "github.com/meigma/imgcli/internal/publish/mocks" "github.com/meigma/imgcli/schemas/core" ) @@ -272,13 +274,77 @@ func TestPublishCommand(t *testing.T) { assert.Empty(t, result.stderr) }) - t.Run("builds and uploads IncusOS artifact", func(t *testing.T) { + t.Run("requires imgsrv token", func(t *testing.T) { + clearIMGCLIEnv(t) + + result := executeCommand( + t, + Options{}, + "publish", + "image.cue", + "--imgsrv-url", + "https://imgsrv.example.invalid", + "--release-version", + "v1.0.0", + ) + + require.Error(t, result.err) + require.ErrorContains(t, result.err, "publish requires imgsrv.token") + assert.Empty(t, result.stdout) + assert.Empty(t, result.stderr) + }) + + t.Run("requires release version", func(t *testing.T) { + clearIMGCLIEnv(t) + + result := executeCommand( + t, + Options{}, + "publish", + "image.cue", + "--imgsrv-url", + "https://imgsrv.example.invalid", + "--imgsrv-token", + "test-token", + ) + + require.Error(t, result.err) + require.ErrorContains(t, result.err, "publish requires publish.version") + assert.Empty(t, result.stdout) + assert.Empty(t, result.stderr) + }) + + t.Run("rejects disabled upload readiness wait", func(t *testing.T) { + clearIMGCLIEnv(t) + + result := executeCommand( + t, + Options{}, + "publish", + "image.cue", + "--imgsrv-url", + "https://imgsrv.example.invalid", + "--imgsrv-token", + "test-token", + "--release-version", + "v1.0.0", + "--publish-wait=false", + ) + + require.Error(t, result.err) + require.ErrorContains(t, result.err, "publish requires CAS-ready uploads") + assert.Empty(t, result.stdout) + assert.Empty(t, result.stderr) + }) + + t.Run("builds and publishes IncusOS release", func(t *testing.T) { clearIMGCLIEnv(t) outputDir := filepath.Join(t.TempDir(), "out") configPath := writeImageConfig(t, ` apiVersion: "imgcli.meigma.io/v0alpha1" kind: "ImagePlan" image: name: "test-image" +publish: imageName: "published-image" output: dir: "`+outputDir+`" incusos: { defaults: source: channel: "testing" @@ -316,11 +382,14 @@ incusos: { sha256: "abc123", } uploads := publishmocks.NewMockUploadsClient(t) + publishCatalog := publishmocks.NewMockCatalogClient(t) + mediaTypeHint := "application/gzip" filenameHint := filepath.Base(wantOutputPath) uploads.EXPECT(). BeginUpload(mock.Anything, imgsrv.BeginUploadRequest{ ExpectedDigest: "sha256:abc123", ExpectedSizeBytes: int64(len(artifactBody)), + MediaTypeHint: &mediaTypeHint, FilenameHint: &filenameHint, }). Return(imgsrv.UploadSession{ID: "upload-1", State: imgsrv.UploadStateCreated}, nil). @@ -337,6 +406,45 @@ incusos: { }). Return(imgsrv.UploadSession{ID: "upload-1", State: imgsrv.UploadStateReady}, nil). Once() + publishCatalog.EXPECT(). + CreateImage(mock.Anything, imgsrv.CreateImageRequest{Name: "published-image"}). + Return(imgsrv.Image{Name: "published-image"}, nil). + Once() + publishCatalog.EXPECT(). + CreateDraftVersion(mock.Anything, "published-image", imgsrv.CreateDraftVersionRequest{Version: "v1.0.0"}). + Return(imgsrv.ImageVersion{Version: "v1.0.0", State: imgsrv.ImageVersionStateDraft}, nil). + Once() + publishCatalog.EXPECT(). + AddArtifact(mock.Anything, "published-image", "v1.0.0", imgsrv.AddArtifactRequest{ + OperatingSystem: "incusos", + Architecture: "x86_64", + Format: imgsrv.ArtifactFormatRawGZ, + PrimaryBlobDigest: "sha256:abc123", + PrimaryBlobSizeBytes: int64(len(artifactBody)), + PrimaryMediaType: "application/gzip", + }). + Return(imgsrv.Artifact{ + ID: "artifact-1", + OperatingSystem: "incusos", + Architecture: "x86_64", + Format: imgsrv.ArtifactFormatRawGZ, + PrimaryBlobDigest: "sha256:abc123", + PrimaryBlobSizeBytes: int64(len(artifactBody)), + PrimaryMediaType: "application/gzip", + }, nil). + Once() + publishCatalog.EXPECT(). + PublishVersion(mock.Anything, "published-image", "v1.0.0"). + Return(imgsrv.ImageVersion{Version: "v1.0.0", State: imgsrv.ImageVersionStatePublished}, nil). + Once() + publishCatalog.EXPECT(). + PutAlias(mock.Anything, "published-image", "latest", imgsrv.PutAliasRequest{Version: "v1.0.0"}). + Return(imgsrv.Alias{Alias: "latest", Version: "v1.0.0"}, nil). + Once() + publishCatalog.EXPECT(). + PutAlias(mock.Anything, "published-image", "prod", imgsrv.PutAliasRequest{Version: "v1.0.0"}). + Return(imgsrv.Alias{Alias: "prod", Version: "v1.0.0"}, nil). + Once() result := executeCommand(t, Options{ IncusOSCatalog: catalog, @@ -344,11 +452,46 @@ incusos: { IncusOSSeedBuilder: seedBuilder, IncusOSImageInjector: injector, ImgsrvUploadsClient: uploads, - }, "--imgsrv-url", "https://imgsrv.example.invalid", "publish", configPath) + ImgsrvCatalogClient: publishCatalog, + }, + "publish", + configPath, + "--imgsrv-url", + "https://imgsrv.example.invalid", + "--imgsrv-token", + "test-token", + "--release-version", + "v1.0.0", + "--alias", + "latest", + "--alias", + "prod", + ) require.NoError(t, result.err) - assert.Equal(t, "sha256:abc123\n", result.stdout) assert.Empty(t, result.stderr) + var manifest publish.ReleaseResult + require.NoError(t, json.Unmarshal([]byte(result.stdout), &manifest)) + assert.Equal(t, publish.ReleaseResult{ + Image: "published-image", + Version: "v1.0.0", + State: imgsrv.ImageVersionStatePublished, + Aliases: []string{"latest", "prod"}, + Artifacts: []publish.PublishedReleaseArtifact{ + { + ArtifactKey: "default", + Variant: "default", + LocalPath: wantOutputPath, + ServerArtifactID: "artifact-1", + OperatingSystem: "incusos", + Architecture: "x86_64", + Format: imgsrv.ArtifactFormatRawGZ, + Digest: "sha256:abc123", + Size: int64(len(artifactBody)), + MediaType: "application/gzip", + }, + }, + }, manifest) require.Len(t, catalog.queries, 1) assert.Equal(t, incusos.ImageQuery{ Channel: incusos.ChannelTesting, @@ -380,12 +523,16 @@ func TestPublishConfigIsPublishOnly(t *testing.T) { result := executeCommand( t, Options{}, + "publish", + "image.cue", "--imgsrv-url", "https://imgsrv.example.invalid", + "--imgsrv-token", + "test-token", + "--release-version", + "v1.0.0", "--publish-part-size", "1MB", - "publish", - "image.cue", ) require.Error(t, result.err) @@ -411,12 +558,16 @@ func TestPublishConfigIsPublishOnly(t *testing.T) { result := executeCommand( t, Options{}, + "publish", + "image.cue", "--imgsrv-url", "https://imgsrv.example.invalid", + "--imgsrv-token", + "test-token", + "--release-version", + "v1.0.0", "--publish-timeout", "soon", - "publish", - "image.cue", ) require.Error(t, result.err) @@ -463,6 +614,7 @@ func clearIMGCLIEnv(t *testing.T) { "IMGCLI_PUBLISH_PART_SIZE", "IMGCLI_PUBLISH_POLL_INTERVAL", "IMGCLI_PUBLISH_TIMEOUT", + "IMGCLI_PUBLISH_VERSION", "IMGCLI_PUBLISH_WAIT", } { t.Setenv(key, "") diff --git a/internal/integration/publish_flow_test.go b/internal/integration/publish_flow_test.go new file mode 100644 index 0000000..3583996 --- /dev/null +++ b/internal/integration/publish_flow_test.go @@ -0,0 +1,227 @@ +//go:build integration + +package integration_test + +import ( + "bytes" + "context" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "io" + "os" + "path/filepath" + "testing" + + imgsrv "github.com/meigma/imgsrv/client" + imgsrvtest "github.com/meigma/imgsrv/test" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/meigma/imgcli/internal/cli" + "github.com/meigma/imgcli/internal/providers/incusos" + "github.com/meigma/imgcli/internal/publish" + "github.com/meigma/imgcli/schemas/core" +) + +const integrationAPIToken = "testtok.imgcli-publish" + +func TestPublishIncusOSReleaseToImgsrv(t *testing.T) { + clearIntegrationEnv(t) + ctx := context.Background() + env := imgsrvtest.Start(t, imgsrvtest.WithCASPromotion(), imgsrvtest.WithAPIToken(integrationAPIToken)) + imageName := "incusos-test" + version := "2026.05.06" + outputDir := filepath.Join(t.TempDir(), "out") + configPath := writeIntegrationConfig(t, imageName, outputDir) + artifactBody := []byte("published IncusOS artifact bytes") + catalog := &integrationCatalog{ + asset: incusos.ImageAsset{ + Version: incusos.Version("202604261712"), + Architecture: core.Architecture("amd64"), + Type: incusos.ImageTypeRaw, + URL: "https://example.invalid/os/202604261712/x86_64/IncusOS_202604261712.img.gz", + SHA256: "source-sha", + Size: 42, + }, + } + downloader := &integrationDownloader{ + image: incusos.DownloadedImage{ + Path: "/cache/source.img.gz", + SHA256: "source-sha", + Size: 42, + }, + } + seedBuilder := &integrationSeedBuilder{seed: incusos.SeedArchive{Data: []byte("seed")}} + injector := &integrationInjector{body: artifactBody} + var stdout bytes.Buffer + var stderr bytes.Buffer + + cmd, err := cli.NewRootCommand(cli.Options{ + Stdout: &stdout, + Stderr: &stderr, + Stdin: bytes.NewReader(nil), + Environ: []string{"TERM=dumb"}, + IncusOSCatalog: catalog, + IncusOSDownloader: downloader, + IncusOSSeedBuilder: seedBuilder, + IncusOSImageInjector: injector, + }) + require.NoError(t, err) + cmd.SetArgs([]string{ + "publish", + configPath, + "--imgsrv-url", + env.BaseURL(), + "--imgsrv-token", + integrationAPIToken, + "--release-version", + version, + "--alias", + "latest", + "--publish-timeout", + "10s", + "--publish-poll-interval", + "10ms", + }) + + require.NoError(t, cmd.ExecuteContext(ctx)) + assert.Empty(t, stderr.String()) + var published publish.ReleaseResult + require.NoError(t, json.Unmarshal(stdout.Bytes(), &published)) + assert.Equal(t, imageName, published.Image) + assert.Equal(t, version, published.Version) + assert.Equal(t, imgsrv.ImageVersionStatePublished, published.State) + assert.Equal(t, []string{"latest"}, published.Aliases) + require.Len(t, published.Artifacts, 1) + assert.Equal(t, "incusos", published.Artifacts[0].OperatingSystem) + assert.Equal(t, "x86_64", published.Artifacts[0].Architecture) + assert.Equal(t, imgsrv.ArtifactFormatRawGZ, published.Artifacts[0].Format) + + imgsrvClient := env.Client(t) + manifest, err := imgsrvClient.Catalog().ResolveManifest(ctx, imageName, "latest") + require.NoError(t, err) + require.Len(t, manifest.Artifacts, 1) + artifact := manifest.Artifacts[0].Artifact + assert.Equal(t, imgsrv.ArtifactFormatRawGZ, artifact.Format) + assert.Equal(t, "x86_64", artifact.Architecture) + assert.Equal(t, "incusos", artifact.OperatingSystem) + + download, err := imgsrvClient.Catalog().OpenArtifactDownload( + ctx, + imageName, + version, + artifact.ID.String(), + imgsrv.OpenBlobOptions{}, + ) + require.NoError(t, err) + defer download.Body.Close() + got, err := io.ReadAll(download.Body) + require.NoError(t, err) + assert.Equal(t, artifactBody, got) +} + +func clearIntegrationEnv(t *testing.T) { + t.Helper() + + for _, key := range []string{ + "IMGCLI_CACHE_DIR", + "IMGCLI_CACHE_MAX_SIZE", + "IMGCLI_CONFIG", + "IMGCLI_IMGSRV_TOKEN", + "IMGCLI_IMGSRV_URL", + "IMGCLI_LOG_LEVEL", + "IMGCLI_LOG_FORMAT", + "IMGCLI_NO_COLOR", + "IMGCLI_PUBLISH_PART_SIZE", + "IMGCLI_PUBLISH_POLL_INTERVAL", + "IMGCLI_PUBLISH_TIMEOUT", + "IMGCLI_PUBLISH_VERSION", + "IMGCLI_PUBLISH_WAIT", + } { + t.Setenv(key, "") + } + t.Setenv("XDG_CONFIG_HOME", t.TempDir()) +} + +func writeIntegrationConfig(t *testing.T, imageName string, outputDir string) string { + t.Helper() + + path := filepath.Join(t.TempDir(), "image.cue") + content := ` +apiVersion: "imgcli.meigma.io/v0alpha1" +kind: "ImagePlan" +image: name: "` + imageName + `" +output: dir: "` + outputDir + `" +incusos: { + defaults: source: channel: "testing" + seed: install: {} + variants: default: { + source: version: "202604261712" + artifact: { + architecture: "amd64" + format: "raw.gz" + } + } +} +` + require.NoError(t, os.WriteFile(path, []byte(content), 0o600)) + return path +} + +type integrationCatalog struct { + asset incusos.ImageAsset +} + +func (c *integrationCatalog) ResolveImage(_ context.Context, query incusos.ImageQuery) (incusos.ImageAsset, error) { + c.asset.Architecture = query.Architecture + c.asset.Type = query.Type + return c.asset, nil +} + +type integrationDownloader struct { + image incusos.DownloadedImage +} + +func (d *integrationDownloader) DownloadImage( + _ context.Context, + asset incusos.ImageAsset, +) (incusos.DownloadedImage, error) { + image := d.image + image.Asset = asset + return image, nil +} + +type integrationSeedBuilder struct { + seed incusos.SeedArchive +} + +func (b *integrationSeedBuilder) BuildSeed(_ context.Context, _ incusos.Config) (incusos.SeedArchive, error) { + return b.seed, nil +} + +type integrationInjector struct { + body []byte +} + +func (i *integrationInjector) InjectSeed( + _ context.Context, + image incusos.DownloadedImage, + _ incusos.SeedArchive, + outputPath string, +) (incusos.CustomizedImage, error) { + if err := os.MkdirAll(filepath.Dir(outputPath), 0o750); err != nil { + return incusos.CustomizedImage{}, err + } + if err := os.WriteFile(outputPath, i.body, 0o600); err != nil { + return incusos.CustomizedImage{}, err + } + + sum := sha256.Sum256(i.body) + return incusos.CustomizedImage{ + Source: image, + Path: outputPath, + Size: int64(len(i.body)), + SHA256: hex.EncodeToString(sum[:]), + }, nil +} diff --git a/internal/providers/incusos/provider.go b/internal/providers/incusos/provider.go index 3c69a67..5d1c52f 100644 --- a/internal/providers/incusos/provider.go +++ b/internal/providers/incusos/provider.go @@ -119,14 +119,15 @@ func (p *Provider) Build(ctx context.Context, req providers.BuildRequest) (provi } artifactPlan := providers.ArtifactPlan{ - Key: core.ArtifactKey(variantName), - Variant: variantName, - Architecture: variant.Artifact.Architecture, - Format: variant.Artifact.Format, - MediaType: variant.Artifact.MediaType, - OutputPath: outputPath, - Labels: variant.Artifact.Labels, - Annotations: variant.Artifact.Annotations, + Key: core.ArtifactKey(variantName), + Variant: variantName, + Architecture: variant.Artifact.Architecture, + OperatingSystem: artifactOperatingSystem(variant.Artifact), + Format: variant.Artifact.Format, + MediaType: artifactMediaType(variant.Artifact), + OutputPath: outputPath, + Labels: variant.Artifact.Labels, + Annotations: variant.Artifact.Annotations, } plan := req.Plan plan.Provider = providerName @@ -233,3 +234,24 @@ func imageTypeForFormat(format core.ArtifactFormat) (ImageType, error) { return "", fmt.Errorf("unsupported incusos artifact format %q", format) } } + +func artifactMediaType(artifact core.ArtifactIntent) string { + if strings.TrimSpace(artifact.MediaType) != "" { + return artifact.MediaType + } + + switch artifact.Format { + case "raw.gz": + return "application/gzip" + default: + return "application/octet-stream" + } +} + +func artifactOperatingSystem(artifact core.ArtifactIntent) string { + if strings.TrimSpace(artifact.Os) != "" { + return artifact.Os + } + + return string(providerName) +} diff --git a/internal/providers/incusos/provider_test.go b/internal/providers/incusos/provider_test.go index c2f51bd..56143be 100644 --- a/internal/providers/incusos/provider_test.go +++ b/internal/providers/incusos/provider_test.go @@ -147,14 +147,15 @@ func TestProviderBuildCreatesCustomizedImage(t *testing.T) { require.Len(t, result.Plan.Artifacts, 1) assert.Equal(t, result.Plan.Artifacts[0], result.Artifacts[0].Plan) assert.Equal(t, providers.ArtifactPlan{ - Key: core.ArtifactKey("default"), - Variant: core.VariantName("default"), - Architecture: tt.artifact.Architecture, - Format: tt.artifact.Format, - MediaType: tt.artifact.MediaType, - OutputPath: wantOutputPath, - Labels: tt.artifact.Labels, - Annotations: tt.artifact.Annotations, + Key: core.ArtifactKey("default"), + Variant: core.VariantName("default"), + Architecture: tt.artifact.Architecture, + OperatingSystem: "incusos", + Format: tt.artifact.Format, + MediaType: artifactMediaType(tt.artifact), + OutputPath: wantOutputPath, + Labels: tt.artifact.Labels, + Annotations: tt.artifact.Annotations, }, result.Plan.Artifacts[0]) }) } diff --git a/internal/providers/provider.go b/internal/providers/provider.go index 995764a..ce3f507 100644 --- a/internal/providers/provider.go +++ b/internal/providers/provider.go @@ -71,6 +71,9 @@ type ArtifactPlan struct { // Architecture is the target architecture for this artifact. Architecture core.Architecture + // OperatingSystem is the artifact operating-system token published to imgsrv. + OperatingSystem string + // Format is the artifact file format. Format core.ArtifactFormat diff --git a/internal/publish/mocks/catalog_client.go b/internal/publish/mocks/catalog_client.go new file mode 100644 index 0000000..f40860f --- /dev/null +++ b/internal/publish/mocks/catalog_client.go @@ -0,0 +1,405 @@ +// Code generated by mockery; DO NOT EDIT. +// github.com/vektra/mockery +// template: testify + +package mocks + +import ( + "context" + + "github.com/meigma/imgsrv/client" + mock "github.com/stretchr/testify/mock" +) + +// NewMockCatalogClient creates a new instance of MockCatalogClient. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewMockCatalogClient(t interface { + mock.TestingT + Cleanup(func()) +}) *MockCatalogClient { + mock := &MockCatalogClient{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} + +// MockCatalogClient is an autogenerated mock type for the CatalogClient type +type MockCatalogClient struct { + mock.Mock +} + +type MockCatalogClient_Expecter struct { + mock *mock.Mock +} + +func (_m *MockCatalogClient) EXPECT() *MockCatalogClient_Expecter { + return &MockCatalogClient_Expecter{mock: &_m.Mock} +} + +// AddArtifact provides a mock function for the type MockCatalogClient +func (_mock *MockCatalogClient) AddArtifact(context1 context.Context, s string, s1 string, addArtifactRequest client.AddArtifactRequest) (client.Artifact, error) { + ret := _mock.Called(context1, s, s1, addArtifactRequest) + + if len(ret) == 0 { + panic("no return value specified for AddArtifact") + } + + var r0 client.Artifact + var r1 error + if returnFunc, ok := ret.Get(0).(func(context.Context, string, string, client.AddArtifactRequest) (client.Artifact, error)); ok { + return returnFunc(context1, s, s1, addArtifactRequest) + } + if returnFunc, ok := ret.Get(0).(func(context.Context, string, string, client.AddArtifactRequest) client.Artifact); ok { + r0 = returnFunc(context1, s, s1, addArtifactRequest) + } else { + r0 = ret.Get(0).(client.Artifact) + } + if returnFunc, ok := ret.Get(1).(func(context.Context, string, string, client.AddArtifactRequest) error); ok { + r1 = returnFunc(context1, s, s1, addArtifactRequest) + } else { + r1 = ret.Error(1) + } + return r0, r1 +} + +// MockCatalogClient_AddArtifact_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AddArtifact' +type MockCatalogClient_AddArtifact_Call struct { + *mock.Call +} + +// AddArtifact is a helper method to define mock.On call +// - context1 context.Context +// - s string +// - s1 string +// - addArtifactRequest client.AddArtifactRequest +func (_e *MockCatalogClient_Expecter) AddArtifact(context1 interface{}, s interface{}, s1 interface{}, addArtifactRequest interface{}) *MockCatalogClient_AddArtifact_Call { + return &MockCatalogClient_AddArtifact_Call{Call: _e.mock.On("AddArtifact", context1, s, s1, addArtifactRequest)} +} + +func (_c *MockCatalogClient_AddArtifact_Call) Run(run func(context1 context.Context, s string, s1 string, addArtifactRequest client.AddArtifactRequest)) *MockCatalogClient_AddArtifact_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 string + if args[1] != nil { + arg1 = args[1].(string) + } + var arg2 string + if args[2] != nil { + arg2 = args[2].(string) + } + var arg3 client.AddArtifactRequest + if args[3] != nil { + arg3 = args[3].(client.AddArtifactRequest) + } + run( + arg0, + arg1, + arg2, + arg3, + ) + }) + return _c +} + +func (_c *MockCatalogClient_AddArtifact_Call) Return(artifact client.Artifact, err error) *MockCatalogClient_AddArtifact_Call { + _c.Call.Return(artifact, err) + return _c +} + +func (_c *MockCatalogClient_AddArtifact_Call) RunAndReturn(run func(context1 context.Context, s string, s1 string, addArtifactRequest client.AddArtifactRequest) (client.Artifact, error)) *MockCatalogClient_AddArtifact_Call { + _c.Call.Return(run) + return _c +} + +// CreateDraftVersion provides a mock function for the type MockCatalogClient +func (_mock *MockCatalogClient) CreateDraftVersion(context1 context.Context, s string, createDraftVersionRequest client.CreateDraftVersionRequest) (client.ImageVersion, error) { + ret := _mock.Called(context1, s, createDraftVersionRequest) + + if len(ret) == 0 { + panic("no return value specified for CreateDraftVersion") + } + + var r0 client.ImageVersion + var r1 error + if returnFunc, ok := ret.Get(0).(func(context.Context, string, client.CreateDraftVersionRequest) (client.ImageVersion, error)); ok { + return returnFunc(context1, s, createDraftVersionRequest) + } + if returnFunc, ok := ret.Get(0).(func(context.Context, string, client.CreateDraftVersionRequest) client.ImageVersion); ok { + r0 = returnFunc(context1, s, createDraftVersionRequest) + } else { + r0 = ret.Get(0).(client.ImageVersion) + } + if returnFunc, ok := ret.Get(1).(func(context.Context, string, client.CreateDraftVersionRequest) error); ok { + r1 = returnFunc(context1, s, createDraftVersionRequest) + } else { + r1 = ret.Error(1) + } + return r0, r1 +} + +// MockCatalogClient_CreateDraftVersion_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CreateDraftVersion' +type MockCatalogClient_CreateDraftVersion_Call struct { + *mock.Call +} + +// CreateDraftVersion is a helper method to define mock.On call +// - context1 context.Context +// - s string +// - createDraftVersionRequest client.CreateDraftVersionRequest +func (_e *MockCatalogClient_Expecter) CreateDraftVersion(context1 interface{}, s interface{}, createDraftVersionRequest interface{}) *MockCatalogClient_CreateDraftVersion_Call { + return &MockCatalogClient_CreateDraftVersion_Call{Call: _e.mock.On("CreateDraftVersion", context1, s, createDraftVersionRequest)} +} + +func (_c *MockCatalogClient_CreateDraftVersion_Call) Run(run func(context1 context.Context, s string, createDraftVersionRequest client.CreateDraftVersionRequest)) *MockCatalogClient_CreateDraftVersion_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 string + if args[1] != nil { + arg1 = args[1].(string) + } + var arg2 client.CreateDraftVersionRequest + if args[2] != nil { + arg2 = args[2].(client.CreateDraftVersionRequest) + } + run( + arg0, + arg1, + arg2, + ) + }) + return _c +} + +func (_c *MockCatalogClient_CreateDraftVersion_Call) Return(imageVersion client.ImageVersion, err error) *MockCatalogClient_CreateDraftVersion_Call { + _c.Call.Return(imageVersion, err) + return _c +} + +func (_c *MockCatalogClient_CreateDraftVersion_Call) RunAndReturn(run func(context1 context.Context, s string, createDraftVersionRequest client.CreateDraftVersionRequest) (client.ImageVersion, error)) *MockCatalogClient_CreateDraftVersion_Call { + _c.Call.Return(run) + return _c +} + +// CreateImage provides a mock function for the type MockCatalogClient +func (_mock *MockCatalogClient) CreateImage(context1 context.Context, createImageRequest client.CreateImageRequest) (client.Image, error) { + ret := _mock.Called(context1, createImageRequest) + + if len(ret) == 0 { + panic("no return value specified for CreateImage") + } + + var r0 client.Image + var r1 error + if returnFunc, ok := ret.Get(0).(func(context.Context, client.CreateImageRequest) (client.Image, error)); ok { + return returnFunc(context1, createImageRequest) + } + if returnFunc, ok := ret.Get(0).(func(context.Context, client.CreateImageRequest) client.Image); ok { + r0 = returnFunc(context1, createImageRequest) + } else { + r0 = ret.Get(0).(client.Image) + } + if returnFunc, ok := ret.Get(1).(func(context.Context, client.CreateImageRequest) error); ok { + r1 = returnFunc(context1, createImageRequest) + } else { + r1 = ret.Error(1) + } + return r0, r1 +} + +// MockCatalogClient_CreateImage_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CreateImage' +type MockCatalogClient_CreateImage_Call struct { + *mock.Call +} + +// CreateImage is a helper method to define mock.On call +// - context1 context.Context +// - createImageRequest client.CreateImageRequest +func (_e *MockCatalogClient_Expecter) CreateImage(context1 interface{}, createImageRequest interface{}) *MockCatalogClient_CreateImage_Call { + return &MockCatalogClient_CreateImage_Call{Call: _e.mock.On("CreateImage", context1, createImageRequest)} +} + +func (_c *MockCatalogClient_CreateImage_Call) Run(run func(context1 context.Context, createImageRequest client.CreateImageRequest)) *MockCatalogClient_CreateImage_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 client.CreateImageRequest + if args[1] != nil { + arg1 = args[1].(client.CreateImageRequest) + } + run( + arg0, + arg1, + ) + }) + return _c +} + +func (_c *MockCatalogClient_CreateImage_Call) Return(image client.Image, err error) *MockCatalogClient_CreateImage_Call { + _c.Call.Return(image, err) + return _c +} + +func (_c *MockCatalogClient_CreateImage_Call) RunAndReturn(run func(context1 context.Context, createImageRequest client.CreateImageRequest) (client.Image, error)) *MockCatalogClient_CreateImage_Call { + _c.Call.Return(run) + return _c +} + +// PublishVersion provides a mock function for the type MockCatalogClient +func (_mock *MockCatalogClient) PublishVersion(context1 context.Context, s string, s1 string) (client.ImageVersion, error) { + ret := _mock.Called(context1, s, s1) + + if len(ret) == 0 { + panic("no return value specified for PublishVersion") + } + + var r0 client.ImageVersion + var r1 error + if returnFunc, ok := ret.Get(0).(func(context.Context, string, string) (client.ImageVersion, error)); ok { + return returnFunc(context1, s, s1) + } + if returnFunc, ok := ret.Get(0).(func(context.Context, string, string) client.ImageVersion); ok { + r0 = returnFunc(context1, s, s1) + } else { + r0 = ret.Get(0).(client.ImageVersion) + } + if returnFunc, ok := ret.Get(1).(func(context.Context, string, string) error); ok { + r1 = returnFunc(context1, s, s1) + } else { + r1 = ret.Error(1) + } + return r0, r1 +} + +// MockCatalogClient_PublishVersion_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'PublishVersion' +type MockCatalogClient_PublishVersion_Call struct { + *mock.Call +} + +// PublishVersion is a helper method to define mock.On call +// - context1 context.Context +// - s string +// - s1 string +func (_e *MockCatalogClient_Expecter) PublishVersion(context1 interface{}, s interface{}, s1 interface{}) *MockCatalogClient_PublishVersion_Call { + return &MockCatalogClient_PublishVersion_Call{Call: _e.mock.On("PublishVersion", context1, s, s1)} +} + +func (_c *MockCatalogClient_PublishVersion_Call) Run(run func(context1 context.Context, s string, s1 string)) *MockCatalogClient_PublishVersion_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 string + if args[1] != nil { + arg1 = args[1].(string) + } + var arg2 string + if args[2] != nil { + arg2 = args[2].(string) + } + run( + arg0, + arg1, + arg2, + ) + }) + return _c +} + +func (_c *MockCatalogClient_PublishVersion_Call) Return(imageVersion client.ImageVersion, err error) *MockCatalogClient_PublishVersion_Call { + _c.Call.Return(imageVersion, err) + return _c +} + +func (_c *MockCatalogClient_PublishVersion_Call) RunAndReturn(run func(context1 context.Context, s string, s1 string) (client.ImageVersion, error)) *MockCatalogClient_PublishVersion_Call { + _c.Call.Return(run) + return _c +} + +// PutAlias provides a mock function for the type MockCatalogClient +func (_mock *MockCatalogClient) PutAlias(context1 context.Context, s string, s1 string, putAliasRequest client.PutAliasRequest) (client.Alias, error) { + ret := _mock.Called(context1, s, s1, putAliasRequest) + + if len(ret) == 0 { + panic("no return value specified for PutAlias") + } + + var r0 client.Alias + var r1 error + if returnFunc, ok := ret.Get(0).(func(context.Context, string, string, client.PutAliasRequest) (client.Alias, error)); ok { + return returnFunc(context1, s, s1, putAliasRequest) + } + if returnFunc, ok := ret.Get(0).(func(context.Context, string, string, client.PutAliasRequest) client.Alias); ok { + r0 = returnFunc(context1, s, s1, putAliasRequest) + } else { + r0 = ret.Get(0).(client.Alias) + } + if returnFunc, ok := ret.Get(1).(func(context.Context, string, string, client.PutAliasRequest) error); ok { + r1 = returnFunc(context1, s, s1, putAliasRequest) + } else { + r1 = ret.Error(1) + } + return r0, r1 +} + +// MockCatalogClient_PutAlias_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'PutAlias' +type MockCatalogClient_PutAlias_Call struct { + *mock.Call +} + +// PutAlias is a helper method to define mock.On call +// - context1 context.Context +// - s string +// - s1 string +// - putAliasRequest client.PutAliasRequest +func (_e *MockCatalogClient_Expecter) PutAlias(context1 interface{}, s interface{}, s1 interface{}, putAliasRequest interface{}) *MockCatalogClient_PutAlias_Call { + return &MockCatalogClient_PutAlias_Call{Call: _e.mock.On("PutAlias", context1, s, s1, putAliasRequest)} +} + +func (_c *MockCatalogClient_PutAlias_Call) Run(run func(context1 context.Context, s string, s1 string, putAliasRequest client.PutAliasRequest)) *MockCatalogClient_PutAlias_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 string + if args[1] != nil { + arg1 = args[1].(string) + } + var arg2 string + if args[2] != nil { + arg2 = args[2].(string) + } + var arg3 client.PutAliasRequest + if args[3] != nil { + arg3 = args[3].(client.PutAliasRequest) + } + run( + arg0, + arg1, + arg2, + arg3, + ) + }) + return _c +} + +func (_c *MockCatalogClient_PutAlias_Call) Return(alias client.Alias, err error) *MockCatalogClient_PutAlias_Call { + _c.Call.Return(alias, err) + return _c +} + +func (_c *MockCatalogClient_PutAlias_Call) RunAndReturn(run func(context1 context.Context, s string, s1 string, putAliasRequest client.PutAliasRequest) (client.Alias, error)) *MockCatalogClient_PutAlias_Call { + _c.Call.Return(run) + return _c +} diff --git a/internal/publish/publish.go b/internal/publish/publish.go index 0d5e7d2..9960767 100644 --- a/internal/publish/publish.go +++ b/internal/publish/publish.go @@ -1,4 +1,4 @@ -// Package publish uploads built image artifacts to imgsrv. +// Package publish publishes built image artifacts to imgsrv. package publish import ( @@ -138,6 +138,16 @@ func (u *Uploader) UploadArtifact(ctx context.Context, artifact Artifact) (Resul if err != nil { return Result{}, fmt.Errorf("begin imgsrv upload for %s: %w", digest, err) } + if session.State == imgsrv.UploadStateReady { + return Result{ + Digest: digest, + UploadID: session.ID, + State: session.State, + }, nil + } + if stateErr := terminalStateError(session); stateErr != nil { + return Result{}, stateErr + } uploadID := session.ID.String() parts, err := u.putParts(ctx, file, uploadID, artifact.Size) diff --git a/internal/publish/publish_test.go b/internal/publish/publish_test.go index b846b36..572c5ff 100644 --- a/internal/publish/publish_test.go +++ b/internal/publish/publish_test.go @@ -79,6 +79,38 @@ func TestUploaderUploadsMultipartArtifact(t *testing.T) { assert.Equal(t, imgsrv.UploadStateCompleted, result.State) } +func TestUploaderSkipsMultipartUploadWhenDigestIsReady(t *testing.T) { + uploads := mocks.NewMockUploadsClient(t) + body := bytes.Repeat([]byte("a"), int(publish.MinPartSizeBytes)) + path := writePublishTestArtifact(t, "artifact.raw.gz", body) + filenameHint := "artifact.raw.gz" + uploads.EXPECT(). + BeginUpload(mock.Anything, imgsrv.BeginUploadRequest{ + ExpectedDigest: "sha256:abc123", + ExpectedSizeBytes: int64(len(body)), + FilenameHint: &filenameHint, + }). + Return(imgsrv.UploadSession{ID: "upload-1", State: imgsrv.UploadStateReady}, nil). + Once() + + uploader := newPublishTestUploader(t, uploads, publish.Options{ + PartSizeBytes: publish.MinPartSizeBytes, + Wait: true, + Timeout: time.Second, + PollInterval: time.Nanosecond, + }) + result, err := uploader.UploadArtifact(context.Background(), publish.Artifact{ + Path: path, + Size: int64(len(body)), + SHA256: "abc123", + }) + + require.NoError(t, err) + assert.Equal(t, imgsrv.Digest("sha256:abc123"), result.Digest) + assert.Equal(t, imgsrv.UploadID("upload-1"), result.UploadID) + assert.Equal(t, imgsrv.UploadStateReady, result.State) +} + func TestUploaderWaitsUntilReady(t *testing.T) { uploads := mocks.NewMockUploadsClient(t) body := bytes.Repeat([]byte("a"), int(publish.MinPartSizeBytes)) diff --git a/internal/publish/release.go b/internal/publish/release.go new file mode 100644 index 0000000..2ab5612 --- /dev/null +++ b/internal/publish/release.go @@ -0,0 +1,270 @@ +package publish + +import ( + "context" + "errors" + "fmt" + "net/http" + "strings" + + imgsrv "github.com/meigma/imgsrv/client" +) + +// CatalogClient is the imgsrv catalog operation seam used by the release publisher. +type CatalogClient interface { + CreateImage(context.Context, imgsrv.CreateImageRequest) (imgsrv.Image, error) + CreateDraftVersion(context.Context, string, imgsrv.CreateDraftVersionRequest) (imgsrv.ImageVersion, error) + AddArtifact(context.Context, string, string, imgsrv.AddArtifactRequest) (imgsrv.Artifact, error) + PublishVersion(context.Context, string, string) (imgsrv.ImageVersion, error) + PutAlias(context.Context, string, string, imgsrv.PutAliasRequest) (imgsrv.Alias, error) +} + +// Publisher publishes uploaded artifacts into imgsrv image versions. +type Publisher struct { + uploader *Uploader + catalog CatalogClient +} + +// ReleaseRequest describes one image release publication. +type ReleaseRequest struct { + ImageName string + ImageDescription string + Version string + Aliases []string + Artifacts []ReleaseArtifact +} + +// ReleaseArtifact describes one local artifact to upload and publish. +type ReleaseArtifact struct { + Key string + Variant string + LocalPath string + OperatingSystem string + Architecture string + Format imgsrv.ArtifactFormat + Digest string + Size int64 + MediaType string +} + +// ReleaseResult is the stable JSON result printed by publish. +type ReleaseResult struct { + Image string `json:"image"` + Version string `json:"version"` + State imgsrv.ImageVersionState `json:"state"` + Aliases []string `json:"aliases"` + Artifacts []PublishedReleaseArtifact `json:"artifacts"` +} + +// PublishedReleaseArtifact describes one artifact published into imgsrv. +type PublishedReleaseArtifact struct { + ArtifactKey string `json:"artifactKey"` + Variant string `json:"variant"` + LocalPath string `json:"localPath"` + ServerArtifactID string `json:"serverArtifactId"` + OperatingSystem string `json:"operatingSystem"` + Architecture string `json:"architecture"` + Format imgsrv.ArtifactFormat `json:"format"` + Digest imgsrv.Digest `json:"digest"` + Size int64 `json:"size"` + MediaType string `json:"mediaType"` +} + +type uploadedReleaseArtifact struct { + request ReleaseArtifact + upload Result +} + +// NewPublisher constructs a release publisher. +func NewPublisher(catalog CatalogClient, uploader *Uploader) (*Publisher, error) { + if catalog == nil { + return nil, errors.New("configure imgsrv publisher: catalog client is required") + } + if uploader == nil { + return nil, errors.New("configure imgsrv publisher: uploader is required") + } + + return &Publisher{ + uploader: uploader, + catalog: catalog, + }, nil +} + +// PublishRelease uploads artifacts, creates a draft version, publishes it, and moves aliases. +func (p *Publisher) PublishRelease(ctx context.Context, request ReleaseRequest) (ReleaseResult, error) { + if err := validateReleaseRequest(request); err != nil { + return ReleaseResult{}, err + } + + uploaded := make([]uploadedReleaseArtifact, 0, len(request.Artifacts)) + for _, artifact := range request.Artifacts { + result, err := p.uploader.UploadArtifact(ctx, uploadArtifact(artifact)) + if err != nil { + return ReleaseResult{}, err + } + if result.State != imgsrv.UploadStateReady { + return ReleaseResult{}, fmt.Errorf( + "publish release artifact %q: upload %s is %q; release publishing requires CAS-ready uploads", + artifact.Key, + result.UploadID, + result.State, + ) + } + uploaded = append(uploaded, uploadedReleaseArtifact{ + request: artifact, + upload: result, + }) + } + + if err := p.createImage(ctx, request); err != nil { + return ReleaseResult{}, err + } + + version, err := p.catalog.CreateDraftVersion(ctx, request.ImageName, imgsrv.CreateDraftVersionRequest{ + Version: request.Version, + }) + if err != nil { + return ReleaseResult{}, fmt.Errorf( + "create imgsrv draft version %s %s: %w", + request.ImageName, + request.Version, + err, + ) + } + + result := ReleaseResult{ + Image: request.ImageName, + Version: version.Version, + State: version.State, + Aliases: []string{}, + Artifacts: make([]PublishedReleaseArtifact, 0, len(uploaded)), + } + for _, artifact := range uploaded { + published, addErr := p.addArtifact(ctx, request, artifact) + if addErr != nil { + return ReleaseResult{}, addErr + } + result.Artifacts = append(result.Artifacts, published) + } + + publishedVersion, err := p.catalog.PublishVersion(ctx, request.ImageName, request.Version) + if err != nil { + return ReleaseResult{}, fmt.Errorf( + "publish imgsrv version %s %s: %w", + request.ImageName, + request.Version, + err, + ) + } + result.State = publishedVersion.State + + for _, alias := range request.Aliases { + if _, err := p.catalog.PutAlias(ctx, request.ImageName, alias, imgsrv.PutAliasRequest{ + Version: request.Version, + }); err != nil { + return result, fmt.Errorf( + "published imgsrv version %s %s but failed to set alias %q: %w", + request.ImageName, + request.Version, + alias, + err, + ) + } + result.Aliases = append(result.Aliases, alias) + } + + return result, nil +} + +func validateReleaseRequest(request ReleaseRequest) error { + if strings.TrimSpace(request.ImageName) == "" { + return errors.New("publish release: image name is required") + } + if strings.TrimSpace(request.Version) == "" { + return errors.New("publish release: version is required") + } + if len(request.Artifacts) == 0 { + return errors.New("publish release: at least one artifact is required") + } + for _, artifact := range request.Artifacts { + if strings.TrimSpace(artifact.LocalPath) == "" { + return errors.New("publish release: artifact path is required") + } + if strings.TrimSpace(artifact.Digest) == "" { + return fmt.Errorf("publish release artifact %q: digest is required", artifact.LocalPath) + } + if artifact.Size <= 0 { + return fmt.Errorf("publish release artifact %q: size must be positive", artifact.LocalPath) + } + } + + return nil +} + +func (p *Publisher) createImage(ctx context.Context, request ReleaseRequest) error { + _, err := p.catalog.CreateImage(ctx, imgsrv.CreateImageRequest{ + Name: request.ImageName, + Description: optionalString(request.ImageDescription), + }) + if err == nil || isConflict(err) { + return nil + } + + return fmt.Errorf("create imgsrv image %s: %w", request.ImageName, err) +} + +func (p *Publisher) addArtifact( + ctx context.Context, + request ReleaseRequest, + artifact uploadedReleaseArtifact, +) (PublishedReleaseArtifact, error) { + added, err := p.catalog.AddArtifact(ctx, request.ImageName, request.Version, imgsrv.AddArtifactRequest{ + OperatingSystem: artifact.request.OperatingSystem, + Architecture: artifact.request.Architecture, + Format: artifact.request.Format, + PrimaryBlobDigest: artifact.upload.Digest, + PrimaryBlobSizeBytes: artifact.request.Size, + PrimaryMediaType: artifact.request.MediaType, + }) + if err != nil { + return PublishedReleaseArtifact{}, fmt.Errorf( + "add imgsrv artifact %s to %s %s: %w", + artifact.request.Key, + request.ImageName, + request.Version, + err, + ) + } + + return PublishedReleaseArtifact{ + ArtifactKey: artifact.request.Key, + Variant: artifact.request.Variant, + LocalPath: artifact.request.LocalPath, + ServerArtifactID: added.ID.String(), + OperatingSystem: added.OperatingSystem, + Architecture: added.Architecture, + Format: added.Format, + Digest: added.PrimaryBlobDigest, + Size: added.PrimaryBlobSizeBytes, + MediaType: added.PrimaryMediaType, + }, nil +} + +func uploadArtifact(artifact ReleaseArtifact) Artifact { + return Artifact{ + Path: artifact.LocalPath, + Size: artifact.Size, + SHA256: artifact.Digest, + MediaType: artifact.MediaType, + } +} + +func isConflict(err error) bool { + var problem *imgsrv.ProblemError + if errors.As(err, &problem) && problem.HTTPStatus == http.StatusConflict { + return true + } + + var httpErr *imgsrv.HTTPError + return errors.As(err, &httpErr) && httpErr.StatusCode == http.StatusConflict +} diff --git a/internal/publish/release_test.go b/internal/publish/release_test.go new file mode 100644 index 0000000..244c19d --- /dev/null +++ b/internal/publish/release_test.go @@ -0,0 +1,313 @@ +package publish_test + +import ( + "bytes" + "context" + "errors" + "net/http" + "path/filepath" + "testing" + "time" + + imgsrv "github.com/meigma/imgsrv/client" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + + "github.com/meigma/imgcli/internal/publish" + "github.com/meigma/imgcli/internal/publish/mocks" +) + +func TestPublisherPublishesReleaseAndAliases(t *testing.T) { + uploads := mocks.NewMockUploadsClient(t) + catalog := mocks.NewMockCatalogClient(t) + artifactBody := bytes.Repeat([]byte("a"), int(publish.MinPartSizeBytes)) + artifactPath := writePublishTestArtifact(t, "artifact.raw.gz", artifactBody) + events := []string{} + + expectReadyUpload(t, uploads, artifactPath, int64(len(artifactBody)), "abc123", "application/gzip") + catalog.EXPECT(). + CreateImage(mock.Anything, imgsrv.CreateImageRequest{Name: "incusos"}). + Run(func(_ context.Context, _ imgsrv.CreateImageRequest) { events = append(events, "create-image") }). + Return(imgsrv.Image{}, &imgsrv.ProblemError{HTTPStatus: http.StatusConflict, Title: "Conflict"}). + Once() + catalog.EXPECT(). + CreateDraftVersion(mock.Anything, "incusos", imgsrv.CreateDraftVersionRequest{Version: "v1.0.0"}). + Run(func(_ context.Context, _ string, _ imgsrv.CreateDraftVersionRequest) { + events = append(events, "create-version") + }). + Return(imgsrv.ImageVersion{Version: "v1.0.0", State: imgsrv.ImageVersionStateDraft}, nil). + Once() + catalog.EXPECT(). + AddArtifact(mock.Anything, "incusos", "v1.0.0", imgsrv.AddArtifactRequest{ + OperatingSystem: "incusos", + Architecture: "x86_64", + Format: imgsrv.ArtifactFormatRawGZ, + PrimaryBlobDigest: "sha256:abc123", + PrimaryBlobSizeBytes: int64(len(artifactBody)), + PrimaryMediaType: "application/gzip", + }). + Run(func(_ context.Context, _ string, _ string, _ imgsrv.AddArtifactRequest) { + events = append(events, "add-artifact") + }). + Return(imgsrv.Artifact{ + ID: "artifact-1", + OperatingSystem: "incusos", + Architecture: "x86_64", + Format: imgsrv.ArtifactFormatRawGZ, + PrimaryBlobDigest: "sha256:abc123", + PrimaryBlobSizeBytes: int64(len(artifactBody)), + PrimaryMediaType: "application/gzip", + }, nil). + Once() + catalog.EXPECT(). + PublishVersion(mock.Anything, "incusos", "v1.0.0"). + Run(func(_ context.Context, _ string, _ string) { events = append(events, "publish-version") }). + Return(imgsrv.ImageVersion{Version: "v1.0.0", State: imgsrv.ImageVersionStatePublished}, nil). + Once() + catalog.EXPECT(). + PutAlias(mock.Anything, "incusos", "latest", imgsrv.PutAliasRequest{Version: "v1.0.0"}). + Run(func(_ context.Context, _ string, _ string, _ imgsrv.PutAliasRequest) { + events = append(events, "alias-latest") + }). + Return(imgsrv.Alias{Alias: "latest", Version: "v1.0.0"}, nil). + Once() + catalog.EXPECT(). + PutAlias(mock.Anything, "incusos", "prod", imgsrv.PutAliasRequest{Version: "v1.0.0"}). + Run(func(_ context.Context, _ string, _ string, _ imgsrv.PutAliasRequest) { + events = append(events, "alias-prod") + }). + Return(imgsrv.Alias{Alias: "prod", Version: "v1.0.0"}, nil). + Once() + + publisher := newReleaseTestPublisher(t, catalog, uploads) + result, err := publisher.PublishRelease(context.Background(), publish.ReleaseRequest{ + ImageName: "incusos", + Version: "v1.0.0", + Aliases: []string{"latest", "prod"}, + Artifacts: []publish.ReleaseArtifact{ + releaseTestArtifact(artifactPath, int64(len(artifactBody))), + }, + }) + + require.NoError(t, err) + assert.Equal(t, "incusos", result.Image) + assert.Equal(t, "v1.0.0", result.Version) + assert.Equal(t, imgsrv.ImageVersionStatePublished, result.State) + assert.Equal(t, []string{"latest", "prod"}, result.Aliases) + require.Len(t, result.Artifacts, 1) + assert.Equal(t, publish.PublishedReleaseArtifact{ + ArtifactKey: "root", + Variant: "default", + LocalPath: artifactPath, + ServerArtifactID: "artifact-1", + OperatingSystem: "incusos", + Architecture: "x86_64", + Format: imgsrv.ArtifactFormatRawGZ, + Digest: "sha256:abc123", + Size: int64(len(artifactBody)), + MediaType: "application/gzip", + }, result.Artifacts[0]) + assert.Equal(t, []string{ + "create-image", + "create-version", + "add-artifact", + "publish-version", + "alias-latest", + "alias-prod", + }, events) +} + +func TestPublisherFailsOnDraftVersionConflict(t *testing.T) { + uploads := mocks.NewMockUploadsClient(t) + catalog := mocks.NewMockCatalogClient(t) + artifactBody := bytes.Repeat([]byte("a"), int(publish.MinPartSizeBytes)) + artifactPath := writePublishTestArtifact(t, "artifact.raw.gz", artifactBody) + conflict := &imgsrv.ProblemError{HTTPStatus: http.StatusConflict, Title: "Conflict"} + + expectReadyUpload(t, uploads, artifactPath, int64(len(artifactBody)), "abc123", "application/gzip") + catalog.EXPECT(). + CreateImage(mock.Anything, imgsrv.CreateImageRequest{Name: "incusos"}). + Return(imgsrv.Image{Name: "incusos"}, nil). + Once() + catalog.EXPECT(). + CreateDraftVersion(mock.Anything, "incusos", imgsrv.CreateDraftVersionRequest{Version: "v1.0.0"}). + Return(imgsrv.ImageVersion{}, conflict). + Once() + + publisher := newReleaseTestPublisher(t, catalog, uploads) + result, err := publisher.PublishRelease(context.Background(), publish.ReleaseRequest{ + ImageName: "incusos", + Version: "v1.0.0", + Artifacts: []publish.ReleaseArtifact{ + releaseTestArtifact(artifactPath, int64(len(artifactBody))), + }, + }) + + require.Error(t, err) + require.ErrorContains(t, err, "create imgsrv draft version incusos v1.0.0") + require.ErrorIs(t, err, conflict) + assert.Empty(t, result) +} + +func TestPublisherFailsBeforeCatalogWhenUploadIsNotReady(t *testing.T) { + uploads := mocks.NewMockUploadsClient(t) + catalog := mocks.NewMockCatalogClient(t) + artifactBody := bytes.Repeat([]byte("a"), int(publish.MinPartSizeBytes)) + artifactPath := writePublishTestArtifact(t, "artifact.raw.gz", artifactBody) + mediaTypeHint := "application/gzip" + filenameHint := filepath.Base(artifactPath) + + uploads.EXPECT(). + BeginUpload(mock.Anything, imgsrv.BeginUploadRequest{ + ExpectedDigest: "sha256:abc123", + ExpectedSizeBytes: int64(len(artifactBody)), + MediaTypeHint: &mediaTypeHint, + FilenameHint: &filenameHint, + }). + Return(imgsrv.UploadSession{ID: "upload-1", State: imgsrv.UploadStateCreated}, nil). + Once() + uploads.EXPECT(). + PutUploadPart(mock.Anything, "upload-1", 1, mock.Anything, int64(len(artifactBody))). + Return(imgsrv.UploadPart{PartNumber: 1, ETag: "etag-1", SizeBytes: int64(len(artifactBody))}, nil). + Once() + uploads.EXPECT(). + CompleteUpload(mock.Anything, "upload-1", imgsrv.CompleteUploadRequest{ + Parts: []imgsrv.CompleteUploadPart{{Number: 1, ETag: "etag-1", SizeBytes: int64(len(artifactBody))}}, + }). + Return(imgsrv.UploadSession{ID: "upload-1", State: imgsrv.UploadStateCompleted}, nil). + Once() + + uploader := newPublishTestUploader(t, uploads, publish.Options{ + PartSizeBytes: publish.MinPartSizeBytes, + Wait: false, + }) + publisher, err := publish.NewPublisher(catalog, uploader) + require.NoError(t, err) + result, err := publisher.PublishRelease(context.Background(), publish.ReleaseRequest{ + ImageName: "incusos", + Version: "v1.0.0", + Artifacts: []publish.ReleaseArtifact{ + releaseTestArtifact(artifactPath, int64(len(artifactBody))), + }, + }) + + require.Error(t, err) + require.ErrorContains(t, err, `publish release artifact "root": upload upload-1 is "completed"`) + require.ErrorContains(t, err, "release publishing requires CAS-ready uploads") + assert.Empty(t, result) +} + +func TestPublisherSurfacesPartialAliasFailure(t *testing.T) { + uploads := mocks.NewMockUploadsClient(t) + catalog := mocks.NewMockCatalogClient(t) + artifactBody := bytes.Repeat([]byte("a"), int(publish.MinPartSizeBytes)) + artifactPath := writePublishTestArtifact(t, "artifact.raw.gz", artifactBody) + aliasErr := errors.New("policy denied") + + expectReadyUpload(t, uploads, artifactPath, int64(len(artifactBody)), "abc123", "application/gzip") + catalog.EXPECT(). + CreateImage(mock.Anything, imgsrv.CreateImageRequest{Name: "incusos"}). + Return(imgsrv.Image{Name: "incusos"}, nil). + Once() + catalog.EXPECT(). + CreateDraftVersion(mock.Anything, "incusos", imgsrv.CreateDraftVersionRequest{Version: "v1.0.0"}). + Return(imgsrv.ImageVersion{Version: "v1.0.0", State: imgsrv.ImageVersionStateDraft}, nil). + Once() + catalog.EXPECT(). + AddArtifact(mock.Anything, "incusos", "v1.0.0", mock.Anything). + Return(imgsrv.Artifact{ + ID: "artifact-1", + OperatingSystem: "incusos", + Architecture: "x86_64", + Format: imgsrv.ArtifactFormatRawGZ, + PrimaryBlobDigest: "sha256:abc123", + PrimaryBlobSizeBytes: int64(len(artifactBody)), + PrimaryMediaType: "application/gzip", + }, nil). + Once() + catalog.EXPECT(). + PublishVersion(mock.Anything, "incusos", "v1.0.0"). + Return(imgsrv.ImageVersion{Version: "v1.0.0", State: imgsrv.ImageVersionStatePublished}, nil). + Once() + catalog.EXPECT(). + PutAlias(mock.Anything, "incusos", "latest", imgsrv.PutAliasRequest{Version: "v1.0.0"}). + Return(imgsrv.Alias{Alias: "latest", Version: "v1.0.0"}, nil). + Once() + catalog.EXPECT(). + PutAlias(mock.Anything, "incusos", "prod", imgsrv.PutAliasRequest{Version: "v1.0.0"}). + Return(imgsrv.Alias{}, aliasErr). + Once() + + publisher := newReleaseTestPublisher(t, catalog, uploads) + result, err := publisher.PublishRelease(context.Background(), publish.ReleaseRequest{ + ImageName: "incusos", + Version: "v1.0.0", + Aliases: []string{"latest", "prod"}, + Artifacts: []publish.ReleaseArtifact{ + releaseTestArtifact(artifactPath, int64(len(artifactBody))), + }, + }) + + require.Error(t, err) + require.ErrorContains(t, err, `published imgsrv version incusos v1.0.0 but failed to set alias "prod"`) + require.ErrorIs(t, err, aliasErr) + assert.Equal(t, imgsrv.ImageVersionStatePublished, result.State) + assert.Equal(t, []string{"latest"}, result.Aliases) + require.Len(t, result.Artifacts, 1) +} + +func expectReadyUpload( + t *testing.T, + uploads *mocks.MockUploadsClient, + path string, + size int64, + sha256 string, + mediaType string, +) { + t.Helper() + + mediaTypeHint := mediaType + filenameHint := filepath.Base(path) + uploads.EXPECT(). + BeginUpload(mock.Anything, imgsrv.BeginUploadRequest{ + ExpectedDigest: imgsrv.Digest("sha256:" + sha256), + ExpectedSizeBytes: size, + MediaTypeHint: &mediaTypeHint, + FilenameHint: &filenameHint, + }). + Return(imgsrv.UploadSession{ID: "upload-1", State: imgsrv.UploadStateReady}, nil). + Once() +} + +func releaseTestArtifact(path string, size int64) publish.ReleaseArtifact { + return publish.ReleaseArtifact{ + Key: "root", + Variant: "default", + LocalPath: path, + OperatingSystem: "incusos", + Architecture: "x86_64", + Format: imgsrv.ArtifactFormatRawGZ, + Digest: "abc123", + Size: size, + MediaType: "application/gzip", + } +} + +func newReleaseTestPublisher( + t *testing.T, + catalog publish.CatalogClient, + uploads publish.UploadsClient, +) *publish.Publisher { + t.Helper() + + uploader := newPublishTestUploader(t, uploads, publish.Options{ + PartSizeBytes: publish.MinPartSizeBytes, + Wait: true, + Timeout: time.Second, + PollInterval: time.Nanosecond, + }) + publisher, err := publish.NewPublisher(catalog, uploader) + require.NoError(t, err) + return publisher +} diff --git a/moon.yml b/moon.yml index cb7881f..bf59d19 100644 --- a/moon.yml +++ b/moon.yml @@ -85,6 +85,17 @@ tasks: - '@group(go-config)' - '@group(go-sources)' + test-integration: + deps: + - 'root:schemas-generate-check' + command: 'go test -count 1 -tags integration ./internal/integration' + toolchains: ['go'] + inputs: + - '@group(go-config)' + - '@group(go-sources)' + options: + cache: false + schemas-mod-check: script: 'cd schemas && proto run cue -- mod tidy --check' inputs: @@ -137,6 +148,7 @@ tasks: - 'root:lint' - 'root:build' - 'root:test' + - 'root:test-integration' - 'root:schemas-mod-check' - 'root:schemas-vet' - 'root:schemas-test'