aws / aws-sdk-go-v2

AWS SDK for the Go programming language.
https://aws.github.io/aws-sdk-go-v2/docs/
Apache License 2.0
2.5k stars 602 forks source link

Control Tower Landing Zone manifest retentionDays unmarshalled into string instead of number #2659

Closed markwellis closed 1 month ago

markwellis commented 1 month ago

Acknowledgements

Describe the bug

The landing zone manifest has two retentionDays fields that are numbers.

output with sensitive data redacted:

{
    "landingZone": {
        "arn": "$arn",
        "driftStatus": {
            "status": "IN_SYNC"
        },
        "latestAvailableVersion": "3.3",
        "manifest": {
            "accessManagement": {
                "enabled": false
            },
            "securityRoles": {
                "accountId": "$account_id"
            },
            "governedRegions": [
                "eu-west-1",
                "eu-central-1",
                "us-east-1",
                "us-west-2"
            ],
            "organizationStructure": {
                "security": {
                    "name": "Security"
                }
            },
            "centralizedLogging": {
                "accountId": "$account_id",
                "configurations": {
                    "loggingBucket": {
                        "retentionDays": 90
                    },
                    "kmsKeyArn": "$kms_arn",
                    "accessLoggingBucket": {
                        "retentionDays": 90
                    }
                },
                "enabled": true
            }
        },
        "status": "ACTIVE",
        "version": "3.3"
    }
}

when using the go sdk, they are converted to string which is incorrect:

&document.documentUnmarshaler{value:map[string]interface {}{"accessManagement":map[string]interface {}{"enabled":false}, "centralizedLogging":map[string]interface {}{"accountId":"$account_id", "configurations":map[string]interface {}{"accessLoggingBucket":map[string]interface {}{"retentionDays":"90"}, "kmsKeyArn":"$kms_arn", "loggingBucket":map[string]interface {}{"retentionDays":"90"}}, "enabled":true}, "governedRegions":[]interface {}{"eu-west-1", "eu-central-1", "us-east-1", "us-west-2"}, "organizationStructure":map[string]interface {}{"security":map[string]interface {}{"name":"Security"}}, "securityRoles":map[string]interface {}{"accountId":"$account_id"}}}

you can see both retentionDays are "90" instead of 90

Expected Behavior

both centralizedLogging.configurations.accessLoggingBucket.retentionDays & centralizedLogging.configurations.loggingBucket.retentionDays in the manifest should remain numbers and not be strings

Current Behavior

both centralizedLogging.configurations.accessLoggingBucket.retentionDays & centralizedLogging.configurations.loggingBucket.retentionDays in the manifest are converted to strings

Reproduction Steps

package main

import (
    "context"
    "github.com/aws/aws-sdk-go-v2/aws"
    "github.com/aws/aws-sdk-go-v2/config"
    "github.com/aws/aws-sdk-go-v2/service/controltower"
    "log"
    "os"
)

func main() {
    // Load the Shared AWS Configuration (~/.aws/config)
    cfg, err := config.LoadDefaultConfig(context.TODO(), config.WithClientLogMode(aws.LogResponseWithBody))
    if err != nil {
        log.Fatal(err)
    }

    client := controltower.NewFromConfig(cfg)

    input := &controltower.GetLandingZoneInput{
        LandingZoneIdentifier: aws.String(os.Args[1]),
    }

    output, err := client.GetLandingZone(context.TODO(), input)
    if err != nil {
        log.Fatal(err)
    }

    log.Printf("\n\nsmithy document: %#v", output.LandingZone.Manifest)
}

./main "$landing_zone_arn output shows the LogResponseWithBody debug logging that the api response is correct. both {"retentionDays":90} are numbers and not strings (formatted for readability)

{
  "landingZone": {
    "arn": "$landing_zone_arn",
    "driftStatus": {
      "status": "IN_SYNC"
    },
    "latestAvailableVersion": "3.3",
    "manifest": {
      "accessManagement": {
        "enabled": false
      },
      "securityRoles": {
        "accountId": "$account_id"
      },
      "governedRegions": [
        "eu-west-1",
        "eu-central-1",
        "us-east-1",
        "us-west-2"
      ],
      "organizationStructure": {
        "security": {
          "name": "Security"
        }
      },
      "centralizedLogging": {
        "accountId": "$account_id",
        "configurations": {
          "loggingBucket": {
            "retentionDays": 90
          },
          "kmsKeyArn": "$kms_arn",
          "accessLoggingBucket": {
            "retentionDays": 90
          }
        },
        "enabled": true
      }
    },
    "status": "ACTIVE",
    "version": "3.3"
  }
}

