mirror of
https://github.com/aptly-dev/aptly.git
synced 2026-06-04 05:10:40 +00:00
211ac0501f
Allow database to be initialized without opening, unify all the open paths to retry on failure. In API router make sure open requests are matched with acks in explicit way. This also enables re-open attempts in all the aptly commands, so it should make running aptly CLI much easier now hopefully. Fix up system tests for oldoldstable ;)
509 lines
13 KiB
Go
509 lines
13 KiB
Go
// Package context provides single entry to all resources
|
|
package context
|
|
|
|
import (
|
|
"fmt"
|
|
"math/rand"
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
"runtime/pprof"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/smira/aptly/aptly"
|
|
"github.com/smira/aptly/console"
|
|
"github.com/smira/aptly/database"
|
|
"github.com/smira/aptly/deb"
|
|
"github.com/smira/aptly/files"
|
|
"github.com/smira/aptly/http"
|
|
"github.com/smira/aptly/s3"
|
|
"github.com/smira/aptly/swift"
|
|
"github.com/smira/aptly/utils"
|
|
"github.com/smira/commander"
|
|
"github.com/smira/flag"
|
|
)
|
|
|
|
// AptlyContext is a common context shared by all commands
|
|
type AptlyContext struct {
|
|
sync.Mutex
|
|
|
|
flags, globalFlags *flag.FlagSet
|
|
configLoaded bool
|
|
|
|
progress aptly.Progress
|
|
downloader aptly.Downloader
|
|
database database.Storage
|
|
packagePool aptly.PackagePool
|
|
publishedStorages map[string]aptly.PublishedStorage
|
|
collectionFactory *deb.CollectionFactory
|
|
dependencyOptions int
|
|
architecturesList []string
|
|
// Debug features
|
|
fileCPUProfile *os.File
|
|
fileMemProfile *os.File
|
|
fileMemStats *os.File
|
|
}
|
|
|
|
// Check interface
|
|
var _ aptly.PublishedStorageProvider = &AptlyContext{}
|
|
|
|
// FatalError is type for panicking to abort execution with non-zero
|
|
// exit code and print meaningful explanation
|
|
type FatalError struct {
|
|
ReturnCode int
|
|
Message string
|
|
}
|
|
|
|
// Fatal panics and aborts execution with exit code 1
|
|
func Fatal(err error) {
|
|
returnCode := 1
|
|
if err == commander.ErrFlagError || err == commander.ErrCommandError {
|
|
returnCode = 2
|
|
}
|
|
panic(&FatalError{ReturnCode: returnCode, Message: err.Error()})
|
|
}
|
|
|
|
// Config loads and returns current configuration
|
|
func (context *AptlyContext) Config() *utils.ConfigStructure {
|
|
context.Lock()
|
|
defer context.Unlock()
|
|
|
|
return context.config()
|
|
}
|
|
|
|
func (context *AptlyContext) config() *utils.ConfigStructure {
|
|
if !context.configLoaded {
|
|
var err error
|
|
|
|
configLocation := context.globalFlags.Lookup("config").Value.String()
|
|
if configLocation != "" {
|
|
err = utils.LoadConfig(configLocation, &utils.Config)
|
|
|
|
if err != nil {
|
|
Fatal(err)
|
|
}
|
|
} else {
|
|
configLocations := []string{
|
|
filepath.Join(os.Getenv("HOME"), ".aptly.conf"),
|
|
"/etc/aptly.conf",
|
|
}
|
|
|
|
for _, configLocation := range configLocations {
|
|
err = utils.LoadConfig(configLocation, &utils.Config)
|
|
if err == nil {
|
|
break
|
|
}
|
|
if !os.IsNotExist(err) {
|
|
Fatal(fmt.Errorf("error loading config file %s: %s", configLocation, err))
|
|
}
|
|
}
|
|
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "Config file not found, creating default config at %s\n\n", configLocations[0])
|
|
|
|
// as this is fresh aptly installation, we don't need to support legacy pool locations
|
|
utils.Config.SkipLegacyPool = true
|
|
utils.SaveConfig(configLocations[0], &utils.Config)
|
|
}
|
|
}
|
|
|
|
context.configLoaded = true
|
|
|
|
}
|
|
return &utils.Config
|
|
}
|
|
|
|
// LookupOption checks boolean flag with default (usually config) and command-line
|
|
// setting
|
|
func (context *AptlyContext) LookupOption(defaultValue bool, name string) (result bool) {
|
|
context.Lock()
|
|
defer context.Unlock()
|
|
|
|
return context.lookupOption(defaultValue, name)
|
|
}
|
|
|
|
func (context *AptlyContext) lookupOption(defaultValue bool, name string) (result bool) {
|
|
result = defaultValue
|
|
|
|
if context.globalFlags.IsSet(name) {
|
|
result = context.globalFlags.Lookup(name).Value.Get().(bool)
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
// DependencyOptions calculates options related to dependecy handling
|
|
func (context *AptlyContext) DependencyOptions() int {
|
|
context.Lock()
|
|
defer context.Unlock()
|
|
|
|
if context.dependencyOptions == -1 {
|
|
context.dependencyOptions = 0
|
|
if context.lookupOption(context.config().DepFollowSuggests, "dep-follow-suggests") {
|
|
context.dependencyOptions |= deb.DepFollowSuggests
|
|
}
|
|
if context.lookupOption(context.config().DepFollowRecommends, "dep-follow-recommends") {
|
|
context.dependencyOptions |= deb.DepFollowRecommends
|
|
}
|
|
if context.lookupOption(context.config().DepFollowAllVariants, "dep-follow-all-variants") {
|
|
context.dependencyOptions |= deb.DepFollowAllVariants
|
|
}
|
|
if context.lookupOption(context.config().DepFollowSource, "dep-follow-source") {
|
|
context.dependencyOptions |= deb.DepFollowSource
|
|
}
|
|
if context.lookupOption(context.config().DepVerboseResolve, "dep-verbose-resolve") {
|
|
context.dependencyOptions |= deb.DepVerboseResolve
|
|
}
|
|
}
|
|
|
|
return context.dependencyOptions
|
|
}
|
|
|
|
// ArchitecturesList returns list of architectures fixed via command line or config
|
|
func (context *AptlyContext) ArchitecturesList() []string {
|
|
context.Lock()
|
|
defer context.Unlock()
|
|
|
|
if context.architecturesList == nil {
|
|
context.architecturesList = context.config().Architectures
|
|
optionArchitectures := context.globalFlags.Lookup("architectures").Value.String()
|
|
if optionArchitectures != "" {
|
|
context.architecturesList = strings.Split(optionArchitectures, ",")
|
|
}
|
|
}
|
|
|
|
return context.architecturesList
|
|
}
|
|
|
|
// Progress creates or returns Progress object
|
|
func (context *AptlyContext) Progress() aptly.Progress {
|
|
context.Lock()
|
|
defer context.Unlock()
|
|
|
|
return context._progress()
|
|
}
|
|
|
|
func (context *AptlyContext) _progress() aptly.Progress {
|
|
if context.progress == nil {
|
|
context.progress = console.NewProgress()
|
|
context.progress.Start()
|
|
}
|
|
|
|
return context.progress
|
|
}
|
|
|
|
// Downloader returns instance of current downloader
|
|
func (context *AptlyContext) Downloader() aptly.Downloader {
|
|
context.Lock()
|
|
defer context.Unlock()
|
|
|
|
if context.downloader == nil {
|
|
var downloadLimit int64
|
|
limitFlag := context.flags.Lookup("download-limit")
|
|
if limitFlag != nil {
|
|
downloadLimit = limitFlag.Value.Get().(int64)
|
|
}
|
|
if downloadLimit == 0 {
|
|
downloadLimit = context.config().DownloadLimit
|
|
}
|
|
context.downloader = http.NewDownloader(downloadLimit*1024, context._progress())
|
|
}
|
|
|
|
return context.downloader
|
|
}
|
|
|
|
// DBPath builds path to database
|
|
func (context *AptlyContext) DBPath() string {
|
|
context.Lock()
|
|
defer context.Unlock()
|
|
|
|
return context.dbPath()
|
|
}
|
|
|
|
// DBPath builds path to database
|
|
func (context *AptlyContext) dbPath() string {
|
|
return filepath.Join(context.config().RootDir, "db")
|
|
}
|
|
|
|
// Database opens and returns current instance of database
|
|
func (context *AptlyContext) Database() (database.Storage, error) {
|
|
context.Lock()
|
|
defer context.Unlock()
|
|
|
|
return context._database()
|
|
}
|
|
|
|
func (context *AptlyContext) _database() (database.Storage, error) {
|
|
if context.database == nil {
|
|
var err error
|
|
|
|
context.database, err = database.NewDB(context.dbPath())
|
|
if err != nil {
|
|
return nil, fmt.Errorf("can't instanciate database: %s", err)
|
|
}
|
|
}
|
|
|
|
tries := context.flags.Lookup("db-open-attempts").Value.Get().(int)
|
|
const BaseDelay = 10 * time.Second
|
|
const Jitter = 1 * time.Second
|
|
|
|
for ; tries >= 0; tries-- {
|
|
err := context.database.Open()
|
|
if err == nil || !strings.Contains(err.Error(), "resource temporarily unavailable") {
|
|
return context.database, err
|
|
}
|
|
|
|
if tries > 0 {
|
|
delay := time.Duration(rand.NormFloat64()*float64(Jitter) + float64(BaseDelay))
|
|
if delay < 0 {
|
|
delay = time.Second
|
|
}
|
|
|
|
context._progress().Printf("Unable to open database, sleeping %s, attempts left %d...\n", delay, tries)
|
|
time.Sleep(delay)
|
|
}
|
|
}
|
|
|
|
return nil, fmt.Errorf("unable to reopen the DB, maximum number of retries reached")
|
|
}
|
|
|
|
// CloseDatabase closes the db temporarily
|
|
func (context *AptlyContext) CloseDatabase() error {
|
|
context.Lock()
|
|
defer context.Unlock()
|
|
|
|
if context.database == nil {
|
|
return nil
|
|
}
|
|
|
|
return context.database.Close()
|
|
}
|
|
|
|
// ReOpenDatabase reopens the db after close
|
|
func (context *AptlyContext) ReOpenDatabase() error {
|
|
_, err := context.Database()
|
|
|
|
return err
|
|
}
|
|
|
|
// CollectionFactory builds factory producing all kinds of collections
|
|
func (context *AptlyContext) CollectionFactory() *deb.CollectionFactory {
|
|
context.Lock()
|
|
defer context.Unlock()
|
|
|
|
if context.collectionFactory == nil {
|
|
db, err := context._database()
|
|
if err != nil {
|
|
Fatal(err)
|
|
}
|
|
context.collectionFactory = deb.NewCollectionFactory(db)
|
|
}
|
|
|
|
return context.collectionFactory
|
|
}
|
|
|
|
// PackagePool returns instance of PackagePool
|
|
func (context *AptlyContext) PackagePool() aptly.PackagePool {
|
|
context.Lock()
|
|
defer context.Unlock()
|
|
|
|
if context.packagePool == nil {
|
|
context.packagePool = files.NewPackagePool(context.config().RootDir, !context.config().SkipLegacyPool)
|
|
}
|
|
|
|
return context.packagePool
|
|
}
|
|
|
|
// GetPublishedStorage returns instance of PublishedStorage
|
|
func (context *AptlyContext) GetPublishedStorage(name string) aptly.PublishedStorage {
|
|
context.Lock()
|
|
defer context.Unlock()
|
|
|
|
publishedStorage, ok := context.publishedStorages[name]
|
|
if !ok {
|
|
if name == "" {
|
|
publishedStorage = files.NewPublishedStorage(filepath.Join(context.config().RootDir, "public"), "hardlink", "")
|
|
} else if strings.HasPrefix(name, "filesystem:") {
|
|
params, ok := context.config().FileSystemPublishRoots[name[11:]]
|
|
if !ok {
|
|
Fatal(fmt.Errorf("published local storage %v not configured", name[6:]))
|
|
}
|
|
|
|
publishedStorage = files.NewPublishedStorage(params.RootDir, params.LinkMethod, params.VerifyMethod)
|
|
} else if strings.HasPrefix(name, "s3:") {
|
|
params, ok := context.config().S3PublishRoots[name[3:]]
|
|
if !ok {
|
|
Fatal(fmt.Errorf("published S3 storage %v not configured", name[3:]))
|
|
}
|
|
|
|
var err error
|
|
publishedStorage, err = s3.NewPublishedStorage(
|
|
params.AccessKeyID, params.SecretAccessKey, params.SessionToken,
|
|
params.Region, params.Endpoint, params.Bucket, params.ACL, params.Prefix, params.StorageClass,
|
|
params.EncryptionMethod, params.PlusWorkaround, params.DisableMultiDel,
|
|
params.ForceSigV2, params.Debug)
|
|
if err != nil {
|
|
Fatal(err)
|
|
}
|
|
} else if strings.HasPrefix(name, "swift:") {
|
|
params, ok := context.config().SwiftPublishRoots[name[6:]]
|
|
if !ok {
|
|
Fatal(fmt.Errorf("published Swift storage %v not configured", name[6:]))
|
|
}
|
|
|
|
var err error
|
|
publishedStorage, err = swift.NewPublishedStorage(params.UserName, params.Password,
|
|
params.AuthURL, params.Tenant, params.TenantID, params.Domain, params.DomainID, params.TenantDomain, params.TenantDomainID, params.Container, params.Prefix)
|
|
if err != nil {
|
|
Fatal(err)
|
|
}
|
|
} else {
|
|
Fatal(fmt.Errorf("unknown published storage format: %v", name))
|
|
}
|
|
context.publishedStorages[name] = publishedStorage
|
|
}
|
|
|
|
return publishedStorage
|
|
}
|
|
|
|
// UploadPath builds path to upload storage
|
|
func (context *AptlyContext) UploadPath() string {
|
|
return filepath.Join(context.Config().RootDir, "upload")
|
|
}
|
|
|
|
// UpdateFlags sets internal copy of flags in the context
|
|
func (context *AptlyContext) UpdateFlags(flags *flag.FlagSet) {
|
|
context.Lock()
|
|
defer context.Unlock()
|
|
|
|
context.flags = flags
|
|
}
|
|
|
|
// Flags returns current command flags
|
|
func (context *AptlyContext) Flags() *flag.FlagSet {
|
|
context.Lock()
|
|
defer context.Unlock()
|
|
|
|
return context.flags
|
|
}
|
|
|
|
// GlobalFlags returns flags passed to all commands
|
|
func (context *AptlyContext) GlobalFlags() *flag.FlagSet {
|
|
context.Lock()
|
|
defer context.Unlock()
|
|
|
|
return context.globalFlags
|
|
}
|
|
|
|
// Shutdown shuts context down
|
|
func (context *AptlyContext) Shutdown() {
|
|
context.Lock()
|
|
defer context.Unlock()
|
|
|
|
if aptly.EnableDebug {
|
|
if context.fileMemProfile != nil {
|
|
pprof.WriteHeapProfile(context.fileMemProfile)
|
|
context.fileMemProfile.Close()
|
|
context.fileMemProfile = nil
|
|
}
|
|
if context.fileCPUProfile != nil {
|
|
pprof.StopCPUProfile()
|
|
context.fileCPUProfile.Close()
|
|
context.fileCPUProfile = nil
|
|
}
|
|
if context.fileMemProfile != nil {
|
|
context.fileMemProfile.Close()
|
|
context.fileMemProfile = nil
|
|
}
|
|
}
|
|
if context.database != nil {
|
|
context.database.Close()
|
|
context.database = nil
|
|
}
|
|
if context.downloader != nil {
|
|
context.downloader = nil
|
|
}
|
|
if context.progress != nil {
|
|
context.progress.Shutdown()
|
|
context.progress = nil
|
|
}
|
|
}
|
|
|
|
// Cleanup does partial shutdown of context
|
|
func (context *AptlyContext) Cleanup() {
|
|
context.Lock()
|
|
defer context.Unlock()
|
|
|
|
if context.downloader != nil {
|
|
context.downloader = nil
|
|
}
|
|
if context.progress != nil {
|
|
context.progress.Shutdown()
|
|
context.progress = nil
|
|
}
|
|
}
|
|
|
|
// NewContext initializes context with default settings
|
|
func NewContext(flags *flag.FlagSet) (*AptlyContext, error) {
|
|
var err error
|
|
|
|
context := &AptlyContext{
|
|
flags: flags,
|
|
globalFlags: flags,
|
|
dependencyOptions: -1,
|
|
publishedStorages: map[string]aptly.PublishedStorage{},
|
|
}
|
|
|
|
if aptly.EnableDebug {
|
|
cpuprofile := flags.Lookup("cpuprofile").Value.String()
|
|
if cpuprofile != "" {
|
|
context.fileCPUProfile, err = os.Create(cpuprofile)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
pprof.StartCPUProfile(context.fileCPUProfile)
|
|
}
|
|
|
|
memprofile := flags.Lookup("memprofile").Value.String()
|
|
if memprofile != "" {
|
|
context.fileMemProfile, err = os.Create(memprofile)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
memstats := flags.Lookup("memstats").Value.String()
|
|
if memstats != "" {
|
|
interval := flags.Lookup("meminterval").Value.Get().(time.Duration)
|
|
|
|
context.fileMemStats, err = os.Create(memstats)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
context.fileMemStats.WriteString("# Time\tHeapSys\tHeapAlloc\tHeapIdle\tHeapReleased\n")
|
|
|
|
go func() {
|
|
var stats runtime.MemStats
|
|
|
|
start := time.Now().UnixNano()
|
|
|
|
for {
|
|
runtime.ReadMemStats(&stats)
|
|
if context.fileMemStats != nil {
|
|
context.fileMemStats.WriteString(fmt.Sprintf("%d\t%d\t%d\t%d\t%d\n",
|
|
(time.Now().UnixNano()-start)/1000000, stats.HeapSys, stats.HeapAlloc, stats.HeapIdle, stats.HeapReleased))
|
|
time.Sleep(interval)
|
|
} else {
|
|
break
|
|
}
|
|
}
|
|
}()
|
|
}
|
|
}
|
|
|
|
return context, nil
|
|
}
|