diff --git a/builtin/providers/aws/provider.go b/builtin/providers/aws/provider.go index d1426a3f0..be0d9a949 100644 --- a/builtin/providers/aws/provider.go +++ b/builtin/providers/aws/provider.go @@ -430,6 +430,8 @@ func Provider() terraform.ResourceProvider { "aws_ssm_maintenance_window": resourceAwsSsmMaintenanceWindow(), "aws_ssm_maintenance_window_target": resourceAwsSsmMaintenanceWindowTarget(), "aws_ssm_maintenance_window_task": resourceAwsSsmMaintenanceWindowTask(), + "aws_ssm_patch_baseline": resourceAwsSsmPatchBaseline(), + "aws_ssm_patch_group": resourceAwsSsmPatchGroup(), "aws_spot_datafeed_subscription": resourceAwsSpotDataFeedSubscription(), "aws_spot_instance_request": resourceAwsSpotInstanceRequest(), "aws_spot_fleet_request": resourceAwsSpotFleetRequest(), diff --git a/builtin/providers/aws/resource_aws_ssm_patch_baseline.go b/builtin/providers/aws/resource_aws_ssm_patch_baseline.go new file mode 100644 index 000000000..4109c5083 --- /dev/null +++ b/builtin/providers/aws/resource_aws_ssm_patch_baseline.go @@ -0,0 +1,277 @@ +package aws + +import ( + "fmt" + "log" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/ssm" + "github.com/hashicorp/terraform/helper/schema" +) + +func resourceAwsSsmPatchBaseline() *schema.Resource { + return &schema.Resource{ + Create: resourceAwsSsmPatchBaselineCreate, + Read: resourceAwsSsmPatchBaselineRead, + Delete: resourceAwsSsmPatchBaselineDelete, + + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + "description": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + + "global_filter": { + Type: schema.TypeList, + Optional: true, + ForceNew: true, + MaxItems: 4, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "key": { + Type: schema.TypeString, + Required: true, + }, + "values": { + Type: schema.TypeList, + Required: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + }, + }, + }, + + "approval_rule": { + Type: schema.TypeList, + Optional: true, + ForceNew: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "approve_after_days": { + Type: schema.TypeInt, + Required: true, + }, + + "patch_filter": { + Type: schema.TypeList, + Required: true, + MaxItems: 10, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "key": { + Type: schema.TypeString, + Required: true, + }, + "values": { + Type: schema.TypeList, + Required: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + }, + }, + }, + }, + }, + }, + + "approved_patches": { + Type: schema.TypeSet, + Optional: true, + ForceNew: true, + Elem: &schema.Schema{Type: schema.TypeString}, + Set: schema.HashString, + }, + + "rejected_patches": { + Type: schema.TypeSet, + Optional: true, + ForceNew: true, + Elem: &schema.Schema{Type: schema.TypeString}, + Set: schema.HashString, + }, + }, + } +} + +func resourceAwsSsmPatchBaselineCreate(d *schema.ResourceData, meta interface{}) error { + ssmconn := meta.(*AWSClient).ssmconn + + params := &ssm.CreatePatchBaselineInput{ + Name: aws.String(d.Get("name").(string)), + } + + if v, ok := d.GetOk("description"); ok { + params.Description = aws.String(v.(string)) + } + + if v, ok := d.GetOk("approved_patches"); ok && v.(*schema.Set).Len() > 0 { + params.ApprovedPatches = expandStringList(v.(*schema.Set).List()) + } + + if v, ok := d.GetOk("rejected_patches"); ok && v.(*schema.Set).Len() > 0 { + params.RejectedPatches = expandStringList(v.(*schema.Set).List()) + } + + if _, ok := d.GetOk("global_filter"); ok { + params.GlobalFilters = expandAwsSsmPatchFilterGroup(d) + } + + if _, ok := d.GetOk("approval_rule"); ok { + params.ApprovalRules = expandAwsSsmPatchRuleGroup(d) + } + + resp, err := ssmconn.CreatePatchBaseline(params) + if err != nil { + return err + } + + d.SetId(*resp.BaselineId) + return resourceAwsSsmPatchBaselineRead(d, meta) +} + +func resourceAwsSsmPatchBaselineRead(d *schema.ResourceData, meta interface{}) error { + ssmconn := meta.(*AWSClient).ssmconn + + params := &ssm.GetPatchBaselineInput{ + BaselineId: aws.String(d.Id()), + } + + resp, err := ssmconn.GetPatchBaseline(params) + if err != nil { + return err + } + + d.Set("name", resp.Name) + d.Set("description", resp.Description) + d.Set("approved_patches", flattenStringList(resp.ApprovedPatches)) + d.Set("rejected_patches", flattenStringList(resp.RejectedPatches)) + + if err := d.Set("global_filter", flattenAwsSsmPatchFilterGroup(resp.GlobalFilters)); err != nil { + return fmt.Errorf("[DEBUG] Error setting global filters error: %#v", err) + } + + if err := d.Set("approval_rule", flattenAwsSsmPatchRuleGroup(resp.ApprovalRules)); err != nil { + return fmt.Errorf("[DEBUG] Error setting approval rules error: %#v", err) + } + + return nil +} + +func resourceAwsSsmPatchBaselineDelete(d *schema.ResourceData, meta interface{}) error { + ssmconn := meta.(*AWSClient).ssmconn + + log.Printf("[INFO] Deleting SSM Patch Baseline: %s", d.Id()) + + params := &ssm.DeletePatchBaselineInput{ + BaselineId: aws.String(d.Id()), + } + + _, err := ssmconn.DeletePatchBaseline(params) + if err != nil { + return err + } + + return nil +} + +func expandAwsSsmPatchFilterGroup(d *schema.ResourceData) *ssm.PatchFilterGroup { + var filters []*ssm.PatchFilter + + filterConfig := d.Get("global_filter").([]interface{}) + + for _, fConfig := range filterConfig { + config := fConfig.(map[string]interface{}) + + filter := &ssm.PatchFilter{ + Key: aws.String(config["key"].(string)), + Values: expandStringList(config["values"].([]interface{})), + } + + filters = append(filters, filter) + } + + return &ssm.PatchFilterGroup{ + PatchFilters: filters, + } +} + +func flattenAwsSsmPatchFilterGroup(group *ssm.PatchFilterGroup) []map[string]interface{} { + if len(group.PatchFilters) == 0 { + return nil + } + + result := make([]map[string]interface{}, 0, len(group.PatchFilters)) + + for _, filter := range group.PatchFilters { + f := make(map[string]interface{}) + f["key"] = *filter.Key + f["values"] = flattenStringList(filter.Values) + + result = append(result, f) + } + + return result +} + +func expandAwsSsmPatchRuleGroup(d *schema.ResourceData) *ssm.PatchRuleGroup { + var rules []*ssm.PatchRule + + ruleConfig := d.Get("approval_rule").([]interface{}) + + for _, rConfig := range ruleConfig { + rCfg := rConfig.(map[string]interface{}) + + var filters []*ssm.PatchFilter + filterConfig := rCfg["patch_filter"].([]interface{}) + + for _, fConfig := range filterConfig { + fCfg := fConfig.(map[string]interface{}) + + filter := &ssm.PatchFilter{ + Key: aws.String(fCfg["key"].(string)), + Values: expandStringList(fCfg["values"].([]interface{})), + } + + filters = append(filters, filter) + } + + filterGroup := &ssm.PatchFilterGroup{ + PatchFilters: filters, + } + + rule := &ssm.PatchRule{ + ApproveAfterDays: aws.Int64(int64(rCfg["approve_after_days"].(int))), + PatchFilterGroup: filterGroup, + } + + rules = append(rules, rule) + } + + return &ssm.PatchRuleGroup{ + PatchRules: rules, + } +} + +func flattenAwsSsmPatchRuleGroup(group *ssm.PatchRuleGroup) []map[string]interface{} { + if len(group.PatchRules) == 0 { + return nil + } + + result := make([]map[string]interface{}, 0, len(group.PatchRules)) + + for _, rule := range group.PatchRules { + r := make(map[string]interface{}) + r["approve_after_days"] = *rule.ApproveAfterDays + r["patch_filter"] = flattenAwsSsmPatchFilterGroup(rule.PatchFilterGroup) + result = append(result, r) + } + + return result +} diff --git a/builtin/providers/aws/resource_aws_ssm_patch_baseline_test.go b/builtin/providers/aws/resource_aws_ssm_patch_baseline_test.go new file mode 100644 index 000000000..6df3c627b --- /dev/null +++ b/builtin/providers/aws/resource_aws_ssm_patch_baseline_test.go @@ -0,0 +1,137 @@ +package aws + +import ( + "fmt" + "testing" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/ssm" + "github.com/hashicorp/terraform/helper/acctest" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +func TestAccAWSSSMPatchBaseline_basic(t *testing.T) { + name := acctest.RandString(10) + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSSSMPatchBaselineDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSSSMPatchBaselineBasicConfig(name), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSSSMPatchBaselineExists("aws_ssm_patch_baseline.foo"), + resource.TestCheckResourceAttr( + "aws_ssm_patch_baseline.foo", "approved_patches.#", "1"), + resource.TestCheckResourceAttr( + "aws_ssm_patch_baseline.foo", "approved_patches.2062620480", "KB123456"), + resource.TestCheckResourceAttr( + "aws_ssm_patch_baseline.foo", "name", fmt.Sprintf("patch-baseline-%s", name)), + ), + }, + { + Config: testAccAWSSSMPatchBaselineBasicConfigUpdated(name), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSSSMPatchBaselineExists("aws_ssm_patch_baseline.foo"), + resource.TestCheckResourceAttr( + "aws_ssm_patch_baseline.foo", "approved_patches.#", "2"), + resource.TestCheckResourceAttr( + "aws_ssm_patch_baseline.foo", "approved_patches.2062620480", "KB123456"), + resource.TestCheckResourceAttr( + "aws_ssm_patch_baseline.foo", "approved_patches.2291496788", "KB456789"), + resource.TestCheckResourceAttr( + "aws_ssm_patch_baseline.foo", "name", fmt.Sprintf("updated-patch-baseline-%s", name)), + ), + }, + }, + }) +} + +func testAccCheckAWSSSMPatchBaselineExists(n string) 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 SSM Patch Baseline ID is set") + } + + conn := testAccProvider.Meta().(*AWSClient).ssmconn + + resp, err := conn.DescribePatchBaselines(&ssm.DescribePatchBaselinesInput{ + Filters: []*ssm.PatchOrchestratorFilter{ + { + Key: aws.String("NAME_PREFIX"), + Values: []*string{aws.String(rs.Primary.Attributes["name"])}, + }, + }, + }) + + for _, i := range resp.BaselineIdentities { + if *i.BaselineId == rs.Primary.ID { + return nil + } + } + if err != nil { + return err + } + + return fmt.Errorf("No AWS SSM Patch Baseline found") + } +} + +func testAccCheckAWSSSMPatchBaselineDestroy(s *terraform.State) error { + conn := testAccProvider.Meta().(*AWSClient).ssmconn + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_ssm_patch_baseline" { + continue + } + + out, err := conn.DescribePatchBaselines(&ssm.DescribePatchBaselinesInput{ + Filters: []*ssm.PatchOrchestratorFilter{ + { + Key: aws.String("NAME_PREFIX"), + Values: []*string{aws.String(rs.Primary.Attributes["name"])}, + }, + }, + }) + + if err != nil { + return err + } + + if len(out.BaselineIdentities) > 0 { + return fmt.Errorf("Expected AWS SSM Patch Baseline to be gone, but was still found") + } + + return nil + } + + return nil +} + +func testAccAWSSSMPatchBaselineBasicConfig(rName string) string { + return fmt.Sprintf(` + +resource "aws_ssm_patch_baseline" "foo" { + name = "patch-baseline-%s" + approved_patches = ["KB123456"] +} + +`, rName) +} + +func testAccAWSSSMPatchBaselineBasicConfigUpdated(rName string) string { + return fmt.Sprintf(` + +resource "aws_ssm_patch_baseline" "foo" { + name = "updated-patch-baseline-%s" + approved_patches = ["KB123456","KB456789"] +} + +`, rName) +} diff --git a/builtin/providers/aws/resource_aws_ssm_patch_group.go b/builtin/providers/aws/resource_aws_ssm_patch_group.go new file mode 100644 index 000000000..20327b248 --- /dev/null +++ b/builtin/providers/aws/resource_aws_ssm_patch_group.go @@ -0,0 +1,95 @@ +package aws + +import ( + "log" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/ssm" + "github.com/hashicorp/terraform/helper/schema" +) + +func resourceAwsSsmPatchGroup() *schema.Resource { + return &schema.Resource{ + Create: resourceAwsSsmPatchGroupCreate, + Read: resourceAwsSsmPatchGroupRead, + Delete: resourceAwsSsmPatchGroupDelete, + + Schema: map[string]*schema.Schema{ + "baseline_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "patch_group": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + }, + } +} + +func resourceAwsSsmPatchGroupCreate(d *schema.ResourceData, meta interface{}) error { + ssmconn := meta.(*AWSClient).ssmconn + + params := &ssm.RegisterPatchBaselineForPatchGroupInput{ + BaselineId: aws.String(d.Get("baseline_id").(string)), + PatchGroup: aws.String(d.Get("patch_group").(string)), + } + + resp, err := ssmconn.RegisterPatchBaselineForPatchGroup(params) + if err != nil { + return err + } + + d.SetId(*resp.PatchGroup) + return resourceAwsSsmPatchGroupRead(d, meta) +} + +func resourceAwsSsmPatchGroupRead(d *schema.ResourceData, meta interface{}) error { + ssmconn := meta.(*AWSClient).ssmconn + + params := &ssm.DescribePatchGroupsInput{} + + resp, err := ssmconn.DescribePatchGroups(params) + if err != nil { + return err + } + + found := false + for _, t := range resp.Mappings { + if *t.PatchGroup == d.Id() { + found = true + + d.Set("patch_group", t.PatchGroup) + d.Set("baseline_id", t.BaselineIdentity.BaselineId) + } + } + + if !found { + log.Printf("[INFO] Patch Group not found. Removing from state") + d.SetId("") + return nil + } + + return nil + +} + +func resourceAwsSsmPatchGroupDelete(d *schema.ResourceData, meta interface{}) error { + ssmconn := meta.(*AWSClient).ssmconn + + log.Printf("[INFO] Deleting SSM Patch Group: %s", d.Id()) + + params := &ssm.DeregisterPatchBaselineForPatchGroupInput{ + BaselineId: aws.String(d.Get("baseline_id").(string)), + PatchGroup: aws.String(d.Get("patch_group").(string)), + } + + _, err := ssmconn.DeregisterPatchBaselineForPatchGroup(params) + if err != nil { + return err + } + + return nil +} diff --git a/builtin/providers/aws/resource_aws_ssm_patch_group_test.go b/builtin/providers/aws/resource_aws_ssm_patch_group_test.go new file mode 100644 index 000000000..a244beb23 --- /dev/null +++ b/builtin/providers/aws/resource_aws_ssm_patch_group_test.go @@ -0,0 +1,103 @@ +package aws + +import ( + "fmt" + "testing" + + "github.com/aws/aws-sdk-go/aws/awserr" + "github.com/aws/aws-sdk-go/service/ssm" + "github.com/hashicorp/terraform/helper/acctest" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +func TestAccAWSSSMPatchGroup_basic(t *testing.T) { + name := acctest.RandString(10) + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSSSMPatchGroupDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSSSMPatchGroupBasicConfig(name), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSSSMPatchGroupExists("aws_ssm_patch_group.patchgroup"), + ), + }, + }, + }) +} + +func testAccCheckAWSSSMPatchGroupExists(n string) 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 SSM Patch Baseline ID is set") + } + + conn := testAccProvider.Meta().(*AWSClient).ssmconn + + resp, err := conn.DescribePatchGroups(&ssm.DescribePatchGroupsInput{}) + if err != nil { + return err + } + + for _, i := range resp.Mappings { + if *i.BaselineIdentity.BaselineId == rs.Primary.Attributes["baseline_id"] && *i.PatchGroup == rs.Primary.ID { + return nil + } + } + + return fmt.Errorf("No AWS SSM Patch Group found") + } +} + +func testAccCheckAWSSSMPatchGroupDestroy(s *terraform.State) error { + conn := testAccProvider.Meta().(*AWSClient).ssmconn + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_ssm_patch_group" { + continue + } + + resp, err := conn.DescribePatchGroups(&ssm.DescribePatchGroupsInput{}) + + if err != nil { + // Verify the error is what we want + if ae, ok := err.(awserr.Error); ok && ae.Code() == "DoesNotExistException" { + continue + } + return err + } + + for _, i := range resp.Mappings { + if *i.BaselineIdentity.BaselineId == rs.Primary.Attributes["baseline_id"] && *i.PatchGroup == rs.Primary.ID { + return fmt.Errorf("Expected AWS SSM Patch Group to be gone, but was still found") + } + } + + return nil + } + + return nil +} + +func testAccAWSSSMPatchGroupBasicConfig(rName string) string { + return fmt.Sprintf(` + +resource "aws_ssm_patch_baseline" "foo" { + name = "patch-baseline-%s" + approved_patches = ["KB123456"] +} + +resource "aws_ssm_patch_group" "patchgroup" { + baseline_id = "${aws_ssm_patch_baseline.foo.id}" + patch_group = "patch-group" +} + +`, rName) +} diff --git a/website/source/docs/providers/aws/r/ssm_patch_baseline.html.markdown b/website/source/docs/providers/aws/r/ssm_patch_baseline.html.markdown new file mode 100644 index 000000000..48e224139 --- /dev/null +++ b/website/source/docs/providers/aws/r/ssm_patch_baseline.html.markdown @@ -0,0 +1,95 @@ +--- +layout: "aws" +page_title: "AWS: aws_ssm_patch_baseline" +sidebar_current: "docs-aws-resource-ssm-patch-baseline" +description: |- + Provides an SSM Patch Baseline resource +--- + +# aws_ssm_patch_baseline + +Provides an SSM Patch Baseline resource + +~> **NOTE on Patch Baselines:** The `approved_patches` and `approval_rule` are +both marked as optional fields, but the Patch Baseline requires that at least one +of them is specified. + +## Example Usage + +Basic usage using `approved_patches` only + +```hcl +resource "aws_ssm_patch_baseline" "production" { + name = "patch-baseline" + approved_patches = ["KB123456"] +} +``` + +Advanced usage, specifying patch filters + +```hcl +resource "aws_ssm_patch_baseline" "production" { + name = "patch-baseline" + description = "Patch Baseline Description" + approved_patches = ["KB123456", "KB456789"] + rejected_patches = ["KB987654"] + global_filter { + key = "PRODUCT" + values = ["WindowsServer2008"] + } + global_filter { + key = "CLASSIFICATION" + values = ["ServicePacks"] + } + global_filter { + key = "MSRC_SEVERITY" + values = ["Low"] + } + approval_rule { + approve_after_days = 7 + patch_filter { + key = "PRODUCT" + values = ["WindowsServer2016"] + } + patch_filter { + key = "CLASSIFICATION" + values = ["CriticalUpdates", "SecurityUpdates", "Updates"] + } + patch_filter { + key = "MSRC_SEVERITY" + values = ["Critical", "Important", "Moderate"] + } + } + approval_rule { + approve_after_days = 7 + patch_filter { + key = "PRODUCT" + values = ["WindowsServer2012"] + } + } +} +``` + + +## Argument Reference + +The following arguments are supported: + +* `name` - (Required) The name of the patch baseline. +* `description` - (Optional) The description of the patch baseline. +* `approved_patches` - (Optional) A list of explicitly approved patches for the baseline. +* `rejected_patches` - (Optional) A list of rejected patches. +* `global_filter` - (Optional) A set of global filters used to exclude patches from the baseline. Up to 4 global filters can be specified using Key/Value pairs. Valid Keys are `PRODUCT | CLASSIFICATION | MSRC_SEVERITY | PATCH_ID`. +* `approval_rule` - (Optional) A set of rules used to include patches in the baseline. up to 10 approval rules can be specified. Each approval_rule block requires the fields documented below. + +The `approval_rule` block supports: + +* `approve_after_days` - (Required) The number of days after the release date of each patch matched by the rule the patch is marked as approved in the patch baseline. Valid Range: 0 to 100. +* `patch_filter` - (Required) The patch filter group that defines the criteria for the rule. Up to 4 patch filters can be specified per approval rule using Key/Value pairs. Valid Keys are `PRODUCT | CLASSIFICATION | MSRC_SEVERITY | PATCH_ID`. + + +## Attributes Reference + +The following attributes are exported: + +* `id` - The ID of the patch baseline. \ No newline at end of file diff --git a/website/source/docs/providers/aws/r/ssm_patch_group.html.markdown b/website/source/docs/providers/aws/r/ssm_patch_group.html.markdown new file mode 100644 index 000000000..c52fe3e9a --- /dev/null +++ b/website/source/docs/providers/aws/r/ssm_patch_group.html.markdown @@ -0,0 +1,37 @@ +--- +layout: "aws" +page_title: "AWS: aws_ssm_patch_group" +sidebar_current: "docs-aws-resource-ssm-patch-group" +description: |- + Provides an SSM Patch Group resource +--- + +# aws_ssm_patch_group + +Provides an SSM Patch Group resource + +## Example Usage + +```hcl +resource "aws_ssm_patch_baseline" "production" { + name = "patch-baseline" + approved_patches = ["KB123456"] +} + +resource "aws_ssm_patch_group" "patchgroup" { + baseline_id = "${aws_ssm_patch_baseline.production.id}" + patch_group = "patch-group-name" +}``` + +## Argument Reference + +The following arguments are supported: + +* `baseline_id` - (Required) The ID of the patch baseline to register the patch group with. +* `patch_group` - (Required) The name of the patch group that should be registered with the patch baseline. + +## Attributes Reference + +The following attributes are exported: + +* `id` - The ID of the patch baseline. \ No newline at end of file