Skip to content

Commit

Permalink
Support setting Python version when generating Dockerfile (#152)
Browse files Browse the repository at this point in the history
  • Loading branch information
baksetercx authored Dec 27, 2024
1 parent f361836 commit de6e985
Show file tree
Hide file tree
Showing 18 changed files with 309 additions and 109 deletions.
5 changes: 1 addition & 4 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,9 +65,6 @@ func main() {

ctx := context.Background()
if err := app.Run(ctx, os.Args); err != nil {
style.Print(
fmt.Sprintf("\n\nERROR: %s", err),
&style.PrintOptions{Color: "red"},
)
style.PrintError(fmt.Sprintf("\n\nERROR: %s", err))
}
}
4 changes: 2 additions & 2 deletions pkg/build/Dockerfile.python.tmpl
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM ghcr.io/astral-sh/uv:python3.12-bookworm-slim AS build
FROM ghcr.io/astral-sh/uv:python{{ .PythonVersion }}-bookworm-slim AS build
LABEL maintainer="[email protected]"

ENV UV_COMPILE_BYTECODE=1 UV_LINK_MODE=copy
Expand All @@ -16,7 +16,7 @@ RUN --mount=type=cache,target=/root/.cache/uv \
uv sync --frozen --no-dev


FROM python:3.12-slim-bookworm
FROM python:{{ .PythonVersion }}-slim-bookworm
LABEL maintainer="[email protected]"

RUN addgroup application-group --gid 1001 && \
Expand Down
39 changes: 39 additions & 0 deletions pkg/build/_test/Dockerfile.python.test1
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
FROM ghcr.io/astral-sh/uv:python3.13-bookworm-slim AS build
LABEL maintainer="[email protected]"

ENV UV_COMPILE_BYTECODE=1 UV_LINK_MODE=copy

WORKDIR /app

RUN --mount=type=cache,target=/root/.cache/uv \
--mount=type=bind,source=uv.lock,target=uv.lock \
--mount=type=bind,source=pyproject.toml,target=pyproject.toml \
uv sync --frozen --no-install-project --no-dev

ADD . /app

RUN --mount=type=cache,target=/root/.cache/uv \
uv sync --frozen --no-dev


FROM python:3.13-slim-bookworm
LABEL maintainer="[email protected]"

RUN addgroup application-group --gid 1001 && \
adduser application-user --uid 1001 \
--ingroup application-group \
--disabled-password

WORKDIR /app

COPY --from=build /app .

ENV PATH="/app/.venv/bin:$PATH"

RUN chown --recursive application-user .
USER application-user

EXPOSE 8080

CMD ["fastapi", "run", "--host", "0.0.0.0", "--port", "8080", "/app/app/main.py"]

File renamed without changes.
39 changes: 39 additions & 0 deletions pkg/build/_test/Dockerfile.python.test3
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
FROM ghcr.io/astral-sh/uv:python3.10-bookworm-slim AS build
LABEL maintainer="[email protected]"

ENV UV_COMPILE_BYTECODE=1 UV_LINK_MODE=copy

WORKDIR /app

RUN --mount=type=cache,target=/root/.cache/uv \
--mount=type=bind,source=uv.lock,target=uv.lock \
--mount=type=bind,source=pyproject.toml,target=pyproject.toml \
uv sync --frozen --no-install-project --no-dev

ADD . /app

RUN --mount=type=cache,target=/root/.cache/uv \
uv sync --frozen --no-dev


FROM python:3.10-slim-bookworm
LABEL maintainer="[email protected]"

RUN addgroup application-group --gid 1001 && \
adduser application-user --uid 1001 \
--ingroup application-group \
--disabled-password

WORKDIR /app

COPY --from=build /app .

ENV PATH="/app/.venv/bin:$PATH"

RUN chown --recursive application-user .
USER application-user

EXPOSE 8080

CMD ["fastapi", "run", "--host", "0.0.0.0", "--port", "8080", "/app/app/main.py"]

8 changes: 3 additions & 5 deletions pkg/build/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -129,9 +129,8 @@ func Build(_ context.Context, c *cli.Command) error {
possibleSystemName := c.String("system-name")

if possibleSystemName == "" {
style.Print(
style.PrintInfo(
"System name not provided, will try to use the current git repository name.",
nil,
)

repositoryName, err := utils.ResolveRepositoryName("")
Expand Down Expand Up @@ -163,9 +162,8 @@ func Build(_ context.Context, c *cli.Command) error {
}

if c.Bool("generate-only") {
style.Print(
style.PrintSuccess(
fmt.Sprintf("Dockerfile generated at %s\n", dockerfilePath),
nil,
)

return nil
Expand All @@ -178,7 +176,7 @@ func Build(_ context.Context, c *cli.Command) error {
skipAuthentication := c.Bool("skip-authentication") || !push

if strings.Contains(registry, "azurecr.io") && !skipAuthentication {
style.Print("Azure registry detected, will try to authenticate with Azure.", nil)
style.PrintInfo("Azure registry detected, will try to authenticate with Azure.")

azureTenantID := utils.StringWithDefault(
c.String("azure-tenant-id"),
Expand Down
16 changes: 8 additions & 8 deletions pkg/build/generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,14 +49,6 @@ func generateDockerfile(
}

return dockerfile, buildContext, nil
} else if strings.HasPrefix(projectFile, "Dockerfile") ||
strings.HasSuffix(projectFile, "Dockerfile") ||
strings.Contains(projectFile, "Dockerfile") {
if options.BuildContext == "" {
return projectFile, path.Dir(projectFile), nil
}

return projectFile, options.BuildContext, nil
} else if strings.HasSuffix(projectFile, "uv.lock") {
dockerfile, buildContext, err := generateDockerfileForPython(
projectFile,
Expand All @@ -68,6 +60,14 @@ func generateDockerfile(
}

return dockerfile, buildContext, nil
} else if strings.HasPrefix(projectFile, "Dockerfile") ||
strings.HasSuffix(projectFile, "Dockerfile") ||
strings.Contains(projectFile, "Dockerfile") {
if options.BuildContext == "" {
return projectFile, path.Dir(projectFile), nil
}

return projectFile, options.BuildContext, nil
}

return "", "", fmt.Errorf(
Expand Down
98 changes: 96 additions & 2 deletions pkg/build/generate_python.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,20 @@
package build

import (
"fmt"
"io"
"os"
"path"
"slices"
"strings"

"github.com/3lvia/cli/pkg/style"
"github.com/3lvia/cli/pkg/utils"
)

type DockerfileVariablesPython struct{}
type DockerfileVariablesPython struct {
PythonVersion string
}

func generateDockerfileForPython(
projectFile string,
Expand All @@ -16,7 +26,14 @@ func generateDockerfileForPython(
options.BuildContext,
)

dockerfileVariables := DockerfileVariablesPython{}
pythonVersion := getPythonVersion(
path.Dir(projectFile),
buildContext,
)

dockerfileVariables := DockerfileVariablesPython{
PythonVersion: pythonVersion,
}

const templateFile = "Dockerfile.python.tmpl"

Expand All @@ -33,3 +50,80 @@ func generateDockerfileForPython(

return dockerfilePath, buildContext, nil
}

func getPythonVersion(directories ...string) string {
const defaultPythonVersion = "3.13"

// removes duplicates
slices.Sort(directories)
directories = slices.Compact(directories)

style.PrintInfo(
fmt.Sprintf(
"Looking for .python-version file in directories: '%v'.",
strings.Join(directories, ", "),
),
)

for _, directory := range directories {
versionFile := path.Join(directory, ".python-version")

if _, err := os.Stat(versionFile); os.IsNotExist(err) {
style.PrintWarning(
fmt.Sprintf(
"No .python-version file found in '%s', will try next directory.\n",
directory,
),
)

continue
}

file, err := os.Open(versionFile)
if err != nil {
style.PrintWarning(
fmt.Sprintf(
"Failed to open .python-version file in '%s', will try next directory.\n",
directory,
),
)

continue
}

defer file.Close()

contents, err := io.ReadAll(file)
if err != nil {
style.PrintWarning(
fmt.Sprintf(
"Failed to read .python-version file in '%s', will try next directory.\n",
directory,
),
)

continue
}

pythonVersion := strings.TrimSpace(string(contents))

style.PrintInfo(
fmt.Sprintf(
"Found .python-version file in '%s' with version %s.\n",
directory,
pythonVersion,
),
)

return pythonVersion
}

style.PrintWarning(
fmt.Sprintf(
"Did not find any .python-version files, using default version %s.",
defaultPythonVersion,
),
)

return defaultPythonVersion
}
53 changes: 51 additions & 2 deletions pkg/build/generate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package build
import (
"os"
"path/filepath"
"strconv"
"testing"
)

Expand Down Expand Up @@ -296,10 +297,10 @@ func TestGenerateGoDockerfile(t *testing.T) {
}
}

func TestGeneratePythonDockerfile(t *testing.T) {
func TestGeneratePythonDockerfile1(t *testing.T) {
t.Parallel()

expectedDockerfile, err := os.ReadFile("_test/Dockerfile.python.test")
expectedDockerfile, err := os.ReadFile("_test/Dockerfile.python.test1")
if err != nil {
t.Errorf("Error reading file: %v", err)
}
Expand Down Expand Up @@ -334,6 +335,54 @@ func TestGeneratePythonDockerfile(t *testing.T) {
}
}

func TestGeneratePythonDockerfile2(t *testing.T) {
t.Parallel()

for i, version := range []string{"3.12", "3.10"} {
expectedDockerfile, err := os.ReadFile("_test/Dockerfile.python.test" + strconv.FormatInt(int64(i+2), 10))
if err != nil {
t.Errorf("Error reading file: %v", err)
}

tempDir := t.TempDir()
projectFile := filepath.Join(tempDir, "uv.lock")
expectedBuildContext := tempDir

const applicationName = "demo-api-python"

file, err := os.Create(filepath.Join(tempDir, ".python-version"))
if err != nil {
t.Errorf("Error creating file: %v", err)
}

if _, err := file.WriteString(version + "\n"); err != nil {
t.Errorf("Error writing to file: %v", err)
}

actualDockerfilePath, actualBuildContext, err := generateDockerfile(
projectFile,
applicationName,
GenerateDockerfileOptions{},
)
if err != nil {
t.Errorf("Error generating Dockerfile: %v", err)
}

actualDockerfile, err := os.ReadFile(actualDockerfilePath)
if err != nil {
t.Errorf("Error reading file: %v", err)
}

if string(expectedDockerfile) != string(actualDockerfile) {
t.Errorf("Dockerfile mismatch: expected %s, got %s", expectedDockerfile, actualDockerfile)
}

if expectedBuildContext != actualBuildContext {
t.Errorf("Build context mismatch: expected %s, got %s", expectedBuildContext, actualBuildContext)
}
}
}

func TestGenerateDockerfileWithDockerfile1(t *testing.T) {
t.Parallel()

Expand Down
15 changes: 3 additions & 12 deletions pkg/create/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -132,20 +132,14 @@ func Create(ctx context.Context, c *cli.Command) error {
log.Fatal("pipx, which is required for installing cookiecutter, is not installed. Please install it first.")
}

style.Print(
"Installing cookiecutter...",
nil,
)
style.PrintInfo("Installing cookiecutter...")

installCookiecutterOutput := installCookiecutterCommand(nil)
if command.IsError(installCookiecutterOutput) {
return cli.Exit("Failed to install cookiecutter.", 1)
}

style.Print(
"Cookiecutter installed!",
&style.PrintOptions{Color: "green"},
)
style.PrintSuccess("Cookiecutter installed!")
} else {
return cli.Exit("Cookiecutter is required for creating a new project. Please install it first.", 1)
}
Expand Down Expand Up @@ -200,10 +194,7 @@ func Create(ctx context.Context, c *cli.Command) error {
return cli.Exit(err, 1)
}

style.Print(
fmt.Sprintf("Successfully created project at '%s'!", projectDirectory),
&style.PrintOptions{Color: "green"},
)
style.PrintSuccess(fmt.Sprintf("Successfully created project at '%s'!", projectDirectory))

return nil
}
Expand Down
Loading

0 comments on commit de6e985

Please sign in to comment.