Skip to content

feat: support credential tokens for getter #4047

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

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
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
3 changes: 3 additions & 0 deletions test/fixtures/private-registry/env.tfrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
credentials "__registry_host__" {
token = "__registry_token__"
}
4 changes: 4 additions & 0 deletions test/fixtures/private-registry/terragrunt.hcl
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Retrieve a module from the public terraform registry to use with terragrunt
terraform {
source = "tfr://__registry_url__"
}
83 changes: 83 additions & 0 deletions test/integration_private_registry_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
//go:build private_registry

package test_test

import (
"fmt"
"net/url"
"os"
"strings"
"testing"

"github.com/gruntwork-io/terragrunt/test/helpers"
"github.com/gruntwork-io/terragrunt/util"
"github.com/stretchr/testify/require"
)

const (
privateRegistryFixturePath = "fixtures/private-registry"
)

func setupPrivateRegistryTest(t *testing.T) (string, string, string) {
t.Helper()

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")

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 := util.JoinPath(tmpEnvPath, privateRegistryFixturePath)

URL, err := url.Parse("tfr://" + registryUrl)
if err != nil {
t.Fatalf("REGISTRY_URL is invalid: %v", err)
}

if URL.Hostname() == "" {
t.Fatal("REGISTRY_URL is invalid")
}

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

return rootPath, URL.Hostname(), registryToken
}

func TestPrivateRegistryWithConfgFileToken(t *testing.T) {
rootPath, host, token := setupPrivateRegistryTest(t)

helpers.CopyAndFillMapPlaceholders(t, util.JoinPath(privateRegistryFixturePath, "env.tfrc"), util.JoinPath(rootPath, "env.tfrc"), map[string]string{
"__registry_token__": token,
"__registry_host__": host,
})

t.Setenv("TF_CLI_CONFIG_FILE", util.JoinPath(rootPath, "env.tfrc"))

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

// the hashicorp/null provider errors on install, but that indicates that the private tfr module was downloaded
require.Contains(t, err.Error(), "hashicorp/null", "Error accessing the private registry")
}

func TestPrivateRegistryWithEnvToken(t *testing.T) {
rootPath, host, token := setupPrivateRegistryTest(t)

// Convert host to format suitable for Terraform env vars.
// 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)

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

// The main test is for authentication against the private registry, so if the null provider fails then we know
// that terragrunt authenticated and downloaded the module.
require.Contains(t, err.Error(), "hashicorp/null", "Error accessing the private registry")
}
27 changes: 24 additions & 3 deletions tf/getter.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,10 @@ import (
"github.com/hashicorp/go-cleanhttp"
"github.com/hashicorp/go-getter"
safetemp "github.com/hashicorp/go-safetemp"
svchost "github.com/hashicorp/terraform-svchost"

"github.com/gruntwork-io/terragrunt/internal/errors"
"github.com/gruntwork-io/terragrunt/tf/cliconfig"
"github.com/gruntwork-io/terragrunt/util"
)

Expand Down Expand Up @@ -325,6 +327,25 @@ func GetDownloadURLFromHeader(moduleURL url.URL, terraformGet string) (string, e
return terraformGet, nil
}

func applyHostToken(req *http.Request) (*http.Request, error) {
cliCfg, err := cliconfig.LoadUserConfig()
if err != nil {
return nil, err
}

if creds := cliCfg.CredentialsSource().ForHost(svchost.Hostname(req.URL.Hostname())); creds != nil {
creds.PrepareRequest(req)
} else {
// fall back to the TG_TF_REGISTRY_TOKEN
authToken := os.Getenv(authTokenEnvName)
if authToken != "" {
req.Header.Add("Authorization", "Bearer "+authToken)
}
}

return req, nil
}

// httpGETAndGetResponse is a helper function to make a GET request to the given URL using the http client. This
// function will then read the response and return the contents + the response header.
func httpGETAndGetResponse(ctx context.Context, logger log.Logger, getURL url.URL) ([]byte, *http.Header, error) {
Expand All @@ -335,9 +356,9 @@ func httpGETAndGetResponse(ctx context.Context, logger log.Logger, getURL url.UR

// Handle authentication via env var. Authentication is done by providing the registry token as a bearer token in
// the request header.
authToken := os.Getenv(authTokenEnvName)
if authToken != "" {
req.Header.Add("Authorization", "Bearer "+authToken)
req, err = applyHostToken(req)
if err != nil {
return nil, nil, errors.New(err)
}

resp, err := httpClient.Do(req)
Expand Down