diff --git a/builder/tencentcloud/cvm/access_config.go b/builder/tencentcloud/cvm/access_config.go index 66a376d9..89545399 100644 --- a/builder/tencentcloud/cvm/access_config.go +++ b/builder/tencentcloud/cvm/access_config.go @@ -8,16 +8,11 @@ package cvm import ( "context" - "encoding/json" "fmt" - "io/ioutil" "os" - "runtime" "strconv" - "strings" "github.com/hashicorp/packer-plugin-sdk/template/interpolate" - "github.com/mitchellh/go-homedir" cvm "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/cvm/v20170312" vpc "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/vpc/v20170312" ) @@ -222,35 +217,23 @@ func (cf *TencentCloudAccessConfig) Config() error { cf.SharedCredentialsDir = os.Getenv(PACKER_SHARED_CREDENTIALS_DIR) } - var value map[string]interface{} - var err error - getProviderConfig := func(key string) string { - var str string - if value != nil { - if v, ok := value[key]; ok { - str = v.(string) - } - } - return str - } - if cf.SecretId == "" || cf.SecretKey == "" { - value, err = loadConfigProfile(cf) + profile, err := loadConfigProfile(cf) if err != nil { return err } if cf.SecretId == "" { - cf.SecretId = getProviderConfig("secretId") + cf.SecretId = profile.SecretId } if cf.SecretKey == "" { - cf.SecretKey = getProviderConfig("secretKey") + cf.SecretKey = profile.SecretKey } if cf.SecurityToken == "" { - cf.SecurityToken = getProviderConfig("securityToken") + cf.SecurityToken = profile.Token } if cf.Region == "" { - cf.Region = getProviderConfig("region") + cf.Region = profile.Region } } @@ -293,33 +276,29 @@ func (cf *TencentCloudAccessConfig) Config() error { } if cf.AssumeRole.RoleArn == "" || cf.AssumeRole.SessionName == "" { - value, err = loadConfigProfile(cf) + profile, err := loadConfigProfile(cf) if err != nil { return err } if cf.AssumeRole.RoleArn == "" { - roleArn := getProviderConfig("role-arn") + roleArn := profile.RoleArn if roleArn != "" { cf.AssumeRole.RoleArn = roleArn } } if cf.AssumeRole.SessionName == "" { - sessionName := getProviderConfig("role-session-name") + sessionName := profile.RoleSessionName if sessionName != "" { cf.AssumeRole.SessionName = sessionName } } if cf.AssumeRole.SessionDuration == 0 { - duration := getProviderConfig("role-session-duration") - if duration != "" { - durationInt, err := strconv.Atoi(duration) - if err != nil { - return err - } - cf.AssumeRole.SessionDuration = durationInt + duration := profile.RoleSessionDuration + if duration != 0 { + cf.AssumeRole.SessionDuration = int(duration) } } } @@ -344,104 +323,3 @@ func validRegion(region string) error { return fmt.Errorf("unknown region: %s", region) } - -func getProfilePatch(cf *TencentCloudAccessConfig) (string, string, error) { - var ( - profile string - sharedCredentialsDir string - credentialPath string - configurePath string - ) - - if cf.Profile != "" { - profile = cf.Profile - } else { - profile = DEFAULT_PROFILE - } - - if cf.SharedCredentialsDir != "" { - sharedCredentialsDir = cf.SharedCredentialsDir - } - - tmpSharedCredentialsDir, err := homedir.Expand(sharedCredentialsDir) - if err != nil { - return "", "", err - } - - if tmpSharedCredentialsDir == "" { - credentialPath = fmt.Sprintf("%s/.tccli/%s.credential", os.Getenv("HOME"), profile) - configurePath = fmt.Sprintf("%s/.tccli/%s.configure", os.Getenv("HOME"), profile) - if runtime.GOOS == "windows" { - credentialPath = fmt.Sprintf("%s/.tccli/%s.credential", os.Getenv("USERPROFILE"), profile) - configurePath = fmt.Sprintf("%s/.tccli/%s.configure", os.Getenv("USERPROFILE"), profile) - } - } else { - credentialPath = fmt.Sprintf("%s/%s.credential", tmpSharedCredentialsDir, profile) - configurePath = fmt.Sprintf("%s/%s.configure", tmpSharedCredentialsDir, profile) - } - - return credentialPath, configurePath, nil -} - -func loadConfigProfile(cf *TencentCloudAccessConfig) (map[string]interface{}, error) { - var ( - credentialPath string - configurePath string - ) - - credentialPath, configurePath, err := getProfilePatch(cf) - if err != nil { - return nil, err - } - - packerConfig := make(map[string]interface{}) - _, err = os.Stat(credentialPath) - if !os.IsNotExist(err) { - data, err := ioutil.ReadFile(credentialPath) - if err != nil { - return nil, err - } - - config := map[string]interface{}{} - err = json.Unmarshal(data, &config) - if err != nil { - return nil, fmt.Errorf("credential file unmarshal failed, %s", err) - } - - for k, v := range config { - packerConfig[k] = strings.TrimSpace(v.(string)) - } - } else { - return nil, fmt.Errorf("please set a valid secret_id and secret_key or shared_credentials_dir, %s", err) - } - _, err = os.Stat(configurePath) - if !os.IsNotExist(err) { - data, err := ioutil.ReadFile(configurePath) - if err != nil { - return nil, err - } - - config := map[string]interface{}{} - err = json.Unmarshal(data, &config) - if err != nil { - return nil, fmt.Errorf("configure file unmarshal failed, %s", err) - } - - outerLoop: - for k, v := range config { - if k == "_sys_param" { - tmpMap := v.(map[string]interface{}) - for tmpK, tmpV := range tmpMap { - if tmpK == "region" { - packerConfig[tmpK] = strings.TrimSpace(tmpV.(string)) - break outerLoop - } - } - } - } - } else { - return nil, fmt.Errorf("please set a valid region or shared_credentials_dir, %s", err) - } - - return packerConfig, nil -} diff --git a/builder/tencentcloud/cvm/oauth.go b/builder/tencentcloud/cvm/oauth.go new file mode 100644 index 00000000..6deda567 --- /dev/null +++ b/builder/tencentcloud/cvm/oauth.go @@ -0,0 +1,191 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package cvm + +import ( + "bytes" + "encoding/json" + "fmt" + "net/http" + "time" + + "github.com/google/uuid" +) + +const _API_ENDPOINT = "https://cli.cloud.tencent.com" + +func GetOauthConfig(p *Profile) error { + if p.Oauth == nil || p.Oauth.RefreshToken == "" || p.Oauth.OpenId == "" { + return fmt.Errorf("Oauth authentication information is not configured correctly") + } + client := NewAPIClient() + + expired := false + if p.Oauth.ExpiresAt != 0 { + now := time.Now() + futureTime := now.Add(30 * time.Second) + targetTime := time.Unix(p.Oauth.ExpiresAt, 0) + if targetTime.After(futureTime) { + expired = true + } + } + + if expired { + response, err := client.RefreshUserToken(p.Oauth.RefreshToken, p.Oauth.OpenId, p.Oauth.Site) + if err != nil { + return err + } + if response != nil { + if response.AccessToken != "" { + p.Oauth.AccessToken = response.AccessToken + } + if response.ExpiresAt != 0 { + p.Oauth.ExpiresAt = response.ExpiresAt + } + } + } + + // 获取临时token + response, err := client.GetThirdPartyFederationToken(p.Oauth.AccessToken, p.Oauth.Site) + if err != nil { + return err + } + if response != nil { + if response.SecretId != "" { + p.SecretId = response.SecretId + } + if response.SecretKey != "" { + p.SecretKey = response.SecretKey + } + if response.Token != "" { + p.Token = response.Token + } + } + + return nil +} + +type APIClient struct { + Client *http.Client +} + +// 创建新的APIClient +func NewAPIClient() *APIClient { + return &APIClient{ + Client: &http.Client{ + Timeout: 10 * time.Second, + }, + } +} + +// GetThirdPartyFederationToken Obtaining a temporary user certificate +func (c *APIClient) GetThirdPartyFederationToken(accessToken, site string) (*GetTempCredResponse, error) { + apiEndpoint := _API_ENDPOINT + "/get_temp_cred" + traceId := uuid.New().String() + + body := GetTempCredRequest{ + TraceId: traceId, + AccessToken: accessToken, + Site: site, + } + + jsonData, err := json.Marshal(body) + if err != nil { + return nil, fmt.Errorf("failed to marshal request body: %v", err) + } + + req, err := http.NewRequest("POST", apiEndpoint, bytes.NewBuffer(jsonData)) + if err != nil { + return nil, fmt.Errorf("failed to create request: %v", err) + } + + req.Header.Set("Content-Type", "application/json") + + resp, err := c.Client.Do(req) + if err != nil { + return nil, fmt.Errorf("failed to send request: %v", err) + } + defer resp.Body.Close() + + response := &GetTempCredResponse{} + if err := json.NewDecoder(resp.Body).Decode(response); err != nil { + return nil, fmt.Errorf("failed to decode response body: %v", err) + } + + if response.Error != "" { + return nil, fmt.Errorf("get_temp_cred: %s", response.Error) + } + + return response, err +} + +// RefreshUserToken Refresh user third-party access_token +func (c *APIClient) RefreshUserToken(refToken, openId, site string) (*RefreshTokenResponse, error) { + apiEndpoint := _API_ENDPOINT + "/refresh_user_token" + + traceId := uuid.New().String() + + body := RefreshTokenRequest{ + TraceId: traceId, + RefreshToken: refToken, + OpenId: openId, + Site: site, + } + + jsonData, err := json.Marshal(body) + if err != nil { + return nil, fmt.Errorf("failed to marshal request body: %v", err) + } + + // 创建POST请求 + req, err := http.NewRequest("POST", apiEndpoint, bytes.NewBuffer(jsonData)) + if err != nil { + return nil, fmt.Errorf("failed to create request: %v", err) + } + + req.Header.Set("Content-Type", "application/json") + + resp, err := c.Client.Do(req) + if err != nil { + return nil, fmt.Errorf("failed to send request: %v", err) + } + defer resp.Body.Close() + + response := &RefreshTokenResponse{} + if err := json.NewDecoder(resp.Body).Decode(response); err != nil { + return nil, fmt.Errorf("failed to decode response body: %v", err) + } + + if response.Error != "" { + return nil, fmt.Errorf("refresh_user_token: %s", response.Error) + } + return response, nil +} + +type RefreshTokenRequest struct { + TraceId string `json:"TraceId"` + RefreshToken string `json:"RefreshToken"` + OpenId string `json:"OpenId"` + Site string `json:"Site"` +} + +type RefreshTokenResponse struct { + AccessToken string `json:"AccessToken"` + ExpiresAt int64 `json:"ExpiresAt"` + Error string `json:"Error"` +} + +type GetTempCredRequest struct { + TraceId string `json:"TraceId"` + AccessToken string `json:"AccessToken"` + Site string `json:"Site"` +} + +type GetTempCredResponse struct { + SecretId string `json:"SecretId"` + SecretKey string `json:"SecretKey"` + Token string `json:"Token"` + ExpiresAt int64 `json:"ExpiresAt"` + Error string `json:"Error"` +} diff --git a/builder/tencentcloud/cvm/profile.go b/builder/tencentcloud/cvm/profile.go new file mode 100644 index 00000000..ca37d5a1 --- /dev/null +++ b/builder/tencentcloud/cvm/profile.go @@ -0,0 +1,139 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package cvm + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "os" + "runtime" + "strings" + + "github.com/mitchellh/go-homedir" +) + +type Profile struct { + Type string `json:"type,omitempty"` + Region string + SecretId string `json:"secretId,omitempty"` + SecretKey string `json:"secretKey,omitempty"` + Token string `json:"token,omitempty"` + ExpiresAt int64 `json:"expiresAt,omitempty"` + RoleArn string `json:"role-arn,omitempty"` + RoleSessionName string `json:"role-session-name,omitempty"` + RoleSessionDuration int64 `json:"role-session-duration,omitempty"` + Oauth *Oauth `json:"oauth,omitempty"` +} + +type Oauth struct { + OpenId string `json:"openId,omitempty"` + AccessToken string `json:"accessToken,omitempty"` + ExpiresAt int64 `json:"expiresAt,omitempty"` + RefreshToken string `json:"refreshToken,omitempty"` + Site string `json:"site,omitempty"` +} + +func getProfilePatch(cf *TencentCloudAccessConfig) (string, string, error) { + var ( + profile string + sharedCredentialsDir string + credentialPath string + configurePath string + ) + + if cf.Profile != "" { + profile = cf.Profile + } else { + profile = DEFAULT_PROFILE + } + + if cf.SharedCredentialsDir != "" { + sharedCredentialsDir = cf.SharedCredentialsDir + } + + tmpSharedCredentialsDir, err := homedir.Expand(sharedCredentialsDir) + if err != nil { + return "", "", err + } + + if tmpSharedCredentialsDir == "" { + credentialPath = fmt.Sprintf("%s/.tccli/%s.credential", os.Getenv("HOME"), profile) + configurePath = fmt.Sprintf("%s/.tccli/%s.configure", os.Getenv("HOME"), profile) + if runtime.GOOS == "windows" { + credentialPath = fmt.Sprintf("%s/.tccli/%s.credential", os.Getenv("USERPROFILE"), profile) + configurePath = fmt.Sprintf("%s/.tccli/%s.configure", os.Getenv("USERPROFILE"), profile) + } + } else { + credentialPath = fmt.Sprintf("%s/%s.credential", tmpSharedCredentialsDir, profile) + configurePath = fmt.Sprintf("%s/%s.configure", tmpSharedCredentialsDir, profile) + } + + return credentialPath, configurePath, nil +} + +func loadConfigProfile(cf *TencentCloudAccessConfig) (*Profile, error) { + var ( + credentialPath string + configurePath string + ) + + credentialPath, configurePath, err := getProfilePatch(cf) + if err != nil { + return nil, err + } + + tcProfile := &Profile{} + _, err = os.Stat(credentialPath) + if !os.IsNotExist(err) { + data, err := ioutil.ReadFile(credentialPath) + if err != nil { + return nil, err + } + + err = json.Unmarshal(data, tcProfile) + if err != nil { + return nil, fmt.Errorf("credential file unmarshal failed, %s", err) + } + + if tcProfile.Type == "oauth" { + err := GetOauthConfig(tcProfile) + if err != nil { + return nil, fmt.Errorf("getOauthConfig failed, %v", err) + } + } + } else { + return nil, fmt.Errorf("please set a valid secret_id and secret_key or shared_credentials_dir, %s", err) + } + _, err = os.Stat(configurePath) + if !os.IsNotExist(err) { + data, err := ioutil.ReadFile(configurePath) + if err != nil { + return nil, err + } + + config := map[string]interface{}{} + err = json.Unmarshal(data, &config) + if err != nil { + return nil, fmt.Errorf("configure file unmarshal failed, %s", err) + } + + outerLoop: + for k, v := range config { + if k == "_sys_param" { + tmpMap := v.(map[string]interface{}) + for tmpK, tmpV := range tmpMap { + if tmpK == "region" { + tcProfile.Region = strings.TrimSpace(tmpV.(string)) + break outerLoop + } + } + } + } + } else { + return nil, fmt.Errorf("please set a valid region or shared_credentials_dir, %s", err) + } + + return tcProfile, nil +}