kat/internal/config/parse_test.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: "~/.kat/volumes",
BackupPath: "~/.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)
}
}