/
filesystem_search.go
289 lines (251 loc) · 11.1 KB
/
filesystem_search.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
281
282
283
284
285
286
287
288
289
package getproviders
import (
"fmt"
"log"
"os"
"path/filepath"
"strings"
svchost "github.com/hashicorp/terraform-svchost"
"github.com/hashicorp/terraform/internal/addrs"
)
// SearchLocalDirectory performs an immediate, one-off scan of the given base
// directory for provider plugins using the directory structure defined for
// FilesystemMirrorSource.
//
// This is separated to allow other callers, such as the provider plugin cache
// management in the "internal/providercache" package, to use the same
// directory structure conventions.
func SearchLocalDirectory(baseDir string) (map[addrs.Provider]PackageMetaList, error) {
ret := make(map[addrs.Provider]PackageMetaList)
// We don't support symlinks at intermediate points inside the directory
// hierarchy because that could potentially cause our walk to get into
// an infinite loop, but as a measure of pragmatism we'll allow the
// top-level location itself to be a symlink, so that a user can
// potentially keep their plugins in a non-standard location but use a
// symlink to help Terraform find them anyway.
originalBaseDir := baseDir
if finalDir, err := filepath.EvalSymlinks(baseDir); err == nil {
if finalDir != filepath.Clean(baseDir) {
log.Printf("[TRACE] getproviders.SearchLocalDirectory: using %s instead of %s", finalDir, baseDir)
}
baseDir = finalDir
} else {
// We'll eat this particular error because if we're somehow able to
// find plugins via baseDir below anyway then we'd rather do that than
// hard fail, but we'll log it in case it's useful for diagnosing why
// discovery didn't produce the expected outcome.
log.Printf("[TRACE] getproviders.SearchLocalDirectory: failed to resolve symlinks for %s: %s", baseDir, err)
}
err := filepath.Walk(baseDir, func(fullPath string, info os.FileInfo, err error) error {
if err != nil {
return fmt.Errorf("cannot search %s: %s", fullPath, err)
}
// There are two valid directory structures that we support here...
// Unpacked: registry.terraform.io/hashicorp/aws/2.0.0/linux_amd64 (a directory)
// Packed: registry.terraform.io/hashicorp/aws/terraform-provider-aws_2.0.0_linux_amd64.zip (a file)
//
// Both of these give us enough information to identify the package
// metadata.
fsPath, err := filepath.Rel(baseDir, fullPath)
if err != nil {
// This should never happen because the filepath.Walk contract is
// for the paths to include the base path.
log.Printf("[TRACE] getproviders.SearchLocalDirectory: ignoring malformed path %q during walk: %s", fullPath, err)
return nil
}
relPath := filepath.ToSlash(fsPath)
parts := strings.Split(relPath, "/")
if len(parts) < 3 {
// Likely a prefix of a valid path, so we'll ignore it and visit
// the full valid path on a later call.
if (info.Mode() & os.ModeSymlink) != 0 {
// We don't allow symlinks for intermediate steps in the
// hierarchy because otherwise this walk would risk getting
// itself into an infinite loop, but if we do find one then
// we'll warn about it to help with debugging.
log.Printf("[WARN] Provider plugin search ignored symlink %s: only the base directory %s may be a symlink", fullPath, originalBaseDir)
}
return nil
}
hostnameGiven := parts[0]
namespace := parts[1]
typeName := parts[2]
// validate each part
// The legacy provider namespace is a special case.
if namespace != addrs.LegacyProviderNamespace {
_, err = addrs.ParseProviderPart(namespace)
if err != nil {
log.Printf("[WARN] local provider path %q contains invalid namespace %q; ignoring", fullPath, namespace)
return nil
}
}
_, err = addrs.ParseProviderPart(typeName)
if err != nil {
log.Printf("[WARN] local provider path %q contains invalid type %q; ignoring", fullPath, typeName)
return nil
}
hostname, err := svchost.ForComparison(hostnameGiven)
if err != nil {
log.Printf("[WARN] local provider path %q contains invalid hostname %q; ignoring", fullPath, hostnameGiven)
return nil
}
var providerAddr addrs.Provider
if namespace == addrs.LegacyProviderNamespace {
if hostname != addrs.DefaultRegistryHost {
log.Printf("[WARN] local provider path %q indicates a legacy provider not on the default registry host; ignoring", fullPath)
return nil
}
providerAddr = addrs.NewLegacyProvider(typeName)
} else {
providerAddr = addrs.NewProvider(hostname, namespace, typeName)
}
// The "info" passed to our function is an Lstat result, so it might
// be referring to a symbolic link. We'll do a full "Stat" on it
// now to make sure we're making tests against the real underlying
// filesystem object below.
info, err = os.Stat(fullPath)
if err != nil {
log.Printf("[WARN] failed to read metadata about %s: %s", fullPath, err)
return nil
}
switch len(parts) {
case 5: // Might be unpacked layout
if !info.IsDir() {
return nil // packed layout requires a directory
}
versionStr := parts[3]
version, err := ParseVersion(versionStr)
if err != nil {
log.Printf("[WARN] ignoring local provider path %q with invalid version %q: %s", fullPath, versionStr, err)
return nil
}
platformStr := parts[4]
platform, err := ParsePlatform(platformStr)
if err != nil {
log.Printf("[WARN] ignoring local provider path %q with invalid platform %q: %s", fullPath, platformStr, err)
return nil
}
log.Printf("[TRACE] getproviders.SearchLocalDirectory: found %s v%s for %s at %s", providerAddr, version, platform, fullPath)
meta := PackageMeta{
Provider: providerAddr,
Version: version,
// FIXME: How do we populate this?
ProtocolVersions: nil,
TargetPlatform: platform,
// Because this is already unpacked, the filename is synthetic
// based on the standard naming scheme.
Filename: fmt.Sprintf("terraform-provider-%s_%s_%s.zip", providerAddr.Type, version, platform),
Location: PackageLocalDir(fullPath),
// FIXME: What about the SHA256Sum field? As currently specified
// it's a hash of the zip file, but this thing is already
// unpacked and so we don't have the zip file to hash.
}
ret[providerAddr] = append(ret[providerAddr], meta)
case 4: // Might be packed layout
if info.IsDir() {
return nil // packed layout requires a file
}
filename := filepath.Base(fsPath)
// the filename components are matched case-insensitively, and
// the normalized form of them is in lowercase so we'll convert
// to lowercase for comparison here. (This normalizes only for case,
// because that is the primary constraint affecting compatibility
// between filesystem implementations on different platforms;
// filenames are expected to be pre-normalized and valid in other
// regards.)
normFilename := strings.ToLower(filename)
// In the packed layout, the version number and target platform
// are derived from the package filename, but only if the
// filename has the expected prefix identifying it as a package
// for the provider in question, and the suffix identifying it
// as a zip file.
prefix := "terraform-provider-" + providerAddr.Type + "_"
const suffix = ".zip"
if !strings.HasPrefix(normFilename, prefix) {
log.Printf("[WARN] ignoring file %q as possible package for %s: filename lacks expected prefix %q", fsPath, providerAddr, prefix)
return nil
}
if !strings.HasSuffix(normFilename, suffix) {
log.Printf("[WARN] ignoring file %q as possible package for %s: filename lacks expected suffix %q", fsPath, providerAddr, suffix)
return nil
}
// Extract the version and target part of the filename, which
// will look like "2.1.0_linux_amd64"
infoSlice := normFilename[len(prefix) : len(normFilename)-len(suffix)]
infoParts := strings.Split(infoSlice, "_")
if len(infoParts) < 3 {
log.Printf("[WARN] ignoring file %q as possible package for %s: filename does not include version number, target OS, and target architecture", fsPath, providerAddr)
return nil
}
versionStr := infoParts[0]
version, err := ParseVersion(versionStr)
if err != nil {
log.Printf("[WARN] ignoring local provider path %q with invalid version %q: %s", fullPath, versionStr, err)
return nil
}
// We'll reassemble this back into a single string just so we can
// easily re-use our existing parser and its normalization rules.
platformStr := infoParts[1] + "_" + infoParts[2]
platform, err := ParsePlatform(platformStr)
if err != nil {
log.Printf("[WARN] ignoring local provider path %q with invalid platform %q: %s", fullPath, platformStr, err)
return nil
}
log.Printf("[TRACE] getproviders.SearchLocalDirectory: found %s v%s for %s at %s", providerAddr, version, platform, fullPath)
meta := PackageMeta{
Provider: providerAddr,
Version: version,
// FIXME: How do we populate this?
ProtocolVersions: nil,
TargetPlatform: platform,
// Because this is already unpacked, the filename is synthetic
// based on the standard naming scheme.
Filename: normFilename, // normalized filename, because this field says what it _should_ be called, not what it _is_ called
Location: PackageLocalArchive(fullPath), // non-normalized here, because this is the actual physical location
// TODO: Also populate the SHA256Sum field. Skipping that
// for now because our initial uses of this result --
// scanning already-installed providers in local directories,
// rather than explicit filesystem mirrors -- doesn't do
// any hash verification anyway, and this is consistent with
// the FIXME in the unpacked case above even though technically
// we _could_ populate SHA256Sum here right now.
}
ret[providerAddr] = append(ret[providerAddr], meta)
}
return nil
})
if err != nil {
return nil, err
}
// Sort the results to be deterministic (aside from semver build metadata)
// and consistent with ordering from other functions.
for _, l := range ret {
l.Sort()
}
return ret, nil
}
// UnpackedDirectoryPathForPackage is similar to
// PackageMeta.UnpackedDirectoryPath but makes its decision based on
// individually-passed provider address, version, and target platform so that
// it can be used by callers outside this package that may have other
// types that represent package identifiers.
func UnpackedDirectoryPathForPackage(baseDir string, provider addrs.Provider, version Version, platform Platform) string {
return filepath.ToSlash(filepath.Join(
baseDir,
provider.Hostname.ForDisplay(), provider.Namespace, provider.Type,
version.String(),
platform.String(),
))
}
// PackedFilePathForPackage is similar to
// PackageMeta.PackedFilePath but makes its decision based on
// individually-passed provider address, version, and target platform so that
// it can be used by callers outside this package that may have other
// types that represent package identifiers.
func PackedFilePathForPackage(baseDir string, provider addrs.Provider, version Version, platform Platform) string {
return filepath.ToSlash(filepath.Join(
baseDir,
provider.Hostname.ForDisplay(), provider.Namespace, provider.Type,
fmt.Sprintf("terraform-provider-%s_%s_%s.zip", provider.Type, version.String(), platform.String()),
))
}