func compatCheck(client controller.Client, minVersion string) (bool, error) { status, err := client.Status() if err != nil { return false, err } v := version.Parse(status.Version) return v.Dev || !v.Before(version.Parse(minVersion)), nil }
func run() error { log := log15.New() if *isTTY { log.SetHandler(log15.StreamHandler(colorable.NewColorableStdout(), log15.TerminalFormat())) } var images map[string]*ct.Artifact if err := json.NewDecoder(os.Stdin).Decode(&images); err != nil { log.Error("error decoding images", "err", err) return err } req, err := http.NewRequest("GET", "http://status-web.discoverd", nil) if err != nil { return err } req.Header = make(http.Header) req.Header.Set("Accept", "application/json") res, err := http.DefaultClient.Do(req) if err != nil { log.Error("error getting cluster status", "err", err) return err } defer res.Body.Close() if res.StatusCode != 200 { log.Error("cluster status is unhealthy", "code", res.StatusCode) return fmt.Errorf("cluster is unhealthy") } var statusWrapper struct { Data struct { Detail map[string]status.Status } } if err := json.NewDecoder(res.Body).Decode(&statusWrapper); err != nil { log.Error("error decoding cluster status JSON", "err", err) return err } statuses := statusWrapper.Data.Detail instances, err := discoverd.GetInstances("controller", 10*time.Second) if err != nil { log.Error("error looking up controller in service discovery", "err", err) return err } client, err := controller.NewClient("", instances[0].Meta["AUTH_KEY"]) if err != nil { log.Error("error creating controller client", "err", err) return err } log.Info("validating images") for _, app := range updater.SystemApps { if v := version.Parse(statuses[app.Name].Version); !v.Dev && app.MinVersion != "" && v.Before(version.Parse(app.MinVersion)) { log.Info( "not updating image of system app, can't upgrade from running version", "app", app.Name, "version", v, ) continue } if _, ok := images[app.Name]; !ok { err := fmt.Errorf("missing image: %s", app.Name) log.Error(err.Error()) return err } } log.Info("creating new image artifacts") redisImage = images["redis"] if err := client.CreateArtifact(redisImage); err != nil { log.Error("error creating redis image artifact", "err", err) return err } slugRunner = images["slugrunner"] if err := client.CreateArtifact(slugRunner); err != nil { log.Error("error creating slugrunner image artifact", "err", err) return err } slugBuilder = images["slugbuilder"] if err := client.CreateArtifact(slugBuilder); err != nil { log.Error("error creating slugbuilder image artifact", "err", err) return err } // deploy system apps in order first for _, appInfo := range updater.SystemApps { if appInfo.ImageOnly { continue // skip ImageOnly updates } log := log.New("name", appInfo.Name) log.Info("starting deploy of system app") app, err := client.GetApp(appInfo.Name) if err == controller.ErrNotFound && appInfo.Optional { log.Info( "skipped deploy of system app", "reason", "optional app not present", "app", appInfo.Name, ) continue } else if err != nil { log.Error("error getting app", "err", err) return err } if err := deployApp(client, app, images[appInfo.Name], appInfo.UpdateRelease, log); err != nil { if e, ok := err.(errDeploySkipped); ok { log.Info( "skipped deploy of system app", "reason", e.reason, "app", appInfo.Name, ) continue } return err } log.Info("finished deploy of system app") } // deploy all other apps (including provisioned Redis apps) apps, err := client.AppList() if err != nil { log.Error("error getting apps", "err", err) return err } for _, app := range apps { log := log.New("name", app.Name) if app.RedisAppliance() { log.Info("starting deploy of Redis app") if err := deployApp(client, app, redisImage, nil, log); err != nil { if e, ok := err.(errDeploySkipped); ok { log.Info("skipped deploy of Redis app", "reason", e.reason) continue } return err } log.Info("finished deploy of Redis app") continue } if app.System() { continue } log.Info("starting deploy of app to update slugrunner") if err := deployApp(client, app, slugRunner, nil, log); err != nil { if e, ok := err.(errDeploySkipped); ok { log.Info("skipped deploy of app", "reason", e.reason) continue } return err } log.Info("finished deploy of app") } return nil }
func runUpdate(args *docopt.Args) error { log := log15.New() // create and update a TUF client log.Info("initializing TUF client") local, err := tuf.FileLocalStore(args.String["--tuf-db"]) if err != nil { log.Error("error creating local TUF client", "err", err) return err } remote, err := tuf.HTTPRemoteStore(args.String["--repository"], tufHTTPOpts("updater")) if err != nil { log.Error("error creating remote TUF client", "err", err) return err } client := tuf.NewClient(local, remote) if !args.Bool["--is-latest"] { return updateAndExecLatest(args.String["--config-dir"], client, log) } // unlink the current binary if it is a temp file if args.Bool["--is-tempfile"] { os.Remove(os.Args[0]) } // read the TUF db so we can pass it to hosts log.Info("reading TUF database") tufDB, err := ioutil.ReadFile(args.String["--tuf-db"]) if err != nil { log.Error("error reading the TUF database", "err", err) return err } log.Info("getting host list") clusterClient := cluster.NewClient() hosts, err := clusterClient.Hosts() if err != nil { log.Error("error getting host list", "err", err) return err } if len(hosts) == 0 { return errors.New("no hosts found") } log.Info(fmt.Sprintf("updating %d hosts", len(hosts))) // eachHost invokes the given function in a goroutine for each host, // returning an error if any of the functions returns an error. eachHost := func(f func(*cluster.Host, log15.Logger) error) (err error) { errs := make(chan error) for _, h := range hosts { go func(host *cluster.Host) { log := log.New("host", host.ID()) errs <- f(host, log) }(h) } for range hosts { if e := <-errs; e != nil { err = e } } return } log.Info("checking host version compatibility") if err := eachHost(func(host *cluster.Host, log log15.Logger) error { status, err := host.GetStatus() if err != nil { log.Error("error getting host status", "err", err) return err } v := version.Parse(status.Version) if v.Before(version.Parse(minVersion)) && !v.Dev { log.Error(ErrIncompatibleVersion.Error(), "version", status.Version) return ErrIncompatibleVersion } return nil }); err != nil { return err } var mtx sync.Mutex images := make(map[string]string) log.Info("pulling latest images on all hosts") if err := eachHost(func(host *cluster.Host, log log15.Logger) error { log.Info("pulling images") ch := make(chan *layer.PullInfo) stream, err := host.PullImages( args.String["--repository"], args.String["--driver"], args.String["--root"], version.String(), bytes.NewReader(tufDB), ch, ) if err != nil { log.Error("error pulling images", "err", err) return err } defer stream.Close() for info := range ch { if info.Type == layer.TypeLayer { continue } log.Info("pulled image", "name", info.Repo) imageURI := fmt.Sprintf("%s?name=%s&id=%s", args.String["--repository"], info.Repo, info.ID) mtx.Lock() images[info.Repo] = imageURI mtx.Unlock() } if err := stream.Err(); err != nil { log.Error("error pulling images", "err", err) return err } return nil }); err != nil { return err } var binaries map[string]string log.Info("pulling latest binaries and config on all hosts") if err := eachHost(func(host *cluster.Host, log log15.Logger) error { log.Info("pulling binaries and config") paths, err := host.PullBinariesAndConfig( args.String["--repository"], args.String["--bin-dir"], args.String["--config-dir"], version.String(), bytes.NewReader(tufDB), ) if err != nil { log.Error("error pulling binaries and config", "err", err) return err } mtx.Lock() binaries = paths mtx.Unlock() log.Info("binaries and config pulled successfully") return nil }); err != nil { return err } log.Info("validating binaries") flynnHost, ok := binaries["flynn-host"] if !ok { return fmt.Errorf("missing flynn-host binary") } flynnInit, ok := binaries["flynn-init"] if !ok { return fmt.Errorf("missing flynn-init binary") } log.Info("updating flynn-host daemon on all hosts") if err := eachHost(func(host *cluster.Host, log log15.Logger) error { // TODO(lmars): handle daemons using custom flags (e.g. --state=/foo) _, err := host.Update( flynnHost, "daemon", "--id", host.ID(), "--flynn-init", flynnInit, ) if err != nil { log.Error("error updating binaries", "err", err) return err } log.Info("flynn-host updated successfully") return nil }); err != nil { return err } updaterImage, ok := images["flynn/updater"] if !ok { e := "missing flynn/updater image" log.Error(e) return errors.New(e) } imageJSON, err := json.Marshal(images) if err != nil { log.Error("error encoding images", "err", err) return err } // use a flag to determine whether to use a TTY log formatter because actually // assigning a TTY to the job causes reading images via stdin to fail. cmd := exec.Command(exec.DockerImage(updaterImage), "/bin/updater", fmt.Sprintf("--tty=%t", term.IsTerminal(os.Stdout.Fd()))) cmd.Stdin = bytes.NewReader(imageJSON) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr if err := cmd.Run(); err != nil { return err } log.Info("update complete") return nil }