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

New Resource: aws_custom_resource #10096

Closed
Closed
Show file tree
Hide file tree
Changes from all 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
1 change: 1 addition & 0 deletions aws/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -402,6 +402,7 @@ func Provider() terraform.ResourceProvider {
"aws_codepipeline_webhook": resourceAwsCodePipelineWebhook(),
"aws_cur_report_definition": resourceAwsCurReportDefinition(),
"aws_customer_gateway": resourceAwsCustomerGateway(),
"aws_custom_resource": resourceAwsCustomResource(),
"aws_datapipeline_pipeline": resourceAwsDataPipelinePipeline(),
"aws_datasync_agent": resourceAwsDataSyncAgent(),
"aws_datasync_location_efs": resourceAwsDataSyncLocationEfs(),
Expand Down
176 changes: 176 additions & 0 deletions aws/resource_aws_custom_resource.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
package aws

import (
"encoding/json"
"fmt"
"log"
"math/rand"
"strconv"

"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/lambda"
"github.com/hashicorp/terraform/helper/schema"
)

func resourceAwsCustomResource() *schema.Resource {
return &schema.Resource{
Create: resourceAwsCustomResourceCreate,
Read: resourceAwsCustomResourceRead,
Update: resourceAwsCustomResourceUpdate,
Delete: resourceAwsCustomResourceDelete,
Importer: &schema.ResourceImporter{
State: schema.ImportStatePassthrough,
},

Schema: map[string]*schema.Schema{
"service_token": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"resource_type": {
Type: schema.TypeString,
Required: true,
},
"resource_properties": {
Type: schema.TypeMap,
Optional: true,
Elem: schema.TypeString,
},
"old_resource_properties": {
Type: schema.TypeMap,
Computed: true,
Elem: schema.TypeString,
},
"data": {
Type: schema.TypeMap,
Computed: true,
Elem: schema.TypeString,
},
},
}
}

func resourceAwsCustomResourceCreate(d *schema.ResourceData, meta interface{}) error {
d.Set("old_resource_properties", d.Get("resource_properties"))
data, err := invokeLambda("Create", d, meta)
if err != nil {
return err
}
d.Set("data", data)
id := strconv.FormatUint(rand.Uint64(), 10)
d.SetId(id)
return resourceAwsCustomResourceRead(d, meta)
}

func resourceAwsCustomResourceRead(d *schema.ResourceData, meta interface{}) error {
//AWS Custom Resource does not support "Read" https://docs.aws.amazon.com/en_pv/AWSCloudFormation/latest/UserGuide/crpg-ref-requesttypes.html
return nil
}

func resourceAwsCustomResourceUpdate(d *schema.ResourceData, meta interface{}) error {
if d.HasChange("resource_properties") {
oldResourceProperties, _ := d.GetChange("resource_properties")
d.Set("oldResourceProperties", oldResourceProperties)
data, err := invokeLambda("Update", d, meta)
if err != nil {
return err
}

d.Set("data", data)
}

return resourceAwsCustomResourceRead(d, meta)
}

func resourceAwsCustomResourceDelete(d *schema.ResourceData, meta interface{}) error {
data, err := invokeLambda("Delete", d, meta)
if err != nil {
return err
}

d.Set("data", data)
d.SetId("")
return nil
}

func invokeLambda(requestType string, d *schema.ResourceData, meta interface{}) (map[string]string, error) {
conn := meta.(*AWSClient).lambdaconn
serviceToken := d.Get("service_token").(string)
resourceType := d.Get("resource_type").(string)

var oldResourceProperties map[string]string
if v, ok := d.GetOk("old_resource_properties"); ok {
oldResourceProperties = readProperties(v.(map[string]interface{}))
}

var resourceProperties map[string]string
if v, ok := d.GetOk("resource_properties"); ok {
resourceProperties = readProperties(v.(map[string]interface{}))
}

customResourceRequest := &CustomResourceRequest{
RequestType: requestType,
ResourceType: resourceType,
OldResourceProperties: oldResourceProperties,
ResourceProperties: resourceProperties,
}

payload, _ := json.Marshal(customResourceRequest)

prettyPayload := string(payload)
log.Printf("[DEBUG] Input payload to lambda: %s", prettyPayload)

log.Printf("[DEBUG] %s: Lambda-backed Custom Resource with lambda arn %s", requestType, serviceToken)

logType := "Tail"
invokeRequest := &lambda.InvokeInput{
FunctionName: aws.String(serviceToken),
Payload: payload,
LogType: &logType,
}

var invokeResponse *lambda.InvokeOutput
invokeResponse, err := conn.Invoke(invokeRequest)

if err != nil {
return nil, fmt.Errorf("Error invoking lambda function %s: %s", serviceToken, err)
}
if *invokeResponse.StatusCode != 200 {
return nil, fmt.Errorf("Lambda returned %d status code with error message: %s", *invokeResponse.StatusCode, *invokeResponse.LogResult)
}
var customResourceResponse CustomResourceResponse
err = json.Unmarshal(invokeResponse.Payload, &customResourceResponse)
if err != nil {
return nil, fmt.Errorf("Response cannot be unmarshalled into JSON: %v", err)
}
log.Printf("[DEBUG] Output from lambda function: %v", customResourceResponse)
if customResourceResponse.Status == "FAILED" {
return nil, fmt.Errorf(`Custom resource returned "FAILED" Status code with Reason: %s`, customResourceResponse.Reason)
}
data := readProperties(customResourceResponse.Data)
return data, nil
}

//CustomResourceRequest is an adapter model for requests
type CustomResourceRequest struct {
RequestType string
ResourceType string
OldResourceProperties map[string]string
ResourceProperties map[string]string
}

//CustomResourceResponse is an adapter model for responses
type CustomResourceResponse struct {
Status string
Reason string
Data map[string]interface{}
}

func readProperties(ev map[string]interface{}) map[string]string {
variables := make(map[string]string)
for k, v := range ev {
variables[k] = v.(string)
}
return variables
}
116 changes: 116 additions & 0 deletions aws/resource_aws_custom_resource_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
package aws

import (
"fmt"
"testing"

"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform"
)

func TestAccAWSCustomResource_basic(t *testing.T) {
resourceName := "aws_custom_resource.custom_resource"
rType := "CustomResource"
resource.ParallelTest(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckAwsCustomResourceDestroy, //theres nothing to cleanup
Steps: []resource.TestStep{
{
Config: testAccCustomResourceConfig(rType),
Check: resource.ComposeTestCheckFunc(
testAccCheckCustomResource(resourceName),
),
},
},
})
}

