-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathcheck.go
More file actions
226 lines (205 loc) · 8.21 KB
/
Copy pathcheck.go
File metadata and controls
226 lines (205 loc) · 8.21 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
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
package sawchain
import (
"context"
"errors"
"github.com/onsi/gomega"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"github.com/guidewire-oss/sawchain/internal/chainsaw"
"github.com/guidewire-oss/sawchain/internal/options"
"github.com/guidewire-oss/sawchain/internal/util"
)
// formatMatchError renders a check error: if it is a *chainsaw.MatchError, it is rendered at
// the given verbosity while remaining unwrappable to *chainsaw.MatchError via errors.As;
// otherwise it is returned unchanged.
func formatMatchError(err error, verbosity options.Verbosity, template string, bindings chainsaw.Bindings) error {
var me *chainsaw.MatchError
if errors.As(err, &me) {
return me.FormatError(verbosity, template, bindings)
}
return err
}
// Check searches the cluster for resources matching YAML expectations defined in a template and optionally
// saves found matches to objects for type-safe access. If no match is found, a detailed error will be
// returned.
//
// # Arguments
//
// The following arguments may be provided in any order after the context:
//
// - Template (string): Required. File path or content of a static manifest or Chainsaw template containing
// type metadata and expectations of resources to check. If provided with an object, must contain exactly
// one resource expectation document matching the type of the object. If provided with a slice of objects,
// must contain resource expectation documents exactly matching the count, order, and types of the objects.
//
// - Bindings (map[string]any): Bindings to be applied to the Chainsaw template (if provided) in addition
// to (or overriding) Sawchain's global bindings. If multiple maps are provided, they will be merged in
// natural order.
//
// - Object (client.Object): Typed or unstructured object to populate with the state of the first match (if
// found) for the expected resource defined in the template. Only valid with a single-document template.
//
// - Objects ([]client.Object): Slice of typed or unstructured objects to populate with the states of the
// first matches (if found) for each expected resource defined in the template.
//
// # Notes
//
// - Invalid input will result in immediate test failure.
//
// - When dealing with typed objects, the client scheme will be used for internal conversions.
//
// - Templates will be sanitized before use, including de-indenting (removing any common leading
// whitespace prefix from non-empty lines) and pruning empty documents.
//
// - A "check" for one resource is equivalent to a Chainsaw assert resource operation without polling,
// including full support for Chainsaw JMESPath expressions.
//
// - Because Chainsaw performs partial/subset matching on resource fields (expected fields must exist,
// extras are allowed), template expectations only have to include fields of interest, not necessarily
// complete resource definitions.
//
// - When no match is found, the returned error unwraps to a *MatchError via errors.As for
// programmatic inspection, and its detail level follows the Sawchain instance's configured
// Verbosity.
//
// - Assert the returned error with Succeed (e.g. Expect(sc.Check(...)).To(Succeed())) for the
// clearest failure output; other error matchers fall back to Gomega's struct formatting,
// which is noisier.
//
// - Use CheckFunc if you need to create a Check function for polling.
//
// # Examples
//
// Check with a file:
//
// err := sc.Check(ctx, "path/to/expectation.yaml")
//
// Check for a ConfigMap with specific name, namespace, and data:
//
// err := sc.Check(ctx, `
// apiVersion: v1
// kind: ConfigMap
// metadata:
// name: test-cm
// namespace: ($namespace)
// data:
// key: value
// `, map[string]any{"namespace": "default"})
//
// Check for a ConfigMap with specific data and save the first match to an object:
//
// configMap := &corev1.ConfigMap{}
// err := sc.Check(ctx, configMap, `
// apiVersion: v1
// kind: ConfigMap
// data:
// foo: bar
// (length(bar) >= `3`): true
// `)
//
// Check for multiple resources with labels and save the first matches to objects:
//
// configMap := &corev1.ConfigMap{}
// secret := &corev1.Secret{}
// err := sc.Check(ctx, []client.Object{configMap, secret}, `
// apiVersion: v1
// kind: ConfigMap
// metadata:
// namespace: ($namespace)
// labels:
// foo: bar
// ---
// apiVersion: v1
// kind: Secret
// metadata:
// namespace: ($namespace)
// labels:
// bar: baz
// `, map[string]any{"namespace": "default"})
//
// For more Chainsaw examples, see https://github.com/guidewire-oss/sawchain/blob/main/docs/chainsaw-cheatsheet.md.
func (s *Sawchain) Check(ctx context.Context, args ...any) error {
s.t.Helper()
// Parse options
opts, err := options.ParseAndApplyDefaults(&s.opts, false, false, true, true, true, args...)
s.g.Expect(err).NotTo(gomega.HaveOccurred(), errInvalidArgs)
s.g.Expect(opts).NotTo(gomega.BeNil(), errNilOpts)
// Check required options
s.g.Expect(options.RequireTemplate(opts)).To(gomega.Succeed(), errInvalidArgs)
// Split documents
documents, err := util.SplitYAML(opts.Template)
s.g.Expect(err).NotTo(gomega.HaveOccurred(), errFailedSplitYAML)
// Validate objects length
if opts.Object != nil {
s.g.Expect(documents).To(gomega.HaveLen(1), errObjectInsufficient)
} else if opts.Objects != nil {
s.g.Expect(opts.Objects).To(gomega.HaveLen(len(documents)), errObjectsWrongLength)
}
// Execute checks
bindings, err := chainsaw.BindingsFromMap(opts.Bindings)
s.g.Expect(err).NotTo(gomega.HaveOccurred(), errInvalidBindings)
matches := make([]unstructured.Unstructured, len(documents))
for i, document := range documents {
match, err := chainsaw.Check(s.c, ctx, document, bindings)
if err != nil {
return formatMatchError(err, s.opts.Verbosity, document, bindings)
}
matches[i] = match
}
// Save matches
if opts.Object != nil {
s.g.Expect(util.CopyUnstructuredToObject(s.c, matches[0], opts.Object)).To(gomega.Succeed(), errFailedSave)
} else if opts.Objects != nil {
for i, match := range matches {
s.g.Expect(util.CopyUnstructuredToObject(s.c, match, opts.Objects[i])).To(gomega.Succeed(), errFailedSave)
}
}
return nil
}
// CheckFunc returns a function that searches the cluster for resources matching YAML expectations defined
// in a template and optionally saves found matches to objects for type-safe access.
//
// The returned function performs the same operations as Check, but is particularly useful for
// polling scenarios where resources might not be immediately available.
//
// For details on arguments, examples, and behavior, see the documentation for Check.
func (s *Sawchain) CheckFunc(ctx context.Context, args ...any) func() error {
s.t.Helper()
// Parse options
opts, err := options.ParseAndApplyDefaults(&s.opts, false, false, true, true, true, args...)
s.g.Expect(err).NotTo(gomega.HaveOccurred(), errInvalidArgs)
s.g.Expect(opts).NotTo(gomega.BeNil(), errNilOpts)
// Check required options
s.g.Expect(options.RequireTemplate(opts)).To(gomega.Succeed(), errInvalidArgs)
// Split documents
documents, err := util.SplitYAML(opts.Template)
s.g.Expect(err).NotTo(gomega.HaveOccurred(), errFailedSplitYAML)
// Validate objects length
if opts.Object != nil {
s.g.Expect(documents).To(gomega.HaveLen(1), errObjectInsufficient)
} else if opts.Objects != nil {
s.g.Expect(opts.Objects).To(gomega.HaveLen(len(documents)), errObjectsWrongLength)
}
return func() error {
s.t.Helper()
// Execute checks
bindings, err := chainsaw.BindingsFromMap(opts.Bindings)
s.g.Expect(err).NotTo(gomega.HaveOccurred(), errInvalidBindings)
matches := make([]unstructured.Unstructured, len(documents))
for i, document := range documents {
match, err := chainsaw.Check(s.c, ctx, document, bindings)
if err != nil {
return formatMatchError(err, s.opts.Verbosity, document, bindings)
}
matches[i] = match
}
// Save matches
if opts.Object != nil {
s.g.Expect(util.CopyUnstructuredToObject(s.c, matches[0], opts.Object)).To(gomega.Succeed(), errFailedSave)
} else if opts.Objects != nil {
for i, match := range matches {
s.g.Expect(util.CopyUnstructuredToObject(s.c, match, opts.Objects[i])).To(gomega.Succeed(), errFailedSave)
}
}
return nil
}
}