package getproviders import ( "context" "fmt" svchost "github.com/hashicorp/terraform-svchost" disco "github.com/hashicorp/terraform-svchost/disco" "github.com/hashicorp/terraform/internal/addrs" ) // RegistrySource is a Source that knows how to find and install providers from // their originating provider registries. type RegistrySource struct { services *disco.Disco } var _ Source = (*RegistrySource)(nil) // NewRegistrySource creates and returns a new source that will install // providers from their originating provider registries. func NewRegistrySource(services *disco.Disco) *RegistrySource { return &RegistrySource{ services: services, } } // AvailableVersions returns all of the versions available for the provider // with the given address, or an error if that result cannot be determined. // // If the request fails, the returned error might be an value of // ErrHostNoProviders, ErrHostUnreachable, ErrUnauthenticated, // ErrProviderNotKnown, or ErrQueryFailed. Callers must be defensive and // expect errors of other types too, to allow for future expansion. func (s *RegistrySource) AvailableVersions(ctx context.Context, provider addrs.Provider) (VersionList, Warnings, error) { client, err := s.registryClient(provider.Hostname) if err != nil { return nil, nil, err } versionsResponse, warnings, err := client.ProviderVersions(ctx, provider) if err != nil { return nil, nil, err } if len(versionsResponse) == 0 { return nil, warnings, nil } // We ignore protocols here because our goal is to find out which versions // are available _at all_. Which ones are compatible with the current // Terraform becomes relevant only once we've selected one, at which point // we'll return an error if the selected one is incompatible. // // We intentionally produce an error on incompatibility, rather than // silently ignoring an incompatible version, in order to give the user // explicit feedback about why their selection wasn't valid and allow them // to decide whether to fix that by changing the selection or by some other // action such as upgrading Terraform, using a different OS to run // Terraform, etc. Changes that affect compatibility are considered breaking // changes from a provider API standpoint, so provider teams should change // compatibility only in new major versions. ret := make(VersionList, 0, len(versionsResponse)) for str := range versionsResponse { v, err := ParseVersion(str) if err != nil { return nil, nil, ErrQueryFailed{ Provider: provider, Wrapped: fmt.Errorf("registry response includes invalid version string %q: %s", str, err), } } ret = append(ret, v) } ret.Sort() // lowest precedence first, preserving order when equal precedence return ret, warnings, nil } // PackageMeta returns metadata about the location and capabilities of // a distribution package for a particular provider at a particular version // targeting a particular platform. // // Callers of PackageMeta should first call AvailableVersions and pass // one of the resulting versions to this function. This function cannot // distinguish between a version that is not available and an unsupported // target platform, so if it encounters either case it will return an error // suggesting that the target platform isn't supported under the assumption // that the caller already checked that the version is available at all. // // To find a package suitable for the platform where the provider installation // process is running, set the "target" argument to // getproviders.CurrentPlatform. // // If the request fails, the returned error might be an value of // ErrHostNoProviders, ErrHostUnreachable, ErrUnauthenticated, // ErrPlatformNotSupported, or ErrQueryFailed. Callers must be defensive and // expect errors of other types too, to allow for future expansion. func (s *RegistrySource) PackageMeta(ctx context.Context, provider addrs.Provider, version Version, target Platform) (PackageMeta, error) { client, err := s.registryClient(provider.Hostname) if err != nil { return PackageMeta{}, err } return client.PackageMeta(ctx, provider, version, target) } func (s *RegistrySource) registryClient(hostname svchost.Hostname) (*registryClient, error) { host, err := s.services.Discover(hostname) if err != nil { return nil, ErrHostUnreachable{ Hostname: hostname, Wrapped: err, } } url, err := host.ServiceURL("providers.v1") switch err := err.(type) { case nil: // okay! We'll fall through and return below. case *disco.ErrServiceNotProvided: return nil, ErrHostNoProviders{ Hostname: hostname, } case *disco.ErrVersionNotSupported: return nil, ErrHostNoProviders{ Hostname: hostname, HasOtherVersion: true, } default: return nil, ErrHostUnreachable{ Hostname: hostname, Wrapped: err, } } // Check if we have credentials configured for this hostname. creds, err := s.services.CredentialsForHost(hostname) if err != nil { // This indicates that a credentials helper failed, which means we // can't do anything better than just pass through the helper's // own error message. return nil, fmt.Errorf("failed to retrieve credentials for %s: %s", hostname, err) } return newRegistryClient(url, creds), nil } func (s *RegistrySource) ForDisplay(provider addrs.Provider) string { return fmt.Sprintf("registry %s", provider.Hostname.ForDisplay()) }