package consul import ( "fmt" "net/http" "strconv" "time" "git.pbiernat.io/egommerce/go-api-pkg/config" consul "github.com/hashicorp/consul/api" "github.com/hashicorp/consul/connect" ) type Service struct { Name string Address string appID string domain string pathPrefix string tls bool port int ttl time.Duration client *consul.Client agent *consul.Agent connect *connect.Service kv *consul.KV hcTicker *time.Ticker ttlTicker *time.Ticker } var ErrServiceUnavailable = fmt.Errorf("Service is unavailable") func NewService(servAddr, id, name, useDomainOverIp, addr, domain, pathPrefix string, appPort int) (*Service, error) { s := new(Service) s.Name = name s.Address = addr s.appID = id s.domain = domain s.pathPrefix = pathPrefix s.tls = true // FIXME add arg s.port = appPort s.ttl = time.Second * 10 if useDomainOverIp == "true" { // FIXME types... s.Address = domain } client, err := consul.NewClient(newClientConfig(servAddr)) if err != nil { return nil, err } s.client = client s.agent = client.Agent() s.kv = client.KV() return s, nil } func newClientConfig(serverAddr string) *consul.Config { conf := consul.DefaultConfig() conf.Address = serverAddr return conf } func (s *Service) GetID() string { return fmt.Sprintf("%s:%s", s.Name, s.appID) } func (s *Service) GetFullAddr() string { isTLS := s.port == 443 proto := "http" if isTLS { proto = "https" } return fmt.Sprintf("%s://%s:%d/", proto, s.Address, s.port) } func (s *Service) Register() error { def := &consul.AgentServiceRegistration{ ID: s.GetID(), // Kind: consul.ServiceKindConnectProxy, Name: s.Name, Address: s.Address, Port: s.port, Tags: s.getTags(), // Connect: &consul.AgentServiceConnect{Native: true}, // Proxy: &consul.AgentServiceConnectProxyConfig{ // DestinationServiceName: s.Name, // }, Check: &consul.AgentServiceCheck{ TTL: s.ttl.String(), Status: "passing", DeregisterCriticalServiceAfter: "10s", }, } if err := s.agent.ServiceRegister(def); err != nil { return err } return nil } func (s *Service) Unregister() error { // s.ttlTicker.Stop() // s.hcTicker.Stop() s.client.Catalog().Deregister(&consul.CatalogDeregistration{ Address: s.Address, ServiceID: s.GetID(), }, nil) return s.agent.ServiceDeregister(s.GetID()) } func (s *Service) RegisterHealthChecks() { go func() { // startup register t := time.NewTicker(time.Second) for range t.C { if ok, _ := s.healthCheck(); ok { t.Stop() } } }() go func() { // TTL t := time.NewTicker(s.ttl) for range t.C { if _, err := s.healthCheck(); err != nil { // fmt.Printf("HealthCheck endpoint not available (%s)#: %v\n", s.GetFullAddr(), err) t.Stop() } } }() } func (s *Service) Connect() (*connect.Service, error) { // l := hclog.New(&hclog.LoggerOptions{ // Name: "consul-registry", // Level: hclog.Trace, // }) svc, err := connect.NewService(s.Name, s.client) s.connect = svc cnf := svc.ServerTLSConfig() fmt.Printf("CONNECT SERVER:: %s CONFIG:: %v\n", s.Name, cnf) for k, c := range cnf.Certificates { fmt.Printf("CONNECT CERT %d: %v", k, c) } return svc, err } func (s *Service) KV() *consul.KV { return s.kv } func (s *Service) healthCheck() (bool, error) { alive := func() bool { client := &http.Client{} healthUrl := fmt.Sprintf("%s%s?name=%s", s.GetFullAddr(), "health", s.Name) fmt.Printf("HealthCheck URL: %s%s?name=%s", s.GetFullAddr(), "health", s.Name) req, err := http.NewRequest(http.MethodGet, healthUrl, nil) if err != nil { return false } req.Header.Set("User-Agent", "service/internal") resp, err := client.Do(req) if err != nil { return false } defer resp.Body.Close() var body []byte resp.Body.Read(body) return resp.StatusCode == http.StatusOK }() if alive { if err := s.agent.PassTTL("service:"+s.GetID(), "OK"); err != nil { fmt.Printf("Failed to pass TTL: %v", err) return false, err } return true, nil } if err := s.agent.FailTTL("service:"+s.GetID(), ErrServiceUnavailable.Error()); err != nil { return false, err } return false, ErrServiceUnavailable } func (s *Service) getTags() []string { tags := []string{ "traefik.enable=true", // "traefik.http.middlewares.auth_" + s.Name + ".forwardauth.trustForwardHeader=true", // "traefik.http.middlewares.auth_" + s.Name + ".forwardauth.authRequestHeaders=Cookie", // "traefik.http.middlewares.auth_" + s.Name + ".forwardauth.authResponseHeaders=Set-Cookie, Server", "traefik.http.middlewares.auth_" + s.Name + ".plugin.auth.handlerURL=" + config.GetEnv("AUTH_HANDLER_URL", "https://identity.service.ego.io/api/v1/traefik"), "traefik.http.middlewares.stripprefix_" + s.Name + ".stripprefix.prefixes=" + s.pathPrefix, "traefik.http.middlewares.requestid_" + s.Name + ".plugin.requestid.headerName=X-Request-ID", // "treafik.http.middlewares.retryif_" + s.Name + ".plugin.retryif.attempts=3", // "treafik.http.middlewares.retryif_" + s.Name + ".plugin.retryif.statusCode=503", "traefik.http.routers." + s.Name + ".rule=PathPrefix(`" + s.pathPrefix + "`)", "traefik.http.routers." + s.Name + ".rule=Host(`" + s.domain + "`)", "traefik.http.routers." + s.Name + ".entryPoints=https", "traefik.http.routers." + s.Name + ".tls=true", "traefik.http.routers." + s.Name + ".service=" + s.Name, // "traefik.http.routers." + s.Name + ".middlewares=auth_" + s.Name + ",stripprefix_" + s.Name, "traefik.http.routers." + s.Name + ".middlewares=auth_" + s.Name + ",stripprefix_" + s.Name + ",requestid_" + s.Name + "", "traefik.http.services." + s.Name + ".loadbalancer.server.scheme=https", "traefik.http.services." + s.Name + ".loadbalancer.server.port=" + strconv.Itoa(s.port), "traefik.http.services." + s.Name + ".loadbalancer.passhostheader=true", "traefik.http.services." + s.Name + ".loadbalancer.healthcheck.interval=1s", "traefik.http.services." + s.Name + ".loadbalancer.healthcheck.timeout=1s", "traefik.http.services." + s.Name + ".loadbalancer.healthcheck.path=/health", "traefik.tls.certificates.certfile=certs/client.cert", "traefik.tls.certificates.keyfile=certs/client.key", } return tags }