diff --git a/aws/config.go b/aws/config.go index 3e1aa9f3f0d5..9fc0a088bcc1 100644 --- a/aws/config.go +++ b/aws/config.go @@ -60,6 +60,7 @@ import ( "github.com/aws/aws-sdk-go/service/redshift" "github.com/aws/aws-sdk-go/service/route53" "github.com/aws/aws-sdk-go/service/s3" + "github.com/aws/aws-sdk-go/service/servicecatalog" "github.com/aws/aws-sdk-go/service/ses" "github.com/aws/aws-sdk-go/service/sfn" "github.com/aws/aws-sdk-go/service/simpledb" @@ -144,6 +145,7 @@ type AWSClient struct { appautoscalingconn *applicationautoscaling.ApplicationAutoScaling autoscalingconn *autoscaling.AutoScaling s3conn *s3.S3 + scconn *servicecatalog.ServiceCatalog sesConn *ses.SES simpledbconn *simpledb.SimpleDB sqsconn *sqs.SQS @@ -379,6 +381,7 @@ func (c *Config) Client() (interface{}, error) { client.redshiftconn = redshift.New(sess) client.simpledbconn = simpledb.New(sess) client.s3conn = s3.New(awsS3Sess) + client.scconn = servicecatalog.New(sess) client.sesConn = ses.New(sess) client.sfnconn = sfn.New(sess) client.snsconn = sns.New(awsSnsSess) diff --git a/aws/provider.go b/aws/provider.go index fea7bceb456d..b0dab0f5576a 100644 --- a/aws/provider.go +++ b/aws/provider.go @@ -440,6 +440,7 @@ func Provider() terraform.ResourceProvider { "aws_network_interface_sg_attachment": resourceAwsNetworkInterfaceSGAttachment(), "aws_default_security_group": resourceAwsDefaultSecurityGroup(), "aws_security_group_rule": resourceAwsSecurityGroupRule(), + "aws_servicecatalog_portfolio": resourceAwsServiceCatalogPortfolio(), "aws_simpledb_domain": resourceAwsSimpleDBDomain(), "aws_ssm_activation": resourceAwsSsmActivation(), "aws_ssm_association": resourceAwsSsmAssociation(), diff --git a/aws/resource_aws_servicecatalog_portfolio.go b/aws/resource_aws_servicecatalog_portfolio.go new file mode 100644 index 000000000000..fd669e1e91da --- /dev/null +++ b/aws/resource_aws_servicecatalog_portfolio.go @@ -0,0 +1,228 @@ +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/servicecatalog" + "github.com/hashicorp/terraform/helper/schema" +) + +func resourceAwsServiceCatalogPortfolio() *schema.Resource { + return &schema.Resource{ + Create: resourceAwsServiceCatalogPortfolioCreate, + Read: resourceAwsServiceCatalogPortfolioRead, + Update: resourceAwsServiceCatalogPortfolioUpdate, + Delete: resourceAwsServiceCatalogPortfolioDelete, + + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + + Timeouts: &schema.ResourceTimeout{ + Create: schema.DefaultTimeout(30 * time.Minute), + Update: schema.DefaultTimeout(30 * time.Minute), + Delete: schema.DefaultTimeout(30 * time.Minute), + }, + + Schema: map[string]*schema.Schema{ + "id": { + Type: schema.TypeString, + Computed: true, + }, + "arn": { + Type: schema.TypeString, + Computed: true, + }, + "created_time": { + Type: schema.TypeString, + Computed: true, + }, + "name": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validateServiceCatalogPortfolioName, + }, + "description": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ValidateFunc: validateServiceCatalogPortfolioDescription, + }, + "provider_name": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validateServiceCatalogPortfolioProviderName, + }, + "tags": tagsSchema(), + }, + } +} +func resourceAwsServiceCatalogPortfolioCreate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).scconn + input := servicecatalog.CreatePortfolioInput{ + AcceptLanguage: aws.String("en"), + } + name := d.Get("name").(string) + input.DisplayName = &name + now := time.Now() + input.IdempotencyToken = aws.String(fmt.Sprintf("%d", now.UnixNano())) + + if v, ok := d.GetOk("description"); ok { + input.Description = aws.String(v.(string)) + } + + if v, ok := d.GetOk("provider_name"); ok { + input.ProviderName = aws.String(v.(string)) + } + + if v, ok := d.GetOk("tags"); ok { + tags := []*servicecatalog.Tag{} + t := v.(map[string]interface{}) + for k, v := range t { + tag := servicecatalog.Tag{ + Key: aws.String(k), + Value: aws.String(v.(string)), + } + tags = append(tags, &tag) + } + input.Tags = tags + } + + log.Printf("[DEBUG] Creating Service Catalog Portfolio: %#v", input) + resp, err := conn.CreatePortfolio(&input) + if err != nil { + return fmt.Errorf("Creating Service Catalog Portfolio failed: %s", err.Error()) + } + d.SetId(*resp.PortfolioDetail.Id) + + return resourceAwsServiceCatalogPortfolioRead(d, meta) +} + +func resourceAwsServiceCatalogPortfolioRead(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).scconn + input := servicecatalog.DescribePortfolioInput{ + AcceptLanguage: aws.String("en"), + } + input.Id = aws.String(d.Id()) + + log.Printf("[DEBUG] Reading Service Catalog Portfolio: %#v", input) + resp, err := conn.DescribePortfolio(&input) + if err != nil { + if scErr, ok := err.(awserr.Error); ok && scErr.Code() == "ResourceNotFoundException" { + log.Printf("[WARN] Service Catalog Portfolio %q not found, removing from state", d.Id()) + d.SetId("") + return nil + } + return fmt.Errorf("Reading ServiceCatalog Portfolio '%s' failed: %s", *input.Id, err.Error()) + } + portfolioDetail := resp.PortfolioDetail + if err := d.Set("created_time", portfolioDetail.CreatedTime.Format(time.RFC3339)); err != nil { + log.Printf("[DEBUG] Error setting created_time: %s", err) + } + d.Set("arn", portfolioDetail.ARN) + d.Set("description", portfolioDetail.Description) + d.Set("name", portfolioDetail.DisplayName) + d.Set("provider_name", portfolioDetail.ProviderName) + tags := map[string]string{} + for _, tag := range resp.Tags { + tags[*tag.Key] = *tag.Value + } + d.Set("tags", tags) + return nil +} + +func resourceAwsServiceCatalogPortfolioUpdate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).scconn + input := servicecatalog.UpdatePortfolioInput{ + AcceptLanguage: aws.String("en"), + Id: aws.String(d.Id()), + } + + if d.HasChange("name") { + v, _ := d.GetOk("name") + input.DisplayName = aws.String(v.(string)) + } + + if d.HasChange("accept_language") { + v, _ := d.GetOk("accept_language") + input.AcceptLanguage = aws.String(v.(string)) + } + + if d.HasChange("description") { + v, _ := d.GetOk("description") + input.Description = aws.String(v.(string)) + } + + if d.HasChange("provider_name") { + v, _ := d.GetOk("provider_name") + input.ProviderName = aws.String(v.(string)) + } + + if d.HasChange("tags") { + currentTags, requiredTags := d.GetChange("tags") + log.Printf("[DEBUG] Current Tags: %#v", currentTags) + log.Printf("[DEBUG] Required Tags: %#v", requiredTags) + + tagsToAdd, tagsToRemove := tagUpdates(requiredTags.(map[string]interface{}), currentTags.(map[string]interface{})) + log.Printf("[DEBUG] Tags To Add: %#v", tagsToAdd) + log.Printf("[DEBUG] Tags To Remove: %#v", tagsToRemove) + input.AddTags = tagsToAdd + input.RemoveTags = tagsToRemove + } + + log.Printf("[DEBUG] Update Service Catalog Portfolio: %#v", input) + _, err := conn.UpdatePortfolio(&input) + if err != nil { + return fmt.Errorf("Updating Service Catalog Portfolio '%s' failed: %s", *input.Id, err.Error()) + } + return resourceAwsServiceCatalogPortfolioRead(d, meta) +} + +func tagUpdates(requriedTags, currentTags map[string]interface{}) ([]*servicecatalog.Tag, []*string) { + var tagsToAdd []*servicecatalog.Tag + var tagsToRemove []*string + + for rk, rv := range requriedTags { + addTag := true + for ck, cv := range currentTags { + if (rk == ck) && (rv.(string) == cv.(string)) { + addTag = false + } + } + if addTag { + tag := &servicecatalog.Tag{Key: aws.String(rk), Value: aws.String(rv.(string))} + tagsToAdd = append(tagsToAdd, tag) + } + } + + for ck, cv := range currentTags { + removeTag := true + for rk, rv := range requriedTags { + if (rk == ck) && (rv.(string) == cv.(string)) { + removeTag = false + } + } + if removeTag { + tagsToRemove = append(tagsToRemove, aws.String(ck)) + } + } + + return tagsToAdd, tagsToRemove +} + +func resourceAwsServiceCatalogPortfolioDelete(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).scconn + input := servicecatalog.DeletePortfolioInput{} + input.Id = aws.String(d.Id()) + + log.Printf("[DEBUG] Delete Service Catalog Portfolio: %#v", input) + _, err := conn.DeletePortfolio(&input) + if err != nil { + return fmt.Errorf("Deleting Service Catalog Portfolio '%s' failed: %s", *input.Id, err.Error()) + } + return nil +} diff --git a/aws/resource_aws_servicecatalog_portfolio_test.go b/aws/resource_aws_servicecatalog_portfolio_test.go new file mode 100644 index 000000000000..99da34c2054c --- /dev/null +++ b/aws/resource_aws_servicecatalog_portfolio_test.go @@ -0,0 +1,209 @@ +package aws + +import ( + "fmt" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/servicecatalog" + + "github.com/hashicorp/terraform/helper/acctest" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" + + "testing" +) + +func TestAccAWSServiceCatalogPortfolioBasic(t *testing.T) { + name := acctest.RandString(5) + var dpo servicecatalog.DescribePortfolioOutput + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckServiceCatlaogPortfolioDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccCheckAwsServiceCatalogPortfolioResourceConfigBasic1(name), + Check: resource.ComposeTestCheckFunc( + testAccCheckPortfolio("aws_servicecatalog_portfolio.test", &dpo), + resource.TestCheckResourceAttrSet("aws_servicecatalog_portfolio.test", "arn"), + resource.TestCheckResourceAttrSet("aws_servicecatalog_portfolio.test", "created_time"), + resource.TestCheckResourceAttr("aws_servicecatalog_portfolio.test", "name", name), + resource.TestCheckResourceAttr("aws_servicecatalog_portfolio.test", "description", "test-2"), + resource.TestCheckResourceAttr("aws_servicecatalog_portfolio.test", "provider_name", "test-3"), + resource.TestCheckResourceAttr("aws_servicecatalog_portfolio.test", "tags.%", "1"), + resource.TestCheckResourceAttr("aws_servicecatalog_portfolio.test", "tags.Key1", "Value One"), + ), + }, + resource.TestStep{ + Config: testAccCheckAwsServiceCatalogPortfolioResourceConfigBasic2(name), + Check: resource.ComposeTestCheckFunc( + testAccCheckPortfolio("aws_servicecatalog_portfolio.test", &dpo), + resource.TestCheckResourceAttrSet("aws_servicecatalog_portfolio.test", "arn"), + resource.TestCheckResourceAttrSet("aws_servicecatalog_portfolio.test", "created_time"), + resource.TestCheckResourceAttr("aws_servicecatalog_portfolio.test", "name", name), + resource.TestCheckResourceAttr("aws_servicecatalog_portfolio.test", "description", "test-b"), + resource.TestCheckResourceAttr("aws_servicecatalog_portfolio.test", "provider_name", "test-c"), + resource.TestCheckResourceAttr("aws_servicecatalog_portfolio.test", "tags.%", "2"), + resource.TestCheckResourceAttr("aws_servicecatalog_portfolio.test", "tags.Key1", "Value 1"), + resource.TestCheckResourceAttr("aws_servicecatalog_portfolio.test", "tags.Key2", "Value Two"), + ), + }, + resource.TestStep{ + Config: testAccCheckAwsServiceCatalogPortfolioResourceConfigBasic3(name), + Check: resource.ComposeTestCheckFunc( + testAccCheckPortfolio("aws_servicecatalog_portfolio.test", &dpo), + resource.TestCheckResourceAttrSet("aws_servicecatalog_portfolio.test", "arn"), + resource.TestCheckResourceAttrSet("aws_servicecatalog_portfolio.test", "created_time"), + resource.TestCheckResourceAttr("aws_servicecatalog_portfolio.test", "name", name), + resource.TestCheckResourceAttr("aws_servicecatalog_portfolio.test", "description", "test-only-change-me"), + resource.TestCheckResourceAttr("aws_servicecatalog_portfolio.test", "provider_name", "test-c"), + resource.TestCheckResourceAttr("aws_servicecatalog_portfolio.test", "tags.%", "1"), + resource.TestCheckResourceAttr("aws_servicecatalog_portfolio.test", "tags.Key3", "Value Three"), + ), + }, + }, + }) +} + +func TestAccAWSServiceCatalogPortfolioDisappears(t *testing.T) { + name := acctest.RandString(5) + var dpo servicecatalog.DescribePortfolioOutput + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckServiceCatlaogPortfolioDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccCheckAwsServiceCatalogPortfolioResourceConfigBasic1(name), + Check: resource.ComposeTestCheckFunc( + testAccCheckPortfolio("aws_servicecatalog_portfolio.test", &dpo), + testAccCheckServiceCatlaogPortfolioDisappears(&dpo), + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + +func TestAccAWSServiceCatalogPortfolioImport(t *testing.T) { + resourceName := "aws_servicecatalog_portfolio.test" + + name := acctest.RandString(5) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckServiceCatlaogPortfolioDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccCheckAwsServiceCatalogPortfolioResourceConfigBasic1(name), + }, + + resource.TestStep{ + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func testAccCheckPortfolio(pr string, dpo *servicecatalog.DescribePortfolioOutput) resource.TestCheckFunc { + return func(s *terraform.State) error { + conn := testAccProvider.Meta().(*AWSClient).scconn + rs, ok := s.RootModule().Resources[pr] + if !ok { + return fmt.Errorf("Not found: %s", pr) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No ID is set") + } + + input := servicecatalog.DescribePortfolioInput{} + input.Id = aws.String(rs.Primary.ID) + + resp, err := conn.DescribePortfolio(&input) + if err != nil { + return err + } + + *dpo = *resp + return nil + } +} + +func testAccCheckServiceCatlaogPortfolioDisappears(dpo *servicecatalog.DescribePortfolioOutput) resource.TestCheckFunc { + return func(s *terraform.State) error { + conn := testAccProvider.Meta().(*AWSClient).scconn + + input := servicecatalog.DeletePortfolioInput{} + input.Id = dpo.PortfolioDetail.Id + + _, err := conn.DeletePortfolio(&input) + if err != nil { + return err + } + + return nil + } +} + +func testAccCheckServiceCatlaogPortfolioDestroy(s *terraform.State) error { + conn := testAccProvider.Meta().(*AWSClient).scconn + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_servicecatalog_portfolio" { + continue + } + input := servicecatalog.DescribePortfolioInput{} + input.Id = aws.String(rs.Primary.ID) + + _, err := conn.DescribePortfolio(&input) + if err == nil { + return fmt.Errorf("Portfolio still exists") + } + } + + return nil +} + +func testAccCheckAwsServiceCatalogPortfolioResourceConfigBasic1(name string) string { + return fmt.Sprintf(` +resource "aws_servicecatalog_portfolio" "test" { + name = "%s" + description = "test-2" + provider_name = "test-3" + tags { + Key1 = "Value One" + } +} +`, name) +} + +func testAccCheckAwsServiceCatalogPortfolioResourceConfigBasic2(name string) string { + return fmt.Sprintf(` +resource "aws_servicecatalog_portfolio" "test" { + name = "%s" + description = "test-b" + provider_name = "test-c" + tags { + Key1 = "Value 1" + Key2 = "Value Two" + } +} +`, name) +} + +func testAccCheckAwsServiceCatalogPortfolioResourceConfigBasic3(name string) string { + return fmt.Sprintf(` +resource "aws_servicecatalog_portfolio" "test" { + name = "%s" + description = "test-only-change-me" + provider_name = "test-c" + tags { + Key3 = "Value Three" + } +} +`, name) +} diff --git a/aws/validators.go b/aws/validators.go index 426f560430c1..0defee525a72 100644 --- a/aws/validators.go +++ b/aws/validators.go @@ -1511,3 +1511,27 @@ func validateSecurityGroupRuleDescription(v interface{}, k string) (ws []string, } return } + +func validateServiceCatalogPortfolioName(v interface{}, k string) (ws []string, errors []error) { + value := v.(string) + if (len(value) > 20) || (len(value) == 0) { + errors = append(errors, fmt.Errorf("Service catalog name must be between 1 and 20 characters.")) + } + return +} + +func validateServiceCatalogPortfolioDescription(v interface{}, k string) (ws []string, errors []error) { + value := v.(string) + if len(value) > 2000 { + errors = append(errors, fmt.Errorf("Service catalog description must be less than 2000 characters.")) + } + return +} + +func validateServiceCatalogPortfolioProviderName(v interface{}, k string) (ws []string, errors []error) { + value := v.(string) + if (len(value) > 20) || (len(value) == 0) { + errors = append(errors, fmt.Errorf("Service catalog provider name must be between 1 and 20 characters.")) + } + return +} diff --git a/website/aws.erb b/website/aws.erb index 5b16e5d2ea9a..d2cbf0efe199 100644 --- a/website/aws.erb +++ b/website/aws.erb @@ -1341,6 +1341,16 @@ +