fix: Add support for SOURCE_DATE_EPOCH in reproducible builds

Signed-off-by: Kunal Jain <qlapon@gmail.com>
pull/32060/head
Kunal Jain 6 months ago committed by Kunal Jain
parent 29d309e56b
commit f7591abe2a

@ -25,6 +25,7 @@ import (
"io/fs"
"os"
"path/filepath"
"strconv"
"time"
"sigs.k8s.io/yaml"
@ -234,7 +235,9 @@ func writeToTar(out *tar.Writer, name string, body []byte, modTime time.Time) er
Size: int64(len(body)),
ModTime: modTime,
}
if h.ModTime.IsZero() {
if epoch, ok := sourceDateEpoch(); ok {
h.ModTime = epoch
} else if h.ModTime.IsZero() {
h.ModTime = time.Now()
}
if err := out.WriteHeader(h); err != nil {
@ -244,6 +247,20 @@ func writeToTar(out *tar.Writer, name string, body []byte, modTime time.Time) er
return err
}
// sourceDateEpoch returns the time specified by SOURCE_DATE_EPOCH env var, if set.
// SOURCE_DATE_EPOCH is a Unix timestamp used to ensure reproducible builds.
func sourceDateEpoch() (time.Time, bool) {
s, ok := os.LookupEnv("SOURCE_DATE_EPOCH")
if !ok {
return time.Time{}, false
}
secs, err := strconv.ParseInt(s, 10, 64)
if err != nil {
return time.Time{}, false
}
return time.Unix(secs, 0), true
}
// If the name has directory name has characters which would change the location
// they need to be removed.
func validateName(name string) error {

@ -23,6 +23,7 @@ import (
"crypto/sha256"
"encoding/hex"
"errors"
"fmt"
"io"
"os"
"path"
@ -341,6 +342,60 @@ func TestRepeatableSave(t *testing.T) {
}
}
func TestSourceDateEpoch(t *testing.T) {
epoch := int64(1630000000)
t.Setenv("SOURCE_DATE_EPOCH", fmt.Sprintf("%d", epoch))
mkChart := func(modTime time.Time) *chart.Chart {
return &chart.Chart{
Metadata: &chart.Metadata{
APIVersion: chart.APIVersionV3,
Name: "ahab",
Version: "1.2.3",
},
ModTime: modTime,
Files: []*common.File{
{Name: "scheherazade/shahryar.txt", ModTime: modTime, Data: []byte("1,001 Nights")},
},
}
}
tmp := t.TempDir()
where1, err := Save(mkChart(time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC)), filepath.Join(tmp, "build1"))
if err != nil {
t.Fatal(err)
}
where2, err := Save(mkChart(time.Date(2023, 6, 15, 12, 0, 0, 0, time.UTC)), filepath.Join(tmp, "build2"))
if err != nil {
t.Fatal(err)
}
sum1, err := sha256Sum(where1)
if err != nil {
t.Fatal(err)
}
sum2, err := sha256Sum(where2)
if err != nil {
t.Fatal(err)
}
if sum1 != sum2 {
t.Errorf("builds with same SOURCE_DATE_EPOCH should be byte-identical: %s != %s", sum1, sum2)
}
allHeaders, err := retrieveAllHeadersFromTar(where1)
if err != nil {
t.Fatal(err)
}
expectedTime := time.Unix(epoch, 0)
for _, header := range allHeaders {
if !header.ModTime.Equal(expectedTime) {
t.Errorf("expected timestamp %v, got %v for %s", expectedTime, header.ModTime, header.Name)
}
}
}
func sha256Sum(filePath string) (string, error) {
f, err := os.Open(filePath)
if err != nil {

@ -25,6 +25,7 @@ import (
"io/fs"
"os"
"path/filepath"
"strconv"
"time"
"sigs.k8s.io/yaml"
@ -246,7 +247,9 @@ func writeToTar(out *tar.Writer, name string, body []byte, modTime time.Time) er
Size: int64(len(body)),
ModTime: modTime,
}
if h.ModTime.IsZero() {
if epoch, ok := sourceDateEpoch(); ok {
h.ModTime = epoch
} else if h.ModTime.IsZero() {
h.ModTime = time.Now()
}
if err := out.WriteHeader(h); err != nil {
@ -256,6 +259,20 @@ func writeToTar(out *tar.Writer, name string, body []byte, modTime time.Time) er
return err
}
// sourceDateEpoch returns the time specified by SOURCE_DATE_EPOCH env var, if set.
// SOURCE_DATE_EPOCH is a Unix timestamp used to ensure reproducible builds.
func sourceDateEpoch() (time.Time, bool) {
s, ok := os.LookupEnv("SOURCE_DATE_EPOCH")
if !ok {
return time.Time{}, false
}
secs, err := strconv.ParseInt(s, 10, 64)
if err != nil {
return time.Time{}, false
}
return time.Unix(secs, 0), true
}
// If the name has directory name has characters which would change the location
// they need to be removed.
func validateName(name string) error {

@ -23,6 +23,7 @@ import (
"crypto/sha256"
"encoding/hex"
"errors"
"fmt"
"io"
"os"
"path"
@ -345,6 +346,60 @@ func TestRepeatableSave(t *testing.T) {
}
}
func TestSourceDateEpoch(t *testing.T) {
epoch := int64(1630000000)
t.Setenv("SOURCE_DATE_EPOCH", fmt.Sprintf("%d", epoch))
mkChart := func(modTime time.Time) *chart.Chart {
return &chart.Chart{
Metadata: &chart.Metadata{
APIVersion: chart.APIVersionV2,
Name: "ahab",
Version: "1.2.3",
},
ModTime: modTime,
Files: []*common.File{
{Name: "scheherazade/shahryar.txt", ModTime: modTime, Data: []byte("1,001 Nights")},
},
}
}
tmp := t.TempDir()
where1, err := Save(mkChart(time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC)), filepath.Join(tmp, "build1"))
if err != nil {
t.Fatal(err)
}
where2, err := Save(mkChart(time.Date(2023, 6, 15, 12, 0, 0, 0, time.UTC)), filepath.Join(tmp, "build2"))
if err != nil {
t.Fatal(err)
}
sum1, err := sha256Sum(where1)
if err != nil {
t.Fatal(err)
}
sum2, err := sha256Sum(where2)
if err != nil {
t.Fatal(err)
}
if sum1 != sum2 {
t.Errorf("builds with same SOURCE_DATE_EPOCH should be byte-identical: %s != %s", sum1, sum2)
}
allHeaders, err := retrieveAllHeadersFromTar(where1)
if err != nil {
t.Fatal(err)
}
expectedTime := time.Unix(epoch, 0)
for _, header := range allHeaders {
if !header.ModTime.Equal(expectedTime) {
t.Errorf("expected timestamp %v, got %v for %s", expectedTime, header.ModTime, header.Name)
}
}
}
func sha256Sum(filePath string) (string, error) {
f, err := os.Open(filePath)
if err != nil {

Loading…
Cancel
Save