func (c *controllerAPI) UpdateApp(ctx context.Context, rw http.ResponseWriter, req *http.Request) { params, _ := ctxhelper.ParamsFromContext(ctx) var data appUpdate if err := httphelper.DecodeJSON(req, &data); err != nil { respondWithError(rw, err) return } if v, ok := data["meta"]; ok && v == nil { // handle {"meta": null} delete(data, "meta") } if err := schema.Validate(data); err != nil { respondWithError(rw, err) return } app, err := c.appRepo.Update(params.ByName("apps_id"), data) if err != nil { respondWithError(rw, err) return } httphelper.JSON(rw, 200, app) }
func (c *controllerAPI) UpdateAppMeta(ctx context.Context, rw http.ResponseWriter, req *http.Request) { params, _ := ctxhelper.ParamsFromContext(ctx) var data appUpdate if err := httphelper.DecodeJSON(req, &data); err != nil { respondWithError(rw, err) return } if err := schema.Validate(data); err != nil { respondWithError(rw, err) return } if data["meta"] == nil { data["meta"] = make(map[string]interface{}) } app, err := c.appRepo.Update(params.ByName("apps_id"), data) if err != nil { respondWithError(rw, err) return } httphelper.JSON(rw, 200, app) }
func (c *controllerAPI) PutFormation(ctx context.Context, w http.ResponseWriter, req *http.Request) { app := c.getApp(ctx) release, err := c.getRelease(ctx) if err != nil { respondWithError(w, err) return } var formation ct.Formation if err = httphelper.DecodeJSON(req, &formation); err != nil { respondWithError(w, err) return } if release.ArtifactID == "" { respondWithError(w, ct.ValidationError{Message: "release is not deployable"}) return } formation.AppID = app.ID formation.ReleaseID = release.ID if err = schema.Validate(formation); err != nil { respondWithError(w, err) return } if err = c.formationRepo.Add(&formation); err != nil { respondWithError(w, err) return } httphelper.JSON(w, 200, &formation) }
func (c *controllerAPI) SetAppRelease(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) if err := schema.Validate(release); err != nil { respondWithError(w, err) return } app := c.getApp(ctx) c.appRepo.SetRelease(app, release.ID) httphelper.JSON(w, 200, release) }
func (c *controllerAPI) PutResource(ctx context.Context, w http.ResponseWriter, req *http.Request) { params, _ := ctxhelper.ParamsFromContext(ctx) p, err := c.getProvider(ctx) if err != nil { respondWithError(w, err) return } var resource ct.Resource if err = httphelper.DecodeJSON(req, &resource); err != nil { respondWithError(w, err) return } resource.ID = params.ByName("resources_id") resource.ProviderID = p.ID if err := schema.Validate(resource); err != nil { respondWithError(w, err) return } if err := c.resourceRepo.Add(&resource); err != nil { respondWithError(w, err) return } httphelper.JSON(w, 200, &resource) }
func createRoute(db *postgres.DB, rc routerc.Client, appID string, route *router.Route) error { route.ParentRef = routeParentRef(appID) if err := schema.Validate(route); err != nil { return err } return rc.CreateRoute(route) }
func (c *controllerAPI) ProvisionResource(ctx context.Context, w http.ResponseWriter, req *http.Request) { p, err := c.getProvider(ctx) if err != nil { respondWithError(w, err) return } var rr ct.ResourceReq if err = httphelper.DecodeJSON(req, &rr); err != nil { respondWithError(w, err) return } var config []byte if rr.Config != nil { config = *rr.Config } else { config = []byte(`{}`) } data, err := resource.Provision(p.URL, config) if err != nil { respondWithError(w, err) return } res := &ct.Resource{ ProviderID: p.ID, ExternalID: data.ID, Env: data.Env, Apps: rr.Apps, } if err := schema.Validate(res); err != nil { respondWithError(w, err) return } if err := c.resourceRepo.Add(res); err != nil { // TODO: attempt to "rollback" provisioning respondWithError(w, err) return } httphelper.JSON(w, 200, res) }
func (c *controllerAPI) CreateRoute(ctx context.Context, w http.ResponseWriter, req *http.Request) { var route router.Route if err := httphelper.DecodeJSON(req, &route); err != nil { respondWithError(w, err) return } route.ParentRef = routeParentRef(c.getApp(ctx).ID) if err := schema.Validate(route); err != nil { respondWithError(w, err) return } if err := c.routerc.CreateRoute(&route); err != nil { respondWithError(w, err) return } httphelper.JSON(w, 200, &route) }
func (c *controllerAPI) PutJob(ctx context.Context, w http.ResponseWriter, req *http.Request) { app := c.getApp(ctx) var job ct.Job if err := httphelper.DecodeJSON(req, &job); err != nil { respondWithError(w, err) return } job.AppID = app.ID if err := schema.Validate(job); err != nil { respondWithError(w, err) return } if err := c.jobRepo.Add(&job); err != nil { respondWithError(w, err) return } httphelper.JSON(w, 200, &job) }
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) data, err = c.artifactRepo.Get(release.ArtifactID) if err != nil { respondWithError(w, err) return } artifact := data.(*ct.Artifact) attach := strings.Contains(req.Header.Get("Upgrade"), "flynn-attach/0") hosts, err := c.clusterClient.ListHosts() if err != nil { respondWithError(w, err) return } if len(hosts) == 0 { respondWithError(w, errors.New("no hosts found")) return } hostID := schedutil.PickHost(hosts).ID id := cluster.RandomJobID("") app := c.getApp(ctx) env := make(map[string]string, 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"] = hostID + "-" + id 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, Artifact: host.Artifact{ Type: artifact.Type, URI: artifact.URI, }, Config: host.ContainerConfig{ Cmd: newJob.Cmd, Env: env, TTY: newJob.TTY, Stdin: attach, DisableLog: newJob.DisableLog, }, } if len(newJob.Entrypoint) > 0 { job.Config.Entrypoint = newJob.Entrypoint } 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), } client, err := c.clusterClient.DialHost(hostID) if err != nil { respondWithError(w, fmt.Errorf("host connect failed: %s", err.Error())) return } attachClient, err = client.Attach(attachReq, true) if err != nil { respondWithError(w, fmt.Errorf("attach failed: %s", err.Error())) return } defer attachClient.Close() } _, err = c.clusterClient.AddJobs(map[string][]*host.Job{hostID: {job}}) if err != nil { respondWithError(w, fmt.Errorf("schedule failed: %s", err.Error())) return } if attach { 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) <-done <-done return } else { httphelper.JSON(w, 200, &ct.Job{ ID: hostID + "-" + job.ID, ReleaseID: newJob.ReleaseID, Cmd: newJob.Cmd, }) } }
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) }
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) 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) if release.ImageArtifactID() == "" { httphelper.ValidationError(w, "release.ImageArtifact", "must be set") return } 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(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 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{ Cmd: newJob.Cmd, Env: env, TTY: newJob.TTY, Stdin: attach, DisableLog: newJob.DisableLog, }, Resources: newJob.Resources, } resource.SetDefaults(&job.Resources) if len(newJob.Entrypoint) > 0 { job.Config.Entrypoint = newJob.Entrypoint } if len(release.ArtifactIDs) > 0 { artifacts, err := c.artifactRepo.ListIDs(release.ArtifactIDs...) if err != nil { respondWithError(w, err) return } job.ImageArtifact = artifacts[release.ImageArtifactID()].HostArtifact() job.FileArtifacts = make([]*host.Artifact, len(release.FileArtifactIDs())) for i, id := range release.FileArtifactIDs() { job.FileArtifacts[i] = artifacts[id].HostArtifact() } } 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 { 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) <-done <-done return } else { httphelper.JSON(w, 200, &ct.Job{ ID: job.ID, UUID: uuid, HostID: hostID, ReleaseID: newJob.ReleaseID, Cmd: newJob.Cmd, }) } }
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) if release.ImageArtifactID() == "" { httphelper.ValidationError(w, "release.ImageArtifact", "must be set") return } 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(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 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{ Env: env, TTY: newJob.TTY, Stdin: attach, DisableLog: newJob.DisableLog, }, Resources: newJob.Resources, } resource.SetDefaults(&job.Resources) if len(newJob.Args) > 0 { job.Config.Args = newJob.Args } if len(release.ArtifactIDs) > 0 { artifacts, err := c.artifactRepo.ListIDs(release.ArtifactIDs...) if err != nil { respondWithError(w, err) return } job.ImageArtifact = artifacts[release.ImageArtifactID()].HostArtifact() job.FileArtifacts = make([]*host.Artifact, len(release.FileArtifactIDs())) for i, id := range release.FileArtifactIDs() { job.FileArtifacts[i] = artifacts[id].HostArtifact() } } // ensure slug apps use /runner/init if release.IsGitDeploy() && (len(job.Config.Args) == 0 || job.Config.Args[0] != "/runner/init") { job.Config.Args = append([]string{"/runner/init"}, job.Config.Args...) } 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 crud(r *httprouter.Router, resource string, example interface{}, repo Repository) { resourceType := reflect.TypeOf(example) prefix := "/" + resource r.POST(prefix, httphelper.WrapHandler(func(ctx context.Context, rw http.ResponseWriter, req *http.Request) { thing := reflect.New(resourceType).Interface() if err := httphelper.DecodeJSON(req, thing); err != nil { respondWithError(rw, err) return } if err := schema.Validate(thing); err != nil { respondWithError(rw, err) return } if err := repo.Add(thing); err != nil { respondWithError(rw, err) return } httphelper.JSON(rw, 200, thing) })) lookup := func(ctx context.Context) (interface{}, error) { params, _ := ctxhelper.ParamsFromContext(ctx) return repo.Get(params.ByName(resource + "_id")) } singletonPath := prefix + "/:" + resource + "_id" r.GET(singletonPath, httphelper.WrapHandler(func(ctx context.Context, rw http.ResponseWriter, _ *http.Request) { thing, err := lookup(ctx) if err != nil { respondWithError(rw, err) return } httphelper.JSON(rw, 200, thing) })) r.GET(prefix, httphelper.WrapHandler(func(ctx context.Context, rw http.ResponseWriter, _ *http.Request) { list, err := repo.List() if err != nil { respondWithError(rw, err) return } httphelper.JSON(rw, 200, list) })) if remover, ok := repo.(Remover); ok { r.DELETE(singletonPath, httphelper.WrapHandler(func(ctx context.Context, rw http.ResponseWriter, _ *http.Request) { _, err := lookup(ctx) if err != nil { respondWithError(rw, err) return } params, _ := ctxhelper.ParamsFromContext(ctx) if err = remover.Remove(params.ByName(resource + "_id")); err != nil { respondWithError(rw, err) return } rw.WriteHeader(200) })) } }