| 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.DefaultProviderRegistryHost { |
| 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()), |
| )) |
| } |