├── .dockerignore ├── .github └── workflows │ ├── branch-sync.yaml │ ├── build-and-publish.yaml │ └── ci.yaml ├── .gitignore ├── Dockerfile ├── Makefile ├── README.md ├── buildctx └── context.go ├── buildman_pb └── buildman.pb.go ├── buildpack ├── buildpack.go ├── buildpack_test.go └── ssh-git.sh ├── cmd └── quay-builder │ ├── main.go │ ├── realm_formatter.go │ └── tls.go ├── containerclient ├── container_client_test.go ├── docker_client.go ├── docker_log_writer.go ├── dockerfile │ ├── context.go │ ├── context_test.go │ ├── dockerfile.go │ ├── dockerfile_test.go │ ├── main_test.go │ ├── testdata │ │ └── moby.txt │ └── util.go ├── interface.go ├── log_writer_test.go ├── podman_client.go └── podman_log_write.go ├── entrypoint.sh ├── go.mod ├── go.sum ├── rpc ├── grpcbuild │ └── grpcbuild.go └── rpc.go ├── scripts └── git-version └── version └── version.go /.dockerignore: -------------------------------------------------------------------------------- 1 | vendor/* 2 | Dockerfile.* 3 | -------------------------------------------------------------------------------- /.github/workflows/branch-sync.yaml: -------------------------------------------------------------------------------- 1 | name: Sync Master to Release Branch 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | sync_master_to_release: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Check out the repo 13 | uses: actions/checkout@v4 14 | with: 15 | fetch-depth: 0 16 | 17 | - name: Configure Git 18 | run: | 19 | git config user.name "GitHub Actions Bot" 20 | git config user.email "github-actions-bot@users.noreply.github.com" 21 | 22 | - name: Merge master into release 23 | run: | 24 | BRANCH_NAME="redhat-3.15" 25 | echo "Syncing master to $BRANCH_NAME" 26 | git checkout master 27 | git pull origin master 28 | MASTER_COMMIT=$(git rev-parse HEAD) 29 | 30 | git fetch origin $BRANCH_NAME 31 | git checkout $BRANCH_NAME 32 | git pull origin $BRANCH_NAME 33 | RELEASE_COMMIT=$(git rev-parse HEAD) 34 | echo "Rebasing $BRANCH_NAME ($RELEASE_COMMIT) onto master ($MASTER_COMMIT)" 35 | git rebase master 36 | git push origin $BRANCH_NAME 37 | 38 | echo "Merged master ($MASTER_COMMIT) into $BRANCH_NAME ($RELEASE_COMMIT)" 39 | env: 40 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 41 | -------------------------------------------------------------------------------- /.github/workflows/build-and-publish.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Build and Publish 3 | 4 | on: 5 | push: 6 | branches: 7 | - redhat-** # IMPORTANT! this must match the jobs.build-and-publish.env.BRANCH_PREFIX (save the **). 8 | 9 | workflow_dispatch: 10 | # Allows you to run this workflow manually from the Actions tab 11 | inputs: 12 | tag: 13 | description: 'Tag to attach to image' 14 | required: true 15 | 16 | jobs: 17 | build-and-publish: 18 | name: Publish container image 19 | env: 20 | BRANCH_PREFIX: redhat- # IMPORTANT! this must match the .on.push.branches prefix! 21 | REGISTRY: quay.io/projectquay 22 | REPO_NAME: ${{ github.event.repository.name }} 23 | TAG_SUFFIX: -unstable 24 | runs-on: 'ubuntu-latest' 25 | steps: 26 | - name: Check out the repo 27 | uses: actions/checkout@v2 28 | 29 | - name: Set version from branch name 30 | id: version-from-branch 31 | if: startsWith('env.BRANCH_PREFIX', env.GITHUB_REF) 32 | run: | 33 | BRANCH_NAME=${GITHUB_REF#refs/heads/} 34 | echo "::set-output name=version::${BRANCH_NAME/${{ env.BRANCH_PREFIX }}/}" 35 | 36 | - name: Setup SSH config for builders 37 | env: 38 | BUILDER_PPC64LE_SSH_CONFIG: ${{ secrets.BUILDER_PPC64LE_SSH_CONFIG }} 39 | BUILDER_PPC64LE_SSH_KEY: ${{ secrets.BUILDER_PPC64LE_SSH_KEY }} 40 | BUILDER_PPC64LE_SSH_KNOWN_HOSTS: ${{ secrets.BUILDER_PPC64LE_SSH_KNOWN_HOSTS }} 41 | BUILDER_S390X_SSH_CONFIG: ${{ secrets.BUILDER_S390X_SSH_CONFIG }} 42 | BUILDER_S390X_SSH_KEY: ${{ secrets.BUILDER_S390X_SSH_KEY }} 43 | run: | 44 | mkdir ~/.ssh 45 | chmod 700 ~/.ssh 46 | touch ~/.ssh/id_builder_ppc64le 47 | chmod 600 ~/.ssh/id_builder_ppc64le 48 | echo "$BUILDER_PPC64LE_SSH_KEY" >~/.ssh/id_builder_ppc64le 49 | touch ~/.ssh/id_builder_s390x 50 | chmod 600 ~/.ssh/id_builder_s390x 51 | echo "$BUILDER_S390X_SSH_KEY" > ~/.ssh/id_builder_s390x 52 | touch ~/.ssh/known_hosts 53 | chmod 600 ~/.ssh/known_hosts 54 | cat >~/.ssh/known_hosts <~/.ssh/config <" 9 | 10 | RUN set -ex\ 11 | ; dnf install -y --setopt=tsflags=nodocs --setopt=skip_missing_names_on_install=False git wget \ 12 | ; dnf -y -q clean all 13 | 14 | COPY --from=build /go/src/bin/quay-builder /usr/local/bin 15 | COPY buildpack/ssh-git.sh / 16 | COPY entrypoint.sh /home/podman/entrypoint.sh 17 | 18 | # Rootless/unprivileged buildah configurations 19 | # https://github.com/containers/buildah/blob/main/docs/tutorials/05-openshift-rootless-build.md 20 | RUN touch /etc/subgid /etc/subuid && \ 21 | chmod g=u /etc/subgid /etc/subuid /etc/passwd && \ 22 | echo 'podman:100000:65536' > /etc/subuid && echo 'podman:100000:65536' > /etc/subgid && \ 23 | # Set driver to VFS, which doesn't require host modifications compared to overlay 24 | # Set shortname aliasing to permissive - https://www.redhat.com/sysadmin/container-image-short-names 25 | mkdir -p /home/podman/.config/containers && \ 26 | (echo '[storage]';echo 'driver = "vfs"') > /home/podman/.config/containers/storage.conf && \ 27 | sed -i 's/short-name-mode="enforcing"/short-name-mode="permissive"/g' /etc/containers/registries.conf && \ 28 | mkdir /certs /home/podman/.config/cni && chown podman:podman /certs /home/podman/.config/cni 29 | 30 | VOLUME [ "/certs" ] 31 | 32 | WORKDIR /home/podman 33 | 34 | USER podman 35 | 36 | ENTRYPOINT ["sh", "/home/podman/entrypoint.sh"] 37 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: vendor test bin/quay-builder 2 | 3 | PROJECT ?= quay-builder 4 | ORG_PATH ?= github.com/quay 5 | REPO_PATH ?= $(ORG_PATH)/$(PROJECT) 6 | IMAGE ?= quay.io/projectquay/$(PROJECT) 7 | VERSION ?= $(shell ./scripts/git-version) 8 | LD_FLAGS ?= "-w -X $(REPO_PATH)/version.Version=$(VERSION)" 9 | IMAGE_TAG ?= latest 10 | SUBSCRIPTION_KEY ?= subscription.pem 11 | BUILD_TAGS ?= 'btrfs_noversion exclude_graphdriver_btrfs exclude_graphdriver_devicemapper containers_image_openpgp' 12 | BUILDER_SRC ?= 'github.com/quay/quay-builder' 13 | 14 | all: vendor test build 15 | 16 | vendor: 17 | @go mod vendor 18 | 19 | test: vendor 20 | @go vet ./... 21 | @go test -v ./... 22 | 23 | build: bin/quay-builder 24 | 25 | bin/quay-builder: 26 | CGO_ENABLED=0 go build -ldflags $(LD_FLAGS) -o bin/quay-builder -tags $(BUILD_TAGS) $(REPO_PATH)/cmd/quay-builder 27 | 28 | install: 29 | go install -ldflags $(LD_FLAGS) $(REPO_PATH)/cmd/quay-builder 30 | 31 | build-ubi8: 32 | docker build --build-arg=BUILDER_SRC=$(BUILDER_SRC) -f Dockerfile -t $(IMAGE):$(IMAGE_TAG)-ubi8 . 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Quay Builder 2 | 3 | This repository is for an automated build worker for a Quay. 4 | 5 | ## Architecture 6 | 7 | There is a client/server relationship between builder and the management server. 8 | Builders are created and connect to the build manager using the GRPC protocol. 9 | Builders are designed to be dynamically created and connect to the management server for a single build and then disappear, 10 | generally on some control plane such as K8s or AWS. 11 | 12 | ## Building the builder 13 | 14 | ``` 15 | make test 16 | make build 17 | ``` 18 | 19 | ## Running the builder 20 | 21 | ### Environment variables 22 | 23 | The builders are bootstrapped and configured using environment variables. These are set when created by the build manager. 24 | The parameters necessary for the actual build are obtained in a subsequent call to the build manager's API 25 | 26 | `CONTAINER_RUNTIME`: "podman" or "docker" 27 | `DOCKER_HOST`: The container runtime socket. Defaults to "unix:///var/run/docker.sock" 28 | `TOKEN`: The registration token needed to get the build args from the build manager 29 | `SERVER`: The build manager's GRPC endpoint. Format: : 30 | `TLS_CERT_PATH`: TLS cert file path (optional) 31 | `INSECURE`: "true" or "false". Of "true" attempt to connect to the build manager without tls. 32 | 33 | ### Container runtimes 34 | 35 | The builder supports Docker and Podman/Buildah to run the builds. The runtime is specified using the `CONTAINER_RUNTIME` and `DOCKER_HOST`. 36 | If these ENV variables are not set, `CONTAINER_RUNTIME` and `DOCKER_HOST` will be set to "docker" and "unix:///var/run/docker.sock", respectively. 37 | If `CONTAINER_RUNTIME` is set to "podman", it is expected that `DOCKER_HOST` is set to podman's equivalent to the docker's docker. e.g unix:///var/run/podman.sock 38 | 39 | ## Building the builder image 40 | 41 | For both images, you can also specify make parameters 42 | 43 | `IMAGE_TAG` ( tag name, default to `latest`) 44 | 45 | `IMAGE` ( repo name, default to `quay.io/quay/quay-builder`) 46 | 47 | and the built image will be tagged with 48 | ``` 49 | :- 50 | ``` 51 | where the `` can be either `alpine` or `centos`. 52 | 53 | ### Building Alpine based image: 54 | ```sh 55 | make build-alpine 56 | ``` 57 | This generates image with tag `quay.io/projectquay/quay-builder:latest-alpine`. 58 | 59 | ### Building CentOS based image: 60 | ```sh 61 | make build-centos 62 | ``` 63 | This generates image with tag `quay.io/projectquay/quay-builder:latest-centos`. 64 | -------------------------------------------------------------------------------- /buildctx/context.go: -------------------------------------------------------------------------------- 1 | package buildctx 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strings" 7 | 8 | log "github.com/sirupsen/logrus" 9 | 10 | uuid "github.com/nu7hatch/gouuid" 11 | "github.com/quay/quay-builder/buildpack" 12 | "github.com/quay/quay-builder/containerclient" 13 | "github.com/quay/quay-builder/containerclient/dockerfile" 14 | "github.com/quay/quay-builder/rpc" 15 | ) 16 | 17 | // scratch is a special case, empty base image. It is not listed after 18 | // executing `docker images`, but `docker pull scratch` will pull the image 19 | // revealing a short ID of "511136ea3c5a" which can be `docker inspect`ed 20 | // to reveal the full image ID. 21 | const ( 22 | scratchImageName = "scratch" 23 | scratchImageID = "511136ea3c5a64f264b78b5433614aec563103b4d4702f3ba7d4d2698e22c158" 24 | ) 25 | 26 | // Context represents the internal state of a build. 27 | type Context struct { 28 | client rpc.Client 29 | writer containerclient.LogWriter 30 | containerClient containerclient.Client 31 | args *rpc.BuildArgs 32 | metadata *dockerfile.Metadata 33 | buildpackDir string 34 | buildID string 35 | cacheTag string 36 | } 37 | 38 | // New connects to the docker daemon and sets up the initial state of a build 39 | // context. 40 | // 41 | // If the connection to the docker daemon fails, exits with log.Fatal. 42 | func New(client rpc.Client, args *rpc.BuildArgs, dockerHost, containerRuntime string) (*Context, error) { 43 | // Connect to the local docker client. 44 | log.Infof("connecting to docker host: %s", dockerHost) 45 | containerClient, err := containerclient.NewClient(dockerHost, containerRuntime) 46 | if err != nil { 47 | log.Fatal(err) 48 | } 49 | log.Infof("connected to docker host: %s", dockerHost) 50 | 51 | return &Context{ 52 | client: client, 53 | writer: containerclient.NewRPCWriter(client, containerRuntime), 54 | containerClient: containerClient, 55 | args: args, 56 | }, nil 57 | } 58 | 59 | // Unpack downloads and expands the buildpack and parses the Dockerfile. 60 | func (bc *Context) Unpack() error { 61 | if err := bc.client.SetPhase(rpc.Unpacking, nil); err != nil { 62 | log.Errorf("failed to update phase to `unpacking`") 63 | return err 64 | } 65 | 66 | // Download and expand the buildpack. 67 | buildpackDir, err := buildpack.Download(bc.args) 68 | if err != nil { 69 | log.Errorf("failed to download buildpack: %v", err) 70 | return err 71 | } 72 | bc.buildpackDir = buildpackDir 73 | 74 | // Parse the Dockerfile. 75 | metadata, err := dockerfile.NewMetadataFromDir(buildpackDir, bc.args.DockerfilePath) 76 | 77 | if err != nil { 78 | log.Errorf("failed to parse dockerfile: %v", err) 79 | return err 80 | } 81 | bc.metadata = metadata 82 | 83 | return nil 84 | } 85 | 86 | // Pull executes "docker pull" for the base image of the build's Dockerfile. 87 | func (bc *Context) Pull() error { 88 | if err := bc.client.SetPhase(rpc.Pulling, &rpc.PullMetadata{ 89 | RegistryURL: bc.args.Registry, 90 | BaseImage: bc.metadata.BaseImage, 91 | BaseImageTag: bc.metadata.BaseImageTag, 92 | PullUsername: bc.args.BaseImage.Username, 93 | }); err != nil { 94 | log.Errorf("failed to update phase to `pulling`") 95 | return err 96 | } 97 | 98 | return pullBaseImage(bc.writer, bc.containerClient, bc.metadata, bc.args) 99 | } 100 | 101 | // Cache calls an RPC to the BuildManager to find the best tag to pull for 102 | // caching and then "docker pull"s it. 103 | func (bc *Context) Cache() error { 104 | // Attempts to update the phase to checking cache. 105 | // We don't handle the error here, as we currently consider the rpc.CheckingCache phase pulling, 106 | // SetPhase will return an rpc.ErrClientRejectedPhaseTransition, since the phase will not change 107 | // from the previous one (rpc.CheckingCache). 108 | bc.client.SetPhase(rpc.CheckingCache, nil) 109 | 110 | // Attempt to calculate the optimal tag. If we cannot find a tag, then caching is simply 111 | // skipped. 112 | cachedTag, err := findCachedTag(bc.writer, bc.client, bc.containerClient, bc.metadata) 113 | if err != nil { 114 | log.Warningf("Failed to lookup caching tag: %v", err) 115 | return nil 116 | } 117 | 118 | // Conduct a pull of the existing tag (if any). This will prime the cache. 119 | if bc.args.PullToken != "" && cachedTag != "" { 120 | bc.client.SetPhase(rpc.PrimingCache, nil) 121 | 122 | err = primeCache(bc.writer, bc.containerClient, bc.args, cachedTag) 123 | if err != nil { 124 | log.Warningf("Error priming cache: %s", err.Error()) 125 | } else { 126 | bc.cacheTag = cachedTag 127 | } 128 | } 129 | 130 | return nil 131 | } 132 | 133 | // Build performs a "docker build". 134 | func (bc *Context) Build() error { 135 | if err := bc.client.SetPhase(rpc.Building, nil); err != nil { 136 | log.Errorf("failed to update phase to `building`") 137 | return err 138 | } 139 | 140 | // Clean up the buildpack. 141 | defer func() { 142 | if err := os.RemoveAll(bc.buildpackDir); err != nil { 143 | log.Errorf("failed to remove buildpack from filesystem: %s", err) 144 | } else { 145 | log.Infof("removed build dir: %s", bc.buildpackDir) 146 | } 147 | }() 148 | var err error 149 | bc.buildID, err = executeBuild(bc.writer, bc.containerClient, bc.buildpackDir, 150 | bc.args.DockerfilePath, bc.args.FullRepoName(), bc.cacheTag) 151 | return err 152 | } 153 | 154 | // Push executes "docker push" and builds a successful call result if no 155 | // failures occur. 156 | func (bc *Context) Push() (*rpc.BuildMetadata, error) { 157 | if err := bc.client.SetPhase(rpc.Pushing, nil); err != nil { 158 | log.Errorf("failed to update phase to `pushing`") 159 | return nil, err 160 | } 161 | 162 | imageID, digests, err := pushBuiltImage(bc.writer, bc.containerClient, bc.args, bc.buildID) 163 | if err != nil { 164 | return nil, err 165 | } 166 | 167 | return &rpc.BuildMetadata{ImageID: imageID, Digests: digests}, nil 168 | } 169 | 170 | // retryDockerRequest retries attempts to execute a closure that alters that 171 | // state of the docker daemon until it succeeds. 172 | func retryDockerRequest(w containerclient.LogWriter, requestFunc func() error) (err error) { 173 | for i := 0; i < 3; i++ { 174 | // Explicitly throw away the errors from any previous attempts to pull. 175 | w.ResetError() 176 | 177 | err = requestFunc() 178 | rerr, hasResponseError := w.ErrResponse() 179 | if err == nil && !hasResponseError { 180 | return nil 181 | } 182 | 183 | log.Infof("failed docker request attempt #%d: err: %s err response %s", i, err, rerr) 184 | if i == 2 { 185 | if err != nil { 186 | return err 187 | } 188 | 189 | return rerr 190 | } 191 | } 192 | 193 | return nil 194 | } 195 | 196 | func primeCache(w containerclient.LogWriter, containerClient containerclient.Client, args *rpc.BuildArgs, cachedTag string) error { 197 | if cachedTag == "" { 198 | // There's nothing to do! 199 | return nil 200 | } 201 | 202 | log.Infof("priming cache with image %s:%s", args.Repository, cachedTag) 203 | 204 | // Attempt to pull the existing tag (if any) three times. 205 | err := retryDockerRequest(w, func() error { 206 | return containerClient.PullImage( 207 | containerclient.PullImageOptions{ 208 | Repository: args.FullRepoName(), 209 | Registry: args.Registry, 210 | Tag: cachedTag, 211 | OutputStream: w, 212 | }, 213 | containerclient.AuthConfiguration{ 214 | Username: "$token", 215 | Password: args.PullToken, 216 | }, 217 | ) 218 | }) 219 | if err != nil { 220 | return rpc.CannotPullForCacheError{Err: err.Error()} 221 | } 222 | 223 | return nil 224 | } 225 | 226 | func pullBaseImage(w containerclient.LogWriter, containerClient containerclient.Client, df *dockerfile.Metadata, args *rpc.BuildArgs) error { 227 | // Skip pulling the base image if it's "scratch" which is a built-in image 228 | // that throws an error after executing `docker pull`. 229 | if df.BaseImage == scratchImageName { 230 | return nil 231 | } 232 | 233 | pullOptions := containerclient.PullImageOptions{ 234 | Registry: args.Registry, 235 | Repository: df.BaseImage, 236 | Tag: df.BaseImageTag, 237 | OutputStream: w, 238 | } 239 | 240 | // Only pull the base image with auth when it is in our own registry. 241 | var pullAuth containerclient.AuthConfiguration 242 | var usesAuth bool 243 | if args.BaseImage.Username != "" && strings.Index(df.BaseImage, args.Registry) == 0 { 244 | pullAuth = containerclient.AuthConfiguration{ 245 | Username: args.BaseImage.Username, 246 | Password: args.BaseImage.Password, 247 | } 248 | usesAuth = true 249 | } 250 | 251 | log.Infof("pulling base image %s:%s (with auth: %t)", df.BaseImage, df.BaseImageTag, usesAuth) 252 | 253 | // Attempt to pull an image three times. 254 | err := retryDockerRequest(w, func() error { 255 | return containerClient.PullImage(pullOptions, pullAuth) 256 | }) 257 | if err != nil { 258 | return rpc.PullError{Err: err.Error()} 259 | } 260 | 261 | return nil 262 | } 263 | 264 | func findCachedTag(w containerclient.LogWriter, client rpc.Client, containerClient containerclient.Client, df *dockerfile.Metadata) (string, error) { 265 | log.Infof("querying Docker for the ID of the pulled base image: %s:%s", df.BaseImage, df.BaseImageTag) 266 | var baseImageID string 267 | if df.BaseImage == scratchImageName { 268 | // scratch is a builtin image that must be manually assigned its proper ID. 269 | baseImageID = scratchImageID 270 | } else { 271 | baseImage, err := containerClient.InspectImage(df.BaseImage + ":" + df.BaseImageTag) 272 | if err != nil { 273 | // TODO(jzelinskie): maybe make this non-fatal 274 | return "", err 275 | } 276 | 277 | if rerr, hasResponseError := w.ErrResponse(); hasResponseError { 278 | // TODO(jzelinskie): maybe make this non-fatal 279 | return "", rerr 280 | } 281 | 282 | baseImageID = baseImage.ID 283 | } 284 | 285 | log.Infof("querying BuildManager for most similar tag") 286 | return client.FindMostSimilarTag(rpc.TagMetadata{ 287 | BaseImage: df.BaseImage, 288 | BaseImageTag: df.BaseImageTag, 289 | BaseImageID: baseImageID, 290 | }) 291 | } 292 | 293 | func pushBuiltImage(w containerclient.LogWriter, containerClient containerclient.Client, args *rpc.BuildArgs, imageID string) (string, []string, error) { 294 | // Push each new tag for the image. 295 | for _, tagName := range args.TagNames { 296 | // Setup tag options. 297 | tagOptions := containerclient.TagImageOptions{ 298 | Repository: args.FullRepoName(), 299 | Tag: tagName, 300 | Force: true, 301 | } 302 | 303 | // Tag the image. 304 | log.Infof("tagging image %s as %s:%s", imageID, args.FullRepoName(), tagName) 305 | err := containerClient.TagImage(imageID, tagOptions) 306 | if err != nil { 307 | return "", nil, rpc.TagError{Err: err.Error()} 308 | } 309 | 310 | if rerr, hasResponseError := w.ErrResponse(); hasResponseError { 311 | return "", nil, rpc.TagError{Err: rerr.Error()} 312 | } 313 | 314 | fullyQualifiedName := args.FullRepoName() + ":" + tagName 315 | log.Infof("pushing image %s (%s)", fullyQualifiedName, imageID) 316 | err = retryDockerRequest(w, func() error { 317 | return containerClient.PushImage( 318 | containerclient.PushImageOptions{ 319 | Repository: args.FullRepoName(), 320 | Registry: args.Registry, 321 | Tag: tagName, 322 | OutputStream: w, 323 | }, 324 | containerclient.AuthConfiguration{ 325 | Username: "$token", 326 | Password: args.PushToken, 327 | }, 328 | ) 329 | }) 330 | if err != nil { 331 | return "", nil, rpc.PushError{Err: err.Error()} 332 | } 333 | 334 | log.Infof("successfully pushed %s", fullyQualifiedName) 335 | } 336 | 337 | // Find the image built. 338 | dockerImage, err := containerClient.InspectImage(imageID) 339 | if err != nil { 340 | return "", nil, rpc.TagError{Err: err.Error()} 341 | } 342 | 343 | if rerr, hasResponseError := w.ErrResponse(); hasResponseError { 344 | return "", nil, rpc.TagError{Err: rerr.Error()} 345 | } 346 | 347 | return dockerImage.ID, dockerImage.RepoDigests, nil 348 | } 349 | 350 | // Cleanup attempts to remove all the images associated with the build. 351 | func (bc *Context) Cleanup(builtImageID string) error { 352 | // Remove the cached image (if any). 353 | if bc.cacheTag != "" { 354 | cacheImage := fmt.Sprintf("%s:%s", bc.args.FullRepoName(), bc.cacheTag) 355 | err := bc.containerClient.RemoveImageExtended(cacheImage, containerclient.RemoveImageOptions{ 356 | Force: true, 357 | }) 358 | if err != nil { 359 | log.Warningf("Could not remove cached image %s: %v", cacheImage, err) 360 | } 361 | } 362 | 363 | // Remove the base image. 364 | baseImage := bc.metadata.BaseImage 365 | if bc.metadata.BaseImageTag != "" { 366 | baseImage = fmt.Sprintf("%s:%s", baseImage, bc.metadata.BaseImageTag) 367 | } 368 | err := bc.containerClient.RemoveImageExtended(baseImage, containerclient.RemoveImageOptions{ 369 | Force: true, 370 | }) 371 | if err != nil { 372 | log.Warningf("Could not remove base image %s: %v", baseImage, err) 373 | } 374 | 375 | // Remove the built image. 376 | brerr := bc.containerClient.RemoveImageExtended(builtImageID, containerclient.RemoveImageOptions{ 377 | Force: true, 378 | }) 379 | if brerr != nil { 380 | log.Warningf("Could not remove built image %s: %v", builtImageID, brerr) 381 | } 382 | 383 | // Prune any other images. 384 | _, perr := bc.containerClient.PruneImages(containerclient.PruneImagesOptions{}) 385 | if perr != nil { 386 | log.Warningf("Could not prune images: %v", perr) 387 | } 388 | 389 | return nil 390 | } 391 | 392 | func executeBuild(w containerclient.LogWriter, containerClient containerclient.Client, buildPackageDirectory string, dockerFileName string, repo string, cacheTag string) (string, error) { 393 | buildUUID, err := uuid.NewV4() 394 | if err != nil { 395 | return "", err 396 | } 397 | buildID := buildUUID.String() 398 | 399 | log.Infof("executing build with ID %s", buildID) 400 | 401 | cacheFrom := []string{} 402 | if cacheTag != "" { 403 | cachedImage := repo + ":" + cacheTag 404 | cacheFrom = []string{cachedImage} 405 | log.Infof("using cache image %s", cachedImage) 406 | } 407 | 408 | err = containerClient.BuildImage(containerclient.BuildImageOptions{ 409 | Name: buildID, 410 | NoCache: false, 411 | CacheFrom: cacheFrom, 412 | SuppressOutput: false, 413 | RmTmpContainer: true, 414 | ForceRmTmpContainer: true, 415 | OutputStream: w, 416 | Dockerfile: dockerFileName, // Required for .dockerignore to work 417 | ContextDir: buildPackageDirectory, 418 | }) 419 | if err != nil { 420 | return "", rpc.BuildError{Err: err.Error()} 421 | } 422 | 423 | if rerr, hasResponseError := w.ErrResponse(); hasResponseError { 424 | return "", rpc.BuildError{Err: rerr.Error()} 425 | } 426 | 427 | return buildID, nil 428 | } 429 | -------------------------------------------------------------------------------- /buildpack/buildpack.go: -------------------------------------------------------------------------------- 1 | package buildpack 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "errors" 7 | "fmt" 8 | "io" 9 | "io/ioutil" 10 | "net/http" 11 | "os" 12 | "os/exec" 13 | "path/filepath" 14 | "strings" 15 | "time" 16 | 17 | "code.cloudfoundry.org/archiver/extractor" 18 | log "github.com/sirupsen/logrus" 19 | 20 | "github.com/quay/quay-builder/rpc" 21 | ) 22 | 23 | const ( 24 | quayDocsSubmoduleURL = "http://docs.quay.io/guides/git-submodules.html" 25 | processIdleTimeout = time.Minute * 3 26 | processTimeout = time.Minute * 15 27 | ) 28 | 29 | // Download downloads the build package found at the given URL, returning the 30 | // path to a temporary directory on the file system with those contents, 31 | // extracted if necessary. 32 | func Download(args *rpc.BuildArgs) (string, error) { 33 | var buildPackDir string 34 | 35 | switch { 36 | // Clone the git repository. 37 | case args.Git != nil: 38 | log.Infof("cloning buildpack: %s at %s", args.Git.SHA, args.Git.URL) 39 | repoDir, err := Clone(args.Git.URL, args.Git.SHA, args.Git.PrivateKey) 40 | if err != nil { 41 | return "", err 42 | } 43 | buildPackDir = repoDir 44 | 45 | // Download the buildpack. 46 | case args.BuildPackage != "": 47 | log.Infof("downloading buildpack: %s", args.BuildPackage) 48 | bpDir, err := download(args.BuildPackage) 49 | if err != nil { 50 | return "", err 51 | } 52 | buildPackDir = bpDir 53 | 54 | // This should never happen! 55 | default: 56 | log.Errorf("insufficient buildpack args: %#v", args) 57 | return "", rpc.BuildPackError{Err: "insufficient buildpack args"} 58 | } 59 | 60 | return filepath.Join(buildPackDir, args.Context), nil 61 | } 62 | 63 | // download downloads (and potentially extracts) non-git buildpacks. 64 | func download(url string) (string, error) { 65 | // Load the build package from the URL. 66 | resp, err := http.Get(url) 67 | if err != nil { 68 | return "", rpc.BuildPackError{Err: err.Error()} 69 | } 70 | defer resp.Body.Close() 71 | 72 | // Find the MIME type of the build package and use it to untar/unzip. 73 | mimetype := resp.Header.Get("Content-Type") 74 | 75 | r := resp.Body 76 | if mimetype == "" { 77 | br := bufio.NewReaderSize(resp.Body, 4096) 78 | r = struct { 79 | *bufio.Reader 80 | io.Closer 81 | }{ 82 | Reader: br, 83 | Closer: resp.Body, 84 | } 85 | const detectContentLen = 512 86 | b, _ := br.Peek(detectContentLen) 87 | mimetype = http.DetectContentType(b) 88 | } 89 | 90 | // Remove any extra data found on the mimetype (i.e. charset). 91 | mimetype = strings.Split(mimetype, ";")[0] 92 | 93 | // Extract the build package into a temp folder. 94 | return extractBuildPackage(r, mimetype) 95 | } 96 | 97 | // extractToTempDir extracts a body into a temporary directory and returns the path. 98 | func extractToTempDir(body io.Reader, xtractor extractor.Extractor) (string, error) { 99 | // Create a temporary file. 100 | archiveFile, err := ioutil.TempFile("", "build_archive") 101 | if err != nil { 102 | return "", err 103 | } 104 | defer archiveFile.Close() 105 | 106 | // Copy the build archive to a temporary file (this forces the actual 107 | // downloading of the file if body is http.Request.Body). 108 | _, err = io.Copy(archiveFile, body) 109 | if err != nil { 110 | return "", err 111 | } 112 | 113 | // Create a temporary directory for the build pack. 114 | tempDir, err := ioutil.TempDir("", "build_pack") 115 | if err != nil { 116 | return "", err 117 | } 118 | 119 | // Extract the contents of the archive into the temporary directory. 120 | err = xtractor.Extract(archiveFile.Name(), tempDir) 121 | if err != nil { 122 | return "", err 123 | } 124 | 125 | return tempDir, nil 126 | } 127 | 128 | // dockerfileTempDir creates a temporary directory, copying over the dockerfile. 129 | func dockerfileTempDir(dockerfile io.Reader) (string, error) { 130 | // Create a directory containing the Dockerfile directly. 131 | tempDir, err := ioutil.TempDir("", "build_pack") 132 | if err != nil { 133 | return "", err 134 | } 135 | 136 | // Create the Dockerfile inside the directory. 137 | fo, err := os.Create(tempDir + "/Dockerfile") 138 | if err != nil { 139 | return "", err 140 | } 141 | defer fo.Close() 142 | 143 | // Read the Dockerfile bytes. 144 | bytes, err := ioutil.ReadAll(dockerfile) 145 | if err != nil { 146 | return "", err 147 | } 148 | 149 | // Write the contents of the Dockerfile. 150 | _, err = fo.Write(bytes) 151 | if err != nil { 152 | return "", err 153 | } 154 | 155 | // Return the directory. 156 | return tempDir, nil 157 | } 158 | 159 | // extractBuildPackage extracts body into a temporary directory and returns the path. 160 | func extractBuildPackage(body io.Reader, mimetype string) (string, error) { 161 | switch mimetype { 162 | case "application/zip", "application/x-zip-compressed": 163 | log.Info("buildpack identified as zip") 164 | dir, err := extractToTempDir(body, extractor.NewZip()) 165 | if err != nil { 166 | return "", rpc.BuildPackError{Err: err.Error()} 167 | } 168 | return dir, nil 169 | 170 | case "application/x-tar", "application/gzip", "application/x-gzip": 171 | log.Info("buildpack identified as tar") 172 | dir, err := extractToTempDir(body, extractor.NewTgz()) 173 | if err != nil { 174 | return "", rpc.BuildPackError{Err: err.Error()} 175 | } 176 | return dir, nil 177 | 178 | case "text/plain", "application/octet-stream": 179 | log.Info("buildpack identified as plain") 180 | dir, err := dockerfileTempDir(body) 181 | if err != nil { 182 | return "", rpc.BuildPackError{Err: err.Error()} 183 | } 184 | return dir, nil 185 | } 186 | 187 | return "", rpc.InvalidDockerfileError{Err: "Unsupported kind of build package: " + mimetype} 188 | } 189 | 190 | // Clone creates a temporary directory and `git clone`s a repository into it. 191 | func Clone(url, sha, privateKey string) (string, error) { 192 | // Create a temp file for the ssh key. 193 | keyFile, err := ioutil.TempFile("", "ssh_key") 194 | if err != nil { 195 | return "", err 196 | } 197 | 198 | keyPath := keyFile.Name() 199 | 200 | // When this function is finished executing, close is guaranteed to execute 201 | // first, then the remove. 202 | defer os.Remove(keyPath) 203 | defer keyFile.Close() 204 | 205 | // Give the file the proper permissions. 206 | err = keyFile.Chmod(0600) 207 | if err != nil { 208 | return "", err 209 | } 210 | 211 | // Write the key to the file. 212 | _, err = io.WriteString(keyFile, privateKey) 213 | if err != nil { 214 | return "", err 215 | } 216 | 217 | // Create a temp directory to clone the buildpack into. 218 | bpPath, err := ioutil.TempDir("", "build_pack") 219 | if err != nil { 220 | return "", err 221 | } 222 | 223 | // In order to specify ssh keys per clone, we use option 1 of 224 | // https://gist.github.com/jzelinskie/1460b991a87220cc8adb 225 | // We assume that ssh-git.sh is located at the root of the filesystem. 226 | err = os.Setenv("GIT_SSH", "/ssh-git.sh") 227 | if err != nil { 228 | return "", err 229 | } 230 | err = os.Setenv("PKEY", keyPath) 231 | if err != nil { 232 | return "", err 233 | } 234 | 235 | // Clone into the temp directory by shelling out to git. 236 | output, err := timeoutActiveCommand("git", "clone", "--progress", url, bpPath) 237 | if err != nil { 238 | if err == ErrKilledInactiveProcess { 239 | return "", rpc.GitCloneError{Err: fmt.Sprintf("Timed out while trying to cloning git repository\n%s", output)} 240 | } 241 | return "", rpc.GitCloneError{Err: fmt.Sprintf("Error cloning git repository (%s)\n%s", err, output)} 242 | } 243 | log.Infof("git clone output: %s", output) 244 | 245 | // cd into the build package. 246 | // I really wish we didn't have to do this, but `git submodule` fails to find 247 | // the work tree when you give it envvars or parameters for GIT_DIR and 248 | // GIT_WORK_TREE. 249 | err = os.Chdir(bpPath) 250 | if err != nil { 251 | log.Fatalf("Error changing directory: %s", err) 252 | } 253 | 254 | // Defer cding into a directory that isn't a build package. Buildpack 255 | // directories are eventually deleted and we don't want to get any weird 256 | // behavior from executing in a removed directory. 257 | defer func() { 258 | err = os.Chdir("/") 259 | if err != nil { 260 | log.Fatalf("Error changing directory: %s", err) 261 | } 262 | }() 263 | 264 | // Checkout the specific SHA for the build. 265 | output, err = timeoutActiveCommand("git", "checkout", sha) 266 | if err != nil { 267 | if err == ErrKilledInactiveProcess { 268 | return "", rpc.GitCloneError{Err: fmt.Sprintf("Timed out while trying to checkout SHA %s in git repository\n%s", sha, output)} 269 | } 270 | return "", rpc.GitCheckoutError{Err: fmt.Sprintf("Error checking out git commit (%s)\n%s", err, output)} 271 | } 272 | log.Infof("git checkout output: %s", output) 273 | 274 | // Initialize any submodules. This will still have an exit code of 0 if there 275 | // are no submodules. 276 | output, err = timeoutCommand("git", "submodule", "update", "--init", "--recursive") 277 | if err != nil { 278 | if err == ErrKilledInactiveProcess { 279 | return "", rpc.GitCloneError{Err: fmt.Sprintf("Timed out while trying to update submodules in git repository\n%s", output)} 280 | } 281 | return "", rpc.GitCheckoutError{Err: fmt.Sprintf("Error initializing git submodules (%s): See submodule documentation at %s\n%s", err, quayDocsSubmoduleURL, output)} 282 | } 283 | log.Infof("git submodule output: %s", output) 284 | 285 | return bpPath, nil 286 | } 287 | 288 | type notifyingWriter struct { 289 | notifyChan chan error 290 | buf *bytes.Buffer 291 | } 292 | 293 | func (w notifyingWriter) Write(p []byte) (n int, err error) { 294 | n, err = w.buf.Write(p) 295 | w.notifyChan <- err 296 | return 297 | } 298 | 299 | // ErrKilledInactiveProcess is used to indicate that a subprocess was killed 300 | // due to a timeout. 301 | var ErrKilledInactiveProcess = errors.New("killed process due to inactivity") 302 | 303 | // timeoutCommand executes a command and kills the process if it doesn't exit 304 | // before processTimeout. It should only be used if the command doesn't write 305 | // frequently enough to standard out, thus timeoutActiveCommand cannot be used. 306 | func timeoutCommand(command ...string) ([]byte, error) { 307 | if len(command) <= 0 { 308 | panic("buildpack: not enough arguments provided to timeoutCommand") 309 | } 310 | 311 | cmd := exec.Command(command[0], command[1:]...) 312 | 313 | type execResponse struct { 314 | data []byte 315 | err error 316 | } 317 | 318 | done := make(chan *execResponse) 319 | go func() { 320 | data, err := cmd.CombinedOutput() 321 | done <- &execResponse{data, err} 322 | }() 323 | 324 | select { 325 | case resp := <-done: 326 | return resp.data, resp.err 327 | case <-time.Tick(processTimeout): 328 | log.Warningf("command `%v` timed out after %v\n", command, processTimeout) 329 | if err := cmd.Process.Kill(); err != nil { 330 | log.Fatalf("failed to kill long-running process: %s", err) 331 | } 332 | return nil, ErrKilledInactiveProcess 333 | } 334 | } 335 | 336 | // timeoutActiveCommand executes a commmand and kills the process if it doesn't 337 | // output anything for more than the duration of processIdleTimeout. 338 | // This function panics if you don't provide at least one string for commands. 339 | func timeoutActiveCommand(command ...string) ([]byte, error) { 340 | if len(command) <= 0 { 341 | panic("buildpack: not enough arguments provided to timeoutActiveCommand") 342 | } 343 | 344 | cmd := exec.Command(command[0], command[1:]...) 345 | 346 | notifyChan := make(chan error, 1) 347 | notifyWriter := notifyingWriter{notifyChan, new(bytes.Buffer)} 348 | 349 | cmd.Stdout = ¬ifyWriter 350 | cmd.Stderr = ¬ifyWriter 351 | 352 | doneChan := make(chan error, 1) 353 | go func() { 354 | err := cmd.Run() 355 | doneChan <- err 356 | }() 357 | 358 | timeout := time.After(processIdleTimeout) 359 | for { 360 | select { 361 | case err := <-notifyChan: 362 | if err != nil { 363 | return notifyWriter.buf.Bytes(), err 364 | } 365 | 366 | timeout = time.After(processIdleTimeout) 367 | 368 | case <-timeout: 369 | log.Warningf("active command `%v` timed out after %v with output: %s\n", command, processTimeout, notifyWriter.buf.String()) 370 | if err := cmd.Process.Kill(); err != nil { 371 | log.Fatalf("failed to kill hung process: %s", err) 372 | } 373 | return notifyWriter.buf.Bytes(), ErrKilledInactiveProcess 374 | 375 | case err := <-doneChan: 376 | return notifyWriter.buf.Bytes(), err 377 | } 378 | } 379 | } 380 | -------------------------------------------------------------------------------- /buildpack/buildpack_test.go: -------------------------------------------------------------------------------- 1 | package buildpack 2 | 3 | import ( 4 | "bytes" 5 | "io/ioutil" 6 | "net/http" 7 | "net/http/httptest" 8 | "os" 9 | "path/filepath" 10 | "testing" 11 | ) 12 | 13 | var ( 14 | gzippedDockerfile = []byte{31, 139, 8, 0, 19, 247, 182, 84, 0, 3, 237, 207, 187, 10, 194, 64, 16, 133, 225, 212, 62, 197, 130, 141, 118, 179, 183, 44, 88, 251, 34, 34, 187, 176, 222, 34, 201, 230, 253, 141, 46, 104, 23, 108, 130, 8, 255, 215, 28, 134, 153, 226, 204, 190, 59, 158, 99, 159, 242, 37, 54, 139, 17, 145, 214, 57, 245, 204, 208, 250, 87, 138, 169, 115, 165, 189, 210, 198, 121, 239, 131, 53, 198, 42, 209, 86, 187, 208, 40, 89, 174, 210, 199, 56, 148, 67, 63, 85, 57, 229, 107, 158, 187, 155, 206, 82, 154, 217, 215, 79, 212, 59, 255, 196, 122, 115, 235, 238, 219, 157, 42, 113, 40, 171, 95, 151, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 124, 237, 1, 92, 183, 98, 255, 0, 40, 0, 0} 15 | 16 | zippedDockerfile = []byte{80, 75, 3, 4, 10, 0, 0, 0, 0, 0, 224, 144, 46, 70, 37, 169, 13, 221, 13, 0, 0, 0, 13, 0, 0, 0, 10, 0, 28, 0, 68, 111, 99, 107, 101, 114, 102, 105, 108, 101, 85, 84, 9, 0, 3, 147, 246, 182, 84, 147, 246, 182, 84, 117, 120, 11, 0, 1, 4, 245, 1, 0, 0, 4, 20, 0, 0, 0, 35, 40, 110, 111, 112, 41, 58, 32, 116, 101, 115, 116, 10, 80, 75, 1, 2, 30, 3, 10, 0, 0, 0, 0, 0, 224, 144, 46, 70, 37, 169, 13, 221, 13, 0, 0, 0, 13, 0, 0, 0, 10, 0, 24, 0, 0, 0, 0, 0, 1, 0, 0, 0, 164, 129, 0, 0, 0, 0, 68, 111, 99, 107, 101, 114, 102, 105, 108, 101, 85, 84, 5, 0, 3, 147, 246, 182, 84, 117, 120, 11, 0, 1, 4, 245, 1, 0, 0, 4, 20, 0, 0, 0, 80, 75, 5, 6, 0, 0, 0, 0, 1, 0, 1, 0, 80, 0, 0, 0, 81, 0, 0, 0, 0, 0} 17 | 18 | dockerfileContents = []byte("#(nop): test dockerfile") 19 | 20 | extractTests = []struct { 21 | body []byte 22 | mimetype string 23 | expectedFileCount int 24 | }{ 25 | {zippedDockerfile, "application/x-zip-compressed", 2}, 26 | {gzippedDockerfile, "application/gzip", 2}, 27 | {gzippedDockerfile, "application/x-gzip", 2}, 28 | {dockerfileContents, "application/octet-stream", 2}, 29 | {dockerfileContents, "text/plain", 2}, 30 | } 31 | ) 32 | 33 | func TestExtractBuildPackage(t *testing.T) { 34 | for _, tt := range extractTests { 35 | path, err := extractBuildPackage(bytes.NewBuffer(tt.body), tt.mimetype) 36 | if err != nil { 37 | t.Fatal(err) 38 | } 39 | 40 | fileCount := 0 41 | filepath.Walk(path, func(path string, f os.FileInfo, err error) error { 42 | fileCount++ 43 | return nil 44 | }) 45 | 46 | if fileCount != tt.expectedFileCount { 47 | t.Fatalf("Unexpected file count in directory: %s", path) 48 | } 49 | } 50 | } 51 | 52 | // TestDownload tests that an empty Content-Type will auto-detect the mimetype 53 | // based on the first bytes of the body. 54 | func TestDownload(t *testing.T) { 55 | s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 56 | w.Header().Set("Content-Type", "") 57 | w.Write(gzippedDockerfile) 58 | })) 59 | defer s.Close() 60 | path, err := download(s.URL) 61 | if err != nil { 62 | t.Fatal(err) 63 | } 64 | b, err := ioutil.ReadFile(filepath.Join(path, "Dockerfile")) 65 | if err != nil { 66 | t.Error(err) 67 | } else if string(b) != "#(nop): test\n" { 68 | t.Errorf("unexpected contents: %s", b) 69 | } 70 | if err := os.RemoveAll(path); err != nil { 71 | t.Fatal(err) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /buildpack/ssh-git.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | if [ -z "$PKEY" ]; then 3 | # if PKEY is not specified, run ssh using default keyfile 4 | ssh "$@" 5 | else 6 | ssh -i "$PKEY" -o StrictHostKeyChecking=no "$@" 7 | fi 8 | -------------------------------------------------------------------------------- /cmd/quay-builder/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "crypto/tls" 6 | "os" 7 | "strings" 8 | "time" 9 | 10 | log "github.com/sirupsen/logrus" 11 | "google.golang.org/grpc" 12 | "google.golang.org/grpc/credentials" 13 | 14 | "github.com/quay/quay-builder/buildctx" 15 | "github.com/quay/quay-builder/rpc" 16 | "github.com/quay/quay-builder/rpc/grpcbuild" 17 | "github.com/quay/quay-builder/version" 18 | ) 19 | 20 | const ( 21 | connectTimeout = 10 * time.Second 22 | ) 23 | 24 | func main() { 25 | // Grab the environment. 26 | containerRuntime := os.Getenv("CONTAINER_RUNTIME") 27 | dockerHost := os.Getenv("DOCKER_HOST") 28 | token := os.Getenv("TOKEN") 29 | server := os.Getenv("SERVER") 30 | certFile := os.Getenv("TLS_CERT_PATH") 31 | insecure := os.Getenv("INSECURE") 32 | 33 | log.Infof("starting quay-builder: %s", version.Version) 34 | 35 | if server == "" { 36 | log.Fatal("missing or empty SERVER env vars: required format :") 37 | } 38 | 39 | if containerRuntime == "" { 40 | containerRuntime = "docker" 41 | } 42 | 43 | if dockerHost == "" { 44 | dockerHost = "unix:///var/run/docker.sock" 45 | } 46 | 47 | // Connection options 48 | var opts []grpc.DialOption 49 | 50 | // Attempt to load the TLS config. 51 | if len(certFile) > 0 { 52 | tlsCfg, err := credentials.NewClientTLSFromFile(certFile, "") 53 | if err != nil { 54 | log.Fatalf("invalid TLS config: %s", err) 55 | } 56 | opts = append(opts, grpc.WithTransportCredentials(tlsCfg)) 57 | } else if strings.ToLower(insecure) == "true" { 58 | opts = append(opts, grpc.WithInsecure()) 59 | } else { 60 | // Load the default system certs 61 | tlsCfg := credentials.NewTLS(&tls.Config{}) 62 | opts = append(opts, grpc.WithTransportCredentials(tlsCfg)) 63 | } 64 | 65 | // Attempt to connect to gRPC server (blocking) 66 | log.Infof("connecting to gRPC server...: %s", server) 67 | opts = append(opts, grpc.WithBlock(), grpc.WithTimeout(connectTimeout)) 68 | conn, err := grpc.Dial(server, opts...) 69 | if err != nil { 70 | log.Fatalf("failed to dial grpc server: %v", err) 71 | } 72 | defer conn.Close() 73 | 74 | // Create a new RPC client instance 75 | log.Infof("pinging buildmanager...") 76 | ctx, cancel := context.WithCancel(context.Background()) 77 | rpcClient, err := grpcbuild.NewClient(ctx, conn) 78 | defer cancel() 79 | if err != nil { 80 | log.Fatalf("failed to connect to build manager: %s", err) 81 | } 82 | 83 | // Attempt to register the build job from TOKEN 84 | log.Infof("registering job for registration token: %s", token) 85 | buildargs, err := rpcClient.RegisterBuildJob(token) 86 | if err != nil { 87 | log.Fatalf("failed to register job to build manager: %s", err) 88 | } 89 | 90 | // Start heartbeating 91 | log.Infof("starting heartbeat to buildmanager") 92 | hbCtx, hbCancel := context.WithCancel(context.Background()) 93 | defer hbCancel() 94 | go rpcClient.Heartbeat(hbCtx) 95 | 96 | // Start build 97 | log.Infof("starting build") 98 | _, err = build(dockerHost, containerRuntime, rpcClient, buildargs, hbCancel) 99 | if err != nil { 100 | log.Fatalf("failed to build buildpack: %s", err) 101 | } 102 | 103 | log.Infof("done") 104 | } 105 | 106 | func build(dockerHost, containerRuntime string, client rpc.Client, args *rpc.BuildArgs, hbCanceller context.CancelFunc) (*rpc.BuildMetadata, error) { 107 | var buildCtx *buildctx.Context 108 | buildCtx, err := buildctx.New(client, args, dockerHost, containerRuntime) 109 | if err != nil { 110 | return nil, err 111 | } 112 | 113 | // Unpack the buildpack. 114 | log.Infof("build: upacking build") 115 | if err = buildCtx.Unpack(); err != nil { 116 | return nil, err 117 | } 118 | 119 | // Pull the base image. 120 | log.Infof("build: pulling base image") 121 | if err = buildCtx.Pull(); err != nil { 122 | return nil, err 123 | } 124 | 125 | // Prime the cache. 126 | log.Infof("build: priming cache") 127 | if err = buildCtx.Cache(); err != nil { 128 | return nil, err 129 | } 130 | 131 | // Kick off the build. 132 | log.Infof("build: building") 133 | if err = buildCtx.Build(); err != nil { 134 | return nil, err 135 | } 136 | 137 | // Push the newly created image to the requested tag(s). 138 | log.Infof("build: pushing") 139 | bmd, err := buildCtx.Push() 140 | if err != nil { 141 | return nil, err 142 | } 143 | 144 | // Stop heartbeats 145 | hbCanceller() 146 | 147 | // Move build to completed phase 148 | if err := client.SetPhase(rpc.Complete, nil); err != nil { 149 | log.Errorf("failed to update phase to `complete`") 150 | return nil, err 151 | } 152 | 153 | // Cleanup any pulled images. 154 | log.Infof("build: cleanup") 155 | if err = buildCtx.Cleanup(bmd.ImageID); err != nil { 156 | return nil, err 157 | } 158 | 159 | return bmd, nil 160 | } 161 | -------------------------------------------------------------------------------- /cmd/quay-builder/realm_formatter.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import log "github.com/sirupsen/logrus" 4 | 5 | // realmFormatter implements the logrus.Formatter interface such that all 6 | // log messages are prefixed with the realm of the build. 7 | type realmFormatter struct { 8 | realm string 9 | formatter log.Formatter 10 | } 11 | 12 | func newRealmFormatter(realm string) log.Formatter { 13 | return &realmFormatter{ 14 | realm: realm, 15 | formatter: new(log.TextFormatter), 16 | } 17 | } 18 | 19 | func (f *realmFormatter) Format(entry *log.Entry) ([]byte, error) { 20 | if f.realm != "" { 21 | entry.Message = f.realm + ": " + entry.Message 22 | } 23 | return f.formatter.Format(entry) 24 | } 25 | -------------------------------------------------------------------------------- /cmd/quay-builder/tls.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto/tls" 5 | "crypto/x509" 6 | "io/ioutil" 7 | ) 8 | 9 | const serverName = "quay-services" 10 | 11 | // LoadTLSClientConfig initializes a *tls.Config using the given certificates 12 | // and private key, that can be used to communicate with a server using client 13 | // certificate authentication. 14 | // 15 | // If no certificates are given, a nil *tls.Config is returned. 16 | // The CA certificate is optional and falls back to the system default. 17 | func LoadTLSClientConfig(certFile, keyFile, caFile string) (*tls.Config, error) { 18 | if certFile == "" || keyFile == "" { 19 | return nil, nil 20 | } 21 | 22 | cert, err := tls.LoadX509KeyPair(certFile, keyFile) 23 | if err != nil { 24 | return nil, err 25 | } 26 | 27 | var caCertPool *x509.CertPool 28 | if caFile != "" { 29 | caCert, err := ioutil.ReadFile(caFile) 30 | if err != nil { 31 | return nil, err 32 | } 33 | caCertPool = x509.NewCertPool() 34 | caCertPool.AppendCertsFromPEM(caCert) 35 | } 36 | 37 | tlsConfig := &tls.Config{ 38 | ServerName: serverName, 39 | Certificates: []tls.Certificate{cert}, 40 | RootCAs: caCertPool, 41 | } 42 | 43 | return tlsConfig, nil 44 | } 45 | -------------------------------------------------------------------------------- /containerclient/container_client_test.go: -------------------------------------------------------------------------------- 1 | package containerclient 2 | 3 | type TestDockerClient struct { 4 | err error 5 | ImagePulled bool 6 | ImagePushed bool 7 | ImageTagged bool 8 | ImageBuilt bool 9 | ImageRemoved bool 10 | } 11 | 12 | func newTestDockerClient(err error) Client { 13 | return &TestDockerClient{ 14 | err: err, 15 | } 16 | } 17 | 18 | func (c *TestDockerClient) BuildImage(BuildImageOptions) error { 19 | c.ImageBuilt = true 20 | return c.err 21 | } 22 | 23 | func (c *TestDockerClient) PullImage(PullImageOptions, AuthConfiguration) error { 24 | c.ImagePulled = true 25 | return c.err 26 | } 27 | 28 | func (c *TestDockerClient) PushImage(PushImageOptions, AuthConfiguration) error { 29 | c.ImagePushed = true 30 | return c.err 31 | } 32 | 33 | func (c *TestDockerClient) TagImage(string, TagImageOptions) error { 34 | c.ImageTagged = true 35 | return c.err 36 | } 37 | 38 | func (c *TestDockerClient) InspectImage(string) (*Image, error) { 39 | return &Image{ID: ""}, c.err 40 | } 41 | 42 | func (c *TestDockerClient) PruneImages(PruneImagesOptions) (*PruneImagesResults, error) { 43 | return &PruneImagesResults{}, c.err 44 | } 45 | 46 | func (c *TestDockerClient) RemoveImageExtended(string, RemoveImageOptions) error { 47 | c.ImageRemoved = true 48 | return c.err 49 | } 50 | -------------------------------------------------------------------------------- /containerclient/docker_client.go: -------------------------------------------------------------------------------- 1 | package containerclient 2 | 3 | import ( 4 | "crypto/tls" 5 | "crypto/x509" 6 | "io/ioutil" 7 | "net/http" 8 | "net/url" 9 | "os" 10 | 11 | "github.com/fsouza/go-dockerclient" 12 | ) 13 | 14 | func buildTLSTransport(basePath string) (*http.Transport, error) { 15 | roots := x509.NewCertPool() 16 | pemData, err := ioutil.ReadFile(basePath + "/ca.pem") 17 | if err != nil { 18 | return nil, err 19 | } 20 | 21 | // Add the certification to the pool. 22 | roots.AppendCertsFromPEM(pemData) 23 | 24 | // Create the certificate; 25 | crt, err := tls.LoadX509KeyPair(basePath+"/cert.pem", basePath+"/key.pem") 26 | if err != nil { 27 | return nil, err 28 | } 29 | 30 | // Create the new tls configuration using both the authority and certificate. 31 | conf := &tls.Config{ 32 | RootCAs: roots, 33 | Certificates: []tls.Certificate{crt}, 34 | } 35 | 36 | // Create our own transport and return it. 37 | return &http.Transport{ 38 | TLSClientConfig: conf, 39 | }, nil 40 | } 41 | 42 | type dockerClient struct { 43 | client *docker.Client 44 | } 45 | 46 | func NewDockerClient(host string) (*dockerClient, error) { 47 | hostURL, err := url.Parse(host) 48 | if err != nil { 49 | return nil, err 50 | } 51 | 52 | // Change to an https connection if we have a cert path. 53 | if os.Getenv("DOCKER_CERT_PATH") != "" { 54 | hostURL.Scheme = "https" 55 | } 56 | 57 | c, err := docker.NewClient(hostURL.String()) 58 | if err != nil { 59 | return nil, err 60 | } 61 | 62 | // Set the client to use https. 63 | if os.Getenv("DOCKER_CERT_PATH") != "" { 64 | transport, err := buildTLSTransport(os.Getenv("DOCKER_CERT_PATH")) 65 | if err != nil { 66 | return nil, err 67 | } 68 | 69 | c.HTTPClient = &http.Client{Transport: transport} 70 | } 71 | 72 | return &dockerClient{client: c}, nil 73 | } 74 | 75 | func (c *dockerClient) BuildImage(opts BuildImageOptions) error { 76 | return c.client.BuildImage(docker.BuildImageOptions{ 77 | Name: opts.Name, 78 | NoCache: opts.NoCache, 79 | CacheFrom: opts.CacheFrom, 80 | SuppressOutput: opts.SuppressOutput, 81 | RmTmpContainer: opts.RmTmpContainer, 82 | ForceRmTmpContainer: opts.ForceRmTmpContainer, 83 | OutputStream: opts.OutputStream, 84 | RawJSONStream: true, 85 | Dockerfile: opts.Dockerfile, 86 | ContextDir: opts.ContextDir, 87 | }) 88 | } 89 | 90 | func (c *dockerClient) PullImage(opts PullImageOptions, auth AuthConfiguration) error { 91 | return c.client.PullImage( 92 | docker.PullImageOptions{ 93 | Repository: opts.Repository, 94 | Registry: opts.Registry, 95 | Tag: opts.Tag, 96 | OutputStream: opts.OutputStream, 97 | RawJSONStream: true, 98 | }, 99 | docker.AuthConfiguration{ 100 | Username: auth.Username, 101 | Password: auth.Password, 102 | }, 103 | ) 104 | } 105 | 106 | func (c *dockerClient) PushImage(opts PushImageOptions, auth AuthConfiguration) error { 107 | return c.client.PushImage( 108 | docker.PushImageOptions{ 109 | Name: opts.Repository, 110 | Registry: opts.Registry, 111 | Tag: opts.Tag, 112 | OutputStream: opts.OutputStream, 113 | RawJSONStream: true, 114 | }, 115 | docker.AuthConfiguration{ 116 | Username: auth.Username, 117 | Password: auth.Password, 118 | }, 119 | ) 120 | } 121 | 122 | func (c *dockerClient) TagImage(name string, opts TagImageOptions) error { 123 | return c.client.TagImage( 124 | name, 125 | docker.TagImageOptions{ 126 | Repo: opts.Repository, 127 | Tag: opts.Tag, 128 | Force: true, 129 | }, 130 | ) 131 | } 132 | 133 | func (c *dockerClient) InspectImage(name string) (*Image, error) { 134 | dockerImage, err := c.client.InspectImage(name) 135 | if err != nil { 136 | return nil, err 137 | } 138 | return &Image{ 139 | ID: dockerImage.ID, 140 | RepoDigests: dockerImage.RepoDigests, 141 | }, nil 142 | } 143 | 144 | func (c *dockerClient) RemoveImageExtended(name string, opts RemoveImageOptions) error { 145 | return c.client.RemoveImageExtended(name, docker.RemoveImageOptions{Force: opts.Force}) 146 | } 147 | 148 | func (c *dockerClient) PruneImages(opts PruneImagesOptions) (*PruneImagesResults, error) { 149 | pruneImageResults, err := c.client.PruneImages(docker.PruneImagesOptions{Filters: opts.Filters}) 150 | if err != nil { 151 | return nil, err 152 | } 153 | 154 | imagesDeleted := []string{} 155 | for _, img := range pruneImageResults.ImagesDeleted { 156 | // We don't really care if the image was untagged or deleted 157 | if len(img.Untagged) > 0 { 158 | imagesDeleted = append(imagesDeleted, img.Untagged) 159 | } else { 160 | imagesDeleted = append(imagesDeleted, img.Deleted) 161 | } 162 | } 163 | 164 | return &PruneImagesResults{ 165 | ImagesDeleted: imagesDeleted, 166 | SpaceReclaimed: pruneImageResults.SpaceReclaimed, 167 | }, nil 168 | } 169 | -------------------------------------------------------------------------------- /containerclient/docker_log_writer.go: -------------------------------------------------------------------------------- 1 | package containerclient 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "errors" 7 | "io" 8 | "io/ioutil" 9 | "runtime" 10 | 11 | log "github.com/sirupsen/logrus" 12 | 13 | "github.com/quay/quay-builder/rpc" 14 | ) 15 | 16 | const ( 17 | minProgressDelta = 10000000 18 | bufferingStatus = "Buffering to disk" 19 | pushingStatus = "Pushing" 20 | ) 21 | 22 | // partialBuffer represents a buffer of data that was unable to be previously 23 | // serialized because it was not enough data was provided to form valid JSON. 24 | type partialBuffer []byte 25 | 26 | func (pb partialBuffer) hasContents() bool { return len(pb) != 0 } 27 | 28 | func (pb *partialBuffer) set(in []byte) { *pb = in } 29 | 30 | func (pb *partialBuffer) getAndEmpty(in []byte) (ret []byte) { 31 | ret = append(ret, *pb...) 32 | ret = append(ret, in...) 33 | 34 | *pb = []byte{} 35 | return 36 | } 37 | 38 | // DockerRPCWriter implements a RPCWriter that consumes encoded JSON data and buffers it 39 | // until it has a valid JSON object and then logs it to an rpc.Client. 40 | type DockerRPCWriter struct { 41 | client rpc.Client 42 | errResponse *Response 43 | partialBuffer *partialBuffer 44 | hasPartialBuffer bool 45 | } 46 | 47 | // Write implements the io.Writer interface for RPCWriter. 48 | func (w *DockerRPCWriter) Write(p []byte) (n int, err error) { 49 | originalLength := len(p) 50 | 51 | // Note: Sometimes Docker returns to us only the beginning of a stream, 52 | // so we have to prepend any existing data from the previous call. 53 | if w.partialBuffer.hasContents() { 54 | p = w.partialBuffer.getAndEmpty(p) 55 | } 56 | 57 | buf := bytes.NewBuffer(p) 58 | dec := json.NewDecoder(buf) 59 | f := &filter{} 60 | 61 | for { 62 | // Yield to the Go scheduler. Sometimes, when we have very large number of 63 | // messages, we need to yield to ensure that other goroutines are not 64 | // starved (specifically the heartbeat). 65 | runtime.Gosched() 66 | 67 | // Attempt to decode what was written into a Docker Reponse. 68 | var m Response 69 | if err = dec.Decode(&m); err == io.EOF { 70 | break 71 | } else if err == io.ErrUnexpectedEOF { 72 | // If we get an unexpected EOF, it means that the JSON response from 73 | // Docker was too large to fit into the single Write call. Therefore, we 74 | // store any unparsed data and prepend it on the next call. 75 | var bufferedData []byte 76 | bufferedData, err = ioutil.ReadAll(dec.Buffered()) 77 | if err != nil { 78 | log.Fatalf("Error when reading buffered logs: %v", err) 79 | } 80 | w.partialBuffer.set(bufferedData) 81 | break 82 | } else if err != nil { 83 | // Try to determine what we failed to decode. 84 | entry, readErr := ioutil.ReadAll(dec.Buffered()) 85 | if readErr != nil { 86 | entry = []byte("unknown") 87 | } 88 | log.Fatalf("Error when reading logs: %v; Failed entry: %v", err, string(entry)) 89 | } 90 | 91 | if m.Error != "" { 92 | w.errResponse = &m 93 | continue 94 | } 95 | 96 | if f.shouldSkip(&m) { 97 | continue 98 | } 99 | 100 | jsonData, err := json.Marshal(&m) 101 | if err != nil { 102 | log.Fatalf("Error when marshaling logs: %v", err) 103 | } 104 | 105 | err = w.client.PublishBuildLogEntry(string(jsonData)) 106 | if err != nil { 107 | log.Fatalf("Failed to publish log entry: %v", err) 108 | } 109 | } 110 | 111 | return originalLength, nil 112 | } 113 | 114 | // ErrResponse returns an error that occurred from Docker and then calls 115 | // ResetError(). 116 | func (w *DockerRPCWriter) ErrResponse() (error, bool) { 117 | err := w.errResponse 118 | w.ResetError() 119 | 120 | if err == nil { 121 | return nil, false 122 | } 123 | 124 | return errors.New(err.Error), true 125 | } 126 | 127 | // ResetError throws away any error state from previously streamed logs. 128 | func (w *DockerRPCWriter) ResetError() { 129 | w.errResponse = nil 130 | } 131 | 132 | type filter struct { 133 | lastSent *Response 134 | } 135 | 136 | func (f filter) shouldSkip(resp *Response) bool { 137 | if f.lastSent == nil { 138 | f.lastSent = resp 139 | return false 140 | } 141 | 142 | // Don't send the response if it hasn't transfered the minimum amount across 143 | // the docker socket. 144 | if resp.Status == bufferingStatus && f.lastSent.Status == bufferingStatus { 145 | switch { 146 | case resp.ProgressDetail.Current < f.lastSent.ProgressDetail.Current+minProgressDelta: 147 | return true 148 | default: 149 | return false 150 | } 151 | } 152 | 153 | // Don't send the push response unless it has pushed more than the minimum. 154 | if resp.Status == pushingStatus && f.lastSent.Status == pushingStatus { 155 | switch { 156 | case resp.ProgressDetail.Current == f.lastSent.ProgressDetail.Total: 157 | // Always send the final response. 158 | return false 159 | case resp.ProgressDetail.Current < f.lastSent.ProgressDetail.Current+minProgressDelta: 160 | return true 161 | default: 162 | return false 163 | } 164 | } 165 | 166 | return false 167 | } 168 | -------------------------------------------------------------------------------- /containerclient/dockerfile/context.go: -------------------------------------------------------------------------------- 1 | package dockerfile 2 | 3 | import ( 4 | "io/ioutil" 5 | "os" 6 | "path/filepath" 7 | "strings" 8 | "time" 9 | 10 | "github.com/docker/docker/pkg/archive" 11 | "github.com/docker/docker/pkg/tarsum" 12 | ) 13 | 14 | const unix1980 int64 = 315532800 15 | 16 | // loadBuildContext loads the build context into a tarsum. 17 | func loadBuildContext(buildContextDirectory string) (tarsum.TarSum, error) { 18 | // Zero out mtimes so that they don't effect the outcome of our tarsum. 19 | err := chtimesDir(buildContextDirectory) 20 | if err != nil { 21 | return nil, err 22 | } 23 | 24 | // Compress our build context directory into a tar. 25 | tarred, err := archive.Tar(buildContextDirectory, archive.Uncompressed) 26 | if err != nil { 27 | return nil, err 28 | } 29 | 30 | // Create a tarsum of the tar. 31 | buildContext, err := tarsum.NewTarSum(tarred, true, tarsum.Version0) 32 | if err != nil { 33 | return nil, err 34 | } 35 | 36 | // Create a temporary directory. 37 | tmpdirPath, err := ioutil.TempDir("", "docker-build") 38 | if err != nil { 39 | return nil, err 40 | } 41 | defer os.RemoveAll(tmpdirPath) 42 | 43 | // Extract the tar to the temporary directory. 44 | // This is required in order for the tarsum to be calculated. 45 | if err := archive.Untar(buildContext, tmpdirPath, nil); err != nil { 46 | return nil, err 47 | } 48 | 49 | return buildContext, nil 50 | } 51 | 52 | // chtimesDir walks a directory and sets the atime and mtime to 1 Jan 1980 -- 53 | // the earliest timestamp supported by zip. 54 | func chtimesDir(root string) error { 55 | return filepath.Walk(root, walkFunc) 56 | } 57 | 58 | func walkFunc(path string, info os.FileInfo, err error) error { 59 | // This handles any errors we got walking the filesystem. 60 | if err != nil { 61 | return err 62 | } 63 | 64 | err = os.Chtimes(path, time.Unix(unix1980, 0), time.Unix(unix1980, 0)) 65 | 66 | // If we can't find the file we just walked to, it's a broken symlink. 67 | // Just ignore these. 68 | if err != nil && strings.Contains(err.Error(), "no such file or directory") { 69 | err = nil 70 | } 71 | 72 | return err 73 | } 74 | -------------------------------------------------------------------------------- /containerclient/dockerfile/context_test.go: -------------------------------------------------------------------------------- 1 | package dockerfile 2 | 3 | import ( 4 | "errors" 5 | "os" 6 | "testing" 7 | ) 8 | 9 | func TestWalkFunc(t *testing.T) { 10 | table := []struct { 11 | path string 12 | info os.FileInfo 13 | err error 14 | result error 15 | }{ 16 | {"", nil, errors.New("boom"), errors.New("boom")}, 17 | } 18 | 19 | for _, tt := range table { 20 | result := walkFunc(tt.path, tt.info, tt.err) 21 | if result.Error() != tt.result.Error() { 22 | t.Errorf("got %v, wanted %v", result, tt.result) 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /containerclient/dockerfile/dockerfile.go: -------------------------------------------------------------------------------- 1 | package dockerfile 2 | 3 | import ( 4 | "bufio" 5 | "io" 6 | "os" 7 | "path" 8 | "strings" 9 | 10 | "github.com/distribution/reference" 11 | log "github.com/sirupsen/logrus" 12 | 13 | "github.com/moby/buildkit/frontend/dockerfile/instructions" 14 | "github.com/moby/buildkit/frontend/dockerfile/linter" 15 | "github.com/moby/buildkit/frontend/dockerfile/parser" 16 | "github.com/moby/buildkit/frontend/dockerfile/shell" 17 | 18 | "github.com/quay/quay-builder/rpc" 19 | ) 20 | 21 | var ( 22 | // ErrInvalidBuildContext is returned when there is a failure finding the 23 | // location of the source files of an image. 24 | ErrInvalidBuildContext = rpc.InvalidDockerfileError{Err: "Invalid build pack. Are you sure it is a Dockerfile, .tar.gz or .zip archive?"} 25 | 26 | // ErrMissingDockerfile is returned when no Dockerfile was found. 27 | ErrMissingDockerfile = rpc.InvalidDockerfileError{Err: "Missing Dockerfile"} 28 | 29 | // ErrEmptyDockerfile is returned when a parsed Dockerfile has no nodes. 30 | ErrEmptyDockerfile = rpc.InvalidDockerfileError{Err: "Empty Dockerfile"} 31 | 32 | // ErrInvalidDockerfile is returned when a Dockerfile cannot be parsed. 33 | ErrInvalidDockerfile = rpc.InvalidDockerfileError{Err: "Could not parse Dockerfile"} 34 | 35 | // ErrDockerfileMissingFROMorARG is returned the first directive of a Dockerfile 36 | // isn't FROM or ARG. 37 | ErrDockerfileMissingFROMorARG = rpc.InvalidDockerfileError{Err: "First line in Dockerfile isn't FROM or ARG"} 38 | 39 | // ErrInvalidBaseImage is returned when the base image referenced in the FROM 40 | // directive is invalid. 41 | ErrInvalidBaseImage = rpc.InvalidDockerfileError{Err: "FROM line specifies an invalid base image"} 42 | ) 43 | 44 | // Metadata represents a parsed Dockerfile. 45 | type Metadata struct { 46 | BaseImage string 47 | BaseImageTag string 48 | } 49 | 50 | type envGetter struct { 51 | env map[string]string 52 | keys []string 53 | } 54 | 55 | func (e *envGetter) Add(key string, val string) { 56 | e.env[key] = val 57 | e.keys = append(e.keys, key) 58 | } 59 | 60 | func (e *envGetter) Get(key string) (string, bool) { 61 | v, ok := e.env[key] 62 | return v, ok 63 | } 64 | 65 | func (e *envGetter) Keys() []string { 66 | return e.keys 67 | } 68 | 69 | // NewMetadataFromReader parses a Dockerfile reader generates metadata based on 70 | // the contents. 71 | func NewMetadataFromReader(r io.Reader, buildContextDirectory string) (*Metadata, error) { 72 | var imageAndTag string 73 | 74 | // Parse the Dockerfile. 75 | parsed, err := parser.Parse(bufio.NewReader(r)) 76 | if err != nil { 77 | log.Errorf("Could not parse Dockerfile: %v", err) 78 | if strings.Contains(err.Error(), "file with no instructions") { 79 | return nil, ErrEmptyDockerfile 80 | } 81 | return nil, ErrInvalidDockerfile 82 | } 83 | 84 | ast := parsed.AST 85 | if len(ast.Children) == 0 { 86 | return nil, ErrEmptyDockerfile 87 | } 88 | 89 | first_cmd := strings.ToLower(ast.Children[0].Value) 90 | 91 | // Make sure the first command is either FROM or ARG 92 | if first_cmd != "arg" && first_cmd != "from" { 93 | return nil, ErrDockerfileMissingFROMorARG 94 | } 95 | 96 | linter := linter.New(&linter.Config{}) 97 | stages, metaArgs, _ := instructions.Parse(ast, linter) 98 | envGetter := &envGetter{env: map[string]string{}, keys: []string{}} 99 | if first_cmd == "arg" { 100 | for _, metaArg := range metaArgs { 101 | for _, arg := range metaArg.Args { 102 | if arg.Value != nil { 103 | envGetter.Add(arg.Key, *arg.Value) 104 | } 105 | } 106 | } 107 | shlex := shell.NewLex(parsed.EscapeToken) 108 | imageAndTag, _, _ = shlex.ProcessWord(stages[0].BaseName, envGetter) 109 | 110 | } else if first_cmd == "from" { 111 | imageAndTag = stages[0].BaseName 112 | } 113 | 114 | ref, err := reference.Parse(imageAndTag) 115 | if err != nil { 116 | return nil, ErrInvalidBaseImage 117 | } 118 | 119 | // Parse the image name. 120 | var image string 121 | named, ok := ref.(reference.Named) 122 | if !ok { 123 | return nil, ErrInvalidBaseImage 124 | } 125 | image = named.Name() 126 | 127 | // Attempt to parse the tag name. 128 | var tag string 129 | nametag, ok := ref.(reference.NamedTagged) 130 | if ok { 131 | tag = nametag.Tag() 132 | } 133 | if tag == "" { 134 | tag = "latest" 135 | } 136 | 137 | return &Metadata{ 138 | BaseImage: image, 139 | BaseImageTag: tag, 140 | }, nil 141 | } 142 | 143 | // NewMetadataFromDir parses a Dockerfile located within the provided directory 144 | // and generates metadata based on the contents. 145 | func NewMetadataFromDir(buildContextDirectory, dockerfileName string) (*Metadata, error) { 146 | // Load the contents of the Dockerfile. 147 | file, err := os.Open(path.Join(buildContextDirectory, dockerfileName)) 148 | if err != nil { 149 | return nil, ErrMissingDockerfile 150 | } 151 | defer file.Close() 152 | 153 | return NewMetadataFromReader(file, buildContextDirectory) 154 | } 155 | -------------------------------------------------------------------------------- /containerclient/dockerfile/dockerfile_test.go: -------------------------------------------------------------------------------- 1 | package dockerfile 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | ) 7 | 8 | func TestNewMetadataFailures(t *testing.T) { 9 | var table = []struct { 10 | dockerfile string 11 | expectedErr error 12 | expectedErrorMessage string 13 | }{ 14 | {"", ErrEmptyDockerfile, ""}, 15 | {"ADD . .", ErrDockerfileMissingFROMorARG, ""}, 16 | {"FROM /invalid", ErrInvalidBaseImage, ""}, 17 | } 18 | 19 | for _, tt := range table { 20 | t.Run(tt.dockerfile, func(t *testing.T) { 21 | _, err := NewMetadataFromReader(bytes.NewBufferString(tt.dockerfile), ".") 22 | if tt.expectedErrorMessage == "" { 23 | if err != tt.expectedErr { 24 | t.Fatalf("unexpected error: got: %s wanted: %s", err, tt.expectedErr) 25 | } 26 | } else { 27 | if err.Error() != tt.expectedErrorMessage { 28 | t.Fatalf("unexpected error: got: %s wanted: %s", err, tt.expectedErr) 29 | } 30 | } 31 | }) 32 | } 33 | } 34 | 35 | func TestValidParsing(t *testing.T) { 36 | var table = []struct { 37 | dockerfile string 38 | expectedMetadata *Metadata 39 | }{ 40 | // Original format. 41 | { 42 | "FROM jzelinskie:image", 43 | &Metadata{ 44 | BaseImage: "jzelinskie", 45 | BaseImageTag: "image", 46 | }, 47 | }, 48 | { 49 | "FROM jzelinskie:image\nRUN foo", 50 | &Metadata{ 51 | BaseImage: "jzelinskie", 52 | BaseImageTag: "image", 53 | }, 54 | }, 55 | { 56 | "FROM jzelinskie:image\nRUN foo\nADD . .", 57 | &Metadata{ 58 | BaseImage: "jzelinskie", 59 | BaseImageTag: "image", 60 | }, 61 | }, 62 | 63 | // ADD and COPY 64 | { 65 | "FROM jzelinskie:image\nADD moby.txt .\nCOPY moby.txt .", 66 | &Metadata{ 67 | BaseImage: "jzelinskie", 68 | BaseImageTag: "image", 69 | }, 70 | }, 71 | 72 | // Environment variable replacement. 73 | { 74 | "FROM jzelinskie:image\nENV foo=bar\nWORKDIR $foo", 75 | &Metadata{ 76 | BaseImage: "jzelinskie", 77 | BaseImageTag: "image", 78 | }, 79 | }, 80 | { 81 | "FROM jzelinskie:image\nENV foo=bar\nWORKDIR ${foo}", 82 | &Metadata{ 83 | BaseImage: "jzelinskie", 84 | BaseImageTag: "image", 85 | }, 86 | }, 87 | 88 | // Multi-from 89 | { 90 | "FROM jzelinskie:image\nRUN foo\nFROM other:thing\nRUN bar", 91 | &Metadata{ 92 | BaseImage: "jzelinskie", 93 | BaseImageTag: "image", 94 | }, 95 | }, 96 | 97 | // Port in EXPOSE 98 | { 99 | "FROM ubuntu:latest\nENV PORT 3000\nEXPOSE $PORT", 100 | &Metadata{ 101 | BaseImage: "ubuntu", 102 | BaseImageTag: "latest", 103 | }, 104 | }, 105 | } 106 | 107 | for _, tt := range table { 108 | m, err := NewMetadataFromReader(bytes.NewBufferString(tt.dockerfile), "testdata") 109 | if err != nil { 110 | t.Fatalf("unexpected error: got: %s", err) 111 | continue 112 | } 113 | 114 | if m.BaseImage != tt.expectedMetadata.BaseImage { 115 | t.Fatalf("unexpected metadata: got: %s. expected: %s", m, tt.expectedMetadata) 116 | continue 117 | } 118 | 119 | if m.BaseImageTag != tt.expectedMetadata.BaseImageTag { 120 | t.Fatalf("unexpected metadata: got: %s. expected: %s", m, tt.expectedMetadata) 121 | continue 122 | } 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /containerclient/dockerfile/main_test.go: -------------------------------------------------------------------------------- 1 | package dockerfile 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "github.com/docker/docker/pkg/tarsum" 8 | ) 9 | 10 | var mobyHash string 11 | 12 | func TestMain(m *testing.M) { 13 | var err error 14 | 15 | // This needs to happen before using fileHash(). 16 | chtimesDir("testdata") 17 | 18 | mobyHash, err = fileHash(tarsum.Version0, "testdata/moby.txt") 19 | if err != nil { 20 | panic(err) 21 | } 22 | 23 | os.Exit(m.Run()) 24 | } 25 | -------------------------------------------------------------------------------- /containerclient/dockerfile/testdata/moby.txt: -------------------------------------------------------------------------------- 1 | Call me Ishmael. Some years ago--never mind how long 2 | precisely--having little or no money in my purse, and nothing 3 | particular to interest me on shore, I thought I would sail about a 4 | little and see the watery part of the world. It is a way I have of 5 | driving off the spleen and regulating the circulation. Whenever I 6 | find myself growing grim about the mouth; whenever it is a damp, 7 | drizzly November in my soul; whenever I find myself involuntarily 8 | pausing before coffin warehouses, and bringing up the rear of every 9 | funeral I meet; and especially whenever my hypos get such an upper 10 | hand of me, that it requires a strong moral principle to prevent me 11 | from deliberately stepping into the street, and methodically knocking 12 | people's hats off--then, I account it high time to get to sea as soon 13 | as I can. This is my substitute for pistol and ball. With a 14 | philosophical flourish Cato throws himself upon his sword; I quietly 15 | take to the ship. There is nothing surprising in this. If they but 16 | knew it, almost all men in their degree, some time or other, cherish 17 | very nearly the same feelings towards the ocean with me. 18 | -------------------------------------------------------------------------------- /containerclient/dockerfile/util.go: -------------------------------------------------------------------------------- 1 | package dockerfile 2 | 3 | import ( 4 | "archive/tar" 5 | "crypto/sha256" 6 | "encoding/hex" 7 | "errors" 8 | "io" 9 | "os" 10 | "strconv" 11 | "syscall" 12 | 13 | "github.com/docker/docker/pkg/tarsum" 14 | ) 15 | 16 | // quoteSlice calls strconv.Quote on each of the elements in the slice, returning a new 17 | // slice with the values. 18 | func quoteSlice(slice []string) []string { 19 | var parts = make([]string, len(slice)) 20 | for index := range slice { 21 | parts[index] = strconv.Quote(slice[index]) 22 | } 23 | return parts 24 | } 25 | 26 | // fileHash calculates the hash of a standalone regular file for a 27 | // given tarsum version. 28 | func fileHash(v tarsum.Version, name string) (string, error) { 29 | if v != tarsum.Version0 { 30 | return "", errors.New("fileHash: unsupported tarsum version") 31 | } 32 | 33 | f, err := os.Open(name) 34 | if err != nil { 35 | return "", err 36 | } 37 | 38 | defer f.Close() 39 | 40 | fi, err := os.Stat(name) 41 | if err != nil { 42 | return "", err 43 | } 44 | 45 | if !fi.Mode().IsRegular() { 46 | return "", errors.New("fileHash called on non-regular file") 47 | } 48 | 49 | fih, err := tar.FileInfoHeader(fi, "") 50 | if err != nil { 51 | return "", err 52 | } 53 | 54 | header := []string{ 55 | "name", fih.Name, 56 | "mode", strconv.FormatInt(fih.Mode|syscall.S_IFREG, 10), 57 | "uid", strconv.Itoa(fih.Uid), 58 | "gid", strconv.Itoa(fih.Gid), 59 | "size", strconv.FormatInt(fih.Size, 10), 60 | "mtime", strconv.FormatInt(fih.ModTime.UTC().Unix(), 10), 61 | "typeflag", string([]byte{fih.Typeflag}), 62 | "linkname", fih.Linkname, 63 | "uname", fih.Uname, 64 | "gname", fih.Gname, 65 | "devmajor", strconv.FormatInt(fih.Devmajor, 10), 66 | "devminor", strconv.FormatInt(fih.Devminor, 10), 67 | } 68 | 69 | // Could get this from tarsum.DefaultTHash, but this seems fine. 70 | h := sha256.New() 71 | 72 | // Write the header to the hash function. 73 | for _, s := range header { 74 | h.Write([]byte(s)) 75 | } 76 | 77 | // Followed by the body of the file. 78 | if _, err := io.Copy(h, f); err != nil { 79 | return "", err 80 | } 81 | 82 | return hex.EncodeToString(h.Sum(nil)), nil 83 | } 84 | -------------------------------------------------------------------------------- /containerclient/interface.go: -------------------------------------------------------------------------------- 1 | package containerclient 2 | 3 | import ( 4 | "io" 5 | "strings" 6 | 7 | log "github.com/sirupsen/logrus" 8 | 9 | "github.com/quay/quay-builder/rpc" 10 | ) 11 | 12 | type BuildImageOptions struct { 13 | // name:tag 14 | Name string 15 | NoCache bool 16 | CacheFrom []string 17 | SuppressOutput bool 18 | RmTmpContainer bool 19 | ForceRmTmpContainer bool 20 | OutputStream io.Writer 21 | Dockerfile string 22 | ContextDir string 23 | } 24 | 25 | type AuthConfiguration struct { 26 | Username string 27 | Password string 28 | } 29 | 30 | type PullImageOptions struct { 31 | Repository string 32 | Registry string 33 | Tag string 34 | OutputStream io.Writer 35 | } 36 | 37 | type PushImageOptions struct { 38 | Repository string 39 | Registry string 40 | Tag string 41 | OutputStream io.Writer 42 | } 43 | 44 | type TagImageOptions struct { 45 | Repository string 46 | Tag string 47 | Force bool 48 | } 49 | 50 | type Image struct { 51 | ID string 52 | RepoDigests []string 53 | } 54 | 55 | type RemoveImageOptions struct { 56 | Force bool 57 | } 58 | 59 | type PruneImagesOptions struct { 60 | Filters map[string][]string 61 | } 62 | 63 | type PruneImagesResults struct { 64 | ImagesDeleted []string 65 | SpaceReclaimed int64 66 | } 67 | 68 | // Client is an interface for all of the container/image interactions required of a 69 | // worker. This includes Docker and/or Podman 70 | type Client interface { 71 | BuildImage(BuildImageOptions) error 72 | PullImage(PullImageOptions, AuthConfiguration) error 73 | PushImage(PushImageOptions, AuthConfiguration) error 74 | TagImage(string, TagImageOptions) error 75 | InspectImage(string) (*Image, error) 76 | RemoveImageExtended(string, RemoveImageOptions) error 77 | PruneImages(PruneImagesOptions) (*PruneImagesResults, error) 78 | } 79 | 80 | func NewClient(host, containerRuntime string) (Client, error) { 81 | containerRuntime = strings.ToLower(containerRuntime) 82 | if containerRuntime != "docker" && containerRuntime != "podman" { 83 | log.Fatal("Invalid container runtime:", containerRuntime) 84 | } 85 | 86 | if containerRuntime == "docker" { 87 | return NewDockerClient(host) 88 | } else { 89 | return NewPodmanClient(host) 90 | } 91 | } 92 | 93 | // LogWriter represents anything that can stream Docker logs from the daemon 94 | // and check if any error has occured. 95 | type LogWriter interface { 96 | // ErrResponse returns an error that occurred from Docker and resets the 97 | // state of that internal error value to nil. If there is no error, returns 98 | // false as part of the tuple. 99 | ErrResponse() (error, bool) 100 | 101 | // ResetError throws away any error state from previously streamed logs. 102 | ResetError() 103 | 104 | io.Writer 105 | } 106 | 107 | // NewRPCWriter allocates a new Writer that streams logs via an RPC client. 108 | func NewRPCWriter(client rpc.Client, containerRuntime string) LogWriter { 109 | containerRuntime = strings.ToLower(containerRuntime) 110 | if containerRuntime != "docker" && containerRuntime != "podman" { 111 | log.Fatal("Invalid container runtime:", containerRuntime) 112 | } 113 | 114 | if containerRuntime == "docker" { 115 | return &DockerRPCWriter{ 116 | client: client, 117 | partialBuffer: new(partialBuffer), 118 | } 119 | } else { 120 | return &PodmanRPCWriter{ 121 | client: client, 122 | partialBuffer: new(partialBuffer), 123 | } 124 | } 125 | 126 | } 127 | 128 | // Response represents a response from a Docker™ daemon or podman. 129 | type Response struct { 130 | Error string `json:"error,omitempty"` 131 | Stream string `json:"stream,omitempty"` 132 | Status string `json:"status,omitempty"` 133 | ID string `json:"id,omitempty"` 134 | ProgressDetail progressDetail `json:"progressDetail,omitempty"` 135 | } 136 | 137 | // progressDetail represents the progress made by a Docker™ command. 138 | type progressDetail struct { 139 | Current int `json:"current,omitempty"` 140 | Total int `json:"total,omitempty"` 141 | } 142 | -------------------------------------------------------------------------------- /containerclient/log_writer_test.go: -------------------------------------------------------------------------------- 1 | package containerclient 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "testing" 7 | ) 8 | 9 | type testWriter struct { 10 | buf bytes.Buffer 11 | errResponse *Response 12 | } 13 | 14 | func (w testWriter) Write(p []byte) (int, error) { 15 | resp := Response{} 16 | json.Unmarshal(p, &resp) 17 | if resp.Error != "" { 18 | w.errResponse = &resp 19 | } 20 | 21 | return w.buf.Write(p) 22 | } 23 | 24 | func (w testWriter) HasErrResponse() bool { 25 | return false 26 | } 27 | 28 | func (w testWriter) ErrResponse() *Response { 29 | return nil 30 | } 31 | 32 | func TestPartialBufferHasContents(t *testing.T) { 33 | table := []struct { 34 | buf []byte 35 | expected bool 36 | }{ 37 | {[]byte{}, false}, 38 | {[]byte{0x1, 0x2}, true}, 39 | } 40 | 41 | for _, tt := range table { 42 | pbuf := partialBuffer(tt.buf) 43 | got := pbuf.hasContents() 44 | if got != tt.expected { 45 | t.Errorf("want: %v, got: %v", tt.expected, got) 46 | } 47 | } 48 | } 49 | 50 | func TestPartialBufferGetAndEmpty(t *testing.T) { 51 | table := [][]byte{ 52 | {}, 53 | {0x1, 0x2}, 54 | } 55 | 56 | for _, tt := range table { 57 | og := make([]byte, len(tt)) 58 | copy(og, tt) 59 | pbuf := partialBuffer(tt) 60 | 61 | got := pbuf.getAndEmpty([]byte{}) 62 | 63 | if !bytes.Equal(got, og) { 64 | t.Errorf("want: %v, got: %v", og, got) 65 | } 66 | 67 | if len(pbuf) != 0 { 68 | t.Errorf("failed to empty %v", og) 69 | } 70 | } 71 | } 72 | 73 | func TestPartialBufferSet(t *testing.T) { 74 | table := []struct { 75 | buf []byte 76 | expected partialBuffer 77 | }{ 78 | {[]byte{}, partialBuffer([]byte{})}, 79 | {[]byte{0x1, 0x2}, partialBuffer([]byte{0x1, 0x2})}, 80 | } 81 | 82 | for _, tt := range table { 83 | var pbuf partialBuffer 84 | pbuf.set(tt.buf) 85 | if !bytes.Equal(pbuf, tt.expected) { 86 | t.Errorf("want: %v, got: %v", tt.expected, pbuf) 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /containerclient/podman_client.go: -------------------------------------------------------------------------------- 1 | package containerclient 2 | 3 | import ( 4 | "context" 5 | "net/url" 6 | "os" 7 | "strings" 8 | 9 | "github.com/containers/buildah" 10 | "github.com/containers/buildah/define" 11 | "github.com/containers/podman/v5/pkg/bindings" 12 | "github.com/containers/podman/v5/pkg/bindings/images" 13 | "github.com/containers/podman/v5/pkg/domain/entities" 14 | "github.com/containers/podman/v5/pkg/domain/entities/reports" 15 | ) 16 | 17 | func imagePath(repository, tag string) string { 18 | fullRepoPath := strings.Join([]string{repository, tag}, ":") 19 | return fullRepoPath 20 | } 21 | 22 | func fullImageRef(registry, repository, tag string) string { 23 | imagePath := imagePath(repository, tag) 24 | fullImageRef := strings.Join([]string{registry, imagePath}, "/") 25 | return fullImageRef 26 | } 27 | 28 | type podmanClient struct { 29 | podmanContext context.Context 30 | } 31 | 32 | func NewPodmanClient(host string) (*podmanClient, error) { 33 | hostURL, err := url.Parse(host) 34 | if err != nil { 35 | return nil, err 36 | } 37 | 38 | // Podman's connection context. 39 | // This must be passed to every api calls 40 | pmContext, err := bindings.NewConnection(context.Background(), hostURL.String()) 41 | if err != nil { 42 | return nil, err 43 | } 44 | c := &podmanClient{ 45 | podmanContext: pmContext, 46 | } 47 | 48 | return c, nil 49 | } 50 | 51 | func (c *podmanClient) BuildImage(opts BuildImageOptions) error { 52 | buildahOpts := define.BuildOptions{ 53 | NoCache: opts.NoCache, 54 | RemoveIntermediateCtrs: opts.RmTmpContainer, 55 | ForceRmIntermediateCtrs: opts.ForceRmTmpContainer, 56 | ContextDirectory: opts.ContextDir, 57 | Output: opts.Name, 58 | Out: opts.OutputStream, 59 | Err: opts.OutputStream, 60 | Quiet: opts.SuppressOutput, 61 | CommonBuildOpts: &buildah.CommonBuildOptions{}, 62 | } 63 | if os.Getenv("BULDAH_ISOLATION") == "chroot" { 64 | buildahOpts.Isolation = buildah.IsolationChroot 65 | } 66 | podmanBuildOpts := entities.BuildOptions{BuildOptions: buildahOpts} 67 | _, err := images.Build(c.podmanContext, []string{opts.Dockerfile}, podmanBuildOpts) 68 | return err 69 | } 70 | 71 | func (c *podmanClient) PullImage(opts PullImageOptions, auth AuthConfiguration) error { 72 | fullImagePath := imagePath(opts.Repository, opts.Tag) 73 | podmanPullOpts := images.PullOptions{ 74 | Username: &auth.Username, 75 | Password: &auth.Password, 76 | } 77 | _, err := images.Pull(c.podmanContext, fullImagePath, &podmanPullOpts) 78 | return err 79 | } 80 | 81 | func (c *podmanClient) PushImage(opts PushImageOptions, auth AuthConfiguration) error { 82 | 83 | imagePath := imagePath(opts.Repository, opts.Tag) 84 | podmanPushOpts := images.PushOptions{ 85 | Username: &auth.Username, 86 | Password: &auth.Password, 87 | } 88 | err := images.Push(c.podmanContext, imagePath, imagePath, &podmanPushOpts) 89 | return err 90 | } 91 | 92 | func (c *podmanClient) TagImage(name string, opts TagImageOptions) error { 93 | err := images.Tag(c.podmanContext, name, opts.Tag, opts.Repository, &images.TagOptions{}) 94 | return err 95 | } 96 | 97 | func (c *podmanClient) InspectImage(name string) (*Image, error) { 98 | getOptions := images.GetOptions{} 99 | imageReport, err := images.GetImage(c.podmanContext, name, &getOptions) 100 | if err != nil { 101 | return nil, err 102 | } 103 | return &Image{ 104 | ID: imageReport.ImageData.ID, 105 | RepoDigests: imageReport.ImageData.RepoDigests, 106 | }, nil 107 | } 108 | 109 | func (c *podmanClient) RemoveImageExtended(name string, opts RemoveImageOptions) error { 110 | removeOptions := images.RemoveOptions{ 111 | Force: &opts.Force, 112 | } 113 | _, err := images.Remove(c.podmanContext, []string{name}, &removeOptions) 114 | if len(err) > 0 { 115 | return err[0] 116 | } 117 | return nil 118 | } 119 | 120 | func (c *podmanClient) PruneImages(opts PruneImagesOptions) (*PruneImagesResults, error) { 121 | pruneOptions := images.PruneOptions{ 122 | Filters: opts.Filters, 123 | } 124 | imagesDeletedReports, err := images.Prune(c.podmanContext, &pruneOptions) 125 | if err != nil { 126 | return nil, err 127 | } 128 | imagesDeleted := reports.PruneReportsIds(imagesDeletedReports) 129 | return &PruneImagesResults{ImagesDeleted: imagesDeleted}, nil 130 | } 131 | -------------------------------------------------------------------------------- /containerclient/podman_log_write.go: -------------------------------------------------------------------------------- 1 | package containerclient 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | log "github.com/sirupsen/logrus" 7 | 8 | "github.com/quay/quay-builder/rpc" 9 | ) 10 | 11 | // PodmanRPCWriter implements a RPCWriter. 12 | // Unlike the Docker daemon, Podman's build call outputs plain string, and not JSON encoded data, 13 | // so we need to serialize each line into a Response struct before logging it to an rpc.Client. 14 | type PodmanRPCWriter struct { 15 | client rpc.Client 16 | errResponse *Response 17 | partialBuffer *partialBuffer 18 | hasPartialBuffer bool 19 | } 20 | 21 | // Write implements the io.Writer interface for RPCWriter. 22 | func (w *PodmanRPCWriter) Write(p []byte) (n int, err error) { 23 | // Unlike docker, libpod parses the JSON encoded data from stream before writing the output, 24 | // without the option of returning the raw data instead. 25 | // Instead of decoding the stream into a Response, we set the Response's "Stream" before 26 | // marshaling it into JSON to be logged. 27 | originalLength := len(p) 28 | 29 | var m Response 30 | m.Stream = string(p) 31 | 32 | jsonData, err := json.Marshal(&m) 33 | if err != nil { 34 | log.Fatalf("Error when marshaling logs: %v", err) 35 | } 36 | 37 | err = w.client.PublishBuildLogEntry(string(jsonData)) 38 | if err != nil { 39 | log.Fatalf("Failed to publish log entry: %v", err) 40 | } 41 | 42 | return originalLength, nil 43 | } 44 | 45 | func (w *PodmanRPCWriter) ErrResponse() (error, bool) { 46 | // libpod already parses the JSON stream before writing to output. 47 | // So the error would not be returned from the output stream,. but as 48 | // the return value of the API call instead. 49 | // See https://github.com/containers/podman/blob/master/pkg/bindings/images/build.go#L175 50 | return nil, false 51 | } 52 | 53 | func (w *PodmanRPCWriter) ResetError() { 54 | w.errResponse = nil 55 | } 56 | -------------------------------------------------------------------------------- /entrypoint.sh: -------------------------------------------------------------------------------- 1 | #! /bin/sh 2 | set -e 3 | 4 | ################################# Functions ################################# 5 | 6 | setup_kubernetes_podman(){ 7 | # Write out certificate if one was given 8 | if [[ -n "${CA_CERT}" ]]; then 9 | echo "[INFO]: CA_CERT found, writing out to /certs/cacert.crt" 10 | echo "${CA_CERT}" > /certs/cacert.crt 11 | fi 12 | cat /etc/pki/tls/certs/ca-bundle.crt >> /certs/cacert.crt 13 | chmod 400 /certs/cacert.crt 14 | 15 | # Start podman service 16 | PODMAN_OPTS="--log-level=error" 17 | if [[ "$DEBUG" == "true" ]]; then 18 | PODMAN_OPTS="--log-level=debug" 19 | fi 20 | podman $PODMAN_OPTS system service --time 0 & 21 | 22 | # Ensure socket exists 23 | RETRIES=5 24 | while [[ ! -S '/tmp/podman-run-1000/podman/podman.sock' ]] 25 | do 26 | if [[ $RETRIES -eq 0 ]]; then 27 | echo "[ERROR]: podman socket not found, exiting" 28 | exit 1 29 | fi 30 | echo "[INFO]: Waiting for podman to start. Checking again in 3s..." 31 | sleep 3s 32 | RETRIES=$((RETRIES - 1)) 33 | done 34 | } 35 | 36 | load_extra_ca(){ 37 | # This directory is for any custom certificates users want to mount 38 | echo "Copying custom certs to trust if they exist" 39 | if [ "$(ls -A /certs)" ]; then 40 | cp /certs/* /etc/pki/ca-trust/source/anchors 41 | fi 42 | 43 | update-ca-trust extract 44 | 45 | # Update the default bundle to link to the newly generated bundle (not sure why /etc/pki/ca-trust/extracted/pem is not being updated...) 46 | if [ -f "/certs/ssl.cert" ]; then 47 | cat /certs/ssl.cert >> /etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem 48 | fi 49 | } 50 | 51 | ################################# Begin execution ################################# 52 | 53 | case $EXECUTOR in 54 | "kubernetesPodman") 55 | setup_kubernetes_podman 56 | ;; 57 | "popen" | "ec2" | "kubernetes" | "") 58 | load_extra_ca 59 | ;; 60 | *) 61 | echo "[ERROR]: Unrecognized executor: $EXECUTOR" 62 | exit 1 63 | ;; 64 | esac 65 | 66 | exec quay-builder 67 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/quay/quay-builder 2 | 3 | go 1.23.0 4 | 5 | toolchain go1.23.6 6 | 7 | require ( 8 | code.cloudfoundry.org/archiver v0.0.0-20230612152321-46722cbc3f99 9 | github.com/containers/buildah v1.38.0 10 | github.com/containers/podman/v5 v5.3.0 11 | github.com/distribution/reference v0.6.0 12 | github.com/docker/docker v27.4.0-rc.2+incompatible 13 | github.com/fsouza/go-dockerclient v1.12.0 14 | github.com/golang/protobuf v1.5.4 15 | github.com/moby/buildkit v0.18.0 16 | github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d 17 | github.com/sirupsen/logrus v1.9.3 18 | google.golang.org/grpc v1.68.0 19 | google.golang.org/protobuf v1.35.1 20 | ) 21 | 22 | require ( 23 | dario.cat/mergo v1.0.1 // indirect 24 | github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect 25 | github.com/BurntSushi/toml v1.4.0 // indirect 26 | github.com/Microsoft/go-winio v0.6.2 // indirect 27 | github.com/Microsoft/hcsshim v0.12.9 // indirect 28 | github.com/VividCortex/ewma v1.2.0 // indirect 29 | github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d // indirect 30 | github.com/aead/serpent v0.0.0-20160714141033-fba169763ea6 // indirect 31 | github.com/agext/levenshtein v1.2.3 // indirect 32 | github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect 33 | github.com/blang/semver/v4 v4.0.0 // indirect 34 | github.com/chzyer/readline v1.5.1 // indirect 35 | github.com/containerd/cgroups/v3 v3.0.3 // indirect 36 | github.com/containerd/errdefs v0.3.0 // indirect 37 | github.com/containerd/errdefs/pkg v0.3.0 // indirect 38 | github.com/containerd/log v0.1.0 // indirect 39 | github.com/containerd/platforms v0.2.1 // indirect 40 | github.com/containerd/stargz-snapshotter/estargz v0.15.1 // indirect 41 | github.com/containerd/typeurl/v2 v2.2.3 // indirect 42 | github.com/containernetworking/cni v1.2.3 // indirect 43 | github.com/containernetworking/plugins v1.5.1 // indirect 44 | github.com/containers/common v0.61.0 // indirect 45 | github.com/containers/image/v5 v5.33.0 // indirect 46 | github.com/containers/libtrust v0.0.0-20230121012942-c1716e8a8d01 // indirect 47 | github.com/containers/luksy v0.0.0-20241007190014-e2530d691420 // indirect 48 | github.com/containers/ocicrypt v1.2.0 // indirect 49 | github.com/containers/psgo v1.9.0 // indirect 50 | github.com/containers/storage v1.56.0 // indirect 51 | github.com/coreos/go-systemd/v22 v22.5.1-0.20231103132048-7d375ecc2b09 // indirect 52 | github.com/cyberphone/json-canonicalization v0.0.0-20231217050601-ba74d44ecf5f // indirect 53 | github.com/cyphar/filepath-securejoin v0.3.4 // indirect 54 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 55 | github.com/disiqueira/gotree/v3 v3.0.2 // indirect 56 | github.com/docker/distribution v2.8.3+incompatible // indirect 57 | github.com/docker/docker-credential-helpers v0.8.2 // indirect 58 | github.com/docker/go-connections v0.5.0 // indirect 59 | github.com/docker/go-units v0.5.0 // indirect 60 | github.com/felixge/httpsnoop v1.0.4 // indirect 61 | github.com/fsnotify/fsnotify v1.8.0 // indirect 62 | github.com/go-jose/go-jose/v4 v4.0.5 // indirect 63 | github.com/go-logr/logr v1.4.2 // indirect 64 | github.com/go-logr/stdr v1.2.2 // indirect 65 | github.com/go-openapi/analysis v0.23.0 // indirect 66 | github.com/go-openapi/errors v0.22.0 // indirect 67 | github.com/go-openapi/jsonpointer v0.21.0 // indirect 68 | github.com/go-openapi/jsonreference v0.21.0 // indirect 69 | github.com/go-openapi/loads v0.22.0 // indirect 70 | github.com/go-openapi/runtime v0.28.0 // indirect 71 | github.com/go-openapi/spec v0.21.0 // indirect 72 | github.com/go-openapi/strfmt v0.23.0 // indirect 73 | github.com/go-openapi/swag v0.23.0 // indirect 74 | github.com/go-openapi/validate v0.24.0 // indirect 75 | github.com/godbus/dbus/v5 v5.1.1-0.20240921181615-a817f3cc4a9e // indirect 76 | github.com/gogo/protobuf v1.3.2 // indirect 77 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect 78 | github.com/google/go-containerregistry v0.20.2 // indirect 79 | github.com/google/go-intervals v0.0.2 // indirect 80 | github.com/google/uuid v1.6.0 // indirect 81 | github.com/gorilla/mux v1.8.1 // indirect 82 | github.com/hashicorp/errwrap v1.1.0 // indirect 83 | github.com/hashicorp/go-multierror v1.1.1 // indirect 84 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 85 | github.com/jinzhu/copier v0.4.0 // indirect 86 | github.com/josharian/intern v1.0.0 // indirect 87 | github.com/json-iterator/go v1.1.12 // indirect 88 | github.com/kevinburke/ssh_config v1.2.0 // indirect 89 | github.com/klauspost/compress v1.17.11 // indirect 90 | github.com/klauspost/pgzip v1.2.6 // indirect 91 | github.com/kr/fs v0.1.0 // indirect 92 | github.com/letsencrypt/boulder v0.0.0-20240620165639-de9c06129bec // indirect 93 | github.com/mailru/easyjson v0.7.7 // indirect 94 | github.com/manifoldco/promptui v0.9.0 // indirect 95 | github.com/mattn/go-runewidth v0.0.16 // indirect 96 | github.com/mattn/go-shellwords v1.0.12 // indirect 97 | github.com/mattn/go-sqlite3 v1.14.24 // indirect 98 | github.com/miekg/pkcs11 v1.1.1 // indirect 99 | github.com/mistifyio/go-zfs/v3 v3.0.1 // indirect 100 | github.com/mitchellh/mapstructure v1.5.0 // indirect 101 | github.com/moby/docker-image-spec v1.3.1 // indirect 102 | github.com/moby/patternmatcher v0.6.0 // indirect 103 | github.com/moby/sys/capability v0.3.0 // indirect 104 | github.com/moby/sys/mountinfo v0.7.2 // indirect 105 | github.com/moby/sys/sequential v0.6.0 // indirect 106 | github.com/moby/sys/user v0.3.0 // indirect 107 | github.com/moby/sys/userns v0.1.0 // indirect 108 | github.com/moby/term v0.5.0 // indirect 109 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 110 | github.com/modern-go/reflect2 v1.0.2 // indirect 111 | github.com/morikuni/aec v1.0.0 // indirect 112 | github.com/nxadm/tail v1.4.11 // indirect 113 | github.com/oklog/ulid v1.3.1 // indirect 114 | github.com/opencontainers/go-digest v1.0.0 // indirect 115 | github.com/opencontainers/image-spec v1.1.0 // indirect 116 | github.com/opencontainers/runc v1.2.1 // indirect 117 | github.com/opencontainers/runtime-spec v1.2.0 // indirect 118 | github.com/opencontainers/runtime-tools v0.9.1-0.20241001195557-6c9570a1678f // indirect 119 | github.com/opencontainers/selinux v1.11.1 // indirect 120 | github.com/openshift/imagebuilder v1.2.15 // indirect 121 | github.com/ostreedev/ostree-go v0.0.0-20210805093236-719684c64e4f // indirect 122 | github.com/pkg/errors v0.9.1 // indirect 123 | github.com/pkg/sftp v1.13.7 // indirect 124 | github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect 125 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect 126 | github.com/proglottis/gpgme v0.1.3 // indirect 127 | github.com/rivo/uniseg v0.4.7 // indirect 128 | github.com/seccomp/libseccomp-golang v0.10.0 // indirect 129 | github.com/secure-systems-lab/go-securesystemslib v0.8.0 // indirect 130 | github.com/sigstore/fulcio v1.6.4 // indirect 131 | github.com/sigstore/rekor v1.3.6 // indirect 132 | github.com/sigstore/sigstore v1.8.9 // indirect 133 | github.com/skeema/knownhosts v1.3.0 // indirect 134 | github.com/spf13/cobra v1.8.1 // indirect 135 | github.com/spf13/pflag v1.0.5 // indirect 136 | github.com/stefanberger/go-pkcs11uri v0.0.0-20230803200340-78284954bff6 // indirect 137 | github.com/sylabs/sif/v2 v2.19.1 // indirect 138 | github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635 // indirect 139 | github.com/tchap/go-patricia/v2 v2.3.1 // indirect 140 | github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399 // indirect 141 | github.com/tonistiigi/go-csvvalue v0.0.0-20240710180619-ddb21b71c0b4 // indirect 142 | github.com/ulikunitz/xz v0.5.12 // indirect 143 | github.com/vbatts/tar-split v0.11.6 // indirect 144 | github.com/vbauerster/mpb/v8 v8.8.3 // indirect 145 | github.com/vishvananda/netlink v1.3.0 // indirect 146 | github.com/vishvananda/netns v0.0.4 // indirect 147 | go.etcd.io/bbolt v1.3.11 // indirect 148 | go.mongodb.org/mongo-driver v1.14.0 // indirect 149 | go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352 // indirect 150 | go.opencensus.io v0.24.0 // indirect 151 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 // indirect 152 | go.opentelemetry.io/otel v1.28.0 // indirect 153 | go.opentelemetry.io/otel/metric v1.28.0 // indirect 154 | go.opentelemetry.io/otel/trace v1.28.0 // indirect 155 | golang.org/x/crypto v0.35.0 // indirect 156 | golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c // indirect 157 | golang.org/x/mod v0.21.0 // indirect 158 | golang.org/x/net v0.30.0 // indirect 159 | golang.org/x/sync v0.11.0 // indirect 160 | golang.org/x/sys v0.30.0 // indirect 161 | golang.org/x/term v0.29.0 // indirect 162 | golang.org/x/text v0.22.0 // indirect 163 | google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 // indirect 164 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect 165 | gopkg.in/yaml.v3 v3.0.1 // indirect 166 | sigs.k8s.io/yaml v1.4.0 // indirect 167 | tags.cncf.io/container-device-interface v0.8.0 // indirect 168 | tags.cncf.io/container-device-interface/specs-go v0.8.0 // indirect 169 | ) 170 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | code.cloudfoundry.org/archiver v0.0.0-20230612152321-46722cbc3f99 h1:3Oda4fIPTvMioMSbbSoEF1hqarfK0fQC82ikbaufrho= 3 | code.cloudfoundry.org/archiver v0.0.0-20230612152321-46722cbc3f99/go.mod h1:IUG1lSIh6kdVNg6qxbQ9p3V0sKGGwK/jjJtV2eeE9Vg= 4 | dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= 5 | dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= 6 | github.com/14rcole/gopopulate v0.0.0-20180821133914-b175b219e774 h1:SCbEWT58NSt7d2mcFdvxC9uyrdcTfvBbPLThhkDmXzg= 7 | github.com/14rcole/gopopulate v0.0.0-20180821133914-b175b219e774/go.mod h1:6/0dYRLLXyJjbkIPeeGyoJ/eKOSI0eU6eTlCBYibgd0= 8 | github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 h1:bvDV9vkmnHYOMsOr4WLk+Vo07yKIzd94sVoIqshQ4bU= 9 | github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= 10 | github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= 11 | github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= 12 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 13 | github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0= 14 | github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= 15 | github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= 16 | github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= 17 | github.com/Microsoft/hcsshim v0.12.9 h1:2zJy5KA+l0loz1HzEGqyNnjd3fyZA31ZBCGKacp6lLg= 18 | github.com/Microsoft/hcsshim v0.12.9/go.mod h1:fJ0gkFAna6ukt0bLdKB8djt4XIJhF/vEPuoIWYVvZ8Y= 19 | github.com/VividCortex/ewma v1.2.0 h1:f58SaIzcDXrSy3kWaHNvuJgJ3Nmz59Zji6XoJR/q1ow= 20 | github.com/VividCortex/ewma v1.2.0/go.mod h1:nz4BbCtbLyFDeC9SUHbtcT5644juEuWfUAUnGx7j5l4= 21 | github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d h1:licZJFw2RwpHMqeKTCYkitsPqHNxTmd4SNR5r94FGM8= 22 | github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d/go.mod h1:asat636LX7Bqt5lYEZ27JNDcqxfjdBQuJ/MM4CN/Lzo= 23 | github.com/aead/serpent v0.0.0-20160714141033-fba169763ea6 h1:5L8Mj9Co9sJVgW3TpYk2gxGJnDjsYuboNTcRmbtGKGs= 24 | github.com/aead/serpent v0.0.0-20160714141033-fba169763ea6/go.mod h1:3HgLJ9d18kXMLQlJvIY3+FszZYMxCz8WfE2MQ7hDY0w= 25 | github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7lmo= 26 | github.com/agext/levenshtein v1.2.3/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= 27 | github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= 28 | github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= 29 | github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 30 | github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 31 | github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= 32 | github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= 33 | github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= 34 | github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= 35 | github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= 36 | github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= 37 | github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 38 | github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= 39 | github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM= 40 | github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ= 41 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= 42 | github.com/chzyer/readline v1.5.1 h1:upd/6fQk4src78LMRzh5vItIt361/o4uq553V8B5sGI= 43 | github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk= 44 | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= 45 | github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04= 46 | github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8= 47 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 48 | github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= 49 | github.com/containerd/cgroups/v3 v3.0.3 h1:S5ByHZ/h9PMe5IOQoN7E+nMc2UcLEM/V48DGDJ9kip0= 50 | github.com/containerd/cgroups/v3 v3.0.3/go.mod h1:8HBe7V3aWGLFPd/k03swSIsGjZhHI2WzJmticMgVuz0= 51 | github.com/containerd/errdefs v0.3.0 h1:FSZgGOeK4yuT/+DnF07/Olde/q4KBoMsaamhXxIMDp4= 52 | github.com/containerd/errdefs v0.3.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= 53 | github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= 54 | github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= 55 | github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= 56 | github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= 57 | github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= 58 | github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= 59 | github.com/containerd/stargz-snapshotter/estargz v0.15.1 h1:eXJjw9RbkLFgioVaTG+G/ZW/0kEe2oEKCdS/ZxIyoCU= 60 | github.com/containerd/stargz-snapshotter/estargz v0.15.1/go.mod h1:gr2RNwukQ/S9Nv33Lt6UC7xEx58C+LHRdoqbEKjz1Kk= 61 | github.com/containerd/typeurl/v2 v2.2.3 h1:yNA/94zxWdvYACdYO8zofhrTVuQY73fFU1y++dYSw40= 62 | github.com/containerd/typeurl/v2 v2.2.3/go.mod h1:95ljDnPfD3bAbDJRugOiShd/DlAAsxGtUBhJxIn7SCk= 63 | github.com/containernetworking/cni v1.2.3 h1:hhOcjNVUQTnzdRJ6alC5XF+wd9mfGIUaj8FuJbEslXM= 64 | github.com/containernetworking/cni v1.2.3/go.mod h1:DuLgF+aPd3DzcTQTtp/Nvl1Kim23oFKdm2okJzBQA5M= 65 | github.com/containernetworking/plugins v1.5.1 h1:T5ji+LPYjjgW0QM+KyrigZbLsZ8jaX+E5J/EcKOE4gQ= 66 | github.com/containernetworking/plugins v1.5.1/go.mod h1:MIQfgMayGuHYs0XdNudf31cLLAC+i242hNm6KuDGqCM= 67 | github.com/containers/buildah v1.38.0 h1:FmciZMwzhdcvtWj+8IE+61+lfTG2JfgrbZ2DUnEMnTE= 68 | github.com/containers/buildah v1.38.0/go.mod h1:tUsHC2bcgR5Q/R76qZUn7x0FRglqPFry2g5KhWfH4LI= 69 | github.com/containers/common v0.61.0 h1:j/84PTqZIKKYy42OEJsZmjZ4g4Kq2ERuC3tqp2yWdh4= 70 | github.com/containers/common v0.61.0/go.mod h1:NGRISq2vTFPSbhNqj6MLwyes4tWSlCnqbJg7R77B8xc= 71 | github.com/containers/image/v5 v5.33.0 h1:6oPEFwTurf7pDTGw7TghqGs8K0+OvPtY/UyzU0B2DfE= 72 | github.com/containers/image/v5 v5.33.0/go.mod h1:T7HpASmvnp2H1u4cyckMvCzLuYgpD18dSmabSw0AcHk= 73 | github.com/containers/libtrust v0.0.0-20230121012942-c1716e8a8d01 h1:Qzk5C6cYglewc+UyGf6lc8Mj2UaPTHy/iF2De0/77CA= 74 | github.com/containers/libtrust v0.0.0-20230121012942-c1716e8a8d01/go.mod h1:9rfv8iPl1ZP7aqh9YA68wnZv2NUDbXdcdPHVz0pFbPY= 75 | github.com/containers/luksy v0.0.0-20241007190014-e2530d691420 h1:57rxgU2wdI3lZMDZtao09WjCWmxBKOxI/Sj37IpCV50= 76 | github.com/containers/luksy v0.0.0-20241007190014-e2530d691420/go.mod h1:MYzFCudLgMcXgFl7XuFjUowNDTBqL09BfEgMf7QHtO4= 77 | github.com/containers/ocicrypt v1.2.0 h1:X14EgRK3xNFvJEfI5O4Qn4T3E25ANudSOZz/sirVuPM= 78 | github.com/containers/ocicrypt v1.2.0/go.mod h1:ZNviigQajtdlxIZGibvblVuIFBKIuUI2M0QM12SD31U= 79 | github.com/containers/podman/v5 v5.3.0 h1:C/eK7Ek1XV+ZX5nUQaqK5OM2+WTtDpLrybOHEbmgAuA= 80 | github.com/containers/podman/v5 v5.3.0/go.mod h1:xUbIlPCLXYMmjs1FHVtNbYWYlDONCFxzZK8MaHMcFlI= 81 | github.com/containers/psgo v1.9.0 h1:eJ74jzSaCHnWt26OlKZROSyUyRcGDf+gYBdXnxrMW4g= 82 | github.com/containers/psgo v1.9.0/go.mod h1:0YoluUm43Mz2UnBIh1P+6V6NWcbpTL5uRtXyOcH0B5A= 83 | github.com/containers/storage v1.56.0 h1:DZ9KSkj6M2tvj/4bBoaJu3QDHRl35BwsZ4kmLJS97ZI= 84 | github.com/containers/storage v1.56.0/go.mod h1:c6WKowcAlED/DkWGNuL9bvGYqIWCVy7isRMdCSKWNjk= 85 | github.com/coreos/go-systemd/v22 v22.5.1-0.20231103132048-7d375ecc2b09 h1:OoRAFlvDGCUqDLampLQjk0yeeSGdF9zzst/3G9IkBbc= 86 | github.com/coreos/go-systemd/v22 v22.5.1-0.20231103132048-7d375ecc2b09/go.mod h1:m2r/smMKsKwgMSAoFKHaa68ImdCSNuKE1MxvQ64xuCQ= 87 | github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 88 | github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= 89 | github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= 90 | github.com/cyberphone/json-canonicalization v0.0.0-20231217050601-ba74d44ecf5f h1:eHnXnuK47UlSTOQexbzxAZfekVz6i+LKRdj1CU5DPaM= 91 | github.com/cyberphone/json-canonicalization v0.0.0-20231217050601-ba74d44ecf5f/go.mod h1:uzvlm1mxhHkdfqitSA92i7Se+S9ksOn3a3qmv/kyOCw= 92 | github.com/cyphar/filepath-securejoin v0.3.4 h1:VBWugsJh2ZxJmLFSM06/0qzQyiQX2Qs0ViKrUAcqdZ8= 93 | github.com/cyphar/filepath-securejoin v0.3.4/go.mod h1:8s/MCNJREmFK0H02MF6Ihv1nakJe4L/w3WZLHNkvlYM= 94 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 95 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 96 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= 97 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 98 | github.com/disiqueira/gotree/v3 v3.0.2 h1:ik5iuLQQoufZBNPY518dXhiO5056hyNBIK9lWhkNRq8= 99 | github.com/disiqueira/gotree/v3 v3.0.2/go.mod h1:ZuyjE4+mUQZlbpkI24AmruZKhg3VHEgPLDY8Qk+uUu8= 100 | github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= 101 | github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= 102 | github.com/docker/cli v27.4.0-rc.2+incompatible h1:A0GZwegDlt2wdt3tpmrUzkVOZmbhvd7i05wPSf7Oo74= 103 | github.com/docker/cli v27.4.0-rc.2+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= 104 | github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk= 105 | github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= 106 | github.com/docker/docker v27.4.0-rc.2+incompatible h1:9OJjVGtelk/zGC3TyKweJ29b9Axzh0s/0vtU4mneumE= 107 | github.com/docker/docker v27.4.0-rc.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= 108 | github.com/docker/docker-credential-helpers v0.8.2 h1:bX3YxiGzFP5sOXWc3bTPEXdEaZSeVMrFgOr3T+zrFAo= 109 | github.com/docker/docker-credential-helpers v0.8.2/go.mod h1:P3ci7E3lwkZg6XiHdRKft1KckHiO9a2rNtyFbZ/ry9M= 110 | github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= 111 | github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= 112 | github.com/docker/go-metrics v0.0.1 h1:AgB/0SvBxihN0X8OR4SjsblXkbMvalQ8cjmtKQ2rQV8= 113 | github.com/docker/go-metrics v0.0.1/go.mod h1:cG1hvH2utMXtqgqqYE9plW6lDxS3/5ayHzueweSI3Vw= 114 | github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= 115 | github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= 116 | github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 117 | github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 118 | github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= 119 | github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= 120 | github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= 121 | github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= 122 | github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= 123 | github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M= 124 | github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= 125 | github.com/fsouza/go-dockerclient v1.12.0 h1:S2f2crEUbBNCFiF06kR/GvioEB8EMsb3Td/bpawD+aU= 126 | github.com/fsouza/go-dockerclient v1.12.0/go.mod h1:YWUtjg8japrqD/80L98nTtCoxQFp5B5wrSsnyeB5lFo= 127 | github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE= 128 | github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA= 129 | github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= 130 | github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= 131 | github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 132 | github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= 133 | github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= 134 | github.com/go-openapi/analysis v0.23.0 h1:aGday7OWupfMs+LbmLZG4k0MYXIANxcuBTYUC03zFCU= 135 | github.com/go-openapi/analysis v0.23.0/go.mod h1:9mz9ZWaSlV8TvjQHLl2mUW2PbZtemkE8yA5v22ohupo= 136 | github.com/go-openapi/errors v0.22.0 h1:c4xY/OLxUBSTiepAg3j/MHuAv5mJhnf53LLMWFB+u/w= 137 | github.com/go-openapi/errors v0.22.0/go.mod h1:J3DmZScxCDufmIMsdOuDHxJbdOGC0xtUynjIx092vXE= 138 | github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= 139 | github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= 140 | github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ= 141 | github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4= 142 | github.com/go-openapi/loads v0.22.0 h1:ECPGd4jX1U6NApCGG1We+uEozOAvXvJSF4nnwHZ8Aco= 143 | github.com/go-openapi/loads v0.22.0/go.mod h1:yLsaTCS92mnSAZX5WWoxszLj0u+Ojl+Zs5Stn1oF+rs= 144 | github.com/go-openapi/runtime v0.28.0 h1:gpPPmWSNGo214l6n8hzdXYhPuJcGtziTOgUpvsFWGIQ= 145 | github.com/go-openapi/runtime v0.28.0/go.mod h1:QN7OzcS+XuYmkQLw05akXk0jRH/eZ3kb18+1KwW9gyc= 146 | github.com/go-openapi/spec v0.21.0 h1:LTVzPc3p/RzRnkQqLRndbAzjY0d0BCL72A6j3CdL9ZY= 147 | github.com/go-openapi/spec v0.21.0/go.mod h1:78u6VdPw81XU44qEWGhtr982gJ5BWg2c0I5XwVMotYk= 148 | github.com/go-openapi/strfmt v0.23.0 h1:nlUS6BCqcnAk0pyhi9Y+kdDVZdZMHfEKQiS4HaMgO/c= 149 | github.com/go-openapi/strfmt v0.23.0/go.mod h1:NrtIpfKtWIygRkKVsxh7XQMDQW5HKQl6S5ik2elW+K4= 150 | github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= 151 | github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= 152 | github.com/go-openapi/validate v0.24.0 h1:LdfDKwNbpB6Vn40xhTdNZAnfLECL81w+VX3BumrGD58= 153 | github.com/go-openapi/validate v0.24.0/go.mod h1:iyeX1sEufmv3nPbBdX3ieNviWnOZaJ1+zquzJEf2BAQ= 154 | github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= 155 | github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= 156 | github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= 157 | github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U= 158 | github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= 159 | github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= 160 | github.com/godbus/dbus/v5 v5.1.1-0.20240921181615-a817f3cc4a9e h1:znsZ+BW06LsAtZwQvY/rgWQ3o1q0mnR4SG4q8HCP+3Q= 161 | github.com/godbus/dbus/v5 v5.1.1-0.20240921181615-a817f3cc4a9e/go.mod h1:nRJ+j259aT/CW6otoGCHPa1K/lNHLO+UGmW133FNj9s= 162 | github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= 163 | github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 164 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 165 | github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 166 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= 167 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 168 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 169 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 170 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 171 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 172 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 173 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 174 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 175 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 176 | github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= 177 | github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 178 | github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= 179 | github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= 180 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 181 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 182 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 183 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 184 | github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 185 | github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 186 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 187 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 188 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 189 | github.com/google/go-containerregistry v0.20.2 h1:B1wPJ1SN/S7pB+ZAimcciVD+r+yV/l/DSArMxlbwseo= 190 | github.com/google/go-containerregistry v0.20.2/go.mod h1:z38EKdKh4h7IP2gSfUUqEvalZBqs6AoLeWfUy34nQC8= 191 | github.com/google/go-intervals v0.0.2 h1:FGrVEiUnTRKR8yE04qzXYaJMtnIYqobR5QbblK3ixcM= 192 | github.com/google/go-intervals v0.0.2/go.mod h1:MkaR3LNRfeKLPmqgJYs4E66z5InYjmCjbbr4TQlcT6Y= 193 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 194 | github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo= 195 | github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= 196 | github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 197 | github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 198 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 199 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 200 | github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= 201 | github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= 202 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 h1:asbCHRVmodnJTuQ3qamDwqVOIjwqUPTYmYuemVOx+Ys= 203 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0/go.mod h1:ggCgvZ2r7uOoQjOyu2Y1NhHmEPPzzuhWgcza5M1Ji1I= 204 | github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= 205 | github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= 206 | github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= 207 | github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= 208 | github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= 209 | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= 210 | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 211 | github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8= 212 | github.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg= 213 | github.com/jmhodges/clock v1.2.0 h1:eq4kys+NI0PLngzaHEe7AmPT90XMGIEySD1JfV1PDIs= 214 | github.com/jmhodges/clock v1.2.0/go.mod h1:qKjhA7x7u/lQpPB1XAqX1b1lCI/w3/fNuYpI/ZjLynI= 215 | github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= 216 | github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= 217 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 218 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 219 | github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= 220 | github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= 221 | github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= 222 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 223 | github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= 224 | github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= 225 | github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU= 226 | github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs= 227 | github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8= 228 | github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= 229 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 230 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 231 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 232 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 233 | github.com/letsencrypt/boulder v0.0.0-20240620165639-de9c06129bec h1:2tTW6cDth2TSgRbAhD7yjZzTQmcN25sDRPEeinR51yQ= 234 | github.com/letsencrypt/boulder v0.0.0-20240620165639-de9c06129bec/go.mod h1:TmwEoGCwIti7BCeJ9hescZgRtatxRE+A72pCoPfmcfk= 235 | github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= 236 | github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= 237 | github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA= 238 | github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg= 239 | github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= 240 | github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 241 | github.com/mattn/go-shellwords v1.0.12 h1:M2zGm7EW6UQJvDeQxo4T51eKPurbeFbe8WtebGE2xrk= 242 | github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y= 243 | github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM= 244 | github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= 245 | github.com/miekg/pkcs11 v1.1.1 h1:Ugu9pdy6vAYku5DEpVWVFPYnzV+bxB+iRdbuFSu7TvU= 246 | github.com/miekg/pkcs11 v1.1.1/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs= 247 | github.com/mistifyio/go-zfs/v3 v3.0.1 h1:YaoXgBePoMA12+S1u/ddkv+QqxcfiZK4prI6HPnkFiU= 248 | github.com/mistifyio/go-zfs/v3 v3.0.1/go.mod h1:CzVgeB0RvF2EGzQnytKVvVSDwmKJXxkOTUGbNrTja/k= 249 | github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= 250 | github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= 251 | github.com/moby/buildkit v0.18.0 h1:KSelhNINJcNA3FCWBbGCytvicjP+kjU5kZlZhkTUkVo= 252 | github.com/moby/buildkit v0.18.0/go.mod h1:vCR5CX8NGsPTthTg681+9kdmfvkvqJBXEv71GZe5msU= 253 | github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= 254 | github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= 255 | github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk= 256 | github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= 257 | github.com/moby/sys/capability v0.3.0 h1:kEP+y6te0gEXIaeQhIi0s7vKs/w0RPoH1qPa6jROcVg= 258 | github.com/moby/sys/capability v0.3.0/go.mod h1:4g9IK291rVkms3LKCDOoYlnV8xKwoDTpIrNEE35Wq0I= 259 | github.com/moby/sys/mountinfo v0.7.2 h1:1shs6aH5s4o5H2zQLn796ADW1wMrIwHsyJ2v9KouLrg= 260 | github.com/moby/sys/mountinfo v0.7.2/go.mod h1:1YOa8w8Ih7uW0wALDUgT1dTTSBrZ+HiBLGws92L2RU4= 261 | github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU= 262 | github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko= 263 | github.com/moby/sys/user v0.3.0 h1:9ni5DlcW5an3SvRSx4MouotOygvzaXbaSrc/wGDFWPo= 264 | github.com/moby/sys/user v0.3.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs= 265 | github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g= 266 | github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28= 267 | github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= 268 | github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= 269 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 270 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 271 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 272 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 273 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 274 | github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= 275 | github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= 276 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= 277 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= 278 | github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d h1:VhgPp6v9qf9Agr/56bj7Y/xa04UccTW04VP0Qed4vnQ= 279 | github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d/go.mod h1:YUTz3bUH2ZwIWBy3CJBeOBEugqcmXREj14T+iG/4k4U= 280 | github.com/nxadm/tail v1.4.11 h1:8feyoE3OzPrcshW5/MJ4sGESc5cqmGkGCWlco4l0bqY= 281 | github.com/nxadm/tail v1.4.11/go.mod h1:OTaG3NK980DZzxbRq6lEuzgU+mug70nY11sMd4JXXHc= 282 | github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4= 283 | github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= 284 | github.com/onsi/ginkgo/v2 v2.21.0 h1:7rg/4f3rB88pb5obDgNZrNHrQ4e6WpjonchcpuBRnZM= 285 | github.com/onsi/ginkgo/v2 v2.21.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo= 286 | github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4= 287 | github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= 288 | github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= 289 | github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= 290 | github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= 291 | github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= 292 | github.com/opencontainers/runc v1.2.1 h1:mQkmeFSUxqFaVmvIn1VQPeQIKpHFya5R07aJw0DKQa8= 293 | github.com/opencontainers/runc v1.2.1/go.mod h1:/PXzF0h531HTMsYQnmxXkBD7YaGShm/2zcRB79dksUc= 294 | github.com/opencontainers/runtime-spec v1.2.0 h1:z97+pHb3uELt/yiAWD691HNHQIF07bE7dzrbT927iTk= 295 | github.com/opencontainers/runtime-spec v1.2.0/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= 296 | github.com/opencontainers/runtime-tools v0.9.1-0.20241001195557-6c9570a1678f h1:tGGVO3yF9p5s/mPi3kO1AdoUDK49z0dgQqV0jeT1kik= 297 | github.com/opencontainers/runtime-tools v0.9.1-0.20241001195557-6c9570a1678f/go.mod h1:oIH6VwKkaDOO+SIYZpdwrC/0wKYqrfO6E1sG1j3UVws= 298 | github.com/opencontainers/selinux v1.11.1 h1:nHFvthhM0qY8/m+vfhJylliSshm8G1jJ2jDMcgULaH8= 299 | github.com/opencontainers/selinux v1.11.1/go.mod h1:E5dMC3VPuVvVHDYmi78qvhJp8+M586T4DlDRYpFkyec= 300 | github.com/openshift/imagebuilder v1.2.15 h1:MNn1OztEE/l8pSEDPYAQ71Ys6rpXA2P00UFhdY9p/yk= 301 | github.com/openshift/imagebuilder v1.2.15/go.mod h1:cK6MLyBl1IHmIYGLY/2SLOG6p0PtEDUOC7khxsFYUXE= 302 | github.com/ostreedev/ostree-go v0.0.0-20210805093236-719684c64e4f h1:/UDgs8FGMqwnHagNDPGOlts35QkhAZ8by3DR7nMih7M= 303 | github.com/ostreedev/ostree-go v0.0.0-20210805093236-719684c64e4f/go.mod h1:J6OG6YJVEWopen4avK3VNQSnALmmjvniMmni/YFYAwc= 304 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 305 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 306 | github.com/pkg/sftp v1.13.7 h1:uv+I3nNJvlKZIQGSr8JVQLNHFU9YhhNpvC14Y6KgmSM= 307 | github.com/pkg/sftp v1.13.7/go.mod h1:KMKI0t3T6hfA+lTR/ssZdunHo+uwq7ghoN09/FSu3DY= 308 | github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo= 309 | github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= 310 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 311 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= 312 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 313 | github.com/proglottis/gpgme v0.1.3 h1:Crxx0oz4LKB3QXc5Ea0J19K/3ICfy3ftr5exgUK1AU0= 314 | github.com/proglottis/gpgme v0.1.3/go.mod h1:fPbW/EZ0LvwQtH8Hy7eixhp1eF3G39dtx7GUN+0Gmy0= 315 | github.com/prometheus/client_golang v1.20.2 h1:5ctymQzZlyOON1666svgwn3s6IKWgfbjsejTMiXIyjg= 316 | github.com/prometheus/client_golang v1.20.2/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= 317 | github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 318 | github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= 319 | github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= 320 | github.com/prometheus/common v0.57.0 h1:Ro/rKjwdq9mZn1K5QPctzh+MA4Lp0BuYk5ZZEVhoNcY= 321 | github.com/prometheus/common v0.57.0/go.mod h1:7uRPFSUTbfZWsJ7MHY56sqt7hLQu3bxXHDnNhl8E9qI= 322 | github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= 323 | github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= 324 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 325 | github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= 326 | github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= 327 | github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= 328 | github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= 329 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 330 | github.com/sebdah/goldie/v2 v2.5.5 h1:rx1mwF95RxZ3/83sdS4Yp7t2C5TCokvWP4TBRbAyEWY= 331 | github.com/sebdah/goldie/v2 v2.5.5/go.mod h1:oZ9fp0+se1eapSRjfYbsV/0Hqhbuu3bJVvKI/NNtssI= 332 | github.com/seccomp/libseccomp-golang v0.10.0 h1:aA4bp+/Zzi0BnWZ2F1wgNBs5gTpm+na2rWM6M9YjLpY= 333 | github.com/seccomp/libseccomp-golang v0.10.0/go.mod h1:JA8cRccbGaA1s33RQf7Y1+q9gHmZX1yB/z9WDN1C6fg= 334 | github.com/secure-systems-lab/go-securesystemslib v0.8.0 h1:mr5An6X45Kb2nddcFlbmfHkLguCE9laoZCUzEEpIZXA= 335 | github.com/secure-systems-lab/go-securesystemslib v0.8.0/go.mod h1:UH2VZVuJfCYR8WgMlCU1uFsOUU+KeyrTWcSS73NBOzU= 336 | github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= 337 | github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= 338 | github.com/sigstore/fulcio v1.6.4 h1:d86obfxUAG3Y6CYwOx1pdwCZwKmROB6w6927pKOVIRY= 339 | github.com/sigstore/fulcio v1.6.4/go.mod h1:Y6bn3i3KGhXpaHsAtYP3Z4Np0+VzCo1fLv8Ci6mbPDs= 340 | github.com/sigstore/rekor v1.3.6 h1:QvpMMJVWAp69a3CHzdrLelqEqpTM3ByQRt5B5Kspbi8= 341 | github.com/sigstore/rekor v1.3.6/go.mod h1:JDTSNNMdQ/PxdsS49DJkJ+pRJCO/83nbR5p3aZQteXc= 342 | github.com/sigstore/sigstore v1.8.9 h1:NiUZIVWywgYuVTxXmRoTT4O4QAGiTEKup4N1wdxFadk= 343 | github.com/sigstore/sigstore v1.8.9/go.mod h1:d9ZAbNDs8JJfxJrYmulaTazU3Pwr8uLL9+mii4BNR3w= 344 | github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= 345 | github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 346 | github.com/skeema/knownhosts v1.3.0 h1:AM+y0rI04VksttfwjkSTNQorvGqmwATnvnAHpSgc0LY= 347 | github.com/skeema/knownhosts v1.3.0/go.mod h1:sPINvnADmT/qYH1kfv+ePMmOBTH6Tbl7b5LvTDjFK7M= 348 | github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= 349 | github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= 350 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 351 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 352 | github.com/stefanberger/go-pkcs11uri v0.0.0-20230803200340-78284954bff6 h1:pnnLyeX7o/5aX8qUQ69P/mLojDqwda8hFOCBTmP/6hw= 353 | github.com/stefanberger/go-pkcs11uri v0.0.0-20230803200340-78284954bff6/go.mod h1:39R/xuhNgVhi+K0/zst4TLrJrVmbm6LVgl4A0+ZFS5M= 354 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 355 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 356 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 357 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 358 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 359 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 360 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 361 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 362 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 363 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 364 | github.com/sylabs/sif/v2 v2.19.1 h1:1eeMmFc8elqJe60ZiWwXgL3gMheb0IP4GmNZ4q0IEA0= 365 | github.com/sylabs/sif/v2 v2.19.1/go.mod h1:U1SUhvl8X1JIxAylC0DYz1fa/Xba6EMZD1dGPGBH83E= 366 | github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635 h1:kdXcSzyDtseVEc4yCz2qF8ZrQvIDBJLl4S1c3GCXmoI= 367 | github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= 368 | github.com/tchap/go-patricia/v2 v2.3.1 h1:6rQp39lgIYZ+MHmdEq4xzuk1t7OdC35z/xm0BGhTkes= 369 | github.com/tchap/go-patricia/v2 v2.3.1/go.mod h1:VZRHKAb53DLaG+nA9EaYYiaEx6YztwDlLElMsnSHD4k= 370 | github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399 h1:e/5i7d4oYZ+C1wj2THlRK+oAhjeS/TRQwMfkIuet3w0= 371 | github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399/go.mod h1:LdwHTNJT99C5fTAzDz0ud328OgXz+gierycbcIx2fRs= 372 | github.com/tonistiigi/go-csvvalue v0.0.0-20240710180619-ddb21b71c0b4 h1:7I5c2Ig/5FgqkYOh/N87NzoyI9U15qUPXhDD8uCupv8= 373 | github.com/tonistiigi/go-csvvalue v0.0.0-20240710180619-ddb21b71c0b4/go.mod h1:278M4p8WsNh3n4a1eqiFcV2FGk7wE5fwUpUom9mK9lE= 374 | github.com/ulikunitz/xz v0.5.12 h1:37Nm15o69RwBkXM0J6A5OlE67RZTfzUxTj8fB3dfcsc= 375 | github.com/ulikunitz/xz v0.5.12/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= 376 | github.com/vbatts/tar-split v0.11.6 h1:4SjTW5+PU11n6fZenf2IPoV8/tz3AaYHMWjf23envGs= 377 | github.com/vbatts/tar-split v0.11.6/go.mod h1:dqKNtesIOr2j2Qv3W/cHjnvk9I8+G7oAkFDFN6TCBEI= 378 | github.com/vbauerster/mpb/v8 v8.8.3 h1:dTOByGoqwaTJYPubhVz3lO5O6MK553XVgUo33LdnNsQ= 379 | github.com/vbauerster/mpb/v8 v8.8.3/go.mod h1:JfCCrtcMsJwP6ZwMn9e5LMnNyp3TVNpUWWkN+nd4EWk= 380 | github.com/vishvananda/netlink v1.3.0 h1:X7l42GfcV4S6E4vHTsw48qbrV+9PVojNfIhZcwQdrZk= 381 | github.com/vishvananda/netlink v1.3.0/go.mod h1:i6NetklAujEcC6fK0JPjT8qSwWyO0HLn4UKG+hGqeJs= 382 | github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1YX8= 383 | github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM= 384 | github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo= 385 | github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= 386 | github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= 387 | github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= 388 | github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= 389 | github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= 390 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 391 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 392 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 393 | go.etcd.io/bbolt v1.3.11 h1:yGEzV1wPz2yVCLsD8ZAiGHhHVlczyC9d1rP43/VCRJ0= 394 | go.etcd.io/bbolt v1.3.11/go.mod h1:dksAq7YMXoljX0xu6VF5DMZGbhYYoLUalEiSySYAS4I= 395 | go.mongodb.org/mongo-driver v1.14.0 h1:P98w8egYRjYe3XDjxhYJagTokP/H6HzlsnojRgZRd80= 396 | go.mongodb.org/mongo-driver v1.14.0/go.mod h1:Vzb0Mk/pa7e6cWw85R4F/endUC3u0U9jGcNU603k65c= 397 | go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352 h1:CCriYyAfq1Br1aIYettdHZTy8mBTIPo7We18TuO/bak= 398 | go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352/go.mod h1:SNgMg+EgDFwmvSmLRTNKC5fegJjB7v23qTQ0XLGUNHk= 399 | go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= 400 | go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= 401 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 h1:4K4tsIXefpVJtvA/8srF4V4y0akAoPHkIslgAkjixJA= 402 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0/go.mod h1:jjdQuTGVsXV4vSs+CJ2qYDeDPf9yIJV23qlIzBm73Vg= 403 | go.opentelemetry.io/otel v1.28.0 h1:/SqNcYk+idO0CxKEUOtKQClMK/MimZihKYMruSMViUo= 404 | go.opentelemetry.io/otel v1.28.0/go.mod h1:q68ijF8Fc8CnMHKyzqL6akLO46ePnjkgfIMIjUIX9z4= 405 | go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0 h1:3Q/xZUyC1BBkualc9ROb4G8qkH90LXEIICcs5zv1OYY= 406 | go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0/go.mod h1:s75jGIWA9OfCMzF0xr+ZgfrB5FEbbV7UuYo32ahUiFI= 407 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.28.0 h1:j9+03ymgYhPKmeXGk5Zu+cIZOlVzd9Zv7QIiyItjFBU= 408 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.28.0/go.mod h1:Y5+XiUG4Emn1hTfciPzGPJaSI+RpDts6BnCIir0SLqk= 409 | go.opentelemetry.io/otel/metric v1.28.0 h1:f0HGvSl1KRAU1DLgLGFjrwVyismPlnuU6JD6bOeuA5Q= 410 | go.opentelemetry.io/otel/metric v1.28.0/go.mod h1:Fb1eVBFZmLVTMb6PPohq3TO9IIhUisDsbJoL/+uQW4s= 411 | go.opentelemetry.io/otel/sdk v1.28.0 h1:b9d7hIry8yZsgtbmM0DKyPWMMUMlK9NEKuIG4aBqWyE= 412 | go.opentelemetry.io/otel/sdk v1.28.0/go.mod h1:oYj7ClPUA7Iw3m+r7GeEjz0qckQRJK2B8zjcZEfu7Pg= 413 | go.opentelemetry.io/otel/trace v1.28.0 h1:GhQ9cUuQGmNDd5BTCP2dAvv75RdMxEfTmYejp+lkx9g= 414 | go.opentelemetry.io/otel/trace v1.28.0/go.mod h1:jPyXzNPg6da9+38HEwElrQiHlVMTnVfM3/yv2OlIHaI= 415 | go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0= 416 | go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8= 417 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 418 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 419 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 420 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 421 | golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= 422 | golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs= 423 | golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ= 424 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 425 | golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c h1:7dEasQXItcW1xKJ2+gg5VOiBnqWrJc+rq0DPKyvvdbY= 426 | golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c/go.mod h1:NQtJDoLvd6faHhE7m4T/1IY708gDefGGjR/iUW8yQQ8= 427 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 428 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 429 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 430 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 431 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 432 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 433 | golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 434 | golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0= 435 | golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= 436 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 437 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 438 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 439 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 440 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 441 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 442 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 443 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 444 | golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 445 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 446 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 447 | golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 448 | golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= 449 | golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= 450 | golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= 451 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 452 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 453 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 454 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 455 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 456 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 457 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 458 | golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 459 | golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= 460 | golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 461 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 462 | golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 463 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 464 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 465 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 466 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 467 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 468 | golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 469 | golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 470 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 471 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 472 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 473 | golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 474 | golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 475 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 476 | golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 477 | golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 478 | golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 479 | golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= 480 | golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 481 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 482 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 483 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 484 | golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= 485 | golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= 486 | golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU= 487 | golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s= 488 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 489 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 490 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 491 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 492 | golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= 493 | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 494 | golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= 495 | golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= 496 | golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U= 497 | golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= 498 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 499 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 500 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 501 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 502 | golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 503 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 504 | golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 505 | golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 506 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 507 | golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= 508 | golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ= 509 | golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0= 510 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 511 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 512 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 513 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 514 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 515 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 516 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 517 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 518 | google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= 519 | google.golang.org/genproto v0.0.0-20240823204242-4ba0660f739c h1:TYOEhrQMrNDTAd2rX9m+WgGr8Ku6YNuj1D7OX6rWSok= 520 | google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1 h1:hjSy6tcFQZ171igDaN5QHOw2n6vx40juYbC/x67CEhc= 521 | google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:qpvKtACPCQhAdu3PyQgV4l3LMXZEtft7y8QcarRsp9I= 522 | google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 h1:pPJltXNxVzT4pK9yD8vR9X75DaWYYmLGMsEvBfFQZzQ= 523 | google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= 524 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 525 | google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= 526 | google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= 527 | google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 528 | google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= 529 | google.golang.org/grpc v1.68.0 h1:aHQeeJbo8zAkAa3pRzrVjZlbz6uSfeOXlJNQM0RAbz0= 530 | google.golang.org/grpc v1.68.0/go.mod h1:fmSPC5AsjSBCK54MyHRx48kpOti1/jRfOlwEWywNjWA= 531 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 532 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 533 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 534 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 535 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 536 | google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 537 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 538 | google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 539 | google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= 540 | google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= 541 | google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= 542 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 543 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 544 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 545 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= 546 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 547 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 548 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 549 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 550 | gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= 551 | gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= 552 | gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU= 553 | gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= 554 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 555 | honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 556 | sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= 557 | sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= 558 | tags.cncf.io/container-device-interface v0.8.0 h1:8bCFo/g9WODjWx3m6EYl3GfUG31eKJbaggyBDxEldRc= 559 | tags.cncf.io/container-device-interface v0.8.0/go.mod h1:Apb7N4VdILW0EVdEMRYXIDVRZfNJZ+kmEUss2kRRQ6Y= 560 | tags.cncf.io/container-device-interface/specs-go v0.8.0 h1:QYGFzGxvYK/ZLMrjhvY0RjpUavIn4KcmRmVP/JjdBTA= 561 | tags.cncf.io/container-device-interface/specs-go v0.8.0/go.mod h1:BhJIkjjPh4qpys+qm4DAYtUyryaTDg9zris+AczXyws= 562 | -------------------------------------------------------------------------------- /rpc/grpcbuild/grpcbuild.go: -------------------------------------------------------------------------------- 1 | package grpcbuild 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io" 7 | "time" 8 | 9 | log "github.com/sirupsen/logrus" 10 | 11 | pb "github.com/quay/quay-builder/buildman_pb" 12 | "github.com/quay/quay-builder/rpc" 13 | 14 | "google.golang.org/grpc" 15 | ) 16 | 17 | type grpcClient struct { 18 | client pb.BuildManagerClient 19 | currentPhase rpc.Phase 20 | jobToken string 21 | logStream pb.BuildManager_LogMessageClient 22 | logSequenceNum int 23 | phaseSequenceNum int 24 | } 25 | 26 | func NewClient(ctx context.Context, conn *grpc.ClientConn) (rpc.Client, error) { 27 | bmClient := pb.NewBuildManagerClient(conn) 28 | client := &grpcClient{ 29 | client: bmClient, 30 | } 31 | 32 | if ok, err := client.Ping(); !ok { 33 | return nil, err 34 | } 35 | 36 | // Create log stream 37 | log.Infof("starting log stream to buildmanager") 38 | logStream, err := bmClient.LogMessage(ctx) 39 | if err != nil { 40 | return nil, err 41 | } 42 | 43 | client.logStream = logStream 44 | 45 | return client, nil 46 | } 47 | 48 | func (c *grpcClient) Ping() (bool, error) { 49 | ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) 50 | defer cancel() 51 | _, err := c.client.Ping(ctx, &pb.PingRequest{}) 52 | if err != nil { 53 | return false, err 54 | } 55 | return true, nil 56 | } 57 | 58 | func (c *grpcClient) RegisterBuildJob(registrationToken string) (*rpc.BuildArgs, error) { 59 | ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) 60 | defer cancel() 61 | 62 | buildpack, err := c.client.RegisterBuildJob(ctx, &pb.BuildJobArgs{RegisterJwt: registrationToken}) 63 | if err != nil { 64 | return nil, err 65 | } 66 | 67 | c.jobToken = buildpack.GetJobJwt() 68 | 69 | buildArgs := &rpc.BuildArgs{ 70 | Context: buildpack.Context, 71 | DockerfilePath: buildpack.DockerfilePath, 72 | Repository: buildpack.Repository, 73 | Registry: buildpack.Registry, 74 | PullToken: buildpack.PullToken, 75 | PushToken: buildpack.PushToken, 76 | TagNames: buildpack.TagNames, 77 | BaseImage: rpc.BuildArgsBaseImage{ 78 | Username: buildpack.BaseImage.GetUsername(), 79 | Password: buildpack.BaseImage.GetPassword(), 80 | }, 81 | } 82 | 83 | switch bp := buildpack.BuildPack.(type) { 84 | case *pb.BuildPack_PackageUrl: 85 | buildArgs.BuildPackage = bp.PackageUrl 86 | case *pb.BuildPack_GitPackage_: 87 | buildArgs.Git = &rpc.BuildArgsGit{ 88 | URL: bp.GitPackage.GetUrl(), 89 | SHA: bp.GitPackage.GetSha(), 90 | PrivateKey: bp.GitPackage.GetPrivateKey(), 91 | } 92 | default: 93 | return nil, fmt.Errorf("Buildpack.Buildpack has unexpected type %T", bp) 94 | } 95 | 96 | return buildArgs, nil 97 | } 98 | 99 | func (c *grpcClient) SetPhase(phase rpc.Phase, pmd *rpc.PullMetadata) error { 100 | ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) 101 | defer cancel() 102 | 103 | c.currentPhase = phase 104 | c.phaseSequenceNum += 1 105 | 106 | statusData := &pb.SetPhaseRequest_PullMetadata{} 107 | if pmd != nil { 108 | statusData.RegistryUrl = pmd.RegistryURL 109 | statusData.BaseImage = pmd.BaseImage 110 | statusData.BaseImageTag = pmd.BaseImageTag 111 | statusData.PullUsername = pmd.PullUsername 112 | } 113 | 114 | phaseResponse, err := c.client.SetPhase( 115 | ctx, 116 | &pb.SetPhaseRequest{ 117 | JobJwt: c.jobToken, 118 | SequenceNumber: int32(c.phaseSequenceNum), 119 | Phase: phaseEnum(phase), 120 | PullMetadata: statusData, 121 | }, 122 | ) 123 | if err != nil { 124 | log.Errorf("failed to update phase: %v", err) 125 | return err 126 | } 127 | 128 | if !phaseResponse.Success { 129 | log.Errorf("build manager rejected phase transition: %v", err) 130 | return rpc.ErrClientRejectedPhaseTransition{} 131 | } 132 | 133 | if int(phaseResponse.SequenceNumber) != c.phaseSequenceNum { 134 | log.Errorf("build manager rejected phase transition (sequence out of order: %d vs %d)", phaseResponse.SequenceNumber, c.phaseSequenceNum) 135 | return rpc.ErrClientRejectedPhaseTransition{} 136 | } 137 | 138 | return nil 139 | } 140 | 141 | func (c *grpcClient) FindMostSimilarTag(tmd rpc.TagMetadata) (string, error) { 142 | ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) 143 | defer cancel() 144 | 145 | baseImageName := tmd.BaseImage 146 | baseImageID := tmd.BaseImageID 147 | baseImageTag := tmd.BaseImageTag 148 | 149 | cacheTagResponse, err := c.client.DetermineCachedTag( 150 | ctx, 151 | &pb.CachedTagRequest{ 152 | JobJwt: c.jobToken, 153 | BaseImageName: baseImageName, 154 | BaseImageTag: baseImageTag, 155 | BaseImageId: baseImageID, 156 | }, 157 | ) 158 | if err != nil { 159 | return "", err 160 | } 161 | return cacheTagResponse.CachedTag, nil 162 | } 163 | 164 | func (c *grpcClient) PublishBuildLogEntry(entry string) error { 165 | c.logSequenceNum += 1 166 | err := c.logStream.Send( 167 | &pb.LogMessageRequest{ 168 | JobJwt: c.jobToken, 169 | SequenceNumber: int32(c.logSequenceNum), 170 | LogMessage: entry, 171 | }, 172 | ) 173 | if err == io.EOF { 174 | return nil 175 | } 176 | if err != nil { 177 | log.Warningf("failed to get log message: %s", err) 178 | c.logSequenceNum -= 1 179 | return err 180 | } 181 | 182 | logResp, err := c.logStream.Recv() 183 | if err == io.EOF { 184 | return nil 185 | } 186 | if err != nil { 187 | log.Warningf("failed to get log response: %s", err) 188 | } 189 | 190 | if !logResp.GetSuccess() { 191 | log.Warningf("buildmanager failed to log message: %d", c.logSequenceNum) 192 | } 193 | 194 | return nil 195 | } 196 | 197 | func (c *grpcClient) Heartbeat(ctx context.Context) { 198 | failedHeartbeatRetries := 3 199 | 200 | heartbeatStream, err := c.client.Heartbeat(ctx) 201 | if err != nil { 202 | log.Fatalf("failed to start heartbeat: %s", err) 203 | } 204 | 205 | for { 206 | select { 207 | case <-ctx.Done(): 208 | return 209 | default: 210 | if failedHeartbeatRetries == 0 { 211 | log.Fatalf("failed to update heartbeat too many times") 212 | } 213 | 214 | // Send heartbeat 215 | err := heartbeatStream.Send(&pb.HeartbeatRequest{JobJwt: c.jobToken}) 216 | if err != nil { 217 | log.Warningf("failed to send heartbeat: %s", err) 218 | break 219 | } 220 | 221 | // Block until heartbeat response 222 | hearbeatResp, err := heartbeatStream.Recv() 223 | if err == io.EOF { 224 | return 225 | } 226 | if err != nil { 227 | log.Warningf("failed to get heartbeat response: %s", err) 228 | break 229 | } 230 | 231 | if hearbeatResp.GetReply() { 232 | log.Infof("successfully sent heartbeat to BuildManager") 233 | failedHeartbeatRetries = 3 234 | break 235 | } 236 | 237 | // Retry if for some reason heartbeat was not updated 238 | if !hearbeatResp.GetReply() && failedHeartbeatRetries > 0 { 239 | log.Infof("heartbeat failed to update, retrying right away") 240 | failedHeartbeatRetries -= 1 241 | continue 242 | } 243 | } 244 | 245 | time.Sleep(2 * time.Second) 246 | } 247 | } 248 | 249 | func phaseEnum(phase rpc.Phase) pb.Phase { 250 | switch p := phase; p { 251 | case rpc.Waiting: 252 | return pb.Phase_WAITING 253 | case rpc.Unpacking: 254 | return pb.Phase_UNPACKING 255 | // TODO: Should CheckingCache and PrimingCache have separate phases. 256 | // If so, the proto definition would need to be updated with the new phases. 257 | case rpc.Pulling, rpc.CheckingCache, rpc.PrimingCache: 258 | return pb.Phase_PULLING 259 | case rpc.Building: 260 | return pb.Phase_BUILDING 261 | case rpc.Pushing: 262 | return pb.Phase_PUSHING 263 | case rpc.Complete: 264 | return pb.Phase_COMPLETE 265 | case rpc.Error: 266 | return pb.Phase_ERROR 267 | default: 268 | return pb.Phase_ERROR 269 | } 270 | } 271 | -------------------------------------------------------------------------------- /rpc/rpc.go: -------------------------------------------------------------------------------- 1 | package rpc 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | ) 8 | 9 | // Phase represents the milestones in progressing through a build. 10 | type Phase string 11 | 12 | const ( 13 | Waiting Phase = "Waiting" 14 | Unpacking Phase = "unpacking" 15 | CheckingCache Phase = "checking-cache" 16 | PrimingCache Phase = "priming-cache" 17 | Pulling Phase = "pulling" 18 | Building Phase = "building" 19 | Pushing Phase = "pushing" 20 | Complete Phase = "complete" 21 | Error Phase = "error" 22 | ) 23 | 24 | // InvalidDockerfileError is the type of error returned from a BuildCallback when the 25 | // provided BuildArgs do not have parsable Dockerfile. 26 | type InvalidDockerfileError struct{ Err string } 27 | 28 | func (e InvalidDockerfileError) Error() string { 29 | return e.Err 30 | } 31 | 32 | // BuildPackError is the type of error returned from a BuildCallback when the 33 | // provided BuildArgs do not have sufficient data to download a BuildPack. 34 | type BuildPackError struct{ Err string } 35 | 36 | func (e BuildPackError) Error() string { 37 | return e.Err 38 | } 39 | 40 | // GitCheckoutError is the type of error returned from a BuildCallback when the 41 | // provided git ref cannot be checked out. 42 | type GitCheckoutError struct{ Err string } 43 | 44 | func (e GitCheckoutError) Error() string { 45 | return e.Err 46 | } 47 | 48 | // GitCloneError is the type of error returned from a BuildCallback when the 49 | // git clone fails. 50 | type GitCloneError struct{ Err string } 51 | 52 | func (e GitCloneError) Error() string { 53 | return e.Err 54 | } 55 | 56 | // CannotPullForCacheError is the type of error returned from a BuildCallback 57 | // when it fails to pull the image used for caching. 58 | type CannotPullForCacheError struct{ Err string } 59 | 60 | func (e CannotPullForCacheError) Error() string { 61 | return e.Err 62 | } 63 | 64 | // TagError is the type of error returned from a BuildCallback 65 | // when it fails to tag the built image. 66 | type TagError struct{ Err string } 67 | 68 | func (e TagError) Error() string { 69 | return e.Err 70 | } 71 | 72 | // PushError is the type of error returned from a BuildCallback 73 | // when it fails to push the built image. 74 | type PushError struct{ Err string } 75 | 76 | func (e PushError) Error() string { 77 | return e.Err 78 | } 79 | 80 | // PullError is the type of error returned from a BuildCallback 81 | // when it fails to pull the base image. 82 | type PullError struct{ Err string } 83 | 84 | func (e PullError) Error() string { 85 | return e.Err 86 | } 87 | 88 | // BuildError is the type of error returned from a BuildCallback 89 | // when it fails to build the image. 90 | type BuildError struct{ Err string } 91 | 92 | func (e BuildError) Error() string { 93 | return e.Err 94 | } 95 | 96 | // ErrClientRejectedPhaseTransition is the type of error 97 | // returned when buildman rejects a phase transition 98 | type ErrClientRejectedPhaseTransition struct{ Err string } 99 | 100 | func (e ErrClientRejectedPhaseTransition) Error() string { 101 | return e.Err 102 | } 103 | 104 | // BuildArgsBaseImage represents the arguments for a base image. The arguments 105 | // are as follows: 106 | // 107 | // username - the username for pulling the base image (if any), and 108 | // password - the password for pulling the base image (if any). 109 | type BuildArgsBaseImage struct { 110 | Username string `mapstructure:"username"` 111 | Password string `mapstructure:"password"` 112 | } 113 | 114 | // BuildArgsGit represents the arguments related to git (if any). The arguments 115 | // are as follows: 116 | // 117 | // url - URL to clone a repository, 118 | // sha - commit identifier to checkout, and 119 | // private_key - ssh private key needed to clone a repository. 120 | type BuildArgsGit struct { 121 | URL string `mapstructure:"url"` 122 | SHA string `mapstructure:"sha"` 123 | PrivateKey string `mapstructure:"private_key"` 124 | } 125 | 126 | // BuildArgs represents the arguments needed to build an image. The 127 | // arguments are as follows: 128 | // 129 | // build_package - URL to the build package to download and untar/unzip, 130 | // sub_directory - location within the build package of the Dockerfile and the 131 | // build context, 132 | // dockerfile_name - name of the dockerfile within the sub_directory 133 | // repository - repository for which this build is occurring, 134 | // registry - registry for which this build is occuring (e.g. 'quay.io', 135 | // 'staging.quay.io'), 136 | // pull_token - token to use when pulling the cache for building, 137 | // push_token - token to use to push the built image, 138 | // tag_names - name(s) of the tag(s) for the newly built image, 139 | // cached_tag - tag in the repository to pull to prime the cache, 140 | // git - optional git values and credentials used to clone the repository, and 141 | // base_image - image name and credentials used to conduct the base image pull. 142 | type BuildArgs struct { 143 | BuildPackage string `mapstructure:"build_package"` 144 | Context string `mapstructure:"context"` 145 | DockerfilePath string `mapstructure:"dockerfile_path"` 146 | Repository string `mapstructure:"repository"` 147 | Registry string `mapstructure:"registry"` 148 | PullToken string `mapstructure:"pull_token"` 149 | PushToken string `mapstructure:"push_token"` 150 | TagNames []string `mapstructure:"tag_names"` 151 | Git *BuildArgsGit `mapstructure:"git"` 152 | BaseImage BuildArgsBaseImage `mapstructure:"base_image"` 153 | } 154 | 155 | // FullRepoName is a helper function to concatenate the registry and repository. 156 | func (args *BuildArgs) FullRepoName() string { 157 | return fmt.Sprintf("%s/%s", args.Registry, args.Repository) 158 | } 159 | 160 | // TagMetadata is collection of a particular Docker tag's metadata. 161 | type TagMetadata struct { 162 | BaseImage string 163 | BaseImageTag string 164 | BaseImageID string 165 | } 166 | 167 | // PullMetadata represents the metadata being used to pull an image when setting 168 | // the Phase to one related to pulling. 169 | type PullMetadata struct { 170 | RegistryURL string 171 | BaseImage string 172 | BaseImageTag string 173 | PullUsername string 174 | } 175 | 176 | // BuildMetadata is a collection of metadata about the successfully created 177 | // build artifact. 178 | type BuildMetadata struct { 179 | ImageID string 180 | Digests []string 181 | } 182 | 183 | // ErrNoSimilarTags is returned from a Client when FindMostSimilarTag fails 184 | // to find any similar tags. 185 | var ErrNoSimilarTags = errors.New("failed to find any similar tags") 186 | 187 | // Client represents an implementation of a transport between a Builder and a 188 | // BuildManager. 189 | type Client interface { 190 | // Attemps to ping the server 191 | Ping() (bool, error) 192 | 193 | // RegisterBuild 194 | RegisterBuildJob(string) (*BuildArgs, error) 195 | 196 | // Heartbeat 197 | Heartbeat(context.Context) 198 | 199 | // SetPhase informs a BuildManager of a transition between Phases. 200 | SetPhase(Phase, *PullMetadata) error 201 | 202 | // FindMostSimilarTag sends a synchronous request to a BuildManager in order 203 | // to determine if there is a suitable docker tag to pull in order to prime 204 | // the docker build cache. 205 | FindMostSimilarTag(TagMetadata) (string, error) 206 | 207 | // PublishBuildLogEntry records a docker daemon log entry to a BuildManager. 208 | PublishBuildLogEntry(entry string) error 209 | } 210 | -------------------------------------------------------------------------------- /scripts/git-version: -------------------------------------------------------------------------------- 1 | #!/bin/sh -e 2 | 3 | # parse the current git commit hash 4 | COMMIT="$(git rev-parse --short HEAD)" 5 | 6 | # check if the current commit has a matching tag 7 | TAG="$(git describe --exact-match --abbrev=0 --tags "${COMMIT}" 2> /dev/null || true)" 8 | 9 | # use the matching tag as the version, if available 10 | if [ -z "${TAG}" ]; then 11 | VERSION="${COMMIT}" 12 | else 13 | VERSION="${TAG}" 14 | fi 15 | 16 | # check for changed files (not untracked files) 17 | if [ -n "$(git diff --shortstat 2> /dev/null | tail -n1)" ]; then 18 | VERSION="${VERSION}-dirty" 19 | fi 20 | 21 | echo "${VERSION}" 22 | -------------------------------------------------------------------------------- /version/version.go: -------------------------------------------------------------------------------- 1 | // Package version contains version information for this app. 2 | package version 3 | 4 | // Version is set by the build scripts. 5 | var Version = "no-version" 6 | --------------------------------------------------------------------------------