...

Source file src/sigs.k8s.io/gateway-api/conformance/utils/kubernetes/apply.go

Documentation: sigs.k8s.io/gateway-api/conformance/utils/kubernetes

     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  }
   363  

View as plain text