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
}
}