This repository has been archived by the owner on Jan 8, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 330
/
Copy pathstorage.go
280 lines (233 loc) · 5.85 KB
/
storage.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
package clicontext
import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
)
// 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 string
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)
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(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(filepath.Dir(path), 0755); err != nil {
return err
}
f, err := os.Create(path)
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); 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, toPath); 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))
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); 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, []byte(n), 0644)
}
// UnsetDefault unsets the default context.
func (m *Storage) UnsetDefault() error {
err := os.Remove(m.defaultPath())
if os.IsNotExist(err) {
err = nil
}
return err
}
// Default returns the name of the default context.
func (m *Storage) Default() (string, error) {
path := m.defaultPath()
fi, err := os.Lstat(path)
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 {
path, err := os.Readlink(path)
if err != nil {
return "", err
}
return m.nameFromPath(path), nil
}
// If this is a regular file then we just read it cause it a non-symlink mode.
contents, err := ioutil.ReadFile(path)
if err != nil {
return "", err
}
return string(contents), nil
}
func (m *Storage) createSymlink(src, dst string) error {
// delete the old symlink
err := os.Remove(dst)
if err != nil && !os.IsNotExist(err) {
return err
}
err = os.Symlink(src, dst)
// 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)
if ferr != nil {
// symlink has not been created return the original error
return err
}
if ln != src {
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(path string) string {
path = filepath.Base(path)
for i := len(path) - 1; i >= 0; i-- {
if path[i] == '.' {
path = path[:i]
break
}
}
return path
}
func (m *Storage) configPath(n string) string {
return filepath.Join(m.dir, n+".hcl")
}
func (m *Storage) defaultPath() string {
return filepath.Join(m.dir, "_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 string) 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
}
}