diff --git a/knecht/cmd/logs.go b/knecht/cmd/logs.go index 9fc9b03..49b4b9d 100644 --- a/knecht/cmd/logs.go +++ b/knecht/cmd/logs.go @@ -4,24 +4,43 @@ import ( "fmt" "os/exec" "runtime" + "strings" "github.com/spf13/cobra" ) var logsCmd = &cobra.Command{ - Use: "logs", - Short: "Open the Dozzle log viewer in the browser", + Use: "logs [container]", + Short: "Open Dozzle in the browser, optionally deep-linking to a container", + Args: cobra.MaximumNArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - cfg, _, _, err := setup() + cfg, client, _, err := setup() if err != nil { return err } - // Derive logs URL from Portainer URL (same base domain) - // e.g. https://portainer.home.jens.pub → https://logs.home.jens.pub - logsURL := deriveLogsURL(cfg.Portainer.URL) - fmt.Printf("Opening %s\n", logsURL) - return openBrowser(logsURL) + logsBase := deriveLogsURL(cfg.Portainer.URL) + + if len(args) == 0 { + fmt.Printf("Opening %s\n", logsBase) + return openBrowser(logsBase) + } + + containerName := args[0] + id, err := client.FindContainer(containerName) + if err != nil { + return err + } + + // Dozzle uses a short ID (first 12 chars) + shortID := id + if len(shortID) > 12 { + shortID = shortID[:12] + } + + url := logsBase + "/container/" + shortID + fmt.Printf("Opening %s\n", url) + return openBrowser(url) }, } @@ -30,24 +49,17 @@ func init() { } func deriveLogsURL(portainerURL string) string { + domain := stripScheme(portainerURL) // Replace "portainer." prefix with "logs." - return fmt.Sprintf("https://logs.%s", domainFromURL(portainerURL)) -} - -func domainFromURL(u string) string { - // Strip scheme and find domain after first dot - u = stripScheme(u) - for i, c := range u { - if c == '.' { - return u[i+1:] - } + if idx := strings.Index(domain, "."); idx != -1 { + return "https://logs." + domain[idx+1:] } - return u + return "https://logs." + domain } func stripScheme(u string) string { for _, prefix := range []string{"https://", "http://"} { - if len(u) > len(prefix) && u[:len(prefix)] == prefix { + if strings.HasPrefix(u, prefix) { return u[len(prefix):] } } diff --git a/knecht/portainer/client.go b/knecht/portainer/client.go index 3a30ee6..0ace4b0 100644 --- a/knecht/portainer/client.go +++ b/knecht/portainer/client.go @@ -38,6 +38,29 @@ type StackFile struct { StackFileContent string `json:"StackFileContent"` } +type Container struct { + ID string `json:"Id"` + Names []string `json:"Names"` +} + +// FindContainer returns the container ID for a given name (e.g. "seerr"). +// Docker container names have a leading slash, so "/seerr" matches "seerr". +func (c *Client) FindContainer(name string) (string, error) { + var containers []Container + if err := c.get(fmt.Sprintf("/api/endpoints/%d/docker/containers/json", c.endpointID), &containers); err != nil { + return "", err + } + want := "/" + name + for _, ct := range containers { + for _, n := range ct.Names { + if n == want { + return ct.ID, nil + } + } + } + return "", fmt.Errorf("container %q not found", name) +} + func New(baseURL, token, endpointName string) (*Client, error) { c := &Client{ baseURL: strings.TrimRight(baseURL, "/"),