-
-
Notifications
You must be signed in to change notification settings - Fork 174
Expand file tree
/
Copy pathvalidate.go
More file actions
156 lines (135 loc) · 5.58 KB
/
Copy pathvalidate.go
File metadata and controls
156 lines (135 loc) · 5.58 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
package kubernetes
import (
"context"
"errors"
"fmt"
"strings"
"time"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
apivalidation "k8s.io/apimachinery/pkg/util/validation"
kustomizetypes "sigs.k8s.io/kustomize/api/types"
errUtils "github.com/cloudposse/atmos/errors"
"github.com/cloudposse/atmos/pkg/perf"
"github.com/cloudposse/atmos/pkg/ui"
)
// validateOptions controls how rendered manifests are validated.
type validateOptions struct {
// Server enables a server-side dry-run apply against the live cluster in
// addition to the default offline structural checks.
Server bool
}
// resolveValidateOptions extracts validate options from the CLI flag map.
func resolveValidateOptions(flags map[string]any) validateOptions {
options := validateOptions{}
if value, ok := flags["server"].(bool); ok && value {
options.Server = true
}
return options
}
// runValidate validates the rendered objects. Offline structural checks always
// run; the cluster dry-run only runs when --server is set. All failures are
// collected and reported together rather than stopping at the first.
func runValidate(objects []*unstructured.Unstructured, options validateOptions) ([]objectResult, error) {
defer perf.Track(nil, "kubernetes.runValidate")()
if err := validateObjectsStructural(objects); err != nil {
return nil, err
}
if options.Server {
return runServerValidate(objects)
}
ui.Successf("validated %d Kubernetes object(s)", len(objects))
return objectsToResults("valid", objects), nil
}
// validateObjectsStructural runs offline structural validation over every object
// and returns a single aggregate error describing all failures, or nil if every
// object is valid. It is reused by the apply/deploy auto-gate.
func validateObjectsStructural(objects []*unstructured.Unstructured) error {
var errs []error
for i, obj := range objects {
errs = append(errs, structuralErrorsForObject(i, obj)...)
}
if len(errs) > 0 {
return fmt.Errorf("%w: %w", errUtils.ErrKubernetesValidationFailed, errors.Join(errs...))
}
return nil
}
// structuralErrorsForObject returns the offline validation errors for a single
// object: a present, DNS-1123-conformant metadata.name and a resolvable GVK.
// (apiVersion/kind presence is already guaranteed upstream by decodeObjects.)
// Kustomize's own config objects (see isKustomizeConfigObject) are exempt from
// the metadata.name presence requirement only — a name, if given, is still
// validated, and the GVK check remains unconditional.
func structuralErrorsForObject(index int, obj *unstructured.Unstructured) []error {
var errs []error
ref := objectRef(index, obj)
name := obj.GetName()
if name == "" {
if !isKustomizeConfigObject(obj) {
errs = append(errs, fmt.Errorf("%s: %w", ref, errUtils.ErrKubernetesMissingMetadataName))
}
} else if msgs := apivalidation.IsDNS1123Subdomain(name); len(msgs) > 0 {
errs = append(errs, fmt.Errorf("%s: %w: %s", ref, errUtils.ErrKubernetesManifestInvalidName, strings.Join(msgs, "; ")))
}
if obj.GroupVersionKind().Empty() {
errs = append(errs, fmt.Errorf("%s: %w", ref, errUtils.ErrKubernetesMissingGVK))
}
return errs
}
// isKustomizeConfigObject reports whether obj is one of Kustomize's own reserved
// config-object kinds (Kustomization or Component). These are matched against
// Kustomize's own exported kind/version constants (sigs.k8s.io/kustomize/api/types,
// already vendored by this repo's native kustomize provider) rather than a guessed
// string, mirroring exactly what Kustomize's own EnforceFields validation checks.
// Such objects are never submitted to the Kubernetes API — they are local build
// input consumed by the kustomize tool itself — and Kustomize does not require
// (or, historically, even permit) a metadata.name on them.
func isKustomizeConfigObject(obj *unstructured.Unstructured) bool {
apiVersion, kind := obj.GetAPIVersion(), obj.GetKind()
switch {
case apiVersion == kustomizetypes.KustomizationVersion && kind == kustomizetypes.KustomizationKind:
return true
case apiVersion == kustomizetypes.ComponentVersion && kind == kustomizetypes.ComponentKind:
return true
default:
return false
}
}
// resolveComponentValidateEnabled reports whether structural validation is
// enabled for this component. Component-level `validate: false` opts out of all
// automatic (apply/deploy auto-gate) and explicit (`atmos kubernetes validate`)
// structural checks; it does not affect --server, which validates against the
// live cluster's own API rather than Atmos's offline opinion.
func resolveComponentValidateEnabled(componentSection map[string]any) bool {
if v, ok := componentSection["validate"].(bool); ok {
return v
}
return true
}
// objectRef builds a human-readable identifier for an object in validation
// messages, falling back to a positional reference when the name is missing.
func objectRef(index int, obj *unstructured.Unstructured) string {
kind := obj.GetKind()
if kind == "" {
kind = "object"
}
if name := obj.GetName(); name != "" {
return fmt.Sprintf("%s/%s", kind, name)
}
return fmt.Sprintf("%s[%d]", kind, index)
}
// runServerValidate validates the objects against the live cluster using a
// server-side dry-run apply.
func runServerValidate(objects []*unstructured.Unstructured) ([]objectResult, error) {
client, err := newKubernetesSDKClient()
if err != nil {
return nil, err
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
defer cancel()
results, err := client.Validate(ctx, objects)
if err != nil {
return results, fmt.Errorf("%w: %w", errUtils.ErrKubernetesValidationFailed, err)
}
printResults(results)
return results, nil
}