func sendCloudFormationResponse(customResourceRequest *AbstractCustomResourceRequest, results map[string]interface{}, responseErr error, logger *logrus.Logger) error { parsedURL, parsedURLErr := url.ParseRequestURI(customResourceRequest.ResponseURL) if nil != parsedURLErr { return parsedURLErr } status := "FAILED" if nil == responseErr { status = "SUCCESS" } reasonText := "" if nil != responseErr { reasonText = fmt.Sprintf("%s. Details in CloudWatch Logs: %s : %s", responseErr.Error(), customResourceRequest.LogGroupName, customResourceRequest.LogStreamName) } else { reasonText = fmt.Sprintf("Details in CloudWatch Logs: %s : %s", customResourceRequest.LogGroupName, customResourceRequest.LogStreamName) } responseData := map[string]interface{}{ "Status": status, "Reason": reasonText, "PhysicalResourceId": customResourceRequest.PhysicalResourceID, "StackId": customResourceRequest.StackID, "RequestId": customResourceRequest.RequestID, "LogicalResourceId": customResourceRequest.LogicalResourceID, } if nil != responseErr { responseData["Data"] = map[string]interface{}{ "Error": responseErr, } } else if nil != results { responseData["Data"] = results } else { responseData["Data"] = map[string]interface{}{} } logger.WithFields(logrus.Fields{ "ResponsePayload": responseData, }).Debug("Response Info") jsonData, jsonError := json.Marshal(responseData) if nil != jsonError { return jsonError } responseBuffer := strings.NewReader(string(jsonData)) req, httpErr := http.NewRequest("PUT", customResourceRequest.ResponseURL, responseBuffer) if nil != httpErr { return httpErr } // Need to use the Opaque field b/c Go will parse inline encoded values // which are supposed to be roundtripped to AWS. // Ref: https://tools.ietf.org/html/rfc3986#section-2.2 // Ref: https://golang.org/pkg/net/url/#URL req.URL = &url.URL{ Scheme: parsedURL.Scheme, Host: parsedURL.Host, Opaque: parsedURL.RawPath, RawQuery: parsedURL.RawQuery, } logger.WithFields(logrus.Fields{ "URL": req.URL, }).Debug("Created URL response") // Although it seems reasonable to set the Content-Type to "application/json" - don't. // The Content-Type must be an empty string in order for the // AWS Signature checker to pass. // Ref: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-lambda-function-code.html req.Header.Set("Content-Type", "") client := &http.Client{} resp, httpErr := client.Do(req) if httpErr != nil { return httpErr } logger.WithFields(logrus.Fields{ "LogicalResourceId": customResourceRequest.LogicalResourceID, "Result": responseData["Status"], "ResponseStatusCode": resp.StatusCode, }).Info("Sent CloudFormation response") if resp.StatusCode < 200 || resp.StatusCode > 299 { body, bodyErr := ioutil.ReadAll(resp.Body) if bodyErr != nil { logger.Warn("Unable to read body: " + bodyErr.Error()) body = []byte{} } return fmt.Errorf("Error sending response: %d. Data: %s", resp.StatusCode, string(body)) } defer resp.Body.Close() return nil }