Skip to content

Commit

Permalink
Append new functions within functions block
Browse files Browse the repository at this point in the history
When a templates block is present in stack.yaml, new functions
were inserted after that which failed validation. They should
have been inserted between the functions and templates block.

This code was written by prompting ChatGPT's o3-mini-high model.

Tested manually with two functions and with new unit tests.

new --lang dockerfile func1
new --lang dockerfile func2 --append stack.yaml

Signed-off-by: Alex Ellis (OpenFaaS Ltd) <alexellis2@gmail.com>
  • Loading branch information
alexellis committed Feb 7, 2025
1 parent 1ca7ded commit dd2b8c8
Show file tree
Hide file tree
Showing 2 changed files with 178 additions and 7 deletions.
99 changes: 92 additions & 7 deletions commands/new_function.go
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,7 @@ Download templates:
return fmt.Errorf("folder: %s already exists", handlerDir)
}

// In non-append mode, we error if the file exists.
if _, err := os.Stat(fileName); err == nil && !appendMode {
return fmt.Errorf("file: %s already exists. Try \"faas-cli new --append %s\" instead", fileName, fileName)
}
Expand Down Expand Up @@ -258,17 +259,37 @@ Download templates:
}
}

// Build the YAML content. In append mode this is just the function block.
yamlContent := prepareYAMLContent(appendMode, gateway, &function)

f, err := os.OpenFile("./"+fileName, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0600)
if err != nil {
return fmt.Errorf("could not open file '%s' %s", fileName, err)
}
// === Begin Updated Code for Inserting into functions: block ===
if appendMode {
// Read the existing file content.
existingBytes, err := os.ReadFile(fileName)
if err != nil {
return fmt.Errorf("unable to read file: %s", fileName)
}
existingContent := string(existingBytes)

// Insert the new function block into the functions: section.
updatedContent, err := insertFunctionIntoFunctionsBlock(existingContent, yamlContent)
if err != nil {
return fmt.Errorf("error updating functions block: %s", err)
}

_, stackWriteErr := f.Write([]byte(yamlContent))
if stackWriteErr != nil {
return fmt.Errorf("error writing stack file %s", stackWriteErr)
// Write the updated content back to file.
err = os.WriteFile(fileName, []byte(updatedContent), 0644)
if err != nil {
return fmt.Errorf("error writing updated stack file: %s", err)
}
} else {
// In non-append mode, write out the entire YAML file.
err = os.WriteFile(fileName, []byte(yamlContent), 0644)
if err != nil {
return fmt.Errorf("error writing stack file: %s", err)
}
}
// === End Updated Code ===

fmt.Print(outputMsg)

Expand Down Expand Up @@ -371,3 +392,67 @@ Cannot have duplicate function names in same yaml file`, functionName, appendFil

return nil
}

// insertFunctionIntoFunctionsBlock locates the existing "functions:" block
// in the YAML file content and inserts newFuncBlock (which should be indented)
// at the end of the functions: block (i.e. before the next section begins).
func insertFunctionIntoFunctionsBlock(existingContent string, newFuncBlock string) (string, error) {
// If existing content is empty (or only whitespace), return a newline plus the new block.
if strings.TrimSpace(existingContent) == "" {
return "\n" + newFuncBlock, nil
}

// Split the file into lines.
lines := strings.Split(existingContent, "\n")
// Remove any trailing empty lines.
for len(lines) > 0 && lines[len(lines)-1] == "" {
lines = lines[:len(lines)-1]
}

functionsHeaderRegex := regexp.MustCompile(`^\s*functions:\s*$`)
var functionsIndex int = -1

// Find the "functions:" header.
for i, line := range lines {
if functionsHeaderRegex.MatchString(line) {
functionsIndex = i
break
}
}

// If no functions header is found, simply append the new block at the end.
if functionsIndex == -1 {
return strings.Join(lines, "\n") + "\n" + newFuncBlock, nil
}

// Determine where the functions: block ends.
// We assume that function entries are indented (e.g. at least 2 spaces).
insertIndex := len(lines)
for i := functionsIndex + 1; i < len(lines); i++ {
line := lines[i]
if strings.TrimSpace(line) == "" {
continue // Skip blank lines.
}
// Calculate the indent level.
indentLen := len(line) - len(strings.TrimLeft(line, " "))
if indentLen < 2 {
// Found a new section (or unindented line); mark the end of the functions block.
insertIndex = i
break
}
}

// Remove any trailing newline from the new function block.
newFuncBlock = strings.TrimRight(newFuncBlock, "\n")
newFuncLines := strings.Split(newFuncBlock, "\n")

// Insert the new function block just before insertIndex.
newLines := append([]string{}, lines[:insertIndex]...)
newLines = append(newLines, newFuncLines...)
// Append the remainder of the file.
if insertIndex < len(lines) {
newLines = append(newLines, lines[insertIndex:]...)
}
updatedContent := strings.Join(newLines, "\n")
return updatedContent, nil
}
86 changes: 86 additions & 0 deletions commands/new_function_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -518,3 +518,89 @@ func Test_getPrefixValue_Flag(t *testing.T) {
t.Errorf("want %s, got %s", want, val)
}
}

func TestInsertFunctionIntoFunctionsBlock(t *testing.T) {
tests := []struct {
name string
existingContent string
newFuncBlock string
want string
wantErr bool
}{
{
name: "empty file",
existingContent: "",
newFuncBlock: " newFunc:\n image: test\n",
// When the file is empty, we return a newline plus the new block.
want: "\n newFunc:\n image: test\n",
wantErr: false,
},
{
name: "templates present - insert before configuration",
existingContent: "version: 1.0\n" +
"provider:\n" +
" name: openfaas\n" +
" gateway: http://localhost:8080\n" +
"functions:\n" +
" func1:\n" +
" image: img1\n" +
"configuration:\n" +
" templates:\n" +
" - name: go\n",
newFuncBlock: " func2:\n image: img2\n",
want: "version: 1.0\n" +
"provider:\n" +
" name: openfaas\n" +
" gateway: http://localhost:8080\n" +
"functions:\n" +
" func1:\n" +
" image: img1\n" +
" func2:\n" +
" image: img2\n" +
"configuration:\n" +
" templates:\n" +
" - name: go",
wantErr: false,
},
{
name: "no templates block present - simple append",
existingContent: "version: 1.0\n" +
"provider:\n" +
" name: openfaas\n" +
" gateway: http://localhost:8080\n" +
"functions:\n" +
" func1:\n" +
" image: img1\n",
newFuncBlock: " func2:\n image: img2\n",
want: "version: 1.0\n" +
"provider:\n" +
" name: openfaas\n" +
" gateway: http://localhost:8080\n" +
"functions:\n" +
" func1:\n" +
" image: img1\n" +
" func2:\n" +
" image: img2",
wantErr: false,
},
}

for _, tt := range tests {
tt := tt // capture range variable
t.Run(tt.name, func(t *testing.T) {
got, err := insertFunctionIntoFunctionsBlock(tt.existingContent, tt.newFuncBlock)
if tt.wantErr {
if err == nil {
t.Fatalf("Test %s: want error, got nil", tt.name)
}
return
}
if err != nil {
t.Fatalf("Test %s: unexpected error, got: %v", tt.name, err)
}
if got != tt.want {
t.Errorf("Test %s:\nwant:\n%s\ngot:\n%s", tt.name, tt.want, got)
}
})
}
}

0 comments on commit dd2b8c8

Please sign in to comment.