mirror of
https://tangled.org/evan.jarrett.net/at-container-registry
synced 2026-09-21 01:34:16 +00:00
196 lines
5.1 KiB
Go
196 lines
5.1 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"net"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strings"
|
|
|
|
"github.com/charmbracelet/lipgloss"
|
|
)
|
|
|
|
// Status message styles (matching gh CLI conventions)
|
|
var (
|
|
successStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("2")) // green
|
|
warningStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("3")) // yellow
|
|
infoStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("6")) // cyan
|
|
boldStyle = lipgloss.NewStyle().Bold(true)
|
|
)
|
|
|
|
// logSuccess prints a green ✓ prefixed message to stderr
|
|
func logSuccess(format string, a ...any) {
|
|
fmt.Fprintf(os.Stderr, "%s %s\n", successStyle.Render("✓"), fmt.Sprintf(format, a...))
|
|
}
|
|
|
|
// logWarning prints a yellow ! prefixed message to stderr
|
|
func logWarning(format string, a ...any) {
|
|
fmt.Fprintf(os.Stderr, "%s %s\n", warningStyle.Render("!"), fmt.Sprintf(format, a...))
|
|
}
|
|
|
|
// logInfo prints a cyan - prefixed message to stderr
|
|
func logInfo(format string, a ...any) {
|
|
fmt.Fprintf(os.Stderr, "%s %s\n", infoStyle.Render("-"), fmt.Sprintf(format, a...))
|
|
}
|
|
|
|
// logInfof prints a cyan - prefixed message to stderr without a trailing newline
|
|
func logInfof(format string, a ...any) {
|
|
fmt.Fprintf(os.Stderr, "%s %s", infoStyle.Render("-"), fmt.Sprintf(format, a...))
|
|
}
|
|
|
|
// bold renders text in bold
|
|
func bold(s string) string {
|
|
return boldStyle.Render(s)
|
|
}
|
|
|
|
// DockerDaemonConfig represents Docker's daemon.json configuration
|
|
type DockerDaemonConfig struct {
|
|
InsecureRegistries []string `json:"insecure-registries"`
|
|
}
|
|
|
|
// openBrowser opens the specified URL in the default browser
|
|
func openBrowser(url string) error {
|
|
var cmd *exec.Cmd
|
|
|
|
switch runtime.GOOS {
|
|
case "linux":
|
|
cmd = exec.Command("xdg-open", url)
|
|
case "darwin":
|
|
cmd = exec.Command("open", url)
|
|
case "windows":
|
|
cmd = exec.Command("rundll32", "url.dll,FileProtocolHandler", url)
|
|
default:
|
|
return fmt.Errorf("unsupported platform")
|
|
}
|
|
|
|
return cmd.Start()
|
|
}
|
|
|
|
// buildAppViewURL constructs the AppView URL with the appropriate protocol
|
|
func buildAppViewURL(serverURL string) string {
|
|
// If serverURL already has a scheme, use it as-is
|
|
if strings.HasPrefix(serverURL, "http://") || strings.HasPrefix(serverURL, "https://") {
|
|
return serverURL
|
|
}
|
|
|
|
// Determine protocol based on Docker configuration and heuristics
|
|
if isInsecureRegistry(serverURL) {
|
|
return "http://" + serverURL
|
|
}
|
|
|
|
// Default to HTTPS (mirrors Docker's default behavior)
|
|
return "https://" + serverURL
|
|
}
|
|
|
|
// isInsecureRegistry checks if a registry should use HTTP instead of HTTPS
|
|
func isInsecureRegistry(serverURL string) bool {
|
|
// Check Docker's insecure-registries configuration
|
|
insecureRegistries := getDockerInsecureRegistries()
|
|
for _, reg := range insecureRegistries {
|
|
if reg == serverURL || reg == stripPort(serverURL) {
|
|
return true
|
|
}
|
|
}
|
|
|
|
// Fallback heuristics: localhost and private IPs
|
|
host := stripPort(serverURL)
|
|
|
|
if host == "localhost" || host == "127.0.0.1" || host == "::1" {
|
|
return true
|
|
}
|
|
|
|
if ip := net.ParseIP(host); ip != nil {
|
|
if ip.IsLoopback() || ip.IsPrivate() {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// getDockerInsecureRegistries reads Docker's insecure-registries configuration
|
|
func getDockerInsecureRegistries() []string {
|
|
var paths []string
|
|
|
|
switch runtime.GOOS {
|
|
case "windows":
|
|
programData := os.Getenv("ProgramData")
|
|
if programData != "" {
|
|
paths = append(paths, filepath.Join(programData, "docker", "config", "daemon.json"))
|
|
}
|
|
default:
|
|
paths = append(paths, "/etc/docker/daemon.json")
|
|
if homeDir, err := os.UserHomeDir(); err == nil {
|
|
paths = append(paths, filepath.Join(homeDir, ".docker", "daemon.json"))
|
|
}
|
|
}
|
|
|
|
for _, path := range paths {
|
|
if config := readDockerDaemonConfig(path); config != nil && len(config.InsecureRegistries) > 0 {
|
|
return config.InsecureRegistries
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// readDockerDaemonConfig reads and parses a Docker daemon.json file
|
|
func readDockerDaemonConfig(path string) *DockerDaemonConfig {
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
|
|
var config DockerDaemonConfig
|
|
if err := json.Unmarshal(data, &config); err != nil {
|
|
return nil
|
|
}
|
|
|
|
return &config
|
|
}
|
|
|
|
// stripPort removes the port from a host:port string
|
|
func stripPort(hostPort string) string {
|
|
if colonIdx := strings.LastIndex(hostPort, ":"); colonIdx != -1 {
|
|
if strings.Count(hostPort, ":") > 1 {
|
|
return hostPort
|
|
}
|
|
return hostPort[:colonIdx]
|
|
}
|
|
return hostPort
|
|
}
|
|
|
|
// isTerminal checks if the file is a terminal
|
|
func isTerminal(f *os.File) bool {
|
|
stat, err := f.Stat()
|
|
if err != nil {
|
|
return false
|
|
}
|
|
return (stat.Mode() & os.ModeCharDevice) != 0
|
|
}
|
|
|
|
// getConfigDir returns the path to the .atcr config directory, creating it if needed
|
|
func getConfigDir() string {
|
|
homeDir, err := os.UserHomeDir()
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "Error getting home directory: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
atcrDir := filepath.Join(homeDir, ".atcr")
|
|
if err := os.MkdirAll(atcrDir, 0700); err != nil {
|
|
fmt.Fprintf(os.Stderr, "Error creating .atcr directory: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
return atcrDir
|
|
}
|
|
|
|
// getConfigPath returns the path to the device configuration file
|
|
func getConfigPath() string {
|
|
return filepath.Join(getConfigDir(), "device.json")
|
|
}
|