CLI + live dashboard wrapping aws/docker/git/awsmfa for local dev: AWS MFA auth status + expiry, dev compose stack control, container drift vs the dev repo, git repo status, prod-data and DB-access tools. Config-driven (~/.config/mydev/config.yaml). Dashboard runs commands via a command palette with captured/interactive modes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
84 lines
2.3 KiB
Go
84 lines
2.3 KiB
Go
package checks
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"os/exec"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// AuthStatus holds the result of an AWS auth check via the awsmfa tool.
|
|
type AuthStatus struct {
|
|
Authed bool
|
|
Account string
|
|
Expires time.Time // zero if unknown; session expiry from awsmfa
|
|
Detail string // human summary line
|
|
Err string // populated when awsmfa itself failed to run
|
|
}
|
|
|
|
// notAuthedMarker is the exact string awsmfa prints when the session is gone.
|
|
const notAuthedMarker = "Not authenticated."
|
|
|
|
// AWSAuth runs `<awsmfa> status` and decides auth state from its output.
|
|
// This mirrors the shell `myd auth` check — awsmfa is the real MFA session,
|
|
// not `aws sts`. awsmfaPath is the full path to the awsmfa command.
|
|
//
|
|
// Authed output looks like:
|
|
//
|
|
// Success.
|
|
// { "UserId": ..., "Account": "0482...", "Arn": ... }
|
|
// Expire: 2026-06-12T19:25:19+00:00
|
|
func AWSAuth(ctx context.Context, awsmfaPath string) AuthStatus {
|
|
ctx, cancel := context.WithTimeout(ctx, 8*time.Second)
|
|
defer cancel()
|
|
|
|
out, err := exec.CommandContext(ctx, awsmfaPath, "status").CombinedOutput()
|
|
text := strings.TrimSpace(string(out))
|
|
if err != nil && text == "" {
|
|
return AuthStatus{Authed: false, Err: err.Error()}
|
|
}
|
|
if strings.Contains(text, notAuthedMarker) {
|
|
return AuthStatus{Authed: false, Detail: notAuthedMarker}
|
|
}
|
|
|
|
s := AuthStatus{Authed: true}
|
|
|
|
// Account: decode the JSON identity block. Use a Decoder so the trailing
|
|
// "Expire:" line after the closing brace doesn't fail parsing.
|
|
if i := strings.IndexByte(text, '{'); i >= 0 {
|
|
var id struct {
|
|
Account string `json:"Account"`
|
|
}
|
|
if json.NewDecoder(strings.NewReader(text[i:])).Decode(&id) == nil {
|
|
s.Account = id.Account
|
|
}
|
|
}
|
|
|
|
// Expiry: the "Expire: <RFC3339>" line.
|
|
for _, line := range strings.Split(text, "\n") {
|
|
if rest, ok := strings.CutPrefix(strings.TrimSpace(line), "Expire:"); ok {
|
|
if t, perr := time.Parse(time.RFC3339, strings.TrimSpace(rest)); perr == nil {
|
|
s.Expires = t
|
|
}
|
|
}
|
|
}
|
|
|
|
s.Detail = summarize(s.Account, s.Expires)
|
|
return s
|
|
}
|
|
|
|
func summarize(account string, expires time.Time) string {
|
|
var parts []string
|
|
if account != "" {
|
|
parts = append(parts, "account "+account)
|
|
}
|
|
if !expires.IsZero() {
|
|
parts = append(parts, "expires "+expires.Local().Format("15:04"))
|
|
}
|
|
if len(parts) == 0 {
|
|
return "authed"
|
|
}
|
|
return strings.Join(parts, " · ")
|
|
}
|