diff --git a/cargo/extension_config.go b/cargo/extension_config.go new file mode 100644 index 00000000..67cfe826 --- /dev/null +++ b/cargo/extension_config.go @@ -0,0 +1,88 @@ +package cargo + +import ( + "encoding/json" + "io" + + "github.com/BurntSushi/toml" +) + +type ExtensionConfig struct { + API string `toml:"api" json:"api,omitempty"` + Extension ConfigExtension `toml:"extension" json:"extension,omitempty"` + Metadata ConfigExtensionMetadata `toml:"metadata" json:"metadata,omitempty"` +} + +type ConfigExtensionMetadata struct { + IncludeFiles []string `toml:"include-files" json:"include-files,omitempty"` + PrePackage string `toml:"pre-package" json:"pre-package,omitempty"` + DefaultVersions map[string]string `toml:"default-versions" json:"default-versions,omitempty"` + Dependencies []ConfigExtensionMetadataDependency `toml:"dependencies" json:"dependencies,omitempty"` + Configurations []ConfigExtensionMetadataConfiguration `toml:"configurations" json:"configurations,omitempty"` +} + +type ConfigExtensionMetadataDependency struct { + Checksum string `toml:"checksum" json:"checksum,omitempty"` + ID string `toml:"id" json:"id,omitempty"` + Licenses []interface{} `toml:"licenses" json:"licenses,omitempty"` + Name string `toml:"name" json:"name,omitempty"` + SHA256 string `toml:"sha256" json:"sha256,omitempty"` + Source string `toml:"source" json:"source,omitempty"` + SourceChecksum string `toml:"source-checksum" json:"source-checksum,omitempty"` + SourceSHA256 string `toml:"source_sha256" json:"source_sha256,omitempty"` + Stacks []string `toml:"stacks" json:"stacks,omitempty"` + URI string `toml:"uri" json:"uri,omitempty"` + Version string `toml:"version" json:"version,omitempty"` +} +type ConfigExtensionMetadataConfiguration struct { + Default string `toml:"default" json:"default,omitempty"` + Launch bool `toml:"launch" json:"launch,omitempty"` + Description string `toml:"description" json:"description,omitempty"` + Build bool `toml:"build" json:"build,omitempty"` + Name string `toml:"name" json:"name,omitempty"` +} +type ConfigExtension struct { + ID string `toml:"id" json:"id,omitempty"` + Name string `toml:"name" json:"name,omitempty"` + Version string `toml:"version" json:"version,omitempty"` + Homepage string `toml:"homepage,omitempty" json:"homepage,omitempty"` + Description string `toml:"description,omitempty" json:"description,omitempty"` + Keywords []string `toml:"keywords,omitempty" json:"keywords,omitempty"` + Licenses []ConfigExtensionLicense `toml:"licenses,omitempty" json:"licenses,omitempty"` + SBOMFormats []string `toml:"sbom-formats,omitempty" json:"sbom-formats,omitempty"` +} + +type ConfigExtensionLicense struct { + Type string `toml:"type" json:"type"` + URI string `toml:"uri" json:"uri"` +} + +func EncodeExtensionConfig(writer io.Writer, extensionConfig ExtensionConfig) error { + content, err := json.Marshal(extensionConfig) + if err != nil { + return err + } + + c := map[string]interface{}{} + err = json.Unmarshal(content, &c) + if err != nil { + return err + } + + return toml.NewEncoder(writer).Encode(c) +} + +func DecodeExtensionConfig(reader io.Reader, extensionConfig *ExtensionConfig) error { + var c map[string]interface{} + _, err := toml.NewDecoder(reader).Decode(&c) + if err != nil { + return err + } + + content, err := json.Marshal(c) + if err != nil { + return err + } + + return json.Unmarshal(content, extensionConfig) +} diff --git a/cargo/extension_config_test.go b/cargo/extension_config_test.go new file mode 100644 index 00000000..896b9543 --- /dev/null +++ b/cargo/extension_config_test.go @@ -0,0 +1,449 @@ +package cargo_test + +import ( + "bytes" + + "strings" + "testing" + + "github.com/paketo-buildpacks/packit/v2/cargo" + "github.com/sclevine/spec" + + . "github.com/onsi/gomega" + . "github.com/paketo-buildpacks/packit/v2/matchers" +) + +func testExtensionConfig(t *testing.T, context spec.G, it spec.S) { + var ( + Expect = NewWithT(t).Expect + buffer *bytes.Buffer + ) + + it.Before(func() { + buffer = bytes.NewBuffer(nil) + }) + + context("EncodeExtensionConfig", func() { + it("encodes the extension config to TOML", func() { + + err := cargo.EncodeExtensionConfig(buffer, cargo.ExtensionConfig{ + API: "0.7", + Extension: cargo.ConfigExtension{ + ID: "some-extension-id", + Name: "some-extension-name", + Version: "some-extension-version", + Homepage: "some-extension-homepage", + Description: "some-extension-description", + Keywords: []string{"some-extension-keyword"}, + Licenses: []cargo.ConfigExtensionLicense{ + { + Type: "some-license-type", + URI: "some-license-uri", + }, + }, + }, + Metadata: cargo.ConfigExtensionMetadata{ + IncludeFiles: []string{ + "some-include-file", + "other-include-file", + }, + PrePackage: "some-pre-package-script.sh", + Dependencies: []cargo.ConfigExtensionMetadataDependency{ + { + Checksum: "sha256:some-sum", + ID: "some-dependency", + Licenses: []interface{}{"fancy-license", "fancy-license-2"}, + Name: "Some Dependency", + SHA256: "shasum", + Source: "source", + SourceChecksum: "sha256:source-shasum", + SourceSHA256: "source-shasum", + Stacks: []string{"io.buildpacks.stacks.bionic", "org.cloudfoundry.stacks.tiny"}, + URI: "http://some-url", + Version: "1.2.3", + }, + }, + Configurations: []cargo.ConfigExtensionMetadataConfiguration{ + { + Default: "0", + Description: "some-metadata-configuration-description", + Launch: true, + Name: "SOME_METADATA_CONFIGURATION_NAME", + Build: true, + }, + }, + DefaultVersions: map[string]string{ + "some-dependency": "1.2.x", + }, + }, + }) + Expect(err).NotTo(HaveOccurred()) + Expect(buffer.String()).To(MatchTOML(` +api = "0.7" + +[extension] + description = "some-extension-description" + homepage = "some-extension-homepage" + id = "some-extension-id" + keywords = ["some-extension-keyword"] + name = "some-extension-name" + version = "some-extension-version" + + [[extension.licenses]] + type = "some-license-type" + uri = "some-license-uri" + +[metadata] + include-files = ["some-include-file", "other-include-file"] + pre-package = "some-pre-package-script.sh" + + [[metadata.configurations]] + build = true + default = "0" + description = "some-metadata-configuration-description" + launch = true + name = "SOME_METADATA_CONFIGURATION_NAME" + [metadata.default-versions] + some-dependency = "1.2.x" + + [[metadata.dependencies]] + checksum = "sha256:some-sum" + id = "some-dependency" + licenses = ["fancy-license", "fancy-license-2"] + name = "Some Dependency" + sha256 = "shasum" + source = "source" + source-checksum = "sha256:source-shasum" + source_sha256 = "source-shasum" + stacks = ["io.buildpacks.stacks.bionic", "org.cloudfoundry.stacks.tiny"] + uri = "http://some-url" + version = "1.2.3" +`)) + }) + + context("when the config dependency licenses are structured like ConfigExtensionLicenses", func() { + it("encodes the config to TOML", func() { + + err := cargo.EncodeExtensionConfig(buffer, cargo.ExtensionConfig{ + API: "0.7", + Extension: cargo.ConfigExtension{ + ID: "some-extension-id", + Name: "some-extension-name", + Version: "some-extension-version", + Homepage: "some-extension-homepage", + Description: "some-extension-description", + Keywords: []string{"some-extension-keyword"}, + Licenses: []cargo.ConfigExtensionLicense{ + { + Type: "some-license-type", + URI: "some-license-uri", + }, + }, + }, + Metadata: cargo.ConfigExtensionMetadata{ + IncludeFiles: []string{ + "some-include-file", + "other-include-file", + }, + PrePackage: "some-pre-package-script.sh", + Dependencies: []cargo.ConfigExtensionMetadataDependency{ + { + Checksum: "sha256:some-sum", + ID: "some-dependency", + Licenses: []interface{}{ + cargo.ConfigBuildpackLicense{ + Type: "fancy-license", + URI: "some-license-uri", + }, + cargo.ConfigBuildpackLicense{ + Type: "fancy-license-2", + URI: "some-license-uri", + }, + }, Name: "Some Dependency", + SHA256: "shasum", + Source: "source", + SourceChecksum: "sha256:source-shasum", + SourceSHA256: "source-shasum", + Stacks: []string{"io.buildpacks.stacks.bionic", "org.cloudfoundry.stacks.tiny"}, + URI: "http://some-url", + Version: "1.2.3", + }, + }, + Configurations: []cargo.ConfigExtensionMetadataConfiguration{ + { + Default: "0", + Description: "some-metadata-configuration-description", + Launch: true, + Name: "SOME_METADATA_CONFIGURATION_NAME", + Build: true, + }, + }, + DefaultVersions: map[string]string{ + "some-dependency": "1.2.x", + }, + }, + }) + Expect(err).NotTo(HaveOccurred()) + Expect(buffer.String()).To(MatchTOML(` +api = "0.7" + +[extension] + description = "some-extension-description" + homepage = "some-extension-homepage" + id = "some-extension-id" + keywords = ["some-extension-keyword"] + name = "some-extension-name" + version = "some-extension-version" + + [[extension.licenses]] + type = "some-license-type" + uri = "some-license-uri" + +[metadata] + include-files = ["some-include-file", "other-include-file"] + pre-package = "some-pre-package-script.sh" + + [[metadata.configurations]] + build = true + default = "0" + description = "some-metadata-configuration-description" + launch = true + name = "SOME_METADATA_CONFIGURATION_NAME" + [metadata.default-versions] + some-dependency = "1.2.x" + + [[metadata.dependencies]] + checksum = "sha256:some-sum" + id = "some-dependency" + name = "Some Dependency" + sha256 = "shasum" + source = "source" + source-checksum = "sha256:source-shasum" + source_sha256 = "source-shasum" + stacks = ["io.buildpacks.stacks.bionic", "org.cloudfoundry.stacks.tiny"] + uri = "http://some-url" + version = "1.2.3" + + [[metadata.dependencies.licenses]] + type = "fancy-license" + uri = "some-license-uri" + + [[metadata.dependencies.licenses]] + type = "fancy-license-2" + uri = "some-license-uri" +`)) + }) + }) + }) + + context("DecodeExtensionConfig", func() { + it("decodes TOML to Extensionconfig", func() { + tomlBuffer := strings.NewReader(` +api = "0.7" + +[extension] +id = "some-extension-id" +name = "some-extension-name" +version = "some-extension-version" +homepage = "some-extension-homepage" +description = "some-extension-description" +keywords = [ "some-extension-keyword" ] + +[[extension.licenses]] + type = "some-license-type" + uri = "some-license-uri" + +[metadata] + include-files = ["some-include-file", "other-include-file"] + pre-package = "some-pre-package-script.sh" + +[metadata.default-versions] + some-dependency = "1.2.x" + +[[metadata.dependencies]] + checksum = "sha256:some-sum" + id = "some-dependency" + licenses = ["fancy-license", "fancy-license-2"] + name = "Some Dependency" + sha256 = "shasum" + source = "source" + source-checksum = "sha256:source-shasum" + source_sha256 = "source-shasum" + stacks = ["io.buildpacks.stacks.bionic", "org.cloudfoundry.stacks.tiny"] + uri = "http://some-url" + version = "1.2.3" + +[[metadata.configurations]] + default = "0" + description = "some-metadata-configuration-description" + launch = true + name = "SOME_METADATA_CONFIGURATION_NAME" + build = true +`) + + var config cargo.ExtensionConfig + Expect(cargo.DecodeExtensionConfig(tomlBuffer, &config)).To(Succeed()) + Expect(config).To(Equal(cargo.ExtensionConfig{ + API: "0.7", + Extension: cargo.ConfigExtension{ + ID: "some-extension-id", + Name: "some-extension-name", + Version: "some-extension-version", + Homepage: "some-extension-homepage", + Description: "some-extension-description", + Keywords: []string{"some-extension-keyword"}, + Licenses: []cargo.ConfigExtensionLicense{ + { + Type: "some-license-type", + URI: "some-license-uri", + }, + }, + }, + Metadata: cargo.ConfigExtensionMetadata{ + IncludeFiles: []string{ + "some-include-file", + "other-include-file", + }, + PrePackage: "some-pre-package-script.sh", + Dependencies: []cargo.ConfigExtensionMetadataDependency{ + { + Checksum: "sha256:some-sum", + ID: "some-dependency", + Licenses: []interface{}{"fancy-license", "fancy-license-2"}, + Name: "Some Dependency", + SHA256: "shasum", + Source: "source", + SourceChecksum: "sha256:source-shasum", + SourceSHA256: "source-shasum", + Stacks: []string{"io.buildpacks.stacks.bionic", "org.cloudfoundry.stacks.tiny"}, + URI: "http://some-url", + Version: "1.2.3", + }, + }, + Configurations: []cargo.ConfigExtensionMetadataConfiguration{ + { + Default: "0", + Description: "some-metadata-configuration-description", + Launch: true, + Name: "SOME_METADATA_CONFIGURATION_NAME", + Build: true, + }, + }, + DefaultVersions: map[string]string{ + "some-dependency": "1.2.x", + }, + }, + })) + }) + + context("dependency license are not a list of IDs", func() { + it("decodes TOML to extensionConfig", func() { + tomlBuffer := strings.NewReader(` +api = "0.2" + +[extension] + id = "some-extension-id" + name = "some-extension-name" + version = "some-extension-version" + homepage = "some-extension-homepage" + + [[extension.licenses]] + type = "some-license-type" + uri = "some-license-uri" + +[metadata] + include-files = ["some-include-file", "other-include-file"] + pre-package = "some-pre-package-script.sh" + +[metadata.default-versions] + some-dependency = "1.2.x" + +[[metadata.some-map]] + key = "value" + +[[metadata.dependencies]] + checksum = "sha256:some-sum" + id = "some-dependency" + name = "Some Dependency" + sha256 = "shasum" + source = "source" + source-checksum = "sha256:source-shasum" + source_sha256 = "source-shasum" + stacks = ["io.buildpacks.stacks.bionic", "org.cloudfoundry.stacks.tiny"] + uri = "http://some-url" + version = "1.2.3" + + [[metadata.dependencies.licenses]] + type = "fancy-license" + uri = "some-license-uri" + + [[metadata.dependencies.licenses]] + type = "fancy-license-2" + uri = "some-license-uri" +`) + + var config cargo.ExtensionConfig + Expect(cargo.DecodeExtensionConfig(tomlBuffer, &config)).To(Succeed()) + Expect(config).To(Equal(cargo.ExtensionConfig{ + API: "0.2", + Extension: cargo.ConfigExtension{ + ID: "some-extension-id", + Name: "some-extension-name", + Version: "some-extension-version", + Homepage: "some-extension-homepage", + Licenses: []cargo.ConfigExtensionLicense{ + { + Type: "some-license-type", + URI: "some-license-uri", + }, + }, + }, + Metadata: cargo.ConfigExtensionMetadata{ + IncludeFiles: []string{ + "some-include-file", + "other-include-file", + }, + PrePackage: "some-pre-package-script.sh", + Dependencies: []cargo.ConfigExtensionMetadataDependency{ + { + Checksum: "sha256:some-sum", + ID: "some-dependency", + Licenses: []interface{}{ + map[string]interface{}{ + "type": "fancy-license", + "uri": "some-license-uri", + }, + map[string]interface{}{ + "type": "fancy-license-2", + "uri": "some-license-uri", + }, + }, + Name: "Some Dependency", + SHA256: "shasum", + Source: "source", + SourceChecksum: "sha256:source-shasum", + SourceSHA256: "source-shasum", + Stacks: []string{"io.buildpacks.stacks.bionic", "org.cloudfoundry.stacks.tiny"}, + URI: "http://some-url", + Version: "1.2.3", + }, + }, + DefaultVersions: map[string]string{ + "some-dependency": "1.2.x", + }, + }, + })) + }) + + }) + + context("failure cases", func() { + context("when a bad reader is passed in", func() { + it("returns an error", func() { + err := cargo.DecodeExtensionConfig(errorReader{}, &cargo.ExtensionConfig{}) + Expect(err).To(MatchError(ContainSubstring("failed to read"))) + }) + }) + }) + }) +} diff --git a/cargo/extension_parser.go b/cargo/extension_parser.go new file mode 100644 index 00000000..9860f15b --- /dev/null +++ b/cargo/extension_parser.go @@ -0,0 +1,24 @@ +package cargo + +import "os" + +type ExtensionParser struct{} + +func NewExtensionParser() ExtensionParser { + return ExtensionParser{} +} + +func (p ExtensionParser) Parse(path string) (ExtensionConfig, error) { + file, err := os.Open(path) + if err != nil { + return ExtensionConfig{}, err + } + + var config ExtensionConfig + err = DecodeExtensionConfig(file, &config) + if err != nil { + return ExtensionConfig{}, err + } + + return config, nil +} diff --git a/cargo/extension_parser_test.go b/cargo/extension_parser_test.go new file mode 100644 index 00000000..271b5479 --- /dev/null +++ b/cargo/extension_parser_test.go @@ -0,0 +1,116 @@ +package cargo_test + +import ( + "os" + "testing" + + "github.com/paketo-buildpacks/packit/v2/cargo" + "github.com/sclevine/spec" + + . "github.com/onsi/gomega" +) + +func testExtensionParser(t *testing.T, context spec.G, it spec.S) { + var ( + Expect = NewWithT(t).Expect + + path string + parser cargo.ExtensionParser + ) + + it.Before(func() { + file, err := os.CreateTemp("", "extension.toml") + Expect(err).NotTo(HaveOccurred()) + + _, err = file.WriteString(`api = "0.7" +[extension] +id = "some-extension-id" +name = "some-extension-name" +version = "some-extension-version" + +[metadata] + include-files = ["some-include-file", "other-include-file"] + pre-package = "some-pre-package-script.sh" + +[[metadata.some-map]] + key = "value" + +[[metadata.dependencies]] + id = "some-dependency" + name = "Some Dependency" + sha256 = "shasum" + source = "source" + source_sha256 = "source-shasum" + stacks = ["io.buildpacks.stacks.bionic", "org.cloudfoundry.stacks.tiny"] + uri = "http://some-url" + version = "1.2.3" +`) + Expect(err).NotTo(HaveOccurred()) + + Expect(file.Close()).To(Succeed()) + + path = file.Name() + + parser = cargo.NewExtensionParser() + }) + + it.After(func() { + Expect(os.RemoveAll(path)).To(Succeed()) + }) + + context("Parse", func() { + it("parses a given extension.toml", func() { + config, err := parser.Parse(path) + Expect(err).NotTo(HaveOccurred()) + Expect(config).To(Equal(cargo.ExtensionConfig{ + API: "0.7", + Extension: cargo.ConfigExtension{ + ID: "some-extension-id", + Name: "some-extension-name", + Version: "some-extension-version", + }, + Metadata: cargo.ConfigExtensionMetadata{ + IncludeFiles: []string{ + "some-include-file", + "other-include-file", + }, + PrePackage: "some-pre-package-script.sh", + Dependencies: []cargo.ConfigExtensionMetadataDependency{ + { + ID: "some-dependency", + Name: "Some Dependency", + SHA256: "shasum", + Source: "source", + SourceSHA256: "source-shasum", + Stacks: []string{"io.buildpacks.stacks.bionic", "org.cloudfoundry.stacks.tiny"}, + URI: "http://some-url", + Version: "1.2.3", + }, + }, + }, + })) + }) + + context("when the extension.toml does not exist", func() { + it.Before(func() { + Expect(os.Remove(path)).To(Succeed()) + }) + + it("returns an error", func() { + _, err := parser.Parse(path) + Expect(err).To(MatchError(ContainSubstring("no such file or directory"))) + }) + }) + + context("when the extension.toml is malformed", func() { + it.Before(func() { + Expect(os.WriteFile(path, []byte("%%%"), 0644)).To(Succeed()) + }) + + it("returns an error", func() { + _, err := parser.Parse(path) + Expect(err).To(MatchError(ContainSubstring("expected '.' or '=', but got '%' instead"))) + }) + }) + }) +} diff --git a/cargo/init_test.go b/cargo/init_test.go index 275c2173..4c4545b2 100644 --- a/cargo/init_test.go +++ b/cargo/init_test.go @@ -11,7 +11,9 @@ import ( func TestUnitCargo(t *testing.T) { suite := spec.New("cargo", spec.Report(report.Terminal{})) suite("BuildpackParser", testBuildpackParser) + suite("ExtensionParser", testExtensionParser) suite("Config", testConfig) + suite("ExtensionConfig", testExtensionConfig) suite("DirectoryDuplicator", testDirectoryDuplicator) suite("Transport", testTransport) suite("ValidatedReader", testValidatedReader)