but the smithy document is incorrect, both {"retentionDays":"90"} are strings and not numbers

smithy document: &document.documentUnmarshaler{value:map[string]interface {}{"accessManagement":map[string]interface {}{"enabled":false}, "centralizedLogging":map[string]interface {}{"accountId":"$account_id", "configurations":map[string]interface {}{"accessLoggingBucket":map[string]interface {}{"retentionDays":"90"}, "kmsKeyArn":"$kms_arn", "loggingBucket":map[string]interface {}{"retentionDays":"90"}}, "enabled":true}, "governedRegions":[]interface {}{"eu-west-1", "eu-central-1", "us-east-1", "us-west-2"}, "organizationStructure":map[string]interface {}{"security":map[string]interface {}{"name":"Security"}}, "securityRoles":map[string]interface {}{"accountId":"$account_id"}}}

Possible Solution

No response

Additional Information/Context

this is causing half of this terraform issue https://github.com/hashicorp/terraform-provider-aws/issues/35763 - specifically this part of the plan, trying to change the type from a string to a number, because when terraform reads the current manifest using the aws sdk the type is wrong so terraform tries to change it back. It is not wrong at aws, but the go aws sdk is converting them to a string

                      ~ accessLoggingBucket = {
                          ~ retentionDays = "365" -> 365
                        }
                      ~ loggingBucket       = {
                          ~ retentionDays = "365" -> 365
                        }

AWS Go SDK V2 Module Versions Used

go mod graph

