Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 80 additions & 0 deletions cmd/cli/cmd/deploy.go
Original file line number Diff line number Diff line change
@@ -1,16 +1,24 @@
package cmd

import (
"bufio"
"encoding/json"
"faas-engine-go/internal/buildcontext"
"fmt"
"log"
"log/slog"
"net/http"
"os"
"path/filepath"
"strings"

"github.com/fatih/color"
"github.com/spf13/cobra"
)

// Deploy flags
var forceRedeploy bool

// deployCmd represents the deploy command
var deployCmd = &cobra.Command{
Use: "deploy <path>",
Expand Down Expand Up @@ -42,10 +50,39 @@ to quickly create a Cobra application.`,
return fmt.Errorf("failed to print success message: %w", err)
}

// Check if function already exists and ask for confirmation if not using --force
fmt.Print("[2/3] Checking if function exists...")
functionExists, err := checkFunctionExists(functionName)
if err != nil {
color.Red(" Failed. \n\n%s\n", err.Error())
return fmt.Errorf("failed to check if function exists: %w", err)
}

if functionExists && !forceRedeploy {
if _, err := color.New(color.FgYellow).Println(" Found."); err != nil {
return fmt.Errorf("failed to print status message: %w", err)
}

// Ask for confirmation
if !confirmReplacement(functionName) {
color.Yellow("✗ Deployment cancelled")
return nil
}
} else if functionExists {
if _, err := color.New(color.FgYellow).Println(" Found (--force enabled)."); err != nil {
return fmt.Errorf("failed to print status message: %w", err)
}
} else {
if _, err := color.New(color.FgGreen).Println(" Not found."); err != nil {
return fmt.Errorf("failed to print status message: %w", err)
}
}

//send the tarstream to the server
url := fmt.Sprintf("%s/functions", serverAddr)

// Stream deploy logs from server
fmt.Print("[3/3] Deploying function...")
err = buildcontext.SendTarStream(tarstream, url, functionName)
if err != nil {
slog.Error("deployment failed", "error", err)
Expand All @@ -61,6 +98,7 @@ func init() {

deployCmd.Flags().StringVar(&functionName, "name", "", "Name of the function to deploy")
deployCmd.Flags().StringVar(&runtimeName, "runtime", "", "Name of the runtime to use")
deployCmd.Flags().BoolVar(&forceRedeploy, "force", false, "Force redeploy without confirmation if function already exists")

if err := deployCmd.MarkFlagRequired("name"); err != nil {
log.Fatalf("failed to mark flag as required: %v", err)
Expand All @@ -69,3 +107,45 @@ func init() {
log.Fatalf("failed to mark flag as required: %v", err)
}
}

// checkFunctionExists queries the runtime-manager to see if a function with this name exists
func checkFunctionExists(functionName string) (bool, error) {
url := fmt.Sprintf("%s/functions", serverAddr)

resp, err := http.Get(url)
if err != nil {
return false, fmt.Errorf("unable to reach runtime manager at %s: %w", serverAddr, err)
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
return false, fmt.Errorf("failed to list functions: server returned %s", resp.Status)
}

var response struct {
Functions []struct {
Name string `json:"name"`
} `json:"functions"`
}

if err := json.NewDecoder(resp.Body).Decode(&response); err != nil {
return false, fmt.Errorf("failed to parse functions list: %w", err)
}

for _, fn := range response.Functions {
if fn.Name == functionName {
return true, nil
}
}

return false, nil
}

// confirmReplacement prompts the user to confirm redeploying an existing function
func confirmReplacement(functionName string) bool {
fmt.Print(color.YellowString(fmt.Sprintf("Function '%s' already exists. Replace it? (yes/no): ", functionName)))
reader := bufio.NewReader(os.Stdin)
response, _ := reader.ReadString('\n')
response = strings.TrimSpace(strings.ToLower(response))
return response == "yes" || response == "y"
}
17 changes: 15 additions & 2 deletions internal/api/deploy.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"fmt"
"io"
"log/slog"
"net"
"net/http"
"strings"
"time"
Expand Down Expand Up @@ -124,14 +125,26 @@ func DeployHandler(deployer Deployer, fs FunctionStore) http.HandlerFunc {
slog.Error("failed to deactivate old versions", "error", err)
}

host, _, err := net.SplitHostPort(r.Host)
if err != nil {
host = r.Host
}

var endpoint string
if host == "localhost" || host == "127.0.0.1" {
endpoint = fmt.Sprintf("%s.localhost", functionName)
} else {
endpoint = fmt.Sprintf("%s.%s.nip.io", functionName, host)
}

fn := &models.Function{
Name: functionName,
Version: functionVersion,
PackageChecksum: checksum,
Image: config.ImageRef(config.FunctionsRepo, functionName, functionVersion),
Runtime: "node",
ScheduleCron: "",
Endpoint: fmt.Sprintf("%s.localhost", functionName),
Endpoint: endpoint,
Status: "active",
CreatedAt: time.Now(),
}
Expand All @@ -142,7 +155,7 @@ func DeployHandler(deployer Deployer, fs FunctionStore) http.HandlerFunc {
fmt.Fprintf(out, "\nWARNING: function deployed but DB insert failed\n")
}

_, _ = fmt.Fprintf(out, "\nYour function is live at: http://%s.localhost\n\n", nameParam)
_, _ = fmt.Fprintf(out, "\nYour function is live at: http://%s\n\n", endpoint)
w.Header().Set("X-Deploy-Status", "OK")
}
}
Expand Down
1 change: 1 addition & 0 deletions internal/api/deploy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ func TestDeployHandler_Success(t *testing.T) {

req := httptest.NewRequest(http.MethodPost, "/functions", body)
req.Header.Set("Content-Type", writer.FormDataContentType())
req.Host = "localhost"

rr := httptest.NewRecorder()

Expand Down
4 changes: 3 additions & 1 deletion internal/sdk/image.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,9 @@ func (d *DockerClient) BuildImage(

_, err = d.cli.ImagePrune(ctx, client.ImagePruneOptions{})
if err != nil {
return err
if !strings.Contains(err.Error(), "prune operation is already running") {
return err
}
}

return nil
Expand Down
32 changes: 21 additions & 11 deletions internal/sdk/image_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -135,30 +135,40 @@ func TestBuildImage_InvalidDirectory(t *testing.T) {
func TestBuildImage_duplicateImageName(t *testing.T) {
t.Parallel()

err := testDocker.PullImage(testCtx, "alpine")
if err != nil {
t.Fatalf("unexpected error pulling alpine image: %v", err)
}
t.Log("Pulled image successfully")
imageName := "test-rebuild-image:latest"

defer func() {
err := testDocker.RemoveImage(testCtx, "alpine:latest")
err := testDocker.RemoveImage(testCtx, imageName)
if err != nil {
t.Logf("failed to remove image: %v", err)
}
}()

tarstream, err := buildcontext.CreateTarStream("../../samples/hello", "node")
tarstream1, err := buildcontext.CreateTarStream("../../samples/hello", "node")
if err != nil {
t.Skipf("unexpected error - failed to create Tar stream: %v", err)
}

err = testDocker.BuildImage(testCtx, "alpine", tarstream, io.Discard)
if err == nil {
t.Fatal("expected error for duplicate image name, got nil")
// First build
err = testDocker.BuildImage(testCtx, imageName, tarstream1, io.Discard)
if err != nil {
t.Fatalf("expected first build to succeed, got error: %v", err)
}
t.Log("first build succeeded")

// Second build with same image name - simulates redeployment
// BuildImage with ForceRemove: true should handle this gracefully
tarstream2, err := buildcontext.CreateTarStream("../../samples/hello", "node")
if err != nil {
t.Skipf("unexpected error - failed to create Tar stream: %v", err)
}

err = testDocker.BuildImage(testCtx, imageName, tarstream2, io.Discard)
if err != nil {
t.Fatalf("expected rebuild with same image name to succeed, got error: %v", err)
}

t.Logf("received expected error: %v", err)
t.Log("successfully rebuilt image with same name")
}

func TestTagImage_Success(t *testing.T) {
Expand Down
Binary file removed samples/hello/hello
Binary file not shown.
Loading