From ad732d2896cb9e22de2cc3cf98e46043d38ebd05 Mon Sep 17 00:00:00 2001 From: Billy Lynch Date: Thu, 19 Oct 2023 12:38:18 -0400 Subject: [PATCH] Centralize SOURCE_DATE_EPOCH parsing. Before we could get into weird situations where `SOURCE_DATE_EPOCH=""` particularly with Makefiles doing conditional setting. os.LookupEnv will treat "" as the env being set (https://go.dev/play/p/sOI08BfjdhV) which means that we can get inconsistent behavior where the melanage build will default SOURCE_DATE_EPOCH to 0, but the SBOM generation would not. This consolidates the parsing behavior so we are consistent everywhere we are parsing the environment var. Signed-off-by: Billy Lynch --- pkg/build/build.go | 20 +++------- pkg/sbom/implementation.go | 16 +++----- pkg/util/env.go | 55 ++++++++++++++++++++++++++ pkg/util/env_test.go | 81 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 146 insertions(+), 26 deletions(-) create mode 100644 pkg/util/env.go create mode 100644 pkg/util/env_test.go diff --git a/pkg/build/build.go b/pkg/build/build.go index 3c8fa7168..e70c6afd7 100644 --- a/pkg/build/build.go +++ b/pkg/build/build.go @@ -25,7 +25,6 @@ import ( "os" "path/filepath" "runtime" - "strconv" "strings" "time" @@ -49,6 +48,7 @@ import ( "chainguard.dev/melange/pkg/index" "chainguard.dev/melange/pkg/linter" "chainguard.dev/melange/pkg/sbom" + "chainguard.dev/melange/pkg/util" ) type Build struct { @@ -206,22 +206,12 @@ func New(ctx context.Context, opts ...Option) (*Build, error) { } // SOURCE_DATE_EPOCH will always overwrite the build flag - if v, ok := os.LookupEnv("SOURCE_DATE_EPOCH"); ok { - if v == "" { - b.Logger.Warnf("SOURCE_DATE_EPOCH is specified but empty, setting it to 0") - v = "0" - } - // The value MUST be an ASCII representation of an integer - // with no fractional component, identical to the output - // format of date +%s. - sec, err := strconv.ParseInt(v, 10, 64) + if _, ok := os.LookupEnv("SOURCE_DATE_EPOCH"); ok { + t, err := util.SourceDateEpochWithLogger(b.Logger, b.SourceDateEpoch) if err != nil { - // If the value is malformed, the build process - // SHOULD exit with a non-zero error code. - return nil, fmt.Errorf("failed to parse SOURCE_DATE_EPOCH: %w", err) + return nil, err } - - b.SourceDateEpoch = time.Unix(sec, 0) + b.SourceDateEpoch = t } // Check that we actually can run things in containers. diff --git a/pkg/sbom/implementation.go b/pkg/sbom/implementation.go index 0c6399828..af45ef8f6 100644 --- a/pkg/sbom/implementation.go +++ b/pkg/sbom/implementation.go @@ -27,7 +27,6 @@ import ( "path/filepath" "regexp" "sort" - "strconv" "strings" "sync" "time" @@ -39,6 +38,7 @@ import ( "sigs.k8s.io/release-utils/version" "chainguard.dev/apko/pkg/sbom/generator/spdx" + "chainguard.dev/melange/pkg/util" ) type generatorImplementation interface { @@ -372,15 +372,9 @@ func sbomHasRelationship(spdxDoc *spdx.Document, bomRel relationship) bool { // buildDocumentSPDX creates an SPDX 2.3 document from our generic representation func buildDocumentSPDX(spec *Spec, doc *bom) (*spdx.Document, error) { // Build the SBOM time, but respect SOURCE_DATE_EPOCH - sbomTime := time.Now().UTC().Format(time.RFC3339) - if v, ok := os.LookupEnv("SOURCE_DATE_EPOCH"); ok { - sec, err := strconv.ParseInt(v, 10, 64) - if err != nil { - return nil, fmt.Errorf("failed to parse SOURCE_DATE_EPOCH: %w", err) - } - - t := time.Unix(sec, 0) - sbomTime = t.UTC().Format(time.RFC3339) + sbomTime, err := util.SourceDateEpoch(time.Now().UTC()) + if err != nil { + return nil, err } spdxDoc := spdx.Document{ @@ -388,7 +382,7 @@ func buildDocumentSPDX(spec *Spec, doc *bom) (*spdx.Document, error) { Name: fmt.Sprintf("apk-%s-%s", spec.PackageName, spec.PackageVersion), Version: "SPDX-2.3", CreationInfo: spdx.CreationInfo{ - Created: sbomTime, + Created: sbomTime.Format(time.RFC3339), Creators: []string{ fmt.Sprintf("Tool: melange (%s)", version.GetVersionInfo().GitVersion), "Organization: Chainguard, Inc", diff --git a/pkg/util/env.go b/pkg/util/env.go new file mode 100644 index 000000000..234d21aca --- /dev/null +++ b/pkg/util/env.go @@ -0,0 +1,55 @@ +// Copyright 2023 Chainguard, Inc. +// +// 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 util + +import ( + "fmt" + "os" + "strconv" + "strings" + "time" + + "chainguard.dev/apko/pkg/log" + "chainguard.dev/melange/pkg/logger" +) + +// SourceDateEpoch parses the SOURCE_DATE_EPOCH environment variable. +// If it is not set, it returns the defaultTime. +// If it is set, it MUST be an ASCII representation of an integer. +// If it is malformed, it returns an error. +func SourceDateEpoch(defaultTime time.Time) (time.Time, error) { + return SourceDateEpochWithLogger(logger.NopLogger{}, defaultTime) +} + +// SourceDateEpochWithLogger is the same as SourceDateEpoch but will log warning messages +// to the provided logger. +func SourceDateEpochWithLogger(l log.Logger, defaultTime time.Time) (time.Time, error) { + v := strings.TrimSpace(os.Getenv("SOURCE_DATE_EPOCH")) + if v == "" { + l.Warnf("SOURCE_DATE_EPOCH is specified but empty, setting it to %v", defaultTime) + return defaultTime, nil + } + + // The value MUST be an ASCII representation of an integer + // with no fractional component, identical to the output + // format of date +%s. + sec, err := strconv.ParseInt(v, 10, 64) + if err != nil { + // If the value is malformed, the build process + // SHOULD exit with a non-zero error code. + return defaultTime, fmt.Errorf("failed to parse SOURCE_DATE_EPOCH: %w", err) + } + + return time.Unix(sec, 0), nil +} diff --git a/pkg/util/env_test.go b/pkg/util/env_test.go new file mode 100644 index 000000000..ccc613fc1 --- /dev/null +++ b/pkg/util/env_test.go @@ -0,0 +1,81 @@ +// Copyright 2023 Chainguard, Inc. +// +// 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 util + +import ( + "testing" + "time" +) + +func TestSourceDateEpoch(t *testing.T) { + tests := []struct { + name string + sourceDateEpoch string + defaultTime time.Time + want time.Time + wantErr bool + }{ + { + name: "empty", + defaultTime: time.Time{}, + want: time.Time{}, + }, + { + name: "strings", + sourceDateEpoch: " ", + defaultTime: time.Time{}, + want: time.Time{}, + }, + { + name: "defaultTime", + defaultTime: time.Unix(1234567890, 0), + want: time.Unix(1234567890, 0), + }, + { + name: "0", + sourceDateEpoch: "0", + defaultTime: time.Unix(1234567890, 0), + want: time.Unix(0, 0), + }, + { + name: "1234567890", + sourceDateEpoch: "1234567890", + defaultTime: time.Unix(0, 0), + want: time.Unix(1234567890, 0), + }, + { + name: "invalid date", + sourceDateEpoch: "tacocat", + defaultTime: time.Unix(0, 0), + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.sourceDateEpoch != "" { + t.Setenv("SOURCE_DATE_EPOCH", tt.sourceDateEpoch) + } + got, err := SourceDateEpoch(tt.defaultTime) + if err != nil { + if !tt.wantErr { + t.Fatalf("SourceDateEpoch() error = %v, wantErr %v", err, tt.wantErr) + } + return + } + if !got.Equal(tt.want) { + t.Errorf("SourceDateEpoch() = %v, want %v", got, tt.want) + } + }) + } +}