github.com/eliastor/durgaform@v0.0.0-20220816172711-d0ab2d17673e/internal/getproviders/filesystem_search.go (about) 1 package getproviders 2 3 import ( 4 "fmt" 5 "log" 6 "os" 7 "path/filepath" 8 "strings" 9 10 svchost "github.com/hashicorp/terraform-svchost" 11 12 "github.com/eliastor/durgaform/internal/addrs" 13 ) 14 15 // SearchLocalDirectory performs an immediate, one-off scan of the given base 16 // directory for provider plugins using the directory structure defined for 17 // FilesystemMirrorSource. 18 // 19 // This is separated to allow other callers, such as the provider plugin cache 20 // management in the "internal/providercache" package, to use the same 21 // directory structure conventions. 22 func SearchLocalDirectory(baseDir string) (map[addrs.Provider]PackageMetaList, error) { 23 ret := make(map[addrs.Provider]PackageMetaList) 24 25 // We don't support symlinks at intermediate points inside the directory 26 // hierarchy because that could potentially cause our walk to get into 27 // an infinite loop, but as a measure of pragmatism we'll allow the 28 // top-level location itself to be a symlink, so that a user can 29 // potentially keep their plugins in a non-standard location but use a 30 // symlink to help Durgaform find them anyway. 31 originalBaseDir := baseDir 32 if finalDir, err := filepath.EvalSymlinks(baseDir); err == nil { 33 if finalDir != filepath.Clean(baseDir) { 34 log.Printf("[TRACE] getproviders.SearchLocalDirectory: using %s instead of %s", finalDir, baseDir) 35 } 36 baseDir = finalDir 37 } else { 38 // We'll eat this particular error because if we're somehow able to 39 // find plugins via baseDir below anyway then we'd rather do that than 40 // hard fail, but we'll log it in case it's useful for diagnosing why 41 // discovery didn't produce the expected outcome. 42 log.Printf("[TRACE] getproviders.SearchLocalDirectory: failed to resolve symlinks for %s: %s", baseDir, err) 43 } 44 45 err := filepath.Walk(baseDir, func(fullPath string, info os.FileInfo, err error) error { 46 if err != nil { 47 return fmt.Errorf("cannot search %s: %s", fullPath, err) 48 } 49 50 // There are two valid directory structures that we support here... 51 // Unpacked: registry.durgaform.io/hashicorp/aws/2.0.0/linux_amd64 (a directory) 52 // Packed: registry.durgaform.io/hashicorp/aws/terraform-provider-aws_2.0.0_linux_amd64.zip (a file) 53 // 54 // Both of these give us enough information to identify the package 55 // metadata. 56 fsPath, err := filepath.Rel(baseDir, fullPath) 57 if err != nil { 58 // This should never happen because the filepath.Walk contract is 59 // for the paths to include the base path. 60 log.Printf("[TRACE] getproviders.SearchLocalDirectory: ignoring malformed path %q during walk: %s", fullPath, err) 61 return nil 62 } 63 relPath := filepath.ToSlash(fsPath) 64 parts := strings.Split(relPath, "/") 65 66 if len(parts) < 3 { 67 // Likely a prefix of a valid path, so we'll ignore it and visit 68 // the full valid path on a later call. 69 70 if (info.Mode() & os.ModeSymlink) != 0 { 71 // We don't allow symlinks for intermediate steps in the 72 // hierarchy because otherwise this walk would risk getting 73 // itself into an infinite loop, but if we do find one then 74 // we'll warn about it to help with debugging. 75 log.Printf("[WARN] Provider plugin search ignored symlink %s: only the base directory %s may be a symlink", fullPath, originalBaseDir) 76 } 77 78 return nil 79 } 80 81 hostnameGiven := parts[0] 82 namespace := parts[1] 83 typeName := parts[2] 84 85 // validate each part 86 // The legacy provider namespace is a special case. 87 if namespace != addrs.LegacyProviderNamespace { 88 _, err = addrs.ParseProviderPart(namespace) 89 if err != nil { 90 log.Printf("[WARN] local provider path %q contains invalid namespace %q; ignoring", fullPath, namespace) 91 return nil 92 } 93 } 94 95 _, err = addrs.ParseProviderPart(typeName) 96 if err != nil { 97 log.Printf("[WARN] local provider path %q contains invalid type %q; ignoring", fullPath, typeName) 98 return nil 99 } 100 101 hostname, err := svchost.ForComparison(hostnameGiven) 102 if err != nil { 103 log.Printf("[WARN] local provider path %q contains invalid hostname %q; ignoring", fullPath, hostnameGiven) 104 return nil 105 } 106 var providerAddr addrs.Provider 107 if namespace == addrs.LegacyProviderNamespace { 108 if hostname != addrs.DefaultProviderRegistryHost { 109 log.Printf("[WARN] local provider path %q indicates a legacy provider not on the default registry host; ignoring", fullPath) 110 return nil 111 } 112 providerAddr = addrs.NewLegacyProvider(typeName) 113 } else { 114 providerAddr = addrs.NewProvider(hostname, namespace, typeName) 115 } 116 117 // The "info" passed to our function is an Lstat result, so it might 118 // be referring to a symbolic link. We'll do a full "Stat" on it 119 // now to make sure we're making tests against the real underlying 120 // filesystem object below. 121 info, err = os.Stat(fullPath) 122 if err != nil { 123 log.Printf("[WARN] failed to read metadata about %s: %s", fullPath, err) 124 return nil 125 } 126 127 switch len(parts) { 128 case 5: // Might be unpacked layout 129 if !info.IsDir() { 130 return nil // packed layout requires a directory 131 } 132 133 versionStr := parts[3] 134 version, err := ParseVersion(versionStr) 135 if err != nil { 136 log.Printf("[WARN] ignoring local provider path %q with invalid version %q: %s", fullPath, versionStr, err) 137 return nil 138 } 139 140 platformStr := parts[4] 141 platform, err := ParsePlatform(platformStr) 142 if err != nil { 143 log.Printf("[WARN] ignoring local provider path %q with invalid platform %q: %s", fullPath, platformStr, err) 144 return nil 145 } 146 147 log.Printf("[TRACE] getproviders.SearchLocalDirectory: found %s v%s for %s at %s", providerAddr, version, platform, fullPath) 148 149 meta := PackageMeta{ 150 Provider: providerAddr, 151 Version: version, 152 153 // FIXME: How do we populate this? 154 ProtocolVersions: nil, 155 TargetPlatform: platform, 156 157 // Because this is already unpacked, the filename is synthetic 158 // based on the standard naming scheme. 159 Filename: fmt.Sprintf("durgaform-provider-%s_%s_%s.zip", providerAddr.Type, version, platform), 160 Location: PackageLocalDir(fullPath), 161 162 // FIXME: What about the SHA256Sum field? As currently specified 163 // it's a hash of the zip file, but this thing is already 164 // unpacked and so we don't have the zip file to hash. 165 } 166 ret[providerAddr] = append(ret[providerAddr], meta) 167 168 case 4: // Might be packed layout 169 if info.IsDir() { 170 return nil // packed layout requires a file 171 } 172 173 filename := filepath.Base(fsPath) 174 // the filename components are matched case-insensitively, and 175 // the normalized form of them is in lowercase so we'll convert 176 // to lowercase for comparison here. (This normalizes only for case, 177 // because that is the primary constraint affecting compatibility 178 // between filesystem implementations on different platforms; 179 // filenames are expected to be pre-normalized and valid in other 180 // regards.) 181 normFilename := strings.ToLower(filename) 182 183 // In the packed layout, the version number and target platform 184 // are derived from the package filename, but only if the 185 // filename has the expected prefix identifying it as a package 186 // for the provider in question, and the suffix identifying it 187 // as a zip file. 188 prefix := "durgaform-provider-" + providerAddr.Type + "_" 189 const suffix = ".zip" 190 if !strings.HasPrefix(normFilename, prefix) { 191 log.Printf("[WARN] ignoring file %q as possible package for %s: filename lacks expected prefix %q", fsPath, providerAddr, prefix) 192 return nil 193 } 194 if !strings.HasSuffix(normFilename, suffix) { 195 log.Printf("[WARN] ignoring file %q as possible package for %s: filename lacks expected suffix %q", fsPath, providerAddr, suffix) 196 return nil 197 } 198 199 // Extract the version and target part of the filename, which 200 // will look like "2.1.0_linux_amd64" 201 infoSlice := normFilename[len(prefix) : len(normFilename)-len(suffix)] 202 infoParts := strings.Split(infoSlice, "_") 203 if len(infoParts) < 3 { 204 log.Printf("[WARN] ignoring file %q as possible package for %s: filename does not include version number, target OS, and target architecture", fsPath, providerAddr) 205 return nil 206 } 207 208 versionStr := infoParts[0] 209 version, err := ParseVersion(versionStr) 210 if err != nil { 211 log.Printf("[WARN] ignoring local provider path %q with invalid version %q: %s", fullPath, versionStr, err) 212 return nil 213 } 214 215 // We'll reassemble this back into a single string just so we can 216 // easily re-use our existing parser and its normalization rules. 217 platformStr := infoParts[1] + "_" + infoParts[2] 218 platform, err := ParsePlatform(platformStr) 219 if err != nil { 220 log.Printf("[WARN] ignoring local provider path %q with invalid platform %q: %s", fullPath, platformStr, err) 221 return nil 222 } 223 224 log.Printf("[TRACE] getproviders.SearchLocalDirectory: found %s v%s for %s at %s", providerAddr, version, platform, fullPath) 225 226 meta := PackageMeta{ 227 Provider: providerAddr, 228 Version: version, 229 230 // FIXME: How do we populate this? 231 ProtocolVersions: nil, 232 TargetPlatform: platform, 233 234 // Because this is already unpacked, the filename is synthetic 235 // based on the standard naming scheme. 236 Filename: normFilename, // normalized filename, because this field says what it _should_ be called, not what it _is_ called 237 Location: PackageLocalArchive(fullPath), // non-normalized here, because this is the actual physical location 238 239 // TODO: Also populate the SHA256Sum field. Skipping that 240 // for now because our initial uses of this result -- 241 // scanning already-installed providers in local directories, 242 // rather than explicit filesystem mirrors -- doesn't do 243 // any hash verification anyway, and this is consistent with 244 // the FIXME in the unpacked case above even though technically 245 // we _could_ populate SHA256Sum here right now. 246 } 247 ret[providerAddr] = append(ret[providerAddr], meta) 248 249 } 250 251 return nil 252 }) 253 if err != nil { 254 return nil, err 255 } 256 // Sort the results to be deterministic (aside from semver build metadata) 257 // and consistent with ordering from other functions. 258 for _, l := range ret { 259 l.Sort() 260 } 261 return ret, nil 262 } 263 264 // UnpackedDirectoryPathForPackage is similar to 265 // PackageMeta.UnpackedDirectoryPath but makes its decision based on 266 // individually-passed provider address, version, and target platform so that 267 // it can be used by callers outside this package that may have other 268 // types that represent package identifiers. 269 func UnpackedDirectoryPathForPackage(baseDir string, provider addrs.Provider, version Version, platform Platform) string { 270 return filepath.ToSlash(filepath.Join( 271 baseDir, 272 provider.Hostname.ForDisplay(), provider.Namespace, provider.Type, 273 version.String(), 274 platform.String(), 275 )) 276 } 277 278 // PackedFilePathForPackage is similar to 279 // PackageMeta.PackedFilePath but makes its decision based on 280 // individually-passed provider address, version, and target platform so that 281 // it can be used by callers outside this package that may have other 282 // types that represent package identifiers. 283 func PackedFilePathForPackage(baseDir string, provider addrs.Provider, version Version, platform Platform) string { 284 return filepath.ToSlash(filepath.Join( 285 baseDir, 286 provider.Hostname.ForDisplay(), provider.Namespace, provider.Type, 287 fmt.Sprintf("durgaform-provider-%s_%s_%s.zip", provider.Type, version.String(), platform.String()), 288 )) 289 }