diff --git a/go.mod b/go.mod index f8a6c17d..411a7521 100644 --- a/go.mod +++ b/go.mod @@ -34,6 +34,7 @@ require ( github.com/nais/naistrix v0.35.0 github.com/pkg/errors v0.9.1 github.com/pterm/pterm v0.12.83 + github.com/quic-go/quic-go v0.59.1 github.com/sethvargo/go-retry v0.3.0 github.com/stretchr/testify v1.11.1 github.com/suessflorian/gqlfetch v0.7.0 @@ -198,6 +199,7 @@ require ( github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/common v0.67.5 // indirect github.com/prometheus/procfs v0.19.2 // indirect + github.com/quic-go/qpack v0.6.0 // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/sagikazarmark/locafero v0.12.0 // indirect diff --git a/go.sum b/go.sum index b6770883..ec9c9769 100644 --- a/go.sum +++ b/go.sum @@ -457,6 +457,12 @@ github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4 github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw= github.com/pterm/pterm v0.12.83 h1:ie+YmGmA727VuhxBlyGr74Ks+7McV6kT99IB8EU80aA= github.com/pterm/pterm v0.12.83/go.mod h1:xlgc6bFWyJIMtmLJvGim+L7jhSReilOlOnodeIYe4Tk= +github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= +github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII= +github.com/quic-go/quic-go v0.57.0 h1:AsSSrrMs4qI/hLrKlTH/TGQeTMY0ib1pAOX7vA3AdqE= +github.com/quic-go/quic-go v0.57.0/go.mod h1:ly4QBAjHA2VhdnxhojRsCUOeJwKYg+taDlos92xb1+s= +github.com/quic-go/quic-go v0.59.1 h1:0Gmua0HW1Tv7ANR7hUYwRyD0MG5OJfgvYSZasGZzBic= +github.com/quic-go/quic-go v0.59.1/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= diff --git a/internal/alpha/command/alpha.go b/internal/alpha/command/alpha.go index 016137af..0c617226 100644 --- a/internal/alpha/command/alpha.go +++ b/internal/alpha/command/alpha.go @@ -2,6 +2,7 @@ package command import ( "github.com/nais/cli/internal/alpha/command/flag" + postgrescmd "github.com/nais/cli/internal/alpha/postgres/command" "github.com/nais/cli/internal/flags" krakend "github.com/nais/cli/internal/krakend/command" mcpcmd "github.com/nais/cli/internal/mcp/command" @@ -18,6 +19,7 @@ func Alpha(parentFlags *flags.GlobalFlags) *naistrix.Command { SubCommands: []*naistrix.Command{ krakend.Krakend(flags), mcpcmd.MCP(flags), + postgrescmd.Postgres(parentFlags), }, } } diff --git a/internal/alpha/postgres/access.go b/internal/alpha/postgres/access.go new file mode 100644 index 00000000..3d6d1208 --- /dev/null +++ b/internal/alpha/postgres/access.go @@ -0,0 +1,134 @@ +package postgres + +import ( + "context" + "fmt" + "time" + + "github.com/Khan/genqlient/graphql" + "github.com/nais/cli/internal/naisapi" + "github.com/nais/cli/internal/naisapi/gql" +) + +// Access contains the brokered connection materials. Do not log this value. +type Access struct { + State gql.PostgresAccessState + Message string + Connection *Connection +} + +type Connection struct { + Username, Password, CACertificate, ServerName, RelayEndpoint, RelayAccess, RelayToken string +} + +type AccessAPI interface { + ActiveBranch(context.Context, string, string, string) (string, error) + Create(context.Context, gql.CreatePostgresAccessInput) (string, error) + Get(context.Context, string, string, string) (Access, error) +} + +type graphqlAccessAPI struct{ client graphql.Client } + +func NewAPI(ctx context.Context) (AccessAPI, error) { + client, err := naisapi.GraphqlClient(ctx) + if err != nil { + return nil, err + } + return graphqlAccessAPI{client}, nil +} + +func (a graphqlAccessAPI) ActiveBranch(ctx context.Context, team, environment, name string) (string, error) { + _ = `# @genqlient + query GetActivePostgresBranchAlpha($team: Slug!, $environment: String!, $postgres: String!) { + team(slug: $team) { environment(name: $environment) { postgres(name: $postgres) { activeBranch { name } } } } + } + ` + result, err := gql.GetActivePostgresBranchAlpha(ctx, a.client, team, environment, name) + if err != nil { + return "", err + } + if result.Team.Environment.Postgres.ActiveBranch == nil { + return "", fmt.Errorf("postgres %q has no active branch; specify --branch", name) + } + return result.Team.Environment.Postgres.ActiveBranch.Name, nil +} + +func (a graphqlAccessAPI) Create(ctx context.Context, input gql.CreatePostgresAccessInput) (string, error) { + _ = `# @genqlient + mutation CreatePostgresAccessAlpha($input: CreatePostgresAccessInput!) { + createPostgresAccess(input: $input) { name } + } + ` + result, err := gql.CreatePostgresAccessAlpha(ctx, a.client, input) + if err != nil { + return "", err + } + return result.CreatePostgresAccess.Name, nil +} + +func (a graphqlAccessAPI) Get(ctx context.Context, team, environment, name string) (Access, error) { + _ = `# @genqlient + query GetPostgresAccessAlpha($team: Slug!, $environment: String!, $name: String!) { + team(slug: $team) { environment(name: $environment) { postgresAccess(name: $name) { + state message connection { username password caCertificate serverName relayEndpoint relayAccess relayToken } + } } } + } + ` + result, err := gql.GetPostgresAccessAlpha(ctx, a.client, team, environment, name) + if err != nil { + return Access{}, err + } + got := result.Team.Environment.PostgresAccess + access := Access{State: got.State} + if got.Message != nil { + access.Message = *got.Message + } + if got.Connection != nil { + c := got.Connection + access.Connection = &Connection{c.Username, c.Password, c.CaCertificate, c.ServerName, c.RelayEndpoint, c.RelayAccess, c.RelayToken} + } + return access, nil +} + +func waitForAccess(ctx context.Context, api AccessAPI, team, environment, name string, interval time.Duration) (Connection, error) { + for { + access, err := api.Get(ctx, team, environment, name) + if err != nil { + return Connection{}, fmt.Errorf("retrieve postgres access: %w", err) + } + switch access.State { + case gql.PostgresAccessStateReady: + if access.Connection == nil { + return Connection{}, fmt.Errorf("postgres access %q is ready without connection materials", name) + } + return *access.Connection, nil + case gql.PostgresAccessStateFailed, gql.PostgresAccessStateExpired: + return Connection{}, fmt.Errorf("postgres access %q is %s: %s", name, access.State, access.Message) + case gql.PostgresAccessStatePending: + default: + return Connection{}, fmt.Errorf("postgres access %q has unknown state %q", name, access.State) + } + timer := time.NewTimer(interval) + select { + case <-ctx.Done(): + timer.Stop() + return Connection{}, fmt.Errorf("waiting for postgres access %q: %w", name, ctx.Err()) + case <-timer.C: + } + } +} + +func CreateAndWait(ctx context.Context, api AccessAPI, input gql.CreatePostgresAccessInput) (Connection, error) { + // Bound creation and polling together; a stalled API must not hang the command. + setupCtx, cancel := context.WithTimeout(ctx, 60*time.Second) + defer cancel() + name, err := api.Create(setupCtx, input) + if err != nil { + return Connection{}, fmt.Errorf("create postgres access: %w", err) + } + connection, err := waitForAccess(setupCtx, api, input.TeamSlug, input.EnvironmentName, name, time.Second) + if err != nil { + return Connection{}, fmt.Errorf("access %q was created but is not ready (it expires after its requested TTL): %w", name, err) + } + return connection, nil +} diff --git a/internal/alpha/postgres/access_test.go b/internal/alpha/postgres/access_test.go new file mode 100644 index 00000000..a8b5aec2 --- /dev/null +++ b/internal/alpha/postgres/access_test.go @@ -0,0 +1,71 @@ +package postgres + +import ( + "context" + "errors" + "strings" + "testing" + "time" + + "github.com/nais/cli/internal/naisapi/gql" +) + +type fakeAccessAPI struct { + states []Access + calls int +} + +func (f *fakeAccessAPI) ActiveBranch(context.Context, string, string, string) (string, error) { + return "main", nil +} + +func (f *fakeAccessAPI) Create(context.Context, gql.CreatePostgresAccessInput) (string, error) { + return "access-1", nil +} + +func (f *fakeAccessAPI) Get(context.Context, string, string, string) (Access, error) { + index := f.calls + f.calls++ + if index >= len(f.states) { + return Access{}, errors.New("unexpected poll") + } + return f.states[index], nil +} + +func TestWaitForAccess(t *testing.T) { + for _, tt := range []struct { + name string + states []Access + want string + }{ + {"ready", []Access{{State: gql.PostgresAccessStatePending}, {State: gql.PostgresAccessStateReady, Connection: &Connection{Username: "alice"}}}, ""}, + {"failed", []Access{{State: gql.PostgresAccessStateFailed, Message: "database unavailable"}}, "database unavailable"}, + {"expired", []Access{{State: gql.PostgresAccessStateExpired}}, "EXPIRED"}, + {"missing materials", []Access{{State: gql.PostgresAccessStateReady}}, "without connection materials"}, + } { + t.Run(tt.name, func(t *testing.T) { + fake := &fakeAccessAPI{states: tt.states} + got, err := waitForAccess(context.Background(), fake, "team", "dev", "access-1", time.Millisecond) + if tt.want == "" { + if err != nil || got.Username != "alice" { + t.Fatalf("got %+v, err %v", got, err) + } + } else if err == nil || !strings.Contains(err.Error(), tt.want) { + t.Fatalf("expected %q, got %v", tt.want, err) + } + if fake.calls != len(tt.states) { + t.Fatalf("polled %d times, want %d", fake.calls, len(tt.states)) + } + }) + } +} + +func TestWaitCancellation(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + cancel() + fake := &fakeAccessAPI{states: []Access{{State: gql.PostgresAccessStatePending}}} + _, err := waitForAccess(ctx, fake, "team", "dev", "access-1", time.Hour) + if !errors.Is(err, context.Canceled) { + t.Fatalf("expected cancellation, got %v", err) + } +} diff --git a/internal/alpha/postgres/command/access.go b/internal/alpha/postgres/command/access.go new file mode 100644 index 00000000..701f67f7 --- /dev/null +++ b/internal/alpha/postgres/command/access.go @@ -0,0 +1,183 @@ +package command + +import ( + "context" + "fmt" + "net" + "os" + "os/exec" + "os/signal" + "strings" + "syscall" + "time" + + "github.com/nais/cli/internal/alpha/postgres" + "github.com/nais/cli/internal/alpha/postgres/command/flag" + "github.com/nais/cli/internal/alpha/postgres/relay" + "github.com/nais/cli/internal/naisapi/gql" + "github.com/nais/naistrix" +) + +func accessFlags(parent *flag.Postgres) *flag.Access { + return &flag.Access{Postgres: parent, AccessLevel: "read", TTL: 30 * time.Minute, Database: "app"} +} + +func psqlCommand(parent *flag.Postgres) *naistrix.Command { + f := accessFlags(parent) + return &naistrix.Command{ + Name: "psql", Title: "Connect to Nais Postgres via psql (experimental).", + Description: "Request personal access, open a local relay tunnel and start psql with end-to-end TLS verification.", + Args: []naistrix.Argument{{Name: "postgres"}}, Flags: f, + RunFunc: func(ctx context.Context, args *naistrix.Arguments, out *naistrix.OutputWriter) error { + // SIGTERM must run the deferred cleanup (CA file, relay). Ctrl-C aborts the setup, but once + // psql runs it is left to psql (query cancellation) and ignored here until cleanup is done. + ctx, stopTerm := signal.NotifyContext(ctx, syscall.SIGTERM) + defer stopTerm() + setupCtx, stopSetup := signal.NotifyContext(ctx, os.Interrupt) + connection, err := requestAccess(setupCtx, args.Get("postgres"), f) + // Register the ignore-channel before releasing the setup handler so there is no unhandled window. + interrupts := make(chan os.Signal, 1) + signal.Notify(interrupts, os.Interrupt) + defer signal.Stop(interrupts) + stopSetup() + if err != nil { + return err + } + listener, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + return err + } + tunnelCtx, cancel := context.WithCancel(ctx) + defer cancel() + done := make(chan error, 1) + go func() { + done <- relay.Serve(tunnelCtx, listener, relay.Tunnel{Endpoint: connection.RelayEndpoint, Access: connection.RelayAccess, Token: connection.RelayToken}) + }() + defer func() { cancel(); <-done }() + ca, err := os.CreateTemp("", "nais-postgres-ca-*.crt") + if err != nil { + return err + } + defer func() { _ = os.Remove(ca.Name()) }() + defer func() { _ = ca.Close() }() + if err := ca.Chmod(0o600); err != nil { + return err + } + if _, err := ca.WriteString(connection.CACertificate); err != nil { + return err + } + if err := ca.Close(); err != nil { + return err + } + path, err := exec.LookPath("psql") + if err != nil { + return fmt.Errorf("psql not found: %w", err) + } + cmd := exec.CommandContext(ctx, path, "-X", "-w") + cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr + cmd.Env = append(withoutPostgresEnv(os.Environ()), + "PGHOST="+connection.ServerName, "PGHOSTADDR=127.0.0.1", fmt.Sprintf("PGPORT=%d", listener.Addr().(*net.TCPAddr).Port), + "PGUSER="+connection.Username, "PGPASSWORD="+connection.Password, "PGDATABASE="+f.Database, + "PGSSLMODE=verify-full", "PGSSLROOTCERT="+ca.Name(), "PGCONNECT_TIMEOUT=10") + out.Println("Connecting with verified PostgreSQL TLS through the local relay...") + return cmd.Run() + }, + } +} + +func proxyCommand(parent *flag.Postgres) *naistrix.Command { + f := &flag.Proxy{Postgres: parent, AccessLevel: "read", TTL: 30 * time.Minute, Host: "127.0.0.1"} + return &naistrix.Command{ + Name: "proxy", Title: "Expose a Nais Postgres relay tunnel locally (experimental).", + Description: "Request personal access and listen on loopback. PostgreSQL clients must verify the server certificate; use psql for automatic TLS setup. Credentials are not printed unless --print-password is set.", + Args: []naistrix.Argument{{Name: "postgres"}}, Flags: f, + RunFunc: func(ctx context.Context, args *naistrix.Arguments, out *naistrix.OutputWriter) error { + ctx, stop := signal.NotifyContext(ctx, os.Interrupt, syscall.SIGTERM) + defer stop() + if net.ParseIP(f.Host) == nil || !net.ParseIP(f.Host).IsLoopback() { + return fmt.Errorf("--host must be a loopback IP address") + } + if f.Port < 0 || f.Port > 65535 { + return fmt.Errorf("--port must be between 0 and 65535") + } + connection, err := requestAccess(ctx, args.Get("postgres"), &flag.Access{ + Postgres: f.Postgres, Branch: f.Branch, AccessLevel: f.AccessLevel, Reason: f.Reason, TTL: f.TTL, + }) + if err != nil { + return err + } + listener, err := net.Listen("tcp", net.JoinHostPort(f.Host, fmt.Sprint(f.Port))) + if err != nil { + return err + } + ca, err := os.CreateTemp("", "nais-postgres-ca-*.crt") + if err != nil { + _ = listener.Close() + return err + } + defer func() { _ = os.Remove(ca.Name()) }() + if _, err := ca.WriteString(connection.CACertificate); err != nil { + _ = ca.Close() + _ = listener.Close() + return err + } + if err := ca.Close(); err != nil { + _ = listener.Close() + return err + } + out.Printf("Postgres relay listening at %s; user: %s; TLS server name: %s; CA: %s\n", listener.Addr(), connection.Username, connection.ServerName, ca.Name()) + out.Println("Use host= hostaddr= sslmode=verify-full sslrootcert=. For automatic setup use 'nais alpha postgres psql'.") + out.Println("Database password is only available here with --print-password (sensitive output).") + if f.PrintPassword { + out.Errorf("Warning: printing a database password; avoid terminal capture and shell history.\n") + out.Printf("Database password (sensitive): %s\n", connection.Password) + } + return relay.Serve(ctx, listener, relay.Tunnel{Endpoint: connection.RelayEndpoint, Access: connection.RelayAccess, Token: connection.RelayToken}) + }, + } +} + +func withoutPostgresEnv(env []string) []string { + ret := make([]string, 0, len(env)) + for _, item := range env { + if !strings.HasPrefix(item, "PG") { + ret = append(ret, item) + } + } + return ret +} + +func requestAccess(ctx context.Context, name string, f *flag.Access) (postgres.Connection, error) { + if f.Team == "" || f.Environment == "" { + return postgres.Connection{}, fmt.Errorf("--team and --environment are required") + } + if len(strings.TrimSpace(f.Reason)) < 10 { + return postgres.Connection{}, fmt.Errorf("--reason must contain at least 10 characters") + } + if f.TTL < time.Second || f.TTL > time.Hour { + return postgres.Connection{}, fmt.Errorf("--ttl must be between 1s and 1h") + } + levels := map[string]gql.PostgresAccessLevel{"read": gql.PostgresAccessLevelRead, "write": gql.PostgresAccessLevelReadwrite, "admin": gql.PostgresAccessLevelReadwritecreate} + level, ok := levels[f.AccessLevel] + if !ok { + return postgres.Connection{}, fmt.Errorf("--access-level must be read, write, or admin") + } + api, err := postgres.NewAPI(ctx) + if err != nil { + return postgres.Connection{}, err + } + branch := f.Branch + if branch == "" { + lookupCtx, cancel := context.WithTimeout(ctx, 30*time.Second) + defer cancel() + branch, err = api.ActiveBranch(lookupCtx, f.Team, string(f.Environment), name) + if err != nil { + return postgres.Connection{}, err + } + } + ttl := f.TTL.String() + return postgres.CreateAndWait(ctx, api, gql.CreatePostgresAccessInput{ + Postgres: name, Branch: branch, TeamSlug: f.Team, EnvironmentName: string(f.Environment), + AccessLevel: level, Reason: f.Reason, Ttl: &ttl, + }) +} diff --git a/internal/alpha/postgres/command/access_test.go b/internal/alpha/postgres/command/access_test.go new file mode 100644 index 00000000..0eead293 --- /dev/null +++ b/internal/alpha/postgres/command/access_test.go @@ -0,0 +1,14 @@ +package command + +import ( + "slices" + "testing" +) + +func TestPsqlEnvironmentIgnoresInheritedPostgresSettings(t *testing.T) { + got := withoutPostgresEnv([]string{"PATH=/bin", "PGSERVICE=unsafe", "PGSSLMODE=disable", "PGPASSWORD=old", "HOME=/home/user"}) + want := []string{"PATH=/bin", "HOME=/home/user"} + if !slices.Equal(got, want) { + t.Fatalf("environment = %v, want %v", got, want) + } +} diff --git a/internal/alpha/postgres/command/flag/flag.go b/internal/alpha/postgres/command/flag/flag.go new file mode 100644 index 00000000..55fca69c --- /dev/null +++ b/internal/alpha/postgres/command/flag/flag.go @@ -0,0 +1,47 @@ +package flag + +import ( + "context" + "time" + + "github.com/nais/cli/internal/flags" + "github.com/nais/cli/internal/labels" + "github.com/nais/naistrix" +) + +type ( + Postgres struct{ *flags.GlobalFlags } + Access struct { + *Postgres + Branch string `name:"branch" usage:"Branch to access (defaults to the active branch)."` + AccessLevel string `name:"access-level" usage:"Access level: read, write, or admin."` + Reason string `name:"reason" usage:"Reason for personal access (at least 10 characters)."` + TTL time.Duration `name:"ttl" usage:"Requested lifetime (default 30m, maximum 1h)."` + Database string `name:"database" usage:"Database name for psql (default app)."` + } + Proxy struct { + *Postgres + Branch string `name:"branch" usage:"Branch to access (defaults to the active branch)."` + AccessLevel string `name:"access-level" usage:"Access level: read, write, or admin."` + Reason string `name:"reason" usage:"Reason for personal access (at least 10 characters)."` + TTL time.Duration `name:"ttl" usage:"Requested lifetime (default 30m, maximum 1h)."` + Host string `name:"host" usage:"Local loopback address (default 127.0.0.1)."` + Port int `name:"port" usage:"Local port (default random)."` + PrintPassword bool `name:"print-password" usage:"Print the database password to stdout (sensitive)."` + } + List struct { + *Postgres + Output Output `name:"output" short:"o" usage:"Format output (table or json)."` + Labels labels.LabelFilters `name:"label" short:"l" usage:"Filter by label in |KEY=VALUE| form. Can be repeated."` + } +) + +func (*List) LabelFacetResource() string { return "postgresBranches" } + +type Output string + +var _ naistrix.FlagAutoCompleter = (*Output)(nil) + +func (o *Output) AutoComplete(context.Context, *naistrix.Arguments, string, any) ([]string, string) { + return []string{"table", "json"}, "Available output formats." +} diff --git a/internal/alpha/postgres/command/postgres.go b/internal/alpha/postgres/command/postgres.go new file mode 100644 index 00000000..25d84a59 --- /dev/null +++ b/internal/alpha/postgres/command/postgres.go @@ -0,0 +1,51 @@ +package command + +import ( + "context" + + "github.com/nais/cli/internal/alpha/postgres" + "github.com/nais/cli/internal/alpha/postgres/command/flag" + "github.com/nais/cli/internal/flags" + "github.com/nais/cli/internal/labels" + "github.com/nais/naistrix" + "github.com/nais/naistrix/output" +) + +func Postgres(parentFlags *flags.GlobalFlags) *naistrix.Command { + flags := &flag.Postgres{GlobalFlags: parentFlags} + return &naistrix.Command{ + Name: "postgres", Title: "Manage Nais Postgres instances (experimental).", + Description: "Experimental commands for Nais Postgres branches and brokered personal access.", StickyFlags: flags, + SubCommands: []*naistrix.Command{listCommand(flags), psqlCommand(flags), proxyCommand(flags)}, + } +} + +func listCommand(parentFlags *flag.Postgres) *naistrix.Command { + flags := &flag.List{Postgres: parentFlags} + return &naistrix.Command{ + Name: "list", Title: "List Nais Postgres branches for a team.", + Description: "List Nais Postgres branches owned by a team.", Flags: flags, + RunFunc: func(ctx context.Context, _ *naistrix.Arguments, out *naistrix.OutputWriter) error { + labelFilters, err := labels.ParseFilters(flags.Labels) + if err != nil { + return err + } + var environments []string + if flags.Environment != "" { + environments = []string{string(flags.Environment)} + } + ret, err := postgres.GetTeamPostgresBranches(ctx, flags.Team, environments, labelFilters) + if err != nil { + return err + } + if flags.Output == "json" { + return out.JSON(output.JSONWithPrettyOutput()).Render(ret) + } + if len(ret) == 0 { + out.Println("Team has no Nais Postgres branches.") + return nil + } + return out.Table().Render(ret) + }, + } +} diff --git a/internal/alpha/postgres/list.go b/internal/alpha/postgres/list.go new file mode 100644 index 00000000..860a5852 --- /dev/null +++ b/internal/alpha/postgres/list.go @@ -0,0 +1,85 @@ +package postgres + +import ( + "context" + "fmt" + "slices" + "sort" + + "github.com/nais/cli/internal/naisapi" + "github.com/nais/cli/internal/naisapi/gql" + "github.com/nais/naistrix/output" +) + +const consoleBaseURL = "https://console.nav.cloud.nais.io" + +type Instance struct { + Name output.Link `json:"name"` + Type string `json:"type"` + Environment string `json:"environment"` + Version string `heading:"Version" json:"version"` + HighAvailability bool `heading:"HA" json:"high_availability"` + State State `json:"state"` +} + +type State string + +func (s State) String() string { + switch s { + case State(gql.PostgresBranchStateAvailable): + return "Available" + case State(gql.PostgresBranchStateProgressing): + return "Progressing" + case State(gql.PostgresBranchStateDegraded): + return "Degraded" + } + return "Unknown" +} + +func GetTeamPostgresBranches(ctx context.Context, team string, environments []string, labelFilters []gql.LabelFilter) ([]Instance, error) { + _ = `# @genqlient + query GetTeamPostgresBranchesAlpha($team: Slug!, $postgresFilter: PostgresBranchFilter) { + team(slug: $team) { + postgresBranches(first: 1000, filter: $postgresFilter) { + nodes { + name + teamEnvironment { environment { name } } + postgres { name majorVersion highAvailability } + state + } + } + } + } + ` + client, err := naisapi.GraphqlClient(ctx) + if err != nil { + return nil, err + } + resp, err := gql.GetTeamPostgresBranchesAlpha(ctx, client, team, &gql.PostgresBranchFilter{Environments: environments, Labels: labelFilters}) + if err != nil { + return nil, err + } + return instancesFromTeam(resp.Team, team, environments), nil +} + +func instancesFromTeam(data gql.GetTeamPostgresBranchesAlphaTeam, team string, environments []string) []Instance { + var ret []Instance + for _, p := range data.PostgresBranches.Nodes { + env := p.TeamEnvironment.Environment.Name + if len(environments) > 0 && !slices.Contains(environments, env) { + continue + } + ret = append(ret, Instance{ + Name: output.Link{Name: p.Postgres.Name + "/" + p.Name, URL: fmt.Sprintf("%s/team/%s/%s/postgres/%s", consoleBaseURL, team, env, p.Postgres.Name)}, + Type: "PostgreSQL", Environment: env, Version: p.Postgres.MajorVersion, + HighAvailability: p.Postgres.HighAvailability, State: State(p.State), + }) + } + sort.Slice(ret, func(i, j int) bool { + if ret[i].Name.Name == ret[j].Name.Name { + return ret[i].Environment < ret[j].Environment + } + return ret[i].Name.Name < ret[j].Name.Name + }) + return ret +} diff --git a/internal/alpha/postgres/list_test.go b/internal/alpha/postgres/list_test.go new file mode 100644 index 00000000..52a96bcf --- /dev/null +++ b/internal/alpha/postgres/list_test.go @@ -0,0 +1,37 @@ +package postgres + +import ( + "encoding/json" + "reflect" + "testing" + + "github.com/nais/cli/internal/naisapi/gql" + "github.com/nais/naistrix/output" +) + +func TestInstancesFromTeam(t *testing.T) { + const teamData = `{"postgresBranches":{"nodes":[ + {"name":"preview","teamEnvironment":{"environment":{"name":"prod"}},"postgres":{"name":"orders","majorVersion":"16","highAvailability":true},"state":"PROGRESSING"}, + {"name":"main","teamEnvironment":{"environment":{"name":"dev"}},"postgres":{"name":"orders","majorVersion":"16","highAvailability":true},"state":"AVAILABLE"} + ]}}` + var data gql.GetTeamPostgresBranchesAlphaTeam + if err := json.Unmarshal([]byte(teamData), &data); err != nil { + t.Fatal(err) + } + dev := Instance{Name: output.Link{Name: "orders/main", URL: consoleBaseURL + "/team/my-team/dev/postgres/orders"}, Type: "PostgreSQL", Environment: "dev", Version: "16", HighAvailability: true, State: State(gql.PostgresBranchStateAvailable)} + prod := Instance{Name: output.Link{Name: "orders/preview", URL: consoleBaseURL + "/team/my-team/prod/postgres/orders"}, Type: "PostgreSQL", Environment: "prod", Version: "16", HighAvailability: true, State: State(gql.PostgresBranchStateProgressing)} + for _, tt := range []struct { + name string + environments []string + want []Instance + }{ + {name: "sorted branches", want: []Instance{dev, prod}}, + {name: "environment filter", environments: []string{"dev"}, want: []Instance{dev}}, + } { + t.Run(tt.name, func(t *testing.T) { + if got := instancesFromTeam(data, "my-team", tt.environments); !reflect.DeepEqual(got, tt.want) { + t.Errorf("instancesFromTeam() = %#v, want %#v", got, tt.want) + } + }) + } +} diff --git a/internal/alpha/postgres/relay/relay.go b/internal/alpha/postgres/relay/relay.go new file mode 100644 index 00000000..66bf76c0 --- /dev/null +++ b/internal/alpha/postgres/relay/relay.go @@ -0,0 +1,122 @@ +// Package relay forwards local TCP connections over authenticated HTTP/3 CONNECT streams. +package relay + +import ( + "context" + "encoding/base64" + "errors" + "fmt" + "io" + "net" + "net/http" + "net/url" + "os" + "strings" + + "github.com/quic-go/quic-go/http3" +) + +// Tunnel is a brokered relay endpoint and owner-only proof. Never log its token. +type Tunnel struct{ Endpoint, Access, Token string } + +func (t Tunnel) request(ctx context.Context, body io.Reader) (*http.Request, error) { + u, err := url.Parse(t.Endpoint) + if err != nil || u.Scheme != "https" || u.Host == "" || u.Path != "" || u.RawQuery != "" || u.User != nil || u.Fragment != "" { + return nil, fmt.Errorf("invalid relay endpoint") + } + // Validate before building headers: the HTTP stack echoes invalid header values in its errors. + if !strings.Contains(t.Access, "/") || !validToken(t.Token) { + return nil, fmt.Errorf("invalid relay access credentials") + } + req, err := http.NewRequestWithContext(ctx, http.MethodConnect, t.Endpoint, body) + if err != nil { + return nil, err + } + req.Host = u.Host // CONNECT authority is the relay, not the database target. + req.Header.Set("Authorization", "Bearer "+t.Token) + req.Header.Set("Relay-Access", t.Access) + return req, nil +} + +// validToken accepts only the relay contract: unpadded base64url of 32 bytes. +func validToken(token string) bool { + raw, err := base64.RawURLEncoding.DecodeString(token) + // The decoder silently skips \r and \n, so also require the canonical encoding. + return err == nil && len(raw) == 32 && base64.RawURLEncoding.EncodeToString(raw) == token +} + +// Serve forwards each TCP connection to the relay until ctx is cancelled. +func Serve(ctx context.Context, listener net.Listener, tunnel Tunnel) error { + transport := &http3.Transport{} + defer func() { _ = transport.Close() }() + defer func() { _ = listener.Close() }() + stop := context.AfterFunc(ctx, func() { _ = listener.Close() }) + defer stop() + for { + conn, err := listener.Accept() + if err != nil { + if ctx.Err() != nil { + return nil + } + return fmt.Errorf("accept local connection: %w", err) + } + go func() { + defer func() { _ = conn.Close() }() + if err := forward(ctx, transport, tunnel, conn); err != nil && ctx.Err() == nil { + // An individual connection must not terminate other local clients. + // Callers can retry; no credentials are included in the error. + fmt.Fprintf(os.Stderr, "postgres relay connection failed: %v\n", err) + } + }() + } +} + +func forward(ctx context.Context, transport *http3.Transport, tunnel Tunnel, local net.Conn) error { + ctx, cancel := context.WithCancel(ctx) + defer cancel() + stop := context.AfterFunc(ctx, func() { _ = local.Close() }) + defer stop() + reader, writer := io.Pipe() + defer func() { _ = reader.Close() }() + defer func() { _ = writer.Close() }() + req, err := tunnel.request(ctx, reader) + if err != nil { + return err + } + done := make(chan error, 1) + go func() { + _, err := io.Copy(writer, local) + _ = writer.CloseWithError(err) // Send HTTP request FIN when TCP input is closed. + done <- err + }() + response, err := transport.RoundTrip(req) + if err != nil { + _ = local.Close() + _ = reader.CloseWithError(err) + <-done + return fmt.Errorf("relay CONNECT: %w", err) + } + defer func() { _ = response.Body.Close() }() + if response.StatusCode != http.StatusOK { + _ = local.Close() + _ = reader.Close() + <-done + return fmt.Errorf("relay CONNECT returned HTTP %d", response.StatusCode) + } + _, downloadErr := io.Copy(local, response.Body) + if tcp, ok := local.(interface{ CloseWrite() error }); ok && downloadErr == nil { + downloadErr = tcp.CloseWrite() + } + if downloadErr != nil { + _ = local.Close() + _ = reader.CloseWithError(downloadErr) + } + uploadErr := <-done + if downloadErr != nil { + return downloadErr + } + if uploadErr != nil && !errors.Is(uploadErr, net.ErrClosed) { + return uploadErr + } + return nil +} diff --git a/internal/alpha/postgres/relay/relay_test.go b/internal/alpha/postgres/relay/relay_test.go new file mode 100644 index 00000000..edfdad8c --- /dev/null +++ b/internal/alpha/postgres/relay/relay_test.go @@ -0,0 +1,161 @@ +package relay + +import ( + "bytes" + "context" + "crypto/rand" + "crypto/rsa" + "crypto/tls" + "crypto/x509" + "crypto/x509/pkix" + "encoding/base64" + "encoding/pem" + "io" + "math/big" + "net" + "net/http" + "strings" + "testing" + "time" + + "github.com/quic-go/quic-go/http3" +) + +// 32 bytes, unpadded base64url, as issued by the relay contract. +var ( + proofToken = base64.RawURLEncoding.EncodeToString(bytes.Repeat([]byte{1}, 32)) + deniedToken = base64.RawURLEncoding.EncodeToString(bytes.Repeat([]byte{2}, 32)) +) + +func TestConnectStreamsAfterHalfClose(t *testing.T) { + key, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + t.Fatal(err) + } + certDER, err := x509.CreateCertificate(rand.Reader, &x509.Certificate{ + SerialNumber: big.NewInt(1), Subject: pkix.Name{CommonName: "localhost"}, DNSNames: []string{"localhost"}, + NotBefore: time.Now().Add(-time.Minute), NotAfter: time.Now().Add(time.Hour), KeyUsage: x509.KeyUsageDigitalSignature, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, + }, &x509.Certificate{ + SerialNumber: big.NewInt(1), Subject: pkix.Name{CommonName: "localhost"}, DNSNames: []string{"localhost"}, + NotBefore: time.Now().Add(-time.Minute), NotAfter: time.Now().Add(time.Hour), KeyUsage: x509.KeyUsageDigitalSignature, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, + }, &key.PublicKey, key) + if err != nil { + t.Fatal(err) + } + cert, err := tls.X509KeyPair(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER}), pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key)})) + if err != nil { + t.Fatal(err) + } + roots := x509.NewCertPool() + roots.AddCert(cert.Leaf) + packet, err := net.ListenPacket("udp", "127.0.0.1:0") + if err != nil { + t.Fatal(err) + } + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Header.Get("Authorization") == "Bearer "+deniedToken { + w.WriteHeader(http.StatusUnauthorized) + return + } + if r.Method != http.MethodConnect || r.Host != "localhost:"+strings.Split(packet.LocalAddr().String(), ":")[1] || r.Header.Get("Authorization") != "Bearer "+proofToken || r.Header.Get("Relay-Access") != "team/access" { + t.Errorf("unexpected CONNECT: method=%s host=%s auth=%s access=%s", r.Method, r.Host, r.Header.Get("Authorization"), r.Header.Get("Relay-Access")) + w.WriteHeader(http.StatusUnauthorized) + return + } + w.WriteHeader(http.StatusOK) + _ = http.NewResponseController(w).Flush() + data, err := io.ReadAll(r.Body) + if err != nil { + t.Error(err) + return + } + _, _ = w.Write([]byte("reply:" + string(data))) + }) + server := &http3.Server{Handler: handler, TLSConfig: &tls.Config{Certificates: []tls.Certificate{cert}}} + serverDone := make(chan struct{}) + go func() { defer close(serverDone); _ = server.Serve(packet) }() + defer func() { _ = server.Close(); <-serverDone; _ = packet.Close() }() + transport := &http3.Transport{TLSClientConfig: &tls.Config{RootCAs: roots, ServerName: "localhost"}} + defer func() { _ = transport.Close() }() + listener, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatal(err) + } + defer func() { _ = listener.Close() }() + client, err := net.Dial("tcp", listener.Addr().String()) + if err != nil { + t.Fatal(err) + } + defer func() { _ = client.Close() }() + _ = client.SetDeadline(time.Now().Add(5 * time.Second)) + local, err := listener.Accept() + if err != nil { + t.Fatal(err) + } + defer func() { _ = local.Close() }() + done := make(chan error, 1) + go func() { + done <- forward(context.Background(), transport, Tunnel{Endpoint: "https://localhost:" + strings.Split(packet.LocalAddr().String(), ":")[1], Access: "team/access", Token: proofToken}, local) + }() + _, _ = client.Write([]byte("hello")) + _ = client.(*net.TCPConn).CloseWrite() + data, err := io.ReadAll(client) + if err != nil || string(data) != "reply:hello" { + t.Fatalf("reply %q: %v", data, err) + } + if err := <-done; err != nil { + t.Fatal(err) + } + deniedClient, err := net.Dial("tcp", listener.Addr().String()) + if err != nil { + t.Fatal(err) + } + defer func() { _ = deniedClient.Close() }() + deniedLocal, err := listener.Accept() + if err != nil { + t.Fatal(err) + } + defer func() { _ = deniedLocal.Close() }() + denied := make(chan error, 1) + go func() { + denied <- forward(context.Background(), transport, Tunnel{Endpoint: "https://localhost:" + strings.Split(packet.LocalAddr().String(), ":")[1], Access: "team/access", Token: deniedToken}, deniedLocal) + }() + select { + case err := <-denied: + if err == nil || !strings.Contains(err.Error(), "401") { + t.Fatalf("expected 401, got %v", err) + } + case <-time.After(5 * time.Second): + t.Fatal("denied stream did not close") + } +} + +func TestRequestRejectsUntrustedEndpoint(t *testing.T) { + for _, endpoint := range []string{"http://relay", "https://relay/path", "https://user@relay", "https://relay?target=db"} { + if _, err := (Tunnel{Endpoint: endpoint, Access: "team/access", Token: proofToken}).request(context.Background(), nil); err == nil { + t.Errorf("accepted %q", endpoint) + } + } +} + +func TestRequestRejectsMalformedTokensWithoutEchoingThem(t *testing.T) { + for name, token := range map[string]string{ + "empty": "", + "short": "c2hvcnQ", + "padded": base64.URLEncoding.EncodeToString(bytes.Repeat([]byte{1}, 32)), + "newline": proofToken + "\n", + "not-base64url": strings.Repeat("!", 43), + } { + t.Run(name, func(t *testing.T) { + _, err := (Tunnel{Endpoint: "https://relay.example:8443", Access: "team/access", Token: token}).request(context.Background(), nil) + if err == nil { + t.Fatal("expected malformed token to be rejected") + } + if token != "" && strings.Contains(err.Error(), token) { + t.Fatalf("error leaks token: %v", err) + } + }) + } +} diff --git a/internal/application/application.go b/internal/application/application.go index 2c3d90ff..90d19f0a 100644 --- a/internal/application/application.go +++ b/internal/application/application.go @@ -12,6 +12,7 @@ import ( appCommand "github.com/nais/cli/internal/app/command" applyCommand "github.com/nais/cli/internal/apply/command" authCommand "github.com/nais/cli/internal/auth/command" + cloudsqlCommand "github.com/nais/cli/internal/cloudsql/command" configCommand "github.com/nais/cli/internal/config/command" debugCommand "github.com/nais/cli/internal/debug/command" "github.com/nais/cli/internal/flags" @@ -27,7 +28,6 @@ import ( naisapiCommand "github.com/nais/cli/internal/naisapi/command" naisdeviceCommand "github.com/nais/cli/internal/naisdevice/command" opensearchCommand "github.com/nais/cli/internal/opensearch/command" - postgresCommand "github.com/nais/cli/internal/postgres/command" secretCommand "github.com/nais/cli/internal/secret/command" statusCommand "github.com/nais/cli/internal/status/command" validateCommand "github.com/nais/cli/internal/validate/command" @@ -85,7 +85,7 @@ func New(w io.Writer) (*Application, *flags.GlobalFlags, error) { naisapiCommand.Api(globalFlags), naisdeviceCommand.Naisdevice(globalFlags), opensearchCommand.OpenSearch(globalFlags), - postgresCommand.Postgres(globalFlags), + cloudsqlCommand.CloudSQL(globalFlags), secretCommand.Secrets(globalFlags), statusCommand.Status(globalFlags), validateCommand.Validate(globalFlags), diff --git a/internal/postgres/access.go b/internal/cloudsql/access.go similarity index 95% rename from internal/postgres/access.go rename to internal/cloudsql/access.go index 424de863..18074b19 100644 --- a/internal/postgres/access.go +++ b/internal/cloudsql/access.go @@ -1,4 +1,4 @@ -package postgres +package cloudsql import ( "context" @@ -6,7 +6,7 @@ import ( "strings" "github.com/lib/pq" - "github.com/nais/cli/internal/postgres/command/flag" + "github.com/nais/cli/internal/cloudsql/command/flag" "github.com/nais/naistrix" ) @@ -36,7 +36,7 @@ var ( func PrepareAccess(ctx context.Context, appName, team, environment string, fl *flag.Prepare, out *naistrix.OutputWriter) error { // Get secret values (access is logged for audit purposes) - sv, err := GetSecretValues(ctx, appName, team, environment, fl.Postgres, ReasonPrepareAccess, out) + sv, err := GetSecretValues(ctx, appName, team, environment, fl.CloudSQL, ReasonPrepareAccess, out) if err != nil { return err } @@ -57,7 +57,7 @@ func PrepareAccess(ctx context.Context, appName, team, environment string, fl *f func RevokeAccess(ctx context.Context, appName, team, environment string, fl *flag.Revoke, out *naistrix.OutputWriter) error { // Get secret values (access is logged for audit purposes) - sv, err := GetSecretValues(ctx, appName, team, environment, fl.Postgres, ReasonRevokeAccess, out) + sv, err := GetSecretValues(ctx, appName, team, environment, fl.CloudSQL, ReasonRevokeAccess, out) if err != nil { return err } diff --git a/internal/postgres/audit.go b/internal/cloudsql/audit.go similarity index 96% rename from internal/postgres/audit.go rename to internal/cloudsql/audit.go index 6495e10f..f9545b29 100644 --- a/internal/postgres/audit.go +++ b/internal/cloudsql/audit.go @@ -1,4 +1,4 @@ -package postgres +package cloudsql import ( "context" @@ -6,7 +6,7 @@ import ( "fmt" "github.com/lib/pq" - "github.com/nais/cli/internal/postgres/command/flag" + "github.com/nais/cli/internal/cloudsql/command/flag" "github.com/nais/naistrix" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" @@ -14,7 +14,7 @@ import ( func EnableAuditLogging(ctx context.Context, appName, team, environment string, fl *flag.EnableAudit, out *naistrix.OutputWriter) error { // Get secret values (access is logged for audit purposes) - sv, err := GetSecretValues(ctx, appName, team, environment, fl.Postgres, ReasonEnableAudit, out) + sv, err := GetSecretValues(ctx, appName, team, environment, fl.CloudSQL, ReasonEnableAudit, out) if err != nil { return err } @@ -23,7 +23,7 @@ func EnableAuditLogging(ctx context.Context, appName, team, environment string, func VerifyAuditLogging(ctx context.Context, appName, team, environment string, fl *flag.VerifyAudit, out *naistrix.OutputWriter) error { // Get secret values (access is logged for audit purposes) - sv, err := GetSecretValues(ctx, appName, team, environment, fl.Postgres, ReasonVerifyAudit, out) + sv, err := GetSecretValues(ctx, appName, team, environment, fl.CloudSQL, ReasonVerifyAudit, out) if err != nil { return err } @@ -44,10 +44,7 @@ func enableAuditAsAppUser(ctx context.Context, appName, team, environment string return err } - cloudSQLDbInfo, err := dbInfo.ToCloudSQLDBInfo() - if err != nil { - return err - } + cloudSQLDbInfo := dbInfo err = validateAuditFlags(ctx, cloudSQLDbInfo) if err != nil { @@ -215,10 +212,7 @@ func verifyAuditAsAppUser(ctx context.Context, appName, team, environment string return false, err } - cloudSQLDbInfo, err := dbInfo.ToCloudSQLDBInfo() - if err != nil { - return false, err - } + cloudSQLDbInfo := dbInfo out.Println("\nVerifying audit configuration for application: " + appName + "\n") diff --git a/internal/postgres/cloudsqldbinfo.go b/internal/cloudsql/cloudsqldbinfo.go similarity index 97% rename from internal/postgres/cloudsqldbinfo.go rename to internal/cloudsql/cloudsqldbinfo.go index 64b33ae1..48e6fba9 100644 --- a/internal/postgres/cloudsqldbinfo.go +++ b/internal/cloudsql/cloudsqldbinfo.go @@ -1,4 +1,4 @@ -package postgres +package cloudsql import ( "context" @@ -29,10 +29,6 @@ type CloudSQLDBInfo struct { secretValues *SecretValues } -func (i *CloudSQLDBInfo) ToCloudSQLDBInfo() (*CloudSQLDBInfo, error) { - return i, nil -} - func (i *CloudSQLDBInfo) SetSecretValues(sv *SecretValues) { i.secretValues = sv } @@ -212,7 +208,7 @@ func (d *CloudSQLDBInfo) RunProxy(ctx context.Context, host string, port *uint, out.Println("If you are using a JDBC client, you can connect to the database by using the following connection string:") out.Printf("Connection URL: jdbc:postgresql://%v/%v?user=%v\n", address, connectionInfo.dbName, email) out.Println() - out.Println("If you get asked for a password, you can leave it blank. If that doesn't work, try running 'nais postgres grant", d.AppName()+"' again.") + out.Println("If you get asked for a password, you can leave it blank. Check your Cloud SQL IAM access if authentication fails.") } err = runProxy(ctx, projectID, connectionName, address, portCh, out) @@ -228,7 +224,7 @@ func (d *CloudSQLDBInfo) RunProxy(ctx context.Context, host string, port *uint, } func runProxy(ctx context.Context, projectID, connectionName, address string, port chan<- int, out *naistrix.OutputWriter) error { - err := checkPostgresqlPassword(out) + err := checkDatabasePassword(out) if err != nil { return err } diff --git a/internal/cloudsql/command/cloudsql.go b/internal/cloudsql/command/cloudsql.go new file mode 100644 index 00000000..7831e829 --- /dev/null +++ b/internal/cloudsql/command/cloudsql.go @@ -0,0 +1,60 @@ +package command + +import ( + "context" + "fmt" + "io" + "os" + "strings" + + "github.com/nais/cli/internal/cloudsql/command/flag" + "github.com/nais/cli/internal/flags" + "github.com/nais/cli/internal/gcloud" + "github.com/nais/naistrix" +) + +func CloudSQL(parentFlags *flags.GlobalFlags) *naistrix.Command { + flags := &flag.CloudSQL{ + GlobalFlags: parentFlags, + } + + return &naistrix.Command{ + Name: "cloudsql", + // TODO: Remove the aliases once users have moved from the old `nais postgres` (Cloud SQL) commands. + Aliases: []string{"postgres", "pg"}, + Title: "Manage Google Cloud SQL instances.", + Description: "Manage Google Cloud SQL instances, including listing, migration, user management, password rotation, and direct database access.", + StickyFlags: flags, + SubCommands: []*naistrix.Command{ + listCommand(flags), + migrateCommand(flags), + passwordCommand(flags), + usersCommand(flags), + enableAuditCommand(flags), + verifyAuditCommand(flags), + prepareCommand(flags), + proxyCommand(flags), + psqlCommand(flags), + revokeCommand(flags), + }, + ValidateFunc: func(ctx context.Context, _ *naistrix.Arguments) error { + warnIfLegacyAlias(os.Args[1:], os.Stderr) + _, err := gcloud.ValidateAndGetUserLogin(ctx, false) + return err + }, + } +} + +// warnIfLegacyAlias tells users who typed `nais postgres` or `nais pg` that these now mean Cloud SQL. +// naistrix does not expose which alias was used, so the first non-flag argument is inspected. +func warnIfLegacyAlias(args []string, w io.Writer) { + for _, arg := range args { + if strings.HasPrefix(arg, "-") { + continue + } + if arg == "postgres" || arg == "pg" { + _, _ = fmt.Fprintf(w, "Warning: nais %s has moved to nais cloudsql. Use nais cloudsql instead, as nais %[1]s will stop working for Cloud SQL in the future.\n", arg) + } + return + } +} diff --git a/internal/cloudsql/command/cloudsql_test.go b/internal/cloudsql/command/cloudsql_test.go new file mode 100644 index 00000000..71ce8b7e --- /dev/null +++ b/internal/cloudsql/command/cloudsql_test.go @@ -0,0 +1,30 @@ +package command + +import ( + "bytes" + "strings" + "testing" +) + +func TestWarnIfLegacyAlias(t *testing.T) { + for name, tt := range map[string]struct { + args []string + warn bool + }{ + "postgres": {[]string{"postgres", "list"}, true}, + "pg": {[]string{"pg", "psql", "app"}, true}, + "flag before": {[]string{"--team", "x", "postgres"}, false}, + "cloudsql": {[]string{"cloudsql", "list"}, false}, + "alpha postgres": {[]string{"alpha", "postgres", "list"}, false}, + "only flags": {[]string{"--help"}, false}, + "no args": {nil, false}, + } { + t.Run(name, func(t *testing.T) { + var buf bytes.Buffer + warnIfLegacyAlias(tt.args, &buf) + if got := strings.Contains(buf.String(), "has moved to nais cloudsql"); got != tt.warn { + t.Fatalf("warned=%v, want %v (%q)", got, tt.warn, buf.String()) + } + }) + } +} diff --git a/internal/postgres/command/enable_audit.go b/internal/cloudsql/command/enable_audit.go similarity index 75% rename from internal/postgres/command/enable_audit.go rename to internal/cloudsql/command/enable_audit.go index dfb78644..3240f624 100644 --- a/internal/postgres/command/enable_audit.go +++ b/internal/cloudsql/command/enable_audit.go @@ -3,15 +3,15 @@ package command import ( "context" + "github.com/nais/cli/internal/cloudsql" + "github.com/nais/cli/internal/cloudsql/command/flag" "github.com/nais/cli/internal/metric" - "github.com/nais/cli/internal/postgres" - "github.com/nais/cli/internal/postgres/command/flag" "github.com/nais/cli/internal/validation" "github.com/nais/naistrix" ) -func enableAuditCommand(parentFlags *flag.Postgres) *naistrix.Command { - flags := &flag.EnableAudit{Postgres: parentFlags} +func enableAuditCommand(parentFlags *flag.CloudSQL) *naistrix.Command { + flags := &flag.EnableAudit{CloudSQL: parentFlags} return &naistrix.Command{ Name: "enable-audit", Title: "Enable audit extension in SQL instance database.", @@ -22,7 +22,7 @@ func enableAuditCommand(parentFlags *flag.Postgres) *naistrix.Command { Flags: flags, ValidateFunc: validation.RequireTeamAndEnvironment(flags), RunFunc: func(ctx context.Context, args *naistrix.Arguments, out *naistrix.OutputWriter) error { - err := postgres.EnableAuditLogging(ctx, args.Get("app_name"), flags.Team, string(flags.Environment), flags, out) + err := cloudsql.EnableAuditLogging(ctx, args.Get("app_name"), flags.Team, string(flags.Environment), flags, out) if err != nil { metric.CreateAndIncreaseCounter(ctx, "enable_audit_logging_error") } diff --git a/internal/postgres/command/flag/flag.go b/internal/cloudsql/command/flag/flag.go similarity index 91% rename from internal/postgres/command/flag/flag.go rename to internal/cloudsql/command/flag/flag.go index 6c93e20c..c34684a9 100644 --- a/internal/postgres/command/flag/flag.go +++ b/internal/cloudsql/command/flag/flag.go @@ -8,13 +8,13 @@ import ( "github.com/nais/naistrix" ) -type Postgres struct { +type CloudSQL struct { *flags.GlobalFlags Reason string `name:"reason" short:"r" usage:"Justification for accessing the database. Required for audit logging."` } type Migrate struct { - *Postgres + *CloudSQL DryRun bool `name:"dry-run" usage:"Perform a dry run of the migration without applying changes."` } @@ -41,7 +41,7 @@ type MigrateRollback struct { } type Password struct { - *Postgres + *CloudSQL } type PasswordRotate struct { @@ -49,7 +49,7 @@ type PasswordRotate struct { } type User struct { - *Postgres + *CloudSQL } type UserAdd struct { @@ -66,45 +66,41 @@ type UserList struct { } type EnableAudit struct { - *Postgres + *CloudSQL } type VerifyAudit struct { - *Postgres -} - -type Grant struct { - *Postgres + *CloudSQL } type Prepare struct { - *Postgres + *CloudSQL AllPrivileges bool `name:"all-privileges" usage:"Grant all privileges on the schema to the current user."` Schema string `name:"schema" usage:"Schema to grant access to."` } type Proxy struct { - *Postgres + *CloudSQL Port uint `name:"port" short:"p" usage:"Port to use for the proxy. Defaults to 5432."` Host string `name:"host" short:"H" usage:"Host to proxy to. Defaults to localhost."` } type Psql struct { - *Postgres + *CloudSQL } type Revoke struct { - *Postgres + *CloudSQL Schema string `name:"schema" usage:"The schema to revoke privileges from."` } type List struct { - *Postgres + *CloudSQL Output Output `name:"output" short:"o" usage:"Format output (table or json)."` Labels labels.LabelFilters `name:"label" short:"l" usage:"Filter by label in |KEY=VALUE| form. Can be repeated."` } -func (*List) LabelFacetResource() string { return "postgresInstances" } +func (*List) LabelFacetResource() string { return "sqlInstances" } type Output string diff --git a/internal/postgres/command/list.go b/internal/cloudsql/command/list.go similarity index 62% rename from internal/postgres/command/list.go rename to internal/cloudsql/command/list.go index 9f7857db..57181b83 100644 --- a/internal/postgres/command/list.go +++ b/internal/cloudsql/command/list.go @@ -3,20 +3,20 @@ package command import ( "context" + "github.com/nais/cli/internal/cloudsql" + "github.com/nais/cli/internal/cloudsql/command/flag" "github.com/nais/cli/internal/labels" - "github.com/nais/cli/internal/postgres" - "github.com/nais/cli/internal/postgres/command/flag" "github.com/nais/naistrix" "github.com/nais/naistrix/output" ) -func listCommand(parentFlags *flag.Postgres) *naistrix.Command { - flags := &flag.List{Postgres: parentFlags} +func listCommand(parentFlags *flag.CloudSQL) *naistrix.Command { + flags := &flag.List{CloudSQL: parentFlags} return &naistrix.Command{ Name: "list", - Title: "List postgres instances for a team.", - Description: "List all Google Cloud SQL Postgres instances owned by a team, showing instance details.", + Title: "List Cloud SQL instances for a team.", + Description: "List Google Cloud SQL instances owned by a team.", Flags: flags, RunFunc: func(ctx context.Context, args *naistrix.Arguments, out *naistrix.OutputWriter) error { labelFilters, err := labels.ParseFilters(flags.Labels) @@ -29,7 +29,7 @@ func listCommand(parentFlags *flag.Postgres) *naistrix.Command { environments = []string{string(flags.Environment)} } - ret, err := postgres.GetTeamPostgresInstances(ctx, flags.Team, environments, labelFilters) + ret, err := cloudsql.GetTeamCloudSQLInstances(ctx, flags.Team, environments, labelFilters) if err != nil { return err } @@ -39,7 +39,7 @@ func listCommand(parentFlags *flag.Postgres) *naistrix.Command { } if len(ret) == 0 { - out.Println("Team has no postgres instances.") + out.Println("Team has no Cloud SQL instances.") return nil } diff --git a/internal/postgres/command/migrate.go b/internal/cloudsql/command/migrate.go similarity index 88% rename from internal/postgres/command/migrate.go rename to internal/cloudsql/command/migrate.go index 3d600d82..0e4cedd9 100644 --- a/internal/postgres/command/migrate.go +++ b/internal/cloudsql/command/migrate.go @@ -7,21 +7,21 @@ import ( "strconv" "strings" - "github.com/nais/cli/internal/postgres/command/flag" - "github.com/nais/cli/internal/postgres/migrate/finalize" - "github.com/nais/cli/internal/postgres/migrate/promote" - "github.com/nais/cli/internal/postgres/migrate/rollback" - "github.com/nais/cli/internal/postgres/migrate/setup" + "github.com/nais/cli/internal/cloudsql/command/flag" + "github.com/nais/cli/internal/cloudsql/migrate/finalize" + "github.com/nais/cli/internal/cloudsql/migrate/promote" + "github.com/nais/cli/internal/cloudsql/migrate/rollback" + "github.com/nais/cli/internal/cloudsql/migrate/setup" "github.com/nais/cli/internal/validation" "github.com/nais/naistrix" ) -func migrateCommand(parentFlags *flag.Postgres) *naistrix.Command { - flags := &flag.Migrate{Postgres: parentFlags} +func migrateCommand(parentFlags *flag.CloudSQL) *naistrix.Command { + flags := &flag.Migrate{CloudSQL: parentFlags} return &naistrix.Command{ Name: "migrate", Title: "Migrate to a new SQL instance.", - Description: "Commands for migrating a Postgres database to a new Cloud SQL instance, including setup, promotion, finalization, and rollback.", + Description: "Commands for migrating a database to a new Cloud SQL instance, including setup, promotion, finalization, and rollback.", StickyFlags: flags, ValidateFunc: validation.RequireTeamAndEnvironment(flags), SubCommands: []*naistrix.Command{ diff --git a/internal/postgres/command/password.go b/internal/cloudsql/command/password.go similarity index 66% rename from internal/postgres/command/password.go rename to internal/cloudsql/command/password.go index 778675cb..6644f1bf 100644 --- a/internal/postgres/command/password.go +++ b/internal/cloudsql/command/password.go @@ -3,18 +3,18 @@ package command import ( "context" - "github.com/nais/cli/internal/postgres" - "github.com/nais/cli/internal/postgres/command/flag" + "github.com/nais/cli/internal/cloudsql" + "github.com/nais/cli/internal/cloudsql/command/flag" "github.com/nais/cli/internal/validation" "github.com/nais/naistrix" ) -func passwordCommand(parentFlags *flag.Postgres) *naistrix.Command { - flags := &flag.Password{Postgres: parentFlags} +func passwordCommand(parentFlags *flag.CloudSQL) *naistrix.Command { + flags := &flag.Password{CloudSQL: parentFlags} return &naistrix.Command{ Name: "password", Title: "Manage SQL instance passwords.", - Description: "Commands for managing Postgres instance passwords, including password rotation.", + Description: "Commands for managing Cloud SQL instance passwords, including password rotation.", StickyFlags: flags, SubCommands: []*naistrix.Command{ { @@ -26,7 +26,7 @@ func passwordCommand(parentFlags *flag.Postgres) *naistrix.Command { }, ValidateFunc: validation.RequireTeamAndEnvironment(flags), RunFunc: func(ctx context.Context, args *naistrix.Arguments, out *naistrix.OutputWriter) error { - return postgres.RotatePassword(ctx, args.Get("app_name"), flags.Team, string(flags.Environment), flags, out) + return cloudsql.RotatePassword(ctx, args.Get("app_name"), flags.Team, string(flags.Environment), flags, out) }, }, }, diff --git a/internal/postgres/command/prepare.go b/internal/cloudsql/command/prepare.go similarity index 83% rename from internal/postgres/command/prepare.go rename to internal/cloudsql/command/prepare.go index 83095005..57bbad9f 100644 --- a/internal/postgres/command/prepare.go +++ b/internal/cloudsql/command/prepare.go @@ -6,16 +6,16 @@ import ( _ "github.com/GoogleCloudPlatform/cloudsql-proxy/proxy/dialers/postgres" "github.com/MakeNowJust/heredoc/v2" - "github.com/nais/cli/internal/postgres" - "github.com/nais/cli/internal/postgres/command/flag" + "github.com/nais/cli/internal/cloudsql" + "github.com/nais/cli/internal/cloudsql/command/flag" "github.com/nais/cli/internal/validation" "github.com/nais/naistrix" "github.com/nais/naistrix/input" ) -func prepareCommand(parentFlags *flag.Postgres) *naistrix.Command { +func prepareCommand(parentFlags *flag.CloudSQL) *naistrix.Command { flags := &flag.Prepare{ - Postgres: parentFlags, + CloudSQL: parentFlags, Schema: "public", } @@ -41,7 +41,7 @@ func prepareCommand(parentFlags *flag.Postgres) *naistrix.Command { return fmt.Errorf("cancelled by user") } - return postgres.PrepareAccess(ctx, args.Get("app_name"), flags.Team, string(flags.Environment), flags, out) + return cloudsql.PrepareAccess(ctx, args.Get("app_name"), flags.Team, string(flags.Environment), flags, out) }, } } diff --git a/internal/postgres/command/proxy.go b/internal/cloudsql/command/proxy.go similarity index 72% rename from internal/postgres/command/proxy.go rename to internal/cloudsql/command/proxy.go index 701828f6..44a57d97 100644 --- a/internal/postgres/command/proxy.go +++ b/internal/cloudsql/command/proxy.go @@ -3,15 +3,15 @@ package command import ( "context" - "github.com/nais/cli/internal/postgres" - "github.com/nais/cli/internal/postgres/command/flag" + "github.com/nais/cli/internal/cloudsql" + "github.com/nais/cli/internal/cloudsql/command/flag" "github.com/nais/cli/internal/validation" "github.com/nais/naistrix" ) -func proxyCommand(parentFlags *flag.Postgres) *naistrix.Command { +func proxyCommand(parentFlags *flag.CloudSQL) *naistrix.Command { flags := &flag.Proxy{ - Postgres: parentFlags, + CloudSQL: parentFlags, Port: 5432, Host: "localhost", } @@ -25,7 +25,7 @@ func proxyCommand(parentFlags *flag.Postgres) *naistrix.Command { Flags: flags, ValidateFunc: validation.RequireTeamAndEnvironment(flags), RunFunc: func(ctx context.Context, args *naistrix.Arguments, out *naistrix.OutputWriter) error { - return postgres.RunProxy(ctx, args.Get("app_name"), flags.Team, string(flags.Environment), flags, out) + return cloudsql.RunProxy(ctx, args.Get("app_name"), flags.Team, string(flags.Environment), flags, out) }, } } diff --git a/internal/postgres/command/psql.go b/internal/cloudsql/command/psql.go similarity index 71% rename from internal/postgres/command/psql.go rename to internal/cloudsql/command/psql.go index 505e7edf..ea6c41ab 100644 --- a/internal/postgres/command/psql.go +++ b/internal/cloudsql/command/psql.go @@ -3,14 +3,14 @@ package command import ( "context" - "github.com/nais/cli/internal/postgres" - "github.com/nais/cli/internal/postgres/command/flag" + "github.com/nais/cli/internal/cloudsql" + "github.com/nais/cli/internal/cloudsql/command/flag" "github.com/nais/cli/internal/validation" "github.com/nais/naistrix" ) -func psqlCommand(parentFlags *flag.Postgres) *naistrix.Command { - flags := &flag.Psql{Postgres: parentFlags} +func psqlCommand(parentFlags *flag.CloudSQL) *naistrix.Command { + flags := &flag.Psql{CloudSQL: parentFlags} return &naistrix.Command{ Name: "psql", Title: "Connect to the database using psql.", @@ -21,7 +21,7 @@ func psqlCommand(parentFlags *flag.Postgres) *naistrix.Command { Flags: flags, ValidateFunc: validation.RequireTeamAndEnvironment(flags), RunFunc: func(ctx context.Context, args *naistrix.Arguments, out *naistrix.OutputWriter) error { - return postgres.RunPSQL(ctx, args.Get("app_name"), flags.Team, string(flags.Environment), flags, out) + return cloudsql.RunPSQL(ctx, args.Get("app_name"), flags.Team, string(flags.Environment), flags, out) }, } } diff --git a/internal/postgres/command/revoke.go b/internal/cloudsql/command/revoke.go similarity index 83% rename from internal/postgres/command/revoke.go rename to internal/cloudsql/command/revoke.go index 1a45a10f..9d64f89f 100644 --- a/internal/postgres/command/revoke.go +++ b/internal/cloudsql/command/revoke.go @@ -6,16 +6,16 @@ import ( _ "github.com/GoogleCloudPlatform/cloudsql-proxy/proxy/dialers/postgres" "github.com/MakeNowJust/heredoc/v2" - "github.com/nais/cli/internal/postgres" - "github.com/nais/cli/internal/postgres/command/flag" + "github.com/nais/cli/internal/cloudsql" + "github.com/nais/cli/internal/cloudsql/command/flag" "github.com/nais/cli/internal/validation" "github.com/nais/naistrix" "github.com/nais/naistrix/input" ) -func revokeCommand(parentFlags *flag.Postgres) *naistrix.Command { +func revokeCommand(parentFlags *flag.CloudSQL) *naistrix.Command { flags := &flag.Revoke{ - Postgres: parentFlags, + CloudSQL: parentFlags, Schema: "public", } return &naistrix.Command{ @@ -40,7 +40,7 @@ func revokeCommand(parentFlags *flag.Postgres) *naistrix.Command { return fmt.Errorf("cancelled by user") } - return postgres.RevokeAccess(ctx, args.Get("app_name"), flags.Team, string(flags.Environment), flags, out) + return cloudsql.RevokeAccess(ctx, args.Get("app_name"), flags.Team, string(flags.Environment), flags, out) }, } } diff --git a/internal/postgres/command/users.go b/internal/cloudsql/command/users.go similarity index 77% rename from internal/postgres/command/users.go rename to internal/cloudsql/command/users.go index cda82424..c09e3cac 100644 --- a/internal/postgres/command/users.go +++ b/internal/cloudsql/command/users.go @@ -3,18 +3,18 @@ package command import ( "context" - "github.com/nais/cli/internal/postgres" - "github.com/nais/cli/internal/postgres/command/flag" + "github.com/nais/cli/internal/cloudsql" + "github.com/nais/cli/internal/cloudsql/command/flag" "github.com/nais/cli/internal/validation" "github.com/nais/naistrix" ) -func usersCommand(parentFlags *flag.Postgres) *naistrix.Command { - flags := &flag.User{Postgres: parentFlags} +func usersCommand(parentFlags *flag.CloudSQL) *naistrix.Command { + flags := &flag.User{CloudSQL: parentFlags} return &naistrix.Command{ Name: "users", Title: "Manage users in your SQL instance.", - Description: "Commands for adding, listing, and dropping users in a Postgres SQL instance.", + Description: "Commands for adding, listing, and dropping users in a Cloud SQL instance.", StickyFlags: flags, ValidateFunc: validation.RequireTeamAndEnvironment(flags), SubCommands: []*naistrix.Command{ @@ -41,7 +41,7 @@ func addCommand(parentFlags *flag.User) *naistrix.Command { }, Flags: flags, RunFunc: func(ctx context.Context, args *naistrix.Arguments, out *naistrix.OutputWriter) error { - return postgres.AddUser(ctx, args.Get("app_name"), flags.Team, string(flags.Environment), args.Get("username"), args.Get("password"), flags, out) + return cloudsql.AddUser(ctx, args.Get("app_name"), flags.Team, string(flags.Environment), args.Get("username"), args.Get("password"), flags, out) }, } } @@ -51,13 +51,13 @@ func listUsersCommand(parentFlags *flag.User) *naistrix.Command { return &naistrix.Command{ Name: "list", Title: "List users in a SQL instance database.", - Description: "List all users in a Postgres SQL instance database for a given application.", + Description: "List all users in a Cloud SQL instance database for a given application.", Args: []naistrix.Argument{ {Name: "app_name"}, }, Flags: flags, RunFunc: func(ctx context.Context, args *naistrix.Arguments, out *naistrix.OutputWriter) error { - return postgres.ListUsers(ctx, args.Get("app_name"), flags.Team, string(flags.Environment), flags, out) + return cloudsql.ListUsers(ctx, args.Get("app_name"), flags.Team, string(flags.Environment), flags, out) }, } } @@ -67,14 +67,14 @@ func dropCommand(parentFlags *flag.User) *naistrix.Command { return &naistrix.Command{ Name: "drop", Title: "Drop a user from a SQL instance database.", - Description: "Remove a user from a Postgres SQL instance database.", + Description: "Remove a user from a Cloud SQL instance database.", Args: []naistrix.Argument{ {Name: "app_name"}, {Name: "username"}, }, Flags: flags, RunFunc: func(ctx context.Context, args *naistrix.Arguments, out *naistrix.OutputWriter) error { - return postgres.DropUser(ctx, args.Get("app_name"), flags.Team, string(flags.Environment), args.Get("username"), flags, out) + return cloudsql.DropUser(ctx, args.Get("app_name"), flags.Team, string(flags.Environment), args.Get("username"), flags, out) }, } } diff --git a/internal/postgres/command/verify_audit.go b/internal/cloudsql/command/verify_audit.go similarity index 75% rename from internal/postgres/command/verify_audit.go rename to internal/cloudsql/command/verify_audit.go index ca735c14..de9f7613 100644 --- a/internal/postgres/command/verify_audit.go +++ b/internal/cloudsql/command/verify_audit.go @@ -3,15 +3,15 @@ package command import ( "context" + "github.com/nais/cli/internal/cloudsql" + "github.com/nais/cli/internal/cloudsql/command/flag" "github.com/nais/cli/internal/metric" - "github.com/nais/cli/internal/postgres" - "github.com/nais/cli/internal/postgres/command/flag" "github.com/nais/cli/internal/validation" "github.com/nais/naistrix" ) -func verifyAuditCommand(parentFlags *flag.Postgres) *naistrix.Command { - flags := &flag.VerifyAudit{Postgres: parentFlags} +func verifyAuditCommand(parentFlags *flag.CloudSQL) *naistrix.Command { + flags := &flag.VerifyAudit{CloudSQL: parentFlags} return &naistrix.Command{ Name: "verify-audit", Title: "Verify audit extension and configuration in SQL instance database.", @@ -22,7 +22,7 @@ func verifyAuditCommand(parentFlags *flag.Postgres) *naistrix.Command { Flags: flags, ValidateFunc: validation.RequireTeamAndEnvironment(flags), RunFunc: func(ctx context.Context, args *naistrix.Arguments, out *naistrix.OutputWriter) error { - err := postgres.VerifyAuditLogging(ctx, args.Get("app_name"), flags.Team, string(flags.Environment), flags, out) + err := cloudsql.VerifyAuditLogging(ctx, args.Get("app_name"), flags.Team, string(flags.Environment), flags, out) if err != nil { metric.CreateAndIncreaseCounter(ctx, "verify_audit_logging_error") } diff --git a/internal/cloudsql/dbinfo.go b/internal/cloudsql/dbinfo.go new file mode 100644 index 00000000..ef8a47f9 --- /dev/null +++ b/internal/cloudsql/dbinfo.go @@ -0,0 +1,85 @@ +package cloudsql + +import ( + "context" + "errors" + "fmt" + "net/url" + + "golang.org/x/oauth2" + "k8s.io/client-go/dynamic" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/tools/clientcmd" +) + +type DBInfo struct { + k8sClient kubernetes.Interface + dynamicClient dynamic.Interface + config clientcmd.ClientConfig + namespace string + appName string +} + +func NewDBInfo(_ context.Context, appName, team, environment string) (*CloudSQLDBInfo, error) { + loadingRules := clientcmd.NewDefaultClientConfigLoadingRules() + kubeConfig := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loadingRules, &clientcmd.ConfigOverrides{CurrentContext: environment}) + config, err := kubeConfig.ClientConfig() + if err != nil { + return nil, fmt.Errorf("NewDBInfo: unable to get kubeconfig: %w", err) + } + if team == "" { + team, _, err = kubeConfig.Namespace() + if err != nil { + return nil, fmt.Errorf("NewDBInfo: unable to get namespace: %w", err) + } + } + k8sClient, err := kubernetes.NewForConfig(config) + if err != nil { + return nil, fmt.Errorf("NewDBInfo: load kubeclient configuration: %w", err) + } + dynamicClient, err := dynamic.NewForConfig(config) + if err != nil { + return nil, fmt.Errorf("NewDBInfo: load kubeclient configuration: %w", err) + } + return &CloudSQLDBInfo{DBInfo: &DBInfo{k8sClient: k8sClient, dynamicClient: dynamicClient, config: kubeConfig, namespace: team, appName: appName}}, nil +} + +type ConnectionInfo struct { + username string + email string + password string + dbName string + instance string + port string + url *url.URL + jdbcUrl *url.URL +} + +func (c *ConnectionInfo) ProxyConnectionString() string { + return fmt.Sprintf("host=%v user=%v dbname=%v password=%v sslmode=disable", c.instance, c.username, c.dbName, c.password) +} + +func (c *ConnectionInfo) SetPassword(password string) { + c.password = password + if c.url != nil { + c.url.User = url.UserPassword(c.username, password) + } + if c.jdbcUrl != nil { + queries := c.jdbcUrl.Query() + queries.Set("password", password) + c.jdbcUrl.RawQuery = queries.Encode() + } else if c.url != nil { + queries := c.url.Query() + queries.Set("password", password) + queries.Set("user", c.username) + c.jdbcUrl = &url.URL{Scheme: "jdbc:postgresql", Host: c.url.Host, Path: c.dbName, RawQuery: queries.Encode()} + } +} + +func formatInvalidGrantError(err error) error { + var retrieve *oauth2.RetrieveError + if errors.As(err, &retrieve) && retrieve.ErrorCode == "invalid_grant" { + return fmt.Errorf("looks like you are missing Application Default Credentials, run `gcloud auth login --update-adc` first") + } + return err +} diff --git a/internal/postgres/iam.go b/internal/cloudsql/iam.go similarity index 81% rename from internal/postgres/iam.go rename to internal/cloudsql/iam.go index 6daa8f6a..f222504f 100644 --- a/internal/postgres/iam.go +++ b/internal/cloudsql/iam.go @@ -1,4 +1,4 @@ -package postgres +package cloudsql import ( "bytes" @@ -13,73 +13,10 @@ import ( "strings" "time" - "github.com/nais/cli/internal/postgres/command/flag" + "github.com/nais/cli/internal/cloudsql/command/flag" "github.com/nais/naistrix" ) -func GrantAndCreateSQLUser(ctx context.Context, appName, team, environment string, out *naistrix.OutputWriter) error { - dbInfo, err := NewDBInfo(ctx, appName, team, environment) - if err != nil { - return err - } - - cloudSQLDBInfo, err := dbInfo.ToCloudSQLDBInfo() - if err != nil { - return err - } - - projectID, err := cloudSQLDBInfo.ProjectID(ctx) - if err != nil { - return err - } - - connectionName, err := cloudSQLDBInfo.ConnectionName(ctx) - if err != nil { - return err - } - - out.Println("Grant user access") - err = grantUserAccess(ctx, projectID, "roles/cloudsql.admin", 5*time.Minute, out) - if err != nil { - return err - } - - out.Println("Create sql user") - err = createSQLUser(ctx, projectID, connectionName) - if err != nil { - return fmt.Errorf("error creating SQL user. One might already exist: %v", err) - } - - return nil -} - -func createSQLUser(ctx context.Context, projectID, instance string) error { - email, err := currentEmail(ctx) - if err != nil { - return err - } - - args := []string{ - "sql", - "users", - "create", - email, - "--instance", strings.Split(instance, ":")[2], - "--type", "cloud_iam_user", - "--project", projectID, - } - - buf := &bytes.Buffer{} - cmd := exec.CommandContext(ctx, "gcloud", args...) - cmd.Stdout = buf - cmd.Stderr = os.Stderr - if err := cmd.Run(); err != nil { - _, _ = io.Copy(os.Stdout, buf) - return fmt.Errorf("error running gcloud command: %w", err) - } - return nil -} - func currentEmail(ctx context.Context) (string, error) { cmd := exec.CommandContext(ctx, "gcloud", "config", "get-value", "account") out, err := cmd.Output() @@ -214,7 +151,7 @@ func formatCondition(expr, title string) string { func ListUsers(ctx context.Context, appName, team, environment string, fl *flag.UserList, out *naistrix.OutputWriter) error { // Get secret values (access is logged for audit purposes) - sv, err := GetSecretValues(ctx, appName, team, environment, fl.Postgres, ReasonListUsers, out) + sv, err := GetSecretValues(ctx, appName, team, environment, fl.CloudSQL, ReasonListUsers, out) if err != nil { return err } @@ -266,7 +203,7 @@ func AddUser(ctx context.Context, appName, team, environment, username, password } // Get secret values (access is logged for audit purposes) - sv, err := GetSecretValues(ctx, appName, team, environment, fl.Postgres, ReasonAddUser, out) + sv, err := GetSecretValues(ctx, appName, team, environment, fl.CloudSQL, ReasonAddUser, out) if err != nil { return err } @@ -309,7 +246,7 @@ func AddUser(ctx context.Context, appName, team, environment, username, password func DropUser(ctx context.Context, appName, team, environment, username string, fl *flag.UserDrop, out *naistrix.OutputWriter) error { // Get secret values (access is logged for audit purposes) - sv, err := GetSecretValues(ctx, appName, team, environment, fl.Postgres, ReasonDropUser, out) + sv, err := GetSecretValues(ctx, appName, team, environment, fl.CloudSQL, ReasonDropUser, out) if err != nil { return err } diff --git a/internal/cloudsql/list.go b/internal/cloudsql/list.go new file mode 100644 index 00000000..24904e43 --- /dev/null +++ b/internal/cloudsql/list.go @@ -0,0 +1,98 @@ +package cloudsql + +import ( + "context" + "fmt" + "slices" + "sort" + + "github.com/nais/cli/internal/naisapi" + "github.com/nais/cli/internal/naisapi/gql" + "github.com/nais/naistrix/output" + "k8s.io/utils/ptr" +) + +const consoleBaseURL = "https://console.nav.cloud.nais.io" + +type Instance struct { + Name output.Link `json:"name"` + Type string `json:"type"` + Environment string `json:"environment"` + Version string `heading:"Version" json:"version"` + HighAvailability bool `heading:"HA" json:"high_availability"` + Audit *bool `json:"audit,omitempty"` + State State `json:"state"` +} + +type State string + +func (s State) String() string { + switch s { + case State(gql.SqlInstanceStateRunnable): + return "Runnable" + case State(gql.SqlInstanceStateStopped): + return "Stopped" + case State(gql.SqlInstanceStateSuspended): + return "Suspended" + case State(gql.SqlInstanceStatePendingCreate): + return "Pending Create" + case State(gql.SqlInstanceStatePendingDelete): + return "Pending Delete" + case State(gql.SqlInstanceStateMaintenance): + return "Maintenance" + case State(gql.SqlInstanceStateFailed): + return "Failed" + } + return "Unknown" +} + +func GetTeamCloudSQLInstances(ctx context.Context, team string, environments []string, labelFilters []gql.LabelFilter) ([]Instance, error) { + _ = `# @genqlient + query GetTeamCloudSQLInstances($team: Slug!, $sqlFilter: SqlInstanceFilter) { + team(slug: $team) { + sqlInstances(first: 1000, filter: $sqlFilter) { + nodes { + name + teamEnvironment { environment { name } } + version + highAvailability + # @genqlient(pointer: true) + auditLog { logUrl } + state + } + } + } + } + ` + client, err := naisapi.GraphqlClient(ctx) + if err != nil { + return nil, err + } + resp, err := gql.GetTeamCloudSQLInstances(ctx, client, team, &gql.SqlInstanceFilter{Labels: labelFilters}) + if err != nil { + return nil, err + } + return instancesFromTeam(resp.Team, team, environments), nil +} + +func instancesFromTeam(teamData gql.GetTeamCloudSQLInstancesTeam, team string, environments []string) []Instance { + var ret []Instance + for _, s := range teamData.SqlInstances.Nodes { + env := s.TeamEnvironment.Environment.Name + if len(environments) > 0 && !slices.Contains(environments, env) { + continue + } + ret = append(ret, Instance{ + Name: output.Link{Name: s.Name, URL: fmt.Sprintf("%s/team/%s/%s/cloudsql/%s", consoleBaseURL, team, env, s.Name)}, + Type: "Cloud SQL", Environment: env, Version: ptr.Deref(s.Version, ""), + HighAvailability: s.HighAvailability, Audit: new(s.AuditLog != nil), State: State(s.State), + }) + } + sort.Slice(ret, func(i, j int) bool { + if ret[i].Name.Name == ret[j].Name.Name { + return ret[i].Environment < ret[j].Environment + } + return ret[i].Name.Name < ret[j].Name.Name + }) + return ret +} diff --git a/internal/cloudsql/list_test.go b/internal/cloudsql/list_test.go new file mode 100644 index 00000000..8c496624 --- /dev/null +++ b/internal/cloudsql/list_test.go @@ -0,0 +1,40 @@ +package cloudsql + +import ( + "encoding/json" + "reflect" + "testing" + + "github.com/nais/cli/internal/naisapi/gql" + "github.com/nais/naistrix/output" +) + +func TestInstancesFromTeam(t *testing.T) { + const teamData = `{"sqlInstances":{"nodes":[ + {"name":"other","teamEnvironment":{"environment":{"name":"prod"}},"version":null,"highAvailability":true,"auditLog":null,"state":"STOPPED"}, + {"name":"legacy","teamEnvironment":{"environment":{"name":"dev"}},"version":"POSTGRES_14","highAvailability":false,"auditLog":{"logUrl":"https://example.test"},"state":"RUNNABLE"} + ]}}` + var data gql.GetTeamCloudSQLInstancesTeam + if err := json.Unmarshal([]byte(teamData), &data); err != nil { + t.Fatal(err) + } + for _, tt := range []struct { + name string + environments []string + want []Instance + }{ + {name: "sorted", want: []Instance{ + {Name: output.Link{Name: "legacy", URL: consoleBaseURL + "/team/my-team/dev/cloudsql/legacy"}, Type: "Cloud SQL", Environment: "dev", Version: "POSTGRES_14", Audit: new(true), State: State(gql.SqlInstanceStateRunnable)}, + {Name: output.Link{Name: "other", URL: consoleBaseURL + "/team/my-team/prod/cloudsql/other"}, Type: "Cloud SQL", Environment: "prod", HighAvailability: true, Audit: new(false), State: State(gql.SqlInstanceStateStopped)}, + }}, + {name: "environment filter", environments: []string{"dev"}, want: []Instance{ + {Name: output.Link{Name: "legacy", URL: consoleBaseURL + "/team/my-team/dev/cloudsql/legacy"}, Type: "Cloud SQL", Environment: "dev", Version: "POSTGRES_14", Audit: new(true), State: State(gql.SqlInstanceStateRunnable)}, + }}, + } { + t.Run(tt.name, func(t *testing.T) { + if got := instancesFromTeam(data, "my-team", tt.environments); !reflect.DeepEqual(got, tt.want) { + t.Errorf("instancesFromTeam() = %#v, want %#v", got, tt.want) + } + }) + } +} diff --git a/internal/postgres/migrate/config/config.go b/internal/cloudsql/migrate/config/config.go similarity index 100% rename from internal/postgres/migrate/config/config.go rename to internal/cloudsql/migrate/config/config.go diff --git a/internal/postgres/migrate/config/config_test.go b/internal/cloudsql/migrate/config/config_test.go similarity index 99% rename from internal/postgres/migrate/config/config_test.go rename to internal/cloudsql/migrate/config/config_test.go index 1140c389..2bc1d251 100644 --- a/internal/postgres/migrate/config/config_test.go +++ b/internal/cloudsql/migrate/config/config_test.go @@ -6,8 +6,8 @@ import ( "strconv" "testing" + "github.com/nais/cli/internal/cloudsql/migrate/config" "github.com/nais/cli/internal/option" - "github.com/nais/cli/internal/postgres/migrate/config" nais_io_v1 "github.com/nais/liberator/pkg/apis/nais.io/v1" nais_io_v1alpha1 "github.com/nais/liberator/pkg/apis/nais.io/v1alpha1" liberatorscheme "github.com/nais/liberator/pkg/scheme" diff --git a/internal/postgres/migrate/finalize.go b/internal/cloudsql/migrate/finalize.go similarity index 100% rename from internal/postgres/migrate/finalize.go rename to internal/cloudsql/migrate/finalize.go diff --git a/internal/postgres/migrate/finalize/command.go b/internal/cloudsql/migrate/finalize/command.go similarity index 87% rename from internal/postgres/migrate/finalize/command.go rename to internal/cloudsql/migrate/finalize/command.go index 8ee95394..6509dc2a 100644 --- a/internal/postgres/migrate/finalize/command.go +++ b/internal/cloudsql/migrate/finalize/command.go @@ -4,10 +4,10 @@ import ( "context" "fmt" + "github.com/nais/cli/internal/cloudsql/migrate" + "github.com/nais/cli/internal/cloudsql/migrate/config" "github.com/nais/cli/internal/k8s" "github.com/nais/cli/internal/option" - "github.com/nais/cli/internal/postgres/migrate" - "github.com/nais/cli/internal/postgres/migrate/config" ) func Run(ctx context.Context, applicationName, targetInstanceName, team, environment string, dryRun bool) error { diff --git a/internal/postgres/migrate/migrate.go b/internal/cloudsql/migrate/migrate.go similarity index 99% rename from internal/postgres/migrate/migrate.go rename to internal/cloudsql/migrate/migrate.go index 58618294..c2023674 100644 --- a/internal/postgres/migrate/migrate.go +++ b/internal/cloudsql/migrate/migrate.go @@ -12,7 +12,7 @@ import ( "strings" "time" - "github.com/nais/cli/internal/postgres/migrate/config" + "github.com/nais/cli/internal/cloudsql/migrate/config" nais_io_v1 "github.com/nais/liberator/pkg/apis/nais.io/v1" "github.com/nais/liberator/pkg/namegen" "github.com/pterm/pterm" diff --git a/internal/postgres/migrate/migrate_test.go b/internal/cloudsql/migrate/migrate_test.go similarity index 96% rename from internal/postgres/migrate/migrate_test.go rename to internal/cloudsql/migrate/migrate_test.go index 4c06127d..69faf16c 100644 --- a/internal/postgres/migrate/migrate_test.go +++ b/internal/cloudsql/migrate/migrate_test.go @@ -3,8 +3,8 @@ package migrate import ( "testing" + "github.com/nais/cli/internal/cloudsql/migrate/config" "github.com/nais/cli/internal/option" - "github.com/nais/cli/internal/postgres/migrate/config" ) func TestCommand(t *testing.T) { diff --git a/internal/postgres/migrate/promote.go b/internal/cloudsql/migrate/promote.go similarity index 93% rename from internal/postgres/migrate/promote.go rename to internal/cloudsql/migrate/promote.go index 342ee05a..5ab7c7f9 100644 --- a/internal/postgres/migrate/promote.go +++ b/internal/cloudsql/migrate/promote.go @@ -4,7 +4,7 @@ import ( "context" "fmt" - "github.com/nais/cli/internal/postgres/migrate/ui" + "github.com/nais/cli/internal/cloudsql/migrate/ui" "github.com/pterm/pterm" ) @@ -59,7 +59,7 @@ The database will be unavailable for a short period of time while the promotion pterm.Info.Println(`At this point it is important to verify that your application works as expected, and that all data is present. It is now possible to deploy changes to your application, but you must update the manifest to use the new database instance before doing so (see below). Once you are satisfied that everything works as expected, you must perform the final finalize step:`) - ui.CmdStyle.Printfln("\tnais postgres migrate finalize %s %s", m.cfg.AppName, m.cfg.Target.InstanceName) + ui.CmdStyle.Printfln("\tnais cloudsql migrate finalize %s %s", m.cfg.AppName, m.cfg.Target.InstanceName) pterm.Println() pterm.Info.Println("Your next application deploy must update your manifests to use the new database instance:") diskSizeLine := "" @@ -78,6 +78,6 @@ Once you are satisfied that everything works as expected, you must perform the f `, m.cfg.Target.InstanceName, m.cfg.Target.Type, m.cfg.Target.Tier, diskSizeLine) pterm.Println() pterm.Println("If things are not working as expected, and you need to rollback to the previous database instance, you can run:") - ui.CmdStyle.Printfln("\tnais postgres migrate rollback %s %s", m.cfg.AppName, m.cfg.Target.InstanceName) + ui.CmdStyle.Printfln("\tnais cloudsql migrate rollback %s %s", m.cfg.AppName, m.cfg.Target.InstanceName) return nil } diff --git a/internal/postgres/migrate/promote/command.go b/internal/cloudsql/migrate/promote/command.go similarity index 83% rename from internal/postgres/migrate/promote/command.go rename to internal/cloudsql/migrate/promote/command.go index 3fcf1de0..107deff2 100644 --- a/internal/postgres/migrate/promote/command.go +++ b/internal/cloudsql/migrate/promote/command.go @@ -4,11 +4,11 @@ import ( "context" "fmt" + "github.com/nais/cli/internal/cloudsql/command/flag" + "github.com/nais/cli/internal/cloudsql/migrate" + "github.com/nais/cli/internal/cloudsql/migrate/config" "github.com/nais/cli/internal/k8s" "github.com/nais/cli/internal/option" - "github.com/nais/cli/internal/postgres/command/flag" - "github.com/nais/cli/internal/postgres/migrate" - "github.com/nais/cli/internal/postgres/migrate/config" ) func Run(ctx context.Context, applicationName, targetInstanceName, team, environment string, flags *flag.MigratePromote) error { diff --git a/internal/postgres/migrate/rollback.go b/internal/cloudsql/migrate/rollback.go similarity index 100% rename from internal/postgres/migrate/rollback.go rename to internal/cloudsql/migrate/rollback.go diff --git a/internal/postgres/migrate/rollback/command.go b/internal/cloudsql/migrate/rollback/command.go similarity index 83% rename from internal/postgres/migrate/rollback/command.go rename to internal/cloudsql/migrate/rollback/command.go index aad1ba25..2acc7d87 100644 --- a/internal/postgres/migrate/rollback/command.go +++ b/internal/cloudsql/migrate/rollback/command.go @@ -4,11 +4,11 @@ import ( "context" "fmt" + "github.com/nais/cli/internal/cloudsql/command/flag" + "github.com/nais/cli/internal/cloudsql/migrate" + "github.com/nais/cli/internal/cloudsql/migrate/config" "github.com/nais/cli/internal/k8s" "github.com/nais/cli/internal/option" - "github.com/nais/cli/internal/postgres/command/flag" - "github.com/nais/cli/internal/postgres/migrate" - "github.com/nais/cli/internal/postgres/migrate/config" ) func Run(ctx context.Context, applicationName, targetInstanceName, team, environment string, flags *flag.MigrateRollback) error { diff --git a/internal/postgres/migrate/setup.go b/internal/cloudsql/migrate/setup.go similarity index 97% rename from internal/postgres/migrate/setup.go rename to internal/cloudsql/migrate/setup.go index 4d886727..d148bc86 100644 --- a/internal/postgres/migrate/setup.go +++ b/internal/cloudsql/migrate/setup.go @@ -5,9 +5,9 @@ import ( "errors" "fmt" + "github.com/nais/cli/internal/cloudsql/migrate/config" + "github.com/nais/cli/internal/cloudsql/migrate/ui" "github.com/nais/cli/internal/option" - "github.com/nais/cli/internal/postgres/migrate/config" - "github.com/nais/cli/internal/postgres/migrate/ui" "github.com/nais/liberator/pkg/namegen" "github.com/pterm/pterm" v1 "k8s.io/api/core/v1" @@ -141,7 +141,7 @@ func (m *Migrator) Setup(ctx context.Context) error { ui.LinkStyle.Println("\thttps://docs.nais.io/persistence/cloudsql/how-to/personal-access/") pterm.Println() pterm.DefaultParagraph.Println("When you are ready to proceed with the next step of the migration, run the promote command:") - ui.CmdStyle.Printfln("\tnais postgres migrate promote %s %s", m.cfg.AppName, m.cfg.Target.InstanceName) + ui.CmdStyle.Printfln("\tnais cloudsql migrate promote %s %s", m.cfg.AppName, m.cfg.Target.InstanceName) pterm.Println() pterm.Info.Println("Be aware that during promotion (the next step), your instance will be unavailable for some time.") return nil diff --git a/internal/postgres/migrate/setup/command.go b/internal/cloudsql/migrate/setup/command.go similarity index 90% rename from internal/postgres/migrate/setup/command.go rename to internal/cloudsql/migrate/setup/command.go index c0816bde..5a43b8b0 100644 --- a/internal/postgres/migrate/setup/command.go +++ b/internal/cloudsql/migrate/setup/command.go @@ -4,11 +4,11 @@ import ( "context" "fmt" + "github.com/nais/cli/internal/cloudsql/command/flag" + "github.com/nais/cli/internal/cloudsql/migrate" + "github.com/nais/cli/internal/cloudsql/migrate/config" "github.com/nais/cli/internal/k8s" "github.com/nais/cli/internal/option" - "github.com/nais/cli/internal/postgres/command/flag" - "github.com/nais/cli/internal/postgres/migrate" - "github.com/nais/cli/internal/postgres/migrate/config" ) func Run(ctx context.Context, applicationName, targetInstanceName, team, environment string, flags *flag.MigrateSetup) error { diff --git a/internal/postgres/migrate/setup_test.go b/internal/cloudsql/migrate/setup_test.go similarity index 98% rename from internal/postgres/migrate/setup_test.go rename to internal/cloudsql/migrate/setup_test.go index fe1b2a02..8dc88ecb 100644 --- a/internal/postgres/migrate/setup_test.go +++ b/internal/cloudsql/migrate/setup_test.go @@ -6,10 +6,10 @@ import ( "strings" "testing" + "github.com/nais/cli/internal/cloudsql/migrate" + "github.com/nais/cli/internal/cloudsql/migrate/config" + "github.com/nais/cli/internal/cloudsql/migrate/ui" "github.com/nais/cli/internal/option" - "github.com/nais/cli/internal/postgres/migrate" - "github.com/nais/cli/internal/postgres/migrate/config" - "github.com/nais/cli/internal/postgres/migrate/ui" nais_io_v1 "github.com/nais/liberator/pkg/apis/nais.io/v1" nais_io_v1alpha1 "github.com/nais/liberator/pkg/apis/nais.io/v1alpha1" liberatorscheme "github.com/nais/liberator/pkg/scheme" diff --git a/internal/postgres/migrate/ui/ui.go b/internal/cloudsql/migrate/ui/ui.go similarity index 97% rename from internal/postgres/migrate/ui/ui.go rename to internal/cloudsql/migrate/ui/ui.go index d940cc9a..141a98e9 100644 --- a/internal/postgres/migrate/ui/ui.go +++ b/internal/cloudsql/migrate/ui/ui.go @@ -112,7 +112,7 @@ func askForTier(sourceTier string) func() option.Option[string] { } return askForOption("Select a tier for the target instance", sourceTier, options, stringCaster, func() string { pterm.Println("Check the documentation for possible options:") - LinkStyle.Printfln("\thttps://doc.nais.io/persistence/postgres/reference/#server-size") + LinkStyle.Printfln("\thttps://docs.nais.io/persistence/cloudsql/reference/#server-size") tier, err := TextInput.Show("Enter the tier for the target instance") if err != nil { log.Fatalf("Error while creating text UI: %v", err) @@ -141,7 +141,7 @@ var AskForType = askForType // It returns a function that can be called to ask for the type. // The function returns the selected type as an Option[string]. // If the selected type is the "Same as source" type, it returns None[string]. -// It is not possible to select a type (postgres version) less than source. +// It is not possible to select a type (database version) less than source. // The selected value is returned as Some[string]. func askForType(sourceType string) func() option.Option[string] { sourceVersion := typeToVersion[sourceType] diff --git a/internal/postgres/migrate/ui/ui_test.go b/internal/cloudsql/migrate/ui/ui_test.go similarity index 99% rename from internal/postgres/migrate/ui/ui_test.go rename to internal/cloudsql/migrate/ui/ui_test.go index 087ff58e..d4db7b25 100644 --- a/internal/postgres/migrate/ui/ui_test.go +++ b/internal/cloudsql/migrate/ui/ui_test.go @@ -6,8 +6,8 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/nais/cli/internal/cloudsql/migrate/ui" "github.com/nais/cli/internal/option" - "github.com/nais/cli/internal/postgres/migrate/ui" ) type fakeTextInput struct { diff --git a/internal/postgres/password.go b/internal/cloudsql/password.go similarity index 95% rename from internal/postgres/password.go rename to internal/cloudsql/password.go index 1497c029..116bc0e9 100644 --- a/internal/postgres/password.go +++ b/internal/cloudsql/password.go @@ -1,4 +1,4 @@ -package postgres +package cloudsql import ( "bytes" @@ -11,7 +11,7 @@ import ( "strings" "time" - "github.com/nais/cli/internal/postgres/command/flag" + "github.com/nais/cli/internal/cloudsql/command/flag" "github.com/nais/liberator/pkg/keygen" "github.com/nais/naistrix" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -19,7 +19,7 @@ import ( func RotatePassword(ctx context.Context, appName, team, environment string, fl *flag.Password, out *naistrix.OutputWriter) error { // Get secret values (access is logged for audit purposes) - sv, err := GetSecretValues(ctx, appName, team, environment, fl.Postgres, ReasonPasswordRotate, out) + sv, err := GetSecretValues(ctx, appName, team, environment, fl.CloudSQL, ReasonPasswordRotate, out) if err != nil { return err } @@ -31,10 +31,7 @@ func RotatePassword(ctx context.Context, appName, team, environment string, fl * dbInfo.SetSecretValues(sv) - cloudSQLDBInfo, err := dbInfo.ToCloudSQLDBInfo() - if err != nil { - return err - } + cloudSQLDBInfo := dbInfo projectID, err := cloudSQLDBInfo.ProjectID(ctx) if err != nil { diff --git a/internal/postgres/password_test.go b/internal/cloudsql/password_test.go similarity index 99% rename from internal/postgres/password_test.go rename to internal/cloudsql/password_test.go index f9f0de5a..65fca7fc 100644 --- a/internal/postgres/password_test.go +++ b/internal/cloudsql/password_test.go @@ -1,4 +1,4 @@ -package postgres +package cloudsql import ( "fmt" diff --git a/internal/postgres/proxy.go b/internal/cloudsql/proxy.go similarity index 87% rename from internal/postgres/proxy.go rename to internal/cloudsql/proxy.go index 76100235..fdcac14f 100644 --- a/internal/postgres/proxy.go +++ b/internal/cloudsql/proxy.go @@ -1,4 +1,4 @@ -package postgres +package cloudsql import ( "context" @@ -7,13 +7,13 @@ import ( "os" "path/filepath" - "github.com/nais/cli/internal/postgres/command/flag" + "github.com/nais/cli/internal/cloudsql/command/flag" "github.com/nais/naistrix" ) func RunProxy(ctx context.Context, appName, team, environment string, fl *flag.Proxy, out *naistrix.OutputWriter) error { // Get secret values with user-provided reason (access is logged for audit purposes) - sv, err := GetSecretValuesWithUserReason(ctx, appName, team, environment, fl.Postgres, fl.Reason, out) + sv, err := GetSecretValuesWithUserReason(ctx, appName, team, environment, fl.CloudSQL, fl.Reason, out) if err != nil { return err } @@ -33,7 +33,7 @@ func copy(closer chan struct{}, dst io.Writer, src io.Reader) { closer <- struct{}{} // connection is closed, send signal to stop proxy } -func checkPostgresqlPassword(out *naistrix.OutputWriter) error { +func checkDatabasePassword(out *naistrix.OutputWriter) error { if _, ok := os.LookupEnv("PGPASSWORD"); ok { return fmt.Errorf("PGPASSWORD is set, please unset it before running this command") } diff --git a/internal/postgres/psql.go b/internal/cloudsql/psql.go similarity index 93% rename from internal/postgres/psql.go rename to internal/cloudsql/psql.go index 8bb0786e..7a7afdb0 100644 --- a/internal/postgres/psql.go +++ b/internal/cloudsql/psql.go @@ -1,4 +1,4 @@ -package postgres +package cloudsql import ( "context" @@ -7,13 +7,13 @@ import ( "os" "os/exec" - "github.com/nais/cli/internal/postgres/command/flag" + "github.com/nais/cli/internal/cloudsql/command/flag" "github.com/nais/naistrix" ) func RunPSQL(ctx context.Context, appName, team, environment string, fl *flag.Psql, out *naistrix.OutputWriter) error { // Get secret values with user-provided reason (access is logged for audit purposes) - sv, err := GetSecretValuesWithUserReason(ctx, appName, team, environment, fl.Postgres, fl.Reason, out) + sv, err := GetSecretValuesWithUserReason(ctx, appName, team, environment, fl.CloudSQL, fl.Reason, out) if err != nil { return err } diff --git a/internal/cloudsql/secret.go b/internal/cloudsql/secret.go new file mode 100644 index 00000000..af6079e5 --- /dev/null +++ b/internal/cloudsql/secret.go @@ -0,0 +1,72 @@ +package cloudsql + +import ( + "context" + "fmt" + "strings" + + "github.com/nais/cli/internal/cloudsql/command/flag" + "github.com/nais/cli/internal/naisapi" + "github.com/nais/naistrix" +) + +const ( + ReasonPasswordRotate = "Rotating database password via nais CLI" + ReasonPrepareAccess = "Preparing database for IAM user access via nais CLI" + ReasonRevokeAccess = "Revoking IAM user access from database via nais CLI" + ReasonListUsers = "Listing database users via nais CLI" + ReasonAddUser = "Adding database user via nais CLI" + ReasonDropUser = "Dropping database user via nais CLI" + ReasonEnableAudit = "Enabling audit logging via nais CLI" + ReasonVerifyAudit = "Verifying audit configuration via nais CLI" +) + +type SecretValues struct{ values map[string]string } + +func (s *SecretValues) Get(suffix string) string { + for name, val := range s.values { + if strings.HasSuffix(name, suffix) { + return val + } + } + return "" +} + +// GetSecretValues retrieves Cloud SQL secret values through the audited API. +func GetSecretValues(ctx context.Context, appName, team, environment string, fl *flag.CloudSQL, reason string, out *naistrix.OutputWriter) (*SecretValues, error) { + if reason == "" { + reason = fl.Reason + if reason == "" { + return nil, fmt.Errorf("reason is required for accessing database secrets") + } + } + out.Printf("Using team %q\n", team) + secretName := "google-sql-" + appName + out.Debugf("Requesting access to Cloud SQL secret %q...\n", secretName) + values, err := naisapi.ViewSecretValues(ctx, team, environment, secretName, reason) + if err != nil { + if strings.Contains(err.Error(), "not authorized") || strings.Contains(err.Error(), "Not authorized") { + return nil, fmt.Errorf("you are not authorized to access this database. Make sure you are a member of team %q", team) + } + return nil, err + } + out.Debugf("✅ Access granted.\n") + result := &SecretValues{values: make(map[string]string, len(values))} + for _, v := range values { + result.values[v.Name] = v.Value + } + return result, nil +} + +func GetSecretValuesWithUserReason(ctx context.Context, appName, team, environment string, fl *flag.CloudSQL, reason string, out *naistrix.OutputWriter) (*SecretValues, error) { + if reason == "" { + reason = fl.Reason + if reason == "" { + return nil, fmt.Errorf("reason is required for accessing database secrets (use --reason flag)") + } + } + if len(reason) < 10 { + return nil, fmt.Errorf("reason must be at least 10 characters") + } + return GetSecretValues(ctx, appName, team, environment, fl, reason, out) +} diff --git a/internal/naisapi/gql/generated.go b/internal/naisapi/gql/generated.go index 68b407d8..9f942e6b 100644 --- a/internal/naisapi/gql/generated.go +++ b/internal/naisapi/gql/generated.go @@ -61,7 +61,11 @@ const ( ActivityLogActivityTypeOpensearchCredentialsCreated ActivityLogActivityType = "OPENSEARCH_CREDENTIALS_CREATED" // A user was granted access to a Postgres cluster ActivityLogActivityTypePostgresGrantAccess ActivityLogActivityType = "POSTGRES_GRANT_ACCESS" - // A Postgres instance was deleted + // A personal Postgres access was created through the API broker + ActivityLogActivityTypePostgresPersonalAccessCreated ActivityLogActivityType = "POSTGRES_PERSONAL_ACCESS_CREATED" + // Personal Postgres connection materials were retrieved + ActivityLogActivityTypePostgresPersonalAccessConnection ActivityLogActivityType = "POSTGRES_PERSONAL_ACCESS_CONNECTION" + // A Postgres branch was deleted ActivityLogActivityTypePostgresDeleted ActivityLogActivityType = "POSTGRES_DELETED" // Reconciler enabled activity log entry. ActivityLogActivityTypeReconcilerEnabled ActivityLogActivityType = "RECONCILER_ENABLED" @@ -171,6 +175,8 @@ var AllActivityLogActivityType = []ActivityLogActivityType{ ActivityLogActivityTypeOpensearchMaintenanceStarted, ActivityLogActivityTypeOpensearchCredentialsCreated, ActivityLogActivityTypePostgresGrantAccess, + ActivityLogActivityTypePostgresPersonalAccessCreated, + ActivityLogActivityTypePostgresPersonalAccessConnection, ActivityLogActivityTypePostgresDeleted, ActivityLogActivityTypeReconcilerEnabled, ActivityLogActivityTypeReconcilerDisabled, @@ -1038,6 +1044,72 @@ func (v *CreateOpenSearchResponse) GetCreateOpenSearch() CreateOpenSearchCreateO return v.CreateOpenSearch } +// CreatePostgresAccessAlphaCreatePostgresAccessCreatePostgresAccessPayload includes the requested fields of the GraphQL type CreatePostgresAccessPayload. +// The GraphQL type's documentation follows. +// +// Result of creating a personal Postgres access. +type CreatePostgresAccessAlphaCreatePostgresAccessCreatePostgresAccessPayload struct { + // Name of the newly created PostgresAccess resource. + Name string `json:"name"` +} + +// GetName returns CreatePostgresAccessAlphaCreatePostgresAccessCreatePostgresAccessPayload.Name, and is useful for accessing the field via an interface. +func (v *CreatePostgresAccessAlphaCreatePostgresAccessCreatePostgresAccessPayload) GetName() string { + return v.Name +} + +// CreatePostgresAccessAlphaResponse is returned by CreatePostgresAccessAlpha on success. +type CreatePostgresAccessAlphaResponse struct { + // EXPERIMENTAL: DO NOT USE + // Create time-limited personal access to a NAIS Postgres branch through the brokered PostgresAccess and relay flow. + // When the access is ready, retrieve its connection materials through PostgresAccess.connection. + CreatePostgresAccess CreatePostgresAccessAlphaCreatePostgresAccessCreatePostgresAccessPayload `json:"createPostgresAccess"` +} + +// GetCreatePostgresAccess returns CreatePostgresAccessAlphaResponse.CreatePostgresAccess, and is useful for accessing the field via an interface. +func (v *CreatePostgresAccessAlphaResponse) GetCreatePostgresAccess() CreatePostgresAccessAlphaCreatePostgresAccessCreatePostgresAccessPayload { + return v.CreatePostgresAccess +} + +// Input for creating a time-limited personal Postgres access. +type CreatePostgresAccessInput struct { + // Name of the Postgres containing the branch. + Postgres string `json:"postgres"` + // Local name of the branch to access. + Branch string `json:"branch"` + // Team that owns the Postgres branch. + TeamSlug string `json:"teamSlug"` + // Environment containing the Postgres branch. + EnvironmentName string `json:"environmentName"` + // Privileges requested for the personal database role. + AccessLevel PostgresAccessLevel `json:"accessLevel"` + // Reason for personal database access. Must be at least 10 characters. + Reason string `json:"reason"` + // Requested access lifetime (for example '30m' or '1h'). Defaults to '1h' and cannot exceed '1h'. + Ttl *string `json:"ttl"` +} + +// GetPostgres returns CreatePostgresAccessInput.Postgres, and is useful for accessing the field via an interface. +func (v *CreatePostgresAccessInput) GetPostgres() string { return v.Postgres } + +// GetBranch returns CreatePostgresAccessInput.Branch, and is useful for accessing the field via an interface. +func (v *CreatePostgresAccessInput) GetBranch() string { return v.Branch } + +// GetTeamSlug returns CreatePostgresAccessInput.TeamSlug, and is useful for accessing the field via an interface. +func (v *CreatePostgresAccessInput) GetTeamSlug() string { return v.TeamSlug } + +// GetEnvironmentName returns CreatePostgresAccessInput.EnvironmentName, and is useful for accessing the field via an interface. +func (v *CreatePostgresAccessInput) GetEnvironmentName() string { return v.EnvironmentName } + +// GetAccessLevel returns CreatePostgresAccessInput.AccessLevel, and is useful for accessing the field via an interface. +func (v *CreatePostgresAccessInput) GetAccessLevel() PostgresAccessLevel { return v.AccessLevel } + +// GetReason returns CreatePostgresAccessInput.Reason, and is useful for accessing the field via an interface. +func (v *CreatePostgresAccessInput) GetReason() string { return v.Reason } + +// GetTtl returns CreatePostgresAccessInput.Ttl, and is useful for accessing the field via an interface. +func (v *CreatePostgresAccessInput) GetTtl() *string { return v.Ttl } + // CreateSecretCreateSecretCreateSecretPayload includes the requested fields of the GraphQL type CreateSecretPayload. type CreateSecretCreateSecretCreateSecretPayload struct { // The created secret. @@ -1789,6 +1861,74 @@ type FindWorkloadsForCveResponse struct { // GetCve returns FindWorkloadsForCveResponse.Cve, and is useful for accessing the field via an interface. func (v *FindWorkloadsForCveResponse) GetCve() FindWorkloadsForCveCveCVE { return v.Cve } +// GetActivePostgresBranchAlphaResponse is returned by GetActivePostgresBranchAlpha on success. +type GetActivePostgresBranchAlphaResponse struct { + // Get a team by its slug. + Team GetActivePostgresBranchAlphaTeam `json:"team"` +} + +// GetTeam returns GetActivePostgresBranchAlphaResponse.Team, and is useful for accessing the field via an interface. +func (v *GetActivePostgresBranchAlphaResponse) GetTeam() GetActivePostgresBranchAlphaTeam { + return v.Team +} + +// GetActivePostgresBranchAlphaTeam includes the requested fields of the GraphQL type Team. +// The GraphQL type's documentation follows. +// +// The team type represents a team on the [Nais platform](https://nais.io/). +// +// Learn more about what Nais teams are and what they can be used for in the [official Nais documentation](https://docs.nais.io/explanations/team/). +// +// External resources (e.g. entraIDGroupID, gitHubTeamSlug) are managed by [Nais API reconcilers](https://github.com/nais/api-reconcilers). +type GetActivePostgresBranchAlphaTeam struct { + // Get a specific environment for the team. + Environment GetActivePostgresBranchAlphaTeamEnvironment `json:"environment"` +} + +// GetEnvironment returns GetActivePostgresBranchAlphaTeam.Environment, and is useful for accessing the field via an interface. +func (v *GetActivePostgresBranchAlphaTeam) GetEnvironment() GetActivePostgresBranchAlphaTeamEnvironment { + return v.Environment +} + +// GetActivePostgresBranchAlphaTeamEnvironment includes the requested fields of the GraphQL type TeamEnvironment. +type GetActivePostgresBranchAlphaTeamEnvironment struct { + // Postgres in the team environment. + Postgres GetActivePostgresBranchAlphaTeamEnvironmentPostgres `json:"postgres"` +} + +// GetPostgres returns GetActivePostgresBranchAlphaTeamEnvironment.Postgres, and is useful for accessing the field via an interface. +func (v *GetActivePostgresBranchAlphaTeamEnvironment) GetPostgres() GetActivePostgresBranchAlphaTeamEnvironmentPostgres { + return v.Postgres +} + +// GetActivePostgresBranchAlphaTeamEnvironmentPostgres includes the requested fields of the GraphQL type Postgres. +// The GraphQL type's documentation follows. +// +// A Postgres whose active branch can change. +type GetActivePostgresBranchAlphaTeamEnvironmentPostgres struct { + // Currently active branch, if selected. + ActiveBranch *GetActivePostgresBranchAlphaTeamEnvironmentPostgresActiveBranchPostgresBranch `json:"activeBranch"` +} + +// GetActiveBranch returns GetActivePostgresBranchAlphaTeamEnvironmentPostgres.ActiveBranch, and is useful for accessing the field via an interface. +func (v *GetActivePostgresBranchAlphaTeamEnvironmentPostgres) GetActiveBranch() *GetActivePostgresBranchAlphaTeamEnvironmentPostgresActiveBranchPostgresBranch { + return v.ActiveBranch +} + +// GetActivePostgresBranchAlphaTeamEnvironmentPostgresActiveBranchPostgresBranch includes the requested fields of the GraphQL type PostgresBranch. +// The GraphQL type's documentation follows. +// +// A named PostgresBranch belonging to a Postgres. +type GetActivePostgresBranchAlphaTeamEnvironmentPostgresActiveBranchPostgresBranch struct { + // Local name of this branch within its Postgres. + Name string `json:"name"` +} + +// GetName returns GetActivePostgresBranchAlphaTeamEnvironmentPostgresActiveBranchPostgresBranch.Name, and is useful for accessing the field via an interface. +func (v *GetActivePostgresBranchAlphaTeamEnvironmentPostgresActiveBranchPostgresBranch) GetName() string { + return v.Name +} + // GetAllConfigsResponse is returned by GetAllConfigs on success. type GetAllConfigsResponse struct { // Get a team by its slug. @@ -5383,6 +5523,8 @@ func (v *GetApplicationActivityTeamApplicationsApplicationConnectionNodesApplica // GetApplicationActivityTeamApplicationsApplicationConnectionNodesApplicationActivityLogActivityLogEntryConnectionNodesOpenSearchUpdatedActivityLogEntry // GetApplicationActivityTeamApplicationsApplicationConnectionNodesApplicationActivityLogActivityLogEntryConnectionNodesPostgresDeletedActivityLogEntry // GetApplicationActivityTeamApplicationsApplicationConnectionNodesApplicationActivityLogActivityLogEntryConnectionNodesPostgresGrantAccessActivityLogEntry +// GetApplicationActivityTeamApplicationsApplicationConnectionNodesApplicationActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessConnectionActivityLogEntry +// GetApplicationActivityTeamApplicationsApplicationConnectionNodesApplicationActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessCreatedActivityLogEntry // GetApplicationActivityTeamApplicationsApplicationConnectionNodesApplicationActivityLogActivityLogEntryConnectionNodesReconcilerConfiguredActivityLogEntry // GetApplicationActivityTeamApplicationsApplicationConnectionNodesApplicationActivityLogActivityLogEntryConnectionNodesReconcilerDisabledActivityLogEntry // GetApplicationActivityTeamApplicationsApplicationConnectionNodesApplicationActivityLogActivityLogEntryConnectionNodesReconcilerEnabledActivityLogEntry @@ -5502,6 +5644,10 @@ func (v *GetApplicationActivityTeamApplicationsApplicationConnectionNodesApplica } func (v *GetApplicationActivityTeamApplicationsApplicationConnectionNodesApplicationActivityLogActivityLogEntryConnectionNodesPostgresGrantAccessActivityLogEntry) implementsGraphQLInterfaceGetApplicationActivityTeamApplicationsApplicationConnectionNodesApplicationActivityLogActivityLogEntryConnectionNodesActivityLogEntry() { } +func (v *GetApplicationActivityTeamApplicationsApplicationConnectionNodesApplicationActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessConnectionActivityLogEntry) implementsGraphQLInterfaceGetApplicationActivityTeamApplicationsApplicationConnectionNodesApplicationActivityLogActivityLogEntryConnectionNodesActivityLogEntry() { +} +func (v *GetApplicationActivityTeamApplicationsApplicationConnectionNodesApplicationActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessCreatedActivityLogEntry) implementsGraphQLInterfaceGetApplicationActivityTeamApplicationsApplicationConnectionNodesApplicationActivityLogActivityLogEntryConnectionNodesActivityLogEntry() { +} func (v *GetApplicationActivityTeamApplicationsApplicationConnectionNodesApplicationActivityLogActivityLogEntryConnectionNodesReconcilerConfiguredActivityLogEntry) implementsGraphQLInterfaceGetApplicationActivityTeamApplicationsApplicationConnectionNodesApplicationActivityLogActivityLogEntryConnectionNodesActivityLogEntry() { } func (v *GetApplicationActivityTeamApplicationsApplicationConnectionNodesApplicationActivityLogActivityLogEntryConnectionNodesReconcilerDisabledActivityLogEntry) implementsGraphQLInterfaceGetApplicationActivityTeamApplicationsApplicationConnectionNodesApplicationActivityLogActivityLogEntryConnectionNodesActivityLogEntry() { @@ -5673,6 +5819,12 @@ func __unmarshalGetApplicationActivityTeamApplicationsApplicationConnectionNodes case "PostgresGrantAccessActivityLogEntry": *v = new(GetApplicationActivityTeamApplicationsApplicationConnectionNodesApplicationActivityLogActivityLogEntryConnectionNodesPostgresGrantAccessActivityLogEntry) return json.Unmarshal(b, *v) + case "PostgresPersonalAccessConnectionActivityLogEntry": + *v = new(GetApplicationActivityTeamApplicationsApplicationConnectionNodesApplicationActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessConnectionActivityLogEntry) + return json.Unmarshal(b, *v) + case "PostgresPersonalAccessCreatedActivityLogEntry": + *v = new(GetApplicationActivityTeamApplicationsApplicationConnectionNodesApplicationActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessCreatedActivityLogEntry) + return json.Unmarshal(b, *v) case "ReconcilerConfiguredActivityLogEntry": *v = new(GetApplicationActivityTeamApplicationsApplicationConnectionNodesApplicationActivityLogActivityLogEntryConnectionNodesReconcilerConfiguredActivityLogEntry) return json.Unmarshal(b, *v) @@ -6004,6 +6156,22 @@ func __marshalGetApplicationActivityTeamApplicationsApplicationConnectionNodesAp *GetApplicationActivityTeamApplicationsApplicationConnectionNodesApplicationActivityLogActivityLogEntryConnectionNodesPostgresGrantAccessActivityLogEntry }{typename, v} return json.Marshal(result) + case *GetApplicationActivityTeamApplicationsApplicationConnectionNodesApplicationActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessConnectionActivityLogEntry: + typename = "PostgresPersonalAccessConnectionActivityLogEntry" + + result := struct { + TypeName string `json:"__typename"` + *GetApplicationActivityTeamApplicationsApplicationConnectionNodesApplicationActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessConnectionActivityLogEntry + }{typename, v} + return json.Marshal(result) + case *GetApplicationActivityTeamApplicationsApplicationConnectionNodesApplicationActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessCreatedActivityLogEntry: + typename = "PostgresPersonalAccessCreatedActivityLogEntry" + + result := struct { + TypeName string `json:"__typename"` + *GetApplicationActivityTeamApplicationsApplicationConnectionNodesApplicationActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessCreatedActivityLogEntry + }{typename, v} + return json.Marshal(result) case *GetApplicationActivityTeamApplicationsApplicationConnectionNodesApplicationActivityLogActivityLogEntryConnectionNodesReconcilerConfiguredActivityLogEntry: typename = "ReconcilerConfiguredActivityLogEntry" @@ -7270,6 +7438,88 @@ func (v *GetApplicationActivityTeamApplicationsApplicationConnectionNodesApplica return v.EnvironmentName } +// GetApplicationActivityTeamApplicationsApplicationConnectionNodesApplicationActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessConnectionActivityLogEntry includes the requested fields of the GraphQL type PostgresPersonalAccessConnectionActivityLogEntry. +// The GraphQL type's documentation follows. +// +// An audit-log entry for retrieval of personal Postgres connection materials. +type GetApplicationActivityTeamApplicationsApplicationConnectionNodesApplicationActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessConnectionActivityLogEntry struct { + Typename *string `json:"__typename"` + // Interface for activity log entries. + Actor string `json:"actor"` + // Interface for activity log entries. + CreatedAt time.Time `json:"createdAt"` + // Interface for activity log entries. + Message string `json:"message"` + // Interface for activity log entries. + EnvironmentName *string `json:"environmentName"` +} + +// GetTypename returns GetApplicationActivityTeamApplicationsApplicationConnectionNodesApplicationActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessConnectionActivityLogEntry.Typename, and is useful for accessing the field via an interface. +func (v *GetApplicationActivityTeamApplicationsApplicationConnectionNodesApplicationActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessConnectionActivityLogEntry) GetTypename() *string { + return v.Typename +} + +// GetActor returns GetApplicationActivityTeamApplicationsApplicationConnectionNodesApplicationActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessConnectionActivityLogEntry.Actor, and is useful for accessing the field via an interface. +func (v *GetApplicationActivityTeamApplicationsApplicationConnectionNodesApplicationActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessConnectionActivityLogEntry) GetActor() string { + return v.Actor +} + +// GetCreatedAt returns GetApplicationActivityTeamApplicationsApplicationConnectionNodesApplicationActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessConnectionActivityLogEntry.CreatedAt, and is useful for accessing the field via an interface. +func (v *GetApplicationActivityTeamApplicationsApplicationConnectionNodesApplicationActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessConnectionActivityLogEntry) GetCreatedAt() time.Time { + return v.CreatedAt +} + +// GetMessage returns GetApplicationActivityTeamApplicationsApplicationConnectionNodesApplicationActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessConnectionActivityLogEntry.Message, and is useful for accessing the field via an interface. +func (v *GetApplicationActivityTeamApplicationsApplicationConnectionNodesApplicationActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessConnectionActivityLogEntry) GetMessage() string { + return v.Message +} + +// GetEnvironmentName returns GetApplicationActivityTeamApplicationsApplicationConnectionNodesApplicationActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessConnectionActivityLogEntry.EnvironmentName, and is useful for accessing the field via an interface. +func (v *GetApplicationActivityTeamApplicationsApplicationConnectionNodesApplicationActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessConnectionActivityLogEntry) GetEnvironmentName() *string { + return v.EnvironmentName +} + +// GetApplicationActivityTeamApplicationsApplicationConnectionNodesApplicationActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessCreatedActivityLogEntry includes the requested fields of the GraphQL type PostgresPersonalAccessCreatedActivityLogEntry. +// The GraphQL type's documentation follows. +// +// An audit-log entry for personal Postgres access created through the API broker. +type GetApplicationActivityTeamApplicationsApplicationConnectionNodesApplicationActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessCreatedActivityLogEntry struct { + Typename *string `json:"__typename"` + // Interface for activity log entries. + Actor string `json:"actor"` + // Interface for activity log entries. + CreatedAt time.Time `json:"createdAt"` + // Interface for activity log entries. + Message string `json:"message"` + // Interface for activity log entries. + EnvironmentName *string `json:"environmentName"` +} + +// GetTypename returns GetApplicationActivityTeamApplicationsApplicationConnectionNodesApplicationActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessCreatedActivityLogEntry.Typename, and is useful for accessing the field via an interface. +func (v *GetApplicationActivityTeamApplicationsApplicationConnectionNodesApplicationActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessCreatedActivityLogEntry) GetTypename() *string { + return v.Typename +} + +// GetActor returns GetApplicationActivityTeamApplicationsApplicationConnectionNodesApplicationActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessCreatedActivityLogEntry.Actor, and is useful for accessing the field via an interface. +func (v *GetApplicationActivityTeamApplicationsApplicationConnectionNodesApplicationActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessCreatedActivityLogEntry) GetActor() string { + return v.Actor +} + +// GetCreatedAt returns GetApplicationActivityTeamApplicationsApplicationConnectionNodesApplicationActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessCreatedActivityLogEntry.CreatedAt, and is useful for accessing the field via an interface. +func (v *GetApplicationActivityTeamApplicationsApplicationConnectionNodesApplicationActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessCreatedActivityLogEntry) GetCreatedAt() time.Time { + return v.CreatedAt +} + +// GetMessage returns GetApplicationActivityTeamApplicationsApplicationConnectionNodesApplicationActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessCreatedActivityLogEntry.Message, and is useful for accessing the field via an interface. +func (v *GetApplicationActivityTeamApplicationsApplicationConnectionNodesApplicationActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessCreatedActivityLogEntry) GetMessage() string { + return v.Message +} + +// GetEnvironmentName returns GetApplicationActivityTeamApplicationsApplicationConnectionNodesApplicationActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessCreatedActivityLogEntry.EnvironmentName, and is useful for accessing the field via an interface. +func (v *GetApplicationActivityTeamApplicationsApplicationConnectionNodesApplicationActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessCreatedActivityLogEntry) GetEnvironmentName() *string { + return v.EnvironmentName +} + // GetApplicationActivityTeamApplicationsApplicationConnectionNodesApplicationActivityLogActivityLogEntryConnectionNodesReconcilerConfiguredActivityLogEntry includes the requested fields of the GraphQL type ReconcilerConfiguredActivityLogEntry. type GetApplicationActivityTeamApplicationsApplicationConnectionNodesApplicationActivityLogActivityLogEntryConnectionNodesReconcilerConfiguredActivityLogEntry struct { Typename *string `json:"__typename"` @@ -10707,6 +10957,8 @@ func (v *GetConfigActivityTeamConfigsConfigConnectionNodesConfigActivityLogActiv // GetConfigActivityTeamConfigsConfigConnectionNodesConfigActivityLogActivityLogEntryConnectionNodesOpenSearchUpdatedActivityLogEntry // GetConfigActivityTeamConfigsConfigConnectionNodesConfigActivityLogActivityLogEntryConnectionNodesPostgresDeletedActivityLogEntry // GetConfigActivityTeamConfigsConfigConnectionNodesConfigActivityLogActivityLogEntryConnectionNodesPostgresGrantAccessActivityLogEntry +// GetConfigActivityTeamConfigsConfigConnectionNodesConfigActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessConnectionActivityLogEntry +// GetConfigActivityTeamConfigsConfigConnectionNodesConfigActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessCreatedActivityLogEntry // GetConfigActivityTeamConfigsConfigConnectionNodesConfigActivityLogActivityLogEntryConnectionNodesReconcilerConfiguredActivityLogEntry // GetConfigActivityTeamConfigsConfigConnectionNodesConfigActivityLogActivityLogEntryConnectionNodesReconcilerDisabledActivityLogEntry // GetConfigActivityTeamConfigsConfigConnectionNodesConfigActivityLogActivityLogEntryConnectionNodesReconcilerEnabledActivityLogEntry @@ -10826,6 +11078,10 @@ func (v *GetConfigActivityTeamConfigsConfigConnectionNodesConfigActivityLogActiv } func (v *GetConfigActivityTeamConfigsConfigConnectionNodesConfigActivityLogActivityLogEntryConnectionNodesPostgresGrantAccessActivityLogEntry) implementsGraphQLInterfaceGetConfigActivityTeamConfigsConfigConnectionNodesConfigActivityLogActivityLogEntryConnectionNodesActivityLogEntry() { } +func (v *GetConfigActivityTeamConfigsConfigConnectionNodesConfigActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessConnectionActivityLogEntry) implementsGraphQLInterfaceGetConfigActivityTeamConfigsConfigConnectionNodesConfigActivityLogActivityLogEntryConnectionNodesActivityLogEntry() { +} +func (v *GetConfigActivityTeamConfigsConfigConnectionNodesConfigActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessCreatedActivityLogEntry) implementsGraphQLInterfaceGetConfigActivityTeamConfigsConfigConnectionNodesConfigActivityLogActivityLogEntryConnectionNodesActivityLogEntry() { +} func (v *GetConfigActivityTeamConfigsConfigConnectionNodesConfigActivityLogActivityLogEntryConnectionNodesReconcilerConfiguredActivityLogEntry) implementsGraphQLInterfaceGetConfigActivityTeamConfigsConfigConnectionNodesConfigActivityLogActivityLogEntryConnectionNodesActivityLogEntry() { } func (v *GetConfigActivityTeamConfigsConfigConnectionNodesConfigActivityLogActivityLogEntryConnectionNodesReconcilerDisabledActivityLogEntry) implementsGraphQLInterfaceGetConfigActivityTeamConfigsConfigConnectionNodesConfigActivityLogActivityLogEntryConnectionNodesActivityLogEntry() { @@ -10997,6 +11253,12 @@ func __unmarshalGetConfigActivityTeamConfigsConfigConnectionNodesConfigActivityL case "PostgresGrantAccessActivityLogEntry": *v = new(GetConfigActivityTeamConfigsConfigConnectionNodesConfigActivityLogActivityLogEntryConnectionNodesPostgresGrantAccessActivityLogEntry) return json.Unmarshal(b, *v) + case "PostgresPersonalAccessConnectionActivityLogEntry": + *v = new(GetConfigActivityTeamConfigsConfigConnectionNodesConfigActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessConnectionActivityLogEntry) + return json.Unmarshal(b, *v) + case "PostgresPersonalAccessCreatedActivityLogEntry": + *v = new(GetConfigActivityTeamConfigsConfigConnectionNodesConfigActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessCreatedActivityLogEntry) + return json.Unmarshal(b, *v) case "ReconcilerConfiguredActivityLogEntry": *v = new(GetConfigActivityTeamConfigsConfigConnectionNodesConfigActivityLogActivityLogEntryConnectionNodesReconcilerConfiguredActivityLogEntry) return json.Unmarshal(b, *v) @@ -11328,6 +11590,22 @@ func __marshalGetConfigActivityTeamConfigsConfigConnectionNodesConfigActivityLog *GetConfigActivityTeamConfigsConfigConnectionNodesConfigActivityLogActivityLogEntryConnectionNodesPostgresGrantAccessActivityLogEntry }{typename, v} return json.Marshal(result) + case *GetConfigActivityTeamConfigsConfigConnectionNodesConfigActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessConnectionActivityLogEntry: + typename = "PostgresPersonalAccessConnectionActivityLogEntry" + + result := struct { + TypeName string `json:"__typename"` + *GetConfigActivityTeamConfigsConfigConnectionNodesConfigActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessConnectionActivityLogEntry + }{typename, v} + return json.Marshal(result) + case *GetConfigActivityTeamConfigsConfigConnectionNodesConfigActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessCreatedActivityLogEntry: + typename = "PostgresPersonalAccessCreatedActivityLogEntry" + + result := struct { + TypeName string `json:"__typename"` + *GetConfigActivityTeamConfigsConfigConnectionNodesConfigActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessCreatedActivityLogEntry + }{typename, v} + return json.Marshal(result) case *GetConfigActivityTeamConfigsConfigConnectionNodesConfigActivityLogActivityLogEntryConnectionNodesReconcilerConfiguredActivityLogEntry: typename = "ReconcilerConfiguredActivityLogEntry" @@ -12594,6 +12872,88 @@ func (v *GetConfigActivityTeamConfigsConfigConnectionNodesConfigActivityLogActiv return v.EnvironmentName } +// GetConfigActivityTeamConfigsConfigConnectionNodesConfigActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessConnectionActivityLogEntry includes the requested fields of the GraphQL type PostgresPersonalAccessConnectionActivityLogEntry. +// The GraphQL type's documentation follows. +// +// An audit-log entry for retrieval of personal Postgres connection materials. +type GetConfigActivityTeamConfigsConfigConnectionNodesConfigActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessConnectionActivityLogEntry struct { + Typename *string `json:"__typename"` + // Interface for activity log entries. + Actor string `json:"actor"` + // Interface for activity log entries. + CreatedAt time.Time `json:"createdAt"` + // Interface for activity log entries. + Message string `json:"message"` + // Interface for activity log entries. + EnvironmentName *string `json:"environmentName"` +} + +// GetTypename returns GetConfigActivityTeamConfigsConfigConnectionNodesConfigActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessConnectionActivityLogEntry.Typename, and is useful for accessing the field via an interface. +func (v *GetConfigActivityTeamConfigsConfigConnectionNodesConfigActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessConnectionActivityLogEntry) GetTypename() *string { + return v.Typename +} + +// GetActor returns GetConfigActivityTeamConfigsConfigConnectionNodesConfigActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessConnectionActivityLogEntry.Actor, and is useful for accessing the field via an interface. +func (v *GetConfigActivityTeamConfigsConfigConnectionNodesConfigActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessConnectionActivityLogEntry) GetActor() string { + return v.Actor +} + +// GetCreatedAt returns GetConfigActivityTeamConfigsConfigConnectionNodesConfigActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessConnectionActivityLogEntry.CreatedAt, and is useful for accessing the field via an interface. +func (v *GetConfigActivityTeamConfigsConfigConnectionNodesConfigActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessConnectionActivityLogEntry) GetCreatedAt() time.Time { + return v.CreatedAt +} + +// GetMessage returns GetConfigActivityTeamConfigsConfigConnectionNodesConfigActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessConnectionActivityLogEntry.Message, and is useful for accessing the field via an interface. +func (v *GetConfigActivityTeamConfigsConfigConnectionNodesConfigActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessConnectionActivityLogEntry) GetMessage() string { + return v.Message +} + +// GetEnvironmentName returns GetConfigActivityTeamConfigsConfigConnectionNodesConfigActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessConnectionActivityLogEntry.EnvironmentName, and is useful for accessing the field via an interface. +func (v *GetConfigActivityTeamConfigsConfigConnectionNodesConfigActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessConnectionActivityLogEntry) GetEnvironmentName() *string { + return v.EnvironmentName +} + +// GetConfigActivityTeamConfigsConfigConnectionNodesConfigActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessCreatedActivityLogEntry includes the requested fields of the GraphQL type PostgresPersonalAccessCreatedActivityLogEntry. +// The GraphQL type's documentation follows. +// +// An audit-log entry for personal Postgres access created through the API broker. +type GetConfigActivityTeamConfigsConfigConnectionNodesConfigActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessCreatedActivityLogEntry struct { + Typename *string `json:"__typename"` + // Interface for activity log entries. + Actor string `json:"actor"` + // Interface for activity log entries. + CreatedAt time.Time `json:"createdAt"` + // Interface for activity log entries. + Message string `json:"message"` + // Interface for activity log entries. + EnvironmentName *string `json:"environmentName"` +} + +// GetTypename returns GetConfigActivityTeamConfigsConfigConnectionNodesConfigActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessCreatedActivityLogEntry.Typename, and is useful for accessing the field via an interface. +func (v *GetConfigActivityTeamConfigsConfigConnectionNodesConfigActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessCreatedActivityLogEntry) GetTypename() *string { + return v.Typename +} + +// GetActor returns GetConfigActivityTeamConfigsConfigConnectionNodesConfigActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessCreatedActivityLogEntry.Actor, and is useful for accessing the field via an interface. +func (v *GetConfigActivityTeamConfigsConfigConnectionNodesConfigActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessCreatedActivityLogEntry) GetActor() string { + return v.Actor +} + +// GetCreatedAt returns GetConfigActivityTeamConfigsConfigConnectionNodesConfigActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessCreatedActivityLogEntry.CreatedAt, and is useful for accessing the field via an interface. +func (v *GetConfigActivityTeamConfigsConfigConnectionNodesConfigActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessCreatedActivityLogEntry) GetCreatedAt() time.Time { + return v.CreatedAt +} + +// GetMessage returns GetConfigActivityTeamConfigsConfigConnectionNodesConfigActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessCreatedActivityLogEntry.Message, and is useful for accessing the field via an interface. +func (v *GetConfigActivityTeamConfigsConfigConnectionNodesConfigActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessCreatedActivityLogEntry) GetMessage() string { + return v.Message +} + +// GetEnvironmentName returns GetConfigActivityTeamConfigsConfigConnectionNodesConfigActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessCreatedActivityLogEntry.EnvironmentName, and is useful for accessing the field via an interface. +func (v *GetConfigActivityTeamConfigsConfigConnectionNodesConfigActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessCreatedActivityLogEntry) GetEnvironmentName() *string { + return v.EnvironmentName +} + // GetConfigActivityTeamConfigsConfigConnectionNodesConfigActivityLogActivityLogEntryConnectionNodesReconcilerConfiguredActivityLogEntry includes the requested fields of the GraphQL type ReconcilerConfiguredActivityLogEntry. type GetConfigActivityTeamConfigsConfigConnectionNodesConfigActivityLogActivityLogEntryConnectionNodesReconcilerConfiguredActivityLogEntry struct { Typename *string `json:"__typename"` @@ -14879,6 +15239,8 @@ func (v *GetJobActivityTeamJobsJobConnectionNodesJobActivityLogActivityLogEntryC // GetJobActivityTeamJobsJobConnectionNodesJobActivityLogActivityLogEntryConnectionNodesOpenSearchUpdatedActivityLogEntry // GetJobActivityTeamJobsJobConnectionNodesJobActivityLogActivityLogEntryConnectionNodesPostgresDeletedActivityLogEntry // GetJobActivityTeamJobsJobConnectionNodesJobActivityLogActivityLogEntryConnectionNodesPostgresGrantAccessActivityLogEntry +// GetJobActivityTeamJobsJobConnectionNodesJobActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessConnectionActivityLogEntry +// GetJobActivityTeamJobsJobConnectionNodesJobActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessCreatedActivityLogEntry // GetJobActivityTeamJobsJobConnectionNodesJobActivityLogActivityLogEntryConnectionNodesReconcilerConfiguredActivityLogEntry // GetJobActivityTeamJobsJobConnectionNodesJobActivityLogActivityLogEntryConnectionNodesReconcilerDisabledActivityLogEntry // GetJobActivityTeamJobsJobConnectionNodesJobActivityLogActivityLogEntryConnectionNodesReconcilerEnabledActivityLogEntry @@ -14998,6 +15360,10 @@ func (v *GetJobActivityTeamJobsJobConnectionNodesJobActivityLogActivityLogEntryC } func (v *GetJobActivityTeamJobsJobConnectionNodesJobActivityLogActivityLogEntryConnectionNodesPostgresGrantAccessActivityLogEntry) implementsGraphQLInterfaceGetJobActivityTeamJobsJobConnectionNodesJobActivityLogActivityLogEntryConnectionNodesActivityLogEntry() { } +func (v *GetJobActivityTeamJobsJobConnectionNodesJobActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessConnectionActivityLogEntry) implementsGraphQLInterfaceGetJobActivityTeamJobsJobConnectionNodesJobActivityLogActivityLogEntryConnectionNodesActivityLogEntry() { +} +func (v *GetJobActivityTeamJobsJobConnectionNodesJobActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessCreatedActivityLogEntry) implementsGraphQLInterfaceGetJobActivityTeamJobsJobConnectionNodesJobActivityLogActivityLogEntryConnectionNodesActivityLogEntry() { +} func (v *GetJobActivityTeamJobsJobConnectionNodesJobActivityLogActivityLogEntryConnectionNodesReconcilerConfiguredActivityLogEntry) implementsGraphQLInterfaceGetJobActivityTeamJobsJobConnectionNodesJobActivityLogActivityLogEntryConnectionNodesActivityLogEntry() { } func (v *GetJobActivityTeamJobsJobConnectionNodesJobActivityLogActivityLogEntryConnectionNodesReconcilerDisabledActivityLogEntry) implementsGraphQLInterfaceGetJobActivityTeamJobsJobConnectionNodesJobActivityLogActivityLogEntryConnectionNodesActivityLogEntry() { @@ -15169,6 +15535,12 @@ func __unmarshalGetJobActivityTeamJobsJobConnectionNodesJobActivityLogActivityLo case "PostgresGrantAccessActivityLogEntry": *v = new(GetJobActivityTeamJobsJobConnectionNodesJobActivityLogActivityLogEntryConnectionNodesPostgresGrantAccessActivityLogEntry) return json.Unmarshal(b, *v) + case "PostgresPersonalAccessConnectionActivityLogEntry": + *v = new(GetJobActivityTeamJobsJobConnectionNodesJobActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessConnectionActivityLogEntry) + return json.Unmarshal(b, *v) + case "PostgresPersonalAccessCreatedActivityLogEntry": + *v = new(GetJobActivityTeamJobsJobConnectionNodesJobActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessCreatedActivityLogEntry) + return json.Unmarshal(b, *v) case "ReconcilerConfiguredActivityLogEntry": *v = new(GetJobActivityTeamJobsJobConnectionNodesJobActivityLogActivityLogEntryConnectionNodesReconcilerConfiguredActivityLogEntry) return json.Unmarshal(b, *v) @@ -15500,6 +15872,22 @@ func __marshalGetJobActivityTeamJobsJobConnectionNodesJobActivityLogActivityLogE *GetJobActivityTeamJobsJobConnectionNodesJobActivityLogActivityLogEntryConnectionNodesPostgresGrantAccessActivityLogEntry }{typename, v} return json.Marshal(result) + case *GetJobActivityTeamJobsJobConnectionNodesJobActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessConnectionActivityLogEntry: + typename = "PostgresPersonalAccessConnectionActivityLogEntry" + + result := struct { + TypeName string `json:"__typename"` + *GetJobActivityTeamJobsJobConnectionNodesJobActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessConnectionActivityLogEntry + }{typename, v} + return json.Marshal(result) + case *GetJobActivityTeamJobsJobConnectionNodesJobActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessCreatedActivityLogEntry: + typename = "PostgresPersonalAccessCreatedActivityLogEntry" + + result := struct { + TypeName string `json:"__typename"` + *GetJobActivityTeamJobsJobConnectionNodesJobActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessCreatedActivityLogEntry + }{typename, v} + return json.Marshal(result) case *GetJobActivityTeamJobsJobConnectionNodesJobActivityLogActivityLogEntryConnectionNodesReconcilerConfiguredActivityLogEntry: typename = "ReconcilerConfiguredActivityLogEntry" @@ -16766,6 +17154,88 @@ func (v *GetJobActivityTeamJobsJobConnectionNodesJobActivityLogActivityLogEntryC return v.EnvironmentName } +// GetJobActivityTeamJobsJobConnectionNodesJobActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessConnectionActivityLogEntry includes the requested fields of the GraphQL type PostgresPersonalAccessConnectionActivityLogEntry. +// The GraphQL type's documentation follows. +// +// An audit-log entry for retrieval of personal Postgres connection materials. +type GetJobActivityTeamJobsJobConnectionNodesJobActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessConnectionActivityLogEntry struct { + Typename *string `json:"__typename"` + // Interface for activity log entries. + Actor string `json:"actor"` + // Interface for activity log entries. + CreatedAt time.Time `json:"createdAt"` + // Interface for activity log entries. + Message string `json:"message"` + // Interface for activity log entries. + EnvironmentName *string `json:"environmentName"` +} + +// GetTypename returns GetJobActivityTeamJobsJobConnectionNodesJobActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessConnectionActivityLogEntry.Typename, and is useful for accessing the field via an interface. +func (v *GetJobActivityTeamJobsJobConnectionNodesJobActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessConnectionActivityLogEntry) GetTypename() *string { + return v.Typename +} + +// GetActor returns GetJobActivityTeamJobsJobConnectionNodesJobActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessConnectionActivityLogEntry.Actor, and is useful for accessing the field via an interface. +func (v *GetJobActivityTeamJobsJobConnectionNodesJobActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessConnectionActivityLogEntry) GetActor() string { + return v.Actor +} + +// GetCreatedAt returns GetJobActivityTeamJobsJobConnectionNodesJobActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessConnectionActivityLogEntry.CreatedAt, and is useful for accessing the field via an interface. +func (v *GetJobActivityTeamJobsJobConnectionNodesJobActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessConnectionActivityLogEntry) GetCreatedAt() time.Time { + return v.CreatedAt +} + +// GetMessage returns GetJobActivityTeamJobsJobConnectionNodesJobActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessConnectionActivityLogEntry.Message, and is useful for accessing the field via an interface. +func (v *GetJobActivityTeamJobsJobConnectionNodesJobActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessConnectionActivityLogEntry) GetMessage() string { + return v.Message +} + +// GetEnvironmentName returns GetJobActivityTeamJobsJobConnectionNodesJobActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessConnectionActivityLogEntry.EnvironmentName, and is useful for accessing the field via an interface. +func (v *GetJobActivityTeamJobsJobConnectionNodesJobActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessConnectionActivityLogEntry) GetEnvironmentName() *string { + return v.EnvironmentName +} + +// GetJobActivityTeamJobsJobConnectionNodesJobActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessCreatedActivityLogEntry includes the requested fields of the GraphQL type PostgresPersonalAccessCreatedActivityLogEntry. +// The GraphQL type's documentation follows. +// +// An audit-log entry for personal Postgres access created through the API broker. +type GetJobActivityTeamJobsJobConnectionNodesJobActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessCreatedActivityLogEntry struct { + Typename *string `json:"__typename"` + // Interface for activity log entries. + Actor string `json:"actor"` + // Interface for activity log entries. + CreatedAt time.Time `json:"createdAt"` + // Interface for activity log entries. + Message string `json:"message"` + // Interface for activity log entries. + EnvironmentName *string `json:"environmentName"` +} + +// GetTypename returns GetJobActivityTeamJobsJobConnectionNodesJobActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessCreatedActivityLogEntry.Typename, and is useful for accessing the field via an interface. +func (v *GetJobActivityTeamJobsJobConnectionNodesJobActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessCreatedActivityLogEntry) GetTypename() *string { + return v.Typename +} + +// GetActor returns GetJobActivityTeamJobsJobConnectionNodesJobActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessCreatedActivityLogEntry.Actor, and is useful for accessing the field via an interface. +func (v *GetJobActivityTeamJobsJobConnectionNodesJobActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessCreatedActivityLogEntry) GetActor() string { + return v.Actor +} + +// GetCreatedAt returns GetJobActivityTeamJobsJobConnectionNodesJobActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessCreatedActivityLogEntry.CreatedAt, and is useful for accessing the field via an interface. +func (v *GetJobActivityTeamJobsJobConnectionNodesJobActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessCreatedActivityLogEntry) GetCreatedAt() time.Time { + return v.CreatedAt +} + +// GetMessage returns GetJobActivityTeamJobsJobConnectionNodesJobActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessCreatedActivityLogEntry.Message, and is useful for accessing the field via an interface. +func (v *GetJobActivityTeamJobsJobConnectionNodesJobActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessCreatedActivityLogEntry) GetMessage() string { + return v.Message +} + +// GetEnvironmentName returns GetJobActivityTeamJobsJobConnectionNodesJobActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessCreatedActivityLogEntry.EnvironmentName, and is useful for accessing the field via an interface. +func (v *GetJobActivityTeamJobsJobConnectionNodesJobActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessCreatedActivityLogEntry) GetEnvironmentName() *string { + return v.EnvironmentName +} + // GetJobActivityTeamJobsJobConnectionNodesJobActivityLogActivityLogEntryConnectionNodesReconcilerConfiguredActivityLogEntry includes the requested fields of the GraphQL type ReconcilerConfiguredActivityLogEntry. type GetJobActivityTeamJobsJobConnectionNodesJobActivityLogActivityLogEntryConnectionNodesReconcilerConfiguredActivityLogEntry struct { Typename *string `json:"__typename"` @@ -20102,6 +20572,128 @@ func (v *GetOpenSearchTeamEnvironmentOpenSearchVersion) GetDesiredMajor() OpenSe return v.DesiredMajor } +// GetPostgresAccessAlphaResponse is returned by GetPostgresAccessAlpha on success. +type GetPostgresAccessAlphaResponse struct { + // Get a team by its slug. + Team GetPostgresAccessAlphaTeam `json:"team"` +} + +// GetTeam returns GetPostgresAccessAlphaResponse.Team, and is useful for accessing the field via an interface. +func (v *GetPostgresAccessAlphaResponse) GetTeam() GetPostgresAccessAlphaTeam { return v.Team } + +// GetPostgresAccessAlphaTeam includes the requested fields of the GraphQL type Team. +// The GraphQL type's documentation follows. +// +// The team type represents a team on the [Nais platform](https://nais.io/). +// +// Learn more about what Nais teams are and what they can be used for in the [official Nais documentation](https://docs.nais.io/explanations/team/). +// +// External resources (e.g. entraIDGroupID, gitHubTeamSlug) are managed by [Nais API reconcilers](https://github.com/nais/api-reconcilers). +type GetPostgresAccessAlphaTeam struct { + // Get a specific environment for the team. + Environment GetPostgresAccessAlphaTeamEnvironment `json:"environment"` +} + +// GetEnvironment returns GetPostgresAccessAlphaTeam.Environment, and is useful for accessing the field via an interface. +func (v *GetPostgresAccessAlphaTeam) GetEnvironment() GetPostgresAccessAlphaTeamEnvironment { + return v.Environment +} + +// GetPostgresAccessAlphaTeamEnvironment includes the requested fields of the GraphQL type TeamEnvironment. +type GetPostgresAccessAlphaTeamEnvironment struct { + // EXPERIMENTAL: DO NOT USE + // Get a PostgresAccess and its state. Available to authorized team members. + PostgresAccess GetPostgresAccessAlphaTeamEnvironmentPostgresAccess `json:"postgresAccess"` +} + +// GetPostgresAccess returns GetPostgresAccessAlphaTeamEnvironment.PostgresAccess, and is useful for accessing the field via an interface. +func (v *GetPostgresAccessAlphaTeamEnvironment) GetPostgresAccess() GetPostgresAccessAlphaTeamEnvironmentPostgresAccess { + return v.PostgresAccess +} + +// GetPostgresAccessAlphaTeamEnvironmentPostgresAccess includes the requested fields of the GraphQL type PostgresAccess. +// The GraphQL type's documentation follows. +// +// A time-limited personal access request for a Postgres branch. +type GetPostgresAccessAlphaTeamEnvironmentPostgresAccess struct { + // High-level state of the access. + State PostgresAccessState `json:"state"` + // Human-readable message for the current state. + Message *string `json:"message"` + // EXPERIMENTAL: DO NOT USE + // Get connection materials for this ready access. Only its owner can read them. + Connection *GetPostgresAccessAlphaTeamEnvironmentPostgresAccessConnectionPostgresAccessConnectionDetails `json:"connection"` +} + +// GetState returns GetPostgresAccessAlphaTeamEnvironmentPostgresAccess.State, and is useful for accessing the field via an interface. +func (v *GetPostgresAccessAlphaTeamEnvironmentPostgresAccess) GetState() PostgresAccessState { + return v.State +} + +// GetMessage returns GetPostgresAccessAlphaTeamEnvironmentPostgresAccess.Message, and is useful for accessing the field via an interface. +func (v *GetPostgresAccessAlphaTeamEnvironmentPostgresAccess) GetMessage() *string { return v.Message } + +// GetConnection returns GetPostgresAccessAlphaTeamEnvironmentPostgresAccess.Connection, and is useful for accessing the field via an interface. +func (v *GetPostgresAccessAlphaTeamEnvironmentPostgresAccess) GetConnection() *GetPostgresAccessAlphaTeamEnvironmentPostgresAccessConnectionPostgresAccessConnectionDetails { + return v.Connection +} + +// GetPostgresAccessAlphaTeamEnvironmentPostgresAccessConnectionPostgresAccessConnectionDetails includes the requested fields of the GraphQL type PostgresAccessConnectionDetails. +// The GraphQL type's documentation follows. +// +// Sensitive connection materials for a ready personal Postgres access. +type GetPostgresAccessAlphaTeamEnvironmentPostgresAccessConnectionPostgresAccessConnectionDetails struct { + // Database username for the caller's personal role. + Username string `json:"username"` + // Short-lived password for the caller's database role. + Password string `json:"password"` + // CA certificate required to verify the PostgreSQL server certificate. + CaCertificate string `json:"caCertificate"` + // PostgreSQL server name used for TLS verification. + ServerName string `json:"serverName"` + // Public HTTP/3 relay endpoint. + RelayEndpoint string `json:"relayEndpoint"` + // Relay-Access header value (namespace/name). + RelayAccess string `json:"relayAccess"` + // Owner-only bearer token for this access; never log it. + RelayToken string `json:"relayToken"` +} + +// GetUsername returns GetPostgresAccessAlphaTeamEnvironmentPostgresAccessConnectionPostgresAccessConnectionDetails.Username, and is useful for accessing the field via an interface. +func (v *GetPostgresAccessAlphaTeamEnvironmentPostgresAccessConnectionPostgresAccessConnectionDetails) GetUsername() string { + return v.Username +} + +// GetPassword returns GetPostgresAccessAlphaTeamEnvironmentPostgresAccessConnectionPostgresAccessConnectionDetails.Password, and is useful for accessing the field via an interface. +func (v *GetPostgresAccessAlphaTeamEnvironmentPostgresAccessConnectionPostgresAccessConnectionDetails) GetPassword() string { + return v.Password +} + +// GetCaCertificate returns GetPostgresAccessAlphaTeamEnvironmentPostgresAccessConnectionPostgresAccessConnectionDetails.CaCertificate, and is useful for accessing the field via an interface. +func (v *GetPostgresAccessAlphaTeamEnvironmentPostgresAccessConnectionPostgresAccessConnectionDetails) GetCaCertificate() string { + return v.CaCertificate +} + +// GetServerName returns GetPostgresAccessAlphaTeamEnvironmentPostgresAccessConnectionPostgresAccessConnectionDetails.ServerName, and is useful for accessing the field via an interface. +func (v *GetPostgresAccessAlphaTeamEnvironmentPostgresAccessConnectionPostgresAccessConnectionDetails) GetServerName() string { + return v.ServerName +} + +// GetRelayEndpoint returns GetPostgresAccessAlphaTeamEnvironmentPostgresAccessConnectionPostgresAccessConnectionDetails.RelayEndpoint, and is useful for accessing the field via an interface. +func (v *GetPostgresAccessAlphaTeamEnvironmentPostgresAccessConnectionPostgresAccessConnectionDetails) GetRelayEndpoint() string { + return v.RelayEndpoint +} + +// GetRelayAccess returns GetPostgresAccessAlphaTeamEnvironmentPostgresAccessConnectionPostgresAccessConnectionDetails.RelayAccess, and is useful for accessing the field via an interface. +func (v *GetPostgresAccessAlphaTeamEnvironmentPostgresAccessConnectionPostgresAccessConnectionDetails) GetRelayAccess() string { + return v.RelayAccess +} + +// GetRelayToken returns GetPostgresAccessAlphaTeamEnvironmentPostgresAccessConnectionPostgresAccessConnectionDetails.RelayToken, and is useful for accessing the field via an interface. +func (v *GetPostgresAccessAlphaTeamEnvironmentPostgresAccessConnectionPostgresAccessConnectionDetails) GetRelayToken() string { + return v.RelayToken +} + // GetSecretActivityResponse is returned by GetSecretActivity on success. type GetSecretActivityResponse struct { // Get a team by its slug. @@ -20282,6 +20874,8 @@ func (v *GetSecretActivityTeamSecretsSecretConnectionNodesSecretActivityLogActiv // GetSecretActivityTeamSecretsSecretConnectionNodesSecretActivityLogActivityLogEntryConnectionNodesOpenSearchUpdatedActivityLogEntry // GetSecretActivityTeamSecretsSecretConnectionNodesSecretActivityLogActivityLogEntryConnectionNodesPostgresDeletedActivityLogEntry // GetSecretActivityTeamSecretsSecretConnectionNodesSecretActivityLogActivityLogEntryConnectionNodesPostgresGrantAccessActivityLogEntry +// GetSecretActivityTeamSecretsSecretConnectionNodesSecretActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessConnectionActivityLogEntry +// GetSecretActivityTeamSecretsSecretConnectionNodesSecretActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessCreatedActivityLogEntry // GetSecretActivityTeamSecretsSecretConnectionNodesSecretActivityLogActivityLogEntryConnectionNodesReconcilerConfiguredActivityLogEntry // GetSecretActivityTeamSecretsSecretConnectionNodesSecretActivityLogActivityLogEntryConnectionNodesReconcilerDisabledActivityLogEntry // GetSecretActivityTeamSecretsSecretConnectionNodesSecretActivityLogActivityLogEntryConnectionNodesReconcilerEnabledActivityLogEntry @@ -20401,6 +20995,10 @@ func (v *GetSecretActivityTeamSecretsSecretConnectionNodesSecretActivityLogActiv } func (v *GetSecretActivityTeamSecretsSecretConnectionNodesSecretActivityLogActivityLogEntryConnectionNodesPostgresGrantAccessActivityLogEntry) implementsGraphQLInterfaceGetSecretActivityTeamSecretsSecretConnectionNodesSecretActivityLogActivityLogEntryConnectionNodesActivityLogEntry() { } +func (v *GetSecretActivityTeamSecretsSecretConnectionNodesSecretActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessConnectionActivityLogEntry) implementsGraphQLInterfaceGetSecretActivityTeamSecretsSecretConnectionNodesSecretActivityLogActivityLogEntryConnectionNodesActivityLogEntry() { +} +func (v *GetSecretActivityTeamSecretsSecretConnectionNodesSecretActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessCreatedActivityLogEntry) implementsGraphQLInterfaceGetSecretActivityTeamSecretsSecretConnectionNodesSecretActivityLogActivityLogEntryConnectionNodesActivityLogEntry() { +} func (v *GetSecretActivityTeamSecretsSecretConnectionNodesSecretActivityLogActivityLogEntryConnectionNodesReconcilerConfiguredActivityLogEntry) implementsGraphQLInterfaceGetSecretActivityTeamSecretsSecretConnectionNodesSecretActivityLogActivityLogEntryConnectionNodesActivityLogEntry() { } func (v *GetSecretActivityTeamSecretsSecretConnectionNodesSecretActivityLogActivityLogEntryConnectionNodesReconcilerDisabledActivityLogEntry) implementsGraphQLInterfaceGetSecretActivityTeamSecretsSecretConnectionNodesSecretActivityLogActivityLogEntryConnectionNodesActivityLogEntry() { @@ -20572,6 +21170,12 @@ func __unmarshalGetSecretActivityTeamSecretsSecretConnectionNodesSecretActivityL case "PostgresGrantAccessActivityLogEntry": *v = new(GetSecretActivityTeamSecretsSecretConnectionNodesSecretActivityLogActivityLogEntryConnectionNodesPostgresGrantAccessActivityLogEntry) return json.Unmarshal(b, *v) + case "PostgresPersonalAccessConnectionActivityLogEntry": + *v = new(GetSecretActivityTeamSecretsSecretConnectionNodesSecretActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessConnectionActivityLogEntry) + return json.Unmarshal(b, *v) + case "PostgresPersonalAccessCreatedActivityLogEntry": + *v = new(GetSecretActivityTeamSecretsSecretConnectionNodesSecretActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessCreatedActivityLogEntry) + return json.Unmarshal(b, *v) case "ReconcilerConfiguredActivityLogEntry": *v = new(GetSecretActivityTeamSecretsSecretConnectionNodesSecretActivityLogActivityLogEntryConnectionNodesReconcilerConfiguredActivityLogEntry) return json.Unmarshal(b, *v) @@ -20903,6 +21507,22 @@ func __marshalGetSecretActivityTeamSecretsSecretConnectionNodesSecretActivityLog *GetSecretActivityTeamSecretsSecretConnectionNodesSecretActivityLogActivityLogEntryConnectionNodesPostgresGrantAccessActivityLogEntry }{typename, v} return json.Marshal(result) + case *GetSecretActivityTeamSecretsSecretConnectionNodesSecretActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessConnectionActivityLogEntry: + typename = "PostgresPersonalAccessConnectionActivityLogEntry" + + result := struct { + TypeName string `json:"__typename"` + *GetSecretActivityTeamSecretsSecretConnectionNodesSecretActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessConnectionActivityLogEntry + }{typename, v} + return json.Marshal(result) + case *GetSecretActivityTeamSecretsSecretConnectionNodesSecretActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessCreatedActivityLogEntry: + typename = "PostgresPersonalAccessCreatedActivityLogEntry" + + result := struct { + TypeName string `json:"__typename"` + *GetSecretActivityTeamSecretsSecretConnectionNodesSecretActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessCreatedActivityLogEntry + }{typename, v} + return json.Marshal(result) case *GetSecretActivityTeamSecretsSecretConnectionNodesSecretActivityLogActivityLogEntryConnectionNodesReconcilerConfiguredActivityLogEntry: typename = "ReconcilerConfiguredActivityLogEntry" @@ -22169,6 +22789,88 @@ func (v *GetSecretActivityTeamSecretsSecretConnectionNodesSecretActivityLogActiv return v.EnvironmentName } +// GetSecretActivityTeamSecretsSecretConnectionNodesSecretActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessConnectionActivityLogEntry includes the requested fields of the GraphQL type PostgresPersonalAccessConnectionActivityLogEntry. +// The GraphQL type's documentation follows. +// +// An audit-log entry for retrieval of personal Postgres connection materials. +type GetSecretActivityTeamSecretsSecretConnectionNodesSecretActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessConnectionActivityLogEntry struct { + Typename *string `json:"__typename"` + // Interface for activity log entries. + Actor string `json:"actor"` + // Interface for activity log entries. + CreatedAt time.Time `json:"createdAt"` + // Interface for activity log entries. + Message string `json:"message"` + // Interface for activity log entries. + EnvironmentName *string `json:"environmentName"` +} + +// GetTypename returns GetSecretActivityTeamSecretsSecretConnectionNodesSecretActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessConnectionActivityLogEntry.Typename, and is useful for accessing the field via an interface. +func (v *GetSecretActivityTeamSecretsSecretConnectionNodesSecretActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessConnectionActivityLogEntry) GetTypename() *string { + return v.Typename +} + +// GetActor returns GetSecretActivityTeamSecretsSecretConnectionNodesSecretActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessConnectionActivityLogEntry.Actor, and is useful for accessing the field via an interface. +func (v *GetSecretActivityTeamSecretsSecretConnectionNodesSecretActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessConnectionActivityLogEntry) GetActor() string { + return v.Actor +} + +// GetCreatedAt returns GetSecretActivityTeamSecretsSecretConnectionNodesSecretActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessConnectionActivityLogEntry.CreatedAt, and is useful for accessing the field via an interface. +func (v *GetSecretActivityTeamSecretsSecretConnectionNodesSecretActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessConnectionActivityLogEntry) GetCreatedAt() time.Time { + return v.CreatedAt +} + +// GetMessage returns GetSecretActivityTeamSecretsSecretConnectionNodesSecretActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessConnectionActivityLogEntry.Message, and is useful for accessing the field via an interface. +func (v *GetSecretActivityTeamSecretsSecretConnectionNodesSecretActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessConnectionActivityLogEntry) GetMessage() string { + return v.Message +} + +// GetEnvironmentName returns GetSecretActivityTeamSecretsSecretConnectionNodesSecretActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessConnectionActivityLogEntry.EnvironmentName, and is useful for accessing the field via an interface. +func (v *GetSecretActivityTeamSecretsSecretConnectionNodesSecretActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessConnectionActivityLogEntry) GetEnvironmentName() *string { + return v.EnvironmentName +} + +// GetSecretActivityTeamSecretsSecretConnectionNodesSecretActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessCreatedActivityLogEntry includes the requested fields of the GraphQL type PostgresPersonalAccessCreatedActivityLogEntry. +// The GraphQL type's documentation follows. +// +// An audit-log entry for personal Postgres access created through the API broker. +type GetSecretActivityTeamSecretsSecretConnectionNodesSecretActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessCreatedActivityLogEntry struct { + Typename *string `json:"__typename"` + // Interface for activity log entries. + Actor string `json:"actor"` + // Interface for activity log entries. + CreatedAt time.Time `json:"createdAt"` + // Interface for activity log entries. + Message string `json:"message"` + // Interface for activity log entries. + EnvironmentName *string `json:"environmentName"` +} + +// GetTypename returns GetSecretActivityTeamSecretsSecretConnectionNodesSecretActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessCreatedActivityLogEntry.Typename, and is useful for accessing the field via an interface. +func (v *GetSecretActivityTeamSecretsSecretConnectionNodesSecretActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessCreatedActivityLogEntry) GetTypename() *string { + return v.Typename +} + +// GetActor returns GetSecretActivityTeamSecretsSecretConnectionNodesSecretActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessCreatedActivityLogEntry.Actor, and is useful for accessing the field via an interface. +func (v *GetSecretActivityTeamSecretsSecretConnectionNodesSecretActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessCreatedActivityLogEntry) GetActor() string { + return v.Actor +} + +// GetCreatedAt returns GetSecretActivityTeamSecretsSecretConnectionNodesSecretActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessCreatedActivityLogEntry.CreatedAt, and is useful for accessing the field via an interface. +func (v *GetSecretActivityTeamSecretsSecretConnectionNodesSecretActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessCreatedActivityLogEntry) GetCreatedAt() time.Time { + return v.CreatedAt +} + +// GetMessage returns GetSecretActivityTeamSecretsSecretConnectionNodesSecretActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessCreatedActivityLogEntry.Message, and is useful for accessing the field via an interface. +func (v *GetSecretActivityTeamSecretsSecretConnectionNodesSecretActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessCreatedActivityLogEntry) GetMessage() string { + return v.Message +} + +// GetEnvironmentName returns GetSecretActivityTeamSecretsSecretConnectionNodesSecretActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessCreatedActivityLogEntry.EnvironmentName, and is useful for accessing the field via an interface. +func (v *GetSecretActivityTeamSecretsSecretConnectionNodesSecretActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessCreatedActivityLogEntry) GetEnvironmentName() *string { + return v.EnvironmentName +} + // GetSecretActivityTeamSecretsSecretConnectionNodesSecretActivityLogActivityLogEntryConnectionNodesReconcilerConfiguredActivityLogEntry includes the requested fields of the GraphQL type ReconcilerConfiguredActivityLogEntry. type GetSecretActivityTeamSecretsSecretConnectionNodesSecretActivityLogActivityLogEntryConnectionNodesReconcilerConfiguredActivityLogEntry struct { Typename *string `json:"__typename"` @@ -24261,6 +24963,8 @@ func (v *GetTeamActivityTeamActivityLogActivityLogEntryConnection) __premarshalJ // GetTeamActivityTeamActivityLogActivityLogEntryConnectionNodesOpenSearchUpdatedActivityLogEntry // GetTeamActivityTeamActivityLogActivityLogEntryConnectionNodesPostgresDeletedActivityLogEntry // GetTeamActivityTeamActivityLogActivityLogEntryConnectionNodesPostgresGrantAccessActivityLogEntry +// GetTeamActivityTeamActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessConnectionActivityLogEntry +// GetTeamActivityTeamActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessCreatedActivityLogEntry // GetTeamActivityTeamActivityLogActivityLogEntryConnectionNodesReconcilerConfiguredActivityLogEntry // GetTeamActivityTeamActivityLogActivityLogEntryConnectionNodesReconcilerDisabledActivityLogEntry // GetTeamActivityTeamActivityLogActivityLogEntryConnectionNodesReconcilerEnabledActivityLogEntry @@ -24390,6 +25094,10 @@ func (v *GetTeamActivityTeamActivityLogActivityLogEntryConnectionNodesPostgresDe } func (v *GetTeamActivityTeamActivityLogActivityLogEntryConnectionNodesPostgresGrantAccessActivityLogEntry) implementsGraphQLInterfaceGetTeamActivityTeamActivityLogActivityLogEntryConnectionNodesActivityLogEntry() { } +func (v *GetTeamActivityTeamActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessConnectionActivityLogEntry) implementsGraphQLInterfaceGetTeamActivityTeamActivityLogActivityLogEntryConnectionNodesActivityLogEntry() { +} +func (v *GetTeamActivityTeamActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessCreatedActivityLogEntry) implementsGraphQLInterfaceGetTeamActivityTeamActivityLogActivityLogEntryConnectionNodesActivityLogEntry() { +} func (v *GetTeamActivityTeamActivityLogActivityLogEntryConnectionNodesReconcilerConfiguredActivityLogEntry) implementsGraphQLInterfaceGetTeamActivityTeamActivityLogActivityLogEntryConnectionNodesActivityLogEntry() { } func (v *GetTeamActivityTeamActivityLogActivityLogEntryConnectionNodesReconcilerDisabledActivityLogEntry) implementsGraphQLInterfaceGetTeamActivityTeamActivityLogActivityLogEntryConnectionNodesActivityLogEntry() { @@ -24561,6 +25269,12 @@ func __unmarshalGetTeamActivityTeamActivityLogActivityLogEntryConnectionNodesAct case "PostgresGrantAccessActivityLogEntry": *v = new(GetTeamActivityTeamActivityLogActivityLogEntryConnectionNodesPostgresGrantAccessActivityLogEntry) return json.Unmarshal(b, *v) + case "PostgresPersonalAccessConnectionActivityLogEntry": + *v = new(GetTeamActivityTeamActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessConnectionActivityLogEntry) + return json.Unmarshal(b, *v) + case "PostgresPersonalAccessCreatedActivityLogEntry": + *v = new(GetTeamActivityTeamActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessCreatedActivityLogEntry) + return json.Unmarshal(b, *v) case "ReconcilerConfiguredActivityLogEntry": *v = new(GetTeamActivityTeamActivityLogActivityLogEntryConnectionNodesReconcilerConfiguredActivityLogEntry) return json.Unmarshal(b, *v) @@ -24892,6 +25606,22 @@ func __marshalGetTeamActivityTeamActivityLogActivityLogEntryConnectionNodesActiv *GetTeamActivityTeamActivityLogActivityLogEntryConnectionNodesPostgresGrantAccessActivityLogEntry }{typename, v} return json.Marshal(result) + case *GetTeamActivityTeamActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessConnectionActivityLogEntry: + typename = "PostgresPersonalAccessConnectionActivityLogEntry" + + result := struct { + TypeName string `json:"__typename"` + *GetTeamActivityTeamActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessConnectionActivityLogEntry + }{typename, v} + return json.Marshal(result) + case *GetTeamActivityTeamActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessCreatedActivityLogEntry: + typename = "PostgresPersonalAccessCreatedActivityLogEntry" + + result := struct { + TypeName string `json:"__typename"` + *GetTeamActivityTeamActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessCreatedActivityLogEntry + }{typename, v} + return json.Marshal(result) case *GetTeamActivityTeamActivityLogActivityLogEntryConnectionNodesReconcilerConfiguredActivityLogEntry: typename = "ReconcilerConfiguredActivityLogEntry" @@ -26494,6 +27224,116 @@ func (v *GetTeamActivityTeamActivityLogActivityLogEntryConnectionNodesPostgresGr return v.ResourceName } +// GetTeamActivityTeamActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessConnectionActivityLogEntry includes the requested fields of the GraphQL type PostgresPersonalAccessConnectionActivityLogEntry. +// The GraphQL type's documentation follows. +// +// An audit-log entry for retrieval of personal Postgres connection materials. +type GetTeamActivityTeamActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessConnectionActivityLogEntry struct { + Typename *string `json:"__typename"` + // Interface for activity log entries. + Actor string `json:"actor"` + // Interface for activity log entries. + CreatedAt time.Time `json:"createdAt"` + // Interface for activity log entries. + Message string `json:"message"` + // Interface for activity log entries. + EnvironmentName *string `json:"environmentName"` + // Interface for activity log entries. + ResourceType ActivityLogEntryResourceType `json:"resourceType"` + // Interface for activity log entries. + ResourceName string `json:"resourceName"` +} + +// GetTypename returns GetTeamActivityTeamActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessConnectionActivityLogEntry.Typename, and is useful for accessing the field via an interface. +func (v *GetTeamActivityTeamActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessConnectionActivityLogEntry) GetTypename() *string { + return v.Typename +} + +// GetActor returns GetTeamActivityTeamActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessConnectionActivityLogEntry.Actor, and is useful for accessing the field via an interface. +func (v *GetTeamActivityTeamActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessConnectionActivityLogEntry) GetActor() string { + return v.Actor +} + +// GetCreatedAt returns GetTeamActivityTeamActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessConnectionActivityLogEntry.CreatedAt, and is useful for accessing the field via an interface. +func (v *GetTeamActivityTeamActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessConnectionActivityLogEntry) GetCreatedAt() time.Time { + return v.CreatedAt +} + +// GetMessage returns GetTeamActivityTeamActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessConnectionActivityLogEntry.Message, and is useful for accessing the field via an interface. +func (v *GetTeamActivityTeamActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessConnectionActivityLogEntry) GetMessage() string { + return v.Message +} + +// GetEnvironmentName returns GetTeamActivityTeamActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessConnectionActivityLogEntry.EnvironmentName, and is useful for accessing the field via an interface. +func (v *GetTeamActivityTeamActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessConnectionActivityLogEntry) GetEnvironmentName() *string { + return v.EnvironmentName +} + +// GetResourceType returns GetTeamActivityTeamActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessConnectionActivityLogEntry.ResourceType, and is useful for accessing the field via an interface. +func (v *GetTeamActivityTeamActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessConnectionActivityLogEntry) GetResourceType() ActivityLogEntryResourceType { + return v.ResourceType +} + +// GetResourceName returns GetTeamActivityTeamActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessConnectionActivityLogEntry.ResourceName, and is useful for accessing the field via an interface. +func (v *GetTeamActivityTeamActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessConnectionActivityLogEntry) GetResourceName() string { + return v.ResourceName +} + +// GetTeamActivityTeamActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessCreatedActivityLogEntry includes the requested fields of the GraphQL type PostgresPersonalAccessCreatedActivityLogEntry. +// The GraphQL type's documentation follows. +// +// An audit-log entry for personal Postgres access created through the API broker. +type GetTeamActivityTeamActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessCreatedActivityLogEntry struct { + Typename *string `json:"__typename"` + // Interface for activity log entries. + Actor string `json:"actor"` + // Interface for activity log entries. + CreatedAt time.Time `json:"createdAt"` + // Interface for activity log entries. + Message string `json:"message"` + // Interface for activity log entries. + EnvironmentName *string `json:"environmentName"` + // Interface for activity log entries. + ResourceType ActivityLogEntryResourceType `json:"resourceType"` + // Interface for activity log entries. + ResourceName string `json:"resourceName"` +} + +// GetTypename returns GetTeamActivityTeamActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessCreatedActivityLogEntry.Typename, and is useful for accessing the field via an interface. +func (v *GetTeamActivityTeamActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessCreatedActivityLogEntry) GetTypename() *string { + return v.Typename +} + +// GetActor returns GetTeamActivityTeamActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessCreatedActivityLogEntry.Actor, and is useful for accessing the field via an interface. +func (v *GetTeamActivityTeamActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessCreatedActivityLogEntry) GetActor() string { + return v.Actor +} + +// GetCreatedAt returns GetTeamActivityTeamActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessCreatedActivityLogEntry.CreatedAt, and is useful for accessing the field via an interface. +func (v *GetTeamActivityTeamActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessCreatedActivityLogEntry) GetCreatedAt() time.Time { + return v.CreatedAt +} + +// GetMessage returns GetTeamActivityTeamActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessCreatedActivityLogEntry.Message, and is useful for accessing the field via an interface. +func (v *GetTeamActivityTeamActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessCreatedActivityLogEntry) GetMessage() string { + return v.Message +} + +// GetEnvironmentName returns GetTeamActivityTeamActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessCreatedActivityLogEntry.EnvironmentName, and is useful for accessing the field via an interface. +func (v *GetTeamActivityTeamActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessCreatedActivityLogEntry) GetEnvironmentName() *string { + return v.EnvironmentName +} + +// GetResourceType returns GetTeamActivityTeamActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessCreatedActivityLogEntry.ResourceType, and is useful for accessing the field via an interface. +func (v *GetTeamActivityTeamActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessCreatedActivityLogEntry) GetResourceType() ActivityLogEntryResourceType { + return v.ResourceType +} + +// GetResourceName returns GetTeamActivityTeamActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessCreatedActivityLogEntry.ResourceName, and is useful for accessing the field via an interface. +func (v *GetTeamActivityTeamActivityLogActivityLogEntryConnectionNodesPostgresPersonalAccessCreatedActivityLogEntry) GetResourceName() string { + return v.ResourceName +} + // GetTeamActivityTeamActivityLogActivityLogEntryConnectionNodesReconcilerConfiguredActivityLogEntry includes the requested fields of the GraphQL type ReconcilerConfiguredActivityLogEntry. type GetTeamActivityTeamActivityLogActivityLogEntryConnectionNodesReconcilerConfiguredActivityLogEntry struct { Typename *string `json:"__typename"` @@ -29517,6 +30357,122 @@ func (v *GetTeamApplicationsTeamApplicationsApplicationConnectionNodesApplicatio return v.Name } +// GetTeamCloudSQLInstancesResponse is returned by GetTeamCloudSQLInstances on success. +type GetTeamCloudSQLInstancesResponse struct { + // Get a team by its slug. + Team GetTeamCloudSQLInstancesTeam `json:"team"` +} + +// GetTeam returns GetTeamCloudSQLInstancesResponse.Team, and is useful for accessing the field via an interface. +func (v *GetTeamCloudSQLInstancesResponse) GetTeam() GetTeamCloudSQLInstancesTeam { return v.Team } + +// GetTeamCloudSQLInstancesTeam includes the requested fields of the GraphQL type Team. +// The GraphQL type's documentation follows. +// +// The team type represents a team on the [Nais platform](https://nais.io/). +// +// Learn more about what Nais teams are and what they can be used for in the [official Nais documentation](https://docs.nais.io/explanations/team/). +// +// External resources (e.g. entraIDGroupID, gitHubTeamSlug) are managed by [Nais API reconcilers](https://github.com/nais/api-reconcilers). +type GetTeamCloudSQLInstancesTeam struct { + // SQL instances owned by the team. + SqlInstances GetTeamCloudSQLInstancesTeamSqlInstancesSqlInstanceConnection `json:"sqlInstances"` +} + +// GetSqlInstances returns GetTeamCloudSQLInstancesTeam.SqlInstances, and is useful for accessing the field via an interface. +func (v *GetTeamCloudSQLInstancesTeam) GetSqlInstances() GetTeamCloudSQLInstancesTeamSqlInstancesSqlInstanceConnection { + return v.SqlInstances +} + +// GetTeamCloudSQLInstancesTeamSqlInstancesSqlInstanceConnection includes the requested fields of the GraphQL type SqlInstanceConnection. +type GetTeamCloudSQLInstancesTeamSqlInstancesSqlInstanceConnection struct { + Nodes []GetTeamCloudSQLInstancesTeamSqlInstancesSqlInstanceConnectionNodesSqlInstance `json:"nodes"` +} + +// GetNodes returns GetTeamCloudSQLInstancesTeamSqlInstancesSqlInstanceConnection.Nodes, and is useful for accessing the field via an interface. +func (v *GetTeamCloudSQLInstancesTeamSqlInstancesSqlInstanceConnection) GetNodes() []GetTeamCloudSQLInstancesTeamSqlInstancesSqlInstanceConnectionNodesSqlInstance { + return v.Nodes +} + +// GetTeamCloudSQLInstancesTeamSqlInstancesSqlInstanceConnectionNodesSqlInstance includes the requested fields of the GraphQL type SqlInstance. +type GetTeamCloudSQLInstancesTeamSqlInstancesSqlInstanceConnectionNodesSqlInstance struct { + Name string `json:"name"` + TeamEnvironment GetTeamCloudSQLInstancesTeamSqlInstancesSqlInstanceConnectionNodesSqlInstanceTeamEnvironment `json:"teamEnvironment"` + Version *string `json:"version"` + HighAvailability bool `json:"highAvailability"` + // Indicates whether audit logging is enabled for this SQL instance and provides a link to the logs if set. + AuditLog *GetTeamCloudSQLInstancesTeamSqlInstancesSqlInstanceConnectionNodesSqlInstanceAuditLog `json:"auditLog"` + State SqlInstanceState `json:"state"` +} + +// GetName returns GetTeamCloudSQLInstancesTeamSqlInstancesSqlInstanceConnectionNodesSqlInstance.Name, and is useful for accessing the field via an interface. +func (v *GetTeamCloudSQLInstancesTeamSqlInstancesSqlInstanceConnectionNodesSqlInstance) GetName() string { + return v.Name +} + +// GetTeamEnvironment returns GetTeamCloudSQLInstancesTeamSqlInstancesSqlInstanceConnectionNodesSqlInstance.TeamEnvironment, and is useful for accessing the field via an interface. +func (v *GetTeamCloudSQLInstancesTeamSqlInstancesSqlInstanceConnectionNodesSqlInstance) GetTeamEnvironment() GetTeamCloudSQLInstancesTeamSqlInstancesSqlInstanceConnectionNodesSqlInstanceTeamEnvironment { + return v.TeamEnvironment +} + +// GetVersion returns GetTeamCloudSQLInstancesTeamSqlInstancesSqlInstanceConnectionNodesSqlInstance.Version, and is useful for accessing the field via an interface. +func (v *GetTeamCloudSQLInstancesTeamSqlInstancesSqlInstanceConnectionNodesSqlInstance) GetVersion() *string { + return v.Version +} + +// GetHighAvailability returns GetTeamCloudSQLInstancesTeamSqlInstancesSqlInstanceConnectionNodesSqlInstance.HighAvailability, and is useful for accessing the field via an interface. +func (v *GetTeamCloudSQLInstancesTeamSqlInstancesSqlInstanceConnectionNodesSqlInstance) GetHighAvailability() bool { + return v.HighAvailability +} + +// GetAuditLog returns GetTeamCloudSQLInstancesTeamSqlInstancesSqlInstanceConnectionNodesSqlInstance.AuditLog, and is useful for accessing the field via an interface. +func (v *GetTeamCloudSQLInstancesTeamSqlInstancesSqlInstanceConnectionNodesSqlInstance) GetAuditLog() *GetTeamCloudSQLInstancesTeamSqlInstancesSqlInstanceConnectionNodesSqlInstanceAuditLog { + return v.AuditLog +} + +// GetState returns GetTeamCloudSQLInstancesTeamSqlInstancesSqlInstanceConnectionNodesSqlInstance.State, and is useful for accessing the field via an interface. +func (v *GetTeamCloudSQLInstancesTeamSqlInstancesSqlInstanceConnectionNodesSqlInstance) GetState() SqlInstanceState { + return v.State +} + +// GetTeamCloudSQLInstancesTeamSqlInstancesSqlInstanceConnectionNodesSqlInstanceAuditLog includes the requested fields of the GraphQL type AuditLog. +type GetTeamCloudSQLInstancesTeamSqlInstancesSqlInstanceConnectionNodesSqlInstanceAuditLog struct { + // Link to the audit log for this SQL instance. + LogUrl *string `json:"logUrl"` +} + +// GetLogUrl returns GetTeamCloudSQLInstancesTeamSqlInstancesSqlInstanceConnectionNodesSqlInstanceAuditLog.LogUrl, and is useful for accessing the field via an interface. +func (v *GetTeamCloudSQLInstancesTeamSqlInstancesSqlInstanceConnectionNodesSqlInstanceAuditLog) GetLogUrl() *string { + return v.LogUrl +} + +// GetTeamCloudSQLInstancesTeamSqlInstancesSqlInstanceConnectionNodesSqlInstanceTeamEnvironment includes the requested fields of the GraphQL type TeamEnvironment. +type GetTeamCloudSQLInstancesTeamSqlInstancesSqlInstanceConnectionNodesSqlInstanceTeamEnvironment struct { + // Get the environment. + Environment GetTeamCloudSQLInstancesTeamSqlInstancesSqlInstanceConnectionNodesSqlInstanceTeamEnvironmentEnvironment `json:"environment"` +} + +// GetEnvironment returns GetTeamCloudSQLInstancesTeamSqlInstancesSqlInstanceConnectionNodesSqlInstanceTeamEnvironment.Environment, and is useful for accessing the field via an interface. +func (v *GetTeamCloudSQLInstancesTeamSqlInstancesSqlInstanceConnectionNodesSqlInstanceTeamEnvironment) GetEnvironment() GetTeamCloudSQLInstancesTeamSqlInstancesSqlInstanceConnectionNodesSqlInstanceTeamEnvironmentEnvironment { + return v.Environment +} + +// GetTeamCloudSQLInstancesTeamSqlInstancesSqlInstanceConnectionNodesSqlInstanceTeamEnvironmentEnvironment includes the requested fields of the GraphQL type Environment. +// The GraphQL type's documentation follows. +// +// An environment represents a runtime environment for workloads. +// +// Learn more in the [official Nais documentation](https://docs.nais.io/workloads/explanations/environment/). +type GetTeamCloudSQLInstancesTeamSqlInstancesSqlInstanceConnectionNodesSqlInstanceTeamEnvironmentEnvironment struct { + // Unique name of the environment. + Name string `json:"name"` +} + +// GetName returns GetTeamCloudSQLInstancesTeamSqlInstancesSqlInstanceConnectionNodesSqlInstanceTeamEnvironmentEnvironment.Name, and is useful for accessing the field via an interface. +func (v *GetTeamCloudSQLInstancesTeamSqlInstancesSqlInstanceConnectionNodesSqlInstanceTeamEnvironmentEnvironment) GetName() string { + return v.Name +} + // GetTeamJobsResponse is returned by GetTeamJobs on success. type GetTeamJobsResponse struct { // Get a team by its slug. @@ -29858,16 +30814,18 @@ func (v *GetTeamKafkaTopicsTeamKafkaTopicsKafkaTopicConnectionNodesKafkaTopicTea return v.Name } -// GetTeamPostgresInstancesResponse is returned by GetTeamPostgresInstances on success. -type GetTeamPostgresInstancesResponse struct { +// GetTeamPostgresBranchesAlphaResponse is returned by GetTeamPostgresBranchesAlpha on success. +type GetTeamPostgresBranchesAlphaResponse struct { // Get a team by its slug. - Team GetTeamPostgresInstancesTeam `json:"team"` + Team GetTeamPostgresBranchesAlphaTeam `json:"team"` } -// GetTeam returns GetTeamPostgresInstancesResponse.Team, and is useful for accessing the field via an interface. -func (v *GetTeamPostgresInstancesResponse) GetTeam() GetTeamPostgresInstancesTeam { return v.Team } +// GetTeam returns GetTeamPostgresBranchesAlphaResponse.Team, and is useful for accessing the field via an interface. +func (v *GetTeamPostgresBranchesAlphaResponse) GetTeam() GetTeamPostgresBranchesAlphaTeam { + return v.Team +} -// GetTeamPostgresInstancesTeam includes the requested fields of the GraphQL type Team. +// GetTeamPostgresBranchesAlphaTeam includes the requested fields of the GraphQL type Team. // The GraphQL type's documentation follows. // // The team type represents a team on the [Nais platform](https://nais.io/). @@ -29875,201 +30833,112 @@ func (v *GetTeamPostgresInstancesResponse) GetTeam() GetTeamPostgresInstancesTea // Learn more about what Nais teams are and what they can be used for in the [official Nais documentation](https://docs.nais.io/explanations/team/). // // External resources (e.g. entraIDGroupID, gitHubTeamSlug) are managed by [Nais API reconcilers](https://github.com/nais/api-reconcilers). -type GetTeamPostgresInstancesTeam struct { - // Postgres instances owned by the team. - PostgresInstances GetTeamPostgresInstancesTeamPostgresInstancesPostgresInstanceConnection `json:"postgresInstances"` - // SQL instances owned by the team. - SqlInstances GetTeamPostgresInstancesTeamSqlInstancesSqlInstanceConnection `json:"sqlInstances"` -} - -// GetPostgresInstances returns GetTeamPostgresInstancesTeam.PostgresInstances, and is useful for accessing the field via an interface. -func (v *GetTeamPostgresInstancesTeam) GetPostgresInstances() GetTeamPostgresInstancesTeamPostgresInstancesPostgresInstanceConnection { - return v.PostgresInstances +type GetTeamPostgresBranchesAlphaTeam struct { + // Postgres branches owned by the team. + PostgresBranches GetTeamPostgresBranchesAlphaTeamPostgresBranchesPostgresBranchConnection `json:"postgresBranches"` } -// GetSqlInstances returns GetTeamPostgresInstancesTeam.SqlInstances, and is useful for accessing the field via an interface. -func (v *GetTeamPostgresInstancesTeam) GetSqlInstances() GetTeamPostgresInstancesTeamSqlInstancesSqlInstanceConnection { - return v.SqlInstances +// GetPostgresBranches returns GetTeamPostgresBranchesAlphaTeam.PostgresBranches, and is useful for accessing the field via an interface. +func (v *GetTeamPostgresBranchesAlphaTeam) GetPostgresBranches() GetTeamPostgresBranchesAlphaTeamPostgresBranchesPostgresBranchConnection { + return v.PostgresBranches } -// GetTeamPostgresInstancesTeamPostgresInstancesPostgresInstanceConnection includes the requested fields of the GraphQL type PostgresInstanceConnection. -type GetTeamPostgresInstancesTeamPostgresInstancesPostgresInstanceConnection struct { - Nodes []GetTeamPostgresInstancesTeamPostgresInstancesPostgresInstanceConnectionNodesPostgresInstance `json:"nodes"` +// GetTeamPostgresBranchesAlphaTeamPostgresBranchesPostgresBranchConnection includes the requested fields of the GraphQL type PostgresBranchConnection. +type GetTeamPostgresBranchesAlphaTeamPostgresBranchesPostgresBranchConnection struct { + Nodes []GetTeamPostgresBranchesAlphaTeamPostgresBranchesPostgresBranchConnectionNodesPostgresBranch `json:"nodes"` } -// GetNodes returns GetTeamPostgresInstancesTeamPostgresInstancesPostgresInstanceConnection.Nodes, and is useful for accessing the field via an interface. -func (v *GetTeamPostgresInstancesTeamPostgresInstancesPostgresInstanceConnection) GetNodes() []GetTeamPostgresInstancesTeamPostgresInstancesPostgresInstanceConnectionNodesPostgresInstance { +// GetNodes returns GetTeamPostgresBranchesAlphaTeamPostgresBranchesPostgresBranchConnection.Nodes, and is useful for accessing the field via an interface. +func (v *GetTeamPostgresBranchesAlphaTeamPostgresBranchesPostgresBranchConnection) GetNodes() []GetTeamPostgresBranchesAlphaTeamPostgresBranchesPostgresBranchConnectionNodesPostgresBranch { return v.Nodes } -// GetTeamPostgresInstancesTeamPostgresInstancesPostgresInstanceConnectionNodesPostgresInstance includes the requested fields of the GraphQL type PostgresInstance. -type GetTeamPostgresInstancesTeamPostgresInstancesPostgresInstanceConnectionNodesPostgresInstance struct { - Name string `json:"name"` - TeamEnvironment GetTeamPostgresInstancesTeamPostgresInstancesPostgresInstanceConnectionNodesPostgresInstanceTeamEnvironment `json:"teamEnvironment"` - // Major version of PostgreSQL. - MajorVersion string `json:"majorVersion"` - // Indicates whether the Postgres cluster is configured for high availability. - HighAvailability bool `json:"highAvailability"` - // Audit logging configuration for the Postgres cluster. - Audit GetTeamPostgresInstancesTeamPostgresInstancesPostgresInstanceConnectionNodesPostgresInstanceAudit `json:"audit"` - // Current state of the Postgres cluster. - State PostgresInstanceState `json:"state"` +// GetTeamPostgresBranchesAlphaTeamPostgresBranchesPostgresBranchConnectionNodesPostgresBranch includes the requested fields of the GraphQL type PostgresBranch. +// The GraphQL type's documentation follows. +// +// A named PostgresBranch belonging to a Postgres. +type GetTeamPostgresBranchesAlphaTeamPostgresBranchesPostgresBranchConnectionNodesPostgresBranch struct { + // Local name of this branch within its Postgres. + Name string `json:"name"` + TeamEnvironment GetTeamPostgresBranchesAlphaTeamPostgresBranchesPostgresBranchConnectionNodesPostgresBranchTeamEnvironment `json:"teamEnvironment"` + // Postgres owning this PostgresBranch. + Postgres GetTeamPostgresBranchesAlphaTeamPostgresBranchesPostgresBranchConnectionNodesPostgresBranchPostgres `json:"postgres"` + // Current observed state of the branch. + State PostgresBranchState `json:"state"` } -// GetName returns GetTeamPostgresInstancesTeamPostgresInstancesPostgresInstanceConnectionNodesPostgresInstance.Name, and is useful for accessing the field via an interface. -func (v *GetTeamPostgresInstancesTeamPostgresInstancesPostgresInstanceConnectionNodesPostgresInstance) GetName() string { +// GetName returns GetTeamPostgresBranchesAlphaTeamPostgresBranchesPostgresBranchConnectionNodesPostgresBranch.Name, and is useful for accessing the field via an interface. +func (v *GetTeamPostgresBranchesAlphaTeamPostgresBranchesPostgresBranchConnectionNodesPostgresBranch) GetName() string { return v.Name } -// GetTeamEnvironment returns GetTeamPostgresInstancesTeamPostgresInstancesPostgresInstanceConnectionNodesPostgresInstance.TeamEnvironment, and is useful for accessing the field via an interface. -func (v *GetTeamPostgresInstancesTeamPostgresInstancesPostgresInstanceConnectionNodesPostgresInstance) GetTeamEnvironment() GetTeamPostgresInstancesTeamPostgresInstancesPostgresInstanceConnectionNodesPostgresInstanceTeamEnvironment { +// GetTeamEnvironment returns GetTeamPostgresBranchesAlphaTeamPostgresBranchesPostgresBranchConnectionNodesPostgresBranch.TeamEnvironment, and is useful for accessing the field via an interface. +func (v *GetTeamPostgresBranchesAlphaTeamPostgresBranchesPostgresBranchConnectionNodesPostgresBranch) GetTeamEnvironment() GetTeamPostgresBranchesAlphaTeamPostgresBranchesPostgresBranchConnectionNodesPostgresBranchTeamEnvironment { return v.TeamEnvironment } -// GetMajorVersion returns GetTeamPostgresInstancesTeamPostgresInstancesPostgresInstanceConnectionNodesPostgresInstance.MajorVersion, and is useful for accessing the field via an interface. -func (v *GetTeamPostgresInstancesTeamPostgresInstancesPostgresInstanceConnectionNodesPostgresInstance) GetMajorVersion() string { - return v.MajorVersion +// GetPostgres returns GetTeamPostgresBranchesAlphaTeamPostgresBranchesPostgresBranchConnectionNodesPostgresBranch.Postgres, and is useful for accessing the field via an interface. +func (v *GetTeamPostgresBranchesAlphaTeamPostgresBranchesPostgresBranchConnectionNodesPostgresBranch) GetPostgres() GetTeamPostgresBranchesAlphaTeamPostgresBranchesPostgresBranchConnectionNodesPostgresBranchPostgres { + return v.Postgres } -// GetHighAvailability returns GetTeamPostgresInstancesTeamPostgresInstancesPostgresInstanceConnectionNodesPostgresInstance.HighAvailability, and is useful for accessing the field via an interface. -func (v *GetTeamPostgresInstancesTeamPostgresInstancesPostgresInstanceConnectionNodesPostgresInstance) GetHighAvailability() bool { - return v.HighAvailability -} - -// GetAudit returns GetTeamPostgresInstancesTeamPostgresInstancesPostgresInstanceConnectionNodesPostgresInstance.Audit, and is useful for accessing the field via an interface. -func (v *GetTeamPostgresInstancesTeamPostgresInstancesPostgresInstanceConnectionNodesPostgresInstance) GetAudit() GetTeamPostgresInstancesTeamPostgresInstancesPostgresInstanceConnectionNodesPostgresInstanceAudit { - return v.Audit -} - -// GetState returns GetTeamPostgresInstancesTeamPostgresInstancesPostgresInstanceConnectionNodesPostgresInstance.State, and is useful for accessing the field via an interface. -func (v *GetTeamPostgresInstancesTeamPostgresInstancesPostgresInstanceConnectionNodesPostgresInstance) GetState() PostgresInstanceState { +// GetState returns GetTeamPostgresBranchesAlphaTeamPostgresBranchesPostgresBranchConnectionNodesPostgresBranch.State, and is useful for accessing the field via an interface. +func (v *GetTeamPostgresBranchesAlphaTeamPostgresBranchesPostgresBranchConnectionNodesPostgresBranch) GetState() PostgresBranchState { return v.State } -// GetTeamPostgresInstancesTeamPostgresInstancesPostgresInstanceConnectionNodesPostgresInstanceAudit includes the requested fields of the GraphQL type PostgresInstanceAudit. -type GetTeamPostgresInstancesTeamPostgresInstancesPostgresInstanceConnectionNodesPostgresInstanceAudit struct { - // Indicates whether audit logging is enabled for the Postgres cluster. - Enabled bool `json:"enabled"` -} - -// GetEnabled returns GetTeamPostgresInstancesTeamPostgresInstancesPostgresInstanceConnectionNodesPostgresInstanceAudit.Enabled, and is useful for accessing the field via an interface. -func (v *GetTeamPostgresInstancesTeamPostgresInstancesPostgresInstanceConnectionNodesPostgresInstanceAudit) GetEnabled() bool { - return v.Enabled -} - -// GetTeamPostgresInstancesTeamPostgresInstancesPostgresInstanceConnectionNodesPostgresInstanceTeamEnvironment includes the requested fields of the GraphQL type TeamEnvironment. -type GetTeamPostgresInstancesTeamPostgresInstancesPostgresInstanceConnectionNodesPostgresInstanceTeamEnvironment struct { - // Get the environment. - Environment GetTeamPostgresInstancesTeamPostgresInstancesPostgresInstanceConnectionNodesPostgresInstanceTeamEnvironmentEnvironment `json:"environment"` -} - -// GetEnvironment returns GetTeamPostgresInstancesTeamPostgresInstancesPostgresInstanceConnectionNodesPostgresInstanceTeamEnvironment.Environment, and is useful for accessing the field via an interface. -func (v *GetTeamPostgresInstancesTeamPostgresInstancesPostgresInstanceConnectionNodesPostgresInstanceTeamEnvironment) GetEnvironment() GetTeamPostgresInstancesTeamPostgresInstancesPostgresInstanceConnectionNodesPostgresInstanceTeamEnvironmentEnvironment { - return v.Environment -} - -// GetTeamPostgresInstancesTeamPostgresInstancesPostgresInstanceConnectionNodesPostgresInstanceTeamEnvironmentEnvironment includes the requested fields of the GraphQL type Environment. +// GetTeamPostgresBranchesAlphaTeamPostgresBranchesPostgresBranchConnectionNodesPostgresBranchPostgres includes the requested fields of the GraphQL type Postgres. // The GraphQL type's documentation follows. // -// An environment represents a runtime environment for workloads. -// -// Learn more in the [official Nais documentation](https://docs.nais.io/workloads/explanations/environment/). -type GetTeamPostgresInstancesTeamPostgresInstancesPostgresInstanceConnectionNodesPostgresInstanceTeamEnvironmentEnvironment struct { - // Unique name of the environment. +// A Postgres whose active branch can change. +type GetTeamPostgresBranchesAlphaTeamPostgresBranchesPostgresBranchConnectionNodesPostgresBranchPostgres struct { + // Name of this Postgres. Name string `json:"name"` + // Configured PostgreSQL major version. + MajorVersion string `json:"majorVersion"` + // Whether high availability is configured. + HighAvailability bool `json:"highAvailability"` } -// GetName returns GetTeamPostgresInstancesTeamPostgresInstancesPostgresInstanceConnectionNodesPostgresInstanceTeamEnvironmentEnvironment.Name, and is useful for accessing the field via an interface. -func (v *GetTeamPostgresInstancesTeamPostgresInstancesPostgresInstanceConnectionNodesPostgresInstanceTeamEnvironmentEnvironment) GetName() string { - return v.Name -} - -// GetTeamPostgresInstancesTeamSqlInstancesSqlInstanceConnection includes the requested fields of the GraphQL type SqlInstanceConnection. -type GetTeamPostgresInstancesTeamSqlInstancesSqlInstanceConnection struct { - Nodes []GetTeamPostgresInstancesTeamSqlInstancesSqlInstanceConnectionNodesSqlInstance `json:"nodes"` -} - -// GetNodes returns GetTeamPostgresInstancesTeamSqlInstancesSqlInstanceConnection.Nodes, and is useful for accessing the field via an interface. -func (v *GetTeamPostgresInstancesTeamSqlInstancesSqlInstanceConnection) GetNodes() []GetTeamPostgresInstancesTeamSqlInstancesSqlInstanceConnectionNodesSqlInstance { - return v.Nodes -} - -// GetTeamPostgresInstancesTeamSqlInstancesSqlInstanceConnectionNodesSqlInstance includes the requested fields of the GraphQL type SqlInstance. -type GetTeamPostgresInstancesTeamSqlInstancesSqlInstanceConnectionNodesSqlInstance struct { - Name string `json:"name"` - TeamEnvironment GetTeamPostgresInstancesTeamSqlInstancesSqlInstanceConnectionNodesSqlInstanceTeamEnvironment `json:"teamEnvironment"` - Version *string `json:"version"` - HighAvailability bool `json:"highAvailability"` - // Indicates whether audit logging is enabled for this SQL instance and provides a link to the logs if set. - AuditLog *GetTeamPostgresInstancesTeamSqlInstancesSqlInstanceConnectionNodesSqlInstanceAuditLog `json:"auditLog"` - State SqlInstanceState `json:"state"` -} - -// GetName returns GetTeamPostgresInstancesTeamSqlInstancesSqlInstanceConnectionNodesSqlInstance.Name, and is useful for accessing the field via an interface. -func (v *GetTeamPostgresInstancesTeamSqlInstancesSqlInstanceConnectionNodesSqlInstance) GetName() string { +// GetName returns GetTeamPostgresBranchesAlphaTeamPostgresBranchesPostgresBranchConnectionNodesPostgresBranchPostgres.Name, and is useful for accessing the field via an interface. +func (v *GetTeamPostgresBranchesAlphaTeamPostgresBranchesPostgresBranchConnectionNodesPostgresBranchPostgres) GetName() string { return v.Name } -// GetTeamEnvironment returns GetTeamPostgresInstancesTeamSqlInstancesSqlInstanceConnectionNodesSqlInstance.TeamEnvironment, and is useful for accessing the field via an interface. -func (v *GetTeamPostgresInstancesTeamSqlInstancesSqlInstanceConnectionNodesSqlInstance) GetTeamEnvironment() GetTeamPostgresInstancesTeamSqlInstancesSqlInstanceConnectionNodesSqlInstanceTeamEnvironment { - return v.TeamEnvironment -} - -// GetVersion returns GetTeamPostgresInstancesTeamSqlInstancesSqlInstanceConnectionNodesSqlInstance.Version, and is useful for accessing the field via an interface. -func (v *GetTeamPostgresInstancesTeamSqlInstancesSqlInstanceConnectionNodesSqlInstance) GetVersion() *string { - return v.Version +// GetMajorVersion returns GetTeamPostgresBranchesAlphaTeamPostgresBranchesPostgresBranchConnectionNodesPostgresBranchPostgres.MajorVersion, and is useful for accessing the field via an interface. +func (v *GetTeamPostgresBranchesAlphaTeamPostgresBranchesPostgresBranchConnectionNodesPostgresBranchPostgres) GetMajorVersion() string { + return v.MajorVersion } -// GetHighAvailability returns GetTeamPostgresInstancesTeamSqlInstancesSqlInstanceConnectionNodesSqlInstance.HighAvailability, and is useful for accessing the field via an interface. -func (v *GetTeamPostgresInstancesTeamSqlInstancesSqlInstanceConnectionNodesSqlInstance) GetHighAvailability() bool { +// GetHighAvailability returns GetTeamPostgresBranchesAlphaTeamPostgresBranchesPostgresBranchConnectionNodesPostgresBranchPostgres.HighAvailability, and is useful for accessing the field via an interface. +func (v *GetTeamPostgresBranchesAlphaTeamPostgresBranchesPostgresBranchConnectionNodesPostgresBranchPostgres) GetHighAvailability() bool { return v.HighAvailability } -// GetAuditLog returns GetTeamPostgresInstancesTeamSqlInstancesSqlInstanceConnectionNodesSqlInstance.AuditLog, and is useful for accessing the field via an interface. -func (v *GetTeamPostgresInstancesTeamSqlInstancesSqlInstanceConnectionNodesSqlInstance) GetAuditLog() *GetTeamPostgresInstancesTeamSqlInstancesSqlInstanceConnectionNodesSqlInstanceAuditLog { - return v.AuditLog -} - -// GetState returns GetTeamPostgresInstancesTeamSqlInstancesSqlInstanceConnectionNodesSqlInstance.State, and is useful for accessing the field via an interface. -func (v *GetTeamPostgresInstancesTeamSqlInstancesSqlInstanceConnectionNodesSqlInstance) GetState() SqlInstanceState { - return v.State -} - -// GetTeamPostgresInstancesTeamSqlInstancesSqlInstanceConnectionNodesSqlInstanceAuditLog includes the requested fields of the GraphQL type AuditLog. -type GetTeamPostgresInstancesTeamSqlInstancesSqlInstanceConnectionNodesSqlInstanceAuditLog struct { - // Link to the audit log for this SQL instance. - LogUrl string `json:"logUrl"` -} - -// GetLogUrl returns GetTeamPostgresInstancesTeamSqlInstancesSqlInstanceConnectionNodesSqlInstanceAuditLog.LogUrl, and is useful for accessing the field via an interface. -func (v *GetTeamPostgresInstancesTeamSqlInstancesSqlInstanceConnectionNodesSqlInstanceAuditLog) GetLogUrl() string { - return v.LogUrl -} - -// GetTeamPostgresInstancesTeamSqlInstancesSqlInstanceConnectionNodesSqlInstanceTeamEnvironment includes the requested fields of the GraphQL type TeamEnvironment. -type GetTeamPostgresInstancesTeamSqlInstancesSqlInstanceConnectionNodesSqlInstanceTeamEnvironment struct { +// GetTeamPostgresBranchesAlphaTeamPostgresBranchesPostgresBranchConnectionNodesPostgresBranchTeamEnvironment includes the requested fields of the GraphQL type TeamEnvironment. +type GetTeamPostgresBranchesAlphaTeamPostgresBranchesPostgresBranchConnectionNodesPostgresBranchTeamEnvironment struct { // Get the environment. - Environment GetTeamPostgresInstancesTeamSqlInstancesSqlInstanceConnectionNodesSqlInstanceTeamEnvironmentEnvironment `json:"environment"` + Environment GetTeamPostgresBranchesAlphaTeamPostgresBranchesPostgresBranchConnectionNodesPostgresBranchTeamEnvironmentEnvironment `json:"environment"` } -// GetEnvironment returns GetTeamPostgresInstancesTeamSqlInstancesSqlInstanceConnectionNodesSqlInstanceTeamEnvironment.Environment, and is useful for accessing the field via an interface. -func (v *GetTeamPostgresInstancesTeamSqlInstancesSqlInstanceConnectionNodesSqlInstanceTeamEnvironment) GetEnvironment() GetTeamPostgresInstancesTeamSqlInstancesSqlInstanceConnectionNodesSqlInstanceTeamEnvironmentEnvironment { +// GetEnvironment returns GetTeamPostgresBranchesAlphaTeamPostgresBranchesPostgresBranchConnectionNodesPostgresBranchTeamEnvironment.Environment, and is useful for accessing the field via an interface. +func (v *GetTeamPostgresBranchesAlphaTeamPostgresBranchesPostgresBranchConnectionNodesPostgresBranchTeamEnvironment) GetEnvironment() GetTeamPostgresBranchesAlphaTeamPostgresBranchesPostgresBranchConnectionNodesPostgresBranchTeamEnvironmentEnvironment { return v.Environment } -// GetTeamPostgresInstancesTeamSqlInstancesSqlInstanceConnectionNodesSqlInstanceTeamEnvironmentEnvironment includes the requested fields of the GraphQL type Environment. +// GetTeamPostgresBranchesAlphaTeamPostgresBranchesPostgresBranchConnectionNodesPostgresBranchTeamEnvironmentEnvironment includes the requested fields of the GraphQL type Environment. // The GraphQL type's documentation follows. // // An environment represents a runtime environment for workloads. // // Learn more in the [official Nais documentation](https://docs.nais.io/workloads/explanations/environment/). -type GetTeamPostgresInstancesTeamSqlInstancesSqlInstanceConnectionNodesSqlInstanceTeamEnvironmentEnvironment struct { +type GetTeamPostgresBranchesAlphaTeamPostgresBranchesPostgresBranchConnectionNodesPostgresBranchTeamEnvironmentEnvironment struct { // Unique name of the environment. Name string `json:"name"` } -// GetName returns GetTeamPostgresInstancesTeamSqlInstancesSqlInstanceConnectionNodesSqlInstanceTeamEnvironmentEnvironment.Name, and is useful for accessing the field via an interface. -func (v *GetTeamPostgresInstancesTeamSqlInstancesSqlInstanceConnectionNodesSqlInstanceTeamEnvironmentEnvironment) GetName() string { +// GetName returns GetTeamPostgresBranchesAlphaTeamPostgresBranchesPostgresBranchConnectionNodesPostgresBranchTeamEnvironmentEnvironment.Name, and is useful for accessing the field via an interface. +func (v *GetTeamPostgresBranchesAlphaTeamPostgresBranchesPostgresBranchConnectionNodesPostgresBranchTeamEnvironmentEnvironment) GetName() string { return v.Name } @@ -30545,50 +31414,6 @@ func (v *GrantAccessToKafkaTopicUpdateKafkaTopicUpdateKafkaTopicPayloadKafkaTopi return v.Id } -// GrantPostgresAccessGrantPostgresAccessGrantPostgresAccessPayload includes the requested fields of the GraphQL type GrantPostgresAccessPayload. -type GrantPostgresAccessGrantPostgresAccessGrantPostgresAccessPayload struct { - Error *string `json:"error"` -} - -// GetError returns GrantPostgresAccessGrantPostgresAccessGrantPostgresAccessPayload.Error, and is useful for accessing the field via an interface. -func (v *GrantPostgresAccessGrantPostgresAccessGrantPostgresAccessPayload) GetError() *string { - return v.Error -} - -type GrantPostgresAccessInput struct { - ClusterName string `json:"clusterName"` - TeamSlug string `json:"teamSlug"` - EnvironmentName string `json:"environmentName"` - Grantee string `json:"grantee"` - Duration string `json:"duration"` -} - -// GetClusterName returns GrantPostgresAccessInput.ClusterName, and is useful for accessing the field via an interface. -func (v *GrantPostgresAccessInput) GetClusterName() string { return v.ClusterName } - -// GetTeamSlug returns GrantPostgresAccessInput.TeamSlug, and is useful for accessing the field via an interface. -func (v *GrantPostgresAccessInput) GetTeamSlug() string { return v.TeamSlug } - -// GetEnvironmentName returns GrantPostgresAccessInput.EnvironmentName, and is useful for accessing the field via an interface. -func (v *GrantPostgresAccessInput) GetEnvironmentName() string { return v.EnvironmentName } - -// GetGrantee returns GrantPostgresAccessInput.Grantee, and is useful for accessing the field via an interface. -func (v *GrantPostgresAccessInput) GetGrantee() string { return v.Grantee } - -// GetDuration returns GrantPostgresAccessInput.Duration, and is useful for accessing the field via an interface. -func (v *GrantPostgresAccessInput) GetDuration() string { return v.Duration } - -// GrantPostgresAccessResponse is returned by GrantPostgresAccess on success. -type GrantPostgresAccessResponse struct { - // Grant temporary access to a Postgres cluster. - GrantPostgresAccess GrantPostgresAccessGrantPostgresAccessGrantPostgresAccessPayload `json:"grantPostgresAccess"` -} - -// GetGrantPostgresAccess returns GrantPostgresAccessResponse.GrantPostgresAccess, and is useful for accessing the field via an interface. -func (v *GrantPostgresAccessResponse) GetGrantPostgresAccess() GrantPostgresAccessGrantPostgresAccessGrantPostgresAccessPayload { - return v.GrantPostgresAccess -} - type ImageVulnerabilitySeverity string const ( @@ -31923,52 +32748,85 @@ var AllOrderDirection = []OrderDirection{ OrderDirectionDesc, } -// Input for filtering Postgres instances. -type PostgresInstanceFilter struct { - // Input for filtering Postgres instances. +// Privilege level granted to a personal Postgres database role. +type PostgresAccessLevel string + +const ( + // Read data without modifying it. + PostgresAccessLevelRead PostgresAccessLevel = "READ" + // Read and modify existing data. + PostgresAccessLevelReadwrite PostgresAccessLevel = "READWRITE" + // Read, modify, and create database objects where supported. + PostgresAccessLevelReadwritecreate PostgresAccessLevel = "READWRITECREATE" +) + +var AllPostgresAccessLevel = []PostgresAccessLevel{ + PostgresAccessLevelRead, + PostgresAccessLevelReadwrite, + PostgresAccessLevelReadwritecreate, +} + +// High-level reconciliation state of a personal Postgres access. +type PostgresAccessState string + +const ( + // The controller has not finished provisioning the access. + PostgresAccessStatePending PostgresAccessState = "PENDING" + // The access and its connection materials are ready. + PostgresAccessStateReady PostgresAccessState = "READY" + // The controller cannot provision the requested access. + PostgresAccessStateFailed PostgresAccessState = "FAILED" + // The server-controlled expiry time has passed. + PostgresAccessStateExpired PostgresAccessState = "EXPIRED" +) + +var AllPostgresAccessState = []PostgresAccessState{ + PostgresAccessStatePending, + PostgresAccessStateReady, + PostgresAccessStateFailed, + PostgresAccessStateExpired, +} + +// Input for filtering Postgres branches. +type PostgresBranchFilter struct { + // Filter by the name of the branch. Name *string `json:"name"` - // Input for filtering Postgres instances. + // Filter by environments. Environments []string `json:"environments"` - // Input for filtering Postgres instances. - States []PostgresInstanceState `json:"states"` - // Input for filtering Postgres instances. - HighAvailability *bool `json:"highAvailability"` - // Input for filtering Postgres instances. - MajorVersions []string `json:"majorVersions"` - // Input for filtering Postgres instances. + // Filter by branch state. + States []PostgresBranchState `json:"states"` + // Filter by user-defined labels. All listed labels must match. Labels []LabelFilter `json:"labels"` } -// GetName returns PostgresInstanceFilter.Name, and is useful for accessing the field via an interface. -func (v *PostgresInstanceFilter) GetName() *string { return v.Name } - -// GetEnvironments returns PostgresInstanceFilter.Environments, and is useful for accessing the field via an interface. -func (v *PostgresInstanceFilter) GetEnvironments() []string { return v.Environments } - -// GetStates returns PostgresInstanceFilter.States, and is useful for accessing the field via an interface. -func (v *PostgresInstanceFilter) GetStates() []PostgresInstanceState { return v.States } +// GetName returns PostgresBranchFilter.Name, and is useful for accessing the field via an interface. +func (v *PostgresBranchFilter) GetName() *string { return v.Name } -// GetHighAvailability returns PostgresInstanceFilter.HighAvailability, and is useful for accessing the field via an interface. -func (v *PostgresInstanceFilter) GetHighAvailability() *bool { return v.HighAvailability } +// GetEnvironments returns PostgresBranchFilter.Environments, and is useful for accessing the field via an interface. +func (v *PostgresBranchFilter) GetEnvironments() []string { return v.Environments } -// GetMajorVersions returns PostgresInstanceFilter.MajorVersions, and is useful for accessing the field via an interface. -func (v *PostgresInstanceFilter) GetMajorVersions() []string { return v.MajorVersions } +// GetStates returns PostgresBranchFilter.States, and is useful for accessing the field via an interface. +func (v *PostgresBranchFilter) GetStates() []PostgresBranchState { return v.States } -// GetLabels returns PostgresInstanceFilter.Labels, and is useful for accessing the field via an interface. -func (v *PostgresInstanceFilter) GetLabels() []LabelFilter { return v.Labels } +// GetLabels returns PostgresBranchFilter.Labels, and is useful for accessing the field via an interface. +func (v *PostgresBranchFilter) GetLabels() []LabelFilter { return v.Labels } -type PostgresInstanceState string +// Reconciliation and observed health of a PostgresBranch. +type PostgresBranchState string const ( - PostgresInstanceStateAvailable PostgresInstanceState = "AVAILABLE" - PostgresInstanceStateProgressing PostgresInstanceState = "PROGRESSING" - PostgresInstanceStateDegraded PostgresInstanceState = "DEGRADED" + // The branch is healthy and ready. + PostgresBranchStateAvailable PostgresBranchState = "AVAILABLE" + // The branch is provisioning or its state has not been observed yet. + PostgresBranchStateProgressing PostgresBranchState = "PROGRESSING" + // The branch has reported a failure. + PostgresBranchStateDegraded PostgresBranchState = "DEGRADED" ) -var AllPostgresInstanceState = []PostgresInstanceState{ - PostgresInstanceStateAvailable, - PostgresInstanceStateProgressing, - PostgresInstanceStateDegraded, +var AllPostgresBranchState = []PostgresBranchState{ + PostgresBranchStateAvailable, + PostgresBranchStateProgressing, + PostgresBranchStateDegraded, } // RemoveConfigValueRemoveConfigValueRemoveConfigValuePayload includes the requested fields of the GraphQL type RemoveConfigValuePayload. @@ -33975,6 +34833,14 @@ type __CreateOpenSearchInput struct { // GetInput returns __CreateOpenSearchInput.Input, and is useful for accessing the field via an interface. func (v *__CreateOpenSearchInput) GetInput() CreateOpenSearchInput { return v.Input } +// __CreatePostgresAccessAlphaInput is used internally by genqlient +type __CreatePostgresAccessAlphaInput struct { + Input CreatePostgresAccessInput `json:"input"` +} + +// GetInput returns __CreatePostgresAccessAlphaInput.Input, and is useful for accessing the field via an interface. +func (v *__CreatePostgresAccessAlphaInput) GetInput() CreatePostgresAccessInput { return v.Input } + // __CreateSecretInput is used internally by genqlient type __CreateSecretInput struct { Name string `json:"name"` @@ -34135,6 +35001,22 @@ type __FindWorkloadsForCveInput struct { // GetIdentifier returns __FindWorkloadsForCveInput.Identifier, and is useful for accessing the field via an interface. func (v *__FindWorkloadsForCveInput) GetIdentifier() string { return v.Identifier } +// __GetActivePostgresBranchAlphaInput is used internally by genqlient +type __GetActivePostgresBranchAlphaInput struct { + Team string `json:"team"` + Environment string `json:"environment"` + Postgres string `json:"postgres"` +} + +// GetTeam returns __GetActivePostgresBranchAlphaInput.Team, and is useful for accessing the field via an interface. +func (v *__GetActivePostgresBranchAlphaInput) GetTeam() string { return v.Team } + +// GetEnvironment returns __GetActivePostgresBranchAlphaInput.Environment, and is useful for accessing the field via an interface. +func (v *__GetActivePostgresBranchAlphaInput) GetEnvironment() string { return v.Environment } + +// GetPostgres returns __GetActivePostgresBranchAlphaInput.Postgres, and is useful for accessing the field via an interface. +func (v *__GetActivePostgresBranchAlphaInput) GetPostgres() string { return v.Postgres } + // __GetAllConfigsInput is used internally by genqlient type __GetAllConfigsInput struct { TeamSlug string `json:"teamSlug"` @@ -34511,6 +35393,22 @@ func (v *__GetOpenSearchInput) GetEnvironmentName() string { return v.Environmen // GetTeamSlug returns __GetOpenSearchInput.TeamSlug, and is useful for accessing the field via an interface. func (v *__GetOpenSearchInput) GetTeamSlug() string { return v.TeamSlug } +// __GetPostgresAccessAlphaInput is used internally by genqlient +type __GetPostgresAccessAlphaInput struct { + Team string `json:"team"` + Environment string `json:"environment"` + Name string `json:"name"` +} + +// GetTeam returns __GetPostgresAccessAlphaInput.Team, and is useful for accessing the field via an interface. +func (v *__GetPostgresAccessAlphaInput) GetTeam() string { return v.Team } + +// GetEnvironment returns __GetPostgresAccessAlphaInput.Environment, and is useful for accessing the field via an interface. +func (v *__GetPostgresAccessAlphaInput) GetEnvironment() string { return v.Environment } + +// GetName returns __GetPostgresAccessAlphaInput.Name, and is useful for accessing the field via an interface. +func (v *__GetPostgresAccessAlphaInput) GetName() string { return v.Name } + // __GetSecretActivityInput is used internally by genqlient type __GetSecretActivityInput struct { Team string `json:"team"` @@ -34581,6 +35479,18 @@ func (v *__GetTeamApplicationsInput) GetOrderBy() *ApplicationOrder { return v.O // GetFilter returns __GetTeamApplicationsInput.Filter, and is useful for accessing the field via an interface. func (v *__GetTeamApplicationsInput) GetFilter() *TeamApplicationsFilter { return v.Filter } +// __GetTeamCloudSQLInstancesInput is used internally by genqlient +type __GetTeamCloudSQLInstancesInput struct { + Team string `json:"team"` + SqlFilter *SqlInstanceFilter `json:"sqlFilter"` +} + +// GetTeam returns __GetTeamCloudSQLInstancesInput.Team, and is useful for accessing the field via an interface. +func (v *__GetTeamCloudSQLInstancesInput) GetTeam() string { return v.Team } + +// GetSqlFilter returns __GetTeamCloudSQLInstancesInput.SqlFilter, and is useful for accessing the field via an interface. +func (v *__GetTeamCloudSQLInstancesInput) GetSqlFilter() *SqlInstanceFilter { return v.SqlFilter } + // __GetTeamJobsInput is used internally by genqlient type __GetTeamJobsInput struct { Team string `json:"team"` @@ -34621,24 +35531,20 @@ func (v *__GetTeamKafkaTopicsInput) GetTeam() string { return v.Team } // GetFilter returns __GetTeamKafkaTopicsInput.Filter, and is useful for accessing the field via an interface. func (v *__GetTeamKafkaTopicsInput) GetFilter() *KafkaTopicFilter { return v.Filter } -// __GetTeamPostgresInstancesInput is used internally by genqlient -type __GetTeamPostgresInstancesInput struct { - Team string `json:"team"` - PostgresFilter *PostgresInstanceFilter `json:"postgresFilter"` - SqlFilter *SqlInstanceFilter `json:"sqlFilter"` +// __GetTeamPostgresBranchesAlphaInput is used internally by genqlient +type __GetTeamPostgresBranchesAlphaInput struct { + Team string `json:"team"` + PostgresFilter *PostgresBranchFilter `json:"postgresFilter"` } -// GetTeam returns __GetTeamPostgresInstancesInput.Team, and is useful for accessing the field via an interface. -func (v *__GetTeamPostgresInstancesInput) GetTeam() string { return v.Team } +// GetTeam returns __GetTeamPostgresBranchesAlphaInput.Team, and is useful for accessing the field via an interface. +func (v *__GetTeamPostgresBranchesAlphaInput) GetTeam() string { return v.Team } -// GetPostgresFilter returns __GetTeamPostgresInstancesInput.PostgresFilter, and is useful for accessing the field via an interface. -func (v *__GetTeamPostgresInstancesInput) GetPostgresFilter() *PostgresInstanceFilter { +// GetPostgresFilter returns __GetTeamPostgresBranchesAlphaInput.PostgresFilter, and is useful for accessing the field via an interface. +func (v *__GetTeamPostgresBranchesAlphaInput) GetPostgresFilter() *PostgresBranchFilter { return v.PostgresFilter } -// GetSqlFilter returns __GetTeamPostgresInstancesInput.SqlFilter, and is useful for accessing the field via an interface. -func (v *__GetTeamPostgresInstancesInput) GetSqlFilter() *SqlInstanceFilter { return v.SqlFilter } - // __GetTeamVulnerabilitySummaryInput is used internally by genqlient type __GetTeamVulnerabilitySummaryInput struct { Team string `json:"team"` @@ -34689,14 +35595,6 @@ func (v *__GrantAccessToKafkaTopicInput) GetEnvironmentName() string { return v. // GetGrant returns __GrantAccessToKafkaTopicInput.Grant, and is useful for accessing the field via an interface. func (v *__GrantAccessToKafkaTopicInput) GetGrant() KafkaTopicGrantInput { return v.Grant } -// __GrantPostgresAccessInput is used internally by genqlient -type __GrantPostgresAccessInput struct { - Input GrantPostgresAccessInput `json:"input"` -} - -// GetInput returns __GrantPostgresAccessInput.Input, and is useful for accessing the field via an interface. -func (v *__GrantPostgresAccessInput) GetInput() GrantPostgresAccessInput { return v.Input } - // __ListCVEsInput is used internally by genqlient type __ListCVEsInput struct { Team string `json:"team"` @@ -35428,6 +36326,40 @@ func CreateOpenSearchCredentials( return data_, err_ } +// The mutation executed by CreatePostgresAccessAlpha. +const CreatePostgresAccessAlpha_Operation = ` +mutation CreatePostgresAccessAlpha ($input: CreatePostgresAccessInput!) { + createPostgresAccess(input: $input) { + name + } +} +` + +func CreatePostgresAccessAlpha( + ctx_ context.Context, + client_ graphql.Client, + input CreatePostgresAccessInput, +) (data_ *CreatePostgresAccessAlphaResponse, err_ error) { + req_ := &graphql.Request{ + OpName: "CreatePostgresAccessAlpha", + Query: CreatePostgresAccessAlpha_Operation, + Variables: &__CreatePostgresAccessAlphaInput{ + Input: input, + }, + } + + data_ = &CreatePostgresAccessAlphaResponse{} + resp_ := &graphql.Response{Data: data_} + + err_ = client_.MakeRequest( + ctx_, + req_, + resp_, + ) + + return data_, err_ +} + // The mutation executed by CreateSecret. const CreateSecret_Operation = ` mutation CreateSecret ($name: String!, $environment: String!, $team: Slug!) { @@ -35910,6 +36842,50 @@ func FindWorkloadsForCve( return data_, err_ } +// The query executed by GetActivePostgresBranchAlpha. +const GetActivePostgresBranchAlpha_Operation = ` +query GetActivePostgresBranchAlpha ($team: Slug!, $environment: String!, $postgres: String!) { + team(slug: $team) { + environment(name: $environment) { + postgres(name: $postgres) { + activeBranch { + name + } + } + } + } +} +` + +func GetActivePostgresBranchAlpha( + ctx_ context.Context, + client_ graphql.Client, + team string, + environment string, + postgres string, +) (data_ *GetActivePostgresBranchAlphaResponse, err_ error) { + req_ := &graphql.Request{ + OpName: "GetActivePostgresBranchAlpha", + Query: GetActivePostgresBranchAlpha_Operation, + Variables: &__GetActivePostgresBranchAlphaInput{ + Team: team, + Environment: environment, + Postgres: postgres, + }, + } + + data_ = &GetActivePostgresBranchAlphaResponse{} + resp_ := &graphql.Response{Data: data_} + + err_ = client_.MakeRequest( + ctx_, + req_, + resp_, + ) + + return data_, err_ +} + // The query executed by GetAllConfigs. const GetAllConfigs_Operation = ` query GetAllConfigs ($teamSlug: Slug!, $filter: ConfigFilter) { @@ -37323,6 +38299,58 @@ func GetOpenSearch( return data_, err_ } +// The query executed by GetPostgresAccessAlpha. +const GetPostgresAccessAlpha_Operation = ` +query GetPostgresAccessAlpha ($team: Slug!, $environment: String!, $name: String!) { + team(slug: $team) { + environment(name: $environment) { + postgresAccess(name: $name) { + state + message + connection { + username + password + caCertificate + serverName + relayEndpoint + relayAccess + relayToken + } + } + } + } +} +` + +func GetPostgresAccessAlpha( + ctx_ context.Context, + client_ graphql.Client, + team string, + environment string, + name string, +) (data_ *GetPostgresAccessAlphaResponse, err_ error) { + req_ := &graphql.Request{ + OpName: "GetPostgresAccessAlpha", + Query: GetPostgresAccessAlpha_Operation, + Variables: &__GetPostgresAccessAlphaInput{ + Team: team, + Environment: environment, + Name: name, + }, + } + + data_ = &GetPostgresAccessAlphaResponse{} + resp_ := &graphql.Response{Data: data_} + + err_ = client_.MakeRequest( + ctx_, + req_, + resp_, + ) + + return data_, err_ +} + // The query executed by GetSecret. const GetSecret_Operation = ` query GetSecret ($name: String!, $environmentName: String!, $teamSlug: Slug!) { @@ -37553,6 +38581,57 @@ func GetTeamApplications( return data_, err_ } +// The query executed by GetTeamCloudSQLInstances. +const GetTeamCloudSQLInstances_Operation = ` +query GetTeamCloudSQLInstances ($team: Slug!, $sqlFilter: SqlInstanceFilter) { + team(slug: $team) { + sqlInstances(first: 1000, filter: $sqlFilter) { + nodes { + name + teamEnvironment { + environment { + name + } + } + version + highAvailability + auditLog { + logUrl + } + state + } + } + } +} +` + +func GetTeamCloudSQLInstances( + ctx_ context.Context, + client_ graphql.Client, + team string, + sqlFilter *SqlInstanceFilter, +) (data_ *GetTeamCloudSQLInstancesResponse, err_ error) { + req_ := &graphql.Request{ + OpName: "GetTeamCloudSQLInstances", + Query: GetTeamCloudSQLInstances_Operation, + Variables: &__GetTeamCloudSQLInstancesInput{ + Team: team, + SqlFilter: sqlFilter, + }, + } + + data_ = &GetTeamCloudSQLInstancesResponse{} + resp_ := &graphql.Response{Data: data_} + + err_ = client_.MakeRequest( + ctx_, + req_, + resp_, + ) + + return data_, err_ +} + // The query executed by GetTeamJobs. const GetTeamJobs_Operation = ` query GetTeamJobs ($team: Slug!, $orderBy: JobOrder, $filter: TeamJobsFilter) { @@ -37708,27 +38787,11 @@ func GetTeamKafkaTopics( return data_, err_ } -// The query executed by GetTeamPostgresInstances. -const GetTeamPostgresInstances_Operation = ` -query GetTeamPostgresInstances ($team: Slug!, $postgresFilter: PostgresInstanceFilter, $sqlFilter: SqlInstanceFilter) { +// The query executed by GetTeamPostgresBranchesAlpha. +const GetTeamPostgresBranchesAlpha_Operation = ` +query GetTeamPostgresBranchesAlpha ($team: Slug!, $postgresFilter: PostgresBranchFilter) { team(slug: $team) { - postgresInstances(first: 1000, filter: $postgresFilter) { - nodes { - name - teamEnvironment { - environment { - name - } - } - majorVersion - highAvailability - audit { - enabled - } - state - } - } - sqlInstances(first: 1000, filter: $sqlFilter) { + postgresBranches(first: 1000, filter: $postgresFilter) { nodes { name teamEnvironment { @@ -37736,10 +38799,10 @@ query GetTeamPostgresInstances ($team: Slug!, $postgresFilter: PostgresInstanceF name } } - version - highAvailability - auditLog { - logUrl + postgres { + name + majorVersion + highAvailability } state } @@ -37748,24 +38811,22 @@ query GetTeamPostgresInstances ($team: Slug!, $postgresFilter: PostgresInstanceF } ` -func GetTeamPostgresInstances( +func GetTeamPostgresBranchesAlpha( ctx_ context.Context, client_ graphql.Client, team string, - postgresFilter *PostgresInstanceFilter, - sqlFilter *SqlInstanceFilter, -) (data_ *GetTeamPostgresInstancesResponse, err_ error) { + postgresFilter *PostgresBranchFilter, +) (data_ *GetTeamPostgresBranchesAlphaResponse, err_ error) { req_ := &graphql.Request{ - OpName: "GetTeamPostgresInstances", - Query: GetTeamPostgresInstances_Operation, - Variables: &__GetTeamPostgresInstancesInput{ + OpName: "GetTeamPostgresBranchesAlpha", + Query: GetTeamPostgresBranchesAlpha_Operation, + Variables: &__GetTeamPostgresBranchesAlphaInput{ Team: team, PostgresFilter: postgresFilter, - SqlFilter: sqlFilter, }, } - data_ = &GetTeamPostgresInstancesResponse{} + data_ = &GetTeamPostgresBranchesAlphaResponse{} resp_ := &graphql.Response{Data: data_} err_ = client_.MakeRequest( @@ -37927,40 +38988,6 @@ func GrantAccessToKafkaTopic( return data_, err_ } -// The mutation executed by GrantPostgresAccess. -const GrantPostgresAccess_Operation = ` -mutation GrantPostgresAccess ($input: GrantPostgresAccessInput!) { - grantPostgresAccess(input: $input) { - error - } -} -` - -func GrantPostgresAccess( - ctx_ context.Context, - client_ graphql.Client, - input GrantPostgresAccessInput, -) (data_ *GrantPostgresAccessResponse, err_ error) { - req_ := &graphql.Request{ - OpName: "GrantPostgresAccess", - Query: GrantPostgresAccess_Operation, - Variables: &__GrantPostgresAccessInput{ - Input: input, - }, - } - - data_ = &GrantPostgresAccessResponse{} - resp_ := &graphql.Response{Data: data_} - - err_ = client_.MakeRequest( - ctx_, - req_, - resp_, - ) - - return data_, err_ -} - // The query executed by IsAdmin. const IsAdmin_Operation = ` query IsAdmin { diff --git a/internal/postgres/command/grant.go b/internal/postgres/command/grant.go deleted file mode 100644 index 8cf39f73..00000000 --- a/internal/postgres/command/grant.go +++ /dev/null @@ -1,27 +0,0 @@ -package command - -import ( - "context" - - "github.com/nais/cli/internal/postgres" - "github.com/nais/cli/internal/postgres/command/flag" - "github.com/nais/cli/internal/validation" - "github.com/nais/naistrix" -) - -func grantCommand(parentFlags *flag.Postgres) *naistrix.Command { - flags := &flag.Grant{Postgres: parentFlags} - return &naistrix.Command{ - Name: "grant", - Title: "Grant yourself access to a SQL instance database.", - Description: "This is done by temporarily adding your user to the list of users that can administrate Cloud SQL instances and creating a user with your email.", - Args: []naistrix.Argument{ - {Name: "app_name"}, - }, - Flags: flags, - ValidateFunc: validation.RequireTeamAndEnvironment(flags), - RunFunc: func(ctx context.Context, args *naistrix.Arguments, out *naistrix.OutputWriter) error { - return postgres.GrantAndCreateSQLUser(ctx, args.Get("app_name"), flags.Team, string(flags.Environment), out) - }, - } -} diff --git a/internal/postgres/command/postgres.go b/internal/postgres/command/postgres.go deleted file mode 100644 index 06e0bb96..00000000 --- a/internal/postgres/command/postgres.go +++ /dev/null @@ -1,41 +0,0 @@ -package command - -import ( - "context" - - "github.com/nais/cli/internal/flags" - "github.com/nais/cli/internal/gcloud" - "github.com/nais/cli/internal/postgres/command/flag" - "github.com/nais/naistrix" -) - -func Postgres(parentFlags *flags.GlobalFlags) *naistrix.Command { - flags := &flag.Postgres{ - GlobalFlags: parentFlags, - } - - return &naistrix.Command{ - Name: "postgres", - Title: "Manage postgres instances.", - Description: "Commands for managing Google Cloud SQL Postgres instances, including listing, migration, user management, password rotation, and direct database access.", - Aliases: []string{"pg"}, - StickyFlags: flags, - SubCommands: []*naistrix.Command{ - listCommand(flags), - migrateCommand(flags), - passwordCommand(flags), - usersCommand(flags), - enableAuditCommand(flags), - verifyAuditCommand(flags), - grantCommand(flags), - prepareCommand(flags), - proxyCommand(flags), - psqlCommand(flags), - revokeCommand(flags), - }, - ValidateFunc: func(ctx context.Context, _ *naistrix.Arguments) error { - _, err := gcloud.ValidateAndGetUserLogin(ctx, false) - return err - }, - } -} diff --git a/internal/postgres/dbinfo.go b/internal/postgres/dbinfo.go deleted file mode 100644 index 2829a64e..00000000 --- a/internal/postgres/dbinfo.go +++ /dev/null @@ -1,160 +0,0 @@ -package postgres - -import ( - "context" - "errors" - "fmt" - "net/url" - - "github.com/nais/naistrix" - "golang.org/x/oauth2" - meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/client-go/dynamic" - "k8s.io/client-go/kubernetes" - "k8s.io/client-go/tools/clientcmd" -) - -type DB interface { - DBConnection(ctx context.Context) (*ConnectionInfo, error) - RunProxy(ctx context.Context, host string, port *uint, portCh chan<- int, out *naistrix.OutputWriter, printInstructions bool) error - - AppName() string - SetSecretValues(sv *SecretValues) - - // TODO: Remove when interface migration complete - ToCloudSQLDBInfo() (*CloudSQLDBInfo, error) -} - -type DBInfo struct { - k8sClient kubernetes.Interface - dynamicClient dynamic.Interface - config clientcmd.ClientConfig - namespace string - appName string -} - -func (d *DBInfo) AppName() string { - return d.appName -} - -func NewDBInfo(ctx context.Context, appName, team, environment string) (DB, error) { - loadingRules := clientcmd.NewDefaultClientConfigLoadingRules() - configOverrides := &clientcmd.ConfigOverrides{ - CurrentContext: environment, - } - kubeConfig := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loadingRules, configOverrides) - config, err := kubeConfig.ClientConfig() - if err != nil { - return nil, fmt.Errorf("NewDBInfo: unable to get kubeconfig: %w", err) - } - - if team == "" { - ns, _, err := kubeConfig.Namespace() - if err != nil { - return nil, fmt.Errorf("NewDBInfo: unable to get namespace: %w", err) - } - team = ns - } - - k8sClient, err := kubernetes.NewForConfig(config) - if err != nil { - return nil, fmt.Errorf("NewDBInfo: load kubeclient configuration: %w", err) - } - - dynamicClient, err := dynamic.NewForConfig(config) - if err != nil { - return nil, fmt.Errorf("NewDBInfo: load kubeclient configuration: %w", err) - } - - dbInfo := &DBInfo{ - k8sClient: k8sClient, - dynamicClient: dynamicClient, - config: kubeConfig, - namespace: team, - appName: appName, - } - - isCloudSQL, err := IsCloudSQL(ctx, dbInfo) - if err != nil { - return nil, err - } - if isCloudSQL { - return &CloudSQLDBInfo{ - DBInfo: dbInfo, - }, nil - } else { - return NewPostgresDBInfo(ctx, dbInfo) - } -} - -func IsCloudSQL(ctx context.Context, i *DBInfo) (bool, error) { - sqlInstances, err := i.dynamicClient.Resource(schema.GroupVersionResource{ - Group: "sql.cnrm.cloud.google.com", - Version: "v1beta1", - Resource: "sqlinstances", - }).Namespace(string(i.namespace)).List(ctx, meta_v1.ListOptions{ - LabelSelector: "app=" + i.appName, - }) - if err != nil { - return false, fmt.Errorf("fetchDBInstance: error looking for sqlinstance %q in %q: %w", i.appName, i.namespace, err) - } - - if len(sqlInstances.Items) == 1 { - return true, nil - } else if len(sqlInstances.Items) > 1 { - return true, fmt.Errorf("fetchDBInstance: multiple sqlinstances found for app %q in %q", i.appName, i.namespace) - } - - return false, nil -} - -type ConnectionInfo struct { - username string - email string - password string - dbName string - instance string - port string - url *url.URL - jdbcUrl *url.URL -} - -func (c *ConnectionInfo) ProxyConnectionString() string { - return fmt.Sprintf("host=%v user=%v dbname=%v password=%v sslmode=disable", c.instance, c.username, c.dbName, c.password) -} - -func (c *ConnectionInfo) SetPassword(password string) { - c.password = password - if c.url != nil { - c.url.User = url.UserPassword(c.username, password) - } - if c.jdbcUrl != nil { - queries := c.jdbcUrl.Query() - queries.Set("password", password) - c.jdbcUrl.RawQuery = queries.Encode() - } else if c.url != nil { - queries := c.url.Query() - queries.Set("password", password) - queries.Set("user", c.username) - c.jdbcUrl = &url.URL{ - Scheme: "jdbc:postgresql", - Host: c.url.Host, - Path: c.dbName, - RawQuery: queries.Encode(), - } - } -} - -// formatInvalidGrantError returns a custom error message if the error is of type oauth2.RetrieveError and if it has the -// error code invalid_grant. If not it returns the error. -func formatInvalidGrantError(err error) error { - var retrieve *oauth2.RetrieveError - if errors.As(err, &retrieve) { - if retrieve.ErrorCode == "invalid_grant" { - return fmt.Errorf("looks like you are missing Application Default Credentials, run `gcloud auth login --update-adc` first") - } - } - - return err -} diff --git a/internal/postgres/list.go b/internal/postgres/list.go deleted file mode 100644 index 1322e7f5..00000000 --- a/internal/postgres/list.go +++ /dev/null @@ -1,170 +0,0 @@ -package postgres - -import ( - "context" - "fmt" - "slices" - "sort" - - "github.com/nais/cli/internal/naisapi" - "github.com/nais/cli/internal/naisapi/gql" - "github.com/nais/naistrix/output" - "k8s.io/utils/ptr" -) - -const consoleBaseURL = "https://console.nav.cloud.nais.io" - -type Instance struct { - Name output.Link `json:"name"` - Type string `json:"type"` - Environment string `json:"environment"` - Version string `heading:"Version" json:"version"` - HighAvailability bool `heading:"HA" json:"high_availability"` - Audit bool `json:"audit"` - State State `json:"state"` -} - -type State string - -func (s State) String() string { - // PostgresInstance states - switch s { - case State(gql.PostgresInstanceStateAvailable): - return "Available" - case State(gql.PostgresInstanceStateProgressing): - return "Progressing" - case State(gql.PostgresInstanceStateDegraded): - return "Degraded" - } - - // SqlInstance states - switch s { - case State(gql.SqlInstanceStateRunnable): - return "Runnable" - case State(gql.SqlInstanceStateStopped): - return "Stopped" - case State(gql.SqlInstanceStateSuspended): - return "Suspended" - case State(gql.SqlInstanceStatePendingCreate): - return "Pending Create" - case State(gql.SqlInstanceStatePendingDelete): - return "Pending Delete" - case State(gql.SqlInstanceStateMaintenance): - return "Maintenance" - case State(gql.SqlInstanceStateFailed): - return "Failed" - } - - return "Unknown" -} - -func GetTeamPostgresInstances(ctx context.Context, team string, environments []string, labelFilters []gql.LabelFilter) ([]Instance, error) { - _ = `# @genqlient - query GetTeamPostgresInstances($team: Slug!, $postgresFilter: PostgresInstanceFilter, $sqlFilter: SqlInstanceFilter) { - team(slug: $team) { - postgresInstances(first: 1000, filter: $postgresFilter) { - nodes { - name - teamEnvironment { - environment { - name - } - } - majorVersion - highAvailability - audit { - enabled - } - state - } - } - sqlInstances(first: 1000, filter: $sqlFilter) { - nodes { - name - teamEnvironment { - environment { - name - } - } - version - highAvailability - # @genqlient(pointer: true) - auditLog { - logUrl - } - state - } - } - } - } - ` - - client, err := naisapi.GraphqlClient(ctx) - if err != nil { - return nil, err - } - - postgresFilter := gql.PostgresInstanceFilter{ - Environments: environments, - Labels: labelFilters, - } - sqlFilter := gql.SqlInstanceFilter{ - Labels: labelFilters, - } - - resp, err := gql.GetTeamPostgresInstances(ctx, client, team, new(postgresFilter), new(sqlFilter)) - if err != nil { - return nil, err - } - - var ret []Instance - - for _, p := range resp.Team.PostgresInstances.Nodes { - env := p.TeamEnvironment.Environment.Name - if len(environments) > 0 && !slices.Contains(environments, env) { - continue - } - - ret = append(ret, Instance{ - Name: output.Link{ - Name: p.Name, - URL: fmt.Sprintf("%s/team/%s/%s/postgres/%s", consoleBaseURL, team, env, p.Name), - }, - Type: "PostgreSQL", - Environment: env, - Version: p.MajorVersion, - HighAvailability: p.HighAvailability, - Audit: p.Audit.Enabled, - State: State(p.State), - }) - } - - for _, s := range resp.Team.SqlInstances.Nodes { - env := s.TeamEnvironment.Environment.Name - if len(environments) > 0 && !slices.Contains(environments, env) { - continue - } - - ret = append(ret, Instance{ - Name: output.Link{ - Name: s.Name, - URL: fmt.Sprintf("%s/team/%s/%s/cloudsql/%s", consoleBaseURL, team, env, s.Name), - }, - Type: "Cloud SQL", - Environment: env, - Version: ptr.Deref(s.Version, ""), - HighAvailability: s.HighAvailability, - Audit: s.AuditLog != nil, - State: State(s.State), - }) - } - - sort.Slice(ret, func(i, j int) bool { - if ret[i].Name.Name == ret[j].Name.Name { - return ret[i].Environment < ret[j].Environment - } - return ret[i].Name.Name < ret[j].Name.Name - }) - - return ret, nil -} diff --git a/internal/postgres/postgresinfo.go b/internal/postgres/postgresinfo.go deleted file mode 100644 index add98d15..00000000 --- a/internal/postgres/postgresinfo.go +++ /dev/null @@ -1,259 +0,0 @@ -package postgres - -import ( - "context" - "fmt" - "io" - "net/http" - "net/url" - - "github.com/nais/cli/internal/naisapi" - nais_io_v1alpha1 "github.com/nais/liberator/pkg/apis/nais.io/v1alpha1" - "github.com/nais/naistrix" - "github.com/pkg/errors" - apierrors "k8s.io/apimachinery/pkg/api/errors" - meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/client-go/tools/portforward" - "k8s.io/client-go/transport/spdy" -) - -type postgresDBInfo struct { - *DBInfo - clusterName string -} - -func NewPostgresDBInfo(ctx context.Context, dbInfo *DBInfo) (DB, error) { - p := &postgresDBInfo{ - DBInfo: dbInfo, - } - err := p.fetchClusterInfo(ctx) - if err != nil { - return nil, err - } - return p, nil -} - -func (p *postgresDBInfo) DBConnection(ctx context.Context) (*ConnectionInfo, error) { - user, err := naisapi.GetAuthenticatedUser(ctx) - if err != nil { - return nil, err - } - token, err := user.AccessToken() - if err != nil { - return nil, err - } - - email := user.Email() - - queries := url.Values{} - queries.Add("sslmode", "required") - pgUrl := &url.URL{ - Scheme: "postgresql", - User: url.UserPassword(email, token), - Host: "localhost", - Path: "app", - RawQuery: queries.Encode(), - } - - queries.Add("user", email) - queries.Add("password", token) - jdbcUrl := &url.URL{ - Scheme: "jdbc:postgresql", - Host: "localhost:5432", - Path: "app", - RawQuery: queries.Encode(), - } - - return &ConnectionInfo{ - username: email, - email: email, - password: token, - dbName: "app", - instance: "localhost", - port: "5432", - url: pgUrl, - jdbcUrl: jdbcUrl, - }, nil -} - -func (p *postgresDBInfo) RunProxy(ctx context.Context, host string, port *uint, portCh chan<- int, out *naistrix.OutputWriter, printInstructions bool) error { - cfg, err := p.config.ClientConfig() - if err != nil { - return err - } - - pods, err := p.k8sClient.CoreV1().Pods(fmt.Sprintf("pg-%s", p.namespace)).List(ctx, meta_v1.ListOptions{ - LabelSelector: fmt.Sprintf("spilo-role=master,application=spilo,cluster-name=%s", p.clusterName), - }) - if err != nil { - return err - } - if len(pods.Items) != 1 { - return fmt.Errorf("found %d pods marked as master for cluster %s", len(pods.Items), p.clusterName) - } - - user, err := naisapi.GetAuthenticatedUser(ctx) - if err != nil { - return err - } - email := user.Email() - - masterPod := pods.Items[0] - pfUrl := p.k8sClient.CoreV1().RESTClient().Post(). - Resource("pods"). - Namespace(masterPod.GetNamespace()). - Name(masterPod.GetName()). - SubResource("portforward"). - URL() - - out.Verbosef("attempting port forward with URL: %s\n", pfUrl.String()) - transport, upgrader, err := spdy.RoundTripperFor(cfg) - if err != nil { - return errors.Wrap(err, "Could not create round tripper") - } - - dialer := spdy.NewDialer(upgrader, &http.Client{Transport: transport}, "POST", pfUrl) - - stopChan := make(chan struct{}, 1) - readyChan := make(chan struct{}, 1) - errChan := make(chan error, 1) - - ports := []string{":5432"} - if port != nil { - ports = []string{fmt.Sprintf("%d:5432", *port)} - } - - out.Verbosef("Creating new portforward on %s for ports %v\n", host, ports) - pf, err := portforward.NewOnAddresses(dialer, []string{host}, ports, stopChan, readyChan, NewNaisOut(out), NewNaisErr(out)) - if err != nil { - return err - } - - go func() { - out.Verbosef("forwarding ports ...\n") - errChan <- pf.ForwardPorts() - }() - - out.Verbosef("Waiting for forwarding to be ready ...\n") - select { - case err = <-errChan: - return errors.Wrap(err, "Could not create port forward") - case <-readyChan: - } - - if printInstructions { - connectionInfo, err := p.DBConnection(ctx) - if err != nil { - return err - } - - out.Printf("Starting proxy on %s:%d\n", host, *port) - out.Println() - out.Println("Before you can connect, you need to request an access token:") - out.Println("nais login --nais") - out.Println("After logging in, you can get the current password using this command:") - out.Println("nais auth print-access-token --nais") - out.Println() - out.Println("To connect to the database using psql, use the following command:") - out.Printf("PGPASSWORD=$(nais auth print-access-token --nais) psql -h %v -p %v -U %v %v\n", host, *port, email, connectionInfo.dbName) - out.Println() - out.Println("If you are using a JDBC client, you can connect to the database by using the following connection string:") - out.Printf("Connection URL: %s\n", connectionInfo.jdbcUrl) - } - - forwardedPorts, err := pf.GetPorts() - if err != nil { - return err - } - for _, forwardedPort := range forwardedPorts { - out.Infof("Listening on %s:%d\n", host, forwardedPort.Local) - portCh <- int(forwardedPort.Local) - } - - select { - case <-ctx.Done(): - return ctx.Err() - case err = <-errChan: - return errors.Wrap(err, "Could not create port forward") - } -} - -func (p *postgresDBInfo) ToCloudSQLDBInfo() (*CloudSQLDBInfo, error) { - return nil, fmt.Errorf("not a CloudSQL instance") -} - -func (p *postgresDBInfo) SetSecretValues(_ *SecretValues) { - // No-op for in-cluster postgres; authentication uses OAuth tokens -} - -func (p *postgresDBInfo) fetchClusterInfo(ctx context.Context) error { - unstructuredApp, err := p.dynamicClient.Resource(schema.GroupVersionResource{ - Group: "nais.io", - Version: "v1alpha1", - Resource: "applications", - }).Namespace(string(p.namespace)).Get(ctx, p.appName, meta_v1.GetOptions{}) - if err != nil { - if apierrors.IsNotFound(err) { - return p.fetchClusterInfoFromCluster(ctx) - } - return fmt.Errorf("fetchClusterInfo: error looking for Application %q in %q: %w", p.appName, p.namespace, err) - } - - app := &nais_io_v1alpha1.Application{} - err = runtime.DefaultUnstructuredConverter.FromUnstructured(unstructuredApp.Object, app) - if err != nil { - return fmt.Errorf("fetchClusterInfo: error converting to Application %q in %q: %w", p.appName, p.namespace, err) - } - - if app.Spec.Postgres == nil { - return fmt.Errorf("fetchClusterInfo: application %q in %q does not have a Postgres cluster", p.appName, p.namespace) - } - - p.clusterName = app.Spec.Postgres.ClusterName - - return nil -} - -// fetchClusterInfoFromCluster assumes the given "appname" is in reality the name of a postgres cluster directly -// Attempts to verify that this is the case by looking for such a cluster and using it if found -func (p *postgresDBInfo) fetchClusterInfoFromCluster(ctx context.Context) error { - _, err := p.dynamicClient.Resource(schema.GroupVersionResource{ - Group: "data.nais.io", - Version: "v1", - Resource: "postgres", - }).Namespace(string(p.namespace)).Get(ctx, p.appName, meta_v1.GetOptions{}) - if err != nil { - if apierrors.IsNotFound(err) { - return fmt.Errorf("unable to find either Application or Postgres cluster named %q in %q: %w", p.appName, p.namespace, err) - } - return fmt.Errorf("fetchClusterInfo: error looking for Postgres %q in %q: %w", p.appName, p.namespace, err) - } - - p.clusterName = p.appName - - return nil -} - -func NewNaisOut(out *naistrix.OutputWriter) io.Writer { - return &NaisWriter{ - writeFunc: func(format string, v ...any) { out.Infof(format, v...) }, - } -} - -func NewNaisErr(out *naistrix.OutputWriter) io.Writer { - return &NaisWriter{ - writeFunc: func(format string, v ...any) { out.Errorf(format, v...) }, - } -} - -type NaisWriter struct { - writeFunc func(string, ...any) -} - -func (o *NaisWriter) Write(p []byte) (n int, err error) { - msg := string(p) - o.writeFunc(msg) - return len(msg), nil -} diff --git a/internal/postgres/secret.go b/internal/postgres/secret.go deleted file mode 100644 index 3e086c7c..00000000 --- a/internal/postgres/secret.go +++ /dev/null @@ -1,288 +0,0 @@ -package postgres - -import ( - "context" - "fmt" - "strings" - - "github.com/nais/cli/internal/naisapi" - "github.com/nais/cli/internal/naisapi/gql" - "github.com/nais/cli/internal/postgres/command/flag" - "github.com/nais/naistrix" - v1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/client-go/dynamic" - "k8s.io/client-go/tools/clientcmd" - "k8s.io/utils/ptr" -) - -// Hardcoded reasons for administrative operations -const ( - ReasonPasswordRotate = "Rotating database password via nais CLI" - ReasonPrepareAccess = "Preparing database for IAM user access via nais CLI" - ReasonRevokeAccess = "Revoking IAM user access from database via nais CLI" - ReasonListUsers = "Listing database users via nais CLI" - ReasonAddUser = "Adding database user via nais CLI" - ReasonDropUser = "Dropping database user via nais CLI" - ReasonEnableAudit = "Enabling audit logging via nais CLI" - ReasonVerifyAudit = "Verifying audit configuration via nais CLI" -) - -// Default duration for in-cluster postgres access grants -const defaultPostgresAccessDuration = "1h" - -// SecretValues holds the secret values retrieved from the API -type SecretValues struct { - values map[string]string -} - -// Get returns the value for a key with the given suffix (e.g. "_PASSWORD", "_USERNAME"). -// Keys are matched by suffix to handle prefixed key names like "NAIS_DATABASE_MYAPP_PASSWORD". -func (s *SecretValues) Get(suffix string) string { - for name, val := range s.values { - if strings.HasSuffix(name, suffix) { - return val - } - } - return "" -} - -// GetSecretValues retrieves the values of a database secret via the API. -// For CloudSQL databases, this retrieves the secret values directly. -// For in-cluster postgres, this grants temporary access to the database. -// In both cases, the access is logged for audit purposes. -func GetSecretValues(ctx context.Context, appName, team, environment string, fl *flag.Postgres, reason string, out *naistrix.OutputWriter) (*SecretValues, error) { - if reason == "" { - reason = fl.Reason - if reason == "" { - return nil, fmt.Errorf("reason is required for accessing database secrets") - } - } - - out.Printf("Using team %q\n", team) - - // Check if this is a CloudSQL or in-cluster postgres database - isCloudSQL, err := isCloudSQLDatabase(ctx, appName, fl) - if err != nil { - return nil, fmt.Errorf("checking database type: %w", err) - } - - if isCloudSQL { - return getCloudSQLSecretValues(ctx, appName, team, environment, reason, out) - } - - return grantInClusterPostgresAccess(ctx, appName, fl, team, environment, reason, out) -} - -// GetSecretValuesWithUserReason retrieves secret values with a user-provided reason. -// This should be used for interactive operations like proxy and psql where the user -// should provide justification for accessing the database. -func GetSecretValuesWithUserReason(ctx context.Context, appName, team, environment string, fl *flag.Postgres, reason string, out *naistrix.OutputWriter) (*SecretValues, error) { - if reason == "" { - reason = fl.Reason - if reason == "" { - return nil, fmt.Errorf("reason is required for accessing database secrets (use --reason flag)") - } - } - - if len(reason) < 10 { - return nil, fmt.Errorf("reason must be at least 10 characters") - } - - return GetSecretValues(ctx, appName, team, environment, fl, reason, out) -} - -// isCloudSQLDatabase checks if the given app uses CloudSQL or in-cluster postgres -func isCloudSQLDatabase(ctx context.Context, appName string, fl *flag.Postgres) (bool, error) { - loadingRules := clientcmd.NewDefaultClientConfigLoadingRules() - // Use Context if set, otherwise fall back to Environment (they often map to the same thing) - kubeContext := string(fl.Environment) - if kubeContext == "" { - kubeContext = string(fl.Environment) - } - configOverrides := &clientcmd.ConfigOverrides{ - CurrentContext: kubeContext, - } - kubeConfig := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loadingRules, configOverrides) - - ns := fl.Team - - config, err := kubeConfig.ClientConfig() - if err != nil { - return false, fmt.Errorf("unable to get kubeconfig: %w", err) - } - - dynamicClient, err := dynamic.NewForConfig(config) - if err != nil { - return false, fmt.Errorf("unable to create dynamic client: %w", err) - } - - // Check for CloudSQL SQLInstance resources - sqlInstances, err := dynamicClient.Resource(schema.GroupVersionResource{ - Group: "sql.cnrm.cloud.google.com", - Version: "v1beta1", - Resource: "sqlinstances", - }).Namespace(ns).List(ctx, v1.ListOptions{ - LabelSelector: "app=" + appName, - }) - if err != nil { - return false, fmt.Errorf("error looking for sqlinstance for application %q in %q: %w", appName, ns, err) - } - - return len(sqlInstances.Items) >= 1, nil -} - -// getCloudSQLSecretValues retrieves secret values for CloudSQL databases -func getCloudSQLSecretValues(ctx context.Context, appName, team, environment, reason string, out *naistrix.OutputWriter) (*SecretValues, error) { - // The secret name follows the pattern "google-sql-" - secretName := "google-sql-" + appName - - out.Debugf("Requesting access to CloudSQL secret %q...\n", secretName) - - values, err := naisapi.ViewSecretValues(ctx, team, environment, secretName, reason) - if err != nil { - // Check if the error indicates the user is not authorized - if strings.Contains(err.Error(), "not authorized") || strings.Contains(err.Error(), "Not authorized") { - return nil, fmt.Errorf("you are not authorized to access this database. Make sure you are a member of team %q", team) - } - return nil, err - } - - out.Debugf("✅ Access granted.\n") - - // Convert to SecretValues - result := &SecretValues{ - values: make(map[string]string, len(values)), - } - for _, v := range values { - result.values[v.Name] = v.Value - } - - return result, nil -} - -// getPostgresClusterName retrieves the postgres cluster name for an app -func getPostgresClusterName(ctx context.Context, appName string, fl *flag.Postgres) (string, error) { - loadingRules := clientcmd.NewDefaultClientConfigLoadingRules() - // Use Context if set, otherwise fall back to Environment (they often map to the same thing) - kubeContext := string(fl.Environment) - if kubeContext == "" { - kubeContext = string(fl.Environment) - } - configOverrides := &clientcmd.ConfigOverrides{ - CurrentContext: kubeContext, - } - kubeConfig := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loadingRules, configOverrides) - - ns := fl.Team - - config, err := kubeConfig.ClientConfig() - if err != nil { - return "", fmt.Errorf("unable to get kubeconfig: %w", err) - } - - dynamicClient, err := dynamic.NewForConfig(config) - if err != nil { - return "", fmt.Errorf("unable to create dynamic client: %w", err) - } - - // First try to get the cluster name from the Application spec - unstructuredApp, err := dynamicClient.Resource(schema.GroupVersionResource{ - Group: "nais.io", - Version: "v1alpha1", - Resource: "applications", - }).Namespace(ns).Get(ctx, appName, v1.GetOptions{}) - if err == nil { - spec, ok := unstructuredApp.Object["spec"].(map[string]any) - if ok { - postgres, ok := spec["postgres"].(map[string]any) - if ok { - clusterName, ok := postgres["clusterName"].(string) - if ok && clusterName != "" { - return clusterName, nil - } - } - } - } - - // If no Application found or no clusterName in spec, check if there's a Postgres resource with this name - _, err = dynamicClient.Resource(schema.GroupVersionResource{ - Group: "data.nais.io", - Version: "v1", - Resource: "postgres", - }).Namespace(ns).Get(ctx, appName, v1.GetOptions{}) - if err == nil { - // The appName is actually a postgres cluster name - return appName, nil - } - - return "", fmt.Errorf("unable to find postgres cluster for application %q in %q", appName, ns) -} - -// grantPostgresAccess grants temporary access to an in-cluster postgres database. -// This creates a time-limited grant for the user and logs the access for auditing purposes. -func grantPostgresAccess(ctx context.Context, clusterName, teamSlug, environmentName, grantee, duration string) error { - _ = `# @genqlient -mutation GrantPostgresAccess($input: GrantPostgresAccessInput!) { - grantPostgresAccess(input: $input) { - error - } -} -` - - client, err := naisapi.GraphqlClient(ctx) - if err != nil { - return fmt.Errorf("creating GraphQL client: %w", err) - } - - resp, err := gql.GrantPostgresAccess(ctx, client, gql.GrantPostgresAccessInput{ - ClusterName: clusterName, - TeamSlug: teamSlug, - EnvironmentName: environmentName, - Grantee: grantee, - Duration: duration, - }) - if err != nil { - return fmt.Errorf("granting postgres access: %w", err) - } - - if ptr.Deref(resp.GrantPostgresAccess.Error, "") != "" { - return fmt.Errorf("granting postgres access: %s", ptr.Deref(resp.GrantPostgresAccess.Error, "")) - } - - return nil -} - -// grantInClusterPostgresAccess grants access to in-cluster postgres databases -func grantInClusterPostgresAccess(ctx context.Context, appName string, fl *flag.Postgres, team, environment, reason string, out *naistrix.OutputWriter) (*SecretValues, error) { - // Get the postgres cluster name - clusterName, err := getPostgresClusterName(ctx, appName, fl) - if err != nil { - return nil, err - } - - // Get the authenticated user's email - user, err := naisapi.GetAuthenticatedUser(ctx) - if err != nil { - return nil, fmt.Errorf("getting authenticated user: %w", err) - } - grantee := user.Email() - - out.Debugf("Requesting access to in-cluster postgres %q for user %q...\n", clusterName, grantee) - - // Grant access via the API (this logs the access for audit purposes) - err = grantPostgresAccess(ctx, clusterName, team, environment, grantee, defaultPostgresAccessDuration) - if err != nil { - // Check if the error indicates the user is not authorized - if strings.Contains(err.Error(), "not authorized") || strings.Contains(err.Error(), "Not authorized") { - return nil, fmt.Errorf("you are not authorized to access this database. Make sure you are a member of team %q", team) - } - return nil, fmt.Errorf("granting postgres access: %w", err) - } - - out.Debugf("✅ Access granted for %s.\n", defaultPostgresAccessDuration) - - // For in-cluster postgres, we don't return secret values as authentication - // happens via OAuth tokens, not via secrets - return &SecretValues{values: make(map[string]string)}, nil -} diff --git a/schema.graphql b/schema.graphql index 017f979f..9c186046 100644 --- a/schema.graphql +++ b/schema.graphql @@ -150,9 +150,11 @@ Filter for credential creation events. A user was granted access to a Postgres cluster """ POSTGRES_GRANT_ACCESS -""" -A Postgres instance was deleted -""" + "A personal Postgres access was created through the API broker" + POSTGRES_PERSONAL_ACCESS_CREATED + "Personal Postgres connection materials were retrieved" + POSTGRES_PERSONAL_ACCESS_CONNECTION + "A Postgres branch was deleted" POSTGRES_DELETED """ Reconciler enabled activity log entry. @@ -983,15 +985,13 @@ Network policies for the application. OpenSearch instance referenced by the workload. """ openSearch: OpenSearch -""" -Postgres instances referenced by the application. This does not currently support pagination, but will return all available Postgres instances. -""" - postgresInstances( +"Active PostgresBranches for all Postgres entries in uses.postgres." + postgresBranches( """ Ordering options for items returned from the connection. """ - orderBy: PostgresInstanceOrder - ): PostgresInstanceConnection! + orderBy: PostgresBranchOrder + ): PostgresBranchConnection! """ Secrets used by the application. """ @@ -2907,17 +2907,56 @@ Whether or not the OpenSearch instance was deleted. openSearchDeleted: Boolean } -input DeletePostgresInput { +"Result of creating a personal Postgres access." +type CreatePostgresAccessPayload { + "Name of the newly created PostgresAccess resource." name: String! + "Server-controlled expiry for this personal access." + expiresAt: Time! +} + +"Input for creating a time-limited personal Postgres access." +input CreatePostgresAccessInput { + "Name of the Postgres containing the branch." + postgres: String! + "Local name of the branch to access." + branch: String! + "Team that owns the Postgres branch." + teamSlug: Slug! + "Environment containing the Postgres branch." environmentName: String! + "Privileges requested for the personal database role." + accessLevel: PostgresAccessLevel! + "Reason for personal database access. Must be at least 10 characters." + reason: String! + "Requested access lifetime (for example '30m' or '1h'). Defaults to '1h' and cannot exceed '1h'." + ttl: String +} + +"Privilege level granted to a personal Postgres database role." +enum PostgresAccessLevel { + "Read data without modifying it." + READ + "Read and modify existing data." + READWRITE + "Read, modify, and create database objects where supported." + READWRITECREATE +} + +input DeletePostgresBranchInput { + "Name of the Postgres containing the branch." + postgres: String! + "Local name of the branch to delete." + branch: String! + "The environment containing the PostgresBranch." + environmentName: String! + "The team that owns the PostgresBranch." teamSlug: Slug! } -type DeletePostgresPayload { -""" -Whether or not the Postgres instance was deleted. -""" - postgresDeleted: Boolean +type DeletePostgresBranchPayload { +"Whether the PostgresBranch was deleted." + postgresBranchDeleted: Boolean } input DeleteSecretInput { @@ -4620,15 +4659,13 @@ Network policies for the job. OpenSearch instance referenced by the workload. """ openSearch: OpenSearch -""" -Postgres instances referenced by the job. This does not currently support pagination, but will return all available Postgres instances. -""" - postgresInstances( +"Active PostgresBranches for all Postgres entries in uses.postgres." + postgresBranches( """ Ordering options for items returned from the connection. """ - orderBy: PostgresInstanceOrder - ): PostgresInstanceConnection! + orderBy: PostgresBranchOrder + ): PostgresBranchConnection! """ Secrets used by the job. """ @@ -5924,12 +5961,16 @@ Grant temporary access to a Postgres cluster. grantPostgresAccess( input: GrantPostgresAccessInput! ): GrantPostgresAccessPayload! -""" -Delete an existing Postgres instance. -""" - deletePostgres( - input: DeletePostgresInput! - ): DeletePostgresPayload! + """ + EXPERIMENTAL: DO NOT USE + Create time-limited personal access to a NAIS Postgres branch through the brokered PostgresAccess and relay flow. + When the access is ready, retrieve its connection materials through PostgresAccess.connection. + """ + createPostgresAccess(input: CreatePostgresAccessInput!): CreatePostgresAccessPayload! +"Delete a PostgresBranch that is not active on its Postgres." + deletePostgresBranch( + input: DeletePostgresBranchInput! + ): DeletePostgresBranchPayload! """ Enable a reconciler @@ -6941,6 +6982,8 @@ ID of the entry. The identity of the actor who performed the action. The value is either the name of a service account, or the email address of a user. """ actor: String! + "GitHub Actions OIDC claims when authenticated by a GitHub repository." + gitHubActorClaims: GitHubActorClaims """ Creation time of the entry. """ @@ -6976,6 +7019,8 @@ ID of the entry. The identity of the actor who performed the action. The value is either the name of a service account, or the email address of a user. """ actor: String! + "GitHub Actions OIDC claims when authenticated by a GitHub repository." + gitHubActorClaims: GitHubActorClaims """ Creation time of the entry. """ @@ -7011,14 +7056,74 @@ type PostgresGrantAccessActivityLogEntryData { until: Time! } -type PostgresInstance implements Persistence & Node{ +"An audit-log entry for personal Postgres access created through the API broker." +type PostgresPersonalAccessCreatedActivityLogEntry implements ActivityLogEntry & Node { + "ID of the entry." + id: ID! + "The identity of the actor who created the personal access." + actor: String! + "GitHub Actions OIDC claims when authenticated by a GitHub repository." + gitHubActorClaims: GitHubActorClaims + "Creation time of the entry." + createdAt: Time! + "Message that summarizes the entry." + message: String! + "Type of the affected resource." + resourceType: ActivityLogEntryResourceType! + "Name of the affected Postgres branch." + resourceName: String! + "The team slug that the entry belongs to." + teamSlug: Slug! + "The environment name that the entry belongs to." + environmentName: String + "Personal-access specific audit data." + data: PostgresPersonalAccessCreatedActivityLogEntryData! +} + +"Personal-access-specific audit data." +type PostgresPersonalAccessCreatedActivityLogEntryData { + "Identity that owns the new personal access." + username: String! + "Requested privilege level; null for events recorded before this field was added." + accessLevel: PostgresAccessLevel + "Server-controlled expiry of the access." + expiresAt: Time! + "Caller-provided audit reason." + reason: String! +} + +"An audit-log entry for retrieval of personal Postgres connection materials." +type PostgresPersonalAccessConnectionActivityLogEntry implements ActivityLogEntry & Node { + "ID of the entry." id: ID! + "Identity that retrieved the connection materials." + actor: String! + "GitHub Actions OIDC claims when authenticated by a GitHub repository." + gitHubActorClaims: GitHubActorClaims + "Creation time of the entry." + createdAt: Time! + "Message that summarizes the entry." + message: String! + "Type of the affected resource." + resourceType: ActivityLogEntryResourceType! + "Name of the affected PostgresAccess resource." + resourceName: String! + "Team slug that the entry belongs to." + teamSlug: Slug! + "Environment name that the entry belongs to." + environmentName: String +} + +"A named PostgresBranch belonging to a Postgres." +type PostgresBranch implements Persistence & Node{ + id: ID! + "Local name of this branch within its Postgres." name: String! team: Team! teamEnvironment: TeamEnvironment! -""" -Workloads that reference the Postgres instance. -""" + "Postgres owning this PostgresBranch." + postgres: Postgres! + "Workloads using this branch while it is active." workloads( """ Get the first n items in the connection. This can be used in combination with the after parameter. @@ -7037,161 +7142,168 @@ Get items before this cursor. """ before: Cursor ): WorkloadConnection! -""" -Resource allocation for the Postgres cluster. -""" - resources: PostgresInstanceResources! -""" -Major version of PostgreSQL. -""" +"Current observed state of the branch." + state: PostgresBranchState! + "User-defined labels on this branch." + labels: [ResourceLabel!]! +} + +"A Postgres whose active branch can change." +type Postgres implements Node { + "Opaque identifier for this Postgres." + id: ID! + "Name of this Postgres." + name: String! + "Configured PostgreSQL major version." majorVersion: String! -""" -Audit logging configuration for the Postgres cluster. -""" - audit: PostgresInstanceAudit! -""" -Indicates whether the Postgres cluster is configured for high availability. -""" + "Whether high availability is configured." highAvailability: Boolean! -""" -Current state of the Postgres cluster. -""" - state: PostgresInstanceState! -""" -Maintenance window for the Postgres cluster, if configured. -""" - maintenanceWindow: PostgresInstanceMaintenanceWindow -""" -User-defined labels attached to this instance. -""" + "Requested CPU, memory and disk size, when present on this Postgres." + resources: PostgresResources! + "Currently active branch, if selected." + activeBranch: PostgresBranch + "Branch with this local name in this Postgres." + branch(name: String!): PostgresBranch! + "Branches belonging to this Postgres." + branches(first: Int, after: Cursor, last: Int, before: Cursor, orderBy: PostgresBranchOrder): PostgresBranchConnection! + "User-defined labels on this Postgres." labels: [ResourceLabel!]! } -type PostgresInstanceAudit { -""" -Indicates whether audit logging is enabled for the Postgres cluster. -""" - enabled: Boolean! -""" -URL for accessing the audit logs. -""" - url: String -""" -List of statement classes that are being logged, such as `ddl`, `dml`, and `read`. -""" - statementClasses: [String!] +"Resource requests configured on Postgres. Omitted requests are null." +type PostgresResources { + "Requested CPU." + cpu: String + "Requested memory." + memory: String + "Requested disk size." + diskSize: String } -type PostgresInstanceConnection { +"A time-limited personal access request for a Postgres branch." +type PostgresAccess implements Node { + "Opaque ID for this PostgresAccess resource." + id: ID! + "Name of the PostgresAccess resource." + name: String! + "Team that owns the access." + team: Team! + "Environment for the access." + teamEnvironment: TeamEnvironment! + "PostgresBranch selected by this access." + postgresBranch: PostgresBranch! + "Requested access level." + accessLevel: PostgresAccessLevel! + "Server-controlled expiry for this personal access." + expiresAt: Time! + "High-level state of the access." + state: PostgresAccessState! + "Human-readable message for the current state." + message: String + "Name of the controller-owned relay mapping, once created. Contains no credential." + relayAccess: String + """ + EXPERIMENTAL: DO NOT USE + Get connection materials for this ready access. Only its owner can read them. + """ + connection: PostgresAccessConnectionDetails +} + +"High-level reconciliation state of a personal Postgres access." +enum PostgresAccessState { + "The controller has not finished provisioning the access." + PENDING + "The access and its connection materials are ready." + READY + "The controller cannot provision the requested access." + FAILED + "The server-controlled expiry time has passed." + EXPIRED +} + +"Sensitive connection materials for a ready personal Postgres access." +type PostgresAccessConnectionDetails { + "Database username for the caller's personal role." + username: String! + "Short-lived password for the caller's database role." + password: String! + "CA certificate required to verify the PostgreSQL server certificate." + caCertificate: String! + "PostgreSQL server name used for TLS verification." + serverName: String! + "Public HTTP/3 relay endpoint." + relayEndpoint: String! + "Relay-Access header value (namespace/name)." + relayAccess: String! + "Owner-only bearer token for this access; never log it." + relayToken: String! +} + +type PostgresBranchConnection { pageInfo: PageInfo! - nodes: [PostgresInstance!]! - edges: [PostgresInstanceEdge!]! + nodes: [PostgresBranch!]! + edges: [PostgresBranchEdge!]! """ -Facets for Postgres instances. Provides distribution counts to help narrow down results. +Facets for Postgres branches. Provides distribution counts to help narrow down results. Facet counts are computed over the full result set (ignoring pagination) but respect the current filter. """ - facets: PostgresInstanceFacets + facets: PostgresBranchFacets } -type PostgresInstanceEdge { +type PostgresBranchEdge { cursor: Cursor! - node: PostgresInstance! + node: PostgresBranch! } """ -Facets for Postgres instances, providing distribution counts across different dimensions. -""" -type PostgresInstanceFacets { -""" -Distribution of instances by environment. +Facets for Postgres branches, providing distribution counts across different dimensions. """ +type PostgresBranchFacets { + "Distribution of branches by environment." environments: [StringFacetItem!]! -""" -Distribution of instances by state. -""" - states: [PostgresInstanceStateFacetItem!]! -""" -Distribution of instances by high availability. -""" - highAvailability: [BooleanFacetItem!]! -""" -Distribution of instances by major version. -""" - majorVersions: [StringFacetItem!]! -""" -Distribution of instances by user-defined labels. -""" + "Distribution of branches by state." + states: [PostgresBranchStateFacetItem!]! + "Distribution of branches by user-defined labels." labels: [LabelFacetItem!]! } -""" -Input for filtering Postgres instances. -""" -input PostgresInstanceFilter { -""" -Input for filtering Postgres instances. -""" +"Input for filtering Postgres branches." +input PostgresBranchFilter { + "Filter by the name of the branch." name: String -""" -Input for filtering Postgres instances. -""" + "Filter by environments." environments: [String!] -""" -Input for filtering Postgres instances. -""" - states: [PostgresInstanceState!] -""" -Input for filtering Postgres instances. -""" - highAvailability: Boolean -""" -Input for filtering Postgres instances. -""" - majorVersions: [String!] -""" -Input for filtering Postgres instances. -""" + "Filter by branch state." + states: [PostgresBranchState!] + "Filter by user-defined labels. All listed labels must match." labels: [LabelFilter!] } -type PostgresInstanceMaintenanceWindow { - day: Int! - hour: Int! -} - -input PostgresInstanceOrder { - field: PostgresInstanceOrderField! +input PostgresBranchOrder { + field: PostgresBranchOrderField! direction: OrderDirection! } -enum PostgresInstanceOrderField { +enum PostgresBranchOrderField { NAME ENVIRONMENT } -type PostgresInstanceResources { - cpu: String! - memory: String! - diskSize: String! -} - -enum PostgresInstanceState { +"Reconciliation and observed health of a PostgresBranch." +enum PostgresBranchState { + "The branch is healthy and ready." AVAILABLE + "The branch is provisioning or its state has not been observed yet." PROGRESSING + "The branch has reported a failure." DEGRADED } -""" -A single facet item for Postgres instance states. -""" -type PostgresInstanceStateFacetItem { -""" -The Postgres instance state. -""" - state: PostgresInstanceState! -""" -Number of matching instances. -""" +"A single facet item for Postgres branch states." +type PostgresBranchStateFacetItem { + "The Postgres branch state." + state: PostgresBranchState! + "Number of matching branches." count: Int! } @@ -8514,7 +8626,7 @@ Search filter for filtering search results. """ Types that can be searched for. """ -union SearchNode =Team | Application | BigQueryDataset | Bucket | Job | KafkaTopic | OpenSearch | PostgresInstance | SqlInstance | Valkey +union SearchNode =Team | Application | BigQueryDataset | Bucket | Job | KafkaTopic | OpenSearch | PostgresBranch | SqlInstance | Valkey """ Search node connection. @@ -8565,7 +8677,7 @@ Search for applications. JOB KAFKA_TOPIC OPENSEARCH - POSTGRES + POSTGRES_BRANCH SQL_INSTANCE VALKEY } @@ -10611,10 +10723,8 @@ Filtering options for items returned from the connection. """ filter: OpenSearchFilter ): OpenSearchConnection! -""" -Postgres instances owned by the team. -""" - postgresInstances( +"Postgres branches owned by the team." + postgresBranches( """ Get the first n items in the connection. This can be used in combination with the after parameter. """ @@ -10634,12 +10744,12 @@ Get items before this cursor. """ Ordering options for items returned from the connection. """ - orderBy: PostgresInstanceOrder + orderBy: PostgresBranchOrder """ Filtering options for items returned from the connection. """ - filter: PostgresInstanceFilter - ): PostgresInstanceConnection! + filter: PostgresBranchFilter + ): PostgresBranchConnection! repositories( """ Get the first n items in the connection. This can be used in combination with the after parameter. @@ -11256,11 +11366,20 @@ OpenSearch instance in the team environment. name: String! ): OpenSearch! """ -Postgres instance in the team environment. +Postgres in the team environment. """ - postgresInstance( + postgres( + "Name of the Postgres in this team environment." + name: String! + ): Postgres! + """ + EXPERIMENTAL: DO NOT USE + Get a PostgresAccess and its state. Available to authorized team members. + """ + postgresAccess( + "Name of the PostgresAccess in this team environment." name: String! - ): PostgresInstance! + ): PostgresAccess! """ Get a secret by name. """ @@ -11518,9 +11637,9 @@ Total number of OpenSearch instances. total: Int! } -type TeamInventoryCountPostgresInstances { +type TeamInventoryCountPostgresBranches { """ -Total number of Postgres instances. +Total number of Postgres branches. """ total: Int! } @@ -11563,7 +11682,7 @@ Config inventory count for a team. jobs: TeamInventoryCountJobs! kafkaTopics: TeamInventoryCountKafkaTopics! openSearches: TeamInventoryCountOpenSearches! - postgresInstances: TeamInventoryCountPostgresInstances! + postgresBranches: TeamInventoryCountPostgresBranches! """ Secret inventory count for a team. """ @@ -14135,9 +14254,9 @@ Interface for workloads. """ Interface for workloads. """ - postgresInstances( - orderBy: PostgresInstanceOrder - ): PostgresInstanceConnection! + postgresBranches( + orderBy: PostgresBranchOrder + ): PostgresBranchConnection! """ Interface for workloads. """