diff --git a/configuration/configuration.go b/configuration/configuration.go index 5ef93c4..5a299a1 100644 --- a/configuration/configuration.go +++ b/configuration/configuration.go @@ -63,6 +63,10 @@ func (c *Configuration) Validate() error { if !(u.Scheme == "http" || u.Scheme == "https") { return fmt.Errorf("PVE host '%s' must be protocol type of HTTP or HTTPS.", host) } + + if u.Hostname() == "" { + return fmt.Errorf("PVE host '%s' must contain an IP address or DNS name.", host) + } } // Validate PVE token diff --git a/configuration/configuration_test.go b/configuration/configuration_test.go new file mode 100644 index 0000000..89c536d --- /dev/null +++ b/configuration/configuration_test.go @@ -0,0 +1,32 @@ +package configuration + +import "testing" + +func TestConfigurationValidateAcceptsIPAndDNSPveHosts(t *testing.T) { + tests := []struct { + name string + host string + }{ + {name: "IP address", host: "https://192.168.0.10:8006"}, + {name: "DNS name", host: "https://pve.example.com:8006"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + configuration := Configuration{ + Host: "0.0.0.0", + PVE: PveConfiguration{ + Hosts: []string{tt.host}, + Token: PveTokenConfiguration{ + TokenId: "token", + Secret: "secret", + }, + }, + } + + if err := configuration.Validate(); err != nil { + t.Fatalf("Validate() rejected %s host %q: %v", tt.name, tt.host, err) + } + }) + } +} diff --git a/proxmox/api_client.go b/proxmox/api_client.go index 1f5832d..a72f067 100644 --- a/proxmox/api_client.go +++ b/proxmox/api_client.go @@ -7,6 +7,7 @@ import ( "fmt" "net" "net/http" + "strings" "sync" "time" @@ -55,7 +56,7 @@ func NewApiClient(endpoints []string, tokenId string, secret string, checkInterv // Prepare API endpoints. for _, endpoint := range endpoints { apiEndpoint := ApiEndpoint{ - host: endpoint, + host: normalizeApiHost(endpoint), alive: false, } instance.endpoints = append(instance.endpoints, &apiEndpoint) @@ -86,6 +87,11 @@ func NewApiClient(endpoints []string, tokenId string, secret string, checkInterv return &instance } +// normalizeApiHost ensures API paths can be appended to either IP- or DNS-based hosts. +func normalizeApiHost(host string) string { + return strings.TrimRight(strings.TrimSpace(host), "/") + "/" +} + // Check endpoint liveness state. func (instance *ApiClient) checkEndpointsLiveness() { // We want to make sure other routines won't make any requests until we have checked for alive connections. diff --git a/proxmox/api_client_test.go b/proxmox/api_client_test.go new file mode 100644 index 0000000..3f9ff46 --- /dev/null +++ b/proxmox/api_client_test.go @@ -0,0 +1,49 @@ +package proxmox + +import ( + "testing" + "time" +) + +func TestNewApiClientNormalizesEndpointHosts(t *testing.T) { + tests := []struct { + name string + host string + want string + }{ + { + name: "IP without trailing slash", + host: "https://192.168.0.10:8006", + want: "https://192.168.0.10:8006/", + }, + { + name: "IP with trailing slash", + host: "https://192.168.0.10:8006/", + want: "https://192.168.0.10:8006/", + }, + { + name: "DNS without trailing slash", + host: "https://pve.example.com:8006", + want: "https://pve.example.com:8006/", + }, + { + name: "DNS with trailing slash", + host: "https://pve.example.com:8006/", + want: "https://pve.example.com:8006/", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + client := NewApiClient([]string{tt.host}, "token", "secret", time.Second) + defer client.httpClient.CloseIdleConnections() + + if got := client.endpoints[0].host; got != tt.want { + t.Fatalf("normalized endpoint host = %q, want %q", got, tt.want) + } + if got := client.endpoints[0].host + "api2/json/"; got != tt.want+"api2/json/" { + t.Fatalf("liveness URL = %q, want %q", got, tt.want+"api2/json/") + } + }) + } +}