diff --git a/CubeMaster/go.mod b/CubeMaster/go.mod index 7ef722dbd..654a35c37 100644 --- a/CubeMaster/go.mod +++ b/CubeMaster/go.mod @@ -12,9 +12,11 @@ require ( github.com/charmbracelet/bubbletea v1.3.10 github.com/charmbracelet/lipgloss v1.1.0 github.com/charmbracelet/x/term v0.2.2 + github.com/containerd/containerd v1.7.32 github.com/fsnotify/fsnotify v1.9.0 github.com/go-sql-driver/mysql v1.9.3 github.com/gomodule/redigo v1.9.3 + github.com/google/go-containerregistry v0.21.6 github.com/google/uuid v1.6.0 github.com/gorilla/mux v1.8.0 github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 @@ -32,7 +34,7 @@ require ( github.com/urfave/cli v1.22.14 golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f golang.org/x/sync v0.20.0 - golang.org/x/sys v0.43.0 + golang.org/x/sys v0.44.0 golang.org/x/time v0.14.0 google.golang.org/grpc v1.80.0 google.golang.org/protobuf v1.36.11 @@ -47,8 +49,9 @@ require ( require ( dario.cat/mergo v1.0.0 // indirect filippo.io/edwards25519 v1.2.0 // indirect - github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect + github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect github.com/Microsoft/go-winio v0.6.2 // indirect + github.com/Microsoft/hcsshim v0.11.7 // indirect github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/beorn7/perks v1.0.1 // indirect @@ -61,31 +64,43 @@ require ( github.com/clipperhouse/displaywidth v0.9.0 // indirect github.com/clipperhouse/stringish v0.1.1 // indirect github.com/clipperhouse/uax29/v2 v2.5.0 // indirect + github.com/containerd/cgroups v1.1.0 // indirect github.com/containerd/continuity v0.4.5 // indirect - github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect + github.com/containerd/errdefs v1.0.0 // indirect + github.com/containerd/errdefs/pkg v0.3.0 // indirect + github.com/containerd/log v0.1.0 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect github.com/davecgh/go-spew v1.1.1 // indirect - github.com/docker/cli v27.4.1+incompatible // indirect - github.com/docker/docker v27.1.1+incompatible // indirect + github.com/distribution/reference v0.6.0 // indirect + github.com/docker/cli v29.4.3+incompatible // indirect + github.com/docker/docker-credential-helpers v0.9.3 // indirect github.com/docker/go-connections v0.7.0 // indirect github.com/docker/go-units v0.5.0 // indirect github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fxamacker/cbor/v2 v2.9.0 // indirect github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect github.com/go-viper/mapstructure/v2 v2.1.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect - github.com/kr/text v0.2.0 // indirect + github.com/klauspost/compress v1.18.6 // indirect github.com/lucasb-eyer/go-colorful v1.3.0 // indirect github.com/mattn/go-isatty v0.0.21 // indirect github.com/mattn/go-localereader v0.0.1 // indirect github.com/mattn/go-runewidth v0.0.19 // indirect github.com/mfridman/interpolate v0.0.2 // indirect github.com/moby/docker-image-spec v1.3.1 // indirect + github.com/moby/moby/api v1.54.2 // indirect + github.com/moby/moby/client v0.4.1 // indirect + github.com/moby/sys/sequential v0.7.0 // indirect github.com/moby/sys/user v0.3.0 // indirect - github.com/moby/term v0.5.0 // indirect + github.com/moby/sys/userns v0.1.0 // indirect + github.com/moby/term v0.5.2 // 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/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect @@ -100,23 +115,28 @@ require ( github.com/prometheus/common v0.66.1 // indirect github.com/prometheus/procfs v0.20.1 // indirect github.com/rivo/uniseg v0.4.7 // indirect - github.com/rogpeppe/go-internal v1.14.1 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/sethvargo/go-retry v0.3.0 // indirect - github.com/sirupsen/logrus v1.9.3 // indirect + github.com/sirupsen/logrus v1.9.4 // indirect github.com/x448/float16 v0.8.4 // indirect github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect github.com/xeipuuv/gojsonschema v1.2.0 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect github.com/yuin/gopher-lua v1.1.1 // indirect + go.opencensus.io v0.24.0 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 // indirect + go.opentelemetry.io/otel v1.43.0 // indirect + go.opentelemetry.io/otel/metric v1.43.0 // indirect + go.opentelemetry.io/otel/trace v1.43.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect - golang.org/x/net v0.53.0 // indirect - golang.org/x/text v0.36.0 // indirect - google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect + golang.org/x/net v0.54.0 // indirect + golang.org/x/text v0.37.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260420184626-e10c466a9529 // indirect gopkg.in/inf.v0 v0.9.1 // indirect - gopkg.in/yaml.v2 v2.4.0 // indirect k8s.io/klog/v2 v2.130.1 // indirect k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 // indirect sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect diff --git a/CubeMaster/go.sum b/CubeMaster/go.sum index a8ee95a7a..6edc1194e 100644 --- a/CubeMaster/go.sum +++ b/CubeMaster/go.sum @@ -1,27 +1,26 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo= filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc= -github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= -github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg= +github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/Microsoft/hcsshim v0.11.7 h1:vl/nj3Bar/CvJSYo7gIQPyRWc9f3c6IeSNavBTSZNZQ= +github.com/Microsoft/hcsshim v0.11.7/go.mod h1:MV8xMfmECjl5HdO7U/3/hFVnkmSBjAjmA09d4bExKcU= github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEVMRuU21PR1EtLVZJmdB18Gu3Rw= github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk= -github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/agiledragon/gomonkey v2.0.2+incompatible h1:eXKi9/piiC3cjJD1658mEE2o3NjkJ5vDLgYjCQu0Xlw= github.com/agiledragon/gomonkey v2.0.2+incompatible/go.mod h1:2NGfXu1a80LLr2cmWXGBDaHEjb1idR6+FVlX5T3D9hw= github.com/agiledragon/gomonkey/v2 v2.9.0 h1:PDiKKybR596O6FHW+RVSG0Z7uGCBNbmbUXh3uCNQ7Hc= github.com/agiledragon/gomonkey/v2 v2.9.0/go.mod h1:ap1AmDzcVOAz1YpeJ3TCzIgstoaWLA6jbbgxfB4w2iY= github.com/alicebob/miniredis/v2 v2.35.0 h1:QwLphYqCEAo1eu1TqPRN2jgVMPBweeQcR21jeqDCONI= github.com/alicebob/miniredis/v2 v2.35.0/go.mod h1:TcL7YfarKPGDAthEtl5NBeHZfeUQj6OXMm/+iu5cLMM= -github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= @@ -29,7 +28,6 @@ github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6r github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/charmbracelet/bubbles v1.0.0 h1:12J8/ak/uCZEMQ6KU7pcfwceyjLlWsDLAxB5fXonfvc= @@ -56,22 +54,32 @@ github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEX github.com/clipperhouse/uax29/v2 v2.5.0 h1:x7T0T4eTHDONxFJsL94uKNKPHrclyFI0lm7+w94cO8U= github.com/clipperhouse/uax29/v2 v2.5.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= -github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= -github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/containerd/cgroups v1.1.0 h1:v8rEWFl6EoqHB+swVNjVoCJE8o3jX7e8nqBGPLaDFBM= +github.com/containerd/cgroups v1.1.0/go.mod h1:6ppBcbh/NOOUU+dMKrykgaBnK9lCIBxHqJDGwsa1mIw= +github.com/containerd/containerd v1.7.32 h1:S54xuVcPxeLaYgaRABtpJ2VyVUVsy0IGf7qHBs+sbY8= +github.com/containerd/containerd v1.7.32/go.mod h1:jdwD6s/BhV4XVJGrvtziNPVA+83n66TwptVaPKprq4E= github.com/containerd/continuity v0.4.5 h1:ZRoN1sXq9u7V6QoHMcVWGhOwDFqZ4B9i5H6un1Wh0x4= github.com/containerd/continuity v0.4.5/go.mod h1:/lNJvtJKUQStBzpVQ1+rasXO1LAWtUQssk28EZvJ3nE= -github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= +github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= +github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= +github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= +github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= +github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= +github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= -github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= +github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo= +github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s= +github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/docker/cli v27.4.1+incompatible h1:VzPiUlRJ/xh+otB75gva3r05isHMo5wXDfPRi5/b4hI= -github.com/docker/cli v27.4.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= -github.com/docker/docker v27.1.1+incompatible h1:hO/M4MtV36kzKldqnA37IWhebRA+LnqqcqDja6kVaKY= -github.com/docker/docker v27.1.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= +github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= +github.com/docker/cli v29.4.3+incompatible h1:u+UliYm2J/rYrIh2FqHQg32neRG8GjbvNuwQRTzGspU= +github.com/docker/cli v29.4.3+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +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/docker/go-connections v0.7.0 h1:6SsRfJddP22WMrCkj19x9WKjEDTB+ahsdiGYf0mN39c= github.com/docker/go-connections v0.7.0/go.mod h1:no1qkHdjq7kLMGUXYAduOhYPSJxxvgWBh7ogVvptn3Q= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= @@ -81,17 +89,17 @@ github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+m github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= -github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= -github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= -github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= @@ -104,20 +112,19 @@ github.com/go-viper/mapstructure/v2 v2.1.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlnd github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= -github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/gomodule/redigo v1.9.3 h1:dNPSXeXv6HCq2jdyWfjgmhBdqnR6PRO3m/G05nvpPC8= @@ -127,9 +134,11 @@ github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.3/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.6 h1:T+yqQIlJXKrM98Om4DlW3GoWQAmhZuLMwoDOvVrtiUM= +github.com/google/go-containerregistry v0.21.6/go.mod h1:U7MMSBIJynke2MVQrQk19NP9k/uQsGz/h0amIFSHMbo= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= @@ -141,7 +150,6 @@ github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo= github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA= -github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -156,8 +164,8 @@ github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHm github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE= -github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= +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/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= @@ -178,10 +186,20 @@ github.com/mfridman/interpolate v0.0.2 h1:pnuTK7MQIxxFz1Gr+rjSIx9u7qVjf5VOoM/u6B github.com/mfridman/interpolate v0.0.2/go.mod h1:p+7uk6oE07mpE/Ik1b8EckO0O4ZXiGAfshKBWLUM9Xg= github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= +github.com/moby/moby/api v1.54.2 h1:wiat9QAhnDQjA7wk1kh/TqHz2I1uUA7M7t9SAl/JNXg= +github.com/moby/moby/api v1.54.2/go.mod h1:+RQ6wluLwtYaTd1WnPLykIDPekkuyD/ROWQClE83pzs= +github.com/moby/moby/client v0.4.1 h1:DMQgisVoMkmMs7fp3ROSdiBnoAu8+vo3GggFl06M/wY= +github.com/moby/moby/client v0.4.1/go.mod h1:z52C9O2POPOsnxZAy//WtKcQ32P+jT/NGeXu/7nfjGQ= +github.com/moby/sys/mountinfo v0.7.1 h1:/tTvQaSJRr2FshkhXiIpux6fQ2Zvc4j7tAhMTStAG2g= +github.com/moby/sys/mountinfo v0.7.1/go.mod h1:IJb6JQeOklcdMU9F5xQ8ZALD+CUr5VlGpwtX+VE0rpI= +github.com/moby/sys/sequential v0.7.0 h1:ASQNGNROJSuOO6LL6bPHbKvuZu6NU8P4ldPWk31zj/8= +github.com/moby/sys/sequential v0.7.0/go.mod h1:NfSTAp6V3fw4tmkD62PEcOKeZKquXT8VKCkf7aVR79o= github.com/moby/sys/user v0.3.0 h1:9ni5DlcW5an3SvRSx4MouotOygvzaXbaSrc/wGDFWPo= github.com/moby/sys/user v0.3.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs= -github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= -github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= +github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g= +github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28= +github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ= +github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -229,30 +247,27 @@ github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94 github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= -github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sethvargo/go-retry v0.3.0 h1:EEt31A35QhrcRZtrYFDTBg91cqZVnFL2navjDrah2SE= github.com/sethvargo/go-retry v0.3.0/go.mod h1:mNX17F0C/HguQMyMyJxcnU471gOZGxCLyYaFyAZraas= -github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= -github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w= +github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= github.com/smallnest/weighted v0.0.0-20230419055410-36b780e40a7a h1:eieNTZmrnPzIBWd/tAc2+60qroyOzAoM/Q3FiTwHG1o= github.com/smallnest/weighted v0.0.0-20230419055410-36b780e40a7a/go.mod h1:xc9CoZ+ZBGwajnWto5Aqw/wWg8euy4HtOr6K9Fxp9iw= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= -github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= -github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= -github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= +github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= @@ -273,25 +288,30 @@ github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M= github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw= +go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= +go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 h1:CqXxU8VOmDefoh0+ztfGaymYbhdB/tT3zs79QaZTNGY= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0/go.mod h1:BuhAPThV8PBHBvg8ZzZ/Ok3idOdhWIodywz2xEcRbJo= go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0= go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM= go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY= -go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18= -go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE= -go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8= -go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew= +go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg= +go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg= +go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw= +go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A= go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A= go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= -go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= @@ -310,24 +330,22 @@ golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCc golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= 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.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4= +golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ= 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-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA= -golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs= +golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.54.0 h1:2zJIZAxAHV/OHCDTCOHAYehQzLfSXuf/5SoL/Dv6w/w= +golang.org/x/net v0.54.0/go.mod h1:Sj4oj8jK6XmHpBZU/zWHw3BV3abl4Kvi+Ut7cQcY+cQ= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 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-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 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= @@ -337,22 +355,15 @@ golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= -golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ= +golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 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.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg= -golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164= +golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= +golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -365,6 +376,8 @@ golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 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.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8= +golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0= 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= @@ -375,17 +388,14 @@ google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9Ywl google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa h1:I0YcKz0I7OAhddo7ya8kMnvprhcWM045PmkBdMO9zN0= -google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260420184626-e10c466a9529 h1:XF8+t6QQiS0o9ArVan/HW8Q7cycNPGsJf6GA2nXxYAg= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260420184626-e10c466a9529/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= -google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= +google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM= google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= @@ -397,9 +407,6 @@ google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2 google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= -google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -407,9 +414,6 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntN gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= @@ -419,8 +423,8 @@ gorm.io/driver/mysql v1.5.1/go.mod h1:Jo3Xu7mMhCyj8dlrb3WoCaRd1FhsVh+yMXb1jUInf5 gorm.io/gorm v1.25.1/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k= gorm.io/gorm v1.25.2 h1:gs1o6Vsa+oVKG/a9ElL3XgyGfghFfkKA2SInQaCyMho= gorm.io/gorm v1.25.2/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k= -gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU= -gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= +gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= +gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= 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.34.1 h1:jC+153630BMdlFukegoEL8E/yT7aLyQkIVuwhmwDgJM= @@ -441,6 +445,8 @@ modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= modernc.org/sqlite v1.49.1 h1:dYGHTKcX1sJ+EQDnUzvz4TJ5GbuvhNJa8Fg6ElGx73U= modernc.org/sqlite v1.49.1/go.mod h1:m0w8xhwYUVY3H6pSDwc3gkJ/irZT/0YEXwBlhaxQEew= +pgregory.net/rapid v1.2.0 h1:keKAYRcjm+e1F0oAuU5F5+YPAWcyxNNRK2wud503Gnk= +pgregory.net/rapid v1.2.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04= sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg= sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= diff --git a/CubeMaster/pkg/templatecenter/image/disk.go b/CubeMaster/pkg/templatecenter/image/disk.go index abf86b9e3..130fe08b5 100644 --- a/CubeMaster/pkg/templatecenter/image/disk.go +++ b/CubeMaster/pkg/templatecenter/image/disk.go @@ -41,7 +41,7 @@ func sourceRefForLog(source *PreparedSource) string { // an ext4 image, avoiding any intermediate rootfs directory on disk (Phase 2). // Falls back to Phase 1 when prerequisites are not met. // estimatedSizeBytes should be obtained from estimateImageSizeFromInspect. -func createExt4ImageStreaming(ctx context.Context, source *PreparedSource, workDir, ext4Path string, estimatedSizeBytes int64) error { +func createExt4ImageStreaming(ctx context.Context, source *PreparedSource, workDir, ext4Path string, estimatedSizeBytes int64, postExport func(context.Context, string) error) error { if !canUseLoopMount() { return fmt.Errorf("loop mount not available") } @@ -92,18 +92,31 @@ func createExt4ImageStreaming(ctx context.Context, source *PreparedSource, workD return fmt.Errorf("mount loop device %s: %w", loopDevice, err) } - // 4. Create container and stream export directly into the mounted ext4. - containerIDBytes, err := dockerOutput(ctx, "", "create", "--", source.LocalRef) - if err != nil { - return fmt.Errorf("docker create for streaming: %w", err) + // 4. Stream export directly into the mounted ext4. + if source.ExportMode == ExportModeNative { + if err := StreamRegistryToDir(ctx, source, mountPoint); err != nil { + return fmt.Errorf("native streaming to mount point: %w", err) + } + } else { + containerIDBytes, err := dockerOutput(ctx, "", "create", "--", source.LocalRef) + if err != nil { + return fmt.Errorf("docker create for streaming: %w", err) + } + containerID := strings.TrimSpace(string(containerIDBytes)) + defer func() { + _ = dockerRun(cleanupCtx, "", "rm", "-f", containerID) + }() + + if err := pipeExportToDir(ctx, containerID, mountPoint); err != nil { + return fmt.Errorf("pipe export to mount point: %w", err) + } } - containerID := strings.TrimSpace(string(containerIDBytes)) - defer func() { - _ = dockerRun(cleanupCtx, "", "rm", "-f", containerID) - }() - if err := pipeExportToDir(ctx, containerID, mountPoint); err != nil { - return fmt.Errorf("pipe export to mount point: %w", err) + // 4.5 Execute post-export hook before unmounting. + if postExport != nil { + if err := postExport(ctx, mountPoint); err != nil { + return fmt.Errorf("post-rootfs export hook failed during streaming: %w", err) + } } // 5. Unmount (via cleanup). @@ -195,17 +208,17 @@ func skopeoLayersTotalSize(info skopeoInspectImage) int64 { // time via `skopeo inspect`. The docker path keeps using the per-image // cumulative Size field from `docker image inspect`. func estimateImageSizeFromInspect(ctx context.Context, source *PreparedSource) (int64, error) { - if source != nil && source.UseDockerless { - return estimateImageSizeFromSkopeo(source) + if source != nil && (source.ExportMode == ExportModeDockerless || source.ExportMode == ExportModeNative) { + return estimateImageSizeFromCompressedBytes(source) } return estimateImageSizeFromDocker(ctx, source) } -// estimateImageSizeFromSkopeo derives the estimate from the compressed layer -// sizes captured by `skopeo inspect`, avoiding any docker invocation. -func estimateImageSizeFromSkopeo(source *PreparedSource) (int64, error) { +// estimateImageSizeFromCompressedBytes derives the estimate from the compressed layer +// sizes recorded during PrepareSource, avoiding any docker invocation. +func estimateImageSizeFromCompressedBytes(source *PreparedSource) (int64, error) { if source == nil || source.CompressedSizeBytes <= 0 { - return 0, fmt.Errorf("skopeo inspect did not report any layer sizes for %s", sourceRefForLog(source)) + return 0, fmt.Errorf("did not find any compressed layer sizes for %s", sourceRefForLog(source)) } return source.CompressedSizeBytes * skopeoInspectSizeMultiplier, nil } diff --git a/CubeMaster/pkg/templatecenter/image/export.go b/CubeMaster/pkg/templatecenter/image/export.go index 80afe98df..fe65fa9f5 100644 --- a/CubeMaster/pkg/templatecenter/image/export.go +++ b/CubeMaster/pkg/templatecenter/image/export.go @@ -17,12 +17,25 @@ func exportImageRootfs(ctx context.Context, source *PreparedSource, destRootfsDi if source == nil { return errors.New("resolved source image is nil") } - if err := validateImageRef(source.LocalRef); err != nil { - return err + if source.ExportMode != ExportModeNative { + if err := validateImageRef(source.LocalRef); err != nil { + return err + } + } + + if source.ExportMode == ExportModeNative { + if err := os.RemoveAll(destRootfsDir); err != nil { + return err + } + if err := os.MkdirAll(destRootfsDir, 0o755); err != nil { + return err + } + return StreamRegistryToDir(ctx, source, destRootfsDir) } + // Use the strategy chosen at prepare time rather than re-detecting, so the - // prepare and export phases never diverge (see PreparedSource.useDockerless). - if source.UseDockerless { + // prepare and export phases never diverge. + if source.ExportMode == ExportModeDockerless { return dockerlessExportImageRootfs(ctx, source, destRootfsDir) } return dockerExportImageRootfs(ctx, source, destRootfsDir) diff --git a/CubeMaster/pkg/templatecenter/image/ext4.go b/CubeMaster/pkg/templatecenter/image/ext4.go index 1c06c39a6..60dddb446 100644 --- a/CubeMaster/pkg/templatecenter/image/ext4.go +++ b/CubeMaster/pkg/templatecenter/image/ext4.go @@ -55,18 +55,30 @@ func createExt4Image(ctx context.Context, rootfsDir, ext4Path string) error { return nil } +// EnsureArtifactBuildPreflight asserts that the host has all necessary tools +// installed to build images before starting a long-running workflow. func EnsureArtifactBuildPreflight(ctx context.Context) error { requiredCommands := []string{"mkfs.ext4", "truncate", "cp"} - if hasDockerlessRootfsExportTools() { - requiredCommands = append(requiredCommands, "skopeo", "umoci") - } else { - requiredCommands = append(requiredCommands, "docker", "tar") + if !nativeRootfsExportEnabled() { + if hasDockerlessRootfsExportTools() { + requiredCommands = append(requiredCommands, "skopeo", "umoci") + } else { + requiredCommands = append(requiredCommands, "docker", "tar") + } + } + if loopMountExt4Enabled() { + requiredCommands = append(requiredCommands, "losetup", "mount", "umount", "resize2fs") } + for _, cmd := range requiredCommands { if _, err := executableLookPath(cmd); err != nil { - return fmt.Errorf("required command %q is not available on cubemaster node", cmd) + return fmt.Errorf("required command %q is not available: %w", cmd, err) } } + return checkMkfsExt4DSupport(ctx) +} + +func checkMkfsExt4DSupport(ctx context.Context) error { output, err := exec.CommandContext(ctx, "mkfs.ext4", "-h").CombinedOutput() helpText := string(output) if err != nil && helpText == "" { @@ -115,10 +127,9 @@ func BuildExt4(ctx context.Context, source *PreparedSource, opts BuildOptions) ( keepStoreDir := false // Phase 2: loop-mount streaming build (optional, auto-detects capability). - // Skip when a PostRootfsExport hook is requested — the streaming path - // writes directly into the ext4 image and offers no rootfs directory to - // mutate before mkfs.ext4. - if opts.PostRootfsExport == nil && loopMountExt4Enabled() && canUseLoopMount() && !source.UseDockerless { + // Passes PostRootfsExport down to be executed before unmounting the loop device. + // Streaming Phase 2 is currently only implemented for docker and native modes. + if loopMountExt4Enabled() && canUseLoopMount() && (source.ExportMode == ExportModeDocker || source.ExportMode == ExportModeNative) { estimatedPhase2, err := estimateImageSizeFromInspect(ctx, source) if err != nil { log.G(ctx).Warnf("cannot estimate image size for Phase 2, falling back to Phase 1: %v", err) @@ -126,7 +137,7 @@ func BuildExt4(ctx context.Context, source *PreparedSource, opts BuildOptions) ( if err := checkDiskSpace(ctx, storeDir, estimatedPhase2); err != nil { return BuildResult{}, err } - if err := createExt4ImageStreaming(ctx, source, workDir, ext4Path, estimatedPhase2); err != nil { + if err := createExt4ImageStreaming(ctx, source, workDir, ext4Path, estimatedPhase2, opts.PostRootfsExport); err != nil { log.G(ctx).Warnf("loop-mount streaming ext4 build failed, falling back to phase-1: %v", err) _ = os.RemoveAll(workDir) _ = os.Remove(ext4Path) diff --git a/CubeMaster/pkg/templatecenter/image/image_test.go b/CubeMaster/pkg/templatecenter/image/image_test.go index bffe96f0d..6e34f9388 100644 --- a/CubeMaster/pkg/templatecenter/image/image_test.go +++ b/CubeMaster/pkg/templatecenter/image/image_test.go @@ -266,8 +266,8 @@ func TestExportImageRootfsUsesDockerlessSkopeoUmociWhenAvailable(t *testing.T) { }) source := &PreparedSource{ - LocalRef: "cube-sandbox-cn.tencentcloudcr.com/cube-sandbox/sandbox-code:v1.2.3", - UseDockerless: true, + LocalRef: "cube-sandbox-cn.tencentcloudcr.com/cube-sandbox/sandbox-code:v1.2.3", + ExportMode: ExportModeDockerless, } if err := exportImageRootfs(context.Background(), source, rootfsDir); err != nil { t.Fatalf("exportImageRootfs failed: %v", err) @@ -289,11 +289,11 @@ func TestExportImageRootfsUsesDockerlessSkopeoUmociWhenAvailable(t *testing.T) { } wantUmociArgs := []string{ "unpack", - "--rootless", - "--image", - ociDir + ":v1.2.3", - filepath.Join(filepath.Dir(ociDir), "bundle"), } + if os.Geteuid() != 0 { + wantUmociArgs = append(wantUmociArgs, "--rootless") + } + wantUmociArgs = append(wantUmociArgs, "--image", ociDir+":v1.2.3", filepath.Join(filepath.Dir(ociDir), "bundle")) if calls[1].name != "umoci" || !reflect.DeepEqual(calls[1].args, wantUmociArgs) { t.Fatalf("unexpected umoci call: %#v", calls[1]) } @@ -319,9 +319,9 @@ func TestExportImageRootfsUsesDockerPathWhenSourceNotDockerless(t *testing.T) { return nil }) - // useDockerless is false, so the export must honor the docker path chosen at + // ExportMode: ExportModeDocker, so the export must honor the docker path chosen at // prepare time even if skopeo/umoci happen to be installed. - if err := exportImageRootfs(context.Background(), &PreparedSource{LocalRef: "example.com/app:latest"}, t.TempDir()); err != nil { + if err := exportImageRootfs(context.Background(), &PreparedSource{LocalRef: "example.com/app:latest", ExportMode: ExportModeDocker}, t.TempDir()); err != nil { t.Fatalf("exportImageRootfs failed: %v", err) } if !dockerExportCalled { @@ -357,7 +357,7 @@ func TestExportImageRootfsPassesAuthFileToSkopeoCopy(t *testing.T) { source := &PreparedSource{ LocalRef: "example.com/app:latest", - UseDockerless: true, + ExportMode: ExportModeDockerless, SkopeoAuthFile: "/tmp/auth-xyz/auth.json", } if err := exportImageRootfs(context.Background(), source, rootfsDir); err != nil { @@ -546,7 +546,7 @@ func TestEstimateImageSizeFromInspectDockerlessUsesSkopeoSize(t *testing.T) { source := &PreparedSource{ LocalRef: "example.com/app:latest", - UseDockerless: true, + ExportMode: ExportModeDockerless, CompressedSizeBytes: 1000, } got, err := estimateImageSizeFromInspect(context.Background(), source) @@ -560,8 +560,8 @@ func TestEstimateImageSizeFromInspectDockerlessUsesSkopeoSize(t *testing.T) { func TestEstimateImageSizeFromInspectDockerlessMissingSize(t *testing.T) { source := &PreparedSource{ - LocalRef: "example.com/app:latest", - UseDockerless: true, + LocalRef: "example.com/app:latest", + ExportMode: ExportModeDockerless, } if _, err := estimateImageSizeFromInspect(context.Background(), source); err == nil { t.Fatal("expected error when skopeo reports no layer sizes") @@ -855,7 +855,7 @@ func TestBuildExt4StreamingSuccessSkipsPhase1(t *testing.T) { return nil }) streamingCalled := false - patches.ApplyFunc(createExt4ImageStreaming, func(ctx context.Context, source *PreparedSource, workDir, ext4Path string, estimatedSizeBytes int64) error { + patches.ApplyFunc(createExt4ImageStreaming, func(ctx context.Context, source *PreparedSource, workDir, ext4Path string, estimatedSizeBytes int64, postExport func(context.Context, string) error) error { streamingCalled = true if err := os.MkdirAll(workDir, 0o755); err != nil { return err @@ -877,7 +877,7 @@ func TestBuildExt4StreamingSuccessSkipsPhase1(t *testing.T) { return nil }) - result, err := BuildExt4(context.Background(), &PreparedSource{LocalRef: "docker.io/library/nginx:latest"}, BuildOptions{ArtifactID: "artifact-stream"}) + result, err := BuildExt4(context.Background(), &PreparedSource{LocalRef: "docker.io/library/nginx:latest", ExportMode: ExportModeNative}, BuildOptions{ArtifactID: "artifact-stream"}) if err != nil { t.Fatalf("BuildExt4 failed: %v", err) } @@ -892,6 +892,65 @@ func TestBuildExt4StreamingSuccessSkipsPhase1(t *testing.T) { } } +func TestBuildExt4StreamingWithPostExport(t *testing.T) { + workRoot := t.TempDir() + storeRoot := t.TempDir() + t.Setenv("CUBEMASTER_ROOTFS_ARTIFACT_DIR", workRoot) + t.Setenv("CUBEMASTER_ROOTFS_ARTIFACT_STORE_DIR", storeRoot) + + patches := gomonkey.NewPatches() + defer patches.Reset() + patches.ApplyFuncReturn(loopMountExt4Enabled, true) + patches.ApplyFuncReturn(canUseLoopMount, true) + patches.ApplyFuncReturn(estimateImageSizeFromInspect, int64(1024), nil) + patches.ApplyFuncReturn(checkDiskSpace, nil) + postExportCalled := false + streamingCalled := false + patches.ApplyFunc(createExt4ImageStreaming, func(ctx context.Context, source *PreparedSource, workDir, ext4Path string, estimatedSizeBytes int64, postExport func(context.Context, string) error) error { + streamingCalled = true + if postExport != nil { + _ = postExport(ctx, workDir) + } + if err := os.MkdirAll(workDir, 0o755); err != nil { + return err + } + if err := os.MkdirAll(filepath.Dir(ext4Path), 0o755); err != nil { + return err + } + return os.WriteFile(ext4Path, []byte("streaming-post"), 0o644) + }) + patches.ApplyFunc(computeFileSHA256, func(path string) (string, int64, error) { + return "sha-streaming-post", 14, nil + }) + patches.ApplyFunc(exportImageRootfs, func(ctx context.Context, source *PreparedSource, destRootfsDir string) error { + t.Fatal("phase-1 export should not run after streaming success") + return nil + }) + patches.ApplyFunc(createExt4Image, func(ctx context.Context, rootfsDir, ext4Path string) error { + t.Fatal("phase-1 ext4 creation should not run after streaming success") + return nil + }) + + postHook := func(ctx context.Context, mountPoint string) error { + postExportCalled = true + return nil + } + + result, err := BuildExt4(context.Background(), &PreparedSource{LocalRef: "docker.io/library/nginx:latest", ExportMode: ExportModeDocker}, BuildOptions{ArtifactID: "artifact-post", PostRootfsExport: postHook}) + if err != nil { + t.Fatalf("BuildExt4 failed: %v", err) + } + if !streamingCalled { + t.Fatal("expected streaming build to run") + } + if !postExportCalled { + t.Fatal("expected postExport hook to be called") + } + if result.SHA256 != "sha-streaming-post" || result.SizeBytes != 14 { + t.Fatalf("unexpected result: %#v", result) + } +} + func TestBuildExt4StreamingFailureFallsBackToPhase1(t *testing.T) { workRoot := t.TempDir() storeRoot := t.TempDir() @@ -909,7 +968,7 @@ func TestBuildExt4StreamingFailureFallsBackToPhase1(t *testing.T) { return nil }) patches.ApplyFunc(isLocalFastFS, func(path string) bool { return true }) - patches.ApplyFunc(createExt4ImageStreaming, func(ctx context.Context, source *PreparedSource, workDir, ext4Path string, estimatedSizeBytes int64) error { + patches.ApplyFunc(createExt4ImageStreaming, func(ctx context.Context, source *PreparedSource, workDir, ext4Path string, estimatedSizeBytes int64, postExport func(context.Context, string) error) error { if err := os.MkdirAll(workDir, 0o755); err != nil { return err } @@ -1009,7 +1068,7 @@ func TestPrepareLocalSourceUsesDockerlessWhenAvailable(t *testing.T) { if err != nil { t.Fatalf("PrepareLocalSource failed: %v", err) } - if !got.UseDockerless { + if got.ExportMode != ExportModeDockerless { t.Fatalf("expected dockerless source, got %#v", got) } if got.CompressedSizeBytes != 10 { diff --git a/CubeMaster/pkg/templatecenter/image/native.go b/CubeMaster/pkg/templatecenter/image/native.go new file mode 100644 index 000000000..42e45cff1 --- /dev/null +++ b/CubeMaster/pkg/templatecenter/image/native.go @@ -0,0 +1,324 @@ +// SPDX-License-Identifier: Apache-2.0 +// + +package image + +import ( + "bufio" + "context" + "fmt" + "io" + "os" + "path/filepath" + "runtime" + "strconv" + "sync" + "sync/atomic" + "time" + + "github.com/containerd/containerd/archive" + "github.com/containerd/containerd/archive/compression" + "github.com/google/go-containerregistry/pkg/authn" + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/remote" + "golang.org/x/sync/errgroup" +) + +const ( + defaultNativeExportConcurrency = 6 + maxNativeExportConcurrency = 32 + nativeExportJobsEnv = "CUBEMASTER_NATIVE_ROOTFS_EXPORT_JOBS" + // nativeCopyBufferSize is the buffer size for I/O operations. 1MB is chosen as a + // good balance between memory usage and read/write system call overhead. + nativeCopyBufferSize = 1024 * 1024 +) + +var nativeCopyBufferPool = sync.Pool{ + New: func() any { + return make([]byte, nativeCopyBufferSize) + }, +} + +// nativeRootfsExportEnabled checks if CUBEMASTER_NATIVE_ROOTFS_EXPORT_ENABLED is enabled. +// By default, it is enabled, which avoids using external CLI tools (docker, skopeo, umoci). +func nativeRootfsExportEnabled() bool { + v, err := strconv.ParseBool(os.Getenv("CUBEMASTER_NATIVE_ROOTFS_EXPORT_ENABLED")) + return err != nil || v +} + +// registryAuthOption converts PreparedSource credentials into a remote.Option. +// Falls back to the DefaultKeychain if explicit credentials are not provided. +func registryAuthOption(auth *RegistryAuthConfig) remote.Option { + if auth != nil && (auth.Username != "" || auth.Password != "") { + return remote.WithAuth(authn.FromConfig(authn.AuthConfig{ + Username: auth.Username, + Password: auth.Password, + })) + } + return remote.WithAuthFromKeychain(authn.DefaultKeychain) +} + +type progressReader struct { + io.Reader + onRead func(n int) +} + +func (pr *progressReader) Read(p []byte) (int, error) { + n, err := pr.Reader.Read(p) + if n > 0 && pr.onRead != nil { + pr.onRead(n) + } + return n, err +} + +// StreamRegistryToDir fetches and applies OCI layers directly into destDir. +func StreamRegistryToDir(ctx context.Context, source *PreparedSource, destDir string) error { + img, err := nativeImageForSource(ctx, source) + if err != nil { + return err + } + + layers, err := img.Layers() + if err != nil { + return fmt.Errorf("native export failed to get layers: %w", err) + } + + var ( + downloadedBytes int64 + completedLayers int32 + totalBytes = source.CompressedSizeBytes + totalLayers = len(layers) + + progressMu sync.Mutex + lastTime time.Time + lastBytes int64 + ) + + reportProgress := func(force bool) { + if source.OnPullProgress == nil { + return + } + + progressMu.Lock() + defer progressMu.Unlock() + + now := time.Now() + if !force && !lastTime.IsZero() && now.Sub(lastTime) < 800*time.Millisecond { + return + } + + downloaded := atomic.LoadInt64(&downloadedBytes) + completed := int(atomic.LoadInt32(&completedLayers)) + + var speed int64 + if !lastTime.IsZero() { + dt := now.Sub(lastTime).Seconds() + if dt > 0 { + speed = int64(float64(downloaded-lastBytes) / dt) + } + } + + var percent float64 + if totalBytes > 0 { + percent = float64(downloaded) / float64(totalBytes) * 100 + if percent > 100 { + percent = 100 + } + } else if totalLayers > 0 { + percent = float64(completed) / float64(totalLayers) * 100 + } + + source.OnPullProgress(PullProgress{ + TotalBytes: totalBytes, + DownloadedBytes: downloaded, + TotalLayers: totalLayers, + CompletedLayers: completed, + SpeedBPS: speed, + Percent: percent, + }) + + lastTime = now + lastBytes = downloaded + } + + reportProgress(true) + + // Step 1: Concurrently prefetch layers to disk to maximize network throughput. + // Temp directory is created in destDir's workspace to utilize the same fast disk. + prefetchDir, err := os.MkdirTemp(filepath.Dir(destDir), "native-prefetch-*") + if err != nil { + return fmt.Errorf("native export failed to create prefetch dir: %w", err) + } + defer os.RemoveAll(prefetchDir) + + jobs := nativeExportConcurrency() + sem := make(chan struct{}, jobs) + + type layerFetch struct { + path string + err error + done chan struct{} + } + fetches := make([]*layerFetch, len(layers)) + for i := range fetches { + fetches[i] = &layerFetch{done: make(chan struct{})} + } + + eg, egCtx := errgroup.WithContext(ctx) + + // Schedule concurrent downloads. + for i, l := range layers { + layerIdx := i + layer := l + + eg.Go(func() error { + select { + case sem <- struct{}{}: + case <-egCtx.Done(): + return egCtx.Err() + } + defer func() { <-sem }() + + rc, err := layer.Compressed() + if err != nil { + err = fmt.Errorf("failed to open compressed stream for layer %d: %w", layerIdx, err) + fetches[layerIdx].err = err + close(fetches[layerIdx].done) + return err + } + var closeOnce sync.Once + closeRC := func() { closeOnce.Do(func() { _ = rc.Close() }) } + defer closeRC() + + // Bridge context to explicitly abort network read if context cancels early. + stopWatch := context.AfterFunc(egCtx, closeRC) + defer stopWatch() + + f, err := os.CreateTemp(prefetchDir, fmt.Sprintf("layer-%03d-*.tar", layerIdx)) + if err != nil { + err = fmt.Errorf("failed to create temp file for layer %d: %w", layerIdx, err) + fetches[layerIdx].err = err + close(fetches[layerIdx].done) + return err + } + path := f.Name() + + buf := nativeCopyBufferPool.Get().([]byte) + defer nativeCopyBufferPool.Put(buf) + + pr := &progressReader{ + Reader: rc, + onRead: func(n int) { + atomic.AddInt64(&downloadedBytes, int64(n)) + reportProgress(false) + }, + } + + if _, err := io.CopyBuffer(f, pr, buf); err != nil { + _ = f.Close() + if ctxErr := egCtx.Err(); ctxErr != nil { + err = fmt.Errorf("failed to download layer %d: %v (context canceled: %w)", layerIdx, err, ctxErr) + } else { + err = fmt.Errorf("failed to download layer %d: %w", layerIdx, err) + } + fetches[layerIdx].err = err + close(fetches[layerIdx].done) + return err + } + + if err := f.Close(); err != nil { + err = fmt.Errorf("failed to close temp file for layer %d: %w", layerIdx, err) + fetches[layerIdx].err = err + close(fetches[layerIdx].done) + return err + } + + atomic.AddInt32(&completedLayers, 1) + reportProgress(true) + + fetches[layerIdx].path = path + close(fetches[layerIdx].done) + return nil + }) + } + + // Step 2: Sequentially apply and immediately delete layers as they finish downloading. + eg.Go(func() error { + var br *bufio.Reader + + for i := 0; i < len(layers); i++ { + select { + case <-egCtx.Done(): + return egCtx.Err() + case <-fetches[i].done: + } + + if fetches[i].err != nil { + return fetches[i].err + } + + path := fetches[i].path + f, err := os.Open(path) + if err != nil { + return fmt.Errorf("native export failed to open prefetched layer %d: %w", i, err) + } + + // Reuse the same large buffer across all layers since extraction is strictly sequential. + if br == nil { + br = bufio.NewReaderSize(f, nativeCopyBufferSize) + } else { + br.Reset(f) + } + + decompressed, err := compression.DecompressStream(br) + if err != nil { + _ = f.Close() + return fmt.Errorf("native export failed to decompress layer %d: %w", i, err) + } + + _, applyErr := archive.Apply(egCtx, destDir, decompressed, archive.WithNoSameOwner()) + _ = decompressed.Close() + _ = f.Close() // safe to double close, ensures FD is freed immediately + + if applyErr != nil { + return fmt.Errorf("native export failed to apply layer %d to %q (Hint: this might require root privileges, CAP_MKNOD, or a destination filesystem that supports xattrs/capabilities): %w", i, destDir, applyErr) + } + + // TODO: Cache layers securely per-tenant. + // Delete temp file immediately to save disk space. + _ = os.Remove(path) + } + return nil + }) + + return eg.Wait() +} + +func nativeImageForSource(ctx context.Context, source *PreparedSource) (v1.Image, error) { + if source == nil { + return nil, fmt.Errorf("native export source is nil") + } + if source.nativeImage != nil { + return source.nativeImage, nil + } + return nil, fmt.Errorf("native export source image was not prepared") +} + +func nativeExportConcurrency() int { + raw := os.Getenv(nativeExportJobsEnv) + if raw == "" { + return defaultNativeExportConcurrency + } + jobs, err := strconv.Atoi(raw) + if err != nil || jobs <= 0 { + return defaultNativeExportConcurrency + } + if jobs > maxNativeExportConcurrency { + return maxNativeExportConcurrency + } + return jobs +} + +func defaultPlatform() v1.Platform { + return v1.Platform{OS: "linux", Architecture: runtime.GOARCH} +} diff --git a/CubeMaster/pkg/templatecenter/image/native_test.go b/CubeMaster/pkg/templatecenter/image/native_test.go new file mode 100644 index 000000000..b344782a4 --- /dev/null +++ b/CubeMaster/pkg/templatecenter/image/native_test.go @@ -0,0 +1,536 @@ +// SPDX-License-Identifier: Apache-2.0 +// + +package image + +import ( + "archive/tar" + "bytes" + "context" + "errors" + "io" + "net/http/httptest" + "os" + "path/filepath" + "testing" + "testing/synctest" + + "github.com/agiledragon/gomonkey/v2" + "github.com/google/go-containerregistry/pkg/name" + "github.com/google/go-containerregistry/pkg/registry" + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/empty" + "github.com/google/go-containerregistry/pkg/v1/mutate" + "github.com/google/go-containerregistry/pkg/v1/remote" + "github.com/google/go-containerregistry/pkg/v1/tarball" +) + +func TestNativeRootfsExportEnabledParsesEnv(t *testing.T) { + t.Setenv("CUBEMASTER_NATIVE_ROOTFS_EXPORT_ENABLED", "") + if !nativeRootfsExportEnabled() { + t.Fatal("expected native rootfs export to be enabled by default") + } + + t.Setenv("CUBEMASTER_NATIVE_ROOTFS_EXPORT_ENABLED", "false") + if nativeRootfsExportEnabled() { + t.Fatal("expected native rootfs export to be disabled") + } + + t.Setenv("CUBEMASTER_NATIVE_ROOTFS_EXPORT_ENABLED", "not-a-bool") + if !nativeRootfsExportEnabled() { + t.Fatal("expected invalid native export env value to fallback to enabled") + } +} + +func TestNativeExportConcurrencyParsesEnv(t *testing.T) { + t.Setenv(nativeExportJobsEnv, "") + if got := nativeExportConcurrency(); got != defaultNativeExportConcurrency { + t.Fatalf("default concurrency=%d, want %d", got, defaultNativeExportConcurrency) + } + + t.Setenv(nativeExportJobsEnv, "12") + if got := nativeExportConcurrency(); got != 12 { + t.Fatalf("configured concurrency=%d, want 12", got) + } + + t.Setenv(nativeExportJobsEnv, "0") + if got := nativeExportConcurrency(); got != defaultNativeExportConcurrency { + t.Fatalf("invalid concurrency=%d, want %d", got, defaultNativeExportConcurrency) + } + + t.Setenv(nativeExportJobsEnv, "128") + if got := nativeExportConcurrency(); got != maxNativeExportConcurrency { + t.Fatalf("capped concurrency=%d, want %d", got, maxNativeExportConcurrency) + } +} + +func TestPrepareNativeSourceExtractsDigestAndConfig(t *testing.T) { + s := httptest.NewServer(registry.New()) + defer s.Close() + + // Create a dummy image + img, err := mutate.Config(empty.Image, v1.Config{ + Cmd: []string{"/bin/sh"}, + }) + if err != nil { + t.Fatalf("mutate.Config: %v", err) + } + + // Create a dummy layer + var b bytes.Buffer + tw := tar.NewWriter(&b) + tw.WriteHeader(&tar.Header{Name: "test.txt", Size: 4, Mode: 0644}) + tw.Write([]byte("test")) + tw.Close() + layer, _ := tarball.LayerFromOpener(func() (io.ReadCloser, error) { + return io.NopCloser(bytes.NewReader(b.Bytes())), nil + }) + img, _ = mutate.AppendLayers(img, layer) + + ref, _ := name.ParseReference(s.URL[7:] + "/test-native:latest") + if err := remote.Write(ref, img); err != nil { + t.Fatalf("remote.Write: %v", err) + } + + spec := SourceSpec{ + ImageRef: "docker://" + ref.Name(), + RegistryUsername: "", + RegistryPassword: "", + } + + source, err := prepareNativeSource(context.Background(), spec) + if err != nil { + t.Fatalf("prepareNativeSource failed: %v", err) + } + + if source.ExportMode != ExportModeNative { + t.Errorf("expected ExportMode=ExportModeNative, got %q", source.ExportMode) + } + if len(source.Config.Cmd) == 0 || source.Config.Cmd[0] != "/bin/sh" { + t.Errorf("expected Config.Cmd=[/bin/sh], got %v", source.Config.Cmd) + } + + digest, _ := img.Digest() + expectedDigest := s.URL[7:] + "/test-native@" + digest.String() + if source.Digest != expectedDigest { + t.Errorf("expected Digest=%q, got %q", expectedDigest, source.Digest) + } + + // We appended 1 layer, let's just make sure CompressedSizeBytes > 0 + if source.CompressedSizeBytes <= 0 { + t.Errorf("expected CompressedSizeBytes > 0, got %d", source.CompressedSizeBytes) + } + if source.RegistryAuth != nil { + t.Errorf("expected RegistryAuth to be nil without explicit credentials, got %#v", source.RegistryAuth) + } +} + +func TestPrepareSourceUsesNativeWhenEnabled(t *testing.T) { + t.Setenv("CUBEMASTER_NATIVE_ROOTFS_EXPORT_ENABLED", "true") + + img, err := mutate.Config(empty.Image, v1.Config{ + Cmd: []string{"native"}, + }) + if err != nil { + t.Fatalf("mutate.Config: %v", err) + } + + tests := []struct { + name string + fn func(context.Context, SourceSpec) (*PreparedSource, error) + }{ + {name: "PrepareSource", fn: PrepareSource}, + {name: "PrepareLocalSource", fn: PrepareLocalSource}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + patches := gomonkey.ApplyFuncReturn(remote.Image, img, nil) + defer patches.Reset() + source, err := tt.fn(context.Background(), SourceSpec{ + ImageRef: "example.com/native:latest", + RegistryUsername: "user", + RegistryPassword: "pass", + }) + if err != nil { + t.Fatalf("%s failed: %v", tt.name, err) + } + if source.ExportMode != ExportModeNative { + t.Fatalf("ExportMode=%q, want %q", source.ExportMode, ExportModeNative) + } + if source.RegistryAuth == nil || source.RegistryAuth.Username != "user" || source.RegistryAuth.Password != "pass" { + t.Fatalf("unexpected RegistryAuth: %#v", source.RegistryAuth) + } + if len(source.Config.Cmd) != 1 || source.Config.Cmd[0] != "native" { + t.Fatalf("Config.Cmd=%v, want [native]", source.Config.Cmd) + } + if source.nativeImage == nil { + t.Fatal("expected prepared native image to be cached") + } + }) + } +} + +func TestImageDigestFromReferenceMatchesDockerlessCanonicalName(t *testing.T) { + tests := []struct { + name string + imageRef string + want string + }{ + { + name: "docker hub short name", + imageRef: "nginx:latest", + want: "docker.io/library/nginx@sha256:abcd", + }, + { + name: "docker hub explicit alias", + imageRef: "docker.io/library/nginx:latest", + want: "docker.io/library/nginx@sha256:abcd", + }, + { + name: "non docker hub registry", + imageRef: "example.com/ns/app:stable", + want: "example.com/ns/app@sha256:abcd", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ref, err := name.ParseReference(tt.imageRef) + if err != nil { + t.Fatalf("ParseReference(%q): %v", tt.imageRef, err) + } + if got := imageDigestFromReference(ref, "sha256:abcd"); got != tt.want { + t.Fatalf("imageDigestFromReference(%q)=%q, want %q", tt.imageRef, got, tt.want) + } + }) + } +} + +func TestStreamRegistryWhiteoutResolution(t *testing.T) { + // Base layer: creates /dir/file1 and /dir/file2 + var b1 bytes.Buffer + tw1 := tar.NewWriter(&b1) + tw1.WriteHeader(&tar.Header{Name: "dir/", Mode: 0755, Typeflag: tar.TypeDir}) + tw1.WriteHeader(&tar.Header{Name: "dir/file1", Size: 4, Mode: 0644}) + tw1.Write([]byte("data")) + tw1.WriteHeader(&tar.Header{Name: "dir/file2", Size: 4, Mode: 0644}) + tw1.Write([]byte("data")) + tw1.Close() + l1, _ := tarball.LayerFromOpener(func() (io.ReadCloser, error) { + return io.NopCloser(bytes.NewReader(b1.Bytes())), nil + }) + + // Second layer: deletes /dir/file1 via whiteout, and makes an opaque dir marker /dir/.wh..wh..opq + var b2 bytes.Buffer + tw2 := tar.NewWriter(&b2) + tw2.WriteHeader(&tar.Header{Name: "dir/.wh.file1", Size: 0, Mode: 0644}) + tw2.WriteHeader(&tar.Header{Name: "dir/.wh..wh..opq", Size: 0, Mode: 0644}) + tw2.WriteHeader(&tar.Header{Name: "dir/file3", Size: 4, Mode: 0644}) // New file + tw2.Write([]byte("data")) + tw2.Close() + l2, _ := tarball.LayerFromOpener(func() (io.ReadCloser, error) { + return io.NopCloser(bytes.NewReader(b2.Bytes())), nil + }) + + img, _ := mutate.AppendLayers(empty.Image, l1, l2) + + destDir := t.TempDir() + source := &PreparedSource{ + LocalRef: "docker.io/library/test-whiteout:latest", + nativeImage: img, + } + + err := StreamRegistryToDir(context.Background(), source, destDir) + if err != nil { + t.Fatalf("StreamRegistryToDir failed: %v", err) + } + + // Verify the result + // file1 should be deleted by whiteout + if _, err := os.Stat(filepath.Join(destDir, "dir", "file1")); !os.IsNotExist(err) { + t.Errorf("expected dir/file1 to be deleted by whiteout, but it exists") + } + // file2 should be deleted by the opaque directory marker + if _, err := os.Stat(filepath.Join(destDir, "dir", "file2")); !os.IsNotExist(err) { + t.Errorf("expected dir/file2 to be deleted by opaque dir marker, but it exists") + } + // file3 should exist + if _, err := os.Stat(filepath.Join(destDir, "dir", "file3")); err != nil { + t.Errorf("expected dir/file3 to exist, but got: %v", err) + } +} + +func TestStreamRegistryUsesPreparedNativeImage(t *testing.T) { + var b1 bytes.Buffer + tw1 := tar.NewWriter(&b1) + tw1.WriteHeader(&tar.Header{Name: "dir/", Mode: 0755, Typeflag: tar.TypeDir}) + tw1.WriteHeader(&tar.Header{Name: "dir/file1", Size: 4, Mode: 0644}) + tw1.Write([]byte("data")) + tw1.Close() + l1, _ := tarball.LayerFromOpener(func() (io.ReadCloser, error) { + return io.NopCloser(bytes.NewReader(b1.Bytes())), nil + }) + + img, _ := mutate.AppendLayers(empty.Image, l1) + + patches := gomonkey.ApplyFunc(remote.Image, func(name.Reference, ...remote.Option) (v1.Image, error) { + t.Fatal("remote.Image should not be called when nativeImage is already cached") + return nil, nil + }) + t.Cleanup(func() { + patches.Reset() + }) + + source := &PreparedSource{ + LocalRef: "docker.io/library/test-native:latest", + nativeImage: img, + } + + destDir := t.TempDir() + if err := StreamRegistryToDir(context.Background(), source, destDir); err != nil { + t.Fatalf("StreamRegistryToDir failed: %v", err) + } + + if _, err := os.Stat(filepath.Join(destDir, "dir", "file1")); err != nil { + t.Fatalf("expected dir/file1 to exist, but got: %v", err) + } +} + +func TestStreamRegistryCancelWhileSchedulingReturnsContextError(t *testing.T) { + t.Setenv(nativeExportJobsEnv, "1") + + synctest.Test(t, func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + canceled := make(chan struct{}) + release := make(chan struct{}) + firstLayer := testCompressedLayer{ + rc: &cancelOnReadCloser{ + cancel: cancel, + canceled: canceled, + release: release, + }, + } + secondLayer := testCompressedLayer{ + rc: io.NopCloser(bytes.NewReader(nil)), + } + + destDir := t.TempDir() + done := make(chan error, 1) + go func() { + done <- StreamRegistryToDir(ctx, &PreparedSource{ + LocalRef: "docker.io/library/test-native:latest", + nativeImage: testImage{ + Image: empty.Image, + layers: []v1.Layer{firstLayer, secondLayer}, + }, + }, destDir) + }() + + <-canceled + close(release) + + err := <-done + if !errors.Is(err, context.Canceled) { + t.Fatalf("StreamRegistryToDir error=%v, want context.Canceled", err) + } + }) +} + +type testImage struct { + v1.Image + layers []v1.Layer +} + +func (i testImage) Layers() ([]v1.Layer, error) { + return i.layers, nil +} + +type testCompressedLayer struct { + v1.Layer + rc io.ReadCloser +} + +func (l testCompressedLayer) Compressed() (io.ReadCloser, error) { + return l.rc, nil +} + +type cancelOnReadCloser struct { + cancel context.CancelFunc + canceled chan struct{} + release chan struct{} +} + +func (r *cancelOnReadCloser) Read([]byte) (int, error) { + if r.cancel != nil { + r.cancel() + close(r.canceled) + r.cancel = nil + } + <-r.release + return 0, io.EOF +} + +func (r *cancelOnReadCloser) Close() error { + return nil +} + +func TestStreamRegistryConcurrencyAndPipelining(t *testing.T) { + makeTar := func(filename string) []byte { + var buf bytes.Buffer + tw := tar.NewWriter(&buf) + _ = tw.WriteHeader(&tar.Header{Name: filename, Size: 4, Mode: 0644}) + _, _ = tw.Write([]byte("data")) + _ = tw.Close() + return buf.Bytes() + } + + // Verify that the semaphore correctly limits concurrent downloads. + t.Run("ConcurrencyLimit", func(t *testing.T) { + synctest.Test(t, func(t *testing.T) { + t.Setenv(nativeExportJobsEnv, "2") + + gates := [3]chan struct{}{make(chan struct{}), make(chan struct{}), make(chan struct{})} + started := [3]chan struct{}{make(chan struct{}), make(chan struct{}), make(chan struct{})} + layers := []v1.Layer{ + gatedCompressedLayer{data: makeTar("a.txt"), gate: gates[0], started: started[0]}, + gatedCompressedLayer{data: makeTar("b.txt"), gate: gates[1], started: started[1]}, + gatedCompressedLayer{data: makeTar("c.txt"), gate: gates[2], started: started[2]}, + } + + destDir := t.TempDir() + var streamErr error + go func() { + streamErr = StreamRegistryToDir(context.Background(), &PreparedSource{ + LocalRef: "test:latest", nativeImage: testImage{Image: empty.Image, layers: layers}, + }, destDir) + }() + synctest.Wait() + + // With jobs=2, exactly 2 goroutines should have acquired the semaphore. + count := 0 + for _, ch := range started { + select { + case <-ch: + count++ + default: + } + } + if count != 2 { + t.Fatalf("concurrent downloads = %d, want 2", count) + } + + for i := range gates { + close(gates[i]) + } + synctest.Wait() + + if streamErr != nil { + t.Fatalf("StreamRegistryToDir: %v", streamErr) + } + }) + }) + + // Verify that layers are extracted in order even when downloads complete out of order. + t.Run("OrderPreservingExtraction", func(t *testing.T) { + synctest.Test(t, func(t *testing.T) { + // Set jobs >= numLayers so all goroutines acquire the semaphore immediately, + // eliminating scheduling non-determinism and letting us focus on extraction ordering. + t.Setenv(nativeExportJobsEnv, "3") + + gates := [3]chan struct{}{make(chan struct{}), make(chan struct{}), make(chan struct{})} + layers := []v1.Layer{ + gatedCompressedLayer{data: makeTar("layer0.txt"), gate: gates[0]}, + gatedCompressedLayer{data: makeTar("layer1.txt"), gate: gates[1]}, + gatedCompressedLayer{data: makeTar("layer2.txt"), gate: gates[2]}, + } + + destDir := t.TempDir() + var streamErr error + go func() { + streamErr = StreamRegistryToDir(context.Background(), &PreparedSource{ + LocalRef: "test:latest", nativeImage: testImage{Image: empty.Image, layers: layers}, + }, destDir) + }() + synctest.Wait() + + // Release layer 2 first (out of order). + // Phase 2 is blocked waiting for layer 0, so nothing should be extracted. + close(gates[2]) + synctest.Wait() + assertNotExists(t, filepath.Join(destDir, "layer2.txt")) + + // Release layer 0. + // Phase 2 extracts layer 0, then blocks waiting for layer 1. + close(gates[0]) + synctest.Wait() + assertExists(t, filepath.Join(destDir, "layer0.txt")) + assertNotExists(t, filepath.Join(destDir, "layer1.txt")) + assertNotExists(t, filepath.Join(destDir, "layer2.txt")) + + // Release layer 1. + // Phase 2 extracts layer 1, then immediately extracts layer 2 (already downloaded). + close(gates[1]) + synctest.Wait() + assertExists(t, filepath.Join(destDir, "layer1.txt")) + assertExists(t, filepath.Join(destDir, "layer2.txt")) + + if streamErr != nil { + t.Fatalf("StreamRegistryToDir: %v", streamErr) + } + }) + }) +} + +// --- Test helpers for gated layers --- + +// gatedCompressedLayer is a v1.Layer whose Compressed() returns a reader that +// blocks on a gate channel, giving the test precise control over download completion order. +type gatedCompressedLayer struct { + v1.Layer + data []byte + gate chan struct{} // close to unblock the download + started chan struct{} // optional; closed on first Read to signal sem acquisition +} + +func (l gatedCompressedLayer) Compressed() (io.ReadCloser, error) { + return &gatedReader{data: l.data, gate: l.gate, started: l.started}, nil +} + +type gatedReader struct { + data []byte + gate chan struct{} + started chan struct{} + gateOpened bool + reader *bytes.Reader +} + +func (r *gatedReader) Read(p []byte) (int, error) { + if !r.gateOpened { + if r.started != nil { + close(r.started) + } + <-r.gate + r.gateOpened = true + r.reader = bytes.NewReader(r.data) + } + return r.reader.Read(p) +} + +func (r *gatedReader) Close() error { return nil } + +func assertExists(t *testing.T, path string) { + t.Helper() + if _, err := os.Stat(path); err != nil { + t.Fatalf("expected %s to exist: %v", filepath.Base(path), err) + } +} + +func assertNotExists(t *testing.T, path string) { + t.Helper() + if _, err := os.Stat(path); !os.IsNotExist(err) { + t.Fatalf("expected %s to NOT exist", filepath.Base(path)) + } +} diff --git a/CubeMaster/pkg/templatecenter/image/source.go b/CubeMaster/pkg/templatecenter/image/source.go index 3d9265dc4..55927501f 100644 --- a/CubeMaster/pkg/templatecenter/image/source.go +++ b/CubeMaster/pkg/templatecenter/image/source.go @@ -14,6 +14,10 @@ import ( "path/filepath" "regexp" "strings" + + "github.com/google/go-containerregistry/pkg/name" + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/remote" ) func PrepareLocalSource(ctx context.Context, spec SourceSpec) (*PreparedSource, error) { @@ -23,6 +27,9 @@ func PrepareLocalSource(ctx context.Context, spec SourceSpec) (*PreparedSource, if err := validateImageRef(spec.ImageRef); err != nil { return nil, err } + if nativeRootfsExportEnabled() { + return prepareNativeSource(ctx, spec) + } // In dockerless mode there is no local docker daemon to hold the image, so a // redo re-resolves the source from the registry via skopeo. This intentionally // relaxes the docker-path requirement that the image still exist locally. @@ -56,12 +63,99 @@ func PrepareLocalSource(ctx context.Context, spec SourceSpec) (*PreparedSource, } func PrepareSource(ctx context.Context, spec SourceSpec) (*PreparedSource, error) { + if nativeRootfsExportEnabled() { + return prepareNativeSource(ctx, spec) + } if hasDockerlessRootfsExportTools() { return prepareDockerlessSource(ctx, spec) } return prepareDockerSource(ctx, spec) } +func prepareNativeSource(ctx context.Context, spec SourceSpec) (*PreparedSource, error) { + if err := validateImageRef(spec.ImageRef); err != nil { + return nil, err + } + + rawRef := strings.TrimPrefix(spec.ImageRef, "docker://") + ref, err := name.ParseReference(rawRef) + if err != nil { + return nil, fmt.Errorf("native export failed to parse image ref %q: %w", rawRef, err) + } + + var authCfg *RegistryAuthConfig + if spec.RegistryUsername != "" || spec.RegistryPassword != "" { + authCfg = &RegistryAuthConfig{Username: spec.RegistryUsername, Password: spec.RegistryPassword} + } + authOpt := registryAuthOption(authCfg) + jobs := nativeExportConcurrency() + platOpt := remote.WithPlatform(defaultPlatform()) + jobsOpt := remote.WithJobs(jobs) + + img, err := remote.Image(ref, remote.WithContext(ctx), authOpt, platOpt, jobsOpt) + if err != nil { + return nil, fmt.Errorf("native export failed to resolve image %q (if this is a multi-arch index, verify the requested platform exists): %w", rawRef, err) + } + + cfgFile, err := img.ConfigFile() + if err != nil { + return nil, fmt.Errorf("failed to get config file for %q: %w", rawRef, err) + } + + manifest, err := img.Manifest() + if err != nil { + return nil, fmt.Errorf("failed to get manifest for %q: %w", rawRef, err) + } + + var compressedSize int64 + for _, layer := range manifest.Layers { + compressedSize += layer.Size + } + + digest, err := img.Digest() + if err != nil { + return nil, fmt.Errorf("failed to get digest for %q: %w", rawRef, err) + } + + // Make sure the digest aligns with the dockerless/skopeo canonical format + // (name@sha256:...) to preserve fingerprint cache hits. + unifiedDigest := imageDigestFromReference(ref, digest.String()) + + cfg := convertV1Config(cfgFile.Config) + configJSON, err := json.Marshal(cfg) + if err != nil { + return nil, fmt.Errorf("marshal image Config: %w", err) + } + + return &PreparedSource{ + LocalRef: spec.ImageRef, + Digest: unifiedDigest, + Config: cfg, + ConfigJSON: string(configJSON), + MasterNodeIP: NormalizeBaseURL(spec.DownloadBaseURL), + ExportMode: ExportModeNative, + CompressedSizeBytes: compressedSize, + RegistryAuth: authCfg, + nativeImage: img, + Cleanup: func(context.Context) {}, // no-op for native + OnPullProgress: spec.OnPullProgress, + }, nil +} + +// convertV1Config extracts only the fields necessary for container execution. +// Fields like ExposedPorts, Volumes, Labels, StopSignal, and Healthcheck +// are intentionally omitted as they are not relevant to Cube's runtime +// configuration or rootfs extraction logic. +func convertV1Config(cfg v1.Config) DockerImageConfig { + return DockerImageConfig{ + Entrypoint: cfg.Entrypoint, + Cmd: cfg.Cmd, + Env: cfg.Env, + WorkingDir: cfg.WorkingDir, + User: cfg.User, + } +} + func prepareDockerlessSource(ctx context.Context, spec SourceSpec) (*PreparedSource, error) { if err := validateImageRef(spec.ImageRef); err != nil { return nil, err @@ -103,7 +197,7 @@ func prepareDockerlessSource(ctx context.Context, spec SourceSpec) (*PreparedSou Config: configInfo.Config, ConfigJSON: string(configJSON), MasterNodeIP: NormalizeBaseURL(spec.DownloadBaseURL), - UseDockerless: true, + ExportMode: ExportModeDockerless, SkopeoAuthFile: authFile, CompressedSizeBytes: skopeoLayersTotalSize(inspectInfo), OnPullProgress: spec.OnPullProgress, @@ -297,6 +391,26 @@ func skopeoImageDigest(info skopeoInspectImage, imageRef string) string { return name + "@" + info.Digest } +// imageDigestFromReference unifies native digest formatting with skopeo's +// canonical dockerless form so equivalent refs share cache keys. +func imageDigestFromReference(ref name.Reference, hexDigest string) string { + if hexDigest == "" { + return "" + } + namePart := dockerlessCanonicalName(ref.Context().Name()) + if namePart == "" { + return hexDigest + } + return namePart + "@" + hexDigest +} + +func dockerlessCanonicalName(namePart string) string { + if strings.HasPrefix(namePart, "index.docker.io/") { + return "docker.io/" + strings.TrimPrefix(namePart, "index.docker.io/") + } + return namePart +} + func firstNonEmptyDigest(info dockerInspectImage) string { if len(info.RepoDigests) > 0 && info.RepoDigests[0] != "" { rd := info.RepoDigests[0] diff --git a/CubeMaster/pkg/templatecenter/image/types.go b/CubeMaster/pkg/templatecenter/image/types.go index 19f21519c..d2f2f65a5 100644 --- a/CubeMaster/pkg/templatecenter/image/types.go +++ b/CubeMaster/pkg/templatecenter/image/types.go @@ -4,7 +4,11 @@ package image -import "context" +import ( + "context" + + v1 "github.com/google/go-containerregistry/pkg/v1" +) type SourceSpec struct { ImageRef string @@ -71,16 +75,42 @@ type PreparedSource struct { Config DockerImageConfig ConfigJSON string MasterNodeIP string - UseDockerless bool SkopeoAuthFile string - // compressedSizeBytes is the sum of the compressed layer blob sizes reported - // by `skopeo inspect` (LayersData[].Size). It is only populated on the - // dockerless path and lets the disk-space pre-check estimate the image size - // without invoking the docker daemon. Zero means "unknown". + // CompressedSizeBytes is the sum of compressed layer blob sizes reported by + // skopeo inspect on the dockerless path, or by the image manifest on the + // native path. It lets disk-space pre-checks estimate image size without + // invoking the docker daemon. Zero means "unknown". CompressedSizeBytes int64 - Cleanup func(context.Context) + // ExportMode is determined during the Prepare phase and controls which path is used during the Export phase. + // An empty value is equivalent to ExportModeDocker (for backward compatibility). + ExportMode ExportMode + + // RegistryAuth preserves the original Registry credentials for use by the native path. + RegistryAuth *RegistryAuthConfig + + // nativeImage caches the prepared go-containerregistry image so export can + // reuse the same remote resolution instead of resolving the reference twice. + nativeImage v1.Image + + Cleanup func(context.Context) // OnPullProgress is propagated from SourceSpec so that the export phase // (skopeo copy on the dockerless path) can stream pull progress even // though it runs after PrepareSource has returned. OnPullProgress ProgressFunc } + +// RegistryAuthConfig holds the authentication credentials used for pulling the image +// natively from the registry without relying on external CLI tools. +type RegistryAuthConfig struct { + Username string + Password string +} + +// ExportMode defines the backend used for exporting the image rootfs. +type ExportMode string + +const ( + ExportModeDocker ExportMode = "" // Default backward-compatible fallback + ExportModeDockerless ExportMode = "dockerless" + ExportModeNative ExportMode = "native" +) diff --git a/CubeMaster/pkg/templatecenter/image_job_runner.go b/CubeMaster/pkg/templatecenter/image_job_runner.go index c664c6195..98709ea8c 100644 --- a/CubeMaster/pkg/templatecenter/image_job_runner.go +++ b/CubeMaster/pkg/templatecenter/image_job_runner.go @@ -57,7 +57,7 @@ func runTemplateImageJob(ctx context.Context, jobID string, req *types.CreateTem defer source.Cleanup(ctx) } pullProgressFlushed := false - if !source.UseDockerless { + if source.ExportMode == image.ExportModeDocker { // Docker/Podman Engine pulls happen during PrepareSource. Flush before // moving to UNPACKING so stale live cache cannot show 13/14 after // PULLING has already completed.