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 }