func (a *API) createDatabase(ctx context.Context, w http.ResponseWriter, req *http.Request) { username, password, database := random.Hex(16), random.Hex(16), random.Hex(16) if _, err := a.db.Exec(fmt.Sprintf("CREATE USER '%s'@'%%' IDENTIFIED BY '%s'", username, password)); err != nil { httphelper.Error(w, err) return } if _, err := a.db.Exec(fmt.Sprintf("CREATE DATABASE `%s`", database)); err != nil { a.db.Exec(fmt.Sprintf("DROP USER '%s'", username)) httphelper.Error(w, err) return } if _, err := a.db.Exec(fmt.Sprintf("GRANT ALL ON `%s`.* TO '%s'@'%%'", database, username)); err != nil { a.db.Exec(fmt.Sprintf("DROP DATABASE `%s`", database)) a.db.Exec(fmt.Sprintf("DROP USER '%s'", username)) httphelper.Error(w, err) return } url := fmt.Sprintf("mysql://%s:%s@%s:3306/%s", username, password, serviceHost, database) httphelper.JSON(w, 200, resource.Resource{ ID: fmt.Sprintf("/databases/%s:%s", username, database), Env: map[string]string{ "FLYNN_MYSQL": serviceName, "MYSQL_HOST": serviceHost, "MYSQL_USER": username, "MYSQL_PWD": password, "MYSQL_DATABASE": database, "DATABASE_URL": url, }, }) }
func (a *API) ping(ctx context.Context, w http.ResponseWriter, req *http.Request) { logger := a.logger().New("fn", "ping") logger.Info("checking status", "host", serviceHost) if status, err := sirenia.NewClient(serviceHost + ":3306").Status(); err == nil && status.Database != nil && status.Database.ReadWrite { logger.Info("database is up, skipping scale check") } else { scaled, err := scale.CheckScale(app, controllerKey, "mariadb", a.logger()) if err != nil { httphelper.Error(w, err) return } // Cluster has yet to be scaled, return healthy if !scaled { w.WriteHeader(200) return } } db, err := a.connect() if err != nil { httphelper.Error(w, err) return } defer db.Close() if _, err := db.Exec("SELECT 1"); err != nil { httphelper.Error(w, err) return } w.WriteHeader(200) }
func (api *httpAPI) ServeTemplate(w http.ResponseWriter, req *http.Request, params httprouter.Params) { if req.Header.Get("Accept") == "application/json" { s, err := api.Installer.FindBaseCluster(params.ByName("id")) if err != nil { httphelper.ObjectNotFoundError(w, err.Error()) return } httphelper.JSON(w, 200, s) return } manifest, err := api.AssetManifest() if err != nil { httphelper.Error(w, err) api.logger.Debug(err.Error()) return } w.Header().Add("Content-Type", "text/html; charset=utf-8") w.Header().Add("Cache-Control", "max-age=0") err = htmlTemplate.Execute(w, &htmlTemplateData{ ApplicationJSPath: manifest.Assets["application.js"], ApplicationCSSPath: manifest.Assets["application.css"], ReactJSPath: manifest.Assets["react.js"], }) if err != nil { httphelper.Error(w, err) api.logger.Debug(err.Error()) return } }
func (h *jobAPI) Update(w http.ResponseWriter, req *http.Request, _ httprouter.Params) { log := h.host.log.New("fn", "Update") log.Info("decoding command") var cmd host.Command if err := httphelper.DecodeJSON(req, &cmd); err != nil { log.Error("error decoding command", "err", err) httphelper.Error(w, err) return } log.Info("updating host") err := h.host.Update(&cmd) if err != nil { httphelper.Error(w, err) return } // send an ok response and then shutdown after 1s to give the response // chance to reach the client. httphelper.JSON(w, http.StatusOK, cmd) log.Info("shutting down in 1s") time.AfterFunc(time.Second, func() { log.Info("exiting") os.Exit(0) }) }
func (p *pgAPI) createDatabase(ctx context.Context, w http.ResponseWriter, req *http.Request) { username, password, database := random.Hex(16), random.Hex(16), random.Hex(16) if err := p.db.Exec(fmt.Sprintf(`CREATE USER "%s" WITH PASSWORD '%s'`, username, password)); err != nil { httphelper.Error(w, err) return } if err := p.db.Exec(fmt.Sprintf(`CREATE DATABASE "%s"`, database)); err != nil { p.db.Exec(fmt.Sprintf(`DROP USER "%s"`, username)) httphelper.Error(w, err) return } if err := p.db.Exec(fmt.Sprintf(`GRANT ALL ON DATABASE "%s" TO "%s"`, database, username)); err != nil { p.db.Exec(fmt.Sprintf(`DROP DATABASE "%s"`, database)) p.db.Exec(fmt.Sprintf(`DROP USER "%s"`, username)) httphelper.Error(w, err) return } url := fmt.Sprintf("postgres://%s:%s@%s:5432/%s", username, password, serviceHost, database) httphelper.JSON(w, 200, resource.Resource{ ID: fmt.Sprintf("/databases/%s:%s", username, database), Env: map[string]string{ "FLYNN_POSTGRES": serviceName, "PGHOST": serviceHost, "PGUSER": username, "PGPASSWORD": password, "PGDATABASE": database, "DATABASE_URL": url, }, }) }
func (api *API) Login(ctx context.Context, w http.ResponseWriter, req *http.Request) { var info LoginInfo if strings.Contains(req.Header.Get("Content-Type"), "form-urlencoded") { if err := req.ParseForm(); err != nil { httphelper.Error(w, err) return } info = LoginInfo{Token: req.PostForm.Get("token")} } else { if err := json.NewDecoder(req.Body).Decode(&info); err != nil { httphelper.Error(w, err) return } } if len(info.Token) != len(api.conf.LoginToken) || subtle.ConstantTimeCompare([]byte(info.Token), []byte(api.conf.LoginToken)) != 1 { httphelper.Error(w, httphelper.JSONError{ Code: httphelper.UnauthorizedErrorCode, Message: "Invalid login token", }) return } api.SetAuthenticated(ctx, w, req) if strings.Contains(req.Header.Get("Content-Type"), "form-urlencoded") { http.Redirect(w, req, api.conf.CookiePath, 302) } else { 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 (api *API) CreateCert(ctx context.Context, w http.ResponseWriter, req *http.Request) { var cert *router.Certificate if err := json.NewDecoder(req.Body).Decode(&cert); err != nil { httphelper.Error(w, err) return } l := api.router.HTTP.(*HTTPListener) err := l.AddCert(cert) if err != nil { jsonError := httphelper.JSONError{} switch err { case ErrConflict: jsonError.Code = httphelper.ConflictErrorCode jsonError.Message = "Duplicate cert" case ErrInvalid: jsonError.Code = httphelper.ValidationErrorCode jsonError.Message = "Invalid cert" default: httphelper.Error(w, err) return } } httphelper.JSON(w, 200, cert) }
func (api *API) DeleteRoute(ctx context.Context, w http.ResponseWriter, req *http.Request) { log, _ := ctxhelper.LoggerFromContext(ctx) params, _ := ctxhelper.ParamsFromContext(ctx) l := api.router.ListenerFor(params.ByName("route_type")) if l == nil { w.WriteHeader(404) return } err := l.RemoveRoute(params.ByName("id")) if err != nil { switch err { case ErrNotFound: w.WriteHeader(404) return case ErrInvalid: httphelper.Error(w, httphelper.JSONError{ Code: httphelper.ValidationErrorCode, Message: "Route has dependent routes", }) return default: log.Error(err.Error()) httphelper.Error(w, err) return } } w.WriteHeader(200) }
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 (api *httpAPI) NewCredential(w http.ResponseWriter, req *http.Request, params httprouter.Params) { creds := &Credential{} if err := httphelper.DecodeJSON(req, &creds); err != nil { httphelper.Error(w, err) return } if creds.Type == "azure" { oauthCreds := make([]*OAuthCredential, 0, 2) for _, resource := range []string{azure.JSONAPIResource, azure.XMLAPIResource} { token, err := azure.OAuth2Config(creds.ID, creds.Endpoint, resource).Exchange(oauth2.NoContext, creds.Secret) if err != nil { httphelper.Error(w, err) return } oauthCreds = append(oauthCreds, &OAuthCredential{ ClientID: creds.ID, AccessToken: token.AccessToken, RefreshToken: token.RefreshToken, ExpiresAt: &token.Expiry, Scope: resource, }) } creds.Secret = "" creds.OAuthCreds = oauthCreds } if err := api.Installer.SaveCredentials(creds); err != nil { if err == credentialExistsError { httphelper.ObjectExistsError(w, err.Error()) return } httphelper.Error(w, err) return } w.WriteHeader(200) }
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) }
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 } } }
// 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 } }
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) }
// 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 } }
// 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 (api *API) GetRoutes(ctx context.Context, w http.ResponseWriter, req *http.Request) { log, _ := ctxhelper.LoggerFromContext(ctx) routes, err := api.router.HTTP.List() if err != nil { log.Error(err.Error()) httphelper.Error(w, err) return } tcpRoutes, err := api.router.TCP.List() if err != nil { log.Error(err.Error()) httphelper.Error(w, err) return } routes = append(routes, tcpRoutes...) if ref := req.URL.Query().Get("parent_ref"); ref != "" { filtered := make([]*router.Route, 0) for _, route := range routes { if route.ParentRef == ref { filtered = append(filtered, route) } } routes = filtered } sort.Sort(sortedRoutes(routes)) httphelper.JSON(w, 200, routes) }
func (api *HTTPAPI) Pull(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { volumeID := ps.ByName("volume_id") pull := &volume.PullCoordinate{} if err := httphelper.DecodeJSON(r, &pull); err != nil { httphelper.Error(w, err) return } hostClient, err := api.cluster.Host(pull.HostID) if err != nil { httphelper.Error(w, err) return } haves, err := api.vman.ListHaves(volumeID) if err != nil { httphelper.Error(w, err) return } reader, err := hostClient.SendSnapshot(pull.SnapshotID, haves) if err != nil { httphelper.Error(w, err) return } snap, err := api.vman.ReceiveSnapshot(volumeID, reader) if err != nil { httphelper.Error(w, err) return } httphelper.JSON(w, 200, snap.Info()) }
func (h *jobAPI) ResourceCheck(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { var req host.ResourceCheck if err := httphelper.DecodeJSON(r, &req); err != nil { httphelper.Error(w, err) return } var conflicts []host.Port for _, p := range req.Ports { if p.Proto == "" { p.Proto = "tcp" } if !checkPort(p) { conflicts = append(conflicts, p) } } if len(conflicts) > 0 { resp := host.ResourceCheck{Ports: conflicts} detail, err := json.Marshal(resp) if err != nil { httphelper.Error(w, err) return } httphelper.JSON(w, 409, &httphelper.JSONError{ Code: httphelper.ConflictErrorCode, Message: "Conflicting resources found", Detail: detail, }) return } httphelper.JSON(w, 200, struct{}{}) }
func (a *API) ping(w http.ResponseWriter, req *http.Request, _ httprouter.Params) { logger := a.logger().New("fn", "ping") logger.Info("checking status", "host", serviceHost) if status, err := sirenia.NewClient(serviceHost + ":3306").Status(); err == nil && status.Database != nil && status.Database.ReadWrite { logger.Info("database is up, skipping scale check") } else { scaled, err := scale.CheckScale(app, controllerKey, "mongodb", a.logger()) if err != nil { httphelper.Error(w, err) return } // Cluster has yet to be scaled, return healthy if !scaled { w.WriteHeader(200) return } } 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() w.WriteHeader(200) }
func (h *jobAPI) PullBinariesAndConfig(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { log := h.host.log.New("fn", "PullBinariesAndConfig") log.Info("extracting TUF database") tufDB, err := extractTufDB(r) if err != nil { log.Error("error extracting TUF database", "err", err) httphelper.Error(w, err) return } defer os.Remove(tufDB) query := r.URL.Query() log.Info("creating local TUF store") local, err := tuf.FileLocalStore(tufDB) if err != nil { log.Error("error creating local TUF store", "err", err) httphelper.Error(w, err) return } opts := &tuf.HTTPRemoteOptions{ UserAgent: fmt.Sprintf("flynn-host/%s %s-%s pull", version.String(), runtime.GOOS, runtime.GOARCH), Retries: tuf.DefaultHTTPRetries, } log.Info("creating remote TUF store") remote, err := tuf.HTTPRemoteStore(query.Get("repository"), opts) if err != nil { log.Error("error creating remote TUF store", "err", err) httphelper.Error(w, err) return } client := tuf.NewClient(local, remote) d := downloader.New(client, query.Get("version")) log.Info("downloading binaries") paths, err := d.DownloadBinaries(query.Get("bin-dir")) if err != nil { log.Error("error downloading binaries", "err", err) httphelper.Error(w, err) return } log.Info("downloading config") configs, err := d.DownloadConfig(query.Get("config-dir")) if err != nil { log.Error("error downloading config", "err", err) httphelper.Error(w, err) return } for k, v := range configs { paths[k] = v } httphelper.JSON(w, 200, paths) }
func (h *Handler) handlePostStop(w http.ResponseWriter, req *http.Request, _ httprouter.Params) { if err := h.Peer.Stop(); err != nil { httphelper.Error(w, err) return } if err := h.Heartbeater.Close(); err != nil { httphelper.Error(w, err) return } w.WriteHeader(200) }
func (h *HTTP) Stop(w http.ResponseWriter, req *http.Request, _ httprouter.Params) { if err := h.peer.Stop(); err != nil { httphelper.Error(w, err) return } if err := h.hb.Close(); err != nil { httphelper.Error(w, err) return } w.WriteHeader(200) }
func (h *jobAPI) UpdateTags(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { var tags map[string]string if err := httphelper.DecodeJSON(r, &tags); err != nil { httphelper.Error(w, err) return } if err := h.host.UpdateTags(tags); err != nil { httphelper.Error(w, err) return } w.WriteHeader(200) }
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 (h *Handler) serveShutdown(w http.ResponseWriter, r *http.Request, params httprouter.Params) { // deregister the server before marking as shutdown as deregistration // is performed using the HTTP server if err := h.Main.Deregister(); err != nil { hh.Error(w, err) return } h.Shutdown.Store(true) targetLogIndex, err := h.Main.Close() if err != nil { hh.Error(w, err) return } hh.JSON(w, 200, targetLogIndex) }
func (h *jobAPI) PullImages(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { log := h.host.log.New("fn", "PullImages") log.Info("extracting TUF database") tufDB, err := extractTufDB(r) if err != nil { log.Error("error extracting TUF database", "err", err) httphelper.Error(w, err) return } defer os.Remove(tufDB) info := make(chan layer.PullInfo) stream := sse.NewStream(w, info, nil) go stream.Serve() log.Info("pulling images") if err := pinkerton.PullImages( tufDB, r.URL.Query().Get("repository"), r.URL.Query().Get("driver"), r.URL.Query().Get("root"), r.URL.Query().Get("version"), info, ); err != nil { log.Error("error pulling images", "err", err) stream.CloseWithError(err) return } stream.Wait() }
func (h *jobAPI) ConfigureDiscoverd(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { log := h.host.log.New("fn", "ConfigureDiscoverd") log.Info("decoding config") var config host.DiscoverdConfig if err := httphelper.DecodeJSON(r, &config); err != nil { log.Error("error decoding config", "err", err) httphelper.Error(w, err) return } log.Info("config decoded", "url", config.URL, "dns", config.DNS) h.host.statusMtx.Lock() h.host.status.Discoverd = &config h.host.statusMtx.Unlock() if config.URL != "" && config.DNS != "" { go h.host.discoverdOnce.Do(func() { log.Info("connecting to service discovery", "url", config.URL) if err := h.host.discMan.ConnectLocal(config.URL); err != nil { log.Error("error connecting to service discovery", "err", err) shutdown.Fatal(err) } }) } }