Skip to content

Commit

Permalink
Use hashicorp/terraform-registry-address as a decoupled library (#28338)
Browse files Browse the repository at this point in the history
* refactor: Use tfaddr for provider address parsing

* refactor: Use tfaddr for module address parsing

* deps: introduce hashicorp/terraform-registry-address
  • Loading branch information
radeksimko authored Jul 8, 2022
1 parent de8eef1 commit 7feef1c
Show file tree
Hide file tree
Showing 14 changed files with 104 additions and 542 deletions.
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ require (
github.com/dylanmei/winrmtest v0.0.0-20210303004826-fbc9ae56efb6
github.com/go-test/deep v1.0.3
github.com/golang/mock v1.6.0
github.com/google/go-cmp v0.5.6
github.com/google/go-cmp v0.5.8
github.com/google/uuid v1.2.0
github.com/gophercloud/gophercloud v0.10.1-0.20200424014253-c3bfe50899e5
github.com/gophercloud/utils v0.0.0-20200423144003-7c72efc7435d
Expand All @@ -46,6 +46,7 @@ require (
github.com/hashicorp/hcl v0.0.0-20170504190234-a4b07c25de5f
github.com/hashicorp/hcl/v2 v2.13.0
github.com/hashicorp/terraform-config-inspect v0.0.0-20210209133302-4fd17a0faac2
github.com/hashicorp/terraform-registry-address v0.0.0-20220623143253-7d51757b572c
github.com/hashicorp/terraform-svchost v0.0.0-20200729002733-f050f53b9734
github.com/jmespath/go-jmespath v0.4.0
github.com/joyent/triton-go v0.0.0-20180313100802-d8f9c0314926
Expand Down Expand Up @@ -177,7 +178,6 @@ require (
golang.org/x/exp/typeparams v0.0.0-20220218215828-6cf2b201936e // indirect
golang.org/x/lint v0.0.0-20210508222113-6edffad5e616 // indirect
golang.org/x/time v0.0.0-20220224211638-0e9765cccd65 // indirect
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
Expand Down
5 changes: 4 additions & 1 deletion go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -282,8 +282,9 @@ github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
Expand Down Expand Up @@ -403,6 +404,8 @@ github.com/hashicorp/serf v0.9.5 h1:EBWvyu9tcRszt3Bxp3KNssBMP1KuHWyO51lz9+786iM=
github.com/hashicorp/serf v0.9.5/go.mod h1:UWDWwZeL5cuWDJdl0C6wrvrUwEqtQ4ZKBKKENpqIUyk=
github.com/hashicorp/terraform-config-inspect v0.0.0-20210209133302-4fd17a0faac2 h1:l+bLFvHjqtgNQwWxwrFX9PemGAAO2P1AGZM7zlMNvCs=
github.com/hashicorp/terraform-config-inspect v0.0.0-20210209133302-4fd17a0faac2/go.mod h1:Z0Nnk4+3Cy89smEbrq+sl1bxc9198gIP4I7wcQF6Kqs=
github.com/hashicorp/terraform-registry-address v0.0.0-20220623143253-7d51757b572c h1:D8aRO6+mTqHfLsK/BC3j5OAoogv1WLRWzY1AaTo3rBg=
github.com/hashicorp/terraform-registry-address v0.0.0-20220623143253-7d51757b572c/go.mod h1:Wn3Na71knbXc1G8Lh+yu/dQWWJeFQEpDeJMtWMtlmNI=
github.com/hashicorp/terraform-svchost v0.0.0-20200729002733-f050f53b9734 h1:HKLsbzeOsfXmKNpr3GiT18XAblV0BjCbzL8KQAMZGa0=
github.com/hashicorp/terraform-svchost v0.0.0-20200729002733-f050f53b9734/go.mod h1:kNDNcF7sN4DocDLBkQYz73HGKwN1ANB1blq4lIYLYvg=
github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM=
Expand Down
47 changes: 2 additions & 45 deletions internal/addrs/module_package.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
package addrs

import (
"strings"

svchost "github.com/hashicorp/terraform-svchost"
tfaddr "github.com/hashicorp/terraform-registry-address"
)

// A ModulePackage represents a physical location where Terraform can retrieve
Expand Down Expand Up @@ -45,45 +43,4 @@ func (p ModulePackage) String() string {
// registry in order to find a real module package address. These being
// distinct is intended to help future maintainers more easily follow the
// series of steps in the module installer, with the help of the type checker.
type ModuleRegistryPackage struct {
Host svchost.Hostname
Namespace string
Name string
TargetSystem string
}

func (s ModuleRegistryPackage) String() string {
var buf strings.Builder
// Note: we're using the "display" form of the hostname here because
// for our service hostnames "for display" means something different:
// it means to render non-ASCII characters directly as Unicode
// characters, rather than using the "punycode" representation we
// use for internal processing, and so the "display" representation
// is actually what users would write in their configurations.
return s.Host.ForDisplay() + "/" + s.ForRegistryProtocol()
return buf.String()
}

func (s ModuleRegistryPackage) ForDisplay() string {
if s.Host == DefaultModuleRegistryHost {
return s.ForRegistryProtocol()
}
return s.Host.ForDisplay() + "/" + s.ForRegistryProtocol()
}

// ForRegistryProtocol returns a string representation of just the namespace,
// name, and target system portions of the address, always omitting the
// registry hostname and the subdirectory portion, if any.
//
// This is primarily intended for generating addresses to send to the
// registry in question via the registry protocol, since the protocol
// skips sending the registry its own hostname as part of identifiers.
func (s ModuleRegistryPackage) ForRegistryProtocol() string {
var buf strings.Builder
buf.WriteString(s.Namespace)
buf.WriteByte('/')
buf.WriteString(s.Name)
buf.WriteByte('/')
buf.WriteString(s.TargetSystem)
return buf.String()
}
type ModuleRegistryPackage = tfaddr.ModulePackage
173 changes: 18 additions & 155 deletions internal/addrs/module_source.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,9 @@ package addrs
import (
"fmt"
"path"
"regexp"
"strings"

svchost "github.com/hashicorp/terraform-svchost"
tfaddr "github.com/hashicorp/terraform-registry-address"
"github.com/hashicorp/terraform/internal/getmodules"
)

Expand Down Expand Up @@ -197,30 +196,11 @@ func (s ModuleSourceLocal) ForDisplay() string {
// combination of a ModuleSourceRegistry and a module version number into
// a concrete ModuleSourceRemote that Terraform will then download and
// install.
type ModuleSourceRegistry struct {
// PackageAddr is the registry package that the target module belongs to.
// The module installer must translate this into a ModuleSourceRemote
// using the registry API and then take that underlying address's
// PackageAddr in order to find the actual package location.
PackageAddr ModuleRegistryPackage

// If Subdir is non-empty then it represents a sub-directory within the
// remote package that the registry address eventually resolves to.
// This will ultimately become the suffix of the Subdir of the
// ModuleSourceRemote that the registry address translates to.
//
// Subdir uses a normalized forward-slash-based path syntax within the
// virtual filesystem represented by the final package. It will never
// include `../` or `./` sequences.
Subdir string
}
type ModuleSourceRegistry tfaddr.Module

// DefaultModuleRegistryHost is the hostname used for registry-based module
// source addresses that do not have an explicit hostname.
const DefaultModuleRegistryHost = svchost.Hostname("registry.terraform.io")

var moduleRegistryNamePattern = regexp.MustCompile("^[0-9A-Za-z](?:[0-9A-Za-z-_]{0,62}[0-9A-Za-z])?$")
var moduleRegistryTargetSystemPattern = regexp.MustCompile("^[0-9a-z]{1,64}$")
const DefaultModuleRegistryHost = tfaddr.DefaultModuleRegistryHost

// ParseModuleSourceRegistry is a variant of ParseModuleSource which only
// accepts module registry addresses, and will reject any other address type.
Expand All @@ -237,147 +217,30 @@ func ParseModuleSourceRegistry(raw string) (ModuleSource, error) {
return ModuleSourceRegistry{}, fmt.Errorf("can't use local directory %q as a module registry address", raw)
}

ret, err := parseModuleSourceRegistry(raw)
src, err := tfaddr.ParseModuleSource(raw)
if err != nil {
// This is to make sure we return a nil ModuleSource, rather than
// a non-nil ModuleSource containing a zero-value ModuleSourceRegistry.
return nil, err
}
return ret, nil
}

func parseModuleSourceRegistry(raw string) (ModuleSourceRegistry, error) {
var err error

var subDir string
raw, subDir = getmodules.SplitPackageSubdir(raw)
if strings.HasPrefix(subDir, "../") {
return ModuleSourceRegistry{}, fmt.Errorf("subdirectory path %q leads outside of the module package", subDir)
}

parts := strings.Split(raw, "/")
// A valid registry address has either three or four parts, because the
// leading hostname part is optional.
if len(parts) != 3 && len(parts) != 4 {
return ModuleSourceRegistry{}, fmt.Errorf("a module registry source address must have either three or four slash-separated components")
}

host := DefaultModuleRegistryHost
if len(parts) == 4 {
host, err = svchost.ForComparison(parts[0])
if err != nil {
// The svchost library doesn't produce very good error messages to
// return to an end-user, so we'll use some custom ones here.
switch {
case strings.Contains(parts[0], "--"):
// Looks like possibly punycode, which we don't allow here
// to ensure that source addresses are written readably.
return ModuleSourceRegistry{}, fmt.Errorf("invalid module registry hostname %q; internationalized domain names must be given as direct unicode characters, not in punycode", parts[0])
default:
return ModuleSourceRegistry{}, fmt.Errorf("invalid module registry hostname %q", parts[0])
}
}
if !strings.Contains(host.String(), ".") {
return ModuleSourceRegistry{}, fmt.Errorf("invalid module registry hostname: must contain at least one dot")
}
// Discard the hostname prefix now that we've processed it
parts = parts[1:]
}

ret := ModuleSourceRegistry{
PackageAddr: ModuleRegistryPackage{
Host: host,
},

Subdir: subDir,
}

if host == svchost.Hostname("github.com") || host == svchost.Hostname("bitbucket.org") {
return ret, fmt.Errorf("can't use %q as a module registry host, because it's reserved for installing directly from version control repositories", host)
}

if ret.PackageAddr.Namespace, err = parseModuleRegistryName(parts[0]); err != nil {
if strings.Contains(parts[0], ".") {
// Seems like the user omitted one of the latter components in
// an address with an explicit hostname.
return ret, fmt.Errorf("source address must have three more components after the hostname: the namespace, the name, and the target system")
}
return ret, fmt.Errorf("invalid namespace %q: %s", parts[0], err)
}
if ret.PackageAddr.Name, err = parseModuleRegistryName(parts[1]); err != nil {
return ret, fmt.Errorf("invalid module name %q: %s", parts[1], err)
}
if ret.PackageAddr.TargetSystem, err = parseModuleRegistryTargetSystem(parts[2]); err != nil {
if strings.Contains(parts[2], "?") {
// The user was trying to include a query string, probably?
return ret, fmt.Errorf("module registry addresses may not include a query string portion")
}
return ret, fmt.Errorf("invalid target system %q: %s", parts[2], err)
}

return ret, nil
}

// parseModuleRegistryName validates and normalizes a string in either the
// "namespace" or "name" position of a module registry source address.
func parseModuleRegistryName(given string) (string, error) {
// Similar to the names in provider source addresses, we defined these
// to be compatible with what filesystems and typical remote systems
// like GitHub allow in names. Unfortunately we didn't end up defining
// these exactly equivalently: provider names can only use dashes as
// punctuation, whereas module names can use underscores. So here we're
// using some regular expressions from the original module source
// implementation, rather than using the IDNA rules as we do in
// ParseProviderPart.

if !moduleRegistryNamePattern.MatchString(given) {
return "", fmt.Errorf("must be between one and 64 characters, including ASCII letters, digits, dashes, and underscores, where dashes and underscores may not be the prefix or suffix")
}

// We also skip normalizing the name to lowercase, because we historically
// didn't do that and so existing module registries might be doing
// case-sensitive matching.
return given, nil
}

// parseModuleRegistryTargetSystem validates and normalizes a string in the
// "target system" position of a module registry source address. This is
// what we historically called "provider" but never actually enforced as
// being a provider address, and now _cannot_ be a provider address because
// provider addresses have three slash-separated components of their own.
func parseModuleRegistryTargetSystem(given string) (string, error) {
// Similar to the names in provider source addresses, we defined these
// to be compatible with what filesystems and typical remote systems
// like GitHub allow in names. Unfortunately we didn't end up defining
// these exactly equivalently: provider names can't use dashes or
// underscores. So here we're using some regular expressions from the
// original module source implementation, rather than using the IDNA rules
// as we do in ParseProviderPart.

if !moduleRegistryTargetSystemPattern.MatchString(given) {
return "", fmt.Errorf("must be between one and 64 ASCII letters or digits")
}

// We also skip normalizing the name to lowercase, because we historically
// didn't do that and so existing module registries might be doing
// case-sensitive matching.
return given, nil
return ModuleSourceRegistry{
Package: src.Package,
Subdir: src.Subdir,
}, nil
}

func (s ModuleSourceRegistry) moduleSource() {}

func (s ModuleSourceRegistry) String() string {
if s.Subdir != "" {
return s.PackageAddr.String() + "//" + s.Subdir
return s.Package.String() + "//" + s.Subdir
}
return s.PackageAddr.String()
return s.Package.String()
}

func (s ModuleSourceRegistry) ForDisplay() string {
if s.Subdir != "" {
return s.PackageAddr.ForDisplay() + "//" + s.Subdir
return s.Package.ForDisplay() + "//" + s.Subdir
}
return s.PackageAddr.ForDisplay()
return s.Package.ForDisplay()
}

// ModuleSourceRemote is a ModuleSource representing a remote location from
Expand All @@ -387,9 +250,9 @@ func (s ModuleSourceRegistry) ForDisplay() string {
// means that it's selecting a sub-directory of the given package to use as
// the entry point into the package.
type ModuleSourceRemote struct {
// PackageAddr is the address of the remote package that the requested
// Package is the address of the remote package that the requested
// module belongs to.
PackageAddr ModulePackage
Package ModulePackage

// If Subdir is non-empty then it represents a sub-directory within the
// remote package which will serve as the entry-point for the package.
Expand Down Expand Up @@ -445,18 +308,18 @@ func parseModuleSourceRemote(raw string) (ModuleSourceRemote, error) {
}

return ModuleSourceRemote{
PackageAddr: ModulePackage(norm),
Subdir: subDir,
Package: ModulePackage(norm),
Subdir: subDir,
}, nil
}

func (s ModuleSourceRemote) moduleSource() {}

func (s ModuleSourceRemote) String() string {
if s.Subdir != "" {
return s.PackageAddr.String() + "//" + s.Subdir
return s.Package.String() + "//" + s.Subdir
}
return s.PackageAddr.String()
return s.Package.String()
}

func (s ModuleSourceRemote) ForDisplay() string {
Expand Down
Loading

0 comments on commit 7feef1c

Please sign in to comment.