Skip to content
Open
Show file tree
Hide file tree
Changes from 6 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
75 changes: 15 additions & 60 deletions cmd/git/executor.go
Original file line number Diff line number Diff line change
@@ -1,18 +1,14 @@
package git

import (
"bytes"
"context"
"errors"
"fmt"
"io"
"os"
"path/filepath"
"strings"

errUtils "github.com/cloudposse/atmos/errors"
atmosgit "github.com/cloudposse/atmos/pkg/git"
iolib "github.com/cloudposse/atmos/pkg/io"
"github.com/cloudposse/atmos/pkg/perf"
"github.com/cloudposse/atmos/pkg/ui"
"github.com/cloudposse/atmos/pkg/ui/spinner"
Expand All @@ -38,10 +34,6 @@ type Executor struct {
provider atmosgit.Provider
}

type stderrSwapper interface {
SwapStderr(io.Writer) func()
}

// newExecutor builds an Executor using the named provider from the registry.
// Pass an empty string to use the default "cli" provider.
func newExecutor(providerName string) (*Executor, error) {
Expand All @@ -65,12 +57,12 @@ func (e *Executor) Init(ctx context.Context, opts *atmosgit.InitOptions, label s
reconcile := initWillReconcile(opts)
progressMsg := initProgressMessage(label, opts, reconcile)
completedMsg := initCompletedMessage(label, opts, reconcile)
stderr, err := e.captureStderr(func() error {
stderr, err := atmosgit.CaptureStderr(e.provider, func() error {
return spinner.ExecWithSpinner(progressMsg, completedMsg, func() error {
return e.provider.Init(ctx, opts)
})
})
return wrapGitOperationError(
return atmosgit.WrapOperationError(
fmt.Sprintf("initialize Git repository %q", label),
opts.Workdir,
stderr,
Expand Down Expand Up @@ -133,7 +125,7 @@ func (e *Executor) Clone(ctx context.Context, opts *atmosgit.CloneOptions, label

progressMsg := fmt.Sprintf("Cloning %s", label)
completedMsg := fmt.Sprintf("Cloned %s into %s.", label, opts.Workdir)
stderr, err := e.captureStderr(func() error {
stderr, err := atmosgit.CaptureStderr(e.provider, func() error {
return spinner.ExecWithSpinner(progressMsg, completedMsg, func() error {
return e.provider.Clone(ctx, opts)
})
Expand All @@ -144,7 +136,7 @@ func (e *Executor) Clone(ctx context.Context, opts *atmosgit.CloneOptions, label
func (e *Executor) CloneWithoutSpinner(ctx context.Context, opts *atmosgit.CloneOptions, label string) error {
defer perf.Track(nil, "git.Executor.CloneWithoutSpinner")()

stderr, err := e.captureStderr(func() error {
stderr, err := atmosgit.CaptureStderr(e.provider, func() error {
return e.provider.Clone(ctx, opts)
})
if err != nil {
Expand All @@ -155,22 +147,8 @@ func (e *Executor) CloneWithoutSpinner(ctx context.Context, opts *atmosgit.Clone
return nil
}

func (e *Executor) captureStderr(operation func() error) (string, error) {
swapper, ok := e.provider.(stderrSwapper)
if !ok {
return "", operation()
}

var stderr bytes.Buffer
restore := swapper.SwapStderr(iolib.MaskWriter(&stderr))
defer restore()

err := operation()
return strings.TrimSpace(stderr.String()), err
}

func wrapCloneError(label, workdir, stderr string, err error) error {
return wrapGitOperationError(
return atmosgit.WrapOperationError(
fmt.Sprintf("clone Git repository %q", label),
workdir,
stderr,
Expand All @@ -179,34 +157,11 @@ func wrapCloneError(label, workdir, stderr string, err error) error {
)
}

func wrapGitOperationError(action, workdir, stderr string, err error, hint string) error {
if err == nil {
return nil
}

explanation := fmt.Sprintf("Failed to %s.", action)
if workdir != "" {
explanation = fmt.Sprintf("Failed to %s in %q.", action, workdir)
}
explanation += "\n\nUnderlying error:\n\n```text\n" + err.Error() + "\n```"
if stderr != "" {
explanation += "\n\nGit output:\n\n```text\n" + stderr + "\n```"
}

builder := errUtils.Build(err).
WithExplanation(explanation).
WithExitCode(2)
if hint != "" {
builder = builder.WithHint(hint)
}
return builder.Err()
}

// Pull delegates to the provider.
func (e *Executor) Pull(ctx context.Context, opts *atmosgit.PullOptions) error {
defer perf.Track(nil, "git.Executor.Pull")()

stderr, err := e.captureStderr(func() error {
stderr, err := atmosgit.CaptureStderr(e.provider, func() error {
return e.provider.Pull(ctx, opts)
})
if errors.Is(err, errUtils.ErrGitNoTrackingBranch) {
Expand All @@ -218,7 +173,7 @@ func (e *Executor) Pull(ctx context.Context, opts *atmosgit.PullOptions) error {
Err()
}
if err != nil {
return wrapGitOperationError(
return atmosgit.WrapOperationError(
"pull Git repository",
opts.Workdir,
stderr,
Expand All @@ -236,13 +191,13 @@ func (e *Executor) Status(ctx context.Context, opts *atmosgit.StatusOptions) (*a
defer perf.Track(nil, "git.Executor.Status")()

var result *atmosgit.StatusResult
stderr, err := e.captureStderr(func() error {
stderr, err := atmosgit.CaptureStderr(e.provider, func() error {
var opErr error
result, opErr = e.provider.Status(ctx, opts)
return opErr
})
if err != nil {
return nil, wrapGitOperationError("read Git status", opts.Workdir, stderr, err, "")
return nil, atmosgit.WrapOperationError("read Git status", opts.Workdir, stderr, err, "")
}
return result, nil
}
Expand All @@ -252,13 +207,13 @@ func (e *Executor) Diff(ctx context.Context, opts *atmosgit.DiffOptions) (*atmos
defer perf.Track(nil, "git.Executor.Diff")()

var result *atmosgit.DiffResult
stderr, err := e.captureStderr(func() error {
stderr, err := atmosgit.CaptureStderr(e.provider, func() error {
var opErr error
result, opErr = e.provider.Diff(ctx, opts)
return opErr
})
if err != nil {
return nil, wrapGitOperationError("show Git diff", opts.Workdir, stderr, err, "")
return nil, atmosgit.WrapOperationError("show Git diff", opts.Workdir, stderr, err, "")
}
return result, nil
}
Expand All @@ -268,13 +223,13 @@ func (e *Executor) Commit(ctx context.Context, opts *atmosgit.CommitOptions) (*a
defer perf.Track(nil, "git.Executor.Commit")()

var result *atmosgit.CommitResult
stderr, err := e.captureStderr(func() error {
stderr, err := atmosgit.CaptureStderr(e.provider, func() error {
var opErr error
result, opErr = e.provider.Commit(ctx, opts)
return opErr
})
if err != nil {
return nil, wrapGitOperationError("commit Git changes", opts.Workdir, stderr, err, "")
return nil, atmosgit.WrapOperationError("commit Git changes", opts.Workdir, stderr, err, "")
}
return result, nil
}
Expand All @@ -283,11 +238,11 @@ func (e *Executor) Commit(ctx context.Context, opts *atmosgit.CommitOptions) (*a
func (e *Executor) Push(ctx context.Context, opts *atmosgit.PushOptions) error {
defer perf.Track(nil, "git.Executor.Push")()

stderr, err := e.captureStderr(func() error {
stderr, err := atmosgit.CaptureStderr(e.provider, func() error {
return e.provider.Push(ctx, opts)
})
if err != nil {
return wrapGitOperationError(
return atmosgit.WrapOperationError(
"push Git repository",
opts.Workdir,
stderr,
Expand Down
1 change: 1 addition & 0 deletions internal/exec/describe_affected_components.go
Original file line number Diff line number Diff line change
Expand Up @@ -590,6 +590,7 @@ func addKubernetesSectionAffected(
{sectionNamePaths, affectedReasonStackPaths},
{sectionNameManifests, affectedReasonStackManifests},
{sectionNameRender, affectedReasonStackRender},
{cfg.ValidateSectionName, fmt.Sprintf("stack.%s", cfg.ValidateSectionName)},
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}...)
sections = appendSectionChecks(sections, resolveComponentSectionChecks(atmosConfig)...)

Expand Down
5 changes: 5 additions & 0 deletions internal/exec/stack_processor_cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,11 @@ func deepCopyBaseComponentConfigMaps(dst, src *schema.BaseComponentConfig) error
return err
}
}
if src.BaseComponentValidate != nil {
if dst.BaseComponentValidate, err = deepCopyComponentAnySection(src.BaseComponentValidate); err != nil {
return err
}
}
if src.BaseComponentPlugins != nil {
if dst.BaseComponentPlugins, err = deepCopyComponentAnySection(src.BaseComponentPlugins); err != nil {
return err
Expand Down
14 changes: 14 additions & 0 deletions internal/exec/stack_processor_merge.go
Original file line number Diff line number Diff line change
Expand Up @@ -381,6 +381,17 @@ func mergeComponentConfigurations(atmosConfig *schema.AtmosConfiguration, opts *
return nil, err
}

finalComponentValidate, err := mergeComponentAnySection(
mergeConfig,
cfg.ValidateSectionName,
opts.GlobalKubernetesValidate,
result.BaseComponentValidate,
result.ComponentValidate,
)
if err != nil {
return nil, err
}

var finalComponentRender map[string]any
if opts.ComponentType == cfg.KubernetesComponentType {
finalComponentRender, err = m.Merge(
Expand Down Expand Up @@ -608,6 +619,9 @@ func mergeComponentConfigurations(atmosConfig *schema.AtmosConfiguration, opts *
if len(finalComponentRender) > 0 {
comp[cfg.RenderSectionName] = finalComponentRender
}
if finalComponentValidate != nil {
comp[cfg.ValidateSectionName] = finalComponentValidate
}
comp[cfg.GenerateSectionName] = finalComponentGenerate
}

Expand Down
42 changes: 42 additions & 0 deletions internal/exec/stack_processor_merge_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -735,6 +735,48 @@ func TestMergeComponentConfigurations_Kubernetes(t *testing.T) {
assert.Equal(t, "global-wd", provision["workdir"])
assert.Equal(t, "5m", provision["timeout"])
})

t.Run("validate-component-instance-false-overrides-base-true", func(t *testing.T) {
opts := ComponentProcessorOptions{
ComponentType: cfg.KubernetesComponentType,
Component: "api",
AtmosConfig: atmosCfg,
}
res := minimalComponentResult()
res.BaseComponentValidate = true
res.ComponentValidate = false
comp, err := mergeComponentConfigurations(atmosCfg, &opts, res)
require.NoError(t, err)
assert.Equal(t, false, comp[cfg.ValidateSectionName],
"an explicit component-instance validate:false must override a base-component validate:true")
})

t.Run("validate-base-true-flows-through-when-component-unset", func(t *testing.T) {
opts := ComponentProcessorOptions{
ComponentType: cfg.KubernetesComponentType,
Component: "api",
AtmosConfig: atmosCfg,
}
res := minimalComponentResult()
res.BaseComponentValidate = true
comp, err := mergeComponentConfigurations(atmosCfg, &opts, res)
require.NoError(t, err)
assert.Equal(t, true, comp[cfg.ValidateSectionName],
"base-component validate:true must flow through when the component instance sets nothing")
})

t.Run("validate-unset-everywhere-is-absent-from-comp", func(t *testing.T) {
opts := ComponentProcessorOptions{
ComponentType: cfg.KubernetesComponentType,
Component: "api",
AtmosConfig: atmosCfg,
}
res := minimalComponentResult()
comp, err := mergeComponentConfigurations(atmosCfg, &opts, res)
require.NoError(t, err)
_, ok := comp[cfg.ValidateSectionName]
assert.False(t, ok, "validate must be absent (not defaulted to any value) when unset at every layer")
})
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

// TestMergeComponentConfigurations_Retry covers the per-component retry merge added by
Expand Down
6 changes: 6 additions & 0 deletions internal/exec/stack_processor_process_stacks.go
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,7 @@ func ProcessStackConfig(
var kubernetesPaths any
var kubernetesManifests any
kubernetesRender := map[string]any{}
var kubernetesValidate any

helmVars := map[string]any{}
helmSettings := map[string]any{}
Expand Down Expand Up @@ -812,6 +813,10 @@ func ProcessStackConfig(
}
}

if i, ok := globalKubernetesSection[cfg.ValidateSectionName]; ok {
kubernetesValidate = i
}

// Helm section.
if i, ok := globalHelmSection[cfg.CommandSectionName]; ok {
helmCommand, ok = i.(string)
Expand Down Expand Up @@ -1146,6 +1151,7 @@ func ProcessStackConfig(
GlobalKubernetesPaths: kubernetesPaths,
GlobalKubernetesManifests: kubernetesManifests,
GlobalKubernetesRender: kubernetesRender,
GlobalKubernetesValidate: kubernetesValidate,
AtmosConfig: atmosConfig,
}, nil
}
Expand Down
3 changes: 3 additions & 0 deletions internal/exec/stack_processor_process_stacks_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ type ComponentProcessorOptions struct {
GlobalKubernetesPaths any
GlobalKubernetesManifests any
GlobalKubernetesRender map[string]any
GlobalKubernetesValidate any

// Atmos configuration.
AtmosConfig *schema.AtmosConfiguration
Expand All @@ -70,6 +71,7 @@ type ComponentProcessorResult struct {
ComponentProvider string
ComponentPaths any
ComponentManifests any
ComponentValidate any
// ComponentPlugins holds the Helm CLI plugins list (helm/helmfile components).
ComponentPlugins any
ComponentRender map[string]any
Expand All @@ -94,6 +96,7 @@ type ComponentProcessorResult struct {
BaseComponentProvider string
BaseComponentPaths any
BaseComponentManifests any
BaseComponentValidate any
// BaseComponentPlugins holds the inherited Helm CLI plugins list from base components.
BaseComponentPlugins any
BaseComponentRender map[string]any
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,10 @@ func extractComponentSections(opts *ComponentProcessorOptions, result *Component
result.ComponentManifests = i
}

if i, ok := opts.ComponentMap[cfg.ValidateSectionName]; ok {
result.ComponentValidate = i
}

if i, ok := opts.ComponentMap[cfg.RenderSectionName]; ok {
componentRender, ok := i.(map[string]any)
if !ok {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,7 @@ func applyBaseComponentConfig(opts *ComponentProcessorOptions, result *Component
result.BaseComponentProvider = baseComponentConfig.BaseComponentProvider
result.BaseComponentPaths = baseComponentConfig.BaseComponentPaths
result.BaseComponentManifests = baseComponentConfig.BaseComponentManifests
result.BaseComponentValidate = baseComponentConfig.BaseComponentValidate
result.BaseComponentRender = baseComponentConfig.BaseComponentRender
result.BaseComponentHelm = baseComponentConfig.BaseComponentHelm
// BaseComponentRetry flows from the inheritance chain through to merge — see
Expand Down
12 changes: 12 additions & 0 deletions internal/exec/stack_processor_utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -2319,6 +2319,7 @@ func processBaseComponentConfigInternal(
var baseComponentProvider string
var baseComponentPaths any
var baseComponentManifests any
var baseComponentValidate any
var baseComponentPlugins any
var baseComponentRender map[string]any
var baseComponentHelm map[string]any
Expand Down Expand Up @@ -2547,6 +2548,10 @@ func processBaseComponentConfigInternal(
baseComponentManifests = baseComponentManifestsSection
}

if baseComponentValidateSection, baseComponentValidateSectionExist := baseComponentMap[cfg.ValidateSectionName]; baseComponentValidateSectionExist {
baseComponentValidate = baseComponentValidateSection
}

if baseComponentPluginsSection, baseComponentPluginsSectionExist := baseComponentMap[cfg.PluginsSectionName]; baseComponentPluginsSectionExist {
baseComponentPlugins = baseComponentPluginsSection
}
Expand Down Expand Up @@ -2784,6 +2789,13 @@ func processBaseComponentConfigInternal(
}
baseComponentConfig.BaseComponentManifests = mergedAny

// Base component `validate`
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
mergedAny, err = mergeComponentAnySection(levelMergeConfig, cfg.ValidateSectionName, baseComponentConfig.BaseComponentValidate, baseComponentValidate)
if err != nil {
return err
}
baseComponentConfig.BaseComponentValidate = mergedAny

// Base component `plugins` (Helm CLI plugins list).
mergedAny, err = mergeComponentAnySection(levelMergeConfig, cfg.PluginsSectionName, baseComponentConfig.BaseComponentPlugins, baseComponentPlugins)
if err != nil {
Expand Down
Loading