mirror of
https://github.com/containers/skopeo.git
synced 2025-04-27 19:05:32 +00:00
Some files in integration did not have _test, resulting in lots of complains when running golangci-lint with --tests=false. Signed-off-by: Kir Kolyshkin <kolyshkin@gmail.com>
292 lines
10 KiB
Go
292 lines
10 KiB
Go
package main
|
||
|
||
import (
|
||
"bytes"
|
||
"compress/gzip"
|
||
"encoding/json"
|
||
"io"
|
||
"net"
|
||
"net/netip"
|
||
"os"
|
||
"os/exec"
|
||
"path/filepath"
|
||
"strings"
|
||
"testing"
|
||
"time"
|
||
|
||
"github.com/containers/image/v5/manifest"
|
||
"github.com/opencontainers/go-digest"
|
||
"github.com/stretchr/testify/assert"
|
||
"github.com/stretchr/testify/require"
|
||
)
|
||
|
||
const skopeoBinary = "skopeo"
|
||
|
||
const testFQIN = "docker://quay.io/libpod/busybox" // tag left off on purpose, some tests need to add a special one
|
||
const testFQIN64 = "docker://quay.io/libpod/busybox:amd64"
|
||
const testFQINMultiLayer = "docker://quay.io/libpod/alpine_nginx:latest" // multi-layer
|
||
|
||
// consumeAndLogOutputStream takes (f, err) from an exec.*Pipe(), and causes all output to it to be logged to t.
|
||
func consumeAndLogOutputStream(t *testing.T, id string, f io.ReadCloser, err error) {
|
||
require.NoError(t, err)
|
||
go func() {
|
||
defer func() {
|
||
f.Close()
|
||
t.Logf("Output %s: Closed", id)
|
||
}()
|
||
buf := make([]byte, 1024)
|
||
for {
|
||
t.Logf("Output %s: waiting", id)
|
||
n, err := f.Read(buf)
|
||
t.Logf("Output %s: got %d,%#v: %s", id, n, err, strings.TrimSuffix(string(buf[:n]), "\n"))
|
||
if n <= 0 {
|
||
break
|
||
}
|
||
}
|
||
}()
|
||
}
|
||
|
||
// consumeAndLogOutputs causes all output to stdout and stderr from an *exec.Cmd to be logged to c.
|
||
func consumeAndLogOutputs(t *testing.T, id string, cmd *exec.Cmd) {
|
||
stdout, err := cmd.StdoutPipe()
|
||
consumeAndLogOutputStream(t, id+" stdout", stdout, err)
|
||
stderr, err := cmd.StderrPipe()
|
||
consumeAndLogOutputStream(t, id+" stderr", stderr, err)
|
||
}
|
||
|
||
// combinedOutputOfCommand runs a command as if exec.Command().CombinedOutput(), verifies that the exit status is 0, and returns the output,
|
||
// or terminates c on failure.
|
||
func combinedOutputOfCommand(t *testing.T, name string, args ...string) string {
|
||
t.Logf("Running %s %s", name, strings.Join(args, " "))
|
||
out, err := exec.Command(name, args...).CombinedOutput()
|
||
require.NoError(t, err, "%s", out)
|
||
return string(out)
|
||
}
|
||
|
||
// assertSkopeoSucceeds runs a skopeo command as if exec.Command().CombinedOutput, verifies that the exit status is 0,
|
||
// and optionally that the output matches a multi-line regexp if it is nonempty;
|
||
// or terminates c on failure
|
||
func assertSkopeoSucceeds(t *testing.T, regexp string, args ...string) {
|
||
t.Logf("Running %s %s", skopeoBinary, strings.Join(args, " "))
|
||
out, err := exec.Command(skopeoBinary, args...).CombinedOutput()
|
||
assert.NoError(t, err, "%s", out)
|
||
if regexp != "" {
|
||
assert.Regexp(t, "(?s)"+regexp, string(out)) // (?s) : '.' will also match newlines
|
||
}
|
||
}
|
||
|
||
// assertSkopeoFails runs a skopeo command as if exec.Command().CombinedOutput, verifies that the exit status is 0,
|
||
// and that the output matches a multi-line regexp;
|
||
// or terminates c on failure
|
||
func assertSkopeoFails(t *testing.T, regexp string, args ...string) {
|
||
t.Logf("Running %s %s", skopeoBinary, strings.Join(args, " "))
|
||
out, err := exec.Command(skopeoBinary, args...).CombinedOutput()
|
||
assert.Error(t, err, "%s", out)
|
||
assert.Regexp(t, "(?s)"+regexp, string(out)) // (?s) : '.' will also match newlines
|
||
}
|
||
|
||
// runCommandWithInput runs a command as if exec.Command(), sending it the input to stdin,
|
||
// and verifies that the exit status is 0, or terminates c on failure.
|
||
func runCommandWithInput(t *testing.T, input string, name string, args ...string) {
|
||
cmd := exec.Command(name, args...)
|
||
runExecCmdWithInput(t, cmd, input)
|
||
}
|
||
|
||
// runExecCmdWithInput runs an exec.Cmd, sending it the input to stdin,
|
||
// and verifies that the exit status is 0, or terminates c on failure.
|
||
func runExecCmdWithInput(t *testing.T, cmd *exec.Cmd, input string) {
|
||
t.Logf("Running %s %s", cmd.Path, strings.Join(cmd.Args, " "))
|
||
consumeAndLogOutputs(t, cmd.Path+" "+strings.Join(cmd.Args, " "), cmd)
|
||
stdin, err := cmd.StdinPipe()
|
||
require.NoError(t, err)
|
||
err = cmd.Start()
|
||
require.NoError(t, err)
|
||
_, err = io.WriteString(stdin, input)
|
||
require.NoError(t, err)
|
||
err = stdin.Close()
|
||
require.NoError(t, err)
|
||
err = cmd.Wait()
|
||
assert.NoError(t, err)
|
||
}
|
||
|
||
// isPortOpen returns true iff the specified port on localhost is open.
|
||
func isPortOpen(port uint16) bool {
|
||
ap := netip.AddrPortFrom(netip.AddrFrom4([4]byte{127, 0, 0, 1}), port)
|
||
conn, err := net.DialTCP("tcp", nil, net.TCPAddrFromAddrPort(ap))
|
||
if err != nil {
|
||
return false
|
||
}
|
||
conn.Close()
|
||
return true
|
||
}
|
||
|
||
// newPortChecker sets up a portOpen channel which will receive true after the specified port is open.
|
||
// The checking can be aborted by sending a value to the terminate channel, which the caller should
|
||
// always do using
|
||
// defer func() {terminate <- true}()
|
||
func newPortChecker(t *testing.T, port uint16) (portOpen <-chan bool, terminate chan<- bool) {
|
||
portOpenBidi := make(chan bool)
|
||
// Buffered, so that sending a terminate request after the goroutine has exited does not block.
|
||
terminateBidi := make(chan bool, 1)
|
||
|
||
go func() {
|
||
defer func() {
|
||
t.Logf("Port checker for port %d exiting", port)
|
||
}()
|
||
for {
|
||
t.Logf("Checking for port %d...", port)
|
||
if isPortOpen(port) {
|
||
t.Logf("Port %d open", port)
|
||
portOpenBidi <- true
|
||
return
|
||
}
|
||
t.Logf("Sleeping for port %d", port)
|
||
sleepChan := time.After(100 * time.Millisecond)
|
||
select {
|
||
case <-sleepChan: // Try again
|
||
t.Logf("Sleeping for port %d done, will retry", port)
|
||
case <-terminateBidi:
|
||
t.Logf("Check for port %d terminated", port)
|
||
return
|
||
}
|
||
}
|
||
}()
|
||
return portOpenBidi, terminateBidi
|
||
}
|
||
|
||
// modifyEnviron modifies os.Environ()-like list of name=value assignments to set name to value.
|
||
func modifyEnviron(env []string, name, value string) []string {
|
||
prefix := name + "="
|
||
res := []string{}
|
||
for _, e := range env {
|
||
if !strings.HasPrefix(e, prefix) {
|
||
res = append(res, e)
|
||
}
|
||
}
|
||
return append(res, prefix+value)
|
||
}
|
||
|
||
// fileFromFixture applies edits to inputPath and returns a path to the temporary file.
|
||
// Callers should defer os.Remove(the_returned_path)
|
||
func fileFromFixture(t *testing.T, inputPath string, edits map[string]string) string {
|
||
contents, err := os.ReadFile(inputPath)
|
||
require.NoError(t, err)
|
||
for template, value := range edits {
|
||
updated := bytes.ReplaceAll(contents, []byte(template), []byte(value))
|
||
require.NotEqual(t, contents, updated, "Replacing %s in %#v failed", template, string(contents)) // Verify that the template has matched something and we are not silently ignoring it.
|
||
contents = updated
|
||
}
|
||
|
||
file, err := os.CreateTemp("", "policy.json")
|
||
require.NoError(t, err)
|
||
path := file.Name()
|
||
|
||
_, err = file.Write(contents)
|
||
require.NoError(t, err)
|
||
err = file.Close()
|
||
require.NoError(t, err)
|
||
return path
|
||
}
|
||
|
||
// decompressDirs decompresses specified dir:-formatted directories
|
||
func decompressDirs(t *testing.T, dirs ...string) {
|
||
t.Logf("Decompressing %s", strings.Join(dirs, " "))
|
||
for i, dir := range dirs {
|
||
m, err := os.ReadFile(filepath.Join(dir, "manifest.json"))
|
||
require.NoError(t, err)
|
||
t.Logf("manifest %d before: %s", i+1, string(m))
|
||
|
||
decompressDir(t, dir)
|
||
|
||
m, err = os.ReadFile(filepath.Join(dir, "manifest.json"))
|
||
require.NoError(t, err)
|
||
t.Logf("manifest %d after: %s", i+1, string(m))
|
||
}
|
||
}
|
||
|
||
// getRawMapField assigns a value of rawMap[key] to dest,
|
||
// failing if it does not exist or if it doesn’t have the expected type
|
||
func getRawMapField[T any](t *testing.T, rawMap map[string]any, key string, dest *T) {
|
||
rawValue, ok := rawMap[key]
|
||
require.True(t, ok, key)
|
||
value, ok := rawValue.(T)
|
||
require.True(t, ok, key, "%#v", value)
|
||
*dest = value
|
||
}
|
||
|
||
// decompressDir modifies a dir:-formatted directory to replace gzip-compressed layers with uncompressed variants,
|
||
// and to use a ~canonical formatting of manifest.json.
|
||
func decompressDir(t *testing.T, dir string) {
|
||
// This is, overall, very dumb; the “obvious” way would be to invoke skopeo to decompress,
|
||
// or at least to use c/image to parse/format the manifest.
|
||
//
|
||
// But this is used to test (aspects of) those code paths… so, it’s acceptable for this to be
|
||
// dumb and to make assumptions about the data, but it should not share code.
|
||
|
||
manifestBlob, err := os.ReadFile(filepath.Join(dir, "manifest.json"))
|
||
require.NoError(t, err)
|
||
var rawManifest map[string]any
|
||
err = json.Unmarshal(manifestBlob, &rawManifest)
|
||
require.NoError(t, err)
|
||
var rawLayers []any
|
||
getRawMapField(t, rawManifest, "layers", &rawLayers)
|
||
for i, rawLayerValue := range rawLayers {
|
||
rawLayer, ok := rawLayerValue.(map[string]any)
|
||
require.True(t, ok)
|
||
var digestString string
|
||
getRawMapField(t, rawLayer, "digest", &digestString)
|
||
compressedDigest, err := digest.Parse(digestString)
|
||
require.NoError(t, err)
|
||
if compressedDigest.String() == "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" { // An empty file
|
||
continue
|
||
}
|
||
|
||
compressedPath := filepath.Join(dir, compressedDigest.Encoded())
|
||
compressedStream, err := os.Open(compressedPath)
|
||
require.NoError(t, err)
|
||
defer compressedStream.Close()
|
||
|
||
uncompressedStream, err := gzip.NewReader(compressedStream)
|
||
if err != nil {
|
||
continue // Silently assume the layer is not gzip-compressed
|
||
}
|
||
tempDest, err := os.CreateTemp(dir, "decompressing")
|
||
require.NoError(t, err)
|
||
digester := digest.Canonical.Digester()
|
||
uncompressedSize, err := io.Copy(tempDest, io.TeeReader(uncompressedStream, digester.Hash()))
|
||
require.NoError(t, err)
|
||
err = uncompressedStream.Close()
|
||
require.NoError(t, err)
|
||
uncompressedDigest := digester.Digest()
|
||
uncompressedPath := filepath.Join(dir, uncompressedDigest.Encoded())
|
||
err = os.Rename(tempDest.Name(), uncompressedPath)
|
||
require.NoError(t, err)
|
||
err = os.Remove(compressedPath)
|
||
require.NoError(t, err)
|
||
|
||
rawLayer["digest"] = uncompressedDigest.String()
|
||
rawLayer["size"] = uncompressedSize
|
||
var mimeType string
|
||
getRawMapField(t, rawLayer, "mediaType", &mimeType)
|
||
if uncompressedMIMEType, ok := strings.CutSuffix(mimeType, ".gzip"); ok {
|
||
rawLayer["mediaType"] = uncompressedMIMEType
|
||
}
|
||
|
||
rawLayers[i] = rawLayer
|
||
}
|
||
rawManifest["layers"] = rawLayers
|
||
|
||
manifestBlob, err = json.Marshal(rawManifest)
|
||
require.NoError(t, err)
|
||
err = os.WriteFile(filepath.Join(dir, "manifest.json"), manifestBlob, 0o600)
|
||
require.NoError(t, err)
|
||
}
|
||
|
||
// Verify manifest in a dir: image at dir is expectedMIMEType.
|
||
func verifyManifestMIMEType(t *testing.T, dir string, expectedMIMEType string) {
|
||
manifestBlob, err := os.ReadFile(filepath.Join(dir, "manifest.json"))
|
||
require.NoError(t, err)
|
||
mimeType := manifest.GuessMIMEType(manifestBlob)
|
||
assert.Equal(t, expectedMIMEType, mimeType)
|
||
}
|