diff --git a/build.go b/build.go index 5a5ab19..fe54b3e 100644 --- a/build.go +++ b/build.go @@ -55,8 +55,14 @@ type BuildContext struct { // Platform is the contents of the platform. Platform Platform - // StackID is the ID of the stack. + // Deprecated: StackID is the ID of the stack. StackID string + + // TargetInfo contains info of the target (os, arch, ...). + TargetInfo TargetInfo + + // TargetDistro is the target distribution (name, version). + TargetDistro TargetDistro } // BuildResult contains the results of detection. @@ -87,7 +93,7 @@ const ( MinSupportedBPVersion = "0.8" // MaxSupportedBPVersion indicates the maximum supported version of the Buildpacks API - MaxSupportedBPVersion = "0.9" + MaxSupportedBPVersion = "0.10" ) // NewBuildResult creates a new BuildResult instance, initializing empty fields. @@ -231,6 +237,19 @@ func Build(build BuildFunc, config Config) { config.logger.Debugf("Stack: %s", ctx.StackID) } + if API.GreaterThan(semver.MustParse("0.9")) { + ctx.TargetInfo = TargetInfo{} + ctx.TargetInfo.OS, _ = os.LookupEnv(EnvTargetOS) + ctx.TargetInfo.Arch, _ = os.LookupEnv(EnvTargetArch) + ctx.TargetInfo.Variant, _ = os.LookupEnv(EnvTargetArchVariant) + config.logger.Debugf("System: %+v", ctx.TargetInfo) + + ctx.TargetDistro = TargetDistro{} + ctx.TargetDistro.Name, _ = os.LookupEnv(EnvTargetDistroName) + ctx.TargetDistro.Version, _ = os.LookupEnv(EnvTargetDistroVersion) + config.logger.Debugf("Distro: %+v", ctx.TargetDistro) + } + result, err := build(ctx) if err != nil { config.exitHandler.Error(err) diff --git a/build_test.go b/build_test.go index 7a951ca..730ca5e 100644 --- a/build_test.go +++ b/build_test.go @@ -165,6 +165,12 @@ test-key = "test-value" Expect(os.Setenv("CNB_PLATFORM_DIR", platformPath)).To(Succeed()) Expect(os.Setenv("CNB_BP_PLAN_PATH", buildpackPlanPath)).To(Succeed()) + Expect(os.Setenv("CNB_TARGET_OS", "linux")).To(Succeed()) + Expect(os.Setenv("CNB_TARGET_ARCH", "arm")).To(Succeed()) + Expect(os.Setenv("CNB_TARGET_ARCH_VARIANT", "v6")).To(Succeed()) + Expect(os.Setenv("CNB_TARGET_DISTRO_NAME", "ubuntu")).To(Succeed()) + Expect(os.Setenv("CNB_TARGET_DISTRO_VERSION", "24.04")).To(Succeed()) + workingDir, err = os.Getwd() Expect(err).NotTo(HaveOccurred()) Expect(os.Chdir(applicationPath)).To(Succeed()) @@ -178,6 +184,12 @@ test-key = "test-value" Expect(os.Unsetenv("CNB_BP_PLAN_PATH")).To(Succeed()) Expect(os.Unsetenv("CNB_LAYERS_DIR")).To(Succeed()) + Expect(os.Unsetenv("CNB_TARGET_OS")) + Expect(os.Unsetenv("CNB_TARGET_ARCH")) + Expect(os.Unsetenv("CNB_TARGET_ARCH_VARIANT")) + Expect(os.Unsetenv("CNB_TARGET_DISTRO_NAME")) + Expect(os.Unsetenv("CNB_TARGET_DISTRO_VERSION")) + Expect(os.RemoveAll(applicationPath)).To(Succeed()) Expect(os.RemoveAll(buildpackPath)).To(Succeed()) Expect(os.RemoveAll(buildpackPlanPath)).To(Succeed()) @@ -320,6 +332,69 @@ version = "1.1.1" }) }) + context("has a build environment specifying target metadata", func() { + var ctx libcnb.BuildContext + + it.Before(func() { + Expect(os.WriteFile(filepath.Join(buildpackPath, "buildpack.toml"), + []byte(` + api = "0.10" + + [buildpack] + id = "test-id" + name = "test-name" + version = "1.1.1" + + [[targets]] + os = "linux" + arch = "amd64" + + [[targets.distros]] + name = "ubuntu" + version = "18.04" + + [[targets.distros]] + name = "debian" + + [[targets]] + os = "linux" + arch = "arm" + variant = "v6" + `), 0600), + ).To(Succeed()) + + buildFunc = func(context libcnb.BuildContext) (libcnb.BuildResult, error) { + ctx = context + return libcnb.NewBuildResult(), nil + } + }) + + it("provides target information", func() { + libcnb.Build(buildFunc, + libcnb.NewConfig( + libcnb.WithArguments([]string{commandPath}), + libcnb.WithLogger(log.New(os.Stdout)), + ), + ) + + Expect(ctx.Buildpack.Targets).To(HaveLen(2)) + Expect(ctx.Buildpack.Targets[0].OS).To(Equal("linux")) + Expect(ctx.Buildpack.Targets[0].Arch).To(Equal("amd64")) + Expect(ctx.Buildpack.Targets[0].Distros).To(HaveLen(2)) + Expect(ctx.Buildpack.Targets[0].Distros[0].Name).To(Equal("ubuntu")) + Expect(ctx.Buildpack.Targets[0].Distros[0].Version).To(Equal("18.04")) + Expect(ctx.Buildpack.Targets[0].Distros[1].Name).To(Equal("debian")) + + Expect(ctx.Buildpack.Targets[1].Variant).To(Equal("v6")) + + Expect(ctx.TargetInfo.OS).To(Equal("linux")) + Expect(ctx.TargetInfo.Arch).To(Equal("arm")) + Expect(ctx.TargetInfo.Variant).To(Equal("v6")) + Expect(ctx.TargetDistro.Name).To(Equal("ubuntu")) + Expect(ctx.TargetDistro.Version).To(Equal("24.04")) + }) + }) + it("fails if CNB_BUILDPACK_DIR is not set", func() { Expect(os.Unsetenv("CNB_BUILDPACK_DIR")).To(Succeed()) diff --git a/buildpack.go b/buildpack.go index a81ba2c..d6195d9 100644 --- a/buildpack.go +++ b/buildpack.go @@ -76,7 +76,7 @@ type BuildpackOrder struct { Groups []BuildpackOrderBuildpack `toml:"group"` } -// BuildpackStack is a stack supported by the buildpack. +// Deprecated: BuildpackStack is a stack supported by the buildpack. type BuildpackStack struct { // ID is the id of the stack. ID string `toml:"id"` @@ -85,6 +85,35 @@ type BuildpackStack struct { Mixins []string `toml:"mixins"` } +// TargetDistro is the supported target distro +type TargetDistro struct { + // Name is the name of the supported distro. + Name string `toml:"name"` + + // Version is the version of the supported distro. + Version string `toml:"version"` +} + +// TargetInfo is the supported target +type TargetInfo struct { + // OS is the supported os. + OS string `toml:"os"` + + // Arch is the supported architecture. + Arch string `toml:"arch"` + + // Variant is the supported variant of the architecture. + Variant string `toml:"variant"` +} + +// Target is a target supported by the buildpack. +type Target struct { + TargetInfo + + // Distros is the collection of distros associated with the target. + Distros []TargetDistro `toml:"distros"` +} + // Buildpack is the contents of the buildpack.toml file. type Buildpack struct { // API is the api version expected by the buildpack. @@ -96,9 +125,12 @@ type Buildpack struct { // Path is the path to the buildpack. Path string `toml:"-"` - // Stacks is the collection of stacks supported by the buildpack. + // Deprecated: Stacks is the collection of stacks supported by the buildpack. Stacks []BuildpackStack `toml:"stacks"` + // Targets is the collection of targets supported by the buildpack. + Targets []Target `toml:"targets"` + // Metadata is arbitrary metadata attached to the buildpack. Metadata map[string]interface{} `toml:"metadata"` } diff --git a/extension.go b/extension.go index 763ede1..d100799 100644 --- a/extension.go +++ b/extension.go @@ -51,6 +51,9 @@ type Extension struct { // Path is the path to the extension. Path string `toml:"-"` + // Targets is the collection of targets supported by the buildpack. + Targets []Target `toml:"targets"` + // Metadata is arbitrary metadata attached to the extension. Metadata map[string]interface{} `toml:"metadata"` } diff --git a/generate.go b/generate.go index 600b13c..701d2d8 100644 --- a/generate.go +++ b/generate.go @@ -50,7 +50,13 @@ type GenerateContext struct { // Platform is the contents of the platform. Platform Platform - // StackID is the ID of the stack. + // TargetInfo contains info of the target (os, arch, ...). + TargetInfo TargetInfo + + // TargetDistro is the target distribution (name, version). + TargetDistro TargetDistro + + // Deprecated: StackID is the ID of the stack. StackID string } @@ -58,10 +64,30 @@ type GenerateContext struct { type GenerateResult struct { // Unmet contains buildpack plan entries that were not satisfied by the buildpack and therefore should be // passed to subsequent providers. - Unmet []UnmetPlanEntry + Unmet []UnmetPlanEntry + RunDockerfile []byte + BuildDockerfile []byte + Config *ExtendConfig +} + +// DockerfileArg is a Dockerfile argument +type DockerfileArg struct { + Name string `toml:"name"` + Value string `toml:"value"` } -// NewBuildResult creates a new BuildResult instance, initializing empty fields. +// BuildConfig contains additional arguments passed to the generated Dockerfiles +type BuildConfig struct { + Args []DockerfileArg `toml:"args"` +} + +// ExtendConfig contains additional configuration for the Dockerfiles +type ExtendConfig struct { + Build BuildConfig `toml:"build"` + Run BuildConfig `toml:"run"` +} + +// NewGenerateResult creates a new BuildResult instance, initializing empty fields. func NewGenerateResult() GenerateResult { return GenerateResult{} } @@ -73,7 +99,7 @@ func (b GenerateResult) String() string { ) } -// BuildFunc takes a context and returns a result, performing extension generate behaviors. +// GenerateFunc takes a context and returns a result, performing extension generate behaviors. type GenerateFunc func(context GenerateContext) (GenerateResult, error) // Generate is called by the main function of a extension, for generate phase @@ -184,10 +210,52 @@ func Generate(generate GenerateFunc, config Config) { config.logger.Debugf("Stack: %s", ctx.StackID) } + if API.GreaterThan(semver.MustParse("0.9")) { + ctx.TargetInfo = TargetInfo{} + ctx.TargetInfo.OS, _ = os.LookupEnv(EnvTargetOS) + ctx.TargetInfo.Arch, _ = os.LookupEnv(EnvTargetArch) + ctx.TargetInfo.Variant, _ = os.LookupEnv(EnvTargetArchVariant) + config.logger.Debugf("System: %+v", ctx.TargetInfo) + + ctx.TargetDistro = TargetDistro{} + ctx.TargetDistro.Name, _ = os.LookupEnv(EnvTargetDistroName) + ctx.TargetDistro.Version, _ = os.LookupEnv(EnvTargetDistroVersion) + config.logger.Debugf("Distro: %+v", ctx.TargetDistro) + } + result, err := generate(ctx) if err != nil { config.exitHandler.Error(err) return } config.logger.Debugf("Result: %+v", result) + + if len(result.RunDockerfile) > 0 { + // #nosec + if err := os.WriteFile(filepath.Join(ctx.OutputDirectory, "run.Dockerfile"), result.RunDockerfile, 0644); err != nil { + config.exitHandler.Error(err) + return + } + } + + if len(result.BuildDockerfile) > 0 { + // #nosec + if err := os.WriteFile(filepath.Join(ctx.OutputDirectory, "build.Dockerfile"), result.BuildDockerfile, 0644); err != nil { + config.exitHandler.Error(err) + return + } + } + + if result.Config != nil { + configFile, err := os.Create(filepath.Join(ctx.OutputDirectory, "extend-config.toml")) + if err != nil { + config.exitHandler.Error(err) + return + } + + if err := toml.NewEncoder(configFile).Encode(result.Config); err != nil { + config.exitHandler.Error(err) + return + } + } } diff --git a/generate_test.go b/generate_test.go index 54a77d2..2f9be6a 100644 --- a/generate_test.go +++ b/generate_test.go @@ -76,8 +76,8 @@ api = "{{.APIVersion}}" id = "test-id" name = "test-name" version = "1.1.1" -description = "A test buildpack" -keywords = ["test", "buildpack"] +description = "A test extension" +keywords = ["test", "extension"] [[extension.licenses]] type = "Apache-2.0" @@ -147,6 +147,12 @@ test-key = "test-value" Expect(os.Setenv("CNB_PLATFORM_DIR", platformPath)).To(Succeed()) Expect(os.Setenv("CNB_BP_PLAN_PATH", buildpackPlanPath)).To(Succeed()) + Expect(os.Setenv("CNB_TARGET_OS", "linux")).To(Succeed()) + Expect(os.Setenv("CNB_TARGET_ARCH", "arm")).To(Succeed()) + Expect(os.Setenv("CNB_TARGET_ARCH_VARIANT", "v6")).To(Succeed()) + Expect(os.Setenv("CNB_TARGET_DISTRO_NAME", "ubuntu")).To(Succeed()) + Expect(os.Setenv("CNB_TARGET_DISTRO_VERSION", "24.04")).To(Succeed()) + workingDir, err = os.Getwd() Expect(err).NotTo(HaveOccurred()) Expect(os.Chdir(applicationPath)).To(Succeed()) @@ -160,6 +166,12 @@ test-key = "test-value" Expect(os.Unsetenv("CNB_BP_PLAN_PATH")).To(Succeed()) Expect(os.Unsetenv("CNB_OUTPUT_DIR")).To(Succeed()) + Expect(os.Unsetenv("CNB_TARGET_OS")) + Expect(os.Unsetenv("CNB_TARGET_ARCH")) + Expect(os.Unsetenv("CNB_TARGET_ARCH_VARIANT")) + Expect(os.Unsetenv("CNB_TARGET_DISTRO_NAME")) + Expect(os.Unsetenv("CNB_TARGET_DISTRO_VERSION")) + Expect(os.RemoveAll(applicationPath)).To(Succeed()) Expect(os.RemoveAll(extensionPath)).To(Succeed()) Expect(os.RemoveAll(buildpackPlanPath)).To(Succeed()) @@ -300,6 +312,69 @@ version = "1.1.1" }) }) + context("has a build environment specifying target metadata", func() { + var ctx libcnb.GenerateContext + + it.Before(func() { + Expect(os.WriteFile(filepath.Join(extensionPath, "extension.toml"), + []byte(` + api = "0.10" + + [extension] + id = "test-id" + name = "test-name" + version = "1.1.1" + + [[targets]] + os = "linux" + arch = "amd64" + + [[targets.distros]] + name = "ubuntu" + version = "18.04" + + [[targets.distros]] + name = "debian" + + [[targets]] + os = "linux" + arch = "arm" + variant = "v6" + `), 0600), + ).To(Succeed()) + + generateFunc = func(context libcnb.GenerateContext) (libcnb.GenerateResult, error) { + ctx = context + return libcnb.NewGenerateResult(), nil + } + }) + + it("provides target information", func() { + libcnb.Generate(generateFunc, + libcnb.NewConfig( + libcnb.WithArguments([]string{commandPath}), + libcnb.WithLogger(log.New(os.Stdout)), + ), + ) + + Expect(ctx.Extension.Targets).To(HaveLen(2)) + Expect(ctx.Extension.Targets[0].OS).To(Equal("linux")) + Expect(ctx.Extension.Targets[0].Arch).To(Equal("amd64")) + Expect(ctx.Extension.Targets[0].Distros).To(HaveLen(2)) + Expect(ctx.Extension.Targets[0].Distros[0].Name).To(Equal("ubuntu")) + Expect(ctx.Extension.Targets[0].Distros[0].Version).To(Equal("18.04")) + Expect(ctx.Extension.Targets[0].Distros[1].Name).To(Equal("debian")) + + Expect(ctx.Extension.Targets[1].Variant).To(Equal("v6")) + + Expect(ctx.TargetInfo.OS).To(Equal("linux")) + Expect(ctx.TargetInfo.Arch).To(Equal("arm")) + Expect(ctx.TargetInfo.Variant).To(Equal("v6")) + Expect(ctx.TargetDistro.Name).To(Equal("ubuntu")) + Expect(ctx.TargetDistro.Version).To(Equal("24.04")) + }) + }) + it("fails if CNB_EXTENSION_DIR is not set", func() { Expect(os.Unsetenv("CNB_EXTENSION_DIR")).To(Succeed()) @@ -328,10 +403,12 @@ version = "1.1.1" Expect(exitHandler.Calls[0].Arguments.Get(0)).To(MatchError("test-error")) }) - it("writes a Dockerfile", func() { + it("writes Dockerfiles", func() { generateFunc = func(ctx libcnb.GenerateContext) (libcnb.GenerateResult, error) { - os.WriteFile(filepath.Join(ctx.OutputDirectory, "build.Dockerfile"), []byte(""), 0600) - return libcnb.NewGenerateResult(), nil + result := libcnb.NewGenerateResult() + result.BuildDockerfile = []byte(`FROM foo:latest`) + result.RunDockerfile = []byte(`FROM bar:latest`) + return result, nil } libcnb.Generate(generateFunc, @@ -342,5 +419,40 @@ version = "1.1.1" ) Expect(filepath.Join(outputPath, "build.Dockerfile")).To(BeARegularFile()) + Expect(filepath.Join(outputPath, "run.Dockerfile")).To(BeARegularFile()) + }) + + it("writes extend-config.toml", func() { + generateFunc = func(ctx libcnb.GenerateContext) (libcnb.GenerateResult, error) { + result := libcnb.NewGenerateResult() + result.Config = &libcnb.ExtendConfig{ + Build: libcnb.BuildConfig{ + Args: []libcnb.DockerfileArg{ + { + Name: "foo", + Value: "bar", + }, + }, + }, + Run: libcnb.BuildConfig{ + Args: []libcnb.DockerfileArg{ + { + Name: "bar", + Value: "bazz", + }, + }, + }, + } + return result, nil + } + + libcnb.Generate(generateFunc, + libcnb.NewConfig( + libcnb.WithArguments([]string{commandPath, outputPath, platformPath, buildpackPlanPath}), + libcnb.WithTOMLWriter(tomlWriter), + libcnb.WithLogger(log.NewDiscard())), + ) + + Expect(filepath.Join(outputPath, "extend-config.toml")).To(BeARegularFile()) }) } diff --git a/platform.go b/platform.go index a9c09c5..e38d45b 100644 --- a/platform.go +++ b/platform.go @@ -65,9 +65,24 @@ const ( // EnvBuildPlanPath is the name of the environment variable that contains the path to the build plan EnvBuildPlanPath = "CNB_BP_PLAN_PATH" - // EnvStackID is the name of the environment variable that contains the stack id + // Deprecated: EnvStackID is the name of the environment variable that contains the stack id EnvStackID = "CNB_STACK_ID" + // EnvTargetOS contains the name of the os + EnvTargetOS = "CNB_TARGET_OS" + + // EnvTargetArch contains the architecture + EnvTargetArch = "CNB_TARGET_ARCH" + + // EnvTargetOS contains the variant of the architecture + EnvTargetArchVariant = "CNB_TARGET_ARCH_VARIANT" + + // EnvTargetDistroName contains the name of the ditro + EnvTargetDistroName = "CNB_TARGET_DISTRO_NAME" + + // EnvTargetDistroVersion contains the version of the distro + EnvTargetDistroVersion = "CNB_TARGET_DISTRO_VERSION" + // DefaultPlatformBindingsLocation is the typical location for bindings, which exists under the platform directory // // Not guaranteed to exist, but often does. This should only be used as a fallback if EnvServiceBindings and EnvPlatformDirectory are not set