mirror of https://github.com/buildpacks/pack.git
371 lines
9.2 KiB
Go
371 lines
9.2 KiB
Go
package registry
|
|
|
|
import (
|
|
"bufio"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/url"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"runtime"
|
|
"time"
|
|
|
|
"github.com/go-git/go-git/v5"
|
|
"github.com/go-git/go-git/v5/plumbing/object"
|
|
"github.com/pkg/errors"
|
|
"golang.org/x/mod/semver"
|
|
|
|
"github.com/buildpacks/pack/internal/style"
|
|
"github.com/buildpacks/pack/pkg/buildpack"
|
|
"github.com/buildpacks/pack/pkg/logging"
|
|
)
|
|
|
|
const DefaultRegistryURL = "https://github.com/buildpacks/registry-index"
|
|
const DefaultRegistryName = "official"
|
|
const defaultRegistryDir = "registry"
|
|
|
|
// Cache is a RegistryCache
|
|
type Cache struct {
|
|
logger logging.Logger
|
|
url *url.URL
|
|
Root string
|
|
RegistryDir string
|
|
}
|
|
|
|
const GithubIssueTitleTemplate = "{{ if .Yanked }}YANK{{ else }}ADD{{ end }} {{.Namespace}}/{{.Name}}@{{.Version}}"
|
|
const GithubIssueBodyTemplate = `
|
|
id = "{{.Namespace}}/{{.Name}}"
|
|
version = "{{.Version}}"
|
|
{{ if .Yanked }}{{ else if .Address }}addr = "{{.Address}}"{{ end }}
|
|
`
|
|
const GitCommitTemplate = `{{ if .Yanked }}YANK{{else}}ADD{{end}} {{.Namespace}}/{{.Name}}@{{.Version}}`
|
|
|
|
// Entry is a list of buildpacks stored in a registry
|
|
type Entry struct {
|
|
Buildpacks []Buildpack `json:"buildpacks"`
|
|
}
|
|
|
|
// NewDefaultRegistryCache creates a new registry cache with default options
|
|
func NewDefaultRegistryCache(logger logging.Logger, home string) (Cache, error) {
|
|
return NewRegistryCache(logger, home, DefaultRegistryURL)
|
|
}
|
|
|
|
// NewRegistryCache creates a new registry cache
|
|
func NewRegistryCache(logger logging.Logger, home, registryURL string) (Cache, error) {
|
|
if _, err := os.Stat(home); err != nil {
|
|
return Cache{}, errors.Wrapf(err, "finding home %s", home)
|
|
}
|
|
|
|
normalizedURL, err := url.Parse(registryURL)
|
|
if err != nil {
|
|
return Cache{}, errors.Wrapf(err, "parsing registry url %s", registryURL)
|
|
}
|
|
|
|
key := sha256.New()
|
|
key.Write([]byte(normalizedURL.String()))
|
|
cacheDir := fmt.Sprintf("%s-%s", defaultRegistryDir, hex.EncodeToString(key.Sum(nil)))
|
|
|
|
return Cache{
|
|
url: normalizedURL,
|
|
logger: logger,
|
|
Root: filepath.Join(home, cacheDir),
|
|
}, nil
|
|
}
|
|
|
|
// LocateBuildpack stored in registry
|
|
func (r *Cache) LocateBuildpack(bp string) (Buildpack, error) {
|
|
err := r.Refresh()
|
|
if err != nil {
|
|
return Buildpack{}, errors.Wrap(err, "refreshing cache")
|
|
}
|
|
|
|
ns, name, version, err := buildpack.ParseRegistryID(bp)
|
|
if err != nil {
|
|
return Buildpack{}, errors.Wrap(err, "parsing buildpacks registry id")
|
|
}
|
|
|
|
entry, err := r.readEntry(ns, name)
|
|
if err != nil {
|
|
return Buildpack{}, errors.Wrap(err, "reading entry")
|
|
}
|
|
|
|
if len(entry.Buildpacks) > 0 {
|
|
if version == "" {
|
|
highestVersion := entry.Buildpacks[0]
|
|
if len(entry.Buildpacks) > 1 {
|
|
for _, bp := range entry.Buildpacks[1:] {
|
|
if semver.Compare(fmt.Sprintf("v%s", bp.Version), fmt.Sprintf("v%s", highestVersion.Version)) > 0 {
|
|
highestVersion = bp
|
|
}
|
|
}
|
|
}
|
|
return highestVersion, Validate(highestVersion)
|
|
}
|
|
|
|
for _, bpIndex := range entry.Buildpacks {
|
|
if bpIndex.Version == version {
|
|
return bpIndex, Validate(bpIndex)
|
|
}
|
|
}
|
|
return Buildpack{}, fmt.Errorf("could not find version for buildpack: %s", bp)
|
|
}
|
|
|
|
return Buildpack{}, fmt.Errorf("no entries for buildpack: %s", bp)
|
|
}
|
|
|
|
// Refresh local Registry Cache
|
|
func (r *Cache) Refresh() error {
|
|
r.logger.Debugf("Refreshing registry cache for %s/%s", r.url.Host, r.url.Path)
|
|
|
|
if err := r.Initialize(); err != nil {
|
|
return errors.Wrapf(err, "initializing (%s)", r.Root)
|
|
}
|
|
|
|
repository, err := git.PlainOpen(r.Root)
|
|
if err != nil {
|
|
return errors.Wrapf(err, "opening (%s)", r.Root)
|
|
}
|
|
|
|
w, err := repository.Worktree()
|
|
if err != nil {
|
|
return errors.Wrapf(err, "reading (%s)", r.Root)
|
|
}
|
|
|
|
err = w.Pull(&git.PullOptions{RemoteName: "origin"})
|
|
if err == git.NoErrAlreadyUpToDate {
|
|
return nil
|
|
}
|
|
return err
|
|
}
|
|
|
|
// Initialize a local Registry Cache
|
|
func (r *Cache) Initialize() error {
|
|
_, err := os.Stat(r.Root)
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
err = r.CreateCache()
|
|
if err != nil {
|
|
return errors.Wrap(err, "creating registry cache")
|
|
}
|
|
}
|
|
}
|
|
|
|
if err := r.validateCache(); err != nil {
|
|
err = os.RemoveAll(r.Root)
|
|
if err != nil {
|
|
return errors.Wrap(err, "resetting registry cache")
|
|
}
|
|
err = r.CreateCache()
|
|
if err != nil {
|
|
return errors.Wrap(err, "rebuilding registry cache")
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// CreateCache creates the cache on the filesystem
|
|
func (r *Cache) CreateCache() error {
|
|
var repository *git.Repository
|
|
r.logger.Debugf("Creating registry cache for %s/%s", r.url.Host, r.url.Path)
|
|
|
|
registryDir, err := os.MkdirTemp(filepath.Dir(r.Root), "registry")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
r.RegistryDir = registryDir
|
|
|
|
if r.url.Host == "dev.azure.com" {
|
|
err = exec.Command("git", "clone", r.url.String(), r.RegistryDir).Run()
|
|
if err != nil {
|
|
return errors.Wrap(err, "cloning remote registry with native git")
|
|
}
|
|
|
|
repository, err = git.PlainOpen(r.RegistryDir)
|
|
if err != nil {
|
|
return errors.Wrap(err, "opening remote registry clone")
|
|
}
|
|
} else {
|
|
repository, err = git.PlainClone(r.RegistryDir, false, &git.CloneOptions{
|
|
URL: r.url.String(),
|
|
})
|
|
if err != nil {
|
|
return errors.Wrap(err, "cloning remote registry")
|
|
}
|
|
}
|
|
|
|
w, err := repository.Worktree()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = os.Rename(w.Filesystem.Root(), r.Root)
|
|
if err != nil {
|
|
if err == os.ErrExist {
|
|
// If pack is run concurrently, this action might have already occurred
|
|
return nil
|
|
}
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (r *Cache) validateCache() error {
|
|
r.logger.Debugf("Validating registry cache for %s/%s", r.url.Host, r.url.Path)
|
|
|
|
repository, err := git.PlainOpen(r.Root)
|
|
if err != nil {
|
|
return errors.Wrap(err, "opening registry cache")
|
|
}
|
|
|
|
remotes, err := repository.Remotes()
|
|
if err != nil {
|
|
return errors.Wrap(err, "accessing registry cache")
|
|
}
|
|
|
|
for _, remote := range remotes {
|
|
if remote.Config().Name == "origin" && remotes[0].Config().URLs[0] != r.url.String() {
|
|
return nil
|
|
}
|
|
}
|
|
return errors.New("invalid registry cache remote")
|
|
}
|
|
|
|
// Commit a Buildpack change
|
|
func (r *Cache) Commit(b Buildpack, username, msg string) error {
|
|
r.logger.Debugf("Creating commit in registry cache")
|
|
|
|
if msg == "" {
|
|
return errors.New("invalid commit message")
|
|
}
|
|
|
|
repository, err := git.PlainOpen(r.Root)
|
|
if err != nil {
|
|
return errors.Wrap(err, "opening registry cache")
|
|
}
|
|
|
|
w, err := repository.Worktree()
|
|
if err != nil {
|
|
return errors.Wrapf(err, "reading %s", style.Symbol(r.Root))
|
|
}
|
|
|
|
index, err := r.writeEntry(b)
|
|
if err != nil {
|
|
return errors.Wrapf(err, "writing %s", style.Symbol(index))
|
|
}
|
|
|
|
relativeIndexFile, err := filepath.Rel(r.Root, index)
|
|
if err != nil {
|
|
return errors.Wrap(err, "resolving relative path")
|
|
}
|
|
|
|
if _, err := w.Add(relativeIndexFile); err != nil {
|
|
return errors.Wrapf(err, "adding %s", style.Symbol(index))
|
|
}
|
|
|
|
if _, err := w.Commit(msg, &git.CommitOptions{
|
|
Author: &object.Signature{
|
|
Name: username,
|
|
Email: "",
|
|
When: time.Now(),
|
|
},
|
|
}); err != nil {
|
|
return errors.Wrapf(err, "committing")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (r *Cache) writeEntry(b Buildpack) (string, error) {
|
|
var ns = b.Namespace
|
|
var name = b.Name
|
|
|
|
index, err := IndexPath(r.Root, ns, name)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
if _, err := os.Stat(index); os.IsNotExist(err) {
|
|
if err := os.MkdirAll(filepath.Dir(index), 0750); err != nil {
|
|
return "", errors.Wrapf(err, "creating directory structure for: %s/%s", ns, name)
|
|
}
|
|
} else {
|
|
if _, err := os.Stat(index); err == nil {
|
|
entry, err := r.readEntry(ns, name)
|
|
if err != nil {
|
|
return "", errors.Wrapf(err, "reading existing buildpack entries")
|
|
}
|
|
|
|
availableBuildpacks := entry.Buildpacks
|
|
|
|
if len(availableBuildpacks) != 0 {
|
|
if availableBuildpacks[len(availableBuildpacks)-1].Version == b.Version {
|
|
return "", errors.Wrapf(err, "same version exists, upgrade the version to add")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
f, err := os.OpenFile(filepath.Clean(index), os.O_APPEND|os.O_CREATE|os.O_RDWR, 0644)
|
|
if err != nil {
|
|
return "", errors.Wrapf(err, "creating buildpack file: %s/%s", ns, name)
|
|
}
|
|
defer f.Close()
|
|
|
|
newline := "\n"
|
|
if runtime.GOOS == "windows" {
|
|
newline = "\r\n"
|
|
}
|
|
|
|
fileContents, err := json.Marshal(b)
|
|
if err != nil {
|
|
return "", errors.Wrapf(err, "converting buildpack file to json: %s/%s", ns, name)
|
|
}
|
|
|
|
fileContentsFormatted := string(fileContents) + newline
|
|
if _, err := f.WriteString(fileContentsFormatted); err != nil {
|
|
return "", errors.Wrapf(err, "writing buildpack to file: %s/%s", ns, name)
|
|
}
|
|
|
|
return index, nil
|
|
}
|
|
|
|
func (r *Cache) readEntry(ns, name string) (Entry, error) {
|
|
index, err := IndexPath(r.Root, ns, name)
|
|
if err != nil {
|
|
return Entry{}, err
|
|
}
|
|
|
|
if _, err := os.Stat(index); err != nil {
|
|
return Entry{}, errors.Wrapf(err, "finding buildpack: %s/%s", ns, name)
|
|
}
|
|
|
|
file, err := os.Open(filepath.Clean(index))
|
|
if err != nil {
|
|
return Entry{}, errors.Wrapf(err, "opening index for buildpack: %s/%s", ns, name)
|
|
}
|
|
defer file.Close()
|
|
|
|
entry := Entry{}
|
|
scanner := bufio.NewScanner(file)
|
|
for scanner.Scan() {
|
|
var bp Buildpack
|
|
err = json.Unmarshal([]byte(scanner.Text()), &bp)
|
|
if err != nil {
|
|
return Entry{}, errors.Wrapf(err, "parsing index for buildpack: %s/%s", ns, name)
|
|
}
|
|
|
|
entry.Buildpacks = append(entry.Buildpacks, bp)
|
|
}
|
|
|
|
if err := scanner.Err(); err != nil {
|
|
return entry, errors.Wrapf(err, "reading index for buildpack: %s/%s", ns, name)
|
|
}
|
|
|
|
return entry, nil
|
|
}
|