Skip to content

Commit

Permalink
fix: Caching the same provider with different versions from various s…
Browse files Browse the repository at this point in the history
…ources (#3935)

* fix: caching the same provider from different sources

* chore: update test

* fix: addressed to coderabbitai review
  • Loading branch information
levkohimins authored Feb 27, 2025
1 parent 27dd83b commit ab8a2f4
Show file tree
Hide file tree
Showing 31 changed files with 805 additions and 499 deletions.
40 changes: 7 additions & 33 deletions cli/provider_cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,47 +96,21 @@ func InitProviderCacheServer(opts *options.TerragruntOptions) (*ProviderCache, e
}

providerService := services.NewProviderService(opts.ProviderCacheDir, userProviderDir, cliCfg.CredentialsSource(), opts.Logger)
proxyProviderHandler := handlers.NewProxyProviderHandler(opts.Logger, cliCfg.CredentialsSource())

var (
providerHandlers = make([]handlers.ProviderHandler, 0, len(cliCfg.ProviderInstallation.Methods))
excludeAddrs = make([]string, 0, len(cliCfg.ProviderInstallation.Methods))
directIsdefined bool
)

for _, registryName := range opts.ProviderCacheRegistryNames {
excludeAddrs = append(excludeAddrs, registryName+"/*/*")
}

for _, method := range cliCfg.ProviderInstallation.Methods {
switch method := method.(type) {
case *cliconfig.ProviderInstallationFilesystemMirror:
providerHandlers = append(providerHandlers, handlers.NewProviderFilesystemMirrorHandler(providerService, CacheProviderHTTPStatusCode, method))
case *cliconfig.ProviderInstallationNetworkMirror:
networkMirrorHandler, err := handlers.NewProviderNetworkMirrorHandler(providerService, CacheProviderHTTPStatusCode, method, cliCfg.CredentialsSource())
if err != nil {
return nil, err
}

providerHandlers = append(providerHandlers, networkMirrorHandler)
case *cliconfig.ProviderInstallationDirect:
providerHandlers = append(providerHandlers, handlers.NewProviderDirectHandler(providerService, CacheProviderHTTPStatusCode, method, cliCfg.CredentialsSource()))
directIsdefined = true
}

method.AppendExclude(excludeAddrs)
}

if !directIsdefined {
// In a case if none of direct provider installation methods `cliCfg.ProviderInstallation.Methods` are specified.
providerHandlers = append(providerHandlers, handlers.NewProviderDirectHandler(providerService, CacheProviderHTTPStatusCode, new(cliconfig.ProviderInstallationDirect), cliCfg.CredentialsSource()))
providerHandlers, err := handlers.NewProviderHandlers(cliCfg, opts.Logger, opts.ProviderCacheRegistryNames)
if err != nil {
return nil, errors.Errorf("creating provider handlers failed: %w", err)
}

cache := cache.NewServer(
cache.WithHostname(opts.ProviderCacheHostname),
cache.WithPort(opts.ProviderCachePort),
cache.WithToken(opts.ProviderCacheToken),
cache.WithServices(providerService),
cache.WithProviderService(providerService),
cache.WithProviderHandlers(providerHandlers...),
cache.WithProxyProviderHandler(proxyProviderHandler),
cache.WithCacheProviderHTTPStatusCode(CacheProviderHTTPStatusCode),
cache.WithLogger(opts.Logger),
)

Expand Down
14 changes: 10 additions & 4 deletions cli/provider_cache_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ func TestProviderCache(t *testing.T) {
providerCacheDir := t.TempDir()
pluginCacheDir := t.TempDir()

opts := []cache.Option{cache.WithToken(token)}
opts := []cache.Option{cache.WithToken(token), cache.WithCacheProviderHTTPStatusCode(cli.CacheProviderHTTPStatusCode)}

testCases := []struct {
opts []cache.Option
Expand Down Expand Up @@ -112,11 +112,17 @@ func TestProviderCache(t *testing.T) {
defer cancel()

errGroup, ctx := errgroup.WithContext(ctx)
logger := log.New()

providerService := services.NewProviderService(providerCacheDir, pluginCacheDir, nil, log.New())
providerHandler := handlers.NewProviderDirectHandler(providerService, cli.CacheProviderHTTPStatusCode, new(cliconfig.ProviderInstallationDirect), nil)
providerService := services.NewProviderService(providerCacheDir, pluginCacheDir, nil, logger)
providerHandler := handlers.NewDirectProviderHandler(logger, new(cliconfig.ProviderInstallationDirect), nil)
proxyProviderHandler := handlers.NewProxyProviderHandler(logger, nil)

testCase.opts = append(testCase.opts, cache.WithServices(providerService), cache.WithProviderHandlers(providerHandler))
testCase.opts = append(testCase.opts,
cache.WithProviderService(providerService),
cache.WithProviderHandlers(providerHandler),
cache.WithProxyProviderHandler(proxyProviderHandler),
)

server := cache.NewServer(testCase.opts...)
ln, err := server.Listen()
Expand Down
6 changes: 6 additions & 0 deletions pkg/log/format/format.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,12 @@ func NewPrettyFormatPlaceholders() Placeholders {
Message(
PathFormat(RelativePath),
),
Field(CacheServerURLKeyName,
Prefix(" "+CacheServerURLKeyName+"="),
),
Field(CacheServerStatusKeyName,
Prefix(" "+CacheServerStatusKeyName+"="),
),
}
}

Expand Down
4 changes: 4 additions & 0 deletions pkg/log/format/placeholders/field.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ const (
TFPathKeyName = "tf-path"
TFCmdArgsKeyName = "tf-command-args"
TFCmdKeyName = "tf-command"

// Terragrunt Provider Cache Server fields.
CacheServerURLKeyName = "url"
CacheServerStatusKeyName = "status"
)

type fieldPlaceholder struct {
Expand Down
8 changes: 8 additions & 0 deletions test/fixtures/provider-cache/network-mirror/apps/app1/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
terraform {
required_providers {
aws = {
source = "example.com/hashicorp/aws"
version = "5.58.0"
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Intentionally empty
28 changes: 20 additions & 8 deletions test/integration_serial_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ func TestTerragruntProviderCacheWithNetworkMirror(t *testing.T) {
tmpEnvPath := helpers.CopyEnvironment(t, testFixtureProviderCacheNetworkMirror)
rootPath := util.JoinPath(tmpEnvPath, testFixtureProviderCacheNetworkMirror)

appPath := filepath.Join(rootPath, "app")
appsPath := filepath.Join(rootPath, "apps")
providersNetkworMirrorPath := filepath.Join(rootPath, "providers-network-mirror")
providersFilesystemMirrorPath := filepath.Join(rootPath, "providers-filesystem-mirror")

Expand All @@ -122,6 +122,16 @@ func TestTerragruntProviderCacheWithNetworkMirror(t *testing.T) {
netowrkProvider.CreateMirror(t, providersNetkworMirrorPath)

filesystemProvider := FakeProvider{
RegistryName: "example.com",
Namespace: "hashicorp",
Name: "aws",
Version: "5.58.0",
PlatformOS: runtime.GOOS,
PlatformArch: runtime.GOARCH,
}
filesystemProvider.CreateMirror(t, providersFilesystemMirrorPath)

filesystemProvider = FakeProvider{
RegistryName: "example.com",
Namespace: "hashicorp",
Name: "azurerm",
Expand Down Expand Up @@ -169,7 +179,7 @@ func TestTerragruntProviderCacheWithNetworkMirror(t *testing.T) {
FilesystemMirrorMethods: []test.CLIConfigProviderInstallationFilesystemMirror{
{
Path: providersFilesystemMirrorPath,
Include: []string{"example.com/hashicorp/azurerm"},
Include: []string{"example.com/hashicorp/azurerm", "example.com/hashicorp/aws"},
},
},
NetworkMirrorMethods: []test.CLIConfigProviderInstallationNetworkMirror{
Expand All @@ -181,16 +191,18 @@ func TestTerragruntProviderCacheWithNetworkMirror(t *testing.T) {
}
test.CreateCLIConfig(t, cliConfigFilename, cliConfigSettings)

helpers.RunTerragrunt(t, fmt.Sprintf("terragrunt run-all init --terragrunt-provider-cache --terragrunt-provider-cache-registry-names example.com --terragrunt-provider-cache-registry-names registry.opentofu.org --terragrunt-provider-cache-registry-names registry.terraform.io --terragrunt-provider-cache-dir %s --terragrunt-log-level trace --terragrunt-non-interactive --terragrunt-working-dir %s", providerCacheDir, appPath))
helpers.RunTerragrunt(t, fmt.Sprintf("terragrunt run-all init --terragrunt-provider-cache --terragrunt-provider-cache-registry-names example.com --terragrunt-provider-cache-registry-names registry.opentofu.org --terragrunt-provider-cache-registry-names registry.terraform.io --terragrunt-provider-cache-dir %s --terragrunt-log-level trace --terragrunt-non-interactive --terragrunt-working-dir %s", providerCacheDir, appsPath))

expectedProviderInstallation := `provider_installation { "filesystem_mirror" { path = "%s" include = ["example.com/hashicorp/azurerm"] exclude = ["example.com/*/*", "registry.opentofu.org/*/*", "registry.terraform.io/*/*"] } "network_mirror" { url = "%s" exclude = ["example.com/hashicorp/azurerm", "example.com/*/*", "registry.opentofu.org/*/*", "registry.terraform.io/*/*"] } "filesystem_mirror" { path = "%s" include = ["example.com/*/*", "registry.opentofu.org/*/*", "registry.terraform.io/*/*"] } "direct" { exclude = ["example.com/*/*", "registry.opentofu.org/*/*", "registry.terraform.io/*/*"] } }`
expectedProviderInstallation := `provider_installation { "filesystem_mirror" { path = "%s" include = ["example.com/hashicorp/azurerm", "example.com/hashicorp/aws"] exclude = ["example.com/*/*", "registry.opentofu.org/*/*", "registry.terraform.io/*/*"] } "network_mirror" { url = "%s" exclude = ["example.com/hashicorp/azurerm", "example.com/*/*", "registry.opentofu.org/*/*", "registry.terraform.io/*/*"] } "filesystem_mirror" { path = "%s" include = ["example.com/*/*", "registry.opentofu.org/*/*", "registry.terraform.io/*/*"] } "direct" { exclude = ["example.com/*/*", "registry.opentofu.org/*/*", "registry.terraform.io/*/*"] } }`
expectedProviderInstallation = fmt.Sprintf(strings.Join(strings.Fields(expectedProviderInstallation), " "), providersFilesystemMirrorPath, networkMirrorURL.String(), providerCacheDir)

terraformrcBytes, err := os.ReadFile(filepath.Join(appPath, ".terraformrc"))
require.NoError(t, err)
terraformrc := strings.Join(strings.Fields(string(terraformrcBytes)), " ")
for _, filename := range []string{"app0/.terraformrc", "app1/.terraformrc"} {
terraformrcBytes, err := os.ReadFile(filepath.Join(appsPath, filename))
require.NoError(t, err)
terraformrc := strings.Join(strings.Fields(string(terraformrcBytes)), " ")

assert.Contains(t, terraformrc, expectedProviderInstallation, "%s\n\n%s", terraformrc, expectedProviderInstallation)
assert.Contains(t, terraformrc, expectedProviderInstallation, "%s\n\n%s", terraformrc, expectedProviderInstallation)
}
}

func TestTerragruntInputsFromDependency(t *testing.T) {
Expand Down
24 changes: 20 additions & 4 deletions tf/cache/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,9 @@ func WithToken(token string) Option {
}
}

func WithServices(services ...services.Service) Option {
func WithProviderService(service *services.ProviderService) Option {
return func(cfg Config) Config {
cfg.services = services
cfg.providerService = service
return cfg
}
}
Expand All @@ -58,6 +58,20 @@ func WithProviderHandlers(handlers ...handlers.ProviderHandler) Option {
}
}

func WithProxyProviderHandler(handler *handlers.ProxyProviderHandler) Option {
return func(cfg Config) Config {
cfg.proxyProviderHandler = handler
return cfg
}
}

func WithCacheProviderHTTPStatusCode(statusCode int) Option {
return func(cfg Config) Config {
cfg.cacheProviderHTTPStatusCode = statusCode
return cfg
}
}

func WithLogger(logger log.Logger) Option {
return func(cfg Config) Config {
cfg.logger = logger
Expand All @@ -71,8 +85,10 @@ type Config struct {
token string
shutdownTimeout time.Duration

services []services.Service
providerHandlers handlers.ProviderHandlers
providerService *services.ProviderService
providerHandlers handlers.ProviderHandlers
proxyProviderHandler *handlers.ProxyProviderHandler
cacheProviderHTTPStatusCode int

logger log.Logger
}
Expand Down
17 changes: 11 additions & 6 deletions tf/cache/controllers/downloader.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"github.com/gruntwork-io/terragrunt/tf/cache/handlers"
"github.com/gruntwork-io/terragrunt/tf/cache/models"
"github.com/gruntwork-io/terragrunt/tf/cache/router"
"github.com/gruntwork-io/terragrunt/tf/cache/services"
"github.com/labstack/echo/v4"
)

Expand All @@ -17,7 +18,8 @@ const (
type DownloaderController struct {
*router.Router

ProviderHandlers []handlers.ProviderHandler
ProviderService *services.ProviderService
ProxyProviderHandler *handlers.ProxyProviderHandler
}

// Register implements router.Controller.Register
Expand Down Expand Up @@ -45,13 +47,16 @@ func (controller *DownloaderController) downloadProviderAction(ctx echo.Context)
},
}

for _, handler := range controller.ProviderHandlers {
if handler.CanHandleProvider(provider) {
if err := handler.Download(ctx, provider); err == nil {
break
}
if cache := controller.ProviderService.GetProviderCache(provider); cache != nil {
if path := cache.ArchivePath(); path != "" {
controller.ProviderService.Logger().Debugf("Download cached provider %s", cache.Provider)
return ctx.File(path)
}
}

if err := controller.ProxyProviderHandler.Download(ctx, provider); err != nil {
return err
}

return ctx.NoContent(http.StatusNotFound)
}
54 changes: 47 additions & 7 deletions tf/cache/controllers/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@ package controllers
import (
"net/http"

"github.com/gruntwork-io/terragrunt/pkg/log"
"github.com/gruntwork-io/terragrunt/tf/cache/handlers"
"github.com/gruntwork-io/terragrunt/tf/cache/models"
"github.com/gruntwork-io/terragrunt/tf/cache/router"
"github.com/gruntwork-io/terragrunt/tf/cache/services"
"github.com/labstack/echo/v4"
)

Expand All @@ -20,11 +22,15 @@ const (
type ProviderController struct {
*router.Router

Logger log.Logger
Server http.Server
DownloaderController router.Controller

AuthMiddleware echo.MiddlewareFunc
ProviderHandlers []handlers.ProviderHandler
AuthMiddleware echo.MiddlewareFunc
ProviderHandlers []handlers.ProviderHandler
ProxyProviderHandler *handlers.ProxyProviderHandler
ProviderService *services.ProviderService
CacheProviderHTTPStatusCode int
}

// Endpoints implements controllers.Endpointer.Endpoints
Expand Down Expand Up @@ -65,15 +71,30 @@ func (controller *ProviderController) getVersionsAction(ctx echo.Context) error
Name: name,
}

var allVersions models.Versions

for _, handler := range controller.ProviderHandlers {
if handler.CanHandleProvider(provider) {
if err := handler.GetVersions(ctx, provider); err == nil {
break
versions, err := handler.GetVersions(ctx.Request().Context(), provider)
if err != nil {
controller.Logger.Errorf("Failed to get provider versions from %q: %s", handler, err.Error())
}

if versions != nil {
allVersions = append(allVersions, versions...)
}
}
}

return ctx.NoContent(http.StatusNotFound)
versions := struct {
ID string `json:"id"`
Versions models.Versions `json:"versions"`
}{
ID: provider.Address(),
Versions: allVersions,
}

return ctx.JSON(http.StatusOK, versions)
}

func (controller *ProviderController) getPlatformsAction(ctx echo.Context) (er error) {
Expand All @@ -96,13 +117,32 @@ func (controller *ProviderController) getPlatformsAction(ctx echo.Context) (er e
Arch: arch,
}

if cacheRequestID == "" {
return controller.ProxyProviderHandler.GetPlatform(ctx, provider, controller.DownloaderController)
}

var (
resp *models.ResponseBody
err error
)

for _, handler := range controller.ProviderHandlers {
if handler.CanHandleProvider(provider) {
if err := handler.GetPlatform(ctx, provider, controller.DownloaderController, cacheRequestID); err == nil {
resp, err = handler.GetPlatform(ctx.Request().Context(), provider)
if err != nil {
controller.Logger.Errorf("Failed to get provider platform from %q: %s", handler, err.Error())
}

if resp != nil {
break
}
}
}

return ctx.NoContent(http.StatusNotFound)
provider.ResponseBody = resp

// start caching and return 423 status
controller.ProviderService.CacheProvider(ctx.Request().Context(), cacheRequestID, provider)

return ctx.NoContent(controller.CacheProviderHTTPStatusCode)
}
Loading

0 comments on commit ab8a2f4

Please sign in to comment.