terraform-provider-greenhost/digitalocean/resource_digitalocean_certi...

309 lines
8.5 KiB
Go

package digitalocean
import (
"bytes"
"context"
crand "crypto/rand"
"crypto/rsa"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"fmt"
"log"
"math/big"
"reflect"
"regexp"
"strings"
"testing"
"time"
"github.com/digitalocean/godo"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
"github.com/hashicorp/terraform-plugin-sdk/v2/terraform"
)
func init() {
resource.AddTestSweepers("digitalocean_certificate", &resource.Sweeper{
Name: "digitalocean_certificate",
F: testSweepCertificate,
})
}
func testSweepCertificate(region string) error {
meta, err := sharedConfigForRegion(region)
if err != nil {
return err
}
client := meta.(*CombinedConfig).godoClient()
opt := &godo.ListOptions{PerPage: 200}
certs, _, err := client.Certificates.List(context.Background(), opt)
if err != nil {
return err
}
for _, c := range certs {
if strings.HasPrefix(c.Name, "certificate-") {
log.Printf("Destroying certificate %s", c.Name)
if _, err := client.Certificates.Delete(context.Background(), c.ID); err != nil {
return err
}
}
}
return nil
}
func testCertificateStateDataV0() map[string]interface{} {
return map[string]interface{}{
"name": "test",
"id": "aaa-bbb-123-ccc",
}
}
func testCertificateStateDataV1() map[string]interface{} {
v0 := testCertificateStateDataV0()
return map[string]interface{}{
"name": v0["name"],
"uuid": v0["id"],
"id": v0["name"],
}
}
func TestResourceExampleInstanceStateUpgradeV0(t *testing.T) {
expected := testCertificateStateDataV1()
actual, err := migrateCertificateStateV0toV1(context.Background(), testCertificateStateDataV0(), nil)
if err != nil {
t.Fatalf("error migrating state: %s", err)
}
if !reflect.DeepEqual(actual, expected) {
t.Fatalf("\n\nexpected:\n\n%#v\n\ngot:\n\n%#v\n\n", actual, expected)
}
}
func TestAccDigitalOceanCertificate_Basic(t *testing.T) {
var cert godo.Certificate
rInt := acctest.RandInt()
name := fmt.Sprintf("certificate-%d", rInt)
privateKeyMaterial, leafCertMaterial, certChainMaterial := generateTestCertMaterial(t)
resource.ParallelTest(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
ProviderFactories: testAccProviderFactories,
CheckDestroy: testAccCheckDigitalOceanCertificateDestroy,
Steps: []resource.TestStep{
{
Config: testAccCheckDigitalOceanCertificateConfig_basic(rInt, privateKeyMaterial, leafCertMaterial, certChainMaterial),
Check: resource.ComposeTestCheckFunc(
testAccCheckDigitalOceanCertificateExists("digitalocean_certificate.foobar", &cert),
resource.TestCheckResourceAttr(
"digitalocean_certificate.foobar", "id", name),
resource.TestCheckResourceAttr(
"digitalocean_certificate.foobar", "name", name),
resource.TestCheckResourceAttr(
"digitalocean_certificate.foobar", "private_key", HashString(fmt.Sprintf("%s\n", privateKeyMaterial))),
resource.TestCheckResourceAttr(
"digitalocean_certificate.foobar", "leaf_certificate", HashString(fmt.Sprintf("%s\n", leafCertMaterial))),
resource.TestCheckResourceAttr(
"digitalocean_certificate.foobar", "certificate_chain", HashString(fmt.Sprintf("%s\n", certChainMaterial))),
),
},
},
})
}
func TestAccDigitalOceanCertificate_ExpectedErrors(t *testing.T) {
rInt := acctest.RandInt()
privateKeyMaterial, leafCertMaterial, certChainMaterial := generateTestCertMaterial(t)
resource.ParallelTest(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
ProviderFactories: testAccProviderFactories,
CheckDestroy: testAccCheckDigitalOceanCertificateDestroy,
Steps: []resource.TestStep{
{
Config: testAccCheckDigitalOceanCertificateConfig_customNoLeaf(rInt, privateKeyMaterial, certChainMaterial),
ExpectError: regexp.MustCompile("`leaf_certificate` is required for when type is `custom` or empty"),
},
{
Config: testAccCheckDigitalOceanCertificateConfig_customNoKey(rInt, leafCertMaterial, certChainMaterial),
ExpectError: regexp.MustCompile("`private_key` is required for when type is `custom` or empty"),
},
{
Config: testAccCheckDigitalOceanCertificateConfig_noDomains(rInt),
ExpectError: regexp.MustCompile("`domains` is required for when type is `lets_encrypt`"),
},
},
})
}
func testAccCheckDigitalOceanCertificateDestroy(s *terraform.State) error {
client := testAccProvider.Meta().(*CombinedConfig).godoClient()
for _, rs := range s.RootModule().Resources {
if rs.Type != "digitalocean_certificate" {
continue
}
_, err := findCertificateByName(client, rs.Primary.ID)
if err != nil && !strings.Contains(err.Error(), "not found") {
return fmt.Errorf(
"Error waiting for certificate (%s) to be destroyed: %s",
rs.Primary.ID, err)
}
}
return nil
}
func testAccCheckDigitalOceanCertificateExists(n string, cert *godo.Certificate) resource.TestCheckFunc {
return func(s *terraform.State) error {
rs, ok := s.RootModule().Resources[n]
if !ok {
return fmt.Errorf("Not found: %s", n)
}
if rs.Primary.ID == "" {
return fmt.Errorf("No Certificate ID is set")
}
client := testAccProvider.Meta().(*CombinedConfig).godoClient()
c, err := findCertificateByName(client, rs.Primary.ID)
if err != nil {
return err
}
*cert = *c
return nil
}
}
func generateTestCertMaterial(t *testing.T) (string, string, string) {
leafCertMaterial, privateKeyMaterial, err := randTLSCert("Acme Co", "example.com")
if err != nil {
t.Fatalf("Cannot generate test TLS certificate: %s", err)
}
rootCertMaterial, _, err := randTLSCert("Acme Go", "example.com")
if err != nil {
t.Fatalf("Cannot generate test TLS certificate: %s", err)
}
certChainMaterial := fmt.Sprintf("%s\n%s", strings.TrimSpace(rootCertMaterial), leafCertMaterial)
return privateKeyMaterial, leafCertMaterial, certChainMaterial
}
// Based on Terraform's acctest.RandTLSCert, but allows for passing DNS name.
func randTLSCert(orgName string, dnsName string) (string, string, error) {
template := &x509.Certificate{
SerialNumber: big.NewInt(int64(acctest.RandInt())),
Subject: pkix.Name{
Organization: []string{orgName},
},
NotBefore: time.Now(),
NotAfter: time.Now().Add(24 * time.Hour),
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
BasicConstraintsValid: true,
DNSNames: []string{dnsName},
}
privateKey, privateKeyPEM, err := genPrivateKey()
if err != nil {
return "", "", err
}
cert, err := x509.CreateCertificate(crand.Reader, template, template, &privateKey.PublicKey, privateKey)
if err != nil {
return "", "", err
}
certPEM, err := pemEncode(cert, "CERTIFICATE")
if err != nil {
return "", "", err
}
return certPEM, privateKeyPEM, nil
}
func genPrivateKey() (*rsa.PrivateKey, string, error) {
privateKey, err := rsa.GenerateKey(crand.Reader, 1024)
if err != nil {
return nil, "", err
}
privateKeyPEM, err := pemEncode(x509.MarshalPKCS1PrivateKey(privateKey), "RSA PRIVATE KEY")
if err != nil {
return nil, "", err
}
return privateKey, privateKeyPEM, nil
}
func pemEncode(b []byte, block string) (string, error) {
var buf bytes.Buffer
pb := &pem.Block{Type: block, Bytes: b}
if err := pem.Encode(&buf, pb); err != nil {
return "", err
}
return buf.String(), nil
}
func testAccCheckDigitalOceanCertificateConfig_basic(rInt int, privateKeyMaterial, leafCert, certChain string) string {
return fmt.Sprintf(`
resource "digitalocean_certificate" "foobar" {
name = "certificate-%d"
private_key = <<EOF
%s
EOF
leaf_certificate = <<EOF
%s
EOF
certificate_chain = <<EOF
%s
EOF
}`, rInt, privateKeyMaterial, leafCert, certChain)
}
func testAccCheckDigitalOceanCertificateConfig_customNoLeaf(rInt int, privateKeyMaterial, certChain string) string {
return fmt.Sprintf(`
resource "digitalocean_certificate" "foobar" {
name = "certificate-%d"
private_key = <<EOF
%s
EOF
certificate_chain = <<EOF
%s
EOF
}`, rInt, privateKeyMaterial, certChain)
}
func testAccCheckDigitalOceanCertificateConfig_customNoKey(rInt int, leafCert, certChain string) string {
return fmt.Sprintf(`
resource "digitalocean_certificate" "foobar" {
name = "certificate-%d"
leaf_certificate = <<EOF
%s
EOF
certificate_chain = <<EOF
%s
EOF
}`, rInt, leafCert, certChain)
}
func testAccCheckDigitalOceanCertificateConfig_noDomains(rInt int) string {
return fmt.Sprintf(`
resource "digitalocean_certificate" "foobar" {
name = "certificate-%d"
type = "lets_encrypt"
}`, rInt)
}