Arnav Nagzirkar 2 days ago committed by GitHub
commit 8efaadae62
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -19,6 +19,7 @@ package action
import (
"bytes"
"fmt"
"path"
"strings"
"k8s.io/cli-runtime/pkg/printers"
@ -61,7 +62,10 @@ type Show struct {
Devel bool
OutputFormat ShowOutputFormat
JSONPathTemplate string
chart *chart.Chart // for testing
// OCIRef is the original OCI reference (without oci:// scheme) used to fetch
// manifest annotations when showing an OCI chart. Leave empty for non-OCI charts.
OCIRef string
chart *chart.Chart // for testing
}
// NewShow creates a new Show object with the given configuration.
@ -96,6 +100,25 @@ func (s *Show) Run(chartpath string) (string, error) {
var out strings.Builder
if s.OutputFormat == ShowChart || s.OutputFormat == ShowAll {
fmt.Fprintf(&out, "%s\n", cf)
if s.OCIRef != "" && s.registryClient != nil {
ref := s.OCIRef
// When the reference has no explicit tag or digest, fall back to the
// resolved chart version. Helm stores an OCI chart under a tag that
// matches its version, and s.chart.Metadata.Version is the concrete
// version actually downloaded (the requested version may be a range).
if s.chart.Metadata != nil && s.chart.Metadata.Version != "" &&
!strings.Contains(path.Base(ref), ":") && !strings.Contains(ref, "@") {
ref = fmt.Sprintf("%s:%s", ref, s.chart.Metadata.Version)
}
annotations, err := s.registryClient.GetManifestAnnotations(ref)
if err == nil && len(annotations) > 0 {
annotationsYAML, marshalErr := yaml.Marshal(annotations)
if marshalErr == nil {
fmt.Fprintln(&out, "---")
fmt.Fprintf(&out, "%s\n", annotationsYAML)
}
}
}
}
if (s.OutputFormat == ShowValues || s.OutputFormat == ShowAll) && s.chart.Values != nil {

@ -17,10 +17,15 @@ limitations under the License.
package action
import (
"io"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"helm.sh/helm/v4/pkg/chart/common"
chart "helm.sh/helm/v4/pkg/chart/v2"
@ -180,3 +185,129 @@ func TestShowSetRegistryClient(t *testing.T) {
client.SetRegistryClient(registryClient)
assert.Equal(t, registryClient, client.registryClient)
}
func TestShowOCIAnnotations(t *testing.T) {
const manifestJSON = `{
"schemaVersion": 2,
"mediaType": "application/vnd.oci.image.manifest.v1+json",
"config": {
"mediaType": "application/vnd.cncf.helm.config.v1+json",
"digest": "sha256:abc123",
"size": 100
},
"layers": [],
"annotations": {
"org.opencontainers.image.created": "2025-04-11T20:12:25Z"
}
}`
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/v2/" {
w.WriteHeader(http.StatusOK)
return
}
if strings.Contains(r.URL.Path, "/manifests/") {
w.Header().Set("Content-Type", "application/vnd.oci.image.manifest.v1+json")
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(manifestJSON))
return
}
http.NotFound(w, r)
}))
defer srv.Close()
host := strings.TrimPrefix(srv.URL, "http://")
registryClient, err := registry.NewClient(
registry.ClientOptWriter(io.Discard),
registry.ClientOptPlainHTTP(),
)
require.NoError(t, err)
config := actionConfigFixture(t)
client := NewShow(ShowChart, config)
client.SetRegistryClient(registryClient)
client.OCIRef = host + "/test/chart:1.0.0"
client.chart = &chart.Chart{
Metadata: &chart.Metadata{Name: "test-chart", Version: "1.0.0"},
}
output, err := client.Run("")
require.NoError(t, err)
assert.Contains(t, output, "---")
assert.Contains(t, output, "org.opencontainers.image.created")
assert.Contains(t, output, "2025-04-11T20:12:25Z")
}
func TestShowOCIAnnotationsNoRegistry(t *testing.T) {
config := actionConfigFixture(t)
client := NewShow(ShowChart, config)
// OCIRef set but no registry client - should not fail, just omit annotations
client.OCIRef = "host/test/chart:1.0.0"
client.chart = &chart.Chart{
Metadata: &chart.Metadata{Name: "test-chart", Version: "1.0.0"},
}
output, err := client.Run("")
require.NoError(t, err)
// No annotations section, no "---" separator
assert.NotContains(t, output, "---")
}
func TestShowOCIAnnotationsResolvesVersion(t *testing.T) {
const manifestJSON = `{
"schemaVersion": 2,
"mediaType": "application/vnd.oci.image.manifest.v1+json",
"config": {
"mediaType": "application/vnd.cncf.helm.config.v1+json",
"digest": "sha256:abc123",
"size": 100
},
"layers": [],
"annotations": {
"org.opencontainers.image.created": "2025-04-11T20:12:25Z"
}
}`
var requestedManifest string
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/v2/" {
w.WriteHeader(http.StatusOK)
return
}
if strings.Contains(r.URL.Path, "/manifests/") {
requestedManifest = r.URL.Path
w.Header().Set("Content-Type", "application/vnd.oci.image.manifest.v1+json")
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(manifestJSON))
return
}
http.NotFound(w, r)
}))
defer srv.Close()
host := strings.TrimPrefix(srv.URL, "http://")
registryClient, err := registry.NewClient(
registry.ClientOptWriter(io.Discard),
registry.ClientOptPlainHTTP(),
)
require.NoError(t, err)
config := actionConfigFixture(t)
client := NewShow(ShowChart, config)
client.SetRegistryClient(registryClient)
// OCIRef has no explicit tag; the concrete tag is taken from the chart version.
client.OCIRef = host + "/test/chart"
client.chart = &chart.Chart{
Metadata: &chart.Metadata{Name: "test-chart", Version: "1.0.0"},
}
output, err := client.Run("")
require.NoError(t, err)
assert.Contains(t, output, "org.opencontainers.image.created")
assert.Contains(t, output, "2025-04-11T20:12:25Z")
assert.Contains(t, requestedManifest, "/manifests/1.0.0")
}

