provider/aws: Add support for aws_ssm_patch_baseline (#14954)

* Add support for aws_ssm_patch_baseline and aws_ssm_patch_group

* Fix failing test

* Cleanup commented out code
This commit is contained in:
PaulAtkins 2017-06-01 05:16:35 +12:00 committed by Paul Stack
parent d400fe23e0
commit d3eed78d95
7 changed files with 746 additions and 0 deletions

View File

@ -430,6 +430,8 @@ func Provider() terraform.ResourceProvider {
"aws_ssm_maintenance_window": resourceAwsSsmMaintenanceWindow(), "aws_ssm_maintenance_window": resourceAwsSsmMaintenanceWindow(),
"aws_ssm_maintenance_window_target": resourceAwsSsmMaintenanceWindowTarget(), "aws_ssm_maintenance_window_target": resourceAwsSsmMaintenanceWindowTarget(),
"aws_ssm_maintenance_window_task": resourceAwsSsmMaintenanceWindowTask(), "aws_ssm_maintenance_window_task": resourceAwsSsmMaintenanceWindowTask(),
"aws_ssm_patch_baseline": resourceAwsSsmPatchBaseline(),
"aws_ssm_patch_group": resourceAwsSsmPatchGroup(),
"aws_spot_datafeed_subscription": resourceAwsSpotDataFeedSubscription(), "aws_spot_datafeed_subscription": resourceAwsSpotDataFeedSubscription(),
"aws_spot_instance_request": resourceAwsSpotInstanceRequest(), "aws_spot_instance_request": resourceAwsSpotInstanceRequest(),
"aws_spot_fleet_request": resourceAwsSpotFleetRequest(), "aws_spot_fleet_request": resourceAwsSpotFleetRequest(),

View File

@ -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
}

View File

@ -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)
}

View File

@ -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
}

View File

@ -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)
}

View File

@ -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.

View File

@ -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.