Skip to content

Commit 331ddbd

Browse files
committed
libartifact: strong type where possible
introduction of ArtifactReference and ArtifactStorageReference types for libartifact. ArtifactReference is a theoretical reference to an artifact like for things like pull. ArtifactStorageReference is for looking things up in the existing store for things like rm or inspect. ArtifactStoreReference allows for things like full or partial "id" lookups. the pr also accomdates a user ask for appending "latest" as a tag when no tag is provided. and finally, here we introduce a bunch of artifact tests to get the ball rolling. it should not be considered fully complete but a good start. Signed-off-by: Brent Baude <[email protected]>
1 parent 36964d1 commit 331ddbd

File tree

11 files changed

+1609
-107
lines changed

11 files changed

+1609
-107
lines changed

common/go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ require (
4545
go.etcd.io/bbolt v1.4.3
4646
go.podman.io/image/v5 v5.38.0
4747
go.podman.io/storage v1.61.0
48+
go.step.sm/crypto v0.57.0
4849
golang.org/x/crypto v0.43.0
4950
golang.org/x/sync v0.18.0
5051
golang.org/x/sys v0.38.0

common/go.sum

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,8 @@ github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ
254254
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
255255
github.com/skeema/knownhosts v1.3.2 h1:EDL9mgf4NzwMXCTfaxSD/o/a5fxDw/xL9nkU28JjdBg=
256256
github.com/skeema/knownhosts v1.3.2/go.mod h1:bEg3iQAuw+jyiw+484wwFJoKSLwcfd7fqRy+N0QTiow=
257+
github.com/smallstep/assert v0.0.0-20200723003110-82e2b9b3b262 h1:unQFBIznI+VYD1/1fApl1A+9VcBk+9dcqGfnePY87LY=
258+
github.com/smallstep/assert v0.0.0-20200723003110-82e2b9b3b262/go.mod h1:MyOHs9Po2fbM1LHej6sBUT8ozbxmMOFG+E+rx/GSGuc=
257259
github.com/smallstep/pkcs7 v0.1.1 h1:x+rPdt2W088V9Vkjho4KtoggyktZJlMduZAtRHm68LU=
258260
github.com/smallstep/pkcs7 v0.1.1/go.mod h1:dL6j5AIz9GHjVEBTXtW+QliALcgM19RtXaTeyxI+AfA=
259261
github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s=
@@ -325,6 +327,8 @@ go.podman.io/image/v5 v5.38.0 h1:aUKrCANkPvze1bnhLJsaubcfz0d9v/bSDLnwsXJm6G4=
325327
go.podman.io/image/v5 v5.38.0/go.mod h1:hSIoIUzgBnmc4DjoIdzk63aloqVbD7QXDMkSE/cvG90=
326328
go.podman.io/storage v1.61.0 h1:5hD/oyRYt1f1gxgvect+8syZBQhGhV28dCw2+CZpx0Q=
327329
go.podman.io/storage v1.61.0/go.mod h1:A3UBK0XypjNZ6pghRhuxg62+2NIm5lcUGv/7XyMhMUI=
330+
go.step.sm/crypto v0.57.0 h1:YjoRQDaJYAxHLVwjst0Bl0xcnoKzVwuHCJtEo2VSHYU=
331+
go.step.sm/crypto v0.57.0/go.mod h1:+Lwp5gOVPaTa3H/Ul/TzGbxQPXZZcKIUGMS0lG6n9Go=
328332
go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
329333
go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
330334
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=

common/pkg/libartifact/artifact.go renamed to common/pkg/libartifact/store/artifact.go

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package libartifact
1+
package store
22

33
import (
44
"encoding/json"
@@ -7,6 +7,7 @@ import (
77

88
"github.com/opencontainers/go-digest"
99
"go.podman.io/common/pkg/libartifact/types"
10+
"go.podman.io/image/v5/docker/reference"
1011
"go.podman.io/image/v5/manifest"
1112
)
1213

@@ -36,7 +37,7 @@ func (a *Artifact) GetName() (string, error) {
3637
return "", types.ErrArtifactUnamed
3738
}
3839

39-
// SetName is a accessor for setting the artifact name
40+
// SetName is an accessor for setting the artifact name
4041
// Note: long term this may not be needed, and we would
4142
// be comfortable with simply using the exported field
4243
// called Name.
@@ -55,16 +56,16 @@ func (a *Artifact) GetDigest() (*digest.Digest, error) {
5556

5657
type ArtifactList []*Artifact
5758

58-
// GetByNameOrDigest returns an artifact, if present, by a given name
59+
// getByNameOrDigest returns an artifact, if present, by a given name
5960
// Returns an error if not found.
60-
func (al ArtifactList) GetByNameOrDigest(nameOrDigest string) (*Artifact, bool, error) {
61+
func (al ArtifactList) getByNameOrDigest(nameOrDigest string) (*Artifact, bool, error) {
6162
// This is the hot route through
6263
for _, artifact := range al {
6364
if artifact.Name == nameOrDigest {
6465
return artifact, false, nil
6566
}
6667
}
67-
// Before giving up, check by digest
68+
// Before giving up, check by full or partial ID
6869
for _, artifact := range al {
6970
artifactDigest, err := artifact.GetDigest()
7071
if err != nil {
@@ -75,5 +76,30 @@ func (al ArtifactList) GetByNameOrDigest(nameOrDigest string) (*Artifact, bool,
7576
return artifact, true, nil
7677
}
7778
}
79+
named, err := reference.ParseNamed(nameOrDigest)
80+
if err != nil {
81+
return nil, false, fmt.Errorf("invalid artifact: %q", nameOrDigest)
82+
}
83+
84+
// And finally, check for things with a name and digest
85+
// i.e. quay.io/podman/machine-os:sha256:7e952f1deece2717022d7cc066dd21d1468236560d23a79c80448d49b2048e99
86+
if d, isDigested := named.(reference.Digested); isDigested {
87+
for _, a := range al {
88+
storedArtifactNamed, err := reference.ParseNamed(a.Name)
89+
if err != nil {
90+
return nil, false, err
91+
}
92+
if storedArtifactNamed.Name() == named.Name() {
93+
artifactDigest, err := a.GetDigest()
94+
if err != nil {
95+
return nil, false, err
96+
}
97+
if d.Digest() == *artifactDigest {
98+
return a, true, nil
99+
}
100+
}
101+
}
102+
}
103+
// Nothing was found in the store that matches
78104
return nil, false, fmt.Errorf("%s: %w", nameOrDigest, types.ErrArtifactNotExist)
79105
}
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
//go:build !remote
2+
3+
package store
4+
5+
import (
6+
"context"
7+
8+
"go.podman.io/image/v5/docker/reference"
9+
)
10+
11+
type ArtifactReference struct {
12+
reference.Named
13+
}
14+
15+
// NewArtifactReference is a theoretical reference to an artifact. It needs to be
16+
// a fully qualified oci reference except for tag, where we add
17+
// "latest" as the tag if tag is empty. Valid references:
18+
//
19+
// quay.io/podman/machine-os:latest
20+
// quay.io/podman/machine-os
21+
// quay.io/podman/machine-os@sha256:916ede4b2b9012f91f63100f8ba82d07ed81bf8a55d23c1503285a22a9759a1e
22+
//
23+
// Note: Partial sha references and digests (IDs) are not allowed.
24+
func NewArtifactReference(input string) (ArtifactReference, error) {
25+
ar := ArtifactReference{}
26+
named, err := stringToNamed(input)
27+
if err != nil {
28+
return ArtifactReference{}, err
29+
}
30+
ar.Named = named
31+
return ar, nil
32+
}
33+
34+
func (ar ArtifactReference) IsDigested() bool {
35+
_, isDigested := ar.Named.(reference.Digested)
36+
return isDigested
37+
}
38+
39+
type ArtifactStoreReference struct {
40+
ArtifactFromStore *Artifact
41+
IsDigested bool
42+
Ref reference.Named
43+
}
44+
45+
// NewArtifactStorageReference refers to an object already in the artifact store. It
46+
// can be a name or a full or partial digest. Conveniently, it also embeds the artifact
47+
// as part of its return.
48+
func NewArtifactStorageReference(nameOrDigest string, as *ArtifactStore) (ArtifactStoreReference, error) {
49+
lookupInput := nameOrDigest
50+
asf := ArtifactStoreReference{}
51+
al, err := as.getArtifacts(context.Background(), nil)
52+
if err != nil {
53+
return ArtifactStoreReference{}, err
54+
}
55+
56+
// Try to parse as a valid OCI reference
57+
named, parseErr := stringToNamed(nameOrDigest)
58+
if parseErr == nil {
59+
lookupInput = named.String()
60+
}
61+
62+
// Lookup in the store
63+
a, isDigest, err := al.getByNameOrDigest(lookupInput)
64+
if err != nil {
65+
return ArtifactStoreReference{}, err
66+
}
67+
68+
// If parsing failed, parse the artifact's name instead
69+
if parseErr != nil {
70+
fqName, err := a.GetName()
71+
if err != nil {
72+
return ArtifactStoreReference{}, err
73+
}
74+
named, err = stringToNamed(fqName)
75+
if err != nil {
76+
return ArtifactStoreReference{}, err
77+
}
78+
}
79+
80+
asf.Ref = named
81+
asf.IsDigested = isDigest
82+
asf.ArtifactFromStore = a
83+
return asf, nil
84+
}
85+
86+
// stringToNamed converts a string to a reference.Named.
87+
func stringToNamed(s string) (reference.Named, error) {
88+
named, err := reference.ParseNamed(s)
89+
if err != nil {
90+
return ArtifactReference{}, err
91+
}
92+
// If the supplied input is neither tagged nor has
93+
// a digest, then add "latest"
94+
_, isTagged := named.(reference.Tagged)
95+
_, isDigested := named.(reference.Digested)
96+
if !isTagged && !isDigested {
97+
named = reference.TagNameOnly(named)
98+
}
99+
return named, nil
100+
}
Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
package store
2+
3+
import (
4+
"fmt"
5+
"os"
6+
"path/filepath"
7+
"testing"
8+
9+
"github.com/stretchr/testify/assert"
10+
"github.com/stretchr/testify/require"
11+
"go.podman.io/image/v5/types"
12+
)
13+
14+
func TestNewArtifactReference(t *testing.T) {
15+
// Test valid reference
16+
ar, err := NewArtifactReference("quay.io/podman/machine-os:5.1")
17+
assert.NoError(t, err)
18+
assert.NotNil(t, ar.Named)
19+
assert.Equal(t, "quay.io/podman/machine-os:5.1", ar.Named.String())
20+
21+
// Test another valid reference
22+
ar, err = NewArtifactReference("docker.io/library/nginx:latest")
23+
assert.NoError(t, err)
24+
assert.NotNil(t, ar.Named)
25+
26+
// Test invalid reference - empty string
27+
_, err = NewArtifactReference("")
28+
assert.Error(t, err)
29+
30+
// Test invalid reference - malformed
31+
_, err = NewArtifactReference("invalid::reference")
32+
assert.Error(t, err)
33+
34+
// Test latest is added when no tag is provided
35+
ar, err = NewArtifactReference("quay.io/machine-os/podman")
36+
assert.NoError(t, err)
37+
assert.Equal(t, "quay.io/machine-os/podman:latest", ar.Named.String())
38+
39+
// Input with a digest is good
40+
ar, err = NewArtifactReference("quay.io/machine-os/podman@sha256:8b96f36deaf1d2713858eebd9ef2fee9610df8452fbd083bbfa7dca66d6fcd0b")
41+
assert.NoError(t, err)
42+
assert.True(t, ar.IsDigested())
43+
44+
// Partial digests are a no-go
45+
_, err = NewArtifactReference("quay.io/machine-os/podman@sha256:8b96f36deaf1d2")
46+
assert.Error(t, err)
47+
48+
// "IDs" are also a no-go
49+
_, err = NewArtifactReference("84ddb405470e733d0202d6946e48fc75a7ee231337bdeb31a8579407a7052d9e")
50+
assert.Error(t, err)
51+
}
52+
53+
func TestArtifactReference_IsDigested(t *testing.T) {
54+
// Test reference with tag (not digested)
55+
ar, err := NewArtifactReference("quay.io/podman/machine-os:5.1")
56+
require.NoError(t, err)
57+
assert.False(t, ar.IsDigested())
58+
59+
// Test reference with digest (digested)
60+
ar, err = NewArtifactReference("quay.io/podman/machine-os@sha256:8b96f36deaf1d2713858eebd9ef2fee9610df8452fbd083bbfa7dca66d6fcd0b")
61+
require.NoError(t, err)
62+
assert.True(t, ar.IsDigested())
63+
64+
// Test reference with latest tag (not digested)
65+
ar, err = NewArtifactReference("quay.io/podman/machine-os:latest")
66+
require.NoError(t, err)
67+
assert.False(t, ar.IsDigested())
68+
}
69+
70+
func TestNewArtifactStorageReference_ValidReference(t *testing.T) {
71+
repo := "quay.io/podman/machine-os"
72+
tag := "5.1"
73+
ref := fmt.Sprintf("%s:%s", repo, tag)
74+
as, artifactDigest := setupNewStore(t, ref, nil, nil)
75+
76+
// Test with a valid named reference - should find the artifact in the store
77+
asr, err := NewArtifactStorageReference(ref, as)
78+
assert.NoError(t, err)
79+
assert.NotNil(t, asr.Ref)
80+
assert.Equal(t, "quay.io/podman/machine-os:5.1", asr.Ref.String())
81+
assert.False(t, asr.IsDigested)
82+
assert.NotNil(t, asr.ArtifactFromStore)
83+
assert.Equal(t, "quay.io/podman/machine-os:5.1", asr.ArtifactFromStore.Name)
84+
85+
// Lookup by Digest
86+
asr, err = NewArtifactStorageReference(fmt.Sprintf("%s@%s", repo, artifactDigest.String()), as)
87+
assert.NoError(t, err)
88+
assert.NotNil(t, asr.ArtifactFromStore)
89+
assert.True(t, asr.IsDigested)
90+
assert.NotNil(t, asr.ArtifactFromStore)
91+
}
92+
93+
func TestNewArtifactStorageReference_AutoTagLatest(t *testing.T) {
94+
repoNameOnly := "quay.io/podman/machine-os"
95+
as, _ := setupNewStore(t, repoNameOnly, nil, nil)
96+
97+
// Test with a reference without a tag (should auto-add :latest)
98+
asr, err := NewArtifactStorageReference(repoNameOnly, as)
99+
assert.NoError(t, err)
100+
assert.NotNil(t, asr.Ref)
101+
assert.Equal(t, fmt.Sprintf("%s:latest", repoNameOnly), asr.Ref.String())
102+
assert.False(t, asr.IsDigested)
103+
}
104+
105+
func TestNewArtifactStorageReference_InvalidReference(t *testing.T) {
106+
storePath := filepath.Join(t.TempDir(), "store")
107+
sc := &types.SystemContext{}
108+
109+
// Create an artifact store
110+
as, err := NewArtifactStore(storePath, sc)
111+
require.NoError(t, err)
112+
require.NotNil(t, as)
113+
114+
// Test with an invalid reference that also doesn't exist in the store
115+
// This should fail both as a reference parse and as a store lookup
116+
_, err = NewArtifactStorageReference("nonexistent-digest-12345", as)
117+
assert.Error(t, err)
118+
}
119+
120+
func TestNewArtifactStorageReference_EmptyString(t *testing.T) {
121+
storePath := filepath.Join(t.TempDir(), "store")
122+
sc := &types.SystemContext{}
123+
124+
// Create an artifact store
125+
as, err := NewArtifactStore(storePath, sc)
126+
require.NoError(t, err)
127+
require.NotNil(t, as)
128+
129+
// Test with empty string
130+
_, err = NewArtifactStorageReference("", as)
131+
assert.Error(t, err)
132+
}
133+
134+
func TestStringToNamed(t *testing.T) {
135+
// Test valid named reference
136+
named, err := stringToNamed("quay.io/podman/machine-os:5.1")
137+
assert.NoError(t, err)
138+
assert.NotNil(t, named)
139+
assert.Equal(t, "quay.io/podman/machine-os:5.1", named.String())
140+
141+
// Test reference without tag (should add :latest)
142+
named, err = stringToNamed("quay.io/podman/machine-os")
143+
assert.NoError(t, err)
144+
assert.NotNil(t, named)
145+
assert.Equal(t, "quay.io/podman/machine-os:latest", named.String())
146+
147+
// Test reference with digest
148+
named, err = stringToNamed("quay.io/podman/machine-os@sha256:8b96f36deaf1d2713858eebd9ef2fee9610df8452fbd083bbfa7dca66d6fcd0b")
149+
assert.NoError(t, err)
150+
assert.NotNil(t, named)
151+
assert.Equal(t, "quay.io/podman/machine-os@sha256:8b96f36deaf1d2713858eebd9ef2fee9610df8452fbd083bbfa7dca66d6fcd0b", named.String())
152+
153+
// Test invalid reference
154+
_, err = stringToNamed("invalid::reference")
155+
assert.Error(t, err)
156+
157+
// Test empty string
158+
_, err = stringToNamed("")
159+
assert.Error(t, err)
160+
}
161+
162+
func TestNewArtifactStore(t *testing.T) {
163+
// Test with valid absolute path
164+
storePath := filepath.Join(t.TempDir(), "store")
165+
sc := &types.SystemContext{}
166+
167+
as, err := NewArtifactStore(storePath, sc)
168+
assert.NoError(t, err)
169+
assert.NotNil(t, as)
170+
assert.Equal(t, storePath, as.storePath)
171+
172+
// Verify the index file was created
173+
indexPath := filepath.Join(storePath, "index.json")
174+
_, err = os.Stat(indexPath)
175+
assert.NoError(t, err)
176+
177+
// Test with empty path
178+
_, err = NewArtifactStore("", sc)
179+
assert.Error(t, err)
180+
assert.Contains(t, err.Error(), "store path cannot be empty")
181+
182+
// Test with relative path
183+
_, err = NewArtifactStore("relative/path", sc)
184+
assert.Error(t, err)
185+
assert.Contains(t, err.Error(), "must be absolute")
186+
}

0 commit comments

Comments
 (0)