Skip to content
Open
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
5 changes: 5 additions & 0 deletions .osc-metadata/sync.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"fork_synced_at": "2026-07-10T13:14:53.649718+00:00",
"commits_behind_before_sync": 0,
"action_taken": "noop"
}
21 changes: 18 additions & 3 deletions internal/runner/actions.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,15 +60,30 @@ func (r *Runner) ExecAction() error {

// Run the `init` command
func (r *Runner) ExecInit() error {
log.Infof("launching %s init in %s", r.exec.TenvName(), r.workingDir)
if r.exec == nil {
err := errors.New("terraform or terragrunt binary not installed")
return err
}
tenvName := r.exec.TenvName()
log.Infof("launching %s init in %s", tenvName, r.workingDir)
err := r.exec.Init(r.workingDir)
if err != nil {
log.Errorf("error executing %s init: %s", r.exec.TenvName(), err)
return err
if !r.config.Hermitcrab.Enabled {
log.Errorf("error executing %s init: %s", tenvName, err)
return err
}
log.Warnf("error executing %s init through Hermitcrab network mirror: %s", tenvName, err)
log.Warnf("Hermitcrab network mirror may be stale or unreachable, removing mirror configuration and retrying %s init directly", tenvName)
removeErr := r.DisableHermitcrab()
if removeErr != nil {
return fmt.Errorf("could not remove Hermitcrab network mirror configuration: %w", removeErr)
}
log.Infof("retrying %s init without Hermitcrab network mirror", tenvName)
err = r.exec.Init(r.workingDir)
if err != nil {
log.Errorf("error executing %s init without Hermitcrab network mirror: %s", tenvName, err)
return err
}
}
return nil
}
Expand Down
170 changes: 170 additions & 0 deletions internal/runner/hermitcrab_fallback_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
package runner

import (
"errors"
"os"
"path/filepath"
"reflect"
"testing"

"github.com/padok-team/burrito/internal/burrito/config"
)

type fakeExec struct {
initErrs []error
initCalls []string
}

func (f *fakeExec) Init(workingDir string) error {
f.initCalls = append(f.initCalls, workingDir)
if len(f.initErrs) == 0 {
return nil
}
err := f.initErrs[0]
f.initErrs = f.initErrs[1:]
return err
}

func (f *fakeExec) Plan(string) error {
return nil
}

func (f *fakeExec) Apply(string) error {
return nil
}

func (f *fakeExec) Show(string, string) ([]byte, error) {
return []byte{}, nil
}

func (f *fakeExec) TenvName() string {
return "terraform"
}

func (f *fakeExec) GetExecPath() string {
return "/tmp/terraform"
}

func testRunnerConfig(t *testing.T) *config.Config {
t.Helper()

conf := config.TestConfig()
conf.Runner.RepositoryPath = t.TempDir()
conf.Hermitcrab.URL = "http://hermitcrab.local"
return conf
}

func TestExecInitWithHermitcrabKeepsMirrorConfigWhenInitSucceeds(t *testing.T) {
conf := testRunnerConfig(t)
conf.Hermitcrab.Enabled = true
_ = os.Unsetenv("TF_CLI_CONFIG_FILE")
t.Cleanup(func() {
_ = os.Unsetenv("TF_CLI_CONFIG_FILE")
})

runnerInstance := New(conf)
if err := runnerInstance.EnableHermitcrab(); err != nil {
t.Fatalf("EnableHermitcrab() error = %v", err)
}

exec := &fakeExec{}
runnerInstance.exec = exec
runnerInstance.workingDir = filepath.Join(conf.Runner.RepositoryPath, "content")

if err := runnerInstance.ExecInit(); err != nil {
t.Fatalf("ExecInit() error = %v", err)
}
expectedCalls := []string{runnerInstance.workingDir}
if !reflect.DeepEqual(exec.initCalls, expectedCalls) {
t.Fatalf("Init() calls = %v, want %v", exec.initCalls, expectedCalls)
}
configPath := filepath.Join(conf.Runner.RepositoryPath, "config.tfrc")
if got := os.Getenv("TF_CLI_CONFIG_FILE"); got != configPath {
t.Fatalf("TF_CLI_CONFIG_FILE = %q, want %q", got, configPath)
}
if _, err := os.Stat(configPath); err != nil {
t.Fatalf("network mirror config should exist: %v", err)
}
}

func TestExecInitWithHermitcrabRetriesWithoutMirrorConfigWhenInitFails(t *testing.T) {
conf := testRunnerConfig(t)
conf.Hermitcrab.Enabled = true
_ = os.Unsetenv("TF_CLI_CONFIG_FILE")
t.Cleanup(func() {
_ = os.Unsetenv("TF_CLI_CONFIG_FILE")
})

runnerInstance := New(conf)
if err := runnerInstance.EnableHermitcrab(); err != nil {
t.Fatalf("EnableHermitcrab() error = %v", err)
}

exec := &fakeExec{initErrs: []error{errors.New("mirror init failed"), nil}}
runnerInstance.exec = exec
runnerInstance.workingDir = filepath.Join(conf.Runner.RepositoryPath, "content")

if err := runnerInstance.ExecInit(); err != nil {
t.Fatalf("ExecInit() error = %v", err)
}
expectedCalls := []string{runnerInstance.workingDir, runnerInstance.workingDir}
if !reflect.DeepEqual(exec.initCalls, expectedCalls) {
t.Fatalf("Init() calls = %v, want %v", exec.initCalls, expectedCalls)
}
if got := os.Getenv("TF_CLI_CONFIG_FILE"); got != "" {
t.Fatalf("TF_CLI_CONFIG_FILE = %q, want empty", got)
}
if _, err := os.Stat(filepath.Join(conf.Runner.RepositoryPath, "config.tfrc")); !os.IsNotExist(err) {
t.Fatalf("network mirror config should be removed, stat error = %v", err)
}
}

func TestExecInitWithHermitcrabReturnsRetryErrorWhenDirectInitFails(t *testing.T) {
conf := testRunnerConfig(t)
conf.Hermitcrab.Enabled = true
_ = os.Unsetenv("TF_CLI_CONFIG_FILE")
t.Cleanup(func() {
_ = os.Unsetenv("TF_CLI_CONFIG_FILE")
})

runnerInstance := New(conf)
if err := runnerInstance.EnableHermitcrab(); err != nil {
t.Fatalf("EnableHermitcrab() error = %v", err)
}

retryErr := errors.New("direct init failed")
exec := &fakeExec{initErrs: []error{errors.New("mirror init failed"), retryErr}}
runnerInstance.exec = exec
runnerInstance.workingDir = filepath.Join(conf.Runner.RepositoryPath, "content")

if err := runnerInstance.ExecInit(); !errors.Is(err, retryErr) {
t.Fatalf("ExecInit() error = %v, want %v", err, retryErr)
}
expectedCalls := []string{runnerInstance.workingDir, runnerInstance.workingDir}
if !reflect.DeepEqual(exec.initCalls, expectedCalls) {
t.Fatalf("Init() calls = %v, want %v", exec.initCalls, expectedCalls)
}
if got := os.Getenv("TF_CLI_CONFIG_FILE"); got != "" {
t.Fatalf("TF_CLI_CONFIG_FILE = %q, want empty", got)
}
if _, err := os.Stat(filepath.Join(conf.Runner.RepositoryPath, "config.tfrc")); !os.IsNotExist(err) {
t.Fatalf("network mirror config should be removed, stat error = %v", err)
}
}

func TestExecInitWithoutHermitcrabReturnsInitErrorWithoutRetrying(t *testing.T) {
conf := testRunnerConfig(t)
initErr := errors.New("init failed")
exec := &fakeExec{initErrs: []error{initErr}}
runnerInstance := New(conf)
runnerInstance.exec = exec
runnerInstance.workingDir = filepath.Join(conf.Runner.RepositoryPath, "content")

if err := runnerInstance.ExecInit(); !errors.Is(err, initErr) {
t.Fatalf("ExecInit() error = %v, want %v", err, initErr)
}
expectedCalls := []string{runnerInstance.workingDir}
if !reflect.DeepEqual(exec.initCalls, expectedCalls) {
t.Fatalf("Init() calls = %v, want %v", exec.initCalls, expectedCalls)
}
}
10 changes: 10 additions & 0 deletions internal/runner/runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,16 @@ func (r *Runner) EnableHermitcrab() error {
return err
}

// Disable Hermitcrab network mirror configuration.
func (r *Runner) DisableHermitcrab() error {
log.Infof("removing Hermitcrab network mirror configuration...")
err := runnerutils.RemoveNetworkMirrorConfig(r.config.Runner.RepositoryPath)
if err != nil {
log.Errorf("error removing network mirror configuration: %s", err)
}
return err
}

// Retrieve linked resources (layer, run, repository) from the Kubernetes API.
func (r *Runner) GetResources() error {
layer := &configv1alpha1.TerraformLayer{}
Expand Down
29 changes: 29 additions & 0 deletions internal/runner/runner_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"os"
"path/filepath"
"strings"
"testing"

. "github.com/onsi/ginkgo/v2"
Expand Down Expand Up @@ -31,11 +32,39 @@ const binaryPath string = "bin/tenv-binaries"
const repositoryPath string = "test.out/runner-repository"

func TestRunner(t *testing.T) {
if !envtestAssetsConfigured() {
t.Skip("envtest is not configured; run make test or set USE_EXISTING_CLUSTER, KUBEBUILDER_ASSETS, or TEST_ASSET_*")
}

RegisterFailHandler(Fail)

RunSpecs(t, "Runner Suite")
}

func envtestAssetsConfigured() bool {
if strings.EqualFold(os.Getenv("USE_EXISTING_CLUSTER"), "true") {
return true
}

if os.Getenv("KUBEBUILDER_ASSETS") != "" {
return true
}

for binary, assetEnv := range map[string]string{
"etcd": "TEST_ASSET_ETCD",
"kube-apiserver": "TEST_ASSET_KUBE_APISERVER",
"kubectl": "TEST_ASSET_KUBECTL",
} {
if os.Getenv(assetEnv) != "" {
continue
}
if _, err := os.Stat(filepath.Join("/usr/local/kubebuilder/bin", binary)); err != nil {
return false
}
}
return true
}

var _ = BeforeSuite(func() {
logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true)))
By("bootstrapping test environment")
Expand Down
21 changes: 20 additions & 1 deletion internal/utils/runner/network_mirror.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
package runner

import (
"errors"
"fmt"
"os"
"path/filepath"

log "github.com/sirupsen/logrus"
)

const networkMirrorConfigFile = "config.tfrc"

// Creates a network mirror configuration file for Terraform with the given endpoint
func CreateNetworkMirrorConfig(targetPath string, endpoint string) error {
terraformrcContent := fmt.Sprintf(`
Expand All @@ -15,7 +19,7 @@ provider_installation {
url = "%s"
}
}`, endpoint)
filePath := fmt.Sprintf("%s/config.tfrc", targetPath)
filePath := filepath.Join(targetPath, networkMirrorConfigFile)
err := os.WriteFile(filePath, []byte(terraformrcContent), 0644)
if err != nil {
return err
Expand All @@ -27,3 +31,18 @@ provider_installation {
log.Infof("network mirror configuration created")
return nil
}

// RemoveNetworkMirrorConfig removes the generated network mirror configuration file.
func RemoveNetworkMirrorConfig(targetPath string) error {
filePath := filepath.Join(targetPath, networkMirrorConfigFile)
err := os.Remove(filePath)
if err != nil && !errors.Is(err, os.ErrNotExist) {
return err
}
err = os.Unsetenv("TF_CLI_CONFIG_FILE")
if err != nil {
return err
}
log.Infof("network mirror configuration removed")
return nil
}
20 changes: 20 additions & 0 deletions internal/utils/runner/network_mirror_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package runner

import (
"os"
"testing"
)

func TestRemoveNetworkMirrorConfigMissingFile(t *testing.T) {
_ = os.Setenv("TF_CLI_CONFIG_FILE", "stale-config")
t.Cleanup(func() {
_ = os.Unsetenv("TF_CLI_CONFIG_FILE")
})

if err := RemoveNetworkMirrorConfig(t.TempDir()); err != nil {
t.Fatalf("RemoveNetworkMirrorConfig() error = %v", err)
}
if got := os.Getenv("TF_CLI_CONFIG_FILE"); got != "" {
t.Fatalf("TF_CLI_CONFIG_FILE = %q, want empty", got)
}
}