1package manager
2
3import (
4	"context"
5	"errors"
6	"fmt"
7	"net/http"
8
9	"github.com/aws/aws-sdk-go-v2/aws"
10	"github.com/aws/aws-sdk-go-v2/service/s3"
11	"github.com/aws/smithy-go/middleware"
12	smithyhttp "github.com/aws/smithy-go/transport/http"
13)
14
15const bucketRegionHeader = "X-Amz-Bucket-Region"
16
17// GetBucketRegion will attempt to get the region for a bucket using the
18// client's configured region to determine which AWS partition to perform the query on.
19//
20// The request will not be signed, and will not use your AWS credentials.
21//
22// A BucketNotFound error will be returned if the bucket does not exist in the
23// AWS partition the client region belongs to.
24//
25// For example to get the region of a bucket which exists in "eu-central-1"
26// you could provide a region hint of "us-west-2".
27//
28//	cfg, err := config.LoadDefaultConfig(context.TODO())
29//	if err != nil {
30//		log.Println("error:", err)
31//		return
32//	}
33//
34//	bucket := "my-bucket"
35//	region, err := manager.GetBucketRegion(ctx, s3.NewFromConfig(cfg), bucket)
36//	if err != nil {
37//		var bnf manager.BucketNotFound
38//		if errors.As(err, &bnf) {
39//			fmt.Fprintf(os.Stderr, "unable to find bucket %s's region\n", bucket)
40//		}
41//		return
42//	}
43//	fmt.Printf("Bucket %s is in %s region\n", bucket, region)
44//
45// By default the request will be made to the Amazon S3 endpoint using the virtual-hosted-style addressing.
46//
47//	bucketname.s3.us-west-2.amazonaws.com/
48//
49// To configure the GetBucketRegion to make a request via the Amazon
50// S3 FIPS endpoints directly when a FIPS region name is not available, (e.g.
51// fips-us-gov-west-1) set the EndpointResolver on the config or client the
52// utility is called with.
53//
54//	cfg, err := config.LoadDefaultConfig(context.TODO(),
55//		config.WithEndpointResolver(
56//			aws.EndpointResolverFunc(func(service, region string) (aws.Endpoint, error) {
57//				return aws.Endpoint{URL: "https://s3-fips.us-west-2.amazonaws.com"}, nil
58//			}),
59//	)
60//	if err != nil {
61//		panic(err)
62//	}
63func GetBucketRegion(ctx context.Context, client HeadBucketAPIClient, bucket string, optFns ...func(*s3.Options)) (string, error) {
64	var captureBucketRegion deserializeBucketRegion
65
66	clientOptionFns := make([]func(*s3.Options), len(optFns)+1)
67	clientOptionFns[0] = func(options *s3.Options) {
68		options.Credentials = aws.AnonymousCredentials{}
69		options.APIOptions = append(options.APIOptions, captureBucketRegion.RegisterMiddleware)
70	}
71	copy(clientOptionFns[1:], optFns)
72
73	_, err := client.HeadBucket(ctx, &s3.HeadBucketInput{
74		Bucket: aws.String(bucket),
75	}, clientOptionFns...)
76	if len(captureBucketRegion.BucketRegion) == 0 && err != nil {
77		var httpStatusErr interface {
78			HTTPStatusCode() int
79		}
80		if !errors.As(err, &httpStatusErr) {
81			return "", err
82		}
83
84		if httpStatusErr.HTTPStatusCode() == http.StatusNotFound {
85			return "", &bucketNotFound{}
86		}
87
88		return "", err
89	}
90
91	return captureBucketRegion.BucketRegion, nil
92}
93
94type deserializeBucketRegion struct {
95	BucketRegion string
96}
97
98func (d *deserializeBucketRegion) RegisterMiddleware(stack *middleware.Stack) error {
99	return stack.Deserialize.Add(d, middleware.After)
100}
101
102func (d *deserializeBucketRegion) ID() string {
103	return "DeserializeBucketRegion"
104}
105
106func (d *deserializeBucketRegion) HandleDeserialize(ctx context.Context, in middleware.DeserializeInput, next middleware.DeserializeHandler) (
107	out middleware.DeserializeOutput, metadata middleware.Metadata, err error,
108) {
109	out, metadata, err = next.HandleDeserialize(ctx, in)
110	if err != nil {
111		return out, metadata, err
112	}
113
114	resp, ok := out.RawResponse.(*smithyhttp.Response)
115	if !ok {
116		return out, metadata, fmt.Errorf("unknown transport type %T", out.RawResponse)
117	}
118
119	d.BucketRegion = resp.Header.Get(bucketRegionHeader)
120
121	return out, metadata, err
122}
123
124// BucketNotFound indicates the bucket was not found in the partition when calling GetBucketRegion.
125type BucketNotFound interface {
126	error
127
128	isBucketNotFound()
129}
130
131type bucketNotFound struct{}
132
133func (b *bucketNotFound) Error() string {
134	return "bucket not found"
135}
136
137func (b *bucketNotFound) isBucketNotFound() {}
138
139var _ BucketNotFound = (*bucketNotFound)(nil)
140