podman/test/e2e/artifact_test.go

383 lines
14 KiB
Go

//go:build linux || freebsd
package integration
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"
"github.com/containers/podman/v5/pkg/libartifact"
. "github.com/containers/podman/v5/test/utils"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
. "github.com/onsi/gomega/gexec"
)
const (
//nolint:revive,stylecheck
ARTIFACT_SINGLE = "quay.io/libpod/testartifact:20250206-single"
//nolint:revive,stylecheck
ARTIFACT_MULTI = "quay.io/libpod/testartifact:20250206-multi"
//nolint:revive,stylecheck
ARTIFACT_MULTI_NO_TITLE = "quay.io/libpod/testartifact:20250206-multi-no-title"
//nolint:revive,stylecheck
ARTIFACT_EVIL = "quay.io/libpod/testartifact:20250206-evil"
)
var _ = Describe("Podman artifact", func() {
BeforeEach(func() {
SkipIfRemote("artifacts are not supported on the remote client yet due to being in development still")
})
It("podman artifact ls", func() {
artifact1File, err := createArtifactFile(4192)
Expect(err).ToNot(HaveOccurred())
artifact1Name := "localhost/test/artifact1"
add1 := podmanTest.PodmanExitCleanly([]string{"artifact", "add", artifact1Name, artifact1File}...)
artifact2File, err := createArtifactFile(10240)
Expect(err).ToNot(HaveOccurred())
artifact2Name := "localhost/test/artifact2"
podmanTest.PodmanExitCleanly([]string{"artifact", "add", artifact2Name, artifact2File}...)
// Should be three items in the list
listSession := podmanTest.PodmanExitCleanly([]string{"artifact", "ls"}...)
Expect(listSession.OutputToStringArray()).To(HaveLen(3))
// --format should work
listFormatSession := podmanTest.PodmanExitCleanly([]string{"artifact", "ls", "--format", "{{.Repository}}"}...)
output := listFormatSession.OutputToStringArray()
// There should be only 2 "lines" because the header should not be output
Expect(output).To(HaveLen(2))
// Make sure the names are what we expect
Expect(output).To(ContainElement(artifact1Name))
Expect(output).To(ContainElement(artifact2Name))
// Check default digest length (should be 12)
defaultFormatSession := podmanTest.PodmanExitCleanly([]string{"artifact", "ls", "--format", "{{.Digest}}"}...)
defaultOutput := defaultFormatSession.OutputToStringArray()[0]
Expect(defaultOutput).To(HaveLen(12))
// Check with --no-trunc and verify the len of the digest is the same as the len what was returned when the artifact
// was added
noTruncSession := podmanTest.PodmanExitCleanly([]string{"artifact", "ls", "--no-trunc", "--format", "{{.Digest}}"}...)
truncOutput := noTruncSession.OutputToStringArray()[0]
Expect(truncOutput).To(HaveLen(len(add1.OutputToString())))
// check with --noheading and verify the header is not present through a line count AND substring match
noHeaderSession := podmanTest.PodmanExitCleanly([]string{"artifact", "ls", "--noheading"}...)
noHeaderOutput := noHeaderSession.OutputToStringArray()
Expect(noHeaderOutput).To(HaveLen(2))
Expect(noHeaderOutput).ToNot(ContainElement("REPOSITORY"))
})
It("podman artifact simple add", func() {
artifact1File, err := createArtifactFile(1024)
Expect(err).ToNot(HaveOccurred())
artifact1Name := "localhost/test/artifact1"
podmanTest.PodmanExitCleanly([]string{"artifact", "add", artifact1Name, artifact1File}...)
inspectSingleSession := podmanTest.PodmanExitCleanly([]string{"artifact", "inspect", artifact1Name}...)
a := libartifact.Artifact{}
inspectOut := inspectSingleSession.OutputToString()
err = json.Unmarshal([]byte(inspectOut), &a)
Expect(err).ToNot(HaveOccurred())
Expect(a.Name).To(Equal(artifact1Name))
// Adding an artifact with an existing name should fail
addAgain := podmanTest.Podman([]string{"artifact", "add", artifact1Name, artifact1File})
addAgain.WaitWithDefaultTimeout()
Expect(addAgain).ShouldNot(ExitCleanly())
Expect(addAgain.ErrorToString()).To(Equal(fmt.Sprintf("Error: artifact %s already exists", artifact1Name)))
})
It("podman artifact add with options", func() {
artifact1Name := "localhost/test/artifact1"
artifact1File, err := createArtifactFile(1024)
Expect(err).ToNot(HaveOccurred())
artifactType := "octet/foobar"
annotation1 := "color=blue"
annotation2 := "flavor=lemon"
podmanTest.PodmanExitCleanly([]string{"artifact", "add", "--type", artifactType, "--annotation", annotation1, "--annotation", annotation2, artifact1Name, artifact1File}...)
inspectSingleSession := podmanTest.PodmanExitCleanly([]string{"artifact", "inspect", artifact1Name}...)
a := libartifact.Artifact{}
err = json.Unmarshal([]byte(inspectSingleSession.OutputToString()), &a)
Expect(err).ToNot(HaveOccurred())
Expect(a.Name).To(Equal(artifact1Name))
Expect(a.Manifest.ArtifactType).To(Equal(artifactType))
Expect(a.Manifest.Layers[0].Annotations["color"]).To(Equal("blue"))
Expect(a.Manifest.Layers[0].Annotations["flavor"]).To(Equal("lemon"))
failSession := podmanTest.Podman([]string{"artifact", "add", "--annotation", "org.opencontainers.image.title=foobar", "foobar", artifact1File})
failSession.WaitWithDefaultTimeout()
Expect(failSession).Should(Exit(125))
Expect(failSession.ErrorToString()).Should(Equal("Error: cannot override filename with org.opencontainers.image.title annotation"))
})
It("podman artifact add multiple", func() {
artifact1File1, err := createArtifactFile(1024)
Expect(err).ToNot(HaveOccurred())
artifact1File2, err := createArtifactFile(8192)
Expect(err).ToNot(HaveOccurred())
artifact1Name := "localhost/test/artifact1"
podmanTest.PodmanExitCleanly([]string{"artifact", "add", artifact1Name, artifact1File1, artifact1File2}...)
inspectSingleSession := podmanTest.PodmanExitCleanly([]string{"artifact", "inspect", artifact1Name}...)
a := libartifact.Artifact{}
inspectOut := inspectSingleSession.OutputToString()
err = json.Unmarshal([]byte(inspectOut), &a)
Expect(err).ToNot(HaveOccurred())
Expect(a.Name).To(Equal(artifact1Name))
Expect(a.Manifest.Layers).To(HaveLen(2))
})
It("podman artifact push and pull", func() {
artifact1File, err := createArtifactFile(1024)
Expect(err).ToNot(HaveOccurred())
lock, port, err := setupRegistry(nil)
if err == nil {
defer lock.Unlock()
}
Expect(err).ToNot(HaveOccurred())
artifact1Name := fmt.Sprintf("localhost:%s/test/artifact1", port)
podmanTest.PodmanExitCleanly([]string{"artifact", "add", artifact1Name, artifact1File}...)
podmanTest.PodmanExitCleanly([]string{"artifact", "push", "-q", "--tls-verify=false", artifact1Name}...)
podmanTest.PodmanExitCleanly([]string{"artifact", "rm", artifact1Name}...)
podmanTest.PodmanExitCleanly([]string{"artifact", "pull", "--tls-verify=false", artifact1Name}...)
inspectSingleSession := podmanTest.PodmanExitCleanly([]string{"artifact", "inspect", artifact1Name}...)
a := libartifact.Artifact{}
inspectOut := inspectSingleSession.OutputToString()
err = json.Unmarshal([]byte(inspectOut), &a)
Expect(err).ToNot(HaveOccurred())
Expect(a.Name).To(Equal(artifact1Name))
})
It("podman artifact remove", func() {
// Trying to remove an image that does not exist should fail
rmFail := podmanTest.Podman([]string{"artifact", "rm", "foobar"})
rmFail.WaitWithDefaultTimeout()
Expect(rmFail).Should(Exit(125))
Expect(rmFail.ErrorToString()).Should(Equal(fmt.Sprintf("Error: no artifact found with name or digest of %s", "foobar")))
// Add an artifact to remove later
artifact1File, err := createArtifactFile(4192)
Expect(err).ToNot(HaveOccurred())
artifact1Name := "localhost/test/artifact1"
addArtifact1 := podmanTest.PodmanExitCleanly([]string{"artifact", "add", artifact1Name, artifact1File}...)
// Removing that artifact should work
rmWorks := podmanTest.PodmanExitCleanly([]string{"artifact", "rm", artifact1Name}...)
// The digests printed by removal should be the same as the digest that was added
Expect(addArtifact1.OutputToString()).To(Equal(rmWorks.OutputToString()))
// Inspecting that the removed artifact should fail
inspectArtifact := podmanTest.Podman([]string{"artifact", "inspect", artifact1Name})
inspectArtifact.WaitWithDefaultTimeout()
Expect(inspectArtifact).Should(Exit(125))
Expect(inspectArtifact.ErrorToString()).To(Equal(fmt.Sprintf("Error: no artifact found with name or digest of %s", artifact1Name)))
})
It("podman artifact inspect with full or partial digest", func() {
artifact1File, err := createArtifactFile(4192)
Expect(err).ToNot(HaveOccurred())
artifact1Name := "localhost/test/artifact1"
addArtifact1 := podmanTest.PodmanExitCleanly([]string{"artifact", "add", artifact1Name, artifact1File}...)
artifactDigest := addArtifact1.OutputToString()
podmanTest.PodmanExitCleanly([]string{"artifact", "inspect", artifactDigest}...)
podmanTest.PodmanExitCleanly([]string{"artifact", "inspect", artifactDigest[:12]}...)
})
It("podman artifact extract single", func() {
podmanTest.PodmanExitCleanly("artifact", "pull", ARTIFACT_SINGLE)
const (
artifactContent = "mRuO9ykak1Q2j\n"
artifactDigest = "sha256:e9510923578af3632946ecf5ae479c1b5f08b47464e707b5cbab9819272a9752"
artifactTitle = "testfile"
)
path := filepath.Join(podmanTest.TempDir, "testfile")
// Extract to non existing file
podmanTest.PodmanExitCleanly("artifact", "extract", ARTIFACT_SINGLE, path)
Expect(readFileToString(path)).To(Equal(artifactContent))
// Extract to existing file will overwrite file
path = filepath.Join(podmanTest.TempDir, "abcd")
f, err := os.Create(path)
Expect(err).ToNot(HaveOccurred())
f.Close()
podmanTest.PodmanExitCleanly("artifact", "extract", ARTIFACT_SINGLE, path)
Expect(readFileToString(path)).To(Equal(artifactContent))
tests := []struct {
name string
filename string
extraArgs []string
}{
{
name: "extract to dir",
filename: artifactTitle,
},
{
name: "extract to dir by digest",
filename: digestToFilename(artifactDigest),
extraArgs: []string{"--digest", artifactDigest},
},
{
name: "extract to dir by title",
filename: artifactTitle,
extraArgs: []string{"--title", artifactTitle},
},
}
for _, tt := range tests {
By(tt.name)
dir := makeTempDirInDir(podmanTest.TempDir)
args := append([]string{"artifact", "extract"}, tt.extraArgs...)
args = append(args, ARTIFACT_SINGLE, dir)
podmanTest.PodmanExitCleanly(args...)
Expect(readFileToString(filepath.Join(dir, tt.filename))).To(Equal(artifactContent))
}
// invalid digest
session := podmanTest.Podman([]string{"artifact", "extract", "--digest", "blah", ARTIFACT_SINGLE, podmanTest.TempDir})
session.WaitWithDefaultTimeout()
Expect(session).To(ExitWithError(125, `no blob with the digest "blah"`))
// invalid title
session = podmanTest.Podman([]string{"artifact", "extract", "--title", "abcd", ARTIFACT_SINGLE, podmanTest.TempDir})
session.WaitWithDefaultTimeout()
Expect(session).To(ExitWithError(125, `no blob with the title "abcd"`))
})
It("podman artifact extract multi", func() {
podmanTest.PodmanExitCleanly("artifact", "pull", ARTIFACT_MULTI)
podmanTest.PodmanExitCleanly("artifact", "pull", ARTIFACT_MULTI_NO_TITLE)
const (
artifactContent1 = "xuHWedtC0ADST\n"
artifactDigest1 = "sha256:8257bba28b9d19ac353c4b713b470860278857767935ef7e139afd596cb1bb2d"
artifactTitle1 = "test1"
artifactContent2 = "tAyZczFlgFsi4\n"
artifactDigest2 = "sha256:63700c54129c6daaafe3a20850079f82d6d658d69de73d6158d81f920c6fbdd7"
artifactTitle2 = "test2"
)
type expect struct {
filename string
content string
}
tests := []struct {
name string
image string
extraArgs []string
expect []expect
}{
{
name: "extract multi blob to dir",
image: ARTIFACT_MULTI,
expect: []expect{
{filename: artifactTitle1, content: artifactContent1},
{filename: artifactTitle2, content: artifactContent2},
},
},
{
name: "extract multi blob to dir without title",
image: ARTIFACT_MULTI_NO_TITLE,
expect: []expect{
{filename: digestToFilename(artifactDigest1), content: artifactContent1},
{filename: digestToFilename(artifactDigest2), content: artifactContent2},
},
},
{
name: "extract multi blob to dir with --title",
image: ARTIFACT_MULTI,
extraArgs: []string{"--title", artifactTitle1},
expect: []expect{
{filename: artifactTitle1, content: artifactContent1},
},
},
{
name: "extract multi blob to dir with --digest",
image: ARTIFACT_MULTI,
extraArgs: []string{"--digest", artifactDigest2},
expect: []expect{
{filename: digestToFilename(artifactDigest2), content: artifactContent2},
},
},
}
for _, tt := range tests {
By(tt.name)
dir := makeTempDirInDir(podmanTest.TempDir)
args := append([]string{"artifact", "extract"}, tt.extraArgs...)
args = append(args, tt.image, dir)
podmanTest.PodmanExitCleanly(args...)
files, err := os.ReadDir(dir)
Expect(err).ToNot(HaveOccurred())
Expect(files).To(HaveLen(len(tt.expect)))
for _, expect := range tt.expect {
Expect(readFileToString(filepath.Join(dir, expect.filename))).To(Equal(expect.content))
}
}
})
It("podman artifact extract evil", func() {
path := filepath.Join(podmanTest.TempDir, "testfile")
podmanTest.PodmanExitCleanly("artifact", "pull", ARTIFACT_EVIL)
const (
artifactContent = "RM5eA27F9psa2\n"
artifactDigest = "sha256:4c29da41ff27fcbf273653bcfba58ed69efa4aefec7b6c486262711cb1dfd050"
)
// Extract to file is fine as we are not using the malicious title
podmanTest.PodmanExitCleanly("artifact", "extract", ARTIFACT_EVIL, path)
Expect(readFileToString(path)).To(Equal(artifactContent))
// This must fail for security reasons we do not allow a title with /
session := podmanTest.Podman([]string{"artifact", "extract", ARTIFACT_EVIL, podmanTest.TempDir})
session.WaitWithDefaultTimeout()
Expect(session).To(ExitWithError(125, `invalid name: "../../../../tmp/evil" cannot contain /`))
// Extracting by digest should be fine too
podmanTest.PodmanExitCleanly("artifact", "extract", "--digest", artifactDigest, ARTIFACT_EVIL, podmanTest.TempDir)
Expect(readFileToString(filepath.Join(podmanTest.TempDir, digestToFilename(artifactDigest)))).To(Equal(artifactContent))
})
})
func digestToFilename(digest string) string {
return strings.ReplaceAll(digest, ":", "-")
}
func readFileToString(path string) string {
GinkgoHelper()
b, err := os.ReadFile(path)
Expect(err).ToNot(HaveOccurred())
return string(b)
}