Add image verifier transfer service plugin system based on a binary directory
Signed-off-by: Ethan Lowman <ethan.lowman@datadoghq.com>
This commit is contained in:
parent
5c37d3827b
commit
ac1d556b92
@ -24,6 +24,7 @@ import (
|
||||
_ "github.com/containerd/containerd/leases/plugin"
|
||||
_ "github.com/containerd/containerd/metadata/plugin"
|
||||
_ "github.com/containerd/containerd/pkg/nri/plugin"
|
||||
_ "github.com/containerd/containerd/plugins/imageverifier"
|
||||
_ "github.com/containerd/containerd/plugins/sandbox"
|
||||
_ "github.com/containerd/containerd/plugins/streaming"
|
||||
_ "github.com/containerd/containerd/plugins/transfer"
|
||||
|
51
docs/image-verification.md
Normal file
51
docs/image-verification.md
Normal file
@ -0,0 +1,51 @@
|
||||
# Image Verification
|
||||
|
||||
The following covers the default "bindir" `ImageVerifier` plugin implementation.
|
||||
|
||||
To enable image verification, add a stanza like the following to the containerd config:
|
||||
|
||||
```yaml
|
||||
[plugins]
|
||||
[plugins."io.containerd.image-verifier.v1.bindir"]
|
||||
bin_dir = "/opt/containerd/image-verifier/bin"
|
||||
max_verifiers = 10
|
||||
per_verifier_timeout = "10s"
|
||||
```
|
||||
|
||||
All files in `bin_dir`, if it exists, must be verifier executables which conform to the following API.
|
||||
|
||||
## Image Verifier Binary API
|
||||
|
||||
### CLI Arguments
|
||||
|
||||
- `-name`: The given reference to the image that may be pulled.
|
||||
- `-digest`: The resolved digest of the image that may be pulled.
|
||||
- `-stdin-media-type`: The media type of the JSON data passed to stdin.
|
||||
|
||||
### Standard Input
|
||||
|
||||
A JSON encoded payload is passed to the verifier binary's standard input. The
|
||||
media type of this payload is specified by the `-stdin-media-type` CLI
|
||||
argument, and may change in future versions of containerd. Currently, the
|
||||
payload has a media type of `application/vnd.oci.descriptor.v1+json` and
|
||||
represents the OCI Content Descriptor of the image that may be pulled. See
|
||||
[the OCI specification](https://github.com/opencontainers/image-spec/blob/main/descriptor.md)
|
||||
for more details.
|
||||
|
||||
### Image Pull Judgement
|
||||
|
||||
Print to standard output a reason for the image pull judgement.
|
||||
|
||||
Return an exit code of 0 to allow the image to be pulled and any other exit code to block the image from being pulled.
|
||||
|
||||
## Image Verifier Caller Contract
|
||||
|
||||
- If `bin_dir` does not exist or contains no files, the image verifier does not block image pulls.
|
||||
- An image is pulled only if all verifiers that are called return an "ok" judgement (exit with status code 0). In other words, image pull judgements are combined with an `AND` operator.
|
||||
- If any verifiers exceeds the `per_verifier_timeout` or fails to exec, the verification fails with an error and a `nil` judgement is returned.
|
||||
- If `max_verifiers < 0`, there is no imposed limit on the number of image verifiers called.
|
||||
- If `max_verifiers >= 0`, there is a limit imposed on the number of image verifiers called. The entries in `bin_dir` are lexicographically sorted by name, and the first `n = max_verifiers` of the verifiers will be called, and the rest will be skipped.
|
||||
- There is no guarantee for the order of execution of verifier binaries.
|
||||
- Standard error output of verifier binaries is logged at debug level by containerd, subject to truncation.
|
||||
- Standard output of verifier binaries (the "reason" for the judgement) is subject to truncation.
|
||||
- System resources used by verifier binaries are currently accounted for in and constrained by containerd's own cgroup, but this is subject to change.
|
259
pkg/imageverifier/bindir/bindir.go
Normal file
259
pkg/imageverifier/bindir/bindir.go
Normal file
@ -0,0 +1,259 @@
|
||||
/*
|
||||
Copyright The containerd Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package bindir
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/containerd/containerd/log"
|
||||
"github.com/containerd/containerd/pkg/imageverifier"
|
||||
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
const outputLimitBytes = 1 << 15 // 32 KiB
|
||||
|
||||
type Config struct {
|
||||
BinDir string `toml:"bin_dir"`
|
||||
MaxVerifiers int `toml:"max_verifiers"`
|
||||
PerVerifierTimeout time.Duration `toml:"per_verifier_timeout"`
|
||||
}
|
||||
|
||||
type ImageVerifier struct {
|
||||
config *Config
|
||||
}
|
||||
|
||||
var _ imageverifier.ImageVerifier = (*ImageVerifier)(nil)
|
||||
|
||||
func NewImageVerifier(c *Config) *ImageVerifier {
|
||||
return &ImageVerifier{
|
||||
config: c,
|
||||
}
|
||||
}
|
||||
|
||||
func (v *ImageVerifier) VerifyImage(ctx context.Context, name string, desc ocispec.Descriptor) (*imageverifier.Judgement, error) {
|
||||
// os.ReadDir sorts entries by name.
|
||||
entries, err := os.ReadDir(v.config.BinDir)
|
||||
if err != nil {
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return &imageverifier.Judgement{
|
||||
OK: true,
|
||||
Reason: fmt.Sprintf("image verifier directory %v does not exist", v.config.BinDir),
|
||||
}, nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("failed to list directory contents: %w", err)
|
||||
}
|
||||
|
||||
if len(entries) == 0 {
|
||||
return &imageverifier.Judgement{
|
||||
OK: true,
|
||||
Reason: fmt.Sprintf("no image verifier binaries found in %v", v.config.BinDir),
|
||||
}, nil
|
||||
}
|
||||
|
||||
reason := &strings.Builder{}
|
||||
for i, entry := range entries {
|
||||
if (i+1) > v.config.MaxVerifiers && v.config.MaxVerifiers >= 0 {
|
||||
log.G(ctx).Warnf("image verifiers are being skipped since directory %v has %v entries, more than configured max of %v verifiers", v.config.BinDir, len(entries), v.config.MaxVerifiers)
|
||||
break
|
||||
}
|
||||
|
||||
bin := entry.Name()
|
||||
start := time.Now()
|
||||
exitCode, vr, err := v.runVerifier(ctx, bin, name, desc)
|
||||
runtime := time.Since(start)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to call verifier %v (runtime %v): %w", bin, runtime, err)
|
||||
}
|
||||
|
||||
if exitCode != 0 {
|
||||
return &imageverifier.Judgement{
|
||||
OK: false,
|
||||
Reason: fmt.Sprintf("verifier %v rejected image (exit code %v): %v", bin, exitCode, vr),
|
||||
}, nil
|
||||
}
|
||||
|
||||
if i > 0 {
|
||||
reason.WriteString(", ")
|
||||
}
|
||||
reason.WriteString(fmt.Sprintf("%v => %v", bin, vr))
|
||||
}
|
||||
|
||||
return &imageverifier.Judgement{
|
||||
OK: true,
|
||||
Reason: reason.String(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (v *ImageVerifier) runVerifier(ctx context.Context, bin string, imageName string, desc ocispec.Descriptor) (exitCode int, reason string, err error) {
|
||||
ctx, cancel := context.WithTimeout(ctx, v.config.PerVerifierTimeout)
|
||||
defer cancel()
|
||||
|
||||
binPath := filepath.Join(v.config.BinDir, bin)
|
||||
args := []string{
|
||||
"-name", imageName,
|
||||
"-digest", desc.Digest.String(),
|
||||
"-stdin-media-type", ocispec.MediaTypeDescriptor,
|
||||
}
|
||||
|
||||
cmd := exec.CommandContext(ctx, binPath, args...)
|
||||
|
||||
// We construct our own pipes instead of using the default StdinPipe,
|
||||
// StoutPipe, and StderrPipe in order to set timeouts on reads and writes.
|
||||
stdinRead, stdinWrite, err := os.Pipe()
|
||||
if err != nil {
|
||||
return -1, "", err
|
||||
}
|
||||
cmd.Stdin = stdinRead
|
||||
defer stdinRead.Close()
|
||||
defer stdinWrite.Close()
|
||||
|
||||
stdoutRead, stdoutWrite, err := os.Pipe()
|
||||
if err != nil {
|
||||
return -1, "", err
|
||||
}
|
||||
cmd.Stdout = stdoutWrite
|
||||
defer stdoutRead.Close()
|
||||
defer stdoutWrite.Close()
|
||||
|
||||
stderrRead, stderrWrite, err := os.Pipe()
|
||||
if err != nil {
|
||||
return -1, "", err
|
||||
}
|
||||
cmd.Stderr = stderrWrite
|
||||
defer stderrRead.Close()
|
||||
defer stderrWrite.Close()
|
||||
|
||||
// Close parent ends of pipes on timeout. Without this, I/O may hang in the
|
||||
// parent process.
|
||||
if d, ok := ctx.Deadline(); ok {
|
||||
stdinWrite.SetDeadline(d)
|
||||
stdoutRead.SetDeadline(d)
|
||||
stderrRead.SetDeadline(d)
|
||||
}
|
||||
|
||||
// Finish configuring, and then fork & exec the child process.
|
||||
p, err := startProcess(ctx, cmd)
|
||||
if err != nil {
|
||||
return -1, "", err
|
||||
}
|
||||
defer p.cleanup(ctx)
|
||||
|
||||
// Close the child ends of the pipes in the parent process.
|
||||
stdinRead.Close()
|
||||
stdoutWrite.Close()
|
||||
stderrWrite.Close()
|
||||
|
||||
// Write the descriptor to stdin.
|
||||
go func() {
|
||||
// Descriptors are usually small enough to fit in a pipe buffer (which is
|
||||
// often 64 KiB on Linux) so this write usually won't block on the child
|
||||
// process reading stdin. However, synchronously writing to stdin may cause
|
||||
// the parent to block if the descriptor is larger than the pipe buffer and
|
||||
// the child process doesn't read stdin. Therefore, we write to stdin
|
||||
// asynchronously, limited by the stdinWrite deadline set above.
|
||||
err := json.NewEncoder(stdinWrite).Encode(desc)
|
||||
if err != nil {
|
||||
// This may error out with a "broken pipe" error if the descriptor is
|
||||
// larger than the pipe buffer and the child process does not read all
|
||||
// of stdin.
|
||||
log.G(ctx).WithError(err).Warn("failed to completely write descriptor to stdin")
|
||||
}
|
||||
stdinWrite.Close()
|
||||
}()
|
||||
|
||||
// Pipe verifier stderr lines to debug logs.
|
||||
stderrLog := log.G(ctx).Logger.WithFields(logrus.Fields{
|
||||
"image_verifier": bin,
|
||||
"stream": "stderr",
|
||||
})
|
||||
stderrLogDone := make(chan struct{})
|
||||
go func() {
|
||||
defer close(stderrLogDone)
|
||||
defer stderrRead.Close()
|
||||
lr := &io.LimitedReader{
|
||||
R: stderrRead,
|
||||
N: outputLimitBytes,
|
||||
}
|
||||
|
||||
s := bufio.NewScanner(lr)
|
||||
for s.Scan() {
|
||||
stderrLog.Debug(s.Text())
|
||||
}
|
||||
if err := s.Err(); err != nil {
|
||||
stderrLog.WithError(err).Debug("error logging image verifier stderr")
|
||||
}
|
||||
|
||||
if lr.N == 0 {
|
||||
// Peek ahead to see if stderr reader was truncated.
|
||||
b := make([]byte, 1)
|
||||
if n, _ := stderrRead.Read(b); n > 0 {
|
||||
stderrLog.Debug("(previous logs may be truncated)")
|
||||
}
|
||||
}
|
||||
|
||||
// Discard the truncated part of stderr. Doing this rather than closing the
|
||||
// reader avoids broken pipe errors. This is bounded by the stderrRead
|
||||
// deadline.
|
||||
if _, err := io.Copy(io.Discard, stderrRead); err != nil {
|
||||
log.G(ctx).WithError(err).Error("error flushing stderr")
|
||||
}
|
||||
}()
|
||||
|
||||
stdout, err := io.ReadAll(io.LimitReader(stdoutRead, outputLimitBytes))
|
||||
if err != nil {
|
||||
log.G(ctx).WithError(err).Error("error reading stdout")
|
||||
} else {
|
||||
m := strings.Builder{}
|
||||
m.WriteString(strings.TrimSpace(string(stdout)))
|
||||
// Peek ahead to see if stdout is truncated.
|
||||
b := make([]byte, 1)
|
||||
if n, _ := stdoutRead.Read(b); n > 0 {
|
||||
m.WriteString("(stdout truncated)")
|
||||
}
|
||||
reason = m.String()
|
||||
}
|
||||
|
||||
// Discard the truncated part of stdout. Doing this rather than closing the
|
||||
// reader avoids broken pipe errors. This is bounded by the stdoutRead
|
||||
// deadline.
|
||||
if _, err := io.Copy(io.Discard, stdoutRead); err != nil {
|
||||
log.G(ctx).WithError(err).Error("error flushing stdout")
|
||||
}
|
||||
stdoutRead.Close()
|
||||
|
||||
<-stderrLogDone
|
||||
if err := cmd.Wait(); err != nil {
|
||||
if ee := (&exec.ExitError{}); errors.As(err, &ee) && ee.ProcessState.Exited() {
|
||||
return ee.ProcessState.ExitCode(), reason, nil
|
||||
}
|
||||
return -1, "", fmt.Errorf("waiting on command to exit: %v", err)
|
||||
}
|
||||
|
||||
return cmd.ProcessState.ExitCode(), reason, nil
|
||||
}
|
419
pkg/imageverifier/bindir/bindir_test.go
Normal file
419
pkg/imageverifier/bindir/bindir_test.go
Normal file
@ -0,0 +1,419 @@
|
||||
/*
|
||||
Copyright The containerd Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package bindir
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
"github.com/containerd/containerd/log"
|
||||
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// buildGoVerifiers uses the local Go toolchain to build each of the standalone
|
||||
// main package source files in srcDir into binaries placed in binDir.
|
||||
func buildGoVerifiers(t *testing.T, srcsDir string, binDir string) {
|
||||
srcs, err := os.ReadDir(srcsDir)
|
||||
require.NoError(t, err)
|
||||
|
||||
for _, srcFile := range srcs {
|
||||
// Build the source into a Go binary.
|
||||
src := filepath.Join(srcsDir, srcFile.Name())
|
||||
bin := filepath.Join(binDir, strings.Split(srcFile.Name(), ".")[0]+exeIfWindows())
|
||||
cmd := exec.Command("go", "build", "-o", bin, src)
|
||||
|
||||
code, err := os.ReadFile(src)
|
||||
require.NoError(t, err)
|
||||
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
t.Fatalf("failed to build test verifier %s: %v\n%s\nGo code:\n%s", src, err, out, code)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func exeIfWindows() string {
|
||||
// The command `go build -o abc abc.go` creates abc.exe on Windows.
|
||||
if runtime.GOOS == "windows" {
|
||||
return ".exe"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// newBinDir creates a temporary directory and copies each of the selected bins
|
||||
// fromSrcDir into that directory. The copied verifier binaries are given names
|
||||
// such that they sort (and therefore execute) in the order that bins is given.
|
||||
func newBinDir(t *testing.T, srcDir string, bins ...string) string {
|
||||
binDir := t.TempDir()
|
||||
|
||||
for i, bin := range bins {
|
||||
src, err := os.Open(filepath.Join(srcDir, bin+exeIfWindows()))
|
||||
require.NoError(t, err)
|
||||
defer src.Close()
|
||||
|
||||
dst, err := os.OpenFile(filepath.Join(binDir, fmt.Sprintf("verifier-%v%v", i, exeIfWindows())), os.O_WRONLY|os.O_CREATE, 0755)
|
||||
require.NoError(t, err)
|
||||
defer dst.Close()
|
||||
|
||||
_, err = io.Copy(dst, src)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
return binDir
|
||||
}
|
||||
|
||||
func TestBinDirVerifyImage(t *testing.T) {
|
||||
// Enable debug logs to easily see stderr for verifiers upon test failure.
|
||||
logger := log.L.Dup()
|
||||
logger.Logger.SetLevel(logrus.DebugLevel)
|
||||
ctx := log.WithLogger(context.Background(), logger)
|
||||
|
||||
// Build verifiers from plain Go file.
|
||||
allBinsDir := t.TempDir()
|
||||
buildGoVerifiers(t, "testdata/verifiers", allBinsDir)
|
||||
|
||||
// Build verifiers from templates.
|
||||
data := struct {
|
||||
ArgsFile string
|
||||
StdinFile string
|
||||
}{
|
||||
ArgsFile: filepath.Join(t.TempDir(), "args.txt"),
|
||||
StdinFile: filepath.Join(t.TempDir(), "stdin.txt"),
|
||||
}
|
||||
|
||||
tmplDir := "testdata/verifier_templates"
|
||||
templates, err := os.ReadDir(tmplDir)
|
||||
require.NoError(t, err)
|
||||
|
||||
renderedVerifierTmplDir := t.TempDir()
|
||||
for _, tmplFile := range templates {
|
||||
tmplPath := filepath.Join(tmplDir, tmplFile.Name())
|
||||
|
||||
tmpl, err := template.New(tmplFile.Name()).ParseFiles(tmplPath)
|
||||
require.NoError(t, err)
|
||||
|
||||
goFileName := strings.ReplaceAll(tmplFile.Name(), ".go.tmpl", ".go")
|
||||
f, err := os.Create(filepath.Join(renderedVerifierTmplDir, goFileName))
|
||||
require.NoError(t, err)
|
||||
defer f.Close()
|
||||
|
||||
require.NoError(t, tmpl.Execute(f, data))
|
||||
f.Close()
|
||||
}
|
||||
buildGoVerifiers(t, renderedVerifierTmplDir, allBinsDir)
|
||||
|
||||
// Actual tests begin here.
|
||||
t.Run("proper input/output management", func(t *testing.T) {
|
||||
binDir := newBinDir(t, allBinsDir,
|
||||
"verifier_test_input_output_management",
|
||||
)
|
||||
|
||||
v := NewImageVerifier(&Config{
|
||||
BinDir: binDir,
|
||||
MaxVerifiers: -1,
|
||||
PerVerifierTimeout: 5 * time.Second,
|
||||
})
|
||||
|
||||
j, err := v.VerifyImage(ctx, "registry.example.com/image:abc", ocispec.Descriptor{
|
||||
Digest: "sha256:98ea6e4f216f2fb4b69fff9b3a44842c38686ca685f3f55dc48c5d3fb1107be4",
|
||||
MediaType: "application/vnd.docker.distribution.manifest.list.v2+json",
|
||||
Size: 2048,
|
||||
Annotations: map[string]string{"a": "b"},
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, j.OK)
|
||||
assert.Equal(t, fmt.Sprintf("verifier-0%[1]v => Reason A line 1\nReason A line 2", exeIfWindows()), j.Reason)
|
||||
|
||||
b, err := os.ReadFile(data.ArgsFile)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "-name registry.example.com/image:abc -digest sha256:98ea6e4f216f2fb4b69fff9b3a44842c38686ca685f3f55dc48c5d3fb1107be4 -stdin-media-type application/vnd.oci.descriptor.v1+json", string(b))
|
||||
|
||||
b, err = os.ReadFile(data.StdinFile)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, `{"mediaType":"application/vnd.docker.distribution.manifest.list.v2+json","digest":"sha256:98ea6e4f216f2fb4b69fff9b3a44842c38686ca685f3f55dc48c5d3fb1107be4","size":2048,"annotations":{"a":"b"}}`, strings.TrimSpace(string(b)))
|
||||
})
|
||||
|
||||
t.Run("large output is truncated", func(t *testing.T) {
|
||||
bins := []string{
|
||||
"large_stdout",
|
||||
"large_stdout_chunked",
|
||||
"large_stderr",
|
||||
"large_stderr_chunked",
|
||||
}
|
||||
binDir := newBinDir(t, allBinsDir, bins...)
|
||||
|
||||
v := NewImageVerifier(&Config{
|
||||
BinDir: binDir,
|
||||
MaxVerifiers: -1,
|
||||
PerVerifierTimeout: 30 * time.Second,
|
||||
})
|
||||
|
||||
j, err := v.VerifyImage(ctx, "registry.example.com/image:abc", ocispec.Descriptor{})
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, j.OK, "expected OK, got not OK with reason: %v", j.Reason)
|
||||
assert.Less(t, len(j.Reason), len(bins)*(outputLimitBytes+1024), "reason is: %v", j.Reason) // 1024 leaves margin for the formatting around the reason.
|
||||
})
|
||||
|
||||
t.Run("missing directory", func(t *testing.T) {
|
||||
v := NewImageVerifier(&Config{
|
||||
BinDir: filepath.Join(t.TempDir(), "missing_directory"),
|
||||
MaxVerifiers: 10,
|
||||
PerVerifierTimeout: 5 * time.Second,
|
||||
})
|
||||
|
||||
j, err := v.VerifyImage(ctx, "registry.example.com/image:abc", ocispec.Descriptor{})
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, j.OK)
|
||||
assert.NotEmpty(t, j.Reason)
|
||||
})
|
||||
|
||||
t.Run("empty directory", func(t *testing.T) {
|
||||
v := NewImageVerifier(&Config{
|
||||
BinDir: t.TempDir(),
|
||||
MaxVerifiers: 10,
|
||||
PerVerifierTimeout: 5 * time.Second,
|
||||
})
|
||||
|
||||
j, err := v.VerifyImage(ctx, "registry.example.com/image:abc", ocispec.Descriptor{})
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, j.OK)
|
||||
assert.NotEmpty(t, j.Reason)
|
||||
})
|
||||
|
||||
t.Run("max verifiers = 0", func(t *testing.T) {
|
||||
binDir := newBinDir(t, allBinsDir,
|
||||
"reject_reason_d", // This never runs.
|
||||
)
|
||||
|
||||
v := NewImageVerifier(&Config{
|
||||
BinDir: binDir,
|
||||
MaxVerifiers: 0,
|
||||
PerVerifierTimeout: 5 * time.Second,
|
||||
})
|
||||
|
||||
j, err := v.VerifyImage(ctx, "registry.example.com/image:abc", ocispec.Descriptor{})
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, j.OK)
|
||||
assert.Empty(t, j.Reason)
|
||||
})
|
||||
|
||||
t.Run("max verifiers = 1", func(t *testing.T) {
|
||||
binDir := newBinDir(t, allBinsDir,
|
||||
"accept_reason_a",
|
||||
"reject_reason_d", // This never runs.
|
||||
)
|
||||
|
||||
v := NewImageVerifier(&Config{
|
||||
BinDir: binDir,
|
||||
MaxVerifiers: 1,
|
||||
PerVerifierTimeout: 5 * time.Second,
|
||||
})
|
||||
|
||||
j, err := v.VerifyImage(ctx, "registry.example.com/image:abc", ocispec.Descriptor{})
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, j.OK)
|
||||
assert.NotEmpty(t, j.Reason)
|
||||
})
|
||||
|
||||
t.Run("max verifiers = 2", func(t *testing.T) {
|
||||
binDir := newBinDir(t, allBinsDir,
|
||||
"accept_reason_a",
|
||||
"accept_reason_a",
|
||||
"reject_reason_d", // This never runs.
|
||||
)
|
||||
|
||||
v := NewImageVerifier(&Config{
|
||||
BinDir: binDir,
|
||||
MaxVerifiers: 2,
|
||||
PerVerifierTimeout: 5 * time.Second,
|
||||
})
|
||||
|
||||
j, err := v.VerifyImage(ctx, "registry.example.com/image:abc", ocispec.Descriptor{})
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, j.OK)
|
||||
assert.NotEmpty(t, j.Reason)
|
||||
})
|
||||
|
||||
t.Run("max verifiers = 3, all accept", func(t *testing.T) {
|
||||
binDir := newBinDir(t, allBinsDir,
|
||||
"accept_reason_a",
|
||||
"accept_reason_b",
|
||||
"accept_reason_c",
|
||||
)
|
||||
v := NewImageVerifier(&Config{
|
||||
BinDir: binDir,
|
||||
MaxVerifiers: 3,
|
||||
PerVerifierTimeout: 5 * time.Second,
|
||||
})
|
||||
|
||||
j, err := v.VerifyImage(ctx, "registry.example.com/image:abc", ocispec.Descriptor{})
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, j.OK)
|
||||
assert.Equal(t, fmt.Sprintf("verifier-0%[1]v => Reason A, verifier-1%[1]v => Reason B, verifier-2%[1]v => Reason C", exeIfWindows()), j.Reason)
|
||||
})
|
||||
|
||||
t.Run("max verifiers = 3, with reject", func(t *testing.T) {
|
||||
binDir := newBinDir(t, allBinsDir,
|
||||
"accept_reason_a",
|
||||
"accept_reason_b",
|
||||
"reject_reason_d",
|
||||
)
|
||||
|
||||
v := NewImageVerifier(&Config{
|
||||
BinDir: binDir,
|
||||
MaxVerifiers: 3,
|
||||
PerVerifierTimeout: 5 * time.Second,
|
||||
})
|
||||
|
||||
j, err := v.VerifyImage(ctx, "registry.example.com/image:abc", ocispec.Descriptor{})
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, j.OK)
|
||||
assert.Equal(t, fmt.Sprintf("verifier verifier-2%[1]v rejected image (exit code 1): Reason D", exeIfWindows()), j.Reason)
|
||||
})
|
||||
|
||||
t.Run("max verifiers = -1, all accept", func(t *testing.T) {
|
||||
binDir := newBinDir(t, allBinsDir,
|
||||
"accept_reason_a",
|
||||
"accept_reason_b",
|
||||
"accept_reason_c",
|
||||
)
|
||||
|
||||
v := NewImageVerifier(&Config{
|
||||
BinDir: binDir,
|
||||
MaxVerifiers: -1,
|
||||
PerVerifierTimeout: 5 * time.Second,
|
||||
})
|
||||
|
||||
j, err := v.VerifyImage(ctx, "registry.example.com/image:abc", ocispec.Descriptor{})
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, j.OK)
|
||||
assert.Equal(t, fmt.Sprintf("verifier-0%[1]v => Reason A, verifier-1%[1]v => Reason B, verifier-2%[1]v => Reason C", exeIfWindows()), j.Reason)
|
||||
})
|
||||
|
||||
t.Run("max verifiers = -1, with reject", func(t *testing.T) {
|
||||
binDir := newBinDir(t, allBinsDir,
|
||||
"accept_reason_a",
|
||||
"accept_reason_b",
|
||||
"reject_reason_d",
|
||||
)
|
||||
|
||||
v := NewImageVerifier(&Config{
|
||||
BinDir: binDir,
|
||||
MaxVerifiers: -1,
|
||||
PerVerifierTimeout: 5 * time.Second,
|
||||
})
|
||||
|
||||
j, err := v.VerifyImage(ctx, "registry.example.com/image:abc", ocispec.Descriptor{})
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, j.OK)
|
||||
assert.Equal(t, fmt.Sprintf("verifier verifier-2%[1]v rejected image (exit code 1): Reason D", exeIfWindows()), j.Reason)
|
||||
})
|
||||
|
||||
t.Run("max verifiers = -1, with timeout", func(t *testing.T) {
|
||||
binDir := newBinDir(t, allBinsDir,
|
||||
"accept_reason_a",
|
||||
"accept_reason_b",
|
||||
"slow_child_process",
|
||||
)
|
||||
|
||||
v := NewImageVerifier(&Config{
|
||||
BinDir: binDir,
|
||||
MaxVerifiers: -1,
|
||||
PerVerifierTimeout: 5 * time.Second,
|
||||
})
|
||||
|
||||
j, err := v.VerifyImage(ctx, "registry.example.com/image:abc", ocispec.Descriptor{})
|
||||
if runtime.GOOS == "windows" {
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, j.OK)
|
||||
assert.Equal(t, "verifier verifier-2.exe rejected image (exit code 1): ", j.Reason)
|
||||
} else {
|
||||
assert.ErrorContains(t, err, "signal: killed")
|
||||
assert.Nil(t, j)
|
||||
}
|
||||
|
||||
command := []string{"ps", "ax"}
|
||||
if runtime.GOOS == "windows" {
|
||||
command = []string{"tasklist"}
|
||||
}
|
||||
b, err := exec.Command(command[0], command[1:]...).CombinedOutput()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if strings.Contains(string(b), "verifier-") {
|
||||
t.Fatalf("killing the verifier binary didn't kill all its children:\n%v", string(b))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("max verifiers = -1, with exec failure", func(t *testing.T) {
|
||||
binDir := t.TempDir()
|
||||
err := os.WriteFile(filepath.Join(binDir, "bad.sh"), []byte("BAD"), 0744)
|
||||
require.NoError(t, err)
|
||||
|
||||
v := NewImageVerifier(&Config{
|
||||
BinDir: binDir,
|
||||
MaxVerifiers: -1,
|
||||
PerVerifierTimeout: 5 * time.Second,
|
||||
})
|
||||
|
||||
j, err := v.VerifyImage(ctx, "registry.example.com/image:abc", ocispec.Descriptor{})
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, j)
|
||||
})
|
||||
|
||||
t.Run("descriptor larger than linux pipe buffer, verifier doesn't read stdin", func(t *testing.T) {
|
||||
binDir := newBinDir(t, allBinsDir,
|
||||
"accept_reason_a",
|
||||
)
|
||||
|
||||
v := NewImageVerifier(&Config{
|
||||
BinDir: binDir,
|
||||
MaxVerifiers: 1,
|
||||
PerVerifierTimeout: 5 * time.Second,
|
||||
})
|
||||
|
||||
j, err := v.VerifyImage(ctx, "registry.example.com/image:abc", ocispec.Descriptor{
|
||||
Digest: "sha256:98ea6e4f216f2fb4b69fff9b3a44842c38686ca685f3f55dc48c5d3fb1107be4",
|
||||
MediaType: "application/vnd.docker.distribution.manifest.list.v2+json",
|
||||
Size: 2048,
|
||||
Annotations: map[string]string{
|
||||
// Pipe buffer is usually 64KiB.
|
||||
"large_payload": strings.Repeat("0", 2*64*(1<<10)),
|
||||
},
|
||||
})
|
||||
|
||||
// Should see a log like the following, but verification still succeeds:
|
||||
// time="2023-09-05T11:15:50-04:00" level=warning msg="failed to completely write descriptor to stdin" error="write |1: broken pipe"
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, j.OK)
|
||||
assert.NotEmpty(t, j.Reason)
|
||||
})
|
||||
}
|
55
pkg/imageverifier/bindir/processes_unix.go
Normal file
55
pkg/imageverifier/bindir/processes_unix.go
Normal file
@ -0,0 +1,55 @@
|
||||
//go:build !windows
|
||||
|
||||
/*
|
||||
Copyright The containerd Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package bindir
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
type process struct {
|
||||
cmd *exec.Cmd
|
||||
}
|
||||
|
||||
// Configure the verifier command so that killing it kills all child
|
||||
// processes of the verifier process.
|
||||
func startProcess(ctx context.Context, cmd *exec.Cmd) (*process, error) {
|
||||
// Assign the verifier a new process group so that killing its process group
|
||||
// in Cancel() doesn't kill the parent process (containerd).
|
||||
cmd.SysProcAttr = &unix.SysProcAttr{Setpgid: true}
|
||||
|
||||
cmd.Cancel = func() error {
|
||||
// Passing a negative PID causes kill(2) to kill all processes in the
|
||||
// process group whose ID is cmd.Process.Pid.
|
||||
return unix.Kill(-cmd.Process.Pid, unix.SIGKILL)
|
||||
}
|
||||
|
||||
if err := cmd.Start(); err != nil {
|
||||
return nil, fmt.Errorf("starting process: %w", err)
|
||||
}
|
||||
|
||||
return &process{
|
||||
cmd: cmd,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (p *process) cleanup(ctx context.Context) {}
|
105
pkg/imageverifier/bindir/processes_windows.go
Normal file
105
pkg/imageverifier/bindir/processes_windows.go
Normal file
@ -0,0 +1,105 @@
|
||||
//go:build windows
|
||||
|
||||
/*
|
||||
Copyright The containerd Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package bindir
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"unsafe"
|
||||
|
||||
"github.com/containerd/containerd/log"
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
type process struct {
|
||||
cmd *exec.Cmd
|
||||
|
||||
jobHandle *windows.Handle
|
||||
processHandle *windows.Handle
|
||||
}
|
||||
|
||||
// Configure the verifier command so that killing it kills all child
|
||||
// processes of the verifier process.
|
||||
//
|
||||
// Job/process management based on:
|
||||
// https://devblogs.microsoft.com/oldnewthing/20131209-00/?p=2433
|
||||
func startProcess(ctx context.Context, cmd *exec.Cmd) (*process, error) {
|
||||
p := &process{
|
||||
cmd: cmd,
|
||||
}
|
||||
|
||||
jobHandle, err := windows.CreateJobObject(nil, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("creating job object: %w", err)
|
||||
}
|
||||
p.jobHandle = &jobHandle
|
||||
|
||||
info := windows.JOBOBJECT_EXTENDED_LIMIT_INFORMATION{
|
||||
BasicLimitInformation: windows.JOBOBJECT_BASIC_LIMIT_INFORMATION{
|
||||
LimitFlags: windows.JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE,
|
||||
},
|
||||
}
|
||||
_, err = windows.SetInformationJobObject(
|
||||
jobHandle,
|
||||
windows.JobObjectExtendedLimitInformation,
|
||||
uintptr(unsafe.Pointer(&info)),
|
||||
uint32(unsafe.Sizeof(info)),
|
||||
)
|
||||
if err != nil {
|
||||
p.cleanup(ctx)
|
||||
return nil, fmt.Errorf("setting limits for job object: %w", err)
|
||||
}
|
||||
|
||||
if err := cmd.Start(); err != nil {
|
||||
p.cleanup(ctx)
|
||||
return nil, fmt.Errorf("starting process: %w", err)
|
||||
}
|
||||
|
||||
processHandle, err := windows.OpenProcess(
|
||||
windows.PROCESS_QUERY_INFORMATION|windows.PROCESS_SET_QUOTA|windows.PROCESS_TERMINATE,
|
||||
false,
|
||||
uint32(cmd.Process.Pid),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("getting handle for verifier process: %w", err)
|
||||
}
|
||||
p.processHandle = &processHandle
|
||||
|
||||
err = windows.AssignProcessToJobObject(jobHandle, processHandle)
|
||||
if err != nil {
|
||||
p.cleanup(ctx)
|
||||
return nil, fmt.Errorf("associating new process to job object: %w", err)
|
||||
}
|
||||
|
||||
return p, nil
|
||||
}
|
||||
|
||||
func (p *process) cleanup(ctx context.Context) {
|
||||
if p.jobHandle != nil {
|
||||
if err := windows.CloseHandle(*p.jobHandle); err != nil {
|
||||
log.G(ctx).WithError(err).Error("failed to close job handle")
|
||||
}
|
||||
}
|
||||
if p.processHandle != nil {
|
||||
if err := windows.CloseHandle(*p.processHandle); err != nil {
|
||||
log.G(ctx).WithError(err).Error("failed to close process handle")
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,45 @@
|
||||
/*
|
||||
Copyright The containerd Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func main() {
|
||||
err := os.WriteFile(`{{.ArgsFile}}`, []byte(strings.Join(os.Args[1:], " ")), 0644)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
stdin, err := io.ReadAll(os.Stdin)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
err = os.WriteFile(`{{.StdinFile}}`, stdin, 0644)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
fmt.Println("Reason A line 1")
|
||||
fmt.Fprintln(os.Stderr, "Debug A line 1")
|
||||
fmt.Println("Reason A line 2")
|
||||
fmt.Fprintln(os.Stderr, "Debug A line 2")
|
||||
}
|
23
pkg/imageverifier/bindir/testdata/verifiers/accept_reason_a.go
vendored
Normal file
23
pkg/imageverifier/bindir/testdata/verifiers/accept_reason_a.go
vendored
Normal file
@ -0,0 +1,23 @@
|
||||
/*
|
||||
Copyright The containerd Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package main
|
||||
|
||||
import "fmt"
|
||||
|
||||
func main() {
|
||||
fmt.Println("Reason A")
|
||||
}
|
23
pkg/imageverifier/bindir/testdata/verifiers/accept_reason_b.go
vendored
Normal file
23
pkg/imageverifier/bindir/testdata/verifiers/accept_reason_b.go
vendored
Normal file
@ -0,0 +1,23 @@
|
||||
/*
|
||||
Copyright The containerd Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package main
|
||||
|
||||
import "fmt"
|
||||
|
||||
func main() {
|
||||
fmt.Println("Reason B")
|
||||
}
|
23
pkg/imageverifier/bindir/testdata/verifiers/accept_reason_c.go
vendored
Normal file
23
pkg/imageverifier/bindir/testdata/verifiers/accept_reason_c.go
vendored
Normal file
@ -0,0 +1,23 @@
|
||||
/*
|
||||
Copyright The containerd Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package main
|
||||
|
||||
import "fmt"
|
||||
|
||||
func main() {
|
||||
fmt.Println("Reason C")
|
||||
}
|
35
pkg/imageverifier/bindir/testdata/verifiers/large_stderr.go
vendored
Normal file
35
pkg/imageverifier/bindir/testdata/verifiers/large_stderr.go
vendored
Normal file
@ -0,0 +1,35 @@
|
||||
/*
|
||||
Copyright The containerd Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func main() {
|
||||
n := 50000
|
||||
fmt.Fprintf(os.Stderr, "attempting to write %v bytes to stderr\n", n)
|
||||
|
||||
wrote, err := fmt.Fprintf(os.Stderr, strings.Repeat("A", n))
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "got error writing to stderr: %v\n", err)
|
||||
}
|
||||
|
||||
fmt.Fprintf(os.Stderr, "wrote %v bytes to stderr\n", wrote)
|
||||
}
|
45
pkg/imageverifier/bindir/testdata/verifiers/large_stderr_chunked.go
vendored
Normal file
45
pkg/imageverifier/bindir/testdata/verifiers/large_stderr_chunked.go
vendored
Normal file
@ -0,0 +1,45 @@
|
||||
/*
|
||||
Copyright The containerd Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
func main() {
|
||||
n := 500000
|
||||
fmt.Fprintf(os.Stderr, "attempting to write %v bytes to stderr\n", n)
|
||||
|
||||
// Writing this all in one fmt.Fprintf has a different interaction with
|
||||
// stderr pipe buffering than writing one byte at a time.
|
||||
wrote := 0
|
||||
for i := 0; i < n; i++ {
|
||||
w, err := fmt.Fprintf(os.Stderr, "A")
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "got error writing to stderr: %v\n", err)
|
||||
}
|
||||
|
||||
wrote += w
|
||||
|
||||
if i%10000 == 0 {
|
||||
fmt.Fprintf(os.Stderr, "progress: wrote %v bytes to stderr\n", wrote)
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Fprintf(os.Stderr, "wrote %v bytes to stderr\n", wrote)
|
||||
}
|
35
pkg/imageverifier/bindir/testdata/verifiers/large_stdout.go
vendored
Normal file
35
pkg/imageverifier/bindir/testdata/verifiers/large_stdout.go
vendored
Normal file
@ -0,0 +1,35 @@
|
||||
/*
|
||||
Copyright The containerd Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func main() {
|
||||
n := 50000
|
||||
fmt.Fprintf(os.Stderr, "attempting to write %v bytes to stdout\n", n)
|
||||
|
||||
wrote, err := fmt.Print(strings.Repeat("A", n))
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "got error writing to stdout: %v\n", err)
|
||||
}
|
||||
|
||||
fmt.Fprintf(os.Stderr, "wrote %v bytes to stdout\n", wrote)
|
||||
}
|
45
pkg/imageverifier/bindir/testdata/verifiers/large_stdout_chunked.go
vendored
Normal file
45
pkg/imageverifier/bindir/testdata/verifiers/large_stdout_chunked.go
vendored
Normal file
@ -0,0 +1,45 @@
|
||||
/*
|
||||
Copyright The containerd Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
func main() {
|
||||
n := 500000
|
||||
fmt.Fprintf(os.Stderr, "attempting to write %v bytes to stdout\n", n)
|
||||
|
||||
// Writing this all in one fmt.Print has a different interaction with stdout
|
||||
// pipe buffering than writing one byte at a time.
|
||||
wrote := 0
|
||||
for i := 0; i < n; i++ {
|
||||
w, err := fmt.Print("A")
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "got error writing to stdout: %v\n", err)
|
||||
}
|
||||
|
||||
wrote += w
|
||||
|
||||
if i%10000 == 0 {
|
||||
fmt.Fprintf(os.Stderr, "progress: wrote %v bytes to stdout\n", wrote)
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Fprintf(os.Stderr, "wrote %v bytes to stdout\n", wrote)
|
||||
}
|
27
pkg/imageverifier/bindir/testdata/verifiers/reject_reason_d.go
vendored
Normal file
27
pkg/imageverifier/bindir/testdata/verifiers/reject_reason_d.go
vendored
Normal file
@ -0,0 +1,27 @@
|
||||
/*
|
||||
Copyright The containerd Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
func main() {
|
||||
fmt.Println("Reason D")
|
||||
os.Exit(1)
|
||||
}
|
40
pkg/imageverifier/bindir/testdata/verifiers/slow_child_process.go
vendored
Normal file
40
pkg/imageverifier/bindir/testdata/verifiers/slow_child_process.go
vendored
Normal file
@ -0,0 +1,40 @@
|
||||
/*
|
||||
Copyright The containerd Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Launch a slow child process by re-executing this binary with the -sleep-forever flag.
|
||||
if len(os.Args) == 2 && os.Args[1] == "-sleep-forever" {
|
||||
fmt.Println("sleeping forever...")
|
||||
for {
|
||||
}
|
||||
}
|
||||
|
||||
thisBin := os.Args[0]
|
||||
cmd := exec.Command(thisBin, "-sleep-forever")
|
||||
b, err := cmd.CombinedOutput()
|
||||
fmt.Println(string(b))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
32
pkg/imageverifier/image_verifier.go
Normal file
32
pkg/imageverifier/image_verifier.go
Normal file
@ -0,0 +1,32 @@
|
||||
/*
|
||||
Copyright The containerd Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package imageverifier
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
)
|
||||
|
||||
type ImageVerifier interface {
|
||||
VerifyImage(ctx context.Context, name string, desc ocispec.Descriptor) (*Judgement, error)
|
||||
}
|
||||
|
||||
type Judgement struct {
|
||||
OK bool
|
||||
Reason string
|
||||
}
|
@ -30,6 +30,7 @@ import (
|
||||
"github.com/containerd/containerd/remotes"
|
||||
"github.com/containerd/containerd/remotes/docker"
|
||||
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func (ts *localTransferService) pull(ctx context.Context, ir transfer.ImageFetcher, is transfer.ImageStorer, tops *transfer.Config) error {
|
||||
@ -54,6 +55,32 @@ func (ts *localTransferService) pull(ctx context.Context, ir transfer.ImageFetch
|
||||
return fmt.Errorf("schema 1 image manifests are no longer supported: %w", errdefs.ErrInvalidArgument)
|
||||
}
|
||||
|
||||
// Verify image before pulling.
|
||||
for vfName, vf := range ts.verifiers {
|
||||
log := log.G(ctx).WithFields(logrus.Fields{
|
||||
"name": name,
|
||||
"digest": desc.Digest.String(),
|
||||
"verifier": vfName,
|
||||
})
|
||||
log.Debug("Verifying image pull")
|
||||
|
||||
jdg, err := vf.VerifyImage(ctx, name, desc)
|
||||
if err != nil {
|
||||
log.WithError(err).Error("No judgement received from verifier")
|
||||
return fmt.Errorf("blocking pull of %v with digest %v: image verifier %v returned error: %w", name, desc.Digest.String(), vfName, err)
|
||||
}
|
||||
log = log.WithFields(logrus.Fields{
|
||||
"ok": jdg.OK,
|
||||
"reason": jdg.Reason,
|
||||
})
|
||||
|
||||
if !jdg.OK {
|
||||
log.Warn("Image verifier blocked pull")
|
||||
return fmt.Errorf("image verifier %s blocked pull of %v with digest %v for reason: %v", vfName, name, desc.Digest.String(), jdg.Reason)
|
||||
}
|
||||
log.Debug("Image verifier allowed pull")
|
||||
}
|
||||
|
||||
// TODO: Handle already exists
|
||||
if tops.Progress != nil {
|
||||
tops.Progress(transfer.Progress{
|
||||
|
@ -29,15 +29,17 @@ import (
|
||||
"github.com/containerd/containerd/errdefs"
|
||||
"github.com/containerd/containerd/images"
|
||||
"github.com/containerd/containerd/leases"
|
||||
"github.com/containerd/containerd/pkg/imageverifier"
|
||||
"github.com/containerd/containerd/pkg/kmutex"
|
||||
"github.com/containerd/containerd/pkg/transfer"
|
||||
"github.com/containerd/containerd/pkg/unpack"
|
||||
)
|
||||
|
||||
type localTransferService struct {
|
||||
leases leases.Manager
|
||||
content content.Store
|
||||
images images.Store
|
||||
leases leases.Manager
|
||||
content content.Store
|
||||
images images.Store
|
||||
verifiers map[string]imageverifier.ImageVerifier
|
||||
// limiter for upload
|
||||
limiterU *semaphore.Weighted
|
||||
// limiter for download operation
|
||||
@ -45,12 +47,13 @@ type localTransferService struct {
|
||||
config TransferConfig
|
||||
}
|
||||
|
||||
func NewTransferService(lm leases.Manager, cs content.Store, is images.Store, tc *TransferConfig) transfer.Transferrer {
|
||||
func NewTransferService(lm leases.Manager, cs content.Store, is images.Store, vfs map[string]imageverifier.ImageVerifier, tc *TransferConfig) transfer.Transferrer {
|
||||
ts := &localTransferService{
|
||||
leases: lm,
|
||||
content: cs,
|
||||
images: is,
|
||||
config: *tc,
|
||||
leases: lm,
|
||||
content: cs,
|
||||
images: is,
|
||||
verifiers: vfs,
|
||||
config: *tc,
|
||||
}
|
||||
if tc.MaxConcurrentUploadedLayers > 0 {
|
||||
ts.limiterU = semaphore.NewWeighted(int64(tc.MaxConcurrentUploadedLayers))
|
||||
@ -88,6 +91,7 @@ func (ts *localTransferService) Transfer(ctx context.Context, src interface{}, d
|
||||
case transfer.ImageExportStreamer:
|
||||
return ts.echo(ctx, s, d, topts)
|
||||
case transfer.ImageStorer:
|
||||
// TODO: verify imports with ImageVerifiers?
|
||||
return ts.importStream(ctx, s, d, topts)
|
||||
}
|
||||
}
|
||||
|
@ -90,6 +90,8 @@ const (
|
||||
SandboxStorePlugin Type = "io.containerd.sandbox.store.v1"
|
||||
// SandboxControllerPlugin implements a sandbox controller
|
||||
SandboxControllerPlugin Type = "io.containerd.sandbox.controller.v1"
|
||||
// ImageVerifierPlugin implements an image verifier service
|
||||
ImageVerifierPlugin Type = "io.containerd.image-verifier.v1"
|
||||
)
|
||||
|
||||
const (
|
||||
|
21
plugins/imageverifier/path_unix.go
Normal file
21
plugins/imageverifier/path_unix.go
Normal file
@ -0,0 +1,21 @@
|
||||
//go:build !windows
|
||||
|
||||
/*
|
||||
Copyright The containerd Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package imageverifier
|
||||
|
||||
var defaultPath = "/opt/containerd/image-verifier/bin"
|
25
plugins/imageverifier/path_windows.go
Normal file
25
plugins/imageverifier/path_windows.go
Normal file
@ -0,0 +1,25 @@
|
||||
/*
|
||||
Copyright The containerd Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package imageverifier
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
|
||||
"github.com/containerd/containerd/defaults"
|
||||
)
|
||||
|
||||
var defaultPath = filepath.Join(defaults.DefaultRootDir, "opt", "image-verifier", "bin")
|
45
plugins/imageverifier/plugin.go
Normal file
45
plugins/imageverifier/plugin.go
Normal file
@ -0,0 +1,45 @@
|
||||
/*
|
||||
Copyright The containerd Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package imageverifier
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/containerd/containerd/pkg/imageverifier/bindir"
|
||||
"github.com/containerd/containerd/plugin"
|
||||
)
|
||||
|
||||
// Register default image verifier service plugin
|
||||
func init() {
|
||||
plugin.Register(&plugin.Registration{
|
||||
Type: plugin.ImageVerifierPlugin,
|
||||
ID: "bindir",
|
||||
Config: defaultConfig(),
|
||||
InitFn: func(ic *plugin.InitContext) (interface{}, error) {
|
||||
cfg := ic.Config.(*bindir.Config)
|
||||
return bindir.NewImageVerifier(cfg), nil
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func defaultConfig() *bindir.Config {
|
||||
return &bindir.Config{
|
||||
BinDir: defaultPath,
|
||||
MaxVerifiers: 10,
|
||||
PerVerifierTimeout: 10 * time.Second,
|
||||
}
|
||||
}
|
@ -25,6 +25,7 @@ import (
|
||||
"github.com/containerd/containerd/leases"
|
||||
"github.com/containerd/containerd/log"
|
||||
"github.com/containerd/containerd/metadata"
|
||||
"github.com/containerd/containerd/pkg/imageverifier"
|
||||
"github.com/containerd/containerd/pkg/transfer/local"
|
||||
"github.com/containerd/containerd/pkg/unpack"
|
||||
"github.com/containerd/containerd/platforms"
|
||||
@ -45,6 +46,7 @@ func init() {
|
||||
plugin.LeasePlugin,
|
||||
plugin.MetadataPlugin,
|
||||
plugin.DiffPlugin,
|
||||
plugin.ImageVerifierPlugin,
|
||||
},
|
||||
Config: defaultConfig(),
|
||||
InitFn: func(ic *plugin.InitContext) (interface{}, error) {
|
||||
@ -59,6 +61,20 @@ func init() {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
vfs := make(map[string]imageverifier.ImageVerifier)
|
||||
vps, err := ic.GetByType(plugin.ImageVerifierPlugin)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for name, vp := range vps {
|
||||
inst, err := vp.Instance()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
vfs[name] = inst.(imageverifier.ImageVerifier)
|
||||
}
|
||||
|
||||
// Set configuration based on default or user input
|
||||
var lc local.TransferConfig
|
||||
lc.MaxConcurrentDownloads = config.MaxConcurrentDownloads
|
||||
@ -126,7 +142,7 @@ func init() {
|
||||
}
|
||||
lc.RegistryConfigPath = config.RegistryConfigPath
|
||||
|
||||
return local.NewTransferService(l.(leases.Manager), ms.ContentStore(), metadata.NewImageStore(ms), &lc), nil
|
||||
return local.NewTransferService(l.(leases.Manager), ms.ContentStore(), metadata.NewImageStore(ms), vfs, &lc), nil
|
||||
},
|
||||
})
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user