Repository: helm/helm (go)

Issue:
helm oci image doesn't contain 'org.opencontainers.image.created'
Used helm version: v3.12.0 

Pushing a helm chart to an oci-registry does not create the annotation `org.opencontainers.image.created`. This would be very helpful to add this annotation to the oci-image on helm push command.

status quo by pulling oci-image from a docker registry:

```
curl --location 'https://<dockerregistry>/v2/helm-charts/<imagename>/manifests/<imagetag>' --header 'Accept: application/vnd.docker.distribution.manifest.v2+json, application/vnd.docker.distribution.manifest.list.v2+json, application/vnd.oci.image.manifest.v1+json, application/vnd.oci.image.index.v1+json, */*' 


{
    "schemaVersion": 2,
    "config": {
        "mediaType": "application/vnd.cncf.helm.config.v1+json",
        "digest": "sha256:0d69a88fd9c9e6b528314359b6a2ab69c1f407498c2ed27589a033173c5693ba",
        "size": 177
    },
    "layers": [
        {
            "mediaType": "application/vnd.cncf.helm.chart.content.v1.tar+gzip",
            "digest": "sha256:af3469083a43166ebdb85c8d5fdc25521fd10fb92488694cdbc7977f921a4d46",
            "size": 3399
        }
    ],
    "annotations": {
        "org.opencontainers.image.description": "Description",
        "org.opencontainers.image.title": "myImageName",
        "org.opencontainers.image.version": "myImageTag"
    }
}


```

`org.opencontainers.image.created` is missing

A possible code change would be on …
https://github.com/helm/helm/blob/4e447d87cd3222aa5e5ea2b38170e9d80b99c0b4/pkg/registry/util.go#LL196C1-L196C1 

… by adding  line:
`	chartOCIAnnotations = addToMap(chartOCIAnnotations, ocispec.AnnotationCreated, time.Now())`



Pull request:
Add created OCI annotation
Added OCI annotation `org.opencontainers.image.created` to resolve #12159

**If applicable**:
- [ ] this PR contains documentation
- [x] this PR contains unit tests
- [ ] this PR has been tested for backwards compatibility


Patch:
diff --git a/pkg/registry/client.go b/pkg/registry/client.go
index f9569b62b..509f82d4a 100644
--- a/pkg/registry/client.go
+++ b/pkg/registry/client.go
@@ -498,6 +498,7 @@ type (
 	pushOperation struct {
 		provData   []byte
 		strictMode bool
+		test       bool
 	}
 )
 
@@ -551,7 +552,7 @@ func (c *Client) Push(data []byte, ref string, options ...PushOption) (*PushResu
 		descriptors = append(descriptors, provDescriptor)
 	}
 
-	ociAnnotations := generateOCIAnnotations(meta)
+	ociAnnotations := generateOCIAnnotations(meta, operation.test)
 
 	manifestData, manifest, err := content.GenerateManifest(&configDescriptor, ociAnnotations, descriptors...)
 	if err != nil {
@@ -616,6 +617,13 @@ func PushOptStrictMode(strictMode bool) PushOption {
 	}
 }
 
