blob: 6a4ae0ce6460fab8527515d2977258b1d9df49d0 [file] [log] [blame]
package main
import (
"context"
"net/http"
"os"
"strings"
"time"
"go.skia.org/infra/go/metrics2"
"go.skia.org/infra/go/packages"
"go.skia.org/infra/go/sklog"
"go.skia.org/infra/go/util"
storage "google.golang.org/api/storage/v1"
)
var (
store *storage.Service
allPackages map[string]*packages.Package
failedInstallCounter = metrics2.GetCounter("pulld_failed_install", nil)
)
// differences returns all strings that appear in server but not local.
func differences(server, local []string) ([]string, []string) {
newPackages := []string{}
installedPackages := []string{}
for _, s := range server {
if util.In(s, local) {
installedPackages = append(installedPackages, s)
} else {
newPackages = append(newPackages, s)
}
}
return newPackages, installedPackages
}
func step(ctx context.Context, client *http.Client, store *storage.Service, hostname string) {
sklog.Info("About to read package list.")
// Read the old and new packages from their respective storage locations.
serverList, err := packages.InstalledForServer(client, store, hostname)
if err != nil {
sklog.Errorf("Failed to retrieve remote package list: %s", err)
return
}
localList, err := packages.FromLocalFile(*installedPackagesFile)
if err != nil {
sklog.Errorf("Failed to retrieve local package list: %s", err)
return
}
// Install any new or updated packages.
newPackages, installed := differences(serverList.Names, localList)
sklog.Infof("New: %v, Installed: %v", newPackages, installed)
for _, name := range newPackages {
// If just an appname appears w/o a package name then that means
// that package hasn't been selected, so just skip it for now.
if len(strings.Split(name, "/")) == 1 {
continue
}
installed = append(installed, name)
if err := packages.ToLocalFile(installed, *installedPackagesFile); err != nil {
sklog.Errorf("Failed to write local package list: %s", err)
continue
}
if err := packages.Install(ctx, client, store, name); err != nil {
failedInstallCounter.Inc(1)
sklog.Errorf("Failed to install package %s: %s", name, err)
// Pop last name from 'installed' then rewrite the file since the
// install failed.
installed = installed[:len(installed)-1]
if err := packages.ToLocalFile(installed, *installedPackagesFile); err != nil {
sklog.Errorf("Failed to rewrite local package list after install failure for %s: %s", name, err)
}
continue
}
// The pull application is special in that it's not restarted by the
// the postinstall script of the debian package, because that might kill
// pullg while it was updating itself. Instead pulld will just exit when
// it notices that it has been updated and count on systemd to restart it.
if containsPulld(newPackages) {
sklog.Info("The pulld package has been updated, exiting to allow a restart.")
sklog.Flush()
os.Exit(0)
}
}
if len(newPackages) > 0 {
allPackages, err = packages.AllAvailableByPackageName(store)
if err != nil {
sklog.Errorf("Failed to update the list of all packages: %s", err)
}
}
}
// containsPull returns true if the list of installed packages contains the 'pull' package.
func containsPulld(packages []string) bool {
for _, s := range packages {
if p := strings.Split(s, "/")[0]; p == "pulld" || p == "pulld-not-gce" {
return true
}
}
return false
}
// metadataWait waits for the instance level metadata 'pushrev' to change, at
// which point the server should check for new versions of apps to install.
func metadataWait(triggerPullCh chan bool) {
for {
req, err := http.NewRequest("GET", "http://metadata.google.internal/computeMetadata/v1/instance/attributes/pushrev?wait_for_change=true", nil)
if err != nil {
sklog.Errorf("Failed to create request: %s", err)
continue
}
req.Header.Set("Metadata-Flavor", "Google")
// We use the default client which should never timeout.
resp, err := http.DefaultClient.Do(req)
if err != nil || resp.StatusCode != 200 {
if err == nil {
util.Close(resp.Body)
}
sklog.Errorf("wait_for_change failed: %s", err)
if resp != nil {
sklog.Errorf("Response: %+v", *resp)
}
time.Sleep(time.Minute)
continue
}
util.Close(resp.Body)
triggerPullCh <- true
sklog.Infof("Pull triggered via metadata.")
}
}
func pullInit(ctx context.Context, client *http.Client, triggerPullCh chan bool) {
hostname, err := os.Hostname()
if err != nil {
sklog.Fatal(err)
}
sklog.Infof("Running with hostname: %s", hostname)
store, err = storage.New(client)
if err != nil {
sklog.Fatalf("Failed to create storage service client: %s", err)
}
allPackages, err = packages.AllAvailableByPackageName(store)
if err != nil {
sklog.Fatalf("Failed to retrieve a list of all packages: %s", err)
}
if *onGCE {
go metadataWait(triggerPullCh)
}
step(ctx, client, store, hostname)
timeCh := time.Tick(*pullPeriod)
go func() {
for {
select {
case <-timeCh:
case <-triggerPullCh:
}
step(ctx, client, store, hostname)
}
}()
}