mirror of
https://github.com/mulgadc/spinifex.git
synced 2026-10-15 09:01:31 +00:00
refactor: move spx command tree to operator/cli
Moving the command tree out of cmd/ brings it under errcheck and gosec. Flag and config wiring now fails loudly instead of silently dropping a binding, listing commands fail when they cannot write their table, and the formation server's unauthenticated TLS is annotated as a known gap.
This commit is contained in:
@@ -59,7 +59,7 @@ build-ui:
|
||||
# GO commands
|
||||
VERSION ?= $(shell git describe --tags --always --dirty)
|
||||
COMMIT ?= $(shell git rev-parse --short HEAD)
|
||||
LDFLAGS := -s -w -X github.com/mulgadc/spinifex/cmd/spinifex/cmd.Version=$(VERSION) -X github.com/mulgadc/spinifex/cmd/spinifex/cmd.Commit=$(COMMIT)
|
||||
LDFLAGS := -s -w -X github.com/mulgadc/spinifex/spinifex/operator/cli.Version=$(VERSION) -X github.com/mulgadc/spinifex/spinifex/operator/cli.Commit=$(COMMIT)
|
||||
|
||||
go_build:
|
||||
@echo -e "\n....Building $(GO_PROJECT_NAME)"
|
||||
|
||||
@@ -35,8 +35,8 @@ COPY bluebottle/ ./bluebottle/
|
||||
# Build spinifex binary (spx)
|
||||
RUN COMMIT=$(cd spinifex && git rev-parse --short HEAD 2>/dev/null || echo "unknown") && \
|
||||
cd spinifex && GOFIPS140=v1.0.0 go build -ldflags "-s -w \
|
||||
-X github.com/mulgadc/spinifex/cmd/spinifex/cmd.Version=${VERSION} \
|
||||
-X github.com/mulgadc/spinifex/cmd/spinifex/cmd.Commit=${COMMIT}" \
|
||||
-X github.com/mulgadc/spinifex/spinifex/operator/cli.Version=${VERSION} \
|
||||
-X github.com/mulgadc/spinifex/spinifex/operator/cli.Commit=${COMMIT}" \
|
||||
-o /out/spx cmd/spinifex/main.go
|
||||
|
||||
# Build nbdkit viperblock plugin (.so)
|
||||
|
||||
@@ -2,9 +2,9 @@ package main
|
||||
|
||||
import (
|
||||
_ "github.com/mulgadc/bluebottle/pkg/fipsboot"
|
||||
"github.com/mulgadc/spinifex/cmd/spinifex/cmd"
|
||||
"github.com/mulgadc/spinifex/spinifex/operator/cli"
|
||||
)
|
||||
|
||||
func main() {
|
||||
cmd.Execute()
|
||||
cli.Execute()
|
||||
}
|
||||
|
||||
@@ -273,7 +273,7 @@ if [ "$DEV_BIND_PLANES" = "1" ]; then
|
||||
# Pinning --bind moves every service rendered as {{.BindIP}} onto the lan
|
||||
# plane. awsgw and predastore must be on the wildcard first, or the public
|
||||
# AWS API and S3 endpoint leave the wan plane with them.
|
||||
TEMPLATE="$PROJECT_ROOT/cmd/spinifex/cmd/templates/spinifex.toml"
|
||||
TEMPLATE="$PROJECT_ROOT/spinifex/operator/cli/templates/spinifex.toml"
|
||||
if [ -f "$TEMPLATE" ] && ! grep -q '^host = "0.0.0.0:9999"' "$TEMPLATE"; then
|
||||
fail "DEV_BIND_PLANES=1 but $TEMPLATE still renders awsgw on {{.BindIP}}. This branch predates the wildcard public listeners — rebase, or set DEV_BIND_PLANES=0."
|
||||
fi
|
||||
|
||||
@@ -93,10 +93,13 @@ func listenerBindScanFiles(t *testing.T, root string) []string {
|
||||
filepath.Join(root, "scripts", "install-node.sh"),
|
||||
filepath.Join(root, "scripts", "setup.sh"),
|
||||
}
|
||||
tomls, err := filepath.Glob(filepath.Join(root, "cmd", "spinifex", "cmd", "templates", "*.toml"))
|
||||
tomls, err := filepath.Glob(filepath.Join(root, "spinifex", "operator", "cli", "templates", "*.toml"))
|
||||
if err != nil {
|
||||
t.Fatalf("glob templates: %v", err)
|
||||
}
|
||||
if len(tomls) == 0 {
|
||||
t.Fatal("listener bind scan found no config templates")
|
||||
}
|
||||
sort.Strings(tomls)
|
||||
files = append(files, tomls...)
|
||||
for _, f := range files {
|
||||
|
||||
@@ -29,7 +29,7 @@ const bedrockAgentTestAccount = "111111111111"
|
||||
|
||||
// fakeBedrockAgentVectorService serves scripted responses so bedrockagent.go's
|
||||
// mapping logic can be exercised without a daemon or a live NATS connection,
|
||||
// mirroring cmd/spinifex/cmd's fakeVectorService for this package.
|
||||
// mirroring operator/cli's fakeVectorService for this package.
|
||||
type fakeBedrockAgentVectorService struct {
|
||||
createIndexReq *ochrevector.CreateIndexRequest
|
||||
createIndexErr error
|
||||
|
||||
@@ -4,11 +4,11 @@ Single source of truth for the current schema version of every config file Spini
|
||||
|
||||
| Target | Current version | Canonical template |
|
||||
|---|---|---|
|
||||
| `nats.conf` | `3` | `cmd/spinifex/cmd/templates/nats.conf` |
|
||||
| `awsgw.toml` | `3` | `cmd/spinifex/cmd/templates/awsgw.toml` |
|
||||
| `spinifex.toml` | `4` | `cmd/spinifex/cmd/templates/spinifex.toml` |
|
||||
| `predastore.toml` | `1` | `cmd/spinifex/cmd/templates/predastore.toml` |
|
||||
| `predastore-multinode.toml` | `1` | `cmd/spinifex/cmd/templates/predastore-multinode.toml` |
|
||||
| `nats.conf` | `3` | `spinifex/operator/cli/templates/nats.conf` |
|
||||
| `awsgw.toml` | `3` | `spinifex/operator/cli/templates/awsgw.toml` |
|
||||
| `spinifex.toml` | `4` | `spinifex/operator/cli/templates/spinifex.toml` |
|
||||
| `predastore.toml` | `1` | `spinifex/operator/cli/templates/predastore.toml` |
|
||||
| `predastore-multinode.toml` | `1` | `spinifex/operator/cli/templates/predastore-multinode.toml` |
|
||||
|
||||
## How versions are stamped
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package cmd
|
||||
package cli
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
@@ -365,7 +365,7 @@ func init() {
|
||||
certRenewCmd.Flags().StringSlice("extra-ip", nil, "Additional IP addresses to include in SANs")
|
||||
certRenewCmd.Flags().StringSlice("extra-dns", nil, "Additional DNS names to include in SANs")
|
||||
accountCreateCmd.Flags().String("name", "", "Account name (required)")
|
||||
accountCreateCmd.MarkFlagRequired("name")
|
||||
mustMarkFlagRequired(accountCreateCmd, "name")
|
||||
|
||||
rootCmd.PersistentFlags().String("config-dir", DefaultConfigDir(), "Configuration directory")
|
||||
rootCmd.PersistentFlags().String("spinifex-dir", DefaultDataDir(), "Spinifex base directory")
|
||||
@@ -424,9 +424,9 @@ func init() {
|
||||
adminJoinCmd.Flags().String("email", "", "Operator email address (used for update and security notifications)")
|
||||
adminJoinCmd.Flags().Bool("skip-host-dns", false, "Do not point this node's host resolver at its local northstar (LB/EKS names then won't resolve from the node)")
|
||||
adminJoinCmd.Flags().Int("predastore-compaction-interval", 0, "Predastore compactor interval in seconds (0 = unset, uses built-in default). Test clusters set a short interval.")
|
||||
adminJoinCmd.MarkFlagRequired("node")
|
||||
adminJoinCmd.MarkFlagRequired("host")
|
||||
adminJoinCmd.MarkFlagRequired("token")
|
||||
mustMarkFlagRequired(adminJoinCmd, "node")
|
||||
mustMarkFlagRequired(adminJoinCmd, "host")
|
||||
mustMarkFlagRequired(adminJoinCmd, "token")
|
||||
|
||||
imagesImportCmd.Flags().String("tmp-dir", os.TempDir(), "Temporary directory for image import processing")
|
||||
|
||||
@@ -446,14 +446,14 @@ func init() {
|
||||
imagesRemoveCmd.Flags().String("image-id", "", "AMI ID to remove (required)")
|
||||
imagesRemoveCmd.Flags().Bool("force", false, "Bypass dependency, ownership and config-corrupt checks (salvage mode)")
|
||||
imagesRemoveCmd.Flags().Bool("yes", false, "Skip interactive confirmation prompt")
|
||||
_ = imagesRemoveCmd.MarkFlagRequired("image-id")
|
||||
mustMarkFlagRequired(imagesRemoveCmd, "image-id")
|
||||
|
||||
imagesPromoteCmd.Flags().String("image-id", "", "AMI ID to promote to system image (required)")
|
||||
imagesPromoteCmd.Flags().Bool("yes", false, "Skip interactive confirmation prompt")
|
||||
_ = imagesPromoteCmd.MarkFlagRequired("image-id")
|
||||
mustMarkFlagRequired(imagesPromoteCmd, "image-id")
|
||||
|
||||
imagesDescribeCmd.Flags().String("image-id", "", "AMI ID to describe (required)")
|
||||
_ = imagesDescribeCmd.MarkFlagRequired("image-id")
|
||||
mustMarkFlagRequired(imagesDescribeCmd, "image-id")
|
||||
}
|
||||
|
||||
const bytesPerGiB = 1024 * 1024 * 1024
|
||||
@@ -718,7 +718,7 @@ func runimagesImportCmd(cmd *cobra.Command, args []string) {
|
||||
// 0770, matching the root:spinifex convention setup.sh gives the shared data
|
||||
// dirs. At 0700 the first sudo import leaves a directory no group member can
|
||||
// descend into, so every later import without sudo fails on mkdir.
|
||||
if err := os.MkdirAll(imagePath, 0770); err != nil {
|
||||
if err := os.MkdirAll(imagePath, 0770); err != nil { //nolint:gosec // G301: shared image directory is group-writable (0770) so later imports by other spinifex group members can create entries; it is not world-accessible.
|
||||
fmt.Fprintf(os.Stderr, "Error creating image directory %s: %v\n", imagePath, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
@@ -1206,10 +1206,15 @@ func runimagesListCmd(cmd *cobra.Command, args []string) {
|
||||
tableData = append(tableData, []string{img.Name, img.Distro, img.Version, img.Arch, img.BootMode})
|
||||
}
|
||||
|
||||
// Create a table with the defined data.
|
||||
// The table has a header and the text in the cells is right-aligned.
|
||||
// The Render() method is used to print the table to the console.
|
||||
pterm.DefaultTable.WithHasHeader().WithLeftAlignment().WithData(tableData).Render()
|
||||
// Render discards its write error, so write the table here to fail on it.
|
||||
rendered, err := pterm.DefaultTable.WithHasHeader().WithLeftAlignment().WithData(tableData).Srender()
|
||||
if err == nil {
|
||||
_, err = fmt.Fprintln(os.Stdout, rendered)
|
||||
}
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error: render table: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
pterm.Println("To install a selected image as an AMI use:")
|
||||
|
||||
@@ -2062,7 +2067,7 @@ func runAdminInitMultiNode(cmd *cobra.Command, accessKey, secretKey, accountID,
|
||||
// Wait for all nodes to register
|
||||
if err := fs.WaitForCompletion(formationTimeout); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "❌ %v\n", err)
|
||||
fs.Shutdown(context.Background())
|
||||
_ = fs.Shutdown(context.Background())
|
||||
closeFormationPort()
|
||||
os.Remove(tokenPath)
|
||||
os.Exit(1)
|
||||
@@ -2193,7 +2198,7 @@ func runAdminInitMultiNode(cmd *cobra.Command, accessKey, secretKey, accountID,
|
||||
time.Sleep(15 * time.Second)
|
||||
|
||||
// Shutdown formation server
|
||||
fs.Shutdown(context.Background())
|
||||
_ = fs.Shutdown(context.Background())
|
||||
closeFormationPort()
|
||||
if err := os.Remove(tokenPath); err != nil && !errors.Is(err, os.ErrNotExist) {
|
||||
slog.Warn("Failed to remove join token file", "path", tokenPath, "error", err)
|
||||
@@ -2394,7 +2399,7 @@ func runAdminJoin(cmd *cobra.Command, args []string) {
|
||||
client := &http.Client{
|
||||
Timeout: 30 * time.Second,
|
||||
Transport: &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, // formation server uses ephemeral self-signed cert
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, //nolint:gosec // G402: KNOWN SECURITY GAP, not a false positive. The formation trust bootstrap does not authenticate the server, so the join token and returned cluster secrets are exposed to interception. Remove once formation TLS is authenticated by a pinned key/certificate or an out-of-band CA.
|
||||
},
|
||||
}
|
||||
|
||||
@@ -3657,12 +3662,12 @@ func installCACertificate(caPemPath string) {
|
||||
return
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(filepath.Dir(systemCertPath), 0755); err != nil {
|
||||
if err := os.MkdirAll(filepath.Dir(systemCertPath), 0755); err != nil { //nolint:gosec // G301: system CA trust-store directory must be 0755 for every TLS client to read it. Contains no secret material.
|
||||
fmt.Fprintf(os.Stderr, "Warning: could not create certificate directory: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := os.WriteFile(systemCertPath, data, 0644); err != nil {
|
||||
if err := os.WriteFile(systemCertPath, data, 0644); err != nil { //nolint:gosec // G306: public CA certificate in the system trust store must be world-readable (0644); no private key is written here.
|
||||
fmt.Fprintf(os.Stderr, "Warning: could not install CA certificate: %v\n", err)
|
||||
return
|
||||
}
|
||||
@@ -3757,7 +3762,7 @@ func runAdminBanner(cmd *cobra.Command, _ []string) {
|
||||
|
||||
// Write to /etc/issue — displayed on the console before the login prompt.
|
||||
// Overwrite entirely; this is a purpose-built appliance so we own this file.
|
||||
if err := os.WriteFile("/etc/issue", []byte(banner), 0o644); err != nil {
|
||||
if err := os.WriteFile("/etc/issue", []byte(banner), 0o644); err != nil { //nolint:gosec // G306: /etc/issue is shown before login and must be world-readable (0644). Contains no secret material.
|
||||
slog.Warn("Failed to write /etc/issue", "err", err)
|
||||
}
|
||||
|
||||
@@ -3791,7 +3796,7 @@ func appendBannerToMotd(banner string) error {
|
||||
base += "\n\n"
|
||||
}
|
||||
}
|
||||
return os.WriteFile(motdPath, []byte(base+sentinel+banner), 0o644)
|
||||
return os.WriteFile(motdPath, []byte(base+sentinel+banner), 0o644) //nolint:gosec // G306: motd is shown at login and must be world-readable (0644). Contains no secret material.
|
||||
}
|
||||
|
||||
// gpuBannerSection returns an optional banner box section describing GPU state.
|
||||
@@ -3913,7 +3918,7 @@ func writeNodeConf(path string, conf map[string]string) error {
|
||||
fmt.Fprintf(&b, "%s=%s\n", k, v)
|
||||
}
|
||||
}
|
||||
return os.WriteFile(path, []byte(b.String()), 0o644)
|
||||
return os.WriteFile(path, []byte(b.String()), 0o644) //nolint:gosec // G306: node.conf holds the management IP, interface and hostname, read by non-root Spinifex services (0644). Contains no secret material.
|
||||
}
|
||||
|
||||
// resolveIfaceIP returns the first IPv4 address assigned to iface, or "".
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package cmd
|
||||
package cli
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
+7
-7
@@ -1,4 +1,4 @@
|
||||
package cmd_test
|
||||
package cli_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
@@ -6,8 +6,8 @@ import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/mulgadc/spinifex/cmd/spinifex/cmd"
|
||||
"github.com/mulgadc/spinifex/spinifex/accountteardown"
|
||||
"github.com/mulgadc/spinifex/spinifex/operator/cli"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
@@ -75,7 +75,7 @@ func teardownPlan() *accountteardown.Result {
|
||||
// The plan is what the operator confirms against, so every resource has to
|
||||
// appear under its stage — and an empty stage must not pad the list.
|
||||
func TestPrintTeardownPlanListsEveryResource(t *testing.T) {
|
||||
output := captureOutput(t, func() { cmd.PrintTeardownPlan(teardownPlan()) })
|
||||
output := captureOutput(t, func() { cli.PrintTeardownPlan(teardownPlan()) })
|
||||
|
||||
assert.Contains(t, output, "000000000042")
|
||||
assert.Contains(t, output, "tenant@example.com")
|
||||
@@ -90,7 +90,7 @@ func TestPrintTeardownPlanListsEveryResource(t *testing.T) {
|
||||
// output, which is exactly when an operator stops and asks.
|
||||
func TestPrintTeardownPlanSaysWhenTheAccountIsEmpty(t *testing.T) {
|
||||
output := captureOutput(t, func() {
|
||||
cmd.PrintTeardownPlan(&accountteardown.Result{
|
||||
cli.PrintTeardownPlan(&accountteardown.Result{
|
||||
AccountID: "000000000042",
|
||||
AccountName: "tenant@example.com",
|
||||
Stages: []accountteardown.StageResult{{Stage: accountteardown.StageCompute, Elapsed: "0s"}},
|
||||
@@ -123,7 +123,7 @@ func TestPrintTeardownResultNamesStuckResources(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
output := captureOutput(t, func() { cmd.PrintTeardownResult(result) })
|
||||
output := captureOutput(t, func() { cli.PrintTeardownResult(result) })
|
||||
|
||||
assert.Contains(t, output, "1 deleted, 0 stuck")
|
||||
assert.Contains(t, output, "STUCK")
|
||||
@@ -140,7 +140,7 @@ func TestPromptAccountNameReadsTheConfirmation(t *testing.T) {
|
||||
|
||||
captureOutput(t, func() {
|
||||
feedStdin(t, " tenant@example.com \n", func() {
|
||||
name, err = cmd.PromptAccountName("000000000042")
|
||||
name, err = cli.PromptAccountName("000000000042")
|
||||
})
|
||||
})
|
||||
|
||||
@@ -153,7 +153,7 @@ func TestPromptAccountNameTreatsAnEmptyReplyAsCancelled(t *testing.T) {
|
||||
|
||||
captureOutput(t, func() {
|
||||
feedStdin(t, "\n", func() {
|
||||
_, err = cmd.PromptAccountName("000000000042")
|
||||
_, err = cli.PromptAccountName("000000000042")
|
||||
})
|
||||
})
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package cmd
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
+23
-23
@@ -1,4 +1,4 @@
|
||||
package cmd_test
|
||||
package cli_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -9,8 +9,8 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/mulgadc/spinifex/cmd/spinifex/cmd"
|
||||
"github.com/mulgadc/spinifex/spinifex/gateway"
|
||||
"github.com/mulgadc/spinifex/spinifex/operator/cli"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
@@ -51,8 +51,8 @@ func TestDeleteAccountRemoteSignsAndPosts(t *testing.T) {
|
||||
srv := newAdminTestServer(t, http.StatusOK,
|
||||
`{"deletionId":"d-1","accountId":"000000000042","state":"RUNNING"}`, &got)
|
||||
|
||||
out, err := cmd.DeleteAccountRemote(t.Context(),
|
||||
cmd.AdminTarget(srv.URL, "us-west-1", writeServerCA(t, srv)),
|
||||
out, err := cli.DeleteAccountRemote(t.Context(),
|
||||
cli.AdminTarget(srv.URL, "us-west-1", writeServerCA(t, srv)),
|
||||
gateway.DeleteAccountRequest{
|
||||
AccountID: "000000000042",
|
||||
AccountName: "tenant@example.com",
|
||||
@@ -78,8 +78,8 @@ func TestDeleteAccountRemoteDryRunCarriesTheInventory(t *testing.T) {
|
||||
`{"accountId":"000000000042","state":"DRY_RUN","dryRun":true,"inventory":{"accountId":"000000000042",`+
|
||||
`"dryRun":true,"stages":[{"stage":1,"deleted":[{"kind":"instance","id":"i-1"}],"elapsed":"0s"}]}}`, &got)
|
||||
|
||||
out, err := cmd.DeleteAccountRemote(t.Context(),
|
||||
cmd.AdminTarget(srv.URL, "us-west-1", writeServerCA(t, srv)),
|
||||
out, err := cli.DeleteAccountRemote(t.Context(),
|
||||
cli.AdminTarget(srv.URL, "us-west-1", writeServerCA(t, srv)),
|
||||
gateway.DeleteAccountRequest{AccountID: "000000000042", DryRun: true})
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -97,8 +97,8 @@ func TestDeleteAccountRemoteSurfacesGatewayError(t *testing.T) {
|
||||
srv := newAdminTestServer(t, http.StatusForbidden,
|
||||
`{"error":{"code":"AccessDenied","message":"denied"},"requestId":"req-9"}`, &got)
|
||||
|
||||
_, err := cmd.DeleteAccountRemote(t.Context(),
|
||||
cmd.AdminTarget(srv.URL, "us-west-1", writeServerCA(t, srv)),
|
||||
_, err := cli.DeleteAccountRemote(t.Context(),
|
||||
cli.AdminTarget(srv.URL, "us-west-1", writeServerCA(t, srv)),
|
||||
gateway.DeleteAccountRequest{
|
||||
AccountID: "000000000001",
|
||||
AccountName: "spinifex",
|
||||
@@ -108,7 +108,7 @@ func TestDeleteAccountRemoteSurfacesGatewayError(t *testing.T) {
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "AccessDenied")
|
||||
assert.Contains(t, err.Error(), "req-9")
|
||||
assert.False(t, cmd.RetryableAdminError(err), "a protected account is not fixed by retrying")
|
||||
assert.False(t, cli.RetryableAdminError(err), "a protected account is not fixed by retrying")
|
||||
}
|
||||
|
||||
func TestDescribeAccountDeletionRemoteReportsProgress(t *testing.T) {
|
||||
@@ -118,8 +118,8 @@ func TestDescribeAccountDeletionRemoteReportsProgress(t *testing.T) {
|
||||
srv := newAdminTestServer(t, http.StatusOK,
|
||||
`{"deletionId":"d-1","state":"RUNNING","stages":[{"stage":1,"elapsed":"5s"}]}`, &got)
|
||||
|
||||
out, err := cmd.DescribeAccountDeletionRemote(t.Context(),
|
||||
cmd.AdminTarget(srv.URL, "us-west-1", writeServerCA(t, srv)), "000000000042")
|
||||
out, err := cli.DescribeAccountDeletionRemote(t.Context(),
|
||||
cli.AdminTarget(srv.URL, "us-west-1", writeServerCA(t, srv)), "000000000042")
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "/admin/DescribeAccountDeletion", got.URL.Path)
|
||||
@@ -132,7 +132,7 @@ func TestDescribeAccountDeletionRemoteReportsProgress(t *testing.T) {
|
||||
// terminal state.
|
||||
func TestFollowAccountDeletionPollsUntilComplete(t *testing.T) {
|
||||
adminTestCredentials(t)
|
||||
defer cmd.SetAccountDeletePollInterval(time.Millisecond)()
|
||||
defer cli.SetAccountDeletePollInterval(time.Millisecond)()
|
||||
|
||||
srv, calls := newScriptedAdminServer(t,
|
||||
`{"deletionId":"d-1","state":"RUNNING","stages":[{"stage":1,"elapsed":"5s"}]}`,
|
||||
@@ -140,8 +140,8 @@ func TestFollowAccountDeletionPollsUntilComplete(t *testing.T) {
|
||||
`{"deletionId":"d-1","state":"COMPLETED","stages":[{"stage":1,"elapsed":"5s"},{"stage":2,"elapsed":"1s"}]}`,
|
||||
)
|
||||
|
||||
err := cmd.FollowAccountDeletion(t.Context(),
|
||||
cmd.AdminTarget(srv.URL, "us-west-1", writeServerCA(t, srv)), "000000000042")
|
||||
err := cli.FollowAccountDeletion(t.Context(),
|
||||
cli.AdminTarget(srv.URL, "us-west-1", writeServerCA(t, srv)), "000000000042")
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, int32(3), calls.Load(), "the loop must keep polling while the job runs")
|
||||
@@ -151,13 +151,13 @@ func TestFollowAccountDeletionPollsUntilComplete(t *testing.T) {
|
||||
// left TERMINATING, and an operator who reads "done" would not go looking.
|
||||
func TestFollowAccountDeletionReportsFailure(t *testing.T) {
|
||||
adminTestCredentials(t)
|
||||
defer cmd.SetAccountDeletePollInterval(time.Millisecond)()
|
||||
defer cli.SetAccountDeletePollInterval(time.Millisecond)()
|
||||
|
||||
srv, _ := newScriptedAdminServer(t,
|
||||
`{"deletionId":"d-1","state":"FAILED","stages":[],"error":"2 resources left"}`)
|
||||
|
||||
err := cmd.FollowAccountDeletion(t.Context(),
|
||||
cmd.AdminTarget(srv.URL, "us-west-1", writeServerCA(t, srv)), "000000000042")
|
||||
err := cli.FollowAccountDeletion(t.Context(),
|
||||
cli.AdminTarget(srv.URL, "us-west-1", writeServerCA(t, srv)), "000000000042")
|
||||
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "2 resources left")
|
||||
@@ -167,15 +167,15 @@ func TestFollowAccountDeletionReportsFailure(t *testing.T) {
|
||||
// message has to name the account the caller must go back to.
|
||||
func TestFollowAccountDeletionStopsWhenTheContextEnds(t *testing.T) {
|
||||
adminTestCredentials(t)
|
||||
defer cmd.SetAccountDeletePollInterval(50 * time.Millisecond)()
|
||||
defer cli.SetAccountDeletePollInterval(50 * time.Millisecond)()
|
||||
|
||||
srv, _ := newScriptedAdminServer(t, `{"deletionId":"d-1","state":"RUNNING","stages":[]}`)
|
||||
|
||||
ctx, cancel := context.WithTimeout(t.Context(), 100*time.Millisecond)
|
||||
defer cancel()
|
||||
|
||||
err := cmd.FollowAccountDeletion(ctx,
|
||||
cmd.AdminTarget(srv.URL, "us-west-1", writeServerCA(t, srv)), "000000000042")
|
||||
err := cli.FollowAccountDeletion(ctx,
|
||||
cli.AdminTarget(srv.URL, "us-west-1", writeServerCA(t, srv)), "000000000042")
|
||||
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "000000000042")
|
||||
@@ -185,14 +185,14 @@ func TestFollowAccountDeletionStopsWhenTheContextEnds(t *testing.T) {
|
||||
// a teardown still holding an account's resources.
|
||||
func TestFollowAccountDeletionFailsOnAnUnreadableJob(t *testing.T) {
|
||||
adminTestCredentials(t)
|
||||
defer cmd.SetAccountDeletePollInterval(time.Millisecond)()
|
||||
defer cli.SetAccountDeletePollInterval(time.Millisecond)()
|
||||
|
||||
var got http.Request
|
||||
srv := newAdminTestServer(t, http.StatusNotFound,
|
||||
`{"error":{"code":"NoSuchEntity","message":"no job"},"requestId":"req-2"}`, &got)
|
||||
|
||||
err := cmd.FollowAccountDeletion(t.Context(),
|
||||
cmd.AdminTarget(srv.URL, "us-west-1", writeServerCA(t, srv)), "000000000042")
|
||||
err := cli.FollowAccountDeletion(t.Context(),
|
||||
cli.AdminTarget(srv.URL, "us-west-1", writeServerCA(t, srv)), "000000000042")
|
||||
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "NoSuchEntity")
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package cmd
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package cmd
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
//test:in-package — drives the unexported flag table, parser and printer the quota subcommands are built from.
|
||||
package cmd
|
||||
package cli
|
||||
|
||||
import (
|
||||
"testing"
|
||||
@@ -1,4 +1,4 @@
|
||||
package cmd
|
||||
package cli
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
@@ -60,12 +60,12 @@ func init() {
|
||||
certCreateTenantCACmd.Flags().StringSlice("domain", nil, "Domain the tenant CA is permitted to sign for (repeatable)")
|
||||
certCreateTenantCACmd.Flags().Bool("regenerate", false, "Replace an existing tenant CA with a fresh root (invalidates every device's trust store — requires confirmation)")
|
||||
certCreateTenantCACmd.Flags().Bool("yes", false, "Skip the --regenerate confirmation prompt")
|
||||
_ = certCreateTenantCACmd.MarkFlagRequired("domain")
|
||||
mustMarkFlagRequired(certCreateTenantCACmd, "domain")
|
||||
|
||||
certCmd.AddCommand(certForceRenewCmd)
|
||||
certForceRenewCmd.Flags().String("arn", "", "Certificate ARN to force-renew (required)")
|
||||
certForceRenewCmd.Flags().String("account", "", "Account ID that owns the certificate; defaults to the bootstrap account")
|
||||
_ = certForceRenewCmd.MarkFlagRequired("arn")
|
||||
mustMarkFlagRequired(certForceRenewCmd, "arn")
|
||||
}
|
||||
|
||||
func runCertForceRenew(cmd *cobra.Command, _ []string) {
|
||||
@@ -1,4 +1,4 @@
|
||||
package cmd
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -38,7 +38,7 @@ func init() {
|
||||
adminEksRestoreSnapshotCmd.Flags().String("cluster", "", "Cluster name to restore (required)")
|
||||
adminEksRestoreSnapshotCmd.Flags().String("snapshot", "", "Etcd snapshot key to restore; defaults to the latest snapshot found in predastore")
|
||||
adminEksRestoreSnapshotCmd.Flags().String("account", "", "Account ID that owns the cluster; defaults to the bootstrap account")
|
||||
_ = adminEksRestoreSnapshotCmd.MarkFlagRequired("cluster")
|
||||
mustMarkFlagRequired(adminEksRestoreSnapshotCmd, "cluster")
|
||||
}
|
||||
|
||||
func runAdminEksRestoreSnapshot(cmd *cobra.Command, _ []string) {
|
||||
@@ -1,4 +1,4 @@
|
||||
package cmd
|
||||
package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
+7
-7
@@ -1,4 +1,4 @@
|
||||
package cmd_test
|
||||
package cli_test
|
||||
|
||||
import (
|
||||
"os"
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/mulgadc/spinifex/cmd/spinifex/cmd"
|
||||
"github.com/mulgadc/spinifex/spinifex/operator/cli"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
@@ -20,7 +20,7 @@ func stubFirewallHelper(t *testing.T, body string) string {
|
||||
helper := filepath.Join(dir, "spinifex-firewall-apply")
|
||||
script := "#!/bin/sh\necho \"$*\" >> " + log + "\n" + body
|
||||
require.NoError(t, os.WriteFile(helper, []byte(script), 0o755))
|
||||
t.Cleanup(cmd.SetFirewallApplyHelper(helper))
|
||||
t.Cleanup(cli.SetFirewallApplyHelper(helper))
|
||||
return log
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ func helperCalls(t *testing.T, log string) []string {
|
||||
func TestOpenFormationPort(t *testing.T) {
|
||||
log := stubFirewallHelper(t, "exit 0\n")
|
||||
|
||||
closePort := cmd.OpenFormationPort(4432)
|
||||
closePort := cli.OpenFormationPort(4432)
|
||||
assert.Equal(t, []string{"open-port", "4432"}, helperCalls(t, log))
|
||||
|
||||
closePort()
|
||||
@@ -48,9 +48,9 @@ func TestOpenFormationPort(t *testing.T) {
|
||||
// A machine with no policy installed has nothing to open, and formation must not
|
||||
// be held up by looking for a helper that was never installed.
|
||||
func TestOpenFormationPort_NoHelperInstalled(t *testing.T) {
|
||||
t.Cleanup(cmd.SetFirewallApplyHelper(filepath.Join(t.TempDir(), "absent")))
|
||||
t.Cleanup(cli.SetFirewallApplyHelper(filepath.Join(t.TempDir(), "absent")))
|
||||
|
||||
closePort := cmd.OpenFormationPort(4432)
|
||||
closePort := cli.OpenFormationPort(4432)
|
||||
require.NotNil(t, closePort)
|
||||
closePort()
|
||||
}
|
||||
@@ -61,7 +61,7 @@ func TestOpenFormationPort_NoHelperInstalled(t *testing.T) {
|
||||
func TestOpenFormationPort_FailureDoesNotBlockFormation(t *testing.T) {
|
||||
log := stubFirewallHelper(t, "exit 1\n")
|
||||
|
||||
closePort := cmd.OpenFormationPort(4432)
|
||||
closePort := cli.OpenFormationPort(4432)
|
||||
require.NotNil(t, closePort)
|
||||
closePort()
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package cmd
|
||||
package cli
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
@@ -329,7 +329,7 @@ func runAdminGpuSetup(_ *cobra.Command, _ []string) {
|
||||
const vfioUdevRule = "/etc/udev/rules.d/99-spinifex-vfio.rules"
|
||||
if _, err := os.Stat(vfioUdevRule); os.IsNotExist(err) {
|
||||
rule := "SUBSYSTEM==\"vfio\", GROUP=\"spinifex\", MODE=\"0660\"\n"
|
||||
if err := os.WriteFile(vfioUdevRule, []byte(rule), 0o644); err != nil {
|
||||
if err := os.WriteFile(vfioUdevRule, []byte(rule), 0o644); err != nil { //nolint:gosec // G306: udev rule granting the spinifex group VFIO access is conventionally world-readable (0644). Contains no secret material.
|
||||
fmt.Fprintf(os.Stderr, "Failed to write vfio udev rule: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
@@ -343,7 +343,7 @@ func runAdminGpuSetup(_ *cobra.Command, _ []string) {
|
||||
fmt.Println("==> Configuring nouveau blacklist")
|
||||
const nouveauConf = "/etc/modprobe.d/blacklist-nouveau.conf"
|
||||
if _, err := os.Stat(nouveauConf); os.IsNotExist(err) {
|
||||
if err := os.WriteFile(nouveauConf, []byte("blacklist nouveau\noptions nouveau modeset=0\n"), 0o644); err != nil {
|
||||
if err := os.WriteFile(nouveauConf, []byte("blacklist nouveau\noptions nouveau modeset=0\n"), 0o644); err != nil { //nolint:gosec // G306: modprobe.d blacklist is conventionally world-readable (0644). Contains no secret material.
|
||||
fmt.Fprintf(os.Stderr, "Failed to write nouveau blacklist: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
@@ -364,7 +364,7 @@ func runAdminGpuSetup(_ *cobra.Command, _ []string) {
|
||||
}
|
||||
if hasAMD {
|
||||
if _, err := os.Stat(amdgpuConf); os.IsNotExist(err) {
|
||||
if err := os.WriteFile(amdgpuConf, []byte("blacklist amdgpu\n"), 0o644); err != nil {
|
||||
if err := os.WriteFile(amdgpuConf, []byte("blacklist amdgpu\n"), 0o644); err != nil { //nolint:gosec // G306: modprobe.d blacklist is conventionally world-readable (0644). Contains no secret material.
|
||||
fmt.Fprintf(os.Stderr, "Failed to write amdgpu blacklist: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
@@ -381,7 +381,7 @@ func runAdminGpuSetup(_ *cobra.Command, _ []string) {
|
||||
const vfioPCIConf = "/etc/modprobe.d/vfio-pci.conf"
|
||||
existing, _ := os.ReadFile(vfioPCIConf)
|
||||
if !strings.Contains(string(existing), "ids="+idsCSV) {
|
||||
if err := os.WriteFile(vfioPCIConf, []byte("options vfio-pci ids="+idsCSV+"\n"), 0o644); err != nil {
|
||||
if err := os.WriteFile(vfioPCIConf, []byte("options vfio-pci ids="+idsCSV+"\n"), 0o644); err != nil { //nolint:gosec // G306: modprobe.d vfio-pci options are conventionally world-readable (0644). Contains no secret material.
|
||||
fmt.Fprintf(os.Stderr, "Failed to write vfio-pci.conf: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
@@ -394,7 +394,7 @@ func runAdminGpuSetup(_ *cobra.Command, _ []string) {
|
||||
const initramfsModules = "/etc/initramfs-tools/modules"
|
||||
modData, _ := os.ReadFile(initramfsModules)
|
||||
if !strings.Contains(string(modData), "vfio_pci") {
|
||||
f, err := os.OpenFile(initramfsModules, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0o644)
|
||||
f, err := os.OpenFile(initramfsModules, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0o644) //nolint:gosec // G302: initramfs modules list is conventionally world-readable (0644). Contains no secret material.
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Failed to open initramfs modules: %v\n", err)
|
||||
os.Exit(1)
|
||||
@@ -796,7 +796,7 @@ func gpuSetupAddGRUBParams(params string) (bool, error) {
|
||||
if updated == content {
|
||||
return false, fmt.Errorf("GRUB_CMDLINE_LINUX_DEFAULT not found in %s", grubFile)
|
||||
}
|
||||
if err := os.WriteFile(grubFile, []byte(updated), 0o644); err != nil {
|
||||
if err := os.WriteFile(grubFile, []byte(updated), 0o644); err != nil { //nolint:gosec // G306: /etc/default/grub is read by grub-mkconfig and is conventionally 0644. Contains no secret material.
|
||||
return false, fmt.Errorf("write %s: %w", grubFile, err)
|
||||
}
|
||||
return true, nil
|
||||
@@ -1,4 +1,4 @@
|
||||
package cmd
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package cmd
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -1,4 +1,4 @@
|
||||
package cmd
|
||||
package cli
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
@@ -1,4 +1,4 @@
|
||||
package cmd
|
||||
package cli
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
+24
-24
@@ -1,4 +1,4 @@
|
||||
package cmd_test
|
||||
package cli_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/mulgadc/spinifex/cmd/spinifex/cmd"
|
||||
"github.com/mulgadc/spinifex/spinifex/operator/cli"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
@@ -80,7 +80,7 @@ func kvs(msgs ...msg) []msg { return msgs }
|
||||
func TestKVCompareIdentical(t *testing.T) {
|
||||
s := stream{name: "KV_a", replicas: 3, msgs: kvs(msg{1, "k", "h1", t0}, msg{2, "k", "h2", t0})}
|
||||
// The digest field embeds the host, so this exercises the per-seq path too.
|
||||
divergent, report, err := cmd.CompareKVDigests(doc(t, "n1", t0, s), doc(t, "n2", t0, s), doc(t, "n3", t0, s))
|
||||
divergent, report, err := cli.CompareKVDigests(doc(t, "n1", t0, s), doc(t, "n2", t0, s), doc(t, "n3", t0, s))
|
||||
require.NoError(t, err)
|
||||
assert.False(t, divergent, report)
|
||||
assert.Contains(t, report, "1 streams: 1 consistent, 0 diverged")
|
||||
@@ -91,7 +91,7 @@ func TestKVCompareIdentical(t *testing.T) {
|
||||
func TestKVCompareSameSeqDifferentContent(t *testing.T) {
|
||||
a := stream{name: "KV_acct", replicas: 2, msgs: kvs(msg{1, "$KV.acct.000000000002", "aaaa", t0})}
|
||||
b := stream{name: "KV_acct", replicas: 2, msgs: kvs(msg{1, "$KV.acct.000000000002", "bbbb", t0})}
|
||||
divergent, report, err := cmd.CompareKVDigests(doc(t, "node1", t0, a), doc(t, "node2", t0, b))
|
||||
divergent, report, err := cli.CompareKVDigests(doc(t, "node1", t0, a), doc(t, "node2", t0, b))
|
||||
require.NoError(t, err)
|
||||
assert.True(t, divergent)
|
||||
assert.Contains(t, report, "DIVERGED KV_acct")
|
||||
@@ -104,7 +104,7 @@ func TestKVCompareMissingMessage(t *testing.T) {
|
||||
t.Run("superseded by a later write to the subject", func(t *testing.T) {
|
||||
lagging := stream{name: "KV_a", replicas: 2, msgs: full}
|
||||
purged := stream{name: "KV_a", replicas: 2, msgs: kvs(msg{1, "", "deleted", time.Time{}}, full[1], full[2])}
|
||||
divergent, report, err := cmd.CompareKVDigests(doc(t, "n1", t0, lagging), doc(t, "n2", t0, purged))
|
||||
divergent, report, err := cli.CompareKVDigests(doc(t, "n1", t0, lagging), doc(t, "n2", t0, purged))
|
||||
require.NoError(t, err)
|
||||
assert.False(t, divergent, report)
|
||||
assert.Contains(t, report, "1 superseded or expired")
|
||||
@@ -113,7 +113,7 @@ func TestKVCompareMissingMessage(t *testing.T) {
|
||||
t.Run("nothing explains it", func(t *testing.T) {
|
||||
have := stream{name: "KV_a", replicas: 2, msgs: full}
|
||||
lost := stream{name: "KV_a", replicas: 2, msgs: kvs(full[0], msg{2, "", "deleted", time.Time{}}, full[2])}
|
||||
divergent, report, err := cmd.CompareKVDigests(doc(t, "n1", t0, have), doc(t, "n2", t0, lost))
|
||||
divergent, report, err := cli.CompareKVDigests(doc(t, "n1", t0, have), doc(t, "n2", t0, lost))
|
||||
require.NoError(t, err)
|
||||
assert.True(t, divergent)
|
||||
assert.Contains(t, report, "seq 2 j is on n1 but missing from n2")
|
||||
@@ -123,12 +123,12 @@ func TestKVCompareMissingMessage(t *testing.T) {
|
||||
old := t0.Add(-time.Hour)
|
||||
have := stream{name: "KV_lease", replicas: 2, maxAge: time.Minute, msgs: kvs(msg{1, "l", "h1", old}, msg{2, "m", "h2", t0})}
|
||||
gone := stream{name: "KV_lease", replicas: 2, maxAge: time.Minute, msgs: kvs(msg{2, "m", "h2", t0})}
|
||||
divergent, report, err := cmd.CompareKVDigests(doc(t, "n1", t0, have), doc(t, "n2", t0, gone))
|
||||
divergent, report, err := cli.CompareKVDigests(doc(t, "n1", t0, have), doc(t, "n2", t0, gone))
|
||||
require.NoError(t, err)
|
||||
assert.False(t, divergent, report)
|
||||
|
||||
have.maxAge, gone.maxAge = 0, 0
|
||||
divergent, _, err = cmd.CompareKVDigests(doc(t, "n1", t0, have), doc(t, "n2", t0, gone))
|
||||
divergent, _, err = cli.CompareKVDigests(doc(t, "n1", t0, have), doc(t, "n2", t0, gone))
|
||||
require.NoError(t, err)
|
||||
assert.True(t, divergent, "without max_age the same gap is unexplained")
|
||||
})
|
||||
@@ -136,7 +136,7 @@ func TestKVCompareMissingMessage(t *testing.T) {
|
||||
t.Run("head dropped by a stream limit", func(t *testing.T) {
|
||||
behind := stream{name: "S", replicas: 2, maxMsgs: 2, msgs: kvs(msg{1, "a", "h1", t0}, msg{2, "b", "h2", t0})}
|
||||
ahead := stream{name: "S", replicas: 2, maxMsgs: 2, msgs: kvs(msg{2, "b", "h2", t0}, msg{3, "c", "h3", t0})}
|
||||
divergent, report, err := cmd.CompareKVDigests(doc(t, "n1", t0, behind), doc(t, "n2", t0, ahead))
|
||||
divergent, report, err := cli.CompareKVDigests(doc(t, "n1", t0, behind), doc(t, "n2", t0, ahead))
|
||||
require.NoError(t, err)
|
||||
assert.False(t, divergent, report)
|
||||
})
|
||||
@@ -145,7 +145,7 @@ func TestKVCompareMissingMessage(t *testing.T) {
|
||||
func TestKVCompareTailLag(t *testing.T) {
|
||||
behind := stream{name: "KV_a", replicas: 2, msgs: kvs(msg{1, "k", "h1", t0}, msg{2, "k", "h2", t0})}
|
||||
ahead := stream{name: "KV_a", replicas: 2, msgs: kvs(msg{1, "k", "h1", t0}, msg{2, "k", "h2", t0}, msg{3, "k", "h3", t0}, msg{4, "k", "h4", t0})}
|
||||
divergent, report, err := cmd.CompareKVDigests(doc(t, "n1", t0, behind), doc(t, "n2", t0, ahead))
|
||||
divergent, report, err := cli.CompareKVDigests(doc(t, "n1", t0, behind), doc(t, "n2", t0, ahead))
|
||||
require.NoError(t, err)
|
||||
assert.False(t, divergent, report)
|
||||
assert.Contains(t, report, "tail not compared: n2 +2")
|
||||
@@ -154,7 +154,7 @@ func TestKVCompareTailLag(t *testing.T) {
|
||||
func TestKVCompareNoCommonRange(t *testing.T) {
|
||||
a := stream{name: "KV_a", replicas: 2, msgs: kvs(msg{1, "k", "h1", t0})}
|
||||
b := stream{name: "KV_a", replicas: 2, msgs: kvs(msg{5, "k", "h5", t0})}
|
||||
divergent, report, err := cmd.CompareKVDigests(doc(t, "n1", t0, a), doc(t, "n2", t0, b))
|
||||
divergent, report, err := cli.CompareKVDigests(doc(t, "n1", t0, a), doc(t, "n2", t0, b))
|
||||
require.NoError(t, err)
|
||||
assert.True(t, divergent)
|
||||
assert.Contains(t, report, "no sequence range is common")
|
||||
@@ -165,14 +165,14 @@ func TestKVCompareReplicaPlacement(t *testing.T) {
|
||||
other := stream{name: "KV_b", replicas: 1, msgs: kvs(msg{1, "k", "h1", t0})}
|
||||
|
||||
t.Run("fewer holders than replicas", func(t *testing.T) {
|
||||
divergent, report, err := cmd.CompareKVDigests(doc(t, "n1", t0, s), doc(t, "n2", t0, s), doc(t, "n3", t0, other))
|
||||
divergent, report, err := cli.CompareKVDigests(doc(t, "n1", t0, s), doc(t, "n2", t0, s), doc(t, "n3", t0, other))
|
||||
require.NoError(t, err)
|
||||
assert.True(t, divergent)
|
||||
assert.Contains(t, report, "held by 2 of 3 replicas; missing from n3")
|
||||
})
|
||||
|
||||
t.Run("more holders than replicas", func(t *testing.T) {
|
||||
divergent, report, err := cmd.CompareKVDigests(doc(t, "n1", t0, other), doc(t, "n2", t0, other))
|
||||
divergent, report, err := cli.CompareKVDigests(doc(t, "n1", t0, other), doc(t, "n2", t0, other))
|
||||
require.NoError(t, err)
|
||||
assert.True(t, divergent)
|
||||
assert.Contains(t, report, "held by 2 nodes but configured for 1 replicas")
|
||||
@@ -181,14 +181,14 @@ func TestKVCompareReplicaPlacement(t *testing.T) {
|
||||
t.Run("replica counts disagree", func(t *testing.T) {
|
||||
r1 := s
|
||||
r1.replicas = 1
|
||||
divergent, report, err := cmd.CompareKVDigests(doc(t, "n1", t0, s), doc(t, "n2", t0, r1))
|
||||
divergent, report, err := cli.CompareKVDigests(doc(t, "n1", t0, s), doc(t, "n2", t0, r1))
|
||||
require.NoError(t, err)
|
||||
assert.True(t, divergent)
|
||||
assert.Contains(t, report, "replicas disagree on the replica count")
|
||||
})
|
||||
|
||||
t.Run("single replica stream on one node", func(t *testing.T) {
|
||||
divergent, report, err := cmd.CompareKVDigests(doc(t, "n1", t0, other), doc(t, "n2", t0))
|
||||
divergent, report, err := cli.CompareKVDigests(doc(t, "n1", t0, other), doc(t, "n2", t0))
|
||||
require.NoError(t, err)
|
||||
assert.False(t, divergent, report)
|
||||
})
|
||||
@@ -197,7 +197,7 @@ func TestKVCompareReplicaPlacement(t *testing.T) {
|
||||
func TestKVCompareNeedsSeqs(t *testing.T) {
|
||||
a := stream{name: "KV_a", replicas: 2, noSeqs: true, msgs: kvs(msg{1, "k", "h1", t0})}
|
||||
b := stream{name: "KV_a", replicas: 2, noSeqs: true, msgs: kvs(msg{1, "k", "h2", t0})}
|
||||
divergent, report, err := cmd.CompareKVDigests(doc(t, "n1", t0, a), doc(t, "n2", t0, b))
|
||||
divergent, report, err := cli.CompareKVDigests(doc(t, "n1", t0, a), doc(t, "n2", t0, b))
|
||||
require.NoError(t, err)
|
||||
assert.True(t, divergent)
|
||||
assert.Contains(t, report, "re-run digest with --seqs")
|
||||
@@ -207,27 +207,27 @@ func TestKVCompareInput(t *testing.T) {
|
||||
s := stream{name: "KV_a", replicas: 2, msgs: kvs(msg{1, "k", "h1", t0})}
|
||||
|
||||
// Several documents in one stream, as "-" reads them from stdin.
|
||||
divergent, _, err := cmd.CompareKVDigests(doc(t, "n1", t0, s) + "\n" + doc(t, "n2", t0, s))
|
||||
divergent, _, err := cli.CompareKVDigests(doc(t, "n1", t0, s) + "\n" + doc(t, "n2", t0, s))
|
||||
require.NoError(t, err)
|
||||
assert.False(t, divergent)
|
||||
|
||||
_, _, err = cmd.CompareKVDigests(doc(t, "n1", t0, s))
|
||||
_, _, err = cli.CompareKVDigests(doc(t, "n1", t0, s))
|
||||
assert.ErrorContains(t, err, "at least two nodes")
|
||||
|
||||
_, _, err = cmd.CompareKVDigests(doc(t, "n1", t0, s), doc(t, "n1", t0, s))
|
||||
_, _, err = cli.CompareKVDigests(doc(t, "n1", t0, s), doc(t, "n1", t0, s))
|
||||
assert.ErrorContains(t, err, "two digests are from host n1")
|
||||
|
||||
_, _, err = cmd.CompareKVDigests(`KV_a first=1 last=1`)
|
||||
_, _, err = cli.CompareKVDigests(`KV_a first=1 last=1`)
|
||||
assert.Error(t, err, "text digest output is rejected")
|
||||
|
||||
_, _, err = cmd.CompareKVDigests(`{"streams":[]}`)
|
||||
_, _, err = cli.CompareKVDigests(`{"streams":[]}`)
|
||||
assert.ErrorContains(t, err, "no host")
|
||||
|
||||
_, _, err = cmd.CompareKVDigests(" ")
|
||||
_, _, err = cli.CompareKVDigests(" ")
|
||||
assert.ErrorContains(t, err, "no digest documents")
|
||||
}
|
||||
|
||||
func TestAdminInitDiscardsJetStreamByDefault(t *testing.T) {
|
||||
assert.Equal(t, "true", cmd.AdminInitFlagDefault("discard-jetstream"))
|
||||
assert.True(t, strings.HasPrefix(cmd.AdminInitFlagDefault("ipsec"), "true"))
|
||||
assert.Equal(t, "true", cli.AdminInitFlagDefault("discard-jetstream"))
|
||||
assert.True(t, strings.HasPrefix(cli.AdminInitFlagDefault("ipsec"), "true"))
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package cmd
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
//test:in-package — it tests this command's unexported rendering and repair
|
||||
// helpers, which exist only to be exercised without a live cluster.
|
||||
|
||||
package cmd
|
||||
package cli
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
@@ -1,7 +1,7 @@
|
||||
//test:in-package: exercises the unexported digest helpers against a real clustered
|
||||
// store, which the cobra command only reaches through a live node.
|
||||
|
||||
package cmd
|
||||
package cli
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
@@ -1,4 +1,4 @@
|
||||
package cmd
|
||||
package cli
|
||||
|
||||
import (
|
||||
"os"
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
//test:in-package — validateNodeCount is an unexported guard on this command's
|
||||
// flags, and exporting it would widen the package's API for a test's convenience.
|
||||
|
||||
package cmd
|
||||
package cli
|
||||
|
||||
import (
|
||||
"strings"
|
||||
@@ -1,4 +1,4 @@
|
||||
package cmd
|
||||
package cli
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
@@ -128,24 +128,24 @@ func init() {
|
||||
ochreWeightsStageCmd.Flags().String("model-id", "", "Catalog model ID to stage weights for (required)")
|
||||
ochreWeightsStageCmd.Flags().String("s3-uri", "", "predastore S3 URI holding the model's Hugging Face files, e.g. s3://bucket/prefix/ (required)")
|
||||
ochreWeightsStageCmd.Flags().String("tmp-dir", os.TempDir(), "Temporary directory for download and volume staging")
|
||||
_ = ochreWeightsStageCmd.MarkFlagRequired("model-id")
|
||||
_ = ochreWeightsStageCmd.MarkFlagRequired("s3-uri")
|
||||
mustMarkFlagRequired(ochreWeightsStageCmd, "model-id")
|
||||
mustMarkFlagRequired(ochreWeightsStageCmd, "s3-uri")
|
||||
|
||||
ochreWeightsRemoveCmd.Flags().String("model-id", "", "Model ID to remove from staged weights (required)")
|
||||
_ = ochreWeightsRemoveCmd.MarkFlagRequired("model-id")
|
||||
mustMarkFlagRequired(ochreWeightsRemoveCmd, "model-id")
|
||||
|
||||
ochreWeightsPullCmd.Flags().String("model-id", "", "Self-host catalog model ID this pull is for (required)")
|
||||
ochreWeightsPullCmd.Flags().String("hf-repo", "", "Hugging Face repo, e.g. meta-llama/Llama-3.2-1B-Instruct (default: the catalog entry's repo)")
|
||||
ochreWeightsPullCmd.Flags().String("revision", "main", "Hugging Face branch, tag or commit SHA to resolve and pin (default: the catalog entry's pinned revision)")
|
||||
ochreWeightsPullCmd.Flags().String("s3-uri", "", "predastore destination prefix, e.g. s3://bucket/prefix/ (default: s3://ochre-weights/<repo>/<sha>/)")
|
||||
ochreWeightsPullCmd.Flags().String("hf-token", "", "Hugging Face token for a gated repo (falls back to HF_TOKEN, then a stored platform credential)")
|
||||
_ = ochreWeightsPullCmd.MarkFlagRequired("model-id")
|
||||
mustMarkFlagRequired(ochreWeightsPullCmd, "model-id")
|
||||
|
||||
ochreCredentialsSetCmd.Flags().String("vendor", "", "Vendor to store a credential for, e.g. huggingface (required)")
|
||||
ochreCredentialsSetCmd.Flags().String("account", "", "Account ID to store the credential under (default: platform account)")
|
||||
ochreCredentialsSetCmd.Flags().String("token", "", "Must be '-': reads the secret from stdin (required)")
|
||||
_ = ochreCredentialsSetCmd.MarkFlagRequired("vendor")
|
||||
_ = ochreCredentialsSetCmd.MarkFlagRequired("token")
|
||||
mustMarkFlagRequired(ochreCredentialsSetCmd, "vendor")
|
||||
mustMarkFlagRequired(ochreCredentialsSetCmd, "token")
|
||||
}
|
||||
|
||||
// requiredWeightsFiles are the fixed-name Hugging Face artefacts stage
|
||||
@@ -1172,11 +1172,11 @@ func init() {
|
||||
c.Flags().String("account-id", "", "12-digit account ID to change (required)")
|
||||
c.Flags().String("model-id", "", "Model ID to change (e.g. meta.llama3-2-1b-instruct-v1:0)")
|
||||
c.Flags().Bool("all-models", false, "Apply to every model in the platform catalog")
|
||||
_ = c.MarkFlagRequired("account-id")
|
||||
mustMarkFlagRequired(c, "account-id")
|
||||
}
|
||||
|
||||
adminOchreAccessListCmd.Flags().String("account-id", "", "12-digit account ID to inspect (required)")
|
||||
_ = adminOchreAccessListCmd.MarkFlagRequired("account-id")
|
||||
mustMarkFlagRequired(adminOchreAccessListCmd, "account-id")
|
||||
}
|
||||
|
||||
// ochreAccessStore connects to the cluster and returns the grant store along
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package cmd
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
// exported surface to drive them through.
|
||||
//
|
||||
//test:in-package
|
||||
package cmd
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
+4
-4
@@ -1,4 +1,4 @@
|
||||
package cmd
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -72,15 +72,15 @@ func init() {
|
||||
ochreEndpointEnsureCmd.Flags().String("model-id", "", "Catalog model ID to bring an endpoint up for (required)")
|
||||
ochreEndpointEnsureCmd.Flags().Bool("wait", false, "Poll until the endpoint is READY and report the elapsed cold start")
|
||||
ochreEndpointEnsureCmd.Flags().Duration("timeout", defaultEndpointWaitTimeout, "How long --wait polls before giving up")
|
||||
_ = ochreEndpointEnsureCmd.MarkFlagRequired("model-id")
|
||||
mustMarkFlagRequired(ochreEndpointEnsureCmd, "model-id")
|
||||
|
||||
ochreEndpointDescribeCmd.Flags().String("model-id", "", "Model ID to describe (required)")
|
||||
ochreEndpointDescribeCmd.Flags().String("account", "", "Account ID scoping the lookup (defaults to the shared platform account; set to see a pinned provisioned-throughput endpoint)")
|
||||
_ = ochreEndpointDescribeCmd.MarkFlagRequired("model-id")
|
||||
mustMarkFlagRequired(ochreEndpointDescribeCmd, "model-id")
|
||||
|
||||
ochreEndpointDeleteCmd.Flags().String("model-id", "", "Model ID whose endpoint to tear down (required)")
|
||||
ochreEndpointDeleteCmd.Flags().String("account", "", "Account ID scoping the target (defaults to the shared platform account; set to tear down a pinned provisioned-throughput endpoint)")
|
||||
_ = ochreEndpointDeleteCmd.MarkFlagRequired("model-id")
|
||||
mustMarkFlagRequired(ochreEndpointDeleteCmd, "model-id")
|
||||
}
|
||||
|
||||
const (
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package cmd
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package cmd
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -1,4 +1,4 @@
|
||||
package cmd
|
||||
package cli
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
+10
-10
@@ -1,4 +1,4 @@
|
||||
package cmd
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -133,8 +133,8 @@ func init() {
|
||||
ochreVectorIndexCreateCmd.Flags().String("name", "", "Human-readable index name (required)")
|
||||
ochreVectorIndexCreateCmd.Flags().Int("dimension", 0, "Embedding vector dimension (required)")
|
||||
ochreVectorIndexCreateCmd.Flags().String("model", defaultVectorEmbeddingModel, "Embedding model this index is pinned to")
|
||||
_ = ochreVectorIndexCreateCmd.MarkFlagRequired("name")
|
||||
_ = ochreVectorIndexCreateCmd.MarkFlagRequired("dimension")
|
||||
mustMarkFlagRequired(ochreVectorIndexCreateCmd, "name")
|
||||
mustMarkFlagRequired(ochreVectorIndexCreateCmd, "dimension")
|
||||
|
||||
ochreVectorIngestCmd.Flags().String("index", "", "Target index ID (required)")
|
||||
ochreVectorIngestCmd.Flags().String("bucket", "", "Source predastore bucket (required)")
|
||||
@@ -142,24 +142,24 @@ func init() {
|
||||
ochreVectorIngestCmd.Flags().Int("chunk-size", 0, "Chunk size in runes (0 takes the package default)")
|
||||
ochreVectorIngestCmd.Flags().Int("chunk-overlap", 0, "Chunk overlap in runes (0 takes the package default)")
|
||||
ochreVectorIngestCmd.Flags().StringToString("meta", nil, "Static metadata tag stamped on every ingested row, k=v (repeatable)")
|
||||
_ = ochreVectorIngestCmd.MarkFlagRequired("index")
|
||||
_ = ochreVectorIngestCmd.MarkFlagRequired("bucket")
|
||||
mustMarkFlagRequired(ochreVectorIngestCmd, "index")
|
||||
mustMarkFlagRequired(ochreVectorIngestCmd, "bucket")
|
||||
|
||||
ochreVectorQueryCmd.Flags().String("index", "", "Index ID to query (required)")
|
||||
ochreVectorQueryCmd.Flags().String("text", "", "Query text, embedded server-side against the index's pinned model (required)")
|
||||
ochreVectorQueryCmd.Flags().Int("k", 0, "Number of results (0 takes the service default; hard-capped at 100)")
|
||||
ochreVectorQueryCmd.Flags().String("filter", "", "JSON-encoded metadata Filter AST, e.g. {\"Op\":\"equals\",\"Key\":\"category\",\"Value\":\"faq\"}")
|
||||
ochreVectorQueryCmd.Flags().Bool("json", false, "Print full, untruncated results as JSON")
|
||||
_ = ochreVectorQueryCmd.MarkFlagRequired("index")
|
||||
_ = ochreVectorQueryCmd.MarkFlagRequired("text")
|
||||
mustMarkFlagRequired(ochreVectorQueryCmd, "index")
|
||||
mustMarkFlagRequired(ochreVectorQueryCmd, "text")
|
||||
|
||||
ochreVectorBackupCmd.Flags().String("account", "", "Account ID to back up (required)")
|
||||
_ = ochreVectorBackupCmd.MarkFlagRequired("account")
|
||||
mustMarkFlagRequired(ochreVectorBackupCmd, "account")
|
||||
|
||||
ochreVectorRestoreCmd.Flags().String("account", "", "Account ID to restore into (required)")
|
||||
ochreVectorRestoreCmd.Flags().String("from", "", "Backup object key to restore from, as printed by 'backup' (required)")
|
||||
_ = ochreVectorRestoreCmd.MarkFlagRequired("account")
|
||||
_ = ochreVectorRestoreCmd.MarkFlagRequired("from")
|
||||
mustMarkFlagRequired(ochreVectorRestoreCmd, "account")
|
||||
mustMarkFlagRequired(ochreVectorRestoreCmd, "from")
|
||||
}
|
||||
|
||||
// vectorServiceFn indirects the NATS-backed client so the Run functions'
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
// exported surface to drive them through.
|
||||
//
|
||||
//test:in-package
|
||||
package cmd
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -1,4 +1,4 @@
|
||||
package cmd
|
||||
package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@@ -1,4 +1,4 @@
|
||||
package cmd
|
||||
package cli
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
+9
-9
@@ -1,4 +1,4 @@
|
||||
package cmd_test
|
||||
package cli_test
|
||||
|
||||
import (
|
||||
"errors"
|
||||
@@ -6,8 +6,8 @@ import (
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/service/iam"
|
||||
"github.com/mulgadc/spinifex/cmd/spinifex/cmd"
|
||||
handlers_iam "github.com/mulgadc/spinifex/spinifex/handlers/iam"
|
||||
"github.com/mulgadc/spinifex/spinifex/operator/cli"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
@@ -79,14 +79,14 @@ func (s *principalIAMStub) DeleteAccessKey(_ string, input *iam.DeleteAccessKeyI
|
||||
// from an earlier name would keep authorising what the operator just removed.
|
||||
func TestCreateDropsAnInlinePolicyItDidNotWrite(t *testing.T) {
|
||||
svc := &principalIAMStub{inlinePolicies: []string{
|
||||
"operator-admin-methods", cmd.AdminPrincipalPolicyName, "signup-createaccount",
|
||||
"operator-admin-methods", cli.AdminPrincipalPolicyName, "signup-createaccount",
|
||||
}}
|
||||
|
||||
dropped, err := cmd.DropStalePrincipalPolicies(svc, superAdminAccountID, "operator")
|
||||
dropped, err := cli.DropStalePrincipalPolicies(svc, superAdminAccountID, "operator")
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, []string{"operator-admin-methods", "signup-createaccount"}, dropped)
|
||||
assert.NotContains(t, svc.deletedPolicies, cmd.AdminPrincipalPolicyName,
|
||||
assert.NotContains(t, svc.deletedPolicies, cli.AdminPrincipalPolicyName,
|
||||
"the policy the command owns must survive its own rewrite")
|
||||
}
|
||||
|
||||
@@ -95,7 +95,7 @@ func TestCreateDropsAnInlinePolicyItDidNotWrite(t *testing.T) {
|
||||
func TestRevokeRemovesEveryAccessKey(t *testing.T) {
|
||||
svc := &principalIAMStub{accessKeys: []string{"AKIAONE", "AKIATWO"}}
|
||||
|
||||
revoked, err := cmd.RevokePrincipalKeys(svc, superAdminAccountID, "operator")
|
||||
revoked, err := cli.RevokePrincipalKeys(svc, superAdminAccountID, "operator")
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, []string{"AKIAONE", "AKIATWO"}, revoked)
|
||||
@@ -107,7 +107,7 @@ func TestRevokeRemovesEveryAccessKey(t *testing.T) {
|
||||
func TestRevokeFailsRatherThanGuessing(t *testing.T) {
|
||||
svc := &principalIAMStub{listKeysErr: errors.New("kv unavailable")}
|
||||
|
||||
_, err := cmd.RevokePrincipalKeys(svc, superAdminAccountID, "operator")
|
||||
_, err := cli.RevokePrincipalKeys(svc, superAdminAccountID, "operator")
|
||||
|
||||
require.Error(t, err)
|
||||
assert.Empty(t, svc.deletedKeys)
|
||||
@@ -122,7 +122,7 @@ func TestDescribePrincipalReportsScopedGrants(t *testing.T) {
|
||||
accessKeys: []string{"AKIAONE"},
|
||||
}
|
||||
|
||||
row, err := cmd.DescribePrincipal(svc, superAdminAccountID, "signup")
|
||||
row, err := cli.DescribePrincipal(svc, superAdminAccountID, "signup")
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "signup", row.UserName)
|
||||
@@ -135,7 +135,7 @@ func TestDescribePrincipalReportsScopedGrants(t *testing.T) {
|
||||
func TestDescribePrincipalMarksAnAttachedPolicy(t *testing.T) {
|
||||
svc := &principalIAMStub{attachedPolicies: []string{handlers_iam.AdminPolicyName}}
|
||||
|
||||
row, err := cmd.DescribePrincipal(svc, superAdminAccountID, handlers_iam.AdminUserName)
|
||||
row, err := cli.DescribePrincipal(svc, superAdminAccountID, handlers_iam.AdminUserName)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, []string{handlers_iam.AdminPolicyName + " (attached)"}, row.Grants)
|
||||
+23
-23
@@ -1,13 +1,13 @@
|
||||
package cmd_test
|
||||
package cli_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/mulgadc/spinifex/cmd/spinifex/cmd"
|
||||
"github.com/mulgadc/spinifex/spinifex/gateway"
|
||||
handlers_iam "github.com/mulgadc/spinifex/spinifex/handlers/iam"
|
||||
"github.com/mulgadc/spinifex/spinifex/operator/cli"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
@@ -38,7 +38,7 @@ func policyActions(t *testing.T, document string) []string {
|
||||
// cover the surface the gateway actually serves rather than a list that can
|
||||
// fall behind it.
|
||||
func TestPrincipalGrantsDefaultToEveryAdminMethod(t *testing.T) {
|
||||
grants, err := cmd.ResolvePrincipalGrants(nil)
|
||||
grants, err := cli.ResolvePrincipalGrants(nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
var want []string
|
||||
@@ -51,7 +51,7 @@ func TestPrincipalGrantsDefaultToEveryAdminMethod(t *testing.T) {
|
||||
// Scoping is the whole point of the flag: a signup credential holds exactly
|
||||
// CreateAccount, so a leaked key cannot remove a tenant.
|
||||
func TestPrincipalGrantsScopeToTheRequestedMethods(t *testing.T) {
|
||||
grants, err := cmd.ResolvePrincipalGrants([]string{"CreateAccount"})
|
||||
grants, err := cli.ResolvePrincipalGrants([]string{"CreateAccount"})
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, []string{"spinifex:CreateAccount"}, grants)
|
||||
@@ -60,7 +60,7 @@ func TestPrincipalGrantsScopeToTheRequestedMethods(t *testing.T) {
|
||||
// Operators write the method either way, and a duplicate is a typo rather than
|
||||
// a reason to refuse.
|
||||
func TestPrincipalGrantsAcceptQualifiedAndRepeatedNames(t *testing.T) {
|
||||
grants, err := cmd.ResolvePrincipalGrants([]string{
|
||||
grants, err := cli.ResolvePrincipalGrants([]string{
|
||||
"spinifex:ListAccounts", "listaccounts", " DeleteAccount ",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
@@ -71,7 +71,7 @@ func TestPrincipalGrantsAcceptQualifiedAndRepeatedNames(t *testing.T) {
|
||||
// A misspelt method must fail rather than mint a key that silently authorises
|
||||
// nothing — the caller would only find out at the first AccessDenied.
|
||||
func TestPrincipalGrantsRejectAnUnknownMethod(t *testing.T) {
|
||||
_, err := cmd.ResolvePrincipalGrants([]string{"DeleteEverything"})
|
||||
_, err := cli.ResolvePrincipalGrants([]string{"DeleteEverything"})
|
||||
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "DeleteEverything")
|
||||
@@ -81,10 +81,10 @@ func TestPrincipalGrantsRejectAnUnknownMethod(t *testing.T) {
|
||||
// A wildcard here would authorise whatever is added to the surface next with a
|
||||
// key minted before it existed.
|
||||
func TestPrincipalPolicyNamesEveryMethod(t *testing.T) {
|
||||
grants, err := cmd.ResolvePrincipalGrants(nil)
|
||||
grants, err := cli.ResolvePrincipalGrants(nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
actions := policyActions(t, cmd.PrincipalPolicyDocument(grants))
|
||||
actions := policyActions(t, cli.PrincipalPolicyDocument(grants))
|
||||
|
||||
assert.ElementsMatch(t, grants, actions)
|
||||
for _, action := range actions {
|
||||
@@ -95,11 +95,11 @@ func TestPrincipalPolicyNamesEveryMethod(t *testing.T) {
|
||||
// The listing reads back what the create path wrote, so a principal's grants
|
||||
// can be audited without decoding the document by hand.
|
||||
func TestPrincipalPolicyActionsRoundTrip(t *testing.T) {
|
||||
grants, err := cmd.ResolvePrincipalGrants([]string{"CreateAccount", "ListAccounts"})
|
||||
grants, err := cli.ResolvePrincipalGrants([]string{"CreateAccount", "ListAccounts"})
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, []string{"CreateAccount", "ListAccounts"},
|
||||
cmd.PrincipalPolicyActions(cmd.PrincipalPolicyDocument(grants)))
|
||||
cli.PrincipalPolicyActions(cli.PrincipalPolicyDocument(grants)))
|
||||
}
|
||||
|
||||
// A NotAction grant covers everything but its list, so rendering only Action
|
||||
@@ -111,37 +111,37 @@ func TestPrincipalPolicyActionsRenderNotAction(t *testing.T) {
|
||||
{"Effect":"Deny","NotAction":"spinifex:ListAccounts","Resource":"*"}]}`
|
||||
|
||||
assert.Equal(t, []string{"ListAccounts", "NotAction(DeleteAccount, CreateAccount)"},
|
||||
cmd.PrincipalPolicyActions(document))
|
||||
cli.PrincipalPolicyActions(document))
|
||||
}
|
||||
|
||||
// A document nobody can parse is reported as no grants rather than as a guess.
|
||||
func TestPrincipalPolicyActionsIgnoreAnUndecodableDocument(t *testing.T) {
|
||||
assert.Empty(t, cmd.PrincipalPolicyActions("{not json"))
|
||||
assert.Empty(t, cli.PrincipalPolicyActions("{not json"))
|
||||
}
|
||||
|
||||
// Rotating the super-admin account's own administrator would revoke the
|
||||
// bootstrap credential the operator is holding.
|
||||
func TestPrincipalNameRefusesTheAccountAdministrator(t *testing.T) {
|
||||
err := cmd.ValidatePrincipalName(handlers_iam.AdminUserName)
|
||||
err := cli.ValidatePrincipalName(handlers_iam.AdminUserName)
|
||||
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), handlers_iam.AdminUserName)
|
||||
}
|
||||
|
||||
func TestPrincipalNameRejectsUnusableNames(t *testing.T) {
|
||||
assert.Error(t, cmd.ValidatePrincipalName(""))
|
||||
assert.Error(t, cmd.ValidatePrincipalName("has space"))
|
||||
assert.Error(t, cmd.ValidatePrincipalName(strings.Repeat("a", 65)))
|
||||
assert.Error(t, cli.ValidatePrincipalName(""))
|
||||
assert.Error(t, cli.ValidatePrincipalName("has space"))
|
||||
assert.Error(t, cli.ValidatePrincipalName(strings.Repeat("a", 65)))
|
||||
|
||||
assert.NoError(t, cmd.ValidatePrincipalName("signup"))
|
||||
assert.NoError(t, cmd.ValidatePrincipalName("ops-team_1@example.com"))
|
||||
assert.NoError(t, cli.ValidatePrincipalName("signup"))
|
||||
assert.NoError(t, cli.ValidatePrincipalName("ops-team_1@example.com"))
|
||||
}
|
||||
|
||||
// The listing prints one line per principal, including a revoked credential
|
||||
// that holds no key — an operator has to see it to know it is still there.
|
||||
func TestPrintPrincipalTableRendersEveryPrincipal(t *testing.T) {
|
||||
output := captureOutput(t, func() {
|
||||
cmd.PrintPrincipalTable("000000000001", []cmd.PrincipalRowSpec{
|
||||
cli.PrintPrincipalTable("000000000001", []cli.PrincipalRowSpec{
|
||||
{UserName: "admin", Grants: []string{"AdministratorAccess (attached)"}, Keys: 1},
|
||||
{UserName: "signup", Grants: []string{"CreateAccount"}, Keys: 1},
|
||||
{UserName: "retired", Keys: 0},
|
||||
@@ -159,7 +159,7 @@ func TestPrintPrincipalTableRendersEveryPrincipal(t *testing.T) {
|
||||
// a rendering fault rather than as an answer.
|
||||
func TestPrintPrincipalTableMarksAnEmptyGrant(t *testing.T) {
|
||||
output := captureOutput(t, func() {
|
||||
cmd.PrintPrincipalTable("000000000001", []cmd.PrincipalRowSpec{{UserName: "retired"}})
|
||||
cli.PrintPrincipalTable("000000000001", []cli.PrincipalRowSpec{{UserName: "retired"}})
|
||||
})
|
||||
|
||||
assert.Contains(t, output, "-")
|
||||
@@ -169,7 +169,7 @@ func TestPrintPrincipalTableMarksAnEmptyGrant(t *testing.T) {
|
||||
// TERMINATING reads the same either way — which is how a stuck teardown is
|
||||
// noticed.
|
||||
func TestAccountSummariesCarryStatus(t *testing.T) {
|
||||
summaries := cmd.AccountSummaries([]*handlers_iam.Account{
|
||||
summaries := cli.AccountSummaries([]*handlers_iam.Account{
|
||||
{AccountID: "000000000042", AccountName: "t@example.com", Status: handlers_iam.AccountStatusActive},
|
||||
nil,
|
||||
{AccountID: "000000000043", AccountName: "u@example.com", Status: handlers_iam.AccountStatusTerminating},
|
||||
@@ -181,7 +181,7 @@ func TestAccountSummariesCarryStatus(t *testing.T) {
|
||||
|
||||
func TestPrintAccountTableRendersEveryAccount(t *testing.T) {
|
||||
output := captureOutput(t, func() {
|
||||
cmd.PrintAccountTable([]gateway.AccountSummary{
|
||||
cli.PrintAccountTable([]gateway.AccountSummary{
|
||||
{
|
||||
AccountID: "000000000042",
|
||||
AccountName: "tenant@example.com",
|
||||
@@ -199,7 +199,7 @@ func TestPrintAccountTableRendersEveryAccount(t *testing.T) {
|
||||
|
||||
// An empty listing must say so. A bare header reads as truncated output.
|
||||
func TestPrintAccountTableSaysWhenThereAreNone(t *testing.T) {
|
||||
output := captureOutput(t, func() { cmd.PrintAccountTable(nil) })
|
||||
output := captureOutput(t, func() { cli.PrintAccountTable(nil) })
|
||||
|
||||
assert.Contains(t, output, "No accounts found")
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package cmd
|
||||
package cli
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
@@ -246,7 +246,11 @@ func newClientToken() string {
|
||||
// command works against a cluster's self-signed CA and against a public
|
||||
// certificate. An unreadable bundle is an error, never a silent downgrade.
|
||||
func adminHTTPClient(caBundle string) (*http.Client, error) {
|
||||
transport := http.DefaultTransport.(*http.Transport).Clone()
|
||||
base, ok := http.DefaultTransport.(*http.Transport)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("http.DefaultTransport is %T, not *http.Transport", http.DefaultTransport)
|
||||
}
|
||||
transport := base.Clone()
|
||||
if caBundle != "" {
|
||||
pem, err := os.ReadFile(caBundle)
|
||||
if err != nil {
|
||||
+19
-19
@@ -1,4 +1,4 @@
|
||||
package cmd_test
|
||||
package cli_test
|
||||
|
||||
import (
|
||||
"encoding/pem"
|
||||
@@ -10,9 +10,9 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/mulgadc/spinifex/cmd/spinifex/cmd"
|
||||
"github.com/mulgadc/spinifex/spinifex/bootstrap/config"
|
||||
"github.com/mulgadc/spinifex/spinifex/gateway"
|
||||
"github.com/mulgadc/spinifex/spinifex/operator/cli"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
@@ -77,7 +77,7 @@ func TestTrustsWholeAccount(t *testing.T) {
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
assert.Equal(t, tc.want, cmd.TrustsWholeAccount(tc.document, auditAccountID))
|
||||
assert.Equal(t, tc.want, cli.TrustsWholeAccount(tc.document, auditAccountID))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -85,8 +85,8 @@ func TestTrustsWholeAccount(t *testing.T) {
|
||||
func TestNewClientTokenIsAccepted(t *testing.T) {
|
||||
valid := regexp.MustCompile(`^[A-Za-z0-9_-]{32,128}$`)
|
||||
|
||||
first := cmd.NewClientToken()
|
||||
second := cmd.NewClientToken()
|
||||
first := cli.NewClientToken()
|
||||
second := cli.NewClientToken()
|
||||
|
||||
assert.Regexp(t, valid, first)
|
||||
assert.NotEqual(t, first, second)
|
||||
@@ -106,7 +106,7 @@ func TestLocalGatewayEndpoint(t *testing.T) {
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
node := config.Config{AWSGW: config.AWSGWConfig{Host: tc.host}}
|
||||
assert.Equal(t, tc.want, cmd.LocalGatewayEndpoint(node))
|
||||
assert.Equal(t, tc.want, cli.LocalGatewayEndpoint(node))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -114,12 +114,12 @@ func TestLocalGatewayEndpoint(t *testing.T) {
|
||||
// An unreadable or empty CA bundle must be an error, never a silent fallback
|
||||
// to an unverified connection.
|
||||
func TestAdminHTTPClientRejectsBadCABundle(t *testing.T) {
|
||||
_, err := cmd.AdminHTTPClient(filepath.Join(t.TempDir(), "absent.pem"))
|
||||
_, err := cli.AdminHTTPClient(filepath.Join(t.TempDir(), "absent.pem"))
|
||||
assert.Error(t, err)
|
||||
|
||||
empty := filepath.Join(t.TempDir(), "empty.pem")
|
||||
require.NoError(t, os.WriteFile(empty, []byte("not a certificate"), 0o600))
|
||||
_, err = cmd.AdminHTTPClient(empty)
|
||||
_, err = cli.AdminHTTPClient(empty)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
@@ -159,8 +159,8 @@ func TestCreateAccountRemoteSignsAndPosts(t *testing.T) {
|
||||
var got http.Request
|
||||
srv := newAdminTestServer(t, http.StatusOK, createAccountOK, &got)
|
||||
|
||||
out, err := cmd.CreateAccountRemote(t.Context(),
|
||||
cmd.AdminTarget(srv.URL, "us-west-1", writeServerCA(t, srv)),
|
||||
out, err := cli.CreateAccountRemote(t.Context(),
|
||||
cli.AdminTarget(srv.URL, "us-west-1", writeServerCA(t, srv)),
|
||||
gateway.CreateAccountRequest{Name: "ben@example.com", ClientToken: strings.Repeat("a", 32)})
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -184,8 +184,8 @@ func TestCreateAccountRemoteTrimsEndpointSlash(t *testing.T) {
|
||||
var got http.Request
|
||||
srv := newAdminTestServer(t, http.StatusOK, createAccountOK, &got)
|
||||
|
||||
_, err := cmd.CreateAccountRemote(t.Context(),
|
||||
cmd.AdminTarget(srv.URL+"/", "us-west-1", writeServerCA(t, srv)),
|
||||
_, err := cli.CreateAccountRemote(t.Context(),
|
||||
cli.AdminTarget(srv.URL+"/", "us-west-1", writeServerCA(t, srv)),
|
||||
gateway.CreateAccountRequest{Name: "ben@example.com", ClientToken: strings.Repeat("a", 32)})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "/admin/CreateAccount", got.URL.Path)
|
||||
@@ -199,14 +199,14 @@ func TestCreateAccountRemoteSurfacesGatewayError(t *testing.T) {
|
||||
srv := newAdminTestServer(t, http.StatusConflict,
|
||||
`{"error":{"code":"AccountAlreadyExists","message":"taken"},"requestId":"req-1"}`, &got)
|
||||
|
||||
_, err := cmd.CreateAccountRemote(t.Context(),
|
||||
cmd.AdminTarget(srv.URL, "us-west-1", writeServerCA(t, srv)),
|
||||
_, err := cli.CreateAccountRemote(t.Context(),
|
||||
cli.AdminTarget(srv.URL, "us-west-1", writeServerCA(t, srv)),
|
||||
gateway.CreateAccountRequest{Name: "ben@example.com", ClientToken: strings.Repeat("a", 32)})
|
||||
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "AccountAlreadyExists")
|
||||
assert.Contains(t, err.Error(), "req-1")
|
||||
assert.False(t, cmd.RetryableAdminError(err), "a taken name is not fixed by retrying")
|
||||
assert.False(t, cli.RetryableAdminError(err), "a taken name is not fixed by retrying")
|
||||
}
|
||||
|
||||
// An untrusted certificate must fail rather than fall back to no verification.
|
||||
@@ -217,8 +217,8 @@ func TestCreateAccountRemoteRefusesUntrustedServer(t *testing.T) {
|
||||
var got http.Request
|
||||
srv := newAdminTestServer(t, http.StatusOK, createAccountOK, &got)
|
||||
|
||||
_, err := cmd.CreateAccountRemote(t.Context(),
|
||||
cmd.AdminTarget(srv.URL, "us-west-1", ""),
|
||||
_, err := cli.CreateAccountRemote(t.Context(),
|
||||
cli.AdminTarget(srv.URL, "us-west-1", ""),
|
||||
gateway.CreateAccountRequest{Name: "ben@example.com", ClientToken: strings.Repeat("a", 32)})
|
||||
assert.Error(t, err)
|
||||
}
|
||||
@@ -276,10 +276,10 @@ func TestDecodeAdminErrorRetryability(t *testing.T) {
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
err := cmd.DecodeAdminError(tc.status, []byte(tc.body))
|
||||
err := cli.DecodeAdminError(tc.status, []byte(tc.body))
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), tc.wantMessage)
|
||||
assert.Equal(t, tc.wantRetry, cmd.RetryableAdminError(err))
|
||||
assert.Equal(t, tc.wantRetry, cli.RetryableAdminError(err))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package cmd
|
||||
package cli
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
@@ -1,4 +1,4 @@
|
||||
package cmd
|
||||
package cli
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package cmd
|
||||
package cli
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
@@ -1,4 +1,4 @@
|
||||
package cmd
|
||||
package cli
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
@@ -1,4 +1,4 @@
|
||||
package cmd
|
||||
package cli
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
@@ -1,7 +1,7 @@
|
||||
//test:in-package — the file exists to expose unexported identifiers to the
|
||||
// external cmd_test package, which it can only do from inside cmd.
|
||||
|
||||
package cmd
|
||||
package cli
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
@@ -1,4 +1,4 @@
|
||||
package cmd
|
||||
package cli
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
@@ -229,7 +229,15 @@ func runGetNodes(cmd *cobra.Command, args []string) {
|
||||
}
|
||||
}
|
||||
|
||||
pterm.DefaultTable.WithHasHeader().WithLeftAlignment().WithData(tableData).Render()
|
||||
// Render discards its write error, so write the table here to fail on it.
|
||||
rendered, err := pterm.DefaultTable.WithHasHeader().WithLeftAlignment().WithData(tableData).Srender()
|
||||
if err == nil {
|
||||
_, err = fmt.Fprintln(os.Stdout, rendered)
|
||||
}
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error: render table: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func runGetVMs(cmd *cobra.Command, args []string) {
|
||||
@@ -305,5 +313,13 @@ func runGetVMs(cmd *cobra.Command, args []string) {
|
||||
})
|
||||
}
|
||||
|
||||
pterm.DefaultTable.WithHasHeader().WithLeftAlignment().WithData(tableData).Render()
|
||||
// Render discards its write error, so write the table here to fail on it.
|
||||
rendered, err := pterm.DefaultTable.WithHasHeader().WithLeftAlignment().WithData(tableData).Srender()
|
||||
if err == nil {
|
||||
_, err = fmt.Fprintln(os.Stdout, rendered)
|
||||
}
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error: render table: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package cmd
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package cmd
|
||||
package cli
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
@@ -1,4 +1,4 @@
|
||||
package cmd
|
||||
package cli
|
||||
|
||||
import (
|
||||
"testing"
|
||||
@@ -1,4 +1,4 @@
|
||||
package cmd
|
||||
package cli
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
//test:in-package: swaps the unexported procRoot for a fake /proc tree and asserts on
|
||||
// the store helpers the init and join commands call.
|
||||
|
||||
package cmd
|
||||
package cli
|
||||
|
||||
import (
|
||||
"os"
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
//test:in-package — the embedded nats.conf template it checks is unexported,
|
||||
// and exporting it would widen the package's API for a test's convenience.
|
||||
|
||||
package cmd
|
||||
package cli
|
||||
|
||||
import (
|
||||
"strings"
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package cmd
|
||||
package cli
|
||||
|
||||
import (
|
||||
"os"
|
||||
@@ -1,4 +1,4 @@
|
||||
package cmd
|
||||
package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@@ -1,4 +1,4 @@
|
||||
package cmd
|
||||
package cli
|
||||
|
||||
import (
|
||||
"os"
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package cmd
|
||||
package cli
|
||||
|
||||
import (
|
||||
"os"
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package cmd
|
||||
package cli
|
||||
|
||||
import (
|
||||
"os"
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package cmd
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -1,4 +1,4 @@
|
||||
package cmd
|
||||
package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@@ -7,7 +7,6 @@ import (
|
||||
|
||||
"github.com/mulgadc/spinifex/spinifex/bootstrap/config"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -83,39 +82,39 @@ func init() {
|
||||
|
||||
// Global flags
|
||||
rootCmd.PersistentFlags().String("config", "", "config file (required)")
|
||||
viper.BindEnv("config", "SPINIFEX_CONFIG_PATH")
|
||||
viper.BindPFlag("config", rootCmd.PersistentFlags().Lookup("config"))
|
||||
mustBindEnv("config", "SPINIFEX_CONFIG_PATH")
|
||||
mustBindPFlag("config", rootCmd.PersistentFlags(), "config")
|
||||
|
||||
// Authentication (access_key, secret)
|
||||
rootCmd.PersistentFlags().String("access-key", "", "AWS access key (overrides config file and env)")
|
||||
viper.BindEnv("access-key", "SPINIFEX_ACCESS_KEY")
|
||||
viper.BindPFlag("access-key", rootCmd.PersistentFlags().Lookup("access-key"))
|
||||
mustBindEnv("access-key", "SPINIFEX_ACCESS_KEY")
|
||||
mustBindPFlag("access-key", rootCmd.PersistentFlags(), "access-key")
|
||||
|
||||
rootCmd.PersistentFlags().String("secret-key", "", "AWS secret key (overrides config file and env)")
|
||||
viper.BindEnv("secret-key", "SPINIFEX_SECRET_KEY")
|
||||
viper.BindPFlag("secret-key", rootCmd.PersistentFlags().Lookup("secret-key"))
|
||||
mustBindEnv("secret-key", "SPINIFEX_SECRET_KEY")
|
||||
mustBindPFlag("secret-key", rootCmd.PersistentFlags(), "secret-key")
|
||||
|
||||
rootCmd.PersistentFlags().String("host", "", "AWS Endpoint (overrides config file and env)")
|
||||
viper.BindEnv("host", "SPINIFEX_HOST")
|
||||
viper.BindPFlag("host", rootCmd.PersistentFlags().Lookup("host"))
|
||||
mustBindEnv("host", "SPINIFEX_HOST")
|
||||
mustBindPFlag("host", rootCmd.PersistentFlags(), "host")
|
||||
|
||||
// Viperblock config
|
||||
rootCmd.PersistentFlags().String("base-dir", "", "Viperblock base directory (overrides config file and env)")
|
||||
viper.BindEnv("base-dir", "SPINIFEX_BASE_DIR")
|
||||
viper.BindPFlag("base-dir", rootCmd.PersistentFlags().Lookup("base-dir"))
|
||||
mustBindEnv("base-dir", "SPINIFEX_BASE_DIR")
|
||||
mustBindPFlag("base-dir", rootCmd.PersistentFlags(), "base-dir")
|
||||
|
||||
// NATS specific flags
|
||||
rootCmd.PersistentFlags().String("nats-host", "", "NATS server host (overrides config file and env)")
|
||||
viper.BindEnv("nats-host", "SPINIFEX_NATS_HOST")
|
||||
viper.BindPFlag("nats-host", rootCmd.PersistentFlags().Lookup("nats-host"))
|
||||
mustBindEnv("nats-host", "SPINIFEX_NATS_HOST")
|
||||
mustBindPFlag("nats-host", rootCmd.PersistentFlags(), "nats-host")
|
||||
|
||||
rootCmd.PersistentFlags().String("nats-token", "", "NATS authentication token (overrides config file and env)")
|
||||
viper.BindEnv("nats-token", "SPINIFEX_NATS_TOKEN")
|
||||
viper.BindPFlag("nats-token", rootCmd.PersistentFlags().Lookup("nats-token"))
|
||||
mustBindEnv("nats-token", "SPINIFEX_NATS_TOKEN")
|
||||
mustBindPFlag("nats-token", rootCmd.PersistentFlags(), "nats-token")
|
||||
|
||||
rootCmd.PersistentFlags().String("nats-subject", "", "NATS subscription subject (overrides config file and env)")
|
||||
viper.BindEnv("nats-subject", "SPINIFEX_NATS_SUBJECT")
|
||||
viper.BindPFlag("nats-subject", rootCmd.PersistentFlags().Lookup("nats-subject"))
|
||||
mustBindEnv("nats-subject", "SPINIFEX_NATS_SUBJECT")
|
||||
mustBindPFlag("nats-subject", rootCmd.PersistentFlags(), "nats-subject")
|
||||
|
||||
// Bind flags to viper
|
||||
//viper.BindPFlag("nats.host", rootCmd.PersistentFlags().Lookup("nats-host"))
|
||||
@@ -1,4 +1,4 @@
|
||||
package cmd_test
|
||||
package cli_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/mulgadc/spinifex/cmd/spinifex/cmd"
|
||||
"github.com/mulgadc/spinifex/spinifex/operator/cli"
|
||||
)
|
||||
|
||||
// handlerAtStartup samples the process logger once this test package's variables
|
||||
@@ -45,12 +45,12 @@ func TestInitCLILoggerLevels(t *testing.T) {
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Setenv("SPX_LOG_LEVEL", tc.env)
|
||||
defer cmd.SetVerboseFlag(tc.verbose)()
|
||||
defer cli.SetVerboseFlag(tc.verbose)()
|
||||
|
||||
cmd.CLILogLevel.Set(slog.LevelError)
|
||||
cmd.InitCLILogger()
|
||||
cli.CLILogLevel.Set(slog.LevelError)
|
||||
cli.InitCLILogger()
|
||||
|
||||
if got := cmd.CLILogLevel.Level(); got != tc.want {
|
||||
if got := cli.CLILogLevel.Level(); got != tc.want {
|
||||
t.Errorf("level = %v, want %v", got, tc.want)
|
||||
}
|
||||
})
|
||||
@@ -1,4 +1,4 @@
|
||||
package cmd
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -991,82 +991,82 @@ func registerPredastoreNamespacedFlags() {
|
||||
// key's AutomaticEnv-derived name matches its explicit BindEnv target. Viper
|
||||
// checks AutomaticEnv first, so a bare key resolves SPINIFEX_HOST instead.
|
||||
func bindPredastoreNamespacedEnv() {
|
||||
viper.BindEnv("predastore-host", "SPINIFEX_PREDASTORE_HOST")
|
||||
viper.BindPFlag("predastore-host", predastoreCmd.PersistentFlags().Lookup("host"))
|
||||
mustBindEnv("predastore-host", "SPINIFEX_PREDASTORE_HOST")
|
||||
mustBindPFlag("predastore-host", predastoreCmd.PersistentFlags(), "host")
|
||||
|
||||
viper.BindEnv("predastore-base-path", "SPINIFEX_PREDASTORE_BASE_PATH")
|
||||
viper.BindPFlag("predastore-base-path", predastoreCmd.PersistentFlags().Lookup("base-path"))
|
||||
mustBindEnv("predastore-base-path", "SPINIFEX_PREDASTORE_BASE_PATH")
|
||||
mustBindPFlag("predastore-base-path", predastoreCmd.PersistentFlags(), "base-path")
|
||||
|
||||
viper.BindEnv("predastore-config-path", "SPINIFEX_PREDASTORE_CONFIG_PATH")
|
||||
viper.BindPFlag("predastore-config-path", predastoreCmd.PersistentFlags().Lookup("config-path"))
|
||||
mustBindEnv("predastore-config-path", "SPINIFEX_PREDASTORE_CONFIG_PATH")
|
||||
mustBindPFlag("predastore-config-path", predastoreCmd.PersistentFlags(), "config-path")
|
||||
}
|
||||
|
||||
// bindPredastoreCollisionEnv namespaces predastore's port, tls-cert, tls-key,
|
||||
// encryption-key-file and host-id keys, which nats and awsgw also bind bare.
|
||||
// Each derived env name now matches its own BindEnv target.
|
||||
func bindPredastoreCollisionEnv() {
|
||||
viper.BindEnv("predastore-port", "SPINIFEX_PREDASTORE_PORT")
|
||||
viper.BindPFlag("predastore-port", predastoreCmd.PersistentFlags().Lookup("port"))
|
||||
mustBindEnv("predastore-port", "SPINIFEX_PREDASTORE_PORT")
|
||||
mustBindPFlag("predastore-port", predastoreCmd.PersistentFlags(), "port")
|
||||
|
||||
viper.BindEnv("predastore-tls-cert", "SPINIFEX_PREDASTORE_TLS_CERT")
|
||||
viper.BindPFlag("predastore-tls-cert", predastoreCmd.PersistentFlags().Lookup("tls-cert"))
|
||||
mustBindEnv("predastore-tls-cert", "SPINIFEX_PREDASTORE_TLS_CERT")
|
||||
mustBindPFlag("predastore-tls-cert", predastoreCmd.PersistentFlags(), "tls-cert")
|
||||
|
||||
viper.BindEnv("predastore-tls-key", "SPINIFEX_PREDASTORE_TLS_KEY")
|
||||
viper.BindPFlag("predastore-tls-key", predastoreCmd.PersistentFlags().Lookup("tls-key"))
|
||||
mustBindEnv("predastore-tls-key", "SPINIFEX_PREDASTORE_TLS_KEY")
|
||||
mustBindPFlag("predastore-tls-key", predastoreCmd.PersistentFlags(), "tls-key")
|
||||
|
||||
viper.BindEnv("predastore-encryption-key-file", "SPINIFEX_PREDASTORE_ENCRYPTION_KEY_FILE")
|
||||
viper.BindPFlag("predastore-encryption-key-file", predastoreCmd.PersistentFlags().Lookup("encryption-key-file"))
|
||||
mustBindEnv("predastore-encryption-key-file", "SPINIFEX_PREDASTORE_ENCRYPTION_KEY_FILE")
|
||||
mustBindPFlag("predastore-encryption-key-file", predastoreCmd.PersistentFlags(), "encryption-key-file")
|
||||
|
||||
viper.BindEnv("predastore-host-id", "SPINIFEX_PREDASTORE_HOST_ID")
|
||||
viper.BindPFlag("predastore-host-id", predastoreCmd.PersistentFlags().Lookup("host-id"))
|
||||
mustBindEnv("predastore-host-id", "SPINIFEX_PREDASTORE_HOST_ID")
|
||||
mustBindPFlag("predastore-host-id", predastoreCmd.PersistentFlags(), "host-id")
|
||||
}
|
||||
|
||||
// bindNatsCollisionEnv namespaces nats's port, host and debug keys, which
|
||||
// awsgw also binds bare. The host key is "nats-service-host": rootCmd already
|
||||
// owns "nats-host" for its cluster-wide override, so reusing it would clobber.
|
||||
func bindNatsCollisionEnv() {
|
||||
viper.BindEnv("nats-port", "SPINIFEX_NATS_PORT")
|
||||
viper.BindPFlag("nats-port", natsCmd.PersistentFlags().Lookup("port"))
|
||||
mustBindEnv("nats-port", "SPINIFEX_NATS_PORT")
|
||||
mustBindPFlag("nats-port", natsCmd.PersistentFlags(), "port")
|
||||
|
||||
viper.BindEnv("nats-service-host", "SPINIFEX_NATS_HOST")
|
||||
viper.BindPFlag("nats-service-host", natsCmd.PersistentFlags().Lookup("host"))
|
||||
mustBindEnv("nats-service-host", "SPINIFEX_NATS_HOST")
|
||||
mustBindPFlag("nats-service-host", natsCmd.PersistentFlags(), "host")
|
||||
|
||||
viper.BindEnv("nats-debug", "SPINIFEX_NATS_DEBUG")
|
||||
viper.BindPFlag("nats-debug", natsCmd.PersistentFlags().Lookup("debug"))
|
||||
mustBindEnv("nats-debug", "SPINIFEX_NATS_DEBUG")
|
||||
mustBindPFlag("nats-debug", natsCmd.PersistentFlags(), "debug")
|
||||
}
|
||||
|
||||
// bindAwsgwCollisionEnv namespaces awsgw's host, tls-cert, tls-key and debug
|
||||
// viper keys, which predastore and/or nats also bind bare. Each
|
||||
// AutomaticEnv-derived name now matches its own explicit BindEnv target.
|
||||
func bindAwsgwCollisionEnv() {
|
||||
viper.BindEnv("awsgw-host", "SPINIFEX_AWSGW_HOST")
|
||||
viper.BindPFlag("awsgw-host", awsgwCmd.PersistentFlags().Lookup("host"))
|
||||
mustBindEnv("awsgw-host", "SPINIFEX_AWSGW_HOST")
|
||||
mustBindPFlag("awsgw-host", awsgwCmd.PersistentFlags(), "host")
|
||||
|
||||
viper.BindEnv("awsgw-tls-cert", "SPINIFEX_AWSGW_TLS_CERT")
|
||||
viper.BindPFlag("awsgw-tls-cert", awsgwCmd.PersistentFlags().Lookup("tls-cert"))
|
||||
mustBindEnv("awsgw-tls-cert", "SPINIFEX_AWSGW_TLS_CERT")
|
||||
mustBindPFlag("awsgw-tls-cert", awsgwCmd.PersistentFlags(), "tls-cert")
|
||||
|
||||
viper.BindEnv("awsgw-tls-key", "SPINIFEX_AWSGW_TLS_KEY")
|
||||
viper.BindPFlag("awsgw-tls-key", awsgwCmd.PersistentFlags().Lookup("tls-key"))
|
||||
mustBindEnv("awsgw-tls-key", "SPINIFEX_AWSGW_TLS_KEY")
|
||||
mustBindPFlag("awsgw-tls-key", awsgwCmd.PersistentFlags(), "tls-key")
|
||||
|
||||
viper.BindEnv("awsgw-debug", "SPINIFEX_AWSGW_DEBUG")
|
||||
viper.BindPFlag("awsgw-debug", awsgwCmd.PersistentFlags().Lookup("debug"))
|
||||
mustBindEnv("awsgw-debug", "SPINIFEX_AWSGW_DEBUG")
|
||||
mustBindPFlag("awsgw-debug", awsgwCmd.PersistentFlags(), "debug")
|
||||
}
|
||||
|
||||
// bindViperblockEnv binds viperblock's S3 and plugin flags. The lookups must
|
||||
// target viperblockCmd, which declares them; a predastoreCmd lookup yields nil
|
||||
// and BindPFlag drops it silently, hiding both the flag and its default.
|
||||
func bindViperblockEnv() {
|
||||
viper.BindEnv("s3-host", "SPINIFEX_VIPERBLOCK_S3_HOST")
|
||||
viper.BindPFlag("s3-host", viperblockCmd.PersistentFlags().Lookup("s3-host"))
|
||||
mustBindEnv("s3-host", "SPINIFEX_VIPERBLOCK_S3_HOST")
|
||||
mustBindPFlag("s3-host", viperblockCmd.PersistentFlags(), "s3-host")
|
||||
|
||||
viper.BindEnv("s3-bucket", "SPINIFEX_VIPERBLOCK_S3_BUCKET")
|
||||
viper.BindPFlag("s3-bucket", viperblockCmd.PersistentFlags().Lookup("s3-bucket"))
|
||||
mustBindEnv("s3-bucket", "SPINIFEX_VIPERBLOCK_S3_BUCKET")
|
||||
mustBindPFlag("s3-bucket", viperblockCmd.PersistentFlags(), "s3-bucket")
|
||||
|
||||
viper.BindEnv("s3-region", "SPINIFEX_VIPERBLOCK_S3_REGION")
|
||||
viper.BindPFlag("s3-region", viperblockCmd.PersistentFlags().Lookup("s3-region"))
|
||||
mustBindEnv("s3-region", "SPINIFEX_VIPERBLOCK_S3_REGION")
|
||||
mustBindPFlag("s3-region", viperblockCmd.PersistentFlags(), "s3-region")
|
||||
|
||||
viper.BindEnv("plugin-path", "SPINIFEX_VIPERBLOCK_PLUGIN_PATH")
|
||||
viper.BindPFlag("plugin-path", viperblockCmd.PersistentFlags().Lookup("plugin-path"))
|
||||
mustBindEnv("plugin-path", "SPINIFEX_VIPERBLOCK_PLUGIN_PATH")
|
||||
mustBindPFlag("plugin-path", viperblockCmd.PersistentFlags(), "plugin-path")
|
||||
}
|
||||
|
||||
func init() {
|
||||
@@ -1123,20 +1123,20 @@ func init() {
|
||||
// key from predastore's encryption-key-file so the two BindPFlag calls
|
||||
// don't collide globally.
|
||||
viperblockCmd.PersistentFlags().String("encryption-key-file", "", "Path to the 32-byte AES-256 master key file for at-rest encryption (empty disables encryption)")
|
||||
viper.BindEnv("viperblock-encryption-key-file", "SPINIFEX_VIPERBLOCK_ENCRYPTION_KEY_FILE")
|
||||
viper.BindPFlag("viperblock-encryption-key-file", viperblockCmd.PersistentFlags().Lookup("encryption-key-file"))
|
||||
mustBindEnv("viperblock-encryption-key-file", "SPINIFEX_VIPERBLOCK_ENCRYPTION_KEY_FILE")
|
||||
mustBindPFlag("viperblock-encryption-key-file", viperblockCmd.PersistentFlags(), "encryption-key-file")
|
||||
|
||||
viperblockCmd.PersistentFlags().Bool("debug", false, "Viperblock (EBS) debug logging")
|
||||
viper.BindEnv("viperblock-debug", "SPINIFEX_VIPERBLOCK_DEBUG")
|
||||
viper.BindPFlag("viperblock-debug", viperblockCmd.PersistentFlags().Lookup("debug"))
|
||||
mustBindEnv("viperblock-debug", "SPINIFEX_VIPERBLOCK_DEBUG")
|
||||
mustBindPFlag("viperblock-debug", viperblockCmd.PersistentFlags(), "debug")
|
||||
|
||||
viperblockCmd.AddCommand(viperblockStartCmd)
|
||||
|
||||
serviceCmd.AddCommand(qemunbdCmd)
|
||||
|
||||
qemunbdCmd.PersistentFlags().Bool("debug", false, "qemunbd (EBS) debug logging")
|
||||
viper.BindEnv("qemunbd-debug", "SPINIFEX_QEMUNBD_DEBUG")
|
||||
viper.BindPFlag("qemunbd-debug", qemunbdCmd.PersistentFlags().Lookup("debug"))
|
||||
mustBindEnv("qemunbd-debug", "SPINIFEX_QEMUNBD_DEBUG")
|
||||
mustBindPFlag("qemunbd-debug", qemunbdCmd.PersistentFlags(), "debug")
|
||||
|
||||
qemunbdCmd.AddCommand(qemunbdStartCmd)
|
||||
|
||||
@@ -1154,12 +1154,12 @@ func init() {
|
||||
bindNatsCollisionEnv()
|
||||
|
||||
natsCmd.PersistentFlags().String("data-dir", "", "NATS data directory")
|
||||
viper.BindEnv("data-dir", "SPINIFEX_NATS_DATA_DIR")
|
||||
viper.BindPFlag("data-dir", natsCmd.PersistentFlags().Lookup("data-dir"))
|
||||
mustBindEnv("data-dir", "SPINIFEX_NATS_DATA_DIR")
|
||||
mustBindPFlag("data-dir", natsCmd.PersistentFlags(), "data-dir")
|
||||
|
||||
natsCmd.PersistentFlags().Bool("jetstream", false, "Enable JetStream")
|
||||
viper.BindEnv("jetstream", "SPINIFEX_NATS_JETSTREAM")
|
||||
viper.BindPFlag("jetstream", natsCmd.PersistentFlags().Lookup("jetstream"))
|
||||
mustBindEnv("jetstream", "SPINIFEX_NATS_JETSTREAM")
|
||||
mustBindPFlag("jetstream", natsCmd.PersistentFlags(), "jetstream")
|
||||
|
||||
// Spinifex
|
||||
serviceCmd.AddCommand(spinifexCmd)
|
||||
@@ -1167,8 +1167,8 @@ func init() {
|
||||
spinifexCmd.AddCommand(spinifexStartCmd)
|
||||
|
||||
spinifexCmd.PersistentFlags().String("wal-dir", "", "Write-ahead log (WAL) directory. Place on high-speed NVMe disk, or tmpfs for development.")
|
||||
viper.BindEnv("wal-dir", "SPINIFEX_WAL_DIR")
|
||||
viper.BindPFlag("wal-dir", spinifexCmd.PersistentFlags().Lookup("wal-dir"))
|
||||
mustBindEnv("wal-dir", "SPINIFEX_WAL_DIR")
|
||||
mustBindPFlag("wal-dir", spinifexCmd.PersistentFlags(), "wal-dir")
|
||||
|
||||
// AWS GW
|
||||
serviceCmd.AddCommand(awsgwCmd)
|
||||
@@ -1192,28 +1192,28 @@ func init() {
|
||||
serviceCmd.AddCommand(spinifexUICmd)
|
||||
|
||||
spinifexUICmd.PersistentFlags().Int("port", 3000, "spinifex-ui server port")
|
||||
viper.BindEnv("spinifex-ui-port", "SPINIFEX_UI_PORT")
|
||||
viper.BindPFlag("spinifex-ui-port", spinifexUICmd.PersistentFlags().Lookup("port"))
|
||||
mustBindEnv("spinifex-ui-port", "SPINIFEX_UI_PORT")
|
||||
mustBindPFlag("spinifex-ui-port", spinifexUICmd.PersistentFlags(), "port")
|
||||
|
||||
spinifexUICmd.PersistentFlags().String("host", "0.0.0.0", "spinifex-ui server host")
|
||||
viper.BindEnv("spinifex-ui-host", "SPINIFEX_UI_HOST")
|
||||
viper.BindPFlag("spinifex-ui-host", spinifexUICmd.PersistentFlags().Lookup("host"))
|
||||
mustBindEnv("spinifex-ui-host", "SPINIFEX_UI_HOST")
|
||||
mustBindPFlag("spinifex-ui-host", spinifexUICmd.PersistentFlags(), "host")
|
||||
|
||||
spinifexUICmd.PersistentFlags().String("tls-cert", "", "TLS certificate path")
|
||||
viper.BindEnv("spinifex-ui-tls-cert", "SPINIFEX_UI_TLS_CERT")
|
||||
viper.BindPFlag("spinifex-ui-tls-cert", spinifexUICmd.PersistentFlags().Lookup("tls-cert"))
|
||||
mustBindEnv("spinifex-ui-tls-cert", "SPINIFEX_UI_TLS_CERT")
|
||||
mustBindPFlag("spinifex-ui-tls-cert", spinifexUICmd.PersistentFlags(), "tls-cert")
|
||||
|
||||
spinifexUICmd.PersistentFlags().String("tls-key", "", "TLS key path")
|
||||
viper.BindEnv("spinifex-ui-tls-key", "SPINIFEX_UI_TLS_KEY")
|
||||
viper.BindPFlag("spinifex-ui-tls-key", spinifexUICmd.PersistentFlags().Lookup("tls-key"))
|
||||
mustBindEnv("spinifex-ui-tls-key", "SPINIFEX_UI_TLS_KEY")
|
||||
mustBindPFlag("spinifex-ui-tls-key", spinifexUICmd.PersistentFlags(), "tls-key")
|
||||
|
||||
spinifexUICmd.PersistentFlags().String("base-dir", "", "spinifex-ui base directory for PID files and state")
|
||||
viper.BindEnv("spinifex-ui-base-dir", "SPINIFEX_UI_BASE_DIR")
|
||||
viper.BindPFlag("spinifex-ui-base-dir", spinifexUICmd.PersistentFlags().Lookup("base-dir"))
|
||||
mustBindEnv("spinifex-ui-base-dir", "SPINIFEX_UI_BASE_DIR")
|
||||
mustBindPFlag("spinifex-ui-base-dir", spinifexUICmd.PersistentFlags(), "base-dir")
|
||||
|
||||
spinifexUICmd.PersistentFlags().Bool("ochre-enabled", false, "enable the Ochre console")
|
||||
viper.BindEnv("spinifex-ui-ochre-enabled", "SPINIFEX_UI_OCHRE_ENABLED")
|
||||
viper.BindPFlag("spinifex-ui-ochre-enabled", spinifexUICmd.PersistentFlags().Lookup("ochre-enabled"))
|
||||
mustBindEnv("spinifex-ui-ochre-enabled", "SPINIFEX_UI_OCHRE_ENABLED")
|
||||
mustBindPFlag("spinifex-ui-ochre-enabled", spinifexUICmd.PersistentFlags(), "ochre-enabled")
|
||||
|
||||
spinifexUICmd.AddCommand(spinifexUIStartCmd)
|
||||
|
||||
@@ -1226,8 +1226,8 @@ func init() {
|
||||
serviceCmd.AddCommand(northstarCmd)
|
||||
|
||||
northstarCmd.PersistentFlags().String("northstar-config", "", "Path to northstar.toml (overrides config file)")
|
||||
viper.BindEnv("northstar-config", "SPINIFEX_NORTHSTAR_CONFIG")
|
||||
viper.BindPFlag("northstar-config", northstarCmd.PersistentFlags().Lookup("northstar-config"))
|
||||
mustBindEnv("northstar-config", "SPINIFEX_NORTHSTAR_CONFIG")
|
||||
mustBindPFlag("northstar-config", northstarCmd.PersistentFlags(), "northstar-config")
|
||||
|
||||
northstarCmd.AddCommand(northstarStartCmd)
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package cmd
|
||||
package cli
|
||||
|
||||
import (
|
||||
"strings"
|
||||
+5
-5
@@ -1,10 +1,10 @@
|
||||
package cmd_test
|
||||
package cli_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/mulgadc/spinifex/cmd/spinifex/cmd"
|
||||
"github.com/mulgadc/spinifex/spinifex/bootstrap/config"
|
||||
"github.com/mulgadc/spinifex/spinifex/operator/cli"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
@@ -18,7 +18,7 @@ func TestConsoleRegion_ReadsTheServingNode(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
region, err := cmd.ConsoleRegion(cfg)
|
||||
region, err := cli.ConsoleRegion(cfg)
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "us-west-1", region)
|
||||
@@ -31,14 +31,14 @@ func TestConsoleRegion_FailsWhenNodeHasNoRegion(t *testing.T) {
|
||||
Nodes: map[string]config.Config{"node1": {}},
|
||||
}
|
||||
|
||||
_, err := cmd.ConsoleRegion(cfg)
|
||||
_, err := cli.ConsoleRegion(cfg)
|
||||
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "node1")
|
||||
}
|
||||
|
||||
func TestConsoleRegion_FailsWhenNodeIsAbsent(t *testing.T) {
|
||||
_, err := cmd.ConsoleRegion(&config.ClusterConfig{Node: "ghost"})
|
||||
_, err := cli.ConsoleRegion(&config.ClusterConfig{Node: "ghost"})
|
||||
|
||||
require.Error(t, err)
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package cmd
|
||||
package cli
|
||||
|
||||
// Drives the unexported *cobra.Command vars (predastoreStartCmd, etc.)
|
||||
// directly via RunE, so it must stay in-package.
|
||||
@@ -1,4 +1,4 @@
|
||||
package cmd
|
||||
package cli
|
||||
|
||||
import (
|
||||
"errors"
|
||||
@@ -1,4 +1,4 @@
|
||||
package cmd
|
||||
package cli
|
||||
|
||||
import "testing"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package cmd
|
||||
package cli
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
@@ -113,7 +113,15 @@ func runTopNodes(cmd *cobra.Command, args []string) {
|
||||
}
|
||||
}
|
||||
|
||||
pterm.DefaultTable.WithHasHeader().WithLeftAlignment().WithData(nodeTable).Render()
|
||||
// Render discards its write error, so write the table here to fail on it.
|
||||
rendered, err := pterm.DefaultTable.WithHasHeader().WithLeftAlignment().WithData(nodeTable).Srender()
|
||||
if err == nil {
|
||||
_, err = fmt.Fprintln(os.Stdout, rendered)
|
||||
}
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error: render table: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Instance type capacity summary
|
||||
if len(capacityMap) == 0 {
|
||||
@@ -138,7 +146,15 @@ func runTopNodes(cmd *cobra.Command, args []string) {
|
||||
})
|
||||
}
|
||||
|
||||
pterm.DefaultTable.WithHasHeader().WithLeftAlignment().WithData(capTable).Render()
|
||||
// Render discards its write error, so write the table here to fail on it.
|
||||
rendered, err = pterm.DefaultTable.WithHasHeader().WithLeftAlignment().WithData(capTable).Srender()
|
||||
if err == nil {
|
||||
_, err = fmt.Fprintln(os.Stdout, rendered)
|
||||
}
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error: render table: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
type aggregatedCap struct {
|
||||
@@ -1,4 +1,4 @@
|
||||
package cmd
|
||||
package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
)
|
||||
|
||||
// Version variables set via ldflags at build time.
|
||||
// Example: go build -ldflags "-X github.com/mulgadc/spinifex/cmd/spinifex/cmd.Version=v1.0.0".
|
||||
// Example: go build -ldflags "-X github.com/mulgadc/spinifex/spinifex/operator/cli.Version=v1.0.0".
|
||||
var (
|
||||
Version = "dev"
|
||||
Commit = "unknown"
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package cmd
|
||||
package cli
|
||||
|
||||
import (
|
||||
"testing"
|
||||
@@ -0,0 +1,33 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
// mustBindEnv binds a config key to its environment variables. A wiring error
|
||||
// would otherwise leave the key silently unbound, so it panics at startup.
|
||||
func mustBindEnv(key string, envVars ...string) {
|
||||
if err := viper.BindEnv(append([]string{key}, envVars...)...); err != nil {
|
||||
panic(fmt.Sprintf("bind config key %q to env %v: %v", key, envVars, err)) //nolint:forbidigo // Startup wiring error: fail loudly rather than run with a silently unbound flag or config key.
|
||||
}
|
||||
}
|
||||
|
||||
// mustBindPFlag binds a config key to a flag. Looking the flag up on a command
|
||||
// that does not declare it yields nil, which viper would otherwise drop silently.
|
||||
func mustBindPFlag(key string, flags *pflag.FlagSet, name string) {
|
||||
if err := viper.BindPFlag(key, flags.Lookup(name)); err != nil {
|
||||
panic(fmt.Sprintf("bind config key %q to flag %q: %v", key, name, err)) //nolint:forbidigo // Startup wiring error: fail loudly rather than run with a silently unbound flag or config key.
|
||||
}
|
||||
}
|
||||
|
||||
// mustMarkFlagRequired marks a flag required, panicking when the command does
|
||||
// not declare it rather than silently dropping the required check.
|
||||
func mustMarkFlagRequired(cmd *cobra.Command, name string) {
|
||||
if err := cmd.MarkFlagRequired(name); err != nil {
|
||||
panic(fmt.Sprintf("mark flag %q required on %q: %v", name, cmd.Name(), err)) //nolint:forbidigo // Startup wiring error: fail loudly rather than run with a silently unbound flag or config key.
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestMustBindPFlag_PanicsNamingKeyAndFlagWhenUndeclared(t *testing.T) {
|
||||
flags := pflag.NewFlagSet("declares-other", pflag.ContinueOnError)
|
||||
flags.String("other", "", "")
|
||||
assert.PanicsWithValue(t, `bind config key "s3-host" to flag "s3-host": flag for "s3-host" is nil`, func() {
|
||||
mustBindPFlag("s3-host", flags, "s3-host")
|
||||
})
|
||||
}
|
||||
|
||||
func TestMustMarkFlagRequired_PanicsNamingFlagAndCommandWhenUndeclared(t *testing.T) {
|
||||
c := &cobra.Command{Use: "probe"}
|
||||
assert.PanicsWithValue(t, `mark flag "model-id" required on "probe": no such flag -model-id`, func() {
|
||||
mustMarkFlagRequired(c, "model-id")
|
||||
})
|
||||
}
|
||||
@@ -23,7 +23,7 @@ func templatePath(t *testing.T) string {
|
||||
t.Helper()
|
||||
_, thisFile, _, ok := runtime.Caller(0)
|
||||
require.True(t, ok)
|
||||
return filepath.Join(filepath.Dir(thisFile), "..", "..", "..", "..", "cmd", "spinifex", "cmd", "templates", "nats.conf")
|
||||
return filepath.Join(filepath.Dir(thisFile), "..", "..", "..", "..", "spinifex", "operator", "cli", "templates", "nats.conf")
|
||||
}
|
||||
|
||||
// TestRenderedConfig_EnforcesAuth renders the production nats.conf template,
|
||||
|
||||
Reference in New Issue
Block a user