+// PushOptTest returns a function that sets whether test setting on push
+func PushOptTest(test bool) PushOption {
+	return func(operation *pushOperation) {
+		operation.test = test
+	}
+}
+
 // Tags provides a sorted list all semver compliant tags for a given repository
 func (c *Client) Tags(ref string) ([]string, error) {
 	parsedReference, err := registry.ParseReference(ref)
diff --git a/pkg/registry/util.go b/pkg/registry/util.go
index a4b90c80c..8baf0852a 100644
--- a/pkg/registry/util.go
+++ b/pkg/registry/util.go
@@ -23,6 +23,9 @@ import (
 	"io"
 	"net/http"
 	"strings"
+	"time"
+
+	helmtime "helm.sh/helm/v3/pkg/time"
 
 	"github.com/Masterminds/semver/v3"
 	ocispec "github.com/opencontainers/image-spec/specs-go/v1"
@@ -163,10 +166,10 @@ func NewRegistryClientWithTLS(out io.Writer, certFile, keyFile, caFile string, i
 }
 
 // generateOCIAnnotations will generate OCI annotations to include within the OCI manifest
-func generateOCIAnnotations(meta *chart.Metadata) map[string]string {
+func generateOCIAnnotations(meta *chart.Metadata, test bool) map[string]string {
 
 	// Get annotations from Chart attributes
-	ociAnnotations := generateChartOCIAnnotations(meta)
+	ociAnnotations := generateChartOCIAnnotations(meta, test)
 
 	// Copy Chart annotations
 annotations:
@@ -187,7 +190,7 @@ annotations:
 }
 
 // getChartOCIAnnotations will generate OCI annotations from the provided chart
-func generateChartOCIAnnotations(meta *chart.Metadata) map[string]string {
+func generateChartOCIAnnotations(meta *chart.Metadata, test bool) map[string]string {
 	chartOCIAnnotations := map[string]string{}
 
 	chartOCIAnnotations = addToMap(chartOCIAnnotations, ocispec.AnnotationDescription, meta.Description)
@@ -195,6 +198,10 @@ func generateChartOCIAnnotations(meta *chart.Metadata) map[string]string {
 	chartOCIAnnotations = addToMap(chartOCIAnnotations, ocispec.AnnotationVersion, meta.Version)
 	chartOCIAnnotations = addToMap(chartOCIAnnotations, ocispec.AnnotationURL, meta.Home)
 
+	if !test {
+		chartOCIAnnotations = addToMap(chartOCIAnnotations, ocispec.AnnotationCreated, helmtime.Now().UTC().Format(time.RFC3339))
+	}
+
 	if len(meta.Sources) > 0 {
 		chartOCIAnnotations = addToMap(chartOCIAnnotations, ocispec.AnnotationSource, meta.Sources[0])
 	}


Test patch:
diff --git a/pkg/registry/util_test.go b/pkg/registry/util_test.go
index 3066257ad..fdf09360b 100644
--- a/pkg/registry/util_test.go
+++ b/pkg/registry/util_test.go
@@ -19,8 +19,12 @@ package registry // import "helm.sh/helm/v3/pkg/registry"
 import (
 	"reflect"
 	"testing"
+	"time"
+
+	ocispec "github.com/opencontainers/image-spec/specs-go/v1"
 
 	"helm.sh/helm/v3/pkg/chart"
+	helmtime "helm.sh/helm/v3/pkg/time"
 )
 
 func TestGenerateOCIChartAnnotations(t *testing.T) {
@@ -137,7 +141,7 @@ func TestGenerateOCIChartAnnotations(t *testing.T) {
 
 	for _, tt := range tests {
 
-		result := generateChartOCIAnnotations(tt.chart)
+		result := generateChartOCIAnnotations(tt.chart, true)
 
 		if !reflect.DeepEqual(tt.expect, result) {
 			t.Errorf("%s: expected map %v, got %v", tt.name, tt.expect, result)
@@ -206,7 +210,7 @@ func TestGenerateOCIAnnotations(t *testing.T) {
 
 	for _, tt := range tests {
 
-		result := generateOCIAnnotations(tt.chart)
+		result := generateOCIAnnotations(tt.chart, true)
 
 		if !reflect.DeepEqual(tt.expect, result) {
 			t.Errorf("%s: expected map %v, got %v", tt.name, tt.expect, result)
@@ -214,3 +218,23 @@ func TestGenerateOCIAnnotations(t *testing.T) {
 
 	}
 }
+
+func TestGenerateOCICreatedAnnotations(t *testing.T) {
+	chart := &chart.Metadata{
+		Name:    "oci",
+		Version: "0.0.1",
+	}
+
+	result := generateOCIAnnotations(chart, false)
+
+	// Check that created annotation exists
+	if _, ok := result[ocispec.AnnotationCreated]; !ok {
+		t.Errorf("%s annotation not created", ocispec.AnnotationCreated)
+	}
+
+	// Verify value of created artifact in RFC3339 format
+	if _, err := helmtime.Parse(time.RFC3339, result[ocispec.AnnotationCreated]); err != nil {
+		t.Errorf("%s annotation with value '%s' not in RFC3339 format", ocispec.AnnotationCreated, result[ocispec.AnnotationCreated])
+	}
+
+}
diff --git a/pkg/registry/utils_test.go b/pkg/registry/utils_test.go
index bdeacc712..664a3d84c 100644
--- a/pkg/registry/utils_test.go
+++ b/pkg/registry/utils_test.go
@@ -216,7 +216,7 @@ func initCompromisedRegistryTestServer() string {
 func testPush(suite *TestSuite) {
 	// Bad bytes
 	ref := fmt.Sprintf("%s/testrepo/testchart:1.2.3", suite.DockerRegistryHost)
-	_, err := suite.RegistryClient.Push([]byte("hello"), ref)
+	_, err := suite.RegistryClient.Push([]byte("hello"), ref, PushOptTest(true))
 	suite.NotNil(err, "error pushing non-chart bytes")
 
 	// Load a test chart
@@ -227,20 +227,20 @@ func testPush(suite *TestSuite) {
 
 	// non-strict ref (chart name)
 	ref = fmt.Sprintf("%s/testrepo/boop:%s", suite.DockerRegistryHost, meta.Version)
-	_, err = suite.RegistryClient.Push(chartData, ref)
+	_, err = suite.RegistryClient.Push(chartData, ref, PushOptTest(true))
 	suite.NotNil(err, "error pushing non-strict ref (bad basename)")
 
 	// non-strict ref (chart name), with strict mode disabled
-	_, err = suite.RegistryClient.Push(chartData, ref, PushOptStrictMode(false))
+	_, err = suite.RegistryClient.Push(chartData, ref, PushOptStrictMode(false), PushOptTest(true))
 	suite.Nil(err, "no error pushing non-strict ref (bad basename), with strict mode disabled")
 
 	// non-strict ref (chart version)
 	ref = fmt.Sprintf("%s/testrepo/%s:latest", suite.DockerRegistryHost, meta.Name)
-	_, err = suite.RegistryClient.Push(chartData, ref)
+	_, err = suite.RegistryClient.Push(chartData, ref, PushOptTest(true))
 	suite.NotNil(err, "error pushing non-strict ref (bad tag)")
 
 	// non-strict ref (chart version), with strict mode disabled
-	_, err = suite.RegistryClient.Push(chartData, ref, PushOptStrictMode(false))
+	_, err = suite.RegistryClient.Push(chartData, ref, PushOptStrictMode(false), PushOptTest(true))
 	suite.Nil(err, "no error pushing non-strict ref (bad tag), with strict mode disabled")
 
 	// basic push, good ref
@@ -249,7 +249,7 @@ func testPush(suite *TestSuite) {
 	meta, err = extractChartMeta(chartData)
 	suite.Nil(err, "no error extracting chart meta")
 	ref = fmt.Sprintf("%s/testrepo/%s:%s", suite.DockerRegistryHost, meta.Name, meta.Version)
-	_, err = suite.RegistryClient.Push(chartData, ref)
+	_, err = suite.RegistryClient.Push(chartData, ref, PushOptTest(true))
 	suite.Nil(err, "no error pushing good ref")
 
 	_, err = suite.RegistryClient.Pull(ref)
@@ -267,7 +267,7 @@ func testPush(suite *TestSuite) {
 
 	// push with prov
 	ref = fmt.Sprintf("%s/testrepo/%s:%s", suite.DockerRegistryHost, meta.Name, meta.Version)
-	result, err := suite.RegistryClient.Push(chartData, ref, PushOptProvData(provData))
+	result, err := suite.RegistryClient.Push(chartData, ref, PushOptProvData(provData), PushOptTest(true))
 	suite.Nil(err, "no error pushing good ref with prov")
 
 	_, err = suite.RegistryClient.Pull(ref)