func runLimitSet(args *docopt.Args, client *controller.Client) error { proc := args.String["<proc>"] release, err := client.GetAppRelease(mustApp()) if err == controller.ErrNotFound { release = &ct.Release{} if proc != "" { release.Processes = make(map[string]ct.ProcessType) release.Processes[proc] = ct.ProcessType{} } } else if err != nil { return err } t, ok := release.Processes[proc] if !ok { return fmt.Errorf("unknown process type %q", proc) } if t.Resources == nil { t.Resources = resource.Defaults() } limits := args.All["<var>=<val>"].([]string) for _, limit := range limits { typVal := strings.SplitN(limit, "=", 2) if len(typVal) != 2 { return fmt.Errorf("invalid resource limit: %q", limit) } typ, ok := resource.ToType(typVal[0]) if !ok { return fmt.Errorf("invalid resource limit type: %q", typVal) } val, err := resource.ParseLimit(typ, typVal[1]) if err != nil { return fmt.Errorf("invalid resource limit value: %q", typVal[1]) } t.Resources[typ] = resource.Spec{Limit: typeconv.Int64Ptr(val)} } release.Processes[proc] = t release.ID = "" if err := client.CreateRelease(release); err != nil { return err } if err := client.DeployAppRelease(mustApp(), release.ID); err != nil { return err } fmt.Printf("Created release %s\n", release.ID) return nil }
func run() error { client, err := controller.NewClient("", os.Getenv("CONTROLLER_KEY")) if err != nil { return fmt.Errorf("Unable to connect to controller: %s", err) } usage := ` Usage: flynn-receiver <app> <rev> [-e <var>=<val>]... [-m <key>=<val>]... Options: -e,--env <var>=<val> -m,--meta <key>=<val> `[1:] args, _ := docopt.Parse(usage, nil, true, version.String(), false) appName := args.String["<app>"] env, err := parsePairs(args, "--env") if err != nil { return err } meta, err := parsePairs(args, "--meta") if err != nil { return err } slugBuilder, err := client.GetArtifact(os.Getenv("SLUGBUILDER_IMAGE_ID")) if err != nil { return fmt.Errorf("Error getting slugbuilder image: %s", err) } slugRunnerID := os.Getenv("SLUGRUNNER_IMAGE_ID") if _, err := client.GetArtifact(slugRunnerID); err != nil { return fmt.Errorf("Error getting slugrunner image: %s", err) } app, err := client.GetApp(appName) if err == controller.ErrNotFound { return fmt.Errorf("Unknown app %q", appName) } else if err != nil { return fmt.Errorf("Error retrieving app: %s", err) } prevRelease, err := client.GetAppRelease(app.Name) if err == controller.ErrNotFound { prevRelease = &ct.Release{} } else if err != nil { return fmt.Errorf("Error getting current app release: %s", err) } fmt.Printf("-----> Building %s...\n", app.Name) slugImageID := random.UUID() jobEnv := map[string]string{ "BUILD_CACHE_URL": fmt.Sprintf("%s/%s-cache.tgz", blobstoreURL, app.ID), "CONTROLLER_KEY": os.Getenv("CONTROLLER_KEY"), "SLUG_IMAGE_ID": slugImageID, } if buildpackURL, ok := env["BUILDPACK_URL"]; ok { jobEnv["BUILDPACK_URL"] = buildpackURL } else if buildpackURL, ok := prevRelease.Env["BUILDPACK_URL"]; ok { jobEnv["BUILDPACK_URL"] = buildpackURL } for _, k := range []string{"SSH_CLIENT_KEY", "SSH_CLIENT_HOSTS"} { if v := os.Getenv(k); v != "" { jobEnv[k] = v } } job := &host.Job{ Config: host.ContainerConfig{ Args: []string{"/builder/build.sh"}, Env: jobEnv, Stdin: true, DisableLog: true, }, Partition: "background", Metadata: map[string]string{ "flynn-controller.app": app.ID, "flynn-controller.app_name": app.Name, "flynn-controller.release": prevRelease.ID, "flynn-controller.type": "slugbuilder", }, Resources: resource.Defaults(), } if sb, ok := prevRelease.Processes["slugbuilder"]; ok { job.Resources = sb.Resources } else if rawLimit := os.Getenv("SLUGBUILDER_DEFAULT_MEMORY_LIMIT"); rawLimit != "" { if limit, err := resource.ParseLimit(resource.TypeMemory, rawLimit); err == nil { job.Resources[resource.TypeMemory] = resource.Spec{Limit: &limit, Request: &limit} } } cmd := exec.Job(slugBuilder, job) cmd.Volumes = []*ct.VolumeReq{{Path: "/tmp", DeleteOnStop: true}} var output bytes.Buffer cmd.Stdout = io.MultiWriter(os.Stdout, &output) cmd.Stderr = os.Stderr releaseEnv := make(map[string]string, len(env)) if prevRelease.Env != nil { for k, v := range prevRelease.Env { releaseEnv[k] = v } } for k, v := range env { releaseEnv[k] = v } if len(releaseEnv) > 0 { stdin, err := cmd.StdinPipe() if err != nil { return err } go func() { if err := appendEnvDir(os.Stdin, stdin, releaseEnv); err != nil { log.Fatalln("ERROR:", err) } }() } else { cmd.Stdin = os.Stdin } shutdown.BeforeExit(func() { cmd.Kill() }) if err := cmd.Run(); err != nil { return fmt.Errorf("Build failed: %s", err) } var types []string if match := typesPattern.FindSubmatch(output.Bytes()); match != nil { types = strings.Split(string(match[1]), ", ") } fmt.Printf("-----> Creating release...\n") release := &ct.Release{ ArtifactIDs: []string{slugRunnerID, slugImageID}, Env: releaseEnv, Meta: prevRelease.Meta, } if release.Meta == nil { release.Meta = make(map[string]string, len(meta)) } for k, v := range meta { release.Meta[k] = v } procs := make(map[string]ct.ProcessType) for _, t := range types { proc := prevRelease.Processes[t] proc.Args = []string{"/runner/init", "start", t} if (t == "web" || strings.HasSuffix(t, "-web")) && proc.Service == "" { proc.Service = app.Name + "-" + t proc.Ports = []ct.Port{{ Port: 8080, Proto: "tcp", Service: &host.Service{ Name: proc.Service, Create: true, Check: &host.HealthCheck{Type: "tcp"}, }, }} } procs[t] = proc } if sb, ok := prevRelease.Processes["slugbuilder"]; ok { procs["slugbuilder"] = sb } release.Processes = procs if err := client.CreateRelease(release); err != nil { return fmt.Errorf("Error creating release: %s", err) } if err := client.DeployAppRelease(app.Name, release.ID, nil); err != nil { return fmt.Errorf("Error deploying app release: %s", err) } // if the app has a web job and has not been scaled before, create a // web=1 formation and wait for the "APPNAME-web" service to start // (whilst also watching job events so the deploy fails if the job // crashes) if needsDefaultScale(app.ID, prevRelease.ID, procs, client) { fmt.Println("=====> Scaling initial release to web=1") formation := &ct.Formation{ AppID: app.ID, ReleaseID: release.ID, Processes: map[string]int{"web": 1}, } jobEvents := make(chan *ct.Job) jobStream, err := client.StreamJobEvents(app.ID, jobEvents) if err != nil { return fmt.Errorf("Error streaming job events: %s", err) } defer jobStream.Close() serviceEvents := make(chan *discoverd.Event) serviceStream, err := discoverd.NewService(app.Name + "-web").Watch(serviceEvents) if err != nil { return fmt.Errorf("Error streaming service events: %s", err) } defer serviceStream.Close() if err := client.PutFormation(formation); err != nil { return fmt.Errorf("Error putting formation: %s", err) } fmt.Println("-----> Waiting for initial web job to start...") err = func() error { for { select { case e, ok := <-serviceEvents: if !ok { return fmt.Errorf("Service stream closed unexpectedly: %s", serviceStream.Err()) } if e.Kind == discoverd.EventKindUp && e.Instance.Meta["FLYNN_RELEASE_ID"] == release.ID { fmt.Println("=====> Initial web job started") return nil } case e, ok := <-jobEvents: if !ok { return fmt.Errorf("Job stream closed unexpectedly: %s", jobStream.Err()) } if e.State == ct.JobStateDown { return errors.New("Initial web job failed to start") } case <-time.After(time.Duration(app.DeployTimeout) * time.Second): return errors.New("Timed out waiting for initial web job to start") } } }() if err != nil { fmt.Println("-----> WARN: scaling initial release down to web=0 due to error") formation.Processes["web"] = 0 if err := client.PutFormation(formation); err != nil { // just print this error and return the original error fmt.Println("-----> WARN: could not scale the initial release down (it may continue to run):", err) } return err } } fmt.Println("=====> Application deployed") return nil }