@ -21,11 +21,13 @@ import (
"io"
"log"
"log/slog"
"strings"
"github.com/spf13/cobra"
"helm.sh/helm/v4/pkg/action"
"helm.sh/helm/v4/pkg/cmd/require"
"helm.sh/helm/v4/pkg/registry"
)
const showDesc = `
@ -218,6 +220,14 @@ func runShow(args []string, client *action.Show) (string, error) {
client.Version = ">0.0.0-0"
}
if registry.IsOCI(args[0]) {
// Store the bare OCI reference (without the oci:// scheme). The concrete
// tag is resolved from the downloaded chart version in Show.Run, because
// client.Version may be a range (for example ">0.0.0-0" with --devel)
// rather than an exact tag.
client.OCIRef = strings.TrimPrefix(args[0], registry.OCIScheme+"://")
}
cp, err := client.LocateChart(args[0], settings)
if err != nil {
return "", err

@ -910,6 +910,36 @@ func (c *Client) ValidateReference(ref, version string, u *url.URL) (string, *ur
return "", u, err
}
// GetManifestAnnotations fetches the OCI manifest annotations for the given reference.
// It returns nil, nil when no annotations are present. The caller should treat errors as
// non-fatal and proceed without annotations.
func (c *Client) GetManifestAnnotations(ref string) (map[string]string, error) {
parsedRef, err := newReference(ref)
if err != nil {
return nil, err
}
remoteRepository, err := remote.NewRepository(parsedRef.String())
if err != nil {
return nil, err
}
remoteRepository.PlainHTTP = c.plainHTTP
remoteRepository.Client = c.authorizer
ctx := context.Background()
_, rc, err := remoteRepository.FetchReference(ctx, parsedRef.String())
if err != nil {
return nil, err
}
defer rc.Close()
var manifest ocispec.Manifest
if err := json.NewDecoder(rc).Decode(&manifest); err != nil {
return nil, err
}
return manifest.Annotations, nil
}
// tagManifest prepares and tags a manifest in memory storage
func (c *Client) tagManifest(ctx context.Context, memoryStore *memory.Store,
configDescriptor ocispec.Descriptor, layers []ocispec.Descriptor,

Loading…
Cancel
Save