sigs.k8s.io/cluster-api-provider-aws@v1.5.5/test/helpers/envtest.go (about)

     1  /*
     2  Copyright 2020 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 helpers
    18  
    19  import (
    20  	"context"
    21  	"fmt"
    22  	"go/build"
    23  	"net"
    24  	"os"
    25  	"path"
    26  	"path/filepath"
    27  	"regexp"
    28  	goruntime "runtime"
    29  	"strconv"
    30  	"strings"
    31  	"time"
    32  
    33  	"github.com/onsi/ginkgo"
    34  	"github.com/pkg/errors"
    35  	admissionv1 "k8s.io/api/admissionregistration/v1"
    36  	corev1 "k8s.io/api/core/v1"
    37  	apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
    38  	apierrors "k8s.io/apimachinery/pkg/api/errors"
    39  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    40  	kerrors "k8s.io/apimachinery/pkg/util/errors"
    41  	utilruntime "k8s.io/apimachinery/pkg/util/runtime"
    42  	"k8s.io/client-go/kubernetes/scheme"
    43  	"k8s.io/client-go/rest"
    44  	"k8s.io/klog/v2"
    45  	"k8s.io/klog/v2/klogr"
    46  	ctrl "sigs.k8s.io/controller-runtime"
    47  	"sigs.k8s.io/controller-runtime/pkg/client"
    48  	"sigs.k8s.io/controller-runtime/pkg/envtest"
    49  	"sigs.k8s.io/controller-runtime/pkg/manager"
    50  
    51  	clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
    52  	"sigs.k8s.io/cluster-api/cmd/clusterctl/log"
    53  	utilyaml "sigs.k8s.io/cluster-api/util/yaml"
    54  )
    55  
    56  const (
    57  	mutatingWebhookKind          = "MutatingWebhookConfiguration"
    58  	validatingWebhookKind        = "ValidatingWebhookConfiguration"
    59  	defaultMutatingWebhookName   = "mutating-webhook-configuration"
    60  	defaultValidatingWebhookName = "validating-webhook-configuration"
    61  )
    62  
    63  var (
    64  	root                   string
    65  	clusterAPIVersionRegex = regexp.MustCompile(`^(\W)sigs.k8s.io/cluster-api v(.+)`)
    66  )
    67  
    68  func init() {
    69  	klog.InitFlags(nil)
    70  
    71  	logger := klogr.New()
    72  	// use klog as the internal logger for this envtest environment.
    73  	log.SetLogger(logger)
    74  	// additionally force all of the controllers to use the Ginkgo logger.
    75  	ctrl.SetLogger(logger)
    76  	// add logger for ginkgo
    77  	klog.SetOutput(ginkgo.GinkgoWriter)
    78  
    79  	// Calculate the scheme.
    80  	utilruntime.Must(apiextensionsv1.AddToScheme(scheme.Scheme))
    81  	utilruntime.Must(admissionv1.AddToScheme(scheme.Scheme))
    82  	utilruntime.Must(clusterv1.AddToScheme(scheme.Scheme))
    83  
    84  	// Get the root of the current file to use in CRD paths.
    85  	_, filename, _, _ := goruntime.Caller(0) //nolint
    86  	root = path.Join(path.Dir(filename), "..", "..")
    87  }
    88  
    89  type webhookConfiguration struct {
    90  	tag              string
    91  	relativeFilePath string
    92  }
    93  
    94  // TestEnvironmentConfiguration encapsulates the interim, mutable configuration of the Kubernetes local test environment.
    95  type TestEnvironmentConfiguration struct {
    96  	env                   *envtest.Environment
    97  	webhookConfigurations []webhookConfiguration
    98  }
    99  
   100  // TestEnvironment encapsulates a Kubernetes local test environment.
   101  type TestEnvironment struct {
   102  	manager.Manager
   103  	client.Client
   104  	Config *rest.Config
   105  	env    *envtest.Environment
   106  	cancel context.CancelFunc
   107  }
   108  
   109  // Cleanup deletes all the given objects.
   110  func (t *TestEnvironment) Cleanup(ctx context.Context, objs ...client.Object) error {
   111  	errs := []error{}
   112  	for _, o := range objs {
   113  		err := t.Client.Delete(ctx, o)
   114  		if apierrors.IsNotFound(err) {
   115  			continue
   116  		}
   117  		errs = append(errs, err)
   118  	}
   119  	return kerrors.NewAggregate(errs)
   120  }
   121  
   122  // CreateNamespace creates a new namespace with a generated name.
   123  func (t *TestEnvironment) CreateNamespace(ctx context.Context, generateName string) (*corev1.Namespace, error) {
   124  	ns := &corev1.Namespace{
   125  		ObjectMeta: metav1.ObjectMeta{
   126  			GenerateName: fmt.Sprintf("%s-", generateName),
   127  			Labels: map[string]string{
   128  				"testenv/original-name": generateName,
   129  			},
   130  		},
   131  	}
   132  	if err := t.Client.Create(ctx, ns); err != nil {
   133  		return nil, err
   134  	}
   135  
   136  	return ns, nil
   137  }
   138  
   139  // NewTestEnvironmentConfiguration creates a new test environment configuration for running tests.
   140  func NewTestEnvironmentConfiguration(crdDirectoryPaths []string) *TestEnvironmentConfiguration {
   141  	resolvedCrdDirectoryPaths := make([]string, len(crdDirectoryPaths))
   142  
   143  	for i, p := range crdDirectoryPaths {
   144  		resolvedCrdDirectoryPaths[i] = path.Join(root, p)
   145  	}
   146  
   147  	if capiPath := getFilePathToCAPICRDs(root); capiPath != "" {
   148  		resolvedCrdDirectoryPaths = append(resolvedCrdDirectoryPaths, capiPath)
   149  	}
   150  
   151  	return &TestEnvironmentConfiguration{
   152  		env: &envtest.Environment{
   153  			ErrorIfCRDPathMissing: true,
   154  			CRDDirectoryPaths:     resolvedCrdDirectoryPaths,
   155  		},
   156  	}
   157  }
   158  
   159  // WithWebhookConfiguration adds the CRD webhook configuration given a named tag and file path for the webhook manifest.
   160  func (t *TestEnvironmentConfiguration) WithWebhookConfiguration(tag string, relativeFilePath string) *TestEnvironmentConfiguration {
   161  	t.webhookConfigurations = append(t.webhookConfigurations, webhookConfiguration{tag: tag, relativeFilePath: relativeFilePath})
   162  	return t
   163  }
   164  
   165  // Build creates a new environment spinning up a local api-server.
   166  // This function should be called only once for each package you're running tests within,
   167  // usually the environment is initialized in a suite_test.go file within a `BeforeSuite` ginkgo block.
   168  func (t *TestEnvironmentConfiguration) Build() (*TestEnvironment, error) {
   169  	mutatingWebhooks := make([]*admissionv1.MutatingWebhookConfiguration, 0, len(t.webhookConfigurations))
   170  	validatingWebhooks := make([]*admissionv1.ValidatingWebhookConfiguration, 0, len(t.webhookConfigurations))
   171  	for _, w := range t.webhookConfigurations {
   172  		m, v, err := buildModifiedWebhook(w.tag, w.relativeFilePath)
   173  		if err != nil {
   174  			return nil, err
   175  		}
   176  		if m.Webhooks != nil {
   177  			// No mutating webhook defined.
   178  			mutatingWebhooks = append(mutatingWebhooks, &m)
   179  		}
   180  		if v.Webhooks != nil {
   181  			// No validating webhook defined.
   182  			validatingWebhooks = append(validatingWebhooks, &v)
   183  		}
   184  	}
   185  
   186  	t.env.WebhookInstallOptions = envtest.WebhookInstallOptions{
   187  		MaxTime:            20 * time.Second,
   188  		PollInterval:       time.Second,
   189  		ValidatingWebhooks: validatingWebhooks,
   190  		MutatingWebhooks:   mutatingWebhooks,
   191  	}
   192  
   193  	if _, err := t.env.Start(); err != nil {
   194  		panic(err)
   195  	}
   196  
   197  	options := manager.Options{
   198  		Scheme:             scheme.Scheme,
   199  		MetricsBindAddress: "0",
   200  		CertDir:            t.env.WebhookInstallOptions.LocalServingCertDir,
   201  		Port:               t.env.WebhookInstallOptions.LocalServingPort,
   202  	}
   203  
   204  	mgr, err := ctrl.NewManager(t.env.Config, options)
   205  
   206  	if err != nil {
   207  		klog.Fatalf("Failed to start testenv manager: %v", err)
   208  	}
   209  
   210  	return &TestEnvironment{
   211  		Manager: mgr,
   212  		Client:  mgr.GetClient(),
   213  		Config:  mgr.GetConfig(),
   214  		env:     t.env,
   215  	}, nil
   216  }
   217  
   218  func buildModifiedWebhook(tag string, relativeFilePath string) (admissionv1.MutatingWebhookConfiguration, admissionv1.ValidatingWebhookConfiguration, error) {
   219  	var mutatingWebhook admissionv1.MutatingWebhookConfiguration
   220  	var validatingWebhook admissionv1.ValidatingWebhookConfiguration
   221  	data, err := os.ReadFile(filepath.Clean(filepath.Join(root, relativeFilePath)))
   222  	if err != nil {
   223  		return mutatingWebhook, validatingWebhook, errors.Wrap(err, "failed to read webhook configuration file")
   224  	}
   225  	objs, err := utilyaml.ToUnstructured(data)
   226  	if err != nil {
   227  		return mutatingWebhook, validatingWebhook, errors.Wrap(err, "failed to parse yaml")
   228  	}
   229  	for i := range objs {
   230  		o := objs[i]
   231  		if o.GetKind() == mutatingWebhookKind {
   232  			// update the name in metadata
   233  			if o.GetName() == defaultMutatingWebhookName {
   234  				o.SetName(strings.Join([]string{defaultMutatingWebhookName, "-", tag}, ""))
   235  				if err := scheme.Scheme.Convert(&o, &mutatingWebhook, nil); err != nil {
   236  					klog.Fatalf("failed to convert MutatingWebhookConfiguration %s", o.GetName())
   237  				}
   238  			}
   239  		}
   240  		if o.GetKind() == validatingWebhookKind {
   241  			// update the name in metadata
   242  			if o.GetName() == defaultValidatingWebhookName {
   243  				o.SetName(strings.Join([]string{defaultValidatingWebhookName, "-", tag}, ""))
   244  				if err := scheme.Scheme.Convert(&o, &validatingWebhook, nil); err != nil {
   245  					klog.Fatalf("failed to convert ValidatingWebhookConfiguration %s", o.GetName())
   246  				}
   247  			}
   248  		}
   249  	}
   250  	return mutatingWebhook, validatingWebhook, nil
   251  }
   252  
   253  // StartManager starts the test controller against the local API server.
   254  func (t *TestEnvironment) StartManager(ctx context.Context) error {
   255  	ctx, cancel := context.WithCancel(ctx)
   256  	t.cancel = cancel
   257  	return t.Manager.Start(ctx)
   258  }
   259  
   260  // WaitForWebhooks will not return until the webhook port is open.
   261  func (t *TestEnvironment) WaitForWebhooks() {
   262  	port := t.env.WebhookInstallOptions.LocalServingPort
   263  	klog.V(2).Infof("Waiting for webhook port %d to be open prior to running tests", port)
   264  	timeout := 1 * time.Second
   265  	for {
   266  		time.Sleep(1 * time.Second)
   267  		conn, err := net.DialTimeout("tcp", net.JoinHostPort("127.0.0.1", strconv.Itoa(port)), timeout)
   268  		if err != nil {
   269  			klog.V(2).Infof("Webhook port is not ready, will retry in %v: %s", timeout, err)
   270  			continue
   271  		}
   272  		conn.Close()
   273  		klog.V(2).Info("Webhook port is now open. Continuing with tests...")
   274  		return
   275  	}
   276  }
   277  
   278  // Stop stops the test environment.
   279  func (t *TestEnvironment) Stop() error {
   280  	t.cancel()
   281  	return t.env.Stop()
   282  }
   283  
   284  func getFilePathToCAPICRDs(root string) string {
   285  	modBits, err := os.ReadFile(filepath.Join(root, "go.mod")) //nolint:gosec
   286  	if err != nil {
   287  		return ""
   288  	}
   289  
   290  	var clusterAPIVersion string
   291  	for _, line := range strings.Split(string(modBits), "\n") {
   292  		matches := clusterAPIVersionRegex.FindStringSubmatch(line)
   293  		if len(matches) == 3 {
   294  			clusterAPIVersion = matches[2]
   295  		}
   296  	}
   297  
   298  	if clusterAPIVersion == "" {
   299  		return ""
   300  	}
   301  
   302  	gopath := envOr("GOPATH", build.Default.GOPATH)
   303  	return filepath.Join(gopath, "pkg", "mod", "sigs.k8s.io", fmt.Sprintf("cluster-api@v%s", clusterAPIVersion), "config", "crd", "bases")
   304  }
   305  
   306  func envOr(envKey, defaultValue string) string {
   307  	if value, ok := os.LookupEnv(envKey); ok {
   308  		return value
   309  	}
   310  	return defaultValue
   311  }