Repository: openfaas/faas-cli
Pull request #1000 (2024-08-05 13:43:20)

Export CreateBuildContext function from the builder package.
<!--- Provide a general summary of your changes in the Title above -->
## Description
<!--- Describe your changes in detail -->

Simplify the signature of `createBuildContext` and export the function so that it can be used by other packages.

Usage example:

```go
import (
    "log"

    "github.com/openfaas/faas-cli/builder"
    "github.com/openfaas/faas-cli/stack"
)

handler := "./hello-world"
lang := "node20"
fnName := "hello-world"

template, err := stack.LoadLanguageTemplate(handler)
if err != nil {
    log.Fatal(err)
}

buildContext, err := builder.CreateBuildContext(fnName,  handler, template, []string{})
if err != nil {
    log.Fatal(err)
}
```

## Motivation and Context
<!--- Why is this change required? What problem does it solve? -->
<!--- If it fixes an open issue, please link to the issue here. -->
- [ ] I have raised an issue to propose this change ([required](https://github.com/openfaas/faas/blob/master/CONTRIBUTING.md))

Expose more of the builder functionality use outside of the faas-cli. This change was primarily made for OpenFaaS users that want to integration with the Function Builder API to allow them to easily create the function build context from go code.

## How Has This Been Tested?
<!--- Please describe in detail how you tested your changes. -->
<!--- Include details of your testing environment, and the tests you ran to -->
<!--- see how your change affects other areas of the code, etc. -->

- Verified the `faas-cli build` and `faas-cli` publish commands still function as expected for functions using both the `dockerfile` template and other templates.

- Used the `CreateBuildContext` function in an updated version of the [function builder examples](https://github.com/openfaas/function-builder-examples)

## Types of changes
<!--- What types of changes does your code introduce? Put an `x` in all the boxes that apply: -->
- [ ] Bug fix (non-breaking change which fixes an issue)
- [x] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to change)

## Checklist:
<!--- Go over all the following points, and put an `x` in all the boxes that apply. -->
<!--- If you're unsure about any of these, don't hesitate to ask. We're here to help! -->
- [x] My code follows the code style of this project.
- [ ] My change requires a change to the documentation.
- [ ] I have updated the documentation accordingly.
- [x] I've read the [CONTRIBUTION](https://github.com/openfaas/faas/blob/master/CONTRIBUTING.md) guide
- [x] I have signed-off my commits with `git commit -s`
- [x] I have added tests to cover my changes.
- [x] All new and existing tests passed.



Patch:
diff --git a/builder/build.go b/builder/build.go
index 27fb8be9..6a6cd093 100644
--- a/builder/build.go
+++ b/builder/build.go
@@ -52,7 +52,18 @@ func BuildImage(image string, handler string, functionName string, language stri
 			return fmt.Errorf("building %s, %s is an invalid path", functionName, handler)
 		}
 
-		tempPath, err := createBuildContext(functionName, handler, language, isLanguageTemplate(language), langTemplate.HandlerFolder, copyExtraPaths)
+		// To avoid breaking the CLI for custom templates that do not set the language attribute
+		// we ensure it is always set.
+		//
+		// While templates are expected to have the language in `template.yaml` set to the same name as the template folder
+		// this was never enforced.
+		langTemplate.Language = language
+
+		if isDockerfileTemplate(langTemplate.Language) {
+			langTemplate = nil
+		}
+
+		tempPath, err := CreateBuildContext(functionName, handler, langTemplate, copyExtraPaths)
 		if err != nil {
 			return err
 		}
@@ -303,8 +314,17 @@ func isRunningInCI() bool {
 	return false
 }
 
-// createBuildContext creates temporary build folder to perform a Docker build with language template
-func createBuildContext(functionName string, handler string, language string, useFunction bool, handlerFolder string, copyExtraPaths []string) (string, error) {
+// CreateBuildContext creates a Docker build context using the provided function handler and language template.
+//
+// Parameters:
+//   - functionName: the name of the function.
+//   - handler: path to the function handler folder.
+//   - template: function language template to use. Set to nil if no template is required (e.g. the handler folder is the build context with Dockerfile).
+//   - copyExtraPaths: additional path to copy into the function handler folder. Paths should be relative to the current directory.
+//     Any path outside of this current directory will be skipped.
+//
+// Returns the path to the new build context. An error is returned if creating the build context fails.
+func CreateBuildContext(functionName string, handler string, template *stack.LanguageTemplate, copyExtraPaths []string) (string, error) {
 	tempPath := fmt.Sprintf("./build/%s/", functionName)
 
 	if err := os.RemoveAll(tempPath); err != nil {
@@ -313,16 +333,19 @@ func createBuildContext(functionName string, handler string, language string, us
 
 	functionPath := tempPath
 
-	if useFunction {
-		if handlerFolder == "" {
-			functionPath = path.Join(functionPath, defaultHandlerFolder)
+	useTemplate := false
+	if template != nil {
+		useTemplate = true
+	}
+
+	if useTemplate {
+		if len(template.HandlerFolder) > 0 {
+			functionPath = path.Join(functionPath, template.HandlerFolder)
 		} else {
-			functionPath = path.Join(functionPath, handlerFolder)
+			functionPath = path.Join(functionPath, defaultHandlerFolder)
 		}
 	}
 
-	// fmt.Printf("Preparing: %s %s\n", handler+"/", functionPath)
-
 	if isRunningInCI() {
 		defaultDirPermissions = 0777
 	}
@@ -333,8 +356,8 @@ func createBuildContext(functionName string, handler string, language string, us
 		return tempPath, mkdirErr
 	}
 
-	if useFunction {
-		if err := CopyFiles(path.Join("./template/", language), tempPath); err != nil {
+	if useTemplate {
+		if err := CopyFiles(path.Join("./template/", template.Language), tempPath); err != nil {
 			fmt.Printf("Error copying template directory: %s.\n", err.Error())
 			return tempPath, err
 		}
@@ -350,6 +373,7 @@ func createBuildContext(functionName string, handler string, language string, us
 
 	for _, info := range infos {
 		switch info.Name() {
+		// Ignore the build and template folder.
 		case "build", "template":
 			fmt.Printf("Skipping \"%s\" folder\n", info.Name())
 			continue
@@ -368,16 +392,12 @@ func createBuildContext(functionName string, handler string, language string, us
 		if err != nil {
 			return tempPath, err
 		}
-		// Note that if useFunction is false, ie is a `dockerfile` template, then
+
+		// Note that if template is nil, i.e. is a `dockerfile` template, then
 		// functionPath == tempPath, the docker build context, not the `function` handler folder
 		// inside the docker build context
-		copyErr := CopyFiles(
-			extraPathAbs,
-			filepath.Clean(path.Join(functionPath, extraPath)),
-		)
-
-		if copyErr != nil {
-			return tempPath, copyErr
+		if err := CopyFiles(extraPathAbs, filepath.Clean(path.Join(functionPath, extraPath))); err != nil {
+			return tempPath, err
 		}
 	}
 
@@ -516,6 +536,6 @@ func deDuplicate(buildOptPackages []string) []string {
 	return retPackages
 }
 
-func isLanguageTemplate(language string) bool {
-	return strings.ToLower(language) != "dockerfile"
+func isDockerfileTemplate(language string) bool {
+	return strings.ToLower(language) == "dockerfile"
 }
diff --git a/builder/publish.go b/builder/publish.go
index 6bca112c..d6b29ad1 100644
--- a/builder/publish.go
+++ b/builder/publish.go
@@ -61,7 +61,18 @@ func PublishImage(image string, handler string, functionName string, language st
 			return fmt.Errorf("building %s, %s is an invalid path", functionName, handler)
 		}
 
-		tempPath, err := createBuildContext(functionName, handler, language, isLanguageTemplate(language), langTemplate.HandlerFolder, copyExtraPaths)
+		// To avoid breaking the CLI for custom templates that do not set the language attribute
+		// we ensure it is always set.
+		//
+		// While templates are expected to have the language in `template.yaml` set to the same name as the template folder
+		// this was never enforced.
+		langTemplate.Language = language
+
+		if isDockerfileTemplate(langTemplate.Language) {
+			langTemplate = nil
+		}
+
+		tempPath, err := CreateBuildContext(functionName, handler, langTemplate, copyExtraPaths)
 		if err != nil {
 			return err
 		}


Test patch:
diff --git a/builder/build_test.go b/builder/build_test.go
index d740c9b1..61d0528c 100644
--- a/builder/build_test.go
+++ b/builder/build_test.go
@@ -9,23 +9,22 @@ import (
 	"github.com/openfaas/faas-cli/stack"
 )
 
-func Test_isLanguageTemplate_Dockerfile(t *testing.T) {
+func Test_isDockerfileTemplate_Dockerfile(t *testing.T) {
 
 	language := "Dockerfile"
 
-	want := false
-	got := isLanguageTemplate(language)
+	want := true
+	got := isDockerfileTemplate(language)
 	if got != want {
 		t.Errorf("language: %s got %v, want %v", language, got, want)
 	}
 }
 
-func Test_isLanguageTemplate_Node(t *testing.T) {
-
+func Test_isDockerfileTemplate_Node(t *testing.T) {
 	language := "node"
 
-	want := true
-	got := isLanguageTemplate(language)
+	want := false
+	got := isDockerfileTemplate(language)
 	if got != want {
 		t.Errorf("language: %s got %v, want %v", language, got, want)
 	}