-
Notifications
You must be signed in to change notification settings - Fork 51
Expand file tree
/
Copy pathcmd_validate.go
More file actions
207 lines (173 loc) · 6.19 KB
/
cmd_validate.go
File metadata and controls
207 lines (173 loc) · 6.19 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
package playground
import (
"fmt"
"os"
"strings"
)
// ValidationResult holds the results of recipe validation
type ValidationResult struct {
Errors []string
Warnings []string
}
func (v *ValidationResult) AddError(format string, args ...interface{}) {
v.Errors = append(v.Errors, fmt.Sprintf(format, args...))
}
func (v *ValidationResult) AddWarning(format string, args ...interface{}) {
v.Warnings = append(v.Warnings, fmt.Sprintf(format, args...))
}
func (v *ValidationResult) IsValid() bool {
return len(v.Errors) == 0
}
// ValidateRecipe validates a recipe without starting it
func ValidateRecipe(recipe Recipe, baseRecipes []Recipe) *ValidationResult {
result := &ValidationResult{}
// For YAML recipes, do additional validation
if yamlRecipe, ok := recipe.(*YAMLRecipe); ok {
validateYAMLRecipe(yamlRecipe, baseRecipes, result)
}
// Build a minimal manifest to validate structure
exCtx := &ExContext{
LogLevel: LevelInfo,
Contender: &ContenderContext{
Enabled: false,
},
}
component := recipe.Apply(exCtx)
manifest := NewManifest("validation-test", component)
// Validate service names are unique
validateUniqueServiceNames(manifest, result)
// Validate port configurations
validatePorts(manifest, result)
// Validate dependencies
validateDependencies(manifest, result)
// Validate host paths exist (if specified)
validateHostPaths(manifest, result)
return result
}
func validateYAMLRecipe(recipe *YAMLRecipe, baseRecipes []Recipe, result *ValidationResult) {
// Check base recipe exists
baseFound := false
for _, r := range baseRecipes {
if r.Name() == recipe.config.Base {
baseFound = true
break
}
}
if !baseFound {
result.AddError("base recipe '%s' not found", recipe.config.Base)
}
// Check for potential component/service name mismatches
if recipe.config.Recipe != nil {
for componentName, componentConfig := range recipe.config.Recipe {
if componentConfig == nil {
continue
}
// Warn about common naming issues
if componentConfig.Remove {
// Check if trying to remove a component that might not exist
if strings.Contains(componentName, "-") {
result.AddWarning("removing component '%s' - verify this matches the base recipe component name", componentName)
}
}
if componentConfig.Services != nil {
for serviceName, serviceConfig := range componentConfig.Services {
if serviceConfig == nil {
continue
}
if serviceConfig.Remove {
result.AddWarning("removing service '%s' from component '%s' - verify names match base recipe", serviceName, componentName)
}
// Validate lifecycle cannot be used with host_path, release, or args
validateLifecycleConfig(serviceName, componentName, serviceConfig, result)
}
}
}
}
}
// validateLifecycleConfig checks that lifecycle_hooks is not used with incompatible options
// and that init/start/stop are only used when lifecycle_hooks is true
func validateLifecycleConfig(serviceName, componentName string, config *YAMLServiceConfig, result *ValidationResult) {
hasLifecycleFields := len(config.Init) > 0 || config.Start != "" || len(config.Stop) > 0
// If lifecycle_hooks is not set but lifecycle fields are used, that's an error
if !config.LifecycleHooks {
if hasLifecycleFields {
result.AddError("service '%s' in component '%s': init, start, and stop require lifecycle_hooks: true", serviceName, componentName)
}
return
}
// lifecycle_hooks is true - check for incompatible options
if config.HostPath != "" {
result.AddError("service '%s' in component '%s': lifecycle_hooks cannot be used with host_path", serviceName, componentName)
}
if config.Release != nil {
result.AddError("service '%s' in component '%s': lifecycle_hooks cannot be used with release", serviceName, componentName)
}
if len(config.Args) > 0 {
result.AddError("service '%s' in component '%s': lifecycle_hooks cannot be used with args", serviceName, componentName)
}
// Validate that at least one of init or start is specified
if len(config.Init) == 0 && config.Start == "" {
result.AddError("service '%s' in component '%s': lifecycle_hooks requires at least one of init or start", serviceName, componentName)
}
}
func validateUniqueServiceNames(manifest *Manifest, result *ValidationResult) {
seen := make(map[string]bool)
for _, svc := range manifest.Services {
if seen[svc.Name] {
result.AddError("duplicate service name: %s", svc.Name)
}
seen[svc.Name] = true
}
}
func validatePorts(manifest *Manifest, result *ValidationResult) {
// Check for port number conflicts across services
portUsage := make(map[int][]string) // port number -> service names
for _, svc := range manifest.Services {
for _, port := range svc.Ports {
portUsage[port.Port] = append(portUsage[port.Port], svc.Name)
}
}
for portNum, services := range portUsage {
if len(services) > 1 {
result.AddWarning("port %d used by multiple services: %s (may conflict if running on same host)", portNum, strings.Join(services, ", "))
}
}
}
func validateDependencies(manifest *Manifest, result *ValidationResult) {
serviceNames := make(map[string]bool)
for _, svc := range manifest.Services {
serviceNames[svc.Name] = true
}
for _, svc := range manifest.Services {
for _, dep := range svc.DependsOn {
depName := dep.Name
// Handle healthmon sidecars
depName = strings.TrimSuffix(depName, "_readycheck")
// Handle component.service format (e.g., "merger.merger-builder")
if strings.Contains(depName, ".") {
parts := strings.SplitN(depName, ".", 2)
if len(parts) == 2 {
depName = parts[1] // Use just the service name
}
}
if !serviceNames[depName] && !serviceNames[dep.Name] {
result.AddError("service '%s' depends on unknown service '%s'", svc.Name, dep.Name)
}
}
// Check NodeRefs
for _, ref := range svc.NodeRefs {
if !serviceNames[ref.Service] {
result.AddError("service '%s' references unknown service '%s'", svc.Name, ref.Service)
}
}
}
}
func validateHostPaths(manifest *Manifest, result *ValidationResult) {
for _, svc := range manifest.Services {
if svc.HostPath != "" {
if _, err := os.Stat(svc.HostPath); os.IsNotExist(err) {
result.AddError("service '%s' host_path does not exist: %s", svc.Name, svc.HostPath)
}
}
}
}