diff --git a/builtin/providers/aws/provider.go b/builtin/providers/aws/provider.go index 7b7aaabd5..b02119ed2 100644 --- a/builtin/providers/aws/provider.go +++ b/builtin/providers/aws/provider.go @@ -286,6 +286,7 @@ func Provider() terraform.ResourceProvider { "aws_lambda_permission": resourceAwsLambdaPermission(), "aws_launch_configuration": resourceAwsLaunchConfiguration(), "aws_lightsail_instance": resourceAwsLightsailInstance(), + "aws_lightsail_key_pair": resourceAwsLightsailKeyPair(), "aws_lb_cookie_stickiness_policy": resourceAwsLBCookieStickinessPolicy(), "aws_load_balancer_policy": resourceAwsLoadBalancerPolicy(), "aws_load_balancer_backend_server_policy": resourceAwsLoadBalancerBackendServerPolicies(), diff --git a/builtin/providers/aws/resource_aws_iam_user_login_profile_test.go b/builtin/providers/aws/resource_aws_iam_user_login_profile_test.go index fe4b6f3d4..24e39f9e4 100644 --- a/builtin/providers/aws/resource_aws_iam_user_login_profile_test.go +++ b/builtin/providers/aws/resource_aws_iam_user_login_profile_test.go @@ -6,6 +6,8 @@ import ( "testing" "time" + "regexp" + "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/awserr" "github.com/aws/aws-sdk-go/aws/credentials" @@ -15,7 +17,6 @@ import ( "github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/terraform" "github.com/hashicorp/vault/helper/pgpkeys" - "regexp" ) func TestAccAWSUserLoginProfile_basic(t *testing.T) { diff --git a/builtin/providers/aws/resource_aws_lightsail_instance.go b/builtin/providers/aws/resource_aws_lightsail_instance.go index b572b083c..34f249573 100644 --- a/builtin/providers/aws/resource_aws_lightsail_instance.go +++ b/builtin/providers/aws/resource_aws_lightsail_instance.go @@ -141,7 +141,7 @@ func resourceAwsLightsailInstanceCreate(d *schema.ResourceData, meta interface{} stateConf := &resource.StateChangeConf{ Pending: []string{"Started"}, Target: []string{"Completed", "Succeeded"}, - Refresh: resourceAwsLightsailInstanceOperationRefreshFunc(op.Id, meta), + Refresh: resourceAwsLightsailOperationRefreshFunc(op.Id, meta), Timeout: 10 * time.Minute, Delay: 5 * time.Second, MinTimeout: 3 * time.Second, @@ -217,7 +217,7 @@ func resourceAwsLightsailInstanceDelete(d *schema.ResourceData, meta interface{} stateConf := &resource.StateChangeConf{ Pending: []string{"Started"}, Target: []string{"Completed", "Succeeded"}, - Refresh: resourceAwsLightsailInstanceOperationRefreshFunc(op.Id, meta), + Refresh: resourceAwsLightsailOperationRefreshFunc(op.Id, meta), Timeout: 10 * time.Minute, Delay: 5 * time.Second, MinTimeout: 3 * time.Second, @@ -242,11 +242,11 @@ func resourceAwsLightsailInstanceDelete(d *schema.ResourceData, meta interface{} // - Failed // - Completed // - Succeeded (not documented?) -func resourceAwsLightsailInstanceOperationRefreshFunc( +func resourceAwsLightsailOperationRefreshFunc( oid *string, meta interface{}) resource.StateRefreshFunc { return func() (interface{}, string, error) { conn := meta.(*AWSClient).lightsailconn - log.Printf("[DEBUG] Checking if Lightsail Instance Operation (%s) is Completed", *oid) + log.Printf("[DEBUG] Checking if Lightsail Operation (%s) is Completed", *oid) o, err := conn.GetOperation(&lightsail.GetOperationInput{ OperationId: oid, }) @@ -258,7 +258,7 @@ func resourceAwsLightsailInstanceOperationRefreshFunc( return nil, "Failed", fmt.Errorf("[ERR] Error retrieving Operation info for operation (%s)", *oid) } - log.Printf("[DEBUG] Lightsail Instance Operation (%s) is currently %q", *oid, *o.Operation.Status) + log.Printf("[DEBUG] Lightsail Operation (%s) is currently %q", *oid, *o.Operation.Status) return o, *o.Operation.Status, nil } } diff --git a/builtin/providers/aws/resource_aws_lightsail_key_pair.go b/builtin/providers/aws/resource_aws_lightsail_key_pair.go new file mode 100644 index 000000000..24138aaa9 --- /dev/null +++ b/builtin/providers/aws/resource_aws_lightsail_key_pair.go @@ -0,0 +1,225 @@ +package aws + +import ( + "fmt" + "log" + "time" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/awserr" + "github.com/aws/aws-sdk-go/service/lightsail" + "github.com/hashicorp/terraform/helper/encryption" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/helper/schema" +) + +func resourceAwsLightsailKeyPair() *schema.Resource { + return &schema.Resource{ + Create: resourceAwsLightsailKeyPairCreate, + Read: resourceAwsLightsailKeyPairRead, + Delete: resourceAwsLightsailKeyPairDelete, + + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + ConflictsWith: []string{"name_prefix"}, + }, + "name_prefix": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + + // optional fields + "pgp_key": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + + // additional info returned from the API + "arn": { + Type: schema.TypeString, + Computed: true, + }, + + // fields returned from CreateKey + "fingerprint": { + Type: schema.TypeString, + Computed: true, + }, + "public_key": { + Type: schema.TypeString, + Computed: true, + Optional: true, + ForceNew: true, + }, + "private_key": { + Type: schema.TypeString, + Computed: true, + }, + + // encrypted fields if pgp_key is given + "encrypted_fingerprint": { + Type: schema.TypeString, + Computed: true, + }, + "encrypted_private_key": { + Type: schema.TypeString, + Computed: true, + }, + }, + } +} + +func resourceAwsLightsailKeyPairCreate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).lightsailconn + + var kName string + if v, ok := d.GetOk("name"); ok { + kName = v.(string) + } else if v, ok := d.GetOk("name_prefix"); ok { + kName = resource.PrefixedUniqueId(v.(string)) + } else { + kName = resource.UniqueId() + } + + var pubKey string + var op *lightsail.Operation + if pubKeyInterface, ok := d.GetOk("public_key"); ok { + pubKey = pubKeyInterface.(string) + } + + if pubKey == "" { + // creating new key + resp, err := conn.CreateKeyPair(&lightsail.CreateKeyPairInput{ + KeyPairName: aws.String(kName), + }) + if err != nil { + return err + } + if resp.Operation == nil { + return fmt.Errorf("[ERR] No operation found for CreateKeyPair response") + } + if resp.KeyPair == nil { + return fmt.Errorf("[ERR] No KeyPair information found for CreateKeyPair response") + } + d.SetId(kName) + + // private_key and public_key are only available in the response from + // CreateKey pair. Here we set the public_key, and encrypt the private_key + // if a pgp_key is given, else we store the private_key in state + d.Set("public_key", resp.PublicKeyBase64) + + // encrypt private key if pgp_key is given + pgpKey, err := encryption.RetrieveGPGKey(d.Get("pgp_key").(string)) + if err != nil { + return err + } + if pgpKey != "" { + fingerprint, encrypted, err := encryption.EncryptValue(pgpKey, *resp.PrivateKeyBase64, "Lightsail Private Key") + if err != nil { + return err + } + + d.Set("encrypted_fingerprint", fingerprint) + d.Set("encrypted_private_key", encrypted) + } else { + d.Set("private_key", resp.PrivateKeyBase64) + } + + op = resp.Operation + } else { + // importing key + resp, err := conn.ImportKeyPair(&lightsail.ImportKeyPairInput{ + KeyPairName: aws.String(kName), + PublicKeyBase64: aws.String(pubKey), + }) + + if err != nil { + log.Printf("[ERR] Error importing key: %s", err) + return err + } + d.SetId(kName) + + op = resp.Operation + } + + stateConf := &resource.StateChangeConf{ + Pending: []string{"Started"}, + Target: []string{"Completed", "Succeeded"}, + Refresh: resourceAwsLightsailOperationRefreshFunc(op.Id, meta), + Timeout: 10 * time.Minute, + Delay: 5 * time.Second, + MinTimeout: 3 * time.Second, + } + + _, err := stateConf.WaitForState() + if err != nil { + // We don't return an error here because the Create call succeded + log.Printf("[ERR] Error waiting for KeyPair (%s) to become ready: %s", d.Id(), err) + } + + return resourceAwsLightsailKeyPairRead(d, meta) +} + +func resourceAwsLightsailKeyPairRead(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).lightsailconn + + resp, err := conn.GetKeyPair(&lightsail.GetKeyPairInput{ + KeyPairName: aws.String(d.Id()), + }) + + if err != nil { + log.Printf("[WARN] Error getting KeyPair (%s): %s", d.Id(), err) + // check for known not found error + if awsErr, ok := err.(awserr.Error); ok { + if awsErr.Code() == "NotFoundException" { + log.Printf("[WARN] Lightsail KeyPair (%s) not found, removing from state", d.Id()) + d.SetId("") + return nil + } + } + return err + } + + d.Set("arn", resp.KeyPair.Arn) + d.Set("name", resp.KeyPair.Name) + d.Set("fingerprint", resp.KeyPair.Fingerprint) + + return nil +} + +func resourceAwsLightsailKeyPairDelete(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).lightsailconn + resp, err := conn.DeleteKeyPair(&lightsail.DeleteKeyPairInput{ + KeyPairName: aws.String(d.Id()), + }) + + if err != nil { + return err + } + + op := resp.Operation + stateConf := &resource.StateChangeConf{ + Pending: []string{"Started"}, + Target: []string{"Completed", "Succeeded"}, + Refresh: resourceAwsLightsailOperationRefreshFunc(op.Id, meta), + Timeout: 10 * time.Minute, + Delay: 5 * time.Second, + MinTimeout: 3 * time.Second, + } + + _, err = stateConf.WaitForState() + if err != nil { + return fmt.Errorf( + "Error waiting for KeyPair (%s) to become destroyed: %s", + d.Id(), err) + } + + d.SetId("") + return nil +} diff --git a/builtin/providers/aws/resource_aws_lightsail_key_pair_test.go b/builtin/providers/aws/resource_aws_lightsail_key_pair_test.go new file mode 100644 index 000000000..e2ff906e8 --- /dev/null +++ b/builtin/providers/aws/resource_aws_lightsail_key_pair_test.go @@ -0,0 +1,247 @@ +package aws + +import ( + "errors" + "fmt" + "testing" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/awserr" + "github.com/aws/aws-sdk-go/service/lightsail" + "github.com/hashicorp/terraform/helper/acctest" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +func TestAccAWSLightsailKeyPair_basic(t *testing.T) { + var conf lightsail.KeyPair + lightsailName := fmt.Sprintf("tf-test-lightsail-%d", acctest.RandInt()) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSLightsailKeyPairDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSLightsailKeyPairConfig_basic(lightsailName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckAWSLightsailKeyPairExists("aws_lightsail_key_pair.lightsail_key_pair_test", &conf), + resource.TestCheckResourceAttrSet("aws_lightsail_key_pair.lightsail_key_pair_test", "arn"), + resource.TestCheckResourceAttrSet("aws_lightsail_key_pair.lightsail_key_pair_test", "fingerprint"), + resource.TestCheckResourceAttrSet("aws_lightsail_key_pair.lightsail_key_pair_test", "public_key"), + resource.TestCheckResourceAttrSet("aws_lightsail_key_pair.lightsail_key_pair_test", "private_key"), + ), + }, + }, + }) +} + +func TestAccAWSLightsailKeyPair_imported(t *testing.T) { + var conf lightsail.KeyPair + lightsailName := fmt.Sprintf("tf-test-lightsail-%d", acctest.RandInt()) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSLightsailKeyPairDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSLightsailKeyPairConfig_imported(lightsailName, testLightsailKeyPairPubKey1), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckAWSLightsailKeyPairExists("aws_lightsail_key_pair.lightsail_key_pair_test", &conf), + resource.TestCheckResourceAttrSet("aws_lightsail_key_pair.lightsail_key_pair_test", "arn"), + resource.TestCheckResourceAttrSet("aws_lightsail_key_pair.lightsail_key_pair_test", "fingerprint"), + resource.TestCheckResourceAttrSet("aws_lightsail_key_pair.lightsail_key_pair_test", "public_key"), + resource.TestCheckResourceAttr("aws_lightsail_key_pair.lightsail_key_pair_test", "encrypted_fingerprint", ""), + resource.TestCheckResourceAttr("aws_lightsail_key_pair.lightsail_key_pair_test", "encrypted_private_key", ""), + resource.TestCheckResourceAttr("aws_lightsail_key_pair.lightsail_key_pair_test", "private_key", ""), + ), + }, + }, + }) +} + +func TestAccAWSLightsailKeyPair_encrypted(t *testing.T) { + var conf lightsail.KeyPair + lightsailName := fmt.Sprintf("tf-test-lightsail-%d", acctest.RandInt()) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSLightsailKeyPairDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSLightsailKeyPairConfig_encrypted(lightsailName, testLightsailKeyPairPubKey1), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckAWSLightsailKeyPairExists("aws_lightsail_key_pair.lightsail_key_pair_test", &conf), + resource.TestCheckResourceAttrSet("aws_lightsail_key_pair.lightsail_key_pair_test", "arn"), + resource.TestCheckResourceAttrSet("aws_lightsail_key_pair.lightsail_key_pair_test", "fingerprint"), + resource.TestCheckResourceAttrSet("aws_lightsail_key_pair.lightsail_key_pair_test", "encrypted_fingerprint"), + resource.TestCheckResourceAttrSet("aws_lightsail_key_pair.lightsail_key_pair_test", "encrypted_private_key"), + resource.TestCheckResourceAttrSet("aws_lightsail_key_pair.lightsail_key_pair_test", "public_key"), + resource.TestCheckResourceAttr("aws_lightsail_key_pair.lightsail_key_pair_test", "private_key", ""), + ), + }, + }, + }) +} + +func TestAccAWSLightsailKeyPair_nameprefix(t *testing.T) { + var conf1, conf2 lightsail.KeyPair + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSLightsailKeyPairDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSLightsailKeyPairConfig_prefixed(), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckAWSLightsailKeyPairExists("aws_lightsail_key_pair.lightsail_key_pair_test_omit", &conf1), + testAccCheckAWSLightsailKeyPairExists("aws_lightsail_key_pair.lightsail_key_pair_test_prefixed", &conf2), + resource.TestCheckResourceAttrSet("aws_lightsail_key_pair.lightsail_key_pair_test_omit", "name"), + resource.TestCheckResourceAttrSet("aws_lightsail_key_pair.lightsail_key_pair_test_prefixed", "name"), + ), + }, + }, + }) +} + +func testAccCheckAWSLightsailKeyPairExists(n string, res *lightsail.KeyPair) 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 errors.New("No LightsailKeyPair set") + } + + conn := testAccProvider.Meta().(*AWSClient).lightsailconn + + respKeyPair, err := conn.GetKeyPair(&lightsail.GetKeyPairInput{ + KeyPairName: aws.String(rs.Primary.Attributes["name"]), + }) + + if err != nil { + return err + } + + if respKeyPair == nil || respKeyPair.KeyPair == nil { + return fmt.Errorf("KeyPair (%s) not found", rs.Primary.Attributes["name"]) + } + *res = *respKeyPair.KeyPair + return nil + } +} + +func testAccCheckAWSLightsailKeyPairDestroy(s *terraform.State) error { + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_lightsail_key_pair" { + continue + } + + conn := testAccProvider.Meta().(*AWSClient).lightsailconn + + respKeyPair, err := conn.GetKeyPair(&lightsail.GetKeyPairInput{ + KeyPairName: aws.String(rs.Primary.Attributes["name"]), + }) + + if err == nil { + if respKeyPair.KeyPair != nil { + return fmt.Errorf("LightsailKeyPair %q still exists", rs.Primary.ID) + } + } + + // Verify the error + if awsErr, ok := err.(awserr.Error); ok { + if awsErr.Code() == "NotFoundException" { + return nil + } + } + return err + } + + return nil +} + +func testAccAWSLightsailKeyPairConfig_basic(lightsailName string) string { + return fmt.Sprintf(` +provider "aws" { + region = "us-east-1" +} +resource "aws_lightsail_key_pair" "lightsail_key_pair_test" { + name = "%s" +} +`, lightsailName) +} + +func testAccAWSLightsailKeyPairConfig_imported(lightsailName, key string) string { + return fmt.Sprintf(` +provider "aws" { + region = "us-east-1" +} +resource "aws_lightsail_key_pair" "lightsail_key_pair_test" { + name = "%s" + + public_key = "%s" +} +`, lightsailName, lightsailPubKey) +} + +func testAccAWSLightsailKeyPairConfig_encrypted(lightsailName, key string) string { + return fmt.Sprintf(` +provider "aws" { + region = "us-east-1" +} +resource "aws_lightsail_key_pair" "lightsail_key_pair_test" { + name = "%s" + + pgp_key = < **Note:** Lightsail is currently only supported in `us-east-1` region. + +## Example Usage, creating a new Key Pair + +``` +# Create a new Lightsail Key Pair +resource "aws_lightsail_key_pair" "lg_key_pair" { + name = "lg_key_pair" +} +``` + +## Create new Key Pair, encrypting the private key with a PGP Key + +``` +resource "aws_lightsail_key_pair" "lg_key_pair" { + name = "lg_key_pair" + pgp_key = "keybase:keybaseusername" +} +``` + +## Import an existing public key + +``` +resource "aws_lightsail_key_pair" "lg_key_pair" { + name = "importing" + public_key = "${file("~/.ssh/id_rsa.pub")}" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `name` - (Optional) The name of the Lightsail Key Pair. If omitted, a unique +name will be generated by Terraform +* `pgp_key` – (Optional) An optional PGP key to encrypt the resulting private +key material. Only used when creating a new key pair +* `public_key` - (Required) The public key material. This public key will be +imported into Lightsail + +~> **NOTE:** a PGP key is not required, however it is strongly encouraged. +Without a PGP key, the private key material will be stored in state unencrypted. +`pgp_key` is ignored if `public_key` is supplied. + +## Attributes Reference + +The following attributes are exported in addition to the arguments listed above: + +* `id` - The name used for this key pair +* `arn` - The ARN of the Lightsail key pair +* `fingerprint` - The MD5 public key fingerprint as specified in section 4 of RFC 4716. +* `public_key` - the public key, base64 encoded +* `private_key` - the private key, base64 encoded. This is only populated +when creating a new key, and when no `pgp_key` is provided +* `encrypted_private_key` – the private key material, base 64 encoded and +encrypted with the given `pgp_key`. This is only populated when creating a new +key and `pgp_key` is supplied +* `encrypted_fingerprint` - The MD5 public key fingerprint for the encrypted +private key + +## Import + +Lightsail Key Pairs cannot be imported, because the private and public key are +only available on initial creation. diff --git a/website/source/layouts/aws.erb b/website/source/layouts/aws.erb index c28fbdb06..304e079d3 100644 --- a/website/source/layouts/aws.erb +++ b/website/source/layouts/aws.erb @@ -689,6 +689,21 @@ + > + Lightsail Resources + + + > OpsWorks Resources