mirror of
https://github.com/opentffoundation/opentf.git
synced 2025-12-25 01:00:16 -05:00
Backend/S3: Custom Service Endpoint Configuration (#794)
Signed-off-by: Marcin Białoń <mbialon@spacelift.io>
This commit is contained in:
@@ -66,25 +66,56 @@ func (b *Backend) ConfigSchema(context.Context) *configschema.Block {
|
||||
Optional: true,
|
||||
Description: "AWS region of the S3 Bucket and DynamoDB Table (if used).",
|
||||
},
|
||||
"endpoints": {
|
||||
NestedType: &configschema.Object{
|
||||
Nesting: configschema.NestingSingle,
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"s3": {
|
||||
Type: cty.String,
|
||||
Optional: true,
|
||||
Description: "A custom endpoint for the S3 API.",
|
||||
},
|
||||
"iam": {
|
||||
Type: cty.String,
|
||||
Optional: true,
|
||||
Description: "A custom endpoint for the IAM API.",
|
||||
},
|
||||
"sts": {
|
||||
Type: cty.String,
|
||||
Optional: true,
|
||||
Description: "A custom endpoint for the STS API.",
|
||||
},
|
||||
"dynamodb": {
|
||||
Type: cty.String,
|
||||
Optional: true,
|
||||
Description: "A custom endpoint for the DynamoDB API.",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"dynamodb_endpoint": {
|
||||
Type: cty.String,
|
||||
Optional: true,
|
||||
Description: "A custom endpoint for the DynamoDB API",
|
||||
Description: "A custom endpoint for the DynamoDB API. Use `endpoints.dynamodb` instead.",
|
||||
Deprecated: true,
|
||||
},
|
||||
"endpoint": {
|
||||
Type: cty.String,
|
||||
Optional: true,
|
||||
Description: "A custom endpoint for the S3 API",
|
||||
Description: "A custom endpoint for the S3 API. Use `endpoints.s3` instead",
|
||||
Deprecated: true,
|
||||
},
|
||||
"iam_endpoint": {
|
||||
Type: cty.String,
|
||||
Optional: true,
|
||||
Description: "A custom endpoint for the IAM API",
|
||||
Description: "A custom endpoint for the IAM API. Use `endpoints.iam` instead",
|
||||
Deprecated: true,
|
||||
},
|
||||
"sts_endpoint": {
|
||||
Type: cty.String,
|
||||
Optional: true,
|
||||
Description: "A custom endpoint for the STS API",
|
||||
Description: "A custom endpoint for the STS API. Use `endpoints.sts` instead",
|
||||
Deprecated: true,
|
||||
},
|
||||
"sts_region": {
|
||||
Type: cty.String,
|
||||
@@ -568,6 +599,10 @@ func (b *Backend) PrepareConfig(ctx context.Context, obj cty.Value) (cty.Value,
|
||||
}
|
||||
}
|
||||
|
||||
for _, endpoint := range customEndpoints {
|
||||
endpoint.Validate(obj, &diags)
|
||||
}
|
||||
|
||||
return obj, diags
|
||||
}
|
||||
|
||||
@@ -651,13 +686,13 @@ func (b *Backend) Configure(ctx context.Context, obj cty.Value) tfdiags.Diagnost
|
||||
CallerDocumentationURL: "https://opentofu.org/docs/language/settings/backends/s3",
|
||||
CallerName: "S3 Backend",
|
||||
SuppressDebugLog: logging.IsDebugOrHigher(),
|
||||
IamEndpoint: stringAttrDefaultEnvVar(obj, "iam_endpoint", "AWS_IAM_ENDPOINT"),
|
||||
IamEndpoint: customEndpoints["iam"].String(obj),
|
||||
MaxRetries: intAttrDefault(obj, "max_retries", 5),
|
||||
Profile: stringAttr(obj, "profile"),
|
||||
Region: stringAttr(obj, "region"),
|
||||
SecretKey: stringAttr(obj, "secret_key"),
|
||||
SkipCredsValidation: boolAttr(obj, "skip_credentials_validation"),
|
||||
StsEndpoint: stringAttrDefaultEnvVar(obj, "sts_endpoint", "AWS_STS_ENDPOINT"),
|
||||
StsEndpoint: customEndpoints["sts"].String(obj),
|
||||
StsRegion: stringAttr(obj, "sts_region"),
|
||||
Token: stringAttr(obj, "token"),
|
||||
HTTPProxy: stringAttrDefaultEnvVar(obj, "http_proxy", "HTTP_PROXY", "HTTPS_PROXY"),
|
||||
@@ -779,7 +814,7 @@ func verifyAllowedAccountID(ctx context.Context, awsConfig aws.Config, cfg *awsb
|
||||
|
||||
func getDynamoDBConfig(obj cty.Value) func(options *dynamodb.Options) {
|
||||
return func(options *dynamodb.Options) {
|
||||
if v, ok := stringAttrDefaultEnvVarOk(obj, "dynamodb_endpoint", "AWS_DYNAMODB_ENDPOINT", "AWS_ENDPOINT_URL_DYNAMODB"); ok {
|
||||
if v, ok := customEndpoints["dynamodb"].StringOk(obj); ok {
|
||||
options.BaseEndpoint = aws.String(v)
|
||||
}
|
||||
}
|
||||
@@ -787,7 +822,7 @@ func getDynamoDBConfig(obj cty.Value) func(options *dynamodb.Options) {
|
||||
|
||||
func getS3Config(obj cty.Value) func(options *s3.Options) {
|
||||
return func(options *s3.Options) {
|
||||
if v, ok := stringAttrDefaultEnvVarOk(obj, "endpoint", "AWS_S3_ENDPOINT", "AWS_ENDPOINT_URL_S3"); ok {
|
||||
if v, ok := customEndpoints["s3"].StringOk(obj); ok {
|
||||
options.BaseEndpoint = aws.String(v)
|
||||
}
|
||||
if v, ok := boolAttrOk(obj, "force_path_style"); ok {
|
||||
@@ -1021,6 +1056,15 @@ func stringMapAttrOk(obj cty.Value, name string) (map[string]string, bool) {
|
||||
return stringMapValueOk(obj.GetAttr(name))
|
||||
}
|
||||
|
||||
func customEndpointAttrDefaultEnvVarOk(obj cty.Value, endpointsKey, deprecatedKey string, envvars ...string) (string, bool) {
|
||||
if val := obj.GetAttr("endpoints"); !val.IsNull() {
|
||||
if v, ok := stringAttrDefaultEnvVarOk(val, endpointsKey, envvars...); ok {
|
||||
return v, true
|
||||
}
|
||||
}
|
||||
return stringAttrDefaultEnvVarOk(obj, deprecatedKey, envvars...)
|
||||
}
|
||||
|
||||
func pathString(path cty.Path) string {
|
||||
var buf strings.Builder
|
||||
for i, step := range path {
|
||||
@@ -1098,3 +1142,78 @@ const encryptionKeyConflictEnvVarError = `Only one of "kms_key_id" and the envir
|
||||
The "kms_key_id" is used for encryption with KMS-Managed Keys (SSE-KMS)
|
||||
while "AWS_SSE_CUSTOMER_KEY" is used for encryption with customer-managed keys (SSE-C).
|
||||
Please choose one or the other.`
|
||||
|
||||
type customEndpoint struct {
|
||||
Paths []cty.Path
|
||||
EnvVars []string
|
||||
}
|
||||
|
||||
func (e customEndpoint) Validate(obj cty.Value, diags *tfdiags.Diagnostics) {
|
||||
validateAttributesConflict(e.Paths...)(obj, cty.Path{}, diags)
|
||||
}
|
||||
|
||||
func (e customEndpoint) String(obj cty.Value) string {
|
||||
v, _ := e.StringOk(obj)
|
||||
return v
|
||||
}
|
||||
|
||||
func (e customEndpoint) StringOk(obj cty.Value) (string, bool) {
|
||||
for _, path := range e.Paths {
|
||||
val, err := path.Apply(obj)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if s, ok := stringValueOk(val); ok {
|
||||
return s, true
|
||||
}
|
||||
}
|
||||
for _, envVar := range e.EnvVars {
|
||||
if v := os.Getenv(envVar); v != "" {
|
||||
return v, true
|
||||
}
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
var customEndpoints = map[string]customEndpoint{
|
||||
"s3": {
|
||||
Paths: []cty.Path{
|
||||
cty.GetAttrPath("endpoints").GetAttr("s3"),
|
||||
cty.GetAttrPath("endpoint"),
|
||||
},
|
||||
EnvVars: []string{
|
||||
"AWS_ENDPOINT_URL_S3",
|
||||
"AWS_S3_ENDPOINT",
|
||||
},
|
||||
},
|
||||
"iam": {
|
||||
Paths: []cty.Path{
|
||||
cty.GetAttrPath("endpoints").GetAttr("iam"),
|
||||
cty.GetAttrPath("iam_endpoint"),
|
||||
},
|
||||
EnvVars: []string{
|
||||
"AWS_ENDPOINT_URL_IAM",
|
||||
"AWS_IAM_ENDPOINT",
|
||||
},
|
||||
},
|
||||
"sts": {
|
||||
Paths: []cty.Path{
|
||||
cty.GetAttrPath("endpoints").GetAttr("sts"),
|
||||
cty.GetAttrPath("sts_endpoint"),
|
||||
},
|
||||
EnvVars: []string{
|
||||
"AWS_ENDPOINT_URL_STS",
|
||||
"AWS_STS_ENDPOINT",
|
||||
},
|
||||
},
|
||||
"dynamodb": {
|
||||
Paths: []cty.Path{
|
||||
cty.GetAttrPath("endpoints").GetAttr("dynamodb"),
|
||||
cty.GetAttrPath("dynamodb_endpoint"),
|
||||
},
|
||||
EnvVars: []string{
|
||||
"AWS_ENDPOINT_URL_DYNAMODB",
|
||||
"AWS_DYNAMODB_ENDPOINT",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -738,6 +738,54 @@ func TestBackendConfig_PrepareConfigValidation(t *testing.T) {
|
||||
}),
|
||||
expectedErr: `Invalid retry mode: Valid values are "standard" and "adaptive".`,
|
||||
},
|
||||
"s3 endpoint conflict": {
|
||||
config: cty.ObjectVal(map[string]cty.Value{
|
||||
"bucket": cty.StringVal("test"),
|
||||
"key": cty.StringVal("test"),
|
||||
"region": cty.StringVal("us-west-2"),
|
||||
"endpoint": cty.StringVal("x1"),
|
||||
"endpoints": cty.ObjectVal(map[string]cty.Value{
|
||||
"s3": cty.StringVal("x2"),
|
||||
}),
|
||||
}),
|
||||
expectedErr: `Invalid Attribute Combination: Only one of endpoints.s3, endpoint can be set.`,
|
||||
},
|
||||
"iam endpoint conflict": {
|
||||
config: cty.ObjectVal(map[string]cty.Value{
|
||||
"bucket": cty.StringVal("test"),
|
||||
"key": cty.StringVal("test"),
|
||||
"region": cty.StringVal("us-west-2"),
|
||||
"iam_endpoint": cty.StringVal("x1"),
|
||||
"endpoints": cty.ObjectVal(map[string]cty.Value{
|
||||
"iam": cty.StringVal("x2"),
|
||||
}),
|
||||
}),
|
||||
expectedErr: `Invalid Attribute Combination: Only one of endpoints.iam, iam_endpoint can be set.`,
|
||||
},
|
||||
"sts endpoint conflict": {
|
||||
config: cty.ObjectVal(map[string]cty.Value{
|
||||
"bucket": cty.StringVal("test"),
|
||||
"key": cty.StringVal("test"),
|
||||
"region": cty.StringVal("us-west-2"),
|
||||
"sts_endpoint": cty.StringVal("x1"),
|
||||
"endpoints": cty.ObjectVal(map[string]cty.Value{
|
||||
"sts": cty.StringVal("x2"),
|
||||
}),
|
||||
}),
|
||||
expectedErr: `Invalid Attribute Combination: Only one of endpoints.sts, sts_endpoint can be set.`,
|
||||
},
|
||||
"dynamodb endpoint conflict": {
|
||||
config: cty.ObjectVal(map[string]cty.Value{
|
||||
"bucket": cty.StringVal("test"),
|
||||
"key": cty.StringVal("test"),
|
||||
"region": cty.StringVal("us-west-2"),
|
||||
"dynamodb_endpoint": cty.StringVal("x1"),
|
||||
"endpoints": cty.ObjectVal(map[string]cty.Value{
|
||||
"dynamodb": cty.StringVal("x2"),
|
||||
}),
|
||||
}),
|
||||
expectedErr: `Invalid Attribute Combination: Only one of endpoints.dynamodb, dynamodb_endpoint can be set.`,
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range cases {
|
||||
|
||||
@@ -154,10 +154,27 @@ func aliasIdFromARNResource(s string) string {
|
||||
type objectValidator func(obj cty.Value, objPath cty.Path, diags *tfdiags.Diagnostics)
|
||||
|
||||
func validateAttributesConflict(paths ...cty.Path) objectValidator {
|
||||
applyPath := func(obj cty.Value, path cty.Path) (cty.Value, error) {
|
||||
if len(path) == 0 {
|
||||
return cty.NilVal, nil
|
||||
}
|
||||
for _, step := range path {
|
||||
val, err := step.Apply(obj)
|
||||
if err != nil {
|
||||
return cty.NilVal, err
|
||||
}
|
||||
if val.IsNull() {
|
||||
return cty.NilVal, nil
|
||||
}
|
||||
obj = val
|
||||
}
|
||||
return obj, nil
|
||||
}
|
||||
|
||||
return func(obj cty.Value, objPath cty.Path, diags *tfdiags.Diagnostics) {
|
||||
found := false
|
||||
for _, path := range paths {
|
||||
val, err := path.Apply(obj)
|
||||
val, err := applyPath(obj, path)
|
||||
if err != nil {
|
||||
*diags = diags.Append(attributeErrDiag(
|
||||
"Invalid Path for Schema",
|
||||
|
||||
@@ -153,6 +153,7 @@ func Test_validateAttributesConflict(t *testing.T) {
|
||||
objValues: map[string]cty.Value{
|
||||
"attr1": cty.StringVal("value1"),
|
||||
"attr2": cty.StringVal("value2"),
|
||||
"attr3": cty.StringVal("value3"),
|
||||
},
|
||||
expectErr: true,
|
||||
},
|
||||
@@ -160,14 +161,43 @@ func Test_validateAttributesConflict(t *testing.T) {
|
||||
name: "No Conflict",
|
||||
paths: []cty.Path{
|
||||
{cty.GetAttrStep{Name: "attr1"}},
|
||||
{cty.GetAttrStep{Name: "attr3"}},
|
||||
{cty.GetAttrStep{Name: "attr2"}},
|
||||
},
|
||||
objValues: map[string]cty.Value{
|
||||
"attr1": cty.StringVal("value1"),
|
||||
"attr2": cty.NilVal,
|
||||
"attr3": cty.StringVal("value3"),
|
||||
},
|
||||
expectErr: false,
|
||||
},
|
||||
{
|
||||
name: "Nested: Conflict Found",
|
||||
paths: []cty.Path{
|
||||
(cty.Path{cty.GetAttrStep{Name: "nested"}}).GetAttr("attr1"),
|
||||
{cty.GetAttrStep{Name: "attr2"}},
|
||||
},
|
||||
objValues: map[string]cty.Value{
|
||||
"nested": cty.ObjectVal(map[string]cty.Value{
|
||||
"attr1": cty.StringVal("value1"),
|
||||
}),
|
||||
"attr2": cty.StringVal("value2"),
|
||||
"attr3": cty.StringVal("value3"),
|
||||
},
|
||||
expectErr: true,
|
||||
},
|
||||
{
|
||||
name: "Nested: No Conflict",
|
||||
paths: []cty.Path{
|
||||
(cty.Path{cty.GetAttrStep{Name: "nested"}}).GetAttr("attr1"),
|
||||
{cty.GetAttrStep{Name: "attr3"}},
|
||||
},
|
||||
objValues: map[string]cty.Value{
|
||||
"nested": cty.NilVal,
|
||||
"attr1": cty.StringVal("value1"),
|
||||
"attr3": cty.StringVal("value3"),
|
||||
},
|
||||
expectErr: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
@@ -182,7 +212,11 @@ func Test_validateAttributesConflict(t *testing.T) {
|
||||
|
||||
if test.expectErr {
|
||||
if !diags.HasErrors() {
|
||||
t.Errorf("Expected validation errors, but got none.")
|
||||
t.Error("Expected validation errors, but got none.")
|
||||
}
|
||||
} else {
|
||||
if diags.HasErrors() {
|
||||
t.Errorf("Expected no errors, but got %s.", diags.Err())
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user