From a82b461492c7fcee3dce5953b5416956c14c88e9 Mon Sep 17 00:00:00 2001 From: Daniel Mikusa Date: Mon, 11 Nov 2024 08:48:16 -0500 Subject: [PATCH] Backport https://github.com/paketo-buildpacks/libpak/pull/304 Signed-off-by: Daniel Mikusa --- layer.go | 51 ++++++++++++++++++++++++-------- layer_test.go | 82 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 120 insertions(+), 13 deletions(-) diff --git a/layer.go b/layer.go index d406b8f..b7d9fe1 100644 --- a/layer.go +++ b/layer.go @@ -166,29 +166,54 @@ func (l *LayerContributor) checkIfMetadataMatches(layer libcnb.Layer) (map[strin } func (l *LayerContributor) Equals(expectedM map[string]interface{}, layerM map[string]interface{}) (bool, error) { - if dep, ok := expectedM["dependency"].(map[string]interface{}); ok { - for k, v := range dep { - if k == "deprecation_date" { - deprecationDate := v.(time.Time).Truncate(time.Second).In(time.UTC) - dep["deprecation_date"] = deprecationDate - break - } - } + // TODO Do we want the Equals method to modify the underlying maps? Else we need to make a copy here. + + if err := l.normalizeDependencyDeprecationDate(expectedM); err != nil { + return false, fmt.Errorf("%w (expected layer)", err) + } + + if err := l.normalizeDependencyDeprecationDate(layerM); err != nil { + return false, fmt.Errorf("%w (actual layer)", err) } - if dep, ok := layerM["dependency"].(map[string]interface{}); ok { + + return reflect.DeepEqual(expectedM, layerM), nil +} + +// normalizeDependencyDeprecationDate makes sure the dependency deprecation date is represented as a time.Time object +// in the map whenever it exists. +func (l *LayerContributor) normalizeDependencyDeprecationDate(input map[string]interface{}) error { + if dep, ok := input["dependency"].(map[string]interface{}); ok { for k, v := range dep { if k == "deprecation_date" { - deprecationDate, err := time.Parse(time.RFC3339, v.(string)) + deprecationDate, err := l.parseDeprecationDate(v) if err != nil { - return false, fmt.Errorf("unable to parse deprecation_date %s", v.(string)) + return err } - deprecationDate = deprecationDate.Truncate(time.Second).In(time.UTC) dep["deprecation_date"] = deprecationDate break } } } - return reflect.DeepEqual(expectedM, layerM), nil + return nil +} + +// parseDeprecationDate accepts both string and time.Time as input, and returns +// a truncated time.Time value. +func (l *LayerContributor) parseDeprecationDate(v interface{}) (deprecationDate time.Time, err error) { + switch vDate := v.(type) { + case time.Time: + deprecationDate = vDate + case string: + deprecationDate, err = time.Parse(time.RFC3339, vDate) + if err != nil { + return time.Time{}, fmt.Errorf("unable to parse deprecation_date %s", vDate) + } + default: + return time.Time{}, fmt.Errorf("unexpected type %T for deprecation_date %v", v, v) + } + + deprecationDate = deprecationDate.Truncate(time.Second).In(time.UTC) + return } func (l *LayerContributor) checkIfLayerRestored(layer libcnb.Layer) (bool, error) { diff --git a/layer_test.go b/layer_test.go index 951d959..bedb599 100644 --- a/layer_test.go +++ b/layer_test.go @@ -459,6 +459,88 @@ func testLayer(t *testing.T, context spec.G, it spec.S) { Expect(called).To(BeFalse()) }) + it("gracefully handles a deprecationDate in time.Time format in actual layer metadata", func() { + // reusing It: does not call function with non-matching deprecation_date format + // but this time with a deprecationDate formatted as time.Time in the actual layer metadata + actualDeprecationDate, _ := time.Parse(time.RFC3339, "2021-04-01T00:00:00Z") + + dependency = libpak.BuildModuleDependency{ + ID: "test-id", + Name: "test-name", + Version: "1.1.1", + URI: fmt.Sprintf("%s/test-path", server.URL()), + SHA256: "576dd8416de5619ea001d9662291d62444d1292a38e96956bc4651c01f14bca1", + Stacks: []string{"test-stack"}, + Licenses: []libpak.BuildModuleDependencyLicense{ + { + Type: "test-type", + URI: "test-uri", + }, + }, + CPEs: []string{"cpe:2.3:a:some:jre:11.0.2:*:*:*:*:*:*:*"}, + PURL: "pkg:generic/some-java11@11.0.2?arch=amd64", + DeprecationDate: dependency.DeprecationDate, // parsed as '2021-04-01 00:00:00 +0000 UTC' + } + dlc.ExpectedMetadata = map[string]interface{}{"dependency": dependency} + + layer.Metadata = map[string]interface{}{"dependency": map[string]interface{}{ + "id": dependency.ID, + "name": dependency.Name, + "version": dependency.Version, + "uri": dependency.URI, + "sha256": dependency.SHA256, + "stacks": []interface{}{dependency.Stacks[0]}, + "licenses": []map[string]interface{}{ + { + "type": dependency.Licenses[0].Type, + "uri": dependency.Licenses[0].URI, + }, + }, + "cpes": []interface{}{"cpe:2.3:a:some:jre:11.0.2:*:*:*:*:*:*:*"}, + "purl": "pkg:generic/some-java11@11.0.2?arch=amd64", + "deprecation_date": actualDeprecationDate, // does not match without truncation + }} + + var called bool + + err := dlc.Contribute(layer, func(layer *libcnb.Layer, artifact *os.File) error { + defer artifact.Close() + + called = true + return nil + }) + Expect(err).NotTo(HaveOccurred()) + + Expect(called).To(BeFalse()) + }) + + it("does not panic on unsupported deprecationDate format in layer metadata", func() { + // Unexpected type (not string or time.Time) + actualDeprecationDate := 1234 + + dependency = libpak.BuildModuleDependency{ + ID: "test-id", + DeprecationDate: dependency.DeprecationDate, // parsed as '2021-04-01 00:00:00 +0000 UTC' + } + dlc.ExpectedMetadata = map[string]interface{}{"dependency": dependency} + + layer.Metadata = map[string]interface{}{"dependency": map[string]interface{}{ + "id": dependency.ID, + "deprecation_date": actualDeprecationDate, // does not match without truncation + }} + + var called bool + + err := dlc.Contribute(layer, func(layer *libcnb.Layer, artifact *os.File) error { + defer artifact.Close() + + called = true + return nil + }) + Expect(err).To(MatchError(ContainSubstring("unexpected type int for deprecation_date"))) + Expect(called).To(BeFalse()) + }) + it("does not call function with missing deprecation_date", func() { dependency = libpak.BuildModuleDependency{ ID: "test-id",