14 Commits

Author SHA1 Message Date
52914d50b0 Pass mode, backend-tls, and tls cert/key through route add
The --mode flag was defined but never wired through to the RPC.
Add tls_cert and tls_key fields to AddProxyRouteRequest so L7
routes can be created via mcp route add.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 20:44:44 -07:00
bb4bee51ba Add mono-repo consideration to ARCHITECTURE_V2.md open questions
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 20:40:32 -07:00
4ac8a6d60b Add ARCHITECTURE_V2.md for multi-node master/agent topology
Documents the planned v2 architecture: mcp-master on straylight
coordinates deployments across worker (rift) and edge (svc) nodes.
Includes edge routing flow, agent RPCs, migration plan, and
operational issues from v1 that motivate the redesign.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 20:37:24 -07:00
d8f45ca520 Merge explicit ports with route-allocated ports during deploy
Previously, explicit port mappings from the service definition were
ignored when routes were present. Now both are included, allowing
services to have stable external port bindings alongside dynamic
route-allocated ports.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 19:28:40 -07:00
95f86157b4 Add mcp route command for managing mc-proxy routes
New top-level command with list, add, remove subcommands. Supports
-n/--node to target a specific node. Adds AddProxyRoute and
RemoveProxyRoute RPCs to the agent. Moves route listing from
mcp node routes to mcp route list.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 19:10:11 -07:00
93e26d3789 Add mcp dns and mcp node routes commands
mcp dns queries MCNS via an agent to list all zones and DNS records.
mcp node routes queries mc-proxy on each node for listener/route status,
matching the mcproxyctl status output format.

New agent RPCs: ListDNSRecords, ListProxyRoutes.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 18:51:53 -07:00
3d2edb7c26 Fall back to podman logs when journalctl is inaccessible
Probe journalctl with -n 0 before committing to it. When the journal
is not readable (e.g. rootless podman without user journal storage),
fall back to podman logs instead of streaming the permission error.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 17:54:14 -07:00
bf02935716 Add agent version to mcp node list
Thread the linker-injected version string into the Agent struct and
return it in the NodeStatus RPC. The CLI now dials each node and
displays the agent version alongside name and address.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 17:49:49 -07:00
c4f0d7be8e Fix mcp logs permission error for rootless podman journald driver
Rootless podman writes container logs to the user journal, but
journalctl without --user only reads the system journal. Add --user
when the agent is running as a non-root user.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 16:46:01 -07:00
4d900eafd1 Derive flake version from git rev instead of hardcoding
Eliminates the manual version bump in flake.nix on each release.
Uses self.shortRev (or dirtyShortRev) since self.gitDescribe is not
yet available in this Nix version. Makefile builds still get the full
git describe output via ldflags.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 22:48:44 -07:00
38f9070c24 Add top-level mcp edit command
Shortcut for mcp service edit — opens the service definition in $EDITOR.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 22:44:19 -07:00
67d0ab1d9d Bump flake.nix version to 0.7.5
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 19:40:29 -07:00
7383b370f0 Fix mcp ps showing registry version instead of runtime, error on unknown component
mcp ps now uses the actual container image and version from the runtime
instead of the registry, which could be stale after a failed deploy.

Deploy now returns an error when the component filter matches nothing
instead of silently succeeding with zero results.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 19:13:02 -07:00
4c847e6de9 Fix extraneous blank lines in mcp logs output
Skip empty lines from the scanner that result from double newlines
(application slog trailing newline + container runtime newline).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 18:22:38 -07:00
22 changed files with 2205 additions and 78 deletions

502
ARCHITECTURE_V2.md Normal file
View File

