Compare commits
21 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 14b978861f | |||
| 18365cc0a8 | |||
| 86d516acf6 | |||
| dd167b8e0b | |||
| 41437e3730 | |||
| cedba9bf83 | |||
| f06ab9aeb6 | |||
| f932dd64cc | |||
| b2eaa69619 | |||
| 43789dd6be | |||
| 2dd0ea93fc | |||
| 169b3a0d4a | |||
| 2bda7fc138 | |||
| 76247978c2 | |||
| ca3bc736f6 | |||
| 9d9ad6588e | |||
| e4d131021e | |||
| 8d6c060483 | |||
| c7e1232f98 | |||
| 572d2fb196 | |||
| c6a84a1b80 |
@@ -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"
|
|
||||||
|
|||||||
283
ARCHITECTURE.md
283
ARCHITECTURE.md
@@ -121,9 +121,26 @@ option for future security hardening.
|
|||||||
## Authentication and Authorization
|
## Authentication and Authorization
|
||||||
|
|
||||||
MCP follows the platform authentication model: all auth is delegated to
|
MCP follows the platform authentication model: all auth is delegated to
|
||||||
MCIAS.
|
MCIAS. The auth model separates three concerns: operator intent (CLI to
|
||||||
|
agent), infrastructure automation (agent to platform services), and
|
||||||
|
access control (who can do what).
|
||||||
|
|
||||||
### Agent Authentication
|
### Identity Model
|
||||||
|
|
||||||
|
| Identity | Type | Purpose |
|
||||||
|
|----------|------|---------|
|
||||||
|
| Human operator (e.g., `kyle`) | human | CLI operations: deploy, stop, start, build |
|
||||||
|
| `mcp-agent` | system | Agent-to-service automation: certs, DNS, routes, image pull |
|
||||||
|
| Per-service accounts (e.g., `mcq`) | system | Scoped self-management (own DNS records only) |
|
||||||
|
| `admin` role | role | MCIAS account management, policy changes, zone creation |
|
||||||
|
| `guest` role | role | Explicitly rejected by the agent |
|
||||||
|
|
||||||
|
The `admin` role is reserved for MCIAS-level administrative operations
|
||||||
|
(account creation, policy management, zone mutations). Routine MCP
|
||||||
|
operations (deploy, stop, start, build) do not require admin — any
|
||||||
|
authenticated non-guest user or system account is accepted.
|
||||||
|
|
||||||
|
### Agent Authentication (CLI → Agent)
|
||||||
|
|
||||||
The agent is a gRPC server with a unary interceptor that enforces
|
The agent is a gRPC server with a unary interceptor that enforces
|
||||||
authentication on every RPC:
|
authentication on every RPC:
|
||||||
@@ -132,10 +149,34 @@ authentication on every RPC:
|
|||||||
(`authorization: Bearer <token>`).
|
(`authorization: Bearer <token>`).
|
||||||
2. Agent extracts the token and validates it against MCIAS (cached 30s by
|
2. Agent extracts the token and validates it against MCIAS (cached 30s by
|
||||||
SHA-256 of the token, per platform convention).
|
SHA-256 of the token, per platform convention).
|
||||||
3. Agent checks that the caller has the `admin` role. All MCP operations
|
3. Agent rejects guests (`guest` role → `PERMISSION_DENIED`). All other
|
||||||
require admin -- there is no unprivileged MCP access.
|
authenticated users and system accounts are accepted.
|
||||||
4. If validation fails, the RPC returns `UNAUTHENTICATED` (invalid/expired
|
4. If validation fails, the RPC returns `UNAUTHENTICATED` (invalid/expired
|
||||||
token) or `PERMISSION_DENIED` (valid token, not admin).
|
token) or `PERMISSION_DENIED` (guest).
|
||||||
|
|
||||||
|
### Agent Service Authentication (Agent → Platform Services)
|
||||||
|
|
||||||
|
The agent authenticates to platform services using a long-lived system
|
||||||
|
account token (`mcp-agent`). Each service has its own token file:
|
||||||
|
|
||||||
|
| Service | Token Path | Operations |
|
||||||
|
|---------|------------|------------|
|
||||||
|
| Metacrypt | `/srv/mcp/metacrypt-token` | TLS cert provisioning (PKI issue) |
|
||||||
|
| MCNS | `/srv/mcp/mcns-token` | DNS record create/delete (any name) |
|
||||||
|
| mc-proxy | Unix socket (no auth) | Route registration/removal |
|
||||||
|
| MCR | podman auth store | Image pull (JWT-as-password) |
|
||||||
|
|
||||||
|
These tokens are issued by MCIAS for the `mcp-agent` system account.
|
||||||
|
They carry no roles — authorization is handled by each service's policy
|
||||||
|
engine:
|
||||||
|
|
||||||
|
- **Metacrypt:** Policy rule grants `mcp-agent` write access to
|
||||||
|
`engine/pki/issue`.
|
||||||
|
- **MCNS:** Code-level authorization: system account `mcp-agent` can
|
||||||
|
manage any record; other system accounts can only manage records
|
||||||
|
matching their username.
|
||||||
|
- **MCR:** Default policy allows all authenticated users to push/pull.
|
||||||
|
MCR accepts MCIAS JWTs as passwords at the `/v2/token` endpoint.
|
||||||
|
|
||||||
### CLI Authentication
|
### CLI Authentication
|
||||||
|
|
||||||
@@ -148,6 +189,15 @@ obtained by:
|
|||||||
|
|
||||||
The stored token is used for all subsequent agent RPCs until it expires.
|
The stored token is used for all subsequent agent RPCs until it expires.
|
||||||
|
|
||||||
|
### MCR Registry Authentication
|
||||||
|
|
||||||
|
`mcp build` auto-authenticates to MCR before pushing images. It reads
|
||||||
|
the CLI's stored MCIAS token and uses it as the password for `podman
|
||||||
|
login`. MCR's token endpoint accepts MCIAS JWTs as passwords (the
|
||||||
|
personal-access-token pattern), so both human and system account tokens
|
||||||
|
work. This eliminates the need for a separate interactive `podman login`
|
||||||
|
step.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Services and Components
|
## Services and Components
|
||||||
@@ -198,6 +248,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
|
||||||
@@ -223,6 +274,9 @@ mcp pull <service> <path> [local-file] Copy a file from /srv/<service>/<path> to
|
|||||||
mcp node list List registered nodes
|
mcp node list List registered nodes
|
||||||
mcp node add <name> <address> Register a node
|
mcp node add <name> <address> Register a node
|
||||||
mcp node remove <name> Deregister a node
|
mcp node remove <name> Deregister a node
|
||||||
|
|
||||||
|
mcp agent upgrade [node] Build, push, and restart agent on all (or one) node(s)
|
||||||
|
mcp agent status Show agent version on each node
|
||||||
```
|
```
|
||||||
|
|
||||||
### Service Definition Files
|
### Service Definition Files
|
||||||
@@ -453,6 +507,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 +769,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
|
||||||
@@ -1108,20 +1197,84 @@ The agent's data directory follows the platform convention:
|
|||||||
|
|
||||||
### Agent Deployment (on nodes)
|
### Agent Deployment (on nodes)
|
||||||
|
|
||||||
The agent is deployed like any other Metacircular service:
|
#### Provisioning (one-time per node)
|
||||||
|
|
||||||
1. Provision the `mcp` system user via NixOS config (with podman access
|
Each node needs a one-time setup before the agent can run. The steps are
|
||||||
and subuid/subgid ranges for rootless containers).
|
the same regardless of OS, but the mechanism differs:
|
||||||
|
|
||||||
|
1. Create `mcp` system user with podman access and subuid/subgid ranges.
|
||||||
2. Set `/srv/` ownership to the `mcp` user (the agent creates and manages
|
2. Set `/srv/` ownership to the `mcp` user (the agent creates and manages
|
||||||
`/srv/<service>/` directories for all services).
|
`/srv/<service>/` directories for all services).
|
||||||
3. Create `/srv/mcp/` directory and config file.
|
3. Create `/srv/mcp/` directory and config file.
|
||||||
4. Provision TLS certificate from Metacrypt.
|
4. Provision TLS certificate from Metacrypt.
|
||||||
5. Create an MCIAS system account for the agent (`mcp-agent`).
|
5. Create an MCIAS system account for the agent (`mcp-agent`).
|
||||||
6. Install the `mcp-agent` binary.
|
6. Install the initial `mcp-agent` binary to `/srv/mcp/mcp-agent`.
|
||||||
7. Start via systemd unit.
|
7. Install and start the systemd unit.
|
||||||
|
|
||||||
The agent runs as a systemd service. Container-first deployment is a v2
|
On **NixOS** (rift), provisioning is declarative via the NixOS config.
|
||||||
concern -- MCP needs to be running before it can manage its own agent.
|
The NixOS config owns the infrastructure (user, systemd unit, podman,
|
||||||
|
directories, permissions) but **not** the binary. `ExecStart` points to
|
||||||
|
`/srv/mcp/mcp-agent`, a mutable path that MCP manages. NixOS may
|
||||||
|
bootstrap the initial binary there, but subsequent updates come from MCP.
|
||||||
|
|
||||||
|
On **Debian** (hyperborea, svc), provisioning is done via a setup script
|
||||||
|
or ansible playbook that creates the same layout.
|
||||||
|
|
||||||
|
#### Binary Location
|
||||||
|
|
||||||
|
The agent binary lives at `/srv/mcp/mcp-agent` on **all** nodes,
|
||||||
|
regardless of OS. This unifies the update mechanism across the fleet.
|
||||||
|
|
||||||
|
#### Agent Upgrades
|
||||||
|
|
||||||
|
After initial provisioning, the agent binary is updated via
|
||||||
|
`mcp agent upgrade`. The CLI:
|
||||||
|
|
||||||
|
1. Cross-compiles the agent for each target architecture
|
||||||
|
(`GOARCH=amd64` for rift/svc, `GOARCH=arm64` for hyperborea).
|
||||||
|
2. SSHs to each node, pushes the binary to `/srv/mcp/mcp-agent.new`.
|
||||||
|
3. Atomically swaps the binary (`mv mcp-agent.new mcp-agent`).
|
||||||
|
4. Restarts the systemd service (`systemctl restart mcp-agent`).
|
||||||
|
|
||||||
|
SSH is used instead of gRPC because:
|
||||||
|
- It works even when the agent is broken or has an incompatible version.
|
||||||
|
- The binary is ~17MB, which exceeds gRPC default message limits.
|
||||||
|
- No self-restart coordination needed.
|
||||||
|
|
||||||
|
The CLI uses `golang.org/x/crypto/ssh` for native SSH, keeping the
|
||||||
|
entire workflow in a single binary with no external tool dependencies.
|
||||||
|
|
||||||
|
#### Node Configuration
|
||||||
|
|
||||||
|
Node config includes SSH and architecture info for agent management:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[[nodes]]
|
||||||
|
name = "rift"
|
||||||
|
address = "100.95.252.120:9444"
|
||||||
|
ssh = "rift" # SSH host (from ~/.ssh/config or hostname)
|
||||||
|
arch = "amd64" # GOARCH for cross-compilation
|
||||||
|
|
||||||
|
[[nodes]]
|
||||||
|
name = "hyperborea"
|
||||||
|
address = "100.x.x.x:9444"
|
||||||
|
ssh = "hyperborea"
|
||||||
|
arch = "arm64"
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Coordinated Upgrades
|
||||||
|
|
||||||
|
New MCP releases often add new RPCs. A CLI at v0.6.0 calling an agent
|
||||||
|
at v0.5.0 fails with `Unimplemented`. Therefore agent upgrades must be
|
||||||
|
coordinated: `mcp agent upgrade` (with no node argument) upgrades all
|
||||||
|
nodes before the CLI is used for other operations.
|
||||||
|
|
||||||
|
If a node fails to upgrade, it is reported but the others still proceed.
|
||||||
|
The operator can retry or investigate via SSH.
|
||||||
|
|
||||||
|
#### Systemd Unit
|
||||||
|
|
||||||
|
The systemd unit is the same on all nodes:
|
||||||
|
|
||||||
```ini
|
```ini
|
||||||
[Unit]
|
[Unit]
|
||||||
@@ -1131,7 +1284,7 @@ Wants=network-online.target
|
|||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
Type=simple
|
Type=simple
|
||||||
ExecStart=/usr/local/bin/mcp-agent server --config /srv/mcp/mcp-agent.toml
|
ExecStart=/srv/mcp/mcp-agent server --config /srv/mcp/mcp-agent.toml
|
||||||
Restart=on-failure
|
Restart=on-failure
|
||||||
RestartSec=5
|
RestartSec=5
|
||||||
|
|
||||||
@@ -1139,17 +1292,14 @@ User=mcp
|
|||||||
Group=mcp
|
Group=mcp
|
||||||
|
|
||||||
NoNewPrivileges=true
|
NoNewPrivileges=true
|
||||||
ProtectSystem=strict
|
ProtectSystem=full
|
||||||
ProtectHome=true
|
ProtectHome=false
|
||||||
PrivateTmp=true
|
PrivateTmp=true
|
||||||
PrivateDevices=true
|
PrivateDevices=true
|
||||||
ProtectKernelTunables=true
|
ProtectKernelTunables=true
|
||||||
ProtectKernelModules=true
|
ProtectKernelModules=true
|
||||||
ProtectControlGroups=true
|
|
||||||
RestrictSUIDSGID=true
|
RestrictSUIDSGID=true
|
||||||
RestrictNamespaces=true
|
|
||||||
LockPersonality=true
|
LockPersonality=true
|
||||||
MemoryDenyWriteExecute=true
|
|
||||||
RestrictRealtime=true
|
RestrictRealtime=true
|
||||||
ReadWritePaths=/srv
|
ReadWritePaths=/srv
|
||||||
|
|
||||||
@@ -1159,6 +1309,7 @@ WantedBy=multi-user.target
|
|||||||
|
|
||||||
Note: `ReadWritePaths=/srv` (not `/srv/mcp`) because the agent writes
|
Note: `ReadWritePaths=/srv` (not `/srv/mcp`) because the agent writes
|
||||||
files to any service's `/srv/<service>/` directory on behalf of the CLI.
|
files to any service's `/srv/<service>/` directory on behalf of the CLI.
|
||||||
|
`ProtectHome=false` because the `mcp` user's home is `/srv/mcp`.
|
||||||
|
|
||||||
### CLI Installation (on operator workstation)
|
### CLI Installation (on operator workstation)
|
||||||
|
|
||||||
@@ -1203,6 +1354,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
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
|
"git.wntrmute.dev/mc/mcp/internal/auth"
|
||||||
"git.wntrmute.dev/mc/mcp/internal/config"
|
"git.wntrmute.dev/mc/mcp/internal/config"
|
||||||
"git.wntrmute.dev/mc/mcp/internal/runtime"
|
"git.wntrmute.dev/mc/mcp/internal/runtime"
|
||||||
"git.wntrmute.dev/mc/mcp/internal/servicedef"
|
"git.wntrmute.dev/mc/mcp/internal/servicedef"
|
||||||
@@ -52,6 +53,17 @@ func buildServiceImages(ctx context.Context, cfg *config.CLIConfig, def *service
|
|||||||
|
|
||||||
sourceDir := filepath.Join(cfg.Build.Workspace, def.Path)
|
sourceDir := filepath.Join(cfg.Build.Workspace, def.Path)
|
||||||
|
|
||||||
|
// Auto-login to the registry using the CLI's stored MCIAS token.
|
||||||
|
// MCR accepts JWTs as passwords, so this works for both human and
|
||||||
|
// service account tokens. Failures are non-fatal — existing podman
|
||||||
|
// auth may suffice.
|
||||||
|
if token, err := auth.LoadToken(cfg.Auth.TokenPath); err == nil && token != "" {
|
||||||
|
registry := extractRegistry(def)
|
||||||
|
if registry != "" {
|
||||||
|
_ = rt.Login(ctx, registry, "mcp", token)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for imageName, dockerfile := range def.Build.Images {
|
for imageName, dockerfile := range def.Build.Images {
|
||||||
if imageFilter != "" && imageName != imageFilter {
|
if imageFilter != "" && imageName != imageFilter {
|
||||||
continue
|
continue
|
||||||
@@ -96,6 +108,19 @@ func findImageRef(def *servicedef.ServiceDef, imageName string) string {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// extractRegistry returns the registry host from the first component's
|
||||||
|
// image reference (e.g., "mcr.svc.mcp.metacircular.net:8443" from
|
||||||
|
// "mcr.svc.mcp.metacircular.net:8443/mcq:v0.1.1"). Returns empty
|
||||||
|
// string if no slash is found.
|
||||||
|
func extractRegistry(def *servicedef.ServiceDef) string {
|
||||||
|
for _, c := range def.Components {
|
||||||
|
if i := strings.LastIndex(c.Image, "/"); i > 0 {
|
||||||
|
return c.Image[:i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
// extractRepoName returns the repository name from an image reference.
|
// extractRepoName returns the repository name from an image reference.
|
||||||
// Examples:
|
// Examples:
|
||||||
//
|
//
|
||||||
@@ -124,6 +149,8 @@ func ensureImages(ctx context.Context, cfg *config.CLIConfig, def *servicedef.Se
|
|||||||
return nil // no build config, skip auto-build
|
return nil // no build config, skip auto-build
|
||||||
}
|
}
|
||||||
|
|
||||||
|
registryLoginDone := false
|
||||||
|
|
||||||
for _, c := range def.Components {
|
for _, c := range def.Components {
|
||||||
if component != "" && c.Name != component {
|
if component != "" && c.Name != component {
|
||||||
continue
|
continue
|
||||||
@@ -153,6 +180,17 @@ func ensureImages(ctx context.Context, cfg *config.CLIConfig, def *servicedef.Se
|
|||||||
|
|
||||||
sourceDir := filepath.Join(cfg.Build.Workspace, def.Path)
|
sourceDir := filepath.Join(cfg.Build.Workspace, def.Path)
|
||||||
|
|
||||||
|
// Auto-login to registry before first push.
|
||||||
|
if !registryLoginDone {
|
||||||
|
if token, err := auth.LoadToken(cfg.Auth.TokenPath); err == nil && token != "" {
|
||||||
|
registry := extractRegistry(def)
|
||||||
|
if registry != "" {
|
||||||
|
_ = rt.Login(ctx, registry, "mcp", token)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
registryLoginDone = true
|
||||||
|
}
|
||||||
|
|
||||||
fmt.Printf("image %s not found, building from %s\n", c.Image, dockerfile)
|
fmt.Printf("image %s not found, building from %s\n", c.Image, dockerfile)
|
||||||
if err := rt.Build(ctx, c.Image, sourceDir, dockerfile); err != nil {
|
if err := rt.Build(ctx, c.Image, sourceDir, dockerfile); err != nil {
|
||||||
return fmt.Errorf("auto-build %s: %w", c.Image, err)
|
return fmt.Errorf("auto-build %s: %w", c.Image, err)
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ func dialAgent(address string, cfg *config.CLIConfig) (mcpv1.McpAgentServiceClie
|
|||||||
address,
|
address,
|
||||||
grpc.WithTransportCredentials(credentials.NewTLS(tlsConfig)),
|
grpc.WithTransportCredentials(credentials.NewTLS(tlsConfig)),
|
||||||
grpc.WithUnaryInterceptor(tokenInterceptor(token)),
|
grpc.WithUnaryInterceptor(tokenInterceptor(token)),
|
||||||
|
grpc.WithStreamInterceptor(streamTokenInterceptor(token)),
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, fmt.Errorf("dial %q: %w", address, err)
|
return nil, nil, fmt.Errorf("dial %q: %w", address, err)
|
||||||
@@ -60,6 +61,15 @@ func tokenInterceptor(token string) grpc.UnaryClientInterceptor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// streamTokenInterceptor returns a gRPC client stream interceptor that
|
||||||
|
// attaches the bearer token to outgoing stream metadata.
|
||||||
|
func streamTokenInterceptor(token string) grpc.StreamClientInterceptor {
|
||||||
|
return func(ctx context.Context, desc *grpc.StreamDesc, cc *grpc.ClientConn, method string, streamer grpc.Streamer, opts ...grpc.CallOption) (grpc.ClientStream, error) {
|
||||||
|
ctx = metadata.AppendToOutgoingContext(ctx, "authorization", "Bearer "+token)
|
||||||
|
return streamer(ctx, desc, cc, method, opts...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// loadBearerToken reads the token from file or env var.
|
// loadBearerToken reads the token from file or env var.
|
||||||
func loadBearerToken(cfg *config.CLIConfig) (string, error) {
|
func loadBearerToken(cfg *config.CLIConfig) (string, error) {
|
||||||
if token := os.Getenv("MCP_TOKEN"); token != "" {
|
if token := os.Getenv("MCP_TOKEN"); token != "" {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
|
"git.wntrmute.dev/mc/mcdsl/terminal"
|
||||||
"git.wntrmute.dev/mc/mcp/internal/auth"
|
"git.wntrmute.dev/mc/mcp/internal/auth"
|
||||||
"git.wntrmute.dev/mc/mcp/internal/config"
|
"git.wntrmute.dev/mc/mcp/internal/config"
|
||||||
)
|
)
|
||||||
@@ -33,14 +34,11 @@ func loginCmd() *cobra.Command {
|
|||||||
}
|
}
|
||||||
username := strings.TrimSpace(scanner.Text())
|
username := strings.TrimSpace(scanner.Text())
|
||||||
|
|
||||||
fmt.Print("Password: ")
|
password, err := terminal.ReadPassword("Password: ")
|
||||||
if !scanner.Scan() {
|
if err != nil {
|
||||||
if err := scanner.Err(); err != nil {
|
return fmt.Errorf("read password: %w", err)
|
||||||
return fmt.Errorf("read password: %w", err)
|
|
||||||
}
|
|
||||||
return fmt.Errorf("read password: unexpected end of input")
|
|
||||||
}
|
}
|
||||||
password := strings.TrimSpace(scanner.Text())
|
password = strings.TrimSpace(password)
|
||||||
|
|
||||||
token, err := auth.Login(cfg.MCIAS.ServerURL, cfg.MCIAS.CACert, username, password)
|
token, err := auth.Login(cfg.MCIAS.ServerURL, cfg.MCIAS.CACert, username, password)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
81
cmd/mcp/logs.go
Normal file
81
cmd/mcp/logs.go
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
|
mcpv1 "git.wntrmute.dev/mc/mcp/gen/mcp/v1"
|
||||||
|
"git.wntrmute.dev/mc/mcp/internal/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
func logsCmd() *cobra.Command {
|
||||||
|
var (
|
||||||
|
tail int
|
||||||
|
follow bool
|
||||||
|
timestamps bool
|
||||||
|
since string
|
||||||
|
)
|
||||||
|
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "logs <service>[/<component>]",
|
||||||
|
Short: "Show container logs",
|
||||||
|
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, component := parseServiceArg(args[0])
|
||||||
|
|
||||||
|
def, err := loadServiceDef(cmd, cfg, serviceName)
|
||||||
|
if err != nil {
|
||||||
|
return 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() }()
|
||||||
|
|
||||||
|
stream, err := client.Logs(cmd.Context(), &mcpv1.LogsRequest{
|
||||||
|
Service: serviceName,
|
||||||
|
Component: component,
|
||||||
|
Tail: int32(tail),
|
||||||
|
Follow: follow,
|
||||||
|
Timestamps: timestamps,
|
||||||
|
Since: since,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("logs: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for {
|
||||||
|
resp, err := stream.Recv()
|
||||||
|
if err == io.EOF {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("recv: %w", err)
|
||||||
|
}
|
||||||
|
_, _ = os.Stdout.Write(resp.Data)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.Flags().IntVarP(&tail, "tail", "n", 0, "number of lines from end (0 = all)")
|
||||||
|
cmd.Flags().BoolVarP(&follow, "follow", "f", false, "follow log output")
|
||||||
|
cmd.Flags().BoolVarP(×tamps, "timestamps", "t", false, "show timestamps")
|
||||||
|
cmd.Flags().StringVar(&since, "since", "", "show logs since (e.g., 2h, 2026-03-28T00:00:00Z)")
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
@@ -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())
|
||||||
@@ -49,6 +50,7 @@ func main() {
|
|||||||
root.AddCommand(pullCmd())
|
root.AddCommand(pullCmd())
|
||||||
root.AddCommand(nodeCmd())
|
root.AddCommand(nodeCmd())
|
||||||
root.AddCommand(purgeCmd())
|
root.AddCommand(purgeCmd())
|
||||||
|
root.AddCommand(logsCmd())
|
||||||
|
|
||||||
if err := root.Execute(); err != nil {
|
if err := root.Execute(); err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
|
|||||||
63
cmd/mcp/undeploy.go
Normal file
63
cmd/mcp/undeploy.go
Normal 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
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
10
flake.nix
10
flake.nix
@@ -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 {
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||||
@@ -32,6 +33,7 @@ const (
|
|||||||
McpAgentService_PushFile_FullMethodName = "/mcp.v1.McpAgentService/PushFile"
|
McpAgentService_PushFile_FullMethodName = "/mcp.v1.McpAgentService/PushFile"
|
||||||
McpAgentService_PullFile_FullMethodName = "/mcp.v1.McpAgentService/PullFile"
|
McpAgentService_PullFile_FullMethodName = "/mcp.v1.McpAgentService/PullFile"
|
||||||
McpAgentService_NodeStatus_FullMethodName = "/mcp.v1.McpAgentService/NodeStatus"
|
McpAgentService_NodeStatus_FullMethodName = "/mcp.v1.McpAgentService/NodeStatus"
|
||||||
|
McpAgentService_Logs_FullMethodName = "/mcp.v1.McpAgentService/Logs"
|
||||||
)
|
)
|
||||||
|
|
||||||
// McpAgentServiceClient is the client API for McpAgentService service.
|
// McpAgentServiceClient is the client API for McpAgentService service.
|
||||||
@@ -40,6 +42,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)
|
||||||
@@ -58,6 +61,8 @@ type McpAgentServiceClient interface {
|
|||||||
PullFile(ctx context.Context, in *PullFileRequest, opts ...grpc.CallOption) (*PullFileResponse, error)
|
PullFile(ctx context.Context, in *PullFileRequest, opts ...grpc.CallOption) (*PullFileResponse, error)
|
||||||
// Node
|
// Node
|
||||||
NodeStatus(ctx context.Context, in *NodeStatusRequest, opts ...grpc.CallOption) (*NodeStatusResponse, error)
|
NodeStatus(ctx context.Context, in *NodeStatusRequest, opts ...grpc.CallOption) (*NodeStatusResponse, error)
|
||||||
|
// Logs
|
||||||
|
Logs(ctx context.Context, in *LogsRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[LogsResponse], error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type mcpAgentServiceClient struct {
|
type mcpAgentServiceClient struct {
|
||||||
@@ -78,6 +83,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)
|
||||||
@@ -198,12 +213,32 @@ func (c *mcpAgentServiceClient) NodeStatus(ctx context.Context, in *NodeStatusRe
|
|||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *mcpAgentServiceClient) Logs(ctx context.Context, in *LogsRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[LogsResponse], error) {
|
||||||
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
|
stream, err := c.cc.NewStream(ctx, &McpAgentService_ServiceDesc.Streams[0], McpAgentService_Logs_FullMethodName, cOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
x := &grpc.GenericClientStream[LogsRequest, LogsResponse]{ClientStream: stream}
|
||||||
|
if err := x.ClientStream.SendMsg(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := x.ClientStream.CloseSend(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return x, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
|
||||||
|
type McpAgentService_LogsClient = grpc.ServerStreamingClient[LogsResponse]
|
||||||
|
|
||||||
// McpAgentServiceServer is the server API for McpAgentService service.
|
// McpAgentServiceServer is the server API for McpAgentService service.
|
||||||
// All implementations must embed UnimplementedMcpAgentServiceServer
|
// All implementations must embed UnimplementedMcpAgentServiceServer
|
||||||
// for forward compatibility.
|
// for forward compatibility.
|
||||||
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)
|
||||||
@@ -222,6 +257,8 @@ type McpAgentServiceServer interface {
|
|||||||
PullFile(context.Context, *PullFileRequest) (*PullFileResponse, error)
|
PullFile(context.Context, *PullFileRequest) (*PullFileResponse, error)
|
||||||
// Node
|
// Node
|
||||||
NodeStatus(context.Context, *NodeStatusRequest) (*NodeStatusResponse, error)
|
NodeStatus(context.Context, *NodeStatusRequest) (*NodeStatusResponse, error)
|
||||||
|
// Logs
|
||||||
|
Logs(*LogsRequest, grpc.ServerStreamingServer[LogsResponse]) error
|
||||||
mustEmbedUnimplementedMcpAgentServiceServer()
|
mustEmbedUnimplementedMcpAgentServiceServer()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -235,6 +272,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")
|
||||||
}
|
}
|
||||||
@@ -271,6 +311,9 @@ func (UnimplementedMcpAgentServiceServer) PullFile(context.Context, *PullFileReq
|
|||||||
func (UnimplementedMcpAgentServiceServer) NodeStatus(context.Context, *NodeStatusRequest) (*NodeStatusResponse, error) {
|
func (UnimplementedMcpAgentServiceServer) NodeStatus(context.Context, *NodeStatusRequest) (*NodeStatusResponse, error) {
|
||||||
return nil, status.Error(codes.Unimplemented, "method NodeStatus not implemented")
|
return nil, status.Error(codes.Unimplemented, "method NodeStatus not implemented")
|
||||||
}
|
}
|
||||||
|
func (UnimplementedMcpAgentServiceServer) Logs(*LogsRequest, grpc.ServerStreamingServer[LogsResponse]) error {
|
||||||
|
return status.Error(codes.Unimplemented, "method Logs not implemented")
|
||||||
|
}
|
||||||
func (UnimplementedMcpAgentServiceServer) mustEmbedUnimplementedMcpAgentServiceServer() {}
|
func (UnimplementedMcpAgentServiceServer) mustEmbedUnimplementedMcpAgentServiceServer() {}
|
||||||
func (UnimplementedMcpAgentServiceServer) testEmbeddedByValue() {}
|
func (UnimplementedMcpAgentServiceServer) testEmbeddedByValue() {}
|
||||||
|
|
||||||
@@ -310,6 +353,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 {
|
||||||
@@ -526,6 +587,17 @@ func _McpAgentService_NodeStatus_Handler(srv interface{}, ctx context.Context, d
|
|||||||
return interceptor(ctx, in, info, handler)
|
return interceptor(ctx, in, info, handler)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func _McpAgentService_Logs_Handler(srv interface{}, stream grpc.ServerStream) error {
|
||||||
|
m := new(LogsRequest)
|
||||||
|
if err := stream.RecvMsg(m); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return srv.(McpAgentServiceServer).Logs(m, &grpc.GenericServerStream[LogsRequest, LogsResponse]{ServerStream: stream})
|
||||||
|
}
|
||||||
|
|
||||||
|
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
|
||||||
|
type McpAgentService_LogsServer = grpc.ServerStreamingServer[LogsResponse]
|
||||||
|
|
||||||
// McpAgentService_ServiceDesc is the grpc.ServiceDesc for McpAgentService service.
|
// McpAgentService_ServiceDesc is the grpc.ServiceDesc for McpAgentService service.
|
||||||
// It's only intended for direct use with grpc.RegisterService,
|
// It's only intended for direct use with grpc.RegisterService,
|
||||||
// and not to be introspected or modified (even as a copy)
|
// and not to be introspected or modified (even as a copy)
|
||||||
@@ -537,6 +609,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,
|
||||||
@@ -586,6 +662,12 @@ var McpAgentService_ServiceDesc = grpc.ServiceDesc{
|
|||||||
Handler: _McpAgentService_NodeStatus_Handler,
|
Handler: _McpAgentService_NodeStatus_Handler,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Streams: []grpc.StreamDesc{},
|
Streams: []grpc.StreamDesc{
|
||||||
|
{
|
||||||
|
StreamName: "Logs",
|
||||||
|
Handler: _McpAgentService_Logs_Handler,
|
||||||
|
ServerStreams: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
Metadata: "proto/mcp/v1/mcp.proto",
|
Metadata: "proto/mcp/v1/mcp.proto",
|
||||||
}
|
}
|
||||||
|
|||||||
4
go.mod
4
go.mod
@@ -3,7 +3,8 @@ 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
|
||||||
|
git.wntrmute.dev/mc/mcdsl v1.3.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
|
||||||
@@ -21,6 +22,7 @@ require (
|
|||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||||
github.com/spf13/pflag v1.0.9 // indirect
|
github.com/spf13/pflag v1.0.9 // indirect
|
||||||
golang.org/x/net v0.48.0 // indirect
|
golang.org/x/net v0.48.0 // indirect
|
||||||
|
golang.org/x/term v0.41.0 // indirect
|
||||||
golang.org/x/text v0.32.0 // indirect
|
golang.org/x/text v0.32.0 // indirect
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect
|
||||||
modernc.org/libc v1.70.0 // indirect
|
modernc.org/libc v1.70.0 // indirect
|
||||||
|
|||||||
10
go.sum
10
go.sum
@@ -1,7 +1,7 @@
|
|||||||
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.3.0 h1:QYmRdGDHjDEyNQpiKqHqPflpwNJcP0cFR9hcfMza/x4=
|
||||||
git.wntrmute.dev/mc/mcdsl v1.2.0/go.mod h1:lXYrAt74ZUix6rx9oVN8d2zH1YJoyp4uxPVKQ+SSxuM=
|
git.wntrmute.dev/mc/mcdsl v1.3.0/go.mod h1:MhYahIu7Sg53lE2zpQ20nlrsoNRjQzOJBAlCmom2wJc=
|
||||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||||
@@ -74,6 +74,8 @@ golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
|||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
|
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
|
||||||
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||||
|
golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU=
|
||||||
|
golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A=
|
||||||
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
|
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
|
||||||
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
|
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
|
||||||
golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
|
golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -86,6 +100,9 @@ func Run(cfg *config.AgentConfig) error {
|
|||||||
grpc.ChainUnaryInterceptor(
|
grpc.ChainUnaryInterceptor(
|
||||||
auth.AuthInterceptor(validator),
|
auth.AuthInterceptor(validator),
|
||||||
),
|
),
|
||||||
|
grpc.ChainStreamInterceptor(
|
||||||
|
auth.StreamAuthInterceptor(validator),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
mcpv1.RegisterMcpAgentServiceServer(server, a)
|
mcpv1.RegisterMcpAgentServiceServer(server, a)
|
||||||
|
|
||||||
|
|||||||
263
internal/agent/certs.go
Normal file
263
internal/agent/certs.go
Normal 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
|
||||||
|
}
|
||||||
392
internal/agent/certs_test.go
Normal file
392
internal/agent/certs_test.go
Normal 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)
|
||||||
|
}
|
||||||
@@ -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 {
|
||||||
|
|||||||
159
internal/agent/deploy_test.go
Normal file
159
internal/agent/deploy_test.go
Normal 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, ®istry.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
265
internal/agent/dns.go
Normal 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
214
internal/agent/dns_test.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
75
internal/agent/logs.go
Normal file
75
internal/agent/logs.go
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
package agent
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"io"
|
||||||
|
|
||||||
|
mcpv1 "git.wntrmute.dev/mc/mcp/gen/mcp/v1"
|
||||||
|
"git.wntrmute.dev/mc/mcp/internal/registry"
|
||||||
|
"git.wntrmute.dev/mc/mcp/internal/runtime"
|
||||||
|
"google.golang.org/grpc/codes"
|
||||||
|
"google.golang.org/grpc/status"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Logs streams container logs for a service component.
|
||||||
|
func (a *Agent) Logs(req *mcpv1.LogsRequest, stream mcpv1.McpAgentService_LogsServer) error {
|
||||||
|
if req.GetService() == "" {
|
||||||
|
return status.Error(codes.InvalidArgument, "service name is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve component name.
|
||||||
|
component := req.GetComponent()
|
||||||
|
if component == "" {
|
||||||
|
components, err := registry.ListComponents(a.DB, req.GetService())
|
||||||
|
if err != nil {
|
||||||
|
return status.Errorf(codes.Internal, "list components: %v", err)
|
||||||
|
}
|
||||||
|
if len(components) == 0 {
|
||||||
|
return status.Error(codes.NotFound, "no components found for service")
|
||||||
|
}
|
||||||
|
component = components[0].Name
|
||||||
|
}
|
||||||
|
|
||||||
|
containerName := ContainerNameFor(req.GetService(), component)
|
||||||
|
|
||||||
|
podman, ok := a.Runtime.(*runtime.Podman)
|
||||||
|
if !ok {
|
||||||
|
return status.Error(codes.Internal, "logs requires podman runtime")
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := podman.Logs(stream.Context(), containerName, int(req.GetTail()), req.GetFollow(), req.GetTimestamps(), req.GetSince())
|
||||||
|
|
||||||
|
a.Logger.Info("running podman logs", "container", containerName, "args", cmd.Args)
|
||||||
|
|
||||||
|
// Podman writes container stdout to its stdout and container stderr
|
||||||
|
// to its stderr. Merge both into a single pipe.
|
||||||
|
pr, pw := io.Pipe()
|
||||||
|
cmd.Stdout = pw
|
||||||
|
cmd.Stderr = pw
|
||||||
|
|
||||||
|
if err := cmd.Start(); err != nil {
|
||||||
|
pw.Close()
|
||||||
|
return status.Errorf(codes.Internal, "start podman logs: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close the write end when the command exits so the scanner finishes.
|
||||||
|
go func() {
|
||||||
|
err := cmd.Wait()
|
||||||
|
if err != nil {
|
||||||
|
a.Logger.Warn("podman logs exited", "container", containerName, "error", err)
|
||||||
|
}
|
||||||
|
pw.Close()
|
||||||
|
}()
|
||||||
|
|
||||||
|
scanner := bufio.NewScanner(pr)
|
||||||
|
for scanner.Scan() {
|
||||||
|
if err := stream.Send(&mcpv1.LogsResponse{
|
||||||
|
Data: append(scanner.Bytes(), '\n'),
|
||||||
|
}); err != nil {
|
||||||
|
_ = cmd.Process.Kill()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -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 ®istry.Component{
|
return ®istry.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
100
internal/agent/undeploy.go
Normal 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
|
||||||
|
}
|
||||||
@@ -206,7 +206,10 @@ func TokenInfoFromContext(ctx context.Context) *TokenInfo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// AuthInterceptor returns a gRPC unary server interceptor that validates
|
// AuthInterceptor returns a gRPC unary server interceptor that validates
|
||||||
// bearer tokens and requires the "admin" role.
|
// bearer tokens. Any authenticated user or system account is accepted,
|
||||||
|
// except guests which are explicitly rejected. Admin role is not required
|
||||||
|
// for agent operations — it is reserved for MCIAS account management and
|
||||||
|
// policy changes.
|
||||||
func AuthInterceptor(validator TokenValidator) grpc.UnaryServerInterceptor {
|
func AuthInterceptor(validator TokenValidator) grpc.UnaryServerInterceptor {
|
||||||
return func(
|
return func(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
@@ -240,9 +243,9 @@ func AuthInterceptor(validator TokenValidator) grpc.UnaryServerInterceptor {
|
|||||||
return nil, status.Error(codes.Unauthenticated, "invalid token")
|
return nil, status.Error(codes.Unauthenticated, "invalid token")
|
||||||
}
|
}
|
||||||
|
|
||||||
if !tokenInfo.HasRole("admin") {
|
if tokenInfo.HasRole("guest") {
|
||||||
slog.Warn("permission denied", "method", info.FullMethod, "user", tokenInfo.Username)
|
slog.Warn("guest access denied", "method", info.FullMethod, "user", tokenInfo.Username)
|
||||||
return nil, status.Error(codes.PermissionDenied, "admin role required")
|
return nil, status.Error(codes.PermissionDenied, "guest access not permitted")
|
||||||
}
|
}
|
||||||
|
|
||||||
slog.Info("rpc", "method", info.FullMethod, "user", tokenInfo.Username, "account_type", tokenInfo.AccountType)
|
slog.Info("rpc", "method", info.FullMethod, "user", tokenInfo.Username, "account_type", tokenInfo.AccountType)
|
||||||
@@ -252,6 +255,52 @@ func AuthInterceptor(validator TokenValidator) grpc.UnaryServerInterceptor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// StreamAuthInterceptor returns a gRPC stream server interceptor with
|
||||||
|
// the same authentication rules as AuthInterceptor.
|
||||||
|
func StreamAuthInterceptor(validator TokenValidator) grpc.StreamServerInterceptor {
|
||||||
|
return func(
|
||||||
|
srv any,
|
||||||
|
ss grpc.ServerStream,
|
||||||
|
info *grpc.StreamServerInfo,
|
||||||
|
handler grpc.StreamHandler,
|
||||||
|
) error {
|
||||||
|
md, ok := metadata.FromIncomingContext(ss.Context())
|
||||||
|
if !ok {
|
||||||
|
return status.Error(codes.Unauthenticated, "missing metadata")
|
||||||
|
}
|
||||||
|
|
||||||
|
authValues := md.Get("authorization")
|
||||||
|
if len(authValues) == 0 {
|
||||||
|
return status.Error(codes.Unauthenticated, "missing authorization header")
|
||||||
|
}
|
||||||
|
|
||||||
|
authHeader := authValues[0]
|
||||||
|
if !strings.HasPrefix(authHeader, "Bearer ") {
|
||||||
|
return status.Error(codes.Unauthenticated, "malformed authorization header")
|
||||||
|
}
|
||||||
|
token := strings.TrimPrefix(authHeader, "Bearer ")
|
||||||
|
|
||||||
|
tokenInfo, err := validator.ValidateToken(ss.Context(), token)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("token validation failed", "method", info.FullMethod, "error", err)
|
||||||
|
return status.Error(codes.Unauthenticated, "token validation failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !tokenInfo.Valid {
|
||||||
|
return status.Error(codes.Unauthenticated, "invalid token")
|
||||||
|
}
|
||||||
|
|
||||||
|
if tokenInfo.HasRole("guest") {
|
||||||
|
slog.Warn("guest access denied", "method", info.FullMethod, "user", tokenInfo.Username)
|
||||||
|
return status.Error(codes.PermissionDenied, "guest access not permitted")
|
||||||
|
}
|
||||||
|
|
||||||
|
slog.Info("rpc", "method", info.FullMethod, "user", tokenInfo.Username, "account_type", tokenInfo.AccountType)
|
||||||
|
|
||||||
|
return handler(srv, ss)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Login authenticates with MCIAS and returns a bearer token.
|
// Login authenticates with MCIAS and returns a bearer token.
|
||||||
func Login(serverURL, caCertPath, username, password string) (string, error) {
|
func Login(serverURL, caCertPath, username, password string) (string, error) {
|
||||||
client, err := newHTTPClient(caCertPath)
|
client, err := newHTTPClient(caCertPath)
|
||||||
|
|||||||
@@ -126,7 +126,7 @@ func TestInterceptorRejectsInvalidToken(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestInterceptorRejectsNonAdmin(t *testing.T) {
|
func TestInterceptorAcceptsRegularUser(t *testing.T) {
|
||||||
server := mockMCIAS(t, func(authHeader string) (any, int) {
|
server := mockMCIAS(t, func(authHeader string) (any, int) {
|
||||||
return &TokenInfo{
|
return &TokenInfo{
|
||||||
Valid: true,
|
Valid: true,
|
||||||
@@ -142,6 +142,28 @@ func TestInterceptorRejectsNonAdmin(t *testing.T) {
|
|||||||
md := metadata.Pairs("authorization", "Bearer user-token")
|
md := metadata.Pairs("authorization", "Bearer user-token")
|
||||||
ctx := metadata.NewIncomingContext(context.Background(), md)
|
ctx := metadata.NewIncomingContext(context.Background(), md)
|
||||||
|
|
||||||
|
_, err := callInterceptor(ctx, v)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected regular user to be accepted, got %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInterceptorRejectsGuest(t *testing.T) {
|
||||||
|
server := mockMCIAS(t, func(authHeader string) (any, int) {
|
||||||
|
return &TokenInfo{
|
||||||
|
Valid: true,
|
||||||
|
Username: "visitor",
|
||||||
|
Roles: []string{"guest"},
|
||||||
|
AccountType: "human",
|
||||||
|
}, http.StatusOK
|
||||||
|
})
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
v := validatorFromServer(t, server)
|
||||||
|
|
||||||
|
md := metadata.Pairs("authorization", "Bearer guest-token")
|
||||||
|
ctx := metadata.NewIncomingContext(context.Background(), md)
|
||||||
|
|
||||||
_, err := callInterceptor(ctx, v)
|
_, err := callInterceptor(ctx, v)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatal("expected error, got nil")
|
t.Fatal("expected error, got nil")
|
||||||
|
|||||||
@@ -10,13 +10,55 @@ import (
|
|||||||
|
|
||||||
// AgentConfig is the configuration for the mcp-agent daemon.
|
// AgentConfig is the configuration for the mcp-agent daemon.
|
||||||
type AgentConfig struct {
|
type AgentConfig struct {
|
||||||
Server ServerConfig `toml:"server"`
|
Server ServerConfig `toml:"server"`
|
||||||
Database DatabaseConfig `toml:"database"`
|
Database DatabaseConfig `toml:"database"`
|
||||||
MCIAS MCIASConfig `toml:"mcias"`
|
MCIAS MCIASConfig `toml:"mcias"`
|
||||||
Agent AgentSettings `toml:"agent"`
|
Agent AgentSettings `toml:"agent"`
|
||||||
MCProxy MCProxyConfig `toml:"mcproxy"`
|
MCProxy MCProxyConfig `toml:"mcproxy"`
|
||||||
Monitor MonitorConfig `toml:"monitor"`
|
Metacrypt MetacryptConfig `toml:"metacrypt"`
|
||||||
Log LogConfig `toml:"log"`
|
MCNS MCNSConfig `toml:"mcns"`
|
||||||
|
Monitor MonitorConfig `toml:"monitor"`
|
||||||
|
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.
|
||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -178,6 +178,61 @@ func (p *Podman) Inspect(ctx context.Context, name string) (ContainerInfo, error
|
|||||||
return info, nil
|
return info, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Logs returns an exec.Cmd that streams container logs. For containers
|
||||||
|
// using the journald log driver, it uses journalctl (podman logs can't
|
||||||
|
// read journald outside the originating user session). For k8s-file or
|
||||||
|
// other drivers, it uses podman logs directly.
|
||||||
|
func (p *Podman) Logs(ctx context.Context, containerName string, tail int, follow, timestamps bool, since string) *exec.Cmd {
|
||||||
|
// Check if this container uses the journald log driver.
|
||||||
|
inspectCmd := exec.CommandContext(ctx, p.command(), "inspect", "--format", "{{.HostConfig.LogConfig.Type}}", containerName) //nolint:gosec
|
||||||
|
if out, err := inspectCmd.Output(); err == nil && strings.TrimSpace(string(out)) == "journald" {
|
||||||
|
return p.journalLogs(ctx, containerName, tail, follow, since)
|
||||||
|
}
|
||||||
|
|
||||||
|
args := []string{"logs"}
|
||||||
|
if tail > 0 {
|
||||||
|
args = append(args, "--tail", fmt.Sprintf("%d", tail))
|
||||||
|
}
|
||||||
|
if follow {
|
||||||
|
args = append(args, "--follow")
|
||||||
|
}
|
||||||
|
if timestamps {
|
||||||
|
args = append(args, "--timestamps")
|
||||||
|
}
|
||||||
|
if since != "" {
|
||||||
|
args = append(args, "--since", since)
|
||||||
|
}
|
||||||
|
args = append(args, containerName)
|
||||||
|
return exec.CommandContext(ctx, p.command(), args...) //nolint:gosec // args built programmatically
|
||||||
|
}
|
||||||
|
|
||||||
|
// journalLogs returns a journalctl command filtered by container name.
|
||||||
|
func (p *Podman) journalLogs(ctx context.Context, containerName string, tail int, follow bool, since string) *exec.Cmd {
|
||||||
|
args := []string{"--no-pager", "--output", "cat", "CONTAINER_NAME=" + containerName}
|
||||||
|
if tail > 0 {
|
||||||
|
args = append(args, "--lines", fmt.Sprintf("%d", tail))
|
||||||
|
}
|
||||||
|
if follow {
|
||||||
|
args = append(args, "--follow")
|
||||||
|
}
|
||||||
|
if since != "" {
|
||||||
|
args = append(args, "--since", since)
|
||||||
|
}
|
||||||
|
return exec.CommandContext(ctx, "journalctl", args...) //nolint:gosec // args built programmatically
|
||||||
|
}
|
||||||
|
|
||||||
|
// Login authenticates to a container registry using the given token as
|
||||||
|
// the password. This enables non-interactive push with service account
|
||||||
|
// tokens (MCR accepts MCIAS JWTs as passwords).
|
||||||
|
func (p *Podman) Login(ctx context.Context, registry, username, token string) error {
|
||||||
|
cmd := exec.CommandContext(ctx, p.command(), "login", "--username", username, "--password-stdin", registry) //nolint:gosec // args built programmatically
|
||||||
|
cmd.Stdin = strings.NewReader(token)
|
||||||
|
if out, err := cmd.CombinedOutput(); err != nil {
|
||||||
|
return fmt.Errorf("podman login %q: %w: %s", registry, err, out)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// Build builds a container image from a Dockerfile.
|
// Build builds a container image from a Dockerfile.
|
||||||
func (p *Podman) Build(ctx context.Context, image, contextDir, dockerfile string) error {
|
func (p *Podman) Build(ctx context.Context, image, contextDir, dockerfile string) error {
|
||||||
args := []string{"build", "-t", image, "-f", dockerfile, contextDir}
|
args := []string{"build", "-t", image, "-f", dockerfile, contextDir}
|
||||||
@@ -199,25 +254,27 @@ 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
|
||||||
}
|
}
|
||||||
|
|
||||||
// podmanPSEntry is a single entry from podman ps --format json.
|
// podmanPSEntry is a single entry from podman ps --format json.
|
||||||
type podmanPSEntry struct {
|
type podmanPSEntry struct {
|
||||||
Names []string `json:"Names"`
|
Names []string `json:"Names"`
|
||||||
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 +296,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
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -25,8 +25,8 @@ type ServiceDef struct {
|
|||||||
|
|
||||||
// BuildDef describes how to build container images for a service.
|
// BuildDef describes how to build container images for a service.
|
||||||
type BuildDef struct {
|
type BuildDef struct {
|
||||||
Images map[string]string `toml:"images"`
|
Images map[string]string `toml:"images"`
|
||||||
UsesMCDSL bool `toml:"uses_mcdsl,omitempty"`
|
UsesMCDSL bool `toml:"uses_mcdsl,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// RouteDef describes a route for a component, used for automatic port
|
// RouteDef describes a route for a component, used for automatic port
|
||||||
@@ -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,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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);
|
||||||
@@ -32,13 +33,16 @@ service McpAgentService {
|
|||||||
|
|
||||||
// Node
|
// Node
|
||||||
rpc NodeStatus(NodeStatusRequest) returns (NodeStatusResponse);
|
rpc NodeStatus(NodeStatusRequest) returns (NodeStatusResponse);
|
||||||
|
|
||||||
|
// Logs
|
||||||
|
rpc Logs(LogsRequest) returns (stream LogsResponse);
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Service lifecycle ---
|
// --- Service lifecycle ---
|
||||||
|
|
||||||
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 +106,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 {
|
||||||
@@ -273,3 +285,18 @@ message PurgeResult {
|
|||||||
// Why eligible, or why refused.
|
// Why eligible, or why refused.
|
||||||
string reason = 4;
|
string reason = 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Logs ---
|
||||||
|
|
||||||
|
message LogsRequest {
|
||||||
|
string service = 1;
|
||||||
|
string component = 2; // optional; defaults to first/only component
|
||||||
|
int32 tail = 3; // number of lines from the end (0 = all)
|
||||||
|
bool follow = 4; // stream new output
|
||||||
|
bool timestamps = 5; // prepend timestamps
|
||||||
|
string since = 6; // show logs since (e.g., "2h", "2026-03-28T00:00:00Z")
|
||||||
|
}
|
||||||
|
|
||||||
|
message LogsResponse {
|
||||||
|
bytes data = 1;
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
22
vendor/git.wntrmute.dev/mc/mcdsl/terminal/terminal.go
vendored
Normal file
22
vendor/git.wntrmute.dev/mc/mcdsl/terminal/terminal.go
vendored
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
// Package terminal provides secure terminal input helpers for CLI tools.
|
||||||
|
package terminal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"golang.org/x/term"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ReadPassword prints the given prompt to stderr and reads a password
|
||||||
|
// from the terminal with echo disabled. It prints a newline after the
|
||||||
|
// input is complete so the cursor advances normally.
|
||||||
|
func ReadPassword(prompt string) (string, error) {
|
||||||
|
fmt.Fprint(os.Stderr, prompt)
|
||||||
|
b, err := term.ReadPassword(int(os.Stdin.Fd())) //nolint:gosec // fd fits in int
|
||||||
|
fmt.Fprintln(os.Stderr)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return string(b), nil
|
||||||
|
}
|
||||||
8
vendor/golang.org/x/sys/plan9/asm.s
generated
vendored
Normal file
8
vendor/golang.org/x/sys/plan9/asm.s
generated
vendored
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
// Copyright 2014 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
#include "textflag.h"
|
||||||
|
|
||||||
|
TEXT ·use(SB),NOSPLIT,$0
|
||||||
|
RET
|
||||||
30
vendor/golang.org/x/sys/plan9/asm_plan9_386.s
generated
vendored
Normal file
30
vendor/golang.org/x/sys/plan9/asm_plan9_386.s
generated
vendored
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
// Copyright 2009 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
#include "textflag.h"
|
||||||
|
|
||||||
|
//
|
||||||
|
// System call support for 386, Plan 9
|
||||||
|
//
|
||||||
|
|
||||||
|
// Just jump to package syscall's implementation for all these functions.
|
||||||
|
// The runtime may know about them.
|
||||||
|
|
||||||
|
TEXT ·Syscall(SB),NOSPLIT,$0-32
|
||||||
|
JMP syscall·Syscall(SB)
|
||||||
|
|
||||||
|
TEXT ·Syscall6(SB),NOSPLIT,$0-44
|
||||||
|
JMP syscall·Syscall6(SB)
|
||||||
|
|
||||||
|
TEXT ·RawSyscall(SB),NOSPLIT,$0-28
|
||||||
|
JMP syscall·RawSyscall(SB)
|
||||||
|
|
||||||
|
TEXT ·RawSyscall6(SB),NOSPLIT,$0-40
|
||||||
|
JMP syscall·RawSyscall6(SB)
|
||||||
|
|
||||||
|
TEXT ·seek(SB),NOSPLIT,$0-36
|
||||||
|
JMP syscall·seek(SB)
|
||||||
|
|
||||||
|
TEXT ·exit(SB),NOSPLIT,$4-4
|
||||||
|
JMP syscall·exit(SB)
|
||||||
30
vendor/golang.org/x/sys/plan9/asm_plan9_amd64.s
generated
vendored
Normal file
30
vendor/golang.org/x/sys/plan9/asm_plan9_amd64.s
generated
vendored
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
// Copyright 2009 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
#include "textflag.h"
|
||||||
|
|
||||||
|
//
|
||||||
|
// System call support for amd64, Plan 9
|
||||||
|
//
|
||||||
|
|
||||||
|
// Just jump to package syscall's implementation for all these functions.
|
||||||
|
// The runtime may know about them.
|
||||||
|
|
||||||
|
TEXT ·Syscall(SB),NOSPLIT,$0-64
|
||||||
|
JMP syscall·Syscall(SB)
|
||||||
|
|
||||||
|
TEXT ·Syscall6(SB),NOSPLIT,$0-88
|
||||||
|
JMP syscall·Syscall6(SB)
|
||||||
|
|
||||||
|
TEXT ·RawSyscall(SB),NOSPLIT,$0-56
|
||||||
|
JMP syscall·RawSyscall(SB)
|
||||||
|
|
||||||
|
TEXT ·RawSyscall6(SB),NOSPLIT,$0-80
|
||||||
|
JMP syscall·RawSyscall6(SB)
|
||||||
|
|
||||||
|
TEXT ·seek(SB),NOSPLIT,$0-56
|
||||||
|
JMP syscall·seek(SB)
|
||||||
|
|
||||||
|
TEXT ·exit(SB),NOSPLIT,$8-8
|
||||||
|
JMP syscall·exit(SB)
|
||||||
25
vendor/golang.org/x/sys/plan9/asm_plan9_arm.s
generated
vendored
Normal file
25
vendor/golang.org/x/sys/plan9/asm_plan9_arm.s
generated
vendored
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
// Copyright 2009 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
#include "textflag.h"
|
||||||
|
|
||||||
|
// System call support for plan9 on arm
|
||||||
|
|
||||||
|
// Just jump to package syscall's implementation for all these functions.
|
||||||
|
// The runtime may know about them.
|
||||||
|
|
||||||
|
TEXT ·Syscall(SB),NOSPLIT,$0-32
|
||||||
|
JMP syscall·Syscall(SB)
|
||||||
|
|
||||||
|
TEXT ·Syscall6(SB),NOSPLIT,$0-44
|
||||||
|
JMP syscall·Syscall6(SB)
|
||||||
|
|
||||||
|
TEXT ·RawSyscall(SB),NOSPLIT,$0-28
|
||||||
|
JMP syscall·RawSyscall(SB)
|
||||||
|
|
||||||
|
TEXT ·RawSyscall6(SB),NOSPLIT,$0-40
|
||||||
|
JMP syscall·RawSyscall6(SB)
|
||||||
|
|
||||||
|
TEXT ·seek(SB),NOSPLIT,$0-36
|
||||||
|
JMP syscall·exit(SB)
|
||||||
70
vendor/golang.org/x/sys/plan9/const_plan9.go
generated
vendored
Normal file
70
vendor/golang.org/x/sys/plan9/const_plan9.go
generated
vendored
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
package plan9
|
||||||
|
|
||||||
|
// Plan 9 Constants
|
||||||
|
|
||||||
|
// Open modes
|
||||||
|
const (
|
||||||
|
O_RDONLY = 0
|
||||||
|
O_WRONLY = 1
|
||||||
|
O_RDWR = 2
|
||||||
|
O_TRUNC = 16
|
||||||
|
O_CLOEXEC = 32
|
||||||
|
O_EXCL = 0x1000
|
||||||
|
)
|
||||||
|
|
||||||
|
// Rfork flags
|
||||||
|
const (
|
||||||
|
RFNAMEG = 1 << 0
|
||||||
|
RFENVG = 1 << 1
|
||||||
|
RFFDG = 1 << 2
|
||||||
|
RFNOTEG = 1 << 3
|
||||||
|
RFPROC = 1 << 4
|
||||||
|
RFMEM = 1 << 5
|
||||||
|
RFNOWAIT = 1 << 6
|
||||||
|
RFCNAMEG = 1 << 10
|
||||||
|
RFCENVG = 1 << 11
|
||||||
|
RFCFDG = 1 << 12
|
||||||
|
RFREND = 1 << 13
|
||||||
|
RFNOMNT = 1 << 14
|
||||||
|
)
|
||||||
|
|
||||||
|
// Qid.Type bits
|
||||||
|
const (
|
||||||
|
QTDIR = 0x80
|
||||||
|
QTAPPEND = 0x40
|
||||||
|
QTEXCL = 0x20
|
||||||
|
QTMOUNT = 0x10
|
||||||
|
QTAUTH = 0x08
|
||||||
|
QTTMP = 0x04
|
||||||
|
QTFILE = 0x00
|
||||||
|
)
|
||||||
|
|
||||||
|
// Dir.Mode bits
|
||||||
|
const (
|
||||||
|
DMDIR = 0x80000000
|
||||||
|
DMAPPEND = 0x40000000
|
||||||
|
DMEXCL = 0x20000000
|
||||||
|
DMMOUNT = 0x10000000
|
||||||
|
DMAUTH = 0x08000000
|
||||||
|
DMTMP = 0x04000000
|
||||||
|
DMREAD = 0x4
|
||||||
|
DMWRITE = 0x2
|
||||||
|
DMEXEC = 0x1
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
STATMAX = 65535
|
||||||
|
ERRMAX = 128
|
||||||
|
STATFIXLEN = 49
|
||||||
|
)
|
||||||
|
|
||||||
|
// Mount and bind flags
|
||||||
|
const (
|
||||||
|
MREPL = 0x0000
|
||||||
|
MBEFORE = 0x0001
|
||||||
|
MAFTER = 0x0002
|
||||||
|
MORDER = 0x0003
|
||||||
|
MCREATE = 0x0004
|
||||||
|
MCACHE = 0x0010
|
||||||
|
MMASK = 0x0017
|
||||||
|
)
|
||||||
212
vendor/golang.org/x/sys/plan9/dir_plan9.go
generated
vendored
Normal file
212
vendor/golang.org/x/sys/plan9/dir_plan9.go
generated
vendored
Normal file
@@ -0,0 +1,212 @@
|
|||||||
|
// Copyright 2012 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
// Plan 9 directory marshalling. See intro(5).
|
||||||
|
|
||||||
|
package plan9
|
||||||
|
|
||||||
|
import "errors"
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrShortStat = errors.New("stat buffer too short")
|
||||||
|
ErrBadStat = errors.New("malformed stat buffer")
|
||||||
|
ErrBadName = errors.New("bad character in file name")
|
||||||
|
)
|
||||||
|
|
||||||
|
// A Qid represents a 9P server's unique identification for a file.
|
||||||
|
type Qid struct {
|
||||||
|
Path uint64 // the file server's unique identification for the file
|
||||||
|
Vers uint32 // version number for given Path
|
||||||
|
Type uint8 // the type of the file (plan9.QTDIR for example)
|
||||||
|
}
|
||||||
|
|
||||||
|
// A Dir contains the metadata for a file.
|
||||||
|
type Dir struct {
|
||||||
|
// system-modified data
|
||||||
|
Type uint16 // server type
|
||||||
|
Dev uint32 // server subtype
|
||||||
|
|
||||||
|
// file data
|
||||||
|
Qid Qid // unique id from server
|
||||||
|
Mode uint32 // permissions
|
||||||
|
Atime uint32 // last read time
|
||||||
|
Mtime uint32 // last write time
|
||||||
|
Length int64 // file length
|
||||||
|
Name string // last element of path
|
||||||
|
Uid string // owner name
|
||||||
|
Gid string // group name
|
||||||
|
Muid string // last modifier name
|
||||||
|
}
|
||||||
|
|
||||||
|
var nullDir = Dir{
|
||||||
|
Type: ^uint16(0),
|
||||||
|
Dev: ^uint32(0),
|
||||||
|
Qid: Qid{
|
||||||
|
Path: ^uint64(0),
|
||||||
|
Vers: ^uint32(0),
|
||||||
|
Type: ^uint8(0),
|
||||||
|
},
|
||||||
|
Mode: ^uint32(0),
|
||||||
|
Atime: ^uint32(0),
|
||||||
|
Mtime: ^uint32(0),
|
||||||
|
Length: ^int64(0),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Null assigns special "don't touch" values to members of d to
|
||||||
|
// avoid modifying them during plan9.Wstat.
|
||||||
|
func (d *Dir) Null() { *d = nullDir }
|
||||||
|
|
||||||
|
// Marshal encodes a 9P stat message corresponding to d into b
|
||||||
|
//
|
||||||
|
// If there isn't enough space in b for a stat message, ErrShortStat is returned.
|
||||||
|
func (d *Dir) Marshal(b []byte) (n int, err error) {
|
||||||
|
n = STATFIXLEN + len(d.Name) + len(d.Uid) + len(d.Gid) + len(d.Muid)
|
||||||
|
if n > len(b) {
|
||||||
|
return n, ErrShortStat
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, c := range d.Name {
|
||||||
|
if c == '/' {
|
||||||
|
return n, ErrBadName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
b = pbit16(b, uint16(n)-2)
|
||||||
|
b = pbit16(b, d.Type)
|
||||||
|
b = pbit32(b, d.Dev)
|
||||||
|
b = pbit8(b, d.Qid.Type)
|
||||||
|
b = pbit32(b, d.Qid.Vers)
|
||||||
|
b = pbit64(b, d.Qid.Path)
|
||||||
|
b = pbit32(b, d.Mode)
|
||||||
|
b = pbit32(b, d.Atime)
|
||||||
|
b = pbit32(b, d.Mtime)
|
||||||
|
b = pbit64(b, uint64(d.Length))
|
||||||
|
b = pstring(b, d.Name)
|
||||||
|
b = pstring(b, d.Uid)
|
||||||
|
b = pstring(b, d.Gid)
|
||||||
|
b = pstring(b, d.Muid)
|
||||||
|
|
||||||
|
return n, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalDir decodes a single 9P stat message from b and returns the resulting Dir.
|
||||||
|
//
|
||||||
|
// If b is too small to hold a valid stat message, ErrShortStat is returned.
|
||||||
|
//
|
||||||
|
// If the stat message itself is invalid, ErrBadStat is returned.
|
||||||
|
func UnmarshalDir(b []byte) (*Dir, error) {
|
||||||
|
if len(b) < STATFIXLEN {
|
||||||
|
return nil, ErrShortStat
|
||||||
|
}
|
||||||
|
size, buf := gbit16(b)
|
||||||
|
if len(b) != int(size)+2 {
|
||||||
|
return nil, ErrBadStat
|
||||||
|
}
|
||||||
|
b = buf
|
||||||
|
|
||||||
|
var d Dir
|
||||||
|
d.Type, b = gbit16(b)
|
||||||
|
d.Dev, b = gbit32(b)
|
||||||
|
d.Qid.Type, b = gbit8(b)
|
||||||
|
d.Qid.Vers, b = gbit32(b)
|
||||||
|
d.Qid.Path, b = gbit64(b)
|
||||||
|
d.Mode, b = gbit32(b)
|
||||||
|
d.Atime, b = gbit32(b)
|
||||||
|
d.Mtime, b = gbit32(b)
|
||||||
|
|
||||||
|
n, b := gbit64(b)
|
||||||
|
d.Length = int64(n)
|
||||||
|
|
||||||
|
var ok bool
|
||||||
|
if d.Name, b, ok = gstring(b); !ok {
|
||||||
|
return nil, ErrBadStat
|
||||||
|
}
|
||||||
|
if d.Uid, b, ok = gstring(b); !ok {
|
||||||
|
return nil, ErrBadStat
|
||||||
|
}
|
||||||
|
if d.Gid, b, ok = gstring(b); !ok {
|
||||||
|
return nil, ErrBadStat
|
||||||
|
}
|
||||||
|
if d.Muid, b, ok = gstring(b); !ok {
|
||||||
|
return nil, ErrBadStat
|
||||||
|
}
|
||||||
|
|
||||||
|
return &d, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// pbit8 copies the 8-bit number v to b and returns the remaining slice of b.
|
||||||
|
func pbit8(b []byte, v uint8) []byte {
|
||||||
|
b[0] = byte(v)
|
||||||
|
return b[1:]
|
||||||
|
}
|
||||||
|
|
||||||
|
// pbit16 copies the 16-bit number v to b in little-endian order and returns the remaining slice of b.
|
||||||
|
func pbit16(b []byte, v uint16) []byte {
|
||||||
|
b[0] = byte(v)
|
||||||
|
b[1] = byte(v >> 8)
|
||||||
|
return b[2:]
|
||||||
|
}
|
||||||
|
|
||||||
|
// pbit32 copies the 32-bit number v to b in little-endian order and returns the remaining slice of b.
|
||||||
|
func pbit32(b []byte, v uint32) []byte {
|
||||||
|
b[0] = byte(v)
|
||||||
|
b[1] = byte(v >> 8)
|
||||||
|
b[2] = byte(v >> 16)
|
||||||
|
b[3] = byte(v >> 24)
|
||||||
|
return b[4:]
|
||||||
|
}
|
||||||
|
|
||||||
|
// pbit64 copies the 64-bit number v to b in little-endian order and returns the remaining slice of b.
|
||||||
|
func pbit64(b []byte, v uint64) []byte {
|
||||||
|
b[0] = byte(v)
|
||||||
|
b[1] = byte(v >> 8)
|
||||||
|
b[2] = byte(v >> 16)
|
||||||
|
b[3] = byte(v >> 24)
|
||||||
|
b[4] = byte(v >> 32)
|
||||||
|
b[5] = byte(v >> 40)
|
||||||
|
b[6] = byte(v >> 48)
|
||||||
|
b[7] = byte(v >> 56)
|
||||||
|
return b[8:]
|
||||||
|
}
|
||||||
|
|
||||||
|
// pstring copies the string s to b, prepending it with a 16-bit length in little-endian order, and
|
||||||
|
// returning the remaining slice of b..
|
||||||
|
func pstring(b []byte, s string) []byte {
|
||||||
|
b = pbit16(b, uint16(len(s)))
|
||||||
|
n := copy(b, s)
|
||||||
|
return b[n:]
|
||||||
|
}
|
||||||
|
|
||||||
|
// gbit8 reads an 8-bit number from b and returns it with the remaining slice of b.
|
||||||
|
func gbit8(b []byte) (uint8, []byte) {
|
||||||
|
return uint8(b[0]), b[1:]
|
||||||
|
}
|
||||||
|
|
||||||
|
// gbit16 reads a 16-bit number in little-endian order from b and returns it with the remaining slice of b.
|
||||||
|
func gbit16(b []byte) (uint16, []byte) {
|
||||||
|
return uint16(b[0]) | uint16(b[1])<<8, b[2:]
|
||||||
|
}
|
||||||
|
|
||||||
|
// gbit32 reads a 32-bit number in little-endian order from b and returns it with the remaining slice of b.
|
||||||
|
func gbit32(b []byte) (uint32, []byte) {
|
||||||
|
return uint32(b[0]) | uint32(b[1])<<8 | uint32(b[2])<<16 | uint32(b[3])<<24, b[4:]
|
||||||
|
}
|
||||||
|
|
||||||
|
// gbit64 reads a 64-bit number in little-endian order from b and returns it with the remaining slice of b.
|
||||||
|
func gbit64(b []byte) (uint64, []byte) {
|
||||||
|
lo := uint32(b[0]) | uint32(b[1])<<8 | uint32(b[2])<<16 | uint32(b[3])<<24
|
||||||
|
hi := uint32(b[4]) | uint32(b[5])<<8 | uint32(b[6])<<16 | uint32(b[7])<<24
|
||||||
|
return uint64(lo) | uint64(hi)<<32, b[8:]
|
||||||
|
}
|
||||||
|
|
||||||
|
// gstring reads a string from b, prefixed with a 16-bit length in little-endian order.
|
||||||
|
// It returns the string with the remaining slice of b and a boolean. If the length is
|
||||||
|
// greater than the number of bytes in b, the boolean will be false.
|
||||||
|
func gstring(b []byte) (string, []byte, bool) {
|
||||||
|
n, b := gbit16(b)
|
||||||
|
if int(n) > len(b) {
|
||||||
|
return "", b, false
|
||||||
|
}
|
||||||
|
return string(b[:n]), b[n:], true
|
||||||
|
}
|
||||||
31
vendor/golang.org/x/sys/plan9/env_plan9.go
generated
vendored
Normal file
31
vendor/golang.org/x/sys/plan9/env_plan9.go
generated
vendored
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
// Copyright 2011 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
// Plan 9 environment variables.
|
||||||
|
|
||||||
|
package plan9
|
||||||
|
|
||||||
|
import (
|
||||||
|
"syscall"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Getenv(key string) (value string, found bool) {
|
||||||
|
return syscall.Getenv(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Setenv(key, value string) error {
|
||||||
|
return syscall.Setenv(key, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Clearenv() {
|
||||||
|
syscall.Clearenv()
|
||||||
|
}
|
||||||
|
|
||||||
|
func Environ() []string {
|
||||||
|
return syscall.Environ()
|
||||||
|
}
|
||||||
|
|
||||||
|
func Unsetenv(key string) error {
|
||||||
|
return syscall.Unsetenv(key)
|
||||||
|
}
|
||||||
50
vendor/golang.org/x/sys/plan9/errors_plan9.go
generated
vendored
Normal file
50
vendor/golang.org/x/sys/plan9/errors_plan9.go
generated
vendored
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
// Copyright 2011 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package plan9
|
||||||
|
|
||||||
|
import "syscall"
|
||||||
|
|
||||||
|
// Constants
|
||||||
|
const (
|
||||||
|
// Invented values to support what package os expects.
|
||||||
|
O_CREAT = 0x02000
|
||||||
|
O_APPEND = 0x00400
|
||||||
|
O_NOCTTY = 0x00000
|
||||||
|
O_NONBLOCK = 0x00000
|
||||||
|
O_SYNC = 0x00000
|
||||||
|
O_ASYNC = 0x00000
|
||||||
|
|
||||||
|
S_IFMT = 0x1f000
|
||||||
|
S_IFIFO = 0x1000
|
||||||
|
S_IFCHR = 0x2000
|
||||||
|
S_IFDIR = 0x4000
|
||||||
|
S_IFBLK = 0x6000
|
||||||
|
S_IFREG = 0x8000
|
||||||
|
S_IFLNK = 0xa000
|
||||||
|
S_IFSOCK = 0xc000
|
||||||
|
)
|
||||||
|
|
||||||
|
// Errors
|
||||||
|
var (
|
||||||
|
EINVAL = syscall.NewError("bad arg in system call")
|
||||||
|
ENOTDIR = syscall.NewError("not a directory")
|
||||||
|
EISDIR = syscall.NewError("file is a directory")
|
||||||
|
ENOENT = syscall.NewError("file does not exist")
|
||||||
|
EEXIST = syscall.NewError("file already exists")
|
||||||
|
EMFILE = syscall.NewError("no free file descriptors")
|
||||||
|
EIO = syscall.NewError("i/o error")
|
||||||
|
ENAMETOOLONG = syscall.NewError("file name too long")
|
||||||
|
EINTR = syscall.NewError("interrupted")
|
||||||
|
EPERM = syscall.NewError("permission denied")
|
||||||
|
EBUSY = syscall.NewError("no free devices")
|
||||||
|
ETIMEDOUT = syscall.NewError("connection timed out")
|
||||||
|
EPLAN9 = syscall.NewError("not supported by plan 9")
|
||||||
|
|
||||||
|
// The following errors do not correspond to any
|
||||||
|
// Plan 9 system messages. Invented to support
|
||||||
|
// what package os and others expect.
|
||||||
|
EACCES = syscall.NewError("access permission denied")
|
||||||
|
EAFNOSUPPORT = syscall.NewError("address family not supported by protocol")
|
||||||
|
)
|
||||||
150
vendor/golang.org/x/sys/plan9/mkall.sh
generated
vendored
Normal file
150
vendor/golang.org/x/sys/plan9/mkall.sh
generated
vendored
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Copyright 2009 The Go Authors. All rights reserved.
|
||||||
|
# Use of this source code is governed by a BSD-style
|
||||||
|
# license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
# The plan9 package provides access to the raw system call
|
||||||
|
# interface of the underlying operating system. Porting Go to
|
||||||
|
# a new architecture/operating system combination requires
|
||||||
|
# some manual effort, though there are tools that automate
|
||||||
|
# much of the process. The auto-generated files have names
|
||||||
|
# beginning with z.
|
||||||
|
#
|
||||||
|
# This script runs or (given -n) prints suggested commands to generate z files
|
||||||
|
# for the current system. Running those commands is not automatic.
|
||||||
|
# This script is documentation more than anything else.
|
||||||
|
#
|
||||||
|
# * asm_${GOOS}_${GOARCH}.s
|
||||||
|
#
|
||||||
|
# This hand-written assembly file implements system call dispatch.
|
||||||
|
# There are three entry points:
|
||||||
|
#
|
||||||
|
# func Syscall(trap, a1, a2, a3 uintptr) (r1, r2, err uintptr);
|
||||||
|
# func Syscall6(trap, a1, a2, a3, a4, a5, a6 uintptr) (r1, r2, err uintptr);
|
||||||
|
# func RawSyscall(trap, a1, a2, a3 uintptr) (r1, r2, err uintptr);
|
||||||
|
#
|
||||||
|
# The first and second are the standard ones; they differ only in
|
||||||
|
# how many arguments can be passed to the kernel.
|
||||||
|
# The third is for low-level use by the ForkExec wrapper;
|
||||||
|
# unlike the first two, it does not call into the scheduler to
|
||||||
|
# let it know that a system call is running.
|
||||||
|
#
|
||||||
|
# * syscall_${GOOS}.go
|
||||||
|
#
|
||||||
|
# This hand-written Go file implements system calls that need
|
||||||
|
# special handling and lists "//sys" comments giving prototypes
|
||||||
|
# for ones that can be auto-generated. Mksyscall reads those
|
||||||
|
# comments to generate the stubs.
|
||||||
|
#
|
||||||
|
# * syscall_${GOOS}_${GOARCH}.go
|
||||||
|
#
|
||||||
|
# Same as syscall_${GOOS}.go except that it contains code specific
|
||||||
|
# to ${GOOS} on one particular architecture.
|
||||||
|
#
|
||||||
|
# * types_${GOOS}.c
|
||||||
|
#
|
||||||
|
# This hand-written C file includes standard C headers and then
|
||||||
|
# creates typedef or enum names beginning with a dollar sign
|
||||||
|
# (use of $ in variable names is a gcc extension). The hardest
|
||||||
|
# part about preparing this file is figuring out which headers to
|
||||||
|
# include and which symbols need to be #defined to get the
|
||||||
|
# actual data structures that pass through to the kernel system calls.
|
||||||
|
# Some C libraries present alternate versions for binary compatibility
|
||||||
|
# and translate them on the way in and out of system calls, but
|
||||||
|
# there is almost always a #define that can get the real ones.
|
||||||
|
# See types_darwin.c and types_linux.c for examples.
|
||||||
|
#
|
||||||
|
# * zerror_${GOOS}_${GOARCH}.go
|
||||||
|
#
|
||||||
|
# This machine-generated file defines the system's error numbers,
|
||||||
|
# error strings, and signal numbers. The generator is "mkerrors.sh".
|
||||||
|
# Usually no arguments are needed, but mkerrors.sh will pass its
|
||||||
|
# arguments on to godefs.
|
||||||
|
#
|
||||||
|
# * zsyscall_${GOOS}_${GOARCH}.go
|
||||||
|
#
|
||||||
|
# Generated by mksyscall.pl; see syscall_${GOOS}.go above.
|
||||||
|
#
|
||||||
|
# * zsysnum_${GOOS}_${GOARCH}.go
|
||||||
|
#
|
||||||
|
# Generated by mksysnum_${GOOS}.
|
||||||
|
#
|
||||||
|
# * ztypes_${GOOS}_${GOARCH}.go
|
||||||
|
#
|
||||||
|
# Generated by godefs; see types_${GOOS}.c above.
|
||||||
|
|
||||||
|
GOOSARCH="${GOOS}_${GOARCH}"
|
||||||
|
|
||||||
|
# defaults
|
||||||
|
mksyscall="go run mksyscall.go"
|
||||||
|
mkerrors="./mkerrors.sh"
|
||||||
|
zerrors="zerrors_$GOOSARCH.go"
|
||||||
|
mksysctl=""
|
||||||
|
zsysctl="zsysctl_$GOOSARCH.go"
|
||||||
|
mksysnum=
|
||||||
|
mktypes=
|
||||||
|
run="sh"
|
||||||
|
|
||||||
|
case "$1" in
|
||||||
|
-syscalls)
|
||||||
|
for i in zsyscall*go
|
||||||
|
do
|
||||||
|
sed 1q $i | sed 's;^// ;;' | sh > _$i && gofmt < _$i > $i
|
||||||
|
rm _$i
|
||||||
|
done
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
-n)
|
||||||
|
run="cat"
|
||||||
|
shift
|
||||||
|
esac
|
||||||
|
|
||||||
|
case "$#" in
|
||||||
|
0)
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo 'usage: mkall.sh [-n]' 1>&2
|
||||||
|
exit 2
|
||||||
|
esac
|
||||||
|
|
||||||
|
case "$GOOSARCH" in
|
||||||
|
_* | *_ | _)
|
||||||
|
echo 'undefined $GOOS_$GOARCH:' "$GOOSARCH" 1>&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
plan9_386)
|
||||||
|
mkerrors=
|
||||||
|
mksyscall="go run mksyscall.go -l32 -plan9 -tags plan9,386"
|
||||||
|
mksysnum="./mksysnum_plan9.sh /n/sources/plan9/sys/src/libc/9syscall/sys.h"
|
||||||
|
mktypes="XXX"
|
||||||
|
;;
|
||||||
|
plan9_amd64)
|
||||||
|
mkerrors=
|
||||||
|
mksyscall="go run mksyscall.go -l32 -plan9 -tags plan9,amd64"
|
||||||
|
mksysnum="./mksysnum_plan9.sh /n/sources/plan9/sys/src/libc/9syscall/sys.h"
|
||||||
|
mktypes="XXX"
|
||||||
|
;;
|
||||||
|
plan9_arm)
|
||||||
|
mkerrors=
|
||||||
|
mksyscall="go run mksyscall.go -l32 -plan9 -tags plan9,arm"
|
||||||
|
mksysnum="./mksysnum_plan9.sh /n/sources/plan9/sys/src/libc/9syscall/sys.h"
|
||||||
|
mktypes="XXX"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo 'unrecognized $GOOS_$GOARCH: ' "$GOOSARCH" 1>&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
(
|
||||||
|
if [ -n "$mkerrors" ]; then echo "$mkerrors |gofmt >$zerrors"; fi
|
||||||
|
case "$GOOS" in
|
||||||
|
plan9)
|
||||||
|
syscall_goos="syscall_$GOOS.go"
|
||||||
|
if [ -n "$mksyscall" ]; then echo "$mksyscall $syscall_goos |gofmt >zsyscall_$GOOSARCH.go"; fi
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
if [ -n "$mksysctl" ]; then echo "$mksysctl |gofmt >$zsysctl"; fi
|
||||||
|
if [ -n "$mksysnum" ]; then echo "$mksysnum |gofmt >zsysnum_$GOOSARCH.go"; fi
|
||||||
|
if [ -n "$mktypes" ]; then echo "$mktypes types_$GOOS.go |gofmt >ztypes_$GOOSARCH.go"; fi
|
||||||
|
) | $run
|
||||||
246
vendor/golang.org/x/sys/plan9/mkerrors.sh
generated
vendored
Normal file
246
vendor/golang.org/x/sys/plan9/mkerrors.sh
generated
vendored
Normal file
@@ -0,0 +1,246 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Copyright 2009 The Go Authors. All rights reserved.
|
||||||
|
# Use of this source code is governed by a BSD-style
|
||||||
|
# license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
# Generate Go code listing errors and other #defined constant
|
||||||
|
# values (ENAMETOOLONG etc.), by asking the preprocessor
|
||||||
|
# about the definitions.
|
||||||
|
|
||||||
|
unset LANG
|
||||||
|
export LC_ALL=C
|
||||||
|
export LC_CTYPE=C
|
||||||
|
|
||||||
|
CC=${CC:-gcc}
|
||||||
|
|
||||||
|
uname=$(uname)
|
||||||
|
|
||||||
|
includes='
|
||||||
|
#include <sys/types.h>
|
||||||
|
#include <sys/file.h>
|
||||||
|
#include <fcntl.h>
|
||||||
|
#include <dirent.h>
|
||||||
|
#include <sys/socket.h>
|
||||||
|
#include <netinet/in.h>
|
||||||
|
#include <netinet/ip.h>
|
||||||
|
#include <netinet/ip6.h>
|
||||||
|
#include <netinet/tcp.h>
|
||||||
|
#include <errno.h>
|
||||||
|
#include <sys/signal.h>
|
||||||
|
#include <signal.h>
|
||||||
|
#include <sys/resource.h>
|
||||||
|
'
|
||||||
|
|
||||||
|
ccflags="$@"
|
||||||
|
|
||||||
|
# Write go tool cgo -godefs input.
|
||||||
|
(
|
||||||
|
echo package plan9
|
||||||
|
echo
|
||||||
|
echo '/*'
|
||||||
|
indirect="includes_$(uname)"
|
||||||
|
echo "${!indirect} $includes"
|
||||||
|
echo '*/'
|
||||||
|
echo 'import "C"'
|
||||||
|
echo
|
||||||
|
echo 'const ('
|
||||||
|
|
||||||
|
# The gcc command line prints all the #defines
|
||||||
|
# it encounters while processing the input
|
||||||
|
echo "${!indirect} $includes" | $CC -x c - -E -dM $ccflags |
|
||||||
|
awk '
|
||||||
|
$1 != "#define" || $2 ~ /\(/ || $3 == "" {next}
|
||||||
|
|
||||||
|
$2 ~ /^E([ABCD]X|[BIS]P|[SD]I|S|FL)$/ {next} # 386 registers
|
||||||
|
$2 ~ /^(SIGEV_|SIGSTKSZ|SIGRT(MIN|MAX))/ {next}
|
||||||
|
$2 ~ /^(SCM_SRCRT)$/ {next}
|
||||||
|
$2 ~ /^(MAP_FAILED)$/ {next}
|
||||||
|
|
||||||
|
$2 !~ /^ETH_/ &&
|
||||||
|
$2 !~ /^EPROC_/ &&
|
||||||
|
$2 !~ /^EQUIV_/ &&
|
||||||
|
$2 !~ /^EXPR_/ &&
|
||||||
|
$2 ~ /^E[A-Z0-9_]+$/ ||
|
||||||
|
$2 ~ /^B[0-9_]+$/ ||
|
||||||
|
$2 ~ /^V[A-Z0-9]+$/ ||
|
||||||
|
$2 ~ /^CS[A-Z0-9]/ ||
|
||||||
|
$2 ~ /^I(SIG|CANON|CRNL|EXTEN|MAXBEL|STRIP|UTF8)$/ ||
|
||||||
|
$2 ~ /^IGN/ ||
|
||||||
|
$2 ~ /^IX(ON|ANY|OFF)$/ ||
|
||||||
|
$2 ~ /^IN(LCR|PCK)$/ ||
|
||||||
|
$2 ~ /(^FLU?SH)|(FLU?SH$)/ ||
|
||||||
|
$2 ~ /^C(LOCAL|READ)$/ ||
|
||||||
|
$2 == "BRKINT" ||
|
||||||
|
$2 == "HUPCL" ||
|
||||||
|
$2 == "PENDIN" ||
|
||||||
|
$2 == "TOSTOP" ||
|
||||||
|
$2 ~ /^PAR/ ||
|
||||||
|
$2 ~ /^SIG[^_]/ ||
|
||||||
|
$2 ~ /^O[CNPFP][A-Z]+[^_][A-Z]+$/ ||
|
||||||
|
$2 ~ /^IN_/ ||
|
||||||
|
$2 ~ /^LOCK_(SH|EX|NB|UN)$/ ||
|
||||||
|
$2 ~ /^(AF|SOCK|SO|SOL|IPPROTO|IP|IPV6|ICMP6|TCP|EVFILT|NOTE|EV|SHUT|PROT|MAP|PACKET|MSG|SCM|MCL|DT|MADV|PR)_/ ||
|
||||||
|
$2 == "ICMPV6_FILTER" ||
|
||||||
|
$2 == "SOMAXCONN" ||
|
||||||
|
$2 == "NAME_MAX" ||
|
||||||
|
$2 == "IFNAMSIZ" ||
|
||||||
|
$2 ~ /^CTL_(MAXNAME|NET|QUERY)$/ ||
|
||||||
|
$2 ~ /^SYSCTL_VERS/ ||
|
||||||
|
$2 ~ /^(MS|MNT)_/ ||
|
||||||
|
$2 ~ /^TUN(SET|GET|ATTACH|DETACH)/ ||
|
||||||
|
$2 ~ /^(O|F|FD|NAME|S|PTRACE|PT)_/ ||
|
||||||
|
$2 ~ /^LINUX_REBOOT_CMD_/ ||
|
||||||
|
$2 ~ /^LINUX_REBOOT_MAGIC[12]$/ ||
|
||||||
|
$2 !~ "NLA_TYPE_MASK" &&
|
||||||
|
$2 ~ /^(NETLINK|NLM|NLMSG|NLA|IFA|IFAN|RT|RTCF|RTN|RTPROT|RTNH|ARPHRD|ETH_P)_/ ||
|
||||||
|
$2 ~ /^SIOC/ ||
|
||||||
|
$2 ~ /^TIOC/ ||
|
||||||
|
$2 !~ "RTF_BITS" &&
|
||||||
|
$2 ~ /^(IFF|IFT|NET_RT|RTM|RTF|RTV|RTA|RTAX)_/ ||
|
||||||
|
$2 ~ /^BIOC/ ||
|
||||||
|
$2 ~ /^RUSAGE_(SELF|CHILDREN|THREAD)/ ||
|
||||||
|
$2 ~ /^RLIMIT_(AS|CORE|CPU|DATA|FSIZE|NOFILE|STACK)|RLIM_INFINITY/ ||
|
||||||
|
$2 ~ /^PRIO_(PROCESS|PGRP|USER)/ ||
|
||||||
|
$2 ~ /^CLONE_[A-Z_]+/ ||
|
||||||
|
$2 !~ /^(BPF_TIMEVAL)$/ &&
|
||||||
|
$2 ~ /^(BPF|DLT)_/ ||
|
||||||
|
$2 !~ "WMESGLEN" &&
|
||||||
|
$2 ~ /^W[A-Z0-9]+$/ {printf("\t%s = C.%s\n", $2, $2)}
|
||||||
|
$2 ~ /^__WCOREFLAG$/ {next}
|
||||||
|
$2 ~ /^__W[A-Z0-9]+$/ {printf("\t%s = C.%s\n", substr($2,3), $2)}
|
||||||
|
|
||||||
|
{next}
|
||||||
|
' | sort
|
||||||
|
|
||||||
|
echo ')'
|
||||||
|
) >_const.go
|
||||||
|
|
||||||
|
# Pull out the error names for later.
|
||||||
|
errors=$(
|
||||||
|
echo '#include <errno.h>' | $CC -x c - -E -dM $ccflags |
|
||||||
|
awk '$1=="#define" && $2 ~ /^E[A-Z0-9_]+$/ { print $2 }' |
|
||||||
|
sort
|
||||||
|
)
|
||||||
|
|
||||||
|
# Pull out the signal names for later.
|
||||||
|
signals=$(
|
||||||
|
echo '#include <signal.h>' | $CC -x c - -E -dM $ccflags |
|
||||||
|
awk '$1=="#define" && $2 ~ /^SIG[A-Z0-9]+$/ { print $2 }' |
|
||||||
|
grep -v 'SIGSTKSIZE\|SIGSTKSZ\|SIGRT' |
|
||||||
|
sort
|
||||||
|
)
|
||||||
|
|
||||||
|
# Again, writing regexps to a file.
|
||||||
|
echo '#include <errno.h>' | $CC -x c - -E -dM $ccflags |
|
||||||
|
awk '$1=="#define" && $2 ~ /^E[A-Z0-9_]+$/ { print "^\t" $2 "[ \t]*=" }' |
|
||||||
|
sort >_error.grep
|
||||||
|
echo '#include <signal.h>' | $CC -x c - -E -dM $ccflags |
|
||||||
|
awk '$1=="#define" && $2 ~ /^SIG[A-Z0-9]+$/ { print "^\t" $2 "[ \t]*=" }' |
|
||||||
|
grep -v 'SIGSTKSIZE\|SIGSTKSZ\|SIGRT' |
|
||||||
|
sort >_signal.grep
|
||||||
|
|
||||||
|
echo '// mkerrors.sh' "$@"
|
||||||
|
echo '// Code generated by the command above; DO NOT EDIT.'
|
||||||
|
echo
|
||||||
|
go tool cgo -godefs -- "$@" _const.go >_error.out
|
||||||
|
cat _error.out | grep -vf _error.grep | grep -vf _signal.grep
|
||||||
|
echo
|
||||||
|
echo '// Errors'
|
||||||
|
echo 'const ('
|
||||||
|
cat _error.out | grep -f _error.grep | sed 's/=\(.*\)/= Errno(\1)/'
|
||||||
|
echo ')'
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo '// Signals'
|
||||||
|
echo 'const ('
|
||||||
|
cat _error.out | grep -f _signal.grep | sed 's/=\(.*\)/= Signal(\1)/'
|
||||||
|
echo ')'
|
||||||
|
|
||||||
|
# Run C program to print error and syscall strings.
|
||||||
|
(
|
||||||
|
echo -E "
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <errno.h>
|
||||||
|
#include <ctype.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <signal.h>
|
||||||
|
|
||||||
|
#define nelem(x) (sizeof(x)/sizeof((x)[0]))
|
||||||
|
|
||||||
|
enum { A = 'A', Z = 'Z', a = 'a', z = 'z' }; // avoid need for single quotes below
|
||||||
|
|
||||||
|
int errors[] = {
|
||||||
|
"
|
||||||
|
for i in $errors
|
||||||
|
do
|
||||||
|
echo -E ' '$i,
|
||||||
|
done
|
||||||
|
|
||||||
|
echo -E "
|
||||||
|
};
|
||||||
|
|
||||||
|
int signals[] = {
|
||||||
|
"
|
||||||
|
for i in $signals
|
||||||
|
do
|
||||||
|
echo -E ' '$i,
|
||||||
|
done
|
||||||
|
|
||||||
|
# Use -E because on some systems bash builtin interprets \n itself.
|
||||||
|
echo -E '
|
||||||
|
};
|
||||||
|
|
||||||
|
static int
|
||||||
|
intcmp(const void *a, const void *b)
|
||||||
|
{
|
||||||
|
return *(int*)a - *(int*)b;
|
||||||
|
}
|
||||||
|
|
||||||
|
int
|
||||||
|
main(void)
|
||||||
|
{
|
||||||
|
int i, j, e;
|
||||||
|
char buf[1024], *p;
|
||||||
|
|
||||||
|
printf("\n\n// Error table\n");
|
||||||
|
printf("var errors = [...]string {\n");
|
||||||
|
qsort(errors, nelem(errors), sizeof errors[0], intcmp);
|
||||||
|
for(i=0; i<nelem(errors); i++) {
|
||||||
|
e = errors[i];
|
||||||
|
if(i > 0 && errors[i-1] == e)
|
||||||
|
continue;
|
||||||
|
strcpy(buf, strerror(e));
|
||||||
|
// lowercase first letter: Bad -> bad, but STREAM -> STREAM.
|
||||||
|
if(A <= buf[0] && buf[0] <= Z && a <= buf[1] && buf[1] <= z)
|
||||||
|
buf[0] += a - A;
|
||||||
|
printf("\t%d: \"%s\",\n", e, buf);
|
||||||
|
}
|
||||||
|
printf("}\n\n");
|
||||||
|
|
||||||
|
printf("\n\n// Signal table\n");
|
||||||
|
printf("var signals = [...]string {\n");
|
||||||
|
qsort(signals, nelem(signals), sizeof signals[0], intcmp);
|
||||||
|
for(i=0; i<nelem(signals); i++) {
|
||||||
|
e = signals[i];
|
||||||
|
if(i > 0 && signals[i-1] == e)
|
||||||
|
continue;
|
||||||
|
strcpy(buf, strsignal(e));
|
||||||
|
// lowercase first letter: Bad -> bad, but STREAM -> STREAM.
|
||||||
|
if(A <= buf[0] && buf[0] <= Z && a <= buf[1] && buf[1] <= z)
|
||||||
|
buf[0] += a - A;
|
||||||
|
// cut trailing : number.
|
||||||
|
p = strrchr(buf, ":"[0]);
|
||||||
|
if(p)
|
||||||
|
*p = '\0';
|
||||||
|
printf("\t%d: \"%s\",\n", e, buf);
|
||||||
|
}
|
||||||
|
printf("}\n\n");
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
'
|
||||||
|
) >_errors.c
|
||||||
|
|
||||||
|
$CC $ccflags -o _errors _errors.c && $GORUN ./_errors && rm -f _errors.c _errors _const.go _error.grep _signal.grep _error.out
|
||||||
23
vendor/golang.org/x/sys/plan9/mksysnum_plan9.sh
generated
vendored
Normal file
23
vendor/golang.org/x/sys/plan9/mksysnum_plan9.sh
generated
vendored
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# Copyright 2009 The Go Authors. All rights reserved.
|
||||||
|
# Use of this source code is governed by a BSD-style
|
||||||
|
# license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
COMMAND="mksysnum_plan9.sh $@"
|
||||||
|
|
||||||
|
cat <<EOF
|
||||||
|
// $COMMAND
|
||||||
|
// MACHINE GENERATED BY THE ABOVE COMMAND; DO NOT EDIT
|
||||||
|
|
||||||
|
package plan9
|
||||||
|
|
||||||
|
const(
|
||||||
|
EOF
|
||||||
|
|
||||||
|
SP='[ ]' # space or tab
|
||||||
|
sed "s/^#define${SP}\\([A-Z0-9_][A-Z0-9_]*\\)${SP}${SP}*\\([0-9][0-9]*\\)/SYS_\\1=\\2/g" \
|
||||||
|
< $1 | grep -v SYS__
|
||||||
|
|
||||||
|
cat <<EOF
|
||||||
|
)
|
||||||
|
EOF
|
||||||
19
vendor/golang.org/x/sys/plan9/pwd_plan9.go
generated
vendored
Normal file
19
vendor/golang.org/x/sys/plan9/pwd_plan9.go
generated
vendored
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
// Copyright 2015 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package plan9
|
||||||
|
|
||||||
|
import "syscall"
|
||||||
|
|
||||||
|
func fixwd() {
|
||||||
|
syscall.Fixwd()
|
||||||
|
}
|
||||||
|
|
||||||
|
func Getwd() (wd string, err error) {
|
||||||
|
return syscall.Getwd()
|
||||||
|
}
|
||||||
|
|
||||||
|
func Chdir(path string) error {
|
||||||
|
return syscall.Chdir(path)
|
||||||
|
}
|
||||||
30
vendor/golang.org/x/sys/plan9/race.go
generated
vendored
Normal file
30
vendor/golang.org/x/sys/plan9/race.go
generated
vendored
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
// Copyright 2012 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
//go:build plan9 && race
|
||||||
|
|
||||||
|
package plan9
|
||||||
|
|
||||||
|
import (
|
||||||
|
"runtime"
|
||||||
|
"unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
const raceenabled = true
|
||||||
|
|
||||||
|
func raceAcquire(addr unsafe.Pointer) {
|
||||||
|
runtime.RaceAcquire(addr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func raceReleaseMerge(addr unsafe.Pointer) {
|
||||||
|
runtime.RaceReleaseMerge(addr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func raceReadRange(addr unsafe.Pointer, len int) {
|
||||||
|
runtime.RaceReadRange(addr, len)
|
||||||
|
}
|
||||||
|
|
||||||
|
func raceWriteRange(addr unsafe.Pointer, len int) {
|
||||||
|
runtime.RaceWriteRange(addr, len)
|
||||||
|
}
|
||||||
25
vendor/golang.org/x/sys/plan9/race0.go
generated
vendored
Normal file
25
vendor/golang.org/x/sys/plan9/race0.go
generated
vendored
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
// Copyright 2012 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
//go:build plan9 && !race
|
||||||
|
|
||||||
|
package plan9
|
||||||
|
|
||||||
|
import (
|
||||||
|
"unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
const raceenabled = false
|
||||||
|
|
||||||
|
func raceAcquire(addr unsafe.Pointer) {
|
||||||
|
}
|
||||||
|
|
||||||
|
func raceReleaseMerge(addr unsafe.Pointer) {
|
||||||
|
}
|
||||||
|
|
||||||
|
func raceReadRange(addr unsafe.Pointer, len int) {
|
||||||
|
}
|
||||||
|
|
||||||
|
func raceWriteRange(addr unsafe.Pointer, len int) {
|
||||||
|
}
|
||||||
22
vendor/golang.org/x/sys/plan9/str.go
generated
vendored
Normal file
22
vendor/golang.org/x/sys/plan9/str.go
generated
vendored
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
// Copyright 2009 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
//go:build plan9
|
||||||
|
|
||||||
|
package plan9
|
||||||
|
|
||||||
|
func itoa(val int) string { // do it here rather than with fmt to avoid dependency
|
||||||
|
if val < 0 {
|
||||||
|
return "-" + itoa(-val)
|
||||||
|
}
|
||||||
|
var buf [32]byte // big enough for int64
|
||||||
|
i := len(buf) - 1
|
||||||
|
for val >= 10 {
|
||||||
|
buf[i] = byte(val%10 + '0')
|
||||||
|
i--
|
||||||
|
val /= 10
|
||||||
|
}
|
||||||
|
buf[i] = byte(val + '0')
|
||||||
|
return string(buf[i:])
|
||||||
|
}
|
||||||
109
vendor/golang.org/x/sys/plan9/syscall.go
generated
vendored
Normal file
109
vendor/golang.org/x/sys/plan9/syscall.go
generated
vendored
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
// Copyright 2009 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
//go:build plan9
|
||||||
|
|
||||||
|
// Package plan9 contains an interface to the low-level operating system
|
||||||
|
// primitives. OS details vary depending on the underlying system, and
|
||||||
|
// by default, godoc will display the OS-specific documentation for the current
|
||||||
|
// system. If you want godoc to display documentation for another
|
||||||
|
// system, set $GOOS and $GOARCH to the desired system. For example, if
|
||||||
|
// you want to view documentation for freebsd/arm on linux/amd64, set $GOOS
|
||||||
|
// to freebsd and $GOARCH to arm.
|
||||||
|
//
|
||||||
|
// The primary use of this package is inside other packages that provide a more
|
||||||
|
// portable interface to the system, such as "os", "time" and "net". Use
|
||||||
|
// those packages rather than this one if you can.
|
||||||
|
//
|
||||||
|
// For details of the functions and data types in this package consult
|
||||||
|
// the manuals for the appropriate operating system.
|
||||||
|
//
|
||||||
|
// These calls return err == nil to indicate success; otherwise
|
||||||
|
// err represents an operating system error describing the failure and
|
||||||
|
// holds a value of type syscall.ErrorString.
|
||||||
|
package plan9 // import "golang.org/x/sys/plan9"
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"strings"
|
||||||
|
"unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ByteSliceFromString returns a NUL-terminated slice of bytes
|
||||||
|
// containing the text of s. If s contains a NUL byte at any
|
||||||
|
// location, it returns (nil, EINVAL).
|
||||||
|
func ByteSliceFromString(s string) ([]byte, error) {
|
||||||
|
if strings.IndexByte(s, 0) != -1 {
|
||||||
|
return nil, EINVAL
|
||||||
|
}
|
||||||
|
a := make([]byte, len(s)+1)
|
||||||
|
copy(a, s)
|
||||||
|
return a, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// BytePtrFromString returns a pointer to a NUL-terminated array of
|
||||||
|
// bytes containing the text of s. If s contains a NUL byte at any
|
||||||
|
// location, it returns (nil, EINVAL).
|
||||||
|
func BytePtrFromString(s string) (*byte, error) {
|
||||||
|
a, err := ByteSliceFromString(s)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &a[0], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ByteSliceToString returns a string form of the text represented by the slice s, with a terminating NUL and any
|
||||||
|
// bytes after the NUL removed.
|
||||||
|
func ByteSliceToString(s []byte) string {
|
||||||
|
if i := bytes.IndexByte(s, 0); i != -1 {
|
||||||
|
s = s[:i]
|
||||||
|
}
|
||||||
|
return string(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
// BytePtrToString takes a pointer to a sequence of text and returns the corresponding string.
|
||||||
|
// If the pointer is nil, it returns the empty string. It assumes that the text sequence is terminated
|
||||||
|
// at a zero byte; if the zero byte is not present, the program may crash.
|
||||||
|
func BytePtrToString(p *byte) string {
|
||||||
|
if p == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
if *p == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find NUL terminator.
|
||||||
|
n := 0
|
||||||
|
for ptr := unsafe.Pointer(p); *(*byte)(ptr) != 0; n++ {
|
||||||
|
ptr = unsafe.Pointer(uintptr(ptr) + 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(unsafe.Slice(p, n))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Single-word zero for use when we need a valid pointer to 0 bytes.
|
||||||
|
// See mksyscall.pl.
|
||||||
|
var _zero uintptr
|
||||||
|
|
||||||
|
func (ts *Timespec) Unix() (sec int64, nsec int64) {
|
||||||
|
return int64(ts.Sec), int64(ts.Nsec)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tv *Timeval) Unix() (sec int64, nsec int64) {
|
||||||
|
return int64(tv.Sec), int64(tv.Usec) * 1000
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ts *Timespec) Nano() int64 {
|
||||||
|
return int64(ts.Sec)*1e9 + int64(ts.Nsec)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tv *Timeval) Nano() int64 {
|
||||||
|
return int64(tv.Sec)*1e9 + int64(tv.Usec)*1000
|
||||||
|
}
|
||||||
|
|
||||||
|
// use is a no-op, but the compiler cannot see that it is.
|
||||||
|
// Calling use(p) ensures that p is kept live until that point.
|
||||||
|
//
|
||||||
|
//go:noescape
|
||||||
|
func use(p unsafe.Pointer)
|
||||||
355
vendor/golang.org/x/sys/plan9/syscall_plan9.go
generated
vendored
Normal file
355
vendor/golang.org/x/sys/plan9/syscall_plan9.go
generated
vendored
Normal file
@@ -0,0 +1,355 @@
|
|||||||
|
// Copyright 2011 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
// Plan 9 system calls.
|
||||||
|
// This file is compiled as ordinary Go code,
|
||||||
|
// but it is also input to mksyscall,
|
||||||
|
// which parses the //sys lines and generates system call stubs.
|
||||||
|
// Note that sometimes we use a lowercase //sys name and
|
||||||
|
// wrap it in our own nicer implementation.
|
||||||
|
|
||||||
|
package plan9
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"syscall"
|
||||||
|
"unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
// A Note is a string describing a process note.
|
||||||
|
// It implements the os.Signal interface.
|
||||||
|
type Note = syscall.Note
|
||||||
|
|
||||||
|
var (
|
||||||
|
Stdin = 0
|
||||||
|
Stdout = 1
|
||||||
|
Stderr = 2
|
||||||
|
)
|
||||||
|
|
||||||
|
// For testing: clients can set this flag to force
|
||||||
|
// creation of IPv6 sockets to return EAFNOSUPPORT.
|
||||||
|
var SocketDisableIPv6 bool
|
||||||
|
|
||||||
|
func Syscall(trap, a1, a2, a3 uintptr) (r1, r2 uintptr, err syscall.ErrorString)
|
||||||
|
func Syscall6(trap, a1, a2, a3, a4, a5, a6 uintptr) (r1, r2 uintptr, err syscall.ErrorString)
|
||||||
|
func RawSyscall(trap, a1, a2, a3 uintptr) (r1, r2, err uintptr)
|
||||||
|
func RawSyscall6(trap, a1, a2, a3, a4, a5, a6 uintptr) (r1, r2, err uintptr)
|
||||||
|
|
||||||
|
func atoi(b []byte) (n uint) {
|
||||||
|
n = 0
|
||||||
|
for i := 0; i < len(b); i++ {
|
||||||
|
n = n*10 + uint(b[i]-'0')
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func cstring(s []byte) string {
|
||||||
|
i := bytes.IndexByte(s, 0)
|
||||||
|
if i == -1 {
|
||||||
|
i = len(s)
|
||||||
|
}
|
||||||
|
return string(s[:i])
|
||||||
|
}
|
||||||
|
|
||||||
|
func errstr() string {
|
||||||
|
var buf [ERRMAX]byte
|
||||||
|
|
||||||
|
RawSyscall(SYS_ERRSTR, uintptr(unsafe.Pointer(&buf[0])), uintptr(len(buf)), 0)
|
||||||
|
|
||||||
|
buf[len(buf)-1] = 0
|
||||||
|
return cstring(buf[:])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Implemented in assembly to import from runtime.
|
||||||
|
func exit(code int)
|
||||||
|
|
||||||
|
func Exit(code int) { exit(code) }
|
||||||
|
|
||||||
|
func readnum(path string) (uint, error) {
|
||||||
|
var b [12]byte
|
||||||
|
|
||||||
|
fd, e := Open(path, O_RDONLY)
|
||||||
|
if e != nil {
|
||||||
|
return 0, e
|
||||||
|
}
|
||||||
|
defer Close(fd)
|
||||||
|
|
||||||
|
n, e := Pread(fd, b[:], 0)
|
||||||
|
|
||||||
|
if e != nil {
|
||||||
|
return 0, e
|
||||||
|
}
|
||||||
|
|
||||||
|
m := 0
|
||||||
|
for ; m < n && b[m] == ' '; m++ {
|
||||||
|
}
|
||||||
|
|
||||||
|
return atoi(b[m : n-1]), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func Getpid() (pid int) {
|
||||||
|
n, _ := readnum("#c/pid")
|
||||||
|
return int(n)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Getppid() (ppid int) {
|
||||||
|
n, _ := readnum("#c/ppid")
|
||||||
|
return int(n)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Read(fd int, p []byte) (n int, err error) {
|
||||||
|
return Pread(fd, p, -1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Write(fd int, p []byte) (n int, err error) {
|
||||||
|
return Pwrite(fd, p, -1)
|
||||||
|
}
|
||||||
|
|
||||||
|
var ioSync int64
|
||||||
|
|
||||||
|
//sys fd2path(fd int, buf []byte) (err error)
|
||||||
|
|
||||||
|
func Fd2path(fd int) (path string, err error) {
|
||||||
|
var buf [512]byte
|
||||||
|
|
||||||
|
e := fd2path(fd, buf[:])
|
||||||
|
if e != nil {
|
||||||
|
return "", e
|
||||||
|
}
|
||||||
|
return cstring(buf[:]), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
//sys pipe(p *[2]int32) (err error)
|
||||||
|
|
||||||
|
func Pipe(p []int) (err error) {
|
||||||
|
if len(p) != 2 {
|
||||||
|
return syscall.ErrorString("bad arg in system call")
|
||||||
|
}
|
||||||
|
var pp [2]int32
|
||||||
|
err = pipe(&pp)
|
||||||
|
if err == nil {
|
||||||
|
p[0] = int(pp[0])
|
||||||
|
p[1] = int(pp[1])
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Underlying system call writes to newoffset via pointer.
|
||||||
|
// Implemented in assembly to avoid allocation.
|
||||||
|
func seek(placeholder uintptr, fd int, offset int64, whence int) (newoffset int64, err string)
|
||||||
|
|
||||||
|
func Seek(fd int, offset int64, whence int) (newoffset int64, err error) {
|
||||||
|
newoffset, e := seek(0, fd, offset, whence)
|
||||||
|
|
||||||
|
if newoffset == -1 {
|
||||||
|
err = syscall.ErrorString(e)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func Mkdir(path string, mode uint32) (err error) {
|
||||||
|
fd, err := Create(path, O_RDONLY, DMDIR|mode)
|
||||||
|
|
||||||
|
if fd != -1 {
|
||||||
|
Close(fd)
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
type Waitmsg struct {
|
||||||
|
Pid int
|
||||||
|
Time [3]uint32
|
||||||
|
Msg string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w Waitmsg) Exited() bool { return true }
|
||||||
|
func (w Waitmsg) Signaled() bool { return false }
|
||||||
|
|
||||||
|
func (w Waitmsg) ExitStatus() int {
|
||||||
|
if len(w.Msg) == 0 {
|
||||||
|
// a normal exit returns no message
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
//sys await(s []byte) (n int, err error)
|
||||||
|
|
||||||
|
func Await(w *Waitmsg) (err error) {
|
||||||
|
var buf [512]byte
|
||||||
|
var f [5][]byte
|
||||||
|
|
||||||
|
n, err := await(buf[:])
|
||||||
|
|
||||||
|
if err != nil || w == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
nf := 0
|
||||||
|
p := 0
|
||||||
|
for i := 0; i < n && nf < len(f)-1; i++ {
|
||||||
|
if buf[i] == ' ' {
|
||||||
|
f[nf] = buf[p:i]
|
||||||
|
p = i + 1
|
||||||
|
nf++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
f[nf] = buf[p:]
|
||||||
|
nf++
|
||||||
|
|
||||||
|
if nf != len(f) {
|
||||||
|
return syscall.ErrorString("invalid wait message")
|
||||||
|
}
|
||||||
|
w.Pid = int(atoi(f[0]))
|
||||||
|
w.Time[0] = uint32(atoi(f[1]))
|
||||||
|
w.Time[1] = uint32(atoi(f[2]))
|
||||||
|
w.Time[2] = uint32(atoi(f[3]))
|
||||||
|
w.Msg = cstring(f[4])
|
||||||
|
if w.Msg == "''" {
|
||||||
|
// await() returns '' for no error
|
||||||
|
w.Msg = ""
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func Unmount(name, old string) (err error) {
|
||||||
|
fixwd()
|
||||||
|
oldp, err := BytePtrFromString(old)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
oldptr := uintptr(unsafe.Pointer(oldp))
|
||||||
|
|
||||||
|
var r0 uintptr
|
||||||
|
var e syscall.ErrorString
|
||||||
|
|
||||||
|
// bind(2) man page: If name is zero, everything bound or mounted upon old is unbound or unmounted.
|
||||||
|
if name == "" {
|
||||||
|
r0, _, e = Syscall(SYS_UNMOUNT, _zero, oldptr, 0)
|
||||||
|
} else {
|
||||||
|
namep, err := BytePtrFromString(name)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
r0, _, e = Syscall(SYS_UNMOUNT, uintptr(unsafe.Pointer(namep)), oldptr, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
if int32(r0) == -1 {
|
||||||
|
err = e
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func Fchdir(fd int) (err error) {
|
||||||
|
path, err := Fd2path(fd)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return Chdir(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
type Timespec struct {
|
||||||
|
Sec int32
|
||||||
|
Nsec int32
|
||||||
|
}
|
||||||
|
|
||||||
|
type Timeval struct {
|
||||||
|
Sec int32
|
||||||
|
Usec int32
|
||||||
|
}
|
||||||
|
|
||||||
|
func NsecToTimeval(nsec int64) (tv Timeval) {
|
||||||
|
nsec += 999 // round up to microsecond
|
||||||
|
tv.Usec = int32(nsec % 1e9 / 1e3)
|
||||||
|
tv.Sec = int32(nsec / 1e9)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func nsec() int64 {
|
||||||
|
var scratch int64
|
||||||
|
|
||||||
|
r0, _, _ := Syscall(SYS_NSEC, uintptr(unsafe.Pointer(&scratch)), 0, 0)
|
||||||
|
// TODO(aram): remove hack after I fix _nsec in the pc64 kernel.
|
||||||
|
if r0 == 0 {
|
||||||
|
return scratch
|
||||||
|
}
|
||||||
|
return int64(r0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Gettimeofday(tv *Timeval) error {
|
||||||
|
nsec := nsec()
|
||||||
|
*tv = NsecToTimeval(nsec)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func Getpagesize() int { return 0x1000 }
|
||||||
|
|
||||||
|
func Getegid() (egid int) { return -1 }
|
||||||
|
func Geteuid() (euid int) { return -1 }
|
||||||
|
func Getgid() (gid int) { return -1 }
|
||||||
|
func Getuid() (uid int) { return -1 }
|
||||||
|
|
||||||
|
func Getgroups() (gids []int, err error) {
|
||||||
|
return make([]int, 0), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
//sys open(path string, mode int) (fd int, err error)
|
||||||
|
|
||||||
|
func Open(path string, mode int) (fd int, err error) {
|
||||||
|
fixwd()
|
||||||
|
return open(path, mode)
|
||||||
|
}
|
||||||
|
|
||||||
|
//sys create(path string, mode int, perm uint32) (fd int, err error)
|
||||||
|
|
||||||
|
func Create(path string, mode int, perm uint32) (fd int, err error) {
|
||||||
|
fixwd()
|
||||||
|
return create(path, mode, perm)
|
||||||
|
}
|
||||||
|
|
||||||
|
//sys remove(path string) (err error)
|
||||||
|
|
||||||
|
func Remove(path string) error {
|
||||||
|
fixwd()
|
||||||
|
return remove(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
//sys stat(path string, edir []byte) (n int, err error)
|
||||||
|
|
||||||
|
func Stat(path string, edir []byte) (n int, err error) {
|
||||||
|
fixwd()
|
||||||
|
return stat(path, edir)
|
||||||
|
}
|
||||||
|
|
||||||
|
//sys bind(name string, old string, flag int) (err error)
|
||||||
|
|
||||||
|
func Bind(name string, old string, flag int) (err error) {
|
||||||
|
fixwd()
|
||||||
|
return bind(name, old, flag)
|
||||||
|
}
|
||||||
|
|
||||||
|
//sys mount(fd int, afd int, old string, flag int, aname string) (err error)
|
||||||
|
|
||||||
|
func Mount(fd int, afd int, old string, flag int, aname string) (err error) {
|
||||||
|
fixwd()
|
||||||
|
return mount(fd, afd, old, flag, aname)
|
||||||
|
}
|
||||||
|
|
||||||
|
//sys wstat(path string, edir []byte) (err error)
|
||||||
|
|
||||||
|
func Wstat(path string, edir []byte) (err error) {
|
||||||
|
fixwd()
|
||||||
|
return wstat(path, edir)
|
||||||
|
}
|
||||||
|
|
||||||
|
//sys chdir(path string) (err error)
|
||||||
|
//sys Dup(oldfd int, newfd int) (fd int, err error)
|
||||||
|
//sys Pread(fd int, p []byte, offset int64) (n int, err error)
|
||||||
|
//sys Pwrite(fd int, p []byte, offset int64) (n int, err error)
|
||||||
|
//sys Close(fd int) (err error)
|
||||||
|
//sys Fstat(fd int, edir []byte) (n int, err error)
|
||||||
|
//sys Fwstat(fd int, edir []byte) (err error)
|
||||||
284
vendor/golang.org/x/sys/plan9/zsyscall_plan9_386.go
generated
vendored
Normal file
284
vendor/golang.org/x/sys/plan9/zsyscall_plan9_386.go
generated
vendored
Normal file
@@ -0,0 +1,284 @@
|
|||||||
|
// go run mksyscall.go -l32 -plan9 -tags plan9,386 syscall_plan9.go
|
||||||
|
// Code generated by the command above; see README.md. DO NOT EDIT.
|
||||||
|
|
||||||
|
//go:build plan9 && 386
|
||||||
|
|
||||||
|
package plan9
|
||||||
|
|
||||||
|
import "unsafe"
|
||||||
|
|
||||||
|
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
|
||||||
|
|
||||||
|
func fd2path(fd int, buf []byte) (err error) {
|
||||||
|
var _p0 unsafe.Pointer
|
||||||
|
if len(buf) > 0 {
|
||||||
|
_p0 = unsafe.Pointer(&buf[0])
|
||||||
|
} else {
|
||||||
|
_p0 = unsafe.Pointer(&_zero)
|
||||||
|
}
|
||||||
|
r0, _, e1 := Syscall(SYS_FD2PATH, uintptr(fd), uintptr(_p0), uintptr(len(buf)))
|
||||||
|
if int32(r0) == -1 {
|
||||||
|
err = e1
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
|
||||||
|
|
||||||
|
func pipe(p *[2]int32) (err error) {
|
||||||
|
r0, _, e1 := Syscall(SYS_PIPE, uintptr(unsafe.Pointer(p)), 0, 0)
|
||||||
|
if int32(r0) == -1 {
|
||||||
|
err = e1
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
|
||||||
|
|
||||||
|
func await(s []byte) (n int, err error) {
|
||||||
|
var _p0 unsafe.Pointer
|
||||||
|
if len(s) > 0 {
|
||||||
|
_p0 = unsafe.Pointer(&s[0])
|
||||||
|
} else {
|
||||||
|
_p0 = unsafe.Pointer(&_zero)
|
||||||
|
}
|
||||||
|
r0, _, e1 := Syscall(SYS_AWAIT, uintptr(_p0), uintptr(len(s)), 0)
|
||||||
|
n = int(r0)
|
||||||
|
if int32(r0) == -1 {
|
||||||
|
err = e1
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
|
||||||
|
|
||||||
|
func open(path string, mode int) (fd int, err error) {
|
||||||
|
var _p0 *byte
|
||||||
|
_p0, err = BytePtrFromString(path)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
r0, _, e1 := Syscall(SYS_OPEN, uintptr(unsafe.Pointer(_p0)), uintptr(mode), 0)
|
||||||
|
fd = int(r0)
|
||||||
|
if int32(r0) == -1 {
|
||||||
|
err = e1
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
|
||||||
|
|
||||||
|
func create(path string, mode int, perm uint32) (fd int, err error) {
|
||||||
|
var _p0 *byte
|
||||||
|
_p0, err = BytePtrFromString(path)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
r0, _, e1 := Syscall(SYS_CREATE, uintptr(unsafe.Pointer(_p0)), uintptr(mode), uintptr(perm))
|
||||||
|
fd = int(r0)
|
||||||
|
if int32(r0) == -1 {
|
||||||
|
err = e1
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
|
||||||
|
|
||||||
|
func remove(path string) (err error) {
|
||||||
|
var _p0 *byte
|
||||||
|
_p0, err = BytePtrFromString(path)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
r0, _, e1 := Syscall(SYS_REMOVE, uintptr(unsafe.Pointer(_p0)), 0, 0)
|
||||||
|
if int32(r0) == -1 {
|
||||||
|
err = e1
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
|
||||||
|
|
||||||
|
func stat(path string, edir []byte) (n int, err error) {
|
||||||
|
var _p0 *byte
|
||||||
|
_p0, err = BytePtrFromString(path)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var _p1 unsafe.Pointer
|
||||||
|
if len(edir) > 0 {
|
||||||
|
_p1 = unsafe.Pointer(&edir[0])
|
||||||
|
} else {
|
||||||
|
_p1 = unsafe.Pointer(&_zero)
|
||||||
|
}
|
||||||
|
r0, _, e1 := Syscall(SYS_STAT, uintptr(unsafe.Pointer(_p0)), uintptr(_p1), uintptr(len(edir)))
|
||||||
|
n = int(r0)
|
||||||
|
if int32(r0) == -1 {
|
||||||
|
err = e1
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
|
||||||
|
|
||||||
|
func bind(name string, old string, flag int) (err error) {
|
||||||
|
var _p0 *byte
|
||||||
|
_p0, err = BytePtrFromString(name)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var _p1 *byte
|
||||||
|
_p1, err = BytePtrFromString(old)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
r0, _, e1 := Syscall(SYS_BIND, uintptr(unsafe.Pointer(_p0)), uintptr(unsafe.Pointer(_p1)), uintptr(flag))
|
||||||
|
if int32(r0) == -1 {
|
||||||
|
err = e1
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
|
||||||
|
|
||||||
|
func mount(fd int, afd int, old string, flag int, aname string) (err error) {
|
||||||
|
var _p0 *byte
|
||||||
|
_p0, err = BytePtrFromString(old)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var _p1 *byte
|
||||||
|
_p1, err = BytePtrFromString(aname)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
r0, _, e1 := Syscall6(SYS_MOUNT, uintptr(fd), uintptr(afd), uintptr(unsafe.Pointer(_p0)), uintptr(flag), uintptr(unsafe.Pointer(_p1)), 0)
|
||||||
|
if int32(r0) == -1 {
|
||||||
|
err = e1
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
|
||||||
|
|
||||||
|
func wstat(path string, edir []byte) (err error) {
|
||||||
|
var _p0 *byte
|
||||||
|
_p0, err = BytePtrFromString(path)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var _p1 unsafe.Pointer
|
||||||
|
if len(edir) > 0 {
|
||||||
|
_p1 = unsafe.Pointer(&edir[0])
|
||||||
|
} else {
|
||||||
|
_p1 = unsafe.Pointer(&_zero)
|
||||||
|
}
|
||||||
|
r0, _, e1 := Syscall(SYS_WSTAT, uintptr(unsafe.Pointer(_p0)), uintptr(_p1), uintptr(len(edir)))
|
||||||
|
if int32(r0) == -1 {
|
||||||
|
err = e1
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
|
||||||
|
|
||||||
|
func chdir(path string) (err error) {
|
||||||
|
var _p0 *byte
|
||||||
|
_p0, err = BytePtrFromString(path)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
r0, _, e1 := Syscall(SYS_CHDIR, uintptr(unsafe.Pointer(_p0)), 0, 0)
|
||||||
|
if int32(r0) == -1 {
|
||||||
|
err = e1
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
|
||||||
|
|
||||||
|
func Dup(oldfd int, newfd int) (fd int, err error) {
|
||||||
|
r0, _, e1 := Syscall(SYS_DUP, uintptr(oldfd), uintptr(newfd), 0)
|
||||||
|
fd = int(r0)
|
||||||
|
if int32(r0) == -1 {
|
||||||
|
err = e1
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
|
||||||
|
|
||||||
|
func Pread(fd int, p []byte, offset int64) (n int, err error) {
|
||||||
|
var _p0 unsafe.Pointer
|
||||||
|
if len(p) > 0 {
|
||||||
|
_p0 = unsafe.Pointer(&p[0])
|
||||||
|
} else {
|
||||||
|
_p0 = unsafe.Pointer(&_zero)
|
||||||
|
}
|
||||||
|
r0, _, e1 := Syscall6(SYS_PREAD, uintptr(fd), uintptr(_p0), uintptr(len(p)), uintptr(offset), uintptr(offset>>32), 0)
|
||||||
|
n = int(r0)
|
||||||
|
if int32(r0) == -1 {
|
||||||
|
err = e1
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
|
||||||
|
|
||||||
|
func Pwrite(fd int, p []byte, offset int64) (n int, err error) {
|
||||||
|
var _p0 unsafe.Pointer
|
||||||
|
if len(p) > 0 {
|
||||||
|
_p0 = unsafe.Pointer(&p[0])
|
||||||
|
} else {
|
||||||
|
_p0 = unsafe.Pointer(&_zero)
|
||||||
|
}
|
||||||
|
r0, _, e1 := Syscall6(SYS_PWRITE, uintptr(fd), uintptr(_p0), uintptr(len(p)), uintptr(offset), uintptr(offset>>32), 0)
|
||||||
|
n = int(r0)
|
||||||
|
if int32(r0) == -1 {
|
||||||
|
err = e1
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
|
||||||
|
|
||||||
|
func Close(fd int) (err error) {
|
||||||
|
r0, _, e1 := Syscall(SYS_CLOSE, uintptr(fd), 0, 0)
|
||||||
|
if int32(r0) == -1 {
|
||||||
|
err = e1
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
|
||||||
|
|
||||||
|
func Fstat(fd int, edir []byte) (n int, err error) {
|
||||||
|
var _p0 unsafe.Pointer
|
||||||
|
if len(edir) > 0 {
|
||||||
|
_p0 = unsafe.Pointer(&edir[0])
|
||||||
|
} else {
|
||||||
|
_p0 = unsafe.Pointer(&_zero)
|
||||||
|
}
|
||||||
|
r0, _, e1 := Syscall(SYS_FSTAT, uintptr(fd), uintptr(_p0), uintptr(len(edir)))
|
||||||
|
n = int(r0)
|
||||||
|
if int32(r0) == -1 {
|
||||||
|
err = e1
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
|
||||||
|
|
||||||
|
func Fwstat(fd int, edir []byte) (err error) {
|
||||||
|
var _p0 unsafe.Pointer
|
||||||
|
if len(edir) > 0 {
|
||||||
|
_p0 = unsafe.Pointer(&edir[0])
|
||||||
|
} else {
|
||||||
|
_p0 = unsafe.Pointer(&_zero)
|
||||||
|
}
|
||||||
|
r0, _, e1 := Syscall(SYS_FWSTAT, uintptr(fd), uintptr(_p0), uintptr(len(edir)))
|
||||||
|
if int32(r0) == -1 {
|
||||||
|
err = e1
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
284
vendor/golang.org/x/sys/plan9/zsyscall_plan9_amd64.go
generated
vendored
Normal file
284
vendor/golang.org/x/sys/plan9/zsyscall_plan9_amd64.go
generated
vendored
Normal file
@@ -0,0 +1,284 @@
|
|||||||
|
// go run mksyscall.go -l32 -plan9 -tags plan9,amd64 syscall_plan9.go
|
||||||
|
// Code generated by the command above; see README.md. DO NOT EDIT.
|
||||||
|
|
||||||
|
//go:build plan9 && amd64
|
||||||
|
|
||||||
|
package plan9
|
||||||
|
|
||||||
|
import "unsafe"
|
||||||
|
|
||||||
|
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
|
||||||
|
|
||||||
|
func fd2path(fd int, buf []byte) (err error) {
|
||||||
|
var _p0 unsafe.Pointer
|
||||||
|
if len(buf) > 0 {
|
||||||
|
_p0 = unsafe.Pointer(&buf[0])
|
||||||
|
} else {
|
||||||
|
_p0 = unsafe.Pointer(&_zero)
|
||||||
|
}
|
||||||
|
r0, _, e1 := Syscall(SYS_FD2PATH, uintptr(fd), uintptr(_p0), uintptr(len(buf)))
|
||||||
|
if int32(r0) == -1 {
|
||||||
|
err = e1
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
|
||||||
|
|
||||||
|
func pipe(p *[2]int32) (err error) {
|
||||||
|
r0, _, e1 := Syscall(SYS_PIPE, uintptr(unsafe.Pointer(p)), 0, 0)
|
||||||
|
if int32(r0) == -1 {
|
||||||
|
err = e1
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
|
||||||
|
|
||||||
|
func await(s []byte) (n int, err error) {
|
||||||
|
var _p0 unsafe.Pointer
|
||||||
|
if len(s) > 0 {
|
||||||
|
_p0 = unsafe.Pointer(&s[0])
|
||||||
|
} else {
|
||||||
|
_p0 = unsafe.Pointer(&_zero)
|
||||||
|
}
|
||||||
|
r0, _, e1 := Syscall(SYS_AWAIT, uintptr(_p0), uintptr(len(s)), 0)
|
||||||
|
n = int(r0)
|
||||||
|
if int32(r0) == -1 {
|
||||||
|
err = e1
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
|
||||||
|
|
||||||
|
func open(path string, mode int) (fd int, err error) {
|
||||||
|
var _p0 *byte
|
||||||
|
_p0, err = BytePtrFromString(path)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
r0, _, e1 := Syscall(SYS_OPEN, uintptr(unsafe.Pointer(_p0)), uintptr(mode), 0)
|
||||||
|
fd = int(r0)
|
||||||
|
if int32(r0) == -1 {
|
||||||
|
err = e1
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
|
||||||
|
|
||||||
|
func create(path string, mode int, perm uint32) (fd int, err error) {
|
||||||
|
var _p0 *byte
|
||||||
|
_p0, err = BytePtrFromString(path)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
r0, _, e1 := Syscall(SYS_CREATE, uintptr(unsafe.Pointer(_p0)), uintptr(mode), uintptr(perm))
|
||||||
|
fd = int(r0)
|
||||||
|
if int32(r0) == -1 {
|
||||||
|
err = e1
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
|
||||||
|
|
||||||
|
func remove(path string) (err error) {
|
||||||
|
var _p0 *byte
|
||||||
|
_p0, err = BytePtrFromString(path)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
r0, _, e1 := Syscall(SYS_REMOVE, uintptr(unsafe.Pointer(_p0)), 0, 0)
|
||||||
|
if int32(r0) == -1 {
|
||||||
|
err = e1
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
|
||||||
|
|
||||||
|
func stat(path string, edir []byte) (n int, err error) {
|
||||||
|
var _p0 *byte
|
||||||
|
_p0, err = BytePtrFromString(path)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var _p1 unsafe.Pointer
|
||||||
|
if len(edir) > 0 {
|
||||||
|
_p1 = unsafe.Pointer(&edir[0])
|
||||||
|
} else {
|
||||||
|
_p1 = unsafe.Pointer(&_zero)
|
||||||
|
}
|
||||||
|
r0, _, e1 := Syscall(SYS_STAT, uintptr(unsafe.Pointer(_p0)), uintptr(_p1), uintptr(len(edir)))
|
||||||
|
n = int(r0)
|
||||||
|
if int32(r0) == -1 {
|
||||||
|
err = e1
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
|
||||||
|
|
||||||
|
func bind(name string, old string, flag int) (err error) {
|
||||||
|
var _p0 *byte
|
||||||
|
_p0, err = BytePtrFromString(name)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var _p1 *byte
|
||||||
|
_p1, err = BytePtrFromString(old)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
r0, _, e1 := Syscall(SYS_BIND, uintptr(unsafe.Pointer(_p0)), uintptr(unsafe.Pointer(_p1)), uintptr(flag))
|
||||||
|
if int32(r0) == -1 {
|
||||||
|
err = e1
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
|
||||||
|
|
||||||
|
func mount(fd int, afd int, old string, flag int, aname string) (err error) {
|
||||||
|
var _p0 *byte
|
||||||
|
_p0, err = BytePtrFromString(old)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var _p1 *byte
|
||||||
|
_p1, err = BytePtrFromString(aname)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
r0, _, e1 := Syscall6(SYS_MOUNT, uintptr(fd), uintptr(afd), uintptr(unsafe.Pointer(_p0)), uintptr(flag), uintptr(unsafe.Pointer(_p1)), 0)
|
||||||
|
if int32(r0) == -1 {
|
||||||
|
err = e1
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
|
||||||
|
|
||||||
|
func wstat(path string, edir []byte) (err error) {
|
||||||
|
var _p0 *byte
|
||||||
|
_p0, err = BytePtrFromString(path)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var _p1 unsafe.Pointer
|
||||||
|
if len(edir) > 0 {
|
||||||
|
_p1 = unsafe.Pointer(&edir[0])
|
||||||
|
} else {
|
||||||
|
_p1 = unsafe.Pointer(&_zero)
|
||||||
|
}
|
||||||
|
r0, _, e1 := Syscall(SYS_WSTAT, uintptr(unsafe.Pointer(_p0)), uintptr(_p1), uintptr(len(edir)))
|
||||||
|
if int32(r0) == -1 {
|
||||||
|
err = e1
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
|
||||||
|
|
||||||
|
func chdir(path string) (err error) {
|
||||||
|
var _p0 *byte
|
||||||
|
_p0, err = BytePtrFromString(path)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
r0, _, e1 := Syscall(SYS_CHDIR, uintptr(unsafe.Pointer(_p0)), 0, 0)
|
||||||
|
if int32(r0) == -1 {
|
||||||
|
err = e1
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
|
||||||
|
|
||||||
|
func Dup(oldfd int, newfd int) (fd int, err error) {
|
||||||
|
r0, _, e1 := Syscall(SYS_DUP, uintptr(oldfd), uintptr(newfd), 0)
|
||||||
|
fd = int(r0)
|
||||||
|
if int32(r0) == -1 {
|
||||||
|
err = e1
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
|
||||||
|
|
||||||
|
func Pread(fd int, p []byte, offset int64) (n int, err error) {
|
||||||
|
var _p0 unsafe.Pointer
|
||||||
|
if len(p) > 0 {
|
||||||
|
_p0 = unsafe.Pointer(&p[0])
|
||||||
|
} else {
|
||||||
|
_p0 = unsafe.Pointer(&_zero)
|
||||||
|
}
|
||||||
|
r0, _, e1 := Syscall6(SYS_PREAD, uintptr(fd), uintptr(_p0), uintptr(len(p)), uintptr(offset), uintptr(offset>>32), 0)
|
||||||
|
n = int(r0)
|
||||||
|
if int32(r0) == -1 {
|
||||||
|
err = e1
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
|
||||||
|
|
||||||
|
func Pwrite(fd int, p []byte, offset int64) (n int, err error) {
|
||||||
|
var _p0 unsafe.Pointer
|
||||||
|
if len(p) > 0 {
|
||||||
|
_p0 = unsafe.Pointer(&p[0])
|
||||||
|
} else {
|
||||||
|
_p0 = unsafe.Pointer(&_zero)
|
||||||
|
}
|
||||||
|
r0, _, e1 := Syscall6(SYS_PWRITE, uintptr(fd), uintptr(_p0), uintptr(len(p)), uintptr(offset), uintptr(offset>>32), 0)
|
||||||
|
n = int(r0)
|
||||||
|
if int32(r0) == -1 {
|
||||||
|
err = e1
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
|
||||||
|
|
||||||
|
func Close(fd int) (err error) {
|
||||||
|
r0, _, e1 := Syscall(SYS_CLOSE, uintptr(fd), 0, 0)
|
||||||
|
if int32(r0) == -1 {
|
||||||
|
err = e1
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
|
||||||
|
|
||||||
|
func Fstat(fd int, edir []byte) (n int, err error) {
|
||||||
|
var _p0 unsafe.Pointer
|
||||||
|
if len(edir) > 0 {
|
||||||
|
_p0 = unsafe.Pointer(&edir[0])
|
||||||
|
} else {
|
||||||
|
_p0 = unsafe.Pointer(&_zero)
|
||||||
|
}
|
||||||
|
r0, _, e1 := Syscall(SYS_FSTAT, uintptr(fd), uintptr(_p0), uintptr(len(edir)))
|
||||||
|
n = int(r0)
|
||||||
|
if int32(r0) == -1 {
|
||||||
|
err = e1
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
|
||||||
|
|
||||||
|
func Fwstat(fd int, edir []byte) (err error) {
|
||||||
|
var _p0 unsafe.Pointer
|
||||||
|
if len(edir) > 0 {
|
||||||
|
_p0 = unsafe.Pointer(&edir[0])
|
||||||
|
} else {
|
||||||
|
_p0 = unsafe.Pointer(&_zero)
|
||||||
|
}
|
||||||
|
r0, _, e1 := Syscall(SYS_FWSTAT, uintptr(fd), uintptr(_p0), uintptr(len(edir)))
|
||||||
|
if int32(r0) == -1 {
|
||||||
|
err = e1
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
284
vendor/golang.org/x/sys/plan9/zsyscall_plan9_arm.go
generated
vendored
Normal file
284
vendor/golang.org/x/sys/plan9/zsyscall_plan9_arm.go
generated
vendored
Normal file
@@ -0,0 +1,284 @@
|
|||||||
|
// go run mksyscall.go -l32 -plan9 -tags plan9,arm syscall_plan9.go
|
||||||
|
// Code generated by the command above; see README.md. DO NOT EDIT.
|
||||||
|
|
||||||
|
//go:build plan9 && arm
|
||||||
|
|
||||||
|
package plan9
|
||||||
|
|
||||||
|
import "unsafe"
|
||||||
|
|
||||||
|
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
|
||||||
|
|
||||||
|
func fd2path(fd int, buf []byte) (err error) {
|
||||||
|
var _p0 unsafe.Pointer
|
||||||
|
if len(buf) > 0 {
|
||||||
|
_p0 = unsafe.Pointer(&buf[0])
|
||||||
|
} else {
|
||||||
|
_p0 = unsafe.Pointer(&_zero)
|
||||||
|
}
|
||||||
|
r0, _, e1 := Syscall(SYS_FD2PATH, uintptr(fd), uintptr(_p0), uintptr(len(buf)))
|
||||||
|
if int32(r0) == -1 {
|
||||||
|
err = e1
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
|
||||||
|
|
||||||
|
func pipe(p *[2]int32) (err error) {
|
||||||
|
r0, _, e1 := Syscall(SYS_PIPE, uintptr(unsafe.Pointer(p)), 0, 0)
|
||||||
|
if int32(r0) == -1 {
|
||||||
|
err = e1
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
|
||||||
|
|
||||||
|
func await(s []byte) (n int, err error) {
|
||||||
|
var _p0 unsafe.Pointer
|
||||||
|
if len(s) > 0 {
|
||||||
|
_p0 = unsafe.Pointer(&s[0])
|
||||||
|
} else {
|
||||||
|
_p0 = unsafe.Pointer(&_zero)
|
||||||
|
}
|
||||||
|
r0, _, e1 := Syscall(SYS_AWAIT, uintptr(_p0), uintptr(len(s)), 0)
|
||||||
|
n = int(r0)
|
||||||
|
if int32(r0) == -1 {
|
||||||
|
err = e1
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
|
||||||
|
|
||||||
|
func open(path string, mode int) (fd int, err error) {
|
||||||
|
var _p0 *byte
|
||||||
|
_p0, err = BytePtrFromString(path)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
r0, _, e1 := Syscall(SYS_OPEN, uintptr(unsafe.Pointer(_p0)), uintptr(mode), 0)
|
||||||
|
fd = int(r0)
|
||||||
|
if int32(r0) == -1 {
|
||||||
|
err = e1
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
|
||||||
|
|
||||||
|
func create(path string, mode int, perm uint32) (fd int, err error) {
|
||||||
|
var _p0 *byte
|
||||||
|
_p0, err = BytePtrFromString(path)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
r0, _, e1 := Syscall(SYS_CREATE, uintptr(unsafe.Pointer(_p0)), uintptr(mode), uintptr(perm))
|
||||||
|
fd = int(r0)
|
||||||
|
if int32(r0) == -1 {
|
||||||
|
err = e1
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
|
||||||
|
|
||||||
|
func remove(path string) (err error) {
|
||||||
|
var _p0 *byte
|
||||||
|
_p0, err = BytePtrFromString(path)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
r0, _, e1 := Syscall(SYS_REMOVE, uintptr(unsafe.Pointer(_p0)), 0, 0)
|
||||||
|
if int32(r0) == -1 {
|
||||||
|
err = e1
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
|
||||||
|
|
||||||
|
func stat(path string, edir []byte) (n int, err error) {
|
||||||
|
var _p0 *byte
|
||||||
|
_p0, err = BytePtrFromString(path)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var _p1 unsafe.Pointer
|
||||||
|
if len(edir) > 0 {
|
||||||
|
_p1 = unsafe.Pointer(&edir[0])
|
||||||
|
} else {
|
||||||
|
_p1 = unsafe.Pointer(&_zero)
|
||||||
|
}
|
||||||
|
r0, _, e1 := Syscall(SYS_STAT, uintptr(unsafe.Pointer(_p0)), uintptr(_p1), uintptr(len(edir)))
|
||||||
|
n = int(r0)
|
||||||
|
if int32(r0) == -1 {
|
||||||
|
err = e1
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
|
||||||
|
|
||||||
|
func bind(name string, old string, flag int) (err error) {
|
||||||
|
var _p0 *byte
|
||||||
|
_p0, err = BytePtrFromString(name)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var _p1 *byte
|
||||||
|
_p1, err = BytePtrFromString(old)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
r0, _, e1 := Syscall(SYS_BIND, uintptr(unsafe.Pointer(_p0)), uintptr(unsafe.Pointer(_p1)), uintptr(flag))
|
||||||
|
if int32(r0) == -1 {
|
||||||
|
err = e1
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
|
||||||
|
|
||||||
|
func mount(fd int, afd int, old string, flag int, aname string) (err error) {
|
||||||
|
var _p0 *byte
|
||||||
|
_p0, err = BytePtrFromString(old)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var _p1 *byte
|
||||||
|
_p1, err = BytePtrFromString(aname)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
r0, _, e1 := Syscall6(SYS_MOUNT, uintptr(fd), uintptr(afd), uintptr(unsafe.Pointer(_p0)), uintptr(flag), uintptr(unsafe.Pointer(_p1)), 0)
|
||||||
|
if int32(r0) == -1 {
|
||||||
|
err = e1
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
|
||||||
|
|
||||||
|
func wstat(path string, edir []byte) (err error) {
|
||||||
|
var _p0 *byte
|
||||||
|
_p0, err = BytePtrFromString(path)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var _p1 unsafe.Pointer
|
||||||
|
if len(edir) > 0 {
|
||||||
|
_p1 = unsafe.Pointer(&edir[0])
|
||||||
|
} else {
|
||||||
|
_p1 = unsafe.Pointer(&_zero)
|
||||||
|
}
|
||||||
|
r0, _, e1 := Syscall(SYS_WSTAT, uintptr(unsafe.Pointer(_p0)), uintptr(_p1), uintptr(len(edir)))
|
||||||
|
if int32(r0) == -1 {
|
||||||
|
err = e1
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
|
||||||
|
|
||||||
|
func chdir(path string) (err error) {
|
||||||
|
var _p0 *byte
|
||||||
|
_p0, err = BytePtrFromString(path)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
r0, _, e1 := Syscall(SYS_CHDIR, uintptr(unsafe.Pointer(_p0)), 0, 0)
|
||||||
|
if int32(r0) == -1 {
|
||||||
|
err = e1
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
|
||||||
|
|
||||||
|
func Dup(oldfd int, newfd int) (fd int, err error) {
|
||||||
|
r0, _, e1 := Syscall(SYS_DUP, uintptr(oldfd), uintptr(newfd), 0)
|
||||||
|
fd = int(r0)
|
||||||
|
if int32(r0) == -1 {
|
||||||
|
err = e1
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
|
||||||
|
|
||||||
|
func Pread(fd int, p []byte, offset int64) (n int, err error) {
|
||||||
|
var _p0 unsafe.Pointer
|
||||||
|
if len(p) > 0 {
|
||||||
|
_p0 = unsafe.Pointer(&p[0])
|
||||||
|
} else {
|
||||||
|
_p0 = unsafe.Pointer(&_zero)
|
||||||
|
}
|
||||||
|
r0, _, e1 := Syscall6(SYS_PREAD, uintptr(fd), uintptr(_p0), uintptr(len(p)), uintptr(offset), uintptr(offset>>32), 0)
|
||||||
|
n = int(r0)
|
||||||
|
if int32(r0) == -1 {
|
||||||
|
err = e1
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
|
||||||
|
|
||||||
|
func Pwrite(fd int, p []byte, offset int64) (n int, err error) {
|
||||||
|
var _p0 unsafe.Pointer
|
||||||
|
if len(p) > 0 {
|
||||||
|
_p0 = unsafe.Pointer(&p[0])
|
||||||
|
} else {
|
||||||
|
_p0 = unsafe.Pointer(&_zero)
|
||||||
|
}
|
||||||
|
r0, _, e1 := Syscall6(SYS_PWRITE, uintptr(fd), uintptr(_p0), uintptr(len(p)), uintptr(offset), uintptr(offset>>32), 0)
|
||||||
|
n = int(r0)
|
||||||
|
if int32(r0) == -1 {
|
||||||
|
err = e1
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
|
||||||
|
|
||||||
|
func Close(fd int) (err error) {
|
||||||
|
r0, _, e1 := Syscall(SYS_CLOSE, uintptr(fd), 0, 0)
|
||||||
|
if int32(r0) == -1 {
|
||||||
|
err = e1
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
|
||||||
|
|
||||||
|
func Fstat(fd int, edir []byte) (n int, err error) {
|
||||||
|
var _p0 unsafe.Pointer
|
||||||
|
if len(edir) > 0 {
|
||||||
|
_p0 = unsafe.Pointer(&edir[0])
|
||||||
|
} else {
|
||||||
|
_p0 = unsafe.Pointer(&_zero)
|
||||||
|
}
|
||||||
|
r0, _, e1 := Syscall(SYS_FSTAT, uintptr(fd), uintptr(_p0), uintptr(len(edir)))
|
||||||
|
n = int(r0)
|
||||||
|
if int32(r0) == -1 {
|
||||||
|
err = e1
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
|
||||||
|
|
||||||
|
func Fwstat(fd int, edir []byte) (err error) {
|
||||||
|
var _p0 unsafe.Pointer
|
||||||
|
if len(edir) > 0 {
|
||||||
|
_p0 = unsafe.Pointer(&edir[0])
|
||||||
|
} else {
|
||||||
|
_p0 = unsafe.Pointer(&_zero)
|
||||||
|
}
|
||||||
|
r0, _, e1 := Syscall(SYS_FWSTAT, uintptr(fd), uintptr(_p0), uintptr(len(edir)))
|
||||||
|
if int32(r0) == -1 {
|
||||||
|
err = e1
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
49
vendor/golang.org/x/sys/plan9/zsysnum_plan9.go
generated
vendored
Normal file
49
vendor/golang.org/x/sys/plan9/zsysnum_plan9.go
generated
vendored
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
// mksysnum_plan9.sh /opt/plan9/sys/src/libc/9syscall/sys.h
|
||||||
|
// MACHINE GENERATED BY THE ABOVE COMMAND; DO NOT EDIT
|
||||||
|
|
||||||
|
package plan9
|
||||||
|
|
||||||
|
const (
|
||||||
|
SYS_SYSR1 = 0
|
||||||
|
SYS_BIND = 2
|
||||||
|
SYS_CHDIR = 3
|
||||||
|
SYS_CLOSE = 4
|
||||||
|
SYS_DUP = 5
|
||||||
|
SYS_ALARM = 6
|
||||||
|
SYS_EXEC = 7
|
||||||
|
SYS_EXITS = 8
|
||||||
|
SYS_FAUTH = 10
|
||||||
|
SYS_SEGBRK = 12
|
||||||
|
SYS_OPEN = 14
|
||||||
|
SYS_OSEEK = 16
|
||||||
|
SYS_SLEEP = 17
|
||||||
|
SYS_RFORK = 19
|
||||||
|
SYS_PIPE = 21
|
||||||
|
SYS_CREATE = 22
|
||||||
|
SYS_FD2PATH = 23
|
||||||
|
SYS_BRK_ = 24
|
||||||
|
SYS_REMOVE = 25
|
||||||
|
SYS_NOTIFY = 28
|
||||||
|
SYS_NOTED = 29
|
||||||
|
SYS_SEGATTACH = 30
|
||||||
|
SYS_SEGDETACH = 31
|
||||||
|
SYS_SEGFREE = 32
|
||||||
|
SYS_SEGFLUSH = 33
|
||||||
|
SYS_RENDEZVOUS = 34
|
||||||
|
SYS_UNMOUNT = 35
|
||||||
|
SYS_SEMACQUIRE = 37
|
||||||
|
SYS_SEMRELEASE = 38
|
||||||
|
SYS_SEEK = 39
|
||||||
|
SYS_FVERSION = 40
|
||||||
|
SYS_ERRSTR = 41
|
||||||
|
SYS_STAT = 42
|
||||||
|
SYS_FSTAT = 43
|
||||||
|
SYS_WSTAT = 44
|
||||||
|
SYS_FWSTAT = 45
|
||||||
|
SYS_MOUNT = 46
|
||||||
|
SYS_AWAIT = 47
|
||||||
|
SYS_PREAD = 50
|
||||||
|
SYS_PWRITE = 51
|
||||||
|
SYS_TSEMACQUIRE = 52
|
||||||
|
SYS_NSEC = 53
|
||||||
|
)
|
||||||
26
vendor/golang.org/x/term/CONTRIBUTING.md
generated
vendored
Normal file
26
vendor/golang.org/x/term/CONTRIBUTING.md
generated
vendored
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
# Contributing to Go
|
||||||
|
|
||||||
|
Go is an open source project.
|
||||||
|
|
||||||
|
It is the work of hundreds of contributors. We appreciate your help!
|
||||||
|
|
||||||
|
## Filing issues
|
||||||
|
|
||||||
|
When [filing an issue](https://golang.org/issue/new), make sure to answer these five questions:
|
||||||
|
|
||||||
|
1. What version of Go are you using (`go version`)?
|
||||||
|
2. What operating system and processor architecture are you using?
|
||||||
|
3. What did you do?
|
||||||
|
4. What did you expect to see?
|
||||||
|
5. What did you see instead?
|
||||||
|
|
||||||
|
General questions should go to the [golang-nuts mailing list](https://groups.google.com/group/golang-nuts) instead of the issue tracker.
|
||||||
|
The gophers there will answer or ask you to file an issue if you've tripped over a bug.
|
||||||
|
|
||||||
|
## Contributing code
|
||||||
|
|
||||||
|
Please read the [Contribution Guidelines](https://golang.org/doc/contribute.html)
|
||||||
|
before sending patches.
|
||||||
|
|
||||||
|
Unless otherwise noted, the Go source files are distributed under
|
||||||
|
the BSD-style license found in the LICENSE file.
|
||||||
27
vendor/golang.org/x/term/LICENSE
generated
vendored
Normal file
27
vendor/golang.org/x/term/LICENSE
generated
vendored
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
Copyright 2009 The Go Authors.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions are
|
||||||
|
met:
|
||||||
|
|
||||||
|
* Redistributions of source code must retain the above copyright
|
||||||
|
notice, this list of conditions and the following disclaimer.
|
||||||
|
* Redistributions in binary form must reproduce the above
|
||||||
|
copyright notice, this list of conditions and the following disclaimer
|
||||||
|
in the documentation and/or other materials provided with the
|
||||||
|
distribution.
|
||||||
|
* Neither the name of Google LLC nor the names of its
|
||||||
|
contributors may be used to endorse or promote products derived from
|
||||||
|
this software without specific prior written permission.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||||
|
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||||
|
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||||
|
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||||
|
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||||
|
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||||
|
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||||
|
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||||
|
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||||
|
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||||
|
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
22
vendor/golang.org/x/term/PATENTS
generated
vendored
Normal file
22
vendor/golang.org/x/term/PATENTS
generated
vendored
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
Additional IP Rights Grant (Patents)
|
||||||
|
|
||||||
|
"This implementation" means the copyrightable works distributed by
|
||||||
|
Google as part of the Go project.
|
||||||
|
|
||||||
|
Google hereby grants to You a perpetual, worldwide, non-exclusive,
|
||||||
|
no-charge, royalty-free, irrevocable (except as stated in this section)
|
||||||
|
patent license to make, have made, use, offer to sell, sell, import,
|
||||||
|
transfer and otherwise run, modify and propagate the contents of this
|
||||||
|
implementation of Go, where such license applies only to those patent
|
||||||
|
claims, both currently owned or controlled by Google and acquired in
|
||||||
|
the future, licensable by Google that are necessarily infringed by this
|
||||||
|
implementation of Go. This grant does not include claims that would be
|
||||||
|
infringed only as a consequence of further modification of this
|
||||||
|
implementation. If you or your agent or exclusive licensee institute or
|
||||||
|
order or agree to the institution of patent litigation against any
|
||||||
|
entity (including a cross-claim or counterclaim in a lawsuit) alleging
|
||||||
|
that this implementation of Go or any code incorporated within this
|
||||||
|
implementation of Go constitutes direct or contributory patent
|
||||||
|
infringement, or inducement of patent infringement, then any patent
|
||||||
|
rights granted to you under this License for this implementation of Go
|
||||||
|
shall terminate as of the date such litigation is filed.
|
||||||
16
vendor/golang.org/x/term/README.md
generated
vendored
Normal file
16
vendor/golang.org/x/term/README.md
generated
vendored
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
# Go terminal/console support
|
||||||
|
|
||||||
|
[](https://pkg.go.dev/golang.org/x/term)
|
||||||
|
|
||||||
|
This repository provides Go terminal and console support packages.
|
||||||
|
|
||||||
|
## Report Issues / Send Patches
|
||||||
|
|
||||||
|
This repository uses Gerrit for code changes. To learn how to submit changes to
|
||||||
|
this repository, see https://go.dev/doc/contribute.
|
||||||
|
|
||||||
|
The git repository is https://go.googlesource.com/term.
|
||||||
|
|
||||||
|
The main issue tracker for the term repository is located at
|
||||||
|
https://go.dev/issues. Prefix your issue with "x/term:" in the
|
||||||
|
subject line, so it is easy to find.
|
||||||
1
vendor/golang.org/x/term/codereview.cfg
generated
vendored
Normal file
1
vendor/golang.org/x/term/codereview.cfg
generated
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
issuerepo: golang/go
|
||||||
60
vendor/golang.org/x/term/term.go
generated
vendored
Normal file
60
vendor/golang.org/x/term/term.go
generated
vendored
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
// Copyright 2019 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
// Package term provides support functions for dealing with terminals, as
|
||||||
|
// commonly found on UNIX systems.
|
||||||
|
//
|
||||||
|
// Putting a terminal into raw mode is the most common requirement:
|
||||||
|
//
|
||||||
|
// oldState, err := term.MakeRaw(int(os.Stdin.Fd()))
|
||||||
|
// if err != nil {
|
||||||
|
// panic(err)
|
||||||
|
// }
|
||||||
|
// defer term.Restore(int(os.Stdin.Fd()), oldState)
|
||||||
|
//
|
||||||
|
// Note that on non-Unix systems os.Stdin.Fd() may not be 0.
|
||||||
|
package term
|
||||||
|
|
||||||
|
// State contains the state of a terminal.
|
||||||
|
type State struct {
|
||||||
|
state
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsTerminal returns whether the given file descriptor is a terminal.
|
||||||
|
func IsTerminal(fd int) bool {
|
||||||
|
return isTerminal(fd)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MakeRaw puts the terminal connected to the given file descriptor into raw
|
||||||
|
// mode and returns the previous state of the terminal so that it can be
|
||||||
|
// restored.
|
||||||
|
func MakeRaw(fd int) (*State, error) {
|
||||||
|
return makeRaw(fd)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetState returns the current state of a terminal which may be useful to
|
||||||
|
// restore the terminal after a signal.
|
||||||
|
func GetState(fd int) (*State, error) {
|
||||||
|
return getState(fd)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restore restores the terminal connected to the given file descriptor to a
|
||||||
|
// previous state.
|
||||||
|
func Restore(fd int, oldState *State) error {
|
||||||
|
return restore(fd, oldState)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSize returns the visible dimensions of the given terminal.
|
||||||
|
//
|
||||||
|
// These dimensions don't include any scrollback buffer height.
|
||||||
|
func GetSize(fd int) (width, height int, err error) {
|
||||||
|
return getSize(fd)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadPassword reads a line of input from a terminal without local echo. This
|
||||||
|
// is commonly used for inputting passwords and other sensitive data. The slice
|
||||||
|
// returned does not include the \n.
|
||||||
|
func ReadPassword(fd int) ([]byte, error) {
|
||||||
|
return readPassword(fd)
|
||||||
|
}
|
||||||
42
vendor/golang.org/x/term/term_plan9.go
generated
vendored
Normal file
42
vendor/golang.org/x/term/term_plan9.go
generated
vendored
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
// Copyright 2019 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package term
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"runtime"
|
||||||
|
|
||||||
|
"golang.org/x/sys/plan9"
|
||||||
|
)
|
||||||
|
|
||||||
|
type state struct{}
|
||||||
|
|
||||||
|
func isTerminal(fd int) bool {
|
||||||
|
path, err := plan9.Fd2path(fd)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return path == "/dev/cons" || path == "/mnt/term/dev/cons"
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeRaw(fd int) (*State, error) {
|
||||||
|
return nil, fmt.Errorf("terminal: MakeRaw not implemented on %s/%s", runtime.GOOS, runtime.GOARCH)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getState(fd int) (*State, error) {
|
||||||
|
return nil, fmt.Errorf("terminal: GetState not implemented on %s/%s", runtime.GOOS, runtime.GOARCH)
|
||||||
|
}
|
||||||
|
|
||||||
|
func restore(fd int, state *State) error {
|
||||||
|
return fmt.Errorf("terminal: Restore not implemented on %s/%s", runtime.GOOS, runtime.GOARCH)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getSize(fd int) (width, height int, err error) {
|
||||||
|
return 0, 0, fmt.Errorf("terminal: GetSize not implemented on %s/%s", runtime.GOOS, runtime.GOARCH)
|
||||||
|
}
|
||||||
|
|
||||||
|
func readPassword(fd int) ([]byte, error) {
|
||||||
|
return nil, fmt.Errorf("terminal: ReadPassword not implemented on %s/%s", runtime.GOOS, runtime.GOARCH)
|
||||||
|
}
|
||||||
91
vendor/golang.org/x/term/term_unix.go
generated
vendored
Normal file
91
vendor/golang.org/x/term/term_unix.go
generated
vendored
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
// Copyright 2019 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
//go:build aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris || zos
|
||||||
|
|
||||||
|
package term
|
||||||
|
|
||||||
|
import (
|
||||||
|
"golang.org/x/sys/unix"
|
||||||
|
)
|
||||||
|
|
||||||
|
type state struct {
|
||||||
|
termios unix.Termios
|
||||||
|
}
|
||||||
|
|
||||||
|
func isTerminal(fd int) bool {
|
||||||
|
_, err := unix.IoctlGetTermios(fd, ioctlReadTermios)
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeRaw(fd int) (*State, error) {
|
||||||
|
termios, err := unix.IoctlGetTermios(fd, ioctlReadTermios)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
oldState := State{state{termios: *termios}}
|
||||||
|
|
||||||
|
// This attempts to replicate the behaviour documented for cfmakeraw in
|
||||||
|
// the termios(3) manpage.
|
||||||
|
termios.Iflag &^= unix.IGNBRK | unix.BRKINT | unix.PARMRK | unix.ISTRIP | unix.INLCR | unix.IGNCR | unix.ICRNL | unix.IXON
|
||||||
|
termios.Oflag &^= unix.OPOST
|
||||||
|
termios.Lflag &^= unix.ECHO | unix.ECHONL | unix.ICANON | unix.ISIG | unix.IEXTEN
|
||||||
|
termios.Cflag &^= unix.CSIZE | unix.PARENB
|
||||||
|
termios.Cflag |= unix.CS8
|
||||||
|
termios.Cc[unix.VMIN] = 1
|
||||||
|
termios.Cc[unix.VTIME] = 0
|
||||||
|
if err := unix.IoctlSetTermios(fd, ioctlWriteTermios, termios); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &oldState, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getState(fd int) (*State, error) {
|
||||||
|
termios, err := unix.IoctlGetTermios(fd, ioctlReadTermios)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &State{state{termios: *termios}}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func restore(fd int, state *State) error {
|
||||||
|
return unix.IoctlSetTermios(fd, ioctlWriteTermios, &state.termios)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getSize(fd int) (width, height int, err error) {
|
||||||
|
ws, err := unix.IoctlGetWinsize(fd, unix.TIOCGWINSZ)
|
||||||
|
if err != nil {
|
||||||
|
return 0, 0, err
|
||||||
|
}
|
||||||
|
return int(ws.Col), int(ws.Row), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// passwordReader is an io.Reader that reads from a specific file descriptor.
|
||||||
|
type passwordReader int
|
||||||
|
|
||||||
|
func (r passwordReader) Read(buf []byte) (int, error) {
|
||||||
|
return unix.Read(int(r), buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
func readPassword(fd int) ([]byte, error) {
|
||||||
|
termios, err := unix.IoctlGetTermios(fd, ioctlReadTermios)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
newState := *termios
|
||||||
|
newState.Lflag &^= unix.ECHO
|
||||||
|
newState.Lflag |= unix.ICANON | unix.ISIG
|
||||||
|
newState.Iflag |= unix.ICRNL
|
||||||
|
if err := unix.IoctlSetTermios(fd, ioctlWriteTermios, &newState); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer unix.IoctlSetTermios(fd, ioctlWriteTermios, termios)
|
||||||
|
|
||||||
|
return readPasswordLine(passwordReader(fd))
|
||||||
|
}
|
||||||
12
vendor/golang.org/x/term/term_unix_bsd.go
generated
vendored
Normal file
12
vendor/golang.org/x/term/term_unix_bsd.go
generated
vendored
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
// Copyright 2013 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
//go:build darwin || dragonfly || freebsd || netbsd || openbsd
|
||||||
|
|
||||||
|
package term
|
||||||
|
|
||||||
|
import "golang.org/x/sys/unix"
|
||||||
|
|
||||||
|
const ioctlReadTermios = unix.TIOCGETA
|
||||||
|
const ioctlWriteTermios = unix.TIOCSETA
|
||||||
12
vendor/golang.org/x/term/term_unix_other.go
generated
vendored
Normal file
12
vendor/golang.org/x/term/term_unix_other.go
generated
vendored
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
// Copyright 2021 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
//go:build aix || linux || solaris || zos
|
||||||
|
|
||||||
|
package term
|
||||||
|
|
||||||
|
import "golang.org/x/sys/unix"
|
||||||
|
|
||||||
|
const ioctlReadTermios = unix.TCGETS
|
||||||
|
const ioctlWriteTermios = unix.TCSETS
|
||||||
38
vendor/golang.org/x/term/term_unsupported.go
generated
vendored
Normal file
38
vendor/golang.org/x/term/term_unsupported.go
generated
vendored
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
// Copyright 2019 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
//go:build !aix && !darwin && !dragonfly && !freebsd && !linux && !netbsd && !openbsd && !zos && !windows && !solaris && !plan9
|
||||||
|
|
||||||
|
package term
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"runtime"
|
||||||
|
)
|
||||||
|
|
||||||
|
type state struct{}
|
||||||
|
|
||||||
|
func isTerminal(fd int) bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeRaw(fd int) (*State, error) {
|
||||||
|
return nil, fmt.Errorf("terminal: MakeRaw not implemented on %s/%s", runtime.GOOS, runtime.GOARCH)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getState(fd int) (*State, error) {
|
||||||
|
return nil, fmt.Errorf("terminal: GetState not implemented on %s/%s", runtime.GOOS, runtime.GOARCH)
|
||||||
|
}
|
||||||
|
|
||||||
|
func restore(fd int, state *State) error {
|
||||||
|
return fmt.Errorf("terminal: Restore not implemented on %s/%s", runtime.GOOS, runtime.GOARCH)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getSize(fd int) (width, height int, err error) {
|
||||||
|
return 0, 0, fmt.Errorf("terminal: GetSize not implemented on %s/%s", runtime.GOOS, runtime.GOARCH)
|
||||||
|
}
|
||||||
|
|
||||||
|
func readPassword(fd int) ([]byte, error) {
|
||||||
|
return nil, fmt.Errorf("terminal: ReadPassword not implemented on %s/%s", runtime.GOOS, runtime.GOARCH)
|
||||||
|
}
|
||||||
82
vendor/golang.org/x/term/term_windows.go
generated
vendored
Normal file
82
vendor/golang.org/x/term/term_windows.go
generated
vendored
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
// Copyright 2019 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package term
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"golang.org/x/sys/windows"
|
||||||
|
)
|
||||||
|
|
||||||
|
type state struct {
|
||||||
|
mode uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
func isTerminal(fd int) bool {
|
||||||
|
var st uint32
|
||||||
|
err := windows.GetConsoleMode(windows.Handle(fd), &st)
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is intended to be used on a console input handle.
|
||||||
|
// See https://learn.microsoft.com/en-us/windows/console/setconsolemode
|
||||||
|
func makeRaw(fd int) (*State, error) {
|
||||||
|
var st uint32
|
||||||
|
if err := windows.GetConsoleMode(windows.Handle(fd), &st); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
raw := st &^ (windows.ENABLE_ECHO_INPUT | windows.ENABLE_PROCESSED_INPUT | windows.ENABLE_LINE_INPUT)
|
||||||
|
raw |= windows.ENABLE_VIRTUAL_TERMINAL_INPUT
|
||||||
|
if err := windows.SetConsoleMode(windows.Handle(fd), raw); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &State{state{st}}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getState(fd int) (*State, error) {
|
||||||
|
var st uint32
|
||||||
|
if err := windows.GetConsoleMode(windows.Handle(fd), &st); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &State{state{st}}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func restore(fd int, state *State) error {
|
||||||
|
return windows.SetConsoleMode(windows.Handle(fd), state.mode)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getSize(fd int) (width, height int, err error) {
|
||||||
|
var info windows.ConsoleScreenBufferInfo
|
||||||
|
if err := windows.GetConsoleScreenBufferInfo(windows.Handle(fd), &info); err != nil {
|
||||||
|
return 0, 0, err
|
||||||
|
}
|
||||||
|
return int(info.Window.Right - info.Window.Left + 1), int(info.Window.Bottom - info.Window.Top + 1), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func readPassword(fd int) ([]byte, error) {
|
||||||
|
var st uint32
|
||||||
|
if err := windows.GetConsoleMode(windows.Handle(fd), &st); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
old := st
|
||||||
|
|
||||||
|
st &^= (windows.ENABLE_ECHO_INPUT | windows.ENABLE_LINE_INPUT)
|
||||||
|
st |= (windows.ENABLE_PROCESSED_OUTPUT | windows.ENABLE_PROCESSED_INPUT)
|
||||||
|
if err := windows.SetConsoleMode(windows.Handle(fd), st); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer windows.SetConsoleMode(windows.Handle(fd), old)
|
||||||
|
|
||||||
|
var h windows.Handle
|
||||||
|
p, _ := windows.GetCurrentProcess()
|
||||||
|
if err := windows.DuplicateHandle(p, windows.Handle(fd), p, &h, 0, false, windows.DUPLICATE_SAME_ACCESS); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
f := os.NewFile(uintptr(h), "stdin")
|
||||||
|
defer f.Close()
|
||||||
|
return readPasswordLine(f)
|
||||||
|
}
|
||||||
1074
vendor/golang.org/x/term/terminal.go
generated
vendored
Normal file
1074
vendor/golang.org/x/term/terminal.go
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
9
vendor/modules.txt
vendored
9
vendor/modules.txt
vendored
@@ -1,7 +1,10 @@
|
|||||||
# 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
|
||||||
|
# git.wntrmute.dev/mc/mcdsl v1.3.0
|
||||||
|
## explicit; go 1.25.7
|
||||||
|
git.wntrmute.dev/mc/mcdsl/terminal
|
||||||
# github.com/dustin/go-humanize v1.0.1
|
# github.com/dustin/go-humanize v1.0.1
|
||||||
## explicit; go 1.16
|
## explicit; go 1.16
|
||||||
github.com/dustin/go-humanize
|
github.com/dustin/go-humanize
|
||||||
@@ -43,8 +46,12 @@ golang.org/x/net/internal/timeseries
|
|||||||
golang.org/x/net/trace
|
golang.org/x/net/trace
|
||||||
# golang.org/x/sys v0.42.0
|
# golang.org/x/sys v0.42.0
|
||||||
## explicit; go 1.25.0
|
## explicit; go 1.25.0
|
||||||
|
golang.org/x/sys/plan9
|
||||||
golang.org/x/sys/unix
|
golang.org/x/sys/unix
|
||||||
golang.org/x/sys/windows
|
golang.org/x/sys/windows
|
||||||
|
# golang.org/x/term v0.41.0
|
||||||
|
## explicit; go 1.25.0
|
||||||
|
golang.org/x/term
|
||||||
# golang.org/x/text v0.32.0
|
# golang.org/x/text v0.32.0
|
||||||
## explicit; go 1.24.0
|
## explicit; go 1.24.0
|
||||||
golang.org/x/text/secure/bidirule
|
golang.org/x/text/secure/bidirule
|
||||||
|
|||||||
Reference in New Issue
Block a user