Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3d2edb7c26 | |||
| bf02935716 | |||
| c4f0d7be8e | |||
| 4d900eafd1 | |||
| 38f9070c24 | |||
| 67d0ab1d9d | |||
| 7383b370f0 |
@@ -38,7 +38,7 @@ func main() {
|
||||
if err != nil {
|
||||
return fmt.Errorf("load config: %w", err)
|
||||
}
|
||||
return agent.Run(cfg)
|
||||
return agent.Run(cfg, version)
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
12
cmd/mcp/edit.go
Normal file
12
cmd/mcp/edit.go
Normal file
@@ -0,0 +1,12 @@
|
||||
package main
|
||||
|
||||
import "github.com/spf13/cobra"
|
||||
|
||||
func editCmd() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "edit <service>",
|
||||
Short: "Open service definition in $EDITOR",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: runServiceEdit,
|
||||
}
|
||||
}
|
||||
@@ -51,6 +51,7 @@ func main() {
|
||||
root.AddCommand(nodeCmd())
|
||||
root.AddCommand(purgeCmd())
|
||||
root.AddCommand(logsCmd())
|
||||
root.AddCommand(editCmd())
|
||||
|
||||
if err := root.Execute(); err != nil {
|
||||
log.Fatal(err)
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"text/tabwriter"
|
||||
"time"
|
||||
|
||||
toml "github.com/pelletier/go-toml/v2"
|
||||
|
||||
mcpv1 "git.wntrmute.dev/mc/mcp/gen/mcp/v1"
|
||||
"git.wntrmute.dev/mc/mcp/internal/config"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
@@ -48,13 +51,35 @@ func runNodeList(_ *cobra.Command, _ []string) error {
|
||||
}
|
||||
|
||||
w := tabwriter.NewWriter(os.Stdout, 0, 4, 2, ' ', 0)
|
||||
_, _ = fmt.Fprintln(w, "NAME\tADDRESS")
|
||||
_, _ = fmt.Fprintln(w, "NAME\tADDRESS\tVERSION")
|
||||
for _, n := range cfg.Nodes {
|
||||
_, _ = fmt.Fprintf(w, "%s\t%s\n", n.Name, n.Address)
|
||||
ver := queryAgentVersion(cfg, n.Address)
|
||||
_, _ = fmt.Fprintf(w, "%s\t%s\t%s\n", n.Name, n.Address, ver)
|
||||
}
|
||||
return w.Flush()
|
||||
}
|
||||
|
||||
// queryAgentVersion dials the agent and returns its version, or an error indicator.
|
||||
func queryAgentVersion(cfg *config.CLIConfig, address string) string {
|
||||
client, conn, err := dialAgent(address, cfg)
|
||||
if err != nil {
|
||||
return "error"
|
||||
}
|
||||
defer func() { _ = conn.Close() }()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
resp, err := client.NodeStatus(ctx, &mcpv1.NodeStatusRequest{})
|
||||
if err != nil {
|
||||
return "error"
|
||||
}
|
||||
if resp.AgentVersion == "" {
|
||||
return "unknown"
|
||||
}
|
||||
return resp.AgentVersion
|
||||
}
|
||||
|
||||
func runNodeAdd(_ *cobra.Command, args []string) error {
|
||||
cfg, err := config.LoadCLIConfig(cfgPath)
|
||||
if err != nil {
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
let
|
||||
system = "x86_64-linux";
|
||||
pkgs = nixpkgs.legacyPackages.${system};
|
||||
version = "0.6.0";
|
||||
version = pkgs.lib.removePrefix "v" (self.gitDescribe or self.shortRev or self.dirtyShortRev or "unknown");
|
||||
in
|
||||
{
|
||||
packages.${system} = {
|
||||
|
||||
@@ -1926,6 +1926,7 @@ type NodeStatusResponse struct {
|
||||
MemoryFreeBytes uint64 `protobuf:"varint,9,opt,name=memory_free_bytes,json=memoryFreeBytes,proto3" json:"memory_free_bytes,omitempty"`
|
||||
CpuUsagePercent float64 `protobuf:"fixed64,10,opt,name=cpu_usage_percent,json=cpuUsagePercent,proto3" json:"cpu_usage_percent,omitempty"`
|
||||
UptimeSince *timestamppb.Timestamp `protobuf:"bytes,11,opt,name=uptime_since,json=uptimeSince,proto3" json:"uptime_since,omitempty"`
|
||||
AgentVersion string `protobuf:"bytes,12,opt,name=agent_version,json=agentVersion,proto3" json:"agent_version,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
@@ -2037,6 +2038,13 @@ func (x *NodeStatusResponse) GetUptimeSince() *timestamppb.Timestamp {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *NodeStatusResponse) GetAgentVersion() string {
|
||||
if x != nil {
|
||||
return x.AgentVersion
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type PurgeRequest struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
// Service name (empty = all services).
|
||||
@@ -2474,7 +2482,7 @@ const file_proto_mcp_v1_mcp_proto_rawDesc = "" +
|
||||
"\acontent\x18\x01 \x01(\fR\acontent\x12\x12\n" +
|
||||
"\x04mode\x18\x02 \x01(\rR\x04mode\x12\x14\n" +
|
||||
"\x05error\x18\x03 \x01(\tR\x05error\"\x13\n" +
|
||||
"\x11NodeStatusRequest\"\xd9\x03\n" +
|
||||
"\x11NodeStatusRequest\"\xfe\x03\n" +
|
||||
"\x12NodeStatusResponse\x12\x1b\n" +
|
||||
"\tnode_name\x18\x01 \x01(\tR\bnodeName\x12\x18\n" +
|
||||
"\aruntime\x18\x02 \x01(\tR\aruntime\x12'\n" +
|
||||
@@ -2487,7 +2495,8 @@ const file_proto_mcp_v1_mcp_proto_rawDesc = "" +
|
||||
"\x11memory_free_bytes\x18\t \x01(\x04R\x0fmemoryFreeBytes\x12*\n" +
|
||||
"\x11cpu_usage_percent\x18\n" +
|
||||
" \x01(\x01R\x0fcpuUsagePercent\x12=\n" +
|
||||
"\fuptime_since\x18\v \x01(\v2\x1a.google.protobuf.TimestampR\vuptimeSince\"\x8e\x01\n" +
|
||||
"\fuptime_since\x18\v \x01(\v2\x1a.google.protobuf.TimestampR\vuptimeSince\x12#\n" +
|
||||
"\ragent_version\x18\f \x01(\tR\fagentVersion\"\x8e\x01\n" +
|
||||
"\fPurgeRequest\x12\x18\n" +
|
||||
"\aservice\x18\x01 \x01(\tR\aservice\x12\x1c\n" +
|
||||
"\tcomponent\x18\x02 \x01(\tR\tcomponent\x12\x17\n" +
|
||||
|
||||
@@ -35,11 +35,12 @@ type Agent struct {
|
||||
Proxy *ProxyRouter
|
||||
Certs *CertProvisioner
|
||||
DNS *DNSRegistrar
|
||||
Version string
|
||||
}
|
||||
|
||||
// Run starts the agent: opens the database, sets up the gRPC server with
|
||||
// TLS and auth, and blocks until SIGINT/SIGTERM.
|
||||
func Run(cfg *config.AgentConfig) error {
|
||||
func Run(cfg *config.AgentConfig, version string) error {
|
||||
logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{
|
||||
Level: parseLogLevel(cfg.Log.Level),
|
||||
}))
|
||||
@@ -79,6 +80,7 @@ func Run(cfg *config.AgentConfig) error {
|
||||
Proxy: proxy,
|
||||
Certs: certs,
|
||||
DNS: dns,
|
||||
Version: version,
|
||||
}
|
||||
|
||||
tlsCert, err := tls.LoadX509KeyPair(cfg.Server.TLSCert, cfg.Server.TLSKey)
|
||||
|
||||
@@ -34,6 +34,9 @@ func (a *Agent) Deploy(ctx context.Context, req *mcpv1.DeployRequest) (*mcpv1.De
|
||||
filtered = append(filtered, cs)
|
||||
}
|
||||
}
|
||||
if len(filtered) == 0 {
|
||||
return nil, fmt.Errorf("component %q not found in service %q", target, serviceName)
|
||||
}
|
||||
components = filtered
|
||||
}
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@ func (a *Agent) NodeStatus(ctx context.Context, _ *mcpv1.NodeStatusRequest) (*mc
|
||||
Runtime: a.Config.Agent.ContainerRuntime,
|
||||
ServiceCount: uint32(len(services)), //nolint:gosec // bounded
|
||||
ComponentCount: componentCount,
|
||||
AgentVersion: a.Version,
|
||||
}
|
||||
|
||||
// Runtime version.
|
||||
|
||||
@@ -99,6 +99,12 @@ func (a *Agent) liveCheckServices(ctx context.Context) ([]*mcpv1.ServiceInfo, er
|
||||
|
||||
if rc, ok := runtimeByName[containerName]; ok {
|
||||
ci.ObservedState = rc.State
|
||||
if rc.Version != "" {
|
||||
ci.Version = rc.Version
|
||||
}
|
||||
if rc.Image != "" {
|
||||
ci.Image = rc.Image
|
||||
}
|
||||
if !rc.Started.IsZero() {
|
||||
ci.Started = timestamppb.New(rc.Started)
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -179,16 +180,53 @@ func (p *Podman) Inspect(ctx context.Context, name string) (ContainerInfo, error
|
||||
}
|
||||
|
||||
// Logs returns an exec.Cmd that streams container logs. For containers
|
||||
// using the journald log driver, it uses journalctl (podman logs can't
|
||||
// read journald outside the originating user session). For k8s-file or
|
||||
// other drivers, it uses podman logs directly.
|
||||
// using the journald log driver, it tries journalctl first (podman logs
|
||||
// can't read journald outside the originating user session). If journalctl
|
||||
// can't access the journal, it falls back to podman logs.
|
||||
func (p *Podman) Logs(ctx context.Context, containerName string, tail int, follow, timestamps bool, since string) *exec.Cmd {
|
||||
// Check if this container uses the journald log driver.
|
||||
inspectCmd := exec.CommandContext(ctx, p.command(), "inspect", "--format", "{{.HostConfig.LogConfig.Type}}", containerName) //nolint:gosec
|
||||
if out, err := inspectCmd.Output(); err == nil && strings.TrimSpace(string(out)) == "journald" {
|
||||
return p.journalLogs(ctx, containerName, tail, follow, since)
|
||||
if p.journalAccessible(ctx, containerName) {
|
||||
return p.journalLogs(ctx, containerName, tail, follow, since)
|
||||
}
|
||||
}
|
||||
|
||||
return p.podmanLogs(ctx, containerName, tail, follow, timestamps, since)
|
||||
}
|
||||
|
||||
// journalAccessible probes whether journalctl can read logs for the container.
|
||||
func (p *Podman) journalAccessible(ctx context.Context, containerName string) bool {
|
||||
args := []string{"--no-pager", "-n", "0"}
|
||||
if os.Getuid() != 0 {
|
||||
args = append(args, "--user")
|
||||
}
|
||||
args = append(args, "CONTAINER_NAME="+containerName)
|
||||
cmd := exec.CommandContext(ctx, "journalctl", args...) //nolint:gosec
|
||||
return cmd.Run() == nil
|
||||
}
|
||||
|
||||
// journalLogs returns a journalctl command filtered by container name.
|
||||
func (p *Podman) journalLogs(ctx context.Context, containerName string, tail int, follow bool, since string) *exec.Cmd {
|
||||
args := []string{"--no-pager", "--output", "cat"}
|
||||
if os.Getuid() != 0 {
|
||||
args = append(args, "--user")
|
||||
}
|
||||
args = append(args, "CONTAINER_NAME="+containerName)
|
||||
if tail > 0 {
|
||||
args = append(args, "--lines", fmt.Sprintf("%d", tail))
|
||||
}
|
||||
if follow {
|
||||
args = append(args, "--follow")
|
||||
}
|
||||
if since != "" {
|
||||
args = append(args, "--since", since)
|
||||
}
|
||||
return exec.CommandContext(ctx, "journalctl", args...) //nolint:gosec // args built programmatically
|
||||
}
|
||||
|
||||
// podmanLogs returns a podman logs command.
|
||||
func (p *Podman) podmanLogs(ctx context.Context, containerName string, tail int, follow, timestamps bool, since string) *exec.Cmd {
|
||||
args := []string{"logs"}
|
||||
if tail > 0 {
|
||||
args = append(args, "--tail", fmt.Sprintf("%d", tail))
|
||||
@@ -206,21 +244,6 @@ func (p *Podman) Logs(ctx context.Context, containerName string, tail int, follo
|
||||
return exec.CommandContext(ctx, p.command(), args...) //nolint:gosec // args built programmatically
|
||||
}
|
||||
|
||||
// journalLogs returns a journalctl command filtered by container name.
|
||||
func (p *Podman) journalLogs(ctx context.Context, containerName string, tail int, follow bool, since string) *exec.Cmd {
|
||||
args := []string{"--no-pager", "--output", "cat", "CONTAINER_NAME=" + containerName}
|
||||
if tail > 0 {
|
||||
args = append(args, "--lines", fmt.Sprintf("%d", tail))
|
||||
}
|
||||
if follow {
|
||||
args = append(args, "--follow")
|
||||
}
|
||||
if since != "" {
|
||||
args = append(args, "--since", since)
|
||||
}
|
||||
return exec.CommandContext(ctx, "journalctl", args...) //nolint:gosec // args built programmatically
|
||||
}
|
||||
|
||||
// Login authenticates to a container registry using the given token as
|
||||
// the password. This enables non-interactive push with service account
|
||||
// tokens (MCR accepts MCIAS JWTs as passwords).
|
||||
|
||||
@@ -257,6 +257,7 @@ message NodeStatusResponse {
|
||||
uint64 memory_free_bytes = 9;
|
||||
double cpu_usage_percent = 10;
|
||||
google.protobuf.Timestamp uptime_since = 11;
|
||||
string agent_version = 12;
|
||||
}
|
||||
|
||||
// --- Purge ---
|
||||
|
||||
Reference in New Issue
Block a user