16 Commits

Author SHA1 Message Date
cedba9bf83 Fix mcp ps uptime: parse StartedAt from podman ps JSON
List() was not extracting the StartedAt field from podman's JSON
output, so LiveCheck always returned zero timestamps and the CLI
showed "-" for every container's uptime.

podman ps --format json includes StartedAt as a Unix timestamp
(int64). Parse it into ContainerInfo.Started so the existing
LiveCheck → CLI uptime display chain works.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 22:56:39 -07:00
f06ab9aeb6 Bump flake version to 0.6.0
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 21:46:23 -07:00
f932dd64cc Add undeploy command: full inverse of deploy
Implements `mcp undeploy <service>` which tears down all infrastructure
for a service: removes mc-proxy routes, DNS records, TLS certificates,
stops and removes containers, releases allocated ports, and marks the
service inactive.

This fills the gap between `stop` (temporary pause) and `purge` (registry
cleanup). Undeploy is the complete teardown that returns the node to the
state before the service was deployed.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 21:45:42 -07:00
b2eaa69619 flake: install shell completions for mcp
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 21:41:25 -07:00
43789dd6be Fix route-based port mapping: use hostPort as container port
allocateRoutePorts() was using the route's port field (the mc-proxy
listener port, e.g. 443) as the container internal port in the podman
port mapping. For L7 routes, apps don't listen on the mc-proxy port —
they read $PORT (set to the assigned host port) and listen on that.

The mapping host:53204 → container:443 fails because nothing listens
on 443 inside the container. Fix: use hostPort as both the host and
container port, so $PORT = host port = container port.

Broke mcdoc in production (manually fixed, now permanently fixed).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 16:50:48 -07:00
2dd0ea93fc Fix ImageExists to use skopeo instead of podman manifest inspect
podman manifest inspect only works for multi-arch manifest lists,
returning exit code 125 for regular single-arch images. Switch to
skopeo inspect which works for both.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 16:49:48 -07:00
169b3a0d4a Fix EnsureRecord to check all existing records before updating
When multiple A records exist for a service (e.g., LAN and Tailscale
IPs), check all of them for the correct value before attempting an
update. Previously only checked the first record, which could trigger
a 409 conflict if another record already had the target value.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 15:17:19 -07:00
2bda7fc138 Fix DNS record JSON parsing for MCNS response format
MCNS returns records wrapped in {"records": [...]} envelope with
uppercase field names (ID, Name, Type, Value), not bare arrays
with lowercase fields.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 15:12:43 -07:00
76247978c2 Fix protoToComponent to include routes in synced components
Routes from the proto ComponentSpec were dropped during sync, causing
the deploy flow to see empty regRoutes and skip cert provisioning,
route registration, and DNS registration.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 14:39:26 -07:00
ca3bc736f6 Bump version to v0.5.0 for Phase D release
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 14:33:54 -07:00
9d9ad6588e Phase D: Automated DNS registration via MCNS
Add DNSRegistrar that creates/updates/deletes A records in MCNS
during deploy and stop. When a service has routes, the agent ensures
an A record exists in the configured zone pointing to the node's
address. On stop, the record is removed.

- Add MCNSConfig to agent config (server_url, ca_cert, token_path,
  zone, node_addr) with defaults and env overrides
- Add DNSRegistrar (internal/agent/dns.go): REST client for MCNS
  record CRUD, nil-receiver safe
- Wire into deploy flow (EnsureRecord after route registration)
- Wire into stop flow (RemoveRecord before container stop)
- 7 new tests, make all passes with 0 issues

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 14:33:41 -07:00
e4d131021e Bump version to v0.4.0 for Phase C release
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 13:47:02 -07:00
8d6c060483 Update mc-proxy dependency to v1.2.0, drop replace directive
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 13:39:41 -07:00
c7e1232f98 Phase C: Automated TLS cert provisioning for L7 routes
Add CertProvisioner that requests TLS certificates from Metacrypt's CA
API during deploy. When a service has L7 routes, the agent checks for
an existing cert, re-issues if missing or within 30 days of expiry,
and writes chain+key to mc-proxy's cert directory before registering
routes.

- Add MetacryptConfig to agent config (server_url, ca_cert, mount,
  issuer, token_path) with defaults and env overrides
- Add CertProvisioner (internal/agent/certs.go): REST client for
  Metacrypt IssueCert, atomic file writes, cert expiry checking
- Wire into Agent struct and deploy flow (before route registration)
- Add hasL7Routes/l7Hostnames helpers in deploy.go
- Fix pre-existing lint issues: unreachable code in portalloc.go,
  gofmt in servicedef.go, gosec suppressions, golangci v2 config
- Update vendored mc-proxy to fix protobuf init panic
- 10 new tests, make all passes with 0 issues

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 13:31:11 -07:00
572d2fb196 Regenerate proto files for mc/ module path
Raw descriptor bytes in .pb.go files were corrupted by the sed-based
module path rename (string length changed, breaking protobuf binary
encoding). Regenerated with protoc to fix.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 02:54:40 -07:00
c6a84a1b80 Bump flake.nix version to match latest tag
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 02:16:45 -07:00
28 changed files with 2267 additions and 193 deletions

View File

@@ -5,6 +5,24 @@ run:
tests: true tests: true
linters: linters:
exclusions:
paths:
- vendor
rules:
# In test files, suppress gosec rules that are false positives:
# G101: hardcoded test credentials
# G304: file paths from variables (t.TempDir paths)
# G306: WriteFile with 0644 (cert files need to be readable)
# G404: weak RNG (not security-relevant in tests)
- path: "_test\\.go"
linters:
- gosec
text: "G101|G304|G306|G404"
# Nil context is acceptable in tests for nil-receiver safety checks.
- path: "_test\\.go"
linters:
- staticcheck
text: "SA1012"
default: none default: none
enable: enable:
- errcheck - errcheck
@@ -69,12 +87,3 @@ formatters:
issues: issues:
max-issues-per-linter: 0 max-issues-per-linter: 0
max-same-issues: 0 max-same-issues: 0
exclusions:
paths:
- vendor
rules:
- path: "_test\\.go"
linters:
- gosec
text: "G101"

View File

