mirror of
https://github.com/VictoriaMetrics/VictoriaMetrics.git
synced 2024-11-21 14:44:00 +00:00
lib/promscrape/discovery/ec2: code prettifying after 312fead9a2
This commit is contained in:
parent
0069353d5e
commit
f961838290
3 changed files with 319 additions and 183 deletions
|
@ -9,7 +9,7 @@ import (
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"sync/atomic"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promscrape/discoveryutils"
|
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promscrape/discoveryutils"
|
||||||
|
@ -21,45 +21,35 @@ const (
|
||||||
)
|
)
|
||||||
|
|
||||||
type apiConfig struct {
|
type apiConfig struct {
|
||||||
endpoint string
|
region string
|
||||||
region string
|
roleARN string
|
||||||
roleARN string
|
filters string
|
||||||
filters string
|
port int
|
||||||
port int
|
|
||||||
// this keys needed,
|
ec2Endpoint string
|
||||||
// when we are using temporary credentials
|
stsEndpoint string
|
||||||
|
|
||||||
|
// these keys are needed for obtaining creds.
|
||||||
defaultAccessKey string
|
defaultAccessKey string
|
||||||
defaultSecretKey string
|
defaultSecretKey string
|
||||||
apiCredentials atomic.Value
|
|
||||||
|
// Real credentials used for accessing EC2 API.
|
||||||
|
creds *apiCredentials
|
||||||
|
credsLock sync.Mutex
|
||||||
}
|
}
|
||||||
|
|
||||||
// apiCredentials represents aws api credentials
|
// apiCredentials represents aws api credentials
|
||||||
type apiCredentials struct {
|
type apiCredentials struct {
|
||||||
AccessKeyID string `json:"AccessKeyId"`
|
AccessKeyID string
|
||||||
SecretAccessKey string `json:"SecretAccessKey"`
|
SecretAccessKey string
|
||||||
Token string `json:"Token"`
|
Token string
|
||||||
Expiration time.Time `json:"Expiration"`
|
Expiration time.Time
|
||||||
}
|
|
||||||
|
|
||||||
// stsCredentialsResponse - represents aws sts AssumeRole response
|
|
||||||
// https://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRole.html
|
|
||||||
type stsCredentialsResponse struct {
|
|
||||||
AssumeRoleResult struct {
|
|
||||||
Credentials struct {
|
|
||||||
AccessKeyID string `xml:"AccessKeyId"`
|
|
||||||
SecretAccessKey string `xml:"SecretAccessKey"`
|
|
||||||
SessionToken string `xml:"SessionToken"`
|
|
||||||
Expiration time.Time `xml:"Expiration"`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var configMap = discoveryutils.NewConfigMap()
|
var configMap = discoveryutils.NewConfigMap()
|
||||||
|
|
||||||
func getAPIConfig(sdc *SDConfig) (*apiConfig, error) {
|
func getAPIConfig(sdc *SDConfig) (*apiConfig, error) {
|
||||||
v, err := configMap.Get(sdc, func() (interface{}, error) {
|
v, err := configMap.Get(sdc, func() (interface{}, error) { return newAPIConfig(sdc) })
|
||||||
return newAPIConfig(sdc)
|
|
||||||
})
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -81,12 +71,14 @@ func newAPIConfig(sdc *SDConfig) (*apiConfig, error) {
|
||||||
port = *sdc.Port
|
port = *sdc.Port
|
||||||
}
|
}
|
||||||
cfg := &apiConfig{
|
cfg := &apiConfig{
|
||||||
endpoint: sdc.Endpoint,
|
region: region,
|
||||||
region: region,
|
roleARN: sdc.RoleARN,
|
||||||
roleARN: sdc.RoleARN,
|
filters: filters,
|
||||||
filters: filters,
|
port: port,
|
||||||
port: port,
|
|
||||||
}
|
}
|
||||||
|
cfg.ec2Endpoint = buildAPIEndpoint(sdc.Endpoint, region, "ec2")
|
||||||
|
cfg.stsEndpoint = buildAPIEndpoint(sdc.Endpoint, region, "sts")
|
||||||
|
|
||||||
// explicitly set credentials has priority over env variables
|
// explicitly set credentials has priority over env variables
|
||||||
cfg.defaultAccessKey = os.Getenv(awsAccessKeyEnv)
|
cfg.defaultAccessKey = os.Getenv(awsAccessKeyEnv)
|
||||||
cfg.defaultSecretKey = os.Getenv(awsSecretKeyEnv)
|
cfg.defaultSecretKey = os.Getenv(awsSecretKeyEnv)
|
||||||
|
@ -96,20 +88,10 @@ func newAPIConfig(sdc *SDConfig) (*apiConfig, error) {
|
||||||
if len(sdc.SecretKey) > 0 {
|
if len(sdc.SecretKey) > 0 {
|
||||||
cfg.defaultSecretKey = sdc.SecretKey
|
cfg.defaultSecretKey = sdc.SecretKey
|
||||||
}
|
}
|
||||||
|
cfg.creds = &apiCredentials{
|
||||||
// fast return if credentials are set and there is no roleARN
|
AccessKeyID: cfg.defaultAccessKey,
|
||||||
if len(cfg.defaultAccessKey) > 0 && len(cfg.defaultSecretKey) > 0 && len(sdc.RoleARN) == 0 {
|
SecretAccessKey: cfg.defaultSecretKey,
|
||||||
cfg.apiCredentials.Store(&apiCredentials{
|
|
||||||
AccessKeyID: cfg.defaultAccessKey,
|
|
||||||
SecretAccessKey: cfg.defaultSecretKey,
|
|
||||||
})
|
|
||||||
return cfg, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := cfg.refreshAPICredentials(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return cfg, nil
|
return cfg, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -137,89 +119,6 @@ func getDefaultRegion() (string, error) {
|
||||||
return id.Region, nil
|
return id.Region, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// refreshAPICredentials updates expired tokens.
|
|
||||||
func (cfg *apiConfig) refreshAPICredentials() error {
|
|
||||||
|
|
||||||
newAPICredentials := &apiCredentials{
|
|
||||||
AccessKeyID: cfg.defaultAccessKey,
|
|
||||||
SecretAccessKey: cfg.defaultSecretKey,
|
|
||||||
}
|
|
||||||
|
|
||||||
// we need instance credentials
|
|
||||||
// if dont have key and secret
|
|
||||||
if len(newAPICredentials.AccessKeyID) == 0 && len(newAPICredentials.SecretAccessKey) == 0 {
|
|
||||||
|
|
||||||
ac, err := getInstanceRoleCredentials()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
newAPICredentials.Token = ac.Token
|
|
||||||
newAPICredentials.SecretAccessKey = ac.SecretAccessKey
|
|
||||||
newAPICredentials.AccessKeyID = ac.AccessKeyID
|
|
||||||
newAPICredentials.Expiration = ac.Expiration
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// read credentials from sts api, if role_arn is defined
|
|
||||||
if cfg.roleARN != "" {
|
|
||||||
|
|
||||||
ac, err := getRoleARNCredentials(cfg.region, cfg.endpoint, cfg.roleARN, newAPICredentials)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("cannot get credentials for role_arn: %s: %w", cfg.roleARN, err)
|
|
||||||
}
|
|
||||||
if newAPICredentials.Expiration.IsZero() || ac.Expiration.Before(newAPICredentials.Expiration) {
|
|
||||||
newAPICredentials.Expiration = ac.Expiration
|
|
||||||
}
|
|
||||||
newAPICredentials.AccessKeyID = ac.AccessKeyID
|
|
||||||
newAPICredentials.SecretAccessKey = ac.SecretAccessKey
|
|
||||||
newAPICredentials.Token = ac.Token
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(newAPICredentials.AccessKeyID) == 0 {
|
|
||||||
return fmt.Errorf("missing `access_key`, you can set it with %s env var, "+
|
|
||||||
"directly at `ec2_sd_config` as `access_key` or use instance iam role", awsAccessKeyEnv)
|
|
||||||
}
|
|
||||||
if len(newAPICredentials.SecretAccessKey) == 0 {
|
|
||||||
return fmt.Errorf("missing `secret_key`, you can set it with %s env var,"+
|
|
||||||
"directly at `ec2_sd_config` as `secret_key` or use instance iam role", awsSecretKeyEnv)
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg.apiCredentials.Store(newAPICredentials)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// credentialsExpired - checks if tokens refresh is needed
|
|
||||||
func (cfg *apiConfig) credentialsExpired() bool {
|
|
||||||
ac := cfg.credentials()
|
|
||||||
return !ac.Expiration.IsZero() && time.Since(ac.Expiration) > -5*time.Second
|
|
||||||
}
|
|
||||||
|
|
||||||
// credentials - returns current api credentials
|
|
||||||
func (cfg *apiConfig) credentials() *apiCredentials {
|
|
||||||
return cfg.apiCredentials.Load().(*apiCredentials)
|
|
||||||
}
|
|
||||||
|
|
||||||
// getInstanceRoleCredentials makes request to local ec2 instance metadata service
|
|
||||||
// and tries to retrieve credentials from assigned iam role.
|
|
||||||
// https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/iam-roles-for-amazon-ec2.html
|
|
||||||
func getInstanceRoleCredentials() (*apiCredentials, error) {
|
|
||||||
instanceRoleName, err := getMetadataByPath("meta-data/iam/security-credentials/")
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("cannot get instanceRoleName: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
credentials, err := getMetadataByPath("meta-data/iam/security-credentials/" + string(instanceRoleName))
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("cannot get instanceCredentials with role %s, error: %w", instanceRoleName, err)
|
|
||||||
}
|
|
||||||
ac := &apiCredentials{}
|
|
||||||
if err := json.Unmarshal(credentials, ac); err != nil {
|
|
||||||
return nil, fmt.Errorf("cannot parse instance metadata role response : %w", err)
|
|
||||||
}
|
|
||||||
return ac, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// IdentityDocument is identity document returned from AWS metadata server.
|
// IdentityDocument is identity document returned from AWS metadata server.
|
||||||
//
|
//
|
||||||
// See https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/instance-identity-documents.html
|
// See https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/instance-identity-documents.html
|
||||||
|
@ -227,10 +126,113 @@ type IdentityDocument struct {
|
||||||
Region string
|
Region string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getFreshAPICredentials returns fresh EC2 API credentials.
|
||||||
|
//
|
||||||
|
// The credentials are refreshed if needed.
|
||||||
|
func (cfg *apiConfig) getFreshAPICredentials() (*apiCredentials, error) {
|
||||||
|
cfg.credsLock.Lock()
|
||||||
|
defer cfg.credsLock.Unlock()
|
||||||
|
|
||||||
|
if len(cfg.defaultAccessKey) > 0 && len(cfg.defaultSecretKey) > 0 && len(cfg.roleARN) == 0 {
|
||||||
|
// There is no need in refreshing statically set api credentials if `role_arn` isn't set.
|
||||||
|
return cfg.creds, nil
|
||||||
|
}
|
||||||
|
if time.Until(cfg.creds.Expiration) > 10*time.Second {
|
||||||
|
// Credentials aren't expired yet.
|
||||||
|
return cfg.creds, nil
|
||||||
|
}
|
||||||
|
// Credentials have been expired. Update them.
|
||||||
|
ac, err := getAPICredentials(cfg)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
cfg.creds = ac
|
||||||
|
return ac, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getAPICredentials obtains new EC2 API credentials from instance metadata and role_arn.
|
||||||
|
func getAPICredentials(cfg *apiConfig) (*apiCredentials, error) {
|
||||||
|
acNew := &apiCredentials{
|
||||||
|
AccessKeyID: cfg.defaultAccessKey,
|
||||||
|
SecretAccessKey: cfg.defaultSecretKey,
|
||||||
|
}
|
||||||
|
|
||||||
|
// we need instance credentials if dont have access keys
|
||||||
|
if len(acNew.AccessKeyID) == 0 && len(acNew.SecretAccessKey) == 0 {
|
||||||
|
ac, err := getInstanceRoleCredentials()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
acNew = ac
|
||||||
|
}
|
||||||
|
|
||||||
|
// read credentials from sts api, if role_arn is defined
|
||||||
|
if len(cfg.roleARN) > 0 {
|
||||||
|
ac, err := getRoleARNCredentials(cfg.region, cfg.stsEndpoint, cfg.roleARN, acNew)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot get credentials for role_arn %q: %w", cfg.roleARN, err)
|
||||||
|
}
|
||||||
|
acNew = ac
|
||||||
|
}
|
||||||
|
if len(acNew.AccessKeyID) == 0 {
|
||||||
|
return nil, fmt.Errorf("missing `access_key`, you can set it with %s env var, "+
|
||||||
|
"directly at `ec2_sd_config` as `access_key` or use instance iam role", awsAccessKeyEnv)
|
||||||
|
}
|
||||||
|
if len(acNew.SecretAccessKey) == 0 {
|
||||||
|
return nil, fmt.Errorf("missing `secret_key`, you can set it with %s env var,"+
|
||||||
|
"directly at `ec2_sd_config` as `secret_key` or use instance iam role", awsSecretKeyEnv)
|
||||||
|
}
|
||||||
|
return acNew, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getInstanceRoleCredentials makes request to local ec2 instance metadata service
|
||||||
|
// and tries to retrieve credentials from assigned iam role.
|
||||||
|
//
|
||||||
|
// See https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/iam-roles-for-amazon-ec2.html
|
||||||
|
func getInstanceRoleCredentials() (*apiCredentials, error) {
|
||||||
|
instanceRoleName, err := getMetadataByPath("meta-data/iam/security-credentials/")
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot get instanceRoleName: %w", err)
|
||||||
|
}
|
||||||
|
data, err := getMetadataByPath("meta-data/iam/security-credentials/" + string(instanceRoleName))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot get security credentails for instanceRoleName %q: %w", instanceRoleName, err)
|
||||||
|
}
|
||||||
|
return parseMetadataSecurityCredentials(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseMetadataSecurityCredentials parses apiCredentials from metadata response to http://169.254.169.254/latest/meta-data/iam/security-credentials/*
|
||||||
|
//
|
||||||
|
// See https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/iam-roles-for-amazon-ec2.html
|
||||||
|
func parseMetadataSecurityCredentials(data []byte) (*apiCredentials, error) {
|
||||||
|
var msc MetadataSecurityCredentials
|
||||||
|
if err := json.Unmarshal(data, &msc); err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot parse metadata security credentials from %q: %w", data, err)
|
||||||
|
}
|
||||||
|
return &apiCredentials{
|
||||||
|
AccessKeyID: msc.AccessKeyID,
|
||||||
|
SecretAccessKey: msc.SecretAccessKey,
|
||||||
|
Token: msc.Token,
|
||||||
|
Expiration: msc.Expiration,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MetadataSecurityCredentials represents credentials obtained from http://169.254.169.254/latest/meta-data/iam/security-credentials/*
|
||||||
|
//
|
||||||
|
// See https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/iam-roles-for-amazon-ec2.html
|
||||||
|
type MetadataSecurityCredentials struct {
|
||||||
|
AccessKeyID string `json:"AccessKeyId"`
|
||||||
|
SecretAccessKey string `json:"SecretAccessKey"`
|
||||||
|
Token string `json:"Token"`
|
||||||
|
Expiration time.Time `json:"Expiration"`
|
||||||
|
}
|
||||||
|
|
||||||
// getMetadataByPath returns instance metadata by url path
|
// getMetadataByPath returns instance metadata by url path
|
||||||
func getMetadataByPath(apiPath string) ([]byte, error) {
|
func getMetadataByPath(apiPath string) ([]byte, error) {
|
||||||
// See https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/configuring-instance-metadata-service.html
|
// See https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/configuring-instance-metadata-service.html
|
||||||
|
|
||||||
|
client := discoveryutils.GetHTTPClient()
|
||||||
|
|
||||||
// Obtain session token
|
// Obtain session token
|
||||||
sessionTokenURL := "http://169.254.169.254/latest/api/token"
|
sessionTokenURL := "http://169.254.169.254/latest/api/token"
|
||||||
req, err := http.NewRequest("PUT", sessionTokenURL, nil)
|
req, err := http.NewRequest("PUT", sessionTokenURL, nil)
|
||||||
|
@ -238,7 +240,7 @@ func getMetadataByPath(apiPath string) ([]byte, error) {
|
||||||
return nil, fmt.Errorf("cannot create request for IMDSv2 session token at url %q: %w", sessionTokenURL, err)
|
return nil, fmt.Errorf("cannot create request for IMDSv2 session token at url %q: %w", sessionTokenURL, err)
|
||||||
}
|
}
|
||||||
req.Header.Set("X-aws-ec2-metadata-token-ttl-seconds", "60")
|
req.Header.Set("X-aws-ec2-metadata-token-ttl-seconds", "60")
|
||||||
resp, err := discoveryutils.GetHTTPClient().Do(req)
|
resp, err := client.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("cannot obtain IMDSv2 session token from %q; probably, `region` is missing in `ec2_sd_config`; error: %w", sessionTokenURL, err)
|
return nil, fmt.Errorf("cannot obtain IMDSv2 session token from %q; probably, `region` is missing in `ec2_sd_config`; error: %w", sessionTokenURL, err)
|
||||||
}
|
}
|
||||||
|
@ -254,62 +256,81 @@ func getMetadataByPath(apiPath string) ([]byte, error) {
|
||||||
return nil, fmt.Errorf("cannot create request to %q: %w", apiURL, err)
|
return nil, fmt.Errorf("cannot create request to %q: %w", apiURL, err)
|
||||||
}
|
}
|
||||||
req.Header.Set("X-aws-ec2-metadata-token", string(token))
|
req.Header.Set("X-aws-ec2-metadata-token", string(token))
|
||||||
resp, err = discoveryutils.GetHTTPClient().Do(req)
|
resp, err = client.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("cannot obtain response for %q: %w", apiURL, err)
|
return nil, fmt.Errorf("cannot obtain response for %q: %w", apiURL, err)
|
||||||
}
|
}
|
||||||
return readResponseBody(resp, apiURL)
|
return readResponseBody(resp, apiURL)
|
||||||
}
|
}
|
||||||
|
|
||||||
// getRoleARNCredentials - retrieves credentials from aws sts with role_arn
|
// getRoleARNCredentials obtains credentials fo the given roleARN.
|
||||||
func getRoleARNCredentials(region, endpoint, roleARN string, credentials *apiCredentials) (*apiCredentials, error) {
|
func getRoleARNCredentials(region, stsEndpoint, roleARN string, creds *apiCredentials) (*apiCredentials, error) {
|
||||||
|
data, err := getSTSAPIResponse(region, stsEndpoint, roleARN, creds)
|
||||||
data, err := getSTSAPIResponse(region, endpoint, roleARN, credentials)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
scr := &stsCredentialsResponse{}
|
return parseARNCredentials(data)
|
||||||
if err := xml.Unmarshal(data, scr); err != nil {
|
|
||||||
return nil, fmt.Errorf("cannot parse sts api response: %w", err)
|
|
||||||
}
|
|
||||||
return &apiCredentials{
|
|
||||||
AccessKeyID: scr.AssumeRoleResult.Credentials.AccessKeyID,
|
|
||||||
SecretAccessKey: scr.AssumeRoleResult.Credentials.SecretAccessKey,
|
|
||||||
Token: scr.AssumeRoleResult.Credentials.SessionToken,
|
|
||||||
Expiration: scr.AssumeRoleResult.Credentials.Expiration,
|
|
||||||
}, nil
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// buildAPIEndpoint - creates endpoint for aws api access
|
// parseARNCredentials parses apiCredentials from AssumeRole response.
|
||||||
func buildAPIEndpoint(region, cfgEndpoint, service string) string {
|
//
|
||||||
// See https://docs.aws.amazon.com/AWSEC2/latest/APIReference/Query-Requests.html
|
// See https://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRole.html
|
||||||
apiEndpoint := fmt.Sprintf("https://%s.%s.amazonaws.com/", service, region)
|
func parseARNCredentials(data []byte) (*apiCredentials, error) {
|
||||||
if len(cfgEndpoint) > 0 {
|
var arr AssumeRoleResponse
|
||||||
apiEndpoint = cfgEndpoint
|
if err := xml.Unmarshal(data, &arr); err != nil {
|
||||||
// endpoint may contain only hostname. Convert it to proper url then.
|
return nil, fmt.Errorf("cannot parse AssumeRoleResponse response from %q: %w", data, err)
|
||||||
if !strings.Contains(apiEndpoint, "://") {
|
}
|
||||||
apiEndpoint = "https://" + apiEndpoint
|
return &apiCredentials{
|
||||||
}
|
AccessKeyID: arr.AssumeRoleResult.Credentials.AccessKeyID,
|
||||||
if !strings.HasSuffix(apiEndpoint, "/") {
|
SecretAccessKey: arr.AssumeRoleResult.Credentials.SecretAccessKey,
|
||||||
apiEndpoint += "/"
|
Token: arr.AssumeRoleResult.Credentials.SessionToken,
|
||||||
|
Expiration: arr.AssumeRoleResult.Credentials.Expiration,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// AssumeRoleResponse represents AssumeRole response
|
||||||
|
//
|
||||||
|
// See https://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRole.html
|
||||||
|
type AssumeRoleResponse struct {
|
||||||
|
AssumeRoleResult struct {
|
||||||
|
Credentials struct {
|
||||||
|
AccessKeyID string `xml:"AccessKeyId"`
|
||||||
|
SecretAccessKey string `xml:"SecretAccessKey"`
|
||||||
|
SessionToken string `xml:"SessionToken"`
|
||||||
|
Expiration time.Time `xml:"Expiration"`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return apiEndpoint
|
}
|
||||||
|
|
||||||
|
// buildAPIEndpoint creates endpoint for aws api access
|
||||||
|
func buildAPIEndpoint(customEndpoint, region, service string) string {
|
||||||
|
// See https://docs.aws.amazon.com/AWSEC2/latest/APIReference/Query-Requests.html
|
||||||
|
if len(customEndpoint) == 0 {
|
||||||
|
return fmt.Sprintf("https://%s.%s.amazonaws.com/", service, region)
|
||||||
|
}
|
||||||
|
endpoint := customEndpoint
|
||||||
|
// endpoint may contain only hostname. Convert it to proper url then.
|
||||||
|
if !strings.Contains(endpoint, "://") {
|
||||||
|
endpoint = "https://" + endpoint
|
||||||
|
}
|
||||||
|
if !strings.HasSuffix(endpoint, "/") {
|
||||||
|
endpoint += "/"
|
||||||
|
}
|
||||||
|
return endpoint
|
||||||
}
|
}
|
||||||
|
|
||||||
// getSTSAPIResponse makes request to aws sts api with role_arn
|
// getSTSAPIResponse makes request to aws sts api with role_arn
|
||||||
// and returns temporary credentials with expiration time
|
// and returns temporary credentials with expiration time
|
||||||
// https://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRole.html
|
//
|
||||||
func getSTSAPIResponse(region, endpoint, roleARN string, credentials *apiCredentials) ([]byte, error) {
|
// See https://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRole.html
|
||||||
endpoint = buildAPIEndpoint(region, endpoint, "sts")
|
func getSTSAPIResponse(region, stsEndpoint, roleARN string, creds *apiCredentials) ([]byte, error) {
|
||||||
// See https://docs.aws.amazon.com/AWSEC2/latest/APIReference/Query-Requests.html
|
// See https://docs.aws.amazon.com/AWSEC2/latest/APIReference/Query-Requests.html
|
||||||
apiURL := fmt.Sprintf("%s?Action=%s", endpoint, "AssumeRole")
|
apiURL := fmt.Sprintf("%s?Action=%s", stsEndpoint, "AssumeRole")
|
||||||
apiURL += "&Version=2011-06-15"
|
apiURL += "&Version=2011-06-15"
|
||||||
apiURL += fmt.Sprintf("&RoleArn=%s", roleARN)
|
apiURL += fmt.Sprintf("&RoleArn=%s", roleARN)
|
||||||
// we have to provide unique session name for cloudtrail audit
|
// we have to provide unique session name for cloudtrail audit
|
||||||
apiURL += "&RoleSessionName=vmagent-ec2-discovery"
|
apiURL += "&RoleSessionName=vmagent-ec2-discovery"
|
||||||
req, err := newSignedRequest(apiURL, "sts", region, credentials)
|
req, err := newSignedRequest(apiURL, "sts", region, creds)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("cannot create signed request: %w", err)
|
return nil, fmt.Errorf("cannot create signed request: %w", err)
|
||||||
}
|
}
|
||||||
|
@ -321,17 +342,13 @@ func getSTSAPIResponse(region, endpoint, roleARN string, credentials *apiCredent
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// getEC2APIResponse lists ec2 instances with given action
|
// getEC2APIResponse performs EC2 API request with given action.
|
||||||
func getEC2APIResponse(cfg *apiConfig, action, nextPageToken string) ([]byte, error) {
|
func getEC2APIResponse(cfg *apiConfig, action, nextPageToken string) ([]byte, error) {
|
||||||
endpoint := buildAPIEndpoint(cfg.region, cfg.endpoint, "ec2")
|
ac, err := cfg.getFreshAPICredentials()
|
||||||
// refresh api credentials if needed
|
if err != nil {
|
||||||
if cfg.credentialsExpired() {
|
return nil, fmt.Errorf("cannot obtain fresh credentials for EC2 API: %w", err)
|
||||||
if err := cfg.refreshAPICredentials(); err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to update expired aws credentials: %w", err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
apiURL := fmt.Sprintf("%s?Action=%s", cfg.ec2Endpoint, url.QueryEscape(action))
|
||||||
apiURL := fmt.Sprintf("%s?Action=%s", endpoint, url.QueryEscape(action))
|
|
||||||
if len(cfg.filters) > 0 {
|
if len(cfg.filters) > 0 {
|
||||||
apiURL += "&" + cfg.filters
|
apiURL += "&" + cfg.filters
|
||||||
}
|
}
|
||||||
|
@ -339,7 +356,7 @@ func getEC2APIResponse(cfg *apiConfig, action, nextPageToken string) ([]byte, er
|
||||||
apiURL += fmt.Sprintf("&NextToken=%s", url.QueryEscape(nextPageToken))
|
apiURL += fmt.Sprintf("&NextToken=%s", url.QueryEscape(nextPageToken))
|
||||||
}
|
}
|
||||||
apiURL += "&Version=2013-10-15"
|
apiURL += "&Version=2013-10-15"
|
||||||
req, err := newSignedRequest(apiURL, "ec2", cfg.region, cfg.credentials())
|
req, err := newSignedRequest(apiURL, "ec2", cfg.region, ac)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("cannot create signed request: %w", err)
|
return nil, fmt.Errorf("cannot create signed request: %w", err)
|
||||||
}
|
}
|
||||||
|
|
120
lib/promscrape/discovery/ec2/api_test.go
Normal file
120
lib/promscrape/discovery/ec2/api_test.go
Normal file
|
@ -0,0 +1,120 @@
|
||||||
|
package ec2
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestParseMetadataSecurityCredentialsFailure(t *testing.T) {
|
||||||
|
f := func(s string) {
|
||||||
|
t.Helper()
|
||||||
|
creds, err := parseMetadataSecurityCredentials([]byte(s))
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("expecting non-nil error")
|
||||||
|
}
|
||||||
|
if creds != nil {
|
||||||
|
t.Fatalf("expecting nil apiCreds; got %v", creds)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
f("")
|
||||||
|
f("foobar")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseMetadataSecurityCredentialsSuccess(t *testing.T) {
|
||||||
|
// See https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/iam-roles-for-amazon-ec2.html
|
||||||
|
s := `{
|
||||||
|
"Code" : "Success",
|
||||||
|
"LastUpdated" : "2012-04-26T16:39:16Z",
|
||||||
|
"Type" : "AWS-HMAC",
|
||||||
|
"AccessKeyId" : "ASIAIOSFODNN7EXAMPLE",
|
||||||
|
"SecretAccessKey" : "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
|
||||||
|
"Token" : "token",
|
||||||
|
"Expiration" : "2017-05-17T15:09:54Z"
|
||||||
|
}`
|
||||||
|
creds, err := parseMetadataSecurityCredentials([]byte(s))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %s", err)
|
||||||
|
}
|
||||||
|
credsExpected := &apiCredentials{
|
||||||
|
AccessKeyID: "ASIAIOSFODNN7EXAMPLE",
|
||||||
|
SecretAccessKey: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
|
||||||
|
Token: "token",
|
||||||
|
Expiration: mustParseRFC3339("2017-05-17T15:09:54Z"),
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(creds, credsExpected) {
|
||||||
|
t.Fatalf("unexpected creds;\ngot\n%+v\nwant\n%+v", creds, credsExpected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseARNCredentialsFailure(t *testing.T) {
|
||||||
|
f := func(s string) {
|
||||||
|
t.Helper()
|
||||||
|
creds, err := parseARNCredentials([]byte(s))
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("expecting non-nil error")
|
||||||
|
}
|
||||||
|
if creds != nil {
|
||||||
|
t.Fatalf("expecting nil apiCreds; got %v", creds)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
f("")
|
||||||
|
f("foobar")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseARNCredentialsSuccess(t *testing.T) {
|
||||||
|
// See https://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRole.html
|
||||||
|
s := `<AssumeRoleResponse xmlns="https://sts.amazonaws.com/doc/2011-06-15/">
|
||||||
|
<AssumeRoleResult>
|
||||||
|
<AssumedRoleUser>
|
||||||
|
<Arn>arn:aws:sts::123456789012:assumed-role/demo/TestAR</Arn>
|
||||||
|
<AssumedRoleId>ARO123EXAMPLE123:TestAR</AssumedRoleId>
|
||||||
|
</AssumedRoleUser>
|
||||||
|
<Credentials>
|
||||||
|
<AccessKeyId>ASIAIOSFODNN7EXAMPLE</AccessKeyId>
|
||||||
|
<SecretAccessKey>wJalrXUtnFEMI/K7MDENG/bPxRfiCYzEXAMPLEKEY</SecretAccessKey>
|
||||||
|
<SessionToken>
|
||||||
|
AQoDYXdzEPT//////////wEXAMPLEtc764bNrC9SAPBSM22wDOk4x4HIZ8j4FZTwdQW
|
||||||
|
LWsKWHGBuFqwAeMicRXmxfpSPfIeoIYRqTflfKD8YUuwthAx7mSEI/qkPpKPi/kMcGd
|
||||||
|
QrmGdeehM4IC1NtBmUpp2wUE8phUZampKsburEDy0KPkyQDYwT7WZ0wq5VSXDvp75YU
|
||||||
|
9HFvlRd8Tx6q6fE8YQcHNVXAkiY9q6d+xo0rKwT38xVqr7ZD0u0iPPkUL64lIZbqBAz
|
||||||
|
+scqKmlzm8FDrypNC9Yjc8fPOLn9FX9KSYvKTr4rvx3iSIlTJabIQwj2ICCR/oLxBA==
|
||||||
|
</SessionToken>
|
||||||
|
<Expiration>2019-11-09T13:34:41Z</Expiration>
|
||||||
|
</Credentials>
|
||||||
|
<PackedPolicySize>6</PackedPolicySize>
|
||||||
|
</AssumeRoleResult>
|
||||||
|
<ResponseMetadata>
|
||||||
|
<RequestId>c6104cbe-af31-11e0-8154-cbc7ccf896c7</RequestId>
|
||||||
|
</ResponseMetadata>
|
||||||
|
</AssumeRoleResponse>
|
||||||
|
`
|
||||||
|
creds, err := parseARNCredentials([]byte(s))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %s", err)
|
||||||
|
}
|
||||||
|
credsExpected := &apiCredentials{
|
||||||
|
AccessKeyID: "ASIAIOSFODNN7EXAMPLE",
|
||||||
|
SecretAccessKey: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYzEXAMPLEKEY",
|
||||||
|
Token: `
|
||||||
|
AQoDYXdzEPT//////////wEXAMPLEtc764bNrC9SAPBSM22wDOk4x4HIZ8j4FZTwdQW
|
||||||
|
LWsKWHGBuFqwAeMicRXmxfpSPfIeoIYRqTflfKD8YUuwthAx7mSEI/qkPpKPi/kMcGd
|
||||||
|
QrmGdeehM4IC1NtBmUpp2wUE8phUZampKsburEDy0KPkyQDYwT7WZ0wq5VSXDvp75YU
|
||||||
|
9HFvlRd8Tx6q6fE8YQcHNVXAkiY9q6d+xo0rKwT38xVqr7ZD0u0iPPkUL64lIZbqBAz
|
||||||
|
+scqKmlzm8FDrypNC9Yjc8fPOLn9FX9KSYvKTr4rvx3iSIlTJabIQwj2ICCR/oLxBA==
|
||||||
|
`,
|
||||||
|
Expiration: mustParseRFC3339("2019-11-09T13:34:41Z"),
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(creds, credsExpected) {
|
||||||
|
t.Fatalf("unexpected creds;\ngot\n%+v\nwant\n%+v", creds, credsExpected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func mustParseRFC3339(s string) time.Time {
|
||||||
|
expTime, err := time.Parse(time.RFC3339, s)
|
||||||
|
if err != nil {
|
||||||
|
panic(fmt.Errorf("unexpected error when parsing time from %q: %s", s, err))
|
||||||
|
}
|
||||||
|
return expTime
|
||||||
|
}
|
|
@ -16,12 +16,12 @@ import (
|
||||||
// newSignedRequest signed request for apiURL according to aws signature algorithm.
|
// newSignedRequest signed request for apiURL according to aws signature algorithm.
|
||||||
//
|
//
|
||||||
// See the algorithm at https://docs.aws.amazon.com/general/latest/gr/sigv4-signed-request-examples.html
|
// See the algorithm at https://docs.aws.amazon.com/general/latest/gr/sigv4-signed-request-examples.html
|
||||||
func newSignedRequest(apiURL, service, region string, credentials *apiCredentials) (*http.Request, error) {
|
func newSignedRequest(apiURL, service, region string, creds *apiCredentials) (*http.Request, error) {
|
||||||
t := time.Now().UTC()
|
t := time.Now().UTC()
|
||||||
return newSignedRequestWithTime(apiURL, service, region, credentials, t)
|
return newSignedRequestWithTime(apiURL, service, region, creds, t)
|
||||||
}
|
}
|
||||||
|
|
||||||
func newSignedRequestWithTime(apiURL, service, region string, credentials *apiCredentials, t time.Time) (*http.Request, error) {
|
func newSignedRequestWithTime(apiURL, service, region string, creds *apiCredentials, t time.Time) (*http.Request, error) {
|
||||||
uri, err := url.Parse(apiURL)
|
uri, err := url.Parse(apiURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("cannot parse %q: %w", apiURL, err)
|
return nil, fmt.Errorf("cannot parse %q: %w", apiURL, err)
|
||||||
|
@ -57,11 +57,11 @@ func newSignedRequestWithTime(apiURL, service, region string, credentials *apiCr
|
||||||
stringToSign := strings.Join(tmp, "\n")
|
stringToSign := strings.Join(tmp, "\n")
|
||||||
|
|
||||||
// Calculate the signature
|
// Calculate the signature
|
||||||
signingKey := getSignatureKey(credentials.SecretAccessKey, datestamp, region, service)
|
signingKey := getSignatureKey(creds.SecretAccessKey, datestamp, region, service)
|
||||||
signature := hmacHex(signingKey, stringToSign)
|
signature := hmacHex(signingKey, stringToSign)
|
||||||
|
|
||||||
// Calculate autheader
|
// Calculate autheader
|
||||||
authHeader := fmt.Sprintf("%s Credential=%s/%s, SignedHeaders=%s, Signature=%s", algorithm, credentials.AccessKeyID, credentialScope, signedHeaders, signature)
|
authHeader := fmt.Sprintf("%s Credential=%s/%s, SignedHeaders=%s, Signature=%s", algorithm, creds.AccessKeyID, credentialScope, signedHeaders, signature)
|
||||||
|
|
||||||
req, err := http.NewRequest("GET", apiURL, nil)
|
req, err := http.NewRequest("GET", apiURL, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -69,10 +69,9 @@ func newSignedRequestWithTime(apiURL, service, region string, credentials *apiCr
|
||||||
}
|
}
|
||||||
req.Header.Set("x-amz-date", amzdate)
|
req.Header.Set("x-amz-date", amzdate)
|
||||||
req.Header.Set("Authorization", authHeader)
|
req.Header.Set("Authorization", authHeader)
|
||||||
if credentials.Token != "" {
|
if creds.Token != "" {
|
||||||
req.Header.Set("X-Amz-Security-Token", credentials.Token)
|
req.Header.Set("X-Amz-Security-Token", creds.Token)
|
||||||
}
|
}
|
||||||
|
|
||||||
return req, nil
|
return req, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue