packer-cn/builder/ucloud/common/access_config.go

359 lines
10 KiB
Go

//go:generate struct-markdown
package common
import (
"encoding/json"
"fmt"
"io/ioutil"
"net/url"
"os"
"path/filepath"
"runtime"
"github.com/hashicorp/packer-plugin-sdk/template/interpolate"
"github.com/hashicorp/packer/builder/ucloud/version"
"github.com/ucloud/ucloud-sdk-go/external"
"github.com/ucloud/ucloud-sdk-go/private/protocol/http"
"github.com/ucloud/ucloud-sdk-go/services/uaccount"
"github.com/ucloud/ucloud-sdk-go/services/ufile"
"github.com/ucloud/ucloud-sdk-go/services/uhost"
"github.com/ucloud/ucloud-sdk-go/services/unet"
"github.com/ucloud/ucloud-sdk-go/services/vpc"
"github.com/ucloud/ucloud-sdk-go/ucloud"
"github.com/ucloud/ucloud-sdk-go/ucloud/auth"
"github.com/ucloud/ucloud-sdk-go/ucloud/log"
)
type AccessConfig struct {
// This is the UCloud public key. It must be provided unless `profile` is set,
// but it can also be sourced from the `UCLOUD_PUBLIC_KEY` environment variable.
PublicKey string `mapstructure:"public_key" required:"true"`
// This is the UCloud private key. It must be provided unless `profile` is set,
// but it can also be sourced from the `UCLOUD_PRIVATE_KEY` environment variable.
PrivateKey string `mapstructure:"private_key" required:"true"`
// This is the UCloud region. It must be provided, but it can also be sourced from
// the `UCLOUD_REGION` environment variables.
Region string `mapstructure:"region" required:"true"`
// This is the UCloud project id. It must be provided, but it can also be sourced
// from the `UCLOUD_PROJECT_ID` environment variables.
ProjectId string `mapstructure:"project_id" required:"true"`
// This is the base url. (Default: `https://api.ucloud.cn`).
BaseUrl string `mapstructure:"base_url" required:"false"`
// This is the UCloud profile name as set in the shared credentials file, it can
// also be sourced from the `UCLOUD_PROFILE` environment variables.
Profile string `mapstructure:"profile" required:"false"`
// This is the path to the shared credentials file, it can also be sourced from
// the `UCLOUD_SHARED_CREDENTIAL_FILE` environment variables. If this is not set
// and a profile is specified, `~/.ucloud/credential.json` will be used.
SharedCredentialsFile string `mapstructure:"shared_credentials_file" required:"false"`
client *UCloudClient
}
type cloudShellCredential struct {
Cookie string `json:"cookie"`
Profile string `json:"profile"`
CSRFToken string `json:"csrf_token"`
}
func (c *AccessConfig) Client() (*UCloudClient, error) {
if c.client != nil {
return c.client, nil
}
cfg := ucloud.NewConfig()
cfg.Region = c.Region
cfg.ProjectId = c.ProjectId
if c.BaseUrl != "" {
cfg.BaseUrl = c.BaseUrl
}
cfg.LogLevel = log.PanicLevel
cfg.UserAgent = fmt.Sprintf("Packer-UCloud/%s", version.UcloudPluginVersion.FormattedVersion())
// set default max retry count
cfg.MaxRetries = 3
cred := auth.NewCredential()
var cloudShellCredHandler ucloud.HttpRequestHandler
if len(c.Profile) > 0 {
// load public/private key from shared credential file
credV, err := external.LoadUCloudCredentialFile(c.SharedCredentialsFile, c.Profile)
if err != nil {
return nil, fmt.Errorf("cannot load shared %q credential file, %s", c.Profile, err)
}
cred = *credV
} else if len(c.PublicKey) > 0 && len(c.PrivateKey) > 0 {
cred.PublicKey = c.PublicKey
cred.PrivateKey = c.PrivateKey
} else if v := os.Getenv("CLOUD_SHELL"); v == "true" {
csCred := make([]cloudShellCredential, 0)
// load credential from default cloud shell credential path
if err := loadJSONFile(defaultCloudShellCredPath(), &csCred); err != nil {
return nil, fmt.Errorf("must set credential about public_key and private_key, %s", err)
}
// get default cloud shell credential
defaultCsCred := &cloudShellCredential{}
for i := 0; i < len(csCred); i++ {
if csCred[i].Profile == "default" {
defaultCsCred = &csCred[i]
break
}
}
if defaultCsCred == nil || len(defaultCsCred.Cookie) == 0 || len(defaultCsCred.CSRFToken) == 0 {
return nil, fmt.Errorf("must set credential about public_key and private_key, default credential is null")
}
// set cloud shell client handler
cloudShellCredHandler = func(c *ucloud.Client, req *http.HttpRequest) (*http.HttpRequest, error) {
if err := req.SetHeader("Cookie", defaultCsCred.Cookie); err != nil {
return nil, err
}
if err := req.SetHeader("Csrf-Token", defaultCsCred.CSRFToken); err != nil {
return nil, err
}
return req, nil
}
} else {
return nil, fmt.Errorf("must set credential about public_key and private_key")
}
c.client = &UCloudClient{}
c.client.UHostConn = uhost.NewClient(&cfg, &cred)
c.client.UNetConn = unet.NewClient(&cfg, &cred)
c.client.VPCConn = vpc.NewClient(&cfg, &cred)
c.client.UAccountConn = uaccount.NewClient(&cfg, &cred)
c.client.UFileConn = ufile.NewClient(&cfg, &cred)
if cloudShellCredHandler != nil {
if err := c.client.UHostConn.AddHttpRequestHandler(cloudShellCredHandler); err != nil {
return nil, err
}
if err := c.client.UNetConn.AddHttpRequestHandler(cloudShellCredHandler); err != nil {
return nil, err
}
if err := c.client.VPCConn.AddHttpRequestHandler(cloudShellCredHandler); err != nil {
return nil, err
}
if err := c.client.UAccountConn.AddHttpRequestHandler(cloudShellCredHandler); err != nil {
return nil, err
}
if err := c.client.UFileConn.AddHttpRequestHandler(cloudShellCredHandler); err != nil {
return nil, err
}
}
return c.client, nil
}
func (c *AccessConfig) Prepare(ctx *interpolate.Context) []error {
var errs []error
if err := c.Config(); err != nil {
errs = append(errs, err)
}
if c.Region == "" {
c.Region = os.Getenv("UCLOUD_REGION")
}
if c.Region == "" {
errs = append(errs, fmt.Errorf("%q must be set", "region"))
}
if c.ProjectId == "" {
c.ProjectId = os.Getenv("UCLOUD_PROJECT_ID")
}
if c.ProjectId == "" {
errs = append(errs, fmt.Errorf("%q must be set", "projectId"))
}
if c.BaseUrl != "" {
if _, err := url.Parse(c.BaseUrl); err != nil {
errs = append(errs, fmt.Errorf("%q is invalid, should be an valid ucloud base_url, got %q, parse error: %s", "base_url", c.BaseUrl, err))
}
}
if c.Profile == "" {
c.Profile = os.Getenv("UCLOUD_PROFILE")
}
if c.SharedCredentialsFile == "" {
c.SharedCredentialsFile = os.Getenv("UCLOUD_SHARED_CREDENTIAL_FILE")
}
if len(errs) > 0 {
return errs
}
return nil
}
func (c *AccessConfig) Config() error {
if c.PublicKey == "" {
c.PublicKey = os.Getenv("UCLOUD_PUBLIC_KEY")
}
if c.PrivateKey == "" {
c.PrivateKey = os.Getenv("UCLOUD_PRIVATE_KEY")
}
if c.Profile == "" {
c.Profile = os.Getenv("UCLOUD_PROFILE")
}
if c.SharedCredentialsFile == "" {
c.SharedCredentialsFile = os.Getenv("UCLOUD_SHARED_CREDENTIAL_FILE")
}
if (c.PublicKey == "" || c.PrivateKey == "") && c.Profile == "" && os.Getenv("CLOUD_SHELL") != "true" {
return fmt.Errorf("%q, %q must be set in template file or environment variables", "public_key", "private_key")
}
return nil
}
func (c *AccessConfig) ValidateProjectId(projectId string) error {
supportedProjectIds, err := c.getSupportedProjectIds()
if err != nil {
return err
}
for _, supportedProjectId := range supportedProjectIds {
if projectId == supportedProjectId {
return nil
}
}
return fmt.Errorf("%q is invalid, should be an valid ucloud project_id, got %q", "project_id", projectId)
}
func (c *AccessConfig) ValidateRegion(region string) error {
supportedRegions, err := c.getSupportedRegions()
if err != nil {
return err
}
for _, supportedRegion := range supportedRegions {
if region == supportedRegion {
return nil
}
}
return fmt.Errorf("%q is invalid, should be an valid ucloud region, got %q", "region", region)
}
func (c *AccessConfig) ValidateZone(region, zone string) error {
supportedZones, err := c.getSupportedZones(region)
if err != nil {
return err
}
for _, supportedZone := range supportedZones {
if zone == supportedZone {
return nil
}
}
return fmt.Errorf("%q is invalid, should be an valid ucloud zone, got %q", "availability_zone", zone)
}
func (c *AccessConfig) getSupportedProjectIds() ([]string, error) {
client, err := c.Client()
if err != nil {
return nil, err
}
conn := client.UAccountConn
req := conn.NewGetProjectListRequest()
resp, err := conn.GetProjectList(req)
if err != nil {
return nil, err
}
validProjectIds := make([]string, len(resp.ProjectSet))
for _, val := range resp.ProjectSet {
if !IsStringIn(val.ProjectId, validProjectIds) {
validProjectIds = append(validProjectIds, val.ProjectId)
}
}
return validProjectIds, nil
}
func (c *AccessConfig) getSupportedRegions() ([]string, error) {
client, err := c.Client()
if err != nil {
return nil, err
}
conn := client.UAccountConn
req := conn.NewGetRegionRequest()
resp, err := conn.GetRegion(req)
if err != nil {
return nil, err
}
validRegions := make([]string, len(resp.Regions))
for _, val := range resp.Regions {
if !IsStringIn(val.Region, validRegions) {
validRegions = append(validRegions, val.Region)
}
}
return validRegions, nil
}
func (c *AccessConfig) getSupportedZones(region string) ([]string, error) {
client, err := c.Client()
if err != nil {
return nil, err
}
conn := client.UAccountConn
req := conn.NewGetRegionRequest()
resp, err := conn.GetRegion(req)
if err != nil {
return nil, err
}
validZones := make([]string, len(resp.Regions))
for _, val := range resp.Regions {
if val.Region == region && !IsStringIn(val.Zone, validZones) {
validZones = append(validZones, val.Zone)
}
}
return validZones, nil
}
func defaultCloudShellCredPath() string {
return filepath.Join(userHomeDir(), ".ucloud", "credential.json")
}
func loadJSONFile(path string, p interface{}) error {
f, err := os.Open(path)
if err != nil {
return err
}
c, err := ioutil.ReadAll(f)
if err != nil {
return err
}
err = json.Unmarshal(c, p)
if err != nil {
return err
}
return nil
}
func userHomeDir() string {
if runtime.GOOS == "windows" {
home := os.Getenv("HOMEDRIVE") + os.Getenv("HOMEPATH")
if home == "" {
home = os.Getenv("USERPROFILE")
}
return home
}
return os.Getenv("HOME")
}