Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
50 changes: 37 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,46 @@ 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)
Comment thread
vjda marked this conversation as resolved.
Outdated
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.
break
}
if len(resultMap) == 0 {
log.Debug("Did not find any OCI artifacts for dependency: ", dep.Id)
Expand Down
157 changes: 157 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,156 @@ func TestExtractDependencyPathInLayers(t *testing.T) {
})
}
}

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 using host fallback",
dependency: entities.Dependency{
Id: chartName + ":" + chartVersion,
Repository: "oci://helm-repo.art.com/team-a",
},
responses: map[pushSearchCall][]servicesUtils.ResultItem{
{repo: "team-a", path: "chart/0.1.0"}: nil,
{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"),
},
},
expectedSearchCalls: []pushSearchCall{
{repo: "team-a", path: "chart/0.1.0"},
{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 using validated candidate",
dependency: entities.Dependency{
Id: chartName + ":" + chartVersion,
Repository: "oci://helm-repo.art.com/team-a/charts",
},
responses: map[pushSearchCall][]servicesUtils.ResultItem{
{repo: "team-a", path: "charts/chart/0.1.0"}: nil,
{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"),
},
},
expectedSearchCalls: []pushSearchCall{
{repo: "team-a", path: "charts/chart/0.1.0"},
{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 candidate 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: "team-a", path: "charts/chart/0.1.0"}: nil,
{repo: "helm-repo", path: "team-a/charts/chart/0.1.0"}: nil,
},
expectedSearchCalls: []pushSearchCall{
{repo: "team-a", path: "charts/chart/0.1.0"},
{repo: "helm-repo", path: "team-a/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 {
return entities.Dependency{
Id: name,
Repository: repo,
Checksum: entities.Checksum{
Sha256: sha256,
},
}
}
Loading
Loading