diff --git a/carton/buildpack_dependency.go b/carton/buildpack_dependency.go index bd5a52c..6e901a4 100644 --- a/carton/buildpack_dependency.go +++ b/carton/buildpack_dependency.go @@ -17,6 +17,7 @@ package carton import ( + "bytes" "fmt" "io/ioutil" "os" @@ -24,6 +25,7 @@ import ( "github.com/paketo-buildpacks/libpak/bard" "github.com/paketo-buildpacks/libpak/internal" + "github.com/pelletier/go-toml" ) const ( @@ -55,26 +57,94 @@ func (b BuildpackDependency) Update(options ...Option) { logger.Headerf("URI: %s", b.URI) logger.Headerf("SHA256: %s", b.SHA256) + versionExp, err := regexp.Compile(b.VersionPattern) + if err != nil { + config.exitHandler.Error(fmt.Errorf("unable to compile regex %s\n%w", b.VersionPattern, err)) + return + } + c, err := ioutil.ReadFile(b.BuildpackPath) if err != nil { config.exitHandler.Error(fmt.Errorf("unable to read %s\n%w", b.BuildpackPath, err)) return } - s := fmt.Sprintf(BuildpackDependencyPattern, b.ID, b.VersionPattern) - r, err := regexp.Compile(s) - if err != nil { - config.exitHandler.Error(fmt.Errorf("unable to compile regex %s\n%w", s, err)) + // save any leading comments, this is to preserve license headers + // inline comments will be lost + comments := []byte{} + for i, line := range bytes.SplitAfter(c, []byte("\n")) { + if bytes.HasPrefix(line, []byte("#")) || (i > 0 && len(bytes.TrimSpace(line)) == 0) { + comments = append(comments, line...) + } else { + break // stop on first comment + } + } + + md := make(map[string]interface{}) + if err := toml.Unmarshal(c, &md); err != nil { + config.exitHandler.Error(fmt.Errorf("unable to decode md%s\n%w", b.BuildpackPath, err)) + return + } + + metadataUnwrapped, found := md["metadata"] + if !found { + config.exitHandler.Error(fmt.Errorf("unable to find metadata block")) + return + } + + metadata, ok := metadataUnwrapped.(map[string]interface{}) + if !ok { + config.exitHandler.Error(fmt.Errorf("unable to cast metadata")) + return + } + + dependenciesUnwrapped, found := metadata["dependencies"] + if !found { + config.exitHandler.Error(fmt.Errorf("unable to find dependencies block")) + return + } + + dependencies, ok := dependenciesUnwrapped.([]map[string]interface{}) + if !ok { + config.exitHandler.Error(fmt.Errorf("unable to cast dependencies")) return } - if !r.Match(c) { - config.exitHandler.Error(fmt.Errorf("unable to match '%s' '%s'", b.ID, b.VersionPattern)) + for _, dep := range dependencies { + depIdUnwrapped, found := dep["id"] + if !found { + continue + } + depId, ok := depIdUnwrapped.(string) + if !ok { + continue + } + + if depId == b.ID { + depVersionUnwrapped, found := dep["version"] + if !found { + continue + } + + depVersion, ok := depVersionUnwrapped.(string) + if !ok { + continue + } + if versionExp.MatchString(depVersion) { + dep["version"] = b.Version + dep["uri"] = b.URI + dep["sha256"] = b.SHA256 + } + } + } + + c, err = toml.Marshal(md) + if err != nil { + config.exitHandler.Error(fmt.Errorf("unable to encode md %s\n%w", b.BuildpackPath, err)) return } - s = fmt.Sprintf(BuildpackDependencySubstitution, b.Version, b.URI, b.SHA256) - c = r.ReplaceAll(c, []byte(s)) + c = append(comments, c...) if err := ioutil.WriteFile(b.BuildpackPath, c, 0644); err != nil { config.exitHandler.Error(fmt.Errorf("unable to write %s\n%w", b.BuildpackPath, err)) diff --git a/carton/buildpack_dependency_test.go b/carton/buildpack_dependency_test.go index 40edc3b..90ba4e8 100644 --- a/carton/buildpack_dependency_test.go +++ b/carton/buildpack_dependency_test.go @@ -27,6 +27,7 @@ import ( "github.com/stretchr/testify/mock" "github.com/paketo-buildpacks/libpak/carton" + "github.com/paketo-buildpacks/libpak/internal" ) func testBuildpackDependency(t *testing.T, context spec.G, it spec.S) { @@ -54,7 +55,14 @@ func testBuildpackDependency(t *testing.T, context spec.G, it spec.S) { }) it("updates dependency", func() { - Expect(ioutil.WriteFile(path, []byte(`id = "test-id" + Expect(ioutil.WriteFile(path, []byte(`api = "0.6" +[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" @@ -73,17 +81,34 @@ stacks = [ "test-stack" ] d.Update(carton.WithExitHandler(exitHandler)) - Expect(ioutil.ReadFile(path)).To(Equal([]byte(`id = "test-id" + Expect(ioutil.ReadFile(path)).To(internal.MatchTOML(`api = "0.6" +[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" ] -`))) +`)) }) it("updates indented dependency", func() { - Expect(ioutil.WriteFile(path, []byte(` id = "test-id" + Expect(ioutil.WriteFile(path, []byte(`# it should preserve +# these comments +# exactly + +api = "0.6" +[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" @@ -102,13 +127,26 @@ stacks = [ "test-stack" ] d.Update(carton.WithExitHandler(exitHandler)) - Expect(ioutil.ReadFile(path)).To(Equal([]byte(` id = "test-id" + body, err := ioutil.ReadFile(path) + Expect(err).NotTo(HaveOccurred()) + Expect(string(body)).To(HavePrefix(`# it should preserve +# these comments +# exactly + +api = "0.6"`)) + Expect(body).To(internal.MatchTOML(`api = "0.6" +[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" ] -`))) +`)) }) - } diff --git a/carton/package.go b/carton/package.go index fcf1d91..05a9a79 100644 --- a/carton/package.go +++ b/carton/package.go @@ -188,7 +188,7 @@ func (p Package) Create(options ...Option) { } var files []string - for d, _ := range entries { + for d := range entries { files = append(files, d) } sort.Strings(files) diff --git a/carton/package_dependency.go b/carton/package_dependency.go index 1099f32..5db0a38 100644 --- a/carton/package_dependency.go +++ b/carton/package_dependency.go @@ -17,20 +17,15 @@ package carton import ( + "bytes" "fmt" "io/ioutil" "os" - "regexp" "strings" "github.com/paketo-buildpacks/libpak/bard" "github.com/paketo-buildpacks/libpak/internal" -) - -const ( - PackageIdDependencyPattern = `(?m)(.*id[\s]+=[\s]+".+/%s",[\s]+version=")[^"]+(".*)` - PackageImageDependencyPattern = `(?m)(.*uri[\s]+=[\s]+".*%s:)[^"]+(".*)` - PackageDependencySubstitution = "${1}%s${2}" + "github.com/pelletier/go-toml" ) type PackageDependency struct { @@ -53,68 +48,131 @@ func (p PackageDependency) Update(options ...Option) { logger := bard.NewLogger(os.Stdout) _, _ = fmt.Fprintf(logger.TitleWriter(), "\n%s\n", bard.FormatIdentity(p.ID, p.Version)) - var paths []string if p.BuilderPath != "" { - paths = append(paths, p.BuilderPath) + if err := updateFile(p.BuilderPath, updateByKey("buildpacks", p.ID, p.Version)); err != nil { + config.exitHandler.Error(fmt.Errorf("unable to update %s\n%w", p.BuilderPath, err)) + } } + if p.PackagePath != "" { - paths = append(paths, p.PackagePath) + if err := updateFile(p.PackagePath, updateByKey("dependencies", p.ID, p.Version)); err != nil { + config.exitHandler.Error(fmt.Errorf("unable to update %s\n%w", p.PackagePath, err)) + } } - for _, path := range paths { - c, err := ioutil.ReadFile(path) - if err != nil { - config.exitHandler.Error(fmt.Errorf("unable to read %s\n%w", path, err)) - return + if p.BuildpackPath != "" { + if err := updateFile(p.BuildpackPath, func(md map[string]interface{}) { + parts := strings.Split(p.ID, "/") + id := strings.Join(parts[len(parts)-2:], "/") + + groupsUnwrapped, found := md["order"] + if !found { + return + } + + groups, ok := groupsUnwrapped.([]map[string]interface{}) + if !ok { + return + } + + for _, group := range groups { + buildpacksUnwrapped, found := group["group"] + if !found { + continue + } + + buildpacks, ok := buildpacksUnwrapped.([]map[string]interface{}) + if !ok { + continue + } + + for _, bp := range buildpacks { + bpIdUnwrappd, found := bp["id"] + if !found { + continue + } + + bpId, ok := bpIdUnwrappd.(string) + if !ok { + continue + } + + if bpId == id { + bp["version"] = p.Version + } + } + } + }); err != nil { + config.exitHandler.Error(fmt.Errorf("unable to update %s\n%w", p.BuildpackPath, err)) } + } +} - s := fmt.Sprintf(PackageImageDependencyPattern, p.ID) - r, err := regexp.Compile(s) - if err != nil { - config.exitHandler.Error(fmt.Errorf("unable to compile regex %s\n%w", s, err)) +func updateByKey(key, id, version string) func(md map[string]interface{}) { + return func(md map[string]interface{}) { + valuesUnwrapped, found := md[key] + if !found { return } - if !r.Match(c) { - config.exitHandler.Error(fmt.Errorf("unable to match '%s'", s)) + values, ok := valuesUnwrapped.([]map[string]interface{}) + if !ok { return } - s = fmt.Sprintf(PackageDependencySubstitution, p.Version) - c = r.ReplaceAll(c, []byte(s)) - - if err := ioutil.WriteFile(path, c, 0644); err != nil { - config.exitHandler.Error(fmt.Errorf("unable to write %s\n%w", path, err)) - return + for _, bp := range values { + uriUnwrapped, found := bp["uri"] + if !found { + continue + } + + uri, ok := uriUnwrapped.(string) + if !ok { + continue + } + + if strings.HasPrefix(uri, fmt.Sprintf("docker://%s", id)) { + parts := strings.Split(uri, ":") + bp["uri"] = fmt.Sprintf("%s:%s", strings.Join(parts[0:2], ":"), version) + } } } +} - if p.BuildpackPath != "" { - c, err := ioutil.ReadFile(p.BuildpackPath) - if err != nil { - config.exitHandler.Error(fmt.Errorf("unable to read %s\n%w", p.BuildpackPath, err)) - return - } +func updateFile(cfgPath string, f func(md map[string]interface{})) error { + c, err := ioutil.ReadFile(cfgPath) + if err != nil { + return fmt.Errorf("unable to read %s\n%w", cfgPath, err) + } - id := strings.Join(strings.Split(p.ID, "/")[2:], "/") - s := fmt.Sprintf(PackageIdDependencyPattern, id) - r, err := regexp.Compile(s) - if err != nil { - config.exitHandler.Error(fmt.Errorf("unable to compile regex %s\n%w", s, err)) - return + // save any leading comments, this is to preserve license headers + // inline comments will be lost + comments := []byte{} + for i, line := range bytes.SplitAfter(c, []byte("\n")) { + if bytes.HasPrefix(line, []byte("#")) || (i > 0 && len(bytes.TrimSpace(line)) == 0) { + comments = append(comments, line...) + } else { + break // stop on first comment } + } - if !r.Match(c) { - config.exitHandler.Error(fmt.Errorf("unable to match '%s'", s)) - return - } + md := make(map[string]interface{}) + if err := toml.Unmarshal(c, &md); err != nil { + return fmt.Errorf("unable to decode md %s\n%w", cfgPath, err) + } - s = fmt.Sprintf(PackageDependencySubstitution, p.Version) - c = r.ReplaceAll(c, []byte(s)) + f(md) - if err := ioutil.WriteFile(p.BuildpackPath, c, 0644); err != nil { - config.exitHandler.Error(fmt.Errorf("unable to write %s\n%w", p.BuildpackPath, err)) - return - } + b, err := toml.Marshal(md) + if err != nil { + return fmt.Errorf("unable to encode md %s\n%w", cfgPath, err) + } + + b = append(comments, b...) + + if err := ioutil.WriteFile(cfgPath, b, 0644); err != nil { + return fmt.Errorf("unable to write %s\n%w", cfgPath, err) } + + return nil } diff --git a/carton/package_dependency_test.go b/carton/package_dependency_test.go index fa1aac1..6b47e93 100644 --- a/carton/package_dependency_test.go +++ b/carton/package_dependency_test.go @@ -27,6 +27,7 @@ import ( "github.com/stretchr/testify/mock" "github.com/paketo-buildpacks/libpak/carton" + "github.com/paketo-buildpacks/libpak/internal" ) func testPackageDependency(t *testing.T, context spec.G, it spec.S) { @@ -53,11 +54,27 @@ func testPackageDependency(t *testing.T, context spec.G, it spec.S) { Expect(os.RemoveAll(path)).To(Succeed()) }) - it("updates paketo-buildpacks dependency", func() { - Expect(ioutil.WriteFile(path, []byte(` -{ id = "paketo-buildpacks/test-1", version="test-version-1" }, -{ id = "paketo-buildpacks/test-2", version="test-version-2" }, -`), 0644)).To(Succeed()) + it("updates paketo-buildpacks dependency without losing other fields", func() { + Expect(ioutil.WriteFile(path, []byte(`# it should preserve +# these comments +# exactly + +api = "0.6" +[buildpack] +id = "some-id" +name = "some-name" + +[[order]] +group = [ + { id = "paketo-buildpacks/test-1", version="test-version-1" }, + { id = "paketo-buildpacks/test-2", version="test-version-2" }, +] +[metadata] +include-files = [ + "LICENSE", + "README.md", + "buildpack.toml", +]`), 0644)).To(Succeed()) p := carton.PackageDependency{ BuildpackPath: path, @@ -67,17 +84,60 @@ func testPackageDependency(t *testing.T, context spec.G, it spec.S) { p.Update(carton.WithExitHandler(exitHandler)) - Expect(ioutil.ReadFile(path)).To(Equal([]byte(` -{ id = "paketo-buildpacks/test-1", version="test-version-3" }, -{ id = "paketo-buildpacks/test-2", version="test-version-2" }, -`))) + body, err := ioutil.ReadFile(path) + Expect(err).NotTo(HaveOccurred()) + Expect(string(body)).To(HavePrefix(`# it should preserve +# these comments +# exactly + +api = "0.6"`)) + Expect(body).To(internal.MatchTOML(`api = "0.6" +[buildpack] +id = "some-id" +name = "some-name" + +[[order]] +group = [ + { id = "paketo-buildpacks/test-1", version="test-version-3" }, + { id = "paketo-buildpacks/test-2", version="test-version-2" }, +] +[metadata] +include-files = [ + "LICENSE", + "README.md", + "buildpack.toml", +]`)) }) - it("updates paketocommunity dependency", func() { + it("updates paketo-buildpacks dependency id partial id", func() { Expect(ioutil.WriteFile(path, []byte(` -{ id = "paketocommunity/test-1", version="test-version-1" }, -{ id = "paketocommunity/test-2", version="test-version-2" }, -`), 0644)).To(Succeed()) +[[order]] +group = [ + { id = "paketo-buildpacks/test-1", version="test-version-1" }, + { id = "paketo-buildpacks/test-2", version="test-version-2" }, +]`), 0644)).To(Succeed()) + + p := carton.PackageDependency{ + BuildpackPath: path, + ID: "paketo-buildpacks/test-1", + Version: "test-version-3", + } + + p.Update(carton.WithExitHandler(exitHandler)) + + Expect(ioutil.ReadFile(path)).To(internal.MatchTOML(`[[order]] +group = [ + { id = "paketo-buildpacks/test-1", version="test-version-3" }, + { id = "paketo-buildpacks/test-2", version="test-version-2" }, +]`)) + }) + + it("updates paketocommunity dependency", func() { + Expect(ioutil.WriteFile(path, []byte(`[[order]] +group = [ + { id = "paketocommunity/test-1", version="test-version-1" }, + { id = "paketocommunity/test-2", version="test-version-2" }, +]`), 0644)).To(Succeed()) p := carton.PackageDependency{ BuildpackPath: path, @@ -87,17 +147,18 @@ func testPackageDependency(t *testing.T, context spec.G, it spec.S) { p.Update(carton.WithExitHandler(exitHandler)) - Expect(ioutil.ReadFile(path)).To(Equal([]byte(` -{ id = "paketocommunity/test-1", version="test-version-3" }, -{ id = "paketocommunity/test-2", version="test-version-2" }, -`))) + Expect(ioutil.ReadFile(path)).To(internal.MatchTOML(`[[order]] +group = [ + { id = "paketocommunity/test-1", version="test-version-3" }, + { id = "paketocommunity/test-2", version="test-version-2" }, +]`)) }) it("updates builder dependency", func() { - Expect(ioutil.WriteFile(path, []byte(` -{ id = "paketo-buildpacks/test-1", uri = "docker://gcr.io/paketo-buildpacks/test-1:test-version-1" }, -{ id = "paketo-buildpacks/test-2", uri = "docker://gcr.io/paketo-buildpacks/test-2:test-version-2" }, -`), 0644)).To(Succeed()) + Expect(ioutil.WriteFile(path, []byte(`buildpacks = [ + { id = "paketo-buildpacks/test-1", uri = "docker://gcr.io/paketo-buildpacks/test-1:test-version-1" }, + { id = "paketo-buildpacks/test-2", uri = "docker://gcr.io/paketo-buildpacks/test-2:test-version-2" }, +]`), 0644)).To(Succeed()) p := carton.PackageDependency{ BuilderPath: path, @@ -107,17 +168,17 @@ func testPackageDependency(t *testing.T, context spec.G, it spec.S) { p.Update(carton.WithExitHandler(exitHandler)) - Expect(ioutil.ReadFile(path)).To(Equal([]byte(` -{ id = "paketo-buildpacks/test-1", uri = "docker://gcr.io/paketo-buildpacks/test-1:test-version-3" }, -{ id = "paketo-buildpacks/test-2", uri = "docker://gcr.io/paketo-buildpacks/test-2:test-version-2" }, -`))) + Expect(ioutil.ReadFile(path)).To(internal.MatchTOML(`buildpacks = [ + { id = "paketo-buildpacks/test-1", uri = "docker://gcr.io/paketo-buildpacks/test-1:test-version-3" }, + { id = "paketo-buildpacks/test-2", uri = "docker://gcr.io/paketo-buildpacks/test-2:test-version-2" }, +]`)) }) it("updates paketo-buildpacks package dependency", func() { - Expect(ioutil.WriteFile(path, []byte(` -{ uri = "docker://gcr.io/paketo-buildpacks/test-1:test-version-1" }, -{ uri = "docker://gcr.io/paketo-buildpacks/test-2:test-version-2" }, -`), 0644)).To(Succeed()) + Expect(ioutil.WriteFile(path, []byte(`dependencies = [ + { uri = "docker://gcr.io/paketo-buildpacks/test-1:test-version-1" }, + { uri = "docker://gcr.io/paketo-buildpacks/test-2:test-version-2" }, +]`), 0644)).To(Succeed()) p := carton.PackageDependency{ PackagePath: path, @@ -127,17 +188,17 @@ func testPackageDependency(t *testing.T, context spec.G, it spec.S) { p.Update(carton.WithExitHandler(exitHandler)) - Expect(ioutil.ReadFile(path)).To(Equal([]byte(` -{ uri = "docker://gcr.io/paketo-buildpacks/test-1:test-version-3" }, -{ uri = "docker://gcr.io/paketo-buildpacks/test-2:test-version-2" }, -`))) + Expect(ioutil.ReadFile(path)).To(internal.MatchTOML(`dependencies = [ + { uri = "docker://gcr.io/paketo-buildpacks/test-1:test-version-3" }, + { uri = "docker://gcr.io/paketo-buildpacks/test-2:test-version-2" }, +]`)) }) it("updates paketocommunity package dependency", func() { - Expect(ioutil.WriteFile(path, []byte(` -{ uri = "docker://docker.io/paketocommunity/test-1:test-version-1" }, -{ uri = "docker://docker.io/paketocommunity/test-2:test-version-2" }, -`), 0644)).To(Succeed()) + Expect(ioutil.WriteFile(path, []byte(`dependencies = [ + { uri = "docker://docker.io/paketocommunity/test-1:test-version-1" }, + { uri = "docker://docker.io/paketocommunity/test-2:test-version-2" }, +]`), 0644)).To(Succeed()) p := carton.PackageDependency{ PackagePath: path, @@ -147,10 +208,10 @@ func testPackageDependency(t *testing.T, context spec.G, it spec.S) { p.Update(carton.WithExitHandler(exitHandler)) - Expect(ioutil.ReadFile(path)).To(Equal([]byte(` -{ uri = "docker://docker.io/paketocommunity/test-1:test-version-3" }, -{ uri = "docker://docker.io/paketocommunity/test-2:test-version-2" }, -`))) + Expect(ioutil.ReadFile(path)).To(internal.MatchTOML(`dependencies = [ + { uri = "docker://docker.io/paketocommunity/test-1:test-version-3" }, + { uri = "docker://docker.io/paketocommunity/test-2:test-version-2" }, +]`)) }) }