From 37c1106b795c2b9702e62c14cd63268f9b0e8790 Mon Sep 17 00:00:00 2001 From: Costas Papastathis Date: Fri, 13 Sep 2024 17:11:52 +0300 Subject: [PATCH] Installing node from images.json (#203) * Fetch supported run images images.json in the build image --- constants/constants.go | 4 + extension.toml | 23 - generate.go | 120 +-- generate_test.go | 819 ++++++------------ init_test.go | 6 +- integration/fetch_run_image_from_env.go | 59 +- integration/init_test.go | 16 - integration/provides_test.go | 5 +- integration/simple_app_test.go | 7 +- internal/testhelpers/testhelpers.go | 74 ++ internal/utils/init_test.go | 21 + .../utils/templates}/build.Dockerfile | 0 .../utils/templates}/run.Dockerfile | 0 internal/utils/utils.go | 230 +++++ internal/utils/utils_test.go | 541 ++++++++++++ run/main.go | 7 +- structs/structs.go | 15 + 17 files changed, 1182 insertions(+), 765 deletions(-) create mode 100644 constants/constants.go create mode 100644 internal/testhelpers/testhelpers.go create mode 100644 internal/utils/init_test.go rename {templates => internal/utils/templates}/build.Dockerfile (100%) rename {templates => internal/utils/templates}/run.Dockerfile (100%) create mode 100644 internal/utils/utils.go create mode 100644 internal/utils/utils_test.go create mode 100644 structs/structs.go diff --git a/constants/constants.go b/constants/constants.go new file mode 100644 index 0000000..8bb8d65 --- /dev/null +++ b/constants/constants.go @@ -0,0 +1,4 @@ +package constants + +const DEFAULT_USER_ID = 1002 +const DEFAULT_GROUP_ID = 1000 diff --git a/extension.toml b/extension.toml index 7b23cdc..659b1a5 100644 --- a/extension.toml +++ b/extension.toml @@ -9,26 +9,3 @@ description = "This extension installs the appropriate Node.js runtime via dnf" [metadata] pre-package = "./scripts/build.sh" include-files = ["bin/generate", "bin/detect", "bin/run", "extension.toml"] - [metadata.default-versions] - node = "20.*.*" - - [[metadata.dependencies]] - id = "node" - name = "Ubi Node Extension" - stacks = ["io.buildpacks.stacks.ubi8"] - source = "paketocommunity/run-nodejs-20-ubi-base" - version = "20.1000" - - [[metadata.dependencies]] - id = "node" - name = "Ubi Node Extension" - stacks = ["io.buildpacks.stacks.ubi8"] - source = "paketocommunity/run-nodejs-18-ubi-base" - version = "18.1000" - - [[metadata.dependencies]] - id = "node" - name = "Ubi Node Extension" - stacks = ["io.buildpacks.stacks.ubi8"] - source = "paketocommunity/run-nodejs-16-ubi-base" - version = "16.1000" diff --git a/generate.go b/generate.go index f9d66b6..1120cb5 100644 --- a/generate.go +++ b/generate.go @@ -1,14 +1,11 @@ package ubinodejsextension import ( - "bytes" - _ "embed" "os" - "path/filepath" - "regexp" - "strconv" "strings" - "text/template" + + "github.com/paketo-community/ubi-nodejs-extension/internal/utils" + "github.com/paketo-community/ubi-nodejs-extension/structs" "github.com/Masterminds/semver/v3" "github.com/paketo-buildpacks/libnodejs" @@ -18,30 +15,8 @@ import ( "github.com/paketo-buildpacks/packit/v2/scribe" ) -var PACKAGES = "make gcc gcc-c++ libatomic_ops git openssl-devel nodejs npm nodejs-nodemon nss_wrapper which python3" - -var DEFAULT_USER_ID = 1002 -var DEFAULT_GROUP_ID = 1000 - -type DuringBuildPermissions struct { - CNB_USER_ID, CNB_GROUP_ID int -} - -//go:embed templates/build.Dockerfile -var buildDockerfileTemplate string - -type BuildDockerfileProps struct { - NODEJS_VERSION uint64 - CNB_USER_ID, CNB_GROUP_ID int - CNB_STACK_ID, PACKAGES string -} - -//go:embed templates/run.Dockerfile -var runDockerfileTemplate string - -type RunDockerfileProps struct { - Source string -} +const PACKAGES = "make gcc gcc-c++ libatomic_ops git openssl-devel nodejs npm nodejs-nodemon nss_wrapper which python3" +const CONFIG_TOML_PATH = "/tmp/config.toml" //go:generate faux --interface DependencyManager --output fakes/dependency_manager.go type DependencyManager interface { @@ -50,7 +25,7 @@ type DependencyManager interface { GenerateBillOfMaterials(dependencies ...postal.Dependency) []packit.BOMEntry } -func Generate(dependencyManager DependencyManager, logger scribe.Emitter, duringBuildPermissions DuringBuildPermissions) packit.GenerateFunc { +func Generate(dependencyManager DependencyManager, logger scribe.Emitter, duringBuildPermissions structs.DuringBuildPermissions, imagesJsonPath string) packit.GenerateFunc { return func(context packit.GenerateContext) (packit.GenerateResult, error) { logger.Title("%s %s", context.Info.Name, context.Info.Version) @@ -65,10 +40,19 @@ func Generate(dependencyManager DependencyManager, logger scribe.Emitter, during logger.Candidates(allNodeVersionsInPriorityOrder) - // Search and fetch the version from the extension.toml + configTomlFileContent, err := utils.GenerateConfigTomlContentFromImagesJson(imagesJsonPath, context.Stack) + if err != nil { + return packit.GenerateResult{}, err + } + + //save config.toml file + err = os.WriteFile(CONFIG_TOML_PATH, configTomlFileContent, 0644) + if err != nil { + return packit.GenerateResult{}, err + } + nodeVersion, _ := highestPriorityNodeVersion.Metadata["version"].(string) - extensionFilePath := filepath.Join(context.CNBPath, "extension.toml") - dependency, err := dependencyManager.Resolve(extensionFilePath, highestPriorityNodeVersion.Name, nodeVersion, context.Stack) + dependency, err := dependencyManager.Resolve(CONFIG_TOML_PATH, highestPriorityNodeVersion.Name, nodeVersion, context.Stack) if err != nil { return packit.GenerateResult{}, err } @@ -91,26 +75,23 @@ func Generate(dependencyManager DependencyManager, logger scribe.Emitter, during logger.Process("Selected Node Engine Major version %d", selectedNodeMajorVersion) - // These variables have to be fetched from the env - CNB_STACK_ID := os.Getenv("CNB_STACK_ID") - // Generating build.Dockerfile - buildDockerfileContent, err := FillPropsToTemplate(BuildDockerfileProps{ + buildDockerfileContent, err := utils.GenerateBuildDockerfile(structs.BuildDockerfileProps{ NODEJS_VERSION: selectedNodeMajorVersion, CNB_USER_ID: duringBuildPermissions.CNB_USER_ID, CNB_GROUP_ID: duringBuildPermissions.CNB_GROUP_ID, - CNB_STACK_ID: CNB_STACK_ID, + CNB_STACK_ID: context.Stack, PACKAGES: PACKAGES, - }, buildDockerfileTemplate) + }) if err != nil { return packit.GenerateResult{}, err } // Generating run.Dockerfile - runDockerfileContent, err := FillPropsToTemplate(RunDockerfileProps{ + runDockerfileContent, err := utils.GenerateRunDockerfile(structs.RunDockerfileProps{ Source: selectedNodeRunImage, - }, runDockerfileTemplate) + }) if err != nil { return packit.GenerateResult{}, err @@ -123,58 +104,3 @@ func Generate(dependencyManager DependencyManager, logger scribe.Emitter, during }, nil } } - -func FillPropsToTemplate(properties interface{}, templateString string) (result string, Error error) { - - templ, err := template.New("template").Parse(templateString) - if err != nil { - return "", err - } - - var buf bytes.Buffer - err = templ.Execute(&buf, properties) - if err != nil { - panic(err) - } - - return buf.String(), nil -} - -func GetDuringBuildPermissions(filepath string) DuringBuildPermissions { - - defaultPermissions := DuringBuildPermissions{ - CNB_USER_ID: DEFAULT_USER_ID, - CNB_GROUP_ID: DEFAULT_GROUP_ID, - } - re := regexp.MustCompile(`cnb:x:(\d+):(\d+)::`) - - etcPasswdFile, err := os.ReadFile(filepath) - - if err != nil { - return defaultPermissions - } - etcPasswdContent := string(etcPasswdFile) - - matches := re.FindStringSubmatch(etcPasswdContent) - - if len(matches) != 3 { - return defaultPermissions - } - - CNB_USER_ID, err := strconv.Atoi(matches[1]) - - if err != nil { - return defaultPermissions - } - - CNB_GROUP_ID, err := strconv.Atoi(matches[2]) - - if err != nil { - return defaultPermissions - } - - return DuringBuildPermissions{ - CNB_USER_ID: CNB_USER_ID, - CNB_GROUP_ID: CNB_GROUP_ID, - } -} diff --git a/generate_test.go b/generate_test.go index c3ae76d..6e96db8 100644 --- a/generate_test.go +++ b/generate_test.go @@ -14,6 +14,9 @@ import ( "github.com/paketo-buildpacks/packit/cargo" "github.com/paketo-buildpacks/packit/v2" ubinodejsextension "github.com/paketo-community/ubi-nodejs-extension" + "github.com/paketo-community/ubi-nodejs-extension/internal/testhelpers" + "github.com/paketo-community/ubi-nodejs-extension/internal/utils" + "github.com/paketo-community/ubi-nodejs-extension/structs" "github.com/sclevine/spec" "github.com/paketo-buildpacks/packit/v2/scribe" @@ -22,184 +25,18 @@ import ( postal "github.com/paketo-buildpacks/packit/v2/postal" ) -type RunDockerfileProps struct { - Source string -} - -//go:embed templates/run.Dockerfile -var runDockerfileTemplate string - -type BuildDockerfileProps struct { - NODEJS_VERSION uint64 - CNB_USER_ID, CNB_GROUP_ID int - CNB_STACK_ID, PACKAGES string -} - -//go:embed templates/build.Dockerfile -var buildDockerfileTemplate string - -func testFillPropsToTemplate(t *testing.T, context spec.G, it spec.S) { - - var ( - Expect = NewWithT(t).Expect - ) - - context("Adding props on templates with FillPropsToTemplate", func() { - - it("Should fill with properties the template/build.Dockerfile", func() { - - output, err := ubinodejsextension.FillPropsToTemplate(BuildDockerfileProps{ - NODEJS_VERSION: 16, - CNB_USER_ID: 1000, - CNB_GROUP_ID: 1000, - CNB_STACK_ID: "", - PACKAGES: ubinodejsextension.PACKAGES, - }, buildDockerfileTemplate) - - Expect(err).NotTo(HaveOccurred()) - Expect(output).To(Equal(fmt.Sprintf(`ARG base_image -FROM ${base_image} - -USER root - -ARG build_id=0 -RUN echo ${build_id} - -RUN microdnf -y module enable nodejs:16 -RUN microdnf --setopt=install_weak_deps=0 --setopt=tsflags=nodocs install -y %s && microdnf clean all - -RUN echo uid:gid "1000:1000" -USER 1000:1000 - -RUN echo "CNB_STACK_ID: "`, ubinodejsextension.PACKAGES))) - - }) - - it("Should fill with properties the template/run.Dockerfile", func() { - - RunDockerfileProps := RunDockerfileProps{ - Source: "paketocommunity/run-nodejs-18-ubi-base", - } - - output, err := ubinodejsextension.FillPropsToTemplate(RunDockerfileProps, runDockerfileTemplate) - - Expect(err).NotTo(HaveOccurred()) - Expect(output).To(Equal(`FROM paketocommunity/run-nodejs-18-ubi-base`)) - - }) - }) -} - -func testFetchingPermissionsFromEtchPasswdFile(t *testing.T, context spec.G, it spec.S) { - - var ( - Expect = NewWithT(t).Expect - tmpDir string - path string - err error - ) - - context("/etc/passwd exists and has the cnb user", func() { - - it("It should return the permissions specified for the cnb user", func() { - tmpDir, err = os.MkdirTemp("", "") - Expect(err).NotTo(HaveOccurred()) - - path = filepath.Join(tmpDir, "/passwd") - - Expect(os.WriteFile(path, []byte(`root:x:0:0:root:/root:/bin/bash -bin:x:1:1:bin:/bin:/sbin/nologin -daemon:x:2:2:daemon:/sbin:/sbin/nologin -adm:x:3:4:adm:/var/adm:/sbin/nologin -lp:x:4:7:lp:/var/spool/lpd:/sbin/nologin -sync:x:5:0:sync:/sbin:/bin/sync -shutdown:x:6:0:shutdown:/sbin:/sbin/shutdown -halt:x:7:0:halt:/sbin:/sbin/halt -mail:x:8:12:mail:/var/spool/mail:/sbin/nologin -operator:x:11:0:operator:/root:/sbin/nologin -games:x:12:100:games:/usr/games:/sbin/nologin -ftp:x:14:50:FTP User:/var/ftp:/sbin/nologin -cnb:x:1234:2345::/home/cnb:/bin/bash -nobody:x:65534:65534:Kernel Overflow User:/:/sbin/nologin -`), 0600)).To(Succeed()) - - duringBuilderPermissions := ubinodejsextension.GetDuringBuildPermissions(path) - - Expect(duringBuilderPermissions).To(Equal( - ubinodejsextension.DuringBuildPermissions{ - CNB_USER_ID: 1234, - CNB_GROUP_ID: 2345}, - )) - }) - }) - - context("/etc/passwd exists and does NOT have the cnb user", func() { - - it("It should return the default permissions", func() { - tmpDir, err = os.MkdirTemp("", "") - Expect(err).NotTo(HaveOccurred()) - - path = filepath.Join(tmpDir, "/passwd") - - Expect(os.WriteFile(path, []byte(`root:x:0:0:root:/root:/bin/bash -bin:x:1:1:bin:/bin:/sbin/nologin -daemon:x:2:2:daemon:/sbin:/sbin/nologin -adm:x:3:4:adm:/var/adm:/sbin/nologin -lp:x:4:7:lp:/var/spool/lpd:/sbin/nologin -sync:x:5:0:sync:/sbin:/bin/sync -shutdown:x:6:0:shutdown:/sbin:/sbin/shutdown -halt:x:7:0:halt:/sbin:/sbin/halt -mail:x:8:12:mail:/var/spool/mail:/sbin/nologin -operator:x:11:0:operator:/root:/sbin/nologin -games:x:12:100:games:/usr/games:/sbin/nologin -ftp:x:14:50:FTP User:/var/ftp:/sbin/nologin -nobody:x:65534:65534:Kernel Overflow User:/:/sbin/nologin -`), 0600)).To(Succeed()) - - duringBuilderPermissions := ubinodejsextension.GetDuringBuildPermissions(path) - - Expect(duringBuilderPermissions).To(Equal( - ubinodejsextension.DuringBuildPermissions{ - CNB_USER_ID: ubinodejsextension.DEFAULT_USER_ID, - CNB_GROUP_ID: ubinodejsextension.DEFAULT_GROUP_ID}, - )) - }) - }) - - context("/etc/passwd does NOT exist", func() { - - it("It should return the default permissions", func() { - tmpDir, err = os.MkdirTemp("", "") - Expect(err).NotTo(HaveOccurred()) - - duringBuilderPermissions := ubinodejsextension.GetDuringBuildPermissions(tmpDir) - - Expect(duringBuilderPermissions).To(Equal( - ubinodejsextension.DuringBuildPermissions{ - CNB_USER_ID: ubinodejsextension.DEFAULT_USER_ID, - CNB_GROUP_ID: ubinodejsextension.DEFAULT_GROUP_ID}, - )) - }) - }) -} - func testGenerate(t *testing.T, context spec.G, it spec.S) { var ( - Expect = NewWithT(t).Expect - workingDir string - planPath string - testBuildPlan packit.BuildpackPlan - buf = new(bytes.Buffer) - generateResult packit.GenerateResult - err error - cnbDir string - BuildDockerfileProps = ubinodejsextension.BuildDockerfileProps{ - CNB_USER_ID: 1002, - CNB_GROUP_ID: 1000, - CNB_STACK_ID: "", - PACKAGES: ubinodejsextension.PACKAGES, - } + Expect = NewWithT(t).Expect + workingDir string + imagesJsonTmpDir string + imagesJsonPath string + planPath string + testBuildPlan packit.BuildpackPlan + buf = new(bytes.Buffer) + generateResult packit.GenerateResult + err error generate packit.GenerateFunc buffer *bytes.Buffer logger scribe.Emitter @@ -212,17 +49,18 @@ func testGenerate(t *testing.T, context spec.G, it spec.S) { dependencyManager = postal.NewService(cargo.NewTransport()) }) - context("Generate called with NO node in buildplan", func() { + context("Generate called with NO node in build plan", func() { it.Before(func() { - - workingDir = t.TempDir() - Expect(err).NotTo(HaveOccurred()) - - generate = ubinodejsextension.Generate(dependencyManager, logger, ubinodejsextension.DuringBuildPermissions{CNB_USER_ID: 1002, CNB_GROUP_ID: 1000}) + generate = ubinodejsextension.Generate( + dependencyManager, + logger, + structs.DuringBuildPermissions{CNB_USER_ID: 1002, CNB_GROUP_ID: 1000}, + "/path/to/images.json") err = toml.NewEncoder(buf).Encode(testBuildPlan) Expect(err).NotTo(HaveOccurred()) + workingDir := t.TempDir() Expect(os.WriteFile(filepath.Join(workingDir, "plan"), buf.Bytes(), 0600)).To(Succeed()) err = os.Chdir(workingDir) @@ -233,7 +71,7 @@ func testGenerate(t *testing.T, context spec.G, it spec.S) { Expect(os.RemoveAll(workingDir)).To(Succeed()) }) - it("Node no longer requested in buildplan", func() { + it("the extension should exit with an error", func() { generateResult, err = generate(packit.GenerateContext{ WorkingDir: workingDir, @@ -241,22 +79,21 @@ func testGenerate(t *testing.T, context spec.G, it spec.S) { Entries: []packit.BuildpackPlanEntry{}, }, }) + Expect(err).To(HaveOccurred()) - Expect(generateResult.BuildDockerfile).To(BeNil()) + Expect(err.Error()).To(ContainSubstring("Node.js no longer requested by build plan")) + Expect(generateResult).To(Equal(packit.GenerateResult{})) }) }, spec.Sequential()) - context("Generate called with node in the buildplan", func() { + context("Generate called with node in the build plan", func() { it.Before(func() { - workingDir = t.TempDir() - cnbDir, err = os.MkdirTemp("", "cnb") - - generate = ubinodejsextension.Generate(dependencyManager, logger, ubinodejsextension.DuringBuildPermissions{CNB_USER_ID: 1002, CNB_GROUP_ID: 1000}) - err = toml.NewEncoder(buf).Encode(testBuildPlan) Expect(err).NotTo(HaveOccurred()) + workingDir = t.TempDir() + planPath = filepath.Join(workingDir, "plan") t.Setenv("CNB_BP_PLAN_PATH", planPath) @@ -268,179 +105,110 @@ func testGenerate(t *testing.T, context spec.G, it spec.S) { it.After(func() { Expect(os.RemoveAll(workingDir)).To(Succeed()) + Expect(os.RemoveAll(imagesJsonTmpDir)).To(Succeed()) }) it("Specific version of node requested", func() { - extensionToml, _ := readExtensionTomlTemplateFile() + imagesJsonContent := testhelpers.GenerateImagesJsonFile([]string{"16", "18"}, []bool{false, true}, false) + imagesJsonTmpDir = t.TempDir() + imagesJsonPath = filepath.Join(imagesJsonTmpDir, "images.json") + Expect(os.WriteFile(imagesJsonPath, []byte(imagesJsonContent), 0644)).To(Succeed()) - cnbDir, err = os.MkdirTemp("", "cnb") - Expect(err).NotTo(HaveOccurred()) - Expect(os.WriteFile(cnbDir+"/extension.toml", []byte(extensionToml), 0600)).To(Succeed()) + generate = ubinodejsextension.Generate( + dependencyManager, + logger, + structs.DuringBuildPermissions{CNB_USER_ID: 1002, CNB_GROUP_ID: 1000}, + imagesJsonPath, + ) versionTests := []struct { - Name string - Metadata map[string]interface{} - RunDockerfileProps ubinodejsextension.RunDockerfileProps - BuildDockerfileProps ubinodejsextension.BuildDockerfileProps - buildDockerfileExpectedNodejsVersion int + requestedNodeVersion string + expectedNodeVersion int }{ { - Name: "node", - Metadata: map[string]interface{}{ - "version": "16 - 18", - "version-source": "BP_NODE_VERSION", - }, - RunDockerfileProps: ubinodejsextension.RunDockerfileProps{ - Source: "paketocommunity/run-nodejs-18-ubi-base", - }, - BuildDockerfileProps: BuildDockerfileProps, - buildDockerfileExpectedNodejsVersion: 18, + requestedNodeVersion: "16 - 18", + expectedNodeVersion: 18, }, { - Name: "node", - Metadata: map[string]interface{}{ - "version": "16.0.0 - 18.0.0", - "version-source": "BP_NODE_VERSION", - }, - RunDockerfileProps: ubinodejsextension.RunDockerfileProps{ - Source: "paketocommunity/run-nodejs-16-ubi-base", - }, - BuildDockerfileProps: BuildDockerfileProps, - buildDockerfileExpectedNodejsVersion: 16, + requestedNodeVersion: "16.0.0 - 18.0.0", + expectedNodeVersion: 16, }, { - Name: "node", - Metadata: map[string]interface{}{ - "version": "<18.5.1", - "version-source": "BP_NODE_VERSION", - }, - RunDockerfileProps: ubinodejsextension.RunDockerfileProps{ - Source: "paketocommunity/run-nodejs-16-ubi-base", - }, - BuildDockerfileProps: BuildDockerfileProps, - buildDockerfileExpectedNodejsVersion: 16, + requestedNodeVersion: "<18.5.1", + expectedNodeVersion: 16, }, { - Name: "node", - Metadata: map[string]interface{}{ - "version": ">18.5.1", - "version-source": "BP_NODE_VERSION", - }, - RunDockerfileProps: ubinodejsextension.RunDockerfileProps{ - Source: "paketocommunity/run-nodejs-18-ubi-base", - }, - BuildDockerfileProps: BuildDockerfileProps, - buildDockerfileExpectedNodejsVersion: 18, + requestedNodeVersion: ">18.5.1", + expectedNodeVersion: 18, }, { - Name: "node", - Metadata: map[string]interface{}{ - "version": "16 <18.5.1", - "version-source": "BP_NODE_VERSION", - }, - RunDockerfileProps: ubinodejsextension.RunDockerfileProps{ - Source: "paketocommunity/run-nodejs-16-ubi-base", - }, - BuildDockerfileProps: BuildDockerfileProps, - buildDockerfileExpectedNodejsVersion: 16, + requestedNodeVersion: "16 <18.5.1", + expectedNodeVersion: 16, }, { - Name: "node", - Metadata: map[string]interface{}{ - "version": "<1.0.0 || >=2.5.2 <3.0.0 || >=2.3.1 <18.4.5", - "version-source": "BP_NODE_VERSION", - }, - RunDockerfileProps: ubinodejsextension.RunDockerfileProps{ - Source: "paketocommunity/run-nodejs-16-ubi-base", - }, - BuildDockerfileProps: BuildDockerfileProps, - buildDockerfileExpectedNodejsVersion: 16, + requestedNodeVersion: "<1.0.0 || >=2.5.2 <3.0.0 || >=2.3.1 <18.4.5", + expectedNodeVersion: 16, }, { - Name: "node", - Metadata: map[string]interface{}{ - "version": "v18", - "version-source": "BP_NODE_VERSION", - }, - RunDockerfileProps: ubinodejsextension.RunDockerfileProps{ - Source: "paketocommunity/run-nodejs-18-ubi-base", - }, - BuildDockerfileProps: BuildDockerfileProps, - buildDockerfileExpectedNodejsVersion: 18, + requestedNodeVersion: "v18", + expectedNodeVersion: 18, }, { - Name: "node", - Metadata: map[string]interface{}{ - "version": "16", - "version-source": "BP_NODE_VERSION", - }, - RunDockerfileProps: ubinodejsextension.RunDockerfileProps{ - Source: "paketocommunity/run-nodejs-16-ubi-base", - }, - BuildDockerfileProps: BuildDockerfileProps, - buildDockerfileExpectedNodejsVersion: 16, + requestedNodeVersion: "16", + expectedNodeVersion: 16, }, { - Name: "node", - Metadata: map[string]interface{}{ - "version": "18", - "version-source": "BP_NODE_VERSION", - }, - RunDockerfileProps: ubinodejsextension.RunDockerfileProps{ - Source: "paketocommunity/run-nodejs-18-ubi-base", - }, - BuildDockerfileProps: BuildDockerfileProps, - buildDockerfileExpectedNodejsVersion: 18, + requestedNodeVersion: "18", + expectedNodeVersion: 18, }, { - Name: "node", - Metadata: map[string]interface{}{ - "version": "~16", - "version-source": "BP_NODE_VERSION", - }, - RunDockerfileProps: ubinodejsextension.RunDockerfileProps{ - Source: "paketocommunity/run-nodejs-16-ubi-base", - }, - BuildDockerfileProps: BuildDockerfileProps, - buildDockerfileExpectedNodejsVersion: 16, + requestedNodeVersion: "~16", + expectedNodeVersion: 16, }, { - Name: "node", - Metadata: map[string]interface{}{ - "version": "^18.0.x", - "version-source": "BP_NODE_VERSION", - }, - RunDockerfileProps: ubinodejsextension.RunDockerfileProps{ - Source: "paketocommunity/run-nodejs-18-ubi-base", - }, - BuildDockerfileProps: BuildDockerfileProps, - buildDockerfileExpectedNodejsVersion: 18, + requestedNodeVersion: "^18.0.x", + expectedNodeVersion: 18, }, } for _, tt := range versionTests { - generateResult, err = generate(packit.GenerateContext{ - WorkingDir: workingDir, - CNBPath: cnbDir, - Plan: packit.BuildpackPlan{ - Entries: []packit.BuildpackPlanEntry{ - { - Name: tt.Name, - Metadata: tt.Metadata, + buildplan := packit.BuildpackPlan{ + Entries: []packit.BuildpackPlanEntry{ + { + Name: "node", + Metadata: map[string]interface{}{ + "version": tt.requestedNodeVersion, + "version-source": "BP_NODE_VERSION", }, }, }, - Stack: "io.buildpacks.stacks.ubi8", + } + + generateResult, err = generate(packit.GenerateContext{ + WorkingDir: workingDir, + Plan: buildplan, + Stack: "io.buildpacks.stacks.ubi8", }) Expect(err).NotTo(HaveOccurred()) Expect(generateResult).NotTo(Equal(nil)) - runDockerfileContent, _ := ubinodejsextension.FillPropsToTemplate(tt.RunDockerfileProps, runDockerfileTemplate) - tt.BuildDockerfileProps.NODEJS_VERSION = uint64(tt.buildDockerfileExpectedNodejsVersion) - buildDockerfileContent, _ := ubinodejsextension.FillPropsToTemplate(tt.BuildDockerfileProps, buildDockerfileTemplate) + runDockerFileProps := structs.RunDockerfileProps{ + Source: fmt.Sprintf("paketocommunity/run-nodejs-%d-ubi-base", tt.expectedNodeVersion), + } + runDockerfileContent, _ := utils.GenerateRunDockerfile(runDockerFileProps) + + buildDockerfileProps := structs.BuildDockerfileProps{ + CNB_USER_ID: 1002, + CNB_GROUP_ID: 1000, + CNB_STACK_ID: "io.buildpacks.stacks.ubi8", + PACKAGES: ubinodejsextension.PACKAGES, + NODEJS_VERSION: uint64(tt.expectedNodeVersion), + } + + buildDockerfileContent, _ := utils.GenerateBuildDockerfile(buildDockerfileProps) buf := new(strings.Builder) _, _ = io.Copy(buf, generateResult.RunDockerfile) @@ -448,85 +216,84 @@ func testGenerate(t *testing.T, context spec.G, it spec.S) { buf.Reset() _, _ = io.Copy(buf, generateResult.BuildDockerfile) Expect(buf.String()).To(Equal(buildDockerfileContent)) - } + } }) it("should return the default when node version has NOT been requested", func() { - extensionToml, _ := readExtensionTomlTemplateFile("16") + imagesJsonContent := testhelpers.GenerateImagesJsonFile([]string{"16", "18"}, []bool{true, false}, false) + imagesJsonTmpDir = t.TempDir() + imagesJsonPath = filepath.Join(imagesJsonTmpDir, "images.json") + Expect(os.WriteFile(imagesJsonPath, []byte(imagesJsonContent), 0644)).To(Succeed()) - cnbDir, err = os.MkdirTemp("", "cnb") - Expect(err).NotTo(HaveOccurred()) - Expect(os.WriteFile(cnbDir+"/extension.toml", []byte(extensionToml), 0600)).To(Succeed()) + generate = ubinodejsextension.Generate( + dependencyManager, + logger, + structs.DuringBuildPermissions{CNB_USER_ID: 1002, CNB_GROUP_ID: 1000}, + imagesJsonPath, + ) versionTests := []struct { - Name string - Metadata map[string]interface{} - RunDockerfileProps ubinodejsextension.RunDockerfileProps - BuildDockerfileProps ubinodejsextension.BuildDockerfileProps - buildDockerfileExpectedNodejsVersion int + requestedNodeVersion string + versionSource string + Metadata map[string]interface{} + expectedNodeVersion int }{ { - Name: "node", - Metadata: map[string]interface{}{ - "version": "", - "version-source": "", - }, - RunDockerfileProps: ubinodejsextension.RunDockerfileProps{ - Source: "paketocommunity/run-nodejs-16-ubi-base", - }, - BuildDockerfileProps: BuildDockerfileProps, - buildDockerfileExpectedNodejsVersion: 16, + requestedNodeVersion: "", + versionSource: "", + expectedNodeVersion: 16, }, { - Name: "node", - Metadata: map[string]interface{}{ - "version": "", - "version-source": "BP_NODE_VERSION", - }, - RunDockerfileProps: ubinodejsextension.RunDockerfileProps{ - Source: "paketocommunity/run-nodejs-16-ubi-base", - }, - BuildDockerfileProps: BuildDockerfileProps, - buildDockerfileExpectedNodejsVersion: 16, + requestedNodeVersion: "", + versionSource: "BP_NODE_VERSION", + expectedNodeVersion: 16, }, { - Name: "node", - Metadata: map[string]interface{}{ - "version": "x", - "version-source": "BP_NODE_VERSION", - }, - RunDockerfileProps: ubinodejsextension.RunDockerfileProps{ - Source: "paketocommunity/run-nodejs-18-ubi-base", - }, - BuildDockerfileProps: BuildDockerfileProps, - buildDockerfileExpectedNodejsVersion: 18, + requestedNodeVersion: "x", + versionSource: "BP_NODE_VERSION", + expectedNodeVersion: 18, }, } for _, tt := range versionTests { - generateResult, err = generate(packit.GenerateContext{ - WorkingDir: workingDir, - CNBPath: cnbDir, - Plan: packit.BuildpackPlan{ - Entries: []packit.BuildpackPlanEntry{ - { - Name: tt.Name, - Metadata: tt.Metadata, + buildplan := packit.BuildpackPlan{ + Entries: []packit.BuildpackPlanEntry{ + { + Name: "node", + Metadata: map[string]interface{}{ + "version": tt.requestedNodeVersion, + "version-source": tt.versionSource, }, }, }, - Stack: "io.buildpacks.stacks.ubi8", + } + + generateResult, err = generate(packit.GenerateContext{ + WorkingDir: workingDir, + Plan: buildplan, + Stack: "io.buildpacks.stacks.ubi8", }) Expect(err).NotTo(HaveOccurred()) Expect(generateResult).NotTo(Equal(nil)) - runDockerfileContent, _ := ubinodejsextension.FillPropsToTemplate(tt.RunDockerfileProps, runDockerfileTemplate) - tt.BuildDockerfileProps.NODEJS_VERSION = uint64(tt.buildDockerfileExpectedNodejsVersion) - buildDockerfileContent, _ := ubinodejsextension.FillPropsToTemplate(tt.BuildDockerfileProps, buildDockerfileTemplate) + runDockerFileProps := structs.RunDockerfileProps{ + Source: fmt.Sprintf("paketocommunity/run-nodejs-%d-ubi-base", tt.expectedNodeVersion), + } + + runDockerfileContent, _ := utils.GenerateRunDockerfile(runDockerFileProps) + buildDockerfileProps := structs.BuildDockerfileProps{ + CNB_USER_ID: 1002, + CNB_GROUP_ID: 1000, + CNB_STACK_ID: "io.buildpacks.stacks.ubi8", + PACKAGES: ubinodejsextension.PACKAGES, + NODEJS_VERSION: uint64(tt.expectedNodeVersion), + } + + buildDockerfileContent, _ := utils.GenerateBuildDockerfile(buildDockerfileProps) buf := new(strings.Builder) _, _ = io.Copy(buf, generateResult.RunDockerfile) @@ -540,67 +307,68 @@ func testGenerate(t *testing.T, context spec.G, it spec.S) { it("should return the higher node version when it requests for >=nodeVersion", func() { - extensionToml, _ := readExtensionTomlTemplateFile() + imagesJsonContent := testhelpers.GenerateImagesJsonFile([]string{"16", "18"}, []bool{false, true}, false) + imagesJsonTmpDir = t.TempDir() + imagesJsonPath = filepath.Join(imagesJsonTmpDir, "images.json") + Expect(os.WriteFile(imagesJsonPath, []byte(imagesJsonContent), 0644)).To(Succeed()) - cnbDir, err = os.MkdirTemp("", "cnb") - Expect(err).NotTo(HaveOccurred()) - Expect(os.WriteFile(cnbDir+"/extension.toml", []byte(extensionToml), 0600)).To(Succeed()) + generate = ubinodejsextension.Generate( + dependencyManager, + logger, + structs.DuringBuildPermissions{CNB_USER_ID: 1002, CNB_GROUP_ID: 1000}, + imagesJsonPath, + ) versionTests := []struct { - Name string - Metadata map[string]interface{} - RunDockerfileProps ubinodejsextension.RunDockerfileProps - BuildDockerfileProps ubinodejsextension.BuildDockerfileProps - buildDockerfileExpectedNodejsVersion int + requestedNodeVersion string + expectedNodeVersion int }{ { - Name: "node", - Metadata: map[string]interface{}{ - "version": ">16", - "version-source": "BP_NODE_VERSION", - }, - RunDockerfileProps: ubinodejsextension.RunDockerfileProps{ - Source: "paketocommunity/run-nodejs-18-ubi-base", - }, - BuildDockerfileProps: BuildDockerfileProps, - buildDockerfileExpectedNodejsVersion: 18, + requestedNodeVersion: ">16", + expectedNodeVersion: 18, }, { - Name: "node", - Metadata: map[string]interface{}{ - "version": ">13", - "version-source": "BP_NODE_VERSION", - }, - RunDockerfileProps: ubinodejsextension.RunDockerfileProps{ - Source: "paketocommunity/run-nodejs-18-ubi-base", - }, - BuildDockerfileProps: BuildDockerfileProps, - buildDockerfileExpectedNodejsVersion: 18, + requestedNodeVersion: ">13", + expectedNodeVersion: 18, }, } for _, tt := range versionTests { - - generateResult, err = generate(packit.GenerateContext{ - WorkingDir: workingDir, - CNBPath: cnbDir, - Plan: packit.BuildpackPlan{ - Entries: []packit.BuildpackPlanEntry{ - { - Name: tt.Name, - Metadata: tt.Metadata, + buildplan := packit.BuildpackPlan{ + Entries: []packit.BuildpackPlanEntry{ + { + Name: "node", + Metadata: map[string]interface{}{ + "version": tt.requestedNodeVersion, + "version-source": "BP_NODE_VERSION", }, }, }, - Stack: "io.buildpacks.stacks.ubi8", + } + + generateResult, err = generate(packit.GenerateContext{ + WorkingDir: workingDir, + Plan: buildplan, + Stack: "io.buildpacks.stacks.ubi8", }) Expect(err).NotTo(HaveOccurred()) Expect(generateResult).NotTo(Equal(nil)) - runDockerfileContent, _ := ubinodejsextension.FillPropsToTemplate(tt.RunDockerfileProps, runDockerfileTemplate) - tt.BuildDockerfileProps.NODEJS_VERSION = uint64(tt.buildDockerfileExpectedNodejsVersion) - buildDockerfileContent, _ := ubinodejsextension.FillPropsToTemplate(tt.BuildDockerfileProps, buildDockerfileTemplate) + runDockerFileProps := structs.RunDockerfileProps{ + Source: fmt.Sprintf("paketocommunity/run-nodejs-%d-ubi-base", tt.expectedNodeVersion), + } + runDockerfileContent, _ := utils.GenerateRunDockerfile(runDockerFileProps) + + buildDockerfileProps := structs.BuildDockerfileProps{ + CNB_USER_ID: 1002, + CNB_GROUP_ID: 1000, + CNB_STACK_ID: "io.buildpacks.stacks.ubi8", + PACKAGES: ubinodejsextension.PACKAGES, + NODEJS_VERSION: uint64(tt.expectedNodeVersion), + } + + buildDockerfileContent, _ := utils.GenerateBuildDockerfile(buildDockerfileProps) buf := new(strings.Builder) _, _ = io.Copy(buf, generateResult.RunDockerfile) @@ -609,86 +377,66 @@ func testGenerate(t *testing.T, context spec.G, it spec.S) { _, _ = io.Copy(buf, generateResult.BuildDockerfile) Expect(buf.String()).To(Equal(buildDockerfileContent)) } - }) it("Should error on below cases of requested node", func() { - extensionToml, _ := readExtensionTomlTemplateFile() + imagesJsonContent := testhelpers.GenerateImagesJsonFile([]string{"16", "18"}, []bool{false, true}, false) + imagesJsonTmpDir = t.TempDir() + imagesJsonPath = filepath.Join(imagesJsonTmpDir, "images.json") + Expect(os.WriteFile(imagesJsonPath, []byte(imagesJsonContent), 0644)).To(Succeed()) - cnbDir, err = os.MkdirTemp("", "cnb") - Expect(err).NotTo(HaveOccurred()) - Expect(os.WriteFile(cnbDir+"/extension.toml", []byte(extensionToml), 0600)).To(Succeed()) + generate = ubinodejsextension.Generate( + dependencyManager, + logger, + structs.DuringBuildPermissions{CNB_USER_ID: 1002, CNB_GROUP_ID: 1000}, + imagesJsonPath, + ) versionTests := []struct { - Name string - Metadata map[string]interface{} + requestedNodeVersion string }{ { - Name: "node", - Metadata: map[string]interface{}{ - "version": "17 - 18.0.0", - "version-source": "BP_NODE_VERSION", - }, + requestedNodeVersion: "17 - 18.0.0", }, { - Name: "node", - Metadata: map[string]interface{}{ - "version": "15", - "version-source": "BP_NODE_VERSION", - }, + requestedNodeVersion: "15", }, { - Name: "node", - Metadata: map[string]interface{}{ - "version": "18.0.0", - "version-source": "BP_NODE_VERSION", - }, + requestedNodeVersion: "18.0.0", }, { - Name: "node", - Metadata: map[string]interface{}{ - "version": "v18.999.0", - "version-source": "BP_NODE_VERSION", - }, + requestedNodeVersion: "v18.999.0", }, { - Name: "node", - Metadata: map[string]interface{}{ - "version": ">18", - "version-source": "BP_NODE_VERSION", - }, + requestedNodeVersion: ">18", }, { - Name: "node", - Metadata: map[string]interface{}{ - "version": "~16.2", - "version-source": "BP_NODE_VERSION", - }, + requestedNodeVersion: "~16.2", }, { - Name: "node", - Metadata: map[string]interface{}{ - "version": "16.5.x", - "version-source": "BP_NODE_VERSION", - }, + requestedNodeVersion: "16.5.x", }, } for _, tt := range versionTests { - generateResult, err = generate(packit.GenerateContext{ - WorkingDir: workingDir, - CNBPath: cnbDir, - Plan: packit.BuildpackPlan{ - Entries: []packit.BuildpackPlanEntry{ - { - Name: tt.Name, - Metadata: tt.Metadata, + buildplan := packit.BuildpackPlan{ + Entries: []packit.BuildpackPlanEntry{ + { + Name: "node", + Metadata: map[string]interface{}{ + "version": tt.requestedNodeVersion, + "version-source": "BP_NODE_VERSION", }, }, }, - Stack: "io.buildpacks.stacks.ubi8", + } + + generateResult, err = generate(packit.GenerateContext{ + WorkingDir: workingDir, + Plan: buildplan, + Stack: "io.buildpacks.stacks.ubi8", }) Expect(err).To(HaveOccurred()) @@ -702,9 +450,6 @@ func testGenerate(t *testing.T, context spec.G, it spec.S) { it.Before(func() { workingDir = t.TempDir() - cnbDir, err = os.MkdirTemp("", "cnb") - - generate = ubinodejsextension.Generate(dependencyManager, logger, ubinodejsextension.DuringBuildPermissions{CNB_USER_ID: 1002, CNB_GROUP_ID: 1000}) err = toml.NewEncoder(buf).Encode(testBuildPlan) Expect(err).NotTo(HaveOccurred()) @@ -720,19 +465,26 @@ func testGenerate(t *testing.T, context spec.G, it spec.S) { it.After(func() { Expect(os.RemoveAll(workingDir)).To(Succeed()) + Expect(os.RemoveAll(imagesJsonTmpDir)).To(Succeed()) }) it("Should respect the priorities and return the proper Node.js version", func() { - extensionToml, _ := readExtensionTomlTemplateFile() + imagesJsonContent := testhelpers.GenerateImagesJsonFile([]string{"16", "18"}, []bool{false, true}, false) + imagesJsonTmpDir = t.TempDir() + imagesJsonPath = filepath.Join(imagesJsonTmpDir, "images.json") + Expect(os.WriteFile(imagesJsonPath, []byte(imagesJsonContent), 0644)).To(Succeed()) - cnbDir, err = os.MkdirTemp("", "cnb") - Expect(err).NotTo(HaveOccurred()) - Expect(os.WriteFile(cnbDir+"/extension.toml", []byte(extensionToml), 0600)).To(Succeed()) + generate = ubinodejsextension.Generate( + dependencyManager, + logger, + structs.DuringBuildPermissions{CNB_USER_ID: 1002, CNB_GROUP_ID: 1000}, + imagesJsonPath, + ) entriesTests := []struct { Entries []packit.BuildpackPlanEntry - RunDockerfileProps ubinodejsextension.RunDockerfileProps + RunDockerfileProps structs.RunDockerfileProps }{ { Entries: []packit.BuildpackPlanEntry{ @@ -753,7 +505,7 @@ func testGenerate(t *testing.T, context spec.G, it spec.S) { Metadata: map[string]interface{}{"version": "=16", "version-source": "BP_NODE_VERSION"}, }, }, - RunDockerfileProps: ubinodejsextension.RunDockerfileProps{ + RunDockerfileProps: structs.RunDockerfileProps{ Source: "paketocommunity/run-nodejs-16-ubi-base", }, }, @@ -772,7 +524,7 @@ func testGenerate(t *testing.T, context spec.G, it spec.S) { Metadata: map[string]interface{}{"version": "=16", "version-source": "package.json"}, }, }, - RunDockerfileProps: ubinodejsextension.RunDockerfileProps{ + RunDockerfileProps: structs.RunDockerfileProps{ Source: "paketocommunity/run-nodejs-16-ubi-base", }, }, @@ -783,7 +535,7 @@ func testGenerate(t *testing.T, context spec.G, it spec.S) { Metadata: map[string]interface{}{"version": "=16", "version-source": ".node-version"}, }, }, - RunDockerfileProps: ubinodejsextension.RunDockerfileProps{ + RunDockerfileProps: structs.RunDockerfileProps{ Source: "paketocommunity/run-nodejs-16-ubi-base", }, }, @@ -793,7 +545,6 @@ func testGenerate(t *testing.T, context spec.G, it spec.S) { generateResult, err = generate(packit.GenerateContext{ WorkingDir: workingDir, - CNBPath: cnbDir, Plan: packit.BuildpackPlan{ Entries: tt.Entries, }, @@ -803,7 +554,7 @@ func testGenerate(t *testing.T, context spec.G, it spec.S) { Expect(err).NotTo(HaveOccurred()) Expect(generateResult).NotTo(Equal(nil)) - runDockerfileContent, _ := ubinodejsextension.FillPropsToTemplate(tt.RunDockerfileProps, runDockerfileTemplate) + runDockerfileContent, _ := utils.GenerateRunDockerfile(tt.RunDockerfileProps) buf := new(strings.Builder) _, _ = io.Copy(buf, generateResult.RunDockerfile) @@ -812,48 +563,12 @@ func testGenerate(t *testing.T, context spec.G, it spec.S) { }) - it("Should error in case there are no entries in the buildpack plan.", func() { - - extensionToml, _ := readExtensionTomlTemplateFile() - - cnbDir, err = os.MkdirTemp("", "cnb") - Expect(err).NotTo(HaveOccurred()) - Expect(os.WriteFile(cnbDir+"/extension.toml", []byte(extensionToml), 0600)).To(Succeed()) - - entriesTests := []struct { - Entries []packit.BuildpackPlanEntry - }{ - { - Entries: []packit.BuildpackPlanEntry{}, - }, - } - - for _, tt := range entriesTests { - - generateResult, err = generate(packit.GenerateContext{ - WorkingDir: workingDir, - CNBPath: cnbDir, - Plan: packit.BuildpackPlan{ - Entries: tt.Entries, - }, - Stack: "io.buildpacks.stacks.ubi8", - }) - - Expect(err).To(HaveOccurred()) - - } - - }) }, spec.Sequential()) context("When BP_UBI_RUN_IMAGE_OVERRIDE env has been set", func() { it.Before(func() { - workingDir = t.TempDir() - cnbDir, err = os.MkdirTemp("", "cnb") - - generate = ubinodejsextension.Generate(dependencyManager, logger, ubinodejsextension.DuringBuildPermissions{CNB_USER_ID: 1002, CNB_GROUP_ID: 1000}) err = toml.NewEncoder(buf).Encode(testBuildPlan) Expect(err).NotTo(HaveOccurred()) @@ -873,11 +588,17 @@ func testGenerate(t *testing.T, context spec.G, it spec.S) { it("Should have the same value as the BP_UBI_RUN_IMAGE_OVERRIDE if is not empty string", func() { - extensionToml, _ := readExtensionTomlTemplateFile() + imagesJsonContent := testhelpers.GenerateImagesJsonFile([]string{"16", "18"}, []bool{false, true}, false) + imagesJsonTmpDir = t.TempDir() + imagesJsonPath = filepath.Join(imagesJsonTmpDir, "images.json") + Expect(os.WriteFile(imagesJsonPath, []byte(imagesJsonContent), 0644)).To(Succeed()) - cnbDir, err = os.MkdirTemp("", "cnb") - Expect(err).NotTo(HaveOccurred()) - Expect(os.WriteFile(cnbDir+"/extension.toml", []byte(extensionToml), 0600)).To(Succeed()) + generate = ubinodejsextension.Generate( + dependencyManager, + logger, + structs.DuringBuildPermissions{CNB_USER_ID: 1002, CNB_GROUP_ID: 1000}, + imagesJsonPath, + ) entriesTests := []struct { Entries []packit.BuildpackPlanEntry @@ -899,7 +620,6 @@ func testGenerate(t *testing.T, context spec.G, it spec.S) { generateResult, err = generate(packit.GenerateContext{ WorkingDir: workingDir, - CNBPath: cnbDir, Plan: packit.BuildpackPlan{ Entries: tt.Entries, }, @@ -909,11 +629,11 @@ func testGenerate(t *testing.T, context spec.G, it spec.S) { Expect(err).NotTo(HaveOccurred()) Expect(generateResult).NotTo(Equal(nil)) - RunDockerfileProps := ubinodejsextension.RunDockerfileProps{ + RunDockerfileProps := structs.RunDockerfileProps{ Source: tt.BP_UBI_RUN_IMAGE_OVERRIDE, } - runDockerfileContent, _ := ubinodejsextension.FillPropsToTemplate(RunDockerfileProps, runDockerfileTemplate) + runDockerfileContent, _ := utils.GenerateRunDockerfile(RunDockerfileProps) buf := new(strings.Builder) _, _ = io.Copy(buf, generateResult.RunDockerfile) @@ -923,11 +643,17 @@ func testGenerate(t *testing.T, context spec.G, it spec.S) { it("Should fallback to the run image which corresponds to the selected node version during build", func() { - extensionToml, _ := readExtensionTomlTemplateFile() + imagesJsonContent := testhelpers.GenerateImagesJsonFile([]string{"16", "18"}, []bool{false, true}, false) + imagesJsonTmpDir = t.TempDir() + imagesJsonPath = filepath.Join(imagesJsonTmpDir, "images.json") + Expect(os.WriteFile(imagesJsonPath, []byte(imagesJsonContent), 0644)).To(Succeed()) - cnbDir, err = os.MkdirTemp("", "cnb") - Expect(err).NotTo(HaveOccurred()) - Expect(os.WriteFile(cnbDir+"/extension.toml", []byte(extensionToml), 0600)).To(Succeed()) + generate = ubinodejsextension.Generate( + dependencyManager, + logger, + structs.DuringBuildPermissions{CNB_USER_ID: 1002, CNB_GROUP_ID: 1000}, + imagesJsonPath, + ) entriesTests := []struct { Entries []packit.BuildpackPlanEntry @@ -951,7 +677,6 @@ func testGenerate(t *testing.T, context spec.G, it spec.S) { generateResult, err = generate(packit.GenerateContext{ WorkingDir: workingDir, - CNBPath: cnbDir, Plan: packit.BuildpackPlan{ Entries: tt.Entries, }, @@ -961,11 +686,11 @@ func testGenerate(t *testing.T, context spec.G, it spec.S) { Expect(err).NotTo(HaveOccurred()) Expect(generateResult).NotTo(Equal(nil)) - RunDockerfileProps := ubinodejsextension.RunDockerfileProps{ + RunDockerfileProps := structs.RunDockerfileProps{ Source: fmt.Sprintf("paketocommunity/run-nodejs-%d-ubi-base", tt.selectedNodeVersion), } - runDockerfileContent, _ := ubinodejsextension.FillPropsToTemplate(RunDockerfileProps, runDockerfileTemplate) + runDockerfileContent, _ := utils.GenerateRunDockerfile(RunDockerfileProps) buf := new(strings.Builder) _, _ = io.Copy(buf, generateResult.RunDockerfile) @@ -975,41 +700,3 @@ func testGenerate(t *testing.T, context spec.G, it spec.S) { }, spec.Sequential()) } - -func readExtensionTomlTemplateFile(defaultNodeVersion ...string) (string, error) { - var version string - if len(defaultNodeVersion) == 0 { - version = "18.*.*" - } else { - version = defaultNodeVersion[0] - } - - template := ` -api = "0.7" - -[extension] -id = "redhat-runtimes/nodejs" -name = "RedHat Runtimes Node.js Dependency Extension" -version = "0.0.1" -description = "This extension installs the appropriate nodejs runtime via dnf" - -[metadata] - [metadata.default-versions] - node = "%s" - - [[metadata.dependencies]] - id = "node" - name = "Ubi Node Extension" - stacks = ["io.buildpacks.stacks.ubi8"] - source = "paketocommunity/run-nodejs-18-ubi-base" - version = "18.1000" - - [[metadata.dependencies]] - id = "node" - name = "Ubi Node Extension" - stacks = ["io.buildpacks.stacks.ubi8"] - source = "paketocommunity/run-nodejs-16-ubi-base" - version = "16.1000" - ` - return fmt.Sprintf(template, version), nil -} diff --git a/init_test.go b/init_test.go index fc4cdd2..f843288 100644 --- a/init_test.go +++ b/init_test.go @@ -7,11 +7,9 @@ import ( "github.com/sclevine/spec/report" ) -func TestUnitNode(t *testing.T) { - suite := spec.New("node", spec.Report(report.Terminal{})) +func TestUnitUbiNodejsExtension(t *testing.T) { + suite := spec.New("ubi-nodejs-extension", spec.Report(report.Terminal{})) suite("Detect", testDetect) suite("Generate", testGenerate) - suite("Dockerfile Creation", testFillPropsToTemplate) - suite("Fetching during build permissions", testFetchingPermissionsFromEtchPasswdFile) suite.Run(t) } diff --git a/integration/fetch_run_image_from_env.go b/integration/fetch_run_image_from_env.go index fea1258..c874be4 100644 --- a/integration/fetch_run_image_from_env.go +++ b/integration/fetch_run_image_from_env.go @@ -2,13 +2,10 @@ package integration import ( "fmt" - "io" - "net/http" "os" "path/filepath" "testing" - "github.com/Masterminds/semver" "github.com/paketo-buildpacks/occam" "github.com/sclevine/spec" @@ -18,20 +15,13 @@ import ( func testFetchRunImageFromEnv(t *testing.T, context spec.G, it spec.S) { var ( - Expect = NewWithT(t).Expect - Eventually = NewWithT(t).Eventually - - docker occam.Docker + Expect = NewWithT(t).Expect pack occam.Pack - - image occam.Image - container occam.Container - name string - source string + name string + source string ) it.Before(func() { - docker = occam.NewDocker() pack = occam.NewPack() var err error @@ -40,9 +30,6 @@ func testFetchRunImageFromEnv(t *testing.T, context spec.G, it spec.S) { }) it.After(func() { - Expect(docker.Container.Remove.Execute(container.ID)).To(Succeed()) - Expect(docker.Volume.Remove.Execute(occam.CacheVolumeNames(name))).To(Succeed()) - Expect(docker.Image.Remove.Execute(image.ID)).To(Succeed()) Expect(os.RemoveAll(source)).To(Succeed()) }) @@ -52,18 +39,10 @@ func testFetchRunImageFromEnv(t *testing.T, context spec.G, it spec.S) { source, err = occam.Source(filepath.Join("testdata", "simple_app")) Expect(err).NotTo(HaveOccurred()) - nodeRunImage := settings.Metadata.Dependencies[0].Source - - runNodeVersion, err := semver.NewVersion(settings.Metadata.Dependencies[0].Version) - Expect(err).NotTo(HaveOccurred()) - runNodeMajorVersion := runNodeVersion.Major() - - buildNodeVersion, err := semver.NewVersion(settings.Metadata.Dependencies[len(settings.Metadata.Dependencies)-1].Version) - Expect(err).NotTo(HaveOccurred()) - buildNodeMajorVersion := buildNodeVersion.Major() + nodeRunImage := "this-is-a-run-image" var logs fmt.Stringer - image, logs, err = pack.WithNoColor().Build. + _, logs, err = pack.WithNoColor().Build. WithExtensions( settings.Buildpacks.NodeExtension.Online, ). @@ -71,38 +50,18 @@ func testFetchRunImageFromEnv(t *testing.T, context spec.G, it spec.S) { settings.Buildpacks.NodeEngine.Online, settings.Buildpacks.Processes.Online, ). - WithEnv(map[string]string{"BP_UBI_RUN_IMAGE_OVERRIDE": nodeRunImage, "BP_NODE_VERSION": fmt.Sprint(buildNodeMajorVersion)}). + WithEnv(map[string]string{"BP_UBI_RUN_IMAGE_OVERRIDE": nodeRunImage}). WithPullPolicy("always"). Execute(name, source) - Expect(err).NotTo(HaveOccurred(), logs.String()) + Expect(err).To(HaveOccurred()) Expect(logs).To(ContainLines(fmt.Sprintf("%s 1.2.3", settings.Extension.Name))) Expect(logs).To(ContainLines(" Resolving Node Engine version")) Expect(logs).To(ContainLines(" Candidate version sources (in priority order):")) - Expect(logs).To(ContainLines(fmt.Sprintf(" BP_NODE_VERSION -> \"%d\"", buildNodeMajorVersion))) - Expect(logs).To(ContainLines(" -> \"\"")) + Expect(logs).To(ContainLines(" -> \"\"")) Expect(logs).To(ContainLines(fmt.Sprintf(" Using run image specified by BP_UBI_RUN_IMAGE_OVERRIDE %s", nodeRunImage))) - - Expect(logs).To(ContainLines( - "[extender (build)] Enabling module streams:", - fmt.Sprintf("[extender (build)] nodejs:%d", buildNodeMajorVersion))) - - container, err = docker.Container.Run. - WithPublish("8080"). - Execute(image.ID) - Expect(err).NotTo(HaveOccurred()) - - Eventually(container).Should(BeAvailable()) - - response, err := http.Get(fmt.Sprintf("http://localhost:%s/version/major", container.HostPort("8080"))) - Expect(err).NotTo(HaveOccurred()) - Expect(response.StatusCode).To(Equal(http.StatusOK)) - - content, err := io.ReadAll(response.Body) - Expect(err).NotTo(HaveOccurred()) - Expect(string(content)).To(ContainSubstring(fmt.Sprint(runNodeMajorVersion))) - + Expect(logs).To(ContainLines(MatchRegexp(` Selected Node Engine Major version \d+`))) }) }) } diff --git a/integration/init_test.go b/integration/init_test.go index 3172487..4285230 100644 --- a/integration/init_test.go +++ b/integration/init_test.go @@ -4,7 +4,6 @@ import ( "encoding/json" "os" "path/filepath" - "sort" "testing" "time" @@ -40,16 +39,6 @@ var settings struct { } Metadata struct { - DefaultVersions struct { - Node string `toml:"node"` - } `toml:"default-versions"` - Dependencies []struct { - ID string `toml:"id"` - Name string `toml:"name"` - Stacks []string `toml:"stacks"` - Source string `toml:"source"` - Version string `toml:"version"` - } `toml:"dependencies"` } `toml:"metadata"` Config struct { @@ -69,11 +58,6 @@ func TestIntegration(t *testing.T) { Expect(err).NotTo(HaveOccurred()) Expect(file.Close()).To(Succeed()) - // order by descending version - sort.Slice(settings.Metadata.Dependencies, func(i, j int) bool { - return settings.Metadata.Dependencies[i].Version > settings.Metadata.Dependencies[j].Version - }) - //reading the integration.json file file, err = os.Open("../integration.json") Expect(err).NotTo(HaveOccurred()) diff --git a/integration/provides_test.go b/integration/provides_test.go index c32db32..e759e11 100644 --- a/integration/provides_test.go +++ b/integration/provides_test.go @@ -4,7 +4,6 @@ import ( "fmt" "os" "path/filepath" - "strings" "testing" "github.com/paketo-buildpacks/occam" @@ -70,12 +69,12 @@ func testProvides(t *testing.T, context spec.G, it spec.S) { " Candidate version sources (in priority order):", " -> \"\"")) - Expect(logs).To(ContainLines(fmt.Sprintf(" Selected Node Engine Major version %s", strings.Split(settings.Metadata.DefaultVersions.Node, ".")[0]))) + Expect(logs).To(ContainLines(MatchRegexp(` Selected Node Engine Major version \d+`))) Expect(logs).To(ContainLines("===> RESTORING")) Expect(logs).To(ContainLines("===> EXTENDING (BUILD)")) Expect(logs).To(ContainLines( "[extender (build)] Enabling module streams:", - fmt.Sprintf("[extender (build)] nodejs:%s", strings.Split(settings.Metadata.DefaultVersions.Node, ".")[0]))) + MatchRegexp(`\[extender \(build\)\] nodejs:\d+`))) // SBOM is not supported at the moment from UBI image // therefore there are no available logs to test/validate diff --git a/integration/simple_app_test.go b/integration/simple_app_test.go index 6b54ecc..e5b8a0b 100644 --- a/integration/simple_app_test.go +++ b/integration/simple_app_test.go @@ -6,7 +6,6 @@ import ( "net/http" "os" "path/filepath" - "strings" "testing" "github.com/paketo-buildpacks/occam" @@ -87,13 +86,13 @@ func testSimple(t *testing.T, context spec.G, it spec.S) { " Candidate version sources (in priority order):", " -> \"\"", )) - Expect(logs).To(ContainLines( - fmt.Sprintf(" Selected Node Engine Major version %s", strings.Split(settings.Metadata.DefaultVersions.Node, ".")[0]))) + + Expect(logs).To(ContainLines(MatchRegexp(` Selected Node Engine Major version \d+`))) Expect(logs).To(ContainLines("===> RESTORING")) Expect(logs).To(ContainLines("===> EXTENDING (BUILD)")) Expect(logs).To(ContainLines( "[extender (build)] Enabling module streams:", - fmt.Sprintf("[extender (build)] nodejs:%s", strings.Split(settings.Metadata.DefaultVersions.Node, ".")[0]))) + MatchRegexp(`\[extender \(build\)\] nodejs:\d+`))) // SBOM is not supported at the moment from UBI image // therefore there are no available logs to test/validate diff --git a/internal/testhelpers/testhelpers.go b/internal/testhelpers/testhelpers.go new file mode 100644 index 0000000..0d3d5db --- /dev/null +++ b/internal/testhelpers/testhelpers.go @@ -0,0 +1,74 @@ +package testhelpers + +import "fmt" + +func GenerateImagesJsonFile(nodeVersions []string, isDefault []bool, isCorrupted bool) string { + addStacks := "" + + for i, nodeVersion := range nodeVersions { + + addStacks += fmt.Sprintf(`, + { + "name": "nodejs-%s", + "is_default_run_image": %t, + "config_dir": "stack-nodejs-%s", + "output_dir": "build-nodejs-%s", + "build_image": "build-nodejs-%s", + "run_image": "run-nodejs-%s", + "build_receipt_filename": "build-nodejs-%s-receipt.cyclonedx.json", + "run_receipt_filename": "run-nodejs-%s-receipt.cyclonedx.json", + "base_run_container_image": "docker://registry.access.redhat.com/ubi8/nodejs-%s-runtime" + }`, nodeVersion, isDefault[i], nodeVersion, nodeVersion, nodeVersion, nodeVersion, nodeVersion, nodeVersion, nodeVersion) + } + + if isCorrupted { + addStacks += `, + { + "name": "nodejs-18",} + not a valid json + }` + } + + stacks := fmt.Sprintf(`{ + "support_usns": false, + "update_on_new_image": true, + "receipts_show_limit": 16, + "images": [ + { + "name": "default", + "config_dir": "stack", + "output_dir": "build", + "build_image": "build", + "run_image": "run", + "build_receipt_filename": "build-receipt.cyclonedx.json", + "run_receipt_filename": "run-receipt.cyclonedx.json", + "create_build_image": true, + "base_build_container_image": "docker://registry.access.redhat.com/ubi8/ubi-minimal", + "base_run_container_image": "docker://registry.access.redhat.com/ubi8/ubi-minimal" + }, + { + "name": "java-17", + "config_dir": "stack-java-17", + "output_dir": "build-java-17", + "build_image": "build-java-17", + "run_image": "run-java-17", + "build_receipt_filename": "build-java-17-receipt.cyclonedx.json", + "run_receipt_filename": "run-java-17-receipt.cyclonedx.json", + "base_run_container_image": "docker://registry.access.redhat.com/ubi8/openjdk-17-runtime" + }, + { + "name": "java-21", + "config_dir": "stack-java-21", + "output_dir": "build-java-21", + "build_image": "build-java-21", + "run_image": "run-java-21", + "build_receipt_filename": "build-java-21-receipt.cyclonedx.json", + "run_receipt_filename": "run-java-21-receipt.cyclonedx.json", + "base_run_container_image": "docker://registry.access.redhat.com/ubi8/openjdk-21-runtime" + }%s + ] + } +`, addStacks) + + return stacks +} diff --git a/internal/utils/init_test.go b/internal/utils/init_test.go new file mode 100644 index 0000000..da4aada --- /dev/null +++ b/internal/utils/init_test.go @@ -0,0 +1,21 @@ +package utils_test + +import ( + "testing" + + "github.com/sclevine/spec" + "github.com/sclevine/spec/report" +) + +func TestUnitUtils(t *testing.T) { + suite := spec.New("utils-ubi-nodejs-extension", spec.Report(report.Terminal{})) + suite("GenerateConfigTomlContentFromImagesJson", testGenerateConfigTomlContentFromImagesJson) + suite("GetDefaultNodeVersion", testGetDefaultNodeVersion) + suite("CreateConfigTomlFileContent", testCreateConfigTomlFileContent) + suite("ParseImagesJsonFile", testParseImagesJsonFile) + suite("GetNodejsStackImages", testGetNodejsStackImages) + suite("GetDuringBuildPermissions", testGetDuringBuildPermissions) + suite("testGenerateBuildDockerfile", testGenerateBuildDockerfile) + suite("testGenerateRunDockerfile", testGenerateRunDockerfile) + suite.Run(t) +} diff --git a/templates/build.Dockerfile b/internal/utils/templates/build.Dockerfile similarity index 100% rename from templates/build.Dockerfile rename to internal/utils/templates/build.Dockerfile diff --git a/templates/run.Dockerfile b/internal/utils/templates/run.Dockerfile similarity index 100% rename from templates/run.Dockerfile rename to internal/utils/templates/run.Dockerfile diff --git a/internal/utils/utils.go b/internal/utils/utils.go new file mode 100644 index 0000000..7b12359 --- /dev/null +++ b/internal/utils/utils.go @@ -0,0 +1,230 @@ +package utils + +import ( + _ "embed" + + "bytes" + "encoding/json" + "errors" + "fmt" + "os" + "regexp" + "strconv" + "strings" + "text/template" + + "github.com/paketo-community/ubi-nodejs-extension/constants" + "github.com/paketo-community/ubi-nodejs-extension/structs" + + "github.com/BurntSushi/toml" +) + +//go:embed templates/build.Dockerfile +var buildDockerfileTemplate string + +//go:embed templates/run.Dockerfile +var runDockerfileTemplate string + +type StackImages struct { + Name string `json:"name"` + IsDefaultRunImage bool `json:"is_default_run_image,omitempty"` + NodeVersion string +} + +type ImagesJson struct { + StackImages []StackImages `json:"images"` +} + +func GenerateConfigTomlContentFromImagesJson(imagesJsonPath string, stackId string) ([]byte, error) { + imagesJsonData, err := ParseImagesJsonFile(imagesJsonPath) + if err != nil { + return []byte{}, err + } + + nodejsStacks, err := GetNodejsStackImages(imagesJsonData) + if err != nil { + return []byte{}, err + } + + defaultNodeVersion, err := GetDefaultNodeVersion(nodejsStacks) + if err != nil { + return []byte{}, err + } + + configTomlContent, err := CreateConfigTomlFileContent(defaultNodeVersion, nodejsStacks, stackId) + if err != nil { + return []byte{}, err + } + + configTomlContentString := configTomlContent.Bytes() + return configTomlContentString, nil +} + +func GetDefaultNodeVersion(stacks []StackImages) (string, error) { + var defaultNodeVersionsFound []string + for _, stack := range stacks { + if stack.IsDefaultRunImage { + defaultNodeVersionsFound = append(defaultNodeVersionsFound, strings.Split(stack.Name, "-")[1]) + } + } + if len(defaultNodeVersionsFound) == 1 { + return defaultNodeVersionsFound[0], nil + } else if len(defaultNodeVersionsFound) > 1 { + return "", errors.New("multiple default node.js versions found") + } else { + return "", errors.New("default node.js version not found") + } +} + +func CreateConfigTomlFileContent(defaultNodeVersion string, nodejsStacks []StackImages, stackId string) (bytes.Buffer, error) { + + var dependencies []map[string]interface{} + + for _, stack := range nodejsStacks { + dependency := map[string]interface{}{ + "id": "node", + "stacks": []string{stackId}, + "version": fmt.Sprintf("%s.1000", stack.NodeVersion), + "source": fmt.Sprintf("paketocommunity/run-nodejs-%s-ubi-base", stack.NodeVersion), + } + dependencies = append(dependencies, dependency) + } + + config := map[string]interface{}{ + "metadata": map[string]interface{}{ + "default-versions": map[string]string{ + "node": fmt.Sprintf("%s.*.*", defaultNodeVersion), + }, + "dependencies": dependencies, + }, + } + + buf := new(bytes.Buffer) + if err := toml.NewEncoder(buf).Encode(config); err != nil { + return bytes.Buffer{}, err + } + + return *buf, nil +} + +func GetNodejsStackImages(imagesJsonData ImagesJson) ([]StackImages, error) { + + // Filter out the nodejs stacks based on the stack name + nodejsRegex, _ := regexp.Compile("^nodejs") + + nodejsStacks := []StackImages{} + for _, stack := range imagesJsonData.StackImages { + + if nodejsRegex.MatchString(stack.Name) { + //Extract the node version from the stack name + extractedNodeVersion := strings.Split(stack.Name, "-")[1] + + _, err := strconv.Atoi(extractedNodeVersion) + if err != nil { + return []StackImages{}, fmt.Errorf("extracted Node.js version [%s] for stack %s is not an integer", extractedNodeVersion, stack.Name) + } + + stack.NodeVersion = extractedNodeVersion + + nodejsStacks = append(nodejsStacks, stack) + } + } + if len(nodejsStacks) == 0 { + return []StackImages{}, errors.New("no nodejs stacks found") + } + + return nodejsStacks, nil +} + +func ParseImagesJsonFile(imagesJsonPath string) (ImagesJson, error) { + filepath, err := os.Open(imagesJsonPath) + if err != nil { + return ImagesJson{}, err + } + + defer filepath.Close() + + var imagesJsonData ImagesJson + err = json.NewDecoder(filepath).Decode(&imagesJsonData) + if err != nil { + return ImagesJson{}, err + } + + return imagesJsonData, nil +} + +func GetDuringBuildPermissions(filepath string) structs.DuringBuildPermissions { + + defaultPermissions := structs.DuringBuildPermissions{ + CNB_USER_ID: constants.DEFAULT_USER_ID, + CNB_GROUP_ID: constants.DEFAULT_GROUP_ID, + } + re := regexp.MustCompile(`cnb:x:(\d+):(\d+)::`) + + etcPasswdFile, err := os.ReadFile(filepath) + + if err != nil { + return defaultPermissions + } + etcPasswdContent := string(etcPasswdFile) + + matches := re.FindStringSubmatch(etcPasswdContent) + + if len(matches) != 3 { + return defaultPermissions + } + + CNB_USER_ID, err := strconv.Atoi(matches[1]) + + if err != nil { + return defaultPermissions + } + + CNB_GROUP_ID, err := strconv.Atoi(matches[2]) + + if err != nil { + return defaultPermissions + } + + return structs.DuringBuildPermissions{ + CNB_USER_ID: CNB_USER_ID, + CNB_GROUP_ID: CNB_GROUP_ID, + } +} + +func GenerateBuildDockerfile(buildProps structs.BuildDockerfileProps) (result string, Error error) { + + result, err := fillPropsToTemplate(buildProps, buildDockerfileTemplate) + + if err != nil { + return "", err + } + + return result, nil +} + +func GenerateRunDockerfile(runProps structs.RunDockerfileProps) (result string, Error error) { + + result, err := fillPropsToTemplate(runProps, runDockerfileTemplate) + + if err != nil { + return "", err + } + return result, nil +} + +func fillPropsToTemplate(properties interface{}, templateString string) (result string, Error error) { + + templ, err := template.New("template").Parse(templateString) + if err != nil { + return "", err + } + + var buf bytes.Buffer + err = templ.Execute(&buf, properties) + if err != nil { + panic(err) + } + + return buf.String(), nil +} diff --git a/internal/utils/utils_test.go b/internal/utils/utils_test.go new file mode 100644 index 0000000..bb4f262 --- /dev/null +++ b/internal/utils/utils_test.go @@ -0,0 +1,541 @@ +package utils_test + +import ( + _ "embed" + "fmt" + "os" + "path/filepath" + "testing" + + . "github.com/onsi/gomega" + ubinodejsextension "github.com/paketo-community/ubi-nodejs-extension" + "github.com/paketo-community/ubi-nodejs-extension/constants" + testhelpers "github.com/paketo-community/ubi-nodejs-extension/internal/testhelpers" + "github.com/paketo-community/ubi-nodejs-extension/internal/utils" + "github.com/paketo-community/ubi-nodejs-extension/structs" + "github.com/sclevine/spec" +) + +func testGenerateConfigTomlContentFromImagesJson(t *testing.T, context spec.G, it spec.S) { + + var ( + Expect = NewWithT(t).Expect + ) + + var imagesJsonDir string + it.Before(func() { + imagesJsonDir = t.TempDir() + }) + + it.After(func() { + Expect(os.RemoveAll(imagesJsonDir)).To(Succeed()) + }) + context("When GenerateConfigTomlContentFromImagesJson is being called with a valid images.json file ", func() { + + it("successfully parses images.json file and returns the config.toml content", func() { + + imagesJsonContent := testhelpers.GenerateImagesJsonFile([]string{"16", "18", "20"}, []bool{false, false, true}, false) + imagesJsonTmpDir := t.TempDir() + imagesJsonPath := filepath.Join(imagesJsonTmpDir, "images.json") + Expect(os.WriteFile(imagesJsonPath, []byte(imagesJsonContent), 0644)).To(Succeed()) + + configTomlContent, err := utils.GenerateConfigTomlContentFromImagesJson(imagesJsonPath, "io.buildpacks.stacks.ubix") + + Expect(err).ToNot(HaveOccurred()) + Expect(string(configTomlContent)).To(ContainSubstring(`[metadata] + [metadata.default-versions] + node = "20.*.*" + + [[metadata.dependencies]] + id = "node" + source = "paketocommunity/run-nodejs-16-ubi-base" + stacks = ["io.buildpacks.stacks.ubix"] + version = "16.1000" + + [[metadata.dependencies]] + id = "node" + source = "paketocommunity/run-nodejs-18-ubi-base" + stacks = ["io.buildpacks.stacks.ubix"] + version = "18.1000" + + [[metadata.dependencies]] + id = "node" + source = "paketocommunity/run-nodejs-20-ubi-base" + stacks = ["io.buildpacks.stacks.ubix"] + version = "20.1000"`)) + }) + }) + + context("When GenerateConfigTomlContentFromImagesJson is being called with an invalide images.json file ", func() { + + it("It should throw an error with a message", func() { + + _, err := utils.GenerateConfigTomlContentFromImagesJson("/path/to/invalid/images.json", "io.buildpacks.stacks.ubix") + + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("no such file or directory")) + }) + }) + +} +func testGetDefaultNodeVersion(t *testing.T, context spec.G, it spec.S) { + + var ( + Expect = NewWithT(t).Expect + ) + + context("When passing an array of stacks with nodejs images", func() { + + context("and there is a default run image", func() { + it("should find the default node version", func() { + defaultNodeVersion, err := utils.GetDefaultNodeVersion([]utils.StackImages{ + { + Name: "nodejs-22", + IsDefaultRunImage: true, + NodeVersion: "22", + }, + { + Name: "nodejs-20", + IsDefaultRunImage: false, + NodeVersion: "20", + }, + }) + + Expect(err).ToNot(HaveOccurred()) + Expect(defaultNodeVersion).To(Equal("22")) + }) + }) + + context("and there are no default run images", func() { + it("should error with a message", func() { + defaultNodeVersion, err := utils.GetDefaultNodeVersion([]utils.StackImages{ + { + Name: "nodejs-22", + IsDefaultRunImage: false, + NodeVersion: "22", + }, + { + Name: "nodejs-20", + IsDefaultRunImage: false, + NodeVersion: "20", + }, + }) + + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(Equal("default node.js version not found")) + Expect(defaultNodeVersion).To(Equal("")) + }) + }) + + context("and there are more than one default run images", func() { + it("should error", func() { + defaultNodeVersion, err := utils.GetDefaultNodeVersion([]utils.StackImages{ + { + Name: "nodejs-18", + IsDefaultRunImage: true, + NodeVersion: "18", + }, + { + Name: "nodejs-22", + IsDefaultRunImage: false, + NodeVersion: "22", + }, + { + Name: "nodejs-20", + IsDefaultRunImage: true, + NodeVersion: "20", + }, + }) + + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(Equal("multiple default node.js versions found")) + Expect(defaultNodeVersion).To(Equal("")) + }) + }) + }) +} + +func testCreateConfigTomlFileContent(t *testing.T, context spec.G, it spec.S) { + + var ( + Expect = NewWithT(t).Expect + ) + + context("When passing data properly to CreateConfigTomlFileContent function", func() { + + it("successfly create the content of config.toml", func() { + configTomlFileContent, err := utils.CreateConfigTomlFileContent("22", []utils.StackImages{ + { + Name: "nodejs-22", + IsDefaultRunImage: true, + NodeVersion: "22", + }, + { + Name: "nodejs-20", + IsDefaultRunImage: false, + NodeVersion: "20", + }, + }, "io.buildpacks.stacks.ubix") + + Expect(err).ToNot(HaveOccurred()) + Expect(configTomlFileContent.String()).To(ContainSubstring(`[metadata] + [metadata.default-versions] + node = "22.*.*" + + [[metadata.dependencies]] + id = "node" + source = "paketocommunity/run-nodejs-22-ubi-base" + stacks = ["io.buildpacks.stacks.ubix"] + version = "22.1000" + + [[metadata.dependencies]] + id = "node" + source = "paketocommunity/run-nodejs-20-ubi-base" + stacks = ["io.buildpacks.stacks.ubix"] + version = "20.1000"`)) + }) + }) +} + +func testParseImagesJsonFile(t *testing.T, _ spec.G, it spec.S) { + + var ( + Expect = NewWithT(t).Expect + ) + + var imagesJsonDir string + it.Before(func() { + imagesJsonDir = t.TempDir() + }) + + it.After(func() { + Expect(os.RemoveAll(imagesJsonDir)).To(Succeed()) + }) + + it("successfully parses images.json file", func() { + + imagesJsonContent := testhelpers.GenerateImagesJsonFile([]string{"16", "18", "20"}, []bool{false, false, true}, false) + imagesJsonTmpDir := t.TempDir() + imagesJsonPath := filepath.Join(imagesJsonTmpDir, "images.json") + Expect(os.WriteFile(imagesJsonPath, []byte(imagesJsonContent), 0644)).To(Succeed()) + + imagesJsonData, err := utils.ParseImagesJsonFile(imagesJsonPath) + Expect(err).ToNot(HaveOccurred()) + + Expect(imagesJsonData).To(Equal(utils.ImagesJson{ + StackImages: []utils.StackImages{ + { + Name: "default", + IsDefaultRunImage: false, + }, + { + Name: "java-17", + IsDefaultRunImage: false, + }, + { + Name: "java-21", + IsDefaultRunImage: false, + }, + { + Name: "nodejs-16", + IsDefaultRunImage: false, + }, + { + Name: "nodejs-18", + IsDefaultRunImage: false, + }, + { + Name: "nodejs-20", + IsDefaultRunImage: true, + }, + }, + })) + }) + + it("erros when images.json file does not exist", func() { + imagesJsonData, err := utils.ParseImagesJsonFile("/does/not/exist") + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("no such file or directory")) + Expect(imagesJsonData).To(Equal(utils.ImagesJson{})) + }) + + it("erros when images.json file is not a valid json", func() { + + imagesJsonContent := testhelpers.GenerateImagesJsonFile([]string{"16", "18", "20"}, []bool{false, false, true}, true) + imagesJsonTmpDir := t.TempDir() + imagesJsonPath := filepath.Join(imagesJsonTmpDir, "images_not_valid.json") + Expect(os.WriteFile(imagesJsonPath, []byte(imagesJsonContent), 0644)).To(Succeed()) + + imagesJsonData, err := utils.ParseImagesJsonFile(imagesJsonPath) + + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("invalid character")) + Expect(imagesJsonData).To(Equal(utils.ImagesJson{})) + }) +} + +func testGetNodejsStackImages(t *testing.T, context spec.G, it spec.S) { + + var ( + Expect = NewWithT(t).Expect + ) + + context("When passing the array with all the stacks", func() { + + it("should return only the nodejs stacks", func() { + nodejsStacks, err := utils.GetNodejsStackImages(utils.ImagesJson{ + StackImages: []utils.StackImages{ + { + Name: "default", + IsDefaultRunImage: false, + }, + { + Name: "java-17", + IsDefaultRunImage: false, + }, + { + Name: "java-21", + IsDefaultRunImage: false, + }, + { + Name: "nodejs-16", + IsDefaultRunImage: false, + }, + { + Name: "nodejs-18", + IsDefaultRunImage: false, + }, + { + Name: "nodejs-20", + IsDefaultRunImage: true, + }, + }, + }) + Expect(err).ToNot(HaveOccurred()) + + Expect(nodejsStacks).To(Equal([]utils.StackImages{ + { + Name: "nodejs-16", + IsDefaultRunImage: false, + NodeVersion: "16", + }, + { + Name: "nodejs-18", + IsDefaultRunImage: false, + NodeVersion: "18", + }, + { + Name: "nodejs-20", + IsDefaultRunImage: true, + NodeVersion: "20", + }, + })) + }) + }) + + context("When passing a stack images array without any nodejs stacks in it", func() { + + it("should return an error with an appropriate message", func() { + nodejsStacks, err := utils.GetNodejsStackImages(utils.ImagesJson{ + StackImages: []utils.StackImages{ + { + Name: "default", + IsDefaultRunImage: false, + }, + { + Name: "java-17", + IsDefaultRunImage: false, + }, + { + Name: "java-21", + IsDefaultRunImage: false, + }, + }, + }) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(Equal("no nodejs stacks found")) + Expect(nodejsStacks).To(Equal([]utils.StackImages{})) + }) + }) + + context("When node version is malformed or does not exist", func() { + + it("should error with a message", func() { + + imagesJsonTmpDir := t.TempDir() + imagesJsonNodeVersionNotIntegerContent := testhelpers.GenerateImagesJsonFile([]string{"16", "18", "hello"}, []bool{false, false, true}, false) + imagesJsonNodeVersionNotIntegerPath := filepath.Join(imagesJsonTmpDir, "images_node_version_not_integer.json") + Expect(os.WriteFile(imagesJsonNodeVersionNotIntegerPath, []byte(imagesJsonNodeVersionNotIntegerContent), 0600)).To(Succeed()) + + imagesJsonNoNodeVersionContent := testhelpers.GenerateImagesJsonFile([]string{"16", "", "20"}, []bool{false, false, true}, false) + imagesJsonNoNodeVersionPath := filepath.Join(imagesJsonTmpDir, "images_no_node_version.json") + Expect(os.WriteFile(imagesJsonNoNodeVersionPath, []byte(imagesJsonNoNodeVersionContent), 0600)).To(Succeed()) + + for _, tt := range []struct { + errorMessage string + imagesJsonPath string + }{ + { + errorMessage: "extracted Node.js version [hello] for stack nodejs-hello is not an integer", + imagesJsonPath: imagesJsonNodeVersionNotIntegerPath, + }, + { + errorMessage: "extracted Node.js version [] for stack nodejs- is not an integer", + imagesJsonPath: imagesJsonNoNodeVersionPath, + }, + } { + imagesJsonData, err := utils.ParseImagesJsonFile(filepath.Join(tt.imagesJsonPath)) + Expect(err).ToNot(HaveOccurred()) + + nodejsStacks, err := utils.GetNodejsStackImages(imagesJsonData) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring(tt.errorMessage)) + Expect(nodejsStacks).To(Equal([]utils.StackImages{})) + } + }) + }) +} + +func testGenerateBuildDockerfile(t *testing.T, context spec.G, it spec.S) { + + var ( + Expect = NewWithT(t).Expect + ) + + context("Adding props on build.dockerfile template", func() { + + it("Should fill with properties the template/build.Dockerfile", func() { + + output, err := utils.GenerateBuildDockerfile(structs.BuildDockerfileProps{ + NODEJS_VERSION: 16, + CNB_USER_ID: 1000, + CNB_GROUP_ID: 1000, + CNB_STACK_ID: "io.buildpacks.stacks.ubi8", + PACKAGES: ubinodejsextension.PACKAGES, + }) + + Expect(err).NotTo(HaveOccurred()) + Expect(output).To(Equal(fmt.Sprintf(`ARG base_image +FROM ${base_image} + +USER root + +ARG build_id=0 +RUN echo ${build_id} + +RUN microdnf -y module enable nodejs:16 +RUN microdnf --setopt=install_weak_deps=0 --setopt=tsflags=nodocs install -y %s && microdnf clean all + +RUN echo uid:gid "1000:1000" +USER 1000:1000 + +RUN echo "CNB_STACK_ID: io.buildpacks.stacks.ubi8"`, ubinodejsextension.PACKAGES))) + + }) + + }) +} + +func testGenerateRunDockerfile(t *testing.T, context spec.G, it spec.S) { + + var ( + Expect = NewWithT(t).Expect + ) + + context("Adding props on build.dockerfile template", func() { + + it("Should fill with properties the template/run.Dockerfile", func() { + + RunDockerfileProps := structs.RunDockerfileProps{ + Source: "paketocommunity/run-nodejs-18-ubi-base", + } + + output, err := utils.GenerateRunDockerfile(RunDockerfileProps) + + Expect(err).NotTo(HaveOccurred()) + Expect(output).To(Equal(`FROM paketocommunity/run-nodejs-18-ubi-base`)) + + }) + }) +} + +func testGetDuringBuildPermissions(t *testing.T, context spec.G, it spec.S) { + + var Expect = NewWithT(t).Expect + + context("/etc/passwd exists and has the cnb user", func() { + + it("It should return the permissions specified for the cnb user", func() { + tmpDir := t.TempDir() + path := filepath.Join(tmpDir, "/passwd") + Expect(os.WriteFile(path, []byte(`root:x:0:0:root:/root:/bin/bash +bin:x:1:1:bin:/bin:/sbin/nologin +daemon:x:2:2:daemon:/sbin:/sbin/nologin +adm:x:3:4:adm:/var/adm:/sbin/nologin +lp:x:4:7:lp:/var/spool/lpd:/sbin/nologin +sync:x:5:0:sync:/sbin:/bin/sync +shutdown:x:6:0:shutdown:/sbin:/sbin/shutdown +halt:x:7:0:halt:/sbin:/sbin/halt +mail:x:8:12:mail:/var/spool/mail:/sbin/nologin +operator:x:11:0:operator:/root:/sbin/nologin +games:x:12:100:games:/usr/games:/sbin/nologin +ftp:x:14:50:FTP User:/var/ftp:/sbin/nologin +cnb:x:1234:2345::/home/cnb:/bin/bash +nobody:x:65534:65534:Kernel Overflow User:/:/sbin/nologin +`), 0600)).To(Succeed()) + + duringBuilderPermissions := utils.GetDuringBuildPermissions(path) + + Expect(duringBuilderPermissions).To(Equal( + structs.DuringBuildPermissions{ + CNB_USER_ID: 1234, + CNB_GROUP_ID: 2345, + }, + )) + }) + }) + + context("/etc/passwd exists and does NOT have the cnb user", func() { + + it("It should return the default permissions", func() { + tmpDir := t.TempDir() + path := filepath.Join(tmpDir, "/passwd") + + Expect(os.WriteFile(path, []byte(`root:x:0:0:root:/root:/bin/bash +bin:x:1:1:bin:/bin:/sbin/nologin +daemon:x:2:2:daemon:/sbin:/sbin/nologin +adm:x:3:4:adm:/var/adm:/sbin/nologin +lp:x:4:7:lp:/var/spool/lpd:/sbin/nologin +sync:x:5:0:sync:/sbin:/bin/sync +shutdown:x:6:0:shutdown:/sbin:/sbin/shutdown +halt:x:7:0:halt:/sbin:/sbin/halt +mail:x:8:12:mail:/var/spool/mail:/sbin/nologin +operator:x:11:0:operator:/root:/sbin/nologin +games:x:12:100:games:/usr/games:/sbin/nologin +ftp:x:14:50:FTP User:/var/ftp:/sbin/nologin +nobody:x:65534:65534:Kernel Overflow User:/:/sbin/nologin +`), 0600)).To(Succeed()) + + duringBuildPermissions := utils.GetDuringBuildPermissions(path) + + Expect(duringBuildPermissions).To(Equal( + structs.DuringBuildPermissions{ + CNB_USER_ID: constants.DEFAULT_USER_ID, + CNB_GROUP_ID: constants.DEFAULT_GROUP_ID}, + )) + }) + }) + + context("/etc/passwd does NOT exist", func() { + it("It should return the default permissions", func() { + tmpDir := t.TempDir() + duringBuilderPermissions := utils.GetDuringBuildPermissions(tmpDir) + + Expect(duringBuilderPermissions).To(Equal( + structs.DuringBuildPermissions{ + CNB_USER_ID: constants.DEFAULT_USER_ID, + CNB_GROUP_ID: constants.DEFAULT_GROUP_ID}, + )) + }) + }) +} diff --git a/run/main.go b/run/main.go index 798cad2..b4afe1a 100644 --- a/run/main.go +++ b/run/main.go @@ -9,15 +9,18 @@ import ( "github.com/paketo-buildpacks/packit/v2/scribe" ubinodejsextension "github.com/paketo-community/ubi-nodejs-extension" + "github.com/paketo-community/ubi-nodejs-extension/internal/utils" ) +const IMAGES_JSON_PATH = "/etc/buildpacks/images.json" + func main() { dependencyManager := postal.NewService(cargo.NewTransport()) logEmitter := scribe.NewEmitter(os.Stdout).WithLevel(os.Getenv("BP_LOG_LEVEL")) - duringBuildPermissions := ubinodejsextension.GetDuringBuildPermissions("/etc/passwd") + duringBuildPermissions := utils.GetDuringBuildPermissions("/etc/passwd") packit.RunExtension( ubinodejsextension.Detect(), - ubinodejsextension.Generate(dependencyManager, logEmitter, duringBuildPermissions), + ubinodejsextension.Generate(dependencyManager, logEmitter, duringBuildPermissions, IMAGES_JSON_PATH), ) } diff --git a/structs/structs.go b/structs/structs.go new file mode 100644 index 0000000..3bc5050 --- /dev/null +++ b/structs/structs.go @@ -0,0 +1,15 @@ +package structs + +type DuringBuildPermissions struct { + CNB_USER_ID, CNB_GROUP_ID int +} + +type BuildDockerfileProps struct { + NODEJS_VERSION uint64 + CNB_USER_ID, CNB_GROUP_ID int + CNB_STACK_ID, PACKAGES string +} + +type RunDockerfileProps struct { + Source string +}