Upgrade to dappco.re/go/core v0.8.0-alpha.1
Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
parent
1cecf00148
commit
67dae9cc94
16 changed files with 269 additions and 228 deletions
38
client.go
38
client.go
|
|
@ -1,8 +1,6 @@
|
|||
package infra
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"math/rand/v2"
|
||||
|
|
@ -11,7 +9,7 @@ import (
|
|||
"sync"
|
||||
"time"
|
||||
|
||||
coreerr "forge.lthn.ai/core/go-log"
|
||||
core "dappco.re/go/core"
|
||||
)
|
||||
|
||||
// RetryConfig controls exponential backoff retry behaviour.
|
||||
|
|
@ -37,11 +35,11 @@ func DefaultRetryConfig() RetryConfig {
|
|||
// and configurable authentication. Provider-specific clients embed or
|
||||
// delegate to this struct.
|
||||
type APIClient struct {
|
||||
client *http.Client
|
||||
retry RetryConfig
|
||||
authFn func(req *http.Request)
|
||||
prefix string // error prefix, e.g. "hcloud API"
|
||||
mu sync.Mutex
|
||||
client *http.Client
|
||||
retry RetryConfig
|
||||
authFn func(req *http.Request)
|
||||
prefix string // error prefix, e.g. "hcloud API"
|
||||
mu sync.Mutex
|
||||
blockedUntil time.Time // rate-limit window
|
||||
}
|
||||
|
||||
|
|
@ -107,7 +105,7 @@ func (a *APIClient) Do(req *http.Request, result any) error {
|
|||
|
||||
resp, err := a.client.Do(req)
|
||||
if err != nil {
|
||||
lastErr = coreerr.E(a.prefix, "request failed", err)
|
||||
lastErr = core.E(a.prefix, "request failed", err)
|
||||
if attempt < attempts-1 {
|
||||
a.backoff(attempt, req)
|
||||
}
|
||||
|
|
@ -117,7 +115,7 @@ func (a *APIClient) Do(req *http.Request, result any) error {
|
|||
data, err := io.ReadAll(resp.Body)
|
||||
_ = resp.Body.Close()
|
||||
if err != nil {
|
||||
lastErr = coreerr.E("client.Do", "read response", err)
|
||||
lastErr = core.E("client.Do", "read response", err)
|
||||
if attempt < attempts-1 {
|
||||
a.backoff(attempt, req)
|
||||
}
|
||||
|
|
@ -131,7 +129,7 @@ func (a *APIClient) Do(req *http.Request, result any) error {
|
|||
a.blockedUntil = time.Now().Add(retryAfter)
|
||||
a.mu.Unlock()
|
||||
|
||||
lastErr = coreerr.E(a.prefix, fmt.Sprintf("rate limited: HTTP %d", resp.StatusCode), nil)
|
||||
lastErr = core.E(a.prefix, core.Sprintf("rate limited: HTTP %d", resp.StatusCode), nil)
|
||||
if attempt < attempts-1 {
|
||||
select {
|
||||
case <-req.Context().Done():
|
||||
|
|
@ -144,7 +142,7 @@ func (a *APIClient) Do(req *http.Request, result any) error {
|
|||
|
||||
// Server errors are retryable.
|
||||
if resp.StatusCode >= 500 {
|
||||
lastErr = coreerr.E(a.prefix, fmt.Sprintf("HTTP %d: %s", resp.StatusCode, truncateBody(data, maxErrBodyLen)), nil)
|
||||
lastErr = core.E(a.prefix, core.Sprintf("HTTP %d: %s", resp.StatusCode, truncateBody(data, maxErrBodyLen)), nil)
|
||||
if attempt < attempts-1 {
|
||||
a.backoff(attempt, req)
|
||||
}
|
||||
|
|
@ -153,13 +151,13 @@ func (a *APIClient) Do(req *http.Request, result any) error {
|
|||
|
||||
// Client errors (4xx, except 429 handled above) are not retried.
|
||||
if resp.StatusCode >= 400 {
|
||||
return coreerr.E(a.prefix, fmt.Sprintf("HTTP %d: %s", resp.StatusCode, truncateBody(data, maxErrBodyLen)), nil)
|
||||
return core.E(a.prefix, core.Sprintf("HTTP %d: %s", resp.StatusCode, truncateBody(data, maxErrBodyLen)), nil)
|
||||
}
|
||||
|
||||
// Success — decode if requested.
|
||||
if result != nil {
|
||||
if err := json.Unmarshal(data, result); err != nil {
|
||||
return coreerr.E("client.Do", "decode response", err)
|
||||
if r := core.JSONUnmarshal(data, result); !r.OK {
|
||||
return core.E("client.Do", "decode response", coreResultErr(r, "client.Do"))
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
|
@ -193,7 +191,7 @@ func (a *APIClient) DoRaw(req *http.Request) ([]byte, error) {
|
|||
|
||||
resp, err := a.client.Do(req)
|
||||
if err != nil {
|
||||
lastErr = coreerr.E(a.prefix, "request failed", err)
|
||||
lastErr = core.E(a.prefix, "request failed", err)
|
||||
if attempt < attempts-1 {
|
||||
a.backoff(attempt, req)
|
||||
}
|
||||
|
|
@ -203,7 +201,7 @@ func (a *APIClient) DoRaw(req *http.Request) ([]byte, error) {
|
|||
data, err := io.ReadAll(resp.Body)
|
||||
_ = resp.Body.Close()
|
||||
if err != nil {
|
||||
lastErr = coreerr.E("client.DoRaw", "read response", err)
|
||||
lastErr = core.E("client.DoRaw", "read response", err)
|
||||
if attempt < attempts-1 {
|
||||
a.backoff(attempt, req)
|
||||
}
|
||||
|
|
@ -216,7 +214,7 @@ func (a *APIClient) DoRaw(req *http.Request) ([]byte, error) {
|
|||
a.blockedUntil = time.Now().Add(retryAfter)
|
||||
a.mu.Unlock()
|
||||
|
||||
lastErr = coreerr.E(a.prefix, fmt.Sprintf("rate limited: HTTP %d", resp.StatusCode), nil)
|
||||
lastErr = core.E(a.prefix, core.Sprintf("rate limited: HTTP %d", resp.StatusCode), nil)
|
||||
if attempt < attempts-1 {
|
||||
select {
|
||||
case <-req.Context().Done():
|
||||
|
|
@ -228,7 +226,7 @@ func (a *APIClient) DoRaw(req *http.Request) ([]byte, error) {
|
|||
}
|
||||
|
||||
if resp.StatusCode >= 500 {
|
||||
lastErr = coreerr.E(a.prefix, fmt.Sprintf("HTTP %d: %s", resp.StatusCode, truncateBody(data, maxErrBodyLen)), nil)
|
||||
lastErr = core.E(a.prefix, core.Sprintf("HTTP %d: %s", resp.StatusCode, truncateBody(data, maxErrBodyLen)), nil)
|
||||
if attempt < attempts-1 {
|
||||
a.backoff(attempt, req)
|
||||
}
|
||||
|
|
@ -236,7 +234,7 @@ func (a *APIClient) DoRaw(req *http.Request) ([]byte, error) {
|
|||
}
|
||||
|
||||
if resp.StatusCode >= 400 {
|
||||
return nil, coreerr.E(a.prefix, fmt.Sprintf("HTTP %d: %s", resp.StatusCode, truncateBody(data, maxErrBodyLen)), nil)
|
||||
return nil, core.E(a.prefix, core.Sprintf("HTTP %d: %s", resp.StatusCode, truncateBody(data, maxErrBodyLen)), nil)
|
||||
}
|
||||
|
||||
return data, nil
|
||||
|
|
|
|||
33
cloudns.go
33
cloudns.go
|
|
@ -2,12 +2,11 @@ package infra
|
|||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
|
||||
coreerr "forge.lthn.ai/core/go-log"
|
||||
core "dappco.re/go/core"
|
||||
)
|
||||
|
||||
const cloudnsBaseURL = "https://api.cloudns.net"
|
||||
|
|
@ -63,7 +62,7 @@ func (c *CloudNSClient) ListZones(ctx context.Context) ([]CloudNSZone, error) {
|
|||
}
|
||||
|
||||
var zones []CloudNSZone
|
||||
if err := json.Unmarshal(data, &zones); err != nil {
|
||||
if r := core.JSONUnmarshal(data, &zones); !r.OK {
|
||||
// CloudNS returns an empty object {} for no results instead of []
|
||||
return nil, nil
|
||||
}
|
||||
|
|
@ -81,8 +80,8 @@ func (c *CloudNSClient) ListRecords(ctx context.Context, domain string) (map[str
|
|||
}
|
||||
|
||||
var records map[string]CloudNSRecord
|
||||
if err := json.Unmarshal(data, &records); err != nil {
|
||||
return nil, coreerr.E("CloudNSClient.ListRecords", "parse records", err)
|
||||
if r := core.JSONUnmarshal(data, &records); !r.OK {
|
||||
return nil, core.E("CloudNSClient.ListRecords", "parse records", coreResultErr(r, "CloudNSClient.ListRecords"))
|
||||
}
|
||||
return records, nil
|
||||
}
|
||||
|
|
@ -108,12 +107,12 @@ func (c *CloudNSClient) CreateRecord(ctx context.Context, domain, host, recordTy
|
|||
ID int `json:"id"`
|
||||
} `json:"data"`
|
||||
}
|
||||
if err := json.Unmarshal(data, &result); err != nil {
|
||||
return "", coreerr.E("CloudNSClient.CreateRecord", "parse response", err)
|
||||
if r := core.JSONUnmarshal(data, &result); !r.OK {
|
||||
return "", core.E("CloudNSClient.CreateRecord", "parse response", coreResultErr(r, "CloudNSClient.CreateRecord"))
|
||||
}
|
||||
|
||||
if result.Status != "Success" {
|
||||
return "", coreerr.E("CloudNSClient.CreateRecord", result.StatusDescription, nil)
|
||||
return "", core.E("CloudNSClient.CreateRecord", result.StatusDescription, nil)
|
||||
}
|
||||
|
||||
return strconv.Itoa(result.Data.ID), nil
|
||||
|
|
@ -138,12 +137,12 @@ func (c *CloudNSClient) UpdateRecord(ctx context.Context, domain, recordID, host
|
|||
Status string `json:"status"`
|
||||
StatusDescription string `json:"statusDescription"`
|
||||
}
|
||||
if err := json.Unmarshal(data, &result); err != nil {
|
||||
return coreerr.E("CloudNSClient.UpdateRecord", "parse response", err)
|
||||
if r := core.JSONUnmarshal(data, &result); !r.OK {
|
||||
return core.E("CloudNSClient.UpdateRecord", "parse response", coreResultErr(r, "CloudNSClient.UpdateRecord"))
|
||||
}
|
||||
|
||||
if result.Status != "Success" {
|
||||
return coreerr.E("CloudNSClient.UpdateRecord", result.StatusDescription, nil)
|
||||
return core.E("CloudNSClient.UpdateRecord", result.StatusDescription, nil)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
|
@ -164,12 +163,12 @@ func (c *CloudNSClient) DeleteRecord(ctx context.Context, domain, recordID strin
|
|||
Status string `json:"status"`
|
||||
StatusDescription string `json:"statusDescription"`
|
||||
}
|
||||
if err := json.Unmarshal(data, &result); err != nil {
|
||||
return coreerr.E("CloudNSClient.DeleteRecord", "parse response", err)
|
||||
if r := core.JSONUnmarshal(data, &result); !r.OK {
|
||||
return core.E("CloudNSClient.DeleteRecord", "parse response", coreResultErr(r, "CloudNSClient.DeleteRecord"))
|
||||
}
|
||||
|
||||
if result.Status != "Success" {
|
||||
return coreerr.E("CloudNSClient.DeleteRecord", result.StatusDescription, nil)
|
||||
return core.E("CloudNSClient.DeleteRecord", result.StatusDescription, nil)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
|
@ -180,7 +179,7 @@ func (c *CloudNSClient) DeleteRecord(ctx context.Context, domain, recordID strin
|
|||
func (c *CloudNSClient) EnsureRecord(ctx context.Context, domain, host, recordType, value string, ttl int) (bool, error) {
|
||||
records, err := c.ListRecords(ctx, domain)
|
||||
if err != nil {
|
||||
return false, coreerr.E("CloudNSClient.EnsureRecord", "list records", err)
|
||||
return false, core.E("CloudNSClient.EnsureRecord", "list records", err)
|
||||
}
|
||||
|
||||
// Check if record already exists
|
||||
|
|
@ -191,7 +190,7 @@ func (c *CloudNSClient) EnsureRecord(ctx context.Context, domain, host, recordTy
|
|||
}
|
||||
// Update existing record
|
||||
if err := c.UpdateRecord(ctx, domain, id, host, recordType, value, ttl); err != nil {
|
||||
return false, coreerr.E("CloudNSClient.EnsureRecord", "update record", err)
|
||||
return false, core.E("CloudNSClient.EnsureRecord", "update record", err)
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
|
@ -199,7 +198,7 @@ func (c *CloudNSClient) EnsureRecord(ctx context.Context, domain, host, recordTy
|
|||
|
||||
// Create new record
|
||||
if _, err := c.CreateRecord(ctx, domain, host, recordType, value, ttl); err != nil {
|
||||
return false, coreerr.E("CloudNSClient.EnsureRecord", "create record", err)
|
||||
return false, core.E("CloudNSClient.EnsureRecord", "create record", err)
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,11 +2,11 @@ package infra
|
|||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
core "dappco.re/go/core"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
|
@ -114,9 +114,7 @@ func TestCloudNSZone_JSON_Good(t *testing.T) {
|
|||
]`
|
||||
|
||||
var zones []CloudNSZone
|
||||
err := json.Unmarshal([]byte(data), &zones)
|
||||
|
||||
require.NoError(t, err)
|
||||
requireCloudNSJSON(t, data, &zones)
|
||||
require.Len(t, zones, 2)
|
||||
assert.Equal(t, "example.com", zones[0].Name)
|
||||
assert.Equal(t, "master", zones[0].Type)
|
||||
|
|
@ -128,10 +126,10 @@ func TestCloudNSZone_JSON_Good_EmptyResponse(t *testing.T) {
|
|||
data := `{}`
|
||||
|
||||
var zones []CloudNSZone
|
||||
err := json.Unmarshal([]byte(data), &zones)
|
||||
r := core.JSONUnmarshal([]byte(data), &zones)
|
||||
|
||||
// Should fail to parse as slice — this is the edge case ListZones handles
|
||||
assert.Error(t, err)
|
||||
assert.False(t, r.OK)
|
||||
}
|
||||
|
||||
// --- Record JSON parsing ---
|
||||
|
|
@ -158,9 +156,7 @@ func TestCloudNSRecord_JSON_Good(t *testing.T) {
|
|||
}`
|
||||
|
||||
var records map[string]CloudNSRecord
|
||||
err := json.Unmarshal([]byte(data), &records)
|
||||
|
||||
require.NoError(t, err)
|
||||
requireCloudNSJSON(t, data, &records)
|
||||
require.Len(t, records, 2)
|
||||
|
||||
aRecord := records["12345"]
|
||||
|
|
@ -190,9 +186,7 @@ func TestCloudNSRecord_JSON_Good_TXTRecord(t *testing.T) {
|
|||
}`
|
||||
|
||||
var records map[string]CloudNSRecord
|
||||
err := json.Unmarshal([]byte(data), &records)
|
||||
|
||||
require.NoError(t, err)
|
||||
requireCloudNSJSON(t, data, &records)
|
||||
require.Len(t, records, 1)
|
||||
|
||||
txt := records["99"]
|
||||
|
|
@ -215,8 +209,7 @@ func TestCloudNSClient_CreateRecord_Good_ResponseParsing(t *testing.T) {
|
|||
} `json:"data"`
|
||||
}
|
||||
|
||||
err := json.Unmarshal([]byte(data), &result)
|
||||
require.NoError(t, err)
|
||||
requireCloudNSJSON(t, data, &result)
|
||||
assert.Equal(t, "Success", result.Status)
|
||||
assert.Equal(t, 54321, result.Data.ID)
|
||||
}
|
||||
|
|
@ -229,8 +222,7 @@ func TestCloudNSClient_CreateRecord_Bad_FailedStatus(t *testing.T) {
|
|||
StatusDescription string `json:"statusDescription"`
|
||||
}
|
||||
|
||||
err := json.Unmarshal([]byte(data), &result)
|
||||
require.NoError(t, err)
|
||||
requireCloudNSJSON(t, data, &result)
|
||||
assert.Equal(t, "Failed", result.Status)
|
||||
assert.Equal(t, "Record already exists.", result.StatusDescription)
|
||||
}
|
||||
|
|
@ -245,8 +237,7 @@ func TestCloudNSClient_UpdateDelete_Good_ResponseParsing(t *testing.T) {
|
|||
StatusDescription string `json:"statusDescription"`
|
||||
}
|
||||
|
||||
err := json.Unmarshal([]byte(data), &result)
|
||||
require.NoError(t, err)
|
||||
requireCloudNSJSON(t, data, &result)
|
||||
assert.Equal(t, "Success", result.Status)
|
||||
}
|
||||
|
||||
|
|
@ -511,12 +502,17 @@ func TestCloudNSRecord_JSON_Good_EmptyMap(t *testing.T) {
|
|||
data := `{}`
|
||||
|
||||
var records map[string]CloudNSRecord
|
||||
err := json.Unmarshal([]byte(data), &records)
|
||||
|
||||
require.NoError(t, err)
|
||||
requireCloudNSJSON(t, data, &records)
|
||||
assert.Empty(t, records)
|
||||
}
|
||||
|
||||
func requireCloudNSJSON(t *testing.T, data string, target any) {
|
||||
t.Helper()
|
||||
|
||||
r := core.JSONUnmarshal([]byte(data), target)
|
||||
require.True(t, r.OK)
|
||||
}
|
||||
|
||||
// --- UpdateRecord round-trip ---
|
||||
|
||||
func TestCloudNSClient_UpdateRecord_Good_ViaDoRaw(t *testing.T) {
|
||||
|
|
|
|||
|
|
@ -11,17 +11,14 @@ package monitor
|
|||
|
||||
import (
|
||||
"cmp"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"maps"
|
||||
"os/exec"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
core "dappco.re/go/core"
|
||||
"forge.lthn.ai/core/cli/pkg/cli"
|
||||
"forge.lthn.ai/core/go-i18n"
|
||||
"forge.lthn.ai/core/go-io"
|
||||
"forge.lthn.ai/core/go-log"
|
||||
"forge.lthn.ai/core/go-scm/repos"
|
||||
)
|
||||
|
||||
|
|
@ -110,7 +107,7 @@ type SecretScanningAlert struct {
|
|||
func runMonitor() error {
|
||||
// Check gh is available
|
||||
if _, err := exec.LookPath("gh"); err != nil {
|
||||
return log.E("monitor", i18n.T("error.gh_not_found"), err)
|
||||
return core.E("monitor", i18n.T("error.gh_not_found"), err)
|
||||
}
|
||||
|
||||
// Determine repos to scan
|
||||
|
|
@ -120,7 +117,7 @@ func runMonitor() error {
|
|||
}
|
||||
|
||||
if len(repoList) == 0 {
|
||||
return log.E("monitor", i18n.T("cmd.monitor.error.no_repos"), nil)
|
||||
return core.E("monitor", i18n.T("cmd.monitor.error.no_repos"), nil)
|
||||
}
|
||||
|
||||
// Collect all findings and errors
|
||||
|
|
@ -166,7 +163,7 @@ func runMonitor() error {
|
|||
func resolveRepos() ([]string, error) {
|
||||
if monitorRepo != "" {
|
||||
// Specific repo - if fully qualified (org/repo), use as-is
|
||||
if strings.Contains(monitorRepo, "/") {
|
||||
if core.Contains(monitorRepo, "/") {
|
||||
return []string{monitorRepo}, nil
|
||||
}
|
||||
// Otherwise, try to detect org from git remote, fallback to host-uk
|
||||
|
|
@ -182,12 +179,12 @@ func resolveRepos() ([]string, error) {
|
|||
// All repos from registry
|
||||
registry, err := repos.FindRegistry(io.Local)
|
||||
if err != nil {
|
||||
return nil, log.E("monitor", "failed to find registry", err)
|
||||
return nil, core.E("monitor", "failed to find registry", err)
|
||||
}
|
||||
|
||||
loaded, err := repos.LoadRegistry(io.Local, registry)
|
||||
if err != nil {
|
||||
return nil, log.E("monitor", "failed to load registry", err)
|
||||
return nil, core.E("monitor", "failed to load registry", err)
|
||||
}
|
||||
|
||||
var repoList []string
|
||||
|
|
@ -215,21 +212,21 @@ func fetchRepoFindings(repoFullName string) ([]Finding, []string) {
|
|||
// Fetch code scanning alerts
|
||||
codeFindings, err := fetchCodeScanningAlerts(repoFullName)
|
||||
if err != nil {
|
||||
errs = append(errs, fmt.Sprintf("%s: code-scanning: %s", repoName, err))
|
||||
errs = append(errs, core.Sprintf("%s: code-scanning: %s", repoName, err))
|
||||
}
|
||||
findings = append(findings, codeFindings...)
|
||||
|
||||
// Fetch Dependabot alerts
|
||||
depFindings, err := fetchDependabotAlerts(repoFullName)
|
||||
if err != nil {
|
||||
errs = append(errs, fmt.Sprintf("%s: dependabot: %s", repoName, err))
|
||||
errs = append(errs, core.Sprintf("%s: dependabot: %s", repoName, err))
|
||||
}
|
||||
findings = append(findings, depFindings...)
|
||||
|
||||
// Fetch secret scanning alerts
|
||||
secretFindings, err := fetchSecretScanningAlerts(repoFullName)
|
||||
if err != nil {
|
||||
errs = append(errs, fmt.Sprintf("%s: secret-scanning: %s", repoName, err))
|
||||
errs = append(errs, core.Sprintf("%s: secret-scanning: %s", repoName, err))
|
||||
}
|
||||
findings = append(findings, secretFindings...)
|
||||
|
||||
|
|
@ -240,7 +237,7 @@ func fetchRepoFindings(repoFullName string) ([]Finding, []string) {
|
|||
func fetchCodeScanningAlerts(repoFullName string) ([]Finding, error) {
|
||||
args := []string{
|
||||
"api",
|
||||
fmt.Sprintf("repos/%s/code-scanning/alerts", repoFullName),
|
||||
core.Sprintf("repos/%s/code-scanning/alerts", repoFullName),
|
||||
}
|
||||
|
||||
cmd := exec.Command("gh", args...)
|
||||
|
|
@ -250,18 +247,18 @@ func fetchCodeScanningAlerts(repoFullName string) ([]Finding, error) {
|
|||
if exitErr, ok := err.(*exec.ExitError); ok {
|
||||
stderr := string(exitErr.Stderr)
|
||||
// These are expected conditions, not errors
|
||||
if strings.Contains(stderr, "Advanced Security must be enabled") ||
|
||||
strings.Contains(stderr, "no analysis found") ||
|
||||
strings.Contains(stderr, "Not Found") {
|
||||
if core.Contains(stderr, "Advanced Security must be enabled") ||
|
||||
core.Contains(stderr, "no analysis found") ||
|
||||
core.Contains(stderr, "Not Found") {
|
||||
return nil, nil
|
||||
}
|
||||
}
|
||||
return nil, log.E("monitor.fetchCodeScanning", "API request failed", err)
|
||||
return nil, core.E("monitor.fetchCodeScanning", "API request failed", err)
|
||||
}
|
||||
|
||||
var alerts []CodeScanningAlert
|
||||
if err := json.Unmarshal(output, &alerts); err != nil {
|
||||
return nil, log.E("monitor.fetchCodeScanning", "failed to parse response", err)
|
||||
if r := core.JSONUnmarshal(output, &alerts); !r.OK {
|
||||
return nil, core.E("monitor.fetchCodeScanning", "failed to parse response", monitorResultErr(r, "monitor.fetchCodeScanning"))
|
||||
}
|
||||
|
||||
repoName := repoShortName(repoFullName)
|
||||
|
|
@ -296,7 +293,7 @@ func fetchCodeScanningAlerts(repoFullName string) ([]Finding, error) {
|
|||
func fetchDependabotAlerts(repoFullName string) ([]Finding, error) {
|
||||
args := []string{
|
||||
"api",
|
||||
fmt.Sprintf("repos/%s/dependabot/alerts", repoFullName),
|
||||
core.Sprintf("repos/%s/dependabot/alerts", repoFullName),
|
||||
}
|
||||
|
||||
cmd := exec.Command("gh", args...)
|
||||
|
|
@ -305,17 +302,17 @@ func fetchDependabotAlerts(repoFullName string) ([]Finding, error) {
|
|||
if exitErr, ok := err.(*exec.ExitError); ok {
|
||||
stderr := string(exitErr.Stderr)
|
||||
// Dependabot not enabled is expected
|
||||
if strings.Contains(stderr, "Dependabot alerts are not enabled") ||
|
||||
strings.Contains(stderr, "Not Found") {
|
||||
if core.Contains(stderr, "Dependabot alerts are not enabled") ||
|
||||
core.Contains(stderr, "Not Found") {
|
||||
return nil, nil
|
||||
}
|
||||
}
|
||||
return nil, log.E("monitor.fetchDependabot", "API request failed", err)
|
||||
return nil, core.E("monitor.fetchDependabot", "API request failed", err)
|
||||
}
|
||||
|
||||
var alerts []DependabotAlert
|
||||
if err := json.Unmarshal(output, &alerts); err != nil {
|
||||
return nil, log.E("monitor.fetchDependabot", "failed to parse response", err)
|
||||
if r := core.JSONUnmarshal(output, &alerts); !r.OK {
|
||||
return nil, core.E("monitor.fetchDependabot", "failed to parse response", monitorResultErr(r, "monitor.fetchDependabot"))
|
||||
}
|
||||
|
||||
repoName := repoShortName(repoFullName)
|
||||
|
|
@ -330,7 +327,7 @@ func fetchDependabotAlerts(repoFullName string) ([]Finding, error) {
|
|||
Rule: alert.SecurityAdvisory.CVEID,
|
||||
File: alert.Dependency.ManifestPath,
|
||||
Line: 0,
|
||||
Message: fmt.Sprintf("%s: %s", alert.SecurityVulnerability.Package.Name, alert.SecurityAdvisory.Summary),
|
||||
Message: core.Sprintf("%s: %s", alert.SecurityVulnerability.Package.Name, alert.SecurityAdvisory.Summary),
|
||||
URL: alert.HTMLURL,
|
||||
State: alert.State,
|
||||
RepoName: repoName,
|
||||
|
|
@ -347,7 +344,7 @@ func fetchDependabotAlerts(repoFullName string) ([]Finding, error) {
|
|||
func fetchSecretScanningAlerts(repoFullName string) ([]Finding, error) {
|
||||
args := []string{
|
||||
"api",
|
||||
fmt.Sprintf("repos/%s/secret-scanning/alerts", repoFullName),
|
||||
core.Sprintf("repos/%s/secret-scanning/alerts", repoFullName),
|
||||
}
|
||||
|
||||
cmd := exec.Command("gh", args...)
|
||||
|
|
@ -356,17 +353,17 @@ func fetchSecretScanningAlerts(repoFullName string) ([]Finding, error) {
|
|||
if exitErr, ok := err.(*exec.ExitError); ok {
|
||||
stderr := string(exitErr.Stderr)
|
||||
// Secret scanning not enabled is expected
|
||||
if strings.Contains(stderr, "Secret scanning is disabled") ||
|
||||
strings.Contains(stderr, "Not Found") {
|
||||
if core.Contains(stderr, "Secret scanning is disabled") ||
|
||||
core.Contains(stderr, "Not Found") {
|
||||
return nil, nil
|
||||
}
|
||||
}
|
||||
return nil, log.E("monitor.fetchSecretScanning", "API request failed", err)
|
||||
return nil, core.E("monitor.fetchSecretScanning", "API request failed", err)
|
||||
}
|
||||
|
||||
var alerts []SecretScanningAlert
|
||||
if err := json.Unmarshal(output, &alerts); err != nil {
|
||||
return nil, log.E("monitor.fetchSecretScanning", "failed to parse response", err)
|
||||
if r := core.JSONUnmarshal(output, &alerts); !r.OK {
|
||||
return nil, core.E("monitor.fetchSecretScanning", "failed to parse response", monitorResultErr(r, "monitor.fetchSecretScanning"))
|
||||
}
|
||||
|
||||
repoName := repoShortName(repoFullName)
|
||||
|
|
@ -381,7 +378,7 @@ func fetchSecretScanningAlerts(repoFullName string) ([]Finding, error) {
|
|||
Rule: alert.SecretType,
|
||||
File: alert.LocationType,
|
||||
Line: 0,
|
||||
Message: fmt.Sprintf("Exposed %s detected", alert.SecretType),
|
||||
Message: core.Sprintf("Exposed %s detected", alert.SecretType),
|
||||
URL: alert.HTMLURL,
|
||||
State: alert.State,
|
||||
RepoName: repoName,
|
||||
|
|
@ -396,7 +393,7 @@ func fetchSecretScanningAlerts(repoFullName string) ([]Finding, error) {
|
|||
|
||||
// normalizeSeverity normalizes severity strings to standard values
|
||||
func normalizeSeverity(s string) string {
|
||||
s = strings.ToLower(s)
|
||||
s = core.Lower(s)
|
||||
switch s {
|
||||
case "critical", "crit":
|
||||
return "critical"
|
||||
|
|
@ -415,7 +412,7 @@ func normalizeSeverity(s string) string {
|
|||
func filterBySeverity(findings []Finding, severities []string) []Finding {
|
||||
sevSet := make(map[string]bool)
|
||||
for _, s := range severities {
|
||||
sevSet[strings.ToLower(s)] = true
|
||||
sevSet[core.Lower(s)] = true
|
||||
}
|
||||
|
||||
var filtered []Finding
|
||||
|
|
@ -446,11 +443,11 @@ func sortBySeverity(findings []Finding) {
|
|||
|
||||
// outputJSON outputs findings as JSON
|
||||
func outputJSON(findings []Finding) error {
|
||||
data, err := json.MarshalIndent(findings, "", " ")
|
||||
if err != nil {
|
||||
return log.E("monitor", "failed to marshal findings", err)
|
||||
r := core.JSONMarshal(findings)
|
||||
if !r.OK {
|
||||
return core.E("monitor", "failed to marshal findings", monitorResultErr(r, "monitor"))
|
||||
}
|
||||
cli.Print("%s\n", string(data))
|
||||
cli.Print("%s\n", string(r.Value.([]byte)))
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
@ -470,18 +467,18 @@ func outputTable(findings []Finding) error {
|
|||
// Header summary
|
||||
var parts []string
|
||||
if counts["critical"] > 0 {
|
||||
parts = append(parts, errorStyle.Render(fmt.Sprintf("%d critical", counts["critical"])))
|
||||
parts = append(parts, errorStyle.Render(core.Sprintf("%d critical", counts["critical"])))
|
||||
}
|
||||
if counts["high"] > 0 {
|
||||
parts = append(parts, errorStyle.Render(fmt.Sprintf("%d high", counts["high"])))
|
||||
parts = append(parts, errorStyle.Render(core.Sprintf("%d high", counts["high"])))
|
||||
}
|
||||
if counts["medium"] > 0 {
|
||||
parts = append(parts, warningStyle.Render(fmt.Sprintf("%d medium", counts["medium"])))
|
||||
parts = append(parts, warningStyle.Render(core.Sprintf("%d medium", counts["medium"])))
|
||||
}
|
||||
if counts["low"] > 0 {
|
||||
parts = append(parts, dimStyle.Render(fmt.Sprintf("%d low", counts["low"])))
|
||||
parts = append(parts, dimStyle.Render(core.Sprintf("%d low", counts["low"])))
|
||||
}
|
||||
cli.Print("%s: %s\n", i18n.T("cmd.monitor.found"), strings.Join(parts, ", "))
|
||||
cli.Print("%s: %s\n", i18n.T("cmd.monitor.found"), core.Join(", ", parts...))
|
||||
cli.Blank()
|
||||
|
||||
// Group by repo
|
||||
|
|
@ -511,12 +508,12 @@ func outputTable(findings []Finding) error {
|
|||
if f.File != "" {
|
||||
location = f.File
|
||||
if f.Line > 0 {
|
||||
location = fmt.Sprintf("%s:%d", f.File, f.Line)
|
||||
location = core.Sprintf("%s:%d", f.File, f.Line)
|
||||
}
|
||||
}
|
||||
|
||||
cli.Print(" %s %s: %s",
|
||||
sevStyle.Render(fmt.Sprintf("[%s]", f.Severity)),
|
||||
sevStyle.Render(core.Sprintf("[%s]", f.Severity)),
|
||||
dimStyle.Render(f.Source),
|
||||
truncate(f.Message, 60))
|
||||
if location != "" {
|
||||
|
|
@ -533,8 +530,9 @@ func outputTable(findings []Finding) error {
|
|||
// repoShortName extracts the repo name from "org/repo" format.
|
||||
// Returns the full string if no "/" is present.
|
||||
func repoShortName(fullName string) string {
|
||||
if i := strings.LastIndex(fullName, "/"); i >= 0 {
|
||||
return fullName[i+1:]
|
||||
parts := core.Split(fullName, "/")
|
||||
if len(parts) > 0 {
|
||||
return parts[len(parts)-1]
|
||||
}
|
||||
return fullName
|
||||
}
|
||||
|
|
@ -553,10 +551,10 @@ func detectRepoFromGit() (string, error) {
|
|||
cmd := exec.Command("git", "remote", "get-url", "origin")
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
return "", log.E("monitor", i18n.T("cmd.monitor.error.not_git_repo"), err)
|
||||
return "", core.E("monitor", i18n.T("cmd.monitor.error.not_git_repo"), err)
|
||||
}
|
||||
|
||||
url := strings.TrimSpace(string(output))
|
||||
url := core.Trim(string(output))
|
||||
return parseGitHubRepo(url)
|
||||
}
|
||||
|
||||
|
|
@ -566,7 +564,7 @@ func detectOrgFromGit() string {
|
|||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
parts := strings.Split(repo, "/")
|
||||
parts := core.Split(repo, "/")
|
||||
if len(parts) >= 1 {
|
||||
return parts[0]
|
||||
}
|
||||
|
|
@ -576,20 +574,33 @@ func detectOrgFromGit() string {
|
|||
// parseGitHubRepo extracts org/repo from a git URL
|
||||
func parseGitHubRepo(url string) (string, error) {
|
||||
// Handle SSH URLs: git@github.com:org/repo.git
|
||||
if strings.HasPrefix(url, "git@github.com:") {
|
||||
path := strings.TrimPrefix(url, "git@github.com:")
|
||||
path = strings.TrimSuffix(path, ".git")
|
||||
if core.HasPrefix(url, "git@github.com:") {
|
||||
path := core.TrimPrefix(url, "git@github.com:")
|
||||
path = core.TrimSuffix(path, ".git")
|
||||
return path, nil
|
||||
}
|
||||
|
||||
// Handle HTTPS URLs: https://github.com/org/repo.git
|
||||
if strings.Contains(url, "github.com/") {
|
||||
parts := strings.Split(url, "github.com/")
|
||||
if core.Contains(url, "github.com/") {
|
||||
parts := core.Split(url, "github.com/")
|
||||
if len(parts) >= 2 {
|
||||
path := strings.TrimSuffix(parts[1], ".git")
|
||||
path := core.TrimSuffix(parts[1], ".git")
|
||||
return path, nil
|
||||
}
|
||||
}
|
||||
|
||||
return "", log.E("monitor.parseGitHubRepo", "could not parse GitHub repo from URL: "+url, nil)
|
||||
return "", core.E("monitor.parseGitHubRepo", core.Concat("could not parse GitHub repo from URL: ", url), nil)
|
||||
}
|
||||
|
||||
func monitorResultErr(r core.Result, op string) error {
|
||||
if r.OK {
|
||||
return nil
|
||||
}
|
||||
if err, ok := r.Value.(error); ok && err != nil {
|
||||
return err
|
||||
}
|
||||
if r.Value == nil {
|
||||
return core.E(op, "unexpected empty core result", nil)
|
||||
}
|
||||
return core.E(op, core.Sprint(r.Value), nil)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,11 +2,10 @@ package prod
|
|||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
core "dappco.re/go/core"
|
||||
"forge.lthn.ai/core/cli/pkg/cli"
|
||||
coreerr "forge.lthn.ai/core/go-log"
|
||||
"forge.lthn.ai/core/go-infra"
|
||||
)
|
||||
|
||||
|
|
@ -52,10 +51,10 @@ func init() {
|
|||
}
|
||||
|
||||
func getDNSClient() (*infra.CloudNSClient, error) {
|
||||
authID := os.Getenv("CLOUDNS_AUTH_ID")
|
||||
authPass := os.Getenv("CLOUDNS_AUTH_PASSWORD")
|
||||
authID := core.Env("CLOUDNS_AUTH_ID")
|
||||
authPass := core.Env("CLOUDNS_AUTH_PASSWORD")
|
||||
if authID == "" || authPass == "" {
|
||||
return nil, coreerr.E("prod.getDNSClient", "CLOUDNS_AUTH_ID and CLOUDNS_AUTH_PASSWORD required", nil)
|
||||
return nil, core.E("prod.getDNSClient", "CLOUDNS_AUTH_ID and CLOUDNS_AUTH_PASSWORD required", nil)
|
||||
}
|
||||
return infra.NewCloudNSClient(authID, authPass), nil
|
||||
}
|
||||
|
|
@ -76,7 +75,7 @@ func runDNSList(cmd *cli.Command, args []string) error {
|
|||
|
||||
records, err := dns.ListRecords(ctx, zone)
|
||||
if err != nil {
|
||||
return coreerr.E("prod.runDNSList", "list records", err)
|
||||
return core.E("prod.runDNSList", "list records", err)
|
||||
}
|
||||
|
||||
cli.Print("%s DNS records for %s\n\n", cli.BoldStyle.Render("▶"), cli.TitleStyle.Render(zone))
|
||||
|
|
@ -113,7 +112,7 @@ func runDNSSet(cmd *cli.Command, args []string) error {
|
|||
|
||||
changed, err := dns.EnsureRecord(ctx, dnsZone, host, recordType, value, dnsTTL)
|
||||
if err != nil {
|
||||
return coreerr.E("prod.runDNSSet", "set record", err)
|
||||
return core.E("prod.runDNSSet", "set record", err)
|
||||
}
|
||||
|
||||
if changed {
|
||||
|
|
|
|||
|
|
@ -2,12 +2,10 @@ package prod
|
|||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
core "dappco.re/go/core"
|
||||
"forge.lthn.ai/core/cli/pkg/cli"
|
||||
coreerr "forge.lthn.ai/core/go-log"
|
||||
"forge.lthn.ai/core/go-infra"
|
||||
)
|
||||
|
||||
|
|
@ -37,9 +35,9 @@ func init() {
|
|||
}
|
||||
|
||||
func getHCloudClient() (*infra.HCloudClient, error) {
|
||||
token := os.Getenv("HCLOUD_TOKEN")
|
||||
token := core.Env("HCLOUD_TOKEN")
|
||||
if token == "" {
|
||||
return nil, coreerr.E("prod.getHCloudClient", "HCLOUD_TOKEN environment variable required", nil)
|
||||
return nil, core.E("prod.getHCloudClient", "HCLOUD_TOKEN environment variable required", nil)
|
||||
}
|
||||
return infra.NewHCloudClient(token), nil
|
||||
}
|
||||
|
|
@ -55,7 +53,7 @@ func runLBStatus(cmd *cli.Command, args []string) error {
|
|||
|
||||
lbs, err := hc.ListLoadBalancers(ctx)
|
||||
if err != nil {
|
||||
return coreerr.E("prod.runLBStatus", "list load balancers", err)
|
||||
return core.E("prod.runLBStatus", "list load balancers", err)
|
||||
}
|
||||
|
||||
if len(lbs) == 0 {
|
||||
|
|
@ -94,7 +92,7 @@ func runLBStatus(cmd *cli.Command, args []string) error {
|
|||
}
|
||||
}
|
||||
}
|
||||
fmt.Println()
|
||||
core.Println()
|
||||
}
|
||||
|
||||
return nil
|
||||
|
|
|
|||
|
|
@ -2,11 +2,10 @@ package prod
|
|||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
core "dappco.re/go/core"
|
||||
"forge.lthn.ai/core/cli/pkg/cli"
|
||||
coreerr "forge.lthn.ai/core/go-log"
|
||||
"forge.lthn.ai/core/go-infra"
|
||||
)
|
||||
|
||||
|
|
@ -70,7 +69,7 @@ func runSetup(cmd *cli.Command, args []string) error {
|
|||
|
||||
if err := step.fn(ctx, cfg); err != nil {
|
||||
cli.Print(" %s %s: %s\n", cli.ErrorStyle.Render("✗"), step.name, err)
|
||||
return coreerr.E("prod.setup", "step "+step.name+" failed", err)
|
||||
return core.E("prod.setup", core.Concat("step ", step.name, " failed"), err)
|
||||
}
|
||||
|
||||
cli.Print(" %s %s complete\n", cli.SuccessStyle.Render("✓"), step.name)
|
||||
|
|
@ -82,14 +81,14 @@ func runSetup(cmd *cli.Command, args []string) error {
|
|||
|
||||
func stepDiscover(ctx context.Context, cfg *infra.Config) error {
|
||||
// Discover HCloud servers
|
||||
hcloudToken := os.Getenv("HCLOUD_TOKEN")
|
||||
hcloudToken := core.Env("HCLOUD_TOKEN")
|
||||
if hcloudToken != "" {
|
||||
cli.Print(" Discovering Hetzner Cloud servers...\n")
|
||||
|
||||
hc := infra.NewHCloudClient(hcloudToken)
|
||||
servers, err := hc.ListServers(ctx)
|
||||
if err != nil {
|
||||
return coreerr.E("prod.stepDiscover", "list HCloud servers", err)
|
||||
return core.E("prod.stepDiscover", "list HCloud servers", err)
|
||||
}
|
||||
|
||||
for _, s := range servers {
|
||||
|
|
@ -106,15 +105,15 @@ func stepDiscover(ctx context.Context, cfg *infra.Config) error {
|
|||
}
|
||||
|
||||
// Discover Robot servers
|
||||
robotUser := os.Getenv("HETZNER_ROBOT_USER")
|
||||
robotPass := os.Getenv("HETZNER_ROBOT_PASS")
|
||||
robotUser := core.Env("HETZNER_ROBOT_USER")
|
||||
robotPass := core.Env("HETZNER_ROBOT_PASS")
|
||||
if robotUser != "" && robotPass != "" {
|
||||
cli.Print(" Discovering Hetzner Robot servers...\n")
|
||||
|
||||
hr := infra.NewHRobotClient(robotUser, robotPass)
|
||||
servers, err := hr.ListServers(ctx)
|
||||
if err != nil {
|
||||
return coreerr.E("prod.stepDiscover", "list Robot servers", err)
|
||||
return core.E("prod.stepDiscover", "list Robot servers", err)
|
||||
}
|
||||
|
||||
for _, s := range servers {
|
||||
|
|
@ -138,9 +137,9 @@ func stepDiscover(ctx context.Context, cfg *infra.Config) error {
|
|||
}
|
||||
|
||||
func stepLoadBalancer(ctx context.Context, cfg *infra.Config) error {
|
||||
hcloudToken := os.Getenv("HCLOUD_TOKEN")
|
||||
hcloudToken := core.Env("HCLOUD_TOKEN")
|
||||
if hcloudToken == "" {
|
||||
return coreerr.E("prod.stepLoadBalancer", "HCLOUD_TOKEN required for load balancer management", nil)
|
||||
return core.E("prod.stepLoadBalancer", "HCLOUD_TOKEN required for load balancer management", nil)
|
||||
}
|
||||
|
||||
hc := infra.NewHCloudClient(hcloudToken)
|
||||
|
|
@ -148,7 +147,7 @@ func stepLoadBalancer(ctx context.Context, cfg *infra.Config) error {
|
|||
// Check if LB already exists
|
||||
lbs, err := hc.ListLoadBalancers(ctx)
|
||||
if err != nil {
|
||||
return coreerr.E("prod.stepLoadBalancer", "list load balancers", err)
|
||||
return core.E("prod.stepLoadBalancer", "list load balancers", err)
|
||||
}
|
||||
|
||||
for _, lb := range lbs {
|
||||
|
|
@ -175,7 +174,7 @@ func stepLoadBalancer(ctx context.Context, cfg *infra.Config) error {
|
|||
for _, b := range cfg.LoadBalancer.Backends {
|
||||
host, ok := cfg.Hosts[b.Host]
|
||||
if !ok {
|
||||
return coreerr.E("prod.stepLoadBalancer", "backend host '"+b.Host+"' not found in config", nil)
|
||||
return core.E("prod.stepLoadBalancer", core.Concat("backend host '", b.Host, "' not found in config"), nil)
|
||||
}
|
||||
targets = append(targets, infra.HCloudLBCreateTarget{
|
||||
Type: "ip",
|
||||
|
|
@ -223,7 +222,7 @@ func stepLoadBalancer(ctx context.Context, cfg *infra.Config) error {
|
|||
|
||||
lb, err := hc.CreateLoadBalancer(ctx, req)
|
||||
if err != nil {
|
||||
return coreerr.E("prod.stepLoadBalancer", "create load balancer", err)
|
||||
return core.E("prod.stepLoadBalancer", "create load balancer", err)
|
||||
}
|
||||
|
||||
cli.Print(" Created: %s (ID: %d, IP: %s)\n",
|
||||
|
|
@ -233,10 +232,10 @@ func stepLoadBalancer(ctx context.Context, cfg *infra.Config) error {
|
|||
}
|
||||
|
||||
func stepDNS(ctx context.Context, cfg *infra.Config) error {
|
||||
authID := os.Getenv("CLOUDNS_AUTH_ID")
|
||||
authPass := os.Getenv("CLOUDNS_AUTH_PASSWORD")
|
||||
authID := core.Env("CLOUDNS_AUTH_ID")
|
||||
authPass := core.Env("CLOUDNS_AUTH_PASSWORD")
|
||||
if authID == "" || authPass == "" {
|
||||
return coreerr.E("prod.stepDNS", "CLOUDNS_AUTH_ID and CLOUDNS_AUTH_PASSWORD required", nil)
|
||||
return core.E("prod.stepDNS", "CLOUDNS_AUTH_ID and CLOUDNS_AUTH_PASSWORD required", nil)
|
||||
}
|
||||
|
||||
dns := infra.NewCloudNSClient(authID, authPass)
|
||||
|
|
|
|||
|
|
@ -1,13 +1,12 @@
|
|||
package prod
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"syscall"
|
||||
|
||||
core "dappco.re/go/core"
|
||||
"forge.lthn.ai/core/cli/pkg/cli"
|
||||
coreerr "forge.lthn.ai/core/go-log"
|
||||
)
|
||||
|
||||
var sshCmd = &cli.Command{
|
||||
|
|
@ -38,15 +37,15 @@ func runSSH(cmd *cli.Command, args []string) error {
|
|||
for n, h := range cfg.Hosts {
|
||||
cli.Print(" %s %s (%s)\n", cli.BoldStyle.Render(n), h.IP, h.Role)
|
||||
}
|
||||
return coreerr.E("prod.ssh", "host '"+name+"' not found in infra.yaml", nil)
|
||||
return core.E("prod.ssh", core.Concat("host '", name, "' not found in infra.yaml"), nil)
|
||||
}
|
||||
|
||||
sshArgs := []string{
|
||||
"ssh",
|
||||
"-i", host.SSH.Key,
|
||||
"-p", fmt.Sprintf("%d", host.SSH.Port),
|
||||
"-p", core.Sprintf("%d", host.SSH.Port),
|
||||
"-o", "StrictHostKeyChecking=accept-new",
|
||||
fmt.Sprintf("%s@%s", host.SSH.User, host.IP),
|
||||
core.Sprintf("%s@%s", host.SSH.User, host.IP),
|
||||
}
|
||||
|
||||
cli.Print("%s %s@%s (%s)\n",
|
||||
|
|
@ -56,7 +55,7 @@ func runSSH(cmd *cli.Command, args []string) error {
|
|||
|
||||
sshPath, err := exec.LookPath("ssh")
|
||||
if err != nil {
|
||||
return coreerr.E("prod.ssh", "ssh not found", err)
|
||||
return core.E("prod.ssh", "ssh not found", err)
|
||||
}
|
||||
|
||||
// Replace current process with SSH
|
||||
|
|
|
|||
|
|
@ -2,15 +2,13 @@ package prod
|
|||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"forge.lthn.ai/core/go-ansible"
|
||||
core "dappco.re/go/core"
|
||||
"forge.lthn.ai/core/cli/pkg/cli"
|
||||
coreerr "forge.lthn.ai/core/go-log"
|
||||
"forge.lthn.ai/core/go-ansible"
|
||||
"forge.lthn.ai/core/go-infra"
|
||||
)
|
||||
|
||||
|
|
@ -85,11 +83,11 @@ func runStatus(cmd *cli.Command, args []string) error {
|
|||
}
|
||||
|
||||
// Check LB if token available
|
||||
if token := os.Getenv("HCLOUD_TOKEN"); token != "" {
|
||||
fmt.Println()
|
||||
if token := core.Env("HCLOUD_TOKEN"); token != "" {
|
||||
core.Println()
|
||||
checkLoadBalancer(ctx, token)
|
||||
} else {
|
||||
fmt.Println()
|
||||
core.Println()
|
||||
cli.Print("%s Load balancer: %s\n",
|
||||
cli.DimStyle.Render(" ○"),
|
||||
cli.DimStyle.Render("HCLOUD_TOKEN not set (skipped)"))
|
||||
|
|
@ -115,14 +113,14 @@ func checkHost(ctx context.Context, name string, host *infra.Host) hostStatus {
|
|||
|
||||
client, err := ansible.NewSSHClient(sshCfg)
|
||||
if err != nil {
|
||||
s.Error = coreerr.E("prod.checkHost", "create SSH client", err)
|
||||
s.Error = core.E("prod.checkHost", "create SSH client", err)
|
||||
return s
|
||||
}
|
||||
defer func() { _ = client.Close() }()
|
||||
|
||||
start := time.Now()
|
||||
if err := client.Connect(ctx); err != nil {
|
||||
s.Error = coreerr.E("prod.checkHost", "SSH connect", err)
|
||||
s.Error = core.E("prod.checkHost", "SSH connect", err)
|
||||
return s
|
||||
}
|
||||
s.Connected = true
|
||||
|
|
@ -130,12 +128,12 @@ func checkHost(ctx context.Context, name string, host *infra.Host) hostStatus {
|
|||
|
||||
// OS info
|
||||
stdout, _, _, _ := client.Run(ctx, "cat /etc/os-release 2>/dev/null | grep PRETTY_NAME | cut -d'\"' -f2")
|
||||
s.OS = strings.TrimSpace(stdout)
|
||||
s.OS = core.Trim(stdout)
|
||||
|
||||
// Docker
|
||||
stdout, _, _, err = client.Run(ctx, "docker --version 2>/dev/null | head -1")
|
||||
if err == nil && stdout != "" {
|
||||
s.Docker = strings.TrimSpace(stdout)
|
||||
s.Docker = core.Trim(stdout)
|
||||
}
|
||||
|
||||
// Check each expected service
|
||||
|
|
@ -151,14 +149,14 @@ func checkService(ctx context.Context, client *ansible.SSHClient, service string
|
|||
switch service {
|
||||
case "coolify":
|
||||
stdout, _, _, _ := client.Run(ctx, "docker ps --format '{{.Names}}' 2>/dev/null | grep -c coolify")
|
||||
if strings.TrimSpace(stdout) != "0" && strings.TrimSpace(stdout) != "" {
|
||||
if core.Trim(stdout) != "0" && core.Trim(stdout) != "" {
|
||||
return "running"
|
||||
}
|
||||
return "not running"
|
||||
|
||||
case "traefik":
|
||||
stdout, _, _, _ := client.Run(ctx, "docker ps --format '{{.Names}}' 2>/dev/null | grep -c traefik")
|
||||
if strings.TrimSpace(stdout) != "0" && strings.TrimSpace(stdout) != "" {
|
||||
if core.Trim(stdout) != "0" && core.Trim(stdout) != "" {
|
||||
return "running"
|
||||
}
|
||||
return "not running"
|
||||
|
|
@ -168,16 +166,16 @@ func checkService(ctx context.Context, client *ansible.SSHClient, service string
|
|||
stdout, _, _, _ := client.Run(ctx,
|
||||
"docker exec $(docker ps -q --filter name=mariadb 2>/dev/null || echo none) "+
|
||||
"mariadb -u root -e \"SHOW STATUS LIKE 'wsrep_cluster_size'\" --skip-column-names 2>/dev/null | awk '{print $2}'")
|
||||
size := strings.TrimSpace(stdout)
|
||||
size := core.Trim(stdout)
|
||||
if size != "" && size != "0" {
|
||||
return fmt.Sprintf("cluster_size=%s", size)
|
||||
return core.Sprintf("cluster_size=%s", size)
|
||||
}
|
||||
// Try non-Docker
|
||||
stdout, _, _, _ = client.Run(ctx,
|
||||
"mariadb -u root -e \"SHOW STATUS LIKE 'wsrep_cluster_size'\" --skip-column-names 2>/dev/null | awk '{print $2}'")
|
||||
size = strings.TrimSpace(stdout)
|
||||
size = core.Trim(stdout)
|
||||
if size != "" && size != "0" {
|
||||
return fmt.Sprintf("cluster_size=%s", size)
|
||||
return core.Sprintf("cluster_size=%s", size)
|
||||
}
|
||||
return "not running"
|
||||
|
||||
|
|
@ -185,18 +183,18 @@ func checkService(ctx context.Context, client *ansible.SSHClient, service string
|
|||
stdout, _, _, _ := client.Run(ctx,
|
||||
"docker exec $(docker ps -q --filter name=redis 2>/dev/null || echo none) "+
|
||||
"redis-cli ping 2>/dev/null")
|
||||
if strings.TrimSpace(stdout) == "PONG" {
|
||||
if core.Trim(stdout) == "PONG" {
|
||||
return "running"
|
||||
}
|
||||
stdout, _, _, _ = client.Run(ctx, "redis-cli ping 2>/dev/null")
|
||||
if strings.TrimSpace(stdout) == "PONG" {
|
||||
if core.Trim(stdout) == "PONG" {
|
||||
return "running"
|
||||
}
|
||||
return "not running"
|
||||
|
||||
case "forgejo-runner":
|
||||
stdout, _, _, _ := client.Run(ctx, "systemctl is-active forgejo-runner 2>/dev/null || docker ps --format '{{.Names}}' 2>/dev/null | grep -c runner")
|
||||
val := strings.TrimSpace(stdout)
|
||||
val := core.Trim(stdout)
|
||||
if val == "active" || (val != "0" && val != "") {
|
||||
return "running"
|
||||
}
|
||||
|
|
@ -205,8 +203,8 @@ func checkService(ctx context.Context, client *ansible.SSHClient, service string
|
|||
default:
|
||||
// Generic docker container check
|
||||
stdout, _, _, _ := client.Run(ctx,
|
||||
fmt.Sprintf("docker ps --format '{{.Names}}' 2>/dev/null | grep -c %s", service))
|
||||
if strings.TrimSpace(stdout) != "0" && strings.TrimSpace(stdout) != "" {
|
||||
core.Sprintf("docker ps --format '{{.Names}}' 2>/dev/null | grep -c %s", service))
|
||||
if core.Trim(stdout) != "0" && core.Trim(stdout) != "" {
|
||||
return "running"
|
||||
}
|
||||
return "not running"
|
||||
|
|
@ -248,7 +246,7 @@ func printHostStatus(s hostStatus) {
|
|||
if s.OS != "" {
|
||||
cli.Print(" %s", cli.DimStyle.Render(s.OS))
|
||||
}
|
||||
fmt.Println()
|
||||
core.Println()
|
||||
|
||||
if s.Docker != "" {
|
||||
cli.Print(" %s %s\n", cli.SuccessStyle.Render("✓"), cli.DimStyle.Render(s.Docker))
|
||||
|
|
@ -271,7 +269,7 @@ func printHostStatus(s hostStatus) {
|
|||
cli.Print(" %s %s %s\n", icon, svc, style.Render(status))
|
||||
}
|
||||
|
||||
fmt.Println()
|
||||
core.Println()
|
||||
}
|
||||
|
||||
func checkLoadBalancer(ctx context.Context, token string) {
|
||||
|
|
|
|||
39
config.go
39
config.go
|
|
@ -3,11 +3,7 @@
|
|||
package infra
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
coreerr "forge.lthn.ai/core/go-log"
|
||||
coreio "forge.lthn.ai/core/go-io"
|
||||
core "dappco.re/go/core"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
|
|
@ -230,14 +226,14 @@ type BackupJob struct {
|
|||
|
||||
// Load reads and parses an infra.yaml file.
|
||||
func Load(path string) (*Config, error) {
|
||||
data, err := coreio.Local.Read(path)
|
||||
if err != nil {
|
||||
return nil, coreerr.E("infra.Load", "read infra config", err)
|
||||
read := localFS.Read(path)
|
||||
if !read.OK {
|
||||
return nil, core.E("infra.Load", "read infra config", coreResultErr(read, "infra.Load"))
|
||||
}
|
||||
|
||||
var cfg Config
|
||||
if err := yaml.Unmarshal([]byte(data), &cfg); err != nil {
|
||||
return nil, coreerr.E("infra.Load", "parse infra config", err)
|
||||
if err := yaml.Unmarshal([]byte(read.Value.(string)), &cfg); err != nil {
|
||||
return nil, core.E("infra.Load", "parse infra config", err)
|
||||
}
|
||||
|
||||
// Expand SSH key paths
|
||||
|
|
@ -257,19 +253,19 @@ func Load(path string) (*Config, error) {
|
|||
func Discover(startDir string) (*Config, string, error) {
|
||||
dir := startDir
|
||||
for {
|
||||
path := filepath.Join(dir, "infra.yaml")
|
||||
if _, err := os.Stat(path); err == nil {
|
||||
path := core.JoinPath(dir, "infra.yaml")
|
||||
if localFS.Exists(path) {
|
||||
cfg, err := Load(path)
|
||||
return cfg, path, err
|
||||
}
|
||||
|
||||
parent := filepath.Dir(dir)
|
||||
parent := core.PathDir(dir)
|
||||
if parent == dir {
|
||||
break
|
||||
}
|
||||
dir = parent
|
||||
}
|
||||
return nil, "", coreerr.E("infra.Discover", "infra.yaml not found (searched from "+startDir+")", nil)
|
||||
return nil, "", core.E("infra.Discover", core.Concat("infra.yaml not found (searched from ", startDir, ")"), nil)
|
||||
}
|
||||
|
||||
// HostsByRole returns all hosts matching the given role.
|
||||
|
|
@ -290,12 +286,19 @@ func (c *Config) AppServers() map[string]*Host {
|
|||
|
||||
// expandPath expands ~ to home directory.
|
||||
func expandPath(path string) string {
|
||||
if len(path) > 0 && path[0] == '~' {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
if core.HasPrefix(path, "~") {
|
||||
home := core.Env("DIR_HOME")
|
||||
if home == "" {
|
||||
return path
|
||||
}
|
||||
return filepath.Join(home, path[1:])
|
||||
suffix := core.TrimPrefix(path, "~")
|
||||
if suffix == "" {
|
||||
return home
|
||||
}
|
||||
if core.HasPrefix(suffix, "/") {
|
||||
return core.Concat(home, suffix)
|
||||
}
|
||||
return core.JoinPath(home, suffix)
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,8 +2,9 @@ package infra
|
|||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
core "dappco.re/go/core"
|
||||
)
|
||||
|
||||
func TestLoad_Good(t *testing.T) {
|
||||
|
|
@ -68,9 +69,9 @@ func TestLoad_Bad(t *testing.T) {
|
|||
|
||||
func TestLoad_Ugly(t *testing.T) {
|
||||
// Invalid YAML
|
||||
tmp := filepath.Join(t.TempDir(), "infra.yaml")
|
||||
if err := os.WriteFile(tmp, []byte("{{invalid yaml"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
tmp := core.JoinPath(t.TempDir(), "infra.yaml")
|
||||
if r := localFS.WriteMode(tmp, "{{invalid yaml", 0644); !r.OK {
|
||||
t.Fatal(coreResultErr(r, "TestLoad_Ugly"))
|
||||
}
|
||||
|
||||
_, err := Load(tmp)
|
||||
|
|
@ -129,13 +130,13 @@ func TestConfig_AppServers_Good(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestExpandPath(t *testing.T) {
|
||||
home, _ := os.UserHomeDir()
|
||||
home := core.Env("DIR_HOME")
|
||||
|
||||
tests := []struct {
|
||||
input string
|
||||
want string
|
||||
}{
|
||||
{"~/.ssh/id_rsa", filepath.Join(home, ".ssh/id_rsa")},
|
||||
{"~/.ssh/id_rsa", core.JoinPath(home, ".ssh", "id_rsa")},
|
||||
{"/absolute/path", "/absolute/path"},
|
||||
{"relative/path", "relative/path"},
|
||||
}
|
||||
|
|
|
|||
18
core_helpers.go
Normal file
18
core_helpers.go
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
package infra
|
||||
|
||||
import core "dappco.re/go/core"
|
||||
|
||||
var localFS = (&core.Fs{}).NewUnrestricted()
|
||||
|
||||
func coreResultErr(r core.Result, op string) error {
|
||||
if r.OK {
|
||||
return nil
|
||||
}
|
||||
if err, ok := r.Value.(error); ok && err != nil {
|
||||
return err
|
||||
}
|
||||
if r.Value == nil {
|
||||
return core.E(op, "unexpected empty core result", nil)
|
||||
}
|
||||
return core.E(op, core.Sprint(r.Value), nil)
|
||||
}
|
||||
3
go.mod
3
go.mod
|
|
@ -3,11 +3,11 @@ module forge.lthn.ai/core/go-infra
|
|||
go 1.26.0
|
||||
|
||||
require (
|
||||
dappco.re/go/core v0.8.0-alpha.1
|
||||
forge.lthn.ai/core/cli v0.3.5
|
||||
forge.lthn.ai/core/go-ansible v0.1.4
|
||||
forge.lthn.ai/core/go-i18n v0.1.5
|
||||
forge.lthn.ai/core/go-io v0.1.5
|
||||
forge.lthn.ai/core/go-log v0.0.4
|
||||
forge.lthn.ai/core/go-scm v0.3.4
|
||||
github.com/stretchr/testify v1.11.1
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
|
|
@ -16,6 +16,7 @@ require (
|
|||
require (
|
||||
forge.lthn.ai/core/go v0.3.1 // indirect
|
||||
forge.lthn.ai/core/go-inference v0.1.4 // indirect
|
||||
forge.lthn.ai/core/go-log v0.0.4 // indirect
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||
github.com/charmbracelet/bubbletea v1.3.10 // indirect
|
||||
github.com/charmbracelet/colorprofile v0.4.3 // indirect
|
||||
|
|
|
|||
2
go.sum
2
go.sum
|
|
@ -1,3 +1,5 @@
|
|||
dappco.re/go/core v0.8.0-alpha.1 h1:gj7+Scv+L63Z7wMxbJYHhaRFkHJo2u4MMPuUSv/Dhtk=
|
||||
dappco.re/go/core v0.8.0-alpha.1/go.mod h1:f2/tBZ3+3IqDrg2F5F598llv0nmb/4gJVCFzM5geE4A=
|
||||
forge.lthn.ai/core/cli v0.3.5 h1:P7yK0DmSA1QnUMFuCjJZf/fk/akKPIxopQ6OwD8Sar8=
|
||||
forge.lthn.ai/core/cli v0.3.5/go.mod h1:SeArHx+hbpX5iZqgASCD7Q1EDoc6uaaGiGBotmNzIx4=
|
||||
forge.lthn.ai/core/go v0.3.1 h1:5FMTsUhLcxSr07F9q3uG0Goy4zq4eLivoqi8shSY4UM=
|
||||
|
|
|
|||
25
hetzner.go
25
hetzner.go
|
|
@ -2,12 +2,9 @@ package infra
|
|||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
coreerr "forge.lthn.ai/core/go-log"
|
||||
core "dappco.re/go/core"
|
||||
)
|
||||
|
||||
const (
|
||||
|
|
@ -202,7 +199,7 @@ func (c *HCloudClient) GetLoadBalancer(ctx context.Context, id int) (*HCloudLoad
|
|||
var result struct {
|
||||
LoadBalancer HCloudLoadBalancer `json:"load_balancer"`
|
||||
}
|
||||
if err := c.get(ctx, fmt.Sprintf("/load_balancers/%d", id), &result); err != nil {
|
||||
if err := c.get(ctx, core.Sprintf("/load_balancers/%d", id), &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &result.LoadBalancer, nil
|
||||
|
|
@ -210,10 +207,11 @@ func (c *HCloudClient) GetLoadBalancer(ctx context.Context, id int) (*HCloudLoad
|
|||
|
||||
// CreateLoadBalancer creates a new load balancer.
|
||||
func (c *HCloudClient) CreateLoadBalancer(ctx context.Context, req HCloudLBCreateRequest) (*HCloudLoadBalancer, error) {
|
||||
body, err := json.Marshal(req)
|
||||
if err != nil {
|
||||
return nil, coreerr.E("HCloudClient.CreateLoadBalancer", "marshal request", err)
|
||||
marshaled := core.JSONMarshal(req)
|
||||
if !marshaled.OK {
|
||||
return nil, core.E("HCloudClient.CreateLoadBalancer", "marshal request", coreResultErr(marshaled, "HCloudClient.CreateLoadBalancer"))
|
||||
}
|
||||
body := marshaled.Value.([]byte)
|
||||
|
||||
var result struct {
|
||||
LoadBalancer HCloudLoadBalancer `json:"load_balancer"`
|
||||
|
|
@ -226,16 +224,19 @@ func (c *HCloudClient) CreateLoadBalancer(ctx context.Context, req HCloudLBCreat
|
|||
|
||||
// DeleteLoadBalancer deletes a load balancer by ID.
|
||||
func (c *HCloudClient) DeleteLoadBalancer(ctx context.Context, id int) error {
|
||||
return c.delete(ctx, fmt.Sprintf("/load_balancers/%d", id))
|
||||
return c.delete(ctx, core.Sprintf("/load_balancers/%d", id))
|
||||
}
|
||||
|
||||
// CreateSnapshot creates a server snapshot.
|
||||
func (c *HCloudClient) CreateSnapshot(ctx context.Context, serverID int, description string) error {
|
||||
body, _ := json.Marshal(map[string]string{
|
||||
marshaled := core.JSONMarshal(map[string]string{
|
||||
"description": description,
|
||||
"type": "snapshot",
|
||||
})
|
||||
return c.post(ctx, fmt.Sprintf("/servers/%d/actions/create_image", serverID), body, nil)
|
||||
if !marshaled.OK {
|
||||
return core.E("HCloudClient.CreateSnapshot", "marshal request", coreResultErr(marshaled, "HCloudClient.CreateSnapshot"))
|
||||
}
|
||||
return c.post(ctx, core.Sprintf("/servers/%d/actions/create_image", serverID), marshaled.Value.([]byte), nil)
|
||||
}
|
||||
|
||||
func (c *HCloudClient) get(ctx context.Context, path string, result any) error {
|
||||
|
|
@ -247,7 +248,7 @@ func (c *HCloudClient) get(ctx context.Context, path string, result any) error {
|
|||
}
|
||||
|
||||
func (c *HCloudClient) post(ctx context.Context, path string, body []byte, result any) error {
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.baseURL+path, strings.NewReader(string(body)))
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.baseURL+path, core.NewReader(string(body)))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,11 +2,12 @@ package infra
|
|||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
core "dappco.re/go/core"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
|
@ -41,7 +42,7 @@ func TestHCloudClient_ListServers_Good(t *testing.T) {
|
|||
},
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(resp)
|
||||
writeCoreJSON(t, w, resp)
|
||||
})
|
||||
|
||||
ts := httptest.NewServer(mux)
|
||||
|
|
@ -274,8 +275,7 @@ func TestHCloudClient_CreateLoadBalancer_Good(t *testing.T) {
|
|||
assert.Equal(t, "/load_balancers", r.URL.Path)
|
||||
|
||||
var body HCloudLBCreateRequest
|
||||
err := json.NewDecoder(r.Body).Decode(&body)
|
||||
require.NoError(t, err)
|
||||
decodeCoreJSONBody(t, r, &body)
|
||||
assert.Equal(t, "hermes", body.Name)
|
||||
assert.Equal(t, "lb11", body.LoadBalancerType)
|
||||
assert.Equal(t, "round_robin", body.Algorithm.Type)
|
||||
|
|
@ -333,8 +333,7 @@ func TestHCloudClient_CreateSnapshot_Good(t *testing.T) {
|
|||
assert.Equal(t, "/servers/123/actions/create_image", r.URL.Path)
|
||||
|
||||
var body map[string]string
|
||||
err := json.NewDecoder(r.Body).Decode(&body)
|
||||
require.NoError(t, err)
|
||||
decodeCoreJSONBody(t, r, &body)
|
||||
assert.Equal(t, "daily backup", body["description"])
|
||||
assert.Equal(t, "snapshot", body["type"])
|
||||
|
||||
|
|
@ -399,9 +398,7 @@ func TestHCloudServer_JSON_Good(t *testing.T) {
|
|||
}`
|
||||
|
||||
var server HCloudServer
|
||||
err := json.Unmarshal([]byte(data), &server)
|
||||
|
||||
require.NoError(t, err)
|
||||
requireHetznerJSON(t, data, &server)
|
||||
assert.Equal(t, 123, server.ID)
|
||||
assert.Equal(t, "web-1", server.Name)
|
||||
assert.Equal(t, "running", server.Status)
|
||||
|
|
@ -431,9 +428,7 @@ func TestHCloudLoadBalancer_JSON_Good(t *testing.T) {
|
|||
}`
|
||||
|
||||
var lb HCloudLoadBalancer
|
||||
err := json.Unmarshal([]byte(data), &lb)
|
||||
|
||||
require.NoError(t, err)
|
||||
requireHetznerJSON(t, data, &lb)
|
||||
assert.Equal(t, 789, lb.ID)
|
||||
assert.Equal(t, "hermes", lb.Name)
|
||||
assert.True(t, lb.PublicNet.Enabled)
|
||||
|
|
@ -460,9 +455,7 @@ func TestHRobotServer_JSON_Good(t *testing.T) {
|
|||
}`
|
||||
|
||||
var server HRobotServer
|
||||
err := json.Unmarshal([]byte(data), &server)
|
||||
|
||||
require.NoError(t, err)
|
||||
requireHetznerJSON(t, data, &server)
|
||||
assert.Equal(t, "1.2.3.4", server.ServerIP)
|
||||
assert.Equal(t, "noc", server.ServerName)
|
||||
assert.Equal(t, "EX44", server.Product)
|
||||
|
|
@ -471,3 +464,28 @@ func TestHRobotServer_JSON_Good(t *testing.T) {
|
|||
assert.False(t, server.Cancelled)
|
||||
assert.Equal(t, "2026-03-01", server.PaidUntil)
|
||||
}
|
||||
|
||||
func requireHetznerJSON(t *testing.T, data string, target any) {
|
||||
t.Helper()
|
||||
|
||||
r := core.JSONUnmarshal([]byte(data), target)
|
||||
require.True(t, r.OK)
|
||||
}
|
||||
|
||||
func writeCoreJSON(t *testing.T, w http.ResponseWriter, value any) {
|
||||
t.Helper()
|
||||
|
||||
r := core.JSONMarshal(value)
|
||||
require.True(t, r.OK)
|
||||
_, err := w.Write(r.Value.([]byte))
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func decodeCoreJSONBody(t *testing.T, r *http.Request, target any) {
|
||||
t.Helper()
|
||||
|
||||
body, err := io.ReadAll(r.Body)
|
||||
require.NoError(t, err)
|
||||
result := core.JSONUnmarshal(body, target)
|
||||
require.True(t, result.OK)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue