Skip to content

Commit

Permalink
test: add runtime tests (#5)
Browse files Browse the repository at this point in the history
  • Loading branch information
jaredLunde authored May 15, 2024
1 parent e318fcb commit 4f9b5e4
Show file tree
Hide file tree
Showing 88 changed files with 1,480 additions and 65 deletions.
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
node_modules
dist/
/dist/
/Dockerfile
27 changes: 15 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,13 @@ related to this tool.

- [x] Automatically detect the runtime and framework used by your project
- [x] Use version managers like [asdf](https://github.com/asdf-vm), nvm, rbenv, and pyenv to install the correct version of the runtime
- [x] Make a best effort to detect any install, build, and run commands
- [x] Make a best effort to detect any install, build, and start commands
- [x] Generate a Dockerfile with sensible defaults that are configurable via [Docker Build Args](https://docs.docker.com/build/guide/build-args/)
- [x] Support for a wide range of the most popular languages and frameworks including Next.js, Phoenix, Spring Boot, Django, and more
- [x] Use Debian Slim as the runtime image for a smaller image size and better security, while still supporting the most common dependencies and avoiding deployment headaches caused by Alpine Linux gotchas
- [x] Includes `wget` in the runtime image for adding health checks to services, e.g. `wget -nv -t1 --spider 'http://localhost:8080/healthz' || exit 1`
- [x] Use multi-stage builds to reduce the size of the final image
- [x] Run the application as a non-root user for better security
- [x] Supports multi-platform images that run on both x86 and ARM CPU architectures

## Supported Runtimes
Expand Down Expand Up @@ -105,8 +106,8 @@ For example, if it finds a `package.json` file, it will assume the project is a
a `next.config.js` file is present, in which case it will assume the project is a Next.js project.

From there, it will read any `.tool-versions` or other version manager files to determine the correct version
of the runtime to install. It will then make a best effort to detect any install, build, and run commands.
For example, a `serve`, `start`, `start:prod` command in a `package.json` file will be used as the run command.
of the runtime to install. It will then make a best effort to detect any install, build, and start commands.
For example, a `serve`, `start`, `start:prod` command in a `package.json` file will be used as the start command.

Runtimes are matched against in the order they appear when you run `new-dockerfile --runtime list`.

Expand Down Expand Up @@ -251,15 +252,11 @@ Detected in order of precedence:
[Java](https://www.java.com/) is a class-based, object-oriented programming language that is designed to have as few implementation dependencies as possible.

#### Detected Files
- `build.gradle`
- `gradlew`
- `pom.{xml,atom,clj,groovy,rb,scala,yml,yaml}`

#### Version Detection
JDK version:
- `.tool-versions` - `java {VERSION}`
Gradle version:
- `.tool-versions` - `gradle {VERSION}`
Maven version:
- `.tool-versions` - `maven {VERSION}`

Expand All @@ -268,7 +265,6 @@ Maven version:

#### Build Args
- `VERSION` - The version of the JDK to install (default: `17`)
- `GRADLE_VERSION` - The version of Gradle to install (default: `8`)
- `MAVEN_VERSION` - The version of Maven to install (default: `3`)
- `JAVA_OPTS` - The Java options to pass to the JVM (default: `-Xmx512m -Xms256m`)
- `BUILD_CMD` - The command to build the project (default: best guess via source code)
Expand All @@ -279,13 +275,10 @@ Maven version:

#### Build Command
- If Maven: `mvn -DoutputFile=target/mvn-dependency-list.log -B -DskipTests clean dependency:list install`
- If Gradle: `./gradlew clean build -x check -x test`

#### Start Command
- Default: `java $JAVA_OPTS -jar target/*jar`
- If Gradle: `java $JAVA_OPTS -jar $(ls -1 build/libs/*jar | grep -v plain)`
- If Spring Boot: `java -Dserver.port=${PORT} $JAVA_OPTS -jar target/*jar`
- If Spring Boot w/ Gradle: `java -Dserver.port=${PORT} $JAVA_OPTS -jar $(ls -1 build/libs/*jar | grep -v plain)`

---

Expand All @@ -300,6 +293,8 @@ Maven version:

#### Version Detection
- `.tool-versions` - `nodejs {VERSION}`
- `.nvmrc` - `v{VERSION}`
- `.node-version` - `v{VERSION}`

#### Runtime Image
`node:${VERSION}-slim`
Expand Down Expand Up @@ -343,6 +338,8 @@ fi

#### Version Detection
- `.tool-versions` - `nodejs {VERSION}`
- `.nvmrc` - `v{VERSION}`
- `.node-version` - `v{VERSION}`

#### Runtime Image
`node:${VERSION}-slim`
Expand All @@ -365,7 +362,7 @@ In order of precedence:
#### Start Command
In order of precedence:
- `package.json` scripts: `"serve", "start:prod", "start:production", "start-prod", "start-production", "start"`
- `package.json` scripts search for regex matching: `^.*?\bnode(mon)?\b.*?(index|main|server|client)\.js\b`
- `package.json` scripts search for regex matching: `^.*?\b(ts-)?node(mon)?\b.*?(index|main|server|client)\.([cm]?[tj]s)\b`
- `package.json` main/module file: `node ${mainFile}`

---
Expand Down Expand Up @@ -504,6 +501,12 @@ In order of precedence:
- `TARGETARCH` - The target architecture for the build (default: `amd64`)
- `BIN_NAME` - The name of the release binary (default: detected via `Cargo.toml`)

#### Build Command
```sh
if [ "${TARGETARCH}" = "amd64" ]; then rustup target add x86_64-unknown-linux-gnu; else rustup target add aarch64-unknown-linux-gnu; fi
if [ "${TARGETARCH}" = "amd64" ]; then cargo zigbuild --release --target x86_64-unknown-linux-gnu; else cargo zigbuild --release --target aarch64-unknown-linux-gnu; fi
```

#### Start Command
Determined by the binary name in the `Cargo.toml` file
- `["/app/app"]`
Expand Down
5 changes: 4 additions & 1 deletion main.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// A library for auto-generating Dockerfiles from project source code.
package dockerfile

import (
Expand All @@ -9,7 +10,7 @@ import (
"github.com/flexstack/new-dockerfile/runtime"
)

// Creates a new Dockerfile generator.
// Creates a new Dockerfile generator. If no logger is provided, a default logger is created.
func New(log ...*slog.Logger) *Dockerfile {
var logger *slog.Logger

Expand Down Expand Up @@ -50,6 +51,7 @@ func (a *Dockerfile) Write(path string) error {
return nil
}

// Lists all runtimes that the Dockerfile generator can auto-generate.
func (a *Dockerfile) ListRuntimes() []runtime.Runtime {
return []runtime.Runtime{
&runtime.Golang{Log: a.log},
Expand All @@ -67,6 +69,7 @@ func (a *Dockerfile) ListRuntimes() []runtime.Runtime {
}
}

// Matches the runtime of the project at the given path.
func (a *Dockerfile) MatchRuntime(path string) (runtime.Runtime, error) {
for _, r := range a.ListRuntimes() {
if r.Match(path) {
Expand Down
1 change: 0 additions & 1 deletion node/.tool-versions

This file was deleted.

7 changes: 4 additions & 3 deletions node/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,13 @@ related to this tool.

- [x] Automatically detect the runtime and framework used by your project
- [x] Use version managers like [asdf](https://github.com/asdf-vm), nvm, rbenv, and pyenv to install the correct version of the runtime
- [x] Make a best effort to detect any install, build, and run commands
- [x] Make a best effort to detect any install, build, and start commands
- [x] Generate a Dockerfile with sensible defaults that are configurable via [Docker Build Args](https://docs.docker.com/build/guide/build-args/)
- [x] Support for a wide range of the most popular languages and frameworks including Next.js, Phoenix, Spring Boot, Django, and more
- [x] Use Debian Slim as the runtime image for a smaller image size and better security, while still supporting the most common dependencies and avoiding deployment headaches caused by Alpine Linux gotchas
- [x] Includes `wget` in the runtime image for adding health checks to services, e.g. `wget -nv -t1 --spider 'http://localhost:8080/healthz' || exit 1`
- [x] Use multi-stage builds to reduce the size of the final image
- [x] Run the application as a non-root user for better security
- [x] Supports multi-platform images that run on both x86 and ARM CPU architectures

## Supported Runtimes
Expand Down Expand Up @@ -93,8 +94,8 @@ For example, if it finds a `package.json` file, it will assume the project is a
a `next.config.js` file is present, in which case it will assume the project is a Next.js project.

From there, it will read any `.tool-versions` or other version manager files to determine the correct version
of the runtime to install. It will then make a best effort to detect any install, build, and run commands.
For example, a `serve`, `start`, `start:prod` command in a `package.json` file will be used as the run command.
of the runtime to install. It will then make a best effort to detect any install, build, and start commands.
For example, a `serve`, `start`, `start:prod` command in a `package.json` file will be used as the start command.

Runtimes are matched against in the order they appear when you run `new-dockerfile --runtime list`.

Expand Down
5 changes: 5 additions & 0 deletions runtime/bun.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,11 @@ func (d *Bun) GenerateDockerfile(path string) ([]byte, error) {
startCMD = string(startCMDJSON)
}

if buildCMD != "" {
buildCMDJSON, _ := json.Marshal(buildCMD)
buildCMD = string(buildCMDJSON)
}

var buf bytes.Buffer
if err := tmpl.Option("missingkey=zero").Execute(&buf, map[string]string{
"Version": *version,
Expand Down
100 changes: 100 additions & 0 deletions runtime/bun_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
package runtime_test

import (
"regexp"
"strings"
"testing"

"github.com/flexstack/new-dockerfile/runtime"
)

func TestBunMatch(t *testing.T) {
tests := []struct {
name string
path string
expected bool
}{
{
name: "Bun project",
path: "../testdata/bun",
expected: true,
},
{
name: "Bun project with .ts file",
path: "../testdata/bun-bunfig",
expected: true,
},
{
name: "Not a Bun project",
path: "../testdata/deno",
expected: false,
},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
bun := &runtime.Bun{Log: logger}
if bun.Match(test.path) != test.expected {
t.Errorf("expected %v, got %v", test.expected, bun.Match(test.path))
}
})
}
}

func TestBunGenerateDockerfile(t *testing.T) {
tests := []struct {
name string
path string
expected []any
}{
{
name: "Bun project",
path: "../testdata/bun",
expected: []any{`ARG VERSION=1`, `ARG INSTALL_CMD="bun install"`, regexp.MustCompile(`^ARG BUILD_CMD=$`), `ARG START_CMD="bun index.ts"`},
},
{
name: "Bun project with .ts file",
path: "../testdata/bun-bunfig",
expected: []any{`ARG VERSION=1.1.4`, `ARG INSTALL_CMD="bun install"`, `ARG BUILD_CMD="bun run build:prod"`, `ARG START_CMD="bun run start:production"`},
},
{
name: "Not a Bun project",
path: "../testdata/deno",
expected: []any{`ARG VERSION=1`, regexp.MustCompile(`^ARG INSTALL_CMD="bun install"`), regexp.MustCompile(`^ARG BUILD_CMD=$`), regexp.MustCompile(`^ARG START_CMD=$`)},
},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
bun := &runtime.Bun{Log: logger}
dockerfile, err := bun.GenerateDockerfile(test.path)
if err != nil {
t.Errorf("unexpected error: %v", err)
}

for _, line := range test.expected {
found := false
lines := strings.Split(string(dockerfile), "\n")

for _, l := range lines {
switch v := line.(type) {
case string:
if strings.Contains(l, v) {
found = true
break
}
case *regexp.Regexp:
if v.MatchString(l) {
found = true
break
}
}
}

if !found {
t.Errorf("expected %v, not found in %v", line, string(dockerfile))
}
}
})
}
}
4 changes: 2 additions & 2 deletions runtime/deno_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import (
"github.com/flexstack/new-dockerfile/runtime"
)

func TestRuntimeMatch(t *testing.T) {
func TestDenoMatch(t *testing.T) {
tests := []struct {
name string
path string
Expand Down Expand Up @@ -41,7 +41,7 @@ func TestRuntimeMatch(t *testing.T) {
}
}

func TestRuntimeGenerateDockerfile(t *testing.T) {
func TestDenoGenerateDockerfile(t *testing.T) {
tests := []struct {
name string
path string
Expand Down
4 changes: 4 additions & 0 deletions runtime/elixir.go
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,10 @@ func isPhoenixProject(path string) bool {
}

func findBinName(path string) (string, error) {
if _, err := os.Stat(filepath.Join(path, "mix.exs")); err != nil {
return "", nil
}

configFile, err := os.Open(filepath.Join(path, "mix.exs"))
if err != nil {
return "", err
Expand Down
Loading

0 comments on commit 4f9b5e4

Please sign in to comment.