// Copyright (c) 2009-present, Alibaba Cloud All rights reserved. // // 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 config import ( "encoding/json" "fmt" "io" "os" "path/filepath" "runtime" "github.com/aliyun/aliyun-cli/v3/cli" "github.com/aliyun/aliyun-cli/v3/util" ) const ( configPath = "/.aliyun" configFile = "config.json" DefaultConfigProfileName = "default" ) type Configuration struct { CurrentProfile string `json:"current"` Profiles []Profile `json:"profiles"` MetaPath string `json:"meta_path"` //Plugins []Plugin `json:"plugin"` } var hookGetHomePath = func(fn func() string) func() string { return fn } func NewConfiguration() *Configuration { return &Configuration{ CurrentProfile: DefaultConfigProfileName, Profiles: []Profile{ NewProfile(DefaultConfigProfileName), }, } } func (c *Configuration) NewProfile(pn string) Profile { p, ok := c.GetProfile(pn) if !ok { p = NewProfile(pn) c.PutProfile(p) } return p } func (c *Configuration) GetProfile(pn string) (Profile, bool) { for _, p := range c.Profiles { if p.Name == pn { return p, true } } return Profile{Name: pn}, false } func (c *Configuration) GetCurrentProfile(ctx *cli.Context) Profile { profileName := ProfileFlag(ctx.Flags()).GetStringOrDefault(c.CurrentProfile) if profileName == "" || profileName == "default" { if v := util.GetFromEnv("ALIBABACLOUD_PROFILE", "ALIBABA_CLOUD_PROFILE", "ALICLOUD_PROFILE"); v != "" { profileName = v } } p, _ := c.GetProfile(profileName) p.OverwriteWithFlags(ctx) return p } func (c *Configuration) PutProfile(profile Profile) { for i, p := range c.Profiles { if p.Name == profile.Name { c.Profiles[i] = profile return } } c.Profiles = append(c.Profiles, profile) } func LoadOrCreateDefaultProfile() (Profile, error) { return LoadProfile(GetConfigPath()+"/"+configFile, "") } func LoadProfile(path string, name string) (Profile, error) { var p Profile config, err := hookLoadOrCreateConfiguration(LoadOrCreateConfiguration)(path) if err != nil { return p, fmt.Errorf("init config failed %v", err) } if name == "" { name = config.CurrentProfile } p, ok := config.GetProfile(name) p.parent = config if !ok { return p, fmt.Errorf("unknown profile %s, run configure to check", name) } return p, nil } func getConfigurePath(ctx *cli.Context) (currentPath string) { if path, ok := ConfigurePathFlag(ctx.Flags()).GetValue(); ok { currentPath = path } else { currentPath = GetConfigPath() + "/" + configFile } return } func GetConfigDir(ctx *cli.Context) string { return filepath.Dir(getConfigurePath(ctx)) } func getProfileName(ctx *cli.Context) (name string) { if name, ok := ProfileFlag(ctx.Flags()).GetValue(); ok { return name } else if profileNameInEnv := util.GetFromEnv("ALIBABACLOUD_PROFILE", "ALIBABA_CLOUD_PROFILE", "ALICLOUD_PROFILE"); profileNameInEnv != "" { return profileNameInEnv } return "" } func LoadProfileWithContext(ctx *cli.Context) (profile Profile, err error) { if util.GetFromEnv("ALIBABA_CLOUD_IGNORE_PROFILE", "ALIBABACLOUD_IGNORE_PROFILE") == "TRUE" { profile = NewProfile("default") profile.RegionId = "cn-hangzhou" } else { currentPath := getConfigurePath(ctx) profileName := getProfileName(ctx) profile, err = LoadProfile(currentPath, profileName) if err != nil { return } } // Load from flags if ctx.InConfigureMode() { // If not in configure mode, we will not overwrite the profile with flags profile.OverwriteWithFlags(ctx) } err = profile.Validate() return } // LoadProfileForDryRunWithContext resolves only local, non-executing profile // data. It intentionally skips profile validation because validation requires // credentials even though client-side dry-run never authenticates or sends a // request. Missing default configuration is represented by an empty default // profile; an explicitly selected missing config or profile still fails. func LoadProfileForDryRunWithContext(ctx *cli.Context) (profile Profile, err error) { if util.GetFromEnv("ALIBABA_CLOUD_IGNORE_PROFILE", "ALIBABACLOUD_IGNORE_PROFILE") == "TRUE" { profile = NewProfile(DefaultConfigProfileName) profile.RegionId = "cn-hangzhou" profile.OverwriteWithFlags(ctx) return profile, nil } conf, err := LoadConfigurationWithContext(ctx) if err != nil { if _, explicit := ConfigurePathFlag(ctx.Flags()).GetValue(); explicit || !os.IsNotExist(err) { return profile, err } conf = NewConfiguration() } profileName := getProfileName(ctx) if profileName == "" { profileName = conf.CurrentProfile } profile, ok := conf.GetProfile(profileName) if !ok { return profile, fmt.Errorf("unknown profile %s, run configure to check", profileName) } profile.parent = conf profile.OverwriteWithFlags(ctx) return profile, nil } func LoadOrCreateConfiguration(path string) (conf *Configuration, err error) { _, statErr := os.Stat(path) if os.IsNotExist(statErr) { conf, err = MigrateLegacyConfiguration() if err != nil { return } if conf != nil { err = SaveConfiguration(conf) if err != nil { err = fmt.Errorf("save failed %v", err) return } return } conf = NewConfiguration() return } bytes, err := os.ReadFile(path) if err != nil { err = fmt.Errorf("reading config from '%s' failed %v", path, err) return } conf, err = NewConfigFromBytes(bytes) return } func LoadConfigurationFromFile(filePath string) (conf *Configuration, err error) { bytes, err := os.ReadFile(filePath) if err != nil { err = fmt.Errorf("reading config from '%s' failed %v", filePath, err) return nil, err } conf, err = NewConfigFromBytes(bytes) return } func LoadConfigurationWithContext(ctx *cli.Context) (conf *Configuration, err error) { confPath := hookGetHomePath(GetHomePath)() + configPath + "/" + configFile if customPath, ok := ConfigurePathFlag(ctx.Flags()).GetValue(); ok { if _, err := hookFileStat(os.Stat)(customPath); os.IsNotExist(err) { // invalid config path from user input should be blocked return nil, fmt.Errorf("config path input does not exist: %s", customPath) } confPath = customPath } _, statErr := os.Stat(confPath) if os.IsNotExist(statErr) { return nil, statErr } return LoadConfigurationFromFile(confPath) } func SaveConfiguration(config *Configuration) (err error) { bytes, err := json.MarshalIndent(config, "", "\t") if err != nil { return } path := filepath.Join(GetConfigPath(), configFile) err = atomicWriteFile(path, bytes, 0600) return } func SaveConfigurationWithContext(ctx *cli.Context, config *Configuration) (err error) { bytes, err := json.MarshalIndent(config, "", "\t") if err != nil { return } confFilePath := filepath.Join(hookGetHomePath(GetHomePath)()+configPath, configFile) if customPath, ok := ConfigurePathFlag(ctx.Flags()).GetValue(); ok { confFilePath = customPath } dir := filepath.Dir(confFilePath) if _, err := os.Stat(dir); os.IsNotExist(err) { if err := os.MkdirAll(dir, 0755); err != nil { panic(fmt.Errorf("failed to create config directory %q: %w", dir, err)) } } err = atomicWriteFile(confFilePath, bytes, 0600) return } // atomicWriteFile writes data via a same-directory temp file then os.Rename. // On Windows, os.Rename replaces an existing destination (MoveFileEx REPLACE_EXISTING), // matching credentials-go / mcpproxy behavior and avoiding truncated config.json on crash. func atomicWriteFile(path string, data []byte, perm os.FileMode) error { return atomicWriteFileWithRename(path, data, perm, os.Rename) } type atomicTempFile interface { Name() string Chmod(os.FileMode) error Write([]byte) (int, error) Sync() error Close() error } var createAtomicTempFile = func(dir, pattern string) (atomicTempFile, error) { return os.CreateTemp(dir, pattern) } var lstatAtomicPath = os.Lstat func atomicWriteFileWithRename(path string, data []byte, perm os.FileMode, rename func(string, string) error) error { if info, err := lstatAtomicPath(path); err == nil && info.Mode()&os.ModeSymlink != 0 { resolvedPath, err := filepath.EvalSymlinks(path) if err != nil { return fmt.Errorf("failed to resolve config symlink %q: %w", path, err) } path = resolvedPath } else if err != nil && !os.IsNotExist(err) { return fmt.Errorf("failed to inspect config path %q: %w", path, err) } dir := filepath.Dir(path) base := filepath.Base(path) temp, err := createAtomicTempFile(dir, "."+base+".tmp-*") if err != nil { return fmt.Errorf("failed to create temp config in %q: %w", dir, err) } tempPath := temp.Name() closed := false defer func() { if !closed { _ = temp.Close() } _ = os.Remove(tempPath) }() if err := temp.Chmod(perm); err != nil { return fmt.Errorf("failed to set temp config permissions %q: %w", tempPath, err) } if n, err := temp.Write(data); err != nil { return fmt.Errorf("failed to write temp config %q: %w", tempPath, err) } else if n != len(data) { return fmt.Errorf("failed to write temp config %q: %w", tempPath, io.ErrShortWrite) } if err := temp.Sync(); err != nil { return fmt.Errorf("failed to sync temp config %q: %w", tempPath, err) } if err := temp.Close(); err != nil { return fmt.Errorf("failed to close temp config %q: %w", tempPath, err) } closed = true if err := rename(tempPath, path); err != nil { return fmt.Errorf("failed to rename temp config to %q: %w", path, err) } return nil } func NewConfigFromBytes(bytes []byte) (conf *Configuration, err error) { conf = NewConfiguration() err = json.Unmarshal(bytes, conf) return } func GetConfigPath() string { path := hookGetHomePath(GetHomePath)() + configPath if _, err := os.Stat(path); os.IsNotExist(err) { err = os.MkdirAll(path, 0755) if err != nil { panic(err) } } return path } func GetHomePath() string { if runtime.GOOS == "windows" { home := os.Getenv("HOMEDRIVE") + os.Getenv("HOMEPATH") if home == "" { home = os.Getenv("USERPROFILE") } return home } return os.Getenv("HOME") }