Repository: google/ko
Pull request #332 (2021-03-30 05:11:17)

Add a tag-only flag to publisher.
The `--tag-only` flag resolves images into tag-only references. This flag is useful when digests are not preserved when images are repopulated. It can only be used when a single tag is specified, and the tag is not `latest`.


Patch:
diff --git a/.github/workflows/knative-boilerplate.yaml b/.github/workflows/boilerplate.yaml
similarity index 62%
rename from .github/workflows/knative-boilerplate.yaml
rename to .github/workflows/boilerplate.yaml
index c0e648b1..38c77b2d 100644
--- a/.github/workflows/knative-boilerplate.yaml
+++ b/.github/workflows/boilerplate.yaml
@@ -1,25 +1,8 @@
-# Copyright 2020 The Knative Authors.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#     http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-# This file is automagically synced here from github.com/knative-sandbox/.github
-# repo by knobots: https://github.com/knative-sandbox/knobots and will be overwritten.
-
 name: Boilerplate
 
 on:
   pull_request:
-    branches: [ 'main', 'master', 'release-*' ]
+    branches: ['main']
 
 jobs:
 
@@ -41,15 +24,11 @@ jobs:
           language: Bash
 
     steps:
-
-      - name: Set up Go 1.15.x
-        uses: actions/setup-go@v2
+      - uses: actions/setup-go@v2
         with:
-          go-version: 1.15.x
-        id: go
+          go-version: 1.16.x
 
-      - name: Check out code
-        uses: actions/checkout@v2
+      - uses: actions/checkout@v2
 
       - name: Install Tools
         run: |
@@ -66,13 +45,8 @@ jobs:
 
           echo "${TEMP_PATH}" >> $GITHUB_PATH
 
-      - id: boilerplate_txt
-        uses: andstor/file-existence-action@v1
-        with:
-          files: ./hack/boilerplate/boilerplate.${{ matrix.extension }}.txt
       - name: ${{ matrix.language }} license boilerplate
         shell: bash
-        if: ${{ steps.boilerplate_txt.outputs.files_exists == 'true' }}
         env:
           REVIEWDOG_GITHUB_API_TOKEN: ${{ github.token }}
         run: |
diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml
new file mode 100644
index 00000000..0c2b8d9b
--- /dev/null
+++ b/.github/workflows/build.yaml
@@ -0,0 +1,21 @@
+name: Build
+
+on:
+  pull_request: 
+    branches: ['main']
+
+jobs:
+
+  build:
+    name: Build
+    runs-on: ubuntu-latest
+
+    steps:
+      - uses: actions/checkout@v2
+      - uses: actions/setup-go@v2
+        with:
+          go-version: 1.16.x
+
+      - run: |
+          go build ./...
+          go test -run=^$ ./...
diff --git a/.github/workflows/knative-donotsubmit.yaml b/.github/workflows/donotsubmit.yaml
similarity index 58%
rename from .github/workflows/knative-donotsubmit.yaml
rename to .github/workflows/donotsubmit.yaml
index cec5c307..a48c1b33 100644
--- a/.github/workflows/knative-donotsubmit.yaml
+++ b/.github/workflows/donotsubmit.yaml
@@ -1,25 +1,8 @@
-# Copyright 2020 The Knative Authors.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#     http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-# This file is automagically synced here from github.com/knative-sandbox/.github
-# repo by knobots: https://github.com/knative-sandbox/knobots and will be overwritten.
-
 name: Do Not Submit
 
 on:
   pull_request:
-    branches: [ 'main', 'master', 'release-*' ]
+    branches: ['main']
 
 jobs:
 
@@ -28,9 +11,7 @@ jobs:
     runs-on: ubuntu-latest
 
     steps:
-      - name: Check out code
-        uses: actions/checkout@v2
-
+      - uses: actions/checkout@v2
       - name: Do Not Submit
         shell: bash
         env:
diff --git a/.github/workflows/knative-go-build.yaml b/.github/workflows/knative-go-build.yaml
deleted file mode 100644
index 7bd13004..00000000
--- a/.github/workflows/knative-go-build.yaml
+++ /dev/null
@@ -1,59 +0,0 @@
-# Copyright 2020 The Knative Authors.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#     http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-# This file is automagically synced here from github.com/knative-sandbox/.github
-# repo by knobots: https://github.com/knative-sandbox/knobots and will be overwritten.
-
-name: Build
-
-on:
-  pull_request:
-    branches: [ 'main', 'master', 'release-*' ]
-
-jobs:
-
-  build:
-    name: Build
-    strategy:
-      matrix:
-        go-version: [1.15.x]
-        platform: [ubuntu-latest]
-
-    runs-on: ${{ matrix.platform }}
-
-    steps:
-
-      - name: Set up Go ${{ matrix.go-version }}
-        uses: actions/setup-go@v2
-        with:
-          go-version: ${{ matrix.go-version }}
-        id: go
-
-      - name: Check out code
-        uses: actions/checkout@v2
-
-      - name: Build
-        run: |
-          tags="$(grep -I  -r '// +build' . | \
-                grep -v '^./vendor/' | \
-                grep -v '^./hack/' | \
-                grep -v '^./third_party' | \
-                cut -f3 -d' ' | \
-                sort | uniq | \
-                grep -v '^!' | \
-                tr '\n' ' ')"
-
-          echo "Building with tags: ${tags}"
-          go test -vet=off -tags "${tags}" -run=^$ ./... | grep -v "no test" || true
-
diff --git a/.github/workflows/knative-releasability.yaml b/.github/workflows/knative-releasability.yaml
deleted file mode 100644
index cf885877..00000000
--- a/.github/workflows/knative-releasability.yaml
+++ /dev/null
@@ -1,167 +0,0 @@
-# Copyright 2020 The Knative Authors.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#     http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-# This file is automagically synced here from github.com/knative-sandbox/.github
-# repo by knobots: https://github.com/mattmoor/knobots and will be overwritten.
-
-name: 'Releasability'
-
-on:
-  schedule:
-    - cron: '0 1 * * 1-5' # 6am Pacific, weekdays.
-
-  workflow_dispatch:      # Manual trigger.
-    inputs:
-      releaseFamily:
-        description: 'Release? (vX.Y)'
-      slackChannel:
-        description: 'Slack Channel? (release-#)'
-
-jobs:
-  releasability:
-    name: Releasability
-    runs-on: 'ubuntu-latest'
-
-    env:
-      #########################################
-      #   Update this section each release.   #
-      RELEASE: 'v0.21'
-      SLACK_CHANNEL: 'release-21'
-      #########################################
-
-    steps:
-      - name: Defaults
-        run: |
-          # If manual trigger sent releaseFamily and slackChannel, then override them
-          if [[ "${{ github.event.inputs.releaseFamily }}" != "" ]]; then
-            echo "RELEASE=${{ github.event.inputs.releaseFamily }}" >> $GITHUB_ENV
-          fi
-          if [[ "${{ github.event.inputs.slackChannel }}" != "" ]]; then
-            echo "SLACK_CHANNEL=${{ github.event.inputs.slackChannel }}" >> $GITHUB_ENV
-          fi
-          if [[ "${{ secrets.SLACK_WEBHOOK }}" != "" ]]; then
-            echo "SLACK_WEBHOOK=exists" >> $GITHUB_ENV
-          fi
-
-      - name: Set up Go 1.15.x
-        uses: actions/setup-go@v2
-        with:
-          go-version: 1.15.x
-
-      - name: Install Dependencies
-        run: GO111MODULE=on go get knative.dev/test-infra/buoy@main
-
-      - name: Check out code
-        uses: actions/checkout@v2
-
-      - name: Exists
-        id: exists
-        run: |
-          EXISTS=0
-          buoy exists go.mod --release ${RELEASE} --verbose || EXISTS=$?
-          if [[ "$EXISTS" -eq "0" ]]; then
-            EXISTS=true
-          else
-            EXISTS=false
-          fi
-          echo ::set-output name=release-branch::${EXISTS}
-
-      - name: Check
-        if: steps.exists.outputs.release-branch == 'false'
-        run: |
-          # The following pushes the stdout of buoy into $CHECK_MESSAGE
-          CHECK=0
-          echo 'CHECK_MESSAGE<<EOF' >> $GITHUB_ENV
-          buoy check go.mod --release ${RELEASE} --domain knative.dev --verbose >> $GITHUB_ENV 2>&1 || CHECK=$?
-          echo 'EOF' >> $GITHUB_ENV
-
-          # We just captured the return code of the buoy call, test it to see
-          # if we should continue validating. The next steps short circuit if
-          # we already know we are not ready for a release.
-          if [[ "$CHECK" -eq "0"  ]]; then
-            echo 'current=true' >> $GITHUB_ENV
-          else
-            echo 'current=false' >> $GITHUB_ENV
-          fi
-
-      - name: Upgrade
-        if: steps.exists.outputs.release-branch == 'false' && env.current == 'true'
-        run: |
-          # if update deps returns un-successful, then mark current to false.
-          if ! ./hack/update-deps.sh --release ${RELEASE} --upgrade; then
-            echo "VERIFY_MESSAGE=Unable to update deps for ${{ github.repository }}." >> $GITHUB_ENV
-            echo 'current=false' >> $GITHUB_ENV
-          fi
-
-      - name: Verify
-        if: steps.exists.outputs.release-branch == 'false' && env.current == 'true'
-        run: |
-          # If we don't run `git status` before the "git diff-index" it seems
-          # to list every file that's been touched by codegen.
-          git status
-          FOUND_DIFF=0
-          for x in $(git diff-index --name-only HEAD --); do
-            if [ "$(basename $x)" = "go.mod" ]; then
-              continue
-            elif [ "$(basename $x)" = "go.sum" ]; then
-              continue
-            elif [ "$(basename $x)" = "modules.txt" ]; then
-              continue
-            fi
-            echo "Found non-module diff: $x"
-            FOUND_DIFF=1
-            break
-          done
-
-          # If we see no changes after the upgrade, then we are up to date.
-          if [[ "$FOUND_DIFF" -eq "0"  ]]; then
-              echo "VERIFY_MESSAGE=${{ github.repository }} up to date." >> $GITHUB_ENV
-          else
-              echo "VERIFY_MESSAGE=${{ github.repository }} is out of date." >> $GITHUB_ENV
-              echo 'current=false' >> $GITHUB_ENV
-          fi
-
-      - name: Status GO
-        if: steps.exists.outputs.release-branch == 'false' && env.current == 'true'
-        run: |
-          echo 'SLACK_COLOR=#098e00' >> $GITHUB_ENV
-          echo 'SLACK_TITLE=Releasability for ${{ github.repository }} @ ${{ env.RELEASE }} is GO!' >> $GITHUB_ENV
-
-      - name: Status NO-GO
-        if: steps.exists.outputs.release-branch == 'false' && env.current == 'false'
-        run: |
-          echo 'SLACK_COLOR=#8E1600' >> $GITHUB_ENV
-          echo 'SLACK_TITLE=Releasability for ${{ github.repository }} @ ${{ env.RELEASE }} is NO-GO!' >> $GITHUB_ENV
-
-      - name: Post status to Slack
-        # Note: using env.SLACK_WEBHOOK here because secrets are not allowed in the if block.
-        if: env.SLACK_WEBHOOK != '' && steps.exists.outputs.release-branch == 'false'
-        uses: rtCamp/action-slack-notify@v2.1.0
-        env:
-          SLACK_ICON: http://github.com/knative.png?size=48
-          SLACK_USERNAME: github-actions
-          SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
-          MSG_MINIMAL: 'true'
-          SLACK_MESSAGE: |
-            ${{ env.CHECK_MESSAGE }}
-            ${{ env.VERIFY_MESSAGE }}
-            For detailed logs: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
-
-      - name: Fail if NO-GO
-        if: steps.exists.outputs.release-branch == 'false' && env.current == 'false'
-        run: |
-          # When we have figured out that things are NO-GO, we intentionally fail the job
-          # so that the status badge shows up red and we can use the badges to create a
-          # releasability dashboard for all of the repos.
-          exit 1
diff --git a/.github/workflows/knative-release-notes.yaml b/.github/workflows/knative-release-notes.yaml
deleted file mode 100644
index 517f429b..00000000
--- a/.github/workflows/knative-release-notes.yaml
+++ /dev/null
@@ -1,96 +0,0 @@
-# Copyright 2020 The Knative Authors.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#     http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-# This file is automagically synced here from github.com/knative-sandbox/.github
-# repo by knobots: https://github.com/knative-sandbox/knobots and will be overwritten.
-
-name: 'Release Notes'
-
-on:
-  workflow_dispatch:
-    inputs:
-      branch:
-        description: 'Branch? (main)'
-        required: true
-        default: 'main'
-      start-sha:
-        description: 'Starting SHA? (last tag on branch)'
-      end-sha:
-        description: 'Ending SHA? (latest HEAD)'
-
-jobs:
-  release-notes:
-    name: Release Notes
-    runs-on: 'ubuntu-latest'
-
-    env:
-      GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
-
-    steps:
-      - name: Set up Go
-        uses: actions/setup-go@v2
-        with:
-          go-version: 1.15.x
-
-      - name: Install Dependencies
-        run: GO111MODULE=on go get k8s.io/release/cmd/release-notes
-
-      - name: Check out code
-        uses: actions/checkout@v2
-        with:
-          # fetch-depth of 0 indicates all history for all branches and tags.
-          fetch-depth: 0
-
-      # Note: Defaults needs to run after we check out the repo.
-      - name: Defaults
-        run: |
-          echo ORG=$(echo '${{ github.repository }}' | awk -F '/' '{print $1}') >> $GITHUB_ENV
-          echo REPO=$(echo '${{ github.repository }}' | awk -F '/' '{print $2}') >> $GITHUB_ENV
-
-          echo "BRANCH=${{ github.event.inputs.branch }}" >> $GITHUB_ENV
-
-          if [[ "${{ github.event.inputs.start-sha }}" != "" ]]; then
-            echo "START_SHA=${{ github.event.inputs.start-sha }}" >> $GITHUB_ENV
-          else
-            # Default Starting SHA (thanks @dprotaso)
-            export semver=$(git describe --match "v[0-9]*" --abbrev=0)
-            echo "Using ${semver} tag for starting sha."
-            echo START_SHA=$(git rev-list -n 1 "${semver}") >> $GITHUB_ENV
-          fi
-
-          if [[ "${{ github.event.inputs.end-sha }}" != "" ]]; then
-            echo "END_SHA=${{ github.event.inputs.end-sha }}" >> $GITHUB_ENV
-          else
-            # Default Ending SHA (thanks @dprotaso)
-            echo "END_SHA=$(git rev-list -n 1 HEAD)" >> $GITHUB_ENV
-          fi
-
-      - name: Generate Notes
-        run: |
-          # See  https://github.com/kubernetes/release/tree/master/cmd/release-notes for options.
-          # Note: we are setting env vars in the Defaults step.
-          release-notes \
-            --required-author "" \
-            --repo-path       "$(pwd)" \
-            --output          release-notes.md
-
-      - name: Display Notes
-        run: |
-          cat release-notes.md
-
-      - name: Archive Release Notes
-        uses: actions/upload-artifact@v2
-        with:
-          name: release-notes.md
-          path: release-notes.md
diff --git a/.github/workflows/knative-security.yaml b/.github/workflows/knative-security.yaml
deleted file mode 100644
index fbfe531d..00000000
--- a/.github/workflows/knative-security.yaml
+++ /dev/null
@@ -1,50 +0,0 @@
-# Copyright 2020 The Knative Authors.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#     http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-# This file is automagically synced here from github.com/knative-sandbox/.github
-# repo by knobots: https://github.com/knative-sandbox/knobots and will be overwritten.
-
-name: 'Security'
-
-on:
-  push:
-    branches: [ 'main', 'master', 'release-*' ]
-
-  pull_request:
-    branches: [ 'main', 'master', 'release-*' ]
-
-jobs:
-  analyze:
-    name: Analyze
-    runs-on: ubuntu-latest
-
-    steps:
-    - name: Checkout repository
-      uses: actions/checkout@v2
-      with:
-        # We must fetch at least the immediate parents so that if this is
-        # a pull request then we can checkout the head.
-        fetch-depth: 2
-
-    # Initializes the CodeQL tools for scanning.
-    - name: Initialize CodeQL
-      uses: github/codeql-action/init@v1
-      with:
-        languages: go
-
-    - name: Autobuild
-      uses: github/codeql-action/autobuild@v1
-
-    - name: Perform CodeQL Analysis
-      uses: github/codeql-action/analyze@v1
diff --git a/.github/workflows/knative-stale.yaml b/.github/workflows/knative-stale.yaml
deleted file mode 100644
index 97282697..00000000
--- a/.github/workflows/knative-stale.yaml
+++ /dev/null
@@ -1,49 +0,0 @@
-# Copyright 2020 The Knative Authors.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#     http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-# This file is automagically synced here from github.com/knative-sandbox/.github
-# repo by knobots: https://github.com/knative-sandbox/knobots and will be overwritten.
-
-name: 'Close stale'
-
-on:
-  schedule:
-  - cron: '0 1 * * *'
-
-jobs:
-  stale:
-    runs-on: 'ubuntu-latest'
-    steps:
-    - uses: 'actions/stale@v3'
-      with:
-        repo-token: '${{ secrets.GITHUB_TOKEN }}' # No need to setup
-
-        stale-issue-message: |-
-          This issue is stale because it has been open for 90 days with no
-          activity. It will automatically close after 30 more days of
-          inactivity. Reopen the issue with `/reopen`. Mark the issue as
-          fresh by adding the comment `/remove-lifecycle stale`.
-        stale-issue-label: 'lifecycle/stale'
-        exempt-issue-labels: 'lifecycle/frozen'
-
-        stale-pr-message: |-
-          This Pull Request is stale because it has been open for 90 days with
-          no activity. It will automatically close after 30 more days of
-          inactivity. Reopen with `/reopen`. Mark as fresh by adding the
-          comment `/remove-lifecycle stale`.
-        stale-pr-label: 'lifecycle/stale'
-        exempt-pr-labels: 'lifecycle/frozen'
-
-        days-before-stale: 90
-        days-before-close: 30
diff --git a/.github/workflows/stale.yaml b/.github/workflows/stale.yaml
new file mode 100644
index 00000000..12d7444c
--- /dev/null
+++ b/.github/workflows/stale.yaml
@@ -0,0 +1,30 @@
+name: 'Close stale'
+
+on:
+  schedule:
+  - cron: '0 1 * * *'
+
+jobs:
+  stale:
+    runs-on: 'ubuntu-latest'
+    steps:
+    - uses: 'actions/stale@v3'
+      with:
+        repo-token: ${{ secrets.GITHUB_TOKEN }}
+
+        stale-issue-message: |-
+          This issue is stale because it has been open for 90 days with no
+          activity. It will automatically close after 30 more days of
+          inactivity. Keep frash with the 'lifecycle/frozen' label.
+        stale-issue-label: 'lifecycle/stale'
+        exempt-issue-labels: 'lifecycle/frozen'
+
+        stale-pr-message: |-
+          This Pull Request is stale because it has been open for 90 days with
+          no activity. It will automatically close after 30 more days of
+          inactivity. Keep fresh with the 'lifecycle/frozen' label. 
+        stale-pr-label: 'lifecycle/stale'
+        exempt-pr-labels: 'lifecycle/frozen'
+
+        days-before-stale: 90
+        days-before-close: 30
diff --git a/.github/workflows/knative-style.yaml b/.github/workflows/style.yaml
similarity index 89%
rename from .github/workflows/knative-style.yaml
rename to .github/workflows/style.yaml
index c5770a4b..dda7e58f 100644
--- a/.github/workflows/knative-style.yaml
+++ b/.github/workflows/style.yaml
@@ -1,25 +1,8 @@
-# Copyright 2020 The Knative Authors.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#     http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-# This file is automagically synced here from github.com/knative-sandbox/.github
-# repo by knobots: https://github.com/knative-sandbox/knobots and will be overwritten.
-
 name: Code Style
 
 on:
   pull_request:
-    branches: [ 'main', 'master', 'release-*' ]
+    branches: ['main']
 
 jobs:
 
@@ -40,10 +23,10 @@ jobs:
           importpath: golang.org/x/tools/cmd/goimports
 
     steps:
-      - name: Set up Go 1.15.x
+      - name: Set up Go 1.16.x
         uses: actions/setup-go@v2
         with:
-          go-version: 1.15.x
+          go-version: 1.16.x
         id: go
 
       - name: Check out code
@@ -89,10 +72,10 @@ jobs:
     runs-on: ubuntu-latest
 
     steps:
-      - name: Set up Go 1.15.x
+      - name: Set up Go 1.16.x
         uses: actions/setup-go@v2
         with:
-          go-version: 1.15.x
+          go-version: 1.16.x
         id: go
 
       - name: Check out code
diff --git a/.github/workflows/knative-verify.yaml b/.github/workflows/verify.yaml
similarity index 51%
rename from .github/workflows/knative-verify.yaml
rename to .github/workflows/verify.yaml
index 5f20c548..b696f490 100644
--- a/.github/workflows/knative-verify.yaml
+++ b/.github/workflows/verify.yaml
@@ -1,64 +1,35 @@
-# Copyright 2020 The Knative Authors.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#     http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-# This file is automagically synced here from github.com/knative-sandbox/.github
-# repo by knobots: https://github.com/knative-sandbox/knobots and will be overwritten.
-
 name: Verify
 
 on:
   pull_request:
-    branches: [ 'main', 'master', 'release-*' ]
+    branches: ['main']
 
 jobs:
 
   verify:
     name: Verify Deps and Codegen
-    strategy:
-      matrix:
-        go-version: [1.15.x]
-        platform: [ubuntu-latest]
-
-    runs-on: ${{ matrix.platform }}
+    runs-on: ubuntu-latest
 
     env:
       GOPATH: ${{ github.workspace }}
 
     steps:
-
-    - name: Set up Go ${{ matrix.go-version }}
-      uses: actions/setup-go@v2
+    - uses: actions/setup-go@v2
       with:
-        go-version: ${{ matrix.go-version }}
-      id: go
+        go-version: 1.16.x
+    - name: Check out code onto GOPATH
+      uses: actions/checkout@v2
+      with:
+        path: ./src/${{ github.event.repository.name }}
 
     - name: Install Dependencies
       run: |
-        curl -L https://github.com/google/ko/releases/download/v0.6.0/ko_0.6.0_Linux_x86_64.tar.gz | tar xzf - ko
-        chmod +x ./ko
-        sudo mv ko /usr/local/bin
         go get github.com/google/go-licenses
 
-    - name: Check out code onto GOPATH
-      uses: actions/checkout@v2
-      with:
-        path: ./src/knative.dev/${{ github.event.repository.name }}
-
     - name: Update Codegen
       shell: bash
       run: |
-        pushd ./src/knative.dev/${{ github.event.repository.name }}
+        pushd ./src/${{ github.event.repository.name }}
         [[ ! -f hack/update-codegen.sh ]] || ./hack/update-codegen.sh
         popd
 
@@ -72,7 +43,7 @@ jobs:
           sed ':begin;$!N;s/\n/%0A/;tbegin'
         }
 
-        pushd ./src/knative.dev/${{ github.event.repository.name }}
+        pushd ./src/${{ github.event.repository.name }}
         if [[ -z "$(git status --porcelain)" ]]; then
             echo "${{ github.repository }} up to date."
         else
diff --git a/.goreleaser.yml b/.goreleaser.yml
index 95fd8173..0946f8e1 100644
--- a/.goreleaser.yml
+++ b/.goreleaser.yml
@@ -14,12 +14,17 @@ builds:
   - -trimpath
   ldflags:
   - "-s -w -X github.com/google/ko/pkg/commands.Version={{.Version}}"
+  goos:
+  - windows
+  - linux
+  - darwin
   goarch:
   - amd64
   - arm64
   - s390x
   - 386
   - mips64le
+  - ppc64le
 archives:
 - replacements:
     darwin: Darwin
