Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support setting Python version when generating Dockerfile #152

Merged
merged 1 commit into from
Dec 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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"]

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