sigs.k8s.io/gateway-api@v1.0.0/conformance/utils/kubernetes/apply.go (about) 1 /* 2 Copyright 2022 The Kubernetes Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 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 // Applier prepares manifests depending on the available options and applies 43 // them to the Kubernetes cluster. 44 type Applier struct { 45 NamespaceLabels map[string]string 46 NamespaceAnnotations map[string]string 47 48 // GatewayClass will be used as the spec.gatewayClassName when applying Gateway resources 49 GatewayClass string 50 51 // ControllerName will be used as the spec.controllerName when applying GatewayClass resources 52 ControllerName string 53 54 // FS is the filesystem to use when reading manifests. 55 FS embed.FS 56 57 // UsableNetworkAddresses is a list of addresses that are expected to be 58 // supported AND usable for Gateways in the underlying implementation. 59 UsableNetworkAddresses []v1beta1.GatewayAddress 60 61 // UnusableNetworkAddresses is a list of addresses that are expected to be 62 // supported, but not usable for Gateways in the underlying implementation. 63 UnusableNetworkAddresses []v1beta1.GatewayAddress 64 } 65 66 // prepareGateway adjusts the gatewayClassName. 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 // for tests which have placeholders for static gateway addresses we will 85 // inject real addresses from the address pools the caller provided. 86 if len(gwspec.Addresses) > 0 { 87 // this is a hack because we don't have any other great way to inject custom 88 // values into the test YAML at the time of writing: Gateways that include 89 // addresses with the following values: 90 // 91 // * PLACEHOLDER_USABLE_ADDRS 92 // * PLACEHOLDER_UNUSABLE_ADDRS 93 // 94 // indicate that they expect the caller of the test suite to have provided 95 // relevant addresses (usable, or unusable ones) in the test suite, and those 96 // addresses will be injected into the Gateway and the placeholders removed. 97 // 98 // A special "test/fake-invalid-type" can be provided as well in the test to 99 // explicitly trigger a failure to support a type. If an implementation ever 100 // comes along actually trying to support that type, I'm going to be very 101 // cranky. 102 // 103 // Note: I would really love to find a better way to do this kind of 104 // thing in the future. 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 // prepareGatewayClass adjust the spec.controllerName on the resource 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 // prepareNamespace adjusts the Namespace labels. 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 // SetNestedStringMap converts nil to an empty map 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 // SetNestedStringMap converts nil to an empty map 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 // prepareResources uses the options from an Applier to tweak resources given by 183 // a set of manifests. 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 // MustApplyWithCleanup creates or updates Kubernetes resources defined with the 245 // provided YAML file and registers a cleanup function for resources it created. 246 // Note that this does not remove resources that already existed in the cluster. 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 // getContentsFromPathOrURL takes a string that can either be a local file 310 // path or an https:// URL to YAML manifests and provides the contents. 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 // convertGatewayAddrsToPrimitives converts a slice of Gateway addresses 348 // to a slice of primitive types and then returns them as a []interface{} so that 349 // they can be applied back to an unstructured Gateway. 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 }