@@ -0,0 +1,502 @@
# MCP v2 -- Multi-Node Control Plane
## Overview
MCP v2 introduces multi-node orchestration with a master/agent topology.
The CLI no longer dials agents directly. A dedicated **mcp-master** daemon
coordinates deployments across nodes, handles cross-node concerns (edge
routing, certificate provisioning, DNS), and serves as the single control
point for the platform.
### Motivation
v1 deployed successfully on a single node (rift) but exposed operational
pain points as services needed public-facing routes through svc:
- **Manual edge routing**: Exposing mcq.metacircular.net required hand-editing
mc-proxy's TOML config on svc, provisioning a TLS cert manually, updating
the SQLite database when the config and database diverged, and debugging
silent failures. Every redeployment risked breaking the public route.
- **Dynamic port instability**: The route system assigns ephemeral host ports
that change on every deploy. svc's mc-proxy pointed at a specific port
(e.g., `100.95.252.120:48080`), which went stale after redeployment.
Container ports are also localhost-only under rootless podman, requiring
explicit Tailscale IP bindings for external access.
- **$PORT env override conflict**: The mcdsl config loader overrides
`listen_addr` from `$PORT` when routes are present. This meant containers
ignored their configured port and listened on the route-allocated one
instead, breaking explicit port mappings that expected the config port.
- **Cert chain issues**: mc-proxy requires full certificate chains (leaf +
intermediates). Certs provisioned outside the standard metacrypt flow
were leaf-only and caused silent TLS handshake failures (`client_bytes=7
backend_bytes=0` with no error logged).
- **mc-proxy database divergence**: mc-proxy persists routes in SQLite.
Routes added via the admin API override the TOML config. Editing the TOML
alone had no effect until the database was manually updated -- a failure
mode that took hours to diagnose.
- **No cross-node coordination**: The v1 CLI talks directly to individual
agents. There is no mechanism for one agent to tell another "set up a
route for this service." Every cross-node operation was manual.
v2 addresses all of these by making the master the single coordination
point for deployments, with agents handling local concerns (containers,
mc-proxy routes, cert provisioning) on instruction from the master.
### What Changes from v1
| Concern | v1 | v2 |
|---------|----|----|
| CLI target | CLI dials agents directly | CLI dials the master |
| Node awareness | CLI routes by `node` field in service defs | Master owns the node registry |
| Service definitions | Live on operator workstation | Pushed to master, which distributes to agents |
| Edge routing | Manual mc-proxy config on svc | Master coordinates agent-to-agent setup |
| Cert provisioning | Agent provisions for local mc-proxy only | Any agent can provision certs (edge included) |
| DNS registration | Agent registers records on deploy | Master coordinates DNS across zones |
### What Stays the Same
The agent's core responsibilities are unchanged: it manages containers via
podman, stores its local registry in SQLite, monitors for drift, and alerts
the operator. The agent gains new RPCs for edge routing but does not become
aware of other nodes -- the master handles all cross-node coordination.
---
## Topology
```
Operator workstation (vade)
┌──────────────────────────┐
│ mcp (CLI) │
│ │
│ gRPC ───────────────────┼─── overlay ───┐
└──────────────────────────┘ │
Master node (straylight)
┌──────────────────────────────────────────────────────┐
│ mcp-master │
│ ├── node registry (all nodes, roles, addresses) │
│ ├── service definitions (pushed from CLI) │
│ └── deployment coordinator │
│ │
│ mcp-agent │
│ ├── mcns container │
│ ├── metacrypt container │
│ ├── mcr container │
│ └── mc-proxy (straylight) │
└──────────┬──────────────────────────┬────────────────┘
│ │
overlay overlay
│ │
▼ ▼
Worker node (rift) Edge node (svc)
┌─────────────────────┐ ┌─────────────────────────┐
│ mcp-agent │ │ mcp-agent │
│ ├── exo │ │ ├── mc-proxy (svc) │
│ ├── mcq │ │ └── (edge routes only) │
│ ├── mcdoc │ │ │
│ ├── sgard │ │ Edge routes: │
│ ├── kls │ │ mcq.metacircular.net │
│ └── mc-proxy │ │ mcdoc.metacircular.net │
│ (rift) │ │ exo.metacircular.net │
└─────────────────────┘ │ sgard.metacircular.net │
└─────────────────────────┘
```
### Node Roles
| Role | Purpose | Nodes |
|------|---------|-------|
| **master** | Runs mcp-master + mcp-agent. Hosts core infrastructure (mcns, metacrypt, mcr). Single coordination point. | straylight |
| **worker** | Runs mcp-agent. Hosts application services. | rift |
| **edge** | Runs mcp-agent. Terminates public TLS, forwards to internal services. No application containers. | svc |
Every node runs an mcp-agent. The master node also runs mcp-master.
The master's local agent manages the infrastructure services (mcns,
metacrypt, mcr) the same way rift's agent manages application services.
### mc-proxy Mesh
Each node runs its own mc-proxy instance. They form a routing mesh:
```
mc-proxy (straylight)
├── :443 L7 routes for metacrypt-web, mcr-web
├── :8443 L4 passthrough for metacrypt-api, mcr-api
└── :9443 L4 passthrough for gRPC services
mc-proxy (rift)
├── :443 L7 routes for internal .svc.mcp hostnames
└── :8443 L4/L7 routes for internal APIs
mc-proxy (svc)
└── :443 L7 termination for public hostnames
→ forwards to internal .svc.mcp endpoints
```
---
## mcp-master
The master is a new binary that coordinates cross-node operations. It is
**not** a replacement for the agent -- it sits above agents and orchestrates
them.
### Responsibilities
1. **Accept CLI commands** via gRPC (deploy, undeploy, status, sync).
2. **Route deployments** to the correct agent based on the service
definition's `node` field.
3. **Detect public hostnames** in service definitions and coordinate edge
routing with the edge node's agent.
4. **Validate public hostnames** against a configured allowlist of domains
(e.g., `metacircular.net`, `wntrmute.net`).
5. **Resolve edge nodes** by checking DNS CNAME records to determine which
node handles public traffic for a given hostname.
6. **Coordinate undeploy** across nodes: tear down the service on the
worker, then clean up edge routes on the edge node.
### What the Master Does NOT Do
- Store container state (agents own their registries).
- Manage container lifecycle directly (agents do this).
- Run containers (the co-located agent does).
- Replace the agent on any node.
### Master Configuration
```toml
[server]
grpc_addr = "100.x.x.x:9555" # master listens on overlay
tls_cert = "/srv/mcp-master/certs/cert.pem"
tls_key = "/srv/mcp-master/certs/key.pem"
[mcias]
server_url = "https://mcias.metacircular.net:8443"
service_name = "mcp-master"
# Allowed public domains. Hostnames in service definitions must fall
# under one of these suffixes.
[edge]
allowed_domains = ["metacircular.net", "wntrmute.net"]
# Node registry. The master knows about all nodes.
[[nodes]]
name = "straylight"
address = "100.x.x.x:9444"
role = "master"
[[nodes]]
name = "rift"
address = "100.95.252.120:9444"
role = "worker"
[[nodes]]
name = "svc"
address = "100.x.x.x:9444"
role = "edge"
```
---
## Edge Routing
The core v2 feature: when a service declares a public hostname, the
master automatically provisions the edge route.
### Service Definition
Public hostnames are declared in the route's `hostname` field. The
master distinguishes public from internal hostnames by checking whether
they fall under a `.svc.mcp.` subdomain:
- `mcq.svc.mcp.metacircular.net` → internal (handled by local mc-proxy)
- `mcq.metacircular.net` → public (requires edge routing)
```toml
name = "mcq"
node = "rift"
active = true
[[components]]
name = "mcq"
image = "mcr.svc.mcp.metacircular.net:8443/mcq:v0.4.0"
volumes = ["/srv/mcq:/srv/mcq"]
cmd = ["server", "--config", "/srv/mcq/mcq.toml"]
# Internal route: handled by rift's mc-proxy.
[[components.routes]]
name = "internal"
port = 8443
mode = "l7"
# Public route: master detects this and sets up edge routing on svc.
[[components.routes]]
name = "public"
port = 8443
mode = "l7"
hostname = "mcq.metacircular.net"
```
### Deploy Flow with Edge Routing
When the master receives `Deploy(mcq)`:
1. **Route to worker**: Master sends `Deploy` RPC to rift's agent with
the full service spec. Rift's agent deploys the container and
registers mc-proxy routes for all hostnames (both internal and public)
on its local mc-proxy.
2. **Detect public hostnames**: Master inspects the service spec for
hostnames that are not `.svc.mcp.` subdomains.
3. **Validate domains**: Master checks that `mcq.metacircular.net` falls
under an allowed domain (`metacircular.net` ✓).
4. **Resolve edge node**: Master performs a DNS lookup for
`mcq.metacircular.net`. If it's a CNAME to `svc.metacircular.net`,
the master resolves `svc.metacircular.net` to identify the edge node
as `svc`. If DNS is not yet configured (no CNAME), the master uses
the default edge node from config.
5. **Set up edge route**: Master sends an `SetupEdgeRoute` RPC to svc's
agent:
```
SetupEdgeRoute(
hostname: "mcq.metacircular.net"
backend_hostname: "mcq.svc.mcp.metacircular.net"
backend_port: 8443
)
```
6. **Svc agent provisions**: On receiving `SetupEdgeRoute`, svc's agent:
a. Provisions a TLS certificate from Metacrypt for
`mcq.metacircular.net`.
b. Registers an L7 route in its local mc-proxy:
`mcq.metacircular.net:443 → mcq.svc.mcp.metacircular.net:8443`
with the provisioned cert.
7. **Master records the edge route** in its own registry for undeploy
cleanup.
### Undeploy Flow
When the master receives `Undeploy(mcq)`:
1. **Look up edge routes**: Master checks its registry for edge routes
associated with mcq.
2. **Remove edge route**: Master sends `RemoveEdgeRoute(mcq.metacircular.net)`
to svc's agent. Svc's agent removes the mc-proxy route and cleans up
the cert.
3. **Undeploy on worker**: Master sends `Undeploy` RPC to rift's agent.
Rift's agent tears down the container, routes, DNS, and certs as in v1.
### Edge Node DNS Resolution
The master determines which edge node handles a public hostname by
checking DNS:
1. Look up `mcq.metacircular.net` → CNAME `svc.metacircular.net`
2. Look up `svc.metacircular.net` → IP address
3. Match the IP against known edge nodes
If no CNAME exists yet (operator hasn't set it up), the master warns but
does not fail. The operator sets up DNS manually at Hurricane Electric.
The master can provide a `mcp dns check` command that verifies all public
hostnames resolve correctly.
---
## Agent Changes for v2
### New RPCs
```protobuf
// Edge routing -- called by master on edge nodes.
rpc SetupEdgeRoute(SetupEdgeRouteRequest) returns (SetupEdgeRouteResponse);
rpc RemoveEdgeRoute(RemoveEdgeRouteRequest) returns (RemoveEdgeRouteResponse);
rpc ListEdgeRoutes(ListEdgeRoutesRequest) returns (ListEdgeRoutesResponse);
message SetupEdgeRouteRequest {
string hostname = 1; // public hostname (e.g. "mcq.metacircular.net")
string backend_hostname = 2; // internal hostname (e.g. "mcq.svc.mcp.metacircular.net")
int32 backend_port = 3; // port on the worker's mc-proxy (e.g. 8443)
}
message SetupEdgeRouteResponse {}
message RemoveEdgeRouteRequest {
string hostname = 1;
}
message RemoveEdgeRouteResponse {}
message ListEdgeRoutesRequest {}
message ListEdgeRoutesResponse {
repeated EdgeRoute routes = 1;
}
message EdgeRoute {
string hostname = 1;
string backend_hostname = 2;
int32 backend_port = 3;
string cert_serial = 4;
string cert_expires = 5;
}
```
### SetupEdgeRoute Implementation
When the agent receives `SetupEdgeRoute`:
1. **Resolve backend address**: The agent resolves `backend_hostname` to
an IP address (the worker node's overlay IP). It uses the port from
the request to form the backend address (e.g., `100.95.252.120:8443`).
2. **Provision TLS cert**: The agent calls Metacrypt's CA API to issue a
certificate for the public hostname. The cert and key are written to
the mc-proxy cert directory.
3. **Register mc-proxy route**: The agent adds an L7 route to its local
mc-proxy:
- Listener: `:443`
- Hostname: `mcq.metacircular.net`
- Backend: `100.95.252.120:8443`
- Mode: `l7`
- TLS cert/key: the provisioned cert
- Backend TLS: `true` (worker's mc-proxy serves TLS)
4. **Record the edge route** in the agent's local registry for listing
and cleanup.
### Cert Provisioning on All Agents
All agents need Metacrypt configuration to provision certs:
```toml
[metacrypt]
server_url = "https://metacrypt.svc.mcp.metacircular.net:8443"
ca_cert = "/srv/mcp/certs/metacircular-ca.pem"
mount = "pki"
issuer = "infra"
token_path = "/srv/mcp/metacrypt-token"
```
The svc agent provisions certs for public hostnames. The rift agent
provisions certs for internal hostnames. Both use the same Metacrypt API.
---
## CLI Changes for v2
The CLI's `[[nodes]]` config is replaced by a single master address:
```toml
[master]
address = "100.x.x.x:9555"
[mcias]
server_url = "https://mcias.metacircular.net:8443"
service_name = "mcp"
[auth]
token_path = "/home/kyle/.config/mcp/token"
[services]
dir = "/home/kyle/.config/mcp/services"
```
Commands that currently iterate over nodes (`mcp ps`, `mcp list`,
`mcp node list`) instead query the master, which aggregates from all
agents.
Service definition files remain on the operator's workstation. The CLI
pushes them to the master on `mcp deploy` and `mcp sync`.
---
## Migration Plan
### Phase 1: Deploy mcp-agent on svc
svc currently has no mcp-agent. Install and configure one:
- Create `mcp` user on svc (Debian: `install-agent.sh`)
- Install mcp-agent binary
- Configure with Metacrypt and mc-proxy socket access
- Verify with `mcp node list` (svc shows up)
### Phase 2: Add edge routing RPCs to agents
Implement `SetupEdgeRoute`, `RemoveEdgeRoute`, `ListEdgeRoutes` on the
agent. Test by calling them directly from the CLI before the master exists.
### Phase 3: Build mcp-master
Start with the core coordination loop:
1. Accept `Deploy` from CLI
2. Forward to the correct agent
3. Detect public hostnames
4. Call `SetupEdgeRoute` on the edge agent
### Phase 4: Provision straylight
New node (straylight) takes over as master and hosts core infrastructure:
1. Deploy mcp-agent on straylight
2. Migrate mcns, metacrypt, mcr from rift to straylight
3. Deploy mcp-master on straylight
4. Update CLI config to point at master
### Phase 5: Cut over
- Update DNS to point `*.svc.mcp.metacircular.net` at straylight
- Update service definitions to use new node assignments
- Verify all services via `mcp ps` and public endpoint tests
---
## Open Questions
1. **Master HA**: mcp-master is a single point of failure. For v2, this
is acceptable (operator can SSH to agents directly if master is down).
v3 could add master replication or make agents self-sufficient for
local operations when the master is unreachable.
2. **Service placement**: v2 still requires explicit `node` assignment
in service definitions. Automatic placement based on resource
availability is a future concern.
3. **Cert renewal on edge**: Edge certs have a 90-day TTL. The edge
agent needs a renewal loop (similar to the existing `renewWindow`
check in `EnsureCert`) or the master needs to periodically re-check
edge routes.
4. **mc-proxy database vs config**: mc-proxy persists routes in SQLite,
which can diverge from the TOML config. The agent should be the sole
manager of mc-proxy routes via the gRPC admin API, not the TOML file.
This avoids the stale-database problem encountered during v1
operations on svc.
5. **straylight hardware**: What hardware is straylight? Does it run
NixOS or Debian? Does it use rootless podman like rift?
6. **Mono-repo for core infrastructure**: The current layout has each
service as a separate git repo under `~/src/metacircular/`. A
mono-repo for core infrastructure (mcp, mcp-master, mcns, metacrypt,
mcr, mc-proxy, mcdsl) would simplify coordinated changes (e.g., a
proto change that touches agent + CLI + mc-proxy client), eliminate
the `uses_mcdsl` build flag / vendoring, enable a single CI pipeline,
and allow atomic platform versioning (one tag per release). Non-core
application services (exo, mcq, mcdoc, sgard, kls, mcat) would
remain as separate repos with independent release cadences. This is
a large migration best tackled after straylight is running and the
master exists, when the build/deploy pipeline is already being
reorganized.

View File

@@ -38,7 +38,7 @@ func main() {
if err != nil {
return fmt.Errorf("load config: %w", err)
}
return agent.Run(cfg)
return agent.Run(cfg, version)
},
})

87
cmd/mcp/dns.go Normal file
View File

@@ -0,0 +1,87 @@
package main
import (
"context"
"fmt"
"os"
"time"
mcpv1 "git.wntrmute.dev/mc/mcp/gen/mcp/v1"
"git.wntrmute.dev/mc/mcp/internal/config"
"github.com/spf13/cobra"
)
func dnsCmd() *cobra.Command {
return &cobra.Command{
Use: "dns",
Short: "List all DNS zones and records from MCNS",
RunE: runDNS,
}
}
func runDNS(_ *cobra.Command, _ []string) error {
cfg, err := config.LoadCLIConfig(cfgPath)
if err != nil {
return fmt.Errorf("load config: %w", err)
}
// DNS is centralized — query the first reachable agent.
resp, nodeName, err := queryDNS(cfg)
if err != nil {
return err
}
if len(resp.GetZones()) == 0 {
fmt.Println("no DNS zones configured")
return nil
}
_ = nodeName
for i, zone := range resp.GetZones() {
if i > 0 {
fmt.Println()
}
fmt.Printf("ZONE: %s\n", zone.GetName())
if len(zone.GetRecords()) == 0 {
fmt.Println(" (no records)")
continue
}
w := newTable()
_, _ = fmt.Fprintln(w, " NAME\tTYPE\tVALUE\tTTL")
for _, r := range zone.GetRecords() {
_, _ = fmt.Fprintf(w, " %s\t%s\t%s\t%d\n",
r.GetName(), r.GetType(), r.GetValue(), r.GetTtl())
}
_ = w.Flush()
}
return nil
}
// queryDNS tries each configured agent and returns the first successful
// DNS listing. DNS is centralized so any agent with MCNS configured works.
func queryDNS(cfg *config.CLIConfig) (*mcpv1.ListDNSRecordsResponse, string, error) {
for _, node := range cfg.Nodes {
client, conn, err := dialAgent(node.Address, cfg)
if err != nil {
_, _ = fmt.Fprintf(os.Stderr, "warning: %s: %v\n", node.Name, err)
continue
}
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
resp, err := client.ListDNSRecords(ctx, &mcpv1.ListDNSRecordsRequest{})
cancel()
_ = conn.Close()
if err != nil {
_, _ = fmt.Fprintf(os.Stderr, "warning: %s: list DNS: %v\n", node.Name, err)
continue
}
return resp, node.Name, nil
}
return nil, "", fmt.Errorf("no reachable agent with DNS configured")
}

12
cmd/mcp/edit.go Normal file
View File

@@ -0,0 +1,12 @@
package main
import "github.com/spf13/cobra"
func editCmd() *cobra.Command {
return &cobra.Command{
Use: "edit <service>",
Short: "Open service definition in $EDITOR",
Args: cobra.ExactArgs(1),
RunE: runServiceEdit,
}
}

View File

@@ -51,6 +51,9 @@ func main() {
root.AddCommand(nodeCmd())
root.AddCommand(purgeCmd())
root.AddCommand(logsCmd())
root.AddCommand(editCmd())
root.AddCommand(dnsCmd())
root.AddCommand(routeCmd())
if err := root.Execute(); err != nil {
log.Fatal(err)

View File

@@ -1,12 +1,15 @@
package main
import (
"context"
"fmt"
"os"
"text/tabwriter"
"time"
toml "github.com/pelletier/go-toml/v2"
mcpv1 "git.wntrmute.dev/mc/mcp/gen/mcp/v1"
"git.wntrmute.dev/mc/mcp/internal/config"
"github.com/spf13/cobra"
)
@@ -48,13 +51,35 @@ func runNodeList(_ *cobra.Command, _ []string) error {
}
w := tabwriter.NewWriter(os.Stdout, 0, 4, 2, ' ', 0)
_, _ = fmt.Fprintln(w, "NAME\tADDRESS")
_, _ = fmt.Fprintln(w, "NAME\tADDRESS\tVERSION")
for _, n := range cfg.Nodes {
_, _ = fmt.Fprintf(w, "%s\t%s\n", n.Name, n.Address)
ver := queryAgentVersion(cfg, n.Address)
_, _ = fmt.Fprintf(w, "%s\t%s\t%s\n", n.Name, n.Address, ver)
}
return w.Flush()
}
// queryAgentVersion dials the agent and returns its version, or an error indicator.
func queryAgentVersion(cfg *config.CLIConfig, address string) string {
client, conn, err := dialAgent(address, cfg)
if err != nil {
return "error"
}
defer func() { _ = conn.Close() }()
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
resp, err := client.NodeStatus(ctx, &mcpv1.NodeStatusRequest{})
if err != nil {
return "error"
}
if resp.AgentVersion == "" {
return "unknown"
}
return resp.AgentVersion
}
func runNodeAdd(_ *cobra.Command, args []string) error {
cfg, err := config.LoadCLIConfig(cfgPath)
if err != nil {

225
cmd/mcp/route.go Normal file
View File

@@ -0,0 +1,225 @@
package main
import (
"context"
"fmt"
"os"
"time"
"github.com/spf13/cobra"
mcpv1 "git.wntrmute.dev/mc/mcp/gen/mcp/v1"
"git.wntrmute.dev/mc/mcp/internal/config"
)
func routeCmd() *cobra.Command {
var nodeName string
cmd := &cobra.Command{
Use: "route",
Short: "Manage mc-proxy routes",
}
list := &cobra.Command{
Use: "list",
Short: "List mc-proxy routes",
RunE: func(_ *cobra.Command, _ []string) error {
return runRouteList(nodeName)
},
}
var (
routeMode string
backendTLS bool
tlsCert string
tlsKey string
)
add := &cobra.Command{
Use: "add <listener> <hostname> <backend>",
Short: "Add a route to mc-proxy",
Long: "Add a route. Example: mcp route add -n rift :443 mcq.svc.mcp.metacircular.net 127.0.0.1:48080 --mode l7 --tls-cert /srv/mc-proxy/certs/mcq.pem --tls-key /srv/mc-proxy/certs/mcq.key",
Args: cobra.ExactArgs(3),
RunE: func(_ *cobra.Command, args []string) error {
return runRouteAdd(nodeName, args, routeMode, backendTLS, tlsCert, tlsKey)
},
}
add.Flags().StringVar(&routeMode, "mode", "l4", "route mode (l4 or l7)")
add.Flags().BoolVar(&backendTLS, "backend-tls", false, "re-encrypt traffic to backend")
add.Flags().StringVar(&tlsCert, "tls-cert", "", "path to TLS cert on the node (required for l7)")
add.Flags().StringVar(&tlsKey, "tls-key", "", "path to TLS key on the node (required for l7)")
remove := &cobra.Command{
Use: "remove <listener> <hostname>",
Short: "Remove a route from mc-proxy",
Long: "Remove a route. Example: mcp route remove -n rift :443 mcq.metacircular.net",
Args: cobra.ExactArgs(2),
RunE: func(_ *cobra.Command, args []string) error {
return runRouteRemove(nodeName, args)
},
}
cmd.PersistentFlags().StringVarP(&nodeName, "node", "n", "", "target node (required)")
cmd.AddCommand(list, add, remove)
return cmd
}
func runRouteList(nodeName string) error {
if nodeName == "" {
return runRouteListAll()
}
cfg, err := config.LoadCLIConfig(cfgPath)
if err != nil {
return fmt.Errorf("load config: %w", err)
}
address, err := findNodeAddress(cfg, nodeName)
if err != nil {
return err
}
client, conn, err := dialAgent(address, cfg)
if err != nil {
return fmt.Errorf("dial agent: %w", err)
}
defer func() { _ = conn.Close() }()
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
resp, err := client.ListProxyRoutes(ctx, &mcpv1.ListProxyRoutesRequest{})
if err != nil {
return fmt.Errorf("list routes: %w", err)
}
printRoutes(nodeName, resp)
return nil
}
func runRouteListAll() error {
first := true
return forEachNode(func(node config.NodeConfig, client mcpv1.McpAgentServiceClient) error {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
resp, err := client.ListProxyRoutes(ctx, &mcpv1.ListProxyRoutesRequest{})
if err != nil {
_, _ = fmt.Fprintf(os.Stderr, "warning: %s: list routes: %v\n", node.Name, err)
return nil
}
if !first {
fmt.Println()
}
first = false
printRoutes(node.Name, resp)
return nil
})
}
func printRoutes(nodeName string, resp *mcpv1.ListProxyRoutesResponse) {
fmt.Printf("NODE: %s\n", nodeName)
fmt.Printf("mc-proxy %s\n", resp.GetVersion())
if resp.GetStartedAt() != nil {
uptime := time.Since(resp.GetStartedAt().AsTime()).Truncate(time.Second)
fmt.Printf("uptime: %s\n", uptime)
}
fmt.Printf("connections: %d\n", resp.GetTotalConnections())
fmt.Println()
for _, ls := range resp.GetListeners() {
fmt.Printf(" %s routes=%d active=%d\n",
ls.GetAddr(), ls.GetRouteCount(), ls.GetActiveConnections())
for _, r := range ls.GetRoutes() {
mode := r.GetMode()
if mode == "" {
mode = "l4"
}
extra := ""
if r.GetBackendTls() {
extra = " (re-encrypt)"
}
fmt.Printf(" %s %s → %s%s\n", mode, r.GetHostname(), r.GetBackend(), extra)
}
}
}
func runRouteAdd(nodeName string, args []string, mode string, backendTLS bool, tlsCert, tlsKey string) error {
if nodeName == "" {
return fmt.Errorf("--node is required")
}
cfg, err := config.LoadCLIConfig(cfgPath)
if err != nil {
return fmt.Errorf("load config: %w", err)
}
address, err := findNodeAddress(cfg, nodeName)
if err != nil {
return err
}
client, conn, err := dialAgent(address, cfg)
if err != nil {
return fmt.Errorf("dial agent: %w", err)
}
defer func() { _ = conn.Close() }()
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
_, err = client.AddProxyRoute(ctx, &mcpv1.AddProxyRouteRequest{
ListenerAddr: args[0],
Hostname: args[1],
Backend: args[2],
Mode: mode,
BackendTls: backendTLS,
TlsCert: tlsCert,
TlsKey: tlsKey,
})
if err != nil {
return fmt.Errorf("add route: %w", err)
}
fmt.Printf("Added route: %s %s → %s on %s (%s)\n", mode, args[1], args[2], args[0], nodeName)
return nil
}
func runRouteRemove(nodeName string, args []string) error {
if nodeName == "" {
return fmt.Errorf("--node is required")
}
cfg, err := config.LoadCLIConfig(cfgPath)
if err != nil {
return fmt.Errorf("load config: %w", err)
}
address, err := findNodeAddress(cfg, nodeName)
if err != nil {
return err
}
client, conn, err := dialAgent(address, cfg)
if err != nil {
return fmt.Errorf("dial agent: %w", err)
}
defer func() { _ = conn.Close() }()
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
_, err = client.RemoveProxyRoute(ctx, &mcpv1.RemoveProxyRouteRequest{
ListenerAddr: args[0],
Hostname: args[1],
})
if err != nil {
return fmt.Errorf("remove route: %w", err)
}
fmt.Printf("Removed route: %s from %s (%s)\n", args[1], args[0], nodeName)
return nil
}

View File

@@ -10,7 +10,7 @@
let
system = "x86_64-linux";
pkgs = nixpkgs.legacyPackages.${system};
version = "0.6.0";
version = pkgs.lib.removePrefix "v" (self.gitDescribe or self.shortRev or self.dirtyShortRev or "unknown");
in
{
packages.${system} = {

View File

@@ -1926,6 +1926,7 @@ type NodeStatusResponse struct {
MemoryFreeBytes uint64 `protobuf:"varint,9,opt,name=memory_free_bytes,json=memoryFreeBytes,proto3" json:"memory_free_bytes,omitempty"`
CpuUsagePercent float64 `protobuf:"fixed64,10,opt,name=cpu_usage_percent,json=cpuUsagePercent,proto3" json:"cpu_usage_percent,omitempty"`
UptimeSince *timestamppb.Timestamp `protobuf:"bytes,11,opt,name=uptime_since,json=uptimeSince,proto3" json:"uptime_since,omitempty"`
AgentVersion string `protobuf:"bytes,12,opt,name=agent_version,json=agentVersion,proto3" json:"agent_version,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
@@ -2037,6 +2038,13 @@ func (x *NodeStatusResponse) GetUptimeSince() *timestamppb.Timestamp {
return nil
}
func (x *NodeStatusResponse) GetAgentVersion() string {
if x != nil {
return x.AgentVersion
}
return ""
}
type PurgeRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
// Service name (empty = all services).
@@ -2352,6 +2360,670 @@ func (x *LogsResponse) GetData() []byte {
return nil
}
type ListDNSRecordsRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *ListDNSRecordsRequest) Reset() {
*x = ListDNSRecordsRequest{}
mi := &file_proto_mcp_v1_mcp_proto_msgTypes[41]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *ListDNSRecordsRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ListDNSRecordsRequest) ProtoMessage() {}
func (x *ListDNSRecordsRequest) ProtoReflect() protoreflect.Message {
mi := &file_proto_mcp_v1_mcp_proto_msgTypes[41]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ListDNSRecordsRequest.ProtoReflect.Descriptor instead.
func (*ListDNSRecordsRequest) Descriptor() ([]byte, []int) {
return file_proto_mcp_v1_mcp_proto_rawDescGZIP(), []int{41}
}
type DNSZone struct {
state protoimpl.MessageState `protogen:"open.v1"`
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
Records []*DNSRecord `protobuf:"bytes,2,rep,name=records,proto3" json:"records,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *DNSZone) Reset() {
*x = DNSZone{}
mi := &file_proto_mcp_v1_mcp_proto_msgTypes[42]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *DNSZone) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*DNSZone) ProtoMessage() {}
func (x *DNSZone) ProtoReflect() protoreflect.Message {
mi := &file_proto_mcp_v1_mcp_proto_msgTypes[42]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use DNSZone.ProtoReflect.Descriptor instead.
func (*DNSZone) Descriptor() ([]byte, []int) {
return file_proto_mcp_v1_mcp_proto_rawDescGZIP(), []int{42}
}
func (x *DNSZone) GetName() string {
if x != nil {
return x.Name
}
return ""
}
func (x *DNSZone) GetRecords() []*DNSRecord {
if x != nil {
return x.Records
}
return nil
}
type DNSRecord struct {
state protoimpl.MessageState `protogen:"open.v1"`
Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"`
Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"`
Type string `protobuf:"bytes,3,opt,name=type,proto3" json:"type,omitempty"`
Value string `protobuf:"bytes,4,opt,name=value,proto3" json:"value,omitempty"`
Ttl int32 `protobuf:"varint,5,opt,name=ttl,proto3" json:"ttl,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *DNSRecord) Reset() {
*x = DNSRecord{}
mi := &file_proto_mcp_v1_mcp_proto_msgTypes[43]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *DNSRecord) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*DNSRecord) ProtoMessage() {}
func (x *DNSRecord) ProtoReflect() protoreflect.Message {
mi := &file_proto_mcp_v1_mcp_proto_msgTypes[43]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use DNSRecord.ProtoReflect.Descriptor instead.
func (*DNSRecord) Descriptor() ([]byte, []int) {
return file_proto_mcp_v1_mcp_proto_rawDescGZIP(), []int{43}
}
func (x *DNSRecord) GetId() int64 {
if x != nil {
return x.Id
}
return 0
}
func (x *DNSRecord) GetName() string {
if x != nil {
return x.Name
}
return ""
}
func (x *DNSRecord) GetType() string {
if x != nil {
return x.Type
}
return ""
}
func (x *DNSRecord) GetValue() string {
if x != nil {
return x.Value
}
return ""
}
func (x *DNSRecord) GetTtl() int32 {
if x != nil {
return x.Ttl
}
return 0
}
type ListDNSRecordsResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
Zones []*DNSZone `protobuf:"bytes,1,rep,name=zones,proto3" json:"zones,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *ListDNSRecordsResponse) Reset() {
*x = ListDNSRecordsResponse{}
mi := &file_proto_mcp_v1_mcp_proto_msgTypes[44]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *ListDNSRecordsResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ListDNSRecordsResponse) ProtoMessage() {}
func (x *ListDNSRecordsResponse) ProtoReflect() protoreflect.Message {
mi := &file_proto_mcp_v1_mcp_proto_msgTypes[44]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ListDNSRecordsResponse.ProtoReflect.Descriptor instead.
func (*ListDNSRecordsResponse) Descriptor() ([]byte, []int) {
return file_proto_mcp_v1_mcp_proto_rawDescGZIP(), []int{44}
}
func (x *ListDNSRecordsResponse) GetZones() []*DNSZone {
if x != nil {
return x.Zones
}
return nil
}
type ListProxyRoutesRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *ListProxyRoutesRequest) Reset() {
*x = ListProxyRoutesRequest{}
mi := &file_proto_mcp_v1_mcp_proto_msgTypes[45]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *ListProxyRoutesRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ListProxyRoutesRequest) ProtoMessage() {}
func (x *ListProxyRoutesRequest) ProtoReflect() protoreflect.Message {
mi := &file_proto_mcp_v1_mcp_proto_msgTypes[45]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ListProxyRoutesRequest.ProtoReflect.Descriptor instead.
func (*ListProxyRoutesRequest) Descriptor() ([]byte, []int) {
return file_proto_mcp_v1_mcp_proto_rawDescGZIP(), []int{45}
}
type ProxyRouteInfo struct {
state protoimpl.MessageState `protogen:"open.v1"`
Hostname string `protobuf:"bytes,1,opt,name=hostname,proto3" json:"hostname,omitempty"`
Backend string `protobuf:"bytes,2,opt,name=backend,proto3" json:"backend,omitempty"`
Mode string `protobuf:"bytes,3,opt,name=mode,proto3" json:"mode,omitempty"`
BackendTls bool `protobuf:"varint,4,opt,name=backend_tls,json=backendTls,proto3" json:"backend_tls,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *ProxyRouteInfo) Reset() {
*x = ProxyRouteInfo{}
mi := &file_proto_mcp_v1_mcp_proto_msgTypes[46]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *ProxyRouteInfo) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ProxyRouteInfo) ProtoMessage() {}
func (x *ProxyRouteInfo) ProtoReflect() protoreflect.Message {
mi := &file_proto_mcp_v1_mcp_proto_msgTypes[46]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ProxyRouteInfo.ProtoReflect.Descriptor instead.
func (*ProxyRouteInfo) Descriptor() ([]byte, []int) {
return file_proto_mcp_v1_mcp_proto_rawDescGZIP(), []int{46}
}
func (x *ProxyRouteInfo) GetHostname() string {
if x != nil {
return x.Hostname
}
return ""
}
func (x *ProxyRouteInfo) GetBackend() string {
if x != nil {
return x.Backend
}
return ""
}
func (x *ProxyRouteInfo) GetMode() string {
if x != nil {
return x.Mode
}
return ""
}
func (x *ProxyRouteInfo) GetBackendTls() bool {
if x != nil {
return x.BackendTls
}
return false
}
type ProxyListenerInfo struct {
state protoimpl.MessageState `protogen:"open.v1"`
Addr string `protobuf:"bytes,1,opt,name=addr,proto3" json:"addr,omitempty"`
RouteCount int32 `protobuf:"varint,2,opt,name=route_count,json=routeCount,proto3" json:"route_count,omitempty"`
ActiveConnections int64 `protobuf:"varint,3,opt,name=active_connections,json=activeConnections,proto3" json:"active_connections,omitempty"`
Routes []*ProxyRouteInfo `protobuf:"bytes,4,rep,name=routes,proto3" json:"routes,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *ProxyListenerInfo) Reset() {
*x = ProxyListenerInfo{}
mi := &file_proto_mcp_v1_mcp_proto_msgTypes[47]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *ProxyListenerInfo) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ProxyListenerInfo) ProtoMessage() {}
func (x *ProxyListenerInfo) ProtoReflect() protoreflect.Message {
mi := &file_proto_mcp_v1_mcp_proto_msgTypes[47]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ProxyListenerInfo.ProtoReflect.Descriptor instead.
func (*ProxyListenerInfo) Descriptor() ([]byte, []int) {
return file_proto_mcp_v1_mcp_proto_rawDescGZIP(), []int{47}
}
func (x *ProxyListenerInfo) GetAddr() string {
if x != nil {
return x.Addr
}
return ""
}
func (x *ProxyListenerInfo) GetRouteCount() int32 {
if x != nil {
return x.RouteCount
}
return 0
}
func (x *ProxyListenerInfo) GetActiveConnections() int64 {
if x != nil {
return x.ActiveConnections
}
return 0
}
func (x *ProxyListenerInfo) GetRoutes() []*ProxyRouteInfo {
if x != nil {
return x.Routes
}
return nil
}
type ListProxyRoutesResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
Version string `protobuf:"bytes,1,opt,name=version,proto3" json:"version,omitempty"`
TotalConnections int64 `protobuf:"varint,2,opt,name=total_connections,json=totalConnections,proto3" json:"total_connections,omitempty"`
StartedAt *timestamppb.Timestamp `protobuf:"bytes,3,opt,name=started_at,json=startedAt,proto3" json:"started_at,omitempty"`
Listeners []*ProxyListenerInfo `protobuf:"bytes,4,rep,name=listeners,proto3" json:"listeners,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *ListProxyRoutesResponse) Reset() {
*x = ListProxyRoutesResponse{}
mi := &file_proto_mcp_v1_mcp_proto_msgTypes[48]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *ListProxyRoutesResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ListProxyRoutesResponse) ProtoMessage() {}
func (x *ListProxyRoutesResponse) ProtoReflect() protoreflect.Message {
mi := &file_proto_mcp_v1_mcp_proto_msgTypes[48]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ListProxyRoutesResponse.ProtoReflect.Descriptor instead.
func (*ListProxyRoutesResponse) Descriptor() ([]byte, []int) {
return file_proto_mcp_v1_mcp_proto_rawDescGZIP(), []int{48}
}
func (x *ListProxyRoutesResponse) GetVersion() string {
if x != nil {
return x.Version
}
return ""
}
func (x *ListProxyRoutesResponse) GetTotalConnections() int64 {
if x != nil {
return x.TotalConnections
}
return 0
}
func (x *ListProxyRoutesResponse) GetStartedAt() *timestamppb.Timestamp {
if x != nil {
return x.StartedAt
}
return nil
}
func (x *ListProxyRoutesResponse) GetListeners() []*ProxyListenerInfo {
if x != nil {
return x.Listeners
}
return nil
}
type AddProxyRouteRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
ListenerAddr string `protobuf:"bytes,1,opt,name=listener_addr,json=listenerAddr,proto3" json:"listener_addr,omitempty"` // e.g. ":443"
Hostname string `protobuf:"bytes,2,opt,name=hostname,proto3" json:"hostname,omitempty"`
Backend string `protobuf:"bytes,3,opt,name=backend,proto3" json:"backend,omitempty"`
Mode string `protobuf:"bytes,4,opt,name=mode,proto3" json:"mode,omitempty"` // "l4" or "l7"
BackendTls bool `protobuf:"varint,5,opt,name=backend_tls,json=backendTls,proto3" json:"backend_tls,omitempty"`
TlsCert string `protobuf:"bytes,6,opt,name=tls_cert,json=tlsCert,proto3" json:"tls_cert,omitempty"` // path to TLS cert (required for l7)
TlsKey string `protobuf:"bytes,7,opt,name=tls_key,json=tlsKey,proto3" json:"tls_key,omitempty"` // path to TLS key (required for l7)
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *AddProxyRouteRequest) Reset() {
*x = AddProxyRouteRequest{}
mi := &file_proto_mcp_v1_mcp_proto_msgTypes[49]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *AddProxyRouteRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*AddProxyRouteRequest) ProtoMessage() {}
func (x *AddProxyRouteRequest) ProtoReflect() protoreflect.Message {
mi := &file_proto_mcp_v1_mcp_proto_msgTypes[49]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use AddProxyRouteRequest.ProtoReflect.Descriptor instead.
func (*AddProxyRouteRequest) Descriptor() ([]byte, []int) {
return file_proto_mcp_v1_mcp_proto_rawDescGZIP(), []int{49}
}
func (x *AddProxyRouteRequest) GetListenerAddr() string {
if x != nil {
return x.ListenerAddr
}
return ""
}
func (x *AddProxyRouteRequest) GetHostname() string {
if x != nil {
return x.Hostname
}
return ""
}
func (x *AddProxyRouteRequest) GetBackend() string {
if x != nil {
return x.Backend
}
return ""
}
func (x *AddProxyRouteRequest) GetMode() string {
if x != nil {
return x.Mode
}
return ""
}
func (x *AddProxyRouteRequest) GetBackendTls() bool {
if x != nil {
return x.BackendTls
}
return false
}
func (x *AddProxyRouteRequest) GetTlsCert() string {
if x != nil {
return x.TlsCert
}
return ""
}
func (x *AddProxyRouteRequest) GetTlsKey() string {
if x != nil {
return x.TlsKey
}
return ""
}
type AddProxyRouteResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *AddProxyRouteResponse) Reset() {
*x = AddProxyRouteResponse{}
mi := &file_proto_mcp_v1_mcp_proto_msgTypes[50]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *AddProxyRouteResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*AddProxyRouteResponse) ProtoMessage() {}
func (x *AddProxyRouteResponse) ProtoReflect() protoreflect.Message {
mi := &file_proto_mcp_v1_mcp_proto_msgTypes[50]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use AddProxyRouteResponse.ProtoReflect.Descriptor instead.
func (*AddProxyRouteResponse) Descriptor() ([]byte, []int) {
return file_proto_mcp_v1_mcp_proto_rawDescGZIP(), []int{50}
}
type RemoveProxyRouteRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
ListenerAddr string `protobuf:"bytes,1,opt,name=listener_addr,json=listenerAddr,proto3" json:"listener_addr,omitempty"` // e.g. ":443"
Hostname string `protobuf:"bytes,2,opt,name=hostname,proto3" json:"hostname,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *RemoveProxyRouteRequest) Reset() {
*x = RemoveProxyRouteRequest{}
mi := &file_proto_mcp_v1_mcp_proto_msgTypes[51]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *RemoveProxyRouteRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*RemoveProxyRouteRequest) ProtoMessage() {}
func (x *RemoveProxyRouteRequest) ProtoReflect() protoreflect.Message {
mi := &file_proto_mcp_v1_mcp_proto_msgTypes[51]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use RemoveProxyRouteRequest.ProtoReflect.Descriptor instead.
func (*RemoveProxyRouteRequest) Descriptor() ([]byte, []int) {
return file_proto_mcp_v1_mcp_proto_rawDescGZIP(), []int{51}
}
func (x *RemoveProxyRouteRequest) GetListenerAddr() string {
if x != nil {
return x.ListenerAddr
}
return ""
}
func (x *RemoveProxyRouteRequest) GetHostname() string {
if x != nil {
return x.Hostname
}
return ""
}
type RemoveProxyRouteResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *RemoveProxyRouteResponse) Reset() {
*x = RemoveProxyRouteResponse{}
mi := &file_proto_mcp_v1_mcp_proto_msgTypes[52]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *RemoveProxyRouteResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*RemoveProxyRouteResponse) ProtoMessage() {}
func (x *RemoveProxyRouteResponse) ProtoReflect() protoreflect.Message {
mi := &file_proto_mcp_v1_mcp_proto_msgTypes[52]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use RemoveProxyRouteResponse.ProtoReflect.Descriptor instead.
func (*RemoveProxyRouteResponse) Descriptor() ([]byte, []int) {
return file_proto_mcp_v1_mcp_proto_rawDescGZIP(), []int{52}
}
var File_proto_mcp_v1_mcp_proto protoreflect.FileDescriptor
const file_proto_mcp_v1_mcp_proto_rawDesc = "" +
@@ -2474,7 +3146,7 @@ const file_proto_mcp_v1_mcp_proto_rawDesc = "" +
"\acontent\x18\x01 \x01(\fR\acontent\x12\x12\n" +
"\x04mode\x18\x02 \x01(\rR\x04mode\x12\x14\n" +
"\x05error\x18\x03 \x01(\tR\x05error\"\x13\n" +
"\x11NodeStatusRequest\"\xd9\x03\n" +
"\x11NodeStatusRequest\"\xfe\x03\n" +
"\x12NodeStatusResponse\x12\x1b\n" +
"\tnode_name\x18\x01 \x01(\tR\bnodeName\x12\x18\n" +
"\aruntime\x18\x02 \x01(\tR\aruntime\x12'\n" +
@@ -2487,7 +3159,8 @@ const file_proto_mcp_v1_mcp_proto_rawDesc = "" +
"\x11memory_free_bytes\x18\t \x01(\x04R\x0fmemoryFreeBytes\x12*\n" +
"\x11cpu_usage_percent\x18\n" +
" \x01(\x01R\x0fcpuUsagePercent\x12=\n" +
"\fuptime_since\x18\v \x01(\v2\x1a.google.protobuf.TimestampR\vuptimeSince\"\x8e\x01\n" +
"\fuptime_since\x18\v \x01(\v2\x1a.google.protobuf.TimestampR\vuptimeSince\x12#\n" +
"\ragent_version\x18\f \x01(\tR\fagentVersion\"\x8e\x01\n" +
"\fPurgeRequest\x12\x18\n" +
"\aservice\x18\x01 \x01(\tR\aservice\x12\x1c\n" +
"\tcomponent\x18\x02 \x01(\tR\tcomponent\x12\x17\n" +
@@ -2510,7 +3183,52 @@ const file_proto_mcp_v1_mcp_proto_rawDesc = "" +
"timestamps\x12\x14\n" +
"\x05since\x18\x06 \x01(\tR\x05since\"\"\n" +
"\fLogsResponse\x12\x12\n" +
"\x04data\x18\x01 \x01(\fR\x04data2\xc8\b\n" +
"\x04data\x18\x01 \x01(\fR\x04data\"\x17\n" +
"\x15ListDNSRecordsRequest\"J\n" +
"\aDNSZone\x12\x12\n" +
"\x04name\x18\x01 \x01(\tR\x04name\x12+\n" +
"\arecords\x18\x02 \x03(\v2\x11.mcp.v1.DNSRecordR\arecords\"k\n" +
"\tDNSRecord\x12\x0e\n" +
"\x02id\x18\x01 \x01(\x03R\x02id\x12\x12\n" +
"\x04name\x18\x02 \x01(\tR\x04name\x12\x12\n" +
"\x04type\x18\x03 \x01(\tR\x04type\x12\x14\n" +
"\x05value\x18\x04 \x01(\tR\x05value\x12\x10\n" +
"\x03ttl\x18\x05 \x01(\x05R\x03ttl\"?\n" +
"\x16ListDNSRecordsResponse\x12%\n" +
"\x05zones\x18\x01 \x03(\v2\x0f.mcp.v1.DNSZoneR\x05zones\"\x18\n" +
"\x16ListProxyRoutesRequest\"{\n" +
"\x0eProxyRouteInfo\x12\x1a\n" +
"\bhostname\x18\x01 \x01(\tR\bhostname\x12\x18\n" +
"\abackend\x18\x02 \x01(\tR\abackend\x12\x12\n" +
"\x04mode\x18\x03 \x01(\tR\x04mode\x12\x1f\n" +
"\vbackend_tls\x18\x04 \x01(\bR\n" +
"backendTls\"\xa7\x01\n" +
"\x11ProxyListenerInfo\x12\x12\n" +
"\x04addr\x18\x01 \x01(\tR\x04addr\x12\x1f\n" +
"\vroute_count\x18\x02 \x01(\x05R\n" +
"routeCount\x12-\n" +
"\x12active_connections\x18\x03 \x01(\x03R\x11activeConnections\x12.\n" +
"\x06routes\x18\x04 \x03(\v2\x16.mcp.v1.ProxyRouteInfoR\x06routes\"\xd4\x01\n" +
"\x17ListProxyRoutesResponse\x12\x18\n" +
"\aversion\x18\x01 \x01(\tR\aversion\x12+\n" +
"\x11total_connections\x18\x02 \x01(\x03R\x10totalConnections\x129\n" +
"\n" +
"started_at\x18\x03 \x01(\v2\x1a.google.protobuf.TimestampR\tstartedAt\x127\n" +
"\tlisteners\x18\x04 \x03(\v2\x19.mcp.v1.ProxyListenerInfoR\tlisteners\"\xda\x01\n" +
"\x14AddProxyRouteRequest\x12#\n" +
"\rlistener_addr\x18\x01 \x01(\tR\flistenerAddr\x12\x1a\n" +
"\bhostname\x18\x02 \x01(\tR\bhostname\x12\x18\n" +
"\abackend\x18\x03 \x01(\tR\abackend\x12\x12\n" +
"\x04mode\x18\x04 \x01(\tR\x04mode\x12\x1f\n" +
"\vbackend_tls\x18\x05 \x01(\bR\n" +
"backendTls\x12\x19\n" +
"\btls_cert\x18\x06 \x01(\tR\atlsCert\x12\x17\n" +
"\atls_key\x18\a \x01(\tR\x06tlsKey\"\x17\n" +
"\x15AddProxyRouteResponse\"Z\n" +
"\x17RemoveProxyRouteRequest\x12#\n" +
"\rlistener_addr\x18\x01 \x01(\tR\flistenerAddr\x12\x1a\n" +
"\bhostname\x18\x02 \x01(\tR\bhostname\"\x1a\n" +
"\x18RemoveProxyRouteResponse2\x92\v\n" +
"\x0fMcpAgentService\x127\n" +
"\x06Deploy\x12\x15.mcp.v1.DeployRequest\x1a\x16.mcp.v1.DeployResponse\x12R\n" +
"\x0fUndeployService\x12\x1e.mcp.v1.UndeployServiceRequest\x1a\x1f.mcp.v1.UndeployServiceResponse\x12F\n" +
@@ -2526,7 +3244,11 @@ const file_proto_mcp_v1_mcp_proto_rawDesc = "" +
"\bPushFile\x12\x17.mcp.v1.PushFileRequest\x1a\x18.mcp.v1.PushFileResponse\x12=\n" +
"\bPullFile\x12\x17.mcp.v1.PullFileRequest\x1a\x18.mcp.v1.PullFileResponse\x12C\n" +
"\n" +
"NodeStatus\x12\x19.mcp.v1.NodeStatusRequest\x1a\x1a.mcp.v1.NodeStatusResponse\x123\n" +
"NodeStatus\x12\x19.mcp.v1.NodeStatusRequest\x1a\x1a.mcp.v1.NodeStatusResponse\x12O\n" +
"\x0eListDNSRecords\x12\x1d.mcp.v1.ListDNSRecordsRequest\x1a\x1e.mcp.v1.ListDNSRecordsResponse\x12R\n" +
"\x0fListProxyRoutes\x12\x1e.mcp.v1.ListProxyRoutesRequest\x1a\x1f.mcp.v1.ListProxyRoutesResponse\x12L\n" +
"\rAddProxyRoute\x12\x1c.mcp.v1.AddProxyRouteRequest\x1a\x1d.mcp.v1.AddProxyRouteResponse\x12U\n" +
"\x10RemoveProxyRoute\x12\x1f.mcp.v1.RemoveProxyRouteRequest\x1a .mcp.v1.RemoveProxyRouteResponse\x123\n" +
"\x04Logs\x12\x13.mcp.v1.LogsRequest\x1a\x14.mcp.v1.LogsResponse0\x01B*Z(git.wntrmute.dev/mc/mcp/gen/mcp/v1;mcpv1b\x06proto3"
var (
@@ -2541,7 +3263,7 @@ func file_proto_mcp_v1_mcp_proto_rawDescGZIP() []byte {
return file_proto_mcp_v1_mcp_proto_rawDescData
}
var file_proto_mcp_v1_mcp_proto_msgTypes = make([]protoimpl.MessageInfo, 41)
var file_proto_mcp_v1_mcp_proto_msgTypes = make([]protoimpl.MessageInfo, 53)
var file_proto_mcp_v1_mcp_proto_goTypes = []any{
(*RouteSpec)(nil), // 0: mcp.v1.RouteSpec
(*ComponentSpec)(nil), // 1: mcp.v1.ComponentSpec
@@ -2584,7 +3306,19 @@ var file_proto_mcp_v1_mcp_proto_goTypes = []any{
(*PurgeResult)(nil), // 38: mcp.v1.PurgeResult
(*LogsRequest)(nil), // 39: mcp.v1.LogsRequest
(*LogsResponse)(nil), // 40: mcp.v1.LogsResponse
(*timestamppb.Timestamp)(nil), // 41: google.protobuf.Timestamp
(*ListDNSRecordsRequest)(nil), // 41: mcp.v1.ListDNSRecordsRequest
(*DNSZone)(nil), // 42: mcp.v1.DNSZone
(*DNSRecord)(nil), // 43: mcp.v1.DNSRecord
(*ListDNSRecordsResponse)(nil), // 44: mcp.v1.ListDNSRecordsResponse
(*ListProxyRoutesRequest)(nil), // 45: mcp.v1.ListProxyRoutesRequest
(*ProxyRouteInfo)(nil), // 46: mcp.v1.ProxyRouteInfo
(*ProxyListenerInfo)(nil), // 47: mcp.v1.ProxyListenerInfo
(*ListProxyRoutesResponse)(nil), // 48: mcp.v1.ListProxyRoutesResponse
(*AddProxyRouteRequest)(nil), // 49: mcp.v1.AddProxyRouteRequest
(*AddProxyRouteResponse)(nil), // 50: mcp.v1.AddProxyRouteResponse
(*RemoveProxyRouteRequest)(nil), // 51: mcp.v1.RemoveProxyRouteRequest
(*RemoveProxyRouteResponse)(nil), // 52: mcp.v1.RemoveProxyRouteResponse
(*timestamppb.Timestamp)(nil), // 53: google.protobuf.Timestamp
}
var file_proto_mcp_v1_mcp_proto_depIdxs = []int32{
0, // 0: mcp.v1.ComponentSpec.routes:type_name -> mcp.v1.RouteSpec
@@ -2598,51 +3332,64 @@ var file_proto_mcp_v1_mcp_proto_depIdxs = []int32{
2, // 8: mcp.v1.SyncDesiredStateRequest.services:type_name -> mcp.v1.ServiceSpec
16, // 9: mcp.v1.SyncDesiredStateResponse.results:type_name -> mcp.v1.ServiceSyncResult
19, // 10: mcp.v1.ServiceInfo.components:type_name -> mcp.v1.ComponentInfo
41, // 11: mcp.v1.ComponentInfo.started:type_name -> google.protobuf.Timestamp
53, // 11: mcp.v1.ComponentInfo.started:type_name -> google.protobuf.Timestamp
18, // 12: mcp.v1.ListServicesResponse.services:type_name -> mcp.v1.ServiceInfo
41, // 13: mcp.v1.EventInfo.timestamp:type_name -> google.protobuf.Timestamp
53, // 13: mcp.v1.EventInfo.timestamp:type_name -> google.protobuf.Timestamp
18, // 14: mcp.v1.GetServiceStatusResponse.services:type_name -> mcp.v1.ServiceInfo
22, // 15: mcp.v1.GetServiceStatusResponse.drift:type_name -> mcp.v1.DriftInfo
23, // 16: mcp.v1.GetServiceStatusResponse.recent_events:type_name -> mcp.v1.EventInfo
18, // 17: mcp.v1.LiveCheckResponse.services:type_name -> mcp.v1.ServiceInfo
28, // 18: mcp.v1.AdoptContainersResponse.results:type_name -> mcp.v1.AdoptResult
41, // 19: mcp.v1.NodeStatusResponse.uptime_since:type_name -> google.protobuf.Timestamp
53, // 19: mcp.v1.NodeStatusResponse.uptime_since:type_name -> google.protobuf.Timestamp
38, // 20: mcp.v1.PurgeResponse.results:type_name -> mcp.v1.PurgeResult
3, // 21: mcp.v1.McpAgentService.Deploy:input_type -> mcp.v1.DeployRequest
12, // 22: mcp.v1.McpAgentService.UndeployService:input_type -> mcp.v1.UndeployServiceRequest
6, // 23: mcp.v1.McpAgentService.StopService:input_type -> mcp.v1.StopServiceRequest
8, // 24: mcp.v1.McpAgentService.StartService:input_type -> mcp.v1.StartServiceRequest
10, // 25: mcp.v1.McpAgentService.RestartService:input_type -> mcp.v1.RestartServiceRequest
14, // 26: mcp.v1.McpAgentService.SyncDesiredState:input_type -> mcp.v1.SyncDesiredStateRequest
17, // 27: mcp.v1.McpAgentService.ListServices:input_type -> mcp.v1.ListServicesRequest
21, // 28: mcp.v1.McpAgentService.GetServiceStatus:input_type -> mcp.v1.GetServiceStatusRequest
25, // 29: mcp.v1.McpAgentService.LiveCheck:input_type -> mcp.v1.LiveCheckRequest
27, // 30: mcp.v1.McpAgentService.AdoptContainers:input_type -> mcp.v1.AdoptContainersRequest
36, // 31: mcp.v1.McpAgentService.PurgeComponent:input_type -> mcp.v1.PurgeRequest
30, // 32: mcp.v1.McpAgentService.PushFile:input_type -> mcp.v1.PushFileRequest
32, // 33: mcp.v1.McpAgentService.PullFile:input_type -> mcp.v1.PullFileRequest
34, // 34: mcp.v1.McpAgentService.NodeStatus:input_type -> mcp.v1.NodeStatusRequest
39, // 35: mcp.v1.McpAgentService.Logs:input_type -> mcp.v1.LogsRequest
4, // 36: mcp.v1.McpAgentService.Deploy:output_type -> mcp.v1.DeployResponse
13, // 37: mcp.v1.McpAgentService.UndeployService:output_type -> mcp.v1.UndeployServiceResponse
7, // 38: mcp.v1.McpAgentService.StopService:output_type -> mcp.v1.StopServiceResponse
9, // 39: mcp.v1.McpAgentService.StartService:output_type -> mcp.v1.StartServiceResponse
11, // 40: mcp.v1.McpAgentService.RestartService:output_type -> mcp.v1.RestartServiceResponse
15, // 41: mcp.v1.McpAgentService.SyncDesiredState:output_type -> mcp.v1.SyncDesiredStateResponse
20, // 42: mcp.v1.McpAgentService.ListServices:output_type -> mcp.v1.ListServicesResponse
24, // 43: mcp.v1.McpAgentService.GetServiceStatus:output_type -> mcp.v1.GetServiceStatusResponse
26, // 44: mcp.v1.McpAgentService.LiveCheck:output_type -> mcp.v1.LiveCheckResponse
29, // 45: mcp.v1.McpAgentService.AdoptContainers:output_type -> mcp.v1.AdoptContainersResponse
37, // 46: mcp.v1.McpAgentService.PurgeComponent:output_type -> mcp.v1.PurgeResponse
31, // 47: mcp.v1.McpAgentService.PushFile:output_type -> mcp.v1.PushFileResponse
33, // 48: mcp.v1.McpAgentService.PullFile:output_type -> mcp.v1.PullFileResponse
35, // 49: mcp.v1.McpAgentService.NodeStatus:output_type -> mcp.v1.NodeStatusResponse
40, // 50: mcp.v1.McpAgentService.Logs:output_type -> mcp.v1.LogsResponse
36, // [36:51] is the sub-list for method output_type
21, // [21:36] is the sub-list for method input_type
21, // [21:21] is the sub-list for extension type_name
21, // [21:21] is the sub-list for extension extendee
0, // [0:21] is the sub-list for field type_name
43, // 21: mcp.v1.DNSZone.records:type_name -> mcp.v1.DNSRecord
42, // 22: mcp.v1.ListDNSRecordsResponse.zones:type_name -> mcp.v1.DNSZone
46, // 23: mcp.v1.ProxyListenerInfo.routes:type_name -> mcp.v1.ProxyRouteInfo
53, // 24: mcp.v1.ListProxyRoutesResponse.started_at:type_name -> google.protobuf.Timestamp
47, // 25: mcp.v1.ListProxyRoutesResponse.listeners:type_name -> mcp.v1.ProxyListenerInfo
3, // 26: mcp.v1.McpAgentService.Deploy:input_type -> mcp.v1.DeployRequest
12, // 27: mcp.v1.McpAgentService.UndeployService:input_type -> mcp.v1.UndeployServiceRequest
6, // 28: mcp.v1.McpAgentService.StopService:input_type -> mcp.v1.StopServiceRequest
8, // 29: mcp.v1.McpAgentService.StartService:input_type -> mcp.v1.StartServiceRequest
10, // 30: mcp.v1.McpAgentService.RestartService:input_type -> mcp.v1.RestartServiceRequest
14, // 31: mcp.v1.McpAgentService.SyncDesiredState:input_type -> mcp.v1.SyncDesiredStateRequest
17, // 32: mcp.v1.McpAgentService.ListServices:input_type -> mcp.v1.ListServicesRequest
21, // 33: mcp.v1.McpAgentService.GetServiceStatus:input_type -> mcp.v1.GetServiceStatusRequest
25, // 34: mcp.v1.McpAgentService.LiveCheck:input_type -> mcp.v1.LiveCheckRequest
27, // 35: mcp.v1.McpAgentService.AdoptContainers:input_type -> mcp.v1.AdoptContainersRequest
36, // 36: mcp.v1.McpAgentService.PurgeComponent:input_type -> mcp.v1.PurgeRequest
30, // 37: mcp.v1.McpAgentService.PushFile:input_type -> mcp.v1.PushFileRequest
32, // 38: mcp.v1.McpAgentService.PullFile:input_type -> mcp.v1.PullFileRequest
34, // 39: mcp.v1.McpAgentService.NodeStatus:input_type -> mcp.v1.NodeStatusRequest
41, // 40: mcp.v1.McpAgentService.ListDNSRecords:input_type -> mcp.v1.ListDNSRecordsRequest
45, // 41: mcp.v1.McpAgentService.ListProxyRoutes:input_type -> mcp.v1.ListProxyRoutesRequest
49, // 42: mcp.v1.McpAgentService.AddProxyRoute:input_type -> mcp.v1.AddProxyRouteRequest
51, // 43: mcp.v1.McpAgentService.RemoveProxyRoute:input_type -> mcp.v1.RemoveProxyRouteRequest
39, // 44: mcp.v1.McpAgentService.Logs:input_type -> mcp.v1.LogsRequest
4, // 45: mcp.v1.McpAgentService.Deploy:output_type -> mcp.v1.DeployResponse
13, // 46: mcp.v1.McpAgentService.UndeployService:output_type -> mcp.v1.UndeployServiceResponse
7, // 47: mcp.v1.McpAgentService.StopService:output_type -> mcp.v1.StopServiceResponse
9, // 48: mcp.v1.McpAgentService.StartService:output_type -> mcp.v1.StartServiceResponse
11, // 49: mcp.v1.McpAgentService.RestartService:output_type -> mcp.v1.RestartServiceResponse
15, // 50: mcp.v1.McpAgentService.SyncDesiredState:output_type -> mcp.v1.SyncDesiredStateResponse
20, // 51: mcp.v1.McpAgentService.ListServices:output_type -> mcp.v1.ListServicesResponse
24, // 52: mcp.v1.McpAgentService.GetServiceStatus:output_type -> mcp.v1.GetServiceStatusResponse
26, // 53: mcp.v1.McpAgentService.LiveCheck:output_type -> mcp.v1.LiveCheckResponse
29, // 54: mcp.v1.McpAgentService.AdoptContainers:output_type -> mcp.v1.AdoptContainersResponse
37, // 55: mcp.v1.McpAgentService.PurgeComponent:output_type -> mcp.v1.PurgeResponse
31, // 56: mcp.v1.McpAgentService.PushFile:output_type -> mcp.v1.PushFileResponse
33, // 57: mcp.v1.McpAgentService.PullFile:output_type -> mcp.v1.PullFileResponse
35, // 58: mcp.v1.McpAgentService.NodeStatus:output_type -> mcp.v1.NodeStatusResponse
44, // 59: mcp.v1.McpAgentService.ListDNSRecords:output_type -> mcp.v1.ListDNSRecordsResponse
48, // 60: mcp.v1.McpAgentService.ListProxyRoutes:output_type -> mcp.v1.ListProxyRoutesResponse
50, // 61: mcp.v1.McpAgentService.AddProxyRoute:output_type -> mcp.v1.AddProxyRouteResponse
52, // 62: mcp.v1.McpAgentService.RemoveProxyRoute:output_type -> mcp.v1.RemoveProxyRouteResponse
40, // 63: mcp.v1.McpAgentService.Logs:output_type -> mcp.v1.LogsResponse
45, // [45:64] is the sub-list for method output_type
26, // [26:45] is the sub-list for method input_type
26, // [26:26] is the sub-list for extension type_name
26, // [26:26] is the sub-list for extension extendee
0, // [0:26] is the sub-list for field type_name
}
func init() { file_proto_mcp_v1_mcp_proto_init() }
@@ -2656,7 +3403,7 @@ func file_proto_mcp_v1_mcp_proto_init() {
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_proto_mcp_v1_mcp_proto_rawDesc), len(file_proto_mcp_v1_mcp_proto_rawDesc)),
NumEnums: 0,
NumMessages: 41,
NumMessages: 53,
NumExtensions: 0,
NumServices: 1,
},

View File

@@ -33,6 +33,10 @@ const (
McpAgentService_PushFile_FullMethodName = "/mcp.v1.McpAgentService/PushFile"
McpAgentService_PullFile_FullMethodName = "/mcp.v1.McpAgentService/PullFile"
McpAgentService_NodeStatus_FullMethodName = "/mcp.v1.McpAgentService/NodeStatus"
McpAgentService_ListDNSRecords_FullMethodName = "/mcp.v1.McpAgentService/ListDNSRecords"
McpAgentService_ListProxyRoutes_FullMethodName = "/mcp.v1.McpAgentService/ListProxyRoutes"
McpAgentService_AddProxyRoute_FullMethodName = "/mcp.v1.McpAgentService/AddProxyRoute"
McpAgentService_RemoveProxyRoute_FullMethodName = "/mcp.v1.McpAgentService/RemoveProxyRoute"
McpAgentService_Logs_FullMethodName = "/mcp.v1.McpAgentService/Logs"
)
@@ -61,6 +65,12 @@ type McpAgentServiceClient interface {
PullFile(ctx context.Context, in *PullFileRequest, opts ...grpc.CallOption) (*PullFileResponse, error)
// Node
NodeStatus(ctx context.Context, in *NodeStatusRequest, opts ...grpc.CallOption) (*NodeStatusResponse, error)
// DNS (query MCNS)
ListDNSRecords(ctx context.Context, in *ListDNSRecordsRequest, opts ...grpc.CallOption) (*ListDNSRecordsResponse, error)
// Proxy routes (query mc-proxy)
ListProxyRoutes(ctx context.Context, in *ListProxyRoutesRequest, opts ...grpc.CallOption) (*ListProxyRoutesResponse, error)
AddProxyRoute(ctx context.Context, in *AddProxyRouteRequest, opts ...grpc.CallOption) (*AddProxyRouteResponse, error)
RemoveProxyRoute(ctx context.Context, in *RemoveProxyRouteRequest, opts ...grpc.CallOption) (*RemoveProxyRouteResponse, error)
// Logs
Logs(ctx context.Context, in *LogsRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[LogsResponse], error)
}
@@ -213,6 +223,46 @@ func (c *mcpAgentServiceClient) NodeStatus(ctx context.Context, in *NodeStatusRe
return out, nil
}
func (c *mcpAgentServiceClient) ListDNSRecords(ctx context.Context, in *ListDNSRecordsRequest, opts ...grpc.CallOption) (*ListDNSRecordsResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(ListDNSRecordsResponse)
err := c.cc.Invoke(ctx, McpAgentService_ListDNSRecords_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *mcpAgentServiceClient) ListProxyRoutes(ctx context.Context, in *ListProxyRoutesRequest, opts ...grpc.CallOption) (*ListProxyRoutesResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(ListProxyRoutesResponse)
err := c.cc.Invoke(ctx, McpAgentService_ListProxyRoutes_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *mcpAgentServiceClient) AddProxyRoute(ctx context.Context, in *AddProxyRouteRequest, opts ...grpc.CallOption) (*AddProxyRouteResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(AddProxyRouteResponse)
err := c.cc.Invoke(ctx, McpAgentService_AddProxyRoute_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *mcpAgentServiceClient) RemoveProxyRoute(ctx context.Context, in *RemoveProxyRouteRequest, opts ...grpc.CallOption) (*RemoveProxyRouteResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(RemoveProxyRouteResponse)
err := c.cc.Invoke(ctx, McpAgentService_RemoveProxyRoute_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *mcpAgentServiceClient) Logs(ctx context.Context, in *LogsRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[LogsResponse], error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
stream, err := c.cc.NewStream(ctx, &McpAgentService_ServiceDesc.Streams[0], McpAgentService_Logs_FullMethodName, cOpts...)
@@ -257,6 +307,12 @@ type McpAgentServiceServer interface {
PullFile(context.Context, *PullFileRequest) (*PullFileResponse, error)
// Node
NodeStatus(context.Context, *NodeStatusRequest) (*NodeStatusResponse, error)
// DNS (query MCNS)
ListDNSRecords(context.Context, *ListDNSRecordsRequest) (*ListDNSRecordsResponse, error)
// Proxy routes (query mc-proxy)
ListProxyRoutes(context.Context, *ListProxyRoutesRequest) (*ListProxyRoutesResponse, error)
AddProxyRoute(context.Context, *AddProxyRouteRequest) (*AddProxyRouteResponse, error)
RemoveProxyRoute(context.Context, *RemoveProxyRouteRequest) (*RemoveProxyRouteResponse, error)
// Logs
Logs(*LogsRequest, grpc.ServerStreamingServer[LogsResponse]) error
mustEmbedUnimplementedMcpAgentServiceServer()
@@ -311,6 +367,18 @@ func (UnimplementedMcpAgentServiceServer) PullFile(context.Context, *PullFileReq
func (UnimplementedMcpAgentServiceServer) NodeStatus(context.Context, *NodeStatusRequest) (*NodeStatusResponse, error) {
return nil, status.Error(codes.Unimplemented, "method NodeStatus not implemented")
}
func (UnimplementedMcpAgentServiceServer) ListDNSRecords(context.Context, *ListDNSRecordsRequest) (*ListDNSRecordsResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ListDNSRecords not implemented")
}
func (UnimplementedMcpAgentServiceServer) ListProxyRoutes(context.Context, *ListProxyRoutesRequest) (*ListProxyRoutesResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ListProxyRoutes not implemented")
}
func (UnimplementedMcpAgentServiceServer) AddProxyRoute(context.Context, *AddProxyRouteRequest) (*AddProxyRouteResponse, error) {
return nil, status.Error(codes.Unimplemented, "method AddProxyRoute not implemented")
}
func (UnimplementedMcpAgentServiceServer) RemoveProxyRoute(context.Context, *RemoveProxyRouteRequest) (*RemoveProxyRouteResponse, error) {
return nil, status.Error(codes.Unimplemented, "method RemoveProxyRoute not implemented")
}
func (UnimplementedMcpAgentServiceServer) Logs(*LogsRequest, grpc.ServerStreamingServer[LogsResponse]) error {
return status.Error(codes.Unimplemented, "method Logs not implemented")
}
@@ -587,6 +655,78 @@ func _McpAgentService_NodeStatus_Handler(srv interface{}, ctx context.Context, d
return interceptor(ctx, in, info, handler)
}
func _McpAgentService_ListDNSRecords_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ListDNSRecordsRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(McpAgentServiceServer).ListDNSRecords(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: McpAgentService_ListDNSRecords_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(McpAgentServiceServer).ListDNSRecords(ctx, req.(*ListDNSRecordsRequest))
}
return interceptor(ctx, in, info, handler)
}
func _McpAgentService_ListProxyRoutes_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ListProxyRoutesRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(McpAgentServiceServer).ListProxyRoutes(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: McpAgentService_ListProxyRoutes_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(McpAgentServiceServer).ListProxyRoutes(ctx, req.(*ListProxyRoutesRequest))
}
return interceptor(ctx, in, info, handler)
}
func _McpAgentService_AddProxyRoute_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(AddProxyRouteRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(McpAgentServiceServer).AddProxyRoute(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: McpAgentService_AddProxyRoute_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(McpAgentServiceServer).AddProxyRoute(ctx, req.(*AddProxyRouteRequest))
}
return interceptor(ctx, in, info, handler)
}
func _McpAgentService_RemoveProxyRoute_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(RemoveProxyRouteRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(McpAgentServiceServer).RemoveProxyRoute(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: McpAgentService_RemoveProxyRoute_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(McpAgentServiceServer).RemoveProxyRoute(ctx, req.(*RemoveProxyRouteRequest))
}
return interceptor(ctx, in, info, handler)
}
func _McpAgentService_Logs_Handler(srv interface{}, stream grpc.ServerStream) error {
m := new(LogsRequest)
if err := stream.RecvMsg(m); err != nil {
@@ -661,6 +801,22 @@ var McpAgentService_ServiceDesc = grpc.ServiceDesc{
MethodName: "NodeStatus",
Handler: _McpAgentService_NodeStatus_Handler,
},
{
MethodName: "ListDNSRecords",
Handler: _McpAgentService_ListDNSRecords_Handler,
},
{
MethodName: "ListProxyRoutes",
Handler: _McpAgentService_ListProxyRoutes_Handler,
},
{
MethodName: "AddProxyRoute",
Handler: _McpAgentService_AddProxyRoute_Handler,
},
{
MethodName: "RemoveProxyRoute",
Handler: _McpAgentService_RemoveProxyRoute_Handler,
},
},
Streams: []grpc.StreamDesc{
{

View File

@@ -35,11 +35,12 @@ type Agent struct {
Proxy *ProxyRouter
Certs *CertProvisioner
DNS *DNSRegistrar
Version string
}
// Run starts the agent: opens the database, sets up the gRPC server with
// TLS and auth, and blocks until SIGINT/SIGTERM.
func Run(cfg *config.AgentConfig) error {
func Run(cfg *config.AgentConfig, version string) error {
logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{
Level: parseLogLevel(cfg.Log.Level),
}))
@@ -79,6 +80,7 @@ func Run(cfg *config.AgentConfig) error {
Proxy: proxy,
Certs: certs,
DNS: dns,
Version: version,
}
tlsCert, err := tls.LoadX509KeyPair(cfg.Server.TLSCert, cfg.Server.TLSKey)

View File

@@ -34,6 +34,9 @@ func (a *Agent) Deploy(ctx context.Context, req *mcpv1.DeployRequest) (*mcpv1.De
filtered = append(filtered, cs)
}
}
if len(filtered) == 0 {
return nil, fmt.Errorf("component %q not found in service %q", target, serviceName)
}
components = filtered
}
@@ -131,7 +134,8 @@ func (a *Agent) deployComponent(ctx context.Context, serviceName string, cs *mcp
Error: fmt.Sprintf("allocate route ports: %v", err),
}
}
runSpec.Ports = ports
// Merge explicit ports from the spec with route-allocated ports.
runSpec.Ports = append(cs.GetPorts(), ports...)
runSpec.Env = append(runSpec.Env, env...)
} else {
// Legacy: use ports directly from the spec.

View File

@@ -26,8 +26,8 @@ type DNSRegistrar struct {
logger *slog.Logger
}
// dnsRecord is the JSON representation of an MCNS record.
type dnsRecord struct {
// DNSRecord is the JSON representation of an MCNS record.
type DNSRecord struct {
ID int `json:"ID"`
Name string `json:"Name"`
Type string `json:"Type"`
@@ -136,8 +136,87 @@ func (d *DNSRegistrar) RemoveRecord(ctx context.Context, serviceName string) err
return nil
}
// DNSZone is the JSON representation of an MCNS zone.
type DNSZone struct {
Name string `json:"Name"`
}
// ListZones returns all zones from MCNS.
func (d *DNSRegistrar) ListZones(ctx context.Context) ([]DNSZone, error) {
if d == nil {
return nil, fmt.Errorf("DNS registrar not configured")
}
url := fmt.Sprintf("%s/v1/zones", d.serverURL)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return nil, fmt.Errorf("create list zones request: %w", err)
}
req.Header.Set("Authorization", "Bearer "+d.token)
resp, err := d.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("list zones: %w", err)
}
defer func() { _ = resp.Body.Close() }()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("read list zones response: %w", err)
}
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("list zones: mcns returned %d: %s", resp.StatusCode, string(body))
}
var envelope struct {
Zones []DNSZone `json:"zones"`
}
if err := json.Unmarshal(body, &envelope); err != nil {
return nil, fmt.Errorf("parse list zones response: %w", err)
}
return envelope.Zones, nil
}
// ListZoneRecords returns all records in the given zone (no filters).
func (d *DNSRegistrar) ListZoneRecords(ctx context.Context, zone string) ([]DNSRecord, error) {
if d == nil {
return nil, fmt.Errorf("DNS registrar not configured")
}
url := fmt.Sprintf("%s/v1/zones/%s/records", d.serverURL, zone)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return nil, fmt.Errorf("create list zone records request: %w", err)
}
req.Header.Set("Authorization", "Bearer "+d.token)
resp, err := d.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("list zone records: %w", err)
}
defer func() { _ = resp.Body.Close() }()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("read list zone records response: %w", err)
}
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("list zone records: mcns returned %d: %s", resp.StatusCode, string(body))
}
var envelope struct {
Records []DNSRecord `json:"records"`
}
if err := json.Unmarshal(body, &envelope); err != nil {
return nil, fmt.Errorf("parse list zone records response: %w", err)
}
return envelope.Records, nil
}
// listRecords returns A records matching the service name in the zone.
func (d *DNSRegistrar) listRecords(ctx context.Context, serviceName string) ([]dnsRecord, error) {
func (d *DNSRegistrar) listRecords(ctx context.Context, serviceName string) ([]DNSRecord, error) {
url := fmt.Sprintf("%s/v1/zones/%s/records?name=%s&type=A", d.serverURL, d.zone, serviceName)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
@@ -161,7 +240,7 @@ func (d *DNSRegistrar) listRecords(ctx context.Context, serviceName string) ([]d
}
var envelope struct {
Records []dnsRecord `json:"records"`
Records []DNSRecord `json:"records"`
}
if err := json.Unmarshal(body, &envelope); err != nil {
return nil, fmt.Errorf("parse list response: %w", err)

40
internal/agent/dns_rpc.go Normal file
View File

@@ -0,0 +1,40 @@
package agent
import (
"context"
"fmt"
mcpv1 "git.wntrmute.dev/mc/mcp/gen/mcp/v1"
)
// ListDNSRecords queries MCNS for all zones and their records.
func (a *Agent) ListDNSRecords(ctx context.Context, _ *mcpv1.ListDNSRecordsRequest) (*mcpv1.ListDNSRecordsResponse, error) {
a.Logger.Debug("ListDNSRecords called")
zones, err := a.DNS.ListZones(ctx)
if err != nil {
return nil, fmt.Errorf("list zones: %w", err)
}
resp := &mcpv1.ListDNSRecordsResponse{}
for _, z := range zones {
records, err := a.DNS.ListZoneRecords(ctx, z.Name)
if err != nil {
return nil, fmt.Errorf("list records for zone %q: %w", z.Name, err)
}
zone := &mcpv1.DNSZone{Name: z.Name}
for _, r := range records {
zone.Records = append(zone.Records, &mcpv1.DNSRecord{
Id: int64(r.ID),
Name: r.Name,
Type: r.Type,
Value: r.Value,
Ttl: int32(r.TTL), //nolint:gosec // TTL is bounded
})
}
resp.Zones = append(resp.Zones, zone)
}
return resp, nil
}

View File

@@ -90,7 +90,7 @@ func TestEnsureRecordSkipsWhenExists(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodGet {
// Return an existing record with the correct value.
resp := map[string][]dnsRecord{"records": {{ID: 1, Name: "myservice", Type: "A", Value: "192.168.88.181", TTL: 300}}}
resp := map[string][]DNSRecord{"records": {{ID: 1, Name: "myservice", Type: "A", Value: "192.168.88.181", TTL: 300}}}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(resp)
return
@@ -124,7 +124,7 @@ func TestEnsureRecordUpdatesWrongValue(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodGet {
// Return a record with a stale value.
resp := map[string][]dnsRecord{"records": {{ID: 42, Name: "myservice", Type: "A", Value: "10.0.0.1", TTL: 300}}}
resp := map[string][]DNSRecord{"records": {{ID: 42, Name: "myservice", Type: "A", Value: "10.0.0.1", TTL: 300}}}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(resp)
return
@@ -160,7 +160,7 @@ func TestRemoveRecordDeletes(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodGet {
resp := map[string][]dnsRecord{"records": {{ID: 7, Name: "myservice", Type: "A", Value: "192.168.88.181", TTL: 300}}}
resp := map[string][]DNSRecord{"records": {{ID: 7, Name: "myservice", Type: "A", Value: "192.168.88.181", TTL: 300}}}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(resp)
return

View File

@@ -63,8 +63,12 @@ func (a *Agent) Logs(req *mcpv1.LogsRequest, stream mcpv1.McpAgentService_LogsSe
scanner := bufio.NewScanner(pr)
for scanner.Scan() {
line := scanner.Bytes()
if len(line) == 0 {
continue
}
if err := stream.Send(&mcpv1.LogsResponse{
Data: append(scanner.Bytes(), '\n'),
Data: append(line, '\n'),
}); err != nil {
_ = cmd.Process.Kill()
return err

View File

@@ -31,6 +31,7 @@ func (a *Agent) NodeStatus(ctx context.Context, _ *mcpv1.NodeStatusRequest) (*mc
Runtime: a.Config.Agent.ContainerRuntime,
ServiceCount: uint32(len(services)), //nolint:gosec // bounded
ComponentCount: componentCount,
AgentVersion: a.Version,
}
// Runtime version.

View File

@@ -48,6 +48,30 @@ func (p *ProxyRouter) Close() error {
return p.client.Close()
}
// GetStatus returns the mc-proxy server status.
func (p *ProxyRouter) GetStatus(ctx context.Context) (*mcproxy.Status, error) {
if p == nil {
return nil, fmt.Errorf("mc-proxy not configured")
}
return p.client.GetStatus(ctx)
}
// AddRoute adds a single route to mc-proxy.
func (p *ProxyRouter) AddRoute(ctx context.Context, listenerAddr string, route mcproxy.Route) error {
if p == nil {
return fmt.Errorf("mc-proxy not configured")
}
return p.client.AddRoute(ctx, listenerAddr, route)
}
// RemoveRoute removes a single route from mc-proxy.
func (p *ProxyRouter) RemoveRoute(ctx context.Context, listenerAddr, hostname string) error {
if p == nil {
return fmt.Errorf("mc-proxy not configured")
}
return p.client.RemoveRoute(ctx, listenerAddr, hostname)
}
// RegisterRoutes registers all routes for a service component with mc-proxy.
// It uses the assigned host ports from the registry.
func (p *ProxyRouter) RegisterRoutes(ctx context.Context, serviceName string, routes []registry.Route, hostPorts map[string]int) error {

113
internal/agent/proxy_rpc.go Normal file
View File

@@ -0,0 +1,113 @@
package agent
import (
"context"
"fmt"
"git.wntrmute.dev/mc/mc-proxy/client/mcproxy"
mcpv1 "git.wntrmute.dev/mc/mcp/gen/mcp/v1"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/timestamppb"
)
// ListProxyRoutes queries mc-proxy for its current status and routes.
func (a *Agent) ListProxyRoutes(ctx context.Context, _ *mcpv1.ListProxyRoutesRequest) (*mcpv1.ListProxyRoutesResponse, error) {
a.Logger.Debug("ListProxyRoutes called")
status, err := a.Proxy.GetStatus(ctx)
if err != nil {
return nil, fmt.Errorf("get mc-proxy status: %w", err)
}
resp := &mcpv1.ListProxyRoutesResponse{
Version: status.Version,
TotalConnections: status.TotalConnections,
}
if !status.StartedAt.IsZero() {
resp.StartedAt = timestamppb.New(status.StartedAt)
}
for _, ls := range status.Listeners {
listener := &mcpv1.ProxyListenerInfo{
Addr: ls.Addr,
RouteCount: int32(ls.RouteCount), //nolint:gosec // bounded
ActiveConnections: ls.ActiveConnections,
}
for _, r := range ls.Routes {
listener.Routes = append(listener.Routes, &mcpv1.ProxyRouteInfo{
Hostname: r.Hostname,
Backend: r.Backend,
Mode: r.Mode,
BackendTls: r.BackendTLS,
})
}
resp.Listeners = append(resp.Listeners, listener)
}
return resp, nil
}
// AddProxyRoute adds a route to mc-proxy.
func (a *Agent) AddProxyRoute(ctx context.Context, req *mcpv1.AddProxyRouteRequest) (*mcpv1.AddProxyRouteResponse, error) {
if req.GetListenerAddr() == "" {
return nil, status.Error(codes.InvalidArgument, "listener_addr is required")
}
if req.GetHostname() == "" {
return nil, status.Error(codes.InvalidArgument, "hostname is required")
}
if req.GetBackend() == "" {
return nil, status.Error(codes.InvalidArgument, "backend is required")
}
if a.Proxy == nil {
return nil, status.Error(codes.FailedPrecondition, "mc-proxy not configured")
}
route := mcproxy.Route{
Hostname: req.GetHostname(),
Backend: req.GetBackend(),
Mode: req.GetMode(),
BackendTLS: req.GetBackendTls(),
TLSCert: req.GetTlsCert(),
TLSKey: req.GetTlsKey(),
}
if err := a.Proxy.AddRoute(ctx, req.GetListenerAddr(), route); err != nil {
return nil, fmt.Errorf("add route: %w", err)
}
a.Logger.Info("route added",
"listener", req.GetListenerAddr(),
"hostname", req.GetHostname(),
"backend", req.GetBackend(),
"mode", req.GetMode(),
)
return &mcpv1.AddProxyRouteResponse{}, nil
}
// RemoveProxyRoute removes a route from mc-proxy.
func (a *Agent) RemoveProxyRoute(ctx context.Context, req *mcpv1.RemoveProxyRouteRequest) (*mcpv1.RemoveProxyRouteResponse, error) {
if req.GetListenerAddr() == "" {
return nil, status.Error(codes.InvalidArgument, "listener_addr is required")
}
if req.GetHostname() == "" {
return nil, status.Error(codes.InvalidArgument, "hostname is required")
}
if a.Proxy == nil {
return nil, status.Error(codes.FailedPrecondition, "mc-proxy not configured")
}
if err := a.Proxy.RemoveRoute(ctx, req.GetListenerAddr(), req.GetHostname()); err != nil {
return nil, fmt.Errorf("remove route: %w", err)
}
a.Logger.Info("route removed",
"listener", req.GetListenerAddr(),
"hostname", req.GetHostname(),
)
return &mcpv1.RemoveProxyRouteResponse{}, nil
}

View File

@@ -99,6 +99,12 @@ func (a *Agent) liveCheckServices(ctx context.Context) ([]*mcpv1.ServiceInfo, er
if rc, ok := runtimeByName[containerName]; ok {
ci.ObservedState = rc.State
if rc.Version != "" {
ci.Version = rc.Version
}
if rc.Image != "" {
ci.Image = rc.Image
}
if !rc.Started.IsZero() {
ci.Started = timestamppb.New(rc.Started)
}

View File

@@ -5,6 +5,7 @@ import (
"encoding/json"
"errors"
"fmt"
"os"
"os/exec"
"strings"
"time"
@@ -179,16 +180,53 @@ func (p *Podman) Inspect(ctx context.Context, name string) (ContainerInfo, error
}
// Logs returns an exec.Cmd that streams container logs. For containers
// using the journald log driver, it uses journalctl (podman logs can't
// read journald outside the originating user session). For k8s-file or
// other drivers, it uses podman logs directly.
// using the journald log driver, it tries journalctl first (podman logs
// can't read journald outside the originating user session). If journalctl
// can't access the journal, it falls back to podman logs.
func (p *Podman) Logs(ctx context.Context, containerName string, tail int, follow, timestamps bool, since string) *exec.Cmd {
// Check if this container uses the journald log driver.
inspectCmd := exec.CommandContext(ctx, p.command(), "inspect", "--format", "{{.HostConfig.LogConfig.Type}}", containerName) //nolint:gosec
if out, err := inspectCmd.Output(); err == nil && strings.TrimSpace(string(out)) == "journald" {
if p.journalAccessible(ctx, containerName) {
return p.journalLogs(ctx, containerName, tail, follow, since)
}
}
return p.podmanLogs(ctx, containerName, tail, follow, timestamps, since)
}
// journalAccessible probes whether journalctl can read logs for the container.
func (p *Podman) journalAccessible(ctx context.Context, containerName string) bool {
args := []string{"--no-pager", "-n", "0"}
if os.Getuid() != 0 {
args = append(args, "--user")
}
args = append(args, "CONTAINER_NAME="+containerName)
cmd := exec.CommandContext(ctx, "journalctl", args...) //nolint:gosec
return cmd.Run() == nil
}
// journalLogs returns a journalctl command filtered by container name.
func (p *Podman) journalLogs(ctx context.Context, containerName string, tail int, follow bool, since string) *exec.Cmd {
args := []string{"--no-pager", "--output", "cat"}
if os.Getuid() != 0 {
args = append(args, "--user")
}
args = append(args, "CONTAINER_NAME="+containerName)
if tail > 0 {
args = append(args, "--lines", fmt.Sprintf("%d", tail))
}
if follow {
args = append(args, "--follow")
}
if since != "" {
args = append(args, "--since", since)
}
return exec.CommandContext(ctx, "journalctl", args...) //nolint:gosec // args built programmatically
}
// podmanLogs returns a podman logs command.
func (p *Podman) podmanLogs(ctx context.Context, containerName string, tail int, follow, timestamps bool, since string) *exec.Cmd {
args := []string{"logs"}
if tail > 0 {
args = append(args, "--tail", fmt.Sprintf("%d", tail))
@@ -206,21 +244,6 @@ func (p *Podman) Logs(ctx context.Context, containerName string, tail int, follo
return exec.CommandContext(ctx, p.command(), args...) //nolint:gosec // args built programmatically
}
// journalLogs returns a journalctl command filtered by container name.
func (p *Podman) journalLogs(ctx context.Context, containerName string, tail int, follow bool, since string) *exec.Cmd {
args := []string{"--no-pager", "--output", "cat", "CONTAINER_NAME=" + containerName}
if tail > 0 {
args = append(args, "--lines", fmt.Sprintf("%d", tail))
}
if follow {
args = append(args, "--follow")
}
if since != "" {
args = append(args, "--since", since)
}
return exec.CommandContext(ctx, "journalctl", args...) //nolint:gosec // args built programmatically
}
// Login authenticates to a container registry using the given token as
// the password. This enables non-interactive push with service account
// tokens (MCR accepts MCIAS JWTs as passwords).

View File

@@ -34,6 +34,14 @@ service McpAgentService {
// Node
rpc NodeStatus(NodeStatusRequest) returns (NodeStatusResponse);
// DNS (query MCNS)
rpc ListDNSRecords(ListDNSRecordsRequest) returns (ListDNSRecordsResponse);
// Proxy routes (query mc-proxy)
rpc ListProxyRoutes(ListProxyRoutesRequest) returns (ListProxyRoutesResponse);
rpc AddProxyRoute(AddProxyRouteRequest) returns (AddProxyRouteResponse);
rpc RemoveProxyRoute(RemoveProxyRouteRequest) returns (RemoveProxyRouteResponse);
// Logs
rpc Logs(LogsRequest) returns (stream LogsResponse);
}
@@ -257,6 +265,7 @@ message NodeStatusResponse {
uint64 memory_free_bytes = 9;
double cpu_usage_percent = 10;
google.protobuf.Timestamp uptime_since = 11;
string agent_version = 12;
}
// --- Purge ---
@@ -300,3 +309,68 @@ message LogsRequest {
message LogsResponse {
bytes data = 1;
}
// --- DNS ---
message ListDNSRecordsRequest {}
message DNSZone {
string name = 1;
repeated DNSRecord records = 2;
}
message DNSRecord {
int64 id = 1;
string name = 2;
string type = 3;
string value = 4;
int32 ttl = 5;
}
message ListDNSRecordsResponse {
repeated DNSZone zones = 1;
}
// --- Proxy routes ---
message ListProxyRoutesRequest {}
message ProxyRouteInfo {
string hostname = 1;
string backend = 2;
string mode = 3;
bool backend_tls = 4;
}
message ProxyListenerInfo {
string addr = 1;
int32 route_count = 2;
int64 active_connections = 3;
repeated ProxyRouteInfo routes = 4;
}
message ListProxyRoutesResponse {
string version = 1;
int64 total_connections = 2;
google.protobuf.Timestamp started_at = 3;
repeated ProxyListenerInfo listeners = 4;
}
message AddProxyRouteRequest {
string listener_addr = 1; // e.g. ":443"
string hostname = 2;
string backend = 3;
string mode = 4; // "l4" or "l7"
bool backend_tls = 5;
string tls_cert = 6; // path to TLS cert (required for l7)
string tls_key = 7; // path to TLS key (required for l7)
}
message AddProxyRouteResponse {}
message RemoveProxyRouteRequest {
string listener_addr = 1; // e.g. ":443"
string hostname = 2;
}
message RemoveProxyRouteResponse {}