blob: b68de0afa376526e12c29de3569c451ab0baa0c2 [file] [log] [blame]
// packages is utilities for working with Debian packages and package lists.
package packages
import (
"bufio"
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/http"
"os"
"os/exec"
"sort"
"strings"
"sync"
"time"
"github.com/flynn/json5"
iexec "go.skia.org/infra/go/exec"
"go.skia.org/infra/go/gcs"
"go.skia.org/infra/go/sklog"
"go.skia.org/infra/go/util"
"google.golang.org/api/storage/v1"
)
var (
bucketName = "skia-push"
)
func SetBucketName(s string) {
bucketName = s
}
// Package represents a single Debian package uploaded to Google Storage.
type Package struct {
Name string // The unique name of this release package.
Hash string
UserID string
Built time.Time
Dirty bool
Note string
Services []string
}
func (p *Package) String() string {
return fmt.Sprintf(`Package of %q@%s Note: %q built on %s by %s`, p.Services, p.Hash[0:6], p.Note, p.Built, p.UserID)
}
// Installed is a list of all the packages installed on a server.
//
type Installed struct {
// Names is a list of package names, of the form "{appname}/{appname}:{author}:{date}:{githash}.deb"
Names []string
// Generation is the Google Storage generation number of the config file at the time we read it.
// Use this to avoid the lost-update problem: https://cloud.google.com/storage/docs/generations-preconditions#_ReadModWrite
Generation int64
}
// AllInfo keeps a cache of all installed packages and all available to be installed packages.
type AllInfo struct {
mutex sync.Mutex // protects allInstalled and allAvailable
allInstalled map[string]*Installed
allAvailable map[string][]*Package
client *http.Client
store *storage.Service
serverNames []string
}
func NewAllInfo(client *http.Client, store *storage.Service, serverNames []string) (*AllInfo, error) {
a := &AllInfo{
client: client,
store: store,
serverNames: serverNames,
}
if err := a.step(); err != nil {
return nil, fmt.Errorf("Failed to create packages.AllInfo: %s", err)
}
go func() {
for range time.Tick(1 * time.Minute) {
if err := a.step(); err != nil {
sklog.Errorf("Failed to update AllInfo: %s", err)
}
}
}()
return a, nil
}
func (a *AllInfo) ForceRefresh() error {
return a.step()
}
func (a *AllInfo) step() error {
allInstalled, err := allInstalled(a.client, a.store, a.serverNames)
if err != nil {
return err
}
allAvailable, err := AllAvailable(a.store)
if err != nil {
return err
}
a.mutex.Lock()
defer a.mutex.Unlock()
a.allInstalled = allInstalled
a.allAvailable = allAvailable
return nil
}
func (a *AllInfo) AllAvailable() map[string][]*Package {
a.mutex.Lock()
defer a.mutex.Unlock()
return a.allAvailable
}
// AllAvailableByPackageName returns all known packages for all applications
// uploaded to gs://<bucketName>/debs/. They are mapped by the package name.
func (a *AllInfo) AllAvailableByPackageName() map[string]*Package {
allAvailable := a.AllAvailable()
ret := map[string]*Package{}
for _, ps := range allAvailable {
for _, p := range ps {
ret[p.Name] = p
}
}
return ret
}
// PutInstalled writes a new list of installed packages for the given server.
func (a *AllInfo) PutInstalled(serverName string, packages []string, generation int64) error {
err := PutInstalled(a.store, serverName, packages, generation)
if err != nil {
return err
}
return a.step()
}
// PutInstalled writes a new list of installed packages for the given server.
func PutInstalled(store *storage.Service, serverName string, packages []string, generation int64) error {
b, err := json.Marshal(packages)
if err != nil {
return fmt.Errorf("Failed to encode installed packages: %s", err)
}
buf := bytes.NewBuffer(b)
req := store.Objects.Insert(bucketName, &storage.Object{Name: "server/" + serverName + ".json"}).Media(buf)
if generation != -1 {
req = req.IfGenerationMatch(generation)
}
if _, err = req.Do(); err != nil {
return fmt.Errorf("Failed to write installed packages list to Google Storage for %s: %s", serverName, err)
}
return nil
}
func (a *AllInfo) AllInstalled() map[string]*Installed {
a.mutex.Lock()
defer a.mutex.Unlock()
return a.allInstalled
}
// allInstalled returns a map of all known server names to their list of installed package names.
func allInstalled(client *http.Client, store *storage.Service, names []string) (map[string]*Installed, error) {
ret := map[string]*Installed{}
for _, name := range names {
p, err := InstalledForServer(client, store, name)
if err != nil {
sklog.Errorf("Failed to retrieve remote package list: %s", err)
}
ret[name] = p
}
return ret, nil
}
func safeGetTime(m map[string]string, key string) time.Time {
value := safeGet(m, key, "")
if value == "" {
return time.Time{}
}
ret, err := time.Parse("2006-01-02T15:04:05Z", value)
if err != nil {
sklog.Errorf("Failed to parse metadata datatime %s: %s", value, err)
}
return ret
}
func safeGetBool(m map[string]string, key string) bool {
if safeGet(m, key, "false") == "true" {
return true
}
return false
}
func safeGet(m map[string]string, key string, def string) string {
if value, ok := m[key]; ok {
return value
} else {
return def
}
}
func safeGetStringSlice(m map[string]string, key string) []string {
if value, ok := m[key]; !ok {
return []string{}
} else {
value = strings.TrimSpace(value)
if value == "" {
return []string{}
} else {
return strings.Split(value, " ")
}
}
}
// PackageSlice is for sorting Packages by Built time.
type PackageSlice []*Package
func (p PackageSlice) Len() int { return len(p) }
func (p PackageSlice) Less(i, j int) bool { return p[i].Built.After(p[j].Built) }
func (p PackageSlice) Swap(i, j int) { p[i], p[j] = p[j], p[i] }
func (p PackageSlice) String() string {
if len(p) == 0 {
return "Package Slice:\n<empty>\n"
}
strs := make([]string, 0, len(p)+1)
strs = append(strs, "Package Slice:")
for _, v := range p {
strs = append(strs, v.String())
}
return strings.Join(strs, "\n")
}
// AllAvailable returns all known packages for all applications uploaded to
// gs://<bucketName>/debs/.
func AllAvailable(store *storage.Service) (map[string][]*Package, error) {
req := store.Objects.List(bucketName).Prefix("debs")
ret := map[string][]*Package{}
for {
objs, err := req.Do()
if err != nil {
return nil, fmt.Errorf("Failed to list debian packages in Google Storage: %s", err)
}
for _, o := range objs.Items {
key := safeGet(o.Metadata, "appname", "")
if key == "" {
sklog.Errorf("Debian package without proper metadata: %s", o.Name)
continue
}
p := &Package{
Name: o.Name[5:], // Strip of debs/ from the beginning.
Hash: safeGet(o.Metadata, "hash", ""),
UserID: safeGet(o.Metadata, "userid", ""),
Built: safeGetTime(o.Metadata, "datetime"),
Dirty: safeGetBool(o.Metadata, "dirty"),
Note: safeGet(o.Metadata, "note", ""),
Services: safeGetStringSlice(o.Metadata, "services"),
}
if _, ok := ret[key]; !ok {
ret[key] = []*Package{}
}
ret[key] = append(ret[key], p)
}
if objs.NextPageToken == "" {
break
}
req.PageToken(objs.NextPageToken)
}
for _, value := range ret {
sort.Sort(PackageSlice(value))
}
return ret, nil
}
// AllAvailableApp returns all known packages for the given applications uploaded to
// gs://<bucketName>/debs/<appname>.
func AllAvailableApp(store *storage.Service, appName string) ([]*Package, error) {
prefix := fmt.Sprintf("debs/%s/", appName)
req := store.Objects.List(bucketName).Prefix(prefix)
ret := []*Package{}
for {
objs, err := req.Do()
if err != nil {
return nil, fmt.Errorf("Failed to list debian packages in Google Storage: %s", err)
}
for _, o := range objs.Items {
key := safeGet(o.Metadata, "appname", "")
if key == "" {
sklog.Errorf("Debian package without proper metadata: %s", o.Name)
continue
}
p := &Package{
Name: o.Name[len(prefix):], // Strip of debs/ from the beginning.
Hash: safeGet(o.Metadata, "hash", ""),
UserID: safeGet(o.Metadata, "userid", ""),
Built: safeGetTime(o.Metadata, "datetime"),
Dirty: safeGetBool(o.Metadata, "dirty"),
Note: safeGet(o.Metadata, "note", ""),
Services: safeGetStringSlice(o.Metadata, "services"),
}
ret = append(ret, p)
}
if objs.NextPageToken == "" {
break
}
req.PageToken(objs.NextPageToken)
}
sort.Sort(PackageSlice(ret))
return ret, nil
}
// AllAvailableByPackageName returns all known packages for all applications
// uploaded to gs://<bucketName>/debs/. They are mapped by the package name.
func AllAvailableByPackageName(store *storage.Service) (map[string]*Package, error) {
allAvailable, err := AllAvailable(store)
if err != nil {
return nil, fmt.Errorf("Failed to retrieve all available packages: %s", err)
}
ret := map[string]*Package{}
for _, ps := range allAvailable {
for _, p := range ps {
ret[p.Name] = p
}
}
return ret, nil
}
// InstalledForServer returns a list of package names of installed packages for
// the given server.
func InstalledForServer(client *http.Client, store *storage.Service, serverName string) (*Installed, error) {
ret := &Installed{
Names: []string{},
Generation: -1,
}
filename := "server/" + serverName + ".json"
obj, err := store.Objects.Get(bucketName, filename).Do()
if err != nil {
return ret, fmt.Errorf("Failed to retrieve Google Storage metadata about packages file %q: %s", filename, err)
}
sklog.Infof("Fetching: %s", obj.MediaLink)
req, err := gcs.RequestForStorageURL(obj.MediaLink)
if err != nil {
return ret, fmt.Errorf("Failed to construct request object for media: %s", err)
}
resp, err := client.Do(req)
if err != nil {
return ret, fmt.Errorf("Failed to retrieve packages file: %s", err)
}
defer util.Close(resp.Body)
if resp.StatusCode != 200 {
return ret, fmt.Errorf("Wrong status code: %#v", *resp)
}
dec := json.NewDecoder(resp.Body)
value := []string{}
if err := dec.Decode(&value); err != nil {
return ret, fmt.Errorf("Failed to decode packages file: %s", err)
}
sort.Strings(value)
ret.Names = value
ret.Generation = obj.Generation
return ret, nil
}
// FromLocalFile loads a list of installed debian package names from a local file.
func FromLocalFile(filename string) ([]string, error) {
if _, err := os.Stat(filename); os.IsNotExist(err) {
return nil, nil
}
f, err := os.Open(filename)
if err != nil {
return nil, fmt.Errorf("Failed to open file %s: %s", filename, err)
}
defer util.Close(f)
dec := json.NewDecoder(f)
value := []string{}
if err := dec.Decode(&value); err != nil {
if err := os.Remove(filename); err != nil {
sklog.Warningf("Error when trying to remove malformed JSON packages file: %s", err)
}
return nil, fmt.Errorf("Failed to decode packages file: %s", err)
}
return value, nil
}
// ToLocalFile writes a list of debian packages to a local file.
func ToLocalFile(packages []string, filename string) error {
f, err := os.Create(filename)
if err != nil {
return fmt.Errorf("Failed to create file %s: %s", filename, err)
}
defer util.Close(f)
enc := json.NewEncoder(f)
if err := enc.Encode(packages); err != nil {
return fmt.Errorf("Failed to write %s: %s", filename, err)
}
return nil
}
// Install downloads and installs a debian package from Google Storage.
func Install(ctx context.Context, client *http.Client, store *storage.Service, name string) error {
sklog.Infof("Installing: %s", name)
obj, err := store.Objects.Get(bucketName, "debs/"+name).Do()
if err != nil {
return fmt.Errorf("Failed to retrieve Google Storage metadata about debian package: %s", err)
}
req, err := gcs.RequestForStorageURL(obj.MediaLink)
if err != nil {
return fmt.Errorf("Failed to construct request object for media: %s", err)
}
resp, err := client.Do(req)
if err != nil {
return fmt.Errorf("Failed to retrieve packages file: %s", err)
}
defer util.Close(resp.Body)
f, err := ioutil.TempFile("", "skia-pull")
if err != nil {
return fmt.Errorf("Failed to create tmp file: %s", err)
}
_, copyErr := io.Copy(f, resp.Body)
if err := f.Close(); err != nil {
return fmt.Errorf("Failed to close temporary file: %v", err)
}
if copyErr != nil {
return fmt.Errorf("Failed to download file: %s", copyErr)
}
if err := installDependencies(ctx, f.Name()); err != nil {
return fmt.Errorf("Error installing dependencies: %s", err)
}
cmd := exec.Command("sudo", "dpkg", "-i", f.Name())
var out bytes.Buffer
cmd.Stdout = &out
if err := cmd.Run(); err != nil {
sklog.Errorf("Install package stdout: %s", out.String())
return fmt.Errorf("Failed to install package: %s", err)
}
sklog.Infof("Install package stdout: %s", out.String())
return nil
}
// getDependencies returns the value of the 'Depends' field in the control
// file of the given package.
func getDependencies(ctx context.Context, packageName string) (string, error) {
const DEPENDS_PREFIX = "Depends:"
output, err := iexec.RunSimple(ctx, fmt.Sprintf("dpkg -I %s", packageName))
if err != nil {
return "", err
}
sklog.Infof("Got output for %s :\n\n%s", packageName, output)
buf := bytes.NewBuffer([]byte(output))
scanner := bufio.NewScanner(buf)
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if strings.HasPrefix(line, DEPENDS_PREFIX) {
return strings.Replace(strings.TrimSpace(strings.TrimPrefix(line, DEPENDS_PREFIX)), ",", " ", -1), nil
}
}
return "", nil
}
// installDependencies installs all dependencies that are named in the
// 'Depends' field of the control file via apt-get.
func installDependencies(ctx context.Context, packageFileName string) error {
dependencies, err := getDependencies(ctx, packageFileName)
if err != nil {
return fmt.Errorf("Error getting dependencies for %s: %s", packageFileName, err)
}
if dependencies != "" {
if output, err := iexec.RunSimple(ctx, "sudo apt-get update"); err != nil {
return fmt.Errorf("Unable to update package cache Got error %s\n\n and output: %s\n\n", err, output)
}
sklog.Infof("Installing via apt-get: %s", dependencies)
if output, err := iexec.RunSimple(ctx, fmt.Sprintf("sudo apt-get -y install %s", dependencies)); err != nil {
return fmt.Errorf("Unable to install dependencies for %s. Got error: \n %s \n\n and output:\n\n%s", packageFileName, err, output)
}
} else {
sklog.Infof("No deps found.")
}
return nil
}
// ServerConfig is used in PackageConfig.
type ServerConfig struct {
AppNames []string
}
// PackageConfig is the configuration of the application.
//
// It is a list of servers (by GCE domain name) and the list
// of apps that are allowed to be installed on them. It is
// loaded from infra/push/skiapush.json5 in JSON5 format.
type PackageConfig struct {
Servers map[string]ServerConfig
}
func New() PackageConfig {
return PackageConfig{
Servers: map[string]ServerConfig{},
}
}
func LoadPackageConfig(filename string) (PackageConfig, error) {
var config PackageConfig
f, err := os.Open(filename)
if err != nil {
return config, fmt.Errorf("Failed to open config file: %s", err)
}
defer util.Close(f)
dec := json5.NewDecoder(f)
if err := dec.Decode(&config); err != nil {
return config, fmt.Errorf("Failed to decode config file: %s", err)
}
return config, nil
}
func (p *PackageConfig) AllServerNames() []string {
serverNames := make([]string, 0, len(p.Servers))
for k := range p.Servers {
serverNames = append(serverNames, k)
}
return serverNames
}
func (p *PackageConfig) AllServerNamesWithPackage(name string) []string {
serverNames := make([]string, 0, len(p.Servers))
for k, config := range p.Servers {
for _, app := range config.AppNames {
if app == name {
serverNames = append(serverNames, k)
break
}
}
}
return serverNames
}