From 0895101d8e2e45ba25e8dc619561a952be1f5c2c Mon Sep 17 00:00:00 2001
From: Philippe Scorsolini
Date: Thu, 17 Aug 2023 10:27:03 +0200
Subject: [PATCH] Moving XFN as is, just renamed and added ci/scripts
Signed-off-by: Philippe Scorsolini
---
.github/renovate.json5 | 102 ++
.github/workflows/backport.yml | 34 +
.github/workflows/ci.yml | 178 ++++
.github/workflows/tag.yml | 26 +
.gitignore | 6 +-
.gitmodules | 3 +
.golangci.yml | 216 ++++
Makefile | 85 ++
build | 1 +
.../images/function-runtime-oci/Dockerfile | 28 +
cluster/images/function-runtime-oci/Makefile | 35 +
cmd/function-runtime-oci/main.go | 83 ++
cmd/function-runtime-oci/run/run.go | 145 +++
cmd/function-runtime-oci/spark/spark.go | 275 ++++++
cmd/function-runtime-oci/start/start.go | 71 ++
go.mod | 119 +++
go.sum | 761 ++++++++++++++
internal/function-runtime-oci/container.go | 128 +++
.../function-runtime-oci/container_linux.go | 185 ++++
.../container_nonlinux.go | 40 +
.../function-runtime-oci/container_nonunix.go | 30 +
.../function-runtime-oci/container_unix.go | 77 ++
internal/function-runtime-oci/doc.go | 19 +
internal/oci/certs.go | 48 +
internal/oci/doc.go | 19 +
internal/oci/layer/layer.go | 342 +++++++
internal/oci/layer/layer_nonunix.go | 31 +
internal/oci/layer/layer_test.go | 466 +++++++++
internal/oci/layer/layer_unix.go | 45 +
internal/oci/layer/layer_unix_test.go | 78 ++
internal/oci/pull.go | 250 +++++
internal/oci/pull_test.go | 402 ++++++++
internal/oci/spec/millicpu.go | 81 ++
internal/oci/spec/spec.go | 601 +++++++++++
internal/oci/spec/spec_test.go | 931 ++++++++++++++++++
internal/oci/store/overlay/store_overlay.go | 479 +++++++++
.../oci/store/overlay/store_overlay_linux.go | 59 ++
.../store/overlay/store_overlay_nonlinux.go | 37 +
.../oci/store/overlay/store_overlay_test.go | 453 +++++++++
internal/oci/store/store.go | 371 +++++++
internal/oci/store/store_test.go | 353 +++++++
.../store/uncompressed/store_uncompressed.go | 153 +++
.../uncompressed/store_uncompressed_test.go | 247 +++++
internal/proto/buf.gen.yaml | 10 +
internal/proto/generate.go | 31 +
internal/proto/v1alpha1/run_function.pb.go | 920 +++++++++++++++++
internal/proto/v1alpha1/run_function.proto | 129 +++
.../proto/v1alpha1/run_function_grpc.pb.go | 129 +++
internal/version/fake/mocks.go | 63 ++
internal/version/version.go | 67 ++
internal/version/version_test.go | 96 ++
51 files changed, 9537 insertions(+), 1 deletion(-)
create mode 100644 .github/renovate.json5
create mode 100644 .github/workflows/backport.yml
create mode 100644 .github/workflows/ci.yml
create mode 100644 .github/workflows/tag.yml
create mode 100644 .gitmodules
create mode 100644 .golangci.yml
create mode 100644 Makefile
create mode 160000 build
create mode 100644 cluster/images/function-runtime-oci/Dockerfile
create mode 100755 cluster/images/function-runtime-oci/Makefile
create mode 100644 cmd/function-runtime-oci/main.go
create mode 100644 cmd/function-runtime-oci/run/run.go
create mode 100644 cmd/function-runtime-oci/spark/spark.go
create mode 100644 cmd/function-runtime-oci/start/start.go
create mode 100644 go.mod
create mode 100644 go.sum
create mode 100644 internal/function-runtime-oci/container.go
create mode 100644 internal/function-runtime-oci/container_linux.go
create mode 100644 internal/function-runtime-oci/container_nonlinux.go
create mode 100644 internal/function-runtime-oci/container_nonunix.go
create mode 100644 internal/function-runtime-oci/container_unix.go
create mode 100644 internal/function-runtime-oci/doc.go
create mode 100644 internal/oci/certs.go
create mode 100644 internal/oci/doc.go
create mode 100644 internal/oci/layer/layer.go
create mode 100644 internal/oci/layer/layer_nonunix.go
create mode 100644 internal/oci/layer/layer_test.go
create mode 100644 internal/oci/layer/layer_unix.go
create mode 100644 internal/oci/layer/layer_unix_test.go
create mode 100644 internal/oci/pull.go
create mode 100644 internal/oci/pull_test.go
create mode 100644 internal/oci/spec/millicpu.go
create mode 100644 internal/oci/spec/spec.go
create mode 100644 internal/oci/spec/spec_test.go
create mode 100644 internal/oci/store/overlay/store_overlay.go
create mode 100644 internal/oci/store/overlay/store_overlay_linux.go
create mode 100644 internal/oci/store/overlay/store_overlay_nonlinux.go
create mode 100644 internal/oci/store/overlay/store_overlay_test.go
create mode 100644 internal/oci/store/store.go
create mode 100644 internal/oci/store/store_test.go
create mode 100644 internal/oci/store/uncompressed/store_uncompressed.go
create mode 100644 internal/oci/store/uncompressed/store_uncompressed_test.go
create mode 100644 internal/proto/buf.gen.yaml
create mode 100644 internal/proto/generate.go
create mode 100644 internal/proto/v1alpha1/run_function.pb.go
create mode 100644 internal/proto/v1alpha1/run_function.proto
create mode 100644 internal/proto/v1alpha1/run_function_grpc.pb.go
create mode 100644 internal/version/fake/mocks.go
create mode 100644 internal/version/version.go
create mode 100644 internal/version/version_test.go
diff --git a/.github/renovate.json5 b/.github/renovate.json5
new file mode 100644
index 0000000..7bf8e14
--- /dev/null
+++ b/.github/renovate.json5
@@ -0,0 +1,102 @@
+{
+ "$schema": "https://docs.renovatebot.com/renovate-schema.json",
+ "extends": [
+ "config:base",
+ "helpers:pinGitHubActionDigests"
+ ],
+// We only want renovate to rebase PRs when they have conflicts,
+// default "auto" mode is not required.
+ "rebaseWhen": "conflicted",
+// The maximum number of PRs to be created in parallel
+ "prConcurrentLimit": 5,
+// The branches renovate should target
+// PLEASE UPDATE THIS WHEN RELEASING.
+ "baseBranches": ["main"],
+ "ignorePaths": ["design/**"],
+ "postUpdateOptions": ["gomodTidy"],
+// By default renovate will auto detect whether semantic commits have been used
+// in the recent history and comply with that, we explicitly disable it
+ "semanticCommits": "disabled",
+// All PRs should have a label
+ "labels": ["automated"],
+ "regexManagers": [
+ {
+ "description": "Bump Go version used in workflows",
+ "fileMatch": ["^\\.github\\/workflows\\/[^/]+\\.ya?ml$"],
+ "matchStrings": [
+ "GO_VERSION: '(?.*?)'\\n"
+ ],
+ "datasourceTemplate": "golang-version",
+ "depNameTemplate": "golang"
+ }, {
+ "description": "Bump golangci-lint version in workflows and the Makefile",
+ "fileMatch": ["^\\.github\\/workflows\\/[^/]+\\.ya?ml$","^Makefile$"],
+ "matchStrings": [
+ "GOLANGCI_VERSION: 'v(?.*?)'\\n",
+ "GOLANGCILINT_VERSION = (?.*?)\\n"
+ ],
+ "datasourceTemplate": "github-tags",
+ "depNameTemplate": "golangci/golangci-lint",
+ "extractVersionTemplate": "^v(?.*)$"
+ }, {
+ "description": "Bump helm version in the Makefile",
+ "fileMatch": ["^Makefile$"],
+ "matchStrings": [
+ "HELM3_VERSION = (?.*?)\\n"
+ ],
+ "datasourceTemplate": "github-tags",
+ "depNameTemplate": "helm/helm",
+ }, {
+ "description": "Bump kind version in the Makefile",
+ "fileMatch": ["^Makefile$"],
+ "matchStrings": [
+ "KIND_VERSION = (?.*?)\\n"
+ ],
+ "datasourceTemplate": "github-tags",
+ "depNameTemplate": "kubernetes-sigs/kind",
+ }
+ ],
+// PackageRules disabled below should be enabled in case of vulnerabilities
+ "vulnerabilityAlerts": {
+ "enabled": true
+ },
+ "osvVulnerabilityAlerts": true,
+// Renovate evaluates all packageRules in order, so low priority rules should
+// be at the beginning, high priority at the end
+ "packageRules": [
+ {
+ "description": "Only get Docker image updates every 2 weeks to reduce noise",
+ "matchDatasources": ["docker"],
+ "schedule": ["every 2 week on monday"],
+ enabled: true,
+ }, {
+ "description": "Ignore k8s.io/client-go older versions, they switched to semantic version and old tags are still available in the repo",
+ "matchDatasources": [
+ "go"
+ ],
+ "matchDepNames": [
+ "k8s.io/client-go"
+ ],
+ "allowedVersions": "<1.0",
+ }, {
+ "description": "Ignore k8s dependencies, should be updated on crossplane-runtime",
+ "matchDatasources": [
+ "go"
+ ],
+ "matchPackagePrefixes": [
+ "k8s.io",
+ "sigs.k8s.io"
+ ],
+ "enabled": false,
+ },{
+ "description": "Only get dependency digest updates every month to reduce noise",
+ "matchDatasources": [
+ "go"
+ ],
+ "matchUpdateTypes": [
+ "digest",
+ ],
+ "extends": ["schedule:monthly"],
+ }
+ ]
+}
diff --git a/.github/workflows/backport.yml b/.github/workflows/backport.yml
new file mode 100644
index 0000000..2738a0b
--- /dev/null
+++ b/.github/workflows/backport.yml
@@ -0,0 +1,34 @@
+name: Backport
+
+on:
+ # NOTE(negz): This is a risky target, but we run this action only when and if
+ # a PR is closed, then filter down to specifically merged PRs. We also don't
+ # invoke any scripts, etc from within the repo. I believe the fact that we'll
+ # be able to review PRs before this runs makes this fairly safe.
+ # https://securitylab.github.com/research/github-actions-preventing-pwn-requests/
+ pull_request_target:
+ types: [closed]
+ # See also commands.yml for the /backport triggered variant of this workflow.
+
+jobs:
+ # NOTE(negz): I tested many backport GitHub actions before landing on this
+ # one. Many do not support merge commits, or do not support pull requests with
+ # more than one commit. This one does. It also handily links backport PRs with
+ # new PRs, and provides commentary and instructions when it can't backport.
+ # The main gotchas with this action are that it _only_ supports merge commits,
+ # and that PRs _must_ be labelled before they're merged to trigger a backport.
+ open-pr:
+ runs-on: ubuntu-20.04
+ if: github.event.pull_request.merged
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v3
+ with:
+ fetch-depth: 0
+
+ - name: Open Backport PR
+ uses: zeebe-io/backport-action@v0.0.4
+ with:
+ github_token: ${{ secrets.GITHUB_TOKEN }}
+ github_workspace: ${{ github.workspace }}
+ version: v0.0.4
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..724157d
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,178 @@
+name: CI
+
+on:
+ push:
+ branches:
+ - main
+ - release-*
+ pull_request: {}
+ workflow_dispatch: {}
+
+env:
+ # Common versions
+ GO_VERSION: '1.20.6'
+ GOLANGCI_VERSION: 'v1.53.3'
+ DOCKER_BUILDX_VERSION: 'v0.10.0'
+
+ UPBOUND_MARKETPLACE_PUSH_ROBOT_USR: ${{ secrets.UPBOUND_MARKETPLACE_PUSH_ROBOT_USR }}
+
+jobs:
+ detect-noop:
+ runs-on: ubuntu-20.04
+ outputs:
+ noop: ${{ steps.noop.outputs.should_skip }}
+ steps:
+ - name: Detect No-op Changes
+ id: noop
+ uses: fkirc/skip-duplicate-actions@v2.1.0
+ with:
+ github_token: ${{ secrets.GITHUB_TOKEN }}
+ paths_ignore: '["**.md", "**.png", "**.jpg"]'
+ do_not_skip: '["workflow_dispatch", "schedule", "push"]'
+ concurrent_skipping: false
+
+ lint:
+ runs-on: ubuntu-20.04
+ needs: detect-noop
+ if: needs.detect-noop.outputs.noop != 'true'
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v3
+ with:
+ submodules: true
+
+ - name: Setup Go
+ uses: actions/setup-go@v4
+ with:
+ go-version: ${{ env.GO_VERSION }}
+
+ # We could run 'make lint' to ensure our desired Go version, but we prefer
+ # this action because it leaves 'annotations' (i.e. it comments on PRs to
+ # point out linter violations).
+ - name: Lint
+ uses: golangci/golangci-lint-action@v3
+ with:
+ version: ${{ env.GOLANGCI_VERSION }}
+ skip-go-installation: true
+ args: --timeout 3m
+
+ check-diff:
+ runs-on: ubuntu-20.04
+ needs: detect-noop
+ if: needs.detect-noop.outputs.noop != 'true'
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v3
+ with:
+ submodules: true
+
+ - name: Setup Go
+ uses: actions/setup-go@v4
+ with:
+ go-version: ${{ env.GO_VERSION }}
+
+ - name: Check Diff
+ run: make check-diff
+
+ build:
+ runs-on: ubuntu-20.04
+ needs: detect-noop
+ if: needs.detect-noop.outputs.noop != 'true'
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v3
+ with:
+ submodules: true
+
+ - name: Fetch History
+ run: git fetch --prune --unshallow
+
+ - name: Setup Go
+ uses: actions/setup-go@v4
+ with:
+ go-version: ${{ env.GO_VERSION }}
+
+ - name: Build Go Artifacts
+ run: |
+ make build.code.platform PLATFORM=linux_amd64
+ make build.code.platform PLATFORM=linux_arm64
+
+ unit-tests:
+ runs-on: ubuntu-20.04
+ needs: detect-noop
+ if: needs.detect-noop.outputs.noop != 'true'
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v3
+ with:
+ submodules: true
+
+ - name: Fetch History
+ run: git fetch --prune --unshallow
+
+ - name: Setup Go
+ uses: actions/setup-go@v4
+ with:
+ go-version: ${{ env.GO_VERSION }}
+
+ - name: Run Unit Tests
+ run: make test
+
+ - name: Publish Unit Test Coverage
+ uses: codecov/codecov-action@v1
+ with:
+ flags: unittests
+ file: _output/tests/linux_amd64/coverage.txt
+
+ publish-artifacts:
+ runs-on: ubuntu-20.04
+ needs: detect-noop
+ if: needs.detect-noop.outputs.noop != 'true'
+
+ steps:
+ - name: Setup QEMU
+ uses: docker/setup-qemu-action@v1
+ with:
+ platforms: all
+
+ - name: Setup Docker Buildx
+ uses: docker/setup-buildx-action@v1
+ with:
+ version: ${{ env.DOCKER_BUILDX_VERSION }}
+ install: true
+
+ - name: Checkout
+ uses: actions/checkout@v3
+ with:
+ submodules: true
+
+ - name: Fetch History
+ run: git fetch --prune --unshallow
+
+ - name: Setup Go
+ uses: actions/setup-go@v4
+ with:
+ go-version: ${{ env.GO_VERSION }}
+
+ - name: Build Artifacts
+ run: make -j2 build.all
+ env:
+ # We're using docker buildx, which doesn't actually load the images it
+ # builds by default. Specifying --load does so.
+ BUILD_ARGS: "--load"
+
+ - name: Login to Upbound
+ uses: docker/login-action@v2
+ if: env.UPBOUND_MARKETPLACE_PUSH_ROBOT_USR != ''
+ with:
+ registry: xpkg.upbound.io
+ username: ${{ secrets.UPBOUND_MARKETPLACE_PUSH_ROBOT_USR }}
+ password: ${{ secrets.UPBOUND_MARKETPLACE_PUSH_ROBOT_PSW }}
+
+ - name: Publish Artifacts
+ if: env.UPBOUND_MARKETPLACE_PUSH_ROBOT_USR != ''
+ run: make publish BRANCH_NAME=${GITHUB_REF##*/}
diff --git a/.github/workflows/tag.yml b/.github/workflows/tag.yml
new file mode 100644
index 0000000..d1d5939
--- /dev/null
+++ b/.github/workflows/tag.yml
@@ -0,0 +1,26 @@
+name: Tag
+
+on:
+ workflow_dispatch:
+ inputs:
+ version:
+ description: 'Release version (e.g. v0.1.0)'
+ required: true
+ message:
+ description: 'Tag message'
+ required: true
+
+jobs:
+ create-tag:
+ runs-on: ubuntu-20.04
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v3
+
+ - name: Create Tag
+ uses: negz/create-tag@v1
+ with:
+ version: ${{ github.event.inputs.version }}
+ message: ${{ github.event.inputs.message }}
+ token: ${{ secrets.GITHUB_TOKEN }}
diff --git a/.gitignore b/.gitignore
index 3b735ec..898908b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -15,7 +15,11 @@
*.out
# Dependency directories (remove the comment below to include it)
-# vendor/
+vendor/
# Go workspace file
go.work
+
+.cache/
+.idea/
+_output/
diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 0000000..c2fad47
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,3 @@
+[submodule "build"]
+ path = build
+ url = https://github.com/upbound/build
diff --git a/.golangci.yml b/.golangci.yml
new file mode 100644
index 0000000..127da1b
--- /dev/null
+++ b/.golangci.yml
@@ -0,0 +1,216 @@
+run:
+ timeout: 10m
+
+ skip-files:
+ - "zz_generated\\..+\\.go$"
+
+output:
+ # colored-line-number|line-number|json|tab|checkstyle|code-climate, default is "colored-line-number"
+ format: colored-line-number
+
+linters-settings:
+ errcheck:
+ # report about not checking of errors in type assetions: `a := b.(MyStruct)`;
+ # default is false: such cases aren't reported by default.
+ check-type-assertions: false
+
+ # report about assignment of errors to blank identifier: `num, _ := strconv.Atoi(numStr)`;
+ # default is false: such cases aren't reported by default.
+ check-blank: false
+
+ # [deprecated] comma-separated list of pairs of the form pkg:regex
+ # the regex is used to ignore names within pkg. (default "fmt:.*").
+ # see https://github.com/kisielk/errcheck#the-deprecated-method for details
+ ignore: fmt:.*,io/ioutil:^Read.*
+
+ govet:
+ # report about shadowed variables
+ check-shadowing: false
+
+ gofmt:
+ # simplify code: gofmt with `-s` option, true by default
+ simplify: true
+
+ gci:
+ custom-order: true
+ sections:
+ - standard
+ - default
+ - prefix(github.com/crossplane/crossplane-runtime)
+ - prefix(github.com/crossplane/crossplane)
+ - prefix(github.com/crossplane/function-runtime-oci)
+ - blank
+ - dot
+
+ gocyclo:
+ # minimal code complexity to report, 30 by default (but we recommend 10-20)
+ min-complexity: 10
+
+ maligned:
+ # print struct with more effective memory layout or not, false by default
+ suggest-new: true
+
+ dupl:
+ # tokens count to trigger issue, 150 by default
+ threshold: 100
+
+ goconst:
+ # minimal length of string constant, 3 by default
+ min-len: 3
+ # minimal occurrences count to trigger, 3 by default
+ min-occurrences: 5
+
+ lll:
+ # tab width in spaces. Default to 1.
+ tab-width: 1
+
+ unused:
+ # treat code as a program (not a library) and report unused exported identifiers; default is false.
+ # XXX: if you enable this setting, unused will report a lot of false-positives in text editors:
+ # if it's called for subdir of a project it can't find funcs usages. All text editor integrations
+ # with golangci-lint call it on a directory with the changed file.
+ check-exported: false
+
+ unparam:
+ # Inspect exported functions, default is false. Set to true if no external program/library imports your code.
+ # XXX: if you enable this setting, unparam will report a lot of false-positives in text editors:
+ # if it's called for subdir of a project it can't find external interfaces. All text editor integrations
+ # with golangci-lint call it on a directory with the changed file.
+ check-exported: false
+
+ nakedret:
+ # make an issue if func has more lines of code than this setting and it has naked returns; default is 30
+ max-func-lines: 30
+
+ prealloc:
+ # XXX: we don't recommend using this linter before doing performance profiling.
+ # For most programs usage of prealloc will be a premature optimization.
+
+ # Report preallocation suggestions only on simple loops that have no returns/breaks/continues/gotos in them.
+ # True by default.
+ simple: true
+ range-loops: true # Report preallocation suggestions on range loops, true by default
+ for-loops: false # Report preallocation suggestions on for loops, false by default
+
+ gocritic:
+ # Enable multiple checks by tags, run `GL_DEBUG=gocritic golangci-lint` run to see all tags and checks.
+ # Empty list by default. See https://github.com/go-critic/go-critic#usage -> section "Tags".
+ enabled-tags:
+ - performance
+
+ settings: # settings passed to gocritic
+ captLocal: # must be valid enabled check name
+ paramsOnly: true
+ rangeValCopy:
+ sizeThreshold: 32
+
+ nolintlint:
+ require-explanation: true
+ require-specific: true
+
+
+linters:
+ enable:
+ - megacheck
+ - govet
+ - gocyclo
+ - gocritic
+ - goconst
+ - gci
+ - gofmt # We enable this as well as goimports for its simplify mode.
+ - prealloc
+ - revive
+ - unconvert
+ - misspell
+ - nakedret
+ - nolintlint
+
+ disable:
+ # These linters are all deprecated as of golangci-lint v1.49.0. We disable
+ # them explicitly to avoid the linter logging deprecation warnings.
+ - deadcode
+ - varcheck
+ - scopelint
+ - structcheck
+ - interfacer
+
+ presets:
+ - bugs
+ - unused
+ fast: false
+
+
+issues:
+ # Excluding configuration per-path and per-linter
+ exclude-rules:
+ # Exclude some linters from running on tests files.
+ - path: _test(ing)?\.go
+ linters:
+ - gocyclo
+ - errcheck
+ - dupl
+ - gosec
+ - scopelint
+ - unparam
+
+ # Ease some gocritic warnings on test files.
+ - path: _test\.go
+ text: "(unnamedResult|exitAfterDefer)"
+ linters:
+ - gocritic
+
+ # These are performance optimisations rather than style issues per se.
+ # They warn when function arguments or range values copy a lot of memory
+ # rather than using a pointer.
+ - text: "(hugeParam|rangeValCopy):"
+ linters:
+ - gocritic
+
+ # This "TestMain should call os.Exit to set exit code" warning is not clever
+ # enough to notice that we call a helper method that calls os.Exit.
+ - text: "SA3000:"
+ linters:
+ - staticcheck
+
+ - text: "k8s.io/api/core/v1"
+ linters:
+ - goimports
+
+ # This is a "potential hardcoded credentials" warning. It's triggered by
+ # any variable with 'secret' in the same, and thus hits a lot of false
+ # positives in Kubernetes land where a Secret is an object type.
+ - text: "G101:"
+ linters:
+ - gosec
+ - gas
+
+ # This is an 'errors unhandled' warning that duplicates errcheck.
+ - text: "G104:"
+ linters:
+ - gosec
+ - gas
+
+ # Some k8s dependencies do not have JSON tags on all fields in structs.
+ - path: k8s.io/
+ linters:
+ - musttag
+
+ # Independently from option `exclude` we use default exclude patterns,
+ # it can be disabled by this option. To list all
+ # excluded by default patterns execute `golangci-lint run --help`.
+ # Default value for this option is true.
+ exclude-use-default: false
+
+ # Show only new issues: if there are unstaged changes or untracked files,
+ # only those changes are analyzed, else only changes in HEAD~ are analyzed.
+ # It's a super-useful option for integration of golangci-lint into existing
+ # large codebase. It's not practical to fix all existing issues at the moment
+ # of integration: much better don't allow issues in new code.
+ # Default is false.
+ new: false
+
+ # Maximum issues count per one linter. Set to 0 to disable. Default is 50.
+ max-per-linter: 0
+
+ # Maximum count of issues with the same text. Set to 0 to disable. Default is 3.
+ max-same-issues: 0
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..440304d
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,85 @@
+# ====================================================================================
+# Setup Project
+
+PROJECT_NAME := function-runtime-oci
+PROJECT_REPO := github.com/crossplane/$(PROJECT_NAME)
+
+PLATFORMS ?= linux_amd64 linux_arm64 linux_arm linux_ppc64le darwin_amd64 darwin_arm64 windows_amd64
+# -include will silently skip missing files, which allows us
+# to load those files with a target in the Makefile. If only
+# "include" was used, the make command would fail and refuse
+# to run a target until the include commands succeeded.
+-include build/makelib/common.mk
+
+# ====================================================================================
+# Setup Go
+
+# Set a sane default so that the nprocs calculation below is less noisy on the initial
+# loading of this file
+NPROCS ?= 1
+
+# each of our test suites starts a kube-apiserver and running many test suites in
+# parallel can lead to high CPU utilization. by default we reduce the parallelism
+# to half the number of CPU cores.
+GO_TEST_PARALLEL := $(shell echo $$(( $(NPROCS) / 2 )))
+
+GO_STATIC_PACKAGES = $(GO_PROJECT)/cmd/function-runtime-oci
+# GO_TEST_PACKAGES = $(GO_PROJECT)/test
+GO_LDFLAGS += -X $(GO_PROJECT)/internal/version.version=$(VERSION)
+GO_SUBDIRS += cmd internal
+GO111MODULE = on
+GOLANGCILINT_VERSION = 1.53.3
+-include build/makelib/golang.mk
+
+# ====================================================================================
+# Setup Images
+# Due to the way that the shared build logic works, images should
+# all be in folders at the same level (no additional levels of nesting).
+
+REGISTRY_ORGS = docker.io/crossplane xpkg.upbound.io/crossplane
+IMAGES = function-runtime-oci
+-include build/makelib/imagelight.mk
+
+# ====================================================================================
+# Targets
+
+# run `make help` to see the targets and options
+
+# We want submodules to be set up the first time `make` is run.
+# We manage the build/ folder and its Makefiles as a submodule.
+# The first time `make` is run, the includes of build/*.mk files will
+# all fail, and this target will be run. The next time, the default as defined
+# by the includes will be run instead.
+fallthrough: submodules
+ @echo Initial setup complete. Running make again . . .
+ @make
+
+# Generate a coverage report for cobertura applying exclusions on
+# - generated file
+cobertura:
+ @cat $(GO_TEST_OUTPUT)/coverage.txt | \
+ grep -v zz_generated.deepcopy | \
+ $(GOCOVER_COBERTURA) > $(GO_TEST_OUTPUT)/cobertura-coverage.xml
+
+# Update the submodules, such as the common build scripts.
+submodules:
+ @git submodule sync
+ @git submodule update --init --recursive
+
+# NOTE(hasheddan): the build submodule currently overrides XDG_CACHE_HOME in
+# order to force the Helm 3 to use the .work/helm directory. This causes Go on
+# Linux machines to use that directory as the build cache as well. We should
+# adjust this behavior in the build submodule because it is also causing Linux
+# users to duplicate their build cache, but for now we just make it easier to
+# identify its location in CI so that we cache between builds.
+go.cachedir:
+ @go env GOCACHE
+
+# This is for running out-of-cluster locally, and is for convenience. Running
+# this make target will print out the command which was used. For more control,
+# try running the binary directly with different arguments.
+run: go.build
+ @$(INFO) running crossplane locally
+ $(GO_OUT_DIR) start --debug
+
+.PHONY: cobertura submodules fallthrough run
diff --git a/build b/build
new file mode 160000
index 0000000..bd5297b
--- /dev/null
+++ b/build
@@ -0,0 +1 @@
+Subproject commit bd5297bd16c113cbc5ed1905b1d96aa1cb3078ec
diff --git a/cluster/images/function-runtime-oci/Dockerfile b/cluster/images/function-runtime-oci/Dockerfile
new file mode 100644
index 0000000..4064c3e
--- /dev/null
+++ b/cluster/images/function-runtime-oci/Dockerfile
@@ -0,0 +1,28 @@
+# This is debian:bookworm-slim (i.e. Debian 12, testing)
+FROM debian:bookworm-slim@sha256:9bd077d2f77c754f4f7f5ee9e6ded9ff1dff92c6dce877754da21b917c122c77
+
+ARG TARGETOS
+ARG TARGETARCH
+
+# TODO(negz): Find a better way to get an OCI runtime? Ideally we'd grab a
+# static build of crun (or runc) that we could drop into a distroless image. We
+# slightly prefer crun for its nascent WASM and KVM capabilities, but they only
+# offer static builds for amd64 and arm64 and building our own takes a long
+# time.
+RUN apt-get update && apt-get install -y ca-certificates crun && rm -rf /var/lib/apt/lists/*
+
+COPY bin/${TARGETOS}\_${TARGETARCH}/function-runtime-oci /usr/local/bin/
+
+# We run xfn as root in order to grant it CAP_SETUID and CAP_SETGID, which are
+# required in order to create a user namespace with more than one available UID
+# and GID. xfn invokes all of the logic that actually fetches, caches, and runs
+# a container as an unprivileged user (relative to the root/initial user
+# namespace - the user is privileged inside the user namespace xfn creates).
+#
+# It's possible to run xfn without any root privileges at all - uncomment the
+# following line to do so. Note that in this mode xfn will only be able to
+# create containers with a single UID and GID (0), so Containerized Functions
+# that don't run as root may not work.
+# USER 65532
+
+ENTRYPOINT ["function-runtime-oci"]
diff --git a/cluster/images/function-runtime-oci/Makefile b/cluster/images/function-runtime-oci/Makefile
new file mode 100755
index 0000000..ae125eb
--- /dev/null
+++ b/cluster/images/function-runtime-oci/Makefile
@@ -0,0 +1,35 @@
+# ====================================================================================
+# Setup Project
+
+include ../../../build/makelib/common.mk
+
+# ====================================================================================
+# Options
+
+include ../../../build/makelib/imagelight.mk
+
+# ====================================================================================
+# Targets
+
+img.build:
+ @$(INFO) docker build $(IMAGE)
+ @$(MAKE) BUILD_ARGS="--load" img.build.shared
+ @$(OK) docker build $(IMAGE)
+
+img.publish:
+ @$(INFO) docker publish $(IMAGE)
+ @$(MAKE) BUILD_ARGS="--push" img.build.shared
+ @$(OK) docker publish $(IMAGE)
+
+img.build.shared:
+ @cp Dockerfile $(IMAGE_TEMP_DIR) || $(FAIL)
+ @cp -r $(OUTPUT_DIR)/bin/ $(IMAGE_TEMP_DIR)/bin || $(FAIL)
+ @docker buildx build $(BUILD_ARGS) \
+ --platform $(IMAGE_PLATFORMS) \
+ -t $(IMAGE) \
+ $(IMAGE_TEMP_DIR) || $(FAIL)
+
+img.promote:
+ @$(INFO) docker promote $(FROM_IMAGE) to $(TO_IMAGE)
+ @docker buildx imagetools create -t $(TO_IMAGE) $(FROM_IMAGE)
+ @$(OK) docker promote $(FROM_IMAGE) to $(TO_IMAGE)
diff --git a/cmd/function-runtime-oci/main.go b/cmd/function-runtime-oci/main.go
new file mode 100644
index 0000000..52638af
--- /dev/null
+++ b/cmd/function-runtime-oci/main.go
@@ -0,0 +1,83 @@
+/*
+Copyright 2019 The Crossplane 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 main is the reference implementation of Composition Functions.
+package main
+
+import (
+ "fmt"
+
+ "github.com/alecthomas/kong"
+ "github.com/google/go-containerregistry/pkg/name"
+ "sigs.k8s.io/controller-runtime/pkg/log/zap"
+
+ "github.com/crossplane/crossplane-runtime/pkg/logging"
+
+ "github.com/crossplane/function-runtime-oci/cmd/function-runtime-oci/run"
+ "github.com/crossplane/function-runtime-oci/cmd/function-runtime-oci/spark"
+ "github.com/crossplane/function-runtime-oci/cmd/function-runtime-oci/start"
+ "github.com/crossplane/function-runtime-oci/internal/version"
+)
+
+type debugFlag bool
+type versionFlag bool
+
+// KongVars represent the kong variables associated with the CLI parser
+// required for the Registry default variable interpolation.
+var KongVars = kong.Vars{
+ "default_registry": name.DefaultRegistry,
+}
+
+var cli struct {
+ Debug debugFlag `short:"d" help:"Print verbose logging statements."`
+
+ Version versionFlag `short:"v" help:"Print version and quit."`
+ Registry string `short:"r" help:"Default registry used to fetch containers when not specified in tag." default:"${default_registry}" env:"REGISTRY"`
+
+ Start start.Command `cmd:"" help:"Start listening for Composition Function runs over gRPC." default:"1"`
+ Run run.Command `cmd:"" help:"Run a Composition Function."`
+ Spark spark.Command `cmd:"" help:"function-runtime-oci executes Spark inside a user namespace to run a Composition Function. You shouldn't run it directly." hidden:""`
+}
+
+// BeforeApply binds the dev mode logger to the kong context when debugFlag is
+// passed.
+func (d debugFlag) BeforeApply(ctx *kong.Context) error { //nolint:unparam // BeforeApply requires this signature.
+ zl := zap.New(zap.UseDevMode(true)).WithName("function-runtime-oci")
+ // BindTo uses reflect.TypeOf to get reflection type of used interface
+ // A *logging.Logger value here is used to find the reflection type here.
+ // Please refer: https://golang.org/pkg/reflect/#TypeOf
+ ctx.BindTo(logging.NewLogrLogger(zl), (*logging.Logger)(nil))
+ return nil
+}
+
+func (v versionFlag) BeforeApply(app *kong.Kong) error { //nolint:unparam // BeforeApply requires this signature.
+ fmt.Fprintln(app.Stdout, version.New().GetVersionString())
+ app.Exit(0)
+ return nil
+}
+
+func main() {
+ zl := zap.New().WithName("function-runtime-oci")
+
+ ctx := kong.Parse(&cli,
+ kong.Name("function-runtime-oci"),
+ kong.Description("Crossplane Composition Functions."),
+ kong.BindTo(logging.NewLogrLogger(zl), (*logging.Logger)(nil)),
+ kong.UsageOnError(),
+ KongVars,
+ )
+ ctx.FatalIfErrorf(ctx.Run(&start.Args{Registry: cli.Registry}))
+}
diff --git a/cmd/function-runtime-oci/run/run.go b/cmd/function-runtime-oci/run/run.go
new file mode 100644
index 0000000..d64f72c
--- /dev/null
+++ b/cmd/function-runtime-oci/run/run.go
@@ -0,0 +1,145 @@
+/*
+Copyright 2022 The Crossplane 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 run implements a convenience CLI to run and test Composition Functions.
+package run
+
+import (
+ "context"
+ "os"
+ "path/filepath"
+ "time"
+
+ "github.com/google/go-containerregistry/pkg/authn"
+ "github.com/google/go-containerregistry/pkg/name"
+ "google.golang.org/protobuf/types/known/durationpb"
+
+ "github.com/crossplane/crossplane-runtime/pkg/errors"
+
+ "github.com/crossplane/function-runtime-oci/cmd/function-runtime-oci/start"
+ "github.com/crossplane/function-runtime-oci/internal/function-runtime-oci"
+ "github.com/crossplane/function-runtime-oci/internal/proto/v1alpha1"
+)
+
+// Error strings
+const (
+ errWriteFIO = "cannot write FunctionIO YAML to stdout"
+ errRunFunction = "cannot run function"
+ errParseImage = "cannot parse image reference"
+ errResolveKeychain = "cannot resolve default registry authentication keychain"
+ errAuthCfg = "cannot get default registry authentication credentials"
+)
+
+// Command runs a Composition function.
+type Command struct {
+ CacheDir string `short:"c" help:"Directory used for caching function images and containers." default:"/function-runtime-oci"`
+ Timeout time.Duration `help:"Maximum time for which the function may run before being killed." default:"30s"`
+ ImagePullPolicy string `help:"Whether the image may be pulled from a remote registry." enum:"Always,Never,IfNotPresent" default:"IfNotPresent"`
+ NetworkPolicy string `help:"Whether the function may access the network." enum:"Runner,Isolated" default:"Isolated"`
+ MapRootUID int `help:"UID that will map to 0 in the function's user namespace. The following 65336 UIDs must be available. Ignored if function-runtime-oci does not have CAP_SETUID and CAP_SETGID." default:"100000"`
+ MapRootGID int `help:"GID that will map to 0 in the function's user namespace. The following 65336 GIDs must be available. Ignored if function-runtime-oci does not have CAP_SETUID and CAP_SETGID." default:"100000"`
+
+ // TODO(negz): filecontent appears to take multiple args when it does not.
+ // Bump kong once https://github.com/alecthomas/kong/issues/346 is fixed.
+
+ Image string `arg:"" help:"OCI image to run."`
+ FunctionIO []byte `arg:"" help:"YAML encoded FunctionIO to pass to the function." type:"filecontent"`
+}
+
+// Run a Composition container function.
+func (c *Command) Run(args *start.Args) error {
+ // If we don't have CAP_SETUID or CAP_SETGID, we'll only be able to map our
+ // own UID and GID to root inside the user namespace.
+ rootUID := os.Getuid()
+ rootGID := os.Getgid()
+ setuid := function_runtime_oci.HasCapSetUID() && function_runtime_oci.HasCapSetGID() // We're using 'setuid' as shorthand for both here.
+ if setuid {
+ rootUID = c.MapRootUID
+ rootGID = c.MapRootGID
+ }
+
+ ref, err := name.ParseReference(c.Image, name.WithDefaultRegistry(args.Registry))
+ if err != nil {
+ return errors.Wrap(err, errParseImage)
+ }
+
+ // We want to resolve authentication credentials here, using the caller's
+ // environment rather than inside the user namespace that spark will create.
+ // DefaultKeychain uses credentials from ~/.docker/config.json to pull
+ // private images. Despite being 'the default' it must be explicitly
+ // provided, or go-containerregistry will use anonymous authentication.
+ auth, err := authn.DefaultKeychain.Resolve(ref.Context())
+ if err != nil {
+ return errors.Wrap(err, errResolveKeychain)
+ }
+
+ a, err := auth.Authorization()
+ if err != nil {
+ return errors.Wrap(err, errAuthCfg)
+ }
+
+ f := function_runtime_oci.NewContainerRunner(function_runtime_oci.SetUID(setuid), function_runtime_oci.MapToRoot(rootUID, rootGID), function_runtime_oci.WithCacheDir(filepath.Clean(c.CacheDir)), function_runtime_oci.WithRegistry(args.Registry))
+ rsp, err := f.RunFunction(context.Background(), &v1alpha1.RunFunctionRequest{
+ Image: c.Image,
+ Input: c.FunctionIO,
+ ImagePullConfig: &v1alpha1.ImagePullConfig{
+ PullPolicy: pullPolicy(c.ImagePullPolicy),
+ Auth: &v1alpha1.ImagePullAuth{
+ Username: a.Username,
+ Password: a.Password,
+ Auth: a.Auth,
+ IdentityToken: a.IdentityToken,
+ RegistryToken: a.RegistryToken,
+ },
+ },
+ RunFunctionConfig: &v1alpha1.RunFunctionConfig{
+ Timeout: durationpb.New(c.Timeout),
+ Network: &v1alpha1.NetworkConfig{
+ Policy: networkPolicy(c.NetworkPolicy),
+ },
+ },
+ })
+ if err != nil {
+ return errors.Wrap(err, errRunFunction)
+ }
+
+ _, err = os.Stdout.Write(rsp.GetOutput())
+ return errors.Wrap(err, errWriteFIO)
+}
+
+func pullPolicy(p string) v1alpha1.ImagePullPolicy {
+ switch p {
+ case "Always":
+ return v1alpha1.ImagePullPolicy_IMAGE_PULL_POLICY_ALWAYS
+ case "Never":
+ return v1alpha1.ImagePullPolicy_IMAGE_PULL_POLICY_NEVER
+ case "IfNotPresent":
+ fallthrough
+ default:
+ return v1alpha1.ImagePullPolicy_IMAGE_PULL_POLICY_IF_NOT_PRESENT
+ }
+}
+
+func networkPolicy(p string) v1alpha1.NetworkPolicy {
+ switch p {
+ case "Runner":
+ return v1alpha1.NetworkPolicy_NETWORK_POLICY_RUNNER
+ case "Isolated":
+ fallthrough
+ default:
+ return v1alpha1.NetworkPolicy_NETWORK_POLICY_ISOLATED
+ }
+}
diff --git a/cmd/function-runtime-oci/spark/spark.go b/cmd/function-runtime-oci/spark/spark.go
new file mode 100644
index 0000000..ac71431
--- /dev/null
+++ b/cmd/function-runtime-oci/spark/spark.go
@@ -0,0 +1,275 @@
+/*
+Copyright 2022 The Crossplane 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 spark runs a Composition Function. It is designed to be run as root
+// inside an unprivileged user namespace.
+package spark
+
+import (
+ "bytes"
+ "context"
+ "io"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "time"
+
+ "github.com/google/go-containerregistry/pkg/name"
+ "github.com/google/uuid"
+ runtime "github.com/opencontainers/runtime-spec/specs-go"
+ "google.golang.org/protobuf/proto"
+
+ "github.com/crossplane/crossplane-runtime/pkg/errors"
+
+ "github.com/crossplane/function-runtime-oci/cmd/function-runtime-oci/start"
+ "github.com/crossplane/function-runtime-oci/internal/oci"
+ "github.com/crossplane/function-runtime-oci/internal/oci/spec"
+ "github.com/crossplane/function-runtime-oci/internal/oci/store"
+ "github.com/crossplane/function-runtime-oci/internal/oci/store/overlay"
+ "github.com/crossplane/function-runtime-oci/internal/oci/store/uncompressed"
+ "github.com/crossplane/function-runtime-oci/internal/proto/v1alpha1"
+)
+
+// Error strings.
+const (
+ errReadRequest = "cannot read request from stdin"
+ errUnmarshalRequest = "cannot unmarshal request data from stdin"
+ errNewBundleStore = "cannot create OCI runtime bundle store"
+ errNewDigestStore = "cannot create OCI image digest store"
+ errParseRef = "cannot parse OCI image reference"
+ errPull = "cannot pull OCI image"
+ errBundleFn = "cannot create OCI runtime bundle"
+ errMkRuntimeRootdir = "cannot make OCI runtime cache"
+ errRuntime = "OCI runtime error"
+ errCleanupBundle = "cannot cleanup OCI runtime bundle"
+ errMarshalResponse = "cannot marshal response data to stdout"
+ errWriteResponse = "cannot write response data to stdout"
+ errCPULimit = "cannot limit container CPU"
+ errMemoryLimit = "cannot limit container memory"
+ errHostNetwork = "cannot configure container to run in host network namespace"
+)
+
+// The path within the cache dir that the OCI runtime should use for its
+// '--root' cache.
+const ociRuntimeRoot = "runtime"
+
+// The time after which the OCI runtime will be killed if none is specified in
+// the RunFunctionRequest.
+const defaultTimeout = 25 * time.Second
+
+// Command runs a containerized Composition Function.
+type Command struct {
+ CacheDir string `short:"c" help:"Directory used for caching function images and containers." default:"/function-runtime-oci"`
+ Runtime string `help:"OCI runtime binary to invoke." default:"crun"`
+ MaxStdioBytes int64 `help:"Maximum size of stdout and stderr for functions." default:"0"`
+ CABundlePath string `help:"Additional CA bundle to use when fetching function images from registry." env:"CA_BUNDLE_PATH"`
+}
+
+// Run a Composition Function inside an unprivileged user namespace. Reads a
+// protocol buffer serialized RunFunctionRequest from stdin, and writes a
+// protocol buffer serialized RunFunctionResponse to stdout.
+func (c *Command) Run(args *start.Args) error { //nolint:gocyclo // TODO(negz): Refactor some of this out into functions, add tests.
+ pb, err := io.ReadAll(os.Stdin)
+ if err != nil {
+ return errors.Wrap(err, errReadRequest)
+ }
+
+ req := &v1alpha1.RunFunctionRequest{}
+ if err := proto.Unmarshal(pb, req); err != nil {
+ return errors.Wrap(err, errUnmarshalRequest)
+ }
+
+ t := req.GetRunFunctionConfig().GetTimeout().AsDuration()
+ if t == 0 {
+ t = defaultTimeout
+ }
+ ctx, cancel := context.WithTimeout(context.Background(), t)
+ defer cancel()
+
+ runID := uuid.NewString()
+
+ // We prefer to use an overlayfs bundler where possible. It roughly doubles
+ // the disk space per image because it caches layers as overlay compatible
+ // directories in addition to the CachingImagePuller's cache of uncompressed
+ // layer tarballs. The advantage is faster start times for containers with
+ // cached image, because it creates an overlay rootfs. The uncompressed
+ // bundler on the other hand must untar all of a containers layers to create
+ // a new rootfs each time it runs a container.
+ var s store.Bundler = uncompressed.NewBundler(c.CacheDir)
+ if overlay.Supported(c.CacheDir) {
+ s, err = overlay.NewCachingBundler(c.CacheDir)
+ }
+ if err != nil {
+ return errors.Wrap(err, errNewBundleStore)
+ }
+
+ // This store maps OCI references to their last known digests. We use it to
+ // resolve references when the imagePullPolicy is Never or IfNotPresent.
+ h, err := store.NewDigest(c.CacheDir)
+ if err != nil {
+ return errors.Wrap(err, errNewDigestStore)
+ }
+
+ r, err := name.ParseReference(req.GetImage(), name.WithDefaultRegistry(args.Registry))
+ if err != nil {
+ return errors.Wrap(err, errParseRef)
+ }
+
+ opts := []oci.ImageClientOption{FromImagePullConfig(req.GetImagePullConfig())}
+ if c.CABundlePath != "" {
+ rootCA, err := oci.ParseCertificatesFromPath(c.CABundlePath)
+ if err != nil {
+ return errors.Wrap(err, "Cannot parse CA bundle")
+ }
+ opts = append(opts, oci.WithCustomCA(rootCA))
+ }
+ // We cache every image we pull to the filesystem. Layers are cached as
+ // uncompressed tarballs. This allows them to be extracted quickly when
+ // using the uncompressed.Bundler, which extracts a new root filesystem for
+ // every container run.
+ p := oci.NewCachingPuller(h, store.NewImage(c.CacheDir), &oci.RemoteClient{})
+ img, err := p.Image(ctx, r, opts...)
+ if err != nil {
+ return errors.Wrap(err, errPull)
+ }
+
+ // Create an OCI runtime bundle for this container run.
+ b, err := s.Bundle(ctx, img, runID, FromRunFunctionConfig(req.GetRunFunctionConfig()))
+ if err != nil {
+ return errors.Wrap(err, errBundleFn)
+ }
+
+ root := filepath.Join(c.CacheDir, ociRuntimeRoot)
+ if err := os.MkdirAll(root, 0700); err != nil {
+ _ = b.Cleanup()
+ return errors.Wrap(err, errMkRuntimeRootdir)
+ }
+
+ // TODO(negz): Consider using the OCI runtime's lifecycle management commands
+ // (i.e create, start, and delete) rather than run. This would allow spark
+ // to return without sitting in-between function-runtime-oci and crun. It's also generally
+ // recommended; 'run' is more for testing. In practice though run seems to
+ // work just fine for our use case.
+
+ //nolint:gosec // Executing with user-supplied input is intentional.
+ cmd := exec.CommandContext(ctx, c.Runtime, "--root="+root, "run", "--bundle="+b.Path(), runID)
+ cmd.Stdin = bytes.NewReader(req.GetInput())
+
+ stdoutPipe, err := cmd.StdoutPipe()
+ if err != nil {
+ _ = b.Cleanup()
+ return errors.Wrap(err, errRuntime)
+ }
+ stderrPipe, err := cmd.StderrPipe()
+ if err != nil {
+ _ = b.Cleanup()
+ return errors.Wrap(err, errRuntime)
+ }
+
+ if err := cmd.Start(); err != nil {
+ _ = b.Cleanup()
+ return errors.Wrap(err, errRuntime)
+ }
+
+ stdout, err := io.ReadAll(limitReaderIfNonZero(stdoutPipe, c.MaxStdioBytes))
+ if err != nil {
+ _ = b.Cleanup()
+ return errors.Wrap(err, errRuntime)
+ }
+ stderr, err := io.ReadAll(limitReaderIfNonZero(stderrPipe, c.MaxStdioBytes))
+ if err != nil {
+ _ = b.Cleanup()
+ return errors.Wrap(err, errRuntime)
+ }
+
+ if err := cmd.Wait(); err != nil {
+ var exitErr *exec.ExitError
+ if errors.As(err, &exitErr) {
+ exitErr.Stderr = stderr
+ }
+ _ = b.Cleanup()
+ return errors.Wrap(err, errRuntime)
+ }
+
+ if err := b.Cleanup(); err != nil {
+ return errors.Wrap(err, errCleanupBundle)
+ }
+
+ rsp := &v1alpha1.RunFunctionResponse{Output: stdout}
+ pb, err = proto.Marshal(rsp)
+ if err != nil {
+ return errors.Wrap(err, errMarshalResponse)
+ }
+ _, err = os.Stdout.Write(pb)
+ return errors.Wrap(err, errWriteResponse)
+}
+
+func limitReaderIfNonZero(r io.Reader, limit int64) io.Reader {
+ if limit == 0 {
+ return r
+ }
+ return io.LimitReader(r, limit)
+}
+
+// FromImagePullConfig configures an image client with options derived from the
+// supplied ImagePullConfig.
+func FromImagePullConfig(cfg *v1alpha1.ImagePullConfig) oci.ImageClientOption {
+ return func(o *oci.ImageClientOptions) {
+ switch cfg.GetPullPolicy() {
+ case v1alpha1.ImagePullPolicy_IMAGE_PULL_POLICY_ALWAYS:
+ oci.WithPullPolicy(oci.ImagePullPolicyAlways)(o)
+ case v1alpha1.ImagePullPolicy_IMAGE_PULL_POLICY_NEVER:
+ oci.WithPullPolicy(oci.ImagePullPolicyNever)(o)
+ case v1alpha1.ImagePullPolicy_IMAGE_PULL_POLICY_IF_NOT_PRESENT, v1alpha1.ImagePullPolicy_IMAGE_PULL_POLICY_UNSPECIFIED:
+ oci.WithPullPolicy(oci.ImagePullPolicyIfNotPresent)(o)
+ }
+ if a := cfg.GetAuth(); a != nil {
+ oci.WithPullAuth(&oci.ImagePullAuth{
+ Username: a.GetUsername(),
+ Password: a.GetPassword(),
+ Auth: a.GetAuth(),
+ IdentityToken: a.GetIdentityToken(),
+ RegistryToken: a.GetRegistryToken(),
+ })(o)
+ }
+ }
+}
+
+// FromRunFunctionConfig extends a runtime spec with configuration derived from
+// the supplied RunFunctionConfig.
+func FromRunFunctionConfig(cfg *v1alpha1.RunFunctionConfig) spec.Option {
+ return func(s *runtime.Spec) error {
+ if l := cfg.GetResources().GetLimits().GetCpu(); l != "" {
+ if err := spec.WithCPULimit(l)(s); err != nil {
+ return errors.Wrap(err, errCPULimit)
+ }
+ }
+
+ if l := cfg.GetResources().GetLimits().GetMemory(); l != "" {
+ if err := spec.WithMemoryLimit(l)(s); err != nil {
+ return errors.Wrap(err, errMemoryLimit)
+ }
+ }
+
+ if cfg.GetNetwork().GetPolicy() == v1alpha1.NetworkPolicy_NETWORK_POLICY_RUNNER {
+ if err := spec.WithHostNetwork()(s); err != nil {
+ return errors.Wrap(err, errHostNetwork)
+ }
+ }
+
+ return nil
+ }
+}
diff --git a/cmd/function-runtime-oci/start/start.go b/cmd/function-runtime-oci/start/start.go
new file mode 100644
index 0000000..077dc4a
--- /dev/null
+++ b/cmd/function-runtime-oci/start/start.go
@@ -0,0 +1,71 @@
+/*
+Copyright 2022 The Crossplane 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 start implements the reference Composition Function runner.
+// It exposes a gRPC API that may be used to run Composition Functions.
+package start
+
+import (
+ "os"
+ "path/filepath"
+
+ "github.com/crossplane/crossplane-runtime/pkg/errors"
+ "github.com/crossplane/crossplane-runtime/pkg/logging"
+
+ "github.com/crossplane/function-runtime-oci/internal/function-runtime-oci"
+)
+
+// Error strings
+const (
+ errListenAndServe = "cannot listen for and serve gRPC API"
+)
+
+// Args contains the default registry used to pull function-runtime-oci
+// containers.
+type Args struct {
+ Registry string
+}
+
+// Command starts a gRPC API to run Composition Functions.
+type Command struct {
+ CacheDir string `short:"c" help:"Directory used for caching function images and containers." default:"/function-runtime-oci"`
+ MapRootUID int `help:"UID that will map to 0 in the function's user namespace. The following 65336 UIDs must be available. Ignored if function-runtime-oci does not have CAP_SETUID and CAP_SETGID." default:"100000"`
+ MapRootGID int `help:"GID that will map to 0 in the function's user namespace. The following 65336 GIDs must be available. Ignored if function-runtime-oci does not have CAP_SETUID and CAP_SETGID." default:"100000"`
+ Network string `help:"Network on which to listen for gRPC connections." default:"unix"`
+ Address string `help:"Address at which to listen for gRPC connections." default:"@crossplane/fn/default.sock"`
+}
+
+// Run a Composition Function gRPC API.
+func (c *Command) Run(args *Args, log logging.Logger) error {
+ // If we don't have CAP_SETUID or CAP_SETGID, we'll only be able to map our
+ // own UID and GID to root inside the user namespace.
+ rootUID := os.Getuid()
+ rootGID := os.Getgid()
+ setuid := function_runtime_oci.HasCapSetUID() && function_runtime_oci.HasCapSetGID() // We're using 'setuid' as shorthand for both here.
+ if setuid {
+ rootUID = c.MapRootUID
+ rootGID = c.MapRootGID
+ }
+
+ // TODO(negz): Expose a healthz endpoint and otel metrics.
+ f := function_runtime_oci.NewContainerRunner(
+ function_runtime_oci.SetUID(setuid),
+ function_runtime_oci.MapToRoot(rootUID, rootGID),
+ function_runtime_oci.WithCacheDir(filepath.Clean(c.CacheDir)),
+ function_runtime_oci.WithLogger(log),
+ function_runtime_oci.WithRegistry(args.Registry))
+ return errors.Wrap(f.ListenAndServe(c.Network, c.Address), errListenAndServe)
+}
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000..90ee533
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,119 @@
+module github.com/crossplane/function-runtime-oci
+
+go 1.20
+
+require (
+ github.com/Masterminds/semver v1.5.0
+ github.com/alecthomas/kong v0.8.0
+ github.com/bufbuild/buf v1.24.0
+ github.com/crossplane/crossplane-runtime v1.13.0
+ github.com/cyphar/filepath-securejoin v0.2.3
+ github.com/google/go-cmp v0.5.9
+ github.com/google/go-containerregistry v0.16.1
+ github.com/google/uuid v1.3.0
+ github.com/opencontainers/runtime-spec v1.1.0-rc.3.0.20230610073135-48415de180cf
+ golang.org/x/sync v0.3.0
+ golang.org/x/sys v0.11.0
+ google.golang.org/grpc v1.57.0
+ google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.3.0
+ google.golang.org/protobuf v1.31.0
+ k8s.io/apimachinery v0.27.3
+ k8s.io/code-generator v0.28.0
+ kernel.org/pub/linux/libs/security/libcap/cap v1.2.69
+ sigs.k8s.io/controller-runtime v0.15.0
+)
+
+require (
+ dario.cat/mergo v1.0.0 // indirect
+ github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect
+ github.com/Microsoft/go-winio v0.6.1 // indirect
+ github.com/bufbuild/connect-go v1.9.0 // indirect
+ github.com/bufbuild/connect-opentelemetry-go v0.4.0 // indirect
+ github.com/bufbuild/protocompile v0.5.1 // indirect
+ github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
+ github.com/docker/go-connections v0.4.0 // indirect
+ github.com/docker/go-units v0.5.0 // indirect
+ github.com/felixge/fgprof v0.9.3 // indirect
+ github.com/go-chi/chi/v5 v5.0.8 // indirect
+ github.com/go-logr/stdr v1.2.2 // indirect
+ github.com/gofrs/uuid/v5 v5.0.0 // indirect
+ github.com/google/gnostic-models v0.6.8 // indirect
+ github.com/google/pprof v0.0.0-20230705174524-200ffdc848b8 // indirect
+ github.com/inconshreveable/mousetrap v1.1.0 // indirect
+ github.com/jdxcode/netrc v0.0.0-20221124155335-4616370d1a84 // indirect
+ github.com/klauspost/pgzip v1.2.6 // indirect
+ github.com/moby/term v0.5.0 // indirect
+ github.com/morikuni/aec v1.0.0 // indirect
+ github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 // indirect
+ github.com/pkg/errors v0.9.1 // indirect
+ github.com/pkg/profile v1.7.0 // indirect
+ github.com/rs/cors v1.9.0 // indirect
+ github.com/russross/blackfriday/v2 v2.1.0 // indirect
+ github.com/sirupsen/logrus v1.9.3 // indirect
+ github.com/spf13/afero v1.9.5 // indirect
+ github.com/spf13/cobra v1.7.0 // indirect
+ github.com/spf13/pflag v1.0.5 // indirect
+ github.com/tetratelabs/wazero v1.2.1 // indirect
+ go.opentelemetry.io/otel v1.16.0 // indirect
+ go.opentelemetry.io/otel/metric v1.16.0 // indirect
+ go.opentelemetry.io/otel/sdk v1.16.0 // indirect
+ go.opentelemetry.io/otel/trace v1.16.0 // indirect
+ golang.org/x/crypto v0.11.0 // indirect
+ golang.org/x/mod v0.12.0 // indirect
+ golang.org/x/tools v0.11.0 // indirect
+ google.golang.org/genproto/googleapis/rpc v0.0.0-20230525234030-28d5490b6b19 // indirect
+ gotest.tools/v3 v3.1.0 // indirect
+ k8s.io/api v0.27.3 // indirect
+ k8s.io/client-go v0.27.3 // indirect
+ k8s.io/gengo v0.0.0-20220902162205-c0856e24416d // indirect
+ k8s.io/utils v0.0.0-20230505201702-9f6742963106 // indirect
+ sigs.k8s.io/yaml v1.3.0 // indirect
+)
+
+require (
+ github.com/containerd/stargz-snapshotter/estargz v0.14.3 // indirect
+ github.com/davecgh/go-spew v1.1.1 // indirect
+ github.com/docker/cli v24.0.4+incompatible // indirect
+ github.com/docker/distribution v2.8.2+incompatible // indirect
+ github.com/docker/docker v24.0.4+incompatible // indirect
+ github.com/docker/docker-credential-helpers v0.8.0 // indirect
+ github.com/emicklei/go-restful/v3 v3.10.2 // indirect
+ github.com/evanphx/json-patch/v5 v5.6.0 // indirect
+ github.com/go-logr/logr v1.2.4 // indirect
+ github.com/go-logr/zapr v1.2.4 // indirect
+ github.com/go-openapi/jsonpointer v0.19.6 // indirect
+ github.com/go-openapi/jsonreference v0.20.2 // indirect
+ github.com/go-openapi/swag v0.22.3 // indirect
+ github.com/gogo/protobuf v1.3.2 // indirect
+ github.com/golang/protobuf v1.5.3 // indirect
+ github.com/google/gnostic v0.6.9 // indirect
+ github.com/google/gofuzz v1.2.0 // indirect
+ github.com/josharian/intern v1.0.0 // indirect
+ github.com/json-iterator/go v1.1.12 // indirect
+ github.com/klauspost/compress v1.16.7 // indirect
+ github.com/mailru/easyjson v0.7.7 // indirect
+ github.com/mitchellh/go-homedir v1.1.0 // indirect
+ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
+ github.com/modern-go/reflect2 v1.0.2 // indirect
+ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
+ github.com/opencontainers/go-digest v1.0.0 // indirect
+ github.com/opencontainers/image-spec v1.1.0-rc4 // indirect
+ github.com/vbatts/tar-split v0.11.3 // indirect
+ go.uber.org/atomic v1.11.0 // indirect
+ go.uber.org/multierr v1.11.0 // indirect
+ go.uber.org/zap v1.24.0 // indirect
+ golang.org/x/net v0.13.0 // indirect; indirect // indirect
+ golang.org/x/oauth2 v0.8.0 // indirect
+ golang.org/x/term v0.10.0 // indirect
+ golang.org/x/text v0.11.0 // indirect
+ golang.org/x/time v0.3.0 // indirect
+ google.golang.org/appengine v1.6.7 // indirect
+ gopkg.in/inf.v0 v0.9.1 // indirect
+ gopkg.in/yaml.v2 v2.4.0 // indirect
+ gopkg.in/yaml.v3 v3.0.1 // indirect
+ k8s.io/klog/v2 v2.100.1 // indirect
+ k8s.io/kube-openapi v0.0.0-20230717233707-2695361300d9 // indirect
+ kernel.org/pub/linux/libs/security/libcap/psx v1.2.69 // indirect
+ sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
+ sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect
+)
diff --git a/go.sum b/go.sum
new file mode 100644
index 0000000..c792e42
--- /dev/null
+++ b/go.sum
@@ -0,0 +1,761 @@
+cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
+cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
+cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
+cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
+cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
+cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
+cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
+cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
+cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
+cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
+cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
+cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
+cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
+cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
+cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
+cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
+cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI=
+cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk=
+cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY=
+cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
+cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
+cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
+cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
+cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
+cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
+cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
+cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
+cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
+cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
+cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
+cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
+cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
+cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
+cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
+cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
+cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
+cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo=
+dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk=
+dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
+dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
+github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0=
+github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
+github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
+github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
+github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
+github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww=
+github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y=
+github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow=
+github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM=
+github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
+github.com/alecthomas/assert/v2 v2.1.0 h1:tbredtNcQnoSd3QBhQWI7QZ3XHOVkw1Moklp2ojoH/0=
+github.com/alecthomas/kong v0.8.0 h1:ryDCzutfIqJPnNn0omnrgHLbAggDQM2VWHikE1xqK7s=
+github.com/alecthomas/kong v0.8.0/go.mod h1:n1iCIO2xS46oE8ZfYCNDqdR0b0wZNrXAIAqro/2132U=
+github.com/alecthomas/repr v0.1.0 h1:ENn2e1+J3k09gyj2shc0dHr/yjaWSHRlrJ4DPMevDqE=
+github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
+github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
+github.com/benbjohnson/clock v1.3.5 h1:VvXlSJBzZpA/zum6Sj74hxwYI2DIxRWuNIoXAzHZz5o=
+github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
+github.com/bufbuild/buf v1.24.0 h1:36rVJMJX7BI9Z6nUPpirF+TUO9tVZ45u5VAfj5E7Bgw=
+github.com/bufbuild/buf v1.24.0/go.mod h1:cacBvncWbYnUcNX580lqllR3qlEetMX/KVm27pUc4Kc=
+github.com/bufbuild/connect-go v1.9.0 h1:JIgAeNuFpo+SUPfU19Yt5TcWlznsN5Bv10/gI/6Pjoc=
+github.com/bufbuild/connect-go v1.9.0/go.mod h1:CAIePUgkDR5pAFaylSMtNK45ANQjp9JvpluG20rhpV8=
+github.com/bufbuild/connect-opentelemetry-go v0.4.0 h1:6JAn10SNqlQ/URhvRNGrIlczKw1wEXknBUUtmWqOiak=
+github.com/bufbuild/connect-opentelemetry-go v0.4.0/go.mod h1:nwPXYoDOoc2DGyKE/6pT1Q9MPSi2Et2e6BieMD0l6WU=
+github.com/bufbuild/protocompile v0.5.1 h1:mixz5lJX4Hiz4FpqFREJHIXLfaLBntfaJv1h+/jS+Qg=
+github.com/bufbuild/protocompile v0.5.1/go.mod h1:G5iLmavmF4NsYtpZFvE3B/zFch2GIY8+wjsYLR/lc40=
+github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
+github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
+github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
+github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
+github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
+github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
+github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
+github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
+github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
+github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
+github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
+github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
+github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
+github.com/containerd/stargz-snapshotter/estargz v0.14.3 h1:OqlDCK3ZVUO6C3B/5FSkDwbkEETK84kQgEeFwDC+62k=
+github.com/containerd/stargz-snapshotter/estargz v0.14.3/go.mod h1:KY//uOCIkSuNAHhJogcZtrNHdKrA99/FCCRjE3HD36o=
+github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
+github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
+github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
+github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=
+github.com/crossplane/crossplane-runtime v1.13.0 h1:EumInUbS8mXV7otwoI3xa0rPczexJOky4XLVlHxxjO0=
+github.com/crossplane/crossplane-runtime v1.13.0/go.mod h1:FuKIC8Mg8hE2gIAMyf2wCPkxkFPz+VnMQiYWBq1/p5A=
+github.com/cyphar/filepath-securejoin v0.2.3 h1:YX6ebbZCZP7VkM3scTTokDgBL2TY741X51MTk3ycuNI=
+github.com/cyphar/filepath-securejoin v0.2.3/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/docker/cli v24.0.4+incompatible h1:Y3bYF9ekNTm2VFz5U/0BlMdJy73D+Y1iAAZ8l63Ydzw=
+github.com/docker/cli v24.0.4+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
+github.com/docker/distribution v2.8.2+incompatible h1:T3de5rq0dB1j30rp0sA2rER+m322EBzniBPB6ZIzuh8=
+github.com/docker/distribution v2.8.2+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
+github.com/docker/docker v24.0.4+incompatible h1:s/LVDftw9hjblvqIeTiGYXBCD95nOEEl7qRsRrIOuQI=
+github.com/docker/docker v24.0.4+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
+github.com/docker/docker-credential-helpers v0.8.0 h1:YQFtbBQb4VrpoPxhFuzEBPQ9E16qz5SpHLS+uswaCp8=
+github.com/docker/docker-credential-helpers v0.8.0/go.mod h1:UGFXcuoQ5TxPiB54nHOZ32AWRqQdECoh/Mg0AlEYb40=
+github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ=
+github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
+github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
+github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
+github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
+github.com/emicklei/go-restful/v3 v3.10.2 h1:hIovbnmBTLjHXkqEBUz3HGpXZdM7ZrE9fJIZIqlJLqE=
+github.com/emicklei/go-restful/v3 v3.10.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
+github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
+github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
+github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
+github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=
+github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
+github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ=
+github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
+github.com/evanphx/json-patch/v5 v5.6.0 h1:b91NhWfaz02IuVxO9faSllyAtNXHMPkC5J8sJCLunww=
+github.com/evanphx/json-patch/v5 v5.6.0/go.mod h1:G79N1coSVB93tBe7j6PhzjmR3/2VvlbKOFpnXhI9Bw4=
+github.com/felixge/fgprof v0.9.3 h1:VvyZxILNuCiUCSXtPtYmmtGvb65nqXh2QFWc0Wpf2/g=
+github.com/felixge/fgprof v0.9.3/go.mod h1:RdbpDgzqYVh/T9fPELJyV7EYJuHB55UTEULNun8eiPw=
+github.com/flowstack/go-jsonschema v0.1.1/go.mod h1:yL7fNggx1o8rm9RlgXv7hTBWxdBM0rVwpMwimd3F3N0=
+github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
+github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
+github.com/go-chi/chi/v5 v5.0.8 h1:lD+NLqFcAi1ovnVZpsnObHGW4xb4J8lNmoYVfECH1Y0=
+github.com/go-chi/chi/v5 v5.0.8/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
+github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
+github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
+github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
+github.com/go-logr/logr v0.2.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU=
+github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
+github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
+github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ=
+github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
+github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
+github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
+github.com/go-logr/zapr v1.2.4 h1:QHVo+6stLbfJmYGkQ7uGHUCu5hnAFAj6mDe6Ea0SeOo=
+github.com/go-logr/zapr v1.2.4/go.mod h1:FyHWQIzQORZ0QVE1BtVHv3cKtNLuXsbNLtpuhNapBOA=
+github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE=
+github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs=
+github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE=
+github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k=
+github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g=
+github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
+github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
+github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw=
+github.com/gofrs/uuid/v5 v5.0.0 h1:p544++a97kEL+svbcFbCQVM9KFu0Yo25UoISXGNNH9M=
+github.com/gofrs/uuid/v5 v5.0.0/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8=
+github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
+github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
+github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
+github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
+github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
+github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
+github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
+github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
+github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
+github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
+github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
+github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
+github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
+github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
+github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
+github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
+github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
+github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
+github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
+github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
+github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
+github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
+github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
+github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
+github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
+github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
+github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
+github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
+github.com/google/gnostic v0.6.9 h1:ZK/5VhkoX835RikCHpSUJV9a+S3e1zLh59YnyWeBW+0=
+github.com/google/gnostic v0.6.9/go.mod h1:Nm8234We1lq6iB9OmlgNv3nH91XLLVZHCDayfA3xq+E=
+github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I=
+github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U=
+github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
+github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
+github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/google/go-containerregistry v0.16.1 h1:rUEt426sR6nyrL3gt+18ibRcvYpKYdpsa5ZW7MA08dQ=
+github.com/google/go-containerregistry v0.16.1/go.mod h1:u0qB2l7mvtWVR5kNcbFIhFY1hLbf8eeGapA+vbFDCtQ=
+github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
+github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
+github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
+github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
+github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
+github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
+github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
+github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
+github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
+github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
+github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
+github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
+github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
+github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
+github.com/google/pprof v0.0.0-20211214055906-6f57359322fd/go.mod h1:KgnwoLYCZ8IQu3XUZ8Nc/bM9CCZFOyjUNOSygVozoDg=
+github.com/google/pprof v0.0.0-20230705174524-200ffdc848b8 h1:n6vlPhxsA+BW/XsS5+uqi7GyzaLa5MH7qlSLBZtRdiA=
+github.com/google/pprof v0.0.0-20230705174524-200ffdc848b8/go.mod h1:Jh3hGz2jkYak8qXPD19ryItVnUgpgeqzdkY/D0EaeuA=
+github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
+github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
+github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
+github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
+github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g=
+github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
+github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
+github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
+github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
+github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
+github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
+github.com/ianlancetaylor/demangle v0.0.0-20210905161508-09a460cdf81d/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w=
+github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4=
+github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
+github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
+github.com/jdxcode/netrc v0.0.0-20221124155335-4616370d1a84 h1:2uT3aivO7NVpUPGcQX7RbHijHMyWix/yCnIrCWc+5co=
+github.com/jdxcode/netrc v0.0.0-20221124155335-4616370d1a84/go.mod h1:Zi/ZFkEqFHTm7qkjyNJjaWH4LQA9LQhGJyF0lTYGpxw=
+github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
+github.com/jhump/protoreflect v1.15.1 h1:HUMERORf3I3ZdX05WaQ6MIpd/NJ434hTp5YiKgfCL6c=
+github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
+github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
+github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
+github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
+github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
+github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
+github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
+github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
+github.com/klauspost/compress v1.16.7 h1:2mk3MPGNzKyxErAw8YaohYh69+pa4sIQSC0fPGCFR9I=
+github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
+github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU=
+github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
+github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
+github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
+github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
+github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
+github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
+github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
+github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
+github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
+github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
+github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
+github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=
+github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
+github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
+github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
+github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
+github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
+github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
+github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
+github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
+github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
+github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
+github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
+github.com/onsi/ginkgo/v2 v2.9.5 h1:+6Hr4uxzP4XIUyAkg61dWBw8lb/gc4/X5luuxN/EC+Q=
+github.com/onsi/gomega v1.27.7 h1:fVih9JD6ogIiHUN6ePK7HJidyEDpWGVB5mzM7cWNXoU=
+github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
+github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
+github.com/opencontainers/image-spec v1.1.0-rc4 h1:oOxKUJWnFC4YGHCCMNql1x4YaDfYBTS5Y4x/Cgeo1E0=
+github.com/opencontainers/image-spec v1.1.0-rc4/go.mod h1:X4pATf0uXsnn3g5aiGIsVnJBR4mxhKzfwmvK/B2NTm8=
+github.com/opencontainers/runtime-spec v1.1.0-rc.3.0.20230610073135-48415de180cf h1:AGnwZS8lmjGxN2/XlzORiYESAk7HOlE3XI37uhIP9Vw=
+github.com/opencontainers/runtime-spec v1.1.0-rc.3.0.20230610073135-48415de180cf/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=
+github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU=
+github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI=
+github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
+github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pkg/profile v1.7.0 h1:hnbDkaNWPCLMO9wGLdBFTIZvzDrDfBM2072E1S9gJkA=
+github.com/pkg/profile v1.7.0/go.mod h1:8Uer0jas47ZQMJ7VD+OHknK4YDY07LPUC6dEvqDjvNo=
+github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/prometheus/client_golang v1.15.1 h1:8tXpTmJbyH5lydzFPoxSIJ0J46jdh3tylbvM1xCv0LI=
+github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
+github.com/prometheus/client_model v0.4.0 h1:5lQXD3cAg1OXBf4Wq03gTrXHeaV0TQvGfUooCfx1yqY=
+github.com/prometheus/common v0.44.0 h1:+5BrQJwiBB9xsMygAB3TNvpQKOwlkc25LbISbrdOOfY=
+github.com/prometheus/procfs v0.10.0 h1:UkG7GPYkO4UZyLnyXjaWYcgOSONqwdBqFUT95ugmt6I=
+github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
+github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
+github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
+github.com/rs/cors v1.9.0 h1:l9HGsTsHJcvW14Nk7J9KFz8bzeAWXn3CG6bgt7LsrAE=
+github.com/rs/cors v1.9.0/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=
+github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
+github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
+github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
+github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
+github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
+github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
+github.com/spf13/afero v1.9.5 h1:stMpOSZFs//0Lv29HduCmli3GUfpFoF3Y1Q/aXj/wVM=
+github.com/spf13/afero v1.9.5/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ=
+github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I=
+github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0=
+github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
+github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
+github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
+github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
+github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
+github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
+github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
+github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
+github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
+github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
+github.com/tetratelabs/wazero v1.2.1 h1:J4X2hrGzJvt+wqltuvcSjHQ7ujQxA9gb6PeMs4qlUWs=
+github.com/tetratelabs/wazero v1.2.1/go.mod h1:wYx2gNRg8/WihJfSDxA1TIL8H+GkfLYm+bIfbblu9VQ=
+github.com/urfave/cli v1.22.12/go.mod h1:sSBEIC79qR6OvcmsD4U3KABeOTxDqQtdDnaFuUN30b8=
+github.com/vbatts/tar-split v0.11.3 h1:hLFqsOLQ1SsppQNTMpkpPXClLDfC2A3Zgy9OUU+RVck=
+github.com/vbatts/tar-split v0.11.3/go.mod h1:9QlHN18E+fEH7RdG+QAJJcuya3rqT7eXSTY7wGrAokY=
+github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
+github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
+github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
+github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
+go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
+go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
+go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
+go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
+go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
+go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
+go.opentelemetry.io/otel v1.16.0 h1:Z7GVAX/UkAXPKsy94IU+i6thsQS4nb7LviLpnaNeW8s=
+go.opentelemetry.io/otel v1.16.0/go.mod h1:vl0h9NUa1D5s1nv3A5vZOYWn8av4K8Ml6JDeHrT/bx4=
+go.opentelemetry.io/otel/metric v1.16.0 h1:RbrpwVG1Hfv85LgnZ7+txXioPDoh6EdbZHo26Q3hqOo=
+go.opentelemetry.io/otel/metric v1.16.0/go.mod h1:QE47cpOmkwipPiefDwo2wDzwJrlfxxNYodqc4xnGCo4=
+go.opentelemetry.io/otel/sdk v1.16.0 h1:Z1Ok1YsijYL0CSJpHt4cS3wDDh7p572grzNrBMiMWgE=
+go.opentelemetry.io/otel/sdk v1.16.0/go.mod h1:tMsIuKXuuIWPBAOrH+eHtvhTL+SntFtXF9QD68aP6p4=
+go.opentelemetry.io/otel/sdk/metric v0.39.0 h1:Kun8i1eYf48kHH83RucG93ffz0zGV1sh46FAScOTuDI=
+go.opentelemetry.io/otel/trace v1.16.0 h1:8JRpaObFoW0pxuVPapkgH8UhHQj+bJW8jJsCZEu5MQs=
+go.opentelemetry.io/otel/trace v1.16.0/go.mod h1:Yt9vYq1SdNz3xdjZZK7wcXv1qv2pwLkqr2QVwea0ef0=
+go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
+go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
+go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
+go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
+go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
+go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A=
+go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
+go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
+go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
+go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60=
+go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
+golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
+golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA=
+golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio=
+golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
+golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
+golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
+golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
+golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
+golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
+golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
+golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
+golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
+golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
+golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
+golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
+golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
+golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
+golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
+golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
+golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
+golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
+golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
+golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
+golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
+golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
+golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
+golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
+golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
+golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc=
+golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
+golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
+golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
+golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
+golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
+golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
+golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
+golang.org/x/net v0.13.0 h1:Nvo8UFsZ8X3BhAC9699Z1j7XQ3rsZnUUm7jfBEk1ueY=
+golang.org/x/net v0.13.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA=
+golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
+golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
+golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
+golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
+golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
+golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
+golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
+golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
+golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
+golang.org/x/oauth2 v0.8.0 h1:6dkIjl3j3LtZ/O3sTgZTMsLKSftL/B8Zgq4huOIIUu8=
+golang.org/x/oauth2 v0.8.0/go.mod h1:yr7u4HXZRm1R1kBWqr/xKNqewf0plRYoB7sla+BCIXE=
+golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
+golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
+golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220906165534-d0df966e6959/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM=
+golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/term v0.10.0 h1:3R7pNqamzBraeqj/Tj8qt1aQ2HpmlC+Cx/qL/7hn4/c=
+golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o=
+golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
+golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
+golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4=
+golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
+golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
+golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
+golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
+golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
+golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
+golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
+golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
+golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
+golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
+golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
+golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
+golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20200505023115-26f46d2f7ef8/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
+golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
+golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
+golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=
+golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
+golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
+golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
+golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
+golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
+golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
+golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
+golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
+golang.org/x/tools v0.11.0 h1:EMCa6U9S2LtZXLAMoWiR/R8dAQFRqbAitmbJ2UKhoi8=
+golang.org/x/tools v0.11.0/go.mod h1:anzJrxPjNtfgiYQYirP2CPGzGLxrH2u2QBhn6Bf3qY8=
+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+gomodules.xyz/jsonpatch/v2 v2.3.0 h1:8NFhfS6gzxNqjLIYnZxg319wZ5Qjnx4m/CcX+Klzazc=
+google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
+google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
+google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
+google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
+google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
+google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
+google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
+google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
+google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
+google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
+google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
+google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
+google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
+google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
+google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
+google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
+google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg=
+google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE=
+google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8=
+google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
+google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
+google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
+google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
+google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
+google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
+google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
+google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
+google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
+google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
+google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
+google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
+google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
+google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
+google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
+google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
+google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
+google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
+google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
+google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
+google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
+google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
+google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
+google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
+google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
+google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
+google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20220107163113-42d7afdf6368/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20230525234030-28d5490b6b19 h1:0nDDozoAU19Qb2HwhXadU8OcsiO/09cnTqhUtq2MEOM=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20230525234030-28d5490b6b19/go.mod h1:66JfowdXAEgad5O9NnYcsNPLCPZJD++2L9X0PCMODrA=
+google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
+google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
+google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
+google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
+google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
+google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
+google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
+google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
+google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
+google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
+google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
+google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
+google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
+google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0=
+google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
+google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8=
+google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
+google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
+google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
+google.golang.org/grpc v1.57.0 h1:kfzNeI/klCGD2YPMUlaGNT3pxvYfga7smW3Vth8Zsiw=
+google.golang.org/grpc v1.57.0/go.mod h1:Sd+9RMTACXwmub0zcNY2c4arhtrbBYD1AUHI/dt16Mo=
+google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.3.0 h1:rNBFJjBCOgVr9pWD7rs/knKL4FRTKgpZmsRfV214zcA=
+google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.3.0/go.mod h1:Dk1tviKTvMCz5tvh7t+fh94dhmQVHuCt2OzJB3CTW9Y=
+google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
+google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
+google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
+google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
+google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
+google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
+google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
+google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
+google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
+google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
+google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
+google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
+gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
+gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
+gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
+gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
+gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gotest.tools/v3 v3.1.0 h1:rVV8Tcg/8jHUkPUorwjaMTtemIMVXfIPKiOqnhEhakk=
+gotest.tools/v3 v3.1.0/go.mod h1:fHy7eyTmJFO5bQbUsEGQ1v4m2J3Jz9eWL54TP2/ZuYQ=
+honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
+honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
+honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
+k8s.io/api v0.27.3 h1:yR6oQXXnUEBWEWcvPWS0jQL575KoAboQPfJAuKNrw5Y=
+k8s.io/api v0.27.3/go.mod h1:C4BNvZnQOF7JA/0Xed2S+aUyJSfTGkGFxLXz9MnpIpg=
+k8s.io/apiextensions-apiserver v0.27.3 h1:xAwC1iYabi+TDfpRhxh4Eapl14Hs2OftM2DN5MpgKX4=
+k8s.io/apimachinery v0.27.3 h1:Ubye8oBufD04l9QnNtW05idcOe9Z3GQN8+7PqmuVcUM=
+k8s.io/apimachinery v0.27.3/go.mod h1:XNfZ6xklnMCOGGFNqXG7bUrQCoR04dh/E7FprV6pb+E=
+k8s.io/client-go v0.27.3 h1:7dnEGHZEJld3lYwxvLl7WoehK6lAq7GvgjxpA3nv1E8=
+k8s.io/client-go v0.27.3/go.mod h1:2MBEKuTo6V1lbKy3z1euEGnhPfGZLKTS9tiJ2xodM48=
+k8s.io/code-generator v0.28.0 h1:msdkRVJNVFgdiIJ8REl/d3cZsMB9HByFcWMmn13NyuE=
+k8s.io/code-generator v0.28.0/go.mod h1:ueeSJZJ61NHBa0ccWLey6mwawum25vX61nRZ6WOzN9A=
+k8s.io/component-base v0.27.3 h1:g078YmdcdTfrCE4fFobt7qmVXwS8J/3cI1XxRi/2+6k=
+k8s.io/gengo v0.0.0-20220902162205-c0856e24416d h1:U9tB195lKdzwqicbJvyJeOXV7Klv+wNAWENRnXEGi08=
+k8s.io/gengo v0.0.0-20220902162205-c0856e24416d/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E=
+k8s.io/klog/v2 v2.2.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y=
+k8s.io/klog/v2 v2.100.1 h1:7WCHKK6K8fNhTqfBhISHQ97KrnJNFZMcQvKp7gP/tmg=
+k8s.io/klog/v2 v2.100.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0=
+k8s.io/kube-openapi v0.0.0-20230717233707-2695361300d9 h1:LyMgNKD2P8Wn1iAwQU5OhxCKlKJy0sHc+PcDwFB24dQ=
+k8s.io/kube-openapi v0.0.0-20230717233707-2695361300d9/go.mod h1:wZK2AVp1uHCp4VamDVgBP2COHZjqD1T68Rf0CM3YjSM=
+k8s.io/utils v0.0.0-20230505201702-9f6742963106 h1:EObNQ3TW2D+WptiYXlApGNLVy0zm/JIBVY9i+M4wpAU=
+k8s.io/utils v0.0.0-20230505201702-9f6742963106/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
+kernel.org/pub/linux/libs/security/libcap/cap v1.2.69 h1:N0m3tKYbkRMmDobh/47ngz+AWeV7PcfXMDi8xu3Vrag=
+kernel.org/pub/linux/libs/security/libcap/cap v1.2.69/go.mod h1:Tk5Ip2TuxaWGpccL7//rAsLRH6RQ/jfqTGxuN/+i/FQ=
+kernel.org/pub/linux/libs/security/libcap/psx v1.2.69 h1:IdrOs1ZgwGw5CI+BH6GgVVlOt+LAXoPyh7enr8lfaXs=
+kernel.org/pub/linux/libs/security/libcap/psx v1.2.69/go.mod h1:+l6Ee2F59XiJ2I6WR5ObpC1utCQJZ/VLsEbQCD8RG24=
+rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
+rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
+rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
+sigs.k8s.io/controller-runtime v0.15.0 h1:ML+5Adt3qZnMSYxZ7gAverBLNPSMQEibtzAgp0UPojU=
+sigs.k8s.io/controller-runtime v0.15.0/go.mod h1:7ngYvp1MLT+9GeZ+6lH3LOlcHkp/+tzA/fmHa4iq9kk=
+sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo=
+sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0=
+sigs.k8s.io/structured-merge-diff/v4 v4.2.3 h1:PRbqxJClWWYMNV1dhaG4NsibJbArud9kFxnAMREiWFE=
+sigs.k8s.io/structured-merge-diff/v4 v4.2.3/go.mod h1:qjx8mGObPmV2aSZepjQjbmb2ihdVs8cGKBraizNC69E=
+sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc=
+sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo=
+sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8=
diff --git a/internal/function-runtime-oci/container.go b/internal/function-runtime-oci/container.go
new file mode 100644
index 0000000..ec58ec3
--- /dev/null
+++ b/internal/function-runtime-oci/container.go
@@ -0,0 +1,128 @@
+/*
+Copyright 2022 The Crossplane 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 function_runtime_oci
+
+import (
+ "io"
+ "net"
+
+ "google.golang.org/grpc"
+
+ "github.com/crossplane/crossplane-runtime/pkg/errors"
+ "github.com/crossplane/crossplane-runtime/pkg/logging"
+
+ "github.com/crossplane/function-runtime-oci/internal/proto/v1alpha1"
+)
+
+// Error strings.
+const (
+ errListen = "cannot listen for gRPC connections"
+ errServe = "cannot serve gRPC API"
+)
+
+const defaultCacheDir = "/function-runtime-oci"
+
+// An ContainerRunner runs a Composition Function packaged as an OCI image by
+// extracting it and running it as a 'rootless' container.
+type ContainerRunner struct {
+ v1alpha1.UnimplementedContainerizedFunctionRunnerServiceServer
+
+ log logging.Logger
+
+ rootUID int
+ rootGID int
+ setuid bool // Specifically, CAP_SETUID and CAP_SETGID.
+ cache string
+ registry string
+}
+
+// A ContainerRunnerOption configures a new ContainerRunner.
+type ContainerRunnerOption func(*ContainerRunner)
+
+// MapToRoot configures what UID and GID should map to root (UID/GID 0) in the
+// user namespace in which the function will be run.
+func MapToRoot(uid, gid int) ContainerRunnerOption {
+ return func(r *ContainerRunner) {
+ r.rootUID = uid
+ r.rootGID = gid
+ }
+}
+
+// SetUID indicates that the container runner should attempt operations that
+// require CAP_SETUID and CAP_SETGID, for example creating a user namespace that
+// maps arbitrary UIDs and GIDs to the parent namespace.
+func SetUID(s bool) ContainerRunnerOption {
+ return func(r *ContainerRunner) {
+ r.setuid = s
+ }
+}
+
+// WithCacheDir specifies the directory used for caching function images and
+// containers.
+func WithCacheDir(d string) ContainerRunnerOption {
+ return func(r *ContainerRunner) {
+ r.cache = d
+ }
+}
+
+// WithRegistry specifies the default registry used to retrieve function images and
+// containers.
+func WithRegistry(dr string) ContainerRunnerOption {
+ return func(r *ContainerRunner) {
+ r.registry = dr
+ }
+}
+
+// WithLogger configures which logger the container runner should use. Logging
+// is disabled by default.
+func WithLogger(l logging.Logger) ContainerRunnerOption {
+ return func(cr *ContainerRunner) {
+ cr.log = l
+ }
+}
+
+// NewContainerRunner returns a new Runner that runs functions as rootless
+// containers.
+func NewContainerRunner(o ...ContainerRunnerOption) *ContainerRunner {
+ r := &ContainerRunner{cache: defaultCacheDir, log: logging.NewNopLogger()}
+ for _, fn := range o {
+ fn(r)
+ }
+
+ return r
+}
+
+// ListenAndServe gRPC connections at the supplied address.
+func (r *ContainerRunner) ListenAndServe(network, address string) error {
+ r.log.Debug("Listening", "network", network, "address", address)
+ lis, err := net.Listen(network, address)
+ if err != nil {
+ return errors.Wrap(err, errListen)
+ }
+
+ // TODO(negz): Limit concurrent function runs?
+ srv := grpc.NewServer()
+ v1alpha1.RegisterContainerizedFunctionRunnerServiceServer(srv, r)
+ return errors.Wrap(srv.Serve(lis), errServe)
+}
+
+// Stdio can be used to read and write a command's standard I/O.
+type Stdio struct {
+ Stdin io.WriteCloser
+ Stdout io.ReadCloser
+ Stderr io.ReadCloser
+}
diff --git a/internal/function-runtime-oci/container_linux.go b/internal/function-runtime-oci/container_linux.go
new file mode 100644
index 0000000..dad3801
--- /dev/null
+++ b/internal/function-runtime-oci/container_linux.go
@@ -0,0 +1,185 @@
+//go:build linux
+
+/*
+Copyright 2022 The Crossplane 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 function_runtime_oci
+
+import (
+ "bytes"
+ "context"
+ "fmt"
+ "io"
+ "os"
+ "os/exec"
+ "syscall"
+
+ "google.golang.org/protobuf/proto"
+ "kernel.org/pub/linux/libs/security/libcap/cap"
+
+ "github.com/crossplane/crossplane-runtime/pkg/errors"
+
+ "github.com/crossplane/function-runtime-oci/internal/proto/v1alpha1"
+)
+
+// NOTE(negz): Technically _all_ of the containerized Composition Functions
+// implementation is only useful on Linux, but we want to support building what
+// we can on other operating systems (e.g. Darwin) to make it possible for folks
+// running them to ensure that code compiles and passes tests during
+// development. Avoid adding code to this file unless it actually needs Linux to
+// run.
+
+// Error strings.
+const (
+ errCreateStdioPipes = "cannot create stdio pipes"
+ errStartSpark = "cannot start " + spark
+ errCloseStdin = "cannot close stdin pipe"
+ errReadStdout = "cannot read from stdout pipe"
+ errReadStderr = "cannot read from stderr pipe"
+ errMarshalRequest = "cannot marshal RunFunctionRequest for " + spark
+ errWriteRequest = "cannot write RunFunctionRequest to " + spark + " stdin"
+ errUnmarshalResponse = "cannot unmarshal RunFunctionRequest from " + spark + " stdout"
+)
+
+// How many UIDs and GIDs to map from the parent to the child user namespace, if
+// possible. Doing so requires CAP_SETUID and CAP_SETGID.
+const (
+ UserNamespaceUIDs = 65536
+ UserNamespaceGIDs = 65536
+ MaxStdioBytes = 100 << 20 // 100 MB
+)
+
+// The subcommand of function-runtime-oci to invoke - i.e. "function-runtime-oci spark "
+const spark = "spark"
+
+// HasCapSetUID returns true if this process has CAP_SETUID.
+func HasCapSetUID() bool {
+ pc := cap.GetProc()
+ setuid, _ := pc.GetFlag(cap.Effective, cap.SETUID)
+ return setuid
+}
+
+// HasCapSetGID returns true if this process has CAP_SETGID.
+func HasCapSetGID() bool {
+ pc := cap.GetProc()
+ setgid, _ := pc.GetFlag(cap.Effective, cap.SETGID)
+ return setgid
+}
+
+// RunFunction runs a function as a rootless OCI container. Functions that
+// return non-zero, or that cannot be executed in the first place (e.g. because
+// they cannot be fetched from the registry) will return an error.
+func (r *ContainerRunner) RunFunction(ctx context.Context, req *v1alpha1.RunFunctionRequest) (*v1alpha1.RunFunctionResponse, error) {
+ r.log.Debug("Running function", "image", req.Image)
+
+ /*
+ We want to create an overlayfs with the cached rootfs as the lower layer
+ and the bundle's rootfs as the upper layer, if possible. Kernel 5.11 and
+ later supports using overlayfs inside a user (and mount) namespace. The
+ best way to run code in a user namespace in Go is to execute a separate
+ binary; the unix.Unshare syscall affects only one OS thread, and the Go
+ scheduler might move the goroutine to another.
+
+ Therefore we execute a shim - function-runtime-oci spark - in a new user
+ and mount namespace. spark fetches and caches the image, creates an OCI
+ runtime bundle, then executes an OCI runtime in order to actually
+ execute the function.
+ */
+ cmd := exec.CommandContext(ctx, os.Args[0], spark, "--cache-dir="+r.cache, "--registry="+r.registry, //nolint:gosec // We're intentionally executing with variable input.
+ fmt.Sprintf("--max-stdio-bytes=%d", MaxStdioBytes))
+ cmd.SysProcAttr = &syscall.SysProcAttr{
+ Cloneflags: syscall.CLONE_NEWUSER | syscall.CLONE_NEWNS,
+ UidMappings: []syscall.SysProcIDMap{{ContainerID: 0, HostID: r.rootUID, Size: 1}},
+ GidMappings: []syscall.SysProcIDMap{{ContainerID: 0, HostID: r.rootGID, Size: 1}},
+ }
+
+ // When we have CAP_SETUID and CAP_SETGID (i.e. typically when root), we can
+ // map a range of UIDs (0 to 65,336) inside the user namespace to a range in
+ // its parent. We can also drop privileges (in the parent user namespace) by
+ // running spark as root in the user namespace.
+ if r.setuid {
+ cmd.SysProcAttr.UidMappings = []syscall.SysProcIDMap{{ContainerID: 0, HostID: r.rootUID, Size: UserNamespaceUIDs}}
+ cmd.SysProcAttr.GidMappings = []syscall.SysProcIDMap{{ContainerID: 0, HostID: r.rootGID, Size: UserNamespaceGIDs}}
+ cmd.SysProcAttr.GidMappingsEnableSetgroups = true
+
+ /*
+ UID and GID 0 here are relative to the new user namespace - i.e. they
+ correspond to HostID in the parent. We're able to do this because
+ Go's exec.Command will:
+
+ 1. Call clone(2) to create a child process in a new user namespace.
+ 2. In the child process, wait for /proc/self/uid_map to be written.
+ 3. In the parent process, write the child's /proc/$pid/uid_map.
+ 4. In the child process, call setuid(2) and setgid(2) per Credential.
+ 5. In the child process, call execve(2) to execute spark.
+
+ Per user_namespaces(7) the child process created by clone(2) starts
+ out with a complete set of capabilities in the new user namespace
+ until the call to execve(2) causes them to be recalculated. This
+ includes the CAP_SETUID and CAP_SETGID necessary to become UID 0 in
+ the child user namespace, effectively dropping privileges to UID
+ 100000 in the parent user namespace.
+
+ https://github.com/golang/go/blob/1b03568/src/syscall/exec_linux.go#L446
+ */
+ cmd.SysProcAttr.Credential = &syscall.Credential{Uid: 0, Gid: 0}
+ }
+
+ stdio, err := StdioPipes(cmd, r.rootUID, r.rootGID)
+ if err != nil {
+ return nil, errors.Wrap(err, errCreateStdioPipes)
+ }
+
+ b, err := proto.Marshal(req)
+ if err != nil {
+ return nil, errors.Wrap(err, errMarshalRequest)
+ }
+ if err := cmd.Start(); err != nil {
+ return nil, errors.Wrap(err, errStartSpark)
+ }
+ if _, err := stdio.Stdin.Write(b); err != nil {
+ return nil, errors.Wrap(err, errWriteRequest)
+ }
+
+ // Closing the write end of the stdio pipe will cause the read end to return
+ // EOF. This is necessary to avoid a function blocking forever while reading
+ // from stdin.
+ if err := stdio.Stdin.Close(); err != nil {
+ return nil, errors.Wrap(err, errCloseStdin)
+ }
+
+ // We must read all of stdout and stderr before calling cmd.Wait, which
+ // closes the underlying pipes.
+ // Limited to MaxStdioBytes to avoid OOMing if the function writes a lot of
+ // data to stdout or stderr.
+ stdout, err := io.ReadAll(io.LimitReader(stdio.Stdout, MaxStdioBytes))
+ if err != nil {
+ return nil, errors.Wrap(err, errReadStdout)
+ }
+
+ stderr, err := io.ReadAll(io.LimitReader(stdio.Stderr, MaxStdioBytes))
+ if err != nil {
+ return nil, errors.Wrap(err, errReadStderr)
+ }
+
+ if err := cmd.Wait(); err != nil {
+ // TODO(negz): Handle stderr being too long to be a useful error.
+ return nil, errors.Errorf("%w: %s", err, bytes.TrimSuffix(stderr, []byte("\n")))
+ }
+
+ rsp := &v1alpha1.RunFunctionResponse{}
+ return rsp, errors.Wrap(proto.Unmarshal(stdout, rsp), errUnmarshalResponse)
+}
diff --git a/internal/function-runtime-oci/container_nonlinux.go b/internal/function-runtime-oci/container_nonlinux.go
new file mode 100644
index 0000000..680701c
--- /dev/null
+++ b/internal/function-runtime-oci/container_nonlinux.go
@@ -0,0 +1,40 @@
+//go:build !linux
+
+/*
+Copyright 2022 The Crossplane 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 function_runtime_oci
+
+import (
+ "context"
+
+ "github.com/crossplane/crossplane-runtime/pkg/errors"
+
+ "github.com/crossplane/function-runtime-oci/internal/proto/v1alpha1"
+)
+
+const errLinuxOnly = "containerized functions are only supported on Linux"
+
+// HasCapSetUID returns false on non-Linux.
+func HasCapSetUID() bool { return false }
+
+// HasCapSetGID returns false on non-Linux.
+func HasCapSetGID() bool { return false }
+
+// RunFunction returns an error on non-Linux.
+func (r *ContainerRunner) RunFunction(_ context.Context, _ *v1alpha1.RunFunctionRequest) (*v1alpha1.RunFunctionResponse, error) {
+ return nil, errors.New(errLinuxOnly)
+}
diff --git a/internal/function-runtime-oci/container_nonunix.go b/internal/function-runtime-oci/container_nonunix.go
new file mode 100644
index 0000000..c77c298
--- /dev/null
+++ b/internal/function-runtime-oci/container_nonunix.go
@@ -0,0 +1,30 @@
+//go:build !unix
+
+/*
+Copyright 2022 The Crossplane 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 function_runtime_oci
+
+import (
+ "os/exec"
+
+ "github.com/crossplane/crossplane-runtime/pkg/errors"
+)
+
+// StdioPipes returns an error on non-Linux.
+func StdioPipes(cmd *exec.Cmd, uid, gid int) (*Stdio, error) {
+ return nil, errors.New(errLinuxOnly)
+}
diff --git a/internal/function-runtime-oci/container_unix.go b/internal/function-runtime-oci/container_unix.go
new file mode 100644
index 0000000..5b2fa22
--- /dev/null
+++ b/internal/function-runtime-oci/container_unix.go
@@ -0,0 +1,77 @@
+//go:build unix
+
+/*
+Copyright 2022 The Crossplane 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 function_runtime_oci
+
+import (
+ "os/exec"
+ "syscall"
+
+ "github.com/crossplane/crossplane-runtime/pkg/errors"
+)
+
+// NOTE(negz): We build this function for unix so that folks running (e.g.)
+// Darwin can build and test the code, even though it's only really useful for
+// Linux systems.
+
+// Error strings.
+const (
+ errCreateStdinPipe = "cannot create stdin pipe"
+ errCreateStdoutPipe = "cannot create stdout pipe"
+ errCreateStderrPipe = "cannot create stderr pipe"
+ errChownFd = "cannot chown file descriptor"
+)
+
+// StdioPipes creates and returns pipes that will be connected to the supplied
+// command's stdio when it starts. It calls fchown(2) to ensure all pipes are
+// owned by the supplied user and group ID; this ensures that the command can
+// read and write its stdio even when function-runtime-oci is running as root
+// (in the parent namespace) and the command is not.
+func StdioPipes(cmd *exec.Cmd, uid, gid int) (*Stdio, error) {
+ stdin, err := cmd.StdinPipe()
+ if err != nil {
+ return nil, errors.Wrap(err, errCreateStdinPipe)
+ }
+ stdout, err := cmd.StdoutPipe()
+ if err != nil {
+ return nil, errors.Wrap(err, errCreateStdoutPipe)
+ }
+ stderr, err := cmd.StderrPipe()
+ if err != nil {
+ return nil, errors.Wrap(err, errCreateStderrPipe)
+ }
+
+ // StdinPipe and friends above return "our end" of the pipe - i.e. stdin is
+ // the io.WriteCloser we can use to write to the command's stdin. They also
+ // setup the "command's end" of the pipe - i.e. cmd.Stdin is the io.Reader
+ // the command can use to read its stdin. In all cases these pipes _should_
+ // be *os.Files.
+ for _, s := range []any{stdin, stdout, stderr, cmd.Stdin, cmd.Stdout, cmd.Stderr} {
+ f, ok := s.(interface{ Fd() uintptr })
+ if !ok {
+ return nil, errors.Errorf("stdio pipe (type: %T) missing required Fd() method", f)
+ }
+ // We only build this file on unix because Fchown does not take an
+ // integer fd on Windows.
+ if err := syscall.Fchown(int(f.Fd()), uid, gid); err != nil {
+ return nil, errors.Wrap(err, errChownFd)
+ }
+ }
+
+ return &Stdio{Stdin: stdin, Stdout: stdout, Stderr: stderr}, nil
+}
diff --git a/internal/function-runtime-oci/doc.go b/internal/function-runtime-oci/doc.go
new file mode 100644
index 0000000..4dfe0c2
--- /dev/null
+++ b/internal/function-runtime-oci/doc.go
@@ -0,0 +1,19 @@
+/*
+Copyright 2022 The Crossplane 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 function-runtime-oci is the reference implementation of Composition
+// Functions.
+package function_runtime_oci
diff --git a/internal/oci/certs.go b/internal/oci/certs.go
new file mode 100644
index 0000000..d1e3d2d
--- /dev/null
+++ b/internal/oci/certs.go
@@ -0,0 +1,48 @@
+/*
+Copyright 2023 The Crossplane 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 oci
+
+import (
+ "crypto/x509"
+ "os"
+ "path/filepath"
+
+ "github.com/crossplane/crossplane-runtime/pkg/errors"
+)
+
+// ParseCertificatesFromPath parses PEM file containing extra x509
+// certificates(s) and combines them with the built in root CA CertPool.
+func ParseCertificatesFromPath(path string) (*x509.CertPool, error) {
+ // Get the SystemCertPool, continue with an empty pool on error
+ rootCAs, _ := x509.SystemCertPool()
+ if rootCAs == nil {
+ rootCAs = x509.NewCertPool()
+ }
+
+ // Read in the cert file
+ certs, err := os.ReadFile(filepath.Clean(path))
+ if err != nil {
+ return nil, errors.Wrapf(err, "Failed to append %q to RootCAs", path)
+ }
+
+ // Append our cert to the system pool
+ if ok := rootCAs.AppendCertsFromPEM(certs); !ok {
+ return nil, errors.Errorf("No certificates could be parsed from %q", path)
+ }
+
+ return rootCAs, nil
+}
diff --git a/internal/oci/doc.go b/internal/oci/doc.go
new file mode 100644
index 0000000..03b63ad
--- /dev/null
+++ b/internal/oci/doc.go
@@ -0,0 +1,19 @@
+/*
+Copyright 2022 The Crossplane 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 oci contains functionality for working with Open Container Initiative
+// (OCI) images and containers.
+package oci
diff --git a/internal/oci/layer/layer.go b/internal/oci/layer/layer.go
new file mode 100644
index 0000000..421ca67
--- /dev/null
+++ b/internal/oci/layer/layer.go
@@ -0,0 +1,342 @@
+/*
+Copyright 2022 The Crossplane 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 layer extracts OCI image layer tarballs.
+package layer
+
+import (
+ "archive/tar"
+ "context"
+ "io"
+ "io/fs"
+ "os"
+ "path/filepath"
+ "strings"
+
+ securejoin "github.com/cyphar/filepath-securejoin"
+
+ "github.com/crossplane/crossplane-runtime/pkg/errors"
+)
+
+// Error strings.
+const (
+ errAdvanceTarball = "cannot advance to next entry in tarball"
+ errExtractTarHeader = "cannot extract tar header"
+ errEvalSymlinks = "cannot evaluate symlinks"
+ errMkdir = "cannot make directory"
+ errLstat = "cannot lstat directory"
+ errChmod = "cannot chmod path"
+ errSymlink = "cannot create symlink"
+ errOpenFile = "cannot open file"
+ errCopyFile = "cannot copy file"
+ errCloseFile = "cannot close file"
+
+ errFmtHandleTarHeader = "cannot handle tar header for %q"
+ errFmtWhiteoutFile = "cannot whiteout file %q"
+ errFmtWhiteoutDir = "cannot whiteout opaque directory %q"
+ errFmtUnsupportedType = "tarball contained header %q with unknown type %q"
+ errFmtNotDir = "path %q exists but is not a directory"
+ errFmtSize = "wrote %d bytes to %q; expected %d"
+)
+
+// OCI whiteouts.
+// See https://github.com/opencontainers/image-spec/blob/v1.0/layer.md#whiteouts
+const (
+ ociWhiteoutPrefix = ".wh."
+ ociWhiteoutMetaPrefix = ociWhiteoutPrefix + ociWhiteoutPrefix
+ ociWhiteoutOpaqueDir = ociWhiteoutMetaPrefix + ".opq"
+)
+
+// A HeaderHandler handles a single file (header) within a tarball.
+type HeaderHandler interface {
+ // Handle the supplied tarball header by applying it to the supplied path,
+ // e.g. creating a file, directory, etc. The supplied io.Reader is expected
+ // to be a tarball advanced to the supplied header, i.e. via tr.Next().
+ Handle(h *tar.Header, tr io.Reader, path string) error
+}
+
+// A HeaderHandlerFn is a function that acts as a HeaderHandler.
+type HeaderHandlerFn func(h *tar.Header, tr io.Reader, path string) error
+
+// Handle the supplied tarball header.
+func (fn HeaderHandlerFn) Handle(h *tar.Header, tr io.Reader, path string) error {
+ return fn(h, tr, path)
+}
+
+// A StackingExtractor is a Extractor that extracts an OCI layer by
+// 'stacking' it atop the supplied root directory.
+type StackingExtractor struct {
+ h HeaderHandler
+}
+
+// NewStackingExtractor extracts an OCI layer by 'stacking' it atop the
+// supplied root directory.
+func NewStackingExtractor(h HeaderHandler) *StackingExtractor {
+ return &StackingExtractor{h: h}
+}
+
+// Apply calls the StackingExtractor's HeaderHandler for each file in the
+// supplied layer tarball, adjusting their path to be rooted under the supplied
+// root directory. That is, /foo would be extracted to /bar as /bar/foo.
+func (e *StackingExtractor) Apply(ctx context.Context, tb io.Reader, root string) error {
+ tr := tar.NewReader(tb)
+ for {
+ select {
+ case <-ctx.Done():
+ return ctx.Err()
+ default:
+ }
+ hdr, err := tr.Next()
+ if errors.Is(err, io.EOF) {
+ break
+ }
+ if err != nil {
+ return errors.Wrap(err, errAdvanceTarball)
+ }
+
+ // SecureJoin joins hdr.Name to root, ensuring the resulting path does
+ // not escape root either syntactically (via "..") or via symlinks in
+ // the path. For example:
+ //
+ // * Joining "/a" and "../etc/passwd" results in "/a/etc/passwd".
+ // * Joining "/a" and "evil/passwd" where "/a/evil" exists and is a
+ // symlink to "/etc" results in "/a/etc/passwd".
+ //
+ // https://codeql.github.com/codeql-query-help/go/go-unsafe-unzip-symlink/
+ path, err := securejoin.SecureJoin(root, hdr.Name)
+ if err != nil {
+ return errors.Wrap(err, errEvalSymlinks)
+ }
+
+ if err := e.h.Handle(hdr, tr, path); err != nil {
+ return errors.Wrapf(err, errFmtHandleTarHeader, hdr.Name)
+ }
+ }
+
+ // TODO(negz): Handle MAC times for directories. This needs to be done last,
+ // since mutating a directory's contents will update its MAC times.
+
+ return nil
+}
+
+// A WhiteoutHandler handles OCI whiteouts by deleting the corresponding files.
+// It passes anything that is not a whiteout to an underlying HeaderHandler. It
+// avoids deleting any file created by the underling HeaderHandler.
+type WhiteoutHandler struct {
+ wrapped HeaderHandler
+ handled map[string]bool
+}
+
+// NewWhiteoutHandler returns a HeaderHandler that handles OCI whiteouts by
+// deleting the corresponding files.
+func NewWhiteoutHandler(hh HeaderHandler) *WhiteoutHandler {
+ return &WhiteoutHandler{wrapped: hh, handled: make(map[string]bool)}
+}
+
+// Handle the supplied tar header.
+func (w *WhiteoutHandler) Handle(h *tar.Header, tr io.Reader, path string) error {
+ // If this isn't a whiteout file, extract it.
+ if !strings.HasPrefix(filepath.Base(path), ociWhiteoutPrefix) {
+ w.handled[path] = true
+ return w.wrapped.Handle(h, tr, path)
+ }
+
+ // We must only whiteout files from previous layers; i.e. not files that
+ // we've extracted from this layer. We're operating on a merged overlayfs,
+ // so we can't rely on the filesystem to distinguish what files are from a
+ // previous layer. Instead we track which files we've extracted from this
+ // layer and avoid whiting-out any file we've extracted. It's possible we'll
+ // see a whiteout out-of-order; i.e. we'll whiteout /foo, then later extract
+ // /foo from the same layer. This should be fine; we'll delete it, then
+ // recreate it, resulting in the desired file in our overlayfs upper dir.
+ // https://github.com/opencontainers/image-spec/blob/v1.0/layer.md#whiteouts
+
+ base := filepath.Base(path)
+ dir := filepath.Dir(path)
+
+ // Handle explicit whiteout files. These files resolve to an explicit path
+ // that should be deleted from the current layer.
+ if base != ociWhiteoutOpaqueDir {
+ whiteout := filepath.Join(dir, base[len(ociWhiteoutPrefix):])
+
+ if w.handled[whiteout] {
+ return nil
+ }
+
+ return errors.Wrapf(os.RemoveAll(whiteout), errFmtWhiteoutFile, whiteout)
+ }
+
+ // Handle an opaque directory. These files indicate that all siblings in
+ // their directory should be deleted from the current layer.
+ err := filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error {
+ if errors.Is(err, os.ErrNotExist) {
+ // Either this path is under a directory we already deleted or we've
+ // been asked to whiteout a directory that doesn't exist.
+ return nil
+ }
+ if err != nil {
+ return err
+ }
+
+ // Don't delete the directory we're whiting out, or a file we've
+ // extracted from this layer.
+ if path == dir || w.handled[path] {
+ return nil
+ }
+
+ return os.RemoveAll(path)
+ })
+
+ return errors.Wrapf(err, errFmtWhiteoutDir, dir)
+}
+
+// An ExtractHandler extracts from a tarball per the supplied tar header by
+// calling a handler that knows how to extract the type of file.
+type ExtractHandler struct {
+ handler map[byte]HeaderHandler
+}
+
+// NewExtractHandler returns a HeaderHandler that extracts from a tarball per
+// the supplied tar header by calling a handler that knows how to extract the
+// type of file.
+func NewExtractHandler() *ExtractHandler {
+ return &ExtractHandler{handler: map[byte]HeaderHandler{
+ tar.TypeDir: HeaderHandlerFn(ExtractDir),
+ tar.TypeSymlink: HeaderHandlerFn(ExtractSymlink),
+ tar.TypeReg: HeaderHandlerFn(ExtractFile),
+ tar.TypeFifo: HeaderHandlerFn(ExtractFIFO),
+
+ // TODO(negz): Don't extract hard links as symlinks. Creating an actual
+ // hard link would require us to securely join the path of the 'root'
+ // directory we're untarring into with h.Linkname, but we don't
+ // currently plumb the root directory down to this level.
+ tar.TypeLink: HeaderHandlerFn(ExtractSymlink),
+ }}
+}
+
+// Handle creates a file at the supplied path per the supplied tar header.
+func (e *ExtractHandler) Handle(h *tar.Header, tr io.Reader, path string) error {
+ // ExtractDir should correct these permissions.
+ if err := os.MkdirAll(filepath.Dir(path), 0750); err != nil {
+ return errors.Wrap(err, errMkdir)
+ }
+
+ hd, ok := e.handler[h.Typeflag]
+ if !ok {
+ // Better to return an error than to write a partial layer. Note that
+ // tar.TypeBlock and tar.TypeChar in particular are unsupported because
+ // they can't be created without CAP_MKNOD in the 'root' user namespace
+ // per https://man7.org/linux/man-pages/man7/user_namespaces.7.html
+ return errors.Errorf(errFmtUnsupportedType, h.Name, h.Typeflag)
+ }
+
+ if err := hd.Handle(h, tr, path); err != nil {
+ return errors.Wrap(err, errExtractTarHeader)
+ }
+
+ // We expect to have CAP_CHOWN (inside a user namespace) when running
+ // this code, but if that namespace was created by a user without
+ // CAP_SETUID and CAP_SETGID only one UID and GID (root) will exist and
+ // we'll get syscall.EINVAL if we try to chown to any other. We ignore
+ // this error and attempt to run the function regardless; functions that
+ // run 'as root' (in their namespace) should work fine.
+
+ // TODO(negz): Return this error if it isn't syscall.EINVAL? Currently
+ // doing so would require taking a dependency on the syscall package per
+ // https://groups.google.com/g/golang-nuts/c/BpWN9N-hw3s.
+ _ = os.Lchown(path, h.Uid, h.Gid)
+
+ // TODO(negz): Handle MAC times.
+
+ return nil
+}
+
+// ExtractDir is a HeaderHandler that creates a directory at the supplied path
+// per the supplied tar header.
+func ExtractDir(h *tar.Header, _ io.Reader, path string) error {
+ mode := h.FileInfo().Mode()
+ fi, err := os.Lstat(path)
+ if errors.Is(err, os.ErrNotExist) {
+ return errors.Wrap(os.MkdirAll(path, mode.Perm()), errMkdir)
+ }
+ if err != nil {
+ return errors.Wrap(err, errLstat)
+ }
+
+ if !fi.IsDir() {
+ return errors.Errorf(errFmtNotDir, path)
+ }
+
+ // We've been asked to extract a directory that exists; just try to ensure
+ // it has the correct permissions. It could be that we saw a file in this
+ // directory before we saw the directory itself, and created it with the
+ // file's permissions in a MkdirAll call.
+ return errors.Wrap(os.Chmod(path, mode.Perm()), errChmod)
+}
+
+// ExtractSymlink is a HeaderHandler that creates a symlink at the supplied path
+// per the supplied tar header.
+func ExtractSymlink(h *tar.Header, _ io.Reader, path string) error {
+ // We don't sanitize h.LinkName (the symlink's target). It will be sanitized
+ // by SecureJoin above to prevent malicious writes during the untar process,
+ // and will be evaluated relative to root during function execution.
+ return errors.Wrap(os.Symlink(h.Linkname, path), errSymlink)
+}
+
+// ExtractFile is a HeaderHandler that creates a regular file at the supplied
+// path per the supplied tar header.
+func ExtractFile(h *tar.Header, tr io.Reader, path string) error {
+ mode := h.FileInfo().Mode()
+
+ //nolint:gosec // The root of this path is user supplied input.
+ dst, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, mode)
+ if err != nil {
+ return errors.Wrap(err, errOpenFile)
+ }
+
+ n, err := copyChunks(dst, tr, 1024*1024) // Copy in 1MB chunks.
+ if err != nil {
+ _ = dst.Close()
+ return errors.Wrap(err, errCopyFile)
+ }
+ if err := dst.Close(); err != nil {
+ return errors.Wrap(err, errCloseFile)
+ }
+ if n != h.Size {
+ return errors.Errorf(errFmtSize, n, path, h.Size)
+ }
+ return nil
+}
+
+// copyChunks pleases gosec per https://github.com/securego/gosec/pull/433.
+// Like Copy it reads from src until EOF, it does not treat an EOF from Read as
+// an error to be reported.
+//
+// NOTE(negz): This rule confused me at first because io.Copy appears to use a
+// buffer, but in fact it bypasses it if src/dst is an io.WriterTo/ReaderFrom.
+func copyChunks(dst io.Writer, src io.Reader, chunkSize int64) (int64, error) {
+ var written int64
+ for {
+ w, err := io.CopyN(dst, src, chunkSize)
+ written += w
+ if errors.Is(err, io.EOF) {
+ return written, nil
+ }
+ if err != nil {
+ return written, err
+ }
+ }
+}
diff --git a/internal/oci/layer/layer_nonunix.go b/internal/oci/layer/layer_nonunix.go
new file mode 100644
index 0000000..82f26ee
--- /dev/null
+++ b/internal/oci/layer/layer_nonunix.go
@@ -0,0 +1,31 @@
+//go:build !unix
+
+/*
+Copyright 2022 The Crossplane 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 layer
+
+import (
+ "archive/tar"
+ "io"
+
+ "github.com/crossplane/crossplane-runtime/pkg/errors"
+)
+
+// ExtractFIFO returns an error on non-Unix systems
+func ExtractFIFO(_ *tar.Header, _ io.Reader, _ string) error {
+ return errors.New("FIFOs are only supported on Unix")
+}
diff --git a/internal/oci/layer/layer_test.go b/internal/oci/layer/layer_test.go
new file mode 100644
index 0000000..fdb0e0a
--- /dev/null
+++ b/internal/oci/layer/layer_test.go
@@ -0,0 +1,466 @@
+/*
+Copyright 2022 The Crossplane 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 layer
+
+import (
+ "archive/tar"
+ "bytes"
+ "context"
+ "io"
+ "os"
+ "path/filepath"
+ "testing"
+
+ "github.com/google/go-cmp/cmp"
+
+ "github.com/crossplane/crossplane-runtime/pkg/errors"
+ "github.com/crossplane/crossplane-runtime/pkg/test"
+)
+
+type MockHandler struct{ err error }
+
+func (h *MockHandler) Handle(_ *tar.Header, _ io.Reader, _ string) error {
+ return h.err
+}
+
+func TestStackingExtractor(t *testing.T) {
+ errBoom := errors.New("boom")
+ coolFile := "/cool/file"
+ cancelled, cancel := context.WithCancel(context.Background())
+ cancel()
+
+ type args struct {
+ ctx context.Context
+ tb io.Reader
+ root string
+ }
+ cases := map[string]struct {
+ reason string
+ e *StackingExtractor
+ args args
+ want error
+ }{
+ "ContextDone": {
+ reason: "If the supplied context is done we should return its error.",
+ e: NewStackingExtractor(&MockHandler{}),
+ args: args{
+ ctx: cancelled,
+ },
+ want: cancelled.Err(),
+ },
+ "NotATarball": {
+ reason: "If the supplied io.Reader is not a tarball we should return an error.",
+ e: NewStackingExtractor(&MockHandler{}),
+ args: args{
+ ctx: context.Background(),
+ tb: func() io.Reader {
+ b := &bytes.Buffer{}
+ _, _ = b.WriteString("hi!")
+ return b
+ }(),
+ },
+ want: errors.Wrap(errors.New("unexpected EOF"), errAdvanceTarball),
+ },
+ "ErrorHandlingHeader": {
+ reason: "If our HeaderHandler returns an error we should surface it.",
+ e: NewStackingExtractor(&MockHandler{err: errBoom}),
+ args: args{
+ ctx: context.Background(),
+ tb: func() io.Reader {
+ b := &bytes.Buffer{}
+ tb := tar.NewWriter(b)
+ tb.WriteHeader(&tar.Header{
+ Typeflag: tar.TypeReg,
+ Name: coolFile,
+ })
+ _, _ = io.WriteString(tb, "hi!")
+ tb.Close()
+ return b
+ }(),
+ },
+ want: errors.Wrapf(errBoom, errFmtHandleTarHeader, coolFile),
+ },
+ "Success": {
+ reason: "If we successfully extract our tarball we should return a nil error.",
+ e: NewStackingExtractor(&MockHandler{}),
+ args: args{
+ ctx: context.Background(),
+ tb: func() io.Reader {
+ b := &bytes.Buffer{}
+ tb := tar.NewWriter(b)
+ tb.WriteHeader(&tar.Header{
+ Typeflag: tar.TypeReg,
+ Name: coolFile,
+ })
+ _, _ = io.WriteString(tb, "hi!")
+ tb.Close()
+ return b
+ }(),
+ },
+ want: nil,
+ },
+ }
+
+ for name, tc := range cases {
+ t.Run(name, func(t *testing.T) {
+ err := tc.e.Apply(tc.args.ctx, tc.args.tb, tc.args.root)
+ if diff := cmp.Diff(tc.want, err, test.EquateErrors()); diff != "" {
+ t.Errorf("\n%s\ne.Apply(...): -want error, +got error:\n%s", tc.reason, diff)
+ }
+ })
+ }
+}
+
+func TestWhiteoutHandler(t *testing.T) {
+ errBoom := errors.New("boom")
+
+ tmp, _ := os.MkdirTemp(os.TempDir(), t.Name())
+ defer os.RemoveAll(tmp)
+
+ coolDir := filepath.Join(tmp, "cool")
+ coolFile := filepath.Join(coolDir, "file")
+ coolWhiteout := filepath.Join(coolDir, ociWhiteoutPrefix+"file")
+ _ = os.MkdirAll(coolDir, 0700)
+
+ opaqueDir := filepath.Join(tmp, "opaque")
+ opaqueDirWhiteout := filepath.Join(opaqueDir, ociWhiteoutOpaqueDir)
+ _ = os.MkdirAll(opaqueDir, 0700)
+ f, _ := os.Create(filepath.Join(opaqueDir, "some-file"))
+ f.Close()
+
+ nonExistentDirWhiteout := filepath.Join(tmp, "non-exist", ociWhiteoutOpaqueDir)
+
+ type args struct {
+ h *tar.Header
+ tr io.Reader
+ path string
+ }
+ cases := map[string]struct {
+ reason string
+ h HeaderHandler
+ args args
+ want error
+ }{
+ "NotAWhiteout": {
+ reason: "Files that aren't whiteouts should be passed to the underlying handler.",
+ h: NewWhiteoutHandler(&MockHandler{err: errBoom}),
+ args: args{
+ path: coolFile,
+ },
+ want: errBoom,
+ },
+ "HeaderAlreadyHandled": {
+ reason: "We shouldn't whiteout a file that was already handled.",
+ h: func() HeaderHandler {
+ w := NewWhiteoutHandler(&MockHandler{})
+ _ = w.Handle(nil, nil, coolFile) // Handle the file we'll try to whiteout.
+ return w
+ }(),
+ args: args{
+ path: coolWhiteout,
+ },
+ want: nil,
+ },
+ "WhiteoutFile": {
+ reason: "We should delete a whited-out file.",
+ h: NewWhiteoutHandler(&MockHandler{}),
+ args: args{
+ path: filepath.Join(tmp, coolWhiteout),
+ },
+ // os.RemoveAll won't return an error even if this doesn't exist.
+ want: nil,
+ },
+ "OpaqueDirDoesNotExist": {
+ reason: "We should return early if asked to whiteout a directory that doesn't exist.",
+ h: NewWhiteoutHandler(&MockHandler{}),
+ args: args{
+ path: nonExistentDirWhiteout,
+ },
+ want: nil,
+ },
+ "WhiteoutOpaqueDir": {
+ reason: "We should whiteout all files in an opaque directory.",
+ h: NewWhiteoutHandler(&MockHandler{}),
+ args: args{
+ path: opaqueDirWhiteout,
+ },
+ want: nil,
+ },
+ }
+
+ for name, tc := range cases {
+ t.Run(name, func(t *testing.T) {
+ err := tc.h.Handle(tc.args.h, tc.args.tr, tc.args.path)
+ if diff := cmp.Diff(tc.want, err, test.EquateErrors()); diff != "" {
+ t.Errorf("\n%s\nh.Handle(...): -want error, +got error:\n%s", tc.reason, diff)
+ }
+ })
+ }
+}
+
+func TestExtractHandler(t *testing.T) {
+ errBoom := errors.New("boom")
+
+ tmp, _ := os.MkdirTemp(os.TempDir(), t.Name())
+ defer os.RemoveAll(tmp)
+
+ coolDir := filepath.Join(tmp, "cool")
+ coolFile := filepath.Join(coolDir, "file")
+
+ type args struct {
+ h *tar.Header
+ tr io.Reader
+ path string
+ }
+ cases := map[string]struct {
+ reason string
+ h HeaderHandler
+ args args
+ want error
+ }{
+ "UnsupportedMode": {
+ reason: "Handling an unsupported file type should return an error.",
+ h: &ExtractHandler{handler: map[byte]HeaderHandler{}},
+ args: args{
+ h: &tar.Header{
+ Typeflag: tar.TypeReg,
+ Name: coolFile,
+ },
+ },
+ want: errors.Errorf(errFmtUnsupportedType, coolFile, tar.TypeReg),
+ },
+ "HandlerError": {
+ reason: "Errors from an underlying handler should be returned.",
+ h: &ExtractHandler{handler: map[byte]HeaderHandler{
+ tar.TypeReg: &MockHandler{err: errBoom},
+ }},
+ args: args{
+ h: &tar.Header{
+ Typeflag: tar.TypeReg,
+ Name: coolFile,
+ },
+ },
+ want: errors.Wrap(errBoom, errExtractTarHeader),
+ },
+ "Success": {
+ reason: "If the underlying handler works we should return a nil error.",
+ h: &ExtractHandler{handler: map[byte]HeaderHandler{
+ tar.TypeReg: &MockHandler{},
+ }},
+ args: args{
+ h: &tar.Header{
+ Typeflag: tar.TypeReg,
+
+ // We don't currently check the return value of Lchown, but
+ // this will increase the chances it works by ensuring we
+ // try to chown to our own UID/GID.
+ Uid: os.Getuid(),
+ Gid: os.Getgid(),
+ },
+ path: coolFile,
+ },
+ want: nil,
+ },
+ }
+
+ for name, tc := range cases {
+ t.Run(name, func(t *testing.T) {
+ err := tc.h.Handle(tc.args.h, tc.args.tr, tc.args.path)
+ if diff := cmp.Diff(tc.want, err, test.EquateErrors()); diff != "" {
+ t.Errorf("\n%s\nh.Handle(...): -want error, +got error:\n%s", tc.reason, diff)
+ }
+ })
+ }
+}
+
+func TestExtractDir(t *testing.T) {
+ tmp, _ := os.MkdirTemp(os.TempDir(), t.Name())
+ defer os.RemoveAll(tmp)
+
+ newDir := filepath.Join(tmp, "new")
+ existingDir := filepath.Join(tmp, "existing-dir")
+ existingFile := filepath.Join(tmp, "existing-file")
+ _ = os.MkdirAll(existingDir, 0700)
+ f, _ := os.Create(existingFile)
+ f.Close()
+
+ type args struct {
+ h *tar.Header
+ tr io.Reader
+ path string
+ }
+ cases := map[string]struct {
+ reason string
+ h HeaderHandler
+ args args
+ want error
+ }{
+ "ExistingPathIsNotADir": {
+ reason: "We should return an error if trying to extract a dir to a path that exists but is not a dir.",
+ h: HeaderHandlerFn(ExtractDir),
+ args: args{
+ h: &tar.Header{Mode: 0700},
+ path: existingFile,
+ },
+ want: errors.Errorf(errFmtNotDir, existingFile),
+ },
+ "SuccessfulCreate": {
+ reason: "We should not return an error if we can create the dir.",
+ h: HeaderHandlerFn(ExtractDir),
+ args: args{
+ h: &tar.Header{Mode: 0700},
+ path: newDir,
+ },
+ want: nil,
+ },
+ "SuccessfulChmod": {
+ reason: "We should not return an error if we can chmod the existing dir",
+ h: HeaderHandlerFn(ExtractDir),
+ args: args{
+ h: &tar.Header{Mode: 0700},
+ path: existingDir,
+ },
+ want: nil,
+ },
+ }
+
+ for name, tc := range cases {
+ t.Run(name, func(t *testing.T) {
+ err := tc.h.Handle(tc.args.h, tc.args.tr, tc.args.path)
+ if diff := cmp.Diff(tc.want, err, test.EquateErrors()); diff != "" {
+ t.Errorf("\n%s\nh.Handle(...): -want error, +got error:\n%s", tc.reason, diff)
+ }
+ })
+ }
+}
+
+func TestExtractSymlink(t *testing.T) {
+ tmp, _ := os.MkdirTemp(os.TempDir(), t.Name())
+ defer os.RemoveAll(tmp)
+
+ linkSrc := filepath.Join(tmp, "src")
+ linkDst := filepath.Join(tmp, "dst")
+ inNonExistentDir := filepath.Join(tmp, "non-exist", "src")
+
+ type args struct {
+ h *tar.Header
+ tr io.Reader
+ path string
+ }
+ cases := map[string]struct {
+ reason string
+ h HeaderHandler
+ args args
+ want error
+ }{
+ "SymlinkError": {
+ reason: "We should return an error if we can't create a symlink",
+ h: HeaderHandlerFn(ExtractSymlink),
+ args: args{
+ h: &tar.Header{Linkname: linkDst},
+ path: inNonExistentDir,
+ },
+ want: errors.Wrap(errors.Errorf("symlink %s %s: no such file or directory", linkDst, inNonExistentDir), errSymlink),
+ },
+ "Successful": {
+ reason: "We should not return an error if we can create a symlink",
+ h: HeaderHandlerFn(ExtractSymlink),
+ args: args{
+ h: &tar.Header{Linkname: linkDst},
+ path: linkSrc,
+ },
+ want: nil,
+ },
+ }
+
+ for name, tc := range cases {
+ t.Run(name, func(t *testing.T) {
+ err := tc.h.Handle(tc.args.h, tc.args.tr, tc.args.path)
+ if diff := cmp.Diff(tc.want, err, test.EquateErrors()); diff != "" {
+ t.Errorf("\n%s\nh.Handle(...): -want error, +got error:\n%s", tc.reason, diff)
+ }
+ })
+ }
+}
+
+func TestExtractFile(t *testing.T) {
+ tmp, _ := os.MkdirTemp(os.TempDir(), t.Name())
+ defer os.RemoveAll(tmp)
+
+ inNonExistentDir := filepath.Join(tmp, "non-exist", "file")
+ newFile := filepath.Join(tmp, "coolFile")
+
+ type args struct {
+ h *tar.Header
+ tr io.Reader
+ path string
+ }
+ cases := map[string]struct {
+ reason string
+ h HeaderHandler
+ args args
+ want error
+ }{
+ "OpenFileError": {
+ reason: "We should return an error if we can't create a file",
+ h: HeaderHandlerFn(ExtractFile),
+ args: args{
+ h: &tar.Header{},
+ path: inNonExistentDir,
+ },
+ want: errors.Wrap(errors.Errorf("open %s: no such file or directory", inNonExistentDir), errOpenFile),
+ },
+ "SuccessfulWRite": {
+ reason: "We should return a nil error if we successfully wrote the file.",
+ h: HeaderHandlerFn(ExtractFile),
+ args: func() args {
+ b := &bytes.Buffer{}
+ tw := tar.NewWriter(b)
+
+ content := []byte("hi!")
+ h := &tar.Header{
+ Typeflag: tar.TypeReg,
+ Mode: 0600,
+ Size: int64(len(content)),
+ }
+
+ _ = tw.WriteHeader(h)
+ _, _ = tw.Write(content)
+ _ = tw.Close()
+
+ tr := tar.NewReader(b)
+ tr.Next()
+
+ return args{
+ h: h,
+ tr: tr,
+ path: newFile,
+ }
+ }(),
+ want: nil,
+ },
+ }
+
+ for name, tc := range cases {
+ t.Run(name, func(t *testing.T) {
+ err := tc.h.Handle(tc.args.h, tc.args.tr, tc.args.path)
+ if diff := cmp.Diff(tc.want, err, test.EquateErrors()); diff != "" {
+ t.Errorf("\n%s\nh.Handle(...): -want error, +got error:\n%s", tc.reason, diff)
+ }
+ })
+ }
+}
diff --git a/internal/oci/layer/layer_unix.go b/internal/oci/layer/layer_unix.go
new file mode 100644
index 0000000..25bf85a
--- /dev/null
+++ b/internal/oci/layer/layer_unix.go
@@ -0,0 +1,45 @@
+//go:build unix
+
+/*
+Copyright 2022 The Crossplane 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 layer
+
+import (
+ "archive/tar"
+ "io"
+
+ "golang.org/x/sys/unix"
+
+ "github.com/crossplane/crossplane-runtime/pkg/errors"
+)
+
+// Error strings.
+const (
+ errCreateFIFO = "cannot create FIFO"
+)
+
+// ExtractFIFO is a HeaderHandler that creates a FIFO at the supplied path per
+// the supplied tar header.
+func ExtractFIFO(h *tar.Header, _ io.Reader, path string) error {
+ // We won't have CAP_MKNOD in a user namespace created by a user who doesn't
+ // have CAP_MKNOD in the initial/root user namespace, but we don't need it
+ // to use mknod to create a FIFO.
+ // https://man7.org/linux/man-pages/man2/mknod.2.html
+ mode := uint32(h.Mode&0777) | unix.S_IFIFO
+ dev := unix.Mkdev(uint32(h.Devmajor), uint32(h.Devminor))
+ return errors.Wrap(unix.Mknod(path, mode, int(dev)), errCreateFIFO)
+}
diff --git a/internal/oci/layer/layer_unix_test.go b/internal/oci/layer/layer_unix_test.go
new file mode 100644
index 0000000..6dcb9c9
--- /dev/null
+++ b/internal/oci/layer/layer_unix_test.go
@@ -0,0 +1,78 @@
+/*
+Copyright 2022 The Crossplane 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 layer
+
+import (
+ "archive/tar"
+ "io"
+ "os"
+ "path/filepath"
+ "testing"
+
+ "github.com/google/go-cmp/cmp"
+
+ "github.com/crossplane/crossplane-runtime/pkg/errors"
+ "github.com/crossplane/crossplane-runtime/pkg/test"
+)
+
+func TestExtractFIFO(t *testing.T) {
+ tmp, _ := os.MkdirTemp(os.TempDir(), t.Name())
+ defer os.RemoveAll(tmp)
+
+ inNonExistentDir := filepath.Join(tmp, "non-exist", "src")
+ newFIFO := filepath.Join(tmp, "fifo")
+
+ type args struct {
+ h *tar.Header
+ tr io.Reader
+ path string
+ }
+ cases := map[string]struct {
+ reason string
+ h HeaderHandler
+ args args
+ want error
+ }{
+ "FIFOError": {
+ reason: "We should return an error if we can't create a FIFO",
+ h: HeaderHandlerFn(ExtractFIFO),
+ args: args{
+ h: &tar.Header{Mode: 0700},
+ path: inNonExistentDir,
+ },
+ want: errors.Wrap(errors.New("no such file or directory"), errCreateFIFO),
+ },
+ "Successful": {
+ reason: "We should not return an error if we can create a symlink",
+ h: HeaderHandlerFn(ExtractFIFO),
+ args: args{
+ h: &tar.Header{Mode: 0700},
+ path: newFIFO,
+ },
+ want: nil,
+ },
+ }
+
+ for name, tc := range cases {
+ t.Run(name, func(t *testing.T) {
+ err := tc.h.Handle(tc.args.h, tc.args.tr, tc.args.path)
+ if diff := cmp.Diff(tc.want, err, test.EquateErrors()); diff != "" {
+ t.Errorf("\n%s\nh.Handle(...): -want error, +got error:\n%s", tc.reason, diff)
+ }
+ })
+ }
+}
diff --git a/internal/oci/pull.go b/internal/oci/pull.go
new file mode 100644
index 0000000..4ec4af9
--- /dev/null
+++ b/internal/oci/pull.go
@@ -0,0 +1,250 @@
+/*
+Copyright 2022 The Crossplane 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 oci
+
+import (
+ "context"
+ "crypto/tls"
+ "crypto/x509"
+ "net/http"
+
+ "github.com/google/go-containerregistry/pkg/authn"
+ "github.com/google/go-containerregistry/pkg/name"
+ ociv1 "github.com/google/go-containerregistry/pkg/v1"
+ "github.com/google/go-containerregistry/pkg/v1/remote"
+
+ "github.com/crossplane/crossplane-runtime/pkg/errors"
+)
+
+// Error strings.
+const (
+ errPullNever = "refusing to pull from remote with image pull policy " + string(ImagePullPolicyNever)
+ errNewDigestStore = "cannot create new image digest store"
+ errPullImage = "cannot pull image from remote"
+ errStoreImage = "cannot cache image"
+ errImageDigest = "cannot get image digest"
+ errStoreDigest = "cannot cache image digest"
+ errLoadImage = "cannot load image from cache"
+ errLoadHash = "cannot load image digest"
+)
+
+// An ImagePullPolicy dictates when an image may be pulled from a remote.
+type ImagePullPolicy string
+
+// Image pull policies
+const (
+ // ImagePullPolicyIfNotPresent only pulls from a remote if the image is not
+ // in the local cache. It is equivalent to ImagePullPolicyNever with a
+ // fall-back to ImagePullPolicyAlways.
+ ImagePullPolicyIfNotPresent ImagePullPolicy = "IfNotPresent"
+
+ // ImagePullPolicyAlways always pulls at least the image manifest from the
+ // remote. Layers are pulled if they are not in cache.
+ ImagePullPolicyAlways ImagePullPolicy = "Always"
+
+ // ImagePullPolicyNever never pulls anything from the remote. It resolves
+ // OCI references to digests (i.e. SHAs) using a local cache of known
+ // mappings.
+ ImagePullPolicyNever ImagePullPolicy = "Never"
+)
+
+// ImagePullAuth configures authentication to a remote registry.
+type ImagePullAuth struct {
+ Username string
+ Password string
+ Auth string
+
+ // IdentityToken is used to authenticate the user and get
+ // an access token for the registry.
+ IdentityToken string
+
+ // RegistryToken is a bearer token to be sent to a registry.
+ RegistryToken string
+}
+
+// Authorization builds a go-containerregistry compatible AuthConfig.
+func (a ImagePullAuth) Authorization() (*authn.AuthConfig, error) {
+ return &authn.AuthConfig{
+ Username: a.Username,
+ Password: a.Password,
+ Auth: a.Auth,
+ IdentityToken: a.IdentityToken,
+ RegistryToken: a.RegistryToken,
+ }, nil
+}
+
+// ImageClientOptions configure an ImageClient.
+type ImageClientOptions struct {
+ pull ImagePullPolicy
+ auth *ImagePullAuth
+ transport *http.Transport
+}
+
+func parse(o ...ImageClientOption) ImageClientOptions {
+ opt := &ImageClientOptions{
+ pull: ImagePullPolicyIfNotPresent, // The default.
+ }
+ for _, fn := range o {
+ fn(opt)
+ }
+ return *opt
+}
+
+// An ImageClientOption configures an ImageClient.
+type ImageClientOption func(c *ImageClientOptions)
+
+// WithPullPolicy specifies whether a client may pull from a remote.
+func WithPullPolicy(p ImagePullPolicy) ImageClientOption {
+ return func(c *ImageClientOptions) {
+ c.pull = p
+ }
+}
+
+// WithPullAuth specifies how a client should authenticate to a remote.
+func WithPullAuth(a *ImagePullAuth) ImageClientOption {
+ return func(c *ImageClientOptions) {
+ c.auth = a
+ }
+}
+
+// WithCustomCA adds given root certificates to tls client configuration
+func WithCustomCA(rootCAs *x509.CertPool) ImageClientOption {
+ return func(c *ImageClientOptions) {
+ c.transport = remote.DefaultTransport.(*http.Transport).Clone()
+ c.transport.TLSClientConfig = &tls.Config{RootCAs: rootCAs, MinVersion: tls.VersionTLS12}
+ }
+}
+
+// An ImageClient is an OCI registry client.
+type ImageClient interface {
+ // Image pulls an OCI image.
+ Image(ctx context.Context, ref name.Reference, o ...ImageClientOption) (ociv1.Image, error)
+}
+
+// An ImageCache caches OCI images.
+type ImageCache interface {
+ Image(h ociv1.Hash) (ociv1.Image, error)
+ WriteImage(img ociv1.Image) error
+}
+
+// A HashCache maps OCI references to hashes.
+type HashCache interface {
+ Hash(r name.Reference) (ociv1.Hash, error)
+ WriteHash(r name.Reference, h ociv1.Hash) error
+}
+
+// A RemoteClient fetches OCI image manifests.
+type RemoteClient struct{}
+
+// Image fetches an image manifest. The returned image lazily pulls its layers.
+func (i *RemoteClient) Image(ctx context.Context, ref name.Reference, o ...ImageClientOption) (ociv1.Image, error) {
+ opts := parse(o...)
+ iOpts := []remote.Option{remote.WithContext(ctx)}
+ if opts.auth != nil {
+ iOpts = append(iOpts, remote.WithAuth(opts.auth))
+ }
+ if opts.transport != nil {
+ iOpts = append(iOpts, remote.WithTransport(opts.transport))
+ }
+ if opts.pull == ImagePullPolicyNever {
+ return nil, errors.New(errPullNever)
+ }
+ return remote.Image(ref, iOpts...)
+}
+
+// A CachingPuller pulls OCI images. Images are pulled either from a local cache
+// or a remote depending on whether they are available locally and a supplied
+// ImagePullPolicy.
+type CachingPuller struct {
+ remote ImageClient
+ local ImageCache
+ mapping HashCache
+}
+
+// NewCachingPuller returns an OCI image puller with a local cache.
+func NewCachingPuller(h HashCache, i ImageCache, r ImageClient) *CachingPuller {
+ return &CachingPuller{remote: r, local: i, mapping: h}
+}
+
+// Image pulls the supplied image and all of its layers. The supplied config
+// determines where the image may be pulled from - i.e. the local store or a
+// remote. Images that are pulled from a remote are cached in the local store.
+func (f *CachingPuller) Image(ctx context.Context, r name.Reference, o ...ImageClientOption) (ociv1.Image, error) {
+ opts := parse(o...)
+
+ switch opts.pull {
+ case ImagePullPolicyNever:
+ return f.never(r)
+ case ImagePullPolicyAlways:
+ return f.always(ctx, r, o...)
+ case ImagePullPolicyIfNotPresent:
+ fallthrough
+ default:
+ img, err := f.never(r)
+ if err == nil {
+ return img, nil
+ }
+ return f.always(ctx, r, o...)
+ }
+}
+func (f *CachingPuller) never(r name.Reference) (ociv1.Image, error) {
+ var h ociv1.Hash
+ var err error
+
+ // Avoid a cache lookup if the digest was specified explicitly.
+ switch d := r.(type) {
+ case name.Digest:
+ h, err = ociv1.NewHash(d.DigestStr())
+ default:
+ h, err = f.mapping.Hash(r)
+ }
+
+ if err != nil {
+ return nil, errors.Wrap(err, errLoadHash)
+ }
+
+ i, err := f.local.Image(h)
+ return i, errors.Wrap(err, errLoadImage)
+}
+
+func (f *CachingPuller) always(ctx context.Context, r name.Reference, o ...ImageClientOption) (ociv1.Image, error) {
+ // This will only pull the image's manifest and config, not layers.
+ img, err := f.remote.Image(ctx, r, o...)
+ if err != nil {
+ return nil, errors.Wrap(err, errPullImage)
+ }
+
+ // This will fetch any layers that aren't already in the store.
+ if err := f.local.WriteImage(img); err != nil {
+ return nil, errors.Wrap(err, errStoreImage)
+ }
+
+ d, err := img.Digest()
+ if err != nil {
+ return nil, errors.Wrap(err, errImageDigest)
+ }
+
+ // Store a mapping from this reference to its digest.
+ if err := f.mapping.WriteHash(r, d); err != nil {
+ return nil, errors.Wrap(err, errStoreDigest)
+ }
+
+ // Return the stored image to ensure future reads are from disk, not
+ // from remote.
+ img, err = f.local.Image(d)
+ return img, errors.Wrap(err, errLoadImage)
+}
diff --git a/internal/oci/pull_test.go b/internal/oci/pull_test.go
new file mode 100644
index 0000000..1580e11
--- /dev/null
+++ b/internal/oci/pull_test.go
@@ -0,0 +1,402 @@
+/*
+Copyright 2022 The Crossplane 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 oci
+
+import (
+ "context"
+ "crypto/x509"
+ "testing"
+
+ "github.com/google/go-cmp/cmp"
+ "github.com/google/go-containerregistry/pkg/name"
+ ociv1 "github.com/google/go-containerregistry/pkg/v1"
+
+ "github.com/crossplane/crossplane-runtime/pkg/errors"
+ "github.com/crossplane/crossplane-runtime/pkg/test"
+)
+
+type MockImage struct {
+ ociv1.Image
+
+ MockDigest func() (ociv1.Hash, error)
+}
+
+func (i *MockImage) Digest() (ociv1.Hash, error) { return i.MockDigest() }
+
+type MockImageClient struct {
+ MockImage func(ctx context.Context, ref name.Reference, o ...ImageClientOption) (ociv1.Image, error)
+}
+
+func (c *MockImageClient) Image(ctx context.Context, ref name.Reference, o ...ImageClientOption) (ociv1.Image, error) {
+ return c.MockImage(ctx, ref, o...)
+}
+
+type MockImageCache struct {
+ MockImage func(h ociv1.Hash) (ociv1.Image, error)
+ MockWriteImage func(img ociv1.Image) error
+}
+
+func (c *MockImageCache) Image(h ociv1.Hash) (ociv1.Image, error) {
+ return c.MockImage(h)
+}
+
+func (c *MockImageCache) WriteImage(img ociv1.Image) error {
+ return c.MockWriteImage(img)
+}
+
+type MockHashCache struct {
+ MockHash func(r name.Reference) (ociv1.Hash, error)
+ MockWriteHash func(r name.Reference, h ociv1.Hash) error
+}
+
+func (c *MockHashCache) Hash(r name.Reference) (ociv1.Hash, error) {
+ return c.MockHash(r)
+}
+
+func (c *MockHashCache) WriteHash(r name.Reference, h ociv1.Hash) error {
+ return c.MockWriteHash(r, h)
+}
+
+func TestImage(t *testing.T) {
+ errBoom := errors.New("boom")
+ coolImage := &MockImage{}
+
+ type args struct {
+ ctx context.Context
+ r name.Reference
+ o []ImageClientOption
+ }
+ type want struct {
+ i ociv1.Image
+ err error
+ }
+
+ cases := map[string]struct {
+ reason string
+ p *CachingPuller
+ args args
+ want want
+ }{
+ "NeverPullHashError": {
+ reason: "We should return an error if we must but can't read a hash from our HashStore.",
+ p: NewCachingPuller(
+ &MockHashCache{
+ MockHash: func(r name.Reference) (ociv1.Hash, error) { return ociv1.Hash{}, errBoom },
+ },
+ &MockImageCache{},
+ &MockImageClient{},
+ ),
+ args: args{
+ o: []ImageClientOption{WithPullPolicy(ImagePullPolicyNever)},
+ },
+ want: want{
+ err: errors.Wrap(errBoom, errLoadHash),
+ },
+ },
+ "NeverPullImageError": {
+ reason: "We should return an error if we must but can't read our image from cache.",
+ p: NewCachingPuller(
+ &MockHashCache{
+ MockHash: func(r name.Reference) (ociv1.Hash, error) { return ociv1.Hash{}, nil },
+ },
+ &MockImageCache{
+ MockImage: func(h ociv1.Hash) (ociv1.Image, error) { return nil, errBoom },
+ },
+ &MockImageClient{},
+ ),
+ args: args{
+ o: []ImageClientOption{WithPullPolicy(ImagePullPolicyNever)},
+ },
+ want: want{
+ err: errors.Wrap(errBoom, errLoadImage),
+ },
+ },
+ "NeverPullSuccess": {
+ reason: "We should return our image from cache.",
+ p: NewCachingPuller(
+ &MockHashCache{
+ MockHash: func(r name.Reference) (ociv1.Hash, error) { return ociv1.Hash{}, nil },
+ },
+ &MockImageCache{
+ MockImage: func(h ociv1.Hash) (ociv1.Image, error) { return coolImage, nil },
+ },
+ &MockImageClient{},
+ ),
+ args: args{
+ o: []ImageClientOption{WithPullPolicy(ImagePullPolicyNever)},
+ },
+ want: want{
+ i: coolImage,
+ },
+ },
+ "NeverPullSuccessExplicit": {
+ reason: "We should return our image from cache without looking up its digest if the digest was specified explicitly.",
+ p: NewCachingPuller(
+ &MockHashCache{},
+ &MockImageCache{
+ MockImage: func(h ociv1.Hash) (ociv1.Image, error) {
+ if h.Hex != "c34045c1a1db8d1b3fca8a692198466952daae07eaf6104b4c87ed3b55b6af1b" {
+ return nil, errors.New("unexpected hash")
+ }
+ return coolImage, nil
+ },
+ },
+ &MockImageClient{},
+ ),
+ args: args{
+ r: name.MustParseReference("example.org/coolimage@sha256:c34045c1a1db8d1b3fca8a692198466952daae07eaf6104b4c87ed3b55b6af1b"),
+ o: []ImageClientOption{WithPullPolicy(ImagePullPolicyNever)},
+ },
+ want: want{
+ i: coolImage,
+ },
+ },
+ "AlwaysPullRemoteError": {
+ reason: "We should return an error if we must but can't pull our image manifest from the remote.",
+ p: NewCachingPuller(
+ &MockHashCache{},
+ &MockImageCache{},
+ &MockImageClient{
+ MockImage: func(ctx context.Context, ref name.Reference, o ...ImageClientOption) (ociv1.Image, error) {
+ return nil, errBoom
+ },
+ },
+ ),
+ args: args{
+ o: []ImageClientOption{WithPullPolicy(ImagePullPolicyAlways)},
+ },
+ want: want{
+ err: errors.Wrap(errBoom, errPullImage),
+ },
+ },
+ "AlwaysPullWriteImageError": {
+ reason: "We should return an error if we must but can't write our image to the local cache.",
+ p: NewCachingPuller(
+ &MockHashCache{},
+ &MockImageCache{
+ MockWriteImage: func(img ociv1.Image) error { return errBoom },
+ },
+ &MockImageClient{
+ MockImage: func(ctx context.Context, ref name.Reference, o ...ImageClientOption) (ociv1.Image, error) {
+ return nil, nil
+ },
+ },
+ ),
+ args: args{
+ o: []ImageClientOption{WithPullPolicy(ImagePullPolicyAlways)},
+ },
+ want: want{
+ err: errors.Wrap(errBoom, errStoreImage),
+ },
+ },
+ "AlwaysPullImageDigestError": {
+ reason: "We should return an error if we can't get our image's digest.",
+ p: NewCachingPuller(
+ &MockHashCache{},
+ &MockImageCache{
+ MockWriteImage: func(img ociv1.Image) error { return nil },
+ },
+ &MockImageClient{
+ MockImage: func(ctx context.Context, ref name.Reference, o ...ImageClientOption) (ociv1.Image, error) {
+ return &MockImage{
+ MockDigest: func() (ociv1.Hash, error) { return ociv1.Hash{}, errBoom },
+ }, nil
+ },
+ },
+ ),
+ args: args{
+ o: []ImageClientOption{WithPullPolicy(ImagePullPolicyAlways)},
+ },
+ want: want{
+ err: errors.Wrap(errBoom, errImageDigest),
+ },
+ },
+ "AlwaysPullWriteDigestError": {
+ reason: "We should return an error if we can't write our digest mapping to the cache.",
+ p: NewCachingPuller(
+ &MockHashCache{
+ MockWriteHash: func(r name.Reference, h ociv1.Hash) error { return errBoom },
+ },
+ &MockImageCache{
+ MockWriteImage: func(img ociv1.Image) error { return nil },
+ },
+ &MockImageClient{
+ MockImage: func(ctx context.Context, ref name.Reference, o ...ImageClientOption) (ociv1.Image, error) {
+ return &MockImage{
+ MockDigest: func() (ociv1.Hash, error) { return ociv1.Hash{}, nil },
+ }, nil
+ },
+ },
+ ),
+ args: args{
+ o: []ImageClientOption{WithPullPolicy(ImagePullPolicyAlways)},
+ },
+ want: want{
+ err: errors.Wrap(errBoom, errStoreDigest),
+ },
+ },
+ "AlwaysPullImageError": {
+ reason: "We should return an error if we must but can't read our image back from cache.",
+ p: NewCachingPuller(
+ &MockHashCache{
+ MockWriteHash: func(r name.Reference, h ociv1.Hash) error { return nil },
+ },
+ &MockImageCache{
+ MockWriteImage: func(img ociv1.Image) error { return nil },
+ MockImage: func(h ociv1.Hash) (ociv1.Image, error) { return nil, errBoom },
+ },
+ &MockImageClient{
+ MockImage: func(ctx context.Context, ref name.Reference, o ...ImageClientOption) (ociv1.Image, error) {
+ return &MockImage{
+ MockDigest: func() (ociv1.Hash, error) { return ociv1.Hash{}, nil },
+ }, nil
+ },
+ },
+ ),
+ args: args{
+ o: []ImageClientOption{WithPullPolicy(ImagePullPolicyAlways)},
+ },
+ want: want{
+ err: errors.Wrap(errBoom, errLoadImage),
+ },
+ },
+ "AlwaysPullSuccess": {
+ reason: "We should return a pulled and cached image.",
+ p: NewCachingPuller(
+ &MockHashCache{
+ MockWriteHash: func(r name.Reference, h ociv1.Hash) error { return nil },
+ },
+ &MockImageCache{
+ MockWriteImage: func(img ociv1.Image) error { return nil },
+ MockImage: func(h ociv1.Hash) (ociv1.Image, error) { return &MockImage{}, nil },
+ },
+ &MockImageClient{
+ MockImage: func(ctx context.Context, ref name.Reference, o ...ImageClientOption) (ociv1.Image, error) {
+ return &MockImage{
+ MockDigest: func() (ociv1.Hash, error) { return ociv1.Hash{}, nil },
+ }, nil
+ },
+ },
+ ),
+ args: args{
+ o: []ImageClientOption{WithPullPolicy(ImagePullPolicyAlways)},
+ },
+ want: want{
+ i: &MockImage{},
+ },
+ },
+ "PullWithCustomCA": {
+ reason: "We should return a pulled and cached image.",
+ p: NewCachingPuller(
+ &MockHashCache{
+ MockHash: func(r name.Reference) (ociv1.Hash, error) {
+ return ociv1.Hash{}, errors.New("this error should not be returned")
+ },
+ MockWriteHash: func(r name.Reference, h ociv1.Hash) error {
+ return nil
+ },
+ },
+ &MockImageCache{
+ MockWriteImage: func(img ociv1.Image) error { return nil },
+ MockImage: func(h ociv1.Hash) (ociv1.Image, error) { return &MockImage{}, nil },
+ },
+ &MockImageClient{
+ MockImage: func(ctx context.Context, ref name.Reference, o ...ImageClientOption) (ociv1.Image, error) {
+ if len(o) != 1 {
+ return nil, errors.New("the number of options should be one")
+ }
+ c := &ImageClientOptions{}
+ o[0](c)
+ if c.transport == nil {
+ return nil, errors.New("Transport should be set")
+ }
+ return &MockImage{
+ MockDigest: func() (ociv1.Hash, error) { return ociv1.Hash{}, nil },
+ }, nil
+ },
+ },
+ ),
+ args: args{
+ o: []ImageClientOption{WithCustomCA(&x509.CertPool{})},
+ },
+ want: want{
+ i: &MockImage{},
+ },
+ },
+ "IfNotPresentTriesCacheFirst": {
+ reason: "The IfNotPresent policy should try to read from cache first.",
+ p: NewCachingPuller(
+ &MockHashCache{
+ MockHash: func(r name.Reference) (ociv1.Hash, error) { return ociv1.Hash{}, nil },
+ },
+ &MockImageCache{
+ MockImage: func(h ociv1.Hash) (ociv1.Image, error) { return &MockImage{}, nil },
+ },
+ &MockImageClient{
+ // If we get here it indicates we called always.
+ MockImage: func(ctx context.Context, ref name.Reference, o ...ImageClientOption) (ociv1.Image, error) {
+ return nil, errors.New("this error should not be returned")
+ },
+ },
+ ),
+ args: args{
+ o: []ImageClientOption{WithPullPolicy(ImagePullPolicyIfNotPresent)},
+ },
+ want: want{
+ i: &MockImage{},
+ },
+ },
+ "IfNotPresentFallsBackToRemote": {
+ reason: "The IfNotPresent policy should fall back to pulling from the remote if it can't read the image from cache.",
+ p: NewCachingPuller(
+ &MockHashCache{
+ MockHash: func(r name.Reference) (ociv1.Hash, error) {
+ // Trigger a fall-back from never to always.
+ return ociv1.Hash{}, errors.New("this error should not be returned")
+ },
+ },
+ &MockImageCache{},
+ &MockImageClient{
+ MockImage: func(ctx context.Context, ref name.Reference, o ...ImageClientOption) (ociv1.Image, error) {
+ return nil, errBoom
+ },
+ },
+ ),
+ args: args{
+ o: []ImageClientOption{WithPullPolicy(ImagePullPolicyIfNotPresent)},
+ },
+ want: want{
+ // This indicates we fell back to always.
+ err: errors.Wrap(errBoom, errPullImage),
+ },
+ },
+ }
+
+ for name, tc := range cases {
+ t.Run(name, func(t *testing.T) {
+
+ i, err := tc.p.Image(tc.args.ctx, tc.args.r, tc.args.o...)
+ if diff := cmp.Diff(tc.want.i, i); diff != "" {
+ t.Errorf("\n%s\nImage(...): -want, +got:\n%s", tc.reason, diff)
+ }
+ if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" {
+ t.Errorf("\n%s\nImage(...): -want error, +got error:\n%s", tc.reason, diff)
+ }
+ })
+ }
+
+}
diff --git a/internal/oci/spec/millicpu.go b/internal/oci/spec/millicpu.go
new file mode 100644
index 0000000..67c5267
--- /dev/null
+++ b/internal/oci/spec/millicpu.go
@@ -0,0 +1,81 @@
+/*
+Copyright 2022 The Crossplane 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 spec
+
+// The below is all copied from k/k to avoid taking a dependency.
+// https://github.com/kubernetes/kubernetes/blob/685d639/pkg/kubelet/cm/helpers_linux.go
+
+const (
+ // These limits are defined in the kernel:
+ // https://github.com/torvalds/linux/blob/0bddd227f3dc55975e2b8dfa7fc6f959b062a2c7/kernel/sched/sched.h#L427-L428
+ minShares = 2
+ maxShares = 262144
+
+ sharesPerCPU = 1024
+ milliCPUToCPU = 1000
+
+ // 100000 microseconds is equivalent to 100ms
+ quotaPeriod = 100000
+
+ // 1000 microseconds is equivalent to 1ms
+ // defined here:
+ // https://github.com/torvalds/linux/blob/cac03ac368fabff0122853de2422d4e17a32de08/kernel/sched/core.c#L10546
+ minQuotaPeriod = 1000
+)
+
+// milliCPUToShares converts the milliCPU to CFS shares.
+func milliCPUToShares(milliCPU int64) uint64 {
+ if milliCPU == 0 {
+ // Docker converts zero milliCPU to unset, which maps to kernel default
+ // for unset: 1024. Return 2 here to really match kernel default for
+ // zero milliCPU.
+ return minShares
+ }
+ // Conceptually (milliCPU / milliCPUToCPU) * sharesPerCPU, but factored to improve rounding.
+ shares := (milliCPU * sharesPerCPU) / milliCPUToCPU
+ if shares < minShares {
+ return minShares
+ }
+ if shares > maxShares {
+ return maxShares
+ }
+ return uint64(shares)
+}
+
+// milliCPUToQuota converts milliCPU to CFS quota and period values.
+// Input parameters and resulting value is number of microseconds.
+func milliCPUToQuota(milliCPU int64, period int64) (quota int64) {
+ // CFS quota is measured in two values:
+ // - cfs_period_us=100ms (the amount of time to measure usage across given by period)
+ // - cfs_quota=20ms (the amount of cpu time allowed to be used across a period)
+ // so in the above example, you are limited to 20% of a single CPU
+ // for multi-cpu environments, you just scale equivalent amounts
+ // see https://www.kernel.org/doc/Documentation/scheduler/sched-bwc.txt for details
+
+ if milliCPU == 0 {
+ return
+ }
+
+ // we then convert your milliCPU to a value normalized over a period
+ quota = (milliCPU * period) / milliCPUToCPU
+
+ // quota needs to be a minimum of 1ms.
+ if quota < minQuotaPeriod {
+ quota = minQuotaPeriod
+ }
+ return
+}
diff --git a/internal/oci/spec/spec.go b/internal/oci/spec/spec.go
new file mode 100644
index 0000000..db57d1c
--- /dev/null
+++ b/internal/oci/spec/spec.go
@@ -0,0 +1,601 @@
+/*
+Copyright 2022 The Crossplane 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 spec implements OCI runtime spec support.
+package spec
+
+import (
+ "encoding/csv"
+ "encoding/json"
+ "io"
+ "os"
+ "strconv"
+ "strings"
+
+ ociv1 "github.com/google/go-containerregistry/pkg/v1"
+ runtime "github.com/opencontainers/runtime-spec/specs-go"
+ "k8s.io/apimachinery/pkg/api/resource"
+
+ "github.com/crossplane/crossplane-runtime/pkg/errors"
+)
+
+const (
+ errApplySpecOption = "cannot apply spec option"
+ errNew = "cannot create new spec"
+ errMarshal = "cannot marshal spec to JSON"
+ errWriteFile = "cannot write file"
+ errParseCPULimit = "cannot parse CPU limit"
+ errParseMemoryLimit = "cannot parse memory limit"
+ errNoCmd = "OCI image must specify entrypoint and/or cmd"
+ errParsePasswd = "cannot parse passwd file data"
+ errParseGroup = "cannot parse group file data"
+ errResolveUser = "cannot resolve user specified by OCI image config"
+ errNonIntegerUID = "cannot parse non-integer UID"
+ errNonIntegerGID = "cannot parse non-integer GID"
+ errOpenPasswdFile = "cannot open passwd file"
+ errOpenGroupFile = "cannot open group file"
+ errParsePasswdFiles = "cannot parse container's /etc/passwd and/or /etc/group files"
+
+ errFmtTooManyColons = "cannot parse user %q (too many colon separators)"
+ errFmtNonExistentUser = "cannot resolve UID of user %q that doesn't exist in container's /etc/passwd"
+ errFmtNonExistentGroup = "cannot resolve GID of group %q that doesn't exist in container's /etc/group"
+)
+
+// An Option specifies optional OCI runtime configuration.
+type Option func(s *runtime.Spec) error
+
+// New produces a new OCI runtime spec (i.e. config.json).
+func New(o ...Option) (*runtime.Spec, error) {
+ // NOTE(negz): Most of this is what `crun spec --rootless` produces.
+ spec := &runtime.Spec{
+ Version: runtime.Version,
+ Process: &runtime.Process{
+ User: runtime.User{UID: 0, GID: 0},
+ Env: []string{"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"},
+ Cwd: "/",
+ Capabilities: &runtime.LinuxCapabilities{
+ Bounding: []string{
+ "CAP_AUDIT_WRITE",
+ "CAP_KILL",
+ "CAP_NET_BIND_SERVICE",
+ },
+ Effective: []string{
+ "CAP_AUDIT_WRITE",
+ "CAP_KILL",
+ "CAP_NET_BIND_SERVICE",
+ },
+ Permitted: []string{
+ "CAP_AUDIT_WRITE",
+ "CAP_KILL",
+ "CAP_NET_BIND_SERVICE",
+ },
+ Ambient: []string{
+ "CAP_AUDIT_WRITE",
+ "CAP_KILL",
+ "CAP_NET_BIND_SERVICE",
+ },
+ },
+ Rlimits: []runtime.POSIXRlimit{
+ {
+ Type: "RLIMIT_NOFILE",
+ Hard: 1024,
+ Soft: 1024,
+ },
+ },
+ },
+ Hostname: "function-runtime-oci",
+ Mounts: []runtime.Mount{
+ {
+ Type: "bind",
+ Destination: "/proc",
+ Source: "/proc",
+ Options: []string{"nosuid", "noexec", "nodev", "rbind"},
+ },
+ {
+ Type: "tmpfs",
+ Destination: "/dev",
+ Source: "tmpfs",
+ Options: []string{"nosuid", "strictatime", "mode=755", "size=65536k"},
+ },
+ {
+ Type: "tmpfs",
+ Destination: "/tmp",
+ Source: "tmp",
+ Options: []string{"nosuid", "strictatime", "mode=755", "size=65536k"},
+ },
+ {
+ Type: "bind",
+ Destination: "/sys",
+ Source: "/sys",
+ Options: []string{"rprivate", "nosuid", "noexec", "nodev", "ro", "rbind"},
+ },
+
+ {
+ Destination: "/dev/pts",
+ Type: "devpts",
+ Source: "devpts",
+ Options: []string{"nosuid", "noexec", "newinstance", "ptmxmode=0666", "mode=0620"},
+ },
+ {
+ Destination: "/dev/mqueue",
+ Type: "mqueue",
+ Source: "mqueue",
+ Options: []string{"nosuid", "noexec", "nodev"},
+ },
+ {
+ Destination: "/sys/fs/cgroup",
+ Type: "cgroup",
+ Source: "cgroup",
+ Options: []string{"rprivate", "nosuid", "noexec", "nodev", "relatime", "ro"},
+ },
+ },
+ // TODO(negz): Do we need a seccomp policy? Our host probably has one.
+ Linux: &runtime.Linux{
+ Resources: &runtime.LinuxResources{
+ Devices: []runtime.LinuxDeviceCgroup{
+ {
+ Allow: false,
+ Access: "rwm",
+ },
+ },
+ Pids: &runtime.LinuxPids{
+ Limit: 32768,
+ },
+ },
+ Namespaces: []runtime.LinuxNamespace{
+ {Type: runtime.PIDNamespace},
+ {Type: runtime.IPCNamespace},
+ {Type: runtime.UTSNamespace},
+ {Type: runtime.MountNamespace},
+ {Type: runtime.CgroupNamespace},
+ {Type: runtime.NetworkNamespace},
+ },
+ MaskedPaths: []string{
+ "/proc/acpi",
+ "/proc/kcore",
+ "/proc/keys",
+ "/proc/latency_stats",
+ "/proc/timer_list",
+ "/proc/timer_stats",
+ "/proc/sched_debug",
+ "/proc/scsi",
+ "/sys/firmware",
+ "/sys/fs/selinux",
+ "/sys/dev/block",
+ },
+ ReadonlyPaths: []string{
+ "/proc/asound",
+ "/proc/bus",
+ "/proc/fs",
+ "/proc/irq",
+ "/proc/sys",
+ "/proc/sysrq-trigger",
+ },
+ },
+ }
+
+ for _, fn := range o {
+ if err := fn(spec); err != nil {
+ return nil, errors.Wrap(err, errApplySpecOption)
+ }
+ }
+
+ return spec, nil
+}
+
+// Write an OCI runtime spec to the supplied path.
+func Write(path string, o ...Option) error {
+ s, err := New(o...)
+ if err != nil {
+ return errors.Wrap(err, errNew)
+ }
+ b, err := json.Marshal(s)
+ if err != nil {
+ return errors.Wrap(err, errMarshal)
+ }
+ return errors.Wrap(os.WriteFile(path, b, 0o600), errWriteFile)
+}
+
+// WithRootFS configures a container's rootfs.
+func WithRootFS(path string, readonly bool) Option {
+ return func(s *runtime.Spec) error {
+ s.Root = &runtime.Root{
+ Path: path,
+ Readonly: readonly,
+ }
+ return nil
+ }
+}
+
+// TODO(negz): Does it make sense to convert Kubernetes-style resource
+// quantities into cgroup limits here, or should our gRPC API accept cgroup
+// style limits like the CRI API does?
+
+// WithCPULimit limits the container's CPU usage per the supplied
+// Kubernetes-style limit string (e.g. 0.5 or 500m for half a core).
+func WithCPULimit(limit string) Option {
+ return func(s *runtime.Spec) error {
+ q, err := resource.ParseQuantity(limit)
+ if err != nil {
+ return errors.Wrap(err, errParseCPULimit)
+ }
+ shares := milliCPUToShares(q.MilliValue())
+ quota := milliCPUToQuota(q.MilliValue(), quotaPeriod)
+
+ if s.Linux == nil {
+ s.Linux = &runtime.Linux{}
+ }
+ if s.Linux.Resources == nil {
+ s.Linux.Resources = &runtime.LinuxResources{}
+ }
+ s.Linux.Resources.CPU = &runtime.LinuxCPU{
+ Shares: &shares,
+ Quota: "a,
+ }
+ return nil
+ }
+}
+
+// WithMemoryLimit limits the container's memory usage per the supplied
+// Kubernetes-style limit string (e.g. 512Mi).
+func WithMemoryLimit(limit string) Option {
+ return func(s *runtime.Spec) error {
+ q, err := resource.ParseQuantity(limit)
+ if err != nil {
+ return errors.Wrap(err, errParseMemoryLimit)
+ }
+ limit := q.Value()
+
+ if s.Linux == nil {
+ s.Linux = &runtime.Linux{}
+ }
+ if s.Linux.Resources == nil {
+ s.Linux.Resources = &runtime.LinuxResources{}
+ }
+ s.Linux.Resources.Memory = &runtime.LinuxMemory{
+ Limit: &limit,
+ }
+ return nil
+ }
+}
+
+// WithHostNetwork configures the container to share the host's (i.e.
+// function-runtime-oci container's) network namespace.
+func WithHostNetwork() Option {
+ return func(s *runtime.Spec) error {
+ s.Mounts = append(s.Mounts, runtime.Mount{
+ Type: "bind",
+ Destination: "/etc/resolv.conf",
+ Source: "/etc/resolv.conf",
+ Options: []string{"rbind", "ro"},
+ })
+ if s.Linux == nil {
+ return nil
+ }
+
+ // We share the host's network by removing any network namespaces.
+ filtered := make([]runtime.LinuxNamespace, 0, len(s.Linux.Namespaces))
+ for _, ns := range s.Linux.Namespaces {
+ if ns.Type == runtime.NetworkNamespace {
+ continue
+ }
+ filtered = append(filtered, ns)
+ }
+ s.Linux.Namespaces = filtered
+ return nil
+ }
+}
+
+// WithImageConfig extends a Spec with configuration derived from an OCI image
+// config file. If the image config specifies a user it will be resolved using
+// the supplied passwd and group files.
+func WithImageConfig(cfg *ociv1.ConfigFile, passwd, group string) Option {
+ return func(s *runtime.Spec) error {
+ if cfg.Config.Hostname != "" {
+ s.Hostname = cfg.Config.Hostname
+ }
+
+ args := make([]string, 0, len(cfg.Config.Entrypoint)+len(cfg.Config.Cmd))
+ args = append(args, cfg.Config.Entrypoint...)
+ args = append(args, cfg.Config.Cmd...)
+ if len(args) == 0 {
+ return errors.New(errNoCmd)
+ }
+
+ if s.Process == nil {
+ s.Process = &runtime.Process{}
+ }
+
+ s.Process.Args = args
+ s.Process.Env = append(s.Process.Env, cfg.Config.Env...)
+
+ if cfg.Config.WorkingDir != "" {
+ s.Process.Cwd = cfg.Config.WorkingDir
+ }
+
+ if cfg.Config.User != "" {
+ p, err := ParsePasswdFiles(passwd, group)
+ if err != nil {
+ return errors.Wrap(err, errParsePasswdFiles)
+ }
+
+ if err := WithUser(cfg.Config.User, p)(s); err != nil {
+ return errors.Wrap(err, errResolveUser)
+ }
+ }
+
+ return nil
+ }
+}
+
+// A Username within an /etc/passwd file.
+type Username string
+
+// A Groupname within an /etc/group file.
+type Groupname string
+
+// A UID within an /etc/passwd file.
+type UID int
+
+// A GID within an /etc/passwd or /etc/group file.
+type GID int
+
+// Unknown UID and GIDs.
+const (
+ UnknownUID = UID(-1)
+ UnknownGID = GID(-1)
+)
+
+// Passwd (and group) file data.
+type Passwd struct {
+ UID map[Username]UID
+ GID map[Groupname]GID
+ Groups map[UID]Groups
+}
+
+// Groups represents a user's groups.
+type Groups struct {
+ // Elsewhere we use types like UID and GID for self-documenting map keys. We
+ // use uint32 here for convenience. It's what runtime.User wants and we
+ // don't want to have to convert a slice of GID to a slice of uint32.
+
+ PrimaryGID uint32
+ AdditionalGIDs []uint32
+}
+
+// ParsePasswdFiles parses the passwd and group files at the supplied paths. If
+// either path does not exist it returns empty Passwd data.
+func ParsePasswdFiles(passwd, group string) (Passwd, error) {
+ p, err := os.Open(passwd) //nolint:gosec // We intentionally take a variable here.
+ if errors.Is(err, os.ErrNotExist) {
+ return Passwd{}, nil
+ }
+ if err != nil {
+ return Passwd{}, errors.Wrap(err, errOpenPasswdFile)
+ }
+ defer p.Close() //nolint:errcheck // Only open for reading.
+
+ g, err := os.Open(group) //nolint:gosec // We intentionally take a variable here.
+ if errors.Is(err, os.ErrNotExist) {
+ return Passwd{}, nil
+ }
+ if err != nil {
+ return Passwd{}, errors.Wrap(err, errOpenGroupFile)
+ }
+ defer g.Close() //nolint:errcheck // Only open for reading.
+
+ return ParsePasswd(p, g)
+}
+
+// ParsePasswd parses the supplied passwd and group data.
+func ParsePasswd(passwd, group io.Reader) (Passwd, error) { //nolint:gocyclo // Breaking each loop into its own function seems more complicated.
+ out := Passwd{
+ UID: make(map[Username]UID),
+ GID: make(map[Groupname]GID),
+ Groups: make(map[UID]Groups),
+ }
+
+ // Formatted as name:password:UID:GID:GECOS:directory:shell
+ p := csv.NewReader(passwd)
+ p.Comma = ':'
+ p.Comment = '#'
+ p.TrimLeadingSpace = true
+ p.FieldsPerRecord = 7 // len(r) will be guaranteed to be 7.
+
+ for {
+ r, err := p.Read()
+ if errors.Is(err, io.EOF) {
+ break
+ }
+ if err != nil {
+ return Passwd{}, errors.Wrap(err, errParsePasswd)
+ }
+
+ username := r[0]
+ uid, err := strconv.ParseUint(r[2], 10, 32)
+ if err != nil {
+ return Passwd{}, errors.Wrap(err, errNonIntegerUID)
+ }
+ gid, err := strconv.ParseUint(r[3], 10, 32)
+ if err != nil {
+ return Passwd{}, errors.Wrap(err, errNonIntegerGID)
+ }
+
+ out.UID[Username(username)] = UID(uid)
+ out.Groups[UID(uid)] = Groups{PrimaryGID: uint32(gid)}
+ }
+
+ // Formatted as group_name:password:GID:comma_separated_user_list
+ g := csv.NewReader(group)
+ g.Comma = ':'
+ g.Comment = '#'
+ g.TrimLeadingSpace = true
+ g.FieldsPerRecord = 4 // len(r) will be guaranteed to be 4.
+
+ for {
+ r, err := g.Read()
+ if errors.Is(err, io.EOF) {
+ break
+ }
+ if err != nil {
+ return Passwd{}, errors.Wrap(err, errParseGroup)
+ }
+
+ groupname := r[0]
+ gid, err := strconv.ParseUint(r[2], 10, 32)
+ if err != nil {
+ return Passwd{}, errors.Wrap(err, errNonIntegerGID)
+ }
+
+ out.GID[Groupname(groupname)] = GID(gid)
+
+ users := r[3]
+
+ // This group has no users (except those with membership via passwd).
+ if users == "" {
+ continue
+ }
+
+ for _, u := range strings.Split(users, ",") {
+ uid, ok := out.UID[Username(u)]
+ if !ok || gid == uint64(out.Groups[uid].PrimaryGID) {
+ // Either this user doesn't exist, or they do and the group is
+ // their primary group. Either way we want to skip it.
+ continue
+ }
+ g := out.Groups[uid]
+ g.AdditionalGIDs = append(g.AdditionalGIDs, uint32(gid))
+ out.Groups[uid] = g
+ }
+ }
+
+ return out, nil
+}
+
+// WithUser resolves an OCI image config user string in order to set the spec's
+// process user. According to the OCI image config v1.0 spec: "For Linux based
+// systems, all of the following are valid: user, uid, user:group, uid:gid,
+// uid:group, user:gid. If group/GID is not specified, the default group and
+// supplementary groups of the given user/UID in /etc/passwd from the container
+// are applied."
+func WithUser(user string, p Passwd) Option {
+ return func(s *runtime.Spec) error {
+ if s.Process == nil {
+ s.Process = &runtime.Process{}
+ }
+
+ parts := strings.Split(user, ":")
+ switch len(parts) {
+ case 1:
+ return WithUserOnly(parts[0], p)(s)
+ case 2:
+ return WithUserAndGroup(parts[0], parts[1], p)(s)
+ default:
+ return errors.Errorf(errFmtTooManyColons, user)
+ }
+ }
+}
+
+// WithUserOnly resolves an OCI Image config user string in order to set the
+// spec's process user. The supplied user string must either be an integer UID
+// (that may or may not exist in the container's /etc/passwd) or a username that
+// exists in the container's /etc/passwd. The supplied user string must not
+// contain any group information.
+func WithUserOnly(user string, p Passwd) Option {
+ return func(s *runtime.Spec) error {
+ if s.Process == nil {
+ s.Process = &runtime.Process{}
+ }
+
+ uid := UnknownUID
+
+ // If user is an integer we treat it as a UID.
+ if v, err := strconv.ParseUint(user, 10, 32); err == nil {
+ uid = UID(v)
+ }
+
+ // If user is not an integer we must resolve it to one using data
+ // extracted from the container's passwd file.
+ if uid == UnknownUID {
+ v, ok := p.UID[Username(user)]
+ if !ok {
+ return errors.Errorf(errFmtNonExistentUser, user)
+ }
+ uid = v
+ }
+
+ // At this point the UID was either explicitly specified or
+ // resolved. Note that if the UID doesn't exist in the supplied
+ // passwd and group data we'll set its GID to 0. This behaviour isn't
+ // specified by the OCI spec, but matches what containerd does.
+ s.Process.User = runtime.User{
+ UID: uint32(uid),
+ GID: p.Groups[uid].PrimaryGID,
+ AdditionalGids: p.Groups[uid].AdditionalGIDs,
+ }
+ return nil
+ }
+}
+
+// WithUserAndGroup resolves an OCI image config user string in order to set the
+// spec's process user. The supplied user string must either be an integer UID
+// (that may or may not exist in the container's /etc/passwd) or a username that
+// exists in the container's /etc/passwd. The supplied group must either be an
+// integer GID (that may or may not exist in the container's /etc/group) or a
+// group name that exists in the container's /etc/group.
+func WithUserAndGroup(user, group string, p Passwd) Option {
+ return func(s *runtime.Spec) error {
+ if s.Process == nil {
+ s.Process = &runtime.Process{}
+ }
+
+ uid, gid := UnknownUID, UnknownGID
+
+ // If user and/or group are integers we treat them as UID/GIDs.
+ if v, err := strconv.ParseUint(user, 10, 32); err == nil {
+ uid = UID(v)
+ }
+ if v, err := strconv.ParseUint(group, 10, 32); err == nil {
+ gid = GID(v)
+ }
+
+ // If user and/or group weren't integers we must resolve them to a
+ // UID/GID that exists within the container's passwd/group files.
+ if uid == UnknownUID {
+ v, ok := p.UID[Username(user)]
+ if !ok {
+ return errors.Errorf(errFmtNonExistentUser, user)
+ }
+ uid = v
+ }
+ if gid == UnknownGID {
+ v, ok := p.GID[Groupname(group)]
+ if !ok {
+ return errors.Errorf(errFmtNonExistentGroup, group)
+ }
+ gid = v
+ }
+
+ // At this point the UID and GID were either explicitly specified or
+ // resolved. All we need to do is supply any additional GIDs.
+ s.Process.User = runtime.User{
+ UID: uint32(uid),
+ GID: uint32(gid),
+ AdditionalGids: p.Groups[uid].AdditionalGIDs,
+ }
+ return nil
+ }
+}
diff --git a/internal/oci/spec/spec_test.go b/internal/oci/spec/spec_test.go
new file mode 100644
index 0000000..c207a26
--- /dev/null
+++ b/internal/oci/spec/spec_test.go
@@ -0,0 +1,931 @@
+/*
+Copyright 2022 The Crossplane 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 spec
+
+import (
+ "io"
+ "os"
+ "path/filepath"
+ "strings"
+ "testing"
+
+ "github.com/google/go-cmp/cmp"
+ "github.com/google/go-cmp/cmp/cmpopts"
+ ociv1 "github.com/google/go-containerregistry/pkg/v1"
+ runtime "github.com/opencontainers/runtime-spec/specs-go"
+ "k8s.io/apimachinery/pkg/api/resource"
+
+ "github.com/crossplane/crossplane-runtime/pkg/errors"
+ "github.com/crossplane/crossplane-runtime/pkg/test"
+)
+
+type TestBundle struct{ path string }
+
+func (b TestBundle) Path() string { return b.path }
+func (b TestBundle) Cleanup() error { return os.RemoveAll(b.path) }
+
+func TestNew(t *testing.T) {
+ errBoom := errors.New("boom")
+
+ type args struct {
+ o []Option
+ }
+ type want struct {
+ s *runtime.Spec
+ err error
+ }
+ cases := map[string]struct {
+ reason string
+ args args
+ want want
+ }{
+ "InvalidOption": {
+ reason: "We should return an error if the supplied option is invalid.",
+ args: args{
+ o: []Option{func(s *runtime.Spec) error { return errBoom }},
+ },
+ want: want{
+ err: errors.Wrap(errBoom, errApplySpecOption),
+ },
+ },
+ "Minimal": {
+ reason: "It should be possible to apply an option to a new spec.",
+ args: args{
+ o: []Option{func(s *runtime.Spec) error {
+ s.Annotations = map[string]string{"cool": "very"}
+ return nil
+ }},
+ },
+ want: want{
+ s: func() *runtime.Spec {
+ s, _ := New()
+ s.Annotations = map[string]string{"cool": "very"}
+ return s
+ }(),
+ },
+ },
+ }
+
+ for name, tc := range cases {
+ t.Run(name, func(t *testing.T) {
+ got, err := New(tc.args.o...)
+ if diff := cmp.Diff(tc.want.s, got); diff != "" {
+ t.Errorf("\n%s\nCreate(...): -want, +got:\n%s", tc.reason, diff)
+ }
+ if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" {
+ t.Errorf("\n%s\nCreate(...): -want error, +got error:\n%s", tc.reason, diff)
+ }
+ })
+ }
+}
+
+func TestWithCPULimit(t *testing.T) {
+ var shares uint64 = 512
+ var quota int64 = 50000
+
+ type args struct {
+ limit string
+ }
+ type want struct {
+ s *runtime.Spec
+ err error
+ }
+
+ cases := map[string]struct {
+ reason string
+ s *runtime.Spec
+ args args
+ want want
+ }{
+ "ParseLimitError": {
+ reason: "We should return any error encountered while parsing the CPU limit.",
+ s: &runtime.Spec{},
+ args: args{
+ limit: "",
+ },
+ want: want{
+ s: &runtime.Spec{},
+ err: errors.Wrap(resource.ErrFormatWrong, errParseCPULimit),
+ },
+ },
+ "SuccessMilliCPUs": {
+ reason: "We should set shares and quota according to the supplied milliCPUs.",
+ s: &runtime.Spec{},
+ args: args{
+ limit: "500m",
+ },
+ want: want{
+ s: &runtime.Spec{
+ Linux: &runtime.Linux{
+ Resources: &runtime.LinuxResources{
+ CPU: &runtime.LinuxCPU{
+ Shares: &shares,
+ Quota: "a,
+ },
+ },
+ },
+ },
+ },
+ },
+ "SuccessCores": {
+ reason: "We should set shares and quota according to the supplied cores.",
+ s: &runtime.Spec{},
+ args: args{
+ limit: "0.5",
+ },
+ want: want{
+ s: &runtime.Spec{
+ Linux: &runtime.Linux{
+ Resources: &runtime.LinuxResources{
+ CPU: &runtime.LinuxCPU{
+ Shares: &shares,
+ Quota: "a,
+ },
+ },
+ },
+ },
+ },
+ },
+ }
+
+ for name, tc := range cases {
+ t.Run(name, func(t *testing.T) {
+ err := WithCPULimit(tc.args.limit)(tc.s)
+
+ if diff := cmp.Diff(tc.want.s, tc.s, cmpopts.EquateEmpty()); diff != "" {
+ t.Errorf("\n%s\nWithCPULimit(...): -want, +got:\n%s", tc.reason, diff)
+ }
+ if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" {
+ t.Errorf("\n%s\nWithCPULimit(...): -want error, +got error:\n%s", tc.reason, diff)
+ }
+ })
+ }
+}
+
+func TestWithMemoryLimit(t *testing.T) {
+ var limit int64 = 512 * 1024 * 1024
+
+ type args struct {
+ limit string
+ }
+ type want struct {
+ s *runtime.Spec
+ err error
+ }
+
+ cases := map[string]struct {
+ reason string
+ s *runtime.Spec
+ args args
+ want want
+ }{
+ "ParseLimitError": {
+ reason: "We should return any error encountered while parsing the memory limit.",
+ s: &runtime.Spec{},
+ args: args{
+ limit: "",
+ },
+ want: want{
+ s: &runtime.Spec{},
+ err: errors.Wrap(resource.ErrFormatWrong, errParseMemoryLimit),
+ },
+ },
+ "Success": {
+ reason: "We should set the supplied memory limit.",
+ s: &runtime.Spec{},
+ args: args{
+ limit: "512Mi",
+ },
+ want: want{
+ s: &runtime.Spec{
+ Linux: &runtime.Linux{
+ Resources: &runtime.LinuxResources{
+ Memory: &runtime.LinuxMemory{
+ Limit: &limit,
+ },
+ },
+ },
+ },
+ },
+ },
+ }
+
+ for name, tc := range cases {
+ t.Run(name, func(t *testing.T) {
+ err := WithMemoryLimit(tc.args.limit)(tc.s)
+
+ if diff := cmp.Diff(tc.want.s, tc.s, cmpopts.EquateEmpty()); diff != "" {
+ t.Errorf("\n%s\nWithMemoryLimit(...): -want, +got:\n%s", tc.reason, diff)
+ }
+ if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" {
+ t.Errorf("\n%s\nWithMemoryLimit(...): -want error, +got error:\n%s", tc.reason, diff)
+ }
+ })
+ }
+}
+
+func TestWithHostNetwork(t *testing.T) {
+ type want struct {
+ s *runtime.Spec
+ err error
+ }
+
+ cases := map[string]struct {
+ reason string
+ s *runtime.Spec
+ want want
+ }{
+ "RemoveNetworkNamespace": {
+ reason: "We should remote the network namespace if it exists.",
+ s: &runtime.Spec{
+ Linux: &runtime.Linux{
+ Namespaces: []runtime.LinuxNamespace{
+ {Type: runtime.CgroupNamespace},
+ {Type: runtime.NetworkNamespace},
+ },
+ },
+ },
+ want: want{
+ s: &runtime.Spec{
+ Mounts: []runtime.Mount{{
+ Type: "bind",
+ Destination: "/etc/resolv.conf",
+ Source: "/etc/resolv.conf",
+ Options: []string{"rbind", "ro"},
+ }},
+ Linux: &runtime.Linux{
+ Namespaces: []runtime.LinuxNamespace{
+ {Type: runtime.CgroupNamespace},
+ },
+ },
+ },
+ },
+ },
+ "EmptySpec": {
+ reason: "We should handle an empty spec without issue.",
+ s: &runtime.Spec{},
+ want: want{
+ s: &runtime.Spec{
+ Mounts: []runtime.Mount{{
+ Type: "bind",
+ Destination: "/etc/resolv.conf",
+ Source: "/etc/resolv.conf",
+ Options: []string{"rbind", "ro"},
+ }},
+ },
+ },
+ },
+ }
+
+ for name, tc := range cases {
+ t.Run(name, func(t *testing.T) {
+ err := WithHostNetwork()(tc.s)
+
+ if diff := cmp.Diff(tc.want.s, tc.s, cmpopts.EquateEmpty()); diff != "" {
+ t.Errorf("\n%s\nWithHostNetwork(...): -want, +got:\n%s", tc.reason, diff)
+ }
+ if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" {
+ t.Errorf("\n%s\nWithHostNetwork(...): -want error, +got error:\n%s", tc.reason, diff)
+ }
+ })
+ }
+}
+
+func TestWithImageConfig(t *testing.T) {
+ type args struct {
+ cfg *ociv1.ConfigFile
+ passwd string
+ group string
+ }
+ type want struct {
+ s *runtime.Spec
+ err error
+ }
+
+ cases := map[string]struct {
+ reason string
+ s *runtime.Spec
+ args args
+ want want
+ }{
+ "NoCommand": {
+ reason: "We should return an error if the supplied image config has no entrypoint and no cmd.",
+ s: &runtime.Spec{},
+ args: args{
+ cfg: &ociv1.ConfigFile{},
+ },
+ want: want{
+ s: &runtime.Spec{},
+ err: errors.New(errNoCmd),
+ },
+ },
+ "UnresolvableUser": {
+ reason: "We should return an error if there is no passwd data and a string username.",
+ s: &runtime.Spec{},
+ args: args{
+ cfg: &ociv1.ConfigFile{
+ Config: ociv1.Config{
+ Entrypoint: []string{"/bin/sh"},
+ User: "negz",
+ },
+ },
+ },
+ want: want{
+ s: &runtime.Spec{
+ Process: &runtime.Process{
+ Args: []string{"/bin/sh"},
+ },
+ },
+ err: errors.Wrap(errors.Errorf(errFmtNonExistentUser, "negz"), errResolveUser),
+ },
+ },
+ "Success": {
+ reason: "We should build a runtime config from the supplied image config.",
+ s: &runtime.Spec{},
+ args: args{
+ cfg: &ociv1.ConfigFile{
+ Config: ociv1.Config{
+ Hostname: "coolhost",
+ Entrypoint: []string{"/bin/sh"},
+ Cmd: []string{"cool"},
+ Env: []string{"COOL=very"},
+ WorkingDir: "/",
+ User: "1000:100",
+ },
+ },
+ },
+ want: want{
+ s: &runtime.Spec{
+ Process: &runtime.Process{
+ Args: []string{"/bin/sh", "cool"},
+ Env: []string{"COOL=very"},
+ Cwd: "/",
+ User: runtime.User{
+ UID: 1000,
+ GID: 100,
+ },
+ },
+ Hostname: "coolhost",
+ },
+ },
+ },
+ }
+
+ for name, tc := range cases {
+ t.Run(name, func(t *testing.T) {
+ err := WithImageConfig(tc.args.cfg, tc.args.passwd, tc.args.group)(tc.s)
+
+ if diff := cmp.Diff(tc.want.s, tc.s, cmpopts.EquateEmpty()); diff != "" {
+ t.Errorf("\n%s\nWithImageConfig(...): -want, +got:\n%s", tc.reason, diff)
+ }
+ if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" {
+ t.Errorf("\n%s\nWithImageConfig(...): -want error, +got error:\n%s", tc.reason, diff)
+ }
+ })
+ }
+}
+
+func TestParsePasswd(t *testing.T) {
+ passwd := `
+# Ensure that comments and leading whitespace are supported.
+root:x:0:0:System administrator:/root:/run/current-system/sw/bin/zsh
+negz:x:1000:100::/home/negz:/run/current-system/sw/bin/zsh
+primary:x:1001:100::/home/primary:/run/current-system/sw/bin/zsh
+`
+
+ group := `
+root:x:0:
+wheel:x:1:negz
+# This is primary's primary group, and doesnotexist doesn't exist in passwd.
+users:x:100:primary,doesnotexist
+`
+
+ type args struct {
+ passwd io.Reader
+ group io.Reader
+ }
+ type want struct {
+ p Passwd
+ err error
+ }
+ cases := map[string]struct {
+ reason string
+ args args
+ want want
+ }{
+ "EmptyFiles": {
+ reason: "We should return an empty Passwd when both files are empty.",
+ args: args{
+ passwd: strings.NewReader(""),
+ group: strings.NewReader(""),
+ },
+ want: want{
+ p: Passwd{},
+ },
+ },
+ // TODO(negz): Should we try fuzz this?
+ "MalformedPasswd": {
+ reason: "We should return an error when the passwd file is malformed.",
+ args: args{
+ passwd: strings.NewReader("@!#!:f"),
+ group: strings.NewReader(""),
+ },
+ want: want{
+ err: errors.Wrap(errors.New("record on line 1: wrong number of fields"), errParsePasswd),
+ },
+ },
+ "MalformedGroup": {
+ reason: "We should return an error when the group file is malformed.",
+ args: args{
+ passwd: strings.NewReader(""),
+ group: strings.NewReader("@!#!:f"),
+ },
+ want: want{
+ err: errors.Wrap(errors.New("record on line 1: wrong number of fields"), errParseGroup),
+ },
+ },
+ "NonIntegerPasswdUID": {
+ reason: "We should return an error when the passwd file contains a non-integer uid.",
+ args: args{
+ passwd: strings.NewReader("username:password:uid:gid:gecos:homedir:shell"),
+ group: strings.NewReader(""),
+ },
+ want: want{
+ err: errors.Wrap(errors.New("strconv.ParseUint: parsing \"uid\": invalid syntax"), errNonIntegerUID),
+ },
+ },
+ "NonIntegerPasswdGID": {
+ reason: "We should return an error when the passwd file contains a non-integer gid.",
+ args: args{
+ passwd: strings.NewReader("username:password:42:gid:gecos:homedir:shell"),
+ group: strings.NewReader(""),
+ },
+ want: want{
+ err: errors.Wrap(errors.New("strconv.ParseUint: parsing \"gid\": invalid syntax"), errNonIntegerGID),
+ },
+ },
+ "NonIntegerGroupGID": {
+ reason: "We should return an error when the group file contains a non-integer gid.",
+ args: args{
+ passwd: strings.NewReader(""),
+ group: strings.NewReader("groupname:password:gid:username"),
+ },
+ want: want{
+ err: errors.Wrap(errors.New("strconv.ParseUint: parsing \"gid\": invalid syntax"), errNonIntegerGID),
+ },
+ },
+ "Success": {
+ reason: "We should successfully parse well formatted passwd and group files.",
+ args: args{
+ passwd: strings.NewReader(passwd),
+ group: strings.NewReader(group),
+ },
+ want: want{
+ p: Passwd{
+ UID: map[Username]UID{
+ "root": 0,
+ "negz": 1000,
+ "primary": 1001,
+ },
+ GID: map[Groupname]GID{
+ "root": 0,
+ "wheel": 1,
+ "users": 100,
+ },
+ Groups: map[UID]Groups{
+ 0: {PrimaryGID: 0},
+ 1000: {PrimaryGID: 100, AdditionalGIDs: []uint32{1}},
+ 1001: {PrimaryGID: 100},
+ },
+ },
+ },
+ },
+ }
+
+ for name, tc := range cases {
+ t.Run(name, func(t *testing.T) {
+ got, err := ParsePasswd(tc.args.passwd, tc.args.group)
+
+ if diff := cmp.Diff(tc.want.p, got, cmpopts.EquateEmpty()); diff != "" {
+ t.Errorf("\n%s\nParsePasswd(...): -want, +got:\n%s", tc.reason, diff)
+ }
+ if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" {
+ t.Errorf("\n%s\nParsePasswd(...): -want error, +got error:\n%s", tc.reason, diff)
+ }
+ })
+ }
+}
+
+func TestParsePasswdFiles(t *testing.T) {
+ passwd := `
+# Ensure that comments and leading whitespace are supported.
+root:x:0:0:System administrator:/root:/run/current-system/sw/bin/zsh
+negz:x:1000:100::/home/negz:/run/current-system/sw/bin/zsh
+primary:x:1001:100::/home/primary:/run/current-system/sw/bin/zsh
+`
+
+ group := `
+root:x:0:
+wheel:x:1:negz
+# This is primary's primary group, and doesnotexist doesn't exist in passwd.
+users:x:100:primary,doesnotexist
+`
+
+ tmp, err := os.MkdirTemp(os.TempDir(), t.Name())
+ if err != nil {
+ t.Fatalf(err.Error())
+ }
+ defer os.RemoveAll(tmp)
+
+ _ = os.WriteFile(filepath.Join(tmp, "passwd"), []byte(passwd), 0600)
+ _ = os.WriteFile(filepath.Join(tmp, "group"), []byte(group), 0600)
+
+ type args struct {
+ passwd string
+ group string
+ }
+ type want struct {
+ p Passwd
+ err error
+ }
+ cases := map[string]struct {
+ reason string
+ args args
+ want want
+ }{
+ "NoPasswdFile": {
+ reason: "We should not return an error if the passwd file doesn't exist.",
+ args: args{
+ passwd: filepath.Join(tmp, "nonexist"),
+ group: filepath.Join(tmp, "group"),
+ },
+ want: want{
+ p: Passwd{},
+ },
+ },
+ "NoGroupFile": {
+ reason: "We should not return an error if the group file doesn't exist.",
+ args: args{
+ passwd: filepath.Join(tmp, "passwd"),
+ group: filepath.Join(tmp, "nonexist"),
+ },
+ want: want{
+ p: Passwd{},
+ },
+ },
+ "Success": {
+ reason: "We should successfully parse well formatted passwd and group files.",
+ args: args{
+ passwd: filepath.Join(tmp, "passwd"),
+ group: filepath.Join(tmp, "group"),
+ },
+ want: want{
+ p: Passwd{
+ UID: map[Username]UID{
+ "root": 0,
+ "negz": 1000,
+ "primary": 1001,
+ },
+ GID: map[Groupname]GID{
+ "root": 0,
+ "wheel": 1,
+ "users": 100,
+ },
+ Groups: map[UID]Groups{
+ 0: {PrimaryGID: 0},
+ 1000: {PrimaryGID: 100, AdditionalGIDs: []uint32{1}},
+ 1001: {PrimaryGID: 100},
+ },
+ },
+ },
+ },
+ }
+
+ for name, tc := range cases {
+ t.Run(name, func(t *testing.T) {
+ got, err := ParsePasswdFiles(tc.args.passwd, tc.args.group)
+
+ if diff := cmp.Diff(tc.want.p, got, cmpopts.EquateEmpty()); diff != "" {
+ t.Errorf("\n%s\nParsePasswd(...): -want, +got:\n%s", tc.reason, diff)
+ }
+ if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" {
+ t.Errorf("\n%s\nParsePasswd(...): -want error, +got error:\n%s", tc.reason, diff)
+ }
+ })
+ }
+}
+
+func TestWithUser(t *testing.T) {
+ type args struct {
+ user string
+ p Passwd
+ }
+ type want struct {
+ s *runtime.Spec
+ err error
+ }
+
+ // NOTE(negz): We 'test through' here only to test that WithUser can
+ // distinguish a user (only) from a user and group and route them to the
+ // right place; see TestWithUserOnly and TestWithUserAndGroup.
+ cases := map[string]struct {
+ reason string
+ s *runtime.Spec
+ args args
+ want want
+ }{
+ "TooManyColons": {
+ reason: "We should return an error if the supplied user string contains more than one colon separator.",
+ s: &runtime.Spec{},
+ args: args{
+ user: "user:group:wat",
+ },
+ want: want{
+ s: &runtime.Spec{Process: &runtime.Process{}},
+ err: errors.Errorf(errFmtTooManyColons, "user:group:wat"),
+ },
+ },
+ "UIDOnly": {
+ reason: "We should handle a user string that is a UID without error.",
+ s: &runtime.Spec{},
+ args: args{
+ user: "1000",
+ },
+ want: want{
+ s: &runtime.Spec{Process: &runtime.Process{
+ User: runtime.User{
+ UID: 1000,
+ },
+ }},
+ },
+ },
+ "UIDAndGID": {
+ reason: "We should handle a user string that is a UID and GID without error.",
+ s: &runtime.Spec{},
+ args: args{
+ user: "1000:100",
+ },
+ want: want{
+ s: &runtime.Spec{Process: &runtime.Process{
+ User: runtime.User{
+ UID: 1000,
+ GID: 100,
+ },
+ }},
+ },
+ },
+ }
+
+ for name, tc := range cases {
+ t.Run(name, func(t *testing.T) {
+ err := WithUser(tc.args.user, tc.args.p)(tc.s)
+
+ if diff := cmp.Diff(tc.want.s, tc.s, cmpopts.EquateEmpty()); diff != "" {
+ t.Errorf("\n%s\nWithUser(...): -want, +got:\n%s", tc.reason, diff)
+ }
+ if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" {
+ t.Errorf("\n%s\nWithUser(...): -want error, +got error:\n%s", tc.reason, diff)
+ }
+ })
+ }
+}
+
+func TestWithUserOnly(t *testing.T) {
+ type args struct {
+ user string
+ p Passwd
+ }
+ type want struct {
+ s *runtime.Spec
+ err error
+ }
+
+ cases := map[string]struct {
+ reason string
+ s *runtime.Spec
+ args args
+ want want
+ }{
+ "UIDOnly": {
+ reason: "We should handle a user string that is a UID without error.",
+ s: &runtime.Spec{},
+ args: args{
+ user: "1000",
+ },
+ want: want{
+ s: &runtime.Spec{Process: &runtime.Process{
+ User: runtime.User{
+ UID: 1000,
+ },
+ }},
+ },
+ },
+ "ResolveUIDGroups": {
+ reason: "We should 'resolve' a UID's groups per the supplied Passwd data.",
+ s: &runtime.Spec{},
+ args: args{
+ user: "1000",
+ p: Passwd{
+ Groups: map[UID]Groups{
+ 1000: {
+ PrimaryGID: 100,
+ AdditionalGIDs: []uint32{1},
+ },
+ },
+ },
+ },
+ want: want{
+ s: &runtime.Spec{Process: &runtime.Process{
+ User: runtime.User{
+ UID: 1000,
+ GID: 100,
+ AdditionalGids: []uint32{1},
+ },
+ }},
+ },
+ },
+ "NonExistentUser": {
+ reason: "We should return an error if the supplied username doesn't exist in the supplied Passwd data.",
+ s: &runtime.Spec{},
+ args: args{
+ user: "doesnotexist",
+ p: Passwd{},
+ },
+ want: want{
+ s: &runtime.Spec{Process: &runtime.Process{}},
+ err: errors.Errorf(errFmtNonExistentUser, "doesnotexist"),
+ },
+ },
+ "ResolveUserToUID": {
+ reason: "We should 'resolve' a username to a UID per the supplied Passwd data.",
+ s: &runtime.Spec{},
+ args: args{
+ user: "negz",
+ p: Passwd{
+ UID: map[Username]UID{
+ "negz": 1000,
+ },
+ },
+ },
+ want: want{
+ s: &runtime.Spec{Process: &runtime.Process{
+ User: runtime.User{
+ UID: 1000,
+ },
+ }},
+ },
+ },
+ }
+
+ for name, tc := range cases {
+ t.Run(name, func(t *testing.T) {
+ err := WithUserOnly(tc.args.user, tc.args.p)(tc.s)
+
+ if diff := cmp.Diff(tc.want.s, tc.s, cmpopts.EquateEmpty()); diff != "" {
+ t.Errorf("\n%s\nWithUserOnly(...): -want, +got:\n%s", tc.reason, diff)
+ }
+ if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" {
+ t.Errorf("\n%s\nWithUserOnly(...): -want error, +got error:\n%s", tc.reason, diff)
+ }
+ })
+ }
+}
+
+func TestWithUserAndGroup(t *testing.T) {
+ type args struct {
+ user string
+ group string
+ p Passwd
+ }
+ type want struct {
+ s *runtime.Spec
+ err error
+ }
+
+ cases := map[string]struct {
+ reason string
+ s *runtime.Spec
+ args args
+ want want
+ }{
+ "UIDAndGID": {
+ reason: "We should handle a UID and GID without error.",
+ s: &runtime.Spec{},
+ args: args{
+ user: "1000",
+ group: "100",
+ },
+ want: want{
+ s: &runtime.Spec{Process: &runtime.Process{
+ User: runtime.User{
+ UID: 1000,
+ GID: 100,
+ },
+ }},
+ },
+ },
+ "ResolveAdditionalGIDs": {
+ reason: "We should resolve any additional GIDs in the supplied Passwd data.",
+ s: &runtime.Spec{},
+ args: args{
+ user: "1000",
+ group: "100",
+ p: Passwd{
+ Groups: map[UID]Groups{
+ 1000: {
+ PrimaryGID: 42, // This should be ignored, since an explicit GID was supplied.
+ AdditionalGIDs: []uint32{1},
+ },
+ },
+ },
+ },
+ want: want{
+ s: &runtime.Spec{Process: &runtime.Process{
+ User: runtime.User{
+ UID: 1000,
+ GID: 100,
+ AdditionalGids: []uint32{1},
+ },
+ }},
+ },
+ },
+ "NonExistentUser": {
+ reason: "We should return an error if the supplied username doesn't exist in the supplied Passwd data.",
+ s: &runtime.Spec{},
+ args: args{
+ user: "doesnotexist",
+ p: Passwd{},
+ },
+ want: want{
+ s: &runtime.Spec{Process: &runtime.Process{}},
+ err: errors.Errorf(errFmtNonExistentUser, "doesnotexist"),
+ },
+ },
+ "NonExistentGroup": {
+ reason: "We should return an error if the supplied group doesn't exist in the supplied Passwd data.",
+ s: &runtime.Spec{},
+ args: args{
+ user: "exists",
+ group: "doesnotexist",
+ p: Passwd{
+ UID: map[Username]UID{"exists": 1000},
+ },
+ },
+ want: want{
+ s: &runtime.Spec{Process: &runtime.Process{}},
+ err: errors.Errorf(errFmtNonExistentGroup, "doesnotexist"),
+ },
+ },
+ "ResolveUserAndGroupToUIDAndGID": {
+ reason: "We should 'resolve' a username to a UID and a groupname to a GID per the supplied Passwd data.",
+ s: &runtime.Spec{},
+ args: args{
+ user: "negz",
+ group: "users",
+ p: Passwd{
+ UID: map[Username]UID{
+ "negz": 1000,
+ },
+ GID: map[Groupname]GID{
+ "users": 100,
+ },
+ },
+ },
+ want: want{
+ s: &runtime.Spec{Process: &runtime.Process{
+ User: runtime.User{
+ UID: 1000,
+ GID: 100,
+ },
+ }},
+ },
+ },
+ }
+
+ for name, tc := range cases {
+ t.Run(name, func(t *testing.T) {
+ err := WithUserAndGroup(tc.args.user, tc.args.group, tc.args.p)(tc.s)
+
+ if diff := cmp.Diff(tc.want.s, tc.s, cmpopts.EquateEmpty()); diff != "" {
+ t.Errorf("\n%s\nWithUserAndGroup(...): -want, +got:\n%s", tc.reason, diff)
+ }
+ if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" {
+ t.Errorf("\n%s\nWithUserAndGroup(...): -want error, +got error:\n%s", tc.reason, diff)
+ }
+ })
+ }
+}
diff --git a/internal/oci/store/overlay/store_overlay.go b/internal/oci/store/overlay/store_overlay.go
new file mode 100644
index 0000000..34f2f46
--- /dev/null
+++ b/internal/oci/store/overlay/store_overlay.go
@@ -0,0 +1,479 @@
+/*
+Copyright 2022 The Crossplane 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 overlay implements an overlay based container store.
+package overlay
+
+import (
+ "context"
+ "fmt"
+ "io"
+ "os"
+ "path/filepath"
+
+ ociv1 "github.com/google/go-containerregistry/pkg/v1"
+
+ "github.com/crossplane/crossplane-runtime/pkg/errors"
+ "github.com/crossplane/crossplane-runtime/pkg/resource"
+
+ "github.com/crossplane/function-runtime-oci/internal/oci/layer"
+ "github.com/crossplane/function-runtime-oci/internal/oci/spec"
+ "github.com/crossplane/function-runtime-oci/internal/oci/store"
+)
+
+// Error strings
+const (
+ errMkContainerStore = "cannot make container store directory"
+ errMkLayerStore = "cannot make layer store directory"
+ errReadConfigFile = "cannot read image config file"
+ errGetLayers = "cannot get image layers"
+ errResolveLayer = "cannot resolve layer to suitable overlayfs lower directory"
+ errBootstrapBundle = "cannot bootstrap bundle rootfs"
+ errWriteRuntimeSpec = "cannot write OCI runtime spec"
+ errGetDigest = "cannot get digest"
+ errMkAlgoDir = "cannot create store directory"
+ errFetchLayer = "cannot fetch and decompress layer"
+ errMkWorkdir = "cannot create work directory to extract layer"
+ errApplyLayer = "cannot apply (extract) uncompressed tarball layer"
+ errMvWorkdir = "cannot move temporary work directory"
+ errStatLayer = "cannot determine whether layer exists in store"
+ errCleanupWorkdir = "cannot cleanup temporary work directory"
+ errMkOverlayDirTmpfs = "cannot make overlay tmpfs dir"
+ errMkdirTemp = "cannot make temporary dir"
+ errMountOverlayfs = "cannot mount overlayfs"
+
+ errFmtMkOverlayDir = "cannot make overlayfs %q dir"
+)
+
+// Common overlayfs directories.
+const (
+ overlayDirTmpfs = "tmpfs"
+ overlayDirUpper = "upper"
+ overlayDirWork = "work"
+ overlayDirLower = "lower" // Only used when there are no parent layers.
+ overlayDirMerged = "merged" // Only used when generating diff layers.
+)
+
+// Supported returns true if the supplied cacheRoot supports the overlay
+// filesystem. Notably overlayfs was not supported in unprivileged user
+// namespaces until Linux kernel 5.11. It's also not possible to create an
+// overlayfs where the upper dir is itself on an overlayfs (i.e. is on a
+// container's root filesystem).
+// https://github.com/torvalds/linux/commit/459c7c565ac36ba09ffbf
+func Supported(cacheRoot string) bool {
+ // We use NewLayerWorkdir to test because it needs to create an upper dir on
+ // the same filesystem as the supplied cacheRoot in order to be able to move
+ // it into place as a cached layer. NewOverlayBundle creates an upper dir on
+ // a tmpfs, and is thus supported in some cases where NewLayerWorkdir isn't.
+ w, err := NewLayerWorkdir(cacheRoot, "supports-overlay-test", []string{})
+ if err != nil {
+ return false
+ }
+ if err := w.Cleanup(); err != nil {
+ return false
+ }
+ return true
+}
+
+// An LayerResolver resolves the supplied layer to a path suitable for use as an
+// overlayfs lower directory.
+type LayerResolver interface {
+ // Resolve the supplied layer to a path suitable for use as a lower dir.
+ Resolve(ctx context.Context, l ociv1.Layer, parents ...ociv1.Layer) (string, error)
+}
+
+// A TarballApplicator applies (i.e. extracts) an OCI layer tarball.
+// https://github.com/opencontainers/image-spec/blob/v1.0/layer.md
+type TarballApplicator interface {
+ // Apply the supplied tarball - an OCI filesystem layer - to the supplied
+ // root directory. Applying all of an image's layers, in the correct order,
+ // should produce the image's "flattened" filesystem.
+ Apply(ctx context.Context, tb io.Reader, root string) error
+}
+
+// A BundleBootstrapper bootstraps a bundle by creating and mounting its rootfs.
+type BundleBootstrapper interface {
+ Bootstrap(path string, parentLayerPaths []string) (Bundle, error)
+}
+
+// A BundleBootstrapperFn bootstraps a bundle by creating and mounting its
+// rootfs.
+type BundleBootstrapperFn func(path string, parentLayerPaths []string) (Bundle, error)
+
+// Bootstrap a bundle by creating and mounting its rootfs.
+func (fn BundleBootstrapperFn) Bootstrap(path string, parentLayerPaths []string) (Bundle, error) {
+ return fn(path, parentLayerPaths)
+}
+
+// A RuntimeSpecWriter writes an OCI runtime spec to the supplied path.
+type RuntimeSpecWriter interface {
+ // Write and write an OCI runtime spec to the supplied path.
+ Write(path string, o ...spec.Option) error
+}
+
+// A RuntimeSpecWriterFn allows a function to satisfy RuntimeSpecCreator.
+type RuntimeSpecWriterFn func(path string, o ...spec.Option) error
+
+// Write an OCI runtime spec to the supplied path.
+func (fn RuntimeSpecWriterFn) Write(path string, o ...spec.Option) error { return fn(path, o...) }
+
+// An CachingBundler stores OCI containers, images, and layers. When asked to
+// bundle a container for a new image the CachingBundler will extract and cache
+// the image's layers as files on disk. The container's root filesystem is then
+// created as an overlay atop the image's layers. The upper layer of this
+// overlay is stored in memory on a tmpfs, and discarded once the container has
+// finished running.
+type CachingBundler struct {
+ root string
+ layer LayerResolver
+ bundle BundleBootstrapper
+ spec RuntimeSpecWriter
+}
+
+// NewCachingBundler returns a bundler that creates container filesystems as
+// overlays on their image's layers, which are stored as extracted, overlay
+// compatible directories of files.
+func NewCachingBundler(root string) (*CachingBundler, error) {
+ l, err := NewCachingLayerResolver(filepath.Join(root, store.DirOverlays))
+ if err != nil {
+ return nil, errors.Wrap(err, errMkLayerStore)
+ }
+
+ s := &CachingBundler{
+ root: filepath.Join(root, store.DirContainers),
+ layer: l,
+ bundle: BundleBootstrapperFn(BootstrapBundle),
+ spec: RuntimeSpecWriterFn(spec.Write),
+ }
+ return s, nil
+}
+
+// Bundle returns an OCI bundle ready for use by an OCI runtime. The supplied
+// image will be fetched and cached in the store if it does not already exist.
+func (c *CachingBundler) Bundle(ctx context.Context, i ociv1.Image, id string, o ...spec.Option) (store.Bundle, error) {
+ cfg, err := i.ConfigFile()
+ if err != nil {
+ return nil, errors.Wrap(err, errReadConfigFile)
+ }
+
+ if err := store.Validate(i); err != nil {
+ return nil, err
+ }
+
+ layers, err := i.Layers()
+ if err != nil {
+ return nil, errors.Wrap(err, errGetLayers)
+ }
+
+ lowerPaths := make([]string, len(layers))
+ for i := range layers {
+ p, err := c.layer.Resolve(ctx, layers[i], layers[:i]...)
+ if err != nil {
+ return nil, errors.Wrap(err, errResolveLayer)
+ }
+ lowerPaths[i] = p
+ }
+
+ path := filepath.Join(c.root, id)
+
+ b, err := c.bundle.Bootstrap(path, lowerPaths)
+ if err != nil {
+ return nil, errors.Wrap(err, errBootstrapBundle)
+ }
+
+ // Inject config derived from the image first, so that any options passed in
+ // by the caller will override it.
+ rootfs := filepath.Join(path, store.DirRootFS)
+ p, g := filepath.Join(rootfs, "etc", "passwd"), filepath.Join(rootfs, "etc", "group")
+ opts := append([]spec.Option{spec.WithImageConfig(cfg, p, g), spec.WithRootFS(store.DirRootFS, true)}, o...)
+
+ if err = c.spec.Write(filepath.Join(path, store.FileSpec), opts...); err != nil {
+ _ = b.Cleanup()
+ return nil, errors.Wrap(err, errWriteRuntimeSpec)
+ }
+
+ return b, nil
+}
+
+// A CachingLayerResolver resolves an OCI layer to an overlay compatible
+// directory on disk. The directory is created the first time a layer is
+// resolved; subsequent calls return the cached directory.
+type CachingLayerResolver struct {
+ root string
+ tarball TarballApplicator
+ wdopts []NewLayerWorkdirOption
+}
+
+// NewCachingLayerResolver returns a LayerResolver that extracts layers upon
+// first resolution, returning cached layer paths on subsequent calls.
+func NewCachingLayerResolver(root string) (*CachingLayerResolver, error) {
+ c := &CachingLayerResolver{
+ root: root,
+ tarball: layer.NewStackingExtractor(layer.NewWhiteoutHandler(layer.NewExtractHandler())),
+ }
+ return c, os.MkdirAll(root, 0700)
+}
+
+// Resolve the supplied layer to a path suitable for use as an overlayfs lower
+// layer directory. The first time a layer is resolved it will be extracted and
+// cached as an overlayfs compatible directory of files, with any OCI whiteouts
+// converted to overlayfs whiteouts.
+func (s *CachingLayerResolver) Resolve(ctx context.Context, l ociv1.Layer, parents ...ociv1.Layer) (string, error) {
+ d, err := l.DiffID() // The uncompressed layer digest.
+ if err != nil {
+ return "", errors.Wrap(err, errGetDigest)
+ }
+
+ path := filepath.Join(s.root, d.Algorithm, d.Hex)
+ if _, err = os.Stat(path); !errors.Is(err, os.ErrNotExist) {
+ // The path exists or we encountered an error other than ErrNotExist.
+ // Either way return the path and the wrapped error - errors.Wrap will
+ // return nil if the path exists.
+ return path, errors.Wrap(err, errStatLayer)
+ }
+
+ // Doesn't exist - cache it. It's possible multiple callers may hit this
+ // branch at once. This will result in multiple extractions to different
+ // temporary dirs. We ignore EEXIST errors from os.Rename, so callers
+ // that lose the race should return the path cached by the successful
+ // caller.
+
+ // This call to Uncompressed is what actually pulls a remote layer. In
+ // most cases we'll be using an image backed by our local image store.
+ tarball, err := l.Uncompressed()
+ if err != nil {
+ return "", errors.Wrap(err, errFetchLayer)
+ }
+
+ parentPaths := make([]string, len(parents))
+ for i := range parents {
+ d, err := parents[i].DiffID()
+ if err != nil {
+ return "", errors.Wrap(err, errGetDigest)
+ }
+ parentPaths[i] = filepath.Join(s.root, d.Algorithm, d.Hex)
+ }
+
+ lw, err := NewLayerWorkdir(filepath.Join(s.root, d.Algorithm), d.Hex, parentPaths, s.wdopts...)
+ if err != nil {
+ return "", errors.Wrap(err, errMkWorkdir)
+ }
+
+ if err := s.tarball.Apply(ctx, tarball, lw.ApplyPath()); err != nil {
+ _ = lw.Cleanup()
+ return "", errors.Wrap(err, errApplyLayer)
+ }
+
+ // If newpath exists now (when it didn't above) we must have lost a race
+ // with another caller to cache this layer.
+ if err := os.Rename(lw.ResultPath(), path); resource.Ignore(os.IsExist, err) != nil {
+ _ = lw.Cleanup()
+ return "", errors.Wrap(err, errMvWorkdir)
+ }
+
+ return path, errors.Wrap(lw.Cleanup(), errCleanupWorkdir)
+}
+
+// An Bundle is an OCI runtime bundle. Its root filesystem is a temporary
+// overlay atop its image's cached layers.
+type Bundle struct {
+ path string
+ mounts []Mount
+}
+
+// BootstrapBundle creates and returns an OCI runtime bundle with a root
+// filesystem backed by a temporary (tmpfs) overlay atop the supplied lower
+// layer paths.
+func BootstrapBundle(path string, parentLayerPaths []string) (Bundle, error) {
+ if err := os.MkdirAll(path, 0700); err != nil {
+ return Bundle{}, errors.Wrap(err, "cannot create bundle dir")
+ }
+
+ if err := os.Mkdir(filepath.Join(path, overlayDirTmpfs), 0700); err != nil {
+ _ = os.RemoveAll(path)
+ return Bundle{}, errors.Wrap(err, errMkOverlayDirTmpfs)
+ }
+
+ tm := TmpFSMount{Mountpoint: filepath.Join(path, overlayDirTmpfs)}
+ if err := tm.Mount(); err != nil {
+ _ = os.RemoveAll(path)
+ return Bundle{}, errors.Wrap(err, "cannot mount workdir tmpfs")
+ }
+
+ for _, p := range []string{
+ filepath.Join(path, overlayDirTmpfs, overlayDirUpper),
+ filepath.Join(path, overlayDirTmpfs, overlayDirWork),
+ filepath.Join(path, store.DirRootFS),
+ } {
+ if err := os.Mkdir(p, 0700); err != nil {
+ _ = os.RemoveAll(path)
+ return Bundle{}, errors.Wrapf(err, "cannot create %s dir", p)
+ }
+ }
+
+ om := OverlayMount{
+ Lower: parentLayerPaths,
+ Upper: filepath.Join(path, overlayDirTmpfs, overlayDirUpper),
+ Work: filepath.Join(path, overlayDirTmpfs, overlayDirWork),
+ Mountpoint: filepath.Join(path, store.DirRootFS),
+ }
+ if err := om.Mount(); err != nil {
+ _ = os.RemoveAll(path)
+ return Bundle{}, errors.Wrap(err, "cannot mount workdir overlayfs")
+ }
+
+ // We pass mounts in the order they should be unmounted.
+ return Bundle{path: path, mounts: []Mount{om, tm}}, nil
+}
+
+// Path to the OCI bundle.
+func (b Bundle) Path() string { return b.path }
+
+// Cleanup the OCI bundle.
+func (b Bundle) Cleanup() error {
+ for _, m := range b.mounts {
+ if err := m.Unmount(); err != nil {
+ return errors.Wrap(err, "cannot unmount bundle filesystem")
+ }
+ }
+ return errors.Wrap(os.RemoveAll(b.path), "cannot remove bundle")
+}
+
+// A Mount of a filesystem.
+type Mount interface {
+ Mount() error
+ Unmount() error
+}
+
+// A TmpFSMount represents a mount of type tmpfs.
+type TmpFSMount struct {
+ Mountpoint string
+}
+
+// An OverlayMount represents a mount of type overlay.
+type OverlayMount struct { //nolint:revive // overlay.OverlayMount makes sense given that overlay.TmpFSMount exists too.
+ Mountpoint string
+ Lower []string
+ Upper string
+ Work string
+}
+
+// A LayerWorkdir is a temporary directory used to produce an overlayfs layer
+// from an OCI layer by applying the OCI layer to a temporary overlay mount.
+// It's not possible to _directly_ create overlay whiteout files in an
+// unprivileged user namespace because doing so requires CAP_MKNOD in the 'root'
+// or 'initial' user namespace - whiteout files are actually character devices
+// per "whiteouts and opaque directories" at
+// https://www.kernel.org/doc/Documentation/filesystems/overlayfs.txt
+//
+// We can however create overlay whiteout files indirectly by creating an
+// overlay where the parent OCI layers are the lower overlayfs layers, and
+// applying the layer to be cached to said fs. Doing so will produce an upper
+// overlayfs layer that we can cache. This layer will be a valid lower layer
+// (complete with overlay whiteout files) for either subsequent layers from the
+// OCI image, or the final container root filesystem layer.
+type LayerWorkdir struct {
+ overlay Mount
+ path string
+}
+
+// NewOverlayMountFn creates an overlay mount.
+type NewOverlayMountFn func(path string, parentLayerPaths []string) Mount
+
+// WorkDirOptions configure how a new layer workdir is created.
+type WorkDirOptions struct {
+ NewOverlayMount NewOverlayMountFn
+}
+
+// NewLayerWorkdirOption configures how a new layer workdir is created.
+type NewLayerWorkdirOption func(*WorkDirOptions)
+
+// WithNewOverlayMountFn configures how a new layer workdir creates an overlay
+// mount.
+func WithNewOverlayMountFn(fn NewOverlayMountFn) NewLayerWorkdirOption {
+ return func(wdo *WorkDirOptions) {
+ wdo.NewOverlayMount = fn
+ }
+}
+
+// DefaultNewOverlayMount is the default OverlayMount created by NewLayerWorkdir.
+func DefaultNewOverlayMount(path string, parentLayerPaths []string) Mount {
+ om := OverlayMount{
+ Lower: []string{filepath.Join(path, overlayDirLower)},
+ Upper: filepath.Join(path, overlayDirUpper),
+ Work: filepath.Join(path, overlayDirWork),
+ Mountpoint: filepath.Join(path, overlayDirMerged),
+ }
+
+ if len(parentLayerPaths) != 0 {
+ om.Lower = parentLayerPaths
+ }
+ return om
+}
+
+// NewLayerWorkdir returns a temporary directory used to produce an overlayfs
+// layer from an OCI layer.
+func NewLayerWorkdir(dir, digest string, parentLayerPaths []string, o ...NewLayerWorkdirOption) (LayerWorkdir, error) {
+ opts := &WorkDirOptions{
+ NewOverlayMount: DefaultNewOverlayMount,
+ }
+
+ for _, fn := range o {
+ fn(opts)
+ }
+
+ if err := os.MkdirAll(dir, 0700); err != nil {
+ return LayerWorkdir{}, errors.Wrap(err, errMkdirTemp)
+ }
+ tmp, err := os.MkdirTemp(dir, fmt.Sprintf("%s-", digest))
+ if err != nil {
+ return LayerWorkdir{}, errors.Wrap(err, errMkdirTemp)
+ }
+
+ for _, d := range []string{overlayDirMerged, overlayDirUpper, overlayDirLower, overlayDirWork} {
+ if err := os.Mkdir(filepath.Join(tmp, d), 0700); err != nil {
+ _ = os.RemoveAll(tmp)
+ return LayerWorkdir{}, errors.Wrapf(err, errFmtMkOverlayDir, d)
+ }
+ }
+
+ om := opts.NewOverlayMount(tmp, parentLayerPaths)
+ if err := om.Mount(); err != nil {
+ _ = os.RemoveAll(tmp)
+ return LayerWorkdir{}, errors.Wrap(err, errMountOverlayfs)
+ }
+
+ return LayerWorkdir{overlay: om, path: tmp}, nil
+}
+
+// ApplyPath returns the path an OCI layer should be applied (i.e. extracted) to
+// in order to create an overlayfs layer.
+func (d LayerWorkdir) ApplyPath() string {
+ return filepath.Join(d.path, overlayDirMerged)
+}
+
+// ResultPath returns the path of the resulting overlayfs layer.
+func (d LayerWorkdir) ResultPath() string {
+ return filepath.Join(d.path, overlayDirUpper)
+}
+
+// Cleanup the temporary directory.
+func (d LayerWorkdir) Cleanup() error {
+ if err := d.overlay.Unmount(); err != nil {
+ return errors.Wrap(err, "cannot unmount workdir overlayfs")
+ }
+ return errors.Wrap(os.RemoveAll(d.path), "cannot remove workdir")
+}
diff --git a/internal/oci/store/overlay/store_overlay_linux.go b/internal/oci/store/overlay/store_overlay_linux.go
new file mode 100644
index 0000000..49eeb1e
--- /dev/null
+++ b/internal/oci/store/overlay/store_overlay_linux.go
@@ -0,0 +1,59 @@
+//go:build linux
+
+/*
+Copyright 2022 The Crossplane 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 overlay
+
+import (
+ "fmt"
+ "strings"
+
+ "golang.org/x/sys/unix"
+
+ "github.com/crossplane/crossplane-runtime/pkg/errors"
+)
+
+// NOTE(negz): Technically _all_ of the overlay implementation is only useful on
+// Linux, but we want to support building what we can on other operating systems
+// (e.g. Darwin) to make it possible for folks running them to ensure that code
+// compiles and passes tests during development. Avoid adding code to this file
+// unless it actually needs Linux to run.
+
+// Mount the tmpfs mount.
+func (m TmpFSMount) Mount() error {
+ var flags uintptr
+ return errors.Wrapf(unix.Mount("tmpfs", m.Mountpoint, "tmpfs", flags, ""), "cannot mount tmpfs at %q", m.Mountpoint)
+}
+
+// Unmount the tmpfs mount.
+func (m TmpFSMount) Unmount() error {
+ var flags int
+ return errors.Wrapf(unix.Unmount(m.Mountpoint, flags), "cannot unmount tmpfs at %q", m.Mountpoint)
+}
+
+// Mount the overlay mount.
+func (m OverlayMount) Mount() error {
+ var flags uintptr
+ data := fmt.Sprintf("lowerdir=%s,upperdir=%s,workdir=%s", strings.Join(m.Lower, ":"), m.Upper, m.Work)
+ return errors.Wrapf(unix.Mount("overlay", m.Mountpoint, "overlay", flags, data), "cannot mount overlayfs at %q", m.Mountpoint)
+}
+
+// Unmount the overlay mount.
+func (m OverlayMount) Unmount() error {
+ var flags int
+ return errors.Wrapf(unix.Unmount(m.Mountpoint, flags), "cannot unmount overlayfs at %q", m.Mountpoint)
+}
diff --git a/internal/oci/store/overlay/store_overlay_nonlinux.go b/internal/oci/store/overlay/store_overlay_nonlinux.go
new file mode 100644
index 0000000..bb820e6
--- /dev/null
+++ b/internal/oci/store/overlay/store_overlay_nonlinux.go
@@ -0,0 +1,37 @@
+//go:build !linux
+
+/*
+Copyright 2022 The Crossplane 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 overlay
+
+import (
+ "github.com/crossplane/crossplane-runtime/pkg/errors"
+)
+
+const errLinuxOnly = "overlayfs is only only supported on Linux"
+
+// Mount returns an error on non-Linux systems.
+func (m TmpFSMount) Mount() error { return errors.New(errLinuxOnly) }
+
+// Unmount returns an error on non-Linux systems.
+func (m TmpFSMount) Unmount() error { return errors.New(errLinuxOnly) }
+
+// Mount returns an error on non-Linux systems.
+func (m OverlayMount) Mount() error { return errors.New(errLinuxOnly) }
+
+// Unmount returns an error on non-Linux systems.
+func (m OverlayMount) Unmount() error { return errors.New(errLinuxOnly) }
diff --git a/internal/oci/store/overlay/store_overlay_test.go b/internal/oci/store/overlay/store_overlay_test.go
new file mode 100644
index 0000000..71d2554
--- /dev/null
+++ b/internal/oci/store/overlay/store_overlay_test.go
@@ -0,0 +1,453 @@
+/*
+Copyright 2022 The Crossplane 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 overlay
+
+import (
+ "context"
+ "io"
+ "os"
+ "path/filepath"
+ "strings"
+ "testing"
+
+ "github.com/google/go-cmp/cmp"
+ ociv1 "github.com/google/go-containerregistry/pkg/v1"
+
+ "github.com/crossplane/crossplane-runtime/pkg/errors"
+ "github.com/crossplane/crossplane-runtime/pkg/test"
+
+ "github.com/crossplane/function-runtime-oci/internal/oci/spec"
+ "github.com/crossplane/function-runtime-oci/internal/oci/store"
+)
+
+type MockImage struct {
+ ociv1.Image
+
+ MockDigest func() (ociv1.Hash, error)
+ MockConfigFile func() (*ociv1.ConfigFile, error)
+ MockLayers func() ([]ociv1.Layer, error)
+}
+
+func (i *MockImage) Digest() (ociv1.Hash, error) { return i.MockDigest() }
+func (i *MockImage) ConfigFile() (*ociv1.ConfigFile, error) { return i.MockConfigFile() }
+func (i *MockImage) Layers() ([]ociv1.Layer, error) { return i.MockLayers() }
+
+type MockLayer struct {
+ ociv1.Layer
+
+ MockDiffID func() (ociv1.Hash, error)
+ MockUncompressed func() (io.ReadCloser, error)
+}
+
+func (l *MockLayer) DiffID() (ociv1.Hash, error) { return l.MockDiffID() }
+func (l *MockLayer) Uncompressed() (io.ReadCloser, error) { return l.MockUncompressed() }
+
+type MockLayerResolver struct {
+ path string
+ err error
+}
+
+func (r *MockLayerResolver) Resolve(_ context.Context, _ ociv1.Layer, _ ...ociv1.Layer) (string, error) {
+ return r.path, r.err
+}
+
+type MockTarballApplicator struct{ err error }
+
+func (a *MockTarballApplicator) Apply(_ context.Context, _ io.Reader, _ string) error { return a.err }
+
+type MockRuntimeSpecWriter struct{ err error }
+
+func (c *MockRuntimeSpecWriter) Write(_ string, _ ...spec.Option) error { return c.err }
+
+type MockCloser struct {
+ io.Reader
+
+ err error
+}
+
+func (c *MockCloser) Close() error { return c.err }
+
+type MockMount struct{ err error }
+
+func (m *MockMount) Mount() error { return m.err }
+func (m *MockMount) Unmount() error { return m.err }
+
+func TestBundle(t *testing.T) {
+ errBoom := errors.New("boom")
+
+ type params struct {
+ layer LayerResolver
+ bundle BundleBootstrapper
+ spec RuntimeSpecWriter
+ }
+ type args struct {
+ ctx context.Context
+ i ociv1.Image
+ id string
+ o []spec.Option
+ }
+ type want struct {
+ b store.Bundle
+ err error
+ }
+
+ cases := map[string]struct {
+ reason string
+ params params
+ args args
+ want want
+ }{
+ "ReadConfigFileError": {
+ reason: "We should return any error encountered reading the image's config file.",
+ params: params{},
+ args: args{
+ i: &MockImage{
+ MockConfigFile: func() (*ociv1.ConfigFile, error) { return nil, errBoom },
+ },
+ },
+ want: want{
+ err: errors.Wrap(errBoom, errReadConfigFile),
+ },
+ },
+ "GetLayersError": {
+ reason: "We should return any error encountered reading the image's layers.",
+ params: params{},
+ args: args{
+ i: &MockImage{
+ MockConfigFile: func() (*ociv1.ConfigFile, error) { return nil, nil },
+ MockLayers: func() ([]ociv1.Layer, error) { return nil, errBoom },
+ },
+ },
+ want: want{
+ err: errors.Wrap(errBoom, errGetLayers),
+ },
+ },
+ "ResolveLayerError": {
+ reason: "We should return any error encountered opening an image's layers.",
+ params: params{
+ layer: &MockLayerResolver{err: errBoom},
+ },
+ args: args{
+ i: &MockImage{
+ MockConfigFile: func() (*ociv1.ConfigFile, error) { return nil, nil },
+ MockLayers: func() ([]ociv1.Layer, error) {
+ return []ociv1.Layer{&MockLayer{}}, nil
+ },
+ },
+ },
+ want: want{
+ err: errors.Wrap(errBoom, errResolveLayer),
+ },
+ },
+ "BootstrapBundleError": {
+ reason: "We should return any error encountered bootstrapping a bundle rootfs.",
+ params: params{
+ layer: &MockLayerResolver{err: nil},
+ bundle: BundleBootstrapperFn(func(path string, parentLayerPaths []string) (Bundle, error) {
+ return Bundle{}, errBoom
+ }),
+ },
+ args: args{
+ i: &MockImage{
+ MockConfigFile: func() (*ociv1.ConfigFile, error) { return nil, nil },
+ MockLayers: func() ([]ociv1.Layer, error) { return nil, nil },
+ },
+ },
+ want: want{
+ err: errors.Wrap(errBoom, errBootstrapBundle),
+ },
+ },
+ "WriteSpecError": {
+ reason: "We should return any error encountered writing a runtime spec to the bundle.",
+ params: params{
+ layer: &MockLayerResolver{err: nil},
+ bundle: BundleBootstrapperFn(func(path string, parentLayerPaths []string) (Bundle, error) {
+ return Bundle{}, nil
+ }),
+ spec: &MockRuntimeSpecWriter{err: errBoom},
+ },
+ args: args{
+ i: &MockImage{
+ MockConfigFile: func() (*ociv1.ConfigFile, error) { return nil, nil },
+ MockLayers: func() ([]ociv1.Layer, error) { return nil, nil },
+ },
+ },
+ want: want{
+ err: errors.Wrap(errBoom, errWriteRuntimeSpec),
+ },
+ },
+ "Success": {
+ reason: "We should successfully return our Bundle.",
+ params: params{
+ layer: &MockLayerResolver{err: nil},
+ bundle: BundleBootstrapperFn(func(path string, parentLayerPaths []string) (Bundle, error) {
+ return Bundle{path: "/coolbundle"}, nil
+ }),
+ spec: &MockRuntimeSpecWriter{err: nil},
+ },
+ args: args{
+ i: &MockImage{
+ MockConfigFile: func() (*ociv1.ConfigFile, error) { return nil, nil },
+ MockLayers: func() ([]ociv1.Layer, error) {
+ return []ociv1.Layer{&MockLayer{}}, nil
+ },
+ },
+ },
+ want: want{
+ b: Bundle{path: "/coolbundle"},
+ },
+ },
+ }
+
+ for name, tc := range cases {
+ t.Run(name, func(t *testing.T) {
+ tmp, err := os.MkdirTemp(os.TempDir(), strings.ReplaceAll(t.Name(), string(os.PathSeparator), "_"))
+ if err != nil {
+ t.Fatal(err.Error())
+ }
+ defer os.RemoveAll(tmp)
+
+ c := &CachingBundler{
+ root: tmp,
+ layer: tc.params.layer,
+ bundle: tc.params.bundle,
+ spec: tc.params.spec,
+ }
+
+ got, err := c.Bundle(tc.args.ctx, tc.args.i, tc.args.id, tc.args.o...)
+
+ if diff := cmp.Diff(tc.want.b, got, cmp.AllowUnexported(Bundle{})); diff != "" {
+ t.Errorf("\n%s\nBundle(...): -want, +got:\n%s", tc.reason, diff)
+ }
+ if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" {
+ t.Errorf("\n%s\nBundle(...): -want error, +got error:\n%s", tc.reason, diff)
+ }
+ })
+ }
+}
+
+func TestResolve(t *testing.T) {
+ errBoom := errors.New("boom")
+
+ type params struct {
+ tarball TarballApplicator
+ wdopts []NewLayerWorkdirOption
+ }
+ type args struct {
+ ctx context.Context
+ l ociv1.Layer
+ parents []ociv1.Layer
+ }
+ type want struct {
+ path string
+ err error
+ }
+
+ cases := map[string]struct {
+ reason string
+ files map[string][]byte
+ params params
+ args args
+ want want
+ }{
+ "DiffIDError": {
+ reason: "We should return any error encountered getting the uncompressed layer's digest.",
+ args: args{
+ l: &MockLayer{
+ MockDiffID: func() (ociv1.Hash, error) { return ociv1.Hash{}, errBoom },
+ },
+ },
+ want: want{
+ err: errors.Wrap(errBoom, errGetDigest),
+ },
+ },
+ "SuccessExistingLayer": {
+ reason: "We should skip straight to returning the layer if it already exists.",
+ files: map[string][]byte{
+ "sha256/deadbeef": nil,
+ },
+ args: args{
+ l: &MockLayer{
+ MockDiffID: func() (ociv1.Hash, error) {
+ return ociv1.Hash{Algorithm: "sha256", Hex: "deadbeef"}, nil
+ },
+ },
+ },
+ want: want{
+ path: "/sha256/deadbeef",
+ },
+ },
+ "FetchLayerError": {
+ reason: "We should return any error we encounter while fetching a layer.",
+ args: args{
+ l: &MockLayer{
+ MockDiffID: func() (ociv1.Hash, error) {
+ return ociv1.Hash{Algorithm: "sha256", Hex: "deadbeef"}, nil
+ },
+ MockUncompressed: func() (io.ReadCloser, error) { return nil, errBoom },
+ },
+ },
+ want: want{
+ err: errors.Wrap(errBoom, errFetchLayer),
+ },
+ },
+ "ParentDiffIDError": {
+ reason: "We should return any error we encounter while fetching a parent's uncompressed digest.",
+ args: args{
+ l: &MockLayer{
+ MockDiffID: func() (ociv1.Hash, error) {
+ return ociv1.Hash{Algorithm: "sha256", Hex: "deadbeef"}, nil
+ },
+ MockUncompressed: func() (io.ReadCloser, error) { return nil, nil },
+ },
+ parents: []ociv1.Layer{
+ &MockLayer{
+ MockDiffID: func() (ociv1.Hash, error) {
+ return ociv1.Hash{}, errBoom
+ },
+ },
+ },
+ },
+ want: want{
+ err: errors.Wrap(errBoom, errGetDigest),
+ },
+ },
+ "NewLayerWorkDirMountOverlayError": {
+ reason: "We should return any error we encounter when mounting our overlayfs",
+ params: params{
+ wdopts: []NewLayerWorkdirOption{
+ WithNewOverlayMountFn(func(path string, parentLayerPaths []string) Mount {
+ return &MockMount{err: errBoom}
+ }),
+ },
+ },
+ args: args{
+ l: &MockLayer{
+ MockDiffID: func() (ociv1.Hash, error) {
+ return ociv1.Hash{Algorithm: "sha256", Hex: "deadbeef"}, nil
+ },
+ MockUncompressed: func() (io.ReadCloser, error) { return nil, nil },
+ },
+ parents: []ociv1.Layer{
+ &MockLayer{
+ MockDiffID: func() (ociv1.Hash, error) {
+ return ociv1.Hash{Algorithm: "sha256", Hex: "badc0ffee"}, nil
+ },
+ },
+ },
+ },
+ want: want{
+ err: errors.Wrap(errors.Wrap(errBoom, errMountOverlayfs), errMkWorkdir),
+ },
+ },
+ "ApplyTarballError": {
+ reason: "We should return any error we encounter while applying our layer tarball.",
+ params: params{
+ tarball: &MockTarballApplicator{err: errBoom},
+ wdopts: []NewLayerWorkdirOption{
+ WithNewOverlayMountFn(func(path string, parentLayerPaths []string) Mount {
+ return &MockMount{err: nil}
+ }),
+ },
+ },
+ args: args{
+ l: &MockLayer{
+ MockDiffID: func() (ociv1.Hash, error) {
+ return ociv1.Hash{Algorithm: "sha256", Hex: "deadbeef"}, nil
+ },
+ MockUncompressed: func() (io.ReadCloser, error) { return nil, nil },
+ },
+ parents: []ociv1.Layer{
+ &MockLayer{
+ MockDiffID: func() (ociv1.Hash, error) {
+ return ociv1.Hash{Algorithm: "sha256", Hex: "badc0ffee"}, nil
+ },
+ },
+ },
+ },
+ want: want{
+ err: errors.Wrap(errBoom, errApplyLayer),
+ },
+ },
+ "SuccessNewlyCachedLayer": {
+ reason: "We should return the path to our successfully cached layer.",
+ params: params{
+ tarball: &MockTarballApplicator{},
+ wdopts: []NewLayerWorkdirOption{
+ WithNewOverlayMountFn(func(path string, parentLayerPaths []string) Mount {
+ return &MockMount{err: nil}
+ }),
+ },
+ },
+ args: args{
+ l: &MockLayer{
+ MockDiffID: func() (ociv1.Hash, error) {
+ return ociv1.Hash{Algorithm: "sha256", Hex: "deadbeef"}, nil
+ },
+ MockUncompressed: func() (io.ReadCloser, error) { return nil, nil },
+ },
+ parents: []ociv1.Layer{
+ &MockLayer{
+ MockDiffID: func() (ociv1.Hash, error) {
+ return ociv1.Hash{Algorithm: "sha256", Hex: "badc0ffee"}, nil
+ },
+ },
+ },
+ },
+ want: want{
+ path: "/sha256/deadbeef",
+ },
+ },
+ }
+
+ for name, tc := range cases {
+ t.Run(name, func(t *testing.T) {
+ tmp, err := os.MkdirTemp(os.TempDir(), strings.ReplaceAll(t.Name(), string(os.PathSeparator), "_"))
+ if err != nil {
+ t.Fatal(err.Error())
+ }
+ defer os.RemoveAll(tmp)
+
+ for name, data := range tc.files {
+ path := filepath.Join(tmp, name)
+ _ = os.MkdirAll(filepath.Dir(path), 0700)
+ _ = os.WriteFile(path, data, 0600)
+ }
+
+ c := &CachingLayerResolver{
+ root: tmp,
+ tarball: tc.params.tarball,
+ wdopts: tc.params.wdopts,
+ }
+
+ // Prepend our randomly named tmp dir to our wanted layer path.
+ wantPath := tc.want.path
+ if tc.want.path != "" {
+ wantPath = filepath.Join(tmp, tc.want.path)
+ }
+
+ path, err := c.Resolve(tc.args.ctx, tc.args.l, tc.args.parents...)
+
+ if diff := cmp.Diff(wantPath, path); diff != "" {
+ t.Errorf("\n%s\nResolve(...): -want, +got:\n%s", tc.reason, diff)
+ }
+ if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" {
+ t.Errorf("\n%s\nResolve(...): -want error, +got error:\n%s", tc.reason, diff)
+ }
+ })
+ }
+}
diff --git a/internal/oci/store/store.go b/internal/oci/store/store.go
new file mode 100644
index 0000000..dc7c90d
--- /dev/null
+++ b/internal/oci/store/store.go
@@ -0,0 +1,371 @@
+/*
+Copyright 2022 The Crossplane 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 store implements OCI container storage.
+package store
+
+import (
+ "context"
+ "crypto/sha256"
+ "fmt"
+ "io"
+ "os"
+ "path/filepath"
+
+ "github.com/google/go-containerregistry/pkg/name"
+ ociv1 "github.com/google/go-containerregistry/pkg/v1"
+ v1 "github.com/google/go-containerregistry/pkg/v1"
+ "github.com/google/go-containerregistry/pkg/v1/partial"
+ "github.com/google/go-containerregistry/pkg/v1/types"
+ "github.com/google/go-containerregistry/pkg/v1/validate"
+ "golang.org/x/sync/errgroup"
+
+ "github.com/crossplane/crossplane-runtime/pkg/errors"
+
+ "github.com/crossplane/function-runtime-oci/internal/oci/spec"
+)
+
+// Store directories.
+// Shorter is better, to avoid passing too much data to the mount syscall when
+// creating an overlay mount with many layers as lower directories.
+const (
+ DirDigests = "d"
+ DirImages = "i"
+ DirOverlays = "o"
+ DirContainers = "c"
+)
+
+// Bundle paths.
+const (
+ DirRootFS = "rootfs"
+ FileConfig = "config.json"
+ FileSpec = "config.json"
+)
+
+// Error strings
+const (
+ errMkDigestStore = "cannot make digest store"
+ errReadDigest = "cannot read digest"
+ errParseDigest = "cannot parse digest"
+ errStoreDigest = "cannot store digest"
+ errPartial = "cannot complete partial implementation" // This should never happen.
+ errInvalidImage = "stored image is invalid"
+ errGetDigest = "cannot get digest"
+ errMkAlgoDir = "cannot create store directory"
+ errGetRawConfigFile = "cannot get image config file"
+ errMkTmpfile = "cannot create temporary layer file"
+ errReadLayer = "cannot read layer"
+ errMvTmpfile = "cannot move temporary layer file"
+ errOpenConfigFile = "cannot open image config file"
+ errWriteLayers = "cannot write image layers"
+ errInvalidLayer = "stored layer is invalid"
+ errWriteConfigFile = "cannot write image config file"
+ errGetLayers = "cannot get image layers"
+ errWriteLayer = "cannot write layer"
+ errOpenLayer = "cannot open layer"
+ errStatLayer = "cannot stat layer"
+ errCheckExistence = "cannot determine whether layer exists"
+ errFmtTooManyLayers = "image has too many layers: %d (max %d)"
+)
+
+var (
+ // MaxLayers is the maximum number of layers an image can have.
+ MaxLayers = 256
+)
+
+// A Bundler prepares OCI runtime bundles for use by an OCI runtime.
+type Bundler interface {
+ // Bundle returns an OCI bundle ready for use by an OCI runtime.
+ Bundle(ctx context.Context, i ociv1.Image, id string, o ...spec.Option) (Bundle, error)
+}
+
+// A Bundle for use by an OCI runtime.
+type Bundle interface {
+ // Path of the OCI bundle.
+ Path() string
+
+ // Cleanup the OCI bundle after the container has finished running.
+ Cleanup() error
+}
+
+// A Digest store is used to map OCI references to digests. Each mapping is a
+// file. The filename is the SHA256 hash of the reference, and the content is
+// the digest in algo:hex format.
+type Digest struct{ root string }
+
+// NewDigest returns a store used to map OCI references to digests.
+func NewDigest(root string) (*Digest, error) {
+ // We only use sha256 hashes. The sha256 subdirectory is for symmetry with
+ // the other stores, which at least hypothetically support other hashes.
+ path := filepath.Join(root, DirDigests, "sha256")
+ err := os.MkdirAll(path, 0700)
+ return &Digest{root: path}, errors.Wrap(err, errMkDigestStore)
+}
+
+// Hash returns the stored hash for the supplied reference.
+func (d *Digest) Hash(r name.Reference) (ociv1.Hash, error) {
+ b, err := os.ReadFile(d.path(r))
+ if err != nil {
+ return ociv1.Hash{}, errors.Wrap(err, errReadDigest)
+ }
+ h, err := ociv1.NewHash(string(b))
+ return h, errors.Wrap(err, errParseDigest)
+}
+
+// WriteHash maps the supplied reference to the supplied hash.
+func (d *Digest) WriteHash(r name.Reference, h ociv1.Hash) error {
+ return errors.Wrap(os.WriteFile(d.path(r), []byte(h.String()), 0600), errStoreDigest)
+}
+
+func (d *Digest) path(r name.Reference) string {
+ return filepath.Join(d.root, fmt.Sprintf("%x", sha256.Sum256([]byte(r.String()))))
+}
+
+// An Image store is used to store OCI images and their layers. It uses a
+// similar disk layout to the blobs directory of an OCI image layout, but may
+// contain blobs for more than one image. Layers are stored as uncompressed
+// tarballs in order to speed up extraction by the uncompressed Bundler, which
+// extracts a fresh root filesystem each time a container is run.
+// https://github.com/opencontainers/image-spec/blob/v1.0/image-layout.md
+type Image struct{ root string }
+
+// NewImage returns a store used to store OCI images and their layers.
+func NewImage(root string) *Image {
+ return &Image{root: filepath.Join(root, DirImages)}
+}
+
+// Image returns the stored image with the supplied hash, if any.
+func (i *Image) Image(h ociv1.Hash) (ociv1.Image, error) {
+ uncompressed := image{root: i.root, h: h}
+
+ // NOTE(negz): At the time of writing UncompressedToImage doesn't actually
+ // return an error.
+ oi, err := partial.UncompressedToImage(uncompressed)
+ if err != nil {
+ return nil, errors.Wrap(err, errPartial)
+ }
+
+ // This validates the image's manifest, config file, and layers. The
+ // manifest and config file are validated fairly extensively (i.e. their
+ // size, digest, etc must be correct). Layers are only validated to exist.
+ return oi, errors.Wrap(validate.Image(oi, validate.Fast), errInvalidImage)
+}
+
+// WriteImage writes the supplied image to the store.
+func (i *Image) WriteImage(img ociv1.Image) error { //nolint:gocyclo // TODO(phisco): Refactor to reduce complexity.
+ d, err := img.Digest()
+ if err != nil {
+ return errors.Wrap(err, errGetDigest)
+ }
+
+ if _, err = i.Image(d); err == nil {
+ // Image already exists in the store.
+ return nil
+ }
+
+ path := filepath.Join(i.root, d.Algorithm, d.Hex)
+
+ if err := os.MkdirAll(filepath.Join(i.root, d.Algorithm), 0700); err != nil {
+ return errors.Wrap(err, errMkAlgoDir)
+ }
+
+ raw, err := img.RawConfigFile()
+ if err != nil {
+ return errors.Wrap(err, errGetRawConfigFile)
+ }
+
+ // CreateTemp creates a file with permission mode 0600.
+ tmp, err := os.CreateTemp(filepath.Join(i.root, d.Algorithm), fmt.Sprintf("%s-", d.Hex))
+ if err != nil {
+ return errors.Wrap(err, errMkTmpfile)
+ }
+
+ if err := os.WriteFile(tmp.Name(), raw, 0600); err != nil {
+ _ = os.Remove(tmp.Name())
+ return errors.Wrap(err, errWriteConfigFile)
+ }
+
+ // TODO(negz): Ignore os.ErrExist? We might get one here if two callers race
+ // to cache the same image.
+ if err := os.Rename(tmp.Name(), path); err != nil {
+ _ = os.Remove(tmp.Name())
+ return errors.Wrap(err, errMvTmpfile)
+ }
+
+ layers, err := img.Layers()
+ if err != nil {
+ return errors.Wrap(err, errGetLayers)
+ }
+
+ if err := Validate(img); err != nil {
+ return err
+ }
+
+ g := &errgroup.Group{}
+ for _, l := range layers {
+ l := l // Pin loop var.
+ g.Go(func() error {
+ return i.WriteLayer(l)
+ })
+ }
+
+ return errors.Wrap(g.Wait(), errWriteLayers)
+}
+
+// Layer returns the stored layer with the supplied hash, if any.
+func (i *Image) Layer(h ociv1.Hash) (ociv1.Layer, error) {
+ uncompressed := layer{root: i.root, h: h}
+
+ // NOTE(negz): At the time of writing UncompressedToLayer doesn't actually
+ // return an error.
+ ol, err := partial.UncompressedToLayer(uncompressed)
+ if err != nil {
+ return nil, errors.Wrap(err, errPartial)
+ }
+
+ // This just validates that the layer exists on disk.
+ return ol, errors.Wrap(validate.Layer(ol, validate.Fast), errInvalidLayer)
+}
+
+// WriteLayer writes the supplied layer to the store.
+func (i *Image) WriteLayer(l ociv1.Layer) error {
+ d, err := l.DiffID() // The digest of the uncompressed layer.
+ if err != nil {
+ return errors.Wrap(err, errGetDigest)
+ }
+
+ if _, err := i.Layer(d); err == nil {
+ // Layer already exists in the store.
+ return nil
+ }
+
+ if err := os.MkdirAll(filepath.Join(i.root, d.Algorithm), 0700); err != nil {
+ return errors.Wrap(err, errMkAlgoDir)
+ }
+
+ // CreateTemp creates a file with permission mode 0600.
+ tmp, err := os.CreateTemp(filepath.Join(i.root, d.Algorithm), fmt.Sprintf("%s-", d.Hex))
+ if err != nil {
+ return errors.Wrap(err, errMkTmpfile)
+ }
+
+ // This call to Uncompressed is what actually pulls the layer.
+ u, err := l.Uncompressed()
+ if err != nil {
+ _ = os.Remove(tmp.Name())
+ return errors.Wrap(err, errReadLayer)
+ }
+
+ if _, err := copyChunks(tmp, u, 1024*1024); err != nil { // Copy 1MB chunks.
+ _ = os.Remove(tmp.Name())
+ return errors.Wrap(err, errWriteLayer)
+ }
+
+ // TODO(negz): Ignore os.ErrExist? We might get one here if two callers race
+ // to cache the same layer.
+ if err := os.Rename(tmp.Name(), filepath.Join(i.root, d.Algorithm, d.Hex)); err != nil {
+ _ = os.Remove(tmp.Name())
+ return errors.Wrap(err, errMvTmpfile)
+ }
+
+ return nil
+}
+
+// image implements partial.UncompressedImage per
+// https://pkg.go.dev/github.com/google/go-containerregistry/pkg/v1/partial
+type image struct {
+ root string
+ h ociv1.Hash
+}
+
+func (i image) RawConfigFile() ([]byte, error) {
+ b, err := os.ReadFile(filepath.Join(i.root, i.h.Algorithm, i.h.Hex))
+ return b, errors.Wrap(err, errOpenConfigFile)
+}
+
+func (i image) MediaType() (types.MediaType, error) {
+ return types.OCIManifestSchema1, nil
+}
+
+func (i image) LayerByDiffID(h ociv1.Hash) (partial.UncompressedLayer, error) {
+ return layer{root: i.root, h: h}, nil
+}
+
+// layer implements partial.UncompressedLayer per
+// https://pkg.go.dev/github.com/google/go-containerregistry/pkg/v1/partial
+type layer struct {
+ root string
+ h ociv1.Hash
+}
+
+func (l layer) DiffID() (v1.Hash, error) {
+ return l.h, nil
+}
+
+func (l layer) Uncompressed() (io.ReadCloser, error) {
+ f, err := os.Open(filepath.Join(l.root, l.h.Algorithm, l.h.Hex))
+ return f, errors.Wrap(err, errOpenLayer)
+}
+
+func (l layer) MediaType() (types.MediaType, error) {
+ return types.OCIUncompressedLayer, nil
+}
+
+// Exists satisfies partial.Exists, which is used to validate the image when
+// validate.Image or validate.Layer is run with the validate.Fast option.
+func (l layer) Exists() (bool, error) {
+ _, err := os.Stat(filepath.Join(l.root, l.h.Algorithm, l.h.Hex))
+ if errors.Is(err, os.ErrNotExist) {
+ return false, nil
+ }
+ if err != nil {
+ return false, errors.Wrap(err, errStatLayer)
+ }
+ return true, nil
+}
+
+// copyChunks pleases gosec per https://github.com/securego/gosec/pull/433.
+// Like Copy it reads from src until EOF, it does not treat an EOF from Read as
+// an error to be reported.
+//
+// NOTE(negz): This rule confused me at first because io.Copy appears to use a
+// buffer, but in fact it bypasses it if src/dst is an io.WriterTo/ReaderFrom.
+func copyChunks(dst io.Writer, src io.Reader, chunkSize int64) (int64, error) {
+ var written int64
+ for {
+ w, err := io.CopyN(dst, src, chunkSize)
+ written += w
+ if errors.Is(err, io.EOF) {
+ return written, nil
+ }
+ if err != nil {
+ return written, err
+ }
+ }
+}
+
+// Validate returns an error if the supplied image is invalid,
+// e.g. the number of layers is above the maximum allowed.
+func Validate(img ociv1.Image) error {
+ layers, err := img.Layers()
+ if err != nil {
+ return errors.Wrap(err, errGetLayers)
+ }
+ if nLayers := len(layers); nLayers > MaxLayers {
+ return errors.Errorf(errFmtTooManyLayers, nLayers, MaxLayers)
+ }
+ return nil
+}
diff --git a/internal/oci/store/store_test.go b/internal/oci/store/store_test.go
new file mode 100644
index 0000000..67aa956
--- /dev/null
+++ b/internal/oci/store/store_test.go
@@ -0,0 +1,353 @@
+/*
+Copyright 2022 The Crossplane 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 store implements OCI container storage.
+package store
+
+import (
+ "io"
+ "os"
+ "path/filepath"
+ "strings"
+ "testing"
+
+ "github.com/google/go-cmp/cmp"
+ "github.com/google/go-cmp/cmp/cmpopts"
+ "github.com/google/go-containerregistry/pkg/name"
+ ociv1 "github.com/google/go-containerregistry/pkg/v1"
+
+ "github.com/crossplane/crossplane-runtime/pkg/errors"
+ "github.com/crossplane/crossplane-runtime/pkg/test"
+)
+
+type MockImage struct {
+ ociv1.Image
+
+ MockDigest func() (ociv1.Hash, error)
+ MockRawConfigFile func() ([]byte, error)
+ MockLayers func() ([]ociv1.Layer, error)
+}
+
+func (i *MockImage) Digest() (ociv1.Hash, error) { return i.MockDigest() }
+func (i *MockImage) RawConfigFile() ([]byte, error) { return i.MockRawConfigFile() }
+func (i *MockImage) Layers() ([]ociv1.Layer, error) { return i.MockLayers() }
+
+type MockLayer struct {
+ ociv1.Layer
+
+ MockDiffID func() (ociv1.Hash, error)
+ MockUncompressed func() (io.ReadCloser, error)
+}
+
+func (l *MockLayer) DiffID() (ociv1.Hash, error) { return l.MockDiffID() }
+func (l *MockLayer) Uncompressed() (io.ReadCloser, error) { return l.MockUncompressed() }
+
+func TestHash(t *testing.T) {
+ type args struct {
+ r name.Reference
+ }
+ type want struct {
+ h ociv1.Hash
+ err error
+ }
+
+ cases := map[string]struct {
+ reason string
+ files map[string][]byte
+ args args
+ want want
+ }{
+ "ReadError": {
+ reason: "We should return any error encountered reading the stored hash.",
+ args: args{
+ r: name.MustParseReference("example.org/image"),
+ },
+ want: want{
+ // Note we're matching with cmpopts.EquateErrors, which only
+ // cares that the returned error errors.Is() this one.
+ err: os.ErrNotExist,
+ },
+ },
+ "ParseError": {
+ reason: "We should return any error encountered reading the stored hash.",
+ files: map[string][]byte{
+ "276640b463239572f62edd97253f05e0de082e9888f57dac0b83d2149efa59e0": []byte("wat"),
+ },
+ args: args{
+ r: name.MustParseReference("example.org/image"),
+ },
+ want: want{
+ err: cmpopts.AnyError,
+ },
+ },
+ "SuccessfulRead": {
+ reason: "We should return the stored hash.",
+ files: map[string][]byte{
+ "276640b463239572f62edd97253f05e0de082e9888f57dac0b83d2149efa59e0": []byte("sha256:c34045c1a1db8d1b3fca8a692198466952daae07eaf6104b4c87ed3b55b6af1b"),
+ },
+ args: args{
+ r: name.MustParseReference("example.org/image"),
+ },
+ want: want{
+ h: ociv1.Hash{
+ Algorithm: "sha256",
+ Hex: "c34045c1a1db8d1b3fca8a692198466952daae07eaf6104b4c87ed3b55b6af1b",
+ },
+ err: nil,
+ },
+ },
+ }
+
+ for name, tc := range cases {
+ t.Run(name, func(t *testing.T) {
+ tmp, err := os.MkdirTemp(os.TempDir(), strings.ReplaceAll(t.Name(), string(os.PathSeparator), "_"))
+ if err != nil {
+ t.Fatal(err.Error())
+ }
+ t.Cleanup(func() {
+ os.RemoveAll(tmp)
+ })
+
+ for name, data := range tc.files {
+ path := filepath.Join(tmp, DirDigests, "sha256", name)
+ _ = os.MkdirAll(filepath.Dir(path), 0700)
+ _ = os.WriteFile(path, data, 0600)
+ }
+
+ c, err := NewDigest(tmp)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ h, err := c.Hash(tc.args.r)
+ if diff := cmp.Diff(tc.want.h, h); diff != "" {
+ t.Errorf("\n%s\nHash(...): -want, +got:\n%s", tc.reason, diff)
+ }
+ // Note cmpopts.EquateErrors, not the usual testing.EquateErrors
+ // from crossplane-runtime. We need this to support cmpopts.AnyError.
+ if diff := cmp.Diff(tc.want.err, err, cmpopts.EquateErrors()); diff != "" {
+ t.Errorf("\n%s\nHash(...): -want error, +got error:\n%s", tc.reason, diff)
+ }
+ })
+ }
+}
+
+func TestWriteImage(t *testing.T) {
+ errBoom := errors.New("boom")
+
+ type args struct {
+ i ociv1.Image
+ }
+ type want struct {
+ err error
+ }
+
+ cases := map[string]struct {
+ reason string
+ files map[string][]byte
+ args args
+ want want
+ }{
+ "DigestError": {
+ reason: "We should return an error if we can't get the image's digest.",
+ args: args{
+ i: &MockImage{
+ MockDigest: func() (ociv1.Hash, error) { return ociv1.Hash{}, errBoom },
+ },
+ },
+ want: want{
+ err: errors.Wrap(errBoom, errGetDigest),
+ },
+ },
+ "RawConfigFileError": {
+ reason: "We should return an error if we can't access the image's raw config file.",
+ args: args{
+ i: &MockImage{
+ MockDigest: func() (ociv1.Hash, error) { return ociv1.Hash{Hex: "cool"}, nil },
+ MockRawConfigFile: func() ([]byte, error) { return nil, errBoom },
+ },
+ },
+ want: want{
+ err: errors.Wrap(errBoom, errGetRawConfigFile),
+ },
+ },
+ "WriteLayerError": {
+ reason: "We should return an error if we can't write a layer to the store.",
+ args: args{
+ i: &MockImage{
+ MockDigest: func() (ociv1.Hash, error) { return ociv1.Hash{Hex: "cool"}, nil },
+ MockRawConfigFile: func() ([]byte, error) { return nil, nil },
+ MockLayers: func() ([]ociv1.Layer, error) {
+ return []ociv1.Layer{
+ &MockLayer{
+ // To cause WriteLayer to fail.
+ MockDiffID: func() (ociv1.Hash, error) { return ociv1.Hash{}, errBoom },
+ },
+ }, nil
+ },
+ },
+ },
+ want: want{
+ err: errors.Wrap(errors.Wrap(errBoom, errGetDigest), errWriteLayers),
+ },
+ },
+ "SuccessfulWrite": {
+ reason: "We should not return an error if we successfully wrote an image to the store.",
+ args: args{
+ i: &MockImage{
+ MockDigest: func() (ociv1.Hash, error) { return ociv1.Hash{Hex: "cool"}, nil },
+ MockRawConfigFile: func() ([]byte, error) { return []byte(`{"variant":"cool"}`), nil },
+ MockLayers: func() ([]ociv1.Layer, error) { return nil, nil },
+ },
+ },
+ want: want{
+ err: nil,
+ },
+ },
+ "SuccessfulNoOp": {
+ reason: "We should return early if the supplied image is already stored.",
+ files: map[string][]byte{
+ // The minimum valid config file required by validate.Image.
+ "cool": []byte(`{"rootfs":{"type":"layers"}}`),
+ },
+ args: args{
+ i: &MockImage{
+ MockDigest: func() (ociv1.Hash, error) { return ociv1.Hash{Hex: "cool"}, nil },
+ },
+ },
+ want: want{
+ err: nil,
+ },
+ },
+ }
+
+ for name, tc := range cases {
+ t.Run(name, func(t *testing.T) {
+ tmp, err := os.MkdirTemp(os.TempDir(), strings.ReplaceAll(t.Name(), string(os.PathSeparator), "_"))
+ if err != nil {
+ t.Fatal(err.Error())
+ }
+ t.Cleanup(func() {
+ os.RemoveAll(tmp)
+ })
+
+ for name, data := range tc.files {
+ path := filepath.Join(tmp, DirImages, name)
+ _ = os.MkdirAll(filepath.Dir(path), 0700)
+ _ = os.WriteFile(path, data, 0600)
+ }
+
+ c := NewImage(tmp)
+ err = c.WriteImage(tc.args.i)
+ if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" {
+ t.Errorf("\n%s\nWriteImage(...): -want error, +got error:\n%s", tc.reason, diff)
+ }
+ })
+ }
+}
+
+func TestWriteLayer(t *testing.T) {
+ errBoom := errors.New("boom")
+
+ type args struct {
+ l ociv1.Layer
+ }
+ type want struct {
+ err error
+ }
+
+ cases := map[string]struct {
+ reason string
+ files map[string][]byte
+ args args
+ want want
+ }{
+ "DiffIDError": {
+ reason: "We should return an error if we can't get the layer's (diff) digest.",
+ args: args{
+ l: &MockLayer{
+ MockDiffID: func() (ociv1.Hash, error) { return ociv1.Hash{}, errBoom },
+ },
+ },
+ want: want{
+ err: errors.Wrap(errBoom, errGetDigest),
+ },
+ },
+ "Uncompressed": {
+ reason: "We should return an error if we can't get the layer's uncompressed tarball reader.",
+ args: args{
+ l: &MockLayer{
+ MockDiffID: func() (ociv1.Hash, error) { return ociv1.Hash{}, nil },
+ MockUncompressed: func() (io.ReadCloser, error) { return nil, errBoom },
+ },
+ },
+ want: want{
+ err: errors.Wrap(errBoom, errReadLayer),
+ },
+ },
+ "SuccessfulWrite": {
+ reason: "We should not return an error if we successfully wrote a layer to the store.",
+ args: args{
+ l: &MockLayer{
+ MockDiffID: func() (ociv1.Hash, error) { return ociv1.Hash{Hex: "cool"}, nil },
+ MockUncompressed: func() (io.ReadCloser, error) { return io.NopCloser(strings.NewReader("")), nil },
+ },
+ },
+ want: want{
+ err: nil,
+ },
+ },
+ "SuccessfulNoOp": {
+ reason: "We should return early if the supplied layer is already stored.",
+ files: map[string][]byte{
+ "cool": nil, // This file just has to exist.
+ },
+ args: args{
+ l: &MockLayer{
+ MockDiffID: func() (ociv1.Hash, error) { return ociv1.Hash{Hex: "cool"}, nil },
+ MockUncompressed: func() (io.ReadCloser, error) { return io.NopCloser(strings.NewReader("")), nil },
+ },
+ },
+ want: want{
+ err: nil,
+ },
+ },
+ }
+
+ for name, tc := range cases {
+ t.Run(name, func(t *testing.T) {
+ tmp, err := os.MkdirTemp(os.TempDir(), strings.ReplaceAll(t.Name(), string(os.PathSeparator), "_"))
+ if err != nil {
+ t.Fatal(err.Error())
+ }
+ t.Cleanup(func() {
+ os.RemoveAll(tmp)
+ })
+
+ for name, data := range tc.files {
+ path := filepath.Join(tmp, DirImages, name)
+ _ = os.MkdirAll(filepath.Dir(path), 0700)
+ _ = os.WriteFile(path, data, 0600)
+ }
+
+ c := NewImage(tmp)
+ err = c.WriteLayer(tc.args.l)
+ if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" {
+ t.Errorf("\n%s\nWriteLayer(...): -want error, +got error:\n%s", tc.reason, diff)
+ }
+ })
+ }
+}
diff --git a/internal/oci/store/uncompressed/store_uncompressed.go b/internal/oci/store/uncompressed/store_uncompressed.go
new file mode 100644
index 0000000..a3a6c9b
--- /dev/null
+++ b/internal/oci/store/uncompressed/store_uncompressed.go
@@ -0,0 +1,153 @@
+/*
+Copyright 2022 The Crossplane 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 uncompressed implemented an uncompressed layer based container store.
+package uncompressed
+
+import (
+ "context"
+ "io"
+ "os"
+ "path/filepath"
+
+ ociv1 "github.com/google/go-containerregistry/pkg/v1"
+
+ "github.com/crossplane/crossplane-runtime/pkg/errors"
+
+ "github.com/crossplane/function-runtime-oci/internal/oci/layer"
+ "github.com/crossplane/function-runtime-oci/internal/oci/spec"
+ "github.com/crossplane/function-runtime-oci/internal/oci/store"
+)
+
+// Error strings
+const (
+ errReadConfigFile = "cannot read image config file"
+ errGetLayers = "cannot get image layers"
+ errMkRootFS = "cannot make rootfs directory"
+ errOpenLayer = "cannot open layer tarball"
+ errApplyLayer = "cannot extract layer tarball"
+ errCloseLayer = "cannot close layer tarball"
+ errWriteRuntimeSpec = "cannot write OCI runtime spec"
+ errCleanupBundle = "cannot cleanup OCI runtime bundle"
+)
+
+// A TarballApplicator applies (i.e. extracts) an OCI layer tarball.
+// https://github.com/opencontainers/image-spec/blob/v1.0/layer.md
+type TarballApplicator interface {
+ // Apply the supplied tarball - an OCI filesystem layer - to the supplied
+ // root directory. Applying all of an image's layers, in the correct order,
+ // should produce the image's "flattened" filesystem.
+ Apply(ctx context.Context, tb io.Reader, root string) error
+}
+
+// A RuntimeSpecWriter writes an OCI runtime spec to the supplied path.
+type RuntimeSpecWriter interface {
+ // Write and write an OCI runtime spec to the supplied path.
+ Write(path string, o ...spec.Option) error
+}
+
+// A RuntimeSpecWriterFn allows a function to satisfy RuntimeSpecCreator.
+type RuntimeSpecWriterFn func(path string, o ...spec.Option) error
+
+// Write an OCI runtime spec to the supplied path.
+func (fn RuntimeSpecWriterFn) Write(path string, o ...spec.Option) error { return fn(path, o...) }
+
+// A Bundler prepares OCI runtime bundles for use by an OCI runtime. It creates
+// the bundle's rootfs by extracting the supplied image's uncompressed layer
+// tarballs.
+type Bundler struct {
+ root string
+ tarball TarballApplicator
+ spec RuntimeSpecWriter
+}
+
+// NewBundler returns a an OCI runtime bundler that creates a bundle's rootfs by
+// extracting uncompressed layer tarballs.
+func NewBundler(root string) *Bundler {
+ s := &Bundler{
+ root: filepath.Join(root, store.DirContainers),
+ tarball: layer.NewStackingExtractor(layer.NewWhiteoutHandler(layer.NewExtractHandler())),
+ spec: RuntimeSpecWriterFn(spec.Write),
+ }
+ return s
+}
+
+// Bundle returns an OCI bundle ready for use by an OCI runtime.
+func (c *Bundler) Bundle(ctx context.Context, i ociv1.Image, id string, o ...spec.Option) (store.Bundle, error) {
+ cfg, err := i.ConfigFile()
+ if err != nil {
+ return nil, errors.Wrap(err, errReadConfigFile)
+ }
+
+ layers, err := i.Layers()
+ if err != nil {
+ return nil, errors.Wrap(err, errGetLayers)
+ }
+
+ path := filepath.Join(c.root, id)
+ rootfs := filepath.Join(path, store.DirRootFS)
+ if err := os.MkdirAll(rootfs, 0700); err != nil {
+ return nil, errors.Wrap(err, errMkRootFS)
+ }
+ b := Bundle{path: path}
+
+ if err := store.Validate(i); err != nil {
+ return nil, err
+ }
+
+ for _, l := range layers {
+ tb, err := l.Uncompressed()
+ if err != nil {
+ _ = b.Cleanup()
+ return nil, errors.Wrap(err, errOpenLayer)
+ }
+ if err := c.tarball.Apply(ctx, tb, rootfs); err != nil {
+ _ = tb.Close()
+ _ = b.Cleanup()
+ return nil, errors.Wrap(err, errApplyLayer)
+ }
+ if err := tb.Close(); err != nil {
+ _ = b.Cleanup()
+ return nil, errors.Wrap(err, errCloseLayer)
+ }
+ }
+
+ // Inject config derived from the image first, so that any options passed in
+ // by the caller will override it.
+ p, g := filepath.Join(rootfs, "etc", "passwd"), filepath.Join(rootfs, "etc", "group")
+ opts := append([]spec.Option{spec.WithImageConfig(cfg, p, g), spec.WithRootFS(store.DirRootFS, true)}, o...)
+
+ if err = c.spec.Write(filepath.Join(path, store.FileSpec), opts...); err != nil {
+ _ = b.Cleanup()
+ return nil, errors.Wrap(err, errWriteRuntimeSpec)
+ }
+
+ return b, nil
+}
+
+// An Bundle is an OCI runtime bundle. Its root filesystem is a temporary
+// extraction of its image's cached layers.
+type Bundle struct {
+ path string
+}
+
+// Path to the OCI bundle.
+func (b Bundle) Path() string { return b.path }
+
+// Cleanup the OCI bundle.
+func (b Bundle) Cleanup() error {
+ return errors.Wrap(os.RemoveAll(b.path), errCleanupBundle)
+}
diff --git a/internal/oci/store/uncompressed/store_uncompressed_test.go b/internal/oci/store/uncompressed/store_uncompressed_test.go
new file mode 100644
index 0000000..676f0da
--- /dev/null
+++ b/internal/oci/store/uncompressed/store_uncompressed_test.go
@@ -0,0 +1,247 @@
+/*
+Copyright 2022 The Crossplane 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 uncompressed
+
+import (
+ "context"
+ "io"
+ "os"
+ "strings"
+ "testing"
+
+ "github.com/google/go-cmp/cmp"
+ "github.com/google/go-cmp/cmp/cmpopts"
+ ociv1 "github.com/google/go-containerregistry/pkg/v1"
+
+ "github.com/crossplane/crossplane-runtime/pkg/errors"
+ "github.com/crossplane/crossplane-runtime/pkg/test"
+
+ "github.com/crossplane/function-runtime-oci/internal/oci/spec"
+ "github.com/crossplane/function-runtime-oci/internal/oci/store"
+)
+
+type MockImage struct {
+ ociv1.Image
+
+ MockDigest func() (ociv1.Hash, error)
+ MockConfigFile func() (*ociv1.ConfigFile, error)
+ MockLayers func() ([]ociv1.Layer, error)
+}
+
+func (i *MockImage) Digest() (ociv1.Hash, error) { return i.MockDigest() }
+func (i *MockImage) ConfigFile() (*ociv1.ConfigFile, error) { return i.MockConfigFile() }
+func (i *MockImage) Layers() ([]ociv1.Layer, error) { return i.MockLayers() }
+
+type MockLayer struct {
+ ociv1.Layer
+
+ MockDigest func() (ociv1.Hash, error)
+ MockUncompressed func() (io.ReadCloser, error)
+}
+
+func (l *MockLayer) Digest() (ociv1.Hash, error) { return l.MockDigest() }
+func (l *MockLayer) Uncompressed() (io.ReadCloser, error) { return l.MockUncompressed() }
+
+type MockTarballApplicator struct{ err error }
+
+func (a *MockTarballApplicator) Apply(_ context.Context, _ io.Reader, _ string) error { return a.err }
+
+type MockRuntimeSpecWriter struct{ err error }
+
+func (c *MockRuntimeSpecWriter) Write(_ string, _ ...spec.Option) error { return c.err }
+
+type MockCloser struct {
+ io.Reader
+
+ err error
+}
+
+func (c *MockCloser) Close() error { return c.err }
+
+func TestBundle(t *testing.T) {
+ errBoom := errors.New("boom")
+
+ type params struct {
+ tarball TarballApplicator
+ spec RuntimeSpecWriter
+ }
+ type args struct {
+ ctx context.Context
+ i ociv1.Image
+ id string
+ o []spec.Option
+ }
+ type want struct {
+ b store.Bundle
+ err error
+ }
+
+ cases := map[string]struct {
+ reason string
+ params params
+ args args
+ want want
+ }{
+ "ReadConfigFileError": {
+ reason: "We should return any error encountered reading the image's config file.",
+ params: params{},
+ args: args{
+ i: &MockImage{
+ MockConfigFile: func() (*ociv1.ConfigFile, error) { return nil, errBoom },
+ },
+ },
+ want: want{
+ err: errors.Wrap(errBoom, errReadConfigFile),
+ },
+ },
+ "GetLayersError": {
+ reason: "We should return any error encountered reading the image's layers.",
+ params: params{},
+ args: args{
+ i: &MockImage{
+ MockConfigFile: func() (*ociv1.ConfigFile, error) { return &ociv1.ConfigFile{}, nil },
+ MockLayers: func() ([]ociv1.Layer, error) { return nil, errBoom },
+ },
+ },
+ want: want{
+ err: errors.Wrap(errBoom, errGetLayers),
+ },
+ },
+ "UncompressedLayerError": {
+ reason: "We should return any error encountered opening an image's uncompressed layers.",
+ params: params{},
+ args: args{
+ i: &MockImage{
+ MockConfigFile: func() (*ociv1.ConfigFile, error) { return &ociv1.ConfigFile{}, nil },
+ MockLayers: func() ([]ociv1.Layer, error) {
+ return []ociv1.Layer{&MockLayer{
+ MockUncompressed: func() (io.ReadCloser, error) { return nil, errBoom },
+ }}, nil
+ },
+ },
+ },
+ want: want{
+ err: errors.Wrap(errBoom, errOpenLayer),
+ },
+ },
+ "ApplyLayerTarballError": {
+ reason: "We should return any error encountered applying an image's layer tarball.",
+ params: params{
+ tarball: &MockTarballApplicator{err: errBoom},
+ },
+ args: args{
+ i: &MockImage{
+ MockConfigFile: func() (*ociv1.ConfigFile, error) { return &ociv1.ConfigFile{}, nil },
+ MockLayers: func() ([]ociv1.Layer, error) {
+ return []ociv1.Layer{&MockLayer{
+ MockUncompressed: func() (io.ReadCloser, error) { return io.NopCloser(strings.NewReader("")), nil },
+ }}, nil
+ },
+ },
+ },
+ want: want{
+ err: errors.Wrap(errBoom, errApplyLayer),
+ },
+ },
+ "CloseLayerError": {
+ reason: "We should return any error encountered closing an image's layer tarball.",
+ params: params{
+ tarball: &MockTarballApplicator{},
+ },
+ args: args{
+ i: &MockImage{
+ MockConfigFile: func() (*ociv1.ConfigFile, error) { return &ociv1.ConfigFile{}, nil },
+ MockLayers: func() ([]ociv1.Layer, error) {
+ return []ociv1.Layer{&MockLayer{
+ MockUncompressed: func() (io.ReadCloser, error) { return &MockCloser{err: errBoom}, nil },
+ }}, nil
+ },
+ },
+ },
+ want: want{
+ err: errors.Wrap(errBoom, errCloseLayer),
+ },
+ },
+ "WriteRuntimeSpecError": {
+ reason: "We should return any error encountered creating the bundle's OCI runtime spec.",
+ params: params{
+ tarball: &MockTarballApplicator{},
+ spec: &MockRuntimeSpecWriter{err: errBoom},
+ },
+ args: args{
+ i: &MockImage{
+ MockConfigFile: func() (*ociv1.ConfigFile, error) { return &ociv1.ConfigFile{}, nil },
+ MockLayers: func() ([]ociv1.Layer, error) {
+ return []ociv1.Layer{&MockLayer{
+ MockUncompressed: func() (io.ReadCloser, error) { return io.NopCloser(strings.NewReader("")), nil },
+ }}, nil
+ },
+ },
+ },
+ want: want{
+ err: errors.Wrap(errBoom, errWriteRuntimeSpec),
+ },
+ },
+ "SuccessfulBundle": {
+ reason: "We should create and return an OCI bundle.",
+ params: params{
+ tarball: &MockTarballApplicator{},
+ spec: &MockRuntimeSpecWriter{},
+ },
+ args: args{
+ i: &MockImage{
+ MockConfigFile: func() (*ociv1.ConfigFile, error) { return &ociv1.ConfigFile{}, nil },
+ MockLayers: func() ([]ociv1.Layer, error) {
+ return []ociv1.Layer{&MockLayer{
+ MockUncompressed: func() (io.ReadCloser, error) { return io.NopCloser(strings.NewReader("")), nil },
+ }}, nil
+ },
+ },
+ },
+ want: want{
+ // NOTE(negz): We cmpopts.IngoreUnexported this type below, so
+ // we're really only testing that a non-nil bundle was returned.
+ b: Bundle{},
+ },
+ },
+ }
+
+ for name, tc := range cases {
+ t.Run(name, func(t *testing.T) {
+ tmp, err := os.MkdirTemp(os.TempDir(), strings.ReplaceAll(t.Name(), string(os.PathSeparator), "_"))
+ if err != nil {
+ t.Fatal(err.Error())
+ }
+ defer os.RemoveAll(tmp)
+
+ c := &Bundler{
+ root: tmp,
+ tarball: tc.params.tarball,
+ spec: tc.params.spec,
+ }
+
+ got, err := c.Bundle(tc.args.ctx, tc.args.i, tc.args.id, tc.args.o...)
+
+ if diff := cmp.Diff(tc.want.b, got, cmpopts.IgnoreUnexported(Bundle{})); diff != "" {
+ t.Errorf("\n%s\nBundle(...): -want, +got:\n%s", tc.reason, diff)
+ }
+ if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" {
+ t.Errorf("\n%s\nBundle(...): -want error, +got error:\n%s", tc.reason, diff)
+ }
+ })
+ }
+}
diff --git a/internal/proto/buf.gen.yaml b/internal/proto/buf.gen.yaml
new file mode 100644
index 0000000..e5ba281
--- /dev/null
+++ b/internal/proto/buf.gen.yaml
@@ -0,0 +1,10 @@
+# This file contains configuration for the `buf generate` command.
+# See generate.go for more details.
+version: v1
+plugins:
+ - plugin: go
+ out: .
+ opt: paths=source_relative
+ - plugin: go-grpc
+ out: .
+ opt: paths=source_relative
\ No newline at end of file
diff --git a/internal/proto/generate.go b/internal/proto/generate.go
new file mode 100644
index 0000000..e9413cc
--- /dev/null
+++ b/internal/proto/generate.go
@@ -0,0 +1,31 @@
+//go:build generate
+// +build generate
+
+/*
+Copyright 2019 The Crossplane 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.
+*/
+
+//go:generate go install google.golang.org/protobuf/cmd/protoc-gen-go google.golang.org/grpc/cmd/protoc-gen-go-grpc
+//go:generate go run github.com/bufbuild/buf/cmd/buf generate
+
+// Package proto contains protocol buffer definitions.
+package proto
+
+import (
+ _ "github.com/bufbuild/buf/cmd/buf" //nolint:typecheck
+ _ "google.golang.org/grpc/cmd/protoc-gen-go-grpc" //nolint:typecheck
+ _ "google.golang.org/protobuf/cmd/protoc-gen-go" //nolint:typecheck
+ _ "k8s.io/code-generator" //nolint:typecheck
+)
diff --git a/internal/proto/v1alpha1/run_function.pb.go b/internal/proto/v1alpha1/run_function.pb.go
new file mode 100644
index 0000000..5428c9b
--- /dev/null
+++ b/internal/proto/v1alpha1/run_function.pb.go
@@ -0,0 +1,920 @@
+//
+//Copyright 2022 The Crossplane 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.
+
+// TODO(negz): Add a make target such that `make lint` runs `buf lint`.
+
+// Code generated by protoc-gen-go. DO NOT EDIT.
+// versions:
+// protoc-gen-go v1.31.0
+// protoc (unknown)
+// source: v1alpha1/run_function.proto
+
+package v1alpha1
+
+import (
+ protoreflect "google.golang.org/protobuf/reflect/protoreflect"
+ protoimpl "google.golang.org/protobuf/runtime/protoimpl"
+ durationpb "google.golang.org/protobuf/types/known/durationpb"
+ reflect "reflect"
+ sync "sync"
+)
+
+const (
+ // Verify that this generated code is sufficiently up-to-date.
+ _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
+ // Verify that runtime/protoimpl is sufficiently up-to-date.
+ _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
+)
+
+// ImagePullPolicy specifies when a Composition Function container should be
+// pulled from a remote OCI registry.
+type ImagePullPolicy int32
+
+const (
+ ImagePullPolicy_IMAGE_PULL_POLICY_UNSPECIFIED ImagePullPolicy = 0
+ ImagePullPolicy_IMAGE_PULL_POLICY_IF_NOT_PRESENT ImagePullPolicy = 1
+ ImagePullPolicy_IMAGE_PULL_POLICY_ALWAYS ImagePullPolicy = 2
+ ImagePullPolicy_IMAGE_PULL_POLICY_NEVER ImagePullPolicy = 3
+)
+
+// Enum value maps for ImagePullPolicy.
+var (
+ ImagePullPolicy_name = map[int32]string{
+ 0: "IMAGE_PULL_POLICY_UNSPECIFIED",
+ 1: "IMAGE_PULL_POLICY_IF_NOT_PRESENT",
+ 2: "IMAGE_PULL_POLICY_ALWAYS",
+ 3: "IMAGE_PULL_POLICY_NEVER",
+ }
+ ImagePullPolicy_value = map[string]int32{
+ "IMAGE_PULL_POLICY_UNSPECIFIED": 0,
+ "IMAGE_PULL_POLICY_IF_NOT_PRESENT": 1,
+ "IMAGE_PULL_POLICY_ALWAYS": 2,
+ "IMAGE_PULL_POLICY_NEVER": 3,
+ }
+)
+
+func (x ImagePullPolicy) Enum() *ImagePullPolicy {
+ p := new(ImagePullPolicy)
+ *p = x
+ return p
+}
+
+func (x ImagePullPolicy) String() string {
+ return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
+}
+
+func (ImagePullPolicy) Descriptor() protoreflect.EnumDescriptor {
+ return file_v1alpha1_run_function_proto_enumTypes[0].Descriptor()
+}
+
+func (ImagePullPolicy) Type() protoreflect.EnumType {
+ return &file_v1alpha1_run_function_proto_enumTypes[0]
+}
+
+func (x ImagePullPolicy) Number() protoreflect.EnumNumber {
+ return protoreflect.EnumNumber(x)
+}
+
+// Deprecated: Use ImagePullPolicy.Descriptor instead.
+func (ImagePullPolicy) EnumDescriptor() ([]byte, []int) {
+ return file_v1alpha1_run_function_proto_rawDescGZIP(), []int{0}
+}
+
+// NetworkPolicy configures whether a container is isolated from the network.
+type NetworkPolicy int32
+
+const (
+ NetworkPolicy_NETWORK_POLICY_UNSPECIFIED NetworkPolicy = 0
+ // Run the container without network access. The default.
+ NetworkPolicy_NETWORK_POLICY_ISOLATED NetworkPolicy = 1
+ // Allow the container to access the same network as the function runner.
+ NetworkPolicy_NETWORK_POLICY_RUNNER NetworkPolicy = 2
+)
+
+// Enum value maps for NetworkPolicy.
+var (
+ NetworkPolicy_name = map[int32]string{
+ 0: "NETWORK_POLICY_UNSPECIFIED",
+ 1: "NETWORK_POLICY_ISOLATED",
+ 2: "NETWORK_POLICY_RUNNER",
+ }
+ NetworkPolicy_value = map[string]int32{
+ "NETWORK_POLICY_UNSPECIFIED": 0,
+ "NETWORK_POLICY_ISOLATED": 1,
+ "NETWORK_POLICY_RUNNER": 2,
+ }
+)
+
+func (x NetworkPolicy) Enum() *NetworkPolicy {
+ p := new(NetworkPolicy)
+ *p = x
+ return p
+}
+
+func (x NetworkPolicy) String() string {
+ return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
+}
+
+func (NetworkPolicy) Descriptor() protoreflect.EnumDescriptor {
+ return file_v1alpha1_run_function_proto_enumTypes[1].Descriptor()
+}
+
+func (NetworkPolicy) Type() protoreflect.EnumType {
+ return &file_v1alpha1_run_function_proto_enumTypes[1]
+}
+
+func (x NetworkPolicy) Number() protoreflect.EnumNumber {
+ return protoreflect.EnumNumber(x)
+}
+
+// Deprecated: Use NetworkPolicy.Descriptor instead.
+func (NetworkPolicy) EnumDescriptor() ([]byte, []int) {
+ return file_v1alpha1_run_function_proto_rawDescGZIP(), []int{1}
+}
+
+// ImagePullAuth configures authentication to a remote OCI registry.
+// It corresponds to go-containerregistry's AuthConfig type.
+// https://pkg.go.dev/github.com/google/go-containerregistry@v0.11.0/pkg/authn#AuthConfig
+type ImagePullAuth struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ Username string `protobuf:"bytes,1,opt,name=username,proto3" json:"username,omitempty"`
+ Password string `protobuf:"bytes,2,opt,name=password,proto3" json:"password,omitempty"`
+ Auth string `protobuf:"bytes,3,opt,name=auth,proto3" json:"auth,omitempty"`
+ IdentityToken string `protobuf:"bytes,4,opt,name=identity_token,json=identityToken,proto3" json:"identity_token,omitempty"`
+ RegistryToken string `protobuf:"bytes,5,opt,name=registry_token,json=registryToken,proto3" json:"registry_token,omitempty"`
+}
+
+func (x *ImagePullAuth) Reset() {
+ *x = ImagePullAuth{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_v1alpha1_run_function_proto_msgTypes[0]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *ImagePullAuth) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*ImagePullAuth) ProtoMessage() {}
+
+func (x *ImagePullAuth) ProtoReflect() protoreflect.Message {
+ mi := &file_v1alpha1_run_function_proto_msgTypes[0]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use ImagePullAuth.ProtoReflect.Descriptor instead.
+func (*ImagePullAuth) Descriptor() ([]byte, []int) {
+ return file_v1alpha1_run_function_proto_rawDescGZIP(), []int{0}
+}
+
+func (x *ImagePullAuth) GetUsername() string {
+ if x != nil {
+ return x.Username
+ }
+ return ""
+}
+
+func (x *ImagePullAuth) GetPassword() string {
+ if x != nil {
+ return x.Password
+ }
+ return ""
+}
+
+func (x *ImagePullAuth) GetAuth() string {
+ if x != nil {
+ return x.Auth
+ }
+ return ""
+}
+
+func (x *ImagePullAuth) GetIdentityToken() string {
+ if x != nil {
+ return x.IdentityToken
+ }
+ return ""
+}
+
+func (x *ImagePullAuth) GetRegistryToken() string {
+ if x != nil {
+ return x.RegistryToken
+ }
+ return ""
+}
+
+// ImagePullConfig configures how a Composition Function container should be
+// pulled from a remote OCI registry.
+type ImagePullConfig struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ PullPolicy ImagePullPolicy `protobuf:"varint,1,opt,name=pull_policy,json=pullPolicy,proto3,enum=apiextensions.fn.proto.v1alpha1.ImagePullPolicy" json:"pull_policy,omitempty"`
+ Auth *ImagePullAuth `protobuf:"bytes,2,opt,name=auth,proto3" json:"auth,omitempty"`
+}
+
+func (x *ImagePullConfig) Reset() {
+ *x = ImagePullConfig{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_v1alpha1_run_function_proto_msgTypes[1]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *ImagePullConfig) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*ImagePullConfig) ProtoMessage() {}
+
+func (x *ImagePullConfig) ProtoReflect() protoreflect.Message {
+ mi := &file_v1alpha1_run_function_proto_msgTypes[1]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use ImagePullConfig.ProtoReflect.Descriptor instead.
+func (*ImagePullConfig) Descriptor() ([]byte, []int) {
+ return file_v1alpha1_run_function_proto_rawDescGZIP(), []int{1}
+}
+
+func (x *ImagePullConfig) GetPullPolicy() ImagePullPolicy {
+ if x != nil {
+ return x.PullPolicy
+ }
+ return ImagePullPolicy_IMAGE_PULL_POLICY_UNSPECIFIED
+}
+
+func (x *ImagePullConfig) GetAuth() *ImagePullAuth {
+ if x != nil {
+ return x.Auth
+ }
+ return nil
+}
+
+// NetworkConfig configures whether and how a Composition Function container may
+// access the network.
+type NetworkConfig struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ // Whether or not the container can access the network.
+ Policy NetworkPolicy `protobuf:"varint,1,opt,name=policy,proto3,enum=apiextensions.fn.proto.v1alpha1.NetworkPolicy" json:"policy,omitempty"`
+}
+
+func (x *NetworkConfig) Reset() {
+ *x = NetworkConfig{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_v1alpha1_run_function_proto_msgTypes[2]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *NetworkConfig) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*NetworkConfig) ProtoMessage() {}
+
+func (x *NetworkConfig) ProtoReflect() protoreflect.Message {
+ mi := &file_v1alpha1_run_function_proto_msgTypes[2]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use NetworkConfig.ProtoReflect.Descriptor instead.
+func (*NetworkConfig) Descriptor() ([]byte, []int) {
+ return file_v1alpha1_run_function_proto_rawDescGZIP(), []int{2}
+}
+
+func (x *NetworkConfig) GetPolicy() NetworkPolicy {
+ if x != nil {
+ return x.Policy
+ }
+ return NetworkPolicy_NETWORK_POLICY_UNSPECIFIED
+}
+
+// Resources configures what compute resources should be available to a
+// Composition Function container.
+type ResourceConfig struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ Limits *ResourceLimits `protobuf:"bytes,1,opt,name=limits,proto3" json:"limits,omitempty"`
+}
+
+func (x *ResourceConfig) Reset() {
+ *x = ResourceConfig{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_v1alpha1_run_function_proto_msgTypes[3]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *ResourceConfig) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*ResourceConfig) ProtoMessage() {}
+
+func (x *ResourceConfig) ProtoReflect() protoreflect.Message {
+ mi := &file_v1alpha1_run_function_proto_msgTypes[3]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use ResourceConfig.ProtoReflect.Descriptor instead.
+func (*ResourceConfig) Descriptor() ([]byte, []int) {
+ return file_v1alpha1_run_function_proto_rawDescGZIP(), []int{3}
+}
+
+func (x *ResourceConfig) GetLimits() *ResourceLimits {
+ if x != nil {
+ return x.Limits
+ }
+ return nil
+}
+
+// ResourceLimits configures the maximum compute resources that will be
+// available to a Composition Function container.
+type ResourceLimits struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ // CPU, in cores. (500m = .5 cores)
+ // Specified in Kubernetes-style resource.Quantity form.
+ Memory string `protobuf:"bytes,1,opt,name=memory,proto3" json:"memory,omitempty"`
+ // Memory, in bytes. (500Gi = 500GiB = 500 * 1024 * 1024 * 1024)
+ // Specified in Kubernetes-style resource.Quantity form.
+ Cpu string `protobuf:"bytes,2,opt,name=cpu,proto3" json:"cpu,omitempty"`
+}
+
+func (x *ResourceLimits) Reset() {
+ *x = ResourceLimits{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_v1alpha1_run_function_proto_msgTypes[4]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *ResourceLimits) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*ResourceLimits) ProtoMessage() {}
+
+func (x *ResourceLimits) ProtoReflect() protoreflect.Message {
+ mi := &file_v1alpha1_run_function_proto_msgTypes[4]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use ResourceLimits.ProtoReflect.Descriptor instead.
+func (*ResourceLimits) Descriptor() ([]byte, []int) {
+ return file_v1alpha1_run_function_proto_rawDescGZIP(), []int{4}
+}
+
+func (x *ResourceLimits) GetMemory() string {
+ if x != nil {
+ return x.Memory
+ }
+ return ""
+}
+
+func (x *ResourceLimits) GetCpu() string {
+ if x != nil {
+ return x.Cpu
+ }
+ return ""
+}
+
+// RunFunctionConfig configures how a Composition Function container is run.
+type RunFunctionConfig struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ // Resources available to the container.
+ Resources *ResourceConfig `protobuf:"bytes,1,opt,name=resources,proto3" json:"resources,omitempty"`
+ // Network configuration for the container.
+ Network *NetworkConfig `protobuf:"bytes,2,opt,name=network,proto3" json:"network,omitempty"`
+ // Timeout after which the container will be killed.
+ Timeout *durationpb.Duration `protobuf:"bytes,3,opt,name=timeout,proto3" json:"timeout,omitempty"`
+}
+
+func (x *RunFunctionConfig) Reset() {
+ *x = RunFunctionConfig{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_v1alpha1_run_function_proto_msgTypes[5]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *RunFunctionConfig) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*RunFunctionConfig) ProtoMessage() {}
+
+func (x *RunFunctionConfig) ProtoReflect() protoreflect.Message {
+ mi := &file_v1alpha1_run_function_proto_msgTypes[5]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use RunFunctionConfig.ProtoReflect.Descriptor instead.
+func (*RunFunctionConfig) Descriptor() ([]byte, []int) {
+ return file_v1alpha1_run_function_proto_rawDescGZIP(), []int{5}
+}
+
+func (x *RunFunctionConfig) GetResources() *ResourceConfig {
+ if x != nil {
+ return x.Resources
+ }
+ return nil
+}
+
+func (x *RunFunctionConfig) GetNetwork() *NetworkConfig {
+ if x != nil {
+ return x.Network
+ }
+ return nil
+}
+
+func (x *RunFunctionConfig) GetTimeout() *durationpb.Duration {
+ if x != nil {
+ return x.Timeout
+ }
+ return nil
+}
+
+// A RunFunctionRequest requests that a Composition Function be run.
+type RunFunctionRequest struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ // OCI image of the Composition Function.
+ Image string `protobuf:"bytes,1,opt,name=image,proto3" json:"image,omitempty"`
+ // A FunctionIO serialized as YAML.
+ Input []byte `protobuf:"bytes,2,opt,name=input,proto3" json:"input,omitempty"`
+ // Configures how the function image is pulled.
+ ImagePullConfig *ImagePullConfig `protobuf:"bytes,3,opt,name=image_pull_config,json=imagePullConfig,proto3" json:"image_pull_config,omitempty"`
+ // Configures how the function container is run.
+ RunFunctionConfig *RunFunctionConfig `protobuf:"bytes,4,opt,name=run_function_config,json=runFunctionConfig,proto3" json:"run_function_config,omitempty"`
+}
+
+func (x *RunFunctionRequest) Reset() {
+ *x = RunFunctionRequest{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_v1alpha1_run_function_proto_msgTypes[6]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *RunFunctionRequest) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*RunFunctionRequest) ProtoMessage() {}
+
+func (x *RunFunctionRequest) ProtoReflect() protoreflect.Message {
+ mi := &file_v1alpha1_run_function_proto_msgTypes[6]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use RunFunctionRequest.ProtoReflect.Descriptor instead.
+func (*RunFunctionRequest) Descriptor() ([]byte, []int) {
+ return file_v1alpha1_run_function_proto_rawDescGZIP(), []int{6}
+}
+
+func (x *RunFunctionRequest) GetImage() string {
+ if x != nil {
+ return x.Image
+ }
+ return ""
+}
+
+func (x *RunFunctionRequest) GetInput() []byte {
+ if x != nil {
+ return x.Input
+ }
+ return nil
+}
+
+func (x *RunFunctionRequest) GetImagePullConfig() *ImagePullConfig {
+ if x != nil {
+ return x.ImagePullConfig
+ }
+ return nil
+}
+
+func (x *RunFunctionRequest) GetRunFunctionConfig() *RunFunctionConfig {
+ if x != nil {
+ return x.RunFunctionConfig
+ }
+ return nil
+}
+
+// A RunFunctionResponse contains the response from a Composition Function run.
+// The output FunctionIO is returned as opaque bytes. Errors encountered while
+// running a function (as opposed to errors returned _by_ a function) will be
+// encapsulated as gRPC errors.
+type RunFunctionResponse struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ Output []byte `protobuf:"bytes,1,opt,name=output,proto3" json:"output,omitempty"`
+}
+
+func (x *RunFunctionResponse) Reset() {
+ *x = RunFunctionResponse{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_v1alpha1_run_function_proto_msgTypes[7]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *RunFunctionResponse) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*RunFunctionResponse) ProtoMessage() {}
+
+func (x *RunFunctionResponse) ProtoReflect() protoreflect.Message {
+ mi := &file_v1alpha1_run_function_proto_msgTypes[7]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use RunFunctionResponse.ProtoReflect.Descriptor instead.
+func (*RunFunctionResponse) Descriptor() ([]byte, []int) {
+ return file_v1alpha1_run_function_proto_rawDescGZIP(), []int{7}
+}
+
+func (x *RunFunctionResponse) GetOutput() []byte {
+ if x != nil {
+ return x.Output
+ }
+ return nil
+}
+
+var File_v1alpha1_run_function_proto protoreflect.FileDescriptor
+
+var file_v1alpha1_run_function_proto_rawDesc = []byte{
+ 0x0a, 0x1b, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x2f, 0x72, 0x75, 0x6e, 0x5f, 0x66,
+ 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x1f, 0x61,
+ 0x70, 0x69, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x66, 0x6e, 0x2e,
+ 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x1a, 0x1e,
+ 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f,
+ 0x64, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xa9,
+ 0x01, 0x0a, 0x0d, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x50, 0x75, 0x6c, 0x6c, 0x41, 0x75, 0x74, 0x68,
+ 0x12, 0x1a, 0x0a, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01,
+ 0x28, 0x09, 0x52, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x1a, 0x0a, 0x08,
+ 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08,
+ 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x61, 0x75, 0x74, 0x68,
+ 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x61, 0x75, 0x74, 0x68, 0x12, 0x25, 0x0a, 0x0e,
+ 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x04,
+ 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x54, 0x6f,
+ 0x6b, 0x65, 0x6e, 0x12, 0x25, 0x0a, 0x0e, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x5f,
+ 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x72, 0x65, 0x67,
+ 0x69, 0x73, 0x74, 0x72, 0x79, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x22, 0xa8, 0x01, 0x0a, 0x0f, 0x49,
+ 0x6d, 0x61, 0x67, 0x65, 0x50, 0x75, 0x6c, 0x6c, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x51,
+ 0x0a, 0x0b, 0x70, 0x75, 0x6c, 0x6c, 0x5f, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x18, 0x01, 0x20,
+ 0x01, 0x28, 0x0e, 0x32, 0x30, 0x2e, 0x61, 0x70, 0x69, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x73, 0x69,
+ 0x6f, 0x6e, 0x73, 0x2e, 0x66, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x76, 0x31, 0x61,
+ 0x6c, 0x70, 0x68, 0x61, 0x31, 0x2e, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x50, 0x75, 0x6c, 0x6c, 0x50,
+ 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x52, 0x0a, 0x70, 0x75, 0x6c, 0x6c, 0x50, 0x6f, 0x6c, 0x69, 0x63,
+ 0x79, 0x12, 0x42, 0x0a, 0x04, 0x61, 0x75, 0x74, 0x68, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32,
+ 0x2e, 0x2e, 0x61, 0x70, 0x69, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x2e,
+ 0x66, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61,
+ 0x31, 0x2e, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x50, 0x75, 0x6c, 0x6c, 0x41, 0x75, 0x74, 0x68, 0x52,
+ 0x04, 0x61, 0x75, 0x74, 0x68, 0x22, 0x57, 0x0a, 0x0d, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b,
+ 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x46, 0x0a, 0x06, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79,
+ 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x2e, 0x2e, 0x61, 0x70, 0x69, 0x65, 0x78, 0x74, 0x65,
+ 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x66, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e,
+ 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x2e, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b,
+ 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x52, 0x06, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x22, 0x59,
+ 0x0a, 0x0e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67,
+ 0x12, 0x47, 0x0a, 0x06, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b,
+ 0x32, 0x2f, 0x2e, 0x61, 0x70, 0x69, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x73,
+ 0x2e, 0x66, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68,
+ 0x61, 0x31, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x4c, 0x69, 0x6d, 0x69, 0x74,
+ 0x73, 0x52, 0x06, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x73, 0x22, 0x3a, 0x0a, 0x0e, 0x52, 0x65, 0x73,
+ 0x6f, 0x75, 0x72, 0x63, 0x65, 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x6d,
+ 0x65, 0x6d, 0x6f, 0x72, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x6d, 0x65, 0x6d,
+ 0x6f, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x63, 0x70, 0x75, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09,
+ 0x52, 0x03, 0x63, 0x70, 0x75, 0x22, 0xe1, 0x01, 0x0a, 0x11, 0x52, 0x75, 0x6e, 0x46, 0x75, 0x6e,
+ 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x4d, 0x0a, 0x09, 0x72,
+ 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2f,
+ 0x2e, 0x61, 0x70, 0x69, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x66,
+ 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31,
+ 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52,
+ 0x09, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x12, 0x48, 0x0a, 0x07, 0x6e, 0x65,
+ 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2e, 0x2e, 0x61, 0x70,
+ 0x69, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x66, 0x6e, 0x2e, 0x70,
+ 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x2e, 0x4e, 0x65,
+ 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x07, 0x6e, 0x65, 0x74,
+ 0x77, 0x6f, 0x72, 0x6b, 0x12, 0x33, 0x0a, 0x07, 0x74, 0x69, 0x6d, 0x65, 0x6f, 0x75, 0x74, 0x18,
+ 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70,
+ 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x44, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e,
+ 0x52, 0x07, 0x74, 0x69, 0x6d, 0x65, 0x6f, 0x75, 0x74, 0x22, 0x82, 0x02, 0x0a, 0x12, 0x52, 0x75,
+ 0x6e, 0x46, 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74,
+ 0x12, 0x14, 0x0a, 0x05, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52,
+ 0x05, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x18,
+ 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x12, 0x5c, 0x0a, 0x11,
+ 0x69, 0x6d, 0x61, 0x67, 0x65, 0x5f, 0x70, 0x75, 0x6c, 0x6c, 0x5f, 0x63, 0x6f, 0x6e, 0x66, 0x69,
+ 0x67, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x30, 0x2e, 0x61, 0x70, 0x69, 0x65, 0x78, 0x74,
+ 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x66, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f,
+ 0x2e, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x2e, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x50,
+ 0x75, 0x6c, 0x6c, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0f, 0x69, 0x6d, 0x61, 0x67, 0x65,
+ 0x50, 0x75, 0x6c, 0x6c, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x62, 0x0a, 0x13, 0x72, 0x75,
+ 0x6e, 0x5f, 0x66, 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x63, 0x6f, 0x6e, 0x66, 0x69,
+ 0x67, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x32, 0x2e, 0x61, 0x70, 0x69, 0x65, 0x78, 0x74,
+ 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x66, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f,
+ 0x2e, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x2e, 0x52, 0x75, 0x6e, 0x46, 0x75, 0x6e,
+ 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x11, 0x72, 0x75, 0x6e,
+ 0x46, 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x22, 0x2d,
+ 0x0a, 0x13, 0x52, 0x75, 0x6e, 0x46, 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73,
+ 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x18,
+ 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x06, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x2a, 0x95, 0x01,
+ 0x0a, 0x0f, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x50, 0x75, 0x6c, 0x6c, 0x50, 0x6f, 0x6c, 0x69, 0x63,
+ 0x79, 0x12, 0x21, 0x0a, 0x1d, 0x49, 0x4d, 0x41, 0x47, 0x45, 0x5f, 0x50, 0x55, 0x4c, 0x4c, 0x5f,
+ 0x50, 0x4f, 0x4c, 0x49, 0x43, 0x59, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49,
+ 0x45, 0x44, 0x10, 0x00, 0x12, 0x24, 0x0a, 0x20, 0x49, 0x4d, 0x41, 0x47, 0x45, 0x5f, 0x50, 0x55,
+ 0x4c, 0x4c, 0x5f, 0x50, 0x4f, 0x4c, 0x49, 0x43, 0x59, 0x5f, 0x49, 0x46, 0x5f, 0x4e, 0x4f, 0x54,
+ 0x5f, 0x50, 0x52, 0x45, 0x53, 0x45, 0x4e, 0x54, 0x10, 0x01, 0x12, 0x1c, 0x0a, 0x18, 0x49, 0x4d,
+ 0x41, 0x47, 0x45, 0x5f, 0x50, 0x55, 0x4c, 0x4c, 0x5f, 0x50, 0x4f, 0x4c, 0x49, 0x43, 0x59, 0x5f,
+ 0x41, 0x4c, 0x57, 0x41, 0x59, 0x53, 0x10, 0x02, 0x12, 0x1b, 0x0a, 0x17, 0x49, 0x4d, 0x41, 0x47,
+ 0x45, 0x5f, 0x50, 0x55, 0x4c, 0x4c, 0x5f, 0x50, 0x4f, 0x4c, 0x49, 0x43, 0x59, 0x5f, 0x4e, 0x45,
+ 0x56, 0x45, 0x52, 0x10, 0x03, 0x2a, 0x67, 0x0a, 0x0d, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b,
+ 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x12, 0x1e, 0x0a, 0x1a, 0x4e, 0x45, 0x54, 0x57, 0x4f, 0x52,
+ 0x4b, 0x5f, 0x50, 0x4f, 0x4c, 0x49, 0x43, 0x59, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49,
+ 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x1b, 0x0a, 0x17, 0x4e, 0x45, 0x54, 0x57, 0x4f, 0x52,
+ 0x4b, 0x5f, 0x50, 0x4f, 0x4c, 0x49, 0x43, 0x59, 0x5f, 0x49, 0x53, 0x4f, 0x4c, 0x41, 0x54, 0x45,
+ 0x44, 0x10, 0x01, 0x12, 0x19, 0x0a, 0x15, 0x4e, 0x45, 0x54, 0x57, 0x4f, 0x52, 0x4b, 0x5f, 0x50,
+ 0x4f, 0x4c, 0x49, 0x43, 0x59, 0x5f, 0x52, 0x55, 0x4e, 0x4e, 0x45, 0x52, 0x10, 0x02, 0x32, 0xa0,
+ 0x01, 0x0a, 0x22, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x69, 0x7a, 0x65, 0x64,
+ 0x46, 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x75, 0x6e, 0x6e, 0x65, 0x72, 0x53, 0x65,
+ 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x7a, 0x0a, 0x0b, 0x52, 0x75, 0x6e, 0x46, 0x75, 0x6e, 0x63,
+ 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x33, 0x2e, 0x61, 0x70, 0x69, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x73,
+ 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x66, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x76, 0x31,
+ 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x2e, 0x52, 0x75, 0x6e, 0x46, 0x75, 0x6e, 0x63, 0x74, 0x69,
+ 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x34, 0x2e, 0x61, 0x70, 0x69, 0x65,
+ 0x78, 0x74, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x66, 0x6e, 0x2e, 0x70, 0x72, 0x6f,
+ 0x74, 0x6f, 0x2e, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x2e, 0x52, 0x75, 0x6e, 0x46,
+ 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22,
+ 0x00, 0x42, 0x44, 0x5a, 0x42, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f,
+ 0x63, 0x72, 0x6f, 0x73, 0x73, 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x2f, 0x66, 0x75, 0x6e, 0x63, 0x74,
+ 0x69, 0x6f, 0x6e, 0x2d, 0x72, 0x75, 0x6e, 0x74, 0x69, 0x6d, 0x65, 0x2d, 0x6f, 0x63, 0x69, 0x2f,
+ 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x76,
+ 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
+}
+
+var (
+ file_v1alpha1_run_function_proto_rawDescOnce sync.Once
+ file_v1alpha1_run_function_proto_rawDescData = file_v1alpha1_run_function_proto_rawDesc
+)
+
+func file_v1alpha1_run_function_proto_rawDescGZIP() []byte {
+ file_v1alpha1_run_function_proto_rawDescOnce.Do(func() {
+ file_v1alpha1_run_function_proto_rawDescData = protoimpl.X.CompressGZIP(file_v1alpha1_run_function_proto_rawDescData)
+ })
+ return file_v1alpha1_run_function_proto_rawDescData
+}
+
+var file_v1alpha1_run_function_proto_enumTypes = make([]protoimpl.EnumInfo, 2)
+var file_v1alpha1_run_function_proto_msgTypes = make([]protoimpl.MessageInfo, 8)
+var file_v1alpha1_run_function_proto_goTypes = []interface{}{
+ (ImagePullPolicy)(0), // 0: apiextensions.fn.proto.v1alpha1.ImagePullPolicy
+ (NetworkPolicy)(0), // 1: apiextensions.fn.proto.v1alpha1.NetworkPolicy
+ (*ImagePullAuth)(nil), // 2: apiextensions.fn.proto.v1alpha1.ImagePullAuth
+ (*ImagePullConfig)(nil), // 3: apiextensions.fn.proto.v1alpha1.ImagePullConfig
+ (*NetworkConfig)(nil), // 4: apiextensions.fn.proto.v1alpha1.NetworkConfig
+ (*ResourceConfig)(nil), // 5: apiextensions.fn.proto.v1alpha1.ResourceConfig
+ (*ResourceLimits)(nil), // 6: apiextensions.fn.proto.v1alpha1.ResourceLimits
+ (*RunFunctionConfig)(nil), // 7: apiextensions.fn.proto.v1alpha1.RunFunctionConfig
+ (*RunFunctionRequest)(nil), // 8: apiextensions.fn.proto.v1alpha1.RunFunctionRequest
+ (*RunFunctionResponse)(nil), // 9: apiextensions.fn.proto.v1alpha1.RunFunctionResponse
+ (*durationpb.Duration)(nil), // 10: google.protobuf.Duration
+}
+var file_v1alpha1_run_function_proto_depIdxs = []int32{
+ 0, // 0: apiextensions.fn.proto.v1alpha1.ImagePullConfig.pull_policy:type_name -> apiextensions.fn.proto.v1alpha1.ImagePullPolicy
+ 2, // 1: apiextensions.fn.proto.v1alpha1.ImagePullConfig.auth:type_name -> apiextensions.fn.proto.v1alpha1.ImagePullAuth
+ 1, // 2: apiextensions.fn.proto.v1alpha1.NetworkConfig.policy:type_name -> apiextensions.fn.proto.v1alpha1.NetworkPolicy
+ 6, // 3: apiextensions.fn.proto.v1alpha1.ResourceConfig.limits:type_name -> apiextensions.fn.proto.v1alpha1.ResourceLimits
+ 5, // 4: apiextensions.fn.proto.v1alpha1.RunFunctionConfig.resources:type_name -> apiextensions.fn.proto.v1alpha1.ResourceConfig
+ 4, // 5: apiextensions.fn.proto.v1alpha1.RunFunctionConfig.network:type_name -> apiextensions.fn.proto.v1alpha1.NetworkConfig
+ 10, // 6: apiextensions.fn.proto.v1alpha1.RunFunctionConfig.timeout:type_name -> google.protobuf.Duration
+ 3, // 7: apiextensions.fn.proto.v1alpha1.RunFunctionRequest.image_pull_config:type_name -> apiextensions.fn.proto.v1alpha1.ImagePullConfig
+ 7, // 8: apiextensions.fn.proto.v1alpha1.RunFunctionRequest.run_function_config:type_name -> apiextensions.fn.proto.v1alpha1.RunFunctionConfig
+ 8, // 9: apiextensions.fn.proto.v1alpha1.ContainerizedFunctionRunnerService.RunFunction:input_type -> apiextensions.fn.proto.v1alpha1.RunFunctionRequest
+ 9, // 10: apiextensions.fn.proto.v1alpha1.ContainerizedFunctionRunnerService.RunFunction:output_type -> apiextensions.fn.proto.v1alpha1.RunFunctionResponse
+ 10, // [10:11] is the sub-list for method output_type
+ 9, // [9:10] is the sub-list for method input_type
+ 9, // [9:9] is the sub-list for extension type_name
+ 9, // [9:9] is the sub-list for extension extendee
+ 0, // [0:9] is the sub-list for field type_name
+}
+
+func init() { file_v1alpha1_run_function_proto_init() }
+func file_v1alpha1_run_function_proto_init() {
+ if File_v1alpha1_run_function_proto != nil {
+ return
+ }
+ if !protoimpl.UnsafeEnabled {
+ file_v1alpha1_run_function_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*ImagePullAuth); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_v1alpha1_run_function_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*ImagePullConfig); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_v1alpha1_run_function_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*NetworkConfig); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_v1alpha1_run_function_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*ResourceConfig); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_v1alpha1_run_function_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*ResourceLimits); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_v1alpha1_run_function_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*RunFunctionConfig); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_v1alpha1_run_function_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*RunFunctionRequest); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_v1alpha1_run_function_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*RunFunctionResponse); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ }
+ type x struct{}
+ out := protoimpl.TypeBuilder{
+ File: protoimpl.DescBuilder{
+ GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
+ RawDescriptor: file_v1alpha1_run_function_proto_rawDesc,
+ NumEnums: 2,
+ NumMessages: 8,
+ NumExtensions: 0,
+ NumServices: 1,
+ },
+ GoTypes: file_v1alpha1_run_function_proto_goTypes,
+ DependencyIndexes: file_v1alpha1_run_function_proto_depIdxs,
+ EnumInfos: file_v1alpha1_run_function_proto_enumTypes,
+ MessageInfos: file_v1alpha1_run_function_proto_msgTypes,
+ }.Build()
+ File_v1alpha1_run_function_proto = out.File
+ file_v1alpha1_run_function_proto_rawDesc = nil
+ file_v1alpha1_run_function_proto_goTypes = nil
+ file_v1alpha1_run_function_proto_depIdxs = nil
+}
diff --git a/internal/proto/v1alpha1/run_function.proto b/internal/proto/v1alpha1/run_function.proto
new file mode 100644
index 0000000..bda66cb
--- /dev/null
+++ b/internal/proto/v1alpha1/run_function.proto
@@ -0,0 +1,129 @@
+/*
+Copyright 2022 The Crossplane 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.
+*/
+
+// TODO(negz): Add a make target such that `make lint` runs `buf lint`.
+
+syntax = "proto3";
+
+import "google/protobuf/duration.proto";
+
+package apiextensions.fn.proto.v1alpha1;
+
+option go_package = "github.com/crossplane/function-runtime-oci/internal/proto/v1alpha1";
+
+// A ContainerizedFunctionRunnerService runs containerized Composition Functions.
+service ContainerizedFunctionRunnerService {
+ // RunFunction runs a containerized function.
+ rpc RunFunction(RunFunctionRequest) returns (RunFunctionResponse) {}
+}
+
+// ImagePullPolicy specifies when a Composition Function container should be
+// pulled from a remote OCI registry.
+enum ImagePullPolicy {
+ IMAGE_PULL_POLICY_UNSPECIFIED = 0;
+ IMAGE_PULL_POLICY_IF_NOT_PRESENT = 1;
+ IMAGE_PULL_POLICY_ALWAYS = 2;
+ IMAGE_PULL_POLICY_NEVER = 3;
+}
+
+// ImagePullAuth configures authentication to a remote OCI registry.
+// It corresponds to go-containerregistry's AuthConfig type.
+// https://pkg.go.dev/github.com/google/go-containerregistry@v0.11.0/pkg/authn#AuthConfig
+message ImagePullAuth {
+ string username = 1;
+ string password = 2;
+ string auth = 3;
+ string identity_token = 4;
+ string registry_token = 5;
+}
+
+// ImagePullConfig configures how a Composition Function container should be
+// pulled from a remote OCI registry.
+message ImagePullConfig {
+ ImagePullPolicy pull_policy = 1;
+ ImagePullAuth auth = 2;
+}
+
+// NetworkPolicy configures whether a container is isolated from the network.
+enum NetworkPolicy {
+ NETWORK_POLICY_UNSPECIFIED = 0;
+
+ // Run the container without network access. The default.
+ NETWORK_POLICY_ISOLATED = 1;
+
+ // Allow the container to access the same network as the function runner.
+ NETWORK_POLICY_RUNNER = 2;
+}
+
+// NetworkConfig configures whether and how a Composition Function container may
+// access the network.
+message NetworkConfig {
+ // Whether or not the container can access the network.
+ NetworkPolicy policy = 1;
+}
+
+// Resources configures what compute resources should be available to a
+// Composition Function container.
+message ResourceConfig {
+ ResourceLimits limits = 1;
+}
+
+// ResourceLimits configures the maximum compute resources that will be
+// available to a Composition Function container.
+message ResourceLimits {
+ // CPU, in cores. (500m = .5 cores)
+ // Specified in Kubernetes-style resource.Quantity form.
+ string memory = 1;
+
+ // Memory, in bytes. (500Gi = 500GiB = 500 * 1024 * 1024 * 1024)
+ // Specified in Kubernetes-style resource.Quantity form.
+ string cpu = 2;
+}
+
+// RunFunctionConfig configures how a Composition Function container is run.
+message RunFunctionConfig {
+ // Resources available to the container.
+ ResourceConfig resources = 1;
+
+ // Network configuration for the container.
+ NetworkConfig network = 2;
+
+ // Timeout after which the container will be killed.
+ google.protobuf.Duration timeout = 3;
+}
+
+// A RunFunctionRequest requests that a Composition Function be run.
+message RunFunctionRequest {
+ // OCI image of the Composition Function.
+ string image = 1;
+
+ // A FunctionIO serialized as YAML.
+ bytes input = 2;
+
+ // Configures how the function image is pulled.
+ ImagePullConfig image_pull_config = 3;
+
+ // Configures how the function container is run.
+ RunFunctionConfig run_function_config = 4;
+}
+
+// A RunFunctionResponse contains the response from a Composition Function run.
+// The output FunctionIO is returned as opaque bytes. Errors encountered while
+// running a function (as opposed to errors returned _by_ a function) will be
+// encapsulated as gRPC errors.
+message RunFunctionResponse {
+ bytes output = 1;
+}
diff --git a/internal/proto/v1alpha1/run_function_grpc.pb.go b/internal/proto/v1alpha1/run_function_grpc.pb.go
new file mode 100644
index 0000000..b00031e
--- /dev/null
+++ b/internal/proto/v1alpha1/run_function_grpc.pb.go
@@ -0,0 +1,129 @@
+//
+//Copyright 2022 The Crossplane 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.
+
+// TODO(negz): Add a make target such that `make lint` runs `buf lint`.
+
+// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
+// versions:
+// - protoc-gen-go-grpc v1.3.0
+// - protoc (unknown)
+// source: v1alpha1/run_function.proto
+
+package v1alpha1
+
+import (
+ context "context"
+ grpc "google.golang.org/grpc"
+ codes "google.golang.org/grpc/codes"
+ status "google.golang.org/grpc/status"
+)
+
+// This is a compile-time assertion to ensure that this generated file
+// is compatible with the grpc package it is being compiled against.
+// Requires gRPC-Go v1.32.0 or later.
+const _ = grpc.SupportPackageIsVersion7
+
+const (
+ ContainerizedFunctionRunnerService_RunFunction_FullMethodName = "/apiextensions.fn.proto.v1alpha1.ContainerizedFunctionRunnerService/RunFunction"
+)
+
+// ContainerizedFunctionRunnerServiceClient is the client API for ContainerizedFunctionRunnerService service.
+//
+// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
+type ContainerizedFunctionRunnerServiceClient interface {
+ // RunFunction runs a containerized function.
+ RunFunction(ctx context.Context, in *RunFunctionRequest, opts ...grpc.CallOption) (*RunFunctionResponse, error)
+}
+
+type containerizedFunctionRunnerServiceClient struct {
+ cc grpc.ClientConnInterface
+}
+
+func NewContainerizedFunctionRunnerServiceClient(cc grpc.ClientConnInterface) ContainerizedFunctionRunnerServiceClient {
+ return &containerizedFunctionRunnerServiceClient{cc}
+}
+
+func (c *containerizedFunctionRunnerServiceClient) RunFunction(ctx context.Context, in *RunFunctionRequest, opts ...grpc.CallOption) (*RunFunctionResponse, error) {
+ out := new(RunFunctionResponse)
+ err := c.cc.Invoke(ctx, ContainerizedFunctionRunnerService_RunFunction_FullMethodName, in, out, opts...)
+ if err != nil {
+ return nil, err
+ }
+ return out, nil
+}
+
+// ContainerizedFunctionRunnerServiceServer is the server API for ContainerizedFunctionRunnerService service.
+// All implementations must embed UnimplementedContainerizedFunctionRunnerServiceServer
+// for forward compatibility
+type ContainerizedFunctionRunnerServiceServer interface {
+ // RunFunction runs a containerized function.
+ RunFunction(context.Context, *RunFunctionRequest) (*RunFunctionResponse, error)
+ mustEmbedUnimplementedContainerizedFunctionRunnerServiceServer()
+}
+
+// UnimplementedContainerizedFunctionRunnerServiceServer must be embedded to have forward compatible implementations.
+type UnimplementedContainerizedFunctionRunnerServiceServer struct {
+}
+
+func (UnimplementedContainerizedFunctionRunnerServiceServer) RunFunction(context.Context, *RunFunctionRequest) (*RunFunctionResponse, error) {
+ return nil, status.Errorf(codes.Unimplemented, "method RunFunction not implemented")
+}
+func (UnimplementedContainerizedFunctionRunnerServiceServer) mustEmbedUnimplementedContainerizedFunctionRunnerServiceServer() {
+}
+
+// UnsafeContainerizedFunctionRunnerServiceServer may be embedded to opt out of forward compatibility for this service.
+// Use of this interface is not recommended, as added methods to ContainerizedFunctionRunnerServiceServer will
+// result in compilation errors.
+type UnsafeContainerizedFunctionRunnerServiceServer interface {
+ mustEmbedUnimplementedContainerizedFunctionRunnerServiceServer()
+}
+
+func RegisterContainerizedFunctionRunnerServiceServer(s grpc.ServiceRegistrar, srv ContainerizedFunctionRunnerServiceServer) {
+ s.RegisterService(&ContainerizedFunctionRunnerService_ServiceDesc, srv)
+}
+
+func _ContainerizedFunctionRunnerService_RunFunction_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+ in := new(RunFunctionRequest)
+ if err := dec(in); err != nil {
+ return nil, err
+ }
+ if interceptor == nil {
+ return srv.(ContainerizedFunctionRunnerServiceServer).RunFunction(ctx, in)
+ }
+ info := &grpc.UnaryServerInfo{
+ Server: srv,
+ FullMethod: ContainerizedFunctionRunnerService_RunFunction_FullMethodName,
+ }
+ handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+ return srv.(ContainerizedFunctionRunnerServiceServer).RunFunction(ctx, req.(*RunFunctionRequest))
+ }
+ return interceptor(ctx, in, info, handler)
+}
+
+// ContainerizedFunctionRunnerService_ServiceDesc is the grpc.ServiceDesc for ContainerizedFunctionRunnerService service.
+// It's only intended for direct use with grpc.RegisterService,
+// and not to be introspected or modified (even as a copy)
+var ContainerizedFunctionRunnerService_ServiceDesc = grpc.ServiceDesc{
+ ServiceName: "apiextensions.fn.proto.v1alpha1.ContainerizedFunctionRunnerService",
+ HandlerType: (*ContainerizedFunctionRunnerServiceServer)(nil),
+ Methods: []grpc.MethodDesc{
+ {
+ MethodName: "RunFunction",
+ Handler: _ContainerizedFunctionRunnerService_RunFunction_Handler,
+ },
+ },
+ Streams: []grpc.StreamDesc{},
+ Metadata: "v1alpha1/run_function.proto",
+}
diff --git a/internal/version/fake/mocks.go b/internal/version/fake/mocks.go
new file mode 100644
index 0000000..edf2638
--- /dev/null
+++ b/internal/version/fake/mocks.go
@@ -0,0 +1,63 @@
+/*
+Copyright 2020 The Crossplane 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 fake contains semantic version mocks.
+package fake
+
+import (
+ "github.com/Masterminds/semver"
+
+ "github.com/crossplane/function-runtime-oci/internal/version"
+)
+
+var _ version.Operations = &MockVersioner{}
+
+// MockVersioner provides mock version operations.
+type MockVersioner struct {
+ MockGetVersionString func() string
+ MockGetSemVer func() (*semver.Version, error)
+ MockInConstraints func() (bool, error)
+}
+
+// NewMockGetVersionStringFn creates new MockGetVersionString function for MockVersioner.
+func NewMockGetVersionStringFn(s string) func() string {
+ return func() string { return s }
+}
+
+// NewMockGetSemVerFn creates new MockGetSemver function for MockVersioner.
+func NewMockGetSemVerFn(s *semver.Version, err error) func() (*semver.Version, error) {
+ return func() (*semver.Version, error) { return s, err }
+}
+
+// NewMockInConstraintsFn creates new MockInConstraintsString function for MockVersioner.
+func NewMockInConstraintsFn(b bool, err error) func() (bool, error) {
+ return func() (bool, error) { return b, err }
+}
+
+// GetVersionString calls the underlying MockGetVersionString.
+func (m *MockVersioner) GetVersionString() string {
+ return m.MockGetVersionString()
+}
+
+// GetSemVer calls the underlying MockGetSemVer.
+func (m *MockVersioner) GetSemVer() (*semver.Version, error) {
+ return m.MockGetSemVer()
+}
+
+// InConstraints calls the underlying MockInConstraints.
+func (m *MockVersioner) InConstraints(_ string) (bool, error) {
+ return m.MockInConstraints()
+}
diff --git a/internal/version/version.go b/internal/version/version.go
new file mode 100644
index 0000000..9146885
--- /dev/null
+++ b/internal/version/version.go
@@ -0,0 +1,67 @@
+/*
+Copyright 2020 The Crossplane 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 version contains utilities for working with semantic versions.
+package version
+
+import (
+ "github.com/Masterminds/semver"
+)
+
+var version string
+
+// Operations provides semantic version operations.
+type Operations interface {
+ GetVersionString() string
+ GetSemVer() (*semver.Version, error)
+ InConstraints(c string) (bool, error)
+}
+
+// Versioner provides semantic version operations.
+type Versioner struct {
+ version string
+}
+
+// New creates a new versioner.
+func New() *Versioner {
+ return &Versioner{
+ version: version,
+ }
+}
+
+// GetVersionString returns the current Crossplane version as string.
+func (v *Versioner) GetVersionString() string {
+ return v.version
+}
+
+// GetSemVer returns the current Crossplane version as a semantic version.
+func (v *Versioner) GetSemVer() (*semver.Version, error) {
+ return semver.NewVersion(v.version)
+}
+
+// InConstraints is a helper function that checks if the current Crossplane
+// version is in the semantic version constraints.
+func (v *Versioner) InConstraints(c string) (bool, error) {
+ ver, err := v.GetSemVer()
+ if err != nil {
+ return false, err
+ }
+ constraint, err := semver.NewConstraint(c)
+ if err != nil {
+ return false, err
+ }
+ return constraint.Check(ver), nil
+}
diff --git a/internal/version/version_test.go b/internal/version/version_test.go
new file mode 100644
index 0000000..772f731
--- /dev/null
+++ b/internal/version/version_test.go
@@ -0,0 +1,96 @@
+/*
+Copyright 2020 The Crossplane 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 version
+
+import (
+ "errors"
+ "testing"
+
+ "github.com/google/go-cmp/cmp"
+
+ "github.com/crossplane/crossplane-runtime/pkg/test"
+)
+
+func TestInRange(t *testing.T) {
+ type args struct {
+ version string
+ r string
+ }
+ type want struct {
+ is bool
+ err error
+ }
+ cases := map[string]struct {
+ reason string
+ args args
+ want want
+ }{
+ "ValidInRange": {
+ reason: "Should return true when a valid semantic version is in a valid range.",
+ args: args{
+ version: "v0.13.0",
+ r: ">0.12.0",
+ },
+ want: want{
+ is: true,
+ },
+ },
+ "ValidNotInRange": {
+ reason: "Should return false when a valid semantic version is not in a valid range.",
+ args: args{
+ version: "v0.13.0",
+ r: ">0.13.0",
+ },
+ want: want{
+ is: false,
+ },
+ },
+ "InvalidVersion": {
+ reason: "Should return error when version is invalid.",
+ args: args{
+ version: "v0a.13.0",
+ },
+ want: want{
+ err: errors.New("Invalid Semantic Version"),
+ },
+ },
+ "InvalidRange": {
+ reason: "Should return error when range is invalid.",
+ args: args{
+ version: "v0.13.0",
+ r: ">a2",
+ },
+ want: want{
+ err: errors.New("improper constraint: >a2"),
+ },
+ },
+ }
+
+ for name, tc := range cases {
+ t.Run(name, func(t *testing.T) {
+ version = tc.args.version
+ is, err := New().InConstraints(tc.args.r)
+ if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" {
+ t.Errorf("\n%s\nInRange(...): -want err, +got err:\n%s", tc.reason, diff)
+ }
+
+ if diff := cmp.Diff(tc.want.is, is, test.EquateErrors()); diff != "" {
+ t.Errorf("\n%s\nInRange(...): -want, +got:\n%s", tc.reason, diff)
+ }
+ })
+ }
+}