Repository: helm/helm (go)

Issue:
OCI image annotation org.opencontainers.image.created should use timestamp of pushed TGZ instead of current time
```
helm create mychart
helm package mychart

# initial push [ok]
helm push mychart*tgz oci://registry:5000 --plain-http

# now, following should be a noop, but it pushes new manifest with different digest
helm push mychart*tgz oci://registry:5000 --plain-http
```

subsequent helm push calls always pushes new manifest with different digest because of change in `org.opencontainers.image.created`

There is no change in config (same digest), no change in layers (same digest). And only change in manifest is `org.opencontainers.image.created` annotation.

This, for example, complicates detection of actual change in mutable tags, and produces useless objects in registry.

At this point, helm push operation should be idempotent, it should even skip push if there are no changes to be pushed.

Helm 3.14.3





Pull request description:
Modified how created annotation is populated based on package creation time
Modifies how the `org.opencontainers.image.created` annotation is populated on the OCI artifact to utilize the creation time of the associated package (if available). Otherwise, utilize the current timestamp.

This allows for the same manifest (and associated digest) for pushing the same chart multiple times

Resolves #12884

- [ ] this PR contains documentation
- [x] this PR contains unit tests
- [ ] this PR has been tested for backwards compatibility


Patch:
diff --git a/pkg/pusher/ocipusher.go b/pkg/pusher/ocipusher.go
index 94154d389..b37a0c605 100644
--- a/pkg/pusher/ocipusher.go
+++ b/pkg/pusher/ocipusher.go
@@ -29,6 +29,7 @@ import (
 	"helm.sh/helm/v3/internal/tlsutil"
 	"helm.sh/helm/v3/pkg/chart/loader"
 	"helm.sh/helm/v3/pkg/registry"
+	"helm.sh/helm/v3/pkg/time/ctime"
 )
 
 // OCIPusher is the default OCI backend handler
@@ -89,6 +90,9 @@ func (pusher *OCIPusher) push(chartRef, href string) error {
 		path.Join(strings.TrimPrefix(href, fmt.Sprintf("%s://", registry.OCIScheme)), meta.Metadata.Name),
 		meta.Metadata.Version)
 
+	chartCreationTime := ctime.Created(stat)
+	pushOpts = append(pushOpts, registry.PushOptCreationTime(chartCreationTime.Format(time.RFC3339)))
+
 	_, err = client.Push(chartBytes, ref, pushOpts...)
 	return err
 }
diff --git a/pkg/registry/client.go b/pkg/registry/client.go
index 7538cf69b..a287afaf7 100644
--- a/pkg/registry/client.go
+++ b/pkg/registry/client.go
@@ -527,9 +527,9 @@ type (
 	}
 
 	pushOperation struct {
-		provData   []byte
-		strictMode bool
-		test       bool
+		provData     []byte
+		strictMode   bool
+		creationTime string
 	}
 )
 
@@ -583,7 +583,7 @@ func (c *Client) Push(data []byte, ref string, options ...PushOption) (*PushResu
 		descriptors = append(descriptors, provDescriptor)
 	}
 
-	ociAnnotations := generateOCIAnnotations(meta, operation.test)
+	ociAnnotations := generateOCIAnnotations(meta, operation.creationTime)
 
 	manifestData, manifest, err := content.GenerateManifest(&configDescriptor, ociAnnotations, descriptors...)
 	if err != nil {
@@ -652,10 +652,10 @@ func PushOptStrictMode(strictMode bool) PushOption {
 	}
 }
 
