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 }