zoobzio February 17, 2025 Edit this page

Services

This guide covers how to define, register, and consume domain services in the aegis mesh.

Defining Services

Services are defined using Protocol Buffers. Aegis hosts shared proto files in proto/:

aegis/
└── proto/
    └── identity/
        ├── identity.proto
        ├── identity.pb.go
        └── identity_grpc.pb.go

Example Proto

syntax = "proto3";

package aegis.identity.v1;

option go_package = "github.com/zoobz-io/aegis/proto/identity";

service IdentityService {
  rpc ValidateSession(ValidateSessionRequest) returns (ValidateSessionResponse);
  rpc GetUser(GetUserRequest) returns (GetUserResponse);
}

message ValidateSessionRequest {
  string token = 1;
}

message ValidateSessionResponse {
  bool valid = 1;
  string user_id = 2;
  int64 expires_at = 3;
}

Generating Code

protoc --go_out=. --go_opt=paths=source_relative \
       --go-grpc_out=. --go-grpc_opt=paths=source_relative \
       proto/identity/identity.proto

Or use the Makefile:

make proto

Registering Services

Implementing the Server

package main

import (
    "context"

    "github.com/zoobz-io/aegis"
    identity "github.com/zoobz-io/aegis/proto/identity"
)

type identityServer struct {
    identity.UnimplementedIdentityServiceServer
    // Add your dependencies
    sessions SessionStore
    users    UserStore
}

func (s *identityServer) ValidateSession(ctx context.Context, req *identity.ValidateSessionRequest) (*identity.ValidateSessionResponse, error) {
    session, err := s.sessions.Get(ctx, req.Token)
    if err != nil {
        return &identity.ValidateSessionResponse{Valid: false}, nil
    }

    return &identity.ValidateSessionResponse{
        Valid:     true,
        UserId:    session.UserID,
        ExpiresAt: session.ExpiresAt.Unix(),
    }, nil
}

Registering with Node

Use WithServiceRegistration to register your service:

node, err := aegis.NewNodeBuilder().
    WithID("morpheus-1").
    WithName("Morpheus").
    WithAddress("localhost:8443").
    WithServices(aegis.ServiceInfo{Name: "identity", Version: "v1"}).
    WithServiceRegistration(func(s *grpc.Server) {
        identity.RegisterIdentityServiceServer(s, &identityServer{
            sessions: mySessionStore,
            users:    myUserStore,
        })
    }).
    WithCertDir("./certs").
    Build()

The WithServices declaration advertises the service in topology. The WithServiceRegistration callback registers the gRPC handler.

Multiple Services

Register multiple services by chaining:

WithServices(
    aegis.ServiceInfo{Name: "identity", Version: "v1"},
    aegis.ServiceInfo{Name: "audit", Version: "v1"},
).
WithServiceRegistration(func(s *grpc.Server) {
    identity.RegisterIdentityServiceServer(s, &identityServer{})
    audit.RegisterAuditServiceServer(s, &auditServer{})
})

Consuming Services

Creating a Client

pool := aegis.NewServiceClientPool(node)
defer pool.Close()

client := aegis.NewServiceClient(pool, "identity", "v1", identity.NewIdentityServiceClient)

Making Calls

// Get a client (round-robin across providers)
identityClient, err := client.Get(ctx)
if err != nil {
    return err // No providers available
}

resp, err := identityClient.ValidateSession(ctx, &identity.ValidateSessionRequest{
    Token: userToken,
})
if err != nil {
    return err // RPC failed
}

if resp.Valid {
    log.Printf("User: %s", resp.UserId)
}

Error Handling

identityClient, err := client.Get(ctx)
if errors.Is(err, aegis.ErrNoProviders) {
    // No nodes provide this service
    return fallbackBehavior()
}
if errors.Is(err, aegis.ErrNoTLSConfig) {
    // Node not configured with TLS
    return configurationError()
}

Caller Authorization

Services can authorize callers using mTLS identity:

func (s *identityServer) ValidateSession(ctx context.Context, req *identity.ValidateSessionRequest) (*identity.ValidateSessionResponse, error) {
    caller, err := aegis.CallerFromContext(ctx)
    if err != nil {
        return nil, status.Error(codes.Unauthenticated, "no caller identity")
    }

    // Check if caller is allowed
    if !s.allowedNodes[caller.NodeID] {
        return nil, status.Error(codes.PermissionDenied, "node not authorized")
    }

    // Proceed with request
    // ...
}

Allowlist Pattern

type identityServer struct {
    identity.UnimplementedIdentityServiceServer
    allowedNodes map[string]bool
}

func NewIdentityServer(allowedNodes []string) *identityServer {
    allowed := make(map[string]bool)
    for _, id := range allowedNodes {
        allowed[id] = true
    }
    return &identityServer{allowedNodes: allowed}
}

Node Type Authorization

// In the calling node
WithType(aegis.NodeType("api-gateway"))

// In the service
caller, _ := aegis.CallerFromContext(ctx)
cert := caller.Certificate
// Extract node type from certificate or lookup from topology

Service Versioning

Multiple Versions

Run multiple versions simultaneously:

WithServices(
    aegis.ServiceInfo{Name: "identity", Version: "v1"},
    aegis.ServiceInfo{Name: "identity", Version: "v2"},
)

Version Selection

Clients explicitly request a version:

// v1 client
clientV1 := aegis.NewServiceClient(pool, "identity", "v1", identityv1.NewIdentityServiceClient)

// v2 client
clientV2 := aegis.NewServiceClient(pool, "identity", "v2", identityv2.NewIdentityServiceClient)

Migration Strategy

  1. Deploy providers with both v1 and v2
  2. Update consumers to use v2
  3. Remove v1 from providers
  4. Remove v1 proto definitions

Next Steps