Browse Source

Migrate from aws-sdk-go to aws-sdk-go-v2

DarthSim 1 year ago
parent
commit
aa1b7b6973
6 changed files with 311 additions and 169 deletions
  1. 31 19
      config/loadenv/aws.go
  2. 26 4
      go.mod
  3. 69 10
      go.sum
  4. 64 51
      metrics/cloudwatch/cloudwatch.go
  5. 114 77
      transport/s3/s3.go
  6. 7 8
      transport/s3/s3_test.go

+ 31 - 19
config/loadenv/aws.go

@@ -1,15 +1,17 @@
 package loadenv
 
 import (
+	"context"
 	"fmt"
 	"os"
 	"strings"
+	"time"
 
 	"github.com/DarthSim/godotenv"
-	"github.com/aws/aws-sdk-go/aws"
-	"github.com/aws/aws-sdk-go/aws/session"
-	"github.com/aws/aws-sdk-go/service/secretsmanager"
-	"github.com/aws/aws-sdk-go/service/ssm"
+	"github.com/aws/aws-sdk-go-v2/aws"
+	awsConfig "github.com/aws/aws-sdk-go-v2/config"
+	"github.com/aws/aws-sdk-go-v2/service/secretsmanager"
+	"github.com/aws/aws-sdk-go-v2/service/ssm"
 )
 
 func loadAWSSecret() error {
@@ -22,18 +24,20 @@ func loadAWSSecret() error {
 		return nil
 	}
 
-	sess, err := session.NewSession()
+	conf, err := awsConfig.LoadDefaultConfig(context.Background())
 	if err != nil {
-		return fmt.Errorf("Can't create AWS Secrets Manager session: %s", err)
+		return fmt.Errorf("can't load AWS Secrets Manager config: %s", err)
 	}
 
-	conf := aws.NewConfig()
-
 	if len(secretRegion) != 0 {
-		conf.Region = aws.String(secretRegion)
+		conf.Region = secretRegion
+	}
+
+	if len(conf.Region) == 0 {
+		conf.Region = "us-west-1"
 	}
 
-	svc := secretsmanager.New(sess, conf)
+	client := secretsmanager.NewFromConfig(conf)
 
 	input := secretsmanager.GetSecretValueInput{SecretId: aws.String(secretID)}
 	if len(secretVersionID) > 0 {
@@ -42,7 +46,10 @@ func loadAWSSecret() error {
 		input.VersionStage = aws.String(secretVersionStage)
 	}
 
-	output, err := svc.GetSecretValue(&input)
+	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
+	defer cancel()
+
+	output, err := client.GetSecretValue(ctx, &input)
 	if err != nil {
 		return fmt.Errorf("Can't retrieve config from AWS Secrets Manager: %s", err)
 	}
@@ -73,18 +80,23 @@ func loadAWSSystemManagerParams() error {
 		return nil
 	}
 
-	sess, err := session.NewSession()
+	conf, err := awsConfig.LoadDefaultConfig(context.Background())
 	if err != nil {
-		return fmt.Errorf("Can't create AWS SSM session: %s", err)
+		return fmt.Errorf("can't load AWS SSM config: %s", err)
 	}
 
-	conf := aws.NewConfig()
-
 	if len(paramsRegion) != 0 {
-		conf.Region = aws.String(paramsRegion)
+		conf.Region = paramsRegion
+	}
+
+	if len(conf.Region) == 0 {
+		conf.Region = "us-west-1"
 	}
 
-	svc := ssm.New(sess, conf)
+	client := ssm.NewFromConfig(conf)
+
+	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
+	defer cancel()
 
 	var nextToken *string
 
@@ -95,13 +107,13 @@ func loadAWSSystemManagerParams() error {
 			NextToken:      nextToken,
 		}
 
-		output, err := svc.GetParametersByPath(&input)
+		output, err := client.GetParametersByPath(ctx, &input)
 		if err != nil {
 			return fmt.Errorf("Can't retrieve parameters from AWS SSM: %s", err)
 		}
 
 		for _, p := range output.Parameters {
-			if p == nil || p.Name == nil || p.Value == nil {
+			if p.Name == nil || p.Value == nil {
 				continue
 			}
 

+ 26 - 4
go.mod

@@ -13,14 +13,24 @@ require (
 	github.com/DarthSim/godotenv v1.3.1
 	github.com/DataDog/datadog-go/v5 v5.5.0
 	github.com/airbrake/gobrake/v5 v5.6.1
-	github.com/aws/aws-sdk-go v1.50.23
+	github.com/aws/amazon-s3-encryption-client-go/v3 v3.0.0
+	github.com/aws/aws-sdk-go-v2 v1.25.2
+	github.com/aws/aws-sdk-go-v2/config v1.27.6
+	github.com/aws/aws-sdk-go-v2/credentials v1.17.6
+	github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.16.8
+	github.com/aws/aws-sdk-go-v2/service/cloudwatch v1.36.1
+	github.com/aws/aws-sdk-go-v2/service/kms v1.29.1
+	github.com/aws/aws-sdk-go-v2/service/s3 v1.51.3
+	github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.28.1
+	github.com/aws/aws-sdk-go-v2/service/ssm v1.49.1
+	github.com/aws/aws-sdk-go-v2/service/sts v1.28.3
 	github.com/benesch/cgosymbolizer v0.0.0-20190515212042-bec6fe6e597b
 	github.com/bugsnag/bugsnag-go/v2 v2.2.1
 	github.com/felixge/httpsnoop v1.0.4
 	github.com/fsouza/fake-gcs-server v1.42.2
 	github.com/getsentry/sentry-go v0.27.0
 	github.com/honeybadger-io/honeybadger-go v0.6.0
-	github.com/johannesboyne/gofakes3 v0.0.0-20221128113635-c2f5cc6b5294
+	github.com/johannesboyne/gofakes3 v0.0.0-20240217095638-c55a48f17be6
 	github.com/matoous/go-nanoid/v2 v2.0.0
 	github.com/ncw/swift/v2 v2.0.2
 	github.com/newrelic/go-agent/v3 v3.30.0
@@ -72,6 +82,20 @@ require (
 	github.com/DataDog/go-tuf v1.0.4-0.5.2-debug // indirect
 	github.com/DataDog/sketches-go v1.4.4 // indirect
 	github.com/Microsoft/go-winio v0.6.1 // indirect
+	github.com/aws/aws-sdk-go v1.50.10 // indirect
+	github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.1 // indirect
+	github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.15.2 // indirect
+	github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.2 // indirect
+	github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.2 // indirect
+	github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 // indirect
+	github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.2 // indirect
+	github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.1 // indirect
+	github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.4 // indirect
+	github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.4 // indirect
+	github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.2 // indirect
+	github.com/aws/aws-sdk-go-v2/service/sso v1.20.1 // indirect
+	github.com/aws/aws-sdk-go-v2/service/ssooidc v1.23.1 // indirect
+	github.com/aws/smithy-go v1.20.1 // indirect
 	github.com/beorn7/perks v1.0.1 // indirect
 	github.com/brunoscheufler/aws-ecs-metadata-go v0.0.0-20221221133751-67e37ae746cd // indirect
 	github.com/bugsnag/panicwrap v1.3.4 // indirect
@@ -167,5 +191,3 @@ require (
 	sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect
 	sigs.k8s.io/yaml v1.4.0 // indirect
 )
-
-replace github.com/johannesboyne/gofakes3 => github.com/DarthSim/gofakes3 v0.0.0-20230502153341-3fc66d2bc272

+ 69 - 10
go.sum

@@ -30,8 +30,6 @@ github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2/go.mod h1:wP83
 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
 github.com/DarthSim/godotenv v1.3.1 h1:NMWdswlRx2M9uPY4Ux8p/Q/rDs7A97OG89fECiQ/Tz0=
 github.com/DarthSim/godotenv v1.3.1/go.mod h1:B3ySe1HYTUFFR6+TPyHyxPWjUdh48il0Blebg9p1cCc=
-github.com/DarthSim/gofakes3 v0.0.0-20230502153341-3fc66d2bc272 h1:Gj21neabaU3DEwwVPG6/Vn0GSuDQ1n2m2z1qV33SCI0=
-github.com/DarthSim/gofakes3 v0.0.0-20230502153341-3fc66d2bc272/go.mod h1:Cnosl0cRZIfKjTMuH49sQog2LeNsU5Hf4WnPIDWIDV0=
 github.com/DataDog/appsec-internal-go v1.4.2 h1:rLOp0mSzJ7L7Nn3jAdWbgvs+1HK25H0DN4XYVDJu72s=
 github.com/DataDog/appsec-internal-go v1.4.2/go.mod h1:pEp8gjfNLtEOmz+iZqC8bXhu0h4k7NUsW/qiQb34k1U=
 github.com/DataDog/datadog-agent/pkg/obfuscate v0.51.0 h1:GztQU5i304cIkHadEDV77lqmW6CaJ36G0XMoXgZcPgA=
@@ -55,9 +53,57 @@ github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migc
 github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM=
 github.com/airbrake/gobrake/v5 v5.6.1 h1:sCDq6EuHO4dFytpXcZ2tNLoJZevaigFiNMusF098CEI=
 github.com/airbrake/gobrake/v5 v5.6.1/go.mod h1:hyuUJaj7We4nB8Evy9n6LOkxRwxSxMW2IIgOMQcz79E=
-github.com/aws/aws-sdk-go v1.33.0/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0=
-github.com/aws/aws-sdk-go v1.50.23 h1:BB99ohyCmq6O7m5RvjN2yqTt57snL8OhDvfxEvM6ihs=
-github.com/aws/aws-sdk-go v1.50.23/go.mod h1:LF8svs817+Nz+DmiMQKTO3ubZ/6IaTpq3TjupRn3Eqk=
+github.com/aws/amazon-s3-encryption-client-go/v3 v3.0.0 h1:p7M5gUM4YpkTAzHjn1TzukYg8jzW5MqE5ea1tUs82pw=
+github.com/aws/amazon-s3-encryption-client-go/v3 v3.0.0/go.mod h1:olnwkBTbWjaJCaGOHohvJu98q40GiJZuDHLXj751mII=
+github.com/aws/aws-sdk-go v1.44.256/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI=
+github.com/aws/aws-sdk-go v1.50.10 h1:H3NQvqRUKG+9oysCKTIyylpkqfPA7MiBtzTnu/cIGqE=
+github.com/aws/aws-sdk-go v1.50.10/go.mod h1:LF8svs817+Nz+DmiMQKTO3ubZ/6IaTpq3TjupRn3Eqk=
+github.com/aws/aws-sdk-go-v2 v1.25.2 h1:/uiG1avJRgLGiQM9X3qJM8+Qa6KRGK5rRPuXE0HUM+w=
+github.com/aws/aws-sdk-go-v2 v1.25.2/go.mod h1:Evoc5AsmtveRt1komDwIsjHFyrP5tDuF1D1U+6z6pNo=
+github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.1 h1:gTK2uhtAPtFcdRRJilZPx8uJLL2J85xK11nKtWL0wfU=
+github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.1/go.mod h1:sxpLb+nZk7tIfCWChfd+h4QwHNUR57d8hA1cleTkjJo=
+github.com/aws/aws-sdk-go-v2/config v1.27.6 h1:WmoH1aPrxwcqAZTTnETjKr+fuvqzKd4hRrKxQUiuKP4=
+github.com/aws/aws-sdk-go-v2/config v1.27.6/go.mod h1:W9RZFF2pL+OhnUSZsQS/eDMWD8v+R+yWgjj3nSlrXVU=
+github.com/aws/aws-sdk-go-v2/credentials v1.17.6 h1:akhj/nSC6SEx3OmiYGG/7mAyXMem9ZNVVf+DXkikcTk=
+github.com/aws/aws-sdk-go-v2/credentials v1.17.6/go.mod h1:chJZuJ7TkW4kiMwmldOJOEueBoSkUb4ynZS1d9dhygo=
+github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.15.2 h1:AK0J8iYBFeUk2Ax7O8YpLtFsfhdOByh2QIkHmigpRYk=
+github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.15.2/go.mod h1:iRlGzMix0SExQEviAyptRWRGdYNo3+ufW/lCzvKVTUc=
+github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.16.8 h1:fjsaZ2EUoOaosuYMLbQAVJsPIAOV4Xn52AQmk5JbhAs=
+github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.16.8/go.mod h1:WPJcs0Mze3WntafH9Df2NdJ1oSQkEQVL6piZxoS0ecY=
+github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.2 h1:bNo4LagzUKbjdxE0tIcR9pMzLR2U/Tgie1Hq1HQ3iH8=
+github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.2/go.mod h1:wRQv0nN6v9wDXuWThpovGQjqF1HFdcgWjporw14lS8k=
+github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.2 h1:EtOU5jsPdIQNP+6Q2C5e3d65NKT1PeCiQk+9OdzO12Q=
+github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.2/go.mod h1:tyF5sKccmDz0Bv4NrstEr+/9YkSPJHrcO7UsUKf7pWM=
+github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 h1:hT8rVHwugYE2lEfdFE0QWVo81lF7jMrYJVDWI+f+VxU=
+github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0/go.mod h1:8tu/lYfQfFe6IGnaOdrpVgEL2IrrDOf6/m9RQum4NkY=
+github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.2 h1:en92G0Z7xlksoOylkUhuBSfJgijC7rHVLRdnIlHEs0E=
+github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.2/go.mod h1:HgtQ/wN5G+8QSlK62lbOtNwQ3wTSByJ4wH2rCkPt+AE=
+github.com/aws/aws-sdk-go-v2/service/cloudwatch v1.36.1 h1:mQySuI87thHtcbZvEDjwUROGWikU6fqgpHklCBXpJU4=
+github.com/aws/aws-sdk-go-v2/service/cloudwatch v1.36.1/go.mod h1:Z1ThUUTuCO9PArtiQsTmBGBv+38NGj+795Zl0n1jgiM=
+github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.1 h1:EyBZibRTVAs6ECHZOw5/wlylS9OcTzwyjeQMudmREjE=
+github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.1/go.mod h1:JKpmtYhhPs7D97NL/ltqz7yCkERFW5dOlHyVl66ZYF8=
+github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.4 h1:J3Q6N2sTChfYLZSTey3Qeo7n3JSm6RTJDcKev+7Sbus=
+github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.4/go.mod h1:ZopsdDMVg1H03X7BdzpGaufOkuz27RjtKDzioP2U0Hg=
+github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.4 h1:jRiWxyuVO8PlkN72wDMVn/haVH4SDCBkUt0Lf/dxd7s=
+github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.4/go.mod h1:Ru7vg1iQ7cR4i7SZ/JTLYN9kaXtbL69UdgG0OQWQxW0=
+github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.2 h1:1oY1AVEisRI4HNuFoLdRUB0hC63ylDAN6Me3MrfclEg=
+github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.2/go.mod h1:KZ03VgvZwSjkT7fOetQ/wF3MZUvYFirlI1H5NklUNsY=
+github.com/aws/aws-sdk-go-v2/service/kms v1.29.1 h1:OdjJjUWFlMZLAMl54ASxIpZdGEesY4BH3/c0HAPSFdI=
+github.com/aws/aws-sdk-go-v2/service/kms v1.29.1/go.mod h1:Cbx2uxEX0bAB7SlSY+ys05ZBkEb8IbmuAOcGVmDfJFs=
+github.com/aws/aws-sdk-go-v2/service/s3 v1.51.3 h1:7cR4xxS480TI0R6Bd75g9Npdw89VriquvQPlMNmuds4=
+github.com/aws/aws-sdk-go-v2/service/s3 v1.51.3/go.mod h1:zb72GZ2MvfCX5ynVJ+Mc/NCx7hncbsko4NZm5E+p6J4=
+github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.28.1 h1:DtKw4TxZT3VrzYupXQJPBqT9ImyobZZE+JIQPPAVxqs=
+github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.28.1/go.mod h1:bit9G2ORpSjUTr4PA4usvbBfbOyvMj0LbE1dXF14Sug=
+github.com/aws/aws-sdk-go-v2/service/ssm v1.49.1 h1:MeYuN4Ld4FWVJb9ZiOJkon7/foj0Zm2GTDorSaInHj4=
+github.com/aws/aws-sdk-go-v2/service/ssm v1.49.1/go.mod h1:TM0pqkfTRMVtsMlPnOivUmrZSIANsLbq9FTm4oJPcPQ=
+github.com/aws/aws-sdk-go-v2/service/sso v1.20.1 h1:utEGkfdQ4L6YW/ietH7111ZYglLJvS+sLriHJ1NBJEQ=
+github.com/aws/aws-sdk-go-v2/service/sso v1.20.1/go.mod h1:RsYqzYr2F2oPDdpy+PdhephuZxTfjHQe7SOBcZGoAU8=
+github.com/aws/aws-sdk-go-v2/service/ssooidc v1.23.1 h1:9/GylMS45hGGFCcMrUZDVayQE1jYSIN6da9jo7RAYIw=
+github.com/aws/aws-sdk-go-v2/service/ssooidc v1.23.1/go.mod h1:YjAPFn4kGFqKC54VsHs5fn5B6d+PCY2tziEa3U/GB5Y=
+github.com/aws/aws-sdk-go-v2/service/sts v1.28.3 h1:TkiFkSVX990ryWIMBCT4kPqZEgThQe1xPU/AQXavtvU=
+github.com/aws/aws-sdk-go-v2/service/sts v1.28.3/go.mod h1:xYNauIUqSuvzlPVb3VB5no/n48YGhmlInD3Uh0Co8Zc=
+github.com/aws/smithy-go v1.20.1 h1:4SZlSlMr36UEqC7XOyRVb27XMeZubNcBNN+9IgEPIQw=
+github.com/aws/smithy-go v1.20.1/go.mod h1:krry+ya/rV9RDcV/Q16kpu6ypI4K2czasz0NC3qS14E=
 github.com/benesch/cgosymbolizer v0.0.0-20190515212042-bec6fe6e597b h1:5JgaFtHFRnOPReItxvhMDXbvuBkjSWE+9glJyF466yw=
 github.com/benesch/cgosymbolizer v0.0.0-20190515212042-bec6fe6e597b/go.mod h1:eMD2XUcPsHYbakFEocKrWZp47G0MRJYoC60qFblGjpA=
 github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
@@ -127,7 +173,6 @@ github.com/go-openapi/jsonreference v0.20.4 h1:bKlDxQxQJgwpUSgOENiMPzCTBVuc7vTdX
 github.com/go-openapi/jsonreference v0.20.4/go.mod h1:5pZJyJP2MnYCpoeoMAql78cCHauHj0V9Lhc506VOpw4=
 github.com/go-openapi/swag v0.22.9 h1:XX2DssF+mQKM2DHsbgZK74y/zj4mo9I99+89xUmuZCE=
 github.com/go-openapi/swag v0.22.9/go.mod h1:3/OXnFfnMAwBD099SwYRk7GD3xOrr1iL7d/XNLXVVwE=
-github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
 github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
 github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
 github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
@@ -200,11 +245,12 @@ github.com/honeybadger-io/honeybadger-go v0.6.0 h1:RHOml6S0+3w0vWGTM8zOfWTGsfHBv
 github.com/honeybadger-io/honeybadger-go v0.6.0/go.mod h1:YaVKI0eSWUwNOzWa4xr5INoBTWGGz6trlcA3a9gzZ6I=
 github.com/ianlancetaylor/cgosymbolizer v0.0.0-20231130194700-cfcb2fd150eb h1:asfjGoPvNgSPvgbBiwFqMUOgWgid8xlQGCGHfgM/PAs=
 github.com/ianlancetaylor/cgosymbolizer v0.0.0-20231130194700-cfcb2fd150eb/go.mod h1:DvXTE/K/RtHehxU8/GtDs4vFtfw64jJ3PaCnFri8CRg=
-github.com/jmespath/go-jmespath v0.3.0/go.mod h1:9QtRXoHjLGCJ5IBSaohpXITPlowMeeYCZ7fLUTSywik=
 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/johannesboyne/gofakes3 v0.0.0-20240217095638-c55a48f17be6 h1:W8heH5NR7dfdB4FehSFI+DxjCbVKe9fPkPqKzCPJwnM=
+github.com/johannesboyne/gofakes3 v0.0.0-20240217095638-c55a48f17be6/go.mod h1:AxgWC4DDX54O2WDoQO1Ceabtn6IbktjU/7bigor+66g=
 github.com/jonboulle/clockwork v0.4.0 h1:p4Cf1aMWXnXAUh8lVfewRBx1zaTSYKrKMF2g3ST4RZ4=
 github.com/jonboulle/clockwork v0.4.0/go.mod h1:xgRqUGwRcjKCO1vbZUEtSLrqKoPSsUpK7fnezOII0kc=
 github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
@@ -286,7 +332,6 @@ github.com/ryszard/goskiplist v0.0.0-20150312221310-2dfbae5fcf46 h1:GHRpF1pTW19a
 github.com/ryszard/goskiplist v0.0.0-20150312221310-2dfbae5fcf46/go.mod h1:uAQ5PCi+MFsC7HjREoAz1BU+Mq60+05gifQSsHSDG/8=
 github.com/secure-systems-lab/go-securesystemslib v0.8.0 h1:mr5An6X45Kb2nddcFlbmfHkLguCE9laoZCUzEEpIZXA=
 github.com/secure-systems-lab/go-securesystemslib v0.8.0/go.mod h1:UH2VZVuJfCYR8WgMlCU1uFsOUU+KeyrTWcSS73NBOzU=
-github.com/shabbyrobe/gocovmerge v0.0.0-20180507124511-f6ea450bfb63/go.mod h1:n+VKSARF5y/tS9XFSP7vWDfS+GUC5vs/YT7M5XDTUEM=
 github.com/shabbyrobe/gocovmerge v0.0.0-20190829150210-3e036491d500 h1:WnNuhiq+FOY3jNj6JXFT+eLN3CQ/oPIsDPRanvwsmbI=
 github.com/shabbyrobe/gocovmerge v0.0.0-20190829150210-3e036491d500/go.mod h1:+njLrG5wSeoG4Ds61rFgEzKvenR2UHbjMoDHsczxly0=
 github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI=
@@ -405,6 +450,8 @@ golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
+golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
+golang.org/x/mod v0.10.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
 golang.org/x/mod v0.15.0 h1:SernR4v+D55NyBH2QiEQrlBAnj1ECL6AGrA5+dPaMY8=
 golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
 golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -413,13 +460,15 @@ golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73r
 golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
 golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
 golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
 golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
 golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
+golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
+golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
+golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
 golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
 golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
@@ -432,6 +481,7 @@ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJ
 golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
 golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
 golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -452,16 +502,24 @@ golang.org/x/sys v0.0.0-20220627191245-f75cf1eec38b/go.mod h1:oPkhp1MJrh7nUepCBc
 golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
 golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
+golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
+golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
+golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
 golang.org/x/term v0.17.0 h1:mkTF7LCd6WGJNL3K1Ad7kwxNfYAW6a8a8QqtMblp/4U=
 golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
 golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
+golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
+golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
+golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
 golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
 golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
 golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
@@ -469,7 +527,6 @@ golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
-golang.org/x/tools v0.0.0-20190308174544-00c44ba9c14f/go.mod h1:25r3+/G6/xytQM8iWZKq3Hn0kr0rgFKPUNVEL/dr3z4=
 golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
 golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
 golang.org/x/tools v0.0.0-20190829051458-42f498d34c4d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
@@ -478,6 +535,8 @@ golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roY
 golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
 golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
 golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
+golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
+golang.org/x/tools v0.8.0/go.mod h1:JxBZ99ISMI5ViVkT1tr6tdNmXeTrcpVSD3vZ1RsRdN4=
 golang.org/x/tools v0.18.0 h1:k8NLag8AGHnn+PHbl7g43CtqZAwG60vZkLqgyZgIHgQ=
 golang.org/x/tools v0.18.0/go.mod h1:GL7B4CwcLLeo59yx/9UWWuNOW1n3VZ4f5axWfML7Lcg=
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

+ 64 - 51
metrics/cloudwatch/cloudwatch.go

@@ -3,22 +3,25 @@ package cloudwatch
 import (
 	"context"
 	"fmt"
+	"slices"
 	"sync"
 	"time"
 
-	"github.com/aws/aws-sdk-go/aws"
-	"github.com/aws/aws-sdk-go/aws/session"
-	"github.com/aws/aws-sdk-go/service/cloudwatch"
+	"github.com/aws/aws-sdk-go-v2/aws"
+	awsConfig "github.com/aws/aws-sdk-go-v2/config"
+	"github.com/aws/aws-sdk-go-v2/service/cloudwatch"
+	cloudwatchTypes "github.com/aws/aws-sdk-go-v2/service/cloudwatch/types"
+	"github.com/sirupsen/logrus"
+
 	"github.com/imgproxy/imgproxy/v3/config"
 	"github.com/imgproxy/imgproxy/v3/imath"
 	"github.com/imgproxy/imgproxy/v3/metrics/stats"
-	"github.com/sirupsen/logrus"
 )
 
 type GaugeFunc func() float64
 
 type gauge struct {
-	unit string
+	unit cloudwatchTypes.StandardUnit
 	f    GaugeFunc
 }
 
@@ -30,7 +33,7 @@ type bufferStats struct {
 var (
 	enabled bool
 
-	client *cloudwatch.CloudWatch
+	client *cloudwatch.Client
 
 	gauges      = make(map[string]gauge)
 	gaugesMutex sync.RWMutex
@@ -49,22 +52,20 @@ func Init() error {
 		return nil
 	}
 
-	conf := aws.NewConfig()
-
-	if len(config.CloudWatchRegion) > 0 {
-		conf = conf.WithRegion(config.CloudWatchRegion)
+	conf, err := awsConfig.LoadDefaultConfig(context.Background())
+	if err != nil {
+		return fmt.Errorf("can't load CloudWatch config: %s", err)
 	}
 
-	sess, err := session.NewSession()
-	if err != nil {
-		return fmt.Errorf("Can't create CloudWatch session: %s", err)
+	if len(config.CloudWatchRegion) != 0 {
+		conf.Region = config.CloudWatchRegion
 	}
 
-	if sess.Config.Region == nil || len(*sess.Config.Region) == 0 {
-		sess.Config.Region = aws.String("us-west-1")
+	if len(conf.Region) == 0 {
+		conf.Region = "us-west-1"
 	}
 
-	client = cloudwatch.New(sess, conf)
+	client = cloudwatch.NewFromConfig(conf)
 
 	collectorCtx, collectorCtxCancel = context.WithCancel(context.Background())
 
@@ -89,7 +90,13 @@ func AddGaugeFunc(name, unit string, f GaugeFunc) {
 	gaugesMutex.Lock()
 	defer gaugesMutex.Unlock()
 
-	gauges[name] = gauge{unit: unit, f: f}
+	standardUnit := cloudwatchTypes.StandardUnit(unit)
+
+	if !slices.Contains(cloudwatchTypes.StandardUnitNone.Values(), standardUnit) {
+		panic(fmt.Errorf("Unknown CloudWatch unit: %s", unit))
+	}
+
+	gauges[name] = gauge{unit: standardUnit, f: f}
 }
 
 func ObserveBufferSize(t string, size int) {
@@ -135,18 +142,18 @@ func runMetricsCollector() {
 	tick := time.NewTicker(10 * time.Second)
 	defer tick.Stop()
 
-	dimension := &cloudwatch.Dimension{
+	dimension := cloudwatchTypes.Dimension{
 		Name:  aws.String("ServiceName"),
 		Value: aws.String(config.CloudWatchServiceName),
 	}
 
-	bufferDimensions := make(map[string]*cloudwatch.Dimension)
-	bufferDimension := func(t string) *cloudwatch.Dimension {
+	bufferDimensions := make(map[string]cloudwatchTypes.Dimension)
+	bufferDimension := func(t string) cloudwatchTypes.Dimension {
 		if d, ok := bufferDimensions[t]; ok {
 			return d
 		}
 
-		d := &cloudwatch.Dimension{
+		d := cloudwatchTypes.Dimension{
 			Name:  aws.String("BufferType"),
 			Value: aws.String(t),
 		}
@@ -160,17 +167,17 @@ func runMetricsCollector() {
 		select {
 		case <-tick.C:
 			metricsCount := len(gauges) + len(bufferDefaultSizes) + len(bufferMaxSizes) + len(bufferSizeStats) + 3
-			metrics := make([]*cloudwatch.MetricDatum, 0, metricsCount)
+			metrics := make([]cloudwatchTypes.MetricDatum, 0, metricsCount)
 
 			func() {
 				gaugesMutex.RLock()
 				defer gaugesMutex.RUnlock()
 
 				for name, g := range gauges {
-					metrics = append(metrics, &cloudwatch.MetricDatum{
-						Dimensions: []*cloudwatch.Dimension{dimension},
+					metrics = append(metrics, cloudwatchTypes.MetricDatum{
+						Dimensions: []cloudwatchTypes.Dimension{dimension},
 						MetricName: aws.String(name),
-						Unit:       aws.String(g.unit),
+						Unit:       g.unit,
 						Value:      aws.Float64(g.f()),
 					})
 				}
@@ -181,29 +188,29 @@ func runMetricsCollector() {
 				defer bufferStatsMutex.Unlock()
 
 				for t, size := range bufferDefaultSizes {
-					metrics = append(metrics, &cloudwatch.MetricDatum{
-						Dimensions: []*cloudwatch.Dimension{dimension, bufferDimension(t)},
+					metrics = append(metrics, cloudwatchTypes.MetricDatum{
+						Dimensions: []cloudwatchTypes.Dimension{dimension, bufferDimension(t)},
 						MetricName: aws.String("BufferDefaultSize"),
-						Unit:       aws.String("Bytes"),
+						Unit:       cloudwatchTypes.StandardUnitBytes,
 						Value:      aws.Float64(float64(size)),
 					})
 				}
 
 				for t, size := range bufferMaxSizes {
-					metrics = append(metrics, &cloudwatch.MetricDatum{
-						Dimensions: []*cloudwatch.Dimension{dimension, bufferDimension(t)},
+					metrics = append(metrics, cloudwatchTypes.MetricDatum{
+						Dimensions: []cloudwatchTypes.Dimension{dimension, bufferDimension(t)},
 						MetricName: aws.String("BufferMaximumSize"),
-						Unit:       aws.String("Bytes"),
+						Unit:       cloudwatchTypes.StandardUnitBytes,
 						Value:      aws.Float64(float64(size)),
 					})
 				}
 
 				for t, stats := range bufferSizeStats {
-					metrics = append(metrics, &cloudwatch.MetricDatum{
-						Dimensions: []*cloudwatch.Dimension{dimension, bufferDimension(t)},
+					metrics = append(metrics, cloudwatchTypes.MetricDatum{
+						Dimensions: []cloudwatchTypes.Dimension{dimension, bufferDimension(t)},
 						MetricName: aws.String("BufferSize"),
-						Unit:       aws.String("Bytes"),
-						StatisticValues: &cloudwatch.StatisticSet{
+						Unit:       cloudwatchTypes.StandardUnitBytes,
+						StatisticValues: &cloudwatchTypes.StatisticSet{
 							SampleCount: aws.Float64(float64(stats.count)),
 							Sum:         aws.Float64(float64(stats.sum)),
 							Minimum:     aws.Float64(float64(stats.min)),
@@ -213,45 +220,51 @@ func runMetricsCollector() {
 				}
 			}()
 
-			metrics = append(metrics, &cloudwatch.MetricDatum{
-				Dimensions: []*cloudwatch.Dimension{dimension},
+			metrics = append(metrics, cloudwatchTypes.MetricDatum{
+				Dimensions: []cloudwatchTypes.Dimension{dimension},
 				MetricName: aws.String("RequestsInProgress"),
-				Unit:       aws.String("Count"),
+				Unit:       cloudwatchTypes.StandardUnitCount,
 				Value:      aws.Float64(stats.RequestsInProgress()),
 			})
 
-			metrics = append(metrics, &cloudwatch.MetricDatum{
-				Dimensions: []*cloudwatch.Dimension{dimension},
+			metrics = append(metrics, cloudwatchTypes.MetricDatum{
+				Dimensions: []cloudwatchTypes.Dimension{dimension},
 				MetricName: aws.String("ImagesInProgress"),
-				Unit:       aws.String("Count"),
+				Unit:       cloudwatchTypes.StandardUnitCount,
 				Value:      aws.Float64(stats.ImagesInProgress()),
 			})
 
-			metrics = append(metrics, &cloudwatch.MetricDatum{
-				Dimensions: []*cloudwatch.Dimension{dimension},
+			metrics = append(metrics, cloudwatchTypes.MetricDatum{
+				Dimensions: []cloudwatchTypes.Dimension{dimension},
 				MetricName: aws.String("ConcurrencyUtilization"),
-				Unit:       aws.String("Percent"),
+				Unit:       cloudwatchTypes.StandardUnitPercent,
 				Value: aws.Float64(
 					stats.RequestsInProgress() / float64(config.Workers) * 100.0,
 				),
 			})
 
-			metrics = append(metrics, &cloudwatch.MetricDatum{
-				Dimensions: []*cloudwatch.Dimension{dimension},
+			metrics = append(metrics, cloudwatchTypes.MetricDatum{
+				Dimensions: []cloudwatchTypes.Dimension{dimension},
 				MetricName: aws.String("WorkersUtilization"),
-				Unit:       aws.String("Percent"),
+				Unit:       cloudwatchTypes.StandardUnitPercent,
 				Value: aws.Float64(
 					stats.RequestsInProgress() / float64(config.Workers) * 100.0,
 				),
 			})
 
-			_, err := client.PutMetricData(&cloudwatch.PutMetricDataInput{
+			input := cloudwatch.PutMetricDataInput{
 				Namespace:  aws.String(config.CloudWatchNamespace),
 				MetricData: metrics,
-			})
-			if err != nil {
-				logrus.Warnf("Can't send CloudWatch metrics: %s", err)
 			}
+
+			func() {
+				ctx, cancel := context.WithTimeout(collectorCtx, 30*time.Second)
+				defer cancel()
+
+				if _, err := client.PutMetricData(ctx, &input); err != nil {
+					logrus.Warnf("Can't send CloudWatch metrics: %s", err)
+				}
+			}()
 		case <-collectorCtx.Done():
 			return
 		}

+ 114 - 77
transport/s3/s3.go

@@ -2,6 +2,7 @@ package s3
 
 import (
 	"context"
+	"errors"
 	"fmt"
 	"io"
 	"net/http"
@@ -10,29 +11,32 @@ import (
 	"sync"
 	"time"
 
-	"github.com/aws/aws-sdk-go/aws"
-	"github.com/aws/aws-sdk-go/aws/awserr"
-	"github.com/aws/aws-sdk-go/aws/credentials/stscreds"
-	"github.com/aws/aws-sdk-go/aws/request"
-	"github.com/aws/aws-sdk-go/aws/session"
-	"github.com/aws/aws-sdk-go/service/kms"
-	"github.com/aws/aws-sdk-go/service/s3"
-	"github.com/aws/aws-sdk-go/service/s3/s3crypto"
-	"github.com/aws/aws-sdk-go/service/s3/s3manager"
+	s3Crypto "github.com/aws/amazon-s3-encryption-client-go/v3/client"
+	s3CryptoMaterials "github.com/aws/amazon-s3-encryption-client-go/v3/materials"
+	"github.com/aws/aws-sdk-go-v2/aws"
+	awsHttp "github.com/aws/aws-sdk-go-v2/aws/transport/http"
+	awsConfig "github.com/aws/aws-sdk-go-v2/config"
+	"github.com/aws/aws-sdk-go-v2/credentials/stscreds"
+	s3Manager "github.com/aws/aws-sdk-go-v2/feature/s3/manager"
+	"github.com/aws/aws-sdk-go-v2/service/kms"
+	"github.com/aws/aws-sdk-go-v2/service/s3"
+	"github.com/aws/aws-sdk-go-v2/service/sts"
 
 	"github.com/imgproxy/imgproxy/v3/config"
 	defaultTransport "github.com/imgproxy/imgproxy/v3/transport"
 )
 
 type s3Client interface {
-	GetObjectRequest(input *s3.GetObjectInput) (req *request.Request, output *s3.GetObjectOutput)
+	GetObject(ctx context.Context, input *s3.GetObjectInput, opts ...func(*s3.Options)) (*s3.GetObjectOutput, error)
+	HeadBucket(ctx context.Context, input *s3.HeadBucketInput, optFns ...func(*s3.Options)) (*s3.HeadBucketOutput, error)
 }
 
 // transport implements RoundTripper for the 's3' protocol.
 type transport struct {
-	session       *session.Session
+	clientOptions []func(*s3.Options)
+
 	defaultClient s3Client
-	defaultConfig *aws.Config
+	defaultConfig aws.Config
 
 	clientsByRegion map[string]s3Client
 	clientsByBucket map[string]s3Client
@@ -41,7 +45,10 @@ type transport struct {
 }
 
 func New() (http.RoundTripper, error) {
-	conf := aws.NewConfig()
+	conf, err := awsConfig.LoadDefaultConfig(context.Background())
+	if err != nil {
+		return nil, fmt.Errorf("can't load AWS S3 config: %s", err)
+	}
 
 	trans, err := defaultTransport.New(false)
 	if err != nil {
@@ -50,40 +57,38 @@ func New() (http.RoundTripper, error) {
 
 	conf.HTTPClient = &http.Client{Transport: trans}
 
-	if len(config.S3Endpoint) != 0 {
-		conf.Endpoint = aws.String(config.S3Endpoint)
-		conf.S3ForcePathStyle = aws.Bool(true)
+	if len(config.S3Region) != 0 {
+		conf.Region = config.S3Region
 	}
 
-	sess, err := session.NewSession()
-	if err != nil {
-		return nil, fmt.Errorf("can't create S3 session: %s", err)
+	if len(conf.Region) == 0 {
+		conf.Region = "us-west-1"
 	}
 
-	if len(config.S3Region) != 0 {
-		sess.Config.Region = aws.String(config.S3Region)
+	if len(config.S3AssumeRoleArn) != 0 {
+		creds := stscreds.NewAssumeRoleProvider(sts.NewFromConfig(conf), config.S3AssumeRoleArn)
+		conf.Credentials = creds
 	}
 
-	if sess.Config.Region == nil || len(*sess.Config.Region) == 0 {
-		sess.Config.Region = aws.String("us-west-1")
-	}
+	clientOptions := []func(*s3.Options){}
 
-	if len(config.S3AssumeRoleArn) != 0 {
-		conf.Credentials = stscreds.NewCredentials(sess, config.S3AssumeRoleArn)
+	if len(config.S3Endpoint) != 0 {
+		clientOptions = append(clientOptions, func(o *s3.Options) {
+			o.BaseEndpoint = aws.String(config.S3Endpoint)
+			o.UsePathStyle = true
+		})
 	}
 
-	client, err := createClient(sess, conf)
+	client, err := createClient(conf, clientOptions)
 	if err != nil {
 		return nil, fmt.Errorf("can't create S3 client: %s", err)
 	}
 
-	clientRegion := *sess.Config.Region
-
 	return &transport{
-		session:         sess,
+		clientOptions:   clientOptions,
 		defaultClient:   client,
 		defaultConfig:   conf,
-		clientsByRegion: map[string]s3Client{clientRegion: client},
+		clientsByRegion: map[string]s3Client{conf.Region: client},
 		clientsByBucket: make(map[string]s3Client),
 	}, nil
 }
@@ -91,13 +96,15 @@ func New() (http.RoundTripper, error) {
 func (t *transport) RoundTrip(req *http.Request) (*http.Response, error) {
 	input := &s3.GetObjectInput{
 		Bucket: aws.String(req.URL.Host),
-		Key:    aws.String(req.URL.Path),
+		Key:    aws.String(strings.TrimPrefix(req.URL.Path, "/")),
 	}
 
 	if len(req.URL.RawQuery) > 0 {
 		input.VersionId = aws.String(req.URL.RawQuery)
 	}
 
+	statusCode := http.StatusOK
+
 	if r := req.Header.Get("Range"); len(r) != 0 {
 		input.Range = aws.String(r)
 	} else {
@@ -121,30 +128,71 @@ func (t *transport) RoundTrip(req *http.Request) (*http.Response, error) {
 		return handleError(req, err)
 	}
 
-	s3req, objectOutput := client.GetObjectRequest(input)
-	s3req.SetContext(req.Context())
-
-	if err := s3req.Send(); err != nil {
-		if s3req.HTTPResponse != nil && s3req.HTTPResponse.Body != nil {
-			s3req.HTTPResponse.Body.Close()
+	output, err := client.GetObject(req.Context(), input)
+	if err != nil {
+		if output != nil && output.Body != nil {
+			output.Body.Close()
 		}
 
 		return handleError(req, err)
 	}
 
-	if config.S3DecryptionClientEnabled {
-		s3req.HTTPResponse.Body = objectOutput.Body
+	contentLength := int64(-1)
+	if output.ContentLength != nil {
+		contentLength = *output.ContentLength
+	}
 
-		if unencryptedContentLength := s3req.HTTPResponse.Header.Get("X-Amz-Meta-X-Amz-Unencrypted-Content-Length"); len(unencryptedContentLength) != 0 {
-			contentLength, err := strconv.ParseInt(unencryptedContentLength, 10, 64)
+	if config.S3DecryptionClientEnabled {
+		if unencryptedContentLength := output.Metadata["X-Amz-Meta-X-Amz-Unencrypted-Content-Length"]; len(unencryptedContentLength) != 0 {
+			cl, err := strconv.ParseInt(unencryptedContentLength, 10, 64)
 			if err != nil {
 				handleError(req, err)
 			}
-			s3req.HTTPResponse.ContentLength = contentLength
+			contentLength = cl
 		}
 	}
 
-	return s3req.HTTPResponse, nil
+	header := make(http.Header)
+	if contentLength > 0 {
+		header.Set("Content-Length", strconv.FormatInt(contentLength, 10))
+	}
+	if output.ContentType != nil {
+		header.Set("Content-Type", *output.ContentType)
+	}
+	if output.ContentEncoding != nil {
+		header.Set("Content-Encoding", *output.ContentEncoding)
+	}
+	if output.CacheControl != nil {
+		header.Set("Cache-Control", *output.CacheControl)
+	}
+	if output.Expires != nil {
+		header.Set("Expires", output.Expires.Format(http.TimeFormat))
+	}
+	if output.ETag != nil {
+		header.Set("ETag", *output.ETag)
+	}
+	if output.LastModified != nil {
+		header.Set("Last-Modified", output.LastModified.Format(http.TimeFormat))
+	}
+	if output.AcceptRanges != nil {
+		header.Set("Accept-Ranges", *output.AcceptRanges)
+	}
+	if output.ContentRange != nil {
+		header.Set("Content-Range", *output.ContentRange)
+		statusCode = http.StatusPartialContent
+	}
+
+	return &http.Response{
+		StatusCode:    statusCode,
+		Proto:         "HTTP/1.0",
+		ProtoMajor:    1,
+		ProtoMinor:    0,
+		Header:        header,
+		ContentLength: contentLength,
+		Body:          output.Body,
+		Close:         true,
+		Request:       req,
+	}, nil
 }
 
 func (t *transport) getClient(ctx context.Context, bucket string) (s3Client, error) {
@@ -172,9 +220,13 @@ func (t *transport) getClient(ctx context.Context, bucket string) (s3Client, err
 		return client, nil
 	}
 
-	region, err := s3manager.GetBucketRegion(ctx, t.session, bucket, *t.session.Config.Region)
+	region, err := s3Manager.GetBucketRegion(ctx, t.defaultClient, bucket)
 	if err != nil {
-		return nil, err
+		return nil, fmt.Errorf("can't get bucket region: %s", err)
+	}
+
+	if len(region) == 0 {
+		region = t.defaultConfig.Region
 	}
 
 	if client = t.clientsByRegion[region]; client != nil {
@@ -183,9 +235,9 @@ func (t *transport) getClient(ctx context.Context, bucket string) (s3Client, err
 	}
 
 	conf := t.defaultConfig.Copy()
-	conf.Region = aws.String(region)
+	conf.Region = region
 
-	client, err = createClient(t.session, conf)
+	client, err = createClient(conf, t.clientOptions)
 	if err != nil {
 		return nil, fmt.Errorf("can't create regional S3 client: %s", err)
 	}
@@ -196,53 +248,38 @@ func (t *transport) getClient(ctx context.Context, bucket string) (s3Client, err
 	return client, nil
 }
 
-func createClient(sess *session.Session, conf *aws.Config) (s3Client, error) {
+func createClient(conf aws.Config, opts []func(*s3.Options)) (s3Client, error) {
+	client := s3.NewFromConfig(conf, opts...)
+
 	if config.S3DecryptionClientEnabled {
-		// `s3crypto.NewDecryptionClientV2` doesn't accept additional configs, so we
-		// need to copy the session with an additional config
-		sess = sess.Copy(conf)
+		kmsClient := kms.NewFromConfig(conf)
+		keyring := s3CryptoMaterials.NewKmsDecryptOnlyAnyKeyKeyring(kmsClient)
 
-		cryptoRegistry, err := createCryptoRegistry(sess)
+		cmm, err := s3CryptoMaterials.NewCryptographicMaterialsManager(keyring)
 		if err != nil {
 			return nil, err
 		}
 
-		return s3crypto.NewDecryptionClientV2(sess, cryptoRegistry)
+		return s3Crypto.New(client, cmm)
 	} else {
-		return s3.New(sess, conf), nil
-	}
-}
-
-func createCryptoRegistry(sess *session.Session) (*s3crypto.CryptoRegistry, error) {
-	kmsClient := kms.New(sess)
-
-	cr := s3crypto.NewCryptoRegistry()
-	if err := s3crypto.RegisterKMSContextWrapWithAnyCMK(cr, kmsClient); err != nil {
-		return nil, err
-	}
-	if err := s3crypto.RegisterAESGCMContentCipher(cr); err != nil {
-		return nil, err
+		return client, nil
 	}
-
-	return cr, nil
 }
 
 func handleError(req *http.Request, err error) (*http.Response, error) {
-	if s3err, ok := err.(awserr.Error); ok && s3err.Code() == request.CanceledErrorCode {
-		if e := s3err.OrigErr(); e != nil {
-			return nil, e
-		}
+	var rerr *awsHttp.ResponseError
+	if !errors.As(err, &rerr) {
+		return nil, err
 	}
 
-	s3err, ok := err.(awserr.RequestFailure)
-	if !ok || s3err.StatusCode() < 100 || s3err.StatusCode() == 301 {
+	if rerr.Response == nil || rerr.Response.StatusCode < 100 || rerr.Response.StatusCode == 301 {
 		return nil, err
 	}
 
-	body := strings.NewReader(s3err.Message())
+	body := strings.NewReader(err.Error())
 
 	return &http.Response{
-		StatusCode:    s3err.StatusCode(),
+		StatusCode:    rerr.Response.StatusCode,
 		Proto:         "HTTP/1.0",
 		ProtoMajor:    1,
 		ProtoMinor:    0,

+ 7 - 8
transport/s3/s3_test.go

@@ -9,8 +9,8 @@ import (
 	"testing"
 	"time"
 
-	"github.com/aws/aws-sdk-go/aws"
-	"github.com/aws/aws-sdk-go/service/s3"
+	"github.com/aws/aws-sdk-go-v2/aws"
+	"github.com/aws/aws-sdk-go-v2/service/s3"
 	"github.com/johannesboyne/gofakes3"
 	"github.com/johannesboyne/gofakes3/backend/s3mem"
 	"github.com/stretchr/testify/require"
@@ -50,18 +50,18 @@ func (s *S3TestSuite) SetupSuite() {
 	svc, err := s.transport.(*transport).getClient(context.Background(), "test")
 	require.Nil(s.T(), err)
 	require.NotNil(s.T(), svc)
-	require.IsType(s.T(), &s3.S3{}, svc)
+	require.IsType(s.T(), &s3.Client{}, svc)
 
-	client := svc.(*s3.S3)
+	client := svc.(*s3.Client)
 
-	_, err = client.PutObject(&s3.PutObjectInput{
+	_, err = client.PutObject(context.Background(), &s3.PutObjectInput{
 		Body:   bytes.NewReader(make([]byte, 32)),
 		Bucket: aws.String("test"),
 		Key:    aws.String("foo/test.png"),
 	})
 	require.Nil(s.T(), err)
 
-	obj, err := client.GetObject(&s3.GetObjectInput{
+	obj, err := client.GetObject(context.Background(), &s3.GetObjectInput{
 		Bucket: aws.String("test"),
 		Key:    aws.String("foo/test.png"),
 	})
@@ -128,7 +128,7 @@ func (s *S3TestSuite) TestRoundTripWithLastModifiedDisabledReturns200() {
 }
 
 func (s *S3TestSuite) TestRoundTripWithLastModifiedEnabled() {
-	config.ETagEnabled = true
+	config.LastModifiedEnabled = true
 	request, _ := http.NewRequest("GET", "s3://test/foo/test.png", nil)
 
 	response, err := s.transport.RoundTrip(request)
@@ -137,7 +137,6 @@ func (s *S3TestSuite) TestRoundTripWithLastModifiedEnabled() {
 	require.Equal(s.T(), s.lastModified.Format(http.TimeFormat), response.Header.Get("Last-Modified"))
 }
 
-// gofakes3 doesn't support If-Modified-Since (yet?)
 func (s *S3TestSuite) TestRoundTripWithIfModifiedSinceReturns304() {
 	config.LastModifiedEnabled = true