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  }