package cmd import ( "fmt" "io" "net/http" "net/url" "os" "strings" "github.com/spf13/cobra" "gopkg.in/yaml.v3" "git.shee.sh/james/ward/pkg/handler" "git.shee.sh/james/ward/pkg/kubeconfig" ) func newLoginCmd() *cobra.Command { var ( server string username string contextName string noKerberos bool noSetContext bool debugFlag bool ) cmd := &cobra.Command{ Use: "login", Short: "Authenticate to ward and configure kubectl", Long: `Authenticates to the ward server, caches a long-lived credential, and updates ~/.kube/config with the cluster, user (exec plugin), and context. After login, kubectl works silently until the credential expires, at which point you run 'ward login' again. Authentication priority: 1. Kerberos SPNEGO using the active credential cache (from kinit) 2. Password prompt`, RunE: func(cmd *cobra.Command, args []string) error { if server == "" { return fmt.Errorf("--server is required") } server = normalizeServer(server) logf := func(format string, a ...any) { if debugFlag { fmt.Fprintf(os.Stderr, "[ward] "+format+"\n", a...) } } if username == "" { username = os.Getenv("USER") } ec, err := loginFetch(server, username, noKerberos, logf) if err != nil { return err } credWriteCache(server, ec, logf) bootstrap, err := fetchBootstrap(server, username, logf) if err != nil { return fmt.Errorf("fetching bootstrap kubeconfig: %w", err) } if contextName != "" { kubeconfig.RenameContext(bootstrap, contextName) } contextApplied := bootstrap.CurrentContext if err := kubeconfig.Merge(bootstrap, !noSetContext); err != nil { return fmt.Errorf("updating kubeconfig: %w", err) } fmt.Fprintf(os.Stderr, "ward: logged in — context %q configured in %s\n", contextApplied, kubeconfig.FilePath()) return nil }, } cmd.Flags().StringVar(&server, "server", "", "ward server URL (required)") cmd.Flags().StringVar(&username, "username", "", "username (default: $USER)") cmd.Flags().StringVar(&contextName, "context", "", "kubectl context/cluster name (overrides server default)") cmd.Flags().BoolVar(&noKerberos, "no-kerberos", false, "skip Kerberos; use password auth") cmd.Flags().BoolVar(&noSetContext, "no-set-context", false, "do not set as current-context") cmd.Flags().BoolVar(&debugFlag, "debug", os.Getenv("WARD_DEBUG") != "", "verbose debug output to stderr") return cmd } func loginFetch(server, username string, noKerberos bool, logf func(string, ...any)) (*handler.ExecCredential, error) { loginURL := strings.TrimRight(server, "/") + "/credential?login=true" if !noKerberos { body, err := credFetchKerberos(loginURL, logf) if err != nil { logf("Kerberos: %v — falling back to password auth", err) fmt.Fprintf(os.Stderr, "ward: Kerberos failed (%v); using password auth\nhint: run 'kinit' to avoid the password prompt next time\n", err) } else { return credParse(body) } } password, err := promptPassword(username) if err != nil { return nil, err } body, err := credFetchBasic(loginURL, username, password, logf) if err != nil { return nil, err } return credParse(body) } func fetchBootstrap(server, username string, logf func(string, ...any)) (*kubeconfig.KubeConfig, error) { bootstrapURL := strings.TrimRight(server, "/") + "/bootstrap?user=" + url.QueryEscape(username) logf("HTTP: GET %s", bootstrapURL) resp, err := http.Get(bootstrapURL) //nolint:gosec // URL derived from user-supplied server flag if err != nil { return nil, fmt.Errorf("HTTP request: %w", err) } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("reading response: %w", err) } if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("server returned %s", resp.Status) } var cfg kubeconfig.KubeConfig if err := yaml.Unmarshal(body, &cfg); err != nil { return nil, fmt.Errorf("parsing bootstrap kubeconfig: %w", err) } return &cfg, nil }