diff --git a/internal/application/create_environment_test.go b/internal/application/create_environment_test.go new file mode 100644 index 00000000..de6ee3ae --- /dev/null +++ b/internal/application/create_environment_test.go @@ -0,0 +1,80 @@ +package application + +import ( + "context" + "io" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/nais/naistrix" +) + +func TestCloudResourceCreateEnvironment(t *testing.T) { + for _, resource := range []string{"valkey", "opensearch"} { + for _, tt := range []struct { + name, environment, configured string + rejected bool + }{ + {name: "dev FSS", environment: "dev-fss", rejected: true}, + {name: "prod FSS", environment: "prod-fss", rejected: true}, + {name: "FSS default", configured: "dev-fss", rejected: true}, + {name: "cloud environment", environment: "dev-gcp"}, + {name: "cloud default", configured: "prod-gcp"}, + {name: "explicit cloud overrides FSS default", environment: "dev-gcp", configured: "dev-fss"}, + {name: "explicit FSS overrides cloud default", environment: "prod-fss", configured: "prod-gcp", rejected: true}, + {name: "non NAV name", environment: "staging"}, + } { + t.Run(resource+"/"+tt.name, func(t *testing.T) { + t.Setenv("NAIS_ENVIRONMENT", "") + config := filepath.Join(t.TempDir(), "config.yaml") + contents := "{}\n" + if tt.configured != "" { + contents = "environment: " + tt.configured + "\n" + } + if err := os.WriteFile(config, []byte(contents), 0o600); err != nil { + t.Fatal(err) + } + app, _, err := New(io.Discard) + if err != nil { + t.Fatal(err) + } + ran := false + for _, cmd := range app.Commands { + if cmd.Name != resource { + continue + } + for _, sub := range cmd.SubCommands { + if sub.Name == "create" { + sub.RunFunc = func(context.Context, *naistrix.Arguments, *naistrix.OutputWriter) error { + ran = true + return nil + } + } + } + } + args := []string{"--config", config, resource, "create", "instance", "-t", "my-team"} + if tt.environment != "" { + args = append(args, "-e", tt.environment) + } + err = app.Run(naistrix.RunWithArgs(args)) + if tt.rejected { + if err == nil || !strings.Contains(err.Error(), "is not supported in environment") { + t.Errorf("error = %v, want unsupported environment", err) + } + if ran { + t.Error("creation reached execution in an unsupported environment") + } + } else { + if err != nil { + t.Fatal(err) + } + if !ran { + t.Error("supported environment did not reach execution") + } + } + }) + } + } +} diff --git a/internal/flags/environment.go b/internal/flags/environment.go index dfbf8dda..5d43133b 100644 --- a/internal/flags/environment.go +++ b/internal/flags/environment.go @@ -3,6 +3,7 @@ package flags import ( "context" "fmt" + "strings" "github.com/nais/cli/internal/naisapi" "github.com/nais/naistrix" @@ -10,6 +11,11 @@ import ( type Environment string +// IsOnPrem reports whether the environment uses NAV's on-premises naming convention. +func (e Environment) IsOnPrem() bool { + return strings.HasSuffix(string(e), "-fss") +} + var _ naistrix.FlagAutoCompleter = (*Environment)(nil) func (e *Environment) AutoComplete(ctx context.Context, _ *naistrix.Arguments, _ string, _ any) ([]string, string) { diff --git a/internal/flags/environment_test.go b/internal/flags/environment_test.go new file mode 100644 index 00000000..91fc1159 --- /dev/null +++ b/internal/flags/environment_test.go @@ -0,0 +1,24 @@ +package flags + +import "testing" + +func TestEnvironmentIsOnPrem(t *testing.T) { + for _, tt := range []struct { + name Environment + onPrem bool + }{ + {name: "dev-fss", onPrem: true}, + {name: "prod-fss", onPrem: true}, + {name: "dev-gcp"}, + {name: "prod-gcp"}, + {name: "staging"}, + {name: "fss-test"}, + {name: ""}, + } { + t.Run(string(tt.name), func(t *testing.T) { + if got := tt.name.IsOnPrem(); got != tt.onPrem { + t.Errorf("IsOnPrem() = %v, want %v", got, tt.onPrem) + } + }) + } +} diff --git a/internal/opensearch/command/flag/flag.go b/internal/opensearch/command/flag/flag.go index 14c14e6b..153b163b 100644 --- a/internal/opensearch/command/flag/flag.go +++ b/internal/opensearch/command/flag/flag.go @@ -29,6 +29,9 @@ type Create struct { } func (c *Create) Validate() error { + if c.Environment.IsOnPrem() { + return fmt.Errorf("OpenSearch is not supported in environment %q; choose an environment without the -fss suffix", c.Environment) + } if c.Memory != "" && !c.Memory.IsValid() { return fmt.Errorf("invalid memory %q, must be one of: %v", c.Memory, gql.AllOpenSearchMemory) } diff --git a/internal/postgres/command/environment.go b/internal/postgres/command/environment.go index 23e184ad..c7d6c555 100644 --- a/internal/postgres/command/environment.go +++ b/internal/postgres/command/environment.go @@ -4,9 +4,11 @@ import ( "context" "errors" "fmt" + "slices" "sort" "strings" + "github.com/nais/cli/internal/flags" "github.com/nais/cli/internal/naisapi" "github.com/nais/cli/internal/postgres" "github.com/nais/cli/internal/postgres/command/flag" @@ -128,11 +130,25 @@ func autoCompletePostgresBranches(f *flag.Postgres) naistrix.AutoCompleteFunc { } } +func postgresCreateEnvironments(ctx context.Context) ([]string, error) { + envs, err := naisapi.GetAllEnvironments(ctx) + if err != nil { + return nil, err + } + // NAV's on-premises environments do not support Nais Postgres. + return slices.DeleteFunc(envs, func(env string) bool { + return flags.Environment(env).IsOnPrem() + }), nil +} + func resolvePostgresCreateEnvironment(ctx context.Context, provided string) (string, error) { if provided != "" { + if flags.Environment(provided).IsOnPrem() { + return "", fmt.Errorf("Postgres is not supported in environment %q; choose an environment without the -fss suffix", provided) + } return provided, nil } - envs, err := naisapi.GetAllEnvironments(ctx) + envs, err := postgresCreateEnvironments(ctx) if err != nil { return "", fmt.Errorf("fetching environments: %w", err) } diff --git a/internal/postgres/command/instance_create_environment_test.go b/internal/postgres/command/instance_create_environment_test.go index 222238a1..6346b0a4 100644 --- a/internal/postgres/command/instance_create_environment_test.go +++ b/internal/postgres/command/instance_create_environment_test.go @@ -18,6 +18,23 @@ import ( "github.com/nais/naistrix" ) +func TestPostgresCreateEnvironmentsExcludesFSS(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`{"data":{"environments":{"nodes":[{"name":"dev-fss"},{"name":"prod-gcp"},{"name":"prod-fss"},{"name":"dev-gcp"},{"name":"staging"},{"name":"fss-test"}]}}}`)) + })) + defer server.Close() + t.Setenv("NAIS_API_LOCAL_HOST", strings.TrimPrefix(server.URL, "http://")) + got, err := postgresCreateEnvironments(t.Context()) + if err != nil { + t.Fatal(err) + } + want := []string{"prod-gcp", "dev-gcp", "staging", "fss-test"} + if !slices.Equal(got, want) { + t.Errorf("Postgres create environment choices = %v, want %v", got, want) + } +} + func TestPostgresCreateEnvironment(t *testing.T) { stdin, err := os.Open(os.DevNull) if err != nil { @@ -39,13 +56,18 @@ func TestPostgresCreateEnvironment(t *testing.T) { wantOperations []string }{ {name: "explicit environment bypasses discovery", args: []string{"-t", "my-team", "-e", "dev-gcp", "--yes"}, wantOperations: []string{"CreatePostgres"}}, + {name: "explicit dev-fss is rejected before API", args: []string{"-t", "my-team", "-e", "dev-fss", "--yes"}, wantError: `Postgres is not supported in environment "dev-fss"`}, + {name: "explicit prod-fss is rejected before API", args: []string{"-t", "my-team", "-e", "prod-fss", "--yes"}, wantError: `Postgres is not supported in environment "prod-fss"`}, + {name: "default FSS environment is rejected before API", config: "environment: dev-fss\n", args: []string{"-t", "my-team", "--yes"}, wantError: `Postgres is not supported in environment "dev-fss"`}, {name: "default environment bypasses discovery", config: "environment: dev-gcp\n", args: []string{"-t", "my-team", "--yes"}, wantOperations: []string{"CreatePostgres"}}, {name: "explicit environment overrides default", config: "environment: prod-gcp\n", args: []string{"-t", "my-team", "-e", "dev-gcp", "--yes"}, wantOperations: []string{"CreatePostgres"}}, + {name: "explicit supported environment overrides FSS default", config: "environment: dev-fss\n", args: []string{"-t", "my-team", "-e", "dev-gcp", "--yes"}, wantOperations: []string{"CreatePostgres"}}, {name: "multiple environments require a choice", args: []string{"-t", "my-team"}, response: `{"data":{"environments":{"nodes":[{"name":"prod-gcp"},{"name":"dev-gcp"}]}}}`, wantError: hint, wantOperations: []string{"Environments"}}, {name: "yes does not choose from multiple environments", args: []string{"-t", "my-team", "--yes"}, response: `{"data":{"environments":{"nodes":[{"name":"prod-gcp"},{"name":"dev-gcp"}]}}}`, wantError: hint, wantOperations: []string{"Environments"}}, {name: "one environment still requires a choice", args: []string{"-t", "my-team"}, response: `{"data":{"environments":{"nodes":[{"name":"dev-gcp"}]}}}`, wantError: hint, wantOperations: []string{"Environments"}}, {name: "yes does not choose the sole environment", args: []string{"-t", "my-team", "--yes"}, response: `{"data":{"environments":{"nodes":[{"name":"dev-gcp"}]}}}`, wantError: hint, wantOperations: []string{"Environments"}}, {name: "no environments", args: []string{"-t", "my-team", "--yes"}, response: `{"data":{"environments":{"nodes":[]}}}`, wantError: hint, wantOperations: []string{"Environments"}}, + {name: "only unsupported environments", args: []string{"-t", "my-team", "--yes"}, response: `{"data":{"environments":{"nodes":[{"name":"dev-fss"},{"name":"prod-fss"}]}}}`, wantError: hint, wantOperations: []string{"Environments"}}, {name: "discovery failure is propagated", args: []string{"-t", "my-team", "--yes"}, response: `{"errors":[{"message":"access denied"}]}`, wantError: "fetching environments:", wantOperations: []string{"Environments"}}, {name: "team is required before discovery", args: []string{"--yes"}, wantError: "missing required team"}, {name: "invalid version fails before discovery", args: []string{"-t", "my-team", "--version", "17", "--yes"}, wantError: "--version must be 18"}, diff --git a/internal/valkey/command/flag/flag.go b/internal/valkey/command/flag/flag.go index caee66c1..4af9b68b 100644 --- a/internal/valkey/command/flag/flag.go +++ b/internal/valkey/command/flag/flag.go @@ -23,6 +23,9 @@ type Create struct { } func (c *Create) Validate() error { + if c.Environment.IsOnPrem() { + return fmt.Errorf("Valkey is not supported in environment %q; choose an environment without the -fss suffix", c.Environment) + } if c.Memory != "" && !c.Memory.IsValid() { return fmt.Errorf("invalid memory %q, must be one of: %v", c.Memory, gql.AllValkeyMemory) }