diff --git a/.github/workflows/ghcr-image-build-and-publish.yml b/.github/workflows/ghcr-image-build-and-publish.yml index e0b812946a8..47356bc76fb 100644 --- a/.github/workflows/ghcr-image-build-and-publish.yml +++ b/.github/workflows/ghcr-image-build-and-publish.yml @@ -16,51 +16,56 @@ on: - '**.md' env: - # Use docker.io for Docker Hub if empty - REGISTRY: ghcr.io # github.repository as / IMAGE_NAME: ${{ github.repository }} jobs: - build: + # Source the common environment + environment: + name: " " + uses: ./.github/workflows/reusable_environment.yml - runs-on: ubuntu-24.04 + build: + name: "project checks" + timeout-minutes: ${{ fromJSON(needs.environment.outputs.LONG_TIMEOUT) }} + runs-on: ${{ needs.environment.outputs.HOST_UBUNTU_LTS }} + needs: environment permissions: contents: read packages: write steps: - - name: Checkout repository - uses: actions/checkout@v4.2.2 + - name: "Checkout" + uses: actions/checkout@v4 - - name: Set up QEMU + - name: "Set up QEMU" uses: docker/setup-qemu-action@v3 - - name: Set up Docker Buildx + - name: "Set up Docker Buildx" uses: docker/setup-buildx-action@v3 # Login against a Docker registry except on PR # https://github.com/docker/login-action - - name: Log into registry ${{ env.REGISTRY }} + - name: "Log into registry ${{ env.REGISTRY }}" if: github.event_name != 'pull_request' - uses: docker/login-action@v3.3.0 + uses: docker/login-action@v3 with: - registry: ${{ env.REGISTRY }} + registry: ${{ needs.environment.outputs.REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} # Extract metadata (tags, labels) for Docker # https://github.com/docker/metadata-action - - name: Extract Docker metadata + - name: "Extract Docker metadata" id: meta - uses: docker/metadata-action@v5.6.1 + uses: docker/metadata-action@v5 with: - images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + images: ${{ needs.environment.outputs.REGISTRY }}/${{ env.IMAGE_NAME }} # Build and push Docker image with Buildx (don't push on PR) # https://github.com/docker/build-push-action - - name: Build and push Docker image - uses: docker/build-push-action@v6.10.0 + - name: "Build and push Docker image" + uses: docker/build-push-action@v6 with: context: . platforms: linux/amd64,linux/arm64 diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 433922ed207..56cfda0f339 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -1,4 +1,5 @@ -name: lint +# "Hide" the name from the GitHub check status line, as it just clutters the display +name: " " on: push: @@ -7,72 +8,42 @@ on: - 'release/**' pull_request: -env: - GO_VERSION: 1.23.x - jobs: - go: - timeout-minutes: 5 - name: "go | ${{ matrix.goos }} | ${{ matrix.canary }}" - runs-on: "${{ matrix.os }}" - defaults: - run: - shell: bash + # Source the common environment + environment: + name: "lint" + uses: ./.github/workflows/reusable_environment.yml + + # Linting go + lint-go: + # Define the matrix we want to lint on: every supported OS, with the current go version, and additionally go canary on linux strategy: matrix: + # The GOOS-es we run golint for, with no canary (eg: the base supported GO_VERSION) + goos: [linux, freebsd, windows] + # And no canary + canary: [false] include: - - os: ubuntu-24.04 - goos: linux - - os: ubuntu-24.04 - goos: freebsd - # FIXME: this is currently failing in a non-sensical way, so, running on linux instead... - # - os: windows-2022 - - os: ubuntu-24.04 - goos: windows - - os: ubuntu-24.04 - goos: linux - # This allows the canary script to select any upcoming golang alpha/beta/RC - canary: go-canary - env: - GOOS: "${{ matrix.goos }}" - steps: - - uses: actions/checkout@v4.2.2 - with: - fetch-depth: 1 - - name: Set GO env - run: | - # If canary is specified, get the latest available golang pre-release instead of the major version - if [ "$canary" != "" ]; then - . ./hack/build-integration-canary.sh - canary::golang::latest - fi - - uses: actions/setup-go@v5 - with: - go-version: ${{ env.GO_VERSION }} - check-latest: true - cache: true - - name: golangci-lint - uses: golangci/golangci-lint-action@v6 - with: - args: --verbose - other: - timeout-minutes: 5 - name: yaml | shell | imports order - runs-on: ubuntu-24.04 - steps: - - uses: actions/checkout@v4.2.2 - with: - fetch-depth: 1 - - uses: actions/setup-go@v5 - with: - go-version: ${{ env.GO_VERSION }} - check-latest: true - cache: true - - name: yaml - run: make lint-yaml - - name: shell - run: make lint-shell - - name: go imports ordering - run: | - go install -v github.com/incu6us/goimports-reviser/v3@latest - make lint-imports + # Only run canary on linux (note: the canary script will select any upcoming golang alpha/beta/RC when the `canary` param is set to a non-empty string) + - goos: linux + canary: true + + # If we do not "collapse" the name using a bogux matrix var, it will display all matrix parameters, which we do not want + name: "lint${{ matrix.c }}" + uses: ./.github/workflows/reusable_lint_go.yml + needs: environment + with: + goos: ${{ matrix.goos }} + canary: ${{ matrix.canary }} + os: ${{ needs.environment.outputs.HOST_UBUNTU_LTS }} + goversion: ${{ needs.environment.outputs.GO_VERSION }} + timeout-minutes: ${{ fromJSON(needs.environment.outputs.SHORT_TIMEOUT) }} + + # Linting other filetypes + lint-other: + name: "lint" + uses: ./.github/workflows/reusable_lint_other.yml + needs: environment + with: + os: ${{ needs.environment.outputs.HOST_UBUNTU_LTS }} + timeout-minutes: ${{ fromJSON(needs.environment.outputs.SHORT_TIMEOUT) }} diff --git a/.github/workflows/project.yml b/.github/workflows/project.yml index ebeef72caec..ededc361ab9 100644 --- a/.github/workflows/project.yml +++ b/.github/workflows/project.yml @@ -1,4 +1,4 @@ -name: project +name: " " on: push: @@ -8,24 +8,40 @@ on: pull_request: jobs: + # Source the common environment + environment: + name: "project checks" + uses: ./.github/workflows/reusable_environment.yml + project: - name: checks - runs-on: ubuntu-24.04 - timeout-minutes: 20 + name: "project checks" + timeout-minutes: ${{ fromJSON(needs.environment.outputs.SHORT_TIMEOUT) }} + runs-on: ${{ needs.environment.outputs.HOST_UBUNTU_LTS }} + needs: environment + steps: - - uses: actions/checkout@v4.2.2 + - name: "Checkout" + uses: actions/checkout@v4 with: path: src/github.com/containerd/nerdctl + # Fetch the last 100 commits fetch-depth: 100 - - uses: actions/setup-go@v5 + + - name: "Install go" + uses: actions/setup-go@v5 with: - go-version: ${{ env.GO_VERSION }} + go-version: ${{ needs.environment.outputs.GO_VERSION }} cache-dependency-path: src/github.com/containerd/nerdctl - - uses: containerd/project-checks@v1.1.0 + + - name: "Install and run default containerd project checks" + uses: containerd/project-checks@v1.1.0 with: working-directory: src/github.com/containerd/nerdctl - repo-access-token: ${{ secrets.GITHUB_TOKEN }} - - run: ./hack/verify-no-patent.sh + + - name: "Verify no patent" + run: ./hack/verify-no-patent.sh working-directory: src/github.com/containerd/nerdctl - - run: ./hack/verify-pkg-isolation.sh + + - name: "Verify package isolation" + run: ./hack/verify-pkg-isolation.sh working-directory: src/github.com/containerd/nerdctl diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4043288037c..47a0fd1e75b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,27 +1,44 @@ # See https://github.com/containerd/nerdctl/blob/main/MAINTAINERS_GUIDE.md for how to make a release. name: Release + on: push: tags: - 'v*' - 'test-action-release-*' jobs: + # Source the common environment + environment: + name: " " + uses: ./.github/workflows/reusable_environment.yml + release: - runs-on: ubuntu-24.04 - timeout-minutes: 40 + name: "checks" + timeout-minutes: ${{ fromJSON(needs.environment.outputs.LONG_TIMEOUT) }} + runs-on: ${{ needs.environment.outputs.HOST_UBUNTU_LTS }} + needs: environment + steps: - - uses: actions/checkout@v4.2.2 - - uses: actions/setup-go@v5 + - name: "Checkout" + uses: actions/checkout@v4 + + - name: "Install go" + uses: actions/setup-go@v5 with: - go-version: 1.23.x + go-version: ${{ needs.environment.outputs.GO_VERSION }} + check-latest: true + - name: "Compile binaries" run: make artifacts + - name: "SHA256SUMS" run: | ( cd _output; sha256sum nerdctl-* ) | tee /tmp/SHA256SUMS mv /tmp/SHA256SUMS _output/SHA256SUMS + - name: "The sha256sum of the SHA256SUMS file" run: (cd _output; sha256sum SHA256SUMS) + - name: "Prepare the release note" run: | shasha=$(sha256sum _output/SHA256SUMS | awk '{print $1}') @@ -35,6 +52,7 @@ jobs: - - - Release manager: [ADD YOUR NAME HERE] (@[ADD YOUR GITHUB ID HERE]) EOF + - name: "Create release" env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/reusable_environment.yml b/.github/workflows/reusable_environment.yml new file mode 100644 index 00000000000..872d7ad89a7 --- /dev/null +++ b/.github/workflows/reusable_environment.yml @@ -0,0 +1,73 @@ +name: common_environment + +env: + GO_VERSION: 1.23.x + HOST_UBUNTU_LTS: ubuntu-24.04 + SHORT_TIMEOUT: 5 + LONG_TIMEOUT: 20 + # Will use docker.io for Docker Hub if empty + REGISTRY: ghcr.io + + # Versions for images we maintain + BUSYBOX_VERSION: 5ad83957fa74aafd061afbfb8da14ce3220659a9 + DISTRIBUTION_VERSION: v2.8.3 + CURL_VERSION: 8.11.0_4 + +on: + workflow_call: + outputs: + GO_VERSION: + description: "The major golang version we are targeting" + value: ${{ jobs.environment.outputs.output_go }} + HOST_UBUNTU_LTS: + description: "The major LTS ubuntu host runner we run our tasks on" + value: ${{ jobs.environment.outputs.output_ubuntu_lts }} + SHORT_TIMEOUT: + description: "The timeout for tasks that are supposed to run fast (lint, etc)" + value: ${{ jobs.environment.outputs.output_short_timeout }} + LONG_TIMEOUT: + description: "The timeout for tasks that are going to run up to 20 minutes (building, integration, etc)" + value: ${{ jobs.environment.outputs.output_long_timeout }} + REGISTRY: + description: "The registry where to push our images" + value: ${{ jobs.environment.outputs.output_registry }} + BUSYBOX_VERSION: + description: "Busybox version" + value: ${{ jobs.environment.outputs.output_busybox }} + DISTRIBUTION_VERSION: + description: "Distribution version" + value: ${{ jobs.environment.outputs.output_distribution }} + CURL_VERSION: + description: "Curl version" + value: ${{ jobs.environment.outputs.output_curl }} + +jobs: + environment: + name: "environ" + runs-on: ubuntu-24.04 + steps: + - id: go + run: echo "GO_VERSION=$GO_VERSION" >> $GITHUB_OUTPUT + - id: ubuntu_lts + run: echo "HOST_UBUNTU_LTS=$HOST_UBUNTU_LTS" >> $GITHUB_OUTPUT + - id: short_timeout + run: echo "SHORT_TIMEOUT=$SHORT_TIMEOUT" >> $GITHUB_OUTPUT + - id: long_timeout + run: echo "LONG_TIMEOUT=$LONG_TIMEOUT" >> $GITHUB_OUTPUT + - id: registry + run: echo "REGISTRY=$REGISTRY" >> $GITHUB_OUTPUT + - id: busybox + run: echo "BUSYBOX_VERSION=$BUSYBOX_VERSION" >> $GITHUB_OUTPUT + - id: distribution + run: echo "DISTRIBUTION_VERSION=$DISTRIBUTION_VERSION" >> $GITHUB_OUTPUT + - id: curl + run: echo "CURL_VERSION=$CURL_VERSION" >> $GITHUB_OUTPUT + outputs: + output_go: ${{ steps.go.outputs.GO_VERSION }} + output_ubuntu_lts: ${{ steps.ubuntu_lts.outputs.HOST_UBUNTU_LTS }} + output_short_timeout: ${{ steps.short_timeout.outputs.SHORT_TIMEOUT }} + output_long_timeout: ${{ steps.long_timeout.outputs.LONG_TIMEOUT }} + output_registry: ${{ steps.registry.outputs.REGISTRY }} + output_busybox: ${{ steps.registry.outputs.BUSYBOX_VERSION }} + output_distribution: ${{ steps.registry.outputs.DISTRIBUTION_VERSION }} + output_curl: ${{ steps.registry.outputs.CURL_VERSION }} diff --git a/.github/workflows/reusable_lint_go.yml b/.github/workflows/reusable_lint_go.yml new file mode 100644 index 00000000000..14d658d7d50 --- /dev/null +++ b/.github/workflows/reusable_lint_go.yml @@ -0,0 +1,69 @@ +# This defines a reusable golint job that will run `make lint-go` and `make lint-imports` +# See `inputs` for expected parameters +name: tasks_lint_go + +on: + workflow_call: + inputs: + os: + required: true + type: string + description: "the host runner we are going to use" + goos: + required: true + type: string + description: "the GOOS we want to lint for (linux/windows/freebsd)" + goversion: + required: true + type: string + description: "the golang version we want to use" + canary: + required: false + type: boolean + default: false + description: "whether we want to try and find an alpha/beta/RC version of golang instead of the default version" + timeout-minutes: + required: false + type: number + default: 100 + description: "the timeout in minutes for this task" + +jobs: + go: + name: "${{ inputs.goos }} ${{ inputs.canary && 'canary' || inputs.goversion }}" + timeout-minutes: ${{ inputs.timeout-minutes }} + runs-on: ${{ inputs.os }} + + env: + GOOS: "${{ inputs.goos }}" + GO_VERSION: "${{ inputs.goversion }}" + + steps: + - name: "Checkout" + uses: actions/checkout@v4 + + - name: "Set go version" + run: | + # If canary is specified, get the latest available golang pre-release instead of the major version + if [ "${{ inputs.canary }}" == true ]; then + . ./hack/build-integration-canary.sh + canary::golang::latest + fi + + - name: "Install go" + uses: actions/setup-go@v5 + with: + go-version: ${{ env.GO_VERSION }} + check-latest: true + + - name: "Run golangci-lint" + uses: golangci/golangci-lint-action@v6 + with: + args: --verbose + + # Go imports ordering applies to all platforms, so, only run it once, for linux / no canary + - name: "Verify imports ordering" + if: ${{ inputs.goos == 'linux' && ! inputs.canary }} + run: | + go install github.com/incu6us/goimports-reviser/v3@latest + make lint-imports diff --git a/.github/workflows/reusable_lint_other.yml b/.github/workflows/reusable_lint_other.yml new file mode 100644 index 00000000000..d63493b3b74 --- /dev/null +++ b/.github/workflows/reusable_lint_other.yml @@ -0,0 +1,33 @@ +# This defines a reusable golint job that will run `make lint-go` and `make lint-imports` +# See `inputs` for expected parameters +name: tasks_lint_go + +on: + workflow_call: + inputs: + os: + required: true + type: string + description: "the host runner we are going to use" + timeout-minutes: + required: false + type: number + default: 100 + description: "the timeout in minutes for this task" + +jobs: + yaml_shell_etc: + # TODO: we might want to add some markdown linter, and maybe a dockerfile linter in the future + name: "yaml | shell" + timeout-minutes: ${{ inputs.timeout-minutes }} + runs-on: ${{ inputs.os }} + + steps: + - name: "Checkout" + uses: actions/checkout@v4 + + - name: "Lint yaml files" + run: make lint-yaml + + - name: "Lint shellscripts" + run: make lint-shell diff --git a/.github/workflows/test-canary.yml b/.github/workflows/test-canary.yml deleted file mode 100644 index 152097cd0fc..00000000000 --- a/.github/workflows/test-canary.yml +++ /dev/null @@ -1,99 +0,0 @@ -# This pipeline purpose is solely meant to run a subset of our test suites against upcoming or unreleased dependencies versions -name: canary - -on: - push: - branches: - - main - - 'release/**' - pull_request: - paths-ignore: - - '**.md' - -env: - UBUNTU_VERSION: "24.04" - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - -jobs: - linux: - runs-on: "ubuntu-24.04" - timeout-minutes: 40 - steps: - - uses: actions/checkout@v4.2.2 - with: - fetch-depth: 1 - - name: "Prepare integration test environment" - run: | - . ./hack/build-integration-canary.sh - canary::build::integration - - name: "Remove snap loopback devices (conflicts with our loopback devices in TestRunDevice)" - run: | - sudo systemctl disable --now snapd.service snapd.socket - sudo apt-get purge -y snapd - sudo losetup -Dv - sudo losetup -lv - - name: "Register QEMU (tonistiigi/binfmt)" - run: | - # `--install all` will only install emulation for architectures that cannot be natively executed - # Since some arm64 platforms do provide native fallback execution for 32 bits, - # armv7 emulation may or may not be installed, causing variance in the result of `uname -m`. - # To avoid that, we explicitly list the architectures we do want emulation for. - docker run --privileged --rm tonistiigi/binfmt --install linux/amd64 - docker run --privileged --rm tonistiigi/binfmt --install linux/arm64 - docker run --privileged --rm tonistiigi/binfmt --install linux/arm/v7 - - name: "Run unit tests" - run: go test -v ./pkg/... - - name: "Run integration tests" - run: docker run -t --rm --privileged test-integration ./hack/test-integration.sh -test.only-flaky=false - - name: "Run integration tests (flaky)" - run: docker run -t --rm --privileged test-integration ./hack/test-integration.sh -test.only-flaky=true - - windows: - timeout-minutes: 30 - runs-on: windows-latest - defaults: - run: - shell: bash - steps: - - uses: actions/checkout@v4.2.2 - with: - fetch-depth: 1 - - name: Set GO env - run: | - # Get latest containerd - args=(curl --proto '=https' --tlsv1.2 -fsSL -H "Accept: application/vnd.github+json" -H "X-GitHub-Api-Version: 2022-11-28") - [ "${GITHUB_TOKEN:-}" == "" ] && { - >&2 printf "GITHUB_TOKEN is not set - you might face rate limitations with the Github API\n" - } || args+=(-H "Authorization: Bearer $GITHUB_TOKEN") - ctd_v="$("${args[@]}" https://api.github.com/repos/containerd/containerd/tags | jq -rc .[0].name)" - echo "CONTAINERD_VERSION=${ctd_v:1}" >> "$GITHUB_ENV" - - . ./hack/build-integration-canary.sh - canary::golang::latest - - uses: actions/setup-go@v5 - with: - go-version: ${{ env.GO_VERSION }} - cache: true - check-latest: true - - run: go install ./cmd/nerdctl - - run: go install -v gotest.tools/gotestsum@v1 - # This here is solely to get the cni install script, which has not been modified in 3+ years. - # There is little to no reason to update this to latest containerd - - uses: actions/checkout@v4.2.2 - with: - repository: containerd/containerd - ref: "v1.7.24" - path: containerd - fetch-depth: 1 - - name: "Set up CNI" - working-directory: containerd - run: GOPATH=$(go env GOPATH) script/setup/install-cni-windows - # Windows setup script can only use released versions - - name: "Set up containerd" - env: - ctrdVersion: ${{ env.CONTAINERD_VERSION }} - run: powershell hack/configure-windows-ci.ps1 - - name: "Run integration tests" - run: ./hack/test-integration.sh -test.only-flaky=false - - name: "Run integration tests (flaky)" - run: ./hack/test-integration.sh -test.only-flaky=true diff --git a/.github/workflows/test-kube.yml b/.github/workflows/test-kube.yml deleted file mode 100644 index 2bd0d00f28c..00000000000 --- a/.github/workflows/test-kube.yml +++ /dev/null @@ -1,27 +0,0 @@ -# This pipeline purpose is solely meant to run a subset of our test suites against a kubernetes cluster -name: kubernetes - -on: - push: - branches: - - main - - 'release/**' - pull_request: - paths-ignore: - - '**.md' - -jobs: - linux: - runs-on: "ubuntu-24.04" - timeout-minutes: 40 - env: - ROOTFUL: true - steps: - - uses: actions/checkout@v4.2.2 - with: - fetch-depth: 1 - - name: "Run Kubernetes integration tests" - # See https://github.com/containerd/nerdctl/blob/main/docs/testing/README.md#about-parallelization - run: | - ./hack/build-integration-kubernetes.sh - sudo ./_output/nerdctl exec nerdctl-test-control-plane bash -c -- 'export TMPDIR="$HOME"/tmp; mkdir -p "$TMPDIR"; cd /nerdctl-source; /usr/local/go/bin/go test -p 1 ./cmd/nerdctl/... -test.only-kubernetes' diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml deleted file mode 100644 index feba1ca4c26..00000000000 --- a/.github/workflows/test.yml +++ /dev/null @@ -1,421 +0,0 @@ -name: test - -on: - push: - branches: - - main - - 'release/**' - pull_request: - paths-ignore: - - '**.md' - -env: - GO_VERSION: 1.23.x - SHORT_TIMEOUT: 5 - LONG_TIMEOUT: 60 - -jobs: - # This job builds the dependency target of the test docker image for all supported architectures and cache it in GHA - build-dependencies: - timeout-minutes: 15 - name: dependencies | ${{ matrix.containerd }} | ${{ matrix.arch }} - runs-on: "${{ matrix.runner }}" - strategy: - fail-fast: false - matrix: - include: - - runner: ubuntu-24.04 - containerd: v1.6.36 - arch: amd64 - - runner: ubuntu-24.04 - containerd: v1.7.24 - arch: amd64 - - runner: ubuntu-24.04 - containerd: v2.0.0 - arch: amd64 - - runner: arm64-8core-32gb - containerd: v2.0.0 - arch: arm64 - env: - CONTAINERD_VERSION: "${{ matrix.containerd }}" - ARCH: "${{ matrix.arch }}" - steps: - - uses: actions/checkout@v4.2.2 - with: - fetch-depth: 1 - - name: "Expose GitHub Runtime variables for gha" - uses: crazy-max/ghaction-github-runtime@v3 - - name: "Build dependencies for the integration test environment image" - run: | - docker buildx create --name with-gha --use - docker buildx build \ - --output=type=docker \ - --cache-to type=gha,mode=max,scope=${ARCH}-${CONTAINERD_VERSION} \ - --cache-from type=gha,scope=${ARCH}-${CONTAINERD_VERSION} \ - --target build-dependencies --build-arg CONTAINERD_VERSION=${CONTAINERD_VERSION} . - - test-unit: - # FIXME: - # Supposed to work: https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/evaluate-expressions-in-workflows-and-actions#example-returning-a-json-data-type - # Apparently does not - # timeout-minutes: ${{ fromJSON(env.SHORT_TIMEOUT) }} - timeout-minutes: 10 - name: unit | ${{ matrix.goos }} - runs-on: "${{ matrix.os }}" - defaults: - run: - shell: bash - strategy: - matrix: - include: - - os: windows-2022 - goos: windows - - os: ubuntu-24.04 - goos: linux - steps: - - uses: actions/checkout@v4.2.2 - with: - fetch-depth: 1 - - uses: actions/setup-go@v5 - with: - go-version: ${{ env.GO_VERSION }} - check-latest: true - cache: true - - if: ${{ matrix.goos=='windows' }} - uses: actions/checkout@v4.2.2 - with: - repository: containerd/containerd - ref: v1.7.24 - path: containerd - fetch-depth: 1 - - if: ${{ matrix.goos=='windows' }} - name: "Set up CNI" - working-directory: containerd - run: GOPATH=$(go env GOPATH) script/setup/install-cni-windows - - name: "Run unit tests" - run: make test-unit - - test-integration: - needs: build-dependencies - timeout-minutes: 30 - name: rootful | ${{ matrix.containerd }} | ${{ matrix.runner }} - runs-on: "${{ matrix.runner }}" - strategy: - fail-fast: false - matrix: - # ubuntu-20.04: cgroup v1, ubuntu-22.04 and later: cgroup v2 - include: - - ubuntu: 20.04 - containerd: v1.6.36 - runner: "ubuntu-20.04" - arch: amd64 - - ubuntu: 22.04 - containerd: v1.7.24 - runner: "ubuntu-22.04" - arch: amd64 - - ubuntu: 24.04 - containerd: v2.0.0 - runner: "ubuntu-24.04" - arch: amd64 - - ubuntu: 24.04 - containerd: v2.0.0 - runner: arm64-8core-32gb - arch: arm64 - env: - CONTAINERD_VERSION: "${{ matrix.containerd }}" - ARCH: "${{ matrix.arch }}" - UBUNTU_VERSION: "${{ matrix.ubuntu }}" - steps: - - uses: actions/checkout@v4.2.2 - with: - fetch-depth: 1 - - name: "Expose GitHub Runtime variables for gha" - uses: crazy-max/ghaction-github-runtime@v3 - - name: "Prepare integration test environment" - run: | - docker buildx create --name with-gha --use - docker buildx build \ - --output=type=docker \ - --cache-from type=gha,scope=${ARCH}-${CONTAINERD_VERSION} \ - -t test-integration --target test-integration --build-arg UBUNTU_VERSION=${UBUNTU_VERSION} --build-arg CONTAINERD_VERSION=${CONTAINERD_VERSION} . - - name: "Remove snap loopback devices (conflicts with our loopback devices in TestRunDevice)" - run: | - sudo systemctl disable --now snapd.service snapd.socket - sudo apt-get purge -y snapd - sudo losetup -Dv - sudo losetup -lv - - name: "Register QEMU (tonistiigi/binfmt)" - run: | - # `--install all` will only install emulation for architectures that cannot be natively executed - # Since some arm64 platforms do provide native fallback execution for 32 bits, - # armv7 emulation may or may not be installed, causing variance in the result of `uname -m`. - # To avoid that, we explicitly list the architectures we do want emulation for. - docker run --privileged --rm tonistiigi/binfmt --install linux/amd64 - docker run --privileged --rm tonistiigi/binfmt --install linux/arm64 - docker run --privileged --rm tonistiigi/binfmt --install linux/arm/v7 - - name: "Run integration tests" - run: docker run -t --rm --privileged test-integration ./hack/test-integration.sh -test.only-flaky=false - - name: "Run integration tests (flaky)" - run: docker run -t --rm --privileged test-integration ./hack/test-integration.sh -test.only-flaky=true - - test-integration-ipv6: - needs: build-dependencies - timeout-minutes: 15 - name: ipv6 | ${{ matrix.containerd }} | ${{ matrix.ubuntu }} - runs-on: "ubuntu-${{ matrix.ubuntu }}" - strategy: - fail-fast: false - matrix: - include: - - ubuntu: 24.04 - containerd: v2.0.0 - arch: amd64 - env: - CONTAINERD_VERSION: "${{ matrix.containerd }}" - ARCH: "${{ matrix.arch }}" - UBUNTU_VERSION: "${{ matrix.ubuntu }}" - steps: - - uses: actions/checkout@v4.2.2 - with: - fetch-depth: 1 - - name: Enable ipv4 and ipv6 forwarding - run: | - sudo sysctl -w net.ipv6.conf.all.forwarding=1 - sudo sysctl -w net.ipv4.ip_forward=1 - - name: "Expose GitHub Runtime variables for gha" - uses: crazy-max/ghaction-github-runtime@v3 - - name: Enable IPv6 for Docker, and configure docker to use containerd for gha - run: | - sudo mkdir -p /etc/docker - echo '{"ipv6": true, "fixed-cidr-v6": "2001:db8:1::/64", "experimental": true, "ip6tables": true}' | sudo tee /etc/docker/daemon.json - sudo systemctl restart docker - - name: "Prepare integration test environment" - run: | - docker buildx create --name with-gha --use - docker buildx build \ - --output=type=docker \ - --cache-from type=gha,scope=${ARCH}-${CONTAINERD_VERSION} \ - -t test-integration --target test-integration --build-arg UBUNTU_VERSION=${UBUNTU_VERSION} --build-arg CONTAINERD_VERSION=${CONTAINERD_VERSION} . - - name: "Remove snap loopback devices (conflicts with our loopback devices in TestRunDevice)" - run: | - sudo systemctl disable --now snapd.service snapd.socket - sudo apt-get purge -y snapd - sudo losetup -Dv - sudo losetup -lv - - name: "Register QEMU (tonistiigi/binfmt)" - run: | - # `--install all` will only install emulation for architectures that cannot be natively executed - # Since some arm64 platforms do provide native fallback execution for 32 bits, - # armv7 emulation may or may not be installed, causing variance in the result of `uname -m`. - # To avoid that, we explicitly list the architectures we do want emulation for. - docker run --privileged --rm tonistiigi/binfmt --install linux/amd64 - docker run --privileged --rm tonistiigi/binfmt --install linux/arm64 - docker run --privileged --rm tonistiigi/binfmt --install linux/arm/v7 - - name: "Run integration tests" - # The nested IPv6 network inside docker and qemu is complex and needs a bunch of sysctl config. - # Therefore, it's hard to debug why the IPv6 tests fail in such an isolation layer. - # On the other side, using the host network is easier at configuration. - # Besides, each job is running on a different instance, which means using host network here - # is safe and has no side effects on others. - run: docker run --network host -t --rm --privileged test-integration ./hack/test-integration.sh -test.only-ipv6 - - test-integration-rootless: - needs: build-dependencies - timeout-minutes: 30 - name: "${{ matrix.target }} | ${{ matrix.containerd }} | ${{ matrix.rootlesskit }} | ${{ matrix.ubuntu }}" - runs-on: "ubuntu-${{ matrix.ubuntu }}" - strategy: - fail-fast: false - matrix: - # ubuntu-20.04: cgroup v1, ubuntu-22.04 and later: cgroup v2 - include: - - ubuntu: 20.04 - containerd: v1.6.36 - rootlesskit: v1.1.1 # Deprecated - target: rootless - arch: amd64 - - ubuntu: 22.04 - containerd: v1.7.24 - rootlesskit: v2.3.1 - target: rootless - arch: amd64 - - ubuntu: 24.04 - containerd: v2.0.0 - rootlesskit: v2.3.1 - target: rootless - arch: amd64 - - ubuntu: 24.04 - containerd: v1.7.24 - rootlesskit: v2.3.1 - target: rootless-port-slirp4netns - arch: amd64 - env: - CONTAINERD_VERSION: "${{ matrix.containerd }}" - ARCH: "${{ matrix.arch }}" - UBUNTU_VERSION: "${{ matrix.ubuntu }}" - ROOTLESSKIT_VERSION: "${{ matrix.rootlesskit }}" - TEST_TARGET: "test-integration-${{ matrix.target }}" - steps: - - name: "Set up AppArmor" - if: matrix.ubuntu == '24.04' - run: | - cat <, - include - - /usr/local/bin/rootlesskit flags=(unconfined) { - userns, - - # Site-specific additions and overrides. See local/README for details. - include if exists - } - EOT - sudo systemctl restart apparmor.service - - uses: actions/checkout@v4.2.2 - with: - fetch-depth: 1 - - name: "Register QEMU (tonistiigi/binfmt)" - run: | - # `--install all` will only install emulation for architectures that cannot be natively executed - # Since some arm64 platforms do provide native fallback execution for 32 bits, - # armv7 emulation may or may not be installed, causing variance in the result of `uname -m`. - # To avoid that, we explicitly list the architectures we do want emulation for. - docker run --privileged --rm tonistiigi/binfmt --install linux/amd64 - docker run --privileged --rm tonistiigi/binfmt --install linux/arm64 - docker run --privileged --rm tonistiigi/binfmt --install linux/arm/v7 - - name: "Expose GitHub Runtime variables for gha" - uses: crazy-max/ghaction-github-runtime@v3 - - name: "Prepare (network driver=slirp4netns, port driver=builtin)" - run: | - docker buildx create --name with-gha --use - docker buildx build \ - --output=type=docker \ - --cache-from type=gha,scope=${ARCH}-${CONTAINERD_VERSION} \ - -t ${TEST_TARGET} --target ${TEST_TARGET} --build-arg UBUNTU_VERSION=${UBUNTU_VERSION} --build-arg CONTAINERD_VERSION=${CONTAINERD_VERSION} --build-arg ROOTLESSKIT_VERSION=${ROOTLESSKIT_VERSION} . - - name: "Disable BuildKit for RootlessKit v1 (workaround for issue #622)" - run: | - # https://github.com/containerd/nerdctl/issues/622 - WORKAROUND_ISSUE_622= - if echo "${ROOTLESSKIT_VERSION}" | grep -q v1; then - WORKAROUND_ISSUE_622=1 - fi - echo "WORKAROUND_ISSUE_622=${WORKAROUND_ISSUE_622}" >> "$GITHUB_ENV" - - name: "Test (network driver=slirp4netns, port driver=builtin)" - run: docker run -t --rm --privileged -e WORKAROUND_ISSUE_622=${WORKAROUND_ISSUE_622} ${TEST_TARGET} /test-integration-rootless.sh ./hack/test-integration.sh -test.only-flaky=false - - name: "Test (network driver=slirp4netns, port driver=builtin) (flaky)" - run: docker run -t --rm --privileged -e WORKAROUND_ISSUE_622=${WORKAROUND_ISSUE_622} ${TEST_TARGET} /test-integration-rootless.sh ./hack/test-integration.sh -test.only-flaky=true - - build: - timeout-minutes: 5 - name: "build | ${{ matrix.go-version }}" - runs-on: ubuntu-24.04 - strategy: - matrix: - go-version: ["1.22.x", "1.23.x"] - steps: - - uses: actions/checkout@v4.2.2 - with: - fetch-depth: 1 - - uses: actions/setup-go@v5 - with: - go-version: ${{ matrix.go-version }} - cache: true - check-latest: true - - name: "build" - run: GO_VERSION="$(echo ${{ matrix.go-version }} | sed -e s/.x//)" make binaries - - test-integration-docker-compatibility: - timeout-minutes: 30 - name: docker - runs-on: ubuntu-24.04 - steps: - - uses: actions/checkout@v4.2.2 - with: - fetch-depth: 1 - - uses: actions/setup-go@v5 - with: - go-version: ${{ env.GO_VERSION }} - cache: true - check-latest: true - - name: "Register QEMU (tonistiigi/binfmt)" - run: | - # `--install all` will only install emulation for architectures that cannot be natively executed - # Since some arm64 platforms do provide native fallback execution for 32 bits, - # armv7 emulation may or may not be installed, causing variance in the result of `uname -m`. - # To avoid that, we explicitly list the architectures we do want emulation for. - docker run --privileged --rm tonistiigi/binfmt --install linux/amd64 - docker run --privileged --rm tonistiigi/binfmt --install linux/arm64 - docker run --privileged --rm tonistiigi/binfmt --install linux/arm/v7 - - name: "Prepare integration test environment" - run: | - sudo apt-get install -y expect - go install -v gotest.tools/gotestsum@v1 - - name: "Ensure that the integration test suite is compatible with Docker" - run: WITH_SUDO=true ./hack/test-integration.sh -test.target=docker - - name: "Ensure that the IPv6 integration test suite is compatible with Docker" - run: WITH_SUDO=true ./hack/test-integration.sh -test.target=docker -test.only-ipv6 - - name: "Ensure that the integration test suite is compatible with Docker (flaky only)" - run: WITH_SUDO=true ./hack/test-integration.sh -test.target=docker -test.only-flaky - - test-integration-windows: - timeout-minutes: 30 - name: windows - runs-on: windows-2022 - defaults: - run: - shell: bash - steps: - - uses: actions/checkout@v4.2.2 - with: - fetch-depth: 1 - - uses: actions/setup-go@v5 - with: - go-version: ${{ env.GO_VERSION }} - cache: true - check-latest: true - - run: go install ./cmd/nerdctl - - run: go install -v gotest.tools/gotestsum@v1 - - uses: actions/checkout@v4.2.2 - with: - repository: containerd/containerd - ref: v1.7.24 - path: containerd - fetch-depth: 1 - - name: "Set up CNI" - working-directory: containerd - run: GOPATH=$(go env GOPATH) script/setup/install-cni-windows - - name: "Set up containerd" - env: - ctrdVersion: 1.7.24 - run: powershell hack/configure-windows-ci.ps1 - - name: "Run integration tests" - run: ./hack/test-integration.sh -test.only-flaky=false - - name: "Run integration tests (flaky)" - run: ./hack/test-integration.sh -test.only-flaky=true - - test-integration-freebsd: - timeout-minutes: 30 - name: FreeBSD - # ubuntu-24.04 lacks the vagrant package - runs-on: ubuntu-22.04 - steps: - - uses: actions/checkout@v4.2.2 - with: - fetch-depth: 1 - - uses: actions/cache@v4 - with: - path: /root/.vagrant.d - key: vagrant-${{ matrix.box }} - - name: Set up vagrant - run: | - sudo apt-get update - sudo apt-get install -y libvirt-daemon libvirt-daemon-system vagrant vagrant-libvirt - sudo systemctl enable --now libvirtd - - name: Boot VM - run: | - ln -sf Vagrantfile.freebsd Vagrantfile - sudo vagrant up --no-tty - - name: test-unit - run: sudo vagrant up --provision-with=test-unit - - name: test-integration - run: sudo vagrant up --provision-with=test-integration diff --git a/Makefile b/Makefile index ae4e18c94f3..097699747ae 100644 --- a/Makefile +++ b/Makefile @@ -67,13 +67,17 @@ clean: find . -name \#\* -delete rm -rf $(CURDIR)/_output/* $(MAKEFILE_DIR)/vendor -lint: lint-go lint-imports lint-yaml lint-shell +lint: lint-go-all lint-imports lint-yaml lint-shell -lint-go: - cd $(MAKEFILE_DIR) && GOOS=linux golangci-lint run $(VERBOSE_FLAG_LONG) ./... && \ +lint-go-all: + cd $(MAKEFILE_DIR) && \ + GOOS=linux golangci-lint run $(VERBOSE_FLAG_LONG) ./... && \ GOOS=windows golangci-lint run $(VERBOSE_FLAG_LONG) ./... && \ GOOS=freebsd golangci-lint run $(VERBOSE_FLAG_LONG) ./... +lint-go: + cd $(MAKEFILE_DIR) && golangci-lint run $(VERBOSE_FLAG_LONG) ./... + lint-imports: cd $(MAKEFILE_DIR) && ./hack/lint-imports.sh diff --git a/cmd/nerdctl/main.go b/cmd/nerdctl/main.go index 50797e5b804..1a831a460a1 100644 --- a/cmd/nerdctl/main.go +++ b/cmd/nerdctl/main.go @@ -49,6 +49,7 @@ import ( "github.com/containerd/nerdctl/v2/pkg/errutil" "github.com/containerd/nerdctl/v2/pkg/logging" "github.com/containerd/nerdctl/v2/pkg/rootlessutil" + "github.com/containerd/nerdctl/v2/pkg/store" "github.com/containerd/nerdctl/v2/pkg/version" ) @@ -239,6 +240,16 @@ Config file ($NERDCTL_TOML): %s return fmt.Errorf("invalid cgroup-manager %q (supported values: \"systemd\", \"cgroupfs\", \"none\")", cgroupManager) } } + + // Since we store containers' stateful information on the filesystem per namespace, we need namespaces to be + // valid, safe path segments. This is enforced by store.ValidatePathComponent. + // Note that the container runtime will further enforce additional restrictions on namespace names + // (containerd treats namespaces as valid identifiers - eg: alphanumericals + dash, starting with a letter) + // See https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#path-segment-names for + // considerations about path segments identifiers. + if err = store.ValidatePathComponent(globalOptions.Namespace); err != nil { + return err + } if appNeedsRootlessParentMain(cmd, args) { // reexec /proc/self/exe with `nsenter` into RootlessKit namespaces return rootlessutil.ParentMain(globalOptions.HostGatewayIP) diff --git a/docs/dev/auth_design.md b/docs/dev/auth_design.md new file mode 100644 index 00000000000..08a9b575288 --- /dev/null +++ b/docs/dev/auth_design.md @@ -0,0 +1,357 @@ +# [TEMP TITLE] Design document for registry resolution and authentication + + IMPORTANT + +This document outlines the desired, future behavior of nerdctl. +Current behavior may diverge, or some parts may be missing. +They will be indicated in the document in sections starting with 🤓. + +## Preamble + +nerdctl supports a set of mechanisms that allows users to control behavior +with regard to registry resolution and authentication. + +Generally speaking, and like most tools in the ecosystem, nerdctl strongly encourages +the use of TLS for all communications, as plain http is widely considered insecure +and outright dangerous to use, even in the most restricted and controlled contexts. + +Nowadays, setting-up a TLS registry is very simple (thanks to letsencrypt), +and configuring nerdctl to recognize self-signed certificates is also trivial. + +Nevertheless, there are still ways to disable TLS certificate validation, or even +force nerdctl to downgrade to plain http communication in certain circumstances. + +Note that nerdctl stores and retrieve credentials using docker's credential store implementation, +allowing for some level of interoperability between the docker cli and nerdctl. + +Finally, thanks to the [hosts.toml mechanism](https://github.com/containerd/containerd/blob/main/docs/hosts.md), +nerdctl can be instructed to _resolve_ a certain _registry namespace_ to a completely different +_endpoint_, or set of _endpoints_, with fine-grain capabilities. + +The interaction between these mechanisms is complex, and if you want to go beyond the simplest +cases (eg: docker cli), you have to understand the implications. + +This document purport to extensively cover these. + +> 🤓 +> - nerdctl currently only support parts of the hosts.toml specification + +## Vocabulary + +### Registry namespace + +A registry namespace is the _host name and port_ that you use +to tag your images with. + +In the following example, the _registry namespace_ is `namespace.example:1234` + +```bash +nerdctl tag debian namespace.example:1234/my_debian +nerdctl images +``` + +If there is no specific (`hosts.toml`) configuration on your side, the _registry namespace_ +will "resolve" to the following http url: `https://namespace.example:1234/v2/` + +The http server at that address will be used when you try to push, or pull, (or login), through a series +of http requests. + +Note that omitting a _registry namespace_ from your image name _implies_ that the +_registry namespace_ is `docker.io`. + +### Registry host / endpoint + +... refers to a fully qualified http url that normally points to an actual, live http server, +able to service the [distribution protocol](https://github.com/opencontainers/distribution-spec). + +As mentioned above, you may configure a _registry namespace_ to resolve to different _registry endpoints_, +each with their own set of _allowed capabilities_ (`resolve`, `pull`, and `push`). + +What that means is that when you: +```bash +nerdctl pull namespace.example:1234/my_debian +``` + +... the http endpoint being contacted may very well be `https://somethingelse.example:5678/v2` + +### Capabilities + +A _registry capability_ refers to a specific registry operation: +- `resolve`: converting a tag (like `latest`) to a digest +- `pull`: retrieving a certain image by digest +- `push`: sending over a locally store image + +These distinct capabilities imply different levels of trust. +While it is possible to `pull` an image by digest from an untrusted source, +it is a bad idea to use that same source to `resolve` a tag to a digest, +and even worse to publish an image there. + +Granting capabilities to specific _registry endpoints_ is something you control +and decide. + +## hosts.toml and registry resolution + +In the simplest scenario, as indicated above, without any specific configuration, +the _registry namespace_ `namespace.example:1234` will resolve to the _registry endpoint_ +`https://namespace.example:1234/v2/`. + +This resolution mechanism can be controlled through the use of `hosts.toml` files. + +Said files should be stored under: +- `~/.config/containerd/certs.d/namespace.example:1234/hosts.toml` (for rootless) +- `/etc/containerd/certs.d/namespace.example:1234/hosts.toml` (for rootful) + +Note that this mechanism being based on DNS names, ability to control DNS resolution +would obviously allow circumventing this, granted the corresponding registry(-ies) would +service requests on a different hostname. + +> 🤓 Note that currently nerdctl only supports resolution for push and pull, but not login. +> This obviously means you currently cannot authenticate against an endpoint. + +### hosts.toml file with a "server" directive + +The simplest way to configure a different _registry endpoint_ is to use the `server` +section of the `hosts.toml` file: + +Effectively, `~/.config/containerd/certs.d/docker.io:443/hosts.toml` +```toml +server = "https://myserver.example:1234" +``` + +... will make all requests using _namespace_ `docker.io` talk with `myserver.example:1234`. + +Note that, in order: +- if you omit the scheme part of the url, `https` is implied +- if you specify any directive applying to the server that implies TLS communication, the scheme will be forced to `https` +- if you omit the port part of the url: + - port `443` is implied if the scheme is `https` + - port `80` is implied if the scheme is (explicitly) `http` + +Note that if you do omit the server directive in your `hosts.toml`, the default, _implied +host_ for that _namespace_ will be used instead. The _implied host_ for a _namespace_ is decided as: +- take the host (and optional port) of the namespace +- if the port is omitted in the _namespace_, default port 443 is used +- scheme `https` is used, enforcing TLS communication + +See section about the `--insecure-registry` flag and `localhost` for exceptions. + +### hosts.toml with "hosts" segments + +You can further control resolution by adding hosts segments: + +```toml +server = "https://myserver.example:1234" + +[host."http://another-endpoint.example:4567"] + capabilities = ["pull", "resolve", "push"] +``` + +In that case, nerdctl will first try all hosts segments successively with the following algorithm: +- if the host does not specify any capability, it is assumed that is has all capabilities +- if the host has a capability that matches the requested operation, try it + - if the operation is successful with that host, we are done + - if the operation was unsuccesful, continue to the next host +- if the host does not have the capability to match the requested operation, continue to the next host + +Once all configured hosts have been exhausted unsuccessfully, nerdctl will try the `server` +(explicit or implied). + +Note that hosts directives use the same heuristic as server with regard to scheme and port. + +### Non-compliant hosts + +Hosts that do implement the protocol correctly should serve under the `/v2/` root path. + +To configure a non-compliant host, you may pass along `override_path = true` as a property, +and specify the full url you expect in the host segment. + +### TLS configuration, custom headers, etc... + +Both server and hosts segments can specify custom TLS configuration, like a custom CA, +client certificates, and the ability to skip verification of TLS certificates, along +with the ability to pass additional http headers. + +TL;DR: +```toml + ca = "/etc/certs/myca.pem" + skip_verify = false + client = [["/etc/certs/client.cert", "/etc/certs/client.key"],["/etc/certs/client.pem", ""]] + [header] + x-custom = "my custom header" +``` + +Refer to the `hosts.toml` dedicated documentation for details. + +## HTTP requests + +Requests sent to a configured `server` or `host` will add a query parameter to the urls. +For example: + +```bash +http://myserver.example/v2/library/debian/manifests/latest?ns=docker.io +``` + +This allows registry servers to understand for what namespace they are serving +resources, and possibly perform additional operations. + +Obviously, nothing prevents a registry server to be used both as a default server +for a namespace, and also as an endpoint for another. + +## What happens with localhost? + +If localhost is used as a _registry namespace_ without any specific configuration, +it is by default treated as if the following had been set in its toml file: + +`~/.config/containerd/certs.d/localhost:443/hosts.toml` +```toml +server = "http://localhost:80" + +[host."https://localhost:443"] + skip_verify = true +``` + +Specifying a port (`localhost:1234`) will not change the overall behavior. +It will be equivalent to setting the following file: + +`~/.config/containerd/certs.d/localhost:1234/hosts.toml` +```toml +[host."https://localhost:1234"] + skip_verify = true +[host."http://localhost:1234"] +``` + +This behavior is historical (and subject to change by docker as well), and can be disabled +for nerdctl by passing an explicit `--insecure-registry=false`, in which case `localhost` will be treated +as any other namespace. + +All of the above solely applies when `localhost` is used as an un-configured namespace. + +> 🤓 currently, nerdctl will treat `--insecure-registry=false` the same way as if the flag was not passed. + +## What does `nerdctl --insecure-registry` do? + +This is a custom flag supported only by nerdctl (docker does not support it). + +Using it is discouraged, as its design is inconsistent with the `hosts.toml` mechanism +which should be used instead. + +The flag only applies when used against a _registry namespace_ with **no** explicit hosts.toml +configuration. +In that scenario, when `--insecure-registry=true` is specified, it will behave as if the +following hosts.toml had been configured. + +For namespace `mynamespace.example` (no port): + +`~/.config/containerd/certs.d/mynamespace.example:443/hosts.toml` +```toml +server = "http://mynamespace.example:80" +[host."https://mynamespace.example:443"] + skip_verify = true +``` + +For namespace `mynamespace.example:1234`: + +`~/.config/containerd/certs.d/mynamespace.example:1234/hosts.toml` +```toml +server = "http://mynamespace.example:1234" +[host."https://mynamespace.example:1234"] + skip_verify = true +``` + +For namespace `mynamespace.example:443`: + +`~/.config/containerd/certs.d/mynamespace.example:443/hosts.toml` +```toml +server = "http://mynamespace.example:443" +[host."https://mynamespace.example:443"] + skip_verify = true +``` + +For namespace `mynamespace.example:80`: + +`~/.config/containerd/certs.d/mynamespace.example:80/hosts.toml` +```toml +server = "http://mynamespace.example:80" +[host."https://mynamespace.example:80"] + skip_verify = true +``` + +The effect of `--insecure-registry=false` is generally a no-op, except in the case of +localhost as described above. + +Note that using `--insecure-registry=true` on a namespace that DO have an explicit `hosts.toml` +configuration is a no-op as well. + +> 🤓 currently, it seems like `insecure-registry` will be applied to endpoints as well (though login is not working). + +## Authentication + +In its simple form, `nerdctl login` will behave exactly +the same way as docker (which does not support `hosts.toml`). + +For example: +```nerdctl login namespace.example``` + +Will resolve to the implied _registry endpoint_ `https://namespace.example:443/` +and authenticate there either prompting for credentials, or, if they exist, +retrieving credentials from the docker store. + +The `--insecure-registry` flag will work in that case with the same semantics as +outlined above. + +Now, when `server` and `hosts` configuration are involved, the behavior is different. + +If there are `host` directives: + +1. and there is no `server` directive, or if the `server` directive matches the scheme, domain and port +of the requested _registry namespace_ implied server, `nerdctl login` will function as above, +but will additionally notify the user that additional endpoints exist for that namespace, +and instruct the user to log in to these endpoints additionally if they need to. +2. if on the other hand there is a `server` directive that does NOT match the namespace host, +`nerdctl login` will decline to log in, and instruct the user to use the endpoint login syntax instead + +To log in into a specific _endpoint_ for a _registry namespace_, you should use the +additional login flag `--endpoint`. + +For example: +```bash +nerdctl login namespace.example --endpoint myserver.example +``` + +Will proceed with the following steps: +- check that there is indeed a `myserver.example` endpoint configured in the hosts.toml for `namespace.example` +- if there is one, try to authenticate against `https://myserver.example:443/v2/?ns=https://namespace.example:443` + +Note that: +- implied scheme and port resolution follow the same rules outlined above, +both for the namespace and the endpoint +- the flag `--insecure-registry` is a no-op + +> 🤓 currently, nerdctl does not allow the user to login when there is an explicit hosts.toml configuration. +> Put otherwise, nerdctl only allows the user to login to raw namespaces. +> This proposal, especially the --endpoint flag will allow login to configured namespaces and endpoints. + +## Credentials storage + +As outlined, credentials are stored using docker facilities. + +This is usually stored inside the file `$DOCKER_CONFIG/config.json`, +and credentials are keyed per-namespace host (domain+port), except for +the docker hub registry which uses a fully qualified URL. + +Since docker does not support `hosts.toml` and since _endpoints_ are not +the same thing as an implied registry host for a namespace, we store +_endpoint_ credentials using a different schema. + +Docker will not recognize this schema, hence will not wrongly send these +credentials when trying to log in into a known _endpoint_ as a registry. + +The schema is: `nerdctl-experimental://namespace.example:123/?endpoint=myserver.example:456` + +As clearly shown above, this is currently experimental, and is subject to change +in the future. +There is no guarantees that credentials stored that way will be able to be retrieved +by future nerdctl versions. + +> 🤓 as outlined above, nerdctl-experimental is a new proposed behavior to support login +> with configured namespaces. \ No newline at end of file diff --git a/pkg/containerutil/containerutil.go b/pkg/containerutil/containerutil.go index 8f215ecf679..4f21fa70546 100644 --- a/pkg/containerutil/containerutil.go +++ b/pkg/containerutil/containerutil.go @@ -46,7 +46,6 @@ import ( "github.com/containerd/nerdctl/v2/pkg/ipcutil" "github.com/containerd/nerdctl/v2/pkg/labels" "github.com/containerd/nerdctl/v2/pkg/labels/k8slabels" - "github.com/containerd/nerdctl/v2/pkg/nsutil" "github.com/containerd/nerdctl/v2/pkg/portutil" "github.com/containerd/nerdctl/v2/pkg/rootlessutil" "github.com/containerd/nerdctl/v2/pkg/signalutil" @@ -529,9 +528,6 @@ func Unpause(ctx context.Context, client *containerd.Client, id string) error { // ContainerStateDirPath returns the path to the Nerdctl-managed state directory for the container with the given ID. func ContainerStateDirPath(ns, dataStore, id string) (string, error) { - if err := nsutil.ValidateNamespaceName(ns); err != nil { - return "", fmt.Errorf("invalid namespace name %q for determining state dir of container %q: %s", ns, id, err) - } return filepath.Join(dataStore, "containers", ns, id), nil } diff --git a/pkg/nsutil/nsutil.go b/pkg/nsutil/nsutil.go deleted file mode 100644 index 9cde4583c87..00000000000 --- a/pkg/nsutil/nsutil.go +++ /dev/null @@ -1,47 +0,0 @@ -/* - Copyright The containerd Authors. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -// Package nsutil provides utilities for namespaces. -package nsutil - -import ( - "fmt" - "strings" -) - -// Ensures the provided namespace name is valid. -// Namespace names cannot be path-like strings or pre-defined aliases such as "..". -// https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#path-segment-names -func ValidateNamespaceName(nsName string) error { - if nsName == "" { - return fmt.Errorf("namespace name cannot be empty") - } - - // Slash and '$' for POSIX and backslash and '%' for Windows. - pathSeparators := "/\\%$" - if strings.ContainsAny(nsName, pathSeparators) { - return fmt.Errorf("namespace name cannot contain any special characters (%q): %s", pathSeparators, nsName) - } - - specialAliases := []string{".", "..", "~"} - for _, alias := range specialAliases { - if nsName == alias { - return fmt.Errorf("namespace name cannot be special path alias %q", alias) - } - } - - return nil -} diff --git a/pkg/nsutil/nsutil_test.go b/pkg/nsutil/nsutil_test.go deleted file mode 100644 index 31d2fdffdc1..00000000000 --- a/pkg/nsutil/nsutil_test.go +++ /dev/null @@ -1,60 +0,0 @@ -/* - Copyright The containerd Authors. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -package nsutil_test - -import ( - "testing" - - "gotest.tools/v3/assert" - - "github.com/containerd/nerdctl/v2/pkg/nsutil" -) - -func TestValidateNamespaceName(t *testing.T) { - testCases := []struct { - inputs []string - errSubstr string - }{ - { - []string{"test", "test-hyphen", ".start.dot", "mid.dot", "end.dot."}, - "", - }, - { - []string{".", "..", "~"}, - "namespace name cannot be special path alias", - }, - { - []string{"$$", "a$VARiable", "a%VAR%iable", "\\.", "\\%", "\\$"}, - "namespace name cannot contain any special characters", - }, - { - []string{"/start", "mid/dle", "end/", "\\start", "mid\\dle", "end\\"}, - "namespace name cannot contain any special characters", - }, - } - - for _, tc := range testCases { - for _, input := range tc.inputs { - err := nsutil.ValidateNamespaceName(input) - if tc.errSubstr == "" { - assert.NilError(t, err) - } else { - assert.ErrorContains(t, err, tc.errSubstr) - } - } - } -} diff --git a/pkg/store/filestore.go b/pkg/store/filestore.go index ec0d98b3585..312155230fa 100644 --- a/pkg/store/filestore.go +++ b/pkg/store/filestore.go @@ -204,7 +204,7 @@ func (vs *fileStore) List(key ...string) ([]string, error) { // Unlike Get, Set and Delete, List can have zero length key for _, k := range key { - if err := validatePathComponent(k); err != nil { + if err := ValidatePathComponent(k); err != nil { return nil, err } } @@ -333,8 +333,8 @@ func (vs *fileStore) GroupSize(key ...string) (int64, error) { return size, nil } -// validatePathComponent will enforce os specific filename restrictions on a single path component -func validatePathComponent(pathComponent string) error { +// ValidatePathComponent will enforce os specific filename restrictions on a single path component +func ValidatePathComponent(pathComponent string) error { // https://en.wikipedia.org/wiki/Comparison_of_file_systems#Limits if len(pathComponent) > 255 { return errors.Join(ErrInvalidArgument, errors.New("identifiers must be stricly shorter than 256 characters")) @@ -358,7 +358,7 @@ func validateAllPathComponents(pathComponent ...string) error { } for _, key := range pathComponent { - if err := validatePathComponent(key); err != nil { + if err := ValidatePathComponent(key); err != nil { return err } } diff --git a/pkg/store/filestore_test.go b/pkg/store/filestore_test.go index 6840443b35f..58f4eebeef0 100644 --- a/pkg/store/filestore_test.go +++ b/pkg/store/filestore_test.go @@ -267,12 +267,12 @@ func TestFileStoreFilesystemRestrictions(t *testing.T) { } for _, v := range invalid { - err := validatePathComponent(v) + err := ValidatePathComponent(v) assert.ErrorIs(t, err, ErrInvalidArgument, v) } for _, v := range valid { - err := validatePathComponent(v) + err := ValidatePathComponent(v) assert.NilError(t, err, v) }