Skip to content

Commit

Permalink
add resource cloudflare_certificate_authorities_hostname_associations
Browse files Browse the repository at this point in the history
  • Loading branch information
ebisso committed Dec 21, 2024
1 parent b7ad19f commit 9dd79d8
Show file tree
Hide file tree
Showing 4 changed files with 323 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package certificate_authorities_hostname_associations

import "github.com/hashicorp/terraform-plugin-framework/types"

type HostnameAssociation = types.String

type CertificateAuthoritiesHostnameAssociationsModel struct {
ZoneID types.String `tfsdk:"zone_id"`
MTLSCertificateID types.String `tfsdk:"mtls_certificate_id"`
Hostnames []HostnameAssociation `tfsdk:"hostnames"`
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
package certificate_authorities_hostname_associations

import (
"context"
"fmt"
"strings"

cfv1 "github.com/cloudflare/cloudflare-go"
"github.com/cloudflare/terraform-provider-cloudflare/internal/framework/muxclient"
"github.com/hashicorp/terraform-plugin-framework/path"
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/hashicorp/terraform-plugin-framework/types/basetypes"
"github.com/hashicorp/terraform-plugin-log/tflog"
)

// Ensure provider defined types fully satisfy framework interfaces.
var _ resource.Resource = &CertificateAuthoritiesHostnameAssociationsResource{}
var _ resource.ResourceWithImportState = &CertificateAuthoritiesHostnameAssociationsResource{}

func NewResource() resource.Resource {
return &CertificateAuthoritiesHostnameAssociationsResource{}
}

type CertificateAuthoritiesHostnameAssociationsResource struct {
client *muxclient.Client
}

func (r *CertificateAuthoritiesHostnameAssociationsResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_certificate_authorities_hostname_associations"
}

func (r *CertificateAuthoritiesHostnameAssociationsResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
if req.ProviderData == nil {
return
}

client, ok := req.ProviderData.(*muxclient.Client)

if !ok {
resp.Diagnostics.AddError(
"unexpected resource configure type",
fmt.Sprintf("Expected *muxclient.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData),
)

return
}

r.client = client
}

func (r *CertificateAuthoritiesHostnameAssociationsResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
var data *CertificateAuthoritiesHostnameAssociationsModel

resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)

if resp.Diagnostics.HasError() {
return
}

updatedHostnames, err := r.update(ctx, data)
if err != nil {
resp.Diagnostics.AddError("error updating Certificate Authorities Hostname Associations", err.Error())
return
}

data = buildCertificateAuthoritiesHostnameAssociationsModel(updatedHostnames, data.MTLSCertificateID, data.ZoneID)
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}

func (r *CertificateAuthoritiesHostnameAssociationsResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
var data *CertificateAuthoritiesHostnameAssociationsModel

resp.Diagnostics.Append(req.State.Get(ctx, &data)...)

if resp.Diagnostics.HasError() {
return
}

identifier := cfv1.ZoneIdentifier(data.ZoneID.ValueString())
params := cfv1.ListCertificateAuthoritiesHostnameAssociationsParams{
MTLSCertificateID: data.MTLSCertificateID.ValueString(),
}

hostnames, err := r.client.V1.ListCertificateAuthoritiesHostnameAssociations(ctx, identifier, params)
if err != nil {
resp.Diagnostics.AddError("error reading Access Mutual TLS Hostname Settings", err.Error())
return
}
data = buildCertificateAuthoritiesHostnameAssociationsModel(hostnames, data.MTLSCertificateID, data.ZoneID)
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}

// Helper function used by both Update, Create and Delete
func (r *CertificateAuthoritiesHostnameAssociationsResource) update(ctx context.Context, data *CertificateAuthoritiesHostnameAssociationsModel) ([]cfv1.HostnameAssociation, error) {
updatedHostnames := []cfv1.HostnameAssociation{}
identifier := cfv1.ZoneIdentifier(data.ZoneID.ValueString())

hostnames := data.Hostnames
for _, hostname := range hostnames {
updatedHostnames = append(updatedHostnames, hostname.ValueString())
}

updatedCertificateAuthoritiesHostnameAssociations := cfv1.UpdateCertificateAuthoritiesHostnameAssociationsParams{
MTLSCertificateID: data.MTLSCertificateID.ValueString(),
Hostnames: updatedHostnames,
}

tflog.Debug(ctx, fmt.Sprintf("Updating Cloudflare Certificate Authorities Hostname Associations from struct: %+v", updatedCertificateAuthoritiesHostnameAssociations))

resultUpdatedHostnames, err := r.client.V1.UpdateCertificateAuthoritiesHostnameAssociations(ctx, identifier, updatedCertificateAuthoritiesHostnameAssociations)
if err != nil {
return nil, fmt.Errorf("error updating Certificate Authorities Hostname Associations for %s %q %s: %w", identifier.Level, identifier.Identifier, data.MTLSCertificateID, err)
}
return resultUpdatedHostnames, nil
}

func (r *CertificateAuthoritiesHostnameAssociationsResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
var data *CertificateAuthoritiesHostnameAssociationsModel

resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)

if resp.Diagnostics.HasError() {
return
}

updatedHostnames, err := r.update(ctx, data)
if err != nil {
resp.Diagnostics.AddError("error updating Certificate Authorities Hostname Associations", err.Error())
return
}

data = buildCertificateAuthoritiesHostnameAssociationsModel(updatedHostnames, data.MTLSCertificateID, data.ZoneID)
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}

