Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for package SSM document type #11492

Merged
merged 4 commits into from
Jan 13, 2020
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 additions & 0 deletions aws/resource_aws_ssm_document.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,32 @@ func resourceAwsSsmDocument() *schema.Resource {
Required: true,
ValidateFunc: validateAwsSSMName,
},
"attachments": {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Per comment in read function, recommend naming this attachments_source (singular for configuration blocks)

Suggested change
"attachments": {
"attachments_source": {

Type: schema.TypeList,
Optional: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"key": {
Type: schema.TypeString,
Required: true,
ValidateFunc: validation.StringInSlice([]string{
ssm.AttachmentsSourceKeySourceUrl,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Additional value is available:

Suggested change
ssm.AttachmentsSourceKeySourceUrl,
ssm.AttachmentsSourceKeyAttachmentReference,
ssm.AttachmentsSourceKeySourceUrl,

ssm.AttachmentsSourceKeyS3fileUrl,
}, false),
},
"name": {
Type: schema.TypeString,
Optional: true,
},
"values": {
Type: schema.TypeList,
MinItems: 1,
Required: true,
Elem: &schema.Schema{Type: schema.TypeString},
},
},
},
},
"content": {
Type: schema.TypeString,
Required: true,
Expand All @@ -60,6 +86,7 @@ func resourceAwsSsmDocument() *schema.Resource {
ssm.DocumentTypePolicy,
ssm.DocumentTypeAutomation,
ssm.DocumentTypeSession,
ssm.DocumentTypePackage,
}, false),
},
"schema_version": {
Expand Down Expand Up @@ -164,6 +191,10 @@ func resourceAwsSsmDocumentCreate(d *schema.ResourceData, meta interface{}) erro
docInput.Tags = keyvaluetags.New(v.(map[string]interface{})).IgnoreAws().SsmTags()
}

if v, ok := d.GetOk("attachments"); ok {
docInput.Attachments = expandAttachments(v.([]interface{}))
}

