pull/31837/merge
Varun Chawla 1 day ago committed by GitHub
commit df483d6fc4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -34,6 +34,7 @@ import (
"helm.sh/helm/v4/pkg/cli/output"
"helm.sh/helm/v4/pkg/cmd/search"
"helm.sh/helm/v4/pkg/helmpath"
"helm.sh/helm/v4/pkg/registry"
"helm.sh/helm/v4/pkg/repo/v1"
)
@ -42,10 +43,17 @@ Search reads through all of the repositories configured on the system, and
looks for matches. Search of these repositories uses the metadata stored on
the system.
Repositories are managed with 'helm repo' commands.
It will display the latest stable versions of the charts found. If you
specify the --devel flag, the output will include pre-release versions.
If you want to search using a version constraint, use --version.
OCI registries are also supported. You can search for chart versions in an
OCI registry by providing an oci:// reference:
$ helm search repo oci://registry/repo/chart --versions
Examples:
# Search for stable release versions matching the keyword "nginx"
@ -57,7 +65,11 @@ Examples:
# Search for the latest stable release for nginx-ingress with a major version of 1
$ helm search repo nginx-ingress --version ^1.0.0
Repositories are managed with 'helm repo' commands.
# List all available versions of a chart in an OCI registry
$ helm search repo oci://ghcr.io/org/charts/mychart --versions
# Search for the latest stable release of an OCI chart
$ helm search repo oci://ghcr.io/org/charts/mychart
`
// searchMaxScore suggests that any score higher than this is not considered a match.
@ -73,6 +85,15 @@ type searchRepoOptions struct {
repoCacheDir string
outputFormat output.Format
failOnNoResult bool
// OCI-related options
certFile string
keyFile string
caFile string
insecureSkipTLSverify bool
plainHTTP bool
username string
password string
}
func newSearchRepoCmd(out io.Writer) *cobra.Command {
@ -97,6 +118,15 @@ func newSearchRepoCmd(out io.Writer) *cobra.Command {
f.UintVar(&o.maxColWidth, "max-col-width", 50, "maximum column width for output table")
f.BoolVar(&o.failOnNoResult, "fail-on-no-result", false, "search fails if no results are found")
// OCI-related flags
f.StringVar(&o.certFile, "cert-file", "", "identify registry client using this SSL certificate file")
f.StringVar(&o.keyFile, "key-file", "", "identify registry client using this SSL key file")
f.StringVar(&o.caFile, "ca-file", "", "verify certificates of HTTPS-enabled servers using this CA bundle")
f.BoolVar(&o.insecureSkipTLSverify, "insecure-skip-tls-verify", false, "skip tls certificate checks for the chart download")
f.BoolVar(&o.plainHTTP, "plain-http", false, "use insecure HTTP connections for the chart download")
f.StringVar(&o.username, "username", "", "chart repository username")
f.StringVar(&o.password, "password", "", "chart repository password")
bindOutputFlag(cmd, &o.outputFormat)
return cmd
@ -105,6 +135,11 @@ func newSearchRepoCmd(out io.Writer) *cobra.Command {
func (o *searchRepoOptions) run(out io.Writer, args []string) error {
o.setupSearchedVersion()
// Check if any argument is an OCI reference
if len(args) > 0 && registry.IsOCI(args[0]) {
return o.runOCI(out, args[0])
}
index, err := o.buildIndex()
if err != nil {
return err
@ -130,6 +165,34 @@ func (o *searchRepoOptions) run(out io.Writer, args []string) error {
return o.outputFormat.Write(out, &repoSearchWriter{data, o.maxColWidth, o.failOnNoResult})
}
// runOCI handles searching for chart versions in an OCI registry.
func (o *searchRepoOptions) runOCI(out io.Writer, ociRef string) error {
registryClient, err := newRegistryClient(
o.certFile, o.keyFile, o.caFile,
o.insecureSkipTLSverify, o.plainHTTP,
o.username, o.password,
)
if err != nil {
return fmt.Errorf("missing registry client: %w", err)
}
// Strip the oci:// prefix for the registry client
ref := strings.TrimPrefix(ociRef, fmt.Sprintf("%s://", registry.OCIScheme))
var searchOpts []registry.SearchOption
searchOpts = append(searchOpts,
registry.SearchOptVersion(o.version),
registry.SearchOptVersions(o.versions),
)
searchResult, err := registryClient.Search(ref, searchOpts...)
if err != nil {
return err
}
return o.outputFormat.Write(out, &ociSearchWriter{searchResult.Charts, o.maxColWidth, o.failOnNoResult})
}
func (o *searchRepoOptions) setupSearchedVersion() {
slog.Debug("original chart version", "version", o.version)
@ -267,6 +330,63 @@ func (r *repoSearchWriter) encodeByFormat(out io.Writer, format output.Format) e
}
}
// ociSearchWriter handles output for OCI registry search results
type ociSearchWriter struct {
results []*registry.SearchResultChart
columnWidth uint
failOnNoResult bool
}
func (r *ociSearchWriter) WriteTable(out io.Writer) error {
if len(r.results) == 0 {
if r.failOnNoResult {
return fmt.Errorf("no results found")
}
_, err := out.Write([]byte("No results found\n"))
if err != nil {
return fmt.Errorf("unable to write results: %s", err)
}
return nil
}
table := uitable.New()
table.MaxColWidth = r.columnWidth
table.AddRow("NAME", "CHART VERSION", "APP VERSION", "DESCRIPTION")
for _, r := range r.results {
table.AddRow(r.Reference, r.Chart.Version, r.Chart.AppVersion, r.Chart.Description)
}
return output.EncodeTable(out, table)
}
func (r *ociSearchWriter) WriteJSON(out io.Writer) error {
return r.encodeByFormat(out, output.JSON)
}
func (r *ociSearchWriter) WriteYAML(out io.Writer) error {
return r.encodeByFormat(out, output.YAML)
}
func (r *ociSearchWriter) encodeByFormat(out io.Writer, format output.Format) error {
if len(r.results) == 0 && r.failOnNoResult {
return fmt.Errorf("no results found")
}
chartList := make([]repoChartElement, 0, len(r.results))
for _, r := range r.results {
chartList = append(chartList, repoChartElement{r.Reference, r.Chart.Version, r.Chart.AppVersion, r.Chart.Description})
}
switch format {
case output.JSON:
return output.EncodeJSON(out, chartList)
case output.YAML:
return output.EncodeYAML(out, chartList)
default:
return nil
}
}
// Provides the list of charts that are part of the specified repo, and that starts with 'prefix'.
func compListChartsOfRepo(repoName string, prefix string) []string {
var charts []string

@ -0,0 +1,174 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package registry // import "helm.sh/helm/v4/pkg/registry"
import (
"encoding/json"
"fmt"
"log/slog"
"strings"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
chart "helm.sh/helm/v4/pkg/chart/v2"
)
type (
// SearchOption allows specifying various settings on search
SearchOption func(*searchOperation)
searchOperation struct {
version string
versions bool
}
// SearchResult is the result returned upon successful search.
SearchResult struct {
Charts []*SearchResultChart `json:"charts"`
}
// SearchResultChart represents a single chart version found in a registry.
SearchResultChart struct {
// Reference is the full OCI reference (e.g., oci://registry/repo/chart)
Reference string `json:"reference"`
// Chart contains the chart metadata extracted from the OCI config layer
Chart *chart.Metadata `json:"chart"`
}
)
// SearchOptVersion sets the version constraint for search
func SearchOptVersion(version string) SearchOption {
return func(operation *searchOperation) {
operation.version = version
}
}
// SearchOptVersions sets whether to return all matching versions
func SearchOptVersions(versions bool) SearchOption {
return func(operation *searchOperation) {
operation.versions = versions
}
}
// Search queries an OCI registry for chart versions matching the given reference.
// It lists all tags for the repository, filters by semver constraint, and fetches
// chart metadata from each matching tag's config layer.
func (c *Client) Search(ref string, options ...SearchOption) (*SearchResult, error) {
searchResult := &SearchResult{
Charts: []*SearchResultChart{},
}
operation := &searchOperation{}
for _, option := range options {
option(operation)
}
// List all tags for the repository
tags, err := c.Tags(ref)
if err != nil {
// If the registry doesn't support tag listing, return empty results
if strings.Contains(err.Error(), "unexpected status code") {
slog.Debug("registry does not support tag listing", slog.String("ref", ref), slog.Any("error", err))
return searchResult, nil
}
return searchResult, err
}
// Filter tags by version constraint
var matchingTags []string
for _, tag := range tags {
match, err := GetTagMatchingVersionOrConstraint([]string{tag}, operation.version)
if err == nil {
matchingTags = append(matchingTags, match)
}
}
parsedRef, err := newReference(ref)
if err != nil {
return searchResult, err
}
ociRef := fmt.Sprintf("%s://%s/%s", OCIScheme, parsedRef.Registry, parsedRef.Repository)
// Fetch chart metadata for each matching tag
for _, tag := range matchingTags {
tagRef := fmt.Sprintf("%s/%s:%s", parsedRef.Registry, parsedRef.Repository, strings.ReplaceAll(tag, "+", "_"))
meta, err := c.fetchChartMetadata(tagRef)
if err != nil {
slog.Debug("failed to fetch chart metadata", slog.String("ref", tagRef), slog.Any("error", err))
continue
}
searchResult.Charts = append(searchResult.Charts, &SearchResultChart{
Reference: ociRef,
Chart: meta,
})
// If not listing all versions, return only the latest (first match, since tags are sorted descending)
if !operation.versions {
break
}
}
return searchResult, nil
}
// fetchChartMetadata pulls only the config layer from an OCI manifest to extract chart metadata.
// This avoids downloading the full chart tarball.
func (c *Client) fetchChartMetadata(ref string) (*chart.Metadata, error) {
genericClient := c.Generic()
// Only fetch the manifest and config layer, skip the chart tarball
genericResult, err := genericClient.PullGeneric(ref, GenericPullOptions{
AllowedMediaTypes: []string{
ocispec.MediaTypeImageManifest,
ocispec.MediaTypeImageIndex,
ConfigMediaType,
},
})
if err != nil {
return nil, err
}
// Find the config descriptor
var configDescriptor *ocispec.Descriptor
for _, desc := range genericResult.Descriptors {
d := desc
if d.MediaType == ConfigMediaType {
configDescriptor = &d
break
}
}
if configDescriptor == nil {
return nil, fmt.Errorf("could not find config layer with mediatype %s", ConfigMediaType)
}
// Fetch and parse the config data
configData, err := genericClient.GetDescriptorData(genericResult.MemoryStore, *configDescriptor)
if err != nil {
return nil, fmt.Errorf("unable to retrieve config blob: %w", err)
}
var meta chart.Metadata
if err := json.Unmarshal(configData, &meta); err != nil {
return nil, fmt.Errorf("unable to parse chart metadata: %w", err)
}
return &meta, nil
}

@ -0,0 +1,67 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package registry
import (
"testing"
"github.com/stretchr/testify/assert"
chart "helm.sh/helm/v4/pkg/chart/v2"
)
func TestSearchOptVersion(t *testing.T) {
op := &searchOperation{}
SearchOptVersion(">=1.0.0")(op)
assert.Equal(t, ">=1.0.0", op.version)
}
func TestSearchOptVersions(t *testing.T) {
op := &searchOperation{}
SearchOptVersions(true)(op)
assert.True(t, op.versions)
}
func TestSearchResult(t *testing.T) {
result := &SearchResult{
Charts: []*SearchResultChart{
{
Reference: "oci://ghcr.io/org/charts/mychart",
Chart: &chart.Metadata{
Name: "mychart",
Version: "1.2.0",
AppVersion: "2.0.0",
Description: "A test chart",
},
},
{
Reference: "oci://ghcr.io/org/charts/mychart",
Chart: &chart.Metadata{
Name: "mychart",
Version: "1.1.0",
AppVersion: "1.9.0",
Description: "A test chart",
},
},
},
}
assert.Equal(t, 2, len(result.Charts))
assert.Equal(t, "1.2.0", result.Charts[0].Chart.Version)
assert.Equal(t, "1.1.0", result.Charts[1].Chart.Version)
assert.Equal(t, "oci://ghcr.io/org/charts/mychart", result.Charts[0].Reference)
}
Loading…
Cancel
Save