blob: cdfdfb0bfed778454ac4cd3356b2f399cd0c915c [file] [log] [blame]
// mirrors package brings up a verdaccio mirror for each supported project.
package mirrors
import (
"context"
"fmt"
"html/template"
"os"
"path"
"path/filepath"
"strings"
"sync"
"go.skia.org/infra/go/executil"
"go.skia.org/infra/go/skerr"
"go.skia.org/infra/go/sklog"
"go.skia.org/infra/npm-audit-mirror/go/types"
)
const (
verdaccioDirName = "verdaccio"
verdaccioStorageDirName = "storage"
verdaccioLogFileName = "verdaccio.log"
)
// VerdaccioMirror implements types.ProjectMirror.
type VerdaccioMirror struct {
workDir string
verdaccioDir string
verdaccioConfigPath string
verdaccioStorageDir string
projectName string
publicURL string
// Maintains an in-memory map of download package tarballs.
// This map is used to determine which packages are not
// available on the mirror yet and will require an external
// network call to download.
downloadedPackageTarballs map[string]interface{}
// Mutex that contains access to the above map.
downloadedPackageTarballsMtx sync.RWMutex
}
// NewVerdaccioMirror returns an instance of VerdaccioMirror.
func NewVerdaccioMirror(projectName, workDir, host string, cfgTmpl *template.Template) (types.ProjectMirror, error) {
// Create all necessary directories and files.
verdaccioDir := path.Join(workDir, verdaccioDirName)
if err := os.MkdirAll(verdaccioDir, 0755); err != nil {
return nil, skerr.Wrapf(err, "Could not create %s", verdaccioDir)
}
// Create the verdaccio config file.
verdaccioConfigPath, err := createCfgFile(projectName, workDir, verdaccioDir, cfgTmpl)
if err != nil {
return nil, skerr.Wrap(err)
}
// Verdaccio storage dir. This will be created automatically by verdaccio.
verdaccioStorageDir := path.Join(verdaccioDir, verdaccioStorageDirName)
// Populate the in-memory map of downloaded package tarballs.
downloadedPackageTarballs, err := GetTarballsInMirrorStorage(verdaccioStorageDir)
if err != nil {
return nil, skerr.Wrap(err)
}
// Find this mirror's publicURL.
publicURL := fmt.Sprintf("%s/%s/", host, projectName)
return &VerdaccioMirror{
workDir: workDir,
verdaccioDir: verdaccioDir,
verdaccioConfigPath: verdaccioConfigPath,
verdaccioStorageDir: verdaccioStorageDir,
projectName: projectName,
downloadedPackageTarballs: downloadedPackageTarballs,
publicURL: publicURL,
}, nil
}
// createCfgFile creates the verdaccio config file using the provided template. Returns
// the path to the config file.
func createCfgFile(projectName, workDir, verdaccioDir string, cfgTmpl *template.Template) (string, error) {
verdaccioConfigFilePath := path.Join(verdaccioDir, "config.yaml")
f, err := os.Create(verdaccioConfigFilePath)
if err != nil {
return "", skerr.Wrapf(err, "Could not create %s", verdaccioConfigFilePath)
}
defer f.Close()
if err := cfgTmpl.Execute(f, map[string]string{
"Path": workDir,
"ProjectName": projectName,
}); err != nil {
return "", skerr.Wrapf(err, "Could not execute template for %s", projectName)
}
return verdaccioConfigFilePath, nil
}
// GetProjectName implements the types.ProjectMirror interface.
func (m *VerdaccioMirror) GetProjectName() string {
return m.projectName
}
// StartMirror implements the types.ProjectMirror interface.
func (m *VerdaccioMirror) StartMirror(ctx context.Context, port int) error {
go func() {
// Create the verdaccio log before starting verdaccio.
verdaccioLogPath := path.Join(m.verdaccioDir, verdaccioLogFileName)
verdaccioLogFile, err := os.Create(verdaccioLogPath)
if err != nil {
sklog.Fatalf("Could not create %s: %s", verdaccioLogFile, err)
}
defer verdaccioLogFile.Close()
// Start verdaccio.
m.startVerdaccioMirror(ctx, port, verdaccioLogFile)
}()
return nil
}
// startVerdaccioMirror brings up a running verdaccio mirror locally.
func (m *VerdaccioMirror) startVerdaccioMirror(ctx context.Context, port int, logFile *os.File) {
verdaccioCmd := executil.CommandContext(ctx, "verdaccio", "--config="+m.verdaccioConfigPath, fmt.Sprintf("--listen=%d", port))
verdaccioCmd.Dir = m.workDir
verdaccioCmd.Stdout = logFile
verdaccioCmd.Env = os.Environ()
verdaccioCmd.Env = append(verdaccioCmd.Env,
fmt.Sprintf("VERDACCIO_PUBLIC_URL=%s", m.publicURL),
// Makes the logs more verbose and useful for debugging.
"NODE_DEBUG=request",
"DEBUG=express:*")
sklog.Info(verdaccioCmd.String())
if err := verdaccioCmd.Run(); err != nil {
sklog.Fatalf("Could not start verdaccio in %s: %s", m.workDir, err)
}
}
// AddToDownloadedPackageTarballs implements the types.ProjectMirror interface.
func (m *VerdaccioMirror) AddToDownloadedPackageTarballs(packageTarballName string) {
m.downloadedPackageTarballsMtx.Lock()
defer m.downloadedPackageTarballsMtx.Unlock()
m.downloadedPackageTarballs[packageTarballName] = true
}
// IsPackageTarballDownloaded implements the types.ProjectMirror interface.
func (m *VerdaccioMirror) IsPackageTarballDownloaded(packageTarballName string) bool {
m.downloadedPackageTarballsMtx.RLock()
defer m.downloadedPackageTarballsMtx.RUnlock()
_, downloaded := m.downloadedPackageTarballs[packageTarballName]
return downloaded
}
// GetDownloadedPackageNames implements the types.ProjectMirror interface.
func (m *VerdaccioMirror) GetDownloadedPackageNames() ([]string, error) {
downloadedPackageNames := []string{}
// Examine the local cache directory.
if _, err := os.Stat(m.verdaccioStorageDir); !os.IsNotExist(err) {
err = filepath.Walk(m.verdaccioStorageDir, func(path string, f os.FileInfo, err error) error {
if !f.IsDir() && filepath.Ext(path) == ".tgz" {
// Find the package name with the scope (if any). Do this by trimming the storage dir
// prefix and the tarball suffix.
// Eg: /tmp/skia-infra/verdaccio/storage/@typescript-eslint/types/types-4.22.0.tgz
// will return "/@typescript-eslint/types/".
packageName := strings.TrimSuffix(strings.TrimPrefix(path, m.verdaccioStorageDir), f.Name())
// Remove the surrounding "/"s.
packageName = strings.Trim(packageName, "/")
downloadedPackageNames = append(downloadedPackageNames, packageName)
}
return nil
})
if err != nil {
return nil, skerr.Wrapf(err, "Could not look for packages in %s", m.verdaccioStorageDir)
}
}
return downloadedPackageNames, nil
}
// GetTarballsInMirrorStorage returns a map of the packages (including their versions)
// that are available locally on the mirror. These are the packages that the mirror
// does not have to hit the public NPM registry for.
func GetTarballsInMirrorStorage(verdaccioStorageDir string) (map[string]interface{}, error) {
installedPackages := map[string]interface{}{}
if _, err := os.Stat(verdaccioStorageDir); !os.IsNotExist(err) {
err = filepath.Walk(verdaccioStorageDir, func(path string, f os.FileInfo, err error) error {
if !f.IsDir() && filepath.Ext(path) == ".tgz" {
installedPackages[f.Name()] = struct{}{}
}
return nil
})
if err != nil {
return nil, skerr.Wrapf(err, "Could not look for packages in %s", verdaccioStorageDir)
}
}
return installedPackages, nil
}