Skip to content
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

Fix Helm index validation for Artifactory #1516

Merged
merged 1 commit into from
Jul 22, 2024
Merged
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
1 change: 1 addition & 0 deletions internal/helm/chart/builder_remote_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ entries:
- https://example.com/grafana.tgz
description: string
version: 6.17.4
name: grafana
`)

mockGetter := &mockIndexChartGetter{
Expand Down
33 changes: 31 additions & 2 deletions internal/helm/repository/chart_repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import (
"os"
"path"
"sort"
"strings"
"sync"

"github.com/Masterminds/semver/v3"
Expand Down Expand Up @@ -86,18 +87,24 @@ func IndexFromBytes(b []byte) (*repo.IndexFile, error) {
return nil, repo.ErrNoAPIVersion
}

for _, cvs := range i.Entries {
for name, cvs := range i.Entries {
for idx := len(cvs) - 1; idx >= 0; idx-- {
if cvs[idx] == nil {
continue
}
// When metadata section missing, initialize with no data
if cvs[idx].Metadata == nil {
cvs[idx].Metadata = &chart.Metadata{}
}
if cvs[idx].APIVersion == "" {
cvs[idx].APIVersion = chart.APIVersionV1
}
if err := cvs[idx].Validate(); err != nil {
if err := cvs[idx].Validate(); ignoreSkippableChartValidationError(err) != nil {
cvs = append(cvs[:idx], cvs[idx+1:]...)
}
}
// adjust slice to only contain a set of valid versions
i.Entries[name] = cvs
}

i.SortEntries()
Expand Down Expand Up @@ -501,3 +508,25 @@ func jsonOrYamlUnmarshal(b []byte, i interface{}) error {
}
return yaml.UnmarshalStrict(b, i)
}

// ignoreSkippableChartValidationError inspect the given error and returns nil if
// the error isn't important for index loading
//
// In particular, charts may introduce validations that don't impact repository indexes
// And repository indexes may be generated by older/non-complient software, which doesn't
// conform to all validations.
//
// this code is taken from https://github.com/helm/helm/blob/v3.15.2/pkg/repo/index.go#L402
func ignoreSkippableChartValidationError(err error) error {
verr, ok := err.(chart.ValidationError)
if !ok {
return err
}

// https://github.com/helm/helm/issues/12748 (JFrog repository strips alias field from index)
if strings.HasPrefix(verr.Error(), "validation: more than one dependency with name or alias") {
return nil
}

return err
}
148 changes: 146 additions & 2 deletions internal/helm/repository/chart_repository_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -672,7 +672,7 @@ func verifyLocalIndex(t *testing.T, i *repo.IndexFile) {
g := NewWithT(t)

g.Expect(i.Entries).ToNot(BeNil())
g.Expect(i.Entries).To(HaveLen(3), "expected 3 entries in index file")
g.Expect(i.Entries).To(HaveLen(4), "expected 4 entries in index file")

alpine, ok := i.Entries["alpine"]
g.Expect(ok).To(BeTrue(), "expected 'alpine' entry to exist")
Expand All @@ -682,6 +682,10 @@ func verifyLocalIndex(t *testing.T, i *repo.IndexFile) {
g.Expect(ok).To(BeTrue(), "expected 'nginx' entry to exist")
g.Expect(nginx).To(HaveLen(2), "'nginx' should have 2 entries")

broken, ok := i.Entries["xChartWithDuplicateDependenciesAndMissingAlias"]
g.Expect(ok).To(BeTrue(), "expected 'xChartWithDuplicateDependenciesAndMissingAlias' entry to exist")
g.Expect(broken).To(HaveLen(1), "'xChartWithDuplicateDependenciesAndMissingAlias' should have 1 entries")

expects := []*repo.ChartVersion{
{
Metadata: &chart.Metadata{
Expand Down Expand Up @@ -723,8 +727,24 @@ func verifyLocalIndex(t *testing.T, i *repo.IndexFile) {
},
Digest: "sha256:1234567890abcdef",
},
{
Metadata: &chart.Metadata{
Name: "xChartWithDuplicateDependenciesAndMissingAlias",
Description: "string",
Version: "1.2.3",
Keywords: []string{"broken", "still accepted"},
Home: "https://example.com/something",
darkowlzz marked this conversation as resolved.
Show resolved Hide resolved
Dependencies: []*chart.Dependency{
{Name: "kube-rbac-proxy", Version: "0.9.1"},
},
},
URLs: []string{
"https://kubernetes-charts.storage.googleapis.com/nginx-1.2.3.tgz",
},
Digest: "sha256:1234567890abcdef",
},
}
tests := []*repo.ChartVersion{alpine[0], nginx[0], nginx[1]}
tests := []*repo.ChartVersion{alpine[0], nginx[0], nginx[1], broken[0]}

for i, tt := range tests {
expect := expects[i]
Expand All @@ -735,5 +755,129 @@ func verifyLocalIndex(t *testing.T, i *repo.IndexFile) {
g.Expect(tt.Home).To(Equal(expect.Home))
g.Expect(tt.URLs).To(ContainElements(expect.URLs))
g.Expect(tt.Keywords).To(ContainElements(expect.Keywords))
g.Expect(tt.Dependencies).To(ContainElements(expect.Dependencies))
}
}

// This code is taken from https://github.com/helm/helm/blob/v3.15.2/pkg/repo/index_test.go#L601
// and refers to: https://github.com/helm/helm/issues/12748
func TestIgnoreSkippableChartValidationError(t *testing.T) {
type TestCase struct {
Input error
ErrorSkipped bool
}
testCases := map[string]TestCase{
"nil": {
Input: nil,
},
"generic_error": {
Input: fmt.Errorf("foo"),
},
"non_skipped_validation_error": {
Input: chart.ValidationError("chart.metadata.type must be application or library"),
},
"skipped_validation_error": {
Input: chart.ValidationErrorf("more than one dependency with name or alias %q", "foo"),
ErrorSkipped: true,
},
}

for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
result := ignoreSkippableChartValidationError(tc.Input)

if tc.Input == nil {
if result != nil {
t.Error("expected nil result for nil input")
}
return
}

if tc.ErrorSkipped {
if result != nil {
t.Error("expected nil result for skipped error")
}
return
}

if tc.Input != result {
t.Error("expected the result equal to input")
}

})
}
}

var indexWithFirstVersionInvalid = `
apiVersion: v1
entries:
nginx:
- urls:
- https://charts.helm.sh/stable/alpine-1.0.0.tgz
- http://storage2.googleapis.com/kubernetes-charts/alpine-1.0.0.tgz
name: nginx
version: 0..1.0
description: string
home: https://github.com/something
digest: "sha256:1234567890abcdef"
- urls:
- https://charts.helm.sh/stable/nginx-0.2.0.tgz
name: nginx
description: string
version: 0.2.0
home: https://github.com/something/else
digest: "sha256:1234567890abcdef"
`
var indexWithLastVersionInvalid = `
apiVersion: v1
entries:
nginx:
- urls:
- https://charts.helm.sh/stable/nginx-0.2.0.tgz
name: nginx
description: string
version: 0.2.0
home: https://github.com/something/else
digest: "sha256:1234567890abcdef"
- urls:
- https://charts.helm.sh/stable/alpine-1.0.0.tgz
- http://storage2.googleapis.com/kubernetes-charts/alpine-1.0.0.tgz
name: nginx
version: 0..1.0
description: string
home: https://github.com/something
digest: "sha256:1234567890abcdef"
`

func TestIndexFromBytes_InvalidEntries(t *testing.T) {
tests := []struct {
source string
data string
}{
{
source: "indexWithFirstVersionInvalid",
data: indexWithFirstVersionInvalid,
},
{
source: "indexWithLastVersionInvalid",
data: indexWithLastVersionInvalid,
},
}
for _, tc := range tests {
t.Run(tc.source, func(t *testing.T) {
idx, err := IndexFromBytes([]byte(tc.data))
if err != nil {
t.Fatalf("unexpected error: %s", err)
}
cvs := idx.Entries["nginx"]
if len(cvs) == 0 {
t.Error("expected one chart version not to be filtered out")
}
for _, v := range cvs {
if v.Version == "0..1.0" {
t.Error("malformed version was not filtered out")
}
}
})
}
}
30 changes: 30 additions & 0 deletions internal/helm/testdata/chartmuseum-index.json
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,36 @@
"created": "0001-01-01T00:00:00Z",
"digest": "sha256:1234567890abcdef"
}
],
"xChartWithDuplicateDependenciesAndMissingAlias": [
{
"name": "xChartWithDuplicateDependenciesAndMissingAlias",
"home": "https://example.com/something",
"version": "1.2.3",
"description": "string",
"keywords": [
"broken",
"still accepted"
],
"apiVersion": "v1",
"dependencies": [
{
"name": "kube-rbac-proxy",
"version": "0.9.1",
"repository": ""
},
{
"name": "kube-rbac-proxy",
"version": "0.9.1",
"repository": ""
}
],
"urls": [
"https://kubernetes-charts.storage.googleapis.com/nginx-1.2.3.tgz"
],
"created": "0001-01-01T00:00:00Z",
"digest": "sha256:1234567890abcdef"
}
]
}
}
16 changes: 16 additions & 0 deletions internal/helm/testdata/chartmuseum-index.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,19 @@ entries:
- small
- sumtin
digest: "sha256:1234567890abcdef"
xChartWithDuplicateDependenciesAndMissingAlias:
- name: xChartWithDuplicateDependenciesAndMissingAlias
description: string
version: 1.2.3
home: https://example.com/something
keywords:
- broken
- still accepted
urls:
- https://kubernetes-charts.storage.googleapis.com/nginx-1.2.3.tgz
digest: "sha256:1234567890abcdef"
dependencies:
- name: kube-rbac-proxy
version: "0.9.1"
- name: kube-rbac-proxy
version: "0.9.1"
16 changes: 16 additions & 0 deletions internal/helm/testdata/local-index-unordered.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,19 @@ entries:
- small
- sumtin
digest: "sha256:1234567890abcdef"
xChartWithDuplicateDependenciesAndMissingAlias:
- name: xChartWithDuplicateDependenciesAndMissingAlias
description: string
version: 1.2.3
home: https://example.com/something
keywords:
- broken
- still accepted
urls:
- https://kubernetes-charts.storage.googleapis.com/nginx-1.2.3.tgz
digest: "sha256:1234567890abcdef"
dependencies:
- name: kube-rbac-proxy
version: "0.9.1"
- name: kube-rbac-proxy
version: "0.9.1"
16 changes: 16 additions & 0 deletions internal/helm/testdata/local-index.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,19 @@ entries:
- small
- sumtin
digest: "sha256:1234567890abcdef"
xChartWithDuplicateDependenciesAndMissingAlias:
- name: xChartWithDuplicateDependenciesAndMissingAlias
description: string
version: 1.2.3
home: https://example.com/something
keywords:
- broken
- still accepted
urls:
- https://kubernetes-charts.storage.googleapis.com/nginx-1.2.3.tgz
digest: "sha256:1234567890abcdef"
dependencies:
- name: kube-rbac-proxy
version: "0.9.1"
- name: kube-rbac-proxy
version: "0.9.1"