package cmd
import (
"bytes"
"context"
"crypto/rand"
"encoding/hex"
"fmt"
"io"
"github.com/spf13/cobra"
)
var doctorCmd = &cobra.Command{
Use: "doctor",
Short: "Smoke-test S3 credentials and bucket reachability",
Long: `Validates that cacher can reach the configured S3 bucket using the
resolved credentials. Performs a HEAD on the bucket and then writes,
reads, and deletes a 1-byte test object. Reports the access key id's
length and first two characters as a diagnostic — mirrors the existing
ci_aws_setup shell helper's "key_id_len=… key_id_prefix=…" output —
without leaking the secret.`,
RunE: func(cmd *cobra.Command, _ []string) error { return runDoctor(cmd) },
}
func init() {
addS3Flags(doctorCmd)
rootCmd.AddCommand(doctorCmd)
}
func runDoctor(cmd *cobra.Command) error {
cli, cfg, err := client()
if err != nil {
return err
}
out := cmd.OutOrStdout()
keyID, secret, _ := cfg.Credentials() // already validated by client()
fmt.Fprintf(out, "endpoint=%s region=%s bucket=%s prefix=%s\n", cfg.Endpoint, cfg.Region, cfg.Bucket, cfg.Prefix)
fmt.Fprintf(out, "key_id_len=%d key_id_prefix=%s secret_len=%d\n",
len(keyID), safePrefix(keyID, 2), len(secret))
ctx := context.Background()
if err := cli.PingBucket(ctx); err != nil {
return err
}
fmt.Fprintln(out, "ping bucket: OK")
// Write/read/delete a tiny canary so credential perms are exercised
// for all three operations, not just Head. Key is random so concurrent
// doctor runs don't fight.
r := make([]byte, 8)
_, _ = rand.Read(r)
key := ".cacher-doctor-" + hex.EncodeToString(r)
body := []byte("ok")
if err := cli.Put(ctx, key, bytes.NewReader(body)); err != nil {
return err
}
fmt.Fprintln(out, "put canary: OK")
rc, err := cli.Get(ctx, key)
if err != nil {
return err
}
got, err := io.ReadAll(rc)
rc.Close()
if err != nil {
return err
}
if !bytes.Equal(got, body) {
return fmt.Errorf("canary read mismatch: got %q want %q", got, body)
}
fmt.Fprintln(out, "get canary: OK")
if err := cli.Delete(ctx, key); err != nil {
return err
}
fmt.Fprintln(out, "delete canary: OK")
fmt.Fprintln(out, "doctor: all checks passed")
return nil
}
func safePrefix(s string, n int) string {
if len(s) < n {
return s
}
return s[:n]
}