From 446039a3e87638ab038a269fd0e231a5b913f99b Mon Sep 17 00:00:00 2001 From: "Han Verstraete (OpenFaaS Ltd)" Date: Fri, 4 Sep 2026 13:50:10 +0200 Subject: [PATCH 1/2] Use OPENFAAS_NS as the default function namespace Use OPENFAAS_NS when a namespace-aware command has no explicit namespace. This avoids implicitly targeting the cluster default for users who work in another namespace while preserving existing flag and stack precedence. Signed-off-by: Han Verstraete (OpenFaaS Ltd) --- README.md | 1 + commands/deploy.go | 4 +- commands/describe.go | 7 ++-- commands/diff.go | 5 ++- commands/invoke.go | 4 +- commands/list.go | 3 +- commands/logs.go | 2 +- commands/logs_test.go | 29 +++++++++++++++ commands/priority.go | 20 ++++++---- commands/priority_test.go | 77 ++++++++++++++++++++++++++++++++++++++- commands/ready.go | 7 ++-- commands/remove.go | 7 ++-- commands/secret_apply.go | 11 +++--- commands/secret_create.go | 5 ++- commands/secret_list.go | 3 +- commands/secret_remove.go | 3 +- commands/secret_update.go | 3 +- commands/store_deploy.go | 2 +- 18 files changed, 156 insertions(+), 37 deletions(-) diff --git a/README.md b/README.md index df2baee8d..8e42e5bcd 100644 --- a/README.md +++ b/README.md @@ -357,6 +357,7 @@ $ uname -a | curl http://127.0.0.1:8080/function/nodejs-echo--data-binary @- * `OPENFAAS_TEMPLATE_URL` - to set the default URL to pull templates from * `OPENFAAS_PREFIX` - for use with `faas-cli new` - this can act in place of `--prefix` * `OPENFAAS_URL` - to override the default gateway URL +* `OPENFAAS_NS` - to set the default function namespace * `OPENFAAS_REMOTE_BUILDER` - default value for `--remote-builder` * `OPENFAAS_PAYLOAD_SECRET` - default value for `--payload-secret` * `OPENFAAS_BUILDER_PUBLIC_KEY` - builder public key as a literal value, or a path to a file containing raw base64 or the JSON response from `/public-key` diff --git a/commands/deploy.go b/commands/deploy.go index 6967cdd6b..36605042c 100644 --- a/commands/deploy.go +++ b/commands/deploy.go @@ -199,7 +199,7 @@ func runDeployCommand(args []string, image string, fprocess string, functionName // Check if there is a functionNamespace flag passed, if so, override the namespace value // defined in the stack.yaml - function.Namespace = getNamespace(functionNamespace, function.Namespace) + function.Namespace = getNamespace(functionNamespace, function.Namespace, os.Getenv(openFaaSNamespaceEnvironment)) fileEnvironment, err := readFiles(function.EnvironmentFile) if err != nil { @@ -319,7 +319,7 @@ Error: %s`, fprocessErr.Error()) tlsInsecure, defaultReadOnlyRFS, token, - functionNamespace, + getNamespace(functionNamespace, "", os.Getenv(openFaaSNamespaceEnvironment)), cpuRequest, cpuLimit, memoryRequest, diff --git a/commands/describe.go b/commands/describe.go index d090e6775..60bf2f280 100644 --- a/commands/describe.go +++ b/commands/describe.go @@ -83,14 +83,15 @@ func runDescribe(cmd *cobra.Command, args []string) error { } ctx := context.Background() + namespace := getNamespace(functionNamespace, "", os.Getenv(openFaaSNamespaceEnvironment)) - function, err := cliClient.GetFunctionInfo(ctx, functionName, functionNamespace) + function, err := cliClient.GetFunctionInfo(ctx, functionName, namespace) if err != nil { return err } //To get correct value for invocation count from /system/functions endpoint - functionList, err := cliClient.ListFunctions(ctx, functionNamespace) + functionList, err := cliClient.ListFunctions(ctx, namespace) if err != nil { return err } @@ -108,7 +109,7 @@ func runDescribe(cmd *cobra.Command, args []string) error { status = "Ready" } - url, asyncURL := getFunctionURLs(gatewayAddress, functionName, functionNamespace) + url, asyncURL := getFunctionURLs(gatewayAddress, functionName, namespace) funcDesc := schema.FunctionDescription{ FunctionStatus: function, diff --git a/commands/diff.go b/commands/diff.go index cb614ea80..6959e1b59 100644 --- a/commands/diff.go +++ b/commands/diff.go @@ -94,7 +94,8 @@ func runDiff(cmd *cobra.Command, args []string) error { return err } - functions, err := proxyClient.ListFunctions(context.Background(), functionNamespace) + namespace := getNamespace(functionNamespace, "", os.Getenv(openFaaSNamespaceEnvironment)) + functions, err := proxyClient.ListFunctions(context.Background(), namespace) if err != nil { return err } @@ -103,7 +104,7 @@ func runDiff(cmd *cobra.Command, args []string) error { yamlMap := make(map[string]funcDiff) for name, fn := range yamlFns { - key := diffKey(name, namespaceForDiffKey(functionNamespace, fn.Namespace)) + key := diffKey(name, getNamespace(functionNamespace, fn.Namespace, os.Getenv(openFaaSNamespaceEnvironment))) imageName, err := buildDiffImageName(fn.Image, fn.Handler, tagFormat) if err != nil { diff --git a/commands/invoke.go b/commands/invoke.go index 7cc10b40d..1db2b6df9 100644 --- a/commands/invoke.go +++ b/commands/invoke.go @@ -97,7 +97,7 @@ func runInvoke(cmd *cobra.Command, args []string) error { } } } - functionNamespace = getNamespace(functionInvokeNamespace, stackNamespace) + functionNamespace = getNamespace(functionInvokeNamespace, stackNamespace, os.Getenv(openFaaSNamespaceEnvironment)) if missingSignFlag(sigHeader, key) { return fmt.Errorf("signing requires both --sign and --key ") @@ -180,7 +180,7 @@ func runInvoke(cmd *cobra.Command, args []string) error { } req.Header = httpHeader - res, err = client.InvokeFunction(functionName, functionInvokeNamespace, invokeAsync, authenticate, req) + res, err = client.InvokeFunction(functionName, functionNamespace, invokeAsync, authenticate, req) if err != nil { return fmt.Errorf("failed to invoke function: %s", err) } diff --git a/commands/list.go b/commands/list.go index 86856943d..0c3a2ff86 100644 --- a/commands/list.go +++ b/commands/list.go @@ -77,7 +77,8 @@ func runList(cmd *cobra.Command, args []string) error { return err } - functions, err := proxyClient.ListFunctions(context.Background(), functionNamespace) + namespace := getNamespace(functionNamespace, "", os.Getenv(openFaaSNamespaceEnvironment)) + functions, err := proxyClient.ListFunctions(context.Background(), namespace) if err != nil { return err } diff --git a/commands/logs.go b/commands/logs.go index 14840d786..464980a25 100644 --- a/commands/logs.go +++ b/commands/logs.go @@ -179,7 +179,7 @@ func logRequestFromFlags(cmd *cobra.Command, args []string) logs.Request { return logs.Request{ Name: args[0], - Namespace: ns, + Namespace: getNamespace(ns, "", os.Getenv(openFaaSNamespaceEnvironment)), Tail: logFlagValues.lines, Since: sinceValue(logFlagValues.sinceTime.AsTime(), logFlagValues.since), Follow: logFlagValues.tail, diff --git a/commands/logs_test.go b/commands/logs_test.go index e4bba39f9..e6ef52322 100644 --- a/commands/logs_test.go +++ b/commands/logs_test.go @@ -11,6 +11,7 @@ import ( ) func Test_logsCmdFlagParsing(t *testing.T) { + t.Setenv(openFaaSNamespaceEnvironment, "") nowFunc = func() time.Time { ts, _ := time.Parse(time.RFC3339, "2019-01-01T01:00:00Z") return ts @@ -46,6 +47,34 @@ func Test_logsCmdFlagParsing(t *testing.T) { } } +func TestLogsNamespacePrecedence(t *testing.T) { + t.Setenv(openFaaSNamespaceEnvironment, "environment") + + tests := []struct { + name string + args []string + want string + }{ + {name: "environment default", args: []string{"echo"}, want: "environment"}, + {name: "flag override", args: []string{"echo", "--namespace=flag"}, want: "flag"}, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + functionLogsCmd.ResetFlags() + initLogCmdFlags(functionLogsCmd) + if err := functionLogsCmd.ParseFlags(test.args); err != nil { + t.Fatal(err) + } + + request := logRequestFromFlags(functionLogsCmd, functionLogsCmd.Flags().Args()) + if request.Namespace != test.want { + t.Fatalf("want namespace %q, got %q", test.want, request.Namespace) + } + }) + } +} + func strP(s string) *string { return &s } diff --git a/commands/priority.go b/commands/priority.go index 85a995af0..ef1278d87 100644 --- a/commands/priority.go +++ b/commands/priority.go @@ -10,13 +10,14 @@ import ( ) const ( - openFaaSURLEnvironment = "OPENFAAS_URL" - remoteBuilderEnvironment = "OPENFAAS_REMOTE_BUILDER" - payloadSecretEnvironment = "OPENFAAS_PAYLOAD_SECRET" - builderPublicKeyEnvironment = "OPENFAAS_BUILDER_PUBLIC_KEY" - templateURLEnvironment = "OPENFAAS_TEMPLATE_URL" - templateStoreURLEnvironment = "OPENFAAS_TEMPLATE_STORE_URL" - defaultFunctionNamespace = "" + openFaaSURLEnvironment = "OPENFAAS_URL" + openFaaSNamespaceEnvironment = "OPENFAAS_NS" + remoteBuilderEnvironment = "OPENFAAS_REMOTE_BUILDER" + payloadSecretEnvironment = "OPENFAAS_PAYLOAD_SECRET" + builderPublicKeyEnvironment = "OPENFAAS_BUILDER_PUBLIC_KEY" + templateURLEnvironment = "OPENFAAS_TEMPLATE_URL" + templateStoreURLEnvironment = "OPENFAAS_TEMPLATE_STORE_URL" + defaultFunctionNamespace = "" ) func getGatewayURL(argumentURL, defaultURL, yamlURL, environmentURL string) string { @@ -64,7 +65,7 @@ func getTemplateStoreURL(argumentURL, environmentURL, defaultURL string) string } } -func getNamespace(flagNamespace, stackNamespace string) string { +func getNamespace(flagNamespace, stackNamespace, environmentNamespace string) string { // If the namespace flag is passed use it if len(flagNamespace) > 0 { return flagNamespace @@ -73,6 +74,9 @@ func getNamespace(flagNamespace, stackNamespace string) string { if len(stackNamespace) > 0 { return stackNamespace } + if len(environmentNamespace) > 0 { + return environmentNamespace + } return defaultFunctionNamespace diff --git a/commands/priority_test.go b/commands/priority_test.go index 307025037..9164244fa 100644 --- a/commands/priority_test.go +++ b/commands/priority_test.go @@ -1,6 +1,81 @@ package commands -import "testing" +import ( + "os" + "path/filepath" + "testing" + + "github.com/openfaas/go-sdk/stack" +) + +func TestGetNamespacePrecedence(t *testing.T) { + tests := []struct { + name string + flagNamespace string + stackNamespace string + environmentNamespace string + want string + }{ + { + name: "environment namespace is the default", + environmentNamespace: "environment", + want: "environment", + }, + { + name: "stack namespace overrides environment", + stackNamespace: "stack", + environmentNamespace: "environment", + want: "stack", + }, + { + name: "flag overrides stack and environment", + flagNamespace: "flag", + stackNamespace: "stack", + environmentNamespace: "environment", + want: "flag", + }, + { + name: "empty values retain the existing default", + want: defaultFunctionNamespace, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got := getNamespace(test.flagNamespace, test.stackNamespace, test.environmentNamespace) + if got != test.want { + t.Fatalf("want namespace %q, got %q", test.want, got) + } + }) + } +} + +func TestGetNamespaceUsesSubstitutedStackNamespaceBeforeEnvironment(t *testing.T) { + t.Setenv("STACK_NAMESPACE", "substituted-stack") + path := filepath.Join(t.TempDir(), "stack.yaml") + contents := `version: 1.0 +provider: + name: openfaas + gateway: http://127.0.0.1:8080 +functions: + echo: + image: echo:latest + namespace: ${STACK_NAMESPACE} +` + if err := os.WriteFile(path, []byte(contents), 0o600); err != nil { + t.Fatal(err) + } + + services, err := stack.ParseYAMLFile(path, "", "", true) + if err != nil { + t.Fatal(err) + } + + got := getNamespace("", services.Functions["echo"].Namespace, "environment") + if got != "substituted-stack" { + t.Fatalf("want substituted stack namespace, got %q", got) + } +} func TestApplyRemoteBuilderEnvironmentUsesEnvFallbacks(t *testing.T) { t.Setenv(remoteBuilderEnvironment, "http://builder.example.com") diff --git a/commands/ready.go b/commands/ready.go index 6cf049143..e621bd65f 100644 --- a/commands/ready.go +++ b/commands/ready.go @@ -128,16 +128,17 @@ func runReadyCmd(cmd *cobra.Command, args []string) error { } ctx := context.Background() + namespace := getNamespace(functionNamespace, "", os.Getenv(openFaaSNamespaceEnvironment)) for i := 0; i < attempts; i++ { suffix := "" - if len(functionNamespace) > 0 { - suffix = "." + functionNamespace + if len(namespace) > 0 { + suffix = "." + namespace } fmt.Printf("[%d/%d] Waiting for function %s%s\n", i+1, attempts, functionName, suffix) - function, err := cliClient.GetFunctionInfo(ctx, functionName, functionNamespace) + function, err := cliClient.GetFunctionInfo(ctx, functionName, namespace) if err != nil { fmt.Printf("[%d/%d] Error getting function info: %s\n", i+1, attempts, err.Error()) } diff --git a/commands/remove.go b/commands/remove.go index f824a6484..9b66cdf8d 100644 --- a/commands/remove.go +++ b/commands/remove.go @@ -74,7 +74,7 @@ func runDelete(cmd *cobra.Command, args []string) error { if len(services.Functions) > 0 { for k, function := range services.Functions { - function.Namespace = getNamespace(functionNamespace, function.Namespace) + function.Namespace = getNamespace(functionNamespace, function.Namespace, os.Getenv(openFaaSNamespaceEnvironment)) function.Name = k fmt.Printf("Deleting: %s.%s\n", function.Name, function.Namespace) @@ -86,8 +86,9 @@ func runDelete(cmd *cobra.Command, args []string) error { } functionName = args[0] - fmt.Printf("Deleting: %s.%s\n", functionName, functionNamespace) - err := proxyclient.DeleteFunction(ctx, functionName, functionNamespace) + namespace := getNamespace(functionNamespace, "", os.Getenv(openFaaSNamespaceEnvironment)) + fmt.Printf("Deleting: %s.%s\n", functionName, namespace) + err := proxyclient.DeleteFunction(ctx, functionName, namespace) if err != nil { return err } diff --git a/commands/secret_apply.go b/commands/secret_apply.go index 6f14e6eae..5bf7bf54c 100644 --- a/commands/secret_apply.go +++ b/commands/secret_apply.go @@ -42,6 +42,7 @@ func init() { } func runSecretApply(cmd *cobra.Command, args []string) error { + namespace := getNamespace(functionNamespace, "", os.Getenv(openFaaSNamespaceEnvironment)) gatewayAddress := getGatewayURL(gateway, defaultGateway, "", os.Getenv(openFaaSURLEnvironment)) if msg := checkTLSInsecure(gatewayAddress, tlsInsecure); len(msg) > 0 { @@ -81,7 +82,7 @@ func runSecretApply(cmd *cobra.Command, args []string) error { } // Get list of existing secrets from gateway to check if they exist - existingSecrets, err := client.GetSecretList(context.Background(), functionNamespace) + existingSecrets, err := client.GetSecretList(context.Background(), namespace) if err != nil { return fmt.Errorf("failed to get secret list: %w", err) } @@ -90,7 +91,7 @@ func runSecretApply(cmd *cobra.Command, args []string) error { secretMap := make(map[string]bool) for _, secret := range existingSecrets { // Match by name and namespace - if secret.Namespace == functionNamespace { + if secret.Namespace == namespace { secretMap[secret.Name] = true } } @@ -130,7 +131,7 @@ func runSecretApply(cmd *cobra.Command, args []string) error { secret := types.Secret{ Name: secretName, - Namespace: functionNamespace, + Namespace: namespace, Value: secretValue, RawValue: fileData, } @@ -140,7 +141,7 @@ func runSecretApply(cmd *cobra.Command, args []string) error { fmt.Printf("Secret %s exists, deleting before recreating...\n", secretName) deleteSecret := types.Secret{ Name: secretName, - Namespace: functionNamespace, + Namespace: namespace, } err = client.RemoveSecret(context.Background(), deleteSecret) if err != nil { @@ -150,7 +151,7 @@ func runSecretApply(cmd *cobra.Command, args []string) error { } // Create the secret - fmt.Printf("Creating secret: %s.%s\n", secret.Name, functionNamespace) + fmt.Printf("Creating secret: %s.%s\n", secret.Name, namespace) status, output := client.CreateSecret(context.Background(), secret) if status == http.StatusConflict { diff --git a/commands/secret_create.go b/commands/secret_create.go index d5d2a7c1e..4fbd9e908 100644 --- a/commands/secret_create.go +++ b/commands/secret_create.go @@ -91,9 +91,10 @@ func preRunSecretCreate(cmd *cobra.Command, args []string) error { } func runSecretCreate(cmd *cobra.Command, args []string) error { + namespace := getNamespace(functionNamespace, "", os.Getenv(openFaaSNamespaceEnvironment)) secret := types.Secret{ Name: args[0], - Namespace: functionNamespace, + Namespace: namespace, } switch { @@ -146,7 +147,7 @@ func runSecretCreate(cmd *cobra.Command, args []string) error { return err } - fmt.Printf("Creating secret: %s.%s\n", secret.Name, functionNamespace) + fmt.Printf("Creating secret: %s.%s\n", secret.Name, namespace) status, output := client.CreateSecret(context.Background(), secret) if status == http.StatusConflict && replaceSecret { fmt.Printf("Secret %s already exists, updating...\n", secret.Name) diff --git a/commands/secret_list.go b/commands/secret_list.go index 9cde237d6..4e432afa3 100644 --- a/commands/secret_list.go +++ b/commands/secret_list.go @@ -63,7 +63,8 @@ func runSecretList(cmd *cobra.Command, args []string) error { return err } - secrets, err := client.GetSecretList(context.Background(), functionNamespace) + namespace := getNamespace(functionNamespace, "", os.Getenv(openFaaSNamespaceEnvironment)) + secrets, err := client.GetSecretList(context.Background(), namespace) if err != nil { return err } diff --git a/commands/secret_remove.go b/commands/secret_remove.go index 1875e7cef..0a5d92850 100644 --- a/commands/secret_remove.go +++ b/commands/secret_remove.go @@ -44,6 +44,7 @@ func preRunSecretRemoveCmd(cmd *cobra.Command, args []string) error { } func runSecretRemove(cmd *cobra.Command, args []string) error { + namespace := getNamespace(functionNamespace, "", os.Getenv(openFaaSNamespaceEnvironment)) var gatewayAddress string gatewayAddress = getGatewayURL(gateway, defaultGateway, "", os.Getenv(openFaaSURLEnvironment)) @@ -53,7 +54,7 @@ func runSecretRemove(cmd *cobra.Command, args []string) error { secret := types.Secret{ Name: args[0], - Namespace: functionNamespace, + Namespace: namespace, } cliAuth, err := proxy.NewCLIAuth(token, gatewayAddress) diff --git a/commands/secret_update.go b/commands/secret_update.go index c253583bb..ebc869890 100644 --- a/commands/secret_update.go +++ b/commands/secret_update.go @@ -58,6 +58,7 @@ func preRunSecretUpdate(cmd *cobra.Command, args []string) error { } func runSecretUpdate(cmd *cobra.Command, args []string) error { + namespace := getNamespace(functionNamespace, "", os.Getenv(openFaaSNamespaceEnvironment)) gatewayAddress := getGatewayURL(gateway, defaultGateway, "", os.Getenv(openFaaSURLEnvironment)) if msg := checkTLSInsecure(gatewayAddress, tlsInsecure); len(msg) > 0 { @@ -66,7 +67,7 @@ func runSecretUpdate(cmd *cobra.Command, args []string) error { secret := types.Secret{ Name: args[0], - Namespace: functionNamespace, + Namespace: namespace, } switch { diff --git a/commands/store_deploy.go b/commands/store_deploy.go index 321aa6b26..ec57f8c1b 100644 --- a/commands/store_deploy.go +++ b/commands/store_deploy.go @@ -156,7 +156,7 @@ func runStoreDeploy(cmd *cobra.Command, args []string) error { tlsInsecure, item.ReadOnlyRootFilesystem, token, - functionNamespace, + getNamespace(functionNamespace, "", os.Getenv(openFaaSNamespaceEnvironment)), cpuRequest, cpuLimit, memoryRequest, From 1259b6daf977e21d997af512d4f5211f8a92ec3a Mon Sep 17 00:00:00 2001 From: "Han Verstraete (OpenFaaS Ltd)" Date: Fri, 4 Sep 2026 13:50:14 +0200 Subject: [PATCH 2/2] Respect function namespaces when diffing stacks Query every namespace resolved by the functions in a stack instead of listing functions from only one namespace. Key each deployed function by the namespace used for its query so a same-named function in another namespace cannot satisfy the wrong stack entry. Signed-off-by: Han Verstraete (OpenFaaS Ltd) --- commands/diff.go | 72 +++++++++++++++---------- commands/diff_test.go | 120 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 163 insertions(+), 29 deletions(-) diff --git a/commands/diff.go b/commands/diff.go index 6959e1b59..3d37d89bc 100644 --- a/commands/diff.go +++ b/commands/diff.go @@ -94,12 +94,6 @@ func runDiff(cmd *cobra.Command, args []string) error { return err } - namespace := getNamespace(functionNamespace, "", os.Getenv(openFaaSNamespaceEnvironment)) - functions, err := proxyClient.ListFunctions(context.Background(), namespace) - if err != nil { - return err - } - yamlFns := parsedServices.Functions yamlMap := make(map[string]funcDiff) @@ -130,27 +124,19 @@ func runDiff(cmd *cobra.Command, args []string) error { } deployedMap := make(map[string]funcDiff) - for _, fn := range functions { - envMap := fn.EnvVars - if envMap == nil { - envMap = make(map[string]string) + ctx := context.Background() + for _, namespace := range namespacesForDiff(functionNamespace, parsedServices.Functions, os.Getenv(openFaaSNamespaceEnvironment)) { + deployed, err := proxyClient.ListFunctions(ctx, namespace) + if err != nil { + return err } - fnDiff := funcDiff{ - Image: fn.Image, - FProcess: fn.EnvProcess, - Env: envMap, - Secrets: fn.Secrets, - Constraints: fn.Constraints, - Labels: mapFromPtr(fn.Labels), - Annotations: mapFromPtr(fn.Annotations), - Limits: resourcesFromStatus(fn.Limits), - Requests: resourcesFromStatus(fn.Requests), - ReadOnlyRootFilesystem: fn.ReadOnlyRootFilesystem, - } - deployedMap[diffKey(fn.Name, "")] = fnDiff - if fn.Namespace != "" { - deployedMap[diffKey(fn.Name, fn.Namespace)] = fnDiff + for _, fn := range deployed { + key := diffKey(fn.Name, namespace) + if _, ok := yamlMap[key]; !ok { + continue + } + deployedMap[key] = functionStatusToDiff(fn) } } @@ -178,6 +164,26 @@ func runDiff(cmd *cobra.Command, args []string) error { return fmt.Errorf("differences found") } +func functionStatusToDiff(fn types.FunctionStatus) funcDiff { + env := fn.EnvVars + if env == nil { + env = make(map[string]string) + } + + return funcDiff{ + Image: fn.Image, + FProcess: fn.EnvProcess, + Env: env, + Secrets: fn.Secrets, + Constraints: fn.Constraints, + Labels: mapFromPtr(fn.Labels), + Annotations: mapFromPtr(fn.Annotations), + Limits: resourcesFromStatus(fn.Limits), + Requests: resourcesFromStatus(fn.Requests), + ReadOnlyRootFilesystem: fn.ReadOnlyRootFilesystem, + } +} + func buildDiffImageName(image string, handler string, tagMode schema.BuildFormat) (string, error) { branch, version, err := builder.GetImageTagValues(tagMode, handler) if err != nil { @@ -435,11 +441,19 @@ func sortedAttrKeys(m map[string]string) []string { return keys } -func namespaceForDiffKey(flagNamespace string, stackNamespace string) string { - if flagNamespace != "" { - return flagNamespace +func namespacesForDiff(flagNamespace string, functions map[string]stack.Function, environmentNamespace string) []string { + namespaces := map[string]struct{}{} + for _, function := range functions { + namespace := getNamespace(flagNamespace, function.Namespace, environmentNamespace) + namespaces[namespace] = struct{}{} + } + + result := make([]string, 0, len(namespaces)) + for namespace := range namespaces { + result = append(result, namespace) } - return stackNamespace + sort.Strings(result) + return result } func diffKey(name string, namespace string) string { diff --git a/commands/diff_test.go b/commands/diff_test.go index b0ce68057..7ffcc2d6c 100644 --- a/commands/diff_test.go +++ b/commands/diff_test.go @@ -4,6 +4,7 @@ import ( "net/http" "os" "path/filepath" + "reflect" "strings" "testing" @@ -11,6 +12,7 @@ import ( "github.com/openfaas/faas-cli/schema" "github.com/openfaas/faas-cli/test" types "github.com/openfaas/faas-provider/types" + "github.com/openfaas/go-sdk/stack" ) func Test_diff_no_changes(t *testing.T) { @@ -126,6 +128,124 @@ functions: } } +func TestNamespacesForDiffPrecedence(t *testing.T) { + functions := map[string]stack.Function{ + "first": {Namespace: "stack-a"}, + "second": {Namespace: "stack-b"}, + "third": {}, + } + + got := namespacesForDiff("", functions, "environment") + want := []string{"environment", "stack-a", "stack-b"} + if !reflect.DeepEqual(got, want) { + t.Fatalf("want namespaces %v, got %v", want, got) + } + + got = namespacesForDiff("flag", functions, "environment") + want = []string{"flag"} + if !reflect.DeepEqual(got, want) { + t.Fatalf("want flag namespace %v, got %v", want, got) + } +} + +func TestDiffDoesNotMatchDefaultFunctionFromAnotherNamespace(t *testing.T) { + t.Setenv(openFaaSNamespaceEnvironment, "") + yamlPath := filepath.Join(t.TempDir(), "stack.yaml") + yamlContent := `version: 1.0 +provider: + name: openfaas + gateway: http://127.0.0.1:8080 +functions: + echo: + image: ttl.sh/test/echo:latest + worker: + image: ttl.sh/test/worker:latest + namespace: team-a +` + if err := os.WriteFile(yamlPath, []byte(yamlContent), 0o600); err != nil { + t.Fatal(err) + } + + s := test.MockHttpServer(t, []test.Request{ + { + Method: http.MethodGet, + Uri: "/system/functions", + ResponseStatusCode: http.StatusOK, + ResponseBody: []types.FunctionStatus{}, + }, + { + Method: http.MethodGet, + Uri: "/system/functions?namespace=team-a", + ResponseStatusCode: http.StatusOK, + ResponseBody: []types.FunctionStatus{ + {Name: "echo", Namespace: "team-a", Image: "ttl.sh/test/echo:latest"}, + {Name: "worker", Namespace: "team-a", Image: "ttl.sh/test/worker:latest"}, + }, + }, + }) + defer s.Close() + + resetForTest() + var executeErr error + stdOut := test.CaptureStdout(func() { + faasCmd.SetArgs([]string{"diff", "--yaml", yamlPath, "--gateway", s.URL}) + executeErr = faasCmd.Execute() + }) + + if executeErr == nil || !strings.Contains(executeErr.Error(), "differences found") { + t.Fatalf("want missing default function to produce a difference, got: %v", executeErr) + } + if !strings.Contains(stdOut, "echo") || !strings.Contains(stdOut, "not deployed") { + t.Fatalf("want default namespace echo to be reported as not deployed, got:\n%s", stdOut) + } + if strings.Contains(stdOut, "worker.team-a") { + t.Fatalf("want matching namespaced worker to be omitted from diff, got:\n%s", stdOut) + } +} + +func TestDiffUsesQueriedNamespaceWhenStatusOmitsNamespace(t *testing.T) { + t.Setenv(openFaaSNamespaceEnvironment, "") + yamlPath := filepath.Join(t.TempDir(), "stack.yaml") + yamlContent := `version: 1.0 +provider: + name: openfaas + gateway: http://127.0.0.1:8080 +functions: + worker: + image: ttl.sh/test/worker:latest + namespace: team-a +` + if err := os.WriteFile(yamlPath, []byte(yamlContent), 0o600); err != nil { + t.Fatal(err) + } + + s := test.MockHttpServer(t, []test.Request{ + { + Method: http.MethodGet, + Uri: "/system/functions?namespace=team-a", + ResponseStatusCode: http.StatusOK, + ResponseBody: []types.FunctionStatus{ + {Name: "worker", Image: "ttl.sh/test/worker:latest"}, + }, + }, + }) + defer s.Close() + + resetForTest() + var executeErr error + stdOut := test.CaptureStdout(func() { + faasCmd.SetArgs([]string{"diff", "--yaml", yamlPath, "--gateway", s.URL}) + executeErr = faasCmd.Execute() + }) + + if executeErr != nil { + t.Fatalf("want queried namespace to match stack function, got: %v\n%s", executeErr, stdOut) + } + if !strings.Contains(stdOut, "no differences found") { + t.Fatalf("want no differences, got:\n%s", stdOut) + } +} + func Test_diff_image_changed(t *testing.T) { tmpDir := t.TempDir() yamlPath := filepath.Join(tmpDir, "stack.yaml")