diff --git a/README.md b/README.md index 79c9c07..ddc952c 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ Run from command-line - example with minimal parameter list: Run as docker container - example for local s3-like buckets: ```sh -docker run -p 9655:9655 -d -e S3_ENDPOINT=http://127.0.0.1:9000 -e S3_ACCESS_KEY=minioadmin -e S3_SECRET_KEY=minioadmin -e S3_BUCKET_NAMES=my-bucket-name docker.io/tropnikovvl/s3bucket_exporter:1.5.0 +docker run -p 9655:9655 -d -e S3_ENDPOINT=http://127.0.0.1:9000 -e S3_ACCESS_KEY=minioadmin -e S3_SECRET_KEY=minioadmin -e S3_BUCKET_NAMES=my-bucket-name docker.io/tropnikovvl/s3bucket_exporter:1.6.0 ``` Run from command-line - example for AWS @@ -42,8 +42,9 @@ As for available flags and equivalent environment variables, here is a list: | S3_REGION | -s3_region | S3 region name | us-east-1 | eu-west-1 | | S3_FORCE_PATH_STYLE | -s3_force_path_style | Force use path style (bucketname not added to url) | False | True | | LISTEN_PORT | -listen_port | Exporter listen Port cluster | :9655 | :9123 | -| LOG_LEVEL | -log_level | Log level. Info or Debug | Info | Debug | -| SCRAPE_INTERVAL | -scrape_interval | Scrape interval | 5m | 30s | +| LOG_LEVEL | -log_level | Log level. Info or Debug | Info | Debug | +| SCRAPE_INTERVAL | -scrape_interval | Scrape interval | 5m | 30s | +| USE_IAM_ROLE | -use_iam_role | Use IAM role instead of access keys | false | true | > Warning: For security reason is not advised to use credential from command line diff --git a/controllers/s3talker.go b/controllers/s3talker.go index dbc05de..3d39bab 100644 --- a/controllers/s3talker.go +++ b/controllers/s3talker.go @@ -37,6 +37,7 @@ type S3Conn struct { S3ConnEndpoint string `json:"s3_conn_endpoint,omitempty"` S3ConnRegion string `json:"s3_conn_region"` S3ConnForcePathStyle bool `json:"s3_conn_force_path_style"` + UseIAMRole bool `json:"use_iam_role"` } type S3ClientInterface interface { @@ -61,13 +62,23 @@ func getS3Client(cfg aws.Config, s3Conn S3Conn) S3ClientInterface { if s3ClientInstance != nil { return s3ClientInstance } - return s3.NewFromConfig(cfg, func(o *s3.Options) { + + options := func(o *s3.Options) { if s3Conn.S3ConnEndpoint != "" { o.BaseEndpoint = aws.String(s3Conn.S3ConnEndpoint) } - o.Credentials = credentials.NewStaticCredentialsProvider(s3Conn.S3ConnAccessKey, s3Conn.S3ConnSecretKey, "") + if !s3Conn.UseIAMRole { + // Only set static credentials if explicitly not using IAM role + o.Credentials = credentials.NewStaticCredentialsProvider( + s3Conn.S3ConnAccessKey, + s3Conn.S3ConnSecretKey, + "", + ) + } o.UsePathStyle = s3Conn.S3ConnForcePathStyle - }) + } + + return s3.NewFromConfig(cfg, options) } // S3UsageInfo - gets S3 connection details and returns S3Summary @@ -76,7 +87,7 @@ func S3UsageInfo(s3Conn S3Conn, s3BucketNames string) (S3Summary, error) { cfg, err := config.LoadDefaultConfig(context.TODO(), config.WithRegion(s3Conn.S3ConnRegion)) if err != nil { - log.Errorf("Failed to create AWS session: %v", err) + log.Errorf("Failed to create AWS config: %v", err) return summary, err } diff --git a/controllers/s3talker_test.go b/controllers/s3talker_test.go index cdf95d7..b56cc2a 100644 --- a/controllers/s3talker_test.go +++ b/controllers/s3talker_test.go @@ -136,3 +136,36 @@ func TestCalculateBucketMetrics(t *testing.T) { assert.Equal(t, float64(7168), size) assert.Equal(t, float64(3), count) } + +func TestS3UsageInfo_WithIAMRole(t *testing.T) { + mockClient := new(MockS3Client) + SetS3Client(mockClient) + defer ResetS3Client() + + s3Conn := S3Conn{ + S3ConnRegion: "us-east-1", + S3ConnEndpoint: "s3.amazonaws.com", + UseIAMRole: true, + } + + mockClient.On("ListBuckets", mock.Anything, mock.Anything, mock.Anything).Return(&s3.ListBucketsOutput{ + Buckets: []types.Bucket{ + {Name: aws.String("bucket1")}, + }, + }, nil) + + mockClient.On("ListObjectsV2", mock.Anything, mock.Anything, mock.Anything).Return(&s3.ListObjectsV2Output{ + Contents: []types.Object{ + {Size: aws.Int64(100)}, + }, + IsTruncated: aws.Bool(false), + }, nil) + + summary, err := S3UsageInfo(s3Conn, "bucket1") + + assert.NoError(t, err) + assert.True(t, summary.S3Status) + assert.Equal(t, float64(100), summary.S3Size) + assert.Equal(t, float64(1), summary.S3ObjectNumber) + assert.Len(t, summary.S3Buckets, 1) +} diff --git a/docker-compose.yaml b/docker-compose.yaml index e1b8c15..3c3dc5e 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,7 +1,7 @@ version: "3.5" services: s3bucketexporter: - image: docker.io/tropnikovvl/s3bucket_exporter:1.5.0 + image: docker.io/tropnikovvl/s3bucket_exporter:1.6.0 restart: always ports: - "9655:9655" diff --git a/helm/Chart.yaml b/helm/Chart.yaml index 96a4d84..043aec3 100644 --- a/helm/Chart.yaml +++ b/helm/Chart.yaml @@ -15,10 +15,10 @@ type: application # This is the chart version. This version number should be incremented each time you make changes # to the chart and its templates, including the app version. # Versions are expected to follow Semantic Versioning (https://semver.org/) -version: 1.2.0 +version: 1.3.0 # This is the version number of the application being deployed. This version number should be # incremented each time you make changes to the application. Versions are not expected to # follow Semantic Versioning. They should reflect the version the application is using. # It is recommended to use it with quotes. -appVersion: "1.5.0" +appVersion: "1.6.0" diff --git a/helm/values.yaml b/helm/values.yaml index aec77d9..400b221 100644 --- a/helm/values.yaml +++ b/helm/values.yaml @@ -34,6 +34,7 @@ serviceAccount: automount: true # Annotations to add to the service account annotations: {} + # eks.amazonaws.com/role-arn: arn:aws:iam::111122223333:role/my-role # The name of the service account to use. # If not set and create is true, a name is generated using the fullname template name: "" diff --git a/main.go b/main.go index 2a4697a..8ee9957 100644 --- a/main.go +++ b/main.go @@ -26,6 +26,7 @@ var ( s3SecretKey string s3Region string s3ForcePathStyle bool + useIAMRole bool metricsMutex sync.RWMutex cachedMetrics controllers.S3Summary @@ -57,6 +58,7 @@ func initFlags() { flag.StringVar(&logLevel, "log_level", envString("LOG_LEVEL", "info"), "LOG_LEVEL") flag.StringVar(&scrapeInterval, "scrape_interval", envString("SCRAPE_INTERVAL", "5m"), "SCRAPE_INTERVAL - eg. 30s, 5m, 1h") flag.BoolVar(&s3ForcePathStyle, "s3_force_path_style", envBool("S3_FORCE_PATH_STYLE", false), "S3_FORCE_PATH_STYLE") + flag.BoolVar(&useIAMRole, "use_iam_role", envBool("USE_IAM_ROLE", false), "USE_IAM_ROLE - use IAM role instead of access keys") } // S3Collector struct @@ -108,6 +110,7 @@ func updateMetrics(interval time.Duration) { S3ConnSecretKey: s3SecretKey, S3ConnForcePathStyle: s3ForcePathStyle, S3ConnRegion: s3Region, + UseIAMRole: useIAMRole, } metrics, err := controllers.S3UsageInfo(s3Conn, s3BucketNames) @@ -146,8 +149,8 @@ func main() { } log.SetLevel(level) - if s3AccessKey == "" || s3SecretKey == "" { - log.Fatal("S3 access key and secret key are required") + if !useIAMRole && (s3AccessKey == "" || s3SecretKey == "") { + log.Fatal("S3 access key and secret key are required when not using IAM role") } interval, err := time.ParseDuration(scrapeInterval)