-
Notifications
You must be signed in to change notification settings - Fork 1.4k
Expand file tree
/
Copy pathcatalog.go
More file actions
214 lines (190 loc) · 6.57 KB
/
Copy pathcatalog.go
File metadata and controls
214 lines (190 loc) · 6.57 KB
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
package templatebuilder
import (
"bytes"
"embed"
"encoding/json"
"io/fs"
"path"
"sync"
"golang.org/x/xerrors"
"github.com/coder/coder/v2/codersdk"
)
var (
//go:embed modules
modulesFS embed.FS
loadModules = sync.OnceValues(func() ([]ModuleManifest, error) {
return parseModulesFromFS(modulesFS)
})
)
const modulesDir = "modules"
// ModuleManifest represents a module.json file from the bundled catalog.
// This is the on-disk schema; codersdk.TemplateBuilderModule is the API type.
type ModuleManifest struct {
ID string `json:"id"`
DisplayName string `json:"display_name"`
Description string `json:"description"`
Icon string `json:"icon"`
Category string `json:"category"`
Tags []string `json:"tags"`
CompatibleOS []string `json:"compatible_os"`
ConflictsWith []string `json:"conflicts_with"`
Namespace string `json:"namespace"`
PinnedVersion string `json:"pinned_version"`
Variables []ModuleVariable `json:"variables"`
}
// ModuleVariable represents a variable declaration within a module manifest.
type ModuleVariable struct {
Name string `json:"name"`
Type string `json:"type"`
Description string `json:"description"`
Default json.RawMessage `json:"default,omitempty"`
Required bool `json:"required"`
Sensitive bool `json:"sensitive"`
Computed bool `json:"computed"`
}
// validVariableTypes maps module.json type strings to their SDK equivalents.
// Used both for validation in parseModulesFromFS and for conversion in ToSDK.
var validVariableTypes = map[string]codersdk.TemplateBuilderVariableType{
"string": codersdk.TemplateBuilderVariableTypeString,
"number": codersdk.TemplateBuilderVariableTypeNumber,
"bool": codersdk.TemplateBuilderVariableTypeBool,
}
// LoadModules returns all module manifests from the embedded catalog.
// Results are cached after the first call, including errors. Each call
// returns a fresh slice so callers can filter or sort without corrupting
// the cache.
func LoadModules() ([]ModuleManifest, error) {
modules, err := loadModules()
if err != nil {
return nil, err
}
out := make([]ModuleManifest, len(modules))
copy(out, modules)
return out, nil
}
// parseModulesFromFS reads and validates all module.json files from the
// given filesystem. Most callers should use LoadModules, which reads from
// the embedded catalog.
func parseModulesFromFS(fsys fs.FS) ([]ModuleManifest, error) {
sub, err := fs.Sub(fsys, modulesDir)
if err != nil {
return nil, xerrors.Errorf("open embedded module catalog: %w", err)
}
dirs, err := fs.ReadDir(sub, ".")
if err != nil {
return nil, xerrors.Errorf("list module catalog entries: %w", err)
}
seen := make(map[string]bool)
var modules []ModuleManifest
for _, dir := range dirs {
if !dir.IsDir() {
continue
}
manifestPath := path.Join(dir.Name(), "module.json")
data, err := fs.ReadFile(sub, manifestPath)
if err != nil {
return nil, xerrors.Errorf("read %s: %w", manifestPath, err)
}
var manifest ModuleManifest
dec := json.NewDecoder(bytes.NewReader(data))
dec.DisallowUnknownFields()
if err := dec.Decode(&manifest); err != nil {
return nil, xerrors.Errorf("decode %s: %w", manifestPath, err)
}
if manifest.ID == "" {
return nil, xerrors.Errorf("module in %s has empty id", dir.Name())
}
if manifest.PinnedVersion == "" {
return nil, xerrors.Errorf("module %q has empty pinned_version", manifest.ID)
}
if seen[manifest.ID] {
return nil, xerrors.Errorf("duplicate module id %q", manifest.ID)
}
seen[manifest.ID] = true
seenVars := make(map[string]bool)
for i, v := range manifest.Variables {
if v.Name == "" {
return nil, xerrors.Errorf("module %q variable %d has empty name", manifest.ID, i)
}
if seenVars[v.Name] {
return nil, xerrors.Errorf("module %q has duplicate variable name %q", manifest.ID, v.Name)
}
seenVars[v.Name] = true
if _, ok := validVariableTypes[v.Type]; !ok {
return nil, xerrors.Errorf("module %q variable %d (%q): unknown type %q", manifest.ID, i, v.Name, v.Type)
}
}
modules = append(modules, manifest)
}
return modules, nil
}
// CompatibleWithOS reports whether the module is compatible with the given OS.
// Modules with an empty CompatibleOS list are compatible with all platforms.
func (m ModuleManifest) CompatibleWithOS(os string) bool {
if len(m.CompatibleOS) == 0 {
return true
}
for _, supported := range m.CompatibleOS {
if supported == os {
return true
}
}
return false
}
// ToSDK converts a ModuleManifest to the API response type.
// PinnedVersion is mapped to Version; tags are not part of the API surface.
// Computed variables are excluded from the output.
func (m ModuleManifest) ToSDK() codersdk.TemplateBuilderModule {
variables := make([]codersdk.TemplateBuilderModuleVariable, 0, len(m.Variables))
for _, v := range m.Variables {
// Computed variables (e.g. agent_id) are wired by the builder
// automatically and must not be surfaced to the user.
if v.Computed {
continue
}
variables = append(variables, codersdk.TemplateBuilderModuleVariable{
Name: v.Name,
Type: validVariableTypes[v.Type],
Description: v.Description,
Default: v.Default,
Required: v.Required,
Sensitive: v.Sensitive,
})
}
// CLEANUP: json/v2
compatibleOS := m.CompatibleOS
if compatibleOS == nil {
compatibleOS = []string{}
}
conflictsWith := m.ConflictsWith
if conflictsWith == nil {
conflictsWith = []string{}
}
return codersdk.TemplateBuilderModule{
ID: m.ID,
DisplayName: m.DisplayName,
Description: m.Description,
Icon: m.Icon,
Category: m.Category,
Version: m.PinnedVersion,
CompatibleOS: compatibleOS,
ConflictsWith: conflictsWith,
Variables: variables,
}
}
// ModuleTemplateFS returns an fs.FS rooted at the embedded directory for
// the given module ID, providing access to its .tf.tmpl file.
func ModuleTemplateFS(moduleID string) (fs.FS, error) {
modPath := modulesDir + "/" + moduleID
// Verify the directory exists. fs.Sub on embed.FS silently succeeds
// for nonexistent paths, so we check for the expected .tf.tmpl file.
tmplName := moduleID + ".tf.tmpl"
if _, err := fs.Stat(modulesFS, modPath+"/"+tmplName); err != nil {
return nil, xerrors.Errorf("module %q not found in embedded catalog: %w", moduleID, err)
}
sub, err := fs.Sub(modulesFS, modPath)
if err != nil {
return nil, xerrors.Errorf("module %q sub-filesystem: %w", moduleID, err)
}
return sub, nil
}