diff --git a/README.md b/README.md index 19319cbd..fc74f898 100644 --- a/README.md +++ b/README.md @@ -138,6 +138,17 @@ the `BP_NODE_PROJECT_PATH` environment variable at build time either directly file](https://github.com/buildpacks/spec/blob/main/extensions/project-descriptor.md). This could be useful if your app is a part of a monorepo. +### Enabling Inspector for Remote Debugging + +To enable the Inspector set the `BPL_DEBUG_ENABLED` environment variable at launch time. Optionally, you can specify the `BPL_DEBUG_PORT` environment variable to use a specific port. + +```shell +$BPL_DEBUG_ENABLED="true" +$BPL_DEBUG_PORT="9009" +``` + +For more information on debugging, see [Official Documentation](https://nodejs.org/en/docs/guides/debugging-getting-started) + ## Run Tests To run all unit tests, run: diff --git a/build.go b/build.go index 59909f09..8cbbed0a 100644 --- a/build.go +++ b/build.go @@ -7,13 +7,13 @@ import ( "strconv" "time" + "github.com/paketo-buildpacks/libnodejs" "github.com/paketo-buildpacks/packit/v2" "github.com/paketo-buildpacks/packit/v2/cargo" "github.com/paketo-buildpacks/packit/v2/chronos" "github.com/paketo-buildpacks/packit/v2/postal" "github.com/paketo-buildpacks/packit/v2/sbom" "github.com/paketo-buildpacks/packit/v2/scribe" - "github.com/paketo-buildpacks/libnodejs" ) //go:generate faux --interface EntryResolver --output fakes/entry_resolver.go @@ -191,8 +191,10 @@ func Build(entryResolver EntryResolver, dependencyManager DependencyManager, sbo } logger.EnvironmentVariables(nodeLayer) - - nodeLayer.ExecD = []string{filepath.Join(context.CNBPath, "bin", "optimize-memory")} + nodeLayer.ExecD = []string{ + filepath.Join(context.CNBPath, "bin", "optimize-memory"), + filepath.Join(context.CNBPath, "bin", "inspector"), + } logger.Subprocess("Writing exec.d/0-optimize-memory") logger.Action("Calculates available memory based on container limits at launch time.") @@ -201,6 +203,7 @@ func Build(entryResolver EntryResolver, dependencyManager DependencyManager, sbo logger.Action("Assigns the NODE_OPTIONS environment variable with flag setting to optimize memory.") logger.Action("Limits the total size of all objects on the heap to 75%% of the MEMORY_AVAILABLE.") } + logger.Subprocess("Writing exec.d/1-inspector") logger.Break() return packit.BuildResult{ diff --git a/build_test.go b/build_test.go index ccd7c37f..5a8332d4 100644 --- a/build_test.go +++ b/build_test.go @@ -134,6 +134,7 @@ func testBuild(t *testing.T, context spec.G, it spec.S) { })) Expect(layer.ExecD).To(Equal([]string{ filepath.Join(cnbDir, "bin", "optimize-memory"), + filepath.Join(cnbDir, "bin", "inspector"), })) Expect(layer.Metadata).To(Equal(map[string]interface{}{ @@ -238,6 +239,7 @@ func testBuild(t *testing.T, context spec.G, it spec.S) { Expect(buffer.String()).To(ContainSubstring(" Made available in the MEMORY_AVAILABLE environment variable.")) Expect(buffer.String()).NotTo(ContainSubstring(" Assigns the NODE_OPTIONS environment variable with flag setting to optimize memory.")) Expect(buffer.String()).NotTo(ContainSubstring(" Limits the total size of all objects on the heap to 75% of the MEMORY_AVAILABLE.")) + Expect(buffer.String()).To(ContainSubstring(" Writing exec.d/1-inspector")) }) context("when the os environment contains a directive to optimize memory", func() { @@ -471,6 +473,7 @@ func testBuild(t *testing.T, context spec.G, it spec.S) { })) Expect(layer.ExecD).To(Equal([]string{ filepath.Join(cnbDir, "bin", "optimize-memory"), + filepath.Join(cnbDir, "bin", "inspector"), })) Expect(layer.Metadata).To(Equal(map[string]interface{}{ @@ -488,6 +491,7 @@ func testBuild(t *testing.T, context spec.G, it spec.S) { Expect(buffer.String()).To(ContainSubstring(" Made available in the MEMORY_AVAILABLE environment variable.")) Expect(buffer.String()).NotTo(ContainSubstring(" Assigns the NODE_OPTIONS environment variable with flag setting to optimize memory.")) Expect(buffer.String()).NotTo(ContainSubstring(" Limits the total size of all objects on the heap to 75% of the MEMORY_AVAILABLE.")) + Expect(buffer.String()).To(ContainSubstring(" Writing exec.d/1-inspector")) }) }) diff --git a/buildpack.toml b/buildpack.toml index f4f9d581..b3ac1288 100644 --- a/buildpack.toml +++ b/buildpack.toml @@ -11,7 +11,7 @@ api = "0.7" uri = "https://github.com/paketo-buildpacks/node-engine/blob/main/LICENSE" [metadata] - include-files = ["bin/build", "bin/detect", "bin/run", "bin/optimize-memory", "buildpack.toml"] + include-files = ["bin/build", "bin/detect", "bin/run", "bin/optimize-memory", "bin/inspector", "buildpack.toml"] pre-package = "./scripts/build.sh" [metadata.default-versions] node = "20.*.*" diff --git a/cmd/inspector/internal/init_test.go b/cmd/inspector/internal/init_test.go new file mode 100644 index 00000000..38038bb8 --- /dev/null +++ b/cmd/inspector/internal/init_test.go @@ -0,0 +1,14 @@ +package internal_test + +import ( + "testing" + + "github.com/sclevine/spec" + "github.com/sclevine/spec/report" +) + +func TestUnitInspector(t *testing.T) { + suite := spec.New("cmd/inspector/internal", spec.Report(report.Terminal{})) + suite("Run", testRun) + suite.Run(t) +} diff --git a/cmd/inspector/internal/run.go b/cmd/inspector/internal/run.go new file mode 100644 index 00000000..68bfc297 --- /dev/null +++ b/cmd/inspector/internal/run.go @@ -0,0 +1,31 @@ +package internal + +import ( + "fmt" + "io" + "strings" + + "github.com/BurntSushi/toml" +) + +func Run(environment map[string]string, output io.Writer, root string) error { + variables := map[string]string{} + if debug, ok := environment["BPL_DEBUG_ENABLED"]; ok && debug == "true" { + option := "--inspect" + if debugPort, ok := environment["BPL_DEBUG_PORT"]; ok { + option = fmt.Sprintf("%s=127.0.0.1:%s", option, debugPort) + } + + if nodeOpts, ok := environment["NODE_OPTIONS"]; ok { + if strings.Contains(nodeOpts, "--inspect") { + return nil + } + + option = strings.Join([]string{nodeOpts, option}, " ") + } + + variables["NODE_OPTIONS"] = option + } + + return toml.NewEncoder(output).Encode(variables) +} diff --git a/cmd/inspector/internal/run_test.go b/cmd/inspector/internal/run_test.go new file mode 100644 index 00000000..673d8dde --- /dev/null +++ b/cmd/inspector/internal/run_test.go @@ -0,0 +1,92 @@ +package internal_test + +import ( + "bytes" + "os" + "testing" + + "github.com/paketo-buildpacks/node-engine/cmd/inspector/internal" + "github.com/sclevine/spec" + + . "github.com/onsi/gomega" + . "github.com/paketo-buildpacks/packit/v2/matchers" +) + +func testRun(t *testing.T, context spec.G, it spec.S) { + var ( + Expect = NewWithT(t).Expect + + environment map[string]string + root string + ) + + it.Before(func() { + environment = map[string]string{} + }) + + it.After(func() { + Expect(os.RemoveAll(root)).To(Succeed()) + }) + + context("when $BPL_DEBUG_ENABLE is set", func() { + it.Before(func() { + environment["BPL_DEBUG_ENABLED"] = "true" + }) + + it("--inspect is added to NODE_OPTIONS", func() { + environment["NODE_OPTIONS"] = "--existing" + buffer := bytes.NewBuffer(nil) + + err := internal.Run(environment, buffer, root) + Expect(err).NotTo(HaveOccurred()) + + Expect(buffer.String()).To(MatchTOML(` + NODE_OPTIONS = "--existing --inspect" + `)) + }) + + context("when $BPL_DEBUG_PORT is set", func() { + it.Before(func() { + environment["BPL_DEBUG_PORT"] = "1111" + }) + + it("sets the inspector port", func() { + buffer := bytes.NewBuffer(nil) + + err := internal.Run(environment, buffer, root) + Expect(err).NotTo(HaveOccurred()) + + Expect(buffer.String()).To(MatchTOML(` + NODE_OPTIONS = "--inspect=127.0.0.1:1111" + `)) + }) + }) + + context("when $NODE_OPTIONS contains --inspect flag", func() { + it.Before(func() { + environment["NODE_OPTIONS"] = "--inspect=0.0.0.0:8888" + }) + + it("does not change it", func() { + buffer := bytes.NewBuffer(nil) + + err := internal.Run(environment, buffer, root) + Expect(err).NotTo(HaveOccurred()) + + Expect(buffer.String()).To(BeEmpty()) + }) + }) + }) + + context("when $BPL_DEBUG_ENABLED is not set", func() { + it("NODE_OPTIONS are not changed", func() { + environment["NODE_OPTIONS"] = "--existing" + + buffer := bytes.NewBuffer(nil) + err := internal.Run(environment, buffer, root) + Expect(err).NotTo(HaveOccurred()) + Expect(buffer.String()).To(BeEmpty()) + }) + }) + +} diff --git a/cmd/inspector/main.go b/cmd/inspector/main.go new file mode 100644 index 00000000..77e1d2a0 --- /dev/null +++ b/cmd/inspector/main.go @@ -0,0 +1,16 @@ +package main + +import ( + "log" + "os" + + "github.com/paketo-buildpacks/node-engine/cmd/inspector/internal" + "github.com/paketo-buildpacks/node-engine/cmd/util" +) + +func main() { + err := internal.Run(util.LoadEnvironmentMap(os.Environ()), os.NewFile(3, "/dev/fd/3"), "/") + if err != nil { + log.Fatal(err) + } +} diff --git a/cmd/optimize-memory/internal/init_test.go b/cmd/optimize-memory/internal/init_test.go index acac3c6c..87e0c702 100644 --- a/cmd/optimize-memory/internal/init_test.go +++ b/cmd/optimize-memory/internal/init_test.go @@ -9,7 +9,6 @@ import ( func TestUnitOptimizeMemory(t *testing.T) { suite := spec.New("cmd/optimize-memory/internal", spec.Report(report.Terminal{})) - suite("EnvironmentMap", testEnvironmentMap) suite("Run", testRun) suite.Run(t) } diff --git a/cmd/optimize-memory/main.go b/cmd/optimize-memory/main.go index 91e4865b..48b07c59 100644 --- a/cmd/optimize-memory/main.go +++ b/cmd/optimize-memory/main.go @@ -5,10 +5,11 @@ import ( "os" "github.com/paketo-buildpacks/node-engine/cmd/optimize-memory/internal" + "github.com/paketo-buildpacks/node-engine/cmd/util" ) func main() { - err := internal.Run(internal.LoadEnvironmentMap(os.Environ()), os.NewFile(3, "/dev/fd/3"), "/") + err := internal.Run(util.LoadEnvironmentMap(os.Environ()), os.NewFile(3, "/dev/fd/3"), "/") if err != nil { log.Fatal(err) } diff --git a/cmd/optimize-memory/internal/environment_map.go b/cmd/util/environment_map.go similarity index 93% rename from cmd/optimize-memory/internal/environment_map.go rename to cmd/util/environment_map.go index 051666b8..cb109375 100644 --- a/cmd/optimize-memory/internal/environment_map.go +++ b/cmd/util/environment_map.go @@ -1,4 +1,4 @@ -package internal +package util import "strings" diff --git a/cmd/optimize-memory/internal/environment_map_test.go b/cmd/util/environment_map_test.go similarity index 75% rename from cmd/optimize-memory/internal/environment_map_test.go rename to cmd/util/environment_map_test.go index 6bc4e124..1dfde5f6 100644 --- a/cmd/optimize-memory/internal/environment_map_test.go +++ b/cmd/util/environment_map_test.go @@ -1,9 +1,9 @@ -package internal_test +package util_test import ( "testing" - "github.com/paketo-buildpacks/node-engine/cmd/optimize-memory/internal" + "github.com/paketo-buildpacks/node-engine/cmd/util" "github.com/sclevine/spec" . "github.com/onsi/gomega" @@ -13,7 +13,7 @@ func testEnvironmentMap(t *testing.T, context spec.G, it spec.S) { var Expect = NewWithT(t).Expect it("loads the environment into a map", func() { - env := internal.LoadEnvironmentMap([]string{ + env := util.LoadEnvironmentMap([]string{ "SOME_KEY=some-value", "OTHER_KEY=other-value=extra-value", }) diff --git a/cmd/util/init_test.go b/cmd/util/init_test.go new file mode 100644 index 00000000..0af85ee3 --- /dev/null +++ b/cmd/util/init_test.go @@ -0,0 +1,14 @@ +package util_test + +import ( + "testing" + + "github.com/sclevine/spec" + "github.com/sclevine/spec/report" +) + +func TestUnitUtils(t *testing.T) { + suite := spec.New("cmd/util", spec.Report(report.Terminal{})) + suite("EnvironmentMap", testEnvironmentMap) + suite.Run(t) +} diff --git a/integration/init_test.go b/integration/init_test.go index 641b75e7..149d5685 100644 --- a/integration/init_test.go +++ b/integration/init_test.go @@ -117,6 +117,7 @@ func TestIntegration(t *testing.T) { suite := spec.New("Integration", spec.Report(report.Terminal{}), spec.Parallel()) suite("Offline", testOffline) suite("OptimizeMemory", testOptimizeMemory) + suite("Inspector", testInspector) suite("ProjectPath", testProjectPath) suite("Provides", testProvides) suite("ReusingLayerRebuild", testReusingLayerRebuild) diff --git a/integration/inspector_test.go b/integration/inspector_test.go new file mode 100644 index 00000000..1a9478c9 --- /dev/null +++ b/integration/inspector_test.go @@ -0,0 +1,76 @@ +package integration + +import ( + "fmt" + "os" + "path/filepath" + "testing" + + "github.com/paketo-buildpacks/occam" + "github.com/sclevine/spec" + + . "github.com/onsi/gomega" + . "github.com/paketo-buildpacks/occam/matchers" +) + +func testInspector(t *testing.T, context spec.G, it spec.S) { + var ( + Expect = NewWithT(t).Expect + Eventually = NewWithT(t).Eventually + + docker occam.Docker + pack occam.Pack + + image occam.Image + container occam.Container + name string + source string + ) + + it.Before(func() { + docker = occam.NewDocker() + pack = occam.NewPack() + + var err error + name, err = occam.RandomName() + Expect(err).NotTo(HaveOccurred()) + }) + + it.After(func() { + Expect(docker.Container.Remove.Execute(container.ID)).To(Succeed()) + Expect(docker.Image.Remove.Execute(image.ID)).To(Succeed()) + Expect(docker.Volume.Remove.Execute(occam.CacheVolumeNames(name))).To(Succeed()) + Expect(os.RemoveAll(source)).To(Succeed()) + }) + + it("sets --inspect if set with env variable BPL_DEBUG_ENABLED", func() { + var err error + source, err = occam.Source(filepath.Join("testdata", "simple_app")) + Expect(err).NotTo(HaveOccurred()) + + var logs fmt.Stringer + image, logs, err = pack.WithNoColor().Build. + WithPullPolicy("never"). + WithBuildpacks( + settings.Buildpacks.NodeEngine.Online, + settings.Buildpacks.Processes.Online, + ). + Execute(name, source) + + Expect(err).NotTo(HaveOccurred()) + Expect(logs).To(ContainLines( + " Writing exec.d/1-inspector", + )) + + container, err = docker.Container.Run. + WithMemory("128m"). + WithPublish("8080"). + WithEnv(map[string]string{"BPL_DEBUG_ENABLED": "true", "BPL_DEBUG_PORT": "9000", "NODE_OPTIONS": "--no-warnings"}). + Execute(image.ID) + Expect(err).NotTo(HaveOccurred()) + + Eventually(container).Should(BeAvailable()) + Eventually(container).Should(Serve(ContainSubstring("NodeOptions: --no-warnings --inspect=127.0.0.1:9000")).OnPort(8080).WithEndpoint("/node-options")) + + }) +} diff --git a/integration/offline_test.go b/integration/offline_test.go index e8bf6652..2ad7a3a7 100644 --- a/integration/offline_test.go +++ b/integration/offline_test.go @@ -49,7 +49,7 @@ func testOffline(t *testing.T, context spec.G, it spec.S) { it("sets max_old_space_size when nodejs.optimize-memory is set with env variable BP_NODE_OPTIMIZE_MEMORY", func() { var err error - source, err = occam.Source(filepath.Join("testdata", "optimize_memory")) + source, err = occam.Source(filepath.Join("testdata", "simple_app")) Expect(err).NotTo(HaveOccurred()) var logs fmt.Stringer @@ -73,7 +73,7 @@ func testOffline(t *testing.T, context spec.G, it spec.S) { Expect(err).NotTo(HaveOccurred()) Eventually(container).Should(BeAvailable()) - Eventually(container).Should(Serve(ContainSubstring("NodeOptions: --use-openssl-ca --max_old_space_size=96")).OnPort(8080)) + Eventually(container).Should(Serve(ContainSubstring("NodeOptions: --use-openssl-ca --max_old_space_size=96")).OnPort(8080).WithEndpoint("/node-options")) }) }) } diff --git a/integration/optimize_memory_test.go b/integration/optimize_memory_test.go index f275511e..5cf7dab4 100644 --- a/integration/optimize_memory_test.go +++ b/integration/optimize_memory_test.go @@ -46,7 +46,7 @@ func testOptimizeMemory(t *testing.T, context spec.G, it spec.S) { it("sets max_old_space_size when nodejs.optimize-memory is set with env variable BP_NODE_OPTIMIZE_MEMORY", func() { var err error - source, err = occam.Source(filepath.Join("testdata", "optimize_memory")) + source, err = occam.Source(filepath.Join("testdata", "simple_app")) Expect(err).NotTo(HaveOccurred()) var logs fmt.Stringer @@ -69,7 +69,7 @@ func testOptimizeMemory(t *testing.T, context spec.G, it spec.S) { Expect(err).NotTo(HaveOccurred()) Eventually(container).Should(BeAvailable()) - Eventually(container).Should(Serve(ContainSubstring("NodeOptions: --no-warnings --max_old_space_size=96")).OnPort(8080)) + Eventually(container).Should(Serve(ContainSubstring("NodeOptions: --no-warnings --max_old_space_size=96")).OnPort(8080).WithEndpoint("/node-options")) Expect(logs).To(ContainLines( " Configuring launch environment", diff --git a/integration/testdata/optimize_memory/plan.toml b/integration/testdata/optimize_memory/plan.toml deleted file mode 100644 index 0746eb97..00000000 --- a/integration/testdata/optimize_memory/plan.toml +++ /dev/null @@ -1,5 +0,0 @@ -[[requires]] - name = "node" - - [requires.metadata] - launch = true diff --git a/integration/testdata/optimize_memory/server.js b/integration/testdata/optimize_memory/server.js deleted file mode 100644 index a413067f..00000000 --- a/integration/testdata/optimize_memory/server.js +++ /dev/null @@ -1,16 +0,0 @@ -const http = require('http') -const port = process.env.PORT || 8080 - -const requestHandler = (request, response) => { - response.end(`NodeOptions: ${process.env.NODE_OPTIONS}`) -} - -const server = http.createServer(requestHandler) - -server.listen(port, (err) => { - if (err) { - return console.log('something bad happened', err) - } - - console.log(`server is listening on ${port}`) -}) diff --git a/integration/testdata/simple_app/server.js b/integration/testdata/simple_app/server.js index d5842327..04b3e10e 100644 --- a/integration/testdata/simple_app/server.js +++ b/integration/testdata/simple_app/server.js @@ -18,6 +18,10 @@ const server = http.createServer((request, response) => { case '/version': response.end(process.version); break; + + case '/node-options': + response.end(`NodeOptions: ${process.env.NODE_OPTIONS}`); + break; default: response.end("hello world"); diff --git a/scripts/build.sh b/scripts/build.sh index a3e1d3d5..a925078e 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -73,16 +73,20 @@ function cmd::build() { for src in "${BUILDPACKDIR}"/cmd/*; do name="$(basename "${src}")" - printf "%s" "Building ${name}... " + if [[ -f "${src}/main.go" ]]; then + printf "%s" "Building ${name}... " - GOOS="linux" \ - CGO_ENABLED=0 \ - go build \ - -ldflags="-s -w" \ - -o "${BUILDPACKDIR}/bin/${name}" \ - "${src}/main.go" + GOOS="linux" \ + CGO_ENABLED=0 \ + go build \ + -ldflags="-s -w" \ + -o "${BUILDPACKDIR}/bin/${name}" \ + "${src}/main.go" - echo "Success!" + echo "Success!" + else + printf "%s" "Skipping ${name}... " + fi done fi }