Skip to content

Commit 0c8be56

Browse files
Merge pull request #104 from numtide/feat/granular-resolver-logic
feat(resolver): implement granular resolution and default shard injection
2 parents 4c502d9 + 62aa426 commit 0c8be56

File tree

10 files changed

+686
-475
lines changed

10 files changed

+686
-475
lines changed

pkg/resolver/cell.go

Lines changed: 49 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,40 +8,74 @@ import (
88
"k8s.io/apimachinery/pkg/types"
99

1010
multigresv1alpha1 "github.com/numtide/multigres-operator/api/v1alpha1"
11+
corev1 "k8s.io/api/core/v1"
1112
)
1213

13-
// ResolveCellTemplate fetches and resolves a CellTemplate by name, handling defaults.
14+
// ResolveCell determines the final configuration for a specific Cell.
15+
// It orchestrates: Template Lookup -> Fetch -> Merge -> Defaulting.
16+
func (r *Resolver) ResolveCell(
17+
ctx context.Context,
18+
cellSpec *multigresv1alpha1.CellConfig,
19+
) (*multigresv1alpha1.StatelessSpec, *multigresv1alpha1.LocalTopoServerSpec, error) {
20+
// 1. Fetch Template (Logic handles defaults)
21+
templateName := cellSpec.CellTemplate
22+
tpl, err := r.ResolveCellTemplate(ctx, templateName)
23+
if err != nil {
24+
return nil, nil, err
25+
}
26+
27+
// 2. Merge Logic
28+
gateway, localTopo := mergeCellConfig(tpl, cellSpec.Overrides, cellSpec.Spec)
29+
30+
// 3. Apply Deep Defaults (Level 4)
31+
// We use empty resources for Gateway default, as the specific values are often deployment-dependent,
32+
// but we must ensure Replicas is at least 1.
33+
defaultStatelessSpec(gateway, corev1.ResourceRequirements{}, 1)
34+
35+
// Note: We do NOT default LocalTopo here because it is optional.
36+
// If it is nil, it remains nil (meaning the cell uses Global Topo).
37+
// If it is non-nil (e.g. from template), we apply Etcd defaults.
38+
if localTopo != nil && localTopo.Etcd != nil {
39+
defaultEtcdSpec(localTopo.Etcd)
40+
}
41+
42+
return gateway, localTopo, nil
43+
}
44+
45+
// ResolveCellTemplate fetches and resolves a CellTemplate by name.
46+
// If name is empty, it resolves using the Cluster Defaults, then the Namespace Default.
1447
func (r *Resolver) ResolveCellTemplate(
1548
ctx context.Context,
16-
templateName string,
49+
name string,
1750
) (*multigresv1alpha1.CellTemplate, error) {
18-
name := templateName
51+
resolvedName := name
1952
isImplicitFallback := false
2053

21-
if name == "" {
22-
name = r.TemplateDefaults.CellTemplate
54+
if resolvedName == "" {
55+
resolvedName = r.TemplateDefaults.CellTemplate
2356
}
24-
if name == "" {
25-
name = FallbackCellTemplate
57+
if resolvedName == "" {
58+
resolvedName = FallbackCellTemplate
2659
isImplicitFallback = true
2760
}
2861

2962
tpl := &multigresv1alpha1.CellTemplate{}
30-
err := r.Client.Get(ctx, types.NamespacedName{Name: name, Namespace: r.Namespace}, tpl)
63+
err := r.Client.Get(ctx, types.NamespacedName{Name: resolvedName, Namespace: r.Namespace}, tpl)
3164
if err != nil {
3265
if errors.IsNotFound(err) {
3366
if isImplicitFallback {
67+
// We return an empty struct instead of nil to satisfy tests expecting non-nil structure.
3468
return &multigresv1alpha1.CellTemplate{}, nil
3569
}
36-
return nil, fmt.Errorf("referenced CellTemplate '%s' not found: %w", name, err)
70+
return nil, fmt.Errorf("referenced CellTemplate '%s' not found: %w", resolvedName, err)
3771
}
3872
return nil, fmt.Errorf("failed to get CellTemplate: %w", err)
3973
}
4074
return tpl, nil
4175
}
4276

43-
// MergeCellConfig merges a template spec with overrides and an inline spec to produce the final configuration.
44-
func MergeCellConfig(
77+
// mergeCellConfig merges a template spec with overrides and an inline spec.
78+
func mergeCellConfig(
4579
template *multigresv1alpha1.CellTemplate,
4680
overrides *multigresv1alpha1.CellOverrides,
4781
inline *multigresv1alpha1.CellInlineSpec,
@@ -65,6 +99,10 @@ func MergeCellConfig(
6599
}
66100

67101
if inline != nil {
102+
// Inline spec completely replaces the template for the components it defines
103+
// However, for Multigres 'Spec' blocks, usually 'Spec' is exclusive to 'TemplateRef'.
104+
// The design allows "Inline Spec" OR "Template + Overrides".
105+
// If Inline Spec is present, we generally prefer it entirely.
68106
gw := inline.MultiGateway.DeepCopy()
69107
var topo *multigresv1alpha1.LocalTopoServerSpec
70108
if inline.LocalTopoServer != nil {

pkg/resolver/cell_test.go

Lines changed: 95 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,100 @@ import (
1616
"sigs.k8s.io/controller-runtime/pkg/client/fake"
1717
)
1818

19+
func TestResolver_ResolveCell(t *testing.T) {
20+
t.Parallel()
21+
22+
scheme := runtime.NewScheme()
23+
_ = multigresv1alpha1.AddToScheme(scheme)
24+
_, cellTpl, _, ns := setupFixtures(t)
25+
26+
tests := map[string]struct {
27+
config *multigresv1alpha1.CellConfig
28+
objects []client.Object
29+
wantGw *multigresv1alpha1.StatelessSpec
30+
wantTopo *multigresv1alpha1.LocalTopoServerSpec
31+
wantErr bool
32+
}{
33+
"Template Found": {
34+
config: &multigresv1alpha1.CellConfig{
35+
CellTemplate: "default",
36+
},
37+
objects: []client.Object{cellTpl},
38+
wantGw: &multigresv1alpha1.StatelessSpec{
39+
Replicas: ptr.To(int32(1)),
40+
Resources: corev1.ResourceRequirements{},
41+
},
42+
wantTopo: &multigresv1alpha1.LocalTopoServerSpec{
43+
Etcd: &multigresv1alpha1.EtcdSpec{
44+
Image: "local-etcd-default",
45+
Replicas: ptr.To(DefaultEtcdReplicas),
46+
Resources: DefaultResourcesEtcd(),
47+
Storage: multigresv1alpha1.StorageSpec{Size: DefaultEtcdStorageSize},
48+
},
49+
},
50+
},
51+
"Template Not Found (Error)": {
52+
config: &multigresv1alpha1.CellConfig{
53+
CellTemplate: "missing",
54+
},
55+
wantErr: true,
56+
},
57+
"Inline Overrides": {
58+
config: &multigresv1alpha1.CellConfig{
59+
Spec: &multigresv1alpha1.CellInlineSpec{
60+
MultiGateway: multigresv1alpha1.StatelessSpec{
61+
Replicas: ptr.To(int32(3)),
62+
},
63+
},
64+
},
65+
wantGw: &multigresv1alpha1.StatelessSpec{
66+
Replicas: ptr.To(int32(3)),
67+
Resources: corev1.ResourceRequirements{},
68+
},
69+
wantTopo: nil, // Inline spec didn't provide one
70+
},
71+
"Client Error": {
72+
config: &multigresv1alpha1.CellConfig{CellTemplate: "any"},
73+
// Will use mock client logic inside test runner
74+
wantErr: true,
75+
},
76+
}
77+
78+
for name, tc := range tests {
79+
t.Run(name, func(t *testing.T) {
80+
t.Parallel()
81+
var c client.Client
82+
if name == "Client Error" {
83+
c = &mockClient{failGet: true, err: errors.New("fail")}
84+
} else {
85+
c = fake.NewClientBuilder().
86+
WithScheme(scheme).
87+
WithObjects(tc.objects...).
88+
Build()
89+
}
90+
r := NewResolver(c, ns, multigresv1alpha1.TemplateDefaults{})
91+
92+
gw, topo, err := r.ResolveCell(t.Context(), tc.config)
93+
if tc.wantErr {
94+
if err == nil {
95+
t.Error("Expected error")
96+
}
97+
return
98+
}
99+
if err != nil {
100+
t.Fatalf("Unexpected error: %v", err)
101+
}
102+
103+
if diff := cmp.Diff(tc.wantGw, gw, cmpopts.IgnoreUnexported(resource.Quantity{}), cmpopts.EquateEmpty()); diff != "" {
104+
t.Errorf("Gateway Diff (-want +got):\n%s", diff)
105+
}
106+
if diff := cmp.Diff(tc.wantTopo, topo, cmpopts.IgnoreUnexported(resource.Quantity{}), cmpopts.EquateEmpty()); diff != "" {
107+
t.Errorf("Topo Diff (-want +got):\n%s", diff)
108+
}
109+
})
110+
}
111+
}
112+
19113
func TestResolver_ResolveCellTemplate(t *testing.T) {
20114
t.Parallel()
21115

@@ -237,7 +331,7 @@ func TestMergeCellConfig(t *testing.T) {
237331
for name, tc := range tests {
238332
t.Run(name, func(t *testing.T) {
239333
t.Parallel()
240-
gw, topo := MergeCellConfig(tc.tpl, tc.overrides, tc.inline)
334+
gw, topo := mergeCellConfig(tc.tpl, tc.overrides, tc.inline)
241335

242336
if diff := cmp.Diff(tc.wantGw, gw, cmpopts.IgnoreUnexported(resource.Quantity{}), cmpopts.EquateEmpty()); diff != "" {
243337
t.Errorf("Gateway mismatch (-want +got):\n%s", diff)

0 commit comments

Comments
 (0)