Skip to content

Commit dd2b8c8

Browse files
committed
Append new functions within functions block
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) <[email protected]>
1 parent 1ca7ded commit dd2b8c8

File tree

2 files changed

+178
-7
lines changed

2 files changed

+178
-7
lines changed

commands/new_function.go

+92-7
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,7 @@ Download templates:
193193
return fmt.Errorf("folder: %s already exists", handlerDir)
194194
}
195195

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

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

263-
f, err := os.OpenFile("./"+fileName, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0600)
264-
if err != nil {
265-
return fmt.Errorf("could not open file '%s' %s", fileName, err)
266-
}
265+
// === Begin Updated Code for Inserting into functions: block ===
266+
if appendMode {
267+
// Read the existing file content.
268+
existingBytes, err := os.ReadFile(fileName)
269+
if err != nil {
270+
return fmt.Errorf("unable to read file: %s", fileName)
271+
}
272+
existingContent := string(existingBytes)
273+
274+
// Insert the new function block into the functions: section.
275+
updatedContent, err := insertFunctionIntoFunctionsBlock(existingContent, yamlContent)
276+
if err != nil {
277+
return fmt.Errorf("error updating functions block: %s", err)
278+
}
267279

268-
_, stackWriteErr := f.Write([]byte(yamlContent))
269-
if stackWriteErr != nil {
270-
return fmt.Errorf("error writing stack file %s", stackWriteErr)
280+
// Write the updated content back to file.
281+
err = os.WriteFile(fileName, []byte(updatedContent), 0644)
282+
if err != nil {
283+
return fmt.Errorf("error writing updated stack file: %s", err)
284+
}
285+
} else {
286+
// In non-append mode, write out the entire YAML file.
287+
err = os.WriteFile(fileName, []byte(yamlContent), 0644)
288+
if err != nil {
289+
return fmt.Errorf("error writing stack file: %s", err)
290+
}
271291
}
292+
// === End Updated Code ===
272293

273294
fmt.Print(outputMsg)
274295

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

372393
return nil
373394
}
395+
396+
// insertFunctionIntoFunctionsBlock locates the existing "functions:" block
397+
// in the YAML file content and inserts newFuncBlock (which should be indented)
398+
// at the end of the functions: block (i.e. before the next section begins).
399+
func insertFunctionIntoFunctionsBlock(existingContent string, newFuncBlock string) (string, error) {
400+
// If existing content is empty (or only whitespace), return a newline plus the new block.
401+
if strings.TrimSpace(existingContent) == "" {
402+
return "\n" + newFuncBlock, nil
403+
}
404+
405+
// Split the file into lines.
406+
lines := strings.Split(existingContent, "\n")
407+
// Remove any trailing empty lines.
408+
for len(lines) > 0 && lines[len(lines)-1] == "" {
409+
lines = lines[:len(lines)-1]
410+
}
411+
412+
functionsHeaderRegex := regexp.MustCompile(`^\s*functions:\s*$`)
413+
var functionsIndex int = -1
414+
415+
// Find the "functions:" header.
416+
for i, line := range lines {
417+
if functionsHeaderRegex.MatchString(line) {
418+
functionsIndex = i
419+
break
420+
}
421+
}
422+
423+
// If no functions header is found, simply append the new block at the end.
424+
if functionsIndex == -1 {
425+
return strings.Join(lines, "\n") + "\n" + newFuncBlock, nil
426+
}
427+
428+
// Determine where the functions: block ends.
429+
// We assume that function entries are indented (e.g. at least 2 spaces).
430+
insertIndex := len(lines)
431+
for i := functionsIndex + 1; i < len(lines); i++ {
432+
line := lines[i]
433+
if strings.TrimSpace(line) == "" {
434+
continue // Skip blank lines.
435+
}
436+
// Calculate the indent level.
437+
indentLen := len(line) - len(strings.TrimLeft(line, " "))
438+
if indentLen < 2 {
439+
// Found a new section (or unindented line); mark the end of the functions block.
440+
insertIndex = i
441+
break
442+
}
443+
}
444+
445+
// Remove any trailing newline from the new function block.
446+
newFuncBlock = strings.TrimRight(newFuncBlock, "\n")
447+
newFuncLines := strings.Split(newFuncBlock, "\n")
448+
449+
// Insert the new function block just before insertIndex.
450+
newLines := append([]string{}, lines[:insertIndex]...)
451+
newLines = append(newLines, newFuncLines...)
452+
// Append the remainder of the file.
453+
if insertIndex < len(lines) {
454+
newLines = append(newLines, lines[insertIndex:]...)
455+
}
456+
updatedContent := strings.Join(newLines, "\n")
457+
return updatedContent, nil
458+
}

