// Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: BUSL-1.1 package clicontext import ( "fmt" "io/ioutil" "os" "strings" "github.com/hashicorp/vagrant-plugin-sdk/helper/path" ) // Storage is the primary struct for interacting with stored CLI contexts. // Contexts are always stored directly on disk with one set as the default. type Storage struct { dir path.Path noSymlink bool } // NewStorage initializes context storage. func NewStorage(opts ...Option) (*Storage, error) { var m Storage for _, opt := range opts { if err := opt(&m); err != nil { return nil, err } } return &m, nil } // List lists the contexts that are available. func (m *Storage) List() ([]string, error) { f, err := os.Open(m.dir.String()) if err != nil { if os.IsNotExist(err) { return nil, nil } return nil, err } defer f.Close() names, err := f.Readdirnames(-1) if err != nil { return nil, err } // Remove all our _-prefixed names which are system settings. result := make([]string, 0, len(names)) for _, n := range names { if n[0] == '_' { continue } result = append(result, m.nameFromPath(path.NewPath(n))) } return result, nil } // Load loads a context with the given name. func (m *Storage) Load(n string) (*Config, error) { return LoadPath(m.configPath(n)) } // Set will set a new configuration with the given name. This will // overwrite any existing context of this name. func (m *Storage) Set(n string, c *Config) error { path := m.configPath(n) if err := os.MkdirAll(path.Dir().String(), 0755); err != nil { return err } f, err := os.Create(path.String()) if err != nil { return err } defer f.Close() _, err = c.WriteTo(f) if err != nil { return err } // If we have no default, set as the default def, err := m.Default() if err != nil { return err } if def == "" { err = m.SetDefault(n) } return err } // Rename renames a context. This will error if the "from" context does not // exist. If "from" is the default context then the default will be switched // to "to". If "to" already exists, this will overwrite it. func (m *Storage) Rename(from, to string) error { fromPath := m.configPath(from) if _, err := os.Stat(fromPath.String()); err != nil { if os.IsNotExist(err) { return fmt.Errorf("context %q does not exist", from) } return err } if err := m.Delete(to); err != nil { return err } toPath := m.configPath(to) if err := os.Rename(fromPath.String(), toPath.String()); err != nil { return err } def, err := m.Default() if err != nil { return err } if def == from { return m.SetDefault(to) } return nil } // Delete deletes the context with the given name. func (m *Storage) Delete(n string) error { // Remove it err := os.Remove(m.configPath(n).String()) if os.IsNotExist(err) { err = nil } if err != nil { return err } // If our default is this, then unset the default def, err := m.Default() if err != nil { return err } if def == n { err = m.UnsetDefault() } return err } // SetDefault sets the default context to use. If the given context // doesn't exist, an os.IsNotExist error will be returned. func (m *Storage) SetDefault(n string) error { src := m.configPath(n) if _, err := os.Stat(src.String()); err != nil { return err } // Attempt to create a symlink defaultPath := m.defaultPath() if !m.noSymlink { err := m.createSymlink(src, defaultPath) if err == nil { return nil } } // If the symlink fails, then we use a plain file approach. The downside // of this approach is that it is not atomic (on Windows it is impossible // to have atomic writes) so we only do it on error cases. return ioutil.WriteFile(defaultPath.String(), []byte(n), 0644) } // UnsetDefault unsets the default context. func (m *Storage) UnsetDefault() error { err := os.Remove(m.defaultPath().String()) if os.IsNotExist(err) { err = nil } return err } // Default returns the name of the default context. func (m *Storage) Default() (string, error) { p := m.defaultPath() fi, err := os.Lstat(p.String()) if err != nil { if os.IsNotExist(err) { err = nil } return "", err } // Symlinks are based on the resulting symlink path if fi.Mode()&os.ModeSymlink != 0 { pth, err := os.Readlink(p.String()) if err != nil { return "", err } return m.nameFromPath(path.NewPath(pth)), nil } // If this is a regular file then we just read it cause it a non-symlink mode. contents, err := ioutil.ReadFile(p.String()) if err != nil { return "", err } return string(contents), nil } func (m *Storage) createSymlink(src, dst path.Path) error { // delete the old symlink err := os.Remove(dst.String()) if err != nil && !os.IsNotExist(err) { return err } err = os.Symlink(src.String(), dst.String()) // On Windows when creating a symlink the Windows API can incorrectly // return an error message when not running as Administrator even when the symlink // is correctly created. // Manually validate the symlink was correctly created before returning an error ln, ferr := os.Readlink(dst.String()) if ferr != nil { // symlink has not been created return the original error return err } if ln != src.String() { return err } return nil } // nameFromPath returns the context name given a path to a context // HCL file. This is just the name of the file without any extension. func (m *Storage) nameFromPath(p path.Path) string { return strings.Replace(p.Base().String(), p.Ext(), "", 1) } func (m *Storage) configPath(n string) path.Path { return m.dir.Join(n + ".hcl") } func (m *Storage) defaultPath() path.Path { return m.dir.Join("_default.hcl") } type Option func(*Storage) error // WithDir specifies the directory where context configuration will be stored. // This doesn't have to exist already but we must have permission to create it. func WithDir(d path.Path) Option { return func(m *Storage) error { m.dir = d return nil } } // WithNoSymlink disables all symlink usage in the Storage. If symlinks were // used previously then they'll still work. func WithNoSymlink() Option { return func(m *Storage) error { m.noSymlink = true return nil } }