diff --git a/go/.gitignore b/go/.gitignore new file mode 100644 index 0000000..ced8b59 --- /dev/null +++ b/go/.gitignore @@ -0,0 +1,2 @@ +cmd/imagebuilder/imagebuilder +go/imagebuilder \ No newline at end of file diff --git a/go/Dockerfile b/go/Dockerfile new file mode 100644 index 0000000..3eb86d7 --- /dev/null +++ b/go/Dockerfile @@ -0,0 +1,62 @@ +# Build stage +FROM docker.io/library/golang:1.23-bookworm AS builder + +# Install build dependencies for CGO +RUN apt-get update && apt-get install -y gcc libc6-dev libgpgme-dev libseccomp-dev libsqlite3-dev libbtrfs-dev libdevmapper-dev && rm -rf /var/lib/apt/lists/* + +WORKDIR /build + +# Copy source code +COPY . ./ + +# Download dependencies +RUN go mod download + +# Build the application with CGO enabled +RUN go build -o image-builder ./cmd/image-builder + +# Runtime stage +FROM docker.io/library/almalinux:9.6 + +RUN dnf clean all && \ + dnf update --nogpgcheck -y && \ + dnf install -y epel-release + +RUN dnf install -y \ + bash \ + buildah \ + squashfs-tools \ + fuse-overlayfs + +# Create local user for rootless image builds +RUN useradd --uid 1002 builder && \ + chown -R builder /home/builder + +# Set up capabilities for newuidmap/newgidmap +RUN setcap cap_setuid=ep "$(command -v newuidmap)" && \ + setcap cap_setgid=ep "$(command -v newgidmap)" &&\ + chmod 0755 "$(command -v newuidmap)" && \ + chmod 0755 "$(command -v newgidmap)" && \ + rpm --restore shadow-utils && \ + echo "builder:2000:50000" > /etc/subuid && \ + echo "builder:2000:50000" > /etc/subgid + +# Make builder the default user when running container +USER builder +WORKDIR /home/builder + +ENV BUILDAH_ISOLATION=chroot + +# Configure container storage for builder user +# Using 'vfs' storage driver to work around whiteout file handling issues in nested containers +RUN mkdir -p /home/builder/.config/containers && \ + { \ + echo '[storage]'; \ + echo 'driver = "vfs"'; \ + echo 'graphroot = "/home/builder/.local/share/containers/storage"'; \ + } > /home/builder/.config/containers/storage.conf + +# Copy binary from builder +COPY --from=builder /build/image-builder /app/image-builder + +ENTRYPOINT ["/app/image-builder"] diff --git a/go/Dockerfile.scratch-helper b/go/Dockerfile.scratch-helper new file mode 100644 index 0000000..1994e89 --- /dev/null +++ b/go/Dockerfile.scratch-helper @@ -0,0 +1,7 @@ +FROM docker.io/library/almalinux:8.8 + +RUN dnf clean all && \ + dnf update --nogpgcheck -y && \ + dnf install -y epel-release && \ + dnf config-manager -y --set-enabled powertools + diff --git a/go/README.md b/go/README.md new file mode 100644 index 0000000..c7dcf22 --- /dev/null +++ b/go/README.md @@ -0,0 +1,17 @@ +This version of image-build uses the buildah API directly instead of shelling out to the buildah command line tool. See the [buildah API documentation](https://pkg.go.dev/github.com/containers/buildah) for more details. The idea is that it will give us tighter integration with container APIs and is a cleaner approach than using subprocesses. + +Its designed to be a drop-in replacement for the python implementation, so the CLI is very similar, appart from following the Go conventions, such as using a single dash for flags. It operates on the same configuration files as the python version. + +It supports: + +- scratch parents using a helper container to provide dnf, yum etc. without them having to be installed on the host. +- ansible layers again using a helper container and the chroot connection plugin, again not need to ansible to be installed on the host. +- publishing to S3, registries and local + +It currently doesn't support the following flags ( although they could be added if needed): +- -scap-benchmark +- -oval-eval +- -install-scap + +Support for `--buildah_extra_args` such as when running command is not implemented, as it probably makes more sense to explictly support the options with +additions to the configation file format, for example supporting mounting volumes when running commands. \ No newline at end of file diff --git a/go/cmd/image-builder/main.go b/go/cmd/image-builder/main.go new file mode 100644 index 0000000..0d729c3 --- /dev/null +++ b/go/cmd/image-builder/main.go @@ -0,0 +1,72 @@ +package main + +import ( + "log" + "os" + + "github.com/OpenCHAMI/image-builder/go/pkg/arguments" + "github.com/OpenCHAMI/image-builder/go/pkg/config" + "github.com/OpenCHAMI/image-builder/go/pkg/layer" + "github.com/containers/buildah" + "github.com/containers/storage/pkg/reexec" + "github.com/containers/storage/pkg/unshare" +) + +func needsRootlessSetup(layerType, parent string) bool { + // Only need rootless setup if we're not running as root + if os.Getuid() == 0 { + return false + } + + // Need rootless setup for layers that require mounting: + // - ansible layers (need to mount for chroot operations) + // - scratch parent (need to mount to build from scratch) + return layerType == "ansible" || parent == "scratch" +} + +func main() { + // Initialize reexec for buildah/containers libraries + if reexec.Init() { + return + } + + // Parse command-line arguments first to determine if we need rootless setup + args, err := arguments.ParseCommandLine() + if err != nil { + log.Fatalf("Error parsing arguments: %v", err) + } + + // Load configuration file to check layer type and parent + log.Printf("DEBUG: Loading config file: %s", args.ConfigFile) + cfg, err := config.LoadConfig(args.ConfigFile) + if err != nil { + log.Fatalf("Error loading config: %v", err) + } + + // Merge command-line arguments into the loaded config + cfg.MergeCommandLineArgs(args) + + // Only initialize rootless setup if needed + if needsRootlessSetup(cfg.LayerType, cfg.Parent) { + log.Printf("DEBUG: Initializing rootless setup for layer_type=%s, parent=%s", cfg.LayerType, cfg.Parent) + // Initialize buildah reexec for rootless operation + if buildah.InitReexec() { + return + } + // Handle user namespace setup for rootless containers + unshare.MaybeReexecUsingUserNamespace(false) + } + + // Create layer builder with unified config + layerBuilder, err := layer.NewLayer(cfg) + if err != nil { + log.Fatalf("Error creating layer builder: %v", err) + } + + // Build the layer + if err := layerBuilder.BuildLayer(); err != nil { + log.Fatalf("Error building layer: %v", err) + } + + log.Printf("Image build completed successfully") +} diff --git a/go/go.mod b/go/go.mod new file mode 100644 index 0000000..43c2b6c --- /dev/null +++ b/go/go.mod @@ -0,0 +1,160 @@ +module github.com/OpenCHAMI/image-builder/go + +go 1.23.3 + +toolchain go1.23.4 + +require ( + github.com/aws/aws-sdk-go-v2 v1.39.2 + github.com/aws/aws-sdk-go-v2/config v1.31.11 + github.com/aws/aws-sdk-go-v2/service/s3 v1.88.3 + github.com/containers/buildah v1.41.4 + github.com/containers/image/v5 v5.36.2 + github.com/containers/storage v1.59.1 + github.com/opencontainers/runtime-spec v1.2.1 + gopkg.in/yaml.v3 v3.0.1 +) + +require ( + dario.cat/mergo v1.0.2 // indirect + github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect + github.com/BurntSushi/toml v1.5.0 // indirect + github.com/Microsoft/go-winio v0.6.2 // indirect + github.com/Microsoft/hcsshim v0.13.0 // indirect + github.com/VividCortex/ewma v1.2.0 // indirect + github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d // indirect + github.com/aead/serpent v0.0.0-20160714141033-fba169763ea6 // indirect + github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.1 // indirect + github.com/aws/aws-sdk-go-v2/credentials v1.18.15 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.9 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.9 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.9 // indirect + github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // indirect + github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.9 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.1 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.8.9 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.9 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.9 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.29.5 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.1 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.38.6 // indirect + github.com/aws/smithy-go v1.23.0 // indirect + github.com/chzyer/readline v1.5.1 // indirect + github.com/containerd/cgroups/v3 v3.0.5 // indirect + github.com/containerd/errdefs v1.0.0 // indirect + github.com/containerd/errdefs/pkg v0.3.0 // indirect + github.com/containerd/log v0.1.0 // indirect + github.com/containerd/platforms v1.0.0-rc.1 // indirect + github.com/containerd/stargz-snapshotter/estargz v0.17.0 // indirect + github.com/containerd/typeurl/v2 v2.2.3 // indirect + github.com/containernetworking/cni v1.3.0 // indirect + github.com/containernetworking/plugins v1.7.1 // indirect + github.com/containers/common v0.64.2 // indirect + github.com/containers/libtrust v0.0.0-20230121012942-c1716e8a8d01 // indirect + github.com/containers/luksy v0.0.0-20250609192159-bc60f96d4194 // indirect + github.com/containers/ocicrypt v1.2.1 // indirect + github.com/coreos/go-systemd/v22 v22.5.0 // indirect + github.com/cyberphone/json-canonicalization v0.0.0-20241213102144-19d51d7fe467 // indirect + github.com/cyphar/filepath-securejoin v0.4.1 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/disiqueira/gotree/v3 v3.0.2 // indirect + github.com/distribution/reference v0.6.0 // indirect + github.com/docker/distribution v2.8.3+incompatible // indirect + github.com/docker/docker v28.3.3+incompatible // indirect + github.com/docker/docker-credential-helpers v0.9.3 // indirect + github.com/docker/go-connections v0.5.0 // indirect + github.com/docker/go-units v0.5.0 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/fsnotify/fsnotify v1.9.0 // indirect + github.com/fsouza/go-dockerclient v1.12.1 // indirect + github.com/go-jose/go-jose/v4 v4.0.5 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/godbus/dbus/v5 v5.1.0 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/google/go-containerregistry v0.20.3 // indirect + github.com/google/go-intervals v0.0.2 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/gorilla/mux v1.8.1 // indirect + github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/go-multierror v1.1.1 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/jinzhu/copier v0.4.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/compress v1.18.0 // indirect + github.com/klauspost/pgzip v1.2.6 // indirect + github.com/letsencrypt/boulder v0.0.0-20240620165639-de9c06129bec // indirect + github.com/manifoldco/promptui v0.9.0 // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect + github.com/mattn/go-shellwords v1.0.12 // indirect + github.com/mattn/go-sqlite3 v1.14.28 // indirect + github.com/miekg/pkcs11 v1.1.1 // indirect + github.com/mistifyio/go-zfs/v3 v3.0.1 // indirect + github.com/moby/buildkit v0.23.2 // indirect + github.com/moby/docker-image-spec v1.3.1 // indirect + github.com/moby/go-archive v0.1.0 // indirect + github.com/moby/patternmatcher v0.6.0 // indirect + github.com/moby/sys/capability v0.4.0 // indirect + github.com/moby/sys/mountinfo v0.7.2 // indirect + github.com/moby/sys/sequential v0.6.0 // indirect + github.com/moby/sys/user v0.4.0 // indirect + github.com/moby/sys/userns v0.1.0 // indirect + github.com/moby/term v0.5.2 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/morikuni/aec v1.0.0 // indirect + github.com/opencontainers/cgroups v0.0.4 // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/opencontainers/image-spec v1.1.1 // indirect + github.com/opencontainers/runc v1.3.0 // indirect + github.com/opencontainers/runtime-tools v0.9.1-0.20250523060157-0ea5ed0382a2 // indirect + github.com/opencontainers/selinux v1.12.0 // indirect + github.com/openshift/imagebuilder v1.2.16 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/proglottis/gpgme v0.1.4 // indirect + github.com/rivo/uniseg v0.4.7 // indirect + github.com/seccomp/libseccomp-golang v0.11.0 // indirect + github.com/secure-systems-lab/go-securesystemslib v0.9.0 // indirect + github.com/sigstore/fulcio v1.6.6 // indirect + github.com/sigstore/protobuf-specs v0.4.1 // indirect + github.com/sigstore/sigstore v1.9.5 // indirect + github.com/sirupsen/logrus v1.9.3 // indirect + github.com/smallstep/pkcs7 v0.1.1 // indirect + github.com/spf13/cobra v1.9.1 // indirect + github.com/spf13/pflag v1.0.6 // indirect + github.com/stefanberger/go-pkcs11uri v0.0.0-20230803200340-78284954bff6 // indirect + github.com/sylabs/sif/v2 v2.21.1 // indirect + github.com/tchap/go-patricia/v2 v2.3.3 // indirect + github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399 // indirect + github.com/ulikunitz/xz v0.5.15 // indirect + github.com/vbatts/tar-split v0.12.1 // indirect + github.com/vbauerster/mpb/v8 v8.10.2 // indirect + github.com/vishvananda/netlink v1.3.1 // indirect + github.com/vishvananda/netns v0.0.5 // indirect + go.etcd.io/bbolt v1.4.2 // indirect + go.opencensus.io v0.24.0 // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect + go.opentelemetry.io/otel v1.35.0 // indirect + go.opentelemetry.io/otel/metric v1.35.0 // indirect + go.opentelemetry.io/otel/trace v1.35.0 // indirect + go.yaml.in/yaml/v2 v2.4.2 // indirect + golang.org/x/crypto v0.40.0 // indirect + golang.org/x/mod v0.25.0 // indirect + golang.org/x/net v0.41.0 // indirect + golang.org/x/sync v0.16.0 // indirect + golang.org/x/sys v0.35.0 // indirect + golang.org/x/term v0.33.0 // indirect + golang.org/x/text v0.27.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250313205543-e70fdf4c4cb4 // indirect + google.golang.org/grpc v1.72.2 // indirect + google.golang.org/protobuf v1.36.6 // indirect + sigs.k8s.io/yaml v1.5.0 // indirect + tags.cncf.io/container-device-interface v1.0.1 // indirect + tags.cncf.io/container-device-interface/specs-go v1.0.0 // indirect +) diff --git a/go/go.sum b/go/go.sum new file mode 100644 index 0000000..0b263cc --- /dev/null +++ b/go/go.sum @@ -0,0 +1,570 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= +dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= +github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg= +github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg= +github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= +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.13.0 h1:/BcXOiS6Qi7N9XqUcv27vkIuVOkBEcWstd2pMlWSeaA= +github.com/Microsoft/hcsshim v0.13.0/go.mod h1:9KWJ/8DgU+QzYGupX4tzMhRQE8h6w90lH6HAaclpEok= +github.com/VividCortex/ewma v1.2.0 h1:f58SaIzcDXrSy3kWaHNvuJgJ3Nmz59Zji6XoJR/q1ow= +github.com/VividCortex/ewma v1.2.0/go.mod h1:nz4BbCtbLyFDeC9SUHbtcT5644juEuWfUAUnGx7j5l4= +github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d h1:licZJFw2RwpHMqeKTCYkitsPqHNxTmd4SNR5r94FGM8= +github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d/go.mod h1:asat636LX7Bqt5lYEZ27JNDcqxfjdBQuJ/MM4CN/Lzo= +github.com/aead/serpent v0.0.0-20160714141033-fba169763ea6 h1:5L8Mj9Co9sJVgW3TpYk2gxGJnDjsYuboNTcRmbtGKGs= +github.com/aead/serpent v0.0.0-20160714141033-fba169763ea6/go.mod h1:3HgLJ9d18kXMLQlJvIY3+FszZYMxCz8WfE2MQ7hDY0w= +github.com/aws/aws-sdk-go-v2 v1.39.2 h1:EJLg8IdbzgeD7xgvZ+I8M1e0fL0ptn/M47lianzth0I= +github.com/aws/aws-sdk-go-v2 v1.39.2/go.mod h1:sDioUELIUO9Znk23YVmIk86/9DOpkbyyVb1i/gUNFXY= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.1 h1:i8p8P4diljCr60PpJp6qZXNlgX4m2yQFpYk+9ZT+J4E= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.1/go.mod h1:ddqbooRZYNoJ2dsTwOty16rM+/Aqmk/GOXrK8cg7V00= +github.com/aws/aws-sdk-go-v2/config v1.31.11 h1:6QOO1mP0MgytbfKsL/r/gE1P6/c/4pPzrrU3hKxa5fs= +github.com/aws/aws-sdk-go-v2/config v1.31.11/go.mod h1:KzpDsPX/dLxaUzoqM3sN2NOhbQIW4HW/0W8rQA1YFEs= +github.com/aws/aws-sdk-go-v2/credentials v1.18.15 h1:Gqy7/05KEfUSulSvwxnB7t8DuZMR3ShzNcwmTD6HOLU= +github.com/aws/aws-sdk-go-v2/credentials v1.18.15/go.mod h1:VWDWSRpYHjcjURRaQ7NUzgeKFN8Iv31+EOMT/W+bFyc= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.9 h1:Mv4Bc0mWmv6oDuSWTKnk+wgeqPL5DRFu5bQL9BGPQ8Y= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.9/go.mod h1:IKlKfRppK2a1y0gy1yH6zD+yX5uplJ6UuPlgd48dJiQ= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.9 h1:se2vOWGD3dWQUtfn4wEjRQJb1HK1XsNIt825gskZ970= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.9/go.mod h1:hijCGH2VfbZQxqCDN7bwz/4dzxV+hkyhjawAtdPWKZA= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.9 h1:6RBnKZLkJM4hQ+kN6E7yWFveOTg8NLPHAkqrs4ZPlTU= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.9/go.mod h1:V9rQKRmK7AWuEsOMnHzKj8WyrIir1yUJbZxDuZLFvXI= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 h1:bIqFDwgGXXN1Kpp99pDOdKMTTb5d2KyU5X/BZxjOkRo= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3/go.mod h1:H5O/EsxDWyU+LP/V8i5sm8cxoZgc2fdNR9bxlOFrQTo= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.9 h1:w9LnHqTq8MEdlnyhV4Bwfizd65lfNCNgdlNC6mM5paE= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.9/go.mod h1:LGEP6EK4nj+bwWNdrvX/FnDTFowdBNwcSPuZu/ouFys= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.1 h1:oegbebPEMA/1Jny7kvwejowCaHz1FWZAQ94WXFNCyTM= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.1/go.mod h1:kemo5Myr9ac0U9JfSjMo9yHLtw+pECEHsFtJ9tqCEI8= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.8.9 h1:by3nYZLR9l8bUH7kgaMU4dJgYFjyRdFEfORlDpPILB4= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.8.9/go.mod h1:IWjQYlqw4EX9jw2g3qnEPPWvCE6bS8fKzhMed1OK7c8= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.9 h1:5r34CgVOD4WZudeEKZ9/iKpiT6cM1JyEROpXjOcdWv8= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.9/go.mod h1:dB12CEbNWPbzO2uC6QSWHteqOg4JfBVJOojbAoAUb5I= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.9 h1:wuZ5uW2uhJR63zwNlqWH2W4aL4ZjeJP3o92/W+odDY4= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.9/go.mod h1:/G58M2fGszCrOzvJUkDdY8O9kycodunH4VdT5oBAqls= +github.com/aws/aws-sdk-go-v2/service/s3 v1.88.3 h1:P18I4ipbk+b/3dZNq5YYh+Hq6XC0vp5RWkLp1tJldDA= +github.com/aws/aws-sdk-go-v2/service/s3 v1.88.3/go.mod h1:Rm3gw2Jov6e6kDuamDvyIlZJDMYk97VeCZ82wz/mVZ0= +github.com/aws/aws-sdk-go-v2/service/sso v1.29.5 h1:WwL5YLHabIBuAlEKRoLgqLz1LxTvCEpwsQr7MiW/vnM= +github.com/aws/aws-sdk-go-v2/service/sso v1.29.5/go.mod h1:5PfYspyCU5Vw1wNPsxi15LZovOnULudOQuVxphSflQA= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.1 h1:5fm5RTONng73/QA73LhCNR7UT9RpFH3hR6HWL6bIgVY= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.1/go.mod h1:xBEjWD13h+6nq+z4AkqSfSvqRKFgDIQeaMguAJndOWo= +github.com/aws/aws-sdk-go-v2/service/sts v1.38.6 h1:p3jIvqYwUZgu/XYeI48bJxOhvm47hZb5HUQ0tn6Q9kA= +github.com/aws/aws-sdk-go-v2/service/sts v1.38.6/go.mod h1:WtKK+ppze5yKPkZ0XwqIVWD4beCwv056ZbPQNoeHqM8= +github.com/aws/smithy-go v1.23.0 h1:8n6I3gXzWJB2DxBDnfxgBaSX6oe0d/t10qGz7OKqMCE= +github.com/aws/smithy-go v1.23.0/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= +github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= +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/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM= +github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/readline v1.5.1 h1:upd/6fQk4src78LMRzh5vItIt361/o4uq553V8B5sGI= +github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04= +github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/containerd/cgroups/v3 v3.0.5 h1:44na7Ud+VwyE7LIoJ8JTNQOa549a8543BmzaJHo6Bzo= +github.com/containerd/cgroups/v3 v3.0.5/go.mod h1:SA5DLYnXO8pTGYiAHXz94qvLQTKfVM5GEVisn4jpins= +github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= +github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= +github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= +github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= +github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= +github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= +github.com/containerd/platforms v1.0.0-rc.1 h1:83KIq4yy1erSRgOVHNk1HYdPvzdJ5CnsWaRoJX4C41E= +github.com/containerd/platforms v1.0.0-rc.1/go.mod h1:J71L7B+aiM5SdIEqmd9wp6THLVRzJGXfNuWCZCllLA4= +github.com/containerd/stargz-snapshotter/estargz v0.17.0 h1:+TyQIsR/zSFI1Rm31EQBwpAA1ovYgIKHy7kctL3sLcE= +github.com/containerd/stargz-snapshotter/estargz v0.17.0/go.mod h1:s06tWAiJcXQo9/8AReBCIo/QxcXFZ2n4qfsRnpl71SM= +github.com/containerd/typeurl/v2 v2.2.3 h1:yNA/94zxWdvYACdYO8zofhrTVuQY73fFU1y++dYSw40= +github.com/containerd/typeurl/v2 v2.2.3/go.mod h1:95ljDnPfD3bAbDJRugOiShd/DlAAsxGtUBhJxIn7SCk= +github.com/containernetworking/cni v1.3.0 h1:v6EpN8RznAZj9765HhXQrtXgX+ECGebEYEmnuFjskwo= +github.com/containernetworking/cni v1.3.0/go.mod h1:Bs8glZjjFfGPHMw6hQu82RUgEPNGEaBb9KS5KtNMnJ4= +github.com/containernetworking/plugins v1.7.1 h1:CNAR0jviDj6FS5Vg85NTgKWLDzZPfi/lj+VJfhMDTIs= +github.com/containernetworking/plugins v1.7.1/go.mod h1:xuMdjuio+a1oVQsHKjr/mgzuZ24leAsqUYRnzGoXHy0= +github.com/containers/buildah v1.41.4 h1:IHYWex7rwhsOwtRXQ+VMEQr96gUbSbSvxJcX6AoiDeA= +github.com/containers/buildah v1.41.4/go.mod h1:IFW8MbAgXYiUBCcAFExlHkPfE41DJWVBCbDZWZ9WEng= +github.com/containers/common v0.64.2 h1:1xepE7QwQggUXxmyQ1Dbh6Cn0yd7ktk14sN3McSWf5I= +github.com/containers/common v0.64.2/go.mod h1:o29GfYy4tefUuShm8mOn2AiL5Mpzdio+viHI7n24KJ4= +github.com/containers/image/v5 v5.36.2 h1:GcxYQyAHRF/pLqR4p4RpvKllnNL8mOBn0eZnqJbfTwk= +github.com/containers/image/v5 v5.36.2/go.mod h1:b4GMKH2z/5t6/09utbse2ZiLK/c72GuGLFdp7K69eA4= +github.com/containers/libtrust v0.0.0-20230121012942-c1716e8a8d01 h1:Qzk5C6cYglewc+UyGf6lc8Mj2UaPTHy/iF2De0/77CA= +github.com/containers/libtrust v0.0.0-20230121012942-c1716e8a8d01/go.mod h1:9rfv8iPl1ZP7aqh9YA68wnZv2NUDbXdcdPHVz0pFbPY= +github.com/containers/luksy v0.0.0-20250609192159-bc60f96d4194 h1:mm+XFgCXPx3pFFkFJ0CH6KgX1os5jfrD/T6S/6ht4FE= +github.com/containers/luksy v0.0.0-20250609192159-bc60f96d4194/go.mod h1:ab2XWZtMgybWBznSwo8BEPeIeSpspKh+wlnkq/UY2Uo= +github.com/containers/ocicrypt v1.2.1 h1:0qIOTT9DoYwcKmxSt8QJt+VzMY18onl9jUXsxpVhSmM= +github.com/containers/ocicrypt v1.2.1/go.mod h1:aD0AAqfMp0MtwqWgHM1bUwe1anx0VazI108CRrSKINQ= +github.com/containers/storage v1.59.1 h1:11Zu68MXsEQGBBd+GadPrHPpWeqjKS8hJDGiAHgIqDs= +github.com/containers/storage v1.59.1/go.mod h1:KoAYHnAjP3/cTsRS+mmWZGkufSY2GACiKQ4V3ZLQnR0= +github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= +github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= +github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= +github.com/cyberphone/json-canonicalization v0.0.0-20241213102144-19d51d7fe467 h1:uX1JmpONuD549D73r6cgnxyUu18Zb7yHAy5AYU0Pm4Q= +github.com/cyberphone/json-canonicalization v0.0.0-20241213102144-19d51d7fe467/go.mod h1:uzvlm1mxhHkdfqitSA92i7Se+S9ksOn3a3qmv/kyOCw= +github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s= +github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/disiqueira/gotree/v3 v3.0.2 h1:ik5iuLQQoufZBNPY518dXhiO5056hyNBIK9lWhkNRq8= +github.com/disiqueira/gotree/v3 v3.0.2/go.mod h1:ZuyjE4+mUQZlbpkI24AmruZKhg3VHEgPLDY8Qk+uUu8= +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 v28.3.2+incompatible h1:mOt9fcLE7zaACbxW1GeS65RI67wIJrTnqS3hP2huFsY= +github.com/docker/cli v28.3.2+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk= +github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= +github.com/docker/docker v28.3.3+incompatible h1:Dypm25kh4rmk49v1eiVbsAtpAsYURjYkaKubwuBdxEI= +github.com/docker/docker v28.3.3+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.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= +github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= +github.com/docker/go-metrics v0.0.1 h1:AgB/0SvBxihN0X8OR4SjsblXkbMvalQ8cjmtKQ2rQV8= +github.com/docker/go-metrics v0.0.1/go.mod h1:cG1hvH2utMXtqgqqYE9plW6lDxS3/5ayHzueweSI3Vw= +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/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/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +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/fsouza/go-dockerclient v1.12.1 h1:FMoLq+Zhv9Oz/rFmu6JWkImfr6CBgZOPcL+bHW4gS0o= +github.com/fsouza/go-dockerclient v1.12.1/go.mod h1:OqsgJJcpCwqyM3JED7TdfM9QVWS5O7jSYwXxYKmOooY= +github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE= +github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= +github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= +github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U= +github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= +github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +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-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= +github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= +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.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.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +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.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +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.20.3 h1:oNx7IdTI936V8CQRveCjaxOiegWwvM7kqkbXTpyiovI= +github.com/google/go-containerregistry v0.20.3/go.mod h1:w00pIgBRDVUDFM6bq+Qx8lwNWK+cxgCuX1vd3PIBDNI= +github.com/google/go-intervals v0.0.2 h1:FGrVEiUnTRKR8yE04qzXYaJMtnIYqobR5QbblK3ixcM= +github.com/google/go-intervals v0.0.2/go.mod h1:MkaR3LNRfeKLPmqgJYs4E66z5InYjmCjbbr4TQlcT6Y= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8= +github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +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/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= +github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1 h1:e9Rjr40Z98/clHv5Yg79Is0NtosR5LXRvdr7o/6NwbA= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1/go.mod h1:tIxuGz/9mpox++sgp9fJjHO0+q1X9/UOWd798aAm22M= +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= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8= +github.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg= +github.com/jmhodges/clock v1.2.0 h1:eq4kys+NI0PLngzaHEe7AmPT90XMGIEySD1JfV1PDIs= +github.com/jmhodges/clock v1.2.0/go.mod h1:qKjhA7x7u/lQpPB1XAqX1b1lCI/w3/fNuYpI/ZjLynI= +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/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.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU= +github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs= +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/letsencrypt/boulder v0.0.0-20240620165639-de9c06129bec h1:2tTW6cDth2TSgRbAhD7yjZzTQmcN25sDRPEeinR51yQ= +github.com/letsencrypt/boulder v0.0.0-20240620165639-de9c06129bec/go.mod h1:TmwEoGCwIti7BCeJ9hescZgRtatxRE+A72pCoPfmcfk= +github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA= +github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg= +github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-shellwords v1.0.12 h1:M2zGm7EW6UQJvDeQxo4T51eKPurbeFbe8WtebGE2xrk= +github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y= +github.com/mattn/go-sqlite3 v1.14.28 h1:ThEiQrnbtumT+QMknw63Befp/ce/nUPgBPMlRFEum7A= +github.com/mattn/go-sqlite3 v1.14.28/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/miekg/pkcs11 v1.1.1 h1:Ugu9pdy6vAYku5DEpVWVFPYnzV+bxB+iRdbuFSu7TvU= +github.com/miekg/pkcs11 v1.1.1/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs= +github.com/mistifyio/go-zfs/v3 v3.0.1 h1:YaoXgBePoMA12+S1u/ddkv+QqxcfiZK4prI6HPnkFiU= +github.com/mistifyio/go-zfs/v3 v3.0.1/go.mod h1:CzVgeB0RvF2EGzQnytKVvVSDwmKJXxkOTUGbNrTja/k= +github.com/moby/buildkit v0.23.2 h1:gt/dkfcpgTXKx+B9I310kV767hhVqTvEyxGgI3mqsGQ= +github.com/moby/buildkit v0.23.2/go.mod h1:iEjAfPQKIuO+8y6OcInInvzqTMiKMbb2RdJz1K/95a0= +github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= +github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= +github.com/moby/go-archive v0.1.0 h1:Kk/5rdW/g+H8NHdJW2gsXyZ7UnzvJNOy6VKJqueWdcQ= +github.com/moby/go-archive v0.1.0/go.mod h1:G9B+YoujNohJmrIYFBpSd54GTUB4lt9S+xVQvsJyFuo= +github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk= +github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= +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/capability v0.4.0 h1:4D4mI6KlNtWMCM1Z/K0i7RV1FkX+DBDHKVJpCndZoHk= +github.com/moby/sys/capability v0.4.0/go.mod h1:4g9IK291rVkms3LKCDOoYlnV8xKwoDTpIrNEE35Wq0I= +github.com/moby/sys/mountinfo v0.7.2 h1:1shs6aH5s4o5H2zQLn796ADW1wMrIwHsyJ2v9KouLrg= +github.com/moby/sys/mountinfo v0.7.2/go.mod h1:1YOa8w8Ih7uW0wALDUgT1dTTSBrZ+HiBLGws92L2RU4= +github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU= +github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko= +github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs= +github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs= +github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g= +github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28= +github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ= +github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc= +github.com/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 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= +github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/onsi/ginkgo/v2 v2.23.4 h1:ktYTpKJAVZnDT4VjxSbiBenUjmlL/5QkBEocaWXiQus= +github.com/onsi/ginkgo/v2 v2.23.4/go.mod h1:Bt66ApGPBFzHyR+JO10Zbt0Gsp4uWxu5mIOTusL46e8= +github.com/onsi/gomega v1.37.0 h1:CdEG8g0S133B4OswTDC/5XPSzE1OeP29QOioj2PID2Y= +github.com/onsi/gomega v1.37.0/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0= +github.com/opencontainers/cgroups v0.0.4 h1:XVj8P/IHVms/j+7eh8ggdkTLAxjz84ZzuFyGoE28DR4= +github.com/opencontainers/cgroups v0.0.4/go.mod h1:s8lktyhlGUqM7OSRL5P7eAW6Wb+kWPNvt4qvVfzA5vs= +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/opencontainers/runc v1.3.0 h1:cvP7xbEvD0QQAs0nZKLzkVog2OPZhI/V2w3WmTmUSXI= +github.com/opencontainers/runc v1.3.0/go.mod h1:9wbWt42gV+KRxKRVVugNP6D5+PQciRbenB4fLVsqGPs= +github.com/opencontainers/runtime-spec v1.2.1 h1:S4k4ryNgEpxW1dzyqffOmhI1BHYcjzU8lpJfSlR0xww= +github.com/opencontainers/runtime-spec v1.2.1/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= +github.com/opencontainers/runtime-tools v0.9.1-0.20250523060157-0ea5ed0382a2 h1:2xZEHOdeQBV6PW8ZtimN863bIOl7OCW/X10K0cnxKeA= +github.com/opencontainers/runtime-tools v0.9.1-0.20250523060157-0ea5ed0382a2/go.mod h1:MXdPzqAA8pHC58USHqNCSjyLnRQ6D+NjbpP+02Z1U/0= +github.com/opencontainers/selinux v1.12.0 h1:6n5JV4Cf+4y0KNXW48TLj5DwfXpvWlxXplUkdTrmPb8= +github.com/opencontainers/selinux v1.12.0/go.mod h1:BTPX+bjVbWGXw7ZZWUbdENt8w0htPSrlgOOysQaU62U= +github.com/openshift/imagebuilder v1.2.16 h1:Vqjy5uPoVDJiX5JUKHo0Cf440ih5cKI7lVe2ZJ2X+RA= +github.com/openshift/imagebuilder v1.2.16/go.mod h1:gASl6jikVG3bCFnLjG6Ow5TeKwKVvrqUUj8C7EUmqc8= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo= +github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/proglottis/gpgme v0.1.4 h1:3nE7YNA70o2aLjcg63tXMOhPD7bplfE5CBdV+hLAm2M= +github.com/proglottis/gpgme v0.1.4/go.mod h1:5LoXMgpE4bttgwwdv9bLs/vwqv3qV7F4glEEZ7mRKrM= +github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q= +github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= +github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io= +github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I= +github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= +github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= +github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 h1:KRzFb2m7YtdldCEkzs6KqmJw4nqEVZGK7IN2kJkjTuQ= +github.com/santhosh-tekuri/jsonschema/v6 v6.0.2/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU= +github.com/sebdah/goldie/v2 v2.5.5 h1:rx1mwF95RxZ3/83sdS4Yp7t2C5TCokvWP4TBRbAyEWY= +github.com/sebdah/goldie/v2 v2.5.5/go.mod h1:oZ9fp0+se1eapSRjfYbsV/0Hqhbuu3bJVvKI/NNtssI= +github.com/seccomp/libseccomp-golang v0.11.0 h1:SDkcBRqGLP+sezmMACkxO1EfgbghxIxnRKfd6mHUEis= +github.com/seccomp/libseccomp-golang v0.11.0/go.mod h1:5m1Lk8E9OwgZTTVz4bBOer7JuazaBa+xTkM895tDiWc= +github.com/secure-systems-lab/go-securesystemslib v0.9.0 h1:rf1HIbL64nUpEIZnjLZ3mcNEL9NBPB0iuVjyxvq3LZc= +github.com/secure-systems-lab/go-securesystemslib v0.9.0/go.mod h1:DVHKMcZ+V4/woA/peqr+L0joiRXbPpQ042GgJckkFgw= +github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= +github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= +github.com/sigstore/fulcio v1.6.6 h1:XaMYX6TNT+8n7Npe8D94nyZ7/ERjEsNGFC+REdi/wzw= +github.com/sigstore/fulcio v1.6.6/go.mod h1:BhQ22lwaebDgIxVBEYOOqLRcN5+xOV+C9bh/GUXRhOk= +github.com/sigstore/protobuf-specs v0.4.1 h1:5SsMqZbdkcO/DNHudaxuCUEjj6x29tS2Xby1BxGU7Zc= +github.com/sigstore/protobuf-specs v0.4.1/go.mod h1:+gXR+38nIa2oEupqDdzg4qSBT0Os+sP7oYv6alWewWc= +github.com/sigstore/sigstore v1.9.5 h1:Wm1LT9yF4LhQdEMy5A2JeGRHTrAWGjT3ubE5JUSrGVU= +github.com/sigstore/sigstore v1.9.5/go.mod h1:VtxgvGqCmEZN9X2zhFSOkfXxvKUjpy8RpUW39oCtoII= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/smallstep/pkcs7 v0.1.1 h1:x+rPdt2W088V9Vkjho4KtoggyktZJlMduZAtRHm68LU= +github.com/smallstep/pkcs7 v0.1.1/go.mod h1:dL6j5AIz9GHjVEBTXtW+QliALcgM19RtXaTeyxI+AfA= +github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= +github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= +github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= +github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stefanberger/go-pkcs11uri v0.0.0-20230803200340-78284954bff6 h1:pnnLyeX7o/5aX8qUQ69P/mLojDqwda8hFOCBTmP/6hw= +github.com/stefanberger/go-pkcs11uri v0.0.0-20230803200340-78284954bff6/go.mod h1:39R/xuhNgVhi+K0/zst4TLrJrVmbm6LVgl4A0+ZFS5M= +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.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.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/sylabs/sif/v2 v2.21.1 h1:GZ0b5//AFAqJEChd8wHV/uSKx/l1iuGYwjR8nx+4wPI= +github.com/sylabs/sif/v2 v2.21.1/go.mod h1:YoqEGQnb5x/ItV653bawXHZJOXQaEWpGwHsSD3YePJI= +github.com/tchap/go-patricia/v2 v2.3.3 h1:xfNEsODumaEcCcY3gI0hYPZ/PcpVv5ju6RMAhgwZDDc= +github.com/tchap/go-patricia/v2 v2.3.3/go.mod h1:VZRHKAb53DLaG+nA9EaYYiaEx6YztwDlLElMsnSHD4k= +github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399 h1:e/5i7d4oYZ+C1wj2THlRK+oAhjeS/TRQwMfkIuet3w0= +github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399/go.mod h1:LdwHTNJT99C5fTAzDz0ud328OgXz+gierycbcIx2fRs= +github.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY= +github.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= +github.com/vbatts/tar-split v0.12.1 h1:CqKoORW7BUWBe7UL/iqTVvkTBOF8UvOMKOIZykxnnbo= +github.com/vbatts/tar-split v0.12.1/go.mod h1:eF6B6i6ftWQcDqEn3/iGFRFRo8cBIMSJVOpnNdfTMFA= +github.com/vbauerster/mpb/v8 v8.10.2 h1:2uBykSHAYHekE11YvJhKxYmLATKHAGorZwFlyNw4hHM= +github.com/vbauerster/mpb/v8 v8.10.2/go.mod h1:+Ja4P92E3/CorSZgfDtK46D7AVbDqmBQRTmyTqPElo0= +github.com/vishvananda/netlink v1.3.1 h1:3AEMt62VKqz90r0tmNhog0r/PpWKmrEShJU0wJW6bV0= +github.com/vishvananda/netlink v1.3.1/go.mod h1:ARtKouGSTGchR8aMwmkzC0qiNPrrWO5JS/XMVl45+b4= +github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zdEY= +github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM= +github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo= +github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= +github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= +github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +go.etcd.io/bbolt v1.4.2 h1:IrUHp260R8c+zYx/Tm8QZr04CX+qWS5PGfPdevhdm1I= +go.etcd.io/bbolt v1.4.2/go.mod h1:Is8rSHO/b4f3XigBC0lL0+4FwAQv3HXEEIgFMuKHceM= +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.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 h1:sbiXRNDSWJOTobXh5HyQKjq6wUC5tNybqjIqDpAY4CU= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0/go.mod h1:69uWxva0WgAA/4bu2Yy70SLDBwZXuQ6PbBpbsa5iZrQ= +go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= +go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 h1:1fTNlAIJZGWLP5FVu0fikVry1IsiUnXjf7QFvoNN3Xw= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0/go.mod h1:zjPK58DtkqQFn+YUMbx0M2XV3QgKU0gS9LeGohREyK4= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0 h1:xJ2qHD0C1BeYVTLLR9sX12+Qb95kfeD/byKj6Ky1pXg= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0/go.mod h1:u5BF1xyjstDowA1R5QAO9JHzqK+ublenEW/dyqTjBVk= +go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M= +go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE= +go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY= +go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg= +go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o= +go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w= +go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= +go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= +go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4= +go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4= +go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs= +go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8= +go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= +go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= +go.yaml.in/yaml/v3 v3.0.3 h1:bXOww4E/J3f66rav3pX3m8w6jDE4knZjGOw8b5Y6iNE= +go.yaml.in/yaml/v3 v3.0.3/go.mod h1:tBHosrYAkRZjRAOREWbDnBXUf08JOwYq++0QNwQiWzI= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= +golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= +golang.org/x/crypto v0.30.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= +golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM= +golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +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.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w= +golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= +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-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-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= +golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= +golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +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-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= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= +golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/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-20210615035016-665e8c7367d1/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-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= +golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= +golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= +golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= +golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= +golang.org/x/term v0.33.0 h1:NuFncQrRcaRvVmgRkvM3j/F00gWIAlcmlB8ACEKmGIg= +golang.org/x/term v0.33.0/go.mod h1:s18+ql9tYWp1IfpV9DmCtQDDSRBUjKaw9M1eAv5UeF0= +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.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4= +golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU= +golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= +golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +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.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= +golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo= +golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg= +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= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +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-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb h1:p31xT4yrYrSM/G4Sn2+TNUkVhFCbG9y8itM2S6Th950= +google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:jbe3Bkdp+Dh2IrslsFCklNhweNTBgSYanP1UXhJDhKg= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250313205543-e70fdf4c4cb4 h1:iK2jbkWL86DXjEx0qiHcRE9dE4/Ahua5k6V8OWFb//c= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250313205543-e70fdf4c4cb4/go.mod h1:LuRYeWDFV6WOn90g357N17oMCaxpgCnbi/44qJvDn2I= +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.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +google.golang.org/grpc v1.72.2 h1:TdbGzwb82ty4OusHWepvFWGLgIbNo1/SUynEN0ssqv8= +google.golang.org/grpc v1.72.2/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +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.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= +google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= +gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= +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= +sigs.k8s.io/yaml v1.5.0 h1:M10b2U7aEUY6hRtU870n2VTPgR5RZiL/I6Lcc2F4NUQ= +sigs.k8s.io/yaml v1.5.0/go.mod h1:wZs27Rbxoai4C0f8/9urLZtZtF3avA3gKvGyPdDqTO4= +tags.cncf.io/container-device-interface v1.0.1 h1:KqQDr4vIlxwfYh0Ed/uJGVgX+CHAkahrgabg6Q8GYxc= +tags.cncf.io/container-device-interface v1.0.1/go.mod h1:JojJIOeW3hNbcnOH2q0NrWNha/JuHoDZcmYxAZwb2i0= +tags.cncf.io/container-device-interface/specs-go v1.0.0 h1:8gLw29hH1ZQP9K1YtAzpvkHCjjyIxHZYzBAvlQ+0vD8= +tags.cncf.io/container-device-interface/specs-go v1.0.0/go.mod h1:u86hoFWqnh3hWz3esofRFKbI261bUlvUfLKGrDhJkgQ= diff --git a/go/pkg/arguments/arguments.go b/go/pkg/arguments/arguments.go new file mode 100644 index 0000000..b569cda --- /dev/null +++ b/go/pkg/arguments/arguments.go @@ -0,0 +1,131 @@ +package arguments + +import ( + "flag" + "fmt" + "strings" + + "github.com/OpenCHAMI/image-builder/go/pkg/config" +) + +// ParseCommandLine parses command line arguments +func ParseCommandLine() (*config.CLIArgs, error) { + var args config.CLIArgs + + // Define command line flags + logLevel := flag.String("log-level", "info", "Logging level") + config := flag.String("config", "config.yaml", "Configuration file") + layerType := flag.String("layer-type", "", "Layer type (base, ansible)") + pkgMan := flag.String("pkg-manager", "", "Package manager (dnf, zypper)") + groupList := flag.String("groups", "", "Ansible group list (comma separated)") + playbooks := flag.String("pb", "", "Ansible playbooks (comma separated)") + inventory := flag.String("inventory", "", "Ansible inventory") + vars := flag.String("vars", "", "Ansible variables (key=value,key2=value2)") + ansibleVerbosity := flag.Int("ansible-verbosity", 0, "Ansible verbosity (0-4)") + name := flag.String("name", "image", "Image name") + parent := flag.String("parent", "", "Parent image") + registryOptsPull := flag.String("registry-opts-pull", "", "Registry options for pull (comma separated)") + registryOptsPush := flag.String("registry-opts-push", "", "Registry options for push (comma separated)") + + // Publishing options + proxy := flag.String("proxy", "", "HTTP proxy for network operations") + publishS3 := flag.String("publish-s3", "", "S3 publishing destination") + publishRegistry := flag.String("publish-registry", "", "Registry publishing destination") + publishLocal := flag.Bool("publish-local", false, "Keep image in local storage only, don't push to registry") + publishTags := flag.String("publish-tags", "", "Tags for published images (comma separated)") + s3Prefix := flag.String("s3-prefix", "", "S3 key prefix") + s3Bucket := flag.String("s3-bucket", "", "S3 bucket name") + + // Repository options + gpgCheck := flag.Bool("gpgcheck", true, "Enable GPG signature checking") + + // Security scanning options + scapBenchmark := flag.Bool("scap-benchmark", false, "Enable SCAP security benchmarking") + ovalEval := flag.Bool("oval-eval", false, "Enable OVAL security evaluation") + installScap := flag.Bool("install-scap", false, "Install SCAP tools") + + flag.Parse() + + // Handle positional arguments - if a config file is passed as first positional arg, use it + if flag.NArg() > 0 { + *config = flag.Arg(0) + } + + // Process arguments + args.LogLevel = *logLevel + args.ConfigFile = *config + args.LayerType = *layerType + args.PackageManager = *pkgMan + args.Name = *name + args.Parent = *parent + args.AnsibleVerbosity = *ansibleVerbosity + + // Process lists + if *groupList != "" { + args.AnsibleGroups = strings.Split(*groupList, ",") + } + + if *playbooks != "" { + args.AnsiblePlaybook = strings.Split(*playbooks, ",") + } + + if *registryOptsPull != "" { + args.RegistryOptsPull = strings.Split(*registryOptsPull, ",") + } + + if *registryOptsPush != "" { + args.RegistryOptsPush = strings.Split(*registryOptsPush, ",") + } + + args.AnsibleInv = *inventory + + // Process vars + args.AnsibleVars = make(map[string]string) + if *vars != "" { + for _, pair := range strings.Split(*vars, ",") { + kv := strings.SplitN(pair, "=", 2) + if len(kv) == 2 { + args.AnsibleVars[kv[0]] = kv[1] + } + } + } + + // Process publishing options + args.Proxy = *proxy + args.PublishS3 = *publishS3 + args.PublishRegistry = *publishRegistry + args.PublishLocal = *publishLocal + args.S3Prefix = *s3Prefix + args.S3Bucket = *s3Bucket + + if *publishTags != "" { + args.PublishTags = strings.Split(*publishTags, ",") + } + + // Process repository options + args.GPGCheck = *gpgCheck + + // Process security scanning options + args.ScapBenchmark = *scapBenchmark + args.OvalEval = *ovalEval + args.InstallScap = *installScap + + return &args, validateArgs(&args) +} + +// validateArgs performs basic validation on the provided arguments +func validateArgs(args *config.CLIArgs) error { + // Check for unimplemented SCAP security scanning options + if args.ScapBenchmark { + return fmt.Errorf("--scap-benchmark is not currently implemented") + } + if args.OvalEval { + return fmt.Errorf("--oval-eval is not currently implemented") + } + if args.InstallScap { + return fmt.Errorf("--install-scap is not currently implemented") + } + + // Add other validation logic as needed + return nil +} diff --git a/go/pkg/config/config.go b/go/pkg/config/config.go new file mode 100644 index 0000000..1bd02db --- /dev/null +++ b/go/pkg/config/config.go @@ -0,0 +1,377 @@ +package config + +import ( + "fmt" + "os" + "path/filepath" + "strings" + + "gopkg.in/yaml.v3" +) + +// RuntimeArgs represents runtime and CLI-specific configuration +type RuntimeArgs struct { + LogLevel string + ConfigFile string + VerboseLevel int + Debug bool +} + +// ImageArgs represents core image configuration options +type ImageArgs struct { + Name string + Parent string + LayerType string + PackageManager string +} + +// AnsibleArgs represents Ansible configuration options +type AnsibleArgs struct { + AnsibleGroups []string + AnsiblePlaybook []string + AnsibleInv string + AnsibleVars map[string]string + AnsibleVerbosity int +} + +// RegistryArgs represents container registry configuration options +type RegistryArgs struct { + RegistryOptsPull []string + RegistryOptsPush []string +} + +// PackageManagerArgs represents package manager configuration options +type PackageManagerArgs struct { + Proxy string + GPGCheck bool +} + +// S3Args represents S3 storage configuration options +type S3Args struct { + S3Prefix string + S3Bucket string +} + +// PublishingArgs represents publishing and distribution options +type PublishingArgs struct { + PublishS3 string + PublishRegistry string + PublishLocal bool + PublishTags []string +} + +// SecurityArgs represents security scanning options +type SecurityArgs struct { + ScapBenchmark bool + OvalEval bool + InstallScap bool +} + +// CLIArgs represents command-line arguments that can override YAML configuration +type CLIArgs struct { + RuntimeArgs + ImageArgs + AnsibleArgs + RegistryArgs + PackageManagerArgs + S3Args + PublishingArgs + SecurityArgs +} + +// Options represents the legacy options section in YAML files +type Options struct { + Name string `yaml:"name"` + Parent string `yaml:"parent"` + LayerType string `yaml:"layer_type"` + PackageManager string `yaml:"pkg_manager"` + + // Publishing options + PublishLocal bool `yaml:"publish_local"` + PublishS3 string `yaml:"publish_s3"` + PublishRegistry string `yaml:"publish_registry"` + S3Bucket string `yaml:"s3_bucket"` + S3Prefix string `yaml:"s3_prefix"` + PublishTags string `yaml:"publish_tags"` + + // Repository options + Proxy string `yaml:"proxy"` + GPGCheck bool `yaml:"gpgcheck"` + + // Ansible options + Groups []string `yaml:"groups"` + Playbooks string `yaml:"playbooks"` + Inventory string `yaml:"inventory"` + Vars map[string]string `yaml:"vars"` + + // Security scanning options + ScapBenchmark bool `yaml:"scap_benchmark"` + OvalEval bool `yaml:"oval_eval"` + InstallScap bool `yaml:"install_scap"` +} + +// Config represents the unified configuration combining YAML file data and command-line arguments +type Config struct { + // Runtime/CLI options + LogLevel string + ConfigFile string + VerboseLevel int + Debug bool + + // Core image configuration (from YAML, can be overridden by CLI) + Name string `yaml:"name"` + Parent string `yaml:"parent"` + LayerType string `yaml:"layer_type"` + PackageManager string `yaml:"package_manager"` + + // Image build configuration (from YAML) + Options Options `yaml:"options"` + Modules map[string][]string `yaml:"modules"` + Packages []string `yaml:"packages"` + PackageGroups []string `yaml:"package_groups"` + RemovePackages []string `yaml:"remove_packages"` + Commands []Command `yaml:"cmds"` + CopyFiles []CopyFile `yaml:"copyfiles"` + Repositories []Repository `yaml:"repos"` + + // Ansible configuration (from CLI/YAML) + AnsibleGroups []string `yaml:"groups"` + AnsiblePlaybook []string `yaml:"playbooks"` + AnsibleInv string `yaml:"inventory"` + AnsibleVars map[string]string `yaml:"vars"` + AnsibleVerbosity int `yaml:"ansible_verbosity"` + + // Registry configuration (from CLI) + RegistryOptsPull []string + RegistryOptsPush []string + + // Publishing options (from CLI/YAML) + Proxy string `yaml:"proxy"` + PublishS3 string `yaml:"publish_s3"` + PublishRegistry string `yaml:"publish_registry"` + PublishLocal bool `yaml:"publish_local"` + PublishTags []string `yaml:"publish_tags"` + S3Prefix string `yaml:"s3_prefix"` + S3Bucket string `yaml:"s3_bucket"` + + // Repository options (from CLI/YAML) + GPGCheck bool `yaml:"gpgcheck"` + + // Security scanning options (from CLI/YAML) + ScapBenchmark bool `yaml:"scap_benchmark"` + OvalEval bool `yaml:"oval_eval"` + InstallScap bool `yaml:"install_scap"` +} + +// Command represents a command to run during image build +type Command struct { + Cmd string `yaml:"cmd"` +} + +// CopyFile represents a file copy operation +type CopyFile struct { + Src string `yaml:"src"` + Dest string `yaml:"dest"` +} + +// Repository represents a package repository configuration +type Repository struct { + Alias string `yaml:"alias"` + URL string `yaml:"url"` + Priority int `yaml:"priority,omitempty"` + GPG string `yaml:"gpg,omitempty"` +} + +// LoadConfig loads and parses a YAML configuration file into a Config struct +func LoadConfig(yamlFile string) (*Config, error) { + if !filepath.IsAbs(yamlFile) { + cwd, err := os.Getwd() + if err != nil { + return nil, fmt.Errorf("failed to get current directory: %w", err) + } + yamlFile = filepath.Join(cwd, yamlFile) + } + + if _, err := os.Stat(yamlFile); os.IsNotExist(err) { + return nil, fmt.Errorf("config file not found: %s", yamlFile) + } + + data, err := os.ReadFile(yamlFile) + if err != nil { + return nil, fmt.Errorf("failed to read config file: %w", err) + } + + var config Config + if err := yaml.Unmarshal(data, &config); err != nil { + return nil, fmt.Errorf("failed to parse YAML: %w", err) + } + + // Extract values from options section to top-level fields (if top-level fields are empty) + if config.Name == "" && config.Options.Name != "" { + config.Name = config.Options.Name + } + if config.Parent == "" && config.Options.Parent != "" { + config.Parent = config.Options.Parent + } + if config.LayerType == "" && config.Options.LayerType != "" { + config.LayerType = config.Options.LayerType + } + if config.PackageManager == "" && config.Options.PackageManager != "" { + config.PackageManager = config.Options.PackageManager + } + + // Publishing options from options section + if config.PublishS3 == "" && config.Options.PublishS3 != "" { + config.PublishS3 = config.Options.PublishS3 + } + if config.PublishRegistry == "" && config.Options.PublishRegistry != "" { + config.PublishRegistry = config.Options.PublishRegistry + } + if config.S3Bucket == "" && config.Options.S3Bucket != "" { + config.S3Bucket = config.Options.S3Bucket + } + if config.S3Prefix == "" && config.Options.S3Prefix != "" { + config.S3Prefix = config.Options.S3Prefix + } + if len(config.PublishTags) == 0 && config.Options.PublishTags != "" { + for _, tag := range strings.Split(config.Options.PublishTags, ",") { + config.PublishTags = append(config.PublishTags, strings.TrimSpace(tag)) + } + } + // Always use options value for boolean fields (they have proper defaults) + config.PublishLocal = config.Options.PublishLocal + + // Repository options from options section + if config.Proxy == "" && config.Options.Proxy != "" { + config.Proxy = config.Options.Proxy + } + config.GPGCheck = config.Options.GPGCheck + + // Ansible options from options section - force extraction even if already set + // This handles the reexec case where values might get lost + if len(config.Options.Groups) > 0 { + config.AnsibleGroups = make([]string, len(config.Options.Groups)) + copy(config.AnsibleGroups, config.Options.Groups) + } + if config.Options.Playbooks != "" { + config.AnsiblePlaybook = []string{config.Options.Playbooks} + } + if config.Options.Inventory != "" { + config.AnsibleInv = config.Options.Inventory + } + if len(config.Options.Vars) > 0 { + config.AnsibleVars = make(map[string]string) + for k, v := range config.Options.Vars { + config.AnsibleVars[k] = v + } + } + + // Security scanning options from options section + config.ScapBenchmark = config.Options.ScapBenchmark + config.OvalEval = config.Options.OvalEval + config.InstallScap = config.Options.InstallScap + if config.Modules == nil { + config.Modules = make(map[string][]string) + } + if config.Packages == nil { + config.Packages = []string{} + } + if config.PackageGroups == nil { + config.PackageGroups = []string{} + } + if config.RemovePackages == nil { + config.RemovePackages = []string{} + } + if config.Commands == nil { + config.Commands = []Command{} + } + if config.CopyFiles == nil { + config.CopyFiles = []CopyFile{} + } + if config.Repositories == nil { + config.Repositories = []Repository{} + } + if config.AnsibleVars == nil { + config.AnsibleVars = make(map[string]string) + } + + return &config, nil +} + +// MergeCommandLineArgs merges command-line arguments into the config, +// with CLI args taking precedence over YAML values +func (c *Config) MergeCommandLineArgs(cliArgs *CLIArgs) { + // Runtime options (always from CLI) + c.LogLevel = cliArgs.RuntimeArgs.LogLevel + c.ConfigFile = cliArgs.RuntimeArgs.ConfigFile + c.VerboseLevel = cliArgs.RuntimeArgs.VerboseLevel + c.Debug = cliArgs.RuntimeArgs.Debug + + // Core fields - CLI overrides YAML if provided + if cliArgs.ImageArgs.Name != "" && cliArgs.ImageArgs.Name != "image" { // "image" is the default + c.Name = cliArgs.ImageArgs.Name + } + if cliArgs.ImageArgs.Parent != "" { + c.Parent = cliArgs.ImageArgs.Parent + } + if cliArgs.ImageArgs.LayerType != "" { + c.LayerType = cliArgs.ImageArgs.LayerType + } + if cliArgs.ImageArgs.PackageManager != "" { + c.PackageManager = cliArgs.ImageArgs.PackageManager + } + + // Ansible options - CLI overrides YAML if provided + if len(cliArgs.AnsibleArgs.AnsibleGroups) > 0 { + c.AnsibleGroups = cliArgs.AnsibleArgs.AnsibleGroups + } + if len(cliArgs.AnsibleArgs.AnsiblePlaybook) > 0 { + c.AnsiblePlaybook = cliArgs.AnsibleArgs.AnsiblePlaybook + } + if cliArgs.AnsibleArgs.AnsibleInv != "" { + c.AnsibleInv = cliArgs.AnsibleArgs.AnsibleInv + } + if len(cliArgs.AnsibleArgs.AnsibleVars) > 0 { + c.AnsibleVars = cliArgs.AnsibleArgs.AnsibleVars + } + if cliArgs.AnsibleArgs.AnsibleVerbosity > 0 { + c.AnsibleVerbosity = cliArgs.AnsibleArgs.AnsibleVerbosity + } + + // Registry options (always from CLI) + c.RegistryOptsPull = cliArgs.RegistryArgs.RegistryOptsPull + c.RegistryOptsPush = cliArgs.RegistryArgs.RegistryOptsPush + + // Package manager options - CLI overrides YAML if provided + if cliArgs.PackageManagerArgs.Proxy != "" { + c.Proxy = cliArgs.PackageManagerArgs.Proxy + } + c.GPGCheck = cliArgs.PackageManagerArgs.GPGCheck + + // S3 options - CLI overrides YAML if provided + if cliArgs.S3Args.S3Prefix != "" { + c.S3Prefix = cliArgs.S3Args.S3Prefix + } + if cliArgs.S3Args.S3Bucket != "" { + c.S3Bucket = cliArgs.S3Args.S3Bucket + } + + // Publishing options - CLI overrides YAML if provided + if cliArgs.PublishingArgs.PublishS3 != "" { + c.PublishS3 = cliArgs.PublishingArgs.PublishS3 + } + if cliArgs.PublishingArgs.PublishRegistry != "" { + c.PublishRegistry = cliArgs.PublishingArgs.PublishRegistry + } + // PublishLocal defaults to false, so we always use CLI value + c.PublishLocal = cliArgs.PublishingArgs.PublishLocal + if len(cliArgs.PublishingArgs.PublishTags) > 0 { + c.PublishTags = cliArgs.PublishingArgs.PublishTags + } + + // Security scanning options - CLI overrides YAML if provided + // These default to false, so we always use CLI values + c.ScapBenchmark = cliArgs.SecurityArgs.ScapBenchmark + c.OvalEval = cliArgs.SecurityArgs.OvalEval + c.InstallScap = cliArgs.SecurityArgs.InstallScap +} diff --git a/go/pkg/installer/dnf.go b/go/pkg/installer/dnf.go new file mode 100644 index 0000000..7e7b8da --- /dev/null +++ b/go/pkg/installer/dnf.go @@ -0,0 +1,180 @@ +package installer + +import ( + "path/filepath" + + "github.com/OpenCHAMI/image-builder/go/pkg/config" +) + +type Dnf struct { +} + +func (i *Dnf) InstallModulesCommand(command string, modules []string, gpgCheck bool, proxy string) ([]string, error) { + args := []string{"dnf", "-y"} + if proxy != "" { + args = append(args, "--setopt=proxy="+proxy) + } + args = append(args, "module", command) + if !gpgCheck { + args = append(args, "--nogpgcheck") + } + args = append(args, modules...) + + return args, nil +} + +// Command returns the DNF command name +func (i *Dnf) Command() string { + return "dnf" +} + +type DnfScratch struct { + Dnf +} + +// Command returns the DNF command name +func (i *DnfScratch) Command() string { + return i.Dnf.Command() +} + +func NewDnf(scratch bool) (PackageManager, error) { + if scratch { + return &DnfScratch{}, nil + } else { + return &Dnf{}, nil + } +} + +func (i *Dnf) InstallRepoCommand(repo config.Repository, proxy string) ([]string, error) { + args := []string{"dnf", "-y"} + if proxy != "" { + args = append(args, "--setopt=proxy="+proxy) + } + args = append(args, "config-manager", "--save", "--add-repo") + args = append(args, repo.URL) + + return args, nil +} + +func (i *DnfScratch) InstallRepoCommand(repo config.Repository, proxy string) ([]string, error) { + repoDest := "etc/yum.repos.d" + + args, err := i.Dnf.InstallRepoCommand(repo, proxy) + if err != nil { + return nil, err + } + args = append(args, "--setopt=reposdir="+filepath.Join(TARGET_MOUNT_DIR, repoDest)) + + return args, nil +} + +func (i *Dnf) InstallPackagesCommand(packages []string, gpgCheck bool, proxy string) ([]string, error) { + args := []string{"dnf", "-y"} + if proxy != "" { + args = append(args, "--setopt=proxy="+proxy) + } + args = append(args, "install") + if !gpgCheck { + args = append(args, "--nogpgcheck") + } + args = append(args, packages...) + + return args, nil +} + +func (i *DnfScratch) InstallPackagesCommand(packages []string, gpgCheck bool, proxy string) ([]string, error) { + repoDest := "etc/yum.repos.d" + args, err := i.Dnf.InstallPackagesCommand(packages, gpgCheck, proxy) + if err != nil { + return nil, err + } + + args = append(args, "--setopt=reposdir="+filepath.Join(TARGET_MOUNT_DIR, repoDest)) + args = append(args, "--installroot="+TARGET_MOUNT_DIR) + // Force RPM to use sqlite backend instead of bdb_ro to avoid database permission issues + //args = append(args, "--setopt=_db_backend=sqlite") + + return args, nil +} + +func (i *Dnf) InstallGroupsCommand(package_groups []string, gpgCheck bool, proxy string) ([]string, error) { + args := []string{"dnf", "-y"} + if proxy != "" { + args = append(args, "--setopt=proxy="+proxy) + } + args = append(args, "groupinstall") + if !gpgCheck { + args = append(args, "--nogpgcheck") + } + args = append(args, package_groups...) + + return args, nil +} + +func (i *DnfScratch) InstallGroupsCommand(package_groups []string, gpgCheck bool, proxy string) ([]string, error) { + repoDest := "etc/yum.repos.d" + args, err := i.Dnf.InstallGroupsCommand(package_groups, gpgCheck, proxy) + if err != nil { + return nil, err + } + + args = append(args, "--installroot="+TARGET_MOUNT_DIR) + args = append(args, "--setopt=reposdir="+filepath.Join(TARGET_MOUNT_DIR, repoDest)) + // Force RPM to use sqlite backend instead of bdb_ro to avoid database permission issues + args = append(args, "--setopt=_db_backend=sqlite") + + return args, nil +} + +func (i *Dnf) RemovePackagesCommand(packages []string) ([]string, error) { + args := []string{"dnf", "-y", "remove"} + args = append(args, packages...) + + // TODO gpg check option + + return args, nil +} + +func (i *DnfScratch) InstallModulesCommand(command string, modules []string, gpgCheck bool, proxy string) ([]string, error) { + repoDest := "etc/yum.repos.d" + args, err := i.Dnf.InstallModulesCommand(command, modules, gpgCheck, proxy) + if err != nil { + return nil, err + } + + args = append(args, "--installroot="+TARGET_MOUNT_DIR) + args = append(args, "--setopt=reposdir="+filepath.Join(TARGET_MOUNT_DIR, repoDest)) + // Force RPM to use sqlite backend instead of bdb_ro to avoid database permission issues + args = append(args, "--setopt=_db_backend=sqlite") + + return args, nil +} + +func (i *DnfScratch) RemovePackagesCommand(packages []string) ([]string, error) { + repoDest := "etc/yum.repos.d" + args, err := i.Dnf.RemovePackagesCommand(packages) + if err != nil { + return nil, err + } + args = append(args, "--setopt=reposdir="+filepath.Join(TARGET_MOUNT_DIR, repoDest)) + args = append(args, "--installroot="+TARGET_MOUNT_DIR) + // Force RPM to use sqlite backend instead of bdb_ro to avoid database permission issues + args = append(args, "--setopt=_db_backend=sqlite") + + return args, nil +} + +func (i *Dnf) InstallGpgKeyCommand(gpgURL string) ([]string, error) { + args := []string{"rpm", "--import", gpgURL} + return args, nil +} + +func (i *DnfScratch) InstallGpgKeyCommand(gpgURL string) ([]string, error) { + args, err := i.Dnf.InstallGpgKeyCommand(gpgURL) + if err != nil { + return nil, err + } + args = append(args, "--root", TARGET_MOUNT_DIR) + + return args, nil +} diff --git a/go/pkg/installer/installer.go b/go/pkg/installer/installer.go new file mode 100644 index 0000000..f4c5b74 --- /dev/null +++ b/go/pkg/installer/installer.go @@ -0,0 +1,362 @@ +package installer + +import ( + "context" + "fmt" + "log" + "os" + + "github.com/OpenCHAMI/image-builder/go/pkg/config" + "github.com/OpenCHAMI/image-builder/go/pkg/utils" + "github.com/containers/buildah" + "github.com/containers/storage" + "github.com/opencontainers/runtime-spec/specs-go" +) + +const ( + TARGET_MOUNT_DIR = "/mnt/target" +) + +func NewInstaller(targetBuilder *buildah.Builder, pkgMan string, gpgCheck bool, proxy string) (Installer, error) { + scratch := targetBuilder.FromImage == "" + packageManager, err := NewPackageManager(pkgMan, scratch) + if err != nil { + return nil, err + } + + if scratch { + return NewScratchInstaller(targetBuilder, packageManager, gpgCheck, proxy) + } else { + return NewTargetInstaller(targetBuilder, packageManager, gpgCheck, proxy) + } + +} + +type Installer interface { + InstallRepos(repos []config.Repository) error + InstallPackages(packages []string) error + InstallGroups(groups []string) error + InstallModules(modules map[string][]string) error + RemovePackages(packages []string) error + Cleanup() error +} + +// Installer handles package installation and repository management +type TargetInstaller struct { + targetBuilder *buildah.Builder + pkgMan PackageManager + logger *log.Logger + tempDir string + gpgCheck bool + proxy string +} + +type ScratchInstaller struct { + store storage.Store + TargetInstaller + helperBuilder *buildah.Builder + targetMountPoint string +} + +// NewTargetInstaller creates a new installer for the specified package manager +func NewTargetInstaller(builder *buildah.Builder, pkgMan PackageManager, gpgCheck bool, proxy string) (*TargetInstaller, error) { + logger := log.New(os.Stdout, "[INSTALLER]", log.LstdFlags) + + // Create temp directory for package manager logs, cache, etc. + tmpDir, err := os.MkdirTemp("", "image-build-") + if err != nil { + return nil, fmt.Errorf("failed to create temp directory: %w", err) + } + + logger.Printf("Created temp directory: %s", tmpDir) + + // // Ensure /tmp exists in the mount + // if err := utils.CreateDir(filepath.Join(mountPoint, "tmp")); err != nil { + // return nil, fmt.Errorf("failed to create /tmp in container mount: %w", err) + // } + + //logger.Printf("Using mount point: %s", mountPoint) + + logger.Printf("Temporary directory created at %s", tmpDir) + + return &TargetInstaller{ + targetBuilder: builder, + pkgMan: pkgMan, + logger: logger, + tempDir: tmpDir, + gpgCheck: gpgCheck, + proxy: proxy, + }, nil +} + +func (i *TargetInstaller) installPackagesWithBuilder(builder *buildah.Builder, packages []string, mounts []specs.Mount) error { + if len(packages) == 0 { + i.logger.Println("No packages specified to install") + return nil + } + + i.logger.Printf("Installing %d packages", len(packages)) + + args, err := i.pkgMan.InstallPackagesCommand(packages, i.gpgCheck, i.proxy) + if err != nil { + return fmt.Errorf("failed to generate install command: %w", err) + } + + i.logger.Printf("Running command in container: %v", args) + err = utils.RunCommandInBuilder(builder, args, mounts) + if err != nil { + return fmt.Errorf("failed to install packages: %w", err) + } + return nil +} + +func (i *TargetInstaller) InstallPackages(packages []string) error { + return i.installPackagesWithBuilder(i.targetBuilder, packages, nil) +} + +// createTargetMounts creates the standard mount configuration for scratch installer operations +func (i *ScratchInstaller) createTargetMounts() []specs.Mount { + return []specs.Mount{ + { + Destination: TARGET_MOUNT_DIR, + Type: "bind", + Source: i.targetMountPoint, + Options: []string{"rbind", "rw"}, + }, + } +} + +func (i *ScratchInstaller) InstallPackages(packages []string) error { + return i.installPackagesWithBuilder(i.helperBuilder, packages, i.createTargetMounts()) +} + +// InstallGroups installs the specified package groups +func (i *TargetInstaller) installGroups(builder *buildah.Builder, groups []string, mounts []specs.Mount) error { + if len(groups) == 0 { + i.logger.Println("No package groups specified to install") + return nil + } + + i.logger.Printf("Installing %d package groups", len(groups)) + + args, err := i.pkgMan.InstallGroupsCommand(groups, i.gpgCheck, i.proxy) + if err != nil { + return fmt.Errorf("failed to generate install command: %w", err) + } + + i.logger.Printf("Running command in container: %v", args) + err = utils.RunCommandInBuilder(builder, args, mounts) + if err != nil { + return fmt.Errorf("failed to install package groups: %w", err) + } + + return nil +} + +func (i *TargetInstaller) InstallGroups(groups []string) error { + return i.installGroups(i.targetBuilder, groups, nil) +} + +// InstallModules installs the specified modules +func (i *TargetInstaller) installModules(builder *buildah.Builder, modules map[string][]string, mounts []specs.Mount) error { + if len(modules) == 0 { + i.logger.Println("No modules specified to install") + return nil + } + + i.logger.Printf("Installing modules with %d commands", len(modules)) + + for command, moduleList := range modules { + if len(moduleList) == 0 { + continue + } + + i.logger.Printf("Running module command '%s' for modules: %v", command, moduleList) + + args, err := i.pkgMan.InstallModulesCommand(command, moduleList, i.gpgCheck, i.proxy) + if err != nil { + return fmt.Errorf("failed to generate module command: %w", err) + } + + i.logger.Printf("Running command in container: %v", args) + err = utils.RunCommandInBuilder(builder, args, mounts) + if err != nil { + return fmt.Errorf("failed to run module command '%s': %w", command, err) + } + } + + return nil +} + +func (i *TargetInstaller) InstallModules(modules map[string][]string) error { + return i.installModules(i.targetBuilder, modules, nil) +} + +func (i *ScratchInstaller) InstallGroups(groups []string) error { + return i.installGroups(i.helperBuilder, groups, i.createTargetMounts()) +} + +func (i *ScratchInstaller) InstallModules(modules map[string][]string) error { + return i.installModules(i.helperBuilder, modules, i.createTargetMounts()) +} + +func (i *TargetInstaller) installRepos(builder *buildah.Builder, repos []config.Repository, mounts []specs.Mount) error { + if len(repos) == 0 { + i.logger.Printf("No repositories specified to install") + return nil + } + for _, repo := range repos { + i.logger.Printf("Installing repo %s: %s", repo.Alias, repo.URL) + + args, err := i.pkgMan.InstallRepoCommand(repo, i.proxy) + if err != nil { + return fmt.Errorf("failed to generate install repo command: %w", err) + } + + i.logger.Printf("Running command in container: %v", args) + err = utils.RunCommandInBuilder(builder, args, mounts) + if err != nil { + return fmt.Errorf("failed to install repo %s: %w", repo.Alias, err) + } + + // Install GPG key if specified and gpgcheck is enabled + if repo.GPG != "" && i.gpgCheck { + err = i.installGPGKey(builder, repo.GPG, i.proxy, mounts) + if err != nil { + return fmt.Errorf("failed to install GPG key for repo %s: %w", repo.Alias, err) + } + } + } + + return nil +} + +func (i *TargetInstaller) InstallRepos(repos []config.Repository) error { + + return i.installRepos(i.targetBuilder, repos, nil) +} + +func (i *ScratchInstaller) InstallRepos(repos []config.Repository) error { + return i.installRepos(i.helperBuilder, repos, i.createTargetMounts()) +} + +func (i *TargetInstaller) installGPGKey(builder *buildah.Builder, gpgURL string, proxy string, mounts []specs.Mount) error { + i.logger.Printf("Installing GPG key %s", gpgURL) + + args, err := i.pkgMan.InstallGpgKeyCommand(gpgURL) + if err != nil { + return fmt.Errorf("failed to generate GPG key install command: %w", err) + } + + env := []string{} + if proxy != "" { + env = append(env, fmt.Sprintf("http_proxy=%s", proxy)) + env = append(env, fmt.Sprintf("https_proxy=%s", proxy)) + } + + // Run the RPM import command in helper container + err = utils.RunCommandInBuilderWithEnv(builder, args, mounts, env) + if err != nil { + return fmt.Errorf("failed to import GPG key: %w", err) + } + + i.logger.Printf("Successfully imported GPG key from %s", gpgURL) + return nil +} + +func (i *TargetInstaller) Cleanup() error { + if i.tempDir != "" { + i.logger.Printf("Cleaning up temporary directory %s", i.tempDir) + err := os.RemoveAll(i.tempDir) + if err != nil { + return fmt.Errorf("failed to remove temp directory: %v", err) + } + } + + return nil +} + +func (i *ScratchInstaller) Cleanup() error { + if err := i.helperBuilder.Delete(); err != nil { + return fmt.Errorf("failed to delete container: %v", err) + } + + err := i.TargetInstaller.targetBuilder.Unmount() + if err != nil { + return fmt.Errorf("failed to unmount target container: %v", err) + } + + return i.TargetInstaller.Cleanup() +} + +func NewScratchInstaller(targetBuilder *buildah.Builder, pkgMan PackageManager, gpgCheck bool, proxy string) (*ScratchInstaller, error) { + var helperImage string + // TODO can probaly use type switch here + switch pkgMan.Command() { + case "dnf", "yum", "microdnf": + helperImage = "docker.io/cjh1/scratch-image-helper" + case "zypper": + helperImage = "registry.opensuse.org/opensuse/leap:15" + default: + return nil, fmt.Errorf("unsupported package manager for helper container: %s", pkgMan) + } + + store, err := utils.GetStore() + if err != nil { + return nil, fmt.Errorf("failed to get buildah store") + } + + scratchHelperBuilder, err := utils.CreateBuilder(context.Background(), store, helperImage, "scratch-helper") + if err != nil { + return nil, fmt.Errorf("failed to create helper container: %w", err) + } + + // Mount the target container + mountPoint, err := targetBuilder.Mount("") + if err != nil { + return nil, fmt.Errorf("failed to mount container: %w", err) + } + + log.Printf("Container mounted at: %s", mountPoint) + + targetInstaller, err := NewTargetInstaller(targetBuilder, pkgMan, gpgCheck, proxy) + if err != nil { + return nil, fmt.Errorf("failed to create target installer: %w", err) + } + + return &ScratchInstaller{ + store: store, + TargetInstaller: *targetInstaller, + helperBuilder: scratchHelperBuilder, + targetMountPoint: mountPoint, + }, nil +} + +func (i *TargetInstaller) removePackages(builder *buildah.Builder, packages []string, mounts []specs.Mount) error { + if len(packages) == 0 { + i.logger.Println("No packages specified to remove") + return nil + } + + i.logger.Printf("Removing %d packages", len(packages)) + + args, err := i.pkgMan.RemovePackagesCommand(packages) + if err != nil { + return fmt.Errorf("failed to generate remove command: %w", err) + } + + i.logger.Printf("Running command in container: %v", args) + err = utils.RunCommandInBuilder(builder, args, nil) + if err != nil { + return fmt.Errorf("failed to remove packages: %w", err) + } + return nil +} + +func (i *TargetInstaller) RemovePackages(packages []string) error { + return i.removePackages(i.targetBuilder, packages, nil) +} + +func (i *ScratchInstaller) RemovePackages(packages []string) error { + return i.removePackages(i.helperBuilder, packages, i.createTargetMounts()) +} diff --git a/go/pkg/installer/package_manager.go b/go/pkg/installer/package_manager.go new file mode 100644 index 0000000..f5467b0 --- /dev/null +++ b/go/pkg/installer/package_manager.go @@ -0,0 +1,36 @@ +package installer + +import ( + "fmt" + + "github.com/OpenCHAMI/image-builder/go/pkg/config" +) + +// PackageManager interface defines the contract for all package managers +type PackageManager interface { + Command() string + InstallRepoCommand(repo config.Repository, proxy string) ([]string, error) + InstallPackagesCommand(packages []string, gpgCheck bool, proxy string) ([]string, error) + InstallGroupsCommand(groups []string, gpgCheck bool, proxy string) ([]string, error) + InstallModulesCommand(command string, modules []string, gpgCheck bool, proxy string) ([]string, error) + InstallGpgKeyCommand(gpgURL string) ([]string, error) + RemovePackagesCommand([]string) ([]string, error) +} + +// NewPackageManager creates a new package manager instance based on the specified type +func NewPackageManager(pkgMan string, scratch bool) (PackageManager, error) { + if pkgMan == "" { + return nil, fmt.Errorf("package manager name cannot be empty") + } + + switch pkgMan { + case "dnf": + return NewDnf(scratch) + case "yum": + return NewYum(scratch) + case "zypper": + return NewZypper(scratch) + default: + return nil, fmt.Errorf("unsupported package manager: %s", pkgMan) + } +} diff --git a/go/pkg/installer/yum.go b/go/pkg/installer/yum.go new file mode 100644 index 0000000..2b5e196 --- /dev/null +++ b/go/pkg/installer/yum.go @@ -0,0 +1,161 @@ +package installer + +import ( + "fmt" + "path/filepath" + + "github.com/OpenCHAMI/image-builder/go/pkg/config" +) + +type Yum struct { +} + +// Command returns the Yum command name +func (i *Yum) Command() string { + return "yum" +} + +type YumScratch struct { + Yum +} + +// Command returns the Yum command name +func (i *YumScratch) Command() string { + return i.Yum.Command() +} + +func NewYum(scratch bool) (PackageManager, error) { + if scratch { + return &YumScratch{}, nil + } else { + return &Yum{}, nil + } +} + +func (i *Yum) InstallRepoCommand(repo config.Repository, proxy string) ([]string, error) { + args := []string{"yum", "-y"} + if proxy != "" { + args = append(args, "--setopt=proxy="+proxy) + } + args = append(args, "config-manager", "--save", "--add-repo") + args = append(args, repo.URL) + + return args, nil +} + +func (i *YumScratch) InstallRepoCommand(repo config.Repository, proxy string) ([]string, error) { + repoDest := "etc/yum.repos.d" + + args, err := i.Yum.InstallRepoCommand(repo, proxy) + if err != nil { + return nil, err + } + args = append(args, "--setopt=reposdir="+filepath.Join(TARGET_MOUNT_DIR, repoDest)) + + return args, nil +} + +func (i *Yum) InstallPackagesCommand(packages []string, gpgCheck bool, proxy string) ([]string, error) { + args := []string{"yum", "-y"} + if proxy != "" { + args = append(args, "--setopt=proxy="+proxy) + } + args = append(args, "install") + if !gpgCheck { + args = append(args, "--nogpgcheck") + } + args = append(args, packages...) + + return args, nil +} + +func (i *YumScratch) InstallPackagesCommand(packages []string, gpgCheck bool, proxy string) ([]string, error) { + args, err := i.Yum.InstallPackagesCommand(packages, gpgCheck, proxy) + if err != nil { + return nil, err + } + + repoDest := "etc/yum.repos.d" + args = append(args, "--installroot="+TARGET_MOUNT_DIR) + args = append(args, "--setopt=reposdir="+filepath.Join(TARGET_MOUNT_DIR, repoDest)) + + return args, nil +} + +func (i *Yum) InstallGroupsCommand(package_groups []string, gpgCheck bool, proxy string) ([]string, error) { + args := []string{"yum", "-y"} + if proxy != "" { + args = append(args, "--setopt=proxy="+proxy) + } + args = append(args, "groupinstall") + if !gpgCheck { + args = append(args, "--nogpgcheck") + } + args = append(args, package_groups...) + + return args, nil +} + +func (i *YumScratch) InstallGroupsCommand(package_groups []string, gpgCheck bool, proxy string) ([]string, error) { + repoDest := "etc/yum.repos.d" + args, err := i.Yum.InstallGroupsCommand(package_groups, gpgCheck, proxy) + if err != nil { + return nil, err + } + + args = append(args, "--installroot="+TARGET_MOUNT_DIR) + args = append(args, "--setopt=reposdir="+filepath.Join(TARGET_MOUNT_DIR, repoDest)) + + return args, nil +} + +func (i *Yum) InstallModulesCommand(command string, modules []string, gpgCheck bool, proxy string) ([]string, error) { + // Yum typically doesn't support modules - this is primarily a DNF feature + // but some newer versions of Yum may support it + args := []string{"yum", "-y"} + if proxy != "" { + args = append(args, "--setopt=proxy="+proxy) + } + args = append(args, "module", command) + if !gpgCheck { + args = append(args, "--nogpgcheck") + } + args = append(args, modules...) + + return args, nil +} + +func (i *YumScratch) InstallModulesCommand(command string, modules []string, gpgCheck bool, proxy string) ([]string, error) { + repoDest := "etc/yum.repos.d" + args, err := i.Yum.InstallModulesCommand(command, modules, gpgCheck, proxy) + if err != nil { + return nil, err + } + + args = append(args, "--installroot="+TARGET_MOUNT_DIR) + args = append(args, "--setopt=reposdir="+filepath.Join(TARGET_MOUNT_DIR, repoDest)) + + return args, nil +} + +func (i *Yum) RemovePackagesCommand(packages []string) ([]string, error) { + return nil, fmt.Errorf("not implemented") +} + +func (i *YumScratch) RemovePackagesCommand(packages []string) ([]string, error) { + return nil, fmt.Errorf("not implemented") +} + +func (i *Yum) InstallGpgKeyCommand(gpgURL string) ([]string, error) { + return []string{"rpm", "--import", gpgURL}, nil +} + +func (i *YumScratch) InstallGpgKeyCommand(gpgURL string) ([]string, error) { + args, err := i.Yum.InstallGpgKeyCommand(gpgURL) + if err != nil { + return nil, err + } + args = append(args, "--root", TARGET_MOUNT_DIR) + + return args, nil +} diff --git a/go/pkg/installer/zypper.go b/go/pkg/installer/zypper.go new file mode 100644 index 0000000..cf2e4e6 --- /dev/null +++ b/go/pkg/installer/zypper.go @@ -0,0 +1,146 @@ +package installer + +import ( + "fmt" + "path/filepath" + + "github.com/OpenCHAMI/image-builder/go/pkg/config" +) + +type Zypper struct { +} + +// Command returns the Zypper command name +func (i *Zypper) Command() string { + return "zypper" +} + +type ZypperScratch struct { + Zypper +} + +// Command returns the Zypper command name +func (i *ZypperScratch) Command() string { + return i.Zypper.Command() +} + +func NewZypper(scratch bool) (PackageManager, error) { + if scratch { + return &ZypperScratch{}, nil + } else { + return &Zypper{}, nil + } +} + +func (i *Zypper) InstallRepoCommand(repo config.Repository, proxy string) ([]string, error) { + args := []string{"zypper", "addrepo", "-f", "-p"} + priority := repo.Priority + if priority == 0 { + priority = 99 + } + args = append(args, fmt.Sprintf("%d", priority)) + args = append(args, repo.URL, repo.Alias) + + return args, nil +} + +func (i *ZypperScratch) InstallRepoCommand(repo config.Repository, proxy string) ([]string, error) { + repoDest := "etc/zypp/repos.d" + + args, err := i.Zypper.InstallRepoCommand(repo, proxy) + if err != nil { + return nil, err + } + + args = append(args, "-D", repoDest, "addrepo") + + return args, nil +} + +func (i *Zypper) InstallPackagesCommand(packages []string, gpgCheck bool, proxy string) ([]string, error) { + args := []string{"zypper", "-n", "install", "--no-recommends"} + if !gpgCheck { + args = append(args, "--no-gpg-checks") + } + args = append(args, "-l") + args = append(args, packages...) + + return args, nil +} + +func (i *ZypperScratch) InstallPackagesCommand(packages []string, gpgCheck bool, proxy string) ([]string, error) { + repoDest := "etc/zypp/repos.d" + args, err := i.Zypper.InstallPackagesCommand(packages, gpgCheck, proxy) + if err != nil { + return nil, err + } + + args = append(args, "-D") + args = append(args, filepath.Join(TARGET_MOUNT_DIR, repoDest)) + args = append(args, "-C") + args = append(args, filepath.Join(TARGET_MOUNT_DIR, "tmp")) + args = append(args, "--installroot") + args = append(args, TARGET_MOUNT_DIR) + + return args, nil +} + +func (i *Zypper) InstallGroupsCommand(package_groups []string, gpgCheck bool, proxy string) ([]string, error) { + return nil, fmt.Errorf("zypper does not support package groups") +} + +func (i *Zypper) InstallModulesCommand(command string, modules []string, gpgCheck bool, proxy string) ([]string, error) { + return nil, fmt.Errorf("zypper does not support modules") +} + +func (i *ZypperScratch) InstallGroupsCommand(package_groups []string, gpgCheck bool, proxy string) ([]string, error) { + return nil, fmt.Errorf("zypper does not support package groups") +} + +func (i *ZypperScratch) InstallModulesCommand(command string, modules []string, gpgCheck bool, proxy string) ([]string, error) { + return nil, fmt.Errorf("zypper does not support modules") +} + +func (i *Zypper) RemovePackagesCommand(packages []string) ([]string, error) { + args := []string{"zypper", "-n", "remove"} + // Note: Keep --no-gpg-checks for remove operations as GPG checks don't apply to removal + args = append(args, "--no-gpg-checks") + args = append(args, "-l") + args = append(args, packages...) + + return args, nil +} + +func (i *ZypperScratch) RemovePackagesCommand(packages []string) ([]string, error) { + args, err := i.Zypper.RemovePackagesCommand(packages) + if err != nil { + return nil, err + } + + repoDest := "etc/zypp/repos.d" + + args = append(args, "-D") + args = append(args, filepath.Join(TARGET_MOUNT_DIR, repoDest)) + args = append(args, "-C") + args = append(args, filepath.Join(TARGET_MOUNT_DIR, "tmp")) + + args = append(args, "--installroot") + args = append(args, TARGET_MOUNT_DIR) + + return args, nil +} + +func (i *Zypper) InstallGpgKeyCommand(gpgURL string) ([]string, error) { + args := []string{"rpm", "--import", gpgURL} + return args, nil +} + +func (i *ZypperScratch) InstallGpgKeyCommand(gpgURL string) ([]string, error) { + args, err := i.Zypper.InstallGpgKeyCommand(gpgURL) + if err != nil { + return nil, err + } + args = append(args, "--root", TARGET_MOUNT_DIR) + + return args, nil +} diff --git a/go/pkg/layer/ansible.go b/go/pkg/layer/ansible.go new file mode 100644 index 0000000..389a17d --- /dev/null +++ b/go/pkg/layer/ansible.go @@ -0,0 +1,333 @@ +package layer + +import ( + "context" + "crypto/rand" + "encoding/hex" + "fmt" + "log" + "os" + "path/filepath" + "strings" + + "github.com/OpenCHAMI/image-builder/go/pkg/utils" + "github.com/containers/buildah" + "github.com/containers/image/v5/types" + "github.com/containers/storage" + + specs "github.com/opencontainers/runtime-spec/specs-go" +) + +// AnsibleRunner manages containerized Ansible execution +type AnsibleRunner struct { + store storage.Store + helperBuilder *buildah.Builder + logger *log.Logger +} + +// NewAnsibleRunner creates a new containerized Ansible runner +func NewAnsibleRunner(ctx context.Context) (*AnsibleRunner, error) { + // Create Ansible helper container with Python and Ansible pre-installed + // Using a base image that has Ansible and common collections available + ansibleImage := "docker.io/cytopia/ansible:latest" + containerName := "ansible-helper-" + generateRandomString(8) + + store, err := utils.GetStore() + if err != nil { + return nil, fmt.Errorf("failed to get buildah store") + } + + // Use our existing utility to create the builder - this handles storage correctly + helperBuilder, err := utils.CreateBuilder(ctx, store, ansibleImage, containerName) + if err != nil { + return nil, fmt.Errorf("failed to create Ansible helper container: %w", err) + } + + return &AnsibleRunner{ + store: store, + helperBuilder: helperBuilder, + logger: log.New(os.Stdout, "[ANSIBLE] ", log.LstdFlags), + }, nil +} + +// Cleanup cleans up the Ansible runner resources +func (ar *AnsibleRunner) Cleanup() { + _, err := ar.store.Shutdown(false) + if err != nil { + ar.logger.Printf("Warning: failed to delete storage: %v", err) + } + + if ar.helperBuilder != nil { + ar.helperBuilder.Delete() + } + // Store cleanup is handled by the CreateBuilder utility +} + +// RunPlaybooks executes Ansible playbooks against target containers using containerized approach +func (ar *AnsibleRunner) RunPlaybooks(targetBuilder *buildah.Builder, playbooks []string, groups []string, vars map[string]string, inventoryPath string, verbosity int) error { + // Mount the target container to get access to its filesystem + targetMountPoint, err := targetBuilder.Mount("") + if err != nil { + return fmt.Errorf("failed to mount target container: %w", err) + } + defer targetBuilder.Unmount() + + // Create temporary directory in helper container for Ansible work + tempDir := "/tmp/ansible-work" + if err := utils.RunCommandInBuilder(ar.helperBuilder, []string{"mkdir", "-p", tempDir}, nil); err != nil { + return fmt.Errorf("failed to create work directory in Ansible helper: %w", err) + } + + // Get container name for use in playbook execution + containerName := targetBuilder.Container + + // Determine Ansible directory to bind mount (common parent of playbooks) + var ansibleDir string + if len(playbooks) > 0 { + // Use the parent directory of the first playbook's directory (e.g., /path/to/ansible/playbooks -> /path/to/ansible) + playbookDir := filepath.Dir(playbooks[0]) + ansibleDir = filepath.Dir(playbookDir) + } + + // Execute playbooks with bind mounts + for _, playbook := range playbooks { + if err := ar.executePlaybook(containerName, playbook, ansibleDir, inventoryPath, targetMountPoint, vars, verbosity); err != nil { + return fmt.Errorf("failed to execute playbook %s: %w", playbook, err) + } + } + + return nil +} + +// generateRandomString creates a random string for container names +func generateRandomString(length int) string { + bytes := make([]byte, length/2) + rand.Read(bytes) + return hex.EncodeToString(bytes) +} + +// createTempResolvConf creates a temporary resolv.conf file on the host +func createTempResolvConf() (string, error) { + // Create temporary file on host + tmpFile, err := os.CreateTemp("", "resolv-*.conf") + if err != nil { + return "", fmt.Errorf("failed to create temp resolv.conf: %w", err) + } + + // Write DNS content + dnsContent := ` +nameserver 8.8.8.8 +nameserver 8.8.4.4 +` + if _, err := tmpFile.WriteString(dnsContent); err != nil { + tmpFile.Close() + os.Remove(tmpFile.Name()) + return "", fmt.Errorf("failed to write DNS content: %w", err) + } + + tmpFile.Close() + return tmpFile.Name(), nil +} + +// executePlaybook runs an Ansible playbook with bind-mounted Ansible directory +func (ar *AnsibleRunner) executePlaybook(containerName, playbookPath, ansibleDir, inventoryPath, targetMount string, vars map[string]string, verbosity int) error { + // Build ansible-playbook command with paths relative to bind mount + ansibleMountPoint := "/ansible" + chrootPath := "/chroot" + + // Clean up trailing slash from inventory path + cleanInventoryPath := strings.TrimSuffix(inventoryPath, "/") + + relativePath, err := filepath.Rel(ansibleDir, playbookPath) + if err != nil { + return fmt.Errorf("failed to get relative path for playbook: %w", err) + } + + // Get the relative path from ansible dir to inventory + inventoryRelPath, err := filepath.Rel(ansibleDir, cleanInventoryPath) + if err != nil { + return fmt.Errorf("failed to get relative inventory path: %w", err) + } + + // Create a minimal hosts file with our containerized target + hostsFile := "/tmp/hosts" + hostsContent := fmt.Sprintf("[compute]\n%s ansible_connection=chroot ansible_host=%s ansible_python_interpreter=/usr/bin/python3\n", containerName, chrootPath) + + // Prepare ansible-playbook command - create hosts file and use the inventory directory (which will load group_vars automatically) + setupCmd := fmt.Sprintf("echo '%s' > %s", hostsContent, hostsFile) + + // Build extra vars string from the vars map + extraVars := "ansible_host=" + chrootPath + for key, value := range vars { + extraVars += fmt.Sprintf(" %s=%s", key, value) + } + + args := []string{ + "sh", "-c", + fmt.Sprintf("%s && ansible-playbook -i %s -i %s/%s --connection=chroot --limit %s --extra-vars='%s' %s", + setupCmd, hostsFile, ansibleMountPoint, inventoryRelPath, containerName, extraVars, ansibleMountPoint+"/"+relativePath), + } + + // Add verbosity flags (force at least -vv to see chroot operations) + minVerbosity := 2 + if verbosity < minVerbosity { + verbosity = minVerbosity + } + verbosityFlags := "" + for i := 0; i < verbosity; i++ { + verbosityFlags += " -v" + } + + // Update the ansible-playbook command with verbosity + args[2] = fmt.Sprintf("%s && ansible-playbook -i %s -i %s/%s --connection=chroot --limit %s --extra-vars='%s'%s %s", + setupCmd, hostsFile, ansibleMountPoint, inventoryRelPath, containerName, extraVars, verbosityFlags, ansibleMountPoint+"/"+relativePath) + + // Create temporary resolv.conf for DNS resolution in chroot + resolvConfPath, err := createTempResolvConf() + if err != nil { + return fmt.Errorf("failed to create temporary resolv.conf: %w", err) + } + defer os.Remove(resolvConfPath) // Clean up after execution + + // Create bind mount specifications - mount Ansible data and target filesystem + // The ansible mount includes the inventory directory + mounts := []specs.Mount{ + { + Source: ansibleDir, + Destination: ansibleMountPoint, + Type: "bind", + Options: []string{"bind", "ro"}, + }, + { + Source: targetMount, + Destination: chrootPath, + Type: "bind", + Options: []string{"bind", "rw"}, + }, + // Mount /proc, /sys, /dev for network access in chroot + { + Source: "/proc", + Destination: chrootPath + "/proc", + Type: "bind", + Options: []string{"bind"}, + }, + { + Source: "/sys", + Destination: chrootPath + "/sys", + Type: "bind", + Options: []string{"bind"}, + }, + { + Source: "/dev", + Destination: chrootPath + "/dev", + Type: "bind", + Options: []string{"bind"}, + }, + // Mount temporary resolv.conf for DNS resolution + { + Source: resolvConfPath, + Destination: chrootPath + "/etc/resolv.conf", + Type: "bind", + Options: []string{"bind"}, + }, + } + + // Execute command in helper container with bind mount + err = utils.RunCommandInBuilder(ar.helperBuilder, args, mounts) + if err != nil { + return fmt.Errorf("failed to run ansible-playbook: %w", err) + } + + fmt.Printf("Playbook %s executed successfully for container %s\n", filepath.Base(playbookPath), containerName) + return nil +} + +// RunAnsiblePlaybooks executes Ansible playbooks in helper container +func RunAnsiblePlaybooks(ctx context.Context, targetBuilder *buildah.Builder, playbooks []string, groups []string, vars map[string]string, inventoryPath string, verbosity int) error { + // Create new Ansible runner + runner, err := NewAnsibleRunner(ctx) + if err != nil { + return fmt.Errorf("failed to create Ansible runner: %w", err) + } + defer runner.Cleanup() + + // Run playbooks using containerized approach + return runner.RunPlaybooks(targetBuilder, playbooks, groups, vars, inventoryPath, verbosity) +} + +// BuildAnsible builds a layer using Ansible +func (l *Layer) BuildAnsible() error { + containerName := l.Config.Name + + l.Logger.Printf("Building Ansible layer: %s from parent %s", containerName, l.Config.Parent) + + // Create buildah context + ctx := context.Background() + + store, err := utils.GetStore() + if err != nil { + return fmt.Errorf("failed to get buildah store: %w", err) + } + + // Create target builder + builder, err := utils.CreateBuilder(ctx, store, l.Config.Parent, containerName) + if err != nil { + return fmt.Errorf("failed to create container: %w", err) + } + + // Set up cleanup in case of errors + defer func() { + if _, err := store.Shutdown(false); err != nil { + l.Logger.Printf("Warning: failed to delete storage: %v", err) + } + + if deleteErr := builder.Delete(); deleteErr != nil { + l.Logger.Printf("Warning: failed to clean up target container on error: %v", deleteErr) + } + + }() + + // Run Ansible playbooks + l.Logger.Printf("Running Ansible playbooks: %v", l.Config.AnsiblePlaybook) + if err := RunAnsiblePlaybooks( + ctx, + builder, + l.Config.AnsiblePlaybook, + l.Config.AnsibleGroups, + l.Config.AnsibleVars, + l.Config.AnsibleInv, + l.Config.AnsibleVerbosity, + ); err != nil { + return fmt.Errorf("failed to run Ansible playbooks: %w", err) + } + + // Commit the changes to a new image + l.Logger.Printf("Committing changes to image: %s", containerName) + + // Setup system context + systemContext := &types.SystemContext{} + + // Create the commit options + commitOptions := buildah.CommitOptions{ + Squash: true, + OmitTimestamp: false, + SystemContext: systemContext, + ReportWriter: os.Stdout, + // Add additional tags to the image + AdditionalTags: []string{containerName}, + } + + // Use nil as ImageReference to let buildah generate a reference + imageID, _, _, err := builder.Commit(ctx, nil, commitOptions) + if err != nil { + return fmt.Errorf("failed to commit container: %w", err) + } + l.Logger.Printf("Successfully built Ansible image: %s", imageID) + + // Clean up the target container after successful commit + if err := builder.Delete(); err != nil { + l.Logger.Printf("Warning: failed to clean up target container: %v", err) + } + + return nil +} diff --git a/go/pkg/layer/layer.go b/go/pkg/layer/layer.go new file mode 100644 index 0000000..be8e496 --- /dev/null +++ b/go/pkg/layer/layer.go @@ -0,0 +1,216 @@ +package layer + +import ( + "context" + "fmt" + "log" + "os" + "time" + + "github.com/OpenCHAMI/image-builder/go/pkg/config" + "github.com/OpenCHAMI/image-builder/go/pkg/installer" + + "github.com/containers/buildah" + "github.com/containers/storage/pkg/unshare" + + "github.com/OpenCHAMI/image-builder/go/pkg/utils" +) + +// Layer handles image layer building operations +type Layer struct { + Config *config.Config + Logger *log.Logger +} + +// NewLayer creates a new layer builder +func NewLayer(cfg *config.Config) (*Layer, error) { + if cfg == nil { + return nil, fmt.Errorf("config cannot be nil") + } + if cfg.Name == "" { + return nil, fmt.Errorf("config.Name is required") + } + if cfg.LayerType == "" { + return nil, fmt.Errorf("config.LayerType is required") + } + if cfg.Parent == "" { + return nil, fmt.Errorf("config.Parent is required") + } + + return &Layer{ + Config: cfg, + Logger: log.New(os.Stdout, "[LAYER] ", log.LstdFlags), + }, nil +} + +// BuildLayer builds a layer based on the layer type and publishes it if needed +func (l *Layer) BuildLayer() error { + if l.Config == nil { + return fmt.Errorf("layer config is nil") + } + if l.Config.Name == "" { + return fmt.Errorf("layer name cannot be empty") + } + if l.Config.LayerType == "" { + return fmt.Errorf("layer type cannot be empty") + } + + l.Logger.Printf("[INFO] Starting layer build - Name: %s, Type: %s, Parent: %s", l.Config.Name, l.Config.LayerType, l.Config.Parent) + + var err error + + // Build the layer based on the type + switch l.Config.LayerType { + case "base": + err = l.BuildBase() + case "ansible": + err = l.BuildAnsible() + default: + return fmt.Errorf("unsupported layer type: %s", l.Config.LayerType) + } + + if err != nil { + return fmt.Errorf("failed to build layer: %w", err) + } + + l.Logger.Printf("[INFO] Layer build pipeline completed successfully - %s (%s)", l.Config.Name, l.Config.LayerType) + + return nil +} + +// BuildBase builds a base layer +func (l *Layer) BuildBase() error { + if l.Config.Parent == "" { + return fmt.Errorf("parent image cannot be empty for base layer") + } + + dtString := time.Now().Format("20060102150405") + containerName := l.Config.Name + "-" + dtString + + l.Logger.Printf("[INFO] Building base layer - Container: %s, Parent: %s, Package Manager: %s", containerName, l.Config.Parent, l.Config.PackageManager) + + // Create buildah context + ctx := context.Background() + + // Add debugging information + l.Logger.Printf("[DEBUG] Runtime info - UID: %d, Rootless: %v", os.Geteuid(), unshare.IsRootless()) + + // Create a container from the parent image + l.Logger.Printf("[INFO] Creating buildah container from base image: %s", l.Config.Parent) + + store, err := utils.GetStore() + if err != nil { + return fmt.Errorf("failed to get buildah store: %w", err) + } + + targetBuilder, err := utils.CreateBuilder(ctx, store, l.Config.Parent, containerName) + if err != nil { + return fmt.Errorf("failed to create container %s from image %s: %w", containerName, l.Config.Parent, err) + } + + defer func() { + if _, err := store.Shutdown(false); err != nil { + l.Logger.Printf("Warning: failed to delete storage: %v", err) + } + + if deleteErr := targetBuilder.Delete(); deleteErr != nil { + l.Logger.Printf("Warning: failed to clean up target container on error: %v", deleteErr) + } + }() + + // Create installer for package management + inst, err := installer.NewInstaller(targetBuilder, l.Config.PackageManager, l.Config.GPGCheck, l.Config.Proxy) + if err != nil { + return fmt.Errorf("failed to create installer: %w", err) + } + defer inst.Cleanup() + + repositories := l.Config.Repositories + + if err := inst.InstallRepos(repositories); err != nil { + return fmt.Errorf("failed to install repositories: %w", err) + } + + // Install modules + if err := inst.InstallModules(l.Config.Modules); err != nil { + return fmt.Errorf("failed to install modules: %w", err) + } + + // Install package groups + if err := inst.InstallGroups(l.Config.PackageGroups); err != nil { + return fmt.Errorf("failed to install package groups: %w", err) + } + + // Install packages + if err := inst.InstallPackages(l.Config.Packages); err != nil { + return fmt.Errorf("failed to install packages: %w", err) + } + + // Remove specified packages + if err := inst.RemovePackages(l.Config.RemovePackages); err != nil { + return fmt.Errorf("failed to remove packages: %w", err) + } + + // Run commands + for _, cmd := range l.Config.Commands { + l.Logger.Printf("[INFO] Executing command: %s", cmd.Cmd) + + err := utils.RunCommandInBuilder(targetBuilder, []string{"sh", "-c", cmd.Cmd}, nil) + if err != nil { + return fmt.Errorf("failed to run command '%s': %w", cmd.Cmd, err) + } + l.Logger.Printf("[INFO] Command completed successfully: %s", cmd.Cmd) + } + + // Copy files + for _, copyFile := range l.Config.CopyFiles { + src := copyFile.Src + dest := copyFile.Dest + + if src == "" || dest == "" { + continue + } + + if _, err := os.Stat(src); os.IsNotExist(err) { + return fmt.Errorf("source file does not exist: %s", src) + } + + l.Logger.Printf("[INFO] Copying file: %s -> %s (owner: root:root)", src, dest) + + // Build builder to copy files + if err := targetBuilder.Add(dest, false, buildah.AddAndCopyOptions{ + Chown: "root:root", + }, src); err != nil { + return fmt.Errorf("failed to copy file to container: %w", err) + } + l.Logger.Printf("[INFO] Successfully copied file: %s", src) + + } + + // Commit the changes to a new image + l.Logger.Printf("[INFO] Committing container changes - Name: %s, Squash: %t", containerName, true) + + // Create the commit options + commitOptions := buildah.CommitOptions{ + Squash: true, + OmitTimestamp: false, + ReportWriter: os.Stdout, + // Add additional tags to the image + AdditionalTags: []string{containerName}, + } + + // Use nil as ImageReference to let buildah generate a reference + imageID, _, _, err := targetBuilder.Commit(ctx, nil, commitOptions) + if err != nil { + return fmt.Errorf("failed to commit container: %w", err) + } + + l.Logger.Printf("[INFO] Image build completed successfully - ID: %s, Name: %s", imageID, containerName) + + // Publish the image if publishing options are specified + if err := l.Publish(imageID, containerName); err != nil { + return fmt.Errorf("failed to publish image: %w", err) + } + + return nil +} diff --git a/go/pkg/layer/publish.go b/go/pkg/layer/publish.go new file mode 100644 index 0000000..f489d3d --- /dev/null +++ b/go/pkg/layer/publish.go @@ -0,0 +1,85 @@ +package layer + +import ( + "context" + "fmt" + + "github.com/OpenCHAMI/image-builder/go/pkg/publish" +) + +// Publish handles publishing the built container image by delegating to the publish package +func (l *Layer) Publish(imageID, containerName string) error { + ctx := context.Background() + imageConfig := l.createImageConfig() + + // Publish to local storage if requested + if l.Config.PublishLocal { + if err := publish.PublishLocal(ctx, imageID, containerName, imageConfig, l.Logger); err != nil { + return fmt.Errorf("local publishing failed: %w", err) + } + } + + // Publish to S3 if configured + if l.Config.PublishS3 != "" { + + s3Config := l.createS3Config() + if err := publish.PublishToS3(ctx, imageID, containerName, imageConfig, s3Config, l.Logger); err != nil { + return fmt.Errorf("S3 publishing failed: %w", err) + } + } + + // Publish to registry if configured + if l.Config.PublishRegistry != "" { + registryConfig := l.createRegistryConfig() + if err := publish.PublishToRegistry(ctx, imageID, containerName, imageConfig, registryConfig, l.Logger); err != nil { + return fmt.Errorf("registry publishing failed: %w", err) + } + } + + return nil +} + +// createImageConfig creates the image configuration with labels +func (l *Layer) createImageConfig() publish.ImageConfig { + // Get publish tags, default to "latest" if none specified + tags := l.Config.PublishTags + if len(tags) == 0 { + tags = []string{"latest"} + } + + // Extract repository aliases + var repositories []string + for _, repo := range l.Config.Repositories { + repositories = append(repositories, repo.Alias) + } + + imageConfig := publish.ImageConfig{ + Name: l.Config.Name, + LayerType: l.Config.LayerType, + Parent: l.Config.Parent, + PublishTags: tags, + Labels: make(map[string]string), + } + + // Generate standard labels + imageConfig.Labels = publish.GenerateLabels(imageConfig, l.Config.Packages, l.Config.PackageGroups, repositories) + + return imageConfig +} + +// createS3Config creates S3 config if enabled, returns nil if disabled +func (l *Layer) createS3Config() publish.S3PublishConfig { + return publish.S3PublishConfig{ + S3Endpoint: l.Config.PublishS3, + S3Bucket: l.Config.S3Bucket, + S3Prefix: l.Config.S3Prefix, + } +} + +// createRegistryConfig creates registry config if enabled, returns nil if disabled +func (l *Layer) createRegistryConfig() publish.RegistryPublishConfig { + return publish.RegistryPublishConfig{ + RegistryEndpoint: l.Config.PublishRegistry, + RegistryOpts: l.Config.RegistryOptsPush, + } +} diff --git a/go/pkg/publish/local.go b/go/pkg/publish/local.go new file mode 100644 index 0000000..d6e6d54 --- /dev/null +++ b/go/pkg/publish/local.go @@ -0,0 +1,85 @@ +package publish + +import ( + "context" + "fmt" + "log" + "os" + + "github.com/containers/storage" +) + +// LocalPublisher implements Publisher interface for local podman/buildah publishing +type LocalPublisher struct { + logger *log.Logger +} + +// NewLocalPublisher creates a new local publisher +func NewLocalPublisher() *LocalPublisher { + return &LocalPublisher{ + logger: log.New(os.Stdout, "[LocalPublisher] ", log.LstdFlags), + } +} + +// validate checks if local configuration is valid +func (l *LocalPublisher) validate(imageConfig ImageConfig) error { + // Local publishing doesn't need much validation + // Just ensure we have basic config + if imageConfig.Name == "" { + return fmt.Errorf("image name is required") + } + return nil +} + +// Publish publishes the image to local storage +func (l *LocalPublisher) Publish(ctx context.Context, imageID, containerName string, imageConfig ImageConfig, store storage.Store) error { + // Validate configuration first + if err := l.validate(imageConfig); err != nil { + return fmt.Errorf("local configuration validation failed: %w", err) + } + + l.logger.Printf("Publishing to local storage") + + return l.commitToLocal(ctx, imageID, imageConfig, store) +} + +// commitToLocal tags the already committed image with the specified publish tags using explicit imageID +func (l *LocalPublisher) commitToLocal(ctx context.Context, imageID string, imageConfig ImageConfig, store storage.Store) error { + tags := imageConfig.PublishTags + if len(tags) == 0 { + tags = []string{"latest"} + } + + if len(tags) == 0 { + l.logger.Printf("No publish tags specified, using image ID: %s", imageID) + return nil + } + + // Tag the image with each publish tag using buildah API + for _, tag := range tags { + l.logger.Printf("Tagging image with tag: %s", tag) + + // Create target image name for local storage + targetImage := fmt.Sprintf("localhost/%s:%s", imageConfig.Name, tag) + + l.logger.Printf("Tagging image ID %s as %s", imageID, targetImage) + + // Get the existing image and add the new tag + img, err := store.Image(imageID) + if err != nil { + return fmt.Errorf("failed to get image %s: %w", imageID, err) + } + + // Add the new name to the existing names + newNames := append(img.Names, targetImage) + + // Update the image with new names + if err := store.SetNames(imageID, newNames); err != nil { + return fmt.Errorf("failed to set names for image %s: %w", imageID, err) + } + + l.logger.Printf("Successfully tagged image as %s", targetImage) + } + + return nil +} diff --git a/go/pkg/publish/publish.go b/go/pkg/publish/publish.go new file mode 100644 index 0000000..8ab4ddc --- /dev/null +++ b/go/pkg/publish/publish.go @@ -0,0 +1,123 @@ +package publish + +import ( + "context" + "fmt" + "log" + "time" + + "github.com/OpenCHAMI/image-builder/go/pkg/utils" +) + +// PublishLocal publishes the image to local storage only +func PublishLocal(ctx context.Context, imageID, containerName string, imageConfig ImageConfig, logger *log.Logger) error { + logger.Printf("Publishing image ID: %s (container: %s) to local storage", imageID, containerName) + + store, err := utils.GetStore() + if err != nil { + return fmt.Errorf("failed to get buildah store: %w", err) + } + + localPub := NewLocalPublisher() + if err := localPub.Publish(ctx, imageID, containerName, imageConfig, store); err != nil { + return fmt.Errorf("local publishing failed: %w", err) + } + + logger.Printf("Successfully published to local storage") + return nil +} + +// PublishToS3 publishes the image to S3 storage +func PublishToS3(ctx context.Context, imageID, containerName string, imageConfig ImageConfig, s3Config S3PublishConfig, logger *log.Logger) error { + logger.Printf("Publishing image ID: %s (container: %s) to S3: %s", imageID, containerName, s3Config.S3Endpoint) + + store, err := utils.GetStore() + if err != nil { + return fmt.Errorf("failed to get buildah store: %w", err) + } + + s3Pub, err := NewS3Publisher(s3Config) + if err != nil { + return fmt.Errorf("failed to create S3 publisher: %w", err) + } + + if err := s3Pub.Publish(ctx, imageID, containerName, imageConfig, store); err != nil { + return fmt.Errorf("S3 publishing failed: %w", err) + } + + logger.Printf("Successfully published to S3") + return nil +} + +// PublishToRegistry publishes the image to a container registry +func PublishToRegistry(ctx context.Context, imageID, containerName string, imageConfig ImageConfig, registryConfig RegistryPublishConfig, logger *log.Logger) error { + logger.Printf("Publishing image ID: %s (container: %s) to registry", imageID, containerName) + + store, err := utils.GetStore() + if err != nil { + return fmt.Errorf("failed to get buildah store: %w", err) + } + + registryPub, err := NewRegistryPublisher(registryConfig) + if err != nil { + return fmt.Errorf("failed to create registry publisher: %w", err) + } + + if err := registryPub.Publish(ctx, imageID, containerName, imageConfig, store); err != nil { + return fmt.Errorf("registry publishing failed: %w", err) + } + + logger.Printf("Successfully published to registry") + return nil +} + +// GenerateLabels creates standard labels for container images +func GenerateLabels(config ImageConfig, packages, packageGroups, repositories []string) map[string]string { + labels := make(map[string]string) + + // Copy existing labels + for k, v := range config.Labels { + labels[k] = v + } + + // Basic metadata + labels["org.openchami.image.name"] = config.Name + labels["org.openchami.image.type"] = config.LayerType + labels["org.openchami.image.parent"] = config.Parent + + // Tags information + if len(config.PublishTags) > 0 { + labels["org.openchami.image.tags"] = joinStrings(config.PublishTags, ",") + } + + // Build information + labels["org.openchami.image.build-date"] = time.Now().Format(time.RFC3339) + + // Package information + if len(packages) > 0 { + labels["org.openchami.image.packages"] = joinStrings(packages, ",") + } + + if len(packageGroups) > 0 { + labels["org.openchami.image.package-groups"] = joinStrings(packageGroups, ",") + } + + // Repository information + if len(repositories) > 0 { + labels["org.openchami.image.repositories"] = joinStrings(repositories, ",") + } + + return labels +} + +// Helper functions +func joinStrings(strs []string, sep string) string { + if len(strs) == 0 { + return "" + } + result := strs[0] + for i := 1; i < len(strs); i++ { + result += sep + strs[i] + } + return result +} diff --git a/go/pkg/publish/registry.go b/go/pkg/publish/registry.go new file mode 100644 index 0000000..416b1a4 --- /dev/null +++ b/go/pkg/publish/registry.go @@ -0,0 +1,100 @@ +package publish + +import ( + "context" + "fmt" + "log" + "os" + "strings" + + "github.com/containers/buildah" + "github.com/containers/image/v5/transports/alltransports" + "github.com/containers/image/v5/types" + "github.com/containers/storage" +) + +// RegistryPublisher implements Publisher interface for container registry publishing +type RegistryPublisher struct { + logger *log.Logger + config RegistryPublishConfig +} + +// NewRegistryPublisher creates a new registry publisher +func NewRegistryPublisher(config RegistryPublishConfig) (*RegistryPublisher, error) { + if config.RegistryEndpoint == "" { + return nil, fmt.Errorf("RegistryEndpoint must be specified") + } + + return &RegistryPublisher{ + logger: log.New(os.Stdout, "[RegistryPublisher] ", log.LstdFlags), + config: config, + }, nil +} + +// Publish publishes the image to a container registry (implements Publisher interface) +func (r *RegistryPublisher) Publish(ctx context.Context, imageID, containerName string, imageConfig ImageConfig, store storage.Store) error { + r.logger.Printf("Starting registry publishing for image %s", imageID) + + tags := imageConfig.PublishTags + if len(tags) == 0 { + tags = []string{"latest"} + } + + // Create system context with registry options + systemCtx := &types.SystemContext{} + r.setupSystemContext(systemCtx, r.config.RegistryOpts) + + // Publish to registry with all tags + for _, tag := range tags { + if err := r.pushToRegistry(ctx, imageID, imageConfig, tag, store, systemCtx); err != nil { + return fmt.Errorf("failed to push tag %s: %w", tag, err) + } + } + + return nil +} + +// getRegistryEndpoint determines the registry endpoint to use +func (r *RegistryPublisher) getRegistryEndpoint() string { + return r.config.RegistryEndpoint +} + +// pushToRegistry pushes a single tagged image to the registry +func (r *RegistryPublisher) pushToRegistry(ctx context.Context, imageID string, imageConfig ImageConfig, tag string, store storage.Store, systemCtx *types.SystemContext) error { + registryEndpoint := r.getRegistryEndpoint() + registryImage := fmt.Sprintf("%s/%s:%s", registryEndpoint, imageConfig.Name, tag) + + r.logger.Printf("Pushing image %s as %s", imageID, registryImage) + + // Parse the destination reference + destRef, err := alltransports.ParseImageName("docker://" + registryImage) + if err != nil { + return fmt.Errorf("failed to parse registry image name %s: %w", registryImage, err) + } + + // Setup push options + pushOptions := buildah.PushOptions{ + Store: store, + SystemContext: systemCtx, + ReportWriter: r.logger.Writer(), + } + + // Push the image using the explicit imageID + _, digest, err := buildah.Push(ctx, imageID, destRef, pushOptions) + if err != nil { + return fmt.Errorf("failed to push %s: %w", registryImage, err) + } + + r.logger.Printf("Successfully pushed image: %s (digest: %s)", registryImage, digest) + return nil +} + +// setupSystemContext configures the system context based on registry options +func (r *RegistryPublisher) setupSystemContext(systemCtx *types.SystemContext, registryOpts []string) { + // Handle registry options + for _, opt := range registryOpts { + if strings.Contains(opt, "--tls-verify=false") { + systemCtx.DockerInsecureSkipTLSVerify = types.OptionalBoolTrue + } + } +} diff --git a/go/pkg/publish/s3.go b/go/pkg/publish/s3.go new file mode 100644 index 0000000..86e567e --- /dev/null +++ b/go/pkg/publish/s3.go @@ -0,0 +1,325 @@ +package publish + +import ( + "context" + "fmt" + "log" + "os" + "os/exec" + "path/filepath" + "strings" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/service/s3" + "github.com/containers/storage" +) + +// S3Publisher implements Publisher interface for AWS S3 publishing +type S3Publisher struct { + logger *log.Logger + config S3PublishConfig +} + +// NewS3Publisher creates a new S3 publisher +func NewS3Publisher(config S3PublishConfig) (*S3Publisher, error) { + if config.S3Endpoint == "" { + return nil, fmt.Errorf("S3 endpoint is required") + } + if config.S3Bucket == "" { + return nil, fmt.Errorf("S3 bucket is required") + } + + return &S3Publisher{ + logger: log.New(os.Stdout, "[S3Publisher] ", log.LstdFlags), + config: config, + }, nil +} + +// validate checks if S3 configuration is valid +func (s *S3Publisher) validate(config S3PublishConfig) error { + + return nil +} + +// Publish publishes the image to S3 (implements Publisher interface) +func (s *S3Publisher) Publish(ctx context.Context, imageID, containerName string, imageConfig ImageConfig, store storage.Store) error { + // Validate configuration + if err := s.validate(s.config); err != nil { + return fmt.Errorf("S3 configuration validation failed: %w", err) + } + + // Mount the container to access its filesystem + mountPoint, err := s.mountContainer(containerName, store) + if err != nil { + return fmt.Errorf("failed to mount container: %w", err) + } + defer s.unmountContainer(containerName, store) + + return s.uploadToS3(ctx, mountPoint, imageConfig) +} + +// mountContainer mounts the container and returns the mount point +func (s *S3Publisher) mountContainer(containerName string, store storage.Store) (string, error) { + // Use buildah mount command to get the mount point + cmd := exec.Command("buildah", "mount", containerName) + output, err := cmd.Output() + if err != nil { + return "", fmt.Errorf("failed to mount container %s: %w", containerName, err) + } + + mountPoint := strings.TrimSpace(string(output)) + s.logger.Printf("Container mounted at: %s", mountPoint) + return mountPoint, nil +} + +// unmountContainer unmounts the container +func (s *S3Publisher) unmountContainer(containerName string, store storage.Store) { + cmd := exec.Command("buildah", "umount", containerName) + if err := cmd.Run(); err != nil { + s.logger.Printf("Warning: failed to unmount container %s: %v", containerName, err) + } +} + +// uploadToS3 handles the S3 upload process similar to the Python implementation +func (s *S3Publisher) uploadToS3(ctx context.Context, mountPoint string, imageConfig ImageConfig) error { + s.logger.Printf("Starting S3 upload process for mount point: %s", mountPoint) + + // Create S3 client + s3Client, err := s.createS3Client(ctx) + if err != nil { + return fmt.Errorf("failed to create S3 client: %w", err) + } + + // Create temporary directory for squashed image + tempDir, err := os.MkdirTemp("", "image-builder-s3-") + if err != nil { + return fmt.Errorf("failed to create temp directory: %w", err) + } + defer os.RemoveAll(tempDir) + + // Find kernel version and boot files + kernelVersion, initrd, vmlinuz, err := s.findBootFiles(mountPoint) + if err != nil { + return fmt.Errorf("failed to find boot files: %w", err) + } + + s.logger.Printf("Found kernel version: %s, initrd: %s, vmlinuz: %s", kernelVersion, initrd, vmlinuz) + + // Squash the image + squashFile := filepath.Join(tempDir, "rootfs") + if err := s.squashImage(mountPoint, squashFile); err != nil { + return fmt.Errorf("failed to squash image: %w", err) + } + + // Get OS information and build image name + osVersion, err := s.getOSVersion(mountPoint) + if err != nil { + s.logger.Printf("Warning: could not determine OS version: %v", err) + osVersion = "unknown" + } + + // Build S3 keys + s3Prefix := s.config.S3Prefix + if s3Prefix == "" { + s3Prefix = "image" + } + + tags := imageConfig.PublishTags + if len(tags) == 0 { + tags = []string{"latest"} + } + imageName := fmt.Sprintf("%s-%s-%s-%s", s3Prefix, osVersion, imageConfig.Name, tags[0]) + + s.logger.Printf("Uploading files to S3 bucket: %s", s.config.S3Bucket) + + // Upload vmlinuz if it exists + if vmlinuz != "" { + vmlinuzPath := filepath.Join(mountPoint, "boot", vmlinuz) + if err := s.uploadFileToS3(ctx, s3Client, vmlinuzPath, "efi-images/"+s3Prefix+vmlinuz, s.config.S3Bucket); err != nil { + return fmt.Errorf("failed to upload vmlinuz: %w", err) + } + } else { + s.logger.Printf("No vmlinuz to upload - skipping") + } + + // Upload initrd if it exists + if initrd != "" { + initrdPath := filepath.Join(mountPoint, "boot", initrd) + if err := s.uploadFileToS3(ctx, s3Client, initrdPath, "efi-images/"+s3Prefix+initrd, s.config.S3Bucket); err != nil { + return fmt.Errorf("failed to upload initrd: %w", err) + } + } else { + s.logger.Printf("No initramfs to upload - skipping") + } + + // Upload squashed rootfs + if err := s.uploadFileToS3(ctx, s3Client, squashFile, imageName, s.config.S3Bucket); err != nil { + return fmt.Errorf("failed to upload rootfs: %w", err) + } + + s.logger.Printf("Successfully uploaded image to S3: %s", imageName) + return nil +} + +// createS3Client creates an S3 client with proper configuration +func (s *S3Publisher) createS3Client(ctx context.Context) (*s3.Client, error) { + // For now, use default AWS configuration + // TODO: Add support for custom endpoints and credentials from config + cfg, err := config.LoadDefaultConfig(ctx) + if err != nil { + return nil, fmt.Errorf("failed to load AWS config: %w", err) + } + + // If custom credentials are needed, they could be added here + // For example, if config had S3 credential fields: + // cfg.Credentials = credentials.NewStaticCredentialsProvider(accessKey, secretKey, "") + + return s3.NewFromConfig(cfg), nil +} + +// findBootFiles finds the kernel version and corresponding boot files +func (s *S3Publisher) findBootFiles(mountPoint string) (kernelVersion, initrd, vmlinuz string, err error) { + modulesDir := filepath.Join(mountPoint, "lib", "modules") + + entries, err := os.ReadDir(modulesDir) + if err != nil { + return "", "", "", fmt.Errorf("failed to read modules directory: %w", err) + } + + for _, entry := range entries { + if !entry.IsDir() { + continue + } + + kver := entry.Name() + s.logger.Printf("Checking kernel version: %s", kver) + + kernelVersion = kver + + // Look for vmlinuz + vmlinuzPath := filepath.Join(mountPoint, "boot", "vmlinuz-"+kver) + if _, err := os.Stat(vmlinuzPath); err == nil { + vmlinuz = "vmlinuz-" + kver + s.logger.Printf("Found vmlinuz: %s", vmlinuz) + } else { + s.logger.Printf("No vmlinuz found for %s", kver) + vmlinuz = "" + } + + // Look for initramfs + initramfsPath := filepath.Join(mountPoint, "boot", "initramfs-"+kver+".img") + initrdPath := filepath.Join(mountPoint, "boot", "initrd-"+kver) + + if _, err := os.Stat(initramfsPath); err == nil { + initrd = "initramfs-" + kver + ".img" + s.logger.Printf("Found initramfs: %s", initrd) + } else if _, err := os.Stat(initrdPath); err == nil { + initrd = "initrd-" + kver + s.logger.Printf("Found initrd: %s", initrd) + } else { + s.logger.Printf("No initramfs found for %s", kver) + initrd = "" + } + + // If we found at least one boot file, we can proceed + if vmlinuz != "" || initrd != "" { + return kernelVersion, initrd, vmlinuz, nil + } + + s.logger.Printf("No usable boot files found for %s, trying next kernel", kver) + } + + return "", "", "", fmt.Errorf("no valid kernel/initramfs combination found") +} + +// squashImage creates a squashfs image from the mounted filesystem +func (s *S3Publisher) squashImage(mountPoint, outputFile string) error { + s.logger.Printf("Creating squashfs image: %s -> %s", mountPoint, outputFile) + + cmd := exec.Command("mksquashfs", mountPoint, outputFile) + output, err := cmd.CombinedOutput() + if err != nil { + return fmt.Errorf("mksquashfs failed: %w, output: %s", err, string(output)) + } + + s.logger.Printf("Successfully created squashfs image") + return nil +} + +// getOSVersion attempts to determine the OS version from the mounted filesystem +// This matches the Python implementation logic exactly +func (s *S3Publisher) getOSVersion(mountPoint string) (string, error) { + // Try to read /etc/os-release + osReleasePath := filepath.Join(mountPoint, "etc", "os-release") + if data, err := os.ReadFile(osReleasePath); err == nil { + osDict := make(map[string]string) + content := string(data) + + // Parse key=value pairs like Python implementation + for _, line := range strings.Split(content, "\n") { + if strings.Contains(line, "=") { + parts := strings.SplitN(line, "=", 2) + if len(parts) == 2 { + key := strings.TrimSpace(parts[0]) + value := strings.TrimSpace(parts[1]) + osDict[key] = value + } + } + } + + // Follow Python logic: ID + VERSION_ID, or fallback to ID_LIKE + NAME + if id, hasID := osDict["ID"]; hasID { + if versionID, hasVersion := osDict["VERSION_ID"]; hasVersion { + // Remove quotes from both + id = strings.Trim(id, `"`) + versionID = strings.Trim(versionID, `"`) + return strings.ToLower(id + versionID), nil + } + } + + if idLike, hasIDLike := osDict["ID_LIKE"]; hasIDLike { + if name, hasName := osDict["NAME"]; hasName { + // Remove quotes from both + idLike = strings.Trim(idLike, `"`) + name = strings.Trim(name, `"`) + return strings.ToLower(idLike + "-" + name), nil + } + } + } + + // Fallback: try other methods + redhatReleasePath := filepath.Join(mountPoint, "etc", "redhat-release") + if data, err := os.ReadFile(redhatReleasePath); err == nil { + content := strings.TrimSpace(string(data)) + content = strings.ReplaceAll(content, " ", "-") + content = strings.ToLower(content) + return content, nil + } + + return "unknown", fmt.Errorf("could not determine OS version") +} + +// uploadFileToS3 uploads a single file to S3 +func (s *S3Publisher) uploadFileToS3(ctx context.Context, client *s3.Client, filePath, s3Key, bucket string) error { + s.logger.Printf("Uploading %s as %s to bucket %s", filePath, s3Key, bucket) + + file, err := os.Open(filePath) + if err != nil { + return fmt.Errorf("failed to open file %s: %w", filePath, err) + } + defer file.Close() + + _, err = client.PutObject(ctx, &s3.PutObjectInput{ + Bucket: aws.String(bucket), + Key: aws.String(s3Key), + Body: file, + }) + + if err != nil { + return fmt.Errorf("failed to upload file to S3: %w", err) + } + + s.logger.Printf("Successfully uploaded: %s", s3Key) + return nil +} diff --git a/go/pkg/publish/types.go b/go/pkg/publish/types.go new file mode 100644 index 0000000..ab768e4 --- /dev/null +++ b/go/pkg/publish/types.go @@ -0,0 +1,35 @@ +package publish + +import ( + "context" + + "github.com/containers/storage" +) + +// ImageConfig contains image metadata and configuration +type ImageConfig struct { + Name string + LayerType string + Parent string + PublishTags []string + Labels map[string]string +} + +// S3PublishConfig contains configuration specific to S3 publishing +type S3PublishConfig struct { + S3Endpoint string + S3Bucket string + S3Prefix string +} + +// RegistryPublishConfig contains configuration specific to registry publishing +type RegistryPublishConfig struct { + RegistryEndpoint string + RegistryOpts []string +} + +// Publisher defines the interface for publishing container images +type Publisher interface { + // Publish publishes the container image + Publish(ctx context.Context, imageID, containerName string, imageConfig ImageConfig, store storage.Store) error +} diff --git a/go/pkg/utils/builder.go b/go/pkg/utils/builder.go new file mode 100644 index 0000000..f6c3e38 --- /dev/null +++ b/go/pkg/utils/builder.go @@ -0,0 +1,138 @@ +package utils + +import ( + "context" + "fmt" + + "github.com/containers/buildah" + "github.com/containers/buildah/pkg/parse" + "github.com/containers/storage" + "github.com/opencontainers/runtime-spec/specs-go" +) + +// GetStore returns a buildah store using default configuration with rootless-friendly settings +func GetStore() (storage.Store, error) { + // Get buildah store with default options + storeOptions, err := storage.DefaultStoreOptions() + if err != nil { + return nil, fmt.Errorf("failed to get default store options: %w", err) + } + + store, err := storage.GetStore(storeOptions) + if err != nil { + return nil, fmt.Errorf("failed to get buildah store: %w", err) + } + + return store, nil +} + +func CreateBuilder(ctx context.Context, store storage.Store, image string, name string) (*buildah.Builder, error) { + if ctx == nil { + return nil, fmt.Errorf("context cannot be nil") + } + if store == nil { + return nil, fmt.Errorf("store cannot be nil") + } + if image == "" { + return nil, fmt.Errorf("image name cannot be empty") + } + if name == "" { + return nil, fmt.Errorf("container name cannot be empty") + } + + // TODO figure out capabilities needed for non-root users + defaultCaps := []string{ + "CAP_CHOWN", + "CAP_DAC_OVERRIDE", + "CAP_FSETID", + "CAP_FOWNER", + "CAP_MKNOD", + "CAP_NET_RAW", + "CAP_SETGID", + "CAP_SETUID", + "CAP_SETFCAP", + "CAP_SETPCAP", + "CAP_NET_BIND_SERVICE", + "CAP_SYS_CHROOT", + "CAP_KILL", + "CAP_AUDIT_WRITE", + } + + helperBuilder, err := buildah.NewBuilder(ctx, store, buildah.BuilderOptions{ + FromImage: image, + Capabilities: defaultCaps, + PullPolicy: buildah.PullIfMissing, + Container: name, + }) + if err != nil { + return nil, fmt.Errorf("failed to create builder: %w", err) + } + + return helperBuilder, nil + +} + +func runCommandInBuilder(builder *buildah.Builder, cmd []string, mounts []specs.Mount, env []string) error { + if builder == nil { + return fmt.Errorf("builder cannot be nil") + } + if len(cmd) == 0 { + return fmt.Errorf("command cannot be empty") + } + + isolation, err := parse.IsolationOption("") + if err != nil { + return fmt.Errorf("failed to parse isolation option: %w", err) + } + + options := buildah.RunOptions{ + Isolation: isolation, + Terminal: buildah.WithoutTerminal, + } + + if env != nil { + options.Env = append(builder.Env(), env...) + } + + if mounts != nil { + options.Mounts = mounts + } + + // Execute the command in the container + if err := builder.Run(cmd, options); err != nil { + return fmt.Errorf("failed to run command in container: %w", err) + } + + return nil +} + +func RunCommandInBuilder(builder *buildah.Builder, cmd []string, mounts []specs.Mount) error { + return runCommandInBuilder(builder, cmd, mounts, nil) +} + +func RunCommandInBuilderWithEnv(builder *buildah.Builder, cmd []string, mounts []specs.Mount, env []string) error { + return runCommandInBuilder(builder, cmd, mounts, env) +} + +func RunCommandInContainer(container string, cmd []string) error { + // Get buildah store + store, err := GetStore() + if err != nil { + return err + } + + defer func() { + if _, err := store.Shutdown(false); err != nil { + // Log the error but don't fail the operation + fmt.Printf("Warning: failed to shutdown store: %v\n", err) + } + }() + + // Get the builder for this container + builder, err := buildah.OpenBuilder(store, container) + if err != nil { + return fmt.Errorf("failed to open builder for container %s: %w", container, err) + } + + return RunCommandInBuilder(builder, cmd, nil) +}