func (r *CertificateAuthoritiesHostnameAssociationsResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
var data *CertificateAuthoritiesHostnameAssociationsModel

resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
identifier := cfv1.ZoneIdentifier(data.ZoneID.ValueString())

// To delete the associations we issue and update an empty array of hostnames
deletedCertificateAuthoritiesHostnameAssociations := cfv1.UpdateCertificateAuthoritiesHostnameAssociationsParams{
MTLSCertificateID: data.MTLSCertificateID.ValueString(),
Hostnames: []cfv1.HostnameAssociation{},
}

_, err := r.client.V1.UpdateCertificateAuthoritiesHostnameAssociations(ctx, identifier, deletedCertificateAuthoritiesHostnameAssociations)
if err != nil {
resp.Diagnostics.AddError(fmt.Sprintf("error removing Certificate Authorities Hostname Associations for %s %q", identifier.Level, identifier.Identifier), err.Error())
return
}
}

func (r *CertificateAuthoritiesHostnameAssociationsResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
attributes := strings.Split(req.ID, "/")

invalidIDMessage := "invalid ID (\"%s\") specified, should be in format \"<zone_id>\" or \"<zone_id>/<mtls_certificate_id>\""
if len(attributes) != 1 || len(attributes) != 2 {
resp.Diagnostics.AddError("error importing Certificate Authorities Hostname Associations", fmt.Sprintf(invalidIDMessage, req.ID))
return
}

zoneID := attributes[0]
mtlsCertificateID := ""
if len(attributes) == 2 {
mtlsCertificateID = attributes[1]
}

tflog.Debug(ctx, fmt.Sprintf("Importing Certificate Authorities Hostname Associations: for %s %s", zoneID, mtlsCertificateID))

resp.Diagnostics.Append(
resp.State.SetAttribute(ctx, path.Root("zone_id"), zoneID)...,
)

resp.Diagnostics.Append(
resp.State.SetAttribute(ctx, path.Root("mtls_certificate_id"), mtlsCertificateID)...,
)
}

