func (api *HTTPAPI) CreateProvider(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { pspec := &volume.ProviderSpec{} if err := httphelper.DecodeJSON(r, &pspec); err != nil { httphelper.Error(w, err) return } if pspec.ID == "" { pspec.ID = random.UUID() } if pspec.Kind == "" { httphelper.ValidationError(w, "kind", "must not be blank") return } var provider volume.Provider provider, err := volumemanager.NewProvider(pspec) if err == volume.UnknownProviderKind { httphelper.ValidationError(w, "kind", fmt.Sprintf("%q is not known", pspec.Kind)) return } if err := api.vman.AddProvider(pspec.ID, provider); err != nil { switch err { case volumemanager.ErrProviderExists: httphelper.ObjectExistsError(w, fmt.Sprintf("provider %q already exists", pspec.ID)) return default: httphelper.Error(w, err) return } } httphelper.JSON(w, 200, pspec) }
func (a *aggregatorAPI) GetLog(ctx context.Context, w http.ResponseWriter, req *http.Request) { ctx, cancel := context.WithCancel(ctx) if cn, ok := w.(http.CloseNotifier); ok { ch := cn.CloseNotify() go func() { select { case <-ch: cancel() case <-ctx.Done(): } }() } defer cancel() params, _ := ctxhelper.ParamsFromContext(ctx) follow := false if strFollow := req.FormValue("follow"); strFollow == "true" { follow = true } var ( backlog bool lines int err error ) if strLines := req.FormValue("lines"); strLines != "" { if lines, err = strconv.Atoi(strLines); err != nil { httphelper.ValidationError(w, "lines", err.Error()) return } if lines < 0 || lines > 10000 { httphelper.ValidationError(w, "lines", "lines must be an integer between 0 and 10000") return } backlog = lines > 0 } filters := make(filterSlice, 0) if jobID := req.FormValue("job_id"); jobID != "" { filters = append(filters, filterJobID(jobID)) } if processTypeVals, ok := req.Form["process_type"]; ok && len(processTypeVals) > 0 { val := processTypeVals[len(processTypeVals)-1] filters = append(filters, filterProcessType(val)) } iter := &Iterator{ id: params.ByName("channel_id"), follow: follow, backlog: backlog, lines: lines, filter: filters, donec: ctx.Done(), } writeMessages(ctx, w, iter.Scan(a.agg)) }
func (c *controllerAPI) KillJob(ctx context.Context, w http.ResponseWriter, req *http.Request) { params, _ := ctxhelper.ParamsFromContext(ctx) job, err := c.jobRepo.Get(params.ByName("jobs_id")) if err != nil { respondWithError(w, err) return } else if job.HostID == "" { httphelper.ValidationError(w, "", "cannot kill a job which has not been placed on a host") return } client, err := c.clusterClient.Host(job.HostID) if err != nil { respondWithError(w, err) return } if err = client.StopJob(job.ID); err != nil { if _, ok := err.(ct.NotFoundError); ok { err = ErrNotFound } respondWithError(w, err) return } }
func (api *httpAPI) GetCloudRegions(w http.ResponseWriter, req *http.Request, _ httprouter.Params) { params := req.URL.Query() cloud := params.Get("cloud") if cloud != "digital_ocean" && cloud != "azure" { httphelper.ObjectNotFoundError(w, "") return } credentialID := params.Get("credential_id") creds, err := api.Installer.FindCredentials(credentialID) if err != nil { httphelper.ValidationError(w, "credential_id", "Invalid credential id") return } var res interface{} switch cloud { case "digital_ocean": res, err = api.Installer.ListDigitalOceanRegions(creds) case "azure": res, err = api.Installer.ListAzureRegions(creds) } if err != nil { httphelper.Error(w, err) return } httphelper.JSON(w, 200, res) }
func (api *HTTPAPI) Send(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { volumeID := ps.ByName("volume_id") if !strings.Contains(r.Header.Get("Accept"), snapshotContentType) { httphelper.ValidationError(w, "", fmt.Sprintf("must be prepared to accept a content type of %q", snapshotContentType)) return } w.Header().Set("Content-Type", snapshotContentType) var haves []json.RawMessage if err := httphelper.DecodeJSON(r, &haves); err != nil { httphelper.Error(w, err) return } err := api.vman.SendSnapshot(volumeID, haves, w) if err != nil { switch err { case volume.ErrNoSuchVolume: httphelper.ObjectNotFoundError(w, fmt.Sprintf("no volume with id %q", volumeID)) return default: httphelper.Error(w, err) return } } }
// servePutInstance adds an instance to a service. func (h *Handler) servePutInstance(w http.ResponseWriter, r *http.Request, params httprouter.Params) { // Read path parameter. service := params.ByName("service") // Read instance from request. inst := &discoverd.Instance{} if err := json.NewDecoder(r.Body).Decode(inst); err != nil { hh.Error(w, err) return } // Ensure instance is valid. if err := inst.Valid(); err != nil { hh.ValidationError(w, "", err.Error()) return } // Add instance to service in the store. if err := h.Store.AddInstance(service, inst); err == ErrNotLeader { h.redirectToLeader(w, r) return } else if IsNotFound(err) { hh.ObjectNotFoundError(w, err.Error()) return } else if err != nil { hh.Error(w, err) return } }
func (a *API) dropDatabase(ctx context.Context, w http.ResponseWriter, req *http.Request) { id := strings.SplitN(strings.TrimPrefix(req.FormValue("id"), "/databases/"), ":", 2) if len(id) != 2 || id[1] == "" { httphelper.ValidationError(w, "id", "is invalid") return } db, err := a.connect() if err != nil { httphelper.Error(w, err) return } defer db.Close() if _, err := db.Exec(fmt.Sprintf("DROP DATABASE `%s`", id[1])); err != nil { httphelper.Error(w, err) return } if _, err := db.Exec(fmt.Sprintf("DROP USER '%s'", id[0])); err != nil { httphelper.Error(w, err) return } w.WriteHeader(200) }
func (p *pgAPI) dropDatabase(ctx context.Context, w http.ResponseWriter, req *http.Request) { id := strings.SplitN(strings.TrimPrefix(req.FormValue("id"), "/databases/"), ":", 2) if len(id) != 2 || id[1] == "" { httphelper.ValidationError(w, "id", "is invalid") return } // disable new connections to the target database if err := p.db.Exec(disallowConns, id[1]); err != nil { httphelper.Error(w, err) return } // terminate current connections if err := p.db.Exec(disconnectConns, id[1]); err != nil { httphelper.Error(w, err) return } if err := p.db.Exec(fmt.Sprintf(`DROP DATABASE "%s"`, id[1])); err != nil { httphelper.Error(w, err) return } if err := p.db.Exec(fmt.Sprintf(`DROP USER "%s"`, id[0])); err != nil { httphelper.Error(w, err) return } w.WriteHeader(200) }
func (api *API) UpdateRoute(ctx context.Context, w http.ResponseWriter, req *http.Request) { log, _ := ctxhelper.LoggerFromContext(ctx) params, _ := ctxhelper.ParamsFromContext(ctx) var route *router.Route if err := json.NewDecoder(req.Body).Decode(&route); err != nil { log.Error(err.Error()) httphelper.Error(w, err) return } route.Type = params.ByName("route_type") route.ID = params.ByName("id") l := api.router.ListenerFor(route.Type) if l == nil { httphelper.ValidationError(w, "type", "Invalid route type") return } if err := l.UpdateRoute(route); err != nil { if err == ErrNotFound { w.WriteHeader(404) return } log.Error(err.Error()) httphelper.Error(w, err) return } httphelper.JSON(w, 200, route) }
func (a *API) dropDatabase(w http.ResponseWriter, req *http.Request, _ httprouter.Params) { id := strings.SplitN(strings.TrimPrefix(req.FormValue("id"), "/databases/"), ":", 2) if len(id) != 2 || id[1] == "" { httphelper.ValidationError(w, "id", "is invalid") return } user, database := id[0], id[1] session, err := mgo.DialWithInfo(&mgo.DialInfo{ Addrs: []string{net.JoinHostPort(serviceHost, "27017")}, Username: "******", Password: os.Getenv("MONGO_PWD"), Database: "admin", }) if err != nil { httphelper.Error(w, err) return } defer session.Close() // Delete user. if err := session.DB(database).Run(bson.D{{"dropUser", user}}, nil); err != nil { httphelper.Error(w, err) return } // Delete database. if err := session.DB(database).Run(bson.D{{"dropDatabase", 1}}, nil); err != nil { httphelper.Error(w, err) return } w.WriteHeader(200) }
// servePutLeader sets the leader for a service. func (h *Handler) servePutLeader(w http.ResponseWriter, r *http.Request, params httprouter.Params) { // Retrieve path parameters. service := params.ByName("service") // Check if the service allows manual leader election. config := h.Store.Config(service) if config == nil || config.LeaderType != discoverd.LeaderTypeManual { hh.ValidationError(w, "", "service leader election type is not manual") return } // Read instance from the request. inst := &discoverd.Instance{} if err := hh.DecodeJSON(r, inst); err != nil { hh.Error(w, err) return } // Manually set the leader on the service. if err := h.Store.SetServiceLeader(service, inst.ID); err == ErrNotLeader { h.redirectToLeader(w, r) return } else if err != nil { hh.Error(w, err) return } }
// servePutService creates a service. func (h *Handler) servePutService(w http.ResponseWriter, r *http.Request, params httprouter.Params) { // Retrieve the path parameter. service := params.ByName("service") if err := ValidServiceName(service); err != nil { hh.ValidationError(w, "", err.Error()) return } // Read config from the request. config := &discoverd.ServiceConfig{} if err := hh.DecodeJSON(r, config); err != nil { hh.Error(w, err) return } // Add the service to the store. if err := h.Store.AddService(service, config); err == ErrNotLeader { h.redirectToLeader(w, r) return } else if IsServiceExists(err) { hh.ObjectExistsError(w, err.Error()) return } else if err != nil { hh.Error(w, err) return } }
func (h *jobAPI) SignalJob(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { sig := ps.ByName("signal") if sig == "" { httphelper.ValidationError(w, "sig", "must not be empty") return } sigInt, err := strconv.Atoi(sig) if err != nil { httphelper.ValidationError(w, "sig", "must be an integer") return } id := ps.ByName("id") if err := h.host.SignalJob(id, sigInt); err != nil { httphelper.Error(w, err) return } w.WriteHeader(200) }
// NOTE: this is temporary until httphelper supports custom errors func respondWithError(w http.ResponseWriter, err error) { switch v := err.(type) { case ct.ValidationError: httphelper.ValidationError(w, v.Field, v.Message) default: if err == ErrNotFound { w.WriteHeader(404) return } httphelper.Error(w, err) } }
func (api *httpAPI) InstallHandler(w http.ResponseWriter, req *http.Request, params httprouter.Params) { var input *jsonInput if err := httphelper.DecodeJSON(req, &input); err != nil { httphelper.Error(w, err) return } api.InstallerStackMtx.Lock() defer api.InstallerStackMtx.Unlock() if len(api.InstallerStacks) > 0 { httphelper.ObjectExistsError(w, "install already started") return } var id = random.Hex(16) var creds aws.CredentialsProvider if input.Creds.AccessKeyID != "" && input.Creds.SecretAccessKey != "" { creds = aws.Creds(input.Creds.AccessKeyID, input.Creds.SecretAccessKey, "") } else { var err error creds, err = aws.EnvCreds() if err != nil { httphelper.ValidationError(w, "", err.Error()) return } } s := &httpInstaller{ ID: id, PromptOutChan: make(chan *httpPrompt), PromptInChan: make(chan *httpPrompt), logger: log.New(), api: api, } s.Stack = &Stack{ Creds: creds, Region: input.Region, InstanceType: input.InstanceType, NumInstances: input.NumInstances, VpcCidr: input.VpcCidr, SubnetCidr: input.SubnetCidr, PromptInput: s.PromptInput, YesNoPrompt: s.YesNoPrompt, } if err := s.Stack.RunAWS(); err != nil { httphelper.Error(w, err) return } api.InstallerStacks[id] = s go s.handleEvents() httphelper.JSON(w, 200, s) }
func (a *aggregatorAPI) GetLog(ctx context.Context, w http.ResponseWriter, req *http.Request) { ctx, cancel := context.WithCancel(ctx) if cn, ok := w.(http.CloseNotifier); ok { go func() { select { case <-cn.CloseNotify(): cancel() case <-ctx.Done(): } }() } defer cancel() params, _ := ctxhelper.ParamsFromContext(ctx) channelID := params.ByName("channel_id") follow := false if strFollow := req.FormValue("follow"); strFollow == "true" { follow = true } lines := -1 // default to all lines if strLines := req.FormValue("lines"); strLines != "" { var err error lines, err = strconv.Atoi(strLines) if err != nil || lines < 0 || lines > 10000 { httphelper.ValidationError(w, "lines", "lines must be an integer between 0 and 10000") return } } filters := make(filterSlice, 0) if jobID := req.FormValue("job_id"); jobID != "" { filters = append(filters, filterJobID(jobID)) } if processTypeVals, ok := req.Form["process_type"]; ok && len(processTypeVals) > 0 { val := processTypeVals[len(processTypeVals)-1] filters = append(filters, filterProcessType(val)) } w.WriteHeader(200) var msgc <-chan *rfc5424.Message if follow { msgc = a.agg.ReadLastNAndSubscribe(channelID, lines, filters, ctx.Done()) } else { msgc = a.agg.ReadLastN(channelID, lines, filters, ctx.Done()) } writeMessages(ctx, w, msgc) }
func (h *httpAPI) RemoveService(w http.ResponseWriter, r *http.Request, params httprouter.Params) { service := params.ByName("service") if err := ValidServiceName(service); err != nil { hh.ValidationError(w, "", err.Error()) return } if err := h.Store.RemoveService(params.ByName("service")); err != nil { if IsNotFound(err) { hh.ObjectNotFoundError(w, err.Error()) } else { hh.Error(w, err) } return } }
func (api *httpAPI) GetAzureSubscriptions(w http.ResponseWriter, req *http.Request, _ httprouter.Params) { params := req.URL.Query() credentialID := params.Get("credential_id") creds, err := api.Installer.FindCredentials(credentialID) if err != nil { httphelper.ValidationError(w, "credential_id", "Invalid credential id") return } client := api.Installer.azureClient(creds) res, err := client.ListSubscriptions() if err != nil { httphelper.Error(w, err) return } httphelper.JSON(w, 200, res) }
func (h *httpAPI) AddInstance(w http.ResponseWriter, r *http.Request, params httprouter.Params) { inst := &discoverd.Instance{} if err := json.NewDecoder(r.Body).Decode(inst); err != nil { hh.Error(w, err) return } if err := inst.Valid(); err != nil { hh.ValidationError(w, "", err.Error()) return } if err := h.Store.AddInstance(params.ByName("service"), inst); err != nil { if IsNotFound(err) { hh.ObjectNotFoundError(w, err.Error()) } else { hh.Error(w, err) } return } }
func (h *httpAPI) SetLeader(w http.ResponseWriter, r *http.Request, params httprouter.Params) { service := params.ByName("service") config := h.Store.GetConfig(service) if config == nil || config.LeaderType != discoverd.LeaderTypeManual { hh.ValidationError(w, "", "service leader election type is not manual") return } inst := &discoverd.Instance{} if err := hh.DecodeJSON(r, inst); err != nil { hh.Error(w, err) return } if err := h.Store.SetLeader(service, inst.ID); err != nil { hh.Error(w, err) return } }
func (p *pgAPI) dropDatabase(w http.ResponseWriter, req *http.Request, _ httprouter.Params) { id := strings.SplitN(strings.TrimPrefix(req.FormValue("id"), "/databases/"), ":", 2) if len(id) != 2 || id[1] == "" { httphelper.ValidationError(w, "id", "is invalid") return } if err := p.db.Exec(fmt.Sprintf(`DROP DATABASE "%s"`, id[1])); err != nil { httphelper.Error(w, err) return } if err := p.db.Exec(fmt.Sprintf(`DROP USER "%s"`, id[0])); err != nil { httphelper.Error(w, err) return } w.WriteHeader(200) }
func (c *controllerAPI) GetFormations(ctx context.Context, w http.ResponseWriter, req *http.Request) { if strings.Contains(req.Header.Get("Accept"), "text/event-stream") { c.streamFormations(ctx, w, req) return } if req.URL.Query().Get("active") == "true" { list, err := c.formationRepo.ListActive() if err != nil { respondWithError(w, err) return } httphelper.JSON(w, 200, list) } // don't return a list of all formations, there will be lots of them // and no components currently need such a list httphelper.ValidationError(w, "", "must either request a stream or only active formations") }
func (api *API) CreateRoute(ctx context.Context, w http.ResponseWriter, req *http.Request) { log, _ := ctxhelper.LoggerFromContext(ctx) var route *router.Route if err := json.NewDecoder(req.Body).Decode(&route); err != nil { log.Error(err.Error()) httphelper.Error(w, err) return } l := api.router.ListenerFor(route.Type) if l == nil { httphelper.ValidationError(w, "type", "Invalid route type") return } err := l.AddRoute(route) if err != nil { rjson, jerr := json.Marshal(&route) if jerr != nil { log.Error(jerr.Error()) httphelper.Error(w, jerr) return } jsonError := httphelper.JSONError{Detail: rjson} switch err { case ErrConflict: jsonError.Code = httphelper.ConflictErrorCode jsonError.Message = "Duplicate route" case ErrInvalid: jsonError.Code = httphelper.ValidationErrorCode jsonError.Message = "Invalid route" default: log.Error(err.Error()) httphelper.Error(w, err) return } httphelper.Error(w, jsonError) return } httphelper.JSON(w, 200, route) }
// serveDeleteService removes a service from the store by name. func (h *Handler) serveDeleteService(w http.ResponseWriter, r *http.Request, params httprouter.Params) { // Retrieve the path parameter. service := params.ByName("service") if err := ValidServiceName(service); err != nil { hh.ValidationError(w, "", err.Error()) return } // Delete from the store. if err := h.Store.RemoveService(params.ByName("service")); err == ErrNotLeader { h.redirectToLeader(w, r) return } else if IsNotFound(err) { hh.ObjectNotFoundError(w, err.Error()) return } else if err != nil { hh.Error(w, err) return } }
func (h *httpAPI) AddService(w http.ResponseWriter, r *http.Request, params httprouter.Params) { service := params.ByName("service") if err := ValidServiceName(service); err != nil { hh.ValidationError(w, "", err.Error()) return } config := &discoverd.ServiceConfig{} if err := hh.DecodeJSON(r, config); err != nil { hh.Error(w, err) return } if err := h.Store.AddService(service, config); err != nil { if IsServiceExists(err) { hh.ObjectExistsError(w, err.Error()) } else { hh.Error(w, err) } return } }
func (h *Handler) serveDeleteCluster(w http.ResponseWriter, req *http.Request, _ httprouter.Params) { // Extract release ID. h.Logger.Info("parsing id", "id", req.FormValue("id")) releaseID := strings.TrimPrefix(req.FormValue("id"), "/clusters/") if releaseID == "" { h.Logger.Error("error parsing id", "id", req.FormValue("id")) httphelper.ValidationError(w, "id", "is invalid") return } // Retrieve release. h.Logger.Info("retrieving release", "release.id", releaseID) release, err := h.ControllerClient.GetRelease(releaseID) if err != nil { h.Logger.Error("error finding release", "err", err, "release.id", releaseID) httphelper.Error(w, err) return } // Retrieve app name from env variable. appName := release.Env["FLYNN_REDIS"] if appName == "" { h.Logger.Error("unable to find app name", "release.id", releaseID) httphelper.Error(w, err) return } h.Logger.Info("found release app", "app.name", appName) // Destroy app release. h.Logger.Info("destroying app", "app.name", appName) if _, err := h.ControllerClient.DeleteApp(appName); err != nil { h.Logger.Error("error destroying app", "err", err) httphelper.Error(w, err) return } w.WriteHeader(200) }
func (h *jobAPI) AddJob(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { // TODO(titanous): validate UUID id := ps.ByName("id") log := h.host.log.New("fn", "AddJob", "job.id", id) if !h.addJobRateLimitBucket.Take() { log.Warn("maximum concurrent AddJob calls running") httphelper.Error(w, httphelper.JSONError{ Code: httphelper.RatelimitedErrorCode, Message: "maximum concurrent AddJob calls running, try again later", Retry: true, }) return } if shutdown.IsActive() { log.Warn("refusing to start job due to active shutdown") httphelper.JSON(w, 500, struct{}{}) h.addJobRateLimitBucket.Put() return } log.Info("decoding job") job := &host.Job{ID: id} if err := httphelper.DecodeJSON(r, job); err != nil { log.Error("error decoding job", "err", err) httphelper.Error(w, err) h.addJobRateLimitBucket.Put() return } if len(job.Mountspecs) == 0 { log.Warn("rejecting job as no mountspecs set") httphelper.ValidationError(w, "mountspecs", "must be set") h.addJobRateLimitBucket.Put() return } log.Info("acquiring state database") if err := h.host.state.Acquire(); err != nil { log.Error("error acquiring state database", "err", err) httphelper.Error(w, err) h.addJobRateLimitBucket.Put() return } if err := h.host.state.AddJob(job); err != nil { log.Error("error adding job to state database", "err", err) if err == ErrJobExists { httphelper.ConflictError(w, err.Error()) } else { httphelper.Error(w, err) } h.addJobRateLimitBucket.Put() return } go func() { log.Info("running job") err := h.host.backend.Run(job, nil, h.addJobRateLimitBucket) h.host.state.Release() if err != nil { log.Error("error running job", "err", err) h.host.state.SetStatusFailed(job.ID, err) } h.addJobRateLimitBucket.Put() }() // TODO(titanous): return 201 Accepted httphelper.JSON(w, 200, struct{}{}) }
func (api *httpAPI) LaunchCluster(w http.ResponseWriter, req *http.Request, params httprouter.Params) { var inputJSON bytes.Buffer if _, err := inputJSON.ReadFrom(req.Body); err != nil { httphelper.Error(w, err) return } decodeJSON := func(dst interface{}) error { return json.Unmarshal(inputJSON.Bytes(), dst) } var base *BaseCluster if err := decodeJSON(&base); err != nil { httphelper.Error(w, err) return } if base.CredentialID == "" { httphelper.ValidationError(w, "credential_id", "Missing credential id") return } var creds *Credential if base.Type == "aws" && base.CredentialID == "aws_env" { creds = &Credential{ ID: base.CredentialID, } } else { var err error creds, err = api.Installer.FindCredentials(base.CredentialID) if err != nil { httphelper.ValidationError(w, "credential_id", "Invalid credential id") return } } var cluster Cluster switch base.Type { case "aws": cluster = &AWSCluster{} case "digital_ocean": cluster = &DigitalOceanCluster{} case "azure": cluster = &AzureCluster{} default: httphelper.ValidationError(w, "type", fmt.Sprintf("Invalid type \"%s\"", base.Type)) return } base.ID = fmt.Sprintf("flynn-%d", time.Now().Unix()) base.State = "starting" base.installer = api.Installer if err := decodeJSON(&cluster); err != nil { httphelper.Error(w, err) return } cluster.SetBase(base) if err := cluster.SetCreds(creds); err != nil { httphelper.Error(w, err) return } if err := api.Installer.LaunchCluster(cluster); err != nil { httphelper.Error(w, err) return } httphelper.JSON(w, 200, base) }
func (c *controllerAPI) RunJob(ctx context.Context, w http.ResponseWriter, req *http.Request) { var newJob ct.NewJob if err := httphelper.DecodeJSON(req, &newJob); err != nil { respondWithError(w, err) return } if err := schema.Validate(newJob); err != nil { respondWithError(w, err) return } data, err := c.releaseRepo.Get(newJob.ReleaseID) if err != nil { respondWithError(w, err) return } release := data.(*ct.Release) var artifactIDs []string if len(newJob.ArtifactIDs) > 0 { artifactIDs = newJob.ArtifactIDs } else if len(release.ArtifactIDs) > 0 { artifactIDs = release.ArtifactIDs } else { httphelper.ValidationError(w, "release.ArtifactIDs", "cannot be empty") return } artifacts := make([]*ct.Artifact, len(artifactIDs)) artifactList, err := c.artifactRepo.ListIDs(artifactIDs...) if err != nil { respondWithError(w, err) return } for i, id := range artifactIDs { artifacts[i] = artifactList[id] } var entrypoint ct.ImageEntrypoint if e := utils.GetEntrypoint(artifacts, ""); e != nil { entrypoint = *e } attach := strings.Contains(req.Header.Get("Upgrade"), "flynn-attach/0") hosts, err := c.clusterClient.Hosts() if err != nil { respondWithError(w, err) return } if len(hosts) == 0 { respondWithError(w, errors.New("no hosts found")) return } client := hosts[random.Math.Intn(len(hosts))] uuid := random.UUID() hostID := client.ID() id := cluster.GenerateJobID(hostID, uuid) app := c.getApp(ctx) env := make(map[string]string, len(entrypoint.Env)+len(release.Env)+len(newJob.Env)+4) env["FLYNN_APP_ID"] = app.ID env["FLYNN_RELEASE_ID"] = release.ID env["FLYNN_PROCESS_TYPE"] = "" env["FLYNN_JOB_ID"] = id for k, v := range entrypoint.Env { env[k] = v } if newJob.ReleaseEnv { for k, v := range release.Env { env[k] = v } } for k, v := range newJob.Env { env[k] = v } metadata := make(map[string]string, len(newJob.Meta)+3) for k, v := range newJob.Meta { metadata[k] = v } metadata["flynn-controller.app"] = app.ID metadata["flynn-controller.app_name"] = app.Name metadata["flynn-controller.release"] = release.ID job := &host.Job{ ID: id, Metadata: metadata, Config: host.ContainerConfig{ Args: entrypoint.Args, Env: env, WorkingDir: entrypoint.WorkingDir, Uid: entrypoint.Uid, Gid: entrypoint.Gid, TTY: newJob.TTY, Stdin: attach, DisableLog: newJob.DisableLog, }, Resources: newJob.Resources, Partition: string(newJob.Partition), } resource.SetDefaults(&job.Resources) if len(newJob.Args) > 0 { job.Config.Args = newJob.Args } utils.SetupMountspecs(job, artifacts) // provision data volume if required if newJob.Data { vol := &ct.VolumeReq{Path: "/data", DeleteOnStop: true} if _, err := utils.ProvisionVolume(vol, client, job); err != nil { respondWithError(w, err) return } } var attachClient cluster.AttachClient if attach { attachReq := &host.AttachReq{ JobID: job.ID, Flags: host.AttachFlagStdout | host.AttachFlagStderr | host.AttachFlagStdin | host.AttachFlagStream, Height: uint16(newJob.Lines), Width: uint16(newJob.Columns), } attachClient, err = client.Attach(attachReq, true) if err != nil { respondWithError(w, fmt.Errorf("attach failed: %s", err.Error())) return } defer attachClient.Close() } if err := client.AddJob(job); err != nil { respondWithError(w, fmt.Errorf("schedule failed: %s", err.Error())) return } if attach { // TODO(titanous): This Wait could block indefinitely if something goes // wrong, a context should be threaded in that cancels if the client // goes away. if err := attachClient.Wait(); err != nil { respondWithError(w, fmt.Errorf("attach wait failed: %s", err.Error())) return } w.Header().Set("Connection", "upgrade") w.Header().Set("Upgrade", "flynn-attach/0") w.WriteHeader(http.StatusSwitchingProtocols) conn, _, err := w.(http.Hijacker).Hijack() if err != nil { panic(err) } defer conn.Close() done := make(chan struct{}, 2) cp := func(to io.Writer, from io.Reader) { io.Copy(to, from) done <- struct{}{} } go cp(conn, attachClient.Conn()) go cp(attachClient.Conn(), conn) // Wait for one of the connections to be closed or interrupted. EOF is // framed inside the attach protocol, so a read/write error indicates // that we're done and should clean up. <-done return } else { httphelper.JSON(w, 200, &ct.Job{ ID: job.ID, UUID: uuid, HostID: hostID, ReleaseID: newJob.ReleaseID, Args: newJob.Args, }) } }
func (c *controllerAPI) CreateDeployment(ctx context.Context, w http.ResponseWriter, req *http.Request) { var rid releaseID if err := httphelper.DecodeJSON(req, &rid); err != nil { respondWithError(w, err) return } rel, err := c.releaseRepo.Get(rid.ID) if err != nil { if err == ErrNotFound { err = ct.ValidationError{ Message: fmt.Sprintf("could not find release with ID %s", rid.ID), } } respondWithError(w, err) return } release := rel.(*ct.Release) app := c.getApp(ctx) // TODO: wrap all of this in a transaction oldRelease, err := c.appRepo.GetRelease(app.ID) if err == ErrNotFound { oldRelease = &ct.Release{} } else if err != nil { respondWithError(w, err) return } oldFormation, err := c.formationRepo.Get(app.ID, oldRelease.ID) if err == ErrNotFound { oldFormation = &ct.Formation{} } else if err != nil { respondWithError(w, err) return } procCount := 0 for _, i := range oldFormation.Processes { procCount += i } deployment := &ct.Deployment{ AppID: app.ID, NewReleaseID: release.ID, Strategy: app.Strategy, OldReleaseID: oldRelease.ID, Processes: oldFormation.Processes, DeployTimeout: app.DeployTimeout, } if err := schema.Validate(deployment); err != nil { respondWithError(w, err) return } if procCount == 0 { // immediately set app release if err := c.appRepo.SetRelease(app, release.ID); err != nil { respondWithError(w, err) return } now := time.Now() deployment.FinishedAt = &now } d, err := c.deploymentRepo.Add(deployment) if err != nil { if postgres.IsUniquenessError(err, "isolate_deploys") { httphelper.ValidationError(w, "", "Cannot create deploy, there is already one in progress for this app.") return } respondWithError(w, err) return } httphelper.JSON(w, 200, d) }