github.com/YousefHaggyHeroku/pack@v1.5.5/internal/registry/registry_cache.go (about) 1 package registry 2 3 import ( 4 "bufio" 5 "crypto/sha256" 6 "encoding/hex" 7 "encoding/json" 8 "fmt" 9 "io/ioutil" 10 "net/url" 11 "os" 12 "path/filepath" 13 "runtime" 14 "time" 15 16 "github.com/pkg/errors" 17 "golang.org/x/mod/semver" 18 "gopkg.in/src-d/go-git.v4" 19 "gopkg.in/src-d/go-git.v4/plumbing/object" 20 21 "github.com/YousefHaggyHeroku/pack/buildpack" 22 "github.com/YousefHaggyHeroku/pack/internal/style" 23 "github.com/YousefHaggyHeroku/pack/logging" 24 ) 25 26 const DefaultRegistryURL = "https://github.com/buildpacks/registry-index" 27 const DefaultRegistryName = "official" 28 const defaultRegistryDir = "registry" 29 30 // Cache is a RegistryCache 31 type Cache struct { 32 logger logging.Logger 33 url *url.URL 34 Root string 35 } 36 37 const GithubIssueTitleTemplate = "{{ if .Yanked }}YANK{{ else }}ADD{{ end }} {{.Namespace}}/{{.Name}}@{{.Version}}" 38 const GithubIssueBodyTemplate = ` 39 id = "{{.Namespace}}/{{.Name}}" 40 version = "{{.Version}}" 41 {{ if .Yanked }}{{ else if .Address }}addr = "{{.Address}}"{{ end }} 42 ` 43 const GitCommitTemplate = `{{ if .Yanked }}YANK{{else}}ADD{{end}} {{.Namespace}}/{{.Name}}@{{.Version}}` 44 45 // Entry is a list of buildpacks stored in a registry 46 type Entry struct { 47 Buildpacks []Buildpack `json:"buildpacks"` 48 } 49 50 // NewDefaultRegistryCache creates a new registry cache with default options 51 func NewDefaultRegistryCache(logger logging.Logger, home string) (Cache, error) { 52 return NewRegistryCache(logger, home, DefaultRegistryURL) 53 } 54 55 // NewRegistryCache creates a new registry cache 56 func NewRegistryCache(logger logging.Logger, home, registryURL string) (Cache, error) { 57 if _, err := os.Stat(home); err != nil { 58 return Cache{}, errors.Wrapf(err, "finding home %s", home) 59 } 60 61 normalizedURL, err := url.Parse(registryURL) 62 if err != nil { 63 return Cache{}, errors.Wrapf(err, "parsing registry url %s", registryURL) 64 } 65 66 key := sha256.New() 67 key.Write([]byte(normalizedURL.String())) 68 cacheDir := fmt.Sprintf("%s-%s", defaultRegistryDir, hex.EncodeToString(key.Sum(nil))) 69 70 return Cache{ 71 url: normalizedURL, 72 logger: logger, 73 Root: filepath.Join(home, cacheDir), 74 }, nil 75 } 76 77 // LocateBuildpack stored in registry 78 func (r *Cache) LocateBuildpack(bp string) (Buildpack, error) { 79 err := r.Refresh() 80 if err != nil { 81 return Buildpack{}, errors.Wrap(err, "refreshing cache") 82 } 83 84 ns, name, version, err := buildpack.ParseRegistryID(bp) 85 if err != nil { 86 return Buildpack{}, errors.Wrap(err, "parsing buildpacks registry id") 87 } 88 89 entry, err := r.readEntry(ns, name) 90 if err != nil { 91 return Buildpack{}, errors.Wrap(err, "reading entry") 92 } 93 94 if len(entry.Buildpacks) > 0 { 95 if version == "" { 96 highestVersion := entry.Buildpacks[0] 97 if len(entry.Buildpacks) > 1 { 98 for _, bp := range entry.Buildpacks[1:] { 99 if semver.Compare(fmt.Sprintf("v%s", bp.Version), fmt.Sprintf("v%s", highestVersion.Version)) > 0 { 100 highestVersion = bp 101 } 102 } 103 } 104 return highestVersion, Validate(highestVersion) 105 } 106 107 for _, bpIndex := range entry.Buildpacks { 108 if bpIndex.Version == version { 109 return bpIndex, Validate(bpIndex) 110 } 111 } 112 return Buildpack{}, fmt.Errorf("could not find version for buildpack: %s", bp) 113 } 114 115 return Buildpack{}, fmt.Errorf("no entries for buildpack: %s", bp) 116 } 117 118 // Refresh local Registry Cache 119 func (r *Cache) Refresh() error { 120 r.logger.Debugf("Refreshing registry cache for %s/%s", r.url.Host, r.url.Path) 121 122 if err := r.Initialize(); err != nil { 123 return errors.Wrapf(err, "initializing (%s)", r.Root) 124 } 125 126 repository, err := git.PlainOpen(r.Root) 127 if err != nil { 128 return errors.Wrapf(err, "opening (%s)", r.Root) 129 } 130 131 w, err := repository.Worktree() 132 if err != nil { 133 return errors.Wrapf(err, "reading (%s)", r.Root) 134 } 135 136 err = w.Pull(&git.PullOptions{RemoteName: "origin"}) 137 if err == git.NoErrAlreadyUpToDate { 138 return nil 139 } 140 return err 141 } 142 143 // Initialize a local Registry Cache 144 func (r *Cache) Initialize() error { 145 _, err := os.Stat(r.Root) 146 if err != nil { 147 if os.IsNotExist(err) { 148 err = r.CreateCache() 149 if err != nil { 150 return errors.Wrap(err, "creating registry cache") 151 } 152 } 153 } 154 155 if err := r.validateCache(); err != nil { 156 err = os.RemoveAll(r.Root) 157 if err != nil { 158 return errors.Wrap(err, "resetting registry cache") 159 } 160 err = r.CreateCache() 161 if err != nil { 162 return errors.Wrap(err, "rebuilding registry cache") 163 } 164 } 165 166 return nil 167 } 168 169 // CreateCache creates the cache on the filesystem 170 func (r *Cache) CreateCache() error { 171 r.logger.Debugf("Creating registry cache for %s/%s", r.url.Host, r.url.Path) 172 173 root, err := ioutil.TempDir("", "registry") 174 if err != nil { 175 return err 176 } 177 178 repository, err := git.PlainClone(root, false, &git.CloneOptions{ 179 URL: r.url.String(), 180 }) 181 if err != nil { 182 return errors.Wrap(err, "cloning remote registry") 183 } 184 185 w, err := repository.Worktree() 186 if err != nil { 187 return err 188 } 189 190 return os.Rename(w.Filesystem.Root(), r.Root) 191 } 192 193 func (r *Cache) validateCache() error { 194 r.logger.Debugf("Validating registry cache for %s/%s", r.url.Host, r.url.Path) 195 196 repository, err := git.PlainOpen(r.Root) 197 if err != nil { 198 return errors.Wrap(err, "opening registry cache") 199 } 200 201 remotes, err := repository.Remotes() 202 if err != nil { 203 return errors.Wrap(err, "accessing registry cache") 204 } 205 206 for _, remote := range remotes { 207 if remote.Config().Name == "origin" && remotes[0].Config().URLs[0] != r.url.String() { 208 return nil 209 } 210 } 211 return errors.New("invalid registry cache remote") 212 } 213 214 // Commit a Buildpack change 215 func (r *Cache) Commit(b Buildpack, username, msg string) error { 216 r.logger.Debugf("Creating commit in registry cache") 217 218 if msg == "" { 219 return errors.New("invalid commit message") 220 } 221 222 repository, err := git.PlainOpen(r.Root) 223 if err != nil { 224 return errors.Wrap(err, "opening registry cache") 225 } 226 227 w, err := repository.Worktree() 228 if err != nil { 229 return errors.Wrapf(err, "reading %s", style.Symbol(r.Root)) 230 } 231 232 index, err := r.writeEntry(b) 233 if err != nil { 234 return errors.Wrapf(err, "writing %s", style.Symbol(index)) 235 } 236 237 relativeIndexFile, err := filepath.Rel(r.Root, index) 238 if err != nil { 239 return errors.Wrap(err, "resolving relative path") 240 } 241 242 if _, err := w.Add(relativeIndexFile); err != nil { 243 return errors.Wrapf(err, "adding %s", style.Symbol(index)) 244 } 245 246 if _, err := w.Commit(msg, &git.CommitOptions{ 247 Author: &object.Signature{ 248 Name: username, 249 Email: "", 250 When: time.Now(), 251 }, 252 }); err != nil { 253 return errors.Wrapf(err, "committing") 254 } 255 256 return nil 257 } 258 259 func (r *Cache) writeEntry(b Buildpack) (string, error) { 260 var ns = b.Namespace 261 var name = b.Name 262 263 index, err := IndexPath(r.Root, ns, name) 264 if err != nil { 265 return "", err 266 } 267 268 if _, err := os.Stat(index); os.IsNotExist(err) { 269 if err := os.MkdirAll(filepath.Dir(index), 0755); err != nil { 270 return "", errors.Wrapf(err, "creating directory structure for: %s/%s", ns, name) 271 } 272 } else { 273 if _, err := os.Stat(index); err == nil { 274 entry, err := r.readEntry(ns, name) 275 if err != nil { 276 return "", errors.Wrapf(err, "reading existing buildpack entries") 277 } 278 279 availableBuildpacks := entry.Buildpacks 280 281 if len(availableBuildpacks) != 0 { 282 if availableBuildpacks[len(availableBuildpacks)-1].Version == b.Version { 283 return "", errors.Wrapf(err, "same version exists, upgrade the version to add") 284 } 285 } 286 } 287 } 288 289 f, err := os.OpenFile(index, os.O_APPEND|os.O_CREATE|os.O_RDWR, 0644) 290 if err != nil { 291 return "", errors.Wrapf(err, "creating buildpack file: %s/%s", ns, name) 292 } 293 defer f.Close() 294 295 newline := "\n" 296 if runtime.GOOS == "windows" { 297 newline = "\r\n" 298 } 299 300 fileContents, err := json.Marshal(b) 301 if err != nil { 302 return "", errors.Wrapf(err, "converting buildpack file to json: %s/%s", ns, name) 303 } 304 305 fileContentsFormatted := string(fileContents) + newline 306 if _, err := f.WriteString(fileContentsFormatted); err != nil { 307 return "", errors.Wrapf(err, "writing buildpack to file: %s/%s", ns, name) 308 } 309 310 return index, nil 311 } 312 313 func (r *Cache) readEntry(ns, name string) (Entry, error) { 314 index, err := IndexPath(r.Root, ns, name) 315 if err != nil { 316 return Entry{}, err 317 } 318 319 if _, err := os.Stat(index); err != nil { 320 return Entry{}, errors.Wrapf(err, "finding buildpack: %s/%s", ns, name) 321 } 322 323 file, err := os.Open(index) 324 if err != nil { 325 return Entry{}, errors.Wrapf(err, "opening index for buildpack: %s/%s", ns, name) 326 } 327 defer file.Close() 328 329 entry := Entry{} 330 scanner := bufio.NewScanner(file) 331 for scanner.Scan() { 332 var bp Buildpack 333 err = json.Unmarshal([]byte(scanner.Text()), &bp) 334 if err != nil { 335 return Entry{}, errors.Wrapf(err, "parsing index for buildpack: %s/%s", ns, name) 336 } 337 338 entry.Buildpacks = append(entry.Buildpacks, bp) 339 } 340 341 if err := scanner.Err(); err != nil { 342 return entry, errors.Wrapf(err, "reading index for buildpack: %s/%s", ns, name) 343 } 344 345 return entry, nil 346 }