diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 4a32bc1..f1474c9 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -67,6 +67,8 @@ jobs: username: ${{ secrets.QUAY_STACKROX_IO_RW_USERNAME }} password: ${{ secrets.QUAY_STACKROX_IO_RW_PASSWORD }} + # Distro-less "FROM scratch" images. + - name: Prepare manifest OCI metadata for amd64 id: meta-amd64 uses: docker/metadata-action@v6 @@ -204,6 +206,149 @@ jobs: docker manifest push "${tag}" done + # Distro-full "FROM debian:stable-slim" images. + + - name: Prepare manifest OCI metadata for amd64 - distro + id: meta-amd64-distro + uses: docker/metadata-action@v6 + with: + images: "quay.io/stackrox-io/image-prefetcher" + # generate Docker tags based on the following events/attributes + # See https://github.com/docker/metadata-action + flavor: suffix=-distro-amd64,onlatest=true + tags: | + type=ref,event=branch,prefix=branch- + type=semver,pattern=v{{major}}.{{minor}}.{{patch}} + type=semver,pattern=v{{major}}.{{minor}} + type=semver,pattern=v{{major}} + type=sha + + - name: Build and push OCI amd64 image - distro + if: github.event_name != 'pull_request' + uses: docker/build-push-action@v7 + with: + context: . + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.meta-amd64-distro.outputs.tags }} + labels: ${{ steps.meta-amd64-distro.outputs.labels }} + platforms: linux/amd64 + build-args: | + ARCH=amd64 + BASE=debian:stable-slim + + - name: Prepare manifest OCI metadata for arm64 - distro + id: meta-arm64-distro + uses: docker/metadata-action@v6 + with: + images: "quay.io/stackrox-io/image-prefetcher" + # generate Docker tags based on the following events/attributes + # See https://github.com/docker/metadata-action + flavor: suffix=-distro-arm64,onlatest=true + tags: | + type=ref,event=branch,prefix=branch- + type=semver,pattern=v{{major}}.{{minor}}.{{patch}} + type=semver,pattern=v{{major}}.{{minor}} + type=semver,pattern=v{{major}} + type=sha + + - name: Build and push OCI arm64 image - distro + if: github.event_name != 'pull_request' + uses: docker/build-push-action@v7 + with: + context: . + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.meta-arm64-distro.outputs.tags }} + labels: ${{ steps.meta-arm64-distro.outputs.labels }} + platforms: linux/arm64 + build-args: | + ARCH=arm64 + BASE=debian:stable-slim + + - name: Prepare manifest OCI metadata for ppc64le - distro + id: meta-ppc64le-distro + uses: docker/metadata-action@v6 + with: + images: "quay.io/stackrox-io/image-prefetcher" + # generate Docker tags based on the following events/attributes + # See https://github.com/docker/metadata-action + flavor: suffix=-distro-ppc64le,onlatest=true + tags: | + type=ref,event=branch,prefix=branch- + type=semver,pattern=v{{major}}.{{minor}}.{{patch}} + type=semver,pattern=v{{major}}.{{minor}} + type=semver,pattern=v{{major}} + type=sha + + - name: Build and push OCI ppc64le image - distro + if: github.event_name != 'pull_request' + uses: docker/build-push-action@v7 + with: + context: . + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.meta-ppc64le-distro.outputs.tags }} + labels: ${{ steps.meta-ppc64le-distro.outputs.labels }} + platforms: linux/ppc64le + build-args: | + ARCH=ppc64le + BASE=debian:stable-slim + + - name: Prepare manifest OCI metadata for s390x - distro + id: meta-s390x-distro + uses: docker/metadata-action@v6 + with: + images: "quay.io/stackrox-io/image-prefetcher" + # generate Docker tags based on the following events/attributes + # See https://github.com/docker/metadata-action + flavor: suffix=-distro-s390x,onlatest=true + tags: | + type=ref,event=branch,prefix=branch- + type=semver,pattern=v{{major}}.{{minor}}.{{patch}} + type=semver,pattern=v{{major}}.{{minor}} + type=semver,pattern=v{{major}} + type=sha + + - name: Build and push OCI s390x image - distro + if: github.event_name != 'pull_request' + uses: docker/build-push-action@v7 + with: + context: . + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.meta-s390x-distro.outputs.tags }} + labels: ${{ steps.meta-s390x-distro.outputs.labels }} + platforms: linux/s390x + build-args: | + ARCH=s390x + BASE=debian:stable-slim + + - name: Prepare manifest OCI metadata - distro + id: meta-distro + uses: docker/metadata-action@v6 + with: + images: "quay.io/stackrox-io/image-prefetcher" + # generate Docker tags based on the following events/attributes + # See https://github.com/docker/metadata-action + flavor: suffix=-distro + tags: | + type=ref,event=branch,prefix=branch- + type=semver,pattern=v{{major}}.{{minor}}.{{patch}} + type=semver,pattern=v{{major}}.{{minor}} + type=semver,pattern=v{{major}} + type=sha + + - name: Create and push multi-arch manifest - distro + if: github.event_name != 'pull_request' + env: + IMAGE_TAGS: ${{ steps.meta-distro.outputs.tags }} + run: | + for tag in ${IMAGE_TAGS}; + do + docker manifest create "${tag}" \ + --amend "${tag}-amd64" \ + --amend "${tag}-arm64" \ + --amend "${tag}-ppc64le" \ + --amend "${tag}-s390x" + docker manifest push "${tag}" + done e2e: if: github.event_name != 'pull_request' diff --git a/Dockerfile b/Dockerfile index f488d21..7bad588 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,5 @@ -FROM scratch +ARG BASE=scratch +FROM $BASE ARG ARCH=amd64 COPY ./image-prefetcher-${ARCH} /image-prefetcher ENTRYPOINT ["/image-prefetcher"] diff --git a/README.md b/README.md index e1ffbaa..207f324 100644 --- a/README.md +++ b/README.md @@ -53,8 +53,11 @@ It also optionally collects each pull attempt's duration and result. Plugin credentials fetched dynamically and tried for the images configured in the `CredentialProviderConfig` before pull secrets. Currently only supports mode `GKE`, which uses `/etc/srv/kubernetes/cri_auth_config.yaml` and `/home/kubernetes/bin` mounted from the host. - Example: + Note that in this case, the tool uses distro-based prefetcher images, to provide the dynamic + linker and shared libraries that a credential plugin binary might need. + Example: + ``` go run github.com/stackrox/image-prefetcher/deploy@v0.3.0 --version v0.3.0 --namespace prefetch-images my-images > manifest.yaml ``` diff --git a/deploy/main.go b/deploy/main.go index 0b36b48..41bbd5d 100644 --- a/deploy/main.go +++ b/deploy/main.go @@ -56,18 +56,22 @@ func init() { // processVersion processes the version string and returns the appropriate format. // For versions with dashes containing SHA (like v0.4.2-0.20251126115717-559dd9fd402f), // extracts short SHA and returns "sha-" as long as the sha is at least 7 chars long. -// Otherwise, returns as is -func processVersion(version string) string { +// Otherwise, returns as is. +// If distro is true, appends "-distro" to the result. +func processVersion(version string, distro bool) string { + result := version // Pattern: vX.Y.Z-0.timestamp-SHA produced by `go mod tidy`. dashRegex := regexp.MustCompile(`^v\d+\.\d+\.\d+-\d+\.\d+-([a-f0-9]+)$`) matches := dashRegex.FindStringSubmatch(version) - if len(matches) != 2 { - return version + if len(matches) == 2 { + if fullSHA := matches[1]; len(fullSHA) >= 7 { + result = fmt.Sprintf("sha-%s", fullSHA[:7]) + } } - if fullSHA := matches[1]; len(fullSHA) >= 7 { - return fmt.Sprintf("sha-%s", fullSHA[:7]) + if distro { + result += "-distro" } - return version + return result } func main() { @@ -84,7 +88,7 @@ func main() { Name: name, Namespace: namespace, Image: imageRepo, - Version: processVersion(version), + Version: processVersion(version, useKubeletImageCredentialIntegration != ""), Secret: secret, IsCRIO: isOcp, NeedsPrivileged: isOcp, diff --git a/internal/credentialprovider/plugin.go b/internal/credentialprovider/plugin.go index d7fc91b..7734d52 100644 --- a/internal/credentialprovider/plugin.go +++ b/internal/credentialprovider/plugin.go @@ -17,7 +17,7 @@ import ( const ( supportedResponseAPIVersion = "credentialprovider.kubelet.k8s.io/v1" - supportedConfigApiVersion = "kubelet.config.k8s.io/v1" + supportedConfigAPIVersion = "kubelet.config.k8s.io/v1" ) // Minimal mirrors of k8s.io/kubelet/config/v1.CredentialProviderConfig to avoid depending on k8s.io/kubernetes. @@ -101,8 +101,8 @@ func readCredentialProviderConfig(configPath string) (*credentialProviderConfig, return nil, fmt.Errorf("unexpected kind %q, expected CredentialProviderConfig", config.Kind) } - if config.APIVersion != supportedConfigApiVersion { - return nil, fmt.Errorf("unexpected API version %q, expected %q", config.APIVersion, supportedConfigApiVersion) + if config.APIVersion != supportedConfigAPIVersion { + return nil, fmt.Errorf("unexpected API version %q, expected %q", config.APIVersion, supportedConfigAPIVersion) } return &config, nil @@ -119,7 +119,7 @@ func (kr *PluginKeyring) LookupWithCtx(ctx context.Context, image string) ([]Aut return nil, false } - var allCreds []AuthConfig + dk := &BasicDockerKeyring{} for _, provider := range kr.providers { if !kr.matchesImage(provider.matchImages, image) { continue @@ -131,15 +131,10 @@ func (kr *PluginKeyring) LookupWithCtx(ctx context.Context, image string) ([]Aut kr.logger.Warn("credential provider plugin failed", "plugin", provider.name, "image", image, "error", err) continue } - - allCreds = append(allCreds, creds...) - } - - if len(allCreds) > 0 { - return allCreds, true + dk.Add(creds) } - return nil, false + return dk.Lookup(image) } // matchesImage checks if any of the match patterns match the given image. @@ -154,7 +149,7 @@ func (kr *PluginKeyring) matchesImage(patterns []string, image string) bool { } // execPlugin executes the credential provider plugin and parses the response. -func (kr *PluginKeyring) execPlugin(ctx context.Context, provider pluginProviderWrapper, image string) ([]AuthConfig, error) { +func (kr *PluginKeyring) execPlugin(ctx context.Context, provider pluginProviderWrapper, image string) (DockerConfig, error) { // Prepare the request request := credentialproviderv1.CredentialProviderRequest{ Image: image, @@ -188,16 +183,13 @@ func (kr *PluginKeyring) execPlugin(ctx context.Context, provider pluginProvider return nil, fmt.Errorf("apiVersion from credential plugin response did not match expected apiVersion:%s, actual apiVersion:%s", supportedResponseAPIVersion, response.APIVersion) } - // Convert to AuthConfig - var creds []AuthConfig - for registry, authConfig := range response.Auth { - creds = append(creds, AuthConfig{ - Username: authConfig.Username, - Password: authConfig.Password, - ServerAddress: registry, - }) + kr.logger.Debug("received credentials from plugin", "plugin", provider.name, "count", len(response.Auth)) + dockerConfig := make(DockerConfig, len(response.Auth)) + for matchImage, authConfig := range response.Auth { + dockerConfig[matchImage] = DockerConfigEntry{ + Username: authConfig.Username, + Password: authConfig.Password, + } } - - kr.logger.Debug("received credentials from plugin", "plugin", provider.name, "count", len(creds)) - return creds, nil + return dockerConfig, nil } diff --git a/internal/main.go b/internal/main.go index c9e619d..860651d 100644 --- a/internal/main.go +++ b/internal/main.go @@ -78,7 +78,7 @@ func Run(logger *slog.Logger, criSocketPath string, dockerConfigJSONPath string, }, Auth: auth, } - go pullImageWithRetries(ctx, logger.With("image", imageName, "authNum", i), &wg, criClient, metricsSink.Chan(), imageName, request, timing, &results) + go pullImageWithRetries(ctx, logger.With("image", imageName, "authNum", i, "authServer", auth.ServerAddress, "authUsername", auth.Username), &wg, criClient, metricsSink.Chan(), imageName, request, timing, &results) } } wg.Wait() @@ -141,9 +141,8 @@ func getAuthsForImage(ctx context.Context, logger *slog.Logger, pluginKr *creden logger.DebugContext(ctx, "got credentials from plugin", "image", imageName, "count", len(pluginCreds)) for _, creds := range pluginCreds { auth := &criV1.AuthConfig{ - Username: creds.Username, - Password: creds.Password, - ServerAddress: creds.ServerAddress, + Username: creds.Username, + Password: creds.Password, } auths = append(auths, auth) }