diff --git a/.golangci.yml b/.golangci.yml index 3ee184fc4..4362dc2a6 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -22,6 +22,7 @@ linters: - errname # checks that sentinel errors are prefixed with Err, error types suffixed with Error - exptostd # detects golang.org/x/exp/ functions replaceable by stdlib - fatcontext # detects nested contexts in loops + - forcetypeassert # flags unchecked type assertions that can panic - gocheckcompilerdirectives # validates go compiler directive comments (//go:) - goprintffuncname # checks that printf-like functions are named with f at the end - gosec # inspects source code for security problems @@ -29,11 +30,13 @@ linters: - ineffassign # detects assignments to existing variables that are not used - makezero # finds slice declarations with non-zero initial length followed by append - mirror # catches wrong mirror patterns in bytes/strings usage + - musttag # checks structs passed to marshal/unmarshal have proper tags - modernize # modernizes code to use newer Go idioms (go fix analyzers) - nakedret # finds naked returns in functions - nilerr # catches code that returns nil even after checking err != nil - nilnesserr # catches returning a different nil error variable after err != nil check - nolintlint # reports ill-formed or insufficient nolint directives + - nosprintfhostport # flags fmt.Sprintf host:port — use net.JoinHostPort for IPv6 safety - predeclared # catches shadowing of Go builtins (len, cap, error, etc.) - reassign # checks that package variables are not reassigned - recvcheck # checks for receiver type consistency (pointer vs value) diff --git a/cmd/spinifex/cmd/admin.go b/cmd/spinifex/cmd/admin.go index 862eda52a..4bcc1dd83 100644 --- a/cmd/spinifex/cmd/admin.go +++ b/cmd/spinifex/cmd/admin.go @@ -1792,7 +1792,7 @@ func runAccountCreate(cmd *cobra.Command, args []string) { endpointHost = h } } - endpointURL := fmt.Sprintf("https://%s:9999", endpointHost) + endpointURL := "https://" + net.JoinHostPort(endpointHost, "9999") credPath := filepath.Join(homeDir, ".aws", "credentials") configPath := filepath.Join(homeDir, ".aws", "config") diff --git a/spinifex/admin/admin.go b/spinifex/admin/admin.go index 2f5256dfe..e8092b6cb 100644 --- a/spinifex/admin/admin.go +++ b/spinifex/admin/admin.go @@ -843,7 +843,7 @@ func SetupAWSCredentials(accessKey, secretKey, region, certPath, bindIP string) if err := UpdateAWSINIFile(configPath, configSection, map[string]string{ "region": region, - "endpoint_url": fmt.Sprintf("https://%s:9999", endpointHost), + "endpoint_url": "https://" + net.JoinHostPort(endpointHost, "9999"), "ca_bundle": certPath, "output": "json", }); err != nil { diff --git a/spinifex/config/config.go b/spinifex/config/config.go index fb498b643..3ef93893c 100644 --- a/spinifex/config/config.go +++ b/spinifex/config/config.go @@ -55,80 +55,80 @@ type BootstrapConfig struct { // Config holds all configuration for the application type Config struct { // Node config - Node string `mapstructure:"node"` - Host string `mapstructure:"host"` // Unique hostname or IP of this node - Region string `mapstructure:"region"` - AZ string `mapstructure:"az"` - DataDir string `mapstructure:"data_dir"` - Services []string `mapstructure:"services"` // Which services this node runs locally + Node string `json:"Node" mapstructure:"node"` + Host string `json:"Host" mapstructure:"host"` // Unique hostname or IP of this node + Region string `json:"Region" mapstructure:"region"` + AZ string `json:"AZ" mapstructure:"az"` + DataDir string `json:"DataDir" mapstructure:"data_dir"` + Services []string `json:"Services" mapstructure:"services"` // Which services this node runs locally - Daemon DaemonConfig `mapstructure:"daemon"` - NATS NATSConfig `mapstructure:"nats"` - Predastore PredastoreConfig `mapstructure:"predastore"` - Viperblock ViperblockConfig `mapstructure:"viperblock"` - AWSGW AWSGWConfig `mapstructure:"awsgw"` - VPCD VPCDConfig `mapstructure:"vpcd"` + Daemon DaemonConfig `json:"Daemon" mapstructure:"daemon"` + NATS NATSConfig `json:"NATS" mapstructure:"nats"` + Predastore PredastoreConfig `json:"Predastore" mapstructure:"predastore"` + Viperblock ViperblockConfig `json:"Viperblock" mapstructure:"viperblock"` + AWSGW AWSGWConfig `json:"AWSGW" mapstructure:"awsgw"` + VPCD VPCDConfig `json:"VPCD" mapstructure:"vpcd"` - BaseDir string `mapstructure:"base_dir"` - WalDir string `mapstructure:"wal_dir"` + BaseDir string `json:"BaseDir" mapstructure:"base_dir"` + WalDir string `json:"WalDir" mapstructure:"wal_dir"` } type AWSGWConfig struct { - Host string `mapstructure:"host"` - TLSKey string `mapstructure:"tlskey"` - TLSCert string `mapstructure:"tlscert"` - Config string `mapstructure:"config"` + Host string `json:"Host" mapstructure:"host"` + TLSKey string `json:"TLSKey" mapstructure:"tlskey"` + TLSCert string `json:"TLSCert" mapstructure:"tlscert"` + Config string `json:"Config" mapstructure:"config"` - Debug bool `mapstructure:"debug"` - ExpectedNodes int `mapstructure:"expected_nodes"` // TODO: Replace with root cluster config + Debug bool `json:"Debug" mapstructure:"debug"` + ExpectedNodes int `json:"ExpectedNodes" mapstructure:"expected_nodes"` // TODO: Replace with root cluster config } type ViperblockConfig struct { - ShardWAL *bool `mapstructure:"shardwal"` // Enable sharded WAL (default false when nil) + ShardWAL *bool `json:"ShardWAL" mapstructure:"shardwal"` // Enable sharded WAL (default false when nil) } // VPCDConfig holds the VPC daemon (vpcd) configuration. type VPCDConfig struct { - OVNNBAddr string `mapstructure:"ovn_nb_addr"` // OVN Northbound DB address (e.g., "tcp:127.0.0.1:6641") - OVNSBAddr string `mapstructure:"ovn_sb_addr"` // OVN Southbound DB address (e.g., "tcp:127.0.0.1:6642") - ExternalInterface string `mapstructure:"external_interface"` // WAN NIC name (e.g., "eth1", "enp0s3") — the physical NIC on the WAN bridge - WanBridge string `mapstructure:"wan_bridge"` // OVS bridge for WAN traffic (default "br-wan", maps to OVN "external" network) - BridgeMode string `mapstructure:"bridge_mode"` // "direct" or "macvlan" (auto-detected if empty) + OVNNBAddr string `json:"OVNNBAddr" mapstructure:"ovn_nb_addr"` // OVN Northbound DB address (e.g., "tcp:127.0.0.1:6641") + OVNSBAddr string `json:"OVNSBAddr" mapstructure:"ovn_sb_addr"` // OVN Southbound DB address (e.g., "tcp:127.0.0.1:6642") + ExternalInterface string `json:"ExternalInterface" mapstructure:"external_interface"` // WAN NIC name (e.g., "eth1", "enp0s3") — the physical NIC on the WAN bridge + WanBridge string `json:"WanBridge" mapstructure:"wan_bridge"` // OVS bridge for WAN traffic (default "br-wan", maps to OVN "external" network) + BridgeMode string `json:"BridgeMode" mapstructure:"bridge_mode"` // "direct" or "macvlan" (auto-detected if empty) } type PredastoreConfig struct { - Host string `mapstructure:"host"` - Bucket string `mapstructure:"bucket"` - Region string `mapstructure:"region"` - AccessKey string `mapstructure:"accesskey"` - SecretKey string `mapstructure:"secretkey"` - BaseDir string `mapstructure:"base_dir"` - NodeID int `mapstructure:"node_id"` + Host string `json:"Host" mapstructure:"host"` + Bucket string `json:"Bucket" mapstructure:"bucket"` + Region string `json:"Region" mapstructure:"region"` + AccessKey string `json:"AccessKey" mapstructure:"accesskey"` + SecretKey string `json:"SecretKey" mapstructure:"secretkey"` + BaseDir string `json:"BaseDir" mapstructure:"base_dir"` + NodeID int `json:"NodeID" mapstructure:"node_id"` } // DaemonConfig holds the daemon configuration type DaemonConfig struct { - Host string `mapstructure:"host"` - TLSKey string `mapstructure:"tlskey"` - TLSCert string `mapstructure:"tlscert"` - DevNetworking bool `mapstructure:"dev_networking"` // VPC instances get both TAP + hostfwd for SSH dev access + Host string `json:"Host" mapstructure:"host"` + TLSKey string `json:"TLSKey" mapstructure:"tlskey"` + TLSCert string `json:"TLSCert" mapstructure:"tlscert"` + DevNetworking bool `json:"DevNetworking" mapstructure:"dev_networking"` // VPC instances get both TAP + hostfwd for SSH dev access } // NATSConfig holds the NATS configuration type NATSConfig struct { - Host string `mapstructure:"host"` - ACL NATSACL `mapstructure:"acl"` - Sub NATSSub `mapstructure:"sub"` + Host string `json:"Host" mapstructure:"host"` + ACL NATSACL `json:"ACL" mapstructure:"acl"` + Sub NATSSub `json:"Sub" mapstructure:"sub"` } // NATSACL holds the NATS ACL configuration type NATSACL struct { - Token string `mapstructure:"token"` + Token string `json:"Token" mapstructure:"token"` } // NATSSub holds the NATS subscription configuration type NATSSub struct { - Subject string `mapstructure:"subject"` + Subject string `json:"Subject" mapstructure:"subject"` } // NodeBaseDir returns the BaseDir for the current node, or "" if the config diff --git a/spinifex/daemon/daemon_handlers.go b/spinifex/daemon/daemon_handlers.go index 424902082..3cd72b1bd 100644 --- a/spinifex/daemon/daemon_handlers.go +++ b/spinifex/daemon/daemon_handlers.go @@ -7,6 +7,7 @@ import ( "log/slog" "net" "net/http" + "strconv" "sync" "time" @@ -202,7 +203,7 @@ func (d *Daemon) queryNATSRole() string { if !d.config.HasService("nats") { return "" } - url := fmt.Sprintf("http://%s:%d/varz", d.daemonIP(), natsMonitorPort) + url := "http://" + net.JoinHostPort(d.daemonIP(), strconv.Itoa(natsMonitorPort)) + "/varz" return fetchNATSRole(url, roleHTTPClient) } @@ -212,7 +213,7 @@ func (d *Daemon) queryPredastoreRole() string { if !d.config.HasService("predastore") { return "" } - url := fmt.Sprintf("https://%s:%d/status", d.daemonIP(), predastoreDBPort) + url := "https://" + net.JoinHostPort(d.daemonIP(), strconv.Itoa(predastoreDBPort)) + "/status" return fetchPredastoreRole(url, roleTLSHTTPClient) } diff --git a/spinifex/gateway/spx/storage.go b/spinifex/gateway/spx/storage.go index f05753ee6..0498308f7 100644 --- a/spinifex/gateway/spx/storage.go +++ b/spinifex/gateway/spx/storage.go @@ -6,7 +6,9 @@ import ( "encoding/json" "fmt" "log/slog" + "net" "net/http" + "strconv" "sync" "time" @@ -117,7 +119,7 @@ func queryDBNodeStatus(ctx context.Context, out *DBNodeStatus, host string, port } // Query /health - healthURL := fmt.Sprintf("https://%s:%d/health", queryHost, port) + healthURL := "https://" + net.JoinHostPort(queryHost, strconv.Itoa(port)) + "/health" req, err := http.NewRequestWithContext(ctx, http.MethodGet, healthURL, nil) if err != nil { return @@ -131,7 +133,7 @@ func queryDBNodeStatus(ctx context.Context, out *DBNodeStatus, host string, port out.Healthy = resp.StatusCode == http.StatusOK // Query /status - statusURL := fmt.Sprintf("https://%s:%d/status", queryHost, port) + statusURL := "https://" + net.JoinHostPort(queryHost, strconv.Itoa(port)) + "/status" req, err = http.NewRequestWithContext(ctx, http.MethodGet, statusURL, nil) if err != nil { return diff --git a/spinifex/services/predastore/predastore_integration_test.go b/spinifex/services/predastore/predastore_integration_test.go index 3409fd605..7b3081c57 100644 --- a/spinifex/services/predastore/predastore_integration_test.go +++ b/spinifex/services/predastore/predastore_integration_test.go @@ -22,6 +22,7 @@ import ( "os" "path/filepath" "slices" + "strconv" "strings" "sync" "testing" @@ -252,7 +253,7 @@ func waitForServer(timeout time.Duration) bool { deadline := time.Now().Add(timeout) for time.Now().Before(deadline) { - resp, err := client.Get(fmt.Sprintf("https://%s:%d/", testHost, testPort)) + resp, err := client.Get("https://" + net.JoinHostPort(testHost, strconv.Itoa(testPort)) + "/") if err == nil { resp.Body.Close() // Give a bit more time for config to load @@ -273,7 +274,7 @@ func createS3Client(accessKey, secretKey string) *s3.S3 { sess := session.Must(session.NewSession(&aws.Config{ Region: aws.String(testRegion), - Endpoint: aws.String(fmt.Sprintf("https://%s:%d", testHost, testPort)), + Endpoint: aws.String("https://" + net.JoinHostPort(testHost, strconv.Itoa(testPort))), Credentials: credentials.NewStaticCredentials(accessKey, secretKey, ""), S3ForcePathStyle: aws.Bool(true), DisableSSL: aws.Bool(false), diff --git a/spinifex/services/spinifexui/spinifexui_test.go b/spinifex/services/spinifexui/spinifexui_test.go index 87678bde8..79961d0e4 100644 --- a/spinifex/services/spinifexui/spinifexui_test.go +++ b/spinifex/services/spinifexui/spinifexui_test.go @@ -318,7 +318,8 @@ func TestNewReverseProxy_StripsPrefixAndSetsHost(t *testing.T) { defer backend.Close() // Use the test server's TLS transport. - transport := backend.Client().Transport.(*http.Transport) + transport, ok := backend.Client().Transport.(*http.Transport) + require.True(t, ok, "expected *http.Transport") // Point the proxy at the test server instead of the real backend. backendAddr := backend.Listener.Addr().String() diff --git a/spinifex/services/spinifexui/tlssplit.go b/spinifex/services/spinifexui/tlssplit.go index e6288b81a..89c262488 100644 --- a/spinifex/services/spinifexui/tlssplit.go +++ b/spinifex/services/spinifexui/tlssplit.go @@ -4,10 +4,10 @@ import ( "bufio" "bytes" "crypto/tls" - "fmt" "io" "net" "net/http" + "strconv" "strings" "time" ) @@ -81,7 +81,7 @@ func (ln *tlsSplitListener) redirectHTTP(conn net.Conn, firstByte byte) { return } - target := fmt.Sprintf("https://%s:%d%s", host, ln.port, req.URL.RequestURI()) + target := "https://" + net.JoinHostPort(host, strconv.Itoa(ln.port)) + req.URL.RequestURI() resp := &http.Response{ StatusCode: http.StatusMovedPermanently, ProtoMajor: 1, diff --git a/spinifex/services/vpcd/topology_test.go b/spinifex/services/vpcd/topology_test.go index 02d3313e1..b59d6a18e 100644 --- a/spinifex/services/vpcd/topology_test.go +++ b/spinifex/services/vpcd/topology_test.go @@ -95,7 +95,9 @@ func TestTopologyHandler_VPCDelete(t *testing.T) { t.Fatalf("request vpc.delete: %v", err) } - var result struct{ Success bool } + var result struct { + Success bool `json:"Success"` + } _ = json.Unmarshal(resp.Data, &result) if !result.Success { t.Fatal("vpc.delete failed") diff --git a/spinifex/types/ebs.go b/spinifex/types/ebs.go index 34dba4fa2..11ab65786 100644 --- a/spinifex/types/ebs.go +++ b/spinifex/types/ebs.go @@ -3,19 +3,19 @@ package types import "sync" type EBSRequests struct { - Requests []EBSRequest `mapstructure:"ebs_requests"` + Requests []EBSRequest `json:"Requests" mapstructure:"ebs_requests"` Mu sync.Mutex `json:"-"` } type EBSRequest struct { - Name string - VolType string - Boot bool - EFI bool - CloudInit bool - DeleteOnTermination bool - NBDURI string // NBD URI - socket path (nbd:unix:/path.sock) or TCP (nbd://host:port) - DeviceName string // AWS API device name (e.g. /dev/sdf) for hot-plugged volumes + Name string `json:"Name"` + VolType string `json:"VolType"` + Boot bool `json:"Boot"` + EFI bool `json:"EFI"` + CloudInit bool `json:"CloudInit"` + DeleteOnTermination bool `json:"DeleteOnTermination"` + NBDURI string `json:"NBDURI"` // NBD URI - socket path (nbd:unix:/path.sock) or TCP (nbd://host:port) + DeviceName string `json:"DeviceName"` // AWS API device name (e.g. /dev/sdf) for hot-plugged volumes } // NBDTransport defines the transport type for NBD connections @@ -29,44 +29,44 @@ const ( ) type EBSMountResponse struct { - URI string - Mounted bool - Error string + URI string `json:"URI"` + Mounted bool `json:"Mounted"` + Error string `json:"Error"` } type EBSUnMountResponse struct { - Volume string - Mounted bool - Error string + Volume string `json:"Volume"` + Mounted bool `json:"Mounted"` + Error string `json:"Error"` } type EBSSyncRequest struct { - Volume string + Volume string `json:"Volume"` } type EBSSyncResponse struct { - Volume string - Synced bool - Error string + Volume string `json:"Volume"` + Synced bool `json:"Synced"` + Error string `json:"Error"` } type EBSDeleteRequest struct { - Volume string + Volume string `json:"Volume"` } type EBSDeleteResponse struct { - Volume string - Success bool - Error string + Volume string `json:"Volume"` + Success bool `json:"Success"` + Error string `json:"Error"` } type EBSSnapshotRequest struct { - Volume string - SnapshotID string + Volume string `json:"Volume"` + SnapshotID string `json:"SnapshotID"` } type EBSSnapshotResponse struct { - SnapshotID string - Success bool - Error string + SnapshotID string `json:"SnapshotID"` + Success bool `json:"Success"` + Error string `json:"Error"` } diff --git a/spinifex/utils/nbd.go b/spinifex/utils/nbd.go index 4d31b0fc3..28e75901e 100644 --- a/spinifex/utils/nbd.go +++ b/spinifex/utils/nbd.go @@ -3,6 +3,7 @@ package utils import ( "errors" "fmt" + "net" "path/filepath" "strconv" "strings" @@ -69,7 +70,7 @@ func FormatNBDSocketURI(socketPath string) string { // FormatNBDTCPURI formats a host:port as an NBD URI for QEMU. // Returns format: nbd://host:port func FormatNBDTCPURI(host string, port int) string { - return fmt.Sprintf("nbd://%s:%d", host, port) + return "nbd://" + net.JoinHostPort(host, strconv.Itoa(port)) } // ParseNBDURI parses an NBD URI into its components for QMP blockdev-add.