func testAccCheckAwsCustomResourceDestroy(s *terraform.State) error {
//there is nothing to delete because Custom Resource doesn't actually provision anything
return nil

}

func testAccCheckCustomResource(customResource string) resource.TestCheckFunc {
return func(s *terraform.State) error {
rs, ok := s.RootModule().Resources[customResource]
if !ok {
return fmt.Errorf("Not found: %s", customResource)
}

if rs.Primary.ID == "" {
return fmt.Errorf("No ID is set")
}

return nil
}
}

func testAccCustomResourceConfig(resourceType string) string {
return fmt.Sprintf(`
resource "aws_iam_role" "iam_for_lambda" {
name = "iam_for_lambda"
force_detach_policies = true
assume_role_policy = <<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Action": "sts:AssumeRole",
"Principal": {
"Service": "lambda.amazonaws.com"
},
"Effect": "Allow",
"Sid": ""
}
]
}
EOF
}

resource "aws_iam_policy" "lambda_logging" {
name = "lambda_logging"
path = "/"
description = "IAM policy for logging from a lambda"

policy = <<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Action": [
"logs:CreateLogStream",
"logs:PutLogEvents"
],
"Resource": "arn:aws:logs:*:*:*",
"Effect": "Allow"
}
]
}
EOF
}

resource "aws_iam_role_policy_attachment" "lambda_logs" {
role = aws_iam_role.iam_for_lambda.name
policy_arn = aws_iam_policy.lambda_logging.arn
}

resource "aws_lambda_function" "lambda_function" {
filename = "test-fixtures/custom_resource.zip"
function_name = "custom_resource_lambda"
role = aws_iam_role.iam_for_lambda.arn
handler = "index.handler"
runtime = "nodejs10.x"
}

resource "aws_custom_resource" "custom_resource" {
service_token = aws_lambda_function.lambda_function.arn
resource_type = "%s"
resource_properties = {
a = "1"
b = "2"
}
}
`, resourceType)
}
Binary file added aws/test-fixtures/custom_resource.zip
Binary file not shown.
Loading