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
33 changes: 25 additions & 8 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,19 @@ on:

jobs:
lint:
runs-on: ubuntu-latest
name: Lint (${{ matrix.os }})
runs-on: ${{ matrix.os }}-latest
strategy:
fail-fast: false
matrix:
# Windows doesn't get to be linted continuously because it annoyingly requires
# that all files use \r\n line endings.
# os: [ubuntu, macos, windows]
os: [ubuntu, macos]

steps:
- uses: actions/checkout@v6

- id: go-version
run: |
go version

- name: Set up mise
uses: jdx/mise-action@v2
with:
Expand All @@ -24,12 +28,25 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

- id: go-cache-paths
shell: bash
run: |
echo "go-build=$(go env GOCACHE)" >> "$GITHUB_OUTPUT"
echo "go-mod=$(go env GOMODCACHE)" >> "$GITHUB_OUTPUT"

# TODO: Make this less brittle.
echo "golanci-lint-cache=/home/runner/.cache/golangci-lint" >> "$GITHUB_OUTPUT"
if [ "$RUNNER_OS" == "Windows" ]; then
echo "golangci-lint-cache=$LOCALAPPDATA/golangci-lint" >> "$GITHUB_OUTPUT"
exit 0
fi

if [ "$RUNNER_OS" == "macOS" ]; then
echo "golangci-lint-cache=$HOME/Library/Caches/golangci-lint" >> "$GITHUB_OUTPUT"
exit 0
fi

if [ "$RUNNER_OS" == "Linux" ]; then
echo "golangci-lint-cache=$HOME/.cache/golangci-lint" >> "$GITHUB_OUTPUT"
exit 0
fi

- name: Go Build Cache
uses: actions/cache@v5
Expand All @@ -46,7 +63,7 @@ jobs:
- name: golangci-lint Cache
uses: actions/cache@v5
with:
path: ${{ steps.go-cache-paths.outputs.golanci-lint-cache }}
path: ${{ steps.go-cache-paths.outputs.golangci-lint-cache }}
key: ${{ runner.os }}-golangci-lint-${{ hashFiles('**/go.sum') }}-linux-amd64

