From 75a5fd0d0f243f6e4de27ee0d6536e2c212e8eb6 Mon Sep 17 00:00:00 2001 From: CMGS Date: Wed, 1 Jul 2026 11:40:44 +0800 Subject: [PATCH 1/8] refactor(snapshots): abstract the registry client behind a Registry interface MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Decouple vk from epoch's concrete *registryclient.Client so a standard-OCI backend can drop in (issue #25). The new snapshots.Registry composes epoch's snapshot Uploader/Downloader plus BaseURL and DeleteManifest; Pusher, Puller, blobReader and Provider hold the interface. No behavior change — main.go still wires the concrete client, which satisfies it. --- provider/cocoon/provider.go | 3 +-- snapshots/cloudimg.go | 9 ++++----- snapshots/puller.go | 7 +++---- snapshots/pusher.go | 3 +-- snapshots/registry.go | 17 +++++++++++++++++ 5 files changed, 26 insertions(+), 13 deletions(-) create mode 100644 snapshots/registry.go diff --git a/provider/cocoon/provider.go b/provider/cocoon/provider.go index 165a150..b18176f 100644 --- a/provider/cocoon/provider.go +++ b/provider/cocoon/provider.go @@ -21,7 +21,6 @@ import ( commonk8s "github.com/cocoonstack/cocoon-common/k8s" "github.com/cocoonstack/cocoon-common/meta" - "github.com/cocoonstack/epoch/registryclient" "github.com/cocoonstack/vk-cocoon/guest" "github.com/cocoonstack/vk-cocoon/metrics" "github.com/cocoonstack/vk-cocoon/network" @@ -87,7 +86,7 @@ type Provider struct { Runtime vm.Runtime Puller *snapshots.Puller Pusher *snapshots.Pusher - Registry *registryclient.Client + Registry snapshots.Registry LeaseParser *network.LeaseParser Pinger network.Pinger GuestRDP guest.Executor diff --git a/snapshots/cloudimg.go b/snapshots/cloudimg.go index 9f04792..bb45cd9 100644 --- a/snapshots/cloudimg.go +++ b/snapshots/cloudimg.go @@ -5,20 +5,19 @@ import ( "io" "github.com/cocoonstack/epoch/cloudimg" - "github.com/cocoonstack/epoch/registryclient" ) var _ cloudimg.BlobReader = blobReader{} -// blobReader adapts *registryclient.Client to cloudimg.BlobReader. +// blobReader adapts a Registry to cloudimg.BlobReader. type blobReader struct { - client *registryclient.Client - name string + registry Registry + name string } // ReadBlob fetches a blob by digest. func (b blobReader) ReadBlob(ctx context.Context, digest string) (io.ReadCloser, error) { - return b.client.GetBlob(ctx, b.name, digest) + return b.registry.GetBlob(ctx, b.name, digest) } // cloudimgStream wraps cloudimg.Stream. diff --git a/snapshots/puller.go b/snapshots/puller.go index 61bb60f..c734e1f 100644 --- a/snapshots/puller.go +++ b/snapshots/puller.go @@ -9,14 +9,13 @@ import ( "io" "github.com/cocoonstack/epoch/manifest" - "github.com/cocoonstack/epoch/registryclient" "github.com/cocoonstack/epoch/snapshot" "github.com/cocoonstack/vk-cocoon/vm" ) // Puller streams a snapshot or cloud image from epoch into the local cocoon runtime. type Puller struct { - Registry *registryclient.Client + Registry Registry Runtime vm.Runtime } @@ -57,7 +56,7 @@ func (p *Puller) PullCloudImage(ctx context.Context, name, tag string, w io.Writ if err != nil { return err } - adapter := blobReader{client: p.Registry, name: name} + adapter := blobReader{registry: p.Registry, name: name} return cloudimgStream(ctx, raw, adapter, w) } @@ -91,7 +90,7 @@ func (p *Puller) EnsureCloudImageFromRaw(ctx context.Context, name, localName st if err != nil { return fmt.Errorf("open cocoon image import: %w", err) } - adapter := blobReader{client: p.Registry, name: name} + adapter := blobReader{registry: p.Registry, name: name} if err := cloudimgStream(ctx, raw, adapter, importer); err != nil { _ = importer.Close() _ = wait() diff --git a/snapshots/pusher.go b/snapshots/pusher.go index 620b8bd..5f0c296 100644 --- a/snapshots/pusher.go +++ b/snapshots/pusher.go @@ -6,14 +6,13 @@ import ( "fmt" "github.com/cocoonstack/cocoon-common/meta" - "github.com/cocoonstack/epoch/registryclient" "github.com/cocoonstack/epoch/snapshot" "github.com/cocoonstack/vk-cocoon/vm" ) // Pusher streams a local snapshot up into epoch. type Pusher struct { - Registry *registryclient.Client + Registry Registry Runtime vm.Runtime } diff --git a/snapshots/registry.go b/snapshots/registry.go new file mode 100644 index 0000000..18a0333 --- /dev/null +++ b/snapshots/registry.go @@ -0,0 +1,17 @@ +package snapshots + +import ( + "context" + + "github.com/cocoonstack/epoch/snapshot" +) + +// Registry abstracts the OCI backend (vs epoch's concrete *registryclient.Client) +// so a standard-OCI implementation can drop in. Beyond snapshot Uploader/Downloader +// it needs BaseURL (portable pull URLs) and DeleteManifest (hibernate rollback). +type Registry interface { + snapshot.Uploader + snapshot.Downloader + BaseURL() string + DeleteManifest(ctx context.Context, name, reference string) error +} From 2d1391863ac19cb80b59bde7baa14b91e3145113 Mon Sep 17 00:00:00 2001 From: CMGS Date: Wed, 1 Jul 2026 11:55:00 +0800 Subject: [PATCH 2/8] feat(snapshots): add a standard-OCI Registry backend (go-containerregistry) OCIRegistry implements snapshots.Registry against any OCI Distribution registry (e.g. Artifact Registry) using standard upload sessions + keychain auth, so vk can drop epoch's bespoke server (issue #25). streamLayer pushes a multi-GB blob without buffering; rawManifest carries the custom artifactType bytes. Round-trip tested against an in-memory registry. --- go.mod | 11 ++- go.sum | 19 +++++ snapshots/oci.go | 162 ++++++++++++++++++++++++++++++++++++++++++ snapshots/oci_test.go | 77 ++++++++++++++++++++ 4 files changed, 268 insertions(+), 1 deletion(-) create mode 100644 snapshots/oci.go create mode 100644 snapshots/oci_test.go diff --git a/go.mod b/go.mod index 2ed9f3a..80f9821 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.25.6 require ( github.com/cocoonstack/cocoon-common v0.2.2 github.com/cocoonstack/epoch v0.2.4 + github.com/google/go-containerregistry v0.21.0 github.com/projecteru2/core v0.0.0-20241016125006-ff909eefe04c github.com/prometheus/client_golang v1.23.2 github.com/prometheus/client_model v0.6.2 @@ -31,9 +32,13 @@ require ( github.com/cockroachdb/errors v1.9.1 // indirect github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b // indirect github.com/cockroachdb/redact v1.1.3 // indirect + github.com/containerd/stargz-snapshotter/estargz v0.18.2 // indirect github.com/coreos/go-semver v0.3.1 // indirect github.com/coreos/go-systemd/v22 v22.5.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/docker/cli v29.2.1+incompatible // indirect + github.com/docker/distribution v2.8.3+incompatible // indirect + github.com/docker/docker-credential-helpers v0.9.3 // indirect github.com/emicklei/go-restful/v3 v3.12.2 // indirect github.com/evanphx/json-patch/v5 v5.9.11 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect @@ -67,21 +72,26 @@ require ( github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/moby/spdystream v0.5.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/opencontainers/image-spec v1.1.1 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/common v0.67.4 // indirect github.com/prometheus/procfs v0.16.1 // indirect github.com/rogpeppe/go-internal v1.14.1 // indirect github.com/rs/zerolog v1.29.1 // indirect + github.com/sirupsen/logrus v1.9.4 // indirect github.com/spf13/cobra v1.10.2 // indirect github.com/spf13/pflag v1.0.10 // indirect github.com/stoewer/go-strcase v1.3.0 // indirect + github.com/vbatts/tar-split v0.12.2 // indirect github.com/x448/float16 v0.8.4 // indirect go.etcd.io/etcd/api/v3 v3.6.5 // indirect go.etcd.io/etcd/client/pkg/v3 v3.6.5 // indirect @@ -108,7 +118,6 @@ require ( golang.org/x/term v0.40.0 // indirect golang.org/x/text v0.34.0 // indirect golang.org/x/time v0.14.0 // indirect - golang.org/x/tools v0.42.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a // indirect google.golang.org/grpc v1.72.2 // indirect diff --git a/go.sum b/go.sum index 9d730d6..8ddeca2 100644 --- a/go.sum +++ b/go.sum @@ -47,6 +47,8 @@ github.com/cocoonstack/cocoon-common v0.2.2/go.mod h1:xIXbJ83vngQ2mrLC6q0Tw7h21M github.com/cocoonstack/epoch v0.2.4 h1:X18DdsWlPRqEVswR1N3JAazxRZfBUtoL5r7GTgWmhN8= github.com/cocoonstack/epoch v0.2.4/go.mod h1:1bFeUom4aeFbw3vjFbrLegEgVShqe4qSOfpmH5UEpww= github.com/codegangsta/inject v0.0.0-20150114235600-33e0aa1cb7c0/go.mod h1:4Zcjuz89kmFXt9morQgcfYZAYZ5n8WHjt81YYWIwtTM= +github.com/containerd/stargz-snapshotter/estargz v0.18.2 h1:yXkZFYIzz3eoLwlTUZKz2iQ4MrckBxJjkmD16ynUTrw= +github.com/containerd/stargz-snapshotter/estargz v0.18.2/go.mod h1:XyVU5tcJ3PRpkA9XS2T5us6Eg35yM0214Y+wvrZTBrY= github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= @@ -63,6 +65,12 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgraph-io/badger v1.6.0/go.mod h1:zwt7syl517jmP8s94KqSxTlM6IMsdhYy6psNgSztDR4= github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= +github.com/docker/cli v29.2.1+incompatible h1:n3Jt0QVCN65eiVBoUTZQM9mcQICCJt3akW4pKAbKdJg= +github.com/docker/cli v29.2.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk= +github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= +github.com/docker/docker-credential-helpers v0.9.3 h1:gAm/VtF9wgqJMoxzT3Gj5p4AqIjCBS4wrsOh9yRqcz8= +github.com/docker/docker-credential-helpers v0.9.3/go.mod h1:x+4Gbw9aGmChi3qTLZj8Dfn0TD20M/fuWy0E5+WDeCo= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= 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= @@ -164,6 +172,8 @@ github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/go-containerregistry v0.21.0 h1:ocqxUOczFwAZQBMNE7kuzfqvDe0VWoZxQMOesXreCDI= +github.com/google/go-containerregistry v0.21.0/go.mod h1:ctO5aCaewH4AK1AumSF5DPW+0+R+d2FmylMJdp5G7p0= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8= @@ -254,6 +264,7 @@ github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D github.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw= github.com/mediocregopher/radix/v3 v3.4.2/go.mod h1:8FL3F6UQRXHXIBSPUs5h0RybMF8i4n7wVopoX3x7Bv8= github.com/microcosm-cc/bluemonday v1.0.2/go.mod h1:iVP4YcDBq+n/5fb23BhYFvIMq/leAFZyRl6bYmGDlGc= +github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= @@ -285,6 +296,10 @@ github.com/onsi/ginkgo/v2 v2.27.2/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zw github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A= github.com/onsi/gomega v1.38.2/go.mod h1:W2MJcYxRGV63b418Ai34Ud0hEdTVXq9NW9+Sx6uXf3k= +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 v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4= github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8= @@ -366,6 +381,8 @@ github.com/valyala/fasthttp v1.6.0/go.mod h1:FstJa9V+Pj9vQ7OJie2qMHdwemEDaDiSdBn github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio= +github.com/vbatts/tar-split v0.12.2 h1:w/Y6tjxpeiFMR47yzZPlPj/FcPLpXbTUi/9H7d3CPa4= +github.com/vbatts/tar-split v0.12.2/go.mod h1:eF6B6i6ftWQcDqEn3/iGFRFRo8cBIMSJVOpnNdfTMFA= github.com/virtual-kubelet/virtual-kubelet v1.12.0 h1:6RnRE3egGnqw3BDL9PBbP5DPV6OaXC2h/nfq5c7VsF4= github.com/virtual-kubelet/virtual-kubelet v1.12.0/go.mod h1:dVlVEsFfrrwAcj/v0eDGgkTF5r+eAsBnn9gDxx3au2s= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= @@ -604,6 +621,8 @@ 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 v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= +gotest.tools/v3 v3.4.0 h1:ZazjZUfuVeZGLAmlKKuyv3IKP5orXcwtOwDQH6YVr6o= +gotest.tools/v3 v3.4.0/go.mod h1:CtbdzLSsqVhDgMtKsx03ird5YTGB3ar27v0u/yKBW5g= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= k8s.io/api v0.35.3 h1:pA2fiBc6+N9PDf7SAiluKGEBuScsTzd2uYBkA5RzNWQ= diff --git a/snapshots/oci.go b/snapshots/oci.go new file mode 100644 index 0000000..4bc6e3d --- /dev/null +++ b/snapshots/oci.go @@ -0,0 +1,162 @@ +package snapshots + +import ( + "context" + "errors" + "fmt" + "io" + "net/http" + "strings" + + "github.com/google/go-containerregistry/pkg/authn" + "github.com/google/go-containerregistry/pkg/name" + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/remote" + "github.com/google/go-containerregistry/pkg/v1/remote/transport" + "github.com/google/go-containerregistry/pkg/v1/types" +) + +var ( + _ Registry = (*OCIRegistry)(nil) + + // errBlobUncompressed guards the DiffID/Uncompressed accessors: cocoon blobs + // are opaque content-addressed bytes and WriteLayer only reads Compressed(). + errBlobUncompressed = errors.New("cocoon blob layers expose only compressed bytes") +) + +// OCIRegistry is a Registry backed by a standard OCI Distribution registry +// (e.g. Artifact Registry), using OCI upload sessions and keychain auth. +type OCIRegistry struct { + base string // registry host + repo prefix, e.g. "asia-docker.pkg.dev/proj/repo" + opts []remote.Option +} + +// NewOCIRegistry roots a client at base, authenticating via keychain (use +// authn.DefaultKeychain for docker config / GCP application-default credentials). +func NewOCIRegistry(base string, keychain authn.Keychain) *OCIRegistry { + return &OCIRegistry{base: base, opts: []remote.Option{remote.WithAuthFromKeychain(keychain)}} +} + +// BaseURL returns the registry root. +func (r *OCIRegistry) BaseURL() string { return r.base } + +// GetManifest fetches the raw manifest bytes and media type at repo:tag. +func (r *OCIRegistry) GetManifest(ctx context.Context, repo, tag string) ([]byte, string, error) { + ref, err := name.ParseReference(r.base + "/" + repo + ":" + tag) + if err != nil { + return nil, "", fmt.Errorf("parse ref %s:%s: %w", repo, tag, err) + } + desc, err := remote.Get(ref, r.callOpts(ctx)...) + if err != nil { + return nil, "", fmt.Errorf("get manifest %s:%s: %w", repo, tag, err) + } + return desc.Manifest, string(desc.MediaType), nil +} + +// GetBlob streams the blob at the given digest. +func (r *OCIRegistry) GetBlob(ctx context.Context, repo, digest string) (io.ReadCloser, error) { + ref, err := name.NewDigest(r.base + "/" + repo + "@" + digest) + if err != nil { + return nil, fmt.Errorf("parse digest %s@%s: %w", repo, digest, err) + } + layer, err := remote.Layer(ref, r.callOpts(ctx)...) + if err != nil { + return nil, fmt.Errorf("get blob %s@%s: %w", repo, digest, err) + } + return layer.Compressed() +} + +// BlobExists reports whether the blob is already present, so pushes can skip it. +func (r *OCIRegistry) BlobExists(ctx context.Context, repo, digest string) (bool, error) { + ref, err := name.NewDigest(r.base + "/" + repo + "@" + digest) + if err != nil { + return false, fmt.Errorf("parse digest %s@%s: %w", repo, digest, err) + } + // remote.Layer is lazy; Size() issues the HEAD that reveals whether it exists. + layer, err := remote.Layer(ref, r.callOpts(ctx)...) + if err == nil { + _, err = layer.Size() + } + if err == nil { + return true, nil + } + var terr *transport.Error + if errors.As(err, &terr) && terr.StatusCode == http.StatusNotFound { + return false, nil + } + return false, fmt.Errorf("head blob %s@%s: %w", repo, digest, err) +} + +// PutBlob uploads a blob of the given digest/size via a standard upload session. +func (r *OCIRegistry) PutBlob(ctx context.Context, repo, digest string, body io.Reader, size int64) error { + repoRef, err := name.NewRepository(r.base + "/" + repo) + if err != nil { + return fmt.Errorf("parse repo %s: %w", repo, err) + } + hash, err := v1.NewHash(digest) + if err != nil { + return fmt.Errorf("parse digest %s: %w", digest, err) + } + if err := remote.WriteLayer(repoRef, &streamLayer{hash: hash, size: size, body: body}, r.callOpts(ctx)...); err != nil { + return fmt.Errorf("put blob %s@%s: %w", repo, digest, err) + } + return nil +} + +// PutManifest uploads a manifest at repo:tag with the given content type. +func (r *OCIRegistry) PutManifest(ctx context.Context, repo, tag string, data []byte, contentType string) error { + ref, err := name.ParseReference(r.base + "/" + repo + ":" + tag) + if err != nil { + return fmt.Errorf("parse ref %s:%s: %w", repo, tag, err) + } + if err := remote.Put(ref, rawManifest{data: data, mediaType: types.MediaType(contentType)}, r.callOpts(ctx)...); err != nil { + return fmt.Errorf("put manifest %s:%s: %w", repo, tag, err) + } + return nil +} + +// DeleteManifest removes the manifest at repo:reference (tag or digest). +func (r *OCIRegistry) DeleteManifest(ctx context.Context, repo, reference string) error { + // A digest (sha256:...) joins the repo with '@'; a tag with ':'. + sep := ":" + if strings.ContainsRune(reference, ':') { + sep = "@" + } + ref, err := name.ParseReference(r.base + "/" + repo + sep + reference) + if err != nil { + return fmt.Errorf("parse ref %s%s%s: %w", repo, sep, reference, err) + } + if err := remote.Delete(ref, r.callOpts(ctx)...); err != nil { + return fmt.Errorf("delete manifest %s: %w", reference, err) + } + return nil +} + +func (r *OCIRegistry) callOpts(ctx context.Context) []remote.Option { + return append(r.opts, remote.WithContext(ctx)) +} + +// streamLayer is a v1.Layer over a body with a known digest and size, so PutBlob +// streams a raw blob without buffering it (WriteLayer reads only Compressed()). +// body is single-use: a retried upload fails the digest check, not corrupts. +type streamLayer struct { + hash v1.Hash + size int64 + body io.Reader +} + +func (l *streamLayer) Digest() (v1.Hash, error) { return l.hash, nil } +func (l *streamLayer) Size() (int64, error) { return l.size, nil } +func (l *streamLayer) Compressed() (io.ReadCloser, error) { return io.NopCloser(l.body), nil } +func (l *streamLayer) MediaType() (types.MediaType, error) { return types.OCILayer, nil } +func (l *streamLayer) DiffID() (v1.Hash, error) { return v1.Hash{}, errBlobUncompressed } +func (l *streamLayer) Uncompressed() (io.ReadCloser, error) { return nil, errBlobUncompressed } + +// rawManifest is a remote.Taggable over pre-serialized manifest bytes. +type rawManifest struct { + data []byte + mediaType types.MediaType +} + +func (m rawManifest) RawManifest() ([]byte, error) { return m.data, nil } +func (m rawManifest) MediaType() (types.MediaType, error) { return m.mediaType, nil } diff --git a/snapshots/oci_test.go b/snapshots/oci_test.go new file mode 100644 index 0000000..47292e8 --- /dev/null +++ b/snapshots/oci_test.go @@ -0,0 +1,77 @@ +package snapshots + +import ( + "bytes" + "context" + "crypto/sha256" + "encoding/hex" + "io" + "net/http/httptest" + "strconv" + "strings" + "testing" + + "github.com/google/go-containerregistry/pkg/authn" + "github.com/google/go-containerregistry/pkg/registry" +) + +// TestOCIRegistryRoundTrip exercises the full Registry surface against an +// in-memory OCI registry: a blob and a custom-artifactType manifest survive a +// put -> exists -> get -> delete round trip. +func TestOCIRegistryRoundTrip(t *testing.T) { + srv := httptest.NewServer(registry.New()) + t.Cleanup(srv.Close) + + r := NewOCIRegistry(strings.TrimPrefix(srv.URL, "http://")+"/cocoon", authn.DefaultKeychain) + ctx := context.Background() + + blob := []byte("hello cocoon blob") + sum := sha256.Sum256(blob) + digest := "sha256:" + hex.EncodeToString(sum[:]) + + if ok, err := r.BlobExists(ctx, "myvm", digest); err != nil || ok { + t.Fatalf("BlobExists before put = (%v, %v), want (false, nil)", ok, err) + } + if err := r.PutBlob(ctx, "myvm", digest, bytes.NewReader(blob), int64(len(blob))); err != nil { + t.Fatalf("PutBlob: %v", err) + } + if ok, err := r.BlobExists(ctx, "myvm", digest); err != nil || !ok { + t.Fatalf("BlobExists after put = (%v, %v), want (true, nil)", ok, err) + } + + rc, err := r.GetBlob(ctx, "myvm", digest) + if err != nil { + t.Fatalf("GetBlob: %v", err) + } + got, _ := io.ReadAll(rc) + _ = rc.Close() + if !bytes.Equal(got, blob) { + t.Fatalf("GetBlob = %q, want %q", got, blob) + } + + const mt = "application/vnd.oci.image.manifest.v1+json" + mf := []byte(`{"schemaVersion":2,"mediaType":"` + mt + + `","artifactType":"application/vnd.cocoonstack.snapshot.v1+json","config":{"mediaType":` + + `"application/vnd.cocoonstack.snapshot.config.v1+json","digest":"` + digest + + `","size":` + strconv.Itoa(len(blob)) + `},"layers":[]}`) + if err := r.PutManifest(ctx, "myvm", "hibernate", mf, mt); err != nil { + t.Fatalf("PutManifest: %v", err) + } + raw, gotMT, err := r.GetManifest(ctx, "myvm", "hibernate") + if err != nil { + t.Fatalf("GetManifest: %v", err) + } + if !bytes.Equal(raw, mf) { + t.Fatalf("GetManifest bytes mismatch") + } + if gotMT != mt { + t.Fatalf("GetManifest mediaType = %q, want %q", gotMT, mt) + } + + if err := r.DeleteManifest(ctx, "myvm", "hibernate"); err != nil { + t.Fatalf("DeleteManifest: %v", err) + } + if _, _, err := r.GetManifest(ctx, "myvm", "hibernate"); err == nil { + t.Fatal("GetManifest after delete: want error, got nil") + } +} From 60ea5805235f9d20dac02a7df061374995e0d537 Mon Sep 17 00:00:00 2001 From: CMGS Date: Wed, 1 Jul 2026 12:03:52 +0800 Subject: [PATCH 3/8] refactor(provider): drop the epoch /dl/ endpoint; boot cloud images from local import ensureRunImage now imports a cloud-image artifact and returns a local ref (repo:tag) instead of rewriting to /dl/{repo}/{tag}, so booting works against any standard OCI registry, not just epoch's server (issue #25). Removes canonicalCloudImgURL and BaseURL (its only user) from the Registry interface and OCIRegistry. --- provider/cocoon/create.go | 14 ++++---------- provider/cocoon/create_test.go | 21 ++++++++++----------- snapshots/oci.go | 3 --- snapshots/registry.go | 3 +-- 4 files changed, 15 insertions(+), 26 deletions(-) diff --git a/provider/cocoon/create.go b/provider/cocoon/create.go index 94fffed..66eb406 100644 --- a/provider/cocoon/create.go +++ b/provider/cocoon/create.go @@ -232,8 +232,8 @@ func (p *Provider) bringUpVM(ctx context.Context, pod *corev1.Pod, spec meta.VMS } // ensureRunImage materializes the base image locally and returns the ref -// `cocoon vm run` should be invoked with. Cloud-image refs canonicalize to -// the /dl/{repo}/{tag} URL so vmCfg.Image is portable across nodes. +// `cocoon vm run` should be invoked with. A cloud-image artifact is imported +// into the local store and booted from there (repo:tag), not the registry. func (p *Provider) ensureRunImage(ctx context.Context, image string, force bool) (string, error) { if image == "" { return image, nil @@ -253,8 +253,8 @@ func (p *Provider) ensureRunImage(ctx context.Context, image string, force bool) } switch kind { case manifest.KindCloudImage: - canonical := canonicalCloudImgURL(p.Puller.Registry.BaseURL(), repo, tag) - return canonical, p.Puller.EnsureCloudImageFromRaw(ctx, repo, canonical, raw, force) + local := repo + ":" + tag + return local, p.Puller.EnsureCloudImageFromRaw(ctx, repo, local, raw, force) case manifest.KindSnapshot: return image, fmt.Errorf("image %s is a snapshot artifact; use mode=clone instead of mode=run", image) default: @@ -414,12 +414,6 @@ func assertSnapshotBackend(snapshot *vm.Snapshot, targetBackend string) error { snapshot.Name, snapshot.Hypervisor, targetBackend) } -// canonicalCloudImgURL builds the /dl/{repo}/{tag} URL cocoon's cloudimg -// backend can pull via plain http.Get. -func canonicalCloudImgURL(baseURL, repo, tag string) string { - return fmt.Sprintf("%s/dl/%s/%s", strings.TrimRight(baseURL, "/"), repo, tag) -} - // isHTTPURL reports whether ref looks like an HTTP(S) cloud-image URL. func isHTTPURL(ref string) bool { return strings.HasPrefix(ref, "http://") || strings.HasPrefix(ref, "https://") diff --git a/provider/cocoon/create_test.go b/provider/cocoon/create_test.go index 7b110c1..b5a0bed 100644 --- a/provider/cocoon/create_test.go +++ b/provider/cocoon/create_test.go @@ -940,12 +940,12 @@ func TestEnsureRunImageDispatch(t *testing.T) { if err != nil { t.Fatalf("registryclient.New: %v", err) } - // Cloudimg path imports under the canonical /dl URL, so the + // Cloudimg path imports under the local ref (repo:tag), so the // fake runtime's Image lookup must key on the same form. - wantURL := canonicalCloudImgURL(srv.URL, repo, "latest") + wantRef := repo + ":latest" rt := &fakeRuntime{} if tc.imagesPresent { - rt.imagesPresent = map[string]bool{wantURL: true} + rt.imagesPresent = map[string]bool{wantRef: true} } p := newTestProvider(t) p.Runtime = rt @@ -970,15 +970,14 @@ func TestEnsureRunImageDispatch(t *testing.T) { if len(rt.ensuredImages) != 0 { t.Fatalf("Puller path should not shell EnsureImage, got %v", rt.ensuredImages) } - // Cloudimg path returns the canonical URL so vmCfg.Image - // stays portable across nodes. - if got != wantURL { - t.Fatalf("ensureRunImage returned %q, want %q", got, wantURL) + // Cloudimg path returns the local ref it imported under. + if got != wantRef { + t.Fatalf("ensureRunImage returned %q, want %q", got, wantRef) } - // Import name must match the returned URL so the subsequent - // `cocoon vm run` finds the blob. - if len(rt.imageInspectCalls) == 0 || rt.imageInspectCalls[0] != wantURL { - t.Fatalf("Puller imported under %v, want first inspect on %q", rt.imageInspectCalls, wantURL) + // Import name must match the returned ref so the subsequent + // `cocoon vm run` finds the local image. + if len(rt.imageInspectCalls) == 0 || rt.imageInspectCalls[0] != wantRef { + t.Fatalf("Puller imported under %v, want first inspect on %q", rt.imageInspectCalls, wantRef) } default: // fallthrough to Runtime.EnsureImage if err != nil { diff --git a/snapshots/oci.go b/snapshots/oci.go index 4bc6e3d..98826a0 100644 --- a/snapshots/oci.go +++ b/snapshots/oci.go @@ -37,9 +37,6 @@ func NewOCIRegistry(base string, keychain authn.Keychain) *OCIRegistry { return &OCIRegistry{base: base, opts: []remote.Option{remote.WithAuthFromKeychain(keychain)}} } -// BaseURL returns the registry root. -func (r *OCIRegistry) BaseURL() string { return r.base } - // GetManifest fetches the raw manifest bytes and media type at repo:tag. func (r *OCIRegistry) GetManifest(ctx context.Context, repo, tag string) ([]byte, string, error) { ref, err := name.ParseReference(r.base + "/" + repo + ":" + tag) diff --git a/snapshots/registry.go b/snapshots/registry.go index 18a0333..1a290af 100644 --- a/snapshots/registry.go +++ b/snapshots/registry.go @@ -8,10 +8,9 @@ import ( // Registry abstracts the OCI backend (vs epoch's concrete *registryclient.Client) // so a standard-OCI implementation can drop in. Beyond snapshot Uploader/Downloader -// it needs BaseURL (portable pull URLs) and DeleteManifest (hibernate rollback). +// it needs DeleteManifest for hibernate rollback. type Registry interface { snapshot.Uploader snapshot.Downloader - BaseURL() string DeleteManifest(ctx context.Context, name, reference string) error } From ef20d0936acb05b7622d43e3f1e3b60aa57f6fb2 Mon Sep 17 00:00:00 2001 From: CMGS Date: Wed, 1 Jul 2026 12:17:38 +0800 Subject: [PATCH 4/8] feat: select the registry backend via OCI_REGISTRY (OCI or epoch) buildRegistry returns a standard-OCI client (OCI_REGISTRY set) or epoch's client, so vk can run against Artifact Registry without epoch (issue #25). The OCI keychain resolves GCP ADC (google.Keychain) then docker config, so GCE nodes authenticate to AR with no bundled credential helper. --- go.mod | 3 +++ go.sum | 3 ++- main.go | 27 ++++++++++++++++++++++++++- main_test.go | 28 ++++++++++++++++++++++++++++ snapshots/oci.go | 4 ++-- 5 files changed, 61 insertions(+), 4 deletions(-) create mode 100644 main_test.go diff --git a/go.mod b/go.mod index 80f9821..e861bf5 100644 --- a/go.mod +++ b/go.mod @@ -22,6 +22,7 @@ require ( require ( cel.dev/expr v0.24.0 // indirect + cloud.google.com/go/compute/metadata v0.7.0 // indirect github.com/NYTimes/gziphandler v1.1.1 // indirect github.com/alphadose/haxmap v1.2.0 // indirect github.com/antlr4-go/antlr/v4 v4.13.0 // indirect @@ -137,3 +138,5 @@ require ( sigs.k8s.io/structured-merge-diff/v6 v6.3.2-0.20260122202528-d9cc6641c482 // indirect sigs.k8s.io/yaml v1.6.0 // indirect ) + +exclude cloud.google.com/go v0.26.0 diff --git a/go.sum b/go.sum index 8ddeca2..2ccba1e 100644 --- a/go.sum +++ b/go.sum @@ -1,6 +1,7 @@ cel.dev/expr v0.24.0 h1:56OvJKSH3hDGL0ml5uSxZmz3/3Pq4tJ+fb1unVLAFcY= cel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw= -cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go/compute/metadata v0.7.0 h1:PBWF+iiAerVNe8UCHxdOt6eHLVc3ydFeOCw78U8ytSU= +cloud.google.com/go/compute/metadata v0.7.0/go.mod h1:j5MvL9PprKL39t166CoB1uVHfQMs4tFQZZcKwksXUjo= github.com/AndreasBriese/bbloom v0.0.0-20190306092124-e2d15f34fcf9/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53/go.mod h1:+3IMCy2vIlbG1XG/0ggNQv0SvxCAIpPM5b1nCz56Xno= diff --git a/main.go b/main.go index 5989c84..c303dac 100644 --- a/main.go +++ b/main.go @@ -14,6 +14,8 @@ import ( "syscall" "time" + "github.com/google/go-containerregistry/pkg/authn" + "github.com/google/go-containerregistry/pkg/v1/google" "github.com/projecteru2/core/log" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promhttp" @@ -72,6 +74,7 @@ func main() { metricsAddr := commonk8s.EnvOrDefault("VK_METRICS_ADDR", defaultMetricsAddr) epochURL := commonk8s.EnvOrDefault("EPOCH_URL", defaultEpochURL) epochToken := os.Getenv("EPOCH_TOKEN") + ociRegistry := os.Getenv("OCI_REGISTRY") leasesPath := commonk8s.EnvOrDefault("VK_LEASES_PATH", network.DefaultLeasesPath) cocoonBin := commonk8s.EnvOrDefault("VK_COCOON_BIN", "") orphanPolicy := commonk8s.EnvOrDefault("VK_ORPHAN_POLICY", defaultOrphanPolicy) @@ -120,6 +123,7 @@ func main() { nodeName: nodeName, epochURL: epochURL, epochToken: epochToken, + ociRegistry: ociRegistry, leasesPath: leasesPath, cocoonBin: cocoonBin, orphanPolicy: orphanPolicy, @@ -216,6 +220,7 @@ type buildOpts struct { nodeName string epochURL string epochToken string + ociRegistry string leasesPath string cocoonBin string orphanPolicy string @@ -223,12 +228,32 @@ type buildOpts struct { recorder record.EventRecorder } +// buildRegistry selects the registry backend: an OCI Distribution registry +// when OCI_REGISTRY is set, else epoch's server. The OCI keychain resolves GCP +// ADC (google.Keychain) before falling back to docker config. +func buildRegistry(opts buildOpts) (snapshots.Registry, error) { + if opts.ociRegistry != "" { + keychain := authn.NewMultiKeychain(google.Keychain, authn.DefaultKeychain) + return snapshots.NewOCIRegistry(opts.ociRegistry, keychain), nil + } + client, err := registryclient.NewFromEnv(opts.epochURL, opts.epochToken) + if err != nil { + return nil, err + } + return client, nil +} + func buildProvider(ctx context.Context, opts buildOpts) (*cocoon.Provider, error) { logger := log.WithFunc("buildProvider") - registry, err := registryclient.NewFromEnv(opts.epochURL, opts.epochToken) + registry, err := buildRegistry(opts) if err != nil { return nil, fmt.Errorf("construct registry client: %w", err) } + backend := "epoch " + opts.epochURL + if opts.ociRegistry != "" { + backend = "OCI " + opts.ociRegistry + } + logger.Infof(ctx, "registry backend: %s", backend) runtime := vm.NewCocoonCLI(opts.cocoonBin) p := cocoon.NewProvider() p.NodeName = opts.nodeName diff --git a/main_test.go b/main_test.go new file mode 100644 index 0000000..864c6d4 --- /dev/null +++ b/main_test.go @@ -0,0 +1,28 @@ +package main + +import ( + "testing" + + "github.com/cocoonstack/epoch/registryclient" + "github.com/cocoonstack/vk-cocoon/snapshots" +) + +func TestBuildRegistryBackend(t *testing.T) { + t.Setenv("EPOCH_CA_CERT", "") // keep the epoch client deterministic on dev machines + + oci, err := buildRegistry(buildOpts{ociRegistry: "example.com/proj/repo"}) + if err != nil { + t.Fatalf("buildRegistry(OCI): %v", err) + } + if _, ok := oci.(*snapshots.OCIRegistry); !ok { + t.Fatalf("OCI_REGISTRY set: got %T, want *snapshots.OCIRegistry", oci) + } + + ep, err := buildRegistry(buildOpts{epochURL: "http://epoch.example"}) + if err != nil { + t.Fatalf("buildRegistry(epoch): %v", err) + } + if _, ok := ep.(*registryclient.Client); !ok { + t.Fatalf("no OCI_REGISTRY: got %T, want *registryclient.Client", ep) + } +} diff --git a/snapshots/oci.go b/snapshots/oci.go index 98826a0..c9fbc20 100644 --- a/snapshots/oci.go +++ b/snapshots/oci.go @@ -31,8 +31,8 @@ type OCIRegistry struct { opts []remote.Option } -// NewOCIRegistry roots a client at base, authenticating via keychain (use -// authn.DefaultKeychain for docker config / GCP application-default credentials). +// NewOCIRegistry roots a client at base, authenticating via keychain (e.g. +// authn.DefaultKeychain, or a MultiKeychain with google.Keychain for GCP AR). func NewOCIRegistry(base string, keychain authn.Keychain) *OCIRegistry { return &OCIRegistry{base: base, opts: []remote.Option{remote.WithAuthFromKeychain(keychain)}} } From df4fbba076232640504fc7c2699aad1e427c773a Mon Sep 17 00:00:00 2001 From: CMGS Date: Wed, 1 Jul 2026 12:38:45 +0800 Subject: [PATCH 5/8] refactor(snapshots): remove unused cloud-image pull helpers PullCloudImage, EnsureCloudImage and their fetchCloudImageManifest helper had no callers once the boot path settled on EnsureCloudImageFromRaw; drop them plus the now-unused io/manifest imports, and repoint two stale comments. --- snapshots/puller.go | 42 ++---------------------------------------- vm/cocoon_cli.go | 2 +- vm/runtime.go | 4 ++-- 3 files changed, 5 insertions(+), 43 deletions(-) diff --git a/snapshots/puller.go b/snapshots/puller.go index c734e1f..2039d76 100644 --- a/snapshots/puller.go +++ b/snapshots/puller.go @@ -1,4 +1,5 @@ -// Package snapshots wraps the epoch SDK for pulling and pushing cocoon VM snapshots. +// Package snapshots pulls and pushes cocoon VM snapshots and cloud images +// to an OCI registry. package snapshots import ( @@ -6,9 +7,7 @@ import ( "context" "errors" "fmt" - "io" - "github.com/cocoonstack/epoch/manifest" "github.com/cocoonstack/epoch/snapshot" "github.com/cocoonstack/vk-cocoon/vm" ) @@ -50,26 +49,6 @@ func (p *Puller) PullSnapshot(ctx context.Context, name, tag, localName string) return nil } -// PullCloudImage fetches a cloud image manifest and writes raw disk bytes to w. -func (p *Puller) PullCloudImage(ctx context.Context, name, tag string, w io.Writer) error { - raw, err := p.fetchCloudImageManifest(ctx, name, tag) - if err != nil { - return err - } - adapter := blobReader{registry: p.Registry, name: name} - return cloudimgStream(ctx, raw, adapter, w) -} - -// EnsureCloudImage streams a cloud-image artifact from epoch into `cocoon image -// import `. force=true bypasses the local-cache short-circuit. -func (p *Puller) EnsureCloudImage(ctx context.Context, name, tag, localName string, force bool) error { - raw, err := p.fetchCloudImageManifest(ctx, name, tag) - if err != nil { - return err - } - return p.EnsureCloudImageFromRaw(ctx, name, localName, raw, force) -} - // EnsureCloudImageFromRaw streams raw into `cocoon image import `. // Caller MUST have classified raw as KindCloudImage; this skips the fetch + // classify so callers that already verified the manifest don't pay twice. @@ -102,20 +81,3 @@ func (p *Puller) EnsureCloudImageFromRaw(ctx context.Context, name, localName st } return wait() } - -// fetchCloudImageManifest fetches and verifies the manifest at name:tag is a -// cloud-image artifact, returning its raw bytes for downstream streaming. -func (p *Puller) fetchCloudImageManifest(ctx context.Context, name, tag string) ([]byte, error) { - raw, _, err := p.Registry.GetManifest(ctx, name, tag) - if err != nil { - return nil, fmt.Errorf("get cloudimg manifest %s:%s: %w", name, tag, err) - } - kind, err := manifest.Classify(raw) - if err != nil { - return nil, fmt.Errorf("classify manifest: %w", err) - } - if kind != manifest.KindCloudImage { - return nil, fmt.Errorf("manifest %s:%s is not a cloud image (kind=%s)", name, tag, kind) - } - return raw, nil -} diff --git a/vm/cocoon_cli.go b/vm/cocoon_cli.go index 2336f8c..78311e6 100644 --- a/vm/cocoon_cli.go +++ b/vm/cocoon_cli.go @@ -91,7 +91,7 @@ func (c *CocoonCLI) Run(ctx context.Context, opts RunOptions) (*VM, error) { } // EnsureImage shells `cocoon image pull`; force=true adds --force. -// Cocoonstack cloud-image artifacts must go through Puller.EnsureCloudImage +// Cocoonstack cloud-image artifacts must go through Puller.EnsureCloudImageFromRaw // instead — `cocoon image pull` mistakes them for container images. func (c *CocoonCLI) EnsureImage(ctx context.Context, image string, force bool) error { if image == "" { diff --git a/vm/runtime.go b/vm/runtime.go index 6077c8b..b8562e0 100644 --- a/vm/runtime.go +++ b/vm/runtime.go @@ -17,8 +17,8 @@ var ( ErrVMNotFound = errors.New("vm not found") // ErrImageNotFound signals the cocoon CLI has authoritatively reported - // the image is not stored locally. Used by Puller.EnsureCloudImage as - // the idempotency probe for `cocoon image inspect`. + // the image is not stored locally. Used by Puller.EnsureCloudImageFromRaw + // as the idempotency probe for `cocoon image inspect`. ErrImageNotFound = errors.New("image not found") // ErrSnapshotNotFound is the sibling of ErrVMNotFound for snapshot inspect. From c06cc456c0f525a664ce038277d894ad51090bab Mon Sep 17 00:00:00 2001 From: CMGS Date: Wed, 1 Jul 2026 13:25:10 +0800 Subject: [PATCH 6/8] refactor: use cocoon-common for the OCI registry and snapshot bridge Drop vk's local OCIRegistry + Registry interface and the epoch/{snapshot, cloudimg,manifest,utils} imports in favor of cocoon-common (oci.Registry backend + the shared snapshot/cloudimg/manifest packages). epoch is now imported only for registryclient, the transitional backend kept until the data migration completes. Neutralizes stale epoch references in comments, metrics, events, and the README. --- README.md | 23 +++-- go.mod | 28 ++--- go.sum | 30 ++++++ main.go | 5 +- main_test.go | 8 +- metrics/metrics.go | 8 +- provider/cocoon/create.go | 12 +-- provider/cocoon/provider.go | 3 +- provider/cocoon/snapshot_helper.go | 2 +- provider/cocoon/update.go | 4 +- snapshots/cloudimg.go | 5 +- snapshots/oci.go | 159 ----------------------------- snapshots/oci_test.go | 77 -------------- snapshots/puller.go | 9 +- snapshots/pusher.go | 9 +- snapshots/registry.go | 16 --- snapshots/runner.go | 4 +- 17 files changed, 93 insertions(+), 309 deletions(-) delete mode 100644 snapshots/oci.go delete mode 100644 snapshots/oci_test.go delete mode 100644 snapshots/registry.go diff --git a/README.md b/README.md index e2f8020..47ad0e2 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ vk-cocoon is the host-side bridge between the Kubernetes API and the cocoon runt | Provider | `provider/cocoon/` | `Provider` struct with lifecycle methods (CreatePod / DeletePod / UpdatePod / GetPodStatus), startup reconcile, orphan policy, VM event watcher, pod eviction | | Provider iface | `provider/` | Shared provider interface and node-capacity helpers | | Cocoon CLI | `vm/` | `Runtime` interface + the default `CocoonCLI` implementation that shells out to `cocoon` (including `WatchEvents` via `cocoon vm status --event --format json`) | -| Snapshot SDK | `snapshots/` | Wraps the [epoch](https://github.com/cocoonstack/epoch) SDK as a `RegistryClient` interface, plus `Puller` and `Pusher` that stream snapshots and cloud images via `epoch/snapshot` and `epoch/cloudimg` | +| Snapshot SDK | `snapshots/` | `Puller` and `Pusher` stream snapshots and cloud images to any OCI registry through `cocoon-common`'s `oci.Registry` backend (`cocoon-common/snapshot` + `cocoon-common/cloudimg`) | | Network | `network/` | cocoon-net JSON lease parser used to resolve a freshly cloned VM's IP, plus the ICMPv4 `Pinger` the probe loop uses to check guest reachability | | Guest exec | `guest/` | RDP help-text shim (Windows) and SAC dialer (Windows static IP). Linux guest exec / logs go through `cocoon vm exec` and `cocoon vm logs` — see `vm/`. | | Probes | `probes/` | Per-pod probe agents that run a caller-supplied health check on a ticker, update the in-memory readiness map, and invoke an onUpdate callback so the async provider can push fresh status through v-k's notify hook | @@ -27,8 +27,8 @@ vk-cocoon is the host-side bridge between the Kubernetes API and the cocoon runt 2. If a VM with `spec.VMName` already exists locally, adopt it (idempotent on restart). Adoption hinges on `StartupReconcile` having populated `vmsByName`; before reconcile completes, CreatePod treats the pod as new and may collide on VM name. 3. Otherwise branch on `spec.Managed` first, then `spec.Mode`: - **`Managed=false`** (static / externally-managed VMs, e.g. Windows toolboxes on an external QEMU host): skip the runtime entirely and adopt the pre-assigned `VMID` / `IP` / `VNCPort` the operator pre-wrote into the `VMRuntime` annotations. `Managed` is the single source of truth for "vk-cocoon owns this VM's lifecycle". - - **Mode `clone`** (default, `Managed=true`): look up the snapshot locally using a **tag-aware name** (`repo:tag`, or bare `repo` when the tag is `latest` for backward compatibility). If the local snapshot does not exist, pull it from epoch via `Puller.PullSnapshot`. Before cloning, `assertSnapshotBackend` validates the snapshot's recorded hypervisor matches `spec.Backend` — a CH snapshot cannot be cloned onto a FC target and vice-versa. When the snapshot carries a base image, `Pull: true` is passed to `CloneOptions`, which translates to `cocoon vm clone --pull`; cocoon constructs a digest reference (`repo@sha256:xxx`) from the snapshot metadata and pulls the exact image version recorded at snapshot time. Then `Runtime.Clone(from=, to=spec.VMName)`. Pod-side CPU/memory/storage are not plumbed into clone — cocoon clone inherits all guest resources from the snapshot. Only the `vm run` path translates pod resources into VM resources. - - **Mode `run`** (`Managed=true`): `ensureRunImage` makes the image available locally before launching the VM. It peeks the OCI manifest via `Puller.Registry`: cocoonstack cloud-image artifacts (artifactType=`application/vnd.cocoonstack.os-image.v1+json`) take the qcow2 streaming path through `Puller.EnsureCloudImage` → `cocoon image import`, snapshot artifacts are rejected with a "use mode=clone" error, and everything else (HTTP(S) URLs, container images, refs that don't resolve against epoch) falls through to `Runtime.EnsureImage` → `cocoon image pull`. `--force` when `spec.ForcePull` is true. Then `Runtime.Run(image=spec.Image, name=spec.VMName)`. When `spec.Backend` is `firecracker`, `--fc` is passed to select the FC backend; when `spec.OS` is `windows`, `--windows` is passed. When `spec.NoDirectIO` is true, `--no-direct-io` disables O_DIRECT on writable disks (CH only, useful for dev/test). + - **Mode `clone`** (default, `Managed=true`): look up the snapshot locally using a **tag-aware name** (`repo:tag`, or bare `repo` when the tag is `latest` for backward compatibility). If the local snapshot does not exist, pull it from the registry via `Puller.PullSnapshot`. Before cloning, `assertSnapshotBackend` validates the snapshot's recorded hypervisor matches `spec.Backend` — a CH snapshot cannot be cloned onto a FC target and vice-versa. When the snapshot carries a base image, `Pull: true` is passed to `CloneOptions`, which translates to `cocoon vm clone --pull`; cocoon constructs a digest reference (`repo@sha256:xxx`) from the snapshot metadata and pulls the exact image version recorded at snapshot time. Then `Runtime.Clone(from=, to=spec.VMName)`. Pod-side CPU/memory/storage are not plumbed into clone — cocoon clone inherits all guest resources from the snapshot. Only the `vm run` path translates pod resources into VM resources. + - **Mode `run`** (`Managed=true`): `ensureRunImage` makes the image available locally before launching the VM. It peeks the OCI manifest via `Puller.Registry`: cocoonstack cloud-image artifacts (artifactType=`application/vnd.cocoonstack.os-image.v1+json`) take the qcow2 streaming path through `Puller.EnsureCloudImageFromRaw` → `cocoon image import`, snapshot artifacts are rejected with a "use mode=clone" error, and everything else (HTTP(S) URLs, container images, refs that don't resolve against the registry) falls through to `Runtime.EnsureImage` → `cocoon image pull`. `--force` when `spec.ForcePull` is true. Then `Runtime.Run(image=spec.Image, name=spec.VMName)`. When `spec.Backend` is `firecracker`, `--fc` is passed to select the FC backend; when `spec.OS` is `windows`, `--windows` is passed. When `spec.NoDirectIO` is true, `--no-direct-io` disables O_DIRECT on writable disks (CH only, useful for dev/test). - **`vm.cocoonstack.io/clone-from-dir` override** (managed-only, takes precedence over mode/fork-from): clone via `cocoon vm clone --from-dir --pull`, bypassing the local snapshot DB. Pairs with `cocoon snapshot export --to-dir` for cross-node staging. Conflicts with `mode=run` or `fork-from` fast-fail. 4. For clone/fork/wake paths, check whether the VM needs manual network setup (see [Post-clone hints](#post-clone-hints) below). If so, write the required commands as a base64-encoded annotation (`vm.cocoonstack.io/post-clone-hint`) and log a warning. The pod stays Running but Not Ready until the user executes the commands via `cocoon vm console` and the probe detects network connectivity. 5. Resolve the IP from the cocoon-net JSON lease file by MAC. @@ -39,7 +39,7 @@ vk-cocoon is the host-side bridge between the Kubernetes API and the cocoon runt 1. Decode `meta.VMSpec`. 2. `meta.ShouldSnapshotVM(spec)` — the shared cocoon-common decoder — decides whether to snapshot before destroy: - - `always`: `Runtime.SnapshotSave` then `Pusher.PushSnapshot(tag=meta.DefaultSnapshotTag)` to epoch. + - `always`: `Runtime.SnapshotSave` then `Pusher.PushSnapshot(tag=meta.DefaultSnapshotTag)` to the registry. - `main-only`: same, but only when the VM name ends in `-0` (slot 0 = main agent). - `never`: skip snapshots entirely. 3. `Runtime.Remove(vmID)` to destroy the VM. @@ -52,7 +52,7 @@ The only update vk-cocoon honors is a `HibernateState` transition. Anything else | Transition | Behavior | |---|---| | `false → true` | NetResize (CH+Windows) → SnapshotSave → Push → clear VMID before Remove → Remove (rollback on failure). Pod stays alive (`PodRunning`) so K8s controllers do not recreate it. VMID/IP annotations clear between Push and Remove so the operator's manifest+VMID race window collapses to one patch RTT. **Compensating rollback**: if `Runtime.Remove` fails after a successful push, vk-cocoon best-effort `Registry.DeleteManifest` the hibernate tag and re-applies VMID/IP so the pod stays recoverable. Push and Save are idempotent, so a compensated retry re-publishes the tag cleanly on the next attempt. | -| `true → false` (with no live VM) | `Puller.PullSnapshot(tag=meta.HibernateSnapshotTag)` → `Runtime.Clone` → drop the hibernation tag from epoch. | +| `true → false` (with no live VM) | `Puller.PullSnapshot(tag=meta.HibernateSnapshotTag)` → `Runtime.Clone` → drop the hibernation tag from the registry. | The operator's `CocoonHibernation` reconciler tracks the transition by polling `epoch.GetManifest(vmName, "hibernate")`. @@ -87,8 +87,8 @@ vk-cocoon exposes three metrics surfaces: | `vk_cocoon_node_storage_available_bytes` / `total_bytes` | Gauge | Cocoon root filesystem | | `vk_cocoon_vm_boot_duration_seconds{mode,backend}` | Histogram | VM creation time (run or clone) | | `vk_cocoon_snapshot_save_duration_seconds` | Histogram | Snapshot save time | -| `vk_cocoon_snapshot_push_duration_seconds` | Histogram | Epoch push time | -| `vk_cocoon_snapshot_pull_duration_seconds` | Histogram | Epoch pull time | +| `vk_cocoon_snapshot_push_duration_seconds` | Histogram | Registry push time | +| `vk_cocoon_snapshot_pull_duration_seconds` | Histogram | Registry pull time | | `vk_cocoon_probe_duration_seconds` | Histogram | Per-probe health check time (ICMP or TCP) | | `vk_cocoon_pod_lifecycle_total{op,result}` | Counter | Pod lifecycle operations | | `vk_cocoon_snapshot_pull_total{result}` / `push_total` | Counter | Snapshot pull/push counts | @@ -178,9 +178,10 @@ If the ICMP raw socket cannot be opened — typically because the binary is runn | `KUBECONFIG` | unset | Path to kubeconfig (in-cluster used otherwise). | | `VK_NODE_NAME` | `cocoon-pool` | Virtual node name registered with the K8s API. | | `VK_LOG_LEVEL` | `info` | `projecteru2/core/log` level. | -| `EPOCH_URL` | `http://epoch.cocoon-system.svc:8080` | Epoch base URL. | -| `EPOCH_TOKEN` | unset | Bearer token (only needed for `/v2/` pushes; `/dl/` is anonymous). | -| `EPOCH_CA_CERT` | unset | Path to PEM-encoded CA certificate for TLS verification against epoch. | +| `OCI_REGISTRY` | unset | OCI registry base (e.g. an Artifact Registry repo). When set, snapshots and cloud images use it instead of the epoch backend below. | +| `EPOCH_URL` | `http://epoch.cocoon-system.svc:8080` | Epoch backend base URL (used when `OCI_REGISTRY` is unset). | +| `EPOCH_TOKEN` | unset | Bearer token for the epoch backend's `/v2/` API. | +| `EPOCH_CA_CERT` | unset | Path to PEM-encoded CA certificate for TLS verification against the epoch backend. | | `VK_LEASES_PATH` | `/var/lib/cocoon/net/leases.json` | cocoon-net JSON lease file. | | `VK_COCOON_BIN` | `/usr/local/bin/cocoon` | Path to the cocoon CLI binary. | | `VK_ORPHAN_POLICY` | `destroy` | `destroy` (auto-clean), `alert`, or `keep`. | @@ -235,7 +236,7 @@ The Makefile detects Go workspace mode (`go env GOWORK`) and skips `go mod tidy` | [cocoon-common](https://github.com/cocoonstack/cocoon-common) | CRD types, annotation contract, shared helpers. | | [cocoon-operator](https://github.com/cocoonstack/cocoon-operator) | CocoonSet and CocoonHibernation reconcilers. | | [cocoon-webhook](https://github.com/cocoonstack/cocoon-webhook) | Admission webhook for sticky scheduling and CocoonSet validation. | -| [epoch](https://github.com/cocoonstack/epoch) | Snapshot registry; vk-cocoon pulls and pushes via `epoch/snapshot` + `epoch/cloudimg`. | +| [epoch](https://github.com/cocoonstack/epoch) | Transitional registry backend (`registryclient`); the snapshot/cloud-image code now lives in `cocoon-common`. | | [cocoon-net](https://github.com/cocoonstack/cocoon-net) | Per-host networking with embedded DHCP server and iptables setup; vk-cocoon reads its JSON lease file. | ## License diff --git a/go.mod b/go.mod index e861bf5..0f1e7ac 100644 --- a/go.mod +++ b/go.mod @@ -3,15 +3,15 @@ module github.com/cocoonstack/vk-cocoon go 1.25.6 require ( - github.com/cocoonstack/cocoon-common v0.2.2 + github.com/cocoonstack/cocoon-common v0.2.3-0.20260701050723-6e675f810e8d github.com/cocoonstack/epoch v0.2.4 - github.com/google/go-containerregistry v0.21.0 + github.com/google/go-containerregistry v0.21.7 github.com/projecteru2/core v0.0.0-20241016125006-ff909eefe04c github.com/prometheus/client_golang v1.23.2 github.com/prometheus/client_model v0.6.2 github.com/virtual-kubelet/virtual-kubelet v1.12.0 - golang.org/x/net v0.50.0 - golang.org/x/sync v0.19.0 + golang.org/x/net v0.56.0 + golang.org/x/sync v0.21.0 google.golang.org/protobuf v1.36.10 k8s.io/api v0.35.3 k8s.io/apimachinery v0.35.3 @@ -37,7 +37,7 @@ require ( github.com/coreos/go-semver v0.3.1 // indirect github.com/coreos/go-systemd/v22 v22.5.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/docker/cli v29.2.1+incompatible // indirect + github.com/docker/cli v29.5.3+incompatible // indirect github.com/docker/distribution v2.8.3+incompatible // indirect github.com/docker/docker-credential-helpers v0.9.3 // indirect github.com/emicklei/go-restful/v3 v3.12.2 // indirect @@ -66,7 +66,7 @@ require ( github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect - github.com/klauspost/compress v1.18.4 // indirect + github.com/klauspost/compress v1.18.6 // indirect github.com/kr/pretty v0.3.1 // indirect github.com/kr/text v0.2.0 // indirect github.com/kylelemons/godebug v1.1.0 // indirect @@ -101,23 +101,23 @@ require ( go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect - go.opentelemetry.io/otel v1.39.0 // indirect + go.opentelemetry.io/otel v1.41.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0 // indirect - go.opentelemetry.io/otel/metric v1.39.0 // indirect + go.opentelemetry.io/otel/metric v1.41.0 // indirect go.opentelemetry.io/otel/sdk v1.38.0 // indirect - go.opentelemetry.io/otel/trace v1.39.0 // indirect + go.opentelemetry.io/otel/trace v1.41.0 // indirect go.opentelemetry.io/proto/otlp v1.5.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.0 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/crypto v0.48.0 // indirect + golang.org/x/crypto v0.53.0 // indirect golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect - golang.org/x/oauth2 v0.35.0 // indirect - golang.org/x/sys v0.41.0 // indirect - golang.org/x/term v0.40.0 // indirect - golang.org/x/text v0.34.0 // indirect + golang.org/x/oauth2 v0.36.0 // indirect + golang.org/x/sys v0.46.0 // indirect + golang.org/x/term v0.44.0 // indirect + golang.org/x/text v0.38.0 // indirect golang.org/x/time v0.14.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a // indirect diff --git a/go.sum b/go.sum index 2ccba1e..15d1d0f 100644 --- a/go.sum +++ b/go.sum @@ -45,6 +45,8 @@ github.com/cockroachdb/redact v1.1.3 h1:AKZds10rFSIj7qADf0g46UixK8NNLwWTNdCIGS5w github.com/cockroachdb/redact v1.1.3/go.mod h1:BVNblN9mBWFyMyqK1k3AAiSxhvhfK2oOZZ2lK+dpvRg= github.com/cocoonstack/cocoon-common v0.2.2 h1:+qY4Iv1nLZw1brH46j33Kvajqi8UR1QKLLTC/AXhU30= github.com/cocoonstack/cocoon-common v0.2.2/go.mod h1:xIXbJ83vngQ2mrLC6q0Tw7h21M9BYBBqqYTcHaUrm1Y= +github.com/cocoonstack/cocoon-common v0.2.3-0.20260701050723-6e675f810e8d h1:8IAri8O+Sy6r+JDONULPgXx6YULohMFh05108uJLbpg= +github.com/cocoonstack/cocoon-common v0.2.3-0.20260701050723-6e675f810e8d/go.mod h1:/Cf3aBBN0blBxJWexuGuMbTkas+scvQiF2I75aQXkH4= github.com/cocoonstack/epoch v0.2.4 h1:X18DdsWlPRqEVswR1N3JAazxRZfBUtoL5r7GTgWmhN8= github.com/cocoonstack/epoch v0.2.4/go.mod h1:1bFeUom4aeFbw3vjFbrLegEgVShqe4qSOfpmH5UEpww= github.com/codegangsta/inject v0.0.0-20150114235600-33e0aa1cb7c0/go.mod h1:4Zcjuz89kmFXt9morQgcfYZAYZ5n8WHjt81YYWIwtTM= @@ -68,6 +70,8 @@ github.com/dgraph-io/badger v1.6.0/go.mod h1:zwt7syl517jmP8s94KqSxTlM6IMsdhYy6ps github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= github.com/docker/cli v29.2.1+incompatible h1:n3Jt0QVCN65eiVBoUTZQM9mcQICCJt3akW4pKAbKdJg= github.com/docker/cli v29.2.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/cli v29.5.3+incompatible h1:nbEFfz774vBwQ5KRYv7c/AghjReqnGISvrRhzjV0evs= +github.com/docker/cli v29.5.3+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk= github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= github.com/docker/docker-credential-helpers v0.9.3 h1:gAm/VtF9wgqJMoxzT3Gj5p4AqIjCBS4wrsOh9yRqcz8= @@ -175,6 +179,8 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-containerregistry v0.21.0 h1:ocqxUOczFwAZQBMNE7kuzfqvDe0VWoZxQMOesXreCDI= github.com/google/go-containerregistry v0.21.0/go.mod h1:ctO5aCaewH4AK1AumSF5DPW+0+R+d2FmylMJdp5G7p0= +github.com/google/go-containerregistry v0.21.7 h1:/vPFuVXDjtFREsVArW+0h1CIl5urnOhzei4X2DMW9IU= +github.com/google/go-containerregistry v0.21.7/go.mod h1:kjSbt7/zMsKLWfnHrIvKvhXHUw91jbe9DNjPPJ32gXE= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8= @@ -231,6 +237,8 @@ github.com/klauspost/compress v1.8.2/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0 github.com/klauspost/compress v1.9.7/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c= github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= +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 v1.2.1/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= @@ -425,18 +433,24 @@ go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6h go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q= go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48= go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8= +go.opentelemetry.io/otel v1.41.0 h1:YlEwVsGAlCvczDILpUXpIpPSL/VPugt7zHThEMLce1c= +go.opentelemetry.io/otel v1.41.0/go.mod h1:Yt4UwgEKeT05QbLwbyHXEwhnjxNO6D8L5PQP51/46dE= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 h1:OeNbIYk/2C15ckl7glBlOBp5+WlYsOElzTNmiPW/x60= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0/go.mod h1:7Bept48yIeqxP2OZ9/AqIpYS94h2or0aB4FypJTc8ZM= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0 h1:tgJ0uaNS4c98WRNUEx5U3aDlrDOI5Rs+1Vifcw4DJ8U= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0/go.mod h1:U7HYyW0zt/a9x5J1Kjs+r1f/d4ZHnYFclhYY2+YbeoE= go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0= go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs= +go.opentelemetry.io/otel/metric v1.41.0 h1:rFnDcs4gRzBcsO9tS8LCpgR0dxg4aaxWlJxCno7JlTQ= +go.opentelemetry.io/otel/metric v1.41.0/go.mod h1:xPvCwd9pU0VN8tPZYzDZV/BMj9CM9vs00GuBjeKhJps= go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E= go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg= go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM= go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA= go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI= go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA= +go.opentelemetry.io/otel/trace v1.41.0 h1:Vbk2co6bhj8L59ZJ6/xFTskY+tGAbOnCtQGVVa9TIN0= +go.opentelemetry.io/otel/trace v1.41.0/go.mod h1:U1NU4ULCoxeDKc09yCWdWe+3QoyweJcISEVa1RBzOis= go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4= go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= @@ -459,6 +473,8 @@ golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= +golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto= +golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= @@ -472,6 +488,7 @@ golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8= golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w= +golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -492,9 +509,13 @@ golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96b golang.org/x/net v0.0.0-20211008194852-3b03d305991f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60= golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM= +golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o= +golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ= golang.org/x/oauth2 v0.35.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= +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.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -504,6 +525,8 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM= +golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -529,9 +552,13 @@ golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw= +golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg= golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM= +golang.org/x/term v0.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc= +golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= @@ -540,6 +567,8 @@ golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= +golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE= +golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4= golang.org/x/time v0.0.0-20201208040808-7e3f01d25324/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= @@ -558,6 +587,7 @@ golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4f golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k= golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0= +golang.org/x/tools v0.46.0 h1:7jTurBkPZu4moS/Uy4OQT1M+QBlsj3wejyZwsT8Z7rk= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/main.go b/main.go index c303dac..7a171ef 100644 --- a/main.go +++ b/main.go @@ -32,6 +32,7 @@ import ( commonk8s "github.com/cocoonstack/cocoon-common/k8s" commonlog "github.com/cocoonstack/cocoon-common/log" "github.com/cocoonstack/cocoon-common/meta" + "github.com/cocoonstack/cocoon-common/oci" "github.com/cocoonstack/epoch/registryclient" "github.com/cocoonstack/vk-cocoon/guest/rdp" "github.com/cocoonstack/vk-cocoon/guest/sac" @@ -231,10 +232,10 @@ type buildOpts struct { // buildRegistry selects the registry backend: an OCI Distribution registry // when OCI_REGISTRY is set, else epoch's server. The OCI keychain resolves GCP // ADC (google.Keychain) before falling back to docker config. -func buildRegistry(opts buildOpts) (snapshots.Registry, error) { +func buildRegistry(opts buildOpts) (oci.Registry, error) { if opts.ociRegistry != "" { keychain := authn.NewMultiKeychain(google.Keychain, authn.DefaultKeychain) - return snapshots.NewOCIRegistry(opts.ociRegistry, keychain), nil + return oci.NewOCIRegistry(opts.ociRegistry, keychain), nil } client, err := registryclient.NewFromEnv(opts.epochURL, opts.epochToken) if err != nil { diff --git a/main_test.go b/main_test.go index 864c6d4..e9091ed 100644 --- a/main_test.go +++ b/main_test.go @@ -3,19 +3,19 @@ package main import ( "testing" + "github.com/cocoonstack/cocoon-common/oci" "github.com/cocoonstack/epoch/registryclient" - "github.com/cocoonstack/vk-cocoon/snapshots" ) func TestBuildRegistryBackend(t *testing.T) { t.Setenv("EPOCH_CA_CERT", "") // keep the epoch client deterministic on dev machines - oci, err := buildRegistry(buildOpts{ociRegistry: "example.com/proj/repo"}) + reg, err := buildRegistry(buildOpts{ociRegistry: "example.com/proj/repo"}) if err != nil { t.Fatalf("buildRegistry(OCI): %v", err) } - if _, ok := oci.(*snapshots.OCIRegistry); !ok { - t.Fatalf("OCI_REGISTRY set: got %T, want *snapshots.OCIRegistry", oci) + if _, ok := reg.(*oci.OCIRegistry); !ok { + t.Fatalf("OCI_REGISTRY set: got %T, want *oci.OCIRegistry", reg) } ep, err := buildRegistry(buildOpts{epochURL: "http://epoch.example"}) diff --git a/metrics/metrics.go b/metrics/metrics.go index 830125c..9fd99ef 100644 --- a/metrics/metrics.go +++ b/metrics/metrics.go @@ -23,7 +23,7 @@ var ( prometheus.CounterOpts{ Namespace: metricNamespace, Name: "snapshot_pull_total", - Help: "Number of snapshot pulls from epoch by result.", + Help: "Number of snapshot pulls from the registry by result.", }, []string{"result"}, ) @@ -41,7 +41,7 @@ var ( prometheus.CounterOpts{ Namespace: metricNamespace, Name: "snapshot_push_total", - Help: "Number of snapshot pushes to epoch by result.", + Help: "Number of snapshot pushes to the registry by result.", }, []string{"result"}, ) @@ -118,7 +118,7 @@ var ( prometheus.HistogramOpts{ Namespace: metricNamespace, Name: "snapshot_push_duration_seconds", - Help: "Time to push a snapshot to epoch.", + Help: "Time to push a snapshot to the registry.", Buckets: []float64{1, 5, 10, 30, 60, 120, 300}, }, ) @@ -127,7 +127,7 @@ var ( prometheus.HistogramOpts{ Namespace: metricNamespace, Name: "snapshot_pull_duration_seconds", - Help: "Time to pull a snapshot from epoch.", + Help: "Time to pull a snapshot from the registry.", Buckets: []float64{1, 5, 10, 30, 60, 120, 300}, }, ) diff --git a/provider/cocoon/create.go b/provider/cocoon/create.go index 66eb406..23ed3b9 100644 --- a/provider/cocoon/create.go +++ b/provider/cocoon/create.go @@ -15,9 +15,9 @@ import ( cocoonv1 "github.com/cocoonstack/cocoon-common/apis/v1" commonk8s "github.com/cocoonstack/cocoon-common/k8s" + "github.com/cocoonstack/cocoon-common/manifest" "github.com/cocoonstack/cocoon-common/meta" - "github.com/cocoonstack/epoch/manifest" - "github.com/cocoonstack/epoch/utils" + "github.com/cocoonstack/cocoon-common/ociutil" "github.com/cocoonstack/vk-cocoon/metrics" "github.com/cocoonstack/vk-cocoon/vm" ) @@ -198,7 +198,7 @@ func (p *Provider) bringUpVM(ctx context.Context, pod *corev1.Pod, spec meta.VMS return v, "", nil default: // clone is the default - repo, tag := utils.ParseRef(spec.Image) + repo, tag := ociutil.ParseRef(spec.Image) local := localSnapshotName(repo, tag) snapshot, err := p.ensureSnapshot(ctx, repo, tag, local) if err != nil { @@ -241,10 +241,10 @@ func (p *Provider) ensureRunImage(ctx context.Context, image string, force bool) if p.Puller == nil || p.Puller.Registry == nil || isHTTPURL(image) { return image, p.Runtime.EnsureImage(ctx, image, force) } - repo, tag := utils.ParseRef(image) + repo, tag := ociutil.ParseRef(image) raw, _, err := p.Puller.Registry.GetManifest(ctx, repo, tag) if err != nil { - // Non-epoch ref or registry hiccup; cocoon image pull handles non-epoch refs natively. + // Ref absent from the registry or a hiccup; cocoon image pull handles external refs natively. return image, p.Runtime.EnsureImage(ctx, image, force) } kind, classifyErr := manifest.Classify(raw) @@ -262,7 +262,7 @@ func (p *Provider) ensureRunImage(ctx context.Context, image string, force bool) } } -// ensureSnapshot returns the local snapshot, pulling from epoch if needed. +// ensureSnapshot returns the local snapshot, pulling from the registry if needed. // Local name includes the tag so myvm:v1 and myvm:v2 stay separate. func (p *Provider) ensureSnapshot(ctx context.Context, repo, tag, local string) (*vm.Snapshot, error) { if repo == "" { diff --git a/provider/cocoon/provider.go b/provider/cocoon/provider.go index b18176f..8da1521 100644 --- a/provider/cocoon/provider.go +++ b/provider/cocoon/provider.go @@ -21,6 +21,7 @@ import ( commonk8s "github.com/cocoonstack/cocoon-common/k8s" "github.com/cocoonstack/cocoon-common/meta" + "github.com/cocoonstack/cocoon-common/oci" "github.com/cocoonstack/vk-cocoon/guest" "github.com/cocoonstack/vk-cocoon/metrics" "github.com/cocoonstack/vk-cocoon/network" @@ -86,7 +87,7 @@ type Provider struct { Runtime vm.Runtime Puller *snapshots.Puller Pusher *snapshots.Pusher - Registry snapshots.Registry + Registry oci.Registry LeaseParser *network.LeaseParser Pinger network.Pinger GuestRDP guest.Executor diff --git a/provider/cocoon/snapshot_helper.go b/provider/cocoon/snapshot_helper.go index 662c35e..d3aae92 100644 --- a/provider/cocoon/snapshot_helper.go +++ b/provider/cocoon/snapshot_helper.go @@ -23,7 +23,7 @@ func (p *Provider) removeSnapshotDetached(funcLabel, name string) { } } -// saveAndPushSnapshot saves a snapshot and pushes it to epoch, recording +// saveAndPushSnapshot saves a snapshot and pushes it to the registry, recording // timing metrics. Errors are logged and counted but not returned — the // delete path treats snapshot failures as non-fatal. func (p *Provider) saveAndPushSnapshot(ctx context.Context, vmName, vmID, tag, image string) { diff --git a/provider/cocoon/update.go b/provider/cocoon/update.go index b451d38..7920aff 100644 --- a/provider/cocoon/update.go +++ b/provider/cocoon/update.go @@ -134,9 +134,9 @@ func (p *Provider) hibernate(ctx context.Context, pod *corev1.Pod, v *vm.VM) err p.forgetVMOnly(pod.Namespace, pod.Name) p.markLifecycleState(ctx, pod, meta.LifecycleStateHibernated, "") if p.Pusher != nil { - p.emitNormalf(pod, "Hibernated", "snapshot pushed to epoch") + p.emitNormalf(pod, "Hibernated", "snapshot pushed to registry") } else { - p.emitNormalf(pod, "Hibernated", "snapshot saved locally (no epoch pusher configured)") + p.emitNormalf(pod, "Hibernated", "snapshot saved locally (no registry pusher configured)") } return nil } diff --git a/snapshots/cloudimg.go b/snapshots/cloudimg.go index bb45cd9..6ffc393 100644 --- a/snapshots/cloudimg.go +++ b/snapshots/cloudimg.go @@ -4,14 +4,15 @@ import ( "context" "io" - "github.com/cocoonstack/epoch/cloudimg" + "github.com/cocoonstack/cocoon-common/cloudimg" + "github.com/cocoonstack/cocoon-common/oci" ) var _ cloudimg.BlobReader = blobReader{} // blobReader adapts a Registry to cloudimg.BlobReader. type blobReader struct { - registry Registry + registry oci.Registry name string } diff --git a/snapshots/oci.go b/snapshots/oci.go deleted file mode 100644 index c9fbc20..0000000 --- a/snapshots/oci.go +++ /dev/null @@ -1,159 +0,0 @@ -package snapshots - -import ( - "context" - "errors" - "fmt" - "io" - "net/http" - "strings" - - "github.com/google/go-containerregistry/pkg/authn" - "github.com/google/go-containerregistry/pkg/name" - v1 "github.com/google/go-containerregistry/pkg/v1" - "github.com/google/go-containerregistry/pkg/v1/remote" - "github.com/google/go-containerregistry/pkg/v1/remote/transport" - "github.com/google/go-containerregistry/pkg/v1/types" -) - -var ( - _ Registry = (*OCIRegistry)(nil) - - // errBlobUncompressed guards the DiffID/Uncompressed accessors: cocoon blobs - // are opaque content-addressed bytes and WriteLayer only reads Compressed(). - errBlobUncompressed = errors.New("cocoon blob layers expose only compressed bytes") -) - -// OCIRegistry is a Registry backed by a standard OCI Distribution registry -// (e.g. Artifact Registry), using OCI upload sessions and keychain auth. -type OCIRegistry struct { - base string // registry host + repo prefix, e.g. "asia-docker.pkg.dev/proj/repo" - opts []remote.Option -} - -// NewOCIRegistry roots a client at base, authenticating via keychain (e.g. -// authn.DefaultKeychain, or a MultiKeychain with google.Keychain for GCP AR). -func NewOCIRegistry(base string, keychain authn.Keychain) *OCIRegistry { - return &OCIRegistry{base: base, opts: []remote.Option{remote.WithAuthFromKeychain(keychain)}} -} - -// GetManifest fetches the raw manifest bytes and media type at repo:tag. -func (r *OCIRegistry) GetManifest(ctx context.Context, repo, tag string) ([]byte, string, error) { - ref, err := name.ParseReference(r.base + "/" + repo + ":" + tag) - if err != nil { - return nil, "", fmt.Errorf("parse ref %s:%s: %w", repo, tag, err) - } - desc, err := remote.Get(ref, r.callOpts(ctx)...) - if err != nil { - return nil, "", fmt.Errorf("get manifest %s:%s: %w", repo, tag, err) - } - return desc.Manifest, string(desc.MediaType), nil -} - -// GetBlob streams the blob at the given digest. -func (r *OCIRegistry) GetBlob(ctx context.Context, repo, digest string) (io.ReadCloser, error) { - ref, err := name.NewDigest(r.base + "/" + repo + "@" + digest) - if err != nil { - return nil, fmt.Errorf("parse digest %s@%s: %w", repo, digest, err) - } - layer, err := remote.Layer(ref, r.callOpts(ctx)...) - if err != nil { - return nil, fmt.Errorf("get blob %s@%s: %w", repo, digest, err) - } - return layer.Compressed() -} - -// BlobExists reports whether the blob is already present, so pushes can skip it. -func (r *OCIRegistry) BlobExists(ctx context.Context, repo, digest string) (bool, error) { - ref, err := name.NewDigest(r.base + "/" + repo + "@" + digest) - if err != nil { - return false, fmt.Errorf("parse digest %s@%s: %w", repo, digest, err) - } - // remote.Layer is lazy; Size() issues the HEAD that reveals whether it exists. - layer, err := remote.Layer(ref, r.callOpts(ctx)...) - if err == nil { - _, err = layer.Size() - } - if err == nil { - return true, nil - } - var terr *transport.Error - if errors.As(err, &terr) && terr.StatusCode == http.StatusNotFound { - return false, nil - } - return false, fmt.Errorf("head blob %s@%s: %w", repo, digest, err) -} - -// PutBlob uploads a blob of the given digest/size via a standard upload session. -func (r *OCIRegistry) PutBlob(ctx context.Context, repo, digest string, body io.Reader, size int64) error { - repoRef, err := name.NewRepository(r.base + "/" + repo) - if err != nil { - return fmt.Errorf("parse repo %s: %w", repo, err) - } - hash, err := v1.NewHash(digest) - if err != nil { - return fmt.Errorf("parse digest %s: %w", digest, err) - } - if err := remote.WriteLayer(repoRef, &streamLayer{hash: hash, size: size, body: body}, r.callOpts(ctx)...); err != nil { - return fmt.Errorf("put blob %s@%s: %w", repo, digest, err) - } - return nil -} - -// PutManifest uploads a manifest at repo:tag with the given content type. -func (r *OCIRegistry) PutManifest(ctx context.Context, repo, tag string, data []byte, contentType string) error { - ref, err := name.ParseReference(r.base + "/" + repo + ":" + tag) - if err != nil { - return fmt.Errorf("parse ref %s:%s: %w", repo, tag, err) - } - if err := remote.Put(ref, rawManifest{data: data, mediaType: types.MediaType(contentType)}, r.callOpts(ctx)...); err != nil { - return fmt.Errorf("put manifest %s:%s: %w", repo, tag, err) - } - return nil -} - -// DeleteManifest removes the manifest at repo:reference (tag or digest). -func (r *OCIRegistry) DeleteManifest(ctx context.Context, repo, reference string) error { - // A digest (sha256:...) joins the repo with '@'; a tag with ':'. - sep := ":" - if strings.ContainsRune(reference, ':') { - sep = "@" - } - ref, err := name.ParseReference(r.base + "/" + repo + sep + reference) - if err != nil { - return fmt.Errorf("parse ref %s%s%s: %w", repo, sep, reference, err) - } - if err := remote.Delete(ref, r.callOpts(ctx)...); err != nil { - return fmt.Errorf("delete manifest %s: %w", reference, err) - } - return nil -} - -func (r *OCIRegistry) callOpts(ctx context.Context) []remote.Option { - return append(r.opts, remote.WithContext(ctx)) -} - -// streamLayer is a v1.Layer over a body with a known digest and size, so PutBlob -// streams a raw blob without buffering it (WriteLayer reads only Compressed()). -// body is single-use: a retried upload fails the digest check, not corrupts. -type streamLayer struct { - hash v1.Hash - size int64 - body io.Reader -} - -func (l *streamLayer) Digest() (v1.Hash, error) { return l.hash, nil } -func (l *streamLayer) Size() (int64, error) { return l.size, nil } -func (l *streamLayer) Compressed() (io.ReadCloser, error) { return io.NopCloser(l.body), nil } -func (l *streamLayer) MediaType() (types.MediaType, error) { return types.OCILayer, nil } -func (l *streamLayer) DiffID() (v1.Hash, error) { return v1.Hash{}, errBlobUncompressed } -func (l *streamLayer) Uncompressed() (io.ReadCloser, error) { return nil, errBlobUncompressed } - -// rawManifest is a remote.Taggable over pre-serialized manifest bytes. -type rawManifest struct { - data []byte - mediaType types.MediaType -} - -func (m rawManifest) RawManifest() ([]byte, error) { return m.data, nil } -func (m rawManifest) MediaType() (types.MediaType, error) { return m.mediaType, nil } diff --git a/snapshots/oci_test.go b/snapshots/oci_test.go deleted file mode 100644 index 47292e8..0000000 --- a/snapshots/oci_test.go +++ /dev/null @@ -1,77 +0,0 @@ -package snapshots - -import ( - "bytes" - "context" - "crypto/sha256" - "encoding/hex" - "io" - "net/http/httptest" - "strconv" - "strings" - "testing" - - "github.com/google/go-containerregistry/pkg/authn" - "github.com/google/go-containerregistry/pkg/registry" -) - -// TestOCIRegistryRoundTrip exercises the full Registry surface against an -// in-memory OCI registry: a blob and a custom-artifactType manifest survive a -// put -> exists -> get -> delete round trip. -func TestOCIRegistryRoundTrip(t *testing.T) { - srv := httptest.NewServer(registry.New()) - t.Cleanup(srv.Close) - - r := NewOCIRegistry(strings.TrimPrefix(srv.URL, "http://")+"/cocoon", authn.DefaultKeychain) - ctx := context.Background() - - blob := []byte("hello cocoon blob") - sum := sha256.Sum256(blob) - digest := "sha256:" + hex.EncodeToString(sum[:]) - - if ok, err := r.BlobExists(ctx, "myvm", digest); err != nil || ok { - t.Fatalf("BlobExists before put = (%v, %v), want (false, nil)", ok, err) - } - if err := r.PutBlob(ctx, "myvm", digest, bytes.NewReader(blob), int64(len(blob))); err != nil { - t.Fatalf("PutBlob: %v", err) - } - if ok, err := r.BlobExists(ctx, "myvm", digest); err != nil || !ok { - t.Fatalf("BlobExists after put = (%v, %v), want (true, nil)", ok, err) - } - - rc, err := r.GetBlob(ctx, "myvm", digest) - if err != nil { - t.Fatalf("GetBlob: %v", err) - } - got, _ := io.ReadAll(rc) - _ = rc.Close() - if !bytes.Equal(got, blob) { - t.Fatalf("GetBlob = %q, want %q", got, blob) - } - - const mt = "application/vnd.oci.image.manifest.v1+json" - mf := []byte(`{"schemaVersion":2,"mediaType":"` + mt + - `","artifactType":"application/vnd.cocoonstack.snapshot.v1+json","config":{"mediaType":` + - `"application/vnd.cocoonstack.snapshot.config.v1+json","digest":"` + digest + - `","size":` + strconv.Itoa(len(blob)) + `},"layers":[]}`) - if err := r.PutManifest(ctx, "myvm", "hibernate", mf, mt); err != nil { - t.Fatalf("PutManifest: %v", err) - } - raw, gotMT, err := r.GetManifest(ctx, "myvm", "hibernate") - if err != nil { - t.Fatalf("GetManifest: %v", err) - } - if !bytes.Equal(raw, mf) { - t.Fatalf("GetManifest bytes mismatch") - } - if gotMT != mt { - t.Fatalf("GetManifest mediaType = %q, want %q", gotMT, mt) - } - - if err := r.DeleteManifest(ctx, "myvm", "hibernate"); err != nil { - t.Fatalf("DeleteManifest: %v", err) - } - if _, _, err := r.GetManifest(ctx, "myvm", "hibernate"); err == nil { - t.Fatal("GetManifest after delete: want error, got nil") - } -} diff --git a/snapshots/puller.go b/snapshots/puller.go index 2039d76..50083c1 100644 --- a/snapshots/puller.go +++ b/snapshots/puller.go @@ -8,17 +8,18 @@ import ( "errors" "fmt" - "github.com/cocoonstack/epoch/snapshot" + "github.com/cocoonstack/cocoon-common/oci" + "github.com/cocoonstack/cocoon-common/snapshot" "github.com/cocoonstack/vk-cocoon/vm" ) -// Puller streams a snapshot or cloud image from epoch into the local cocoon runtime. +// Puller streams a snapshot or cloud image from an OCI registry into the local cocoon runtime. type Puller struct { - Registry Registry + Registry oci.Registry Runtime vm.Runtime } -// PullSnapshot fetches and imports a snapshot from epoch. localName defaults to name. +// PullSnapshot fetches and imports a snapshot from the registry. localName defaults to name. func (p *Puller) PullSnapshot(ctx context.Context, name, tag, localName string) error { raw, _, err := p.Registry.GetManifest(ctx, name, tag) if err != nil { diff --git a/snapshots/pusher.go b/snapshots/pusher.go index 5f0c296..85b0819 100644 --- a/snapshots/pusher.go +++ b/snapshots/pusher.go @@ -6,17 +6,18 @@ import ( "fmt" "github.com/cocoonstack/cocoon-common/meta" - "github.com/cocoonstack/epoch/snapshot" + "github.com/cocoonstack/cocoon-common/oci" + "github.com/cocoonstack/cocoon-common/snapshot" "github.com/cocoonstack/vk-cocoon/vm" ) -// Pusher streams a local snapshot up into epoch. +// Pusher streams a local snapshot up into an OCI registry. type Pusher struct { - Registry Registry + Registry oci.Registry Runtime vm.Runtime } -// PushSnapshot uploads a snapshot to epoch at the given repo/tag. +// PushSnapshot uploads a snapshot to the registry at the given repo/tag. func (p *Pusher) PushSnapshot(ctx context.Context, vmName, repo, tag, baseImage string) (*snapshot.PushResult, error) { repo = cmp.Or(repo, vmName) tag = cmp.Or(tag, meta.DefaultSnapshotTag) diff --git a/snapshots/registry.go b/snapshots/registry.go deleted file mode 100644 index 1a290af..0000000 --- a/snapshots/registry.go +++ /dev/null @@ -1,16 +0,0 @@ -package snapshots - -import ( - "context" - - "github.com/cocoonstack/epoch/snapshot" -) - -// Registry abstracts the OCI backend (vs epoch's concrete *registryclient.Client) -// so a standard-OCI implementation can drop in. Beyond snapshot Uploader/Downloader -// it needs DeleteManifest for hibernate rollback. -type Registry interface { - snapshot.Uploader - snapshot.Downloader - DeleteManifest(ctx context.Context, name, reference string) error -} diff --git a/snapshots/runner.go b/snapshots/runner.go index f82954e..282ee65 100644 --- a/snapshots/runner.go +++ b/snapshots/runner.go @@ -4,13 +4,13 @@ import ( "context" "io" - "github.com/cocoonstack/epoch/snapshot" + "github.com/cocoonstack/cocoon-common/snapshot" "github.com/cocoonstack/vk-cocoon/vm" ) var _ snapshot.CocoonRunner = runnerAdapter{} -// runnerAdapter wraps vm.Runtime to satisfy epoch's snapshot.CocoonRunner. +// runnerAdapter wraps vm.Runtime to satisfy snapshot.CocoonRunner. type runnerAdapter struct { Runtime vm.Runtime } From ab79acaf2630443faf2e407b65bfad20918e8655 Mon Sep 17 00:00:00 2001 From: CMGS Date: Wed, 1 Jul 2026 14:33:53 +0800 Subject: [PATCH 7/8] feat: drop the epoch backend, go OCI-only vk now imports zero epoch: buildRegistry requires OCI_REGISTRY and always builds a cocoon-common oci client (GCP ADC then docker config). Removes the registryclient fallback, EPOCH_URL/TOKEN, and the dual-backend log. Tests use a fakeRegistry stub; the epoch dependency is gone from go.mod. Bumps cocoon-common to the HasBlob/ImportImage rename. --- README.md | 12 +++------ go.mod | 7 +---- go.sum | 47 +++------------------------------- main.go | 32 ++++++----------------- main_test.go | 17 ++++-------- provider/cocoon/create_test.go | 46 +++++++++++++++++++++++---------- 6 files changed, 54 insertions(+), 107 deletions(-) diff --git a/README.md b/README.md index 47ad0e2..ed326c6 100644 --- a/README.md +++ b/README.md @@ -54,7 +54,7 @@ The only update vk-cocoon honors is a `HibernateState` transition. Anything else | `false → true` | NetResize (CH+Windows) → SnapshotSave → Push → clear VMID before Remove → Remove (rollback on failure). Pod stays alive (`PodRunning`) so K8s controllers do not recreate it. VMID/IP annotations clear between Push and Remove so the operator's manifest+VMID race window collapses to one patch RTT. **Compensating rollback**: if `Runtime.Remove` fails after a successful push, vk-cocoon best-effort `Registry.DeleteManifest` the hibernate tag and re-applies VMID/IP so the pod stays recoverable. Push and Save are idempotent, so a compensated retry re-publishes the tag cleanly on the next attempt. | | `true → false` (with no live VM) | `Puller.PullSnapshot(tag=meta.HibernateSnapshotTag)` → `Runtime.Clone` → drop the hibernation tag from the registry. | -The operator's `CocoonHibernation` reconciler tracks the transition by polling `epoch.GetManifest(vmName, "hibernate")`. +The operator's `CocoonHibernation` reconciler tracks the transition by polling the registry for the `hibernate` manifest. ### Node resources @@ -178,10 +178,7 @@ If the ICMP raw socket cannot be opened — typically because the binary is runn | `KUBECONFIG` | unset | Path to kubeconfig (in-cluster used otherwise). | | `VK_NODE_NAME` | `cocoon-pool` | Virtual node name registered with the K8s API. | | `VK_LOG_LEVEL` | `info` | `projecteru2/core/log` level. | -| `OCI_REGISTRY` | unset | OCI registry base (e.g. an Artifact Registry repo). When set, snapshots and cloud images use it instead of the epoch backend below. | -| `EPOCH_URL` | `http://epoch.cocoon-system.svc:8080` | Epoch backend base URL (used when `OCI_REGISTRY` is unset). | -| `EPOCH_TOKEN` | unset | Bearer token for the epoch backend's `/v2/` API. | -| `EPOCH_CA_CERT` | unset | Path to PEM-encoded CA certificate for TLS verification against the epoch backend. | +| `OCI_REGISTRY` | **required** | OCI registry base for snapshots and cloud images (e.g. an Artifact Registry repo). Auth resolves GCP ADC then docker config. | | `VK_LEASES_PATH` | `/var/lib/cocoon/net/leases.json` | cocoon-net JSON lease file. | | `VK_COCOON_BIN` | `/usr/local/bin/cocoon` | Path to the cocoon CLI binary. | | `VK_ORPHAN_POLICY` | `destroy` | `destroy` (auto-clean), `alert`, or `keep`. | @@ -226,17 +223,16 @@ make fmt # gofumpt + goimports make help # show all targets ``` -The Makefile detects Go workspace mode (`go env GOWORK`) and skips `go mod tidy` when active so cross-module references resolve through `go.work` without forcing a release of cocoon-common or epoch. +The Makefile detects Go workspace mode (`go env GOWORK`) and skips `go mod tidy` when active so cross-module references resolve through `go.work` without forcing a release of cocoon-common. ## Related projects | Project | Role | |---|---| | [cocoon](https://github.com/cocoonstack/cocoon) | The MicroVM runtime vk-cocoon shells out to. | -| [cocoon-common](https://github.com/cocoonstack/cocoon-common) | CRD types, annotation contract, shared helpers. | +| [cocoon-common](https://github.com/cocoonstack/cocoon-common) | CRD types, annotation contract, shared helpers, and the OCI registry + snapshot/cloud-image packages. | | [cocoon-operator](https://github.com/cocoonstack/cocoon-operator) | CocoonSet and CocoonHibernation reconcilers. | | [cocoon-webhook](https://github.com/cocoonstack/cocoon-webhook) | Admission webhook for sticky scheduling and CocoonSet validation. | -| [epoch](https://github.com/cocoonstack/epoch) | Transitional registry backend (`registryclient`); the snapshot/cloud-image code now lives in `cocoon-common`. | | [cocoon-net](https://github.com/cocoonstack/cocoon-net) | Per-host networking with embedded DHCP server and iptables setup; vk-cocoon reads its JSON lease file. | ## License diff --git a/go.mod b/go.mod index 0f1e7ac..e20b35d 100644 --- a/go.mod +++ b/go.mod @@ -3,8 +3,7 @@ module github.com/cocoonstack/vk-cocoon go 1.25.6 require ( - github.com/cocoonstack/cocoon-common v0.2.3-0.20260701050723-6e675f810e8d - github.com/cocoonstack/epoch v0.2.4 + github.com/cocoonstack/cocoon-common v0.2.3-0.20260701062002-fd96a6d3865b github.com/google/go-containerregistry v0.21.7 github.com/projecteru2/core v0.0.0-20241016125006-ff909eefe04c github.com/prometheus/client_golang v1.23.2 @@ -33,12 +32,10 @@ require ( github.com/cockroachdb/errors v1.9.1 // indirect github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b // indirect github.com/cockroachdb/redact v1.1.3 // indirect - github.com/containerd/stargz-snapshotter/estargz v0.18.2 // indirect github.com/coreos/go-semver v0.3.1 // indirect github.com/coreos/go-systemd/v22 v22.5.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/docker/cli v29.5.3+incompatible // indirect - github.com/docker/distribution v2.8.3+incompatible // indirect github.com/docker/docker-credential-helpers v0.9.3 // indirect github.com/emicklei/go-restful/v3 v3.12.2 // indirect github.com/evanphx/json-patch/v5 v5.9.11 // indirect @@ -73,7 +70,6 @@ require ( github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect - github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/moby/spdystream v0.5.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect @@ -92,7 +88,6 @@ require ( github.com/spf13/cobra v1.10.2 // indirect github.com/spf13/pflag v1.0.10 // indirect github.com/stoewer/go-strcase v1.3.0 // indirect - github.com/vbatts/tar-split v0.12.2 // indirect github.com/x448/float16 v0.8.4 // indirect go.etcd.io/etcd/api/v3 v3.6.5 // indirect go.etcd.io/etcd/client/pkg/v3 v3.6.5 // indirect diff --git a/go.sum b/go.sum index 15d1d0f..38689c2 100644 --- a/go.sum +++ b/go.sum @@ -43,15 +43,9 @@ github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b h1:r6VH0faHjZe github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b/go.mod h1:Vz9DsVWQQhf3vs21MhPMZpMGSht7O/2vFW2xusFUVOs= github.com/cockroachdb/redact v1.1.3 h1:AKZds10rFSIj7qADf0g46UixK8NNLwWTNdCIGS5wfSQ= github.com/cockroachdb/redact v1.1.3/go.mod h1:BVNblN9mBWFyMyqK1k3AAiSxhvhfK2oOZZ2lK+dpvRg= -github.com/cocoonstack/cocoon-common v0.2.2 h1:+qY4Iv1nLZw1brH46j33Kvajqi8UR1QKLLTC/AXhU30= -github.com/cocoonstack/cocoon-common v0.2.2/go.mod h1:xIXbJ83vngQ2mrLC6q0Tw7h21M9BYBBqqYTcHaUrm1Y= -github.com/cocoonstack/cocoon-common v0.2.3-0.20260701050723-6e675f810e8d h1:8IAri8O+Sy6r+JDONULPgXx6YULohMFh05108uJLbpg= -github.com/cocoonstack/cocoon-common v0.2.3-0.20260701050723-6e675f810e8d/go.mod h1:/Cf3aBBN0blBxJWexuGuMbTkas+scvQiF2I75aQXkH4= -github.com/cocoonstack/epoch v0.2.4 h1:X18DdsWlPRqEVswR1N3JAazxRZfBUtoL5r7GTgWmhN8= -github.com/cocoonstack/epoch v0.2.4/go.mod h1:1bFeUom4aeFbw3vjFbrLegEgVShqe4qSOfpmH5UEpww= +github.com/cocoonstack/cocoon-common v0.2.3-0.20260701062002-fd96a6d3865b h1:c8hva3RL4dIpWugsvgD7rs/E2IPSE8gKrVy5nrbRlLg= +github.com/cocoonstack/cocoon-common v0.2.3-0.20260701062002-fd96a6d3865b/go.mod h1:/Cf3aBBN0blBxJWexuGuMbTkas+scvQiF2I75aQXkH4= github.com/codegangsta/inject v0.0.0-20150114235600-33e0aa1cb7c0/go.mod h1:4Zcjuz89kmFXt9morQgcfYZAYZ5n8WHjt81YYWIwtTM= -github.com/containerd/stargz-snapshotter/estargz v0.18.2 h1:yXkZFYIzz3eoLwlTUZKz2iQ4MrckBxJjkmD16ynUTrw= -github.com/containerd/stargz-snapshotter/estargz v0.18.2/go.mod h1:XyVU5tcJ3PRpkA9XS2T5us6Eg35yM0214Y+wvrZTBrY= github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= @@ -68,12 +62,8 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgraph-io/badger v1.6.0/go.mod h1:zwt7syl517jmP8s94KqSxTlM6IMsdhYy6psNgSztDR4= github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= -github.com/docker/cli v29.2.1+incompatible h1:n3Jt0QVCN65eiVBoUTZQM9mcQICCJt3akW4pKAbKdJg= -github.com/docker/cli v29.2.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/cli v29.5.3+incompatible h1:nbEFfz774vBwQ5KRYv7c/AghjReqnGISvrRhzjV0evs= github.com/docker/cli v29.5.3+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= -github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk= -github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= github.com/docker/docker-credential-helpers v0.9.3 h1:gAm/VtF9wgqJMoxzT3Gj5p4AqIjCBS4wrsOh9yRqcz8= github.com/docker/docker-credential-helpers v0.9.3/go.mod h1:x+4Gbw9aGmChi3qTLZj8Dfn0TD20M/fuWy0E5+WDeCo= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= @@ -177,8 +167,6 @@ github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= -github.com/google/go-containerregistry v0.21.0 h1:ocqxUOczFwAZQBMNE7kuzfqvDe0VWoZxQMOesXreCDI= -github.com/google/go-containerregistry v0.21.0/go.mod h1:ctO5aCaewH4AK1AumSF5DPW+0+R+d2FmylMJdp5G7p0= github.com/google/go-containerregistry v0.21.7 h1:/vPFuVXDjtFREsVArW+0h1CIl5urnOhzei4X2DMW9IU= github.com/google/go-containerregistry v0.21.7/go.mod h1:kjSbt7/zMsKLWfnHrIvKvhXHUw91jbe9DNjPPJ32gXE= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= @@ -235,8 +223,6 @@ github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.8.2/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= github.com/klauspost/compress v1.9.7/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= -github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c= -github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= 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 v1.2.1/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= @@ -273,7 +259,6 @@ github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D github.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw= github.com/mediocregopher/radix/v3 v3.4.2/go.mod h1:8FL3F6UQRXHXIBSPUs5h0RybMF8i4n7wVopoX3x7Bv8= github.com/microcosm-cc/bluemonday v1.0.2/go.mod h1:iVP4YcDBq+n/5fb23BhYFvIMq/leAFZyRl6bYmGDlGc= -github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= @@ -390,8 +375,6 @@ github.com/valyala/fasthttp v1.6.0/go.mod h1:FstJa9V+Pj9vQ7OJie2qMHdwemEDaDiSdBn github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio= -github.com/vbatts/tar-split v0.12.2 h1:w/Y6tjxpeiFMR47yzZPlPj/FcPLpXbTUi/9H7d3CPa4= -github.com/vbatts/tar-split v0.12.2/go.mod h1:eF6B6i6ftWQcDqEn3/iGFRFRo8cBIMSJVOpnNdfTMFA= github.com/virtual-kubelet/virtual-kubelet v1.12.0 h1:6RnRE3egGnqw3BDL9PBbP5DPV6OaXC2h/nfq5c7VsF4= github.com/virtual-kubelet/virtual-kubelet v1.12.0/go.mod h1:dVlVEsFfrrwAcj/v0eDGgkTF5r+eAsBnn9gDxx3au2s= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= @@ -431,24 +414,18 @@ go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.6 go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0/go.mod h1:rg+RlpR5dKwaS95IyyZqj5Wd4E13lk/msnTS0Xl9lJM= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q= -go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48= -go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8= go.opentelemetry.io/otel v1.41.0 h1:YlEwVsGAlCvczDILpUXpIpPSL/VPugt7zHThEMLce1c= go.opentelemetry.io/otel v1.41.0/go.mod h1:Yt4UwgEKeT05QbLwbyHXEwhnjxNO6D8L5PQP51/46dE= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 h1:OeNbIYk/2C15ckl7glBlOBp5+WlYsOElzTNmiPW/x60= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0/go.mod h1:7Bept48yIeqxP2OZ9/AqIpYS94h2or0aB4FypJTc8ZM= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0 h1:tgJ0uaNS4c98WRNUEx5U3aDlrDOI5Rs+1Vifcw4DJ8U= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0/go.mod h1:U7HYyW0zt/a9x5J1Kjs+r1f/d4ZHnYFclhYY2+YbeoE= -go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0= -go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs= go.opentelemetry.io/otel/metric v1.41.0 h1:rFnDcs4gRzBcsO9tS8LCpgR0dxg4aaxWlJxCno7JlTQ= go.opentelemetry.io/otel/metric v1.41.0/go.mod h1:xPvCwd9pU0VN8tPZYzDZV/BMj9CM9vs00GuBjeKhJps= go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E= go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg= go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM= go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA= -go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI= -go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA= go.opentelemetry.io/otel/trace v1.41.0 h1:Vbk2co6bhj8L59ZJ6/xFTskY+tGAbOnCtQGVVa9TIN0= go.opentelemetry.io/otel/trace v1.41.0/go.mod h1:U1NU4ULCoxeDKc09yCWdWe+3QoyweJcISEVa1RBzOis= go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4= @@ -471,8 +448,6 @@ golang.org/x/crypto v0.0.0-20191227163750-53104e6ec876/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= -golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto= golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -486,9 +461,8 @@ golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzB golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8= -golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w= golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ= +golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -507,13 +481,9 @@ golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwY golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20211008194852-3b03d305991f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60= -golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM= golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o= golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ= -golang.org/x/oauth2 v0.35.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= 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.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -523,8 +493,6 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= -golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM= golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -550,13 +518,9 @@ golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= -golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw= golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg= -golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM= golang.org/x/term v0.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc= golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -565,8 +529,6 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= -golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE= golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4= golang.org/x/time v0.0.0-20201208040808-7e3f01d25324/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -585,9 +547,8 @@ golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapK golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k= -golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0= golang.org/x/tools v0.46.0 h1:7jTurBkPZu4moS/Uy4OQT1M+QBlsj3wejyZwsT8Z7rk= +golang.org/x/tools v0.46.0/go.mod h1:FrD85F8l+NWL+9XWBSyVSHO6Ne4jutsfIFba7AWQ5Ys= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/main.go b/main.go index 7a171ef..b100571 100644 --- a/main.go +++ b/main.go @@ -5,6 +5,7 @@ package main import ( "context" "crypto/tls" + "errors" "fmt" "net/http" "os" @@ -33,7 +34,6 @@ import ( commonlog "github.com/cocoonstack/cocoon-common/log" "github.com/cocoonstack/cocoon-common/meta" "github.com/cocoonstack/cocoon-common/oci" - "github.com/cocoonstack/epoch/registryclient" "github.com/cocoonstack/vk-cocoon/guest/rdp" "github.com/cocoonstack/vk-cocoon/guest/sac" "github.com/cocoonstack/vk-cocoon/metrics" @@ -49,7 +49,6 @@ import ( const ( defaultNodeName = "cocoon-pool" defaultMetricsAddr = ":9091" - defaultEpochURL = "http://epoch.cocoon-system.svc:8080" defaultOrphanPolicy = string(provider.OrphanDestroy) defaultTLSCert = "/etc/cocoon/vk/tls/vk-kubelet.crt" @@ -73,8 +72,6 @@ func main() { nodeName := commonk8s.EnvOrDefault("VK_NODE_NAME", defaultNodeName) metricsAddr := commonk8s.EnvOrDefault("VK_METRICS_ADDR", defaultMetricsAddr) - epochURL := commonk8s.EnvOrDefault("EPOCH_URL", defaultEpochURL) - epochToken := os.Getenv("EPOCH_TOKEN") ociRegistry := os.Getenv("OCI_REGISTRY") leasesPath := commonk8s.EnvOrDefault("VK_LEASES_PATH", network.DefaultLeasesPath) cocoonBin := commonk8s.EnvOrDefault("VK_COCOON_BIN", "") @@ -122,8 +119,6 @@ func main() { p, err := buildProvider(signalCtx, buildOpts{ nodeName: nodeName, - epochURL: epochURL, - epochToken: epochToken, ociRegistry: ociRegistry, leasesPath: leasesPath, cocoonBin: cocoonBin, @@ -219,8 +214,6 @@ func main() { type buildOpts struct { nodeName string - epochURL string - epochToken string ociRegistry string leasesPath string cocoonBin string @@ -229,19 +222,14 @@ type buildOpts struct { recorder record.EventRecorder } -// buildRegistry selects the registry backend: an OCI Distribution registry -// when OCI_REGISTRY is set, else epoch's server. The OCI keychain resolves GCP -// ADC (google.Keychain) before falling back to docker config. +// buildRegistry builds the OCI registry backend from OCI_REGISTRY. The keychain +// resolves GCP ADC (google.Keychain) before falling back to docker config. func buildRegistry(opts buildOpts) (oci.Registry, error) { - if opts.ociRegistry != "" { - keychain := authn.NewMultiKeychain(google.Keychain, authn.DefaultKeychain) - return oci.NewOCIRegistry(opts.ociRegistry, keychain), nil + if opts.ociRegistry == "" { + return nil, errors.New("OCI_REGISTRY must be set") } - client, err := registryclient.NewFromEnv(opts.epochURL, opts.epochToken) - if err != nil { - return nil, err - } - return client, nil + keychain := authn.NewMultiKeychain(google.Keychain, authn.DefaultKeychain) + return oci.NewOCIRegistry(opts.ociRegistry, keychain), nil } func buildProvider(ctx context.Context, opts buildOpts) (*cocoon.Provider, error) { @@ -250,11 +238,7 @@ func buildProvider(ctx context.Context, opts buildOpts) (*cocoon.Provider, error if err != nil { return nil, fmt.Errorf("construct registry client: %w", err) } - backend := "epoch " + opts.epochURL - if opts.ociRegistry != "" { - backend = "OCI " + opts.ociRegistry - } - logger.Infof(ctx, "registry backend: %s", backend) + logger.Infof(ctx, "registry backend: OCI %s", opts.ociRegistry) runtime := vm.NewCocoonCLI(opts.cocoonBin) p := cocoon.NewProvider() p.NodeName = opts.nodeName diff --git a/main_test.go b/main_test.go index e9091ed..f3eb42e 100644 --- a/main_test.go +++ b/main_test.go @@ -4,25 +4,18 @@ import ( "testing" "github.com/cocoonstack/cocoon-common/oci" - "github.com/cocoonstack/epoch/registryclient" ) -func TestBuildRegistryBackend(t *testing.T) { - t.Setenv("EPOCH_CA_CERT", "") // keep the epoch client deterministic on dev machines - +func TestBuildRegistry(t *testing.T) { reg, err := buildRegistry(buildOpts{ociRegistry: "example.com/proj/repo"}) if err != nil { - t.Fatalf("buildRegistry(OCI): %v", err) + t.Fatalf("buildRegistry: %v", err) } if _, ok := reg.(*oci.OCIRegistry); !ok { - t.Fatalf("OCI_REGISTRY set: got %T, want *oci.OCIRegistry", reg) + t.Fatalf("got %T, want *oci.OCIRegistry", reg) } - ep, err := buildRegistry(buildOpts{epochURL: "http://epoch.example"}) - if err != nil { - t.Fatalf("buildRegistry(epoch): %v", err) - } - if _, ok := ep.(*registryclient.Client); !ok { - t.Fatalf("no OCI_REGISTRY: got %T, want *registryclient.Client", ep) + if _, err := buildRegistry(buildOpts{}); err == nil { + t.Fatal("buildRegistry with no OCI_REGISTRY: want error, got nil") } } diff --git a/provider/cocoon/create_test.go b/provider/cocoon/create_test.go index b5a0bed..235286d 100644 --- a/provider/cocoon/create_test.go +++ b/provider/cocoon/create_test.go @@ -6,7 +6,6 @@ import ( "fmt" "io" "net/http" - "net/http/httptest" "os" "path/filepath" "reflect" @@ -24,7 +23,7 @@ import ( utilexec "k8s.io/client-go/util/exec" "github.com/cocoonstack/cocoon-common/meta" - "github.com/cocoonstack/epoch/registryclient" + "github.com/cocoonstack/cocoon-common/oci" "github.com/cocoonstack/vk-cocoon/network" "github.com/cocoonstack/vk-cocoon/probes" "github.com/cocoonstack/vk-cocoon/provider" @@ -378,7 +377,7 @@ func TestCreatePodRunModeInvalidatesForkSnapshot(t *testing.T) { pod := newPodWithSpec(meta.VMSpec{ VMName: "vk-ns-demo-0", - Image: "epoch.example/cocoon/ubuntu:24.04", + Image: "registry.example/cocoon/ubuntu:24.04", Mode: "run", OS: "linux", }) @@ -426,7 +425,7 @@ func TestCreatePodForkFromOverridesRunMode(t *testing.T) { pod := newPodWithSpec(meta.VMSpec{ VMName: "vk-ns-demo-2", - Image: "https://epoch.example.org/dl/windows/win11", + Image: "https://registry.example.org/windows/win11", Mode: "run", OS: "windows", ForkFrom: "vk-ns-demo-0", @@ -890,6 +889,31 @@ func TestEnsureRunImageFallback(t *testing.T) { } } +// fakeRegistry is a stub oci.Registry serving a canned GetManifest; the other +// methods are unused by the ensureRunImage classify dispatch. +type fakeRegistry struct { + manifest []byte + err error +} + +var _ oci.Registry = fakeRegistry{} + +func (f fakeRegistry) GetManifest(context.Context, string, string) ([]byte, string, error) { + return f.manifest, "", f.err +} + +func (fakeRegistry) GetBlob(context.Context, string, string) (io.ReadCloser, error) { return nil, nil } + +func (fakeRegistry) HasBlob(context.Context, string, string) (bool, error) { return false, nil } + +func (fakeRegistry) PutBlob(context.Context, string, string, io.Reader, int64) error { return nil } + +func (fakeRegistry) PutManifest(context.Context, string, string, []byte, string) error { return nil } + +func (fakeRegistry) HasManifest(context.Context, string, string) (bool, error) { return false, nil } + +func (fakeRegistry) DeleteManifest(context.Context, string, string) error { return nil } + func TestEnsureRunImageDispatch(t *testing.T) { const repo = "ubuntu" @@ -930,15 +954,9 @@ func TestEnsureRunImageDispatch(t *testing.T) { } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { - srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.WriteHeader(tc.manifestStatus) - _, _ = io.WriteString(w, tc.manifestBody) - })) - defer srv.Close() - - client, err := registryclient.New(srv.URL, "") - if err != nil { - t.Fatalf("registryclient.New: %v", err) + reg := fakeRegistry{manifest: []byte(tc.manifestBody)} + if tc.manifestStatus/100 != 2 { + reg.err = errors.New("registry error") } // Cloudimg path imports under the local ref (repo:tag), so the // fake runtime's Image lookup must key on the same form. @@ -949,7 +967,7 @@ func TestEnsureRunImageDispatch(t *testing.T) { } p := newTestProvider(t) p.Runtime = rt - p.Puller = &snapshots.Puller{Registry: client, Runtime: rt} + p.Puller = &snapshots.Puller{Registry: reg, Runtime: rt} got, err := p.ensureRunImage(t.Context(), repo, false) switch { From de0a8e62e1b04cadabe25bb813c650345a2aac06 Mon Sep 17 00:00:00 2001 From: CMGS Date: Wed, 1 Jul 2026 14:49:35 +0800 Subject: [PATCH 8/8] build: pin cocoon-common to the merged main commit --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index e20b35d..23535bc 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/cocoonstack/vk-cocoon go 1.25.6 require ( - github.com/cocoonstack/cocoon-common v0.2.3-0.20260701062002-fd96a6d3865b + github.com/cocoonstack/cocoon-common v0.2.3-0.20260701064759-3dcdfdd23a16 github.com/google/go-containerregistry v0.21.7 github.com/projecteru2/core v0.0.0-20241016125006-ff909eefe04c github.com/prometheus/client_golang v1.23.2 diff --git a/go.sum b/go.sum index 38689c2..7931c1e 100644 --- a/go.sum +++ b/go.sum @@ -43,8 +43,8 @@ github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b h1:r6VH0faHjZe github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b/go.mod h1:Vz9DsVWQQhf3vs21MhPMZpMGSht7O/2vFW2xusFUVOs= github.com/cockroachdb/redact v1.1.3 h1:AKZds10rFSIj7qADf0g46UixK8NNLwWTNdCIGS5wfSQ= github.com/cockroachdb/redact v1.1.3/go.mod h1:BVNblN9mBWFyMyqK1k3AAiSxhvhfK2oOZZ2lK+dpvRg= -github.com/cocoonstack/cocoon-common v0.2.3-0.20260701062002-fd96a6d3865b h1:c8hva3RL4dIpWugsvgD7rs/E2IPSE8gKrVy5nrbRlLg= -github.com/cocoonstack/cocoon-common v0.2.3-0.20260701062002-fd96a6d3865b/go.mod h1:/Cf3aBBN0blBxJWexuGuMbTkas+scvQiF2I75aQXkH4= +github.com/cocoonstack/cocoon-common v0.2.3-0.20260701064759-3dcdfdd23a16 h1:4gS1Pr0nCN/qoa344ZHqy2vZPaCW8JcCocB7mgqJmUk= +github.com/cocoonstack/cocoon-common v0.2.3-0.20260701064759-3dcdfdd23a16/go.mod h1:/Cf3aBBN0blBxJWexuGuMbTkas+scvQiF2I75aQXkH4= github.com/codegangsta/inject v0.0.0-20150114235600-33e0aa1cb7c0/go.mod h1:4Zcjuz89kmFXt9morQgcfYZAYZ5n8WHjt81YYWIwtTM= github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=