github.com/wolfi-dev/wolfictl@v0.16.11/pkg/distro/detect.go (about)

     1  package distro
     2  
     3  import (
     4  	"fmt"
     5  	"os"
     6  	"path/filepath"
     7  
     8  	"github.com/go-git/go-git/v5"
     9  	"github.com/go-git/go-git/v5/plumbing"
    10  	wgit "github.com/wolfi-dev/wolfictl/pkg/git"
    11  	"golang.org/x/exp/slices"
    12  )
    13  
    14  // Detect tries to automatically detect which distro the user wants to operate
    15  // on by trying to match the current working directory with a known repository
    16  // for a distro's packages or advisories.
    17  func Detect() (Distro, error) {
    18  	cwd, err := os.Getwd()
    19  	if err != nil {
    20  		return Distro{}, err
    21  	}
    22  
    23  	d, err := DetectFromDir(cwd)
    24  	if err != nil {
    25  		return Distro{}, err
    26  	}
    27  
    28  	return d, nil
    29  }
    30  
    31  // DetectFromDir tries to identify a Distro by inspecting the given directory to
    32  // see if it is a repository for a distro's packages or advisories.
    33  func DetectFromDir(dir string) (Distro, error) {
    34  	distro, err := identifyDistroFromLocalRepoDir(dir)
    35  	if err != nil {
    36  		return Distro{}, err
    37  	}
    38  
    39  	// We assume that the parent directory of the initially found repo directory is
    40  	// a directory that contains all the relevant repo directories.
    41  	dirOfRepos := filepath.Dir(dir)
    42  
    43  	// We either have a distro (packages) repo or an advisories repo, but not both.
    44  	// Now we need to find the other one.
    45  
    46  	switch {
    47  	case distro.Local.PackagesRepo.Dir == "":
    48  		distroDir, err := findDistroDir(distro.Absolute, dirOfRepos)
    49  		if err != nil {
    50  			return Distro{}, fmt.Errorf("unable to find distro (packages) dir: %w", err)
    51  		}
    52  		distro.Local.PackagesRepo.Dir = distroDir
    53  		d, err := identifyDistroFromLocalRepoDir(distroDir)
    54  		if err != nil {
    55  			return Distro{}, err
    56  		}
    57  		distro.Local.PackagesRepo.UpstreamName = d.Local.PackagesRepo.UpstreamName
    58  		forkPoint, err := findRepoForkPoint(distroDir, d.Local.PackagesRepo.UpstreamName)
    59  		if err != nil {
    60  			return Distro{}, err
    61  		}
    62  		distro.Local.PackagesRepo.ForkPoint = forkPoint
    63  
    64  		// We also still need to get the advisories repo fork point.
    65  		fp, err := findRepoForkPoint(distro.Local.AdvisoriesRepo.Dir, distro.Local.AdvisoriesRepo.UpstreamName)
    66  		if err != nil {
    67  			return Distro{}, err
    68  		}
    69  		distro.Local.AdvisoriesRepo.ForkPoint = fp
    70  		return distro, nil
    71  
    72  	case distro.Local.AdvisoriesRepo.Dir == "":
    73  		advisoryDir, err := findAdvisoriesDir(distro.Absolute, dirOfRepos)
    74  		if err != nil {
    75  			return Distro{}, fmt.Errorf("unable to find advisories dir: %w", err)
    76  		}
    77  		distro.Local.AdvisoriesRepo.Dir = advisoryDir
    78  		d, err := identifyDistroFromLocalRepoDir(advisoryDir)
    79  		if err != nil {
    80  			return Distro{}, err
    81  		}
    82  		distro.Local.AdvisoriesRepo.UpstreamName = d.Local.AdvisoriesRepo.UpstreamName
    83  		forkPoint, err := findRepoForkPoint(advisoryDir, d.Local.AdvisoriesRepo.UpstreamName)
    84  		if err != nil {
    85  			return Distro{}, err
    86  		}
    87  		distro.Local.AdvisoriesRepo.ForkPoint = forkPoint
    88  
    89  		// We also still need to get the distro (packages) repo fork point.
    90  		fp, err := findRepoForkPoint(distro.Local.PackagesRepo.Dir, distro.Local.PackagesRepo.UpstreamName)
    91  		if err != nil {
    92  			return Distro{}, err
    93  		}
    94  		distro.Local.PackagesRepo.ForkPoint = fp
    95  		return distro, nil
    96  	}
    97  
    98  	return Distro{}, fmt.Errorf("unable to detect distro")
    99  }
   100  
   101  var ErrNotDistroRepo = fmt.Errorf("directory is not a distro (packages) or advisories repository")
   102  
   103  func identifyDistroFromLocalRepoDir(dir string) (Distro, error) {
   104  	repo, err := git.PlainOpen(dir)
   105  	if err != nil {
   106  		return Distro{}, fmt.Errorf("unable to identify distro: couldn't open git repo: %v: %w", err, ErrNotDistroRepo)
   107  	}
   108  
   109  	config, err := repo.Config()
   110  	if err != nil {
   111  		return Distro{}, err
   112  	}
   113  
   114  	for _, remoteConfig := range config.Remotes {
   115  		urls := remoteConfig.URLs
   116  		if len(urls) == 0 {
   117  			continue
   118  		}
   119  
   120  		url := urls[0]
   121  
   122  		for _, d := range []AbsoluteProperties{wolfiDistro, chainguardDistro, extraPackagesDistro} {
   123  			// Fill in the local properties that we can cheaply here. We'll fill in the rest
   124  			// later, outside of this function call.
   125  
   126  			if slices.Contains(d.DistroRemoteURLs(), url) {
   127  				return Distro{
   128  					Absolute: d,
   129  					Local: LocalProperties{
   130  						PackagesRepo: LocalRepo{
   131  							Dir:          dir,
   132  							UpstreamName: remoteConfig.Name,
   133  							ForkPoint:    "", // This is slightly expensive to compute, so we do it later and only once per repo.
   134  						},
   135  					},
   136  				}, nil
   137  			}
   138  
   139  			if slices.Contains(d.AdvisoriesRemoteURLs(), url) {
   140  				return Distro{
   141  					Absolute: d,
   142  					Local: LocalProperties{
   143  						AdvisoriesRepo: LocalRepo{
   144  							Dir:          dir,
   145  							UpstreamName: remoteConfig.Name,
   146  							ForkPoint:    "", // This is slightly expensive to compute, so we do it later and only once per repo.
   147  						},
   148  					},
   149  				}, nil
   150  			}
   151  		}
   152  	}
   153  
   154  	return Distro{}, ErrNotDistroRepo
   155  }
   156  
   157  func findRepoForkPoint(repoDir, remoteName string) (string, error) {
   158  	repo, err := git.PlainOpen(repoDir)
   159  	if err != nil {
   160  		return "", fmt.Errorf("unable to find fork point for remote %q: %w", remoteName, err)
   161  	}
   162  
   163  	remote, err := repo.Remote(remoteName)
   164  	if err != nil {
   165  		return "", fmt.Errorf("unable to find fork point for remote %q: %w", remoteName, err)
   166  	}
   167  
   168  	head, err := repo.Head()
   169  	if err != nil {
   170  		return "", fmt.Errorf("unable to find fork point for remote %q: %w", remoteName, err)
   171  	}
   172  
   173  	upstreamRefName := fmt.Sprintf("refs/remotes/%s/main", remote.Config().Name)
   174  	upstreamRef, err := repo.Reference(plumbing.ReferenceName(upstreamRefName), true)
   175  	if err != nil {
   176  		return "", fmt.Errorf("unable to get upstream ref %q: %w", upstreamRefName, err)
   177  	}
   178  	forkPoint, err := wgit.FindForkPoint(repo, head, upstreamRef)
   179  	if err != nil {
   180  		return "", fmt.Errorf("unable to find fork point for remote %q: %w", remoteName, err)
   181  	}
   182  
   183  	return forkPoint.String(), nil
   184  }
   185  
   186  // findDistroDir returns the local filesystem path to the directory for the
   187  // distro (packages) repo for the given targetDistro, by examining the child
   188  // directories within dirOfRepos.
   189  func findDistroDir(targetDistro AbsoluteProperties, dirOfRepos string) (string, error) {
   190  	return findRepoDir(targetDistro, dirOfRepos, func(d Distro) string {
   191  		return d.Local.PackagesRepo.Dir
   192  	})
   193  }
   194  
   195  // findAdvisoriesDir returns the local filesystem path to the directory for the
   196  // advisories repo for the given targetDistro, by examining the child
   197  // directories within dirOfRepos.
   198  func findAdvisoriesDir(targetDistro AbsoluteProperties, dirOfRepos string) (string, error) {
   199  	return findRepoDir(targetDistro, dirOfRepos, func(d Distro) string {
   200  		return d.Local.AdvisoriesRepo.Dir
   201  	})
   202  }
   203  
   204  // findRepoDir looks for a repo directory (for either a distro/package repo or
   205  // an advisories repo) in the given directory of repos that matches the given
   206  // distro. It uses the given function to extract the repo directory from a
   207  // Distro.
   208  func findRepoDir(targetDistro AbsoluteProperties, dirOfRepos string, getRepoDir func(Distro) string) (string, error) {
   209  	files, err := os.ReadDir(dirOfRepos)
   210  	if err != nil {
   211  		return "", err
   212  	}
   213  
   214  	for _, f := range files {
   215  		if !f.IsDir() {
   216  			continue
   217  		}
   218  
   219  		d, err := identifyDistroFromLocalRepoDir(filepath.Join(dirOfRepos, f.Name()))
   220  		if err != nil {
   221  			// no usable distro or advisories repo here
   222  			continue
   223  		}
   224  		if d.Absolute.Name != targetDistro.Name {
   225  			// This is not the distro you're looking for... 👋
   226  			continue
   227  		}
   228  
   229  		dir := getRepoDir(d)
   230  		if dir == "" {
   231  			continue
   232  		}
   233  
   234  		return dir, nil
   235  	}
   236  
   237  	return "", fmt.Errorf("unable to find repo dir")
   238  }