log.Printf("[DEBUG] Waiting for SSM Document %q to be created", d.Get("name").(string))
var resp *ssm.CreateDocumentOutput
err := resource.Retry(5*time.Minute, func() *resource.RetryError {
Expand Down Expand Up @@ -266,6 +297,11 @@ func resourceAwsSsmDocumentRead(d *schema.ResourceData, meta interface{}) error

d.Set("status", doc.Status)

if v, ok := d.GetOk("attachments"); ok {
// The API doesn't currently return attachment information so it has to be set this way
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It appears that the SSM GetDocument API does returns attachment information, but in a different form AttachmentContent structure. It is generally better to perform the drift detection and support full resource import, but the mismatched structures will make the implementation harder, if not impossible. 🙁

Since the Create/Update and Read APIs do not share a common API structure, this would have potentially been a case where we could smooth over the user experience in Terraform and support a configuration like the following:

attachment {
  name = ""
  url  = ""
}

Where on Create/Update the Terraform logic converted those to the requisite AttachmentSource objects by automatically inferring the Key value from the URL (e.g. S3FileUrl if url prefix is s3://, SourceUrl if prefix is http(s):// etc). On Read, it would be a simple "passthrough" of the API structure.

However, it appears AttachmentsSource SourceUrl may support providing the URL to a whole S3 "folder" of files. This concept does not map well into Terraform if the API expands all those into individual AttachmentContents objects on Read. Bummer.


It may be best in this case then to just rename this attribute directly attachments_source (prefer singular for configuration block names) to match the Create/Update API and denote to operators its exact usage.

We can also skip this d.GetOk() and d.Set() logic in the Read function since it is a root level attribute. Terraform will automatically "passthrough" a given configuration into the Terraform state if it does not have d.Set() called on it to overwrite the value during refresh.

d.Set("attachments", v)
}

gp, err := getDocumentPermissions(d, meta)

if err != nil {
Expand Down Expand Up @@ -384,6 +420,31 @@ func resourceAwsSsmDocumentDelete(d *schema.ResourceData, meta interface{}) erro
return nil
}

func expandAttachments(a []interface{}) []*ssm.AttachmentsSource {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Resources and their functions currently live in the shared aws Go package across all services and resources. We should prefer to name functions with their service until this shared package is split up.

Suggested change
func expandAttachments(a []interface{}) []*ssm.AttachmentsSource {
func expandSsmAttachmentsSources(a []interface{}) []*ssm.AttachmentsSource {

if len(a) == 0 {
return nil
}

results := make([]*ssm.AttachmentsSource, 0)
for _, raw := range a {
at := raw.(map[string]interface{})
s := &ssm.AttachmentsSource{}
if val, ok := at["key"]; ok {
s.Key = aws.String(val.(string))
}
if val, ok := at["name"]; ok && val != "" {
s.Name = aws.String(val.(string))
}
if val, ok := at["values"]; ok {
s.Values = expandStringList(val.([]interface{}))
}

results = append(results, s)
}
return results

}

func setDocumentPermissions(d *schema.ResourceData, meta interface{}) error {
ssmconn := meta.(*AWSClient).ssmconn

Expand Down Expand Up @@ -572,6 +633,10 @@ func updateAwsSSMDocument(d *schema.ResourceData, meta interface{}) error {
DocumentVersion: aws.String(d.Get("default_version").(string)),
}

if d.HasChange("attachments") {
updateDocInput.Attachments = expandAttachments(d.Get("attachments").([]interface{}))
}

newDefaultVersion := d.Get("default_version").(string)

ssmconn := meta.(*AWSClient).ssmconn
Expand Down
125 changes: 125 additions & 0 deletions aws/resource_aws_ssm_document_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package aws

import (
"fmt"
"os"
"testing"

"github.com/aws/aws-sdk-go/aws"
Expand Down Expand Up @@ -244,6 +245,45 @@ func TestAccAWSSSMDocument_automation(t *testing.T) {
})
}

func TestAccAWSSSMDocument_package(t *testing.T) {
name := acctest.RandString(10)
rInt := acctest.RandInt()
rInt2 := acctest.RandInt()

source := testAccAWSS3BucketObjectCreateTempFile(t, "{anything will do }")
defer os.Remove(source)

resource.ParallelTest(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckAWSSSMDocumentDestroy,
Steps: []resource.TestStep{
{
Config: testAccAWSSSMDocumentTypePackageConfig(name, source, rInt),
Check: resource.ComposeTestCheckFunc(
testAccCheckAWSSSMDocumentExists("aws_ssm_document.test"),
resource.TestCheckResourceAttr(
"aws_ssm_document.test", "document_type", "Package"),
),
},
{
ResourceName: "aws_ssm_document.test",
ImportState: true,
ImportStateVerify: true,
ImportStateVerifyIgnore: []string{"attachments"}, // This doesn't work because the API doesn't provide attachments info
},
{
Config: testAccAWSSSMDocumentTypePackageConfig(name, source, rInt2),
Check: resource.ComposeTestCheckFunc(
testAccCheckAWSSSMDocumentExists("aws_ssm_document.test"),
resource.TestCheckResourceAttr(
"aws_ssm_document.test", "document_type", "Package"),
),
},
},
})
}

func TestAccAWSSSMDocument_SchemaVersion_1(t *testing.T) {
rName := acctest.RandomWithPrefix("tf-acc-test")
resourceName := "aws_ssm_document.test"
Expand Down Expand Up @@ -750,6 +790,91 @@ DOC
`, rName, rName, rName)
}

func testAccAWSSSMDocumentTypePackageConfig(rName, source string, rInt int) string {
return fmt.Sprintf(`
data "aws_ami" "ssm_ami" {
most_recent = true
owners = ["099720109477"] # Canonical

filter {
name = "name"
values = ["*hvm-ssd/ubuntu-trusty-14.04*"]
}
}

resource "aws_iam_instance_profile" "ssm_profile" {
name = "ssm_profile-%s"
role = "${aws_iam_role.ssm_role.name}"
}

resource "aws_iam_role" "ssm_role" {
name = "ssm_role-%s"
path = "/"

assume_role_policy = <<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Action": "sts:AssumeRole",
"Principal": {
"Service": "ec2.amazonaws.com"
},
"Effect": "Allow",
"Sid": ""
}
]
}
EOF
}

resource "aws_s3_bucket" "object_bucket" {
bucket = "tf-object-test-bucket-%d"
}

resource "aws_s3_bucket_object" "object" {
bucket = "${aws_s3_bucket.object_bucket.bucket}"
key = "test.zip"
source = "%s"
content_type = "binary/octet-stream"
}

resource "aws_ssm_document" "test" {
name = "test_document-%s"
document_type = "Package"
attachments {
key = "SourceUrl"
values = ["s3://${aws_s3_bucket.object_bucket.bucket}/test.zip"]
}

content = <<DOC
{
"description": "Systems Manager Package Document Test",
"schemaVersion": "2.0",
"version": "0.1",
"assumeRole": "${aws_iam_role.ssm_role.arn}",
"files": {
"test.zip": {
"checksums": {
"sha256": "thisistwentycharactersatleast"
}
}
},
"packages": {
"amazon": {
"_any": {
"x86_64": {
"file": "test.zip"
}
}
}
}
}
DOC
}
`, rName, rName, rInt, source, rName)
}

func testAccAWSSSMDocumentTypeSessionConfig(rName string) string {
return fmt.Sprintf(`
resource "aws_ssm_document" "foo" {
Expand Down
29 changes: 28 additions & 1 deletion website/docs/r/ssm_document.html.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -48,12 +48,21 @@ DOC
The following arguments are supported:

* `name` - (Required) The name of the document.
* `attachments` - (Optional) A list of key/value pairs describing attachments to a version of a document. Defined below.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Recommend shying away from "list" and "key/value pairs" when referring to a configuration block attributes, since in Terraform 0.12+ they all mean semantically different things (and Terraform 0.12 removed the old attachments_source = [{}] syntax you could do in some undocumented cases).

Suggested change
* `attachments` - (Optional) A list of key/value pairs describing attachments to a version of a document. Defined below.
* `attachments_source` - (Optional) One or more configuration blocks describing attachments sources to a version of a document. Defined below.

* `content` - (Required) The JSON or YAML content of the document.
* `document_format` - (Optional, defaults to JSON) The format of the document. Valid document types include: `JSON` and `YAML`
* `document_type` - (Required) The type of the document. Valid document types include: `Command`, `Policy`, `Automation` and `Session`
* `document_type` - (Required) The type of the document. Valid document types include: `Automation`, `Command`, `Package`, `Policy`, and `Session`
* `permissions` - (Optional) Additional Permissions to attach to the document. See [Permissions](#permissions) below for details.
* `tags` - (Optional) A mapping of tags to assign to the object.

## attachments

The `attachments` block supports the following:

* `key` - (Required) The key describing the location of an attachment to a document. Valid key types include: `SourceUrl` and `S3FileUrl`
* `values` - (Required) The value describing the location of an attachment to a document
* `name` - (Optional) The name of the document attachment file

## Attributes Reference

In addition to all arguments above, the following attributes are exported:
Expand Down Expand Up @@ -90,3 +99,21 @@ SSM Documents can be imported using the name, e.g.
```
$ terraform import aws_ssm_document.example example
```

The `attachments` argument does not have an SSM API method for reading the attachment information detail after creation. If the argument is set in the Terraform configuration on an imported resource, Terraform will always show a difference. To workaround this behavior, either omit the argument from the Terraform configuration or use [`ignore_changes`](/docs/configuration/resources.html#ignore_changes) to hide the difference, e.g.

```hcl
resource "aws_ssm_document" "test" {
name = "test_document"
document_type = "Package"

attachments {
key = "SourceUrl"
values = ["s3://${aws_s3_bucket.object_bucket.bucket}/test.zip"]
}

# There is no AWS SSM API for reading attachments
lifecycle {
ignore_changes = ["attachments"]
}
}