-// PushOptTest returns a function that sets whether test setting on push
-func PushOptTest(test bool) PushOption {
+// PushOptCreationDate returns a function that sets the creation time
+func PushOptCreationTime(creationTime string) PushOption {
 	return func(operation *pushOperation) {
-		operation.test = test
+		operation.creationTime = creationTime
 	}
 }
 
diff --git a/pkg/registry/util.go b/pkg/registry/util.go
index 8baf0852a..4bff39495 100644
--- a/pkg/registry/util.go
+++ b/pkg/registry/util.go
@@ -166,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, test bool) map[string]string {
+func generateOCIAnnotations(meta *chart.Metadata, creationTime string) map[string]string {
 
 	// Get annotations from Chart attributes
-	ociAnnotations := generateChartOCIAnnotations(meta, test)
+	ociAnnotations := generateChartOCIAnnotations(meta, creationTime)
 
 	// Copy Chart annotations
 annotations:
@@ -190,7 +190,7 @@ annotations:
 }
 
 // getChartOCIAnnotations will generate OCI annotations from the provided chart
-func generateChartOCIAnnotations(meta *chart.Metadata, test bool) map[string]string {
+func generateChartOCIAnnotations(meta *chart.Metadata, creationTime string) map[string]string {
 	chartOCIAnnotations := map[string]string{}
 
 	chartOCIAnnotations = addToMap(chartOCIAnnotations, ocispec.AnnotationDescription, meta.Description)
@@ -198,10 +198,12 @@ func generateChartOCIAnnotations(meta *chart.Metadata, test bool) map[string]str
 	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(creationTime) == 0 {
+		creationTime = helmtime.Now().UTC().Format(time.RFC3339)
 	}
 
+	chartOCIAnnotations = addToMap(chartOCIAnnotations, ocispec.AnnotationCreated, creationTime)
+
 	if len(meta.Sources) > 0 {
 		chartOCIAnnotations = addToMap(chartOCIAnnotations, ocispec.AnnotationSource, meta.Sources[0])
 	}
diff --git a/pkg/time/ctime/ctime.go b/pkg/time/ctime/ctime.go
new file mode 100644
index 000000000..f2998d76d
--- /dev/null
+++ b/pkg/time/ctime/ctime.go
@@ -0,0 +1,25 @@
+/*
+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 ctime
+
+import (
+	"os"
+	"time"
+)
+
+func Created(fi os.FileInfo) time.Time {
+	return created(fi)
+}
diff --git a/pkg/time/ctime/ctime_linux.go b/pkg/time/ctime/ctime_linux.go
new file mode 100644
index 000000000..c3cea1d78
--- /dev/null
+++ b/pkg/time/ctime/ctime_linux.go
@@ -0,0 +1,30 @@
+//go:build linux
+
+/*
+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 ctime
+
+import (
+	"os"
+	"syscall"
+	"time"
+)
+
+func created(fi os.FileInfo) time.Time {
+	st := fi.Sys().(*syscall.Stat_t)
+	//nolint
+	return time.Unix(int64(st.Ctim.Sec), int64(st.Ctim.Nsec))
+}
diff --git a/pkg/time/ctime/ctime_other.go b/pkg/time/ctime/ctime_other.go
new file mode 100644
index 000000000..f21ed7347
--- /dev/null
+++ b/pkg/time/ctime/ctime_other.go
@@ -0,0 +1,27 @@
+//go:build !linux
+
+/*
+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 ctime
+
+import (
+	"os"
+	"time"
+)
+
+func created(fi os.FileInfo) time.Time {
+	return fi.ModTime()
+}


Test patch:
diff --git a/pkg/registry/util_test.go b/pkg/registry/util_test.go
index fdf09360b..908ea4950 100644
--- a/pkg/registry/util_test.go
+++ b/pkg/registry/util_test.go
@@ -29,6 +29,8 @@ import (
 
 func TestGenerateOCIChartAnnotations(t *testing.T) {
 
+	nowString := helmtime.Now().Format(time.RFC3339)
+
 	tests := []struct {
 		name   string
 		chart  *chart.Metadata
@@ -43,6 +45,7 @@ func TestGenerateOCIChartAnnotations(t *testing.T) {
 			map[string]string{
 				"org.opencontainers.image.title":   "oci",
 				"org.opencontainers.image.version": "0.0.1",
+				"org.opencontainers.image.created": nowString,
 			},
 		},
 		{
@@ -56,6 +59,7 @@ func TestGenerateOCIChartAnnotations(t *testing.T) {
 			map[string]string{
 				"org.opencontainers.image.title":       "oci",
 				"org.opencontainers.image.version":     "0.0.1",
+				"org.opencontainers.image.created":     nowString,
 				"org.opencontainers.image.description": "OCI Helm Chart",
 				"org.opencontainers.image.url":         "https://helm.sh",
 			},
@@ -76,6 +80,7 @@ func TestGenerateOCIChartAnnotations(t *testing.T) {
 			map[string]string{
 				"org.opencontainers.image.title":       "oci",
 				"org.opencontainers.image.version":     "0.0.1",
+				"org.opencontainers.image.created":     nowString,
 				"org.opencontainers.image.description": "OCI Helm Chart",
 				"org.opencontainers.image.url":         "https://helm.sh",
 				"org.opencontainers.image.authors":     "John Snow",
@@ -95,6 +100,7 @@ func TestGenerateOCIChartAnnotations(t *testing.T) {
 			map[string]string{
 				"org.opencontainers.image.title":       "oci",
 				"org.opencontainers.image.version":     "0.0.1",
+				"org.opencontainers.image.created":     nowString,
 				"org.opencontainers.image.description": "OCI Helm Chart",
 				"org.opencontainers.image.url":         "https://helm.sh",
 				"org.opencontainers.image.authors":     "John Snow (john@winterfell.com)",
@@ -115,6 +121,7 @@ func TestGenerateOCIChartAnnotations(t *testing.T) {
 			map[string]string{
 				"org.opencontainers.image.title":       "oci",
 				"org.opencontainers.image.version":     "0.0.1",
+				"org.opencontainers.image.created":     nowString,
 				"org.opencontainers.image.description": "OCI Helm Chart",
 				"org.opencontainers.image.url":         "https://helm.sh",
 				"org.opencontainers.image.authors":     "John Snow (john@winterfell.com), Jane Snow",
@@ -133,6 +140,7 @@ func TestGenerateOCIChartAnnotations(t *testing.T) {
 			map[string]string{
 				"org.opencontainers.image.title":       "oci",
 				"org.opencontainers.image.version":     "0.0.1",
+				"org.opencontainers.image.created":     nowString,
 				"org.opencontainers.image.description": "OCI Helm Chart",
 				"org.opencontainers.image.source":      "https://github.com/helm/helm",
 			},
@@ -141,7 +149,7 @@ func TestGenerateOCIChartAnnotations(t *testing.T) {
 
 	for _, tt := range tests {
 
-		result := generateChartOCIAnnotations(tt.chart, true)
+		result := generateChartOCIAnnotations(tt.chart, nowString)
 
 		if !reflect.DeepEqual(tt.expect, result) {
 			t.Errorf("%s: expected map %v, got %v", tt.name, tt.expect, result)
@@ -152,6 +160,8 @@ func TestGenerateOCIChartAnnotations(t *testing.T) {
 
 func TestGenerateOCIAnnotations(t *testing.T) {
 
+	nowString := helmtime.Now().Format(time.RFC3339)
+
 	tests := []struct {
 		name   string
 		chart  *chart.Metadata
@@ -166,6 +176,7 @@ func TestGenerateOCIAnnotations(t *testing.T) {
 			map[string]string{
 				"org.opencontainers.image.title":   "oci",
 				"org.opencontainers.image.version": "0.0.1",
+				"org.opencontainers.image.created": nowString,
 			},
 		},
 		{
@@ -183,6 +194,7 @@ func TestGenerateOCIAnnotations(t *testing.T) {
 				"org.opencontainers.image.title":       "oci",
 				"org.opencontainers.image.version":     "0.0.1",
 				"org.opencontainers.image.description": "OCI Helm Chart",
+				"org.opencontainers.image.created":     nowString,
 				"extrakey":                             "extravlue",
 				"anotherkey":                           "anothervalue",
 			},
@@ -203,6 +215,7 @@ func TestGenerateOCIAnnotations(t *testing.T) {
 				"org.opencontainers.image.title":       "oci",
 				"org.opencontainers.image.version":     "0.0.1",
 				"org.opencontainers.image.description": "OCI Helm Chart",
+				"org.opencontainers.image.created":     nowString,
 				"extrakey":                             "extravlue",
 			},
 		},
@@ -210,7 +223,7 @@ func TestGenerateOCIAnnotations(t *testing.T) {
 
 	for _, tt := range tests {
 
-		result := generateOCIAnnotations(tt.chart, true)
+		result := generateOCIAnnotations(tt.chart, nowString)
 
 		if !reflect.DeepEqual(tt.expect, result) {
 			t.Errorf("%s: expected map %v, got %v", tt.name, tt.expect, result)
@@ -220,12 +233,16 @@ func TestGenerateOCIAnnotations(t *testing.T) {
 }
 
 func TestGenerateOCICreatedAnnotations(t *testing.T) {
+
+	nowTime := helmtime.Now()
+	nowTimeString := nowTime.Format(time.RFC3339)
+
 	chart := &chart.Metadata{
 		Name:    "oci",
 		Version: "0.0.1",
 	}
 
-	result := generateOCIAnnotations(chart, false)
+	result := generateOCIAnnotations(chart, nowTimeString)
 
 	// Check that created annotation exists
 	if _, ok := result[ocispec.AnnotationCreated]; !ok {
@@ -237,4 +254,22 @@ func TestGenerateOCICreatedAnnotations(t *testing.T) {
 		t.Errorf("%s annotation with value '%s' not in RFC3339 format", ocispec.AnnotationCreated, result[ocispec.AnnotationCreated])
 	}
 
+	// Verify default creation time set
+	result = generateOCIAnnotations(chart, "")
+
+	// Check that created annotation exists
+	if _, ok := result[ocispec.AnnotationCreated]; !ok {
+		t.Errorf("%s annotation not created", ocispec.AnnotationCreated)
+	}
+
+	if createdTimeAnnotation, 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])
+
+		// Verify creation annotation after time test began
+		if !nowTime.Before(createdTimeAnnotation) {
+			t.Errorf("%s annotation with value '%s' not configured properly. Annotation value is not after %s", ocispec.AnnotationCreated, result[ocispec.AnnotationCreated], nowTimeString)
+		}
+
+	}
+
 }
diff --git a/pkg/registry/utils_test.go b/pkg/registry/utils_test.go
index 74aa0dbc0..d7aba2bb7 100644
--- a/pkg/registry/utils_test.go
+++ b/pkg/registry/utils_test.go
@@ -216,9 +216,12 @@ func initCompromisedRegistryTestServer() string {
 }
 
 func testPush(suite *TestSuite) {
+
+	testingChartCreationTime := "1977-09-02T22:04:05Z"
+
 	// Bad bytes
 	ref := fmt.Sprintf("%s/testrepo/testchart:1.2.3", suite.DockerRegistryHost)
-	_, err := suite.RegistryClient.Push([]byte("hello"), ref, PushOptTest(true))
+	_, err := suite.RegistryClient.Push([]byte("hello"), ref, PushOptCreationTime(testingChartCreationTime))
 	suite.NotNil(err, "error pushing non-chart bytes")
 
 	// Load a test chart
@@ -229,20 +232,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, PushOptTest(true))
+	_, err = suite.RegistryClient.Push(chartData, ref, PushOptCreationTime(testingChartCreationTime))
 	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), PushOptTest(true))
+	_, err = suite.RegistryClient.Push(chartData, ref, PushOptStrictMode(false), PushOptCreationTime(testingChartCreationTime))
 	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, PushOptTest(true))
+	_, err = suite.RegistryClient.Push(chartData, ref, PushOptCreationTime(testingChartCreationTime))
 	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), PushOptTest(true))
+	_, err = suite.RegistryClient.Push(chartData, ref, PushOptStrictMode(false), PushOptCreationTime(testingChartCreationTime))
 	suite.Nil(err, "no error pushing non-strict ref (bad tag), with strict mode disabled")
 
 	// basic push, good ref
@@ -251,7 +254,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, PushOptTest(true))
+	_, err = suite.RegistryClient.Push(chartData, ref, PushOptCreationTime(testingChartCreationTime))
 	suite.Nil(err, "no error pushing good ref")
 
 	_, err = suite.RegistryClient.Pull(ref)
@@ -269,7 +272,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), PushOptTest(true))
+	result, err := suite.RegistryClient.Push(chartData, ref, PushOptProvData(provData), PushOptCreationTime(testingChartCreationTime))
 	suite.Nil(err, "no error pushing good ref with prov")
 
 	_, err = suite.RegistryClient.Pull(ref)
@@ -281,12 +284,12 @@ func testPush(suite *TestSuite) {
 	suite.Equal(ref, result.Ref)
 	suite.Equal(meta.Name, result.Chart.Meta.Name)
 	suite.Equal(meta.Version, result.Chart.Meta.Version)
-	suite.Equal(int64(684), result.Manifest.Size)
+	suite.Equal(int64(742), result.Manifest.Size)
 	suite.Equal(int64(99), result.Config.Size)
 	suite.Equal(int64(973), result.Chart.Size)
 	suite.Equal(int64(695), result.Prov.Size)
 	suite.Equal(
-		"sha256:b57e8ffd938c43253f30afedb3c209136288e6b3af3b33473e95ea3b805888e6",
+		"sha256:fbbade96da6050f68f94f122881e3b80051a18f13ab5f4081868dd494538f5c2",
 		result.Manifest.Digest)
 	suite.Equal(
 		"sha256:8d17cb6bf6ccd8c29aace9a658495cbd5e2e87fc267876e86117c7db681c9580",
@@ -354,12 +357,12 @@ func testPull(suite *TestSuite) {
 	suite.Equal(ref, result.Ref)
 	suite.Equal(meta.Name, result.Chart.Meta.Name)
 	suite.Equal(meta.Version, result.Chart.Meta.Version)
-	suite.Equal(int64(684), result.Manifest.Size)
+	suite.Equal(int64(742), result.Manifest.Size)
 	suite.Equal(int64(99), result.Config.Size)
 	suite.Equal(int64(973), result.Chart.Size)
 	suite.Equal(int64(695), result.Prov.Size)
 	suite.Equal(
-		"sha256:b57e8ffd938c43253f30afedb3c209136288e6b3af3b33473e95ea3b805888e6",
+		"sha256:fbbade96da6050f68f94f122881e3b80051a18f13ab5f4081868dd494538f5c2",
 		result.Manifest.Digest)
 	suite.Equal(
 		"sha256:8d17cb6bf6ccd8c29aace9a658495cbd5e2e87fc267876e86117c7db681c9580",
@@ -370,7 +373,7 @@ func testPull(suite *TestSuite) {
 	suite.Equal(
 		"sha256:b0a02b7412f78ae93324d48df8fcc316d8482e5ad7827b5b238657a29a22f256",
 		result.Prov.Digest)
-	suite.Equal("{\"schemaVersion\":2,\"config\":{\"mediaType\":\"application/vnd.cncf.helm.config.v1+json\",\"digest\":\"sha256:8d17cb6bf6ccd8c29aace9a658495cbd5e2e87fc267876e86117c7db681c9580\",\"size\":99},\"layers\":[{\"mediaType\":\"application/vnd.cncf.helm.chart.provenance.v1.prov\",\"digest\":\"sha256:b0a02b7412f78ae93324d48df8fcc316d8482e5ad7827b5b238657a29a22f256\",\"size\":695},{\"mediaType\":\"application/vnd.cncf.helm.chart.content.v1.tar+gzip\",\"digest\":\"sha256:e5ef611620fb97704d8751c16bab17fedb68883bfb0edc76f78a70e9173f9b55\",\"size\":973}],\"annotations\":{\"org.opencontainers.image.description\":\"A Helm chart for Kubernetes\",\"org.opencontainers.image.title\":\"signtest\",\"org.opencontainers.image.version\":\"0.1.0\"}}",
+	suite.Equal("{\"schemaVersion\":2,\"config\":{\"mediaType\":\"application/vnd.cncf.helm.config.v1+json\",\"digest\":\"sha256:8d17cb6bf6ccd8c29aace9a658495cbd5e2e87fc267876e86117c7db681c9580\",\"size\":99},\"layers\":[{\"mediaType\":\"application/vnd.cncf.helm.chart.provenance.v1.prov\",\"digest\":\"sha256:b0a02b7412f78ae93324d48df8fcc316d8482e5ad7827b5b238657a29a22f256\",\"size\":695},{\"mediaType\":\"application/vnd.cncf.helm.chart.content.v1.tar+gzip\",\"digest\":\"sha256:e5ef611620fb97704d8751c16bab17fedb68883bfb0edc76f78a70e9173f9b55\",\"size\":973}],\"annotations\":{\"org.opencontainers.image.created\":\"1977-09-02T22:04:05Z\",\"org.opencontainers.image.description\":\"A Helm chart for Kubernetes\",\"org.opencontainers.image.title\":\"signtest\",\"org.opencontainers.image.version\":\"0.1.0\"}}",
 		string(result.Manifest.Data))
 	suite.Equal("{\"name\":\"signtest\",\"version\":\"0.1.0\",\"description\":\"A Helm chart for Kubernetes\",\"apiVersion\":\"v1\"}",
 		string(result.Config.Data))