-
Notifications
You must be signed in to change notification settings - Fork 46
libartifact: strong type where possible #463
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,4 @@ | ||
| package libartifact | ||
| package store | ||
|
|
||
| import ( | ||
| "encoding/json" | ||
|
|
@@ -7,6 +7,7 @@ import ( | |
|
|
||
| "github.com/opencontainers/go-digest" | ||
| "go.podman.io/common/pkg/libartifact/types" | ||
| "go.podman.io/image/v5/docker/reference" | ||
| "go.podman.io/image/v5/manifest" | ||
| ) | ||
|
|
||
|
|
@@ -36,7 +37,7 @@ func (a *Artifact) GetName() (string, error) { | |
| return "", types.ErrArtifactUnamed | ||
| } | ||
|
|
||
| // SetName is a accessor for setting the artifact name | ||
| // SetName is an accessor for setting the artifact name | ||
| // Note: long term this may not be needed, and we would | ||
| // be comfortable with simply using the exported field | ||
| // called Name. | ||
|
|
@@ -55,16 +56,16 @@ func (a *Artifact) GetDigest() (*digest.Digest, error) { | |
|
|
||
| type ArtifactList []*Artifact | ||
|
|
||
| // GetByNameOrDigest returns an artifact, if present, by a given name | ||
| // getByNameOrDigest returns an artifact, if present, by a given name | ||
| // Returns an error if not found. | ||
| func (al ArtifactList) GetByNameOrDigest(nameOrDigest string) (*Artifact, bool, error) { | ||
| func (al ArtifactList) getByNameOrDigest(nameOrDigest string) (*Artifact, bool, error) { | ||
| // This is the hot route through | ||
| for _, artifact := range al { | ||
| if artifact.Name == nameOrDigest { | ||
| return artifact, false, nil | ||
| } | ||
| } | ||
| // Before giving up, check by digest | ||
| // Before giving up, check by full or partial ID | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Probably unrelated: The “partial ID” syntax does not work well for multiple digest algorithms. “ID” references are only sort-of-required if it is possible to create an unnamed object, or pull and untag an object. Maybe we can drop the concept?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I dont really love it either but "people" screamed about consistency with images and other objects. I doubt we can drop the concept.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I hate partial IDs, a digest cannot be partial it is only valid in its full form. Script should be using full ids always and for users they can use the name+tag or just press tab for shell completion.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How about supporting ID prefixes (or even all IDs) only for sha256 digests (and sha256-shaped c/storage IDs)? That would preserve all existing code, but force users who want sha512 (or any future digest) to migrate. If we could establish that as a policy for both artifacts and images, that would set clear user expectations — and probably fairly simplify the sha512 migration. |
||
| for _, artifact := range al { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In this loop, the function returns some entry matching the digest; there may be multiple. [MULTI], see elsewhere. |
||
| artifactDigest, err := artifact.GetDigest() | ||
| if err != nil { | ||
|
|
@@ -75,5 +76,30 @@ func (al ArtifactList) GetByNameOrDigest(nameOrDigest string) (*Artifact, bool, | |
| return artifact, true, nil | ||
| } | ||
| } | ||
| named, err := reference.ParseNamed(nameOrDigest) | ||
| if err != nil { | ||
| return nil, false, fmt.Errorf("invalid artifact: %q", nameOrDigest) | ||
| } | ||
|
|
||
| // And finally, check for things with a name and digest | ||
| // i.e. quay.io/podman/machine-os:sha256:7e952f1deece2717022d7cc066dd21d1468236560d23a79c80448d49b2048e99 | ||
| if d, isDigested := named.(reference.Digested); isDigested { | ||
| for _, a := range al { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [MULTI], see elsewhere. |
||
| storedArtifactNamed, err := reference.ParseNamed(a.Name) | ||
| if err != nil { | ||
| return nil, false, err | ||
| } | ||
| if storedArtifactNamed.Name() == named.Name() { | ||
| artifactDigest, err := a.GetDigest() | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This will break for sha256 vs. sha512 references to the same object — but hopefully it can be implemented by re-digesting the original manifest blob using the |
||
| if err != nil { | ||
| return nil, false, err | ||
| } | ||
| if d.Digest() == *artifactDigest { | ||
| return a, true, nil | ||
| } | ||
| } | ||
| } | ||
| } | ||
| // Nothing was found in the store that matches | ||
| return nil, false, fmt.Errorf("%s: %w", nameOrDigest, types.ErrArtifactNotExist) | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,100 @@ | ||
| //go:build !remote | ||
|
|
||
| package store | ||
|
|
||
| import ( | ||
| "context" | ||
|
|
||
| "go.podman.io/image/v5/docker/reference" | ||
| ) | ||
|
|
||
| type ArtifactReference struct { | ||
| reference.Named | ||
baude marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
|
|
||
| // NewArtifactReference is a theoretical reference to an artifact. It needs to be | ||
| // a fully qualified oci reference except for tag, where we add | ||
| // "latest" as the tag if tag is empty. Valid references: | ||
| // | ||
| // quay.io/podman/machine-os:latest | ||
| // quay.io/podman/machine-os | ||
| // quay.io/podman/machine-os@sha256:916ede4b2b9012f91f63100f8ba82d07ed81bf8a55d23c1503285a22a9759a1e | ||
| // | ||
| // Note: Partial sha references and digests (IDs) are not allowed. | ||
| func NewArtifactReference(input string) (ArtifactReference, error) { | ||
baude marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| ar := ArtifactReference{} | ||
| named, err := stringToNamed(input) | ||
| if err != nil { | ||
| return ArtifactReference{}, err | ||
| } | ||
| ar.Named = named | ||
| return ar, nil | ||
| } | ||
|
|
||
| func (ar ArtifactReference) IsDigested() bool { | ||
This comment was marked as resolved.
Sorry, something went wrong.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The way Go works, it is generally better to define interfaces at callers than callees, unless the callee is really an abstraction over multiple implementations.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks, I didn't realize that and learned something new.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oh, and:
|
||
| _, isDigested := ar.Named.(reference.Digested) | ||
| return isDigested | ||
| } | ||
|
|
||
| type ArtifactStoreReference struct { | ||
baude marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| ArtifactFromStore *Artifact | ||
| IsDigested bool | ||
baude marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| Ref reference.Named | ||
baude marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
|
|
||
| // NewArtifactStorageReference refers to an object already in the artifact store. It | ||
| // can be a name or a full or partial digest. Conveniently, it also embeds the artifact | ||
| // as part of its return. | ||
| func NewArtifactStorageReference(nameOrDigest string, as *ArtifactStore) (ArtifactStoreReference, error) { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it would be good to mention that these type of functions require the caller to hold a lock. |
||
| lookupInput := nameOrDigest | ||
| asf := ArtifactStoreReference{} | ||
| al, err := as.getArtifacts(context.Background(), nil) | ||
| if err != nil { | ||
| return ArtifactStoreReference{}, err | ||
| } | ||
|
|
||
| // Try to parse as a valid OCI reference | ||
| named, parseErr := stringToNamed(nameOrDigest) | ||
| if parseErr == nil { | ||
| lookupInput = named.String() | ||
| } | ||
|
|
||
| // Lookup in the store | ||
| a, isDigest, err := al.getByNameOrDigest(lookupInput) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. By this time, the But I’m not saying it doesn’t work, but it feels wasteful (doesn’t really matter) and hard to follow (what I worry about). |
||
| if err != nil { | ||
| return ArtifactStoreReference{}, err | ||
| } | ||
|
|
||
| // If parsing failed, parse the artifact's name instead | ||
| if parseErr != nil { | ||
| fqName, err := a.GetName() | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [MULTI], see elsewhere. … this assigns some of possibly several names matching the digest to the returned value. And then the name is used as the primary lookup key, e.g. in That does something, but it’s not clear that it’s what the user wanted. Again, can we entirely delete the “ID” lookup case? Or at least have it fail if there are multiple matching objects? Or would we want to have it fail only in some situations? … but this also applies to digest lookups: |
||
| if err != nil { | ||
| return ArtifactStoreReference{}, err | ||
| } | ||
| named, err = stringToNamed(fqName) | ||
| if err != nil { | ||
| return ArtifactStoreReference{}, err | ||
| } | ||
| } | ||
|
|
||
| asf.Ref = named | ||
| asf.IsDigested = isDigest | ||
| asf.ArtifactFromStore = a | ||
| return asf, nil | ||
| } | ||
|
|
||
| // stringToNamed converts a string to a reference.Named. | ||
| func stringToNamed(s string) (reference.Named, error) { | ||
| named, err := reference.ParseNamed(s) | ||
| if err != nil { | ||
| return ArtifactReference{}, err | ||
| } | ||
| // If the supplied input is neither tagged nor has | ||
| // a digest, then add "latest" | ||
| _, isTagged := named.(reference.Tagged) | ||
| _, isDigested := named.(reference.Digested) | ||
| if !isTagged && !isDigested { | ||
| named = reference.TagNameOnly(named) | ||
| } | ||
|
Comment on lines
+92
to
+98
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This can just be |
||
| return named, nil | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,186 @@ | ||
| package store | ||
|
|
||
| import ( | ||
| "fmt" | ||
| "os" | ||
| "path/filepath" | ||
| "testing" | ||
|
|
||
| "github.com/stretchr/testify/assert" | ||
| "github.com/stretchr/testify/require" | ||
| "go.podman.io/image/v5/types" | ||
| ) | ||
|
|
||
| func TestNewArtifactReference(t *testing.T) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. (Consider using a table-driven test.)
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. do you have a pointer to something you hold in high regard ?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. (This is a non-blocking comment either way. Having verbose tests is better than not having tests.) I don’t care that much about the structure… the valuable part is avoiding repetition in the common parts.
|
||
| // Test valid reference | ||
| ar, err := NewArtifactReference("quay.io/podman/machine-os:5.1") | ||
| assert.NoError(t, err) | ||
| assert.NotNil(t, ar.Named) | ||
| assert.Equal(t, "quay.io/podman/machine-os:5.1", ar.Named.String()) | ||
|
|
||
| // Test another valid reference | ||
| ar, err = NewArtifactReference("docker.io/library/nginx:latest") | ||
| assert.NoError(t, err) | ||
| assert.NotNil(t, ar.Named) | ||
|
|
||
baude marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| // Test invalid reference - empty string | ||
| _, err = NewArtifactReference("") | ||
| assert.Error(t, err) | ||
|
|
||
| // Test invalid reference - malformed | ||
| _, err = NewArtifactReference("invalid::reference") | ||
| assert.Error(t, err) | ||
|
|
||
| // Test latest is added when no tag is provided | ||
| ar, err = NewArtifactReference("quay.io/machine-os/podman") | ||
| assert.NoError(t, err) | ||
| assert.Equal(t, "quay.io/machine-os/podman:latest", ar.Named.String()) | ||
|
|
||
| // Input with a digest is good | ||
| ar, err = NewArtifactReference("quay.io/machine-os/podman@sha256:8b96f36deaf1d2713858eebd9ef2fee9610df8452fbd083bbfa7dca66d6fcd0b") | ||
| assert.NoError(t, err) | ||
| assert.True(t, ar.IsDigested()) | ||
|
|
||
| // Partial digests are a no-go | ||
| _, err = NewArtifactReference("quay.io/machine-os/podman@sha256:8b96f36deaf1d2") | ||
| assert.Error(t, err) | ||
|
|
||
| // "IDs" are also a no-go | ||
| _, err = NewArtifactReference("84ddb405470e733d0202d6946e48fc75a7ee231337bdeb31a8579407a7052d9e") | ||
| assert.Error(t, err) | ||
| } | ||
|
|
||
| func TestArtifactReference_IsDigested(t *testing.T) { | ||
| // Test reference with tag (not digested) | ||
| ar, err := NewArtifactReference("quay.io/podman/machine-os:5.1") | ||
| require.NoError(t, err) | ||
| assert.False(t, ar.IsDigested()) | ||
|
|
||
| // Test reference with digest (digested) | ||
| ar, err = NewArtifactReference("quay.io/podman/machine-os@sha256:8b96f36deaf1d2713858eebd9ef2fee9610df8452fbd083bbfa7dca66d6fcd0b") | ||
| require.NoError(t, err) | ||
| assert.True(t, ar.IsDigested()) | ||
|
|
||
| // Test reference with latest tag (not digested) | ||
| ar, err = NewArtifactReference("quay.io/podman/machine-os:latest") | ||
| require.NoError(t, err) | ||
| assert.False(t, ar.IsDigested()) | ||
| } | ||
|
|
||
| func TestNewArtifactStorageReference_ValidReference(t *testing.T) { | ||
| repo := "quay.io/podman/machine-os" | ||
| tag := "5.1" | ||
| ref := fmt.Sprintf("%s:%s", repo, tag) | ||
| as, artifactDigest := setupNewStore(t, ref, nil, nil) | ||
|
|
||
| // Test with a valid named reference - should find the artifact in the store | ||
| asr, err := NewArtifactStorageReference(ref, as) | ||
| assert.NoError(t, err) | ||
| assert.NotNil(t, asr.Ref) | ||
| assert.Equal(t, "quay.io/podman/machine-os:5.1", asr.Ref.String()) | ||
| assert.False(t, asr.IsDigested) | ||
| assert.NotNil(t, asr.ArtifactFromStore) | ||
| assert.Equal(t, "quay.io/podman/machine-os:5.1", asr.ArtifactFromStore.Name) | ||
|
|
||
| // Lookup by Digest | ||
| asr, err = NewArtifactStorageReference(fmt.Sprintf("%s@%s", repo, artifactDigest.String()), as) | ||
| assert.NoError(t, err) | ||
| assert.NotNil(t, asr.ArtifactFromStore) | ||
| assert.True(t, asr.IsDigested) | ||
| assert.NotNil(t, asr.ArtifactFromStore) | ||
| } | ||
|
|
||
| func TestNewArtifactStorageReference_AutoTagLatest(t *testing.T) { | ||
| repoNameOnly := "quay.io/podman/machine-os" | ||
| as, _ := setupNewStore(t, repoNameOnly, nil, nil) | ||
baude marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| // Test with a reference without a tag (should auto-add :latest) | ||
| asr, err := NewArtifactStorageReference(repoNameOnly, as) | ||
| assert.NoError(t, err) | ||
| assert.NotNil(t, asr.Ref) | ||
| assert.Equal(t, fmt.Sprintf("%s:latest", repoNameOnly), asr.Ref.String()) | ||
| assert.False(t, asr.IsDigested) | ||
| } | ||
|
|
||
| func TestNewArtifactStorageReference_InvalidReference(t *testing.T) { | ||
| storePath := filepath.Join(t.TempDir(), "store") | ||
| sc := &types.SystemContext{} | ||
|
|
||
| // Create an artifact store | ||
| as, err := NewArtifactStore(storePath, sc) | ||
| require.NoError(t, err) | ||
| require.NotNil(t, as) | ||
|
|
||
| // Test with an invalid reference that also doesn't exist in the store | ||
| // This should fail both as a reference parse and as a store lookup | ||
| _, err = NewArtifactStorageReference("nonexistent-digest-12345", as) | ||
| assert.Error(t, err) | ||
| } | ||
|
|
||
| func TestNewArtifactStorageReference_EmptyString(t *testing.T) { | ||
| storePath := filepath.Join(t.TempDir(), "store") | ||
| sc := &types.SystemContext{} | ||
|
|
||
| // Create an artifact store | ||
| as, err := NewArtifactStore(storePath, sc) | ||
| require.NoError(t, err) | ||
| require.NotNil(t, as) | ||
|
|
||
| // Test with empty string | ||
| _, err = NewArtifactStorageReference("", as) | ||
| assert.Error(t, err) | ||
| } | ||
|
|
||
| func TestStringToNamed(t *testing.T) { | ||
| // Test valid named reference | ||
| named, err := stringToNamed("quay.io/podman/machine-os:5.1") | ||
| assert.NoError(t, err) | ||
| assert.NotNil(t, named) | ||
| assert.Equal(t, "quay.io/podman/machine-os:5.1", named.String()) | ||
|
|
||
| // Test reference without tag (should add :latest) | ||
| named, err = stringToNamed("quay.io/podman/machine-os") | ||
| assert.NoError(t, err) | ||
| assert.NotNil(t, named) | ||
| assert.Equal(t, "quay.io/podman/machine-os:latest", named.String()) | ||
|
|
||
| // Test reference with digest | ||
| named, err = stringToNamed("quay.io/podman/machine-os@sha256:8b96f36deaf1d2713858eebd9ef2fee9610df8452fbd083bbfa7dca66d6fcd0b") | ||
| assert.NoError(t, err) | ||
| assert.NotNil(t, named) | ||
| assert.Equal(t, "quay.io/podman/machine-os@sha256:8b96f36deaf1d2713858eebd9ef2fee9610df8452fbd083bbfa7dca66d6fcd0b", named.String()) | ||
|
|
||
| // Test invalid reference | ||
| _, err = stringToNamed("invalid::reference") | ||
| assert.Error(t, err) | ||
|
|
||
| // Test empty string | ||
| _, err = stringToNamed("") | ||
| assert.Error(t, err) | ||
| } | ||
|
|
||
| func TestNewArtifactStore(t *testing.T) { | ||
| // Test with valid absolute path | ||
| storePath := filepath.Join(t.TempDir(), "store") | ||
| sc := &types.SystemContext{} | ||
|
|
||
| as, err := NewArtifactStore(storePath, sc) | ||
| assert.NoError(t, err) | ||
| assert.NotNil(t, as) | ||
| assert.Equal(t, storePath, as.storePath) | ||
|
|
||
| // Verify the index file was created | ||
| indexPath := filepath.Join(storePath, "index.json") | ||
| _, err = os.Stat(indexPath) | ||
| assert.NoError(t, err) | ||
|
|
||
| // Test with empty path | ||
| _, err = NewArtifactStore("", sc) | ||
| assert.Error(t, err) | ||
| assert.Contains(t, err.Error(), "store path cannot be empty") | ||
|
|
||
| // Test with relative path | ||
| _, err = NewArtifactStore("relative/path", sc) | ||
| assert.Error(t, err) | ||
| assert.Contains(t, err.Error(), "must be absolute") | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see there is a lot of iteration over lists. In extreme cases, when the store has a lot of artifacts, this will cost us some time. I think there could be an optimization of this search.