- name: Lint
Expand Down
13 changes: 12 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,19 @@ terragrunt: $(shell find . \( -type d -name 'vendor' -prune \) \
clean:
rm -f terragrunt

IGNORE_TAGS := windows|linux|darwin|freebsd|openbsd|netbsd|dragonfly|solaris|plan9|js|wasip1|aix|android|illumos|ios|386|amd64|arm|arm64|mips|mips64|mips64le|mipsle|ppc64|ppc64le|riscv64|s390x|wasm

LINT_TAGS := $(shell grep -rh 'go:build' . | \
sed 's/.*go:build\s*//' | \
tr -cs '[:alnum:]_' '\n' | \
grep -vE '^($(IGNORE_TAGS))$$' | \
sed '/^$$/d' | \
sort -u | \
paste -sd, -)

run-lint:
golangci-lint run -v --timeout=10m ./...
@echo "Linting with feature flags: [$(LINT_TAGS)]"
GOFLAGS="-tags=$(LINT_TAGS)" golangci-lint run -v --timeout=10m ./...

run-strict-lint:
golangci-lint run -v --timeout=10m -c .strict.golangci.yml --new-from-rev origin/main ./...
Expand Down
1 change: 1 addition & 0 deletions internal/tf/getproviders/lock_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ func mockProviderUpdateLock(t *testing.T, ctrl *gomock.Controller, address, vers
hasher := sha256.New()
_, err := hasher.Write([]byte(packageName))
require.NoError(t, err)

sha := hex.EncodeToString(hasher.Sum(nil))
document += fmt.Sprintf("%s %s\n", sha, packageName)
}
Expand Down
8 changes: 4 additions & 4 deletions test/integration_aws_oidc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@ const (
func TestAwsAssumeRoleWebIdentityFile(t *testing.T) {
// t.Parallel() cannot be used together with t.Setenv()
// t.Parallel()

token := fetchGitHubOIDCToken(t)

// These tests need to be run without the static key + secret
Expand Down Expand Up @@ -94,7 +93,6 @@ func TestAwsAssumeRoleWebIdentityFile(t *testing.T) {
func TestAwsAssumeRoleWebIdentityFlag(t *testing.T) {
// t.Parallel() cannot be used together with t.Setenv()
// t.Parallel()

token := fetchGitHubOIDCToken(t)

// These tests need to be run without the static key + secret
Expand All @@ -121,7 +119,6 @@ func TestAwsAssumeRoleWebIdentityFlag(t *testing.T) {
func TestAwsReadTerragruntAuthProviderCmdWithOIDC(t *testing.T) {
// t.Parallel() cannot be used together with t.Setenv()
// t.Parallel()

token := fetchGitHubOIDCToken(t)

t.Setenv("OIDC_TOKEN", token)
Expand All @@ -137,7 +134,6 @@ func TestAwsReadTerragruntAuthProviderCmdWithOIDC(t *testing.T) {
func TestAwsReadTerragruntAuthProviderCmdWithOIDCRemoteState(t *testing.T) {
// t.Parallel() cannot be used together with t.Setenv()
// t.Parallel()

token := fetchGitHubOIDCToken(t)

// These tests need to be run without the static key + secret
Expand Down Expand Up @@ -223,23 +219,27 @@ func fetchGitHubOIDCToken(t *testing.T) string {

resp, err := client.Do(req)
require.NoError(t, err, "Failed to execute OIDC token request to %s", requestURL)

defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
bodyBytes, readErr := io.ReadAll(resp.Body)
if readErr != nil {
t.Fatalf("OIDC token request to %s failed with status %s. Additionally, failed to read response body: %v", requestURL, resp.Status, readErr)
}

t.Fatalf("OIDC token request to %s failed with status %s. Response: %s", requestURL, resp.Status, string(bodyBytes))
}

body, err := io.ReadAll(resp.Body)
require.NoError(t, err, "Failed to read OIDC token response body from %s", requestURL)

var tokenResp oidcTokenResponse

err = json.Unmarshal(body, &tokenResp)
require.NoError(t, err, "Failed to unmarshal OIDC token response JSON from %s. Response: %s", requestURL, string(body))

require.NotEmpty(t, tokenResp.Value, "OIDC token 'value' field is empty in response from %s. Response: %s", requestURL, string(body))

return tokenResp.Value
}
2 changes: 2 additions & 0 deletions test/integration_aws_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,7 @@ func TestAwsBootstrapBackendWithoutVersioning(t *testing.T) {
// Add skip_bucket_versioning to disable_versioning feature
contents, err := util.ReadFileAsString(dualLockingConfigPath)
require.NoError(t, err)

anchorText := " enable_lock_table_ssencryption = feature.enable_lock_table_ssencryption.value"
require.Contains(t, contents, anchorText, "Expected anchor text not found in %s", dualLockingConfigPath)
newContents := strings.ReplaceAll(contents, anchorText, anchorText+"\n skip_bucket_versioning = true")
Expand All @@ -315,6 +316,7 @@ func TestAwsBootstrapBackendWithoutVersioning(t *testing.T) {
// Add skip_bucket_versioning for disable_versioning feature
contents, err = util.ReadFileAsString(useLockfileConfigPath)
require.NoError(t, err)

anchorText = " use_lockfile = true"
require.Contains(t, contents, anchorText, "Expected anchor text not found in %s", useLockfileConfigPath)
newContents = strings.ReplaceAll(contents, anchorText, anchorText+"\n skip_bucket_versioning = true")
Expand Down
19 changes: 13 additions & 6 deletions test/integration_engine_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@ func TestEngineChecksumVerification(t *testing.T) {
// open the file and write some data
file, err := os.OpenFile(fullPath, os.O_APPEND|os.O_WRONLY, 0600)
require.NoError(t, err)

nonExecutableData := []byte{0x00}
if _, err := file.Write(nonExecutableData); err != nil {
require.NoError(t, err)
Expand All @@ -186,12 +187,14 @@ func TestEngineDisableChecksumCheck(t *testing.T) {
if err != nil {
return err
}

if strings.HasSuffix(filepath.Base(path), "_SHA256SUMS") {
// clean checksum list
if err := os.Truncate(path, 0); err != nil {
return err
}
}

return nil
})
require.NoError(t, err)
Expand Down Expand Up @@ -291,14 +294,14 @@ func TestNoEngineFlagDisablesEngine(t *testing.T) {
rootPath := filepath.Join(tmpEnvPath, testFixtureOpenTofuEngine)

// First, verify engine is used when experiment is enabled
stdout, stderr, err := helpers.RunTerragruntCommandWithOutput(t, "terragrunt plan --non-interactive --tf-forward-stdout --working-dir "+rootPath)
_, stderr, err := helpers.RunTerragruntCommandWithOutput(t, "terragrunt plan --non-interactive --tf-forward-stdout --working-dir "+rootPath)
require.NoError(t, err)
assert.Contains(t, stderr, "Tofu Initialization started")
assert.Contains(t, stderr, "Tofu Initialization completed")
assert.Contains(t, stderr, "Tofu Shutdown completed")

// Then, verify engine is NOT used when --no-engine flag is set
stdout, stderr, err = helpers.RunTerragruntCommandWithOutput(t, "terragrunt plan --non-interactive --tf-forward-stdout --no-engine --working-dir "+rootPath)
stdout, stderr, err := helpers.RunTerragruntCommandWithOutput(t, "terragrunt plan --non-interactive --tf-forward-stdout --no-engine --working-dir "+rootPath)
require.NoError(t, err)
assert.NotContains(t, stderr, "Tofu Initialization started")
assert.NotContains(t, stderr, "Tofu Initialization completed")
Expand All @@ -313,14 +316,14 @@ func TestNoEngineFlagWithExperimentFlag(t *testing.T) {
rootPath := filepath.Join(tmpEnvPath, testFixtureOpenTofuEngine)

// Verify engine is used when --experiment iac-engine is set
stdout, stderr, err := helpers.RunTerragruntCommandWithOutput(t, "terragrunt plan --non-interactive --tf-forward-stdout --experiment iac-engine --working-dir "+rootPath)
_, stderr, err := helpers.RunTerragruntCommandWithOutput(t, "terragrunt plan --non-interactive --tf-forward-stdout --experiment iac-engine --working-dir "+rootPath)
require.NoError(t, err)
assert.Contains(t, stderr, "Tofu Initialization started")
assert.Contains(t, stderr, "Tofu Initialization completed")
assert.Contains(t, stderr, "Tofu Shutdown completed")

// Verify engine is NOT used when both --experiment iac-engine and --no-engine are set
stdout, stderr, err = helpers.RunTerragruntCommandWithOutput(t, "terragrunt plan --non-interactive --tf-forward-stdout --experiment iac-engine --no-engine --working-dir "+rootPath)
stdout, stderr, err := helpers.RunTerragruntCommandWithOutput(t, "terragrunt plan --non-interactive --tf-forward-stdout --experiment iac-engine --no-engine --working-dir "+rootPath)
require.NoError(t, err)
assert.NotContains(t, stderr, "Tofu Initialization started")
assert.NotContains(t, stderr, "Tofu Initialization completed")
Expand All @@ -337,14 +340,14 @@ func TestNoEngineFlagWithRunAll(t *testing.T) {
rootPath := filepath.Join(tmpEnvPath, testFixtureOpenTofuRunAll)

// Verify engine is used in run --all when experiment is enabled
stdout, stderr, err := helpers.RunTerragruntCommandWithOutput(t, fmt.Sprintf("terragrunt run --all --non-interactive --tf-forward-stdout --working-dir %s -- plan -no-color", rootPath))
_, stderr, err := helpers.RunTerragruntCommandWithOutput(t, fmt.Sprintf("terragrunt run --all --non-interactive --tf-forward-stdout --working-dir %s -- plan -no-color", rootPath))
require.NoError(t, err)
assert.Contains(t, stderr, "Tofu Initialization started")
assert.Contains(t, stderr, "Tofu Initialization completed")
assert.Contains(t, stderr, "Tofu Shutdown completed")

// Verify engine is NOT used in run --all when --no-engine is set
stdout, stderr, err = helpers.RunTerragruntCommandWithOutput(t, fmt.Sprintf("terragrunt run --all --non-interactive --tf-forward-stdout --no-engine --working-dir %s -- plan -no-color", rootPath))
stdout, stderr, err := helpers.RunTerragruntCommandWithOutput(t, fmt.Sprintf("terragrunt run --all --non-interactive --tf-forward-stdout --no-engine --working-dir %s -- plan -no-color", rootPath))
require.NoError(t, err)
assert.NotContains(t, stderr, "Tofu Initialization started")
assert.NotContains(t, stderr, "Tofu Initialization completed")
Expand All @@ -362,6 +365,7 @@ func setupEngineCache(t *testing.T) (string, string) {
helpers.CleanupTerraformFolder(t, testFixtureOpenTofuRunAll)
tmpEnvPath := helpers.CopyEnvironment(t, testFixtureOpenTofuRunAll)
rootPath := filepath.Join(tmpEnvPath, testFixtureOpenTofuRunAll)

return cacheDir, rootPath
}

Expand All @@ -379,12 +383,14 @@ func setupLocalEngine(t *testing.T) string {
if err := os.MkdirAll(engineDir, 0755); err != nil {
require.NoError(t, err)
}

_, err := getter.GetAny(t.Context(), engineDir, downloadURL)
require.NoError(t, err)

helpers.CopyAndFillMapPlaceholders(t, filepath.Join(testFixtureLocalEngine, "terragrunt.hcl"), filepath.Join(rootPath, config.DefaultTerragruntConfigPath), map[string]string{
"__engine_source__": filepath.Join(engineDir, engineAssetName),
})

return rootPath
}

Expand All @@ -394,5 +400,6 @@ func testEngineVersion() string {
if !found {
return "v0.0.16"
}

return value
}
2 changes: 2 additions & 0 deletions test/integration_parse_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -237,9 +237,11 @@ func TestParseFindListAllComponentsWithDAGAndExternal(t *testing.T) {
if aDepLine >= 0 && bDepLine >= 0 {
assert.Greater(t, aDepLine, bDepLine, "a-dependent should come after b-dependency")
}

if dDepsLine >= 0 && bDepLine >= 0 {
assert.Greater(t, dDepsLine, bDepLine, "d-dependencies-only should come after b-dependency")
}

if cMixedLine >= 0 && aDepLine >= 0 && dDepsLine >= 0 {
assert.Greater(t, cMixedLine, aDepLine, "c-mixed-deps should come after a-dependent")
assert.Greater(t, cMixedLine, dDepsLine, "c-mixed-deps should come after d-dependencies-only")
Expand Down
11 changes: 5 additions & 6 deletions test/integration_private_registry_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
package test_test

import (
"fmt"
"net/url"
"os"
"path/filepath"
Expand All @@ -24,17 +23,17 @@ func setupPrivateRegistryTest(t *testing.T) (string, string, string) {
registryToken := os.Getenv("PRIVATE_REGISTRY_TOKEN")

// the private registry test is recommended to be a clone of gruntwork-io/terraform-null-terragrunt-registry-test
registryUrl := os.Getenv("PRIVATE_REGISTRY_URL")
registryURL := os.Getenv("PRIVATE_REGISTRY_URL")

if registryToken == "" || registryUrl == "" {
if registryToken == "" || registryURL == "" {
t.Skip("Skipping test because it requires a valid Terraform registry token and url")
}

helpers.CleanupTerraformFolder(t, privateRegistryFixturePath)
tmpEnvPath := helpers.CopyEnvironment(t, privateRegistryFixturePath)
rootPath := filepath.Join(tmpEnvPath, privateRegistryFixturePath)

URL, err := url.Parse("tfr://" + registryUrl)
URL, err := url.Parse("tfr://" + registryURL)
if err != nil {
t.Fatalf("REGISTRY_URL is invalid: %v", err)
}
Expand All @@ -44,7 +43,7 @@ func setupPrivateRegistryTest(t *testing.T) (string, string, string) {
}

helpers.CopyAndFillMapPlaceholders(t, filepath.Join(privateRegistryFixturePath, "terragrunt.hcl"), filepath.Join(rootPath, "terragrunt.hcl"), map[string]string{
"__registry_url__": registryUrl,
"__registry_url__": registryURL,
})

return rootPath, URL.Hostname(), registryToken
Expand Down Expand Up @@ -73,7 +72,7 @@ func TestPrivateRegistryWithEnvToken(t *testing.T) {
// This is based on the tf/cliconfig/credentials.go collectCredentialsFromEnv
host = strings.ReplaceAll(strings.ReplaceAll(host, ".", "_"), "-", "__")

t.Setenv(fmt.Sprintf("TF_TOKEN_%s", host), token)
t.Setenv("TF_TOKEN_"+host, token)

_, _, err := helpers.RunTerragruntCommandWithOutput(t, "terragrunt init --non-interactive --log-level=trace --working-dir="+rootPath)

Expand Down
1 change: 1 addition & 0 deletions test/integration_scaffold_ssh_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ SourceUrlType: "git-ssh"
require.NoError(t, err)
assert.Contains(t, stderr, "git::ssh://git@github.com/gruntwork-io/terragrunt.git//test/fixtures/inputs?ref=v0.67.4")
assert.Contains(t, stderr, "Scaffolding completed")

content, err := util.ReadFileAsString(tmpEnvPath + "/terragrunt.hcl")
require.NoError(t, err)
assert.NotContains(t, content, "find_in_parent_folders")
Expand Down
7 changes: 7 additions & 0 deletions test/integration_tflint_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ func TestTflintInitSameModule(t *testing.T) {
t.Cleanup(func() {
helpers.RemoveFolder(t, rootPath)
})

modulePath := filepath.Join(rootPath, testFixtureParallelRun)
runPath := filepath.Join(rootPath, testFixtureParallelRun, "dev")
appTemplate := filepath.Join(rootPath, testFixtureParallelRun, "dev", "app")
Expand All @@ -104,6 +105,7 @@ func TestTflintInitSameModule(t *testing.T) {
err := util.CopyFolderContents(createLogger(), appTemplate, appPath, ".terragrunt-test", []string{}, []string{})
require.NoError(t, err)
}

helpers.RunTerragrunt(t, "terragrunt run --all init --log-level trace --non-interactive --working-dir "+runPath)
}

Expand All @@ -117,6 +119,7 @@ func TestTflintFindsNoIssuesWithValidCodeDifferentDownloadDir(t *testing.T) {
t.Cleanup(func() {
helpers.RemoveFolder(t, rootPath)
})

modulePath := filepath.Join(rootPath, testFixtureTflintNoIssuesFound)
err := helpers.RunTerragruntCommand(t, fmt.Sprintf("terragrunt plan --log-level trace --working-dir %s --download-dir %s", modulePath, downloadDir), out, errOut)
require.NoError(t, err)
Expand All @@ -140,6 +143,7 @@ func TestTflintExternalTflint(t *testing.T) {
t.Cleanup(func() {
helpers.RemoveFolder(t, rootPath)
})

runPath := filepath.Join(rootPath, testFixtureTflintExternalTflint)
err := helpers.RunTerragruntCommand(t, "terragrunt plan --log-level trace --working-dir "+runPath, out, errOut)
require.NoError(t, err)
Expand All @@ -156,6 +160,7 @@ func TestTflintTfvarsArePassedToTflint(t *testing.T) {
t.Cleanup(func() {
helpers.RemoveFolder(t, rootPath)
})

runPath := filepath.Join(rootPath, testFixtureTflintTfvarPassing)
err := helpers.RunTerragruntCommand(t, "terragrunt plan --log-level trace --working-dir "+runPath, out, errOut)
require.NoError(t, err)
Expand All @@ -172,6 +177,7 @@ func TestTflintArgumentsPassedIn(t *testing.T) {
t.Cleanup(func() {
helpers.RemoveFolder(t, rootPath)
})

runPath := filepath.Join(rootPath, testFixtureTflintArgs)
err := helpers.RunTerragruntCommand(t, "terragrunt plan --log-level trace --working-dir "+runPath, out, errOut)
require.NoError(t, err)
Expand All @@ -188,6 +194,7 @@ func TestTflintCustomConfig(t *testing.T) {
t.Cleanup(func() {
helpers.RemoveFolder(t, rootPath)
})

runPath := filepath.Join(rootPath, testFixtureTflintCustomConfig)
err := helpers.RunTerragruntCommand(t, "terragrunt plan --log-level trace --working-dir "+runPath, out, errOut)
require.NoError(t, err)
Expand Down
Loading