Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement the consul_config_entry_sameness_group resource #370

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ jobs:
go-version: 1.20.x
- name: Checkout code
uses: actions/checkout@8f4b7f84864484a7bf31766abe9204da3cbe65b3 # v3.5.0
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Install tfplugindocs
run: |
curl -LO https://github.com/hashicorp/terraform-plugin-docs/releases/download/v0.16.0/tfplugindocs_0.16.0_linux_amd64.zip
Expand Down
147 changes: 147 additions & 0 deletions consul/resource_consul_config_entry_concrete.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package consul

import (
"fmt"
"strings"

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

// ConfigEntryImplementation is the common implementation for all specific
// config entries.
type ConfigEntryImplementation interface {
GetKind() string
GetDescription() string
GetSchema() map[string]*schema.Schema
Decode(d *schema.ResourceData) (consulapi.ConfigEntry, error)
Write(ce consulapi.ConfigEntry, d *schema.ResourceData, sw *stateWriter) error
}

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

return &schema.Resource{
Description: c.GetDescription(),
Schema: s,
Create: configEntryImplementationWrite(c),
Update: configEntryImplementationWrite(c),
Read: configEntryImplementationRead(c),
Delete: configEntryImplementationDelete(c),
Importer: &schema.ResourceImporter{
State: func(d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) {
parts := strings.Split(d.Id(), "/")
var name, partition, namespace string

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)

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

err := sw.error()
if err != nil {
return nil, err
}

return []*schema.ResourceData{d}, nil
},
},
}
}

func configEntryImplementationWrite(impl ConfigEntryImplementation) func(d *schema.ResourceData, meta interface{}) error {
return func(d *schema.ResourceData, meta interface{}) error {
client, qOpts, wOpts := getClient(d, meta)

configEntry, err := impl.Decode(d)
if err != nil {
return err
}

if _, _, err := client.ConfigEntries().Set(configEntry, wOpts); err != nil {
return fmt.Errorf("failed to set '%s' config entry: %v", configEntry.GetName(), err)
}
_, _, err = client.ConfigEntries().Get(configEntry.GetKind(), configEntry.GetName(), qOpts)
if err != nil {
if strings.Contains(err.Error(), "Unexpected response code: 404") {
return fmt.Errorf("failed to read config entry after setting it")
}
return fmt.Errorf("failed to read config entry: %v", err)
}

d.SetId(configEntry.GetName())
return configEntryImplementationRead(impl)(d, meta)
}
}

func configEntryImplementationRead(impl ConfigEntryImplementation) func(d *schema.ResourceData, meta interface{}) error {
return func(d *schema.ResourceData, meta interface{}) error {
client, qOpts, _ := getClient(d, meta)
name := d.Get("name").(string)

fixQOptsForConfigEntry(name, impl.GetKind(), qOpts)

ce, _, err := client.ConfigEntries().Get(impl.GetKind(), name, qOpts)
if err != nil {
if strings.Contains(err.Error(), "Unexpected response code: 404") {
// The config entry has been removed
d.SetId("")
return nil
}
return fmt.Errorf("failed to fetch '%s' config entry: %v", name, err)
}
if ce == nil {
d.SetId("")
return nil
}

sw := newStateWriter(d)
if err := impl.Write(ce, d, sw); err != nil {
return err
}
return sw.error()
}
}

func configEntryImplementationDelete(impl ConfigEntryImplementation) func(d *schema.ResourceData, meta interface{}) error {
return func(d *schema.ResourceData, meta interface{}) error {
client, _, wOpts := getClient(d, meta)
name := d.Get("name").(string)

if _, err := client.ConfigEntries().Delete(impl.GetKind(), name, wOpts); err != nil {
return fmt.Errorf("failed to delete '%s' config entry: %v", name, err)
}
d.SetId("")
return nil
}
}
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" }

}
`
Loading