Skip to content

Commit

Permalink
Implement the consul_config_entry_sameness_group resource
Browse files Browse the repository at this point in the history
This resource makes it easier to manage a `sameness-group` config entry.
It builds on hashicorp#364
so should be reviewed and merged after it.

The importer implementation in `resourceFromConfigEntryImplementation()`
has also been updated to support the `"<name>" or "<partition>/<name>"`
ID format has `sameness-group` is not bound to a namespace.
  • Loading branch information
remilapeyre committed Oct 18, 2023
1 parent 074d856 commit 1e27bd2
Show file tree
Hide file tree
Showing 5 changed files with 234 additions and 167 deletions.
40 changes: 29 additions & 11 deletions consul/resource_consul_config_entry_concrete.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,11 @@ type ConfigEntryImplementation interface {
}

func resourceFromConfigEntryImplementation(c ConfigEntryImplementation) *schema.Resource {
s := c.GetSchema()

return &schema.Resource{
Description: c.GetDescription(),
Schema: c.GetSchema(),
Schema: s,
Create: configEntryImplementationWrite(c),
Update: configEntryImplementationWrite(c),
Read: configEntryImplementationRead(c),
Expand All @@ -33,22 +35,38 @@ func resourceFromConfigEntryImplementation(c ConfigEntryImplementation) *schema.
State: func(d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) {
parts := strings.Split(d.Id(), "/")
var name, partition, namespace string
switch len(parts) {
case 1:
name = parts[0]
case 3:
partition = parts[0]
namespace = parts[1]
name = parts[2]
default:
return nil, fmt.Errorf(`expected path of the form "<name>" or "<partition>/<namespace>/<name>"`)

if _, found := s["namespace"]; found {
switch len(parts) {
case 1:
name = parts[0]
case 3:
partition = parts[0]
namespace = parts[1]
name = parts[2]
default:
return nil, fmt.Errorf(`expected path of the form "<name>" or "<partition>/<namespace>/<name>"`)
}
} else {
switch len(parts) {
case 1:
name = parts[0]
case 2:
partition = parts[0]
name = parts[1]
default:
return nil, fmt.Errorf(`expected path of the form "<name>" or "<partition>/<name>"`)
}
}

d.SetId(name)
sw := newStateWriter(d)
sw.set("name", name)
sw.set("partition", partition)
sw.set("namespace", namespace)

if namespace != "" {
sw.set("namespace", namespace)
}

err := sw.error()
if err != nil {
Expand Down
127 changes: 127 additions & 0 deletions consul/resource_consul_config_entry_sameness_group.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package consul

import (
"fmt"

consulapi "github.com/hashicorp/consul/api"
"github.com/hashicorp/terraform-plugin-sdk/helper/schema"
)

type samenessGroup struct{}

func (s *samenessGroup) GetKind() string {
return consulapi.SamenessGroup
}

func (s *samenessGroup) GetDescription() string {
return "The `consul_config_entry_sameness_group` resource configures a [sameness group](https://developer.hashicorp.com/consul/docs/connect/config-entries/sameness-group). Sameness groups associate services with identical names across partitions and cluster peers."
}

func (s *samenessGroup) GetSchema() map[string]*schema.Schema {
return map[string]*schema.Schema{
"name": {
Type: schema.TypeString,
Description: "Specifies a name for the configuration entry.",
Required: true,
ForceNew: true,
},
"partition": {
Type: schema.TypeString,
Description: "Specifies the local admin partition that the sameness group applies to.",
Optional: true,
ForceNew: true,
},
"default_for_failover": {
Type: schema.TypeBool,
Description: "Determines whether the sameness group should be used to establish connections to services with the same name during failover scenarios. When this field is set to `true`, DNS queries and upstream requests automatically failover to services in the sameness group according to the order of the members in the `members` list.\n\nWhen this field is set to `false`, you can still use a sameness group for `failover` by configuring the failover block of a service resolver configuration entry.",
Optional: true,
},
"include_local": {
Type: schema.TypeBool,
Optional: true,
},
"members": {
Type: schema.TypeList,
Description: "Specifies the partitions and cluster peers that are members of the sameness group from the perspective of the local partition.\n\nThe local partition should be the first member listed. The order of the members determines their precedence during failover scenarios. If a member is listed but Consul cannot connect to it, failover proceeds with the next healthy member in the list.",
Required: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"partition": {
Type: schema.TypeString,
Description: "Specifies a partition in the local datacenter that is a member of the sameness group. When the value of this field is set to `*`, all local partitions become members of the sameness group.",
Optional: true,
},
"peer": {
Type: schema.TypeString,
Description: "Specifies the name of a cluster peer that is a member of the sameness group.\n\nCluster peering connections must be established before adding a peer to the list of members.",
Optional: true,
},
},
},
},
"meta": {
Type: schema.TypeMap,
Description: "Specifies key-value pairs to add to the KV store.",
Optional: true,
Elem: &schema.Schema{Type: schema.TypeString},
},
}
}

func (s *samenessGroup) Decode(d *schema.ResourceData) (consulapi.ConfigEntry, error) {
configEntry := &consulapi.SamenessGroupConfigEntry{
Kind: consulapi.SamenessGroup,
Name: d.Get("name").(string),
Partition: d.Get("partition").(string),
DefaultForFailover: d.Get("default_for_failover").(bool),
IncludeLocal: d.Get("include_local").(bool),
Meta: map[string]string{},
}

for k, v := range d.Get("meta").(map[string]interface{}) {
configEntry.Meta[k] = v.(string)
}

for _, raw := range d.Get("members").([]interface{}) {
m := raw.(map[string]interface{})
configEntry.Members = append(configEntry.Members, consulapi.SamenessGroupMember{
Partition: m["partition"].(string),
Peer: m["peer"].(string),
})
}

return configEntry, nil
}

func (s *samenessGroup) Write(ce consulapi.ConfigEntry, d *schema.ResourceData, sw *stateWriter) error {
sp, ok := ce.(*consulapi.SamenessGroupConfigEntry)
if !ok {
return fmt.Errorf("expected '%s' but got '%s'", consulapi.ServiceSplitter, ce.GetKind())
}

sw.set("name", sp.Name)
sw.set("partition", sp.Partition)
sw.set("default_for_failover", sp.DefaultForFailover)
sw.set("include_local", sp.IncludeLocal)

meta := map[string]interface{}{}
for k, v := range sp.Meta {
meta[k] = v
}
sw.set("meta", meta)

members := make([]interface{}, 0)
for _, m := range sp.Members {
member := map[string]interface{}{
"peer": m.Peer,
"partition": m.Partition,
}
members = append(members, member)
}
sw.set("members", members)

return sw.error()
}
77 changes: 77 additions & 0 deletions consul/resource_consul_config_entry_sameness_group_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package consul

import (
"regexp"
"testing"

"github.com/hashicorp/terraform-plugin-sdk/helper/resource"
)

func TestAccConsulConfigEntrySamenessGroupTest(t *testing.T) {
providers, _ := startTestServer(t)

t.Run("community-edition", func(t *testing.T) {
resource.Test(t, resource.TestCase{
PreCheck: func() { skipTestOnConsulEnterpriseEdition(t) },
Providers: providers,
Steps: []resource.TestStep{
{
Config: testConsulConfigEntrySamenessGroup,
ExpectError: regexp.MustCompile("enterprise-only feature"),
},
},
})
})

t.Run("enterprise-edition", func(t *testing.T) {
resource.Test(t, resource.TestCase{
PreCheck: func() { skipTestOnConsulCommunityEdition(t) },
Providers: providers,
Steps: []resource.TestStep{
{
Config: testConsulConfigEntrySamenessGroup,
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr("consul_config_entry_sameness_group.foo", "default_for_failover", "true"),
resource.TestCheckResourceAttr("consul_config_entry_sameness_group.foo", "id", "test"),
resource.TestCheckResourceAttr("consul_config_entry_sameness_group.foo", "include_local", "true"),
resource.TestCheckResourceAttr("consul_config_entry_sameness_group.foo", "members.#", "4"),
resource.TestCheckResourceAttr("consul_config_entry_sameness_group.foo", "members.0.partition", "store-east"),
resource.TestCheckResourceAttr("consul_config_entry_sameness_group.foo", "members.0.peer", ""),
resource.TestCheckResourceAttr("consul_config_entry_sameness_group.foo", "members.1.partition", "inventory-east"),
resource.TestCheckResourceAttr("consul_config_entry_sameness_group.foo", "members.1.peer", ""),
resource.TestCheckResourceAttr("consul_config_entry_sameness_group.foo", "members.2.partition", ""),
resource.TestCheckResourceAttr("consul_config_entry_sameness_group.foo", "members.2.peer", "dc2-store-west"),
resource.TestCheckResourceAttr("consul_config_entry_sameness_group.foo", "members.3.partition", ""),
resource.TestCheckResourceAttr("consul_config_entry_sameness_group.foo", "members.3.peer", "dc2-inventory-west"),
resource.TestCheckResourceAttr("consul_config_entry_sameness_group.foo", "name", "test"),
resource.TestCheckResourceAttr("consul_config_entry_sameness_group.foo", "partition", ""),
),
},
{
Config: testConsulConfigEntrySamenessGroup,
ResourceName: "consul_config_entry_sameness_group.foo",
ImportState: true,
ImportStateVerify: true,
},
},
})
})

}

const testConsulConfigEntrySamenessGroup = `
resource "consul_config_entry_sameness_group" "foo" {
name = "test"
default_for_failover = true
include_local = true
members { partition = "store-east" }
members { partition = "inventory-east" }
members { peer = "dc2-store-west" }
members { peer = "dc2-inventory-west" }
}
`
1 change: 1 addition & 0 deletions consul/resource_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,7 @@ func Provider() terraform.ResourceProvider {
"consul_autopilot_config": resourceConsulAutopilotConfig(),
"consul_catalog_entry": resourceConsulCatalogEntry(),
"consul_certificate_authority": resourceConsulCertificateAuthority(),
"consul_config_entry_sameness_group": resourceFromConfigEntryImplementation(&samenessGroup{}),
"consul_config_entry_service_splitter": resourceFromConfigEntryImplementation(&serviceSplitter{}),
"consul_config_entry": resourceConsulConfigEntry(),
"consul_intention": resourceConsulIntention(),
Expand Down
Loading

0 comments on commit 1e27bd2

Please sign in to comment.