Moving XFN as is, just renamed and added ci/scripts
Signed-off-by: Philippe Scorsolini <p.scorsolini@gmail.com>
This commit is contained in:
parent
3e5580d2b5
commit
0895101d8e
|
|
@ -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: '(?<currentValue>.*?)'\\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(?<currentValue>.*?)'\\n",
|
||||
"GOLANGCILINT_VERSION = (?<currentValue>.*?)\\n"
|
||||
],
|
||||
"datasourceTemplate": "github-tags",
|
||||
"depNameTemplate": "golangci/golangci-lint",
|
||||
"extractVersionTemplate": "^v(?<version>.*)$"
|
||||
}, {
|
||||
"description": "Bump helm version in the Makefile",
|
||||
"fileMatch": ["^Makefile$"],
|
||||
"matchStrings": [
|
||||
"HELM3_VERSION = (?<currentValue>.*?)\\n"
|
||||
],
|
||||
"datasourceTemplate": "github-tags",
|
||||
"depNameTemplate": "helm/helm",
|
||||
}, {
|
||||
"description": "Bump kind version in the Makefile",
|
||||
"fileMatch": ["^Makefile$"],
|
||||
"matchStrings": [
|
||||
"KIND_VERSION = (?<currentValue>.*?)\\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"],
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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##*/}
|
||||
|
|
@ -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 }}
|
||||
|
|
@ -15,7 +15,11 @@
|
|||
*.out
|
||||
|
||||
# Dependency directories (remove the comment below to include it)
|
||||
# vendor/
|
||||
vendor/
|
||||
|
||||
# Go workspace file
|
||||
go.work
|
||||
|
||||
.cache/
|
||||
.idea/
|
||||
_output/
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
[submodule "build"]
|
||||
path = build
|
||||
url = https://github.com/upbound/build
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -0,0 +1 @@
|
|||
Subproject commit bd5297bd16c113cbc5ed1905b1d96aa1cb3078ec
|
||||
|
|
@ -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"]
|
||||
|
|
@ -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)
|
||||
|
|
@ -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}))
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
|
|
@ -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=
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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 <source> <bundle>"
|
||||
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)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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")
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -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")
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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) }
|
||||
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
)
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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",
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue