1
16
17 package kubernetes
18
19 import (
20 "bytes"
21 "context"
22 "embed"
23 "errors"
24 "fmt"
25 "io"
26 "net/http"
27 "strings"
28 "testing"
29
30 "github.com/stretchr/testify/require"
31 apierrors "k8s.io/apimachinery/pkg/api/errors"
32 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
33 "k8s.io/apimachinery/pkg/runtime"
34 "k8s.io/apimachinery/pkg/types"
35 "k8s.io/apimachinery/pkg/util/yaml"
36 "sigs.k8s.io/controller-runtime/pkg/client"
37
38 "sigs.k8s.io/gateway-api/apis/v1beta1"
39 "sigs.k8s.io/gateway-api/conformance/utils/config"
40 )
41
42
43
44 type Applier struct {
45 NamespaceLabels map[string]string
46 NamespaceAnnotations map[string]string
47
48
49 GatewayClass string
50
51
52 ControllerName string
53
54
55 FS embed.FS
56
57
58
59 UsableNetworkAddresses []v1beta1.GatewayAddress
60
61
62
63 UnusableNetworkAddresses []v1beta1.GatewayAddress
64 }
65
66
67 func (a Applier) prepareGateway(t *testing.T, uObj *unstructured.Unstructured) {
68 ns := uObj.GetNamespace()
69 name := uObj.GetName()
70
71 err := unstructured.SetNestedField(uObj.Object, a.GatewayClass, "spec", "gatewayClassName")
72 require.NoErrorf(t, err, "error setting `spec.gatewayClassName` on Gateway %s/%s", ns, name)
73
74 rawSpec, hasSpec, err := unstructured.NestedFieldCopy(uObj.Object, "spec")
75 require.NoError(t, err, "error retrieving spec.addresses to verify if any static addresses were present on Gateway resource %s/%s", ns, name)
76 require.True(t, hasSpec)
77
78 rawSpecMap, ok := rawSpec.(map[string]interface{})
79 require.True(t, ok, "expected gw spec received %T", rawSpec)
80
81 gwspec := &v1beta1.GatewaySpec{}
82 require.NoError(t, runtime.DefaultUnstructuredConverter.FromUnstructured(rawSpecMap, gwspec))
83
84
85
86 if len(gwspec.Addresses) > 0 {
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105 var overlayUsable, overlayUnusable bool
106 var specialAddrs []v1beta1.GatewayAddress
107 for _, addr := range gwspec.Addresses {
108 switch addr.Value {
109 case "PLACEHOLDER_USABLE_ADDRS":
110 overlayUsable = true
111 case "PLACEHOLDER_UNUSABLE_ADDRS":
112 overlayUnusable = true
113 }
114
115 if addr.Type != nil && *addr.Type == "test/fake-invalid-type" {
116 specialAddrs = append(specialAddrs, addr)
117 }
118 }
119
120 var primOverlayAddrs []interface{}
121 if len(specialAddrs) > 0 {
122 t.Logf("the test provides %d special addresses that will be kept", len(specialAddrs))
123 primOverlayAddrs = append(primOverlayAddrs, convertGatewayAddrsToPrimitives(specialAddrs)...)
124 }
125 if overlayUnusable {
126 t.Logf("address pool of %d unusable addresses will be overlaid", len(a.UnusableNetworkAddresses))
127 primOverlayAddrs = append(primOverlayAddrs, convertGatewayAddrsToPrimitives(a.UnusableNetworkAddresses)...)
128 }
129 if overlayUsable {
130 t.Logf("address pool of %d usable addresses will be overlaid", len(a.UsableNetworkAddresses))
131 primOverlayAddrs = append(primOverlayAddrs, convertGatewayAddrsToPrimitives(a.UsableNetworkAddresses)...)
132 }
133
134 err = unstructured.SetNestedSlice(uObj.Object, primOverlayAddrs, "spec", "addresses")
135 require.NoError(t, err, "could not overlay static addresses on Gateway %s/%s", ns, name)
136 }
137 }
138
139
140 func (a Applier) prepareGatewayClass(t *testing.T, uObj *unstructured.Unstructured) {
141 err := unstructured.SetNestedField(uObj.Object, a.ControllerName, "spec", "controllerName")
142 require.NoErrorf(t, err, "error setting `spec.controllerName` on %s GatewayClass resource", uObj.GetName())
143 }
144
145
146 func (a Applier) prepareNamespace(t *testing.T, uObj *unstructured.Unstructured) {
147 labels, _, err := unstructured.NestedStringMap(uObj.Object, "metadata", "labels")
148 require.NoErrorf(t, err, "error getting labels on Namespace %s", uObj.GetName())
149
150 for k, v := range a.NamespaceLabels {
151 if labels == nil {
152 labels = map[string]string{}
153 }
154
155 labels[k] = v
156 }
157
158
159 if labels != nil {
160 err = unstructured.SetNestedStringMap(uObj.Object, labels, "metadata", "labels")
161 }
162 require.NoErrorf(t, err, "error setting labels on Namespace %s", uObj.GetName())
163
164 annotations, _, err := unstructured.NestedStringMap(uObj.Object, "metadata", "annotations")
165 require.NoErrorf(t, err, "error getting annotations on Namespace %s", uObj.GetName())
166
167 for k, v := range a.NamespaceAnnotations {
168 if annotations == nil {
169 annotations = map[string]string{}
170 }
171
172 annotations[k] = v
173 }
174
175
176 if annotations != nil {
177 err = unstructured.SetNestedStringMap(uObj.Object, annotations, "metadata", "annotations")
178 }
179 require.NoErrorf(t, err, "error setting annotations on Namespace %s", uObj.GetName())
180 }
181
182
183
184 func (a Applier) prepareResources(t *testing.T, decoder *yaml.YAMLOrJSONDecoder) ([]unstructured.Unstructured, error) {
185 var resources []unstructured.Unstructured
186
187 for {
188 uObj := unstructured.Unstructured{}
189 if err := decoder.Decode(&uObj); err != nil {
190 if errors.Is(err, io.EOF) {
191 break
192 }
193 return nil, err
194 }
195 if len(uObj.Object) == 0 {
196 continue
197 }
198
199 if uObj.GetKind() == "GatewayClass" {
200 a.prepareGatewayClass(t, &uObj)
201 }
202 if uObj.GetKind() == "Gateway" {
203 a.prepareGateway(t, &uObj)
204 }
205
206 if uObj.GetKind() == "Namespace" && uObj.GetObjectKind().GroupVersionKind().Group == "" {
207 a.prepareNamespace(t, &uObj)
208 }
209
210 resources = append(resources, uObj)
211 }
212
213 return resources, nil
214 }
215
216 func (a Applier) MustApplyObjectsWithCleanup(t *testing.T, c client.Client, timeoutConfig config.TimeoutConfig, resources []client.Object, cleanup bool) {
217 for _, resource := range resources {
218 resource := resource
219
220 ctx, cancel := context.WithTimeout(context.Background(), timeoutConfig.CreateTimeout)
221 defer cancel()
222
223 t.Logf("Creating %s %s", resource.GetName(), resource.GetObjectKind().GroupVersionKind().Kind)
224
225 err := c.Create(ctx, resource)
226 if err != nil {
227 if !apierrors.IsAlreadyExists(err) {
228 require.NoError(t, err, "error creating resource")
229 }
230 }
231
232 if cleanup {
233 t.Cleanup(func() {
234 ctx, cancel = context.WithTimeout(context.Background(), timeoutConfig.DeleteTimeout)
235 defer cancel()
236 t.Logf("Deleting %s %s", resource.GetName(), resource.GetObjectKind().GroupVersionKind().Kind)
237 err = c.Delete(ctx, resource)
238 require.NoErrorf(t, err, "error deleting resource")
239 })
240 }
241 }
242 }
243
244
245
246
247 func (a Applier) MustApplyWithCleanup(t *testing.T, c client.Client, timeoutConfig config.TimeoutConfig, location string, cleanup bool) {
248 data, err := getContentsFromPathOrURL(a.FS, location, timeoutConfig)
249 require.NoError(t, err)
250
251 decoder := yaml.NewYAMLOrJSONDecoder(data, 4096)
252
253 resources, err := a.prepareResources(t, decoder)
254 if err != nil {
255 t.Logf("manifest: %s", data.String())
256 require.NoErrorf(t, err, "error parsing manifest")
257 }
258
259 for i := range resources {
260 uObj := &resources[i]
261
262 ctx, cancel := context.WithTimeout(context.Background(), timeoutConfig.CreateTimeout)
263 defer cancel()
264
265 namespacedName := types.NamespacedName{Namespace: uObj.GetNamespace(), Name: uObj.GetName()}
266 fetchedObj := uObj.DeepCopy()
267 err := c.Get(ctx, namespacedName, fetchedObj)
268 if err != nil {
269 if !apierrors.IsNotFound(err) {
270 require.NoErrorf(t, err, "error getting resource")
271 }
272 t.Logf("Creating %s %s", uObj.GetName(), uObj.GetKind())
273 err = c.Create(ctx, uObj)
274 require.NoErrorf(t, err, "error creating resource")
275
276 if cleanup {
277 t.Cleanup(func() {
278 ctx, cancel = context.WithTimeout(context.Background(), timeoutConfig.DeleteTimeout)
279 defer cancel()
280 t.Logf("Deleting %s %s", uObj.GetName(), uObj.GetKind())
281 err = c.Delete(ctx, uObj)
282 if !apierrors.IsNotFound(err) {
283 require.NoErrorf(t, err, "error deleting resource")
284 }
285 })
286 }
287 continue
288 }
289
290 uObj.SetResourceVersion(fetchedObj.GetResourceVersion())
291 t.Logf("Updating %s %s", uObj.GetName(), uObj.GetKind())
292 err = c.Update(ctx, uObj)
293
294 if cleanup {
295 t.Cleanup(func() {
296 ctx, cancel = context.WithTimeout(context.Background(), timeoutConfig.DeleteTimeout)
297 defer cancel()
298 t.Logf("Deleting %s %s", uObj.GetName(), uObj.GetKind())
299 err = c.Delete(ctx, uObj)
300 if !apierrors.IsNotFound(err) {
301 require.NoErrorf(t, err, "error deleting resource")
302 }
303 })
304 }
305 require.NoErrorf(t, err, "error updating resource")
306 }
307 }
308
309
310
311 func getContentsFromPathOrURL(fs embed.FS, location string, timeoutConfig config.TimeoutConfig) (*bytes.Buffer, error) {
312 if strings.HasPrefix(location, "http://") {
313 return nil, fmt.Errorf("data can't be retrieved from %s: http is not supported, use https", location)
314 } else if strings.HasPrefix(location, "https://") {
315 ctx, cancel := context.WithTimeout(context.Background(), timeoutConfig.ManifestFetchTimeout)
316 defer cancel()
317
318 req, err := http.NewRequestWithContext(ctx, http.MethodGet, location, nil)
319 if err != nil {
320 return nil, err
321 }
322
323 resp, err := http.DefaultClient.Do(req)
324 if err != nil {
325 return nil, err
326 }
327 defer resp.Body.Close()
328
329 manifests := new(bytes.Buffer)
330 count, err := manifests.ReadFrom(resp.Body)
331 if err != nil {
332 return nil, err
333 }
334
335 if resp.ContentLength != -1 && count != resp.ContentLength {
336 return nil, fmt.Errorf("received %d bytes from %s, expected %d", count, location, resp.ContentLength)
337 }
338 return manifests, nil
339 }
340 b, err := fs.ReadFile(location)
341 if err != nil {
342 return nil, err
343 }
344 return bytes.NewBuffer(b), nil
345 }
346
347
348
349
350 func convertGatewayAddrsToPrimitives(gwaddrs []v1beta1.GatewayAddress) (raw []interface{}) {
351 for _, addr := range gwaddrs {
352 addrType := string(v1beta1.IPAddressType)
353 if addr.Type != nil {
354 addrType = string(*addr.Type)
355 }
356 raw = append(raw, map[string]interface{}{
357 "type": addrType,
358 "value": addr.Value,
359 })
360 }
361 return
362 }
363
View as plain text