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  }