Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3d2edb7c26 | |||
| bf02935716 | |||
| c4f0d7be8e | |||
| 4d900eafd1 | |||
| 38f9070c24 | |||
| 67d0ab1d9d |
@@ -38,7 +38,7 @@ func main() {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("load config: %w", err)
|
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(nodeCmd())
|
||||||
root.AddCommand(purgeCmd())
|
root.AddCommand(purgeCmd())
|
||||||
root.AddCommand(logsCmd())
|
root.AddCommand(logsCmd())
|
||||||
|
root.AddCommand(editCmd())
|
||||||
|
|
||||||
if err := root.Execute(); err != nil {
|
if err := root.Execute(); err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"text/tabwriter"
|
"text/tabwriter"
|
||||||
|
"time"
|
||||||
|
|
||||||
toml "github.com/pelletier/go-toml/v2"
|
toml "github.com/pelletier/go-toml/v2"
|
||||||
|
|
||||||
|
mcpv1 "git.wntrmute.dev/mc/mcp/gen/mcp/v1"
|
||||||
"git.wntrmute.dev/mc/mcp/internal/config"
|
"git.wntrmute.dev/mc/mcp/internal/config"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
@@ -48,13 +51,35 @@ func runNodeList(_ *cobra.Command, _ []string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
w := tabwriter.NewWriter(os.Stdout, 0, 4, 2, ' ', 0)
|
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 {
|
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()
|
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 {
|
func runNodeAdd(_ *cobra.Command, args []string) error {
|
||||||
cfg, err := config.LoadCLIConfig(cfgPath)
|
cfg, err := config.LoadCLIConfig(cfgPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
let
|
let
|
||||||
system = "x86_64-linux";
|
system = "x86_64-linux";
|
||||||
pkgs = nixpkgs.legacyPackages.${system};
|
pkgs = nixpkgs.legacyPackages.${system};
|
||||||
version = "0.6.0";
|
version = pkgs.lib.removePrefix "v" (self.gitDescribe or self.shortRev or self.dirtyShortRev or "unknown");
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
packages.${system} = {
|
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"`
|
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"`
|
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"`
|
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
|
unknownFields protoimpl.UnknownFields
|
||||||
sizeCache protoimpl.SizeCache
|
sizeCache protoimpl.SizeCache
|
||||||
}
|
}
|
||||||
@@ -2037,6 +2038,13 @@ func (x *NodeStatusResponse) GetUptimeSince() *timestamppb.Timestamp {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (x *NodeStatusResponse) GetAgentVersion() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.AgentVersion
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
type PurgeRequest struct {
|
type PurgeRequest struct {
|
||||||
state protoimpl.MessageState `protogen:"open.v1"`
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
// Service name (empty = all services).
|
// 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" +
|
"\acontent\x18\x01 \x01(\fR\acontent\x12\x12\n" +
|
||||||
"\x04mode\x18\x02 \x01(\rR\x04mode\x12\x14\n" +
|
"\x04mode\x18\x02 \x01(\rR\x04mode\x12\x14\n" +
|
||||||
"\x05error\x18\x03 \x01(\tR\x05error\"\x13\n" +
|
"\x05error\x18\x03 \x01(\tR\x05error\"\x13\n" +
|
||||||
"\x11NodeStatusRequest\"\xd9\x03\n" +
|
"\x11NodeStatusRequest\"\xfe\x03\n" +
|
||||||
"\x12NodeStatusResponse\x12\x1b\n" +
|
"\x12NodeStatusResponse\x12\x1b\n" +
|
||||||
"\tnode_name\x18\x01 \x01(\tR\bnodeName\x12\x18\n" +
|
"\tnode_name\x18\x01 \x01(\tR\bnodeName\x12\x18\n" +
|
||||||
"\aruntime\x18\x02 \x01(\tR\aruntime\x12'\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" +
|
"\x11memory_free_bytes\x18\t \x01(\x04R\x0fmemoryFreeBytes\x12*\n" +
|
||||||
"\x11cpu_usage_percent\x18\n" +
|
"\x11cpu_usage_percent\x18\n" +
|
||||||
" \x01(\x01R\x0fcpuUsagePercent\x12=\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" +
|
"\fPurgeRequest\x12\x18\n" +
|
||||||
"\aservice\x18\x01 \x01(\tR\aservice\x12\x1c\n" +
|
"\aservice\x18\x01 \x01(\tR\aservice\x12\x1c\n" +
|
||||||
"\tcomponent\x18\x02 \x01(\tR\tcomponent\x12\x17\n" +
|
"\tcomponent\x18\x02 \x01(\tR\tcomponent\x12\x17\n" +
|
||||||
|
|||||||
@@ -35,11 +35,12 @@ type Agent struct {
|
|||||||
Proxy *ProxyRouter
|
Proxy *ProxyRouter
|
||||||
Certs *CertProvisioner
|
Certs *CertProvisioner
|
||||||
DNS *DNSRegistrar
|
DNS *DNSRegistrar
|
||||||
|
Version string
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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
|
||||||
// TLS and auth, and blocks until SIGINT/SIGTERM.
|
// 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{
|
logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{
|
||||||
Level: parseLogLevel(cfg.Log.Level),
|
Level: parseLogLevel(cfg.Log.Level),
|
||||||
}))
|
}))
|
||||||
@@ -79,6 +80,7 @@ func Run(cfg *config.AgentConfig) error {
|
|||||||
Proxy: proxy,
|
Proxy: proxy,
|
||||||
Certs: certs,
|
Certs: certs,
|
||||||
DNS: dns,
|
DNS: dns,
|
||||||
|
Version: version,
|
||||||
}
|
}
|
||||||
|
|
||||||
tlsCert, err := tls.LoadX509KeyPair(cfg.Server.TLSCert, cfg.Server.TLSKey)
|
tlsCert, err := tls.LoadX509KeyPair(cfg.Server.TLSCert, cfg.Server.TLSKey)
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ func (a *Agent) NodeStatus(ctx context.Context, _ *mcpv1.NodeStatusRequest) (*mc
|
|||||||
Runtime: a.Config.Agent.ContainerRuntime,
|
Runtime: a.Config.Agent.ContainerRuntime,
|
||||||
ServiceCount: uint32(len(services)), //nolint:gosec // bounded
|
ServiceCount: uint32(len(services)), //nolint:gosec // bounded
|
||||||
ComponentCount: componentCount,
|
ComponentCount: componentCount,
|
||||||
|
AgentVersion: a.Version,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Runtime version.
|
// Runtime version.
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"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
|
// Logs returns an exec.Cmd that streams container logs. For containers
|
||||||
// using the journald log driver, it uses journalctl (podman logs can't
|
// using the journald log driver, it tries journalctl first (podman logs
|
||||||
// read journald outside the originating user session). For k8s-file or
|
// can't read journald outside the originating user session). If journalctl
|
||||||
// other drivers, it uses podman logs directly.
|
// 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 {
|
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.
|
// Check if this container uses the journald log driver.
|
||||||
inspectCmd := exec.CommandContext(ctx, p.command(), "inspect", "--format", "{{.HostConfig.LogConfig.Type}}", containerName) //nolint:gosec
|
inspectCmd := exec.CommandContext(ctx, p.command(), "inspect", "--format", "{{.HostConfig.LogConfig.Type}}", containerName) //nolint:gosec
|
||||||
if out, err := inspectCmd.Output(); err == nil && strings.TrimSpace(string(out)) == "journald" {
|
if out, err := inspectCmd.Output(); err == nil && strings.TrimSpace(string(out)) == "journald" {
|
||||||
|
if p.journalAccessible(ctx, containerName) {
|
||||||
return p.journalLogs(ctx, containerName, tail, follow, since)
|
return p.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"}
|
args := []string{"logs"}
|
||||||
if tail > 0 {
|
if tail > 0 {
|
||||||
args = append(args, "--tail", fmt.Sprintf("%d", tail))
|
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
|
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
|
// Login authenticates to a container registry using the given token as
|
||||||
// the password. This enables non-interactive push with service account
|
// the password. This enables non-interactive push with service account
|
||||||
// tokens (MCR accepts MCIAS JWTs as passwords).
|
// tokens (MCR accepts MCIAS JWTs as passwords).
|
||||||
|
|||||||
@@ -257,6 +257,7 @@ message NodeStatusResponse {
|
|||||||
uint64 memory_free_bytes = 9;
|
uint64 memory_free_bytes = 9;
|
||||||
double cpu_usage_percent = 10;
|
double cpu_usage_percent = 10;
|
||||||
google.protobuf.Timestamp uptime_since = 11;
|
google.protobuf.Timestamp uptime_since = 11;
|
||||||
|
string agent_version = 12;
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Purge ---
|
// --- Purge ---
|
||||||
|
|||||||
Reference in New Issue
Block a user