Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ CHANGELOG
- New feature: preview mode [#1012](https://github.com/pulumi/pulumi-kubernetes-operator/pull/1012)
- New feature: structured configuration [#1023](https://github.com/pulumi/pulumi-kubernetes-operator/pull/1023)
- Add validation to limit Stack name to 42 characters [#899](https://github.com/pulumi/pulumi-kubernetes-operator/issues/899)
- Fix secretsProvider not being applied to new stacks [#935](https://github.com/pulumi/pulumi-kubernetes-operator/issues/935)

## 2.2.0 (2025-08-11)

Expand Down
20 changes: 12 additions & 8 deletions agent/cmd/serve.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,13 @@ const (
)

var (
_workDir string
_skipInstall bool
_stack string
_envFile string
_host string
_port int
_workDir string
_skipInstall bool
_stack string
_secretsProvider string
_envFile string
_host string
_port int

_authMode string
_audiences []string
Expand Down Expand Up @@ -172,8 +173,9 @@ var serveCmd = &cobra.Command{

// Create the automation service
autoServer, err := server.NewServer(ctx, workspace, &server.Options{
StackName: _stack,
PulumiLogLevel: _pulumiLogLevel,
StackName: _stack,
SecretsProvider: _secretsProvider,
PulumiLogLevel: _pulumiLogLevel,
})
if err != nil {
return fmt.Errorf("unable to make an automation server: %w", err)
Expand Down Expand Up @@ -228,6 +230,8 @@ func init() {

serveCmd.Flags().StringVarP(&_stack, "stack", "s", "", "Select (or create) the stack to use")

serveCmd.Flags().StringVar(&_secretsProvider, "secrets-provider", "", "The secrets provider to use when creating the stack (passphrase, awskms, azurekeyvault, gcpkms, hashivault)")

serveCmd.Flags().StringVar(&_envFile, "env-file", "", "An environment file to load (e.g. .env)")

serveCmd.Flags().StringVar(&_host, "host", "0.0.0.0", "Server bind address (default: 0.0.0.0)")
Expand Down
74 changes: 60 additions & 14 deletions agent/pkg/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,9 +78,12 @@ type Server struct {
var _ = pb.AutomationServiceServer(&Server{})

type Options struct {
// StackName is the name of the stack to upsert (optional).
// StackName is the name of the stack to select or create (optional).
// If the stack exists, it will be selected. If it doesn't exist, it will be created.
StackName string
// SecretsProvider is the secrets provider to use for new stacks (optional).
// SecretsProvider is the secrets provider to use when creating new stacks (optional).
// This is only applied when creating a new stack, not when selecting an existing stack.
// Examples: "passphrase", "awskms://...", "azurekeyvault://...", "gcpkms://...", "hashivault://..."
SecretsProvider string

// PulumiLogLevel is the log level to use for Pulumi CLI operations.
Expand All @@ -106,21 +109,21 @@ func NewServer(ctx context.Context, ws auto.Workspace, opts *Options) (*Server,

// select the initial stack, if provided
if opts.StackName != "" {
stack, err := auto.UpsertStack(ctx, opts.StackName, ws)
stack, err := auto.SelectStack(ctx, opts.StackName, ws)
if err != nil {
return nil, fmt.Errorf("failed to select stack: %w", err)
}
if opts.SecretsProvider != "" {
// We must always make sure the secret provider is initialized in the workspace
// before we set any configs. Otherwise secret provider will mysteriously reset.
// https://github.com/pulumi/pulumi-kubernetes-operator/issues/135
err = stack.ChangeSecretsProvider(ctx, opts.SecretsProvider, &auto.ChangeSecretsProviderOptions{})
if err != nil {
return nil, fmt.Errorf("failed to set secrets provider: %w", err)
if auto.IsSelectStack404Error(err) {
stack, err = initStack(ctx, ws, opts.StackName, opts.SecretsProvider)
if err != nil {
return nil, fmt.Errorf("failed to create stack: %w", err)
}
server.log.Infow("created and selected stack", "name", stack.Name())
} else {
return nil, fmt.Errorf("failed to select stack: %w", err)
}
} else {
server.log.Infow("selected existing stack", "name", stack.Name())
}
server.stack = &stack
server.log.Infow("selected a stack", "name", stack.Name())
}

proj, err := ws.ProjectSettings(ctx)
Expand Down Expand Up @@ -217,7 +220,7 @@ func (s *Server) SelectStack(ctx context.Context, in *pb.SelectStackRequest) (*p
if !in.GetCreate() {
return auto.Stack{}, status.Error(codes.NotFound, "stack not found")
}
return auto.NewStack(ctx, in.StackName, s.ws)
return initStack(ctx, s.ws, in.StackName, in.GetSecretsProvider())
}
return auto.Stack{}, err
}
Expand All @@ -242,6 +245,49 @@ func (s *Server) SelectStack(ctx context.Context, in *pb.SelectStackRequest) (*p
return resp, nil
}

// initStack creates a new stack with the specified secrets provider.
// It uses `pulumi stack init --secrets-provider` to properly initialize the secrets provider from the beginning.
// This is preferred over auto.NewStack which does not support specifying a secrets provider at creation time.
func initStack(ctx context.Context, ws auto.Workspace, stackName, secretsProvider string) (auto.Stack, error) {
log := zap.L().Named("initStack").Sugar()

log.Debugw("initializing stack", "stackName", stackName, "secretsProvider", secretsProvider)

// Create the stack with secrets provider using pulumi stack init
args := []string{"stack", "init", stackName}
if secretsProvider != "" {
args = append(args, "--secrets-provider", secretsProvider)
}
var env []string
if ws.PulumiHome() != "" {
homeEnv := fmt.Sprintf("PULUMI_HOME=%s", ws.PulumiHome())
env = append(env, homeEnv)
}
if envvars := ws.GetEnvVars(); envvars != nil {
for k, v := range envvars {
e := []string{k, v}
env = append(env, strings.Join(e, "="))
}
}

_, stderr, errCode, err := ws.PulumiCommand().Run(ctx, ws.WorkDir(), nil, nil, nil, env, args...)
if err != nil {
log.Errorw("failed to create stack", "stackName", stackName, "exitCode", errCode, "stderr", stderr, zap.Error(err))
return auto.Stack{}, fmt.Errorf("failed to create stack: %w: %s", err, stderr)
}

log.Debugw("stack initialized successfully", "stackName", stackName)

// Now select the newly created stack
stack, err := auto.SelectStack(ctx, stackName, ws)
if err != nil {
log.Errorw("failed to select newly created stack", "stackName", stackName, zap.Error(err))
return auto.Stack{}, err
}

return stack, nil
}

func (s *Server) Info(ctx context.Context, in *pb.InfoRequest) (*pb.InfoResult, error) {
stack, err := s.ensureStack(ctx)
if err != nil {
Expand Down
29 changes: 18 additions & 11 deletions agent/pkg/server/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,12 @@ func TestNewServer(t *testing.T) {
projectDir: "./testdata/simple",
opts: &Options{StackName: "new"},
},
{
name: "new stack with invalid secrets provider",
projectDir: "./testdata/simple",
opts: &Options{StackName: "bad-provider-stack", SecretsProvider: "bad"},
wantErr: gomega.ContainSubstring("unknown secrets provider type 'bad'"),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
Expand Down Expand Up @@ -137,23 +143,14 @@ func TestWhoAmI(t *testing.T) {

func TestSelectStack(t *testing.T) {
t.Parallel()

// hasSummary := func(name string) types.GomegaMatcher {
// return gstruct.MatchFields(gstruct.IgnoreExtras, gstruct.Fields{
// "Summary": gstruct.PointTo(gstruct.MatchFields(gstruct.IgnoreExtras, gstruct.Fields{
// "Name": gomega.Equal(name),
// })),
// })
// }

tests := []struct {
name string
stacks []string
req *pb.SelectStackRequest
wantErr any
}{
{
name: "already selected stack",
name: "existent stack (already selected)",
stacks: []string{"one"},
req: &pb.SelectStackRequest{
StackName: "one",
Expand All @@ -174,17 +171,27 @@ func TestSelectStack(t *testing.T) {
wantErr: status.Error(codes.NotFound, "stack not found"),
},
{
name: "non-existent stack with create",
name: "new stack with create",
req: &pb.SelectStackRequest{
StackName: "one",
Create: ptr.To(true),
},
},
{
name: "new stack with invalid secrets provider",
req: &pb.SelectStackRequest{
StackName: "bad-provider-stack",
Create: ptr.To(true),
SecretsProvider: ptr.To("bad"),
},
wantErr: gomega.ContainSubstring("unknown secrets provider type 'bad'"),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
g := gomega.NewWithT(t)

ctx := newContext(t)
tc := newTC(ctx, t, tcOptions{ProjectDir: "./testdata/simple", Stacks: tt.stacks})

Expand Down