diff --git a/fixtures/with_sealights/dist/server.js b/fixtures/with_sealights/dist/server.js new file mode 100644 index 000000000..87fb305e0 --- /dev/null +++ b/fixtures/with_sealights/dist/server.js @@ -0,0 +1,14 @@ +const express = require("express"); +const logfmt = require("logfmt"); +const app = express(); + +app.use(logfmt.requestLogger()); + +app.get('/', function(req, res) { + res.send('Hello, World!'); +}); + +var port = Number(process.env.PORT || 5000); +app.listen(port, function() { + console.log("Listening on " + port); +}); diff --git a/fixtures/with_sealights/package.json b/fixtures/with_sealights/package.json new file mode 100644 index 000000000..164ed731a --- /dev/null +++ b/fixtures/with_sealights/package.json @@ -0,0 +1,17 @@ +{ + "name": "node_web_app", + "version": "0.0.0", + "description": "hello, world", + "scripts": { + "start": "node ./dist/server.js" + }, + "author": "", + "license": "BSD-2-Clause", + "dependencies": { + "express": "~4.15.2", + "logfmt": "~1.2.0" + }, + "engines": { + "node": "~>16" + } +} diff --git a/src/nodejs/hooks/sealights.go b/src/nodejs/hooks/sealights.go new file mode 100644 index 000000000..e226c6064 --- /dev/null +++ b/src/nodejs/hooks/sealights.go @@ -0,0 +1,411 @@ +package hooks + +import ( + "encoding/json" + "fmt" + "io" + "io/ioutil" + "os" + "path/filepath" + "regexp" + "strings" + + "github.com/cloudfoundry/libbuildpack" +) + +const EmptyTokenError = "token cannot be empty (env SL_TOKEN | SL_TOKEN_FILE)" +const CommandStringError = "Cannot find command begin term" +const NpmCommandStringError = "NPM command without package.json file is not supported" +const SealightsNotBoundError = "Sealights service not bound" +const EmptyBuildError = "build session id cannot be empty (env SL_BUILD_SESSION_ID | SL_BUILD_SESSION_ID_FILE)" +const Procfile = "Procfile" +const PackageJsonFile = "package.json" +const ManifestFile = "manifest.yml" + +type Command interface { + Execute(dir string, stdout io.Writer, stderr io.Writer, program string, args ...string) error +} + +type SealightsHook struct { + libbuildpack.DefaultHook + Log *libbuildpack.Logger + Command Command +} + +type SealightsOptions struct { + Token string + TokenFile string + BsId string + BsIdFile string + Proxy string + LabId string + ProjectRoot string + TestStage string + App string +} + +type Manifest struct { + Applications []struct { + Name string `yaml:"name"` + Command string `yaml:"command"` + } `yaml:"applications"` +} + +type PackageJson struct { + Name string `json:"name"` + Command string `json:"main"` +} + +func init() { + logger := libbuildpack.NewLogger(os.Stdout) + command := &libbuildpack.Command{} + libbuildpack.AddHook(&SealightsHook{ + Log: logger, + Command: command, + }) +} + +func (sl *SealightsHook) AfterCompile(stager *libbuildpack.Stager) error { + sl.Log.Info("inside Sealights hook") + + if !sl.RunWithSealights() { + sl.Log.Info("service is not configured to run with Sealights") + return nil + } + + err := sl.injectSealights(stager) + if err != nil { + sl.Log.Error("error injecting Sealights: %s", err) + return nil + } + + err = sl.installAgent(stager) + if err != nil { + return err + } + + return nil +} + +func (sl *SealightsHook) RunWithSealights() bool { + isTokenFound, _, _ := sl.GetTokenFromEnvironment() + return isTokenFound +} + +func (sl *SealightsHook) SetApplicationStartInProcfile(stager *libbuildpack.Stager) error { + bytes, err := ioutil.ReadFile(filepath.Join(stager.BuildDir(), Procfile)) + if err != nil { + sl.Log.Error("failed to read %s", Procfile) + return err + } + + originalStartCommand := string(bytes) + _, usePackageJson := sl.usePackageJson(originalStartCommand, stager) + if usePackageJson { + // move to package json scenario + return sl.SetApplicationStartInPackageJson(stager) + } + + // we suppose that format is "web: node " + var newCmd string + err, newCmd = sl.updateStartCommand(originalStartCommand) + + if err != nil { + return err + } + + if newCmd == "" { + return nil + } + + startCommand := "web: " + newCmd + + err = ioutil.WriteFile(filepath.Join(stager.BuildDir(), Procfile), []byte(startCommand), 0755) + if err != nil { + sl.Log.Error("failed to update %s, error: %s", Procfile, err.Error()) + return err + } + + return nil +} + +func (sl *SealightsHook) usePackageJson(originalStartCommand string, stager *libbuildpack.Stager) (error, bool) { + + isNpmCommand, err := regexp.MatchString(`(^(web:\s)?cd[^&]*\s&&\snpm)|(^(web:\s)?npm)`, originalStartCommand) + if err != nil { + return err, false + } + + isPackageExists := fileExists(filepath.Join(stager.BuildDir(), PackageJsonFile)) + if !isNpmCommand { + return err, false + } + + if isNpmCommand && isPackageExists { + // move to package json scenario + return nil, true + } + + // case with npm command without package.json is not supported + return fmt.Errorf(NpmCommandStringError), false +} + +func (sl *SealightsHook) getSealightsOptions(app string, token string, tokenFile string) *SealightsOptions { + o := &SealightsOptions{ + Token: token, + TokenFile: tokenFile, + BsId: os.Getenv("SL_BUILD_SESSION_ID"), + BsIdFile: os.Getenv("SL_BUILD_SESSION_ID_FILE"), + Proxy: os.Getenv("SL_PROXY"), + LabId: os.Getenv("SL_LAB_ID"), + ProjectRoot: os.Getenv("SL_PROJECT_ROOT"), + TestStage: os.Getenv("SL_TEST_STAGE"), + App: app, + } + return o +} + +func (sl *SealightsHook) SetApplicationStartInPackageJson(stager *libbuildpack.Stager) error { + packageJson, err := sl.ReadPackageJson(stager) + if err != nil { + return err + } + scripts, _ := packageJson["scripts"].(map[string]interface{}) + if scripts == nil { + return fmt.Errorf("failed to read scripts from %s", PackageJsonFile) + } + originalStartScript, _ := scripts["start"].(string) + if originalStartScript == "" { + return fmt.Errorf("failed to read start from scripts in %s", PackageJsonFile) + } + // we suppose that format is "start: node " + var newCmd string + err, newCmd = sl.updateStartCommand(originalStartScript) + if err != nil { + return err + } + packageJson["scripts"].(map[string]interface{})["start"] = newCmd + + err = libbuildpack.NewJSON().Write(filepath.Join(stager.BuildDir(), PackageJsonFile), packageJson) + if err != nil { + sl.Log.Error("failed to update %s, error: %s", PackageJsonFile, err.Error()) + return err + } + + return nil +} + +func (sl *SealightsHook) ReadPackageJson(stager *libbuildpack.Stager) (map[string]interface{}, error) { + p := map[string]interface{}{} + + if err := libbuildpack.NewJSON().Load(filepath.Join(stager.BuildDir(), "package.json"), &p); err != nil { + if err != nil { + sl.Log.Error("failed to read %s error: %s", Procfile, err.Error()) + return nil, err + } + } + return p, nil +} + +func (sl *SealightsHook) SetApplicationStartInManifest(stager *libbuildpack.Stager) error { + y := &libbuildpack.YAML{} + err, m := sl.ReadManifestFile(stager, y) + if err != nil { + return err + } + originalStartCommand := m.Applications[0].Command + + _, usePackageJson := sl.usePackageJson(originalStartCommand, stager) + if usePackageJson { + // move to package json scenario + return sl.SetApplicationStartInPackageJson(stager) + } + + // we suppose that format is "start: node " + var newCmd string + err, newCmd = sl.updateStartCommand(originalStartCommand) + if err != nil { + return err + } + + m.Applications[0].Command = newCmd + err = y.Write(filepath.Join(stager.BuildDir(), ManifestFile), m) + if err != nil { + sl.Log.Error("failed to update %s, error: %s", ManifestFile, err.Error()) + return err + } + + return nil +} + +func (sl *SealightsHook) updateStartCommand(originalCommand string) (error, string) { + slTokenFound, token, tokenFile := sl.GetTokenFromEnvironment() + + if !slTokenFound { + sl.Log.Info("Sealights service not found") + return fmt.Errorf(SealightsNotBoundError), "" + } + + split := strings.SplitAfterN(originalCommand, "node", 2) + + if len(split) < 2 { + return fmt.Errorf(CommandStringError), "" + } + o := sl.getSealightsOptions(split[1], token, tokenFile) + + err := sl.validate(o) + if err != nil { + return err, "" + } + newCmd := sl.createAppStartCommandLine(o) + sl.Log.Debug("new start script: %s", newCmd) + return nil, newCmd +} + +func (sl *SealightsHook) ReadManifestFile(stager *libbuildpack.Stager, y *libbuildpack.YAML) (error, Manifest) { + var m Manifest + if err := y.Load(filepath.Join(stager.BuildDir(), ManifestFile), &m); err != nil { + if err != nil { + sl.Log.Error("failed to read %s error: %s", ManifestFile, err.Error()) + return err, m + } + } + return nil, m +} + +func (sl *SealightsHook) installAgent(stager *libbuildpack.Stager) error { + err := sl.Command.Execute(stager.BuildDir(), os.Stdout, os.Stderr, "npm", "install", "slnodejs") + if err != nil { + sl.Log.Error("npm install slnodejs failed with error: " + err.Error()) + return err + } + sl.Log.Info("npm install slnodejs finished successfully") + return nil +} + +func (sl *SealightsHook) createAppStartCommandLine(o *SealightsOptions) string { + var sb strings.Builder + sb.WriteString("./node_modules/.bin/slnodejs run --useinitialcolor true ") + + if o.TokenFile != "" { + sb.WriteString(fmt.Sprintf(" --tokenfile %s", o.TokenFile)) + } else { + sb.WriteString(fmt.Sprintf(" --token %s", o.Token)) + } + + if o.BsIdFile != "" { + sb.WriteString(fmt.Sprintf(" --buildsessionidfile %s", o.BsIdFile)) + } else { + sb.WriteString(fmt.Sprintf(" --buildsessionid %s", o.BsId)) + } + + if o.Proxy != "" { + sb.WriteString(fmt.Sprintf(" --proxy %s ", o.Proxy)) + } + + if o.LabId != "" { + sb.WriteString(fmt.Sprintf(" --labid %s ", o.LabId)) + } + + if o.ProjectRoot != "" { + sb.WriteString(fmt.Sprintf(" --projectroot %s ", o.ProjectRoot)) + } + + // test stage contains white space(e.g. "Unit Tests", make it quoted + if o.TestStage != "" { + sb.WriteString(fmt.Sprintf(" --teststage \"%s\" ", o.TestStage)) + } + + sb.WriteString(fmt.Sprintf(" %s", o.App)) + return sb.String() +} + +func (sl *SealightsHook) validate(o *SealightsOptions) error { + if o.Token == "" && o.TokenFile == "" { + sl.Log.Error(EmptyTokenError) + return fmt.Errorf(EmptyTokenError) + } + + if o.BsId == "" && o.BsIdFile == "" { + sl.Log.Error(EmptyBuildError) + return fmt.Errorf(EmptyBuildError) + } + + return nil +} + +func (sl *SealightsHook) injectSealights(stager *libbuildpack.Stager) error { + if _, err := os.Stat(filepath.Join(stager.BuildDir(), Procfile)); err == nil { + sl.Log.Info("Integrating sealights into procfile") + return sl.SetApplicationStartInProcfile(stager) + } else if _, err := os.Stat(filepath.Join(stager.BuildDir(), ManifestFile)); err == nil { + sl.Log.Info("Integrating sealights into manifest.yml") + return sl.SetApplicationStartInManifest(stager) + } else { + sl.Log.Info("Integrating sealights into package.json") + return sl.SetApplicationStartInPackageJson(stager) + } +} + +func containsSealightsService(key string, services interface{}, query string) bool { + var serviceName string + + if strings.Contains(key, query) { + return true + } + val := services.([]interface{}) + for serviceIndex := range val { + service := val[serviceIndex].(map[string]interface{}) + if v, ok := service["name"]; ok { + serviceName = v.(string) + } + if strings.Contains(serviceName, query) { + return true + } + } + return false +} + +func (sl *SealightsHook) GetTokenFromEnvironment() (bool, string, string) { + + type rawVcapServicesJSONValue map[string]interface{} + + var vcapServices rawVcapServicesJSONValue + + vcapServicesEnvironment := os.Getenv("VCAP_SERVICES") + + if vcapServicesEnvironment == "" { + sl.Log.Debug("Sealights could not find VCAP_SERVICES in the environment") + return false, "", "" + } + + err := json.Unmarshal([]byte(vcapServicesEnvironment), &vcapServices) + if err != nil { + sl.Log.Warning("Sealights could not parse VCAP_SERVICES") + return false, "", "" + } + + for key, services := range vcapServices { + if containsSealightsService(key, services, "sealights") { + sl.Log.Debug("Sealights found credentials in VCAP_SERVICES") + val := services.([]interface{}) + for serviceIndex := range val { + service := val[serviceIndex].(map[string]interface{}) + if credentials, exists := service["credentials"].(map[string]interface{}); exists { + token := getContrastCredentialString(credentials, "token") + tokenFile := getContrastCredentialString(credentials, "tokenFile") + if token == "" && tokenFile == "" { + return false, "", "" + } + return true, token, tokenFile + } + } + } + } + return false, "", "" +} + +func fileExists(path string) bool { + _, err := os.Stat(path) + return !os.IsNotExist(err) +} diff --git a/src/nodejs/hooks/sealights_test.go b/src/nodejs/hooks/sealights_test.go new file mode 100644 index 000000000..116d3d42b --- /dev/null +++ b/src/nodejs/hooks/sealights_test.go @@ -0,0 +1,327 @@ +package hooks_test + +import ( + "bytes" + "github.com/cloudfoundry/libbuildpack" + "github.com/cloudfoundry/nodejs-buildpack/src/nodejs/hooks" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + "io" + "io/ioutil" + "os" + "path/filepath" + "strings" +) + +type Command struct { + called bool +} + +func (c *Command) Execute(dir string, stdout io.Writer, stderr io.Writer, program string, args ...string) error { + c.called = true + return nil +} + +var _ = Describe("Sealights hook", func() { + var ( + err error + buildDir string + logger *libbuildpack.Logger + buffer *bytes.Buffer + stager *libbuildpack.Stager + sealights *hooks.SealightsHook + yamlFile *libbuildpack.YAML + build string + proxy string + labId string + projectRoot string + testStage string + procfile string + command *Command + procfileName = "Procfile" + packageJsonName = "package.json" + manifestName = "manifest.yml" + originalStartCommand = "node index.js --build 192 --name Good" + testProcfile = "web: " + originalStartCommand + testPackageJson = "{\n \"scripts\": {\n \"start\": \"" + originalStartCommand + "\"\n }\n}" + testPackageJsonWithoutScripts = "{\n \"skriptz\": {\n \"start\": \"" + originalStartCommand + "\"\n }\n}" + testPackageJsonWithoutStart = "{\n \"scripts\": {\n \"begin\": \"" + originalStartCommand + "\"\n }\n}" + testManifest = "---\n" + + "applications:\n" + + " - name: Good\n" + + " command: " + originalStartCommand + expected = strings.ReplaceAll("./node_modules/.bin/slnodejs run --useinitialcolor true --token good_token --buildsessionid goodBsid --proxy http://localhost:1886 --labid Roni's --projectroot project/root --teststage \"Unit Tests\" index.js --build 192 --name Good", " ", "") + expectedWithFile = strings.ReplaceAll("./node_modules/.bin/slnodejs run --useinitialcolor true --token good_token --buildsessionidfile build/id/file --proxy http://localhost:1886 --labid Roni's --projectroot project/root --teststage \"Unit Tests\" index.js --build 192 --name Good", " ", "") + ) + + BeforeEach(func() { + buildDir, err = ioutil.TempDir("", "nodejs-buildpack.build.") + Expect(err).To(BeNil()) + + buffer = new(bytes.Buffer) + logger = libbuildpack.NewLogger(buffer) + args := []string{buildDir, ""} + stager = libbuildpack.NewStager(args, logger, &libbuildpack.Manifest{}) + + build = os.Getenv("SL_BUILD_SESSION_ID_FILE") + proxy = os.Getenv("SL_PROXY") + labId = os.Getenv("SL_LAB_ID") + projectRoot = os.Getenv("SL_PROJECT_ROOT") + testStage = os.Getenv("SL_TEST_STAGE") + command = &Command{} + sealights = &hooks.SealightsHook{ + libbuildpack.DefaultHook{}, + logger, + command, + } + }) + + AfterEach(func() { + err = os.Setenv("SL_BUILD_SESSION_ID", build) + Expect(err).To(BeNil()) + err = os.Setenv("SL_PROXY", proxy) + Expect(err).To(BeNil()) + err = os.Setenv("SL_LAB_ID", labId) + Expect(err).To(BeNil()) + err = os.Setenv("SL_PROJECT_ROOT", projectRoot) + Expect(err).To(BeNil()) + err = os.Setenv("SL_TEST_STAGE", testStage) + Expect(err).To(BeNil()) + err = os.Unsetenv("VCAP_SERVICES") + Expect(err).To(BeNil()) + err = ioutil.WriteFile(filepath.Join(stager.BuildDir(), procfileName), []byte(procfile), 0755) + Expect(err).To(BeNil()) + err = os.RemoveAll(buildDir) + Expect(err).To(BeNil()) + }) + + Describe("AfterCompile", func() { + var ( + token = "good_token" + bsid = "goodBsid" + bsidFile = "build/id/file" + proxy = "http://localhost:1886" + lab = "Roni's" + root = "project/root" + stage = "Unit Tests" + ) + BeforeEach(func() { + Expect(err).To(BeNil()) + err = os.Setenv("SL_BUILD_SESSION_ID", bsid) + Expect(err).To(BeNil()) + err = os.Setenv("SL_BUILD_SESSION_ID_FILE", bsidFile) + Expect(err).To(BeNil()) + err = os.Setenv("SL_PROXY", proxy) + Expect(err).To(BeNil()) + }) + Context("Sealigts not injected well", func() { + BeforeEach(func() { + err = ioutil.WriteFile(filepath.Join(stager.BuildDir(), procfileName), []byte(testProcfile), 0755) + Expect(err).To(BeNil()) + }) + It("Not found in VCAP_Services", func() { + with := sealights.RunWithSealights() + Expect(with).To(BeFalse()) + err = sealights.AfterCompile(stager) + Expect(err).To(BeNil()) + Expect(command.called).To(BeFalse()) + }) + It("hook fails with empty token", func() { + err = os.Setenv("VCAP_SERVICES", `{"user-provided":[ + { "label": "user-provided", + "name": "sealights", + "credentials": { + "token": "" + } + } + ]}`) + + Expect(err).To(BeNil()) + err = sealights.AfterCompile(stager) + Expect(err).To(BeNil()) + Expect(command.called).To(BeFalse()) + }) + }) + Context("Sealights injection", func() { + BeforeEach(func() { + os.Setenv("VCAP_SERVICES", `{"user-provided":[ + { "label": "user-provided", + "name": "sealights", + "credentials": { + "token": "`+token+`" + } + } + ]}`) + }) + Context("build new application run command in Procfile", func() { + BeforeEach(func() { + err = ioutil.WriteFile(filepath.Join(stager.BuildDir(), procfileName), []byte(testProcfile), 0755) + Expect(err).To(BeNil()) + }) + It("test application run cmd creation from bsid file", func() { + err = os.Setenv("SL_LAB_ID", lab) + Expect(err).To(BeNil()) + err = os.Setenv("SL_PROJECT_ROOT", root) + Expect(err).To(BeNil()) + err = os.Setenv("SL_TEST_STAGE", stage) + Expect(err).To(BeNil()) + err = sealights.SetApplicationStartInProcfile(stager) + Expect(err).To(BeNil()) + bytes, err := ioutil.ReadFile(filepath.Join(stager.BuildDir(), procfileName)) + Expect(err).To(BeNil()) + cleanResult := strings.ReplaceAll(string(bytes), " ", "") + Expect(cleanResult).To(Equal("web:" + expectedWithFile)) + }) + It("hook fails with empty build session id", func() { + err = os.Setenv("SL_BUILD_SESSION_ID", "") + Expect(err).NotTo(HaveOccurred()) + err = os.Setenv("SL_BUILD_SESSION_ID_FILE", "") + Expect(err).NotTo(HaveOccurred()) + err = sealights.SetApplicationStartInProcfile(stager) + Expect(err).To(MatchError(ContainSubstring(hooks.EmptyBuildError))) + }) + It("test application run cmd creation", func() { + err = os.Setenv("SL_LAB_ID", lab) + Expect(err).To(BeNil()) + err = os.Setenv("SL_PROJECT_ROOT", root) + Expect(err).To(BeNil()) + err = os.Setenv("SL_TEST_STAGE", stage) + Expect(err).To(BeNil()) + err = os.Setenv("SL_BUILD_SESSION_ID_FILE", "") + Expect(err).NotTo(HaveOccurred()) + Expect(err).To(BeNil()) + err = sealights.SetApplicationStartInProcfile(stager) + bytes, err := ioutil.ReadFile(filepath.Join(stager.BuildDir(), procfileName)) + Expect(err).To(BeNil()) + cleanResult := strings.ReplaceAll(string(bytes), " ", "") + Expect(cleanResult).To(Equal("web:" + expected)) + }) + }) + + Context("fail to update package.json scripts", func() { + BeforeEach(func() { + err = ioutil.WriteFile(filepath.Join(stager.BuildDir(), packageJsonName), []byte(testPackageJsonWithoutScripts), 0755) + Expect(err).To(BeNil()) + }) + + AfterEach(func() { + os.Remove(filepath.Join(stager.BuildDir(), packageJsonName)) + }) + + It("fail to find scripts section in package.json", func() { + err = sealights.SetApplicationStartInPackageJson(stager) + Expect(err).ShouldNot(BeNil()) + }) + }) + + Context("fail to update package.json start", func() { + BeforeEach(func() { + err = ioutil.WriteFile(filepath.Join(stager.BuildDir(), packageJsonName), []byte(testPackageJsonWithoutStart), 0755) + Expect(err).To(BeNil()) + }) + + AfterEach(func() { + os.Remove(filepath.Join(stager.BuildDir(), packageJsonName)) + }) + + It("fail to start under scripts section in package.json", func() { + err = sealights.SetApplicationStartInPackageJson(stager) + Expect(err).ShouldNot(BeNil()) + }) + }) + + Context("build new application run command in package.json", func() { + BeforeEach(func() { + err = ioutil.WriteFile(filepath.Join(stager.BuildDir(), packageJsonName), []byte(testPackageJson), 0755) + Expect(err).To(BeNil()) + }) + + It("test application run cmd creation from bsid file", func() { + err = os.Setenv("SL_LAB_ID", lab) + Expect(err).To(BeNil()) + err = os.Setenv("SL_PROJECT_ROOT", root) + Expect(err).To(BeNil()) + err = os.Setenv("SL_TEST_STAGE", stage) + Expect(err).To(BeNil()) + err = sealights.SetApplicationStartInPackageJson(stager) + Expect(err).To(BeNil()) + packageJson, err := sealights.ReadPackageJson(stager) + Expect(err).To(BeNil()) + cleanResult := strings.ReplaceAll(packageJson["scripts"].(map[string]interface{})["start"].(string), " ", "") + Expect(cleanResult).To(Equal(expectedWithFile)) + }) + It("hook fails with empty build session id", func() { + err = os.Setenv("SL_BUILD_SESSION_ID", "") + Expect(err).NotTo(HaveOccurred()) + err = os.Setenv("SL_BUILD_SESSION_ID_FILE", "") + Expect(err).NotTo(HaveOccurred()) + err = sealights.SetApplicationStartInPackageJson(stager) + Expect(err).To(MatchError(ContainSubstring(hooks.EmptyBuildError))) + }) + It("test application run cmd creation", func() { + err = os.Setenv("SL_LAB_ID", lab) + Expect(err).To(BeNil()) + err = os.Setenv("SL_PROJECT_ROOT", root) + Expect(err).To(BeNil()) + err = os.Setenv("SL_TEST_STAGE", stage) + Expect(err).To(BeNil()) + err = os.Setenv("SL_BUILD_SESSION_ID_FILE", "") + Expect(err).NotTo(HaveOccurred()) + Expect(err).To(BeNil()) + err = sealights.SetApplicationStartInPackageJson(stager) + packageJson, err := sealights.ReadPackageJson(stager) + Expect(err).To(BeNil()) + cleanResult := strings.ReplaceAll(packageJson["scripts"].(map[string]interface{})["start"].(string), " ", "") + Expect(cleanResult).To(Equal(expected)) + }) + }) + + Context("build new application run command in manifest", func() { + BeforeEach(func() { + err = ioutil.WriteFile(filepath.Join(stager.BuildDir(), manifestName), []byte(testManifest), 0755) + Expect(err).To(BeNil()) + }) + + It("test application run cmd creation from bsid file", func() { + err = os.Setenv("SL_LAB_ID", lab) + Expect(err).To(BeNil()) + err = os.Setenv("SL_PROJECT_ROOT", root) + Expect(err).To(BeNil()) + err = os.Setenv("SL_TEST_STAGE", stage) + Expect(err).To(BeNil()) + err = sealights.SetApplicationStartInManifest(stager) + Expect(err).To(BeNil()) + err, manifestFile := sealights.ReadManifestFile(stager, yamlFile) + Expect(err).To(BeNil()) + cleanResult := strings.ReplaceAll(manifestFile.Applications[0].Command, " ", "") + Expect(cleanResult).To(Equal(expectedWithFile)) + }) + It("hook fails with empty build session id", func() { + err = os.Setenv("SL_BUILD_SESSION_ID", "") + Expect(err).NotTo(HaveOccurred()) + err = os.Setenv("SL_BUILD_SESSION_ID_FILE", "") + Expect(err).NotTo(HaveOccurred()) + err = sealights.SetApplicationStartInManifest(stager) + Expect(err).To(MatchError(ContainSubstring(hooks.EmptyBuildError))) + }) + It("test application run cmd creation", func() { + err = os.Setenv("SL_LAB_ID", lab) + Expect(err).To(BeNil()) + err = os.Setenv("SL_PROJECT_ROOT", root) + Expect(err).To(BeNil()) + err = os.Setenv("SL_TEST_STAGE", stage) + Expect(err).To(BeNil()) + err = os.Setenv("SL_BUILD_SESSION_ID_FILE", "") + Expect(err).NotTo(HaveOccurred()) + Expect(err).To(BeNil()) + err = sealights.SetApplicationStartInManifest(stager) + err, manifestFile := sealights.ReadManifestFile(stager, yamlFile) + Expect(err).To(BeNil()) + cleanResult := strings.ReplaceAll(manifestFile.Applications[0].Command, " ", "") + Expect(cleanResult).To(Equal(expected)) + }) + }) + }) + + }) +}) diff --git a/src/nodejs/integration/nodejs_app_with_sealights_test.go b/src/nodejs/integration/nodejs_app_with_sealights_test.go new file mode 100644 index 000000000..0b8445295 --- /dev/null +++ b/src/nodejs/integration/nodejs_app_with_sealights_test.go @@ -0,0 +1,96 @@ +package integration_test + +import ( + "archive/tar" + "compress/gzip" + "encoding/json" + "fmt" + "io" + "io/ioutil" + "os" + "path/filepath" + "strings" + + "github.com/cloudfoundry/libbuildpack/cutlass" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("CF NodeJS Buildpack", func() { + var ( + app *cutlass.App + serviceBrokerApp *cutlass.App + serviceNameOne string + files []string + expected = strings.ReplaceAll("./node_modules/.bin/slnodejs run --useinitialcolor true --token token1 --buildsessionid bs1 ./dist/server.js", " ", "") + ) + + BeforeEach(func() { + serviceNameOne = "sealights-" + cutlass.RandStringRunes(20) + }) + + AfterEach(func() { + app = DestroyApp(app) + + _ = RunCF("delete-service", "-f", serviceNameOne) + + serviceBrokerApp = DestroyApp(serviceBrokerApp) + + for _, file := range files { + err := os.Remove(file) + Expect(err).NotTo(HaveOccurred()) + } + }) + + It("deploying a NodeJS app with sealights", func() { + app = cutlass.New(Fixtures("with_sealights")) + app.Name = "nodejs-sealights-" + cutlass.RandStringRunes(10) + app.Memory = "256M" + app.Disk = "512M" + + app.SetEnv("SL_BUILD_SESSION_ID", "bs1") + + By("Pushing an app with a user provided service", func() { + Expect(RunCF("create-user-provided-service", serviceNameOne, "-p", `{ + "token": "token1" + }`)).To(Succeed()) + + Expect(app.PushNoStart()).To(Succeed()) + Expect(RunCF("bind-service", app.Name, serviceNameOne)).To(Succeed()) + Expect(app.Restart()).To(Succeed()) + + tmpDropletFile := filepath.Join(os.TempDir(), fmt.Sprintf("droplet-%s.tgz", cutlass.RandStringRunes(10))) + files = append(files, tmpDropletFile) + Expect(app.DownloadDroplet(tmpDropletFile)).To(Succeed()) + file, err := os.Open(tmpDropletFile) + Expect(err).ToNot(HaveOccurred()) + defer file.Close() + gz, err := gzip.NewReader(file) + Expect(err).ToNot(HaveOccurred()) + defer gz.Close() + tr := tar.NewReader(gz) + + for { + hdr, err := tr.Next() + if err == io.EOF { + break + } + if hdr.Name != "./app/package.json" { + continue + } + b, err := ioutil.ReadAll(tr) + p := map[string]interface{}{} + json.Unmarshal(b, &p) + + actual := strings.ReplaceAll(p["scripts"].(map[string]interface{})["start"].(string), " ", "") + Expect(actual).To(Equal(expected)) + } + }) + + By("Unbinding and deleting the CUPS seeker service", func() { + Expect(RunCF("unbind-service", app.Name, serviceNameOne)).To(Succeed()) + Expect(RunCF("delete-service", "-f", serviceNameOne)).To(Succeed()) + }) + }) +})