func buildCertificateAuthoritiesHostnameAssociationsModel(hostnames []cfv1.HostnameAssociation, mtlsCertificateID basetypes.StringValue, zoneID basetypes.StringValue) *CertificateAuthoritiesHostnameAssociationsModel {
model := &CertificateAuthoritiesHostnameAssociationsModel{
ZoneID: zoneID,
MTLSCertificateID: mtlsCertificateID,
}
for _, hostname := range hostnames {
model.Hostnames = append(model.Hostnames, types.StringValue(hostname))
}
return model
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package certificate_authorities_hostname_associations

import (
"context"
"fmt"
"os"
"testing"

cfv1 "github.com/cloudflare/cloudflare-go"
"github.com/cloudflare/terraform-provider-cloudflare/internal/acctest"
"github.com/cloudflare/terraform-provider-cloudflare/internal/consts"
"github.com/cloudflare/terraform-provider-cloudflare/internal/utils"
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
"github.com/hashicorp/terraform-plugin-testing/terraform"
)

func TestAccCloudflareCertificateAuthoritiesHostnameAssociations_Create(t *testing.T) {
t.Parallel()
rnd := utils.GenerateRandomResourceName()
name := fmt.Sprintf("cloudflare_certificate_authorities_hostname_associations.%s", rnd)
accountID := os.Getenv("CLOUDFLARE_ACCOUNT_ID")
zoneID := os.Getenv("CLOUDFLARE_ZONE_ID")
zoneName := os.Getenv("CLOUDFLARE_DOMAIN")
hostname := rnd + "." + zoneName

resource.Test(t, resource.TestCase{
PreCheck: func() {
acctest.TestAccPreCheck(t)
acctest.TestAccPreCheck_Account(t)
},
ProtoV6ProviderFactories: acctest.TestAccProtoV6ProviderFactories,
CheckDestroy: testAccCheckCloudflareCertificateAuthoritiesHostnameAssociationsDestroy,
Steps: []resource.TestStep{
{
Config: testCertificateAuthoritiesHostnameAssociationsConfig(rnd, accountID, zoneID, hostname),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr(name, consts.ZoneIDSchemaKey, zoneID),
resource.TestCheckResourceAttrSet(name, "mtls_certificate_id"),
resource.TestCheckResourceAttr(name, "hostnames.0", hostname),
),
},
},
})
}

func testAccCheckCloudflareCertificateAuthoritiesHostnameAssociationsDestroy(s *terraform.State) error {
client, err := acctest.SharedV1Client()
if err != nil {
return fmt.Errorf("Failed to create Cloudflare client: %w", err)
}

for _, rs := range s.RootModule().Resources {
if rs.Type != "cloudflare_zero_trust_access_mtls_hostname_settings" {
continue
}

if rs.Primary.Attributes[consts.ZoneIDSchemaKey] != "" {
settings, _ := client.GetAccessMutualTLSHostnameSettings(context.Background(), cfv1.ZoneIdentifier(rs.Primary.Attributes[consts.ZoneIDSchemaKey]))
if len(settings) != 0 {
return fmt.Errorf("AccessMutualTLSHostnameSettings still exists")
}
}

if rs.Primary.Attributes[consts.AccountIDSchemaKey] != "" {
settings, _ := client.GetAccessMutualTLSHostnameSettings(context.Background(), cfv1.AccountIdentifier(rs.Primary.Attributes[consts.AccountIDSchemaKey]))
if len(settings) != 0 {
return fmt.Errorf("AccessMutualTLSHostnameSettings still exists")
}
}
}

return nil
}