@@ -198,6 +198,7 @@ mcp build <service>/<image> Build and push a single image
mcp deploy <service> Deploy all components from service definition mcp deploy <service> Deploy all components from service definition
mcp deploy <service>/<component> Deploy a single component mcp deploy <service>/<component> Deploy a single component
mcp deploy <service> -f <file> Deploy from explicit file mcp deploy <service> -f <file> Deploy from explicit file
mcp undeploy <service> Full teardown: remove routes, DNS, certs, containers
mcp stop <service> Stop all components, set active=false mcp stop <service> Stop all components, set active=false
mcp start <service> Start all components, set active=true mcp start <service> Start all components, set active=true
mcp restart <service> Restart all components mcp restart <service> Restart all components
@@ -453,6 +454,7 @@ import "google/protobuf/timestamp.proto";
service McpAgent { service McpAgent {
// Service lifecycle // Service lifecycle
rpc Deploy(DeployRequest) returns (DeployResponse); rpc Deploy(DeployRequest) returns (DeployResponse);
rpc UndeployService(UndeployRequest) returns (UndeployResponse);
rpc StopService(ServiceRequest) returns (ServiceResponse); rpc StopService(ServiceRequest) returns (ServiceResponse);
rpc StartService(ServiceRequest) returns (ServiceResponse); rpc StartService(ServiceRequest) returns (ServiceResponse);
rpc RestartService(ServiceRequest) returns (ServiceResponse); rpc RestartService(ServiceRequest) returns (ServiceResponse);
@@ -714,6 +716,40 @@ The flags passed to `podman run` are derived from the `ComponentSpec`:
| `volumes` | `-v <mapping>` (repeated) | | `volumes` | `-v <mapping>` (repeated) |
| `cmd` | appended after the image name | | `cmd` | appended after the image name |
#### Undeploy Flow
`mcp undeploy <service>` is the full inverse of deploy. It tears down all
infrastructure associated with a service. When the agent receives an
`UndeployService` RPC:
1. For each component:
a. Remove mc-proxy routes (traffic stops flowing).
b. Remove DNS A records from MCNS.
c. Remove TLS certificate and key files from the mc-proxy cert
directory (for L7 routes).
d. Stop and remove the container.
e. Release allocated host ports back to the port allocator.
f. Update component state to `removed` in the registry.
2. Mark the service as inactive.
3. Return success/failure per component.
The CLI also sets `active = false` in the local service definition file
to keep it in sync with the operator's intent.
Undeploy differs from `stop` in three ways:
| Aspect | `stop` | `undeploy` |
|--------|--------|-----------|
| Container | Stopped (still exists) | Stopped and removed |
| TLS certs | Kept | Removed |
| Ports | Kept allocated | Released |
| Service active | Unchanged | Set to inactive |
After undeploy, the service can be redeployed with `mcp deploy`. The
registry entries are preserved (desired state `removed`) so `mcp status`
and `mcp list` still show the service existed. Use `mcp purge` to clean
up the registry entries if desired.
### File Transfer ### File Transfer
The agent supports single-file push and pull, scoped to a specific The agent supports single-file push and pull, scoped to a specific
@@ -1203,6 +1239,102 @@ container, the effective host UID depends on the mapping. Files in
configuration should provision appropriate subuid/subgid ranges when configuration should provision appropriate subuid/subgid ranges when
creating the `mcp` user. creating the `mcp` user.
**Dockerfile convention**: Do not use `USER`, `VOLUME`, or `adduser`
directives in production Dockerfiles. The `user` field in the service
definition (typically `"0:0"`) controls the runtime user, and host
volumes provide the data directories. A non-root `USER` in the
Dockerfile maps to a subordinate UID under rootless podman that cannot
access files owned by the `mcp` user on the host.
#### Infrastructure Boot Order and Circular Dependencies
MCR (container registry) and MCNS (DNS) are both deployed as containers
via MCP, but MCP itself depends on them:
- **MCR** is reachable through mc-proxy (L4 passthrough on `:8443`).
The agent pulls images from MCR during `mcp deploy`.
- **MCNS** serves DNS for internal zones. Tailscale and the overlay
network depend on DNS resolution.
This creates circular dependencies during cold-start or recovery:
```
mcp deploy → agent pulls image → needs MCR → needs mc-proxy
mcp deploy → agent dials MCR → DNS resolves hostname → needs MCNS
```
**Cold-start procedure** (no containers running):
1. **Build images on the operator workstation** for mc-proxy, MCR, and
MCNS. Transfer to rift via `podman save` / `scp` / `podman load`
since the registry is not yet available:
```
docker save <image> -o /tmp/image.tar
scp /tmp/image.tar <rift-lan-ip>:/tmp/
# on rift, as mcp user:
podman load -i /tmp/image.tar
```
Use the LAN IP for scp, not a DNS name (DNS is not running yet).
2. **Start MCNS first** (DNS must come up before anything that resolves
hostnames). Run directly with podman since the MCP agent cannot reach
the registry yet:
```
podman run -d --name mcns --restart unless-stopped \
--sysctl net.ipv4.ip_unprivileged_port_start=53 \
-p <lan-ip>:53:53/tcp -p <lan-ip>:53:53/udp \
-p <overlay-ip>:53:53/tcp -p <overlay-ip>:53:53/udp \
-v /srv/mcns:/srv/mcns \
<mcns-image> server --config /srv/mcns/mcns.toml
```
3. **Start mc-proxy** (registry traffic routes through it):
```
podman run -d --name mc-proxy --network host \
--restart unless-stopped \
-v /srv/mc-proxy:/srv/mc-proxy \
<mc-proxy-image> server --config /srv/mc-proxy/mc-proxy.toml
```
4. **Start MCR** (API server, then web UI):
```
podman run -d --name mcr-api --network mcpnet \
--restart unless-stopped \
-p 127.0.0.1:28443:8443 -p 127.0.0.1:29443:9443 \
-v /srv/mcr:/srv/mcr \
<mcr-image> server --config /srv/mcr/mcr.toml
```
5. **Push images to MCR** from the operator workstation now that the
registry is reachable:
```
docker push <registry>/<image>:<tag>
```
6. **Start the MCP agent** (systemd service). It can now reach MCR for
image pulls.
7. **`mcp adopt`** the manually-started containers to bring them under
MCP management. Then `mcp service export` to generate service
definition files.
From this point, `mcp deploy` works normally. The manually-started
containers are replaced by MCP-managed ones on the next deploy.
**Recovery procedure** (mc-proxy or MCNS crashed):
If mc-proxy or MCNS goes down, the agent cannot pull images (registry
unreachable or DNS broken). Recovery:
1. Check if the required image is cached locally:
`podman images | grep <service>`
2. If cached, start the container directly with `podman run` (same
flags as the cold-start procedure above).
3. If not cached, transfer the image from the operator workstation via
`podman save` / `scp` / `podman load` using the LAN IP.
4. Once the infrastructure service is running, `mcp deploy` resumes
normal operation for other services.
--- ---
## Security Model ## Security Model

View File

@@ -36,6 +36,7 @@ func main() {
root.AddCommand(loginCmd()) root.AddCommand(loginCmd())
root.AddCommand(buildCmd()) root.AddCommand(buildCmd())
root.AddCommand(deployCmd()) root.AddCommand(deployCmd())
root.AddCommand(undeployCmd())
root.AddCommand(stopCmd()) root.AddCommand(stopCmd())
root.AddCommand(startCmd()) root.AddCommand(startCmd())
root.AddCommand(restartCmd()) root.AddCommand(restartCmd())

63
cmd/mcp/undeploy.go Normal file
View File

@@ -0,0 +1,63 @@
package main
import (
"context"
"fmt"
"path/filepath"
"github.com/spf13/cobra"
mcpv1 "git.wntrmute.dev/mc/mcp/gen/mcp/v1"
"git.wntrmute.dev/mc/mcp/internal/config"
"git.wntrmute.dev/mc/mcp/internal/servicedef"
)
func undeployCmd() *cobra.Command {
return &cobra.Command{
Use: "undeploy <service>",
Short: "Fully undeploy a service: remove routes, DNS, certs, and containers",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
cfg, err := config.LoadCLIConfig(cfgPath)
if err != nil {
return fmt.Errorf("load config: %w", err)
}
serviceName := args[0]
defPath := filepath.Join(cfg.Services.Dir, serviceName+".toml")
def, err := servicedef.Load(defPath)
if err != nil {
return fmt.Errorf("load service def: %w", err)
}
// Set active=false in the local file.
active := false
def.Active = &active
if err := servicedef.Write(defPath, def); err != nil {
return fmt.Errorf("write service def: %w", err)
}
address, err := findNodeAddress(cfg, def.Node)
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() }()
resp, err := client.UndeployService(context.Background(), &mcpv1.UndeployServiceRequest{
Name: serviceName,
})
if err != nil {
return fmt.Errorf("undeploy service: %w", err)
}
printComponentResults(resp.GetResults())
return nil
},
}
}

View File

@@ -10,7 +10,7 @@
let let
system = "x86_64-linux"; system = "x86_64-linux";
pkgs = nixpkgs.legacyPackages.${system}; pkgs = nixpkgs.legacyPackages.${system};
version = "0.1.0"; version = "0.6.0";
in in
{ {
packages.${system} = { packages.${system} = {
@@ -27,6 +27,14 @@
"-w" "-w"
"-X main.version=${version}" "-X main.version=${version}"
]; ];
postInstall = ''
mkdir -p $out/share/zsh/site-functions
mkdir -p $out/share/bash-completion/completions
mkdir -p $out/share/fish/vendor_completions.d
$out/bin/mcp completion zsh > $out/share/zsh/site-functions/_mcp
$out/bin/mcp completion bash > $out/share/bash-completion/completions/mcp
$out/bin/mcp completion fish > $out/share/fish/vendor_completions.d/mcp.fish
'';
}; };
mcp-agent = pkgs.buildGoModule { mcp-agent = pkgs.buildGoModule {

View File

@@ -25,7 +25,7 @@ const (
type RouteSpec struct { type RouteSpec struct {
state protoimpl.MessageState `protogen:"open.v1"` state protoimpl.MessageState `protogen:"open.v1"`
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` // route name (used for $PORT_<NAME>) Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` // route name (used for $PORT_<NAME>)
Port int32 `protobuf:"varint,2,opt,name=port,proto3" json:"port,omitempty"` // external port on mc-proxy Port int32 `protobuf:"varint,2,opt,name=port,proto3" json:"port,omitempty"` // mc-proxy listener port (e.g. 443, 8443, 9443); NOT the container internal port
Mode string `protobuf:"bytes,3,opt,name=mode,proto3" json:"mode,omitempty"` // "l4" or "l7" Mode string `protobuf:"bytes,3,opt,name=mode,proto3" json:"mode,omitempty"` // "l4" or "l7"
Hostname string `protobuf:"bytes,4,opt,name=hostname,proto3" json:"hostname,omitempty"` // optional public hostname override Hostname string `protobuf:"bytes,4,opt,name=hostname,proto3" json:"hostname,omitempty"` // optional public hostname override
unknownFields protoimpl.UnknownFields unknownFields protoimpl.UnknownFields
@@ -687,6 +687,94 @@ func (x *RestartServiceResponse) GetResults() []*ComponentResult {
return nil return nil
} }
type UndeployServiceRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *UndeployServiceRequest) Reset() {
*x = UndeployServiceRequest{}
mi := &file_proto_mcp_v1_mcp_proto_msgTypes[12]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *UndeployServiceRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*UndeployServiceRequest) ProtoMessage() {}
func (x *UndeployServiceRequest) ProtoReflect() protoreflect.Message {
mi := &file_proto_mcp_v1_mcp_proto_msgTypes[12]
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 UndeployServiceRequest.ProtoReflect.Descriptor instead.
func (*UndeployServiceRequest) Descriptor() ([]byte, []int) {
return file_proto_mcp_v1_mcp_proto_rawDescGZIP(), []int{12}
}
func (x *UndeployServiceRequest) GetName() string {
if x != nil {
return x.Name
}
return ""
}
type UndeployServiceResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
Results []*ComponentResult `protobuf:"bytes,1,rep,name=results,proto3" json:"results,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *UndeployServiceResponse) Reset() {
*x = UndeployServiceResponse{}
mi := &file_proto_mcp_v1_mcp_proto_msgTypes[13]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *UndeployServiceResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*UndeployServiceResponse) ProtoMessage() {}
func (x *UndeployServiceResponse) ProtoReflect() protoreflect.Message {
mi := &file_proto_mcp_v1_mcp_proto_msgTypes[13]
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 UndeployServiceResponse.ProtoReflect.Descriptor instead.
func (*UndeployServiceResponse) Descriptor() ([]byte, []int) {
return file_proto_mcp_v1_mcp_proto_rawDescGZIP(), []int{13}
}
func (x *UndeployServiceResponse) GetResults() []*ComponentResult {
if x != nil {
return x.Results
}
return nil
}
type SyncDesiredStateRequest struct { type SyncDesiredStateRequest struct {
state protoimpl.MessageState `protogen:"open.v1"` state protoimpl.MessageState `protogen:"open.v1"`
// All services for this node. // All services for this node.
@@ -697,7 +785,7 @@ type SyncDesiredStateRequest struct {
func (x *SyncDesiredStateRequest) Reset() { func (x *SyncDesiredStateRequest) Reset() {
*x = SyncDesiredStateRequest{} *x = SyncDesiredStateRequest{}
mi := &file_proto_mcp_v1_mcp_proto_msgTypes[12] mi := &file_proto_mcp_v1_mcp_proto_msgTypes[14]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -709,7 +797,7 @@ func (x *SyncDesiredStateRequest) String() string {
func (*SyncDesiredStateRequest) ProtoMessage() {} func (*SyncDesiredStateRequest) ProtoMessage() {}
func (x *SyncDesiredStateRequest) ProtoReflect() protoreflect.Message { func (x *SyncDesiredStateRequest) ProtoReflect() protoreflect.Message {
mi := &file_proto_mcp_v1_mcp_proto_msgTypes[12] mi := &file_proto_mcp_v1_mcp_proto_msgTypes[14]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -722,7 +810,7 @@ func (x *SyncDesiredStateRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use SyncDesiredStateRequest.ProtoReflect.Descriptor instead. // Deprecated: Use SyncDesiredStateRequest.ProtoReflect.Descriptor instead.
func (*SyncDesiredStateRequest) Descriptor() ([]byte, []int) { func (*SyncDesiredStateRequest) Descriptor() ([]byte, []int) {
return file_proto_mcp_v1_mcp_proto_rawDescGZIP(), []int{12} return file_proto_mcp_v1_mcp_proto_rawDescGZIP(), []int{14}
} }
func (x *SyncDesiredStateRequest) GetServices() []*ServiceSpec { func (x *SyncDesiredStateRequest) GetServices() []*ServiceSpec {
@@ -741,7 +829,7 @@ type SyncDesiredStateResponse struct {
func (x *SyncDesiredStateResponse) Reset() { func (x *SyncDesiredStateResponse) Reset() {
*x = SyncDesiredStateResponse{} *x = SyncDesiredStateResponse{}
mi := &file_proto_mcp_v1_mcp_proto_msgTypes[13] mi := &file_proto_mcp_v1_mcp_proto_msgTypes[15]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -753,7 +841,7 @@ func (x *SyncDesiredStateResponse) String() string {
func (*SyncDesiredStateResponse) ProtoMessage() {} func (*SyncDesiredStateResponse) ProtoMessage() {}
func (x *SyncDesiredStateResponse) ProtoReflect() protoreflect.Message { func (x *SyncDesiredStateResponse) ProtoReflect() protoreflect.Message {
mi := &file_proto_mcp_v1_mcp_proto_msgTypes[13] mi := &file_proto_mcp_v1_mcp_proto_msgTypes[15]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -766,7 +854,7 @@ func (x *SyncDesiredStateResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use SyncDesiredStateResponse.ProtoReflect.Descriptor instead. // Deprecated: Use SyncDesiredStateResponse.ProtoReflect.Descriptor instead.
func (*SyncDesiredStateResponse) Descriptor() ([]byte, []int) { func (*SyncDesiredStateResponse) Descriptor() ([]byte, []int) {
return file_proto_mcp_v1_mcp_proto_rawDescGZIP(), []int{13} return file_proto_mcp_v1_mcp_proto_rawDescGZIP(), []int{15}
} }
func (x *SyncDesiredStateResponse) GetResults() []*ServiceSyncResult { func (x *SyncDesiredStateResponse) GetResults() []*ServiceSyncResult {
@@ -788,7 +876,7 @@ type ServiceSyncResult struct {
func (x *ServiceSyncResult) Reset() { func (x *ServiceSyncResult) Reset() {
*x = ServiceSyncResult{} *x = ServiceSyncResult{}
mi := &file_proto_mcp_v1_mcp_proto_msgTypes[14] mi := &file_proto_mcp_v1_mcp_proto_msgTypes[16]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -800,7 +888,7 @@ func (x *ServiceSyncResult) String() string {
func (*ServiceSyncResult) ProtoMessage() {} func (*ServiceSyncResult) ProtoMessage() {}
func (x *ServiceSyncResult) ProtoReflect() protoreflect.Message { func (x *ServiceSyncResult) ProtoReflect() protoreflect.Message {
mi := &file_proto_mcp_v1_mcp_proto_msgTypes[14] mi := &file_proto_mcp_v1_mcp_proto_msgTypes[16]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -813,7 +901,7 @@ func (x *ServiceSyncResult) ProtoReflect() protoreflect.Message {
// Deprecated: Use ServiceSyncResult.ProtoReflect.Descriptor instead. // Deprecated: Use ServiceSyncResult.ProtoReflect.Descriptor instead.
func (*ServiceSyncResult) Descriptor() ([]byte, []int) { func (*ServiceSyncResult) Descriptor() ([]byte, []int) {
return file_proto_mcp_v1_mcp_proto_rawDescGZIP(), []int{14} return file_proto_mcp_v1_mcp_proto_rawDescGZIP(), []int{16}
} }
func (x *ServiceSyncResult) GetName() string { func (x *ServiceSyncResult) GetName() string {
@@ -845,7 +933,7 @@ type ListServicesRequest struct {
func (x *ListServicesRequest) Reset() { func (x *ListServicesRequest) Reset() {
*x = ListServicesRequest{} *x = ListServicesRequest{}
mi := &file_proto_mcp_v1_mcp_proto_msgTypes[15] mi := &file_proto_mcp_v1_mcp_proto_msgTypes[17]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -857,7 +945,7 @@ func (x *ListServicesRequest) String() string {
func (*ListServicesRequest) ProtoMessage() {} func (*ListServicesRequest) ProtoMessage() {}
func (x *ListServicesRequest) ProtoReflect() protoreflect.Message { func (x *ListServicesRequest) ProtoReflect() protoreflect.Message {
mi := &file_proto_mcp_v1_mcp_proto_msgTypes[15] mi := &file_proto_mcp_v1_mcp_proto_msgTypes[17]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -870,7 +958,7 @@ func (x *ListServicesRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use ListServicesRequest.ProtoReflect.Descriptor instead. // Deprecated: Use ListServicesRequest.ProtoReflect.Descriptor instead.
func (*ListServicesRequest) Descriptor() ([]byte, []int) { func (*ListServicesRequest) Descriptor() ([]byte, []int) {
return file_proto_mcp_v1_mcp_proto_rawDescGZIP(), []int{15} return file_proto_mcp_v1_mcp_proto_rawDescGZIP(), []int{17}
} }
type ServiceInfo struct { type ServiceInfo struct {
@@ -884,7 +972,7 @@ type ServiceInfo struct {
func (x *ServiceInfo) Reset() { func (x *ServiceInfo) Reset() {
*x = ServiceInfo{} *x = ServiceInfo{}
mi := &file_proto_mcp_v1_mcp_proto_msgTypes[16] mi := &file_proto_mcp_v1_mcp_proto_msgTypes[18]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -896,7 +984,7 @@ func (x *ServiceInfo) String() string {
func (*ServiceInfo) ProtoMessage() {} func (*ServiceInfo) ProtoMessage() {}
func (x *ServiceInfo) ProtoReflect() protoreflect.Message { func (x *ServiceInfo) ProtoReflect() protoreflect.Message {
mi := &file_proto_mcp_v1_mcp_proto_msgTypes[16] mi := &file_proto_mcp_v1_mcp_proto_msgTypes[18]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -909,7 +997,7 @@ func (x *ServiceInfo) ProtoReflect() protoreflect.Message {
// Deprecated: Use ServiceInfo.ProtoReflect.Descriptor instead. // Deprecated: Use ServiceInfo.ProtoReflect.Descriptor instead.
func (*ServiceInfo) Descriptor() ([]byte, []int) { func (*ServiceInfo) Descriptor() ([]byte, []int) {
return file_proto_mcp_v1_mcp_proto_rawDescGZIP(), []int{16} return file_proto_mcp_v1_mcp_proto_rawDescGZIP(), []int{18}
} }
func (x *ServiceInfo) GetName() string { func (x *ServiceInfo) GetName() string {
@@ -950,7 +1038,7 @@ type ComponentInfo struct {
func (x *ComponentInfo) Reset() { func (x *ComponentInfo) Reset() {
*x = ComponentInfo{} *x = ComponentInfo{}
mi := &file_proto_mcp_v1_mcp_proto_msgTypes[17] mi := &file_proto_mcp_v1_mcp_proto_msgTypes[19]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -962,7 +1050,7 @@ func (x *ComponentInfo) String() string {
func (*ComponentInfo) ProtoMessage() {} func (*ComponentInfo) ProtoMessage() {}
func (x *ComponentInfo) ProtoReflect() protoreflect.Message { func (x *ComponentInfo) ProtoReflect() protoreflect.Message {
mi := &file_proto_mcp_v1_mcp_proto_msgTypes[17] mi := &file_proto_mcp_v1_mcp_proto_msgTypes[19]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -975,7 +1063,7 @@ func (x *ComponentInfo) ProtoReflect() protoreflect.Message {
// Deprecated: Use ComponentInfo.ProtoReflect.Descriptor instead. // Deprecated: Use ComponentInfo.ProtoReflect.Descriptor instead.
func (*ComponentInfo) Descriptor() ([]byte, []int) { func (*ComponentInfo) Descriptor() ([]byte, []int) {
return file_proto_mcp_v1_mcp_proto_rawDescGZIP(), []int{17} return file_proto_mcp_v1_mcp_proto_rawDescGZIP(), []int{19}
} }
func (x *ComponentInfo) GetName() string { func (x *ComponentInfo) GetName() string {
@@ -1029,7 +1117,7 @@ type ListServicesResponse struct {
func (x *ListServicesResponse) Reset() { func (x *ListServicesResponse) Reset() {
*x = ListServicesResponse{} *x = ListServicesResponse{}
mi := &file_proto_mcp_v1_mcp_proto_msgTypes[18] mi := &file_proto_mcp_v1_mcp_proto_msgTypes[20]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -1041,7 +1129,7 @@ func (x *ListServicesResponse) String() string {
func (*ListServicesResponse) ProtoMessage() {} func (*ListServicesResponse) ProtoMessage() {}
func (x *ListServicesResponse) ProtoReflect() protoreflect.Message { func (x *ListServicesResponse) ProtoReflect() protoreflect.Message {
mi := &file_proto_mcp_v1_mcp_proto_msgTypes[18] mi := &file_proto_mcp_v1_mcp_proto_msgTypes[20]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -1054,7 +1142,7 @@ func (x *ListServicesResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use ListServicesResponse.ProtoReflect.Descriptor instead. // Deprecated: Use ListServicesResponse.ProtoReflect.Descriptor instead.
func (*ListServicesResponse) Descriptor() ([]byte, []int) { func (*ListServicesResponse) Descriptor() ([]byte, []int) {
return file_proto_mcp_v1_mcp_proto_rawDescGZIP(), []int{18} return file_proto_mcp_v1_mcp_proto_rawDescGZIP(), []int{20}
} }
func (x *ListServicesResponse) GetServices() []*ServiceInfo { func (x *ListServicesResponse) GetServices() []*ServiceInfo {
@@ -1074,7 +1162,7 @@ type GetServiceStatusRequest struct {
func (x *GetServiceStatusRequest) Reset() { func (x *GetServiceStatusRequest) Reset() {
*x = GetServiceStatusRequest{} *x = GetServiceStatusRequest{}
mi := &file_proto_mcp_v1_mcp_proto_msgTypes[19] mi := &file_proto_mcp_v1_mcp_proto_msgTypes[21]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -1086,7 +1174,7 @@ func (x *GetServiceStatusRequest) String() string {
func (*GetServiceStatusRequest) ProtoMessage() {} func (*GetServiceStatusRequest) ProtoMessage() {}
func (x *GetServiceStatusRequest) ProtoReflect() protoreflect.Message { func (x *GetServiceStatusRequest) ProtoReflect() protoreflect.Message {
mi := &file_proto_mcp_v1_mcp_proto_msgTypes[19] mi := &file_proto_mcp_v1_mcp_proto_msgTypes[21]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -1099,7 +1187,7 @@ func (x *GetServiceStatusRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use GetServiceStatusRequest.ProtoReflect.Descriptor instead. // Deprecated: Use GetServiceStatusRequest.ProtoReflect.Descriptor instead.
func (*GetServiceStatusRequest) Descriptor() ([]byte, []int) { func (*GetServiceStatusRequest) Descriptor() ([]byte, []int) {
return file_proto_mcp_v1_mcp_proto_rawDescGZIP(), []int{19} return file_proto_mcp_v1_mcp_proto_rawDescGZIP(), []int{21}
} }
func (x *GetServiceStatusRequest) GetName() string { func (x *GetServiceStatusRequest) GetName() string {
@@ -1121,7 +1209,7 @@ type DriftInfo struct {
func (x *DriftInfo) Reset() { func (x *DriftInfo) Reset() {
*x = DriftInfo{} *x = DriftInfo{}
mi := &file_proto_mcp_v1_mcp_proto_msgTypes[20] mi := &file_proto_mcp_v1_mcp_proto_msgTypes[22]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -1133,7 +1221,7 @@ func (x *DriftInfo) String() string {
func (*DriftInfo) ProtoMessage() {} func (*DriftInfo) ProtoMessage() {}
func (x *DriftInfo) ProtoReflect() protoreflect.Message { func (x *DriftInfo) ProtoReflect() protoreflect.Message {
mi := &file_proto_mcp_v1_mcp_proto_msgTypes[20] mi := &file_proto_mcp_v1_mcp_proto_msgTypes[22]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -1146,7 +1234,7 @@ func (x *DriftInfo) ProtoReflect() protoreflect.Message {
// Deprecated: Use DriftInfo.ProtoReflect.Descriptor instead. // Deprecated: Use DriftInfo.ProtoReflect.Descriptor instead.
func (*DriftInfo) Descriptor() ([]byte, []int) { func (*DriftInfo) Descriptor() ([]byte, []int) {
return file_proto_mcp_v1_mcp_proto_rawDescGZIP(), []int{20} return file_proto_mcp_v1_mcp_proto_rawDescGZIP(), []int{22}
} }
func (x *DriftInfo) GetService() string { func (x *DriftInfo) GetService() string {
@@ -1190,7 +1278,7 @@ type EventInfo struct {
func (x *EventInfo) Reset() { func (x *EventInfo) Reset() {
*x = EventInfo{} *x = EventInfo{}
mi := &file_proto_mcp_v1_mcp_proto_msgTypes[21] mi := &file_proto_mcp_v1_mcp_proto_msgTypes[23]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -1202,7 +1290,7 @@ func (x *EventInfo) String() string {
func (*EventInfo) ProtoMessage() {} func (*EventInfo) ProtoMessage() {}
func (x *EventInfo) ProtoReflect() protoreflect.Message { func (x *EventInfo) ProtoReflect() protoreflect.Message {
mi := &file_proto_mcp_v1_mcp_proto_msgTypes[21] mi := &file_proto_mcp_v1_mcp_proto_msgTypes[23]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -1215,7 +1303,7 @@ func (x *EventInfo) ProtoReflect() protoreflect.Message {
// Deprecated: Use EventInfo.ProtoReflect.Descriptor instead. // Deprecated: Use EventInfo.ProtoReflect.Descriptor instead.
func (*EventInfo) Descriptor() ([]byte, []int) { func (*EventInfo) Descriptor() ([]byte, []int) {
return file_proto_mcp_v1_mcp_proto_rawDescGZIP(), []int{21} return file_proto_mcp_v1_mcp_proto_rawDescGZIP(), []int{23}
} }
func (x *EventInfo) GetService() string { func (x *EventInfo) GetService() string {
@@ -1264,7 +1352,7 @@ type GetServiceStatusResponse struct {
func (x *GetServiceStatusResponse) Reset() { func (x *GetServiceStatusResponse) Reset() {
*x = GetServiceStatusResponse{} *x = GetServiceStatusResponse{}
mi := &file_proto_mcp_v1_mcp_proto_msgTypes[22] mi := &file_proto_mcp_v1_mcp_proto_msgTypes[24]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -1276,7 +1364,7 @@ func (x *GetServiceStatusResponse) String() string {
func (*GetServiceStatusResponse) ProtoMessage() {} func (*GetServiceStatusResponse) ProtoMessage() {}
func (x *GetServiceStatusResponse) ProtoReflect() protoreflect.Message { func (x *GetServiceStatusResponse) ProtoReflect() protoreflect.Message {
mi := &file_proto_mcp_v1_mcp_proto_msgTypes[22] mi := &file_proto_mcp_v1_mcp_proto_msgTypes[24]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -1289,7 +1377,7 @@ func (x *GetServiceStatusResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use GetServiceStatusResponse.ProtoReflect.Descriptor instead. // Deprecated: Use GetServiceStatusResponse.ProtoReflect.Descriptor instead.
func (*GetServiceStatusResponse) Descriptor() ([]byte, []int) { func (*GetServiceStatusResponse) Descriptor() ([]byte, []int) {
return file_proto_mcp_v1_mcp_proto_rawDescGZIP(), []int{22} return file_proto_mcp_v1_mcp_proto_rawDescGZIP(), []int{24}
} }
func (x *GetServiceStatusResponse) GetServices() []*ServiceInfo { func (x *GetServiceStatusResponse) GetServices() []*ServiceInfo {
@@ -1321,7 +1409,7 @@ type LiveCheckRequest struct {
func (x *LiveCheckRequest) Reset() { func (x *LiveCheckRequest) Reset() {
*x = LiveCheckRequest{} *x = LiveCheckRequest{}
mi := &file_proto_mcp_v1_mcp_proto_msgTypes[23] mi := &file_proto_mcp_v1_mcp_proto_msgTypes[25]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -1333,7 +1421,7 @@ func (x *LiveCheckRequest) String() string {
func (*LiveCheckRequest) ProtoMessage() {} func (*LiveCheckRequest) ProtoMessage() {}
func (x *LiveCheckRequest) ProtoReflect() protoreflect.Message { func (x *LiveCheckRequest) ProtoReflect() protoreflect.Message {
mi := &file_proto_mcp_v1_mcp_proto_msgTypes[23] mi := &file_proto_mcp_v1_mcp_proto_msgTypes[25]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -1346,7 +1434,7 @@ func (x *LiveCheckRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use LiveCheckRequest.ProtoReflect.Descriptor instead. // Deprecated: Use LiveCheckRequest.ProtoReflect.Descriptor instead.
func (*LiveCheckRequest) Descriptor() ([]byte, []int) { func (*LiveCheckRequest) Descriptor() ([]byte, []int) {
return file_proto_mcp_v1_mcp_proto_rawDescGZIP(), []int{23} return file_proto_mcp_v1_mcp_proto_rawDescGZIP(), []int{25}
} }
type LiveCheckResponse struct { type LiveCheckResponse struct {
@@ -1359,7 +1447,7 @@ type LiveCheckResponse struct {
func (x *LiveCheckResponse) Reset() { func (x *LiveCheckResponse) Reset() {
*x = LiveCheckResponse{} *x = LiveCheckResponse{}
mi := &file_proto_mcp_v1_mcp_proto_msgTypes[24] mi := &file_proto_mcp_v1_mcp_proto_msgTypes[26]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -1371,7 +1459,7 @@ func (x *LiveCheckResponse) String() string {
func (*LiveCheckResponse) ProtoMessage() {} func (*LiveCheckResponse) ProtoMessage() {}
func (x *LiveCheckResponse) ProtoReflect() protoreflect.Message { func (x *LiveCheckResponse) ProtoReflect() protoreflect.Message {
mi := &file_proto_mcp_v1_mcp_proto_msgTypes[24] mi := &file_proto_mcp_v1_mcp_proto_msgTypes[26]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -1384,7 +1472,7 @@ func (x *LiveCheckResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use LiveCheckResponse.ProtoReflect.Descriptor instead. // Deprecated: Use LiveCheckResponse.ProtoReflect.Descriptor instead.
func (*LiveCheckResponse) Descriptor() ([]byte, []int) { func (*LiveCheckResponse) Descriptor() ([]byte, []int) {
return file_proto_mcp_v1_mcp_proto_rawDescGZIP(), []int{24} return file_proto_mcp_v1_mcp_proto_rawDescGZIP(), []int{26}
} }
func (x *LiveCheckResponse) GetServices() []*ServiceInfo { func (x *LiveCheckResponse) GetServices() []*ServiceInfo {
@@ -1404,7 +1492,7 @@ type AdoptContainersRequest struct {
func (x *AdoptContainersRequest) Reset() { func (x *AdoptContainersRequest) Reset() {
*x = AdoptContainersRequest{} *x = AdoptContainersRequest{}
mi := &file_proto_mcp_v1_mcp_proto_msgTypes[25] mi := &file_proto_mcp_v1_mcp_proto_msgTypes[27]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -1416,7 +1504,7 @@ func (x *AdoptContainersRequest) String() string {
func (*AdoptContainersRequest) ProtoMessage() {} func (*AdoptContainersRequest) ProtoMessage() {}
func (x *AdoptContainersRequest) ProtoReflect() protoreflect.Message { func (x *AdoptContainersRequest) ProtoReflect() protoreflect.Message {
mi := &file_proto_mcp_v1_mcp_proto_msgTypes[25] mi := &file_proto_mcp_v1_mcp_proto_msgTypes[27]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -1429,7 +1517,7 @@ func (x *AdoptContainersRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use AdoptContainersRequest.ProtoReflect.Descriptor instead. // Deprecated: Use AdoptContainersRequest.ProtoReflect.Descriptor instead.
func (*AdoptContainersRequest) Descriptor() ([]byte, []int) { func (*AdoptContainersRequest) Descriptor() ([]byte, []int) {
return file_proto_mcp_v1_mcp_proto_rawDescGZIP(), []int{25} return file_proto_mcp_v1_mcp_proto_rawDescGZIP(), []int{27}
} }
func (x *AdoptContainersRequest) GetService() string { func (x *AdoptContainersRequest) GetService() string {
@@ -1453,7 +1541,7 @@ type AdoptResult struct {
func (x *AdoptResult) Reset() { func (x *AdoptResult) Reset() {
*x = AdoptResult{} *x = AdoptResult{}
mi := &file_proto_mcp_v1_mcp_proto_msgTypes[26] mi := &file_proto_mcp_v1_mcp_proto_msgTypes[28]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -1465,7 +1553,7 @@ func (x *AdoptResult) String() string {
func (*AdoptResult) ProtoMessage() {} func (*AdoptResult) ProtoMessage() {}
func (x *AdoptResult) ProtoReflect() protoreflect.Message { func (x *AdoptResult) ProtoReflect() protoreflect.Message {
mi := &file_proto_mcp_v1_mcp_proto_msgTypes[26] mi := &file_proto_mcp_v1_mcp_proto_msgTypes[28]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -1478,7 +1566,7 @@ func (x *AdoptResult) ProtoReflect() protoreflect.Message {
// Deprecated: Use AdoptResult.ProtoReflect.Descriptor instead. // Deprecated: Use AdoptResult.ProtoReflect.Descriptor instead.
func (*AdoptResult) Descriptor() ([]byte, []int) { func (*AdoptResult) Descriptor() ([]byte, []int) {
return file_proto_mcp_v1_mcp_proto_rawDescGZIP(), []int{26} return file_proto_mcp_v1_mcp_proto_rawDescGZIP(), []int{28}
} }
func (x *AdoptResult) GetContainer() string { func (x *AdoptResult) GetContainer() string {
@@ -1518,7 +1606,7 @@ type AdoptContainersResponse struct {
func (x *AdoptContainersResponse) Reset() { func (x *AdoptContainersResponse) Reset() {
*x = AdoptContainersResponse{} *x = AdoptContainersResponse{}
mi := &file_proto_mcp_v1_mcp_proto_msgTypes[27] mi := &file_proto_mcp_v1_mcp_proto_msgTypes[29]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -1530,7 +1618,7 @@ func (x *AdoptContainersResponse) String() string {
func (*AdoptContainersResponse) ProtoMessage() {} func (*AdoptContainersResponse) ProtoMessage() {}
func (x *AdoptContainersResponse) ProtoReflect() protoreflect.Message { func (x *AdoptContainersResponse) ProtoReflect() protoreflect.Message {
mi := &file_proto_mcp_v1_mcp_proto_msgTypes[27] mi := &file_proto_mcp_v1_mcp_proto_msgTypes[29]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -1543,7 +1631,7 @@ func (x *AdoptContainersResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use AdoptContainersResponse.ProtoReflect.Descriptor instead. // Deprecated: Use AdoptContainersResponse.ProtoReflect.Descriptor instead.
func (*AdoptContainersResponse) Descriptor() ([]byte, []int) { func (*AdoptContainersResponse) Descriptor() ([]byte, []int) {
return file_proto_mcp_v1_mcp_proto_rawDescGZIP(), []int{27} return file_proto_mcp_v1_mcp_proto_rawDescGZIP(), []int{29}
} }
func (x *AdoptContainersResponse) GetResults() []*AdoptResult { func (x *AdoptContainersResponse) GetResults() []*AdoptResult {
@@ -1567,7 +1655,7 @@ type PushFileRequest struct {
func (x *PushFileRequest) Reset() { func (x *PushFileRequest) Reset() {
*x = PushFileRequest{} *x = PushFileRequest{}
mi := &file_proto_mcp_v1_mcp_proto_msgTypes[28] mi := &file_proto_mcp_v1_mcp_proto_msgTypes[30]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -1579,7 +1667,7 @@ func (x *PushFileRequest) String() string {
func (*PushFileRequest) ProtoMessage() {} func (*PushFileRequest) ProtoMessage() {}
func (x *PushFileRequest) ProtoReflect() protoreflect.Message { func (x *PushFileRequest) ProtoReflect() protoreflect.Message {
mi := &file_proto_mcp_v1_mcp_proto_msgTypes[28] mi := &file_proto_mcp_v1_mcp_proto_msgTypes[30]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -1592,7 +1680,7 @@ func (x *PushFileRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use PushFileRequest.ProtoReflect.Descriptor instead. // Deprecated: Use PushFileRequest.ProtoReflect.Descriptor instead.
func (*PushFileRequest) Descriptor() ([]byte, []int) { func (*PushFileRequest) Descriptor() ([]byte, []int) {
return file_proto_mcp_v1_mcp_proto_rawDescGZIP(), []int{28} return file_proto_mcp_v1_mcp_proto_rawDescGZIP(), []int{30}
} }
func (x *PushFileRequest) GetService() string { func (x *PushFileRequest) GetService() string {
@@ -1633,7 +1721,7 @@ type PushFileResponse struct {
func (x *PushFileResponse) Reset() { func (x *PushFileResponse) Reset() {
*x = PushFileResponse{} *x = PushFileResponse{}
mi := &file_proto_mcp_v1_mcp_proto_msgTypes[29] mi := &file_proto_mcp_v1_mcp_proto_msgTypes[31]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -1645,7 +1733,7 @@ func (x *PushFileResponse) String() string {
func (*PushFileResponse) ProtoMessage() {} func (*PushFileResponse) ProtoMessage() {}
func (x *PushFileResponse) ProtoReflect() protoreflect.Message { func (x *PushFileResponse) ProtoReflect() protoreflect.Message {
mi := &file_proto_mcp_v1_mcp_proto_msgTypes[29] mi := &file_proto_mcp_v1_mcp_proto_msgTypes[31]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -1658,7 +1746,7 @@ func (x *PushFileResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use PushFileResponse.ProtoReflect.Descriptor instead. // Deprecated: Use PushFileResponse.ProtoReflect.Descriptor instead.
func (*PushFileResponse) Descriptor() ([]byte, []int) { func (*PushFileResponse) Descriptor() ([]byte, []int) {
return file_proto_mcp_v1_mcp_proto_rawDescGZIP(), []int{29} return file_proto_mcp_v1_mcp_proto_rawDescGZIP(), []int{31}
} }
func (x *PushFileResponse) GetSuccess() bool { func (x *PushFileResponse) GetSuccess() bool {
@@ -1686,7 +1774,7 @@ type PullFileRequest struct {
func (x *PullFileRequest) Reset() { func (x *PullFileRequest) Reset() {
*x = PullFileRequest{} *x = PullFileRequest{}
mi := &file_proto_mcp_v1_mcp_proto_msgTypes[30] mi := &file_proto_mcp_v1_mcp_proto_msgTypes[32]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -1698,7 +1786,7 @@ func (x *PullFileRequest) String() string {
func (*PullFileRequest) ProtoMessage() {} func (*PullFileRequest) ProtoMessage() {}
func (x *PullFileRequest) ProtoReflect() protoreflect.Message { func (x *PullFileRequest) ProtoReflect() protoreflect.Message {
mi := &file_proto_mcp_v1_mcp_proto_msgTypes[30] mi := &file_proto_mcp_v1_mcp_proto_msgTypes[32]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -1711,7 +1799,7 @@ func (x *PullFileRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use PullFileRequest.ProtoReflect.Descriptor instead. // Deprecated: Use PullFileRequest.ProtoReflect.Descriptor instead.
func (*PullFileRequest) Descriptor() ([]byte, []int) { func (*PullFileRequest) Descriptor() ([]byte, []int) {
return file_proto_mcp_v1_mcp_proto_rawDescGZIP(), []int{30} return file_proto_mcp_v1_mcp_proto_rawDescGZIP(), []int{32}
} }
func (x *PullFileRequest) GetService() string { func (x *PullFileRequest) GetService() string {
@@ -1739,7 +1827,7 @@ type PullFileResponse struct {
func (x *PullFileResponse) Reset() { func (x *PullFileResponse) Reset() {
*x = PullFileResponse{} *x = PullFileResponse{}
mi := &file_proto_mcp_v1_mcp_proto_msgTypes[31] mi := &file_proto_mcp_v1_mcp_proto_msgTypes[33]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -1751,7 +1839,7 @@ func (x *PullFileResponse) String() string {
func (*PullFileResponse) ProtoMessage() {} func (*PullFileResponse) ProtoMessage() {}
func (x *PullFileResponse) ProtoReflect() protoreflect.Message { func (x *PullFileResponse) ProtoReflect() protoreflect.Message {
mi := &file_proto_mcp_v1_mcp_proto_msgTypes[31] mi := &file_proto_mcp_v1_mcp_proto_msgTypes[33]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -1764,7 +1852,7 @@ func (x *PullFileResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use PullFileResponse.ProtoReflect.Descriptor instead. // Deprecated: Use PullFileResponse.ProtoReflect.Descriptor instead.
func (*PullFileResponse) Descriptor() ([]byte, []int) { func (*PullFileResponse) Descriptor() ([]byte, []int) {
return file_proto_mcp_v1_mcp_proto_rawDescGZIP(), []int{31} return file_proto_mcp_v1_mcp_proto_rawDescGZIP(), []int{33}
} }
func (x *PullFileResponse) GetContent() []byte { func (x *PullFileResponse) GetContent() []byte {
@@ -1796,7 +1884,7 @@ type NodeStatusRequest struct {
func (x *NodeStatusRequest) Reset() { func (x *NodeStatusRequest) Reset() {
*x = NodeStatusRequest{} *x = NodeStatusRequest{}
mi := &file_proto_mcp_v1_mcp_proto_msgTypes[32] mi := &file_proto_mcp_v1_mcp_proto_msgTypes[34]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -1808,7 +1896,7 @@ func (x *NodeStatusRequest) String() string {
func (*NodeStatusRequest) ProtoMessage() {} func (*NodeStatusRequest) ProtoMessage() {}
func (x *NodeStatusRequest) ProtoReflect() protoreflect.Message { func (x *NodeStatusRequest) ProtoReflect() protoreflect.Message {
mi := &file_proto_mcp_v1_mcp_proto_msgTypes[32] mi := &file_proto_mcp_v1_mcp_proto_msgTypes[34]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -1821,7 +1909,7 @@ func (x *NodeStatusRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use NodeStatusRequest.ProtoReflect.Descriptor instead. // Deprecated: Use NodeStatusRequest.ProtoReflect.Descriptor instead.
func (*NodeStatusRequest) Descriptor() ([]byte, []int) { func (*NodeStatusRequest) Descriptor() ([]byte, []int) {
return file_proto_mcp_v1_mcp_proto_rawDescGZIP(), []int{32} return file_proto_mcp_v1_mcp_proto_rawDescGZIP(), []int{34}
} }
type NodeStatusResponse struct { type NodeStatusResponse struct {
@@ -1844,7 +1932,7 @@ type NodeStatusResponse struct {
func (x *NodeStatusResponse) Reset() { func (x *NodeStatusResponse) Reset() {
*x = NodeStatusResponse{} *x = NodeStatusResponse{}
mi := &file_proto_mcp_v1_mcp_proto_msgTypes[33] mi := &file_proto_mcp_v1_mcp_proto_msgTypes[35]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -1856,7 +1944,7 @@ func (x *NodeStatusResponse) String() string {
func (*NodeStatusResponse) ProtoMessage() {} func (*NodeStatusResponse) ProtoMessage() {}
func (x *NodeStatusResponse) ProtoReflect() protoreflect.Message { func (x *NodeStatusResponse) ProtoReflect() protoreflect.Message {
mi := &file_proto_mcp_v1_mcp_proto_msgTypes[33] mi := &file_proto_mcp_v1_mcp_proto_msgTypes[35]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -1869,7 +1957,7 @@ func (x *NodeStatusResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use NodeStatusResponse.ProtoReflect.Descriptor instead. // Deprecated: Use NodeStatusResponse.ProtoReflect.Descriptor instead.
func (*NodeStatusResponse) Descriptor() ([]byte, []int) { func (*NodeStatusResponse) Descriptor() ([]byte, []int) {
return file_proto_mcp_v1_mcp_proto_rawDescGZIP(), []int{33} return file_proto_mcp_v1_mcp_proto_rawDescGZIP(), []int{35}
} }
func (x *NodeStatusResponse) GetNodeName() string { func (x *NodeStatusResponse) GetNodeName() string {
@@ -1966,7 +2054,7 @@ type PurgeRequest struct {
func (x *PurgeRequest) Reset() { func (x *PurgeRequest) Reset() {
*x = PurgeRequest{} *x = PurgeRequest{}
mi := &file_proto_mcp_v1_mcp_proto_msgTypes[34] mi := &file_proto_mcp_v1_mcp_proto_msgTypes[36]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -1978,7 +2066,7 @@ func (x *PurgeRequest) String() string {
func (*PurgeRequest) ProtoMessage() {} func (*PurgeRequest) ProtoMessage() {}
func (x *PurgeRequest) ProtoReflect() protoreflect.Message { func (x *PurgeRequest) ProtoReflect() protoreflect.Message {
mi := &file_proto_mcp_v1_mcp_proto_msgTypes[34] mi := &file_proto_mcp_v1_mcp_proto_msgTypes[36]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -1991,7 +2079,7 @@ func (x *PurgeRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use PurgeRequest.ProtoReflect.Descriptor instead. // Deprecated: Use PurgeRequest.ProtoReflect.Descriptor instead.
func (*PurgeRequest) Descriptor() ([]byte, []int) { func (*PurgeRequest) Descriptor() ([]byte, []int) {
return file_proto_mcp_v1_mcp_proto_rawDescGZIP(), []int{34} return file_proto_mcp_v1_mcp_proto_rawDescGZIP(), []int{36}
} }
func (x *PurgeRequest) GetService() string { func (x *PurgeRequest) GetService() string {
@@ -2031,7 +2119,7 @@ type PurgeResponse struct {
func (x *PurgeResponse) Reset() { func (x *PurgeResponse) Reset() {
*x = PurgeResponse{} *x = PurgeResponse{}
mi := &file_proto_mcp_v1_mcp_proto_msgTypes[35] mi := &file_proto_mcp_v1_mcp_proto_msgTypes[37]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -2043,7 +2131,7 @@ func (x *PurgeResponse) String() string {
func (*PurgeResponse) ProtoMessage() {} func (*PurgeResponse) ProtoMessage() {}
func (x *PurgeResponse) ProtoReflect() protoreflect.Message { func (x *PurgeResponse) ProtoReflect() protoreflect.Message {
mi := &file_proto_mcp_v1_mcp_proto_msgTypes[35] mi := &file_proto_mcp_v1_mcp_proto_msgTypes[37]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -2056,7 +2144,7 @@ func (x *PurgeResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use PurgeResponse.ProtoReflect.Descriptor instead. // Deprecated: Use PurgeResponse.ProtoReflect.Descriptor instead.
func (*PurgeResponse) Descriptor() ([]byte, []int) { func (*PurgeResponse) Descriptor() ([]byte, []int) {
return file_proto_mcp_v1_mcp_proto_rawDescGZIP(), []int{35} return file_proto_mcp_v1_mcp_proto_rawDescGZIP(), []int{37}
} }
func (x *PurgeResponse) GetResults() []*PurgeResult { func (x *PurgeResponse) GetResults() []*PurgeResult {
@@ -2080,7 +2168,7 @@ type PurgeResult struct {
func (x *PurgeResult) Reset() { func (x *PurgeResult) Reset() {
*x = PurgeResult{} *x = PurgeResult{}
mi := &file_proto_mcp_v1_mcp_proto_msgTypes[36] mi := &file_proto_mcp_v1_mcp_proto_msgTypes[38]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -2092,7 +2180,7 @@ func (x *PurgeResult) String() string {
func (*PurgeResult) ProtoMessage() {} func (*PurgeResult) ProtoMessage() {}
func (x *PurgeResult) ProtoReflect() protoreflect.Message { func (x *PurgeResult) ProtoReflect() protoreflect.Message {
mi := &file_proto_mcp_v1_mcp_proto_msgTypes[36] mi := &file_proto_mcp_v1_mcp_proto_msgTypes[38]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -2105,7 +2193,7 @@ func (x *PurgeResult) ProtoReflect() protoreflect.Message {
// Deprecated: Use PurgeResult.ProtoReflect.Descriptor instead. // Deprecated: Use PurgeResult.ProtoReflect.Descriptor instead.
func (*PurgeResult) Descriptor() ([]byte, []int) { func (*PurgeResult) Descriptor() ([]byte, []int) {
return file_proto_mcp_v1_mcp_proto_rawDescGZIP(), []int{36} return file_proto_mcp_v1_mcp_proto_rawDescGZIP(), []int{38}
} }
func (x *PurgeResult) GetService() string { func (x *PurgeResult) GetService() string {
@@ -2184,6 +2272,10 @@ const file_proto_mcp_v1_mcp_proto_rawDesc = "" +
"\x15RestartServiceRequest\x12\x12\n" + "\x15RestartServiceRequest\x12\x12\n" +
"\x04name\x18\x01 \x01(\tR\x04name\"K\n" + "\x04name\x18\x01 \x01(\tR\x04name\"K\n" +
"\x16RestartServiceResponse\x121\n" + "\x16RestartServiceResponse\x121\n" +
"\aresults\x18\x01 \x03(\v2\x17.mcp.v1.ComponentResultR\aresults\",\n" +
"\x16UndeployServiceRequest\x12\x12\n" +
"\x04name\x18\x01 \x01(\tR\x04name\"L\n" +
"\x17UndeployServiceResponse\x121\n" +
"\aresults\x18\x01 \x03(\v2\x17.mcp.v1.ComponentResultR\aresults\"J\n" + "\aresults\x18\x01 \x03(\v2\x17.mcp.v1.ComponentResultR\aresults\"J\n" +
"\x17SyncDesiredStateRequest\x12/\n" + "\x17SyncDesiredStateRequest\x12/\n" +
"\bservices\x18\x01 \x03(\v2\x13.mcp.v1.ServiceSpecR\bservices\"O\n" + "\bservices\x18\x01 \x03(\v2\x13.mcp.v1.ServiceSpecR\bservices\"O\n" +
@@ -2279,9 +2371,10 @@ const file_proto_mcp_v1_mcp_proto_rawDesc = "" +
"\aservice\x18\x01 \x01(\tR\aservice\x12\x1c\n" + "\aservice\x18\x01 \x01(\tR\aservice\x12\x1c\n" +
"\tcomponent\x18\x02 \x01(\tR\tcomponent\x12\x16\n" + "\tcomponent\x18\x02 \x01(\tR\tcomponent\x12\x16\n" +
"\x06purged\x18\x03 \x01(\bR\x06purged\x12\x16\n" + "\x06purged\x18\x03 \x01(\bR\x06purged\x12\x16\n" +
"\x06reason\x18\x04 \x01(\tR\x06reason2\xbf\a\n" + "\x06reason\x18\x04 \x01(\tR\x06reason2\x93\b\n" +
"\x0fMcpAgentService\x127\n" + "\x0fMcpAgentService\x127\n" +
"\x06Deploy\x12\x15.mcp.v1.DeployRequest\x1a\x16.mcp.v1.DeployResponse\x12F\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" +
"\vStopService\x12\x1a.mcp.v1.StopServiceRequest\x1a\x1b.mcp.v1.StopServiceResponse\x12I\n" + "\vStopService\x12\x1a.mcp.v1.StopServiceRequest\x1a\x1b.mcp.v1.StopServiceResponse\x12I\n" +
"\fStartService\x12\x1b.mcp.v1.StartServiceRequest\x1a\x1c.mcp.v1.StartServiceResponse\x12O\n" + "\fStartService\x12\x1b.mcp.v1.StartServiceRequest\x1a\x1c.mcp.v1.StartServiceResponse\x12O\n" +
"\x0eRestartService\x12\x1d.mcp.v1.RestartServiceRequest\x1a\x1e.mcp.v1.RestartServiceResponse\x12U\n" + "\x0eRestartService\x12\x1d.mcp.v1.RestartServiceRequest\x1a\x1e.mcp.v1.RestartServiceResponse\x12U\n" +
@@ -2294,7 +2387,7 @@ const file_proto_mcp_v1_mcp_proto_rawDesc = "" +
"\bPushFile\x12\x17.mcp.v1.PushFileRequest\x1a\x18.mcp.v1.PushFileResponse\x12=\n" + "\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" + "\bPullFile\x12\x17.mcp.v1.PullFileRequest\x1a\x18.mcp.v1.PullFileResponse\x12C\n" +
"\n" + "\n" +
"NodeStatus\x12\x19.mcp.v1.NodeStatusRequest\x1a\x1a.mcp.v1.NodeStatusResponseB,Z*git.wntrmute.dev/mc/mcp/gen/mcp/v1;mcpv1b\x06proto3" "NodeStatus\x12\x19.mcp.v1.NodeStatusRequest\x1a\x1a.mcp.v1.NodeStatusResponseB*Z(git.wntrmute.dev/mc/mcp/gen/mcp/v1;mcpv1b\x06proto3"
var ( var (
file_proto_mcp_v1_mcp_proto_rawDescOnce sync.Once file_proto_mcp_v1_mcp_proto_rawDescOnce sync.Once
@@ -2308,7 +2401,7 @@ func file_proto_mcp_v1_mcp_proto_rawDescGZIP() []byte {
return file_proto_mcp_v1_mcp_proto_rawDescData return file_proto_mcp_v1_mcp_proto_rawDescData
} }
var file_proto_mcp_v1_mcp_proto_msgTypes = make([]protoimpl.MessageInfo, 37) var file_proto_mcp_v1_mcp_proto_msgTypes = make([]protoimpl.MessageInfo, 39)
var file_proto_mcp_v1_mcp_proto_goTypes = []any{ var file_proto_mcp_v1_mcp_proto_goTypes = []any{
(*RouteSpec)(nil), // 0: mcp.v1.RouteSpec (*RouteSpec)(nil), // 0: mcp.v1.RouteSpec
(*ComponentSpec)(nil), // 1: mcp.v1.ComponentSpec (*ComponentSpec)(nil), // 1: mcp.v1.ComponentSpec
@@ -2322,32 +2415,34 @@ var file_proto_mcp_v1_mcp_proto_goTypes = []any{
(*StartServiceResponse)(nil), // 9: mcp.v1.StartServiceResponse (*StartServiceResponse)(nil), // 9: mcp.v1.StartServiceResponse
(*RestartServiceRequest)(nil), // 10: mcp.v1.RestartServiceRequest (*RestartServiceRequest)(nil), // 10: mcp.v1.RestartServiceRequest
(*RestartServiceResponse)(nil), // 11: mcp.v1.RestartServiceResponse (*RestartServiceResponse)(nil), // 11: mcp.v1.RestartServiceResponse
(*SyncDesiredStateRequest)(nil), // 12: mcp.v1.SyncDesiredStateRequest (*UndeployServiceRequest)(nil), // 12: mcp.v1.UndeployServiceRequest
(*SyncDesiredStateResponse)(nil), // 13: mcp.v1.SyncDesiredStateResponse (*UndeployServiceResponse)(nil), // 13: mcp.v1.UndeployServiceResponse
(*ServiceSyncResult)(nil), // 14: mcp.v1.ServiceSyncResult (*SyncDesiredStateRequest)(nil), // 14: mcp.v1.SyncDesiredStateRequest
(*ListServicesRequest)(nil), // 15: mcp.v1.ListServicesRequest (*SyncDesiredStateResponse)(nil), // 15: mcp.v1.SyncDesiredStateResponse
(*ServiceInfo)(nil), // 16: mcp.v1.ServiceInfo (*ServiceSyncResult)(nil), // 16: mcp.v1.ServiceSyncResult
(*ComponentInfo)(nil), // 17: mcp.v1.ComponentInfo (*ListServicesRequest)(nil), // 17: mcp.v1.ListServicesRequest
(*ListServicesResponse)(nil), // 18: mcp.v1.ListServicesResponse (*ServiceInfo)(nil), // 18: mcp.v1.ServiceInfo
(*GetServiceStatusRequest)(nil), // 19: mcp.v1.GetServiceStatusRequest (*ComponentInfo)(nil), // 19: mcp.v1.ComponentInfo
(*DriftInfo)(nil), // 20: mcp.v1.DriftInfo (*ListServicesResponse)(nil), // 20: mcp.v1.ListServicesResponse
(*EventInfo)(nil), // 21: mcp.v1.EventInfo (*GetServiceStatusRequest)(nil), // 21: mcp.v1.GetServiceStatusRequest
(*GetServiceStatusResponse)(nil), // 22: mcp.v1.GetServiceStatusResponse (*DriftInfo)(nil), // 22: mcp.v1.DriftInfo
(*LiveCheckRequest)(nil), // 23: mcp.v1.LiveCheckRequest (*EventInfo)(nil), // 23: mcp.v1.EventInfo
(*LiveCheckResponse)(nil), // 24: mcp.v1.LiveCheckResponse (*GetServiceStatusResponse)(nil), // 24: mcp.v1.GetServiceStatusResponse
(*AdoptContainersRequest)(nil), // 25: mcp.v1.AdoptContainersRequest (*LiveCheckRequest)(nil), // 25: mcp.v1.LiveCheckRequest
(*AdoptResult)(nil), // 26: mcp.v1.AdoptResult (*LiveCheckResponse)(nil), // 26: mcp.v1.LiveCheckResponse
(*AdoptContainersResponse)(nil), // 27: mcp.v1.AdoptContainersResponse (*AdoptContainersRequest)(nil), // 27: mcp.v1.AdoptContainersRequest
(*PushFileRequest)(nil), // 28: mcp.v1.PushFileRequest (*AdoptResult)(nil), // 28: mcp.v1.AdoptResult
(*PushFileResponse)(nil), // 29: mcp.v1.PushFileResponse (*AdoptContainersResponse)(nil), // 29: mcp.v1.AdoptContainersResponse
(*PullFileRequest)(nil), // 30: mcp.v1.PullFileRequest (*PushFileRequest)(nil), // 30: mcp.v1.PushFileRequest
(*PullFileResponse)(nil), // 31: mcp.v1.PullFileResponse (*PushFileResponse)(nil), // 31: mcp.v1.PushFileResponse
(*NodeStatusRequest)(nil), // 32: mcp.v1.NodeStatusRequest (*PullFileRequest)(nil), // 32: mcp.v1.PullFileRequest
(*NodeStatusResponse)(nil), // 33: mcp.v1.NodeStatusResponse (*PullFileResponse)(nil), // 33: mcp.v1.PullFileResponse
(*PurgeRequest)(nil), // 34: mcp.v1.PurgeRequest (*NodeStatusRequest)(nil), // 34: mcp.v1.NodeStatusRequest
(*PurgeResponse)(nil), // 35: mcp.v1.PurgeResponse (*NodeStatusResponse)(nil), // 35: mcp.v1.NodeStatusResponse
(*PurgeResult)(nil), // 36: mcp.v1.PurgeResult (*PurgeRequest)(nil), // 36: mcp.v1.PurgeRequest
(*timestamppb.Timestamp)(nil), // 37: google.protobuf.Timestamp (*PurgeResponse)(nil), // 37: mcp.v1.PurgeResponse
(*PurgeResult)(nil), // 38: mcp.v1.PurgeResult
(*timestamppb.Timestamp)(nil), // 39: google.protobuf.Timestamp
} }
var file_proto_mcp_v1_mcp_proto_depIdxs = []int32{ var file_proto_mcp_v1_mcp_proto_depIdxs = []int32{
0, // 0: mcp.v1.ComponentSpec.routes:type_name -> mcp.v1.RouteSpec 0, // 0: mcp.v1.ComponentSpec.routes:type_name -> mcp.v1.RouteSpec
@@ -2357,50 +2452,53 @@ var file_proto_mcp_v1_mcp_proto_depIdxs = []int32{
5, // 4: mcp.v1.StopServiceResponse.results:type_name -> mcp.v1.ComponentResult 5, // 4: mcp.v1.StopServiceResponse.results:type_name -> mcp.v1.ComponentResult
5, // 5: mcp.v1.StartServiceResponse.results:type_name -> mcp.v1.ComponentResult 5, // 5: mcp.v1.StartServiceResponse.results:type_name -> mcp.v1.ComponentResult
5, // 6: mcp.v1.RestartServiceResponse.results:type_name -> mcp.v1.ComponentResult 5, // 6: mcp.v1.RestartServiceResponse.results:type_name -> mcp.v1.ComponentResult
2, // 7: mcp.v1.SyncDesiredStateRequest.services:type_name -> mcp.v1.ServiceSpec 5, // 7: mcp.v1.UndeployServiceResponse.results:type_name -> mcp.v1.ComponentResult
14, // 8: mcp.v1.SyncDesiredStateResponse.results:type_name -> mcp.v1.ServiceSyncResult 2, // 8: mcp.v1.SyncDesiredStateRequest.services:type_name -> mcp.v1.ServiceSpec
17, // 9: mcp.v1.ServiceInfo.components:type_name -> mcp.v1.ComponentInfo 16, // 9: mcp.v1.SyncDesiredStateResponse.results:type_name -> mcp.v1.ServiceSyncResult
37, // 10: mcp.v1.ComponentInfo.started:type_name -> google.protobuf.Timestamp 19, // 10: mcp.v1.ServiceInfo.components:type_name -> mcp.v1.ComponentInfo
16, // 11: mcp.v1.ListServicesResponse.services:type_name -> mcp.v1.ServiceInfo 39, // 11: mcp.v1.ComponentInfo.started:type_name -> google.protobuf.Timestamp
37, // 12: mcp.v1.EventInfo.timestamp:type_name -> google.protobuf.Timestamp 18, // 12: mcp.v1.ListServicesResponse.services:type_name -> mcp.v1.ServiceInfo
16, // 13: mcp.v1.GetServiceStatusResponse.services:type_name -> mcp.v1.ServiceInfo 39, // 13: mcp.v1.EventInfo.timestamp:type_name -> google.protobuf.Timestamp
20, // 14: mcp.v1.GetServiceStatusResponse.drift:type_name -> mcp.v1.DriftInfo 18, // 14: mcp.v1.GetServiceStatusResponse.services:type_name -> mcp.v1.ServiceInfo
21, // 15: mcp.v1.GetServiceStatusResponse.recent_events:type_name -> mcp.v1.EventInfo 22, // 15: mcp.v1.GetServiceStatusResponse.drift:type_name -> mcp.v1.DriftInfo
16, // 16: mcp.v1.LiveCheckResponse.services:type_name -> mcp.v1.ServiceInfo 23, // 16: mcp.v1.GetServiceStatusResponse.recent_events:type_name -> mcp.v1.EventInfo
26, // 17: mcp.v1.AdoptContainersResponse.results:type_name -> mcp.v1.AdoptResult 18, // 17: mcp.v1.LiveCheckResponse.services:type_name -> mcp.v1.ServiceInfo
37, // 18: mcp.v1.NodeStatusResponse.uptime_since:type_name -> google.protobuf.Timestamp 28, // 18: mcp.v1.AdoptContainersResponse.results:type_name -> mcp.v1.AdoptResult
36, // 19: mcp.v1.PurgeResponse.results:type_name -> mcp.v1.PurgeResult 39, // 19: mcp.v1.NodeStatusResponse.uptime_since:type_name -> google.protobuf.Timestamp
3, // 20: mcp.v1.McpAgentService.Deploy:input_type -> mcp.v1.DeployRequest 38, // 20: mcp.v1.PurgeResponse.results:type_name -> mcp.v1.PurgeResult
6, // 21: mcp.v1.McpAgentService.StopService:input_type -> mcp.v1.StopServiceRequest 3, // 21: mcp.v1.McpAgentService.Deploy:input_type -> mcp.v1.DeployRequest
8, // 22: mcp.v1.McpAgentService.StartService:input_type -> mcp.v1.StartServiceRequest 12, // 22: mcp.v1.McpAgentService.UndeployService:input_type -> mcp.v1.UndeployServiceRequest
10, // 23: mcp.v1.McpAgentService.RestartService:input_type -> mcp.v1.RestartServiceRequest 6, // 23: mcp.v1.McpAgentService.StopService:input_type -> mcp.v1.StopServiceRequest
12, // 24: mcp.v1.McpAgentService.SyncDesiredState:input_type -> mcp.v1.SyncDesiredStateRequest 8, // 24: mcp.v1.McpAgentService.StartService:input_type -> mcp.v1.StartServiceRequest
15, // 25: mcp.v1.McpAgentService.ListServices:input_type -> mcp.v1.ListServicesRequest 10, // 25: mcp.v1.McpAgentService.RestartService:input_type -> mcp.v1.RestartServiceRequest
19, // 26: mcp.v1.McpAgentService.GetServiceStatus:input_type -> mcp.v1.GetServiceStatusRequest 14, // 26: mcp.v1.McpAgentService.SyncDesiredState:input_type -> mcp.v1.SyncDesiredStateRequest
23, // 27: mcp.v1.McpAgentService.LiveCheck:input_type -> mcp.v1.LiveCheckRequest 17, // 27: mcp.v1.McpAgentService.ListServices:input_type -> mcp.v1.ListServicesRequest
25, // 28: mcp.v1.McpAgentService.AdoptContainers:input_type -> mcp.v1.AdoptContainersRequest 21, // 28: mcp.v1.McpAgentService.GetServiceStatus:input_type -> mcp.v1.GetServiceStatusRequest
34, // 29: mcp.v1.McpAgentService.PurgeComponent:input_type -> mcp.v1.PurgeRequest 25, // 29: mcp.v1.McpAgentService.LiveCheck:input_type -> mcp.v1.LiveCheckRequest
28, // 30: mcp.v1.McpAgentService.PushFile:input_type -> mcp.v1.PushFileRequest 27, // 30: mcp.v1.McpAgentService.AdoptContainers:input_type -> mcp.v1.AdoptContainersRequest
30, // 31: mcp.v1.McpAgentService.PullFile:input_type -> mcp.v1.PullFileRequest 36, // 31: mcp.v1.McpAgentService.PurgeComponent:input_type -> mcp.v1.PurgeRequest
32, // 32: mcp.v1.McpAgentService.NodeStatus:input_type -> mcp.v1.NodeStatusRequest 30, // 32: mcp.v1.McpAgentService.PushFile:input_type -> mcp.v1.PushFileRequest
4, // 33: mcp.v1.McpAgentService.Deploy:output_type -> mcp.v1.DeployResponse 32, // 33: mcp.v1.McpAgentService.PullFile:input_type -> mcp.v1.PullFileRequest
7, // 34: mcp.v1.McpAgentService.StopService:output_type -> mcp.v1.StopServiceResponse 34, // 34: mcp.v1.McpAgentService.NodeStatus:input_type -> mcp.v1.NodeStatusRequest
9, // 35: mcp.v1.McpAgentService.StartService:output_type -> mcp.v1.StartServiceResponse 4, // 35: mcp.v1.McpAgentService.Deploy:output_type -> mcp.v1.DeployResponse
11, // 36: mcp.v1.McpAgentService.RestartService:output_type -> mcp.v1.RestartServiceResponse 13, // 36: mcp.v1.McpAgentService.UndeployService:output_type -> mcp.v1.UndeployServiceResponse
13, // 37: mcp.v1.McpAgentService.SyncDesiredState:output_type -> mcp.v1.SyncDesiredStateResponse 7, // 37: mcp.v1.McpAgentService.StopService:output_type -> mcp.v1.StopServiceResponse
18, // 38: mcp.v1.McpAgentService.ListServices:output_type -> mcp.v1.ListServicesResponse 9, // 38: mcp.v1.McpAgentService.StartService:output_type -> mcp.v1.StartServiceResponse
22, // 39: mcp.v1.McpAgentService.GetServiceStatus:output_type -> mcp.v1.GetServiceStatusResponse 11, // 39: mcp.v1.McpAgentService.RestartService:output_type -> mcp.v1.RestartServiceResponse
24, // 40: mcp.v1.McpAgentService.LiveCheck:output_type -> mcp.v1.LiveCheckResponse 15, // 40: mcp.v1.McpAgentService.SyncDesiredState:output_type -> mcp.v1.SyncDesiredStateResponse
27, // 41: mcp.v1.McpAgentService.AdoptContainers:output_type -> mcp.v1.AdoptContainersResponse 20, // 41: mcp.v1.McpAgentService.ListServices:output_type -> mcp.v1.ListServicesResponse
35, // 42: mcp.v1.McpAgentService.PurgeComponent:output_type -> mcp.v1.PurgeResponse 24, // 42: mcp.v1.McpAgentService.GetServiceStatus:output_type -> mcp.v1.GetServiceStatusResponse
29, // 43: mcp.v1.McpAgentService.PushFile:output_type -> mcp.v1.PushFileResponse 26, // 43: mcp.v1.McpAgentService.LiveCheck:output_type -> mcp.v1.LiveCheckResponse
31, // 44: mcp.v1.McpAgentService.PullFile:output_type -> mcp.v1.PullFileResponse 29, // 44: mcp.v1.McpAgentService.AdoptContainers:output_type -> mcp.v1.AdoptContainersResponse
33, // 45: mcp.v1.McpAgentService.NodeStatus:output_type -> mcp.v1.NodeStatusResponse 37, // 45: mcp.v1.McpAgentService.PurgeComponent:output_type -> mcp.v1.PurgeResponse
33, // [33:46] is the sub-list for method output_type 31, // 46: mcp.v1.McpAgentService.PushFile:output_type -> mcp.v1.PushFileResponse
20, // [20:33] is the sub-list for method input_type 33, // 47: mcp.v1.McpAgentService.PullFile:output_type -> mcp.v1.PullFileResponse
20, // [20:20] is the sub-list for extension type_name 35, // 48: mcp.v1.McpAgentService.NodeStatus:output_type -> mcp.v1.NodeStatusResponse
20, // [20:20] is the sub-list for extension extendee 35, // [35:49] is the sub-list for method output_type
0, // [0:20] is the sub-list for field type_name 21, // [21:35] 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
} }
func init() { file_proto_mcp_v1_mcp_proto_init() } func init() { file_proto_mcp_v1_mcp_proto_init() }
@@ -2414,7 +2512,7 @@ func file_proto_mcp_v1_mcp_proto_init() {
GoPackagePath: reflect.TypeOf(x{}).PkgPath(), 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)), RawDescriptor: unsafe.Slice(unsafe.StringData(file_proto_mcp_v1_mcp_proto_rawDesc), len(file_proto_mcp_v1_mcp_proto_rawDesc)),
NumEnums: 0, NumEnums: 0,
NumMessages: 37, NumMessages: 39,
NumExtensions: 0, NumExtensions: 0,
NumServices: 1, NumServices: 1,
}, },

View File

@@ -20,6 +20,7 @@ const _ = grpc.SupportPackageIsVersion9
const ( const (
McpAgentService_Deploy_FullMethodName = "/mcp.v1.McpAgentService/Deploy" McpAgentService_Deploy_FullMethodName = "/mcp.v1.McpAgentService/Deploy"
McpAgentService_UndeployService_FullMethodName = "/mcp.v1.McpAgentService/UndeployService"
McpAgentService_StopService_FullMethodName = "/mcp.v1.McpAgentService/StopService" McpAgentService_StopService_FullMethodName = "/mcp.v1.McpAgentService/StopService"
McpAgentService_StartService_FullMethodName = "/mcp.v1.McpAgentService/StartService" McpAgentService_StartService_FullMethodName = "/mcp.v1.McpAgentService/StartService"
McpAgentService_RestartService_FullMethodName = "/mcp.v1.McpAgentService/RestartService" McpAgentService_RestartService_FullMethodName = "/mcp.v1.McpAgentService/RestartService"
@@ -40,6 +41,7 @@ const (
type McpAgentServiceClient interface { type McpAgentServiceClient interface {
// Service lifecycle // Service lifecycle
Deploy(ctx context.Context, in *DeployRequest, opts ...grpc.CallOption) (*DeployResponse, error) Deploy(ctx context.Context, in *DeployRequest, opts ...grpc.CallOption) (*DeployResponse, error)
UndeployService(ctx context.Context, in *UndeployServiceRequest, opts ...grpc.CallOption) (*UndeployServiceResponse, error)
StopService(ctx context.Context, in *StopServiceRequest, opts ...grpc.CallOption) (*StopServiceResponse, error) StopService(ctx context.Context, in *StopServiceRequest, opts ...grpc.CallOption) (*StopServiceResponse, error)
StartService(ctx context.Context, in *StartServiceRequest, opts ...grpc.CallOption) (*StartServiceResponse, error) StartService(ctx context.Context, in *StartServiceRequest, opts ...grpc.CallOption) (*StartServiceResponse, error)
RestartService(ctx context.Context, in *RestartServiceRequest, opts ...grpc.CallOption) (*RestartServiceResponse, error) RestartService(ctx context.Context, in *RestartServiceRequest, opts ...grpc.CallOption) (*RestartServiceResponse, error)
@@ -78,6 +80,16 @@ func (c *mcpAgentServiceClient) Deploy(ctx context.Context, in *DeployRequest, o
return out, nil return out, nil
} }
func (c *mcpAgentServiceClient) UndeployService(ctx context.Context, in *UndeployServiceRequest, opts ...grpc.CallOption) (*UndeployServiceResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(UndeployServiceResponse)
err := c.cc.Invoke(ctx, McpAgentService_UndeployService_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *mcpAgentServiceClient) StopService(ctx context.Context, in *StopServiceRequest, opts ...grpc.CallOption) (*StopServiceResponse, error) { func (c *mcpAgentServiceClient) StopService(ctx context.Context, in *StopServiceRequest, opts ...grpc.CallOption) (*StopServiceResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(StopServiceResponse) out := new(StopServiceResponse)
@@ -204,6 +216,7 @@ func (c *mcpAgentServiceClient) NodeStatus(ctx context.Context, in *NodeStatusRe
type McpAgentServiceServer interface { type McpAgentServiceServer interface {
// Service lifecycle // Service lifecycle
Deploy(context.Context, *DeployRequest) (*DeployResponse, error) Deploy(context.Context, *DeployRequest) (*DeployResponse, error)
UndeployService(context.Context, *UndeployServiceRequest) (*UndeployServiceResponse, error)
StopService(context.Context, *StopServiceRequest) (*StopServiceResponse, error) StopService(context.Context, *StopServiceRequest) (*StopServiceResponse, error)
StartService(context.Context, *StartServiceRequest) (*StartServiceResponse, error) StartService(context.Context, *StartServiceRequest) (*StartServiceResponse, error)
RestartService(context.Context, *RestartServiceRequest) (*RestartServiceResponse, error) RestartService(context.Context, *RestartServiceRequest) (*RestartServiceResponse, error)
@@ -235,6 +248,9 @@ type UnimplementedMcpAgentServiceServer struct{}
func (UnimplementedMcpAgentServiceServer) Deploy(context.Context, *DeployRequest) (*DeployResponse, error) { func (UnimplementedMcpAgentServiceServer) Deploy(context.Context, *DeployRequest) (*DeployResponse, error) {
return nil, status.Error(codes.Unimplemented, "method Deploy not implemented") return nil, status.Error(codes.Unimplemented, "method Deploy not implemented")
} }
func (UnimplementedMcpAgentServiceServer) UndeployService(context.Context, *UndeployServiceRequest) (*UndeployServiceResponse, error) {
return nil, status.Error(codes.Unimplemented, "method UndeployService not implemented")
}
func (UnimplementedMcpAgentServiceServer) StopService(context.Context, *StopServiceRequest) (*StopServiceResponse, error) { func (UnimplementedMcpAgentServiceServer) StopService(context.Context, *StopServiceRequest) (*StopServiceResponse, error) {
return nil, status.Error(codes.Unimplemented, "method StopService not implemented") return nil, status.Error(codes.Unimplemented, "method StopService not implemented")
} }
@@ -310,6 +326,24 @@ func _McpAgentService_Deploy_Handler(srv interface{}, ctx context.Context, dec f
return interceptor(ctx, in, info, handler) return interceptor(ctx, in, info, handler)
} }
func _McpAgentService_UndeployService_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(UndeployServiceRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(McpAgentServiceServer).UndeployService(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: McpAgentService_UndeployService_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(McpAgentServiceServer).UndeployService(ctx, req.(*UndeployServiceRequest))
}
return interceptor(ctx, in, info, handler)
}
func _McpAgentService_StopService_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { func _McpAgentService_StopService_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(StopServiceRequest) in := new(StopServiceRequest)
if err := dec(in); err != nil { if err := dec(in); err != nil {
@@ -537,6 +571,10 @@ var McpAgentService_ServiceDesc = grpc.ServiceDesc{
MethodName: "Deploy", MethodName: "Deploy",
Handler: _McpAgentService_Deploy_Handler, Handler: _McpAgentService_Deploy_Handler,
}, },
{
MethodName: "UndeployService",
Handler: _McpAgentService_UndeployService_Handler,
},
{ {
MethodName: "StopService", MethodName: "StopService",
Handler: _McpAgentService_StopService_Handler, Handler: _McpAgentService_StopService_Handler,

2
go.mod
View File

@@ -3,7 +3,7 @@ module git.wntrmute.dev/mc/mcp
go 1.25.7 go 1.25.7
require ( require (
git.wntrmute.dev/mc/mc-proxy v1.1.0 git.wntrmute.dev/mc/mc-proxy v1.2.0
github.com/pelletier/go-toml/v2 v2.3.0 github.com/pelletier/go-toml/v2 v2.3.0
github.com/spf13/cobra v1.10.2 github.com/spf13/cobra v1.10.2
golang.org/x/sys v0.42.0 golang.org/x/sys v0.42.0

4
go.sum
View File

@@ -1,5 +1,5 @@
git.wntrmute.dev/mc/mc-proxy v1.1.0 h1:r8LnBuiS0OqLSuHMuRikQlIhmeNtlJV9IrIvVVTIGuw= git.wntrmute.dev/mc/mc-proxy v1.2.0 h1:TVfwdZzYqMs/ksZ0a6aSR7hKGDDMG8X0Od5RIxlbXKQ=
git.wntrmute.dev/mc/mc-proxy v1.1.0/go.mod h1:6w8smZ/DNJVBb4n5std/faye0ROLEXfk3iJY1XNc1JU= git.wntrmute.dev/mc/mc-proxy v1.2.0/go.mod h1:6w8smZ/DNJVBb4n5std/faye0ROLEXfk3iJY1XNc1JU=
git.wntrmute.dev/mc/mcdsl v1.2.0 h1:41hep7/PNZJfN0SN/nM+rQpyF1GSZcvNNjyVG81DI7U= git.wntrmute.dev/mc/mcdsl v1.2.0 h1:41hep7/PNZJfN0SN/nM+rQpyF1GSZcvNNjyVG81DI7U=
git.wntrmute.dev/mc/mcdsl v1.2.0/go.mod h1:lXYrAt74ZUix6rx9oVN8d2zH1YJoyp4uxPVKQ+SSxuM= git.wntrmute.dev/mc/mcdsl v1.2.0/go.mod h1:lXYrAt74ZUix6rx9oVN8d2zH1YJoyp4uxPVKQ+SSxuM=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=

View File

@@ -33,6 +33,8 @@ type Agent struct {
Logger *slog.Logger Logger *slog.Logger
PortAlloc *PortAllocator PortAlloc *PortAllocator
Proxy *ProxyRouter Proxy *ProxyRouter
Certs *CertProvisioner
DNS *DNSRegistrar
} }
// Run starts the agent: opens the database, sets up the gRPC server with // Run starts the agent: opens the database, sets up the gRPC server with
@@ -57,6 +59,16 @@ func Run(cfg *config.AgentConfig) error {
return fmt.Errorf("connect to mc-proxy: %w", err) return fmt.Errorf("connect to mc-proxy: %w", err)
} }
certs, err := NewCertProvisioner(cfg.Metacrypt, cfg.MCProxy.CertDir, logger)
if err != nil {
return fmt.Errorf("create cert provisioner: %w", err)
}
dns, err := NewDNSRegistrar(cfg.MCNS, logger)
if err != nil {
return fmt.Errorf("create DNS registrar: %w", err)
}
a := &Agent{ a := &Agent{
Config: cfg, Config: cfg,
DB: db, DB: db,
@@ -65,6 +77,8 @@ func Run(cfg *config.AgentConfig) error {
Logger: logger, Logger: logger,
PortAlloc: NewPortAllocator(), PortAlloc: NewPortAllocator(),
Proxy: proxy, Proxy: proxy,
Certs: certs,
DNS: dns,
} }
tlsCert, err := tls.LoadX509KeyPair(cfg.Server.TLSCert, cfg.Server.TLSKey) tlsCert, err := tls.LoadX509KeyPair(cfg.Server.TLSCert, cfg.Server.TLSKey)

263
internal/agent/certs.go Normal file
View File

@@ -0,0 +1,263 @@
package agent
import (
"bytes"
"context"
"crypto/tls"
"crypto/x509"
"encoding/json"
"encoding/pem"
"fmt"
"io"
"log/slog"
"net/http"
"os"
"path/filepath"
"strings"
"time"
"git.wntrmute.dev/mc/mcp/internal/auth"
"git.wntrmute.dev/mc/mcp/internal/config"
)
// renewWindow is how far before expiry a cert is considered stale and
// should be re-issued.
const renewWindow = 30 * 24 * time.Hour // 30 days
// CertProvisioner requests TLS certificates from Metacrypt's CA API
// and writes them to the mc-proxy cert directory. It is nil-safe: all
// methods are no-ops when the receiver is nil.
type CertProvisioner struct {
serverURL string
token string
mount string
issuer string
certDir string
httpClient *http.Client
logger *slog.Logger
}
// NewCertProvisioner creates a CertProvisioner. Returns (nil, nil) if
// cfg.ServerURL is empty (cert provisioning disabled).
func NewCertProvisioner(cfg config.MetacryptConfig, certDir string, logger *slog.Logger) (*CertProvisioner, error) {
if cfg.ServerURL == "" {
logger.Info("metacrypt not configured, cert provisioning disabled")
return nil, nil
}
token, err := auth.LoadToken(cfg.TokenPath)
if err != nil {
return nil, fmt.Errorf("load metacrypt token: %w", err)
}
httpClient, err := newTLSClient(cfg.CACert)
if err != nil {
return nil, fmt.Errorf("create metacrypt HTTP client: %w", err)
}
logger.Info("metacrypt cert provisioner enabled", "server", cfg.ServerURL, "mount", cfg.Mount, "issuer", cfg.Issuer)
return &CertProvisioner{
serverURL: strings.TrimRight(cfg.ServerURL, "/"),
token: token,
mount: cfg.Mount,
issuer: cfg.Issuer,
certDir: certDir,
httpClient: httpClient,
logger: logger,
}, nil
}
// EnsureCert checks whether a valid TLS certificate exists for the
// service. If the cert is missing or near expiry, it requests a new
// one from Metacrypt.
func (p *CertProvisioner) EnsureCert(ctx context.Context, serviceName string, hostnames []string) error {
if p == nil || len(hostnames) == 0 {
return nil
}
certPath := filepath.Join(p.certDir, serviceName+".pem")
if remaining, ok := certTimeRemaining(certPath); ok {
if remaining > renewWindow {
p.logger.Debug("cert valid, skipping provisioning",
"service", serviceName,
"expires_in", remaining.Round(time.Hour),
)
return nil
}
p.logger.Info("cert near expiry, re-issuing",
"service", serviceName,
"expires_in", remaining.Round(time.Hour),
)
}
return p.issueCert(ctx, serviceName, hostnames[0], hostnames)
}
// issueCert calls Metacrypt's CA API to issue a certificate and writes
// the chain and key to the cert directory.
func (p *CertProvisioner) issueCert(ctx context.Context, serviceName, commonName string, dnsNames []string) error {
p.logger.Info("provisioning TLS cert",
"service", serviceName,
"cn", commonName,
"sans", dnsNames,
)
reqBody := map[string]interface{}{
"mount": p.mount,
"operation": "issue",
"data": map[string]interface{}{
"issuer": p.issuer,
"common_name": commonName,
"dns_names": dnsNames,
"profile": "server",
"ttl": "2160h",
},
}
body, err := json.Marshal(reqBody)
if err != nil {
return fmt.Errorf("marshal issue request: %w", err)
}
url := p.serverURL + "/v1/engine/request"
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body))
if err != nil {
return fmt.Errorf("create issue request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+p.token)
resp, err := p.httpClient.Do(req)
if err != nil {
return fmt.Errorf("issue cert: %w", err)
}
defer func() { _ = resp.Body.Close() }()
respBody, err := io.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("read issue response: %w", err)
}
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("issue cert: metacrypt returned %d: %s", resp.StatusCode, string(respBody))
}
var result struct {
ChainPEM string `json:"chain_pem"`
KeyPEM string `json:"key_pem"`
Serial string `json:"serial"`
ExpiresAt string `json:"expires_at"`
}
if err := json.Unmarshal(respBody, &result); err != nil {
return fmt.Errorf("parse issue response: %w", err)
}
if result.ChainPEM == "" || result.KeyPEM == "" {
return fmt.Errorf("issue cert: response missing chain_pem or key_pem")
}
// Write cert and key atomically (temp file + rename).
certPath := filepath.Join(p.certDir, serviceName+".pem")
keyPath := filepath.Join(p.certDir, serviceName+".key")
if err := atomicWrite(certPath, []byte(result.ChainPEM), 0644); err != nil {
return fmt.Errorf("write cert: %w", err)
}
if err := atomicWrite(keyPath, []byte(result.KeyPEM), 0600); err != nil {
return fmt.Errorf("write key: %w", err)
}
p.logger.Info("cert provisioned",
"service", serviceName,
"serial", result.Serial,
"expires_at", result.ExpiresAt,
)
return nil
}
// RemoveCert removes TLS certificate and key files for a service.
func (p *CertProvisioner) RemoveCert(serviceName string) error {
if p == nil {
return nil
}
certPath := filepath.Join(p.certDir, serviceName+".pem")
keyPath := filepath.Join(p.certDir, serviceName+".key")
for _, path := range []string{certPath, keyPath} {
if err := os.Remove(path); err != nil && !os.IsNotExist(err) {
return fmt.Errorf("remove %s: %w", path, err)
}
}
p.logger.Info("cert removed", "service", serviceName)
return nil
}
// certTimeRemaining returns the time until the leaf certificate at
// path expires. Returns (0, false) if the cert cannot be read or parsed.
func certTimeRemaining(path string) (time.Duration, bool) {
data, err := os.ReadFile(path) //nolint:gosec // path from trusted config
if err != nil {
return 0, false
}
block, _ := pem.Decode(data)
if block == nil {
return 0, false
}
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
return 0, false
}
remaining := time.Until(cert.NotAfter)
if remaining <= 0 {
return 0, true // expired
}
return remaining, true
}
// atomicWrite writes data to a temporary file then renames it to path,
// ensuring readers never see a partial file.
func atomicWrite(path string, data []byte, perm os.FileMode) error {
tmp := path + ".tmp"
if err := os.WriteFile(tmp, data, perm); err != nil {
return fmt.Errorf("write %s: %w", tmp, err)
}
if err := os.Rename(tmp, path); err != nil {
_ = os.Remove(tmp)
return fmt.Errorf("rename %s -> %s: %w", tmp, path, err)
}
return nil
}
// newTLSClient creates an HTTP client with TLS 1.3 minimum. If
// caCertPath is non-empty, the CA certificate is loaded into the
// root CA pool.
func newTLSClient(caCertPath string) (*http.Client, error) {
tlsConfig := &tls.Config{
MinVersion: tls.VersionTLS13,
}
if caCertPath != "" {
caCert, err := os.ReadFile(caCertPath) //nolint:gosec // path from trusted config
if err != nil {
return nil, fmt.Errorf("read CA cert %q: %w", caCertPath, err)
}
pool := x509.NewCertPool()
if !pool.AppendCertsFromPEM(caCert) {
return nil, fmt.Errorf("parse CA cert %q: no valid certificates found", caCertPath)
}
tlsConfig.RootCAs = pool
}
return &http.Client{
Timeout: 30 * time.Second,
Transport: &http.Transport{
TLSClientConfig: tlsConfig,
},
}, nil
}

View File

@@ -0,0 +1,392 @@
package agent
import (
"context"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/x509"
"crypto/x509/pkix"
"encoding/json"
"encoding/pem"
"log/slog"
"math/big"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"testing"
"time"
"git.wntrmute.dev/mc/mcp/internal/config"
"git.wntrmute.dev/mc/mcp/internal/registry"
)
func TestNilCertProvisionerIsNoop(t *testing.T) {
var p *CertProvisioner
if err := p.EnsureCert(context.Background(), "svc", []string{"svc.example.com"}); err != nil {
t.Fatalf("EnsureCert on nil: %v", err)
}
}
func TestNewCertProvisionerDisabledWhenUnconfigured(t *testing.T) {
p, err := NewCertProvisioner(config.MetacryptConfig{}, "/tmp", slog.Default())
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if p != nil {
t.Fatal("expected nil provisioner for empty config")
}
}
func TestEnsureCertSkipsValidCert(t *testing.T) {
certDir := t.TempDir()
certPath := filepath.Join(certDir, "svc.pem")
keyPath := filepath.Join(certDir, "svc.key")
// Generate a cert that expires in 90 days.
writeSelfSignedCert(t, certPath, keyPath, "svc.example.com", 90*24*time.Hour)
// Create a provisioner that would fail if it tried to issue.
p := &CertProvisioner{
serverURL: "https://will-fail-if-called:9999",
certDir: certDir,
logger: slog.Default(),
}
if err := p.EnsureCert(context.Background(), "svc", []string{"svc.example.com"}); err != nil {
t.Fatalf("EnsureCert: %v", err)
}
}
func TestEnsureCertReissuesExpiring(t *testing.T) {
certDir := t.TempDir()
certPath := filepath.Join(certDir, "svc.pem")
keyPath := filepath.Join(certDir, "svc.key")
// Generate a cert that expires in 10 days (within 30-day renewal window).
writeSelfSignedCert(t, certPath, keyPath, "svc.example.com", 10*24*time.Hour)
// Mock Metacrypt API.
newCert, newKey := generateCertPEM(t, "svc.example.com", 90*24*time.Hour)
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
resp := map[string]string{
"chain_pem": newCert,
"key_pem": newKey,
"serial": "abc123",
"expires_at": time.Now().Add(90 * 24 * time.Hour).Format(time.RFC3339),
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(resp)
}))
defer srv.Close()
p := &CertProvisioner{
serverURL: srv.URL,
token: "test-token",
mount: "pki",
issuer: "infra",
certDir: certDir,
httpClient: srv.Client(),
logger: slog.Default(),
}
if err := p.EnsureCert(context.Background(), "svc", []string{"svc.example.com"}); err != nil {
t.Fatalf("EnsureCert: %v", err)
}
// Verify new cert was written.
got, err := os.ReadFile(certPath)
if err != nil {
t.Fatalf("read cert: %v", err)
}
if string(got) != newCert {
t.Fatal("cert file was not updated with new cert")
}
}
func TestIssueCertWritesFiles(t *testing.T) {
certDir := t.TempDir()
// Mock Metacrypt API.
certPEM, keyPEM := generateCertPEM(t, "svc.example.com", 90*24*time.Hour)
var gotAuth string
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
gotAuth = r.Header.Get("Authorization")
var req map[string]interface{}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "bad request", http.StatusBadRequest)
return
}
// Verify request structure.
if req["mount"] != "pki" || req["operation"] != "issue" {
t.Errorf("unexpected request: %v", req)
}
resp := map[string]string{
"chain_pem": certPEM,
"key_pem": keyPEM,
"serial": "deadbeef",
"expires_at": time.Now().Add(90 * 24 * time.Hour).Format(time.RFC3339),
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(resp)
}))
defer srv.Close()
p := &CertProvisioner{
serverURL: srv.URL,
token: "my-service-token",
mount: "pki",
issuer: "infra",
certDir: certDir,
httpClient: srv.Client(),
logger: slog.Default(),
}
if err := p.EnsureCert(context.Background(), "svc", []string{"svc.example.com"}); err != nil {
t.Fatalf("EnsureCert: %v", err)
}
// Verify auth header.
if gotAuth != "Bearer my-service-token" {
t.Fatalf("auth header: got %q, want %q", gotAuth, "Bearer my-service-token")
}
// Verify cert file.
certData, err := os.ReadFile(filepath.Join(certDir, "svc.pem"))
if err != nil {
t.Fatalf("read cert: %v", err)
}
if string(certData) != certPEM {
t.Fatal("cert content mismatch")
}
// Verify key file.
keyData, err := os.ReadFile(filepath.Join(certDir, "svc.key"))
if err != nil {
t.Fatalf("read key: %v", err)
}
if string(keyData) != keyPEM {
t.Fatal("key content mismatch")
}
// Verify key file permissions.
info, err := os.Stat(filepath.Join(certDir, "svc.key"))
if err != nil {
t.Fatalf("stat key: %v", err)
}
if perm := info.Mode().Perm(); perm != 0600 {
t.Fatalf("key permissions: got %o, want 0600", perm)
}
}
func TestIssueCertAPIError(t *testing.T) {
certDir := t.TempDir()
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.Error(w, `{"error":"sealed"}`, http.StatusServiceUnavailable)
}))
defer srv.Close()
p := &CertProvisioner{
serverURL: srv.URL,
token: "test-token",
mount: "pki",
issuer: "infra",
certDir: certDir,
httpClient: srv.Client(),
logger: slog.Default(),
}
err := p.EnsureCert(context.Background(), "svc", []string{"svc.example.com"})
if err == nil {
t.Fatal("expected error for sealed metacrypt")
}
}
func TestCertTimeRemaining(t *testing.T) {
t.Run("missing file", func(t *testing.T) {
if _, ok := certTimeRemaining("/nonexistent/cert.pem"); ok {
t.Fatal("expected false for missing file")
}
})
t.Run("valid cert", func(t *testing.T) {
certDir := t.TempDir()
path := filepath.Join(certDir, "test.pem")
writeSelfSignedCert(t, path, filepath.Join(certDir, "test.key"), "test.example.com", 90*24*time.Hour)
remaining, ok := certTimeRemaining(path)
if !ok {
t.Fatal("expected true for valid cert")
}
// Should be close to 90 days.
if remaining < 89*24*time.Hour || remaining > 91*24*time.Hour {
t.Fatalf("remaining: got %v, want ~90 days", remaining)
}
})
t.Run("expired cert", func(t *testing.T) {
certDir := t.TempDir()
path := filepath.Join(certDir, "expired.pem")
// Write a cert that's already expired (valid from -2h to -1h).
writeExpiredCert(t, path, filepath.Join(certDir, "expired.key"), "expired.example.com")
remaining, ok := certTimeRemaining(path)
if !ok {
t.Fatal("expected true for expired cert")
}
if remaining > 0 {
t.Fatalf("remaining: got %v, want <= 0", remaining)
}
})
}
func TestHasL7Routes(t *testing.T) {
tests := []struct {
name string
routes []registry.Route
want bool
}{
{"nil", nil, false},
{"empty", []registry.Route{}, false},
{"l4 only", []registry.Route{{Mode: "l4"}}, false},
{"l7 only", []registry.Route{{Mode: "l7"}}, true},
{"mixed", []registry.Route{{Mode: "l4"}, {Mode: "l7"}}, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := hasL7Routes(tt.routes); got != tt.want {
t.Fatalf("hasL7Routes = %v, want %v", got, tt.want)
}
})
}
}
func TestL7Hostnames(t *testing.T) {
routes := []registry.Route{
{Mode: "l7", Hostname: ""},
{Mode: "l4", Hostname: "ignored.example.com"},
{Mode: "l7", Hostname: "custom.example.com"},
{Mode: "l7", Hostname: ""}, // duplicate default
}
got := l7Hostnames("myservice", routes)
want := []string{"myservice.svc.mcp.metacircular.net", "custom.example.com"}
if len(got) != len(want) {
t.Fatalf("got %v, want %v", got, want)
}
for i := range want {
if got[i] != want[i] {
t.Fatalf("got[%d] = %q, want %q", i, got[i], want[i])
}
}
}
func TestAtomicWrite(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "test.txt")
if err := atomicWrite(path, []byte("hello"), 0644); err != nil {
t.Fatalf("atomicWrite: %v", err)
}
data, err := os.ReadFile(path)
if err != nil {
t.Fatalf("read: %v", err)
}
if string(data) != "hello" {
t.Fatalf("got %q, want %q", string(data), "hello")
}
// Verify no .tmp file left behind.
if _, err := os.Stat(path + ".tmp"); !os.IsNotExist(err) {
t.Fatal("temp file should not exist after atomic write")
}
}
// --- test helpers ---
// writeSelfSignedCert generates a self-signed cert/key and writes them to disk.
func writeSelfSignedCert(t *testing.T, certPath, keyPath, hostname string, validity time.Duration) {
t.Helper()
certPEM, keyPEM := generateCertPEM(t, hostname, validity)
if err := os.WriteFile(certPath, []byte(certPEM), 0644); err != nil {
t.Fatalf("write cert: %v", err)
}
if err := os.WriteFile(keyPath, []byte(keyPEM), 0600); err != nil {
t.Fatalf("write key: %v", err)
}
}
// writeExpiredCert generates a cert that is already expired.
func writeExpiredCert(t *testing.T, certPath, keyPath, hostname string) {
t.Helper()
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
t.Fatalf("generate key: %v", err)
}
tmpl := &x509.Certificate{
SerialNumber: big.NewInt(1),
Subject: pkix.Name{CommonName: hostname},
DNSNames: []string{hostname},
NotBefore: time.Now().Add(-2 * time.Hour),
NotAfter: time.Now().Add(-1 * time.Hour),
}
der, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, &key.PublicKey, key)
if err != nil {
t.Fatalf("create cert: %v", err)
}
certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der})
keyDER, err := x509.MarshalECPrivateKey(key)
if err != nil {
t.Fatalf("marshal key: %v", err)
}
keyPEM := pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: keyDER})
if err := os.WriteFile(certPath, certPEM, 0644); err != nil {
t.Fatalf("write cert: %v", err)
}
if err := os.WriteFile(keyPath, keyPEM, 0600); err != nil {
t.Fatalf("write key: %v", err)
}
}
// generateCertPEM generates a self-signed cert and returns PEM strings.
func generateCertPEM(t *testing.T, hostname string, validity time.Duration) (certPEM, keyPEM string) {
t.Helper()
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
t.Fatalf("generate key: %v", err)
}
tmpl := &x509.Certificate{
SerialNumber: big.NewInt(1),
Subject: pkix.Name{CommonName: hostname},
DNSNames: []string{hostname},
NotBefore: time.Now().Add(-1 * time.Hour),
NotAfter: time.Now().Add(validity),
}
der, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, &key.PublicKey, key)
if err != nil {
t.Fatalf("create cert: %v", err)
}
certBlock := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der})
keyDER, err := x509.MarshalECPrivateKey(key)
if err != nil {
t.Fatalf("marshal key: %v", err)
}
keyBlock := pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: keyDER})
return string(certBlock), string(keyBlock)
}

View File

@@ -146,6 +146,14 @@ func (a *Agent) deployComponent(ctx context.Context, serviceName string, cs *mcp
} }
} }
// Provision TLS certs for L7 routes before registering with mc-proxy.
if a.Certs != nil && hasL7Routes(regRoutes) {
hostnames := l7Hostnames(serviceName, regRoutes)
if err := a.Certs.EnsureCert(ctx, serviceName, hostnames); err != nil {
a.Logger.Warn("failed to provision TLS cert", "service", serviceName, "err", err)
}
}
// Register routes with mc-proxy after the container is running. // Register routes with mc-proxy after the container is running.
if len(regRoutes) > 0 && a.Proxy != nil { if len(regRoutes) > 0 && a.Proxy != nil {
hostPorts, err := registry.GetRouteHostPorts(a.DB, serviceName, compName) hostPorts, err := registry.GetRouteHostPorts(a.DB, serviceName, compName)
@@ -156,6 +164,13 @@ func (a *Agent) deployComponent(ctx context.Context, serviceName string, cs *mcp
} }
} }
// Register DNS record for the service.
if a.DNS != nil && len(regRoutes) > 0 {
if err := a.DNS.EnsureRecord(ctx, serviceName); err != nil {
a.Logger.Warn("failed to register DNS record", "service", serviceName, "err", err)
}
}
if err := registry.UpdateComponentState(a.DB, serviceName, compName, "running", "running"); err != nil { if err := registry.UpdateComponentState(a.DB, serviceName, compName, "running", "running"); err != nil {
a.Logger.Warn("failed to update component state", "service", serviceName, "component", compName, "err", err) a.Logger.Warn("failed to update component state", "service", serviceName, "component", compName, "err", err)
} }
@@ -183,7 +198,10 @@ func (a *Agent) allocateRoutePorts(service, component string, routes []registry.
return nil, nil, fmt.Errorf("store host port for route %q: %w", r.Name, err) return nil, nil, fmt.Errorf("store host port for route %q: %w", r.Name, err)
} }
ports = append(ports, fmt.Sprintf("127.0.0.1:%d:%d", hostPort, r.Port)) // The container port must match hostPort (which is also set as $PORT),
// so the app's listen address matches the podman port mapping.
// r.Port is the mc-proxy listener port, NOT the container port.
ports = append(ports, fmt.Sprintf("127.0.0.1:%d:%d", hostPort, hostPort))
if len(routes) == 1 { if len(routes) == 1 {
env = append(env, fmt.Sprintf("PORT=%d", hostPort)) env = append(env, fmt.Sprintf("PORT=%d", hostPort))
@@ -209,6 +227,37 @@ func ensureService(db *sql.DB, name string, active bool) error {
return registry.UpdateServiceActive(db, name, active) return registry.UpdateServiceActive(db, name, active)
} }
// hasL7Routes reports whether any route uses L7 (TLS-terminating) mode.
func hasL7Routes(routes []registry.Route) bool {
for _, r := range routes {
if r.Mode == "l7" {
return true
}
}
return false
}
// l7Hostnames returns the unique hostnames from L7 routes, applying
// the default hostname convention when a route has no explicit hostname.
func l7Hostnames(serviceName string, routes []registry.Route) []string {
seen := make(map[string]bool)
var hostnames []string
for _, r := range routes {
if r.Mode != "l7" {
continue
}
h := r.Hostname
if h == "" {
h = serviceName + ".svc.mcp.metacircular.net"
}
if !seen[h] {
seen[h] = true
hostnames = append(hostnames, h)
}
}
return hostnames
}
// ensureComponent creates the component if it does not exist, or updates its // ensureComponent creates the component if it does not exist, or updates its
// spec if it does. // spec if it does.
func ensureComponent(db *sql.DB, c *registry.Component) error { func ensureComponent(db *sql.DB, c *registry.Component) error {

View File

@@ -0,0 +1,159 @@
package agent
import (
"database/sql"
"fmt"
"log/slog"
"os"
"path/filepath"
"testing"
"git.wntrmute.dev/mc/mcp/internal/registry"
)
func openTestDB(t *testing.T) *sql.DB {
t.Helper()
db, err := registry.Open(filepath.Join(t.TempDir(), "test.db"))
if err != nil {
t.Fatalf("open db: %v", err)
}
t.Cleanup(func() { _ = db.Close() })
return db
}
func testAgent(t *testing.T) *Agent {
t.Helper()
return &Agent{
DB: openTestDB(t),
PortAlloc: NewPortAllocator(),
Logger: slog.New(slog.NewTextHandler(os.Stderr, nil)),
}
}
// seedComponent creates the service and component in the registry so that
// allocateRoutePorts can store host ports for it.
func seedComponent(t *testing.T, db *sql.DB, service, component string, routes []registry.Route) {
t.Helper()
if err := registry.CreateService(db, service, true); err != nil {
t.Fatalf("create service: %v", err)
}
if err := registry.CreateComponent(db, &registry.Component{
Name: component,
Service: service,
Image: "img:latest",
DesiredState: "running",
ObservedState: "unknown",
Routes: routes,
}); err != nil {
t.Fatalf("create component: %v", err)
}
}
func TestAllocateRoutePorts_SingleRoute(t *testing.T) {
a := testAgent(t)
routes := []registry.Route{
{Name: "default", Port: 443, Mode: "l7"},
}
seedComponent(t, a.DB, "mcdoc", "mcdoc", routes)
ports, env, err := a.allocateRoutePorts("mcdoc", "mcdoc", routes)
if err != nil {
t.Fatalf("allocateRoutePorts: %v", err)
}
if len(ports) != 1 {
t.Fatalf("expected 1 port mapping, got %d", len(ports))
}
if len(env) != 1 {
t.Fatalf("expected 1 env var, got %d", len(env))
}
// Parse the port mapping: should be "127.0.0.1:<hostPort>:<hostPort>"
// NOT "127.0.0.1:<hostPort>:443"
var hostPort, containerPort int
n, _ := fmt.Sscanf(ports[0], "127.0.0.1:%d:%d", &hostPort, &containerPort)
if n != 2 {
t.Fatalf("failed to parse port mapping %q", ports[0])
}
if hostPort != containerPort {
t.Errorf("host port (%d) != container port (%d); container port must match host port for $PORT consistency", hostPort, containerPort)
}
// Env var should be PORT=<hostPort>
var envPort int
n, _ = fmt.Sscanf(env[0], "PORT=%d", &envPort)
if n != 1 {
t.Fatalf("failed to parse env var %q", env[0])
}
if envPort != hostPort {
t.Errorf("PORT env (%d) != host port (%d)", envPort, hostPort)
}
}
func TestAllocateRoutePorts_MultiRoute(t *testing.T) {
a := testAgent(t)
routes := []registry.Route{
{Name: "rest", Port: 8443, Mode: "l4"},
{Name: "grpc", Port: 9443, Mode: "l4"},
}
seedComponent(t, a.DB, "metacrypt", "api", routes)
ports, env, err := a.allocateRoutePorts("metacrypt", "api", routes)
if err != nil {
t.Fatalf("allocateRoutePorts: %v", err)
}
if len(ports) != 2 {
t.Fatalf("expected 2 port mappings, got %d", len(ports))
}
if len(env) != 2 {
t.Fatalf("expected 2 env vars, got %d", len(env))
}
// Each port mapping should have host port == container port.
for i, p := range ports {
var hp, cp int
n, _ := fmt.Sscanf(p, "127.0.0.1:%d:%d", &hp, &cp)
if n != 2 {
t.Fatalf("port[%d]: failed to parse %q", i, p)
}
if hp != cp {
t.Errorf("port[%d]: host port (%d) != container port (%d)", i, hp, cp)
}
}
// Env vars should be PORT_REST and PORT_GRPC (not bare PORT).
if env[0][:10] != "PORT_REST=" {
t.Errorf("env[0] = %q, want PORT_REST=...", env[0])
}
if env[1][:10] != "PORT_GRPC=" {
t.Errorf("env[1] = %q, want PORT_GRPC=...", env[1])
}
}
func TestAllocateRoutePorts_L7PortNotUsedAsContainerPort(t *testing.T) {
a := testAgent(t)
routes := []registry.Route{
{Name: "default", Port: 443, Mode: "l7"},
}
seedComponent(t, a.DB, "svc", "web", routes)
ports, _, err := a.allocateRoutePorts("svc", "web", routes)
if err != nil {
t.Fatalf("allocateRoutePorts: %v", err)
}
// The container port must NOT be 443 (the mc-proxy listener port).
// It must be the host port (which is in range 10000-60000).
var hostPort, containerPort int
n, _ := fmt.Sscanf(ports[0], "127.0.0.1:%d:%d", &hostPort, &containerPort)
if n != 2 {
t.Fatalf("failed to parse port mapping %q", ports[0])
}
if containerPort == 443 {
t.Errorf("container port is 443 (mc-proxy listener); should be %d (host port)", hostPort)
}
if containerPort < portRangeMin || containerPort >= portRangeMax {
t.Errorf("container port %d outside allocation range [%d, %d)", containerPort, portRangeMin, portRangeMax)
}
}

265
internal/agent/dns.go Normal file
View File

@@ -0,0 +1,265 @@
package agent
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"log/slog"
"net/http"
"strings"
"git.wntrmute.dev/mc/mcp/internal/auth"
"git.wntrmute.dev/mc/mcp/internal/config"
)
// DNSRegistrar creates and removes A records in MCNS during deploy
// and stop. It is nil-safe: all methods are no-ops when the receiver
// is nil.
type DNSRegistrar struct {
serverURL string
token string
zone string
nodeAddr string
httpClient *http.Client
logger *slog.Logger
}
// dnsRecord is the JSON representation of an MCNS record.
type dnsRecord struct {
ID int `json:"ID"`
Name string `json:"Name"`
Type string `json:"Type"`
Value string `json:"Value"`
TTL int `json:"TTL"`
}
// NewDNSRegistrar creates a DNSRegistrar. Returns (nil, nil) if
// cfg.ServerURL is empty (DNS registration disabled).
func NewDNSRegistrar(cfg config.MCNSConfig, logger *slog.Logger) (*DNSRegistrar, error) {
if cfg.ServerURL == "" {
logger.Info("mcns not configured, DNS registration disabled")
return nil, nil
}
token, err := auth.LoadToken(cfg.TokenPath)
if err != nil {
return nil, fmt.Errorf("load mcns token: %w", err)
}
httpClient, err := newTLSClient(cfg.CACert)
if err != nil {
return nil, fmt.Errorf("create mcns HTTP client: %w", err)
}
logger.Info("mcns DNS registrar enabled", "server", cfg.ServerURL, "zone", cfg.Zone, "node_addr", cfg.NodeAddr)
return &DNSRegistrar{
serverURL: strings.TrimRight(cfg.ServerURL, "/"),
token: token,
zone: cfg.Zone,
nodeAddr: cfg.NodeAddr,
httpClient: httpClient,
logger: logger,
}, nil
}
// EnsureRecord ensures an A record exists for the service in the
// configured zone, pointing to the node's address.
func (d *DNSRegistrar) EnsureRecord(ctx context.Context, serviceName string) error {
if d == nil {
return nil
}
existing, err := d.listRecords(ctx, serviceName)
if err != nil {
return fmt.Errorf("list DNS records: %w", err)
}
// Check if any existing record already has the correct value.
for _, r := range existing {
if r.Value == d.nodeAddr {
d.logger.Debug("DNS record exists, skipping",
"service", serviceName,
"record", r.Name+"."+d.zone,
"value", r.Value,
)
return nil
}
}
// No record with the correct value — update the first one if it exists.
if len(existing) > 0 {
d.logger.Info("updating DNS record",
"service", serviceName,
"old_value", existing[0].Value,
"new_value", d.nodeAddr,
)
return d.updateRecord(ctx, existing[0].ID, serviceName)
}
// No existing record — create one.
d.logger.Info("creating DNS record",
"service", serviceName,
"record", serviceName+"."+d.zone,
"value", d.nodeAddr,
)
return d.createRecord(ctx, serviceName)
}
// RemoveRecord removes A records for the service from the configured zone.
func (d *DNSRegistrar) RemoveRecord(ctx context.Context, serviceName string) error {
if d == nil {
return nil
}
existing, err := d.listRecords(ctx, serviceName)
if err != nil {
return fmt.Errorf("list DNS records: %w", err)
}
if len(existing) == 0 {
d.logger.Debug("no DNS record to remove", "service", serviceName)
return nil
}
for _, r := range existing {
d.logger.Info("removing DNS record",
"service", serviceName,
"record", r.Name+"."+d.zone,
"id", r.ID,
)
if err := d.deleteRecord(ctx, r.ID); err != nil {
return err
}
}
return nil
}
// listRecords returns A records matching the service name in the zone.
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 {
return nil, fmt.Errorf("create list request: %w", err)
}
req.Header.Set("Authorization", "Bearer "+d.token)
resp, err := d.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("list records: %w", err)
}
defer func() { _ = resp.Body.Close() }()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("read list response: %w", err)
}
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("list 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 response: %w", err)
}
return envelope.Records, nil
}
// createRecord creates an A record in the zone.
func (d *DNSRegistrar) createRecord(ctx context.Context, serviceName string) error {
reqBody := map[string]interface{}{
"name": serviceName,
"type": "A",
"value": d.nodeAddr,
"ttl": 300,
}
body, err := json.Marshal(reqBody)
if err != nil {
return fmt.Errorf("marshal create request: %w", err)
}
url := fmt.Sprintf("%s/v1/zones/%s/records", d.serverURL, d.zone)
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body))
if err != nil {
return fmt.Errorf("create record request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+d.token)
resp, err := d.httpClient.Do(req)
if err != nil {
return fmt.Errorf("create record: %w", err)
}
defer func() { _ = resp.Body.Close() }()
if resp.StatusCode != http.StatusCreated && resp.StatusCode != http.StatusOK {
respBody, _ := io.ReadAll(resp.Body)
return fmt.Errorf("create record: mcns returned %d: %s", resp.StatusCode, string(respBody))
}
return nil
}
// updateRecord updates an existing record's value.
func (d *DNSRegistrar) updateRecord(ctx context.Context, recordID int, serviceName string) error {
reqBody := map[string]interface{}{
"name": serviceName,
"type": "A",
"value": d.nodeAddr,
"ttl": 300,
}
body, err := json.Marshal(reqBody)
if err != nil {
return fmt.Errorf("marshal update request: %w", err)
}
url := fmt.Sprintf("%s/v1/zones/%s/records/%d", d.serverURL, d.zone, recordID)
req, err := http.NewRequestWithContext(ctx, http.MethodPut, url, bytes.NewReader(body))
if err != nil {
return fmt.Errorf("create update request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+d.token)
resp, err := d.httpClient.Do(req)
if err != nil {
return fmt.Errorf("update record: %w", err)
}
defer func() { _ = resp.Body.Close() }()
if resp.StatusCode != http.StatusOK {
respBody, _ := io.ReadAll(resp.Body)
return fmt.Errorf("update record: mcns returned %d: %s", resp.StatusCode, string(respBody))
}
return nil
}
// deleteRecord deletes a record by ID.
func (d *DNSRegistrar) deleteRecord(ctx context.Context, recordID int) error {
url := fmt.Sprintf("%s/v1/zones/%s/records/%d", d.serverURL, d.zone, recordID)
req, err := http.NewRequestWithContext(ctx, http.MethodDelete, url, nil)
if err != nil {
return fmt.Errorf("create delete request: %w", err)
}
req.Header.Set("Authorization", "Bearer "+d.token)
resp, err := d.httpClient.Do(req)
if err != nil {
return fmt.Errorf("delete record: %w", err)
}
defer func() { _ = resp.Body.Close() }()
if resp.StatusCode != http.StatusNoContent && resp.StatusCode != http.StatusOK {
respBody, _ := io.ReadAll(resp.Body)
return fmt.Errorf("delete record: mcns returned %d: %s", resp.StatusCode, string(respBody))
}
return nil
}

214
internal/agent/dns_test.go Normal file
View File

@@ -0,0 +1,214 @@
package agent
import (
"context"
"encoding/json"
"log/slog"
"net/http"
"net/http/httptest"
"testing"
"git.wntrmute.dev/mc/mcp/internal/config"
)
func TestNilDNSRegistrarIsNoop(t *testing.T) {
var d *DNSRegistrar
if err := d.EnsureRecord(context.Background(), "svc"); err != nil {
t.Fatalf("EnsureRecord on nil: %v", err)
}
if err := d.RemoveRecord(context.Background(), "svc"); err != nil {
t.Fatalf("RemoveRecord on nil: %v", err)
}
}
func TestNewDNSRegistrarDisabledWhenUnconfigured(t *testing.T) {
d, err := NewDNSRegistrar(config.MCNSConfig{}, slog.Default())
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if d != nil {
t.Fatal("expected nil registrar for empty config")
}
}
func TestEnsureRecordCreatesWhenMissing(t *testing.T) {
var gotMethod, gotPath, gotAuth string
var gotBody map[string]interface{}
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodGet {
// List returns empty — no existing records.
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"records":[]}`))
return
}
gotMethod = r.Method
gotPath = r.URL.Path
gotAuth = r.Header.Get("Authorization")
_ = json.NewDecoder(r.Body).Decode(&gotBody)
w.WriteHeader(http.StatusCreated)
_, _ = w.Write([]byte(`{"id":1}`))
}))
defer srv.Close()
d := &DNSRegistrar{
serverURL: srv.URL,
token: "test-token",
zone: "svc.mcp.metacircular.net",
nodeAddr: "192.168.88.181",
httpClient: srv.Client(),
logger: slog.Default(),
}
if err := d.EnsureRecord(context.Background(), "myservice"); err != nil {
t.Fatalf("EnsureRecord: %v", err)
}
if gotMethod != http.MethodPost {
t.Fatalf("method: got %q, want POST", gotMethod)
}
if gotPath != "/v1/zones/svc.mcp.metacircular.net/records" {
t.Fatalf("path: got %q", gotPath)
}
if gotAuth != "Bearer test-token" {
t.Fatalf("auth: got %q", gotAuth)
}
if gotBody["name"] != "myservice" {
t.Fatalf("name: got %v", gotBody["name"])
}
if gotBody["type"] != "A" {
t.Fatalf("type: got %v", gotBody["type"])
}
if gotBody["value"] != "192.168.88.181" {
t.Fatalf("value: got %v", gotBody["value"])
}
}
func TestEnsureRecordSkipsWhenExists(t *testing.T) {
createCalled := false
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}}}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(resp)
return
}
createCalled = true
w.WriteHeader(http.StatusCreated)
}))
defer srv.Close()
d := &DNSRegistrar{
serverURL: srv.URL,
token: "test-token",
zone: "svc.mcp.metacircular.net",
nodeAddr: "192.168.88.181",
httpClient: srv.Client(),
logger: slog.Default(),
}
if err := d.EnsureRecord(context.Background(), "myservice"); err != nil {
t.Fatalf("EnsureRecord: %v", err)
}
if createCalled {
t.Fatal("should not create when record already exists with correct value")
}
}
func TestEnsureRecordUpdatesWrongValue(t *testing.T) {
var gotMethod string
var gotPath string
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}}}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(resp)
return
}
gotMethod = r.Method
gotPath = r.URL.Path
w.WriteHeader(http.StatusOK)
}))
defer srv.Close()
d := &DNSRegistrar{
serverURL: srv.URL,
token: "test-token",
zone: "svc.mcp.metacircular.net",
nodeAddr: "192.168.88.181",
httpClient: srv.Client(),
logger: slog.Default(),
}
if err := d.EnsureRecord(context.Background(), "myservice"); err != nil {
t.Fatalf("EnsureRecord: %v", err)
}
if gotMethod != http.MethodPut {
t.Fatalf("method: got %q, want PUT", gotMethod)
}
if gotPath != "/v1/zones/svc.mcp.metacircular.net/records/42" {
t.Fatalf("path: got %q", gotPath)
}
}
func TestRemoveRecordDeletes(t *testing.T) {
var gotMethod, gotPath string
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}}}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(resp)
return
}
gotMethod = r.Method
gotPath = r.URL.Path
w.WriteHeader(http.StatusNoContent)
}))
defer srv.Close()
d := &DNSRegistrar{
serverURL: srv.URL,
token: "test-token",
zone: "svc.mcp.metacircular.net",
nodeAddr: "192.168.88.181",
httpClient: srv.Client(),
logger: slog.Default(),
}
if err := d.RemoveRecord(context.Background(), "myservice"); err != nil {
t.Fatalf("RemoveRecord: %v", err)
}
if gotMethod != http.MethodDelete {
t.Fatalf("method: got %q, want DELETE", gotMethod)
}
if gotPath != "/v1/zones/svc.mcp.metacircular.net/records/7" {
t.Fatalf("path: got %q", gotPath)
}
}
func TestRemoveRecordNoopWhenMissing(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// List returns empty.
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"records":[]}`))
}))
defer srv.Close()
d := &DNSRegistrar{
serverURL: srv.URL,
token: "test-token",
zone: "svc.mcp.metacircular.net",
nodeAddr: "192.168.88.181",
httpClient: srv.Client(),
logger: slog.Default(),
}
if err := d.RemoveRecord(context.Background(), "myservice"); err != nil {
t.Fatalf("RemoveRecord: %v", err)
}
}

View File

@@ -37,6 +37,13 @@ func (a *Agent) StopService(ctx context.Context, req *mcpv1.StopServiceRequest)
} }
} }
// Remove DNS record when stopping the service.
if len(c.Routes) > 0 && a.DNS != nil {
if err := a.DNS.RemoveRecord(ctx, req.GetName()); err != nil {
a.Logger.Warn("failed to remove DNS record", "service", req.GetName(), "err", err)
}
}
if err := a.Runtime.Stop(ctx, containerName); err != nil { if err := a.Runtime.Stop(ctx, containerName); err != nil {
a.Logger.Info("stop container (ignored)", "container", containerName, "error", err) a.Logger.Info("stop container (ignored)", "container", containerName, "error", err)
} }

View File

@@ -33,8 +33,8 @@ func (pa *PortAllocator) Allocate() (int, error) {
pa.mu.Lock() pa.mu.Lock()
defer pa.mu.Unlock() defer pa.mu.Unlock()
for i := range maxRetries { for range maxRetries {
port := portRangeMin + rand.IntN(portRangeMax-portRangeMin) port := portRangeMin + rand.IntN(portRangeMax-portRangeMin) //nolint:gosec // port selection, not security
if pa.allocated[port] { if pa.allocated[port] {
continue continue
} }
@@ -45,7 +45,6 @@ func (pa *PortAllocator) Allocate() (int, error) {
pa.allocated[port] = true pa.allocated[port] = true
return port, nil return port, nil
_ = i
} }
return 0, fmt.Errorf("failed to allocate port after %d attempts", maxRetries) return 0, fmt.Errorf("failed to allocate port after %d attempts", maxRetries)

View File

@@ -157,6 +157,24 @@ func (a *Agent) reconcileUntracked(ctx context.Context, known map[string]bool) e
// protoToComponent converts a proto ComponentSpec to a registry Component. // protoToComponent converts a proto ComponentSpec to a registry Component.
func protoToComponent(service string, cs *mcpv1.ComponentSpec, desiredState string) *registry.Component { func protoToComponent(service string, cs *mcpv1.ComponentSpec, desiredState string) *registry.Component {
var routes []registry.Route
for _, r := range cs.GetRoutes() {
mode := r.GetMode()
if mode == "" {
mode = "l4"
}
name := r.GetName()
if name == "" {
name = "default"
}
routes = append(routes, registry.Route{
Name: name,
Port: int(r.GetPort()),
Mode: mode,
Hostname: r.GetHostname(),
})
}
return &registry.Component{ return &registry.Component{
Name: cs.GetName(), Name: cs.GetName(),
Service: service, Service: service,
@@ -167,6 +185,7 @@ func protoToComponent(service string, cs *mcpv1.ComponentSpec, desiredState stri
Ports: cs.GetPorts(), Ports: cs.GetPorts(),
Volumes: cs.GetVolumes(), Volumes: cs.GetVolumes(),
Cmd: cs.GetCmd(), Cmd: cs.GetCmd(),
Routes: routes,
DesiredState: desiredState, DesiredState: desiredState,
Version: runtime.ExtractVersion(cs.GetImage()), Version: runtime.ExtractVersion(cs.GetImage()),
} }

100
internal/agent/undeploy.go Normal file
View File

@@ -0,0 +1,100 @@
package agent
import (
"context"
"fmt"
mcpv1 "git.wntrmute.dev/mc/mcp/gen/mcp/v1"
"git.wntrmute.dev/mc/mcp/internal/registry"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
// UndeployService fully tears down a service: removes routes, DNS records,
// TLS certificates, stops and removes containers, releases ports, and marks
// the service inactive. This is the inverse of Deploy.
func (a *Agent) UndeployService(ctx context.Context, req *mcpv1.UndeployServiceRequest) (*mcpv1.UndeployServiceResponse, error) {
a.Logger.Info("UndeployService", "service", req.GetName())
if req.GetName() == "" {
return nil, status.Error(codes.InvalidArgument, "service name is required")
}
serviceName := req.GetName()
components, err := registry.ListComponents(a.DB, serviceName)
if err != nil {
return nil, status.Errorf(codes.Internal, "list components: %v", err)
}
var results []*mcpv1.ComponentResult
dnsRemoved := false
for _, c := range components {
r := a.undeployComponent(ctx, serviceName, &c, &dnsRemoved)
results = append(results, r)
}
// Mark the service as inactive.
if err := registry.UpdateServiceActive(a.DB, serviceName, false); err != nil {
a.Logger.Warn("failed to mark service inactive", "service", serviceName, "err", err)
}
return &mcpv1.UndeployServiceResponse{Results: results}, nil
}
// undeployComponent tears down a single component. The dnsRemoved flag
// tracks whether DNS has already been removed for this service (DNS is
// per-service, not per-component).
func (a *Agent) undeployComponent(ctx context.Context, serviceName string, c *registry.Component, dnsRemoved *bool) *mcpv1.ComponentResult {
containerName := ContainerNameFor(serviceName, c.Name)
r := &mcpv1.ComponentResult{Name: c.Name, Success: true}
// 1. Remove mc-proxy routes.
if len(c.Routes) > 0 && a.Proxy != nil {
if err := a.Proxy.RemoveRoutes(ctx, serviceName, c.Routes); err != nil {
a.Logger.Warn("failed to remove routes", "service", serviceName, "component", c.Name, "err", err)
}
}
// 2. Remove DNS records (once per service).
if len(c.Routes) > 0 && a.DNS != nil && !*dnsRemoved {
if err := a.DNS.RemoveRecord(ctx, serviceName); err != nil {
a.Logger.Warn("failed to remove DNS record", "service", serviceName, "err", err)
}
*dnsRemoved = true
}
// 3. Remove TLS certs (L7 routes only).
if hasL7Routes(c.Routes) && a.Certs != nil {
if err := a.Certs.RemoveCert(serviceName); err != nil {
a.Logger.Warn("failed to remove TLS cert", "service", serviceName, "err", err)
}
}
// 4. Stop and remove the container.
if err := a.Runtime.Stop(ctx, containerName); err != nil {
a.Logger.Info("stop container (ignored)", "container", containerName, "error", err)
}
if err := a.Runtime.Remove(ctx, containerName); err != nil {
a.Logger.Info("remove container (ignored)", "container", containerName, "error", err)
}
// 5. Release allocated ports.
if a.PortAlloc != nil {
hostPorts, err := registry.GetRouteHostPorts(a.DB, serviceName, c.Name)
if err == nil {
for _, port := range hostPorts {
a.PortAlloc.Release(port)
}
}
}
// 6. Update registry state.
if err := registry.UpdateComponentState(a.DB, serviceName, c.Name, "removed", "removed"); err != nil {
r.Success = false
r.Error = fmt.Sprintf("update state: %v", err)
}
return r
}

View File

@@ -15,10 +15,52 @@ type AgentConfig struct {
MCIAS MCIASConfig `toml:"mcias"` MCIAS MCIASConfig `toml:"mcias"`
Agent AgentSettings `toml:"agent"` Agent AgentSettings `toml:"agent"`
MCProxy MCProxyConfig `toml:"mcproxy"` MCProxy MCProxyConfig `toml:"mcproxy"`
Metacrypt MetacryptConfig `toml:"metacrypt"`
MCNS MCNSConfig `toml:"mcns"`
Monitor MonitorConfig `toml:"monitor"` Monitor MonitorConfig `toml:"monitor"`
Log LogConfig `toml:"log"` Log LogConfig `toml:"log"`
} }
// MetacryptConfig holds the Metacrypt CA integration settings for
// automated TLS cert provisioning. If ServerURL is empty, cert
// provisioning is disabled.
type MetacryptConfig struct {
// ServerURL is the Metacrypt API base URL (e.g. "https://metacrypt:8443").
ServerURL string `toml:"server_url"`
// CACert is the path to the CA certificate for verifying Metacrypt's TLS.
CACert string `toml:"ca_cert"`
// Mount is the CA engine mount name. Defaults to "pki".
Mount string `toml:"mount"`
// Issuer is the intermediate CA issuer name. Defaults to "infra".
Issuer string `toml:"issuer"`
// TokenPath is the path to the MCIAS service token file.
TokenPath string `toml:"token_path"`
}
// MCNSConfig holds the MCNS DNS integration settings for automated
// DNS record registration. If ServerURL is empty, DNS registration
// is disabled.
type MCNSConfig struct {
// ServerURL is the MCNS API base URL (e.g. "https://localhost:28443").
ServerURL string `toml:"server_url"`
// CACert is the path to the CA certificate for verifying MCNS's TLS.
CACert string `toml:"ca_cert"`
// TokenPath is the path to the MCIAS service token file.
TokenPath string `toml:"token_path"`
// Zone is the DNS zone for service records. Defaults to "svc.mcp.metacircular.net".
Zone string `toml:"zone"`
// NodeAddr is the IP address to register as the A record value.
NodeAddr string `toml:"node_addr"`
}
// MCProxyConfig holds the mc-proxy connection settings. // MCProxyConfig holds the mc-proxy connection settings.
type MCProxyConfig struct { type MCProxyConfig struct {
// Socket is the path to the mc-proxy gRPC admin API Unix socket. // Socket is the path to the mc-proxy gRPC admin API Unix socket.
@@ -150,6 +192,15 @@ func applyAgentDefaults(cfg *AgentConfig) {
if cfg.MCProxy.CertDir == "" { if cfg.MCProxy.CertDir == "" {
cfg.MCProxy.CertDir = "/srv/mc-proxy/certs" cfg.MCProxy.CertDir = "/srv/mc-proxy/certs"
} }
if cfg.Metacrypt.Mount == "" {
cfg.Metacrypt.Mount = "pki"
}
if cfg.Metacrypt.Issuer == "" {
cfg.Metacrypt.Issuer = "infra"
}
if cfg.MCNS.Zone == "" {
cfg.MCNS.Zone = "svc.mcp.metacircular.net"
}
} }
func applyAgentEnvOverrides(cfg *AgentConfig) { func applyAgentEnvOverrides(cfg *AgentConfig) {
@@ -180,6 +231,21 @@ func applyAgentEnvOverrides(cfg *AgentConfig) {
if v := os.Getenv("MCP_AGENT_MCPROXY_CERT_DIR"); v != "" { if v := os.Getenv("MCP_AGENT_MCPROXY_CERT_DIR"); v != "" {
cfg.MCProxy.CertDir = v cfg.MCProxy.CertDir = v
} }
if v := os.Getenv("MCP_AGENT_METACRYPT_SERVER_URL"); v != "" {
cfg.Metacrypt.ServerURL = v
}
if v := os.Getenv("MCP_AGENT_METACRYPT_TOKEN_PATH"); v != "" {
cfg.Metacrypt.TokenPath = v
}
if v := os.Getenv("MCP_AGENT_MCNS_SERVER_URL"); v != "" {
cfg.MCNS.ServerURL = v
}
if v := os.Getenv("MCP_AGENT_MCNS_TOKEN_PATH"); v != "" {
cfg.MCNS.TokenPath = v
}
if v := os.Getenv("MCP_AGENT_MCNS_NODE_ADDR"); v != "" {
cfg.MCNS.NodeAddr = v
}
} }
func validateAgentConfig(cfg *AgentConfig) error { func validateAgentConfig(cfg *AgentConfig) error {

View File

@@ -163,6 +163,19 @@ func TestLoadAgentConfig(t *testing.T) {
if cfg.Log.Level != "debug" { if cfg.Log.Level != "debug" {
t.Fatalf("log.level: got %q", cfg.Log.Level) t.Fatalf("log.level: got %q", cfg.Log.Level)
} }
// Metacrypt defaults when section is omitted.
if cfg.Metacrypt.Mount != "pki" {
t.Fatalf("metacrypt.mount default: got %q, want pki", cfg.Metacrypt.Mount)
}
if cfg.Metacrypt.Issuer != "infra" {
t.Fatalf("metacrypt.issuer default: got %q, want infra", cfg.Metacrypt.Issuer)
}
// MCNS defaults when section is omitted.
if cfg.MCNS.Zone != "svc.mcp.metacircular.net" {
t.Fatalf("mcns.zone default: got %q, want svc.mcp.metacircular.net", cfg.MCNS.Zone)
}
} }
func TestCLIConfigValidation(t *testing.T) { func TestCLIConfigValidation(t *testing.T) {
@@ -439,6 +452,155 @@ level = "info"
}) })
} }
func TestAgentConfigMetacrypt(t *testing.T) {
cfgStr := `
[server]
grpc_addr = "0.0.0.0:9444"
tls_cert = "/srv/mcp/cert.pem"
tls_key = "/srv/mcp/key.pem"
[database]
path = "/srv/mcp/mcp.db"
[mcias]
server_url = "https://mcias.metacircular.net:8443"
service_name = "mcp-agent"
[agent]
node_name = "rift"
[metacrypt]
server_url = "https://metacrypt.metacircular.net:8443"
ca_cert = "/etc/mcp/metacircular-ca.pem"
mount = "custom-pki"
issuer = "custom-issuer"
token_path = "/srv/mcp/metacrypt-token"
`
path := writeTempConfig(t, cfgStr)
cfg, err := LoadAgentConfig(path)
if err != nil {
t.Fatalf("load: %v", err)
}
if cfg.Metacrypt.ServerURL != "https://metacrypt.metacircular.net:8443" {
t.Fatalf("metacrypt.server_url: got %q", cfg.Metacrypt.ServerURL)
}
if cfg.Metacrypt.CACert != "/etc/mcp/metacircular-ca.pem" {
t.Fatalf("metacrypt.ca_cert: got %q", cfg.Metacrypt.CACert)
}
if cfg.Metacrypt.Mount != "custom-pki" {
t.Fatalf("metacrypt.mount: got %q", cfg.Metacrypt.Mount)
}
if cfg.Metacrypt.Issuer != "custom-issuer" {
t.Fatalf("metacrypt.issuer: got %q", cfg.Metacrypt.Issuer)
}
if cfg.Metacrypt.TokenPath != "/srv/mcp/metacrypt-token" {
t.Fatalf("metacrypt.token_path: got %q", cfg.Metacrypt.TokenPath)
}
}
func TestAgentConfigMetacryptEnvOverrides(t *testing.T) {
minimal := `
[server]
grpc_addr = "0.0.0.0:9444"
tls_cert = "/srv/mcp/cert.pem"
tls_key = "/srv/mcp/key.pem"
[database]
path = "/srv/mcp/mcp.db"
[mcias]
server_url = "https://mcias.metacircular.net:8443"
service_name = "mcp-agent"
[agent]
node_name = "rift"
`
t.Setenv("MCP_AGENT_METACRYPT_SERVER_URL", "https://override.metacrypt:8443")
t.Setenv("MCP_AGENT_METACRYPT_TOKEN_PATH", "/override/token")
path := writeTempConfig(t, minimal)
cfg, err := LoadAgentConfig(path)
if err != nil {
t.Fatalf("load: %v", err)
}
if cfg.Metacrypt.ServerURL != "https://override.metacrypt:8443" {
t.Fatalf("metacrypt.server_url: got %q", cfg.Metacrypt.ServerURL)
}
if cfg.Metacrypt.TokenPath != "/override/token" {
t.Fatalf("metacrypt.token_path: got %q", cfg.Metacrypt.TokenPath)
}
}
func TestAgentConfigMCNS(t *testing.T) {
cfgStr := `
[server]
grpc_addr = "0.0.0.0:9444"
tls_cert = "/srv/mcp/cert.pem"
tls_key = "/srv/mcp/key.pem"
[database]
path = "/srv/mcp/mcp.db"
[mcias]
server_url = "https://mcias.metacircular.net:8443"
service_name = "mcp-agent"
[agent]
node_name = "rift"
[mcns]
server_url = "https://localhost:28443"
ca_cert = "/srv/mcp/certs/metacircular-ca.pem"
token_path = "/srv/mcp/metacrypt-token"
zone = "custom.zone"
node_addr = "10.0.0.1"
`
path := writeTempConfig(t, cfgStr)
cfg, err := LoadAgentConfig(path)
if err != nil {
t.Fatalf("load: %v", err)
}
if cfg.MCNS.ServerURL != "https://localhost:28443" {
t.Fatalf("mcns.server_url: got %q", cfg.MCNS.ServerURL)
}
if cfg.MCNS.CACert != "/srv/mcp/certs/metacircular-ca.pem" {
t.Fatalf("mcns.ca_cert: got %q", cfg.MCNS.CACert)
}
if cfg.MCNS.Zone != "custom.zone" {
t.Fatalf("mcns.zone: got %q", cfg.MCNS.Zone)
}
if cfg.MCNS.NodeAddr != "10.0.0.1" {
t.Fatalf("mcns.node_addr: got %q", cfg.MCNS.NodeAddr)
}
}
func TestAgentConfigMCNSEnvOverrides(t *testing.T) {
minimal := `
[server]
grpc_addr = "0.0.0.0:9444"
tls_cert = "/srv/mcp/cert.pem"
tls_key = "/srv/mcp/key.pem"
[database]
path = "/srv/mcp/mcp.db"
[mcias]
server_url = "https://mcias.metacircular.net:8443"
service_name = "mcp-agent"
[agent]
node_name = "rift"
`
t.Setenv("MCP_AGENT_MCNS_SERVER_URL", "https://override:28443")
t.Setenv("MCP_AGENT_MCNS_TOKEN_PATH", "/override/token")
t.Setenv("MCP_AGENT_MCNS_NODE_ADDR", "10.0.0.99")
path := writeTempConfig(t, minimal)
cfg, err := LoadAgentConfig(path)
if err != nil {
t.Fatalf("load: %v", err)
}
if cfg.MCNS.ServerURL != "https://override:28443" {
t.Fatalf("mcns.server_url: got %q", cfg.MCNS.ServerURL)
}
if cfg.MCNS.TokenPath != "/override/token" {
t.Fatalf("mcns.token_path: got %q", cfg.MCNS.TokenPath)
}
if cfg.MCNS.NodeAddr != "10.0.0.99" {
t.Fatalf("mcns.node_addr: got %q", cfg.MCNS.NodeAddr)
}
}
func TestDurationParsing(t *testing.T) { func TestDurationParsing(t *testing.T) {
tests := []struct { tests := []struct {
input string input string

View File

@@ -199,15 +199,16 @@ func (p *Podman) Push(ctx context.Context, image string) error {
} }
// ImageExists checks whether an image tag exists in a remote registry. // ImageExists checks whether an image tag exists in a remote registry.
// Uses skopeo inspect which works for both regular images and multi-arch
// manifests, unlike podman manifest inspect which only handles manifests.
func (p *Podman) ImageExists(ctx context.Context, image string) (bool, error) { func (p *Podman) ImageExists(ctx context.Context, image string) (bool, error) {
cmd := exec.CommandContext(ctx, p.command(), "manifest", "inspect", "docker://"+image) //nolint:gosec // args built programmatically cmd := exec.CommandContext(ctx, "skopeo", "inspect", "--tls-verify=false", "docker://"+image) //nolint:gosec // args built programmatically
if err := cmd.Run(); err != nil { if err := cmd.Run(); err != nil {
// Exit code 1 means the manifest was not found.
var exitErr *exec.ExitError var exitErr *exec.ExitError
if ok := errors.As(err, &exitErr); ok && exitErr.ExitCode() == 1 { if ok := errors.As(err, &exitErr); ok && exitErr.ExitCode() != 0 {
return false, nil return false, nil
} }
return false, fmt.Errorf("podman manifest inspect %q: %w", image, err) return false, fmt.Errorf("skopeo inspect %q: %w", image, err)
} }
return true, nil return true, nil
} }
@@ -218,6 +219,7 @@ type podmanPSEntry struct {
Image string `json:"Image"` Image string `json:"Image"`
State string `json:"State"` State string `json:"State"`
Command []string `json:"Command"` Command []string `json:"Command"`
StartedAt int64 `json:"StartedAt"`
} }
// List returns information about all containers. // List returns information about all containers.
@@ -239,12 +241,16 @@ func (p *Podman) List(ctx context.Context) ([]ContainerInfo, error) {
if len(e.Names) > 0 { if len(e.Names) > 0 {
name = e.Names[0] name = e.Names[0]
} }
infos = append(infos, ContainerInfo{ info := ContainerInfo{
Name: name, Name: name,
Image: e.Image, Image: e.Image,
State: e.State, State: e.State,
Version: ExtractVersion(e.Image), Version: ExtractVersion(e.Image),
}) }
if e.StartedAt > 0 {
info.Started = time.Unix(e.StartedAt, 0)
}
infos = append(infos, info)
} }
return infos, nil return infos, nil

View File

@@ -90,18 +90,19 @@ func TestBuildRunArgs(t *testing.T) {
}) })
t.Run("full spec with env", func(t *testing.T) { t.Run("full spec with env", func(t *testing.T) {
// Route-allocated ports: host port = container port (matches $PORT).
spec := ContainerSpec{ spec := ContainerSpec{
Name: "svc-api", Name: "svc-api",
Image: "img:latest", Image: "img:latest",
Network: "net", Network: "net",
Ports: []string{"127.0.0.1:12345:8443"}, Ports: []string{"127.0.0.1:12345:12345"},
Volumes: []string{"/srv:/srv"}, Volumes: []string{"/srv:/srv"},
Env: []string{"PORT=12345"}, Env: []string{"PORT=12345"},
} }
requireEqualArgs(t, p.BuildRunArgs(spec), []string{ requireEqualArgs(t, p.BuildRunArgs(spec), []string{
"run", "-d", "--name", "svc-api", "run", "-d", "--name", "svc-api",
"--network", "net", "--network", "net",
"-p", "127.0.0.1:12345:8443", "-p", "127.0.0.1:12345:12345",
"-v", "/srv:/srv", "-v", "/srv:/srv",
"-e", "PORT=12345", "-e", "PORT=12345",
"img:latest", "img:latest",

View File

@@ -210,7 +210,7 @@ func ToProto(def *ServiceDef) *mcpv1.ServiceSpec {
for _, r := range c.Routes { for _, r := range c.Routes {
cs.Routes = append(cs.Routes, &mcpv1.RouteSpec{ cs.Routes = append(cs.Routes, &mcpv1.RouteSpec{
Name: r.Name, Name: r.Name,
Port: int32(r.Port), Port: int32(r.Port), //nolint:gosec // port range validated
Mode: r.Mode, Mode: r.Mode,
Hostname: r.Hostname, Hostname: r.Hostname,
}) })

View File

@@ -8,6 +8,7 @@ import "google/protobuf/timestamp.proto";
service McpAgentService { service McpAgentService {
// Service lifecycle // Service lifecycle
rpc Deploy(DeployRequest) returns (DeployResponse); rpc Deploy(DeployRequest) returns (DeployResponse);
rpc UndeployService(UndeployServiceRequest) returns (UndeployServiceResponse);
rpc StopService(StopServiceRequest) returns (StopServiceResponse); rpc StopService(StopServiceRequest) returns (StopServiceResponse);
rpc StartService(StartServiceRequest) returns (StartServiceResponse); rpc StartService(StartServiceRequest) returns (StartServiceResponse);
rpc RestartService(RestartServiceRequest) returns (RestartServiceResponse); rpc RestartService(RestartServiceRequest) returns (RestartServiceResponse);
@@ -38,7 +39,7 @@ service McpAgentService {
message RouteSpec { message RouteSpec {
string name = 1; // route name (used for $PORT_<NAME>) string name = 1; // route name (used for $PORT_<NAME>)
int32 port = 2; // external port on mc-proxy int32 port = 2; // mc-proxy listener port (e.g. 443, 8443, 9443); NOT the container internal port
string mode = 3; // "l4" or "l7" string mode = 3; // "l4" or "l7"
string hostname = 4; // optional public hostname override string hostname = 4; // optional public hostname override
} }
@@ -102,6 +103,14 @@ message RestartServiceResponse {
repeated ComponentResult results = 1; repeated ComponentResult results = 1;
} }
message UndeployServiceRequest {
string name = 1;
}
message UndeployServiceResponse {
repeated ComponentResult results = 1;
}
// --- Desired state --- // --- Desired state ---
message SyncDesiredStateRequest { message SyncDesiredStateRequest {

View File

@@ -1449,7 +1449,7 @@ const file_proto_mc_proxy_v1_admin_proto_rawDesc = "" +
"\x0eListL7Policies\x12\".mc_proxy.v1.ListL7PoliciesRequest\x1a#.mc_proxy.v1.ListL7PoliciesResponse\x12P\n" + "\x0eListL7Policies\x12\".mc_proxy.v1.ListL7PoliciesRequest\x1a#.mc_proxy.v1.ListL7PoliciesResponse\x12P\n" +
"\vAddL7Policy\x12\x1f.mc_proxy.v1.AddL7PolicyRequest\x1a .mc_proxy.v1.AddL7PolicyResponse\x12Y\n" + "\vAddL7Policy\x12\x1f.mc_proxy.v1.AddL7PolicyRequest\x1a .mc_proxy.v1.AddL7PolicyResponse\x12Y\n" +
"\x0eRemoveL7Policy\x12\".mc_proxy.v1.RemoveL7PolicyRequest\x1a#.mc_proxy.v1.RemoveL7PolicyResponse\x12J\n" + "\x0eRemoveL7Policy\x12\".mc_proxy.v1.RemoveL7PolicyRequest\x1a#.mc_proxy.v1.RemoveL7PolicyResponse\x12J\n" +
"\tGetStatus\x12\x1d.mc_proxy.v1.GetStatusRequest\x1a\x1e.mc_proxy.v1.GetStatusResponseB:Z8git.wntrmute.dev/mc/mc-proxy/gen/mc_proxy/v1;mcproxyv1b\x06proto3" "\tGetStatus\x12\x1d.mc_proxy.v1.GetStatusRequest\x1a\x1e.mc_proxy.v1.GetStatusResponseB8Z6git.wntrmute.dev/mc/mc-proxy/gen/mc_proxy/v1;mcproxyv1b\x06proto3"
var ( var (
file_proto_mc_proxy_v1_admin_proto_rawDescOnce sync.Once file_proto_mc_proxy_v1_admin_proto_rawDescOnce sync.Once

2
vendor/modules.txt vendored
View File

@@ -1,4 +1,4 @@
# git.wntrmute.dev/mc/mc-proxy v1.1.0 # git.wntrmute.dev/mc/mc-proxy v1.2.0
## explicit; go 1.25.7 ## explicit; go 1.25.7
git.wntrmute.dev/mc/mc-proxy/client/mcproxy git.wntrmute.dev/mc/mc-proxy/client/mcproxy
git.wntrmute.dev/mc/mc-proxy/gen/mc_proxy/v1 git.wntrmute.dev/mc/mc-proxy/gen/mc_proxy/v1