example github.com/aws/aws-sdk-go-v2@v1.27.0
example github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream@v1.6.2
example github.com/aws/aws-sdk-go-v2/config@v1.27.16
example github.com/aws/aws-sdk-go-v2/credentials@v1.17.16
example github.com/aws/aws-sdk-go-v2/feature/ec2/imds@v1.16.3
example github.com/aws/aws-sdk-go-v2/internal/configsources@v1.3.7
example github.com/aws/aws-sdk-go-v2/internal/endpoints/v2@v2.6.7
example github.com/aws/aws-sdk-go-v2/internal/ini@v1.8.0
example github.com/aws/aws-sdk-go-v2/internal/v4a@v1.3.7
example github.com/aws/aws-sdk-go-v2/service/controltower@v1.14.1
example github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding@v1.11.2
example github.com/aws/aws-sdk-go-v2/service/internal/checksum@v1.3.9
example github.com/aws/aws-sdk-go-v2/service/internal/presigned-url@v1.11.9
example github.com/aws/aws-sdk-go-v2/service/internal/s3shared@v1.17.7
example github.com/aws/aws-sdk-go-v2/service/s3@v1.54.3
example github.com/aws/aws-sdk-go-v2/service/sso@v1.20.9
example github.com/aws/aws-sdk-go-v2/service/ssooidc@v1.24.3
example github.com/aws/aws-sdk-go-v2/service/sts@v1.28.10
example github.com/aws/smithy-go@v1.20.2
example go@1.22.3
github.com/aws/aws-sdk-go-v2@v1.27.0 github.com/aws/smithy-go@v1.20.2
github.com/aws/aws-sdk-go-v2@v1.27.0 github.com/jmespath/go-jmespath@v0.4.0
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream@v1.6.2 github.com/aws/smithy-go@v1.20.2
github.com/aws/aws-sdk-go-v2/config@v1.27.16 github.com/aws/aws-sdk-go-v2@v1.27.0
github.com/aws/aws-sdk-go-v2/config@v1.27.16 github.com/aws/aws-sdk-go-v2/credentials@v1.17.16
github.com/aws/aws-sdk-go-v2/config@v1.27.16 github.com/aws/aws-sdk-go-v2/feature/ec2/imds@v1.16.3
github.com/aws/aws-sdk-go-v2/config@v1.27.16 github.com/aws/aws-sdk-go-v2/internal/ini@v1.8.0
github.com/aws/aws-sdk-go-v2/config@v1.27.16 github.com/aws/aws-sdk-go-v2/service/sso@v1.20.9
github.com/aws/aws-sdk-go-v2/config@v1.27.16 github.com/aws/aws-sdk-go-v2/service/ssooidc@v1.24.3
github.com/aws/aws-sdk-go-v2/config@v1.27.16 github.com/aws/aws-sdk-go-v2/service/sts@v1.28.10
github.com/aws/aws-sdk-go-v2/config@v1.27.16 github.com/aws/smithy-go@v1.20.2
github.com/aws/aws-sdk-go-v2/config@v1.27.16 github.com/aws/aws-sdk-go-v2/internal/configsources@v1.3.7
github.com/aws/aws-sdk-go-v2/config@v1.27.16 github.com/aws/aws-sdk-go-v2/internal/endpoints/v2@v2.6.7
github.com/aws/aws-sdk-go-v2/config@v1.27.16 github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding@v1.11.2
github.com/aws/aws-sdk-go-v2/config@v1.27.16 github.com/aws/aws-sdk-go-v2/service/internal/presigned-url@v1.11.9
github.com/aws/aws-sdk-go-v2/credentials@v1.17.16 github.com/aws/aws-sdk-go-v2@v1.27.0
github.com/aws/aws-sdk-go-v2/credentials@v1.17.16 github.com/aws/aws-sdk-go-v2/feature/ec2/imds@v1.16.3
github.com/aws/aws-sdk-go-v2/credentials@v1.17.16 github.com/aws/aws-sdk-go-v2/service/sso@v1.20.9
github.com/aws/aws-sdk-go-v2/credentials@v1.17.16 github.com/aws/aws-sdk-go-v2/service/ssooidc@v1.24.3
github.com/aws/aws-sdk-go-v2/credentials@v1.17.16 github.com/aws/aws-sdk-go-v2/service/sts@v1.28.10
github.com/aws/aws-sdk-go-v2/credentials@v1.17.16 github.com/aws/smithy-go@v1.20.2
github.com/aws/aws-sdk-go-v2/credentials@v1.17.16 github.com/aws/aws-sdk-go-v2/internal/configsources@v1.3.7
github.com/aws/aws-sdk-go-v2/credentials@v1.17.16 github.com/aws/aws-sdk-go-v2/internal/endpoints/v2@v2.6.7
github.com/aws/aws-sdk-go-v2/credentials@v1.17.16 github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding@v1.11.2
github.com/aws/aws-sdk-go-v2/credentials@v1.17.16 github.com/aws/aws-sdk-go-v2/service/internal/presigned-url@v1.11.9
github.com/aws/aws-sdk-go-v2/feature/ec2/imds@v1.16.3 github.com/aws/aws-sdk-go-v2@v1.27.0
github.com/aws/aws-sdk-go-v2/feature/ec2/imds@v1.16.3 github.com/aws/smithy-go@v1.20.2
github.com/aws/aws-sdk-go-v2/internal/configsources@v1.3.7 github.com/aws/aws-sdk-go-v2@v1.27.0
github.com/aws/aws-sdk-go-v2/internal/configsources@v1.3.7 github.com/aws/smithy-go@v1.20.2
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2@v2.6.7 github.com/aws/aws-sdk-go-v2@v1.27.0
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2@v2.6.7 github.com/aws/smithy-go@v1.20.2
github.com/aws/aws-sdk-go-v2/internal/v4a@v1.3.7 github.com/aws/aws-sdk-go-v2@v1.27.0
github.com/aws/aws-sdk-go-v2/internal/v4a@v1.3.7 github.com/aws/smithy-go@v1.20.2
github.com/aws/aws-sdk-go-v2/service/controltower@v1.14.1 github.com/aws/aws-sdk-go-v2@v1.27.0
github.com/aws/aws-sdk-go-v2/service/controltower@v1.14.1 github.com/aws/aws-sdk-go-v2/internal/configsources@v1.3.7
github.com/aws/aws-sdk-go-v2/service/controltower@v1.14.1 github.com/aws/aws-sdk-go-v2/internal/endpoints/v2@v2.6.7
github.com/aws/aws-sdk-go-v2/service/controltower@v1.14.1 github.com/aws/smithy-go@v1.20.2
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding@v1.11.2 github.com/aws/smithy-go@v1.20.2
github.com/aws/aws-sdk-go-v2/service/internal/checksum@v1.3.9 github.com/aws/aws-sdk-go-v2@v1.27.0
github.com/aws/aws-sdk-go-v2/service/internal/checksum@v1.3.9 github.com/aws/aws-sdk-go-v2/service/internal/presigned-url@v1.11.9
github.com/aws/aws-sdk-go-v2/service/internal/checksum@v1.3.9 github.com/aws/smithy-go@v1.20.2
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url@v1.11.9 github.com/aws/aws-sdk-go-v2@v1.27.0
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url@v1.11.9 github.com/aws/smithy-go@v1.20.2
github.com/aws/aws-sdk-go-v2/service/internal/s3shared@v1.17.7 github.com/aws/aws-sdk-go-v2@v1.27.0
github.com/aws/aws-sdk-go-v2/service/internal/s3shared@v1.17.7 github.com/aws/smithy-go@v1.20.2
github.com/aws/aws-sdk-go-v2/service/s3@v1.54.3 github.com/aws/aws-sdk-go-v2@v1.27.0
github.com/aws/aws-sdk-go-v2/service/s3@v1.54.3 github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream@v1.6.2
github.com/aws/aws-sdk-go-v2/service/s3@v1.54.3 github.com/aws/aws-sdk-go-v2/internal/configsources@v1.3.7
github.com/aws/aws-sdk-go-v2/service/s3@v1.54.3 github.com/aws/aws-sdk-go-v2/internal/endpoints/v2@v2.6.7
github.com/aws/aws-sdk-go-v2/service/s3@v1.54.3 github.com/aws/aws-sdk-go-v2/internal/v4a@v1.3.7
github.com/aws/aws-sdk-go-v2/service/s3@v1.54.3 github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding@v1.11.2
github.com/aws/aws-sdk-go-v2/service/s3@v1.54.3 github.com/aws/aws-sdk-go-v2/service/internal/checksum@v1.3.9
github.com/aws/aws-sdk-go-v2/service/s3@v1.54.3 github.com/aws/aws-sdk-go-v2/service/internal/presigned-url@v1.11.9
github.com/aws/aws-sdk-go-v2/service/s3@v1.54.3 github.com/aws/aws-sdk-go-v2/service/internal/s3shared@v1.17.7
github.com/aws/aws-sdk-go-v2/service/s3@v1.54.3 github.com/aws/smithy-go@v1.20.2
github.com/aws/aws-sdk-go-v2/service/sso@v1.20.9 github.com/aws/aws-sdk-go-v2@v1.27.0
github.com/aws/aws-sdk-go-v2/service/sso@v1.20.9 github.com/aws/aws-sdk-go-v2/internal/configsources@v1.3.7
github.com/aws/aws-sdk-go-v2/service/sso@v1.20.9 github.com/aws/aws-sdk-go-v2/internal/endpoints/v2@v2.6.7
github.com/aws/aws-sdk-go-v2/service/sso@v1.20.9 github.com/aws/smithy-go@v1.20.2
github.com/aws/aws-sdk-go-v2/service/ssooidc@v1.24.3 github.com/aws/aws-sdk-go-v2@v1.27.0
github.com/aws/aws-sdk-go-v2/service/ssooidc@v1.24.3 github.com/aws/aws-sdk-go-v2/internal/configsources@v1.3.7
github.com/aws/aws-sdk-go-v2/service/ssooidc@v1.24.3 github.com/aws/aws-sdk-go-v2/internal/endpoints/v2@v2.6.7
github.com/aws/aws-sdk-go-v2/service/ssooidc@v1.24.3 github.com/aws/smithy-go@v1.20.2
github.com/aws/aws-sdk-go-v2/service/sts@v1.28.10 github.com/aws/aws-sdk-go-v2@v1.27.0
github.com/aws/aws-sdk-go-v2/service/sts@v1.28.10 github.com/aws/aws-sdk-go-v2/internal/configsources@v1.3.7
github.com/aws/aws-sdk-go-v2/service/sts@v1.28.10 github.com/aws/aws-sdk-go-v2/internal/endpoints/v2@v2.6.7
github.com/aws/aws-sdk-go-v2/service/sts@v1.28.10 github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding@v1.11.2
github.com/aws/aws-sdk-go-v2/service/sts@v1.28.10 github.com/aws/aws-sdk-go-v2/service/internal/presigned-url@v1.11.9
github.com/aws/aws-sdk-go-v2/service/sts@v1.28.10 github.com/aws/smithy-go@v1.20.2
go@1.22.3 toolchain@go1.22.3

Compiler and Version used

go version go1.22.3 linux/amd64

Operating System and version

Fedora Linux 40

lucix-aws commented 1 month ago

The use of the #v formatter here is misleading. What you're seeing when you print a document value in a response directly like that is the un-mutated interface{} value for the manifest field that was decoded from the service's JSON response. The deserializers for JSON services all use json.Decoder.UseNumber, so numeric tokens are decoded by the stdlib into json.Number which is an alias for string.

For whatever reason the #v represents these json.Number values with quotes. Couldn't tell you why.

If you actually use UnmarshalSmithyDocument to unwrap the response document into something, it'll recognize the numericness of that inner field just fine. Example at the end.

I can't really speak to these terraform issues. Perhaps they're doing something wrong in regards to this field, but that would surprise me -- the value you're showing through #v really doesn't mean anything programmatically, under the hood I assume they're using the document unmarshaling API as intended in some way to actually do things with this document field, and they can definitely unmarshal the inner fields in question into numeric types as the example shows.

(for this example I just mocked the wire response instead of creating one of these landing zones, which I know nothing about and it seemed involved/impossible for me without additional setup):

package main

import (
    "context"
    "io"
    "log"
    "net/http"
    "strings"

    "github.com/aws/aws-sdk-go-v2/aws"
    "github.com/aws/aws-sdk-go-v2/config"
    "github.com/aws/aws-sdk-go-v2/service/controltower"
)

const lz = `
{
  "landingZone": {
    "arn": "$landing_zone_arn",
    "driftStatus": {
      "status": "IN_SYNC"
    },
    "latestAvailableVersion": "3.3",
    "manifest": {
      "accessManagement": {
        "enabled": false
      },
      "securityRoles": {
        "accountId": "$account_id"
      },
      "governedRegions": [
        "eu-west-1",
        "eu-central-1",
        "us-east-1",
        "us-west-2"
      ],
      "organizationStructure": {
        "security": {
          "name": "Security"
        }
      },
      "centralizedLogging": {
        "accountId": "$account_id",
        "configurations": {
          "loggingBucket": {
            "retentionDays": 90
          },
          "kmsKeyArn": "$kms_arn",
          "accessLoggingBucket": {
            "retentionDays": 90
          }
        },
        "enabled": true
      }
    },
    "status": "ACTIVE",
    "version": "3.3"
  }
}
`

type mockHTTP struct{}

func (mockHTTP) Do(r *http.Request) (*http.Response, error) {
    return &http.Response{
        StatusCode: http.StatusOK,
        Body:       io.NopCloser(strings.NewReader(lz)),
    }, nil
}

type Manifest struct {
    // omitting stuff we don't care about in this example
    CentralizedLogging struct {
        Configurations struct {
            LoggingBucket struct {
                RetentionDays int
            }
        }
    }
}

func main() {
    cfg, err := config.LoadDefaultConfig(context.Background(), config.WithClientLogMode(aws.LogResponseWithBody))
    if err != nil {
        log.Fatal(err)
    }

    client := controltower.NewFromConfig(cfg)

    input := &controltower.GetLandingZoneInput{
        LandingZoneIdentifier: aws.String("foo"),
    }

    output, err := client.GetLandingZone(context.Background(), input, func(o *controltower.Options) {
        o.HTTPClient = mockHTTP{}
    })
    if err != nil {
        log.Fatal(err)
    }

    var manifest Manifest
    if err := output.LandingZone.Manifest.UnmarshalSmithyDocument(&manifest); err != nil {
        panic(err)
    }
    log.Printf("\n\nsmithy document: %#v", output.LandingZone.Manifest)
    println("retention days", manifest.CentralizedLogging.Configurations.LoggingBucket.RetentionDays)
}
markwellis commented 1 month ago

That makes sense, but but doesn't explain the issue with terraform, which is where this issue is causing me problems!

The terraform provider reads the aws api, using the sdk, which returns it as a smithy document, then it re-encodes it back to a json string. I have adapted your example above to do what the terraform provider does, and you can see in the output both retentionDays are strings.

Given this is just round tripping from json string -> smithy document -> json string, shouldn't numbers be preserved as numbers and not turned to strings?

package main

import (
    "context"
    "encoding/json"
    smithydocument "github.com/aws/smithy-go/document"
    "io"
    "log"
    "net/http"
    "strings"

    "github.com/aws/aws-sdk-go-v2/aws"
    "github.com/aws/aws-sdk-go-v2/config"
    "github.com/aws/aws-sdk-go-v2/service/controltower"
)