commands/new_function_test.go

+86
Original file line numberDiff line numberDiff line change
@@ -518,3 +518,89 @@ func Test_getPrefixValue_Flag(t *testing.T) {
518518
t.Errorf("want %s, got %s", want, val)
519519
}
520520
}
521+
522+
func TestInsertFunctionIntoFunctionsBlock(t *testing.T) {
523+
tests := []struct {
524+
name string
525+
existingContent string
526+
newFuncBlock string
527+
want string
528+
wantErr bool
529+
}{
530+
{
531+
name: "empty file",
532+
existingContent: "",
533+
newFuncBlock: " newFunc:\n image: test\n",
534+
// When the file is empty, we return a newline plus the new block.
535+
want: "\n newFunc:\n image: test\n",
536+
wantErr: false,
537+
},
538+
{
539+
name: "templates present - insert before configuration",
540+
existingContent: "version: 1.0\n" +
541+
"provider:\n" +
542+
" name: openfaas\n" +
543+
" gateway: http://localhost:8080\n" +
544+
"functions:\n" +
545+
" func1:\n" +
546+
" image: img1\n" +
547+
"configuration:\n" +
548+
" templates:\n" +
549+
" - name: go\n",
550+
newFuncBlock: " func2:\n image: img2\n",
551+
want: "version: 1.0\n" +
552+
"provider:\n" +
553+
" name: openfaas\n" +
554+
" gateway: http://localhost:8080\n" +
555+
"functions:\n" +
556+
" func1:\n" +
557+
" image: img1\n" +
558+
" func2:\n" +
559+
" image: img2\n" +
560+
"configuration:\n" +
561+
" templates:\n" +
562+
" - name: go",
563+
wantErr: false,
564+
},
565+
{
566+
name: "no templates block present - simple append",
567+
existingContent: "version: 1.0\n" +
568+
"provider:\n" +
569+
" name: openfaas\n" +
570+
" gateway: http://localhost:8080\n" +
571+
"functions:\n" +
572+
" func1:\n" +
573+
" image: img1\n",
574+
newFuncBlock: " func2:\n image: img2\n",
575+
want: "version: 1.0\n" +
576+
"provider:\n" +
577+
" name: openfaas\n" +
578+
" gateway: http://localhost:8080\n" +
579+
"functions:\n" +
580+
" func1:\n" +
581+
" image: img1\n" +
582+
" func2:\n" +
583+
" image: img2",
584+
wantErr: false,
585+
},
586+
}
587+
588+
for _, tt := range tests {
589+
tt := tt // capture range variable
590+
t.Run(tt.name, func(t *testing.T) {
591+
got, err := insertFunctionIntoFunctionsBlock(tt.existingContent, tt.newFuncBlock)
592+
if tt.wantErr {
593+
if err == nil {
594+
t.Fatalf("Test %s: want error, got nil", tt.name)
595+
}
596+
return
597+
}
598+
if err != nil {
599+
t.Fatalf("Test %s: unexpected error, got: %v", tt.name, err)
600+
}
601+
if got != tt.want {
602+
t.Errorf("Test %s:\nwant:\n%s\ngot:\n%s", tt.name, tt.want, got)
603+
}
604+
})
605+
}
606+
}

0 commit comments

Comments
 (0)