diff --git a/docs/data-sources/consul_versions.md b/docs/data-sources/consul_versions.md new file mode 100644 index 000000000..a4f0d61e1 --- /dev/null +++ b/docs/data-sources/consul_versions.md @@ -0,0 +1,38 @@ +--- +page_title: "hcp_consul_versions Data Source - terraform-provider-hcp" +subcategory: "" +description: |- + The Consul versions data source provides the Consul versions supported by HCP. +--- + +# Data Source `hcp_consul_versions` + +The Consul versions data source provides the Consul versions supported by HCP. + +## Example Usage + +```terraform +data "hcp_consul_versions" "default" {} +``` + +## Schema + +### Optional + +- **id** (String) The ID of this resource. +- **timeouts** (Block, Optional) (see [below for nested schema](#nestedblock--timeouts)) + +### Read-only + +- **available** (List of String) The Consul versions available on HCP. +- **preview** (List of String) The preview versions of Consul available on HCP. +- **recommended** (String) The recommended Consul version for HCP clusters. + + +### Nested Schema for `timeouts` + +Optional: + +- **default** (String) + + diff --git a/examples/data-sources/hcp_consul_versions/data-source.tf b/examples/data-sources/hcp_consul_versions/data-source.tf new file mode 100644 index 000000000..932482862 --- /dev/null +++ b/examples/data-sources/hcp_consul_versions/data-source.tf @@ -0,0 +1 @@ +data "hcp_consul_versions" "default" {} \ No newline at end of file diff --git a/internal/clients/consul_cluster.go b/internal/clients/consul_cluster.go index f21144cd7..0e1ef9026 100644 --- a/internal/clients/consul_cluster.go +++ b/internal/clients/consul_cluster.go @@ -112,8 +112,9 @@ func CreateConsulCluster(ctx context.Context, client *Client, loc *sharedmodels. return resp.Payload, nil } -// GetAvailableHCPConsulVersions gets the list of available Consul versions that HCP supports. -func GetAvailableHCPConsulVersions(ctx context.Context, loc *sharedmodels.HashicorpCloudLocationLocation, client *Client) ([]*consulmodels.HashicorpCloudConsul20200826Version, error) { +// GetAvailableHCPConsulVersionsForLocation gets the list of available Consul versions that HCP supports for +// the provided location. +func GetAvailableHCPConsulVersionsForLocation(ctx context.Context, loc *sharedmodels.HashicorpCloudLocationLocation, client *Client) ([]*consulmodels.HashicorpCloudConsul20200826Version, error) { p := consul_service.NewListVersionsParams() p.Context = ctx p.LocationProjectID = loc.ProjectID @@ -128,6 +129,20 @@ func GetAvailableHCPConsulVersions(ctx context.Context, loc *sharedmodels.Hashic return resp.Payload.Versions, nil } +// GetAvailableHCPConsulVersions gets the list of available Consul versions that HCP supports. +func GetAvailableHCPConsulVersions(ctx context.Context, client *Client) ([]*consulmodels.HashicorpCloudConsul20200826Version, error) { + p := consul_service.NewListVersions2Params() + p.Context = ctx + + resp, err := client.Consul.ListVersions2(p, nil) + + if err != nil { + return nil, err + } + + return resp.Payload.Versions, nil +} + // DeleteConsulCluster will make a call to the Consul service to initiate the delete Consul // cluster workflow. func DeleteConsulCluster(ctx context.Context, client *Client, loc *sharedmodels.HashicorpCloudLocationLocation, @@ -147,7 +162,8 @@ func DeleteConsulCluster(ctx context.Context, client *Client, loc *sharedmodels. return deleteResp.Payload, nil } -// GetAvailableHCPConsulVersions gets the list of available Consul versions that HCP supports. +// ListConsulUpgradeVersions gets the list of available Consul versions that the supplied cluster +// can upgrade to. func ListConsulUpgradeVersions(ctx context.Context, client *Client, loc *sharedmodels.HashicorpCloudLocationLocation, clusterID string) ([]*consulmodels.HashicorpCloudConsul20200826Version, error) { diff --git a/internal/consul/version.go b/internal/consul/version.go index 80c99d02e..5397a879c 100644 --- a/internal/consul/version.go +++ b/internal/consul/version.go @@ -1,6 +1,7 @@ package consul import ( + "fmt" "strings" consulmodels "github.com/hashicorp/hcp-sdk-go/clients/cloud-consul-service/preview/2020-08-26/models" @@ -37,3 +38,33 @@ func IsValidVersion(version string, versions []*consulmodels.HashicorpCloudConsu func NormalizeVersion(version string) string { return "v" + strings.TrimPrefix(version, "v") } + +// VersionsToString converts a slice of version pointers to a string of their comma delimited values. +func VersionsToString(versions []*consulmodels.HashicorpCloudConsul20200826Version) string { + var recommendedVersion string + var otherVersions []string + + for _, v := range versions { + if v == nil { + continue + } + + if v.Status == consulmodels.HashicorpCloudConsul20200826VersionStatusRECOMMENDED { + recommendedVersion = v.Version + } else { + otherVersions = append(otherVersions, v.Version) + } + } + + // No other versions found, return recommended even if it's empty + if len(otherVersions) == 0 { + return recommendedVersion + } + + // No recommended found, return others + if recommendedVersion == "" { + return strings.Join(otherVersions, ", ") + } + + return fmt.Sprintf("%s (recommended), %s", recommendedVersion, strings.Join(otherVersions, ", ")) +} diff --git a/internal/consul/version_test.go b/internal/consul/version_test.go index 6e35978d3..aab5efb92 100644 --- a/internal/consul/version_test.go +++ b/internal/consul/version_test.go @@ -141,3 +141,74 @@ func Test_NormalizeVersion(t *testing.T) { }) } } + +func Test_VersionsToString(t *testing.T) { + tcs := map[string]struct { + expected string + input []*consulmodels.HashicorpCloudConsul20200826Version + }{ + "with a recommended version": { + input: []*consulmodels.HashicorpCloudConsul20200826Version{ + { + Version: "v1.9.0", + Status: "RECOMMENDED", + }, + { + Version: "v1.8.6", + Status: "AVAILABLE", + }, + { + Version: "v1.8.4", + Status: "AVAILABLE", + }, + }, + expected: "v1.9.0 (recommended), v1.8.6, v1.8.4", + }, + "without a recommended version": { + input: []*consulmodels.HashicorpCloudConsul20200826Version{ + { + Version: "v1.8.6", + Status: "AVAILABLE", + }, + { + Version: "v1.8.4", + Status: "AVAILABLE", + }, + }, + expected: "v1.8.6, v1.8.4", + }, + "no other versions but recommended": { + input: []*consulmodels.HashicorpCloudConsul20200826Version{ + { + Version: "v1.9.0", + Status: "RECOMMENDED", + }, + }, + expected: "v1.9.0", + }, + "nil input": { + input: nil, + expected: "", + }, + "nil values": { + input: []*consulmodels.HashicorpCloudConsul20200826Version{ + nil, + { + Version: "v1.9.0", + Status: "RECOMMENDED", + }, + nil, + }, + expected: "v1.9.0", + }, + } + + for n, tc := range tcs { + t.Run(n, func(t *testing.T) { + r := require.New(t) + + result := VersionsToString(tc.input) + r.Equal(tc.expected, result) + }) + } +} diff --git a/internal/provider/data_source_consul_versions.go b/internal/provider/data_source_consul_versions.go new file mode 100644 index 000000000..716e1fd79 --- /dev/null +++ b/internal/provider/data_source_consul_versions.go @@ -0,0 +1,98 @@ +package provider + +import ( + "context" + "crypto/md5" + "fmt" + "time" + + consulmodels "github.com/hashicorp/hcp-sdk-go/clients/cloud-consul-service/preview/2020-08-26/models" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + + "github.com/hashicorp/terraform-provider-hcp/internal/clients" + "github.com/hashicorp/terraform-provider-hcp/internal/consul" +) + +// defaultConsulVersionsTimeoutDuration is the default timeout +// for reading the agent Helm config. +var defaultConsulVersionsTimeoutDuration = time.Minute * 5 + +// dataSourceConsulVersions is the data source for the Consul versions supported by HCP. +func dataSourceConsulVersions() *schema.Resource { + return &schema.Resource{ + Description: "The Consul versions data source provides the Consul versions supported by HCP.", + Timeouts: &schema.ResourceTimeout{ + Default: &defaultConsulVersionsTimeoutDuration, + }, + ReadContext: dataSourceConsulVersionsRead, + Schema: map[string]*schema.Schema{ + // Computed outputs + "recommended": { + Description: "The recommended Consul version for HCP clusters.", + Type: schema.TypeString, + Computed: true, + }, + "available": { + Description: "The Consul versions available on HCP.", + Type: schema.TypeList, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + Computed: true, + }, + "preview": { + Description: "The preview versions of Consul available on HCP.", + Type: schema.TypeList, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + Computed: true, + }, + }, + } +} + +// dataSourceConsulVersionsRead is the func to implement reading of the +// supported Consul versions on HCP. +func dataSourceConsulVersionsRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + availableConsulVersions, err := clients.GetAvailableHCPConsulVersions(ctx, meta.(*clients.Client)) + if err != nil || len(availableConsulVersions) == 0 { + return diag.Errorf("error fetching available HCP Consul versions: %v", err) + } + + var recommendedVersion string + availableVersions := make([]string, 0) + previewVersions := make([]string, 0) + + for _, v := range availableConsulVersions { + switch v.Status { + case consulmodels.HashicorpCloudConsul20200826VersionStatusRECOMMENDED: + recommendedVersion = v.Version + availableVersions = append(availableVersions, v.Version) + case consulmodels.HashicorpCloudConsul20200826VersionStatusAVAILABLE: + availableVersions = append(availableVersions, v.Version) + case consulmodels.HashicorpCloudConsul20200826VersionStatusPREVIEW: + previewVersions = append(previewVersions, v.Version) + } + } + + err = d.Set("recommended", recommendedVersion) + if err != nil { + return diag.FromErr(err) + } + + err = d.Set("available", availableVersions) + if err != nil { + return diag.FromErr(err) + } + + err = d.Set("preview", previewVersions) + if err != nil { + return diag.FromErr(err) + } + + d.SetId(fmt.Sprintf("%x", md5.Sum([]byte(consul.VersionsToString(availableConsulVersions))))) + + return nil +} diff --git a/internal/provider/provider.go b/internal/provider/provider.go index c50eee532..738f3562b 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -22,6 +22,7 @@ func New() func() *schema.Provider { "hcp_consul_agent_helm_config": dataSourceConsulAgentHelmConfig(), "hcp_consul_agent_kubernetes_secret": dataSourceConsulAgentKubernetesSecret(), "hcp_consul_cluster": dataSourceConsulCluster(), + "hcp_consul_versions": dataSourceConsulVersions(), "hcp_hvn": dataSourceHvn(), }, ResourcesMap: map[string]*schema.Resource{ diff --git a/internal/provider/resource_consul_cluster.go b/internal/provider/resource_consul_cluster.go index aa766723c..7f9f65def 100644 --- a/internal/provider/resource_consul_cluster.go +++ b/internal/provider/resource_consul_cluster.go @@ -244,7 +244,7 @@ func resourceConsulClusterCreate(ctx context.Context, d *schema.ResourceData, me } // fetch available version from HCP - availableConsulVersions, err := clients.GetAvailableHCPConsulVersions(ctx, loc, client) + availableConsulVersions, err := clients.GetAvailableHCPConsulVersionsForLocation(ctx, loc, client) if err != nil || availableConsulVersions == nil { return diag.Errorf("error fetching available HCP Consul versions: %v", err) } @@ -258,7 +258,7 @@ func resourceConsulClusterCreate(ctx context.Context, d *schema.ResourceData, me // check if version is valid and available if !consul.IsValidVersion(consulVersion, availableConsulVersions) { - return diag.Errorf("specified Consul version (%s) is unavailable; must be one of: %v", consulVersion, availableConsulVersions) + return diag.Errorf("specified Consul version (%s) is unavailable; must be one of: [%s]", consulVersion, consul.VersionsToString(availableConsulVersions)) } datacenter := strings.ToLower(clusterID) @@ -509,7 +509,7 @@ func resourceConsulClusterUpdate(ctx context.Context, d *schema.ResourceData, me // Validate that the upgrade version is valid if !consul.IsValidVersion(newConsulVersion, upgradeVersions) { - return diag.Errorf("specified Consul version (%s) is unavailable; must be one of: %v", newConsulVersion, upgradeVersions) + return diag.Errorf("specified Consul version (%s) is unavailable; must be one of: [%s]", newConsulVersion, consul.VersionsToString(upgradeVersions)) } // Invoke update cluster endpoint