Skip to content

Commit

Permalink
CORE-2170 support python-api template (#153)
Browse files Browse the repository at this point in the history
  • Loading branch information
baksetercx authored Jan 2, 2025
1 parent de6e985 commit 395fc62
Show file tree
Hide file tree
Showing 15 changed files with 192 additions and 45 deletions.
3 changes: 3 additions & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,9 @@ issues:
- linters:
- gochecknoglobals
text: "Dotnet.* is a global variable"
- linters:
- gochecknoglobals
text: "Python.* is a global variable"
- linters:
- gochecknoglobals
text: "Templates is a global variable"
Expand Down
1 change: 0 additions & 1 deletion pkg/build/Dockerfile.python.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,3 @@ USER application-user
EXPOSE 8080

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

1 change: 0 additions & 1 deletion pkg/build/_test/Dockerfile.python.test1
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,3 @@ USER application-user
EXPOSE 8080

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

1 change: 0 additions & 1 deletion pkg/build/_test/Dockerfile.python.test2
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,3 @@ USER application-user
EXPOSE 8080

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

1 change: 0 additions & 1 deletion pkg/build/_test/Dockerfile.python.test3
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,3 @@ USER application-user
EXPOSE 8080

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

16 changes: 9 additions & 7 deletions pkg/build/generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@ func generateDockerfile(
return "", "", fmt.Errorf("Failed to create temporary directory: %w", err)
}

if strings.HasSuffix(projectFile, ".csproj") {
projectFileBase := path.Base(projectFile)

if strings.HasSuffix(projectFileBase, ".csproj") {
dockerfile, buildContext, err := generateDockerfileForDotNet(
projectFile,
directory,
Expand All @@ -37,7 +39,7 @@ func generateDockerfile(
}

return dockerfile, buildContext, nil
} else if strings.HasSuffix(projectFile, "go.mod") {
} else if projectFileBase == "go.mod" {
dockerfile, buildContext, err := generateDockerfileForGo(
projectFile,
applicationName,
Expand All @@ -49,7 +51,7 @@ func generateDockerfile(
}

return dockerfile, buildContext, nil
} else if strings.HasSuffix(projectFile, "uv.lock") {
} else if projectFileBase == "pyproject.toml" {
dockerfile, buildContext, err := generateDockerfileForPython(
projectFile,
directory,
Expand All @@ -60,9 +62,9 @@ func generateDockerfile(
}

return dockerfile, buildContext, nil
} else if strings.HasPrefix(projectFile, "Dockerfile") ||
strings.HasSuffix(projectFile, "Dockerfile") ||
strings.Contains(projectFile, "Dockerfile") {
} else if strings.HasPrefix(projectFileBase, "Dockerfile") ||
strings.HasSuffix(projectFileBase, "Dockerfile") ||
strings.Contains(projectFileBase, "Dockerfile") {
if options.BuildContext == "" {
return projectFile, path.Dir(projectFile), nil
}
Expand All @@ -73,7 +75,7 @@ func generateDockerfile(
return "", "", fmt.Errorf(
"Unsupported project file: %s. If you want to use a Dockerfile directly,"+
" ensure the name of the Dockerfile contains the string 'Dockerfile'",
projectFile,
projectFileBase,
)
}

Expand Down
8 changes: 4 additions & 4 deletions pkg/build/generate_python.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import (
"github.com/3lvia/cli/pkg/utils"
)

const DefaultPythonVersion = "3.13"

type DockerfileVariablesPython struct {
PythonVersion string
}
Expand Down Expand Up @@ -52,8 +54,6 @@ func generateDockerfileForPython(
}

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

// removes duplicates
slices.Sort(directories)
directories = slices.Compact(directories)
Expand Down Expand Up @@ -121,9 +121,9 @@ func getPythonVersion(directories ...string) string {
style.PrintWarning(
fmt.Sprintf(
"Did not find any .python-version files, using default version %s.",
defaultPythonVersion,
DefaultPythonVersion,
),
)

return defaultPythonVersion
return DefaultPythonVersion
}
8 changes: 4 additions & 4 deletions pkg/build/generate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -308,7 +308,7 @@ func TestGeneratePythonDockerfile1(t *testing.T) {
expectedBuildContext := "."

const (
projectFile = "uv.lock"
projectFile = "pyproject.toml"
applicationName = "demo-api-python"
)

Expand Down Expand Up @@ -341,11 +341,11 @@ func TestGeneratePythonDockerfile2(t *testing.T) {
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)
t.Errorf("Error reading expected Dockerfile: %v", err)
}

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

const applicationName = "demo-api-python"
Expand All @@ -370,7 +370,7 @@ func TestGeneratePythonDockerfile2(t *testing.T) {

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

if string(expectedDockerfile) != string(actualDockerfile) {
Expand Down
82 changes: 61 additions & 21 deletions pkg/create/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"path"
"strings"

"github.com/3lvia/cli/pkg/build"
"github.com/3lvia/cli/pkg/command"
"github.com/3lvia/cli/pkg/githubactions"
"github.com/3lvia/cli/pkg/shared"
Expand All @@ -28,11 +29,13 @@ var (
// Dotnet8WebApp = Template{"dotnet8-webapp"}.
Dotnet8Worker = Template{"dotnet8-worker"}
// Go Template = Template{"go"}.
PythonAPI = Template{"python-api"}
Templates = enum.New(
Dotnet8WebAPI,
// Dotnet8WebApp,
Dotnet8Worker,
// Go,
PythonAPI,
)
)

Expand Down Expand Up @@ -83,6 +86,10 @@ var Command *cli.Command = &cli.Command{
Usage: "The root directory of your GitHub repository." +
" The path specified will be prepended to '.github/workflows'.",
},
&cli.StringFlag{
Name: "python-version",
Usage: "The version of Python to use for the project. Only applicable for Python templates.",
},
},
Action: Create,
}
Expand Down Expand Up @@ -118,6 +125,11 @@ func Create(ctx context.Context, c *cli.Command) error {

defaultBranch := c.String("default-branch")
nonInteractive := c.Bool("non-interactive")
pythonVersion := c.String("python-version")

if template != PythonAPI && c.IsSet("python-version") {
style.PrintWarning("Argument 'python-version' is only applicable for Python templates.")
}

checkCoooiecutterInstalledOutput := checkCookiecutterInstalledCommand(nil)
if command.IsError(checkCoooiecutterInstalledOutput) {
Expand Down Expand Up @@ -150,9 +162,9 @@ func Create(ctx context.Context, c *cli.Command) error {
outputDirectory,
applicationName,
systemName,
pythonVersion,
nil,
)

if command.IsError(cookiecutterOutput) {
return cli.Exit("Failed to create project.", 1)
}
Expand All @@ -166,6 +178,13 @@ func Create(ctx context.Context, c *cli.Command) error {
return cli.Exit(err, 1)
}

if template == PythonAPI {
uvSyncOutput := uvSyncCommand(projectDirectory, nil)
if command.IsError(uvSyncOutput) {
return cli.Exit("Failed to generate uv.lock file.", 1)
}
}

githubActionsDirectory := func() string {
if c.IsSet("github-actions-directory") {
return c.String("github-actions-directory")
Expand Down Expand Up @@ -214,10 +233,8 @@ func getProjectDirectoryForTemplate(
outputDirectory,
toPascalCaseWithoutHyphens(applicationName),
), nil
/*
case Go:
return path.Join(outputDirectory, applicationName), nil
*/
case PythonAPI /*, Go*/ :
return path.Join(outputDirectory, applicationName), nil
default:
return "", fmt.Errorf("Could not find project directory for template '%s'", template)
}
Expand All @@ -234,6 +251,8 @@ func getProjectFileForTemplate(
case Go:
return "go.mod", nil
*/
case PythonAPI:
return "pyproject.toml", nil
default:
return "", fmt.Errorf("Could not find project file for template '%s'", template)
}
Expand All @@ -244,25 +263,31 @@ func cookiecutterCommand(
outputDirectory string,
applicationName string,
systemName string,
pythonVersion string,
options *command.RunOptions,
) command.Output {
return command.Run(
*exec.Command(
"cookiecutter",
"gh:3lvia/application-templates",
"--directory",
template.Value,
"--output-dir",
outputDirectory,
"--no-input",
"application_name="+applicationName,
"application_name_pascal_case="+toPascalCaseWithoutHyphens(applicationName),
"system_name="+systemName,
// TODO: is this needed?
"base_dir=./",
),
options,
cmd := *exec.Command(
"cookiecutter",
"gh:3lvia/application-templates",
"--directory",
template.Value,
"--output-dir",
outputDirectory,
"--no-input",
"application_name="+applicationName,
"application_name_pascal_case="+toPascalCaseWithoutHyphens(applicationName),
"system_name="+systemName,
)

if template == PythonAPI {
if pythonVersion == "" {
cmd.Args = append(cmd.Args, "python_version="+build.DefaultPythonVersion)
} else {
cmd.Args = append(cmd.Args, "python_version="+pythonVersion)
}
}

return command.Run(cmd, options)
}

func checkCookiecutterInstalledCommand(
Expand Down Expand Up @@ -303,3 +328,18 @@ func installCookiecutterCommand(
options,
)
}

func uvSyncCommand(
projectDirectory string,
options *command.RunOptions,
) command.Output {
return command.Run(
*exec.Command(
"uv",
"sync",
"--directory",
projectDirectory,
),
options,
)
}
21 changes: 21 additions & 0 deletions pkg/githubactions/githubactions.go
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,10 @@ func getLanguageFromProjectFile(projectFile string) (string, error) {
return "go", nil
}

if projectFile == "pyproject.toml" {
return "python", nil
}

if strings.Contains(projectFile, "Dockerfile") {
return "dockerfile", nil
}
Expand Down Expand Up @@ -362,6 +366,23 @@ func getExampleWorkflowFileURL(language string, runtimeCloudProvider string) (st
)
}

// Python
if language == "python" && runtimeCloudProvider == "aks" {
return exampleWorkflowBaseURL + "/build-deploy-python.yml", nil
}

if language == "python" && runtimeCloudProvider == "gke" {
return exampleWorkflowBaseURL + "/build-deploy-python-google.yml", nil
}

if language == "python" && runtimeCloudProvider == "iss" {
return "",
fmt.Errorf("Example workflow is not implemented yet for language '%s' and runtime cloud provider '%s'",
language,
runtimeCloudProvider,
)
}

// Dockerfile
if language == "dockerfile" && runtimeCloudProvider == "aks" {
return exampleWorkflowBaseURL + "/build-deploy-dockerfile.yml", nil
Expand Down
2 changes: 1 addition & 1 deletion pkg/shared/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ func ProjectFileFlag() *cli.StringFlag {
Name: "project-file",
Aliases: []string{"f"},
Usage: "The project file to use. We currently support .NET (*.csproj), Go (go.mod)," +
" Python with uv (uv.lock) or a generic Docker project (Dockerfile).",
" Python with uv (pyproject.toml) or a generic Docker project (Dockerfile).",
}
}

Expand Down
3 changes: 3 additions & 0 deletions pkg/utils/_test/Dockerfile.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
FROM {{ .ImageName }}:{{ .ImageTag }}

ENTRYPOINT ["/bin/bash"]
1 change: 1 addition & 0 deletions pkg/utils/_test/file.txt.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Hello, {{ .Name }}!
5 changes: 2 additions & 3 deletions pkg/utils/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,14 +104,14 @@ func WriteFileWithTemplate(

defer file.Close()

template, err := template.New(templateFile).ParseFS(templates, templateFile)
tmpl, err := template.New(templateFile).ParseFS(templates, templateFile)
if err != nil {
return "", fmt.Errorf("Failed to parse template: %w", err)
}

var fileBuffer bytes.Buffer

err = template.Execute(&fileBuffer, variables)
err = tmpl.ExecuteTemplate(&fileBuffer, path.Base(templateFile), variables)
if err != nil {
return "", fmt.Errorf("Failed to execute template: %w", err)
}
Expand All @@ -123,7 +123,6 @@ func WriteFileWithTemplate(
return filePath, nil
}

// Will only return false if the response is "n".
func PromptYesNo(question string, nonInteractive bool) (bool, error) {
style.PrintInfo(
question + " (y/n): ",
Expand Down
Loading

0 comments on commit 395fc62

Please sign in to comment.