278 lines
6.0 KiB
Go
278 lines
6.0 KiB
Go
// 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
|
|
}
|
|
}
|