diff --git a/cluster-autoscaler/cloudprovider/alicloud/alibaba-cloud-sdk-go/sdk/auth/credentials/oidc_credential.go b/cluster-autoscaler/cloudprovider/alicloud/alibaba-cloud-sdk-go/sdk/auth/credentials/oidc_credential.go new file mode 100644 index 000000000000..2d23de3cd98b --- /dev/null +++ b/cluster-autoscaler/cloudprovider/alicloud/alibaba-cloud-sdk-go/sdk/auth/credentials/oidc_credential.go @@ -0,0 +1,37 @@ +/* +Copyright 2018 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package credentials + +// OIDCCredential is a kind of credentials +type OIDCCredential struct { + RoleArn string + OIDCProviderArn string + OIDCTokenFilePath string + RoleSessionName string + RoleSessionExpiration int +} + +// NewOIDCRoleArnCredential returns OIDCCredential +func NewOIDCRoleArnCredential(roleArn, OIDCProviderArn, OIDCTokenFilePath, RoleSessionName string, RoleSessionExpiration int) *OIDCCredential { + return &OIDCCredential{ + RoleArn: roleArn, + OIDCProviderArn: OIDCProviderArn, + OIDCTokenFilePath: OIDCTokenFilePath, + RoleSessionName: RoleSessionName, + RoleSessionExpiration: RoleSessionExpiration, + } +} diff --git a/cluster-autoscaler/cloudprovider/alicloud/alibaba-cloud-sdk-go/sdk/auth/signer.go b/cluster-autoscaler/cloudprovider/alicloud/alibaba-cloud-sdk-go/sdk/auth/signer.go index 424db3b3cd3d..3f732a36f7f5 100644 --- a/cluster-autoscaler/cloudprovider/alicloud/alibaba-cloud-sdk-go/sdk/auth/signer.go +++ b/cluster-autoscaler/cloudprovider/alicloud/alibaba-cloud-sdk-go/sdk/auth/signer.go @@ -61,6 +61,10 @@ func NewSignerWithCredential(credential Credential, commonApi func(request *requ { signer, err = signers.NewEcsRamRoleSigner(instance, commonApi) } + case *credentials.OIDCCredential: + { + signer, err = signers.NewOIDCSigner(instance) + } case *credentials.BaseCredential: // deprecated user interface { signer, err = signers.NewAccessKeySigner(instance.ToAccessKeyCredential()) diff --git a/cluster-autoscaler/cloudprovider/alicloud/alibaba-cloud-sdk-go/sdk/auth/signers/signer_oidc.go b/cluster-autoscaler/cloudprovider/alicloud/alibaba-cloud-sdk-go/sdk/auth/signers/signer_oidc.go new file mode 100644 index 000000000000..930e9612e4dc --- /dev/null +++ b/cluster-autoscaler/cloudprovider/alicloud/alibaba-cloud-sdk-go/sdk/auth/signers/signer_oidc.go @@ -0,0 +1,249 @@ +/* +Copyright 2018 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package signers + +import ( + "encoding/json" + "fmt" + "github.com/jmespath/go-jmespath" + "k8s.io/autoscaler/cluster-autoscaler/cloudprovider/alicloud/alibaba-cloud-sdk-go/sdk/auth/credentials" + "k8s.io/autoscaler/cluster-autoscaler/cloudprovider/alicloud/alibaba-cloud-sdk-go/sdk/errors" + "k8s.io/autoscaler/cluster-autoscaler/cloudprovider/alicloud/alibaba-cloud-sdk-go/sdk/requests" + "k8s.io/autoscaler/cluster-autoscaler/cloudprovider/alicloud/alibaba-cloud-sdk-go/sdk/responses" + "k8s.io/autoscaler/cluster-autoscaler/cloudprovider/alicloud/alibaba-cloud-sdk-go/sdk/utils" + "k8s.io/klog/v2" + "net/http" + "os" + "runtime" + "strconv" + "strings" + "time" +) + +const ( + defaultOIDCDurationSeconds = 3600 +) + +// OIDCSigner is kind of signer +type OIDCSigner struct { + *credentialUpdater + roleSessionName string + sessionCredential *SessionCredential + credential *credentials.OIDCCredential +} + +// NewOIDCSigner returns OIDCSigner +func NewOIDCSigner(credential *credentials.OIDCCredential) (signer *OIDCSigner, err error) { + signer = &OIDCSigner{ + credential: credential, + } + + signer.credentialUpdater = &credentialUpdater{ + credentialExpiration: credential.RoleSessionExpiration, + buildRequestMethod: signer.buildCommonRequest, + responseCallBack: signer.refreshCredential, + refreshApi: signer.refreshApi, + } + + if len(credential.RoleSessionName) > 0 { + signer.roleSessionName = credential.RoleSessionName + } else { + signer.roleSessionName = "kubernetes-cluster-autoscaler-" + strconv.FormatInt(time.Now().UnixNano()/1000, 10) + } + if credential.RoleSessionExpiration > 0 { + if credential.RoleSessionExpiration >= 900 && credential.RoleSessionExpiration <= 3600 { + signer.credentialExpiration = credential.RoleSessionExpiration + } else { + err = errors.NewClientError(errors.InvalidParamErrorCode, "Assume Role session duration should be in the range of 15min - 1Hr", nil) + } + } else { + signer.credentialExpiration = defaultOIDCDurationSeconds + } + return +} + +// GetName returns "HMAC-SHA1" +func (*OIDCSigner) GetName() string { + return "HMAC-SHA1" +} + +// GetType returns "" +func (*OIDCSigner) GetType() string { + return "" +} + +// GetVersion returns "1.0" +func (*OIDCSigner) GetVersion() string { + return "1.0" +} + +// GetAccessKeyId returns accessKeyId +func (signer *OIDCSigner) GetAccessKeyId() (accessKeyId string, err error) { + if signer.sessionCredential == nil || signer.needUpdateCredential() { + err = signer.updateCredential() + } + if err != nil && (signer.sessionCredential == nil || len(signer.sessionCredential.AccessKeyId) <= 0) { + return "", err + } + + return signer.sessionCredential.AccessKeyId, nil +} + +// GetExtraParam returns params +func (signer *OIDCSigner) GetExtraParam() map[string]string { + if signer.sessionCredential == nil || signer.needUpdateCredential() { + signer.updateCredential() + } + if signer.sessionCredential == nil || len(signer.sessionCredential.StsToken) <= 0 { + return make(map[string]string) + } + + return map[string]string{"SecurityToken": signer.sessionCredential.StsToken} +} + +// Sign create signer +func (signer *OIDCSigner) Sign(stringToSign, secretSuffix string) string { + secret := signer.sessionCredential.AccessKeySecret + secretSuffix + return ShaHmac1(stringToSign, secret) +} + +func (signer *OIDCSigner) buildCommonRequest() (request *requests.CommonRequest, err error) { + const endpoint = "sts.aliyuncs.com" + const stsApiVersion = "2015-04-01" + const action = "AssumeRoleWithOIDC" + request = requests.NewCommonRequest() + request.Scheme = requests.HTTPS + request.Domain = endpoint + request.Method = requests.POST + request.QueryParams["Action"] = action + request.QueryParams["Version"] = stsApiVersion + request.QueryParams["Format"] = "JSON" + request.QueryParams["Timestamp"] = utils.GetTimeInFormatISO8601() + request.QueryParams["SignatureNonce"] = utils.GetUUIDV4() + request.FormParams["RoleArn"] = signer.credential.RoleArn + request.FormParams["OIDCProviderArn"] = signer.credential.OIDCProviderArn + request.FormParams["OIDCToken"] = signer.getOIDCToken(signer.credential.OIDCTokenFilePath) + request.QueryParams["RoleSessionName"] = signer.credential.RoleSessionName + request.Headers["host"] = endpoint + request.Headers["Accept-Encoding"] = "identity" + request.Headers["content-type"] = "application/x-www-form-urlencoded" + request.Headers["user-agent"] = fmt.Sprintf("AlibabaCloud (%s; %s) Golang/%s Core/%s TeaDSL/1 kubernetes-cluster-autoscaler", runtime.GOOS, runtime.GOARCH, strings.Trim(runtime.Version(), "go"), "0.01") + return +} + +func (signer *OIDCSigner) getOIDCToken(OIDCTokenFilePath string) string { + tokenPath := OIDCTokenFilePath + _, err := os.Stat(tokenPath) + if os.IsNotExist(err) { + tokenPath = os.Getenv("ALIBABA_CLOUD_OIDC_TOKEN_FILE") + if tokenPath == "" { + klog.Error("oidc token file path is missing") + return "" + } + } + + token, err := os.ReadFile(tokenPath) + if err != nil { + klog.Errorf("get oidc token from file %s failed: %s", tokenPath, err) + return "" + } + return string(token) +} + +func (signer *OIDCSigner) refreshApi(request *requests.CommonRequest) (response *responses.CommonResponse, err error) { + body := utils.GetUrlFormedMap(request.FormParams) + httpRequest, err := http.NewRequest(request.Method, fmt.Sprintf("%s://%s/?%s", strings.ToLower(request.Scheme), request.Domain, utils.GetUrlFormedMap(request.QueryParams)), strings.NewReader(body)) + if err != nil { + klog.Errorf("refresh RRSA token failed: %s", err) + return + } + + httpRequest.Proto = "HTTP/1.1" + httpRequest.Host = request.Domain + for k, v := range request.Headers { + httpRequest.Header.Add(k, v) + } + + httpClient := &http.Client{} + httpResponse, err := httpClient.Do(httpRequest) + if err != nil { + klog.Errorf("refresh RRSA token failed: %s", err) + return + } + + response = responses.NewCommonResponse() + err = responses.Unmarshal(response, httpResponse, "") + + return +} + +func (signer *OIDCSigner) refreshCredential(response *responses.CommonResponse) (err error) { + if response.GetHttpStatus() != http.StatusOK { + message := "refresh RRSA failed" + err = errors.NewServerError(response.GetHttpStatus(), response.GetHttpContentString(), message) + return + } + + var data interface{} + err = json.Unmarshal(response.GetHttpContentBytes(), &data) + if err != nil { + klog.Errorf("refresh RRSA token err, json.Unmarshal fail: %s", err) + return + } + accessKeyId, err := jmespath.Search("Credentials.AccessKeyId", data) + if err != nil { + klog.Errorf("refresh RRSA token err, fail to get AccessKeyId: %s", err) + return + } + accessKeySecret, err := jmespath.Search("Credentials.AccessKeySecret", data) + if err != nil { + klog.Errorf("refresh RRSA token err, fail to get AccessKeySecret: %s", err) + return + } + securityToken, err := jmespath.Search("Credentials.SecurityToken", data) + if err != nil { + klog.Errorf("refresh RRSA token err, fail to get SecurityToken: %s", err) + return + } + expiration, err := jmespath.Search("Credentials.Expiration", data) + if err != nil { + klog.Errorf("refresh RRSA token err, fail to get Expiration: %s", err) + return + } + + if accessKeyId == nil || accessKeySecret == nil || securityToken == nil { + return + } + + expirationTime, err := time.Parse("2006-01-02T15:04:05Z", expiration.(string)) + signer.credentialExpiration = int(expirationTime.Unix() - time.Now().Unix()) + signer.sessionCredential = &SessionCredential{ + AccessKeyId: accessKeyId.(string), + AccessKeySecret: accessKeySecret.(string), + StsToken: securityToken.(string), + } + + return +} + +// GetSessionCredential returns SessionCredential +func (signer *OIDCSigner) GetSessionCredential() *SessionCredential { + return signer.sessionCredential +} + +// Shutdown doesn't implement +func (signer *OIDCSigner) Shutdown() {} diff --git a/cluster-autoscaler/cloudprovider/alicloud/alibaba-cloud-sdk-go/sdk/client.go b/cluster-autoscaler/cloudprovider/alicloud/alibaba-cloud-sdk-go/sdk/client.go index 9ba0123d0475..9870f1210587 100644 --- a/cluster-autoscaler/cloudprovider/alicloud/alibaba-cloud-sdk-go/sdk/client.go +++ b/cluster-autoscaler/cloudprovider/alicloud/alibaba-cloud-sdk-go/sdk/client.go @@ -150,6 +150,18 @@ func (client *Client) InitWithEcsRamRole(regionId, roleName string) (err error) return client.InitWithOptions(regionId, config, credential) } +// InitWithRRSA need regionId,roleARN,oidcProviderARN,oidcTokenFilePath and roleSessionName +func (client *Client) InitWithRRSA(regionId, roleARN, oidcProviderARN, oidcTokenFilePath, roleSessionName string) (err error) { + config := client.InitClientConfig() + credential := &credentials.OIDCCredential{ + RoleArn: roleARN, + OIDCProviderArn: oidcProviderARN, + OIDCTokenFilePath: oidcTokenFilePath, + RoleSessionName: roleSessionName, + } + return client.InitWithOptions(regionId, config, credential) +} + // InitClientConfig init client config func (client *Client) InitClientConfig() (config *Config) { if client.config != nil { @@ -395,6 +407,13 @@ func NewClientWithEcsRamRole(regionId string, roleName string) (client *Client, return } +// NewClientWithRRSA create client with RRSA on ECS +func NewClientWithRRSA(regionId, roleARN, oidcProviderARN, oidcTokenFilePath, roleSessionName string) (client *Client, err error) { + client = &Client{} + err = client.InitWithRRSA(regionId, roleARN, oidcProviderARN, oidcTokenFilePath, roleSessionName) + return +} + // NewClientWithRsaKeyPair create client with key-pair func NewClientWithRsaKeyPair(regionId string, publicKeyId, privateKey string, sessionExpiration int) (client *Client, err error) { client = &Client{} diff --git a/cluster-autoscaler/cloudprovider/alicloud/alibaba-cloud-sdk-go/sdk/client_test.go b/cluster-autoscaler/cloudprovider/alicloud/alibaba-cloud-sdk-go/sdk/client_test.go new file mode 100644 index 000000000000..5b1ca237b5c1 --- /dev/null +++ b/cluster-autoscaler/cloudprovider/alicloud/alibaba-cloud-sdk-go/sdk/client_test.go @@ -0,0 +1,35 @@ +/* +Copyright 2018 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package sdk + +import ( + "github.com/stretchr/testify/assert" + "k8s.io/autoscaler/cluster-autoscaler/cloudprovider/alicloud/alibaba-cloud-sdk-go/sdk/auth/signers" + "testing" +) + +func TestRRSAClientInit(t *testing.T) { + oidcProviderARN := "acs:ram::12345:oidc-provider/ack-rrsa-cb123" + oidcTokenFilePath := "/var/run/secrets/tokens/oidc-token" + roleARN := "acs:ram::12345:role/autoscaler-role" + roleSessionName := "session" + regionId := "cn-hangzhou" + + client, err := NewClientWithRRSA(regionId, roleARN, oidcProviderARN, oidcTokenFilePath, roleSessionName) + assert.NoError(t, err) + assert.IsType(t, &signers.OIDCSigner{}, client.signer) +} diff --git a/cluster-autoscaler/cloudprovider/alicloud/alibaba-cloud-sdk-go/services/ecs/client.go b/cluster-autoscaler/cloudprovider/alicloud/alibaba-cloud-sdk-go/services/ecs/client.go index 3d43f1ff45df..170830bfe22f 100644 --- a/cluster-autoscaler/cloudprovider/alicloud/alibaba-cloud-sdk-go/services/ecs/client.go +++ b/cluster-autoscaler/cloudprovider/alicloud/alibaba-cloud-sdk-go/services/ecs/client.go @@ -80,3 +80,10 @@ func NewClientWithRsaKeyPair(regionId string, publicKeyId, privateKey string, se err = client.InitWithRsaKeyPair(regionId, publicKeyId, privateKey, sessionExpiration) return } + +// NewClientWithRRSA is a shortcut to create sdk client with RRSA +func NewClientWithRRSA(regionId, roleARN, oidcProviderARN, oidcTokenFilePath, roleSessionName string) (client *Client, err error) { + client = &Client{} + err = client.InitWithRRSA(regionId, roleARN, oidcProviderARN, oidcTokenFilePath, roleSessionName) + return +} diff --git a/cluster-autoscaler/cloudprovider/alicloud/alibaba-cloud-sdk-go/services/ess/client.go b/cluster-autoscaler/cloudprovider/alicloud/alibaba-cloud-sdk-go/services/ess/client.go index cf19bb4bfdce..3be994bffe65 100755 --- a/cluster-autoscaler/cloudprovider/alicloud/alibaba-cloud-sdk-go/services/ess/client.go +++ b/cluster-autoscaler/cloudprovider/alicloud/alibaba-cloud-sdk-go/services/ess/client.go @@ -80,3 +80,10 @@ func NewClientWithRsaKeyPair(regionId string, publicKeyId, privateKey string, se err = client.InitWithRsaKeyPair(regionId, publicKeyId, privateKey, sessionExpiration) return } + +// NewClientWithRRSA is a shortcut to create sdk client with RRSA +func NewClientWithRRSA(regionId, roleARN, oidcProviderARN, oidcTokenFilePath, roleSessionName string) (client *Client, err error) { + client = &Client{} + err = client.InitWithRRSA(regionId, roleARN, oidcProviderARN, oidcTokenFilePath, roleSessionName) + return +} diff --git a/cluster-autoscaler/cloudprovider/alicloud/alicloud_auto_scaling.go b/cluster-autoscaler/cloudprovider/alicloud/alicloud_auto_scaling.go index 8ccadf572478..4f974be0082a 100644 --- a/cluster-autoscaler/cloudprovider/alicloud/alicloud_auto_scaling.go +++ b/cluster-autoscaler/cloudprovider/alicloud/alicloud_auto_scaling.go @@ -52,7 +52,7 @@ func newAutoScalingWrapper(cfg *cloudConfig) (*autoScalingWrapper, error) { asw := &autoScalingWrapper{ cfg: cfg, } - if cfg.STSEnabled == true { + if cfg.STSEnabled { go func(asw *autoScalingWrapper, cfg *cloudConfig) { timer := time.NewTicker(refreshClientInterval) defer timer.Stop() @@ -76,7 +76,7 @@ func newAutoScalingWrapper(cfg *cloudConfig) (*autoScalingWrapper, error) { func getEssClient(cfg *cloudConfig) (client *ess.Client, err error) { region := cfg.getRegion() - if cfg.STSEnabled == true { + if cfg.STSEnabled { auth, err := cfg.getSTSToken() if err != nil { klog.Errorf("Failed to get sts token from metadata,Because of %s", err.Error()) @@ -86,6 +86,11 @@ func getEssClient(cfg *cloudConfig) (client *ess.Client, err error) { if err != nil { klog.Errorf("Failed to create client with sts in metadata because of %s", err.Error()) } + } else if cfg.RRSAEnabled { + client, err = ess.NewClientWithRRSA(region, cfg.RoleARN, cfg.OIDCProviderARN, cfg.OIDCTokenFilePath, cfg.RoleSessionName) + if err != nil { + klog.Errorf("Failed to create ess client with RRSA, because of %s", err.Error()) + } } else { client, err = ess.NewClientWithAccessKey(region, cfg.AccessKeyID, cfg.AccessKeySecret) if err != nil { diff --git a/cluster-autoscaler/cloudprovider/alicloud/alicloud_auto_scaling_test.go b/cluster-autoscaler/cloudprovider/alicloud/alicloud_auto_scaling_test.go new file mode 100644 index 000000000000..409fc4871b8a --- /dev/null +++ b/cluster-autoscaler/cloudprovider/alicloud/alicloud_auto_scaling_test.go @@ -0,0 +1,38 @@ +/* +Copyright 2018 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package alicloud + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +func TestRRSACloudConfigEssClientCreation(t *testing.T) { + t.Setenv(oidcProviderARN, "acs:ram::12345:oidc-provider/ack-rrsa-cb123") + t.Setenv(oidcTokenFilePath, "/var/run/secrets/tokens/oidc-token") + t.Setenv(roleARN, "acs:ram::12345:role/autoscaler-role") + t.Setenv(roleSessionName, "session") + t.Setenv(regionId, "cn-hangzhou") + + cfg := &cloudConfig{} + assert.True(t, cfg.isValid()) + assert.True(t, cfg.RRSAEnabled) + + client, err := getEssClient(cfg) + assert.NoError(t, err) + assert.NotNil(t, client) +} diff --git a/cluster-autoscaler/cloudprovider/alicloud/alicloud_cloud_config.go b/cluster-autoscaler/cloudprovider/alicloud/alicloud_cloud_config.go index 36d518dac520..9a624459163d 100644 --- a/cluster-autoscaler/cloudprovider/alicloud/alicloud_cloud_config.go +++ b/cluster-autoscaler/cloudprovider/alicloud/alicloud_cloud_config.go @@ -18,21 +18,30 @@ package alicloud import ( "k8s.io/autoscaler/cluster-autoscaler/cloudprovider/alicloud/metadata" - klog "k8s.io/klog/v2" + "k8s.io/klog/v2" "os" ) const ( - accessKeyId = "ACCESS_KEY_ID" - accessKeySecret = "ACCESS_KEY_SECRET" - regionId = "REGION_ID" + accessKeyId = "ACCESS_KEY_ID" + accessKeySecret = "ACCESS_KEY_SECRET" + oidcProviderARN = "ALICLOUD_OIDC_PROVIDER_ARN" + oidcTokenFilePath = "ALICLOUD_OIDC_TOKEN_FILE_PATH" + roleARN = "ALICLOUD_ROLE_ARN" + roleSessionName = "ALICLOUD_SESSION_NAME" + regionId = "REGION_ID" ) type cloudConfig struct { - RegionId string - AccessKeyID string - AccessKeySecret string - STSEnabled bool + RegionId string + AccessKeyID string + AccessKeySecret string + OIDCProviderARN string + OIDCTokenFilePath string + RoleARN string + RoleSessionName string + RRSAEnabled bool + STSEnabled bool } func (cc *cloudConfig) isValid() bool { @@ -48,18 +57,40 @@ func (cc *cloudConfig) isValid() bool { cc.RegionId = os.Getenv(regionId) } - if cc.RegionId == "" || cc.AccessKeyID == "" || cc.AccessKeySecret == "" { - klog.V(5).Infof("Failed to get AccessKeyId:%s,AccessKeySecret:%s,RegionId:%s from cloudConfig and Env\n", cc.AccessKeyID, cc.AccessKeySecret, cc.RegionId) + if cc.OIDCProviderARN == "" { + cc.OIDCProviderARN = os.Getenv(oidcProviderARN) + } + + if cc.OIDCTokenFilePath == "" { + cc.OIDCTokenFilePath = os.Getenv(oidcTokenFilePath) + } + + if cc.RoleARN == "" { + cc.RoleARN = os.Getenv(roleARN) + } + + if cc.RoleSessionName == "" { + cc.RoleSessionName = os.Getenv(roleSessionName) + } + + if cc.RegionId != "" && cc.AccessKeyID != "" && cc.AccessKeySecret != "" { + klog.V(2).Info("Using AccessKey authentication") + return true + } else if cc.RegionId != "" && cc.OIDCProviderARN != "" && cc.OIDCTokenFilePath != "" && cc.RoleARN != "" && cc.RoleSessionName != "" { + klog.V(2).Info("Using RRSA authentication") + cc.RRSAEnabled = true + return true + } else { + klog.V(5).Infof("Failed to get AccessKeyId:%s,RegionId:%s from cloudConfig and Env\n", cc.AccessKeyID, cc.RegionId) + klog.V(5).Infof("Failed to get OIDCProviderARN:%s,OIDCTokenFilePath:%s,RoleARN:%s,RoleSessionName:%s,RegionId:%s from cloudConfig and Env\n", cc.OIDCProviderARN, cc.OIDCTokenFilePath, cc.RoleARN, cc.RoleSessionName, cc.RegionId) klog.V(5).Infof("Try to use sts token in metadata instead.\n") - if cc.validateSTSToken() == true && cc.getRegion() != "" { + if cc.validateSTSToken() && cc.getRegion() != "" { //if CA is working on ECS with valid role name, use sts token instead. cc.STSEnabled = true return true } - } else { - cc.STSEnabled = false - return true } + return false } diff --git a/cluster-autoscaler/cloudprovider/alicloud/alicloud_cloud_config_test.go b/cluster-autoscaler/cloudprovider/alicloud/alicloud_cloud_config_test.go new file mode 100644 index 000000000000..4c828367c5f0 --- /dev/null +++ b/cluster-autoscaler/cloudprovider/alicloud/alicloud_cloud_config_test.go @@ -0,0 +1,44 @@ +/* +Copyright 2018 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package alicloud + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +func TestAccessKeyCloudConfigIsValid(t *testing.T) { + t.Setenv(accessKeyId, "id") + t.Setenv(accessKeySecret, "secret") + t.Setenv(regionId, "cn-hangzhou") + + cfg := &cloudConfig{} + assert.True(t, cfg.isValid()) + assert.False(t, cfg.RRSAEnabled) +} + +func TestRRSACloudConfigIsValid(t *testing.T) { + t.Setenv(oidcProviderARN, "acs:ram::12345:oidc-provider/ack-rrsa-cb123") + t.Setenv(oidcTokenFilePath, "/var/run/secrets/tokens/oidc-token") + t.Setenv(roleARN, "acs:ram::12345:role/autoscaler-role") + t.Setenv(roleSessionName, "session") + t.Setenv(regionId, "cn-hangzhou") + + cfg := &cloudConfig{} + assert.True(t, cfg.isValid()) + assert.True(t, cfg.RRSAEnabled) +} diff --git a/cluster-autoscaler/cloudprovider/alicloud/alicloud_instance_types.go b/cluster-autoscaler/cloudprovider/alicloud/alicloud_instance_types.go index 3f09394d78b9..966d4b6ed00a 100644 --- a/cluster-autoscaler/cloudprovider/alicloud/alicloud_instance_types.go +++ b/cluster-autoscaler/cloudprovider/alicloud/alicloud_instance_types.go @@ -107,7 +107,7 @@ func newInstanceWrapper(cfg *cloudConfig) (*instanceWrapper, error) { return nil, fmt.Errorf("your cloud config is not valid") } iw := &instanceWrapper{} - if cfg.STSEnabled == true { + if cfg.STSEnabled { go func(iw *instanceWrapper, cfg *cloudConfig) { timer := time.NewTicker(refreshClientInterval) defer timer.Stop() @@ -141,6 +141,11 @@ func getEcsClient(cfg *cloudConfig) (client *ecs.Client, err error) { if err != nil { klog.Errorf("failed to create client with sts in metadata,because of %s", err.Error()) } + } else if cfg.RRSAEnabled { + client, err = ecs.NewClientWithRRSA(region, cfg.RoleARN, cfg.OIDCProviderARN, cfg.OIDCTokenFilePath, cfg.RoleSessionName) + if err != nil { + klog.Errorf("Failed to create ess client with RRSA, because of %s", err.Error()) + } } else { client, err = ecs.NewClientWithAccessKey(region, cfg.AccessKeyID, cfg.AccessKeySecret) if err != nil { diff --git a/cluster-autoscaler/cloudprovider/alicloud/alicloud_instance_types_test.go b/cluster-autoscaler/cloudprovider/alicloud/alicloud_instance_types_test.go new file mode 100644 index 000000000000..eaf5cdeec516 --- /dev/null +++ b/cluster-autoscaler/cloudprovider/alicloud/alicloud_instance_types_test.go @@ -0,0 +1,38 @@ +/* +Copyright 2018 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package alicloud + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +func TestRRSACloudConfigEcsClientCreation(t *testing.T) { + t.Setenv(oidcProviderARN, "acs:ram::12345:oidc-provider/ack-rrsa-cb123") + t.Setenv(oidcTokenFilePath, "/var/run/secrets/tokens/oidc-token") + t.Setenv(roleARN, "acs:ram::12345:role/autoscaler-role") + t.Setenv(roleSessionName, "session") + t.Setenv(regionId, "cn-hangzhou") + + cfg := &cloudConfig{} + assert.True(t, cfg.isValid()) + assert.True(t, cfg.RRSAEnabled) + + client, err := getEcsClient(cfg) + assert.NoError(t, err) + assert.NotNil(t, client) +} diff --git a/cluster-autoscaler/cloudprovider/alicloud/examples/cluster-autoscaler-rrsa-standard.yaml b/cluster-autoscaler/cloudprovider/alicloud/examples/cluster-autoscaler-rrsa-standard.yaml new file mode 100644 index 000000000000..bd7d3220ad4c --- /dev/null +++ b/cluster-autoscaler/cloudprovider/alicloud/examples/cluster-autoscaler-rrsa-standard.yaml @@ -0,0 +1,203 @@ +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + labels: + k8s-addon: cluster-autoscaler.addons.k8s.io + k8s-app: cluster-autoscaler + name: cluster-autoscaler + namespace: kube-system +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: cluster-autoscaler + labels: + k8s-addon: cluster-autoscaler.addons.k8s.io + k8s-app: cluster-autoscaler +rules: + - apiGroups: [""] + resources: ["events", "endpoints"] + verbs: ["create", "patch"] + - apiGroups: [""] + resources: ["pods/eviction"] + verbs: ["create"] + - apiGroups: [""] + resources: ["pods/status"] + verbs: ["update"] + - apiGroups: [""] + resources: ["endpoints"] + resourceNames: ["cluster-autoscaler"] + verbs: ["get", "update"] + - apiGroups: [""] + resources: ["nodes"] + verbs: ["watch", "list", "get", "update"] + - apiGroups: [""] + resources: + - "namespaces" + - "pods" + - "services" + - "replicationcontrollers" + - "persistentvolumeclaims" + - "persistentvolumes" + verbs: ["watch", "list", "get"] + - apiGroups: ["extensions"] + resources: ["replicasets", "daemonsets"] + verbs: ["watch", "list", "get"] + - apiGroups: ["policy"] + resources: ["poddisruptionbudgets"] + verbs: ["watch", "list"] + - apiGroups: ["apps"] + resources: ["statefulsets", "replicasets", "daemonsets"] + verbs: ["watch", "list", "get"] + - apiGroups: ["storage.k8s.io"] + resources: ["storageclasses", "csinodes", "csistoragecapacities", "csidrivers"] + verbs: ["watch", "list", "get"] + - apiGroups: ["batch", "extensions"] + resources: ["jobs"] + verbs: ["get", "list", "watch", "patch"] + - apiGroups: ["coordination.k8s.io"] + resources: ["leases"] + verbs: ["*"] + +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: cluster-autoscaler + namespace: kube-system + labels: + k8s-addon: cluster-autoscaler.addons.k8s.io + k8s-app: cluster-autoscaler +rules: +- apiGroups: [""] + resources: ["configmaps"] + verbs: ["create","list","watch"] +- apiGroups: [""] + resources: ["configmaps"] + resourceNames: ["cluster-autoscaler-status", "cluster-autoscaler-priority-expander"] + verbs: ["delete","get","update","watch"] + +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: cluster-autoscaler + labels: + k8s-addon: cluster-autoscaler.addons.k8s.io + k8s-app: cluster-autoscaler +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: cluster-autoscaler +subjects: + - kind: ServiceAccount + name: cluster-autoscaler + namespace: kube-system + +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: cluster-autoscaler + namespace: kube-system + labels: + k8s-addon: cluster-autoscaler.addons.k8s.io + k8s-app: cluster-autoscaler +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: cluster-autoscaler +subjects: + - kind: ServiceAccount + name: cluster-autoscaler + namespace: kube-system + +--- +apiVersion: v1 +kind: Secret +metadata: + name: cloud-config + namespace: kube-system +type: Opaque +data: + oidc-provider-arn: [YOUR_BASE64_OIDC_PROVIDER_ARN] + oidc-token-file-path: [YOUR_BASE64_OIDC_TOKEN_FILE_PATH] + role-arn: [YOUR_BASE64_ROLE_ARN] + session-name: [YOUR_BASE64_SESSION_NAME] + region-id: [YOUR_BASE64_REGION_ID] + +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: cluster-autoscaler + namespace: kube-system + labels: + app: cluster-autoscaler +spec: + replicas: 1 + selector: + matchLabels: + app: cluster-autoscaler + template: + metadata: + labels: + app: cluster-autoscaler + spec: + priorityClassName: system-cluster-critical + serviceAccountName: cluster-autoscaler + containers: + - image: registry.cn-hangzhou.aliyuncs.com/acs/autoscaler:v1.3.1 + name: cluster-autoscaler + resources: + limits: + cpu: 100m + memory: 300Mi + requests: + cpu: 100m + memory: 300Mi + command: + - ./cluster-autoscaler + - --v=4 + - --stderrthreshold=info + - --cloud-provider=alicloud + - --nodes=[min]:[max]:[ASG_ID] + imagePullPolicy: "Always" + env: + - name: ALICLOUD_OIDC_PROVIDER_ARN + valueFrom: + secretKeyRef: + name: cloud-config + key: oidc-provider-arn + - name: ALICLOUD_OIDC_TOKEN_FILE_PATH + valueFrom: + secretKeyRef: + name: cloud-config + key: oidc-token-file-path + - name: ALICLOUD_ROLE_ARN + valueFrom: + secretKeyRef: + name: cloud-config + key: role-arn + - name: ALICLOUD_SESSION_NAME + valueFrom: + secretKeyRef: + name: cloud-config + key: session-name + - name: REGION_ID + valueFrom: + secretKeyRef: + name: cloud-config + key: region-id + volumeMounts: + - name: oidc-token + mountPath: /var/run/secrets/tokens + volumes: + - name: oidc-token + projected: + sources: + - serviceAccountToken: + path: oidc-token + expirationSeconds: 7200 # The validity period of the OIDC token in seconds. + audience: "sts.aliyuncs.com"