Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use a per-stack PULUMI_HOME directory #490

Merged
merged 12 commits into from
Sep 22, 2023
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -88,3 +88,4 @@ tags
### IntelliJ ###
.idea
config/
.envrc
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ CHANGELOG

## HEAD (unreleased)
- Changed indentation in deploy/helm/pulumi-operator/templates/deployment.yaml for volumes and volumeMounts.
- Use a separate PULUMI_HOME for each stack. [#490](https://github.com/pulumi/pulumi-kubernetes-operator/pull/490)

## 1.13.0 (2023-08-04)
- Use digest field for Flux source artifact if present [#459](https://github.com/pulumi/pulumi-kubernetes-operator/pull/459)
Expand Down
13 changes: 9 additions & 4 deletions pkg/controller/stack/flux.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ const maxArtifactDownloadSize = 50 * 1024 * 1024
func (sess *reconcileStackSession) SetupWorkdirFromFluxSource(ctx context.Context, source unstructured.Unstructured, fluxSource *shared.FluxSource) (string, error) {
// this source artifact fetching code is based closely on
// https://github.com/fluxcd/kustomize-controller/blob/db3c321163522259595894ca6c19ed44a876976d/controllers/kustomization_controller.go#L529
homeDir := sess.getPulumiHome()
workspaceDir := sess.getWorkspaceDir()
sess.logger.Debug("Setting up pulumi workspace for stack", "stack", sess.stack, "workspace", workspaceDir)

artifactURL, err := getArtifactField(source, "url")
if err != nil {
Expand All @@ -43,14 +46,16 @@ func (sess *reconcileStackSession) SetupWorkdirFromFluxSource(ctx context.Contex
}

fetcher := fetch.NewArchiveFetcher(1, maxArtifactDownloadSize, maxArtifactDownloadSize*10, "")
if err = fetcher.Fetch(artifactURL, digest, sess.rootDir); err != nil {
if err = fetcher.Fetch(artifactURL, digest, workspaceDir); err != nil {
return "", fmt.Errorf("failed to get artifact from source: %w", err)
}

// woo! now there's a directory with source in `rootdir`. Construct a workspace.

secretsProvider := auto.SecretsProvider(sess.stack.SecretsProvider)
w, err := auto.NewLocalWorkspace(ctx, auto.WorkDir(filepath.Join(sess.rootDir, fluxSource.Dir)), secretsProvider)
w, err := auto.NewLocalWorkspace(
ctx,
auto.PulumiHome(homeDir),
auto.WorkDir(filepath.Join(workspaceDir, fluxSource.Dir)),
secretsProvider)
if err != nil {
return "", fmt.Errorf("failed to create local workspace: %w", err)
}
Expand Down
151 changes: 105 additions & 46 deletions pkg/controller/stack/stack_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -461,16 +461,20 @@ func (r *ReconcileStack) Reconcile(ctx context.Context, request reconcile.Reques
stack := instance.Spec
sess := newReconcileStackSession(reqLogger, stack, r.client, request.Namespace)

// Create a long-term working directory containing the home and workspace directories.
// The working directory is deleted during stack finalization.
// Any problem here is unexpected, and treated as a controller error.
_, err = sess.MakeRootDir(instance.GetNamespace(), instance.GetName())
if err != nil {
return reconcile.Result{}, fmt.Errorf("unable to create root directory for stack: %w", err)
}

// We can exit early if there is no clean-up to do.
if isStackMarkedToBeDeleted && !stack.DestroyOnFinalize {
// We know `!(isStackMarkedToBeDeleted && !contains(finalizer))` from above, and now
// `isStackMarkedToBeDeleted`, implying `contains(finalizer)`; but this would be correct
// even if it's a no-op.
err := sess.removeFinalizerAndUpdate(ctx, instance)
if err != nil {
sess.logger.Error(err, "Failed to delete Pulumi finalizer", "Stack.Name", instance.Spec.Stack)
}
return reconcile.Result{}, err
return reconcile.Result{}, sess.finalize(ctx, instance)
}

// This makes sure the status reflects the outcome of reconcilation. Any non-error return means
Expand Down Expand Up @@ -579,18 +583,15 @@ func (r *ReconcileStack) Reconcile(ctx context.Context, request reconcile.Reques
return found
}

// Create the build working directory. Any problem here is unexpected, and treated as a
// Create the workspace directory. Any problem here is unexpected, and treated as a
// controller error.
workingDir, err := makeWorkingDir(instance)
_, err = sess.MakeWorkspaceDir()
if err != nil {
return reconcile.Result{}, fmt.Errorf("unable to create tmp directory for workspace: %w", err)
}
sess.rootDir = workingDir
defer func() {
if workingDir != "" {
os.RemoveAll(workingDir)
}
}()

// Delete the workspace directory after the reconciliation is completed (regardless of success or failure).
defer sess.CleanupWorkspaceDir()

// Check which kind of source we have.

Expand Down Expand Up @@ -633,7 +634,7 @@ func (r *ReconcileStack) Reconcile(ctx context.Context, request reconcile.Reques

if currentCommit, err = sess.SetupWorkdirFromGitSource(ctx, gitAuth, gitSource); err != nil {
r.emitEvent(instance, pulumiv1.StackInitializationFailureEvent(), "Failed to initialize stack: %v", err.Error())
reqLogger.Error(err, "Failed to setup Pulumi workdir", "Stack.Name", stack.Stack)
reqLogger.Error(err, "Failed to setup Pulumi workspace", "Stack.Name", stack.Stack)
r.markStackFailed(sess, instance, err, "", "")
if isStalledError(err) {
instance.Status.MarkStalledCondition(pulumiv1.StalledCrossNamespaceRefForbiddenReason, err.Error())
Expand Down Expand Up @@ -683,7 +684,7 @@ func (r *ReconcileStack) Reconcile(ctx context.Context, request reconcile.Reques
currentCommit, err = sess.SetupWorkdirFromFluxSource(ctx, sourceObject, fluxSource)
if err != nil {
r.emitEvent(instance, pulumiv1.StackInitializationFailureEvent(), "Failed to initialize stack: %v", err.Error())
reqLogger.Error(err, "Failed to setup Pulumi workdir", "Stack.Name", stack.Stack)
reqLogger.Error(err, "Failed to setup Pulumi workspace", "Stack.Name", stack.Stack)
r.markStackFailed(sess, instance, err, "", "")
if isStalledError(err) {
instance.Status.MarkStalledCondition(pulumiv1.StalledCrossNamespaceRefForbiddenReason, err.Error())
Expand All @@ -698,7 +699,7 @@ func (r *ReconcileStack) Reconcile(ctx context.Context, request reconcile.Reques
programRef := stack.ProgramRef
if currentCommit, err = sess.SetupWorkdirFromYAML(ctx, *programRef); err != nil {
r.emitEvent(instance, pulumiv1.StackInitializationFailureEvent(), "Failed to initialize stack: %v", err.Error())
reqLogger.Error(err, "Failed to setup Pulumi workdir", "Stack.Name", stack.Stack)
reqLogger.Error(err, "Failed to setup Pulumi workspace", "Stack.Name", stack.Stack)
r.markStackFailed(sess, instance, err, "", "")
if errors.Is(err, errProgramNotFound) {
instance.Status.MarkStalledCondition(pulumiv1.StalledSourceUnavailableReason, err.Error())
Expand All @@ -714,9 +715,6 @@ func (r *ReconcileStack) Reconcile(ctx context.Context, request reconcile.Reques
}
}

// Delete the temporary directory after the reconciliation is completed (regardless of success or failure).
defer sess.CleanupPulumiDir()

// Step 2. If there are extra environment variables, read them in now and use them for subsequent commands.
if err = sess.SetEnvs(ctx, stack.Envs, request.Namespace); err != nil {
err := fmt.Errorf("could not find ConfigMap for Envs: %w", err)
Expand Down Expand Up @@ -959,8 +957,11 @@ func (sess *reconcileStackSession) finalize(ctx context.Context, stack *pulumiv1
sess.logger.Error(err, "Failed to run Pulumi finalizer", "Stack.Name", stack.Spec.Stack)
return err
}

return sess.removeFinalizerAndUpdate(ctx, stack)
if err := sess.removeFinalizerAndUpdate(ctx, stack); err != nil {
sess.logger.Error(err, "Failed to delete Pulumi finalizer", "Stack.Name", stack.Spec.Stack)
return err
}
return nil
}

// removeFinalizerAndUpdate makes sure this controller's finalizer is not present in the instance
Expand Down Expand Up @@ -988,6 +989,10 @@ func (sess *reconcileStackSession) finalizeStack(ctx context.Context) error {
return err
}
}

// Delete the root directory for this stack.
sess.cleanupRootDir()

sess.logger.Info("Successfully finalized stack")
return nil
}
Expand Down Expand Up @@ -1220,27 +1225,76 @@ func (sess *reconcileStackSession) lookupPulumiAccessToken(ctx context.Context)
return "", false
}

// Make a working directory for building the given stack. These are stable paths, so that (for one
// Make a root directory for the given stack, containing the home and workspace directories.
func (sess *reconcileStackSession) MakeRootDir(ns, name string) (string, error) {
rootDir := filepath.Join(os.TempDir(), buildDirectoryPrefix, ns, name)
sess.logger.Debug("Creating root dir for stack", "stack", sess.stack, "root", rootDir)
if err := os.MkdirAll(rootDir, 0700); err != nil {
return "", fmt.Errorf("error creating working dir: %w", err)
}
sess.rootDir = rootDir

homeDir := sess.getPulumiHome()
if err := os.MkdirAll(homeDir, 0700); err != nil {
return "", fmt.Errorf("error creating .pulumi dir: %w", err)
}
return rootDir, nil
}

// cleanupRootDir cleans the root directory that contains the Pulumi home and workspace directories.
func (sess *reconcileStackSession) cleanupRootDir() {
if sess.rootDir == "" {
return
}
sess.logger.Debug("Cleaning up root dir for stack", "stack", sess.stack, "root", sess.rootDir)
if err := os.RemoveAll(sess.rootDir); err != nil {
sess.logger.Error(err, "Failed to delete temporary root dir: %s", sess.rootDir)
}
sess.rootDir = ""
}

// getPulumiHome returns the home directory (containing CLI artifacts such as plugins and credentials).
func (sess *reconcileStackSession) getPulumiHome() string {
return filepath.Join(sess.rootDir, ".pulumi")
}

// Make a workspace directory for building the given stack. These are stable paths, so that (for one
// thing) the go build cache does not treat new clones of the same repo as distinct files. Since a
// stack is processed by at most one thread at a time, and stacks have unique qualified names, and
// the directory is expected to be removed after processing, this won't cause collisions; but, we
// check anyway, treating the existence of the build directory as a crude lock.
func makeWorkingDir(s *pulumiv1.Stack) (_path string, _err error) {
path := filepath.Join(os.TempDir(), buildDirectoryPrefix, s.GetNamespace(), s.GetName())
_, err := os.Stat(path)
// the workspace directory is expected to be removed after processing, this won't cause collisions; but, we
// check anyway, treating the existence of the workspace directory as a crude lock.
func (sess *reconcileStackSession) MakeWorkspaceDir() (string, error) {
workspaceDir := filepath.Join(sess.rootDir, "workspace")
_, err := os.Stat(workspaceDir)
switch {
case os.IsNotExist(err):
break
case err == nil:
return "", fmt.Errorf("expected build directory %q for stack not to exist already, but it does", path)
return "", fmt.Errorf("expected workspace directory %q for stack not to exist already, but it does", workspaceDir)
case err != nil:
return "", fmt.Errorf("error while checking for build directory: %w", err)
return "", fmt.Errorf("error while checking for workspace directory: %w", err)
}
if err := os.MkdirAll(workspaceDir, 0700); err != nil {
return "", fmt.Errorf("error creating workspace dir: %w", err)
}
return workspaceDir, nil
}

if err = os.MkdirAll(path, 0700); err != nil {
return "", fmt.Errorf("error creating working dir: %w", err)
// CleanupWorkspace cleans the Pulumi workspace directory, located within the root directory.
func (sess *reconcileStackSession) CleanupWorkspaceDir() {
if sess.rootDir == "" {
return
}
workspaceDir := sess.getWorkspaceDir()
sess.logger.Debug("Cleaning up pulumi workspace for stack", "stack", sess.stack, "workspace", workspaceDir)
if err := os.RemoveAll(workspaceDir); err != nil {
sess.logger.Error(err, "Failed to delete workspace dir: %s", workspaceDir)
}
return path, nil
}

// getWorkspaceDir returns the workspace directory (containing the Pulumi project).
func (sess *reconcileStackSession) getWorkspaceDir() string {
return filepath.Join(sess.rootDir, "workspace")
}

func (sess *reconcileStackSession) SetupWorkdirFromGitSource(ctx context.Context, gitAuth *auto.GitAuth, source *shared.GitSource) (string, error) {
Expand All @@ -1251,12 +1305,20 @@ func (sess *reconcileStackSession) SetupWorkdirFromGitSource(ctx context.Context
Branch: source.Branch,
Auth: gitAuth,
}
homeDir := sess.getPulumiHome()
workspaceDir := sess.getWorkspaceDir()

sess.logger.Debug("Setting up pulumi workdir for stack", "stack", sess.stack)
sess.logger.Debug("Setting up pulumi workspace for stack", "stack", sess.stack, "workspace", workspaceDir)
// Create a new workspace.

secretsProvider := auto.SecretsProvider(sess.stack.SecretsProvider)

w, err := auto.NewLocalWorkspace(ctx, auto.WorkDir(sess.rootDir), auto.Repo(repo), secretsProvider)
w, err := auto.NewLocalWorkspace(
ctx,
auto.PulumiHome(homeDir),
auto.WorkDir(workspaceDir),
auto.Repo(repo),
secretsProvider)
if err != nil {
return "", fmt.Errorf("failed to create local workspace: %w", err)
}
Expand All @@ -1277,10 +1339,11 @@ type ProjectFile struct {
}

func (sess *reconcileStackSession) SetupWorkdirFromYAML(ctx context.Context, programRef shared.ProgramReference) (string, error) {
sess.logger.Debug("Setting up pulumi workdir for stack", "stack", sess.stack)
homeDir := sess.getPulumiHome()
workspaceDir := sess.getWorkspaceDir()
sess.logger.Debug("Setting up pulumi workspace for stack", "stack", sess.stack, "workspace", workspaceDir)

// Create a new workspace.

secretsProvider := auto.SecretsProvider(sess.stack.SecretsProvider)

program := pulumiv1.Program{}
Expand All @@ -1304,13 +1367,17 @@ func (sess *reconcileStackSession) SetupWorkdirFromYAML(ctx context.Context, pro
return "", fmt.Errorf("failed to marshal program object to YAML: %w", err)
}

err = os.WriteFile(filepath.Join(sess.rootDir, "Pulumi.yaml"), out, 0600)
err = os.WriteFile(filepath.Join(workspaceDir, "Pulumi.yaml"), out, 0600)
if err != nil {
return "", fmt.Errorf("failed to write YAML to file: %w", err)
}

var w auto.Workspace
w, err = auto.NewLocalWorkspace(ctx, auto.WorkDir(sess.rootDir), secretsProvider)
w, err = auto.NewLocalWorkspace(
ctx,
auto.PulumiHome(homeDir),
auto.WorkDir(workspaceDir),
secretsProvider)
if err != nil {
return "", fmt.Errorf("failed to create local workspace: %w", err)
}
Expand Down Expand Up @@ -1407,14 +1474,6 @@ func (sess *reconcileStackSession) ensureStackSettings(ctx context.Context, w au
return nil
}

func (sess *reconcileStackSession) CleanupPulumiDir() {
if sess.rootDir != "" {
if err := os.RemoveAll(sess.rootDir); err != nil {
sess.logger.Error(err, "Failed to delete temporary root dir: %s", sess.rootDir)
}
}
}

// Determine the actual commit information from the working directory (Spec commit etc. is optional).
func revisionAtWorkingDir(workingDir string) (string, error) {
gitRepo, err := git.PlainOpenWithOptions(workingDir, &git.PlainOpenOptions{DetectDotGit: true})
Expand Down
2 changes: 2 additions & 0 deletions test/s3backend/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
/bin/
/node_modules/
Loading