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 internal/application/create_environment_test.go
Original file line number Diff line number Diff line change
@@ -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")
}
}
})
}
}
}
6 changes: 6 additions & 0 deletions internal/flags/environment.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,19 @@ package flags
import (
"context"
"fmt"
"strings"

"github.com/nais/cli/internal/naisapi"
"github.com/nais/naistrix"
)

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) {
Expand Down
24 changes: 24 additions & 0 deletions internal/flags/environment_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
})
}
}
3 changes: 3 additions & 0 deletions internal/opensearch/command/flag/flag.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
18 changes: 17 additions & 1 deletion internal/postgres/command/environment.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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)
}
Expand Down
22 changes: 22 additions & 0 deletions internal/postgres/command/instance_create_environment_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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"},
Expand Down
3 changes: 3 additions & 0 deletions internal/valkey/command/flag/flag.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
Loading