diff --git a/README.md b/README.md
index 7c809481..2fc5a22c 100644
--- a/README.md
+++ b/README.md
@@ -20,33 +20,6 @@ fit for lightweight CI/CD use cases.
 `ko` also includes support for simple YAML templating which makes it a powerful
 tool for Kubernetes applications ([See below](#Kubernetes-Integration)).
 
-- [Setup](#setup)
-  - [Install](#install)
-    - [Install from Releases](#install-from-releases)
-    - [Install using Homebrew](#install-using-homebrew)
-    - [Build and Install from Source](#build-and-install-from-source)
-  - [Authenticate](#authenticate)
-  - [Choose Destination](#choose-destination)
-- [Build an Image](#build-an-image)
-  - [Configuration](#configuration)
-    - [Overriding Base Images](#overriding-base-images)
-  - [Naming Images](#naming-images)
-  - [Local Publishing Options](#local-publishing-options)
-  - [Multi-Platform Images](#multi-platform-images)
-  - [Static Assets](#static-assets)
-- [Kubernetes Integration](#kubernetes-integration)
-  - [YAML Changes](#yaml-changes)
-  - [`ko resolve`](#ko-resolve)
-  - [`ko apply`](#ko-apply)
-  - [`ko delete`](#ko-delete)
-- [Frequently Asked Questions](#frequently-asked-questions)
-  - [How can I set `ldflags`?](#how-can-i-set-ldflags)
-  - [Why are my images all created in 1970?](#why-are-my-images-all-created-in-1970)
-  - [Can I optimize images for eStargz support?](#can-i-optimize-images-for-estargz-support)
-  - [Does `ko` support autocompletion?](#does-ko-support-autocompletion)
-  - [Does `ko` work with Kustomize?](#does-ko-work-with-kustomize)
-- [Acknowledgements](#acknowledgements)
-
 # Setup
 
 ## Install
@@ -57,7 +30,7 @@ tool for Kubernetes applications ([See below](#Kubernetes-Integration)).
 VERSION=TODO # choose the latest version
 OS=Linux     # or Darwin
 ARCH=x86_64  # or arm64, i386, s390x
-curl -L https://github.com/google/ko/releases/download/${VERSION}/ko_${VERSION}_${OS}_${ARCH}.tar.gz | tar xzf - ko
+curl -L https://github.com/google/ko/releases/download/v${VERSION}/ko_${VERSION}_${OS}_${ARCH}.tar.gz | tar xzf - ko
 chmod +x ./ko
 ```
 
@@ -162,7 +135,8 @@ import path, to avoid collisions.
 
 - `--preserve-import-path` (`-P`) will include the entire importpath:
   `registry.example.com/repo/github.com/my-user/my-repo/cmd/app`
-- `--base` (`-B`) will omit the MD5 portion: `registry.example.com/repo/app`
+- `--base-import-paths` (`-B`) will omit the MD5 portion:
+  `registry.example.com/repo/app`
 - `--bare` will only include the `KO_DOCKER_REPO`: `registry.example.com/repo`
 
 ## Local Publishing Options
@@ -342,7 +316,7 @@ builds, or delete any previously built images.
 
 [Using -ldflags](https://blog.cloudflare.com/setting-go-variables-at-compile-time/)
 is a common way to embed version info in go binaries (In fact, we do this for
-`ko`!). Unforunately, because `ko` wraps `go build`, it's not possible to use
+`ko`!). Unfortunately, because `ko` wraps `go build`, it's not possible to use
 this flag directly; however, you can use the `GOFLAGS` environment variable
 instead:
 
@@ -399,6 +373,28 @@ Yes! `ko resolve -f -` will read and process input from stdin, so you can have
 kustomize build config | ko resolve -f -
 ```
 
+## Does `ko` work with [OpenShift Internal Registry](https://docs.openshift.com/container-platform/4.7/registry/registry-options.html#registry-integrated-openshift-registry_registry-options)?
+
+Yes! Follow these steps:
+
+- Connect to your OpenShift installation:
+  https://docs.openshift.com/container-platform/4.7/cli_reference/openshift_cli/getting-started-cli.html#cli-logging-in_cli-developer-commands
+- Expose the OpenShift Internal Registry so you can push to it:
+  https://docs.openshift.com/container-platform/4.7/registry/securing-exposing-registry.html
+- Export your token to `$HOME/.docker/config.json`:
+
+```sh
+oc registry login --to=$HOME/.docker/config.json
+```
+
+- Create a namespace where you will push your images, i.e: `ko-images`
+- Execute this command to set `KO_DOCKER_REPO` to publish images to the internal
+  registry.
+
+```sh
+   export KO_DOCKER_REPO=$(oc get route default-route -n openshift-image-registry --template='{{ .spec.host }}')/ko-images
+```
+
 # Acknowledgements
 
 This work is based heavily on learnings from having built the
@@ -406,3 +402,8 @@ This work is based heavily on learnings from having built the
 [Kubernetes](https://github.com/bazelbuild/rules_k8s) support for
 [Bazel](https://bazel.build). That work was presented
 [here](https://www.youtube.com/watch?v=RS1aiQqgUTA).
+
+# Discuss
+
+Questions? Comments? Ideas? Come discuss `ko` with us in the `#ko-project`
+channel on the [Kubernetes Slack](https://slack.k8s.io)! See you there!
diff --git a/go.mod b/go.mod
index 601891d4..db5639c7 100644
--- a/go.mod
+++ b/go.mod
@@ -10,7 +10,7 @@ require (
 	github.com/fsnotify/fsnotify v1.4.9
 	github.com/go-training/helloworld v0.0.0-20200225145412-ba5f4379d78b
 	github.com/google/go-cmp v0.5.4
-	github.com/google/go-containerregistry v0.4.1-0.20210216200643-d81088d9983e
+	github.com/google/go-containerregistry v0.5.0
 	github.com/gregjones/httpcache v0.0.0-20190212212710-3befbb6ad0cc // indirect
 	github.com/mattmoor/dep-notify v0.0.0-20190205035814-a45dec370a17
 	github.com/onsi/gomega v1.10.3 // indirect
diff --git a/go.sum b/go.sum
index 871886ca..0b47f021 100644
--- a/go.sum
+++ b/go.sum
@@ -185,6 +185,8 @@ github.com/google/go-cmp v0.5.4 h1:L8R9j+yAqZuZjsqh/z+F1NCffTKKLShY6zXTItVIZ8M=
 github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-containerregistry v0.4.1-0.20210216200643-d81088d9983e h1:8jPkeUOUhBGSDpuIUyjKHsC0I5/nI4zvmv6DB2TACh4=
 github.com/google/go-containerregistry v0.4.1-0.20210216200643-d81088d9983e/go.mod h1:Ct15B4yir3PLOP5jsy0GNeYVaIZs/MK/Jz5any1wFW0=
+github.com/google/go-containerregistry v0.5.0 h1:eb9sinv4PKm0AUwQGov0mvIdA4pyBGjRofxN4tWnMwM=
+github.com/google/go-containerregistry v0.5.0/go.mod h1:Ct15B4yir3PLOP5jsy0GNeYVaIZs/MK/Jz5any1wFW0=
 github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
 github.com/google/gofuzz v1.1.0 h1:Hsa8mG0dQ46ij8Sl2AYJDUv1oA9/d6Vk+3LG99Oe02g=
 github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
diff --git a/pkg/commands/apply.go b/pkg/commands/apply.go
index 1365bfdd..af9ad24b 100644
--- a/pkg/commands/apply.go
+++ b/pkg/commands/apply.go
@@ -15,8 +15,8 @@
 package commands
 
 import (
+	"errors"
 	"fmt"
-	"log"
 	"os"
 	"os/exec"
 
@@ -62,10 +62,9 @@ func addApply(topLevel *cobra.Command) {
   # Apply from stdin:
   cat config.yaml | ko apply -f -`,
 		Args: cobra.NoArgs,
-		Run: func(cmd *cobra.Command, args []string) {
+		RunE: func(cmd *cobra.Command, args []string) error {
 			if !isKubectlAvailable() {
-				log.Print("error: kubectl is not available. kubectl must be installed to use ko apply.")
-				return
+				return errors.New("error: kubectl is not available. kubectl must be installed to use ko apply")
 			}
 
 			// Cancel on signals.
@@ -73,11 +72,11 @@ func addApply(topLevel *cobra.Command) {
 
 			builder, err := makeBuilder(ctx, bo)
 			if err != nil {
-				log.Fatalf("error creating builder: %v", err)
+				return fmt.Errorf("error creating builder: %v", err)
 			}
 			publisher, err := makePublisher(po)
 			if err != nil {
-				log.Fatalf("error creating publisher: %v", err)
+				return fmt.Errorf("error creating publisher: %v", err)
 			}
 			defer publisher.Close()
 			// Create a set of ko-specific flags to ignore when passing through
@@ -110,7 +109,7 @@ func addApply(topLevel *cobra.Command) {
 			// Wire up kubectl stdin to resolveFilesToWriter.
 			stdin, err := kubectlCmd.StdinPipe()
 			if err != nil {
-				log.Fatalf("error piping to 'kubectl apply': %v", err)
+				return fmt.Errorf("error piping to 'kubectl apply': %v", err)
 			}
 
 			// Make sure builds are cancelled if kubectl apply fails.
@@ -138,9 +137,7 @@ func addApply(topLevel *cobra.Command) {
 				return nil
 			})
 
-			if err := g.Wait(); err != nil {
-				log.Fatal(err)
-			}
+			return g.Wait()
 		},
 	}
 	options.AddPublishArg(apply, po)
diff --git a/pkg/commands/create.go b/pkg/commands/create.go
index e91fb6a8..e29046bf 100644
--- a/pkg/commands/create.go
+++ b/pkg/commands/create.go
@@ -15,8 +15,8 @@
 package commands
 
 import (
+	"errors"
 	"fmt"
-	"log"
 	"os"
 	"os/exec"
 
@@ -62,10 +62,9 @@ func addCreate(topLevel *cobra.Command) {
   # Create from stdin:
   cat config.yaml | ko create -f -`,
 		Args: cobra.NoArgs,
-		Run: func(cmd *cobra.Command, args []string) {
+		RunE: func(cmd *cobra.Command, args []string) error {
 			if !isKubectlAvailable() {
-				log.Print("error: kubectl is not available. kubectl must be installed to use ko create.")
-				return
+				return errors.New("error: kubectl is not available. kubectl must be installed to use ko create")
 			}
 
 			// Cancel on signals.
@@ -73,11 +72,11 @@ func addCreate(topLevel *cobra.Command) {
 
 			builder, err := makeBuilder(ctx, bo)
 			if err != nil {
-				log.Fatalf("error creating builder: %v", err)
+				return fmt.Errorf("error creating builder: %v", err)
 			}
 			publisher, err := makePublisher(po)
 			if err != nil {
-				log.Fatalf("error creating publisher: %v", err)
+				return fmt.Errorf("error creating publisher: %v", err)
 			}
 			defer publisher.Close()
 			// Create a set of ko-specific flags to ignore when passing through
@@ -110,7 +109,7 @@ func addCreate(topLevel *cobra.Command) {
 			// Wire up kubectl stdin to resolveFilesToWriter.
 			stdin, err := kubectlCmd.StdinPipe()
 			if err != nil {
-				log.Fatalf("error piping to 'kubectl create': %v", err)
+				return fmt.Errorf("error piping to 'kubectl create': %v", err)
 			}
 
 			// Make sure builds are cancelled if kubectl create fails.
@@ -138,9 +137,7 @@ func addCreate(topLevel *cobra.Command) {
 				return nil
 			})
 
-			if err := g.Wait(); err != nil {
-				log.Fatal(err)
-			}
+			return g.Wait()
 		},
 	}
 	options.AddPublishArg(create, po)
diff --git a/pkg/commands/delete.go b/pkg/commands/delete.go
index 89f4f931..7f996e20 100644
--- a/pkg/commands/delete.go
+++ b/pkg/commands/delete.go
@@ -15,7 +15,7 @@
 package commands
 
 import (
-	"log"
+	"errors"
 	"os"
 	"os/exec"
 
@@ -23,15 +23,14 @@ import (
 )
 
 // runCmd is suitable for use with cobra.Command's Run field.
-type runCmd func(*cobra.Command, []string)
+type runCmd func(*cobra.Command, []string) error
 
 // passthru returns a runCmd that simply passes our CLI arguments
 // through to a binary named command.
 func passthru(command string) runCmd {
-	return func(_ *cobra.Command, _ []string) {
+	return func(_ *cobra.Command, _ []string) error {
 		if !isKubectlAvailable() {
-			log.Print("error: kubectl is not available. kubectl must be installed to use ko delete.")
-			return
+			return errors.New("error: kubectl is not available. kubectl must be installed to use ko delete")
 		}
 
 		// Cancel on signals.
@@ -49,9 +48,7 @@ func passthru(command string) runCmd {
 		cmd.Stdin = os.Stdin
 
 		// Run it.
-		if err := cmd.Run(); err != nil {
-			log.Fatalf("error executing %q command with args: %v; %v", command, os.Args[1:], err)
-		}
+		return cmd.Run()
 	}
 }
 
@@ -60,7 +57,7 @@ func addDelete(topLevel *cobra.Command) {
 	topLevel.AddCommand(&cobra.Command{
 		Use:   "delete",
 		Short: `See "kubectl help delete" for detailed usage.`,
-		Run:   passthru("kubectl"),
+		RunE:  passthru("kubectl"),
 		// We ignore unknown flags to avoid importing everything Go exposes
 		// from our commands.
 		FParseErrWhitelist: cobra.FParseErrWhitelist{
diff --git a/pkg/commands/options/publish.go b/pkg/commands/options/publish.go
index 928f3aab..b15911da 100644
--- a/pkg/commands/options/publish.go
+++ b/pkg/commands/options/publish.go
@@ -1,22 +1,25 @@
-// Copyright 2018 Google LLC All Rights Reserved.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//    http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
+/*
+Copyright 2021 Google LLC All Rights Reserved.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
 
 package options
 
 import (
 	"crypto/md5" //nolint: gosec // No strong cryptography needed.
 	"encoding/hex"
+	"os"
 	"path"
 
 	"github.com/google/ko/pkg/publish"
@@ -25,7 +28,16 @@ import (
 
 // PublishOptions encapsulates options when publishing.
 type PublishOptions struct {
+	// DockerRepo configures the destination image repository.
+	// In normal ko usage, this is populated with the value of $KO_DOCKER_REPO.
+	DockerRepo string
+
+	// LocalDomain overrides the default domain for images loaded into the local Docker daemon. Use with Local=true.
+	LocalDomain string
+
 	Tags []string
+	// TagOnly resolves images into tag-only references.
+	TagOnly bool
 
 	// Push publishes images to a registry.
 	Push bool
@@ -41,14 +53,22 @@ type PublishOptions struct {
 	PreserveImportPaths bool
 	// BaseImportPaths uses the base path without MD5 hash after KO_DOCKER_REPO.
 	BaseImportPaths bool
-	// Base uses a tag on the KO_DOCKER_REPO without anything additional.
+	// Bare uses a tag on the KO_DOCKER_REPO without anything additional.
 	Bare bool
 }
 
 func AddPublishArg(cmd *cobra.Command, po *PublishOptions) {
+	// Set DockerRepo from the KO_DOCKER_REPO envionment variable.
+	// See https://github.com/google/ko/pull/351 for flag discussion.
+	if dockerRepo, exists := os.LookupEnv("KO_DOCKER_REPO"); exists {
+		po.DockerRepo = dockerRepo
+	}
+
 	cmd.Flags().StringSliceVarP(&po.Tags, "tags", "t", []string{"latest"},
 		"Which tags to use for the produced image instead of the default 'latest' tag "+
 			"(may not work properly with --base-import-paths or --bare).")
+	cmd.Flags().BoolVar(&po.TagOnly, "tag-only", false,
+		"Include tags but not digests in resolved image references. Useful when digests are not preserved when images are repopulated.")
 
 	cmd.Flags().BoolVar(&po.Push, "push", true, "Push images to KO_DOCKER_REPO")
 
@@ -65,7 +85,7 @@ func AddPublishArg(cmd *cobra.Command, po *PublishOptions) {
 	cmd.Flags().BoolVarP(&po.BaseImportPaths, "base-import-paths", "B", po.BaseImportPaths,
 		"Whether to use the base path without MD5 hash after KO_DOCKER_REPO (may not work properly with --tags).")
 	cmd.Flags().BoolVar(&po.Bare, "bare", po.Bare,
-		"Whether to just use KO_DOCKER_REPO without additional context (will not work properly with --tags).")
+		"Whether to just use KO_DOCKER_REPO without additional context (may not work properly with --tags).")
 }
 
 func packageWithMD5(base, importpath string) string {
diff --git a/pkg/commands/publish.go b/pkg/commands/publish.go
index 236dbb94..370e46c0 100644
--- a/pkg/commands/publish.go
+++ b/pkg/commands/publish.go
@@ -16,7 +16,6 @@ package commands
 
 import (
 	"fmt"
-	"log"
 
 	"github.com/google/ko/pkg/commands/options"
 	"github.com/spf13/cobra"
@@ -57,24 +56,25 @@ func addPublish(topLevel *cobra.Command) {
   # This always preserves import paths.
   ko publish --local github.com/foo/bar/cmd/baz github.com/foo/bar/cmd/blah`,
 		Args: cobra.MinimumNArgs(1),
-		Run: func(_ *cobra.Command, args []string) {
+		RunE: func(_ *cobra.Command, args []string) error {
 			ctx := createCancellableContext()
 			builder, err := makeBuilder(ctx, bo)
 			if err != nil {
-				log.Fatalf("error creating builder: %v", err)
+				return fmt.Errorf("error creating builder: %v", err)
 			}
 			publisher, err := makePublisher(po)
 			if err != nil {
-				log.Fatalf("error creating publisher: %v", err)
+				return fmt.Errorf("error creating publisher: %v", err)
 			}
 			defer publisher.Close()
 			images, err := publishImages(ctx, args, publisher, builder)
 			if err != nil {
-				log.Fatalf("failed to publish images: %v", err)
+				return fmt.Errorf("failed to publish images: %v", err)
 			}
 			for _, img := range images {
 				fmt.Println(img)
 			}
+			return nil
 		},
 	}
 	options.AddPublishArg(publish, po)
diff --git a/pkg/commands/resolve.go b/pkg/commands/resolve.go
index b53c18c6..cfc7cb3f 100644
--- a/pkg/commands/resolve.go
+++ b/pkg/commands/resolve.go
@@ -15,7 +15,7 @@
 package commands
 
 import (
-	"log"
+	"fmt"
 	"os"
 
 	"github.com/google/ko/pkg/commands/options"
@@ -54,20 +54,18 @@ func addResolve(topLevel *cobra.Command) {
   # This always preserves import paths.
   ko resolve --local -f config/`,
 		Args: cobra.NoArgs,
-		Run: func(cmd *cobra.Command, args []string) {
+		RunE: func(cmd *cobra.Command, args []string) error {
 			ctx := createCancellableContext()
 			builder, err := makeBuilder(ctx, bo)
 			if err != nil {
-				log.Fatalf("error creating builder: %v", err)
+				return fmt.Errorf("error creating builder: %v", err)
 			}
 			publisher, err := makePublisher(po)
 			if err != nil {
-				log.Fatalf("error creating publisher: %v", err)
+				return fmt.Errorf("error creating publisher: %v", err)
 			}
 			defer publisher.Close()
-			if err := resolveFilesToWriter(ctx, builder, publisher, fo, so, os.Stdout); err != nil {
-				log.Fatal(err)
-			}
+			return resolveFilesToWriter(ctx, builder, publisher, fo, so, os.Stdout)
 		},
 	}
 	options.AddPublishArg(resolve, po)
diff --git a/pkg/commands/resolver.go b/pkg/commands/resolver.go
index 7e9acc1f..2d813196 100644
--- a/pkg/commands/resolver.go
+++ b/pkg/commands/resolver.go
@@ -1,16 +1,18 @@
-// Copyright 2018 Google LLC All Rights Reserved.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//    http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
+/*
+Copyright 2021 Google LLC All Rights Reserved.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
 
 package commands
 
@@ -131,13 +133,14 @@ func makePublisher(po *options.PublishOptions) (publish.Interface, error) {
 	// Create the publish.Interface that we will use to publish image references
 	// to either a docker daemon or a container image registry.
 	innerPublisher, err := func() (publish.Interface, error) {
-		repoName := os.Getenv("KO_DOCKER_REPO")
+		repoName := po.DockerRepo
 		namer := options.MakeNamer(po)
 		if repoName == publish.LocalDomain || po.Local {
 			// TODO(jonjohnsonjr): I'm assuming that nobody will
 			// use local with other publishers, but that might
 			// not be true.
-			return publish.NewDaemon(namer, po.Tags), nil
+			return publish.NewDaemon(namer, po.Tags,
+				publish.WithLocalDomain(po.LocalDomain))
 		}
 		if repoName == publish.KindDomain {
 			return publish.NewKindPublisher(namer, po.Tags), nil
@@ -170,6 +173,7 @@ func makePublisher(po *options.PublishOptions) (publish.Interface, error) {
 				publish.WithAuthFromKeychain(authn.DefaultKeychain),
 				publish.WithNamer(namer),
 				publish.WithTags(po.Tags),
+				publish.WithTagOnly(po.TagOnly),
 				publish.Insecure(po.InsecureRegistry))
 			if err != nil {
 				return nil, err
diff --git a/pkg/commands/run.go b/pkg/commands/run.go
index 87d85214..dd88d180 100644
--- a/pkg/commands/run.go
+++ b/pkg/commands/run.go
@@ -15,6 +15,8 @@
 package commands
 
 import (
+	"errors"
+	"fmt"
 	"log"
 	"os"
 	"os/exec"
@@ -46,7 +48,7 @@ func addRun(topLevel *cobra.Command) {
 
   # You can also supply args and flags to the command.
   ko run ./cmd/baz -- -v arg1 arg2 --yes`,
-		Run: func(cmd *cobra.Command, args []string) {
+		RunE: func(cmd *cobra.Command, args []string) error {
 			ctx := createCancellableContext()
 
 			// Args after -- are for kubectl, so only consider importPaths before it.
@@ -56,7 +58,7 @@ func addRun(topLevel *cobra.Command) {
 				importPaths = args[:cmd.Flags().ArgsLenAtDash()]
 			}
 			if len(importPaths) == 0 {
-				log.Fatalf("ko run: no importpaths listed")
+				return errors.New("ko run: no importpaths listed")
 			}
 
 			kubectlArgs := []string{}
@@ -67,24 +69,24 @@ func addRun(topLevel *cobra.Command) {
 
 			builder, err := makeBuilder(ctx, bo)
 			if err != nil {
-				log.Fatalf("error creating builder: %v", err)
+				return fmt.Errorf("error creating builder: %v", err)
 			}
 			publisher, err := makePublisher(po)
 			if err != nil {
-				log.Fatalf("error creating publisher: %v", err)
+				return fmt.Errorf("error creating publisher: %v", err)
 			}
 			defer publisher.Close()
 
 			if len(os.Args) < 3 {
-				log.Fatalf("usage: %s run <package>", os.Args[0])
+				return fmt.Errorf("usage: %s run <package>", os.Args[0])
 			}
 			ip := os.Args[2]
 			if strings.HasPrefix(ip, "-") {
-				log.Fatalf("expected first arg to be positional, got %q", ip)
+				return fmt.Errorf("expected first arg to be positional, got %q", ip)
 			}
 			imgs, err := publishImages(ctx, importPaths, publisher, builder)
 			if err != nil {
-				log.Fatalf("failed to publish images: %v", err)
+				return fmt.Errorf("failed to publish images: %v", err)
 			}
 
 			// Usually only one, but this is the simple way to access the
@@ -127,9 +129,10 @@ func addRun(topLevel *cobra.Command) {
 
 				// Run it.
 				if err := kubectlCmd.Run(); err != nil {
-					log.Fatalf("error executing \"kubectl run\": %v", err)
+					return err
 				}
 			}
+			return nil
 		},
 		// We ignore unknown flags to avoid importing everything Go exposes
 		// from our commands.
diff --git a/pkg/publish/daemon.go b/pkg/publish/daemon.go
index 2bce7e15..1b0818a6 100644
--- a/pkg/publish/daemon.go
+++ b/pkg/publish/daemon.go
@@ -1,16 +1,18 @@
-// Copyright 2018 Google LLC All Rights Reserved.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//    http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
+/*
+Copyright 2018 Google LLC All Rights Reserved.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
 
 package publish
 
@@ -34,17 +36,46 @@ const (
 
 // demon is intentionally misspelled to avoid name collision (and drive Jon nuts).
 type demon struct {
+	base  string
 	namer Namer
 	tags  []string
 }
 
+// DaemonOption is a functional option for NewDaemon.
+type DaemonOption func(*demon) error
+
+// WithLocalDomain is a functional option for overriding the domain used for images that are side-loaded into the daemon.
+func WithLocalDomain(domain string) DaemonOption {
+	return func(i *demon) error {
+		if domain != "" {
+			i.base = domain
+		}
+		return nil
+	}
+}
+
 // NewDaemon returns a new publish.Interface that publishes images to a container daemon.
-func NewDaemon(namer Namer, tags []string) Interface {
-	return &demon{namer, tags}
+func NewDaemon(namer Namer, tags []string, opts ...DaemonOption) (Interface, error) {
+	d := &demon{
+		base:  LocalDomain,
+		namer: namer,
+		tags:  tags,
+	}
+	for _, option := range opts {
+		if err := option(d); err != nil {
+			return nil, err
+		}
+	}
+	return d, nil
+}
+
+// getOpts returns daemon.Options. It's a var to allow it to be overridden during tests.
+var getOpts = func(ctx context.Context) []daemon.Option {
+	return []daemon.Option{daemon.WithContext(ctx)}
 }
 
 // Publish implements publish.Interface
-func (d *demon) Publish(_ context.Context, br build.Result, s string) (name.Reference, error) {
+func (d *demon) Publish(ctx context.Context, br build.Result, s string) (name.Reference, error) {
 	s = strings.TrimPrefix(s, build.StrictScheme)
 	// https://github.com/google/go-containerregistry/issues/212
 	s = strings.ToLower(s)
@@ -94,27 +125,25 @@ func (d *demon) Publish(_ context.Context, br build.Result, s string) (name.Refe
 		return nil, err
 	}
 
-	digestTag, err := name.NewTag(fmt.Sprintf("%s:%s", d.namer(LocalDomain, s), h.Hex))
+	digestTag, err := name.NewTag(fmt.Sprintf("%s:%s", d.namer(d.base, s), h.Hex))
 	if err != nil {
 		return nil, err
 	}
 
 	log.Printf("Loading %v", digestTag)
-	if _, err := daemon.Write(digestTag, img); err != nil {
+	if _, err := daemon.Write(digestTag, img, getOpts(ctx)...); err != nil {
 		return nil, err
 	}
 	log.Printf("Loaded %v", digestTag)
 
 	for _, tagName := range d.tags {
 		log.Printf("Adding tag %v", tagName)
-		tag, err := name.NewTag(fmt.Sprintf("%s:%s", d.namer(LocalDomain, s), tagName))
+		tag, err := name.NewTag(fmt.Sprintf("%s:%s", d.namer(d.base, s), tagName))
 		if err != nil {
 			return nil, err
 		}
 
-		err = daemon.Tag(digestTag, tag)
-
-		if err != nil {
+		if err := daemon.Tag(digestTag, tag, getOpts(ctx)...); err != nil {
 			return nil, err
 		}
 		log.Printf("Added tag %v", tagName)
diff --git a/pkg/publish/default.go b/pkg/publish/default.go
index 44f97301..8b73c867 100644
--- a/pkg/publish/default.go
+++ b/pkg/publish/default.go
@@ -16,6 +16,7 @@ package publish
 
 import (
 	"context"
+	"errors"
 	"fmt"
 	"log"
 	"net/http"
@@ -38,6 +39,7 @@ type defalt struct {
 	auth      authn.Authenticator
 	namer     Namer
 	tags      []string
+	tagOnly   bool
 	insecure  bool
 }
 
@@ -51,6 +53,7 @@ type defaultOpener struct {
 	auth      authn.Authenticator
 	namer     Namer
 	tags      []string
+	tagOnly   bool
 	insecure  bool
 }
 
@@ -69,6 +72,15 @@ func identity(base, in string) string { return path.Join(base, in) }
 var defaultTags = []string{"latest"}
 
 func (do *defaultOpener) Open() (Interface, error) {
+	if do.tagOnly {
+		if len(do.tags) != 1 {
+			return nil, errors.New("must specify exactly one tag to resolve images into tag-only references")
+		}
+		if do.tags[0] == defaultTags[0] {
+			return nil, errors.New("latest tag cannot be used in tag-only references")
+		}
+	}
+
 	return &defalt{
 		base:      do.base,
 		t:         do.t,
@@ -76,6 +88,7 @@ func (do *defaultOpener) Open() (Interface, error) {
 		auth:      do.auth,
 		namer:     do.namer,
 		tags:      do.tags,
+		tagOnly:   do.tagOnly,
 		insecure:  do.insecure,
 	}, nil
 }
@@ -156,6 +169,15 @@ func (d *defalt) Publish(ctx context.Context, br build.Result, s string) (name.R
 		}
 	}
 
+	if d.tagOnly {
+		// We have already validated that there is a single tag (not latest).
+		tag, err := name.NewTag(fmt.Sprintf("%s:%s", d.namer(d.base, s), d.tags[0]))
+		if err != nil {
+			return nil, err
+		}
+		return &tag, nil
+	}
+
 	h, err := br.Digest()
 	if err != nil {
 		return nil, err
diff --git a/pkg/publish/kind.go b/pkg/publish/kind.go
index 48f7598c..f20303cb 100644
--- a/pkg/publish/kind.go
+++ b/pkg/publish/kind.go
@@ -46,7 +46,7 @@ func NewKindPublisher(namer Namer, tags []string) Interface {
 }
 
 // Publish implements publish.Interface.
-func (t *kindPublisher) Publish(_ context.Context, br build.Result, s string) (name.Reference, error) {
+func (t *kindPublisher) Publish(ctx context.Context, br build.Result, s string) (name.Reference, error) {
 	s = strings.TrimPrefix(s, build.StrictScheme)
 	// https://github.com/google/go-containerregistry/issues/212
 	s = strings.ToLower(s)
@@ -102,7 +102,7 @@ func (t *kindPublisher) Publish(_ context.Context, br build.Result, s string) (n
 	}
 
 	log.Printf("Loading %v", digestTag)
-	if err := kind.Write(digestTag, img); err != nil {
+	if err := kind.Write(ctx, digestTag, img); err != nil {
 		return nil, err
 	}
 	log.Printf("Loaded %v", digestTag)
@@ -114,7 +114,7 @@ func (t *kindPublisher) Publish(_ context.Context, br build.Result, s string) (n
 			return nil, err
 		}
 
-		if err := kind.Tag(digestTag, tag); err != nil {
+		if err := kind.Tag(ctx, digestTag, tag); err != nil {
 			return nil, err
 		}
 		log.Printf("Added tag %v", tagName)
diff --git a/pkg/publish/kind/write.go b/pkg/publish/kind/write.go
index 0e9bb064..179615e3 100644
--- a/pkg/publish/kind/write.go
+++ b/pkg/publish/kind/write.go
@@ -15,6 +15,7 @@
 package kind
 
 import (
+	"context"
 	"fmt"
 	"io"
 	"os"
@@ -43,9 +44,9 @@ var GetProvider = func() provider {
 }
 
 // Tag adds a tag to an already existent image.
-func Tag(src, dest name.Tag) error {
+func Tag(ctx context.Context, src, dest name.Tag) error {
 	return onEachNode(func(n nodes.Node) error {
-		cmd := n.Command("ctr", "--namespace=k8s.io", "images", "tag", "--force", src.String(), dest.String())
+		cmd := n.CommandContext(ctx, "ctr", "--namespace=k8s.io", "images", "tag", "--force", src.String(), dest.String())
 		if err := cmd.Run(); err != nil {
 			return fmt.Errorf("failed to tag image: %w", err)
 		}
@@ -54,7 +55,7 @@ func Tag(src, dest name.Tag) error {
 }
 
 // Write saves the image into the kind nodes as the given tag.
-func Write(tag name.Tag, img v1.Image) error {
+func Write(ctx context.Context, tag name.Tag, img v1.Image) error {
 	return onEachNode(func(n nodes.Node) error {
 		pr, pw := io.Pipe()
 
@@ -63,7 +64,7 @@ func Write(tag name.Tag, img v1.Image) error {
 			return pw.CloseWithError(tarball.Write(tag, img, pw))
 		})
 
-		cmd := n.Command("ctr", "--namespace=k8s.io", "images", "import", "-").SetStdin(pr)
+		cmd := n.CommandContext(ctx, "ctr", "--namespace=k8s.io", "images", "import", "-").SetStdin(pr)
 		if err := cmd.Run(); err != nil {
 			return fmt.Errorf("failed to load image to node %q: %w", n, err)
 		}
diff --git a/pkg/publish/options.go b/pkg/publish/options.go
index 4f8cceeb..8c95f397 100644
--- a/pkg/publish/options.go
+++ b/pkg/publish/options.go
@@ -94,6 +94,14 @@ func WithTags(tags []string) Option {
 	}
 }
 
+// WithTagOnly is a functional option for resolving images into tag-only references
+func WithTagOnly(tagOnly bool) Option {
+	return func(i *defaultOpener) error {
+		i.tagOnly = tagOnly
+		return nil
+	}
+}
+
 func Insecure(b bool) Option {
 	return func(i *defaultOpener) error {
 		i.insecure = b
diff --git a/vendor/github.com/google/go-containerregistry/cmd/crane/cmd/append.go b/vendor/github.com/google/go-containerregistry/cmd/crane/cmd/append.go
index 89b0bb25..a4addd11 100644
--- a/vendor/github.com/google/go-containerregistry/cmd/crane/cmd/append.go
+++ b/vendor/github.com/google/go-containerregistry/cmd/crane/cmd/append.go
@@ -15,10 +15,11 @@
 package cmd
 
 import (
-	"log"
+	"fmt"
 
 	"github.com/google/go-containerregistry/pkg/crane"
 	"github.com/google/go-containerregistry/pkg/logs"
+	"github.com/google/go-containerregistry/pkg/name"
 	v1 "github.com/google/go-containerregistry/pkg/v1"
 	"github.com/google/go-containerregistry/pkg/v1/empty"
 	"github.com/spf13/cobra"
@@ -33,35 +34,44 @@ func NewCmdAppend(options *[]crane.Option) *cobra.Command {
 		Use:   "append",
 		Short: "Append contents of a tarball to a remote image",
 		Args:  cobra.NoArgs,
-		Run: func(_ *cobra.Command, args []string) {
+		RunE: func(_ *cobra.Command, args []string) error {
 			var base v1.Image
 			var err error
 
 			if baseRef == "" {
 				logs.Warn.Printf("base unspecified, using empty image")
 				base = empty.Image
-
 			} else {
 				base, err = crane.Pull(baseRef, *options...)
 				if err != nil {
-					log.Fatalf("pulling %s: %v", baseRef, err)
+					return fmt.Errorf("pulling %s: %v", baseRef, err)
 				}
 			}
 
 			img, err := crane.Append(base, newLayers...)
 			if err != nil {
-				log.Fatalf("appending %v: %v", newLayers, err)
+				return fmt.Errorf("appending %v: %v", newLayers, err)
 			}
 
 			if outFile != "" {
 				if err := crane.Save(img, newTag, outFile); err != nil {
-					log.Fatalf("writing output %q: %v", outFile, err)
+					return fmt.Errorf("writing output %q: %v", outFile, err)
 				}
 			} else {
 				if err := crane.Push(img, newTag, *options...); err != nil {
-					log.Fatalf("pushing image %s: %v", newTag, err)
+					return fmt.Errorf("pushing image %s: %v", newTag, err)
+				}
+				ref, err := name.ParseReference(newTag)
+				if err != nil {
+					return fmt.Errorf("parsing reference %s: %v", newTag, err)
+				}
+				d, err := img.Digest()
+				if err != nil {
+					return fmt.Errorf("digest: %v", err)
 				}
+				fmt.Println(ref.Context().Digest(d.String()))
 			}
+			return nil
 		},
 	}
 	appendCmd.Flags().StringVarP(&baseRef, "base", "b", "", "Name of base image to append to")
diff --git a/vendor/github.com/google/go-containerregistry/cmd/crane/cmd/auth.go b/vendor/github.com/google/go-containerregistry/cmd/crane/cmd/auth.go
index 93ef6565..4c2ab2a4 100644
--- a/vendor/github.com/google/go-containerregistry/cmd/crane/cmd/auth.go
+++ b/vendor/github.com/google/go-containerregistry/cmd/crane/cmd/auth.go
@@ -78,18 +78,18 @@ func NewCmdAuthGet(argv ...string) *cobra.Command {
 		Short:   "Implements a credential helper",
 		Example: eg,
 		Args:    cobra.NoArgs,
-		Run: func(_ *cobra.Command, args []string) {
+		RunE: func(_ *cobra.Command, args []string) error {
 			b, err := ioutil.ReadAll(os.Stdin)
 			if err != nil {
-				log.Fatal(err)
+				return err
 			}
 			reg, err := name.NewRegistry(strings.TrimSpace(string(b)))
 			if err != nil {
-				log.Fatal(err)
+				return err
 			}
 			authorizer, err := authn.DefaultKeychain.Resolve(reg)
 			if err != nil {
-				log.Fatal(err)
+				return err
 			}
 
 			// If we don't find any credentials, there's a magic error to return:
@@ -103,15 +103,13 @@ func NewCmdAuthGet(argv ...string) *cobra.Command {
 
 			auth, err := authorizer.Authorization()
 			if err != nil {
-				log.Fatal(err)
+				return err
 			}
 
 			// Convert back to a form that credential helpers can parse so that this
 			// can act as a meta credential helper.
 			creds := toCreds(auth)
-			if err := json.NewEncoder(os.Stdout).Encode(creds); err != nil {
-				log.Fatal(err)
-			}
+			return json.NewEncoder(os.Stdout).Encode(creds)
 		},
 	}
 }
@@ -132,17 +130,15 @@ func NewCmdAuthLogin(argv ...string) *cobra.Command {
 		Short:   "Log in to a registry",
 		Example: eg,
 		Args:    cobra.ExactArgs(1),
-		Run: func(cmd *cobra.Command, args []string) {
+		RunE: func(cmd *cobra.Command, args []string) error {
 			reg, err := name.NewRegistry(args[0])
 			if err != nil {
-				log.Fatal(err)
+				return err
 			}
 
 			opts.serverAddress = reg.Name()
 
-			if err := login(opts); err != nil {
-				log.Fatal(err)
-			}
+			return login(opts)
 		},
 	}
 
diff --git a/vendor/github.com/google/go-containerregistry/cmd/crane/cmd/blob.go b/vendor/github.com/google/go-containerregistry/cmd/crane/cmd/blob.go
index 7ddc6be3..ca32a983 100644
--- a/vendor/github.com/google/go-containerregistry/cmd/crane/cmd/blob.go
+++ b/vendor/github.com/google/go-containerregistry/cmd/crane/cmd/blob.go
@@ -15,8 +15,8 @@
 package cmd
 
 import (
+	"fmt"
 	"io"
-	"log"
 
 	"github.com/google/go-containerregistry/pkg/crane"
 	"github.com/spf13/cobra"
@@ -29,19 +29,20 @@ func NewCmdBlob(options *[]crane.Option) *cobra.Command {
 		Short:   "Read a blob from the registry",
 		Example: "crane blob ubuntu@sha256:4c1d20cdee96111c8acf1858b62655a37ce81ae48648993542b7ac363ac5c0e5 > blob.tar.gz",
 		Args:    cobra.ExactArgs(1),
-		Run: func(cmd *cobra.Command, args []string) {
+		RunE: func(cmd *cobra.Command, args []string) error {
 			src := args[0]
 			layer, err := crane.PullLayer(src, *options...)
 			if err != nil {
-				log.Fatalf("pulling layer %s: %v", src, err)
+				return fmt.Errorf("pulling layer %s: %v", src, err)
 			}
 			blob, err := layer.Compressed()
 			if err != nil {
-				log.Fatalf("fetching blob %s: %v", src, err)
+				return fmt.Errorf("fetching blob %s: %v", src, err)
 			}
 			if _, err := io.Copy(cmd.OutOrStdout(), blob); err != nil {
-				log.Fatalf("copying blob %s: %v", src, err)
+				return fmt.Errorf("copying blob %s: %v", src, err)
 			}
+			return nil
 		},
 	}
 }
diff --git a/vendor/github.com/google/go-containerregistry/cmd/crane/cmd/catalog.go b/vendor/github.com/google/go-containerregistry/cmd/crane/cmd/catalog.go
index 2c82af57..f8eb7cf1 100644
--- a/vendor/github.com/google/go-containerregistry/cmd/crane/cmd/catalog.go
+++ b/vendor/github.com/google/go-containerregistry/cmd/crane/cmd/catalog.go
@@ -16,7 +16,6 @@ package cmd
 
 import (
 	"fmt"
-	"log"
 
 	"github.com/google/go-containerregistry/pkg/crane"
 	"github.com/spf13/cobra"
@@ -28,16 +27,17 @@ func NewCmdCatalog(options *[]crane.Option) *cobra.Command {
 		Use:   "catalog",
 		Short: "List the repos in a registry",
 		Args:  cobra.ExactArgs(1),
-		Run: func(_ *cobra.Command, args []string) {
+		RunE: func(_ *cobra.Command, args []string) error {
 			reg := args[0]
 			repos, err := crane.Catalog(reg, *options...)
 			if err != nil {
-				log.Fatalf("reading repos for %s: %v", reg, err)
+				return fmt.Errorf("reading repos for %s: %v", reg, err)
 			}
 
 			for _, repo := range repos {
 				fmt.Println(repo)
 			}
+			return nil
 		},
 	}
 }
diff --git a/vendor/github.com/google/go-containerregistry/cmd/crane/cmd/config.go b/vendor/github.com/google/go-containerregistry/cmd/crane/cmd/config.go
index d2339c1c..4fa55162 100644
--- a/vendor/github.com/google/go-containerregistry/cmd/crane/cmd/config.go
+++ b/vendor/github.com/google/go-containerregistry/cmd/crane/cmd/config.go
@@ -16,7 +16,6 @@ package cmd
 
 import (
 	"fmt"
-	"log"
 
 	"github.com/google/go-containerregistry/pkg/crane"
 	"github.com/spf13/cobra"
@@ -28,12 +27,13 @@ func NewCmdConfig(options *[]crane.Option) *cobra.Command {
 		Use:   "config IMAGE",
 		Short: "Get the config of an image",
 		Args:  cobra.ExactArgs(1),
-		Run: func(_ *cobra.Command, args []string) {
+		RunE: func(_ *cobra.Command, args []string) error {
 			cfg, err := crane.Config(args[0], *options...)
 			if err != nil {
-				log.Fatalf("fetching config: %v", err)
+				return fmt.Errorf("fetching config: %v", err)
 			}
 			fmt.Print(string(cfg))
+			return nil
 		},
 	}
 }
diff --git a/vendor/github.com/google/go-containerregistry/cmd/crane/cmd/copy.go b/vendor/github.com/google/go-containerregistry/cmd/crane/cmd/copy.go
index 993233a8..107d00b8 100644
--- a/vendor/github.com/google/go-containerregistry/cmd/crane/cmd/copy.go
+++ b/vendor/github.com/google/go-containerregistry/cmd/crane/cmd/copy.go
@@ -15,8 +15,6 @@
 package cmd
 
 import (
-	"log"
-
 	"github.com/google/go-containerregistry/pkg/crane"
 	"github.com/spf13/cobra"
 )
@@ -28,11 +26,9 @@ func NewCmdCopy(options *[]crane.Option) *cobra.Command {
 		Aliases: []string{"cp"},
 		Short:   "Efficiently copy a remote image from src to dst",
 		Args:    cobra.ExactArgs(2),
-		Run: func(_ *cobra.Command, args []string) {
+		RunE: func(_ *cobra.Command, args []string) error {
 			src, dst := args[0], args[1]
-			if err := crane.Copy(src, dst, *options...); err != nil {
-				log.Fatal(err)
-			}
+			return crane.Copy(src, dst, *options...)
 		},
 	}
 }
diff --git a/vendor/github.com/google/go-containerregistry/cmd/crane/cmd/delete.go b/vendor/github.com/google/go-containerregistry/cmd/crane/cmd/delete.go
index c5839abf..18966ea1 100644
--- a/vendor/github.com/google/go-containerregistry/cmd/crane/cmd/delete.go
+++ b/vendor/github.com/google/go-containerregistry/cmd/crane/cmd/delete.go
@@ -15,8 +15,6 @@
 package cmd
 
 import (
-	"log"
-
 	"github.com/google/go-containerregistry/pkg/crane"
 	"github.com/spf13/cobra"
 )
@@ -27,11 +25,9 @@ func NewCmdDelete(options *[]crane.Option) *cobra.Command {
 		Use:   "delete IMAGE",
 		Short: "Delete an image reference from its registry",
 		Args:  cobra.ExactArgs(1),
-		Run: func(_ *cobra.Command, args []string) {
+		RunE: func(_ *cobra.Command, args []string) error {
 			ref := args[0]
-			if err := crane.Delete(ref, *options...); err != nil {
-				log.Fatalf("deleting %s: %v", ref, err)
-			}
+			return crane.Delete(ref, *options...)
 		},
 	}
 }
diff --git a/vendor/github.com/google/go-containerregistry/cmd/crane/cmd/digest.go b/vendor/github.com/google/go-containerregistry/cmd/crane/cmd/digest.go
index 24659e41..26e30218 100644
--- a/vendor/github.com/google/go-containerregistry/cmd/crane/cmd/digest.go
+++ b/vendor/github.com/google/go-containerregistry/cmd/crane/cmd/digest.go
@@ -15,8 +15,8 @@
 package cmd
 
 import (
+	"errors"
 	"fmt"
-	"log"
 
 	"github.com/google/go-containerregistry/pkg/crane"
 	"github.com/spf13/cobra"
@@ -24,16 +24,52 @@ import (
 
 // NewCmdDigest creates a new cobra.Command for the digest subcommand.
 func NewCmdDigest(options *[]crane.Option) *cobra.Command {
-	return &cobra.Command{
+	var tarball string
+	cmd := &cobra.Command{
 		Use:   "digest IMAGE",
 		Short: "Get the digest of an image",
-		Args:  cobra.ExactArgs(1),
-		Run: func(_ *cobra.Command, args []string) {
-			digest, err := crane.Digest(args[0], *options...)
+		Args:  cobra.MaximumNArgs(1),
+		RunE: func(cmd *cobra.Command, args []string) error {
+			if tarball == "" && len(args) == 0 {
+				cmd.Help()
+				return errors.New("image reference required without --tarball")
+			}
+
+			digest, err := getDigest(tarball, args, options)
 			if err != nil {
-				log.Fatalf("computing digest: %v", err)
+				return err
 			}
 			fmt.Println(digest)
+			return nil
 		},
 	}
+
+	cmd.Flags().StringVar(&tarball, "tarball", "", "(Optional) path to tarball containing the image")
+
+	return cmd
+}
+
+func getDigest(tarball string, args []string, options *[]crane.Option) (string, error) {
+	if tarball != "" {
+		return getTarballDigest(tarball, args, options)
+	}
+
+	return crane.Digest(args[0], *options...)
+}
+
+func getTarballDigest(tarball string, args []string, options *[]crane.Option) (string, error) {
+	tag := ""
+	if len(args) > 0 {
+		tag = args[0]
+	}
+
+	img, err := crane.LoadTag(tarball, tag, *options...)
+	if err != nil {
+		return "", fmt.Errorf("loading image from %q: %v", tarball, err)
+	}
+	digest, err := img.Digest()
+	if err != nil {
+		return "", fmt.Errorf("computing digest: %v", err)
+	}
+	return digest.String(), nil
 }
diff --git a/vendor/github.com/google/go-containerregistry/cmd/crane/cmd/export.go b/vendor/github.com/google/go-containerregistry/cmd/crane/cmd/export.go
index 1c04da1b..268fa8ac 100644
--- a/vendor/github.com/google/go-containerregistry/cmd/crane/cmd/export.go
+++ b/vendor/github.com/google/go-containerregistry/cmd/crane/cmd/export.go
@@ -15,7 +15,7 @@
 package cmd
 
 import (
-	"log"
+	"fmt"
 	"os"
 
 	"github.com/google/go-containerregistry/pkg/crane"
@@ -33,23 +33,21 @@ func NewCmdExport(options *[]crane.Option) *cobra.Command {
   # Write tarball to file
   crane export ubuntu ubuntu.tar`,
 		Args: cobra.ExactArgs(2),
-		Run: func(_ *cobra.Command, args []string) {
+		RunE: func(_ *cobra.Command, args []string) error {
 			src, dst := args[0], args[1]
 
 			f, err := openFile(dst)
 			if err != nil {
-				log.Fatalf("failed to open %s: %v", dst, err)
+				return fmt.Errorf("failed to open %s: %v", dst, err)
 			}
 			defer f.Close()
 
 			img, err := crane.Pull(src, *options...)
 			if err != nil {
-				log.Fatal(err)
+				return fmt.Errorf("pulling %s: %v", src, err)
 			}
 
-			if err := crane.Export(img, f); err != nil {
-				log.Fatalf("exporting %s: %v", src, err)
-			}
+			return crane.Export(img, f)
 		},
 	}
 }
diff --git a/vendor/github.com/google/go-containerregistry/cmd/crane/cmd/list.go b/vendor/github.com/google/go-containerregistry/cmd/crane/cmd/list.go
index f8667dbd..0db1473b 100644
--- a/vendor/github.com/google/go-containerregistry/cmd/crane/cmd/list.go
+++ b/vendor/github.com/google/go-containerregistry/cmd/crane/cmd/list.go
@@ -16,7 +16,6 @@ package cmd
 
 import (
 	"fmt"
-	"log"
 
 	"github.com/google/go-containerregistry/pkg/crane"
 	"github.com/spf13/cobra"
@@ -28,16 +27,17 @@ func NewCmdList(options *[]crane.Option) *cobra.Command {
 		Use:   "ls REPO",
 		Short: "List the tags in a repo",
 		Args:  cobra.ExactArgs(1),
-		Run: func(_ *cobra.Command, args []string) {
+		RunE: func(_ *cobra.Command, args []string) error {
 			repo := args[0]
 			tags, err := crane.ListTags(repo, *options...)
 			if err != nil {
-				log.Fatalf("reading tags for %s: %v", repo, err)
+				return fmt.Errorf("reading tags for %s: %v", repo, err)
 			}
 
 			for _, tag := range tags {
 				fmt.Println(tag)
 			}
+			return nil
 		},
 	}
 }
diff --git a/vendor/github.com/google/go-containerregistry/cmd/crane/cmd/manifest.go b/vendor/github.com/google/go-containerregistry/cmd/crane/cmd/manifest.go
index 35df558c..80d70b02 100644
--- a/vendor/github.com/google/go-containerregistry/cmd/crane/cmd/manifest.go
+++ b/vendor/github.com/google/go-containerregistry/cmd/crane/cmd/manifest.go
@@ -16,7 +16,6 @@ package cmd
 
 import (
 	"fmt"
-	"log"
 
 	"github.com/google/go-containerregistry/pkg/crane"
 	"github.com/spf13/cobra"
@@ -28,13 +27,14 @@ func NewCmdManifest(options *[]crane.Option) *cobra.Command {
 		Use:   "manifest IMAGE",
 		Short: "Get the manifest of an image",
 		Args:  cobra.ExactArgs(1),
-		Run: func(_ *cobra.Command, args []string) {
+		RunE: func(_ *cobra.Command, args []string) error {
 			src := args[0]
 			manifest, err := crane.Manifest(src, *options...)
 			if err != nil {
-				log.Fatalf("fetching manifest %s: %v", src, err)
+				return fmt.Errorf("fetching manifest %s: %v", src, err)
 			}
 			fmt.Print(string(manifest))
+			return nil
 		},
 	}
 }
diff --git a/vendor/github.com/google/go-containerregistry/cmd/crane/cmd/mutate.go b/vendor/github.com/google/go-containerregistry/cmd/crane/cmd/mutate.go
new file mode 100644
index 00000000..c444eaa4
--- /dev/null
+++ b/vendor/github.com/google/go-containerregistry/cmd/crane/cmd/mutate.go
@@ -0,0 +1,107 @@
+// Copyright 2021 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package cmd
+
+import (
+	"fmt"
+	"log"
+	"strings"
+
+	"github.com/google/go-containerregistry/pkg/crane"
+	"github.com/google/go-containerregistry/pkg/name"
+	"github.com/google/go-containerregistry/pkg/v1/mutate"
+	"github.com/spf13/cobra"
+)
+
+// NewCmdMutate creates a new cobra.Command for the mutate subcommand.
+func NewCmdMutate(options *[]crane.Option) *cobra.Command {
+	var lbls []string
+	var entrypoint string
+	var newRef string
+
+	mutateCmd := &cobra.Command{
+		Use:   "mutate",
+		Short: "Modify image labels and annotations",
+		Args:  cobra.ExactArgs(1),
+		Run: func(_ *cobra.Command, args []string) {
+			// Pull image and get config.
+			ref := args[0]
+			img, err := crane.Pull(ref, *options...)
+			if err != nil {
+				log.Fatalf("pulling %s: %v", ref, err)
+			}
+			cfg, err := img.ConfigFile()
+			if err != nil {
+				log.Fatalf("getting config: %v", err)
+			}
+			cfg = cfg.DeepCopy()
+
+			// Set labels.
+			if cfg.Config.Labels == nil {
+				cfg.Config.Labels = map[string]string{}
+			}
+			labels := map[string]string{}
+			for _, l := range lbls {
+				parts := strings.SplitN(l, "=", 2)
+				if len(parts) == 1 {
+					log.Fatalf("parsing label %q, not enough parts", l)
+				}
+				labels[parts[0]] = parts[1]
+			}
+			for k, v := range labels {
+				cfg.Config.Labels[k] = v
+			}
+
+			// Set entrypoint.
+			if entrypoint != "" {
+				// NB: This doesn't attempt to do anything smart about splitting the string into multiple entrypoint elements.
+				cfg.Config.Entrypoint = []string{entrypoint}
+			}
+
+			// Mutate and write image.
+			img, err = mutate.Config(img, cfg.Config)
+			if err != nil {
+				log.Fatalf("mutating config: %v", err)
+			}
+
+			// If the new ref isn't provided, write over the original image.
+			// If that ref was provided by digest (e.g., output from
+			// another crane command), then strip that and push the
+			// mutated image by digest instead.
+			if newRef == "" {
+				newRef = ref
+			}
+			digest, err := img.Digest()
+			if err != nil {
+				log.Fatalf("digesting new image: %v", err)
+			}
+			r, err := name.ParseReference(newRef)
+			if err != nil {
+				log.Fatalf("parsing %s: %v", newRef, err)
+			}
+			if _, ok := r.(name.Digest); ok {
+				newRef = r.Context().Digest(digest.String()).String()
+			}
+			if err := crane.Push(img, newRef, *options...); err != nil {
+				log.Fatalf("pushing %s: %v", newRef, err)
+			}
+			fmt.Println(r.Context().Digest(digest.String()))
+		},
+	}
+	mutateCmd.Flags().StringSliceVarP(&lbls, "label", "l", nil, "New labels to add")
+	mutateCmd.Flags().StringVar(&entrypoint, "entrypoint", "", "New entrypoing to set")
+	mutateCmd.Flags().StringVarP(&newRef, "tag", "t", "", "New tag to apply to mutated image. If not provided, push by digest to the original image repository.")
+	return mutateCmd
+}
diff --git a/vendor/github.com/google/go-containerregistry/cmd/crane/cmd/optimize.go b/vendor/github.com/google/go-containerregistry/cmd/crane/cmd/optimize.go
index 214a0b4e..89c47066 100644
--- a/vendor/github.com/google/go-containerregistry/cmd/crane/cmd/optimize.go
+++ b/vendor/github.com/google/go-containerregistry/cmd/crane/cmd/optimize.go
@@ -15,8 +15,6 @@
 package cmd
 
 import (
-	"log"
-
 	"github.com/google/go-containerregistry/pkg/crane"
 	"github.com/spf13/cobra"
 )
@@ -31,11 +29,9 @@ func NewCmdOptimize(options *[]crane.Option) *cobra.Command {
 		Aliases: []string{"opt"},
 		Short:   "Optimize a remote container image from src to dst",
 		Args:    cobra.ExactArgs(2),
-		Run: func(_ *cobra.Command, args []string) {
+		RunE: func(_ *cobra.Command, args []string) error {
 			src, dst := args[0], args[1]
-			if err := crane.Optimize(src, dst, files, *options...); err != nil {
-				log.Fatal(err)
-			}
+			return crane.Optimize(src, dst, files, *options...)
 		},
 	}
 
diff --git a/vendor/github.com/google/go-containerregistry/cmd/crane/cmd/pull.go b/vendor/github.com/google/go-containerregistry/cmd/crane/cmd/pull.go
index b5559926..08a1e938 100644
--- a/vendor/github.com/google/go-containerregistry/cmd/crane/cmd/pull.go
+++ b/vendor/github.com/google/go-containerregistry/cmd/crane/cmd/pull.go
@@ -16,9 +16,9 @@ package cmd
 
 import (
 	"fmt"
-	"log"
 
 	"github.com/google/go-containerregistry/pkg/crane"
+	v1 "github.com/google/go-containerregistry/pkg/v1"
 	"github.com/google/go-containerregistry/pkg/v1/cache"
 	"github.com/spf13/cobra"
 )
@@ -29,34 +29,40 @@ func NewCmdPull(options *[]crane.Option) *cobra.Command {
 
 	cmd := &cobra.Command{
 		Use:   "pull IMAGE TARBALL",
-		Short: "Pull a remote image by reference and store its contents in a tarball",
-		Args:  cobra.ExactArgs(2),
-		Run: func(_ *cobra.Command, args []string) {
-			src, path := args[0], args[1]
-			img, err := crane.Pull(src, *options...)
-			if err != nil {
-				log.Fatal(err)
-			}
-			if cachePath != "" {
-				img = cache.Image(img, cache.NewFilesystemCache(cachePath))
+		Short: "Pull remote images by reference and store their contents in a tarball",
+		Args:  cobra.MinimumNArgs(2),
+		RunE: func(_ *cobra.Command, args []string) error {
+			imageMap := map[string]v1.Image{}
+			srcList, path := args[:len(args)-1], args[len(args)-1]
+			for _, src := range srcList {
+				img, err := crane.Pull(src, *options...)
+				if err != nil {
+					return fmt.Errorf("pulling %s: %v", src, err)
+				}
+				if cachePath != "" {
+					img = cache.Image(img, cache.NewFilesystemCache(cachePath))
+				}
+
+				imageMap[src] = img
 			}
 
 			switch format {
 			case "tarball":
-				if err := crane.Save(img, src, path); err != nil {
-					log.Fatalf("saving tarball %s: %v", path, err)
+				if err := crane.MultiSave(imageMap, path); err != nil {
+					return fmt.Errorf("saving tarball %s: %v", path, err)
 				}
 			case "legacy":
-				if err := crane.SaveLegacy(img, src, path); err != nil {
-					log.Fatalf("saving legacy tarball %s: %v", path, err)
+				if err := crane.MultiSaveLegacy(imageMap, path); err != nil {
+					return fmt.Errorf("saving legacy tarball %s: %v", path, err)
 				}
 			case "oci":
-				if err := crane.SaveOCI(img, path); err != nil {
-					log.Fatalf("saving oci image layout %s: %v", path, err)
+				if err := crane.MultiSaveOCI(imageMap, path); err != nil {
+					return fmt.Errorf("saving oci image layout %s: %v", path, err)
 				}
 			default:
-				log.Fatalf("unexpected --format: %q (valid values are: tarball, legacy, and oci)", format)
+				return fmt.Errorf("unexpected --format: %q (valid values are: tarball, legacy, and oci)", format)
 			}
+			return nil
 		},
 	}
 	cmd.Flags().StringVarP(&cachePath, "cache_path", "c", "", "Path to cache image layers")
diff --git a/vendor/github.com/google/go-containerregistry/cmd/crane/cmd/push.go b/vendor/github.com/google/go-containerregistry/cmd/crane/cmd/push.go
index fbdc6146..5a4731cd 100644
--- a/vendor/github.com/google/go-containerregistry/cmd/crane/cmd/push.go
+++ b/vendor/github.com/google/go-containerregistry/cmd/crane/cmd/push.go
@@ -15,7 +15,7 @@
 package cmd
 
 import (
-	"log"
+	"fmt"
 
 	"github.com/google/go-containerregistry/pkg/crane"
 	"github.com/spf13/cobra"
@@ -27,16 +27,14 @@ func NewCmdPush(options *[]crane.Option) *cobra.Command {
 		Use:   "push TARBALL IMAGE",
 		Short: "Push image contents as a tarball to a remote registry",
 		Args:  cobra.ExactArgs(2),
-		Run: func(_ *cobra.Command, args []string) {
+		RunE: func(_ *cobra.Command, args []string) error {
 			path, tag := args[0], args[1]
 			img, err := crane.Load(path)
 			if err != nil {
-				log.Fatalf("loading %s as tarball: %v", path, err)
+				return fmt.Errorf("loading %s as tarball: %v", path, err)
 			}
 
-			if err := crane.Push(img, tag, *options...); err != nil {
-				log.Fatalf("pushing %s: %v", tag, err)
-			}
+			return crane.Push(img, tag, *options...)
 		},
 	}
 }
diff --git a/vendor/github.com/google/go-containerregistry/cmd/crane/cmd/rebase.go b/vendor/github.com/google/go-containerregistry/cmd/crane/cmd/rebase.go
index 4226223f..8fe916fe 100644
--- a/vendor/github.com/google/go-containerregistry/cmd/crane/cmd/rebase.go
+++ b/vendor/github.com/google/go-containerregistry/cmd/crane/cmd/rebase.go
@@ -16,7 +16,6 @@ package cmd
 
 import (
 	"fmt"
-	"log"
 
 	"github.com/google/go-containerregistry/pkg/crane"
 	"github.com/google/go-containerregistry/pkg/v1/mutate"
@@ -31,36 +30,37 @@ func NewCmdRebase(options *[]crane.Option) *cobra.Command {
 		Use:   "rebase",
 		Short: "Rebase an image onto a new base image",
 		Args:  cobra.NoArgs,
-		Run: func(*cobra.Command, []string) {
+		RunE: func(*cobra.Command, []string) error {
 			origImg, err := crane.Pull(orig, *options...)
 			if err != nil {
-				log.Fatalf("pulling %s: %v", orig, err)
+				return fmt.Errorf("pulling %s: %v", orig, err)
 			}
 
 			oldBaseImg, err := crane.Pull(oldBase, *options...)
 			if err != nil {
-				log.Fatalf("pulling %s: %v", oldBase, err)
+				return fmt.Errorf("pulling %s: %v", oldBase, err)
 			}
 
 			newBaseImg, err := crane.Pull(newBase, *options...)
 			if err != nil {
-				log.Fatalf("pulling %s: %v", newBase, err)
+				return fmt.Errorf("pulling %s: %v", newBase, err)
 			}
 
 			img, err := mutate.Rebase(origImg, oldBaseImg, newBaseImg)
 			if err != nil {
-				log.Fatalf("rebasing: %v", err)
+				return fmt.Errorf("rebasing: %v", err)
 			}
 
 			if err := crane.Push(img, rebased, *options...); err != nil {
-				log.Fatalf("pushing %s: %v", rebased, err)
+				return fmt.Errorf("pushing %s: %v", rebased, err)
 			}
 
 			digest, err := img.Digest()
 			if err != nil {
-				log.Fatalf("digesting rebased: %v", err)
+				return fmt.Errorf("digesting rebased: %v", err)
 			}
 			fmt.Println(digest.String())
+			return nil
 		},
 	}
 	rebaseCmd.Flags().StringVarP(&orig, "original", "", "", "Original image to rebase")
diff --git a/vendor/github.com/google/go-containerregistry/cmd/crane/cmd/root.go b/vendor/github.com/google/go-containerregistry/cmd/crane/cmd/root.go
index 75997281..f1123425 100644
--- a/vendor/github.com/google/go-containerregistry/cmd/crane/cmd/root.go
+++ b/vendor/github.com/google/go-containerregistry/cmd/crane/cmd/root.go
@@ -93,6 +93,7 @@ func New(use, short string, options []crane.Option) *cobra.Command {
 		NewCmdTag(&options),
 		NewCmdValidate(&options),
 		NewCmdVersion(),
+		NewCmdMutate(&options),
 	}
 
 	root.AddCommand(commands...)
diff --git a/vendor/github.com/google/go-containerregistry/cmd/crane/cmd/tag.go b/vendor/github.com/google/go-containerregistry/cmd/crane/cmd/tag.go
index 9e3338f7..9af803a6 100644
--- a/vendor/github.com/google/go-containerregistry/cmd/crane/cmd/tag.go
+++ b/vendor/github.com/google/go-containerregistry/cmd/crane/cmd/tag.go
@@ -15,8 +15,6 @@
 package cmd
 
 import (
-	"log"
-
 	"github.com/google/go-containerregistry/pkg/crane"
 	"github.com/spf13/cobra"
 )
@@ -38,11 +36,9 @@ crane tag registry.example.com/library/ubuntu:v0 v1
 		Example: `# Add a v1 tag to ubuntu
 crane tag ubuntu v1`,
 		Args: cobra.ExactArgs(2),
-		Run: func(_ *cobra.Command, args []string) {
+		RunE: func(_ *cobra.Command, args []string) error {
 			img, tag := args[0], args[1]
-			if err := crane.Tag(img, tag, *options...); err != nil {
-				log.Fatal(err)
-			}
+			return crane.Tag(img, tag, *options...)
 		},
 	}
 }
diff --git a/vendor/github.com/google/go-containerregistry/cmd/crane/cmd/validate.go b/vendor/github.com/google/go-containerregistry/cmd/crane/cmd/validate.go
index c3edc88f..4494515e 100644
--- a/vendor/github.com/google/go-containerregistry/cmd/crane/cmd/validate.go
+++ b/vendor/github.com/google/go-containerregistry/cmd/crane/cmd/validate.go
@@ -16,7 +16,6 @@ package cmd
 
 import (
 	"fmt"
-	"log"
 
 	"github.com/google/go-containerregistry/pkg/crane"
 	v1 "github.com/google/go-containerregistry/pkg/v1"
@@ -33,7 +32,7 @@ func NewCmdValidate(options *[]crane.Option) *cobra.Command {
 		Use:   "validate",
 		Short: "Validate that an image is well-formed",
 		Args:  cobra.ExactArgs(0),
-		Run: func(_ *cobra.Command, args []string) {
+		RunE: func(_ *cobra.Command, args []string) error {
 			for flag, maker := range map[string]func(string, ...crane.Option) (v1.Image, error){
 				tarballPath: makeTarball,
 				remoteRef:   crane.Pull,
@@ -43,7 +42,7 @@ func NewCmdValidate(options *[]crane.Option) *cobra.Command {
 				}
 				img, err := maker(flag, *options...)
 				if err != nil {
-					log.Fatalf("failed to read image %s: %v", flag, err)
+					return fmt.Errorf("failed to read image %s: %v", flag, err)
 				}
 
 				if err := validate.Image(img); err != nil {
@@ -52,6 +51,7 @@ func NewCmdValidate(options *[]crane.Option) *cobra.Command {
 					fmt.Printf("PASS: %s\n", flag)
 				}
 			}
+			return nil
 		},
 	}
 	validateCmd.Flags().StringVar(&tarballPath, "tarball", "", "Path to tarball to validate")
diff --git a/vendor/github.com/google/go-containerregistry/pkg/v1/internal/and/and_closer.go b/vendor/github.com/google/go-containerregistry/internal/and/and_closer.go
similarity index 100%
rename from vendor/github.com/google/go-containerregistry/pkg/v1/internal/and/and_closer.go
rename to vendor/github.com/google/go-containerregistry/internal/and/and_closer.go
diff --git a/vendor/github.com/google/go-containerregistry/pkg/v1/internal/estargz/estargz.go b/vendor/github.com/google/go-containerregistry/internal/estargz/estargz.go
similarity index 100%
rename from vendor/github.com/google/go-containerregistry/pkg/v1/internal/estargz/estargz.go
rename to vendor/github.com/google/go-containerregistry/internal/estargz/estargz.go
diff --git a/vendor/github.com/google/go-containerregistry/pkg/v1/internal/gzip/zip.go b/vendor/github.com/google/go-containerregistry/internal/gzip/zip.go
similarity index 98%
rename from vendor/github.com/google/go-containerregistry/pkg/v1/internal/gzip/zip.go
rename to vendor/github.com/google/go-containerregistry/internal/gzip/zip.go
index 6f7bfd93..bf1d66a5 100644
--- a/vendor/github.com/google/go-containerregistry/pkg/v1/internal/gzip/zip.go
+++ b/vendor/github.com/google/go-containerregistry/internal/gzip/zip.go
@@ -20,7 +20,7 @@ import (
 	"compress/gzip"
 	"io"
 
-	"github.com/google/go-containerregistry/pkg/v1/internal/and"
+	"github.com/google/go-containerregistry/internal/and"
 )
 
 var gzipMagicHeader = []byte{'\x1f', '\x8b'}
diff --git a/vendor/github.com/google/go-containerregistry/pkg/internal/httptest/httptest.go b/vendor/github.com/google/go-containerregistry/internal/httptest/httptest.go
similarity index 100%
rename from vendor/github.com/google/go-containerregistry/pkg/internal/httptest/httptest.go
rename to vendor/github.com/google/go-containerregistry/internal/httptest/httptest.go
diff --git a/vendor/github.com/google/go-containerregistry/internal/legacy/copy.go b/vendor/github.com/google/go-containerregistry/internal/legacy/copy.go
new file mode 100644
index 00000000..10467ba1
--- /dev/null
+++ b/vendor/github.com/google/go-containerregistry/internal/legacy/copy.go
@@ -0,0 +1,57 @@
+// Copyright 2019 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package legacy provides methods for interacting with legacy image formats.
+package legacy
+
+import (
+	"bytes"
+	"encoding/json"
+
+	"github.com/google/go-containerregistry/pkg/name"
+	"github.com/google/go-containerregistry/pkg/v1/remote"
+)
+
+// CopySchema1 allows `[g]crane cp` to work with old images without adding
+// full support for schema 1 images to this package.
+func CopySchema1(desc *remote.Descriptor, srcRef, dstRef name.Reference, opts ...remote.Option) error {
+	m := schema1{}
+	if err := json.NewDecoder(bytes.NewReader(desc.Manifest)).Decode(&m); err != nil {
+		return err
+	}
+
+	for _, layer := range m.FSLayers {
+		src := srcRef.Context().Digest(layer.BlobSum)
+		dst := dstRef.Context().Digest(layer.BlobSum)
+
+		blob, err := remote.Layer(src, opts...)
+		if err != nil {
+			return err
+		}
+
+		if err := remote.WriteLayer(dst.Context(), blob, opts...); err != nil {
+			return err
+		}
+	}
+
+	return remote.Put(dstRef, desc, opts...)
+}
+
+type fslayer struct {
+	BlobSum string `json:"blobSum"`
+}
+
+type schema1 struct {
+	FSLayers []fslayer `json:"fsLayers"`
+}
diff --git a/vendor/github.com/google/go-containerregistry/pkg/internal/redact/redact.go b/vendor/github.com/google/go-containerregistry/internal/redact/redact.go
similarity index 100%
rename from vendor/github.com/google/go-containerregistry/pkg/internal/redact/redact.go
rename to vendor/github.com/google/go-containerregistry/internal/redact/redact.go
diff --git a/vendor/github.com/google/go-containerregistry/pkg/internal/retry/retry.go b/vendor/github.com/google/go-containerregistry/internal/retry/retry.go
similarity index 97%
rename from vendor/github.com/google/go-containerregistry/pkg/internal/retry/retry.go
rename to vendor/github.com/google/go-containerregistry/internal/retry/retry.go
index 542ba49b..133cb1c1 100644
--- a/vendor/github.com/google/go-containerregistry/pkg/internal/retry/retry.go
+++ b/vendor/github.com/google/go-containerregistry/internal/retry/retry.go
@@ -20,7 +20,7 @@ import (
 	"context"
 	"fmt"
 
-	"github.com/google/go-containerregistry/pkg/internal/retry/wait"
+	"github.com/google/go-containerregistry/internal/retry/wait"
 )
 
 // Backoff is an alias of our own wait.Backoff to avoid name conflicts with
diff --git a/vendor/github.com/google/go-containerregistry/pkg/internal/retry/wait/kubernetes_apimachinery_wait.go b/vendor/github.com/google/go-containerregistry/internal/retry/wait/kubernetes_apimachinery_wait.go
similarity index 100%
rename from vendor/github.com/google/go-containerregistry/pkg/internal/retry/wait/kubernetes_apimachinery_wait.go
rename to vendor/github.com/google/go-containerregistry/internal/retry/wait/kubernetes_apimachinery_wait.go
diff --git a/vendor/github.com/google/go-containerregistry/pkg/v1/internal/verify/verify.go b/vendor/github.com/google/go-containerregistry/internal/verify/verify.go
similarity index 96%
rename from vendor/github.com/google/go-containerregistry/pkg/v1/internal/verify/verify.go
rename to vendor/github.com/google/go-containerregistry/internal/verify/verify.go
index 366b6eef..d0bc8ac2 100644
--- a/vendor/github.com/google/go-containerregistry/pkg/v1/internal/verify/verify.go
+++ b/vendor/github.com/google/go-containerregistry/internal/verify/verify.go
@@ -20,8 +20,8 @@ import (
 	"hash"
 	"io"
 
+	"github.com/google/go-containerregistry/internal/and"
 	v1 "github.com/google/go-containerregistry/pkg/v1"
-	"github.com/google/go-containerregistry/pkg/v1/internal/and"
 )
 
 type verifyReader struct {
diff --git a/vendor/github.com/google/go-containerregistry/pkg/crane/append.go b/vendor/github.com/google/go-containerregistry/pkg/crane/append.go
index 48f3bb6a..6d7253ab 100644
--- a/vendor/github.com/google/go-containerregistry/pkg/crane/append.go
+++ b/vendor/github.com/google/go-containerregistry/pkg/crane/append.go
@@ -40,20 +40,33 @@ func Append(base v1.Image, paths ...string) (v1.Image, error) {
 }
 
 func getLayer(path string) (v1.Layer, error) {
+	f, err := streamFile(path)
+	if err != nil {
+		return nil, err
+	}
+	if f != nil {
+		return stream.NewLayer(f), nil
+	}
+
+	return tarball.LayerFromFile(path)
+}
+
+// If we're dealing with a named pipe, trying to open it multiple times will
+// fail, so we need to do a streaming upload.
+//
+// returns nil, nil for non-streaming files
+func streamFile(path string) (*os.File, error) {
+	if path == "-" {
+		return os.Stdin, nil
+	}
 	fi, err := os.Stat(path)
 	if err != nil {
 		return nil, err
 	}
 
-	// If we're dealing with a named pipe, trying to open it multiple times will
-	// fail, so we need to do a streaming upload.
 	if !fi.Mode().IsRegular() {
-		rc, err := os.Open(path)
-		if err != nil {
-			return nil, err
-		}
-		return stream.NewLayer(rc), nil
+		return os.Open(path)
 	}
 
-	return tarball.LayerFromFile(path)
+	return nil, nil
 }
diff --git a/vendor/github.com/google/go-containerregistry/pkg/crane/copy.go b/vendor/github.com/google/go-containerregistry/pkg/crane/copy.go
index ab73cb36..b922b34a 100644
--- a/vendor/github.com/google/go-containerregistry/pkg/crane/copy.go
+++ b/vendor/github.com/google/go-containerregistry/pkg/crane/copy.go
@@ -17,8 +17,7 @@ package crane
 import (
 	"fmt"
 
-	"github.com/google/go-containerregistry/pkg/authn"
-	"github.com/google/go-containerregistry/pkg/internal/legacy"
+	"github.com/google/go-containerregistry/internal/legacy"
 	"github.com/google/go-containerregistry/pkg/logs"
 	"github.com/google/go-containerregistry/pkg/name"
 	"github.com/google/go-containerregistry/pkg/v1/remote"
@@ -59,7 +58,7 @@ func Copy(src, dst string, opt ...Option) error {
 		}
 	case types.DockerManifestSchema1, types.DockerManifestSchema1Signed:
 		// Handle schema 1 images separately.
-		if err := copySchema1(desc, srcRef, dstRef); err != nil {
+		if err := legacy.CopySchema1(desc, srcRef, dstRef, o.remote...); err != nil {
 			return fmt.Errorf("failed to copy schema 1 image: %v", err)
 		}
 	default:
@@ -87,16 +86,3 @@ func copyIndex(desc *remote.Descriptor, dstRef name.Reference, o options) error
 	}
 	return remote.WriteIndex(dstRef, idx, o.remote...)
 }
-
-func copySchema1(desc *remote.Descriptor, srcRef, dstRef name.Reference) error {
-	srcAuth, err := authn.DefaultKeychain.Resolve(srcRef.Context().Registry)
-	if err != nil {
-		return err
-	}
-	dstAuth, err := authn.DefaultKeychain.Resolve(dstRef.Context().Registry)
-	if err != nil {
-		return err
-	}
-
-	return legacy.CopySchema1(desc, srcRef, dstRef, srcAuth, dstAuth)
-}
diff --git a/vendor/github.com/google/go-containerregistry/pkg/crane/digest.go b/vendor/github.com/google/go-containerregistry/pkg/crane/digest.go
index 7fa774b6..697cc4af 100644
--- a/vendor/github.com/google/go-containerregistry/pkg/crane/digest.go
+++ b/vendor/github.com/google/go-containerregistry/pkg/crane/digest.go
@@ -14,6 +14,8 @@
 
 package crane
 
+import "github.com/google/go-containerregistry/pkg/logs"
+
 // Digest returns the sha256 hash of the remote image at ref.
 func Digest(ref string, opt ...Option) (string, error) {
 	o := makeOptions(opt...)
@@ -39,7 +41,12 @@ func Digest(ref string, opt ...Option) (string, error) {
 	}
 	desc, err := head(ref, opt...)
 	if err != nil {
-		return "", err
+		logs.Warn.Printf("HEAD request failed, falling back on GET: %v", err)
+		rdesc, err := getManifest(ref, opt...)
+		if err != nil {
+			return "", err
+		}
+		return rdesc.Digest.String(), nil
 	}
 	return desc.Digest.String(), nil
 }
diff --git a/vendor/github.com/google/go-containerregistry/pkg/crane/pull.go b/vendor/github.com/google/go-containerregistry/pkg/crane/pull.go
index 74beaa1a..124cfbdb 100644
--- a/vendor/github.com/google/go-containerregistry/pkg/crane/pull.go
+++ b/vendor/github.com/google/go-containerregistry/pkg/crane/pull.go
@@ -37,7 +37,7 @@ func Pull(src string, opt ...Option) (v1.Image, error) {
 	o := makeOptions(opt...)
 	ref, err := name.ParseReference(src, o.name...)
 	if err != nil {
-		return nil, fmt.Errorf("parsing tag %q: %v", src, err)
+		return nil, fmt.Errorf("parsing reference %q: %v", src, err)
 	}
 
 	return remote.Image(ref, o.remote...)
@@ -45,26 +45,36 @@ func Pull(src string, opt ...Option) (v1.Image, error) {
 
 // Save writes the v1.Image img as a tarball at path with tag src.
 func Save(img v1.Image, src, path string) error {
-	ref, err := name.ParseReference(src)
-	if err != nil {
-		return fmt.Errorf("parsing ref %q: %v", src, err)
-	}
+	imgMap := map[string]v1.Image{src: img}
+	return MultiSave(imgMap, path)
+}
+
+// MultiSave writes collection of v1.Image img with tag as a tarball.
+func MultiSave(imgMap map[string]v1.Image, path string) error {
+	tagToImage := map[name.Tag]v1.Image{}
+
+	for src, img := range imgMap {
+		ref, err := name.ParseReference(src)
+		if err != nil {
+			return fmt.Errorf("parsing ref %q: %v", src, err)
+		}
 
-	// WriteToFile wants a tag to write to the tarball, but we might have
-	// been given a digest.
-	// If the original ref was a tag, use that. Otherwise, if it was a
-	// digest, tag the image with :i-was-a-digest instead.
-	tag, ok := ref.(name.Tag)
-	if !ok {
-		d, ok := ref.(name.Digest)
+		// WriteToFile wants a tag to write to the tarball, but we might have
+		// been given a digest.
+		// If the original ref was a tag, use that. Otherwise, if it was a
+		// digest, tag the image with :i-was-a-digest instead.
+		tag, ok := ref.(name.Tag)
 		if !ok {
-			return fmt.Errorf("ref wasn't a tag or digest")
+			d, ok := ref.(name.Digest)
+			if !ok {
+				return fmt.Errorf("ref wasn't a tag or digest")
+			}
+			tag = d.Repository.Tag(iWasADigestTag)
 		}
-		tag = d.Repository.Tag(iWasADigestTag)
+		tagToImage[tag] = img
 	}
-
 	// no progress channel (for now)
-	return tarball.WriteToFile(path, tag, img)
+	return tarball.MultiWriteToFile(path, tagToImage)
 }
 
 // PullLayer returns the given layer from a registry.
@@ -80,9 +90,20 @@ func PullLayer(ref string, opt ...Option) (v1.Layer, error) {
 
 // SaveLegacy writes the v1.Image img as a legacy tarball at path with tag src.
 func SaveLegacy(img v1.Image, src, path string) error {
-	ref, err := name.ParseReference(src)
-	if err != nil {
-		return fmt.Errorf("parsing ref %q: %v", src, err)
+	imgMap := map[string]v1.Image{src: img}
+	return MultiSave(imgMap, path)
+}
+
+// MultiSaveLegacy writes collection of v1.Image img with tag as a legacy tarball.
+func MultiSaveLegacy(imgMap map[string]v1.Image, path string) error {
+	refToImage := map[name.Reference]v1.Image{}
+
+	for src, img := range imgMap {
+		ref, err := name.ParseReference(src)
+		if err != nil {
+			return fmt.Errorf("parsing ref %q: %v", src, err)
+		}
+		refToImage[ref] = img
 	}
 
 	w, err := os.Create(path)
@@ -91,12 +112,19 @@ func SaveLegacy(img v1.Image, src, path string) error {
 	}
 	defer w.Close()
 
-	return legacy.Write(ref, img, w)
+	return legacy.MultiWrite(refToImage, w)
 }
 
 // SaveOCI writes the v1.Image img as an OCI Image Layout at path. If a layout
 // already exists at that path, it will add the image to the index.
 func SaveOCI(img v1.Image, path string) error {
+	imgMap := map[string]v1.Image{"": img}
+	return MultiSaveOCI(imgMap, path)
+}
+
+// MultiSaveOCI writes collection of v1.Image img as an OCI Image Layout at path. If a layout
+// already exists at that path, it will add the image to the index.
+func MultiSaveOCI(imgMap map[string]v1.Image, path string) error {
 	p, err := layout.FromPath(path)
 	if err != nil {
 		p, err = layout.Write(path, empty.Index)
@@ -104,5 +132,10 @@ func SaveOCI(img v1.Image, path string) error {
 			return err
 		}
 	}
-	return p.AppendImage(img)
+	for _, img := range imgMap {
+		if err = p.AppendImage(img); err != nil {
+			return err
+		}
+	}
+	return nil
 }
diff --git a/vendor/github.com/google/go-containerregistry/pkg/crane/push.go b/vendor/github.com/google/go-containerregistry/pkg/crane/push.go
index 52f4fc48..65dee4f8 100644
--- a/vendor/github.com/google/go-containerregistry/pkg/crane/push.go
+++ b/vendor/github.com/google/go-containerregistry/pkg/crane/push.go
@@ -24,17 +24,31 @@ import (
 )
 
 // Load reads the tarball at path as a v1.Image.
-func Load(path string) (v1.Image, error) {
-	// TODO: Allow tag?
-	return tarball.ImageFromPath(path, nil)
+func Load(path string, opt ...Option) (v1.Image, error) {
+	return LoadTag(path, "")
+}
+
+// LoadTag reads a tag from the tarball at path as a v1.Image.
+// If tag is "", will attempt to read the tarball as a single image.
+func LoadTag(path, tag string, opt ...Option) (v1.Image, error) {
+	if tag == "" {
+		return tarball.ImageFromPath(path, nil)
+	}
+
+	o := makeOptions(opt...)
+	t, err := name.NewTag(tag, o.name...)
+	if err != nil {
+		return nil, fmt.Errorf("parsing tag %q: %v", tag, err)
+	}
+	return tarball.ImageFromPath(path, &t)
 }
 
 // Push pushes the v1.Image img to a registry as dst.
 func Push(img v1.Image, dst string, opt ...Option) error {
 	o := makeOptions(opt...)
-	tag, err := name.NewTag(dst, o.name...)
+	tag, err := name.ParseReference(dst, o.name...)
 	if err != nil {
-		return fmt.Errorf("parsing tag %q: %v", dst, err)
+		return fmt.Errorf("parsing reference %q: %v", dst, err)
 	}
 	return remote.Write(tag, img, o.remote...)
 }
diff --git a/vendor/github.com/google/go-containerregistry/pkg/internal/legacy/copy.go b/vendor/github.com/google/go-containerregistry/pkg/internal/legacy/copy.go
deleted file mode 100644
index 557c1d1f..00000000
--- a/vendor/github.com/google/go-containerregistry/pkg/internal/legacy/copy.go
+++ /dev/null
@@ -1,102 +0,0 @@
-// Copyright 2019 Google LLC All Rights Reserved.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//    http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-// Package legacy provides methods for interacting with legacy image formats.
-package legacy
-
-import (
-	"bytes"
-	"encoding/json"
-	"fmt"
-	"net/http"
-	"net/url"
-
-	"github.com/google/go-containerregistry/pkg/authn"
-	"github.com/google/go-containerregistry/pkg/logs"
-	"github.com/google/go-containerregistry/pkg/name"
-	"github.com/google/go-containerregistry/pkg/v1/remote"
-	"github.com/google/go-containerregistry/pkg/v1/remote/transport"
-)
-
-// CopySchema1 allows `[g]crane cp` to work with old images without adding
-// full support for schema 1 images to this package.
-func CopySchema1(desc *remote.Descriptor, srcRef, dstRef name.Reference, srcAuth, dstAuth authn.Authenticator) error {
-	m := schema1{}
-	if err := json.NewDecoder(bytes.NewReader(desc.Manifest)).Decode(&m); err != nil {
-		return err
-	}
-
-	for _, layer := range m.FSLayers {
-		src := srcRef.Context().Digest(layer.BlobSum)
-		dst := dstRef.Context().Digest(layer.BlobSum)
-
-		blob, err := remote.Layer(src, remote.WithAuth(srcAuth))
-		if err != nil {
-			return err
-		}
-
-		if err := remote.WriteLayer(dst.Context(), blob, remote.WithAuth(dstAuth)); err != nil {
-			return err
-		}
-	}
-
-	return putManifest(desc, dstRef, dstAuth)
-}
-
-// TODO: perhaps expose this in remote?
-func putManifest(desc *remote.Descriptor, dstRef name.Reference, dstAuth authn.Authenticator) error {
-	reg := dstRef.Context().Registry
-	scopes := []string{dstRef.Scope(transport.PushScope)}
-
-	// TODO(jonjohnsonjr): Use NewWithContext.
-	tr, err := transport.New(reg, dstAuth, http.DefaultTransport, scopes)
-	if err != nil {
-		return err
-	}
-	client := &http.Client{Transport: tr}
-
-	u := url.URL{
-		Scheme: dstRef.Context().Registry.Scheme(),
-		Host:   dstRef.Context().RegistryStr(),
-		Path:   fmt.Sprintf("/v2/%s/manifests/%s", dstRef.Context().RepositoryStr(), dstRef.Identifier()),
-	}
-
-	req, err := http.NewRequest(http.MethodPut, u.String(), bytes.NewBuffer(desc.Manifest))
-	if err != nil {
-		return err
-	}
-	req.Header.Set("Content-Type", string(desc.MediaType))
-
-	resp, err := client.Do(req)
-	if err != nil {
-		return err
-	}
-	defer resp.Body.Close()
-
-	if err := transport.CheckError(resp, http.StatusOK, http.StatusCreated, http.StatusAccepted); err != nil {
-		return err
-	}
-
-	// The image was successfully pushed!
-	logs.Progress.Printf("%v: digest: %v size: %d", dstRef, desc.Digest, len(desc.Manifest))
-	return nil
-}
-
-type fslayer struct {
-	BlobSum string `json:"blobSum"`
-}
-
-type schema1 struct {
-	FSLayers []fslayer `json:"fsLayers"`
-}
diff --git a/vendor/github.com/google/go-containerregistry/pkg/name/ref.go b/vendor/github.com/google/go-containerregistry/pkg/name/ref.go
index f9388253..e5180b3d 100644
--- a/vendor/github.com/google/go-containerregistry/pkg/name/ref.go
+++ b/vendor/github.com/google/go-containerregistry/pkg/name/ref.go
@@ -47,3 +47,30 @@ func ParseReference(s string, opts ...Option) (Reference, error) {
 	return nil, NewErrBadName("could not parse reference: " + s)
 
 }
+
+type stringConst string
+
+// MustParseReference behaves like ParseReference, but panics instead of
+// returning an error. It's intended for use in tests, or when a value is
+// expected to be valid at code authoring time.
+//
+// To discourage its use in scenarios where the value is not known at code
+// authoring time, it must be passed a string constant:
+//
+//    const str = "valid/string"
+//    MustParseReference(str)
+//    MustParseReference("another/valid/string")
+//    MustParseReference(str + "/and/more")
+//
+// These will not compile:
+//
+//    var str = "valid/string"
+//    MustParseReference(str)
+//    MustParseReference(strings.Join([]string{"valid", "string"}, "/"))
+func MustParseReference(s stringConst, opts ...Option) Reference {
+	ref, err := ParseReference(string(s), opts...)
+	if err != nil {
+		panic(err)
+	}
+	return ref
+}
diff --git a/vendor/github.com/google/go-containerregistry/pkg/registry/manifest.go b/vendor/github.com/google/go-containerregistry/pkg/registry/manifest.go
index 5dc920ab..5bb4fe89 100644
--- a/vendor/github.com/google/go-containerregistry/pkg/registry/manifest.go
+++ b/vendor/github.com/google/go-containerregistry/pkg/registry/manifest.go
@@ -21,6 +21,7 @@ import (
 	"encoding/json"
 	"fmt"
 	"io"
+	"log"
 	"net/http"
 	"sort"
 	"strconv"
@@ -31,6 +32,10 @@ import (
 	"github.com/google/go-containerregistry/pkg/v1/types"
 )
 
+type catalog struct {
+	Repos []string `json:"repositories"`
+}
+
 type listTags struct {
 	Name string   `json:"name"`
 	Tags []string `json:"tags"`
@@ -45,6 +50,7 @@ type manifests struct {
 	// maps repo -> manifest tag/digest -> manifest
 	manifests map[string]map[string]manifest
 	lock      sync.Mutex
+	log       *log.Logger
 }
 
 func isManifest(req *http.Request) bool {
@@ -65,6 +71,16 @@ func isTags(req *http.Request) bool {
 	return elems[len(elems)-2] == "tags"
 }
 
+func isCatalog(req *http.Request) bool {
+	elems := strings.Split(req.URL.Path, "/")
+	elems = elems[1:]
+	if len(elems) < 2 {
+		return false
+	}
+
+	return elems[len(elems)-1] == "_catalog"
+}
+
 // https://github.com/opencontainers/distribution-spec/blob/master/spec.md#pulling-an-image-manifest
 // https://github.com/opencontainers/distribution-spec/blob/master/spec.md#pushing-an-image
 func (m *manifests) handle(resp http.ResponseWriter, req *http.Request) *regError {
@@ -172,6 +188,7 @@ func (m *manifests) handle(resp http.ResponseWriter, req *http.Request) *regErro
 					}
 				} else {
 					// TODO: Probably want to do an existence check for blobs.
+					m.log.Printf("TODO: Check blobs for %q", desc.Digest)
 				}
 			}
 		}
@@ -273,3 +290,45 @@ func (m *manifests) handleTags(resp http.ResponseWriter, req *http.Request) *reg
 		Message: "We don't understand your method + url",
 	}
 }
+
+func (m *manifests) handleCatalog(resp http.ResponseWriter, req *http.Request) *regError {
+	query := req.URL.Query()
+	nStr := query.Get("n")
+	n := 10000
+	if nStr != "" {
+		n, _ = strconv.Atoi(nStr)
+	}
+
+	if req.Method == "GET" {
+		m.lock.Lock()
+		defer m.lock.Unlock()
+
+		var repos []string
+		countRepos := 0
+		// TODO: implement pagination
+		for key := range m.manifests {
+			if countRepos >= n {
+				break
+			}
+			countRepos++
+
+			repos = append(repos, key)
+		}
+
+		repositoriesToList := catalog{
+			Repos: repos,
+		}
+
+		msg, _ := json.Marshal(repositoriesToList)
+		resp.Header().Set("Content-Length", fmt.Sprint(len(msg)))
+		resp.WriteHeader(http.StatusOK)
+		io.Copy(resp, bytes.NewReader([]byte(msg)))
+		return nil
+	}
+
+	return &regError{
+		Status:  http.StatusBadRequest,
+		Code:    "METHOD_UNKNOWN",
+		Message: "We don't understand your method + url",
+	}
+}
diff --git a/vendor/github.com/google/go-containerregistry/pkg/registry/registry.go b/vendor/github.com/google/go-containerregistry/pkg/registry/registry.go
index 8e92bfb2..c56dae26 100644
--- a/vendor/github.com/google/go-containerregistry/pkg/registry/registry.go
+++ b/vendor/github.com/google/go-containerregistry/pkg/registry/registry.go
@@ -47,6 +47,9 @@ func (r *registry) v2(resp http.ResponseWriter, req *http.Request) *regError {
 	if isTags(req) {
 		return r.manifests.handleTags(resp, req)
 	}
+	if isCatalog(req) {
+		return r.manifests.handleCatalog(resp, req)
+	}
 	resp.Header().Set("Docker-Distribution-API-Version", "registry/2.0")
 	if req.URL.Path != "/v2/" && req.URL.Path != "/v2" {
 		return &regError{
@@ -79,6 +82,7 @@ func New(opts ...Option) http.Handler {
 		},
 		manifests: manifests{
 			manifests: map[string]map[string]manifest{},
+			log:       log.New(os.Stderr, "", log.LstdFlags),
 		},
 	}
 	for _, o := range opts {
@@ -95,5 +99,6 @@ type Option func(r *registry)
 func Logger(l *log.Logger) Option {
 	return func(r *registry) {
 		r.log = l
+		r.manifests.log = l
 	}
 }
diff --git a/vendor/github.com/google/go-containerregistry/pkg/registry/tls.go b/vendor/github.com/google/go-containerregistry/pkg/registry/tls.go
index d5e86192..cb2644e6 100644
--- a/vendor/github.com/google/go-containerregistry/pkg/registry/tls.go
+++ b/vendor/github.com/google/go-containerregistry/pkg/registry/tls.go
@@ -17,7 +17,7 @@ package registry
 import (
 	"net/http/httptest"
 
-	ggcrtest "github.com/google/go-containerregistry/pkg/internal/httptest"
+	ggcrtest "github.com/google/go-containerregistry/internal/httptest"
 )
 
 // TLS returns an httptest server, with an http client that has been configured to
diff --git a/vendor/github.com/google/go-containerregistry/pkg/v1/cache/cache.go b/vendor/github.com/google/go-containerregistry/pkg/v1/cache/cache.go
index dd0cdebd..54a0dc43 100644
--- a/vendor/github.com/google/go-containerregistry/pkg/v1/cache/cache.go
+++ b/vendor/github.com/google/go-containerregistry/pkg/v1/cache/cache.go
@@ -3,9 +3,11 @@ package cache
 
 import (
 	"errors"
+	"io"
 
 	"github.com/google/go-containerregistry/pkg/logs"
 	v1 "github.com/google/go-containerregistry/pkg/v1"
+	"github.com/google/go-containerregistry/pkg/v1/types"
 )
 
 // Cache encapsulates methods to interact with cached layers.
@@ -55,46 +57,66 @@ func (i *image) Layers() ([]v1.Layer, error) {
 
 	var out []v1.Layer
 	for _, l := range ls {
-		// Check if this layer is present in the cache in compressed
-		// form.
-		digest, err := l.Digest()
-		if err != nil {
-			return nil, err
-		}
-		if cl, err := i.c.Get(digest); err == nil {
-			// Layer found in the cache.
-			logs.Progress.Printf("Layer %s found (compressed) in cache", digest)
-			out = append(out, cl)
-			continue
-		} else if err != nil && err != ErrNotFound {
-			return nil, err
-		}
+		out = append(out, &lazyLayer{inner: l, c: i.c})
+	}
+	return out, nil
+}
 
-		// Check if this layer is present in the cache in
-		// uncompressed form.
-		diffID, err := l.DiffID()
-		if err != nil {
-			return nil, err
-		}
-		if cl, err := i.c.Get(diffID); err == nil {
-			// Layer found in the cache.
-			logs.Progress.Printf("Layer %s found (uncompressed) in cache", diffID)
-			out = append(out, cl)
-		} else if err != nil && err != ErrNotFound {
-			return nil, err
-		}
+type lazyLayer struct {
+	inner v1.Layer
+	c     Cache
+}
 
-		// Not cached, fall through to real layer.
-		l, err = i.c.Put(l)
-		if err != nil {
-			return nil, err
-		}
-		out = append(out, l)
+func (l *lazyLayer) Compressed() (io.ReadCloser, error) {
+	digest, err := l.inner.Digest()
+	if err != nil {
+		return nil, err
+	}
 
+	if cl, err := l.c.Get(digest); err == nil {
+		// Layer found in the cache.
+		logs.Progress.Printf("Layer %s found (compressed) in cache", digest)
+		return cl.Compressed()
+	} else if err != nil && err != ErrNotFound {
+		return nil, err
 	}
-	return out, nil
+
+	// Not cached, pull and return the real layer.
+	logs.Progress.Printf("Layer %s not found (compressed) in cache, getting", digest)
+	rl, err := l.c.Put(l.inner)
+	if err != nil {
+		return nil, err
+	}
+	return rl.Compressed()
 }
 
+func (l *lazyLayer) Uncompressed() (io.ReadCloser, error) {
+	diffID, err := l.inner.DiffID()
+	if err != nil {
+		return nil, err
+	}
+	if cl, err := l.c.Get(diffID); err == nil {
+		// Layer found in the cache.
+		logs.Progress.Printf("Layer %s found (uncompressed) in cache", diffID)
+		return cl.Uncompressed()
+	} else if err != nil && err != ErrNotFound {
+		return nil, err
+	}
+
+	// Not cached, pull and return the real layer.
+	logs.Progress.Printf("Layer %s not found (uncompressed) in cache, getting", diffID)
+	rl, err := l.c.Put(l.inner)
+	if err != nil {
+		return nil, err
+	}
+	return rl.Uncompressed()
+}
+
+func (l *lazyLayer) Size() (int64, error)                { return l.inner.Size() }
+func (l *lazyLayer) DiffID() (v1.Hash, error)            { return l.inner.DiffID() }
+func (l *lazyLayer) Digest() (v1.Hash, error)            { return l.inner.Digest() }
+func (l *lazyLayer) MediaType() (types.MediaType, error) { return l.inner.MediaType() }
+
 func (i *image) LayerByDigest(h v1.Hash) (v1.Layer, error) {
 	l, err := i.c.Get(h)
 	if err == ErrNotFound {
diff --git a/vendor/github.com/google/go-containerregistry/pkg/v1/daemon/image.go b/vendor/github.com/google/go-containerregistry/pkg/v1/daemon/image.go
index eb7a9d7f..5013c3d3 100644
--- a/vendor/github.com/google/go-containerregistry/pkg/v1/daemon/image.go
+++ b/vendor/github.com/google/go-containerregistry/pkg/v1/daemon/image.go
@@ -19,103 +19,71 @@ import (
 	"context"
 	"io"
 	"io/ioutil"
+	"sync"
 
-	"github.com/docker/docker/client"
 	"github.com/google/go-containerregistry/pkg/name"
 	v1 "github.com/google/go-containerregistry/pkg/v1"
 	"github.com/google/go-containerregistry/pkg/v1/tarball"
 )
 
-// image accesses an image from a docker daemon
-type image struct {
-	v1.Image
-}
-
-var _ v1.Image = (*image)(nil)
-
 type imageOpener struct {
-	ref      name.Reference
+	ref name.Reference
+	ctx context.Context
+
 	buffered bool
 	client   Client
-}
-
-// ImageOption is a functional option for Image.
-type ImageOption func(*imageOpener) error
-
-func (i *imageOpener) Open() (v1.Image, error) {
-	var opener tarball.Opener
-	var err error
-	if i.buffered {
-		opener, err = i.bufferedOpener(i.ref)
-	} else {
-		opener, err = i.unbufferedOpener(i.ref)
-	}
-	if err != nil {
-		return nil, err
-	}
 
-	tb, err := tarball.Image(opener, nil)
-	if err != nil {
-		return nil, err
-	}
-	img := &image{
-		Image: tb,
-	}
-	return img, nil
+	once  sync.Once
+	bytes []byte
+	err   error
 }
 
-func (i *imageOpener) saveImage(ref name.Reference) (io.ReadCloser, error) {
-	return i.client.ImageSave(context.Background(), []string{ref.Name()})
+func (i *imageOpener) saveImage() (io.ReadCloser, error) {
+	return i.client.ImageSave(i.ctx, []string{i.ref.Name()})
 }
 
-func (i *imageOpener) bufferedOpener(ref name.Reference) (tarball.Opener, error) {
+func (i *imageOpener) bufferedOpener() (io.ReadCloser, error) {
 	// Store the tarball in memory and return a new reader into the bytes each time we need to access something.
-	rc, err := i.saveImage(ref)
-	if err != nil {
-		return nil, err
-	}
-	defer rc.Close()
+	i.once.Do(func() {
+		i.bytes, i.err = func() ([]byte, error) {
+			rc, err := i.saveImage()
+			if err != nil {
+				return nil, err
+			}
+			defer rc.Close()
+
+			return ioutil.ReadAll(rc)
+		}()
+	})
+
+	// Wrap the bytes in a ReadCloser so it looks like an opened file.
+	return ioutil.NopCloser(bytes.NewReader(i.bytes)), i.err
+}
 
-	imageBytes, err := ioutil.ReadAll(rc)
-	if err != nil {
-		return nil, err
+func (i *imageOpener) opener() tarball.Opener {
+	if i.buffered {
+		return i.bufferedOpener
 	}
-	// The tarball interface takes a function that it can call to return an opened reader-like object.
-	// Daemon comes from a set of bytes, so wrap them in a ReadCloser so it looks like an opened file.
-	return func() (io.ReadCloser, error) {
-		return ioutil.NopCloser(bytes.NewReader(imageBytes)), nil
-	}, nil
-}
 
-func (i *imageOpener) unbufferedOpener(ref name.Reference) (tarball.Opener, error) {
 	// To avoid storing the tarball in memory, do a save every time we need to access something.
-	return func() (io.ReadCloser, error) {
-		return i.saveImage(ref)
-	}, nil
+	return i.saveImage
 }
 
 // Image provides access to an image reference from the Docker daemon,
 // applying functional options to the underlying imageOpener before
 // resolving the reference into a v1.Image.
-func Image(ref name.Reference, options ...ImageOption) (v1.Image, error) {
-	i := &imageOpener{
-		ref:      ref,
-		buffered: true, // buffer by default
-	}
-	for _, option := range options {
-		if err := option(i); err != nil {
-			return nil, err
-		}
+func Image(ref name.Reference, options ...Option) (v1.Image, error) {
+	o, err := makeOptions(options...)
+	if err != nil {
+		return nil, err
 	}
 
-	if i.client == nil {
-		var err error
-		i.client, err = client.NewClientWithOpts(client.FromEnv)
-		if err != nil {
-			return nil, err
-		}
+	i := &imageOpener{
+		ref:      ref,
+		buffered: o.buffered,
+		client:   o.client,
+		ctx:      o.ctx,
 	}
-	i.client.NegotiateAPIVersion(context.Background())
 
-	return i.Open()
+	return tarball.Image(i.opener(), nil)
 }
diff --git a/vendor/github.com/google/go-containerregistry/pkg/v1/daemon/options.go b/vendor/github.com/google/go-containerregistry/pkg/v1/daemon/options.go
index bb6726bc..c3a0ac66 100644
--- a/vendor/github.com/google/go-containerregistry/pkg/v1/daemon/options.go
+++ b/vendor/github.com/google/go-containerregistry/pkg/v1/daemon/options.go
@@ -19,34 +19,76 @@ import (
 	"io"
 
 	"github.com/docker/docker/api/types"
+	"github.com/docker/docker/client"
 )
 
-// WithBufferedOpener buffers the image.
-func WithBufferedOpener() ImageOption {
-	return func(i *imageOpener) error {
-		return i.setBuffered(true)
+// ImageOption is an alias for Option.
+// Deprecated: Use Option instead.
+type ImageOption Option
+
+// Option is a functional option for daemon operations.
+type Option func(*options)
+
+type options struct {
+	ctx      context.Context
+	client   Client
+	buffered bool
+}
+
+var defaultClient = func() (Client, error) {
+	return client.NewClientWithOpts(client.FromEnv)
+}
+
+func makeOptions(opts ...Option) (*options, error) {
+	o := &options{
+		buffered: true,
+		ctx:      context.Background(),
+	}
+	for _, opt := range opts {
+		opt(o)
+	}
+
+	if o.client == nil {
+		client, err := defaultClient()
+		if err != nil {
+			return nil, err
+		}
+		o.client = client
 	}
+	o.client.NegotiateAPIVersion(o.ctx)
+
+	return o, nil
 }
 
-// WithUnbufferedOpener streams the image to avoid buffering.
-func WithUnbufferedOpener() ImageOption {
-	return func(i *imageOpener) error {
-		return i.setBuffered(false)
+// WithBufferedOpener buffers the image.
+func WithBufferedOpener() Option {
+	return func(o *options) {
+		o.buffered = true
 	}
 }
 
-func (i *imageOpener) setBuffered(buffer bool) error {
-	i.buffered = buffer
-	return nil
+// WithUnbufferedOpener streams the image to avoid buffering.
+func WithUnbufferedOpener() Option {
+	return func(o *options) {
+		o.buffered = false
+	}
 }
 
 // WithClient is a functional option to allow injecting a docker client.
 //
 // By default, github.com/docker/docker/client.FromEnv is used.
-func WithClient(client Client) ImageOption {
-	return func(i *imageOpener) error {
-		i.client = client
-		return nil
+func WithClient(client Client) Option {
+	return func(o *options) {
+		o.client = client
+	}
+}
+
+// WithContext is a functional option to pass through a context.Context.
+//
+// By default, context.Background() is used.
+func WithContext(ctx context.Context) Option {
+	return func(o *options) {
+		o.ctx = ctx
 	}
 }
 
diff --git a/vendor/github.com/google/go-containerregistry/pkg/v1/daemon/write.go b/vendor/github.com/google/go-containerregistry/pkg/v1/daemon/write.go
index f29dd73f..5d400156 100644
--- a/vendor/github.com/google/go-containerregistry/pkg/v1/daemon/write.go
+++ b/vendor/github.com/google/go-containerregistry/pkg/v1/daemon/write.go
@@ -15,47 +15,28 @@
 package daemon
 
 import (
-	"context"
 	"fmt"
 	"io"
 	"io/ioutil"
 
-	"github.com/docker/docker/api/types"
-	"github.com/docker/docker/client"
 	"github.com/google/go-containerregistry/pkg/name"
 	v1 "github.com/google/go-containerregistry/pkg/v1"
 	"github.com/google/go-containerregistry/pkg/v1/tarball"
 )
 
-// ImageLoader is an interface for testing.
-type ImageLoader interface {
-	ImageLoad(context.Context, io.Reader, bool) (types.ImageLoadResponse, error)
-	ImageTag(context.Context, string, string) error
-}
-
-// GetImageLoader is a variable so we can override in tests.
-var GetImageLoader = func() (ImageLoader, error) {
-	cli, err := client.NewClientWithOpts(client.FromEnv)
-	if err != nil {
-		return nil, err
-	}
-	cli.NegotiateAPIVersion(context.Background())
-	return cli, nil
-}
-
 // Tag adds a tag to an already existent image.
-func Tag(src, dest name.Tag) error {
-	cli, err := GetImageLoader()
+func Tag(src, dest name.Tag, options ...Option) error {
+	o, err := makeOptions(options...)
 	if err != nil {
 		return err
 	}
 
-	return cli.ImageTag(context.Background(), src.String(), dest.String())
+	return o.client.ImageTag(o.ctx, src.String(), dest.String())
 }
 
 // Write saves the image into the daemon as the given tag.
-func Write(tag name.Tag, img v1.Image) (string, error) {
-	cli, err := GetImageLoader()
+func Write(tag name.Tag, img v1.Image, options ...Option) (string, error) {
+	o, err := makeOptions(options...)
 	if err != nil {
 		return "", err
 	}
@@ -66,14 +47,14 @@ func Write(tag name.Tag, img v1.Image) (string, error) {
 	}()
 
 	// write the image in docker save format first, then load it
-	resp, err := cli.ImageLoad(context.Background(), pr, false)
+	resp, err := o.client.ImageLoad(o.ctx, pr, false)
 	if err != nil {
 		return "", fmt.Errorf("error loading image: %v", err)
 	}
 	defer resp.Body.Close()
-	b, readErr := ioutil.ReadAll(resp.Body)
+	b, err := ioutil.ReadAll(resp.Body)
 	response := string(b)
-	if readErr != nil {
+	if err != nil {
 		return response, fmt.Errorf("error reading load response body: %v", err)
 	}
 	return response, nil
diff --git a/vendor/github.com/google/go-containerregistry/pkg/v1/hash.go b/vendor/github.com/google/go-containerregistry/pkg/v1/hash.go
index 7fdf22e9..e9630087 100644
--- a/vendor/github.com/google/go-containerregistry/pkg/v1/hash.go
+++ b/vendor/github.com/google/go-containerregistry/pkg/v1/hash.go
@@ -87,7 +87,7 @@ func Hasher(name string) (hash.Hash, error) {
 func (h *Hash) parse(unquoted string) error {
 	parts := strings.Split(unquoted, ":")
 	if len(parts) != 2 {
-		return fmt.Errorf("too many parts in hash: %s", unquoted)
+		return fmt.Errorf("cannot parse hash: %q", unquoted)
 	}
 
 	rest := strings.TrimLeft(parts[1], "0123456789abcdef")
diff --git a/vendor/github.com/google/go-containerregistry/pkg/v1/layout/options.go b/vendor/github.com/google/go-containerregistry/pkg/v1/layout/options.go
index 5569e51d..c322d775 100644
--- a/vendor/github.com/google/go-containerregistry/pkg/v1/layout/options.go
+++ b/vendor/github.com/google/go-containerregistry/pkg/v1/layout/options.go
@@ -3,40 +3,55 @@ package layout
 import v1 "github.com/google/go-containerregistry/pkg/v1"
 
 // Option is a functional option for Layout.
-//
-// TODO: We'll need to change this signature to support Sparse/Thin images.
-// Or, alternatively, wrap it in a sparse.Image that returns an empty list for layers?
-type Option func(*v1.Descriptor) error
+type Option func(*options)
+
+type options struct {
+	descOpts []descriptorOption
+}
+
+func makeOptions(opts ...Option) *options {
+	o := &options{
+		descOpts: []descriptorOption{},
+	}
+	for _, apply := range opts {
+		apply(o)
+	}
+	return o
+}
+
+type descriptorOption func(*v1.Descriptor)
 
 // WithAnnotations adds annotations to the artifact descriptor.
 func WithAnnotations(annotations map[string]string) Option {
-	return func(desc *v1.Descriptor) error {
-		if desc.Annotations == nil {
-			desc.Annotations = make(map[string]string)
-		}
-		for k, v := range annotations {
-			desc.Annotations[k] = v
-		}
-
-		return nil
+	return func(o *options) {
+		o.descOpts = append(o.descOpts, func(desc *v1.Descriptor) {
+			if desc.Annotations == nil {
+				desc.Annotations = make(map[string]string)
+			}
+			for k, v := range annotations {
+				desc.Annotations[k] = v
+			}
+		})
 	}
 }
 
 // WithURLs adds urls to the artifact descriptor.
 func WithURLs(urls []string) Option {
-	return func(desc *v1.Descriptor) error {
-		if desc.URLs == nil {
-			desc.URLs = []string{}
-		}
-		desc.URLs = append(desc.URLs, urls...)
-		return nil
+	return func(o *options) {
+		o.descOpts = append(o.descOpts, func(desc *v1.Descriptor) {
+			if desc.URLs == nil {
+				desc.URLs = []string{}
+			}
+			desc.URLs = append(desc.URLs, urls...)
+		})
 	}
 }
 
 // WithPlatform sets the platform of the artifact descriptor.
 func WithPlatform(platform v1.Platform) Option {
-	return func(desc *v1.Descriptor) error {
-		desc.Platform = &platform
-		return nil
+	return func(o *options) {
+		o.descOpts = append(o.descOpts, func(desc *v1.Descriptor) {
+			desc.Platform = &platform
+		})
 	}
 }
diff --git a/vendor/github.com/google/go-containerregistry/pkg/v1/layout/write.go b/vendor/github.com/google/go-containerregistry/pkg/v1/layout/write.go
index 9de456dd..a9ff91f7 100644
--- a/vendor/github.com/google/go-containerregistry/pkg/v1/layout/write.go
+++ b/vendor/github.com/google/go-containerregistry/pkg/v1/layout/write.go
@@ -62,10 +62,9 @@ func (l Path) AppendImage(img v1.Image, options ...Option) error {
 		Digest:    d,
 	}
 
-	for _, opt := range options {
-		if err := opt(&desc); err != nil {
-			return err
-		}
+	o := makeOptions(options...)
+	for _, opt := range o.descOpts {
+		opt(&desc)
 	}
 
 	return l.AppendDescriptor(desc)
@@ -99,10 +98,9 @@ func (l Path) AppendIndex(ii v1.ImageIndex, options ...Option) error {
 		Digest:    d,
 	}
 
-	for _, opt := range options {
-		if err := opt(&desc); err != nil {
-			return err
-		}
+	o := makeOptions(options...)
+	for _, opt := range o.descOpts {
+		opt(&desc)
 	}
 
 	return l.AppendDescriptor(desc)
@@ -163,10 +161,9 @@ func (l Path) replaceDescriptor(append mutate.Appendable, matcher match.Matcher,
 		return err
 	}
 
-	for _, opt := range options {
-		if err := opt(desc); err != nil {
-			return err
-		}
+	o := makeOptions(options...)
+	for _, opt := range o.descOpts {
+		opt(desc)
 	}
 
 	add := mutate.IndexAddendum{
@@ -368,9 +365,9 @@ func (l Path) writeIndexToFile(indexFile string, ii v1.ImageIndex) error {
 			var blob io.ReadCloser
 			// Workaround for #819.
 			if wl, ok := ii.(withLayer); ok {
-				layer, err := wl.Layer(desc.Digest)
-				if err != nil {
-					return err
+				layer, lerr := wl.Layer(desc.Digest)
+				if lerr != nil {
+					return lerr
 				}
 				blob, err = layer.Compressed()
 			} else if wb, ok := ii.(withBlob); ok {
diff --git a/vendor/github.com/google/go-containerregistry/pkg/v1/manifest.go b/vendor/github.com/google/go-containerregistry/pkg/v1/manifest.go
index 36c341df..51a46704 100644
--- a/vendor/github.com/google/go-containerregistry/pkg/v1/manifest.go
+++ b/vendor/github.com/google/go-containerregistry/pkg/v1/manifest.go
@@ -23,8 +23,8 @@ import (
 
 // Manifest represents the OCI image manifest in a structured way.
 type Manifest struct {
-	SchemaVersion int64             `json:"schemaVersion,omitempty"`
-	MediaType     types.MediaType   `json:"mediaType"`
+	SchemaVersion int64             `json:"schemaVersion"`
+	MediaType     types.MediaType   `json:"mediaType,omitempty"`
 	Config        Descriptor        `json:"config"`
 	Layers        []Descriptor      `json:"layers"`
 	Annotations   map[string]string `json:"annotations,omitempty"`
diff --git a/vendor/github.com/google/go-containerregistry/pkg/v1/mutate/mutate.go b/vendor/github.com/google/go-containerregistry/pkg/v1/mutate/mutate.go
index 78b768fc..7336ed0a 100644
--- a/vendor/github.com/google/go-containerregistry/pkg/v1/mutate/mutate.go
+++ b/vendor/github.com/google/go-containerregistry/pkg/v1/mutate/mutate.go
@@ -24,9 +24,9 @@ import (
 	"strings"
 	"time"
 
+	"github.com/google/go-containerregistry/internal/gzip"
 	v1 "github.com/google/go-containerregistry/pkg/v1"
 	"github.com/google/go-containerregistry/pkg/v1/empty"
-	"github.com/google/go-containerregistry/pkg/v1/internal/gzip"
 	"github.com/google/go-containerregistry/pkg/v1/match"
 	"github.com/google/go-containerregistry/pkg/v1/tarball"
 	"github.com/google/go-containerregistry/pkg/v1/types"
diff --git a/vendor/github.com/google/go-containerregistry/pkg/v1/partial/compressed.go b/vendor/github.com/google/go-containerregistry/pkg/v1/partial/compressed.go
index 0a229557..2e6e548a 100644
--- a/vendor/github.com/google/go-containerregistry/pkg/v1/partial/compressed.go
+++ b/vendor/github.com/google/go-containerregistry/pkg/v1/partial/compressed.go
@@ -17,8 +17,8 @@ package partial
 import (
 	"io"
 
+	"github.com/google/go-containerregistry/internal/gzip"
 	v1 "github.com/google/go-containerregistry/pkg/v1"
-	"github.com/google/go-containerregistry/pkg/v1/internal/gzip"
 	"github.com/google/go-containerregistry/pkg/v1/types"
 )
 
diff --git a/vendor/github.com/google/go-containerregistry/pkg/v1/partial/uncompressed.go b/vendor/github.com/google/go-containerregistry/pkg/v1/partial/uncompressed.go
index 69cea61c..df20d3aa 100644
--- a/vendor/github.com/google/go-containerregistry/pkg/v1/partial/uncompressed.go
+++ b/vendor/github.com/google/go-containerregistry/pkg/v1/partial/uncompressed.go
@@ -19,8 +19,8 @@ import (
 	"io"
 	"sync"
 
+	"github.com/google/go-containerregistry/internal/gzip"
 	v1 "github.com/google/go-containerregistry/pkg/v1"
-	"github.com/google/go-containerregistry/pkg/v1/internal/gzip"
 	"github.com/google/go-containerregistry/pkg/v1/types"
 )
 
diff --git a/vendor/github.com/google/go-containerregistry/pkg/v1/remote/descriptor.go b/vendor/github.com/google/go-containerregistry/pkg/v1/remote/descriptor.go
index 9c248cfe..2e0f9996 100644
--- a/vendor/github.com/google/go-containerregistry/pkg/v1/remote/descriptor.go
+++ b/vendor/github.com/google/go-containerregistry/pkg/v1/remote/descriptor.go
@@ -25,10 +25,10 @@ import (
 	"strconv"
 	"strings"
 
+	"github.com/google/go-containerregistry/internal/verify"
 	"github.com/google/go-containerregistry/pkg/logs"
 	"github.com/google/go-containerregistry/pkg/name"
 	v1 "github.com/google/go-containerregistry/pkg/v1"
-	"github.com/google/go-containerregistry/pkg/v1/internal/verify"
 	"github.com/google/go-containerregistry/pkg/v1/partial"
 	"github.com/google/go-containerregistry/pkg/v1/remote/transport"
 	"github.com/google/go-containerregistry/pkg/v1/types"
@@ -324,14 +324,26 @@ func (f *fetcher) headManifest(ref name.Reference, acceptable []types.MediaType)
 		return nil, err
 	}
 
-	mediaType := types.MediaType(resp.Header.Get("Content-Type"))
+	mth := resp.Header.Get("Content-Type")
+	if mth == "" {
+		return nil, fmt.Errorf("HEAD %s: response did not include Content-Type header", u.String())
+	}
+	mediaType := types.MediaType(mth)
 
-	size, err := strconv.ParseInt(resp.Header.Get("Content-Length"), 10, 64)
+	lh := resp.Header.Get("Content-Length")
+	if lh == "" {
+		return nil, fmt.Errorf("HEAD %s: response did not include Content-Length header", u.String())
+	}
+	size, err := strconv.ParseInt(lh, 10, 64)
 	if err != nil {
 		return nil, err
 	}
 
-	digest, err := v1.NewHash(resp.Header.Get("Docker-Content-Digest"))
+	dh := resp.Header.Get("Docker-Content-Digest")
+	if dh == "" {
+		return nil, fmt.Errorf("HEAD %s: response did not include Docker-Content-Digest header", u.String())
+	}
+	digest, err := v1.NewHash(dh)
 	if err != nil {
 		return nil, err
 	}
diff --git a/vendor/github.com/google/go-containerregistry/pkg/v1/remote/image.go b/vendor/github.com/google/go-containerregistry/pkg/v1/remote/image.go
index e04961e9..64472364 100644
--- a/vendor/github.com/google/go-containerregistry/pkg/v1/remote/image.go
+++ b/vendor/github.com/google/go-containerregistry/pkg/v1/remote/image.go
@@ -21,10 +21,10 @@ import (
 	"net/url"
 	"sync"
 
-	"github.com/google/go-containerregistry/pkg/internal/redact"
+	"github.com/google/go-containerregistry/internal/redact"
+	"github.com/google/go-containerregistry/internal/verify"
 	"github.com/google/go-containerregistry/pkg/name"
 	v1 "github.com/google/go-containerregistry/pkg/v1"
-	"github.com/google/go-containerregistry/pkg/v1/internal/verify"
 	"github.com/google/go-containerregistry/pkg/v1/partial"
 	"github.com/google/go-containerregistry/pkg/v1/remote/transport"
 	"github.com/google/go-containerregistry/pkg/v1/types"
diff --git a/vendor/github.com/google/go-containerregistry/pkg/v1/remote/layer.go b/vendor/github.com/google/go-containerregistry/pkg/v1/remote/layer.go
index 6a941a1a..89b0ede0 100644
--- a/vendor/github.com/google/go-containerregistry/pkg/v1/remote/layer.go
+++ b/vendor/github.com/google/go-containerregistry/pkg/v1/remote/layer.go
@@ -17,7 +17,7 @@ package remote
 import (
 	"io"
 
-	"github.com/google/go-containerregistry/pkg/internal/redact"
+	"github.com/google/go-containerregistry/internal/redact"
 	"github.com/google/go-containerregistry/pkg/name"
 	v1 "github.com/google/go-containerregistry/pkg/v1"
 	"github.com/google/go-containerregistry/pkg/v1/partial"
diff --git a/vendor/github.com/google/go-containerregistry/pkg/v1/remote/multi_write.go b/vendor/github.com/google/go-containerregistry/pkg/v1/remote/multi_write.go
index 6b81044f..45408fc4 100644
--- a/vendor/github.com/google/go-containerregistry/pkg/v1/remote/multi_write.go
+++ b/vendor/github.com/google/go-containerregistry/pkg/v1/remote/multi_write.go
@@ -33,7 +33,7 @@ import (
 // Current limitations:
 // - All refs must share the same repository.
 // - Images cannot consist of stream.Layers.
-func MultiWrite(m map[name.Reference]Taggable, options ...Option) error {
+func MultiWrite(m map[name.Reference]Taggable, options ...Option) (rerr error) {
 	// Determine the repository being pushed to; if asked to push to
 	// multiple repositories, give up.
 	var repo, zero name.Repository
@@ -86,14 +86,54 @@ func MultiWrite(m map[name.Reference]Taggable, options ...Option) error {
 		return err
 	}
 	w := writer{
-		repo:    repo,
-		client:  &http.Client{Transport: tr},
-		context: o.context,
+		repo:       repo,
+		client:     &http.Client{Transport: tr},
+		context:    o.context,
+		updates:    o.updates,
+		lastUpdate: &v1.Update{},
+	}
+
+	// Collect the total size of blobs and manifests we're about to write.
+	if o.updates != nil {
+		defer close(o.updates)
+		defer func() { sendError(o.updates, rerr) }()
+		for _, b := range blobs {
+			size, err := b.Size()
+			if err != nil {
+				return err
+			}
+			w.lastUpdate.Total += size
+		}
+		countManifest := func(t Taggable) error {
+			b, err := t.RawManifest()
+			if err != nil {
+				return err
+			}
+			w.lastUpdate.Total += int64(len(b))
+			return nil
+		}
+		for _, i := range images {
+			if err := countManifest(i); err != nil {
+				return err
+			}
+		}
+		for _, nm := range newManifests {
+			for _, i := range nm {
+				if err := countManifest(i); err != nil {
+					return err
+				}
+			}
+		}
+		for _, i := range indexes {
+			if err := countManifest(i); err != nil {
+				return err
+			}
+		}
 	}
 
 	// Upload individual blobs and collect any errors.
 	blobChan := make(chan v1.Layer, 2*o.jobs)
-	var g errgroup.Group
+	g, ctx := errgroup.WithContext(o.context)
 	for i := 0; i < o.jobs; i++ {
 		// Start N workers consuming blobs to upload.
 		g.Go(func() error {
@@ -105,12 +145,17 @@ func MultiWrite(m map[name.Reference]Taggable, options ...Option) error {
 			return nil
 		})
 	}
-	go func() {
+	g.Go(func() error {
+		defer close(blobChan)
 		for _, b := range blobs {
-			blobChan <- b
+			select {
+			case blobChan <- b:
+			case <-ctx.Done():
+				return ctx.Err()
+			}
 		}
-		close(blobChan)
-	}()
+		return nil
+	})
 	if err := g.Wait(); err != nil {
 		return err
 	}
@@ -155,8 +200,8 @@ func MultiWrite(m map[name.Reference]Taggable, options ...Option) error {
 	}
 	// Push originally requested index manifests, which might depend on
 	// newly discovered manifests.
-	return commitMany(indexes)
 
+	return commitMany(indexes)
 }
 
 // addIndexBlobs adds blobs to the set of blobs we intend to upload, and
diff --git a/vendor/github.com/google/go-containerregistry/pkg/v1/remote/options.go b/vendor/github.com/google/go-containerregistry/pkg/v1/remote/options.go
index 04ff5082..7edebdf7 100644
--- a/vendor/github.com/google/go-containerregistry/pkg/v1/remote/options.go
+++ b/vendor/github.com/google/go-containerregistry/pkg/v1/remote/options.go
@@ -37,6 +37,7 @@ type options struct {
 	jobs                           int
 	userAgent                      string
 	allowNondistributableArtifacts bool
+	updates                        chan<- v1.Update
 }
 
 var defaultPlatform = v1.Platform{
@@ -66,9 +67,6 @@ func makeOptions(target authn.Resource, opts ...Option) (*options, error) {
 		if err != nil {
 			return nil, err
 		}
-		if auth == authn.Anonymous {
-			logs.Warn.Printf("No matching credentials were found for %q, falling back on anonymous", target)
-		}
 		o.auth = auth
 	}
 
@@ -184,3 +182,14 @@ func WithNondistributable(o *options) error {
 	o.allowNondistributableArtifacts = true
 	return nil
 }
+
+// WithProgress takes a channel that will receive progress updates as bytes are written.
+//
+// Sending updates to an unbuffered channel will block writes, so callers
+// should provide a buffered channel to avoid potential deadlocks.
+func WithProgress(updates chan<- v1.Update) Option {
+	return func(o *options) error {
+		o.updates = updates
+		return nil
+	}
+}
diff --git a/vendor/github.com/google/go-containerregistry/pkg/v1/remote/transport/bearer.go b/vendor/github.com/google/go-containerregistry/pkg/v1/remote/transport/bearer.go
index cd05509e..49941bd8 100644
--- a/vendor/github.com/google/go-containerregistry/pkg/v1/remote/transport/bearer.go
+++ b/vendor/github.com/google/go-containerregistry/pkg/v1/remote/transport/bearer.go
@@ -25,8 +25,9 @@ import (
 	"strings"
 
 	authchallenge "github.com/docker/distribution/registry/client/auth/challenge"
+	"github.com/google/go-containerregistry/internal/redact"
 	"github.com/google/go-containerregistry/pkg/authn"
-	"github.com/google/go-containerregistry/pkg/internal/redact"
+	"github.com/google/go-containerregistry/pkg/logs"
 	"github.com/google/go-containerregistry/pkg/name"
 )
 
@@ -262,6 +263,7 @@ func (bt *bearerTransport) refreshOauth(ctx context.Context) ([]byte, error) {
 	defer resp.Body.Close()
 
 	if err := CheckError(resp, http.StatusOK); err != nil {
+		logs.Warn.Printf("No matching credentials were found for %q", bt.registry)
 		return nil, err
 	}
 
@@ -301,6 +303,7 @@ func (bt *bearerTransport) refreshBasic(ctx context.Context) ([]byte, error) {
 	defer resp.Body.Close()
 
 	if err := CheckError(resp, http.StatusOK); err != nil {
+		logs.Warn.Printf("No matching credentials were found for %q", bt.registry)
 		return nil, err
 	}
 
diff --git a/vendor/github.com/google/go-containerregistry/pkg/v1/remote/transport/error.go b/vendor/github.com/google/go-containerregistry/pkg/v1/remote/transport/error.go
index 56708038..bb59d22e 100644
--- a/vendor/github.com/google/go-containerregistry/pkg/v1/remote/transport/error.go
+++ b/vendor/github.com/google/go-containerregistry/pkg/v1/remote/transport/error.go
@@ -100,7 +100,7 @@ func (e *Error) Temporary() bool {
 	return true
 }
 
-// TODO(jonjohnsonjr): Consider moving to pkg/internal/redact.
+// TODO(jonjohnsonjr): Consider moving to internal/redact.
 func redactURL(original *url.URL) *url.URL {
 	qs := original.Query()
 	for k, v := range qs {
@@ -163,6 +163,7 @@ var temporaryErrorCodes = map[ErrorCode]struct{}{
 }
 
 var temporaryStatusCodes = map[int]struct{}{
+	http.StatusRequestTimeout:      {},
 	http.StatusInternalServerError: {},
 	http.StatusBadGateway:          {},
 	http.StatusServiceUnavailable:  {},
diff --git a/vendor/github.com/google/go-containerregistry/pkg/v1/remote/transport/logger.go b/vendor/github.com/google/go-containerregistry/pkg/v1/remote/transport/logger.go
index 3b024866..c341f844 100644
--- a/vendor/github.com/google/go-containerregistry/pkg/v1/remote/transport/logger.go
+++ b/vendor/github.com/google/go-containerregistry/pkg/v1/remote/transport/logger.go
@@ -20,7 +20,7 @@ import (
 	"net/http/httputil"
 	"time"
 
-	"github.com/google/go-containerregistry/pkg/internal/redact"
+	"github.com/google/go-containerregistry/internal/redact"
 	"github.com/google/go-containerregistry/pkg/logs"
 )
 
diff --git a/vendor/github.com/google/go-containerregistry/pkg/v1/remote/transport/retry.go b/vendor/github.com/google/go-containerregistry/pkg/v1/remote/transport/retry.go
index 672622b6..7f7d1e45 100644
--- a/vendor/github.com/google/go-containerregistry/pkg/v1/remote/transport/retry.go
+++ b/vendor/github.com/google/go-containerregistry/pkg/v1/remote/transport/retry.go
@@ -18,7 +18,7 @@ import (
 	"net/http"
 	"time"
 
-	"github.com/google/go-containerregistry/pkg/internal/retry"
+	"github.com/google/go-containerregistry/internal/retry"
 )
 
 // Sleep for 0.1, 0.3, 0.9, 2.7 seconds. This should cover networking blips.
diff --git a/vendor/github.com/google/go-containerregistry/pkg/v1/remote/write.go b/vendor/github.com/google/go-containerregistry/pkg/v1/remote/write.go
index 261e9ec4..219afe51 100644
--- a/vendor/github.com/google/go-containerregistry/pkg/v1/remote/write.go
+++ b/vendor/github.com/google/go-containerregistry/pkg/v1/remote/write.go
@@ -23,15 +23,17 @@ import (
 	"net/http"
 	"net/url"
 	"strings"
+	"sync/atomic"
 	"time"
 
-	"github.com/google/go-containerregistry/pkg/internal/redact"
-	"github.com/google/go-containerregistry/pkg/internal/retry"
+	"github.com/google/go-containerregistry/internal/redact"
+	"github.com/google/go-containerregistry/internal/retry"
 	"github.com/google/go-containerregistry/pkg/logs"
 	"github.com/google/go-containerregistry/pkg/name"
 	v1 "github.com/google/go-containerregistry/pkg/v1"
 	"github.com/google/go-containerregistry/pkg/v1/partial"
 	"github.com/google/go-containerregistry/pkg/v1/remote/transport"
+	"github.com/google/go-containerregistry/pkg/v1/stream"
 	"github.com/google/go-containerregistry/pkg/v1/types"
 	"golang.org/x/sync/errgroup"
 )
@@ -42,61 +44,97 @@ type Taggable interface {
 }
 
 // Write pushes the provided img to the specified image reference.
-func Write(ref name.Reference, img v1.Image, options ...Option) error {
-	ls, err := img.Layers()
+func Write(ref name.Reference, img v1.Image, options ...Option) (rerr error) {
+	o, err := makeOptions(ref.Context(), options...)
 	if err != nil {
 		return err
 	}
 
-	o, err := makeOptions(ref.Context(), options...)
+	var lastUpdate *v1.Update
+	if o.updates != nil {
+		lastUpdate = &v1.Update{}
+		lastUpdate.Total, err = countImage(img, o.allowNondistributableArtifacts)
+		if err != nil {
+			return err
+		}
+		defer close(o.updates)
+		defer func() { sendError(o.updates, rerr) }()
+	}
+	return writeImage(ref, img, o, lastUpdate)
+}
+
+func writeImage(ref name.Reference, img v1.Image, o *options, lastUpdate *v1.Update) error {
+	ls, err := img.Layers()
 	if err != nil {
 		return err
 	}
-
 	scopes := scopesForUploadingImage(ref.Context(), ls)
 	tr, err := transport.NewWithContext(o.context, ref.Context().Registry, o.auth, o.transport, scopes)
 	if err != nil {
 		return err
 	}
 	w := writer{
-		repo:    ref.Context(),
-		client:  &http.Client{Transport: tr},
-		context: o.context,
+		repo:       ref.Context(),
+		client:     &http.Client{Transport: tr},
+		context:    o.context,
+		updates:    o.updates,
+		lastUpdate: lastUpdate,
+	}
+
+	// Upload individual blobs and collect any errors.
+	blobChan := make(chan v1.Layer, 2*o.jobs)
+	g, ctx := errgroup.WithContext(o.context)
+	for i := 0; i < o.jobs; i++ {
+		// Start N workers consuming blobs to upload.
+		g.Go(func() error {
+			for b := range blobChan {
+				if err := w.uploadOne(b); err != nil {
+					return err
+				}
+			}
+			return nil
+		})
 	}
 
 	// Upload individual layers in goroutines and collect any errors.
 	// If we can dedupe by the layer digest, try to do so. If we can't determine
 	// the digest for whatever reason, we can't dedupe and might re-upload.
-	var g errgroup.Group
-	uploaded := map[v1.Hash]bool{}
-	for _, l := range ls {
-		l := l
-
-		// Handle foreign layers.
-		mt, err := l.MediaType()
-		if err != nil {
-			return err
-		}
-		if !mt.IsDistributable() && !o.allowNondistributableArtifacts {
-			continue
-		}
+	g.Go(func() error {
+		defer close(blobChan)
+		uploaded := map[v1.Hash]bool{}
+		for _, l := range ls {
+			l := l
+
+			// Handle foreign layers.
+			mt, err := l.MediaType()
+			if err != nil {
+				return err
+			}
+			if !mt.IsDistributable() && !o.allowNondistributableArtifacts {
+				continue
+			}
 
-		// Streaming layers calculate their digests while uploading them. Assume
-		// an error here indicates we need to upload the layer.
-		h, err := l.Digest()
-		if err == nil {
-			// If we can determine the layer's digest ahead of
-			// time, use it to dedupe uploads.
-			if uploaded[h] {
-				continue // Already uploading.
+			// Streaming layers calculate their digests while uploading them. Assume
+			// an error here indicates we need to upload the layer.
+			h, err := l.Digest()
+			if err == nil {
+				// If we can determine the layer's digest ahead of
+				// time, use it to dedupe uploads.
+				if uploaded[h] {
+					continue // Already uploading.
+				}
+				uploaded[h] = true
+			}
+			select {
+			case blobChan <- l:
+			case <-ctx.Done():
+				return ctx.Err()
 			}
-			uploaded[h] = true
 		}
-
-		// TODO(#803): Pipe through remote.WithJobs and upload these in parallel.
-		g.Go(func() error {
-			return w.uploadOne(l)
-		})
+		return nil
+	})
+	if err := g.Wait(); err != nil {
+		return err
 	}
 
 	if l, err := partial.ConfigLayer(img); err != nil {
@@ -137,6 +175,16 @@ type writer struct {
 	repo    name.Repository
 	client  *http.Client
 	context context.Context
+
+	updates    chan<- v1.Update
+	lastUpdate *v1.Update
+}
+
+func sendError(ch chan<- v1.Update, err error) error {
+	if err != nil && ch != nil {
+		ch <- v1.Update{Error: err}
+	}
+	return err
 }
 
 // url returns a url.Url for the specified path in the context of this remote image reference.
@@ -259,10 +307,49 @@ func (w *writer) initiateUpload(from, mount string) (location string, mounted bo
 	}
 }
 
+type progressReader struct {
+	rc io.ReadCloser
+
+	count      *int64 // number of bytes this reader has read, to support resetting on retry.
+	updates    chan<- v1.Update
+	lastUpdate *v1.Update
+}
+
+func (r *progressReader) Read(b []byte) (int, error) {
+	n, err := r.rc.Read(b)
+	if err != nil {
+		return n, err
+	}
+	atomic.AddInt64(r.count, int64(n))
+	// TODO: warn/debug log if sending takes too long, or if sending is blocked while context is cancelled.
+	r.updates <- v1.Update{
+		Total:    r.lastUpdate.Total,
+		Complete: atomic.AddInt64(&r.lastUpdate.Complete, int64(n)),
+	}
+	return n, nil
+}
+
+func (r *progressReader) Close() error { return r.rc.Close() }
+
 // streamBlob streams the contents of the blob to the specified location.
 // On failure, this will return an error.  On success, this will return the location
 // header indicating how to commit the streamed blob.
-func (w *writer) streamBlob(ctx context.Context, blob io.ReadCloser, streamLocation string) (commitLocation string, err error) {
+func (w *writer) streamBlob(ctx context.Context, blob io.ReadCloser, streamLocation string) (commitLocation string, rerr error) {
+	reset := func() {}
+	defer func() {
+		if rerr != nil {
+			reset()
+		}
+	}()
+	if w.updates != nil {
+		var count int64
+		blob = &progressReader{rc: blob, updates: w.updates, lastUpdate: w.lastUpdate, count: &count}
+		reset = func() {
+			atomic.AddInt64(&w.lastUpdate.Complete, -count)
+			w.updates <- *w.lastUpdate
+		}
+	}
+
 	req, err := http.NewRequest(http.MethodPatch, streamLocation, blob)
 	if err != nil {
 		return "", err
@@ -308,6 +395,17 @@ func (w *writer) commitBlob(location, digest string) error {
 	return transport.CheckError(resp, http.StatusCreated)
 }
 
+// incrProgress increments and sends a progress update, if WithProgress is used.
+func (w *writer) incrProgress(written int64) {
+	if w.updates == nil {
+		return
+	}
+	w.updates <- v1.Update{
+		Total:    w.lastUpdate.Total,
+		Complete: atomic.AddInt64(&w.lastUpdate.Complete, int64(written)),
+	}
+}
+
 // uploadOne performs a complete upload of a single layer.
 func (w *writer) uploadOne(l v1.Layer) error {
 	var from, mount string
@@ -319,6 +417,11 @@ func (w *writer) uploadOne(l v1.Layer) error {
 			return err
 		}
 		if existing {
+			size, err := l.Size()
+			if err != nil {
+				return err
+			}
+			w.incrProgress(size)
 			logs.Progress.Printf("existing blob: %v", h)
 			return nil
 		}
@@ -338,6 +441,11 @@ func (w *writer) uploadOne(l v1.Layer) error {
 		if err != nil {
 			return err
 		} else if mounted {
+			size, err := l.Size()
+			if err != nil {
+				return err
+			}
+			w.incrProgress(size)
 			h, err := l.Digest()
 			if err != nil {
 				return err
@@ -400,6 +508,11 @@ func (w *writer) writeIndex(ref name.Reference, ii v1.ImageIndex, options ...Opt
 		return err
 	}
 
+	o, err := makeOptions(ref.Context(), options...)
+	if err != nil {
+		return err
+	}
+
 	// TODO(#803): Pipe through remote.WithJobs and upload these in parallel.
 	for _, desc := range index.Manifests {
 		ref := ref.Context().Digest(desc.Digest.String())
@@ -418,7 +531,6 @@ func (w *writer) writeIndex(ref name.Reference, ii v1.ImageIndex, options ...Opt
 			if err != nil {
 				return err
 			}
-
 			if err := w.writeIndex(ref, ii); err != nil {
 				return err
 			}
@@ -427,10 +539,7 @@ func (w *writer) writeIndex(ref name.Reference, ii v1.ImageIndex, options ...Opt
 			if err != nil {
 				return err
 			}
-			// TODO: Ideally we could reuse this writer, but we need to know
-			// scopes before we do the token exchange. To be lazy here, just
-			// re-do the token exchange. MultiWrite fixes this.
-			if err := Write(ref, img, options...); err != nil {
+			if err := writeImage(ref, img, o, w.lastUpdate); err != nil {
 				return err
 			}
 		default:
@@ -522,6 +631,7 @@ func (w *writer) commitManifest(t Taggable, ref name.Reference) error {
 
 	// The image was successfully pushed!
 	logs.Progress.Printf("%v: digest: %v size: %d", ref, desc.Digest, desc.Size)
+	w.incrProgress(int64(len(raw)))
 	return nil
 }
 
@@ -553,11 +663,12 @@ func scopesForUploadingImage(repo name.Repository, layers []v1.Layer) []string {
 // WriteIndex pushes the provided ImageIndex to the specified image reference.
 // WriteIndex will attempt to push all of the referenced manifests before
 // attempting to push the ImageIndex, to retain referential integrity.
-func WriteIndex(ref name.Reference, ii v1.ImageIndex, options ...Option) error {
+func WriteIndex(ref name.Reference, ii v1.ImageIndex, options ...Option) (rerr error) {
 	o, err := makeOptions(ref.Context(), options...)
 	if err != nil {
 		return err
 	}
+
 	scopes := []string{ref.Scope(transport.PushScope)}
 	tr, err := transport.NewWithContext(o.context, ref.Context().Registry, o.auth, o.transport, scopes)
 	if err != nil {
@@ -567,12 +678,132 @@ func WriteIndex(ref name.Reference, ii v1.ImageIndex, options ...Option) error {
 		repo:    ref.Context(),
 		client:  &http.Client{Transport: tr},
 		context: o.context,
+		updates: o.updates,
+	}
+
+	if o.updates != nil {
+		w.lastUpdate = &v1.Update{}
+		w.lastUpdate.Total, err = countIndex(ii, o.allowNondistributableArtifacts)
+		if err != nil {
+			return err
+		}
+		defer close(o.updates)
+		defer func() { sendError(o.updates, rerr) }()
 	}
+
 	return w.writeIndex(ref, ii, options...)
 }
 
+// countImage counts the total size of all layers + config blob + manifest for
+// an image. It de-dupes duplicate layers.
+func countImage(img v1.Image, allowNondistributableArtifacts bool) (int64, error) {
+	var total int64
+	ls, err := img.Layers()
+	if err != nil {
+		return 0, err
+	}
+	seen := map[v1.Hash]bool{}
+	for _, l := range ls {
+		// Handle foreign layers.
+		mt, err := l.MediaType()
+		if err != nil {
+			return 0, err
+		}
+		if !mt.IsDistributable() && !allowNondistributableArtifacts {
+			continue
+		}
+
+		// TODO: support streaming layers which update the total count as they write.
+		if _, ok := l.(*stream.Layer); ok {
+			return 0, errors.New("cannot use stream.Layer and WithProgress")
+		}
+
+		// Dedupe layers.
+		d, err := l.Digest()
+		if err != nil {
+			return 0, err
+		}
+		if seen[d] {
+			continue
+		}
+		seen[d] = true
+
+		size, err := l.Size()
+		if err != nil {
+			return 0, err
+		}
+		total += size
+	}
+	b, err := img.RawConfigFile()
+	if err != nil {
+		return 0, err
+	}
+	total += int64(len(b))
+	size, err := img.Size()
+	if err != nil {
+		return 0, err
+	}
+	total += size
+	return total, nil
+}
+
+// countIndex counts the total size of all images + sub-indexes for an index.
+// It does not attempt to de-dupe duplicate images, etc.
+func countIndex(idx v1.ImageIndex, allowNondistributableArtifacts bool) (int64, error) {
+	var total int64
+	mf, err := idx.IndexManifest()
+	if err != nil {
+		return 0, err
+	}
+
+	for _, desc := range mf.Manifests {
+		switch desc.MediaType {
+		case types.OCIImageIndex, types.DockerManifestList:
+			sidx, err := idx.ImageIndex(desc.Digest)
+			if err != nil {
+				return 0, err
+			}
+			size, err := countIndex(sidx, allowNondistributableArtifacts)
+			if err != nil {
+				return 0, err
+			}
+			total += size
+		case types.OCIManifestSchema1, types.DockerManifestSchema2:
+			simg, err := idx.Image(desc.Digest)
+			if err != nil {
+				return 0, err
+			}
+			size, err := countImage(simg, allowNondistributableArtifacts)
+			if err != nil {
+				return 0, err
+			}
+			total += size
+		default:
+			// Workaround for #819.
+			if wl, ok := idx.(withLayer); ok {
+				layer, err := wl.Layer(desc.Digest)
+				if err != nil {
+					return 0, err
+				}
+				size, err := layer.Size()
+				if err != nil {
+					return 0, err
+				}
+				total += size
+			}
+		}
+	}
+
+	size, err := idx.Size()
+	if err != nil {
+		return 0, err
+	}
+	total += size
+	return total, nil
+}
+
 // WriteLayer uploads the provided Layer to the specified repo.
-func WriteLayer(repo name.Repository, layer v1.Layer, options ...Option) error {
+func WriteLayer(repo name.Repository, layer v1.Layer, options ...Option) (rerr error) {
 	o, err := makeOptions(repo, options...)
 	if err != nil {
 		return err
@@ -586,8 +817,23 @@ func WriteLayer(repo name.Repository, layer v1.Layer, options ...Option) error {
 		repo:    repo,
 		client:  &http.Client{Transport: tr},
 		context: o.context,
+		updates: o.updates,
 	}
 
+	if o.updates != nil {
+		defer close(o.updates)
+		defer func() { sendError(o.updates, rerr) }()
+
+		// TODO: support streaming layers which update the total count as they write.
+		if _, ok := layer.(*stream.Layer); ok {
+			return errors.New("cannot use stream.Layer and WithProgress")
+		}
+		size, err := layer.Size()
+		if err != nil {
+			return err
+		}
+		w.lastUpdate = &v1.Update{Total: size}
+	}
 	return w.uploadOne(layer)
 }
 
@@ -603,11 +849,26 @@ func WriteLayer(repo name.Repository, layer v1.Layer, options ...Option) error {
 // should ensure that all blobs or manifests that are referenced by t exist
 // in the target registry.
 func Tag(tag name.Tag, t Taggable, options ...Option) error {
-	o, err := makeOptions(tag.Context(), options...)
+	return Put(tag, t, options...)
+}
+
+// Put adds a manifest from the given Taggable via PUT /v1/.../manifest/<ref>
+//
+// Notable implementations of Taggable are v1.Image, v1.ImageIndex, and
+// remote.Descriptor.
+//
+// If t implements MediaType, we will use that for the Content-Type, otherwise
+// we will default to types.DockerManifestSchema2.
+//
+// Put does not attempt to write anything other than the manifest, so callers
+// should ensure that all blobs or manifests that are referenced by t exist
+// in the target registry.
+func Put(ref name.Reference, t Taggable, options ...Option) error {
+	o, err := makeOptions(ref.Context(), options...)
 	if err != nil {
 		return err
 	}
-	scopes := []string{tag.Scope(transport.PushScope)}
+	scopes := []string{ref.Scope(transport.PushScope)}
 
 	// TODO: This *always* does a token exchange. For some registries,
 	// that's pretty slow. Some ideas;
@@ -615,15 +876,15 @@ func Tag(tag name.Tag, t Taggable, options ...Option) error {
 	// * Allow callers to pass in a transport.Transport, typecheck
 	//   it to allow them to reuse the transport across multiple calls.
 	// * WithTag option to do multiple manifest PUTs in commitManifest.
-	tr, err := transport.NewWithContext(o.context, tag.Context().Registry, o.auth, o.transport, scopes)
+	tr, err := transport.NewWithContext(o.context, ref.Context().Registry, o.auth, o.transport, scopes)
 	if err != nil {
 		return err
 	}
 	w := writer{
-		repo:    tag.Context(),
+		repo:    ref.Context(),
 		client:  &http.Client{Transport: tr},
 		context: o.context,
 	}
 
-	return w.commitManifest(t, tag)
+	return w.commitManifest(t, ref)
 }
diff --git a/vendor/github.com/google/go-containerregistry/pkg/v1/tarball/image.go b/vendor/github.com/google/go-containerregistry/pkg/v1/tarball/image.go
index 08f7a812..980842d2 100644
--- a/vendor/github.com/google/go-containerregistry/pkg/v1/tarball/image.go
+++ b/vendor/github.com/google/go-containerregistry/pkg/v1/tarball/image.go
@@ -23,11 +23,13 @@ import (
 	"io"
 	"io/ioutil"
 	"os"
+	"path"
+	"path/filepath"
 	"sync"
 
+	"github.com/google/go-containerregistry/internal/gzip"
 	"github.com/google/go-containerregistry/pkg/name"
 	v1 "github.com/google/go-containerregistry/pkg/v1"
-	"github.com/google/go-containerregistry/pkg/v1/internal/gzip"
 	"github.com/google/go-containerregistry/pkg/v1/partial"
 	"github.com/google/go-containerregistry/pkg/v1/types"
 )
@@ -216,6 +218,10 @@ func extractFileFromTar(opener Opener, filePath string) (io.ReadCloser, error) {
 			return nil, err
 		}
 		if hdr.Name == filePath {
+			if hdr.Typeflag == tar.TypeSymlink || hdr.Typeflag == tar.TypeLink {
+				currentDir := filepath.Dir(filePath)
+				return extractFileFromTar(opener, path.Join(currentDir, hdr.Linkname))
+			}
 			close = false
 			return tarFile{
 				Reader: tf,
diff --git a/vendor/github.com/google/go-containerregistry/pkg/v1/tarball/layer.go b/vendor/github.com/google/go-containerregistry/pkg/v1/tarball/layer.go
index dc8684ce..5ec1d551 100644
--- a/vendor/github.com/google/go-containerregistry/pkg/v1/tarball/layer.go
+++ b/vendor/github.com/google/go-containerregistry/pkg/v1/tarball/layer.go
@@ -23,10 +23,10 @@ import (
 	"sync"
 
 	"github.com/containerd/stargz-snapshotter/estargz"
+	"github.com/google/go-containerregistry/internal/and"
+	gestargz "github.com/google/go-containerregistry/internal/estargz"
+	ggzip "github.com/google/go-containerregistry/internal/gzip"
 	v1 "github.com/google/go-containerregistry/pkg/v1"
-	"github.com/google/go-containerregistry/pkg/v1/internal/and"
-	gestargz "github.com/google/go-containerregistry/pkg/v1/internal/estargz"
-	ggzip "github.com/google/go-containerregistry/pkg/v1/internal/gzip"
 	"github.com/google/go-containerregistry/pkg/v1/types"
 )
 
diff --git a/vendor/modules.txt b/vendor/modules.txt
index 395e07ed..12c11a77 100644
--- a/vendor/modules.txt
+++ b/vendor/modules.txt
@@ -105,16 +105,20 @@ github.com/google/go-cmp/cmp/internal/diff
 github.com/google/go-cmp/cmp/internal/flags
 github.com/google/go-cmp/cmp/internal/function
 github.com/google/go-cmp/cmp/internal/value
-# github.com/google/go-containerregistry v0.4.1-0.20210216200643-d81088d9983e
+# github.com/google/go-containerregistry v0.5.0
 ## explicit
 github.com/google/go-containerregistry/cmd/crane/cmd
+github.com/google/go-containerregistry/internal/and
+github.com/google/go-containerregistry/internal/estargz
+github.com/google/go-containerregistry/internal/gzip
+github.com/google/go-containerregistry/internal/httptest
+github.com/google/go-containerregistry/internal/legacy
+github.com/google/go-containerregistry/internal/redact
+github.com/google/go-containerregistry/internal/retry
+github.com/google/go-containerregistry/internal/retry/wait
+github.com/google/go-containerregistry/internal/verify
 github.com/google/go-containerregistry/pkg/authn
 github.com/google/go-containerregistry/pkg/crane
-github.com/google/go-containerregistry/pkg/internal/httptest
-github.com/google/go-containerregistry/pkg/internal/legacy
-github.com/google/go-containerregistry/pkg/internal/redact
-github.com/google/go-containerregistry/pkg/internal/retry
-github.com/google/go-containerregistry/pkg/internal/retry/wait
 github.com/google/go-containerregistry/pkg/legacy
 github.com/google/go-containerregistry/pkg/legacy/tarball
 github.com/google/go-containerregistry/pkg/logs
@@ -124,10 +128,6 @@ github.com/google/go-containerregistry/pkg/v1
 github.com/google/go-containerregistry/pkg/v1/cache
 github.com/google/go-containerregistry/pkg/v1/daemon
 github.com/google/go-containerregistry/pkg/v1/empty
-github.com/google/go-containerregistry/pkg/v1/internal/and
-github.com/google/go-containerregistry/pkg/v1/internal/estargz
-github.com/google/go-containerregistry/pkg/v1/internal/gzip
-github.com/google/go-containerregistry/pkg/v1/internal/verify
 github.com/google/go-containerregistry/pkg/v1/layout
 github.com/google/go-containerregistry/pkg/v1/match
 github.com/google/go-containerregistry/pkg/v1/mutate


Test patch:
diff --git a/.github/workflows/kind-e2e.yaml b/.github/workflows/kind-e2e.yaml
index 5429b1c0..423b0e94 100644
--- a/.github/workflows/kind-e2e.yaml
+++ b/.github/workflows/kind-e2e.yaml
@@ -2,7 +2,7 @@ name: KinD e2e tests
 
 on:
   pull_request:
-    branches: [ 'master' ]
+    branches: ['main']
 
 defaults:
   run:
@@ -47,10 +47,9 @@ jobs:
       KO_DOCKER_REPO: registry.local:5000/knative
 
     steps:
-    - name: Set up Go 1.15.x
-      uses: actions/setup-go@v2
+    - uses: actions/setup-go@v2
       with:
-        go-version: 1.15.x
+        go-version: 1.16.x
 
     - name: Check out code onto GOPATH
       uses: actions/checkout@v2
diff --git a/.github/workflows/knative-go-test.yaml b/.github/workflows/knative-go-test.yaml
deleted file mode 100644
index 965df1e7..00000000
--- a/.github/workflows/knative-go-test.yaml
+++ /dev/null
@@ -1,65 +0,0 @@
-# Copyright 2020 The Knative Authors.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#     http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-# This file is automagically synced here from github.com/knative-sandbox/.github
-# repo by knobots: https://github.com/knative-sandbox/knobots and will be overwritten.
-
-name: Test
-
-on:
-
-  push:
-    branches: [ 'main', 'master' ]
-
-  pull_request:
-    branches: [ 'main', 'master', 'release-*' ]
-
-jobs:
-
-  test:
-    name: Unit Tests
-    strategy:
-      matrix:
-        go-version: [1.15.x]
-        platform: [ubuntu-latest]
-
-    runs-on: ${{ matrix.platform }}
-
-    steps:
-
-      - name: Set up Go ${{ matrix.go-version }}
-        uses: actions/setup-go@v2
-        with:
-          go-version: ${{ matrix.go-version }}
-        id: go
-
-      - name: Check out code
-        uses: actions/checkout@v2
-
-      - name: Check for .codecov.yaml
-        id: codecov-enabled
-        uses: andstor/file-existence-action@v1
-        with:
-          files: .codecov.yaml
-
-      - if: steps.codecov-enabled.outputs.files_exists == 'true'
-        name: Produce Go Coverage
-        run: echo 'COVER_OPTS=-coverprofile=coverage.txt -covermode=atomic' >> $GITHUB_ENV
-
-      - name: Test
-        run: go test -race $COVER_OPTS ./...
-
-      - if: steps.codecov-enabled.outputs.files_exists == 'true'
-        name: Codecov
-        uses: codecov/codecov-action@v1
diff --git a/.github/workflows/modules-integration-test.yaml b/.github/workflows/modules-integration-test.yaml
index 737431e2..b81b8755 100644
--- a/.github/workflows/modules-integration-test.yaml
+++ b/.github/workflows/modules-integration-test.yaml
@@ -1,26 +1,8 @@
-# Copyright 2020 The Knative Authors.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#     http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-# This file is automagically synced here from github.com/knative-sandbox/.github
-# repo by knobots: https://github.com/mattmoor/knobots and will be overwritten.
-
 name: Integration Test
 
 on:
-
   pull_request:
-    branches: [ 'master' ]
+    branches: ['main']
 
 jobs:
 
diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml
new file mode 100644
index 00000000..1fa408b4
--- /dev/null
+++ b/.github/workflows/test.yaml
@@ -0,0 +1,23 @@
+name: Test
+
+on:
+  push:
+    branches: ['main']
+  pull_request:
+    branches: ['main']
+
+jobs:
+
+  test:
+    name: Unit Tests
+    runs-on: ubuntu-latest
+
+    steps:
+      - uses: actions/checkout@v2
+      - uses: actions/setup-go@v2
+        with:
+          go-version: 1.16.x
+
+      - run: go test -coverprofile=coverage.txt -covermode=atomic -race ./...
+
+      - uses: codecov/codecov-action@v1
diff --git a/pkg/publish/daemon_test.go b/pkg/publish/daemon_test.go
index 55091d6d..dfc5975f 100644
--- a/pkg/publish/daemon_test.go
+++ b/pkg/publish/daemon_test.go
@@ -1,16 +1,18 @@
-// Copyright 2018 Google LLC All Rights Reserved.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//    http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
+/*
+Copyright 2018 Google LLC All Rights Reserved.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
 
 package publish
 
@@ -26,24 +28,30 @@ import (
 	"github.com/google/go-containerregistry/pkg/v1/random"
 )
 
-type MockImageLoader struct{}
-
-var Tags []string
+type mockClient struct {
+	daemon.Client
+}
 
-func (m *MockImageLoader) ImageLoad(_ context.Context, _ io.Reader, _ bool) (types.ImageLoadResponse, error) {
+func (m *mockClient) NegotiateAPIVersion(context.Context) {}
+func (m *mockClient) ImageLoad(context.Context, io.Reader, bool) (types.ImageLoadResponse, error) {
 	return types.ImageLoadResponse{
 		Body: ioutil.NopCloser(strings.NewReader("Loaded")),
 	}, nil
 }
 
-func (m *MockImageLoader) ImageTag(ctx context.Context, source, target string) error {
-	Tags = append(Tags, target)
+func (m *mockClient) ImageTag(_ context.Context, _ string, tag string) error {
+	Tags = append(Tags, tag)
 	return nil
 }
 
+var Tags []string
+
 func init() {
-	daemon.GetImageLoader = func() (daemon.ImageLoader, error) {
-		return &MockImageLoader{}, nil
+	getOpts = func(ctx context.Context) []daemon.Option {
+		return []daemon.Option{
+			daemon.WithContext(ctx),
+			daemon.WithClient(&mockClient{}),
+		}
 	}
 }
 
@@ -54,7 +62,11 @@ func TestDaemon(t *testing.T) {
 		t.Fatalf("random.Image() = %v", err)
 	}
 
-	def := NewDaemon(md5Hash, []string{})
+	def, err := NewDaemon(md5Hash, []string{})
+	if err != nil {
+		t.Fatalf("NewDaemon() = %v", err)
+	}
+
 	if d, err := def.Publish(context.Background(), img, importpath); err != nil {
 		t.Errorf("Publish() = %v", err)
 	} else if got, want := d.String(), md5Hash("ko.local", importpath); !strings.HasPrefix(got, want) {
@@ -71,7 +83,11 @@ func TestDaemonTags(t *testing.T) {
 		t.Fatalf("random.Image() = %v", err)
 	}
 
-	def := NewDaemon(md5Hash, []string{"v2.0.0", "v1.2.3", "production"})
+	def, err := NewDaemon(md5Hash, []string{"v2.0.0", "v1.2.3", "production"})
+	if err != nil {
+		t.Fatalf("NewDaemon() = %v", err)
+	}
+
 	if d, err := def.Publish(context.Background(), img, importpath); err != nil {
 		t.Errorf("Publish() = %v", err)
 	} else if got, want := d.String(), md5Hash("ko.local", importpath); !strings.HasPrefix(got, want) {
@@ -86,3 +102,25 @@ func TestDaemonTags(t *testing.T) {
 		}
 	}
 }
+
+func TestDaemonDomain(t *testing.T) {
+	Tags = nil
+
+	importpath := "github.com/google/ko"
+	img, err := random.Image(1024, 1)
+	if err != nil {
+		t.Fatalf("random.Image() = %v", err)
+	}
+
+	localDomain := "registry.example.com/repository"
+	def, err := NewDaemon(md5Hash, []string{}, WithLocalDomain(localDomain))
+	if err != nil {
+		t.Fatalf("NewDaemon() = %v", err)
+	}
+
+	if d, err := def.Publish(context.Background(), img, importpath); err != nil {
+		t.Errorf("Publish() = %v", err)
+	} else if got, want := d.String(), md5Hash(localDomain, importpath); !strings.HasPrefix(got, want) {
+		t.Errorf("Publish() = %v, wanted prefix %v", got, want)
+	}
+}
diff --git a/pkg/publish/default_test.go b/pkg/publish/default_test.go
index 597165fc..37dc7ec0 100644
--- a/pkg/publish/default_test.go
+++ b/pkg/publish/default_test.go
@@ -224,4 +224,21 @@ func TestDefaultWithReleaseTag(t *testing.T) {
 	if _, ok := createdTags["v1.2.3"]; !ok {
 		t.Errorf("Tag v1.2.3 was not created.")
 	}
+
+	def, err = NewDefault(repoName, WithTags([]string{releaseTag}), WithTagOnly(true))
+	if err != nil {
+		t.Errorf("NewDefault() = %v", err)
+	}
+	if d, err := def.Publish(context.Background(), img, build.StrictScheme+importpath); err != nil {
+		t.Errorf("Publish() = %v", err)
+	} else if !strings.HasPrefix(d.String(), repoName) {
+		t.Errorf("Publish() = %v, wanted prefix %v", d, tag.Repository)
+	} else if !strings.HasSuffix(d.Context().String(), strings.ToLower(importpath)) {
+		t.Errorf("Publish() = %v, wanted suffix %v", d.Context(), md5Hash("", importpath))
+	} else if !strings.Contains(d.String(), releaseTag) {
+		t.Errorf("Publish() = %v, wanted tag included: %v", d.String(), releaseTag)
+	} else if strings.Contains(d.String(), "@sha256:") {
+		t.Errorf("Publish() = %v, wanted no digest", d.String())
+	}
+
 }
diff --git a/pkg/publish/kind/write_test.go b/pkg/publish/kind/write_test.go
index ec08e936..013e9f1e 100644
--- a/pkg/publish/kind/write_test.go
+++ b/pkg/publish/kind/write_test.go
@@ -30,6 +30,7 @@ import (
 )
 
 func TestWrite(t *testing.T) {
+	ctx := context.Background()
 	img, err := random.Image(1024, 1)
 	if err != nil {
 		t.Fatalf("random.Image() = %v", err)
@@ -46,7 +47,7 @@ func TestWrite(t *testing.T) {
 		return &fakeProvider{nodes: []nodes.Node{n1, n2}}
 	}
 
-	if err := Write(tag, img); err != nil {
+	if err := Write(ctx, tag, img); err != nil {
 		t.Fatalf("Write() = %v", err)
 	}
 
@@ -64,6 +65,7 @@ func TestWrite(t *testing.T) {
 }
 
 func TestTag(t *testing.T) {
+	ctx := context.Background()
 	oldTag, err := name.NewTag("kind.local/test:test")
 	if err != nil {
 		t.Fatalf("name.NewTag() = %v", err)
@@ -80,7 +82,7 @@ func TestTag(t *testing.T) {
 		return &fakeProvider{nodes: []nodes.Node{n1, n2}}
 	}
 
-	if err := Tag(oldTag, newTag); err != nil {
+	if err := Tag(ctx, oldTag, newTag); err != nil {
 		t.Fatalf("Tag() = %v", err)
 	}
 
@@ -98,6 +100,7 @@ func TestTag(t *testing.T) {
 }
 
 func TestFailWithNoNodes(t *testing.T) {
+	ctx := context.Background()
 	img, err := random.Image(1024, 1)
 	if err != nil {
 		panic(err)
@@ -117,15 +120,16 @@ func TestFailWithNoNodes(t *testing.T) {
 		return &fakeProvider{}
 	}
 
-	if err := Write(newTag, img); err == nil {
+	if err := Write(ctx, newTag, img); err == nil {
 		t.Fatal("Write() = nil, wanted an error")
 	}
-	if err := Tag(oldTag, newTag); err == nil {
+	if err := Tag(ctx, oldTag, newTag); err == nil {
 		t.Fatal("Tag() = nil, wanted an error")
 	}
 }
 
 func TestFailCommands(t *testing.T) {
+	ctx := context.Background()
 	img, err := random.Image(1024, 1)
 	if err != nil {
 		panic(err)
@@ -149,10 +153,10 @@ func TestFailCommands(t *testing.T) {
 		return &fakeProvider{nodes: []nodes.Node{n1, n2}}
 	}
 
-	if err := Write(newTag, img); !errors.Is(err, errTest) {
+	if err := Write(ctx, newTag, img); !errors.Is(err, errTest) {
 		t.Fatalf("Write() = %v, want %v", err, errTest)
 	}
-	if err := Tag(oldTag, newTag); !errors.Is(err, errTest) {
+	if err := Tag(ctx, oldTag, newTag); !errors.Is(err, errTest) {
 		t.Fatalf("Write() = %v, want %v", err, errTest)
 	}
 }
@@ -171,7 +175,7 @@ type fakeNode struct {
 	err  error
 }
 
-func (f *fakeNode) Command(cmd string, args ...string) exec.Cmd {
+func (f *fakeNode) CommandContext(ctx context.Context, cmd string, args ...string) exec.Cmd {
 	command := &fakeCmd{
 		cmd: strings.Join(append([]string{cmd}, args...), " "),
 		err: f.err,
@@ -185,10 +189,10 @@ func (f *fakeNode) String() string {
 }
 
 // The following functions are not used by our code at all.
-func (f *fakeNode) CommandContext(context.Context, string, ...string) exec.Cmd { return nil }
-func (f *fakeNode) Role() (string, error)                                      { return "", nil }
-func (f *fakeNode) IP() (ipv4 string, ipv6 string, err error)                  { return "", "", nil }
-func (f *fakeNode) SerialLogs(writer io.Writer) error                          { return nil }
+func (f *fakeNode) Command(string, ...string) exec.Cmd        { return nil }
+func (f *fakeNode) Role() (string, error)                     { return "", nil }
+func (f *fakeNode) IP() (ipv4 string, ipv6 string, err error) { return "", "", nil }
+func (f *fakeNode) SerialLogs(writer io.Writer) error         { return nil }
 
 type fakeCmd struct {
 	cmd   string