Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 38 additions & 13 deletions artifactory/commands/helm/layers.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ package helm

import (
"fmt"
"path"
"strings"

"github.com/jfrog/build-info-go/entities"
ioutils "github.com/jfrog/gofrog/io"
"github.com/jfrog/jfrog-cli-artifactory/artifactory/commands/ocicontainer"
Expand All @@ -10,7 +13,6 @@ import (
"github.com/jfrog/jfrog-client-go/artifactory/services"
servicesUtils "github.com/jfrog/jfrog-client-go/artifactory/services/utils"
"github.com/jfrog/jfrog-client-go/utils/log"
"strings"
)

type manifest struct {
Expand Down Expand Up @@ -64,24 +66,47 @@ func processDependency(dep entities.Dependency, serviceManager artifactory.Artif

// addOCILayersForDependency adds all OCI layers for a dependency that has checksums
func addOCILayersForDependency(dep entities.Dependency, serviceManager artifactory.ArtifactoryServicesManager, processedDependencies *[]entities.Dependency) {
versionPath := extractDependencyPath(dep.Id)
if versionPath == "" {
chartName, chartVersion, err := parseDependencyID(dep.Id)
if err != nil {
log.Error("Failed to find a valid version for dependency: ", dep.Id)
return
}
repoName := extractRepositoryNameFromURL(dep.Repository)
if repoName == "" {
log.Error("Failed to find a valid repository for dependency: ", dep.Id)
return
registryReference := strings.TrimRight(strings.TrimPrefix(dep.Repository, oci), "/")
var candidates []ociRepoCandidate
if !strings.Contains(registryReference, "/") {
candidates = []ociRepoCandidate{{repoKey: extractRepositoryFromHostSubdomain(registryReference)}}
} else {
ref, parseErr := parseOCIReference(registryReference)
if parseErr != nil {
log.Error("Failed to find a valid repository for dependency: ", dep.Id, " : ", parseErr)
return
}
candidates = generateRepoCandidates(ref.Registry, ref.Repository)
}
aqlQuery := fmt.Sprintf(`{
var (
repoName string
resultMap map[string]*servicesUtils.ResultItem
)
for _, candidate := range candidates {
if candidate.repoKey == "" {
continue
}
storagePath := path.Join(candidate.subpath, chartName, chartVersion)
aqlQuery := fmt.Sprintf(`{
"repo": "%s",
"path": "%s"
}`, repoName, versionPath)
resultMap, err := searchOCIArtifactsByAQL(serviceManager, aqlQuery)
if err != nil {
log.Debug("Failed to search OCI artifacts for dependency ", dep.Id, " : ", err)
return
}`, candidate.repoKey, storagePath)
resultMap, err = searchOCIArtifactsByAQL(serviceManager, aqlQuery)
if err != nil {
log.Debug("Failed to search OCI artifacts for dependency ", dep.Id, " : ", err)
return
}
if len(resultMap) == 0 {
continue
}
repoName = candidate.repoKey
Comment thread
vjda marked this conversation as resolved.
log.Debug("Resolved OCI dependency ", dep.Id, " to repo: ", candidate.repoKey, ", subpath: ", candidate.subpath)
break
}
if len(resultMap) == 0 {
log.Debug("Did not find any OCI artifacts for dependency: ", dep.Id)
Expand Down
155 changes: 155 additions & 0 deletions artifactory/commands/helm/layers_test.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
package helm

import (
"fmt"
"testing"

"github.com/jfrog/build-info-go/entities"
servicesUtils "github.com/jfrog/jfrog-client-go/artifactory/services/utils"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

// TestParseDependencyID tests the parseDependencyID function
Expand Down Expand Up @@ -90,3 +94,154 @@
})
}
}

func TestAddOCILayersForDependency(t *testing.T) {
const (
chartName = "chart"
chartVersion = "0.1.0"
)

tests := []struct {
name string
dependency entities.Dependency
responses map[pushSearchCall][]servicesUtils.ResultItem
expectedSearchCalls []pushSearchCall
expectedDependencies []entities.Dependency
manifestRepo string
manifestPath string
}{
{
name: "single-segment virtual host subpath resolves on first host-based candidate",
dependency: entities.Dependency{
Id: chartName + ":" + chartVersion,
Repository: "oci://helm-repo.art.com/team-a",
},
responses: map[pushSearchCall][]servicesUtils.ResultItem{
{repo: "helm-repo", path: "team-a/chart/0.1.0"}: {
newOCIArtifact("helm-repo", "team-a/chart/0.1.0", "manifest.json", "manifest-sha"),
newOCIArtifact("helm-repo", "team-a/chart/0.1.0", "sha256__config", "config-sha"),
newOCIArtifact("helm-repo", "team-a/chart/0.1.0", "sha256__layer", "layer-sha"),
},
{repo: "team-a", path: "chart/0.1.0"}: nil,
},
expectedSearchCalls: []pushSearchCall{
{repo: "helm-repo", path: "team-a/chart/0.1.0"},
},
expectedDependencies: []entities.Dependency{
newProcessedLayerDependency("manifest.json", "helm-repo", "manifest-sha"),
newProcessedLayerDependency("sha256__config", "helm-repo", "config-sha"),
newProcessedLayerDependency("sha256__layer", "helm-repo", "layer-sha"),
},
manifestRepo: "helm-repo",
manifestPath: "team-a/chart/0.1.0",
},
{
name: "oci dependency with non-root subpath resolves on first host-based candidate",
dependency: entities.Dependency{
Id: chartName + ":" + chartVersion,
Repository: "oci://helm-repo.art.com/team-a/charts",
},
responses: map[pushSearchCall][]servicesUtils.ResultItem{
{repo: "helm-repo", path: "team-a/charts/chart/0.1.0"}: {
newOCIArtifact("helm-repo", "team-a/charts/chart/0.1.0", "manifest.json", "manifest-sha"),
newOCIArtifact("helm-repo", "team-a/charts/chart/0.1.0", "sha256__config", "config-sha"),
newOCIArtifact("helm-repo", "team-a/charts/chart/0.1.0", "sha256__layer", "layer-sha"),
},
{repo: "team-a", path: "charts/chart/0.1.0"}: nil,
},
expectedSearchCalls: []pushSearchCall{
{repo: "helm-repo", path: "team-a/charts/chart/0.1.0"},
},
expectedDependencies: []entities.Dependency{
newProcessedLayerDependency("manifest.json", "helm-repo", "manifest-sha"),
newProcessedLayerDependency("sha256__config", "helm-repo", "config-sha"),
newProcessedLayerDependency("sha256__layer", "helm-repo", "layer-sha"),
},
manifestRepo: "helm-repo",
manifestPath: "team-a/charts/chart/0.1.0",
},
{
name: "root-only oci dependency keeps existing resolution",
dependency: entities.Dependency{
Id: chartName + ":" + chartVersion,
Repository: "oci://helm-repo.art.com",
},
responses: map[pushSearchCall][]servicesUtils.ResultItem{
{repo: "helm-repo", path: "chart/0.1.0"}: {
newOCIArtifact("helm-repo", "chart/0.1.0", "manifest.json", "manifest-sha"),
newOCIArtifact("helm-repo", "chart/0.1.0", "sha256__config", "config-sha"),
newOCIArtifact("helm-repo", "chart/0.1.0", "sha256__layer", "layer-sha"),
},
},
expectedSearchCalls: []pushSearchCall{{repo: "helm-repo", path: "chart/0.1.0"}},
expectedDependencies: []entities.Dependency{
newProcessedLayerDependency("manifest.json", "helm-repo", "manifest-sha"),
newProcessedLayerDependency("sha256__config", "helm-repo", "config-sha"),
newProcessedLayerDependency("sha256__layer", "helm-repo", "layer-sha"),
},
manifestRepo: "helm-repo",
manifestPath: "chart/0.1.0",
},
{
name: "oci dependency without matching host-first or path fallback returns without adding layers",
dependency: entities.Dependency{
Id: chartName + ":" + chartVersion,
Repository: "oci://helm-repo.art.com/team-a/charts",
},
responses: map[pushSearchCall][]servicesUtils.ResultItem{
{repo: "helm-repo", path: "team-a/charts/chart/0.1.0"}: nil,
{repo: "team-a", path: "charts/chart/0.1.0"}: nil,
},
expectedSearchCalls: []pushSearchCall{
{repo: "helm-repo", path: "team-a/charts/chart/0.1.0"},
{repo: "team-a", path: "charts/chart/0.1.0"},
},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
serviceManager := newPushTestServiceManager(t)
serviceManager.searchResults = tt.responses
if tt.manifestRepo != "" {
serviceManager.remoteContents[fmt.Sprintf("%s/%s/manifest.json", tt.manifestRepo, tt.manifestPath)] = createPushManifestJSON("sha256:config", "sha256:layer")
}

var processed []entities.Dependency
addOCILayersForDependency(tt.dependency, serviceManager, &processed)

assert.Equal(t, tt.expectedSearchCalls, serviceManager.searchCalls)
assert.Equal(t, tt.expectedDependencies, processed)
})
}
}

func TestUpdateClassicHelmDependencyChecksumsLeavesExistingChecksumsUntouched(t *testing.T) {
serviceManager := newPushTestServiceManager(t)
dep := entities.Dependency{
Id: "classic:1.2.3",
Repository: "https://art.company.com/helm-local",
Checksum: entities.Checksum{
Md5: "md5",
Sha1: "sha1",
Sha256: "sha256",
},
}

var processed []entities.Dependency
updateClassicHelmDependencyChecksums(dep, serviceManager, &processed)

require.Len(t, processed, 1)
assert.Equal(t, dep, processed[0])
assert.Empty(t, serviceManager.searchCalls)
}

func newProcessedLayerDependency(name, repo, sha256 string) entities.Dependency {

Check failure on line 239 in artifactory/commands/helm/layers_test.go

View workflow job for this annotation

GitHub Actions / Static Check ubuntu-latest

newProcessedLayerDependency - repo always receives "helm-repo" (unparam)
return entities.Dependency{
Id: name,
Repository: repo,
Checksum: entities.Checksum{
Sha256: sha256,
},
}
}
Loading
Loading