diff --git a/aws/config.go b/aws/config.go index 4478ba29d5c..2960400fef2 100644 --- a/aws/config.go +++ b/aws/config.go @@ -27,6 +27,7 @@ import ( "github.com/aws/aws-sdk-go/service/cloud9" "github.com/aws/aws-sdk-go/service/cloudformation" "github.com/aws/aws-sdk-go/service/cloudfront" + "github.com/aws/aws-sdk-go/service/cloudhsmv2" "github.com/aws/aws-sdk-go/service/cloudtrail" "github.com/aws/aws-sdk-go/service/cloudwatch" "github.com/aws/aws-sdk-go/service/cloudwatchevents" @@ -160,6 +161,7 @@ type AWSClient struct { cfconn *cloudformation.CloudFormation cloud9conn *cloud9.Cloud9 cloudfrontconn *cloudfront.CloudFront + cloudhsmv2conn *cloudhsmv2.CloudHSMV2 cloudtrailconn *cloudtrail.CloudTrail cloudwatchconn *cloudwatch.CloudWatch cloudwatchlogsconn *cloudwatchlogs.CloudWatchLogs @@ -499,6 +501,7 @@ func (c *Config) Client() (interface{}, error) { client.cloud9conn = cloud9.New(sess) client.cfconn = cloudformation.New(awsCfSess) client.cloudfrontconn = cloudfront.New(sess) + client.cloudhsmv2conn = cloudhsmv2.New(sess) client.cloudtrailconn = cloudtrail.New(sess) client.cloudwatchconn = cloudwatch.New(awsCwSess) client.cloudwatcheventsconn = cloudwatchevents.New(awsCweSess) diff --git a/aws/data_source_aws_cloudhsm2_cluster.go b/aws/data_source_aws_cloudhsm2_cluster.go new file mode 100644 index 00000000000..57535f2c7cd --- /dev/null +++ b/aws/data_source_aws_cloudhsm2_cluster.go @@ -0,0 +1,131 @@ +package aws + +import ( + "fmt" + "log" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/cloudhsmv2" + "github.com/hashicorp/terraform/helper/schema" +) + +func dataSourceCloudHsm2Cluster() *schema.Resource { + return &schema.Resource{ + Read: dataSourceCloudHsm2ClusterRead, + + Schema: map[string]*schema.Schema{ + "cluster_id": { + Type: schema.TypeString, + Required: true, + }, + + "cluster_state": { + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + + "vpc_id": { + Type: schema.TypeString, + Computed: true, + }, + + "security_group_id": { + Type: schema.TypeString, + Computed: true, + }, + + "cluster_certificates": { + Type: schema.TypeList, + MaxItems: 1, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "cluster_certificate": { + Type: schema.TypeString, + Computed: true, + }, + "cluster_csr": { + Type: schema.TypeString, + Computed: true, + }, + "aws_hardware_certificate": { + Type: schema.TypeString, + Computed: true, + }, + "hsm_certificate": { + Type: schema.TypeString, + Computed: true, + }, + "manufacturer_hardware_certificate": { + Type: schema.TypeString, + Computed: true, + }, + }, + }, + }, + "subnet_ids": { + Type: schema.TypeSet, + Computed: true, + Elem: &schema.Schema{Type: schema.TypeString}, + Set: schema.HashString, + }, + }, + } +} + +func dataSourceCloudHsm2ClusterRead(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).cloudhsmv2conn + + clusterId := d.Get("cluster_id").(string) + filters := []*string{&clusterId} + log.Printf("[DEBUG] Reading CloudHSM v2 Cluster %s", clusterId) + result := int64(1) + input := &cloudhsmv2.DescribeClustersInput{ + Filters: map[string][]*string{ + "clusterIds": filters, + }, + MaxResults: &result, + } + state := d.Get("cluster_state").(string) + states := []*string{&state} + if len(state) > 0 { + input.Filters["states"] = states + } + out, err := conn.DescribeClusters(input) + + if err != nil { + return fmt.Errorf("error describing CloudHSM v2 Cluster: %s", err) + } + + var cluster *cloudhsmv2.Cluster + for _, c := range out.Clusters { + if aws.StringValue(c.ClusterId) == clusterId { + cluster = c + break + } + } + + if cluster == nil { + return fmt.Errorf("cluster with id %s not found", clusterId) + } + + d.SetId(clusterId) + d.Set("vpc_id", cluster.VpcId) + d.Set("security_group_id", cluster.SecurityGroup) + d.Set("cluster_state", cluster.State) + if err := d.Set("cluster_certificates", readCloudHsm2ClusterCertificates(cluster)); err != nil { + return fmt.Errorf("error setting cluster_certificates: %s", err) + } + + var subnets []string + for _, sn := range cluster.SubnetMapping { + subnets = append(subnets, *sn) + } + + if err := d.Set("subnet_ids", subnets); err != nil { + return fmt.Errorf("[DEBUG] Error saving Subnet IDs to state for CloudHSM v2 Cluster (%s): %s", d.Id(), err) + } + + return nil +} diff --git a/aws/data_source_aws_cloudhsm2_cluster_test.go b/aws/data_source_aws_cloudhsm2_cluster_test.go new file mode 100644 index 00000000000..d9b608eeed8 --- /dev/null +++ b/aws/data_source_aws_cloudhsm2_cluster_test.go @@ -0,0 +1,73 @@ +package aws + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform/helper/acctest" + "github.com/hashicorp/terraform/helper/resource" +) + +func TestAccDataSourceCloudHsm2Cluster_basic(t *testing.T) { + resourceName := "aws_cloudhsm_v2_cluster.cluster" + dataSourceName := "data.aws_cloudhsm_v2_cluster.default" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccCheckCloudHsm2ClusterDataSourceConfig, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(dataSourceName, "cluster_state", "UNINITIALIZED"), + resource.TestCheckResourceAttrPair(dataSourceName, "cluster_id", resourceName, "cluster_id"), + resource.TestCheckResourceAttrPair(dataSourceName, "cluster_state", resourceName, "cluster_state"), + resource.TestCheckResourceAttrPair(dataSourceName, "security_group_id", resourceName, "security_group_id"), + resource.TestCheckResourceAttrPair(dataSourceName, "subnet_ids.#", resourceName, "subnet_ids.#"), + resource.TestCheckResourceAttrPair(dataSourceName, "vpc_id", resourceName, "vpc_id"), + ), + }, + }, + }) +} + +var testAccCheckCloudHsm2ClusterDataSourceConfig = fmt.Sprintf(` +variable "subnets" { + default = ["10.0.1.0/24", "10.0.2.0/24"] + type = "list" +} + +data "aws_availability_zones" "available" {} + +resource "aws_vpc" "cloudhsm2_test_vpc" { + cidr_block = "10.0.0.0/16" + + tags { + Name = "terraform-testacc-aws_cloudhsm_v2_cluster-data-source-basic" + } +} + +resource "aws_subnet" "cloudhsm2_test_subnets" { + count = 2 + vpc_id = "${aws_vpc.cloudhsm2_test_vpc.id}" + cidr_block = "${element(var.subnets, count.index)}" + map_public_ip_on_launch = false + availability_zone = "${element(data.aws_availability_zones.available.names, count.index)}" + + tags { + Name = "tf-acc-aws_cloudhsm_v2_cluster-data-source-basic" + } +} + +resource "aws_cloudhsm_v2_cluster" "cluster" { + hsm_type = "hsm1.medium" + subnet_ids = ["${aws_subnet.cloudhsm2_test_subnets.*.id}"] + tags { + Name = "tf-acc-aws_cloudhsm_v2_cluster-data-source-basic-%d" + } +} + +data "aws_cloudhsm_v2_cluster" "default" { + cluster_id = "${aws_cloudhsm_v2_cluster.cluster.cluster_id}" +} +`, acctest.RandInt()) diff --git a/aws/provider.go b/aws/provider.go index 6f97120a810..5fa47763f0e 100644 --- a/aws/provider.go +++ b/aws/provider.go @@ -177,6 +177,7 @@ func Provider() terraform.ResourceProvider { "aws_canonical_user_id": dataSourceAwsCanonicalUserId(), "aws_cloudformation_export": dataSourceAwsCloudFormationExport(), "aws_cloudformation_stack": dataSourceAwsCloudFormationStack(), + "aws_cloudhsm_v2_cluster": dataSourceCloudHsm2Cluster(), "aws_cloudtrail_service_account": dataSourceAwsCloudTrailServiceAccount(), "aws_cloudwatch_log_group": dataSourceAwsCloudwatchLogGroup(), "aws_cognito_user_pools": dataSourceAwsCognitoUserPools(), @@ -353,6 +354,8 @@ func Provider() terraform.ResourceProvider { "aws_cognito_user_pool": resourceAwsCognitoUserPool(), "aws_cognito_user_pool_client": resourceAwsCognitoUserPoolClient(), "aws_cognito_user_pool_domain": resourceAwsCognitoUserPoolDomain(), + "aws_cloudhsm_v2_cluster": resourceAwsCloudHsm2Cluster(), + "aws_cloudhsm_v2_hsm": resourceAwsCloudHsm2Hsm(), "aws_cognito_resource_server": resourceAwsCognitoResourceServer(), "aws_cloudwatch_metric_alarm": resourceAwsCloudWatchMetricAlarm(), "aws_cloudwatch_dashboard": resourceAwsCloudWatchDashboard(), diff --git a/aws/resource_aws_cloudhsm2_cluster.go b/aws/resource_aws_cloudhsm2_cluster.go new file mode 100644 index 00000000000..be666fef0de --- /dev/null +++ b/aws/resource_aws_cloudhsm2_cluster.go @@ -0,0 +1,363 @@ +package aws + +import ( + "fmt" + "github.com/hashicorp/terraform/helper/validation" + "log" + "time" + + "github.com/hashicorp/terraform/helper/schema" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/cloudhsmv2" + "github.com/hashicorp/terraform/helper/resource" +) + +func resourceAwsCloudHsm2Cluster() *schema.Resource { + return &schema.Resource{ + Create: resourceAwsCloudHsm2ClusterCreate, + Read: resourceAwsCloudHsm2ClusterRead, + Update: resourceAwsCloudHsm2ClusterUpdate, + Delete: resourceAwsCloudHsm2ClusterDelete, + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + + Timeouts: &schema.ResourceTimeout{ + Create: schema.DefaultTimeout(120 * time.Minute), + Update: schema.DefaultTimeout(120 * time.Minute), + Delete: schema.DefaultTimeout(120 * time.Minute), + }, + + Schema: map[string]*schema.Schema{ + "source_backup_identifier": { + Type: schema.TypeString, + Computed: false, + Optional: true, + ForceNew: true, + }, + + "hsm_type": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validation.StringInSlice([]string{"hsm1.medium"}, false), + }, + + "subnet_ids": { + Type: schema.TypeSet, + Required: true, + Elem: &schema.Schema{Type: schema.TypeString}, + Set: schema.HashString, + ForceNew: true, + }, + + "cluster_id": { + Type: schema.TypeString, + Computed: true, + }, + + "vpc_id": { + Type: schema.TypeString, + Computed: true, + }, + + "cluster_certificates": { + Type: schema.TypeList, + MaxItems: 1, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "cluster_certificate": { + Type: schema.TypeString, + Computed: true, + }, + "cluster_csr": { + Type: schema.TypeString, + Computed: true, + }, + "aws_hardware_certificate": { + Type: schema.TypeString, + Computed: true, + }, + "hsm_certificate": { + Type: schema.TypeString, + Computed: true, + }, + "manufacturer_hardware_certificate": { + Type: schema.TypeString, + Computed: true, + }, + }, + }, + }, + + "security_group_id": { + Type: schema.TypeString, + Computed: true, + }, + + "cluster_state": { + Type: schema.TypeString, + Computed: true, + }, + + "tags": tagsSchema(), + }, + } +} + +func describeCloudHsm2Cluster(conn *cloudhsmv2.CloudHSMV2, clusterId string) (*cloudhsmv2.Cluster, error) { + filters := []*string{&clusterId} + result := int64(1) + out, err := conn.DescribeClusters(&cloudhsmv2.DescribeClustersInput{ + Filters: map[string][]*string{ + "clusterIds": filters, + }, + MaxResults: &result, + }) + if err != nil { + log.Printf("[WARN] Error on retrieving CloudHSMv2 Cluster (%s) when waiting: %s", clusterId, err) + return nil, err + } + + var cluster *cloudhsmv2.Cluster + + for _, c := range out.Clusters { + if aws.StringValue(c.ClusterId) == clusterId { + cluster = c + break + } + } + return cluster, nil +} + +func resourceAwsCloudHsm2ClusterRefreshFunc(conn *cloudhsmv2.CloudHSMV2, clusterId string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + cluster, err := describeCloudHsm2Cluster(conn, clusterId) + + if cluster == nil { + return 42, "destroyed", nil + } + + if cluster.State != nil { + log.Printf("[DEBUG] CloudHSMv2 Cluster status (%s): %s", clusterId, *cluster.State) + } + + return cluster, aws.StringValue(cluster.State), err + } +} + +func resourceAwsCloudHsm2ClusterCreate(d *schema.ResourceData, meta interface{}) error { + cloudhsm2 := meta.(*AWSClient).cloudhsmv2conn + + input := &cloudhsmv2.CreateClusterInput{ + HsmType: aws.String(d.Get("hsm_type").(string)), + SubnetIds: expandStringSet(d.Get("subnet_ids").(*schema.Set)), + } + + backupId := d.Get("source_backup_identifier").(string) + if len(backupId) != 0 { + input.SourceBackupId = aws.String(backupId) + } + + log.Printf("[DEBUG] CloudHSMv2 Cluster create %s", input) + + var output *cloudhsmv2.CreateClusterOutput + + err := resource.Retry(180*time.Second, func() *resource.RetryError { + var err error + output, err = cloudhsm2.CreateCluster(input) + if err != nil { + if isAWSErr(err, cloudhsmv2.ErrCodeCloudHsmInternalFailureException, "request was rejected because of an AWS CloudHSM internal failure") { + log.Printf("[DEBUG] CloudHSMv2 Cluster re-try creating %s", input) + return resource.RetryableError(err) + } + return resource.NonRetryableError(err) + } + return nil + }) + + if err != nil { + return fmt.Errorf("error creating CloudHSMv2 Cluster: %s", err) + } + + d.SetId(aws.StringValue(output.Cluster.ClusterId)) + log.Printf("[INFO] CloudHSMv2 Cluster ID: %s", d.Id()) + log.Println("[INFO] Waiting for CloudHSMv2 Cluster to be available") + + targetState := cloudhsmv2.ClusterStateUninitialized + if len(backupId) > 0 { + targetState = cloudhsmv2.ClusterStateActive + } + + stateConf := &resource.StateChangeConf{ + Pending: []string{cloudhsmv2.ClusterStateCreateInProgress, cloudhsmv2.ClusterStateInitializeInProgress}, + Target: []string{targetState}, + Refresh: resourceAwsCloudHsm2ClusterRefreshFunc(cloudhsm2, d.Id()), + Timeout: d.Timeout(schema.TimeoutCreate), + MinTimeout: 30 * time.Second, + Delay: 30 * time.Second, + } + + // Wait, catching any errors + _, errWait := stateConf.WaitForState() + if errWait != nil { + if len(backupId) == 0 { + return fmt.Errorf("[WARN] Error waiting for CloudHSMv2 Cluster state to be \"UNINITIALIZED\": %s", errWait) + } else { + return fmt.Errorf("[WARN] Error waiting for CloudHSMv2 Cluster state to be \"ACTIVE\": %s", errWait) + } + } + + if err := setTagsAwsCloudHsm2Cluster(cloudhsm2, d); err != nil { + return err + } + + return resourceAwsCloudHsm2ClusterRead(d, meta) +} + +func resourceAwsCloudHsm2ClusterRead(d *schema.ResourceData, meta interface{}) error { + + cluster, err := describeCloudHsm2Cluster(meta.(*AWSClient).cloudhsmv2conn, d.Id()) + + if cluster == nil { + log.Printf("[WARN] CloudHSMv2 Cluster (%s) not found", d.Id()) + d.SetId("") + return err + } + + log.Printf("[INFO] Reading CloudHSMv2 Cluster Information: %s", d.Id()) + + d.Set("cluster_id", cluster.ClusterId) + d.Set("cluster_state", cluster.State) + d.Set("security_group_id", cluster.SecurityGroup) + d.Set("vpc_id", cluster.VpcId) + d.Set("source_backup_identifier", cluster.SourceBackupId) + d.Set("hsm_type", cluster.HsmType) + if err := d.Set("cluster_certificates", readCloudHsm2ClusterCertificates(cluster)); err != nil { + return fmt.Errorf("error setting cluster_certificates: %s", err) + } + + var subnets []string + for _, sn := range cluster.SubnetMapping { + subnets = append(subnets, aws.StringValue(sn)) + } + if err := d.Set("subnet_ids", subnets); err != nil { + return fmt.Errorf("Error saving Subnet IDs to state for CloudHSMv2 Cluster (%s): %s", d.Id(), err) + } + + return nil +} + +func resourceAwsCloudHsm2ClusterUpdate(d *schema.ResourceData, meta interface{}) error { + cloudhsm2 := meta.(*AWSClient).cloudhsmv2conn + + if err := setTagsAwsCloudHsm2Cluster(cloudhsm2, d); err != nil { + return err + } + + return resourceAwsCloudHsm2ClusterRead(d, meta) +} + +func resourceAwsCloudHsm2ClusterDelete(d *schema.ResourceData, meta interface{}) error { + cloudhsm2 := meta.(*AWSClient).cloudhsmv2conn + + log.Printf("[DEBUG] CloudHSMv2 Delete cluster: %s", d.Id()) + err := resource.Retry(180*time.Second, func() *resource.RetryError { + var err error + _, err = cloudhsm2.DeleteCluster(&cloudhsmv2.DeleteClusterInput{ + ClusterId: aws.String(d.Id()), + }) + if err != nil { + if isAWSErr(err, cloudhsmv2.ErrCodeCloudHsmInternalFailureException, "request was rejected because of an AWS CloudHSM internal failure") { + log.Printf("[DEBUG] CloudHSMv2 Cluster re-try deleting %s", d.Id()) + return resource.RetryableError(err) + } + return resource.NonRetryableError(err) + } + return nil + }) + + if err != nil { + return err + } + log.Println("[INFO] Waiting for CloudHSMv2 Cluster to be deleted") + + stateConf := &resource.StateChangeConf{ + Pending: []string{cloudhsmv2.ClusterStateDeleteInProgress}, + Target: []string{cloudhsmv2.ClusterStateDeleted}, + Refresh: resourceAwsCloudHsm2ClusterRefreshFunc(cloudhsm2, d.Id()), + Timeout: d.Timeout(schema.TimeoutCreate), + MinTimeout: 30 * time.Second, + Delay: 30 * time.Second, + } + + // Wait, catching any errors + _, errWait := stateConf.WaitForState() + if errWait != nil { + return fmt.Errorf("Error waiting for CloudHSMv2 Cluster state to be \"DELETED\": %s", errWait) + } + + return nil +} + +func setTagsAwsCloudHsm2Cluster(conn *cloudhsmv2.CloudHSMV2, d *schema.ResourceData) error { + if d.HasChange("tags") { + oraw, nraw := d.GetChange("tags") + create, remove := diffTagsGeneric(oraw.(map[string]interface{}), nraw.(map[string]interface{})) + + if len(remove) > 0 { + log.Printf("[DEBUG] Removing tags: %#v", remove) + keys := make([]*string, 0, len(remove)) + for k := range remove { + keys = append(keys, aws.String(k)) + } + + _, err := conn.UntagResource(&cloudhsmv2.UntagResourceInput{ + ResourceId: aws.String(d.Id()), + TagKeyList: keys, + }) + if err != nil { + return err + } + } + if len(create) > 0 { + log.Printf("[DEBUG] Creating tags: %#v", create) + tagList := make([]*cloudhsmv2.Tag, 0, len(create)) + for k, v := range create { + tagList = append(tagList, &cloudhsmv2.Tag{ + Key: &k, + Value: v, + }) + } + _, err := conn.TagResource(&cloudhsmv2.TagResourceInput{ + ResourceId: aws.String(d.Id()), + TagList: tagList, + }) + if err != nil { + return err + } + } + } + + return nil +} + +func readCloudHsm2ClusterCertificates(cluster *cloudhsmv2.Cluster) []map[string]interface{} { + certs := map[string]interface{}{} + if cluster.Certificates != nil { + if aws.StringValue(cluster.State) == "UNINITIALIZED" { + certs["cluster_csr"] = aws.StringValue(cluster.Certificates.ClusterCsr) + certs["aws_hardware_certificate"] = aws.StringValue(cluster.Certificates.AwsHardwareCertificate) + certs["hsm_certificate"] = aws.StringValue(cluster.Certificates.HsmCertificate) + certs["manufacturer_hardware_certificate"] = aws.StringValue(cluster.Certificates.ManufacturerHardwareCertificate) + } else if aws.StringValue(cluster.State) == "ACTIVE" { + certs["cluster_certificate"] = aws.StringValue(cluster.Certificates.ClusterCertificate) + } + } + if len(certs) > 0 { + return []map[string]interface{}{certs} + } + return []map[string]interface{}{} +} diff --git a/aws/resource_aws_cloudhsm2_cluster_test.go b/aws/resource_aws_cloudhsm2_cluster_test.go new file mode 100644 index 00000000000..917310ca05a --- /dev/null +++ b/aws/resource_aws_cloudhsm2_cluster_test.go @@ -0,0 +1,113 @@ +package aws + +import ( + "fmt" + "testing" + + "github.com/aws/aws-sdk-go/aws" + "github.com/hashicorp/terraform/helper/acctest" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +func TestAccAWSCloudHsm2Cluster_basic(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSCloudHsm2ClusterDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSCloudHsm2Cluster(), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSCloudHsm2ClusterExists("aws_cloudhsm_v2_cluster.cluster"), + resource.TestCheckResourceAttrSet("aws_cloudhsm_v2_cluster.cluster", "cluster_id"), + resource.TestCheckResourceAttrSet("aws_cloudhsm_v2_cluster.cluster", "vpc_id"), + resource.TestCheckResourceAttrSet("aws_cloudhsm_v2_cluster.cluster", "security_group_id"), + resource.TestCheckResourceAttrSet("aws_cloudhsm_v2_cluster.cluster", "cluster_state"), + ), + }, + { + ResourceName: "aws_cloudhsm_v2_cluster.cluster", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"cluster_certificates", "tags"}, + }, + }, + }) +} + +func testAccAWSCloudHsm2Cluster() string { + return fmt.Sprintf(` +variable "subnets" { + default = ["10.0.1.0/24", "10.0.2.0/24"] + type = "list" +} + +data "aws_availability_zones" "available" {} + +resource "aws_vpc" "cloudhsm2_test_vpc" { + cidr_block = "10.0.0.0/16" + + tags { + Name = "terraform-testacc-aws_cloudhsm_v2_cluster-resource-basic" + } +} + +resource "aws_subnet" "cloudhsm2_test_subnets" { + count = 2 + vpc_id = "${aws_vpc.cloudhsm2_test_vpc.id}" + cidr_block = "${element(var.subnets, count.index)}" + map_public_ip_on_launch = false + availability_zone = "${element(data.aws_availability_zones.available.names, count.index)}" + + tags { + Name = "tf-acc-aws_cloudhsm_v2_cluster-resource-basic" + } +} + +resource "aws_cloudhsm_v2_cluster" "cluster" { + hsm_type = "hsm1.medium" + subnet_ids = ["${aws_subnet.cloudhsm2_test_subnets.*.id}"] + tags { + Name = "tf-acc-aws_cloudhsm_v2_cluster-resource-basic-%d" + } +} +`, acctest.RandInt()) +} + +func testAccCheckAWSCloudHsm2ClusterDestroy(s *terraform.State) error { + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_cloudhsm_v2_cluster" { + continue + } + cluster, err := describeCloudHsm2Cluster(testAccProvider.Meta().(*AWSClient).cloudhsmv2conn, rs.Primary.ID) + + if err != nil { + return err + } + + if cluster != nil && aws.StringValue(cluster.State) != "DELETED" { + return fmt.Errorf("CloudHSM cluster still exists %s", cluster) + } + } + + return nil +} + +func testAccCheckAWSCloudHsm2ClusterExists(name string) resource.TestCheckFunc { + return func(s *terraform.State) error { + conn := testAccProvider.Meta().(*AWSClient).cloudhsmv2conn + it, ok := s.RootModule().Resources[name] + if !ok { + return fmt.Errorf("Not found: %s", name) + } + + _, err := describeCloudHsm2Cluster(conn, it.Primary.ID) + + if err != nil { + return fmt.Errorf("CloudHSM cluster not found: %s", err) + } + + return nil + } +} diff --git a/aws/resource_aws_cloudhsm2_hsm.go b/aws/resource_aws_cloudhsm2_hsm.go new file mode 100644 index 00000000000..0bb93ff97ec --- /dev/null +++ b/aws/resource_aws_cloudhsm2_hsm.go @@ -0,0 +1,261 @@ +package aws + +import ( + "fmt" + "log" + "time" + + "github.com/hashicorp/terraform/helper/schema" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/cloudhsmv2" + "github.com/hashicorp/terraform/helper/resource" +) + +func resourceAwsCloudHsm2Hsm() *schema.Resource { + return &schema.Resource{ + Create: resourceAwsCloudHsm2HsmCreate, + Read: resourceAwsCloudHsm2HsmRead, + Delete: resourceAwsCloudHsm2HsmDelete, + Importer: &schema.ResourceImporter{ + State: resourceAwsCloudHsm2HsmImport, + }, + + Timeouts: &schema.ResourceTimeout{ + Create: schema.DefaultTimeout(120 * time.Minute), + Update: schema.DefaultTimeout(120 * time.Minute), + Delete: schema.DefaultTimeout(120 * time.Minute), + }, + + Schema: map[string]*schema.Schema{ + "cluster_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + "subnet_id": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + }, + + "availability_zone": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + }, + + "ip_address": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + }, + + "hsm_id": { + Type: schema.TypeString, + Computed: true, + }, + + "hsm_state": { + Type: schema.TypeString, + Computed: true, + }, + + "hsm_eni_id": { + Type: schema.TypeString, + Computed: true, + }, + }, + } +} + +func resourceAwsCloudHsm2HsmImport( + d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { + d.Set("hsm_id", d.Id()) + return []*schema.ResourceData{d}, nil +} + +func describeHsm(conn *cloudhsmv2.CloudHSMV2, hsmId string) (*cloudhsmv2.Hsm, error) { + out, err := conn.DescribeClusters(&cloudhsmv2.DescribeClustersInput{}) + if err != nil { + log.Printf("[WARN] Error on descibing CloudHSM v2 Cluster: %s", err) + return nil, err + } + + var hsm *cloudhsmv2.Hsm + + for _, c := range out.Clusters { + for _, h := range c.Hsms { + if aws.StringValue(h.HsmId) == hsmId { + hsm = h + break + } + } + } + + return hsm, nil +} + +func resourceAwsCloudHsm2HsmRefreshFunc( + d *schema.ResourceData, meta interface{}) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + hsm, err := describeHsm(meta.(*AWSClient).cloudhsmv2conn, d.Id()) + + if hsm == nil { + return 42, "destroyed", nil + } + + if hsm.State != nil { + log.Printf("[DEBUG] CloudHSMv2 Cluster status (%s): %s", d.Id(), *hsm.State) + } + + return hsm, aws.StringValue(hsm.State), err + } +} + +func resourceAwsCloudHsm2HsmCreate(d *schema.ResourceData, meta interface{}) error { + cloudhsm2 := meta.(*AWSClient).cloudhsmv2conn + + clusterId := d.Get("cluster_id").(string) + + cluster, err := describeCloudHsm2Cluster(cloudhsm2, clusterId) + + if cluster == nil { + log.Printf("[WARN] Error on retrieving CloudHSMv2 Cluster: %s %s", clusterId, err) + return err + } + + availabilityZone := d.Get("availability_zone").(string) + if len(availabilityZone) == 0 { + subnetId := d.Get("subnet_id").(string) + for az, sn := range cluster.SubnetMapping { + if aws.StringValue(sn) == subnetId { + availabilityZone = az + } + } + } + + input := &cloudhsmv2.CreateHsmInput{ + ClusterId: aws.String(clusterId), + AvailabilityZone: aws.String(availabilityZone), + } + + ipAddress := d.Get("ip_address").(string) + if len(ipAddress) != 0 { + input.IpAddress = aws.String(ipAddress) + } + + log.Printf("[DEBUG] CloudHSMv2 HSM create %s", input) + + var output *cloudhsmv2.CreateHsmOutput + + errRetry := resource.Retry(180*time.Second, func() *resource.RetryError { + var err error + output, err = cloudhsm2.CreateHsm(input) + if err != nil { + if isAWSErr(err, cloudhsmv2.ErrCodeCloudHsmInternalFailureException, "request was rejected because of an AWS CloudHSM internal failure") { + log.Printf("[DEBUG] CloudHSMv2 HSM re-try creating %s", input) + return resource.RetryableError(err) + } + return resource.NonRetryableError(err) + } + return nil + }) + + if errRetry != nil { + return fmt.Errorf("error creating CloudHSM v2 HSM module: %s", errRetry) + } + + d.SetId(aws.StringValue(output.Hsm.HsmId)) + log.Printf("[INFO] CloudHSMv2 HSM Id: %s", d.Id()) + log.Println("[INFO] Waiting for CloudHSMv2 HSM to be available") + + stateConf := &resource.StateChangeConf{ + Pending: []string{cloudhsmv2.HsmStateCreateInProgress, "destroyed"}, + Target: []string{cloudhsmv2.HsmStateActive}, + Refresh: resourceAwsCloudHsm2HsmRefreshFunc(d, meta), + Timeout: d.Timeout(schema.TimeoutCreate), + MinTimeout: 30 * time.Second, + Delay: 30 * time.Second, + } + + // Wait, catching any errors + _, errWait := stateConf.WaitForState() + if errWait != nil { + return fmt.Errorf("Error waiting for CloudHSMv2 HSM state to be \"ACTIVE\": %s", errWait) + } + + return resourceAwsCloudHsm2HsmRead(d, meta) +} + +func resourceAwsCloudHsm2HsmRead(d *schema.ResourceData, meta interface{}) error { + + hsm, err := describeHsm(meta.(*AWSClient).cloudhsmv2conn, d.Id()) + + if hsm == nil { + log.Printf("[WARN] CloudHSMv2 HSM (%s) not found", d.Id()) + d.SetId("") + return err + } + + log.Printf("[INFO] Reading CloudHSMv2 HSM Information: %s", d.Id()) + + d.Set("cluster_id", hsm.ClusterId) + d.Set("subnet_id", hsm.SubnetId) + d.Set("availability_zone", hsm.AvailabilityZone) + d.Set("ip_address", hsm.EniIp) + d.Set("hsm_id", hsm.HsmId) + d.Set("hsm_state", hsm.State) + d.Set("hsm_eni_id", hsm.EniId) + + return nil +} + +func resourceAwsCloudHsm2HsmDelete(d *schema.ResourceData, meta interface{}) error { + cloudhsm2 := meta.(*AWSClient).cloudhsmv2conn + clusterId := d.Get("cluster_id").(string) + + log.Printf("[DEBUG] CloudHSMv2 HSM delete %s %s", clusterId, d.Id()) + + errRetry := resource.Retry(180*time.Second, func() *resource.RetryError { + var err error + _, err = cloudhsm2.DeleteHsm(&cloudhsmv2.DeleteHsmInput{ + ClusterId: aws.String(clusterId), + HsmId: aws.String(d.Id()), + }) + if err != nil { + if isAWSErr(err, cloudhsmv2.ErrCodeCloudHsmInternalFailureException, "request was rejected because of an AWS CloudHSM internal failure") { + log.Printf("[DEBUG] CloudHSMv2 HSM re-try deleting %s", d.Id()) + return resource.RetryableError(err) + } + return resource.NonRetryableError(err) + } + return nil + }) + + if errRetry != nil { + return fmt.Errorf("error deleting CloudHSM v2 HSM module (%s): %s", d.Id(), errRetry) + } + log.Println("[INFO] Waiting for CloudHSMv2 HSM to be deleted") + + stateConf := &resource.StateChangeConf{ + Pending: []string{cloudhsmv2.HsmStateDeleteInProgress}, + Target: []string{"destroyed"}, + Refresh: resourceAwsCloudHsm2HsmRefreshFunc(d, meta), + Timeout: d.Timeout(schema.TimeoutCreate), + MinTimeout: 30 * time.Second, + Delay: 30 * time.Second, + } + + // Wait, catching any errors + _, errWait := stateConf.WaitForState() + if errWait != nil { + return fmt.Errorf("Error waiting for CloudHSMv2 HSM state to be \"DELETED\": %s", errWait) + } + + return nil +} diff --git a/aws/resource_aws_cloudhsm2_hsm_test.go b/aws/resource_aws_cloudhsm2_hsm_test.go new file mode 100644 index 00000000000..310d8169f59 --- /dev/null +++ b/aws/resource_aws_cloudhsm2_hsm_test.go @@ -0,0 +1,120 @@ +package aws + +import ( + "fmt" + "testing" + + "github.com/aws/aws-sdk-go/aws" + "github.com/hashicorp/terraform/helper/acctest" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +func TestAccAWSCloudHsm2Hsm_basic(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSCloudHsm2HsmDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSCloudHsm2Hsm(), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSCloudHsm2HsmExists("aws_cloudhsm_v2_hsm.hsm"), + resource.TestCheckResourceAttrSet("aws_cloudhsm_v2_hsm.hsm", "hsm_id"), + resource.TestCheckResourceAttrSet("aws_cloudhsm_v2_hsm.hsm", "hsm_state"), + resource.TestCheckResourceAttrSet("aws_cloudhsm_v2_hsm.hsm", "hsm_eni_id"), + resource.TestCheckResourceAttrSet("aws_cloudhsm_v2_hsm.hsm", "ip_address"), + ), + }, + { + ResourceName: "aws_cloudhsm_v2_hsm.hsm", + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func testAccAWSCloudHsm2Hsm() string { + return fmt.Sprintf(` +variable "subnets" { + default = ["10.0.1.0/24", "10.0.2.0/24"] + type = "list" +} + +data "aws_availability_zones" "available" {} + +resource "aws_vpc" "cloudhsm2_test_vpc" { + cidr_block = "10.0.0.0/16" + + tags { + Name = "terraform-testacc-aws_cloudhsm_v2_hsm-resource-basic" + } +} + +resource "aws_subnet" "cloudhsm2_test_subnets" { + count = 2 + vpc_id = "${aws_vpc.cloudhsm2_test_vpc.id}" + cidr_block = "${element(var.subnets, count.index)}" + map_public_ip_on_launch = false + availability_zone = "${element(data.aws_availability_zones.available.names, count.index)}" + + tags { + Name = "tf-acc-aws_cloudhsm_v2_hsm-resource-basic" + } +} + +resource "aws_cloudhsm_v2_cluster" "cloudhsm_v2_cluster" { + hsm_type = "hsm1.medium" + subnet_ids = ["${aws_subnet.cloudhsm2_test_subnets.*.id}"] + tags { + Name = "tf-acc-aws_cloudhsm_v2_hsm-resource-basic-%d" + } +} + +resource "aws_cloudhsm_v2_hsm" "hsm" { + subnet_id = "${aws_subnet.cloudhsm2_test_subnets.0.id}" + cluster_id = "${aws_cloudhsm_v2_cluster.cloudhsm_v2_cluster.cluster_id}" +} +`, acctest.RandInt()) +} + +func testAccCheckAWSCloudHsm2HsmDestroy(s *terraform.State) error { + conn := testAccProvider.Meta().(*AWSClient).cloudhsmv2conn + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_cloudhsm_v2_hsm" { + continue + } + + hsm, err := describeHsm(conn, rs.Primary.ID) + + if err != nil { + return err + } + + if hsm != nil && aws.StringValue(hsm.State) != "DELETED" { + return fmt.Errorf("HSM still exists:\n%s", hsm) + } + } + + return nil +} + +func testAccCheckAWSCloudHsm2HsmExists(name string) resource.TestCheckFunc { + return func(s *terraform.State) error { + conn := testAccProvider.Meta().(*AWSClient).cloudhsmv2conn + + it, ok := s.RootModule().Resources[name] + if !ok { + return fmt.Errorf("Not found: %s", name) + } + + _, err := describeHsm(conn, it.Primary.ID) + if err != nil { + return fmt.Errorf("CloudHSM cluster not found: %s", err) + } + + return nil + } +} diff --git a/examples/cloudhsm/main.tf b/examples/cloudhsm/main.tf new file mode 100644 index 00000000000..46a8adbd94c --- /dev/null +++ b/examples/cloudhsm/main.tf @@ -0,0 +1,43 @@ +provider "aws" { + region = "${var.aws_region}" +} + +data "aws_availability_zones" "available" {} + +resource "aws_vpc" "cloudhsm2_vpc" { + cidr_block = "10.0.0.0/16" + + tags { + Name = "example-aws_cloudhsm_v2_cluster" + } +} + +resource "aws_subnet" "cloudhsm2_subnets" { + count = 2 + vpc_id = "${aws_vpc.cloudhsm2_vpc.id}" + cidr_block = "${element(var.subnets, count.index)}" + map_public_ip_on_launch = false + availability_zone = "${element(data.aws_availability_zones.available.names, count.index)}" + + tags { + Name = "example-aws_cloudhsm_v2_cluster" + } +} + +resource "aws_cloudhsm_v2_cluster" "cloudhsm_v2_cluster" { + hsm_type = "hsm1.medium" + subnet_ids = ["${aws_subnet.cloudhsm2_subnets.*.id}"] + tags { + Name = "example-aws_cloudhsm_v2_cluster" + } +} + +resource "aws_cloudhsm_v2_hsm" "cloudhsm_v2_hsm" { + subnet_id = "${aws_subnet.cloudhsm2_subnets.0.id}" + cluster_id = "${aws_cloudhsm_v2_cluster.cloudhsm_v2_cluster.cluster_id}" +} + +data "aws_cloudhsm_v2_cluster" "cluster" { + cluster_id = "${aws_cloudhsm_v2_cluster.cloudhsm_v2_cluster.cluster_id}" + depends_on = ["aws_cloudhsm_v2_hsm.cloudhsm_v2_hsm"] +} diff --git a/examples/cloudhsm/outputs.tf b/examples/cloudhsm/outputs.tf new file mode 100644 index 00000000000..06eee1c1601 --- /dev/null +++ b/examples/cloudhsm/outputs.tf @@ -0,0 +1,7 @@ +output "hsm_ip_address" { + value = "${aws_cloudhsm_v2_hsm.cloudhsm_v2_hsm.ip_address}" +} + +output "cluster_data_certificate" { + value = "${data.aws_cloudhsm_v2_cluster.cluster.cluster_certificates.0.cluster_csr}" +} diff --git a/examples/cloudhsm/variables.tf b/examples/cloudhsm/variables.tf new file mode 100644 index 00000000000..31cdfd980ac --- /dev/null +++ b/examples/cloudhsm/variables.tf @@ -0,0 +1,9 @@ +variable "aws_region" { + description = "AWS region to launch cloudHSM cluster." + default = "eu-west-1" +} + +variable "subnets" { + default = ["10.0.1.0/24", "10.0.2.0/24"] + type = "list" +} diff --git a/website/aws.erb b/website/aws.erb index 7281a193c34..48c0dc5c6a5 100644 --- a/website/aws.erb +++ b/website/aws.erb @@ -97,6 +97,9 @@