mirror of https://github.com/containers/skopeo.git
Compare commits
No commits in common. "v0.1" and "main" have entirely different histories.
|
@ -0,0 +1,259 @@
|
|||
---
|
||||
|
||||
# Main collection of env. vars to set for all tasks and scripts.
|
||||
env:
|
||||
####
|
||||
#### Global variables used for all tasks
|
||||
####
|
||||
# Name of the ultimate destination branch for this CI run, PR or post-merge.
|
||||
DEST_BRANCH: "main"
|
||||
# Overrides default location (/tmp/cirrus) for repo clone
|
||||
GOPATH: &gopath "/var/tmp/go"
|
||||
GOBIN: "${GOPATH}/bin"
|
||||
GOCACHE: "${GOPATH}/cache"
|
||||
GOSRC: &gosrc "/var/tmp/go/src/github.com/containers/skopeo"
|
||||
# Required for consistency with containers/image CI
|
||||
SKOPEO_PATH: *gosrc
|
||||
CIRRUS_WORKING_DIR: *gosrc
|
||||
# The default is 'sh' if unspecified
|
||||
CIRRUS_SHELL: "/bin/bash"
|
||||
# Save a little typing (path relative to $CIRRUS_WORKING_DIR)
|
||||
SCRIPT_BASE: "./contrib/cirrus"
|
||||
|
||||
# Google-cloud VM Images
|
||||
IMAGE_SUFFIX: "c20250721t181111z-f42f41d13"
|
||||
FEDORA_CACHE_IMAGE_NAME: "fedora-${IMAGE_SUFFIX}"
|
||||
|
||||
# Container FQIN's
|
||||
FEDORA_CONTAINER_FQIN: "quay.io/libpod/fedora_podman:${IMAGE_SUFFIX}"
|
||||
|
||||
# Built along with the standard PR-based workflow in c/automation_images
|
||||
SKOPEO_CIDEV_CONTAINER_FQIN: "quay.io/libpod/skopeo_cidev:${IMAGE_SUFFIX}"
|
||||
|
||||
|
||||
# Default timeout for each task
|
||||
timeout_in: 45m
|
||||
|
||||
|
||||
gcp_credentials: ENCRYPTED[52d9e807b531b37ab14e958cb5a72499460663f04c8d73e22ad608c027a31118420f1c80f0be0882fbdf96f49d8f9ac0]
|
||||
|
||||
|
||||
validate_task:
|
||||
# The git-validation tool doesn't work well on branch or tag push,
|
||||
# under Cirrus-CI, due to challenges obtaining the starting commit ID.
|
||||
# Only do validation for PRs.
|
||||
only_if: &is_pr $CIRRUS_PR != ''
|
||||
container:
|
||||
image: '${SKOPEO_CIDEV_CONTAINER_FQIN}'
|
||||
cpu: 4
|
||||
memory: 8
|
||||
setup_script: |
|
||||
make tools
|
||||
test_script: |
|
||||
make validate-local
|
||||
make vendor && hack/tree_status.sh
|
||||
|
||||
doccheck_task:
|
||||
only_if: *is_pr
|
||||
depends_on:
|
||||
- validate
|
||||
container:
|
||||
image: "${FEDORA_CONTAINER_FQIN}"
|
||||
cpu: 4
|
||||
memory: 8
|
||||
env:
|
||||
BUILDTAGS: &withopengpg 'containers_image_openpgp'
|
||||
script: |
|
||||
# TODO: Can't use 'runner.sh setup' inside container. However,
|
||||
# removing the pre-installed package is the only necessary step
|
||||
# at the time of this comment.
|
||||
dnf remove -y skopeo # Guarantee non-interference
|
||||
"${SKOPEO_PATH}/${SCRIPT_BASE}/runner.sh" build
|
||||
"${SKOPEO_PATH}/${SCRIPT_BASE}/runner.sh" doccheck
|
||||
|
||||
osx_task:
|
||||
# Don't run for docs-only builds.
|
||||
# Also don't run on release-branches or their PRs,
|
||||
# since base container-image is not version-constrained.
|
||||
only_if: ¬_docs_or_release_branch >-
|
||||
($CIRRUS_BASE_BRANCH == $CIRRUS_DEFAULT_BRANCH ||
|
||||
$CIRRUS_BRANCH == $CIRRUS_DEFAULT_BRANCH ) &&
|
||||
$CIRRUS_CHANGE_TITLE !=~ '.*CI:DOCS.*'
|
||||
depends_on:
|
||||
- validate
|
||||
persistent_worker: &mac_pw
|
||||
labels:
|
||||
os: darwin
|
||||
arch: arm64
|
||||
purpose: prod
|
||||
env:
|
||||
CIRRUS_WORKING_DIR: "$HOME/ci/task-${CIRRUS_TASK_ID}"
|
||||
# Prevent cache-pollution fron one task to the next.
|
||||
GOPATH: "$CIRRUS_WORKING_DIR/.go"
|
||||
GOCACHE: "$CIRRUS_WORKING_DIR/.go/cache"
|
||||
GOENV: "$CIRRUS_WORKING_DIR/.go/support"
|
||||
GOSRC: "$HOME/ci/task-${CIRRUS_TASK_ID}"
|
||||
TMPDIR: "/private/tmp/ci"
|
||||
# This host is/was shared with potentially many other CI tasks.
|
||||
# The previous task may have been canceled or aborted.
|
||||
prep_script: &mac_cleanup "contrib/cirrus/mac_cleanup.sh"
|
||||
test_script:
|
||||
- export PATH=$GOPATH/bin:$PATH
|
||||
- go version
|
||||
- go env
|
||||
- make tools
|
||||
- make validate-local test-unit-local bin/skopeo
|
||||
- bin/skopeo -v
|
||||
# This host is/was shared with potentially many other CI tasks.
|
||||
# Ensure nothing is left running while waiting for the next task.
|
||||
always:
|
||||
task_cleanup_script: *mac_cleanup
|
||||
|
||||
|
||||
cross_task:
|
||||
alias: cross
|
||||
only_if: >-
|
||||
$CIRRUS_CHANGE_TITLE !=~ '.*CI:DOCS.*'
|
||||
depends_on:
|
||||
- validate
|
||||
gce_instance: &standardvm
|
||||
image_project: libpod-218412
|
||||
zone: "us-central1-f"
|
||||
cpu: 2
|
||||
memory: "4Gb"
|
||||
# Required to be 200gig, do not modify - has i/o performance impact
|
||||
# according to gcloud CLI tool warning messages.
|
||||
disk: 200
|
||||
image_name: ${FEDORA_CACHE_IMAGE_NAME}
|
||||
env:
|
||||
BUILDTAGS: *withopengpg
|
||||
setup_script: >-
|
||||
"${GOSRC}/${SCRIPT_BASE}/runner.sh" setup
|
||||
cross_script: >-
|
||||
"${GOSRC}/${SCRIPT_BASE}/runner.sh" cross
|
||||
|
||||
|
||||
ostree-rs-ext_task:
|
||||
alias: proxy_ostree_ext
|
||||
only_if: *not_docs_or_release_branch
|
||||
# WARNING: This task potentially performs a container image
|
||||
# build (on change) with runtime package installs. Therefore,
|
||||
# its behavior can be unpredictable and potentially flake-prone.
|
||||
# In case of emergency, uncomment the next statement to bypass.
|
||||
#
|
||||
# skip: $CI == "true"
|
||||
#
|
||||
depends_on:
|
||||
- validate
|
||||
# Ref: https://cirrus-ci.org/guide/docker-builder-vm/#dockerfile-as-a-ci-environment
|
||||
container:
|
||||
# The runtime image will be rebuilt on change
|
||||
dockerfile: contrib/cirrus/ostree_ext.dockerfile
|
||||
docker_arguments: # required build-args
|
||||
BASE_FQIN: quay.io/coreos-assembler/fcos-buildroot:testing-devel
|
||||
CIRRUS_IMAGE_VERSION: 3
|
||||
env:
|
||||
EXT_REPO_NAME: ostree-rs-ext
|
||||
EXT_REPO_HOME: $CIRRUS_WORKING_DIR/../$EXT_REPO_NAME
|
||||
EXT_REPO: https://github.com/ostreedev/${EXT_REPO_NAME}.git
|
||||
skopeo_build_script:
|
||||
- dnf builddep -y skopeo
|
||||
- make
|
||||
- make install
|
||||
proxy_ostree_ext_build_script:
|
||||
- git clone --depth 1 $EXT_REPO $EXT_REPO_HOME
|
||||
- cd $EXT_REPO_HOME
|
||||
- cargo test --no-run
|
||||
proxy_ostree_ext_test_script:
|
||||
- cd $EXT_REPO_HOME
|
||||
- cargo test -- --nocapture --quiet
|
||||
|
||||
|
||||
#####
|
||||
##### NOTE: This task is subtantially duplicated in the containers/image
|
||||
##### repository's `.cirrus.yml`. Changes made here should be fully merged
|
||||
##### prior to being manually duplicated and maintained in containers/image.
|
||||
#####
|
||||
test_skopeo_task:
|
||||
alias: test_skopeo
|
||||
# Don't test for [CI:DOCS], [CI:BUILD].
|
||||
only_if: >-
|
||||
$CIRRUS_CHANGE_TITLE !=~ '.*CI:BUILD.*' &&
|
||||
$CIRRUS_CHANGE_TITLE !=~ '.*CI:DOCS.*'
|
||||
depends_on:
|
||||
- validate
|
||||
gce_instance:
|
||||
image_project: libpod-218412
|
||||
zone: "us-central1-f"
|
||||
cpu: 2
|
||||
memory: "4Gb"
|
||||
# Required to be 200gig, do not modify - has i/o performance impact
|
||||
# according to gcloud CLI tool warning messages.
|
||||
disk: 200
|
||||
image_name: ${FEDORA_CACHE_IMAGE_NAME}
|
||||
matrix:
|
||||
- name: "Skopeo Test" # N/B: Name ref. by hack/get_fqin.sh
|
||||
env:
|
||||
BUILDTAGS: ''
|
||||
- name: "Skopeo Test w/ opengpg"
|
||||
env:
|
||||
BUILDTAGS: *withopengpg
|
||||
setup_script: >-
|
||||
"${GOSRC}/${SCRIPT_BASE}/runner.sh" setup
|
||||
vendor_script: >-
|
||||
"${SKOPEO_PATH}/${SCRIPT_BASE}/runner.sh" vendor
|
||||
build_script: >-
|
||||
"${SKOPEO_PATH}/${SCRIPT_BASE}/runner.sh" build
|
||||
unit_script: >-
|
||||
"${SKOPEO_PATH}/${SCRIPT_BASE}/runner.sh" unit
|
||||
integration_script: >-
|
||||
"${SKOPEO_PATH}/${SCRIPT_BASE}/runner.sh" integration
|
||||
system_script: >
|
||||
"${SKOPEO_PATH}/${SCRIPT_BASE}/runner.sh" system
|
||||
|
||||
|
||||
# This task is critical. It updates the "last-used by" timestamp stored
|
||||
# in metadata for all VM images. This mechanism functions in tandem with
|
||||
# an out-of-band pruning operation to remove disused VM images.
|
||||
meta_task:
|
||||
name: "VM img. keepalive"
|
||||
alias: meta
|
||||
container: &smallcontainer
|
||||
cpu: 2
|
||||
memory: 2
|
||||
image: quay.io/libpod/imgts:latest
|
||||
env:
|
||||
# Space-separated list of images used by this repository state
|
||||
IMGNAMES: |
|
||||
${FEDORA_CACHE_IMAGE_NAME}
|
||||
build-push-${IMAGE_SUFFIX}
|
||||
BUILDID: "${CIRRUS_BUILD_ID}"
|
||||
REPOREF: "${CIRRUS_REPO_NAME}"
|
||||
GCPJSON: ENCRYPTED[6867b5a83e960e7c159a98fe6c8360064567a071c6f4b5e7d532283ecd870aa65c94ccd74bdaa9bf7aadac9d42e20a67]
|
||||
GCPNAME: ENCRYPTED[1cf558ae125e3c39ec401e443ad76452b25d790c45eb73d77c83eb059a0f7fd5085ef7e2f7e410b04ea6e83b0aab2eb1]
|
||||
GCPPROJECT: libpod-218412
|
||||
clone_script: &noop mkdir -p "$CIRRUS_WORKING_DIR"
|
||||
script: /usr/local/bin/entrypoint.sh
|
||||
|
||||
|
||||
# Status aggregator for all tests. This task simply ensures a defined
|
||||
# set of tasks all passed, and allows confirming that based on the status
|
||||
# of this task.
|
||||
success_task:
|
||||
name: "Total Success"
|
||||
alias: success
|
||||
# N/B: ALL tasks must be listed here, minus their '_task' suffix.
|
||||
depends_on:
|
||||
- validate
|
||||
- doccheck
|
||||
- osx
|
||||
- cross
|
||||
- proxy_ostree_ext
|
||||
- test_skopeo
|
||||
- meta
|
||||
container: *smallcontainer
|
||||
env:
|
||||
CTR_FQIN: ${FEDORA_CONTAINER_FQIN}
|
||||
TEST_ENVIRON: container
|
||||
clone_script: *noop
|
||||
script: /bin/true
|
|
@ -0,0 +1 @@
|
|||
1
|
|
@ -0,0 +1,52 @@
|
|||
/*
|
||||
Renovate is a service similar to GitHub Dependabot, but with
|
||||
(fantastically) more configuration options. So many options
|
||||
in fact, if you're new I recommend glossing over this cheat-sheet
|
||||
prior to the official documentation:
|
||||
|
||||
https://www.augmentedmind.de/2021/07/25/renovate-bot-cheat-sheet
|
||||
|
||||
Configuration Update/Change Procedure:
|
||||
1. Make changes
|
||||
2. Manually validate changes (from repo-root):
|
||||
|
||||
podman run -it \
|
||||
-v ./.github/renovate.json5:/usr/src/app/renovate.json5:z \
|
||||
docker.io/renovate/renovate:latest \
|
||||
renovate-config-validator
|
||||
3. Commit.
|
||||
|
||||
Configuration Reference:
|
||||
https://docs.renovatebot.com/configuration-options/
|
||||
|
||||
Monitoring Dashboard:
|
||||
https://app.renovatebot.com/dashboard#github/containers
|
||||
|
||||
Note: The Renovate bot will create/manage it's business on
|
||||
branches named 'renovate/*'. Otherwise, and by
|
||||
default, the only the copy of this file that matters
|
||||
is the one on the `main` branch. No other branches
|
||||
will be monitored or touched in any way.
|
||||
*/
|
||||
|
||||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
|
||||
/*************************************************
|
||||
****** Global/general configuration options *****
|
||||
*************************************************/
|
||||
|
||||
// Re-use predefined sets of configuration options to DRY
|
||||
"extends": [
|
||||
// https://github.com/containers/automation/blob/main/renovate/defaults.json5
|
||||
"github>containers/automation//renovate/defaults.json5"
|
||||
],
|
||||
|
||||
// Permit automatic rebasing when base-branch changes by more than
|
||||
// one commit.
|
||||
"rebaseWhen": "behind-base-branch",
|
||||
|
||||
/*************************************************
|
||||
*** Repository-specific configuration options ***
|
||||
*************************************************/
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
---
|
||||
|
||||
# See also:
|
||||
# https://github.com/containers/podman/blob/main/.github/workflows/check_cirrus_cron.yml
|
||||
|
||||
on:
|
||||
# Note: This only applies to the default branch.
|
||||
schedule:
|
||||
# N/B: This should correspond to a period slightly after
|
||||
# the last job finishes running. See job defs. at:
|
||||
# https://cirrus-ci.com/settings/repository/6706677464432640
|
||||
- cron: '03 03 * * 1-5'
|
||||
# Debug: Allow triggering job manually in github-actions WebUI
|
||||
workflow_dispatch: {}
|
||||
|
||||
jobs:
|
||||
# Ref: https://docs.github.com/en/actions/using-workflows/reusing-workflows
|
||||
call_cron_failures:
|
||||
uses: containers/podman/.github/workflows/check_cirrus_cron.yml@main
|
||||
secrets:
|
||||
SECRET_CIRRUS_API_KEY: ${{secrets.SECRET_CIRRUS_API_KEY}}
|
||||
ACTION_MAIL_SERVER: ${{secrets.ACTION_MAIL_SERVER}}
|
||||
ACTION_MAIL_USERNAME: ${{secrets.ACTION_MAIL_USERNAME}}
|
||||
ACTION_MAIL_PASSWORD: ${{secrets.ACTION_MAIL_PASSWORD}}
|
||||
ACTION_MAIL_SENDER: ${{secrets.ACTION_MAIL_SENDER}}
|
|
@ -0,0 +1,20 @@
|
|||
---
|
||||
|
||||
# See also:
|
||||
# https://github.com/containers/podman/blob/main/.github/workflows/issue_pr_lock.yml
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 0 * * *'
|
||||
# Debug: Allow triggering job manually in github-actions WebUI
|
||||
workflow_dispatch: {}
|
||||
|
||||
jobs:
|
||||
# Ref: https://docs.github.com/en/actions/using-workflows/reusing-workflows
|
||||
closed_issue_discussion_lock:
|
||||
uses: containers/podman/.github/workflows/issue_pr_lock.yml@main
|
||||
secrets: inherit
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write
|
||||
pull-requests: write
|
|
@ -0,0 +1,29 @@
|
|||
name: Mark stale issues and pull requests
|
||||
|
||||
# Please refer to https://github.com/actions/stale/blob/master/action.yml
|
||||
# to see all config knobs of the stale action.
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 0 * * *"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
stale:
|
||||
permissions:
|
||||
issues: write # for actions/stale to close stale issues
|
||||
pull-requests: write # for actions/stale to close stale PRs
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/stale@v9
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
stale-issue-message: 'A friendly reminder that this issue had no activity for 30 days.'
|
||||
stale-pr-message: 'A friendly reminder that this PR had no activity for 30 days.'
|
||||
stale-issue-label: 'stale-issue'
|
||||
stale-pr-label: 'stale-pr'
|
||||
days-before-stale: 30
|
||||
days-before-close: 365
|
||||
remove-stale-when-updated: true
|
|
@ -0,0 +1,10 @@
|
|||
*.1
|
||||
/layers-*
|
||||
/skopeo
|
||||
result
|
||||
/completions/
|
||||
# ignore JetBrains IDEs (GoLand) config folder
|
||||
.idea
|
||||
|
||||
# Ignore the bin directory
|
||||
bin
|
|
@ -0,0 +1,13 @@
|
|||
version: "2"
|
||||
linters:
|
||||
settings:
|
||||
staticcheck:
|
||||
checks:
|
||||
# Compared to golangci-lint v2.0.2 defaults, we don’t exclude
|
||||
# ST1003, ST1016, ST1020, ST1021, ST1022 as we don't hit those.
|
||||
- all
|
||||
- -ST1000 # Incorrect or missing package comment.
|
||||
- -ST1005 # Incorrectly formatted error string.
|
||||
exclusions:
|
||||
presets:
|
||||
- std-error-handling
|
|
@ -0,0 +1,154 @@
|
|||
---
|
||||
# See the documentation for more information:
|
||||
# https://packit.dev/docs/configuration/
|
||||
|
||||
# NOTE: The Packit copr_build tasks help to check if every commit builds on
|
||||
# supported Fedora and CentOS Stream arches.
|
||||
# They do not block the current Cirrus-based workflow.
|
||||
|
||||
downstream_package_name: skopeo
|
||||
upstream_tag_template: v{version}
|
||||
|
||||
# These files get synced from upstream to downstream (Fedora / CentOS Stream) on every
|
||||
# propose-downstream job. This is done so tests maintained upstream can be run
|
||||
# downstream in Zuul CI and Bodhi.
|
||||
# Ref: https://packit.dev/docs/configuration#files_to_sync
|
||||
files_to_sync:
|
||||
- src: rpm/gating.yaml
|
||||
dest: gating.yaml
|
||||
delete: true
|
||||
- src: plans/
|
||||
dest: plans/
|
||||
delete: true
|
||||
mkpath: true
|
||||
- src: systemtest/tmt/
|
||||
dest: test/tmt/
|
||||
delete: true
|
||||
mkpath: true
|
||||
- src: .fmf/
|
||||
dest: .fmf/
|
||||
delete: true
|
||||
- .packit.yaml
|
||||
|
||||
packages:
|
||||
skopeo-fedora:
|
||||
pkg_tool: fedpkg
|
||||
specfile_path: rpm/skopeo.spec
|
||||
skopeo-centos:
|
||||
pkg_tool: centpkg
|
||||
specfile_path: rpm/skopeo.spec
|
||||
skopeo-eln:
|
||||
specfile_path: rpm/skopeo.spec
|
||||
|
||||
srpm_build_deps:
|
||||
- make
|
||||
|
||||
jobs:
|
||||
- job: copr_build
|
||||
trigger: pull_request
|
||||
packages: [skopeo-fedora]
|
||||
notifications: &copr_build_failure_notification
|
||||
failure_comment:
|
||||
message: "Ephemeral COPR build failed. @containers/packit-build please check."
|
||||
targets: &fedora_copr_targets
|
||||
- fedora-all-x86_64
|
||||
- fedora-all-aarch64
|
||||
enable_net: true
|
||||
# Re-enable these scans if OpenScanHub starts scanning go packages
|
||||
# https://packit.dev/posts/openscanhub-prototype
|
||||
osh_diff_scan_after_copr_build: false
|
||||
|
||||
# Ignore until golang is updated in distro buildroot to go 1.23.3+
|
||||
- job: copr_build
|
||||
trigger: ignore
|
||||
packages: [skopeo-eln]
|
||||
notifications: *copr_build_failure_notification
|
||||
targets:
|
||||
fedora-eln-x86_64:
|
||||
additional_repos:
|
||||
- "https://kojipkgs.fedoraproject.org/repos/eln-build/latest/x86_64/"
|
||||
fedora-eln-aarch64:
|
||||
additional_repos:
|
||||
- "https://kojipkgs.fedoraproject.org/repos/eln-build/latest/aarch64/"
|
||||
enable_net: true
|
||||
|
||||
# Ignore until golang is updated in distro buildroot to go 1.23.3+
|
||||
- job: copr_build
|
||||
trigger: ignore
|
||||
packages: [skopeo-centos]
|
||||
notifications: *copr_build_failure_notification
|
||||
targets: ¢os_copr_targets
|
||||
- centos-stream-9-x86_64
|
||||
- centos-stream-9-aarch64
|
||||
- centos-stream-10-x86_64
|
||||
- centos-stream-10-aarch64
|
||||
enable_net: true
|
||||
|
||||
# Run on commit to main branch
|
||||
- job: copr_build
|
||||
trigger: commit
|
||||
packages: [skopeo-fedora]
|
||||
notifications:
|
||||
failure_comment:
|
||||
message: "podman-next COPR build failed. @containers/packit-build please check."
|
||||
branch: main
|
||||
owner: rhcontainerbot
|
||||
project: podman-next
|
||||
enable_net: true
|
||||
|
||||
# Tests on Fedora for main branch
|
||||
- job: tests
|
||||
trigger: pull_request
|
||||
packages: [skopeo-fedora]
|
||||
notifications: &test_failure_notification
|
||||
failure_comment:
|
||||
message: "Tests failed. @containers/packit-build please check."
|
||||
targets: *fedora_copr_targets
|
||||
tf_extra_params:
|
||||
environments:
|
||||
- artifacts:
|
||||
- type: repository-file
|
||||
id: https://copr.fedorainfracloud.org/coprs/rhcontainerbot/podman-next/repo/fedora-$releasever/rhcontainerbot-podman-next-fedora-$releasever.repo
|
||||
|
||||
# Tests on CentOS Stream for main branch
|
||||
# Ignore until golang is updated in distro buildroot to go 1.23.3+
|
||||
- job: tests
|
||||
trigger: ignore
|
||||
packages: [skopeo-centos]
|
||||
notifications: *test_failure_notification
|
||||
targets: *centos_copr_targets
|
||||
tf_extra_params:
|
||||
environments:
|
||||
- artifacts:
|
||||
- type: repository-file
|
||||
id: https://copr.fedorainfracloud.org/coprs/rhcontainerbot/podman-next/repo/centos-stream-$releasever/rhcontainerbot-podman-next-centos-stream-$releasever.repo
|
||||
|
||||
# Sync to Fedora
|
||||
- job: propose_downstream
|
||||
trigger: release
|
||||
packages: [skopeo-fedora]
|
||||
update_release: false
|
||||
dist_git_branches: &fedora_targets
|
||||
- fedora-all
|
||||
|
||||
# Sync to CentOS Stream
|
||||
# FIXME: Switch trigger whenever we're ready to update CentOS Stream via
|
||||
# Packit
|
||||
- job: propose_downstream
|
||||
trigger: ignore
|
||||
packages: [skopeo-centos]
|
||||
update_release: false
|
||||
dist_git_branches:
|
||||
- c10s
|
||||
|
||||
# Fedora Koji build
|
||||
- job: koji_build
|
||||
trigger: commit
|
||||
packages: [skopeo-fedora]
|
||||
sidetag_group: podman-releases
|
||||
# Dependents are not rpm dependencies, but the package whose bodhi update
|
||||
# should include this package.
|
||||
# Ref: https://packit.dev/docs/fedora-releases-guide/releasing-multiple-packages
|
||||
dependents:
|
||||
- podman
|
||||
dist_git_branches: *fedora_targets
|
|
@ -1,28 +0,0 @@
|
|||
#!/bin/bash
|
||||
set -xeuo pipefail
|
||||
|
||||
export GOPATH=$HOME/gopath
|
||||
export PATH=$HOME/gopath/bin:$PATH
|
||||
export GOSRC=$HOME/gopath/src/github.com/projectatomic/buildah
|
||||
|
||||
(mkdir -p $GOSRC && cd /code && cp -r . $GOSRC)
|
||||
|
||||
dnf install -y \
|
||||
bats \
|
||||
btrfs-progs-devel \
|
||||
bzip2 \
|
||||
device-mapper-devel \
|
||||
findutils \
|
||||
git \
|
||||
golang \
|
||||
gpgme-devel \
|
||||
libassuan-devel \
|
||||
make \
|
||||
which
|
||||
|
||||
# Red Hat CI adds a merge commit, for testing, which fails the
|
||||
# short-commit-subject validation test, so tell git-validate.sh to only check
|
||||
# up to, but not including, the merge commit.
|
||||
export GITVALIDATE_TIP=$(cd $GOSRC; git log -2 --pretty='%H' | tail -n 1)
|
||||
make -C $GOSRC install.tools all validate
|
||||
$GOSRC/tests/test_runner.sh
|
|
@ -1,12 +0,0 @@
|
|||
branches:
|
||||
- master
|
||||
- auto
|
||||
- try
|
||||
|
||||
host:
|
||||
distro: fedora/25/atomic
|
||||
|
||||
required: true
|
||||
|
||||
tests:
|
||||
- docker run --privileged -v $PWD:/code fedora:25 /code/.redhat-ci.sh
|
14
.travis.yml
14
.travis.yml
|
@ -1,14 +0,0 @@
|
|||
language: go
|
||||
go:
|
||||
- 1.7
|
||||
- 1.8
|
||||
- tip
|
||||
dist: trusty
|
||||
sudo: required
|
||||
before_install:
|
||||
- sudo add-apt-repository -y ppa:duggan/bats
|
||||
- sudo apt-get -qq update
|
||||
- sudo apt-get -qq install bats btrfs-tools git libdevmapper-dev libgpgme11-dev
|
||||
script:
|
||||
- make install.tools all validate
|
||||
- cd tests; sudo PATH="$PATH" ./test_runner.sh
|
|
@ -0,0 +1,3 @@
|
|||
## The skopeo Project Community Code of Conduct
|
||||
|
||||
The skopeo project, as part of Podman Container Tools, follows the [CNCF Code of Conduct](https://github.com/cncf/foundation/blob/main/code-of-conduct.md).
|
|
@ -0,0 +1,179 @@
|
|||
# Contributing to Skopeo
|
||||
|
||||
We'd love to have you join the community! Below summarizes the processes
|
||||
that we follow.
|
||||
|
||||
## Topics
|
||||
|
||||
* [Reporting Issues](#reporting-issues)
|
||||
* [Submitting Pull Requests](#submitting-pull-requests)
|
||||
* [Communications](#communications)
|
||||
<!--
|
||||
* [Becoming a Maintainer](#becoming-a-maintainer)
|
||||
-->
|
||||
|
||||
## Reporting Issues
|
||||
|
||||
Before reporting an issue, check our backlog of
|
||||
[open issues](https://github.com/containers/skopeo/issues)
|
||||
to see if someone else has already reported it. If so, feel free to add
|
||||
your scenario, or additional information, to the discussion. Or simply
|
||||
"subscribe" to it to be notified when it is updated.
|
||||
|
||||
If you find a new issue with the project we'd love to hear about it! The most
|
||||
important aspect of a bug report is that it includes enough information for
|
||||
us to reproduce it. So, please include as much detail as possible and try
|
||||
to remove the extra stuff that doesn't really relate to the issue itself.
|
||||
The easier it is for us to reproduce it, the faster it'll be fixed!
|
||||
|
||||
Please don't include any private/sensitive information in your issue!
|
||||
|
||||
## Submitting Pull Requests
|
||||
|
||||
No Pull Request (PR) is too small! Typos, additional comments in the code,
|
||||
new testcases, bug fixes, new features, more documentation, ... it's all
|
||||
welcome!
|
||||
|
||||
While bug fixes can first be identified via an "issue", that is not required.
|
||||
It's ok to just open up a PR with the fix, but make sure you include the same
|
||||
information you would have included in an issue - like how to reproduce it.
|
||||
|
||||
PRs for new features should include some background on what use cases the
|
||||
new code is trying to address. When possible and when it makes sense, try to break-up
|
||||
larger PRs into smaller ones - it's easier to review smaller
|
||||
code changes. But only if those smaller ones make sense as stand-alone PRs.
|
||||
|
||||
Regardless of the type of PR, all PRs should include:
|
||||
* well documented code changes
|
||||
* additional testcases. Ideally, they should fail w/o your code change applied
|
||||
* documentation changes
|
||||
|
||||
Squash your commits into logical pieces of work that might want to be reviewed
|
||||
separate from the rest of the PRs. Ideally, each commit should implement a single
|
||||
idea, and the PR branch should pass the tests at every commit. GitHub makes it easy
|
||||
to review the cumulative effect of many commits; so, when in doubt, use smaller commits.
|
||||
|
||||
PRs that fix issues should include a reference like `Closes #XXXX` in the
|
||||
commit message so that github will automatically close the referenced issue
|
||||
when the PR is merged.
|
||||
|
||||
<!--
|
||||
All PRs require at least two LGTMs (Looks Good To Me) from maintainers.
|
||||
-->
|
||||
|
||||
### Sign your PRs
|
||||
|
||||
The sign-off is a line at the end of the explanation for the patch. Your
|
||||
signature certifies that you wrote the patch or otherwise have the right to pass
|
||||
it on as an open-source patch. The rules are simple: if you can certify
|
||||
the below (from [developercertificate.org](http://developercertificate.org/)):
|
||||
|
||||
```
|
||||
Developer Certificate of Origin
|
||||
Version 1.1
|
||||
|
||||
Copyright (C) 2004, 2006 The Linux Foundation and its contributors.
|
||||
660 York Street, Suite 102,
|
||||
San Francisco, CA 94110 USA
|
||||
|
||||
Everyone is permitted to copy and distribute verbatim copies of this
|
||||
license document, but changing it is not allowed.
|
||||
|
||||
Developer's Certificate of Origin 1.1
|
||||
|
||||
By making a contribution to this project, I certify that:
|
||||
|
||||
(a) The contribution was created in whole or in part by me and I
|
||||
have the right to submit it under the open source license
|
||||
indicated in the file; or
|
||||
|
||||
(b) The contribution is based upon previous work that, to the best
|
||||
of my knowledge, is covered under an appropriate open source
|
||||
license and I have the right under that license to submit that
|
||||
work with modifications, whether created in whole or in part
|
||||
by me, under the same open source license (unless I am
|
||||
permitted to submit under a different license), as indicated
|
||||
in the file; or
|
||||
|
||||
(c) The contribution was provided directly to me by some other
|
||||
person who certified (a), (b) or (c) and I have not modified
|
||||
it.
|
||||
|
||||
(d) I understand and agree that this project and the contribution
|
||||
are public and that a record of the contribution (including all
|
||||
personal information I submit with it, including my sign-off) is
|
||||
maintained indefinitely and may be redistributed consistent with
|
||||
this project or the open source license(s) involved.
|
||||
```
|
||||
|
||||
Then you just add a line to every git commit message:
|
||||
|
||||
Signed-off-by: Joe Smith <joe.smith@email.com>
|
||||
|
||||
Use your real name (sorry, no pseudonyms or anonymous contributions.)
|
||||
|
||||
If you set your `user.name` and `user.email` git configs, you can sign your
|
||||
commit automatically with `git commit -s`.
|
||||
|
||||
### Dependencies management
|
||||
|
||||
Dependencies are managed via [standard go modules](https://golang.org/ref/mod).
|
||||
|
||||
In order to add a new dependency to this project:
|
||||
|
||||
- use `go get -d path/to/dep@version` to add a new line to `go.mod`
|
||||
- run `make vendor`
|
||||
|
||||
In order to update an existing dependency:
|
||||
|
||||
- use `go get -d -u path/to/dep@version` to update the relevant dependency line in `go.mod`
|
||||
- run `make vendor`
|
||||
|
||||
When new PRs for [containers/image](https://github.com/containers/image) break `skopeo` (i.e. `containers/image` tests fail in `make test-skopeo`):
|
||||
|
||||
- create out a new branch in your `skopeo` checkout and switch to it
|
||||
- find out the version of `containers/image` you want to use and note its commit ID. You might also want to use a fork of `containers/image`, in that case note its repo
|
||||
- use `go get -d github.com/$REPO/image/v5@$COMMIT_ID` to download the right version. The command will fetch the dependency and then fail because of a conflict in `go.mod`, this is expected. Note the pseudo-version (eg. `v5.13.1-0.20210707123201-50afbf0a326`)
|
||||
- use `go mod edit -replace=github.com/containers/image/v5=github.com/$REPO/image/v5@$PSEUDO_VERSION` to add a replacement line to `go.mod` (e.g. `replace github.com/containers/image/v5 => github.com/moio/image/v5 v5.13.1-0.20210707123201-50afbf0a3262`)
|
||||
- run `make vendor`
|
||||
- make any other necessary changes in the skopeo repo (e.g. add other dependencies now required by `containers/image`, or update skopeo for changed `containers/image` API)
|
||||
- optionally add new integration tests to the skopeo repo
|
||||
- submit the resulting branch as a skopeo PR, marked “DO NOT MERGE”
|
||||
- iterate until tests pass and the PR is reviewed
|
||||
- then the original `containers/image` PR can be merged, disregarding its `make test-skopeo` failure
|
||||
- as soon as possible after that, in the skopeo PR, use `go mod edit -dropreplace=github.com/containers/image` to remove the `replace` line in `go.mod`
|
||||
- run `make vendor`
|
||||
- update the skopeo PR with the result, drop the “DO NOT MERGE” marking
|
||||
- after tests complete successfully again, merge the skopeo PR
|
||||
|
||||
## Communications
|
||||
|
||||
For general questions, or discussions, please use the
|
||||
[#podman](https://app.slack.com/client/T08PSQ7BQ/C08MXJLCFCN) channel on the [CNCF
|
||||
Slack](https://cloud-native.slack.com).
|
||||
|
||||
For development related discussions, please use the
|
||||
[#podman-dev](https://app.slack.com/client/T08PSQ7BQ/C08NTKCDC1W) channel on the CNCF
|
||||
Slack.
|
||||
|
||||
For discussions around issues/bugs and features, you can use the github
|
||||
[issues](https://github.com/containers/skopeo/issues)
|
||||
and
|
||||
[PRs](https://github.com/containers/skopeo/pulls)
|
||||
tracking system.
|
||||
|
||||
<!--
|
||||
## Becoming a Maintainer
|
||||
|
||||
To become a maintainer you must first be nominated by an existing maintainer.
|
||||
If a majority (>50%) of maintainers agree then the proposal is adopted and
|
||||
you will be added to the list.
|
||||
|
||||
Removing a maintainer requires at least 75% of the remaining maintainers
|
||||
approval, or if the person requests to be removed then it is automatic.
|
||||
Normally, a maintainer will only be removed if they are considered to be
|
||||
inactive for a long period of time or are viewed as disruptive to the community.
|
||||
|
||||
The current list of maintainers can be found in the
|
||||
[MAINTAINERS](MAINTAINERS) file.
|
||||
-->
|
|
@ -0,0 +1,12 @@
|
|||
## The Skopeo Project Community Governance
|
||||
|
||||
The Skopeo project, as part of Podman Container Tools, follows the [Podman Project Governance](https://github.com/containers/podman/blob/main/GOVERNANCE.md)
|
||||
except sections found in this document, which override those found in Podman's Governance.
|
||||
|
||||
---
|
||||
|
||||
# Maintainers File
|
||||
|
||||
The definitive source of truth for maintainers of this repository is the local [MAINTAINERS.md](./MAINTAINERS.md) file. The [MAINTAINERS.md](https://github.com/containers/podman/blob/main/MAINTAINERS.md) file in the main Podman repository is used for project-spanning roles, including Core Maintainer and Community Manager. Some repositories in the project will also have a local [OWNERS](./OWNERS) file, which the CI system uses to map users to roles. Any changes to the [OWNERS](./OWNERS) file must make a corresponding change to the [MAINTAINERS.md](./MAINTAINERS.md) file to ensure that the file remains up to date. Most changes to [MAINTAINERS.md](./MAINTAINERS.md) will require a change to the repository’s [OWNERS](.OWNERS) file (e.g., adding a Reviewer), but some will not (e.g., promoting a Maintainer to a Core Maintainer, which comes with no additional CI-related privileges).
|
||||
|
||||
Any Core Maintainers listed in Podman’s [MAINTAINERS.md](https://github.com/containers/podman/blob/main/MAINTAINERS.md) file should also be added to the list of “approvers” in the local [OWNERS](./OWNERS) file and as a Core Maintainer in the list of “Maintainers” in the local [MAINTAINERS.md](./MAINTAINERS.md) file.
|
18
LICENSE
18
LICENSE
|
@ -1,6 +1,7 @@
|
|||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
https://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
|
@ -175,24 +176,11 @@
|
|||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "{}"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright {yyyy} {name of copyright owner}
|
||||
|
||||
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
|
||||
https://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,
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
# Skopeo Maintainers
|
||||
|
||||
[GOVERNANCE.md](https://github.com/containers/podman/blob/main/GOVERNANCE.md)
|
||||
describes the project's governance and the Project Roles used below.
|
||||
|
||||
## Maintainers
|
||||
|
||||
| Maintainer | GitHub ID | Project Roles | Affiliation |
|
||||
|-------------------|----------------------------------------------------------|----------------------------------|----------------------------------------------|
|
||||
| Brent Baude | [baude](https://github.com/baude) | Core Maintainer | [Red Hat](https://github.com/RedHatOfficial) |
|
||||
| Nalin Dahyabhai | [nalind](https://github.com/nalind) | Core Maintainer | [Red Hat](https://github.com/RedHatOfficial) |
|
||||
| Matthew Heon | [mheon](https://github.com/mheon) | Core Maintainer | [Red Hat](https://github.com/RedHatOfficial) |
|
||||
| Paul Holzinger | [Luap99](https://github.com/Luap99) | Core Maintainer | [Red Hat](https://github.com/RedHatOfficial) |
|
||||
| Giuseppe Scrivano | [giuseppe](https://github.com/giuseppe) | Core Maintainer | [Red Hat](https://github.com/RedHatOfficial) |
|
||||
| Miloslav Trmač | [mtrmac](https://github.com/mtrmac) | Core Maintainer | [Red Hat](https://github.com/RedHatOfficial) |
|
||||
| Mohan Boddu | [mohanboddu](https://github.com/mohanboddu) | Community Manager | [Red Hat](https://github.com/RedHatOfficial) |
|
||||
| Neil Smith | [actionmancan](https://github.com/actionmancan) | Community Manager | [Red Hat](https://github.com/RedHatOfficial) |
|
||||
| Tom Sweeney | [TomSweeneyRedHat](https://github.com/TomSweeneyRedHat/) | Maintainer and Community Manager | [Red Hat](https://github.com/RedHatOfficial) |
|
||||
| Lokesh Mandvekar | [lsm5](https://github.com/lsm5) | Maintainer | [Red Hat](https://github.com/RedHatOfficial) |
|
||||
| Dan Walsh | [rhatdan](https://github.com/rhatdan) | Maintainer | [Red Hat](https://github.com/RedHatOfficial) |
|
||||
| Ashley Cui | [ashley-cui](https://github.com/ashley-cui) | Reviewer | [Red Hat](https://github.com/RedHatOfficial) |
|
||||
| Valentin Rothberg | [vrothberg](https://github.com/vrothberg) | Reviewer | [Red Hat](https://github.com/RedHatOfficial) |
|
||||
| Colin Walters | [cgwalters](https://github.com/cgwalters) | Reviewer | [Red Hat](https://github.com/RedHatOfficial) |
|
||||
|
||||
## Alumni
|
||||
|
||||
None at present
|
||||
|
||||
## Credits
|
||||
|
||||
The structure of this document was based off of the equivalent one in the [CRI-O Project](https://github.com/cri-o/cri-o/blob/main/MAINTAINERS.md).
|
||||
|
||||
## Note
|
||||
|
||||
If there is a discrepancy between the [MAINTAINERS.md](https://github.com/containers/podman/blob/main/MAINTAINERS.md) file in the main Podman repository and this file regarding Core Maintainers or Community Managers, the file in the Podman Repository is considered the source of truth.
|
296
Makefile
296
Makefile
|
@ -1,53 +1,269 @@
|
|||
AUTOTAGS := $(shell ./btrfs_tag.sh) $(shell ./libdm_tag.sh)
|
||||
PREFIX := /usr/local
|
||||
BINDIR := $(PREFIX)/bin
|
||||
.PHONY: all binary docs docs-in-container build-local clean install install-binary install-completions shell test-integration .install.vndr vendor vendor-in-container
|
||||
|
||||
export GOPROXY=https://proxy.golang.org
|
||||
|
||||
# The following variables very roughly follow https://www.gnu.org/prep/standards/standards.html#Makefile-Conventions .
|
||||
DESTDIR ?=
|
||||
PREFIX ?= /usr/local
|
||||
ifeq ($(shell uname -s),FreeBSD)
|
||||
CONTAINERSCONFDIR ?= /usr/local/etc/containers
|
||||
else
|
||||
CONTAINERSCONFDIR ?= /etc/containers
|
||||
endif
|
||||
REGISTRIESDDIR ?= ${CONTAINERSCONFDIR}/registries.d
|
||||
LOOKASIDEDIR ?= /var/lib/containers/sigstore
|
||||
BINDIR ?= ${PREFIX}/bin
|
||||
MANDIR ?= ${PREFIX}/share/man
|
||||
|
||||
BASHINSTALLDIR=${PREFIX}/share/bash-completion/completions
|
||||
BUILDFLAGS := -tags "$(AUTOTAGS) $(TAGS)"
|
||||
ZSHINSTALLDIR=${PREFIX}/share/zsh/site-functions
|
||||
FISHINSTALLDIR=${PREFIX}/share/fish/vendor_completions.d
|
||||
|
||||
all: buildah docs
|
||||
GO ?= go
|
||||
GOBIN := $(shell $(GO) env GOBIN)
|
||||
GOOS ?= $(shell go env GOOS)
|
||||
GOARCH ?= $(shell go env GOARCH)
|
||||
|
||||
buildah: *.go imagebuildah/*.go cmd/buildah/*.go docker/*.go util/*.go
|
||||
go build -o buildah $(BUILDFLAGS) ./cmd/buildah
|
||||
# N/B: This value is managed by Renovate, manual changes are
|
||||
# possible, as long as they don't disturb the formatting
|
||||
# (i.e. DO NOT ADD A 'v' prefix!)
|
||||
GOLANGCI_LINT_VERSION := 2.4.0
|
||||
|
||||
ifeq ($(GOBIN),)
|
||||
GOBIN := $(GOPATH)/bin
|
||||
endif
|
||||
|
||||
# Scripts may also use CONTAINER_RUNTIME, so we need to export it.
|
||||
# Note possibly non-obvious aspects of this:
|
||||
# - We need to use 'command -v' here, not 'which', for compatibility with MacOS.
|
||||
# - GNU Make 4.2.1 (included in Ubuntu 20.04) incorrectly tries to avoid invoking
|
||||
# a shell, and fails because there is no /usr/bin/command. The trailing ';' in
|
||||
# $(shell … ;) defeats that heuristic (recommended in
|
||||
# https://savannah.gnu.org/bugs/index.php?57625 ).
|
||||
export CONTAINER_RUNTIME ?= $(if $(shell command -v podman ;),podman,docker)
|
||||
GOMD2MAN ?= $(if $(shell command -v go-md2man ;),go-md2man,$(GOBIN)/go-md2man)
|
||||
|
||||
ifeq ($(DEBUG), 1)
|
||||
override GOGCFLAGS += -N -l
|
||||
endif
|
||||
|
||||
ifeq ($(GOOS), linux)
|
||||
ifneq ($(GOARCH),$(filter $(GOARCH),mips mipsle mips64 mips64le ppc64 riscv64))
|
||||
GO_DYN_FLAGS="-buildmode=pie"
|
||||
endif
|
||||
endif
|
||||
|
||||
# If $TESTFLAGS is set, it is passed as extra arguments to 'go test' on integration tests.
|
||||
# You can select certain tests to run, with `-run <regex>` for example:
|
||||
#
|
||||
# make test-integration TESTFLAGS='-run copySuite.TestCopy.*'
|
||||
export TESTFLAGS ?= -timeout=15m
|
||||
|
||||
# This is assumed to be set non-empty when operating inside a CI/automation environment
|
||||
CI ?=
|
||||
|
||||
# This env. var. is interpreted by some tests as a permission to
|
||||
# modify local configuration files and services.
|
||||
export SKOPEO_CONTAINER_TESTS ?= $(if $(CI),1,0)
|
||||
|
||||
# This is a compromise, we either use a container for this or require
|
||||
# the local user to have a compatible python3 development environment.
|
||||
# Define it as a "resolve on use" variable to avoid calling out when possible
|
||||
SKOPEO_CIDEV_CONTAINER_FQIN ?= $(shell hack/get_fqin.sh)
|
||||
CONTAINER_CMD ?= ${CONTAINER_RUNTIME} run --rm -i -e TESTFLAGS="$(TESTFLAGS)" -e CI=$(CI) -e SKOPEO_CONTAINER_TESTS=1
|
||||
# if this session isn't interactive, then we don't want to allocate a
|
||||
# TTY, which would fail, but if it is interactive, we do want to attach
|
||||
# so that the user can send e.g. ^C through.
|
||||
INTERACTIVE := $(shell [ -t 0 ] && echo 1 || echo 0)
|
||||
ifeq ($(INTERACTIVE), 1)
|
||||
CONTAINER_CMD += -t
|
||||
endif
|
||||
CONTAINER_GOSRC = /src/github.com/containers/skopeo
|
||||
CONTAINER_RUN ?= $(CONTAINER_CMD) --security-opt label=disable -v $(CURDIR):$(CONTAINER_GOSRC) -w $(CONTAINER_GOSRC) $(SKOPEO_CIDEV_CONTAINER_FQIN)
|
||||
|
||||
GIT_COMMIT := $(shell GIT_CEILING_DIRECTORIES=$$(cd ..; pwd) git rev-parse HEAD 2> /dev/null || true)
|
||||
|
||||
EXTRA_LDFLAGS ?=
|
||||
SKOPEO_LDFLAGS := -ldflags '-X main.gitCommit=${GIT_COMMIT} $(EXTRA_LDFLAGS)'
|
||||
|
||||
MANPAGES_MD = $(wildcard docs/*.md)
|
||||
MANPAGES ?= $(MANPAGES_MD:%.md=%)
|
||||
|
||||
BTRFS_BUILD_TAG = $(shell hack/btrfs_installed_tag.sh)
|
||||
LIBSUBID_BUILD_TAG = $(shell hack/libsubid_tag.sh)
|
||||
SQLITE_BUILD_TAG = $(shell hack/sqlite_tag.sh)
|
||||
LOCAL_BUILD_TAGS = $(BTRFS_BUILD_TAG) $(LIBSUBID_BUILD_TAG) $(SQLITE_BUILD_TAG)
|
||||
BUILDTAGS += $(LOCAL_BUILD_TAGS)
|
||||
|
||||
ifeq ($(DISABLE_CGO), 1)
|
||||
override BUILDTAGS = exclude_graphdriver_btrfs containers_image_openpgp
|
||||
endif
|
||||
|
||||
# make all DEBUG=1
|
||||
# Note: Uses the -N -l go compiler options to disable compiler optimizations
|
||||
# and inlining. Using these build options allows you to subsequently
|
||||
# use source debugging tools like delve.
|
||||
all: bin/skopeo docs
|
||||
|
||||
codespell:
|
||||
codespell -S Makefile,build,buildah,buildah.spec,imgtype,copy,AUTHORS,bin,vendor,.git,go.sum,CHANGELOG.md,changelog.txt,seccomp.json,.cirrus.yml,"*.xz,*.gz,*.tar,*.tgz,*ico,*.png,*.1,*.5,*.orig,*.rej" -L fpr,uint,iff,od,ERRO -w
|
||||
|
||||
help:
|
||||
@echo "Usage: make <target>"
|
||||
@echo
|
||||
@echo "Defaults to building bin/skopeo and docs"
|
||||
@echo
|
||||
@echo " * 'install' - Install binaries and documents to system locations"
|
||||
@echo " * 'binary' - Build skopeo with a container"
|
||||
@echo " * 'bin/skopeo' - Build skopeo locally"
|
||||
@echo " * 'bin/skopeo.OS.ARCH' - Build skopeo for specific OS and ARCH"
|
||||
@echo " * 'test-unit' - Execute unit tests"
|
||||
@echo " * 'test-integration' - Execute integration tests"
|
||||
@echo " * 'validate' - Verify whether there is no conflict and all Go source files have been formatted, linted and vetted"
|
||||
@echo " * 'check' - Including above validate, test-integration and test-unit"
|
||||
@echo " * 'shell' - Run the built image and attach to a shell"
|
||||
@echo " * 'clean' - Clean artifacts"
|
||||
|
||||
# Do the build and the output (skopeo) should appear in current dir
|
||||
binary: cmd/skopeo
|
||||
$(CONTAINER_RUN) make bin/skopeo $(if $(DEBUG),DEBUG=$(DEBUG)) BUILDTAGS='$(BUILDTAGS)'
|
||||
|
||||
# Build w/o using containers
|
||||
.PHONY: bin/skopeo
|
||||
bin/skopeo:
|
||||
$(GO) build ${GO_DYN_FLAGS} ${SKOPEO_LDFLAGS} -gcflags "$(GOGCFLAGS)" -tags "$(BUILDTAGS)" -o $@ ./cmd/skopeo
|
||||
bin/skopeo.%:
|
||||
GOOS=$(word 2,$(subst ., ,$@)) GOARCH=$(word 3,$(subst ., ,$@)) $(GO) build ${SKOPEO_LDFLAGS} -tags "containers_image_openpgp $(BUILDTAGS)" -o $@ ./cmd/skopeo
|
||||
local-cross: bin/skopeo.darwin.amd64 bin/skopeo.linux.arm bin/skopeo.linux.arm64 bin/skopeo.windows.386.exe bin/skopeo.windows.amd64.exe
|
||||
|
||||
$(MANPAGES): %: %.md
|
||||
ifneq ($(DISABLE_DOCS), 1)
|
||||
sed -e 's/\((skopeo.*\.md)\)//' -e 's/\[\(skopeo.*\)\]/\1/' $< | $(GOMD2MAN) -in /dev/stdin -out $@
|
||||
endif
|
||||
|
||||
docs: $(MANPAGES)
|
||||
|
||||
docs-in-container:
|
||||
${CONTAINER_RUN} $(MAKE) docs $(if $(DEBUG),DEBUG=$(DEBUG))
|
||||
|
||||
.PHONY: completions
|
||||
completions: bin/skopeo
|
||||
install -d -m 755 completions/bash completions/zsh completions/fish completions/powershell
|
||||
./bin/skopeo completion bash >| completions/bash/skopeo
|
||||
./bin/skopeo completion zsh >| completions/zsh/_skopeo
|
||||
./bin/skopeo completion fish >| completions/fish/skopeo.fish
|
||||
./bin/skopeo completion powershell >| completions/powershell/skopeo.ps1
|
||||
|
||||
.PHONY: clean
|
||||
clean:
|
||||
$(RM) buildah
|
||||
$(MAKE) -C docs clean
|
||||
rm -rf bin docs/*.1 completions/
|
||||
|
||||
.PHONY: docs
|
||||
docs: ## build the docs on the host
|
||||
$(MAKE) -C docs
|
||||
install: install-binary install-docs install-completions
|
||||
install -d -m 755 ${DESTDIR}${LOOKASIDEDIR}
|
||||
install -d -m 755 ${DESTDIR}${CONTAINERSCONFDIR}
|
||||
install -m 644 default-policy.json ${DESTDIR}${CONTAINERSCONFDIR}/policy.json
|
||||
install -d -m 755 ${DESTDIR}${REGISTRIESDDIR}
|
||||
install -m 644 default.yaml ${DESTDIR}${REGISTRIESDDIR}/default.yaml
|
||||
|
||||
# For vendoring to work right, the checkout directory must be such that our top
|
||||
# level is at $GOPATH/src/github.com/projectatomic/buildah.
|
||||
.PHONY: gopath
|
||||
gopath:
|
||||
test $(shell pwd) = $(shell cd ../../../../src/github.com/projectatomic/buildah ; pwd)
|
||||
install-binary: bin/skopeo
|
||||
install -d -m 755 ${DESTDIR}${BINDIR}
|
||||
install -m 755 bin/skopeo ${DESTDIR}${BINDIR}/skopeo
|
||||
|
||||
# We use https://github.com/lk4d4/vndr to manage dependencies.
|
||||
.PHONY: deps
|
||||
deps: gopath
|
||||
env GOPATH=$(shell cd ../../../.. ; pwd) vndr
|
||||
install-docs: docs
|
||||
ifneq ($(DISABLE_DOCS), 1)
|
||||
install -d -m 755 ${DESTDIR}${MANDIR}/man1
|
||||
install -m 644 docs/*.1 ${DESTDIR}${MANDIR}/man1
|
||||
endif
|
||||
|
||||
install-completions: completions
|
||||
install -d -m 755 ${DESTDIR}${BASHINSTALLDIR}
|
||||
install -m 644 completions/bash/skopeo ${DESTDIR}${BASHINSTALLDIR}
|
||||
install -d -m 755 ${DESTDIR}${ZSHINSTALLDIR}
|
||||
install -m 644 completions/zsh/_skopeo ${DESTDIR}${ZSHINSTALLDIR}
|
||||
install -d -m 755 ${DESTDIR}${FISHINSTALLDIR}
|
||||
install -m 644 completions/fish/skopeo.fish ${DESTDIR}${FISHINSTALLDIR}
|
||||
# There is no common location for powershell files so do not install them. Users have to source the file from their powershell profile.
|
||||
|
||||
shell:
|
||||
$(CONTAINER_RUN) bash
|
||||
|
||||
tools:
|
||||
if [ ! -x "$(GOBIN)/golangci-lint" ]; then \
|
||||
curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(GOBIN) v$(GOLANGCI_LINT_VERSION) ; \
|
||||
fi
|
||||
|
||||
check: validate test-unit test-integration test-system
|
||||
|
||||
test-integration:
|
||||
# This is intended to be equal to $(CONTAINER_RUN), but with --cap-add=cap_mknod.
|
||||
# --cap-add=cap_mknod is important to allow skopeo to use containers-storage: directly as it exists in the callers’ environment, without
|
||||
# creating a nested user namespace (which requires /etc/subuid and /etc/subgid to be set up)
|
||||
$(CONTAINER_CMD) --security-opt label=disable --cap-add=cap_mknod -v $(CURDIR):$(CONTAINER_GOSRC) -w $(CONTAINER_GOSRC) $(SKOPEO_CIDEV_CONTAINER_FQIN) \
|
||||
$(MAKE) test-integration-local
|
||||
|
||||
|
||||
# Primarily intended for CI.
|
||||
test-integration-local: bin/skopeo
|
||||
hack/warn-destructive-tests.sh
|
||||
$(MAKE) PREFIX=/usr install
|
||||
cd ./integration && $(GO) test $(SKOPEO_LDFLAGS) $(TESTFLAGS) $(if $(BUILDTAGS),-tags "$(BUILDTAGS)")
|
||||
|
||||
# complicated set of options needed to run podman-in-podman
|
||||
test-system:
|
||||
DTEMP=$(shell mktemp -d --tmpdir=/var/tmp podman-tmp.XXXXXX); \
|
||||
$(CONTAINER_CMD) --privileged \
|
||||
-v $(CURDIR):$(CONTAINER_GOSRC) -w $(CONTAINER_GOSRC) \
|
||||
-v $$DTEMP:/var/lib/containers:Z -v /run/systemd/journal/socket:/run/systemd/journal/socket \
|
||||
"$(SKOPEO_CIDEV_CONTAINER_FQIN)" \
|
||||
$(MAKE) test-system-local; \
|
||||
rc=$$?; \
|
||||
$(CONTAINER_RUNTIME) unshare rm -rf $$DTEMP; # This probably doesn't work with Docker, oh well, better than nothing... \
|
||||
exit $$rc
|
||||
|
||||
# Primarily intended for CI.
|
||||
test-system-local: $(if $(SKOPEO_BINARY),,bin/skopeo)
|
||||
hack/warn-destructive-tests.sh
|
||||
@echo "Testing with $(or $(SKOPEO_BINARY),$(eval SKOPEO_BINARY := "bin/skopeo")$(SKOPEO_BINARY)) ..."
|
||||
bats --tap systemtest
|
||||
|
||||
test-unit:
|
||||
# Just call (make test unit-local) here instead of worrying about environment differences
|
||||
$(CONTAINER_RUN) $(MAKE) test-unit-local
|
||||
|
||||
.PHONY: validate
|
||||
validate:
|
||||
@./tests/validate/gofmt.sh
|
||||
@./tests/validate/govet.sh
|
||||
@./tests/validate/git-validation.sh
|
||||
@./tests/validate/gometalinter.sh . cmd/buildah
|
||||
$(CONTAINER_RUN) $(MAKE) validate-local
|
||||
|
||||
.PHONY: install.tools
|
||||
install.tools:
|
||||
go get -u $(BUILDFLAGS) github.com/cpuguy83/go-md2man
|
||||
go get -u $(BUILDFLAGS) github.com/vbatts/git-validation
|
||||
go get -u $(BUILDFLAGS) gopkg.in/alecthomas/gometalinter.v1
|
||||
gometalinter.v1 -i
|
||||
# This target is only intended for development, e.g. executing it from an IDE. Use (make test) for CI or pre-release testing.
|
||||
test-all-local: validate-local validate-docs test-unit-local
|
||||
|
||||
.PHONY: install
|
||||
install:
|
||||
install -D -m0755 buildah $(DESTDIR)/$(BINDIR)/buildah
|
||||
$(MAKE) -C docs install
|
||||
.PHONY: validate-local
|
||||
validate-local: tools
|
||||
hack/validate-git-marks.sh
|
||||
hack/validate-gofmt.sh
|
||||
$(GOBIN)/golangci-lint run --build-tags "${BUILDTAGS}"
|
||||
# An extra run with --tests=false allows detecting code unused outside of tests;
|
||||
# ideally the linter should be able to find this automatically.
|
||||
# Since everything is already cached, this additional run doesn't take much time.
|
||||
$(GOBIN)/golangci-lint run --build-tags "${BUILDTAGS}" --tests=false
|
||||
BUILDTAGS="${BUILDTAGS}" hack/validate-vet.sh
|
||||
|
||||
.PHONY: install.completions
|
||||
install.completions:
|
||||
install -m 644 -D contrib/completions/bash/buildah $(DESTDIR)/${BASHINSTALLDIR}/buildah
|
||||
# This invokes bin/skopeo, hence cannot be run as part of validate-local
|
||||
.PHONY: validate-docs
|
||||
validate-docs: bin/skopeo
|
||||
hack/man-page-checker
|
||||
hack/xref-helpmsgs-manpages
|
||||
|
||||
test-unit-local:
|
||||
$(GO) test -tags "$(BUILDTAGS)" $$($(GO) list -tags "$(BUILDTAGS)" -e ./... | grep -v '^github\.com/containers/skopeo/\(integration\|vendor/.*\)$$')
|
||||
|
||||
vendor:
|
||||
$(GO) mod tidy
|
||||
$(GO) mod vendor
|
||||
$(GO) mod verify
|
||||
|
||||
vendor-in-container:
|
||||
podman run --privileged --rm --env HOME=/root -v $(CURDIR):/src -w /src golang $(MAKE) vendor
|
||||
|
||||
# CAUTION: This is not a replacement for RPMs provided by your distro.
|
||||
# Only intended to build and test the latest unreleased changes.
|
||||
rpm:
|
||||
rpkg local
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
approvers:
|
||||
- baude
|
||||
- giuseppe
|
||||
- lsm5
|
||||
- Luap99
|
||||
- mheon
|
||||
- mtrmac
|
||||
- nalind
|
||||
- rhatdan
|
||||
- TomSweeneyRedHat
|
||||
reviewers:
|
||||
- ashley-cui
|
||||
- baude
|
||||
- cgwalters
|
||||
- giuseppe
|
||||
- lsm5
|
||||
- Luap99
|
||||
- mheon
|
||||
- mtrmac
|
||||
- nalind
|
||||
- rhatdan
|
||||
- TomSweeneyRedHat
|
||||
- vrothberg
|
284
README.md
284
README.md
|
@ -1,89 +1,229 @@
|
|||
buildah - a tool which facilitates building OCI container images
|
||||
================================================================
|
||||
<p align="center">
|
||||
<img src="https://cdn.rawgit.com/containers/skopeo/main/docs/skopeo.svg" width="250" alt="Skopeo">
|
||||
</p>
|
||||
|
||||
[](https://goreportcard.com/report/github.com/projectatomic/buildah)
|
||||
[](https://travis-ci.org/projectatomic/buildah)
|
||||
----
|
||||

|
||||

|
||||
[](https://goreportcard.com/report/github.com/containers/skopeo)
|
||||
[](https://www.bestpractices.dev/projects/10516)
|
||||
|
||||
Note: this package is in alpha, but is close to being feature-complete.
|
||||
`skopeo` is a command line utility that performs various operations on container images and image repositories.
|
||||
|
||||
The buildah package provides a command line tool which can be used to
|
||||
* create a working container, either from scratch or using an image as a starting point
|
||||
* create an image, either from a working container or via the instructions in a Dockerfile
|
||||
* images can be built in either the OCI image format or the traditional upstream docker image format
|
||||
* mount a working container's root filesystem for manipulation
|
||||
* unmount a working container's root filesystem
|
||||
* use the updated contents of a container's root filesystem as a filesystem layer to create a new image
|
||||
* delete a working container or an image
|
||||
`skopeo` does not require the user to be running as root to do most of its operations.
|
||||
|
||||
**Installation notes**
|
||||
`skopeo` does not require a daemon to be running to perform its operations.
|
||||
|
||||
Prior to installing buildah, install the following packages on your linux distro:
|
||||
* make
|
||||
* golang (Requires version 1.8.1 or higher.)
|
||||
* bats
|
||||
* btrfs-progs-devel
|
||||
* device-mapper-devel
|
||||
* gpgme-devel
|
||||
* libassuan-devel
|
||||
* git
|
||||
* bzip2
|
||||
* go-md2man
|
||||
* skopeo-containers
|
||||
`skopeo` can work with [OCI images](https://github.com/opencontainers/image-spec) as well as the original Docker v2 images.
|
||||
|
||||
In Fedora, you can use this command:
|
||||
Skopeo works with API V2 container image registries such as [docker.io](https://docker.io) and [quay.io](https://quay.io) registries, private registries, local directories and local OCI-layout directories. Skopeo can perform operations which consist of:
|
||||
|
||||
```
|
||||
dnf -y install \
|
||||
make \
|
||||
golang \
|
||||
bats \
|
||||
btrfs-progs-devel \
|
||||
device-mapper-devel \
|
||||
gpgme-devel \
|
||||
libassuan-devel \
|
||||
git \
|
||||
bzip2 \
|
||||
go-md2man \
|
||||
skopeo-containers
|
||||
* Copying an image from and to various storage mechanisms.
|
||||
For example you can copy images from one registry to another, without requiring privilege.
|
||||
* Inspecting a remote image showing its properties including its layers, without requiring you to pull the image to the host.
|
||||
* Deleting an image from an image repository.
|
||||
* Syncing an external image repository to an internal registry for air-gapped deployments.
|
||||
* When required by the repository, skopeo can pass the appropriate credentials and certificates for authentication.
|
||||
|
||||
Skopeo operates on the following image and repository types:
|
||||
|
||||
* containers-storage:docker-reference
|
||||
An image located in a local containers/storage image store. Both the location and image store are specified in /etc/containers/storage.conf. (This is the backend for [Podman](https://podman.io), [CRI-O](https://cri-o.io), [Buildah](https://buildah.io) and friends)
|
||||
|
||||
* dir:path
|
||||
An existing local directory path storing the manifest, layer tarballs and signatures as individual files. This is a non-standardized format, primarily useful for debugging or noninvasive container inspection.
|
||||
|
||||
* docker://docker-reference
|
||||
An image in a registry implementing the "Docker Registry HTTP API V2". By default, uses the authorization state in `$XDG_RUNTIME_DIR/containers/auth.json`, which is set using `skopeo login`.
|
||||
|
||||
* docker-archive:path[:docker-reference]
|
||||
An image is stored in a `docker save`-formatted file. docker-reference is only used when creating such a file, and it must not contain a digest.
|
||||
|
||||
* docker-daemon:docker-reference
|
||||
An image docker-reference stored in the docker daemon internal storage. docker-reference must contain either a tag or a digest. Alternatively, when reading images, the format can also be docker-daemon:algo:digest (an image ID).
|
||||
|
||||
* oci:path:tag
|
||||
An image tag in a directory compliant with "Open Container Image Layout Specification" at path.
|
||||
|
||||
[Obtaining skopeo](./install.md)
|
||||
-
|
||||
|
||||
For a detailed description how to install or build skopeo, see
|
||||
[install.md](./install.md).
|
||||
|
||||
Skopeo is also available as a Container Image on [quay.io](https://quay.io/skopeo/stable). For more information, see the [Skopeo Image](https://github.com/containers/image_build/blob/main/skopeo/README.md) page.
|
||||
|
||||
## Inspecting a repository
|
||||
`skopeo` is able to _inspect_ a repository on a container registry and fetch images layers.
|
||||
The _inspect_ command fetches the repository's manifest and it is able to show you a `docker inspect`-like
|
||||
json output about a whole repository or a tag. This tool, in contrast to `docker inspect`, helps you gather useful information about
|
||||
a repository or a tag before pulling it (using disk space). The inspect command can show you which tags are available for the given
|
||||
repository, the labels the image has, the creation date and operating system of the image and more.
|
||||
|
||||
Examples:
|
||||
|
||||
#### Show properties of fedora:latest
|
||||
```console
|
||||
$ skopeo inspect docker://registry.fedoraproject.org/fedora:latest
|
||||
{
|
||||
"Name": "registry.fedoraproject.org/fedora",
|
||||
"Digest": "sha256:0f65bee641e821f8118acafb44c2f8fe30c2fc6b9a2b3729c0660376391aa117",
|
||||
"RepoTags": [
|
||||
"34-aarch64",
|
||||
"34",
|
||||
"latest",
|
||||
...
|
||||
],
|
||||
"Created": "2022-11-24T13:54:18Z",
|
||||
"DockerVersion": "1.10.1",
|
||||
"Labels": {
|
||||
"license": "MIT",
|
||||
"name": "fedora",
|
||||
"vendor": "Fedora Project",
|
||||
"version": "37"
|
||||
},
|
||||
"Architecture": "amd64",
|
||||
"Os": "linux",
|
||||
"Layers": [
|
||||
"sha256:2a0fc6bf62e155737f0ace6142ee686f3c471c1aab4241dc3128904db46288f0"
|
||||
],
|
||||
"LayersData": [
|
||||
{
|
||||
"MIMEType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
|
||||
"Digest": "sha256:2a0fc6bf62e155737f0ace6142ee686f3c471c1aab4241dc3128904db46288f0",
|
||||
"Size": 71355009,
|
||||
"Annotations": null
|
||||
}
|
||||
],
|
||||
"Env": [
|
||||
"DISTTAG=f37container",
|
||||
"FGC=f37",
|
||||
"container=oci"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Then to install buildah follow the steps in this example:
|
||||
#### Show container configuration from `fedora:latest`
|
||||
|
||||
```console
|
||||
$ skopeo inspect --config docker://registry.fedoraproject.org/fedora:latest | jq
|
||||
{
|
||||
"created": "2020-04-29T06:48:16Z",
|
||||
"architecture": "amd64",
|
||||
"os": "linux",
|
||||
"config": {
|
||||
"Env": [
|
||||
"DISTTAG=f32container",
|
||||
"FGC=f32",
|
||||
"container=oci"
|
||||
],
|
||||
"Cmd": [
|
||||
"/bin/bash"
|
||||
],
|
||||
"Labels": {
|
||||
"license": "MIT",
|
||||
"name": "fedora",
|
||||
"vendor": "Fedora Project",
|
||||
"version": "32"
|
||||
}
|
||||
},
|
||||
"rootfs": {
|
||||
"type": "layers",
|
||||
"diff_ids": [
|
||||
"sha256:a4c0fa2b217d3fd63d51e55a6fd59432e543d499c0df2b1acd48fbe424f2ddd1"
|
||||
]
|
||||
},
|
||||
"history": [
|
||||
{
|
||||
"created": "2020-04-29T06:48:16Z",
|
||||
"comment": "Created by Image Factory"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
mkdir ~/buildah
|
||||
cd ~/buildah
|
||||
export GOPATH=`pwd`
|
||||
git clone https://github.com/projectatomic/buildah ./src/github.com/projectatomic/buildah
|
||||
cd ./src/github.com/projectatomic/buildah
|
||||
make
|
||||
make install
|
||||
buildah --help
|
||||
#### Show unverified image's digest
|
||||
```console
|
||||
$ skopeo inspect docker://registry.fedoraproject.org/fedora:latest | jq '.Digest'
|
||||
"sha256:655721ff613ee766a4126cb5e0d5ae81598e1b0c3bcf7017c36c4d72cb092fe9"
|
||||
```
|
||||
|
||||
buildah uses `runc` to run commands when `buildah run` is used, or when `buildah build-using-dockerfile`
|
||||
encounters a `RUN` instruction, so you'll also need to build and install a compatible version of
|
||||
[runc](https://github.com/opencontainers/runc) for buildah to call for those cases.
|
||||
## Copying images
|
||||
|
||||
`skopeo` can copy container images between various storage mechanisms, including:
|
||||
* Container registries
|
||||
|
||||
- The Quay, Docker Hub, OpenShift, GCR, Artifactory ...
|
||||
|
||||
* Container Storage backends
|
||||
|
||||
- [github.com/containers/storage](https://github.com/containers/storage) (Backend for [Podman](https://podman.io), [CRI-O](https://cri-o.io), [Buildah](https://buildah.io) and friends)
|
||||
|
||||
- Docker daemon storage
|
||||
|
||||
* Local directories
|
||||
|
||||
* Local OCI-layout directories
|
||||
|
||||
```console
|
||||
$ skopeo copy docker://quay.io/buildah/stable docker://registry.internal.company.com/buildah
|
||||
$ skopeo copy oci:busybox_ocilayout:latest dir:existingemptydirectory
|
||||
```
|
||||
|
||||
## Deleting images
|
||||
```console
|
||||
$ skopeo delete docker://localhost:5000/imagename:latest
|
||||
```
|
||||
|
||||
## Syncing registries
|
||||
```console
|
||||
$ skopeo sync --src docker --dest dir registry.example.com/busybox /media/usb
|
||||
```
|
||||
|
||||
## Authenticating to a registry
|
||||
|
||||
#### Private registries with authentication
|
||||
skopeo uses credentials from the --creds (for skopeo inspect|delete) or --src-creds|--dest-creds (for skopeo copy) flags, if set; otherwise it uses configuration set by skopeo login, podman login, buildah login, or docker login.
|
||||
|
||||
```console
|
||||
$ skopeo login --username USER myregistrydomain.com:5000
|
||||
Password:
|
||||
$ skopeo inspect docker://myregistrydomain.com:5000/busybox
|
||||
{"Tag":"latest","Digest":"sha256:473bb2189d7b913ed7187a33d11e743fdc2f88931122a44d91a301b64419f092","RepoTags":["latest"],"Comment":"","Created":"2016-01-15T18:06:41.282540103Z","ContainerConfig":{"Hostname":"aded96b43f48","Domainname":"","User":"","AttachStdin":false,"AttachStdout":false,"AttachStderr":false,"Tty":false,"OpenStdin":false,"StdinOnce":false,"Env":null,"Cmd":["/bin/sh","-c","#(nop) CMD [\"sh\"]"],"Image":"9e77fef7a1c9f989988c06620dabc4020c607885b959a2cbd7c2283c91da3e33","Volumes":null,"WorkingDir":"","Entrypoint":null,"OnBuild":null,"Labels":null},"DockerVersion":"1.8.3","Author":"","Config":{"Hostname":"aded96b43f48","Domainname":"","User":"","AttachStdin":false,"AttachStdout":false,"AttachStderr":false,"Tty":false,"OpenStdin":false,"StdinOnce":false,"Env":null,"Cmd":["sh"],"Image":"9e77fef7a1c9f989988c06620dabc4020c607885b959a2cbd7c2283c91da3e33","Volumes":null,"WorkingDir":"","Entrypoint":null,"OnBuild":null,"Labels":null},"Architecture":"amd64","Os":"linux"}
|
||||
$ skopeo logout myregistrydomain.com:5000
|
||||
```
|
||||
|
||||
#### Using --creds directly
|
||||
|
||||
```console
|
||||
$ skopeo inspect --creds=testuser:testpassword docker://myregistrydomain.com:5000/busybox
|
||||
{"Tag":"latest","Digest":"sha256:473bb2189d7b913ed7187a33d11e743fdc2f88931122a44d91a301b64419f092","RepoTags":["latest"],"Comment":"","Created":"2016-01-15T18:06:41.282540103Z","ContainerConfig":{"Hostname":"aded96b43f48","Domainname":"","User":"","AttachStdin":false,"AttachStdout":false,"AttachStderr":false,"Tty":false,"OpenStdin":false,"StdinOnce":false,"Env":null,"Cmd":["/bin/sh","-c","#(nop) CMD [\"sh\"]"],"Image":"9e77fef7a1c9f989988c06620dabc4020c607885b959a2cbd7c2283c91da3e33","Volumes":null,"WorkingDir":"","Entrypoint":null,"OnBuild":null,"Labels":null},"DockerVersion":"1.8.3","Author":"","Config":{"Hostname":"aded96b43f48","Domainname":"","User":"","AttachStdin":false,"AttachStdout":false,"AttachStderr":false,"Tty":false,"OpenStdin":false,"StdinOnce":false,"Env":null,"Cmd":["sh"],"Image":"9e77fef7a1c9f989988c06620dabc4020c607885b959a2cbd7c2283c91da3e33","Volumes":null,"WorkingDir":"","Entrypoint":null,"OnBuild":null,"Labels":null},"Architecture":"amd64","Os":"linux"}
|
||||
```
|
||||
|
||||
```console
|
||||
$ skopeo copy --src-creds=testuser:testpassword docker://myregistrydomain.com:5000/private oci:local_oci_image
|
||||
```
|
||||
|
||||
Contributing
|
||||
-
|
||||
|
||||
Please read the [contribution guide](CONTRIBUTING.md) if you want to collaborate in the project.
|
||||
|
||||
## Commands
|
||||
| Command | Description |
|
||||
| --------------------- | --------------------------------------------------- |
|
||||
| buildah-add(1) | Add the contents of a file, URL, or a directory to the container. |
|
||||
| buildah-bud(1) | Build an image using instructions from Dockerfiles. |
|
||||
| buildah-commit(1) | Create an image from a working container. |
|
||||
| buildah-config(1) | Update image configuration settings. |
|
||||
| buildah-containers(1) | List the working containers and their base images. |
|
||||
| buildah-copy(1) | Copies the contents of a file, URL, or directory into a container's working directory. |
|
||||
| buildah-from(1) | Creates a new working container, either from scratch or using a specified image as a starting point. |
|
||||
| buildah-images(1) | List images in local storage. |
|
||||
| buildah-inspect(1) | Inspects the configuration of a container or image. |
|
||||
| buildah-mount(1) | Mount the working container's root filesystem. |
|
||||
| buildah-push(1) | Copies an image from local storage. |
|
||||
| buildah-rm(1) | Removes one or more working containers. |
|
||||
| buildah-rmi(1) | Removes one or more images. |
|
||||
| buildah-run(1) | Run a command inside of the container. |
|
||||
| buildah-tag(1) | Add an additional name to a local image. |
|
||||
| buildah-umount(1) | Unmount a working container's root file system. |
|
||||
| -------------------------------------------------- | ---------------------------------------------------------------------------------------------|
|
||||
| [skopeo-copy(1)](/docs/skopeo-copy.1.md) | Copy an image (manifest, filesystem layers, signatures) from one location to another. |
|
||||
| [skopeo-delete(1)](/docs/skopeo-delete.1.md) | Mark the image-name for later deletion by the registry's garbage collector. |
|
||||
| [skopeo-generate-sigstore-key(1)](/docs/skopeo-generate-sigstore-key.1.md) | Generate a sigstore public/private key pair. |
|
||||
| [skopeo-inspect(1)](/docs/skopeo-inspect.1.md) | Return low-level information about image-name in a registry. |
|
||||
| [skopeo-list-tags(1)](/docs/skopeo-list-tags.1.md) | Return a list of tags for the transport-specific image repository. |
|
||||
| [skopeo-login(1)](/docs/skopeo-login.1.md) | Login to a container registry. |
|
||||
| [skopeo-logout(1)](/docs/skopeo-logout.1.md) | Logout of a container registry. |
|
||||
| [skopeo-manifest-digest(1)](/docs/skopeo-manifest-digest.1.md) | Compute a manifest digest for a manifest-file and write it to standard output. |
|
||||
| [skopeo-standalone-sign(1)](/docs/skopeo-standalone-sign.1.md) | Debugging tool - Sign an image locally without uploading. |
|
||||
| [skopeo-standalone-verify(1)](/docs/skopeo-standalone-verify.1.md)| Debugging tool - Verify an image signature from local files. |
|
||||
| [skopeo-sync(1)](/docs/skopeo-sync.1.md) | Synchronize images between registry repositories and local directories. |
|
||||
|
||||
**Future goals include:**
|
||||
* more CI tests
|
||||
* additional CLI commands (?)
|
||||
License
|
||||
-
|
||||
skopeo is licensed under the Apache License, Version 2.0. See
|
||||
[LICENSE](LICENSE) for the full license text.
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
# Skopeo Roadmap
|
||||
|
||||
Skopeo intends to mostly continue to be a very thin CLI wrapper over the [https://github.com/containers/image](containers/image) library, with most features being added there, not to this repo. A typical new Skopeo feature would only add a CLI for a recent containers/image feature.
|
||||
|
||||
## Future feature focus (most of the work must be done in the containers/image library)
|
||||
|
||||
* OCI artifact support.
|
||||
* Integration of composefs.
|
||||
* Partial pull support (zstd:chunked).
|
||||
* Performance and stability improvements.
|
||||
* Reductions to the size of the Skopeo binary.
|
||||
* `skopeo sync` exists, and bugs in it should be fixed, but we don’t have much of an ambition to compete with much larger projects like [https://github.com/openshift/oc-mirror](oc-mirror).
|
|
@ -0,0 +1,3 @@
|
|||
## Security and Disclosure Information Policy for the skopeo Project
|
||||
|
||||
The skopeo Project follows the [Security and Disclosure Information Policy](https://github.com/containers/common/blob/main/SECURITY.md) for the Containers Projects.
|
164
add.go
164
add.go
|
@ -1,164 +0,0 @@
|
|||
package buildah
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/Sirupsen/logrus"
|
||||
"github.com/containers/storage/pkg/archive"
|
||||
"github.com/containers/storage/pkg/chrootarchive"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// addURL copies the contents of the source URL to the destination. This is
|
||||
// its own function so that deferred closes happen after we're done pulling
|
||||
// down each item of potentially many.
|
||||
func addURL(destination, srcurl string) error {
|
||||
logrus.Debugf("saving %q to %q", srcurl, destination)
|
||||
resp, err := http.Get(srcurl)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "error getting %q", srcurl)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
f, err := os.Create(destination)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "error creating %q", destination)
|
||||
}
|
||||
if last := resp.Header.Get("Last-Modified"); last != "" {
|
||||
if mtime, err2 := time.Parse(time.RFC1123, last); err2 != nil {
|
||||
logrus.Debugf("error parsing Last-Modified time %q: %v", last, err2)
|
||||
} else {
|
||||
defer func() {
|
||||
if err3 := os.Chtimes(destination, time.Now(), mtime); err3 != nil {
|
||||
logrus.Debugf("error setting mtime to Last-Modified time %q: %v", last, err3)
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
||||
defer f.Close()
|
||||
n, err := io.Copy(f, resp.Body)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "error reading contents for %q", destination)
|
||||
}
|
||||
if resp.ContentLength >= 0 && n != resp.ContentLength {
|
||||
return errors.Errorf("error reading contents for %q: wrong length (%d != %d)", destination, n, resp.ContentLength)
|
||||
}
|
||||
if err := f.Chmod(0600); err != nil {
|
||||
return errors.Wrapf(err, "error setting permissions on %q", destination)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Add copies the contents of the specified sources into the container's root
|
||||
// filesystem, optionally extracting contents of local files that look like
|
||||
// non-empty archives.
|
||||
func (b *Builder) Add(destination string, extract bool, source ...string) error {
|
||||
mountPoint, err := b.Mount("")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
if err2 := b.Unmount(); err2 != nil {
|
||||
logrus.Errorf("error unmounting container: %v", err2)
|
||||
}
|
||||
}()
|
||||
dest := mountPoint
|
||||
if destination != "" && filepath.IsAbs(destination) {
|
||||
dest = filepath.Join(dest, destination)
|
||||
} else {
|
||||
if err = os.MkdirAll(filepath.Join(dest, b.WorkDir()), 0755); err != nil {
|
||||
return errors.Wrapf(err, "error ensuring directory %q exists)", filepath.Join(dest, b.WorkDir()))
|
||||
}
|
||||
dest = filepath.Join(dest, b.WorkDir(), destination)
|
||||
}
|
||||
// If the destination was explicitly marked as a directory by ending it
|
||||
// with a '/', create it so that we can be sure that it's a directory,
|
||||
// and any files we're copying will be placed in the directory.
|
||||
if len(destination) > 0 && destination[len(destination)-1] == os.PathSeparator {
|
||||
if err = os.MkdirAll(dest, 0755); err != nil {
|
||||
return errors.Wrapf(err, "error ensuring directory %q exists", dest)
|
||||
}
|
||||
}
|
||||
// Make sure the destination's parent directory is usable.
|
||||
if destpfi, err2 := os.Stat(filepath.Dir(dest)); err2 == nil && !destpfi.IsDir() {
|
||||
return errors.Errorf("%q already exists, but is not a subdirectory)", filepath.Dir(dest))
|
||||
}
|
||||
// Now look at the destination itself.
|
||||
destfi, err := os.Stat(dest)
|
||||
if err != nil {
|
||||
if !os.IsNotExist(err) {
|
||||
return errors.Wrapf(err, "couldn't determine what %q is", dest)
|
||||
}
|
||||
destfi = nil
|
||||
}
|
||||
if len(source) > 1 && (destfi == nil || !destfi.IsDir()) {
|
||||
return errors.Errorf("destination %q is not a directory", dest)
|
||||
}
|
||||
for _, src := range source {
|
||||
if strings.HasPrefix(src, "http://") || strings.HasPrefix(src, "https://") {
|
||||
// We assume that source is a file, and we're copying
|
||||
// it to the destination. If the destination is
|
||||
// already a directory, create a file inside of it.
|
||||
// Otherwise, the destination is the file to which
|
||||
// we'll save the contents.
|
||||
url, err := url.Parse(src)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "error parsing URL %q", src)
|
||||
}
|
||||
d := dest
|
||||
if destfi != nil && destfi.IsDir() {
|
||||
d = filepath.Join(dest, path.Base(url.Path))
|
||||
}
|
||||
if err := addURL(d, src); err != nil {
|
||||
return err
|
||||
}
|
||||
continue
|
||||
}
|
||||
srcfi, err := os.Stat(src)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "error reading %q", src)
|
||||
}
|
||||
if srcfi.IsDir() {
|
||||
// The source is a directory, so copy the contents of
|
||||
// the source directory into the target directory. Try
|
||||
// to create it first, so that if there's a problem,
|
||||
// we'll discover why that won't work.
|
||||
d := dest
|
||||
if err := os.MkdirAll(d, 0755); err != nil {
|
||||
return errors.Wrapf(err, "error ensuring directory %q exists", d)
|
||||
}
|
||||
logrus.Debugf("copying %q to %q", src+string(os.PathSeparator)+"*", d+string(os.PathSeparator)+"*")
|
||||
if err := chrootarchive.CopyWithTar(src, d); err != nil {
|
||||
return errors.Wrapf(err, "error copying %q to %q", src, d)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if !extract || !archive.IsArchivePath(src) {
|
||||
// This source is a file, and either it's not an
|
||||
// archive, or we don't care whether or not it's an
|
||||
// archive.
|
||||
d := dest
|
||||
if destfi != nil && destfi.IsDir() {
|
||||
d = filepath.Join(dest, filepath.Base(src))
|
||||
}
|
||||
// Copy the file, preserving attributes.
|
||||
logrus.Debugf("copying %q to %q", src, d)
|
||||
if err := chrootarchive.CopyFileWithTar(src, d); err != nil {
|
||||
return errors.Wrapf(err, "error copying %q to %q", src, d)
|
||||
}
|
||||
continue
|
||||
}
|
||||
// We're extracting an archive into the destination directory.
|
||||
logrus.Debugf("extracting contents of %q into %q", src, dest)
|
||||
if err := chrootarchive.UntarPath(src, dest); err != nil {
|
||||
return errors.Wrapf(err, "error extracting %q into %q", src, dest)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
#!/bin/bash
|
||||
cc -E - > /dev/null 2> /dev/null << EOF
|
||||
#include <btrfs/version.h>
|
||||
EOF
|
||||
if test $? -ne 0 ; then
|
||||
echo btrfs_noversion
|
||||
fi
|
254
buildah.go
254
buildah.go
|
@ -1,254 +0,0 @@
|
|||
package buildah
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/containers/storage"
|
||||
"github.com/containers/storage/pkg/ioutils"
|
||||
"github.com/opencontainers/image-spec/specs-go/v1"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/projectatomic/buildah/docker"
|
||||
)
|
||||
|
||||
const (
|
||||
// Package is the name of this package, used in help output and to
|
||||
// identify working containers.
|
||||
Package = "buildah"
|
||||
// Version for the Package
|
||||
Version = "0.1"
|
||||
containerType = Package + " 0.0.1"
|
||||
stateFile = Package + ".json"
|
||||
)
|
||||
|
||||
const (
|
||||
// PullIfMissing is one of the values that BuilderOptions.PullPolicy
|
||||
// can take, signalling that the source image should be pulled from a
|
||||
// registry if a local copy of it is not already present.
|
||||
PullIfMissing = iota
|
||||
// PullAlways is one of the values that BuilderOptions.PullPolicy can
|
||||
// take, signalling that a fresh, possibly updated, copy of the image
|
||||
// should be pulled from a registry before the build proceeds.
|
||||
PullAlways
|
||||
// PullNever is one of the values that BuilderOptions.PullPolicy can
|
||||
// take, signalling that the source image should not be pulled from a
|
||||
// registry if a local copy of it is not already present.
|
||||
PullNever
|
||||
)
|
||||
|
||||
// Builder objects are used to represent containers which are being used to
|
||||
// build images. They also carry potential updates which will be applied to
|
||||
// the image's configuration when the container's contents are used to build an
|
||||
// image.
|
||||
type Builder struct {
|
||||
store storage.Store
|
||||
|
||||
// Type is used to help identify a build container's metadata. It
|
||||
// should not be modified.
|
||||
Type string `json:"type"`
|
||||
// FromImage is the name of the source image which was used to create
|
||||
// the container, if one was used. It should not be modified.
|
||||
FromImage string `json:"image,omitempty"`
|
||||
// FromImageID is the ID of the source image which was used to create
|
||||
// the container, if one was used. It should not be modified.
|
||||
FromImageID string `json:"image-id"`
|
||||
// Config is the source image's configuration. It should not be
|
||||
// modified.
|
||||
Config []byte `json:"config,omitempty"`
|
||||
// Manifest is the source image's manifest. It should not be modified.
|
||||
Manifest []byte `json:"manifest,omitempty"`
|
||||
|
||||
// Container is the name of the build container. It should not be modified.
|
||||
Container string `json:"container-name,omitempty"`
|
||||
// ContainerID is the ID of the build container. It should not be modified.
|
||||
ContainerID string `json:"container-id,omitempty"`
|
||||
// MountPoint is the last location where the container's root
|
||||
// filesystem was mounted. It should not be modified.
|
||||
MountPoint string `json:"mountpoint,omitempty"`
|
||||
|
||||
// ImageAnnotations is a set of key-value pairs which is stored in the
|
||||
// image's manifest.
|
||||
ImageAnnotations map[string]string `json:"annotations,omitempty"`
|
||||
// ImageCreatedBy is a description of how this container was built.
|
||||
ImageCreatedBy string `json:"created-by,omitempty"`
|
||||
|
||||
// Image metadata and runtime settings, in multiple formats.
|
||||
OCIv1 v1.Image `json:"ociv1,omitempty"`
|
||||
Docker docker.V2Image `json:"docker,omitempty"`
|
||||
}
|
||||
|
||||
// BuilderOptions are used to initialize a new Builder.
|
||||
type BuilderOptions struct {
|
||||
// FromImage is the name of the image which should be used as the
|
||||
// starting point for the container. It can be set to an empty value
|
||||
// or "scratch" to indicate that the container should not be based on
|
||||
// an image.
|
||||
FromImage string
|
||||
// Container is a desired name for the build container.
|
||||
Container string
|
||||
// PullPolicy decides whether or not we should pull the image that
|
||||
// we're using as a base image. It should be PullIfMissing,
|
||||
// PullAlways, or PullNever.
|
||||
PullPolicy int
|
||||
// Registry is a value which is prepended to the image's name, if it
|
||||
// needs to be pulled and the image name alone can not be resolved to a
|
||||
// reference to a source image.
|
||||
Registry string
|
||||
// Mount signals to NewBuilder() that the container should be mounted
|
||||
// immediately.
|
||||
Mount bool
|
||||
// SignaturePolicyPath specifies an override location for the signature
|
||||
// policy which should be used for verifying the new image as it is
|
||||
// being written. Except in specific circumstances, no value should be
|
||||
// specified, indicating that the shared, system-wide default policy
|
||||
// should be used.
|
||||
SignaturePolicyPath string
|
||||
// ReportWriter is an io.Writer which will be used to log the reading
|
||||
// of the source image from a registry, if we end up pulling the image.
|
||||
ReportWriter io.Writer
|
||||
}
|
||||
|
||||
// ImportOptions are used to initialize a Builder from an existing container
|
||||
// which was created elsewhere.
|
||||
type ImportOptions struct {
|
||||
// Container is the name of the build container.
|
||||
Container string
|
||||
// SignaturePolicyPath specifies an override location for the signature
|
||||
// policy which should be used for verifying the new image as it is
|
||||
// being written. Except in specific circumstances, no value should be
|
||||
// specified, indicating that the shared, system-wide default policy
|
||||
// should be used.
|
||||
SignaturePolicyPath string
|
||||
}
|
||||
|
||||
// ImportFromImageOptions are used to initialize a Builder from an image.
|
||||
type ImportFromImageOptions struct {
|
||||
// Image is the name or ID of the image we'd like to examine.
|
||||
Image string
|
||||
// SignaturePolicyPath specifies an override location for the signature
|
||||
// policy which should be used for verifying the new image as it is
|
||||
// being written. Except in specific circumstances, no value should be
|
||||
// specified, indicating that the shared, system-wide default policy
|
||||
// should be used.
|
||||
SignaturePolicyPath string
|
||||
}
|
||||
|
||||
// NewBuilder creates a new build container.
|
||||
func NewBuilder(store storage.Store, options BuilderOptions) (*Builder, error) {
|
||||
return newBuilder(store, options)
|
||||
}
|
||||
|
||||
// ImportBuilder creates a new build configuration using an already-present
|
||||
// container.
|
||||
func ImportBuilder(store storage.Store, options ImportOptions) (*Builder, error) {
|
||||
return importBuilder(store, options)
|
||||
}
|
||||
|
||||
// ImportBuilderFromImage creates a new builder configuration using an image.
|
||||
// The returned object can be modified and examined, but it can not be saved
|
||||
// or committed because it is not associated with a working container.
|
||||
func ImportBuilderFromImage(store storage.Store, options ImportFromImageOptions) (*Builder, error) {
|
||||
return importBuilderFromImage(store, options)
|
||||
}
|
||||
|
||||
// OpenBuilder loads information about a build container given its name or ID.
|
||||
func OpenBuilder(store storage.Store, container string) (*Builder, error) {
|
||||
cdir, err := store.ContainerDirectory(container)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
buildstate, err := ioutil.ReadFile(filepath.Join(cdir, stateFile))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
b := &Builder{}
|
||||
err = json.Unmarshal(buildstate, &b)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if b.Type != containerType {
|
||||
return nil, errors.Errorf("container is not a %s container", Package)
|
||||
}
|
||||
b.store = store
|
||||
b.fixupConfig()
|
||||
return b, nil
|
||||
}
|
||||
|
||||
// OpenBuilderByPath loads information about a build container given a
|
||||
// path to the container's root filesystem
|
||||
func OpenBuilderByPath(store storage.Store, path string) (*Builder, error) {
|
||||
containers, err := store.Containers()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
abs, err := filepath.Abs(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
builderMatchesPath := func(b *Builder, path string) bool {
|
||||
return (b.MountPoint == path)
|
||||
}
|
||||
for _, container := range containers {
|
||||
cdir, err := store.ContainerDirectory(container.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
buildstate, err := ioutil.ReadFile(filepath.Join(cdir, stateFile))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
b := &Builder{}
|
||||
err = json.Unmarshal(buildstate, &b)
|
||||
if err == nil && b.Type == containerType && builderMatchesPath(b, abs) {
|
||||
b.store = store
|
||||
b.fixupConfig()
|
||||
return b, nil
|
||||
}
|
||||
}
|
||||
return nil, storage.ErrContainerUnknown
|
||||
}
|
||||
|
||||
// OpenAllBuilders loads all containers which have a state file that we use in
|
||||
// their data directory, typically so that they can be listed.
|
||||
func OpenAllBuilders(store storage.Store) (builders []*Builder, err error) {
|
||||
containers, err := store.Containers()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, container := range containers {
|
||||
cdir, err := store.ContainerDirectory(container.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
buildstate, err := ioutil.ReadFile(filepath.Join(cdir, stateFile))
|
||||
if err != nil && os.IsNotExist(err) {
|
||||
continue
|
||||
}
|
||||
b := &Builder{}
|
||||
err = json.Unmarshal(buildstate, &b)
|
||||
if err == nil && b.Type == containerType {
|
||||
b.store = store
|
||||
b.fixupConfig()
|
||||
builders = append(builders, b)
|
||||
}
|
||||
}
|
||||
return builders, nil
|
||||
}
|
||||
|
||||
// Save saves the builder's current state to the build container's metadata.
|
||||
// This should not need to be called directly, as other methods of the Builder
|
||||
// object take care of saving their state.
|
||||
func (b *Builder) Save() error {
|
||||
buildstate, err := json.Marshal(b)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cdir, err := b.store.ContainerDirectory(b.ContainerID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ioutils.AtomicWriteFile(filepath.Join(cdir, stateFile), buildstate, 0600)
|
||||
}
|
|
@ -1,69 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"github.com/pkg/errors"
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
var (
|
||||
addDescription = "Adds the contents of a file, URL, or directory to a container's working\n directory. If a local file appears to be an archive, its contents are\n extracted and added instead of the archive file itself."
|
||||
copyDescription = "Copies the contents of a file, URL, or directory into a container's working\n directory"
|
||||
|
||||
addCommand = cli.Command{
|
||||
Name: "add",
|
||||
Usage: "Add content to the container",
|
||||
Description: addDescription,
|
||||
Action: addCmd,
|
||||
ArgsUsage: "CONTAINER-NAME-OR-ID [[FILE | DIRECTORY | URL] ...] [DESTINATION]",
|
||||
}
|
||||
|
||||
copyCommand = cli.Command{
|
||||
Name: "copy",
|
||||
Usage: "Copy content into the container",
|
||||
Description: copyDescription,
|
||||
Action: copyCmd,
|
||||
ArgsUsage: "CONTAINER-NAME-OR-ID [[FILE | DIRECTORY | URL] ...] [DESTINATION]",
|
||||
}
|
||||
)
|
||||
|
||||
func addAndCopyCmd(c *cli.Context, extractLocalArchives bool) error {
|
||||
args := c.Args()
|
||||
if len(args) == 0 {
|
||||
return errors.Errorf("container ID must be specified")
|
||||
}
|
||||
name := args[0]
|
||||
args = args.Tail()
|
||||
|
||||
// If list is greater then one, the last item is the destination
|
||||
dest := ""
|
||||
size := len(args)
|
||||
if size > 1 {
|
||||
dest = args[size-1]
|
||||
args = args[:size-1]
|
||||
}
|
||||
|
||||
store, err := getStore(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
builder, err := openBuilder(store, name)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "error reading build container %q", name)
|
||||
}
|
||||
|
||||
err = builder.Add(dest, extractLocalArchives, args...)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "error adding content to container %q", builder.Container)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func addCmd(c *cli.Context) error {
|
||||
return addAndCopyCmd(c, true)
|
||||
}
|
||||
|
||||
func copyCmd(c *cli.Context) error {
|
||||
return addAndCopyCmd(c, false)
|
||||
}
|
|
@ -1,227 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/Sirupsen/logrus"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/projectatomic/buildah/imagebuildah"
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
var (
|
||||
budFlags = []cli.Flag{
|
||||
cli.BoolFlag{
|
||||
Name: "quiet, q",
|
||||
Usage: "refrain from announcing build instructions and image read/write progress",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "registry",
|
||||
Usage: "prefix to prepend to the image name in order to pull the image",
|
||||
Value: DefaultRegistry,
|
||||
},
|
||||
cli.BoolTFlag{
|
||||
Name: "pull",
|
||||
Usage: "pull the image if not present",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "pull-always",
|
||||
Usage: "pull the image, even if a version is present",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "signature-policy",
|
||||
Usage: "`pathname` of signature policy file (not usually used)",
|
||||
},
|
||||
cli.StringSliceFlag{
|
||||
Name: "build-arg",
|
||||
Usage: "`argument=value` to supply to the builder",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "runtime",
|
||||
Usage: "`path` to an alternate runtime",
|
||||
Value: imagebuildah.DefaultRuntime,
|
||||
},
|
||||
cli.StringSliceFlag{
|
||||
Name: "runtime-flag",
|
||||
Usage: "add global flags for the container runtime",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "format",
|
||||
Usage: "`format` of the built image's manifest and metadata",
|
||||
},
|
||||
cli.StringSliceFlag{
|
||||
Name: "tag, t",
|
||||
Usage: "`tag` to apply to the built image",
|
||||
},
|
||||
cli.StringSliceFlag{
|
||||
Name: "file, f",
|
||||
Usage: "`pathname or URL` of a Dockerfile",
|
||||
},
|
||||
}
|
||||
budDescription = "Builds an OCI image using instructions in one or more Dockerfiles."
|
||||
budCommand = cli.Command{
|
||||
Name: "build-using-dockerfile",
|
||||
Aliases: []string{"bud"},
|
||||
Usage: "Build an image using instructions in a Dockerfile",
|
||||
Description: budDescription,
|
||||
Flags: budFlags,
|
||||
Action: budCmd,
|
||||
ArgsUsage: "CONTEXT-DIRECTORY | URL",
|
||||
}
|
||||
)
|
||||
|
||||
func budCmd(c *cli.Context) error {
|
||||
output := ""
|
||||
tags := []string{}
|
||||
if c.IsSet("tag") || c.IsSet("t") {
|
||||
tags = c.StringSlice("tag")
|
||||
if len(tags) > 0 {
|
||||
output = tags[0]
|
||||
tags = tags[1:]
|
||||
}
|
||||
}
|
||||
registry := DefaultRegistry
|
||||
if c.IsSet("registry") {
|
||||
registry = c.String("registry")
|
||||
}
|
||||
pull := true
|
||||
if c.IsSet("pull") {
|
||||
pull = c.BoolT("pull")
|
||||
}
|
||||
pullAlways := false
|
||||
if c.IsSet("pull-always") {
|
||||
pull = c.Bool("pull-always")
|
||||
}
|
||||
runtimeFlags := []string{}
|
||||
if c.IsSet("runtime-flag") {
|
||||
runtimeFlags = c.StringSlice("runtime-flag")
|
||||
}
|
||||
runtime := ""
|
||||
if c.IsSet("runtime") {
|
||||
runtime = c.String("runtime")
|
||||
}
|
||||
|
||||
pullPolicy := imagebuildah.PullNever
|
||||
if pull {
|
||||
pullPolicy = imagebuildah.PullIfMissing
|
||||
}
|
||||
if pullAlways {
|
||||
pullPolicy = imagebuildah.PullAlways
|
||||
}
|
||||
|
||||
signaturePolicy := ""
|
||||
if c.IsSet("signature-policy") {
|
||||
signaturePolicy = c.String("signature-policy")
|
||||
}
|
||||
args := make(map[string]string)
|
||||
if c.IsSet("build-arg") {
|
||||
for _, arg := range c.StringSlice("build-arg") {
|
||||
av := strings.SplitN(arg, "=", 2)
|
||||
if len(av) > 1 {
|
||||
args[av[0]] = av[1]
|
||||
} else {
|
||||
delete(args, av[0])
|
||||
}
|
||||
}
|
||||
}
|
||||
quiet := false
|
||||
if c.IsSet("quiet") {
|
||||
quiet = c.Bool("quiet")
|
||||
}
|
||||
dockerfiles := []string{}
|
||||
if c.IsSet("file") || c.IsSet("f") {
|
||||
dockerfiles = c.StringSlice("file")
|
||||
}
|
||||
format := "oci"
|
||||
if c.IsSet("format") {
|
||||
format = strings.ToLower(c.String("format"))
|
||||
}
|
||||
if strings.HasPrefix(format, "oci") {
|
||||
format = imagebuildah.OCIv1ImageFormat
|
||||
} else if strings.HasPrefix(format, "docker") {
|
||||
format = imagebuildah.Dockerv2ImageFormat
|
||||
} else {
|
||||
return errors.Errorf("unrecognized image type %q", format)
|
||||
}
|
||||
contextDir := ""
|
||||
cliArgs := c.Args()
|
||||
if len(cliArgs) > 0 {
|
||||
// The context directory could be a URL. Try to handle that.
|
||||
tempDir, subDir, err := imagebuildah.TempDirForURL("", "buildah", cliArgs[0])
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "error prepping temporary context directory")
|
||||
}
|
||||
if tempDir != "" {
|
||||
// We had to download it to a temporary directory.
|
||||
// Delete it later.
|
||||
defer func() {
|
||||
if err = os.RemoveAll(tempDir); err != nil {
|
||||
logrus.Errorf("error removing temporary directory %q: %v", contextDir, err)
|
||||
}
|
||||
}()
|
||||
contextDir = filepath.Join(tempDir, subDir)
|
||||
} else {
|
||||
// Nope, it was local. Use it as is.
|
||||
absDir, err := filepath.Abs(cliArgs[0])
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "error determining path to directory %q", cliArgs[0])
|
||||
}
|
||||
contextDir = absDir
|
||||
}
|
||||
cliArgs = cliArgs.Tail()
|
||||
} else {
|
||||
// No context directory or URL was specified. Try to use the
|
||||
// home of the first locally-available Dockerfile.
|
||||
for i := range dockerfiles {
|
||||
if strings.HasPrefix(dockerfiles[i], "http://") ||
|
||||
strings.HasPrefix(dockerfiles[i], "https://") ||
|
||||
strings.HasPrefix(dockerfiles[i], "git://") ||
|
||||
strings.HasPrefix(dockerfiles[i], "github.com/") {
|
||||
continue
|
||||
}
|
||||
absFile, err := filepath.Abs(dockerfiles[i])
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "error determining path to file %q", dockerfiles[i])
|
||||
}
|
||||
contextDir = filepath.Dir(absFile)
|
||||
dockerfiles[i], err = filepath.Rel(contextDir, absFile)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "error determining path to file %q", dockerfiles[i])
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
if contextDir == "" {
|
||||
return errors.Errorf("no context directory specified, and no dockerfile specified")
|
||||
}
|
||||
if len(dockerfiles) == 0 {
|
||||
dockerfiles = append(dockerfiles, filepath.Join(contextDir, "Dockerfile"))
|
||||
}
|
||||
|
||||
store, err := getStore(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
options := imagebuildah.BuildOptions{
|
||||
ContextDirectory: contextDir,
|
||||
PullPolicy: pullPolicy,
|
||||
Registry: registry,
|
||||
Compression: imagebuildah.Gzip,
|
||||
Quiet: quiet,
|
||||
SignaturePolicyPath: signaturePolicy,
|
||||
Args: args,
|
||||
Output: output,
|
||||
AdditionalTags: tags,
|
||||
Runtime: runtime,
|
||||
RuntimeArgs: runtimeFlags,
|
||||
OutputFormat: format,
|
||||
}
|
||||
if !quiet {
|
||||
options.ReportWriter = os.Stderr
|
||||
}
|
||||
|
||||
return imagebuildah.BuildDockerfiles(store, options, dockerfiles...)
|
||||
}
|
|
@ -1,132 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/containers/image/storage"
|
||||
"github.com/containers/image/transports/alltransports"
|
||||
"github.com/containers/storage/pkg/archive"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/projectatomic/buildah"
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
var (
|
||||
commitFlags = []cli.Flag{
|
||||
cli.BoolFlag{
|
||||
Name: "disable-compression, D",
|
||||
Usage: "don't compress layers",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "signature-policy",
|
||||
Usage: "`pathname` of signature policy file (not usually used)",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "format, f",
|
||||
Usage: "`format` of the image manifest and metadata",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "reference-time",
|
||||
Usage: "set the timestamp on the image to match the named `file`",
|
||||
Hidden: true,
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "quiet, q",
|
||||
Usage: "don't output progress information when writing images",
|
||||
},
|
||||
}
|
||||
commitDescription = "Writes a new image using the container's read-write layer and, if it is based\n on an image, the layers of that image"
|
||||
commitCommand = cli.Command{
|
||||
Name: "commit",
|
||||
Usage: "Create an image from a working container",
|
||||
Description: commitDescription,
|
||||
Flags: commitFlags,
|
||||
Action: commitCmd,
|
||||
ArgsUsage: "CONTAINER-NAME-OR-ID IMAGE",
|
||||
}
|
||||
)
|
||||
|
||||
func commitCmd(c *cli.Context) error {
|
||||
args := c.Args()
|
||||
if len(args) == 0 {
|
||||
return errors.Errorf("container ID must be specified")
|
||||
}
|
||||
name := args[0]
|
||||
args = args.Tail()
|
||||
if len(args) == 0 {
|
||||
return errors.Errorf("an image name must be specified")
|
||||
}
|
||||
if len(args) > 1 {
|
||||
return errors.Errorf("too many arguments specified")
|
||||
}
|
||||
image := args[0]
|
||||
|
||||
signaturePolicy := ""
|
||||
if c.IsSet("signature-policy") {
|
||||
signaturePolicy = c.String("signature-policy")
|
||||
}
|
||||
compress := archive.Uncompressed
|
||||
if !c.IsSet("disable-compression") || !c.Bool("disable-compression") {
|
||||
compress = archive.Gzip
|
||||
}
|
||||
quiet := false
|
||||
if c.IsSet("quiet") {
|
||||
quiet = c.Bool("quiet")
|
||||
}
|
||||
format := "oci"
|
||||
if c.IsSet("format") {
|
||||
format = c.String("format")
|
||||
}
|
||||
timestamp := time.Now().UTC()
|
||||
if c.IsSet("reference-time") {
|
||||
referenceFile := c.String("reference-time")
|
||||
finfo, err := os.Stat(referenceFile)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "error reading timestamp of file %q", referenceFile)
|
||||
}
|
||||
timestamp = finfo.ModTime().UTC()
|
||||
}
|
||||
if strings.HasPrefix(strings.ToLower(format), "oci") {
|
||||
format = buildah.OCIv1ImageManifest
|
||||
} else if strings.HasPrefix(strings.ToLower(format), "docker") {
|
||||
format = buildah.Dockerv2ImageManifest
|
||||
} else {
|
||||
return errors.Errorf("unrecognized image type %q", format)
|
||||
}
|
||||
store, err := getStore(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
builder, err := openBuilder(store, name)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "error reading build container %q", name)
|
||||
}
|
||||
|
||||
dest, err := alltransports.ParseImageName(image)
|
||||
if err != nil {
|
||||
dest2, err2 := storage.Transport.ParseStoreReference(store, image)
|
||||
if err2 != nil {
|
||||
return errors.Wrapf(err, "error parsing target image name %q", image)
|
||||
}
|
||||
dest = dest2
|
||||
}
|
||||
|
||||
options := buildah.CommitOptions{
|
||||
PreferredManifestType: format,
|
||||
Compression: compress,
|
||||
SignaturePolicyPath: signaturePolicy,
|
||||
HistoryTimestamp: ×tamp,
|
||||
}
|
||||
if !quiet {
|
||||
options.ReportWriter = os.Stderr
|
||||
}
|
||||
err = builder.Commit(dest, options)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "error committing container %q to %q", builder.Container, image)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -1,73 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
is "github.com/containers/image/storage"
|
||||
"github.com/containers/storage"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/projectatomic/buildah"
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
var needToShutdownStore = false
|
||||
|
||||
func getStore(c *cli.Context) (storage.Store, error) {
|
||||
options := storage.DefaultStoreOptions
|
||||
if c.GlobalIsSet("root") || c.GlobalIsSet("runroot") {
|
||||
options.GraphRoot = c.GlobalString("root")
|
||||
options.RunRoot = c.GlobalString("runroot")
|
||||
}
|
||||
if c.GlobalIsSet("storage-driver") {
|
||||
options.GraphDriverName = c.GlobalString("storage-driver")
|
||||
}
|
||||
if c.GlobalIsSet("storage-opt") {
|
||||
opts := c.GlobalStringSlice("storage-opt")
|
||||
if len(opts) > 0 {
|
||||
options.GraphDriverOptions = opts
|
||||
}
|
||||
}
|
||||
store, err := storage.GetStore(options)
|
||||
if store != nil {
|
||||
is.Transport.SetStore(store)
|
||||
}
|
||||
needToShutdownStore = true
|
||||
return store, err
|
||||
}
|
||||
|
||||
func openBuilder(store storage.Store, name string) (builder *buildah.Builder, err error) {
|
||||
if name != "" {
|
||||
builder, err = buildah.OpenBuilder(store, name)
|
||||
if os.IsNotExist(err) {
|
||||
options := buildah.ImportOptions{
|
||||
Container: name,
|
||||
}
|
||||
builder, err = buildah.ImportBuilder(store, options)
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "error reading build container")
|
||||
}
|
||||
if builder == nil {
|
||||
return nil, errors.Errorf("error finding build container")
|
||||
}
|
||||
return builder, nil
|
||||
}
|
||||
|
||||
func openBuilders(store storage.Store) (builders []*buildah.Builder, err error) {
|
||||
return buildah.OpenAllBuilders(store)
|
||||
}
|
||||
|
||||
func openImage(store storage.Store, name string) (builder *buildah.Builder, err error) {
|
||||
options := buildah.ImportFromImageOptions{
|
||||
Image: name,
|
||||
}
|
||||
builder, err = buildah.ImportBuilderFromImage(store, options)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "error reading image")
|
||||
}
|
||||
if builder == nil {
|
||||
return nil, errors.Errorf("error mocking up build configuration")
|
||||
}
|
||||
return builder, nil
|
||||
}
|
|
@ -1,187 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/Sirupsen/logrus"
|
||||
"github.com/mattn/go-shellwords"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/projectatomic/buildah"
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
const (
|
||||
// DefaultCreatedBy is the default description of how an image layer
|
||||
// was created that we use when adding to an image's history.
|
||||
DefaultCreatedBy = "manual edits"
|
||||
)
|
||||
|
||||
var (
|
||||
configFlags = []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "author",
|
||||
Usage: "image author contact `information`",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "created-by",
|
||||
Usage: "`description` of how the image was created",
|
||||
Value: DefaultCreatedBy,
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "arch",
|
||||
Usage: "`architecture` of the target image",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "os",
|
||||
Usage: "`operating system` of the target image",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "user, u",
|
||||
Usage: "`user` to run containers based on image as",
|
||||
},
|
||||
cli.StringSliceFlag{
|
||||
Name: "port, p",
|
||||
Usage: "`port` to expose when running containers based on image",
|
||||
},
|
||||
cli.StringSliceFlag{
|
||||
Name: "env, e",
|
||||
Usage: "`environment variable` to set when running containers based on image",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "entrypoint",
|
||||
Usage: "`entry point` for containers based on image",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "cmd",
|
||||
Usage: "`command` for containers based on image",
|
||||
},
|
||||
cli.StringSliceFlag{
|
||||
Name: "volume, v",
|
||||
Usage: "`volume` to create for containers based on image",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "workingdir",
|
||||
Usage: "working `directory` for containers based on image",
|
||||
},
|
||||
cli.StringSliceFlag{
|
||||
Name: "label, l",
|
||||
Usage: "image configuration `label` e.g. label=value",
|
||||
},
|
||||
cli.StringSliceFlag{
|
||||
Name: "annotation, a",
|
||||
Usage: "`annotation` e.g. annotation=value, for the target image",
|
||||
},
|
||||
}
|
||||
configDescription = "Modifies the configuration values which will be saved to the image"
|
||||
configCommand = cli.Command{
|
||||
Name: "config",
|
||||
Usage: "Update image configuration settings",
|
||||
Description: configDescription,
|
||||
Flags: configFlags,
|
||||
Action: configCmd,
|
||||
ArgsUsage: "CONTAINER-NAME-OR-ID",
|
||||
}
|
||||
)
|
||||
|
||||
func updateConfig(builder *buildah.Builder, c *cli.Context) {
|
||||
if c.IsSet("author") {
|
||||
builder.SetMaintainer(c.String("author"))
|
||||
}
|
||||
if c.IsSet("created-by") {
|
||||
builder.SetCreatedBy(c.String("created-by"))
|
||||
}
|
||||
if c.IsSet("arch") {
|
||||
builder.SetArchitecture(c.String("arch"))
|
||||
}
|
||||
if c.IsSet("os") {
|
||||
builder.SetOS(c.String("os"))
|
||||
}
|
||||
if c.IsSet("user") {
|
||||
builder.SetUser(c.String("user"))
|
||||
}
|
||||
if c.IsSet("port") || c.IsSet("p") {
|
||||
for _, portSpec := range c.StringSlice("port") {
|
||||
builder.SetPort(portSpec)
|
||||
}
|
||||
}
|
||||
if c.IsSet("env") || c.IsSet("e") {
|
||||
for _, envSpec := range c.StringSlice("env") {
|
||||
env := strings.SplitN(envSpec, "=", 2)
|
||||
if len(env) > 1 {
|
||||
builder.SetEnv(env[0], env[1])
|
||||
} else {
|
||||
builder.UnsetEnv(env[0])
|
||||
}
|
||||
}
|
||||
}
|
||||
if c.IsSet("entrypoint") {
|
||||
entrypointSpec, err := shellwords.Parse(c.String("entrypoint"))
|
||||
if err != nil {
|
||||
logrus.Errorf("error parsing --entrypoint %q: %v", c.String("entrypoint"), err)
|
||||
} else {
|
||||
builder.SetEntrypoint(entrypointSpec)
|
||||
}
|
||||
}
|
||||
if c.IsSet("cmd") {
|
||||
cmdSpec, err := shellwords.Parse(c.String("cmd"))
|
||||
if err != nil {
|
||||
logrus.Errorf("error parsing --cmd %q: %v", c.String("cmd"), err)
|
||||
} else {
|
||||
builder.SetCmd(cmdSpec)
|
||||
}
|
||||
}
|
||||
if c.IsSet("volume") {
|
||||
if volSpec := c.StringSlice("volume"); len(volSpec) > 0 {
|
||||
for _, spec := range volSpec {
|
||||
builder.AddVolume(spec)
|
||||
}
|
||||
}
|
||||
}
|
||||
if c.IsSet("label") || c.IsSet("l") {
|
||||
for _, labelSpec := range c.StringSlice("label") {
|
||||
label := strings.SplitN(labelSpec, "=", 2)
|
||||
if len(label) > 1 {
|
||||
builder.SetLabel(label[0], label[1])
|
||||
} else {
|
||||
builder.UnsetLabel(label[0])
|
||||
}
|
||||
}
|
||||
}
|
||||
if c.IsSet("workingdir") {
|
||||
builder.SetWorkDir(c.String("workingdir"))
|
||||
}
|
||||
if c.IsSet("annotation") || c.IsSet("a") {
|
||||
for _, annotationSpec := range c.StringSlice("annotation") {
|
||||
annotation := strings.SplitN(annotationSpec, "=", 2)
|
||||
if len(annotation) > 1 {
|
||||
builder.SetAnnotation(annotation[0], annotation[1])
|
||||
} else {
|
||||
builder.UnsetAnnotation(annotation[0])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func configCmd(c *cli.Context) error {
|
||||
args := c.Args()
|
||||
if len(args) == 0 {
|
||||
return errors.Errorf("container ID must be specified")
|
||||
}
|
||||
if len(args) > 1 {
|
||||
return errors.Errorf("too many arguments specified")
|
||||
}
|
||||
name := args[0]
|
||||
|
||||
store, err := getStore(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
builder, err := openBuilder(store, name)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "error reading build container %q", name)
|
||||
}
|
||||
|
||||
updateConfig(builder, c)
|
||||
return builder.Save()
|
||||
}
|
|
@ -1,83 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/projectatomic/buildah"
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
var (
|
||||
containersFlags = []cli.Flag{
|
||||
cli.BoolFlag{
|
||||
Name: "quiet, q",
|
||||
Usage: "display only container IDs",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "noheading, n",
|
||||
Usage: "do not print column headings",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "notruncate",
|
||||
Usage: "do not truncate output",
|
||||
},
|
||||
}
|
||||
containersDescription = "Lists containers which appear to be " + buildah.Package + " working containers, their\n names and IDs, and the names and IDs of the images from which they were\n initialized"
|
||||
containersCommand = cli.Command{
|
||||
Name: "containers",
|
||||
Usage: "List working containers and their base images",
|
||||
Description: containersDescription,
|
||||
Flags: containersFlags,
|
||||
Action: containersCmd,
|
||||
ArgsUsage: " ",
|
||||
}
|
||||
)
|
||||
|
||||
func containersCmd(c *cli.Context) error {
|
||||
store, err := getStore(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
quiet := false
|
||||
if c.IsSet("quiet") {
|
||||
quiet = c.Bool("quiet")
|
||||
}
|
||||
noheading := false
|
||||
if c.IsSet("noheading") {
|
||||
noheading = c.Bool("noheading")
|
||||
}
|
||||
truncate := true
|
||||
if c.IsSet("notruncate") {
|
||||
truncate = !c.Bool("notruncate")
|
||||
}
|
||||
|
||||
builders, err := openBuilders(store)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "error reading build containers")
|
||||
}
|
||||
if len(builders) > 0 && !noheading && !quiet {
|
||||
if truncate {
|
||||
fmt.Printf("%-12s %-12s %-10s %s\n", "CONTAINER ID", "IMAGE ID", "IMAGE NAME", "CONTAINER NAME")
|
||||
} else {
|
||||
fmt.Printf("%-64s %-64s %-10s %s\n", "CONTAINER ID", "IMAGE ID", "IMAGE NAME", "CONTAINER NAME")
|
||||
}
|
||||
}
|
||||
for _, builder := range builders {
|
||||
if builder.FromImage == "" {
|
||||
builder.FromImage = buildah.BaseImageFakeName
|
||||
}
|
||||
if quiet {
|
||||
fmt.Printf("%s\n", builder.ContainerID)
|
||||
} else {
|
||||
if truncate {
|
||||
fmt.Printf("%-12.12s %-12.12s %-10s %s\n", builder.ContainerID, builder.FromImageID, builder.FromImage, builder.Container)
|
||||
} else {
|
||||
fmt.Printf("%-64s %-64s %-10s %s\n", builder.ContainerID, builder.FromImageID, builder.FromImage, builder.Container)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -1,128 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/projectatomic/buildah"
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
const (
|
||||
// DefaultRegistry is a prefix that we apply to an image name if we
|
||||
// can't find one in the local Store, in order to generate a source
|
||||
// reference for the image that we can then copy to the local Store.
|
||||
DefaultRegistry = "docker://"
|
||||
)
|
||||
|
||||
var (
|
||||
fromFlags = []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "name",
|
||||
Usage: "`name` for the working container",
|
||||
},
|
||||
cli.BoolTFlag{
|
||||
Name: "pull",
|
||||
Usage: "pull the image if not present",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "pull-always",
|
||||
Usage: "pull the image even if one with the same name is already present",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "registry",
|
||||
Usage: "`prefix` to prepend to the image name in order to pull the image",
|
||||
Value: DefaultRegistry,
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "signature-policy",
|
||||
Usage: "`pathname` of signature policy file (not usually used)",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "quiet, q",
|
||||
Usage: "don't output progress information when pulling images",
|
||||
},
|
||||
}
|
||||
fromDescription = "Creates a new working container, either from scratch or using a specified\n image as a starting point"
|
||||
|
||||
fromCommand = cli.Command{
|
||||
Name: "from",
|
||||
Usage: "Create a working container based on an image",
|
||||
Description: fromDescription,
|
||||
Flags: fromFlags,
|
||||
Action: fromCmd,
|
||||
ArgsUsage: "IMAGE",
|
||||
}
|
||||
)
|
||||
|
||||
func fromCmd(c *cli.Context) error {
|
||||
|
||||
args := c.Args()
|
||||
if len(args) == 0 {
|
||||
return errors.Errorf("an image name (or \"scratch\") must be specified")
|
||||
}
|
||||
if len(args) > 1 {
|
||||
return errors.Errorf("too many arguments specified")
|
||||
}
|
||||
image := args[0]
|
||||
|
||||
registry := DefaultRegistry
|
||||
if c.IsSet("registry") {
|
||||
registry = c.String("registry")
|
||||
}
|
||||
pull := true
|
||||
if c.IsSet("pull") {
|
||||
pull = c.BoolT("pull")
|
||||
}
|
||||
pullAlways := false
|
||||
if c.IsSet("pull-always") {
|
||||
pull = c.Bool("pull-always")
|
||||
}
|
||||
|
||||
pullPolicy := buildah.PullNever
|
||||
if pull {
|
||||
pullPolicy = buildah.PullIfMissing
|
||||
}
|
||||
if pullAlways {
|
||||
pullPolicy = buildah.PullAlways
|
||||
}
|
||||
|
||||
name := ""
|
||||
if c.IsSet("name") {
|
||||
name = c.String("name")
|
||||
}
|
||||
signaturePolicy := ""
|
||||
if c.IsSet("signature-policy") {
|
||||
signaturePolicy = c.String("signature-policy")
|
||||
}
|
||||
|
||||
quiet := false
|
||||
if c.IsSet("quiet") {
|
||||
quiet = c.Bool("quiet")
|
||||
}
|
||||
|
||||
store, err := getStore(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
options := buildah.BuilderOptions{
|
||||
FromImage: image,
|
||||
Container: name,
|
||||
PullPolicy: pullPolicy,
|
||||
Registry: registry,
|
||||
SignaturePolicyPath: signaturePolicy,
|
||||
}
|
||||
if !quiet {
|
||||
options.ReportWriter = os.Stderr
|
||||
}
|
||||
|
||||
builder, err := buildah.NewBuilder(store, options)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Printf("%s\n", builder.Container)
|
||||
return builder.Save()
|
||||
}
|
|
@ -1,85 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
var (
|
||||
imagesFlags = []cli.Flag{
|
||||
cli.BoolFlag{
|
||||
Name: "quiet, q",
|
||||
Usage: "display only image IDs",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "noheading, n",
|
||||
Usage: "do not print column headings",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "notruncate",
|
||||
Usage: "do not truncate output",
|
||||
},
|
||||
}
|
||||
imagesDescription = "Lists locally stored images."
|
||||
imagesCommand = cli.Command{
|
||||
Name: "images",
|
||||
Usage: "List images in local storage",
|
||||
Description: imagesDescription,
|
||||
Flags: imagesFlags,
|
||||
Action: imagesCmd,
|
||||
ArgsUsage: " ",
|
||||
}
|
||||
)
|
||||
|
||||
func imagesCmd(c *cli.Context) error {
|
||||
store, err := getStore(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
quiet := false
|
||||
if c.IsSet("quiet") {
|
||||
quiet = c.Bool("quiet")
|
||||
}
|
||||
noheading := false
|
||||
if c.IsSet("noheading") {
|
||||
noheading = c.Bool("noheading")
|
||||
}
|
||||
truncate := true
|
||||
if c.IsSet("notruncate") {
|
||||
truncate = !c.Bool("notruncate")
|
||||
}
|
||||
images, err := store.Images()
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "error reading images")
|
||||
}
|
||||
|
||||
if len(images) > 0 && !noheading && !quiet {
|
||||
if truncate {
|
||||
fmt.Printf("%-12s %s\n", "IMAGE ID", "IMAGE NAME")
|
||||
} else {
|
||||
fmt.Printf("%-64s %s\n", "IMAGE ID", "IMAGE NAME")
|
||||
}
|
||||
}
|
||||
for _, image := range images {
|
||||
if quiet {
|
||||
fmt.Printf("%s\n", image.ID)
|
||||
continue
|
||||
}
|
||||
names := []string{""}
|
||||
if len(image.Names) > 0 {
|
||||
names = image.Names
|
||||
}
|
||||
for _, name := range names {
|
||||
if truncate {
|
||||
fmt.Printf("%-12.12s %s\n", image.ID, name)
|
||||
} else {
|
||||
fmt.Printf("%-64s %s\n", image.ID, name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -1,104 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"text/template"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/projectatomic/buildah"
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultFormat = `Container: {{.Container}}
|
||||
ID: {{.ContainerID}}
|
||||
`
|
||||
inspectTypeContainer = "container"
|
||||
inspectTypeImage = "image"
|
||||
)
|
||||
|
||||
var (
|
||||
inspectFlags = []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "type, t",
|
||||
Usage: "look at the item of the specified `type` (container or image) and name",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "format, f",
|
||||
Usage: "use `format` as a Go template to format the output",
|
||||
},
|
||||
}
|
||||
inspectDescription = "Inspects a build container's or built image's configuration."
|
||||
inspectCommand = cli.Command{
|
||||
Name: "inspect",
|
||||
Usage: "Inspects the configuration of a container or image",
|
||||
Description: inspectDescription,
|
||||
Flags: inspectFlags,
|
||||
Action: inspectCmd,
|
||||
ArgsUsage: "CONTAINER-OR-IMAGE",
|
||||
}
|
||||
)
|
||||
|
||||
func inspectCmd(c *cli.Context) error {
|
||||
var builder *buildah.Builder
|
||||
|
||||
args := c.Args()
|
||||
if len(args) == 0 {
|
||||
return errors.Errorf("container or image name must be specified")
|
||||
}
|
||||
if len(args) > 1 {
|
||||
return errors.Errorf("too many arguments specified")
|
||||
}
|
||||
|
||||
itemType := inspectTypeContainer
|
||||
if c.IsSet("type") {
|
||||
itemType = c.String("type")
|
||||
}
|
||||
switch itemType {
|
||||
case inspectTypeContainer:
|
||||
case inspectTypeImage:
|
||||
default:
|
||||
return errors.Errorf("the only recognized types are %q and %q", inspectTypeContainer, inspectTypeImage)
|
||||
}
|
||||
|
||||
format := defaultFormat
|
||||
if c.IsSet("format") {
|
||||
if c.String("format") != "" {
|
||||
format = c.String("format")
|
||||
}
|
||||
}
|
||||
t := template.Must(template.New("format").Parse(format))
|
||||
|
||||
name := args[0]
|
||||
|
||||
store, err := getStore(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch itemType {
|
||||
case inspectTypeContainer:
|
||||
builder, err = openBuilder(store, name)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "error reading build container %q", name)
|
||||
}
|
||||
case inspectTypeImage:
|
||||
builder, err = openImage(store, name)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "error reading image %q", name)
|
||||
}
|
||||
}
|
||||
|
||||
if c.IsSet("format") {
|
||||
return t.Execute(os.Stdout, builder)
|
||||
}
|
||||
|
||||
b, err := json.MarshalIndent(builder, "", " ")
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "error encoding build container as json")
|
||||
}
|
||||
_, err = fmt.Println(string(b))
|
||||
return err
|
||||
}
|
|
@ -1,97 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/Sirupsen/logrus"
|
||||
"github.com/containers/storage"
|
||||
ispecs "github.com/opencontainers/image-spec/specs-go"
|
||||
rspecs "github.com/opencontainers/runtime-spec/specs-go"
|
||||
"github.com/projectatomic/buildah"
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
func main() {
|
||||
var defaultStoreDriverOptions *cli.StringSlice
|
||||
if buildah.InitReexec() {
|
||||
return
|
||||
}
|
||||
|
||||
app := cli.NewApp()
|
||||
app.Name = buildah.Package
|
||||
app.Version = fmt.Sprintf("%s (image-spec %s, runtime-spec %s)", buildah.Version, ispecs.Version, rspecs.Version)
|
||||
app.Usage = "an image builder"
|
||||
if len(storage.DefaultStoreOptions.GraphDriverOptions) > 0 {
|
||||
var optionSlice cli.StringSlice = storage.DefaultStoreOptions.GraphDriverOptions[:]
|
||||
defaultStoreDriverOptions = &optionSlice
|
||||
}
|
||||
app.Flags = []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "root",
|
||||
Usage: "storage root dir",
|
||||
Value: storage.DefaultStoreOptions.GraphRoot,
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "runroot",
|
||||
Usage: "storage state dir",
|
||||
Value: storage.DefaultStoreOptions.RunRoot,
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "storage-driver",
|
||||
Usage: "storage driver",
|
||||
Value: storage.DefaultStoreOptions.GraphDriverName,
|
||||
},
|
||||
cli.StringSliceFlag{
|
||||
Name: "storage-opt",
|
||||
Usage: "storage driver option",
|
||||
Value: defaultStoreDriverOptions,
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "debug",
|
||||
Usage: "print debugging information",
|
||||
},
|
||||
}
|
||||
app.Before = func(c *cli.Context) error {
|
||||
logrus.SetLevel(logrus.ErrorLevel)
|
||||
if c.GlobalIsSet("debug") {
|
||||
if c.GlobalBool("debug") {
|
||||
logrus.SetLevel(logrus.DebugLevel)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
app.After = func(c *cli.Context) error {
|
||||
if needToShutdownStore {
|
||||
store, err := getStore(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, _ = store.Shutdown(false)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
app.Commands = []cli.Command{
|
||||
addCommand,
|
||||
budCommand,
|
||||
commitCommand,
|
||||
configCommand,
|
||||
containersCommand,
|
||||
copyCommand,
|
||||
fromCommand,
|
||||
imagesCommand,
|
||||
inspectCommand,
|
||||
mountCommand,
|
||||
pushCommand,
|
||||
rmCommand,
|
||||
rmiCommand,
|
||||
runCommand,
|
||||
tagCommand,
|
||||
umountCommand,
|
||||
}
|
||||
err := app.Run(os.Args)
|
||||
if err != nil {
|
||||
logrus.Errorf("%v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
|
@ -1,71 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
var (
|
||||
mountDescription = "Mounts a working container's root filesystem for manipulation"
|
||||
mountFlags = []cli.Flag{
|
||||
cli.BoolFlag{
|
||||
Name: "notruncate",
|
||||
Usage: "do not truncate output",
|
||||
},
|
||||
}
|
||||
mountCommand = cli.Command{
|
||||
Name: "mount",
|
||||
Usage: "Mount a working container's root filesystem",
|
||||
Description: mountDescription,
|
||||
Action: mountCmd,
|
||||
ArgsUsage: "CONTAINER-NAME-OR-ID",
|
||||
Flags: mountFlags,
|
||||
}
|
||||
)
|
||||
|
||||
func mountCmd(c *cli.Context) error {
|
||||
args := c.Args()
|
||||
if len(args) > 1 {
|
||||
return errors.Errorf("too many arguments specified")
|
||||
}
|
||||
|
||||
store, err := getStore(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
truncate := true
|
||||
if c.IsSet("notruncate") {
|
||||
truncate = !c.Bool("notruncate")
|
||||
}
|
||||
|
||||
if len(args) == 1 {
|
||||
name := args[0]
|
||||
builder, err := openBuilder(store, name)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "error reading build container %q", name)
|
||||
}
|
||||
mountPoint, err := builder.Mount("")
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "error mounting %q container %q", name, builder.Container)
|
||||
}
|
||||
fmt.Printf("%s\n", mountPoint)
|
||||
} else {
|
||||
builders, err := openBuilders(store)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "error reading build containers")
|
||||
}
|
||||
for _, builder := range builders {
|
||||
if builder.MountPoint == "" {
|
||||
continue
|
||||
}
|
||||
if truncate {
|
||||
fmt.Printf("%-12.12s %s\n", builder.ContainerID, builder.MountPoint)
|
||||
} else {
|
||||
fmt.Printf("%-64s %s\n", builder.ContainerID, builder.MountPoint)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
|
@ -1,84 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/containers/image/transports/alltransports"
|
||||
"github.com/containers/storage/pkg/archive"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/projectatomic/buildah"
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
var (
|
||||
pushFlags = []cli.Flag{
|
||||
cli.BoolFlag{
|
||||
Name: "disable-compression, D",
|
||||
Usage: "don't compress layers",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "signature-policy",
|
||||
Usage: "`pathname` of signature policy file (not usually used)",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "quiet, q",
|
||||
Usage: "don't output progress information when pushing images",
|
||||
},
|
||||
}
|
||||
pushDescription = "Pushes an image to a specified location."
|
||||
pushCommand = cli.Command{
|
||||
Name: "push",
|
||||
Usage: "Push an image to a specified location",
|
||||
Description: pushDescription,
|
||||
Flags: pushFlags,
|
||||
Action: pushCmd,
|
||||
ArgsUsage: "IMAGE [TRANSPORT:]IMAGE",
|
||||
}
|
||||
)
|
||||
|
||||
func pushCmd(c *cli.Context) error {
|
||||
args := c.Args()
|
||||
if len(args) < 2 {
|
||||
return errors.New("source and destination image IDs must be specified")
|
||||
}
|
||||
src := args[0]
|
||||
destSpec := args[1]
|
||||
|
||||
signaturePolicy := ""
|
||||
if c.IsSet("signature-policy") {
|
||||
signaturePolicy = c.String("signature-policy")
|
||||
}
|
||||
compress := archive.Uncompressed
|
||||
if !c.IsSet("disable-compression") || !c.Bool("disable-compression") {
|
||||
compress = archive.Gzip
|
||||
}
|
||||
quiet := false
|
||||
if c.IsSet("quiet") {
|
||||
quiet = c.Bool("quiet")
|
||||
}
|
||||
|
||||
store, err := getStore(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dest, err := alltransports.ParseImageName(destSpec)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
options := buildah.PushOptions{
|
||||
Compression: compress,
|
||||
SignaturePolicyPath: signaturePolicy,
|
||||
Store: store,
|
||||
}
|
||||
if !quiet {
|
||||
options.ReportWriter = os.Stderr
|
||||
}
|
||||
|
||||
err = buildah.Push(src, dest, options)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "error pushing image %q to %q", src, destSpec)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -1,57 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
var (
|
||||
rmDescription = "Removes one or more working containers, unmounting them if necessary"
|
||||
rmCommand = cli.Command{
|
||||
Name: "rm",
|
||||
Aliases: []string{"delete"},
|
||||
Usage: "Remove one or more working containers",
|
||||
Description: rmDescription,
|
||||
Action: rmCmd,
|
||||
ArgsUsage: "CONTAINER-NAME-OR-ID [...]",
|
||||
}
|
||||
)
|
||||
|
||||
func rmCmd(c *cli.Context) error {
|
||||
args := c.Args()
|
||||
if len(args) == 0 {
|
||||
return errors.Errorf("container ID must be specified")
|
||||
}
|
||||
store, err := getStore(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var e error
|
||||
for _, name := range args {
|
||||
builder, err := openBuilder(store, name)
|
||||
if e == nil {
|
||||
e = err
|
||||
}
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "error reading build container %q: %v\n", name, err)
|
||||
continue
|
||||
}
|
||||
|
||||
id := builder.ContainerID
|
||||
err = builder.Delete()
|
||||
if e == nil {
|
||||
e = err
|
||||
}
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "error removing container %q: %v\n", builder.Container, err)
|
||||
continue
|
||||
}
|
||||
fmt.Printf("%s\n", id)
|
||||
}
|
||||
|
||||
return e
|
||||
}
|
|
@ -1,109 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/Sirupsen/logrus"
|
||||
"github.com/containers/image/storage"
|
||||
"github.com/containers/image/transports"
|
||||
"github.com/containers/image/transports/alltransports"
|
||||
"github.com/containers/image/types"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
var (
|
||||
rmiDescription = "Removes one or more locally stored images."
|
||||
rmiCommand = cli.Command{
|
||||
Name: "rmi",
|
||||
Usage: "Removes one or more images from local storage",
|
||||
Description: rmiDescription,
|
||||
Action: rmiCmd,
|
||||
ArgsUsage: "IMAGE-NAME-OR-ID [...]",
|
||||
}
|
||||
)
|
||||
|
||||
func rmiCmd(c *cli.Context) error {
|
||||
args := c.Args()
|
||||
if len(args) == 0 {
|
||||
return errors.Errorf("image name or ID must be specified")
|
||||
}
|
||||
|
||||
store, err := getStore(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var e error
|
||||
for _, id := range args {
|
||||
// If it's an exact name or ID match with the underlying
|
||||
// storage library's information about the image, then it's
|
||||
// enough.
|
||||
_, err = store.DeleteImage(id, true)
|
||||
if err != nil {
|
||||
var ref types.ImageReference
|
||||
// If it's looks like a proper image reference, parse
|
||||
// it and check if it corresponds to an image that
|
||||
// actually exists.
|
||||
if ref2, err2 := alltransports.ParseImageName(id); err2 == nil {
|
||||
if img, err3 := ref2.NewImage(nil); err3 == nil {
|
||||
img.Close()
|
||||
ref = ref2
|
||||
} else {
|
||||
logrus.Debugf("error confirming presence of image %q: %v", transports.ImageName(ref2), err3)
|
||||
}
|
||||
} else {
|
||||
logrus.Debugf("error parsing %q as an image reference: %v", id, err2)
|
||||
}
|
||||
if ref == nil {
|
||||
// If it's looks like an image reference that's
|
||||
// relative to our storage, parse it and check
|
||||
// if it corresponds to an image that actually
|
||||
// exists.
|
||||
if ref2, err2 := storage.Transport.ParseStoreReference(store, id); err2 == nil {
|
||||
if img, err3 := ref2.NewImage(nil); err3 == nil {
|
||||
img.Close()
|
||||
ref = ref2
|
||||
} else {
|
||||
logrus.Debugf("error confirming presence of image %q: %v", transports.ImageName(ref2), err3)
|
||||
}
|
||||
} else {
|
||||
logrus.Debugf("error parsing %q as a store reference: %v", id, err2)
|
||||
}
|
||||
}
|
||||
if ref == nil {
|
||||
// If it might be an ID that's relative to our
|
||||
// storage, parse it and check if it
|
||||
// corresponds to an image that actually
|
||||
// exists. This _should_ be redundant, since
|
||||
// we already tried deleting the image using
|
||||
// the ID directly above, but it can't hurt,
|
||||
// either.
|
||||
if ref2, err2 := storage.Transport.ParseStoreReference(store, "@"+id); err2 == nil {
|
||||
if img, err3 := ref2.NewImage(nil); err3 == nil {
|
||||
img.Close()
|
||||
ref = ref2
|
||||
} else {
|
||||
logrus.Debugf("error confirming presence of image %q: %v", transports.ImageName(ref2), err3)
|
||||
}
|
||||
} else {
|
||||
logrus.Debugf("error parsing %q as an image reference: %v", "@"+id, err2)
|
||||
}
|
||||
}
|
||||
if ref != nil {
|
||||
err = ref.DeleteImage(nil)
|
||||
}
|
||||
}
|
||||
if e == nil {
|
||||
e = err
|
||||
}
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "error removing image %q: %v\n", id, err)
|
||||
continue
|
||||
}
|
||||
fmt.Printf("%s\n", id)
|
||||
}
|
||||
|
||||
return e
|
||||
}
|
|
@ -1,110 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"github.com/Sirupsen/logrus"
|
||||
specs "github.com/opencontainers/runtime-spec/specs-go"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/projectatomic/buildah"
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
var (
|
||||
runFlags = []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "runtime",
|
||||
Usage: "`path` to an alternate runtime",
|
||||
Value: buildah.DefaultRuntime,
|
||||
},
|
||||
cli.StringSliceFlag{
|
||||
Name: "runtime-flag",
|
||||
Usage: "add global flags for the container runtime",
|
||||
},
|
||||
cli.StringSliceFlag{
|
||||
Name: "volume, v",
|
||||
Usage: "bind mount a host location into the container while running the command",
|
||||
},
|
||||
}
|
||||
runDescription = "Runs a specified command using the container's root filesystem as a root\n filesystem, using configuration settings inherited from the container's\n image or as specified using previous calls to the config command"
|
||||
runCommand = cli.Command{
|
||||
Name: "run",
|
||||
Usage: "Run a command inside of the container",
|
||||
Description: runDescription,
|
||||
Flags: runFlags,
|
||||
Action: runCmd,
|
||||
ArgsUsage: "CONTAINER-NAME-OR-ID COMMAND [ARGS [...]]",
|
||||
}
|
||||
)
|
||||
|
||||
func runCmd(c *cli.Context) error {
|
||||
args := c.Args()
|
||||
if len(args) == 0 {
|
||||
return errors.Errorf("container ID must be specified")
|
||||
}
|
||||
name := args[0]
|
||||
args = args.Tail()
|
||||
|
||||
runtime := ""
|
||||
if c.IsSet("runtime") {
|
||||
runtime = c.String("runtime")
|
||||
}
|
||||
flags := []string{}
|
||||
if c.IsSet("runtime-flag") {
|
||||
flags = c.StringSlice("runtime-flag")
|
||||
}
|
||||
volumes := []string{}
|
||||
if c.IsSet("v") || c.IsSet("volume") {
|
||||
volumes = c.StringSlice("volume")
|
||||
}
|
||||
|
||||
store, err := getStore(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
builder, err := openBuilder(store, name)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "error reading build container %q", name)
|
||||
}
|
||||
|
||||
hostname := ""
|
||||
if c.IsSet("hostname") {
|
||||
hostname = c.String("hostname")
|
||||
}
|
||||
options := buildah.RunOptions{
|
||||
Hostname: hostname,
|
||||
Runtime: runtime,
|
||||
Args: flags,
|
||||
}
|
||||
for _, volumeSpec := range volumes {
|
||||
volSpec := strings.Split(volumeSpec, ":")
|
||||
if len(volSpec) >= 2 {
|
||||
mountOptions := "bind"
|
||||
if len(volSpec) >= 3 {
|
||||
mountOptions = mountOptions + "," + volSpec[2]
|
||||
}
|
||||
mountOpts := strings.Split(mountOptions, ",")
|
||||
mount := specs.Mount{
|
||||
Source: volSpec[0],
|
||||
Destination: volSpec[1],
|
||||
Type: "bind",
|
||||
Options: mountOpts,
|
||||
}
|
||||
options.Mounts = append(options.Mounts, mount)
|
||||
}
|
||||
}
|
||||
runerr := builder.Run(args, options)
|
||||
if runerr != nil {
|
||||
logrus.Debugf("error running %v in container %q: %v", args, builder.Container, runerr)
|
||||
}
|
||||
if ee, ok := runerr.(*exec.ExitError); ok {
|
||||
if w, ok := ee.Sys().(syscall.WaitStatus); ok {
|
||||
os.Exit(w.ExitStatus())
|
||||
}
|
||||
}
|
||||
return runerr
|
||||
}
|
|
@ -1,38 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"github.com/pkg/errors"
|
||||
"github.com/projectatomic/buildah/util"
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
var (
|
||||
tagDescription = "Adds one or more additional names to locally-stored image"
|
||||
tagCommand = cli.Command{
|
||||
Name: "tag",
|
||||
Usage: "Add an additional name to a local image",
|
||||
Description: tagDescription,
|
||||
Action: tagCmd,
|
||||
ArgsUsage: "IMAGE-NAME [IMAGE-NAME ...]",
|
||||
}
|
||||
)
|
||||
|
||||
func tagCmd(c *cli.Context) error {
|
||||
args := c.Args()
|
||||
if len(args) < 2 {
|
||||
return errors.Errorf("image name and at least one new name must be specified")
|
||||
}
|
||||
store, err := getStore(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
img, err := util.FindImage(store, args[0])
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "error finding local image %q", args[0])
|
||||
}
|
||||
err = util.AddImageNames(store, img, args[1:])
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "error adding names %v to image %q", args[1:], args[0])
|
||||
}
|
||||
return nil
|
||||
}
|
|
@ -1,45 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"github.com/pkg/errors"
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
var (
|
||||
umountCommand = cli.Command{
|
||||
Name: "umount",
|
||||
Aliases: []string{"unmount"},
|
||||
Usage: "Unmount a working container's root filesystem",
|
||||
Description: "Unmounts a working container's root filesystem",
|
||||
Action: umountCmd,
|
||||
ArgsUsage: "CONTAINER-NAME-OR-ID",
|
||||
}
|
||||
)
|
||||
|
||||
func umountCmd(c *cli.Context) error {
|
||||
args := c.Args()
|
||||
if len(args) == 0 {
|
||||
return errors.Errorf("container ID must be specified")
|
||||
}
|
||||
if len(args) > 1 {
|
||||
return errors.Errorf("too many arguments specified")
|
||||
}
|
||||
name := args[0]
|
||||
|
||||
store, err := getStore(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
builder, err := openBuilder(store, name)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "error reading build container %q", name)
|
||||
}
|
||||
|
||||
err = builder.Unmount()
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "error unmounting container %q", builder.Container)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,60 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"go.podman.io/image/v5/directory"
|
||||
"go.podman.io/image/v5/docker"
|
||||
dockerArchive "go.podman.io/image/v5/docker/archive"
|
||||
ociArchive "go.podman.io/image/v5/oci/archive"
|
||||
oci "go.podman.io/image/v5/oci/layout"
|
||||
"go.podman.io/image/v5/sif"
|
||||
"go.podman.io/image/v5/tarball"
|
||||
"go.podman.io/image/v5/transports"
|
||||
)
|
||||
|
||||
func autocompleteImageNames(cmd *cobra.Command, args []string, toComplete string) ([]cobra.Completion, cobra.ShellCompDirective) {
|
||||
transport, details, haveTransport := strings.Cut(toComplete, ":")
|
||||
if !haveTransport {
|
||||
transports := supportedTransportSuggestions()
|
||||
return transports, cobra.ShellCompDirectiveNoSpace | cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
switch transport {
|
||||
case ociArchive.Transport.Name(), dockerArchive.Transport.Name():
|
||||
// Can have [:{*reference|@source-index}]
|
||||
// FIXME: `oci-archive:/path/to/a.oci:<TAB>` completes paths
|
||||
return nil, cobra.ShellCompDirectiveNoSpace
|
||||
case sif.Transport.Name():
|
||||
return nil, cobra.ShellCompDirectiveDefault
|
||||
|
||||
// Both directory and oci should have ShellCompDirectiveFilterDirs to complete only directories, but it doesn't currently work in bash: https://github.com/spf13/cobra/issues/2242
|
||||
case oci.Transport.Name():
|
||||
// Can have '[:{reference|@source-index}]'
|
||||
// FIXME: `oci:/path/to/dir/:<TAB>` completes paths
|
||||
return nil, cobra.ShellCompDirectiveDefault | cobra.ShellCompDirectiveNoSpace
|
||||
case directory.Transport.Name():
|
||||
return nil, cobra.ShellCompDirectiveDefault
|
||||
|
||||
case docker.Transport.Name():
|
||||
if details == "" {
|
||||
return []cobra.Completion{transport + "://"}, cobra.ShellCompDirectiveNoSpace | cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
}
|
||||
return nil, cobra.ShellCompDirectiveNoSpace | cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
|
||||
// supportedTransportSuggestions list all supported transports with the colon suffix.
|
||||
func supportedTransportSuggestions() []string {
|
||||
tps := transports.ListNames()
|
||||
suggestions := make([]cobra.Completion, 0, len(tps))
|
||||
for _, tp := range tps {
|
||||
// ListNames is generally expected to filter out deprecated transports.
|
||||
// tarball: is not deprecated, but it is only usable from a Go caller (using tarball.ConfigUpdater),
|
||||
// so don’t offer it on the CLI.
|
||||
if tp != tarball.Transport.Name() {
|
||||
suggestions = append(suggestions, tp+":")
|
||||
}
|
||||
}
|
||||
return suggestions
|
||||
}
|
|
@ -0,0 +1,259 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
encconfig "github.com/containers/ocicrypt/config"
|
||||
enchelpers "github.com/containers/ocicrypt/helpers"
|
||||
"github.com/spf13/cobra"
|
||||
commonFlag "go.podman.io/common/pkg/flag"
|
||||
"go.podman.io/common/pkg/retry"
|
||||
"go.podman.io/image/v5/copy"
|
||||
"go.podman.io/image/v5/docker/reference"
|
||||
"go.podman.io/image/v5/manifest"
|
||||
"go.podman.io/image/v5/transports"
|
||||
"go.podman.io/image/v5/transports/alltransports"
|
||||
)
|
||||
|
||||
type copyOptions struct {
|
||||
global *globalOptions
|
||||
deprecatedTLSVerify *deprecatedTLSVerifyOption
|
||||
srcImage *imageOptions
|
||||
destImage *imageDestOptions
|
||||
retryOpts *retry.Options
|
||||
copy *sharedCopyOptions
|
||||
additionalTags []string // For docker-archive: destinations, in addition to the name:tag specified as destination, also add these
|
||||
signIdentity string // Identity of the signed image, must be a fully specified docker reference
|
||||
digestFile string // Write digest to this file
|
||||
quiet bool // Suppress output information when copying images
|
||||
all bool // Copy all of the images if the source is a list
|
||||
multiArch commonFlag.OptionalString // How to handle multi architecture images
|
||||
encryptLayer []int // The list of layers to encrypt
|
||||
encryptionKeys []string // Keys needed to encrypt the image
|
||||
decryptionKeys []string // Keys needed to decrypt the image
|
||||
imageParallelCopies uint // Maximum number of parallel requests when copying images
|
||||
}
|
||||
|
||||
func copyCmd(global *globalOptions) *cobra.Command {
|
||||
sharedFlags, sharedOpts := sharedImageFlags()
|
||||
deprecatedTLSVerifyFlags, deprecatedTLSVerifyOpt := deprecatedTLSVerifyFlags()
|
||||
srcFlags, srcOpts := imageFlags(global, sharedOpts, deprecatedTLSVerifyOpt, "src-", "screds")
|
||||
destFlags, destOpts := imageDestFlags(global, sharedOpts, deprecatedTLSVerifyOpt, "dest-", "dcreds")
|
||||
retryFlags, retryOpts := retryFlags()
|
||||
copyFlags, copyOpts := sharedCopyFlags()
|
||||
opts := copyOptions{global: global,
|
||||
deprecatedTLSVerify: deprecatedTLSVerifyOpt,
|
||||
srcImage: srcOpts,
|
||||
destImage: destOpts,
|
||||
retryOpts: retryOpts,
|
||||
copy: copyOpts,
|
||||
}
|
||||
cmd := &cobra.Command{
|
||||
Use: "copy [command options] SOURCE-IMAGE DESTINATION-IMAGE",
|
||||
Short: "Copy an IMAGE-NAME from one location to another",
|
||||
Long: fmt.Sprintf(`Container "IMAGE-NAME" uses a "transport":"details" format.
|
||||
|
||||
Supported transports:
|
||||
%s
|
||||
|
||||
See skopeo(1) section "IMAGE NAMES" for the expected format
|
||||
`, strings.Join(transports.ListNames(), ", ")),
|
||||
RunE: commandAction(opts.run),
|
||||
Example: `skopeo copy docker://quay.io/skopeo/stable:latest docker://registry.example.com/skopeo:latest`,
|
||||
ValidArgsFunction: autocompleteImageNames,
|
||||
}
|
||||
adjustUsage(cmd)
|
||||
flags := cmd.Flags()
|
||||
flags.AddFlagSet(&sharedFlags)
|
||||
flags.AddFlagSet(&deprecatedTLSVerifyFlags)
|
||||
flags.AddFlagSet(&srcFlags)
|
||||
flags.AddFlagSet(&destFlags)
|
||||
flags.AddFlagSet(&retryFlags)
|
||||
flags.AddFlagSet(©Flags)
|
||||
flags.StringSliceVar(&opts.additionalTags, "additional-tag", []string{}, "additional tags (supports docker-archive)")
|
||||
flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Suppress output information when copying images")
|
||||
flags.BoolVarP(&opts.all, "all", "a", false, "Copy all images if SOURCE-IMAGE is a list")
|
||||
flags.Var(commonFlag.NewOptionalStringValue(&opts.multiArch), "multi-arch", `How to handle multi-architecture images (system, all, or index-only)`)
|
||||
flags.StringVar(&opts.signIdentity, "sign-identity", "", "Identity of signed image, must be a fully specified docker reference. Defaults to the target docker reference.")
|
||||
flags.StringVar(&opts.digestFile, "digestfile", "", "Write the digest of the pushed image to the specified file")
|
||||
flags.StringSliceVar(&opts.encryptionKeys, "encryption-key", []string{}, "*Experimental* key with the encryption protocol to use needed to encrypt the image (e.g. jwe:/path/to/key.pem)")
|
||||
flags.IntSliceVar(&opts.encryptLayer, "encrypt-layer", []int{}, "*Experimental* the 0-indexed layer indices, with support for negative indexing (e.g. 0 is the first layer, -1 is the last layer)")
|
||||
flags.StringSliceVar(&opts.decryptionKeys, "decryption-key", []string{}, "*Experimental* key needed to decrypt the image")
|
||||
flags.UintVar(&opts.imageParallelCopies, "image-parallel-copies", 0, "Maximum number of image layers to be copied (pulled/pushed) simultaneously. Not setting this field will fall back to containers/image defaults.")
|
||||
return cmd
|
||||
}
|
||||
|
||||
// parseMultiArch parses the list processing selection
|
||||
// It returns the copy.ImageListSelection to use with image.Copy option
|
||||
func parseMultiArch(multiArch string) (copy.ImageListSelection, error) {
|
||||
switch multiArch {
|
||||
case "system":
|
||||
return copy.CopySystemImage, nil
|
||||
case "all":
|
||||
return copy.CopyAllImages, nil
|
||||
// There is no CopyNoImages value in copy.ImageListSelection, but because we
|
||||
// don't provide an option to select a set of images to copy, we can use
|
||||
// CopySpecificImages.
|
||||
case "index-only":
|
||||
return copy.CopySpecificImages, nil
|
||||
// We don't expose CopySpecificImages other than index-only above, because
|
||||
// we currently don't provide an option to choose the images to copy. That
|
||||
// could be added in the future.
|
||||
default:
|
||||
return copy.CopySystemImage, fmt.Errorf("unknown multi-arch option %q. Choose one of the supported options: 'system', 'all', or 'index-only'", multiArch)
|
||||
}
|
||||
}
|
||||
|
||||
func (opts *copyOptions) run(args []string, stdout io.Writer) (retErr error) {
|
||||
if len(args) != 2 {
|
||||
return errorShouldDisplayUsage{errors.New("Exactly two arguments expected")}
|
||||
}
|
||||
opts.deprecatedTLSVerify.warnIfUsed([]string{"--src-tls-verify", "--dest-tls-verify"})
|
||||
imageNames := args
|
||||
|
||||
if err := reexecIfNecessaryForImages(imageNames...); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
policyContext, err := opts.global.getPolicyContext()
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error loading trust policy: %v", err)
|
||||
}
|
||||
defer func() {
|
||||
if err := policyContext.Destroy(); err != nil {
|
||||
retErr = noteCloseFailure(retErr, "tearing down policy context", err)
|
||||
}
|
||||
}()
|
||||
|
||||
srcRef, err := alltransports.ParseImageName(imageNames[0])
|
||||
if err != nil {
|
||||
return fmt.Errorf("Invalid source name %s: %v", imageNames[0], err)
|
||||
}
|
||||
destRef, err := alltransports.ParseImageName(imageNames[1])
|
||||
if err != nil {
|
||||
return fmt.Errorf("Invalid destination name %s: %v", imageNames[1], err)
|
||||
}
|
||||
|
||||
sourceCtx, err := opts.srcImage.newSystemContext()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
destinationCtx, err := opts.destImage.newSystemContext()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, image := range opts.additionalTags {
|
||||
ref, err := reference.ParseNormalizedNamed(image)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error parsing additional-tag '%s': %v", image, err)
|
||||
}
|
||||
namedTagged, isNamedTagged := ref.(reference.NamedTagged)
|
||||
if !isNamedTagged {
|
||||
return fmt.Errorf("additional-tag '%s' must be a tagged reference", image)
|
||||
}
|
||||
destinationCtx.DockerArchiveAdditionalTags = append(destinationCtx.DockerArchiveAdditionalTags, namedTagged)
|
||||
}
|
||||
|
||||
ctx, cancel := opts.global.commandTimeoutContext()
|
||||
defer cancel()
|
||||
|
||||
if opts.quiet {
|
||||
stdout = nil
|
||||
}
|
||||
|
||||
imageListSelection := copy.CopySystemImage
|
||||
if opts.multiArch.Present() && opts.all {
|
||||
return fmt.Errorf("Cannot use --all and --multi-arch flags together")
|
||||
}
|
||||
if opts.multiArch.Present() {
|
||||
imageListSelection, err = parseMultiArch(opts.multiArch.Value())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if opts.all {
|
||||
imageListSelection = copy.CopyAllImages
|
||||
}
|
||||
|
||||
if len(opts.encryptionKeys) > 0 && len(opts.decryptionKeys) > 0 {
|
||||
return fmt.Errorf("--encryption-key and --decryption-key cannot be specified together")
|
||||
}
|
||||
|
||||
var encLayers *[]int
|
||||
var encConfig *encconfig.EncryptConfig
|
||||
var decConfig *encconfig.DecryptConfig
|
||||
|
||||
if len(opts.encryptLayer) > 0 && len(opts.encryptionKeys) == 0 {
|
||||
return fmt.Errorf("--encrypt-layer can only be used with --encryption-key")
|
||||
}
|
||||
|
||||
if len(opts.encryptionKeys) > 0 {
|
||||
// encryption
|
||||
p := opts.encryptLayer
|
||||
encLayers = &p
|
||||
encryptionKeys := opts.encryptionKeys
|
||||
ecc, err := enchelpers.CreateCryptoConfig(encryptionKeys, []string{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("Invalid encryption keys: %v", err)
|
||||
}
|
||||
cc := encconfig.CombineCryptoConfigs([]encconfig.CryptoConfig{ecc})
|
||||
encConfig = cc.EncryptConfig
|
||||
}
|
||||
|
||||
if len(opts.decryptionKeys) > 0 {
|
||||
// decryption
|
||||
decryptionKeys := opts.decryptionKeys
|
||||
dcc, err := enchelpers.CreateCryptoConfig([]string{}, decryptionKeys)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Invalid decryption keys: %v", err)
|
||||
}
|
||||
cc := encconfig.CombineCryptoConfigs([]encconfig.CryptoConfig{dcc})
|
||||
decConfig = cc.DecryptConfig
|
||||
}
|
||||
|
||||
var signIdentity reference.Named = nil
|
||||
if opts.signIdentity != "" {
|
||||
signIdentity, err = reference.ParseNamed(opts.signIdentity)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Could not parse --sign-identity: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
opts.destImage.warnAboutIneffectiveOptions(destRef.Transport())
|
||||
|
||||
copyOpts, cleanupOptions, err := opts.copy.copyOptions(stdout)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer cleanupOptions()
|
||||
copyOpts.SignIdentity = signIdentity
|
||||
copyOpts.SourceCtx = sourceCtx
|
||||
copyOpts.DestinationCtx = destinationCtx
|
||||
copyOpts.ImageListSelection = imageListSelection
|
||||
copyOpts.OciDecryptConfig = decConfig
|
||||
copyOpts.OciEncryptLayers = encLayers
|
||||
copyOpts.OciEncryptConfig = encConfig
|
||||
copyOpts.MaxParallelDownloads = opts.imageParallelCopies
|
||||
|
||||
return retry.IfNecessary(ctx, func() error {
|
||||
manifestBytes, err := copy.Image(ctx, policyContext, destRef, srcRef, copyOpts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if opts.digestFile != "" {
|
||||
manifestDigest, err := manifest.Digest(manifestBytes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err = os.WriteFile(opts.digestFile, []byte(manifestDigest.String()), 0644); err != nil {
|
||||
return fmt.Errorf("Failed to write digest to file %q: %w", opts.digestFile, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}, opts.retryOpts)
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
package main
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestCopy(t *testing.T) {
|
||||
// Invalid command-line arguments
|
||||
for _, args := range [][]string{
|
||||
{},
|
||||
{"a1"},
|
||||
{"a1", "a2", "a3"},
|
||||
} {
|
||||
out, err := runSkopeo(append([]string{"--insecure-policy", "copy"}, args...)...)
|
||||
assertTestFailed(t, out, err, "Exactly two arguments expected")
|
||||
}
|
||||
|
||||
// FIXME: Much more test coverage
|
||||
// Actual feature tests exist in integration and systemtest
|
||||
}
|
|
@ -0,0 +1,76 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"go.podman.io/common/pkg/retry"
|
||||
"go.podman.io/image/v5/transports"
|
||||
"go.podman.io/image/v5/transports/alltransports"
|
||||
)
|
||||
|
||||
type deleteOptions struct {
|
||||
global *globalOptions
|
||||
image *imageOptions
|
||||
retryOpts *retry.Options
|
||||
}
|
||||
|
||||
func deleteCmd(global *globalOptions) *cobra.Command {
|
||||
sharedFlags, sharedOpts := sharedImageFlags()
|
||||
imageFlags, imageOpts := imageFlags(global, sharedOpts, nil, "", "")
|
||||
retryFlags, retryOpts := retryFlags()
|
||||
opts := deleteOptions{
|
||||
global: global,
|
||||
image: imageOpts,
|
||||
retryOpts: retryOpts,
|
||||
}
|
||||
cmd := &cobra.Command{
|
||||
Use: "delete [command options] IMAGE-NAME",
|
||||
Short: "Delete image IMAGE-NAME",
|
||||
Long: fmt.Sprintf(`Delete an "IMAGE_NAME" from a transport
|
||||
Supported transports:
|
||||
%s
|
||||
See skopeo(1) section "IMAGE NAMES" for the expected format
|
||||
`, strings.Join(transports.ListNames(), ", ")),
|
||||
RunE: commandAction(opts.run),
|
||||
Example: `skopeo delete docker://registry.example.com/example/pause:latest`,
|
||||
ValidArgsFunction: autocompleteImageNames,
|
||||
}
|
||||
adjustUsage(cmd)
|
||||
flags := cmd.Flags()
|
||||
flags.AddFlagSet(&sharedFlags)
|
||||
flags.AddFlagSet(&imageFlags)
|
||||
flags.AddFlagSet(&retryFlags)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (opts *deleteOptions) run(args []string, stdout io.Writer) error {
|
||||
if len(args) != 1 {
|
||||
return errors.New("Usage: delete imageReference")
|
||||
}
|
||||
imageName := args[0]
|
||||
|
||||
if err := reexecIfNecessaryForImages(imageName); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ref, err := alltransports.ParseImageName(imageName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Invalid source name %s: %v", imageName, err)
|
||||
}
|
||||
|
||||
sys, err := opts.image.newSystemContext()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ctx, cancel := opts.global.commandTimeoutContext()
|
||||
defer cancel()
|
||||
|
||||
return retry.IfNecessary(ctx, func() error {
|
||||
return ref.DeleteImage(ctx, sys)
|
||||
}, opts.retryOpts)
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
/*.gpg~
|
||||
/.gpg-v21-migrated
|
||||
/private-keys-v1.d
|
||||
/random_seed
|
Binary file not shown.
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
"schemaVersion": 2,
|
||||
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
|
||||
"config": {
|
||||
"mediaType": "application/vnd.docker.container.image.v1+json",
|
||||
"size": 7023,
|
||||
"digest": "sha256:b5b2b2c507a0944348e0303114d8d93aaaa081732b86451d9bce1f432a537bc7"
|
||||
},
|
||||
"layers": [
|
||||
{
|
||||
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
|
||||
"size": 32654,
|
||||
"digest": "sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f"
|
||||
},
|
||||
{
|
||||
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
|
||||
"size": 16724,
|
||||
"digest": "sha256:3c3a4604a545cdc127456d94e421cd355bca5b528f4a9c1905b15da2eb4a4c6b"
|
||||
},
|
||||
{
|
||||
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
|
||||
"size": 73109,
|
||||
"digest": "sha256:ec4b8955958665577945c89419d1af06b5f7636b4ac3da7f12184802ad867736"
|
||||
}
|
||||
]
|
||||
}
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"schemaVersion": 1,
|
||||
"name": "mitr/buxybox",
|
||||
"tag": "latest",
|
||||
"architecture": "amd64",
|
||||
"fsLayers": [
|
||||
],
|
||||
"history": [
|
||||
],
|
||||
"signatures": 1
|
||||
}
|
|
@ -0,0 +1,90 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"os"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"go.podman.io/image/v5/pkg/cli"
|
||||
"go.podman.io/image/v5/signature/sigstore"
|
||||
)
|
||||
|
||||
type generateSigstoreKeyOptions struct {
|
||||
outputPrefix string
|
||||
passphraseFile string
|
||||
}
|
||||
|
||||
func generateSigstoreKeyCmd() *cobra.Command {
|
||||
var opts generateSigstoreKeyOptions
|
||||
cmd := &cobra.Command{
|
||||
Use: "generate-sigstore-key [command options] --output-prefix PREFIX",
|
||||
Short: "Generate a sigstore public/private key pair",
|
||||
RunE: commandAction(opts.run),
|
||||
Example: "skopeo generate-sigstore-key --output-prefix my-key",
|
||||
}
|
||||
adjustUsage(cmd)
|
||||
flags := cmd.Flags()
|
||||
flags.StringVar(&opts.outputPrefix, "output-prefix", "", "Write the keys to `PREFIX`.pub and `PREFIX`.private")
|
||||
flags.StringVar(&opts.passphraseFile, "passphrase-file", "", "Read a passphrase for the private key from `PATH`")
|
||||
return cmd
|
||||
}
|
||||
|
||||
// ensurePathDoesNotExist verifies that path does not refer to an existing file,
|
||||
// and returns an error if so.
|
||||
func ensurePathDoesNotExist(path string) error {
|
||||
switch _, err := os.Stat(path); {
|
||||
case err == nil:
|
||||
return fmt.Errorf("Refusing to overwrite existing %q", path)
|
||||
case errors.Is(err, fs.ErrNotExist):
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("Error checking existence of %q: %w", path, err)
|
||||
}
|
||||
}
|
||||
|
||||
func (opts *generateSigstoreKeyOptions) run(args []string, stdout io.Writer) error {
|
||||
if len(args) != 0 || opts.outputPrefix == "" {
|
||||
return errors.New("Usage: generate-sigstore-key --output-prefix PREFIX")
|
||||
}
|
||||
|
||||
pubKeyPath := opts.outputPrefix + ".pub"
|
||||
privateKeyPath := opts.outputPrefix + ".private"
|
||||
if err := ensurePathDoesNotExist(pubKeyPath); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := ensurePathDoesNotExist(privateKeyPath); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var passphrase string
|
||||
if opts.passphraseFile != "" {
|
||||
p, err := cli.ReadPassphraseFile(opts.passphraseFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
passphrase = p
|
||||
} else {
|
||||
p, err := promptForPassphrase(privateKeyPath, os.Stdin, os.Stdout)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
passphrase = p
|
||||
}
|
||||
|
||||
keys, err := sigstore.GenerateKeyPair([]byte(passphrase))
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error generating key pair: %w", err)
|
||||
}
|
||||
|
||||
if err := os.WriteFile(privateKeyPath, keys.PrivateKey, 0600); err != nil {
|
||||
return fmt.Errorf("Error writing private key to %q: %w", privateKeyPath, err)
|
||||
}
|
||||
if err := os.WriteFile(pubKeyPath, keys.PublicKey, 0644); err != nil {
|
||||
return fmt.Errorf("Error writing private key to %q: %w", pubKeyPath, err)
|
||||
}
|
||||
fmt.Fprintf(stdout, "Key written to %q and %q", privateKeyPath, pubKeyPath)
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,79 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestGenerateSigstoreKey(t *testing.T) {
|
||||
// Invalid command-line arguments
|
||||
for _, args := range [][]string{
|
||||
{},
|
||||
{"--output-prefix", "foo", "a1"},
|
||||
} {
|
||||
out, err := runSkopeo(append([]string{"generate-sigstore-key"}, args...)...)
|
||||
assertTestFailed(t, out, err, "Usage")
|
||||
}
|
||||
|
||||
// One of the destination files already exists
|
||||
outputSuffixes := []string{".pub", ".private"}
|
||||
for _, suffix := range outputSuffixes {
|
||||
dir := t.TempDir()
|
||||
prefix := filepath.Join(dir, "prefix")
|
||||
err := os.WriteFile(prefix+suffix, []byte{}, 0600)
|
||||
require.NoError(t, err)
|
||||
out, err := runSkopeo("generate-sigstore-key",
|
||||
"--output-prefix", prefix, "--passphrase-file", "/dev/null",
|
||||
)
|
||||
assertTestFailed(t, out, err, "Refusing to overwrite")
|
||||
}
|
||||
|
||||
// One of the destinations is inaccessible (simulate by a symlink that tries to
|
||||
// traverse a non-directory)
|
||||
for _, suffix := range outputSuffixes {
|
||||
dir := t.TempDir()
|
||||
nonDirectory := filepath.Join(dir, "nondirectory")
|
||||
err := os.WriteFile(nonDirectory, []byte{}, 0600)
|
||||
require.NoError(t, err)
|
||||
prefix := filepath.Join(dir, "prefix")
|
||||
err = os.Symlink(filepath.Join(nonDirectory, "unaccessible"), prefix+suffix)
|
||||
require.NoError(t, err)
|
||||
out, err := runSkopeo("generate-sigstore-key",
|
||||
"--output-prefix", prefix, "--passphrase-file", "/dev/null",
|
||||
)
|
||||
assertTestFailed(t, out, err, prefix+suffix) // + an OS-specific error message
|
||||
}
|
||||
destDir := t.TempDir()
|
||||
// Error reading passphrase
|
||||
out, err := runSkopeo("generate-sigstore-key",
|
||||
"--output-prefix", filepath.Join(destDir, "prefix"),
|
||||
"--passphrase-file", filepath.Join(destDir, "this-does-not-exist"),
|
||||
)
|
||||
assertTestFailed(t, out, err, "this-does-not-exist")
|
||||
|
||||
// (The interactive passphrase prompting is not yet tested)
|
||||
|
||||
// Error writing outputs is untested: when unit tests run as root, we can’t use permissions on a directory to cause write failures,
|
||||
// with the --output-prefix mechanism, and refusing to even start writing to pre-exisiting files, directories are the only mechanism
|
||||
// we have to trigger a write failure.
|
||||
|
||||
// Success
|
||||
// Just a smoke-test, usability of the keys is tested in the generate implementation.
|
||||
dir := t.TempDir()
|
||||
prefix := filepath.Join(dir, "prefix")
|
||||
passphraseFile := filepath.Join(dir, "passphrase")
|
||||
err = os.WriteFile(passphraseFile, []byte("some passphrase"), 0600)
|
||||
require.NoError(t, err)
|
||||
out, err = runSkopeo("generate-sigstore-key",
|
||||
"--output-prefix", prefix, "--passphrase-file", passphraseFile,
|
||||
)
|
||||
assert.NoError(t, err)
|
||||
for _, suffix := range outputSuffixes {
|
||||
assert.Contains(t, out, prefix+suffix)
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,237 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"github.com/containers/skopeo/cmd/skopeo/inspect"
|
||||
"github.com/docker/distribution/registry/api/errcode"
|
||||
v1 "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
"go.podman.io/common/pkg/report"
|
||||
"go.podman.io/common/pkg/retry"
|
||||
"go.podman.io/image/v5/docker"
|
||||
"go.podman.io/image/v5/image"
|
||||
"go.podman.io/image/v5/manifest"
|
||||
"go.podman.io/image/v5/transports"
|
||||
"go.podman.io/image/v5/types"
|
||||
)
|
||||
|
||||
type inspectOptions struct {
|
||||
global *globalOptions
|
||||
image *imageOptions
|
||||
retryOpts *retry.Options
|
||||
format string
|
||||
raw bool // Output the raw manifest instead of parsing information about the image
|
||||
config bool // Output the raw config blob instead of parsing information about the image
|
||||
doNotListTags bool // Do not list all tags available in the same repository
|
||||
}
|
||||
|
||||
func inspectCmd(global *globalOptions) *cobra.Command {
|
||||
sharedFlags, sharedOpts := sharedImageFlags()
|
||||
imageFlags, imageOpts := imageFlags(global, sharedOpts, nil, "", "")
|
||||
retryFlags, retryOpts := retryFlags()
|
||||
opts := inspectOptions{
|
||||
global: global,
|
||||
image: imageOpts,
|
||||
retryOpts: retryOpts,
|
||||
}
|
||||
cmd := &cobra.Command{
|
||||
Use: "inspect [command options] IMAGE-NAME",
|
||||
Short: "Inspect image IMAGE-NAME",
|
||||
Long: fmt.Sprintf(`Return low-level information about "IMAGE-NAME" in a registry/transport
|
||||
Supported transports:
|
||||
%s
|
||||
|
||||
See skopeo(1) section "IMAGE NAMES" for the expected format
|
||||
`, strings.Join(transports.ListNames(), ", ")),
|
||||
RunE: commandAction(opts.run),
|
||||
Example: `skopeo inspect docker://registry.fedoraproject.org/fedora
|
||||
skopeo inspect --config docker://docker.io/alpine
|
||||
skopeo inspect --format "Name: {{.Name}} Digest: {{.Digest}}" docker://registry.access.redhat.com/ubi8`,
|
||||
ValidArgsFunction: autocompleteImageNames,
|
||||
}
|
||||
adjustUsage(cmd)
|
||||
flags := cmd.Flags()
|
||||
flags.AddFlagSet(&sharedFlags)
|
||||
flags.AddFlagSet(&imageFlags)
|
||||
flags.AddFlagSet(&retryFlags)
|
||||
flags.BoolVar(&opts.raw, "raw", false, "output raw manifest or configuration")
|
||||
flags.BoolVar(&opts.config, "config", false, "output configuration")
|
||||
flags.StringVarP(&opts.format, "format", "f", "", "Format the output to a Go template")
|
||||
flags.BoolVarP(&opts.doNotListTags, "no-tags", "n", false, "Do not list the available tags from the repository in the output")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (opts *inspectOptions) run(args []string, stdout io.Writer) (retErr error) {
|
||||
var (
|
||||
rawManifest []byte
|
||||
src types.ImageSource
|
||||
imgInspect *types.ImageInspectInfo
|
||||
)
|
||||
ctx, cancel := opts.global.commandTimeoutContext()
|
||||
defer cancel()
|
||||
|
||||
if len(args) != 1 {
|
||||
return errors.New("Exactly one argument expected")
|
||||
}
|
||||
if opts.raw && opts.format != "" {
|
||||
return errors.New("raw output does not support format option")
|
||||
}
|
||||
imageName := args[0]
|
||||
|
||||
if err := reexecIfNecessaryForImages(imageName); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sys, err := opts.image.newSystemContext()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := retry.IfNecessary(ctx, func() error {
|
||||
src, err = parseImageSource(ctx, opts.image, imageName)
|
||||
return err
|
||||
}, opts.retryOpts); err != nil {
|
||||
return fmt.Errorf("Error parsing image name %q: %w", imageName, err)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if err := src.Close(); err != nil {
|
||||
retErr = noteCloseFailure(retErr, "closing image", err)
|
||||
}
|
||||
}()
|
||||
|
||||
unparsedInstance := image.UnparsedInstance(src, nil)
|
||||
if err := retry.IfNecessary(ctx, func() error {
|
||||
rawManifest, _, err = unparsedInstance.Manifest(ctx)
|
||||
return err
|
||||
}, opts.retryOpts); err != nil {
|
||||
return fmt.Errorf("Error retrieving manifest for image: %w", err)
|
||||
}
|
||||
|
||||
if opts.raw && !opts.config {
|
||||
_, err := stdout.Write(rawManifest)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error writing manifest to standard output: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
img, err := image.FromUnparsedImage(ctx, sys, unparsedInstance)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error parsing manifest for image: %w", err)
|
||||
}
|
||||
|
||||
if opts.config && opts.raw {
|
||||
var configBlob []byte
|
||||
if err := retry.IfNecessary(ctx, func() error {
|
||||
configBlob, err = img.ConfigBlob(ctx)
|
||||
return err
|
||||
}, opts.retryOpts); err != nil {
|
||||
return fmt.Errorf("Error reading configuration blob: %w", err)
|
||||
}
|
||||
_, err = stdout.Write(configBlob)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error writing configuration blob to standard output: %w", err)
|
||||
}
|
||||
return nil
|
||||
} else if opts.config {
|
||||
var config *v1.Image
|
||||
if err := retry.IfNecessary(ctx, func() error {
|
||||
config, err = img.OCIConfig(ctx)
|
||||
return err
|
||||
}, opts.retryOpts); err != nil {
|
||||
return fmt.Errorf("Error reading OCI-formatted configuration data: %w", err)
|
||||
}
|
||||
if err := opts.writeOutput(stdout, config); err != nil {
|
||||
return fmt.Errorf("Error writing OCI-formatted configuration data to standard output: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := retry.IfNecessary(ctx, func() error {
|
||||
imgInspect, err = img.Inspect(ctx)
|
||||
return err
|
||||
}, opts.retryOpts); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
outputData := inspect.Output{
|
||||
Name: "", // Set below if DockerReference() is known
|
||||
Tag: imgInspect.Tag,
|
||||
// Digest is set below.
|
||||
RepoTags: []string{}, // Possibly overridden for docker.Transport.
|
||||
Created: imgInspect.Created,
|
||||
DockerVersion: imgInspect.DockerVersion,
|
||||
Labels: imgInspect.Labels,
|
||||
Architecture: imgInspect.Architecture,
|
||||
Os: imgInspect.Os,
|
||||
Layers: imgInspect.Layers,
|
||||
LayersData: imgInspect.LayersData,
|
||||
Env: imgInspect.Env,
|
||||
}
|
||||
outputData.Digest, err = manifest.Digest(rawManifest)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error computing manifest digest: %w", err)
|
||||
}
|
||||
if dockerRef := img.Reference().DockerReference(); dockerRef != nil {
|
||||
outputData.Name = dockerRef.Name()
|
||||
}
|
||||
if !opts.doNotListTags && img.Reference().Transport() == docker.Transport {
|
||||
sys, err := opts.image.newSystemContext()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
outputData.RepoTags, err = docker.GetRepositoryTags(ctx, sys, img.Reference())
|
||||
if err != nil {
|
||||
// Some registries may decide to block the "list all tags" endpoint;
|
||||
// gracefully allow the inspect to continue in this case:
|
||||
fatalFailure := true
|
||||
// - AWS ECR rejects it if the "ecr:ListImages" action is not allowed.
|
||||
// https://github.com/containers/skopeo/issues/726
|
||||
var ec errcode.ErrorCoder
|
||||
if ok := errors.As(err, &ec); ok && ec.ErrorCode() == errcode.ErrorCodeDenied {
|
||||
fatalFailure = false
|
||||
}
|
||||
// - public.ecr.aws does not implement the endpoint at all, and fails with 404:
|
||||
// https://github.com/containers/skopeo/issues/1230
|
||||
// This is actually "code":"NOT_FOUND", and the parser doesn’t preserve that.
|
||||
// So, also check the error text.
|
||||
if ok := errors.As(err, &ec); ok && ec.ErrorCode() == errcode.ErrorCodeUnknown {
|
||||
var e errcode.Error
|
||||
if ok := errors.As(err, &e); ok && e.Code == errcode.ErrorCodeUnknown && e.Message == "404 page not found" {
|
||||
fatalFailure = false
|
||||
}
|
||||
}
|
||||
if fatalFailure {
|
||||
return fmt.Errorf("Error determining repository tags: %w", err)
|
||||
}
|
||||
logrus.Warnf("Registry disallows tag list retrieval; skipping")
|
||||
}
|
||||
}
|
||||
return opts.writeOutput(stdout, outputData)
|
||||
}
|
||||
|
||||
// writeOutput writes data depending on opts.format to stdout
|
||||
func (opts *inspectOptions) writeOutput(stdout io.Writer, data any) error {
|
||||
if report.IsJSON(opts.format) || opts.format == "" {
|
||||
out, err := json.MarshalIndent(data, "", " ")
|
||||
if err == nil {
|
||||
fmt.Fprintf(stdout, "%s\n", string(out))
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
rpt, err := report.New(stdout, "skopeo inspect").Parse(report.OriginUser, opts.format)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer rpt.Flush()
|
||||
return rpt.Execute([]any{data})
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
package inspect
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
digest "github.com/opencontainers/go-digest"
|
||||
"go.podman.io/image/v5/types"
|
||||
)
|
||||
|
||||
// Output is the output format of (skopeo inspect),
|
||||
// primarily so that we can format it with a simple json.MarshalIndent.
|
||||
type Output struct {
|
||||
Name string `json:",omitempty"`
|
||||
Tag string `json:",omitempty"`
|
||||
Digest digest.Digest
|
||||
RepoTags []string
|
||||
Created *time.Time
|
||||
DockerVersion string
|
||||
Labels map[string]string
|
||||
Architecture string
|
||||
Os string
|
||||
Layers []string
|
||||
LayersData []types.ImageInspectLayer
|
||||
Env []string
|
||||
}
|
|
@ -0,0 +1,184 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/opencontainers/go-digest"
|
||||
"github.com/spf13/cobra"
|
||||
"go.podman.io/common/pkg/retry"
|
||||
"go.podman.io/image/v5/directory"
|
||||
"go.podman.io/image/v5/image"
|
||||
"go.podman.io/image/v5/pkg/blobinfocache"
|
||||
"go.podman.io/image/v5/types"
|
||||
)
|
||||
|
||||
type layersOptions struct {
|
||||
global *globalOptions
|
||||
image *imageOptions
|
||||
retryOpts *retry.Options
|
||||
}
|
||||
|
||||
func layersCmd(global *globalOptions) *cobra.Command {
|
||||
sharedFlags, sharedOpts := sharedImageFlags()
|
||||
imageFlags, imageOpts := imageFlags(global, sharedOpts, nil, "", "")
|
||||
retryFlags, retryOpts := retryFlags()
|
||||
opts := layersOptions{
|
||||
global: global,
|
||||
image: imageOpts,
|
||||
retryOpts: retryOpts,
|
||||
}
|
||||
cmd := &cobra.Command{
|
||||
Hidden: true,
|
||||
Use: "layers [command options] IMAGE-NAME [LAYER...]",
|
||||
Short: "Get layers of IMAGE-NAME",
|
||||
RunE: commandAction(opts.run),
|
||||
}
|
||||
adjustUsage(cmd)
|
||||
flags := cmd.Flags()
|
||||
flags.AddFlagSet(&sharedFlags)
|
||||
flags.AddFlagSet(&imageFlags)
|
||||
flags.AddFlagSet(&retryFlags)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (opts *layersOptions) run(args []string, stdout io.Writer) (retErr error) {
|
||||
fmt.Fprintln(os.Stderr, `DEPRECATED: skopeo layers is deprecated in favor of skopeo copy`)
|
||||
if len(args) == 0 {
|
||||
return errors.New("Usage: layers imageReference [layer...]")
|
||||
}
|
||||
imageName := args[0]
|
||||
|
||||
if err := reexecIfNecessaryForImages(imageName); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ctx, cancel := opts.global.commandTimeoutContext()
|
||||
defer cancel()
|
||||
|
||||
sys, err := opts.image.newSystemContext()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cache := blobinfocache.DefaultCache(sys)
|
||||
var (
|
||||
rawSource types.ImageSource
|
||||
src types.ImageCloser
|
||||
)
|
||||
if err = retry.IfNecessary(ctx, func() error {
|
||||
rawSource, err = parseImageSource(ctx, opts.image, imageName)
|
||||
return err
|
||||
}, opts.retryOpts); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = retry.IfNecessary(ctx, func() error {
|
||||
src, err = image.FromSource(ctx, sys, rawSource)
|
||||
return err
|
||||
}, opts.retryOpts); err != nil {
|
||||
if closeErr := rawSource.Close(); closeErr != nil {
|
||||
return fmt.Errorf("%w (closing image source: %v)", err, closeErr)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
if err := src.Close(); err != nil {
|
||||
retErr = noteCloseFailure(retErr, "closing image", err)
|
||||
}
|
||||
}()
|
||||
|
||||
type blobDigest struct {
|
||||
digest digest.Digest
|
||||
isConfig bool
|
||||
}
|
||||
var blobDigests []blobDigest
|
||||
for _, dString := range args[1:] {
|
||||
if !strings.HasPrefix(dString, "sha256:") {
|
||||
dString = "sha256:" + dString
|
||||
}
|
||||
d, err := digest.Parse(dString)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
blobDigests = append(blobDigests, blobDigest{digest: d, isConfig: false})
|
||||
}
|
||||
|
||||
if len(blobDigests) == 0 {
|
||||
layers := src.LayerInfos()
|
||||
seenLayers := map[digest.Digest]struct{}{}
|
||||
for _, info := range layers {
|
||||
if _, ok := seenLayers[info.Digest]; !ok {
|
||||
blobDigests = append(blobDigests, blobDigest{digest: info.Digest, isConfig: false})
|
||||
seenLayers[info.Digest] = struct{}{}
|
||||
}
|
||||
}
|
||||
configInfo := src.ConfigInfo()
|
||||
if configInfo.Digest != "" {
|
||||
blobDigests = append(blobDigests, blobDigest{digest: configInfo.Digest, isConfig: true})
|
||||
}
|
||||
}
|
||||
|
||||
tmpDir, err := os.MkdirTemp(".", "layers-")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tmpDirRef, err := directory.NewReference(tmpDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dest, err := tmpDirRef.NewImageDestination(ctx, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if err := dest.Close(); err != nil {
|
||||
retErr = noteCloseFailure(retErr, "closing destination", err)
|
||||
}
|
||||
}()
|
||||
|
||||
for _, bd := range blobDigests {
|
||||
var (
|
||||
r io.ReadCloser
|
||||
blobSize int64
|
||||
)
|
||||
if err = retry.IfNecessary(ctx, func() error {
|
||||
r, blobSize, err = rawSource.GetBlob(ctx, types.BlobInfo{Digest: bd.digest, Size: -1}, cache)
|
||||
return err
|
||||
}, opts.retryOpts); err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
if err := r.Close(); err != nil {
|
||||
retErr = noteCloseFailure(retErr, fmt.Sprintf("closing blob %q", bd.digest.String()), err)
|
||||
}
|
||||
}()
|
||||
verifier := bd.digest.Verifier()
|
||||
tr := io.TeeReader(r, verifier)
|
||||
if _, err := dest.PutBlob(ctx, tr, types.BlobInfo{Digest: bd.digest, Size: blobSize}, cache, bd.isConfig); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := io.Copy(io.Discard, tr); err != nil { // Ensure we process all of tr, so that we can validate the digest.
|
||||
return err
|
||||
}
|
||||
if !verifier.Verified() {
|
||||
return fmt.Errorf("corrupt blob %q", bd.digest.String())
|
||||
}
|
||||
}
|
||||
|
||||
var manifest []byte
|
||||
if err = retry.IfNecessary(ctx, func() error {
|
||||
manifest, _, err = src.Manifest(ctx)
|
||||
return err
|
||||
}, opts.retryOpts); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := dest.PutManifest(ctx, manifest, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return dest.Commit(ctx, image.UnparsedInstance(rawSource, nil))
|
||||
}
|
|
@ -0,0 +1,204 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"maps"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"go.podman.io/common/pkg/retry"
|
||||
"go.podman.io/image/v5/docker"
|
||||
"go.podman.io/image/v5/docker/archive"
|
||||
"go.podman.io/image/v5/docker/reference"
|
||||
"go.podman.io/image/v5/transports/alltransports"
|
||||
"go.podman.io/image/v5/types"
|
||||
)
|
||||
|
||||
// tagListOutput is the output format of (skopeo list-tags), primarily so that we can format it with a simple json.MarshalIndent.
|
||||
type tagListOutput struct {
|
||||
Repository string `json:",omitempty"`
|
||||
Tags []string
|
||||
}
|
||||
|
||||
type tagsOptions struct {
|
||||
global *globalOptions
|
||||
image *imageOptions
|
||||
retryOpts *retry.Options
|
||||
}
|
||||
|
||||
var transportHandlers = map[string]func(ctx context.Context, sys *types.SystemContext, opts *tagsOptions, userInput string) (repositoryName string, tagListing []string, err error){
|
||||
docker.Transport.Name(): listDockerRepoTags,
|
||||
archive.Transport.Name(): listDockerArchiveTags,
|
||||
}
|
||||
|
||||
// supportedTransports returns all the supported transports
|
||||
func supportedTransports(joinStr string) string {
|
||||
res := slices.Sorted(maps.Keys(transportHandlers))
|
||||
return strings.Join(res, joinStr)
|
||||
}
|
||||
|
||||
func tagsCmd(global *globalOptions) *cobra.Command {
|
||||
sharedFlags, sharedOpts := sharedImageFlags()
|
||||
imageFlags, imageOpts := dockerImageFlags(global, sharedOpts, nil, "", "")
|
||||
retryFlags, retryOpts := retryFlags()
|
||||
|
||||
opts := tagsOptions{
|
||||
global: global,
|
||||
image: imageOpts,
|
||||
retryOpts: retryOpts,
|
||||
}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "list-tags [command options] SOURCE-IMAGE",
|
||||
Short: "List tags in the transport/repository specified by the SOURCE-IMAGE",
|
||||
Long: `Return the list of tags from the transport/repository "SOURCE-IMAGE"
|
||||
|
||||
Supported transports:
|
||||
` + supportedTransports(" ") + `
|
||||
|
||||
See skopeo-list-tags(1) section "REPOSITORY NAMES" for the expected format
|
||||
`,
|
||||
RunE: commandAction(opts.run),
|
||||
Example: `skopeo list-tags docker://docker.io/fedora`,
|
||||
}
|
||||
adjustUsage(cmd)
|
||||
flags := cmd.Flags()
|
||||
flags.AddFlagSet(&sharedFlags)
|
||||
flags.AddFlagSet(&imageFlags)
|
||||
flags.AddFlagSet(&retryFlags)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// Customized version of the alltransports.ParseImageName and docker.ParseReference that does not place a default tag in the reference
|
||||
// Would really love to not have this, but needed to enforce tag-less and digest-less names
|
||||
func parseDockerRepositoryReference(refString string) (types.ImageReference, error) {
|
||||
dockerRefString, ok := strings.CutPrefix(refString, docker.Transport.Name()+"://")
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("docker: image reference %s does not start with %s://", refString, docker.Transport.Name())
|
||||
}
|
||||
|
||||
ref, err := reference.ParseNormalizedNamed(dockerRefString)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !reference.IsNameOnly(ref) {
|
||||
return nil, errors.New(`No tag or digest allowed in reference`)
|
||||
}
|
||||
|
||||
// Checks ok, now return a reference. This is a hack because the tag listing code expects a full image reference even though the tag is ignored
|
||||
return docker.NewReference(reference.TagNameOnly(ref))
|
||||
}
|
||||
|
||||
// List the tags from a repository contained in the imgRef reference. Any tag value in the reference is ignored
|
||||
func listDockerTags(ctx context.Context, sys *types.SystemContext, imgRef types.ImageReference) (string, []string, error) {
|
||||
repositoryName := imgRef.DockerReference().Name()
|
||||
|
||||
tags, err := docker.GetRepositoryTags(ctx, sys, imgRef)
|
||||
if err != nil {
|
||||
return ``, nil, fmt.Errorf("Error listing repository tags: %w", err)
|
||||
}
|
||||
return repositoryName, tags, nil
|
||||
}
|
||||
|
||||
// return the tagLists from a docker repo
|
||||
func listDockerRepoTags(ctx context.Context, sys *types.SystemContext, opts *tagsOptions, userInput string) (repositoryName string, tagListing []string, err error) {
|
||||
// Do transport-specific parsing and validation to get an image reference
|
||||
imgRef, err := parseDockerRepositoryReference(userInput)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if err = retry.IfNecessary(ctx, func() error {
|
||||
repositoryName, tagListing, err = listDockerTags(ctx, sys, imgRef)
|
||||
return err
|
||||
}, opts.retryOpts); err != nil {
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// return the tagLists from a docker archive file
|
||||
func listDockerArchiveTags(_ context.Context, sys *types.SystemContext, _ *tagsOptions, userInput string) (repositoryName string, tagListing []string, err error) {
|
||||
ref, err := alltransports.ParseImageName(userInput)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
tarReader, _, err := archive.NewReaderForReference(sys, ref)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer tarReader.Close()
|
||||
|
||||
imageRefs, err := tarReader.List()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var repoTags []string
|
||||
for imageIndex, items := range imageRefs {
|
||||
for _, ref := range items {
|
||||
repoTags, err = tarReader.ManifestTagsForReference(ref)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
// handle for each untagged image
|
||||
if len(repoTags) == 0 {
|
||||
repoTags = []string{fmt.Sprintf("@%d", imageIndex)}
|
||||
}
|
||||
tagListing = append(tagListing, repoTags...)
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (opts *tagsOptions) run(args []string, stdout io.Writer) (retErr error) {
|
||||
ctx, cancel := opts.global.commandTimeoutContext()
|
||||
defer cancel()
|
||||
|
||||
if len(args) != 1 {
|
||||
return errorShouldDisplayUsage{errors.New("Exactly one non-option argument expected")}
|
||||
}
|
||||
|
||||
sys, err := opts.image.newSystemContext()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
transport := alltransports.TransportFromImageName(args[0])
|
||||
if transport == nil {
|
||||
return fmt.Errorf("Invalid %q: does not specify a transport", args[0])
|
||||
}
|
||||
|
||||
var repositoryName string
|
||||
var tagListing []string
|
||||
|
||||
if val, ok := transportHandlers[transport.Name()]; ok {
|
||||
repositoryName, tagListing, err = val(ctx, sys, opts, args[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
return fmt.Errorf("Unsupported transport '%s' for tag listing. Only supported: %s",
|
||||
transport.Name(), supportedTransports(", "))
|
||||
}
|
||||
|
||||
outputData := tagListOutput{
|
||||
Repository: repositoryName,
|
||||
Tags: tagListing,
|
||||
}
|
||||
|
||||
out, err := json.MarshalIndent(outputData, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = fmt.Fprintf(stdout, "%s\n", string(out))
|
||||
|
||||
return err
|
||||
}
|
|
@ -0,0 +1,70 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.podman.io/image/v5/transports/alltransports"
|
||||
)
|
||||
|
||||
// Tests the kinds of inputs allowed and expected to the command
|
||||
func TestDockerRepositoryReferenceParser(t *testing.T) {
|
||||
for _, test := range [][]string{
|
||||
{"docker://myhost.com:1000/nginx"}, //no tag
|
||||
{"docker://myhost.com/nginx"}, //no port or tag
|
||||
{"docker://somehost.com"}, // Valid default expansion
|
||||
{"docker://nginx"}, // Valid default expansion
|
||||
} {
|
||||
ref, err := parseDockerRepositoryReference(test[0])
|
||||
require.NoError(t, err)
|
||||
expected, err := alltransports.ParseImageName(test[0])
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, expected.DockerReference().Name(), ref.DockerReference().Name(), "Mismatched parse result for input %v", test[0])
|
||||
}
|
||||
|
||||
for _, test := range [][]string{
|
||||
{"oci://somedir"},
|
||||
{"dir:/somepath"},
|
||||
{"docker-archive:/tmp/dir"},
|
||||
{"container-storage:myhost.com/someimage"},
|
||||
{"docker-daemon:myhost.com/someimage"},
|
||||
{"docker://myhost.com:1000/nginx:foobar:foobar"}, // Invalid repository ref
|
||||
{"docker://somehost.com:5000/"}, // no repo
|
||||
{"docker://myhost.com:1000/nginx:latest"}, //tag not allowed
|
||||
{"docker://myhost.com:1000/nginx@sha256:abcdef1234567890"}, //digest not allowed
|
||||
} {
|
||||
_, err := parseDockerRepositoryReference(test[0])
|
||||
assert.Error(t, err, test[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestDockerRepositoryReferenceParserDrift(t *testing.T) {
|
||||
for _, test := range [][]string{
|
||||
{"docker://myhost.com:1000/nginx", "myhost.com:1000/nginx"}, //no tag
|
||||
{"docker://myhost.com/nginx", "myhost.com/nginx"}, //no port or tag
|
||||
{"docker://somehost.com", "docker.io/library/somehost.com"}, // Valid default expansion
|
||||
{"docker://nginx", "docker.io/library/nginx"}, // Valid default expansion
|
||||
} {
|
||||
ref, err := parseDockerRepositoryReference(test[0])
|
||||
ref2, err2 := alltransports.ParseImageName(test[0])
|
||||
|
||||
if assert.NoError(t, err, "Could not parse, got error on %v", test[0]) && assert.NoError(t, err2, "Could not parse with regular parser, got error on %v", test[0]) {
|
||||
assert.Equal(t, ref.DockerReference().String(), ref2.DockerReference().String(), "Different parsing output for input %v. Repo parse = %v, regular parser = %v", test[0], ref, ref2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestListTags(t *testing.T) {
|
||||
// Invalid command-line arguments
|
||||
for _, args := range [][]string{
|
||||
{},
|
||||
{"a1", "a2"},
|
||||
} {
|
||||
out, err := runSkopeo(append([]string{"list-tags"}, args...)...)
|
||||
assertTestFailed(t, out, err, "Exactly one non-option argument expected")
|
||||
}
|
||||
|
||||
// FIXME: Much more test coverage
|
||||
// Actual feature tests exist in systemtest
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"go.podman.io/common/pkg/auth"
|
||||
commonFlag "go.podman.io/common/pkg/flag"
|
||||
"go.podman.io/image/v5/types"
|
||||
)
|
||||
|
||||
type loginOptions struct {
|
||||
global *globalOptions
|
||||
loginOpts auth.LoginOptions
|
||||
tlsVerify commonFlag.OptionalBool
|
||||
}
|
||||
|
||||
func loginCmd(global *globalOptions) *cobra.Command {
|
||||
opts := loginOptions{
|
||||
global: global,
|
||||
}
|
||||
cmd := &cobra.Command{
|
||||
Use: "login [command options] REGISTRY",
|
||||
Short: "Login to a container registry",
|
||||
Long: "Login to a container registry on a specified server.",
|
||||
RunE: commandAction(opts.run),
|
||||
Example: `skopeo login quay.io`,
|
||||
}
|
||||
adjustUsage(cmd)
|
||||
flags := cmd.Flags()
|
||||
flags.AddFlagSet(auth.GetLoginFlags(&opts.loginOpts))
|
||||
commonFlag.OptionalBoolFlag(flags, &opts.tlsVerify, "tls-verify", "require HTTPS and verify certificates when accessing the registry")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (opts *loginOptions) run(args []string, stdout io.Writer) error {
|
||||
ctx, cancel := opts.global.commandTimeoutContext()
|
||||
defer cancel()
|
||||
opts.loginOpts.Stdout = stdout
|
||||
opts.loginOpts.Stdin = os.Stdin
|
||||
opts.loginOpts.AcceptRepositories = true
|
||||
sys := opts.global.newSystemContext()
|
||||
if opts.tlsVerify.Present() {
|
||||
sys.DockerInsecureSkipTLSVerify = types.NewOptionalBool(!opts.tlsVerify.Value())
|
||||
}
|
||||
return auth.Login(ctx, sys, &opts.loginOpts, args)
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestLogin(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
authFile := filepath.Join(dir, "auth.json")
|
||||
compatAuthFile := filepath.Join(dir, "config.json")
|
||||
|
||||
// Just a trivial smoke-test exercising one error-handling path.
|
||||
// We can’t test full operation without a registry, unit tests should mostly
|
||||
// exist in c/common/pkg/auth, not here.
|
||||
out, err := runSkopeo("login", "--authfile", authFile, "--compat-auth-file", compatAuthFile, "example.com")
|
||||
assertTestFailed(t, out, err, "options for paths to the credential file and to the Docker-compatible credential file can not be set simultaneously")
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"io"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"go.podman.io/common/pkg/auth"
|
||||
commonFlag "go.podman.io/common/pkg/flag"
|
||||
"go.podman.io/image/v5/types"
|
||||
)
|
||||
|
||||
type logoutOptions struct {
|
||||
global *globalOptions
|
||||
logoutOpts auth.LogoutOptions
|
||||
tlsVerify commonFlag.OptionalBool
|
||||
}
|
||||
|
||||
func logoutCmd(global *globalOptions) *cobra.Command {
|
||||
opts := logoutOptions{
|
||||
global: global,
|
||||
}
|
||||
cmd := &cobra.Command{
|
||||
Use: "logout [command options] REGISTRY",
|
||||
Short: "Logout of a container registry",
|
||||
Long: "Logout of a container registry on a specified server.",
|
||||
RunE: commandAction(opts.run),
|
||||
Example: `skopeo logout quay.io`,
|
||||
}
|
||||
adjustUsage(cmd)
|
||||
flags := cmd.Flags()
|
||||
flags.AddFlagSet(auth.GetLogoutFlags(&opts.logoutOpts))
|
||||
commonFlag.OptionalBoolFlag(flags, &opts.tlsVerify, "tls-verify", "require HTTPS and verify certificates when accessing the registry")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (opts *logoutOptions) run(args []string, stdout io.Writer) error {
|
||||
opts.logoutOpts.Stdout = stdout
|
||||
opts.logoutOpts.AcceptRepositories = true
|
||||
sys := opts.global.newSystemContext()
|
||||
if opts.tlsVerify.Present() {
|
||||
sys.DockerInsecureSkipTLSVerify = types.NewOptionalBool(!opts.tlsVerify.Value())
|
||||
}
|
||||
return auth.Logout(sys, &opts.logoutOpts, args)
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestLogout(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
authFile := filepath.Join(dir, "auth.json")
|
||||
compatAuthFile := filepath.Join(dir, "config.json")
|
||||
|
||||
// Just a trivial smoke-test exercising one error-handling path.
|
||||
// We can’t test full operation without a registry, unit tests should mostly
|
||||
// exist in c/common/pkg/auth, not here.
|
||||
err := os.WriteFile(authFile, []byte("{}"), 0o700)
|
||||
require.NoError(t, err)
|
||||
err = os.WriteFile(compatAuthFile, []byte("{}"), 0o700)
|
||||
require.NoError(t, err)
|
||||
out, err := runSkopeo("logout", "--authfile", authFile, "--compat-auth-file", compatAuthFile, "example.com")
|
||||
assertTestFailed(t, out, err, "options for paths to the credential file and to the Docker-compatible credential file can not be set simultaneously")
|
||||
}
|
|
@ -0,0 +1,185 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/containers/skopeo/version"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
commonFlag "go.podman.io/common/pkg/flag"
|
||||
"go.podman.io/image/v5/signature"
|
||||
"go.podman.io/image/v5/types"
|
||||
"go.podman.io/storage/pkg/reexec"
|
||||
)
|
||||
|
||||
// gitCommit will be the hash that the binary was built from
|
||||
// and will be populated by the Makefile
|
||||
var gitCommit = ""
|
||||
|
||||
var defaultUserAgent = "skopeo/" + version.Version
|
||||
|
||||
type globalOptions struct {
|
||||
debug bool // Enable debug output
|
||||
tlsVerify commonFlag.OptionalBool // Require HTTPS and verify certificates (for docker: and docker-daemon:)
|
||||
policyPath string // Path to a signature verification policy file
|
||||
insecurePolicy bool // Use an "allow everything" signature verification policy
|
||||
registriesDirPath string // Path to a "registries.d" registry configuration directory
|
||||
overrideArch string // Architecture to use for choosing images, instead of the runtime one
|
||||
overrideOS string // OS to use for choosing images, instead of the runtime one
|
||||
overrideVariant string // Architecture variant to use for choosing images, instead of the runtime one
|
||||
commandTimeout time.Duration // Timeout for the command execution
|
||||
registriesConfPath string // Path to the "registries.conf" file
|
||||
tmpDir string // Path to use for big temporary files
|
||||
}
|
||||
|
||||
// requireSubcommand returns an error if no sub command is provided
|
||||
// This was copied from podman: `github.com/containers/podman/cmd/podman/validate/args.go
|
||||
// Some small style changes to match skopeo were applied, but try to apply any
|
||||
// bugfixes there first.
|
||||
func requireSubcommand(cmd *cobra.Command, args []string) error {
|
||||
if len(args) > 0 {
|
||||
suggestions := cmd.SuggestionsFor(args[0])
|
||||
if len(suggestions) == 0 {
|
||||
return fmt.Errorf("Unrecognized command `%[1]s %[2]s`\nTry '%[1]s --help' for more information", cmd.CommandPath(), args[0])
|
||||
}
|
||||
return fmt.Errorf("Unrecognized command `%[1]s %[2]s`\n\nDid you mean this?\n\t%[3]s\n\nTry '%[1]s --help' for more information", cmd.CommandPath(), args[0], strings.Join(suggestions, "\n\t"))
|
||||
}
|
||||
return fmt.Errorf("Missing command '%[1]s COMMAND'\nTry '%[1]s --help' for more information", cmd.CommandPath())
|
||||
}
|
||||
|
||||
// createApp returns a cobra.Command, and the underlying globalOptions object, to be run or tested.
|
||||
func createApp() (*cobra.Command, *globalOptions) {
|
||||
opts := globalOptions{}
|
||||
|
||||
rootCommand := &cobra.Command{
|
||||
Use: "skopeo",
|
||||
Long: "Various operations with container images and container image registries",
|
||||
RunE: requireSubcommand,
|
||||
PersistentPreRunE: opts.before,
|
||||
SilenceUsage: true,
|
||||
SilenceErrors: true,
|
||||
// Hide the completion command which is provided by cobra
|
||||
CompletionOptions: cobra.CompletionOptions{HiddenDefaultCmd: true},
|
||||
// This is documented to parse "local" (non-PersistentFlags) flags of parent commands before
|
||||
// running subcommands and handling their options. We don't really run into such cases,
|
||||
// because all of our flags on rootCommand are in PersistentFlags, except for the deprecated --tls-verify;
|
||||
// in that case we need TraverseChildren so that we can distinguish between
|
||||
// (skopeo --tls-verify inspect) (causes a warning) and (skopeo inspect --tls-verify) (no warning).
|
||||
TraverseChildren: true,
|
||||
}
|
||||
if gitCommit != "" {
|
||||
rootCommand.Version = fmt.Sprintf("%s commit: %s", version.Version, gitCommit)
|
||||
} else {
|
||||
rootCommand.Version = version.Version
|
||||
}
|
||||
// Override default `--version` global flag to enable `-v` shorthand
|
||||
var dummyVersion bool
|
||||
rootCommand.Flags().BoolVarP(&dummyVersion, "version", "v", false, "Version for Skopeo")
|
||||
rootCommand.PersistentFlags().BoolVar(&opts.debug, "debug", false, "enable debug output")
|
||||
rootCommand.PersistentFlags().StringVar(&opts.policyPath, "policy", "", "Path to a trust policy file")
|
||||
rootCommand.PersistentFlags().BoolVar(&opts.insecurePolicy, "insecure-policy", false, "run the tool without any policy check")
|
||||
rootCommand.PersistentFlags().StringVar(&opts.registriesDirPath, "registries.d", "", "use registry configuration files in `DIR` (e.g. for container signature storage)")
|
||||
rootCommand.PersistentFlags().StringVar(&opts.overrideArch, "override-arch", "", "use `ARCH` instead of the architecture of the machine for choosing images")
|
||||
rootCommand.PersistentFlags().StringVar(&opts.overrideOS, "override-os", "", "use `OS` instead of the running OS for choosing images")
|
||||
rootCommand.PersistentFlags().StringVar(&opts.overrideVariant, "override-variant", "", "use `VARIANT` instead of the running architecture variant for choosing images")
|
||||
rootCommand.PersistentFlags().DurationVar(&opts.commandTimeout, "command-timeout", 0, "timeout for the command execution")
|
||||
rootCommand.PersistentFlags().StringVar(&opts.registriesConfPath, "registries-conf", "", "path to the registries.conf file")
|
||||
if err := rootCommand.PersistentFlags().MarkHidden("registries-conf"); err != nil {
|
||||
logrus.Fatal("unable to mark registries-conf flag as hidden")
|
||||
}
|
||||
rootCommand.PersistentFlags().StringVar(&opts.tmpDir, "tmpdir", "", "directory used to store temporary files")
|
||||
flag := commonFlag.OptionalBoolFlag(rootCommand.Flags(), &opts.tlsVerify, "tls-verify", "Require HTTPS and verify certificates when accessing the registry")
|
||||
flag.Hidden = true
|
||||
rootCommand.AddCommand(
|
||||
copyCmd(&opts),
|
||||
deleteCmd(&opts),
|
||||
generateSigstoreKeyCmd(),
|
||||
inspectCmd(&opts),
|
||||
layersCmd(&opts),
|
||||
loginCmd(&opts),
|
||||
logoutCmd(&opts),
|
||||
manifestDigestCmd(),
|
||||
proxyCmd(&opts),
|
||||
syncCmd(&opts),
|
||||
standaloneSignCmd(),
|
||||
standaloneVerifyCmd(),
|
||||
tagsCmd(&opts),
|
||||
untrustedSignatureDumpCmd(),
|
||||
)
|
||||
return rootCommand, &opts
|
||||
}
|
||||
|
||||
// before is run by the cli package for any command, before running the command-specific handler.
|
||||
func (opts *globalOptions) before(cmd *cobra.Command, args []string) error {
|
||||
if opts.debug {
|
||||
logrus.SetLevel(logrus.DebugLevel)
|
||||
}
|
||||
if opts.tlsVerify.Present() {
|
||||
logrus.Warn("'--tls-verify' is deprecated, please set this on the specific subcommand")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
if reexec.Init() {
|
||||
return
|
||||
}
|
||||
rootCmd, _ := createApp()
|
||||
if err := rootCmd.Execute(); err != nil {
|
||||
if isNotFoundImageError(err) {
|
||||
logrus.StandardLogger().Log(logrus.FatalLevel, err)
|
||||
logrus.Exit(2)
|
||||
}
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
// getPolicyContext returns a *signature.PolicyContext based on opts.
|
||||
func (opts *globalOptions) getPolicyContext() (*signature.PolicyContext, error) {
|
||||
var policy *signature.Policy // This could be cached across calls in opts.
|
||||
var err error
|
||||
if opts.insecurePolicy {
|
||||
policy = &signature.Policy{Default: []signature.PolicyRequirement{signature.NewPRInsecureAcceptAnything()}}
|
||||
} else if opts.policyPath == "" {
|
||||
policy, err = signature.DefaultPolicy(nil)
|
||||
} else {
|
||||
policy, err = signature.NewPolicyFromFile(opts.policyPath)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return signature.NewPolicyContext(policy)
|
||||
}
|
||||
|
||||
// commandTimeoutContext returns a context.Context and a cancellation callback based on opts.
|
||||
// The caller should usually "defer cancel()" immediately after calling this.
|
||||
func (opts *globalOptions) commandTimeoutContext() (context.Context, context.CancelFunc) {
|
||||
ctx := context.Background()
|
||||
var cancel context.CancelFunc = func() {}
|
||||
if opts.commandTimeout > 0 {
|
||||
ctx, cancel = context.WithTimeout(ctx, opts.commandTimeout)
|
||||
}
|
||||
return ctx, cancel
|
||||
}
|
||||
|
||||
// newSystemContext returns a *types.SystemContext corresponding to opts.
|
||||
// It is guaranteed to return a fresh instance, so it is safe to make additional updates to it.
|
||||
func (opts *globalOptions) newSystemContext() *types.SystemContext {
|
||||
ctx := &types.SystemContext{
|
||||
RegistriesDirPath: opts.registriesDirPath,
|
||||
ArchitectureChoice: opts.overrideArch,
|
||||
OSChoice: opts.overrideOS,
|
||||
VariantChoice: opts.overrideVariant,
|
||||
SystemRegistriesConfPath: opts.registriesConfPath,
|
||||
BigFilesTemporaryDir: opts.tmpDir,
|
||||
DockerRegistryUserAgent: defaultUserAgent,
|
||||
}
|
||||
// DEPRECATED: We support this for backward compatibility, but override it if a per-image flag is provided.
|
||||
if opts.tlsVerify.Present() {
|
||||
ctx.DockerInsecureSkipTLSVerify = types.NewOptionalBool(!opts.tlsVerify.Value())
|
||||
}
|
||||
return ctx
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"go.podman.io/image/v5/types"
|
||||
)
|
||||
|
||||
// runSkopeo creates an app object and runs it with args, with an implied first "skopeo".
|
||||
// Returns output intended for stdout and the returned error, if any.
|
||||
func runSkopeo(args ...string) (string, error) {
|
||||
app, _ := createApp()
|
||||
stdout := bytes.Buffer{}
|
||||
app.SetOut(&stdout)
|
||||
app.SetArgs(args)
|
||||
err := app.Execute()
|
||||
return stdout.String(), err
|
||||
}
|
||||
|
||||
func TestGlobalOptionsNewSystemContext(t *testing.T) {
|
||||
// Default state
|
||||
opts, _ := fakeGlobalOptions(t, []string{})
|
||||
res := opts.newSystemContext()
|
||||
assert.Equal(t, &types.SystemContext{
|
||||
// User-Agent is set by default.
|
||||
DockerRegistryUserAgent: defaultUserAgent,
|
||||
}, res)
|
||||
// Set everything to non-default values.
|
||||
opts, _ = fakeGlobalOptions(t, []string{
|
||||
"--registries.d", "/srv/registries.d",
|
||||
"--override-arch", "overridden-arch",
|
||||
"--override-os", "overridden-os",
|
||||
"--override-variant", "overridden-variant",
|
||||
"--tmpdir", "/srv",
|
||||
"--registries-conf", "/srv/registries.conf",
|
||||
"--tls-verify=false",
|
||||
})
|
||||
res = opts.newSystemContext()
|
||||
assert.Equal(t, &types.SystemContext{
|
||||
RegistriesDirPath: "/srv/registries.d",
|
||||
ArchitectureChoice: "overridden-arch",
|
||||
OSChoice: "overridden-os",
|
||||
VariantChoice: "overridden-variant",
|
||||
BigFilesTemporaryDir: "/srv",
|
||||
SystemRegistriesConfPath: "/srv/registries.conf",
|
||||
DockerInsecureSkipTLSVerify: types.OptionalBoolTrue,
|
||||
DockerRegistryUserAgent: defaultUserAgent,
|
||||
}, res)
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"go.podman.io/image/v5/manifest"
|
||||
)
|
||||
|
||||
type manifestDigestOptions struct {
|
||||
}
|
||||
|
||||
func manifestDigestCmd() *cobra.Command {
|
||||
var opts manifestDigestOptions
|
||||
cmd := &cobra.Command{
|
||||
Use: "manifest-digest MANIFEST-FILE",
|
||||
Short: "Compute a manifest digest of a file",
|
||||
RunE: commandAction(opts.run),
|
||||
Example: "skopeo manifest-digest manifest.json",
|
||||
}
|
||||
adjustUsage(cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (opts *manifestDigestOptions) run(args []string, stdout io.Writer) error {
|
||||
if len(args) != 1 {
|
||||
return errors.New("Usage: skopeo manifest-digest manifest")
|
||||
}
|
||||
manifestPath := args[0]
|
||||
|
||||
man, err := os.ReadFile(manifestPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error reading manifest from %s: %v", manifestPath, err)
|
||||
}
|
||||
digest, err := manifest.Digest(man)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error computing digest: %v", err)
|
||||
}
|
||||
fmt.Fprintf(stdout, "%s\n", digest)
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestManifestDigest(t *testing.T) {
|
||||
// Invalid command-line arguments
|
||||
for _, args := range [][]string{
|
||||
{},
|
||||
{"a1", "a2"},
|
||||
} {
|
||||
out, err := runSkopeo(append([]string{"manifest-digest"}, args...)...)
|
||||
assertTestFailed(t, out, err, "Usage")
|
||||
}
|
||||
|
||||
// Error reading manifest
|
||||
out, err := runSkopeo("manifest-digest", "/this/does/not/exist")
|
||||
assertTestFailed(t, out, err, "/this/does/not/exist")
|
||||
|
||||
// Error computing manifest
|
||||
out, err = runSkopeo("manifest-digest", "fixtures/v2s1-invalid-signatures.manifest.json")
|
||||
assertTestFailed(t, out, err, "computing digest")
|
||||
|
||||
// Success
|
||||
out, err = runSkopeo("manifest-digest", "fixtures/image.manifest.json")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, fixturesTestImageManifestDigest.String()+"\n", out)
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,29 @@
|
|||
//go:build windows
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type proxyOptions struct {
|
||||
global *globalOptions
|
||||
}
|
||||
|
||||
func proxyCmd(global *globalOptions) *cobra.Command {
|
||||
opts := proxyOptions{global: global}
|
||||
cmd := &cobra.Command{
|
||||
RunE: commandAction(opts.run),
|
||||
Args: cobra.ExactArgs(0),
|
||||
// Not stabilized yet
|
||||
Hidden: true,
|
||||
}
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (opts *proxyOptions) run(args []string, stdout io.Writer) error {
|
||||
return fmt.Errorf("This command is not supported on Windows")
|
||||
}
|
|
@ -0,0 +1,186 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"go.podman.io/image/v5/pkg/cli"
|
||||
"go.podman.io/image/v5/signature"
|
||||
)
|
||||
|
||||
type standaloneSignOptions struct {
|
||||
output string // Output file path
|
||||
passphraseFile string // Path pointing to a passphrase file when signing
|
||||
}
|
||||
|
||||
func standaloneSignCmd() *cobra.Command {
|
||||
opts := standaloneSignOptions{}
|
||||
cmd := &cobra.Command{
|
||||
Use: "standalone-sign [command options] MANIFEST DOCKER-REFERENCE KEY-FINGERPRINT --output|-o SIGNATURE",
|
||||
Short: "Create a signature using local files",
|
||||
RunE: commandAction(opts.run),
|
||||
}
|
||||
adjustUsage(cmd)
|
||||
flags := cmd.Flags()
|
||||
flags.StringVarP(&opts.output, "output", "o", "", "output the signature to `SIGNATURE`")
|
||||
flags.StringVarP(&opts.passphraseFile, "passphrase-file", "", "", "file that contains a passphrase for the --sign-by key")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (opts *standaloneSignOptions) run(args []string, stdout io.Writer) error {
|
||||
if len(args) != 3 || opts.output == "" {
|
||||
return errors.New("Usage: skopeo standalone-sign manifest docker-reference key-fingerprint -o signature")
|
||||
}
|
||||
manifestPath := args[0]
|
||||
dockerReference := args[1]
|
||||
fingerprint := args[2]
|
||||
|
||||
manifest, err := os.ReadFile(manifestPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error reading %s: %w", manifestPath, err)
|
||||
}
|
||||
|
||||
mech, err := signature.NewGPGSigningMechanism()
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error initializing GPG: %w", err)
|
||||
}
|
||||
defer mech.Close()
|
||||
|
||||
passphrase, err := cli.ReadPassphraseFile(opts.passphraseFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
signature, err := signature.SignDockerManifestWithOptions(manifest, dockerReference, mech, fingerprint, &signature.SignOptions{Passphrase: passphrase})
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error creating signature: %w", err)
|
||||
}
|
||||
|
||||
if err := os.WriteFile(opts.output, signature, 0644); err != nil {
|
||||
return fmt.Errorf("Error writing signature to %s: %w", opts.output, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type standaloneVerifyOptions struct {
|
||||
publicKeyFile string
|
||||
}
|
||||
|
||||
func standaloneVerifyCmd() *cobra.Command {
|
||||
opts := standaloneVerifyOptions{}
|
||||
cmd := &cobra.Command{
|
||||
Use: "standalone-verify MANIFEST DOCKER-REFERENCE KEY-FINGERPRINTS SIGNATURE",
|
||||
Short: "Verify a signature using local files",
|
||||
Long: `Verify a signature using local files
|
||||
|
||||
KEY-FINGERPRINTS can be a comma separated list of fingerprints, or "any" if you trust all the keys in the public key file.`,
|
||||
RunE: commandAction(opts.run),
|
||||
}
|
||||
flags := cmd.Flags()
|
||||
flags.StringVar(&opts.publicKeyFile, "public-key-file", "", `File containing public keys. If not specified, will use local GPG keys.`)
|
||||
adjustUsage(cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (opts *standaloneVerifyOptions) run(args []string, stdout io.Writer) error {
|
||||
if len(args) != 4 {
|
||||
return errors.New("Usage: skopeo standalone-verify manifest docker-reference key-fingerprint signature")
|
||||
}
|
||||
manifestPath := args[0]
|
||||
expectedDockerReference := args[1]
|
||||
expectedFingerprints := strings.Split(args[2], ",")
|
||||
signaturePath := args[3]
|
||||
|
||||
if opts.publicKeyFile == "" && len(expectedFingerprints) == 1 && expectedFingerprints[0] == "any" {
|
||||
return fmt.Errorf("Cannot use any fingerprint without a public key file")
|
||||
}
|
||||
unverifiedManifest, err := os.ReadFile(manifestPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error reading manifest from %s: %w", manifestPath, err)
|
||||
}
|
||||
unverifiedSignature, err := os.ReadFile(signaturePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error reading signature from %s: %w", signaturePath, err)
|
||||
}
|
||||
|
||||
var mech signature.SigningMechanism
|
||||
var publicKeyfingerprints []string
|
||||
if opts.publicKeyFile != "" {
|
||||
publicKeys, err := os.ReadFile(opts.publicKeyFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error reading public keys from %s: %w", opts.publicKeyFile, err)
|
||||
}
|
||||
mech, publicKeyfingerprints, err = signature.NewEphemeralGPGSigningMechanism(publicKeys)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error initializing GPG: %w", err)
|
||||
|
||||
}
|
||||
} else {
|
||||
mech, err = signature.NewGPGSigningMechanism()
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error initializing GPG: %w", err)
|
||||
}
|
||||
}
|
||||
defer mech.Close()
|
||||
|
||||
if len(expectedFingerprints) == 1 && expectedFingerprints[0] == "any" {
|
||||
expectedFingerprints = publicKeyfingerprints
|
||||
}
|
||||
|
||||
sig, verificationFingerprint, err := signature.VerifyImageManifestSignatureUsingKeyIdentityList(unverifiedSignature, unverifiedManifest, expectedDockerReference, mech, expectedFingerprints)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error verifying signature: %w", err)
|
||||
}
|
||||
|
||||
fmt.Fprintf(stdout, "Signature verified using fingerprint %s, digest %s\n", verificationFingerprint, sig.DockerManifestDigest)
|
||||
return nil
|
||||
}
|
||||
|
||||
// WARNING: Do not use the contents of this for ANY security decisions,
|
||||
// and be VERY CAREFUL about showing this information to humans in any way which suggest that these values “are probably” reliable.
|
||||
// There is NO REASON to expect the values to be correct, or not intentionally misleading
|
||||
// (including things like “✅ Verified by $authority”)
|
||||
//
|
||||
// The subcommand is undocumented, and it may be renamed or entirely disappear in the future.
|
||||
type untrustedSignatureDumpOptions struct {
|
||||
}
|
||||
|
||||
func untrustedSignatureDumpCmd() *cobra.Command {
|
||||
opts := untrustedSignatureDumpOptions{}
|
||||
cmd := &cobra.Command{
|
||||
Use: "untrusted-signature-dump-without-verification SIGNATURE",
|
||||
Short: "Dump contents of a signature WITHOUT VERIFYING IT",
|
||||
RunE: commandAction(opts.run),
|
||||
Hidden: true,
|
||||
}
|
||||
adjustUsage(cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (opts *untrustedSignatureDumpOptions) run(args []string, stdout io.Writer) error {
|
||||
if len(args) != 1 {
|
||||
return errors.New("Usage: skopeo untrusted-signature-dump-without-verification signature")
|
||||
}
|
||||
untrustedSignaturePath := args[0]
|
||||
|
||||
untrustedSignature, err := os.ReadFile(untrustedSignaturePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error reading untrusted signature from %s: %w", untrustedSignaturePath, err)
|
||||
}
|
||||
|
||||
untrustedInfo, err := signature.GetUntrustedSignatureInformationWithoutVerifying(untrustedSignature)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error decoding untrusted signature: %v", err)
|
||||
}
|
||||
untrustedOut, err := json.MarshalIndent(untrustedInfo, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Fprintln(stdout, string(untrustedOut))
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,199 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/opencontainers/go-digest"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.podman.io/image/v5/signature"
|
||||
)
|
||||
|
||||
const (
|
||||
// fixturesTestImageManifestDigest is the Docker manifest digest of "image.manifest.json"
|
||||
fixturesTestImageManifestDigest = digest.Digest("sha256:20bf21ed457b390829cdbeec8795a7bea1626991fda603e0d01b4e7f60427e55")
|
||||
// fixturesTestKeyFingerprint is the fingerprint of the private key.
|
||||
fixturesTestKeyFingerprint = "08CD26E446E2E95249B7A405E932F44B23E8DD43"
|
||||
// fixturesTestKeyFingerprint is the key ID of the private key.
|
||||
fixturesTestKeyShortID = "E932F44B23E8DD43"
|
||||
)
|
||||
|
||||
// Test that results of runSkopeo failed with nothing on stdout, and substring
|
||||
// within the error message.
|
||||
func assertTestFailed(t *testing.T, stdout string, err error, substring string) {
|
||||
assert.ErrorContains(t, err, substring)
|
||||
assert.Empty(t, stdout)
|
||||
}
|
||||
|
||||
func TestStandaloneSign(t *testing.T) {
|
||||
mech, _, err := signature.NewEphemeralGPGSigningMechanism([]byte{})
|
||||
require.NoError(t, err)
|
||||
defer mech.Close()
|
||||
if err := mech.SupportsSigning(); err != nil {
|
||||
t.Skipf("Signing not supported: %v", err)
|
||||
}
|
||||
|
||||
manifestPath := "fixtures/image.manifest.json"
|
||||
dockerReference := "testing/manifest"
|
||||
t.Setenv("GNUPGHOME", "fixtures")
|
||||
|
||||
// Invalid command-line arguments
|
||||
for _, args := range [][]string{
|
||||
{},
|
||||
{"a1", "a2"},
|
||||
{"a1", "a2", "a3"},
|
||||
{"a1", "a2", "a3", "a4"},
|
||||
{"-o", "o", "a1", "a2"},
|
||||
{"-o", "o", "a1", "a2", "a3", "a4"},
|
||||
} {
|
||||
out, err := runSkopeo(append([]string{"standalone-sign"}, args...)...)
|
||||
assertTestFailed(t, out, err, "Usage")
|
||||
}
|
||||
|
||||
// Error reading manifest
|
||||
out, err := runSkopeo("standalone-sign", "-o", "/dev/null",
|
||||
"/this/does/not/exist", dockerReference, fixturesTestKeyFingerprint)
|
||||
assertTestFailed(t, out, err, "/this/does/not/exist")
|
||||
|
||||
// Invalid Docker reference
|
||||
out, err = runSkopeo("standalone-sign", "-o", "/dev/null",
|
||||
manifestPath, "" /* empty reference */, fixturesTestKeyFingerprint)
|
||||
assertTestFailed(t, out, err, "empty signature content")
|
||||
|
||||
// Unknown key.
|
||||
out, err = runSkopeo("standalone-sign", "-o", "/dev/null",
|
||||
manifestPath, dockerReference, "UNKNOWN GPG FINGERPRINT")
|
||||
assert.Error(t, err)
|
||||
assert.Empty(t, out)
|
||||
|
||||
// Error writing output
|
||||
out, err = runSkopeo("standalone-sign", "-o", "/dev/full",
|
||||
manifestPath, dockerReference, fixturesTestKeyFingerprint)
|
||||
assertTestFailed(t, out, err, "/dev/full")
|
||||
|
||||
// Success
|
||||
sigOutput, err := os.CreateTemp("", "sig")
|
||||
require.NoError(t, err)
|
||||
defer os.Remove(sigOutput.Name())
|
||||
out, err = runSkopeo("standalone-sign", "-o", sigOutput.Name(),
|
||||
manifestPath, dockerReference, fixturesTestKeyFingerprint)
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, out)
|
||||
|
||||
sig, err := os.ReadFile(sigOutput.Name())
|
||||
require.NoError(t, err)
|
||||
manifest, err := os.ReadFile(manifestPath)
|
||||
require.NoError(t, err)
|
||||
mech, err = signature.NewGPGSigningMechanism()
|
||||
require.NoError(t, err)
|
||||
defer mech.Close()
|
||||
verified, err := signature.VerifyDockerManifestSignature(sig, manifest, dockerReference, mech, fixturesTestKeyFingerprint)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, dockerReference, verified.DockerReference)
|
||||
assert.Equal(t, fixturesTestImageManifestDigest, verified.DockerManifestDigest)
|
||||
}
|
||||
|
||||
func TestStandaloneVerify(t *testing.T) {
|
||||
manifestPath := "fixtures/image.manifest.json"
|
||||
signaturePath := "fixtures/image.signature"
|
||||
dockerReference := "testing/manifest"
|
||||
t.Setenv("GNUPGHOME", "fixtures")
|
||||
|
||||
// Invalid command-line arguments
|
||||
for _, args := range [][]string{
|
||||
{},
|
||||
{"a1", "a2", "a3"},
|
||||
{"a1", "a2", "a3", "a4", "a5"},
|
||||
} {
|
||||
out, err := runSkopeo(append([]string{"standalone-verify"}, args...)...)
|
||||
assertTestFailed(t, out, err, "Usage")
|
||||
}
|
||||
|
||||
// Error reading manifest
|
||||
out, err := runSkopeo("standalone-verify", "/this/does/not/exist",
|
||||
dockerReference, fixturesTestKeyFingerprint, signaturePath)
|
||||
assertTestFailed(t, out, err, "/this/does/not/exist")
|
||||
|
||||
// Error reading signature
|
||||
out, err = runSkopeo("standalone-verify", manifestPath,
|
||||
dockerReference, fixturesTestKeyFingerprint, "/this/does/not/exist")
|
||||
assertTestFailed(t, out, err, "/this/does/not/exist")
|
||||
|
||||
// Error verifying signature
|
||||
out, err = runSkopeo("standalone-verify", manifestPath,
|
||||
dockerReference, fixturesTestKeyFingerprint, "fixtures/corrupt.signature")
|
||||
assertTestFailed(t, out, err, "Error verifying signature")
|
||||
|
||||
// Error using any without a public key file
|
||||
out, err = runSkopeo("standalone-verify", manifestPath,
|
||||
dockerReference, "any", signaturePath)
|
||||
assertTestFailed(t, out, err, "Cannot use any fingerprint without a public key file")
|
||||
|
||||
// Success
|
||||
out, err = runSkopeo("standalone-verify", manifestPath,
|
||||
dockerReference, fixturesTestKeyFingerprint, signaturePath)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "Signature verified using fingerprint "+fixturesTestKeyFingerprint+", digest "+fixturesTestImageManifestDigest.String()+"\n", out)
|
||||
|
||||
// Using multiple fingerprints
|
||||
out, err = runSkopeo("standalone-verify", manifestPath,
|
||||
dockerReference, "0123456789ABCDEF0123456789ABCDEF01234567,"+fixturesTestKeyFingerprint+",DEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEF", signaturePath)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "Signature verified using fingerprint "+fixturesTestKeyFingerprint+", digest "+fixturesTestImageManifestDigest.String()+"\n", out)
|
||||
|
||||
// Using a public key file
|
||||
t.Setenv("GNUPGHOME", "")
|
||||
out, err = runSkopeo("standalone-verify", "--public-key-file", "fixtures/pubring.gpg", manifestPath,
|
||||
dockerReference, fixturesTestKeyFingerprint, signaturePath)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "Signature verified using fingerprint "+fixturesTestKeyFingerprint+", digest "+fixturesTestImageManifestDigest.String()+"\n", out)
|
||||
|
||||
// Using a public key file matching any public key
|
||||
t.Setenv("GNUPGHOME", "")
|
||||
out, err = runSkopeo("standalone-verify", "--public-key-file", "fixtures/pubring.gpg", manifestPath,
|
||||
dockerReference, "any", signaturePath)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "Signature verified using fingerprint "+fixturesTestKeyFingerprint+", digest "+fixturesTestImageManifestDigest.String()+"\n", out)
|
||||
}
|
||||
|
||||
func TestUntrustedSignatureDump(t *testing.T) {
|
||||
// Invalid command-line arguments
|
||||
for _, args := range [][]string{
|
||||
{},
|
||||
{"a1", "a2"},
|
||||
{"a1", "a2", "a3", "a4"},
|
||||
} {
|
||||
out, err := runSkopeo(append([]string{"untrusted-signature-dump-without-verification"}, args...)...)
|
||||
assertTestFailed(t, out, err, "Usage")
|
||||
}
|
||||
|
||||
// Error reading manifest
|
||||
out, err := runSkopeo("untrusted-signature-dump-without-verification",
|
||||
"/this/does/not/exist")
|
||||
assertTestFailed(t, out, err, "/this/does/not/exist")
|
||||
|
||||
// Error reading signature (input is not a signature)
|
||||
out, err = runSkopeo("untrusted-signature-dump-without-verification", "fixtures/image.manifest.json")
|
||||
assertTestFailed(t, out, err, "Error decoding untrusted signature")
|
||||
|
||||
// Success
|
||||
for _, path := range []string{"fixtures/image.signature", "fixtures/corrupt.signature"} {
|
||||
// Success
|
||||
out, err = runSkopeo("untrusted-signature-dump-without-verification", path)
|
||||
require.NoError(t, err)
|
||||
|
||||
var info signature.UntrustedSignatureInformation
|
||||
err := json.Unmarshal([]byte(out), &info)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, fixturesTestImageManifestDigest, info.UntrustedDockerManifestDigest)
|
||||
assert.Equal(t, "testing/manifest", info.UntrustedDockerReference)
|
||||
assert.NotNil(t, info.UntrustedCreatorID)
|
||||
assert.Equal(t, "atomic ", *info.UntrustedCreatorID)
|
||||
assert.NotNil(t, info.UntrustedTimestamp)
|
||||
assert.True(t, time.Unix(1458239713, 0).Equal(*info.UntrustedTimestamp))
|
||||
assert.Equal(t, fixturesTestKeyShortID, info.UntrustedShortKeyIdentifier)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,752 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/Masterminds/semver/v3"
|
||||
"github.com/opencontainers/go-digest"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
"go.podman.io/common/pkg/retry"
|
||||
"go.podman.io/image/v5/copy"
|
||||
"go.podman.io/image/v5/directory"
|
||||
"go.podman.io/image/v5/docker"
|
||||
"go.podman.io/image/v5/docker/reference"
|
||||
"go.podman.io/image/v5/manifest"
|
||||
"go.podman.io/image/v5/transports"
|
||||
"go.podman.io/image/v5/types"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// syncOptions contains information retrieved from the skopeo sync command line.
|
||||
type syncOptions struct {
|
||||
global *globalOptions // Global (not command dependent) skopeo options
|
||||
deprecatedTLSVerify *deprecatedTLSVerifyOption
|
||||
srcImage *imageOptions // Source image options
|
||||
destImage *imageDestOptions // Destination image options
|
||||
retryOpts *retry.Options
|
||||
copy *sharedCopyOptions
|
||||
source string // Source repository name
|
||||
destination string // Destination registry name
|
||||
digestFile string // Write digest to this file
|
||||
scoped bool // When true, namespace copied images at destination using the source repository name
|
||||
all bool // Copy all of the images if an image in the source is a list
|
||||
dryRun bool // Don't actually copy anything, just output what it would have done
|
||||
keepGoing bool // Whether or not to abort the sync if there are any errors during syncing the images
|
||||
appendSuffix string // Suffix to append to destination image tag
|
||||
}
|
||||
|
||||
// repoDescriptor contains information of a single repository used as a sync source.
|
||||
type repoDescriptor struct {
|
||||
DirBasePath string // base path when source is 'dir'
|
||||
ImageRefs []types.ImageReference // List of tagged image found for the repository
|
||||
Context *types.SystemContext // SystemContext for the sync command
|
||||
}
|
||||
|
||||
// tlsVerifyConfig is an implementation of the Unmarshaler interface, used to
|
||||
// customize the unmarshaling behaviour of the tls-verify YAML key.
|
||||
type tlsVerifyConfig struct {
|
||||
skip types.OptionalBool // skip TLS verification check (false by default)
|
||||
}
|
||||
|
||||
// registrySyncConfig contains information about a single registry, read from
|
||||
// the source YAML file
|
||||
type registrySyncConfig struct {
|
||||
Images map[string][]string // Images map images name to slices with the images' references (tags, digests)
|
||||
ImagesByTagRegex map[string]string `yaml:"images-by-tag-regex"` // Images map images name to regular expression with the images' tags
|
||||
ImagesBySemver map[string]string `yaml:"images-by-semver"` // ImagesBySemver maps a repository to a semver constraint (e.g. '>=3.14') to match images' tags to
|
||||
Credentials types.DockerAuthConfig // Username and password used to authenticate with the registry
|
||||
TLSVerify tlsVerifyConfig `yaml:"tls-verify"` // TLS verification mode (enabled by default)
|
||||
CertDir string `yaml:"cert-dir"` // Path to the TLS certificates of the registry
|
||||
}
|
||||
|
||||
// sourceConfig contains all registries information read from the source YAML file
|
||||
type sourceConfig map[string]registrySyncConfig
|
||||
|
||||
func syncCmd(global *globalOptions) *cobra.Command {
|
||||
sharedFlags, sharedOpts := sharedImageFlags()
|
||||
deprecatedTLSVerifyFlags, deprecatedTLSVerifyOpt := deprecatedTLSVerifyFlags()
|
||||
srcFlags, srcOpts := dockerImageFlags(global, sharedOpts, deprecatedTLSVerifyOpt, "src-", "screds")
|
||||
destFlags, destOpts := dockerImageFlags(global, sharedOpts, deprecatedTLSVerifyOpt, "dest-", "dcreds")
|
||||
retryFlags, retryOpts := retryFlags()
|
||||
copyFlags, copyOpts := sharedCopyFlags()
|
||||
|
||||
opts := syncOptions{
|
||||
global: global,
|
||||
deprecatedTLSVerify: deprecatedTLSVerifyOpt,
|
||||
srcImage: srcOpts,
|
||||
destImage: &imageDestOptions{imageOptions: destOpts},
|
||||
retryOpts: retryOpts,
|
||||
copy: copyOpts,
|
||||
}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "sync [command options] --src TRANSPORT --dest TRANSPORT SOURCE DESTINATION",
|
||||
Short: "Synchronize one or more images from one location to another",
|
||||
Long: `Copy all the images from a SOURCE to a DESTINATION.
|
||||
|
||||
Allowed SOURCE transports (specified with --src): docker, dir, yaml.
|
||||
Allowed DESTINATION transports (specified with --dest): docker, dir.
|
||||
|
||||
See skopeo-sync(1) for details.
|
||||
`,
|
||||
RunE: commandAction(opts.run),
|
||||
Example: `skopeo sync --src docker --dest dir --scoped registry.example.com/busybox /media/usb`,
|
||||
}
|
||||
adjustUsage(cmd)
|
||||
flags := cmd.Flags()
|
||||
flags.AddFlagSet(&sharedFlags)
|
||||
flags.AddFlagSet(&deprecatedTLSVerifyFlags)
|
||||
flags.AddFlagSet(&srcFlags)
|
||||
flags.AddFlagSet(&destFlags)
|
||||
flags.AddFlagSet(&retryFlags)
|
||||
flags.AddFlagSet(©Flags)
|
||||
flags.StringVarP(&opts.source, "src", "s", "", "SOURCE transport type")
|
||||
flags.StringVarP(&opts.destination, "dest", "d", "", "DESTINATION transport type")
|
||||
flags.BoolVar(&opts.scoped, "scoped", false, "Images at DESTINATION are prefix using the full source image path as scope")
|
||||
flags.StringVar(&opts.appendSuffix, "append-suffix", "", "String to append to DESTINATION tags")
|
||||
flags.StringVar(&opts.digestFile, "digestfile", "", "Write the digests and Image References of the resulting images to the specified file, separated by newlines")
|
||||
flags.BoolVarP(&opts.all, "all", "a", false, "Copy all images if SOURCE-IMAGE is a list")
|
||||
flags.BoolVar(&opts.dryRun, "dry-run", false, "Run without actually copying data")
|
||||
flags.BoolVarP(&opts.keepGoing, "keep-going", "", false, "Do not abort the sync if any image copy fails")
|
||||
return cmd
|
||||
}
|
||||
|
||||
// UnmarshalYAML is the implementation of the Unmarshaler interface method
|
||||
// for the tlsVerifyConfig type.
|
||||
// It unmarshals the 'tls-verify' YAML key so that, when they key is not
|
||||
// specified, tls verification is enforced.
|
||||
func (tls *tlsVerifyConfig) UnmarshalYAML(value *yaml.Node) error {
|
||||
var verify bool
|
||||
if err := value.Decode(&verify); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tls.skip = types.NewOptionalBool(!verify)
|
||||
return nil
|
||||
}
|
||||
|
||||
// newSourceConfig unmarshals the provided YAML file path to the sourceConfig type.
|
||||
// It returns a new unmarshaled sourceConfig object and any error encountered.
|
||||
func newSourceConfig(yamlFile string) (sourceConfig, error) {
|
||||
var cfg sourceConfig
|
||||
source, err := os.ReadFile(yamlFile)
|
||||
if err != nil {
|
||||
return cfg, err
|
||||
}
|
||||
err = yaml.Unmarshal(source, &cfg)
|
||||
if err != nil {
|
||||
return cfg, fmt.Errorf("Failed to unmarshal %q: %w", yamlFile, err)
|
||||
}
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
// parseRepositoryReference parses input into a reference.Named, and verifies that it names a repository, not an image.
|
||||
func parseRepositoryReference(input string) (reference.Named, error) {
|
||||
ref, err := reference.ParseNormalizedNamed(input)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !reference.IsNameOnly(ref) {
|
||||
return nil, errors.New("input names a reference, not a repository")
|
||||
}
|
||||
return ref, nil
|
||||
}
|
||||
|
||||
// destinationReference creates an image reference using the provided transport.
|
||||
// It returns a image reference to be used as destination of an image copy and
|
||||
// any error encountered.
|
||||
func destinationReference(destination string, transport string) (types.ImageReference, error) {
|
||||
var imageTransport types.ImageTransport
|
||||
|
||||
switch transport {
|
||||
case docker.Transport.Name():
|
||||
destination = fmt.Sprintf("//%s", destination)
|
||||
imageTransport = docker.Transport
|
||||
case directory.Transport.Name():
|
||||
_, err := os.Stat(destination)
|
||||
if err == nil {
|
||||
return nil, fmt.Errorf("Refusing to overwrite destination directory %q", destination)
|
||||
}
|
||||
if !os.IsNotExist(err) {
|
||||
return nil, fmt.Errorf("Destination directory could not be used: %w", err)
|
||||
}
|
||||
// the directory holding the image must be created here
|
||||
if err = os.MkdirAll(destination, 0755); err != nil {
|
||||
return nil, fmt.Errorf("Error creating directory for image %s: %w", destination, err)
|
||||
}
|
||||
imageTransport = directory.Transport
|
||||
default:
|
||||
return nil, fmt.Errorf("%q is not a valid destination transport", transport)
|
||||
}
|
||||
logrus.Debugf("Destination for transport %q: %s", transport, destination)
|
||||
|
||||
destRef, err := imageTransport.ParseReference(destination)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Cannot obtain a valid image reference for transport %q and reference %q: %w", imageTransport.Name(), destination, err)
|
||||
}
|
||||
|
||||
return destRef, nil
|
||||
}
|
||||
|
||||
// getImageTags lists all tags in a repository.
|
||||
// It returns a string slice of tags and any error encountered.
|
||||
func getImageTags(ctx context.Context, sysCtx *types.SystemContext, repoRef reference.Named) ([]string, error) {
|
||||
name := repoRef.Name()
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"image": name,
|
||||
}).Info("Getting tags")
|
||||
// Ugly: NewReference rejects IsNameOnly references, and GetRepositoryTags ignores the tag/digest.
|
||||
// So, we use TagNameOnly here only to shut up NewReference
|
||||
dockerRef, err := docker.NewReference(reference.TagNameOnly(repoRef))
|
||||
if err != nil {
|
||||
return nil, err // Should never happen for a reference with tag and no digest
|
||||
}
|
||||
tags, err := docker.GetRepositoryTags(ctx, sysCtx, dockerRef)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Error determining repository tags for repo %s: %w", name, err)
|
||||
}
|
||||
|
||||
return tags, nil
|
||||
}
|
||||
|
||||
// imagesToCopyFromRepo builds a list of image references from the tags
|
||||
// found in a source repository.
|
||||
// It returns an image reference slice with as many elements as the tags found
|
||||
// and any error encountered.
|
||||
func imagesToCopyFromRepo(sys *types.SystemContext, repoRef reference.Named) ([]types.ImageReference, error) {
|
||||
tags, err := getImageTags(context.Background(), sys, repoRef)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var sourceReferences []types.ImageReference
|
||||
for _, tag := range tags {
|
||||
taggedRef, err := reference.WithTag(repoRef, tag)
|
||||
if err != nil {
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"repo": repoRef.Name(),
|
||||
"tag": tag,
|
||||
}).Errorf("Error creating a tagged reference from registry tag list: %v", err)
|
||||
continue
|
||||
}
|
||||
ref, err := docker.NewReference(taggedRef)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Cannot obtain a valid image reference for transport %q and reference %s: %w", docker.Transport.Name(), taggedRef.String(), err)
|
||||
}
|
||||
sourceReferences = append(sourceReferences, ref)
|
||||
}
|
||||
return sourceReferences, nil
|
||||
}
|
||||
|
||||
// imagesToCopyFromDir builds a list of image references from the images found
|
||||
// in the source directory.
|
||||
// It returns an image reference slice with as many elements as the images found
|
||||
// and any error encountered.
|
||||
func imagesToCopyFromDir(dirPath string) ([]types.ImageReference, error) {
|
||||
var sourceReferences []types.ImageReference
|
||||
err := filepath.WalkDir(dirPath, func(path string, d fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !d.IsDir() && d.Name() == "manifest.json" {
|
||||
dirname := filepath.Dir(path)
|
||||
ref, err := directory.Transport.ParseReference(dirname)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Cannot obtain a valid image reference for transport %q and reference %q: %w", directory.Transport.Name(), dirname, err)
|
||||
}
|
||||
sourceReferences = append(sourceReferences, ref)
|
||||
return filepath.SkipDir
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return sourceReferences,
|
||||
fmt.Errorf("Error walking the path %q: %w", dirPath, err)
|
||||
}
|
||||
|
||||
return sourceReferences, nil
|
||||
}
|
||||
|
||||
// imagesToCopyFromRegistry builds a list of repository descriptors from the images
|
||||
// in a registry configuration.
|
||||
// It returns a repository descriptors slice with as many elements as the images
|
||||
// found and any error encountered. Each element of the slice is a list of
|
||||
// image references, to be used as sync source.
|
||||
func imagesToCopyFromRegistry(registryName string, cfg registrySyncConfig, sourceCtx types.SystemContext) ([]repoDescriptor, error) {
|
||||
serverCtx := &sourceCtx
|
||||
// override ctx with per-registryName options
|
||||
serverCtx.DockerCertPath = cfg.CertDir
|
||||
serverCtx.DockerDaemonCertPath = cfg.CertDir
|
||||
serverCtx.DockerDaemonInsecureSkipTLSVerify = (cfg.TLSVerify.skip == types.OptionalBoolTrue)
|
||||
serverCtx.DockerInsecureSkipTLSVerify = cfg.TLSVerify.skip
|
||||
if cfg.Credentials != (types.DockerAuthConfig{}) {
|
||||
serverCtx.DockerAuthConfig = &cfg.Credentials
|
||||
}
|
||||
var repoDescList []repoDescriptor
|
||||
|
||||
if len(cfg.Images) == 0 && len(cfg.ImagesByTagRegex) == 0 && len(cfg.ImagesBySemver) == 0 {
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"registry": registryName,
|
||||
}).Warn("No images specified for registry")
|
||||
return repoDescList, nil
|
||||
}
|
||||
|
||||
for imageName, refs := range cfg.Images {
|
||||
repoLogger := logrus.WithFields(logrus.Fields{
|
||||
"repo": imageName,
|
||||
"registry": registryName,
|
||||
})
|
||||
repoRef, err := parseRepositoryReference(fmt.Sprintf("%s/%s", registryName, imageName))
|
||||
if err != nil {
|
||||
repoLogger.Error("Error parsing repository name, skipping")
|
||||
logrus.Error(err)
|
||||
continue
|
||||
}
|
||||
|
||||
repoLogger.Info("Processing repo")
|
||||
|
||||
var sourceReferences []types.ImageReference
|
||||
if len(refs) != 0 {
|
||||
for _, ref := range refs {
|
||||
tagLogger := logrus.WithFields(logrus.Fields{"ref": ref})
|
||||
var named reference.Named
|
||||
// first try as digest
|
||||
if d, err := digest.Parse(ref); err == nil {
|
||||
named, err = reference.WithDigest(repoRef, d)
|
||||
if err != nil {
|
||||
tagLogger.Error("Error processing ref, skipping")
|
||||
logrus.Error(err)
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
tagLogger.Debugf("Ref was not a digest, trying as a tag: %s", err)
|
||||
named, err = reference.WithTag(repoRef, ref)
|
||||
if err != nil {
|
||||
tagLogger.Error("Error parsing ref, skipping")
|
||||
logrus.Error(err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
imageRef, err := docker.NewReference(named)
|
||||
if err != nil {
|
||||
tagLogger.Error("Error processing ref, skipping")
|
||||
logrus.Errorf("Error getting image reference: %s", err)
|
||||
continue
|
||||
}
|
||||
sourceReferences = append(sourceReferences, imageRef)
|
||||
}
|
||||
} else { // len(refs) == 0
|
||||
repoLogger.Info("Querying registry for image tags")
|
||||
sourceReferences, err = imagesToCopyFromRepo(serverCtx, repoRef)
|
||||
if err != nil {
|
||||
repoLogger.Error("Error processing repo, skipping")
|
||||
logrus.Error(err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if len(sourceReferences) == 0 {
|
||||
repoLogger.Warnf("No refs to sync found")
|
||||
continue
|
||||
}
|
||||
repoDescList = append(repoDescList, repoDescriptor{
|
||||
ImageRefs: sourceReferences,
|
||||
Context: serverCtx})
|
||||
}
|
||||
|
||||
// include repository descriptors for cfg.ImagesByTagRegex
|
||||
{
|
||||
filterCollection, err := tagRegexFilterCollection(cfg.ImagesByTagRegex)
|
||||
if err != nil {
|
||||
logrus.Error(err)
|
||||
} else {
|
||||
additionalRepoDescList := filterSourceReferences(serverCtx, registryName, filterCollection)
|
||||
repoDescList = append(repoDescList, additionalRepoDescList...)
|
||||
}
|
||||
}
|
||||
|
||||
// include repository descriptors for cfg.ImagesBySemver
|
||||
{
|
||||
filterCollection, err := semverFilterCollection(cfg.ImagesBySemver)
|
||||
if err != nil {
|
||||
logrus.Error(err)
|
||||
} else {
|
||||
additionalRepoDescList := filterSourceReferences(serverCtx, registryName, filterCollection)
|
||||
repoDescList = append(repoDescList, additionalRepoDescList...)
|
||||
}
|
||||
}
|
||||
|
||||
return repoDescList, nil
|
||||
}
|
||||
|
||||
// filterFunc is a function used to limit the initial set of image references
|
||||
// using tags, patterns, semver, etc.
|
||||
type filterFunc func(*logrus.Entry, types.ImageReference) bool
|
||||
|
||||
// filterCollection is a map of repository names to filter functions.
|
||||
type filterCollection map[string]filterFunc
|
||||
|
||||
// filterSourceReferences lists tags for images specified in the collection and
|
||||
// filters them using assigned filter functions.
|
||||
// It returns a list of repoDescriptors.
|
||||
func filterSourceReferences(sys *types.SystemContext, registryName string, collection filterCollection) []repoDescriptor {
|
||||
var repoDescList []repoDescriptor
|
||||
for repoName, filter := range collection {
|
||||
logger := logrus.WithFields(logrus.Fields{
|
||||
"repo": repoName,
|
||||
"registry": registryName,
|
||||
})
|
||||
|
||||
repoRef, err := parseRepositoryReference(fmt.Sprintf("%s/%s", registryName, repoName))
|
||||
if err != nil {
|
||||
logger.Error("Error parsing repository name, skipping")
|
||||
logrus.Error(err)
|
||||
continue
|
||||
}
|
||||
|
||||
logger.Info("Processing repo")
|
||||
|
||||
var sourceReferences []types.ImageReference
|
||||
|
||||
logger.Info("Querying registry for image tags")
|
||||
sourceReferences, err = imagesToCopyFromRepo(sys, repoRef)
|
||||
if err != nil {
|
||||
logger.Error("Error processing repo, skipping")
|
||||
logrus.Error(err)
|
||||
continue
|
||||
}
|
||||
|
||||
var filteredSourceReferences []types.ImageReference
|
||||
for _, ref := range sourceReferences {
|
||||
if filter(logger, ref) {
|
||||
filteredSourceReferences = append(filteredSourceReferences, ref)
|
||||
}
|
||||
}
|
||||
|
||||
if len(filteredSourceReferences) == 0 {
|
||||
logger.Warnf("No refs to sync found")
|
||||
continue
|
||||
}
|
||||
|
||||
repoDescList = append(repoDescList, repoDescriptor{
|
||||
ImageRefs: filteredSourceReferences,
|
||||
Context: sys,
|
||||
})
|
||||
}
|
||||
return repoDescList
|
||||
}
|
||||
|
||||
// tagRegexFilterCollection converts a map of (repository name, tag regex) pairs
|
||||
// into a filterCollection, which is a map of (repository name, filter function)
|
||||
// pairs.
|
||||
func tagRegexFilterCollection(collection map[string]string) (filterCollection, error) {
|
||||
filters := filterCollection{}
|
||||
|
||||
for repoName, tagRegex := range collection {
|
||||
pattern, err := regexp.Compile(tagRegex)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
f := func(logger *logrus.Entry, sourceReference types.ImageReference) bool {
|
||||
tagged, isTagged := sourceReference.DockerReference().(reference.Tagged)
|
||||
if !isTagged {
|
||||
logger.Errorf("Internal error, reference %s does not have a tag, skipping", sourceReference.DockerReference())
|
||||
return false
|
||||
}
|
||||
return pattern.MatchString(tagged.Tag())
|
||||
}
|
||||
filters[repoName] = f
|
||||
}
|
||||
|
||||
return filters, nil
|
||||
}
|
||||
|
||||
// semverFilterCollection converts a map of (repository name, array of semver constraints) pairs
|
||||
// into a filterCollection, which is a map of (repository name, filter function)
|
||||
// pairs.
|
||||
func semverFilterCollection(collection map[string]string) (filterCollection, error) {
|
||||
filters := filterCollection{}
|
||||
|
||||
for repoName, constraintString := range collection {
|
||||
constraint, err := semver.NewConstraint(constraintString)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
f := func(logger *logrus.Entry, sourceReference types.ImageReference) bool {
|
||||
tagged, isTagged := sourceReference.DockerReference().(reference.Tagged)
|
||||
if !isTagged {
|
||||
logger.Errorf("Internal error, reference %s does not have a tag, skipping", sourceReference.DockerReference())
|
||||
return false
|
||||
}
|
||||
tagVersion, err := semver.NewVersion(tagged.Tag())
|
||||
if err != nil {
|
||||
logger.Tracef("Tag %q cannot be parsed as semver, skipping", tagged.Tag())
|
||||
return false
|
||||
}
|
||||
return constraint.Check(tagVersion)
|
||||
}
|
||||
|
||||
filters[repoName] = f
|
||||
}
|
||||
|
||||
return filters, nil
|
||||
}
|
||||
|
||||
// imagesToCopy retrieves all the images to copy from a specified sync source
|
||||
// and transport.
|
||||
// It returns a slice of repository descriptors, where each descriptor is a
|
||||
// list of tagged image references to be used as sync source, and any error
|
||||
// encountered.
|
||||
func imagesToCopy(source string, transport string, sourceCtx *types.SystemContext) ([]repoDescriptor, error) {
|
||||
var descriptors []repoDescriptor
|
||||
|
||||
switch transport {
|
||||
case docker.Transport.Name():
|
||||
desc := repoDescriptor{
|
||||
Context: sourceCtx,
|
||||
}
|
||||
named, err := reference.ParseNormalizedNamed(source) // May be a repository or an image.
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Cannot obtain a valid image reference for transport %q and reference %q: %w", docker.Transport.Name(), source, err)
|
||||
}
|
||||
imageTagged := !reference.IsNameOnly(named)
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"imagename": source,
|
||||
"tagged": imageTagged,
|
||||
}).Info("Tag presence check")
|
||||
if imageTagged {
|
||||
srcRef, err := docker.NewReference(named)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Cannot obtain a valid image reference for transport %q and reference %q: %w", docker.Transport.Name(), named.String(), err)
|
||||
}
|
||||
desc.ImageRefs = []types.ImageReference{srcRef}
|
||||
} else {
|
||||
desc.ImageRefs, err = imagesToCopyFromRepo(sourceCtx, named)
|
||||
if err != nil {
|
||||
return descriptors, err
|
||||
}
|
||||
if len(desc.ImageRefs) == 0 {
|
||||
return descriptors, fmt.Errorf("No images to sync found in %q", source)
|
||||
}
|
||||
}
|
||||
descriptors = append(descriptors, desc)
|
||||
|
||||
case directory.Transport.Name():
|
||||
desc := repoDescriptor{
|
||||
Context: sourceCtx,
|
||||
}
|
||||
|
||||
if _, err := os.Stat(source); err != nil {
|
||||
return descriptors, fmt.Errorf("Invalid source directory specified: %w", err)
|
||||
}
|
||||
desc.DirBasePath = source
|
||||
var err error
|
||||
desc.ImageRefs, err = imagesToCopyFromDir(source)
|
||||
if err != nil {
|
||||
return descriptors, err
|
||||
}
|
||||
if len(desc.ImageRefs) == 0 {
|
||||
return descriptors, fmt.Errorf("No images to sync found in %q", source)
|
||||
}
|
||||
descriptors = append(descriptors, desc)
|
||||
|
||||
case "yaml":
|
||||
cfg, err := newSourceConfig(source)
|
||||
if err != nil {
|
||||
return descriptors, err
|
||||
}
|
||||
for registryName, registryConfig := range cfg {
|
||||
descs, err := imagesToCopyFromRegistry(registryName, registryConfig, *sourceCtx)
|
||||
if err != nil {
|
||||
return descriptors, fmt.Errorf("Failed to retrieve list of images from registry %q: %w", registryName, err)
|
||||
}
|
||||
descriptors = append(descriptors, descs...)
|
||||
}
|
||||
}
|
||||
|
||||
return descriptors, nil
|
||||
}
|
||||
|
||||
func (opts *syncOptions) run(args []string, stdout io.Writer) (retErr error) {
|
||||
if len(args) != 2 {
|
||||
return errorShouldDisplayUsage{errors.New("Exactly two arguments expected")}
|
||||
}
|
||||
opts.deprecatedTLSVerify.warnIfUsed([]string{"--src-tls-verify", "--dest-tls-verify"})
|
||||
|
||||
policyContext, err := opts.global.getPolicyContext()
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error loading trust policy: %w", err)
|
||||
}
|
||||
defer func() {
|
||||
if err := policyContext.Destroy(); err != nil {
|
||||
retErr = noteCloseFailure(retErr, "tearing down policy context", err)
|
||||
}
|
||||
}()
|
||||
|
||||
// validate source and destination options
|
||||
if len(opts.source) == 0 {
|
||||
return errors.New("A source transport must be specified")
|
||||
}
|
||||
if !slices.Contains([]string{docker.Transport.Name(), directory.Transport.Name(), "yaml"}, opts.source) {
|
||||
return fmt.Errorf("%q is not a valid source transport", opts.source)
|
||||
}
|
||||
|
||||
if len(opts.destination) == 0 {
|
||||
return errors.New("A destination transport must be specified")
|
||||
}
|
||||
if !slices.Contains([]string{docker.Transport.Name(), directory.Transport.Name()}, opts.destination) {
|
||||
return fmt.Errorf("%q is not a valid destination transport", opts.destination)
|
||||
}
|
||||
|
||||
if opts.source == opts.destination && opts.source == directory.Transport.Name() {
|
||||
return errors.New("sync from 'dir' to 'dir' not implemented, consider using rsync instead")
|
||||
}
|
||||
|
||||
opts.destImage.warnAboutIneffectiveOptions(transports.Get(opts.destination))
|
||||
|
||||
imageListSelection := copy.CopySystemImage
|
||||
if opts.all {
|
||||
imageListSelection = copy.CopyAllImages
|
||||
}
|
||||
|
||||
sourceCtx, err := opts.srcImage.newSystemContext()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ctx, cancel := opts.global.commandTimeoutContext()
|
||||
defer cancel()
|
||||
|
||||
sourceArg := args[0]
|
||||
var srcRepoList []repoDescriptor
|
||||
if err = retry.IfNecessary(ctx, func() error {
|
||||
srcRepoList, err = imagesToCopy(sourceArg, opts.source, sourceCtx)
|
||||
return err
|
||||
}, opts.retryOpts); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
destination := args[1]
|
||||
destinationCtx, err := opts.destImage.newSystemContext()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
options, cleanupOptions, err := opts.copy.copyOptions(stdout)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer cleanupOptions()
|
||||
options.DestinationCtx = destinationCtx
|
||||
options.ImageListSelection = imageListSelection
|
||||
options.OptimizeDestinationImageAlreadyExists = true
|
||||
|
||||
errorsPresent := false
|
||||
imagesNumber := 0
|
||||
if opts.dryRun {
|
||||
logrus.Warn("Running in dry-run mode")
|
||||
}
|
||||
|
||||
var digestFile *os.File
|
||||
if opts.digestFile != "" && !opts.dryRun {
|
||||
digestFile, err = os.OpenFile(opts.digestFile, os.O_TRUNC|os.O_CREATE|os.O_WRONLY, 0644)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error creating digest file: %w", err)
|
||||
}
|
||||
defer func() {
|
||||
if err := digestFile.Close(); err != nil {
|
||||
retErr = noteCloseFailure(retErr, "closing digest file", err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
for _, srcRepo := range srcRepoList {
|
||||
options.SourceCtx = srcRepo.Context
|
||||
for counter, ref := range srcRepo.ImageRefs {
|
||||
var destSuffix string
|
||||
var manifestBytes []byte
|
||||
switch ref.Transport() {
|
||||
case docker.Transport:
|
||||
// docker -> dir or docker -> docker
|
||||
destSuffix = ref.DockerReference().String()
|
||||
case directory.Transport:
|
||||
// dir -> docker (we don't allow `dir` -> `dir` sync operations)
|
||||
destSuffix = strings.TrimPrefix(ref.StringWithinTransport(), srcRepo.DirBasePath)
|
||||
if destSuffix == "" {
|
||||
// if source is a full path to an image, have destPath scoped to repo:tag
|
||||
destSuffix = path.Base(srcRepo.DirBasePath)
|
||||
}
|
||||
}
|
||||
|
||||
if !opts.scoped {
|
||||
destSuffix = path.Base(destSuffix)
|
||||
}
|
||||
|
||||
destRef, err := destinationReference(path.Join(destination, destSuffix)+opts.appendSuffix, opts.destination)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fromToFields := logrus.Fields{
|
||||
"from": transports.ImageName(ref),
|
||||
"to": transports.ImageName(destRef),
|
||||
}
|
||||
if opts.dryRun {
|
||||
logrus.WithFields(fromToFields).Infof("Would have copied image ref %d/%d", counter+1, len(srcRepo.ImageRefs))
|
||||
} else {
|
||||
logrus.WithFields(fromToFields).Infof("Copying image ref %d/%d", counter+1, len(srcRepo.ImageRefs))
|
||||
if err = retry.IfNecessary(ctx, func() error {
|
||||
manifestBytes, err = copy.Image(ctx, policyContext, destRef, ref, options)
|
||||
return err
|
||||
}, opts.retryOpts); err != nil {
|
||||
if !opts.keepGoing {
|
||||
return fmt.Errorf("Error copying ref %q: %w", transports.ImageName(ref), err)
|
||||
}
|
||||
// log the error, keep a note that there was a failure and move on to the next
|
||||
// image ref
|
||||
errorsPresent = true
|
||||
logrus.WithError(err).Errorf("Error copying ref %q", transports.ImageName(ref))
|
||||
continue
|
||||
}
|
||||
// Ensure that we log the manifest digest to a file only if the copy operation was successful
|
||||
if opts.digestFile != "" {
|
||||
manifestDigest, err := manifest.Digest(manifestBytes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
outputStr := fmt.Sprintf("%s %s", manifestDigest.String(), transports.ImageName(destRef))
|
||||
if _, err = digestFile.WriteString(outputStr + "\n"); err != nil {
|
||||
return fmt.Errorf("Failed to write digest to file %q: %w", opts.digestFile, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
imagesNumber++
|
||||
}
|
||||
}
|
||||
|
||||
if opts.dryRun {
|
||||
logrus.Infof("Would have synced %d images from %d sources", imagesNumber, len(srcRepoList))
|
||||
} else {
|
||||
logrus.Infof("Synced %d images from %d sources", imagesNumber, len(srcRepoList))
|
||||
}
|
||||
if !errorsPresent {
|
||||
return nil
|
||||
}
|
||||
return errors.New("Sync failed due to previous reported error(s) for one or more images")
|
||||
}
|
|
@ -0,0 +1,61 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.podman.io/image/v5/types"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
var _ yaml.Unmarshaler = (*tlsVerifyConfig)(nil)
|
||||
|
||||
func TestTLSVerifyConfig(t *testing.T) {
|
||||
type container struct { // An example of a larger config file
|
||||
TLSVerify tlsVerifyConfig `yaml:"tls-verify"`
|
||||
}
|
||||
|
||||
for _, c := range []struct {
|
||||
input string
|
||||
expected tlsVerifyConfig
|
||||
}{
|
||||
{
|
||||
input: `tls-verify: true`,
|
||||
expected: tlsVerifyConfig{skip: types.OptionalBoolFalse},
|
||||
},
|
||||
{
|
||||
input: `tls-verify: false`,
|
||||
expected: tlsVerifyConfig{skip: types.OptionalBoolTrue},
|
||||
},
|
||||
{
|
||||
input: ``, // No value
|
||||
expected: tlsVerifyConfig{skip: types.OptionalBoolUndefined},
|
||||
},
|
||||
} {
|
||||
config := container{}
|
||||
err := yaml.Unmarshal([]byte(c.input), &config)
|
||||
require.NoError(t, err, c.input)
|
||||
assert.Equal(t, c.expected, config.TLSVerify, c.input)
|
||||
}
|
||||
|
||||
// Invalid input
|
||||
config := container{}
|
||||
err := yaml.Unmarshal([]byte(`tls-verify: "not a valid bool"`), &config)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestSync(t *testing.T) {
|
||||
// Invalid command-line arguments
|
||||
for _, args := range [][]string{
|
||||
{},
|
||||
{"a1"},
|
||||
{"a1", "a2", "a3"},
|
||||
} {
|
||||
out, err := runSkopeo(append([]string{"sync"}, args...)...)
|
||||
assertTestFailed(t, out, err, "Exactly two arguments expected")
|
||||
}
|
||||
|
||||
// FIXME: Much more test coverage
|
||||
// Actual feature tests exist in integration and systemtest
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
//go:build !linux
|
||||
|
||||
package main
|
||||
|
||||
func reexecIfNecessaryForImages(_ ...string) error {
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"slices"
|
||||
|
||||
"github.com/moby/sys/capability"
|
||||
"go.podman.io/image/v5/transports/alltransports"
|
||||
"go.podman.io/storage/pkg/unshare"
|
||||
)
|
||||
|
||||
var neededCapabilities = []capability.Cap{
|
||||
capability.CAP_CHOWN,
|
||||
capability.CAP_DAC_OVERRIDE,
|
||||
capability.CAP_FOWNER,
|
||||
capability.CAP_FSETID,
|
||||
capability.CAP_MKNOD,
|
||||
capability.CAP_SETFCAP,
|
||||
capability.CAP_SYS_ADMIN,
|
||||
}
|
||||
|
||||
func maybeReexec() error {
|
||||
// With Skopeo we need only the subset of the root capabilities necessary
|
||||
// for pulling an image to the storage. Do not attempt to create a namespace
|
||||
// if we already have the capabilities we need.
|
||||
capabilities, err := capability.NewPid2(0)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error reading the current capabilities sets: %w", err)
|
||||
}
|
||||
if err := capabilities.Load(); err != nil {
|
||||
return fmt.Errorf("error loading the current capabilities sets: %w", err)
|
||||
}
|
||||
if slices.ContainsFunc(neededCapabilities, func(cap capability.Cap) bool {
|
||||
return !capabilities.Get(capability.EFFECTIVE, cap)
|
||||
}) {
|
||||
// We miss a capability we need, create a user namespaces
|
||||
unshare.MaybeReexecUsingUserNamespace(true)
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func reexecIfNecessaryForImages(imageNames ...string) error {
|
||||
// Check if container-storage is used before doing unshare
|
||||
if slices.ContainsFunc(imageNames, func(imageName string) bool {
|
||||
transport := alltransports.TransportFromImageName(imageName)
|
||||
// Hard-code the storage name to avoid a reference on c/image/storage.
|
||||
// See https://github.com/containers/skopeo/issues/771#issuecomment-563125006.
|
||||
return transport != nil && transport.Name() == "containers-storage"
|
||||
}) {
|
||||
return maybeReexec()
|
||||
}
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,547 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
dockerdistributionerrcode "github.com/docker/distribution/registry/api/errcode"
|
||||
dockerdistributionapi "github.com/docker/distribution/registry/api/v2"
|
||||
imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
commonFlag "go.podman.io/common/pkg/flag"
|
||||
"go.podman.io/common/pkg/retry"
|
||||
"go.podman.io/image/v5/copy"
|
||||
"go.podman.io/image/v5/directory"
|
||||
"go.podman.io/image/v5/manifest"
|
||||
ociarchive "go.podman.io/image/v5/oci/archive"
|
||||
ocilayout "go.podman.io/image/v5/oci/layout"
|
||||
"go.podman.io/image/v5/pkg/cli"
|
||||
"go.podman.io/image/v5/pkg/cli/sigstore"
|
||||
"go.podman.io/image/v5/pkg/compression"
|
||||
"go.podman.io/image/v5/signature/signer"
|
||||
"go.podman.io/image/v5/storage"
|
||||
"go.podman.io/image/v5/transports/alltransports"
|
||||
"go.podman.io/image/v5/types"
|
||||
"golang.org/x/term"
|
||||
)
|
||||
|
||||
// errorShouldDisplayUsage is a subtype of error used by command handlers to indicate that the command’s help should be included.
|
||||
type errorShouldDisplayUsage struct {
|
||||
error
|
||||
}
|
||||
|
||||
// noteCloseFailure returns (possibly-nil) err modified to account for (non-nil) closeErr.
|
||||
// The error for closeErr is annotated with description (which is not a format string)
|
||||
// Typical usage:
|
||||
//
|
||||
// defer func() {
|
||||
// if err := something.Close(); err != nil {
|
||||
// returnedErr = noteCloseFailure(returnedErr, "closing something", err)
|
||||
// }
|
||||
// }
|
||||
func noteCloseFailure(err error, description string, closeErr error) error {
|
||||
// We don’t accept a Closer() and close it ourselves because signature.PolicyContext has .Destroy(), not .Close().
|
||||
// This also makes it harder for a caller to do
|
||||
// defer noteCloseFailure(returnedErr, …)
|
||||
// which doesn’t use the right value of returnedErr, and doesn’t update it.
|
||||
if err == nil {
|
||||
return fmt.Errorf("%s: %w", description, closeErr)
|
||||
}
|
||||
// In this case we prioritize the primary error for use with %w; closeErr is usually less relevant, or might be a consequence of the primary error.
|
||||
return fmt.Errorf("%w (%s: %v)", err, description, closeErr)
|
||||
}
|
||||
|
||||
// commandAction intermediates between the RunE interface and the real handler,
|
||||
// primarily to ensure that cobra.Command is not available to the handler, which in turn
|
||||
// makes sure that the cmd.Flags() etc. flag access functions are not used,
|
||||
// and everything is done using the *Options structures and the *Var() methods of cmd.Flag().
|
||||
// handler may return errorShouldDisplayUsage to cause c.Help to be called.
|
||||
func commandAction(handler func(args []string, stdout io.Writer) error) func(cmd *cobra.Command, args []string) error {
|
||||
return func(c *cobra.Command, args []string) error {
|
||||
err := handler(args, c.OutOrStdout())
|
||||
var shouldDisplayUsage errorShouldDisplayUsage
|
||||
if errors.As(err, &shouldDisplayUsage) {
|
||||
c.SetOut(c.ErrOrStderr()) // This mutates c, but we are failing anyway.
|
||||
_ = c.Help() // Even if this failed, we prefer to report the original error
|
||||
}
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// deprecatedTLSVerifyOption represents a deprecated --tls-verify option,
|
||||
// which was accepted for all subcommands, for a time.
|
||||
// Every user should call deprecatedTLSVerifyOption.warnIfUsed() as part of handling the CLI,
|
||||
// whether or not the value actually ends up being used.
|
||||
// DO NOT ADD ANY NEW USES OF THIS; just call dockerImageFlags with an appropriate, possibly empty, flagPrefix.
|
||||
type deprecatedTLSVerifyOption struct {
|
||||
tlsVerify commonFlag.OptionalBool // FIXME FIXME: Warn if this is used, or even if it is ignored.
|
||||
}
|
||||
|
||||
// warnIfUsed warns if tlsVerify was set by the user, and suggests alternatives (which should
|
||||
// start with "--").
|
||||
// Every user should call this as part of handling the CLI, whether or not the value actually
|
||||
// ends up being used.
|
||||
func (opts *deprecatedTLSVerifyOption) warnIfUsed(alternatives []string) {
|
||||
if opts.tlsVerify.Present() {
|
||||
logrus.Warnf("'--tls-verify' is deprecated, instead use: %s", strings.Join(alternatives, ", "))
|
||||
}
|
||||
}
|
||||
|
||||
// deprecatedTLSVerifyFlags prepares the CLI flag writing into deprecatedTLSVerifyOption, and the managed deprecatedTLSVerifyOption structure.
|
||||
// DO NOT ADD ANY NEW USES OF THIS; just call dockerImageFlags with an appropriate, possibly empty, flagPrefix.
|
||||
func deprecatedTLSVerifyFlags() (pflag.FlagSet, *deprecatedTLSVerifyOption) {
|
||||
opts := deprecatedTLSVerifyOption{}
|
||||
fs := pflag.FlagSet{}
|
||||
flag := commonFlag.OptionalBoolFlag(&fs, &opts.tlsVerify, "tls-verify", "require HTTPS and verify certificates when accessing the container registry")
|
||||
flag.Hidden = true
|
||||
return fs, &opts
|
||||
}
|
||||
|
||||
// sharedImageOptions collects CLI flags which are image-related, but do not change across images.
|
||||
// This really should be a part of globalOptions, but that would break existing users of (skopeo copy --authfile=).
|
||||
type sharedImageOptions struct {
|
||||
authFilePath string // Path to a */containers/auth.json
|
||||
}
|
||||
|
||||
// sharedImageFlags prepares a collection of CLI flags writing into sharedImageOptions, and the managed sharedImageOptions structure.
|
||||
func sharedImageFlags() (pflag.FlagSet, *sharedImageOptions) {
|
||||
opts := sharedImageOptions{}
|
||||
fs := pflag.FlagSet{}
|
||||
fs.StringVar(&opts.authFilePath, "authfile", os.Getenv("REGISTRY_AUTH_FILE"), "path of the registry credentials file. Default is ${XDG_RUNTIME_DIR}/containers/auth.json")
|
||||
return fs, &opts
|
||||
}
|
||||
|
||||
// dockerImageOptions collects CLI flags specific to the "docker" transport, which are
|
||||
// the same across subcommands, but may be different for each image
|
||||
// (e.g. may differ between the source and destination of a copy)
|
||||
type dockerImageOptions struct {
|
||||
global *globalOptions // May be shared across several imageOptions instances.
|
||||
shared *sharedImageOptions // May be shared across several imageOptions instances.
|
||||
deprecatedTLSVerify *deprecatedTLSVerifyOption // May be shared across several imageOptions instances, or nil.
|
||||
authFilePath commonFlag.OptionalString // Path to a */containers/auth.json (prefixed version to override shared image option).
|
||||
credsOption commonFlag.OptionalString // username[:password] for accessing a registry
|
||||
userName commonFlag.OptionalString // username for accessing a registry
|
||||
password commonFlag.OptionalString // password for accessing a registry
|
||||
registryToken commonFlag.OptionalString // token to be used directly as a Bearer token when accessing the registry
|
||||
dockerCertPath string // A directory using Docker-like *.{crt,cert,key} files for connecting to a registry or a daemon
|
||||
tlsVerify commonFlag.OptionalBool // Require HTTPS and verify certificates (for docker: and docker-daemon:)
|
||||
noCreds bool // Access the registry anonymously
|
||||
}
|
||||
|
||||
// imageOptions collects CLI flags which are the same across subcommands, but may be different for each image
|
||||
// (e.g. may differ between the source and destination of a copy)
|
||||
type imageOptions struct {
|
||||
dockerImageOptions
|
||||
sharedBlobDir string // A directory to use for OCI blobs, shared across repositories
|
||||
dockerDaemonHost string // docker-daemon: host to connect to
|
||||
}
|
||||
|
||||
// dockerImageFlags prepares a collection of docker-transport specific CLI flags
|
||||
// writing into imageOptions, and the managed imageOptions structure.
|
||||
func dockerImageFlags(global *globalOptions, shared *sharedImageOptions, deprecatedTLSVerify *deprecatedTLSVerifyOption, flagPrefix, credsOptionAlias string) (pflag.FlagSet, *imageOptions) {
|
||||
flags := imageOptions{
|
||||
dockerImageOptions: dockerImageOptions{
|
||||
global: global,
|
||||
shared: shared,
|
||||
deprecatedTLSVerify: deprecatedTLSVerify,
|
||||
},
|
||||
}
|
||||
|
||||
fs := pflag.FlagSet{}
|
||||
if flagPrefix != "" {
|
||||
// the non-prefixed flag is handled by a shared flag.
|
||||
fs.Var(commonFlag.NewOptionalStringValue(&flags.authFilePath), flagPrefix+"authfile", "path of the registry credentials file. Default is ${XDG_RUNTIME_DIR}/containers/auth.json")
|
||||
}
|
||||
fs.Var(commonFlag.NewOptionalStringValue(&flags.credsOption), flagPrefix+"creds", "Use `USERNAME[:PASSWORD]` for accessing the registry")
|
||||
fs.Var(commonFlag.NewOptionalStringValue(&flags.userName), flagPrefix+"username", "Username for accessing the registry")
|
||||
fs.Var(commonFlag.NewOptionalStringValue(&flags.password), flagPrefix+"password", "Password for accessing the registry")
|
||||
if credsOptionAlias != "" {
|
||||
// This is horribly ugly, but we need to support the old option forms of (skopeo copy) for compatibility.
|
||||
// Don't add any more cases like this.
|
||||
f := fs.VarPF(commonFlag.NewOptionalStringValue(&flags.credsOption), credsOptionAlias, "", "Use `USERNAME[:PASSWORD]` for accessing the registry")
|
||||
f.Hidden = true
|
||||
}
|
||||
fs.Var(commonFlag.NewOptionalStringValue(&flags.registryToken), flagPrefix+"registry-token", "Provide a Bearer token for accessing the registry")
|
||||
fs.StringVar(&flags.dockerCertPath, flagPrefix+"cert-dir", "", "use certificates at `PATH` (*.crt, *.cert, *.key) to connect to the registry or daemon")
|
||||
commonFlag.OptionalBoolFlag(&fs, &flags.tlsVerify, flagPrefix+"tls-verify", "require HTTPS and verify certificates when talking to the container registry or daemon")
|
||||
fs.BoolVar(&flags.noCreds, flagPrefix+"no-creds", false, "Access the registry anonymously")
|
||||
return fs, &flags
|
||||
}
|
||||
|
||||
// imageFlags prepares a collection of CLI flags writing into imageOptions, and the managed imageOptions structure.
|
||||
func imageFlags(global *globalOptions, shared *sharedImageOptions, deprecatedTLSVerify *deprecatedTLSVerifyOption, flagPrefix, credsOptionAlias string) (pflag.FlagSet, *imageOptions) {
|
||||
dockerFlags, opts := dockerImageFlags(global, shared, deprecatedTLSVerify, flagPrefix, credsOptionAlias)
|
||||
|
||||
fs := pflag.FlagSet{}
|
||||
fs.AddFlagSet(&dockerFlags)
|
||||
fs.StringVar(&opts.sharedBlobDir, flagPrefix+"shared-blob-dir", "", "`DIRECTORY` to use to share blobs across OCI repositories")
|
||||
fs.StringVar(&opts.dockerDaemonHost, flagPrefix+"daemon-host", "", "use docker daemon host at `HOST` (docker-daemon: only)")
|
||||
return fs, opts
|
||||
}
|
||||
|
||||
func retryFlags() (pflag.FlagSet, *retry.Options) {
|
||||
opts := retry.Options{}
|
||||
fs := pflag.FlagSet{}
|
||||
fs.IntVar(&opts.MaxRetry, "retry-times", 0, "the number of times to possibly retry")
|
||||
fs.DurationVar(&opts.Delay, "retry-delay", 0*time.Second, "Fixed delay between retries. If not set, retry uses an exponential backoff delay.")
|
||||
return fs, &opts
|
||||
}
|
||||
|
||||
// newSystemContext returns a *types.SystemContext corresponding to opts.
|
||||
// It is guaranteed to return a fresh instance, so it is safe to make additional updates to it.
|
||||
func (opts *imageOptions) newSystemContext() (*types.SystemContext, error) {
|
||||
// *types.SystemContext instance from globalOptions
|
||||
// imageOptions option overrides the instance if both are present.
|
||||
ctx := opts.global.newSystemContext()
|
||||
ctx.DockerCertPath = opts.dockerCertPath
|
||||
ctx.OCISharedBlobDirPath = opts.sharedBlobDir
|
||||
ctx.AuthFilePath = opts.shared.authFilePath
|
||||
ctx.DockerDaemonHost = opts.dockerDaemonHost
|
||||
ctx.DockerDaemonCertPath = opts.dockerCertPath
|
||||
if opts.authFilePath.Present() {
|
||||
ctx.AuthFilePath = opts.authFilePath.Value()
|
||||
}
|
||||
if opts.deprecatedTLSVerify != nil && opts.deprecatedTLSVerify.tlsVerify.Present() {
|
||||
// If both this deprecated option and a non-deprecated option is present, we use the latter value.
|
||||
ctx.DockerInsecureSkipTLSVerify = types.NewOptionalBool(!opts.deprecatedTLSVerify.tlsVerify.Value())
|
||||
}
|
||||
if opts.tlsVerify.Present() {
|
||||
ctx.DockerDaemonInsecureSkipTLSVerify = !opts.tlsVerify.Value()
|
||||
}
|
||||
if opts.tlsVerify.Present() {
|
||||
ctx.DockerInsecureSkipTLSVerify = types.NewOptionalBool(!opts.tlsVerify.Value())
|
||||
}
|
||||
if opts.credsOption.Present() && opts.noCreds {
|
||||
return nil, errors.New("creds and no-creds cannot be specified at the same time")
|
||||
}
|
||||
if opts.userName.Present() && opts.noCreds {
|
||||
return nil, errors.New("username and no-creds cannot be specified at the same time")
|
||||
}
|
||||
if opts.credsOption.Present() && opts.userName.Present() {
|
||||
return nil, errors.New("creds and username cannot be specified at the same time")
|
||||
}
|
||||
// if any of username or password is present, then both are expected to be present
|
||||
if opts.userName.Present() != opts.password.Present() {
|
||||
if opts.userName.Present() {
|
||||
return nil, errors.New("password must be specified when username is specified")
|
||||
}
|
||||
return nil, errors.New("username must be specified when password is specified")
|
||||
}
|
||||
if opts.credsOption.Present() {
|
||||
var err error
|
||||
ctx.DockerAuthConfig, err = getDockerAuth(opts.credsOption.Value())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else if opts.userName.Present() {
|
||||
ctx.DockerAuthConfig = &types.DockerAuthConfig{
|
||||
Username: opts.userName.Value(),
|
||||
Password: opts.password.Value(),
|
||||
}
|
||||
}
|
||||
if opts.registryToken.Present() {
|
||||
ctx.DockerBearerRegistryToken = opts.registryToken.Value()
|
||||
}
|
||||
if opts.noCreds {
|
||||
ctx.DockerAuthConfig = &types.DockerAuthConfig{}
|
||||
}
|
||||
|
||||
return ctx, nil
|
||||
}
|
||||
|
||||
// imageDestOptions is a superset of imageOptions specialized for image destinations.
|
||||
// Every user should call imageDestOptions.warnAboutIneffectiveOptions() as part of handling the CLI
|
||||
type imageDestOptions struct {
|
||||
*imageOptions
|
||||
dirForceCompression bool // Compress layers when saving to the dir: transport
|
||||
dirForceDecompression bool // Decompress layers when saving to the dir: transport
|
||||
ociAcceptUncompressedLayers bool // Whether to accept uncompressed layers in the oci: transport
|
||||
compressionFormat string // Format to use for the compression
|
||||
compressionLevel commonFlag.OptionalInt // Level to use for the compression
|
||||
precomputeDigests bool // Precompute digests to dedup layers when saving to the docker: transport
|
||||
imageDestFlagPrefix string
|
||||
}
|
||||
|
||||
// imageDestFlags prepares a collection of CLI flags writing into imageDestOptions, and the managed imageDestOptions structure.
|
||||
func imageDestFlags(global *globalOptions, shared *sharedImageOptions, deprecatedTLSVerify *deprecatedTLSVerifyOption, flagPrefix, credsOptionAlias string) (pflag.FlagSet, *imageDestOptions) {
|
||||
genericFlags, genericOptions := imageFlags(global, shared, deprecatedTLSVerify, flagPrefix, credsOptionAlias)
|
||||
opts := imageDestOptions{imageOptions: genericOptions, imageDestFlagPrefix: flagPrefix}
|
||||
fs := pflag.FlagSet{}
|
||||
fs.AddFlagSet(&genericFlags)
|
||||
fs.BoolVar(&opts.dirForceCompression, flagPrefix+"compress", false, "Compress tarball image layers when saving to directory using the 'dir' transport. (default is same compression type as source)")
|
||||
fs.BoolVar(&opts.dirForceDecompression, flagPrefix+"decompress", false, "Decompress tarball image layers when saving to directory using the 'dir' transport. (default is same compression type as source)")
|
||||
fs.BoolVar(&opts.ociAcceptUncompressedLayers, flagPrefix+"oci-accept-uncompressed-layers", false, "Allow uncompressed image layers when saving to an OCI image using the 'oci' transport. (default is to compress things that aren't compressed)")
|
||||
fs.StringVar(&opts.compressionFormat, flagPrefix+"compress-format", "", "`FORMAT` to use for the compression")
|
||||
fs.Var(commonFlag.NewOptionalIntValue(&opts.compressionLevel), flagPrefix+"compress-level", "`LEVEL` to use for the compression")
|
||||
fs.BoolVar(&opts.precomputeDigests, flagPrefix+"precompute-digests", false, "Precompute digests to prevent uploading layers already on the registry using the 'docker' transport.")
|
||||
return fs, &opts
|
||||
}
|
||||
|
||||
// newSystemContext returns a *types.SystemContext corresponding to opts.
|
||||
// It is guaranteed to return a fresh instance, so it is safe to make additional updates to it.
|
||||
func (opts *imageDestOptions) newSystemContext() (*types.SystemContext, error) {
|
||||
ctx, err := opts.imageOptions.newSystemContext()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ctx.DirForceCompress = opts.dirForceCompression
|
||||
ctx.DirForceDecompress = opts.dirForceDecompression
|
||||
ctx.OCIAcceptUncompressedLayers = opts.ociAcceptUncompressedLayers
|
||||
if opts.compressionFormat != "" {
|
||||
cf, err := compression.AlgorithmByName(opts.compressionFormat)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ctx.CompressionFormat = &cf
|
||||
}
|
||||
if opts.compressionLevel.Present() {
|
||||
value := opts.compressionLevel.Value()
|
||||
ctx.CompressionLevel = &value
|
||||
}
|
||||
ctx.DockerRegistryPushPrecomputeDigests = opts.precomputeDigests
|
||||
return ctx, err
|
||||
}
|
||||
|
||||
// warnAboutIneffectiveOptions warns if any ineffective option was set by the user
|
||||
// Every user should call this as part of handling the CLI
|
||||
func (opts *imageDestOptions) warnAboutIneffectiveOptions(destTransport types.ImageTransport) {
|
||||
if destTransport.Name() != directory.Transport.Name() {
|
||||
if opts.dirForceCompression {
|
||||
logrus.Warnf("--%s can only be used if the destination transport is 'dir'", opts.imageDestFlagPrefix+"compress")
|
||||
}
|
||||
if opts.dirForceDecompression {
|
||||
logrus.Warnf("--%s can only be used if the destination transport is 'dir'", opts.imageDestFlagPrefix+"decompress")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// sharedCopyOptions collects CLI flags that affect copying images, currently shared between the copy and sync commands.
|
||||
type sharedCopyOptions struct {
|
||||
removeSignatures bool // Do not copy signatures from the source image
|
||||
signByFingerprint string // Sign the image using a GPG key with the specified fingerprint
|
||||
signBySigstoreParamFile string // Sign the image using a sigstore signature per configuration in a param file
|
||||
signBySigstorePrivateKey string // Sign the image using a sigstore private key
|
||||
signPassphraseFile string // Path pointing to a passphrase file when signing
|
||||
preserveDigests bool // Preserve digests during copy
|
||||
format commonFlag.OptionalString // Force conversion of the image to a specified format
|
||||
}
|
||||
|
||||
// sharedCopyFlags prepares a collection of CLI flags writing into sharedCopyoptions.
|
||||
func sharedCopyFlags() (pflag.FlagSet, *sharedCopyOptions) {
|
||||
opts := sharedCopyOptions{}
|
||||
fs := pflag.FlagSet{}
|
||||
fs.BoolVar(&opts.removeSignatures, "remove-signatures", false, "Do not copy signatures from source")
|
||||
fs.StringVar(&opts.signByFingerprint, "sign-by", "", "Sign the image using a GPG key with the specified `FINGERPRINT`")
|
||||
fs.StringVar(&opts.signBySigstoreParamFile, "sign-by-sigstore", "", "Sign the image using a sigstore parameter file at `PATH`")
|
||||
fs.StringVar(&opts.signBySigstorePrivateKey, "sign-by-sigstore-private-key", "", "Sign the image using a sigstore private key at `PATH`")
|
||||
fs.StringVar(&opts.signPassphraseFile, "sign-passphrase-file", "", "Read a passphrase for signing an image from `PATH`")
|
||||
fs.VarP(commonFlag.NewOptionalStringValue(&opts.format), "format", "f", `MANIFEST TYPE (oci, v2s1, or v2s2) to use in the destination (default is manifest type of source, with fallbacks)`)
|
||||
fs.BoolVar(&opts.preserveDigests, "preserve-digests", false, "Preserve digests of images and lists")
|
||||
return fs, &opts
|
||||
}
|
||||
|
||||
// copyOptions interprets opts, returns a partially-filled *copy.Options,
|
||||
// and a function that should be called to clean up.
|
||||
func (opts *sharedCopyOptions) copyOptions(stdout io.Writer) (*copy.Options, func(), error) {
|
||||
var manifestType string
|
||||
if opts.format.Present() {
|
||||
mt, err := parseManifestFormat(opts.format.Value())
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
manifestType = mt
|
||||
}
|
||||
|
||||
// c/image/copy.Image does allow creating both simple signing and sigstore signatures simultaneously,
|
||||
// with independent passphrases, but that would make the CLI probably too confusing.
|
||||
// For now, use the passphrase with either, but only one of them.
|
||||
if opts.signPassphraseFile != "" && opts.signByFingerprint != "" && opts.signBySigstorePrivateKey != "" {
|
||||
return nil, nil, fmt.Errorf("Only one of --sign-by and sign-by-sigstore-private-key can be used with sign-passphrase-file")
|
||||
}
|
||||
var passphrase string
|
||||
if opts.signPassphraseFile != "" {
|
||||
p, err := cli.ReadPassphraseFile(opts.signPassphraseFile)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
passphrase = p
|
||||
} else if opts.signBySigstorePrivateKey != "" {
|
||||
p, err := promptForPassphrase(opts.signBySigstorePrivateKey, os.Stdin, os.Stdout)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
passphrase = p
|
||||
} // opts.signByFingerprint triggers a GPG-agent passphrase prompt, possibly using a more secure channel, so we usually shouldn’t prompt ourselves if no passphrase was explicitly provided.
|
||||
var passphraseBytes []byte
|
||||
if passphrase != "" {
|
||||
passphraseBytes = []byte(passphrase)
|
||||
}
|
||||
|
||||
var signers []*signer.Signer
|
||||
closeSigners := func() {
|
||||
for _, signer := range signers {
|
||||
signer.Close()
|
||||
}
|
||||
}
|
||||
succeeded := false
|
||||
defer func() {
|
||||
if !succeeded {
|
||||
closeSigners()
|
||||
}
|
||||
}()
|
||||
if opts.signBySigstoreParamFile != "" {
|
||||
signer, err := sigstore.NewSignerFromParameterFile(opts.signBySigstoreParamFile, &sigstore.Options{
|
||||
PrivateKeyPassphrasePrompt: func(keyFile string) (string, error) {
|
||||
return promptForPassphrase(keyFile, os.Stdin, os.Stdout)
|
||||
},
|
||||
Stdin: os.Stdin,
|
||||
Stdout: stdout,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("Error using --sign-by-sigstore: %w", err)
|
||||
}
|
||||
signers = append(signers, signer)
|
||||
}
|
||||
|
||||
succeeded = true
|
||||
return ©.Options{
|
||||
RemoveSignatures: opts.removeSignatures,
|
||||
Signers: signers,
|
||||
SignBy: opts.signByFingerprint,
|
||||
SignPassphrase: passphrase,
|
||||
SignBySigstorePrivateKeyFile: opts.signBySigstorePrivateKey,
|
||||
SignSigstorePrivateKeyPassphrase: passphraseBytes,
|
||||
|
||||
ReportWriter: stdout,
|
||||
|
||||
PreserveDigests: opts.preserveDigests,
|
||||
ForceManifestMIMEType: manifestType,
|
||||
}, closeSigners, nil
|
||||
}
|
||||
|
||||
func parseCreds(creds string) (string, string, error) {
|
||||
if creds == "" {
|
||||
return "", "", errors.New("credentials can't be empty")
|
||||
}
|
||||
username, password, _ := strings.Cut(creds, ":") // Sets password to "" if there is no ":"
|
||||
if username == "" {
|
||||
return "", "", errors.New("username can't be empty")
|
||||
}
|
||||
return username, password, nil
|
||||
}
|
||||
|
||||
func getDockerAuth(creds string) (*types.DockerAuthConfig, error) {
|
||||
username, password, err := parseCreds(creds)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &types.DockerAuthConfig{
|
||||
Username: username,
|
||||
Password: password,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// parseImageSource converts image URL-like string to an ImageSource.
|
||||
// The caller must call .Close() on the returned ImageSource.
|
||||
func parseImageSource(ctx context.Context, opts *imageOptions, name string) (types.ImageSource, error) {
|
||||
ref, err := alltransports.ParseImageName(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sys, err := opts.newSystemContext()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ref.NewImageSource(ctx, sys)
|
||||
}
|
||||
|
||||
// parseManifestFormat parses format parameter for copy and sync command.
|
||||
// It returns string value to use as manifest MIME type
|
||||
func parseManifestFormat(manifestFormat string) (string, error) {
|
||||
switch manifestFormat {
|
||||
case "oci":
|
||||
return imgspecv1.MediaTypeImageManifest, nil
|
||||
case "v2s1":
|
||||
return manifest.DockerV2Schema1SignedMediaType, nil
|
||||
case "v2s2":
|
||||
return manifest.DockerV2Schema2MediaType, nil
|
||||
default:
|
||||
return "", fmt.Errorf("unknown format %q. Choose one of the supported formats: 'oci', 'v2s1', or 'v2s2'", manifestFormat)
|
||||
}
|
||||
}
|
||||
|
||||
// usageTemplate returns the usage template for skopeo commands
|
||||
// This blocks the displaying of the global options. The main skopeo
|
||||
// command should not use this.
|
||||
const usageTemplate = `Usage:{{if .Runnable}}
|
||||
{{.UseLine}}{{end}}{{if .HasAvailableSubCommands}}
|
||||
|
||||
{{.CommandPath}} [command]{{end}}{{if gt (len .Aliases) 0}}
|
||||
|
||||
Aliases:
|
||||
{{.NameAndAliases}}{{end}}{{if .HasExample}}
|
||||
|
||||
Examples:
|
||||
{{.Example}}{{end}}{{if .HasAvailableSubCommands}}
|
||||
|
||||
Available Commands:{{range .Commands}}{{if (or .IsAvailableCommand (eq .Name "help"))}}
|
||||
{{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{end}}{{if .HasAvailableLocalFlags}}
|
||||
|
||||
Flags:
|
||||
{{.LocalFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}{{if .HasAvailableInheritedFlags}}
|
||||
{{end}}
|
||||
`
|
||||
|
||||
// adjustUsage uses usageTemplate template to get rid the GlobalOption from usage
|
||||
// and disable [flag] at the end of command usage
|
||||
func adjustUsage(c *cobra.Command) {
|
||||
c.SetUsageTemplate(usageTemplate)
|
||||
c.DisableFlagsInUseLine = true
|
||||
}
|
||||
|
||||
// promptForPassphrase interactively prompts for a passphrase related to privateKeyFile
|
||||
func promptForPassphrase(privateKeyFile string, stdin, stdout *os.File) (string, error) {
|
||||
stdinFd := int(stdin.Fd())
|
||||
if !term.IsTerminal(stdinFd) {
|
||||
return "", fmt.Errorf("Cannot prompt for a passphrase for key %s, standard input is not a TTY", privateKeyFile)
|
||||
}
|
||||
|
||||
fmt.Fprintf(stdout, "Passphrase for key %s: ", privateKeyFile)
|
||||
passphrase, err := term.ReadPassword(stdinFd)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("Error reading password: %w", err)
|
||||
}
|
||||
fmt.Fprintf(stdout, "\n")
|
||||
return string(passphrase), nil
|
||||
}
|
||||
|
||||
// isNotFoundImageError heuristically attempts to determine whether an error
|
||||
// is saying the remote source couldn't find the image (as opposed to an
|
||||
// authentication error, an I/O error etc.)
|
||||
// TODO drive this into containers/image properly
|
||||
func isNotFoundImageError(err error) bool {
|
||||
var layoutImageNotFoundError ocilayout.ImageNotFoundError
|
||||
var archiveImageNotFoundError ociarchive.ImageNotFoundError
|
||||
return isDockerManifestUnknownError(err) ||
|
||||
errors.Is(err, storage.ErrNoSuchImage) ||
|
||||
errors.As(err, &layoutImageNotFoundError) ||
|
||||
errors.As(err, &archiveImageNotFoundError)
|
||||
}
|
||||
|
||||
// isDockerManifestUnknownError is a copy of code from containers/image,
|
||||
// please update there first.
|
||||
func isDockerManifestUnknownError(err error) bool {
|
||||
var ec dockerdistributionerrcode.ErrorCoder
|
||||
if !errors.As(err, &ec) {
|
||||
return false
|
||||
}
|
||||
return ec.ErrorCode() == dockerdistributionapi.ErrorCodeManifestUnknown
|
||||
}
|
|
@ -0,0 +1,516 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.podman.io/image/v5/copy"
|
||||
"go.podman.io/image/v5/manifest"
|
||||
"go.podman.io/image/v5/types"
|
||||
)
|
||||
|
||||
func TestNoteCloseFailure(t *testing.T) {
|
||||
const description = "description"
|
||||
|
||||
mainErr := errors.New("main")
|
||||
closeErr := errors.New("closing")
|
||||
|
||||
// Main success, closing failed
|
||||
res := noteCloseFailure(nil, description, closeErr)
|
||||
require.NotNil(t, res)
|
||||
assert.Contains(t, res.Error(), description)
|
||||
assert.Contains(t, res.Error(), closeErr.Error())
|
||||
|
||||
// Both main and closing failed
|
||||
res = noteCloseFailure(mainErr, description, closeErr)
|
||||
require.NotNil(t, res)
|
||||
assert.Contains(t, res.Error(), mainErr.Error())
|
||||
assert.Contains(t, res.Error(), description)
|
||||
assert.Contains(t, res.Error(), closeErr.Error())
|
||||
assert.ErrorIs(t, res, mainErr)
|
||||
}
|
||||
|
||||
// fakeGlobalOptions creates globalOptions and sets it according to flags.
|
||||
func fakeGlobalOptions(t *testing.T, flags []string) (*globalOptions, *cobra.Command) {
|
||||
app, opts := createApp()
|
||||
cmd := &cobra.Command{}
|
||||
app.AddCommand(cmd)
|
||||
err := app.ParseFlags(flags)
|
||||
require.NoError(t, err)
|
||||
return opts, cmd
|
||||
}
|
||||
|
||||
// fakeImageOptions creates imageOptions and sets it according to globalFlags/cmdFlags.
|
||||
func fakeImageOptions(t *testing.T, flagPrefix string, useDeprecatedTLSVerify bool,
|
||||
globalFlags []string, cmdFlags []string) *imageOptions {
|
||||
globalOpts, cmd := fakeGlobalOptions(t, globalFlags)
|
||||
sharedFlags, sharedOpts := sharedImageFlags()
|
||||
var deprecatedTLSVerifyFlag pflag.FlagSet
|
||||
var deprecatedTLSVerifyOpt *deprecatedTLSVerifyOption
|
||||
if useDeprecatedTLSVerify {
|
||||
deprecatedTLSVerifyFlag, deprecatedTLSVerifyOpt = deprecatedTLSVerifyFlags()
|
||||
}
|
||||
imageFlags, imageOpts := imageFlags(globalOpts, sharedOpts, deprecatedTLSVerifyOpt, flagPrefix, "")
|
||||
cmd.Flags().AddFlagSet(&sharedFlags)
|
||||
if useDeprecatedTLSVerify {
|
||||
cmd.Flags().AddFlagSet(&deprecatedTLSVerifyFlag)
|
||||
}
|
||||
cmd.Flags().AddFlagSet(&imageFlags)
|
||||
err := cmd.ParseFlags(cmdFlags)
|
||||
require.NoError(t, err)
|
||||
return imageOpts
|
||||
}
|
||||
|
||||
func TestImageOptionsNewSystemContext(t *testing.T) {
|
||||
// Default state
|
||||
opts := fakeImageOptions(t, "dest-", true, []string{}, []string{})
|
||||
res, err := opts.newSystemContext()
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, &types.SystemContext{
|
||||
DockerRegistryUserAgent: defaultUserAgent,
|
||||
}, res)
|
||||
|
||||
// Set everything to non-default values.
|
||||
opts = fakeImageOptions(t, "dest-", true, []string{
|
||||
"--registries.d", "/srv/registries.d",
|
||||
"--override-arch", "overridden-arch",
|
||||
"--override-os", "overridden-os",
|
||||
"--override-variant", "overridden-variant",
|
||||
"--tmpdir", "/srv",
|
||||
}, []string{
|
||||
"--authfile", "/srv/authfile",
|
||||
"--dest-authfile", "/srv/dest-authfile",
|
||||
"--dest-cert-dir", "/srv/cert-dir",
|
||||
"--dest-shared-blob-dir", "/srv/shared-blob-dir",
|
||||
"--dest-daemon-host", "daemon-host.example.com",
|
||||
"--dest-tls-verify=false",
|
||||
"--dest-creds", "creds-user:creds-password",
|
||||
"--dest-registry-token", "faketoken",
|
||||
})
|
||||
res, err = opts.newSystemContext()
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, &types.SystemContext{
|
||||
RegistriesDirPath: "/srv/registries.d",
|
||||
AuthFilePath: "/srv/dest-authfile",
|
||||
ArchitectureChoice: "overridden-arch",
|
||||
OSChoice: "overridden-os",
|
||||
VariantChoice: "overridden-variant",
|
||||
OCISharedBlobDirPath: "/srv/shared-blob-dir",
|
||||
DockerCertPath: "/srv/cert-dir",
|
||||
DockerInsecureSkipTLSVerify: types.OptionalBoolTrue,
|
||||
DockerAuthConfig: &types.DockerAuthConfig{Username: "creds-user", Password: "creds-password"},
|
||||
DockerBearerRegistryToken: "faketoken",
|
||||
DockerDaemonCertPath: "/srv/cert-dir",
|
||||
DockerDaemonHost: "daemon-host.example.com",
|
||||
DockerDaemonInsecureSkipTLSVerify: true,
|
||||
DockerRegistryUserAgent: defaultUserAgent,
|
||||
BigFilesTemporaryDir: "/srv",
|
||||
}, res)
|
||||
|
||||
// Global/per-command tlsVerify behavior is tested in TestTLSVerifyFlags.
|
||||
|
||||
// Invalid option values
|
||||
opts = fakeImageOptions(t, "dest-", true, []string{}, []string{"--dest-creds", ""})
|
||||
_, err = opts.newSystemContext()
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
// fakeImageDestOptions creates imageDestOptions and sets it according to globalFlags/cmdFlags.
|
||||
func fakeImageDestOptions(t *testing.T, flagPrefix string, useDeprecatedTLSVerify bool,
|
||||
globalFlags []string, cmdFlags []string) *imageDestOptions {
|
||||
globalOpts, cmd := fakeGlobalOptions(t, globalFlags)
|
||||
sharedFlags, sharedOpts := sharedImageFlags()
|
||||
var deprecatedTLSVerifyFlag pflag.FlagSet
|
||||
var deprecatedTLSVerifyOpt *deprecatedTLSVerifyOption
|
||||
if useDeprecatedTLSVerify {
|
||||
deprecatedTLSVerifyFlag, deprecatedTLSVerifyOpt = deprecatedTLSVerifyFlags()
|
||||
}
|
||||
imageFlags, imageOpts := imageDestFlags(globalOpts, sharedOpts, deprecatedTLSVerifyOpt, flagPrefix, "")
|
||||
cmd.Flags().AddFlagSet(&sharedFlags)
|
||||
if useDeprecatedTLSVerify {
|
||||
cmd.Flags().AddFlagSet(&deprecatedTLSVerifyFlag)
|
||||
}
|
||||
cmd.Flags().AddFlagSet(&imageFlags)
|
||||
err := cmd.ParseFlags(cmdFlags)
|
||||
require.NoError(t, err)
|
||||
return imageOpts
|
||||
}
|
||||
|
||||
func TestImageDestOptionsNewSystemContext(t *testing.T) {
|
||||
// Default state
|
||||
opts := fakeImageDestOptions(t, "dest-", true, []string{}, []string{})
|
||||
res, err := opts.newSystemContext()
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, &types.SystemContext{
|
||||
DockerRegistryUserAgent: defaultUserAgent,
|
||||
}, res)
|
||||
|
||||
authFile := "/tmp/auth.json"
|
||||
// Make sure when REGISTRY_AUTH_FILE is set the auth file is used
|
||||
t.Setenv("REGISTRY_AUTH_FILE", authFile)
|
||||
|
||||
// Explicitly set everything to default, except for when the default is “not present”
|
||||
opts = fakeImageDestOptions(t, "dest-", true, []string{}, []string{
|
||||
"--dest-compress=false",
|
||||
})
|
||||
res, err = opts.newSystemContext()
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, &types.SystemContext{
|
||||
AuthFilePath: authFile,
|
||||
DockerRegistryUserAgent: defaultUserAgent,
|
||||
}, res)
|
||||
|
||||
// Set everything to non-default values.
|
||||
opts = fakeImageDestOptions(t, "dest-", true, []string{
|
||||
"--registries.d", "/srv/registries.d",
|
||||
"--override-arch", "overridden-arch",
|
||||
"--override-os", "overridden-os",
|
||||
"--override-variant", "overridden-variant",
|
||||
"--tmpdir", "/srv",
|
||||
}, []string{
|
||||
"--authfile", "/srv/authfile",
|
||||
"--dest-cert-dir", "/srv/cert-dir",
|
||||
"--dest-shared-blob-dir", "/srv/shared-blob-dir",
|
||||
"--dest-compress=true",
|
||||
"--dest-daemon-host", "daemon-host.example.com",
|
||||
"--dest-tls-verify=false",
|
||||
"--dest-creds", "creds-user:creds-password",
|
||||
"--dest-registry-token", "faketoken",
|
||||
"--dest-precompute-digests=true",
|
||||
})
|
||||
res, err = opts.newSystemContext()
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, &types.SystemContext{
|
||||
RegistriesDirPath: "/srv/registries.d",
|
||||
AuthFilePath: "/srv/authfile",
|
||||
ArchitectureChoice: "overridden-arch",
|
||||
OSChoice: "overridden-os",
|
||||
VariantChoice: "overridden-variant",
|
||||
OCISharedBlobDirPath: "/srv/shared-blob-dir",
|
||||
DockerCertPath: "/srv/cert-dir",
|
||||
DockerInsecureSkipTLSVerify: types.OptionalBoolTrue,
|
||||
DockerAuthConfig: &types.DockerAuthConfig{Username: "creds-user", Password: "creds-password"},
|
||||
DockerBearerRegistryToken: "faketoken",
|
||||
DockerDaemonCertPath: "/srv/cert-dir",
|
||||
DockerDaemonHost: "daemon-host.example.com",
|
||||
DockerDaemonInsecureSkipTLSVerify: true,
|
||||
DockerRegistryUserAgent: defaultUserAgent,
|
||||
DirForceCompress: true,
|
||||
BigFilesTemporaryDir: "/srv",
|
||||
DockerRegistryPushPrecomputeDigests: true,
|
||||
}, res)
|
||||
|
||||
// Global/per-command tlsVerify behavior is tested in TestTLSVerifyFlags.
|
||||
|
||||
// Invalid option values in imageOptions
|
||||
opts = fakeImageDestOptions(t, "dest-", true, []string{}, []string{"--dest-creds", ""})
|
||||
_, err = opts.newSystemContext()
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
// TestImageOptionsUsernamePassword verifies that using the username and password
|
||||
// options works as expected
|
||||
func TestImageOptionsUsernamePassword(t *testing.T) {
|
||||
for _, command := range []struct {
|
||||
commandArgs []string
|
||||
expectedAuthConfig *types.DockerAuthConfig // data to expect, or nil if an error is expected
|
||||
}{
|
||||
// Set only username/password (without --creds), expected to pass
|
||||
{
|
||||
commandArgs: []string{"--dest-username", "foo", "--dest-password", "bar"},
|
||||
expectedAuthConfig: &types.DockerAuthConfig{Username: "foo", Password: "bar"},
|
||||
},
|
||||
// no username but set password, expect error
|
||||
{
|
||||
commandArgs: []string{"--dest-password", "foo"},
|
||||
expectedAuthConfig: nil,
|
||||
},
|
||||
// set username but no password. expected to fail (we currently don't allow a user without password)
|
||||
{
|
||||
commandArgs: []string{"--dest-username", "bar"},
|
||||
expectedAuthConfig: nil,
|
||||
},
|
||||
// set username with --creds, expected to fail
|
||||
{
|
||||
commandArgs: []string{"--dest-username", "bar", "--dest-creds", "hello:world", "--dest-password", "foo"},
|
||||
expectedAuthConfig: nil,
|
||||
},
|
||||
// set username with --no-creds, expected to fail
|
||||
{
|
||||
commandArgs: []string{"--dest-username", "bar", "--dest-no-creds", "--dest-password", "foo"},
|
||||
expectedAuthConfig: nil,
|
||||
},
|
||||
} {
|
||||
opts := fakeImageDestOptions(t, "dest-", true, []string{}, command.commandArgs)
|
||||
// parse the command options
|
||||
res, err := opts.newSystemContext()
|
||||
if command.expectedAuthConfig == nil {
|
||||
assert.Error(t, err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, &types.SystemContext{
|
||||
DockerRegistryUserAgent: defaultUserAgent,
|
||||
DockerAuthConfig: command.expectedAuthConfig,
|
||||
}, res)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestTLSVerifyFlags(t *testing.T) {
|
||||
type systemContextOpts interface { // Either *imageOptions or *imageDestOptions
|
||||
newSystemContext() (*types.SystemContext, error)
|
||||
}
|
||||
|
||||
for _, creator := range []struct {
|
||||
name string
|
||||
newOpts func(useDeprecatedTLSVerify bool, globalFlags, cmdFlags []string) systemContextOpts
|
||||
}{
|
||||
{
|
||||
"imageFlags",
|
||||
func(useDeprecatedTLSVerify bool, globalFlags, cmdFlags []string) systemContextOpts {
|
||||
return fakeImageOptions(t, "dest-", useDeprecatedTLSVerify, globalFlags, cmdFlags)
|
||||
},
|
||||
},
|
||||
{
|
||||
"imageDestFlags",
|
||||
func(useDeprecatedTLSVerify bool, globalFlags, cmdFlags []string) systemContextOpts {
|
||||
return fakeImageDestOptions(t, "dest-", useDeprecatedTLSVerify, globalFlags, cmdFlags)
|
||||
},
|
||||
},
|
||||
} {
|
||||
t.Run(creator.name, func(t *testing.T) {
|
||||
for _, c := range []struct {
|
||||
global, deprecatedCmd, cmd string
|
||||
expectedDocker types.OptionalBool
|
||||
expectedDockerDaemon bool
|
||||
}{
|
||||
{"", "", "", types.OptionalBoolUndefined, false},
|
||||
{"", "", "false", types.OptionalBoolTrue, true},
|
||||
{"", "", "true", types.OptionalBoolFalse, false},
|
||||
{"", "false", "", types.OptionalBoolTrue, false},
|
||||
{"", "false", "false", types.OptionalBoolTrue, true},
|
||||
{"", "false", "true", types.OptionalBoolFalse, false},
|
||||
{"", "true", "", types.OptionalBoolFalse, false},
|
||||
{"", "true", "false", types.OptionalBoolTrue, true},
|
||||
{"", "true", "true", types.OptionalBoolFalse, false},
|
||||
{"false", "", "", types.OptionalBoolTrue, false},
|
||||
{"false", "", "false", types.OptionalBoolTrue, true},
|
||||
{"false", "", "true", types.OptionalBoolFalse, false},
|
||||
{"false", "false", "", types.OptionalBoolTrue, false},
|
||||
{"false", "false", "false", types.OptionalBoolTrue, true},
|
||||
{"false", "false", "true", types.OptionalBoolFalse, false},
|
||||
{"false", "true", "", types.OptionalBoolFalse, false},
|
||||
{"false", "true", "false", types.OptionalBoolTrue, true},
|
||||
{"false", "true", "true", types.OptionalBoolFalse, false},
|
||||
{"true", "", "", types.OptionalBoolFalse, false},
|
||||
{"true", "", "false", types.OptionalBoolTrue, true},
|
||||
{"true", "", "true", types.OptionalBoolFalse, false},
|
||||
{"true", "false", "", types.OptionalBoolTrue, false},
|
||||
{"true", "false", "false", types.OptionalBoolTrue, true},
|
||||
{"true", "false", "true", types.OptionalBoolFalse, false},
|
||||
{"true", "true", "", types.OptionalBoolFalse, false},
|
||||
{"true", "true", "false", types.OptionalBoolTrue, true},
|
||||
{"true", "true", "true", types.OptionalBoolFalse, false},
|
||||
} {
|
||||
globalFlags := []string{}
|
||||
if c.global != "" {
|
||||
globalFlags = append(globalFlags, "--tls-verify="+c.global)
|
||||
}
|
||||
cmdFlags := []string{}
|
||||
if c.deprecatedCmd != "" {
|
||||
cmdFlags = append(cmdFlags, "--tls-verify="+c.deprecatedCmd)
|
||||
}
|
||||
if c.cmd != "" {
|
||||
cmdFlags = append(cmdFlags, "--dest-tls-verify="+c.cmd)
|
||||
}
|
||||
opts := creator.newOpts(true, globalFlags, cmdFlags)
|
||||
res, err := opts.newSystemContext()
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, c.expectedDocker, res.DockerInsecureSkipTLSVerify, "%#v", c)
|
||||
assert.Equal(t, c.expectedDockerDaemon, res.DockerDaemonInsecureSkipTLSVerify, "%#v", c)
|
||||
|
||||
if c.deprecatedCmd == "" { // Test also the behavior when deprecatedTLSFlag is not recognized
|
||||
// Use globalFlags from the previous test
|
||||
cmdFlags := []string{}
|
||||
if c.cmd != "" {
|
||||
cmdFlags = append(cmdFlags, "--dest-tls-verify="+c.cmd)
|
||||
}
|
||||
opts := creator.newOpts(false, globalFlags, cmdFlags)
|
||||
res, err = opts.newSystemContext()
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, c.expectedDocker, res.DockerInsecureSkipTLSVerify, "%#v", c)
|
||||
assert.Equal(t, c.expectedDockerDaemon, res.DockerDaemonInsecureSkipTLSVerify, "%#v", c)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// fakeSharedCopyOptions creates sharedCopyOptions and sets it according to cmdFlags.
|
||||
func fakeSharedCopyOptions(t *testing.T, cmdFlags []string) *sharedCopyOptions {
|
||||
_, cmd := fakeGlobalOptions(t, []string{})
|
||||
sharedCopyFlags, sharedCopyOpts := sharedCopyFlags()
|
||||
cmd.Flags().AddFlagSet(&sharedCopyFlags)
|
||||
err := cmd.ParseFlags(cmdFlags)
|
||||
require.NoError(t, err)
|
||||
return sharedCopyOpts
|
||||
}
|
||||
|
||||
func TestSharedCopyOptionsCopyOptions(t *testing.T) {
|
||||
someStdout := bytes.Buffer{}
|
||||
|
||||
// Default state
|
||||
opts := fakeSharedCopyOptions(t, []string{})
|
||||
res, cleanup, err := opts.copyOptions(&someStdout)
|
||||
require.NoError(t, err)
|
||||
defer cleanup()
|
||||
assert.Equal(t, ©.Options{
|
||||
ReportWriter: &someStdout,
|
||||
}, res)
|
||||
|
||||
// Set most flags to non-default values
|
||||
// This should also test --sign-by-sigstore and --sign-by-sigstore-private-key; we would have
|
||||
// to create test keys for that.
|
||||
opts = fakeSharedCopyOptions(t, []string{
|
||||
"--remove-signatures",
|
||||
"--sign-by", "gpgFingerprint",
|
||||
"--format", "oci",
|
||||
"--preserve-digests",
|
||||
})
|
||||
res, cleanup, err = opts.copyOptions(&someStdout)
|
||||
require.NoError(t, err)
|
||||
defer cleanup()
|
||||
assert.Equal(t, ©.Options{
|
||||
RemoveSignatures: true,
|
||||
SignBy: "gpgFingerprint",
|
||||
ReportWriter: &someStdout,
|
||||
PreserveDigests: true,
|
||||
ForceManifestMIMEType: imgspecv1.MediaTypeImageManifest,
|
||||
}, res)
|
||||
|
||||
// --sign-passphrase-file + --sign-by work
|
||||
passphraseFile, err := os.CreateTemp("", "passphrase") // Eventually we could refer to a passphrase fixture instead
|
||||
require.NoError(t, err)
|
||||
defer os.Remove(passphraseFile.Name())
|
||||
_, err = passphraseFile.WriteString("test-passphrase")
|
||||
require.NoError(t, err)
|
||||
opts = fakeSharedCopyOptions(t, []string{
|
||||
"--sign-by", "gpgFingerprint",
|
||||
"--sign-passphrase-file", passphraseFile.Name(),
|
||||
})
|
||||
res, cleanup, err = opts.copyOptions(&someStdout)
|
||||
require.NoError(t, err)
|
||||
defer cleanup()
|
||||
assert.Equal(t, ©.Options{
|
||||
SignBy: "gpgFingerprint",
|
||||
SignPassphrase: "test-passphrase",
|
||||
SignSigstorePrivateKeyPassphrase: []byte("test-passphrase"),
|
||||
ReportWriter: &someStdout,
|
||||
}, res)
|
||||
// --sign-passphrase-file + --sign-by-sigstore-private-key should be tested here.
|
||||
|
||||
// Invalid --format
|
||||
opts = fakeSharedCopyOptions(t, []string{"--format", "invalid"})
|
||||
_, _, err = opts.copyOptions(&someStdout)
|
||||
assert.Error(t, err)
|
||||
|
||||
// More --sign-passphrase-file, --sign-by-sigstore-private-key, --sign-by-sigstore failure cases should be tested here.
|
||||
|
||||
// --sign-passphrase-file not found
|
||||
opts = fakeSharedCopyOptions(t, []string{
|
||||
"--sign-by", "gpgFingerprint",
|
||||
"--sign-passphrase-file", "/dev/null/this/does/not/exist",
|
||||
})
|
||||
_, _, err = opts.copyOptions(&someStdout)
|
||||
assert.Error(t, err)
|
||||
|
||||
// --sign-by-sigstore file not found
|
||||
opts = fakeSharedCopyOptions(t, []string{
|
||||
"--sign-by-sigstore", "/dev/null/this/does/not/exist",
|
||||
})
|
||||
_, _, err = opts.copyOptions(&someStdout)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestParseManifestFormat(t *testing.T) {
|
||||
for _, testCase := range []struct {
|
||||
formatParam string
|
||||
expectedManifestType string
|
||||
expectErr bool
|
||||
}{
|
||||
{"oci",
|
||||
imgspecv1.MediaTypeImageManifest,
|
||||
false},
|
||||
{"v2s1",
|
||||
manifest.DockerV2Schema1SignedMediaType,
|
||||
false},
|
||||
{"v2s2",
|
||||
manifest.DockerV2Schema2MediaType,
|
||||
false},
|
||||
{"",
|
||||
"",
|
||||
true},
|
||||
{"badValue",
|
||||
"",
|
||||
true},
|
||||
} {
|
||||
manifestType, err := parseManifestFormat(testCase.formatParam)
|
||||
if testCase.expectErr {
|
||||
require.Error(t, err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
assert.Equal(t, manifestType, testCase.expectedManifestType)
|
||||
}
|
||||
}
|
||||
|
||||
// since there is a shared authfile image option and a non-shared (prefixed) one, make sure the override logic
|
||||
// works correctly.
|
||||
func TestImageOptionsAuthfileOverride(t *testing.T) {
|
||||
for _, testCase := range []struct {
|
||||
flagPrefix string
|
||||
cmdFlags []string
|
||||
expectedAuthfilePath string
|
||||
}{
|
||||
// if there is no prefix, only authfile is allowed.
|
||||
{"",
|
||||
[]string{
|
||||
"--authfile", "/srv/authfile",
|
||||
}, "/srv/authfile"},
|
||||
// if authfile and dest-authfile is provided, dest-authfile wins
|
||||
{"dest-",
|
||||
[]string{
|
||||
"--authfile", "/srv/authfile",
|
||||
"--dest-authfile", "/srv/dest-authfile",
|
||||
}, "/srv/dest-authfile",
|
||||
},
|
||||
// if only the shared authfile is provided, authfile must be present in system context
|
||||
{"dest-",
|
||||
[]string{
|
||||
"--authfile", "/srv/authfile",
|
||||
}, "/srv/authfile",
|
||||
},
|
||||
// if only the dest authfile is provided, dest-authfile must be present in system context
|
||||
{"dest-",
|
||||
[]string{
|
||||
"--dest-authfile", "/srv/dest-authfile",
|
||||
}, "/srv/dest-authfile",
|
||||
},
|
||||
} {
|
||||
opts := fakeImageOptions(t, testCase.flagPrefix, false, []string{}, testCase.cmdFlags)
|
||||
res, err := opts.newSystemContext()
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, &types.SystemContext{
|
||||
AuthFilePath: testCase.expectedAuthfilePath,
|
||||
DockerRegistryUserAgent: defaultUserAgent,
|
||||
}, res)
|
||||
}
|
||||
}
|
325
commit.go
325
commit.go
|
@ -1,325 +0,0 @@
|
|||
package buildah
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"time"
|
||||
|
||||
"github.com/Sirupsen/logrus"
|
||||
cp "github.com/containers/image/copy"
|
||||
"github.com/containers/image/signature"
|
||||
is "github.com/containers/image/storage"
|
||||
"github.com/containers/image/transports"
|
||||
"github.com/containers/image/types"
|
||||
"github.com/containers/storage"
|
||||
"github.com/containers/storage/pkg/archive"
|
||||
"github.com/containers/storage/pkg/stringid"
|
||||
"github.com/opencontainers/go-digest"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/projectatomic/buildah/util"
|
||||
)
|
||||
|
||||
var (
|
||||
// gzippedEmptyLayer is a gzip-compressed version of an empty tar file (just 1024 zero bytes). This
|
||||
// comes from github.com/docker/distribution/manifest/schema1/config_builder.go by way of
|
||||
// github.com/containers/image/image/docker_schema2.go; there is a non-zero embedded timestamp; we could
|
||||
// zero that, but that would just waste storage space in registries, so let’s use the same values.
|
||||
gzippedEmptyLayer = []byte{
|
||||
31, 139, 8, 0, 0, 9, 110, 136, 0, 255, 98, 24, 5, 163, 96, 20, 140, 88,
|
||||
0, 8, 0, 0, 255, 255, 46, 175, 181, 239, 0, 4, 0, 0,
|
||||
}
|
||||
)
|
||||
|
||||
// CommitOptions can be used to alter how an image is committed.
|
||||
type CommitOptions struct {
|
||||
// PreferredManifestType is the preferred type of image manifest. The
|
||||
// image configuration format will be of a compatible type.
|
||||
PreferredManifestType string
|
||||
// Compression specifies the type of compression which is applied to
|
||||
// layer blobs. The default is to not use compression, but
|
||||
// archive.Gzip is recommended.
|
||||
Compression archive.Compression
|
||||
// SignaturePolicyPath specifies an override location for the signature
|
||||
// policy which should be used for verifying the new image as it is
|
||||
// being written. Except in specific circumstances, no value should be
|
||||
// specified, indicating that the shared, system-wide default policy
|
||||
// should be used.
|
||||
SignaturePolicyPath string
|
||||
// AdditionalTags is a list of additional names to add to the image, if
|
||||
// the transport to which we're writing the image gives us a way to add
|
||||
// them.
|
||||
AdditionalTags []string
|
||||
// ReportWriter is an io.Writer which will be used to log the writing
|
||||
// of the new image.
|
||||
ReportWriter io.Writer
|
||||
// HistoryTimestamp is the timestamp used when creating new items in the
|
||||
// image's history. If unset, the current time will be used.
|
||||
HistoryTimestamp *time.Time
|
||||
}
|
||||
|
||||
// PushOptions can be used to alter how an image is copied somewhere.
|
||||
type PushOptions struct {
|
||||
// Compression specifies the type of compression which is applied to
|
||||
// layer blobs. The default is to not use compression, but
|
||||
// archive.Gzip is recommended.
|
||||
Compression archive.Compression
|
||||
// SignaturePolicyPath specifies an override location for the signature
|
||||
// policy which should be used for verifying the new image as it is
|
||||
// being written. Except in specific circumstances, no value should be
|
||||
// specified, indicating that the shared, system-wide default policy
|
||||
// should be used.
|
||||
SignaturePolicyPath string
|
||||
// ReportWriter is an io.Writer which will be used to log the writing
|
||||
// of the new image.
|
||||
ReportWriter io.Writer
|
||||
// Store is the local storage store which holds the source image.
|
||||
Store storage.Store
|
||||
}
|
||||
|
||||
// shallowCopy copies the most recent layer, the configuration, and the manifest from one image to another.
|
||||
// For local storage, which doesn't care about histories and the manifest's contents, that's sufficient, but
|
||||
// almost any other destination has higher expectations.
|
||||
// We assume that "dest" is a reference to a local image (specifically, a containers/image/storage.storageReference),
|
||||
// and will fail if it isn't.
|
||||
func (b *Builder) shallowCopy(dest types.ImageReference, src types.ImageReference, systemContext *types.SystemContext) error {
|
||||
// Read the target image name.
|
||||
if dest.DockerReference() == nil {
|
||||
return errors.New("can't write to an unnamed image")
|
||||
}
|
||||
names, err := util.ExpandTags([]string{dest.DockerReference().String()})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Make a temporary image reference.
|
||||
tmpName := stringid.GenerateRandomID() + "-tmp-" + Package + "-commit"
|
||||
tmpRef, err := is.Transport.ParseStoreReference(b.store, tmpName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
if err2 := tmpRef.DeleteImage(systemContext); err2 != nil {
|
||||
logrus.Debugf("error deleting temporary image %q: %v", tmpName, err2)
|
||||
}
|
||||
}()
|
||||
// Open the source for reading and a temporary image for writing.
|
||||
srcImage, err := src.NewImage(systemContext)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "error reading configuration to write to image %q", transports.ImageName(dest))
|
||||
}
|
||||
defer srcImage.Close()
|
||||
tmpImage, err := tmpRef.NewImageDestination(systemContext)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "error opening temporary copy of image %q for writing", transports.ImageName(dest))
|
||||
}
|
||||
defer tmpImage.Close()
|
||||
// Write an empty filesystem layer, because the image layer requires at least one.
|
||||
_, err = tmpImage.PutBlob(bytes.NewReader(gzippedEmptyLayer), types.BlobInfo{Size: int64(len(gzippedEmptyLayer))})
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "error writing dummy layer for image %q", transports.ImageName(dest))
|
||||
}
|
||||
// Read the newly-generated configuration blob.
|
||||
config, err := srcImage.ConfigBlob()
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "error reading new configuration for image %q", transports.ImageName(dest))
|
||||
}
|
||||
if len(config) == 0 {
|
||||
return errors.Errorf("error reading new configuration for image %q: it's empty", transports.ImageName(dest))
|
||||
}
|
||||
logrus.Debugf("read configuration blob %q", string(config))
|
||||
// Write the configuration to the temporary image.
|
||||
configBlobInfo := types.BlobInfo{
|
||||
Digest: digest.Canonical.FromBytes(config),
|
||||
Size: int64(len(config)),
|
||||
}
|
||||
_, err = tmpImage.PutBlob(bytes.NewReader(config), configBlobInfo)
|
||||
if err != nil && len(config) > 0 {
|
||||
return errors.Wrapf(err, "error writing image configuration for temporary copy of %q", transports.ImageName(dest))
|
||||
}
|
||||
// Read the newly-generated, mostly fake, manifest.
|
||||
manifest, _, err := srcImage.Manifest()
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "error reading new manifest for image %q", transports.ImageName(dest))
|
||||
}
|
||||
// Write the manifest to the temporary image.
|
||||
err = tmpImage.PutManifest(manifest)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "error writing new manifest to temporary copy of image %q", transports.ImageName(dest))
|
||||
}
|
||||
// Save the temporary image.
|
||||
err = tmpImage.Commit()
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "error committing new image %q", transports.ImageName(dest))
|
||||
}
|
||||
// Locate the temporary image in the lower-level API. Read its item names.
|
||||
tmpImg, err := is.Transport.GetStoreImage(b.store, tmpRef)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "error locating temporary image %q", transports.ImageName(dest))
|
||||
}
|
||||
items, err := b.store.ListImageBigData(tmpImg.ID)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "error reading list of named data for image %q", tmpImg.ID)
|
||||
}
|
||||
// Look up the container's read-write layer.
|
||||
container, err := b.store.Container(b.ContainerID)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "error reading information about working container %q", b.ContainerID)
|
||||
}
|
||||
parentLayer := ""
|
||||
// Look up the container's source image's layer, if there is a source image.
|
||||
if container.ImageID != "" {
|
||||
img, err2 := b.store.Image(container.ImageID)
|
||||
if err2 != nil {
|
||||
return errors.Wrapf(err2, "error reading information about working container %q's source image", b.ContainerID)
|
||||
}
|
||||
parentLayer = img.TopLayer
|
||||
}
|
||||
// Extract the read-write layer's contents.
|
||||
layerDiff, err := b.store.Diff(parentLayer, container.LayerID)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "error reading layer from source image %q", transports.ImageName(src))
|
||||
}
|
||||
defer layerDiff.Close()
|
||||
// Write a copy of the layer for the new image to reference.
|
||||
layer, _, err := b.store.PutLayer("", parentLayer, []string{}, "", false, layerDiff)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "error creating new read-only layer from container %q", b.ContainerID)
|
||||
}
|
||||
// Create a low-level image record that uses the new layer.
|
||||
image, err := b.store.CreateImage("", []string{}, layer.ID, "", nil)
|
||||
if err != nil {
|
||||
err2 := b.store.DeleteLayer(layer.ID)
|
||||
if err2 != nil {
|
||||
logrus.Debugf("error removing layer %q: %v", layer, err2)
|
||||
}
|
||||
return errors.Wrapf(err, "error creating new low-level image %q", transports.ImageName(dest))
|
||||
}
|
||||
logrus.Debugf("created image ID %q", image.ID)
|
||||
defer func() {
|
||||
if err != nil {
|
||||
_, err2 := b.store.DeleteImage(image.ID, true)
|
||||
if err2 != nil {
|
||||
logrus.Debugf("error removing image %q: %v", image.ID, err2)
|
||||
}
|
||||
}
|
||||
}()
|
||||
// Copy the configuration and manifest, which are big data items, along with whatever else is there.
|
||||
for _, item := range items {
|
||||
var data []byte
|
||||
data, err = b.store.ImageBigData(tmpImg.ID, item)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "error copying data item %q", item)
|
||||
}
|
||||
err = b.store.SetImageBigData(image.ID, item, data)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "error copying data item %q", item)
|
||||
}
|
||||
logrus.Debugf("copied data item %q to %q", item, image.ID)
|
||||
}
|
||||
// Set low-level metadata in the new image so that the image library will accept it as a real image.
|
||||
err = b.store.SetMetadata(image.ID, "{}")
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "error assigning metadata to new image %q", transports.ImageName(dest))
|
||||
}
|
||||
// Move the target name(s) from the temporary image to the new image.
|
||||
err = util.AddImageNames(b.store, image, names)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "error assigning names %v to new image", names)
|
||||
}
|
||||
logrus.Debugf("assigned names %v to image %q", names, image.ID)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Commit writes the contents of the container, along with its updated
|
||||
// configuration, to a new image in the specified location, and if we know how,
|
||||
// add any additional tags that were specified.
|
||||
func (b *Builder) Commit(dest types.ImageReference, options CommitOptions) error {
|
||||
policy, err := signature.DefaultPolicy(getSystemContext(options.SignaturePolicyPath))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
policyContext, err := signature.NewPolicyContext(policy)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Check if we're keeping everything in local storage. If so, we can take certain shortcuts.
|
||||
_, destIsStorage := dest.Transport().(is.StoreTransport)
|
||||
exporting := !destIsStorage
|
||||
src, err := b.makeContainerImageRef(options.PreferredManifestType, exporting, options.Compression, options.HistoryTimestamp)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "error computing layer digests and building metadata")
|
||||
}
|
||||
if exporting {
|
||||
// Copy everything.
|
||||
err = cp.Image(policyContext, dest, src, getCopyOptions(options.ReportWriter))
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "error copying layers and metadata")
|
||||
}
|
||||
} else {
|
||||
// Copy only the most recent layer, the configuration, and the manifest.
|
||||
err = b.shallowCopy(dest, src, getSystemContext(options.SignaturePolicyPath))
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "error copying layer and metadata")
|
||||
}
|
||||
}
|
||||
if len(options.AdditionalTags) > 0 {
|
||||
switch dest.Transport().Name() {
|
||||
case is.Transport.Name():
|
||||
img, err := is.Transport.GetStoreImage(b.store, dest)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "error locating just-written image %q", transports.ImageName(dest))
|
||||
}
|
||||
err = util.AddImageNames(b.store, img, options.AdditionalTags)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "error setting image names to %v", append(img.Names, options.AdditionalTags...))
|
||||
}
|
||||
logrus.Debugf("assigned names %v to image %q", img.Names, img.ID)
|
||||
default:
|
||||
logrus.Warnf("don't know how to add tags to images stored in %q transport", dest.Transport().Name())
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Push copies the contents of the image to a new location.
|
||||
func Push(image string, dest types.ImageReference, options PushOptions) error {
|
||||
systemContext := getSystemContext(options.SignaturePolicyPath)
|
||||
policy, err := signature.DefaultPolicy(systemContext)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
policyContext, err := signature.NewPolicyContext(policy)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
importOptions := ImportFromImageOptions{
|
||||
Image: image,
|
||||
SignaturePolicyPath: options.SignaturePolicyPath,
|
||||
}
|
||||
builder, err := importBuilderFromImage(options.Store, importOptions)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error importing builder information from image")
|
||||
}
|
||||
// Look up the image name and its layer.
|
||||
ref, err := is.Transport.ParseStoreReference(options.Store, image)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "error parsing reference to image %q", image)
|
||||
}
|
||||
img, err := is.Transport.GetStoreImage(options.Store, ref)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "error locating image %q", image)
|
||||
}
|
||||
// Give the image we're producing the same ancestors as its source image.
|
||||
builder.FromImage = builder.Docker.ContainerConfig.Image
|
||||
builder.FromImageID = string(builder.Docker.Parent)
|
||||
// Prep the layers and manifest for export.
|
||||
src, err := builder.makeImageImageRef(options.Compression, img.Names, img.TopLayer, nil)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "error recomputing layer digests and building metadata")
|
||||
}
|
||||
// Copy everything.
|
||||
err = cp.Image(policyContext, dest, src, getCopyOptions(options.ReportWriter))
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "error copying layers and metadata")
|
||||
}
|
||||
return nil
|
||||
}
|
22
common.go
22
common.go
|
@ -1,22 +0,0 @@
|
|||
package buildah
|
||||
|
||||
import (
|
||||
"io"
|
||||
|
||||
cp "github.com/containers/image/copy"
|
||||
"github.com/containers/image/types"
|
||||
)
|
||||
|
||||
func getCopyOptions(reportWriter io.Writer) *cp.Options {
|
||||
return &cp.Options{
|
||||
ReportWriter: reportWriter,
|
||||
}
|
||||
}
|
||||
|
||||
func getSystemContext(signaturePolicyPath string) *types.SystemContext {
|
||||
sc := &types.SystemContext{}
|
||||
if signaturePolicyPath != "" {
|
||||
sc.SignaturePolicyPath = signaturePolicyPath
|
||||
}
|
||||
return sc
|
||||
}
|
561
config.go
561
config.go
|
@ -1,561 +0,0 @@
|
|||
package buildah
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
digest "github.com/opencontainers/go-digest"
|
||||
ociv1 "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/projectatomic/buildah/docker"
|
||||
)
|
||||
|
||||
// makeOCIv1Image builds the best OCIv1 image structure we can from the
|
||||
// contents of the docker image structure.
|
||||
func makeOCIv1Image(dimage *docker.V2Image) (ociv1.Image, error) {
|
||||
config := dimage.Config
|
||||
if config == nil {
|
||||
config = &dimage.ContainerConfig
|
||||
}
|
||||
image := ociv1.Image{
|
||||
Created: dimage.Created.UTC(),
|
||||
Author: dimage.Author,
|
||||
Architecture: dimage.Architecture,
|
||||
OS: dimage.OS,
|
||||
Config: ociv1.ImageConfig{
|
||||
User: config.User,
|
||||
ExposedPorts: map[string]struct{}{},
|
||||
Env: config.Env,
|
||||
Entrypoint: config.Entrypoint,
|
||||
Cmd: config.Cmd,
|
||||
Volumes: config.Volumes,
|
||||
WorkingDir: config.WorkingDir,
|
||||
Labels: config.Labels,
|
||||
},
|
||||
RootFS: ociv1.RootFS{
|
||||
Type: "",
|
||||
DiffIDs: []string{},
|
||||
},
|
||||
History: []ociv1.History{},
|
||||
}
|
||||
for port, what := range config.ExposedPorts {
|
||||
image.Config.ExposedPorts[string(port)] = what
|
||||
}
|
||||
RootFS := docker.V2S2RootFS{}
|
||||
if dimage.RootFS != nil {
|
||||
RootFS = *dimage.RootFS
|
||||
}
|
||||
if RootFS.Type == docker.TypeLayers {
|
||||
image.RootFS.Type = docker.TypeLayers
|
||||
for _, id := range RootFS.DiffIDs {
|
||||
image.RootFS.DiffIDs = append(image.RootFS.DiffIDs, id.String())
|
||||
}
|
||||
}
|
||||
for _, history := range dimage.History {
|
||||
ohistory := ociv1.History{
|
||||
Created: history.Created.UTC(),
|
||||
CreatedBy: history.CreatedBy,
|
||||
Author: history.Author,
|
||||
Comment: history.Comment,
|
||||
EmptyLayer: history.EmptyLayer,
|
||||
}
|
||||
image.History = append(image.History, ohistory)
|
||||
}
|
||||
return image, nil
|
||||
}
|
||||
|
||||
// makeDockerV2S2Image builds the best docker image structure we can from the
|
||||
// contents of the OCI image structure.
|
||||
func makeDockerV2S2Image(oimage *ociv1.Image) (docker.V2Image, error) {
|
||||
image := docker.V2Image{
|
||||
V1Image: docker.V1Image{Created: oimage.Created.UTC(),
|
||||
Author: oimage.Author,
|
||||
Architecture: oimage.Architecture,
|
||||
OS: oimage.OS,
|
||||
ContainerConfig: docker.Config{
|
||||
User: oimage.Config.User,
|
||||
ExposedPorts: docker.PortSet{},
|
||||
Env: oimage.Config.Env,
|
||||
Entrypoint: oimage.Config.Entrypoint,
|
||||
Cmd: oimage.Config.Cmd,
|
||||
Volumes: oimage.Config.Volumes,
|
||||
WorkingDir: oimage.Config.WorkingDir,
|
||||
Labels: oimage.Config.Labels,
|
||||
},
|
||||
},
|
||||
RootFS: &docker.V2S2RootFS{
|
||||
Type: "",
|
||||
DiffIDs: []digest.Digest{},
|
||||
},
|
||||
History: []docker.V2S2History{},
|
||||
}
|
||||
for port, what := range oimage.Config.ExposedPorts {
|
||||
image.ContainerConfig.ExposedPorts[docker.Port(port)] = what
|
||||
}
|
||||
if oimage.RootFS.Type == docker.TypeLayers {
|
||||
image.RootFS.Type = docker.TypeLayers
|
||||
for _, id := range oimage.RootFS.DiffIDs {
|
||||
d, err := digest.Parse(id)
|
||||
if err != nil {
|
||||
return docker.V2Image{}, err
|
||||
}
|
||||
image.RootFS.DiffIDs = append(image.RootFS.DiffIDs, d)
|
||||
}
|
||||
}
|
||||
for _, history := range oimage.History {
|
||||
dhistory := docker.V2S2History{
|
||||
Created: history.Created.UTC(),
|
||||
CreatedBy: history.CreatedBy,
|
||||
Author: history.Author,
|
||||
Comment: history.Comment,
|
||||
EmptyLayer: history.EmptyLayer,
|
||||
}
|
||||
image.History = append(image.History, dhistory)
|
||||
}
|
||||
image.Config = &image.ContainerConfig
|
||||
return image, nil
|
||||
}
|
||||
|
||||
// makeDockerV2S1Image builds the best docker image structure we can from the
|
||||
// contents of the V2S1 image structure.
|
||||
func makeDockerV2S1Image(manifest docker.V2S1Manifest) (docker.V2Image, error) {
|
||||
// Treat the most recent (first) item in the history as a description of the image.
|
||||
if len(manifest.History) == 0 {
|
||||
return docker.V2Image{}, errors.Errorf("error parsing image configuration from manifest")
|
||||
}
|
||||
dimage := docker.V2Image{}
|
||||
err := json.Unmarshal([]byte(manifest.History[0].V1Compatibility), &dimage)
|
||||
if err != nil {
|
||||
return docker.V2Image{}, err
|
||||
}
|
||||
if dimage.DockerVersion == "" {
|
||||
return docker.V2Image{}, errors.Errorf("error parsing image configuration from history")
|
||||
}
|
||||
// The DiffID list is intended to contain the sums of _uncompressed_ blobs, and these are most
|
||||
// likely compressed, so leave the list empty to avoid potential confusion later on. We can
|
||||
// construct a list with the correct values when we prep layers for pushing, so we don't lose.
|
||||
// information by leaving this part undone.
|
||||
rootFS := &docker.V2S2RootFS{
|
||||
Type: docker.TypeLayers,
|
||||
DiffIDs: []digest.Digest{},
|
||||
}
|
||||
// Build a filesystem history.
|
||||
history := []docker.V2S2History{}
|
||||
for i := range manifest.History {
|
||||
h := docker.V2S2History{
|
||||
Created: time.Now().UTC(),
|
||||
Author: "",
|
||||
CreatedBy: "",
|
||||
Comment: "",
|
||||
EmptyLayer: false,
|
||||
}
|
||||
dcompat := docker.V1Compatibility{}
|
||||
if err2 := json.Unmarshal([]byte(manifest.History[i].V1Compatibility), &dcompat); err2 == nil {
|
||||
h.Created = dcompat.Created.UTC()
|
||||
h.Author = dcompat.Author
|
||||
h.Comment = dcompat.Comment
|
||||
if len(dcompat.ContainerConfig.Cmd) > 0 {
|
||||
h.CreatedBy = fmt.Sprintf("%v", dcompat.ContainerConfig.Cmd)
|
||||
}
|
||||
h.EmptyLayer = dcompat.ThrowAway
|
||||
}
|
||||
// Prepend this layer to the list, because a v2s1 format manifest's list is in reverse order
|
||||
// compared to v2s2, which lists earlier layers before later ones.
|
||||
history = append([]docker.V2S2History{h}, history...)
|
||||
}
|
||||
dimage.RootFS = rootFS
|
||||
dimage.History = history
|
||||
return dimage, nil
|
||||
}
|
||||
|
||||
func (b *Builder) initConfig() {
|
||||
image := ociv1.Image{}
|
||||
dimage := docker.V2Image{}
|
||||
if len(b.Config) > 0 {
|
||||
// Try to parse the image configuration. If we fail start over from scratch.
|
||||
if err := json.Unmarshal(b.Config, &dimage); err == nil && dimage.DockerVersion != "" {
|
||||
if image, err = makeOCIv1Image(&dimage); err != nil {
|
||||
image = ociv1.Image{}
|
||||
}
|
||||
} else {
|
||||
if err := json.Unmarshal(b.Config, &image); err != nil {
|
||||
if dimage, err = makeDockerV2S2Image(&image); err != nil {
|
||||
dimage = docker.V2Image{}
|
||||
}
|
||||
}
|
||||
}
|
||||
b.OCIv1 = image
|
||||
b.Docker = dimage
|
||||
} else {
|
||||
// Try to dig out the image configuration from the manifest.
|
||||
manifest := docker.V2S1Manifest{}
|
||||
if err := json.Unmarshal(b.Manifest, &manifest); err == nil && manifest.SchemaVersion == 1 {
|
||||
if dimage, err = makeDockerV2S1Image(manifest); err == nil {
|
||||
if image, err = makeOCIv1Image(&dimage); err != nil {
|
||||
image = ociv1.Image{}
|
||||
}
|
||||
}
|
||||
}
|
||||
b.OCIv1 = image
|
||||
b.Docker = dimage
|
||||
}
|
||||
if len(b.Manifest) > 0 {
|
||||
// Attempt to recover format-specific data from the manifest.
|
||||
v1Manifest := ociv1.Manifest{}
|
||||
if json.Unmarshal(b.Manifest, &v1Manifest) == nil {
|
||||
b.ImageAnnotations = v1Manifest.Annotations
|
||||
}
|
||||
}
|
||||
b.fixupConfig()
|
||||
}
|
||||
|
||||
func (b *Builder) fixupConfig() {
|
||||
if b.Docker.Config != nil {
|
||||
// Prefer image-level settings over those from the container it was built from.
|
||||
b.Docker.ContainerConfig = *b.Docker.Config
|
||||
}
|
||||
b.Docker.Config = &b.Docker.ContainerConfig
|
||||
b.Docker.DockerVersion = ""
|
||||
now := time.Now().UTC()
|
||||
if b.Docker.Created.IsZero() {
|
||||
b.Docker.Created = now
|
||||
}
|
||||
if b.OCIv1.Created.IsZero() {
|
||||
b.OCIv1.Created = now
|
||||
}
|
||||
if b.OS() == "" {
|
||||
b.SetOS(runtime.GOOS)
|
||||
}
|
||||
if b.Architecture() == "" {
|
||||
b.SetArchitecture(runtime.GOARCH)
|
||||
}
|
||||
if b.WorkDir() == "" {
|
||||
b.SetWorkDir(string(filepath.Separator))
|
||||
}
|
||||
}
|
||||
|
||||
// Annotations returns a set of key-value pairs from the image's manifest.
|
||||
func (b *Builder) Annotations() map[string]string {
|
||||
return copyStringStringMap(b.ImageAnnotations)
|
||||
}
|
||||
|
||||
// SetAnnotation adds or overwrites a key's value from the image's manifest.
|
||||
// Note: this setting is not present in the Docker v2 image format, so it is
|
||||
// discarded when writing images using Docker v2 formats.
|
||||
func (b *Builder) SetAnnotation(key, value string) {
|
||||
if b.ImageAnnotations == nil {
|
||||
b.ImageAnnotations = map[string]string{}
|
||||
}
|
||||
b.ImageAnnotations[key] = value
|
||||
}
|
||||
|
||||
// UnsetAnnotation removes a key and its value from the image's manifest, if
|
||||
// it's present.
|
||||
func (b *Builder) UnsetAnnotation(key string) {
|
||||
delete(b.ImageAnnotations, key)
|
||||
}
|
||||
|
||||
// ClearAnnotations removes all keys and their values from the image's
|
||||
// manifest.
|
||||
func (b *Builder) ClearAnnotations() {
|
||||
b.ImageAnnotations = map[string]string{}
|
||||
}
|
||||
|
||||
// CreatedBy returns a description of how this image was built.
|
||||
func (b *Builder) CreatedBy() string {
|
||||
return b.ImageCreatedBy
|
||||
}
|
||||
|
||||
// SetCreatedBy sets the description of how this image was built.
|
||||
func (b *Builder) SetCreatedBy(how string) {
|
||||
b.ImageCreatedBy = how
|
||||
}
|
||||
|
||||
// OS returns a name of the OS on which the container, or a container built
|
||||
// using an image built from this container, is intended to be run.
|
||||
func (b *Builder) OS() string {
|
||||
return b.OCIv1.OS
|
||||
}
|
||||
|
||||
// SetOS sets the name of the OS on which the container, or a container built
|
||||
// using an image built from this container, is intended to be run.
|
||||
func (b *Builder) SetOS(os string) {
|
||||
b.OCIv1.OS = os
|
||||
b.Docker.OS = os
|
||||
}
|
||||
|
||||
// Architecture returns a name of the architecture on which the container, or a
|
||||
// container built using an image built from this container, is intended to be
|
||||
// run.
|
||||
func (b *Builder) Architecture() string {
|
||||
return b.OCIv1.Architecture
|
||||
}
|
||||
|
||||
// SetArchitecture sets the name of the architecture on which the container, or
|
||||
// a container built using an image built from this container, is intended to
|
||||
// be run.
|
||||
func (b *Builder) SetArchitecture(arch string) {
|
||||
b.OCIv1.Architecture = arch
|
||||
b.Docker.Architecture = arch
|
||||
}
|
||||
|
||||
// Maintainer returns contact information for the person who built the image.
|
||||
func (b *Builder) Maintainer() string {
|
||||
return b.OCIv1.Author
|
||||
}
|
||||
|
||||
// SetMaintainer sets contact information for the person who built the image.
|
||||
func (b *Builder) SetMaintainer(who string) {
|
||||
b.OCIv1.Author = who
|
||||
b.Docker.Author = who
|
||||
}
|
||||
|
||||
// User returns information about the user as whom the container, or a
|
||||
// container built using an image built from this container, should be run.
|
||||
func (b *Builder) User() string {
|
||||
return b.OCIv1.Config.User
|
||||
}
|
||||
|
||||
// SetUser sets information about the user as whom the container, or a
|
||||
// container built using an image built from this container, should be run.
|
||||
// Acceptable forms are a user name or ID, optionally followed by a colon and a
|
||||
// group name or ID.
|
||||
func (b *Builder) SetUser(spec string) {
|
||||
b.OCIv1.Config.User = spec
|
||||
b.Docker.Config.User = spec
|
||||
}
|
||||
|
||||
// WorkDir returns the default working directory for running commands in the
|
||||
// container, or in a container built using an image built from this container.
|
||||
func (b *Builder) WorkDir() string {
|
||||
return b.OCIv1.Config.WorkingDir
|
||||
}
|
||||
|
||||
// SetWorkDir sets the location of the default working directory for running
|
||||
// commands in the container, or in a container built using an image built from
|
||||
// this container.
|
||||
func (b *Builder) SetWorkDir(there string) {
|
||||
b.OCIv1.Config.WorkingDir = there
|
||||
b.Docker.Config.WorkingDir = there
|
||||
}
|
||||
|
||||
// Env returns a list of key-value pairs to be set when running commands in the
|
||||
// container, or in a container built using an image built from this container.
|
||||
func (b *Builder) Env() []string {
|
||||
return copyStringSlice(b.OCIv1.Config.Env)
|
||||
}
|
||||
|
||||
// SetEnv adds or overwrites a value to the set of environment strings which
|
||||
// should be set when running commands in the container, or in a container
|
||||
// built using an image built from this container.
|
||||
func (b *Builder) SetEnv(k string, v string) {
|
||||
reset := func(s *[]string) {
|
||||
n := []string{}
|
||||
for i := range *s {
|
||||
if !strings.HasPrefix((*s)[i], k+"=") {
|
||||
n = append(n, (*s)[i])
|
||||
}
|
||||
}
|
||||
n = append(n, k+"="+v)
|
||||
*s = n
|
||||
}
|
||||
reset(&b.OCIv1.Config.Env)
|
||||
reset(&b.Docker.Config.Env)
|
||||
}
|
||||
|
||||
// UnsetEnv removes a value from the set of environment strings which should be
|
||||
// set when running commands in this container, or in a container built using
|
||||
// an image built from this container.
|
||||
func (b *Builder) UnsetEnv(k string) {
|
||||
unset := func(s *[]string) {
|
||||
n := []string{}
|
||||
for i := range *s {
|
||||
if !strings.HasPrefix((*s)[i], k+"=") {
|
||||
n = append(n, (*s)[i])
|
||||
}
|
||||
}
|
||||
*s = n
|
||||
}
|
||||
unset(&b.OCIv1.Config.Env)
|
||||
unset(&b.Docker.Config.Env)
|
||||
}
|
||||
|
||||
// ClearEnv removes all values from the set of environment strings which should
|
||||
// be set when running commands in this container, or in a container built
|
||||
// using an image built from this container.
|
||||
func (b *Builder) ClearEnv() {
|
||||
b.OCIv1.Config.Env = []string{}
|
||||
b.Docker.Config.Env = []string{}
|
||||
}
|
||||
|
||||
// Cmd returns the default command, or command parameters if an Entrypoint is
|
||||
// set, to use when running a container built from an image built from this
|
||||
// container.
|
||||
func (b *Builder) Cmd() []string {
|
||||
return copyStringSlice(b.OCIv1.Config.Cmd)
|
||||
}
|
||||
|
||||
// SetCmd sets the default command, or command parameters if an Entrypoint is
|
||||
// set, to use when running a container built from an image built from this
|
||||
// container.
|
||||
func (b *Builder) SetCmd(cmd []string) {
|
||||
b.OCIv1.Config.Cmd = copyStringSlice(cmd)
|
||||
b.Docker.Config.Cmd = copyStringSlice(cmd)
|
||||
}
|
||||
|
||||
// Entrypoint returns the command to be run for containers built from images
|
||||
// built from this container.
|
||||
func (b *Builder) Entrypoint() []string {
|
||||
return copyStringSlice(b.OCIv1.Config.Entrypoint)
|
||||
}
|
||||
|
||||
// SetEntrypoint sets the command to be run for in containers built from images
|
||||
// built from this container.
|
||||
func (b *Builder) SetEntrypoint(ep []string) {
|
||||
b.OCIv1.Config.Entrypoint = copyStringSlice(ep)
|
||||
b.Docker.Config.Entrypoint = copyStringSlice(ep)
|
||||
}
|
||||
|
||||
// Labels returns a set of key-value pairs from the image's runtime
|
||||
// configuration.
|
||||
func (b *Builder) Labels() map[string]string {
|
||||
return copyStringStringMap(b.OCIv1.Config.Labels)
|
||||
}
|
||||
|
||||
// SetLabel adds or overwrites a key's value from the image's runtime
|
||||
// configuration.
|
||||
func (b *Builder) SetLabel(k string, v string) {
|
||||
if b.OCIv1.Config.Labels == nil {
|
||||
b.OCIv1.Config.Labels = map[string]string{}
|
||||
}
|
||||
b.OCIv1.Config.Labels[k] = v
|
||||
if b.Docker.Config.Labels == nil {
|
||||
b.Docker.Config.Labels = map[string]string{}
|
||||
}
|
||||
b.Docker.Config.Labels[k] = v
|
||||
}
|
||||
|
||||
// UnsetLabel removes a key and its value from the image's runtime
|
||||
// configuration, if it's present.
|
||||
func (b *Builder) UnsetLabel(k string) {
|
||||
delete(b.OCIv1.Config.Labels, k)
|
||||
delete(b.Docker.Config.Labels, k)
|
||||
}
|
||||
|
||||
// ClearLabels removes all keys and their values from the image's runtime
|
||||
// configuration.
|
||||
func (b *Builder) ClearLabels() {
|
||||
b.OCIv1.Config.Labels = map[string]string{}
|
||||
b.Docker.Config.Labels = map[string]string{}
|
||||
}
|
||||
|
||||
// Ports returns the set of ports which should be exposed when a container
|
||||
// based on an image built from this container is run.
|
||||
func (b *Builder) Ports() []string {
|
||||
p := []string{}
|
||||
for k := range b.OCIv1.Config.ExposedPorts {
|
||||
p = append(p, k)
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
// SetPort adds or overwrites an exported port in the set of ports which should
|
||||
// be exposed when a container based on an image built from this container is
|
||||
// run.
|
||||
func (b *Builder) SetPort(p string) {
|
||||
if b.OCIv1.Config.ExposedPorts == nil {
|
||||
b.OCIv1.Config.ExposedPorts = map[string]struct{}{}
|
||||
}
|
||||
b.OCIv1.Config.ExposedPorts[p] = struct{}{}
|
||||
if b.Docker.Config.ExposedPorts == nil {
|
||||
b.Docker.Config.ExposedPorts = make(docker.PortSet)
|
||||
}
|
||||
b.Docker.Config.ExposedPorts[docker.Port(p)] = struct{}{}
|
||||
}
|
||||
|
||||
// UnsetPort removes an exposed port from the set of ports which should be
|
||||
// exposed when a container based on an image built from this container is run.
|
||||
func (b *Builder) UnsetPort(p string) {
|
||||
delete(b.OCIv1.Config.ExposedPorts, p)
|
||||
delete(b.Docker.Config.ExposedPorts, docker.Port(p))
|
||||
}
|
||||
|
||||
// ClearPorts empties the set of ports which should be exposed when a container
|
||||
// based on an image built from this container is run.
|
||||
func (b *Builder) ClearPorts() {
|
||||
b.OCIv1.Config.ExposedPorts = map[string]struct{}{}
|
||||
b.Docker.Config.ExposedPorts = docker.PortSet{}
|
||||
}
|
||||
|
||||
// Volumes returns a list of filesystem locations which should be mounted from
|
||||
// outside of the container when a container built from an image built from
|
||||
// this container is run.
|
||||
func (b *Builder) Volumes() []string {
|
||||
v := []string{}
|
||||
for k := range b.OCIv1.Config.Volumes {
|
||||
v = append(v, k)
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
// AddVolume adds a location to the image's list of locations which should be
|
||||
// mounted from outside of the container when a container based on an image
|
||||
// built from this container is run.
|
||||
func (b *Builder) AddVolume(v string) {
|
||||
if b.OCIv1.Config.Volumes == nil {
|
||||
b.OCIv1.Config.Volumes = map[string]struct{}{}
|
||||
}
|
||||
b.OCIv1.Config.Volumes[v] = struct{}{}
|
||||
if b.Docker.Config.Volumes == nil {
|
||||
b.Docker.Config.Volumes = map[string]struct{}{}
|
||||
}
|
||||
b.Docker.Config.Volumes[v] = struct{}{}
|
||||
}
|
||||
|
||||
// RemoveVolume removes a location from the list of locations which should be
|
||||
// mounted from outside of the container when a container based on an image
|
||||
// built from this container is run.
|
||||
func (b *Builder) RemoveVolume(v string) {
|
||||
delete(b.OCIv1.Config.Volumes, v)
|
||||
delete(b.Docker.Config.Volumes, v)
|
||||
}
|
||||
|
||||
// ClearVolumes removes all locations from the image's list of locations which
|
||||
// should be mounted from outside of the container when a container based on an
|
||||
// image built from this container is run.
|
||||
func (b *Builder) ClearVolumes() {
|
||||
b.OCIv1.Config.Volumes = map[string]struct{}{}
|
||||
b.Docker.Config.Volumes = map[string]struct{}{}
|
||||
}
|
||||
|
||||
// Hostname returns the hostname which will be set in the container and in
|
||||
// containers built using images built from the container.
|
||||
func (b *Builder) Hostname() string {
|
||||
return b.Docker.Config.Hostname
|
||||
}
|
||||
|
||||
// SetHostname sets the hostname which will be set in the container and in
|
||||
// containers built using images built from the container.
|
||||
// Note: this setting is not present in the OCIv1 image format, so it is
|
||||
// discarded when writing images using OCIv1 formats.
|
||||
func (b *Builder) SetHostname(name string) {
|
||||
b.Docker.Config.Hostname = name
|
||||
}
|
||||
|
||||
// Domainname returns the domainname which will be set in the container and in
|
||||
// containers built using images built from the container.
|
||||
func (b *Builder) Domainname() string {
|
||||
return b.Docker.Config.Domainname
|
||||
}
|
||||
|
||||
// SetDomainname sets the domainname which will be set in the container and in
|
||||
// containers built using images built from the container.
|
||||
// Note: this setting is not present in the OCIv1 image format, so it is
|
||||
// discarded when writing images using OCIv1 formats.
|
||||
func (b *Builder) SetDomainname(name string) {
|
||||
b.Docker.Config.Domainname = name
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
#!/bin/bash
|
||||
|
||||
# This script is intended to be called by Cirrus-CI on a Mac M1 persistent worker.
|
||||
# It performs a best-effort attempt at cleaning up from one task execution to the next.
|
||||
# Since it run both before and after tasks, it must exit cleanly if there was a cleanup
|
||||
# failure (i.e. file or directory not found).
|
||||
|
||||
# Help anybody debugging side-effects, since failures are ignored (by necessity).
|
||||
set +e -x
|
||||
|
||||
# These are the main processes which could leak out of testing.
|
||||
killall podman vfkit gvproxy make go ginkgo
|
||||
|
||||
mkdir -p $TMPDIR
|
||||
|
||||
# Golang will leave behind lots of read-only bits, ref:
|
||||
# https://go.dev/ref/mod#module-cache
|
||||
# However other tools/scripts could also set things read-only.
|
||||
# At this point in CI, we really want all this stuff gone-gone,
|
||||
# so there's actually zero-chance it can interfere.
|
||||
chmod -R u+w $TMPDIR/* $TMPDIR/.??*
|
||||
|
||||
# This is defined as $TMPDIR during setup. Name must be kept
|
||||
# "short" as sockets may reside here. Darwin suffers from
|
||||
# the same limited socket-pathname character-length restriction
|
||||
# as Linux.
|
||||
rm -rf $TMPDIR/* $TMPDIR/.??*
|
||||
|
||||
# Don't change or clobber anything under $CIRRUS_WORKING_DIR for
|
||||
# the currently running task. But make sure we have write permission
|
||||
# (go get sets dependencies ro) for everything else, before removing it.
|
||||
# First make everything writeable - see the "Golang will..." comment above.
|
||||
# shellcheck disable=SC2154
|
||||
find "$HOME/ci" -mindepth 1 -maxdepth 1 \
|
||||
-not -name "*task-${CIRRUS_TASK_ID}*" -prune -exec chmod -R u+w '{}' +
|
||||
find "$HOME/ci" -mindepth 1 -maxdepth 1 \
|
||||
-not -name "*task-${CIRRUS_TASK_ID}*" -prune -exec rm -rf '{}' +
|
||||
|
||||
# Bash scripts exit with the status of the last command.
|
||||
true
|
|
@ -0,0 +1,15 @@
|
|||
ARG BASE_FQIN=quay.io/coreos-assembler/fcos-buildroot:testing-devel
|
||||
FROM $BASE_FQIN
|
||||
|
||||
# See 'Danger of using COPY and ADD instructions'
|
||||
# at https://cirrus-ci.org/guide/docker-builder-vm/#dockerfile-as-a-ci-environment
|
||||
# Provide easy way to force-invalidate image cache by .cirrus.yml change
|
||||
ARG CIRRUS_IMAGE_VERSION
|
||||
ENV CIRRUS_IMAGE_VERSION=$CIRRUS_IMAGE_VERSION
|
||||
ADD https://sh.rustup.rs /var/tmp/rustup_installer.sh
|
||||
|
||||
RUN dnf remove -y rust && \
|
||||
chmod +x /var/tmp/rustup_installer.sh && \
|
||||
/var/tmp/rustup_installer.sh -y --default-toolchain stable --profile minimal
|
||||
|
||||
ENV PATH=/root/.cargo/bin:/root/.local/bin:/root/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
|
|
@ -0,0 +1,145 @@
|
|||
#!/bin/bash
|
||||
|
||||
# This script is intended to be executed by automation or humans
|
||||
# under a hack/get_ci_vm.sh context. Use under any other circumstances
|
||||
# is unlikely to function.
|
||||
|
||||
set -e
|
||||
|
||||
# BEGIN Global export of all variables
|
||||
set -a
|
||||
|
||||
# Due to differences across platforms and runtime execution environments,
|
||||
# handling of the (otherwise) default shell setup is non-uniform. Rather
|
||||
# than attempt to workaround differences, simply force-load/set required
|
||||
# items every time this library is utilized.
|
||||
USER="$(whoami)"
|
||||
HOME="$(getent passwd $USER | cut -d : -f 6)"
|
||||
# Some platforms set and make this read-only
|
||||
[[ -n "$UID" ]] || \
|
||||
UID=$(getent passwd $USER | cut -d : -f 3)
|
||||
|
||||
if [[ -r "/etc/automation_environment" ]]; then
|
||||
source /etc/automation_environment
|
||||
source $AUTOMATION_LIB_PATH/common_lib.sh
|
||||
else
|
||||
(
|
||||
echo "WARNING: It does not appear that containers/automation was installed."
|
||||
echo " Functionality of most of ${BASH_SOURCE[0]} will be negatively"
|
||||
echo " impacted."
|
||||
) > /dev/stderr
|
||||
fi
|
||||
|
||||
# This is the magic interpreted by the tests to allow modifying local config/services.
|
||||
SKOPEO_CONTAINER_TESTS=1
|
||||
|
||||
PATH=$PATH:$GOPATH/bin
|
||||
|
||||
# END Global export of all variables
|
||||
set +a
|
||||
|
||||
|
||||
_run_setup() {
|
||||
local mnt
|
||||
local errmsg
|
||||
req_env_vars SKOPEO_CIDEV_CONTAINER_FQIN
|
||||
if [[ "$OS_RELEASE_ID" != "fedora" ]]; then
|
||||
die "Unknown/unsupported distro. $OS_REL_VER"
|
||||
fi
|
||||
|
||||
if [[ -r "/.ci_setup_complete" ]]; then
|
||||
warn "Thwarted an attempt to execute setup more than once."
|
||||
return
|
||||
fi
|
||||
|
||||
# VM's come with the distro. skopeo package pre-installed
|
||||
dnf remove -y skopeo
|
||||
|
||||
msg "Removing systemd-resolved from nsswitch.conf"
|
||||
# /etc/resolv.conf is already set to bypass systemd-resolvd
|
||||
sed -i -r -e 's/^(hosts.+)resolve.+dns/\1dns/' /etc/nsswitch.conf
|
||||
|
||||
# A slew of compiled binaries are pre-built and distributed
|
||||
# within the CI/Dev container image, but we want to run
|
||||
# things directly on the host VM. Fortunately they're all
|
||||
# located in the container under /usr/local/bin
|
||||
msg "Accessing contents of $SKOPEO_CIDEV_CONTAINER_FQIN"
|
||||
podman pull --retry 3 --quiet $SKOPEO_CIDEV_CONTAINER_FQIN
|
||||
mnt=$(podman mount $(podman create $SKOPEO_CIDEV_CONTAINER_FQIN))
|
||||
|
||||
# The container and VM images are built in tandem in the same repo.
|
||||
# automation, but the sources are in different directories. It's
|
||||
# possible for a mismatch to happen, but should (hopefully) be unlikely.
|
||||
# Double-check to make sure.
|
||||
# Temporarily, allow running on Rawhide VMs and consuming older binaries:
|
||||
# that should be compatible enough. Eventually, we’ll stop using Rawhide again.
|
||||
if ! grep -Fqx "ID=$OS_RELEASE_ID" $mnt/etc/os-release || \
|
||||
{ ! [[ "$VM_IMAGE_NAME" =~ "rawhide" ]] && ! grep -Fqx "VERSION_ID=$OS_RELEASE_VER" $mnt/etc/os-release; } then
|
||||
die "Somehow $SKOPEO_CIDEV_CONTAINER_FQIN is not based on $OS_REL_VER."
|
||||
fi
|
||||
msg "Copying test binaries from $SKOPEO_CIDEV_CONTAINER_FQIN /usr/local/bin/"
|
||||
cp -a "$mnt/usr/local/bin/"* "/usr/local/bin/"
|
||||
msg "Configuring the openshift registry"
|
||||
|
||||
# TODO: Put directory & yaml into more sensible place + update integration tests
|
||||
mkdir -vp /registry
|
||||
cp -a "$mnt/atomic-registry-config.yml" /
|
||||
|
||||
msg "Cleaning up"
|
||||
podman umount --latest
|
||||
podman rm --latest
|
||||
|
||||
# Ensure setup can only run once
|
||||
touch "/.ci_setup_complete"
|
||||
}
|
||||
|
||||
_run_vendor() {
|
||||
make vendor BUILDTAGS="$BUILDTAGS"
|
||||
}
|
||||
|
||||
_run_build() {
|
||||
make bin/skopeo BUILDTAGS="$BUILDTAGS"
|
||||
make install PREFIX=/usr/local
|
||||
}
|
||||
|
||||
_run_cross() {
|
||||
make local-cross BUILDTAGS="$BUILDTAGS"
|
||||
}
|
||||
|
||||
_run_doccheck() {
|
||||
make validate-docs BUILDTAGS="$BUILDTAGS"
|
||||
}
|
||||
|
||||
_run_unit() {
|
||||
make test-unit-local BUILDTAGS="$BUILDTAGS"
|
||||
}
|
||||
|
||||
_podman_reset() {
|
||||
# Ensure we start with a clean-slate
|
||||
showrun podman system reset --force
|
||||
}
|
||||
|
||||
_run_integration() {
|
||||
_podman_reset
|
||||
make test-integration-local BUILDTAGS="$BUILDTAGS"
|
||||
}
|
||||
|
||||
_run_system() {
|
||||
_podman_reset
|
||||
##### Note: Test MODIFIES THE HOST SETUP #####
|
||||
make test-system-local BUILDTAGS="$BUILDTAGS"
|
||||
}
|
||||
|
||||
req_env_vars SKOPEO_PATH
|
||||
|
||||
handler="_run_${1}"
|
||||
if [ "$(type -t $handler)" != "function" ]; then
|
||||
die "Unknown/Unsupported command-line argument '$1'"
|
||||
fi
|
||||
|
||||
msg "************************************************************"
|
||||
msg "Runner executing $1 on $OS_REL_VER"
|
||||
msg "************************************************************"
|
||||
|
||||
cd "$SKOPEO_PATH"
|
||||
$handler
|
|
@ -1,705 +0,0 @@
|
|||
#!/bin/bash
|
||||
#
|
||||
# bash completion file for buildah command
|
||||
#
|
||||
# This script provides completion of:
|
||||
# - commands and their options
|
||||
# - filepaths
|
||||
#
|
||||
# To enable the completions either:
|
||||
# - place this file in /usr/share/bash-completion/completions
|
||||
# or
|
||||
# - copy this file to e.g. ~/.buildah-completion.sh and add the line
|
||||
# below to your .bashrc after bash completion features are loaded
|
||||
# . ~/.buildah-completion.sh
|
||||
#
|
||||
# Configuration:
|
||||
#
|
||||
|
||||
# __buildah_to_alternatives transforms a multiline list of strings into a single line
|
||||
# string with the words separated by `|`.
|
||||
# This is used to prepare arguments to __buildah_pos_first_nonflag().
|
||||
__buildah_to_alternatives() {
|
||||
local parts=( $1 )
|
||||
local IFS='|'
|
||||
echo "${parts[*]}"
|
||||
}
|
||||
|
||||
# __buildah_to_extglob transforms a multiline list of options into an extglob pattern
|
||||
# suitable for use in case statements.
|
||||
__buildah_to_extglob() {
|
||||
local extglob=$( __buildah_to_alternatives "$1" )
|
||||
echo "@($extglob)"
|
||||
}
|
||||
|
||||
# __buildah_pos_first_nonflag finds the position of the first word that is neither
|
||||
# option nor an option's argument. If there are options that require arguments,
|
||||
# you should pass a glob describing those options, e.g. "--option1|-o|--option2"
|
||||
# Use this function to restrict completions to exact positions after the argument list.
|
||||
__buildah_pos_first_nonflag() {
|
||||
local argument_flags=$1
|
||||
|
||||
local counter=$((${subcommand_pos:-${command_pos}} + 1))
|
||||
while [ $counter -le $cword ]; do
|
||||
if [ -n "$argument_flags" ] && eval "case '${words[$counter]}' in $argument_flags) true ;; *) false ;; esac"; then
|
||||
(( counter++ ))
|
||||
# eat "=" in case of --option=arg syntax
|
||||
[ "${words[$counter]}" = "=" ] && (( counter++ ))
|
||||
else
|
||||
case "${words[$counter]}" in
|
||||
-*)
|
||||
;;
|
||||
*)
|
||||
break
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
# Bash splits words at "=", retaining "=" as a word, examples:
|
||||
# "--debug=false" => 3 words, "--log-opt syslog-facility=daemon" => 4 words
|
||||
while [ "${words[$counter + 1]}" = "=" ] ; do
|
||||
counter=$(( counter + 2))
|
||||
done
|
||||
|
||||
(( counter++ ))
|
||||
done
|
||||
|
||||
echo $counter
|
||||
}
|
||||
|
||||
# Note for developers:
|
||||
# Please arrange options sorted alphabetically by long name with the short
|
||||
# options immediately following their corresponding long form.
|
||||
# This order should be applied to lists, alternatives and code blocks.
|
||||
|
||||
__buildah_previous_extglob_setting=$(shopt -p extglob)
|
||||
shopt -s extglob
|
||||
|
||||
__buildah_list_containers() {
|
||||
COMPREPLY=($(compgen -W "$(buildah containers -q)" -- $cur))
|
||||
}
|
||||
|
||||
__buildah_list_images() {
|
||||
COMPREPLY=($(compgen -W "$(buildah images -q)" -- $cur))
|
||||
}
|
||||
|
||||
__buildah_pos_first_nonflag() {
|
||||
local argument_flags=$1
|
||||
|
||||
local counter=$((${subcommand_pos:-${command_pos}} + 1))
|
||||
while [ $counter -le $cword ]; do
|
||||
if [ -n "$argument_flags" ] && eval "case '${words[$counter]}' in $argument_flags) true ;; *) false ;; esac"; then
|
||||
((counter++))
|
||||
else
|
||||
case "${words[$counter]}" in
|
||||
-*) ;;
|
||||
*)
|
||||
break
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
((counter++))
|
||||
done
|
||||
|
||||
echo $counter
|
||||
}
|
||||
|
||||
# Transforms a multiline list of strings into a single line string
|
||||
# with the words separated by "|".
|
||||
# This is used to prepare arguments to __buildah_pos_first_nonflag().
|
||||
__buildah_to_alternatives() {
|
||||
local parts=($1)
|
||||
local IFS='|'
|
||||
echo "${parts[*]}"
|
||||
}
|
||||
|
||||
# Transforms a multiline list of options into an extglob pattern
|
||||
# suitable for use in case statements.
|
||||
__buildah_to_extglob() {
|
||||
local extglob=$(__buildah_to_alternatives "$1")
|
||||
echo "@($extglob)"
|
||||
}
|
||||
|
||||
# Subcommand processing.
|
||||
# Locates the first occurrence of any of the subcommands contained in the
|
||||
# first argument. In case of a match, calls the corresponding completion
|
||||
# function and returns 0.
|
||||
# If no match is found, 1 is returned. The calling function can then
|
||||
# continue processing its completion.
|
||||
#
|
||||
# TODO if the preceding command has options that accept arguments and an
|
||||
# argument is equal ot one of the subcommands, this is falsely detected as
|
||||
# a match.
|
||||
__buildah_subcommands() {
|
||||
local subcommands="$1"
|
||||
|
||||
local counter=$(($command_pos + 1))
|
||||
while [ $counter -lt $cword ]; do
|
||||
case "${words[$counter]}" in
|
||||
$(__buildah_to_extglob "$subcommands") )
|
||||
subcommand_pos=$counter
|
||||
local subcommand=${words[$counter]}
|
||||
local completions_func=_buildah_${command}_${subcommand}
|
||||
declare -F $completions_func >/dev/null && $completions_func
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
(( counter++ ))
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
# suppress trailing whitespace
|
||||
__buildah_nospace() {
|
||||
# compopt is not available in ancient bash versions
|
||||
type compopt &>/dev/null && compopt -o nospace
|
||||
}
|
||||
|
||||
|
||||
# global options that may appear after the buildah command
|
||||
_buildah_buildah() {
|
||||
local boolean_options="
|
||||
--debug
|
||||
--help -h
|
||||
--version -v
|
||||
"
|
||||
local options_with_args="
|
||||
--root
|
||||
--runroot
|
||||
--storage-driver
|
||||
--storage-opt
|
||||
"
|
||||
|
||||
case "$prev" in
|
||||
--root | --runroot)
|
||||
case "$cur" in
|
||||
*:*) ;; # TODO somehow do _filedir for stuff inside the image, if it's already specified (which is also somewhat difficult to determine)
|
||||
'')
|
||||
COMPREPLY=($(compgen -W '/' -- "$cur"))
|
||||
__buildah_nospace
|
||||
;;
|
||||
*)
|
||||
_filedir
|
||||
__buildah_nospace
|
||||
;;
|
||||
esac
|
||||
return
|
||||
;;
|
||||
--storage-driver)
|
||||
COMPREPLY=($(compgen -W 'devicemapper overlay2' -- "$cur"))
|
||||
return
|
||||
;;
|
||||
$(__buildah_to_extglob "$options_with_args"))
|
||||
return
|
||||
;;
|
||||
esac
|
||||
|
||||
case "$cur" in
|
||||
-*)
|
||||
COMPREPLY=($(compgen -W "$boolean_options $options_with_args" -- "$cur"))
|
||||
;;
|
||||
*)
|
||||
local counter=$(__buildah_pos_first_nonflag $(__buildah_to_extglob "$options_with_args"))
|
||||
if [ $cword -eq $counter ]; then
|
||||
COMPREPLY=($(compgen -W "${commands[*]} help" -- "$cur"))
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
_buildah_rmi() {
|
||||
local boolean_options="
|
||||
--help
|
||||
-h
|
||||
"
|
||||
|
||||
case "$cur" in
|
||||
-*)
|
||||
COMPREPLY=($(compgen -W "$boolean_options $options_with_args" -- "$cur"))
|
||||
;;
|
||||
*)
|
||||
__buildah_list_images
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
_buildah_rm() {
|
||||
local boolean_options="
|
||||
--help
|
||||
-h
|
||||
"
|
||||
|
||||
case "$cur" in
|
||||
-*)
|
||||
COMPREPLY=($(compgen -W "$boolean_options $options_with_args" -- "$cur"))
|
||||
;;
|
||||
*)
|
||||
__buildah_list_containers
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
_buildah_help() {
|
||||
local counter=$(__buildah_pos_first_nonflag)
|
||||
if [ $cword -eq $counter ]; then
|
||||
COMPREPLY=($(compgen -W "${commands[*]}" -- "$cur"))
|
||||
fi
|
||||
}
|
||||
|
||||
_buildah_config() {
|
||||
local boolean_options="
|
||||
--help
|
||||
-h
|
||||
"
|
||||
|
||||
local options_with_args="
|
||||
--annotation
|
||||
--arch
|
||||
--author
|
||||
--cmd
|
||||
--created-by
|
||||
--entrypoint
|
||||
--env
|
||||
-e
|
||||
--label
|
||||
-l
|
||||
--os
|
||||
--port
|
||||
-p
|
||||
--user
|
||||
-u
|
||||
--volume
|
||||
-v
|
||||
--workingdir
|
||||
"
|
||||
|
||||
local all_options="$options_with_args $boolean_options"
|
||||
|
||||
case "$cur" in
|
||||
-*)
|
||||
COMPREPLY=($(compgen -W "$boolean_options $options_with_args" -- "$cur"))
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
_buildah_commit() {
|
||||
local boolean_options="
|
||||
--help
|
||||
-h
|
||||
--disable-compression
|
||||
-D
|
||||
--quiet
|
||||
-q
|
||||
"
|
||||
|
||||
local options_with_args="
|
||||
--signature-policy
|
||||
--format
|
||||
-f
|
||||
"
|
||||
|
||||
local all_options="$options_with_args $boolean_options"
|
||||
|
||||
case "$prev" in
|
||||
--signature-policy)
|
||||
case "$cur" in
|
||||
*:*) ;; # TODO somehow do _filedir for stuff inside the image, if it's already specified (which is also somewhat difficult to determine)
|
||||
'')
|
||||
COMPREPLY=($(compgen -W '/' -- "$cur"))
|
||||
__buildah_nospace
|
||||
;;
|
||||
*)
|
||||
_filedir
|
||||
__buildah_nospace
|
||||
;;
|
||||
esac
|
||||
return
|
||||
;;
|
||||
|
||||
$(__buildah_to_extglob "$options_with_args"))
|
||||
return
|
||||
;;
|
||||
esac
|
||||
|
||||
case "$cur" in
|
||||
-*)
|
||||
COMPREPLY=($(compgen -W "$boolean_options $options_with_args" -- "$cur"))
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
_buildah_bud() {
|
||||
local boolean_options="
|
||||
--help
|
||||
-h
|
||||
--pull
|
||||
--pull-always
|
||||
--quiet
|
||||
-q
|
||||
"
|
||||
|
||||
local options_with_args="
|
||||
--registry
|
||||
--signature-policy
|
||||
--runtime
|
||||
--runtime-flag
|
||||
--tag
|
||||
-t
|
||||
--file
|
||||
-f
|
||||
--build-arg
|
||||
--format
|
||||
"
|
||||
|
||||
local all_options="$options_with_args $boolean_options"
|
||||
|
||||
case "$prev" in
|
||||
--runtime)
|
||||
COMPREPLY=($(compgen -W 'runc runv' -- "$cur"))
|
||||
;;
|
||||
$(__buildah_to_extglob "$options_with_args"))
|
||||
return
|
||||
;;
|
||||
esac
|
||||
|
||||
case "$cur" in
|
||||
-*)
|
||||
COMPREPLY=($(compgen -W "$boolean_options $options_with_args" -- "$cur"))
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
_buildah_build_using_dockerfile() {
|
||||
_buildah_bud "$@"
|
||||
}
|
||||
|
||||
_buildah_run() {
|
||||
local boolean_options="
|
||||
--help
|
||||
-h
|
||||
"
|
||||
|
||||
local options_with_args="
|
||||
--runtime
|
||||
--runtime-flag
|
||||
--volume
|
||||
-v
|
||||
"
|
||||
|
||||
local all_options="$options_with_args $boolean_options"
|
||||
|
||||
case "$prev" in
|
||||
--runtime)
|
||||
COMPREPLY=($(compgen -W 'runc runv' -- "$cur"))
|
||||
;;
|
||||
$(__buildah_to_extglob "$options_with_args"))
|
||||
return
|
||||
;;
|
||||
esac
|
||||
|
||||
case "$cur" in
|
||||
-*)
|
||||
COMPREPLY=($(compgen -W "$boolean_options $options_with_args" -- "$cur"))
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
_buildah_copy() {
|
||||
local boolean_options="
|
||||
--help
|
||||
-h
|
||||
"
|
||||
|
||||
local options_with_args="
|
||||
"
|
||||
|
||||
local all_options="$options_with_args $boolean_options"
|
||||
|
||||
case "$cur" in
|
||||
-*)
|
||||
COMPREPLY=($(compgen -W "$boolean_options $options_with_args" -- "$cur"))
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
_buildah_add() {
|
||||
local boolean_options="
|
||||
--help
|
||||
-h
|
||||
"
|
||||
|
||||
local options_with_args="
|
||||
"
|
||||
|
||||
local all_options="$options_with_args $boolean_options"
|
||||
|
||||
case "$cur" in
|
||||
-*)
|
||||
COMPREPLY=($(compgen -W "$boolean_options $options_with_args" -- "$cur"))
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
_buildah_unmount() {
|
||||
_buildah_umount $@
|
||||
}
|
||||
|
||||
_buildah_umount() {
|
||||
local boolean_options="
|
||||
--help
|
||||
-h
|
||||
"
|
||||
|
||||
local options_with_args="
|
||||
"
|
||||
|
||||
local all_options="$options_with_args $boolean_options"
|
||||
|
||||
case "$cur" in
|
||||
-*)
|
||||
COMPREPLY=($(compgen -W "$boolean_options $options_with_args" -- "$cur"))
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
_buildah_push() {
|
||||
local boolean_options="
|
||||
--help
|
||||
-h
|
||||
--disable-compression
|
||||
-D
|
||||
--quiet
|
||||
-q
|
||||
"
|
||||
|
||||
local options_with_args="
|
||||
--signature-policy
|
||||
"
|
||||
|
||||
local all_options="$options_with_args $boolean_options"
|
||||
|
||||
case "$prev" in
|
||||
--signature-policy)
|
||||
case "$cur" in
|
||||
*:*) ;; # TODO somehow do _filedir for stuff inside the image, if it's already specified (which is also somewhat difficult to determine)
|
||||
'')
|
||||
COMPREPLY=($(compgen -W '/' -- "$cur"))
|
||||
__buildah_nospace
|
||||
;;
|
||||
*)
|
||||
_filedir
|
||||
__buildah_nospace
|
||||
;;
|
||||
esac
|
||||
return
|
||||
;;
|
||||
|
||||
$(__buildah_to_extglob "$options_with_args"))
|
||||
return
|
||||
;;
|
||||
esac
|
||||
|
||||
case "$cur" in
|
||||
-*)
|
||||
COMPREPLY=($(compgen -W "$boolean_options $options_with_args" -- "$cur"))
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
_buildah_mount() {
|
||||
local boolean_options="
|
||||
--help
|
||||
-h
|
||||
--notruncate
|
||||
"
|
||||
|
||||
local options_with_args="
|
||||
"
|
||||
|
||||
local all_options="$options_with_args $boolean_options"
|
||||
|
||||
case "$cur" in
|
||||
-*)
|
||||
COMPREPLY=($(compgen -W "$boolean_options $options_with_args" -- "$cur"))
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
_buildah_containers() {
|
||||
local boolean_options="
|
||||
--help
|
||||
-h
|
||||
--quiet
|
||||
-q
|
||||
--noheading
|
||||
-n
|
||||
--notruncate
|
||||
"
|
||||
|
||||
local options_with_args="
|
||||
"
|
||||
|
||||
local all_options="$options_with_args $boolean_options"
|
||||
|
||||
case "$cur" in
|
||||
-*)
|
||||
COMPREPLY=($(compgen -W "$boolean_options $options_with_args" -- "$cur"))
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
_buildah_images() {
|
||||
local boolean_options="
|
||||
--help
|
||||
-h
|
||||
--quiet
|
||||
-q
|
||||
--noheading
|
||||
-n
|
||||
--notruncate
|
||||
"
|
||||
|
||||
local options_with_args="
|
||||
"
|
||||
|
||||
local all_options="$options_with_args $boolean_options"
|
||||
|
||||
case "$cur" in
|
||||
-*)
|
||||
COMPREPLY=($(compgen -W "$boolean_options $options_with_args" -- "$cur"))
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
_buildah_inspect() {
|
||||
local options_with_args="
|
||||
--format
|
||||
-f
|
||||
--type
|
||||
-t
|
||||
"
|
||||
|
||||
local all_options="$options_with_args"
|
||||
|
||||
case "$cur" in
|
||||
-*)
|
||||
COMPREPLY=($(compgen -W "$options_with_args" -- "$cur"))
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
_buildah_tag() {
|
||||
local options_with_args="
|
||||
"
|
||||
|
||||
local all_options="$options_with_args"
|
||||
|
||||
case "$cur" in
|
||||
-*)
|
||||
COMPREPLY=($(compgen -W "$options_with_args" -- "$cur"))
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
_buildah_from() {
|
||||
local boolean_options="
|
||||
--help
|
||||
-h
|
||||
--pull
|
||||
--pull-always
|
||||
--quiet
|
||||
-q
|
||||
"
|
||||
|
||||
local options_with_args="
|
||||
--name
|
||||
--registry
|
||||
--signature-policy
|
||||
"
|
||||
|
||||
|
||||
case "$cur" in
|
||||
-*)
|
||||
COMPREPLY=($(compgen -W "$boolean_options $options_with_args" -- "$cur"))
|
||||
;;
|
||||
*)
|
||||
__buildah_list_containers
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
_buildah() {
|
||||
local previous_extglob_setting=$(shopt -p extglob)
|
||||
shopt -s extglob
|
||||
|
||||
local commands=(
|
||||
add
|
||||
bud
|
||||
build-using-dockerfile
|
||||
commit
|
||||
config
|
||||
containers
|
||||
copy
|
||||
from
|
||||
images
|
||||
inspect
|
||||
mount
|
||||
push
|
||||
rm
|
||||
rmi
|
||||
run
|
||||
tag
|
||||
umount
|
||||
unmount
|
||||
)
|
||||
|
||||
# These options are valid as global options for all client commands
|
||||
local global_boolean_options="
|
||||
--debug
|
||||
"
|
||||
|
||||
local global_options_with_args="
|
||||
--root
|
||||
--runroot
|
||||
--storage-driver
|
||||
--storage-opt
|
||||
"
|
||||
|
||||
COMPREPLY=()
|
||||
local cur prev words cword
|
||||
_get_comp_words_by_ref -n : cur prev words cword
|
||||
|
||||
local command='buildah' command_pos=0 subcommand_pos
|
||||
local counter=1
|
||||
while [ $counter -lt $cword ]; do
|
||||
case "${words[$counter]}" in
|
||||
$(__buildah_to_extglob "$global_options_with_args") )
|
||||
(( counter++ ))
|
||||
;;
|
||||
-*)
|
||||
;;
|
||||
=)
|
||||
(( counter++ ))
|
||||
;;
|
||||
*)
|
||||
command="${words[$counter]}"
|
||||
command_pos=$counter
|
||||
break
|
||||
;;
|
||||
esac
|
||||
(( counter++ ))
|
||||
done
|
||||
|
||||
local binary="${words[0]}"
|
||||
|
||||
local completions_func=_buildah_${command/-/_}
|
||||
declare -F $completions_func >/dev/null && $completions_func
|
||||
|
||||
eval "$previous_extglob_setting"
|
||||
return 0
|
||||
}
|
||||
|
||||
eval "$__buildah_previous_extglob_setting"
|
||||
unset __buildah_previous_extglob_setting
|
||||
|
||||
complete -F _buildah buildah
|
|
@ -1,89 +0,0 @@
|
|||
%if 0%{?fedora} || 0%{?rhel} == 6
|
||||
%global with_bundled 1
|
||||
%global with_debug 0
|
||||
%global with_check 1
|
||||
%else
|
||||
%global with_bundled 0
|
||||
%global with_debug 0
|
||||
%global with_check 0
|
||||
%endif
|
||||
|
||||
%if 0%{?with_debug}
|
||||
%global _dwz_low_mem_die_limit 0
|
||||
%else
|
||||
%global debug_package %{nil}
|
||||
%endif
|
||||
|
||||
%global provider github
|
||||
%global provider_tld com
|
||||
%global project projectatomic
|
||||
%global repo buildah
|
||||
# https://github.com/projectatomic/buildah
|
||||
%global provider_prefix %{provider}.%{provider_tld}/%{project}/%{repo}
|
||||
%global import_path %{provider_prefix}
|
||||
%global commit a0a5333b94264d1fb1e072d63bcb98f9e2981b49
|
||||
%global shortcommit %(c=%{commit}; echo ${c:0:7})
|
||||
|
||||
Name: buildah
|
||||
Version: 0.1
|
||||
Release: 1.git%{shortcommit}%{?dist}
|
||||
Summary: A command line tool used to creating OCI Images
|
||||
License: ASL 2.0
|
||||
URL: https://%{provider_prefix}
|
||||
Source: https://%{provider_prefix}/archive/%{commit}/%{repo}-%{shortcommit}.tar.gz
|
||||
|
||||
ExclusiveArch: x86_64 aarch64 ppc64le
|
||||
# If go_compiler is not set to 1, there is no virtual provide. Use golang instead.
|
||||
BuildRequires: %{?go_compiler:compiler(go-compiler)}%{!?go_compiler:golang}
|
||||
BuildRequires: git
|
||||
BuildRequires: go-md2man
|
||||
BuildRequires: gpgme-devel
|
||||
BuildRequires: device-mapper-devel
|
||||
BuildRequires: btrfs-progs-devel
|
||||
BuildRequires: libassuan-devel
|
||||
Requires: runc >= 1.0.0-6
|
||||
Requires: skopeo-containers
|
||||
Provides: %{repo} = %{version}-%{release}
|
||||
|
||||
%description
|
||||
The buildah package provides a command line tool which can be used to
|
||||
* create a working container from scratch
|
||||
or
|
||||
* create a working container from an image as a starting point
|
||||
* mount/umount a working container's root file system for manipulation
|
||||
* save container's root file system layer to create a new image
|
||||
* delete a working container or an image
|
||||
|
||||
%prep
|
||||
%autosetup -Sgit -n %{name}-%{commit}
|
||||
|
||||
%build
|
||||
mkdir _build
|
||||
pushd _build
|
||||
mkdir -p src/%{provider}.%{provider_tld}/%{project}
|
||||
ln -s $(dirs +1 -l) src/%{import_path}
|
||||
popd
|
||||
|
||||
mv vendor src
|
||||
|
||||
export GOPATH=$(pwd)/_build:$(pwd):%{gopath}
|
||||
make all
|
||||
|
||||
%install
|
||||
export GOPATH=$(pwd)/_build:$(pwd):%{gopath}
|
||||
|
||||
make DESTDIR=%{buildroot} PREFIX=%{_prefix} install install.completions
|
||||
|
||||
#define license tag if not already defined
|
||||
%{!?_licensedir:%global license %doc}
|
||||
|
||||
%files
|
||||
%license LICENSE
|
||||
%doc README.md
|
||||
%{_bindir}/%{name}
|
||||
%{_mandir}/man1/buildah*
|
||||
%{_datadir}/bash-completion/completions/*
|
||||
|
||||
%changelog
|
||||
* Fri Apr 14 2017 Dan Walsh <dwalsh@redhat.com> 0.0.1-1.git7a0a5333
|
||||
- First package for Fedora
|
|
@ -0,0 +1,2 @@
|
|||
The skopeo container image build context and automation have been
|
||||
moved to [https://github.com/containers/image_build/tree/main/skopeo](https://github.com/containers/image_build/tree/main/skopeo)
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"default": [
|
||||
{
|
||||
"type": "insecureAcceptAnything"
|
||||
}
|
||||
],
|
||||
"transports":
|
||||
{
|
||||
"docker-daemon":
|
||||
{
|
||||
"": [{"type":"insecureAcceptAnything"}]
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
# This is a default registries.d configuration file. You may
|
||||
# add to this file or create additional files in registries.d/.
|
||||
#
|
||||
# lookaside: for reading/writing simple signing signatures
|
||||
# lookaside-staging: for writing simple signing signatures, preferred over lookaside
|
||||
#
|
||||
# lookaside and lookaside-staging take a value of the following:
|
||||
# lookaside: {schema}://location
|
||||
#
|
||||
# For reading signatures, schema may be http, https, or file.
|
||||
# For writing signatures, schema may only be file.
|
||||
|
||||
# The default locations are built-in, for both reading and writing:
|
||||
# /var/lib/containers/sigstore for root, or
|
||||
# ~/.local/share/containers/sigstore for non-root users.
|
||||
default-docker:
|
||||
# lookaside: https://…
|
||||
# lookaside-staging: file:///…
|
||||
|
||||
# The 'docker' indicator here is the start of the configuration
|
||||
# for docker registries.
|
||||
#
|
||||
# docker:
|
||||
#
|
||||
# privateregistry.com:
|
||||
# lookaside: https://privateregistry.com/sigstore/
|
||||
# lookaside-staging: /mnt/nfs/privateregistry/sigstore
|
||||
|
17
delete.go
17
delete.go
|
@ -1,17 +0,0 @@
|
|||
package buildah
|
||||
|
||||
import (
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// Delete removes the working container. The buildah.Builder object should not
|
||||
// be used after this method is called.
|
||||
func (b *Builder) Delete() error {
|
||||
if err := b.store.DeleteContainer(b.ContainerID); err != nil {
|
||||
return errors.Wrapf(err, "error deleting build container")
|
||||
}
|
||||
b.MountPoint = ""
|
||||
b.Container = ""
|
||||
b.ContainerID = ""
|
||||
return nil
|
||||
}
|
1788
docker/AUTHORS
1788
docker/AUTHORS
File diff suppressed because it is too large
Load Diff
271
docker/types.go
271
docker/types.go
|
@ -1,271 +0,0 @@
|
|||
package docker
|
||||
|
||||
//
|
||||
// Types extracted from Docker
|
||||
//
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/containers/image/pkg/strslice"
|
||||
"github.com/opencontainers/go-digest"
|
||||
)
|
||||
|
||||
// github.com/moby/moby/image/rootfs.go
|
||||
const TypeLayers = "layers"
|
||||
|
||||
// github.com/docker/distribution/manifest/schema2/manifest.go
|
||||
const V2S2MediaTypeManifest = "application/vnd.docker.distribution.manifest.v2+json"
|
||||
|
||||
// github.com/docker/distribution/manifest/schema2/manifest.go
|
||||
const V2S2MediaTypeImageConfig = "application/vnd.docker.container.image.v1+json"
|
||||
|
||||
// github.com/docker/distribution/manifest/schema2/manifest.go
|
||||
const V2S2MediaTypeLayer = "application/vnd.docker.image.rootfs.diff.tar.gzip"
|
||||
|
||||
// github.com/docker/distribution/manifest/schema2/manifest.go
|
||||
const V2S2MediaTypeUncompressedLayer = "application/vnd.docker.image.rootfs.diff.tar"
|
||||
|
||||
// github.com/moby/moby/image/rootfs.go
|
||||
// RootFS describes images root filesystem
|
||||
// This is currently a placeholder that only supports layers. In the future
|
||||
// this can be made into an interface that supports different implementations.
|
||||
type V2S2RootFS struct {
|
||||
Type string `json:"type"`
|
||||
DiffIDs []digest.Digest `json:"diff_ids,omitempty"`
|
||||
}
|
||||
|
||||
// github.com/moby/moby/image/image.go
|
||||
// History stores build commands that were used to create an image
|
||||
type V2S2History struct {
|
||||
// Created is the timestamp at which the image was created
|
||||
Created time.Time `json:"created"`
|
||||
// Author is the name of the author that was specified when committing the image
|
||||
Author string `json:"author,omitempty"`
|
||||
// CreatedBy keeps the Dockerfile command used while building the image
|
||||
CreatedBy string `json:"created_by,omitempty"`
|
||||
// Comment is the commit message that was set when committing the image
|
||||
Comment string `json:"comment,omitempty"`
|
||||
// EmptyLayer is set to true if this history item did not generate a
|
||||
// layer. Otherwise, the history item is associated with the next
|
||||
// layer in the RootFS section.
|
||||
EmptyLayer bool `json:"empty_layer,omitempty"`
|
||||
}
|
||||
|
||||
// github.com/moby/moby/image/image.go
|
||||
// ID is the content-addressable ID of an image.
|
||||
type ID digest.Digest
|
||||
|
||||
// github.com/moby/moby/api/types/container/config.go
|
||||
// HealthConfig holds configuration settings for the HEALTHCHECK feature.
|
||||
type HealthConfig struct {
|
||||
// Test is the test to perform to check that the container is healthy.
|
||||
// An empty slice means to inherit the default.
|
||||
// The options are:
|
||||
// {} : inherit healthcheck
|
||||
// {"NONE"} : disable healthcheck
|
||||
// {"CMD", args...} : exec arguments directly
|
||||
// {"CMD-SHELL", command} : run command with system's default shell
|
||||
Test []string `json:",omitempty"`
|
||||
|
||||
// Zero means to inherit. Durations are expressed as integer nanoseconds.
|
||||
Interval time.Duration `json:",omitempty"` // Interval is the time to wait between checks.
|
||||
Timeout time.Duration `json:",omitempty"` // Timeout is the time to wait before considering the check to have hung.
|
||||
|
||||
// Retries is the number of consecutive failures needed to consider a container as unhealthy.
|
||||
// Zero means inherit.
|
||||
Retries int `json:",omitempty"`
|
||||
}
|
||||
|
||||
// github.com/docker/go-connections/nat/nat.go
|
||||
// PortSet is a collection of structs indexed by Port
|
||||
type PortSet map[Port]struct{}
|
||||
|
||||
// github.com/docker/go-connections/nat/nat.go
|
||||
// Port is a string containing port number and protocol in the format "80/tcp"
|
||||
type Port string
|
||||
|
||||
// github.com/moby/moby/api/types/container/config.go
|
||||
// Config contains the configuration data about a container.
|
||||
// It should hold only portable information about the container.
|
||||
// Here, "portable" means "independent from the host we are running on".
|
||||
// Non-portable information *should* appear in HostConfig.
|
||||
// All fields added to this struct must be marked `omitempty` to keep getting
|
||||
// predictable hashes from the old `v1Compatibility` configuration.
|
||||
type Config struct {
|
||||
Hostname string // Hostname
|
||||
Domainname string // Domainname
|
||||
User string // User that will run the command(s) inside the container, also support user:group
|
||||
AttachStdin bool // Attach the standard input, makes possible user interaction
|
||||
AttachStdout bool // Attach the standard output
|
||||
AttachStderr bool // Attach the standard error
|
||||
ExposedPorts PortSet `json:",omitempty"` // List of exposed ports
|
||||
Tty bool // Attach standard streams to a tty, including stdin if it is not closed.
|
||||
OpenStdin bool // Open stdin
|
||||
StdinOnce bool // If true, close stdin after the 1 attached client disconnects.
|
||||
Env []string // List of environment variable to set in the container
|
||||
Cmd strslice.StrSlice // Command to run when starting the container
|
||||
Healthcheck *HealthConfig `json:",omitempty"` // Healthcheck describes how to check the container is healthy
|
||||
ArgsEscaped bool `json:",omitempty"` // True if command is already escaped (Windows specific)
|
||||
Image string // Name of the image as it was passed by the operator (e.g. could be symbolic)
|
||||
Volumes map[string]struct{} // List of volumes (mounts) used for the container
|
||||
WorkingDir string // Current directory (PWD) in the command will be launched
|
||||
Entrypoint strslice.StrSlice // Entrypoint to run when starting the container
|
||||
NetworkDisabled bool `json:",omitempty"` // Is network disabled
|
||||
MacAddress string `json:",omitempty"` // Mac Address of the container
|
||||
OnBuild []string // ONBUILD metadata that were defined on the image Dockerfile
|
||||
Labels map[string]string // List of labels set to this container
|
||||
StopSignal string `json:",omitempty"` // Signal to stop a container
|
||||
StopTimeout *int `json:",omitempty"` // Timeout (in seconds) to stop a container
|
||||
Shell strslice.StrSlice `json:",omitempty"` // Shell for shell-form of RUN, CMD, ENTRYPOINT
|
||||
}
|
||||
|
||||
// github.com/docker/distribution/manifest/schema1/config_builder.go
|
||||
// For non-top-level layers, create fake V1Compatibility strings that
|
||||
// fit the format and don't collide with anything else, but don't
|
||||
// result in runnable images on their own.
|
||||
type V1Compatibility struct {
|
||||
ID string `json:"id"`
|
||||
Parent string `json:"parent,omitempty"`
|
||||
Comment string `json:"comment,omitempty"`
|
||||
Created time.Time `json:"created"`
|
||||
ContainerConfig struct {
|
||||
Cmd []string
|
||||
} `json:"container_config,omitempty"`
|
||||
Author string `json:"author,omitempty"`
|
||||
ThrowAway bool `json:"throwaway,omitempty"`
|
||||
}
|
||||
|
||||
// github.com/moby/moby/image/image.go
|
||||
// V1Image stores the V1 image configuration.
|
||||
type V1Image struct {
|
||||
// ID is a unique 64 character identifier of the image
|
||||
ID string `json:"id,omitempty"`
|
||||
// Parent is the ID of the parent image
|
||||
Parent string `json:"parent,omitempty"`
|
||||
// Comment is the commit message that was set when committing the image
|
||||
Comment string `json:"comment,omitempty"`
|
||||
// Created is the timestamp at which the image was created
|
||||
Created time.Time `json:"created"`
|
||||
// Container is the id of the container used to commit
|
||||
Container string `json:"container,omitempty"`
|
||||
// ContainerConfig is the configuration of the container that is committed into the image
|
||||
ContainerConfig Config `json:"container_config,omitempty"`
|
||||
// DockerVersion specifies the version of Docker that was used to build the image
|
||||
DockerVersion string `json:"docker_version,omitempty"`
|
||||
// Author is the name of the author that was specified when committing the image
|
||||
Author string `json:"author,omitempty"`
|
||||
// Config is the configuration of the container received from the client
|
||||
Config *Config `json:"config,omitempty"`
|
||||
// Architecture is the hardware that the image is build and runs on
|
||||
Architecture string `json:"architecture,omitempty"`
|
||||
// OS is the operating system used to build and run the image
|
||||
OS string `json:"os,omitempty"`
|
||||
// Size is the total size of the image including all layers it is composed of
|
||||
Size int64 `json:",omitempty"`
|
||||
}
|
||||
|
||||
// github.com/moby/moby/image/image.go
|
||||
// Image stores the image configuration
|
||||
type V2Image struct {
|
||||
V1Image
|
||||
Parent ID `json:"parent,omitempty"`
|
||||
RootFS *V2S2RootFS `json:"rootfs,omitempty"`
|
||||
History []V2S2History `json:"history,omitempty"`
|
||||
OSVersion string `json:"os.version,omitempty"`
|
||||
OSFeatures []string `json:"os.features,omitempty"`
|
||||
|
||||
// rawJSON caches the immutable JSON associated with this image.
|
||||
rawJSON []byte
|
||||
|
||||
// computedID is the ID computed from the hash of the image config.
|
||||
// Not to be confused with the legacy V1 ID in V1Image.
|
||||
computedID ID
|
||||
}
|
||||
|
||||
// github.com/docker/distribution/manifest/versioned.go
|
||||
// Versioned provides a struct with the manifest schemaVersion and mediaType.
|
||||
// Incoming content with unknown schema version can be decoded against this
|
||||
// struct to check the version.
|
||||
type V2Versioned struct {
|
||||
// SchemaVersion is the image manifest schema that this image follows
|
||||
SchemaVersion int `json:"schemaVersion"`
|
||||
|
||||
// MediaType is the media type of this schema.
|
||||
MediaType string `json:"mediaType,omitempty"`
|
||||
}
|
||||
|
||||
// github.com/docker/distribution/manifest/schema1/manifest.go
|
||||
// FSLayer is a container struct for BlobSums defined in an image manifest
|
||||
type V2S1FSLayer struct {
|
||||
// BlobSum is the tarsum of the referenced filesystem image layer
|
||||
BlobSum digest.Digest `json:"blobSum"`
|
||||
}
|
||||
|
||||
// github.com/docker/distribution/manifest/schema1/manifest.go
|
||||
// History stores unstructured v1 compatibility information
|
||||
type V2S1History struct {
|
||||
// V1Compatibility is the raw v1 compatibility information
|
||||
V1Compatibility string `json:"v1Compatibility"`
|
||||
}
|
||||
|
||||
// github.com/docker/distribution/manifest/schema1/manifest.go
|
||||
// Manifest provides the base accessible fields for working with V2 image
|
||||
// format in the registry.
|
||||
type V2S1Manifest struct {
|
||||
V2Versioned
|
||||
|
||||
// Name is the name of the image's repository
|
||||
Name string `json:"name"`
|
||||
|
||||
// Tag is the tag of the image specified by this manifest
|
||||
Tag string `json:"tag"`
|
||||
|
||||
// Architecture is the host architecture on which this image is intended to
|
||||
// run
|
||||
Architecture string `json:"architecture"`
|
||||
|
||||
// FSLayers is a list of filesystem layer blobSums contained in this image
|
||||
FSLayers []V2S1FSLayer `json:"fsLayers"`
|
||||
|
||||
// History is a list of unstructured historical data for v1 compatibility
|
||||
History []V2S1History `json:"history"`
|
||||
}
|
||||
|
||||
// github.com/docker/distribution/blobs.go
|
||||
// Descriptor describes targeted content. Used in conjunction with a blob
|
||||
// store, a descriptor can be used to fetch, store and target any kind of
|
||||
// blob. The struct also describes the wire protocol format. Fields should
|
||||
// only be added but never changed.
|
||||
type V2S2Descriptor struct {
|
||||
// MediaType describe the type of the content. All text based formats are
|
||||
// encoded as utf-8.
|
||||
MediaType string `json:"mediaType,omitempty"`
|
||||
|
||||
// Size in bytes of content.
|
||||
Size int64 `json:"size,omitempty"`
|
||||
|
||||
// Digest uniquely identifies the content. A byte stream can be verified
|
||||
// against against this digest.
|
||||
Digest digest.Digest `json:"digest,omitempty"`
|
||||
|
||||
// URLs contains the source URLs of this content.
|
||||
URLs []string `json:"urls,omitempty"`
|
||||
|
||||
// NOTE: Before adding a field here, please ensure that all
|
||||
// other options have been exhausted. Much of the type relationships
|
||||
// depend on the simplicity of this type.
|
||||
}
|
||||
|
||||
// github.com/docker/distribution/manifest/schema2/manifest.go
|
||||
// Manifest defines a schema2 manifest.
|
||||
type V2S2Manifest struct {
|
||||
V2Versioned
|
||||
|
||||
// Config references the image configuration as a blob.
|
||||
Config V2S2Descriptor `json:"config"`
|
||||
|
||||
// Layers lists descriptors for the layers referenced by the
|
||||
// configuration.
|
||||
Layers []V2S2Descriptor `json:"layers"`
|
||||
}
|
|
@ -0,0 +1,162 @@
|
|||
% skopeo-experimental-image-proxy(1)
|
||||
|
||||
# NAME
|
||||
skopeo-experimental-image-proxy - API server for fetching container images (EXPERIMENTAL)
|
||||
|
||||
# SYNOPSIS
|
||||
**skopeo experimental-image-proxy** [*options*]
|
||||
|
||||
# DESCRIPTION
|
||||
**EXPERIMENTAL COMMAND**: This command is experimental, and its API is subject to change. It is currently hidden from the main help output and not supported on Windows.
|
||||
|
||||
`skopeo experimental-image-proxy` exposes core container image fetching APIs via custom JSON+fd-passing protocol. This provides a lightweight way to fetch container image content (manifests and blobs). This command is primarily intended for programs that want to operate on a storage type that skopeo doesn't natively handle. For example, the bootc project currently has a custom ostree-based container storage backend.
|
||||
|
||||
The client process that invokes `skopeo experimental-image-proxy` is responsible for creating a socket pair and passing one of the file descriptors to the proxy. By default, the proxy expects this file descriptor to be its standard input (fd 0), but a different fd can be specified using the **--sockfd** option.
|
||||
|
||||
**Protocol Overview**
|
||||
|
||||
The protocol requires a `socketpair(2)` of type `SOCK_SEQPACKET`, over which a single JSON message is sent per packet. Large data payloads, such as image manifests and blobs, are transferred over separate pipes (`pipe(2)`), with the read-ends of these pipes passed to the client via file descriptor (FD) passing.
|
||||
|
||||
* **Request Format**: A JSON object: `{ "method": "MethodName", "args": [arguments] }`
|
||||
* **Reply Format**: A JSON object: `{ "success": boolean, "value": JSONValue, "pipeid": number, "error_code": string, "error": string }`
|
||||
* `success`: `true` if the call succeeded, `false` otherwise.
|
||||
* `value`: The return value of the method, if any.
|
||||
* `pipeid`: An integer identifying a pipe for data transfer. This ID is used with the `FinishPipe` method.
|
||||
* `error_code`: A string indicating the type of error if `success` is `false` (e.g., "EPIPE", "retryable", "other"). (Introduced in protocol version 0.2.8)
|
||||
* `error`: A string describing the error if `success` is `false`.
|
||||
|
||||
The current protocol version is `0.2.8`.
|
||||
|
||||
**Supported Protocol Methods**
|
||||
|
||||
The server supports the following methods:
|
||||
|
||||
* **Initialize**: Initializes the proxy. This method must be called before any other method.
|
||||
* Args: `[]` (empty array)
|
||||
* Returns: `string` (the protocol version, e.g., "0.2.8")
|
||||
* **OpenImage**: Opens an image reference (e.g., `docker://quay.io/example/image:latest`).
|
||||
* Args: `[string imageName]`
|
||||
* Returns: `uint64` (an opaque image ID to be used in subsequent calls)
|
||||
* **OpenImageOptional**: Similar to `OpenImage`, but if the image is not found, it returns `0` (a sentinel image ID) instead of an error.
|
||||
* Args: `[string imageName]`
|
||||
* Returns: `uint64` (opaque image ID, or `0` if the image is not found)
|
||||
* **CloseImage**: Closes a previously opened image, releasing associated resources.
|
||||
* Args: `[uint64 imageID]`
|
||||
* Returns: `null`
|
||||
* **GetManifest**: Retrieves the image manifest. If the image is a manifest list, it is resolved to an image matching the proxy's current OS and architecture. The manifest is converted to OCI format if it isn't already. The `value` field in the reply contains the original digest of the manifest (if the image is a manifest list, this is the digest of the list, not the per-platform instance). The manifest content is streamed over a pipe.
|
||||
* Args: `[uint64 imageID]`
|
||||
* Returns: `string` (manifest digest in `value`), manifest data via pipe.
|
||||
* **GetFullConfig**: Retrieves the full image configuration, conforming to the OCI Image Format Specification. Configuration data is streamed over a pipe.
|
||||
* Args: `[uint64 imageID]`
|
||||
* Returns: `null`, configuration data via pipe.
|
||||
* **GetBlob**: Fetches an image blob (e.g., a layer) by its digest and expected size. The proxy performs digest verification on the blob data. The `value` field in the reply contains the blob size. Blob data is streamed over a pipe.
|
||||
* Args: `[uint64 imageID, string digest, uint64 size]`
|
||||
* Returns: `int64` (blob size in `value`, `-1` if unknown), blob data via pipe.
|
||||
* **GetRawBlob**: Fetches an image blob by its digest. Unlike `GetBlob`, this method does not perform server-side digest verification. It returns two file descriptors to the client: one for the blob data and another for reporting errors that occur during the streaming. This method does not use the `FinishPipe` mechanism. The `value` field in the reply contains the blob size. (Introduced in protocol version 0.2.8)
|
||||
* Args: `[uint64 imageID, string digest]`
|
||||
* Returns: `int64` (blob size in `value`, `-1` if unknown), and *two* file descriptors: one for the blob data, one for errors. The error is a `ProxyError` type, see below.
|
||||
* **GetLayerInfoPiped**: Retrieves information about image layers. This replaces `GetLayerInfo`. Layer information data is streamed over a pipe, which makes it more reliable for images with many layers that would exceed message size limits with `GetLayerInfo`. The returned data is a JSON array of `{digest: string, size: int64, media_type: string}`. (Introduced in protocol version 0.2.7)
|
||||
* Args: `[uint64 imageID]`
|
||||
* Returns: `null`, layer information data via pipe.
|
||||
* **FinishPipe**: Signals that the client has finished reading all data from a pipe associated with a `pipeid` (obtained from methods like `GetManifest` or `GetBlob`). This allows the server to close its end of the pipe and report any pending errors (e.g., digest verification failure for `GetBlob`). This method **must** be called by the client after consuming data from a pipe, except for pipes from `GetRawBlob`.
|
||||
* Args: `[uint32 pipeID]`
|
||||
* Returns: `null`
|
||||
* **Shutdown**: Instructs the proxy server to terminate gracefully.
|
||||
* Args: `[]` (empty array)
|
||||
* Returns: `null`
|
||||
|
||||
The following methods are deprecated:
|
||||
|
||||
* **GetConfig**: (deprecated) Retrieves the container runtime configuration part of the image (the OCI `config` field). **Note**: This method returns only a part of the full image configuration due to a historical oversight. Use `GetFullConfig` for the complete image configuration. Configuration data is streamed over a pipe.
|
||||
* Args: `[uint64 imageID]`
|
||||
* Returns: `null`, configuration data via pipe.
|
||||
* **GetLayerInfo**: (deprecated) Retrieves an array of objects, each describing an image layer (digest, size, mediaType). **Note**: This method returns data inline and may fail for images with many layers due to message size limits. Use `GetLayerInfoPiped` for a more robust solution.
|
||||
* Args: `[uint64 imageID]`
|
||||
* Returns: `array` of `{digest: string, size: int64, media_type: string}`.
|
||||
|
||||
**Data Transfer for Pipes**
|
||||
|
||||
When a method returns a `pipeid`, the server also passes the read-end of a pipe via file descriptor (FD) passing. The client reads the data (e.g., manifest content, blob content) from this FD. After successfully reading all data, the client **must** call `FinishPipe` with the corresponding `pipeid`. This signals to the server that the transfer is complete, allows the server to clean up resources, and enables the client to check for any errors that might have occurred during the data streaming process (e.g., a digest mismatch during `GetBlob`). The `GetRawBlob` method is an exception; it uses a dedicated error pipe instead of the `FinishPipe` mechanism.
|
||||
|
||||
**ProxyError**
|
||||
|
||||
`GetBlobRaw` returns a JSON object of the following form in the error pipe where:
|
||||
```
|
||||
{
|
||||
"code": "EPIPE" | "retryable" | "other",
|
||||
"message": "error message"
|
||||
}
|
||||
```
|
||||
|
||||
- EPIPE: The client closed the pipe before reading all data.
|
||||
- retryable: The operation failed but might succeed if retried.
|
||||
- other: A generic error occurred.
|
||||
|
||||
# OPTIONS
|
||||
**--sockfd**=*fd*
|
||||
Serve on the opened socket passed as file descriptor *fd*. Defaults to 0 (standard input).
|
||||
|
||||
The command also supports common skopeo options for interacting with image registries and local storage. These include:
|
||||
|
||||
**--authfile**=*path*
|
||||
|
||||
Path of the primary registry credentials file. On Linux, the default is ${XDG\_RUNTIME\_DIR}/containers/auth.json.
|
||||
See **containers-auth.json**(5) for more details about the credential search mechanism and defaults on other platforms.
|
||||
|
||||
Use `skopeo login` to manage the credentials.
|
||||
|
||||
The default value of this option is read from the `REGISTRY\_AUTH\_FILE` environment variable.
|
||||
|
||||
**--cert-dir**=*path*
|
||||
|
||||
Use certificates at *path* (\*.crt, \*.cert, \*.key) to connect to the registry.
|
||||
|
||||
**--creds** _username[:password]_
|
||||
|
||||
Username and password for accessing the registry.
|
||||
|
||||
**--daemon-host** _host_
|
||||
|
||||
Use docker daemon host at _host_ (`docker-daemon:` transport only)
|
||||
|
||||
**--no-creds**
|
||||
|
||||
Access the registry anonymously.
|
||||
|
||||
**--password**=*password*
|
||||
|
||||
Password for accessing the registry. Use with **--username**.
|
||||
|
||||
**--registry-token**=*token*
|
||||
|
||||
Provide a Bearer *token* for accessing the registry.
|
||||
|
||||
**--shared-blob-dir** _directory_
|
||||
|
||||
Directory to use to share blobs across OCI repositories.
|
||||
|
||||
**--tls-verify**=_bool_
|
||||
|
||||
Require HTTPS and verify certificates when talking to the container registry or daemon. Default to registry.conf setting.
|
||||
|
||||
**--username**=*username*
|
||||
Username for accessing the registry. Use with **--password**.
|
||||
|
||||
# REFERENCE CLIENT LIBRARIES
|
||||
|
||||
- Rust: The [containers-image-proxy-rs project](https://github.com/containers/containers-image-proxy-rs) serves
|
||||
as the reference Rust client.
|
||||
|
||||
# PROTOCOL HISTORY
|
||||
|
||||
- 0.2.1: Initial version
|
||||
- 0.2.2: Added support for fetching image configuration as OCI
|
||||
- 0.2.3: Added GetFullConfig
|
||||
- 0.2.4: Added OpenImageOptional
|
||||
- 0.2.5: Added LayerInfoJSON
|
||||
- 0.2.6: Policy Verification before pulling OCI
|
||||
- 0.2.7: Added GetLayerInfoPiped
|
||||
- 0.2.8: Added GetRawBlob and error_code to replies
|
||||
|
||||
## SEE ALSO
|
||||
skopeo(1), containers-auth.json(5)
|
|
@ -1,18 +0,0 @@
|
|||
PREFIX := /usr/local
|
||||
DATADIR := ${PREFIX}/share
|
||||
MANDIR := $(DATADIR)/man
|
||||
GOMD2MAN = go-md2man
|
||||
|
||||
docs: $(patsubst %.md,%.1,$(wildcard *.md))
|
||||
|
||||
%.1: %.md
|
||||
$(GOMD2MAN) -in $^ -out $@
|
||||
|
||||
.PHONY: install
|
||||
install:
|
||||
install -d ${DESTDIR}/${MANDIR}/man1
|
||||
install -m 0644 buildah*.1 ${DESTDIR}/${MANDIR}/man1
|
||||
|
||||
.PHONY: clean
|
||||
clean:
|
||||
$(RM) buildah*.1
|
|
@ -1,31 +0,0 @@
|
|||
## buildah-add "1" "March 2017" "buildah"
|
||||
|
||||
## NAME
|
||||
buildah add - Add the contents of a file, URL, or a directory to a container.
|
||||
|
||||
## SYNOPSIS
|
||||
**buildah** **add** **containerID** **SRC** [[...] **DEST**]
|
||||
|
||||
## DESCRIPTION
|
||||
Adds the contents of a file, URL, or a directory to a container's working
|
||||
directory or a specified location in the container. If a local source file
|
||||
appears to be an archive, its contents are extracted and added instead of the
|
||||
archive file itself. If a local directory is specified as a source, its
|
||||
*contents* are copied to the destination.
|
||||
|
||||
## EXAMPLE
|
||||
|
||||
buildah add containerID '/myapp/app.conf' '/myapp/app.conf'
|
||||
|
||||
buildah add containerID '/home/myuser/myproject.go'
|
||||
|
||||
buildah add containerID '/home/myuser/myfiles.tar' '/tmp'
|
||||
|
||||
buildah add containerID '/tmp/workingdir' '/tmp/workingdir'
|
||||
|
||||
buildah add containerID 'https://github.com/projectatomic/buildah/blob/master/README.md' '/tmp'
|
||||
|
||||
buildah add containerID 'passwd' 'certs.d' /etc
|
||||
|
||||
## SEE ALSO
|
||||
buildah(1)
|
|
@ -1,95 +0,0 @@
|
|||
## buildah-bud "1" "April 2017" "buildah"
|
||||
|
||||
## NAME
|
||||
buildah bud - Build an image using instructions from Dockerfiles.
|
||||
|
||||
## SYNOPSIS
|
||||
**buildah** **bud | build-using-dockerfile** [*options* [...]] [**context**]
|
||||
|
||||
## DESCRIPTION
|
||||
Builds an image using instructions from one or more Dockerfiles and a specified
|
||||
build context directory. The build context directory can be specified as the
|
||||
**http** or **https** URL of an archive which will be retrieved and extracted
|
||||
to a temporary location.
|
||||
|
||||
## OPTIONS
|
||||
|
||||
**-f, --file** *Dockerfile*
|
||||
|
||||
Specifies a Dockerfile which contains instructions for building the image,
|
||||
either a local file or an **http** or **https** URL. If more than one
|
||||
Dockerfile is specified, *FROM* instructions will only be accepted from the
|
||||
first specified file.
|
||||
|
||||
If a build context is not specified, and at least one Dockerfile is a
|
||||
local file, the directory in which it resides will be used as the build
|
||||
context.
|
||||
|
||||
**--pull**
|
||||
|
||||
Pull the image if it is not present. If this flag is disabled (with
|
||||
*--pull=false*) and the image is not present, the image will not be pulled.
|
||||
Defaults to *true*.
|
||||
|
||||
**--pull-always**
|
||||
|
||||
Pull the image even if a version of the image is already present.
|
||||
|
||||
**--registry** *registry*
|
||||
|
||||
A prefix to prepend to the image name in order to pull the image. Default
|
||||
value is "docker://"
|
||||
|
||||
**--signature-policy** *signaturepolicy*
|
||||
|
||||
Pathname of a signature policy file to use. It is not recommended that this
|
||||
option be used, as the default behavior of using the system-wide default policy
|
||||
(frequently */etc/containers/policy.json*) is most often preferred.
|
||||
|
||||
**--build-arg** *arg=value*
|
||||
|
||||
Specifies a build argument and its value, which will be interpolated in
|
||||
instructions read from the Dockerfiles in the same way that environment
|
||||
variables are, but which will not be added to environment variable list in the
|
||||
resulting image's configuration.
|
||||
|
||||
**--runtime** *path*
|
||||
|
||||
The *path* to an alternate OCI-compatible runtime, which will be used to run
|
||||
commands specified by the **RUN** instruction.
|
||||
|
||||
**--runtime-flag** *flag*
|
||||
|
||||
Adds global flags for the container rutime.
|
||||
|
||||
**-t, --tag** *imageName*
|
||||
|
||||
Specifies the name which will be assigned to the resulting image if the build
|
||||
process completes successfully.
|
||||
|
||||
**--format**
|
||||
|
||||
Control the format for the built image's manifest and configuration data.
|
||||
Recognized formats include *oci* (OCI image-spec v1.0, the default) and
|
||||
*docker* (version 2, using schema format 2 for the manifest).
|
||||
|
||||
**--quiet**
|
||||
|
||||
Suppress output messages which indicate which instruction is being processed,
|
||||
and of progress when pulling images from a registry, and when writing the
|
||||
output image.
|
||||
|
||||
## EXAMPLE
|
||||
|
||||
buildah bud .
|
||||
|
||||
buildah bud -f Dockerfile.simple .
|
||||
|
||||
buildah bud -f Dockerfile.simple -f Dockerfile.notsosimple
|
||||
|
||||
buildah bud -t imageName .
|
||||
|
||||
buildah bud -t imageName -f Dockerfile.simple
|
||||
|
||||
## SEE ALSO
|
||||
buildah(1)
|
|
@ -1,47 +0,0 @@
|
|||
## buildah-commit "1" "March 2017" "buildah"
|
||||
|
||||
## NAME
|
||||
buildah commit - Create an image from a working container.
|
||||
|
||||
## SYNOPSIS
|
||||
**buildah** **commit** [*options* [...]] **containerID** [**imageName**]
|
||||
|
||||
## DESCRIPTION
|
||||
Writes a new image using the specified container's read-write layer and if it
|
||||
is based on an image, the layers of that image. If an image name is not
|
||||
specified, an ID is assigned, but no name is assigned to the image.
|
||||
|
||||
## OPTIONS
|
||||
|
||||
**--disable-compression, -D**
|
||||
|
||||
Don't compress filesystem layers when building the image.
|
||||
|
||||
**--signature-policy**
|
||||
|
||||
Pathname of a signature policy file to use. It is not recommended that this
|
||||
option be used, as the default behavior of using the system-wide default policy
|
||||
(frequently */etc/containers/policy.json*) is most often preferred.
|
||||
|
||||
**--quiet**
|
||||
|
||||
When writing the output image, suppress progress output.
|
||||
|
||||
**--format**
|
||||
|
||||
Control the format for the image manifest and configuration data. Recognized
|
||||
formats include *oci* (OCI image-spec v1.0, the default) and *docker* (version
|
||||
2, using schema format 2 for the manifest).
|
||||
|
||||
## EXAMPLE
|
||||
|
||||
buildah commit containerID
|
||||
|
||||
buildah commit containerID newImageName
|
||||
|
||||
buildah commit --disable-compression --signature-policy '/etc/containers/policy.json' containerID
|
||||
|
||||
buildah commit --disable-compression --signature-policy '/etc/containers/policy.json' containerID newImageName
|
||||
|
||||
## SEE ALSO
|
||||
buildah(1)
|
|
@ -1,91 +0,0 @@
|
|||
## buildah-config "1" "March 2017" "buildah"
|
||||
|
||||
## NAME
|
||||
buildah config - Update image configuration settings.
|
||||
|
||||
## SYNOPSIS
|
||||
**buildah** **config** [*options* [...]] **containerID**
|
||||
|
||||
## DESCRIPTION
|
||||
Updates one or more of the settings kept for a container.
|
||||
|
||||
## OPTIONS
|
||||
|
||||
**--annotation** *annotation*
|
||||
|
||||
Adds an image *annotation* (e.g. annotation=*annotation*) to the image manifest
|
||||
of any images which will be built using the specified container.
|
||||
|
||||
**--arch** *architecture*
|
||||
|
||||
Specify the target *architecture* for any images which will be built using the
|
||||
specified container. By default, if the container was based on an image, that
|
||||
image's target architecture is kept, otherwise the host's architecture is
|
||||
recorded.
|
||||
|
||||
**--author** *author*
|
||||
|
||||
Sets contact information for the *author* for any images which will be built
|
||||
using the specified container.
|
||||
|
||||
**--cmd** *command*
|
||||
|
||||
Sets the default *command* to run for containers based on any images which will
|
||||
be built using the specified container. When used in combination with an
|
||||
*entry point*, this specifies the default parameters for the *entry point*.
|
||||
|
||||
**--created-by** *created*
|
||||
|
||||
Set the description of how the read-write layer *created* (default: "manual
|
||||
edits") in any images which will be created using the specified container.
|
||||
|
||||
**--entrypoint** *entry*
|
||||
|
||||
Sets the *entry point* for containers based on any images which will be built
|
||||
using the specified container.
|
||||
|
||||
**--env** *var=value*
|
||||
|
||||
Adds a value (e.g. name=*value*) to the environment for containers based on any
|
||||
images which will be built using the specified container.
|
||||
|
||||
**--label** *label*
|
||||
|
||||
Adds an image *label* (e.g. label=*value*) to the image configuration of any
|
||||
images which will be built using the specified container.
|
||||
|
||||
**--os** *operating system*
|
||||
|
||||
Specify the target *operating system* for any images which will be built using
|
||||
the specified container. By default, if the container was based on an image,
|
||||
its OS is kept, otherwise the host's OS's name is recorded.
|
||||
|
||||
**--port** *port*
|
||||
|
||||
Specifies a *port* to expose when running containers based on any images which
|
||||
will be built using the specified container.
|
||||
|
||||
**--user** *user*
|
||||
|
||||
Specify the *user* as whom containers based on images which will be built using
|
||||
the specified container should run. The user can be specified as a user name
|
||||
or UID, optionally followed by a group name or GID, separated by a colon (':').
|
||||
If names are used, the container should include entries for those names in its
|
||||
*/etc/passwd* and */etc/group* files.
|
||||
|
||||
**--volume** *volume*
|
||||
|
||||
Specifies a location in the directory tree which should be marked as a *volume*
|
||||
in any images which will be built using the specified container.
|
||||
|
||||
**--workingdir** *directory*
|
||||
|
||||
Sets the initial working *directory* for containers based on images which will
|
||||
be built using the specified container.
|
||||
|
||||
## EXAMPLE
|
||||
|
||||
buildah config --author='Jane Austen' --workingdir='/etc/mycontainers' containerID
|
||||
|
||||
## SEE ALSO
|
||||
buildah(1)
|
|
@ -1,37 +0,0 @@
|
|||
## buildah-containers "1" "March 2017" "buildah"
|
||||
|
||||
## NAME
|
||||
buildah containers - List the working containers and their base images.
|
||||
|
||||
## SYNOPSIS
|
||||
**buildah** **containers** [*options* [...]]
|
||||
|
||||
## DESCRIPTION
|
||||
Lists containers which appear to be buildah working containers, their names and
|
||||
IDs, and the names and IDs of the images from which they were initialized.
|
||||
|
||||
## OPTIONS
|
||||
|
||||
**--noheading, -n**
|
||||
|
||||
Omit the table headings from the listing of containers.
|
||||
|
||||
**--notruncate**
|
||||
|
||||
Do not truncate IDs in output.
|
||||
|
||||
**--quiet, -q**
|
||||
|
||||
Displays only the container IDs.
|
||||
|
||||
## EXAMPLE
|
||||
|
||||
buildah containers
|
||||
|
||||
buildah containers --quiet
|
||||
|
||||
buildah containers -q --noheading --notruncate
|
||||
|
||||
## SEE ALSO
|
||||
buildah(1)
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue