diff --git a/README.md b/README.md index 8d803a4..b63bc09 100644 --- a/README.md +++ b/README.md @@ -124,4 +124,46 @@ func TestMySocket(t *testing.T) { t.Logf("%s", tofutestutils.Must2(io.ReadAll(client))) } -``` \ No newline at end of file +``` + +## AWS backend + +# AWS test backend + +This library implements an AWS test backend using [LocalStack](https://www.localstack.cloud/) and Docker. To use it, you will need a local Docker daemon running. You can use the backend as follows: + +```go +package your_test + +import ( + "bytes" + "context" + "testing" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/s3" + "github.com/opentofu/tofutestutils" +) + +func TestMyApp(t *testing.T) { + awsBackend := tofutestutils.AWS(t) + + s3Connection := s3.NewFromConfig(awsBackend.Config(), func(options *s3.Options) { + options.UsePathStyle = awsBackend.S3UsePathStyle() + }) + + if _, err := s3Connection.PutObject( + context.TODO(), + &s3.PutObjectInput{ + Key: aws.String("test.txt"), + Body: bytes.NewReader([]byte("Hello world!")), + Bucket: aws.String(awsBackend.S3Bucket()), + }, + ); err != nil { + t.Fatalf("❌ Failed to put object (%v)", err) + } +} +``` + +> [!WARNING] +> Always use the [test context](#test-context) for timeouts as described above so the backend has time to clean up the test container. \ No newline at end of file diff --git a/aws.go b/aws.go new file mode 100644 index 0000000..896b633 --- /dev/null +++ b/aws.go @@ -0,0 +1,17 @@ +// Copyright (c) The OpenTofu Authors +// SPDX-License-Identifier: MPL-2.0 +// Copyright (c) 2023 HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package tofutestutils + +import ( + "testing" + + "github.com/opentofu/tofutestutils/testaws" +) + +// AWS creates an AWS service for testing purposes. +func AWS(t *testing.T) testaws.AWSTestService { + return testaws.New(t) +} diff --git a/context.go b/context.go index fa021fc..08c81c6 100644 --- a/context.go +++ b/context.go @@ -8,21 +8,12 @@ package tofutestutils import ( "context" "testing" - "time" "github.com/opentofu/tofutestutils/testcontext" ) -const minCleanupSafety = time.Second * 30 -const maxCleanupSafety = time.Minute * 5 - // Context returns a context configured for the test deadline. This function configures a context with 25% safety for // any cleanup tasks to finish. For a more flexible function see testcontext.Context. func Context(t *testing.T) context.Context { - return testcontext.Context( - t, - 4, - minCleanupSafety, - maxCleanupSafety, - ) + return testcontext.DefaultContext(t) } diff --git a/go.mod b/go.mod index a20de01..a3751d4 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,15 @@ module github.com/opentofu/tofutestutils go 1.22 require ( + github.com/aws/aws-sdk-go v1.55.5 + github.com/aws/aws-sdk-go-v2 v1.30.3 + github.com/aws/aws-sdk-go-v2/service/dynamodb v1.34.4 + github.com/aws/aws-sdk-go-v2/service/iam v1.34.3 + github.com/aws/aws-sdk-go-v2/service/kms v1.35.3 + github.com/aws/aws-sdk-go-v2/service/s3 v1.58.3 + github.com/aws/aws-sdk-go-v2/service/sts v1.30.3 + github.com/docker/docker v27.1.1+incompatible + github.com/docker/go-connections v0.5.0 github.com/hashicorp/go-hclog v1.6.3 github.com/testcontainers/testcontainers-go v0.32.0 ) @@ -12,14 +21,22 @@ require ( github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect github.com/Microsoft/hcsshim v0.11.5 // indirect + github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.3 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.15 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.15 // indirect + github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.15 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.3 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.17 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.9.16 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.17 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.15 // indirect + github.com/aws/smithy-go v1.20.3 // indirect github.com/cenkalti/backoff/v4 v4.2.1 // indirect github.com/containerd/containerd v1.7.18 // indirect github.com/containerd/errdefs v0.1.0 // indirect github.com/containerd/log v0.1.0 // indirect github.com/cpuguy83/dockercfg v0.3.1 // indirect github.com/distribution/reference v0.6.0 // indirect - github.com/docker/docker v27.0.3+incompatible // indirect - github.com/docker/go-connections v0.5.0 // indirect github.com/docker/go-units v0.5.0 // indirect github.com/fatih/color v1.13.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect @@ -29,6 +46,7 @@ require ( github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/google/uuid v1.6.0 // indirect + github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/klauspost/compress v1.17.4 // indirect github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect github.com/magiconair/properties v1.8.7 // indirect diff --git a/go.sum b/go.sum index bd59596..eebc603 100644 --- a/go.sum +++ b/go.sum @@ -8,6 +8,40 @@ github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERo github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/Microsoft/hcsshim v0.11.5 h1:haEcLNpj9Ka1gd3B3tAEs9CpE0c+1IhoL59w/exYU38= github.com/Microsoft/hcsshim v0.11.5/go.mod h1:MV8xMfmECjl5HdO7U/3/hFVnkmSBjAjmA09d4bExKcU= +github.com/aws/aws-sdk-go v1.55.5 h1:KKUZBfBoyqy5d3swXyiC7Q76ic40rYcbqH7qjh59kzU= +github.com/aws/aws-sdk-go v1.55.5/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU= +github.com/aws/aws-sdk-go-v2 v1.30.3 h1:jUeBtG0Ih+ZIFH0F4UkmL9w3cSpaMv9tYYDbzILP8dY= +github.com/aws/aws-sdk-go-v2 v1.30.3/go.mod h1:nIQjQVp5sfpQcTc9mPSr1B0PaWK5ByX9MOoDadSN4lc= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.3 h1:tW1/Rkad38LA15X4UQtjXZXNKsCgkshC3EbmcUmghTg= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.3/go.mod h1:UbnqO+zjqk3uIt9yCACHJ9IVNhyhOCnYk8yA19SAWrM= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.15 h1:SoNJ4RlFEQEbtDcCEt+QG56MY4fm4W8rYirAmq+/DdU= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.15/go.mod h1:U9ke74k1n2bf+RIgoX1SXFed1HLs51OgUSs+Ph0KJP8= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.15 h1:C6WHdGnTDIYETAm5iErQUiVNsclNx9qbJVPIt03B6bI= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.15/go.mod h1:ZQLZqhcu+JhSrA9/NXRm8SkDvsycE+JkV3WGY41e+IM= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.15 h1:Z5r7SycxmSllHYmaAZPpmN8GviDrSGhMS6bldqtXZPw= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.15/go.mod h1:CetW7bDE00QoGEmPUoZuRog07SGVAUVW6LFpNP0YfIg= +github.com/aws/aws-sdk-go-v2/service/dynamodb v1.34.4 h1:utG3S4T+X7nONPIpRoi1tVcQdAdJxntiVS2yolPJyXc= +github.com/aws/aws-sdk-go-v2/service/dynamodb v1.34.4/go.mod h1:q9vzW3Xr1KEXa8n4waHiFt1PrppNDlMymlYP+xpsFbY= +github.com/aws/aws-sdk-go-v2/service/iam v1.34.3 h1:p4L/tixJ3JUIxCteMGT6oMlqCbEv/EzSZoVwdiib8sU= +github.com/aws/aws-sdk-go-v2/service/iam v1.34.3/go.mod h1:rfOWxxwdecWvSC9C2/8K/foW3Blf+aKnIIPP9kQ2DPE= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.3 h1:dT3MqvGhSoaIhRseqw2I0yH81l7wiR2vjs57O51EAm8= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.3/go.mod h1:GlAeCkHwugxdHaueRr4nhPuY+WW+gR8UjlcqzPr1SPI= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.17 h1:YPYe6ZmvUfDDDELqEKtAd6bo8zxhkm+XEFEzQisqUIE= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.17/go.mod h1:oBtcnYua/CgzCWYN7NZ5j7PotFDaFSUjCYVTtfyn7vw= +github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.9.16 h1:lhAX5f7KpgwyieXjbDnRTjPEUI0l3emSRyxXj1PXP8w= +github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.9.16/go.mod h1:AblAlCwvi7Q/SFowvckgN+8M3uFPlopSYeLlbNDArhA= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.17 h1:HGErhhrxZlQ044RiM+WdoZxp0p+EGM62y3L6pwA4olE= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.17/go.mod h1:RkZEx4l0EHYDJpWppMJ3nD9wZJAa8/0lq9aVC+r2UII= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.15 h1:246A4lSTXWJw/rmlQI+TT2OcqeDMKBdyjEQrafMaQdA= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.15/go.mod h1:haVfg3761/WF7YPuJOER2MP0k4UAXyHaLclKXB6usDg= +github.com/aws/aws-sdk-go-v2/service/kms v1.35.3 h1:UPTdlTOwWUX49fVi7cymEN6hDqCwe3LNv1vi7TXUutk= +github.com/aws/aws-sdk-go-v2/service/kms v1.35.3/go.mod h1:gjDP16zn+WWalyaUqwCCioQ8gU8lzttCCc9jYsiQI/8= +github.com/aws/aws-sdk-go-v2/service/s3 v1.58.3 h1:hT8ZAZRIfqBqHbzKTII+CIiY8G2oC9OpLedkZ51DWl8= +github.com/aws/aws-sdk-go-v2/service/s3 v1.58.3/go.mod h1:Lcxzg5rojyVPU/0eFwLtcyTaek/6Mtic5B1gJo7e/zE= +github.com/aws/aws-sdk-go-v2/service/sts v1.30.3 h1:ZsDKRLXGWHk8WdtyYMoGNO7bTudrvuKpDKgMVRlepGE= +github.com/aws/aws-sdk-go-v2/service/sts v1.30.3/go.mod h1:zwySh8fpFyXp9yOr/KVzxOl8SRqgf/IDw5aUt9UKFcQ= +github.com/aws/smithy-go v1.20.3 h1:ryHwveWzPV5BIof6fyDvor6V3iUL7nTfiTKXHiW05nE= +github.com/aws/smithy-go v1.20.3/go.mod h1:krry+ya/rV9RDcV/Q16kpu6ypI4K2czasz0NC3qS14E= github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/containerd/containerd v1.7.18 h1:jqjZTQNfXGoEaZdW1WwPU0RqSn1Bm2Ay/KJPUuO8nao= @@ -25,8 +59,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= -github.com/docker/docker v27.0.3+incompatible h1:aBGI9TeQ4MPlhquTQKq9XbK79rKFVwXNUAYz9aXyEBE= -github.com/docker/docker v27.0.3+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker v27.1.1+incompatible h1:hO/M4MtV36kzKldqnA37IWhebRA+LnqqcqDja6kVaKY= +github.com/docker/docker v27.1.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= @@ -56,6 +90,10 @@ github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 h1:YBftPWNWd4WwGqtY2yeZL2ef8rH github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0/go.mod h1:YN5jB8ie0yfIUg6VvR9Kz84aCaG7AsGZnLjhHbUqwPg= github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= +github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= +github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= +github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4= @@ -195,6 +233,9 @@ google.golang.org/grpc v1.59.0/go.mod h1:aUPDwccQo6OTjy7Hct4AfBPD1GptF4fyUjIkQ9Y google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/testaws/README.md b/testaws/README.md new file mode 100644 index 0000000..ee66fad --- /dev/null +++ b/testaws/README.md @@ -0,0 +1,39 @@ +# AWS test backend + +This package implements creating an AWS test backend using [LocalStack](https://www.localstack.cloud/) and Docker. To use it, you will need a local Docker daemon running. You can use the backend as follows: + +```go +package your_test + +import ( + "bytes" + "context" + "testing" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/s3" + "github.com/opentofu/tofutestutils/testaws" +) + +func TestMyApp(t *testing.T) { + awsBackend := testaws.New(t) + + s3Connection := s3.NewFromConfig(awsBackend.Config(), func(options *s3.Options) { + options.UsePathStyle = awsBackend.S3UsePathStyle() + }) + + if _, err := s3Connection.PutObject( + context.TODO(), + &s3.PutObjectInput{ + Key: aws.String("test.txt"), + Body: bytes.NewReader([]byte("Hello world!")), + Bucket: aws.String(awsBackend.S3Bucket()), + }, + ); err != nil { + t.Fatalf("❌ Failed to put object (%v)", err) + } +} +``` + +> [!WARNING] +> Always use the [test context](../testcontext) for timeouts so the backend has time to clean up the test container. \ No newline at end of file diff --git a/testaws/aws.go b/testaws/aws.go new file mode 100644 index 0000000..10b98c8 --- /dev/null +++ b/testaws/aws.go @@ -0,0 +1,253 @@ +// Copyright (c) The OpenTofu Authors +// SPDX-License-Identifier: MPL-2.0 +// Copyright (c) 2023 HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package testaws + +import ( + "context" + "fmt" + "net/http" + "os" + "path" + "regexp" + "strconv" + "strings" + "testing" + "time" + + awsv2 "github.com/aws/aws-sdk-go-v2/aws" + awsv1 "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/credentials" + "github.com/docker/docker/api/types/container" + "github.com/docker/go-connections/nat" + "github.com/opentofu/tofutestutils/testca" + "github.com/opentofu/tofutestutils/testcontext" + "github.com/opentofu/tofutestutils/testlog" + "github.com/opentofu/tofutestutils/testrandom" + testcontainers "github.com/testcontainers/testcontainers-go" + "github.com/testcontainers/testcontainers-go/wait" +) + +// AWSTestServiceBase is an interface all AWS-related test services should embed. +type AWSTestServiceBase interface { + // AccessKey returns the access key to use for authentication. + AccessKey() string + + // SecretKey returns the secret key to use for authentication. + SecretKey() string + + // Region returns the AWS region to use. + Region() string + + // CACert returns the CA certificate the service will present. This may be empty if the endpoint is not an + // https endpoint. + CACert() []byte + + // CACertFile returns a file on the local disk containing the CA certificate. + CACertFile() string + + // Config creates an AWS Go SDK v2-compatible configuration. + Config() awsv2.Config +} + +// AWSTestService is a top interface for all AWS-related test services. +type AWSTestService interface { + AWSIAMTestService + AWSSTSTestService + AWSS3TestService + AWSDynamoDBTestService + AWSKMSTestService +} + +// New creates a locally-emulated AWS stack or attaches to an already-existing AWS configuration from the environment. +// +// Important: if you are using this tool, please make sure to also use testutils.Context in order to give this tool +// enough time to tear down the test infrastructure. +func New(t *testing.T) AWSTestService { + return newAWSTestService(t, []awsServiceFixture{ + &iamServiceFixture{}, + &stsServiceFixture{}, + &s3ServiceFixture{}, + &dynamoDBServiceFixture{}, + &kmsServiceFixture{}, + }) +} + +var nameRe = regexp.MustCompile(`[^a-zA-Z0-9_.-]`) + +func newAWSTestService(t *testing.T, services []awsServiceFixture) AWSTestService { + t.Logf("🚧 Configuring AWS test service...") + ctx := testcontext.DefaultContext(t) + + ca := testca.New(t, testrandom.Source(), time.Now) + pair := ca.CreateLocalhostServerCert() + tempDir := t.TempDir() + if err := os.WriteFile(path.Join(tempDir, "server.pem"), append(pair.Certificate, pair.PrivateKey...), permAll); err != nil { + t.Skipf("Cannot write to test directory %s: %v", tempDir, err) + } + + caCertFile := path.Join(tempDir, "cacert.pem") + if err := os.WriteFile(caCertFile, ca.GetPEMCACert(), permAll); err != nil { + t.Skipf("Cannot write to test directory %s: %v", tempDir, err) + } + + var ids []string + for _, service := range services { + ids = append(ids, service.LocalStackID()) + } + + const localStackPort = 4566 + natPort := fmt.Sprintf("%d/tcp", localStackPort) + + request := testcontainers.ContainerRequest{ + HostAccessPorts: nil, + Image: "localstack/localstack", + Env: map[string]string{ + "LOCALSTACK_HOST": fmt.Sprintf("localhost:%d", localStackPort), + "SERVICES": strings.Join(ids, ","), + // Eager loading is on because we need to provision test fixtures anyway. + "EAGER_SERVICE_LOADING": "1", + "SKIP_SSL_CERT_DOWNLOAD": "1", + "CUSTOM_SSL_CERT_PATH": "/opt/certs/server.pem", + }, + ExposedPorts: []string{ + natPort, + }, + Name: nameRe.ReplaceAllString(t.Name(), ""), + HostConfigModifier: func(config *container.HostConfig) { + config.Binds = []string{ + fmt.Sprintf("%s:/opt/certs", tempDir), + "/var/run/docker.sock:/var/run/docker.sock", + } + }, + WaitingFor: wait.ForLog("Ready."), + } + localStackContainer, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ + ContainerRequest: request, + Started: true, + Logger: testlog.NewTestContainersLogger(t), + }) + if err != nil { + t.Skipf("❌ Failed to start LocalStack backend: %v", err) + return nil + } + t.Cleanup(func() { + if err := localStackContainer.Terminate(ctx); err != nil { + t.Logf("❌ Failed to stop LocalStack container %s: %v", localStackContainer.GetContainerID(), err) + } + }) + + mappedPort, err := localStackContainer.MappedPort(ctx, nat.Port(natPort)) + if err != nil { + t.Skipf("❌ Failed to get mapped port for LocalStack instance (%v)", err) + } + host, err := localStackContainer.Host(ctx) + if err != nil { + t.Skipf("❌ Failed to get host for LocalStack instance (%v)", err) + } + + svc := &awsTestService{ + t: t, + ctx: ctx, + ca: ca, + caCertFile: caCertFile, + endpoint: "https://" + host + ":" + strconv.Itoa(mappedPort.Int()), + region: "us-east-1", + accessKeyID: "test", + secretKeyID: "test", + } + for _, service := range services { + service := service + if err := service.Setup(svc); err != nil { + t.Skipf("❌ Failed to initialize %s: %v", service.Name(), err) + return nil + } + t.Cleanup(func() { + if err := service.Teardown(svc); err != nil { + t.Errorf("❌ Failed to tear down service %s: %v", service.Name(), err) + } + }) + } + t.Logf("✅ AWS test service is ready for use.") + return svc +} + +type awsServiceFixture interface { + Name() string + LocalStackID() string + Setup(service *awsTestService) error + Teardown(service *awsTestService) error +} + +type awsTestService struct { + t *testing.T + ctx context.Context + ca testca.CertificateAuthority + caCertFile string + endpoint string + region string + accessKeyID string + secretKeyID string + + awsIAMParameters + awsSTSParameters + awsS3Parameters + awsDynamoDBParameters + awsKMSParameters +} + +func (a awsTestService) CACertFile() string { + return a.caCertFile +} + +func (a awsTestService) ConfigV1() awsv1.Config { + return awsv1.Config{ + Credentials: credentials.NewCredentials( + &credentials.StaticProvider{ + Value: credentials.Value{ + AccessKeyID: a.accessKeyID, + SecretAccessKey: a.secretKeyID, + }, + }, + ), + Endpoint: awsv1.String(a.endpoint), + Region: awsv1.String(a.region), + S3ForcePathStyle: awsv1.Bool(a.s3PathStyle), + } +} + +func (a awsTestService) Config() awsv2.Config { + return awsv2.Config{ + Region: a.region, + Credentials: awsv2.CredentialsProviderFunc(func(_ context.Context) (awsv2.Credentials, error) { + return awsv2.Credentials{ + AccessKeyID: a.accessKeyID, + SecretAccessKey: a.secretKeyID, + }, nil + }), + BaseEndpoint: awsv2.String(a.endpoint), + HTTPClient: &http.Client{ + Transport: &http.Transport{ + TLSClientConfig: a.ca.GetClientTLSConfig(), + }, + }, + } +} + +func (a awsTestService) AccessKey() string { + return a.accessKeyID +} + +func (a awsTestService) SecretKey() string { + return a.secretKeyID +} + +func (a awsTestService) Region() string { + return a.region +} + +func (a awsTestService) CACert() []byte { + return a.ca.GetPEMCACert() +} diff --git a/testaws/aws_dynamodb.go b/testaws/aws_dynamodb.go new file mode 100644 index 0000000..99c9784 --- /dev/null +++ b/testaws/aws_dynamodb.go @@ -0,0 +1,114 @@ +// Copyright (c) The OpenTofu Authors +// SPDX-License-Identifier: MPL-2.0 +// Copyright (c) 2023 HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package testaws + +import ( + "context" + "fmt" + "regexp" + "strings" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/dynamodb" + "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" + "github.com/opentofu/tofutestutils/testrandom" +) + +// AWSDynamoDBTestService is a specialized extension to the AWSTestServiceBase containing DynamoDB-specific functions. +type AWSDynamoDBTestService interface { + AWSTestServiceBase + + // DynamoDBEndpoint returns the endpoint for the DynamoDB service. + DynamoDBEndpoint() string + + // DynamoDBTable returns a DynamoDB table suitable for testing. This table will contain an attribute called LockID + // with the type of String and a key for this attribute. You may or may not be able to create additional tables. + DynamoDBTable() string +} + +var dynamoDBNameReplaceRe = regexp.MustCompile(`[^a-zA-Z0-9._-]`) + +type dynamoDBServiceFixture struct { +} + +func (d dynamoDBServiceFixture) Name() string { + return "DynamoDB" +} + +func (d dynamoDBServiceFixture) LocalStackID() string { + return "dynamodb" +} + +func (d dynamoDBServiceFixture) Setup(service *awsTestService) error { + const maxDynamoDBTableNameLength = uint(255) + const desiredDynamoDBTableNameSuffixLength = uint(12) + prefix := fmt.Sprintf("opentofu-test-%s-", strings.ToLower(dynamoDBNameReplaceRe.ReplaceAllString(service.t.Name(), ""))) + tableName := testrandom.IDPrefix( + prefix, + min(maxDynamoDBTableNameLength-uint(len(prefix)), desiredDynamoDBTableNameSuffixLength), + testrandom.CharacterRangeAlphaLower, + ) + dynamoDBClient := dynamodb.NewFromConfig(service.Config()) + + // TODO replace with variable if the config comes from env. + const needsTableDeletion = true + + service.t.Logf("🌟 Creating DynamoDB table %s...", tableName) + + _, err := dynamoDBClient.CreateTable(service.ctx, &dynamodb.CreateTableInput{ + AttributeDefinitions: []types.AttributeDefinition{ + { + AttributeName: aws.String("LockID"), + AttributeType: types.ScalarAttributeTypeS, + }, + }, + KeySchema: []types.KeySchemaElement{{ + AttributeName: aws.String("LockID"), + KeyType: types.KeyTypeHash, + }}, + TableName: &tableName, + BillingMode: types.BillingModePayPerRequest, + }) + if err != nil { + return fmt.Errorf("failed to create the DynamoDB table: %w", err) + } + service.awsDynamoDBParameters = awsDynamoDBParameters{ + dynamoDBEndpoint: service.endpoint, + dynamoDBTable: tableName, + needsTableDeletion: needsTableDeletion, + } + return nil +} + +func (d dynamoDBServiceFixture) Teardown(service *awsTestService) error { + if !service.awsDynamoDBParameters.needsTableDeletion { + return nil + } + cleanupCtx, cancel := context.WithTimeout(context.Background(), cleanupTimeout) + defer cancel() + dynamoDBClient := dynamodb.NewFromConfig(service.Config()) + service.t.Logf("🗑️ Deleting DynamoDB table %s...", service.dynamoDBTable) + if _, err := dynamoDBClient.DeleteTable(cleanupCtx, &dynamodb.DeleteTableInput{ + TableName: &service.dynamoDBTable, + }); err != nil { + return fmt.Errorf("failed to clean up DynamoDB table %s: %w", service.dynamoDBTable, err) + } + return nil +} + +type awsDynamoDBParameters struct { + dynamoDBEndpoint string + dynamoDBTable string + needsTableDeletion bool +} + +func (a awsDynamoDBParameters) DynamoDBEndpoint() string { + return a.dynamoDBEndpoint +} + +func (a awsDynamoDBParameters) DynamoDBTable() string { + return a.dynamoDBTable +} diff --git a/testaws/aws_dynamodb_test.go b/testaws/aws_dynamodb_test.go new file mode 100644 index 0000000..5ce0447 --- /dev/null +++ b/testaws/aws_dynamodb_test.go @@ -0,0 +1,27 @@ +// Copyright (c) The OpenTofu Authors +// SPDX-License-Identifier: MPL-2.0 +// Copyright (c) 2023 HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package testaws_test + +import ( + "testing" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/dynamodb" + "github.com/opentofu/tofutestutils" + "github.com/opentofu/tofutestutils/testaws" +) + +func testDynamoDBService(t *testing.T, dynamoDBService testaws.AWSDynamoDBTestService) { + ctx := tofutestutils.Context(t) + var dynamoDBClient = dynamodb.NewFromConfig(dynamoDBService.Config()) + t.Logf("🔍 Checking if the DynamoDB table from the AWS test service can be described...") + if _, err := dynamoDBClient.DescribeTable(ctx, &dynamodb.DescribeTableInput{ + TableName: aws.String(dynamoDBService.DynamoDBTable()), + }); err != nil { + t.Fatalf("❌ %v", err) + } + t.Logf("✅ DynamoDB works as intended.") +} diff --git a/testaws/aws_iam.go b/testaws/aws_iam.go new file mode 100644 index 0000000..d213466 --- /dev/null +++ b/testaws/aws_iam.go @@ -0,0 +1,44 @@ +// Copyright (c) The OpenTofu Authors +// SPDX-License-Identifier: MPL-2.0 +// Copyright (c) 2023 HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package testaws + +// AWSIAMTestService is a specialized extension to the AWSTestServiceBase containing IAM-specific functions. +type AWSIAMTestService interface { + AWSTestServiceBase + + // IAMEndpoint returns the endpoint for the IAM service. + IAMEndpoint() string +} + +type iamServiceFixture struct { +} + +func (s iamServiceFixture) Name() string { + return "IAM" +} + +func (s iamServiceFixture) LocalStackID() string { + return "iam" +} + +func (s iamServiceFixture) Setup(service *awsTestService) error { + service.awsIAMParameters = awsIAMParameters{ + iamEndpoint: service.endpoint, + } + return nil +} + +func (s iamServiceFixture) Teardown(_ *awsTestService) error { + return nil +} + +type awsIAMParameters struct { + iamEndpoint string +} + +func (a awsIAMParameters) IAMEndpoint() string { + return a.iamEndpoint +} diff --git a/testaws/aws_iam_test.go b/testaws/aws_iam_test.go new file mode 100644 index 0000000..8955485 --- /dev/null +++ b/testaws/aws_iam_test.go @@ -0,0 +1,25 @@ +// Copyright (c) The OpenTofu Authors +// SPDX-License-Identifier: MPL-2.0 +// Copyright (c) 2023 HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package testaws_test + +import ( + "testing" + + "github.com/aws/aws-sdk-go-v2/service/iam" + "github.com/opentofu/tofutestutils" + "github.com/opentofu/tofutestutils/testaws" +) + +func testIAMService(t *testing.T, iamService testaws.AWSIAMTestService) { + ctx := tofutestutils.Context(t) + iamClient := iam.NewFromConfig(iamService.Config()) + t.Logf("\U0001FAAA Checking if the caller identity can be retrieved...") + roles, err := iamClient.ListRoles(ctx, &iam.ListRolesInput{}) + if err != nil { + t.Fatalf("❌ Failed to get caller identity: %v", err) + } + t.Logf("✅ %d roles returned.", len(roles.Roles)) +} diff --git a/testaws/aws_kms.go b/testaws/aws_kms.go new file mode 100644 index 0000000..daf8d82 --- /dev/null +++ b/testaws/aws_kms.go @@ -0,0 +1,88 @@ +// Copyright (c) The OpenTofu Authors +// SPDX-License-Identifier: MPL-2.0 +// Copyright (c) 2023 HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package testaws + +import ( + "context" + "fmt" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/kms" +) + +const awsKMSMinDeletionWindow = 7 + +// AWSKMSTestService is a specialized extension to the AWSTestServiceBase containing KMS-specific functions. +type AWSKMSTestService interface { + AWSTestServiceBase + + // KMSEndpoint returns the endpoint for the KMS service. + KMSEndpoint() string + + // KMSKeyID returns a key ID suitable for testing. + KMSKeyID() string +} + +type kmsServiceFixture struct { +} + +func (k kmsServiceFixture) Name() string { + return "KMS" +} + +func (k kmsServiceFixture) LocalStackID() string { + return "kms" +} + +func (k kmsServiceFixture) Setup(service *awsTestService) error { + kmsClient := kms.NewFromConfig(service.Config()) + + // TODO replace with variable if the config comes from env. + const needsKeyDeletion = true + + service.t.Logf("🌟 Creating KMS key for testing...") + key, err := kmsClient.CreateKey(service.ctx, &kms.CreateKeyInput{}) + if err != nil { + return fmt.Errorf("failed to create the KMS key: %w", err) + } + service.awsKMSParameters = awsKMSParameters{ + kmsEndpoint: service.endpoint, + kmsKeyID: *key.KeyMetadata.KeyId, + needsKeyDeletion: needsKeyDeletion, + } + return nil +} + +func (k kmsServiceFixture) Teardown(service *awsTestService) error { + if !service.needsKeyDeletion { + return nil + } + cleanupCtx, cancel := context.WithTimeout(context.Background(), cleanupTimeout) + defer cancel() + kmsClient := kms.NewFromConfig(service.Config()) + service.t.Logf("🗑️ Scheduling KMS key %s for deletion...", service.kmsKeyID) + if _, err := kmsClient.ScheduleKeyDeletion(cleanupCtx, &kms.ScheduleKeyDeletionInput{ + KeyId: &service.kmsKeyID, + PendingWindowInDays: aws.Int32(awsKMSMinDeletionWindow), + }); err != nil { + return fmt.Errorf("failed to clean up KMS key ID %s: %w", service.kmsKeyID, err) + } + return nil +} + +type awsKMSParameters struct { + kmsEndpoint string + kmsKeyID string + needsKeyDeletion bool +} + +func (a awsKMSParameters) KMSEndpoint() string { + return a.kmsEndpoint +} + +func (a awsKMSParameters) KMSKeyID() string { + return a.kmsKeyID +} diff --git a/testaws/aws_kms_test.go b/testaws/aws_kms_test.go new file mode 100644 index 0000000..8b4ca22 --- /dev/null +++ b/testaws/aws_kms_test.go @@ -0,0 +1,27 @@ +// Copyright (c) The OpenTofu Authors +// SPDX-License-Identifier: MPL-2.0 +// Copyright (c) 2023 HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package testaws_test + +import ( + "testing" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/kms" + "github.com/opentofu/tofutestutils" + "github.com/opentofu/tofutestutils/testaws" +) + +func testKMSService(t *testing.T, kmsService testaws.AWSKMSTestService) { + ctx := tofutestutils.Context(t) + kmsClient := kms.NewFromConfig(kmsService.Config()) + t.Logf("🔍 Checking if the KMS key from the AWS test service can be described...") + if _, err := kmsClient.DescribeKey(ctx, &kms.DescribeKeyInput{ + KeyId: aws.String(kmsService.KMSKeyID()), + }); err != nil { + t.Fatalf("❌ %v", err) + } + t.Logf("✅ KMS works as intended.") +} diff --git a/testaws/aws_s3.go b/testaws/aws_s3.go new file mode 100644 index 0000000..5bc4f1e --- /dev/null +++ b/testaws/aws_s3.go @@ -0,0 +1,175 @@ +// Copyright (c) The OpenTofu Authors +// SPDX-License-Identifier: MPL-2.0 +// Copyright (c) 2023 HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package testaws + +import ( + "context" + "errors" + "fmt" + "regexp" + "strings" + + "github.com/aws/aws-sdk-go-v2/service/s3" + "github.com/aws/aws-sdk-go-v2/service/s3/types" + "github.com/opentofu/tofutestutils/testrandom" +) + +const maxS3DeletionObjects = 1000 + +// AWSS3TestService is a specialized extension to the AWSTestServiceBase containing S3-specific functions. +type AWSS3TestService interface { + AWSTestServiceBase + + // S3Endpoint returns the endpoint for the S3 service. + S3Endpoint() string + + // S3Bucket returns an S3 bucket suitable for testing. + S3Bucket() string + + // S3UsePathStyle returns true if the client should use a path-style access. + S3UsePathStyle() bool +} + +var s3ReplaceRe = regexp.MustCompile(`[^a-zA-Z0-9._-]`) + +type s3ServiceFixture struct { +} + +func (s s3ServiceFixture) Name() string { + return "S3" +} + +func (s s3ServiceFixture) LocalStackID() string { + return "s3" +} + +func (s s3ServiceFixture) Setup(service *awsTestService) error { + const maxS3BucketNameLength = uint(63) + const desiredS3BucketNameSuffixLength = uint(12) + prefix := fmt.Sprintf( + "opentofu-test-%s-", + strings.ToLower(s3ReplaceRe.ReplaceAllString(service.t.Name(), "")), + ) + bucketName := testrandom.IDPrefix( + prefix, + min(maxS3BucketNameLength-uint(len(prefix)), desiredS3BucketNameSuffixLength), + testrandom.CharacterRangeAlphaNumericLower, + ) + + // TODO replace with variable if the config comes from env. + const pathStyle = true + + s3Connection := s3.NewFromConfig(service.Config(), func(options *s3.Options) { + options.UsePathStyle = pathStyle + }) + service.t.Logf("🌟 Creating S3 bucket %s for testing...", bucketName) + _, err := s3Connection.CreateBucket(service.ctx, &s3.CreateBucketInput{ + Bucket: &bucketName, + }) + bucketNeedsDeletion := true + if err != nil { + var bucketAlreadyExistsErr *types.BucketAlreadyExists + if !errors.As(err, &bucketAlreadyExistsErr) { + return fmt.Errorf("failed to create test bucket %s: %w", bucketName, err) + } + bucketNeedsDeletion = false + } + service.awsS3Parameters = awsS3Parameters{ + s3Endpoint: service.endpoint, + s3Bucket: bucketName, + s3PathStyle: pathStyle, + bucketNeedsDeletion: bucketNeedsDeletion, + } + return nil +} + +func (s s3ServiceFixture) Teardown(service *awsTestService) error { + if !service.bucketNeedsDeletion { + return nil + } + cleanupContext, cancel := context.WithTimeout(context.Background(), cleanupTimeout) + defer cancel() + + s3Connection := s3.NewFromConfig(service.Config(), func(options *s3.Options) { + options.UsePathStyle = service.s3PathStyle + }) + + service.t.Logf("🗑️ Deleting all objects from S3 bucket %s...", service.s3Bucket) + err := s.emptyS3Bucket(cleanupContext, service, s3Connection) + if err != nil { + return err + } + + service.t.Logf("🗑️ Deleting S3 bucket %s...", service.s3Bucket) + if _, err := s3Connection.DeleteBucket(service.ctx, &s3.DeleteBucketInput{ + Bucket: &service.s3Bucket, + }); err != nil { + return fmt.Errorf("failed to delete test bucket %s: %w", service.s3Bucket, err) + } + return nil +} + +func (s s3ServiceFixture) emptyS3Bucket(cleanupContext context.Context, service *awsTestService, s3Connection *s3.Client) error { + var continuationToken *string + for { + listObjectsResult, err := s3Connection.ListObjectsV2(cleanupContext, &s3.ListObjectsV2Input{ + Bucket: &service.s3Bucket, + ContinuationToken: continuationToken, + }) + if err != nil { + return fmt.Errorf("failed to clean up test bucket as the list objects call failed %s: %w", service.s3Bucket, err) + } + var objects []types.ObjectIdentifier + deleteObjects := func() error { + _, err := s3Connection.DeleteObjects(cleanupContext, &s3.DeleteObjectsInput{ + Bucket: &service.s3Bucket, + Delete: &types.Delete{ + Objects: objects, + }, + }) + return err + } + for _, object := range listObjectsResult.Contents { + objects = append(objects, types.ObjectIdentifier{ + Key: object.Key, + }) + if len(objects) == maxS3DeletionObjects { + if err := deleteObjects(); err != nil { + return fmt.Errorf("failed to clean up test bucket %s: %w", service.s3Bucket, err) + } + } + } + if len(objects) > 0 { + if err := deleteObjects(); err != nil { + return fmt.Errorf("failed to clean up test bucket %s: %w", service.s3Bucket, err) + } + } + continuationToken = listObjectsResult.NextContinuationToken + if continuationToken == nil { + break + } + } + return nil +} + +type awsS3Parameters struct { + s3Endpoint string + s3Bucket string + s3PathStyle bool + bucketNeedsDeletion bool +} + +func (a awsS3Parameters) S3Endpoint() string { + return a.s3Endpoint +} + +func (a awsS3Parameters) S3UsePathStyle() bool { + return a.s3PathStyle +} + +func (a awsS3Parameters) S3Bucket() string { + return a.s3Bucket +} diff --git a/testaws/aws_s3_test.go b/testaws/aws_s3_test.go new file mode 100644 index 0000000..86356f4 --- /dev/null +++ b/testaws/aws_s3_test.go @@ -0,0 +1,73 @@ +// Copyright (c) The OpenTofu Authors +// SPDX-License-Identifier: MPL-2.0 +// Copyright (c) 2023 HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package testaws_test + +import ( + "bytes" + "io" + "testing" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/s3" + "github.com/opentofu/tofutestutils" + "github.com/opentofu/tofutestutils/testaws" +) + +const s3TestFileName = "test.txt" +const s3TestFileContents = "Hello OpenTofu!" + +func testS3Service(t *testing.T, s3TestBackend testaws.AWSS3TestService) { + s3Connection := s3.NewFromConfig(s3TestBackend.Config(), func(options *s3.Options) { + options.UsePathStyle = s3TestBackend.S3UsePathStyle() + }) + + t.Run("put", func(t *testing.T) { + testS3Put(t, s3Connection, s3TestBackend) + }) + t.Run("get", func(t *testing.T) { + testS3Get(t, s3Connection, s3TestBackend) + }) +} + +func testS3Get(t *testing.T, s3Connection *s3.Client, s3TestBackend testaws.AWSS3TestService) { + ctx := tofutestutils.Context(t) + t.Logf("📂 Checking if an object can be retrieved...") + getObjectResponse, err := s3Connection.GetObject( + ctx, + &s3.GetObjectInput{ + Bucket: aws.String(s3TestBackend.S3Bucket()), + Key: aws.String(s3TestFileName), + }, + ) + if err != nil { + t.Fatalf("❌ Failed to get object (%v)", err) + } + defer func() { + _ = getObjectResponse.Body.Close() + }() + data, err := io.ReadAll(getObjectResponse.Body) + if err != nil { + t.Fatalf("❌ Failed to read get object response body (%v)", err) + } + if string(data) != s3TestFileContents { + t.Fatalf("❌ Incorrect test data in S3 bucket: %s", data) + } +} + +func testS3Put(t *testing.T, s3Connection *s3.Client, s3TestBackend testaws.AWSS3TestService) { + ctx := tofutestutils.Context(t) + t.Logf("💾 Checking if an object can be stored...") + if _, err := s3Connection.PutObject( + ctx, + &s3.PutObjectInput{ + Key: aws.String(s3TestFileName), + Body: bytes.NewReader([]byte(s3TestFileContents)), + Bucket: aws.String(s3TestBackend.S3Bucket()), + }, + ); err != nil { + t.Fatalf("❌ Failed to put object (%v)", err) + } +} diff --git a/testaws/aws_sts.go b/testaws/aws_sts.go new file mode 100644 index 0000000..ac70066 --- /dev/null +++ b/testaws/aws_sts.go @@ -0,0 +1,44 @@ +// Copyright (c) The OpenTofu Authors +// SPDX-License-Identifier: MPL-2.0 +// Copyright (c) 2023 HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package testaws + +// AWSSTSTestService is a specialized extension to the AWSTestServiceBase containing STS-specific functions. +type AWSSTSTestService interface { + AWSTestServiceBase + + // STSEndpoint returns the endpoint for the STS service. + STSEndpoint() string +} + +type stsServiceFixture struct { +} + +func (s stsServiceFixture) Name() string { + return "STS" +} + +func (s stsServiceFixture) LocalStackID() string { + return "sts" +} + +func (s stsServiceFixture) Setup(service *awsTestService) error { + service.awsSTSParameters = awsSTSParameters{ + stsEndpoint: service.endpoint, + } + return nil +} + +func (s stsServiceFixture) Teardown(_ *awsTestService) error { + return nil +} + +type awsSTSParameters struct { + stsEndpoint string +} + +func (a awsSTSParameters) STSEndpoint() string { + return a.stsEndpoint +} diff --git a/testaws/aws_sts_test.go b/testaws/aws_sts_test.go new file mode 100644 index 0000000..7aec69b --- /dev/null +++ b/testaws/aws_sts_test.go @@ -0,0 +1,25 @@ +// Copyright (c) The OpenTofu Authors +// SPDX-License-Identifier: MPL-2.0 +// Copyright (c) 2023 HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package testaws_test + +import ( + "testing" + + "github.com/aws/aws-sdk-go-v2/service/sts" + "github.com/opentofu/tofutestutils" + "github.com/opentofu/tofutestutils/testaws" +) + +func testSTSService(t *testing.T, stsService testaws.AWSSTSTestService) { + ctx := tofutestutils.Context(t) + stsClient := sts.NewFromConfig(stsService.Config()) + t.Logf("\U0001FAAA Checking if the caller identity can be retrieved...") + output, err := stsClient.GetCallerIdentity(ctx, &sts.GetCallerIdentityInput{}) + if err != nil { + t.Fatalf("❌ Failed to get caller identity: %v", err) + } + t.Logf("✅ Caller identity: %s", *output.UserId) +} diff --git a/testaws/aws_test.go b/testaws/aws_test.go new file mode 100644 index 0000000..92360a6 --- /dev/null +++ b/testaws/aws_test.go @@ -0,0 +1,45 @@ +// Copyright (c) The OpenTofu Authors +// SPDX-License-Identifier: MPL-2.0 +// Copyright (c) 2023 HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package testaws_test + +import ( + "testing" + + "github.com/opentofu/tofutestutils/testaws" +) + +func TestAWS(t *testing.T) { + t.Parallel() + awsService := testaws.New(t) + t.Run("DynamoDB", func(t *testing.T) { + t.Parallel() + testDynamoDBService(t, awsService) + }) + t.Run("IAM", func(t *testing.T) { + t.Parallel() + testIAMService(t, awsService) + }) + t.Run("KMS", func(t *testing.T) { + t.Parallel() + testKMSService(t, awsService) + }) + t.Run("S3", func(t *testing.T) { + t.Parallel() + testS3Service(t, awsService) + }) + t.Run("STS", func(t *testing.T) { + t.Parallel() + testSTSService(t, awsService) + }) +} + +func TestAWSSubtestNameSanitization(t *testing.T) { + t.Parallel() + t.Run("subtest", func(t *testing.T) { + t.Parallel() + _ = testaws.New(t) + }) +} diff --git a/testaws/const.go b/testaws/const.go new file mode 100644 index 0000000..d5dc517 --- /dev/null +++ b/testaws/const.go @@ -0,0 +1,13 @@ +// Copyright (c) The OpenTofu Authors +// SPDX-License-Identifier: MPL-2.0 +// Copyright (c) 2023 HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package testaws + +import ( + "time" +) + +const cleanupTimeout = 5 * time.Minute +const permAll = 0777 diff --git a/testcontext/context.go b/testcontext/context.go index 48eda01..44436f6 100644 --- a/testcontext/context.go +++ b/testcontext/context.go @@ -11,6 +11,19 @@ import ( "time" ) +const minCleanupSafety = time.Second * 30 +const maxCleanupSafety = time.Minute * 5 + +// DefaultContext creates a context with the default safety settings. For usage details, see Context. +func DefaultContext(t *testing.T) context.Context { + return Context( + t, + 4, + minCleanupSafety, + maxCleanupSafety, + ) +} + // Context creates a context with a deadline that allows for enough time to clean up a test before the testing framework // unceremoniously kills the process. The desired time is expressed as a fraction of the time remaining until the hard // timeout, such as 4 for 25%. The minimumCleanupTime and maximumCleanupTime clamp the remaining time.