Skip to content

Commit d886a8f

Browse files
authored
feat: add special case for python fastapi (#24)
1 parent fbd43d3 commit d886a8f

25 files changed

+407
-92
lines changed

README.md

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -420,6 +420,7 @@ In order of precedence:
420420

421421
#### Detected Files
422422
- `requirements.txt`
423+
- `uv.lock`
423424
- `poetry.lock`
424425
- `Pipefile.lock`
425426
- `pyproject.toml`
@@ -447,14 +448,16 @@ In order of precedence:
447448
- `START_CMD` - The command to start the project (default: detected from source code)
448449

449450
#### Install Command
450-
- If Poetry: `poetry install --no-dev --no-interactive --no-ansi`
451-
- If Pipenv: `PIPENV_VENV_IN_PROJECT=1 pipenv install --deploy`
452-
- If PDM: `pdm install --prod`
451+
- If Poetry: `pip install poetry && poetry install --no-dev --no-ansi --no-root`
452+
- If Pipenv: `pipenv install --dev --system --deploy`
453+
- If uv: `pip install uv && uv sync --python-preference=only-system --no-cache --no-dev`
454+
- If PDM: `pip install pdm && pdm install --prod`
453455
- If `pyproject.toml` exists: `pip install --upgrade build setuptools && pip install .`
454456
- If `requirements.txt` exists: `pip install -r requirements.txt`
455457

456458
#### Start Command
457459
- If Django is detected: `python manage.py runserver 0.0.0.0:${PORT}`
460+
- If FastAPI is detected: `fastapi run [main.py, app.py, application.py, app/main.py, app/application.py, app/__init__.py] --port ${PORT}`
458461
- If `pyproject.toml` exists: `python -m ${projectName}`
459462
- Otherwise: `python [main.py, app.py, application.py, app/main.py, app/application.py, app/__init__.py]`
460463

runtime/bun.go

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"encoding/json"
77
"fmt"
88
"log/slog"
9+
"maps"
910
"os"
1011
"path/filepath"
1112
"strings"
@@ -37,7 +38,7 @@ func (d *Bun) Match(path string) bool {
3738
return false
3839
}
3940

40-
func (d *Bun) GenerateDockerfile(path string) ([]byte, error) {
41+
func (d *Bun) GenerateDockerfile(path string, data ...map[string]string) ([]byte, error) {
4142
tmpl, err := template.New("Dockerfile").Parse(bunTemplate)
4243
if err != nil {
4344
return nil, fmt.Errorf("Failed to parse template")
@@ -125,11 +126,15 @@ func (d *Bun) GenerateDockerfile(path string) ([]byte, error) {
125126
}
126127

127128
var buf bytes.Buffer
128-
if err := tmpl.Option("missingkey=zero").Execute(&buf, map[string]string{
129+
templateData := map[string]string{
129130
"Version": *version,
130131
"BuildCMD": buildCMD,
131132
"StartCMD": startCMD,
132-
}); err != nil {
133+
}
134+
if len(data) > 0 {
135+
maps.Copy(templateData, data[0])
136+
}
137+
if err := tmpl.Option("missingkey=zero").Execute(&buf, templateData); err != nil {
133138
return nil, fmt.Errorf("Failed to execute template")
134139
}
135140

@@ -145,15 +150,15 @@ FROM base AS deps
145150
WORKDIR /app
146151
COPY package.json bun.lockb ./
147152
ARG INSTALL_CMD="bun install"
148-
RUN if [ ! -z "${INSTALL_CMD}" ]; then sh -c "$INSTALL_CMD"; fi
153+
RUN {{.InstallMounts}}if [ ! -z "${INSTALL_CMD}" ]; then sh -c "$INSTALL_CMD"; fi
149154
150155
FROM base AS builder
151156
WORKDIR /app
152157
COPY --from=deps /app/node_modules* ./node_modules
153158
COPY . .
154159
ENV NODE_ENV=production
155160
ARG BUILD_CMD={{.BuildCMD}}
156-
RUN if [ ! -z "${BUILD_CMD}" ]; then sh -c "$BUILD_CMD"; fi
161+
RUN {{.BuildMounts}}if [ ! -z "${BUILD_CMD}" ]; then sh -c "$BUILD_CMD"; fi
157162
158163
FROM ${BUILDER}:${VERSION}-slim AS runtime
159164
WORKDIR /app
@@ -168,6 +173,7 @@ COPY --chown=nonroot:nonroot --from=builder /app .
168173
USER nonroot:nonroot
169174
170175
ENV PORT=8080
176+
EXPOSE ${PORT}
171177
ENV NODE_ENV=production
172178
ARG START_CMD={{.StartCMD}}
173179
ENV START_CMD=${START_CMD}

runtime/bun_test.go

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ func TestBunGenerateDockerfile(t *testing.T) {
4545
tests := []struct {
4646
name string
4747
path string
48+
data map[string]string
4849
expected []any
4950
}{
5051
{
@@ -57,6 +58,20 @@ func TestBunGenerateDockerfile(t *testing.T) {
5758
path: "../testdata/bun-bunfig",
5859
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"`},
5960
},
61+
{
62+
name: "Bun project with build mounts",
63+
path: "../testdata/bun-bunfig",
64+
data: map[string]string{"BuildMounts": `--mount=type=secret,id=_env,target=/app/.env \
65+
`},
66+
expected: []any{regexp.MustCompile(`^RUN --mount=type=secret,id=_env,target=/app/.env \\$`)},
67+
},
68+
{
69+
name: "Bun project with install mounts",
70+
path: "../testdata/bun-bunfig",
71+
data: map[string]string{"InstallMounts": `--mount=type=secret,id=_env,target=/app/.env \
72+
`},
73+
expected: []any{regexp.MustCompile(`^RUN --mount=type=secret,id=_env,target=/app/.env \\$`)},
74+
},
6075
{
6176
name: "Not a Bun project",
6277
path: "../testdata/deno",
@@ -67,7 +82,7 @@ func TestBunGenerateDockerfile(t *testing.T) {
6782
for _, test := range tests {
6883
t.Run(test.name, func(t *testing.T) {
6984
bun := &runtime.Bun{Log: logger}
70-
dockerfile, err := bun.GenerateDockerfile(test.path)
85+
dockerfile, err := bun.GenerateDockerfile(test.path, test.data)
7186
if err != nil {
7287
t.Errorf("unexpected error: %v", err)
7388
}

runtime/deno.go

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"fmt"
88
"io/fs"
99
"log/slog"
10+
"maps"
1011
"os"
1112
"path/filepath"
1213
"strings"
@@ -70,7 +71,7 @@ func (d *Deno) Match(path string) bool {
7071
return detected
7172
}
7273

73-
func (d *Deno) GenerateDockerfile(path string) ([]byte, error) {
74+
func (d *Deno) GenerateDockerfile(path string, data ...map[string]string) ([]byte, error) {
7475
tmpl, err := template.New("Dockerfile").Parse(denoTemplate)
7576
if err != nil {
7677
return nil, fmt.Errorf("Failed to parse template")
@@ -156,11 +157,15 @@ func (d *Deno) GenerateDockerfile(path string) ([]byte, error) {
156157
}
157158

158159
var buf bytes.Buffer
159-
if err := tmpl.Option("missingkey=zero").Execute(&buf, map[string]string{
160+
templateData := map[string]string{
160161
"Version": *version,
161162
"InstallCMD": installCMD,
162163
"StartCMD": startCMD,
163-
}); err != nil {
164+
}
165+
if len(data) > 0 {
166+
maps.Copy(templateData, data[0])
167+
}
168+
if err := tmpl.Option("missingkey=zero").Execute(&buf, templateData); err != nil {
164169
return nil, fmt.Errorf("Failed to execute template")
165170
}
166171

@@ -192,7 +197,7 @@ USER nonroot:nonroot
192197
ENV PORT=8080
193198
EXPOSE ${PORT}
194199
ARG INSTALL_CMD={{.InstallCMD}}
195-
RUN if [ ! -z "${INSTALL_CMD}" ]; then sh -c "$INSTALL_CMD"; fi
200+
RUN {{.InstallMounts}}if [ ! -z "${INSTALL_CMD}" ]; then sh -c "$INSTALL_CMD"; fi
196201
197202
ARG START_CMD={{.StartCMD}}
198203
ENV START_CMD=${START_CMD}

runtime/deno_test.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ func TestDenoGenerateDockerfile(t *testing.T) {
4545
tests := []struct {
4646
name string
4747
path string
48+
data map[string]string
4849
expected []any
4950
}{
5051
{
@@ -57,6 +58,13 @@ func TestDenoGenerateDockerfile(t *testing.T) {
5758
path: "../testdata/deno-jsonc",
5859
expected: []any{`ARG VERSION=1.43.3`, `ARG INSTALL_CMD="deno task cache"`, `ARG START_CMD="deno task start"`},
5960
},
61+
{
62+
name: "Deno project with install mounts",
63+
path: "../testdata/deno-jsonc",
64+
data: map[string]string{"InstallMounts": `--mount=type=secret,id=_env,target=/app/.env \
65+
`},
66+
expected: []any{regexp.MustCompile(`^RUN --mount=type=secret,id=_env,target=/app/.env \\$`)},
67+
},
6068
{
6169
name: "Not a Deno project",
6270
path: "../testdata/ruby",
@@ -67,7 +75,7 @@ func TestDenoGenerateDockerfile(t *testing.T) {
6775
for _, test := range tests {
6876
t.Run(test.name, func(t *testing.T) {
6977
deno := &runtime.Deno{Log: logger}
70-
dockerfile, err := deno.GenerateDockerfile(test.path)
78+
dockerfile, err := deno.GenerateDockerfile(test.path, test.data)
7179
if err != nil {
7280
t.Errorf("unexpected error: %v", err)
7381
}

runtime/elixir.go

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"bytes"
66
"fmt"
77
"log/slog"
8+
"maps"
89
"os"
910
"path/filepath"
1011
"strings"
@@ -35,7 +36,7 @@ func (d *Elixir) Match(path string) bool {
3536
return false
3637
}
3738

38-
func (d *Elixir) GenerateDockerfile(path string) ([]byte, error) {
39+
func (d *Elixir) GenerateDockerfile(path string, data ...map[string]string) ([]byte, error) {
3940
tmpl, err := template.New("Dockerfile").Parse(elixirTemplate)
4041
if err != nil {
4142
return nil, fmt.Errorf("Failed to parse template")
@@ -68,11 +69,15 @@ func (d *Elixir) GenerateDockerfile(path string) ([]byte, error) {
6869
)
6970

7071
var buf bytes.Buffer
71-
if err := tmpl.Option("missingkey=zero").Execute(&buf, map[string]string{
72+
templateData := map[string]string{
7273
"ElixirVersion": *elixirVersion,
7374
"OTPVersion": strings.Split(*otpVersion, ".")[0],
7475
"BinName": binName,
75-
}); err != nil {
76+
}
77+
if len(data) > 0 {
78+
maps.Copy(templateData, data[0])
79+
}
80+
if err := tmpl.Option("missingkey=zero").Execute(&buf, templateData); err != nil {
7681
return nil, fmt.Errorf("Failed to execute template")
7782
}
7883

@@ -129,6 +134,7 @@ COPY --from=build --chown=nonroot:nonroot /app/_build/${MIX_ENV}/rel/${BIN_NAME}
129134
RUN cp /app/bin/${BIN_NAME} /app/bin/server
130135
131136
ENV PORT=8080
137+
EXPOSE ${PORT}
132138
USER nonroot:nonroot
133139
134140
CMD ["/app/bin/server", "start"]

runtime/golang.go

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"bytes"
66
"fmt"
77
"log/slog"
8+
"maps"
89
"os"
910
"path/filepath"
1011
"strings"
@@ -36,7 +37,7 @@ func (d *Golang) Match(path string) bool {
3637
return false
3738
}
3839

39-
func (d *Golang) GenerateDockerfile(path string) ([]byte, error) {
40+
func (d *Golang) GenerateDockerfile(path string, data ...map[string]string) ([]byte, error) {
4041
tmpl, err := template.New("Dockerfile").Parse(golangTemplate)
4142
if err != nil {
4243
return nil, fmt.Errorf("Failed to parse template")
@@ -84,10 +85,14 @@ func (d *Golang) GenerateDockerfile(path string) ([]byte, error) {
8485

8586
d.Log.Info("Using package: " + pkg)
8687
var buf bytes.Buffer
87-
if err := tmpl.Option("missingkey=zero").Execute(&buf, map[string]string{
88+
templateData := map[string]string{
8889
"Version": *version,
8990
"Package": pkg,
90-
}); err != nil {
91+
}
92+
if len(data) > 0 {
93+
maps.Copy(templateData, data[0])
94+
}
95+
if err := tmpl.Option("missingkey=zero").Execute(&buf, templateData); err != nil {
9196
return nil, fmt.Errorf("Failed to execute template")
9297
}
9398

runtime/java.go

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"encoding/json"
77
"fmt"
88
"log/slog"
9+
"maps"
910
"os"
1011
"path/filepath"
1112
"regexp"
@@ -46,7 +47,7 @@ func (d *Java) Match(path string) bool {
4647
return false
4748
}
4849

49-
func (d *Java) GenerateDockerfile(path string) ([]byte, error) {
50+
func (d *Java) GenerateDockerfile(path string, data ...map[string]string) ([]byte, error) {
5051
version, err := findJDKVersion(path, d.Log)
5152
if err != nil {
5253
return nil, err
@@ -124,14 +125,17 @@ func (d *Java) GenerateDockerfile(path string) ([]byte, error) {
124125
startCMDJSON, _ := json.Marshal(startCMD)
125126
startCMD = string(startCMDJSON)
126127
}
127-
128-
if err := tmpl.Option("missingkey=zero").Execute(&buf, map[string]string{
128+
templateData := map[string]string{
129129
"Version": *version,
130130
"GradleVersion": gradleVersion,
131131
"MavenVersion": mavenVersion,
132132
"BuildCMD": buildCMD,
133133
"StartCMD": startCMD,
134-
}); err != nil {
134+
}
135+
if len(data) > 0 {
136+
maps.Copy(templateData, data[0])
137+
}
138+
if err := tmpl.Option("missingkey=zero").Execute(&buf, templateData); err != nil {
135139
return nil, fmt.Errorf("Failed to execute template")
136140
}
137141

@@ -165,6 +169,7 @@ RUN chown -R nonroot:nonroot /app
165169
COPY --from=build --chown=nonroot:nonroot /app/target/*.jar /app/target/
166170
167171
ENV PORT=8080
172+
EXPOSE ${PORT}
168173
USER nonroot:nonroot
169174
170175
ARG JAVA_OPTS=
@@ -200,6 +205,7 @@ RUN chown -R nonroot:nonroot /app
200205
COPY --from=build --chown=nonroot:nonroot /app/build/libs/*.jar /app/build/libs/
201206
202207
ENV PORT=8080
208+
EXPOSE ${PORT}
203209
USER nonroot:nonroot
204210
205211
ARG JAVA_OPTS=

runtime/main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ type Runtime interface {
77
// Returns true if the runtime can be used for the given path.
88
Match(path string) bool
99
// Generates a Dockerfile for the given path.
10-
GenerateDockerfile(path string) ([]byte, error)
10+
GenerateDockerfile(path string, data ...map[string]string) ([]byte, error)
1111
}
1212

1313
type RuntimeName string

0 commit comments

Comments
 (0)