You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

174 lines
4.5 KiB

package consul
import (
"context"
"fmt"
"log"
"net/url"
"sync"
"time"
"github.com/hashicorp/consul/api"
"google.golang.org/grpc/attributes"
"google.golang.org/grpc/resolver"
)
const (
ConsulScheme = "consul"
)
// ConsulBuilder implements resolver.Builder
type ConsulBuilder struct {
client *api.Client
}
func NewConsulBuilder(client *api.Client) *ConsulBuilder {
return &ConsulBuilder{
client: client,
}
}
// Build creates a new resolver for the given target
func (b *ConsulBuilder) Build(target resolver.Target, cc resolver.ClientConn, opts resolver.BuildOptions) (resolver.Resolver, error) {
// Parse target: consul://consul-host:port/service-name
targetUrl := target.URL.String()
u, err := url.Parse(targetUrl)
if err != nil {
return nil, fmt.Errorf("invalid target URL: %v", err)
}
if u.Scheme != ConsulScheme {
return nil, fmt.Errorf("invalid scheme, expected %s, got %s", ConsulScheme, u.Scheme)
}
serviceName := u.Path[1:] // Remove leading '/'
if serviceName == "" {
return nil, fmt.Errorf("service name is empty")
}
r := &consulResolver{
client: b.client,
cc: cc,
serviceName: serviceName,
ctx: context.Background(),
cancel: nil,
}
r.ctx, r.cancel = context.WithCancel(r.ctx)
r.wg.Add(1)
go r.watch()
return r, nil
}
// Scheme returns the resolver scheme
func (b *ConsulBuilder) Scheme() string {
return ConsulScheme
}
// consulResolver implements resolver.Resolver
type consulResolver struct {
client *api.Client
cc resolver.ClientConn
serviceName string
ctx context.Context
cancel context.CancelFunc
wg sync.WaitGroup
}
// ResolveNow triggers a resolution immediately
func (r *consulResolver) ResolveNow(_ resolver.ResolveNowOptions) {
// Fetch service instances from Consul
services, _, err := r.client.Health().Service(r.serviceName, "", true, nil)
if err != nil {
log.Printf("ResolveNow error: %v", err)
// serviceconfig.ParseResult
r.cc.UpdateState(resolver.State{ServiceConfig: r.cc.ParseServiceConfig(`{"loadBalancingPolicy": "round_robin"}`)})
return
}
// Convert Consul services to gRPC addresses
var addrs []resolver.Address
for _, s := range services {
addr := resolver.Address{
Addr: fmt.Sprintf("%s:%d", s.Service.Address, s.Service.Port),
Attributes: attributes.New("service_id", s.Service.ID),
}
addrs = append(addrs, addr)
}
// Update gRPC client connection state
state := resolver.State{
Addresses: addrs,
ServiceConfig: r.cc.ParseServiceConfig(`{"loadBalancingPolicy": "round_robin"}`),
}
if err := r.cc.UpdateState(state); err != nil {
log.Printf("Failed to update state: %v", err)
}
}
// Close stops the resolver and cleans up
func (r *consulResolver) Close() {
r.cancel()
r.wg.Wait()
}
// watch polls Consul for service changes
func (r *consulResolver) watch() {
defer r.wg.Done()
var lastIndex uint64
for {
select {
case <-r.ctx.Done():
return
default:
services, meta, err := r.client.Health().Service(r.serviceName, "", true, &api.QueryOptions{WaitIndex: lastIndex})
if err != nil {
log.Printf("Watch error: %v", err)
time.Sleep(1 * time.Second)
continue
}
lastIndex = meta.LastIndex
var addrs []resolver.Address
for _, s := range services {
addr := resolver.Address{
Addr: fmt.Sprintf("%s:%d", s.Service.Address, s.Service.Port),
Attributes: attributes.New("service_id", s.Service.ID),
ServerName: s.Service.Service,
}
addrs = append(addrs, addr)
}
state := resolver.State{
Addresses: addrs,
ServiceConfig: r.cc.ParseServiceConfig(`{"loadBalancingPolicy": "round_robin"}`),
}
if err := r.cc.UpdateState(state); err != nil {
log.Printf("Failed to update state: %v", err)
}
time.Sleep(1 * time.Second) // Avoid tight loop
}
}
}
// func init() {
// resolver.Register(&consulBuilder{})
// }
// func main() {
// // Dial gRPC service using Consul resolver
// conn, err := grpc.NewClient(
// "consul://localhost:8500/greeter",
// grpc.WithInsecure(), // Use WithTransportCredentials for TLS in production
// grpc.WithDefaultServiceConfig(`{"loadBalancingPolicy": "round_robin"}`),
// )
// if err != nil {
// log.Fatalf("Failed to dial: %v", err)
// }
// defer conn.Close()
// client := pb.NewGreeterClient(conn)
// resp, err := client.SayHello(context.Background(), &pb.HelloRequest{Name: "World"})
// if err != nil {
// log.Fatalf("SayHello failed: %v", err)
// }
// log.Printf("Response: %s", resp.Message)
// }