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) + } + }) + } +}