| 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) |
| } |
| }() |
| } |