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
|
*.out
|
||||||
|
|
||||||
# Dependency directories (remove the comment below to include it)
|
# Dependency directories (remove the comment below to include it)
|
||||||
# vendor/
|
vendor/
|
||||||
|
|
||||||
# Go workspace file
|
# Go workspace file
|
||||||
go.work
|
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