]>
Commit | Line | Data |
---|---|---|
c680a8e1 RS |
1 | package discovery |
2 | ||
3 | import ( | |
4 | "io/ioutil" | |
5 | "log" | |
15c0b25d | 6 | "os" |
c680a8e1 RS |
7 | "path/filepath" |
8 | "strings" | |
9 | ) | |
10 | ||
11 | // FindPlugins looks in the given directories for files whose filenames | |
12 | // suggest that they are plugins of the given kind (e.g. "provider") and | |
13 | // returns a PluginMetaSet representing the discovered potential-plugins. | |
14 | // | |
15 | // Currently this supports two different naming schemes. The current | |
16 | // standard naming scheme is a subdirectory called $GOOS-$GOARCH containing | |
17 | // files named terraform-$KIND-$NAME-V$VERSION. The legacy naming scheme is | |
18 | // files directly in the given directory whose names are like | |
19 | // terraform-$KIND-$NAME. | |
20 | // | |
21 | // Only one plugin will be returned for each unique plugin (name, version) | |
22 | // pair, with preference given to files found in earlier directories. | |
23 | // | |
24 | // This is a convenience wrapper around FindPluginPaths and ResolvePluginsPaths. | |
25 | func FindPlugins(kind string, dirs []string) PluginMetaSet { | |
26 | return ResolvePluginPaths(FindPluginPaths(kind, dirs)) | |
27 | } | |
28 | ||
29 | // FindPluginPaths looks in the given directories for files whose filenames | |
30 | // suggest that they are plugins of the given kind (e.g. "provider"). | |
31 | // | |
32 | // The return value is a list of absolute paths that appear to refer to | |
33 | // plugins in the given directories, based only on what can be inferred | |
34 | // from the naming scheme. The paths returned are ordered such that files | |
35 | // in later dirs appear after files in earlier dirs in the given directory | |
36 | // list. Within the same directory plugins are returned in a consistent but | |
37 | // undefined order. | |
38 | func FindPluginPaths(kind string, dirs []string) []string { | |
39 | // This is just a thin wrapper around findPluginPaths so that we can | |
40 | // use the latter in tests with a fake machineName so we can use our | |
41 | // test fixtures. | |
42 | return findPluginPaths(kind, dirs) | |
43 | } | |
44 | ||
45 | func findPluginPaths(kind string, dirs []string) []string { | |
46 | prefix := "terraform-" + kind + "-" | |
47 | ||
48 | ret := make([]string, 0, len(dirs)) | |
49 | ||
50 | for _, dir := range dirs { | |
51 | items, err := ioutil.ReadDir(dir) | |
52 | if err != nil { | |
53 | // Ignore missing dirs, non-dirs, etc | |
54 | continue | |
55 | } | |
56 | ||
57 | log.Printf("[DEBUG] checking for %s in %q", kind, dir) | |
58 | ||
59 | for _, item := range items { | |
60 | fullName := item.Name() | |
61 | ||
62 | if !strings.HasPrefix(fullName, prefix) { | |
c680a8e1 RS |
63 | continue |
64 | } | |
65 | ||
66 | // New-style paths must have a version segment in filename | |
67 | if strings.Contains(strings.ToLower(fullName), "_v") { | |
68 | absPath, err := filepath.Abs(filepath.Join(dir, fullName)) | |
69 | if err != nil { | |
70 | log.Printf("[ERROR] plugin filepath error: %s", err) | |
71 | continue | |
72 | } | |
73 | ||
15c0b25d AP |
74 | // Check that the file we found is usable |
75 | if !pathIsFile(absPath) { | |
76 | log.Printf("[ERROR] ignoring non-file %s", absPath) | |
77 | continue | |
78 | } | |
79 | ||
c680a8e1 RS |
80 | log.Printf("[DEBUG] found %s %q", kind, fullName) |
81 | ret = append(ret, filepath.Clean(absPath)) | |
82 | continue | |
83 | } | |
84 | ||
85 | // Legacy style with files directly in the base directory | |
86 | absPath, err := filepath.Abs(filepath.Join(dir, fullName)) | |
87 | if err != nil { | |
88 | log.Printf("[ERROR] plugin filepath error: %s", err) | |
89 | continue | |
90 | } | |
91 | ||
15c0b25d AP |
92 | // Check that the file we found is usable |
93 | if !pathIsFile(absPath) { | |
94 | log.Printf("[ERROR] ignoring non-file %s", absPath) | |
95 | continue | |
96 | } | |
97 | ||
98 | log.Printf("[WARN] found legacy %s %q", kind, fullName) | |
c680a8e1 RS |
99 | |
100 | ret = append(ret, filepath.Clean(absPath)) | |
101 | } | |
102 | } | |
103 | ||
104 | return ret | |
105 | } | |
106 | ||
15c0b25d AP |
107 | // Returns true if and only if the given path refers to a file or a symlink |
108 | // to a file. | |
109 | func pathIsFile(path string) bool { | |
110 | info, err := os.Stat(path) | |
111 | if err != nil { | |
112 | return false | |
113 | } | |
114 | ||
115 | return !info.IsDir() | |
116 | } | |
117 | ||
c680a8e1 RS |
118 | // ResolvePluginPaths takes a list of paths to plugin executables (as returned |
119 | // by e.g. FindPluginPaths) and produces a PluginMetaSet describing the | |
120 | // referenced plugins. | |
121 | // | |
122 | // If the same combination of plugin name and version appears multiple times, | |
123 | // the earlier reference will be preferred. Several different versions of | |
124 | // the same plugin name may be returned, in which case the methods of | |
125 | // PluginMetaSet can be used to filter down. | |
126 | func ResolvePluginPaths(paths []string) PluginMetaSet { | |
127 | s := make(PluginMetaSet) | |
128 | ||
129 | type nameVersion struct { | |
130 | Name string | |
131 | Version string | |
132 | } | |
133 | found := make(map[nameVersion]struct{}) | |
134 | ||
135 | for _, path := range paths { | |
136 | baseName := strings.ToLower(filepath.Base(path)) | |
137 | if !strings.HasPrefix(baseName, "terraform-") { | |
138 | // Should never happen with reasonable input | |
139 | continue | |
140 | } | |
141 | ||
142 | baseName = baseName[10:] | |
143 | firstDash := strings.Index(baseName, "-") | |
144 | if firstDash == -1 { | |
145 | // Should never happen with reasonable input | |
146 | continue | |
147 | } | |
148 | ||
149 | baseName = baseName[firstDash+1:] | |
150 | if baseName == "" { | |
151 | // Should never happen with reasonable input | |
152 | continue | |
153 | } | |
154 | ||
155 | // Trim the .exe suffix used on Windows before we start wrangling | |
156 | // the remainder of the path. | |
157 | if strings.HasSuffix(baseName, ".exe") { | |
158 | baseName = baseName[:len(baseName)-4] | |
159 | } | |
160 | ||
161 | parts := strings.SplitN(baseName, "_v", 2) | |
162 | name := parts[0] | |
163 | version := VersionZero | |
164 | if len(parts) == 2 { | |
165 | version = parts[1] | |
166 | } | |
167 | ||
168 | // Auto-installed plugins contain an extra name portion representing | |
169 | // the expected plugin version, which we must trim off. | |
170 | if underX := strings.Index(version, "_x"); underX != -1 { | |
171 | version = version[:underX] | |
172 | } | |
173 | ||
174 | if _, ok := found[nameVersion{name, version}]; ok { | |
175 | // Skip duplicate versions of the same plugin | |
176 | // (We do this during this step because after this we will be | |
177 | // dealing with sets and thus lose our ordering with which to | |
178 | // decide preference.) | |
179 | continue | |
180 | } | |
181 | ||
182 | s.Add(PluginMeta{ | |
183 | Name: name, | |
184 | Version: VersionStr(version), | |
185 | Path: path, | |
186 | }) | |
187 | found[nameVersion{name, version}] = struct{}{} | |
188 | } | |
189 | ||
190 | return s | |
191 | } |