Skip to content

feat: support tccli oauth #3

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Mar 13, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
144 changes: 11 additions & 133 deletions builder/tencentcloud/cvm/access_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand Down Expand Up @@ -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
}
}

Expand Down Expand Up @@ -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)
}
}
}
Expand All @@ -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
}
191 changes: 191 additions & 0 deletions builder/tencentcloud/cvm/oauth.go
Original file line number Diff line number Diff line change
@@ -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"`
}
Loading