diff --git a/cmd/deploy.go b/cmd/deploy.go index 3a1d2b62..0680b5ff 100644 --- a/cmd/deploy.go +++ b/cmd/deploy.go @@ -29,7 +29,8 @@ import ( ) var ( - sharedNamespace = "stackrox" + sharedNamespace = "stackrox" + imagePreLoadCommand string ) func newDeployCmd(settings *deployer.Config) *cobra.Command { @@ -51,6 +52,8 @@ Examples: cmd.Flags().StringVar(&shell, "shell", "", "Shell to spawn after Central deployment") cmd.Flags().StringVar(&envrc, "envrc", "", "Write environment to file instead of spawning sub-shell") + cmd.Flags().StringVar(&imagePreLoadCommand, "image-preload-command", "", + "Use custom command for pre-loading images to local cluster. Image can be referenced as $IMAGE.") registerFlag(cmd, settings, "olm", "Deploy operator via OLM (requires OLM installed)", withNoOptDefVal("true"), @@ -321,6 +324,35 @@ func runDeploy(cmd *cobra.Command, args []string) error { ctx, cancel := context.WithTimeout(context.Background(), 30*time.Minute) defer cancel() + // If we are deploying to a local cluster and the images exist locally, then we transfer them + // to the local cluster. + if deploySettings.Roxie.ClusterType.IsLocal() && !deploySettings.Roxie.KonfluxImages { + var preLoader deployer.ImagePreLoader + if imagePreLoadCommand != "" { + preLoader = deployer.NewCustomImagePreloader(ctx, log, imagePreLoadCommand) + } else { + preLoader, err = d.GetPreLoaderForCluster() + if err != nil && !errors.Is(err, deployer.ErrLocalImagesUnsupported) { + return fmt.Errorf("obtaining image preloader for cluster: %w", err) + } + // ErrLocalImagesUnsupported indicates that roxie does not contain preloading + // support for the respective cluster type. If preloading is required (because + // the images do not exist on the remote registry), the user needs to take care + // of the preloading. + } + if preLoader == nil { + log.Warningf("Image preloading not supported for cluster %s.", d.GetKubeContext()) + log.Warningf("Use --image-preload-command for specifying custom image preloading mechanism.") + } else { + log.Dimf("Using image pre-loader %q", preLoader.Name()) + + if err := d.TryTransferLocalImages(ctx, preLoader); err != nil { + // Best effort, keep running. + log.Warningf("Transferring images to local cluster failed: %v", err) + } + } + } + if components.IncludesCentral() { d.PrintCentralDeploymentSummary() } @@ -366,6 +398,9 @@ func configureConfig(log *logger.Logger, components component.Component, deployS deploySettings.Roxie.ClusterType = clusterType } clusterType := deploySettings.Roxie.ClusterType + centralDeployLocally := components.IncludesCentral() && clusterType.IsLocal() + sensorDeployLocally := components.IncludesSensor() && clusterType.IsLocal() + defaults, err := clusterdefaults.ApplyClusterDefaults(deploySettings) if err != nil { return err @@ -381,11 +416,18 @@ func configureConfig(log *logger.Logger, components component.Component, deployS log.Dimf("Selecting resource profile %v for Central", profile) deploySettings.Central.ResourceProfile = profile } + if centralDeployLocally && deploySettings.Central.ResourceProfile == types.ResourceProfileAcsDefaults { + log.Warning("You are deploying Central to a local cluster, it is recommended to specify a resource profile (or --resources=auto)") + } + if deploySettings.SecuredCluster.ResourceProfile == types.ResourceProfileAuto { profile := clusterdefaults.ResolveAutoResourceProfile(clusterType) log.Dimf("Selecting resource profile %v for SecuredCluster", profile) deploySettings.SecuredCluster.ResourceProfile = profile } + if sensorDeployLocally && deploySettings.SecuredCluster.ResourceProfile == types.ResourceProfileAcsDefaults { + log.Warning("You are deploying SecuredCluster to a local cluster, it is recommended to specify a resource profile (or --resources=auto)") + } // We need to do this regardless of whether the operator is deployed or not, because // this includes the transformation of StackRox main image tags to semver compatible versions, diff --git a/go.mod b/go.mod index 42988d1b..a273b405 100644 --- a/go.mod +++ b/go.mod @@ -6,15 +6,44 @@ require ( dario.cat/mergo v1.0.2 github.com/fatih/color v1.19.0 github.com/google/go-containerregistry v0.21.5 + github.com/moby/moby/client v0.4.0 github.com/spf13/cobra v1.10.2 github.com/spf13/pflag v1.0.10 github.com/stretchr/testify v1.11.1 - golang.org/x/term v0.42.0 + golang.org/x/term v0.43.0 gopkg.in/yaml.v3 v3.0.1 k8s.io/apimachinery v0.35.3 k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 ) +require github.com/moby/moby/api v1.54.1 // indirect + +require ( + github.com/Microsoft/go-winio v0.6.2 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // 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/distribution/reference v0.6.0 // indirect + github.com/docker/docker v28.5.2+incompatible + github.com/docker/go-connections v0.6.0 // indirect + github.com/docker/go-units v0.5.0 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/moby/docker-image-spec v1.3.1 // indirect + github.com/moby/sys/atomicwriter v0.1.0 // indirect + github.com/morikuni/aec v1.1.0 // indirect + github.com/pkg/errors v0.9.1 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect + go.opentelemetry.io/otel v1.44.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.44.0 // indirect + go.opentelemetry.io/otel/metric v1.44.0 // indirect + go.opentelemetry.io/otel/sdk v1.44.0 // indirect + go.opentelemetry.io/otel/sdk/metric v1.44.0 // indirect + go.opentelemetry.io/otel/trace v1.44.0 // indirect +) + require ( github.com/Masterminds/semver/v3 v3.5.0 github.com/containerd/stargz-snapshotter/estargz v0.18.2 // indirect @@ -38,12 +67,11 @@ require ( github.com/vbatts/tar-split v0.12.2 // indirect github.com/x448/float16 v0.8.4 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect - golang.org/x/net v0.48.0 // indirect + golang.org/x/net v0.55.0 // indirect golang.org/x/sync v0.20.0 // indirect - golang.org/x/sys v0.43.0 // indirect - golang.org/x/text v0.35.0 // indirect + golang.org/x/sys v0.45.0 // indirect + golang.org/x/text v0.37.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect - gotest.tools/v3 v3.5.2 // indirect k8s.io/klog/v2 v2.130.1 // indirect k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 // indirect sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect diff --git a/go.sum b/go.sum index 52f72481..7a208dfc 100644 --- a/go.sum +++ b/go.sum @@ -1,52 +1,105 @@ dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= +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/Masterminds/semver/v3 v3.5.0 h1:kQceYJfbupGfZOKZQg0kou0DgAKhzDg2NZPAwZ/2OOE= github.com/Masterminds/semver/v3 v3.5.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= +github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= +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/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= +github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= +github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= +github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= +github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= +github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= github.com/containerd/stargz-snapshotter/estargz v0.18.2 h1:yXkZFYIzz3eoLwlTUZKz2iQ4MrckBxJjkmD16ynUTrw= github.com/containerd/stargz-snapshotter/estargz v0.18.2/go.mod h1:XyVU5tcJ3PRpkA9XS2T5us6Eg35yM0214Y+wvrZTBrY= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= 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/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.0+incompatible h1:+IjXULMetlvWJiuSI0Nbor36lcJ5BTcVpUmB21KBoVM= github.com/docker/cli v29.4.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/docker v28.5.2+incompatible h1:DBX0Y0zAjZbSrm1uzOkdr1onVghKaftjlSWt4AFexzM= +github.com/docker/docker v28.5.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= 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.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94= +github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w= github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= 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/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/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.5 h1:KTJG9Pn/jC0VdZR6ctV3/jcN+q6/Iqlx0sTVz3ywZlM= github.com/google/go-containerregistry v0.21.5/go.mod h1:ySvMuiWg+dOsRW0Hw8GYwfMwBlNRTmpYBFJPlkco5zU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 h1:5VipnvEpbqr2gA2VbM+nYVbkIF28c5ZQfqCBQ5g2xfk= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0/go.mod h1:Hyl3n6Twe1hvtd9XUXDec4pTvgMSEixRuQKPTMH2bNs= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 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/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 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/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.1 h1:TqVzuJkOLsgLDDwNLmYqACUuTehOHRGKiPhvH8V3Nn4= +github.com/moby/moby/api v1.54.1/go.mod h1:+RQ6wluLwtYaTd1WnPLykIDPekkuyD/ROWQClE83pzs= +github.com/moby/moby/client v0.4.0 h1:S+2XegzHQrrvTCvF6s5HFzcrywWQmuVnhOXe2kiWjIw= +github.com/moby/moby/client v0.4.0/go.mod h1:QWPbvWchQbxBNdaLSpoKpCdf5E+WxFAgNHogCWDoa7g= +github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw= +github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs= +github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU= +github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko= +github.com/moby/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= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8= github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/morikuni/aec v1.1.0 h1:vBBl0pUnvi/Je71dsRrhMBtreIqNMYErSAbEeb8jrXQ= +github.com/morikuni/aec v1.1.0/go.mod h1:xDRgiq/iw5l+zkao76YTKzKttOp2cwPEne25HDkJnBw= 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/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +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/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w= github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= @@ -63,22 +116,53 @@ github.com/vbatts/tar-split v0.12.2 h1:w/Y6tjxpeiFMR47yzZPlPj/FcPLpXbTUi/9H7d3CP github.com/vbatts/tar-split v0.12.2/go.mod h1:eF6B6i6ftWQcDqEn3/iGFRFRo8cBIMSJVOpnNdfTMFA= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +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.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.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU= +go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.44.0 h1:4YsVu3B8+3qtWYYrsUYgn0OG78pN0rnNPRGX4SbokQI= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.44.0/go.mod h1:+wnlSn0mD1ADVMe3v9Z/WIaiz6q6gL2J/ejaAmdmv80= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.44.0 h1:lgh3PiVrRUWMLOVSkQicxzZll5NjF1r+AtsX1XRIHw0= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.44.0/go.mod h1:5Cnhth3m/AgOeTgE3ex12pPmiu/gGtZit03kSzx9X7s= +go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc= +go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo= +go.opentelemetry.io/otel/sdk v1.44.0 h1:nHYwb9lK+fJPU/dnT6s7W7Z8itMWyqrnVfbheVYrZ58= +go.opentelemetry.io/otel/sdk v1.44.0/go.mod h1:Osuydd3Se74nqjAKxid74N5eC+jfEqfTegHRnq58oK0= +go.opentelemetry.io/otel/sdk/metric v1.44.0 h1:3LlKgI+VjbVsjNRFZJZAJ30WjXC5VkNRks6si09iEfI= +go.opentelemetry.io/otel/sdk/metric v1.44.0/go.mod h1:5B5pMARnXxKhltooO4xUuCBorl65a4EpnTalObqOigA= +go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk= +go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE= +go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g= +go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk= 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/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= -golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= -golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= +golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8= +golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww= golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= -golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= -golang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY= -golang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY= -golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= -golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= +golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4= +golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk= +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.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= +golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= +google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa h1:Kjn0N0tCrDgiAFW+lGO4JZ3ck44CehvJQMAwj9QF0G8= +google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:q4lMZS6kskjT5HvCPrnnypcDPVJqT/f4nfxmkE7gryY= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa h1:mZHHdPZl0dbGHCflZgAq/Q468DWVFcU2whhB2KAo8fk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/grpc v1.81.1 h1:VnnIIZ88UzOOKLukQi+ImGz8O1Wdp8nAGGnvOfEIWQQ= +google.golang.org/grpc v1.81.1/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I= +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= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= @@ -93,6 +177,8 @@ k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 h1:Y3gxNAuB0OBLImH611+UDZ k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ= k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck= k8s.io/utils v0.0.0-20251002143259-bc988d571ff4/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +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/internal/containerrt/containerrt.go b/internal/containerrt/containerrt.go new file mode 100644 index 00000000..57a4ce6c --- /dev/null +++ b/internal/containerrt/containerrt.go @@ -0,0 +1,126 @@ +package containerrt + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "os" + + "github.com/stackrox/roxie/internal/logger" + + "github.com/docker/docker/api/types/container" + "github.com/docker/docker/api/types/image" + "github.com/docker/docker/client" + "github.com/docker/docker/pkg/stdcopy" +) + +func ListLocalImages(ctx context.Context, host string) ([]string, error) { + cli, err := client.NewClientWithOpts(client.WithHost(host), client.WithAPIVersionNegotiation()) + if err != nil { + return nil, fmt.Errorf("creating container runtime client: %w", err) + } + defer cli.Close() + + images, err := cli.ImageList(ctx, image.ListOptions{}) + if err != nil { + return nil, fmt.Errorf("listing images: %w", err) + } + + var tags []string + for _, img := range images { + for _, tag := range img.RepoTags { + if tag != "" { + tags = append(tags, tag) + } + } + } + return tags, nil +} + +// ExecInContainer runs a command inside a container and returns its stdout. +func ExecInContainer(ctx context.Context, host, containerName string, cmd []string) ([]byte, error) { + cli, err := client.NewClientWithOpts(client.WithHost(host), client.WithAPIVersionNegotiation()) + if err != nil { + return nil, fmt.Errorf("creating container runtime client: %w", err) + } + defer cli.Close() + + execResp, err := cli.ContainerExecCreate(ctx, containerName, container.ExecOptions{ + Cmd: cmd, + AttachStdout: true, + AttachStderr: true, + }) + if err != nil { + return nil, fmt.Errorf("creating exec in container %s: %w", containerName, err) + } + + attach, err := cli.ContainerExecAttach(ctx, execResp.ID, container.ExecAttachOptions{}) + if err != nil { + return nil, fmt.Errorf("attaching to exec in container %s: %w", containerName, err) + } + defer attach.Close() + + var stdout, stderr bytes.Buffer + if _, err := stdcopy.StdCopy(&stdout, &stderr, attach.Reader); err != nil { + return nil, fmt.Errorf("reading exec output from container %s: %w", containerName, err) + } + + inspect, err := cli.ContainerExecInspect(ctx, execResp.ID) + if err != nil { + return nil, fmt.Errorf("inspecting exec in container %s: %w", containerName, err) + } + if inspect.ExitCode != 0 { + return nil, fmt.Errorf("command %v in container %s exited with code %d: %s", cmd, containerName, inspect.ExitCode, stderr.String()) + } + + return stdout.Bytes(), nil +} + +// ParseCrictlImages extracts image tags from `crictl images -o json` output. +func ParseCrictlImages(data []byte) ([]string, error) { + var result struct { + Images []struct { + RepoTags []string `json:"repoTags"` + } `json:"images"` + } + if err := json.Unmarshal(data, &result); err != nil { + return nil, fmt.Errorf("parsing crictl output: %w", err) + } + var tags []string + for _, img := range result.Images { + for _, tag := range img.RepoTags { + if tag != "" { + tags = append(tags, tag) + } + } + } + return tags, nil +} + +// ResolveSocket returns the container runtime socket URI by checking DOCKER_HOST, +// then probing well-known paths for Docker, Podman. Returns "" if none found. +func ResolveSocket(log *logger.Logger) string { + if host := os.Getenv("DOCKER_HOST"); host != "" { + log.Dimf("Using container runtime socket from DOCKER_HOST: %s", host) + return host + } + + for _, candidate := range socketCandidates() { + if _, err := os.Stat(candidate); err == nil { + socket := "unix://" + candidate + log.Dimf("Detected container runtime socket: %s", socket) + return socket + } + } + log.Dimf("No container runtime socket found") + return "" +} + +func socketCandidates() []string { + candidates := []string{ + "/var/run/docker.sock", + } + candidates = append(candidates, platformSocketCandidates()...) + return candidates +} diff --git a/internal/containerrt/socket_darwin.go b/internal/containerrt/socket_darwin.go new file mode 100644 index 00000000..ea17e453 --- /dev/null +++ b/internal/containerrt/socket_darwin.go @@ -0,0 +1,18 @@ +package containerrt + +import ( + "os" + "path/filepath" +) + +func platformSocketCandidates() []string { + var candidates []string + + if home, err := os.UserHomeDir(); err == nil { + candidates = append(candidates, + filepath.Join(home, ".local", "share", "containers", "podman", "machine", "podman.sock"), + ) + } + + return candidates +} diff --git a/internal/containerrt/socket_linux.go b/internal/containerrt/socket_linux.go new file mode 100644 index 00000000..acd0aafc --- /dev/null +++ b/internal/containerrt/socket_linux.go @@ -0,0 +1,22 @@ +package containerrt + +import ( + "fmt" + "os" + "path/filepath" +) + +func platformSocketCandidates() []string { + var candidates []string + + if xdg := os.Getenv("XDG_RUNTIME_DIR"); xdg != "" { + candidates = append(candidates, filepath.Join(xdg, "podman", "podman.sock")) + } + candidates = append(candidates, + fmt.Sprintf("/run/user/%d/podman/podman.sock", os.Getuid()), + "/run/podman/podman.sock", + "/run/k3s/containerd/containerd.sock", + ) + + return candidates +} diff --git a/internal/deployer/acs_images.go b/internal/deployer/acs_images.go new file mode 100644 index 00000000..0a78be93 --- /dev/null +++ b/internal/deployer/acs_images.go @@ -0,0 +1,34 @@ +package deployer + +import "fmt" + +const ( + imageRegistry = "quay.io/rhacs-eng" +) + +func imagesForConfig(config Config) []string { + images := make([]string, 0) + prefix := "" + if config.Roxie.KonfluxImages { + prefix = "release-" + } + + images = append(images, fmt.Sprintf("%s/%s%s:%s", imageRegistry, prefix, "main", config.Roxie.Version)) + images = append(images, fmt.Sprintf("%s/%s%s:%s", imageRegistry, prefix, "central-db", config.Roxie.Version)) + images = append(images, fmt.Sprintf("%s/%s%s:%s", imageRegistry, prefix, "scanner-v4-db", config.Roxie.Version)) + images = append(images, fmt.Sprintf("%s/%s%s:%s", imageRegistry, prefix, "scanner-v4", config.Roxie.Version)) + if !config.Roxie.KonfluxImages { + prefix = "stackrox-" + } + images = append(images, fmt.Sprintf("%s/%s%s:%s", imageRegistry, prefix, "operator", config.Operator.Version)) + images = append(images, fmt.Sprintf("%s/%s%s:v%s", imageRegistry, prefix, "operator-bundle", config.Operator.Version)) + + return images +} + +func OperatorBundleImage(config Config) string { + if config.Roxie.KonfluxImages { + return fmt.Sprintf("%s/release-operator-bundle:v%s", imageRegistry, config.Operator.Version) + } + return fmt.Sprintf("%s/stackrox-operator-bundle:v%s", imageRegistry, config.Operator.Version) +} diff --git a/internal/deployer/deployer.go b/internal/deployer/deployer.go index ee4c5538..1ee87a6f 100644 --- a/internal/deployer/deployer.go +++ b/internal/deployer/deployer.go @@ -15,6 +15,7 @@ import ( "github.com/fatih/color" "github.com/stackrox/roxie/internal/component" + "github.com/stackrox/roxie/internal/containerrt" "github.com/stackrox/roxie/internal/dockerauth" "github.com/stackrox/roxie/internal/env" "github.com/stackrox/roxie/internal/imagecache" @@ -46,7 +47,8 @@ type Deployer struct { dockerCreds *dockerauth.Credentials envrcFile string - kubeContext string + kubeContext string + containerRuntimeSocket string config Config @@ -233,9 +235,10 @@ func New(log *logger.Logger) (*Deployer, error) { } d := &Deployer{ - logger: log, - startTime: time.Now(), - tempDir: tempDir, + logger: log, + startTime: time.Now(), + tempDir: tempDir, + containerRuntimeSocket: containerrt.ResolveSocket(log), } d.dockerAuth = dockerauth.New(log) @@ -982,3 +985,11 @@ func (d *Deployer) GetCentralDeploymentInfo() types.CentralDeploymentInfo { CACertFile: d.roxCACertFile, } } + +func (d *Deployer) GetKubeContext() string { + return d.kubeContext +} + +func (d *Deployer) GetContainerRuntimeSocket() string { + return d.containerRuntimeSocket +} diff --git a/internal/deployer/local_images.go b/internal/deployer/local_images.go new file mode 100644 index 00000000..750fc483 --- /dev/null +++ b/internal/deployer/local_images.go @@ -0,0 +1,106 @@ +package deployer + +import ( + "context" + "errors" + "fmt" + "slices" + + "github.com/stackrox/roxie/internal/containerrt" + "github.com/stackrox/roxie/internal/types" +) + +var ( + ErrLocalImagesUnsupported = errors.New("cluster does not support deploying with local images") + ErrLocalImageRetrievalNotSupported = errors.New("image pre-loader does not support retrieval of load images") +) + +// ImagePreLoader transfers container images into a local cluster's image store. +type ImagePreLoader interface { + SendImage(ctx context.Context, imageTag string) error + GetImages(ctx context.Context) ([]string, error) + Name() string +} + +// GetPreLoaderForCluster returns an ImagePreLoader suited for the detected +// cluster type, or ErrLocalImagesUnsupported if the cluster does not support it. +func (d *Deployer) GetPreLoaderForCluster() (ImagePreLoader, error) { + switch d.config.Roxie.ClusterType { + case types.ClusterTypeKind: + return d.newKindImagePreloader() + case types.ClusterTypeMinikube: + return d.newMinikubeImagePreloader() + default: + return nil, ErrLocalImagesUnsupported + } +} + +// TryTransferLocalImages sends locally available ACS images to the cluster +// via the given pre-loader, skipping images already present in the cluster. +func (d *Deployer) TryTransferLocalImages(ctx context.Context, preLoader ImagePreLoader) error { + localImages, err := d.collectLocalImages(ctx) + if err != nil { + d.logger.Dimf("Collecting local images failed: %v", err) + return err + } + if len(localImages) == 0 { + d.logger.Dim("No local images found") + return nil + } + + availableImagesInCluster, err := preLoader.GetImages(ctx) + if err != nil && !errors.Is(err, ErrLocalImageRetrievalNotSupported) { + return fmt.Errorf("retrieving list of images available in local cluster: %w", err) + } + + // Simple for now. + for _, image := range localImages { + if slices.Contains(availableImagesInCluster, image) { + // Exists already in local cluster registry. + d.logger.Dimf("Image %s already available in local cluster, skipping.", image) + continue + } + d.logger.Dimf("Transferring local image %s to local cluster...", image) + if err := preLoader.SendImage(ctx, image); err != nil { + d.logger.Warningf("Transferring local image %s to %s cluster failed: %s", + image, d.config.Roxie.ClusterType, err) + } + } + + return nil +} + +// collectLocalImages returns the subset of ACS images that exist in the local +// container runtime (Docker/Podman). Returns nil if no runtime is reachable. +func (d *Deployer) collectLocalImages(ctx context.Context) ([]string, error) { + socket := d.containerRuntimeSocket + if socket == "" { + return nil, nil + } + + if d.verbose { + d.logger.Dimf("Using container runtime socket %s", socket) + } + available, err := containerrt.ListLocalImages(ctx, socket) + if err != nil { + d.logger.Dimf("Could not query container runtime at %s: %v", socket, err) + return nil, err + } + + availableSet := make(map[string]struct{}, len(available)) + for _, img := range available { + availableSet[img] = struct{}{} + } + + wanted := imagesForConfig(d.config) + localImages := make([]string, 0, len(wanted)) + for _, img := range wanted { + if _, ok := availableSet[img]; ok { + d.logger.Dimf("Image %s exists locally", img) + localImages = append(localImages, img) + } else { + d.logger.Dimf("Image %s needs to be pulled from registry", img) + } + } + return localImages, nil +} diff --git a/internal/deployer/local_images_custom.go b/internal/deployer/local_images_custom.go new file mode 100644 index 00000000..b136840b --- /dev/null +++ b/internal/deployer/local_images_custom.go @@ -0,0 +1,48 @@ +package deployer + +import ( + "context" + "fmt" + "os" + "os/exec" + "strings" + + "github.com/stackrox/roxie/internal/logger" +) + +type customImagePreLoader struct { + log *logger.Logger + command string +} + +func NewCustomImagePreloader(_ context.Context, log *logger.Logger, command string) ImagePreLoader { + return &customImagePreLoader{ + log: log, + command: command, + } +} + +func (c *customImagePreLoader) GetImages(_ context.Context) ([]string, error) { + return []string{}, ErrLocalImageRetrievalNotSupported +} + +func (c *customImagePreLoader) SendImage(ctx context.Context, image string) error { + env := os.Environ() + env = append(env, fmt.Sprintf("IMAGE=%s", image)) + c.log.Dimf("Invoking %q...", c.command) + cmd := exec.CommandContext(ctx, "/bin/sh", "-c", c.command) + cmd.Env = env + output, err := cmd.CombinedOutput() + if err != nil { + c.log.Warningf("Image preloading failed: %v", err) + for line := range strings.SplitSeq(strings.TrimSpace(string(output)), "\n") { + c.log.Dimf("| %s", line) + } + return fmt.Errorf("sending image failed: %w", err) + } + return nil +} + +func (c *customImagePreLoader) Name() string { + return "custom image preloader" +} diff --git a/internal/deployer/local_images_generic.go b/internal/deployer/local_images_generic.go new file mode 100644 index 00000000..3afe28c6 --- /dev/null +++ b/internal/deployer/local_images_generic.go @@ -0,0 +1,51 @@ +package deployer + +import ( + "errors" + "fmt" + "os/exec" + "strings" + + "context" + + "github.com/stackrox/roxie/internal/logger" +) + +type genericImageSender struct { + log *logger.Logger + args []string +} + +func newGenericImageSender(log *logger.Logger, args ...string) genericImageSender { + return genericImageSender{ + log: log, + args: args, + } +} + +func (g *genericImageSender) SendImage(ctx context.Context, imageTag string) error { + if len(g.args) == 0 { + return errors.New("genericImageSender: no command configured") + } + + args := make([]string, len(g.args)) + for i, arg := range g.args { + if arg == "" { + args[i] = imageTag + } else { + args[i] = g.args[i] + } + } + + cmd := exec.CommandContext(ctx, args[0], args[1:]...) + output, err := cmd.CombinedOutput() + if err != nil { + argsJoined := strings.Join(args, " ") + g.log.Errorf("Executing %s failed:", argsJoined) + for line := range strings.SplitSeq(strings.TrimSpace(string(output)), "\n") { + g.log.Errorf("| %s", line) + } + return fmt.Errorf("executing '%s': %w", argsJoined, err) + } + return nil +} diff --git a/internal/deployer/local_images_kind.go b/internal/deployer/local_images_kind.go new file mode 100644 index 00000000..e5db7854 --- /dev/null +++ b/internal/deployer/local_images_kind.go @@ -0,0 +1,52 @@ +package deployer + +import ( + "context" + "fmt" + "strings" + + "github.com/stackrox/roxie/internal/containerrt" + "github.com/stackrox/roxie/internal/logger" +) + +type kindImagePreLoader struct { + log *logger.Logger + genericImageSender + kindClusterName string + containerRuntimeSocket string +} + +func (d *Deployer) newKindImagePreloader() (*kindImagePreLoader, error) { + kindClusterName := kubeContextToKindClusterName(d.kubeContext) + d.logger.Dimf("Kind cluster name is %s", kindClusterName) + return &kindImagePreLoader{ + log: d.logger, + kindClusterName: kindClusterName, + genericImageSender: newGenericImageSender(d.logger, "kind", "load", "docker-image", "", "--name", kindClusterName), + containerRuntimeSocket: d.containerRuntimeSocket, + }, nil +} + +func (k *kindImagePreLoader) GetImages(ctx context.Context) ([]string, error) { + if k.containerRuntimeSocket == "" { + return nil, nil + } + + nodeName := k.kindClusterName + "-control-plane" + output, err := containerrt.ExecInContainer(ctx, k.containerRuntimeSocket, nodeName, + []string{"crictl", "images", "-o", "json"}) + if err != nil { + return nil, fmt.Errorf("listing images in kind node %s: %w", nodeName, err) + } + + return containerrt.ParseCrictlImages(output) +} + +func (k *kindImagePreLoader) Name() string { + return "Image pre-loader for local Kind clusters" +} + +func kubeContextToKindClusterName(kubeContext string) string { + // This seems to be a convention for kind clusters. + return strings.TrimPrefix(kubeContext, "kind-") +} diff --git a/internal/deployer/local_images_minikube.go b/internal/deployer/local_images_minikube.go new file mode 100644 index 00000000..a5fe6e95 --- /dev/null +++ b/internal/deployer/local_images_minikube.go @@ -0,0 +1,44 @@ +package deployer + +import ( + "context" + "fmt" + "os/exec" + "strings" + + "github.com/stackrox/roxie/internal/containerrt" + "github.com/stackrox/roxie/internal/logger" +) + +type minikubeImagePreLoader struct { + log *logger.Logger + genericImageSender +} + +var ( + minikubeGetImagesCommand = []string{"minikube", "ssh", "--", "crictl", "images", "-o", "json"} +) + +func (d *Deployer) newMinikubeImagePreloader() (*minikubeImagePreLoader, error) { + return &minikubeImagePreLoader{ + log: d.logger, + genericImageSender: newGenericImageSender(d.logger, "minikube", "image", "load", ""), + }, nil +} + +func (k *minikubeImagePreLoader) Name() string { + return "Image pre-loader for local Minikube clusters" +} + +func (k *minikubeImagePreLoader) GetImages(ctx context.Context) ([]string, error) { + cmd := exec.CommandContext(ctx, minikubeGetImagesCommand[0], minikubeGetImagesCommand[1:]...) + output, err := cmd.CombinedOutput() + if err != nil { + k.log.Warningf("Command %q failed: %v", strings.Join(minikubeGetImagesCommand, " "), err) + for line := range strings.SplitSeq(strings.TrimSpace(string(output)), "\n") { + k.log.Dimf("| %s", line) + } + return nil, fmt.Errorf("listing images in minikube node: %w", err) + } + return containerrt.ParseCrictlImages(output) +} diff --git a/internal/deployer/operator.go b/internal/deployer/operator.go index c10b4c37..29577ab6 100644 --- a/internal/deployer/operator.go +++ b/internal/deployer/operator.go @@ -19,11 +19,9 @@ import ( ) const ( - adminPasswordSecretName = "admin-password" - operatorNamespace = "rhacs-operator-system" - operatorDeploymentName = "rhacs-operator-controller-manager" - operatorBundleImageRepo = "quay.io/rhacs-eng/stackrox-operator-bundle" - operatorBundleImageReleaseRepo = "quay.io/rhacs-eng/release-operator-bundle" + adminPasswordSecretName = "admin-password" + operatorNamespace = "rhacs-operator-system" + operatorDeploymentName = "rhacs-operator-controller-manager" ) var requiredCRDs = []string{ @@ -44,7 +42,7 @@ func (d *Deployer) deployOperatorNonOLM(ctx context.Context) error { return fmt.Errorf("failed to remove Konflux ImageContentSourcePolicy: %v", err) } } - bundleImage := d.getOperatorBundleImage() + bundleImage := OperatorBundleImage(d.config) bundleDir, err := d.downloadAndExtractOperatorBundle(ctx, bundleImage) if err != nil { @@ -81,7 +79,7 @@ func (d *Deployer) downloadAndExtractOperatorBundle(ctx context.Context, bundleI d.logger.Info("Pulling and extracting operator bundle image...") // The bundle images only contain platform-agnostic YAML files. - if err := ocihelper.ExtractManifestsFromImage(ctx, d.logger, bundleImage, bundleDir); err != nil { + if err := ocihelper.ExtractManifestsFromImage(ctx, d.logger, bundleImage, bundleDir, d.containerRuntimeSocket); err != nil { os.RemoveAll(bundleDir) return "", fmt.Errorf("failed to copy bundle contents: %w", err) } @@ -169,7 +167,7 @@ func (d *Deployer) ensureCRDsInstalled(ctx context.Context) error { } if len(missing) > 0 { - bundleImage := d.getOperatorBundleImage() + bundleImage := OperatorBundleImage(d.config) d.logger.Warningf("Missing CRDs detected (%s)", strings.Join(missing, ", ")) d.logger.Warningf("Fetching bundle %s", bundleImage) @@ -190,14 +188,6 @@ func (d *Deployer) ensureCRDsInstalled(ctx context.Context) error { return nil } -func (d *Deployer) getOperatorBundleImage() string { - if d.config.Roxie.KonfluxImages { - d.logger.Infof("Using Konflux-built operator bundle image") - return fmt.Sprintf(operatorBundleImageReleaseRepo+":v%s", d.config.Operator.Version) - } - return fmt.Sprintf(operatorBundleImageRepo+":v%s", d.config.Operator.Version) -} - // ensureKonfluxImageRewriting configures image rewriting for Konflux images func (d *Deployer) ensureKonfluxImageRewriting(ctx context.Context) error { if !d.config.Roxie.ClusterType.IsOpenShift() { diff --git a/internal/helpers/helpers.go b/internal/helpers/helpers.go index 857873b6..c6992e48 100644 --- a/internal/helpers/helpers.go +++ b/internal/helpers/helpers.go @@ -36,11 +36,6 @@ func RunCommandWithOutput(title string, name string, args []string, opts ...Comm // CommandOption is a function that modifies an exec.Cmd type CommandOption func(*exec.Cmd) -// GetContainerTool returns the container tool to use (podman) -func GetContainerTool() string { - return "podman" -} - // LoadYAMLFile loads a YAML file and unmarshals it into a map // Returns an empty map if path is empty or file doesn't exist func LoadYAMLFile(path string) (map[string]interface{}, error) { diff --git a/internal/ocihelper/ocihelper.go b/internal/ocihelper/ocihelper.go index 4b88022a..b2792b7f 100644 --- a/internal/ocihelper/ocihelper.go +++ b/internal/ocihelper/ocihelper.go @@ -11,7 +11,9 @@ import ( "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/daemon" "github.com/google/go-containerregistry/pkg/v1/remote" + dockerclient "github.com/moby/moby/client" "github.com/stackrox/roxie/internal/logger" ) @@ -40,7 +42,7 @@ func VerifyImageExistence(ctx context.Context, log *logger.Logger, imageRef stri // ExtractManifestsFromImage extracts the /manifests/ directory from an operator bundle image. // Authentication is handled automatically from ~/.docker/config.json or $REGISTRY_AUTH_FILE. -func ExtractManifestsFromImage(ctx context.Context, log *logger.Logger, imageRef, destDir string) error { +func ExtractManifestsFromImage(ctx context.Context, log *logger.Logger, imageRef, destDir, containerRuntimeSocket string) error { tempDir, err := os.MkdirTemp("", "oci-image-") if err != nil { return fmt.Errorf("failed to create temp dir: %w", err) @@ -49,7 +51,7 @@ func ExtractManifestsFromImage(ctx context.Context, log *logger.Logger, imageRef log.Dimf("Using temporary directory: %s", tempDir) - img, err := fetchImage(ctx, log, imageRef) + img, err := assureImageExistsLocally(ctx, log, imageRef, containerRuntimeSocket) if err != nil { return err } @@ -63,8 +65,7 @@ func ExtractManifestsFromImage(ctx context.Context, log *logger.Logger, imageRef return nil } -// fetchImage downloads an OCI image from a registry. -func fetchImage(ctx context.Context, log *logger.Logger, imageRef string) (v1.Image, error) { +func assureImageExistsLocally(ctx context.Context, log *logger.Logger, imageRef, containerRuntimeSocket string) (v1.Image, error) { log.Dimf("Fetching image %s", imageRef) ref, err := name.ParseReference(imageRef) @@ -72,6 +73,24 @@ func fetchImage(ctx context.Context, log *logger.Logger, imageRef string) (v1.Im return nil, fmt.Errorf("invalid image reference: %w", err) } + daemonOpts := []daemon.Option{daemon.WithContext(ctx)} + if containerRuntimeSocket != "" { + client, err := dockerclient.New(dockerclient.WithHost(containerRuntimeSocket)) + if err == nil { + daemonOpts = append(daemonOpts, daemon.WithClient(client)) + } else { + log.Dimf("Failed to create moby client for %s: %v", containerRuntimeSocket, err) + } + } + + img, err := daemon.Image(ref, daemonOpts...) + if err == nil { + log.Dimf("✓ Image %s found in local daemon", imageRef) + return img, nil + } + + log.Dimf("Image not found in local daemon, pulling from registry: %v", err) + // For operator bundles, we fetch linux/amd64 by default as they contain // platform-agnostic YAML files. platform := v1.Platform{ @@ -79,7 +98,7 @@ func fetchImage(ctx context.Context, log *logger.Logger, imageRef string) (v1.Im Architecture: "amd64", } - img, err := remote.Image(ref, + img, err = remote.Image(ref, remote.WithContext(ctx), remote.WithAuthFromKeychain(authn.DefaultKeychain), remote.WithPlatform(platform)) diff --git a/internal/ocihelper/ocihelper_integration_test.go b/internal/ocihelper/ocihelper_integration_test.go index 0c320051..15d4af12 100644 --- a/internal/ocihelper/ocihelper_integration_test.go +++ b/internal/ocihelper/ocihelper_integration_test.go @@ -27,7 +27,7 @@ func TestExtractManifestsFromImage_Integration(t *testing.T) { ctx := context.Background() t.Logf("Extracting manifests from %s", bundleImage) - err = ExtractManifestsFromImage(ctx, log, bundleImage, destDir) + err = ExtractManifestsFromImage(ctx, log, bundleImage, destDir, "") if err != nil { t.Fatalf("ExtractManifestsFromImage failed: %v", err) } diff --git a/internal/types/cluster_type.go b/internal/types/cluster_type.go index dfdaa0c4..5621665f 100644 --- a/internal/types/cluster_type.go +++ b/internal/types/cluster_type.go @@ -74,3 +74,11 @@ func (ct *ClusterType) UnmarshalYAML(unmarshal func(any) error) error { func (ct ClusterType) NeedsPullSecrets() bool { return ct != ClusterTypeInfraOpenShift4 } + +func (ct ClusterType) IsLocal() bool { + switch ct { + case ClusterTypeKind, ClusterTypeMinikube, ClusterTypeK3s, ClusterTypeCRC: + return true + } + return false +}