diff --git a/.strict.golangci.yml b/.strict.golangci.yml index b79c801f9..3aab307b4 100644 --- a/.strict.golangci.yml +++ b/.strict.golangci.yml @@ -10,6 +10,9 @@ output: print-linter-name: true linters-settings: + wrapcheck: + ignorePackageGlobs: + - github.com/gruntwork-io/terragrunt/* lll: line-length: 140 staticcheck: diff --git a/cli/provider_cache.go b/cli/provider_cache.go index 19be81d6e..51735de49 100644 --- a/cli/provider_cache.go +++ b/cli/provider_cache.go @@ -2,11 +2,9 @@ package cli import ( "context" - liberrors "errors" "fmt" "io" "net/http" - "net/url" "os" "path/filepath" "regexp" @@ -16,6 +14,7 @@ import ( "github.com/google/uuid" "github.com/gruntwork-io/go-commons/errors" "github.com/gruntwork-io/terragrunt/options" + "github.com/gruntwork-io/terragrunt/pkg/cli" "github.com/gruntwork-io/terragrunt/shell" "github.com/gruntwork-io/terragrunt/terraform" "github.com/gruntwork-io/terragrunt/terraform/cache" @@ -147,23 +146,29 @@ func InitProviderCacheServer(opts *options.TerragruntOptions) (*ProviderCache, e }, nil } -func (cache *ProviderCache) TerraformCommandHook(ctx context.Context, opts *options.TerragruntOptions, args []string) (*util.CmdOutput, error) { +// TerraformCommandHook warms up the providers cache, creates `.terraform.lock.hcl` and runs the `tofu/terraform init` +// command with using this cache. Used as a hook function that is called after running the target tofu/terraform command. +// For example, if the target command is `tofu plan`, it will be intercepted before it is run in the `/shell` package, +// then control will be passed to this function to init the working directory using cached providers. +func (cache *ProviderCache) TerraformCommandHook( + ctx context.Context, + opts *options.TerragruntOptions, + args cli.Args, +) (*util.CmdOutput, error) { // To prevent a loop ctx = shell.ContextWithTerraformCommandHook(ctx, nil) var ( cliConfigFilename = filepath.Join(opts.WorkingDir, localCLIFilename) - cacheRequestID = uuid.New().String() - envs = providerCacheEnvironment(opts, cliConfigFilename) - commandsArgs = convertToMultipleCommandsByPlatforms(args) + env = providerCacheEnvironment(opts, cliConfigFilename) skipRunTargetCommand bool ) // Use Hook only for the `terraform init` command, which can be run explicitly by the user or Terragrunt's `auto-init` feature. switch { - case util.FirstArg(args) == terraform.CommandNameInit: + case args.CommandName() == terraform.CommandNameInit: // Provider caching for `terraform init` command. - case util.FirstArg(args) == terraform.CommandNameProviders && util.SecondArg(args) == terraform.CommandNameLock: + case args.CommandName() == terraform.CommandNameProviders && args.SubCommandName() == terraform.CommandNameLock: // Provider caching for `terraform providers lock` command. // Since the Terragrunt provider cache server creates the cache and generates the lock file, we don't need to run the `terraform providers lock ...` command at all. skipRunTargetCommand = true @@ -172,6 +177,29 @@ func (cache *ProviderCache) TerraformCommandHook(ctx context.Context, opts *opti return shell.RunTerraformCommandWithOutput(ctx, opts, args...) } + if output, err := cache.warmUpCache(ctx, opts, cliConfigFilename, args, env); err != nil { + return output, err + } + + if skipRunTargetCommand { + return &util.CmdOutput{}, nil + } + + return cache.runTerraformWithCache(ctx, opts, cliConfigFilename, args, env) +} + +func (cache *ProviderCache) warmUpCache( + ctx context.Context, + opts *options.TerragruntOptions, + cliConfigFilename string, + args cli.Args, + env map[string]string, +) (*util.CmdOutput, error) { + var ( + cacheRequestID = uuid.New().String() + commandsArgs = convertToMultipleCommandsByPlatforms(args) + ) + // Create terraform cli config file that enables provider caching and does not use provider cache dir if err := cache.createLocalCLIConfig(ctx, opts, cliConfigFilename, cacheRequestID); err != nil { return nil, err @@ -183,7 +211,7 @@ func (cache *ProviderCache) TerraformCommandHook(ctx context.Context, opts *opti // It's low cost operation, because it does not cache the same provider twice, but only new previously non-existent providers. for _, args := range commandsArgs { - if output, err := runTerraformCommand(ctx, opts, args, envs); err != nil { + if output, err := runTerraformCommand(ctx, opts, args, env); err != nil { return output, err } } @@ -193,10 +221,18 @@ func (cache *ProviderCache) TerraformCommandHook(ctx context.Context, opts *opti return nil, err } - if err := getproviders.UpdateLockfile(ctx, opts.WorkingDir, caches); err != nil { - return nil, err - } + err = getproviders.UpdateLockfile(ctx, opts.WorkingDir, caches) + + return nil, err +} +func (cache *ProviderCache) runTerraformWithCache( + ctx context.Context, + opts *options.TerragruntOptions, + cliConfigFilename string, + args cli.Args, + env map[string]string, +) (*util.CmdOutput, error) { // Create terraform cli config file that uses provider cache dir if err := cache.createLocalCLIConfig(ctx, opts, cliConfigFilename, ""); err != nil { return nil, err @@ -208,11 +244,7 @@ func (cache *ProviderCache) TerraformCommandHook(ctx context.Context, opts *opti } cloneOpts.WorkingDir = opts.WorkingDir - cloneOpts.Env = envs - - if skipRunTargetCommand { - return &util.CmdOutput{}, nil - } + cloneOpts.Env = env return shell.RunTerraformCommandWithOutput(ctx, cloneOpts, args...) } @@ -255,22 +287,15 @@ func (cache *ProviderCache) createLocalCLIConfig(ctx context.Context, opts *opti for _, registryName := range opts.ProviderCacheRegistryNames { providerInstallationIncludes = append(providerInstallationIncludes, registryName+"/*/*") - urls, err := DiscoveryURL(ctx, registryName) + apiURLs, err := cache.Server.DiscoveryURL(ctx, registryName) if err != nil { - if !liberrors.As(err, &NotFoundWellKnownURL{}) { - return err - } - - urls = DefaultRegistryURLs - opts.Logger.Debugf("Unable to discover %q registry URLs, reason: %q, use default URLs: %s", registryName, err, urls) - } else { - opts.Logger.Debugf("Discovered %q registry URLs: %s", registryName, urls) + return err } cfg.AddHost(registryName, map[string]string{ - "providers.v1": fmt.Sprintf("%s/%s/%s/%s/", cache.ProviderController.URL(), cacheRequestID, url.PathEscape(urls.ProvidersV1), registryName), + "providers.v1": fmt.Sprintf("%s/%s/%s/", cache.ProviderController.URL(), cacheRequestID, registryName), // Since Terragrunt Provider Cache only caches providers, we need to route module requests to the original registry. - "modules.v1": fmt.Sprintf("https://%s%s", registryName, urls.ModulesV1), + "modules.v1": fmt.Sprintf("https://%s%s", registryName, apiURLs.ModulesV1), }) } diff --git a/cli/provider_cache_test.go b/cli/provider_cache_test.go index c19d4d6a5..d50add609 100644 --- a/cli/provider_cache_test.go +++ b/cli/provider_cache_test.go @@ -5,7 +5,6 @@ import ( "fmt" "io" "net/http" - "net/url" "os" "path/filepath" "regexp" @@ -50,53 +49,52 @@ func TestProviderCache(t *testing.T) { opts := []cache.Option{cache.WithToken(token)} - registryPrefix := url.PathEscape("/v1/providers/") - testCases := []struct { opts []cache.Option - urlPath string + fullURLPath string + relURLPath string expectedStatusCode int expectedBodyReg *regexp.Regexp expectedCachePath string }{ { opts: opts, - urlPath: "/.well-known/terraform.json", + fullURLPath: "/.well-known/terraform.json", expectedStatusCode: http.StatusOK, expectedBodyReg: regexp.MustCompile(regexp.QuoteMeta(`{"providers.v1":"/v1/providers"}`)), }, { opts: append(opts, cache.WithToken("")), - urlPath: "/v1/providers/cache/" + registryPrefix + "/registry.terraform.io/hashicorp/aws/versions", + relURLPath: "/cache/registry.terraform.io/hashicorp/aws/versions", expectedStatusCode: http.StatusUnauthorized, }, { opts: opts, - urlPath: "/v1/providers/cache/" + registryPrefix + "/registry.terraform.io/hashicorp/aws/versions", + relURLPath: "/cache/registry.terraform.io/hashicorp/aws/versions", expectedStatusCode: http.StatusOK, expectedBodyReg: regexp.MustCompile(regexp.QuoteMeta(`"version":"5.36.0","protocols":["5.0"],"platforms"`)), }, { opts: opts, - urlPath: "/v1/providers/cache/" + registryPrefix + "/registry.terraform.io/hashicorp/aws/5.36.0/download/darwin/arm64", + relURLPath: "/cache/registry.terraform.io/hashicorp/aws/5.36.0/download/darwin/arm64", expectedStatusCode: http.StatusLocked, expectedCachePath: "registry.terraform.io/hashicorp/aws/5.36.0/darwin_arm64/terraform-provider-aws_v5.36.0_x5", }, { opts: opts, - urlPath: "/v1/providers/cache/" + registryPrefix + "/registry.terraform.io/hashicorp/template/2.2.0/download/linux/amd64", + relURLPath: "/cache/registry.terraform.io/hashicorp/template/2.2.0/download/linux/amd64", expectedStatusCode: http.StatusLocked, expectedCachePath: "registry.terraform.io/hashicorp/template/2.2.0/linux_amd64/terraform-provider-template_v2.2.0_x4", }, { opts: opts, - urlPath: fmt.Sprintf("/v1/providers/cache/%s/registry.terraform.io/hashicorp/template/1234.5678.9/download/%s/%s", registryPrefix, runtime.GOOS, runtime.GOARCH), + relURLPath: fmt.Sprintf("/cache/registry.terraform.io/hashicorp/template/1234.5678.9/download/%s/%s", runtime.GOOS, runtime.GOARCH), expectedStatusCode: http.StatusLocked, expectedCachePath: createFakeProvider(t, pluginCacheDir, fmt.Sprintf("registry.terraform.io/hashicorp/template/1234.5678.9/%s_%s/terraform-provider-template_1234.5678.9_x5", runtime.GOOS, runtime.GOARCH)), }, { opts: opts, - urlPath: "/v1/providers//" + registryPrefix + "/registry.terraform.io/hashicorp/aws/5.36.0/download/darwin/arm64", + relURLPath: "//registry.terraform.io/hashicorp/aws/5.36.0/download/darwin/arm64", expectedStatusCode: http.StatusOK, expectedBodyReg: regexp.MustCompile(`\{.*` + regexp.QuoteMeta(`"download_url":"http://127.0.0.1:`) + `\d+` + regexp.QuoteMeta(`/downloads/releases.hashicorp.com/terraform-provider-aws/5.36.0/terraform-provider-aws_5.36.0_darwin_arm64.zip"`) + `.*\}`), }, @@ -129,7 +127,11 @@ func TestProviderCache(t *testing.T) { }) urlPath := server.ProviderController.URL() - urlPath.Path = testCase.urlPath + urlPath.Path += testCase.relURLPath + + if testCase.fullURLPath != "" { + urlPath.Path = testCase.fullURLPath + } req, err := http.NewRequestWithContext(ctx, http.MethodGet, urlPath.String(), nil) require.NoError(t, err) diff --git a/pkg/cli/args.go b/pkg/cli/args.go index 27c6641f0..a8c90371a 100644 --- a/pkg/cli/args.go +++ b/pkg/cli/args.go @@ -36,13 +36,18 @@ func (args Args) First() string { return args.Get(0) } -// Last returns the last argument or a blank string +// Second returns the first argument or a blank string. +func (args Args) Second() string { + return args.Get(1) +} + +// Last returns the last argument or a blank string. func (args Args) Last() string { return args.Get(len(args) - 1) } // Tail returns the rest of the arguments (not the first one) -// or else an empty string slice +// or else an empty string slice. func (args Args) Tail() Args { if args.Len() < tailMinArgsLen { return []string{} @@ -101,7 +106,8 @@ func (args Args) Normalize(acts ...NormalizeActsType) Args { return strArgs } -// CommandName returns the first value if it starts without a dash `-`, otherwise that means the args do not consist any command and an empty string is returned. +// CommandName returns the first value if it starts without a dash `-`, +// otherwise that means the args do not consist any command and an empty string is returned. func (args Args) CommandName() string { name := args.First() @@ -112,6 +118,18 @@ func (args Args) CommandName() string { return "" } +// SubCommandName returns the second value if it starts without a dash `-`, +// otherwise that means the args do not consist a subcommand and an empty string is returned. +func (args Args) SubCommandName() string { + name := args.Second() + + if !strings.HasPrefix(name, "-") { + return name + } + + return "" +} + // Contains returns true if args contains the given `target` arg. func (args Args) Contains(target string) bool { for _, arg := range args { diff --git a/shell/context.go b/shell/context.go index f31322a01..14008e04e 100644 --- a/shell/context.go +++ b/shell/context.go @@ -4,6 +4,7 @@ import ( "context" "github.com/gruntwork-io/terragrunt/internal/cache" + "github.com/gruntwork-io/terragrunt/pkg/cli" "github.com/gruntwork-io/terragrunt/util" "github.com/gruntwork-io/terragrunt/options" @@ -18,13 +19,15 @@ const ( type ctxKey byte -type RunShellCommandFunc func(ctx context.Context, opts *options.TerragruntOptions, args []string) (*util.CmdOutput, error) +// RunShellCommandFunc is a context value for `TerraformCommandContextKey` key, used to intercept shell commands. +type RunShellCommandFunc func(ctx context.Context, opts *options.TerragruntOptions, args cli.Args) (*util.CmdOutput, error) func ContextWithTerraformCommandHook(ctx context.Context, fn RunShellCommandFunc) context.Context { ctx = context.WithValue(ctx, RunCmdCacheContextKey, cache.NewCache[string](runCmdCacheName)) return context.WithValue(ctx, TerraformCommandContextKey, fn) } +// TerraformCommandHookFromContext returns `RunShellCommandFunc` from the context if it has been set, otherwise returns nil. func TerraformCommandHookFromContext(ctx context.Context) RunShellCommandFunc { if val := ctx.Value(TerraformCommandContextKey); val != nil { if val, ok := val.(RunShellCommandFunc); ok { diff --git a/terraform/cache/config.go b/terraform/cache/config.go index 6e2cbfcb7..29fe284b3 100644 --- a/terraform/cache/config.go +++ b/terraform/cache/config.go @@ -72,7 +72,7 @@ type Config struct { shutdownTimeout time.Duration services []services.Service - providerHandlers []handlers.ProviderHandler + providerHandlers handlers.ProviderHandlers logger log.Logger } diff --git a/terraform/cache/controllers/provider.go b/terraform/cache/controllers/provider.go index b442ab7a4..5fba7d73c 100644 --- a/terraform/cache/controllers/provider.go +++ b/terraform/cache/controllers/provider.go @@ -3,7 +3,6 @@ package controllers import ( "net/http" - "net/url" "github.com/gruntwork-io/terragrunt/terraform/cache/handlers" "github.com/gruntwork-io/terragrunt/terraform/cache/models" @@ -46,31 +45,24 @@ func (controller *ProviderController) Register(router *router.Router) { // Get All Versions for a Single Provider // https://developer.hashicorp.com/terraform/cloud-docs/api-docs/private-registry/provider-versions-platforms#get-all-versions-for-a-single-provider - controller.GET("/:cache_request_id/:registry_prefix/:registry_name/:namespace/:name/versions", controller.getVersionsAction) + controller.GET("/:cache_request_id/:registry_name/:namespace/:name/versions", controller.getVersionsAction) // Get a Platform // https://developer.hashicorp.com/terraform/cloud-docs/api-docs/private-registry/provider-versions-platforms#get-a-platform - controller.GET("/:cache_request_id/:registry_prefix/:registry_name/:namespace/:name/:version/download/:os/:arch", controller.getPlatformsAction) + controller.GET("/:cache_request_id/:registry_name/:namespace/:name/:version/download/:os/:arch", controller.getPlatformsAction) } func (controller *ProviderController) getVersionsAction(ctx echo.Context) error { var ( - registryPrefix = ctx.Param("registry_prefix") - registryName = ctx.Param("registry_name") - namespace = ctx.Param("namespace") - name = ctx.Param("name") + registryName = ctx.Param("registry_name") + namespace = ctx.Param("namespace") + name = ctx.Param("name") ) - registryPrefix, err := url.QueryUnescape(registryPrefix) - if err != nil { - return err - } - provider := &models.Provider{ - RegistryPrefix: registryPrefix, - RegistryName: registryName, - Namespace: namespace, - Name: name, + RegistryName: registryName, + Namespace: namespace, + Name: name, } for _, handler := range controller.ProviderHandlers { @@ -86,7 +78,6 @@ func (controller *ProviderController) getVersionsAction(ctx echo.Context) error func (controller *ProviderController) getPlatformsAction(ctx echo.Context) (er error) { var ( - registryPrefix = ctx.Param("registry_prefix") registryName = ctx.Param("registry_name") namespace = ctx.Param("namespace") name = ctx.Param("name") @@ -96,19 +87,13 @@ func (controller *ProviderController) getPlatformsAction(ctx echo.Context) (er e cacheRequestID = ctx.Param("cache_request_id") ) - registryPrefix, err := url.QueryUnescape(registryPrefix) - if err != nil { - return err - } - provider := &models.Provider{ - RegistryPrefix: registryPrefix, - RegistryName: registryName, - Namespace: namespace, - Name: name, - Version: version, - OS: os, - Arch: arch, + RegistryName: registryName, + Namespace: namespace, + Name: name, + Version: version, + OS: os, + Arch: arch, } for _, handler := range controller.ProviderHandlers { diff --git a/terraform/cache/handlers/provider.go b/terraform/cache/handlers/provider.go index 035c09774..0b7600272 100644 --- a/terraform/cache/handlers/provider.go +++ b/terraform/cache/handlers/provider.go @@ -2,9 +2,15 @@ package handlers import ( + "context" + liberrors "errors" + "syscall" + "github.com/gruntwork-io/terragrunt/terraform/cache/models" "github.com/gruntwork-io/terragrunt/terraform/cache/router" + "github.com/gruntwork-io/terragrunt/terraform/cache/services" "github.com/labstack/echo/v4" + "github.com/puzpuzpuz/xsync/v3" ) var availablePlatforms []*models.Platform = []*models.Platform{ @@ -25,6 +31,24 @@ var availablePlatforms []*models.Platform = []*models.Platform{ {OS: "windows", Arch: "amd64"}, } +// ProviderHandlers is a slice of ProviderHandler. +type ProviderHandlers []ProviderHandler + +// DiscoveryURL looks for the first handler that can handle the given `registryName`, +// which is determined by the include and exclude settings in the `.terraformrc` CLI config file. +// If the handler is found, tries to discover its API endpoints otherwise return the default registry URLs. +func (handlers ProviderHandlers) DiscoveryURL(ctx context.Context, registryName string) (*RegistryURLs, error) { + provider := models.ParseProvider(registryName) + + for _, handler := range handlers { + if handler.CanHandleProvider(provider) { + return handler.DiscoveryURL(ctx, registryName) + } + } + + return DefaultRegistryURLs, nil +} + type ProviderHandler interface { // CanHandleProvider returns true if the given provider can be handled by this handler. CanHandleProvider(provider *models.Provider) bool @@ -37,15 +61,27 @@ type ProviderHandler interface { // Download serves a request to download the target file. Download(ctx echo.Context, provider *models.Provider) error + + // DiscoveryURL discovers modules and providers API endpoints for the specified `registryName`. + // https://developer.hashicorp.com/terraform/internals/remote-service-discovery#discovery-process + DiscoveryURL(ctx context.Context, registryName string) (*RegistryURLs, error) } type CommonProviderHandler struct { + providerService *services.ProviderService + // includeProviders and excludeProviders are sets of provider matching patterns that together define which providers are eligible to be potentially installed from the corresponding Source. includeProviders models.Providers excludeProviders models.Providers + + // registryURLCache stores discovered registry URLs + // We use [xsync.MapOf](https://github.com/puzpuzpuz/xsync?tab=readme-ov-file#map) + // instead of standard `sync.Map` since it's faster and has generic types. + registryURLCache *xsync.MapOf[string, *RegistryURLs] } -func NewCommonProviderHandler(includes, excludes *[]string) *CommonProviderHandler { +// NewCommonProviderHandler returns a new `CommonProviderHandler` instance with the defined values. +func NewCommonProviderHandler(providerService *services.ProviderService, includes, excludes *[]string) *CommonProviderHandler { var includeProviders, excludeProviders models.Providers if includes != nil { @@ -57,8 +93,10 @@ func NewCommonProviderHandler(includes, excludes *[]string) *CommonProviderHandl } return &CommonProviderHandler{ + providerService: providerService, includeProviders: includeProviders, excludeProviders: excludeProviders, + registryURLCache: xsync.NewMapOf[string, *RegistryURLs](), } } @@ -73,3 +111,26 @@ func (handler *CommonProviderHandler) CanHandleProvider(provider *models.Provide return true } } + +// DiscoveryURL implements ProviderHandler.DiscoveryURL. +func (handler *CommonProviderHandler) DiscoveryURL(ctx context.Context, registryName string) (*RegistryURLs, error) { + if urls, ok := handler.registryURLCache.Load(registryName); ok { + return urls, nil + } + + urls, err := DiscoveryURL(ctx, registryName) + if err != nil { + if !liberrors.As(err, &NotFoundWellKnownURL{}) && !liberrors.Is(err, syscall.ECONNREFUSED) { + return nil, err + } + + urls = DefaultRegistryURLs + handler.providerService.Logger().Debugf("Unable to discover %q registry URLs, reason: %q, use default URLs: %s", registryName, err, urls) + } else { + handler.providerService.Logger().Debugf("Discovered %q registry URLs: %s", registryName, urls) + } + + handler.registryURLCache.Store(registryName, urls) + + return urls, nil +} diff --git a/terraform/cache/handlers/provider_direct.go b/terraform/cache/handlers/provider_direct.go index f515bcdc9..d7f457cd1 100644 --- a/terraform/cache/handlers/provider_direct.go +++ b/terraform/cache/handlers/provider_direct.go @@ -39,15 +39,13 @@ type ProviderDirectHandler struct { *CommonProviderHandler *ReverseProxy - providerService *services.ProviderService cacheProviderHTTPStatusCode int } func NewProviderDirectHandler(providerService *services.ProviderService, cacheProviderHTTPStatusCode int, method *cliconfig.ProviderInstallationDirect, credsSource *cliconfig.CredentialsSource) *ProviderDirectHandler { return &ProviderDirectHandler{ - CommonProviderHandler: NewCommonProviderHandler(method.Include, method.Exclude), + CommonProviderHandler: NewCommonProviderHandler(providerService, method.Include, method.Exclude), ReverseProxy: &ReverseProxy{CredsSource: credsSource, logger: providerService.Logger()}, - providerService: providerService, cacheProviderHTTPStatusCode: cacheProviderHTTPStatusCode, } } @@ -57,12 +55,19 @@ func (handler *ProviderDirectHandler) String() string { } // GetVersions implements ProviderHandler.GetVersions +// https://developer.hashicorp.com/terraform/cloud-docs/api-docs/private-registry/provider-versions-platforms#get-all-versions-for-a-single-provider +// +//nolint:lll func (handler *ProviderDirectHandler) GetVersions(ctx echo.Context, provider *models.Provider) error { - // https://developer.hashicorp.com/terraform/cloud-docs/api-docs/private-registry/provider-versions-platforms#get-all-versions-for-a-single-provider + apiURLs, err := handler.DiscoveryURL(ctx.Request().Context(), provider.RegistryName) + if err != nil { + return err + } + reqURL := &url.URL{ Scheme: "https", Host: provider.RegistryName, - Path: path.Join(provider.RegistryPrefix, provider.Namespace, provider.Name, "versions"), + Path: path.Join(apiURLs.ProvidersV1, provider.Namespace, provider.Name, "versions"), } return handler.ReverseProxy.NewRequest(ctx, reqURL) @@ -70,6 +75,17 @@ func (handler *ProviderDirectHandler) GetVersions(ctx echo.Context, provider *mo // GetPlatform implements ProviderHandler.GetPlatform func (handler *ProviderDirectHandler) GetPlatform(ctx echo.Context, provider *models.Provider, downloaderController router.Controller, cacheRequestID string) error { + apiURLs, err := handler.DiscoveryURL(ctx.Request().Context(), provider.RegistryName) + if err != nil { + return err + } + + platformURL := &url.URL{ + Scheme: "https", + Host: provider.RegistryName, + Path: path.Join(apiURLs.ProvidersV1, provider.Namespace, provider.Name, provider.Version, "download", provider.OS, provider.Arch), + } + return handler.ReverseProxy. WithModifyResponse(func(resp *http.Response) error { // start caching and return 423 status @@ -89,7 +105,7 @@ func (handler *ProviderDirectHandler) GetPlatform(ctx echo.Context, provider *mo // act as a proxy return proxyGetVersionsRequest(resp, downloaderController) }). - NewRequest(ctx, handler.platformURL(provider)) + NewRequest(ctx, platformURL) } // Download implements ProviderHandler.Download @@ -103,10 +119,15 @@ func (handler *ProviderDirectHandler) Download(ctx echo.Context, provider *model // check if the URL contains http scheme, it may just be a filename and we need to build the URL if !strings.Contains(provider.DownloadURL, "://") { + apiURLs, err := handler.DiscoveryURL(ctx.Request().Context(), provider.RegistryName) + if err != nil { + return err + } + downloadURL := &url.URL{ Scheme: "https", Host: provider.RegistryName, - Path: filepath.Join(provider.RegistryPrefix, provider.RegistryName, provider.Namespace, provider.Name, provider.DownloadURL), + Path: filepath.Join(apiURLs.ProvidersV1, provider.RegistryName, provider.Namespace, provider.Name, provider.DownloadURL), } return handler.ReverseProxy.NewRequest(ctx, downloadURL) @@ -120,16 +141,6 @@ func (handler *ProviderDirectHandler) Download(ctx echo.Context, provider *model return handler.ReverseProxy.NewRequest(ctx, downloadURL) } -// platformURL returns the URL used to query the all platforms for a single version. -// https://developer.hashicorp.com/terraform/cloud-docs/api-docs/private-registry/provider-versions-platforms#get-a-platform -func (handler *ProviderDirectHandler) platformURL(provider *models.Provider) *url.URL { - return &url.URL{ - Scheme: "https", - Host: provider.RegistryName, - Path: path.Join(provider.RegistryPrefix, provider.Namespace, provider.Name, provider.Version, "download", provider.OS, provider.Arch), - } -} - // proxyGetVersionsRequest proxies the request to the remote registry and modifies the response to redirect the download URLs to the local server. func proxyGetVersionsRequest(resp *http.Response, downloaderController router.Controller) error { var data map[string]json.RawMessage diff --git a/terraform/cache/handlers/provider_filesystem_mirror.go b/terraform/cache/handlers/provider_filesystem_mirror.go index 78f255eb6..05b4def0e 100644 --- a/terraform/cache/handlers/provider_filesystem_mirror.go +++ b/terraform/cache/handlers/provider_filesystem_mirror.go @@ -18,15 +18,13 @@ import ( type ProviderFilesystemMirrorHandler struct { *CommonProviderHandler - providerService *services.ProviderService cacheProviderHTTPStatusCode int filesystemMirrorPath string } func NewProviderFilesystemMirrorHandler(providerService *services.ProviderService, cacheProviderHTTPStatusCode int, method *cliconfig.ProviderInstallationFilesystemMirror) ProviderHandler { return &ProviderFilesystemMirrorHandler{ - CommonProviderHandler: NewCommonProviderHandler(method.Include, method.Exclude), - providerService: providerService, + CommonProviderHandler: NewCommonProviderHandler(providerService, method.Include, method.Exclude), cacheProviderHTTPStatusCode: cacheProviderHTTPStatusCode, filesystemMirrorPath: method.Path, } diff --git a/terraform/cache/handlers/provider_network_mirror.go b/terraform/cache/handlers/provider_network_mirror.go index 7edf0bdf2..56c51e10e 100644 --- a/terraform/cache/handlers/provider_network_mirror.go +++ b/terraform/cache/handlers/provider_network_mirror.go @@ -22,7 +22,6 @@ type ProviderNetworkMirrorHandler struct { *CommonProviderHandler *http.Client - providerService *services.ProviderService cacheProviderHTTPStatusCode int networkMirrorURL *url.URL credsSource *cliconfig.CredentialsSource @@ -35,9 +34,8 @@ func NewProviderNetworkMirrorHandler(providerService *services.ProviderService, } return &ProviderNetworkMirrorHandler{ - CommonProviderHandler: NewCommonProviderHandler(networkMirror.Include, networkMirror.Exclude), + CommonProviderHandler: NewCommonProviderHandler(providerService, networkMirror.Include, networkMirror.Exclude), Client: &http.Client{}, - providerService: providerService, cacheProviderHTTPStatusCode: cacheProviderHTTPStatusCode, networkMirrorURL: networkMirrorURL, credsSource: credsSource, diff --git a/cli/registry_urls.go b/terraform/cache/handlers/registry_urls.go similarity index 98% rename from cli/registry_urls.go rename to terraform/cache/handlers/registry_urls.go index 299078a23..91651bc90 100644 --- a/cli/registry_urls.go +++ b/terraform/cache/handlers/registry_urls.go @@ -1,4 +1,4 @@ -package cli +package handlers import ( "context" diff --git a/terraform/cache/models/provider.go b/terraform/cache/models/provider.go index a8c1ef62a..2d8faa6a2 100644 --- a/terraform/cache/models/provider.go +++ b/terraform/cache/models/provider.go @@ -90,13 +90,12 @@ func (body ResponseBody) ResolveRelativeReferences(base *url.URL) *ResponseBody type Provider struct { *ResponseBody - RegistryPrefix string - RegistryName string - Namespace string - Name string - Version string - OS string - Arch string + RegistryName string + Namespace string + Name string + Version string + OS string + Arch string } func ParseProvider(str string) *Provider { @@ -123,7 +122,9 @@ func ParseProvider(str string) *Provider { } } - return nil + return &Provider{ + RegistryName: parts[0], + } } func (provider *Provider) String() string { diff --git a/terraform/cache/server.go b/terraform/cache/server.go index 15d1a34aa..048b6bc41 100644 --- a/terraform/cache/server.go +++ b/terraform/cache/server.go @@ -8,6 +8,7 @@ import ( "github.com/gruntwork-io/go-commons/errors" "github.com/gruntwork-io/terragrunt/terraform/cache/controllers" + "github.com/gruntwork-io/terragrunt/terraform/cache/handlers" "github.com/gruntwork-io/terragrunt/terraform/cache/middleware" "github.com/gruntwork-io/terragrunt/terraform/cache/router" "github.com/gruntwork-io/terragrunt/terraform/cache/services" @@ -59,6 +60,13 @@ func NewServer(opts ...Option) *Server { } } +// DiscoveryURL looks for the first handler that can handle the given `registryName`, +// which is determined by the include and exclude settings in the `.terraformrc` CLI config file. +// If the handler is found, tries to discover its API endpoints otherwise return the default registry URLs. +func (server *Server) DiscoveryURL(ctx context.Context, registryName string) (*handlers.RegistryURLs, error) { + return server.providerHandlers.DiscoveryURL(ctx, registryName) +} + // Listen starts listening to the given configuration address. It also automatically chooses a free port if not explicitly specified. func (server *Server) Listen() (net.Listener, error) { ln, err := net.Listen("tcp", server.Addr()) diff --git a/test/integration_serial_test.go b/test/integration_serial_test.go index cb8f1883b..40256c1b7 100644 --- a/test/integration_serial_test.go +++ b/test/integration_serial_test.go @@ -71,7 +71,6 @@ func TestTerragruntProviderCacheWithFilesystemMirror(t *testing.T) { defer cliConfigFilename.Close() t.Setenv(terraform.EnvNameTFCLIConfigFile, cliConfigFilename.Name()) - defer os.Unsetenv(terraform.EnvNameTFCLIConfigFile) t.Logf("%s=%s", terraform.EnvNameTFCLIConfigFile, cliConfigFilename.Name())