const lz = `
{
  "landingZone": {
    "arn": "$landing_zone_arn",
    "driftStatus": {
      "status": "IN_SYNC"
    },
    "latestAvailableVersion": "3.3",
    "manifest": {
      "accessManagement": {
        "enabled": false
      },
      "securityRoles": {
        "accountId": "$account_id"
      },
      "governedRegions": [
        "eu-west-1",
        "eu-central-1",
        "us-east-1",
        "us-west-2"
      ],
      "organizationStructure": {
        "security": {
          "name": "Security"
        }
      },
      "centralizedLogging": {
        "accountId": "$account_id",
        "configurations": {
          "loggingBucket": {
            "retentionDays": 90
          },
          "kmsKeyArn": "$kms_arn",
          "accessLoggingBucket": {
            "retentionDays": 90
          }
        },
        "enabled": true
      }
    },
    "status": "ACTIVE",
    "version": "3.3"
  }
}
`

type mockHTTP struct{}

func (mockHTTP) Do(r *http.Request) (*http.Response, error) {
    return &http.Response{
        StatusCode: http.StatusOK,
        Body:       io.NopCloser(strings.NewReader(lz)),
    }, nil
}

// from https://github.com/hashicorp/terraform-provider-aws/blob/main/internal/json/smithy.go#L25
func SmithyDocumentToString(document smithydocument.Unmarshaler) (string, error) {
    var v map[string]interface{}

    err := document.UnmarshalSmithyDocument(&v)
    if err != nil {
        return "", err
    }

    bytes, err := json.Marshal(v)
    if err != nil {
        return "", err
    }

    return string(bytes), nil
}

func main() {
    cfg, err := config.LoadDefaultConfig(context.Background(), config.WithClientLogMode(aws.LogResponseWithBody))
    if err != nil {
        log.Fatal(err)
    }

    client := controltower.NewFromConfig(cfg)

    input := &controltower.GetLandingZoneInput{
        LandingZoneIdentifier: aws.String("foo"),
    }

    output, err := client.GetLandingZone(context.Background(), input, func(o *controltower.Options) {
        o.HTTPClient = mockHTTP{}
    })
    if err != nil {
        log.Fatal(err)
    }

    //from https://github.com/hashicorp/terraform-provider-aws/blob/main/internal/service/controltower/landing_zone.go#L157
    v, _ := SmithyDocumentToString(output.LandingZone.Manifest)

    println("as json ", v)
}

output

as json {"accessManagement":{"enabled":false},"centralizedLogging":{"accountId":"$account_id","configurations":{"accessLoggingBucket":{"retentionDays":"90"},"kmsKeyArn":"$kms_arn","loggingBucket":{"retentionDays":"90"}},"enabled":true},"governedRegions":["eu-west-1","eu-central-1","us-east-1","us-west-2"],"organizationStructure":{"security":{"name":"Security"}},"securityRoles":{"accountId":"$account_id"}}

Thanks

lucix-aws commented 1 month ago

You've identified exactly where the disconnect occurs there.

Numeric values (so, ones stored in the raw document interface{} as json.Number), when unmarshaled into an interface{} value are unmarshaled as document.Number - which again is just an alias for string, it's mimicking the json.Number API. You can modify my original example to observe this:

type Manifest struct {
    CentralizedLogging struct {
        Configurations struct {
            LoggingBucket struct {
                RetentionDays interface{} // change this from int -> interface{}
            }
        }
    }
}

func main() {
    // ...
    var manifest Manifest
    if err := output.LandingZone.Manifest.UnmarshalSmithyDocument(&manifest); err != nil {
        panic(err)
    }
    // %T shows us the type
    fmt.Printf("RetentionDays is %T\n", manifest.CentralizedLogging.Configurations.LoggingBucket.RetentionDays)
}

prints

RetentionDays is document.Number

json.Marshal doesn't know anything about document.Number - so when it sees that value, it just sees through reflection that it's a string alias and marshals accordingly. Conversely a json.Number would be round-tripped correctly because the implementation is built to recognize that type specifically.

So basically, terraform's logic to map a document type over to a json payload is wrong in the sense that it misses this translation. That's something they need to handle.

FWIW if I had to release this from scratch again, I don't think I would have chosen to handle document numerics this way. There's a disconnect here in the sense that the stdlib json decoder gives you the choice to decode into json.Number, whereas we do not. Unfortunately that ship has sailed.

Going to close this out at this time, since there's definitively no SDK defect either way.

github-actions[bot] commented 1 month ago

This issue is now closed. Comments on closed issues are hard for our team to see. If you need more assistance, please open a new issue that references this one.