package quartermaster import ( "context" "fmt" "time" "go.uber.org/zap" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" "github.com/guildhouse-co/kedge/internal/config" ) // Client is a gRPC client for the Quartermaster governance and notary services. type Client struct { cfg config.QuartermasterConfig conn *grpc.ClientConn log *zap.SugaredLogger // TODO: Add typed gRPC stub clients when proto generation is wired up. // governance quartermasterv1.GovernanceServiceClient // notary quartermasterv1.QuartermasterNotaryClient } // NewClient creates a new Quartermaster gRPC client. func NewClient(cfg config.QuartermasterConfig) (*Client, error) { log := zap.L().Sugar().Named("quartermaster") // TODO: Use mTLS with SPIFFE SVID for production. opts := []grpc.DialOption{ grpc.WithTransportCredentials(insecure.NewCredentials()), } conn, err := grpc.NewClient(cfg.Endpoint, opts...) if err != nil { return nil, fmt.Errorf("failed to connect to quartermaster at %s: %w", cfg.Endpoint, err) } log.Infof("connected to quartermaster at %s", cfg.Endpoint) return &Client{ cfg: cfg, conn: conn, log: log, }, nil } // Close closes the gRPC connection. func (c *Client) Close() error { if c.conn != nil { return c.conn.Close() } return nil } // SubmitSessionTransit submits a SessionTransitArtifact to Quartermaster for notarization. // This records overlay session crossings between clusters. func (c *Client) SubmitSessionTransit(ctx context.Context, artifact *SessionTransitArtifact) error { artifact.Timestamp = time.Now().UTC() // Serialize to canonical JSON (RFC 8785) for hashing. canonical, err := artifact.CanonicalBytes() if err != nil { return fmt.Errorf("failed to serialize session transit artifact: %w", err) } c.log.Infow("submitting session transit", "session_id", artifact.SessionID, "source", artifact.SourceCluster, "dest", artifact.DestCluster, "mode", artifact.GrantedMode, "bytes", len(canonical), ) // TODO: Call governance.CreateIntent + notary.CreateAnchor via gRPC stubs. // For Phase 1, the anchor buffer flush is async — operations are not blocked. _ = canonical return nil } // SubmitNetworkMutation submits a NetworkMutationArtifact to Quartermaster for notarization. // This records underlay infrastructure config changes. func (c *Client) SubmitNetworkMutation(ctx context.Context, artifact *NetworkMutationArtifact) error { artifact.Timestamp = time.Now().UTC() canonical, err := artifact.CanonicalBytes() if err != nil { return fmt.Errorf("failed to serialize network mutation artifact: %w", err) } c.log.Infow("submitting network mutation", "mutation_id", artifact.MutationID, "device", artifact.Device, "operation", artifact.Operation, "bytes", len(canonical), ) // TODO: Call governance.CreateIntent + notary.CreateAnchor via gRPC stubs. _ = canonical return nil }