feat(core): klien Docker SDK + tia-love doctor (docker/audio/go/stacks) + e2e doctor
CI / test (push) Successful in 10s
CI / test (push) Successful in 10s
This commit is contained in:
1 parent
3f3cc9e8bb
commit
18055979da
9 files changed
+322
-9
No files matched your search
@@ -0,0 +1,66 @@
|
||||
// Package cli — mode CLI pendamping (F-028).
|
||||
// Seluruh logika inti di-share dengan TUI; paket ini hanya presentasi + kode keluar.
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"time"
|
||||
|
||||
"gitea.oxpoe.qzz.io/oxpoe/tia-love/internal/audio"
|
||||
"gitea.oxpoe.qzz.io/oxpoe/tia-love/internal/docker"
|
||||
)
|
||||
|
||||
// Doctor memeriksa kesehatan sistem dan menulis laporan ke w.
|
||||
// Kode keluar: 0 = sehat (warning boleh), 1 = ada kegagalan fatal.
|
||||
//
|
||||
// shortcut: teks masih hardcode Bahasa Indonesia — pindah ke katalog i18n
|
||||
// saat F-023 masuk (Phase 1).
|
||||
func Doctor(w io.Writer) int {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
fail := 0
|
||||
|
||||
// Docker — dependency inti.
|
||||
switch cl, err := docker.New(); {
|
||||
case err != nil:
|
||||
fmt.Fprintf(w, "docker : GAGAL %v\n", err)
|
||||
fail++
|
||||
default:
|
||||
defer cl.Close()
|
||||
if err := cl.Ping(ctx); err != nil {
|
||||
fmt.Fprintf(w, "docker : GAGAL daemon tidak merespons: %v\n", err)
|
||||
fail++
|
||||
} else if info, err := cl.Info(ctx); err != nil {
|
||||
fmt.Fprintf(w, "docker : PERINGATAN %v\n", err)
|
||||
} else {
|
||||
fmt.Fprintf(w, "docker : OK engine %s (API %s) · %d/%d kontainer berjalan · %d image\n",
|
||||
info.ServerVersion, info.APIVersion, info.Running, info.Containers, info.Images)
|
||||
}
|
||||
}
|
||||
|
||||
// Audio — opsional; engine init async, tunggu maks 2 dtk.
|
||||
eng := audio.New(audio.Options{Enabled: true, Volume: 70, MuteOnSSH: true})
|
||||
eng.WaitReady(2 * time.Second)
|
||||
fmt.Fprintf(w, "audio : %s\n", eng.Status())
|
||||
|
||||
fmt.Fprintf(w, "go : %s (%s/%s)\n", runtime.Version(), runtime.GOOS, runtime.GOARCH)
|
||||
|
||||
home, _ := os.UserHomeDir()
|
||||
stacks := filepath.Join(home, ".tia", "stacks")
|
||||
if st, err := os.Stat(stacks); err == nil && st.IsDir() {
|
||||
fmt.Fprintf(w, "stacks : %s\n", stacks)
|
||||
} else {
|
||||
fmt.Fprintf(w, "stacks : %s (belum ada — dibuat saat onboarding)\n", stacks)
|
||||
}
|
||||
|
||||
if fail > 0 {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
// Package docker — lapisan tipis di atas Docker SDK untuk core tia.
|
||||
// Bebas UI: dipakai bersama oleh TUI dan CLI (F-028).
|
||||
package docker
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/docker/docker/client"
|
||||
)
|
||||
|
||||
// Client — pembungkus Docker SDK dengan negosiasi versi API
|
||||
// (mendukung Docker Engine 20.10+, PRD §9).
|
||||
type Client struct {
|
||||
cli *client.Client
|
||||
}
|
||||
|
||||
// New membaca konfigurasi dari env (DOCKER_HOST dll) atau default socket.
|
||||
func New() (*Client, error) {
|
||||
cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("docker: buat client: %w", err)
|
||||
}
|
||||
return &Client{cli: cli}, nil
|
||||
}
|
||||
|
||||
func (c *Client) Close() error { return c.cli.Close() }
|
||||
|
||||
// Info — ringkasan daemon untuk dashboard, doctor, dan status bar.
|
||||
type Info struct {
|
||||
ServerVersion string
|
||||
APIVersion string
|
||||
Containers int
|
||||
Running int
|
||||
Images int
|
||||
MemoryTotal int64
|
||||
CPUs int
|
||||
}
|
||||
|
||||
func (c *Client) Info(ctx context.Context) (Info, error) {
|
||||
i, err := c.cli.Info(ctx)
|
||||
if err != nil {
|
||||
return Info{}, fmt.Errorf("docker: info: %w", err)
|
||||
}
|
||||
return Info{
|
||||
ServerVersion: i.ServerVersion,
|
||||
APIVersion: c.cli.ClientVersion(), // hasil negosiasi API
|
||||
Containers: i.Containers,
|
||||
Running: i.ContainersRunning,
|
||||
Images: i.Images,
|
||||
MemoryTotal: i.MemTotal,
|
||||
CPUs: i.NCPU,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Ping — cek daemon hidup (dipakai doctor).
|
||||
func (c *Client) Ping(ctx context.Context) error {
|
||||
if _, err := c.cli.Ping(ctx); err != nil {
|
||||
return fmt.Errorf("docker: ping: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package docker
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// TestIntegrationInfo dijalankan hanya bila docker socket tersedia —
|
||||
// di CI runner tanpa socket, test di-skip otomatis.
|
||||
func TestIntegrationInfo(t *testing.T) {
|
||||
if _, err := os.Stat("/var/run/docker.sock"); err != nil {
|
||||
t.Skip("docker socket tidak ada — skip tes integrasi")
|
||||
}
|
||||
c, err := New()
|
||||
if err != nil {
|
||||
t.Fatalf("New: %v", err)
|
||||
}
|
||||
defer c.Close()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
if err := c.Ping(ctx); err != nil {
|
||||
t.Fatalf("Ping: %v", err)
|
||||
}
|
||||
info, err := c.Info(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("Info: %v", err)
|
||||
}
|
||||
if info.ServerVersion == "" {
|
||||
t.Fatal("ServerVersion kosong")
|
||||
}
|
||||
t.Logf("engine %s (API %s) · %d/%d kontainer berjalan · %d image · %d CPU · %d MB RAM",
|
||||
info.ServerVersion, info.APIVersion, info.Running, info.Containers,
|
||||
info.Images, info.CPUs, info.MemoryTotal/1024/1024)
|
||||
}
|
||||
Reference in new issue
Block a user