Skip to content

Commit

Permalink
Modified how created annotation is populated based on package creatio…
Browse files Browse the repository at this point in the history
…n time

Signed-off-by: Andrew Block <andy.block@gmail.com>
  • Loading branch information
sabre1041 committed Mar 22, 2024
1 parent 976ed8c commit 0a69a0d
Show file tree
Hide file tree
Showing 8 changed files with 153 additions and 27 deletions.
4 changes: 4 additions & 0 deletions pkg/pusher/ocipusher.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
}
Expand Down
14 changes: 7 additions & 7 deletions pkg/registry/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -527,9 +527,9 @@ type (
}

pushOperation struct {
provData []byte
strictMode bool
test bool
provData []byte
strictMode bool
creationTime string
}
)

Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {

This comment has been minimized.

Copy link
@zregvart

zregvart Apr 12, 2024

@sabre1041 a public method was renamed and is breaking backward compatibility

return func(operation *pushOperation) {
operation.test = test
operation.creationTime = creationTime
}
}

Expand Down
12 changes: 7 additions & 5 deletions pkg/registry/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -190,18 +190,20 @@ 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)
chartOCIAnnotations = addToMap(chartOCIAnnotations, ocispec.AnnotationTitle, meta.Name)
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])
}
Expand Down
41 changes: 38 additions & 3 deletions pkg/registry/util_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ import (

func TestGenerateOCIChartAnnotations(t *testing.T) {

nowString := helmtime.Now().Format(time.RFC3339)

tests := []struct {
name string
chart *chart.Metadata
Expand All @@ -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,
},
},
{
Expand All @@ -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",
},
Expand All @@ -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",
Expand All @@ -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)",
Expand All @@ -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",
Expand All @@ -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",
},
Expand All @@ -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)
Expand All @@ -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
Expand All @@ -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,
},
},
{
Expand All @@ -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",
},
Expand All @@ -203,14 +215,15 @@ 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",
},
},
}

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)
Expand All @@ -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 {
Expand All @@ -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)
}

}

}
27 changes: 15 additions & 12 deletions pkg/registry/utils_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -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",
Expand Down Expand Up @@ -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",
Expand All @@ -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))
Expand Down
25 changes: 25 additions & 0 deletions pkg/time/ctime/ctime.go
Original file line number Diff line number Diff line change
@@ -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)
}
30 changes: 30 additions & 0 deletions pkg/time/ctime/ctime_linux.go
Original file line number Diff line number Diff line change
@@ -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))
}
Loading

0 comments on commit 0a69a0d

Please sign in to comment.