diff --git a/carton/buildmodule_dependency.go b/carton/buildmodule_dependency.go index eb0072b..a2fbec5 100644 --- a/carton/buildmodule_dependency.go +++ b/carton/buildmodule_dependency.go @@ -26,6 +26,8 @@ import ( "github.com/paketo-buildpacks/libpak/v2/log" "github.com/paketo-buildpacks/libpak/v2/utils" + + "github.com/paketo-buildpacks/libpak-tools/internal" ) const ( @@ -36,6 +38,7 @@ const ( type BuildModuleDependency struct { BuildModulePath string ID string + Arch string SHA256 string URI string Version string @@ -44,6 +47,9 @@ type BuildModuleDependency struct { CPEPattern string PURL string PURLPattern string + Source string + SourceSHA256 string + EolID string } func (b BuildModuleDependency) Update(options ...Option) { @@ -57,11 +63,15 @@ func (b BuildModuleDependency) Update(options ...Option) { logger := log.NewPaketoLogger(os.Stdout) _, _ = fmt.Fprintf(logger.TitleWriter(), "\n%s\n", log.FormatIdentity(b.ID, b.VersionPattern)) - logger.Headerf("Version: %s", b.Version) - logger.Headerf("PURL: %s", b.PURL) - logger.Headerf("CPEs: %s", b.CPE) - logger.Headerf("URI: %s", b.URI) - logger.Headerf("SHA256: %s", b.SHA256) + logger.Headerf("Arch: %s", b.Arch) + logger.Headerf("Version: %s", b.Version) + logger.Headerf("PURL: %s", b.PURL) + logger.Headerf("CPEs: %s", b.CPE) + logger.Headerf("URI: %s", b.URI) + logger.Headerf("SHA256: %s", b.SHA256) + logger.Headerf("Source: %s", b.Source) + logger.Headerf("SourceSHA256: %s", b.SourceSHA256) + logger.Headerf("EOL ID: %s", b.EolID) versionExp, err := regexp.Compile(b.VersionPattern) if err != nil { @@ -138,7 +148,27 @@ func (b BuildModuleDependency) Update(options ...Option) { continue } - if depID == b.ID { + // extract the arch from the PURL, it's the only place it lives consistently at the moment + var depArch string + purlUnwrapped, found := dep["purl"] + if found { + purl, ok := purlUnwrapped.(string) + if ok { + purlArchExp := regexp.MustCompile(`arch=(.*)`) + purlArchMatches := purlArchExp.FindStringSubmatch(purl) + if len(purlArchMatches) == 2 { + depArch = purlArchMatches[1] + } + } + } + + // if not set, we presently need to default to amd64 because a lot of deps do not specify arch + // in the future when we add the arch field to our deps, then we can remove this because empty should then mean noarch + if depArch == "" { + depArch = "amd64" + } + + if depID == b.ID && depArch == b.Arch { depVersionUnwrapped, found := dep["version"] if !found { continue @@ -148,10 +178,17 @@ func (b BuildModuleDependency) Update(options ...Option) { if !ok { continue } + if versionExp.MatchString(depVersion) { dep["version"] = b.Version dep["uri"] = b.URI dep["sha256"] = b.SHA256 + if b.SourceSHA256 != "" { + dep["source-sha256"] = b.SourceSHA256 + } + if b.Source != "" { + dep["source"] = b.Source + } purlUnwrapped, found := dep["purl"] if found { @@ -175,6 +212,18 @@ func (b BuildModuleDependency) Update(options ...Option) { } } } + + if b.EolID != "" { + eolDate, err := internal.GetEolDate(b.EolID, b.Version) + if err != nil { + config.exitHandler.Error(fmt.Errorf("unable to fetch deprecation_date")) + return + } + + if eolDate != "" { + dep["deprecation_date"] = eolDate + } + } } } } diff --git a/carton/buildmodule_dependency_test.go b/carton/buildmodule_dependency_test.go index 2da18ac..14004c1 100644 --- a/carton/buildmodule_dependency_test.go +++ b/carton/buildmodule_dependency_test.go @@ -62,21 +62,26 @@ name = "Some Buildpack" version = "1.2.3" [[metadata.dependencies]] -id = "test-id" -name = "Test Name" -version = "test-version-1" -uri = "test-uri-1" -sha256 = "test-sha256-1" -stacks = [ "test-stack" ] +id = "test-id" +name = "Test Name" +version = "test-version-1" +uri = "test-uri-1" +sha256 = "test-sha256-1" +stacks = [ "test-stack" ] +source = "test-source-uri-1" +source-sha256 = "test-source-sha256-1" `), 0600)).To(Succeed()) d := carton.BuildModuleDependency{ BuildModulePath: path, ID: "test-id", + Arch: "amd64", SHA256: "test-sha256-2", URI: "test-uri-2", Version: "test-version-2", VersionPattern: `test-version-[\d]`, + Source: "test-source-uri-2", + SourceSHA256: "test-source-sha256-2", } d.Update(carton.WithExitHandler(exitHandler)) @@ -94,6 +99,8 @@ version = "test-version-2" uri = "test-uri-2" sha256 = "test-sha256-2" stacks = [ "test-stack" ] +source = "test-source-uri-2" +source-sha256 = "test-source-sha256-2" `)) }) @@ -118,6 +125,7 @@ cpes = ["cpe:2.3:a:test-vendor:test-product:test-version-1:patch1:*:*:*:*:*:* d := carton.BuildModuleDependency{ BuildModulePath: path, ID: "test-id", + Arch: "amd64", SHA256: "test-sha256-2", URI: "test-uri-2", Version: "test-version-2", @@ -148,13 +156,12 @@ cpes = ["cpe:2.3:a:test-vendor:test-product:test-version-2:patch2:*:*:*:*:*:* `)) }) - it("updates multiple dependencies with different versions", func() { + it("updates dependency with source & sourceSha", func() { Expect(os.WriteFile(path, []byte(`api = "0.7" [buildpack] id = "some-buildpack" name = "Some Buildpack" version = "1.2.3" - [[metadata.dependencies]] id = "test-id" name = "Test Name" @@ -164,21 +171,82 @@ sha256 = "test-sha256-1" stacks = [ "test-stack" ] purl = "pkg:generic/test-jre@different-version-1?arch=amd64" cpes = ["cpe:2.3:a:test-vendor:test-product:test-version-1:patch1:*:*:*:*:*:*:*"] +`), 0600)).To(Succeed()) + + d := carton.BuildModuleDependency{ + BuildModulePath: path, + ID: "test-id", + Arch: "amd64", + SHA256: "test-sha256-2", + URI: "test-uri-2", + Version: "test-version-2", + VersionPattern: `test-version-[\d]`, + PURL: "different-version-2", + PURLPattern: `different-version-[\d]`, + CPE: "test-version-2:patch2", + CPEPattern: `test-version-[\d]:patch[\d]`, + Source: "test-new-source", + SourceSHA256: "test-new-source-sha", + } + + d.Update(carton.WithExitHandler(exitHandler)) + + Expect(os.ReadFile(path)).To(libpakTesting.MatchTOML(`api = "0.7" +[buildpack] +id = "some-buildpack" +name = "Some Buildpack" +version = "1.2.3" [[metadata.dependencies]] -id = "test-id" -name = "Test Name" -version = "test-version-2" -uri = "test-uri-2" -sha256 = "test-sha256-2" -stacks = [ "test-stack" ] -purl = "pkg:generic/test-jre@different-version-2?arch=amd64" -cpes = ["cpe:2.3:a:test-vendor:test-product:test-version-2:patch2:*:*:*:*:*:*:*"] +id = "test-id" +name = "Test Name" +version = "test-version-2" +uri = "test-uri-2" +sha256 = "test-sha256-2" +stacks = [ "test-stack" ] +purl = "pkg:generic/test-jre@different-version-2?arch=amd64" +cpes = ["cpe:2.3:a:test-vendor:test-product:test-version-2:patch2:*:*:*:*:*:*:*"] +source = "test-new-source" +source-sha256 = "test-new-source-sha" +`)) + }) + + it("updates multiple dependencies with different versions", func() { + Expect(os.WriteFile(path, []byte(`api = "0.7" +[buildpack] +id = "some-buildpack" +name = "Some Buildpack" +version = "1.2.3" + +[[metadata.dependencies]] +id = "test-id" +name = "Test Name" +version = "test-version-1" +uri = "test-uri-1" +sha256 = "test-sha256-1" +stacks = [ "test-stack" ] +purl = "pkg:generic/test-jre@different-version-1?arch=amd64" +cpes = ["cpe:2.3:a:test-vendor:test-product:test-version-1:patch1:*:*:*:*:*:*:*"] +source = "test-source-uri-1" +source-sha256 = "test-source-sha256-1" + +[[metadata.dependencies]] +id = "test-id" +name = "Test Name" +version = "test-version-2" +uri = "test-uri-2" +sha256 = "test-sha256-2" +stacks = [ "test-stack" ] +purl = "pkg:generic/test-jre@different-version-2?arch=amd64" +cpes = ["cpe:2.3:a:test-vendor:test-product:test-version-2:patch2:*:*:*:*:*:*:*"] +source = "test-source-uri-2" +source-sha256 = "test-source-sha256-2" `), 0600)).To(Succeed()) d := carton.BuildModuleDependency{ BuildModulePath: path, ID: "test-id", + Arch: "amd64", SHA256: "test-sha256-3", URI: "test-uri-3", Version: "test-version-3", @@ -187,6 +255,8 @@ cpes = ["cpe:2.3:a:test-vendor:test-product:test-version-2:patch2:*:*:*:*:*:* PURLPattern: `different-version-[\d]`, CPE: "test-version-3:patch3", CPEPattern: `test-version-[\d]:patch[\d]`, + Source: "test-source-uri-3", + SourceSHA256: "test-source-sha256-3", } d.Update(carton.WithExitHandler(exitHandler)) @@ -198,24 +268,28 @@ name = "Some Buildpack" version = "1.2.3" [[metadata.dependencies]] -id = "test-id" -name = "Test Name" -version = "test-version-3" -uri = "test-uri-3" -sha256 = "test-sha256-3" -stacks = [ "test-stack" ] -purl = "pkg:generic/test-jre@different-version-3?arch=amd64" -cpes = ["cpe:2.3:a:test-vendor:test-product:test-version-3:patch3:*:*:*:*:*:*:*"] +id = "test-id" +name = "Test Name" +version = "test-version-3" +uri = "test-uri-3" +sha256 = "test-sha256-3" +stacks = [ "test-stack" ] +purl = "pkg:generic/test-jre@different-version-3?arch=amd64" +cpes = ["cpe:2.3:a:test-vendor:test-product:test-version-3:patch3:*:*:*:*:*:*:*"] +source = "test-source-uri-3" +source-sha256 = "test-source-sha256-3" [[metadata.dependencies]] -id = "test-id" -name = "Test Name" -version = "test-version-2" -uri = "test-uri-2" -sha256 = "test-sha256-2" -stacks = [ "test-stack" ] -purl = "pkg:generic/test-jre@different-version-2?arch=amd64" -cpes = ["cpe:2.3:a:test-vendor:test-product:test-version-2:patch2:*:*:*:*:*:*:*"] +id = "test-id" +name = "Test Name" +version = "test-version-2" +uri = "test-uri-2" +sha256 = "test-sha256-2" +stacks = [ "test-stack" ] +purl = "pkg:generic/test-jre@different-version-2?arch=amd64" +cpes = ["cpe:2.3:a:test-vendor:test-product:test-version-2:patch2:*:*:*:*:*:*:*"] +source = "test-source-uri-2" +source-sha256 = "test-source-sha256-2" `)) }) @@ -239,6 +313,7 @@ cpes = ["cpe:2.3:a:test-vendor:test-product:test-version-1:patch1:*:*:*:*:*:* d := carton.BuildModuleDependency{ BuildModulePath: path, ID: "test-id", + Arch: "amd64", SHA256: "test-sha256-2", URI: "test-uri-2", Version: "test-version-2", @@ -289,6 +364,7 @@ cpes = ["cpe:2.3:a:test-vendor:test-product:test-version-1:patch1:*:*:*:*:*:* d := carton.BuildModuleDependency{ BuildModulePath: path, ID: "test-id", + Arch: "amd64", SHA256: "test-sha256-2", URI: "test-uri-2", Version: "test-version-2", @@ -340,6 +416,7 @@ cpes = 1234 d := carton.BuildModuleDependency{ BuildModulePath: path, ID: "test-id", + Arch: "amd64", SHA256: "test-sha256-2", URI: "test-uri-2", Version: "test-version-2", @@ -393,6 +470,7 @@ version = "1.2.3" d := carton.BuildModuleDependency{ BuildModulePath: path, ID: "test-id", + Arch: "amd64", SHA256: "test-sha256-2", URI: "test-uri-2", Version: "test-version-2", diff --git a/carton/package.go b/carton/package.go index 3b0bb5f..6fe6cce 100644 --- a/carton/package.go +++ b/carton/package.go @@ -22,6 +22,7 @@ import ( "path/filepath" "regexp" "sort" + "strings" "text/template" "github.com/BurntSushi/toml" @@ -34,6 +35,8 @@ import ( "github.com/paketo-buildpacks/libpak/v2/utils" ) +const DefaultTargetArch = "all" + // Package is an object that contains the configuration for building a package. type Package struct { @@ -57,6 +60,9 @@ type Package struct { // Version is a version to substitute into an existing buildpack.toml. Version string + + // TargetArch is the target architecture to package. Default is "all". + TargetArch string } // Create creates a package. @@ -133,10 +139,38 @@ func (p Package) Create(options ...Option) { return } + logger.Debugf("IncludeFiles: %+v", metadata.IncludeFiles) + + supportedTargets := []string{} + for _, i := range metadata.IncludeFiles { + if strings.HasPrefix(i, "linux/") { + parts := strings.SplitN(i, "/", 3) + if len(parts) < 3 { + // this shouldn't happen, but if it does for some reason just ignore it + // this entry is not a properly formatted target + continue + } + supportedTargets = append(supportedTargets, fmt.Sprintf("%s/%s", parts[0], parts[1])) + } + } + + oldOutputFormat := len(supportedTargets) == 0 + if oldOutputFormat { + logger.Body("No supported targets found, defaulting to old format") + } + + logger.Debugf("Supported targets: %+v", supportedTargets) + entries := map[string]string{} for _, i := range metadata.IncludeFiles { - entries[i] = filepath.Join(p.Source, i) + if oldOutputFormat || strings.HasPrefix(i, "linux/") || i == "buildpack.toml" { + entries[i] = filepath.Join(p.Source, i) + } else { + for _, target := range supportedTargets { + entries[fmt.Sprintf("%s/%s", target, i)] = filepath.Join(p.Source, i) + } + } } logger.Debugf("Include files: %+v", entries) @@ -228,7 +262,8 @@ func (p Package) Create(options ...Option) { f, err := cache.Artifact(dep, n.BasicAuth) if err != nil { - config.exitHandler.Error(fmt.Errorf("unable to download %s\n%w", dep.URI, err)) + logger.Debugf("fetching dependency %s failed\n%w", dep.Name, err) + config.exitHandler.Error(fmt.Errorf("unable to download %s. see DEBUG log level", dep.Name)) return } if err = f.Close(); err != nil { @@ -247,8 +282,18 @@ func (p Package) Create(options ...Option) { } sort.Strings(files) for _, d := range files { - logger.Bodyf("Adding %s", d) - file = filepath.Join(p.Destination, d) + if p.TargetArch != DefaultTargetArch && !oldOutputFormat && strings.HasPrefix(d, "linux/") && !strings.HasPrefix(d, fmt.Sprintf("linux/%s", p.TargetArch)) { + logger.Debugf("Skipping %s because target arch is %s", d, p.TargetArch) + continue + } + + targetLocation := d + if p.TargetArch != DefaultTargetArch { + targetLocation = strings.Replace(d, fmt.Sprintf("linux/%s/", p.TargetArch), "", 1) + } + + logger.Bodyf("Adding %s", targetLocation) + file = filepath.Join(p.Destination, targetLocation) if err = config.entryWriter.Write(entries[d], file); err != nil { config.exitHandler.Error(fmt.Errorf("unable to write file %s to %s\n%w", entries[d], file, err)) return diff --git a/carton/package_test.go b/carton/package_test.go index 5152188..9808b71 100644 --- a/carton/package_test.go +++ b/carton/package_test.go @@ -375,7 +375,64 @@ include-files = [ Expect(e.Dir).To(Equal(path)) }) - it("includes include_files", func() { + context("has a buildpack.toml with target specific include files", func() { + it.Before(func() { + Expect(os.WriteFile(filepath.Join(path, "buildpack.toml"), []byte(` +api = "0.0.0" +[buildpack] +name = "test-name" +version = "{{.version}}" +[[metadata.dependencies]] +id = "test-id" +name = "test-name" +version = "1.1.1" +uri = "test-uri" +sha256 = "test-sha256" +stacks = [ "test-stack" ] + [[metadata.dependencies.licenses]] + type = "test-type" + uri = "test-uri" +[metadata] +pre-package = "test-pre-package" +include-files = [ + "buildpack.toml", + "README", + "LICENSE", + "linux/amd64/bin/just-once", + "linux/arm64/bin/also-just-once" +] +`), 0600)).To(Succeed()) + }) + + it("includes include_files using the original format", func() { + carton.Package{ + Source: path, + Destination: "test-destination", + }.Create( + carton.WithEntryWriter(entryWriter), + carton.WithExecutor(executor), + carton.WithExitHandler(exitHandler)) + + Expect(entryWriter.Calls[0].Arguments[0]).To(Equal(filepath.Join(path, "buildpack.toml"))) + Expect(entryWriter.Calls[0].Arguments[1]).To(Equal(filepath.Join("test-destination", "buildpack.toml"))) + + Expect(entryWriter.Calls[1].Arguments[0]).To(Equal(filepath.Join(path, "LICENSE"))) + Expect(entryWriter.Calls[1].Arguments[1]).To(Equal(filepath.Join("test-destination", "linux/amd64/LICENSE"))) + Expect(entryWriter.Calls[2].Arguments[0]).To(Equal(filepath.Join(path, "README"))) + Expect(entryWriter.Calls[2].Arguments[1]).To(Equal(filepath.Join("test-destination", "linux/amd64/README"))) + Expect(entryWriter.Calls[3].Arguments[0]).To(Equal(filepath.Join(path, "linux/amd64/bin/just-once"))) + Expect(entryWriter.Calls[3].Arguments[1]).To(Equal(filepath.Join("test-destination", "linux/amd64/bin/just-once"))) + + Expect(entryWriter.Calls[4].Arguments[0]).To(Equal(filepath.Join(path, "LICENSE"))) + Expect(entryWriter.Calls[4].Arguments[1]).To(Equal(filepath.Join("test-destination", "linux/arm64/LICENSE"))) + Expect(entryWriter.Calls[5].Arguments[0]).To(Equal(filepath.Join(path, "README"))) + Expect(entryWriter.Calls[5].Arguments[1]).To(Equal(filepath.Join("test-destination", "linux/arm64/README"))) + Expect(entryWriter.Calls[6].Arguments[0]).To(Equal(filepath.Join(path, "linux/arm64/bin/also-just-once"))) + Expect(entryWriter.Calls[6].Arguments[1]).To(Equal(filepath.Join("test-destination", "linux/arm64/bin/also-just-once"))) + }) + }) + + it("includes include_files using the target format", func() { carton.Package{ Source: path, Destination: "test-destination", diff --git a/commands/dependency_update_build_module.go b/commands/dependency_update_build_module.go index ab5e783..7d392c6 100644 --- a/commands/dependency_update_build_module.go +++ b/commands/dependency_update_build_module.go @@ -39,6 +39,10 @@ func DependencyUpdateBuildModuleCommand() *cobra.Command { log.Fatal("id must be set") } + if b.Arch == "" { + b.Arch = "amd64" + } + if b.SHA256 == "" { log.Fatal("sha256 must be set") } @@ -77,6 +81,7 @@ func DependencyUpdateBuildModuleCommand() *cobra.Command { dependencyUpdateBuildModuleCmd.Flags().StringVar(&b.BuildModulePath, "buildmodule-toml", "", "path to buildpack.toml or extension.toml") dependencyUpdateBuildModuleCmd.Flags().StringVar(&b.ID, "id", "", "the id of the dependency") + dependencyUpdateBuildModuleCmd.Flags().StringVar(&b.Arch, "arch", "", "the arch of the dependency") dependencyUpdateBuildModuleCmd.Flags().StringVar(&b.SHA256, "sha256", "", "the new sha256 of the dependency") dependencyUpdateBuildModuleCmd.Flags().StringVar(&b.URI, "uri", "", "the new uri of the dependency") dependencyUpdateBuildModuleCmd.Flags().StringVar(&b.Version, "version", "", "the new version of the dependency") @@ -85,6 +90,9 @@ func DependencyUpdateBuildModuleCommand() *cobra.Command { dependencyUpdateBuildModuleCmd.Flags().StringVar(&b.PURLPattern, "purl-pattern", "", "the purl version pattern of the dependency, if not set defaults to version-pattern") dependencyUpdateBuildModuleCmd.Flags().StringVar(&b.CPE, "cpe", "", "the new version use in all CPEs, if not set defaults to version") dependencyUpdateBuildModuleCmd.Flags().StringVar(&b.CPEPattern, "cpe-pattern", "", "the cpe version pattern of the dependency, if not set defaults to version-pattern") + dependencyUpdateBuildModuleCmd.Flags().StringVar(&b.Source, "source", "", "the new uri of the dependency source") + dependencyUpdateBuildModuleCmd.Flags().StringVar(&b.SourceSHA256, "source-sha256", "", "the new sha256 of the dependency source") + dependencyUpdateBuildModuleCmd.Flags().StringVar(&b.EolID, "eol-id", "", "id of the dependency for looking up the EOL date on the https://endoflife.date/") return dependencyUpdateBuildModuleCmd } diff --git a/commands/package_create.go b/commands/package_compile.go similarity index 93% rename from commands/package_create.go rename to commands/package_compile.go index 16d7171..9c5a3f5 100644 --- a/commands/package_create.go +++ b/commands/package_compile.go @@ -48,6 +48,7 @@ func PackageCompileCommand() *cobra.Command { packageCreateCommand.Flags().BoolVar(&p.StrictDependencyFilters, "strict-filters", false, "require filter to match all data or just some data (default: false)") packageCreateCommand.Flags().StringVar(&p.Source, "source", defaultSource(), "path to build package source directory (default: $PWD)") packageCreateCommand.Flags().StringVar(&p.Version, "version", "", "version to substitute into buildpack.toml/extension.toml") + packageCreateCommand.Flags().StringVar(&p.TargetArch, "target-arch", carton.DefaultTargetArch, "target architecture for the package (default: all)") return packageCreateCommand } diff --git a/go.mod b/go.mod index 6b4f6d7..8b1fe92 100644 --- a/go.mod +++ b/go.mod @@ -12,10 +12,10 @@ require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/google/go-cmp v0.6.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/rogpeppe/go-internal v1.12.0 // indirect + github.com/rogpeppe/go-internal v1.13.1 // indirect github.com/stretchr/objx v0.5.2 // indirect - golang.org/x/net v0.30.0 // indirect - golang.org/x/text v0.19.0 // indirect + golang.org/x/net v0.31.0 // indirect + golang.org/x/text v0.20.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) @@ -23,16 +23,17 @@ require ( dario.cat/mergo v1.0.1 // indirect github.com/BurntSushi/toml v1.4.0 github.com/Masterminds/semver v1.5.0 // indirect - github.com/Masterminds/semver/v3 v3.3.0 // indirect + github.com/Masterminds/semver/v3 v3.3.0 github.com/buildpacks/libcnb/v2 v2.0.0 github.com/creack/pty v1.1.24 // indirect github.com/heroku/color v0.0.6 github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/jarcoal/httpmock v1.3.1 github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect github.com/onsi/gomega v1.35.1 github.com/sclevine/spec v1.4.0 github.com/spf13/pflag v1.0.5 // indirect - golang.org/x/sys v0.26.0 // indirect + golang.org/x/sys v0.27.0 // indirect ) diff --git a/go.sum b/go.sum index 12981a3..9743080 100644 --- a/go.sum +++ b/go.sum @@ -25,6 +25,8 @@ github.com/heroku/color v0.0.6 h1:UTFFMrmMLFcL3OweqP1lAdp8i1y/9oHqkeHjQ/b/Ny0= github.com/heroku/color v0.0.6/go.mod h1:ZBvOcx7cTF2QKOv4LbmoBtNl5uB17qWxGuzZrsi1wLU= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/jarcoal/httpmock v1.3.1 h1:iUx3whfZWVf3jT01hQTO/Eo5sAYtB2/rqaUuOtpInww= +github.com/jarcoal/httpmock v1.3.1/go.mod h1:3yb8rc4BI7TCBhFY8ng0gjuLKJNquuDNiPaZjnENuYg= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= @@ -36,6 +38,8 @@ github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hd github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/maxatome/go-testdeep v1.12.0 h1:Ql7Go8Tg0C1D/uMMX59LAoYK7LffeJQ6X2T04nTH68g= +github.com/maxatome/go-testdeep v1.12.0/go.mod h1:lPZc/HAcJMP92l7yI6TRz1aZN5URwUBUAfUNvrclaNM= github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4= github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE= github.com/onsi/ginkgo/v2 v2.20.1 h1:YlVIbqct+ZmnEph770q9Q7NVAz4wwIiVNahee6JyUzo= @@ -46,8 +50,8 @@ github.com/paketo-buildpacks/libpak/v2 v2.0.0-alpha.3.0.20241030145014-4e3b6fe37 github.com/paketo-buildpacks/libpak/v2 v2.0.0-alpha.3.0.20241030145014-4e3b6fe37213/go.mod h1:TJnhn128zE4lKVV/UGtpgYoEk4wQyAApcNwB4toRPDM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= -github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= +github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sclevine/spec v1.4.0 h1:z/Q9idDcay5m5irkZ28M7PtQM4aOISzOpj4bUPkDee8= github.com/sclevine/spec v1.4.0/go.mod h1:LvpgJaFyvQzRvc1kaDs0bulYwzC70PbiYjC4QnFHkOM= @@ -59,15 +63,15 @@ github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= -golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= +golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo= +golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= -golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= -golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s= +golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug= +golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4= golang.org/x/tools v0.25.0 h1:oFU9pkj/iJgs+0DT+VMHrx+oBKs/LJMV+Uvg78sl+fE= golang.org/x/tools v0.25.0/go.mod h1:/vtpO8WL1N9cQC3FN5zPqb//fRXskFHbLKk4OW1Q7rg= google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= diff --git a/internal/eol.go b/internal/eol.go new file mode 100644 index 0000000..132ea15 --- /dev/null +++ b/internal/eol.go @@ -0,0 +1,110 @@ +package internal + +import ( + "encoding/json" + "fmt" + "net/http" + "time" + + "github.com/Masterminds/semver/v3" +) + +const eolBaseURL = "https://endoflife.date/api" + +func GetEolDate(eolID, version string) (string, error) { + cycleList, err := getProjectCycleList(eolID) + if err != nil { + return "", fmt.Errorf("could not fetch cycle list: %w", err) + } + + cycle, err := selectCycle(version, cycleList) + if err != nil { + return "", fmt.Errorf("could not find a release cycle: %w", err) + } + + if cycle.EOL == "" { + return "", nil + } + + eol, err := time.Parse(time.DateOnly, cycle.EOL) + if err != nil { + return "", fmt.Errorf("could not parse eol %q: %w", cycle.EOL, err) + } + + return eol.Format(time.RFC3339), nil +} + +func selectCycle(version string, cycles cycleList) (*cycle, error) { + versionParsed, err := semver.NewVersion(version) + if err != nil { + return nil, err + } + + for _, v := range []string{fmt.Sprintf("%d.%d", versionParsed.Major(), versionParsed.Minor()), fmt.Sprintf("%d", versionParsed.Major())} { + for _, c := range cycles { + if c.Cycle == v { + return c, nil + } + } + } + + return nil, fmt.Errorf("no release cycle found for the version %s", version) +} + +func getProjectCycleList(id string) (cycleList, error) { + res, err := http.Get(fmt.Sprintf("%s/%s.json", eolBaseURL, id)) + if err != nil { + return nil, err + } + defer res.Body.Close() + + if res.StatusCode != http.StatusOK { + return nil, fmt.Errorf("failed to fetch release cycles, status: %d", res.StatusCode) + } + + cycles := cycleList{} + if err := json.NewDecoder(res.Body).Decode(&cycles); err != nil { + return nil, err + } + + return cycles, nil +} + +type cycleList []*cycle + +type cycle struct { + Cycle string + EOL string +} + +func (c *cycle) UnmarshalJSON(data []byte) error { + var i map[string]interface{} + + if err := json.Unmarshal(data, &i); err != nil { + return err + } + + if val, ok := i["cycle"]; ok { + switch t := val.(type) { + case string: + c.Cycle = t + case int, float64: + c.Cycle = fmt.Sprintf("%d", t) + default: + return fmt.Errorf("invalid type of the \"cycle\" field: %T", t) + } + } + + if val, ok := i["eol"]; ok { + switch t := val.(type) { + case string: + c.EOL = t + case bool: + c.EOL = "" + default: + return fmt.Errorf("invalid type of the \"eol\" field: %T", t) + } + } + + return nil +} diff --git a/internal/eol_test.go b/internal/eol_test.go new file mode 100644 index 0000000..33f72c3 --- /dev/null +++ b/internal/eol_test.go @@ -0,0 +1,85 @@ +package internal_test + +import ( + "net/http" + "testing" + + "github.com/buildpacks/libcnb/v2/mocks" + "github.com/jarcoal/httpmock" + . "github.com/onsi/gomega" + "github.com/sclevine/spec" + "github.com/stretchr/testify/mock" + + "github.com/paketo-buildpacks/libpak-tools/internal" +) + +func testGetEolDate(t *testing.T, context spec.G, it spec.S) { + var ( + Expect = NewWithT(t).Expect + + exitHandler *mocks.ExitHandler + ) + + it.Before(func() { + httpmock.Activate() + exitHandler = &mocks.ExitHandler{} + exitHandler.On("Error", mock.Anything) + }) + + it.After(func() { + httpmock.DeactivateAndReset() + }) + + context("finds release cycle by major.minor version", func() { + it.Before(func() { + httpmock.RegisterResponder(http.MethodGet, "https://endoflife.date/api/foo.json", httpmock.NewBytesResponder(200, []byte(` +[ + { + "cycle": "10.1", + "releaseDate": "2022-09-23", + "eol": false, + "minJavaVersion": 11, + "latest": "10.1.24", + "latestReleaseDate": "2024-05-09", + "lts": false + }, + { + "cycle": "10.0", + "releaseDate": "2020-12-03", + "eol": "2026-12-31", + "minJavaVersion": 8, + "latest": "10.0.27", + "latestReleaseDate": "2025-10-03", + "lts": false + }, + { + "cycle": "9", + "releaseDate": "2017-09-27", + "eol": "2023-12-31", + "minJavaVersion": 8, + "latest": "9.0.89", + "latestReleaseDate": "2021-05-03", + "lts": false + } +]`))) + }) + + it("finds release cycle by major and minor version", func() { + eolDate, err := internal.GetEolDate("foo", "10.0.1") + Expect(err).NotTo(HaveOccurred()) + Expect(eolDate).To(Equal("2026-12-31T00:00:00Z")) + }) + + it("finds release cycle by major version", func() { + eolDate, err := internal.GetEolDate("foo", "9.5.4") + Expect(err).NotTo(HaveOccurred()) + Expect(eolDate).To(Equal("2023-12-31T00:00:00Z")) + }) + + it("returns empty eol date if eol is a bool", func() { + eolDate, err := internal.GetEolDate("foo", "10.1.0") + Expect(err).NotTo(HaveOccurred()) + Expect(eolDate).To(Equal("")) + }) + }) +} diff --git a/internal/init_test.go b/internal/init_test.go new file mode 100644 index 0000000..2bebe3b --- /dev/null +++ b/internal/init_test.go @@ -0,0 +1,29 @@ +/* + * Copyright 2018-2024 the original author or 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 + * + * https://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 internal_test + +import ( + "testing" + + "github.com/sclevine/spec" + "github.com/sclevine/spec/report" +) + +func TestUnit(t *testing.T) { + suite := spec.New("libpak-tools/internal", spec.Report(report.Terminal{})) + suite("EOL", testGetEolDate) + suite.Run(t) +}