From 014018567830a6a2f0d827d9accdc8e602b27ba1 Mon Sep 17 00:00:00 2001 From: Sam Ruby Date: Wed, 3 May 2023 14:42:55 -0400 Subject: [PATCH 1/4] Create a separate node framework scanner --- scanner/nodeFramework.go | 175 +++++++++++++++++++++++++++++++++++++++ scanner/scanner.go | 1 + 2 files changed, 176 insertions(+) create mode 100644 scanner/nodeFramework.go diff --git a/scanner/nodeFramework.go b/scanner/nodeFramework.go new file mode 100644 index 0000000000..b0533a3c73 --- /dev/null +++ b/scanner/nodeFramework.go @@ -0,0 +1,175 @@ +package scanner + +import ( + "encoding/json" + "io/fs" + "os" + "os/exec" + "regexp" + "strconv" + "strings" + + "github.com/pkg/errors" +) + +var packageJson map[string]interface{} + +// Handle node frameworks separate from other node applications. Currently the requirements +// for a framework is pretty low: to have a "start" script. Because we are actually +// going to be running a node application to generate a Dockerfile there is one more +// criteria: the running node version must be at least 16. If there turns out to be +// demand for earlier versions of node, we can adjust this requirement. +func configureNodeFramework(sourceDir string, config *ScannerConfig) (*SourceInfo, error) { + // first ensure that there is a package.json + if !checksPass(sourceDir, fileExists("package.json")) { + return nil, nil + } + + // ensure package.json has a start script + data, err := os.ReadFile("package.json") + + if err != nil { + return nil, nil + } else { + err = json.Unmarshal(data, &packageJson) + if err != nil { + return nil, nil + } + + scripts, ok := packageJson["scripts"].(map[string]interface{}) + + if !ok || scripts["start"] == nil { + return nil, nil + } + } + + // ensure node version is at least 16 + out, err := exec.Command("node", "-v").Output() + if err != nil { + return nil, nil + } else { + nodeVersion := strings.TrimSpace(string(out)) + if nodeVersion[:1] == "v" { + nodeVersion = nodeVersion[1:] + } + if nodeVersion < "16" { + return nil, nil + } + } + + srcInfo := &SourceInfo{ + Family: "NodeJS", + SkipDeploy: true, + Callback: NodeFrameworkCallback, + } + + return srcInfo, nil +} + +func NodeFrameworkCallback(srcInfo *SourceInfo, options map[string]bool) error { + // generate Dockerfile if it doesn't already exist + _, err := os.Stat("Dockerfile") + if errors.Is(err, fs.ErrNotExist) { + var args []string + + _, err = os.Stat("node_modules") + if errors.Is(err, fs.ErrNotExist) { + // no existing node_modules directory: run package directly + args = []string{"npx", "--yes", "@flydotio/dockerfile@latest"} + } else { + // build command to install package using preferred package manager + args = []string{"npm", "install", "@flydotio/dockerfile", "--save-dev"} + + _, err = os.Stat("yarn.lock") + if !errors.Is(err, fs.ErrNotExist) { + args = []string{"yarn", "add", "@flydotio/dockerfile", "--dev"} + } + + _, err = os.Stat("pnpm-lock.yaml") + if !errors.Is(err, fs.ErrNotExist) { + args = []string{"pnpm", "add", "-D", "@flydotio/dockerfile"} + } + } + + // check first to see if the package is already installed + installed := false + + deps, ok := packageJson["dependencies"].(map[string]interface{}) + if ok && deps["@flydotio/dockerfile"] != nil { + installed = true + } + + deps, ok = packageJson["devDependencies"].(map[string]interface{}) + if ok && deps["@flydotio/dockerfile"] != nil { + installed = true + } + + // install/run command + if !installed || args[0] == "npx" { + cmd := exec.Command(args[0], args[1:]...) + cmd.Stdin = nil + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + if err := cmd.Run(); err != nil { + return errors.Wrap(err, "Failed to install @flydotio/dockerfile") + } + } + + // run the package if we haven't already + if args[0] != "npx" { + cmd := exec.Command("npx", "dockerfile") + cmd.Stdin = nil + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + if err := cmd.Run(); err != nil { + return errors.Wrap(err, "Failed to generate Dockerfile") + } + } + } + + // read dockerfile + dockerfile, err := os.ReadFile("Dockerfile") + if err != nil { + return errors.Wrap(err, "Dockerfile not found") + } + + // extract family + family := "NodeJS" + re := regexp.MustCompile(`(?m)^LABEL\s+fly_launch_runtime="(?P.+?)"`) + m := re.FindStringSubmatch(string(dockerfile)) + + for i, name := range re.SubexpNames() { + if len(m) > 0 && name == "family" { + family = m[i] + } + } + srcInfo.Family = family + + // extract port + port := 3000 + re = regexp.MustCompile(`(?m)^EXPOSE\s+(?P\d+)`) + m = re.FindStringSubmatch(string(dockerfile)) + + for i, name := range re.SubexpNames() { + if len(m) > 0 && name == "port" { + port, err = strconv.Atoi(m[i]) + if err != nil { + panic(err) + } + } + } + srcInfo.Port = port + + // provide some advice + srcInfo.DeployDocs += ` +If you need custom packages installed, or have problems with your deployment +build, you may need to edit the Dockerfile for app-specific changes. If you +need help, please post on https://community.fly.io. + +Now: run 'fly deploy' to deploy your Node app. +` + + return nil +} diff --git a/scanner/scanner.go b/scanner/scanner.go index 75f25fc5a7..2f7c8d2d99 100644 --- a/scanner/scanner.go +++ b/scanner/scanner.go @@ -82,6 +82,7 @@ func Scan(sourceDir string, config *ScannerConfig) (*SourceInfo, error) { configurePhoenix, configureRails, configureRedwood, + configureNodeFramework, /* frameworks scanners are placed before generic scanners, since they might mix languages or have a Dockerfile that doesn't work with Fly */ From 94b863fec6b21dbe519ff25ae22d69709c95654a Mon Sep 17 00:00:00 2001 From: Sam Ruby Date: Wed, 3 May 2023 14:50:06 -0400 Subject: [PATCH 2/4] trim trailing whitespace --- scanner/nodeFramework.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scanner/nodeFramework.go b/scanner/nodeFramework.go index b0533a3c73..e81daaa116 100644 --- a/scanner/nodeFramework.go +++ b/scanner/nodeFramework.go @@ -167,7 +167,7 @@ func NodeFrameworkCallback(srcInfo *SourceInfo, options map[string]bool) error { If you need custom packages installed, or have problems with your deployment build, you may need to edit the Dockerfile for app-specific changes. If you need help, please post on https://community.fly.io. - + Now: run 'fly deploy' to deploy your Node app. ` From ec4164f096a2ee72f462cc02189ead04e2df0f05 Mon Sep 17 00:00:00 2001 From: Sam Ruby Date: Wed, 3 May 2023 17:29:30 -0400 Subject: [PATCH 3/4] import error from go instead --- scanner/nodeFramework.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/scanner/nodeFramework.go b/scanner/nodeFramework.go index e81daaa116..aef26fd61a 100644 --- a/scanner/nodeFramework.go +++ b/scanner/nodeFramework.go @@ -2,14 +2,14 @@ package scanner import ( "encoding/json" + "errors" + "fmt" "io/fs" "os" "os/exec" "regexp" "strconv" "strings" - - "github.com/pkg/errors" ) var packageJson map[string]interface{} @@ -112,7 +112,7 @@ func NodeFrameworkCallback(srcInfo *SourceInfo, options map[string]bool) error { cmd.Stderr = os.Stderr if err := cmd.Run(); err != nil { - return errors.Wrap(err, "Failed to install @flydotio/dockerfile") + return fmt.Errorf("failed to install @flydotio/dockerfile: %w", err) } } @@ -124,7 +124,7 @@ func NodeFrameworkCallback(srcInfo *SourceInfo, options map[string]bool) error { cmd.Stderr = os.Stderr if err := cmd.Run(); err != nil { - return errors.Wrap(err, "Failed to generate Dockerfile") + return fmt.Errorf("failed to generate Dockerfile: %w", err) } } } @@ -132,7 +132,7 @@ func NodeFrameworkCallback(srcInfo *SourceInfo, options map[string]bool) error { // read dockerfile dockerfile, err := os.ReadFile("Dockerfile") if err != nil { - return errors.Wrap(err, "Dockerfile not found") + return err } // extract family From d17787c0aa1335f12463ff9ef9ac2d076688d2f8 Mon Sep 17 00:00:00 2001 From: Sam Ruby Date: Thu, 4 May 2023 14:28:35 -0400 Subject: [PATCH 4/4] use semver --- scanner/nodeFramework.go | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/scanner/nodeFramework.go b/scanner/nodeFramework.go index aef26fd61a..af2506b2bd 100644 --- a/scanner/nodeFramework.go +++ b/scanner/nodeFramework.go @@ -10,6 +10,8 @@ import ( "regexp" "strconv" "strings" + + "github.com/blang/semver" ) var packageJson map[string]interface{} @@ -43,16 +45,24 @@ func configureNodeFramework(sourceDir string, config *ScannerConfig) (*SourceInf } } - // ensure node version is at least 16 + // ensure node version is at least 16.0.0 out, err := exec.Command("node", "-v").Output() if err != nil { return nil, nil } else { - nodeVersion := strings.TrimSpace(string(out)) - if nodeVersion[:1] == "v" { - nodeVersion = nodeVersion[1:] + minVersion, err := semver.Make("16.0.0") + if err != nil { + panic(err) + } + + nodeVersionString := strings.TrimSpace(string(out)) + if nodeVersionString[:1] == "v" { + nodeVersionString = nodeVersionString[1:] } - if nodeVersion < "16" { + + nodeVersion, err := semver.Make(nodeVersionString) + + if err != nil || nodeVersion.LT(minVersion) { return nil, nil } }