335 lines
11 KiB
Go
335 lines
11 KiB
Go
package config
|
|
|
|
import (
|
|
"os"
|
|
"strings"
|
|
"testing"
|
|
|
|
pb "git.dws.rip/dubey/kat/api/v1alpha1"
|
|
)
|
|
|
|
func createTestClusterKatFile(t *testing.T, content string) string {
|
|
t.Helper()
|
|
tmpFile, err := os.CreateTemp(t.TempDir(), "cluster.*.kat")
|
|
if err != nil {
|
|
t.Fatalf("Failed to create temp file: %v", err)
|
|
}
|
|
if _, err := tmpFile.WriteString(content); err != nil {
|
|
tmpFile.Close()
|
|
t.Fatalf("Failed to write to temp file: %v", err)
|
|
}
|
|
if err := tmpFile.Close(); err != nil {
|
|
t.Fatalf("Failed to close temp file: %v", err)
|
|
}
|
|
return tmpFile.Name()
|
|
}
|
|
|
|
func TestParseClusterConfiguration_Valid(t *testing.T) {
|
|
yamlContent := `
|
|
apiVersion: kat.dws.rip/v1alpha1
|
|
kind: ClusterConfiguration
|
|
metadata:
|
|
name: test-cluster
|
|
spec:
|
|
cluster_cidr: "10.0.0.0/16"
|
|
service_cidr: "10.1.0.0/16"
|
|
node_subnet_bits: 8 # /24 for nodes
|
|
api_port: 8080 # Non-default
|
|
`
|
|
filePath := createTestClusterKatFile(t, yamlContent)
|
|
|
|
config, err := ParseClusterConfiguration(filePath)
|
|
if err != nil {
|
|
t.Fatalf("ParseClusterConfiguration() error = %v, wantErr %v", err, false)
|
|
}
|
|
|
|
if config.Metadata.Name != "test-cluster" {
|
|
t.Errorf("Expected metadata.name 'test-cluster', got '%s'", config.Metadata.Name)
|
|
}
|
|
if config.Spec.ClusterCidr != "10.0.0.0/16" {
|
|
t.Errorf("Expected spec.clusterCIDR '10.0.0.0/16', got '%s'", config.Spec.ClusterCidr)
|
|
}
|
|
if config.Spec.ApiPort != 8080 {
|
|
t.Errorf("Expected spec.apiPort 8080, got %d", config.Spec.ApiPort)
|
|
}
|
|
// Check a default value
|
|
if config.Spec.ClusterDomain != DefaultClusterDomain {
|
|
t.Errorf("Expected default spec.clusterDomain '%s', got '%s'", DefaultClusterDomain, config.Spec.ClusterDomain)
|
|
}
|
|
if config.Spec.NodeSubnetBits != 8 {
|
|
t.Errorf("Expected spec.nodeSubnetBits 8, got %d", config.Spec.NodeSubnetBits)
|
|
}
|
|
}
|
|
|
|
func TestParseClusterConfiguration_FileNotFound(t *testing.T) {
|
|
_, err := ParseClusterConfiguration("nonexistent.kat")
|
|
if err == nil {
|
|
t.Fatalf("ParseClusterConfiguration() with non-existent file did not return an error")
|
|
}
|
|
if !strings.Contains(err.Error(), "file not found") {
|
|
t.Errorf("Expected 'file not found' error, got: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestParseClusterConfiguration_InvalidYAML(t *testing.T) {
|
|
filePath := createTestClusterKatFile(t, "this: is: not: valid: yaml")
|
|
_, err := ParseClusterConfiguration(filePath)
|
|
if err == nil {
|
|
t.Fatalf("ParseClusterConfiguration() with invalid YAML did not return an error")
|
|
}
|
|
if !strings.Contains(err.Error(), "unmarshal YAML") {
|
|
t.Errorf("Expected 'unmarshal YAML' error, got: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestParseClusterConfiguration_MissingRequiredFields(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
content string
|
|
wantErr string
|
|
}{
|
|
{
|
|
name: "missing metadata name",
|
|
content: `
|
|
apiVersion: kat.dws.rip/v1alpha1
|
|
kind: ClusterConfiguration
|
|
spec:
|
|
clusterCIDR: "10.0.0.0/16"
|
|
serviceCIDR: "10.1.0.0/16"
|
|
`,
|
|
wantErr: "metadata section not found",
|
|
},
|
|
{
|
|
name: "missing clusterCIDR",
|
|
content: `
|
|
apiVersion: kat.dws.rip/v1alpha1
|
|
kind: ClusterConfiguration
|
|
metadata:
|
|
name: test-cluster
|
|
spec:
|
|
serviceCIDR: "10.1.0.0/16"
|
|
`,
|
|
wantErr: "spec.clusterCIDR is required",
|
|
},
|
|
{
|
|
name: "invalid kind",
|
|
content: `
|
|
apiVersion: kat.dws.rip/v1alpha1
|
|
kind: WrongKind
|
|
metadata:
|
|
name: test-cluster
|
|
spec:
|
|
clusterCIDR: "10.0.0.0/16"
|
|
serviceCIDR: "10.1.0.0/16"
|
|
`,
|
|
wantErr: "invalid kind",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
filePath := createTestClusterKatFile(t, tt.content)
|
|
_, err := ParseClusterConfiguration(filePath)
|
|
if err == nil {
|
|
t.Fatalf("ParseClusterConfiguration() did not return an error for %s", tt.name)
|
|
}
|
|
if !strings.Contains(err.Error(), tt.wantErr) {
|
|
t.Errorf("Expected error containing '%s', got: %v", tt.wantErr, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSetClusterConfigDefaults(t *testing.T) {
|
|
config := &pb.ClusterConfiguration{
|
|
Spec: &pb.ClusterConfigurationSpec{},
|
|
}
|
|
SetClusterConfigDefaults(config)
|
|
|
|
if config.Spec.ClusterDomain != DefaultClusterDomain {
|
|
t.Errorf("DefaultClusterDomain: got %s, want %s", config.Spec.ClusterDomain, DefaultClusterDomain)
|
|
}
|
|
if config.Spec.ApiPort != DefaultApiPort {
|
|
t.Errorf("DefaultApiPort: got %d, want %d", config.Spec.ApiPort, DefaultApiPort)
|
|
}
|
|
if config.Spec.AgentPort != DefaultAgentPort {
|
|
t.Errorf("DefaultAgentPort: got %d, want %d", config.Spec.AgentPort, DefaultAgentPort)
|
|
}
|
|
if config.Spec.EtcdClientPort != DefaultEtcdClientPort {
|
|
t.Errorf("DefaultEtcdClientPort: got %d, want %d", config.Spec.EtcdClientPort, DefaultEtcdClientPort)
|
|
}
|
|
if config.Spec.EtcdPeerPort != DefaultEtcdPeerPort {
|
|
t.Errorf("DefaultEtcdPeerPort: got %d, want %d", config.Spec.EtcdPeerPort, DefaultEtcdPeerPort)
|
|
}
|
|
if config.Spec.VolumeBasePath != DefaultVolumeBasePath {
|
|
t.Errorf("DefaultVolumeBasePath: got %s, want %s", config.Spec.VolumeBasePath, DefaultVolumeBasePath)
|
|
}
|
|
if config.Spec.BackupPath != DefaultBackupPath {
|
|
t.Errorf("DefaultBackupPath: got %s, want %s", config.Spec.BackupPath, DefaultBackupPath)
|
|
}
|
|
if config.Spec.BackupIntervalMinutes != DefaultBackupIntervalMins {
|
|
t.Errorf("DefaultBackupIntervalMins: got %d, want %d", config.Spec.BackupIntervalMinutes, DefaultBackupIntervalMins)
|
|
}
|
|
if config.Spec.AgentTickSeconds != DefaultAgentTickSeconds {
|
|
t.Errorf("DefaultAgentTickSeconds: got %d, want %d", config.Spec.AgentTickSeconds, DefaultAgentTickSeconds)
|
|
}
|
|
if config.Spec.NodeLossTimeoutSeconds != DefaultNodeLossTimeoutSec {
|
|
t.Errorf("DefaultNodeLossTimeoutSec: got %d, want %d", config.Spec.NodeLossTimeoutSeconds, DefaultNodeLossTimeoutSec)
|
|
}
|
|
if config.Spec.NodeSubnetBits != DefaultNodeSubnetBits {
|
|
t.Errorf("DefaultNodeSubnetBits: got %d, want %d", config.Spec.NodeSubnetBits, DefaultNodeSubnetBits)
|
|
}
|
|
|
|
// Test NodeLossTimeoutSeconds derivation
|
|
configWithTick := &pb.ClusterConfiguration{
|
|
Spec: &pb.ClusterConfigurationSpec{AgentTickSeconds: 10},
|
|
}
|
|
SetClusterConfigDefaults(configWithTick)
|
|
if configWithTick.Spec.NodeLossTimeoutSeconds != 40 { // 10 * 4
|
|
t.Errorf("Derived NodeLossTimeoutSeconds: got %d, want %d", configWithTick.Spec.NodeLossTimeoutSeconds, 40)
|
|
}
|
|
}
|
|
|
|
func TestValidateClusterConfiguration_InvalidValues(t *testing.T) {
|
|
baseValidSpec := func() *pb.ClusterConfigurationSpec {
|
|
return &pb.ClusterConfigurationSpec{
|
|
ClusterCidr: "10.0.0.0/16",
|
|
ServiceCidr: "10.1.0.0/16",
|
|
NodeSubnetBits: 8,
|
|
ClusterDomain: "test.local",
|
|
AgentPort: 10250,
|
|
ApiPort: 10251,
|
|
EtcdPeerPort: 2380,
|
|
EtcdClientPort: 2379,
|
|
VolumeBasePath: "/var/lib/kat/volumes",
|
|
BackupPath: "/var/lib/kat/backups",
|
|
BackupIntervalMinutes: 30,
|
|
AgentTickSeconds: 15,
|
|
NodeLossTimeoutSeconds: 60,
|
|
}
|
|
}
|
|
baseValidMetadata := func() *pb.ObjectMeta {
|
|
return &pb.ObjectMeta{Name: "test"}
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
mutator func(cfg *pb.ClusterConfiguration)
|
|
wantErr string
|
|
}{
|
|
{"invalid clusterCIDR", func(cfg *pb.ClusterConfiguration) { cfg.Spec.ClusterCidr = "invalid" }, "invalid spec.clusterCIDR"},
|
|
{"invalid serviceCIDR", func(cfg *pb.ClusterConfiguration) { cfg.Spec.ServiceCidr = "invalid" }, "invalid spec.serviceCIDR"},
|
|
{"invalid agentPort low", func(cfg *pb.ClusterConfiguration) { cfg.Spec.AgentPort = 0 }, "invalid port for agentPort"},
|
|
{"invalid agentPort high", func(cfg *pb.ClusterConfiguration) { cfg.Spec.AgentPort = 70000 }, "invalid port for agentPort"},
|
|
{"port conflict", func(cfg *pb.ClusterConfiguration) { cfg.Spec.ApiPort = cfg.Spec.AgentPort }, "port conflict"},
|
|
{"invalid nodeSubnetBits low", func(cfg *pb.ClusterConfiguration) { cfg.Spec.NodeSubnetBits = 0 }, "invalid spec.nodeSubnetBits"},
|
|
{"invalid nodeSubnetBits high", func(cfg *pb.ClusterConfiguration) { cfg.Spec.NodeSubnetBits = 32 }, "invalid spec.nodeSubnetBits"},
|
|
{"invalid nodeSubnetBits vs clusterCIDR", func(cfg *pb.ClusterConfiguration) {
|
|
cfg.Spec.ClusterCidr = "10.0.0.0/28"
|
|
cfg.Spec.NodeSubnetBits = 8
|
|
}, "results in an invalid subnet size"},
|
|
{"invalid agentTickSeconds", func(cfg *pb.ClusterConfiguration) { cfg.Spec.AgentTickSeconds = 0 }, "agentTickSeconds must be positive"},
|
|
{"invalid nodeLossTimeoutSeconds", func(cfg *pb.ClusterConfiguration) { cfg.Spec.NodeLossTimeoutSeconds = 0 }, "nodeLossTimeoutSeconds must be positive"},
|
|
{"nodeLoss < agentTick", func(cfg *pb.ClusterConfiguration) {
|
|
cfg.Spec.NodeLossTimeoutSeconds = cfg.Spec.AgentTickSeconds - 1
|
|
}, "nodeLossTimeoutSeconds must be greater"},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
config := &pb.ClusterConfiguration{Metadata: baseValidMetadata(), Spec: baseValidSpec()}
|
|
tt.mutator(config)
|
|
err := ValidateClusterConfiguration(config)
|
|
if err == nil {
|
|
t.Fatalf("ValidateClusterConfiguration() did not return an error for %s", tt.name)
|
|
}
|
|
if !strings.Contains(err.Error(), tt.wantErr) {
|
|
t.Errorf("Expected error containing '%s', got: %v", tt.wantErr, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestParseQuadletDirectory_ValidSimple(t *testing.T) {
|
|
files := map[string][]byte{
|
|
"workload.kat": []byte(`
|
|
apiVersion: kat.dws.rip/v1alpha1
|
|
kind: Workload
|
|
metadata:
|
|
name: test-workload
|
|
spec:
|
|
type: SERVICE
|
|
source:
|
|
image: "nginx:latest"
|
|
`),
|
|
"vlb.kat": []byte(`
|
|
apiVersion: kat.dws.rip/v1alpha1
|
|
kind: VirtualLoadBalancer
|
|
metadata:
|
|
name: test-workload # Assumed to match workload name
|
|
spec:
|
|
ports:
|
|
- containerPort: 80
|
|
`),
|
|
}
|
|
|
|
parsed, err := ParseQuadletDirectory(files)
|
|
if err != nil {
|
|
t.Fatalf("ParseQuadletDirectory() error = %v", err)
|
|
}
|
|
if parsed.Workload == nil {
|
|
t.Fatal("Parsed Workload is nil")
|
|
}
|
|
if parsed.Workload.Metadata.Name != "test-workload" {
|
|
t.Errorf("Expected Workload name 'test-workload', got '%s'", parsed.Workload.Metadata.Name)
|
|
}
|
|
if parsed.VirtualLoadBalancer == nil {
|
|
t.Fatal("Parsed VirtualLoadBalancer is nil")
|
|
}
|
|
if parsed.VirtualLoadBalancer.Metadata.Name != "test-workload" {
|
|
t.Errorf("Expected VLB name 'test-workload', got '%s'", parsed.VirtualLoadBalancer.Metadata.Name)
|
|
}
|
|
}
|
|
|
|
func TestParseQuadletDirectory_MissingWorkload(t *testing.T) {
|
|
files := map[string][]byte{
|
|
"vlb.kat": []byte(`kind: VirtualLoadBalancer`),
|
|
}
|
|
_, err := ParseQuadletDirectory(files)
|
|
if err == nil {
|
|
t.Fatal("ParseQuadletDirectory() with missing workload.kat did not return an error")
|
|
}
|
|
if !strings.Contains(err.Error(), "required Workload definition (workload.kat) not found") {
|
|
t.Errorf("Expected 'required Workload' error, got: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestParseQuadletDirectory_MultipleWorkloads(t *testing.T) {
|
|
files := map[string][]byte{
|
|
"workload1.kat": []byte(`
|
|
apiVersion: kat.dws.rip/v1alpha1
|
|
kind: Workload
|
|
metadata:
|
|
name: wl1
|
|
spec:
|
|
type: SERVICE
|
|
source: {image: "img1"}`),
|
|
"workload2.kat": []byte(`
|
|
apiVersion: kat.dws.rip/v1alpha1
|
|
kind: Workload
|
|
metadata:
|
|
name: wl2
|
|
spec:
|
|
type: SERVICE
|
|
source: {image: "img2"}`),
|
|
}
|
|
|
|
_, err := ParseQuadletDirectory(files)
|
|
if err == nil {
|
|
t.Fatal("ParseQuadletDirectory() with multiple workload.kat did not return an error")
|
|
}
|
|
if !strings.Contains(err.Error(), "multiple Workload definitions found") {
|
|
t.Errorf("Expected 'multiple Workload' error, got: %v", err)
|
|
}
|
|
}
|