diff --git a/app/controlplane/internal/biz/errors.go b/app/controlplane/internal/biz/errors.go index 3094ecf71..b4e850104 100644 --- a/app/controlplane/internal/biz/errors.go +++ b/app/controlplane/internal/biz/errors.go @@ -91,3 +91,19 @@ func (e ErrUnauthorized) Error() string { func IsErrUnauthorized(err error) bool { return errors.As(err, &ErrUnauthorized{}) } + +// A referrer with the same digest points to two different artifact types +// and we require filtering out which one +type ErrAmbiguousReferrer struct { + digest string + // what kinds contain duplicates + kinds []string +} + +func NewErrReferrerAmbiguous(digest string, kinds []string) error { + return ErrAmbiguousReferrer{digest, kinds} +} + +func (e ErrAmbiguousReferrer) Error() string { + return fmt.Sprintf("digest %s present in %d kinds %q", e.digest, len(e.kinds), e.kinds) +} diff --git a/app/controlplane/internal/biz/referrer.go b/app/controlplane/internal/biz/referrer.go index da586aa0a..9c5d4054f 100644 --- a/app/controlplane/internal/biz/referrer.go +++ b/app/controlplane/internal/biz/referrer.go @@ -19,8 +19,10 @@ import ( "bytes" "context" "encoding/json" + "errors" "fmt" "io" + "sort" "time" "github.com/chainloop-dev/chainloop/internal/attestation/renderer/chainloop" @@ -37,27 +39,21 @@ type Referrer struct { Kind string // Wether the item is downloadable from CAS or not Downloadable bool - // points to other digests - References []string + References []*Referrer } // Actual referrer stored in the DB which includes a nested list of storedReferences type StoredReferrer struct { - ID uuid.UUID - Digest string - Kind string - // Wether the item is downloadable from CAS or not - Downloadable bool - CreatedAt *time.Time + *Referrer + ID uuid.UUID + CreatedAt *time.Time // Fully expanded list of 1-level off references References []*StoredReferrer OrgIDs []uuid.UUID } -type ReferrerMap map[string]*Referrer - type ReferrerRepo interface { - Save(ctx context.Context, input ReferrerMap, orgID uuid.UUID) error + Save(ctx context.Context, input []*Referrer, orgID uuid.UUID) error // GetFromRoot returns the referrer identified by the provided content digest, including its first-level references // For example if sha:deadbeef represents an attestation, the result will contain the attestation + materials associated to it // OrgIDs represent an allowList of organizations where the referrers should be looked for @@ -129,6 +125,10 @@ func (s *ReferrerUseCase) GetFromRoot(ctx context.Context, digest string, userID ref, err := s.repo.GetFromRoot(ctx, digest, orgIDs) if err != nil { + if errors.As(err, &ErrAmbiguousReferrer{}) { + return nil, NewErrValidation(fmt.Errorf("please provide the referrer kind: %w", err)) + } + return nil, fmt.Errorf("getting referrer from root: %w", err) } else if ref == nil { return nil, NewErrNotFound("referrer") @@ -142,6 +142,14 @@ const ( referrerGitHeadType = "GIT_HEAD_COMMIT" ) +func newRef(digest, kind string) string { + return fmt.Sprintf("%s-%s", kind, digest) +} + +func (r *Referrer) MapID() string { + return newRef(r.Digest, r.Kind) +} + // ExtractReferrers extracts the referrers from the given attestation // this means // 1 - write an entry for the attestation itself @@ -149,7 +157,7 @@ const ( // 3 - and the subjects (some of them) // 4 - creating link between the attestation and the materials/subjects as needed // see tests for examples -func extractReferrers(att *dsse.Envelope) (ReferrerMap, error) { +func extractReferrers(att *dsse.Envelope) ([]*Referrer, error) { // Calculate the attestation hash jsonAtt, err := json.Marshal(att) if err != nil { @@ -162,16 +170,18 @@ func extractReferrers(att *dsse.Envelope) (ReferrerMap, error) { return nil, fmt.Errorf("calculating attestation hash: %w", err) } - referrers := make(ReferrerMap) + referrersMap := make(map[string]*Referrer) // 1 - Attestation referrer // Add the attestation itself as a referrer to the map without references yet attestationHash := h.String() - referrers[attestationHash] = &Referrer{ + attestationReferrer := &Referrer{ Digest: attestationHash, Kind: referrerAttestationType, Downloadable: true, } + referrersMap[newRef(attestationHash, referrerAttestationType)] = attestationReferrer + // 2 - Predicate that's referenced from the attestation predicate, err := chainloop.ExtractPredicate(att) if err != nil { @@ -189,23 +199,23 @@ func extractReferrers(att *dsse.Envelope) (ReferrerMap, error) { // Create its referrer entry if it doesn't exist yet // the reason it might exist is because you might be attaching the same material twice // i.e the same SBOM twice, in that case we don't want to create a new referrer - // If we are providing different types for the same digest, we should error out - if r, ok := referrers[material.Hash.String()]; ok { - if r.Kind != material.Type { - return nil, fmt.Errorf("material %s has different types: %s and %s", material.Hash.String(), r.Kind, material.Type) - } - + materialRef := newRef(material.Hash.String(), material.Type) + if _, ok := referrersMap[materialRef]; ok { continue } - referrers[material.Hash.String()] = &Referrer{ + referrersMap[materialRef] = &Referrer{ Digest: material.Hash.String(), Kind: material.Type, Downloadable: material.UploadedToCAS, } + materialReferrer := referrersMap[materialRef] + // Add the reference to the attestation - referrers[attestationHash].References = append(referrers[attestationHash].References, material.Hash.String()) + attestationReferrer.References = append(attestationReferrer.References, &Referrer{ + Digest: materialReferrer.Digest, Kind: materialReferrer.Kind, + }) } // 3 - Subject that points to the attestation @@ -215,25 +225,42 @@ func extractReferrers(att *dsse.Envelope) (ReferrerMap, error) { } for _, subject := range statement.Subject { - subjectRef, err := intotoSubjectToReferrer(subject) + subjectReferrer, err := intotoSubjectToReferrer(subject) if err != nil { return nil, fmt.Errorf("transforming subject to referrer: %w", err) } - if subjectRef == nil { + if subjectReferrer == nil { continue } + subjectRef := newRef(subjectReferrer.Digest, subjectReferrer.Kind) + // check if we already have a referrer for this digest and set it otherwise // this is the case for example for git.Head ones - if _, ok := referrers[subjectRef.Digest]; !ok { - referrers[subjectRef.Digest] = subjectRef + if _, ok := referrersMap[subjectRef]; !ok { + referrersMap[subjectRef] = subjectReferrer // add it to the list of of attestation-referenced digests - referrers[attestationHash].References = append(referrers[attestationHash].References, subjectRef.Digest) + attestationReferrer.References = append(attestationReferrer.References, + &Referrer{ + Digest: subjectReferrer.Digest, Kind: subjectReferrer.Kind, + }) } // Update referrer to point to the attestation - referrers[subjectRef.Digest].References = []string{attestationHash} + referrersMap[subjectRef].References = []*Referrer{{Digest: attestationReferrer.Digest, Kind: attestationReferrer.Kind}} + } + + // Return a sorted list of referrers + mapKeys := make([]string, 0, len(referrersMap)) + for k := range referrersMap { + mapKeys = append(mapKeys, k) + } + sort.Strings(mapKeys) + + referrers := make([]*Referrer, 0, len(referrersMap)) + for _, k := range mapKeys { + referrers = append(referrers, referrersMap[k]) } return referrers, nil diff --git a/app/controlplane/internal/biz/referrer_integration_test.go b/app/controlplane/internal/biz/referrer_integration_test.go index 1115b49db..f2d1f9dc2 100644 --- a/app/controlplane/internal/biz/referrer_integration_test.go +++ b/app/controlplane/internal/biz/referrer_integration_test.go @@ -36,42 +36,42 @@ func (s *referrerIntegrationTestSuite) TestExtractAndPersists() { var envelope *dsse.Envelope require.NoError(s.T(), json.Unmarshal(attJSON, &envelope)) - wantReferrerAtt := &biz.StoredReferrer{ + wantReferrerAtt := &biz.Referrer{ Digest: "sha256:ad704d286bcad6e155e71c33d48247931231338396acbcd9769087530085b2a2", Kind: "ATTESTATION", Downloadable: true, } - wantReferrerCommit := &biz.StoredReferrer{ + wantReferrerCommit := &biz.Referrer{ Digest: "sha1:78ac366c9e8a300d51808d581422ca61f7b5b721", Kind: "GIT_HEAD_COMMIT", } - wantReferrerSBOM := &biz.StoredReferrer{ + wantReferrerSBOM := &biz.Referrer{ Digest: "sha256:16159bb881eb4ab7eb5d8afc5350b0feeed1e31c0a268e355e74f9ccbe885e0c", Kind: "SBOM_CYCLONEDX_JSON", Downloadable: true, } - wantReferrerArtifact := &biz.StoredReferrer{ + wantReferrerArtifact := &biz.Referrer{ Digest: "sha256:385c4188b9c080499413f2e0fa0b3951ed107b5f0cb35c2f2b1f07a7be9a7512", Kind: "ARTIFACT", Downloadable: true, } - wantReferrerOpenVEX := &biz.StoredReferrer{ + wantReferrerOpenVEX := &biz.Referrer{ Digest: "sha256:b4bd86d5855f94bcac0a92d3100ae7b85d050bd2e5fb9037a200e5f5f0b073a2", Kind: "OPENVEX", Downloadable: true, } - wantReferrerSarif := &biz.StoredReferrer{ + wantReferrerSarif := &biz.Referrer{ Digest: "sha256:c4a63494f9289dd9fd44f841efb4f5b52765c2de6332f2d86e5f6c0340b40a95", Kind: "SARIF", Downloadable: true, } - wantReferrerContainerImage := &biz.StoredReferrer{ + wantReferrerContainerImage := &biz.Referrer{ Digest: "sha256:fbd9335f55d83d8aaf9ab1a539b0f2a87b444e8c54f34c9a1ca9d7df15605db4", Kind: "CONTAINER_IMAGE", } @@ -109,12 +109,10 @@ func (s *referrerIntegrationTestSuite) TestExtractAndPersists() { // it has all the references require.Len(t, got.References, 6) - for i, want := range []*biz.StoredReferrer{ + for i, want := range []*biz.Referrer{ wantReferrerCommit, wantReferrerSBOM, wantReferrerArtifact, wantReferrerOpenVEX, wantReferrerSarif, wantReferrerContainerImage} { gotR := got.References[i] - s.Equal(want.Digest, gotR.Digest) - s.Equal(want.Kind, gotR.Kind) - s.Equal(want.Downloadable, gotR.Downloadable) + s.Equal(want, gotR.Referrer) } s.Equal([]uuid.UUID{s.org1UUID}, got.OrgIDs) }) @@ -181,13 +179,13 @@ func (s *referrerIntegrationTestSuite) TestExtractAndPersists() { s.Nil(got) }) - s.T().Run("it should fail if the attestation has the same material twice with different types", func(t *testing.T) { + s.T().Run("it should NOT fail storing the attestation with the same material twice with different types", func(t *testing.T) { attJSON, err = os.ReadFile("testdata/attestations/with-duplicated-sha.json") require.NoError(s.T(), err) require.NoError(s.T(), json.Unmarshal(attJSON, &envelope)) err := s.Referrer.ExtractAndPersist(ctx, envelope, s.org1.ID) - s.ErrorContains(err, "has different types") + s.NoError(err) }) s.T().Run("it should fail on retrieval if we have stored two referrers with same digest (for two different types)", func(t *testing.T) { @@ -203,7 +201,7 @@ func (s *referrerIntegrationTestSuite) TestExtractAndPersists() { // but retrieval should fail. In the future we will ask the user to provide the artifact type in these cases of ambiguity got, err := s.Referrer.GetFromRoot(ctx, wantReferrerSarif.Digest, s.user.ID) s.Nil(got) - s.ErrorContains(err, "found more than one referrer with digest") + s.ErrorContains(err, "present in 2 kinds") }) s.T().Run("now there should a container image pointing to two attestations", func(t *testing.T) { diff --git a/app/controlplane/internal/biz/referrer_test.go b/app/controlplane/internal/biz/referrer_test.go index bc76c5d43..7b147192c 100644 --- a/app/controlplane/internal/biz/referrer_test.go +++ b/app/controlplane/internal/biz/referrer_test.go @@ -31,50 +31,101 @@ func (s *referrerTestSuite) TestExtractReferrers() { name string inputPath string expectErr bool - want ReferrerMap + want []*Referrer }{ { name: "basic", inputPath: "testdata/attestations/full.json", - want: ReferrerMap{ - "sha256:1a077137aef7ca208b80c339769d0d7eecacc2850368e56e834cda1750ce413a": &Referrer{ + want: []*Referrer{ + { Digest: "sha256:1a077137aef7ca208b80c339769d0d7eecacc2850368e56e834cda1750ce413a", Kind: "ATTESTATION", Downloadable: true, - References: []string{ - "sha256:264f55a6ff9cec2f4742a9faacc033b29f65c04dd4480e71e23579d484288d61", - "sha256:16159bb881eb4ab7eb5d8afc5350b0feeed1e31c0a268e355e74f9ccbe885e0c", + References: []*Referrer{ + { + Digest: "sha256:264f55a6ff9cec2f4742a9faacc033b29f65c04dd4480e71e23579d484288d61", + Kind: "CONTAINER_IMAGE", + }, + { + Digest: "sha256:16159bb881eb4ab7eb5d8afc5350b0feeed1e31c0a268e355e74f9ccbe885e0c", + Kind: "SBOM_CYCLONEDX_JSON", + }, }, }, - "sha256:16159bb881eb4ab7eb5d8afc5350b0feeed1e31c0a268e355e74f9ccbe885e0c": &Referrer{ + { + Digest: "sha256:264f55a6ff9cec2f4742a9faacc033b29f65c04dd4480e71e23579d484288d61", + Kind: "CONTAINER_IMAGE", + }, + { Digest: "sha256:16159bb881eb4ab7eb5d8afc5350b0feeed1e31c0a268e355e74f9ccbe885e0c", Kind: "SBOM_CYCLONEDX_JSON", Downloadable: true, }, - "sha256:264f55a6ff9cec2f4742a9faacc033b29f65c04dd4480e71e23579d484288d61": &Referrer{ - Digest: "sha256:264f55a6ff9cec2f4742a9faacc033b29f65c04dd4480e71e23579d484288d61", - Kind: "CONTAINER_IMAGE", - }, }, }, { - name: "basic", + name: "with string value material to be discarded", inputPath: "testdata/attestations/with-string.json", - want: ReferrerMap{ + want: []*Referrer{ + { + Digest: "sha256:507dddb505ceb53fb32cde31f9935c9a3ebc7b7d82f36101de638b1ab9367344", + Kind: "ATTESTATION", + Downloadable: true, + References: []*Referrer{ + { + Digest: "sha1:58442b61a6564df94857ff69ad7c340c55703e20", + Kind: "GIT_HEAD_COMMIT", + }, + }, + }, // the git commit a subject in the attestation - "sha1:58442b61a6564df94857ff69ad7c340c55703e20": &Referrer{ + { Digest: "sha1:58442b61a6564df94857ff69ad7c340c55703e20", Kind: "GIT_HEAD_COMMIT", - References: []string{ - "sha256:507dddb505ceb53fb32cde31f9935c9a3ebc7b7d82f36101de638b1ab9367344", + References: []*Referrer{ + { + Digest: "sha256:507dddb505ceb53fb32cde31f9935c9a3ebc7b7d82f36101de638b1ab9367344", + Kind: "ATTESTATION", + }, }, }, - "sha256:507dddb505ceb53fb32cde31f9935c9a3ebc7b7d82f36101de638b1ab9367344": &Referrer{ - Digest: "sha256:507dddb505ceb53fb32cde31f9935c9a3ebc7b7d82f36101de638b1ab9367344", - Kind: "ATTESTATION", - References: []string{ - "sha1:58442b61a6564df94857ff69ad7c340c55703e20", + }, + }, + { + name: "with two materials with same digest", + inputPath: "testdata/attestations/with-duplicated-sha.json", + want: []*Referrer{ + { + Digest: "sha256:47e94045e8ffb5ea9a4939a03a21c5ad26f4ea7d463ac6ec46dac15349f45b3f", + Kind: "ATTESTATION", + Downloadable: true, + References: []*Referrer{ + { + Digest: "sha256:264f55a6ff9cec2f4742a9faacc033b29f65c04dd4480e71e23579d484288d61", + Kind: "CONTAINER_IMAGE", + }, + { + Digest: "sha256:264f55a6ff9cec2f4742a9faacc033b29f65c04dd4480e71e23579d484288d61", + Kind: "SBOM_CYCLONEDX_JSON", + }, + { + Digest: "sha256:16159bb881eb4ab7eb5d8afc5350b0feeed1e31c0a268e355e74f9ccbe885e0c", + Kind: "SBOM_CYCLONEDX_JSON", + }, }, + }, + { + Digest: "sha256:264f55a6ff9cec2f4742a9faacc033b29f65c04dd4480e71e23579d484288d61", + Kind: "CONTAINER_IMAGE", + }, + { + Digest: "sha256:16159bb881eb4ab7eb5d8afc5350b0feeed1e31c0a268e355e74f9ccbe885e0c", + Kind: "SBOM_CYCLONEDX_JSON", + Downloadable: true, + }, + { + Digest: "sha256:264f55a6ff9cec2f4742a9faacc033b29f65c04dd4480e71e23579d484288d61", + Kind: "SBOM_CYCLONEDX_JSON", Downloadable: true, }, }, @@ -82,62 +133,80 @@ func (s *referrerTestSuite) TestExtractReferrers() { { name: "with git subject", inputPath: "testdata/attestations/with-git-subject.json", - want: ReferrerMap{ - "sha256:fbd9335f55d83d8aaf9ab1a539b0f2a87b444e8c54f34c9a1ca9d7df15605db4": &Referrer{ + want: []*Referrer{ + { + Digest: "sha256:385c4188b9c080499413f2e0fa0b3951ed107b5f0cb35c2f2b1f07a7be9a7512", + Kind: "ARTIFACT", + Downloadable: true, + }, + { + Digest: "sha256:ad704d286bcad6e155e71c33d48247931231338396acbcd9769087530085b2a2", + Kind: "ATTESTATION", + Downloadable: true, + References: []*Referrer{ + { + Digest: "sha256:fbd9335f55d83d8aaf9ab1a539b0f2a87b444e8c54f34c9a1ca9d7df15605db4", + Kind: "CONTAINER_IMAGE", + }, + { + Digest: "sha256:385c4188b9c080499413f2e0fa0b3951ed107b5f0cb35c2f2b1f07a7be9a7512", + Kind: "ARTIFACT", + }, + { + Digest: "sha256:c4a63494f9289dd9fd44f841efb4f5b52765c2de6332f2d86e5f6c0340b40a95", + Kind: "SARIF", + }, + { + Digest: "sha256:16159bb881eb4ab7eb5d8afc5350b0feeed1e31c0a268e355e74f9ccbe885e0c", + Kind: "SBOM_CYCLONEDX_JSON", + }, + { + Digest: "sha256:b4bd86d5855f94bcac0a92d3100ae7b85d050bd2e5fb9037a200e5f5f0b073a2", + Kind: "OPENVEX", + }, + { + Digest: "sha1:78ac366c9e8a300d51808d581422ca61f7b5b721", + Kind: "GIT_HEAD_COMMIT", + }, + }, + }, + { Digest: "sha256:fbd9335f55d83d8aaf9ab1a539b0f2a87b444e8c54f34c9a1ca9d7df15605db4", Kind: "CONTAINER_IMAGE", // the container image is a subject in the attestation - References: []string{ - "sha256:ad704d286bcad6e155e71c33d48247931231338396acbcd9769087530085b2a2", + References: []*Referrer{ + { + Digest: "sha256:ad704d286bcad6e155e71c33d48247931231338396acbcd9769087530085b2a2", + Kind: "ATTESTATION", + }, }, }, - "sha1:78ac366c9e8a300d51808d581422ca61f7b5b721": &Referrer{ + { Digest: "sha1:78ac366c9e8a300d51808d581422ca61f7b5b721", Kind: "GIT_HEAD_COMMIT", // the git commit a subject in the attestation - References: []string{ - "sha256:ad704d286bcad6e155e71c33d48247931231338396acbcd9769087530085b2a2", + References: []*Referrer{ + { + Digest: "sha256:ad704d286bcad6e155e71c33d48247931231338396acbcd9769087530085b2a2", + Kind: "ATTESTATION", + }, }, }, - "sha256:385c4188b9c080499413f2e0fa0b3951ed107b5f0cb35c2f2b1f07a7be9a7512": &Referrer{ - Digest: "sha256:385c4188b9c080499413f2e0fa0b3951ed107b5f0cb35c2f2b1f07a7be9a7512", - Kind: "ARTIFACT", + { + Digest: "sha256:b4bd86d5855f94bcac0a92d3100ae7b85d050bd2e5fb9037a200e5f5f0b073a2", + Kind: "OPENVEX", Downloadable: true, }, - "sha256:c4a63494f9289dd9fd44f841efb4f5b52765c2de6332f2d86e5f6c0340b40a95": &Referrer{ + { Digest: "sha256:c4a63494f9289dd9fd44f841efb4f5b52765c2de6332f2d86e5f6c0340b40a95", Kind: "SARIF", Downloadable: true, }, - "sha256:16159bb881eb4ab7eb5d8afc5350b0feeed1e31c0a268e355e74f9ccbe885e0c": &Referrer{ + { Digest: "sha256:16159bb881eb4ab7eb5d8afc5350b0feeed1e31c0a268e355e74f9ccbe885e0c", Kind: "SBOM_CYCLONEDX_JSON", Downloadable: true, }, - "sha256:b4bd86d5855f94bcac0a92d3100ae7b85d050bd2e5fb9037a200e5f5f0b073a2": &Referrer{ - Digest: "sha256:b4bd86d5855f94bcac0a92d3100ae7b85d050bd2e5fb9037a200e5f5f0b073a2", - Kind: "OPENVEX", - Downloadable: true, - }, - "sha256:ad704d286bcad6e155e71c33d48247931231338396acbcd9769087530085b2a2": &Referrer{ - Digest: "sha256:ad704d286bcad6e155e71c33d48247931231338396acbcd9769087530085b2a2", - Kind: "ATTESTATION", - Downloadable: true, - References: []string{ - // container image - "sha256:fbd9335f55d83d8aaf9ab1a539b0f2a87b444e8c54f34c9a1ca9d7df15605db4", - // artifact - "sha256:385c4188b9c080499413f2e0fa0b3951ed107b5f0cb35c2f2b1f07a7be9a7512", - // sarif - "sha256:c4a63494f9289dd9fd44f841efb4f5b52765c2de6332f2d86e5f6c0340b40a95", - // sbom - "sha256:16159bb881eb4ab7eb5d8afc5350b0feeed1e31c0a268e355e74f9ccbe885e0c", - // openvex - "sha256:b4bd86d5855f94bcac0a92d3100ae7b85d050bd2e5fb9037a200e5f5f0b073a2", - // git head commit - "sha1:78ac366c9e8a300d51808d581422ca61f7b5b721", - }, - }, }, }, } diff --git a/app/controlplane/internal/data/referrer.go b/app/controlplane/internal/data/referrer.go index 9b5ea8bf8..102ae1e8c 100644 --- a/app/controlplane/internal/data/referrer.go +++ b/app/controlplane/internal/data/referrer.go @@ -41,7 +41,7 @@ func NewReferrerRepo(data *Data, logger log.Logger) biz.ReferrerRepo { type storedReferrerMap map[string]*ent.Referrer -func (r *ReferrerRepo) Save(ctx context.Context, input biz.ReferrerMap, orgID uuid.UUID) error { +func (r *ReferrerRepo) Save(ctx context.Context, referrers []*biz.Referrer, orgID uuid.UUID) error { // Start transaction tx, err := r.data.db.Tx(ctx) if err != nil { @@ -50,16 +50,16 @@ func (r *ReferrerRepo) Save(ctx context.Context, input biz.ReferrerMap, orgID uu storedMap := make(storedReferrerMap) // 1 - Find or create each referrer - for digest, r := range input { + for _, r := range referrers { // Check if it exists already, if not create it - storedRef, err := tx.Referrer.Query().Where(referrer.Digest(digest), referrer.Kind(r.Kind)).Only(ctx) + storedRef, err := tx.Referrer.Query().Where(referrer.Digest(r.Digest), referrer.Kind(r.Kind)).Only(ctx) if err != nil { if !ent.IsNotFound(err) { return fmt.Errorf("failed to query referrer: %w", err) } storedRef, err = tx.Referrer.Create(). - SetDigest(digest).SetKind(r.Kind).SetDownloadable(r.Downloadable).AddOrganizationIDs(orgID).Save(ctx) + SetDigest(r.Digest).SetKind(r.Kind).SetDownloadable(r.Downloadable).AddOrganizationIDs(orgID).Save(ctx) if err != nil { return fmt.Errorf("failed to create referrer: %w", err) } @@ -72,19 +72,19 @@ func (r *ReferrerRepo) Save(ctx context.Context, input biz.ReferrerMap, orgID uu } // Store it in the map - storedMap[digest] = storedRef + storedMap[r.MapID()] = storedRef } // 2 - define the relationship between referrers - for digest, inputRef := range input { + for _, r := range referrers { // This is the current item stored in DB - storedReferrer := storedMap[digest] + storedReferrer := storedMap[r.MapID()] // Iterate on the items it refer to (references) - for _, ref := range inputRef.References { + for _, ref := range r.References { // amd find it in the DB - storedReference, ok := storedMap[ref] + storedReference, ok := storedMap[ref.MapID()] if !ok { - return fmt.Errorf("referrer %s not found", ref) + return fmt.Errorf("referrer %v not found", ref) } // Create the relationship @@ -120,26 +120,37 @@ const maxTraverseLevels = 1 func (r *ReferrerRepo) doGet(ctx context.Context, digest string, orgIDs []uuid.UUID, level int) (*biz.StoredReferrer, error) { // Find the referrer // if there is more than 1 item with the same digest+artifactType it will fail - ref, err := r.data.db.Referrer.Query().Where(referrer.Digest(digest)). + refs, err := r.data.db.Referrer.Query().Where(referrer.Digest(digest)). Where(referrer.HasOrganizationsWith(organization.IDIn(orgIDs...))). - Only(ctx) - if err != nil { - if ent.IsNotFound(err) { - return nil, nil - } else if ent.IsNotSingular(err) { - return nil, fmt.Errorf("found more than one referrer with digest %s, please provide artifact type", digest) - } + All(ctx) + if err != nil { return nil, fmt.Errorf("failed to query referrer: %w", err) } + // No items found + if numrefs := len(refs); numrefs == 0 { + return nil, nil + } else if numrefs > 1 { + // if there is more than 1 item with the same digest+artifactType we will fail + var kinds []string + for _, r := range refs { + kinds = append(kinds, r.Kind) + } + return nil, biz.NewErrReferrerAmbiguous(digest, kinds) + } + + ref := refs[0] + // Assemble the referrer to return res := &biz.StoredReferrer{ - ID: ref.ID, - CreatedAt: toTimePtr(ref.CreatedAt), - Digest: ref.Digest, - Kind: ref.Kind, - Downloadable: ref.Downloadable, + ID: ref.ID, + CreatedAt: toTimePtr(ref.CreatedAt), + Referrer: &biz.Referrer{ + Digest: ref.Digest, + Kind: ref.Kind, + Downloadable: ref.Downloadable, + }, } // with all the organizationIDs attached @@ -154,7 +165,7 @@ func (r *ReferrerRepo) doGet(ctx context.Context, digest string, orgIDs []uuid.U } // Find the references and call recursively - refs, err := ref.QueryReferences(). + refs, err = ref.QueryReferences(). Where(referrer.HasOrganizationsWith(organization.IDIn(orgIDs...))). Order(referrer.ByDigest()).All(ctx) if err != nil {