From 7b566771d064680f0d4a6f9e7fa98168bf6a8c15 Mon Sep 17 00:00:00 2001 From: Andreas Salhus Bakseter <141913422+baksetercx@users.noreply.github.com> Date: Tue, 7 Jan 2025 16:57:50 +0100 Subject: [PATCH] Simplify resolving projectFile/buildContext --- pkg/build/generate.go | 65 +++++---- pkg/build/generate_dotnet.go | 100 +++++--------- pkg/build/generate_go.go | 49 +------ pkg/build/generate_python.go | 13 +- pkg/build/generate_test.go | 247 ++++++++++++++++++++++------------- 5 files changed, 239 insertions(+), 235 deletions(-) diff --git a/pkg/build/generate.go b/pkg/build/generate.go index a0c72c2..f59c0c3 100644 --- a/pkg/build/generate.go +++ b/pkg/build/generate.go @@ -12,12 +12,12 @@ import ( var dockerfileTemplates embed.FS type GenerateDockerfileOptions struct { - GoMainPackageDirectory string BuildContext string + GoMainPackageDirectory string } func generateDockerfile( - projectFile string, + projectFileRelativePath string, options GenerateDockerfileOptions, ) (string, string, error) { directory, err := os.MkdirTemp("", "3lv-build-*") @@ -25,13 +25,16 @@ func generateDockerfile( return "", "", fmt.Errorf("Failed to create temporary directory: %w", err) } - projectFileBase := path.Base(projectFile) + projectFile := getProjectFilePathRelativeToBuildContext(projectFileRelativePath, options.BuildContext) + buildContext := getBuildContextFromProjectFile(projectFileRelativePath, options.BuildContext) + + projectFileBase := path.Base(projectFileRelativePath) // could have used `projectFile` here also, doesn't matter if strings.HasSuffix(projectFileBase, ".csproj") { - dockerfile, buildContext, err := generateDockerfileForDotNet( + dockerfile, err := generateDockerfileForDotNet( projectFile, + buildContext, directory, - options, ) if err != nil { return "", "", fmt.Errorf("Failed to generate Dockerfile for .NET project: %w", err) @@ -39,10 +42,10 @@ func generateDockerfile( return dockerfile, buildContext, nil } else if projectFileBase == "go.mod" { - dockerfile, buildContext, err := generateDockerfileForGo( - projectFile, + dockerfile, err := generateDockerfileForGo( + buildContext, directory, - options, + options.GoMainPackageDirectory, ) if err != nil { return "", "", fmt.Errorf("Failed to generate Dockerfile for Go project: %w", err) @@ -50,10 +53,10 @@ func generateDockerfile( return dockerfile, buildContext, nil } else if projectFileBase == "pyproject.toml" { - dockerfile, buildContext, err := generateDockerfileForPython( + dockerfile, err := generateDockerfileForPython( projectFile, + buildContext, directory, - options, ) if err != nil { return "", "", fmt.Errorf("Failed to generate Dockerfile for Python project: %w", err) @@ -63,37 +66,45 @@ func generateDockerfile( } else if strings.HasPrefix(projectFileBase, "Dockerfile") || strings.HasSuffix(projectFileBase, "Dockerfile") || strings.Contains(projectFileBase, "Dockerfile") { - if options.BuildContext == "" { - return projectFile, path.Dir(projectFile), nil - } - - return projectFile, options.BuildContext, nil + return projectFileRelativePath, buildContext, nil } 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'", - projectFileBase, + projectFileRelativePath, ) } -func getProjectFileAndBuildContext( +func getProjectFilePathRelativeToBuildContext( projectFileRelativePath string, buildContextRelativePath string, -) (string, string) { - if len(buildContextRelativePath) == 0 { - return path.Base(projectFileRelativePath), path.Dir(projectFileRelativePath) +) string { + if buildContextRelativePath == "" { + return path.Base(projectFileRelativePath) } - if strings.HasSuffix(buildContextRelativePath, "/") { - return strings.TrimPrefix( - projectFileRelativePath, - buildContextRelativePath, - ), buildContextRelativePath - } + buildContextRelativePath = strings.TrimSuffix(buildContextRelativePath, "/") return strings.TrimPrefix( projectFileRelativePath, buildContextRelativePath+"/", - ), buildContextRelativePath + ) +} + +func getBuildContextFromProjectFile( + projectFileRelativePath string, + buildContextRelativePath string, +) string { + if buildContextRelativePath == "" { + projectFileDirectory := path.Dir(projectFileRelativePath) + + if projectFileDirectory == "" { + return "." + } + + return projectFileDirectory + } + + return strings.TrimSuffix(buildContextRelativePath, "/") } diff --git a/pkg/build/generate_dotnet.go b/pkg/build/generate_dotnet.go index 1c6e3f8..45d9b7c 100644 --- a/pkg/build/generate_dotnet.go +++ b/pkg/build/generate_dotnet.go @@ -2,9 +2,11 @@ package build import ( "encoding/xml" + "errors" "fmt" "io" "os" + "path" "path/filepath" "strings" @@ -12,59 +14,54 @@ import ( ) type DockerfileVariablesDotnet struct { - CsprojFile string // required - AssemblyName string // required - BaseImageTag string // required - RuntimeBaseImage string // required + CsprojFile string + AssemblyName string + BaseImageTag string + RuntimeBaseImage string } func generateDockerfileForDotNet( projectFile string, + buildContext string, directory string, - options GenerateDockerfileOptions, -) (string, string, error) { - csprojFileName, buildContext := getProjectFileAndBuildContext( - projectFile, - options.BuildContext, - ) - - assemblyName, err := findAssemblyName( - projectFile, - csprojFileName, - ) +) (string, error) { + csprojXML, err := getXMLFromFile(path.Join(buildContext, projectFile)) if err != nil { - return "", "", err + return "", err } - baseImageTag, err := findBaseImageTag(projectFile) + assemblyName, err := findAssemblyName(projectFile, csprojXML) if err != nil { - return "", "", err + return "", err } - runtimeBaseImage, err := findRuntimeBaseImage(projectFile) + baseImageTag, err := findBaseImageTag(csprojXML) if err != nil { - return "", "", err + return "", err } - const templateFile = "Dockerfile.dotnet.tmpl" + runtimeBaseImage, err := findRuntimeBaseImage(csprojXML) + if err != nil { + return "", err + } dockerfilePath, err := utils.WriteFileWithTemplate( directory, "Dockerfile", - templateFile, + "Dockerfile.dotnet.tmpl", dockerfileTemplates, DockerfileVariablesDotnet{ - CsprojFile: csprojFileName, + CsprojFile: projectFile, AssemblyName: assemblyName, BaseImageTag: baseImageTag, RuntimeBaseImage: runtimeBaseImage, }, ) if err != nil { - return "", "", err + return "", err } - return dockerfilePath, buildContext, nil + return dockerfilePath, nil } type CSharpProjectFile struct { @@ -79,42 +76,33 @@ type PropertyGroup struct { } func getXMLFromFile(fileName string) (*CSharpProjectFile, error) { + var project CSharpProjectFile + file, err := os.Open(fileName) if err != nil { - return nil, fmt.Errorf("getXMLFromFile: Failed to open file: %w", err) + return &project, fmt.Errorf("Failed to open file: %w", err) } + defer file.Close() + bytes, err := io.ReadAll(file) if err != nil { - return nil, fmt.Errorf("getXMLFromFile: Failed to read file: %w", err) + return &project, fmt.Errorf("Failed to read file: %w", err) } - var project CSharpProjectFile - err = xml.Unmarshal(bytes, &project) if err != nil { - return nil, fmt.Errorf("getXMLFromFile: Failed to unmarshal file: %w", err) + return &project, fmt.Errorf("Failed to unmarshal file: %w", err) } return &project, nil } -func findAssemblyName( - csprojFileRelativePath string, - csprojFileName string, -) (string, error) { - var assemblyName string - - csprojXML, err := getXMLFromFile(csprojFileRelativePath) - if err != nil { - return "", err - } - - assemblyName = csprojXML.PropertyGroup.AssemblyName +func findAssemblyName(csprojFileName string, csprojXML *CSharpProjectFile) (string, error) { + assemblyName := csprojXML.PropertyGroup.AssemblyName if len(assemblyName) == 0 { - basename := filepath.Base(csprojFileName) - withoutExtension := strings.TrimSuffix(basename, filepath.Ext(basename)) + withoutExtension := strings.TrimSuffix(path.Base(csprojFileName), filepath.Ext(csprojFileName)) return withoutExtension + ".dll", nil } @@ -122,35 +110,19 @@ func findAssemblyName( return assemblyName + ".dll", nil } -func findBaseImageTag(csprojFileRelativePath string) (string, error) { - csprojXML, err := getXMLFromFile(csprojFileRelativePath) - if err != nil { - return "", err - } - +func findBaseImageTag(csprojXML *CSharpProjectFile) (string, error) { targetFramework := csprojXML.PropertyGroup.TargetFramework if len(targetFramework) == 0 { - return "", fmt.Errorf( - "findBaseImageTag: TargetFramework not found in csproj file: %s", - csprojFileRelativePath, - ) + return "", errors.New("TargetFramework not found in .csproj-file.") } return targetFramework[3:] + "-alpine", nil } -func findRuntimeBaseImage(csprojFileRelativePath string) (string, error) { - csprojXML, err := getXMLFromFile(csprojFileRelativePath) - if err != nil { - return "", err - } - +func findRuntimeBaseImage(csprojXML *CSharpProjectFile) (string, error) { sdk := csprojXML.SDK if len(sdk) == 0 { - return "", fmt.Errorf( - "SDK not found in csproj file: %s", - csprojFileRelativePath, - ) + return "", errors.New("SDK not found in .csproj-file.") } switch sdk { diff --git a/pkg/build/generate_go.go b/pkg/build/generate_go.go index 9675aed..fdff7bb 100644 --- a/pkg/build/generate_go.go +++ b/pkg/build/generate_go.go @@ -1,8 +1,6 @@ package build import ( - "strings" - "github.com/3lvia/cli/pkg/utils" ) @@ -12,18 +10,13 @@ type DockerfileVariablesGo struct { } func generateDockerfileForGo( - projectFile string, + buildContext string, directory string, - options GenerateDockerfileOptions, -) (string, string, error) { - goModuleDirectory, buildContext := getGoModuleDirectoryAndBuildContext( - projectFile, - options.BuildContext, - ) - + goMainPackageDirectory string, +) (string, error) { dockerfileVariables := DockerfileVariablesGo{ - GoModuleDirectory: goModuleDirectory, - MainPackageDirectory: options.GoMainPackageDirectory, + GoModuleDirectory: buildContext, + MainPackageDirectory: goMainPackageDirectory, } const templateFile = "Dockerfile.go.tmpl" @@ -36,36 +29,8 @@ func generateDockerfileForGo( dockerfileVariables, ) if err != nil { - return "", "", err - } - - return dockerfilePath, buildContext, nil -} - -func getGoModuleDirectoryAndBuildContext( - projectFileRelativePath string, - buildContextRelativePath string, -) (string, string) { - projectFileName, buildContext := getProjectFileAndBuildContext( - projectFileRelativePath, - buildContextRelativePath, - ) - - return dotIfEmpty( - strings.TrimSuffix( - strings.TrimSuffix( - projectFileName, - "go.mod", - ), - "/", - ), - ), buildContext -} - -func dotIfEmpty(str string) string { - if len(str) == 0 { - return "." + return "", err } - return str + return dockerfilePath, nil } diff --git a/pkg/build/generate_python.go b/pkg/build/generate_python.go index 4d85435..ea2dde7 100644 --- a/pkg/build/generate_python.go +++ b/pkg/build/generate_python.go @@ -20,14 +20,9 @@ type DockerfileVariablesPython struct { func generateDockerfileForPython( projectFile string, + buildContext string, directory string, - options GenerateDockerfileOptions, -) (string, string, error) { - _, buildContext := getProjectFileAndBuildContext( - projectFile, - options.BuildContext, - ) - +) (string, error) { pythonVersion := getPythonVersion( path.Dir(projectFile), buildContext, @@ -47,10 +42,10 @@ func generateDockerfileForPython( dockerfileVariables, ) if err != nil { - return "", "", err + return "", err } - return dockerfilePath, buildContext, nil + return dockerfilePath, nil } func getPythonVersion(directories ...string) string { diff --git a/pkg/build/generate_test.go b/pkg/build/generate_test.go index 93c166c..8d423af 100644 --- a/pkg/build/generate_test.go +++ b/pkg/build/generate_test.go @@ -7,111 +7,181 @@ import ( "testing" ) -func TestGetProjectFileAndBuildContext1(t *testing.T) { +func TestGetProjectFileRelativeToBuildContextAndGetBuildContextFromProjectFile(t *testing.T) { t.Parallel() const ( - expectedCsprojFileName = "demo-api.csproj" - expectedBuildContext = "." + expectedProjectFile = "demo-api.csproj" + expectedBuildContext = "." + + projectFileInput = "demo-api.csproj" + buildContextInput = "" ) - csprojFileName, buildContext := getProjectFileAndBuildContext( - "demo-api.csproj", - "", + projectFile := getProjectFilePathRelativeToBuildContext( + projectFileInput, + buildContextInput, ) - if expectedCsprojFileName != csprojFileName { - t.Errorf("Csproj file name mismatch: expected %s, got %s", expectedCsprojFileName, csprojFileName) + if expectedProjectFile != projectFile { + t.Errorf("Project file mismatch: expected %s, got %s", expectedProjectFile, projectFile) } + buildContext := getBuildContextFromProjectFile( + projectFileInput, + buildContextInput, + ) + if expectedBuildContext != buildContext { t.Errorf("Build context mismatch: expected %s, got %s", expectedBuildContext, buildContext) } } -func TestGetProjectFileAndBuildContext2(t *testing.T) { +func TestGetProjectFileRelativeToBuildContextAndGetBuildContextFromProjectFile2(t *testing.T) { t.Parallel() const ( - expectedCsprojFileName = "demo-api.csproj" - expectedBuildContext = "src/Things/DemoApi" + expectedProjectFile = "demo-api.csproj" + expectedBuildContext = "src/Things/DemoApi" + + projectFileInput = "demo-api.csproj" + buildContextInput = "src/Things/DemoApi" ) - csprojFileName, buildContext := getProjectFileAndBuildContext( - "demo-api.csproj", - "src/Things/DemoApi", + projectFile := getProjectFilePathRelativeToBuildContext( + projectFileInput, + buildContextInput, ) - if expectedCsprojFileName != csprojFileName { - t.Errorf("Csproj file name mismatch: expected %s, got %s", expectedCsprojFileName, csprojFileName) + if expectedProjectFile != projectFile { + t.Errorf("Project file mismatch: expected %s, got %s", expectedProjectFile, projectFile) } + buildContext := getBuildContextFromProjectFile( + projectFileInput, + buildContextInput, + ) + if expectedBuildContext != buildContext { t.Errorf("Build context mismatch: expected %s, got %s", expectedBuildContext, buildContext) } } -func TestGetProjectFileAndBuildContext3(t *testing.T) { +func TestGetProjectFileRelativeToBuildContextAndGetBuildContextFromProjectFile3(t *testing.T) { t.Parallel() const ( - expectedCsprojFileName = "demo-api.csproj" - expectedBuildContext = "src/Things/DemoApi" + expectedProjectFile = "demo-api.csproj" + expectedBuildContext = "src/Things/DemoApi" + + projectFileInput = "src/Things/DemoApi/demo-api.csproj" + buildContextInput = "" ) - csprojFileName, buildContext := getProjectFileAndBuildContext( - "src/Things/DemoApi/demo-api.csproj", - "", + projectFile := getProjectFilePathRelativeToBuildContext( + projectFileInput, + buildContextInput, ) - if expectedCsprojFileName != csprojFileName { - t.Errorf("Csproj file name mismatch: expected %s, got %s", expectedCsprojFileName, csprojFileName) + if expectedProjectFile != projectFile { + t.Errorf("Project file mismatch: expected %s, got %s", expectedProjectFile, projectFile) } + buildContext := getBuildContextFromProjectFile( + projectFileInput, + buildContextInput, + ) + if expectedBuildContext != buildContext { t.Errorf("Build context mismatch: expected %s, got %s", expectedBuildContext, buildContext) } } -func TestGetProjectFileAndBuildContext4(t *testing.T) { +func TestGetProjectFileRelativeToBuildContextAndGetBuildContextFromProjectFile4(t *testing.T) { t.Parallel() const ( - expectedCsprojFileName = "DemoApi/demo-api.csproj" - expectedBuildContext = "src/Things" + expectedProjectFile = "DemoApi/demo-api.csproj" + expectedBuildContext = "src/Things" + + projectFileInput = "src/Things/DemoApi/demo-api.csproj" + buildContextInput = "src/Things" ) - csprojFileName, buildContext := getProjectFileAndBuildContext( - "src/Things/DemoApi/demo-api.csproj", - "src/Things", + projectFile := getProjectFilePathRelativeToBuildContext( + projectFileInput, + buildContextInput, ) - if expectedCsprojFileName != csprojFileName { - t.Errorf("Csproj file name mismatch: expected %s, got %s", expectedCsprojFileName, csprojFileName) + if expectedProjectFile != projectFile { + t.Errorf("Project file mismatch: expected %s, got %s", expectedProjectFile, projectFile) } + buildContext := getBuildContextFromProjectFile( + projectFileInput, + buildContextInput, + ) + if expectedBuildContext != buildContext { t.Errorf("Build context mismatch: expected %s, got %s", expectedBuildContext, buildContext) } } -func TestGetProjectFileAndBuildContext5(t *testing.T) { +func TestGetProjectFileRelativeToBuildContextAndGetBuildContextFromProjectFile5(t *testing.T) { t.Parallel() const ( - expectedCsprojFileName = "Things/DemoApi/demo-api.csproj" - expectedBuildContext = "src" + expectedProjectFile = "DemoApi/demo-api.csproj" + expectedBuildContext = "src/Things" + + projectFileInput = "src/Things/DemoApi/demo-api.csproj" + buildContextInput = "src/Things/" ) - csprojFileName, buildContext := getProjectFileAndBuildContext( - "src/Things/DemoApi/demo-api.csproj", - "src", + projectFile := getProjectFilePathRelativeToBuildContext( + projectFileInput, + buildContextInput, ) - if expectedCsprojFileName != csprojFileName { - t.Errorf("Csproj file name mismatch: expected %s, got %s", expectedCsprojFileName, csprojFileName) + if expectedProjectFile != projectFile { + t.Errorf("Project file mismatch: expected %s, got %s", expectedProjectFile, projectFile) } + buildContext := getBuildContextFromProjectFile( + projectFileInput, + buildContextInput, + ) + + if expectedBuildContext != buildContext { + t.Errorf("Build context mismatch: expected %s, got %s", expectedBuildContext, buildContext) + } +} + +func TestGetProjectFileAndBuildContext6(t *testing.T) { + t.Parallel() + + const ( + expectedProjectFile = "Things/DemoApi/demo-api.csproj" + expectedBuildContext = "src" + + projectFileInput = "src/Things/DemoApi/demo-api.csproj" + buildContextInput = "src" + ) + + projectFile := getProjectFilePathRelativeToBuildContext( + projectFileInput, + buildContextInput, + ) + + if expectedProjectFile != projectFile { + t.Errorf("Project file mismatch: expected %s, got %s", expectedProjectFile, projectFile) + } + + buildContext := getBuildContextFromProjectFile( + projectFileInput, + buildContextInput, + ) + if expectedBuildContext != buildContext { t.Errorf("Build context mismatch: expected %s, got %s", expectedBuildContext, buildContext) } @@ -125,9 +195,14 @@ func TestFindAssemblyName1(t *testing.T) { expectedAssemblyName = "no-assembly-name.dll" ) + csprojXML, err := getXMLFromFile(projectFile) + if err != nil { + t.Errorf("Error reading XML from file: %v", err) + } + actualAssemblyName, err := findAssemblyName( projectFile, - filepath.Base(projectFile), + csprojXML, ) if err != nil { t.Errorf("Error finding assembly name: %v", err) @@ -146,13 +221,15 @@ func TestFindAssemblyName2(t *testing.T) { expectedAssemblyName = "SelfDefinedAssemblyName.dll" ) + csprojXML, err := getXMLFromFile(projectFile) + if err != nil { + t.Errorf("Error reading XML from file: %v", err) + } + actualAssemblyName, err := findAssemblyName( projectFile, - filepath.Base(projectFile), + csprojXML, ) - if err != nil { - t.Errorf("Error finding assembly name: %v", err) - } if expectedAssemblyName != actualAssemblyName { t.Errorf("Assembly name mismatch: expected %s, got %s", expectedAssemblyName, actualAssemblyName) @@ -164,7 +241,12 @@ func TestFindRuntimeBaseImage1(t *testing.T) { const projectFile = "_test/no-sdk.csproj" - _, err := findRuntimeBaseImage(projectFile) + csprojXML, err := getXMLFromFile(projectFile) + if err != nil { + t.Errorf("Error reading XML from file: %v", err) + } + + _, err = findRuntimeBaseImage(csprojXML) if err == nil { t.Errorf("Expected error finding runtime base image") } @@ -178,7 +260,12 @@ func TestFindRuntimeBaseImage2(t *testing.T) { expectedBaseImage = "mcr.microsoft.com/dotnet/runtime" ) - actualBaseImage, err := findRuntimeBaseImage(projectFile) + csprojXML, err := getXMLFromFile(projectFile) + if err != nil { + t.Errorf("Error reading XML from file: %v", err) + } + + actualBaseImage, err := findRuntimeBaseImage(csprojXML) if err != nil { t.Errorf("Error finding runtime base image: %v", err) } @@ -201,7 +288,12 @@ func TestFindRuntimeBaseImage3(t *testing.T) { } for _, projectFile := range projectFiles { - actualBaseImage, err := findRuntimeBaseImage(projectFile) + csprojXML, err := getXMLFromFile(projectFile) + if err != nil { + t.Errorf("Error reading XML from file: %v", err) + } + + actualBaseImage, err := findRuntimeBaseImage(csprojXML) if err != nil { t.Errorf("Error finding runtime base image: %v", err) } @@ -217,9 +309,9 @@ func TestFindBaseImageTag1(t *testing.T) { const projectFile = "_test/this-does-not-exists.csproj" - _, err := findBaseImageTag(projectFile) + _, err := getXMLFromFile(projectFile) if err == nil { - t.Errorf("Expected error finding base image tag") + t.Errorf("Expected error reading XML from file") } } @@ -228,7 +320,12 @@ func TestFindBaseImageTag2(t *testing.T) { const projectFile = "_test/no-target-framework.csproj" - _, err := findBaseImageTag(projectFile) + csprojXML, err := getXMLFromFile(projectFile) + if err == nil { + t.Errorf("Expected error reading XML from file") + } + + _, err = findBaseImageTag(csprojXML) if err == nil { t.Errorf("Expected error finding base image tag") } @@ -248,7 +345,12 @@ func TestFindBaseImageTag3(t *testing.T) { projectFile := "_test/dotnet-" + frameworkVersion + ".csproj" expectedBaseImageTag := frameworkVersion + "-alpine" - actualBaseImageTag, err := findBaseImageTag(projectFile) + csprojXML, err := getXMLFromFile(projectFile) + if err != nil { + t.Errorf("Error reading XML from file: %v", err) + } + + actualBaseImageTag, err := findBaseImageTag(csprojXML) if err != nil { t.Errorf("Error finding base image tag: %v", err) } @@ -451,44 +553,3 @@ func TestGenerateDockerfileWithDockerfile3(t *testing.T) { t.Errorf("Build context mismatch: expected %s, got %s", expectedBuildContext, actualBuildContext) } } - -func TestDotIfEmpty1(t *testing.T) { - t.Parallel() - - const expected = "value" - - actual := dotIfEmpty("value") - - if expected != actual { - t.Errorf("Expected %s, got %s", expected, actual) - } -} - -func TestDotIfEmpty2(t *testing.T) { - t.Parallel() - - const expected = "." - - actual := dotIfEmpty("") - - if expected != actual { - t.Errorf("Expected %s, got %s", expected, actual) - } -} - -func TestDotIfEmpty3(t *testing.T) { - t.Parallel() - - const expected = "." - - type testStruct struct { - value string - } - - test := testStruct{} //nolint:exhaustruct - actual := dotIfEmpty(test.value) - - if expected != actual { - t.Errorf("Expected %s, got %s", expected, actual) - } -}