func testCertificateAuthoritiesHostnameAssociationsConfig(rnd string, accountID string, zoneID string, hostname string) string {
return fmt.Sprintf(`
resource "cloudflare_mtls_certificate" "%[1]s" {
account_id = "%[2]s"
name = ""
certificates = "-----BEGIN CERTIFICATE-----\nMIIDmDCCAoCgAwIBAgIUKTOAZNjcXVZRj4oQt0SHsl1c1vMwDQYJKoZIhvcNAQELBQAwUTELMAkGA1UEBhMCVVMxFjAUBgNVBAgMDVNhbiBGcmFuY2lzY28xEzARBgNVBAcMCkNhbGlmb3JuaWExFTATBgNVBAoMDEV4YW1wbGUgSW5jLjAgFw0yMjExMjIxNjU5NDdaGA8yMTIyMTAyOTE2NTk0N1owUTELMAkGA1UEBhMCVVMxFjAUBgNVBAgMDVNhbiBGcmFuY2lzY28xEzARBgNVBAcMCkNhbGlmb3JuaWExFTATBgNVBAoMDEV4YW1wbGUgSW5jLjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMRcORwgJFTdcG/2GKI+cFYiOBNDKjCZUXEOvXWY42BkH9wxiMT869CO+enA1w5pIrXow6kCM1sQspHHaVmJUlotEMJxyoLFfA/8Kt1EKFyobOjuZs2SwyVyJ2sStvQuUQEosULZCNGZEqoH5g6zhMPxaxm7ZLrrsDZ9maNGVqo7EWLWHrZ57Q/5MtTrbxQL+eXjUmJ9K3kS+3uEwMdqR6Z3BluU1ivanpPc1CN2GNhdO0/hSY4YkGEnuLsqJyDd3cIiB1MxuCBJ4ZaqOd2viV1WcP3oU3dxVPm4MWyfYIldMWB14FahScxLhWdRnM9YZ/i9IFcLypXsuz7DjrJPtPUCAwEAAaNmMGQwHQYDVR0OBBYEFP5JzLUawNF+c3AXsYTEWHh7z2czMB8GA1UdIwQYMBaAFP5JzLUawNF+c3AXsYTEWHh7z2czMA4GA1UdDwEB/wQEAwIBBjASBgNVHRMBAf8ECDAGAQH/AgEBMA0GCSqGSIb3DQEBCwUAA4IBAQBc+Be7NDhpE09y7hLPZGRPl1cSKBw4RI0XIv6rlbSTFs5EebpTGjhx/whNxwEZhB9HZ7111Oa1YlT8xkI9DshB78mjAHCKBAJ76moK8tkG0aqdYpJ4ZcJTVBB7l98Rvgc7zfTii7WemTy72deBbSeiEtXavm4EF0mWjHhQ5Nxpnp00Bqn5g1x8CyTDypgmugnep+xG+iFzNmTdsz7WI9T/7kDMXqB7M/FPWBORyS98OJqNDswCLF8bIZYwUBEe+bRHFomoShMzaC3tvim7WCb16noDkSTMlfKO4pnvKhpcVdSgwcruATV7y+W+Lvmz2OT/Gui4JhqeoTewsxndhDDE\n-----END CERTIFICATE-----"
private_key = ""
ca = true
}
resource "cloudflare_certificate_authorities_hostname_associations" "%[1]s" {
zone_id = "%[3]s"
mtls_certificate_id = cloudflare_mtls_certificate.%[1]s.id
hostnames = ["%[4]s"]
}
`, rnd, accountID, zoneID, hostname)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package certificate_authorities_hostname_associations

import (
"context"

"github.com/cloudflare/terraform-provider-cloudflare/internal/consts"
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
"github.com/hashicorp/terraform-plugin-framework/types"
)

func (r *CertificateAuthoritiesHostnameAssociationsResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
resp.Schema = schema.Schema{
Description: "Provides a Cloudflare Certificate Authorities Hostname Associations resource.",
Attributes: map[string]schema.Attribute{
consts.ZoneIDSchemaKey: schema.StringAttribute{
Description: consts.ZoneIDSchemaDescription,
Required: true,
},
"mtls_certificate_id": schema.StringAttribute{
Description: "TODO",
Optional: true,
},
"hostnames": schema.ListAttribute{
Description: "TODO",
Required: true,
ElementType: types.StringType,
},
},
}
}

0 comments on commit 9dd79d8

Please sign in to comment.