diff --git a/commands/deploy.go b/commands/deploy.go index d2a32038..1ff9e539 100644 --- a/commands/deploy.go +++ b/commands/deploy.go @@ -23,6 +23,7 @@ import ( "encoding/json" "errors" "fmt" + "io/ioutil" "os" "os/exec" "path/filepath" @@ -34,6 +35,7 @@ import ( client "github.com/fnproject/cli/client" common "github.com/fnproject/cli/common" + config "github.com/fnproject/cli/config" apps "github.com/fnproject/cli/objects/app" function "github.com/fnproject/cli/objects/fn" trigger "github.com/fnproject/cli/objects/trigger" @@ -42,6 +44,7 @@ import ( "github.com/oracle/oci-go-sdk/v65/artifacts" ociCommon "github.com/oracle/oci-go-sdk/v65/common" "github.com/oracle/oci-go-sdk/v65/keymanagement" + "github.com/spf13/viper" "github.com/urfave/cli" ) @@ -344,6 +347,10 @@ func (p *deploycmd) deployFuncV20180708(c *cli.Context, app *models.App, funcfil funcfile.Name = filepath.Base(filepath.Dir(funcfilePath)) // todo: should probably make a copy of ff before changing it } + if funcfile.Code_only { + return p.deployCodeOnlyFunc(c, app, funcfilePath, funcfile) + } + oracleProvider, _ := getOracleProvider() if oracleProvider != nil && oracleProvider.ImageCompartmentID != "" { // If the provider is Oracle and ImageCompartmentID is present, we need to deploy image to the ImageCompartmentID. @@ -411,6 +418,105 @@ func (p *deploycmd) deployFuncV20180708(c *cli.Context, app *models.App, funcfil return p.updateFunction(c, app.ID, funcfile) } +func (p *deploycmd) deployCodeOnlyFunc(c *cli.Context, app *models.App, funcfilePath string, funcfile *common.FuncFileV20180708) error { + if !p.noBump { + funcfile2, err := common.BumpItV20180708(funcfilePath, common.Patch) + if err != nil { + return err + } + funcfile.Version = funcfile2.Version + } + + dir := filepath.Dir(funcfilePath) + archivePath, err := buildCodeOnlyArchive(dir, funcfile) + if err != nil { + return err + } + + fn := &models.Fn{} + if err := function.WithFuncFileV20180708(funcfile, fn); err != nil { + return fmt.Errorf("Error getting function with funcfile: %s", err) + } + fn.Name = funcfile.Name + fn.CodeOnly = true + fn.Handler = strings.TrimSpace(funcfile.Handler) + if funcfile.Runtime_config != nil { + fn.RuntimeName = strings.TrimSpace(funcfile.Runtime_config.Runtime_name) + fn.RuntimeVersionID = strings.TrimSpace(funcfile.Runtime_config.Runtime_version_id) + fn.RuntimeConfigType = normalizeRuntimeConfigTypeForDeploy(funcfile.Runtime_config.Type) + } + + bucket, namespace, configured, err := resolveCodeOnlyDeployTargetFromContext() + if err != nil { + return err + } + if configured { + objectName, err := pushCodeOnlyArchive(funcfile) + if err != nil { + return err + } + fn.SourceType = "object-storage" + fn.SourceBucketName = bucket + fn.SourceNamespace = namespace + fn.SourceObjectName = objectName + fn.SourceObjectVersion = "" + } else { + archiveBytes, err := ioutil.ReadFile(archivePath) + if err != nil { + return err + } + fn.SourceType = "direct" + fn.SourceFile = archivePath + fn.SourceArchive = archiveBytes + } + + return p.createOrUpdateCodeOnlyFunction(app.ID, fn) +} + +func (p *deploycmd) createOrUpdateCodeOnlyFunction(appID string, fn *models.Fn) error { + fnRes, err := function.GetFnByName(p.clientV2, appID, fn.Name) + if _, ok := err.(function.NameNotFoundError); ok { + created, err := function.CreateFn(p.clientV2, appID, fn) + if err != nil { + return err + } + fn.ID = created.ID + } else if err != nil { + return err + } else { + fn.ID = fnRes.ID + if err := function.PutFn(p.clientV2, fn.ID, fn); err != nil { + return err + } + } + return nil +} + +func normalizeRuntimeConfigTypeForDeploy(value string) string { + v := strings.ToLower(strings.TrimSpace(value)) + switch v { + case "function-update", "function_update": + return "FUNCTION_UPDATE" + case "manual": + return "MANUAL" + default: + return strings.ToUpper(strings.ReplaceAll(v, "-", "_")) + } +} + +func resolveCodeOnlyDeployTargetFromContext() (bucket, namespace string, configured bool, err error) { + contextName := viper.GetString(config.CurrentContext) + contextPath := filepath.Join(config.GetHomeDir(), ".fn", "contexts", contextName+".yaml") + ctxFile, err := config.NewContextFile(contextPath) + if err != nil { + return "", "", false, err + } + bucket = strings.TrimSpace(ctxFile.ObjectStorageBucketName) + namespace = strings.TrimSpace(ctxFile.ObjectStorageNamespace) + configured = bucket != "" && namespace != "" + return bucket, namespace, configured, nil +} + func (p *deploycmd) updateFunction(c *cli.Context, appID string, ff *common.FuncFileV20180708) error { fmt.Printf("Updating function %s using image %s...\n", ff.Name, ff.ImageNameV20180708()) diff --git a/commands/deploy_test.go b/commands/deploy_test.go new file mode 100644 index 00000000..1d30d4cc --- /dev/null +++ b/commands/deploy_test.go @@ -0,0 +1,111 @@ +package commands + +import ( + "os" + "path/filepath" + "testing" + + "github.com/fnproject/cli/config" + "github.com/spf13/viper" +) + +func TestNormalizeRuntimeConfigTypeForDeploy(t *testing.T) { + tests := []struct { + name string + input string + want string + }{ + {name: "function-update hyphen", input: "function-update", want: "FUNCTION_UPDATE"}, + {name: "function_update underscore", input: "function_update", want: "FUNCTION_UPDATE"}, + {name: "manual", input: "manual", want: "MANUAL"}, + {name: "already upper", input: "FUNCTION_UPDATE", want: "FUNCTION_UPDATE"}, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + got := normalizeRuntimeConfigTypeForDeploy(tc.input) + if got != tc.want { + t.Fatalf("normalizeRuntimeConfigTypeForDeploy(%q) = %q, want %q", tc.input, got, tc.want) + } + }) + } +} + +func TestResolveCodeOnlyDeployTargetFromContext(t *testing.T) { + t.Run("configured context should return bucket namespace and configured true", func(t *testing.T) { + oldHome := os.Getenv("HOME") + defer func() { _ = os.Setenv("HOME", oldHome) }() + + tmpHome := t.TempDir() + if err := os.Setenv("HOME", tmpHome); err != nil { + t.Fatalf("failed to set HOME: %v", err) + } + + contextsDir := filepath.Join(tmpHome, ".fn", "contexts") + if err := os.MkdirAll(contextsDir, 0755); err != nil { + t.Fatalf("failed to create contexts dir: %v", err) + } + + contextName := "testctx" + contextPath := filepath.Join(contextsDir, contextName+".yaml") + content := []byte("provider: oracle\nobject_storage_bucket_name: code-only-test-files\nnamespace: oraclefunctionsdevelopm\n") + if err := os.WriteFile(contextPath, content, 0644); err != nil { + t.Fatalf("failed to write context file: %v", err) + } + + oldContext := viper.GetString(config.CurrentContext) + defer viper.Set(config.CurrentContext, oldContext) + viper.Set(config.CurrentContext, contextName) + + bucket, namespace, configured, err := resolveCodeOnlyDeployTargetFromContext() + if err != nil { + t.Fatalf("resolveCodeOnlyDeployTargetFromContext returned error: %v", err) + } + if bucket != "code-only-test-files" { + t.Fatalf("bucket = %q, want %q", bucket, "code-only-test-files") + } + if namespace != "oraclefunctionsdevelopm" { + t.Fatalf("namespace = %q, want %q", namespace, "oraclefunctionsdevelopm") + } + if !configured { + t.Fatal("configured = false, want true") + } + }) + + t.Run("missing bucket or namespace should return configured false", func(t *testing.T) { + oldHome := os.Getenv("HOME") + defer func() { _ = os.Setenv("HOME", oldHome) }() + + tmpHome := t.TempDir() + if err := os.Setenv("HOME", tmpHome); err != nil { + t.Fatalf("failed to set HOME: %v", err) + } + + contextsDir := filepath.Join(tmpHome, ".fn", "contexts") + if err := os.MkdirAll(contextsDir, 0755); err != nil { + t.Fatalf("failed to create contexts dir: %v", err) + } + + contextName := "emptyctx" + contextPath := filepath.Join(contextsDir, contextName+".yaml") + content := []byte("provider: oracle\n") + if err := os.WriteFile(contextPath, content, 0644); err != nil { + t.Fatalf("failed to write context file: %v", err) + } + + oldContext := viper.GetString(config.CurrentContext) + defer viper.Set(config.CurrentContext, oldContext) + viper.Set(config.CurrentContext, contextName) + + bucket, namespace, configured, err := resolveCodeOnlyDeployTargetFromContext() + if err != nil { + t.Fatalf("resolveCodeOnlyDeployTargetFromContext returned error: %v", err) + } + if bucket != "" || namespace != "" { + t.Fatalf("expected empty bucket/namespace, got %q / %q", bucket, namespace) + } + if configured { + t.Fatal("configured = true, want false") + } + }) +} \ No newline at end of file