|
|
@@ -0,0 +1,190 @@
|
|
|
+/*
|
|
|
+Copyright 2026 LocoStack.
|
|
|
+
|
|
|
+Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
+you may not use this file except in compliance with the License.
|
|
|
+You may obtain a copy of the License at
|
|
|
+
|
|
|
+ http://www.apache.org/licenses/LICENSE-2.0
|
|
|
+
|
|
|
+Unless required by applicable law or agreed to in writing, software
|
|
|
+distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
+See the License for the specific language governing permissions and
|
|
|
+limitations under the License.
|
|
|
+*/
|
|
|
+
|
|
|
+package templates
|
|
|
+
|
|
|
+import (
|
|
|
+ "strings"
|
|
|
+ "testing"
|
|
|
+
|
|
|
+ "github.com/LocoStack/loco-operator/api/v1alpha1"
|
|
|
+ corev1 "k8s.io/api/core/v1"
|
|
|
+)
|
|
|
+
|
|
|
+func TestResolveTemplateMergesBaseAndOverride(t *testing.T) {
|
|
|
+ m := NewTemplateManager()
|
|
|
+ m.RegisterTemplate(v1alpha1.Template{
|
|
|
+ Name: "stack-default",
|
|
|
+ Spec: v1alpha1.TemplateSpec{
|
|
|
+ NodeSelector: map[string]string{"zone": "a", "disk": "hdd"},
|
|
|
+ Runtime: v1alpha1.RuntimeSpec{
|
|
|
+ Name: "go-runtime",
|
|
|
+ Image: "golang:1.24",
|
|
|
+ Port: 8080,
|
|
|
+ },
|
|
|
+ },
|
|
|
+ })
|
|
|
+
|
|
|
+ override := &v1alpha1.Template{
|
|
|
+ Name: "stack-default",
|
|
|
+ Spec: v1alpha1.TemplateSpec{
|
|
|
+ NodeSelector: map[string]string{"disk": "ssd"},
|
|
|
+ Runtime: v1alpha1.RuntimeSpec{
|
|
|
+ Image: "my-custom-image:1",
|
|
|
+ },
|
|
|
+ },
|
|
|
+ }
|
|
|
+
|
|
|
+ resolved, err := m.ResolveTemplate(override, "stack-default")
|
|
|
+ if err != nil {
|
|
|
+ t.Fatalf("expected no error, got %v", err)
|
|
|
+ }
|
|
|
+
|
|
|
+ if got := resolved.Spec.NodeSelector["zone"]; got != "a" {
|
|
|
+ t.Fatalf("expected inherited nodeSelector zone, got %q", got)
|
|
|
+ }
|
|
|
+ if got := resolved.Spec.NodeSelector["disk"]; got != "ssd" {
|
|
|
+ t.Fatalf("expected override nodeSelector disk, got %q", got)
|
|
|
+ }
|
|
|
+ if resolved.Spec.Runtime.Name != "go-runtime" {
|
|
|
+ t.Fatalf("expected runtime name from base, got %q", resolved.Spec.Runtime.Name)
|
|
|
+ }
|
|
|
+ if resolved.Spec.Runtime.Image != "my-custom-image:1" {
|
|
|
+ t.Fatalf("expected runtime image override, got %q", resolved.Spec.Runtime.Image)
|
|
|
+ }
|
|
|
+ if resolved.Spec.Runtime.Port != 8080 {
|
|
|
+ t.Fatalf("expected inherited runtime port, got %d", resolved.Spec.Runtime.Port)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+func TestResolveTemplateErrorsWhenTemplateMissing(t *testing.T) {
|
|
|
+ m := NewTemplateManager()
|
|
|
+
|
|
|
+ _, err := m.ResolveTemplate(&v1alpha1.Template{Name: "missing-template"}, "stack-default")
|
|
|
+ if err == nil {
|
|
|
+ t.Fatalf("expected error for missing template")
|
|
|
+ }
|
|
|
+ if !strings.Contains(err.Error(), "template \"missing-template\" not found") {
|
|
|
+ t.Fatalf("unexpected error: %v", err)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+func TestResolveTemplateErrorsWhenRuntimeMissing(t *testing.T) {
|
|
|
+ m := NewTemplateManager()
|
|
|
+ m.RegisterTemplate(v1alpha1.Template{
|
|
|
+ Name: "stack-default",
|
|
|
+ Spec: v1alpha1.TemplateSpec{
|
|
|
+ Runtime: v1alpha1.RuntimeSpec{Name: "go-runtime"},
|
|
|
+ },
|
|
|
+ })
|
|
|
+
|
|
|
+ _, err := m.ResolveTemplate(&v1alpha1.Template{
|
|
|
+ Name: "stack-default",
|
|
|
+ Spec: v1alpha1.TemplateSpec{
|
|
|
+ Runtime: v1alpha1.RuntimeSpec{Name: "python-runtime"},
|
|
|
+ },
|
|
|
+ }, "stack-default")
|
|
|
+ if err == nil {
|
|
|
+ t.Fatalf("expected error for missing runtime")
|
|
|
+ }
|
|
|
+ if !strings.Contains(err.Error(), "runtime \"python-runtime\" not found") {
|
|
|
+ t.Fatalf("unexpected error: %v", err)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+func TestResolveTemplateResolvesRuntimePresetAndMergesOverride(t *testing.T) {
|
|
|
+ m := NewTemplateManager()
|
|
|
+ m.RegisterTemplate(v1alpha1.Template{
|
|
|
+ Name: "stack-default",
|
|
|
+ Spec: v1alpha1.TemplateSpec{
|
|
|
+ Runtime: v1alpha1.RuntimeSpec{Name: "go-runtime", Image: "golang:1.24"},
|
|
|
+ },
|
|
|
+ })
|
|
|
+ m.RegisterRuntime(v1alpha1.RuntimeSpec{
|
|
|
+ Name: "python-runtime",
|
|
|
+ Image: "python:3.12",
|
|
|
+ Command: []string{"python"},
|
|
|
+ ExtraArgs: []string{"-m", "http.server"},
|
|
|
+ Env: []corev1.EnvVar{{Name: "BASE", Value: "1"}},
|
|
|
+ Port: 8000,
|
|
|
+ })
|
|
|
+
|
|
|
+ override := &v1alpha1.Template{
|
|
|
+ Name: "stack-default",
|
|
|
+ Spec: v1alpha1.TemplateSpec{
|
|
|
+ Runtime: v1alpha1.RuntimeSpec{
|
|
|
+ Name: "python-runtime",
|
|
|
+ Image: "python:3.12-slim",
|
|
|
+ Args: []string{"-u"},
|
|
|
+ Env: []corev1.EnvVar{{Name: "OVERRIDE", Value: "2"}},
|
|
|
+ },
|
|
|
+ },
|
|
|
+ }
|
|
|
+
|
|
|
+ resolved, err := m.ResolveTemplate(override, "stack-default")
|
|
|
+ if err != nil {
|
|
|
+ t.Fatalf("expected no error, got %v", err)
|
|
|
+ }
|
|
|
+
|
|
|
+ if resolved.Spec.Runtime.Name != "python-runtime" {
|
|
|
+ t.Fatalf("expected resolved runtime name, got %q", resolved.Spec.Runtime.Name)
|
|
|
+ }
|
|
|
+ if resolved.Spec.Runtime.Image != "python:3.12-slim" {
|
|
|
+ t.Fatalf("expected override image on resolved runtime, got %q", resolved.Spec.Runtime.Image)
|
|
|
+ }
|
|
|
+ if len(resolved.Spec.Runtime.Command) != 1 || resolved.Spec.Runtime.Command[0] != "python" {
|
|
|
+ t.Fatalf("expected inherited command from runtime preset")
|
|
|
+ }
|
|
|
+ if len(resolved.Spec.Runtime.ExtraArgs) != 2 || resolved.Spec.Runtime.ExtraArgs[0] != "-m" {
|
|
|
+ t.Fatalf("expected inherited extraArgs from runtime preset")
|
|
|
+ }
|
|
|
+ if len(resolved.Spec.Runtime.Args) != 1 || resolved.Spec.Runtime.Args[0] != "-u" {
|
|
|
+ t.Fatalf("expected override args on resolved runtime")
|
|
|
+ }
|
|
|
+ if len(resolved.Spec.Runtime.Env) != 1 || resolved.Spec.Runtime.Env[0].Name != "OVERRIDE" {
|
|
|
+ t.Fatalf("expected override env on resolved runtime")
|
|
|
+ }
|
|
|
+ if resolved.Spec.Runtime.Port != 8000 {
|
|
|
+ t.Fatalf("expected inherited port from runtime preset, got %d", resolved.Spec.Runtime.Port)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+func TestResolveTemplateWithoutNameUsesDefaultTemplate(t *testing.T) {
|
|
|
+ m := NewTemplateManager()
|
|
|
+ m.RegisterTemplate(v1alpha1.Template{
|
|
|
+ Name: "stack-default",
|
|
|
+ Spec: v1alpha1.TemplateSpec{
|
|
|
+ NodeSelector: map[string]string{"zone": "a", "disk": "hdd"},
|
|
|
+ },
|
|
|
+ })
|
|
|
+
|
|
|
+ in := &v1alpha1.Template{
|
|
|
+ Spec: v1alpha1.TemplateSpec{
|
|
|
+ NodeSelector: map[string]string{"disk": "ssd"},
|
|
|
+ },
|
|
|
+ }
|
|
|
+
|
|
|
+ resolved, err := m.ResolveTemplate(in, "stack-default")
|
|
|
+ if err != nil {
|
|
|
+ t.Fatalf("expected no error, got %v", err)
|
|
|
+ }
|
|
|
+ if got := resolved.Spec.NodeSelector["zone"]; got != "a" {
|
|
|
+ t.Fatalf("expected nodeSelector zone from default template, got %q", got)
|
|
|
+ }
|
|
|
+ if got := resolved.Spec.NodeSelector["disk"]; got != "ssd" {
|
|
|
+ t.Fatalf("expected nodeSelector disk override from input template, got %q", got)
|
|
|
+ }
|
|
|
+}
|