From 17db2b0525abb91408bfb9f1d06c2a7238bbd761 Mon Sep 17 00:00:00 2001 From: Michael Pleshakov Date: Wed, 23 Sep 2020 14:55:57 -0700 Subject: [PATCH 1/3] Introduce Configuration type --- go.mod | 1 + internal/k8s/configuration.go | 1121 ++++++++++ internal/k8s/configuration_test.go | 2501 +++++++++++++++++++++++ internal/k8s/reference_checkers.go | 209 ++ internal/k8s/reference_checkers_test.go | 886 ++++++++ internal/k8s/validation.go | 111 + internal/k8s/validation_test.go | 424 ++++ vendor/modules.txt | 1 + 8 files changed, 5254 insertions(+) create mode 100644 internal/k8s/configuration.go create mode 100644 internal/k8s/configuration_test.go create mode 100644 internal/k8s/reference_checkers.go create mode 100644 internal/k8s/reference_checkers_test.go create mode 100644 internal/k8s/validation.go create mode 100644 internal/k8s/validation_test.go diff --git a/go.mod b/go.mod index 3c4015b871..edd19da1a6 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( github.com/evanphx/json-patch v4.5.0+incompatible // indirect github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b github.com/golang/groupcache v0.0.0-20191027212112-611e8accdfc9 // indirect + github.com/google/go-cmp v0.5.0 github.com/googleapis/gnostic v0.3.1 // indirect github.com/hashicorp/golang-lru v0.5.3 // indirect github.com/imdario/mergo v0.3.8 // indirect diff --git a/internal/k8s/configuration.go b/internal/k8s/configuration.go new file mode 100644 index 0000000000..7503bc6461 --- /dev/null +++ b/internal/k8s/configuration.go @@ -0,0 +1,1121 @@ +package k8s + +import ( + "fmt" + "reflect" + "sort" + "strings" + "sync" + + "github.com/nginxinc/kubernetes-ingress/internal/configs" + conf_v1 "github.com/nginxinc/kubernetes-ingress/pkg/apis/configuration/v1" + "github.com/nginxinc/kubernetes-ingress/pkg/apis/configuration/validation" + networking "k8s.io/api/networking/v1beta1" + "k8s.io/apimachinery/pkg/runtime" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +const ingressKind = "Ingress" +const virtualServerKind = "VirtualServer" +const virtualServerRouteKind = "VirtualServerRoute" + +// Operation defines an operation to perform for a resource. +type Operation int + +const ( + // Delete the config of the resource + Delete Operation = iota + // AddOrUpdate the config of the resource + AddOrUpdate +) + +// Resource represents a configuration resource. +// A Resource can be a top level configuration object: +// - Regular or Master Ingress +// - VirtualServer +type Resource interface { + GetObjectMeta() *metav1.ObjectMeta + GetKeyWithKind() string + AcquireHost(host string) + ReleaseHost(host string) + Wins(resource Resource) bool + AddWarning(warning string) + IsEqual(resource Resource) bool +} + +func chooseObjectMetaWinner(meta1 *metav1.ObjectMeta, meta2 *metav1.ObjectMeta) bool { + if meta1.CreationTimestamp.Equal(&meta2.CreationTimestamp) { + return meta1.UID > meta2.UID + } + + return meta1.CreationTimestamp.Before(&meta2.CreationTimestamp) +} + +// ResourceChange represents a change of the resource that needs to be reflected in the NGINX config. +type ResourceChange struct { + // Op is an operation that needs be performed on the resource. + Op Operation + // Resource is the target resource. + Resource Resource + // Error is the error associated with the resource. + Error string +} + +// ConfigurationProblem is a problem associated with a configuration object. +type ConfigurationProblem struct { + // Object is a configuration object. + Object runtime.Object + // IsError tells if the problem is an error. If it is an error, then it is expected that the status of the object + // will be updated to the state 'invalid'. Otherwise, the state will be 'warning'. + IsError bool + // Reason tells the reason. It matches the reason in the events/status of our configuration objects. + Reason string + // Messages gives the details about the problem. It matches the message in the events/status of our configuration objects. + Message string +} + +func compareConfigurationProblems(problem1 *ConfigurationProblem, problem2 *ConfigurationProblem) bool { + return problem1.IsError == problem2.IsError && + problem1.Reason == problem2.Reason && + problem1.Message == problem2.Message +} + +// FullIngress holds an Ingress resource with its minions. It implements the Resource interface. +type FullIngress struct { + // Ingress holds a regular Ingress or a master Ingress. + Ingress *networking.Ingress + // IsMaster is true when the Ingress is a master. + IsMaster bool + // Minions contains minions if the Ingress is a master. + Minions []*FullMinion + // ValidHosts marks the hosts of the Ingress as valid (true) or invalid (false). + // Regular Ingress resources can have multiple hosts. It is possible that some of the hosts are taken by other + // resources. In that case, those hosts will be marked as invalid. + ValidHosts map[string]bool + // Warnings includes all the warnings for the resource. + Warnings []string + // ChildWarnings includes the warnings of the minions. The key is the namespace/name. + ChildWarnings map[string][]string +} + +// NewRegularFullIngress creates a FullIngress from a regular Ingress resource. +func NewRegularFullIngress(ing *networking.Ingress) *FullIngress { + return &FullIngress{ + Ingress: ing, + IsMaster: false, + ValidHosts: make(map[string]bool), + ChildWarnings: make(map[string][]string), + } +} + +// NewMasterFullIngress creates a FullIngress from a master Ingress resource. +func NewMasterFullIngress(ing *networking.Ingress, minions []*FullMinion, childWarnings map[string][]string) *FullIngress { + return &FullIngress{ + Ingress: ing, + IsMaster: true, + Minions: minions, + ValidHosts: make(map[string]bool), + ChildWarnings: childWarnings, + } +} + +// GetObjectMeta returns the resource ObjectMeta. +func (fi *FullIngress) GetObjectMeta() *metav1.ObjectMeta { + return &fi.Ingress.ObjectMeta +} + +// GetKeyWithKind returns the key of the resource with its kind. For example, Ingress/my-namespace/my-name. +func (fi *FullIngress) GetKeyWithKind() string { + key := getResourceKey(&fi.Ingress.ObjectMeta) + return fmt.Sprintf("%s/%s", ingressKind, key) +} + +// AcquireHost acquires the host for this resource. +func (fi *FullIngress) AcquireHost(host string) { + fi.ValidHosts[host] = true +} + +// ReleaseHost releases the host. +func (fi *FullIngress) ReleaseHost(host string) { + fi.ValidHosts[host] = false +} + +// Wins tells if this resource wins over the specified resource. +func (fi *FullIngress) Wins(resource Resource) bool { + return chooseObjectMetaWinner(fi.GetObjectMeta(), resource.GetObjectMeta()) +} + +// AddWarning adds a warning. +func (fi *FullIngress) AddWarning(warning string) { + fi.Warnings = append(fi.Warnings, warning) +} + +// IsEqual tests if the FullIngress is equal to the resource. +func (fi *FullIngress) IsEqual(resource Resource) bool { + fullIngress, ok := resource.(*FullIngress) + if !ok { + return false + } + + if !compareObjectMetasWithAnnotations(&fi.Ingress.ObjectMeta, &fullIngress.Ingress.ObjectMeta) { + return false + } + + if !reflect.DeepEqual(fi.ValidHosts, fullIngress.ValidHosts) { + return false + } + + if fi.IsMaster != fullIngress.IsMaster { + return false + } + + if len(fi.Minions) != len(fullIngress.Minions) { + return false + } + + for i := range fi.Minions { + if !compareObjectMetasWithAnnotations(&fi.Minions[i].Ingress.ObjectMeta, &fullIngress.Minions[i].Ingress.ObjectMeta) { + return false + } + } + + return true +} + +// FullMinion holds a Minion resource. +type FullMinion struct { + // Ingress is the Ingress behind a minion. + Ingress *networking.Ingress + // ValidPaths marks the paths of the Ingress as valid (true) or invalid (false). + // Minion Ingress resources can have multiple paths. It is possible that some of the paths are taken by other + // Minions. In that case, those paths will be marked as invalid. + ValidPaths map[string]bool +} + +// NewFullMinion creates a new FullMinion. +func NewFullMinion(ing *networking.Ingress) *FullMinion { + return &FullMinion{ + Ingress: ing, + ValidPaths: make(map[string]bool), + } +} + +// FullVirtualServer holds a VirtualServer along with its VirtualServerRoutes. +type FullVirtualServer struct { + VirtualServer *conf_v1.VirtualServer + VirtualServerRoutes []*conf_v1.VirtualServerRoute + Warnings []string +} + +// NewFullVirtualServer creates a NewFullVirtualServer. +func NewFullVirtualServer(vs *conf_v1.VirtualServer, vsrs []*conf_v1.VirtualServerRoute, warnings []string) *FullVirtualServer { + return &FullVirtualServer{ + VirtualServer: vs, + VirtualServerRoutes: vsrs, + Warnings: warnings, + } +} + +// GetObjectMeta returns the resource ObjectMeta. +func (fvs *FullVirtualServer) GetObjectMeta() *metav1.ObjectMeta { + return &fvs.VirtualServer.ObjectMeta +} + +// GetKeyWithKind returns the key of the resource with its kind. For example, VirtualServer/my-namespace/my-name. +func (fvs *FullVirtualServer) GetKeyWithKind() string { + key := getResourceKey(&fvs.VirtualServer.ObjectMeta) + return fmt.Sprintf("%s/%s", virtualServerKind, key) +} + +// AcquireHost acquires the host for this resource. +func (fvs *FullVirtualServer) AcquireHost(host string) { + // we do nothing because we don't need to track which host belongs to VirtualServer, in contrast with the Ingress resource. +} + +// ReleaseHost releases the host. +func (fvs *FullVirtualServer) ReleaseHost(host string) { + // we do nothing because we don't need to track which host belongs to VirtualServer, in contrast with the Ingress resource. +} + +// Wins tells if this resource wins over the specified resource. +// It is used to determine which resource should win over a host. +func (fvs *FullVirtualServer) Wins(resource Resource) bool { + return chooseObjectMetaWinner(fvs.GetObjectMeta(), resource.GetObjectMeta()) +} + +// AddWarning adds a warning. +func (fvs *FullVirtualServer) AddWarning(warning string) { + fvs.Warnings = append(fvs.Warnings, warning) +} + +// IsEqual tests if the FullVirtualServer is equal to the resource. +func (fvs *FullVirtualServer) IsEqual(resource Resource) bool { + fullVS, ok := resource.(*FullVirtualServer) + if !ok { + return false + } + + if !compareObjectMetas(&fvs.VirtualServer.ObjectMeta, &fullVS.VirtualServer.ObjectMeta) { + return false + } + + if len(fvs.VirtualServerRoutes) != len(fullVS.VirtualServerRoutes) { + return false + } + + for i := range fvs.VirtualServerRoutes { + if !compareObjectMetas(&fvs.VirtualServerRoutes[i].ObjectMeta, &fullVS.VirtualServerRoutes[i].ObjectMeta) { + return false + } + } + + return true +} + +func compareObjectMetas(meta1 *metav1.ObjectMeta, meta2 *metav1.ObjectMeta) bool { + return meta1.Namespace == meta2.Namespace && + meta1.Name == meta2.Name && + meta1.Generation == meta2.Generation +} + +func compareObjectMetasWithAnnotations(meta1 *metav1.ObjectMeta, meta2 *metav1.ObjectMeta) bool { + return compareObjectMetas(meta1, meta2) && reflect.DeepEqual(meta1.Annotations, meta2.Annotations) +} + +// Configuration represents the configuration of the Ingress Controller - a collection of configuration objects +// (Ingresses, VirtualServers, VirtualServerRoutes) ready to be transformed into NGINX config. +// It holds the latest valid state of those objects. +// The IC needs to ensure that at any point in time the NGINX config on the filesystem reflects the state +// of the objects in the Configuration. +type Configuration struct { + hosts map[string]Resource + + // only valid resources with the matching IngressClass are stored + ingresses map[string]*networking.Ingress + virtualServers map[string]*conf_v1.VirtualServer + virtualServerRoutes map[string]*conf_v1.VirtualServerRoute + + problems map[string]ConfigurationProblem + + hasCorrectIngressClass func(interface{}) bool + + secretReferenceChecker *secretReferenceChecker + serviceReferenceChecker *serviceReferenceChecker + policyReferenceChecker *policyReferenceChecker + appPolicyReferenceChecker *appProtectResourceReferenceChecker + appLogConfReferenceChecker *appProtectResourceReferenceChecker + + isPlus bool + + lock sync.RWMutex +} + +// NewConfiguration creates a new Configuration. +func NewConfiguration(hasCorrectIngressClass func(interface{}) bool, isPlus bool) *Configuration { + return &Configuration{ + hosts: make(map[string]Resource), + ingresses: make(map[string]*networking.Ingress), + virtualServers: make(map[string]*conf_v1.VirtualServer), + virtualServerRoutes: make(map[string]*conf_v1.VirtualServerRoute), + problems: make(map[string]ConfigurationProblem), + hasCorrectIngressClass: hasCorrectIngressClass, + secretReferenceChecker: newSecretReferenceChecker(isPlus), + serviceReferenceChecker: newServiceReferenceChecker(), + policyReferenceChecker: newPolicyReferenceChecker(), + appPolicyReferenceChecker: newAppProtectResourceReferenceChecker(configs.AppProtectPolicyAnnotation), + appLogConfReferenceChecker: newAppProtectResourceReferenceChecker(configs.AppProtectLogConfAnnotation), + isPlus: isPlus, + } +} + +// AddOrUpdateIngress adds or updates the Ingress resource. +func (c *Configuration) AddOrUpdateIngress(ing *networking.Ingress) ([]ResourceChange, []ConfigurationProblem) { + c.lock.Lock() + defer c.lock.Unlock() + + key := getResourceKey(&ing.ObjectMeta) + var validationError error + + if !c.hasCorrectIngressClass(ing) { + delete(c.ingresses, key) + } else { + validationError = validateIngress(ing).ToAggregate() + if validationError != nil { + delete(c.ingresses, key) + } else { + c.ingresses[key] = ing + } + } + + changes, problems := c.rebuild() + + if validationError != nil { + // If the invalid resource has any active hosts, rebuild will create a change + // to remove the resource. + // Here we add the validationErr to that change. + keyWithKind := getResourceKeyWithKind(ingressKind, &ing.ObjectMeta) + for i := range changes { + k := changes[i].Resource.GetKeyWithKind() + + if k == keyWithKind { + changes[i].Error = validationError.Error() + return changes, problems + } + } + + // On the other hand, the invalid resource might not have any active hosts. + // Or the resource was invalid before and is still invalid (in some different way). + // In those cases, rebuild will create no change for that resource. + // To make sure the validationErr is reported to the user, we create a problem. + p := ConfigurationProblem{ + Object: ing, + IsError: true, + Reason: "Rejected", + Message: validationError.Error(), + } + problems = append(problems, p) + } + + return changes, problems +} + +// DeleteIngress deletes an Ingress resource by the key. +func (c *Configuration) DeleteIngress(key string) ([]ResourceChange, []ConfigurationProblem) { + c.lock.Lock() + defer c.lock.Unlock() + + _, exists := c.ingresses[key] + if !exists { + return nil, nil + } + + delete(c.ingresses, key) + + return c.rebuild() +} + +// AddOrUpdateVirtualServer adds or updates the VirtualServer resource. +func (c *Configuration) AddOrUpdateVirtualServer(vs *conf_v1.VirtualServer) ([]ResourceChange, []ConfigurationProblem) { + c.lock.Lock() + defer c.lock.Unlock() + + key := getResourceKey(&vs.ObjectMeta) + var validationError error + + if !c.hasCorrectIngressClass(vs) { + delete(c.virtualServers, key) + } else { + validationError = validation.ValidateVirtualServer(vs, c.isPlus) + if validationError != nil { + delete(c.virtualServers, key) + } else { + c.virtualServers[key] = vs + } + } + + changes, problems := c.rebuild() + + if validationError != nil { + // If the invalid resource has an active host, rebuild will create a change + // to remove the resource. + // Here we add the validationErr to that change. + kind := getResourceKeyWithKind(virtualServerKind, &vs.ObjectMeta) + for i := range changes { + k := changes[i].Resource.GetKeyWithKind() + + if k == kind { + changes[i].Error = validationError.Error() + return changes, problems + } + } + + // On the other hand, the invalid resource might not have any active host. + // Or the resource was invalid before and is still invalid (in some different way). + // In those cases, rebuild will create no change for that resource. + // To make sure the validationErr is reported to the user, we create a problem. + p := ConfigurationProblem{ + Object: vs, + IsError: true, + Reason: "Rejected", + Message: validationError.Error(), + } + problems = append(problems, p) + } + + return changes, problems +} + +// DeleteVirtualServer deletes a VirtualServerResource by the key. +func (c *Configuration) DeleteVirtualServer(key string) ([]ResourceChange, []ConfigurationProblem) { + c.lock.Lock() + defer c.lock.Unlock() + + _, exists := c.virtualServers[key] + if !exists { + return nil, nil + } + + delete(c.virtualServers, key) + + return c.rebuild() +} + +// AddOrUpdateVirtualServerRoute adds or updates the VirtualServerRoute. +func (c *Configuration) AddOrUpdateVirtualServerRoute(vsr *conf_v1.VirtualServerRoute) ([]ResourceChange, []ConfigurationProblem) { + c.lock.Lock() + defer c.lock.Unlock() + + key := getResourceKey(&vsr.ObjectMeta) + var validationError error + + if !c.hasCorrectIngressClass(vsr) { + delete(c.virtualServerRoutes, key) + } else { + validationError = validation.ValidateVirtualServerRoute(vsr, c.isPlus) + if validationError != nil { + delete(c.virtualServerRoutes, key) + } else { + c.virtualServerRoutes[key] = vsr + } + } + + changes, problems := c.rebuild() + + if validationError != nil { + p := ConfigurationProblem{ + Object: vsr, + IsError: true, + Reason: "Rejected", + Message: validationError.Error(), + } + problems = append(problems, p) + } + + return changes, problems +} + +// DeleteVirtualServerRoute deletes a VirtualServerRoute by the key. +func (c *Configuration) DeleteVirtualServerRoute(key string) ([]ResourceChange, []ConfigurationProblem) { + c.lock.Lock() + defer c.lock.Unlock() + + _, exists := c.virtualServerRoutes[key] + if !exists { + return nil, nil + } + + delete(c.virtualServerRoutes, key) + + return c.rebuild() +} + +// GetResources returns all configuration resources. +func (c *Configuration) GetResources() []Resource { + return c.GetResourcesWithFilter(resourceFilter{ + Ingresses: true, + VirtualServers: true, + }) +} + +type resourceFilter struct { + Ingresses bool + VirtualServers bool +} + +// GetResourcesWithFilter returns resources using the filter. +func (c *Configuration) GetResourcesWithFilter(filter resourceFilter) []Resource { + c.lock.RLock() + defer c.lock.RUnlock() + + resources := make(map[string]Resource) + + for _, r := range c.hosts { + switch r.(type) { + case *FullIngress: + if filter.Ingresses { + resources[r.GetKeyWithKind()] = r + } + case *FullVirtualServer: + if filter.VirtualServers { + resources[r.GetKeyWithKind()] = r + } + } + } + + var result []Resource + for _, key := range getSortedResourceKeys(resources) { + result = append(result, resources[key]) + } + + return result +} + +// FindResourcesForService finds resources that reference the specified service. +func (c *Configuration) FindResourcesForService(svcNamespace string, svcName string) []Resource { + return c.findResourcesForResourceReference(svcNamespace, svcName, c.serviceReferenceChecker) +} + +// FindResourcesForSecret finds resources that reference the specified secret. +func (c *Configuration) FindResourcesForSecret(secretNamespace string, secretName string) []Resource { + return c.findResourcesForResourceReference(secretNamespace, secretName, c.secretReferenceChecker) +} + +// FindResourcesForPolicy finds resources that reference the specified policy. +func (c *Configuration) FindResourcesForPolicy(policyNamespace string, policyName string) []Resource { + return c.findResourcesForResourceReference(policyNamespace, policyName, c.policyReferenceChecker) +} + +// FindResourcesForAppProtectPolicy finds resources that reference the specified AppProtect policy. +func (c *Configuration) FindResourcesForAppProtectPolicy(policyNamespace string, policyName string) []Resource { + return c.findResourcesForResourceReference(policyNamespace, policyName, c.appPolicyReferenceChecker) +} + +// FindResourcesForAppProtectLogConf finds resources that reference the specified AppProtect LogConf. +func (c *Configuration) FindResourcesForAppProtectLogConf(logConfNamespace string, logConfName string) []Resource { + return c.findResourcesForResourceReference(logConfNamespace, logConfName, c.appLogConfReferenceChecker) +} + +func (c *Configuration) findResourcesForResourceReference(namespace string, name string, checker resourceReferenceChecker) []Resource { + c.lock.RLock() + defer c.lock.RUnlock() + + var result []Resource + + for _, h := range getSortedResourceKeys(c.hosts) { + r := c.hosts[h] + + switch impl := r.(type) { + case *FullIngress: + if checker.IsReferencedByIngress(namespace, name, impl.Ingress) { + result = append(result, r) + continue + } + + for _, fm := range impl.Minions { + if checker.IsReferencedByMinion(namespace, name, fm.Ingress) { + result = append(result, r) + break + } + } + case *FullVirtualServer: + if checker.IsReferencedByVirtualServer(namespace, name, impl.VirtualServer) { + result = append(result, r) + continue + } + + for _, vsr := range impl.VirtualServerRoutes { + if checker.IsReferencedByVirtualServerRoute(namespace, name, vsr) { + result = append(result, r) + break + } + } + } + } + + return result +} + +func getResourceKey(meta *metav1.ObjectMeta) string { + return fmt.Sprintf("%s/%s", meta.Namespace, meta.Name) +} + +// rebuild rebuilds the Configuration and returns the changes to it and the new problems. +func (c *Configuration) rebuild() ([]ResourceChange, []ConfigurationProblem) { + newHosts, newResources := c.buildHostsAndResources() + + removedHosts, updatedHosts, addedHosts := detectChangesInHosts(c.hosts, newHosts) + changes := c.createResourceChanges(removedHosts, updatedHosts, addedHosts, newHosts) + + // safe to update hosts + c.hosts = newHosts + + changes = squashResourceChanges(changes) + + // Note that the change will not refer to the latest version, if the resource is being removed. + // However, referring to the latest version is necessary so that the resource latest Warnings are reported and not lost. + // So here we make sure that changes always refer to the latest version of resources. + for i := range changes { + key := changes[i].Resource.GetKeyWithKind() + if r, exists := newResources[key]; exists { + changes[i].Resource = r + } + } + + newProblems := make(map[string]ConfigurationProblem) + + c.addProblemsForResourcesWithoutActiveHost(newResources, newProblems) + c.addProblemsForOrphanMinions(newProblems) + c.addProblemsForOrphanOrIgnoredVsrs(newProblems) + + newOrUpdatedProblems := detectChangesInProblems(newProblems, c.problems) + + // safe to update problems + c.problems = newProblems + + return changes, newOrUpdatedProblems +} + +func detectChangesInProblems(newProblems map[string]ConfigurationProblem, oldProblems map[string]ConfigurationProblem) []ConfigurationProblem { + var result []ConfigurationProblem + + for _, key := range getSortedProblemKeys(newProblems) { + newP := newProblems[key] + + oldP, exists := oldProblems[key] + if !exists { + result = append(result, newP) + continue + } + + if !compareConfigurationProblems(&newP, &oldP) { + result = append(result, newP) + } + } + + return result +} + +func (c *Configuration) addProblemsForResourcesWithoutActiveHost(resources map[string]Resource, problems map[string]ConfigurationProblem) { + for _, k := range getSortedResourceKeys(resources) { + r := resources[k] + + switch impl := r.(type) { + case *FullIngress: + atLeastOneValidHost := false + for _, v := range impl.ValidHosts { + if v { + atLeastOneValidHost = true + break + } + } + if !atLeastOneValidHost { + p := ConfigurationProblem{ + Object: impl.Ingress, + IsError: false, + Reason: "Rejected", + Message: "All hosts are taken by other resources", + } + problems[r.GetKeyWithKind()] = p + } + case *FullVirtualServer: + res, exists := c.hosts[impl.VirtualServer.Spec.Host] + + if !exists { + continue + } + + if res.GetKeyWithKind() != r.GetKeyWithKind() { + p := ConfigurationProblem{ + Object: impl.VirtualServer, + IsError: false, + Reason: "Rejected", + Message: "Host is taken by another resource", + } + problems[r.GetKeyWithKind()] = p + } + } + } +} + +func (c *Configuration) addProblemsForOrphanMinions(problems map[string]ConfigurationProblem) { + for _, key := range getSortedIngressKeys(c.ingresses) { + ing := c.ingresses[key] + + if !isMinion(ing) { + continue + } + + r, exists := c.hosts[ing.Spec.Rules[0].Host] + fullIngress, ok := r.(*FullIngress) + + if !exists || !ok || !fullIngress.IsMaster { + p := ConfigurationProblem{ + Object: ing, + IsError: false, + Reason: "NoIngressMasterFound", + Message: "Ingress master is invalid or doesn't exist", + } + k := getResourceKeyWithKind(ingressKind, &ing.ObjectMeta) + problems[k] = p + } + } +} + +func (c *Configuration) addProblemsForOrphanOrIgnoredVsrs(problems map[string]ConfigurationProblem) { + for _, key := range getSortedVirtualServerRouteKeys(c.virtualServerRoutes) { + vsr := c.virtualServerRoutes[key] + + r, exists := c.hosts[vsr.Spec.Host] + fullVS, ok := r.(*FullVirtualServer) + + if !exists || !ok { + p := ConfigurationProblem{ + Object: vsr, + IsError: false, + Reason: "NoVirtualServerFound", + Message: "VirtualServer is invalid or doesn't exist", + } + k := getResourceKeyWithKind(virtualServerRouteKind, &vsr.ObjectMeta) + problems[k] = p + continue + } + + found := false + for _, v := range fullVS.VirtualServerRoutes { + if vsr.Namespace == v.Namespace && vsr.Name == v.Name { + found = true + break + } + } + + if !found { + p := ConfigurationProblem{ + Object: vsr, + IsError: false, + Reason: "Ignored", + Message: fmt.Sprintf("VirtualServer %s ignores VirtualServerRoute", getResourceKey(&fullVS.VirtualServer.ObjectMeta)), + } + k := getResourceKeyWithKind(virtualServerRouteKind, &vsr.ObjectMeta) + problems[k] = p + } + } +} + +func getResourceKeyWithKind(kind string, objectMeta *metav1.ObjectMeta) string { + return fmt.Sprintf("%s/%s/%s", kind, objectMeta.Namespace, objectMeta.Name) +} + +func (c *Configuration) createResourceChanges(removedHosts []string, updatedHosts []string, addedHosts []string, newHosts map[string]Resource) []ResourceChange { + var changes []ResourceChange + var deleteChanges []ResourceChange + + for _, h := range removedHosts { + change := ResourceChange{ + Op: Delete, + Resource: c.hosts[h], + } + deleteChanges = append(deleteChanges, change) + } + + for _, h := range updatedHosts { + if c.hosts[h].GetKeyWithKind() != newHosts[h].GetKeyWithKind() { + deleteChange := ResourceChange{ + Op: Delete, + Resource: c.hosts[h], + } + deleteChanges = append(deleteChanges, deleteChange) + } + + change := ResourceChange{ + Op: AddOrUpdate, + Resource: newHosts[h], + } + changes = append(changes, change) + } + + for _, h := range addedHosts { + change := ResourceChange{ + Op: AddOrUpdate, + Resource: newHosts[h], + } + changes = append(changes, change) + } + + // We need to ensure that delete changes come first. + // This way an addOrUpdate change, which might include a resource that uses the same host as a resource + // in a delete change, will be processed only after the config of the delete change is removed. + // That will prevent any host collisions in the NGINX config in the state between the changes. + return append(deleteChanges, changes...) +} + +func squashResourceChanges(changes []ResourceChange) []ResourceChange { + // deletes for the same resource become a single delete + // updates for the same resource become a single update + // delete and update for the same resource become a single update + + var deletes []ResourceChange + var updates []ResourceChange + + changesPerResource := make(map[string][]ResourceChange) + + for _, c := range changes { + key := c.Resource.GetKeyWithKind() + changesPerResource[key] = append(changesPerResource[key], c) + } + + // we range over the changes again to preserver the original order + for _, c := range changes { + key := c.Resource.GetKeyWithKind() + resChanges, exists := changesPerResource[key] + + if !exists { + continue + } + + // the last element will be an update (if it exists) or a delete + squashedChanged := resChanges[len(resChanges)-1] + if squashedChanged.Op == Delete { + deletes = append(deletes, squashedChanged) + } else { + updates = append(updates, squashedChanged) + } + + delete(changesPerResource, key) + } + + // We need to ensure that delete changes come first. + // This way an addOrUpdate change, which might include a resource that uses the same host as a resource + // in a delete change, will be processed only after the config of the delete change is removed. + // That will prevent any host collisions in the NGINX config in the state between the changes. + return append(deletes, updates...) +} + +func (c *Configuration) buildHostsAndResources() (newHosts map[string]Resource, newResources map[string]Resource) { + newHosts = make(map[string]Resource) + newResources = make(map[string]Resource) + + // Step 1 - Build hosts from Ingress resources + + for _, key := range getSortedIngressKeys(c.ingresses) { + ing := c.ingresses[key] + + if isMinion(ing) { + continue + } + + var resource *FullIngress + + if isMaster(ing) { + minions, childWarnings := c.buildFullMinions(ing.Spec.Rules[0].Host) + resource = NewMasterFullIngress(ing, minions, childWarnings) + } else { + resource = NewRegularFullIngress(ing) + } + + newResources[resource.GetKeyWithKind()] = resource + + for _, rule := range ing.Spec.Rules { + holder, exists := newHosts[rule.Host] + if !exists { + newHosts[rule.Host] = resource + resource.AcquireHost(rule.Host) + continue + } + + warning := fmt.Sprintf("host %s is taken by another resource", rule.Host) + + if !holder.Wins(resource) { + holder.ReleaseHost(rule.Host) + holder.AddWarning(warning) + newHosts[rule.Host] = resource + resource.AcquireHost(rule.Host) + } else { + resource.AddWarning(warning) + } + } + } + + // Step 2 - Build hosts from VirtualServer resources + + for _, key := range getSortedVirtualServerKeys(c.virtualServers) { + vs := c.virtualServers[key] + + vsrs, warnings := c.buildVirtualServerRoutes(vs) + resource := NewFullVirtualServer(vs, vsrs, warnings) + + newResources[resource.GetKeyWithKind()] = resource + + holder, exists := newHosts[vs.Spec.Host] + if !exists { + newHosts[vs.Spec.Host] = resource + resource.AcquireHost(vs.Spec.Host) + continue + } + + warning := fmt.Sprintf("host %s is taken by another resource", vs.Spec.Host) + + if !holder.Wins(resource) { + holder.ReleaseHost(vs.Spec.Host) + newHosts[vs.Spec.Host] = resource + resource.AcquireHost(vs.Spec.Host) + holder.AddWarning(warning) + } else { + resource.AddWarning(warning) + } + } + + return newHosts, newResources +} + +func (c *Configuration) buildFullMinions(masterHost string) ([]*FullMinion, map[string][]string) { + var fullMinions []*FullMinion + childWarnings := make(map[string][]string) + paths := make(map[string]*FullMinion) + + for _, minionKey := range getSortedIngressKeys(c.ingresses) { + ingress := c.ingresses[minionKey] + + if !isMinion(ingress) { + continue + } + + if masterHost != ingress.Spec.Rules[0].Host { + continue + } + + fullMinion := NewFullMinion(ingress) + + for _, p := range ingress.Spec.Rules[0].HTTP.Paths { + holder, exists := paths[p.Path] + if !exists { + paths[p.Path] = fullMinion + fullMinion.ValidPaths[p.Path] = true + continue + } + + warning := fmt.Sprintf("path %s is taken by another resource", p.Path) + + if !chooseObjectMetaWinner(&holder.Ingress.ObjectMeta, &ingress.ObjectMeta) { + paths[p.Path] = fullMinion + fullMinion.ValidPaths[p.Path] = true + + holder.ValidPaths[p.Path] = false + key := getResourceKey(&holder.Ingress.ObjectMeta) + childWarnings[key] = append(childWarnings[key], warning) + } else { + key := getResourceKey(&fullMinion.Ingress.ObjectMeta) + childWarnings[key] = append(childWarnings[key], warning) + } + } + + fullMinions = append(fullMinions, fullMinion) + } + + return fullMinions, childWarnings +} + +func (c *Configuration) buildVirtualServerRoutes(vs *conf_v1.VirtualServer) ([]*conf_v1.VirtualServerRoute, []string) { + var vsrs []*conf_v1.VirtualServerRoute + var warnings []string + + for _, r := range vs.Spec.Routes { + if r.Route == "" { + continue + } + + vsrKey := r.Route + + // if route is defined without a namespace, use the namespace of VirtualServer. + if !strings.Contains(r.Route, "/") { + vsrKey = fmt.Sprintf("%s/%s", vs.Namespace, r.Route) + } + + vsr, exists := c.virtualServerRoutes[vsrKey] + if !exists { + warning := fmt.Sprintf("VirtualServerRoute %s doesn't exist or invalid", vsrKey) + warnings = append(warnings, warning) + continue + } + + err := validation.ValidateVirtualServerRouteForVirtualServer(vsr, vs.Spec.Host, r.Path, c.isPlus) + if err != nil { + warning := fmt.Sprintf("VirtualServerRoute %s is invalid: %v", vsrKey, err) + warnings = append(warnings, warning) + continue + } + + vsrs = append(vsrs, vsr) + } + + return vsrs, warnings +} + +func getSortedIngressKeys(m map[string]*networking.Ingress) []string { + var keys []string + + for k := range m { + keys = append(keys, k) + } + + sort.Strings(keys) + + return keys +} + +func getSortedVirtualServerKeys(m map[string]*conf_v1.VirtualServer) []string { + var keys []string + + for k := range m { + keys = append(keys, k) + } + + sort.Strings(keys) + + return keys +} + +func getSortedVirtualServerRouteKeys(m map[string]*conf_v1.VirtualServerRoute) []string { + var keys []string + + for k := range m { + keys = append(keys, k) + } + + sort.Strings(keys) + + return keys +} + +func getSortedProblemKeys(m map[string]ConfigurationProblem) []string { + var keys []string + + for k := range m { + keys = append(keys, k) + } + + sort.Strings(keys) + + return keys +} + +func getSortedResourceKeys(m map[string]Resource) []string { + var keys []string + + for k := range m { + keys = append(keys, k) + } + + sort.Strings(keys) + + return keys +} + +func detectChangesInHosts(oldHosts map[string]Resource, newHosts map[string]Resource) (removedHosts []string, updatedHosts []string, addedHosts []string) { + for _, h := range getSortedResourceKeys(oldHosts) { + _, exists := newHosts[h] + if !exists { + removedHosts = append(removedHosts, h) + } + } + + for _, h := range getSortedResourceKeys(newHosts) { + _, exists := oldHosts[h] + if !exists { + addedHosts = append(addedHosts, h) + } + } + + for _, h := range getSortedResourceKeys(newHosts) { + oldR, exists := oldHosts[h] + if !exists { + continue + } + + if !oldR.IsEqual(newHosts[h]) { + updatedHosts = append(updatedHosts, h) + } + } + + return removedHosts, updatedHosts, addedHosts +} diff --git a/internal/k8s/configuration_test.go b/internal/k8s/configuration_test.go new file mode 100644 index 0000000000..ff79bd3405 --- /dev/null +++ b/internal/k8s/configuration_test.go @@ -0,0 +1,2501 @@ +package k8s + +import ( + "testing" + "time" + + "github.com/google/go-cmp/cmp" + conf_v1 "github.com/nginxinc/kubernetes-ingress/pkg/apis/configuration/v1" + networking "k8s.io/api/networking/v1beta1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func createTestConfiguration() *Configuration { + lbc := LoadBalancerController{ + ingressClass: "nginx", + useIngressClassOnly: true, + } + return NewConfiguration(lbc.HasCorrectIngressClass, false) +} + +func TestAddIngressForRegularIngress(t *testing.T) { + configuration := createTestConfiguration() + + // no problems are expected for all cases + var expectedProblems []ConfigurationProblem + + // Add a new Ingress + + ing := createTestIngress("ingress", "foo.example.com") + expectedChanges := []ResourceChange{ + { + Op: AddOrUpdate, + Resource: &FullIngress{ + Ingress: ing, + ValidHosts: map[string]bool{ + "foo.example.com": true, + }, + ChildWarnings: map[string][]string{}, + }, + }, + } + + changes, problems := configuration.AddOrUpdateIngress(ing) + if diff := cmp.Diff(expectedChanges, changes); diff != "" { + t.Errorf("AddOrUpdateIngress() returned unexpected result (-want +got):\n%s", diff) + } + if diff := cmp.Diff(expectedProblems, problems); diff != "" { + t.Errorf("AddOrUpdateIngress() returned unexpected result (-want +got):\n%s", diff) + } + + // Update the Ingress + + updatedIng := ing.DeepCopy() + updatedIng.Annotations["nginx.org/max_fails"] = "1" + + expectedChanges = []ResourceChange{ + { + Op: AddOrUpdate, + Resource: &FullIngress{ + Ingress: updatedIng, + ValidHosts: map[string]bool{ + "foo.example.com": true, + }, + ChildWarnings: map[string][]string{}, + }, + }, + } + + changes, problems = configuration.AddOrUpdateIngress(updatedIng) + if diff := cmp.Diff(expectedChanges, changes); diff != "" { + t.Errorf("AddOrUpdateIngress() returned unexpected result (-want +got):\n%s", diff) + } + if diff := cmp.Diff(expectedProblems, problems); diff != "" { + t.Errorf("AddOrUpdateIngress() returned unexpected result (-want +got):\n%s", diff) + } + + // Make the Ingress invalid + + invalidIng := updatedIng.DeepCopy() + invalidIng.Generation++ + invalidIng.Spec.Rules = []networking.IngressRule{ + { + Host: "foo.example.com", + IngressRuleValue: networking.IngressRuleValue{}, + }, + { + Host: "foo.example.com", + IngressRuleValue: networking.IngressRuleValue{}, + }, + } + + expectedChanges = []ResourceChange{ + { + Op: Delete, + Resource: &FullIngress{ + Ingress: updatedIng, + ValidHosts: map[string]bool{ + "foo.example.com": true, + }, + ChildWarnings: map[string][]string{}, + }, + Error: `spec.rules[1].host: Duplicate value: "foo.example.com"`, + }, + } + + changes, problems = configuration.AddOrUpdateIngress(invalidIng) + if diff := cmp.Diff(expectedChanges, changes); diff != "" { + t.Errorf("AddOrUpdateIngress() returned unexpected result (-want +got):\n%s", diff) + } + if diff := cmp.Diff(expectedProblems, problems); diff != "" { + t.Errorf("AddOrUpdateIngress() returned unexpected result (-want +got):\n%s", diff) + } + + // Restore the Ingress + + expectedChanges = []ResourceChange{ + { + Op: AddOrUpdate, + Resource: &FullIngress{ + Ingress: updatedIng, + ValidHosts: map[string]bool{ + "foo.example.com": true, + }, + ChildWarnings: map[string][]string{}, + }, + }, + } + + changes, problems = configuration.AddOrUpdateIngress(updatedIng) + if diff := cmp.Diff(expectedChanges, changes); diff != "" { + t.Errorf("AddOrUpdateIngress() returned unexpected result (-want +got):\n%s", diff) + } + if diff := cmp.Diff(expectedProblems, problems); diff != "" { + t.Errorf("AddOrUpdateIngress() returned unexpected result (-want +got):\n%s", diff) + } + + // Update the host of the Ingress + + updatedHostIng := updatedIng.DeepCopy() + updatedHostIng.Generation++ + updatedHostIng.Spec.Rules = []networking.IngressRule{ + { + Host: "bar.example.com", + IngressRuleValue: networking.IngressRuleValue{}, + }, + } + + expectedChanges = []ResourceChange{ + { + Op: AddOrUpdate, + Resource: &FullIngress{ + Ingress: updatedHostIng, + ValidHosts: map[string]bool{ + "bar.example.com": true, + }, + ChildWarnings: map[string][]string{}, + }, + }, + } + + changes, problems = configuration.AddOrUpdateIngress(updatedHostIng) + if diff := cmp.Diff(expectedChanges, changes); diff != "" { + t.Errorf("AddOrUpdateIngress() returned unexpected result (-want +got):\n%s", diff) + } + if diff := cmp.Diff(expectedProblems, problems); diff != "" { + t.Errorf("AddOrUpdateIngress() returned unexpected result (-want +got):\n%s", diff) + } + + // Delete Ingress + + expectedChanges = []ResourceChange{ + { + Op: Delete, + Resource: &FullIngress{ + Ingress: updatedHostIng, + ValidHosts: map[string]bool{ + "bar.example.com": true, + }, + ChildWarnings: map[string][]string{}, + }, + }, + } + + changes, problems = configuration.DeleteIngress("default/ingress") + if diff := cmp.Diff(expectedChanges, changes); diff != "" { + t.Errorf("DeleteIngress() returned unexpected result (-want +got):\n%s", diff) + } + if diff := cmp.Diff(expectedProblems, problems); diff != "" { + t.Errorf("DeleteIngress() returned unexpected result (-want +got):\n%s", diff) + } +} + +func TestAddInvalidIngress(t *testing.T) { + configuration := createTestConfiguration() + + ing := createTestIngress("ingress", "foo.example.com", "foo.example.com") + + var expectedChanges []ResourceChange + expectedProblems := []ConfigurationProblem{ + { + Object: ing, + IsError: true, + Reason: "Rejected", + Message: `spec.rules[1].host: Duplicate value: "foo.example.com"`, + }, + } + + changes, problems := configuration.AddOrUpdateIngress(ing) + if diff := cmp.Diff(expectedChanges, changes); diff != "" { + t.Errorf("AddOrUpdateIngress() returned unexpected result (-want +got):\n%s", diff) + } + if diff := cmp.Diff(expectedProblems, problems); diff != "" { + t.Errorf("AddOrUpdateIngress() returned unexpected result (-want +got):\n%s", diff) + } +} + +func TestDeleteNonExistingIngress(t *testing.T) { + configuration := createTestConfiguration() + + var expectedChanges []ResourceChange + var expectedProblems []ConfigurationProblem + + changes, problems := configuration.DeleteIngress("default/ingress") + if diff := cmp.Diff(expectedChanges, changes); diff != "" { + t.Errorf("DeleteIngress() returned unexpected result (-want +got):\n%s", diff) + } + if diff := cmp.Diff(expectedProblems, problems); diff != "" { + t.Errorf("DeleteIngress() returned unexpected result (-want +got):\n%s", diff) + } +} + +func TestAddIngressForMergeableIngresses(t *testing.T) { + configuration := createTestConfiguration() + + // Add minion-1 + + minion1 := createTestIngressMinion("ingress-minion-1", "foo.example.com", "/path-1") + var expectedChanges []ResourceChange + expectedProblems := []ConfigurationProblem{ + { + Object: minion1, + Reason: "NoIngressMasterFound", + Message: "Ingress master is invalid or doesn't exist", + }, + } + + changes, problems := configuration.AddOrUpdateIngress(minion1) + if diff := cmp.Diff(expectedChanges, changes); diff != "" { + t.Errorf("AddOrUpdateIngress() returned unexpected result (-want +got):\n%s", diff) + } + if diff := cmp.Diff(expectedProblems, problems); diff != "" { + t.Errorf("AddOrUpdateIngress() returned unexpected result (-want +got):\n%s", diff) + } + + // Add master + + master := createTestIngressMaster("ingress-master", "foo.example.com") + expectedChanges = []ResourceChange{ + { + Op: AddOrUpdate, + Resource: &FullIngress{ + Ingress: master, + ValidHosts: map[string]bool{ + "foo.example.com": true, + }, + IsMaster: true, + Minions: []*FullMinion{ + { + Ingress: minion1, + ValidPaths: map[string]bool{ + "/path-1": true, + }, + }, + }, + ChildWarnings: map[string][]string{}, + }, + }, + } + expectedProblems = nil + + changes, problems = configuration.AddOrUpdateIngress(master) + if diff := cmp.Diff(expectedChanges, changes); diff != "" { + t.Errorf("AddOrUpdateIngress() returned unexpected result (-want +got):\n%s", diff) + } + if diff := cmp.Diff(expectedProblems, problems); diff != "" { + t.Errorf("AddOrUpdateIngress() returned unexpected result (-want +got):\n%s", diff) + } + + // Add minion-2 + + minion2 := createTestIngressMinion("ingress-minion-2", "foo.example.com", "/path-2") + expectedChanges = []ResourceChange{ + { + Op: AddOrUpdate, + Resource: &FullIngress{ + Ingress: master, + ValidHosts: map[string]bool{ + "foo.example.com": true, + }, + IsMaster: true, + Minions: []*FullMinion{ + { + Ingress: minion1, + ValidPaths: map[string]bool{ + "/path-1": true, + }, + }, + { + Ingress: minion2, + ValidPaths: map[string]bool{ + "/path-2": true, + }, + }, + }, + ChildWarnings: map[string][]string{}, + }, + }, + } + expectedProblems = nil + + changes, problems = configuration.AddOrUpdateIngress(minion2) + if diff := cmp.Diff(expectedChanges, changes); diff != "" { + t.Errorf("AddOrUpdateIngress() returned unexpected result (-want +got):\n%s", diff) + } + if diff := cmp.Diff(expectedProblems, problems); diff != "" { + t.Errorf("AddOrUpdateIngress() returned unexpected result (-want +got):\n%s", diff) + } + + // Update minion-1 + + updatedMinion1 := minion1.DeepCopy() + updatedMinion1.Annotations["nginx.org/proxy-connect-timeout"] = "10s" + expectedChanges = []ResourceChange{ + { + Op: AddOrUpdate, + Resource: &FullIngress{ + Ingress: master, + ValidHosts: map[string]bool{ + "foo.example.com": true, + }, + IsMaster: true, + Minions: []*FullMinion{ + { + Ingress: updatedMinion1, + ValidPaths: map[string]bool{ + "/path-1": true, + }, + }, + { + Ingress: minion2, + ValidPaths: map[string]bool{ + "/path-2": true, + }, + }, + }, + ChildWarnings: map[string][]string{}, + }, + }, + } + expectedProblems = nil + + changes, problems = configuration.AddOrUpdateIngress(updatedMinion1) + if diff := cmp.Diff(expectedChanges, changes); diff != "" { + t.Errorf("AddOrUpdateIngress() returned unexpected result (-want +got):\n%s", diff) + } + if diff := cmp.Diff(expectedProblems, problems); diff != "" { + t.Errorf("AddOrUpdateIngress() returned unexpected result (-want +got):\n%s", diff) + } + + // Make minion-1 invalid + + invalidMinion1 := updatedMinion1.DeepCopy() + invalidMinion1.Generation++ + invalidMinion1.Spec.Rules = []networking.IngressRule{ + { + Host: "example.com", + IngressRuleValue: networking.IngressRuleValue{}, + }, + { + Host: "example.com", + IngressRuleValue: networking.IngressRuleValue{}, + }, + } + + expectedChanges = []ResourceChange{ + { + Op: AddOrUpdate, + Resource: &FullIngress{ + Ingress: master, + ValidHosts: map[string]bool{ + "foo.example.com": true, + }, + IsMaster: true, + Minions: []*FullMinion{ + { + Ingress: minion2, + ValidPaths: map[string]bool{ + "/path-2": true, + }, + }, + }, + ChildWarnings: map[string][]string{}, + }, + }, + } + expectedProblems = []ConfigurationProblem{ + { + Object: invalidMinion1, + IsError: true, + Reason: "Rejected", + Message: `[spec.rules[1].host: Duplicate value: "example.com", spec.rules: Too many: 2: must have at most 1 items]`, + }, + } + + changes, problems = configuration.AddOrUpdateIngress(invalidMinion1) + if diff := cmp.Diff(expectedChanges, changes); diff != "" { + t.Errorf("AddOrUpdateIngress() returned unexpected result (-want +got):\n%s", diff) + } + if diff := cmp.Diff(expectedProblems, problems); diff != "" { + t.Errorf("AddOrUpdateIngress() returned unexpected result (-want +got):\n%s", diff) + } + + // Restore minion-1 + + expectedChanges = []ResourceChange{ + { + Op: AddOrUpdate, + Resource: &FullIngress{ + Ingress: master, + ValidHosts: map[string]bool{ + "foo.example.com": true, + }, + IsMaster: true, + Minions: []*FullMinion{ + { + Ingress: updatedMinion1, + ValidPaths: map[string]bool{ + "/path-1": true, + }, + }, + { + Ingress: minion2, + ValidPaths: map[string]bool{ + "/path-2": true, + }, + }, + }, + ChildWarnings: map[string][]string{}, + }, + }, + } + expectedProblems = nil + + changes, problems = configuration.AddOrUpdateIngress(updatedMinion1) + if diff := cmp.Diff(expectedChanges, changes); diff != "" { + t.Errorf("AddOrUpdateIngress() returned unexpected result (-want +got):\n%s", diff) + } + if diff := cmp.Diff(expectedProblems, problems); diff != "" { + t.Errorf("AddOrUpdateIngress() returned unexpected result (-want +got):\n%s", diff) + } + + // Update host of minion-2 + + updatedMinion2 := minion2.DeepCopy() + updatedMinion2.Generation++ + updatedMinion2.Spec.Rules[0].Host = "bar.example.com" + + expectedChanges = []ResourceChange{ + { + Op: AddOrUpdate, + Resource: &FullIngress{ + Ingress: master, + ValidHosts: map[string]bool{ + "foo.example.com": true, + }, + IsMaster: true, + Minions: []*FullMinion{ + { + Ingress: updatedMinion1, + ValidPaths: map[string]bool{ + "/path-1": true, + }, + }, + }, + ChildWarnings: map[string][]string{}, + }, + }, + } + expectedProblems = []ConfigurationProblem{ + { + Object: updatedMinion2, + Reason: "NoIngressMasterFound", + Message: "Ingress master is invalid or doesn't exist", + }, + } + + changes, problems = configuration.AddOrUpdateIngress(updatedMinion2) + if diff := cmp.Diff(expectedChanges, changes); diff != "" { + t.Errorf("AddOrUpdateIngress() returned unexpected result (-want +got):\n%s", diff) + } + if diff := cmp.Diff(expectedProblems, problems); diff != "" { + t.Errorf("AddOrUpdateIngress() returned unexpected result (-want +got):\n%s", diff) + } + + // Update host of master + + updatedMaster := master.DeepCopy() + updatedMaster.Generation++ + updatedMaster.Spec.Rules[0].Host = "bar.example.com" + + expectedChanges = []ResourceChange{ + { + Op: AddOrUpdate, + Resource: &FullIngress{ + Ingress: updatedMaster, + ValidHosts: map[string]bool{ + "bar.example.com": true, + }, + IsMaster: true, + Minions: []*FullMinion{ + { + Ingress: updatedMinion2, + ValidPaths: map[string]bool{ + "/path-2": true, + }, + }, + }, + ChildWarnings: map[string][]string{}, + }, + }, + } + expectedProblems = []ConfigurationProblem{ + { + Object: updatedMinion1, + Reason: "NoIngressMasterFound", + Message: "Ingress master is invalid or doesn't exist", + }, + } + + changes, problems = configuration.AddOrUpdateIngress(updatedMaster) + if diff := cmp.Diff(expectedChanges, changes); diff != "" { + t.Errorf("AddOrUpdateIngress() returned unexpected result (-want +got):\n%s", diff) + } + if diff := cmp.Diff(expectedProblems, problems); diff != "" { + t.Errorf("AddOrUpdateIngress() returned unexpected result (-want +got):\n%s", diff) + } + + // Restore host + expectedChanges = []ResourceChange{ + { + Op: AddOrUpdate, + Resource: &FullIngress{ + Ingress: master, + ValidHosts: map[string]bool{ + "foo.example.com": true, + }, + IsMaster: true, + Minions: []*FullMinion{ + { + Ingress: updatedMinion1, + ValidPaths: map[string]bool{ + "/path-1": true, + }, + }, + }, + ChildWarnings: map[string][]string{}, + }, + }, + } + expectedProblems = []ConfigurationProblem{ + { + Object: updatedMinion2, + Reason: "NoIngressMasterFound", + Message: "Ingress master is invalid or doesn't exist", + }, + } + + changes, problems = configuration.AddOrUpdateIngress(master) + if diff := cmp.Diff(expectedChanges, changes); diff != "" { + t.Errorf("AddOrUpdateIngress() returned unexpected result (-want +got):\n%s", diff) + } + if diff := cmp.Diff(expectedProblems, problems); diff != "" { + t.Errorf("AddOrUpdateIngress() returned unexpected result (-want +got):\n%s", diff) + } + + // Restore host of minion-2 + + expectedChanges = []ResourceChange{ + { + Op: AddOrUpdate, + Resource: &FullIngress{ + Ingress: master, + ValidHosts: map[string]bool{ + "foo.example.com": true, + }, + IsMaster: true, + Minions: []*FullMinion{ + { + Ingress: updatedMinion1, + ValidPaths: map[string]bool{ + "/path-1": true, + }, + }, + { + Ingress: minion2, + ValidPaths: map[string]bool{ + "/path-2": true, + }, + }, + }, + ChildWarnings: map[string][]string{}, + }, + }, + } + expectedProblems = nil + + changes, problems = configuration.AddOrUpdateIngress(minion2) + if diff := cmp.Diff(expectedChanges, changes); diff != "" { + t.Errorf("AddOrUpdateIngress() returned unexpected result (-want +got):\n%s", diff) + } + if diff := cmp.Diff(expectedProblems, problems); diff != "" { + t.Errorf("AddOrUpdateIngress() returned unexpected result (-want +got):\n%s", diff) + } + + // Remove minion-1 + + expectedChanges = []ResourceChange{ + { + Op: AddOrUpdate, + Resource: &FullIngress{ + Ingress: master, + ValidHosts: map[string]bool{ + "foo.example.com": true, + }, + IsMaster: true, + Minions: []*FullMinion{ + { + Ingress: minion2, + ValidPaths: map[string]bool{ + "/path-2": true, + }, + }, + }, + ChildWarnings: map[string][]string{}, + }, + }, + } + expectedProblems = nil + + changes, problems = configuration.DeleteIngress("default/ingress-minion-1") + if diff := cmp.Diff(expectedChanges, changes); diff != "" { + t.Errorf("DeleteIngress() returned unexpected result (-want +got):\n%s", diff) + } + if diff := cmp.Diff(expectedProblems, problems); diff != "" { + t.Errorf("DeleteIngress() returned unexpected result (-want +got):\n%s", diff) + } + + // Remove master + + expectedChanges = []ResourceChange{ + { + Op: Delete, + Resource: &FullIngress{ + Ingress: master, + ValidHosts: map[string]bool{ + "foo.example.com": true, + }, + IsMaster: true, + Minions: []*FullMinion{ + { + Ingress: minion2, + ValidPaths: map[string]bool{ + "/path-2": true, + }, + }, + }, + ChildWarnings: map[string][]string{}, + }, + }, + } + expectedProblems = []ConfigurationProblem{ + { + Object: minion2, + Reason: "NoIngressMasterFound", + Message: "Ingress master is invalid or doesn't exist", + }, + } + + changes, problems = configuration.DeleteIngress("default/ingress-master") + if diff := cmp.Diff(expectedChanges, changes); diff != "" { + t.Errorf("DeleteIngress() returned unexpected result (-want +got):\n%s", diff) + } + if diff := cmp.Diff(expectedProblems, problems); diff != "" { + t.Errorf("DeleteIngress() returned unexpected result (-want +got):\n%s", diff) + } + + // Remove minion-2 + + expectedChanges = nil + expectedProblems = nil + + changes, problems = configuration.DeleteIngress("default/ingress-minion-2") + if diff := cmp.Diff(expectedChanges, changes); diff != "" { + t.Errorf("DeleteIngress() returned unexpected result (-want +got):\n%s", diff) + } + if diff := cmp.Diff(expectedProblems, problems); diff != "" { + t.Errorf("DeleteIngress() returned unexpected result (-want +got):\n%s", diff) + } +} + +func TestMinionPathCollisions(t *testing.T) { + configuration := createTestConfiguration() + + // Add master + + master := createTestIngressMaster("ingress-master", "foo.example.com") + expectedChanges := []ResourceChange{ + { + Op: AddOrUpdate, + Resource: &FullIngress{ + Ingress: master, + ValidHosts: map[string]bool{ + "foo.example.com": true, + }, + IsMaster: true, + ChildWarnings: map[string][]string{}, + }, + }, + } + var expectedProblems []ConfigurationProblem + + changes, problems := configuration.AddOrUpdateIngress(master) + if diff := cmp.Diff(expectedChanges, changes); diff != "" { + t.Errorf("AddOrUpdateIngress() returned unexpected result (-want +got):\n%s", diff) + } + if diff := cmp.Diff(expectedProblems, problems); diff != "" { + t.Errorf("AddOrUpdateIngress() returned unexpected result (-want +got):\n%s", diff) + } + + // Add minion-1 + + minion1 := createTestIngressMinion("ingress-minion-1", "foo.example.com", "/") + expectedChanges = []ResourceChange{ + { + Op: AddOrUpdate, + Resource: &FullIngress{ + Ingress: master, + ValidHosts: map[string]bool{ + "foo.example.com": true, + }, + IsMaster: true, + Minions: []*FullMinion{ + { + Ingress: minion1, + ValidPaths: map[string]bool{ + "/": true, + }, + }, + }, + ChildWarnings: map[string][]string{}, + }, + }, + } + expectedProblems = nil + + changes, problems = configuration.AddOrUpdateIngress(minion1) + if diff := cmp.Diff(expectedChanges, changes); diff != "" { + t.Errorf("AddOrUpdateIngress() returned unexpected result (-want +got):\n%s", diff) + } + if diff := cmp.Diff(expectedProblems, problems); diff != "" { + t.Errorf("AddOrUpdateIngress() returned unexpected result (-want +got):\n%s", diff) + } + + // Add minion-2 + + minion2 := createTestIngressMinion("ingress-minion-2", "foo.example.com", "/") + expectedChanges = []ResourceChange{ + { + Op: AddOrUpdate, + Resource: &FullIngress{ + Ingress: master, + ValidHosts: map[string]bool{ + "foo.example.com": true, + }, + IsMaster: true, + Minions: []*FullMinion{ + { + Ingress: minion1, + ValidPaths: map[string]bool{ + "/": true, + }, + }, + { + Ingress: minion2, + ValidPaths: map[string]bool{}, + }, + }, + ChildWarnings: map[string][]string{ + "default/ingress-minion-2": { + "path / is taken by another resource", + }, + }, + }, + }, + } + expectedProblems = nil + + changes, problems = configuration.AddOrUpdateIngress(minion2) + if diff := cmp.Diff(expectedChanges, changes); diff != "" { + t.Errorf("AddOrUpdateIngress() returned unexpected result (-want +got):\n%s", diff) + } + if diff := cmp.Diff(expectedProblems, problems); diff != "" { + t.Errorf("AddOrUpdateIngress() returned unexpected result (-want +got):\n%s", diff) + } + + // Delete minion-1 + + expectedChanges = []ResourceChange{ + { + Op: AddOrUpdate, + Resource: &FullIngress{ + Ingress: master, + ValidHosts: map[string]bool{ + "foo.example.com": true, + }, + IsMaster: true, + Minions: []*FullMinion{ + { + Ingress: minion2, + ValidPaths: map[string]bool{ + "/": true, + }, + }, + }, + ChildWarnings: map[string][]string{}, + }, + }, + } + expectedProblems = nil + + changes, problems = configuration.DeleteIngress("default/ingress-minion-1") + if diff := cmp.Diff(expectedChanges, changes); diff != "" { + t.Errorf("DeleteIngress() returned unexpected result (-want +got):\n%s", diff) + } + if diff := cmp.Diff(expectedProblems, problems); diff != "" { + t.Errorf("DeleteIngress() returned unexpected result (-want +got):\n%s", diff) + } +} + +func TestAddIngressWithIncorrectClass(t *testing.T) { + configuration := createTestConfiguration() + + // Add Ingress with incorrect class + + ing := createTestIngress("regular-ingress", "foo.example.com") + ing.Annotations["kubernetes.io/ingress.class"] = "someproxy" + + var expectedChanges []ResourceChange + var expectedProblems []ConfigurationProblem + + changes, problems := configuration.AddOrUpdateIngress(ing) + if diff := cmp.Diff(expectedChanges, changes); diff != "" { + t.Errorf("AddOrUpdateIngress() returned unexpected result (-want +got):\n%s", diff) + } + if diff := cmp.Diff(expectedProblems, problems); diff != "" { + t.Errorf("AddOrUpdateIngress() returned unexpected result (-want +got):\n%s", diff) + } + + // Make the class correct + + updatedIng := ing.DeepCopy() + updatedIng.Annotations["kubernetes.io/ingress.class"] = "nginx" + + expectedChanges = []ResourceChange{ + { + Op: AddOrUpdate, + Resource: &FullIngress{ + Ingress: updatedIng, + ValidHosts: map[string]bool{ + "foo.example.com": true, + }, + ChildWarnings: map[string][]string{}, + }, + }, + } + expectedProblems = nil + + changes, problems = configuration.AddOrUpdateIngress(updatedIng) + if diff := cmp.Diff(expectedChanges, changes); diff != "" { + t.Errorf("AddOrUpdateIngress() returned unexpected result (-want +got):\n%s", diff) + } + if diff := cmp.Diff(expectedProblems, problems); diff != "" { + t.Errorf("AddOrUpdateIngress() returned unexpected result (-want +got):\n%s", diff) + } + + // Make the class incorrect + + expectedChanges = []ResourceChange{ + { + Op: Delete, + Resource: &FullIngress{ + Ingress: updatedIng, + ValidHosts: map[string]bool{ + "foo.example.com": true, + }, + ChildWarnings: map[string][]string{}, + }, + }, + } + expectedProblems = nil + + changes, problems = configuration.AddOrUpdateIngress(ing) + if diff := cmp.Diff(expectedChanges, changes); diff != "" { + t.Errorf("AddOrUpdateIngress() returned unexpected result (-want +got):\n%s", diff) + } + if diff := cmp.Diff(expectedProblems, problems); diff != "" { + t.Errorf("AddOrUpdateIngress() returned unexpected result (-want +got):\n%s", diff) + } +} + +func TestAddVirtualServer(t *testing.T) { + configuration := createTestConfiguration() + + // no problems are expected for all cases + var expectedProblems []ConfigurationProblem + + // Add a VirtualServer + + vs := createTestVirtualServer("virtualserver", "foo.example.com") + expectedChanges := []ResourceChange{ + { + Op: AddOrUpdate, + Resource: &FullVirtualServer{ + VirtualServer: vs, + }, + }, + } + + changes, problems := configuration.AddOrUpdateVirtualServer(vs) + if diff := cmp.Diff(expectedChanges, changes); diff != "" { + t.Errorf("AddOrUpdateVirtualServer() returned unexpected result (-want +got):\n%s", diff) + } + if diff := cmp.Diff(expectedProblems, problems); diff != "" { + t.Errorf("AddOrUpdateVirtualServer() returned unexpected result (-want +got):\n%s", diff) + } + + // Update VirtualServer + + updatedVS := vs.DeepCopy() + updatedVS.Generation++ + updatedVS.Spec.ServerSnippets = "# snippet" + + expectedChanges = []ResourceChange{ + { + Op: AddOrUpdate, + Resource: &FullVirtualServer{ + VirtualServer: updatedVS, + }, + }, + } + + changes, problems = configuration.AddOrUpdateVirtualServer(updatedVS) + if diff := cmp.Diff(expectedChanges, changes); diff != "" { + t.Errorf("AddOrUpdateVirtualServer() returned unexpected result (-want +got):\n%s", diff) + } + if diff := cmp.Diff(expectedProblems, problems); diff != "" { + t.Errorf("AddOrUpdateVirtualServer() returned unexpected result (-want +got):\n%s", diff) + } + + // Make VirtualServer invalid + + invalidVS := updatedVS.DeepCopy() + invalidVS.Generation++ + invalidVS.Spec.Host = "" + + expectedChanges = []ResourceChange{ + { + Op: Delete, + Resource: &FullVirtualServer{ + VirtualServer: updatedVS, + }, + Error: "spec.host: Required value", + }, + } + + changes, problems = configuration.AddOrUpdateVirtualServer(invalidVS) + if diff := cmp.Diff(expectedChanges, changes); diff != "" { + t.Errorf("AddOrUpdateVirtualServer() returned unexpected result (-want +got):\n%s", diff) + } + if diff := cmp.Diff(expectedProblems, problems); diff != "" { + t.Errorf("AddOrUpdateVirtualServer() returned unexpected result (-want +got):\n%s", diff) + } + + // Restore VirtualServer + + expectedChanges = []ResourceChange{ + { + Op: AddOrUpdate, + Resource: &FullVirtualServer{ + VirtualServer: updatedVS, + }, + }, + } + + changes, problems = configuration.AddOrUpdateVirtualServer(updatedVS) + if diff := cmp.Diff(expectedChanges, changes); diff != "" { + t.Errorf("AddOrUpdateVirtualServer() returned unexpected result (-want +got):\n%s", diff) + } + if diff := cmp.Diff(expectedProblems, problems); diff != "" { + t.Errorf("AddOrUpdateVirtualServer() returned unexpected result (-want +got):\n%s", diff) + } + + // Update VirtualServer host + + updatedHostVS := updatedVS.DeepCopy() + updatedHostVS.Generation++ + updatedHostVS.Spec.Host = "bar.example.com" + + expectedChanges = []ResourceChange{ + { + Op: AddOrUpdate, + Resource: &FullVirtualServer{ + VirtualServer: updatedHostVS, + }, + }, + } + + changes, problems = configuration.AddOrUpdateVirtualServer(updatedHostVS) + if diff := cmp.Diff(expectedChanges, changes); diff != "" { + t.Errorf("AddOrUpdateVirtualServer() returned unexpected result (-want +got):\n%s", diff) + } + if diff := cmp.Diff(expectedProblems, problems); diff != "" { + t.Errorf("AddOrUpdateVirtualServer() returned unexpected result (-want +got):\n%s", diff) + } + + // Delete VirtualServer + + expectedChanges = []ResourceChange{ + { + Op: Delete, + Resource: &FullVirtualServer{ + VirtualServer: updatedHostVS, + }, + }, + } + + changes, problems = configuration.DeleteVirtualServer("default/virtualserver") + if diff := cmp.Diff(expectedChanges, changes); diff != "" { + t.Errorf("DeleteVirtualServer() returned unexpected result (-want +got):\n%s", diff) + } + if diff := cmp.Diff(expectedProblems, problems); diff != "" { + t.Errorf("DeleteVirtualServer() returned unexpected result (-want +got):\n%s", diff) + } +} + +func TestAddInvalidVirtualServer(t *testing.T) { + configuration := createTestConfiguration() + + vs := createTestVirtualServer("virtualserver", "") + + var expectedChanges []ResourceChange + expectedProblems := []ConfigurationProblem{ + { + Object: vs, + IsError: true, + Reason: "Rejected", + Message: "spec.host: Required value", + }, + } + + changes, problems := configuration.AddOrUpdateVirtualServer(vs) + if diff := cmp.Diff(expectedChanges, changes); diff != "" { + t.Errorf("AddOrUpdateVirtualServer() returned unexpected result (-want +got):\n%s", diff) + } + if diff := cmp.Diff(expectedProblems, problems); diff != "" { + t.Errorf("AddOrUpdateVirtualServer() returned unexpected result (-want +got):\n%s", diff) + } +} + +func TestAddInvalidVirtualServerWithIncorrectClass(t *testing.T) { + configuration := createTestConfiguration() + + // Add VirtualServer with incorrect class + + vs := createTestVirtualServer("virtualserver", "example.com") + vs.Spec.IngressClass = "someproxy" + + var expectedChanges []ResourceChange + var expectedProblems []ConfigurationProblem + + changes, problems := configuration.AddOrUpdateVirtualServer(vs) + if diff := cmp.Diff(expectedChanges, changes); diff != "" { + t.Errorf("AddOrUpdateVirtualServer() returned unexpected result (-want +got):\n%s", diff) + } + if diff := cmp.Diff(expectedProblems, problems); diff != "" { + t.Errorf("AddOrUpdateVirtualServer() returned unexpected result (-want +got):\n%s", diff) + } + + // Make the class correct + + updatedVS := vs.DeepCopy() + updatedVS.Generation++ + updatedVS.Spec.IngressClass = "nginx" + + expectedChanges = []ResourceChange{ + { + Op: AddOrUpdate, + Resource: &FullVirtualServer{ + VirtualServer: updatedVS, + }, + }, + } + expectedProblems = nil + + changes, problems = configuration.AddOrUpdateVirtualServer(updatedVS) + if diff := cmp.Diff(expectedChanges, changes); diff != "" { + t.Errorf("AddOrUpdateVirtualServer() returned unexpected result (-want +got):\n%s", diff) + } + if diff := cmp.Diff(expectedProblems, problems); diff != "" { + t.Errorf("AddOrUpdateVirtualServer() returned unexpected result (-want +got):\n%s", diff) + } + + // Make the class incorrect + + expectedChanges = []ResourceChange{ + { + Op: Delete, + Resource: &FullVirtualServer{ + VirtualServer: updatedVS, + }, + }, + } + expectedProblems = nil + + changes, problems = configuration.AddOrUpdateVirtualServer(vs) + if diff := cmp.Diff(expectedChanges, changes); diff != "" { + t.Errorf("AddOrUpdateVirtualServer() returned unexpected result (-want +got):\n%s", diff) + } + if diff := cmp.Diff(expectedProblems, problems); diff != "" { + t.Errorf("AddOrUpdateVirtualServer() returned unexpected result (-want +got):\n%s", diff) + } +} + +func TestDeleteNonExistingVirtualServer(t *testing.T) { + configuration := createTestConfiguration() + + var expectedChanges []ResourceChange + var expectedProblems []ConfigurationProblem + + changes, problems := configuration.DeleteVirtualServer("default/virtualserver") + if diff := cmp.Diff(expectedChanges, changes); diff != "" { + t.Errorf("DeleteVirtualServer() returned unexpected result (-want +got):\n%s", diff) + } + if diff := cmp.Diff(expectedProblems, problems); diff != "" { + t.Errorf("DeleteVirtualServer() returned unexpected result (-want +got):\n%s", diff) + } +} + +func TestAddVirtualServerWithVirtualServerRoutes(t *testing.T) { + configuration := createTestConfiguration() + + // Add VirtualServerRoute-1 + + vsr1 := createTestVirtualServerRoute("virtualserverroute-1", "foo.example.com", "/first") + var expectedChanges []ResourceChange + expectedProblems := []ConfigurationProblem{ + { + Object: vsr1, + Reason: "NoVirtualServerFound", + Message: "VirtualServer is invalid or doesn't exist", + }, + } + + changes, problems := configuration.AddOrUpdateVirtualServerRoute(vsr1) + if diff := cmp.Diff(expectedChanges, changes); diff != "" { + t.Errorf("AddOrUpdateVirtualServerRoute() returned unexpected result (-want +got):\n%s", diff) + } + if diff := cmp.Diff(expectedProblems, problems); diff != "" { + t.Errorf("AddOrUpdateVirtualServerRoute() returned unexpected result (-want +got):\n%s", diff) + } + + // Add VirtualServer + + vs := createTestVirtualServerWithRoutes( + "virtualserver", + "foo.example.com", + []conf_v1.Route{ + { + Path: "/first", + Route: "virtualserverroute-1", + }, + { + Path: "/second", + Route: "virtualserverroute-2", + }, + }) + expectedChanges = []ResourceChange{ + { + Op: AddOrUpdate, + Resource: &FullVirtualServer{ + VirtualServer: vs, + VirtualServerRoutes: []*conf_v1.VirtualServerRoute{vsr1}, + Warnings: []string{"VirtualServerRoute default/virtualserverroute-2 doesn't exist or invalid"}, + }, + }, + } + expectedProblems = nil + + changes, problems = configuration.AddOrUpdateVirtualServer(vs) + if diff := cmp.Diff(expectedChanges, changes); diff != "" { + t.Errorf("AddOrUpdateVirtualServer() returned unexpected result (-want +got):\n%s", diff) + } + if diff := cmp.Diff(expectedProblems, problems); diff != "" { + t.Errorf("AddOrUpdateVirtualServer() returned unexpected result (-want +got):\n%s", diff) + } + + vsr2 := createTestVirtualServerRoute("virtualserverroute-2", "foo.example.com", "/second") + + // Add VirtualServerRoute-2 + + expectedChanges = []ResourceChange{ + { + Op: AddOrUpdate, + Resource: &FullVirtualServer{ + VirtualServer: vs, + VirtualServerRoutes: []*conf_v1.VirtualServerRoute{vsr1, vsr2}, + }, + }, + } + expectedProblems = nil + + changes, problems = configuration.AddOrUpdateVirtualServerRoute(vsr2) + if diff := cmp.Diff(expectedChanges, changes); diff != "" { + t.Errorf("AddOrUpdateVirtualServerRoute() returned unexpected result (-want +got):\n%s", diff) + } + if diff := cmp.Diff(expectedProblems, problems); diff != "" { + t.Errorf("AddOrUpdateVirtualServerRoute() returned unexpected result (-want +got):\n%s", diff) + } + + // Update VirtualServerRoute-1 + + updatedVSR1 := vsr1.DeepCopy() + updatedVSR1.Generation++ + updatedVSR1.Spec.Subroutes[0].LocationSnippets = "# snippet" + expectedChanges = []ResourceChange{ + { + Op: AddOrUpdate, + Resource: &FullVirtualServer{ + VirtualServer: vs, + VirtualServerRoutes: []*conf_v1.VirtualServerRoute{updatedVSR1, vsr2}, + }, + }, + } + expectedProblems = nil + + changes, problems = configuration.AddOrUpdateVirtualServerRoute(updatedVSR1) + if diff := cmp.Diff(expectedChanges, changes); diff != "" { + t.Errorf("AddOrUpdateVirtualServerRoute() returned unexpected result (-want +got):\n%s", diff) + } + if diff := cmp.Diff(expectedProblems, problems); diff != "" { + t.Errorf("AddOrUpdateVirtualServerRoute() returned unexpected result (-want +got):\n%s", diff) + } + + // Make VirtualServerRoute-1 invalid + + invalidVSR1 := updatedVSR1.DeepCopy() + invalidVSR1.Generation++ + invalidVSR1.Spec.Host = "" + expectedChanges = []ResourceChange{ + { + Op: AddOrUpdate, + Resource: &FullVirtualServer{ + VirtualServer: vs, + VirtualServerRoutes: []*conf_v1.VirtualServerRoute{vsr2}, + Warnings: []string{"VirtualServerRoute default/virtualserverroute-1 doesn't exist or invalid"}, + }, + }, + } + expectedProblems = []ConfigurationProblem{ + { + Object: invalidVSR1, + IsError: true, + Reason: "Rejected", + Message: "spec.host: Required value", + }, + } + + changes, problems = configuration.AddOrUpdateVirtualServerRoute(invalidVSR1) + if diff := cmp.Diff(expectedChanges, changes); diff != "" { + t.Errorf("AddOrUpdateVirtualServerRoute() returned unexpected result (-want +got):\n%s", diff) + } + if diff := cmp.Diff(expectedProblems, problems); diff != "" { + t.Errorf("AddOrUpdateVirtualServerRoute() returned unexpected result (-want +got):\n%s", diff) + } + + // Restore VirtualServerRoute-1 + + expectedChanges = []ResourceChange{ + { + Op: AddOrUpdate, + Resource: &FullVirtualServer{ + VirtualServer: vs, + VirtualServerRoutes: []*conf_v1.VirtualServerRoute{vsr1, vsr2}, + }, + }, + } + expectedProblems = nil + + changes, problems = configuration.AddOrUpdateVirtualServerRoute(vsr1) + if diff := cmp.Diff(expectedChanges, changes); diff != "" { + t.Errorf("AddOrUpdateVirtualServerRoute() returned unexpected result (-want +got):\n%s", diff) + } + if diff := cmp.Diff(expectedProblems, problems); diff != "" { + t.Errorf("AddOrUpdateVirtualServerRoute() returned unexpected result (-want +got):\n%s", diff) + } + + // Make VirtualServerRoute-1 invalid for VirtualServer + + invalidForVSVSR1 := vsr1.DeepCopy() + invalidForVSVSR1.Generation++ + invalidForVSVSR1.Spec.Subroutes[0].Path = "/" + expectedChanges = []ResourceChange{ + { + Op: AddOrUpdate, + Resource: &FullVirtualServer{ + VirtualServer: vs, + VirtualServerRoutes: []*conf_v1.VirtualServerRoute{vsr2}, + Warnings: []string{"VirtualServerRoute default/virtualserverroute-1 is invalid: spec.subroutes[0]: Invalid value: \"/\": must start with '/first'"}, + }, + }, + } + expectedProblems = []ConfigurationProblem{ + { + Object: invalidForVSVSR1, + Reason: "Ignored", + Message: "VirtualServer default/virtualserver ignores VirtualServerRoute", + }, + } + + changes, problems = configuration.AddOrUpdateVirtualServerRoute(invalidForVSVSR1) + if diff := cmp.Diff(expectedChanges, changes); diff != "" { + t.Errorf("AddOrUpdateVirtualServerRoute() returned unexpected result (-want +got):\n%s", diff) + } + if diff := cmp.Diff(expectedProblems, problems); diff != "" { + t.Errorf("AddOrUpdateVirtualServerRoute() returned unexpected result (-want +got):\n%s", diff) + } + + // Restore VirtualServerRoute-1 + + expectedChanges = []ResourceChange{ + { + Op: AddOrUpdate, + Resource: &FullVirtualServer{ + VirtualServer: vs, + VirtualServerRoutes: []*conf_v1.VirtualServerRoute{vsr1, vsr2}, + }, + }, + } + expectedProblems = nil + + changes, problems = configuration.AddOrUpdateVirtualServerRoute(vsr1) + if diff := cmp.Diff(expectedChanges, changes); diff != "" { + t.Errorf("AddOrUpdateVirtualServerRoute() returned unexpected result (-want +got):\n%s", diff) + } + if diff := cmp.Diff(expectedProblems, problems); diff != "" { + t.Errorf("AddOrUpdateVirtualServerRoute() returned unexpected result (-want +got):\n%s", diff) + } + + // Update host of VirtualServerRoute-2 + + updatedVSR2 := vsr2.DeepCopy() + updatedVSR2.Generation++ + updatedVSR2.Spec.Host = "bar.example.com" + expectedChanges = []ResourceChange{ + { + Op: AddOrUpdate, + Resource: &FullVirtualServer{ + VirtualServer: vs, + VirtualServerRoutes: []*conf_v1.VirtualServerRoute{vsr1}, + Warnings: []string{"VirtualServerRoute default/virtualserverroute-2 is invalid: spec.host: Invalid value: \"bar.example.com\": must be equal to 'foo.example.com'"}, + }, + }, + } + expectedProblems = []ConfigurationProblem{ + { + Object: updatedVSR2, + Reason: "NoVirtualServerFound", + Message: "VirtualServer is invalid or doesn't exist", + }, + } + + changes, problems = configuration.AddOrUpdateVirtualServerRoute(updatedVSR2) + if diff := cmp.Diff(expectedChanges, changes); diff != "" { + t.Errorf("AddOrUpdateVirtualServerRoute() returned unexpected result (-want +got):\n%s", diff) + } + if diff := cmp.Diff(expectedProblems, problems); diff != "" { + t.Errorf("AddOrUpdateVirtualServerRoute() returned unexpected result (-want +got):\n%s", diff) + } + + // Update host of VirtualServer + + updatedVS := vs.DeepCopy() + updatedVS.Generation++ + updatedVS.Spec.Host = "bar.example.com" + expectedChanges = []ResourceChange{ + { + Op: AddOrUpdate, + Resource: &FullVirtualServer{ + VirtualServer: updatedVS, + VirtualServerRoutes: []*conf_v1.VirtualServerRoute{updatedVSR2}, + Warnings: []string{"VirtualServerRoute default/virtualserverroute-1 is invalid: spec.host: Invalid value: \"foo.example.com\": must be equal to 'bar.example.com'"}, + }, + }, + } + expectedProblems = []ConfigurationProblem{ + { + Object: vsr1, + Reason: "NoVirtualServerFound", + Message: "VirtualServer is invalid or doesn't exist", + }, + } + + changes, problems = configuration.AddOrUpdateVirtualServer(updatedVS) + if diff := cmp.Diff(expectedChanges, changes); diff != "" { + t.Errorf("AddOrUpdateVirtualServer() returned unexpected result (-want +got):\n%s", diff) + } + if diff := cmp.Diff(expectedProblems, problems); diff != "" { + t.Errorf("AddOrUpdateVirtualServer() returned unexpected result (-want +got):\n%s", diff) + } + + // Restore host of VirtualServer + + expectedChanges = []ResourceChange{ + { + Op: AddOrUpdate, + Resource: &FullVirtualServer{ + VirtualServer: vs, + VirtualServerRoutes: []*conf_v1.VirtualServerRoute{vsr1}, + Warnings: []string{"VirtualServerRoute default/virtualserverroute-2 is invalid: spec.host: Invalid value: \"bar.example.com\": must be equal to 'foo.example.com'"}, + }, + }, + } + expectedProblems = []ConfigurationProblem{ + { + Object: updatedVSR2, + Reason: "NoVirtualServerFound", + Message: "VirtualServer is invalid or doesn't exist", + }, + } + + changes, problems = configuration.AddOrUpdateVirtualServer(vs) + if diff := cmp.Diff(expectedChanges, changes); diff != "" { + t.Errorf("AddOrUpdateVirtualServer() returned unexpected result (-want +got):\n%s", diff) + } + if diff := cmp.Diff(expectedProblems, problems); diff != "" { + t.Errorf("AddOrUpdateVirtualServer() returned unexpected result (-want +got):\n%s", diff) + } + + // Restore host of VirtualServerRoute-2 + + expectedChanges = []ResourceChange{ + { + Op: AddOrUpdate, + Resource: &FullVirtualServer{ + VirtualServer: vs, + VirtualServerRoutes: []*conf_v1.VirtualServerRoute{vsr1, vsr2}, + }, + }, + } + expectedProblems = nil + + changes, problems = configuration.AddOrUpdateVirtualServerRoute(vsr2) + if diff := cmp.Diff(expectedChanges, changes); diff != "" { + t.Errorf("AddOrUpdateVirtualServerRoute() returned unexpected result (-want +got):\n%s", diff) + } + if diff := cmp.Diff(expectedProblems, problems); diff != "" { + t.Errorf("AddOrUpdateVirtualServerRoute() returned unexpected result (-want +got):\n%s", diff) + } + + // Remove VirtualServerRoute-1 + + expectedChanges = []ResourceChange{ + { + Op: AddOrUpdate, + Resource: &FullVirtualServer{ + VirtualServer: vs, + VirtualServerRoutes: []*conf_v1.VirtualServerRoute{vsr2}, + Warnings: []string{"VirtualServerRoute default/virtualserverroute-1 doesn't exist or invalid"}, + }, + }, + } + expectedProblems = nil + + changes, problems = configuration.DeleteVirtualServerRoute("default/virtualserverroute-1") + if diff := cmp.Diff(expectedChanges, changes); diff != "" { + t.Errorf("DeleteVirtualServerRoute() returned unexpected result (-want +got):\n%s", diff) + } + if diff := cmp.Diff(expectedProblems, problems); diff != "" { + t.Errorf("DeleteVirtualServerRoute() returned unexpected result (-want +got):\n%s", diff) + } + + // Remove VirtualServer + + expectedChanges = []ResourceChange{ + { + Op: Delete, + Resource: &FullVirtualServer{ + VirtualServer: vs, + VirtualServerRoutes: []*conf_v1.VirtualServerRoute{vsr2}, + Warnings: []string{"VirtualServerRoute default/virtualserverroute-1 doesn't exist or invalid"}, + }, + }, + } + expectedProblems = []ConfigurationProblem{ + { + Object: vsr2, + Reason: "NoVirtualServerFound", + Message: "VirtualServer is invalid or doesn't exist", + }, + } + + changes, problems = configuration.DeleteVirtualServer("default/virtualserver") + if diff := cmp.Diff(expectedChanges, changes); diff != "" { + t.Errorf("DeleteVirtualServer() returned unexpected result (-want +got):\n%s", diff) + } + if diff := cmp.Diff(expectedProblems, problems); diff != "" { + t.Errorf("DeleteVirtualServer() returned unexpected result (-want +got):\n%s", diff) + } + + // Remove VirtualServerRoute-2 + + expectedChanges = nil + expectedProblems = nil + + changes, problems = configuration.DeleteVirtualServerRoute("default/virtualserverroute-2") + if diff := cmp.Diff(expectedChanges, changes); diff != "" { + t.Errorf("DeleteVirtualServerRoute() returned unexpected result (-want +got):\n%s", diff) + } + if diff := cmp.Diff(expectedProblems, problems); diff != "" { + t.Errorf("DeleteVirtualServerRoute() returned unexpected result (-want +got):\n%s", diff) + } +} + +func TestAddInvalidVirtualServerRoute(t *testing.T) { + configuration := createTestConfiguration() + + vsr := createTestVirtualServerRoute("virtualserverroute", "", "/") + + var expectedChanges []ResourceChange + expectedProblems := []ConfigurationProblem{ + { + Object: vsr, + IsError: true, + Reason: "Rejected", + Message: "spec.host: Required value", + }, + } + + changes, problems := configuration.AddOrUpdateVirtualServerRoute(vsr) + if diff := cmp.Diff(expectedChanges, changes); diff != "" { + t.Errorf("AddOrUpdateVirtualServerRoute() returned unexpected result (-want +got):\n%s", diff) + } + if diff := cmp.Diff(expectedProblems, problems); diff != "" { + t.Errorf("AddOrUpdateVirtualServerRoute() returned unexpected result (-want +got):\n%s", diff) + } +} + +func TestAddVirtualServerWithIncorrectClass(t *testing.T) { + configuration := createTestConfiguration() + + vsr := createTestVirtualServerRoute("virtualserver", "foo.example.com", "/") + vsr.Spec.IngressClass = "someproxy" + + var expectedChanges []ResourceChange + var expectedProblems []ConfigurationProblem + + changes, problems := configuration.AddOrUpdateVirtualServerRoute(vsr) + if diff := cmp.Diff(expectedChanges, changes); diff != "" { + t.Errorf("AddOrUpdateVirtualServerRoute() returned unexpected result (-want +got):\n%s", diff) + } + if diff := cmp.Diff(expectedProblems, problems); diff != "" { + t.Errorf("AddOrUpdateVirtualServerRoute() returned unexpected result (-want +got):\n%s", diff) + } +} + +func TestDeleteNonExistingVirtualServerRoute(t *testing.T) { + configuration := createTestConfiguration() + + var expectedChanges []ResourceChange + var expectedProblems []ConfigurationProblem + + changes, problems := configuration.DeleteVirtualServerRoute("default/virtualserverroute") + if diff := cmp.Diff(expectedChanges, changes); diff != "" { + t.Errorf("DeleteVirtualServerRoute() returned unexpected result (-want +got):\n%s", diff) + } + if diff := cmp.Diff(expectedProblems, problems); diff != "" { + t.Errorf("DeleteVirtualServerRoute() returned unexpected result (-want +got):\n%s", diff) + } +} + +func TestHostCollisions(t *testing.T) { + configuration := createTestConfiguration() + + var expectedProblems []ConfigurationProblem + + masterIng := createTestIngressMaster("master-ingress", "foo.example.com") + regularIng := createTestIngress("regular-ingress", "foo.example.com", "bar.example.com") + vs := createTestVirtualServer("virtualserver", "foo.example.com") + regularIng2 := createTestIngress("regular-ingress-2", "foo.example.com") + + // Add VirtualServer + + expectedChanges := []ResourceChange{ + { + Op: AddOrUpdate, + Resource: &FullVirtualServer{ + VirtualServer: vs, + }, + }, + } + expectedProblems = nil + + changes, problems := configuration.AddOrUpdateVirtualServer(vs) + if diff := cmp.Diff(expectedChanges, changes); diff != "" { + t.Errorf("AddOrUpdateVirtualServer() returned unexpected result (-want +got):\n%s", diff) + } + if diff := cmp.Diff(expectedProblems, problems); diff != "" { + t.Errorf("AddOrUpdateVirtualServer() returned unexpected result (-want +got):\n%s", diff) + } + + // Add regular Ingress + + expectedChanges = []ResourceChange{ + { + Op: Delete, + Resource: &FullVirtualServer{ + VirtualServer: vs, + Warnings: []string{"host foo.example.com is taken by another resource"}, + }, + }, + { + Op: AddOrUpdate, + Resource: &FullIngress{ + Ingress: regularIng, + ValidHosts: map[string]bool{"foo.example.com": true, "bar.example.com": true}, + ChildWarnings: map[string][]string{}, + }, + }, + } + expectedProblems = []ConfigurationProblem{ + { + Object: vs, + IsError: false, + Reason: "Rejected", + Message: "Host is taken by another resource", + }, + } + + changes, problems = configuration.AddOrUpdateIngress(regularIng) + if diff := cmp.Diff(expectedChanges, changes); diff != "" { + t.Errorf("AddOrUpdateIngress() returned unexpected result (-want +got):\n%s", diff) + } + if diff := cmp.Diff(expectedProblems, problems); diff != "" { + t.Errorf("AddOrUpdateIngress() returned unexpected result (-want +got):\n%s", diff) + } + + // Add master Ingress + + expectedChanges = []ResourceChange{ + { + Op: AddOrUpdate, + Resource: &FullIngress{ + Ingress: regularIng, + ValidHosts: map[string]bool{"bar.example.com": true}, + Warnings: []string{"host foo.example.com is taken by another resource"}, + ChildWarnings: map[string][]string{}, + }, + }, + { + Op: AddOrUpdate, + Resource: &FullIngress{ + Ingress: masterIng, + IsMaster: true, + ValidHosts: map[string]bool{"foo.example.com": true}, + ChildWarnings: map[string][]string{}, + }, + }, + } + expectedProblems = nil + + changes, problems = configuration.AddOrUpdateIngress(masterIng) + if diff := cmp.Diff(expectedChanges, changes); diff != "" { + t.Errorf("AddOrUpdateIngress() returned unexpected result (-want +got):\n%s", diff) + } + if diff := cmp.Diff(expectedProblems, problems); diff != "" { + t.Errorf("AddOrUpdateIngress() returned unexpected result (-want +got):\n%s", diff) + } + + // Add regular Ingress-2 + + expectedChanges = nil + expectedProblems = []ConfigurationProblem{ + { + Object: regularIng2, + IsError: false, + Reason: "Rejected", + Message: "All hosts are taken by other resources", + }, + } + + changes, problems = configuration.AddOrUpdateIngress(regularIng2) + if diff := cmp.Diff(expectedChanges, changes); diff != "" { + t.Errorf("AddOrUpdateIngress() returned unexpected result (-want +got):\n%s", diff) + } + if diff := cmp.Diff(expectedProblems, problems); diff != "" { + t.Errorf("AddOrUpdateIngress() returned unexpected result (-want +got):\n%s", diff) + } + + // Delete regular Ingress-2 + expectedChanges = nil + expectedProblems = nil + + changes, problems = configuration.DeleteIngress("default/regular-ingress-2") + if diff := cmp.Diff(expectedChanges, changes); diff != "" { + t.Errorf("DeleteIngress() returned unexpected result (-want +got):\n%s", diff) + } + if diff := cmp.Diff(expectedProblems, problems); diff != "" { + t.Errorf("DeleteIngress() returned unexpected result (-want +got):\n%s", diff) + } + + // Delete master Ingress + + expectedChanges = []ResourceChange{ + { + Op: Delete, + Resource: &FullIngress{ + Ingress: masterIng, + IsMaster: true, + ValidHosts: map[string]bool{"foo.example.com": true}, + ChildWarnings: map[string][]string{}, + }, + }, + { + Op: AddOrUpdate, + Resource: &FullIngress{ + Ingress: regularIng, + ValidHosts: map[string]bool{"foo.example.com": true, "bar.example.com": true}, + ChildWarnings: map[string][]string{}, + }, + }, + } + expectedProblems = nil + + changes, problems = configuration.DeleteIngress("default/master-ingress") + if diff := cmp.Diff(expectedChanges, changes); diff != "" { + t.Errorf("DeleteIngress() returned unexpected result (-want +got):\n%s", diff) + } + if diff := cmp.Diff(expectedProblems, problems); diff != "" { + t.Errorf("DeleteIngress() returned unexpected result (-want +got):\n%s", diff) + } + + // Delete regular Ingress + + expectedChanges = []ResourceChange{ + { + Op: Delete, + Resource: &FullIngress{ + Ingress: regularIng, + ValidHosts: map[string]bool{"foo.example.com": true, "bar.example.com": true}, + ChildWarnings: map[string][]string{}, + }, + }, + { + Op: AddOrUpdate, + Resource: &FullVirtualServer{ + VirtualServer: vs, + }, + }, + } + expectedProblems = nil + + changes, problems = configuration.DeleteIngress("default/regular-ingress") + if diff := cmp.Diff(expectedChanges, changes); diff != "" { + t.Errorf("DeleteIngress() returned unexpected result (-want +got):\n%s", diff) + } + if diff := cmp.Diff(expectedProblems, problems); diff != "" { + t.Errorf("DeleteIngress() returned unexpected result (-want +got):\n%s", diff) + } +} + +func createTestIngressMaster(name string, host string) *networking.Ingress { + ing := createTestIngress(name, host) + ing.Annotations["nginx.org/mergeable-ingress-type"] = "master" + return ing +} + +func createTestIngressMinion(name string, host string, path string) *networking.Ingress { + ing := createTestIngress(name, host) + ing.Spec.Rules[0].IngressRuleValue = networking.IngressRuleValue{ + HTTP: &networking.HTTPIngressRuleValue{ + Paths: []networking.HTTPIngressPath{ + { + Path: path, + }, + }, + }, + } + + ing.Annotations["nginx.org/mergeable-ingress-type"] = "minion" + + return ing +} + +func createTestIngress(name string, hosts ...string) *networking.Ingress { + var rules []networking.IngressRule + + for _, h := range hosts { + rules = append(rules, networking.IngressRule{ + Host: h, + IngressRuleValue: networking.IngressRuleValue{}, + }) + } + + return &networking.Ingress{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: "default", + CreationTimestamp: metav1.Now(), + Annotations: map[string]string{ + "kubernetes.io/ingress.class": "nginx", + }, + }, + Spec: networking.IngressSpec{ + Rules: rules, + }, + } +} + +func createTestVirtualServer(name string, host string) *conf_v1.VirtualServer { + return &conf_v1.VirtualServer{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "default", + Name: name, + CreationTimestamp: metav1.Now(), + }, + Spec: conf_v1.VirtualServerSpec{ + IngressClass: "nginx", + Host: host, + }, + } +} + +func createTestVirtualServerWithRoutes(name string, host string, routes []conf_v1.Route) *conf_v1.VirtualServer { + vs := createTestVirtualServer(name, host) + vs.Spec.Routes = routes + return vs +} + +func createTestVirtualServerRoute(name string, host string, path string) *conf_v1.VirtualServerRoute { + return &conf_v1.VirtualServerRoute{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "default", + Name: name, + }, + Spec: conf_v1.VirtualServerRouteSpec{ + IngressClass: "nginx", + Host: host, + Subroutes: []conf_v1.Route{ + { + Path: path, + Action: &conf_v1.Action{ + Return: &conf_v1.ActionReturn{ + Body: "vsr", + }, + }, + }, + }, + }, + } +} + +func TestChooseObjectMetaWinner(t *testing.T) { + now := metav1.Now() + afterNow := metav1.NewTime(now.Add(1 * time.Second)) + + tests := []struct { + meta1 *metav1.ObjectMeta + meta2 *metav1.ObjectMeta + msg string + expected bool + }{ + { + meta1: &metav1.ObjectMeta{ + UID: "a", + CreationTimestamp: now, + }, + meta2: &metav1.ObjectMeta{ + UID: "b", + CreationTimestamp: afterNow, + }, + msg: "first is older", + expected: true, + }, + { + meta1: &metav1.ObjectMeta{ + UID: "a", + CreationTimestamp: afterNow, + }, + meta2: &metav1.ObjectMeta{ + UID: "b", + CreationTimestamp: now, + }, + msg: "second is older", + expected: false, + }, + { + meta1: &metav1.ObjectMeta{ + UID: "a", + CreationTimestamp: now, + }, + meta2: &metav1.ObjectMeta{ + UID: "b", + CreationTimestamp: now, + }, + msg: "both not older, but second wins", + expected: false, + }, + } + + for _, test := range tests { + result := chooseObjectMetaWinner(test.meta1, test.meta2) + if result != test.expected { + t.Errorf("chooseObjectMetaWinner() returned %v but expected %v for the case %s", result, test.expected, test.msg) + } + } +} + +func TestSquashResourceChanges(t *testing.T) { + fullIngress := &FullIngress{ + Ingress: createTestIngress("test", "foo.example.com"), + } + + fullVS := &FullVirtualServer{ + VirtualServer: createTestVirtualServer("test", "bar.example.com"), + } + + tests := []struct { + changes []ResourceChange + expected []ResourceChange + msg string + }{ + { + changes: []ResourceChange{ + { + Op: Delete, + Resource: fullIngress, + }, + { + Op: Delete, + Resource: fullIngress, + }, + }, + expected: []ResourceChange{ + { + Op: Delete, + Resource: fullIngress, + }, + }, + msg: "squash deletes", + }, + { + changes: []ResourceChange{ + { + Op: AddOrUpdate, + Resource: fullIngress, + }, + { + Op: AddOrUpdate, + Resource: fullIngress, + }, + }, + expected: []ResourceChange{ + { + Op: AddOrUpdate, + Resource: fullIngress, + }, + }, + msg: "squash updates", + }, + { + changes: []ResourceChange{ + { + Op: Delete, + Resource: fullIngress, + }, + { + Op: AddOrUpdate, + Resource: fullIngress, + }, + }, + expected: []ResourceChange{ + { + Op: AddOrUpdate, + Resource: fullIngress, + }, + }, + msg: "squash update and delete", + }, + { + changes: []ResourceChange{ + { + Op: Delete, + Resource: fullVS, + }, + { + Op: AddOrUpdate, + Resource: fullIngress, + }, + }, + expected: []ResourceChange{ + { + Op: Delete, + Resource: fullVS, + }, + { + Op: AddOrUpdate, + Resource: fullIngress, + }, + }, + msg: "preserve the order", + }, + { + changes: []ResourceChange{ + { + Op: Delete, + Resource: fullIngress, + }, + { + Op: AddOrUpdate, + Resource: fullVS, + }, + }, + expected: []ResourceChange{ + { + Op: Delete, + Resource: fullIngress, + }, + { + Op: AddOrUpdate, + Resource: fullVS, + }, + }, + msg: "do not squash different resource with same ns/name", + }, + { + changes: []ResourceChange{ + { + Op: Delete, + Resource: fullIngress, + }, + { + Op: AddOrUpdate, + Resource: fullIngress, + }, + { + Op: Delete, + Resource: fullVS, + }, + }, + expected: []ResourceChange{ + { + Op: Delete, + Resource: fullVS, + }, + { + Op: AddOrUpdate, + Resource: fullIngress, + }, + }, + msg: "squashed delete and update must follow delete", + }, + } + + for _, test := range tests { + result := squashResourceChanges(test.changes) + if diff := cmp.Diff(test.expected, result); diff != "" { + t.Errorf("squashResourceChanges() returned unexpected result for the case of %s (-want +got):\n%s", test.msg, diff) + } + } +} + +type testReferenceChecker struct { + resourceName string + resourceNamespace string + onlyIngresses bool + onlyMinions bool + onlyVirtualServers bool + onlyVirtualServerRoutes bool +} + +func (rc *testReferenceChecker) IsReferencedByIngress(namespace string, name string, ing *networking.Ingress) bool { + return rc.onlyIngresses && namespace == rc.resourceNamespace && name == rc.resourceName +} + +func (rc *testReferenceChecker) IsReferencedByMinion(namespace string, name string, ing *networking.Ingress) bool { + return rc.onlyMinions && namespace == rc.resourceNamespace && name == rc.resourceName +} + +func (rc *testReferenceChecker) IsReferencedByVirtualServer(namespace string, name string, vs *conf_v1.VirtualServer) bool { + return rc.onlyVirtualServers && namespace == rc.resourceNamespace && name == rc.resourceName +} + +func (rc *testReferenceChecker) IsReferencedByVirtualServerRoute(namespace string, name string, vsr *conf_v1.VirtualServerRoute) bool { + return rc.onlyVirtualServerRoutes && namespace == rc.resourceNamespace && name == rc.resourceName +} + +func TestFindResourcesForResourceReference(t *testing.T) { + regularIng := createTestIngress("regular-ingress", "foo.example.com") + master := createTestIngressMaster("master-ingress", "bar.example.com") + minion := createTestIngressMinion("minion-ingress", "bar.example.com", "/") + vs := createTestVirtualServer("virtualserver-1", "qwe.example.com") + vsWithVSR := createTestVirtualServerWithRoutes( + "virtualserver-2", + "asd.example.com", + []conf_v1.Route{ + { + Path: "/", + Route: "virtualserverroute", + }, + }) + vsr := createTestVirtualServerRoute("virtualserverroute", "asd.example.com", "/") + + configuration := createTestConfiguration() + + configuration.AddOrUpdateIngress(regularIng) + configuration.AddOrUpdateIngress(master) + configuration.AddOrUpdateIngress(minion) + configuration.AddOrUpdateVirtualServer(vs) + configuration.AddOrUpdateVirtualServer(vsWithVSR) + configuration.AddOrUpdateVirtualServerRoute(vsr) + + tests := []struct { + rc resourceReferenceChecker + expected []Resource + msg string + }{ + { + rc: &testReferenceChecker{ + resourceNamespace: "default", + resourceName: "test", + onlyIngresses: true, + }, + expected: []Resource{ + configuration.hosts["bar.example.com"], + configuration.hosts["foo.example.com"], + }, + msg: "only Ingresses", + }, + { + rc: &testReferenceChecker{ + resourceNamespace: "default", + resourceName: "test", + onlyMinions: true, + }, + expected: []Resource{ + configuration.hosts["bar.example.com"], + }, + msg: "only Minions", + }, + { + rc: &testReferenceChecker{ + resourceNamespace: "default", + resourceName: "test", + onlyVirtualServers: true, + }, + expected: []Resource{ + configuration.hosts["asd.example.com"], + configuration.hosts["qwe.example.com"], + }, + msg: "only VirtualServers", + }, + { + rc: &testReferenceChecker{ + resourceNamespace: "default", + resourceName: "test", + onlyVirtualServerRoutes: true, + }, + expected: []Resource{ + configuration.hosts["asd.example.com"], + }, + msg: "only VirtualServerRoutes", + }, + } + + for _, test := range tests { + result := configuration.findResourcesForResourceReference("default", "test", test.rc) + if diff := cmp.Diff(test.expected, result); diff != "" { + t.Errorf("findResourcesForResourceReference() returned unexpected result for the case of %s (-want +got):\n%s", test.msg, diff) + } + + var noResources []Resource + + result = configuration.findResourcesForResourceReference("default", "wrong", test.rc) + if diff := cmp.Diff(noResources, result); diff != "" { + t.Errorf("findResourcesForResourceReference() returned unexpected result for the case of %s and wrong name (-want +got):\n%s", test.msg, diff) + } + + result = configuration.findResourcesForResourceReference("wrong", "test", test.rc) + if diff := cmp.Diff(noResources, result); diff != "" { + t.Errorf("findResourcesForResourceReference() returned unexpected result for the case of %s and wrong namespace (-want +got):\n%s", test.msg, diff) + } + } +} + +func TestGetResources(t *testing.T) { + ing := createTestIngress("ingress", "foo.example.com", "bar.example.com") + vs := createTestVirtualServer("virtualserver", "qwe.example.com") + + configuration := createTestConfiguration() + configuration.AddOrUpdateIngress(ing) + configuration.AddOrUpdateVirtualServer(vs) + + expected := []Resource{ + configuration.hosts["foo.example.com"], + configuration.hosts["qwe.example.com"], + } + + result := configuration.GetResources() + if diff := cmp.Diff(expected, result); diff != "" { + t.Errorf("GetResources() returned unexpected result (-want +got):\n%s", diff) + } + + expected = []Resource{ + configuration.hosts["foo.example.com"], + } + + result = configuration.GetResourcesWithFilter(resourceFilter{Ingresses: true}) + if diff := cmp.Diff(expected, result); diff != "" { + t.Errorf("GetResources() returned unexpected result (-want +got):\n%s", diff) + } + + expected = []Resource{ + configuration.hosts["qwe.example.com"], + } + + result = configuration.GetResourcesWithFilter(resourceFilter{VirtualServers: true}) + if diff := cmp.Diff(expected, result); diff != "" { + t.Errorf("GetResources() returned unexpected result (-want +got):\n%s", diff) + } +} + +func TestIsEqualForFullIngresses(t *testing.T) { + regularIng := createTestIngress("regular-ingress", "foo.example.com") + + fullIngWithInvalidHost := NewRegularFullIngress(regularIng) + fullIngWithInvalidHost.ValidHosts["foo.example.com"] = false + + fullIngWithUpdatedIsMaster := NewRegularFullIngress(regularIng) + fullIngWithUpdatedIsMaster.IsMaster = true + + regularIngWithUpdatedGen := regularIng.DeepCopy() + regularIngWithUpdatedGen.Generation++ + + regularIngWithUpdatedAnnotations := regularIng.DeepCopy() + regularIngWithUpdatedAnnotations.Annotations["new"] = "value" + + masterIng := createTestIngressMaster("master-ingress", "bar.example.com") + minionIng := createTestIngressMinion("minion-ingress", "bar.example.com", "/") + + minionIngWithUpdatedGen := minionIng.DeepCopy() + minionIngWithUpdatedGen.Generation++ + + minionIngWithUpdatedAnnotations := minionIng.DeepCopy() + minionIngWithUpdatedAnnotations.Annotations["new"] = "value" + + tests := []struct { + fullIng1 *FullIngress + fullIng2 *FullIngress + expected bool + msg string + }{ + { + fullIng1: NewRegularFullIngress(regularIng), + fullIng2: NewRegularFullIngress(regularIng), + expected: true, + msg: "equal regular ingresses", + }, + { + fullIng1: NewRegularFullIngress(regularIng), + fullIng2: fullIngWithInvalidHost, + expected: false, + msg: "regular ingresses with different valid hosts", + }, + { + fullIng1: NewRegularFullIngress(regularIng), + fullIng2: fullIngWithUpdatedIsMaster, + expected: false, + msg: "regular ingresses with different IsMaster value", + }, + { + fullIng1: NewRegularFullIngress(regularIng), + fullIng2: NewRegularFullIngress(regularIngWithUpdatedGen), + expected: false, + msg: "regular ingresses with different generation", + }, + { + fullIng1: NewRegularFullIngress(regularIng), + fullIng2: NewRegularFullIngress(regularIngWithUpdatedAnnotations), + expected: false, + msg: "regular ingresses with different annotations", + }, + { + fullIng1: NewMasterFullIngress(masterIng, []*FullMinion{NewFullMinion(minionIng)}, map[string][]string{}), + fullIng2: NewMasterFullIngress(masterIng, []*FullMinion{NewFullMinion(minionIng)}, map[string][]string{}), + expected: true, + msg: "equal master ingresses", + }, + { + fullIng1: NewMasterFullIngress(masterIng, []*FullMinion{NewFullMinion(minionIng)}, map[string][]string{}), + fullIng2: NewMasterFullIngress(masterIng, []*FullMinion{}, map[string][]string{}), + expected: false, + msg: "masters with different number of minions", + }, + { + fullIng1: NewMasterFullIngress(masterIng, []*FullMinion{NewFullMinion(minionIng)}, map[string][]string{}), + fullIng2: NewMasterFullIngress(masterIng, []*FullMinion{NewFullMinion(minionIngWithUpdatedGen)}, map[string][]string{}), + expected: false, + msg: "masters with minions with different generation", + }, + { + fullIng1: NewMasterFullIngress(masterIng, []*FullMinion{NewFullMinion(minionIng)}, map[string][]string{}), + fullIng2: NewMasterFullIngress(masterIng, []*FullMinion{NewFullMinion(minionIngWithUpdatedAnnotations)}, map[string][]string{}), + expected: false, + msg: "masters with minions with different annotations", + }, + } + + for _, test := range tests { + result := test.fullIng1.IsEqual(test.fullIng2) + if result != test.expected { + t.Errorf("IsEqual() returned %v but expected %v for the case of %s", result, test.expected, test.msg) + } + } +} + +func TestIsEqualForVirtualServers(t *testing.T) { + vs := createTestVirtualServerWithRoutes( + "virtualserver", + "foo.example.com", + []conf_v1.Route{ + { + Path: "/", + Route: "virtualserverroute", + }, + }) + vsr := createTestVirtualServerRoute("virtualserverroute", "foo.example.com", "/") + + vsWithUpdatedGen := vs.DeepCopy() + vsWithUpdatedGen.Generation++ + + vsrWithUpdatedGen := vsr.DeepCopy() + vsrWithUpdatedGen.Generation++ + + tests := []struct { + fullVS1 *FullVirtualServer + fullVS2 *FullVirtualServer + expected bool + msg string + }{ + { + fullVS1: NewFullVirtualServer(vs, []*conf_v1.VirtualServerRoute{vsr}, []string{}), + fullVS2: NewFullVirtualServer(vs, []*conf_v1.VirtualServerRoute{vsr}, []string{}), + expected: true, + msg: "equal virtual servers", + }, + { + fullVS1: NewFullVirtualServer(vs, []*conf_v1.VirtualServerRoute{vsr}, []string{}), + fullVS2: NewFullVirtualServer(vsWithUpdatedGen, []*conf_v1.VirtualServerRoute{vsr}, []string{}), + expected: false, + msg: "virtual servers with different generation", + }, + { + fullVS1: NewFullVirtualServer(vs, []*conf_v1.VirtualServerRoute{vsr}, []string{}), + fullVS2: NewFullVirtualServer(vs, []*conf_v1.VirtualServerRoute{}, []string{}), + expected: false, + msg: "virtual servers with different number of virtual server routes", + }, + { + fullVS1: NewFullVirtualServer(vs, []*conf_v1.VirtualServerRoute{vsr}, []string{}), + fullVS2: NewFullVirtualServer(vs, []*conf_v1.VirtualServerRoute{vsrWithUpdatedGen}, []string{}), + expected: false, + msg: "virtual servers with virtual server routes with different generation", + }, + } + + for _, test := range tests { + result := test.fullVS1.IsEqual(test.fullVS2) + if result != test.expected { + t.Errorf("IsEqual() returned %v but expected %v for the case of %s", result, test.expected, test.msg) + } + } +} + +func TestIsEqualForDifferentResources(t *testing.T) { + fullIng := NewRegularFullIngress(createTestIngress("ingress", "foo.example.com")) + fullVS := NewFullVirtualServer(createTestVirtualServer("virtualserver", "bar.example.com"), []*conf_v1.VirtualServerRoute{}, []string{}) + + result := fullIng.IsEqual(fullVS) + if result != false { + t.Error("IsEqual() for different resources returned true but expected false") + } +} + +func TestCompareConfigurationProblems(t *testing.T) { + tests := []struct { + problem1 *ConfigurationProblem + problem2 *ConfigurationProblem + expected bool + msg string + }{ + { + problem1: &ConfigurationProblem{ + IsError: false, + Reason: "reason", + Message: "message", + }, + problem2: &ConfigurationProblem{ + IsError: false, + Reason: "reason", + Message: "message", + }, + expected: true, + msg: "equal problems", + }, + { + problem1: &ConfigurationProblem{ + Object: createTestIngress("ingress-1", "foo.example.com"), + IsError: false, + Reason: "reason", + Message: "message", + }, + problem2: &ConfigurationProblem{ + Object: createTestIngress("ingress-2", "bar.example.com"), + IsError: false, + Reason: "reason", + Message: "message", + }, + expected: true, + msg: "equal problems although objects are different", + }, + { + problem1: &ConfigurationProblem{ + IsError: true, + Reason: "reason", + Message: "message", + }, + problem2: &ConfigurationProblem{ + IsError: false, + Reason: "reason", + Message: "message", + }, + expected: false, + msg: "different isError", + }, + { + problem1: &ConfigurationProblem{ + IsError: false, + Reason: "reason", + Message: "message", + }, + problem2: &ConfigurationProblem{ + IsError: false, + Reason: "another reason", + Message: "message", + }, + expected: false, + msg: "different Reason", + }, + { + problem1: &ConfigurationProblem{ + IsError: false, + Reason: "reason", + Message: "message", + }, + problem2: &ConfigurationProblem{ + IsError: false, + Reason: "reason", + Message: "another message", + }, + expected: false, + msg: "different Message", + }, + } + + for _, test := range tests { + result := compareConfigurationProblems(test.problem1, test.problem2) + if result != test.expected { + t.Errorf("compareConfigurationProblems() returned %v but expected %v for the case of %s", result, test.expected, test.msg) + } + } +} diff --git a/internal/k8s/reference_checkers.go b/internal/k8s/reference_checkers.go new file mode 100644 index 0000000000..0f17eaae0e --- /dev/null +++ b/internal/k8s/reference_checkers.go @@ -0,0 +1,209 @@ +package k8s + +import ( + "github.com/nginxinc/kubernetes-ingress/internal/configs" + "github.com/nginxinc/kubernetes-ingress/pkg/apis/configuration/v1" + networking "k8s.io/api/networking/v1beta1" +) + +type resourceReferenceChecker interface { + IsReferencedByIngress(namespace string, name string, ing *networking.Ingress) bool + IsReferencedByMinion(namespace string, name string, ing *networking.Ingress) bool + IsReferencedByVirtualServer(namespace string, name string, vs *v1.VirtualServer) bool + IsReferencedByVirtualServerRoute(namespace string, name string, vsr *v1.VirtualServerRoute) bool +} + +type secretReferenceChecker struct { + isPlus bool +} + +func newSecretReferenceChecker(isPlus bool) *secretReferenceChecker { + return &secretReferenceChecker{isPlus} +} + +func (rc *secretReferenceChecker) IsReferencedByIngress(secretNamespace string, secretName string, ing *networking.Ingress) bool { + if ing.Namespace != secretNamespace { + return false + } + + for _, tls := range ing.Spec.TLS { + if tls.SecretName == secretName { + return true + } + } + + if rc.isPlus { + if jwtKey, exists := ing.Annotations[configs.JWTKeyAnnotation]; exists { + if jwtKey == secretName { + return true + } + } + } + + return false +} + +func (rc *secretReferenceChecker) IsReferencedByMinion(secretNamespace string, secretName string, ing *networking.Ingress) bool { + if ing.Namespace != secretNamespace { + return false + } + + if rc.isPlus { + if jwtKey, exists := ing.Annotations[configs.JWTKeyAnnotation]; exists { + if jwtKey == secretName { + return true + } + } + } + + return false +} + +func (rc *secretReferenceChecker) IsReferencedByVirtualServer(secretNamespace string, secretName string, vs *v1.VirtualServer) bool { + if vs.Namespace != secretNamespace { + return false + } + + if vs.Spec.TLS != nil && vs.Spec.TLS.Secret == secretName { + return true + } + + return false +} + +func (rc *secretReferenceChecker) IsReferencedByVirtualServerRoute(secretNamespace string, secretName string, vsr *v1.VirtualServerRoute) bool { + return false +} + +type serviceReferenceChecker struct{} + +func newServiceReferenceChecker() *serviceReferenceChecker { + return &serviceReferenceChecker{} +} + +func (rc *serviceReferenceChecker) IsReferencedByIngress(svcNamespace string, svcName string, ing *networking.Ingress) bool { + if ing.Namespace != svcNamespace { + return false + } + + if ing.Spec.Backend != nil { + if ing.Spec.Backend.ServiceName == svcName { + return true + } + } + for _, rules := range ing.Spec.Rules { + if rules.IngressRuleValue.HTTP == nil { + continue + } + for _, p := range rules.IngressRuleValue.HTTP.Paths { + if p.Backend.ServiceName == svcName { + return true + } + } + } + + return false +} + +func (rc *serviceReferenceChecker) IsReferencedByMinion(svcNamespace string, svcName string, ing *networking.Ingress) bool { + return rc.IsReferencedByIngress(svcNamespace, svcName, ing) +} + +func (rc *serviceReferenceChecker) IsReferencedByVirtualServer(svcNamespace string, svcName string, vs *v1.VirtualServer) bool { + if vs.Namespace != svcNamespace { + return false + } + + for _, u := range vs.Spec.Upstreams { + if u.Service == svcName { + return true + } + } + + return false +} + +func (rc *serviceReferenceChecker) IsReferencedByVirtualServerRoute(svcNamespace string, svcName string, vsr *v1.VirtualServerRoute) bool { + if vsr.Namespace != svcNamespace { + return false + } + + for _, u := range vsr.Spec.Upstreams { + if u.Service == svcName { + return true + } + } + + return false +} + +type policyReferenceChecker struct { +} + +func newPolicyReferenceChecker() *policyReferenceChecker { + return &policyReferenceChecker{} +} + +func (rc *policyReferenceChecker) IsReferencedByIngress(policyNamespace string, policyName string, ing *networking.Ingress) bool { + return false +} + +func (rc *policyReferenceChecker) IsReferencedByMinion(policyNamespace string, policyName string, ing *networking.Ingress) bool { + return false +} + +func (rc *policyReferenceChecker) IsReferencedByVirtualServer(policyNamespace string, policyName string, vs *v1.VirtualServer) bool { + if isPolicyReferenced(vs.Spec.Policies, vs.Namespace, policyNamespace, policyName) { + return true + } + + for _, r := range vs.Spec.Routes { + if isPolicyReferenced(r.Policies, vs.Namespace, policyNamespace, policyName) { + return true + } + } + + return false +} + +func (rc *policyReferenceChecker) IsReferencedByVirtualServerRoute(policyNamespace string, policyName string, vsr *v1.VirtualServerRoute) bool { + for _, r := range vsr.Spec.Subroutes { + if isPolicyReferenced(r.Policies, vsr.Namespace, policyNamespace, policyName) { + return true + } + } + + return false +} + +// appProtectResourceReferenceChecker is a reference checker for AppProtect related resources. +// Only Regular/Master Ingress can reference those resources. +type appProtectResourceReferenceChecker struct { + annotation string +} + +func newAppProtectResourceReferenceChecker(annotation string) *appProtectResourceReferenceChecker { + return &appProtectResourceReferenceChecker{annotation} +} + +func (rc *appProtectResourceReferenceChecker) IsReferencedByIngress(namespace string, name string, ing *networking.Ingress) bool { + if pol, exists := ing.Annotations[rc.annotation]; exists { + if pol == namespace+"/"+name || (namespace == ing.Namespace && pol == name) { + return true + } + } + + return false +} + +func (rc *appProtectResourceReferenceChecker) IsReferencedByMinion(namespace string, name string, ing *networking.Ingress) bool { + return false +} + +func (rc *appProtectResourceReferenceChecker) IsReferencedByVirtualServer(namespace string, name string, vs *v1.VirtualServer) bool { + return false +} + +func (rc *appProtectResourceReferenceChecker) IsReferencedByVirtualServerRoute(namespace string, name string, vsr *v1.VirtualServerRoute) bool { + return false +} diff --git a/internal/k8s/reference_checkers_test.go b/internal/k8s/reference_checkers_test.go new file mode 100644 index 0000000000..1b9525651a --- /dev/null +++ b/internal/k8s/reference_checkers_test.go @@ -0,0 +1,886 @@ +package k8s + +import ( + "testing" + + "github.com/nginxinc/kubernetes-ingress/internal/configs" + conf_v1 "github.com/nginxinc/kubernetes-ingress/pkg/apis/configuration/v1" + networking "k8s.io/api/networking/v1beta1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func TestSecretIsReferencedByIngress(t *testing.T) { + tests := []struct { + ing *networking.Ingress + secretNamespace string + secretName string + isPlus bool + expected bool + msg string + }{ + { + ing: &networking.Ingress{ + ObjectMeta: v1.ObjectMeta{ + Namespace: "default", + }, + Spec: networking.IngressSpec{ + TLS: []networking.IngressTLS{ + { + SecretName: "test-secret", + }, + }, + }, + }, + secretNamespace: "default", + secretName: "test-secret", + isPlus: false, + expected: true, + msg: "tls secret is referenced", + }, + { + ing: &networking.Ingress{ + ObjectMeta: v1.ObjectMeta{ + Namespace: "default", + }, + Spec: networking.IngressSpec{ + TLS: []networking.IngressTLS{ + { + SecretName: "test-secret", + }, + }, + }, + }, + secretNamespace: "default", + secretName: "some-secret", + isPlus: false, + expected: false, + msg: "wrong name for tls secret", + }, + { + ing: &networking.Ingress{ + ObjectMeta: v1.ObjectMeta{ + Namespace: "default", + }, + Spec: networking.IngressSpec{ + TLS: []networking.IngressTLS{ + { + SecretName: "test-secret", + }, + }, + }, + }, + secretNamespace: "some-namespace", + secretName: "test-secret", + isPlus: false, + expected: false, + msg: "wrong namespace for tls secret", + }, + { + ing: &networking.Ingress{ + ObjectMeta: v1.ObjectMeta{ + Namespace: "default", + Annotations: map[string]string{ + configs.JWTKeyAnnotation: "test-secret", + }, + }, + }, + secretNamespace: "default", + secretName: "test-secret", + isPlus: true, + expected: true, + msg: "jwt secret is referenced for Plus", + }, + { + ing: &networking.Ingress{ + ObjectMeta: v1.ObjectMeta{ + Namespace: "default", + Annotations: map[string]string{ + configs.JWTKeyAnnotation: "test-secret", + }, + }, + }, + secretNamespace: "default", + secretName: "some-secret", + isPlus: true, + expected: false, + msg: "wrong namespace for jwt secret for Plus", + }, + { + ing: &networking.Ingress{ + ObjectMeta: v1.ObjectMeta{ + Namespace: "default", + Annotations: map[string]string{ + configs.JWTKeyAnnotation: "test-secret", + }, + }, + }, + secretNamespace: "default", + secretName: "some-secret", + isPlus: true, + expected: false, + msg: "wrong name for jwt secret for Plus", + }, + { + ing: &networking.Ingress{ + ObjectMeta: v1.ObjectMeta{ + Namespace: "default", + Annotations: map[string]string{ + configs.JWTKeyAnnotation: "test-secret", + }, + }, + }, + secretNamespace: "some-namespace", + secretName: "test-secret", + isPlus: false, + expected: false, + msg: "jwt secret for NGINX OSS is ignored", + }, + } + + for _, test := range tests { + rc := newSecretReferenceChecker(test.isPlus) + + result := rc.IsReferencedByIngress(test.secretNamespace, test.secretName, test.ing) + if result != test.expected { + t.Errorf("IsReferencedByIngress() returned %v but expected %v for the case of %s", result, test.expected, test.msg) + } + } +} + +func TestSecretIsReferencedByMinion(t *testing.T) { + tests := []struct { + ing *networking.Ingress + secretNamespace string + secretName string + isPlus bool + expected bool + msg string + }{ + { + ing: &networking.Ingress{ + ObjectMeta: v1.ObjectMeta{ + Namespace: "default", + Annotations: map[string]string{ + configs.JWTKeyAnnotation: "test-secret", + }, + }, + }, + secretNamespace: "default", + secretName: "test-secret", + isPlus: true, + expected: true, + msg: "jwt secret is referenced for Plus", + }, + { + ing: &networking.Ingress{ + ObjectMeta: v1.ObjectMeta{ + Namespace: "default", + Annotations: map[string]string{ + configs.JWTKeyAnnotation: "test-secret", + }, + }, + }, + secretNamespace: "default", + secretName: "some-secret", + isPlus: true, + expected: false, + msg: "wrong namespace for jwt secret for Plus", + }, + { + ing: &networking.Ingress{ + ObjectMeta: v1.ObjectMeta{ + Namespace: "default", + Annotations: map[string]string{ + configs.JWTKeyAnnotation: "test-secret", + }, + }, + }, + secretNamespace: "default", + secretName: "some-secret", + isPlus: true, + expected: false, + msg: "wrong name for jwt secret for Plus", + }, + { + ing: &networking.Ingress{ + ObjectMeta: v1.ObjectMeta{ + Namespace: "default", + Annotations: map[string]string{ + configs.JWTKeyAnnotation: "test-secret", + }, + }, + }, + secretNamespace: "default", + secretName: "test-secret", + isPlus: false, + expected: false, + msg: "jwt secret for NGINX OSS is ignored", + }, + } + + for _, test := range tests { + rc := newSecretReferenceChecker(test.isPlus) + + result := rc.IsReferencedByMinion(test.secretNamespace, test.secretName, test.ing) + if result != test.expected { + t.Errorf("IsReferencedByMinion() returned %v but expected %v for the case of %s", result, test.expected, test.msg) + } + } +} + +func TestSecretIsReferencedByVirtualServer(t *testing.T) { + tests := []struct { + vs *conf_v1.VirtualServer + secretNamespace string + secretName string + expected bool + msg string + }{ + { + vs: &conf_v1.VirtualServer{ + ObjectMeta: v1.ObjectMeta{ + Namespace: "default", + }, + Spec: conf_v1.VirtualServerSpec{ + TLS: &conf_v1.TLS{ + Secret: "test-secret", + }, + }, + }, + secretNamespace: "default", + secretName: "test-secret", + expected: true, + msg: "tls secret is referenced", + }, + { + vs: &conf_v1.VirtualServer{ + ObjectMeta: v1.ObjectMeta{ + Namespace: "default", + }, + Spec: conf_v1.VirtualServerSpec{ + TLS: &conf_v1.TLS{ + Secret: "test-secret", + }, + }, + }, + secretNamespace: "default", + secretName: "some-secret", + expected: false, + msg: "wrong name for tls secret", + }, + { + vs: &conf_v1.VirtualServer{ + ObjectMeta: v1.ObjectMeta{ + Namespace: "default", + }, + Spec: conf_v1.VirtualServerSpec{ + TLS: &conf_v1.TLS{ + Secret: "test-secret", + }, + }, + }, + secretNamespace: "some-namespace", + secretName: "test-secret", + expected: false, + msg: "wrong namespace for tls secret", + }, + } + + for _, test := range tests { + isPlus := false // doesn't matter for VirtualServer + rc := newSecretReferenceChecker(isPlus) + + result := rc.IsReferencedByVirtualServer(test.secretNamespace, test.secretName, test.vs) + if result != test.expected { + t.Errorf("IsReferencedByVirtualServer() returned %v but expected %v for the case of %s", result, test.expected, test.msg) + } + } +} + +func TestSecretIsReferencedByVirtualServerRoute(t *testing.T) { + isPlus := false // doesn't matter for VirtualServerRoute + rc := newSecretReferenceChecker(isPlus) + + // always returns false + result := rc.IsReferencedByVirtualServerRoute("", "", nil) + if result != false { + t.Error("IsReferencedByVirtualServer() returned true but expected false") + } +} + +func TestServiceIsReferencedByIngressAndMinion(t *testing.T) { + tests := []struct { + ing *networking.Ingress + serviceNamespace string + serviceName string + expected bool + msg string + }{ + { + ing: &networking.Ingress{ + ObjectMeta: v1.ObjectMeta{ + Namespace: "default", + }, + Spec: networking.IngressSpec{ + Backend: &networking.IngressBackend{ + ServiceName: "test-service", + }, + }, + }, + serviceNamespace: "default", + serviceName: "test-service", + expected: true, + msg: "service is referenced in the default backend", + }, + { + ing: &networking.Ingress{ + ObjectMeta: v1.ObjectMeta{ + Namespace: "default", + }, + Spec: networking.IngressSpec{ + Rules: []networking.IngressRule{ + { + IngressRuleValue: networking.IngressRuleValue{ + HTTP: &networking.HTTPIngressRuleValue{ + Paths: []networking.HTTPIngressPath{ + { + Backend: networking.IngressBackend{ + ServiceName: "test-service", + }, + }, + }, + }, + }, + }, + }, + }, + }, + serviceNamespace: "default", + serviceName: "test-service", + expected: true, + msg: "service is referenced in a path", + }, + { + ing: &networking.Ingress{ + ObjectMeta: v1.ObjectMeta{ + Namespace: "default", + }, + Spec: networking.IngressSpec{ + Rules: []networking.IngressRule{ + { + IngressRuleValue: networking.IngressRuleValue{ + HTTP: &networking.HTTPIngressRuleValue{ + Paths: []networking.HTTPIngressPath{ + { + Backend: networking.IngressBackend{ + ServiceName: "test-service", + }, + }, + }, + }, + }, + }, + }, + }, + }, + serviceNamespace: "default", + serviceName: "some-service", + expected: false, + msg: "wrong name for service in a path", + }, + { + ing: &networking.Ingress{ + ObjectMeta: v1.ObjectMeta{ + Namespace: "default", + }, + Spec: networking.IngressSpec{ + Rules: []networking.IngressRule{ + { + IngressRuleValue: networking.IngressRuleValue{ + HTTP: &networking.HTTPIngressRuleValue{ + Paths: []networking.HTTPIngressPath{ + { + Backend: networking.IngressBackend{ + ServiceName: "test-service", + }, + }, + }, + }, + }, + }, + }, + }, + }, + serviceNamespace: "some-namespace", + serviceName: "test-service", + expected: false, + msg: "wrong namespace for service in a path", + }, + } + + for _, test := range tests { + rc := newServiceReferenceChecker() + + result := rc.IsReferencedByIngress(test.serviceNamespace, test.serviceName, test.ing) + if result != test.expected { + t.Errorf("IsReferencedByIngress() returned %v but expected %v for the case of %s", result, test.expected, test.msg) + } + + // same cases for Minions + result = rc.IsReferencedByMinion(test.serviceNamespace, test.serviceName, test.ing) + if result != test.expected { + t.Errorf("IsReferencedByMinion() returned %v but expected %v for the case of %s", result, test.expected, test.msg) + } + } +} + +func TestServiceIsReferencedByVirtualServerAndVirtualServerRoutes(t *testing.T) { + tests := []struct { + vs *conf_v1.VirtualServer + vsr *conf_v1.VirtualServerRoute + serviceNamespace string + serviceName string + expected bool + msg string + }{ + { + vs: &conf_v1.VirtualServer{ + ObjectMeta: v1.ObjectMeta{ + Namespace: "default", + }, + Spec: conf_v1.VirtualServerSpec{ + Upstreams: []conf_v1.Upstream{ + { + Service: "test-service", + }, + }, + }, + }, + vsr: &conf_v1.VirtualServerRoute{ + ObjectMeta: v1.ObjectMeta{ + Namespace: "default", + }, + Spec: conf_v1.VirtualServerRouteSpec{ + Upstreams: []conf_v1.Upstream{ + { + Service: "test-service", + }, + }, + }, + }, + serviceNamespace: "default", + serviceName: "test-service", + expected: true, + msg: "service is referenced in an upstream", + }, + { + vs: &conf_v1.VirtualServer{ + ObjectMeta: v1.ObjectMeta{ + Namespace: "default", + }, + Spec: conf_v1.VirtualServerSpec{ + Upstreams: []conf_v1.Upstream{ + { + Service: "test-service", + }, + }, + }, + }, + vsr: &conf_v1.VirtualServerRoute{ + ObjectMeta: v1.ObjectMeta{ + Namespace: "default", + }, + Spec: conf_v1.VirtualServerRouteSpec{ + Upstreams: []conf_v1.Upstream{ + { + Service: "test-service", + }, + }, + }, + }, + serviceNamespace: "some-namespace", + serviceName: "test-service", + expected: false, + msg: "wrong namespace for service in an upstream", + }, + { + vs: &conf_v1.VirtualServer{ + ObjectMeta: v1.ObjectMeta{ + Namespace: "default", + }, + Spec: conf_v1.VirtualServerSpec{ + Upstreams: []conf_v1.Upstream{ + { + Service: "test-service", + }, + }, + }, + }, + vsr: &conf_v1.VirtualServerRoute{ + ObjectMeta: v1.ObjectMeta{ + Namespace: "default", + }, + Spec: conf_v1.VirtualServerRouteSpec{ + Upstreams: []conf_v1.Upstream{ + { + Service: "test-service", + }, + }, + }, + }, + serviceNamespace: "default", + serviceName: "some-service", + expected: false, + msg: "wrong name for service in an upstream", + }, + } + + for _, test := range tests { + rc := newServiceReferenceChecker() + + result := rc.IsReferencedByVirtualServer(test.serviceNamespace, test.serviceName, test.vs) + if result != test.expected { + t.Errorf("IsReferencedByVirtualServer() returned %v but expected %v for the case of %s", result, test.expected, test.msg) + } + + result = rc.IsReferencedByVirtualServerRoute(test.serviceNamespace, test.serviceName, test.vsr) + if result != test.expected { + t.Errorf("IsReferencedByVirtualServerRoute() returned %v but expected %v for the case of %s", result, test.expected, test.msg) + } + } +} + +func TestPolicyIsReferencedByIngresses(t *testing.T) { + rc := newPolicyReferenceChecker() + + // always returns false + result := rc.IsReferencedByIngress("", "", nil) + if result != false { + t.Error("IsReferencedByIngress() returned true but expected false") + } + + // always returns false + result = rc.IsReferencedByMinion("", "", nil) + if result != false { + t.Error("IsReferencedByMinion() returned true but expected false") + } +} + +func TestPolicyIsReferencedByVirtualServerAndVirtualServerRoute(t *testing.T) { + tests := []struct { + vs *conf_v1.VirtualServer + vsr *conf_v1.VirtualServerRoute + policyNamespace string + policyName string + expected bool + msg string + }{ + { + vs: &conf_v1.VirtualServer{ + ObjectMeta: v1.ObjectMeta{ + Namespace: "default", + }, + Spec: conf_v1.VirtualServerSpec{ + Policies: []conf_v1.PolicyReference{ + { + Name: "test-policy", + Namespace: "default", + }, + }, + }, + }, + policyNamespace: "default", + policyName: "test-policy", + expected: true, + msg: "policy is referenced at the spec level", + }, + { + vs: &conf_v1.VirtualServer{ + ObjectMeta: v1.ObjectMeta{ + Namespace: "default", + }, + Spec: conf_v1.VirtualServerSpec{ + Policies: []conf_v1.PolicyReference{ + { + Name: "test-policy", + Namespace: "default", + }, + }, + }, + }, + policyNamespace: "some-namespace", + policyName: "test-policy", + expected: false, + msg: "wrong namespace for policy at the spec level", + }, + { + vs: &conf_v1.VirtualServer{ + ObjectMeta: v1.ObjectMeta{ + Namespace: "default", + }, + Spec: conf_v1.VirtualServerSpec{ + Policies: []conf_v1.PolicyReference{ + { + Name: "test-policy", + Namespace: "default", + }, + }, + }, + }, + policyNamespace: "default", + policyName: "some-policy", + expected: false, + msg: "wrong name for policy at the spec level", + }, + { + vs: &conf_v1.VirtualServer{ + ObjectMeta: v1.ObjectMeta{ + Namespace: "default", + }, + Spec: conf_v1.VirtualServerSpec{ + Routes: []conf_v1.Route{ + { + Policies: []conf_v1.PolicyReference{ + { + Name: "test-policy", + Namespace: "default", + }, + }, + }, + }, + }, + }, + vsr: &conf_v1.VirtualServerRoute{ + ObjectMeta: v1.ObjectMeta{ + Namespace: "default", + }, + Spec: conf_v1.VirtualServerRouteSpec{ + Subroutes: []conf_v1.Route{ + { + Policies: []conf_v1.PolicyReference{ + { + Name: "test-policy", + Namespace: "default", + }, + }, + }, + }, + }, + }, + policyNamespace: "default", + policyName: "test-policy", + expected: true, + msg: "policy is referenced in a route", + }, + { + vs: &conf_v1.VirtualServer{ + ObjectMeta: v1.ObjectMeta{ + Namespace: "default", + }, + Spec: conf_v1.VirtualServerSpec{ + Routes: []conf_v1.Route{ + { + Policies: []conf_v1.PolicyReference{ + { + Name: "test-policy", + Namespace: "default", + }, + }, + }, + }, + }, + }, + vsr: &conf_v1.VirtualServerRoute{ + ObjectMeta: v1.ObjectMeta{ + Namespace: "default", + }, + Spec: conf_v1.VirtualServerRouteSpec{ + Subroutes: []conf_v1.Route{ + { + Policies: []conf_v1.PolicyReference{ + { + Name: "test-policy", + Namespace: "default", + }, + }, + }, + }, + }, + }, + policyNamespace: "some-namespace", + policyName: "test-policy", + expected: false, + msg: "wrong namespace for policy in a route", + }, + { + vs: &conf_v1.VirtualServer{ + ObjectMeta: v1.ObjectMeta{ + Namespace: "default", + }, + Spec: conf_v1.VirtualServerSpec{ + Routes: []conf_v1.Route{ + { + Policies: []conf_v1.PolicyReference{ + { + Name: "test-policy", + Namespace: "default", + }, + }, + }, + }, + }, + }, + vsr: &conf_v1.VirtualServerRoute{ + ObjectMeta: v1.ObjectMeta{ + Namespace: "default", + }, + Spec: conf_v1.VirtualServerRouteSpec{ + Subroutes: []conf_v1.Route{ + { + Policies: []conf_v1.PolicyReference{ + { + Name: "test-policy", + Namespace: "default", + }, + }, + }, + }, + }, + }, + policyNamespace: "default", + policyName: "some-policy", + expected: false, + msg: "wrong name for policy in a route", + }, + } + + for _, test := range tests { + rc := newPolicyReferenceChecker() + + result := rc.IsReferencedByVirtualServer(test.policyNamespace, test.policyName, test.vs) + if result != test.expected { + t.Errorf("IsReferencedByVirtualServer() returned %v but expected %v for the case of %s", result, test.expected, test.msg) + } + + if test.vsr == nil { + continue + } + + result = rc.IsReferencedByVirtualServerRoute(test.policyNamespace, test.policyName, test.vsr) + if result != test.expected { + t.Errorf("IsReferencedByVirtualServerRoute() returned %v but expected %v for the case of %s", result, test.expected, test.msg) + } + } +} + +func TestAppProtectResourceIsReferencedByIngresses(t *testing.T) { + tests := []struct { + ing *networking.Ingress + resourceNamespace string + resourceName string + expected bool + msg string + }{ + { + ing: &networking.Ingress{ + ObjectMeta: v1.ObjectMeta{ + Namespace: "default", + Annotations: map[string]string{ + "test-annotation": "default/test-resource", + }, + }, + }, + resourceNamespace: "default", + resourceName: "test-resource", + expected: true, + msg: "resource is referenced", + }, + { + ing: &networking.Ingress{ + ObjectMeta: v1.ObjectMeta{ + Namespace: "default", + Annotations: map[string]string{ + "test-annotation": "test-resource", + }, + }, + }, + resourceNamespace: "default", + resourceName: "test-resource", + expected: true, + msg: "resource is referenced with implicit namespace", + }, + { + ing: &networking.Ingress{ + ObjectMeta: v1.ObjectMeta{ + Namespace: "default", + Annotations: map[string]string{ + "test-annotation": "default/test-resource", + }, + }, + }, + resourceNamespace: "default", + resourceName: "some-resource", + expected: false, + msg: "wrong name", + }, + { + ing: &networking.Ingress{ + ObjectMeta: v1.ObjectMeta{ + Namespace: "default", + Annotations: map[string]string{ + "test-annotation": "default/test-resource", + }, + }, + }, + resourceNamespace: "some-namespace", + resourceName: "test-resource", + expected: false, + msg: "wrong namespace", + }, + { + ing: &networking.Ingress{ + ObjectMeta: v1.ObjectMeta{ + Namespace: "default", + Annotations: map[string]string{ + "test-annotation": "test-resource", + }, + }, + }, + resourceNamespace: "some-namespace", + resourceName: "test-resource", + expected: false, + msg: "wrong namespace with implicit namespace", + }, + } + + for _, test := range tests { + rc := newAppProtectResourceReferenceChecker("test-annotation") + + result := rc.IsReferencedByIngress(test.resourceNamespace, test.resourceName, test.ing) + if result != test.expected { + t.Errorf("IsReferencedByIngress() returned %v but expected %v for the case of %s", result, test.expected, test.msg) + } + + // always false for minion + result = rc.IsReferencedByMinion(test.resourceNamespace, test.resourceName, test.ing) + if result != false { + t.Errorf("IsReferencedByMinion() returned true but expected false for the case of %s", test.msg) + } + } +} + +func TestAppProtectResourceIsReferenced(t *testing.T) { + rc := newAppProtectResourceReferenceChecker("test") + + // always returns false + result := rc.IsReferencedByVirtualServer("", "", nil) + if result != false { + t.Error("IsReferencedByVirtualServer() returned true but expected false") + } + + // always returns false + result = rc.IsReferencedByVirtualServerRoute("", "", nil) + if result != false { + t.Error("IsReferencedByVirtualServer() returned true but expected false") + } +} diff --git a/internal/k8s/validation.go b/internal/k8s/validation.go new file mode 100644 index 0000000000..1b5a7358a8 --- /dev/null +++ b/internal/k8s/validation.go @@ -0,0 +1,111 @@ +package k8s + +import ( + networking "k8s.io/api/networking/v1beta1" + "k8s.io/apimachinery/pkg/util/sets" + "k8s.io/apimachinery/pkg/util/validation/field" +) + +const mergeableIngressTypeAnnotationKey = "nginx.org/mergeable-ingress-type" + +// validateIngress validate an Ingress resource with rules that our Ingress Controller enforces. +// Note that the full validation of Ingress resources is done by Kubernetes. +func validateIngress(ing *networking.Ingress) field.ErrorList { + allErrs := field.ErrorList{} + + allErrs = append(allErrs, validateIngressAnnotations(ing.Annotations, field.NewPath("annotations"))...) + + allErrs = append(allErrs, validateIngressSpec(&ing.Spec, field.NewPath("spec"))...) + + if isMaster(ing) { + allErrs = append(allErrs, validateMasterSpec(&ing.Spec, field.NewPath("spec"))...) + } else if isMinion(ing) { + allErrs = append(allErrs, validateMinionSpec(&ing.Spec, field.NewPath("spec"))...) + } + + return allErrs +} + +func validateIngressAnnotations(annotations map[string]string, fieldPath *field.Path) field.ErrorList { + allErrs := field.ErrorList{} + + if value, exists := annotations[mergeableIngressTypeAnnotationKey]; exists { + allErrs = append(allErrs, validateMergeableIngressTypeAnnotation(value, fieldPath.Child(mergeableIngressTypeAnnotationKey))...) + } + + return allErrs +} + +func validateMergeableIngressTypeAnnotation(value string, fieldPath *field.Path) field.ErrorList { + allErrs := field.ErrorList{} + + if value == "" { + return append(allErrs, field.Required(fieldPath, "")) + } + + if value != "master" && value != "minion" { + return append(allErrs, field.Invalid(fieldPath, value, "must be one of: 'master' or 'minion'")) + } + + return allErrs +} + +func validateIngressSpec(spec *networking.IngressSpec, fieldPath *field.Path) field.ErrorList { + allErrs := field.ErrorList{} + + allHosts := sets.String{} + + if len(spec.Rules) == 0 { + return append(allErrs, field.Required(fieldPath.Child("rules"), "")) + } + + for i, r := range spec.Rules { + idxPath := fieldPath.Child("rules").Index(i) + + if r.Host == "" { + allErrs = append(allErrs, field.Required(idxPath.Child("host"), "")) + } else if allHosts.Has(r.Host) { + allErrs = append(allErrs, field.Duplicate(idxPath.Child("host"), r.Host)) + } else { + allHosts.Insert(r.Host) + } + } + + return allErrs +} + +func validateMasterSpec(spec *networking.IngressSpec, fieldPath *field.Path) field.ErrorList { + allErrs := field.ErrorList{} + + if len(spec.Rules) != 1 { + return append(allErrs, field.TooMany(fieldPath.Child("rules"), len(spec.Rules), 1)) + } + + // the number of paths of the first rule of the spec must be 0 + if spec.Rules[0].HTTP != nil && len(spec.Rules[0].HTTP.Paths) > 0 { + pathsField := fieldPath.Child("rules").Index(0).Child("http").Child("paths") + return append(allErrs, field.TooMany(pathsField, len(spec.Rules[0].HTTP.Paths), 0)) + } + + return allErrs +} + +func validateMinionSpec(spec *networking.IngressSpec, fieldPath *field.Path) field.ErrorList { + allErrs := field.ErrorList{} + + if len(spec.TLS) > 0 { + allErrs = append(allErrs, field.TooMany(fieldPath.Child("tls"), len(spec.TLS), 0)) + } + + if len(spec.Rules) != 1 { + return append(allErrs, field.TooMany(fieldPath.Child("rules"), len(spec.Rules), 1)) + } + + // the number of paths of the first rule of the spec must be greater than 0 + if spec.Rules[0].HTTP == nil || len(spec.Rules[0].HTTP.Paths) == 0 { + pathsField := fieldPath.Child("rules").Index(0).Child("http").Child("paths") + return append(allErrs, field.Required(pathsField, "must include at least one path")) + } + + return allErrs +} diff --git a/internal/k8s/validation_test.go b/internal/k8s/validation_test.go new file mode 100644 index 0000000000..9e7d43afd4 --- /dev/null +++ b/internal/k8s/validation_test.go @@ -0,0 +1,424 @@ +package k8s + +import ( + "fmt" + "reflect" + "strings" + "testing" + + networking "k8s.io/api/networking/v1beta1" + meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/validation/field" +) + +func TestValidateIngress(t *testing.T) { + tests := []struct { + ing *networking.Ingress + expectedErrors []string + msg string + }{ + { + ing: &networking.Ingress{ + Spec: networking.IngressSpec{ + Rules: []networking.IngressRule{ + { + Host: "example.com", + }, + }, + }, + }, + expectedErrors: nil, + msg: "valid input", + }, + { + ing: &networking.Ingress{ + ObjectMeta: meta_v1.ObjectMeta{ + Annotations: map[string]string{ + "nginx.org/mergeable-ingress-type": "invalid", + }, + }, + Spec: networking.IngressSpec{ + Rules: []networking.IngressRule{ + { + Host: "", + }, + }, + }, + }, + expectedErrors: []string{ + `annotations.nginx.org/mergeable-ingress-type: Invalid value: "invalid": must be one of: 'master' or 'minion'`, + "spec.rules[0].host: Required value", + }, + msg: "invalid ingress", + }, + { + ing: &networking.Ingress{ + ObjectMeta: meta_v1.ObjectMeta{ + Annotations: map[string]string{ + "nginx.org/mergeable-ingress-type": "master", + }, + }, + Spec: networking.IngressSpec{ + Rules: []networking.IngressRule{ + { + Host: "example.com", + IngressRuleValue: networking.IngressRuleValue{ + HTTP: &networking.HTTPIngressRuleValue{ + Paths: []networking.HTTPIngressPath{ + { + Path: "/", + }, + }, + }, + }, + }, + }, + }, + }, + expectedErrors: []string{ + "spec.rules[0].http.paths: Too many: 1: must have at most 0 items", + }, + msg: "invalid master", + }, + { + ing: &networking.Ingress{ + ObjectMeta: meta_v1.ObjectMeta{ + Annotations: map[string]string{ + "nginx.org/mergeable-ingress-type": "minion", + }, + }, + Spec: networking.IngressSpec{ + Rules: []networking.IngressRule{ + { + Host: "example.com", + IngressRuleValue: networking.IngressRuleValue{}, + }, + }, + }, + }, + expectedErrors: []string{ + "spec.rules[0].http.paths: Required value: must include at least one path", + }, + msg: "invalid minion", + }, + } + + for _, test := range tests { + allErrs := validateIngress(test.ing) + assertion := assertErrors("validateIngress()", test.msg, allErrs, test.expectedErrors) + if assertion != "" { + t.Error(assertion) + } + } +} + +func TestValidateIngressAnnotations(t *testing.T) { + tests := []struct { + annotations map[string]string + expectedErrors []string + msg string + }{ + { + annotations: map[string]string{}, + expectedErrors: nil, + msg: "valid input", + }, + { + annotations: map[string]string{ + "nginx.org/mergeable-ingress-type": "master", + }, + expectedErrors: nil, + msg: "valid input with master annotation", + }, + { + annotations: map[string]string{ + "nginx.org/mergeable-ingress-type": "minion", + }, + expectedErrors: nil, + msg: "valid input with minion annotation", + }, + { + annotations: map[string]string{ + "nginx.org/mergeable-ingress-type": "", + }, + expectedErrors: []string{ + "annotations.nginx.org/mergeable-ingress-type: Required value", + }, + msg: "invalid mergeable type annotation 1", + }, + { + annotations: map[string]string{ + "nginx.org/mergeable-ingress-type": "abc", + }, + expectedErrors: []string{ + `annotations.nginx.org/mergeable-ingress-type: Invalid value: "abc": must be one of: 'master' or 'minion'`, + }, + msg: "invalid mergeable type annotation 2", + }, + } + + for _, test := range tests { + allErrs := validateIngressAnnotations(test.annotations, field.NewPath("annotations")) + assertion := assertErrors("validateIngressAnnotations()", test.msg, allErrs, test.expectedErrors) + if assertion != "" { + t.Error(assertion) + } + } +} + +func TestValidateIngressSpec(t *testing.T) { + tests := []struct { + spec *networking.IngressSpec + expectedErrors []string + msg string + }{ + { + spec: &networking.IngressSpec{ + Rules: []networking.IngressRule{ + { + Host: "foo.example.com", + }, + }, + }, + expectedErrors: nil, + msg: "valid input", + }, + { + spec: &networking.IngressSpec{ + Rules: []networking.IngressRule{}, + }, + expectedErrors: []string{ + "spec.rules: Required value", + }, + msg: "zero rules", + }, + { + spec: &networking.IngressSpec{ + Rules: []networking.IngressRule{ + { + Host: "", + }, + }, + }, + expectedErrors: []string{ + "spec.rules[0].host: Required value", + }, + msg: "empty host", + }, + { + spec: &networking.IngressSpec{ + Rules: []networking.IngressRule{ + { + Host: "foo.example.com", + }, + { + Host: "foo.example.com", + }, + }, + }, + expectedErrors: []string{ + `spec.rules[1].host: Duplicate value: "foo.example.com"`, + }, + msg: "duplicated host", + }, + } + + for _, test := range tests { + allErrs := validateIngressSpec(test.spec, field.NewPath("spec")) + assertion := assertErrors("validateIngressSpec()", test.msg, allErrs, test.expectedErrors) + if assertion != "" { + t.Error(assertion) + } + } +} + +func TestValidateMasterSpec(t *testing.T) { + tests := []struct { + spec *networking.IngressSpec + expectedErrors []string + msg string + }{ + { + spec: &networking.IngressSpec{ + Rules: []networking.IngressRule{ + { + Host: "foo.example.com", + IngressRuleValue: networking.IngressRuleValue{ + HTTP: &networking.HTTPIngressRuleValue{ + Paths: []networking.HTTPIngressPath{}, + }, + }, + }, + }, + }, + expectedErrors: nil, + msg: "valid input", + }, + { + spec: &networking.IngressSpec{ + Rules: []networking.IngressRule{ + { + Host: "foo.example.com", + }, + { + Host: "bar.example.com", + }, + }, + }, + expectedErrors: []string{ + "spec.rules: Too many: 2: must have at most 1 items", + }, + msg: "too many hosts", + }, + { + spec: &networking.IngressSpec{ + Rules: []networking.IngressRule{ + { + Host: "foo.example.com", + IngressRuleValue: networking.IngressRuleValue{ + HTTP: &networking.HTTPIngressRuleValue{ + Paths: []networking.HTTPIngressPath{ + { + Path: "/", + }, + }, + }, + }, + }, + }, + }, + expectedErrors: []string{ + "spec.rules[0].http.paths: Too many: 1: must have at most 0 items", + }, + msg: "too many paths", + }, + } + + for _, test := range tests { + allErrs := validateMasterSpec(test.spec, field.NewPath("spec")) + assertion := assertErrors("validateMasterSpec()", test.msg, allErrs, test.expectedErrors) + if assertion != "" { + t.Error(assertion) + } + } +} + +func TestValidateMinionSpec(t *testing.T) { + tests := []struct { + spec *networking.IngressSpec + expectedErrors []string + msg string + }{ + { + spec: &networking.IngressSpec{ + Rules: []networking.IngressRule{ + { + Host: "foo.example.com", + IngressRuleValue: networking.IngressRuleValue{ + HTTP: &networking.HTTPIngressRuleValue{ + Paths: []networking.HTTPIngressPath{ + { + Path: "/", + }, + }, + }, + }, + }, + }, + }, + expectedErrors: nil, + msg: "valid input", + }, + { + spec: &networking.IngressSpec{ + Rules: []networking.IngressRule{ + { + Host: "foo.example.com", + }, + { + Host: "bar.example.com", + }, + }, + }, + expectedErrors: []string{ + "spec.rules: Too many: 2: must have at most 1 items", + }, + msg: "too many hosts", + }, + { + spec: &networking.IngressSpec{ + Rules: []networking.IngressRule{ + { + Host: "foo.example.com", + IngressRuleValue: networking.IngressRuleValue{ + HTTP: &networking.HTTPIngressRuleValue{ + Paths: []networking.HTTPIngressPath{}, + }, + }, + }, + }, + }, + expectedErrors: []string{ + "spec.rules[0].http.paths: Required value: must include at least one path", + }, + msg: "too few paths", + }, + { + spec: &networking.IngressSpec{ + TLS: []networking.IngressTLS{ + { + Hosts: []string{"foo.example.com"}, + }, + }, + Rules: []networking.IngressRule{ + { + Host: "foo.example.com", + IngressRuleValue: networking.IngressRuleValue{ + HTTP: &networking.HTTPIngressRuleValue{ + Paths: []networking.HTTPIngressPath{ + { + Path: "/", + }, + }, + }, + }, + }, + }, + }, + expectedErrors: []string{ + "spec.tls: Too many: 1: must have at most 0 items", + }, + msg: "tls is forbidden", + }, + } + + for _, test := range tests { + allErrs := validateMinionSpec(test.spec, field.NewPath("spec")) + assertion := assertErrors("validateMinionSpec()", test.msg, allErrs, test.expectedErrors) + if assertion != "" { + t.Error(assertion) + } + } +} + +func assertErrors(funcName string, msg string, allErrs field.ErrorList, expectedErrors []string) string { + errors := errorListToStrings(allErrs) + if !reflect.DeepEqual(errors, expectedErrors) { + result := strings.Join(errors, "\n") + expected := strings.Join(expectedErrors, "\n") + + return fmt.Sprintf("%s returned \n%s \nbut expected \n%s \nfor the case of %s", funcName, result, expected, msg) + } + + return "" +} + +func errorListToStrings(list field.ErrorList) []string { + var result []string + + for _, e := range list { + result = append(result, e.Error()) + } + + return result +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 4840f16341..2881efa864 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -24,6 +24,7 @@ github.com/golang/protobuf/ptypes/duration github.com/golang/protobuf/ptypes/struct github.com/golang/protobuf/ptypes/timestamp # github.com/google/go-cmp v0.5.0 +## explicit github.com/google/go-cmp/cmp github.com/google/go-cmp/cmp/internal/diff github.com/google/go-cmp/cmp/internal/flags From 03e1a47bcbfec163d275441b575a8641a805663d Mon Sep 17 00:00:00 2001 From: Michael Pleshakov Date: Mon, 28 Sep 2020 16:00:12 -0700 Subject: [PATCH 2/3] Use Configuration type in Controller --- internal/configs/configurator.go | 135 +- internal/configs/ingress.go | 27 +- internal/configs/ingress_test.go | 18 + internal/k8s/configuration.go | 23 +- internal/k8s/configuration_test.go | 44 +- internal/k8s/controller.go | 2091 +++++++---------------- internal/k8s/controller_test.go | 1817 +++----------------- internal/k8s/handlers.go | 76 +- internal/k8s/leader.go | 8 +- internal/k8s/reference_checkers.go | 15 + internal/k8s/reference_checkers_test.go | 70 + internal/k8s/status.go | 192 ++- internal/k8s/status_test.go | 2 +- internal/k8s/task_queue.go | 9 +- 14 files changed, 1230 insertions(+), 3297 deletions(-) diff --git a/internal/configs/configurator.go b/internal/configs/configurator.go index 9b8d35aa85..935b55a308 100644 --- a/internal/configs/configurator.go +++ b/internal/configs/configurator.go @@ -598,43 +598,49 @@ func (cnf *Configurator) addOrUpdateJWKSecret(secret *api_v1.Secret) string { } // AddOrUpdateJWKSecret adds a JWK secret to the filesystem or updates it if it already exists. -func (cnf *Configurator) AddOrUpdateJWKSecret(secret *api_v1.Secret, virtualServerExes []*VirtualServerEx) error { +func (cnf *Configurator) AddOrUpdateJWKSecret(secret *api_v1.Secret, virtualServerExes []*VirtualServerEx) (Warnings, error) { cnf.addOrUpdateJWKSecret(secret) + allWarnings := newWarnings() + if len(virtualServerExes) > 0 { for _, vsEx := range virtualServerExes { - // It is safe to ignore warnings here as no new warnings should appear when adding or updating a secret - _, err := cnf.addOrUpdateVirtualServer(vsEx) + warnings, err := cnf.addOrUpdateVirtualServer(vsEx) if err != nil { - return fmt.Errorf("Error adding or updating VirtualServer %v/%v: %v", vsEx.VirtualServer.Namespace, vsEx.VirtualServer.Name, err) + return allWarnings, fmt.Errorf("Error adding or updating VirtualServer %v/%v: %v", vsEx.VirtualServer.Namespace, vsEx.VirtualServer.Name, err) } + allWarnings.Add(warnings) } if err := cnf.nginxManager.Reload(nginx.ReloadForOtherUpdate); err != nil { - return fmt.Errorf("Error when reloading NGINX when updating Secret: %v", err) + return allWarnings, fmt.Errorf("Error when reloading NGINX when updating Secret: %v", err) } } - return nil + + return allWarnings, nil } // AddOrUpdateIngressMTLSSecret adds a IngressMTLS secret to the filesystem or updates it if it already exists. -func (cnf *Configurator) AddOrUpdateIngressMTLSSecret(secret *api_v1.Secret, virtualServerExes []*VirtualServerEx) error { +func (cnf *Configurator) AddOrUpdateIngressMTLSSecret(secret *api_v1.Secret, virtualServerExes []*VirtualServerEx) (Warnings, error) { cnf.addOrUpdateIngressMTLSecret(secret) + allWarnings := newWarnings() + if len(virtualServerExes) > 0 { for _, vsEx := range virtualServerExes { - // TODO we need to read warnings, this will be fixed in upcoming work - _, err := cnf.addOrUpdateVirtualServer(vsEx) + warnings, err := cnf.addOrUpdateVirtualServer(vsEx) if err != nil { - return fmt.Errorf("Error adding or updating VirtualServer %v/%v: %v", vsEx.VirtualServer.Namespace, vsEx.VirtualServer.Name, err) + return allWarnings, fmt.Errorf("Error adding or updating VirtualServer %v/%v: %v", vsEx.VirtualServer.Namespace, vsEx.VirtualServer.Name, err) } + allWarnings.Add(warnings) } if err := cnf.nginxManager.Reload(nginx.ReloadForOtherUpdate); err != nil { - return fmt.Errorf("Error when reloading NGINX when updating Secret: %v", err) + return allWarnings, fmt.Errorf("Error when reloading NGINX when updating Secret: %v", err) } } - return nil + + return allWarnings, nil } // addOrUpdateJWKSecretsForVirtualServer adds JWK secrets to the filesystem or updates them if they already exist. @@ -654,36 +660,43 @@ func (cnf *Configurator) addOrUpdateJWKSecretsForVirtualServer(jwtKeys map[strin return jwkSecrets } -// AddOrUpdateTLSSecret adds or updates a file with the content of the TLS secret. -func (cnf *Configurator) AddOrUpdateTLSSecret(secret *api_v1.Secret, ingExes []IngressEx, mergeableIngresses []MergeableIngresses, virtualServerExes []*VirtualServerEx) error { - cnf.addOrUpdateTLSSecret(secret) - for i := range ingExes { - err := cnf.addOrUpdateIngress(&ingExes[i]) +// AddOrUpdateResources adds or updates configuration for resources. +func (cnf *Configurator) AddOrUpdateResources(ingExes []*IngressEx, mergeableIngresses []*MergeableIngresses, virtualServerExes []*VirtualServerEx) (Warnings, error) { + allWarnings := newWarnings() + + for _, ingEx := range ingExes { + err := cnf.addOrUpdateIngress(ingEx) if err != nil { - return fmt.Errorf("Error adding or updating ingress %v/%v: %v", ingExes[i].Ingress.Namespace, ingExes[i].Ingress.Name, err) + return allWarnings, fmt.Errorf("Error adding or updating ingress %v/%v: %v", ingEx.Ingress.Namespace, ingEx.Ingress.Name, err) } } - for i := range mergeableIngresses { - err := cnf.addOrUpdateMergeableIngress(&mergeableIngresses[i]) + for _, m := range mergeableIngresses { + err := cnf.addOrUpdateMergeableIngress(m) if err != nil { - return fmt.Errorf("Error adding or updating mergeableIngress %v/%v: %v", mergeableIngresses[i].Master.Ingress.Namespace, mergeableIngresses[i].Master.Ingress.Name, err) + return allWarnings, fmt.Errorf("Error adding or updating mergeableIngress %v/%v: %v", m.Master.Ingress.Namespace, m.Master.Ingress.Name, err) } } for _, vsEx := range virtualServerExes { - // It is safe to ignore warnings here as no new warnings should appear when adding or updating a secret - _, err := cnf.addOrUpdateVirtualServer(vsEx) + warnings, err := cnf.addOrUpdateVirtualServer(vsEx) if err != nil { - return fmt.Errorf("Error adding or updating VirtualServer %v/%v: %v", vsEx.VirtualServer.Namespace, vsEx.VirtualServer.Name, err) + return allWarnings, fmt.Errorf("Error adding or updating VirtualServer %v/%v: %v", vsEx.VirtualServer.Namespace, vsEx.VirtualServer.Name, err) } + allWarnings.Add(warnings) } if err := cnf.nginxManager.Reload(nginx.ReloadForOtherUpdate); err != nil { - return fmt.Errorf("Error when reloading NGINX when updating Secret: %v", err) + return allWarnings, fmt.Errorf("Error when reloading NGINX when updating resources: %v", err) } - return nil + return allWarnings, nil +} + +// AddOrUpdateTLSSecret adds or updates a file with the content of the TLS secret. +func (cnf *Configurator) AddOrUpdateTLSSecret(secret *api_v1.Secret, ingExes []*IngressEx, mergeableIngresses []*MergeableIngresses, virtualServerExes []*VirtualServerEx) (Warnings, error) { + cnf.addOrUpdateTLSSecret(secret) + return cnf.AddOrUpdateResources(ingExes, mergeableIngresses, virtualServerExes) } func (cnf *Configurator) addOrUpdateTLSSecret(secret *api_v1.Secret) string { @@ -729,38 +742,40 @@ func GenerateCAFileContent(secret *api_v1.Secret) []byte { // DeleteSecret deletes the file associated with the secret and the configuration files for Ingress and VirtualServer resources. // NGINX is reloaded only when the total number of the resources > 0. -func (cnf *Configurator) DeleteSecret(key string, ingExes []IngressEx, mergeableIngresses []MergeableIngresses, virtualServerExes []*VirtualServerEx) error { +func (cnf *Configurator) DeleteSecret(key string, ingExes []*IngressEx, mergeableIngresses []*MergeableIngresses, virtualServerExes []*VirtualServerEx) (Warnings, error) { cnf.nginxManager.DeleteSecret(keyToFileName(key)) - for i := range ingExes { - err := cnf.addOrUpdateIngress(&ingExes[i]) + allWarnings := newWarnings() + + for _, ingEx := range ingExes { + err := cnf.addOrUpdateIngress(ingEx) if err != nil { - return fmt.Errorf("Error adding or updating ingress %v/%v: %v", ingExes[i].Ingress.Namespace, ingExes[i].Ingress.Name, err) + return allWarnings, fmt.Errorf("Error adding or updating ingress %v/%v: %v", ingEx.Ingress.Namespace, ingEx.Ingress.Name, err) } } - for i := range mergeableIngresses { - err := cnf.addOrUpdateMergeableIngress(&mergeableIngresses[i]) + for _, m := range mergeableIngresses { + err := cnf.addOrUpdateMergeableIngress(m) if err != nil { - return fmt.Errorf("Error adding or updating mergeableIngress %v/%v: %v", mergeableIngresses[i].Master.Ingress.Namespace, mergeableIngresses[i].Master.Ingress.Name, err) + return allWarnings, fmt.Errorf("Error adding or updating mergeableIngress %v/%v: %v", m.Master.Ingress.Namespace, m.Master.Ingress.Name, err) } } for _, vsEx := range virtualServerExes { - // It is safe to ignore warnings here as no new warnings should appear when deleting a secret - _, err := cnf.addOrUpdateVirtualServer(vsEx) + warnings, err := cnf.addOrUpdateVirtualServer(vsEx) if err != nil { - return fmt.Errorf("Error adding or updating VirtualServer %v/%v: %v", vsEx.VirtualServer.Namespace, vsEx.VirtualServer.Name, err) + return allWarnings, fmt.Errorf("Error adding or updating VirtualServer %v/%v: %v", vsEx.VirtualServer.Namespace, vsEx.VirtualServer.Name, err) } + allWarnings.Add(warnings) } if len(ingExes)+len(mergeableIngresses)+len(virtualServerExes) > 0 { if err := cnf.nginxManager.Reload(nginx.ReloadForOtherUpdate); err != nil { - return fmt.Errorf("Error when reloading NGINX when deleting Secret %v: %v", key, err) + return allWarnings, fmt.Errorf("Error when reloading NGINX when deleting Secret %v: %v", key, err) } } - return nil + return allWarnings, nil } // DeleteIngress deletes NGINX configuration for the Ingress resource. @@ -1041,7 +1056,7 @@ func (cnf *Configurator) updatePlusEndpoints(ingEx *IngressEx) error { } // UpdateConfig updates NGINX configuration parameters. -func (cnf *Configurator) UpdateConfig(cfgParams *ConfigParams, ingExes []*IngressEx, mergeableIngs map[string]*MergeableIngresses, virtualServerExes []*VirtualServerEx) (Warnings, error) { +func (cnf *Configurator) UpdateConfig(cfgParams *ConfigParams, ingExes []*IngressEx, mergeableIngs []*MergeableIngresses, virtualServerExes []*VirtualServerEx) (Warnings, error) { cnf.cfgParams = cfgParams allWarnings := newWarnings() @@ -1317,18 +1332,18 @@ func generateApResourceFileContent(apResource *unstructured.Unstructured) []byte } // AddOrUpdateAppProtectResource updates Ingresses that use App Protect Resources -func (cnf *Configurator) AddOrUpdateAppProtectResource(resource *unstructured.Unstructured, ingExes []IngressEx, mergeableIngresses []MergeableIngresses) error { - for i := range ingExes { - err := cnf.addOrUpdateIngress(&ingExes[i]) +func (cnf *Configurator) AddOrUpdateAppProtectResource(resource *unstructured.Unstructured, ingExes []*IngressEx, mergeableIngresses []*MergeableIngresses) error { + for _, ingEx := range ingExes { + err := cnf.addOrUpdateIngress(ingEx) if err != nil { - return fmt.Errorf("Error adding or updating ingress %v/%v: %v", ingExes[i].Ingress.Namespace, ingExes[i].Ingress.Name, err) + return fmt.Errorf("Error adding or updating ingress %v/%v: %v", ingEx.Ingress.Namespace, ingEx.Ingress.Name, err) } } - for i := range mergeableIngresses { - err := cnf.addOrUpdateMergeableIngress(&mergeableIngresses[i]) + for _, m := range mergeableIngresses { + err := cnf.addOrUpdateMergeableIngress(m) if err != nil { - return fmt.Errorf("Error adding or updating mergeableIngress %v/%v: %v", mergeableIngresses[i].Master.Ingress.Namespace, mergeableIngresses[i].Master.Ingress.Name, err) + return fmt.Errorf("Error adding or updating mergeableIngress %v/%v: %v", m.Master.Ingress.Namespace, m.Master.Ingress.Name, err) } } @@ -1340,22 +1355,22 @@ func (cnf *Configurator) AddOrUpdateAppProtectResource(resource *unstructured.Un } // DeleteAppProtectPolicy updates Ingresses that use AP Policy after that policy is deleted -func (cnf *Configurator) DeleteAppProtectPolicy(polNamespaceName string, ingExes []IngressEx, mergeableIngresses []MergeableIngresses) error { +func (cnf *Configurator) DeleteAppProtectPolicy(polNamespaceName string, ingExes []*IngressEx, mergeableIngresses []*MergeableIngresses) error { fName := strings.Replace(polNamespaceName, "/", "_", 1) polFileName := appProtectPolicyFolder + fName cnf.nginxManager.DeleteAppProtectResourceFile(polFileName) - for i := range ingExes { - err := cnf.addOrUpdateIngress(&ingExes[i]) + for _, ingEx := range ingExes { + err := cnf.addOrUpdateIngress(ingEx) if err != nil { - return fmt.Errorf("Error adding or updating ingress %v/%v: %v", ingExes[i].Ingress.Namespace, ingExes[i].Ingress.Name, err) + return fmt.Errorf("Error adding or updating ingress %v/%v: %v", ingEx.Ingress.Namespace, ingEx.Ingress.Name, err) } } - for i := range mergeableIngresses { - err := cnf.addOrUpdateMergeableIngress(&mergeableIngresses[i]) + for _, m := range mergeableIngresses { + err := cnf.addOrUpdateMergeableIngress(m) if err != nil { - return fmt.Errorf("Error adding or updating mergeableIngress %v/%v: %v", mergeableIngresses[i].Master.Ingress.Namespace, mergeableIngresses[i].Master.Ingress.Name, err) + return fmt.Errorf("Error adding or updating mergeableIngress %v/%v: %v", m.Master.Ingress.Namespace, m.Master.Ingress.Name, err) } } @@ -1367,22 +1382,22 @@ func (cnf *Configurator) DeleteAppProtectPolicy(polNamespaceName string, ingExes } // DeleteAppProtectLogConf updates Ingresses that use AP Log Configuration after that policy is deleted -func (cnf *Configurator) DeleteAppProtectLogConf(logConfNamespaceName string, ingExes []IngressEx, mergeableIngresses []MergeableIngresses) error { +func (cnf *Configurator) DeleteAppProtectLogConf(logConfNamespaceName string, ingExes []*IngressEx, mergeableIngresses []*MergeableIngresses) error { fName := strings.Replace(logConfNamespaceName, "/", "_", 1) logConfFileName := appProtectLogConfFolder + fName cnf.nginxManager.DeleteAppProtectResourceFile(logConfFileName) - for i := range ingExes { - err := cnf.addOrUpdateIngress(&ingExes[i]) + for _, ingEx := range ingExes { + err := cnf.addOrUpdateIngress(ingEx) if err != nil { - return fmt.Errorf("Error adding or updating ingress %v/%v: %v", ingExes[i].Ingress.Namespace, ingExes[i].Ingress.Name, err) + return fmt.Errorf("Error adding or updating ingress %v/%v: %v", ingEx.Ingress.Namespace, ingEx.Ingress.Name, err) } } - for i := range mergeableIngresses { - err := cnf.addOrUpdateMergeableIngress(&mergeableIngresses[i]) + for _, m := range mergeableIngresses { + err := cnf.addOrUpdateMergeableIngress(m) if err != nil { - return fmt.Errorf("Error adding or updating mergeableIngress %v/%v: %v", mergeableIngresses[i].Master.Ingress.Namespace, mergeableIngresses[i].Master.Ingress.Name, err) + return fmt.Errorf("Error adding or updating mergeableIngress %v/%v: %v", m.Master.Ingress.Namespace, m.Master.Ingress.Name, err) } } diff --git a/internal/configs/ingress.go b/internal/configs/ingress.go index 7b10fcdd07..ad6c5105ab 100644 --- a/internal/configs/ingress.go +++ b/internal/configs/ingress.go @@ -27,6 +27,8 @@ type IngressEx struct { HealthChecks map[string]*api_v1.Probe ExternalNameSvcs map[string]bool PodsByIP map[string]PodInfo + ValidHosts map[string]bool + ValidMinionPaths map[string]bool AppProtectPolicy *unstructured.Unstructured AppProtectLogConf *unstructured.Unstructured AppProtectLogDst string @@ -87,10 +89,18 @@ func generateNginxCfg(ingEx *IngressEx, pems map[string]string, apResources map[ var servers []version1.Server for _, rule := range ingEx.Ingress.Spec.Rules { - if rule.IngressRuleValue.HTTP == nil { + // skipping invalid hosts + if !ingEx.ValidHosts[rule.Host] { continue } + httpIngressRuleValue := rule.HTTP + + if httpIngressRuleValue == nil { + // the code in this loop expects non-nil + httpIngressRuleValue = &networking.HTTPIngressRuleValue{} + } + serverName := rule.Host statusZone := rule.Host @@ -158,7 +168,7 @@ func generateNginxCfg(ingEx *IngressEx, pems map[string]string, apResources map[ grpcOnly := true if len(grpcServices) > 0 { - for _, path := range rule.HTTP.Paths { + for _, path := range httpIngressRuleValue.Paths { if _, exists := grpcServices[path.Backend.ServiceName]; !exists { grpcOnly = false break @@ -168,7 +178,12 @@ func generateNginxCfg(ingEx *IngressEx, pems map[string]string, apResources map[ grpcOnly = false } - for _, path := range rule.HTTP.Paths { + for _, path := range httpIngressRuleValue.Paths { + // skip invalid paths for minions + if isMinion && !ingEx.ValidMinionPaths[path.Path] { + continue + } + upsName := getNameForUpstream(ingEx.Ingress, rule.Host, &path.Backend) if cfgParams.HealthCheckEnabled { @@ -417,6 +432,9 @@ func generateNginxCfgForMergeableIngresses(mergeableIngs *MergeableIngresses, ma healthChecks := make(map[string]version1.HealthCheck) var keepalive string + // replace master with a deepcopy because we will modify it + mergeableIngs.Master.Ingress = mergeableIngs.Master.Ingress.DeepCopy() + removedAnnotations := filterMasterAnnotations(mergeableIngs.Master.Ingress.Annotations) if len(removedAnnotations) != 0 { glog.Errorf("Ingress Resource %v/%v with the annotation 'nginx.org/mergeable-ingress-type' set to 'master' cannot contain the '%v' annotation(s). They will be ignored", @@ -437,6 +455,9 @@ func generateNginxCfgForMergeableIngresses(mergeableIngs *MergeableIngresses, ma minions := mergeableIngs.Minions for _, minion := range minions { + // replace minion with a deepcopy because we will modify it + minion.Ingress = minion.Ingress.DeepCopy() + // Remove the default backend so that "/" will not be generated minion.Ingress.Spec.Backend = nil diff --git a/internal/configs/ingress_test.go b/internal/configs/ingress_test.go index ba1c24e010..a4613665e3 100644 --- a/internal/configs/ingress_test.go +++ b/internal/configs/ingress_test.go @@ -300,6 +300,9 @@ func createCafeIngressEx() IngressEx { "tea-svc80": {"10.0.0.2:80"}, }, ExternalNameSvcs: map[string]bool{}, + ValidHosts: map[string]bool{ + "cafe.example.com": true, + }, } return cafeIngressEx } @@ -532,6 +535,9 @@ func createMergeableCafeIngress() *MergeableIngresses { "coffee-svc80": {"10.0.0.1:80"}, "tea-svc80": {"10.0.0.2:80"}, }, + ValidHosts: map[string]bool{ + "cafe.example.com": true, + }, }, Minions: []*IngressEx{ { @@ -539,12 +545,24 @@ func createMergeableCafeIngress() *MergeableIngresses { Endpoints: map[string][]string{ "coffee-svc80": {"10.0.0.1:80"}, }, + ValidHosts: map[string]bool{ + "cafe.example.com": true, + }, + ValidMinionPaths: map[string]bool{ + "/coffee": true, + }, }, { Ingress: &teaMinion, Endpoints: map[string][]string{ "tea-svc80": {"10.0.0.2:80"}, }, + ValidHosts: map[string]bool{ + "cafe.example.com": true, + }, + ValidMinionPaths: map[string]bool{ + "/tea": true, + }, }}, } diff --git a/internal/k8s/configuration.go b/internal/k8s/configuration.go index 7503bc6461..621256dbf7 100644 --- a/internal/k8s/configuration.go +++ b/internal/k8s/configuration.go @@ -299,6 +299,7 @@ type Configuration struct { problems map[string]ConfigurationProblem hasCorrectIngressClass func(interface{}) bool + virtualServerValidator *validation.VirtualServerValidator secretReferenceChecker *secretReferenceChecker serviceReferenceChecker *serviceReferenceChecker @@ -306,13 +307,11 @@ type Configuration struct { appPolicyReferenceChecker *appProtectResourceReferenceChecker appLogConfReferenceChecker *appProtectResourceReferenceChecker - isPlus bool - lock sync.RWMutex } // NewConfiguration creates a new Configuration. -func NewConfiguration(hasCorrectIngressClass func(interface{}) bool, isPlus bool) *Configuration { +func NewConfiguration(hasCorrectIngressClass func(interface{}) bool, isPlus bool, virtualServerValidator *validation.VirtualServerValidator) *Configuration { return &Configuration{ hosts: make(map[string]Resource), ingresses: make(map[string]*networking.Ingress), @@ -320,12 +319,12 @@ func NewConfiguration(hasCorrectIngressClass func(interface{}) bool, isPlus bool virtualServerRoutes: make(map[string]*conf_v1.VirtualServerRoute), problems: make(map[string]ConfigurationProblem), hasCorrectIngressClass: hasCorrectIngressClass, + virtualServerValidator: virtualServerValidator, secretReferenceChecker: newSecretReferenceChecker(isPlus), serviceReferenceChecker: newServiceReferenceChecker(), policyReferenceChecker: newPolicyReferenceChecker(), appPolicyReferenceChecker: newAppProtectResourceReferenceChecker(configs.AppProtectPolicyAnnotation), appLogConfReferenceChecker: newAppProtectResourceReferenceChecker(configs.AppProtectLogConfAnnotation), - isPlus: isPlus, } } @@ -406,7 +405,7 @@ func (c *Configuration) AddOrUpdateVirtualServer(vs *conf_v1.VirtualServer) ([]R if !c.hasCorrectIngressClass(vs) { delete(c.virtualServers, key) } else { - validationError = validation.ValidateVirtualServer(vs, c.isPlus) + validationError = c.virtualServerValidator.ValidateVirtualServer(vs) if validationError != nil { delete(c.virtualServers, key) } else { @@ -438,7 +437,7 @@ func (c *Configuration) AddOrUpdateVirtualServer(vs *conf_v1.VirtualServer) ([]R Object: vs, IsError: true, Reason: "Rejected", - Message: validationError.Error(), + Message: fmt.Sprintf("VirtualServer %s was rejected with error: %s", getResourceKey(&vs.ObjectMeta), validationError.Error()), } problems = append(problems, p) } @@ -472,7 +471,7 @@ func (c *Configuration) AddOrUpdateVirtualServerRoute(vsr *conf_v1.VirtualServer if !c.hasCorrectIngressClass(vsr) { delete(c.virtualServerRoutes, key) } else { - validationError = validation.ValidateVirtualServerRoute(vsr, c.isPlus) + validationError = c.virtualServerValidator.ValidateVirtualServerRoute(vsr) if validationError != nil { delete(c.virtualServerRoutes, key) } else { @@ -487,7 +486,7 @@ func (c *Configuration) AddOrUpdateVirtualServerRoute(vsr *conf_v1.VirtualServer Object: vsr, IsError: true, Reason: "Rejected", - Message: validationError.Error(), + Message: fmt.Sprintf("VirtualServerRoute %s was rejected with error: %s", getResourceKey(&vsr.ObjectMeta), validationError.Error()), } problems = append(problems, p) } @@ -556,6 +555,12 @@ func (c *Configuration) FindResourcesForService(svcNamespace string, svcName str return c.findResourcesForResourceReference(svcNamespace, svcName, c.serviceReferenceChecker) } +// FindResourcesForEndpoints finds resources that reference the specified endpoints. +func (c *Configuration) FindResourcesForEndpoints(endpointsNamespace string, endpointsName string) []Resource { + // Resources reference not endpoints but the corresponding service, which has the same namespace and name + return c.FindResourcesForService(endpointsNamespace, endpointsName) +} + // FindResourcesForSecret finds resources that reference the specified secret. func (c *Configuration) FindResourcesForSecret(secretNamespace string, secretName string) []Resource { return c.findResourcesForResourceReference(secretNamespace, secretName, c.secretReferenceChecker) @@ -1018,7 +1023,7 @@ func (c *Configuration) buildVirtualServerRoutes(vs *conf_v1.VirtualServer) ([]* continue } - err := validation.ValidateVirtualServerRouteForVirtualServer(vsr, vs.Spec.Host, r.Path, c.isPlus) + err := c.virtualServerValidator.ValidateVirtualServerRouteForVirtualServer(vsr, vs.Spec.Host, r.Path) if err != nil { warning := fmt.Sprintf("VirtualServerRoute %s is invalid: %v", vsrKey, err) warnings = append(warnings, warning) diff --git a/internal/k8s/configuration_test.go b/internal/k8s/configuration_test.go index ff79bd3405..5f47c79baa 100644 --- a/internal/k8s/configuration_test.go +++ b/internal/k8s/configuration_test.go @@ -6,6 +6,7 @@ import ( "github.com/google/go-cmp/cmp" conf_v1 "github.com/nginxinc/kubernetes-ingress/pkg/apis/configuration/v1" + "github.com/nginxinc/kubernetes-ingress/pkg/apis/configuration/validation" networking "k8s.io/api/networking/v1beta1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -15,7 +16,8 @@ func createTestConfiguration() *Configuration { ingressClass: "nginx", useIngressClassOnly: true, } - return NewConfiguration(lbc.HasCorrectIngressClass, false) + isPlus := false + return NewConfiguration(lbc.HasCorrectIngressClass, isPlus, validation.NewVirtualServerValidator(isPlus)) } func TestAddIngressForRegularIngress(t *testing.T) { @@ -1064,7 +1066,7 @@ func TestAddInvalidVirtualServer(t *testing.T) { Object: vs, IsError: true, Reason: "Rejected", - Message: "spec.host: Required value", + Message: "VirtualServer default/virtualserver was rejected with error: spec.host: Required value", }, } @@ -1281,7 +1283,7 @@ func TestAddVirtualServerWithVirtualServerRoutes(t *testing.T) { Object: invalidVSR1, IsError: true, Reason: "Rejected", - Message: "spec.host: Required value", + Message: "VirtualServerRoute default/virtualserverroute-1 was rejected with error: spec.host: Required value", }, } @@ -1552,7 +1554,7 @@ func TestAddInvalidVirtualServerRoute(t *testing.T) { Object: vsr, IsError: true, Reason: "Rejected", - Message: "spec.host: Required value", + Message: "VirtualServerRoute default/virtualserverroute was rejected with error: spec.host: Required value", }, } @@ -1936,7 +1938,7 @@ func TestChooseObjectMetaWinner(t *testing.T) { } func TestSquashResourceChanges(t *testing.T) { - fullIngress := &FullIngress{ + fullIng := &FullIngress{ Ingress: createTestIngress("test", "foo.example.com"), } @@ -1953,17 +1955,17 @@ func TestSquashResourceChanges(t *testing.T) { changes: []ResourceChange{ { Op: Delete, - Resource: fullIngress, + Resource: fullIng, }, { Op: Delete, - Resource: fullIngress, + Resource: fullIng, }, }, expected: []ResourceChange{ { Op: Delete, - Resource: fullIngress, + Resource: fullIng, }, }, msg: "squash deletes", @@ -1972,17 +1974,17 @@ func TestSquashResourceChanges(t *testing.T) { changes: []ResourceChange{ { Op: AddOrUpdate, - Resource: fullIngress, + Resource: fullIng, }, { Op: AddOrUpdate, - Resource: fullIngress, + Resource: fullIng, }, }, expected: []ResourceChange{ { Op: AddOrUpdate, - Resource: fullIngress, + Resource: fullIng, }, }, msg: "squash updates", @@ -1991,17 +1993,17 @@ func TestSquashResourceChanges(t *testing.T) { changes: []ResourceChange{ { Op: Delete, - Resource: fullIngress, + Resource: fullIng, }, { Op: AddOrUpdate, - Resource: fullIngress, + Resource: fullIng, }, }, expected: []ResourceChange{ { Op: AddOrUpdate, - Resource: fullIngress, + Resource: fullIng, }, }, msg: "squash update and delete", @@ -2014,7 +2016,7 @@ func TestSquashResourceChanges(t *testing.T) { }, { Op: AddOrUpdate, - Resource: fullIngress, + Resource: fullIng, }, }, expected: []ResourceChange{ @@ -2024,7 +2026,7 @@ func TestSquashResourceChanges(t *testing.T) { }, { Op: AddOrUpdate, - Resource: fullIngress, + Resource: fullIng, }, }, msg: "preserve the order", @@ -2033,7 +2035,7 @@ func TestSquashResourceChanges(t *testing.T) { changes: []ResourceChange{ { Op: Delete, - Resource: fullIngress, + Resource: fullIng, }, { Op: AddOrUpdate, @@ -2043,7 +2045,7 @@ func TestSquashResourceChanges(t *testing.T) { expected: []ResourceChange{ { Op: Delete, - Resource: fullIngress, + Resource: fullIng, }, { Op: AddOrUpdate, @@ -2056,11 +2058,11 @@ func TestSquashResourceChanges(t *testing.T) { changes: []ResourceChange{ { Op: Delete, - Resource: fullIngress, + Resource: fullIng, }, { Op: AddOrUpdate, - Resource: fullIngress, + Resource: fullIng, }, { Op: Delete, @@ -2074,7 +2076,7 @@ func TestSquashResourceChanges(t *testing.T) { }, { Op: AddOrUpdate, - Resource: fullIngress, + Resource: fullIng, }, }, msg: "squashed delete and update must follow delete", diff --git a/internal/k8s/controller.go b/internal/k8s/controller.go index 9975b40678..7c7bee5823 100644 --- a/internal/k8s/controller.go +++ b/internal/k8s/controller.go @@ -18,7 +18,6 @@ package k8s import ( "context" - "errors" "fmt" "strings" "sync" @@ -40,8 +39,6 @@ import ( "github.com/nginxinc/kubernetes-ingress/internal/configs" "github.com/nginxinc/kubernetes-ingress/internal/metrics/collectors" - "sort" - api_v1 "k8s.io/api/core/v1" networking "k8s.io/api/networking/v1beta1" meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -151,12 +148,12 @@ type LoadBalancerController struct { metricsCollector collectors.ControllerCollector globalConfigurationValidator *validation.GlobalConfigurationValidator transportServerValidator *validation.TransportServerValidator - virtualServerValidator *validation.VirtualServerValidator spiffeController *spiffeController internalRoutesEnabled bool syncLock sync.Mutex isNginxReady bool isLatencyMetricsEnabled bool + configuration *Configuration } var keyFunc = cache.DeletionHandlingMetaNamespaceKeyFunc @@ -215,7 +212,6 @@ func NewLoadBalancerController(input NewLoadBalancerControllerInput) *LoadBalanc metricsCollector: input.MetricsCollector, globalConfigurationValidator: input.GlobalConfigurationValidator, transportServerValidator: input.TransportServerValidator, - virtualServerValidator: input.VirtualServerValidator, internalRoutesEnabled: input.InternalRoutesEnabled, isLatencyMetricsEnabled: input.IsLatencyMetricsEnabled, } @@ -238,14 +234,6 @@ func NewLoadBalancerController(input NewLoadBalancerControllerInput) *LoadBalanc } glog.V(3).Infof("Nginx Ingress Controller has class: %v", input.IngressClass) - lbc.statusUpdater = &statusUpdater{ - client: input.KubeClient, - namespace: input.ControllerNamespace, - externalServiceName: input.ExternalServiceName, - ingLister: &lbc.ingressLister, - keyFunc: keyFunc, - confClient: input.ConfClient, - } // create handlers for resources we care about lbc.addSecretHandler(createSecretHandlers(lbc)) @@ -288,15 +276,23 @@ func NewLoadBalancerController(input NewLoadBalancerControllerInput) *LoadBalanc lbc.addLeaderHandler(createLeaderHandler(lbc)) } + lbc.statusUpdater = &statusUpdater{ + client: input.KubeClient, + namespace: input.ControllerNamespace, + externalServiceName: input.ExternalServiceName, + ingressLister: &lbc.ingressLister, + virtualServerLister: lbc.virtualServerLister, + virtualServerRouteLister: lbc.virtualServerRouteLister, + keyFunc: keyFunc, + confClient: input.ConfClient, + } + + lbc.configuration = NewConfiguration(lbc.HasCorrectIngressClass, input.IsNginxPlus, input.VirtualServerValidator) + lbc.updateIngressMetrics() return lbc } -// UpdateManagedAndMergeableIngresses invokes the UpdateManagedAndMergeableIngresses method on the Status Updater -func (lbc *LoadBalancerController) UpdateManagedAndMergeableIngresses(ingresses []networking.Ingress, mergeableIngresses map[string]*configs.MergeableIngresses) error { - return lbc.statusUpdater.UpdateManagedAndMergeableIngresses(ingresses, mergeableIngresses) -} - // addLeaderHandler adds the handler for leader election to the controller func (lbc *LoadBalancerController) addLeaderHandler(leaderHandler leaderelection.LeaderCallbacks) { var err error @@ -521,7 +517,7 @@ func (lbc *LoadBalancerController) Stop() { lbc.syncQueue.Shutdown() } -func (lbc *LoadBalancerController) syncEndpoint(task task) { +func (lbc *LoadBalancerController) syncEndpoints(task task) { key := task.Key glog.V(3).Infof("Syncing endpoints %v", key) @@ -531,88 +527,79 @@ func (lbc *LoadBalancerController) syncEndpoint(task task) { return } - if endpExists { - ings := lbc.getIngressForEndpoints(obj) + if !endpExists { + return + } - var ingExes []*configs.IngressEx - var mergableIngressesSlice []*configs.MergeableIngresses + endp := obj.(*api_v1.Endpoints) + resources := lbc.configuration.FindResourcesForEndpoints(endp.Namespace, endp.Name) - for i := range ings { - if !lbc.HasCorrectIngressClass(&ings[i]) { - continue - } - if isMinion(&ings[i]) { - master, err := lbc.FindMasterForMinion(&ings[i]) - if err != nil { - glog.Errorf("Ignoring Ingress %v(Minion): %v", ings[i].Name, err) - continue - } - if !lbc.configurator.HasMinion(master, &ings[i]) { - continue - } - mergeableIngresses, err := lbc.createMergableIngresses(master) - if err != nil { - glog.Errorf("Ignoring Ingress %v(Minion): %v", ings[i].Name, err) - continue - } + ingressExes, mergeableIngresses, virtualServersExes := lbc.createExtendedResources(resources) - mergableIngressesSlice = append(mergableIngressesSlice, mergeableIngresses) - continue - } - if !lbc.configurator.HasIngress(&ings[i]) { - continue - } - ingEx, err := lbc.createIngress(&ings[i]) - if err != nil { - glog.Errorf("Error updating endpoints for %v/%v: %v, skipping", &ings[i].Namespace, &ings[i].Name, err) - continue - } - ingExes = append(ingExes, ingEx) + if len(ingressExes) > 0 { + glog.V(3).Infof("Updating Endpoints for %v", ingressExes) + err = lbc.configurator.UpdateEndpoints(ingressExes) + if err != nil { + glog.Errorf("Error updating endpoints for %v: %v", ingressExes, err) } + } - if len(ingExes) > 0 { - glog.V(3).Infof("Updating Endpoints for %v", ingExes) - err = lbc.configurator.UpdateEndpoints(ingExes) - if err != nil { - glog.Errorf("Error updating endpoints for %v: %v", ingExes, err) - } + if len(mergeableIngresses) > 0 { + glog.V(3).Infof("Updating Endpoints for %v", mergeableIngresses) + err = lbc.configurator.UpdateEndpointsMergeableIngress(mergeableIngresses) + if err != nil { + glog.Errorf("Error updating endpoints for %v: %v", mergeableIngresses, err) } + } - if len(mergableIngressesSlice) > 0 { - glog.V(3).Infof("Updating Endpoints for %v", mergableIngressesSlice) - err = lbc.configurator.UpdateEndpointsMergeableIngress(mergableIngressesSlice) + if lbc.areCustomResourcesEnabled { + if len(virtualServersExes) > 0 { + glog.V(3).Infof("Updating endpoints for %v", virtualServersExes) + err := lbc.configurator.UpdateEndpointsForVirtualServers(virtualServersExes) if err != nil { - glog.Errorf("Error updating endpoints for %v: %v", mergableIngressesSlice, err) + glog.Errorf("Error updating endpoints for %v: %v", virtualServersExes, err) } } - if lbc.areCustomResourcesEnabled { - virtualServers := lbc.getVirtualServersForEndpoints(obj.(*api_v1.Endpoints)) - virtualServersExes := lbc.virtualServersToVirtualServerExes(virtualServers) + transportServers := lbc.getTransportServersForEndpoints(obj.(*api_v1.Endpoints)) + transportServerExes := lbc.transportServersToTransportServerExes(transportServers) - if len(virtualServersExes) > 0 { - glog.V(3).Infof("Updating endpoints for %v", virtualServersExes) - err := lbc.configurator.UpdateEndpointsForVirtualServers(virtualServersExes) - if err != nil { - glog.Errorf("Error updating endpoints for %v: %v", virtualServersExes, err) - } + if len(transportServerExes) > 0 { + glog.V(3).Infof("Updating endpoints for %v", transportServerExes) + err := lbc.configurator.UpdateEndpointsForTransportServers(transportServerExes) + if err != nil { + glog.Errorf("Error updating endpoints for %v: %v", transportServerExes, err) } + } + } +} - transportServers := lbc.getTransportServersForEndpoints(obj.(*api_v1.Endpoints)) - transportServerExes := lbc.transportServersToTransportServerExes(transportServers) +func (lbc *LoadBalancerController) createExtendedResources(resources []Resource) ([]*configs.IngressEx, []*configs.MergeableIngresses, []*configs.VirtualServerEx) { + var ingressExes []*configs.IngressEx + var mergeableIngresses []*configs.MergeableIngresses + var virtualServersExes []*configs.VirtualServerEx - if len(transportServerExes) > 0 { - glog.V(3).Infof("Updating endpoints for %v", transportServerExes) - err := lbc.configurator.UpdateEndpointsForTransportServers(transportServerExes) - if err != nil { - glog.Errorf("Error updating endpoints for %v: %v", transportServerExes, err) - } + for _, r := range resources { + switch impl := r.(type) { + case *FullVirtualServer: + vs := impl.VirtualServer + vsEx := lbc.createVirtualServerEx(vs, impl.VirtualServerRoutes) + virtualServersExes = append(virtualServersExes, vsEx) + case *FullIngress: + if impl.IsMaster { + mergeableIng := lbc.createMergeableIngresses(impl) + mergeableIngresses = append(mergeableIngresses, mergeableIng) + } else { + ingEx := lbc.createIngressEx(impl.Ingress, impl.ValidHosts, nil) + ingressExes = append(ingressExes, ingEx) } } } + + return ingressExes, mergeableIngresses, virtualServersExes } -func (lbc *LoadBalancerController) syncConfig(task task) { +func (lbc *LoadBalancerController) syncConfigMap(task task) { key := task.Key glog.V(3).Infof("Syncing configmap %v", key) @@ -630,29 +617,13 @@ func (lbc *LoadBalancerController) syncConfig(task task) { lbc.statusUpdater.SaveStatusFromExternalStatus(cfgm.Data["external-status-address"]) } - ingresses, mergeableIngresses := lbc.GetManagedIngresses() - ingExes := lbc.ingressesToIngressExes(ingresses) + resources := lbc.configuration.GetResources() - if lbc.reportStatusEnabled() { - err = lbc.statusUpdater.UpdateManagedAndMergeableIngresses(ingresses, mergeableIngresses) - if err != nil { - glog.V(3).Infof("error updating status on ConfigMap change: %v", err) - } - } + glog.V(3).Infof("Updating %v resources", len(resources)) - var virtualServerExes []*configs.VirtualServerEx - if lbc.areCustomResourcesEnabled { - virtualServers := lbc.getVirtualServers() - virtualServerExes = lbc.virtualServersToVirtualServerExes(virtualServers) - if lbc.reportVsVsrStatusEnabled() { - err = lbc.statusUpdater.UpdateVsVsrExternalEndpoints(virtualServers, lbc.getVirtualServerRoutes()) - if err != nil { - glog.V(3).Infof("error updating status on ConfigMap change: %v", err) - } - } - } + ingressExes, mergeableIngresses, virtualServerExes := lbc.createExtendedResources(resources) - warnings, updateErr := lbc.configurator.UpdateConfig(cfgParams, ingExes, mergeableIngresses, virtualServerExes) + warnings, updateErr := lbc.configurator.UpdateConfig(cfgParams, ingressExes, mergeableIngresses, virtualServerExes) eventTitle := "Updated" eventType := api_v1.EventTypeNormal @@ -663,145 +634,17 @@ func (lbc *LoadBalancerController) syncConfig(task task) { eventType = api_v1.EventTypeWarning eventWarningMessage = fmt.Sprintf("but was not applied: %v", updateErr) } - cmWarningMessage := eventWarningMessage if len(warnings) > 0 && updateErr == nil { - cmWarningMessage = "with warnings. Please check the logs" + eventWarningMessage = "with warnings. Please check the logs" } if configExists { cfgm := obj.(*api_v1.ConfigMap) - lbc.recorder.Eventf(cfgm, eventType, eventTitle, "Configuration from %v was updated %s", key, cmWarningMessage) - } - for _, ingEx := range ingExes { - lbc.recorder.Eventf(ingEx.Ingress, eventType, eventTitle, "Configuration for %v/%v was updated %s", - ingEx.Ingress.Namespace, ingEx.Ingress.Name, eventWarningMessage) - } - for _, mergeableIng := range mergeableIngresses { - master := mergeableIng.Master - lbc.recorder.Eventf(master.Ingress, eventType, eventTitle, "Configuration for %v/%v(Master) was updated %s", master.Ingress.Namespace, master.Ingress.Name, eventWarningMessage) - for _, minion := range mergeableIng.Minions { - lbc.recorder.Eventf(minion.Ingress, eventType, eventTitle, "Configuration for %v/%v(Minion) was updated %s", - minion.Ingress.Namespace, minion.Ingress.Name, eventWarningMessage) - } - } - for _, vsEx := range virtualServerExes { - vsEventType := eventType - vsEventTitle := eventTitle - vsEventWarningMessage := eventWarningMessage - vsState := conf_v1.StateValid - - if messages, ok := warnings[vsEx.VirtualServer]; ok && updateErr == nil { - vsEventType = api_v1.EventTypeWarning - vsEventTitle = "UpdatedWithWarning" - vsEventWarningMessage = fmt.Sprintf("with warning(s): %v", formatWarningMessages(messages)) - vsState = conf_v1.StateWarning - } - - msg := fmt.Sprintf("Configuration for %v/%v was updated %s", vsEx.VirtualServer.Namespace, vsEx.VirtualServer.Name, vsEventWarningMessage) - lbc.recorder.Eventf(vsEx.VirtualServer, vsEventType, vsEventTitle, msg) - - if updateErr != nil { - vsState = conf_v1.StateInvalid - } - - if lbc.reportVsVsrStatusEnabled() { - err = lbc.statusUpdater.UpdateVirtualServerStatus(vsEx.VirtualServer, vsState, vsEventTitle, msg) - - if err != nil { - glog.Errorf("Error when updating the status for VirtualServer %v/%v: %v", vsEx.VirtualServer.Namespace, vsEx.VirtualServer.Name, err) - } - } - - for _, vsr := range vsEx.VirtualServerRoutes { - vsrEventType := eventType - vsrEventTitle := eventTitle - vsrEventWarningMessage := eventWarningMessage - vsrState := conf_v1.StateValid - - if messages, ok := warnings[vsr]; ok && updateErr == nil { - vsrEventType = api_v1.EventTypeWarning - vsrEventTitle = "UpdatedWithWarning" - vsrEventWarningMessage = fmt.Sprintf("with warning(s): %v", formatWarningMessages(messages)) - vsrState = conf_v1.StateWarning - } - - msg := fmt.Sprintf("Configuration for %v/%v was updated %s", vsr.Namespace, vsr.Name, vsrEventWarningMessage) - lbc.recorder.Eventf(vsr, vsrEventType, vsrEventTitle, msg) - - if updateErr != nil { - vsrState = conf_v1.StateInvalid - } - - if lbc.reportVsVsrStatusEnabled() { - err = lbc.statusUpdater.UpdateVirtualServerRouteStatus(vsr, vsrState, vsrEventTitle, vsrEventWarningMessage) - - if err != nil { - glog.Errorf("Error when updating the status for VirtualServerRoute %v/%v: %v", vsr.Namespace, vsr.Name, err) - } - } - } - } -} - -// GetManagedIngresses gets Ingress resources that the IC is currently responsible for -func (lbc *LoadBalancerController) GetManagedIngresses() ([]networking.Ingress, map[string]*configs.MergeableIngresses) { - mergeableIngresses := make(map[string]*configs.MergeableIngresses) - var managedIngresses []networking.Ingress - ings, _ := lbc.ingressLister.List() - for i := range ings.Items { - ing := ings.Items[i] - if !lbc.HasCorrectIngressClass(&ing) { - continue - } - if isMinion(&ing) { - master, err := lbc.FindMasterForMinion(&ing) - if err != nil { - glog.Errorf("Ignoring Ingress %v(Minion): %v", ing.Name, err) - continue - } - if !lbc.configurator.HasIngress(master) { - continue - } - if _, exists := mergeableIngresses[master.Name]; !exists { - mergeableIngress, err := lbc.createMergableIngresses(master) - if err != nil { - glog.Errorf("Ignoring Ingress %v(Master): %v", master.Name, err) - continue - } - mergeableIngresses[master.Name] = mergeableIngress - } - continue - } - if !lbc.configurator.HasIngress(&ing) { - continue - } - managedIngresses = append(managedIngresses, ing) - } - return managedIngresses, mergeableIngresses -} - -func (lbc *LoadBalancerController) ingressesToIngressExes(ings []networking.Ingress) []*configs.IngressEx { - var ingExes []*configs.IngressEx - for i := range ings { - ingEx, err := lbc.createIngress(&ings[i]) - if err != nil { - continue - } - ingExes = append(ingExes, ingEx) - } - return ingExes -} - -func (lbc *LoadBalancerController) virtualServersToVirtualServerExes(virtualServers []*conf_v1.VirtualServer) []*configs.VirtualServerEx { - var virtualServersExes []*configs.VirtualServerEx - - for _, vs := range virtualServers { - vsEx, _ := lbc.createVirtualServer(vs) // ignoring VirtualServerRouteErrors - virtualServersExes = append(virtualServersExes, vsEx) + lbc.recorder.Eventf(cfgm, eventType, eventTitle, "Configuration from %v was updated %s", key, eventWarningMessage) } - return virtualServersExes + lbc.updateResourcesStatusAndEvents(resources, warnings, updateErr) } func (lbc *LoadBalancerController) transportServersToTransportServerExes(transportServers []*conf_v1alpha1.TransportServer) []*configs.TransportServerEx { @@ -823,19 +666,16 @@ func (lbc *LoadBalancerController) sync(task task) { } switch task.Kind { case ingress: - lbc.syncIng(task) - lbc.updateIngressMetrics() - case ingressMinion: - lbc.syncIngMinion(task) + lbc.syncIngress(task) lbc.updateIngressMetrics() case configMap: - lbc.syncConfig(task) + lbc.syncConfigMap(task) case endpoints: - lbc.syncEndpoint(task) + lbc.syncEndpoints(task) case secret: lbc.syncSecret(task) case service: - lbc.syncExternalService(task) + lbc.syncService(task) case virtualserver: lbc.syncVirtualServer(task) lbc.updateVirtualServerMetrics() @@ -883,82 +723,18 @@ func (lbc *LoadBalancerController) syncPolicy(task task) { // it is safe to ignore the error namespace, name, _ := ParseNamespaceName(key) - virtualServers := lbc.getVirtualServersForPolicy(namespace, name) - virtualServerExes := lbc.virtualServersToVirtualServerExes(virtualServers) + resources := lbc.configuration.FindResourcesForPolicy(namespace, name) + // Ingresses don't have policies + _, _, virtualServerExes := lbc.createExtendedResources(resources) if len(virtualServerExes) == 0 { return } warnings, updateErr := lbc.configurator.AddOrUpdateVirtualServers(virtualServerExes) + lbc.updateResourcesStatusAndEvents(resources, warnings, updateErr) // Note: updating the status of a policy based on a reload is not needed. - - eventTitle := "Updated" - eventType := api_v1.EventTypeNormal - eventWarningMessage := "" - state := conf_v1.StateValid - - if updateErr != nil { - eventTitle = "UpdatedWithError" - eventType = api_v1.EventTypeWarning - eventWarningMessage = fmt.Sprintf("but was not applied: %v", updateErr) - state = conf_v1.StateInvalid - } - - for _, vsEx := range virtualServerExes { - vsEventType := eventType - vsEventTitle := eventTitle - vsEventWarningMessage := eventWarningMessage - vsState := state - - if messages, ok := warnings[vsEx.VirtualServer]; ok && updateErr == nil { - vsEventType = api_v1.EventTypeWarning - vsEventTitle = "UpdatedWithWarning" - vsEventWarningMessage = fmt.Sprintf("with warning(s): %v", formatWarningMessages(messages)) - vsState = conf_v1.StateWarning - } - - msg := fmt.Sprintf("Configuration for %v/%v was updated %s", vsEx.VirtualServer.Namespace, vsEx.VirtualServer.Name, - vsEventWarningMessage) - lbc.recorder.Eventf(vsEx.VirtualServer, vsEventType, vsEventTitle, msg) - - if lbc.reportVsVsrStatusEnabled() { - err = lbc.statusUpdater.UpdateVirtualServerStatus(vsEx.VirtualServer, vsState, vsEventTitle, msg) - - if err != nil { - glog.Errorf("Error when updating the status for VirtualServer %v/%v: %v", vsEx.VirtualServer.Namespace, - vsEx.VirtualServer.Name, err) - } - } - - for _, vsr := range vsEx.VirtualServerRoutes { - vsrEventType := eventType - vsrEventTitle := eventTitle - vsrEventWarningMessage := eventWarningMessage - vsrState := state - - if messages, ok := warnings[vsr]; ok && updateErr == nil { - vsrEventType = api_v1.EventTypeWarning - vsrEventTitle = "UpdatedWithWarning" - vsrEventWarningMessage = fmt.Sprintf("with warning(s): %v", formatWarningMessages(messages)) - vsrState = conf_v1.StateWarning - } - - msg := fmt.Sprintf("Configuration for %v/%v was added or updated %s", vsr.Namespace, vsr.Name, vsrEventWarningMessage) - lbc.recorder.Eventf(vsr, vsrEventType, vsrEventTitle, msg) - - if lbc.reportVsVsrStatusEnabled() { - virtualServersForVSR := findVirtualServersForVirtualServerRoute(lbc.getVirtualServers(), vsr) - err = lbc.statusUpdater.UpdateVirtualServerRouteStatusWithReferencedBy(vsr, vsrState, vsrEventTitle, msg, virtualServersForVSR) - - if err != nil { - glog.Errorf("Error when updating the status for VirtualServerRoute %v/%v: %v", vsr.Namespace, vsr.Name, err) - } - } - } - } - } func (lbc *LoadBalancerController) syncTransportServer(task task) { @@ -1086,219 +862,363 @@ func (lbc *LoadBalancerController) syncVirtualServer(task task) { lbc.syncQueue.Requeue(task, err) return } - previousVSRs := lbc.configurator.GetVirtualServerRoutesForVirtualServer(key) + + var changes []ResourceChange + var problems []ConfigurationProblem + if !vsExists { glog.V(2).Infof("Deleting VirtualServer: %v\n", key) - err := lbc.configurator.DeleteVirtualServer(key) - if err != nil { - glog.Errorf("Error when deleting configuration for %v: %v", key, err) - } - reason := "NoVirtualServerFound" - for _, vsr := range previousVSRs { - msg := fmt.Sprintf("No VirtualServer references VirtualServerRoute %v/%v", vsr.Namespace, vsr.Name) - lbc.recorder.Eventf(vsr, api_v1.EventTypeWarning, reason, msg) - - if lbc.reportVsVsrStatusEnabled() { - virtualServersForVSR := []*conf_v1.VirtualServer{} - err = lbc.statusUpdater.UpdateVirtualServerRouteStatusWithReferencedBy(vsr, conf_v1.StateInvalid, reason, msg, virtualServersForVSR) - if err != nil { - glog.Errorf("Error when updating the status for VirtualServerRoute %v/%v: %v", vsr.Namespace, vsr.Name, err) - } - } + changes, problems = lbc.configuration.DeleteVirtualServer(key) + } else { + glog.V(2).Infof("Adding or Updating VirtualServer: %v\n", key) - } - return + vs := obj.(*conf_v1.VirtualServer) + changes, problems = lbc.configuration.AddOrUpdateVirtualServer(vs) } - glog.V(2).Infof("Adding or Updating VirtualServer: %v\n", key) - vs := obj.(*conf_v1.VirtualServer) + lbc.processChanges(changes) + lbc.processProblems(problems) +} - validationErr := lbc.virtualServerValidator.ValidateVirtualServer(vs) - if validationErr != nil { - err := lbc.configurator.DeleteVirtualServer(key) - if err != nil { - glog.Errorf("Error when deleting configuration for %v: %v", key, err) - } +func (lbc *LoadBalancerController) processProblems(problems []ConfigurationProblem) { + glog.V(3).Infof("Processing %v problems", len(problems)) - reason := "Rejected" - msg := fmt.Sprintf("VirtualServer %v is invalid and was rejected: %v", key, validationErr) + for _, p := range problems { + eventType := api_v1.EventTypeWarning + lbc.recorder.Event(p.Object, eventType, p.Reason, p.Message) - lbc.recorder.Eventf(vs, api_v1.EventTypeWarning, reason, msg) if lbc.reportVsVsrStatusEnabled() { - err = lbc.statusUpdater.UpdateVirtualServerStatus(vs, conf_v1.StateInvalid, reason, msg) - } - - reason = "NoVirtualServerFound" - for _, vsr := range previousVSRs { - msg := fmt.Sprintf("No VirtualServer references VirtualServerRoute %v/%v", vsr.Namespace, vsr.Name) - lbc.recorder.Eventf(vsr, api_v1.EventTypeWarning, reason, msg) + state := conf_v1.StateWarning + if p.IsError { + state = conf_v1.StateInvalid + } - if lbc.reportVsVsrStatusEnabled() { - virtualServersForVSR := []*conf_v1.VirtualServer{} - err = lbc.statusUpdater.UpdateVirtualServerRouteStatusWithReferencedBy(vsr, conf_v1.StateInvalid, reason, msg, virtualServersForVSR) + switch obj := p.Object.(type) { + case *networking.Ingress: + err := lbc.statusUpdater.ClearIngressStatus(*obj) + if err != nil { + glog.V(3).Infof("Error when updating the status for Ingress %v/%v: %v", obj.Namespace, obj.Name, err) + } + case *conf_v1.VirtualServer: + err := lbc.statusUpdater.UpdateVirtualServerStatus(obj, state, p.Reason, p.Message) + if err != nil { + glog.Errorf("Error when updating the status for VirtualServer %v/%v: %v", obj.Namespace, obj.Name, err) + } + case *conf_v1.VirtualServerRoute: + var emptyVSes []*conf_v1.VirtualServer + err := lbc.statusUpdater.UpdateVirtualServerRouteStatusWithReferencedBy(obj, state, p.Reason, p.Message, emptyVSes) if err != nil { - glog.Errorf("Error when updating the status for VirtualServerRoute %v/%v: %v", vsr.Namespace, vsr.Name, err) + glog.Errorf("Error when updating the status for VirtualServerRoute %v/%v: %v", obj.Namespace, obj.Name, err) } } } - return } +} - var handledVSRs []*conf_v1.VirtualServerRoute +func (lbc *LoadBalancerController) processChanges(changes []ResourceChange) { + glog.V(3).Infof("Processing %v changes", len(changes)) - vsEx, vsrErrors := lbc.createVirtualServer(vs) + for _, c := range changes { + if c.Op == AddOrUpdate { + switch impl := c.Resource.(type) { + case *FullVirtualServer: + vsEx := lbc.createVirtualServerEx(impl.VirtualServer, impl.VirtualServerRoutes) - for _, vsrError := range vsrErrors { - lbc.recorder.Eventf(vs, api_v1.EventTypeWarning, "IgnoredVirtualServerRoute", "Ignored VirtualServerRoute %v: %v", vsrError.VirtualServerRouteNsName, vsrError.Error) - if vsrError.VirtualServerRoute != nil { - handledVSRs = append(handledVSRs, vsrError.VirtualServerRoute) - lbc.recorder.Eventf(vsrError.VirtualServerRoute, api_v1.EventTypeWarning, "Ignored", "Ignored by VirtualServer %v/%v: %v", vs.Namespace, vs.Name, vsrError.Error) - } - } + warnings, addOrUpdateErr := lbc.configurator.AddOrUpdateVirtualServer(vsEx) + lbc.updateVirtualServerStatusAndEvents(impl, warnings, addOrUpdateErr) + case *FullIngress: + if impl.IsMaster { + mergeableIng := lbc.createMergeableIngresses(impl) - warnings, addErr := lbc.configurator.AddOrUpdateVirtualServer(vsEx) + addOrUpdateErr := lbc.configurator.AddOrUpdateMergeableIngress(mergeableIng) + lbc.updateMergeableIngressStatusAndEvents(impl, addOrUpdateErr) + } else { + // for regular Ingress, validMinionPaths is nil + ingEx := lbc.createIngressEx(impl.Ingress, impl.ValidHosts, nil) - eventTitle := "AddedOrUpdated" - eventType := api_v1.EventTypeNormal - eventWarningMessage := "" - state := conf_v1.StateValid + addOrUpdateErr := lbc.configurator.AddOrUpdateIngress(ingEx) + lbc.updateRegularIngressStatusAndEvents(impl, addOrUpdateErr) + } + } + } else if c.Op == Delete { + switch impl := c.Resource.(type) { + case *FullVirtualServer: + key := getResourceKey(&impl.VirtualServer.ObjectMeta) - if addErr != nil { - eventTitle = "AddedOrUpdatedWithError" - eventType = api_v1.EventTypeWarning - eventWarningMessage = fmt.Sprintf("but was not applied: %v", addErr) - state = conf_v1.StateInvalid - } + deleteErr := lbc.configurator.DeleteVirtualServer(key) + if deleteErr != nil { + glog.Errorf("Error when deleting configuration for VirtualServer %v: %v", key, deleteErr) + } - vsEventType := eventType - vsEventTitle := eventTitle - vsEventWarningMessage := eventWarningMessage + _, vsExists, err := lbc.virtualServerLister.GetByKey(key) + if err != nil { + glog.Errorf("Error when getting VirtualServer for %v: %v", key, err) + } - if messages, ok := warnings[vsEx.VirtualServer]; ok && addErr == nil { - vsEventType = api_v1.EventTypeWarning - vsEventTitle = "AddedOrUpdatedWithWarning" - vsEventWarningMessage = fmt.Sprintf("with warning(s): %v", formatWarningMessages(messages)) - state = conf_v1.StateWarning - } + if vsExists { + lbc.UpdateVirtualServerStatusAndEventsOnDelete(impl, c.Error, deleteErr) + } + case *FullIngress: + key := getResourceKey(&impl.Ingress.ObjectMeta) - msg := fmt.Sprintf("Configuration for %v was added or updated %s", key, vsEventWarningMessage) - lbc.recorder.Eventf(vs, vsEventType, vsEventTitle, msg) + glog.V(2).Infof("Deleting Ingress: %v\n", key) - if lbc.reportVsVsrStatusEnabled() { - err = lbc.statusUpdater.UpdateVirtualServerStatus(vs, state, vsEventTitle, msg) + deleteErr := lbc.configurator.DeleteIngress(key) + if deleteErr != nil { + glog.Errorf("Error when deleting configuration for Ingress %v: %v", key, deleteErr) + } - if err != nil { - glog.Errorf("Error when updating the status for VirtualServer %v/%v: %v", vs.Namespace, vs.Name, err) + _, ingExists, err := lbc.ingressLister.GetByKeySafe(key) + if err != nil { + glog.Errorf("Error when getting Ingress for %v: %v", key, err) + } + + if ingExists { + lbc.UpdateIngressStatusAndEventsOnDelete(impl, c.Error, deleteErr) + } + } } } +} - for _, vsr := range vsEx.VirtualServerRoutes { - vsrEventType := eventType - vsrEventTitle := eventTitle - vsrEventWarningMessage := eventWarningMessage - state := conf_v1.StateValid +func (lbc *LoadBalancerController) UpdateVirtualServerStatusAndEventsOnDelete(fullVS *FullVirtualServer, changeError string, deleteErr error) { + eventType := api_v1.EventTypeWarning + eventTitle := "Rejected" + eventWarningMessage := "" + state := "" - if messages, ok := warnings[vsr]; ok && addErr == nil { - vsrEventType = api_v1.EventTypeWarning - vsrEventTitle = "AddedOrUpdatedWithWarning" - vsrEventWarningMessage = fmt.Sprintf("with warning(s): %v", formatWarningMessages(messages)) - state = conf_v1.StateWarning - } - msg := fmt.Sprintf("Configuration for %v/%v was added or updated %s", vsr.Namespace, vsr.Name, vsrEventWarningMessage) - lbc.recorder.Eventf(vsr, vsrEventType, vsrEventTitle, msg) + // VirtualServer either became invalid or lost its host + if changeError != "" { + eventWarningMessage = fmt.Sprintf("with error: %s", changeError) + state = conf_v1.StateInvalid + } else if len(fullVS.Warnings) > 0 { + eventWarningMessage = fmt.Sprintf("with warning(s): %s", formatWarningMessages(fullVS.Warnings)) + state = conf_v1.StateWarning + } - if addErr != nil { + // we don't need to report anything if eventWarningMessage is empty + // in that case, the resource was deleted because its class became incorrect + // (some other Ingress Controller will handle it) + if eventWarningMessage != "" { + if deleteErr != nil { + eventType = api_v1.EventTypeWarning + eventTitle = "RejectedWithError" + eventWarningMessage = fmt.Sprintf("%s; but was not applied: %v", eventWarningMessage, deleteErr) state = conf_v1.StateInvalid } + msg := fmt.Sprintf("VirtualServer %s was rejected %s", getResourceKey(&fullVS.VirtualServer.ObjectMeta), eventWarningMessage) + lbc.recorder.Eventf(fullVS.VirtualServer, eventType, eventTitle, msg) + if lbc.reportVsVsrStatusEnabled() { - vss := []*conf_v1.VirtualServer{vs} - err = lbc.statusUpdater.UpdateVirtualServerRouteStatusWithReferencedBy(vsr, state, vsrEventTitle, msg, vss) + err := lbc.statusUpdater.UpdateVirtualServerStatus(fullVS.VirtualServer, state, eventTitle, msg) if err != nil { - glog.Errorf("Error when updating the status for VirtualServerRoute %v/%v: %v", vsr.Namespace, vsr.Name, err) + glog.Errorf("Error when updating the status for VirtualServer %v/%v: %v", fullVS.VirtualServer.Namespace, fullVS.VirtualServer.Name, err) } } + } + + // for delete, no need to report VirtualServerRoutes + // for each VSR, a dedicated problem exists +} + +func (lbc *LoadBalancerController) UpdateIngressStatusAndEventsOnDelete(fullIng *FullIngress, changeError string, deleteErr error) { + eventTitle := "Rejected" + eventWarningMessage := "" - handledVSRs = append(handledVSRs, vsr) + // Ingress either became invalid or lost all its hosts + if changeError != "" { + eventWarningMessage = fmt.Sprintf("with error: %s", changeError) + } else if len(fullIng.Warnings) > 0 { + eventWarningMessage = fmt.Sprintf("with warning(s): %s", formatWarningMessages(fullIng.Warnings)) } - orphanedVSRs := findOrphanedVirtualServerRoutes(previousVSRs, handledVSRs) - reason := "NoVirtualServerFound" - for _, vsr := range orphanedVSRs { - msg := fmt.Sprintf("No VirtualServer references VirtualServerRoute %v/%v", vsr.Namespace, vsr.Name) - lbc.recorder.Eventf(vsr, api_v1.EventTypeWarning, reason, msg) - if lbc.reportVsVsrStatusEnabled() { - var emptyVSes []*conf_v1.VirtualServer - err := lbc.statusUpdater.UpdateVirtualServerRouteStatusWithReferencedBy(vsr, conf_v1.StateInvalid, reason, msg, emptyVSes) + // we don't need to report anything if eventWarningMessage is empty + // in that case, the resource was deleted because its class became incorrect + // (some other Ingress Controller will handle it) + if eventWarningMessage != "" { + if deleteErr != nil { + eventTitle = "RejectedWithError" + eventWarningMessage = fmt.Sprintf("%s; but was not applied: %v", eventWarningMessage, deleteErr) + } + + lbc.recorder.Eventf(fullIng.Ingress, api_v1.EventTypeWarning, eventTitle, "%v was rejected: %v", getResourceKey(&fullIng.Ingress.ObjectMeta), eventWarningMessage) + if lbc.reportStatusEnabled() { + err := lbc.statusUpdater.ClearIngressStatus(*fullIng.Ingress) if err != nil { - glog.Errorf("Error when updating the status for VirtualServerRoute %v/%v: %v", vsr.Namespace, vsr.Name, err) + glog.V(3).Infof("Error clearing Ingress status: %v", err) } } } + + // for delete, no need to report minions + // for each minion, a dedicated problem exists } -func findOrphanedVirtualServerRoutes(previousVSRs []*conf_v1.VirtualServerRoute, handledVSRs []*conf_v1.VirtualServerRoute) []*conf_v1.VirtualServerRoute { - var orphanedVSRs []*conf_v1.VirtualServerRoute - for _, prev := range previousVSRs { - isIn := false - prevKey := fmt.Sprintf("%s/%s", prev.Namespace, prev.Name) - for _, handled := range handledVSRs { - handledKey := fmt.Sprintf("%s/%s", handled.Namespace, handled.Name) - if prevKey == handledKey { - isIn = true - break +func (lbc *LoadBalancerController) updateResourcesStatusAndEvents(resources []Resource, warnings configs.Warnings, operationErr error) { + for _, r := range resources { + switch impl := r.(type) { + case *FullVirtualServer: + lbc.updateVirtualServerStatusAndEvents(impl, warnings, operationErr) + case *FullIngress: + if impl.IsMaster { + lbc.updateMergeableIngressStatusAndEvents(impl, operationErr) + } else { + lbc.updateRegularIngressStatusAndEvents(impl, operationErr) } } - if !isIn { - orphanedVSRs = append(orphanedVSRs, prev) - } } - return orphanedVSRs } -func (lbc *LoadBalancerController) syncVirtualServerRoute(task task) { - key := task.Key +func (lbc *LoadBalancerController) updateMergeableIngressStatusAndEvents(fullIng *FullIngress, operationErr error) { + eventType := api_v1.EventTypeNormal + eventTitle := "AddedOrUpdated" + eventWarningMessage := "" - obj, exists, err := lbc.virtualServerRouteLister.GetByKey(key) - if err != nil { - lbc.syncQueue.Requeue(task, err) - return + if len(fullIng.Warnings) > 0 { + eventType = api_v1.EventTypeWarning + eventTitle = "AddedOrUpdatedWithWarning" + eventWarningMessage = fmt.Sprintf("with warning(s): %s", formatWarningMessages(fullIng.Warnings)) } - if !exists { - glog.V(2).Infof("Deleting VirtualServerRoute: %v\n", key) - - lbc.enqueueVirtualServersForVirtualServerRouteKey(key) - return + if operationErr != nil { + eventType = api_v1.EventTypeWarning + eventTitle = "AddedOrUpdatedWithError" + eventWarningMessage = fmt.Sprintf("%s; but was not applied: %v", eventWarningMessage, operationErr) } - glog.V(2).Infof("Adding or Updating VirtualServerRoute: %v\n", key) + msg := fmt.Sprintf("Configuration for %v was added or updated %s", getResourceKey(&fullIng.Ingress.ObjectMeta), eventWarningMessage) + lbc.recorder.Eventf(fullIng.Ingress, eventType, eventTitle, msg) - vsr := obj.(*conf_v1.VirtualServerRoute) + for _, fm := range fullIng.Minions { + minionEventType := api_v1.EventTypeNormal + minionEventTitle := "AddedOrUpdated" + minionEventWarningMessage := "" - validationErr := lbc.virtualServerValidator.ValidateVirtualServerRoute(vsr) - if validationErr != nil { - reason := "Rejected" - msg := fmt.Sprintf("VirtualServerRoute %s is invalid and was rejected: %v", key, validationErr) - lbc.recorder.Eventf(vsr, api_v1.EventTypeWarning, reason, msg) - if lbc.reportVsVsrStatusEnabled() { - err = lbc.statusUpdater.UpdateVirtualServerRouteStatus(vsr, conf_v1.StateInvalid, reason, msg) - if err != nil { - glog.Errorf("Error when updating the status for VirtualServerRoute %v/%v: %v", vsr.Namespace, vsr.Name, err) - } + minionChangeWarnings := fullIng.ChildWarnings[getResourceKey(&fm.Ingress.ObjectMeta)] + if len(minionChangeWarnings) > 0 { + minionEventType = api_v1.EventTypeWarning + minionEventTitle = "AddedOrUpdatedWithWarning" + minionEventWarningMessage = fmt.Sprintf("with warning(s): %s", formatWarningMessages(minionChangeWarnings)) + } + + if operationErr != nil { + minionEventType = api_v1.EventTypeWarning + minionEventTitle = "AddedOrUpdatedWithError" + minionEventWarningMessage = fmt.Sprintf("%s; but was not applied: %v", minionEventWarningMessage, operationErr) + } + + minionMsg := fmt.Sprintf("Configuration for %v/%v was added or updated %s", fm.Ingress.Namespace, fm.Ingress.Name, minionEventWarningMessage) + lbc.recorder.Eventf(fm.Ingress, minionEventType, minionEventTitle, minionMsg) + } + + if lbc.reportStatusEnabled() { + ings := []networking.Ingress{*fullIng.Ingress} + + for _, fm := range fullIng.Minions { + ings = append(ings, *fm.Ingress) + } + + err := lbc.statusUpdater.BulkUpdateIngressStatus(ings) + if err != nil { + glog.V(3).Infof("error updating ing status: %v", err) + } + } +} + +func (lbc *LoadBalancerController) updateRegularIngressStatusAndEvents(fullIng *FullIngress, operationErr error) { + eventType := api_v1.EventTypeNormal + eventTitle := "AddedOrUpdated" + eventWarningMessage := "" + + if len(fullIng.Warnings) > 0 { + eventType = api_v1.EventTypeWarning + eventTitle = "AddedOrUpdatedWithWarning" + eventWarningMessage = fmt.Sprintf("with warning(s): %s", formatWarningMessages(fullIng.Warnings)) + } + + if operationErr != nil { + eventType = api_v1.EventTypeWarning + eventTitle = "AddedOrUpdatedWithError" + eventWarningMessage = fmt.Sprintf("%s; but was not applied: %v", eventWarningMessage, operationErr) + } + + msg := fmt.Sprintf("Configuration for %v was added or updated %s", getResourceKey(&fullIng.Ingress.ObjectMeta), eventWarningMessage) + lbc.recorder.Eventf(fullIng.Ingress, eventType, eventTitle, msg) + + if lbc.reportStatusEnabled() { + err := lbc.statusUpdater.UpdateIngressStatus(*fullIng.Ingress) + if err != nil { + glog.V(3).Infof("error updating ing status: %v", err) + } + } +} + +func (lbc *LoadBalancerController) updateVirtualServerStatusAndEvents(fullVS *FullVirtualServer, warnings configs.Warnings, operationErr error) { + eventType := api_v1.EventTypeNormal + eventTitle := "AddedOrUpdated" + eventWarningMessage := "" + state := conf_v1.StateValid + + if len(fullVS.Warnings) > 0 { + eventType = api_v1.EventTypeWarning + eventTitle = "AddedOrUpdatedWithWarning" + eventWarningMessage = fmt.Sprintf("with warning(s): %s", formatWarningMessages(fullVS.Warnings)) + state = conf_v1.StateWarning + } + + if messages, ok := warnings[fullVS.VirtualServer]; ok { + eventType = api_v1.EventTypeWarning + eventTitle = "AddedOrUpdatedWithWarning" + eventWarningMessage = fmt.Sprintf("%s; with warning(s): %v", eventWarningMessage, formatWarningMessages(messages)) + state = conf_v1.StateWarning + } + + if operationErr != nil { + eventType = api_v1.EventTypeWarning + eventTitle = "AddedOrUpdatedWithError" + eventWarningMessage = fmt.Sprintf("%s; but was not applied: %v", eventWarningMessage, operationErr) + state = conf_v1.StateInvalid + } + + msg := fmt.Sprintf("Configuration for %v was added or updated %s", getResourceKey(&fullVS.VirtualServer.ObjectMeta), eventWarningMessage) + lbc.recorder.Eventf(fullVS.VirtualServer, eventType, eventTitle, msg) + + if lbc.reportVsVsrStatusEnabled() { + err := lbc.statusUpdater.UpdateVirtualServerStatus(fullVS.VirtualServer, state, eventTitle, msg) + if err != nil { + glog.Errorf("Error when updating the status for VirtualServer %v/%v: %v", fullVS.VirtualServer.Namespace, fullVS.VirtualServer.Name, err) } } - vsCount := lbc.enqueueVirtualServersForVirtualServerRouteKey(key) - if vsCount == 0 { - reason := "NoVirtualServersFound" - msg := fmt.Sprintf("No VirtualServer references VirtualServerRoute %s", key) - lbc.recorder.Eventf(vsr, api_v1.EventTypeWarning, reason, msg) + for _, vsr := range fullVS.VirtualServerRoutes { + vsrEventType := api_v1.EventTypeNormal + vsrEventTitle := "AddedOrUpdated" + vsrEventWarningMessage := "" + vsrState := conf_v1.StateValid + + if messages, ok := warnings[vsr]; ok { + vsrEventType = api_v1.EventTypeWarning + vsrEventTitle = "AddedOrUpdatedWithWarning" + vsrEventWarningMessage = fmt.Sprintf("with warning(s): %v", formatWarningMessages(messages)) + vsrState = conf_v1.StateWarning + } + + if operationErr != nil { + vsrEventType = api_v1.EventTypeWarning + vsrEventTitle = "AddedOrUpdatedWithError" + vsrEventWarningMessage = fmt.Sprintf("%s; but was not applied: %v", vsrEventWarningMessage, operationErr) + vsrState = conf_v1.StateInvalid + } + + msg := fmt.Sprintf("Configuration for %v/%v was added or updated %s", vsr.Namespace, vsr.Name, vsrEventWarningMessage) + lbc.recorder.Eventf(vsr, vsrEventType, vsrEventTitle, msg) if lbc.reportVsVsrStatusEnabled() { - err = lbc.statusUpdater.UpdateVirtualServerRouteStatus(vsr, conf_v1.StateInvalid, reason, msg) + vss := []*conf_v1.VirtualServer{fullVS.VirtualServer} + err := lbc.statusUpdater.UpdateVirtualServerRouteStatusWithReferencedBy(vsr, vsrState, vsrEventTitle, msg, vss) if err != nil { glog.Errorf("Error when updating the status for VirtualServerRoute %v/%v: %v", vsr.Namespace, vsr.Name, err) } @@ -1306,40 +1226,33 @@ func (lbc *LoadBalancerController) syncVirtualServerRoute(task task) { } } -func (lbc *LoadBalancerController) syncIngMinion(task task) { +func (lbc *LoadBalancerController) syncVirtualServerRoute(task task) { key := task.Key - obj, ingExists, err := lbc.ingressLister.Store.GetByKey(key) + obj, exists, err := lbc.virtualServerRouteLister.GetByKey(key) if err != nil { lbc.syncQueue.Requeue(task, err) return } - if !ingExists { - glog.V(2).Infof("Minion was deleted: %v\n", key) - return - } - glog.V(2).Infof("Adding or Updating Minion: %v\n", key) + var changes []ResourceChange + var problems []ConfigurationProblem - minion := obj.(*networking.Ingress) + if !exists { + glog.V(2).Infof("Deleting VirtualServerRoute: %v\n", key) - master, err := lbc.FindMasterForMinion(minion) - if err != nil { - lbc.syncQueue.RequeueAfter(task, err, 5*time.Second) - return - } + changes, problems = lbc.configuration.DeleteVirtualServerRoute(key) + } else { + glog.V(2).Infof("Adding or Updating VirtualServerRoute: %v\n", key) - _, err = lbc.createIngress(minion) - if err != nil { - lbc.syncQueue.RequeueAfter(task, err, 5*time.Second) - if !lbc.configurator.HasMinion(master, minion) { - return - } + vsr := obj.(*conf_v1.VirtualServerRoute) + changes, problems = lbc.configuration.AddOrUpdateVirtualServerRoute(vsr) } - lbc.syncQueue.Enqueue(master) + lbc.processChanges(changes) + lbc.processProblems(problems) } -func (lbc *LoadBalancerController) syncIng(task task) { +func (lbc *LoadBalancerController) syncIngress(task task) { key := task.Key ing, ingExists, err := lbc.ingressLister.GetByKeySafe(key) if err != nil { @@ -1347,82 +1260,21 @@ func (lbc *LoadBalancerController) syncIng(task task) { return } + var changes []ResourceChange + var problems []ConfigurationProblem + if !ingExists { glog.V(2).Infof("Deleting Ingress: %v\n", key) - err := lbc.configurator.DeleteIngress(key) - if err != nil { - glog.Errorf("Error when deleting configuration for %v: %v", key, err) - } + changes, problems = lbc.configuration.DeleteIngress(key) } else { glog.V(2).Infof("Adding or Updating Ingress: %v\n", key) - if isMaster(ing) { - mergeableIngExs, err := lbc.createMergableIngresses(ing) - if err != nil { - // we need to requeue because an error can occur even if the master is valid - // otherwise, we will not be able to generate the config until there is change - // in the master or minions. - lbc.syncQueue.RequeueAfter(task, err, 5*time.Second) - lbc.recorder.Eventf(ing, api_v1.EventTypeWarning, "Rejected", "%v was rejected: %v", key, err) - if lbc.reportStatusEnabled() { - err = lbc.statusUpdater.ClearIngressStatus(*ing) - if err != nil { - glog.V(3).Infof("error clearing ing status: %v", err) - } - } - return - } - addErr := lbc.configurator.AddOrUpdateMergeableIngress(mergeableIngExs) - - // record correct eventType and message depending on the error - eventTitle := "AddedOrUpdated" - eventType := api_v1.EventTypeNormal - eventWarningMessage := "" - - if addErr != nil { - eventTitle = "AddedOrUpdatedWithError" - eventType = api_v1.EventTypeWarning - eventWarningMessage = fmt.Sprintf("but was not applied: %v", addErr) - } - lbc.recorder.Eventf(ing, eventType, eventTitle, "Configuration for %v(Master) was added or updated %s", key, eventWarningMessage) - for _, minion := range mergeableIngExs.Minions { - lbc.recorder.Eventf(minion.Ingress, eventType, eventTitle, "Configuration for %v/%v(Minion) was added or updated %s", minion.Ingress.Namespace, minion.Ingress.Name, eventWarningMessage) - } - - if lbc.reportStatusEnabled() { - err = lbc.statusUpdater.UpdateMergableIngresses(mergeableIngExs) - if err != nil { - glog.V(3).Infof("error updating ingress status: %v", err) - } - } - return - } - ingEx, err := lbc.createIngress(ing) - if err != nil { - lbc.recorder.Eventf(ing, api_v1.EventTypeWarning, "Rejected", "%v was rejected: %v", key, err) - if lbc.reportStatusEnabled() { - err = lbc.statusUpdater.ClearIngressStatus(*ing) - if err != nil { - glog.V(3).Infof("error clearing ing status: %v", err) - } - } - return - } - - err = lbc.configurator.AddOrUpdateIngress(ingEx) - if err != nil { - lbc.recorder.Eventf(ing, api_v1.EventTypeWarning, "AddedOrUpdatedWithError", "Configuration for %v was added or updated, but not applied: %v", key, err) - } else { - lbc.recorder.Eventf(ing, api_v1.EventTypeNormal, "AddedOrUpdated", "Configuration for %v was added or updated", key) - } - if lbc.reportStatusEnabled() { - err = lbc.statusUpdater.UpdateIngressStatus(*ing) - if err != nil { - glog.V(3).Infof("error updating ing status: %v", err) - } - } + changes, problems = lbc.configuration.AddOrUpdateIngress(ing) } + + lbc.processChanges(changes) + lbc.processProblems(problems) } func (lbc *LoadBalancerController) updateIngressMetrics() { @@ -1438,43 +1290,86 @@ func (lbc *LoadBalancerController) updateVirtualServerMetrics() { lbc.metricsCollector.SetVirtualServerRoutes(vsrCount) } -// syncExternalService does not sync all services. -// We only watch the Service specified by the external-service flag. -func (lbc *LoadBalancerController) syncExternalService(task task) { +func (lbc *LoadBalancerController) syncService(task task) { key := task.Key + glog.V(3).Infof("Syncing service %v", key) + obj, exists, err := lbc.svcLister.GetByKey(key) if err != nil { lbc.syncQueue.Requeue(task, err) return } - statusIngs, mergableIngs := lbc.GetManagedIngresses() - if !exists { - // service got removed - lbc.statusUpdater.ClearStatusFromExternalService() - } else { - // service added or updated - lbc.statusUpdater.SaveStatusFromExternalService(obj.(*api_v1.Service)) - } - if lbc.reportStatusEnabled() { - err = lbc.statusUpdater.UpdateManagedAndMergeableIngresses(statusIngs, mergableIngs) - if err != nil { - glog.Errorf("error updating ingress status in syncExternalService: %v", err) + + // First case: the service is the external service for the Ingress Controller + // In that case we need to update the statuses of all resources + + if lbc.IsExternalServiceKeyForStatus(key) { + + if !exists { + // service got removed + lbc.statusUpdater.ClearStatusFromExternalService() + } else { + // service added or updated + lbc.statusUpdater.SaveStatusFromExternalService(obj.(*api_v1.Service)) } - } - if lbc.areCustomResourcesEnabled && lbc.reportVsVsrStatusEnabled() { - err = lbc.statusUpdater.UpdateVsVsrExternalEndpoints(lbc.getVirtualServers(), lbc.getVirtualServerRoutes()) - if err != nil { - glog.V(3).Infof("error updating VirtualServer/VirtualServerRoute status in syncExternalService: %v", err) + if lbc.reportStatusEnabled() { + ingresses := lbc.configuration.GetResourcesWithFilter(resourceFilter{Ingresses: true}) + + glog.V(3).Infof("Updating status for %v Ingresses", len(ingresses)) + + err := lbc.statusUpdater.UpdateExternalEndpointsForResources(ingresses) + if err != nil { + glog.Errorf("error updating ingress status in syncService: %v", err) + } + } + + if lbc.areCustomResourcesEnabled && lbc.reportVsVsrStatusEnabled() { + virtualServers := lbc.configuration.GetResourcesWithFilter(resourceFilter{VirtualServers: true}) + + glog.V(3).Infof("Updating status for %v VirtualServers", len(virtualServers)) + + err := lbc.statusUpdater.UpdateExternalEndpointsForResources(virtualServers) + if err != nil { + glog.V(3).Infof("error updating VirtualServer/VirtualServerRoute status in syncService: %v", err) + } } + + // we don't return here because technically the same service could be used in the second case + } + + // Second case: the service is referenced by some resources in the cluster + + // it is safe to ignore the error + namespace, name, _ := ParseNamespaceName(key) + + resources := lbc.configuration.FindResourcesForService(namespace, name) + + if len(resources) == 0 { + return } + + glog.V(3).Infof("Updating %v resources", len(resources)) + + ingressExes, mergeableIngresses, virtualServerExes := lbc.createExtendedResources(resources) + + warnings, updateErr := lbc.configurator.AddOrUpdateResources(ingressExes, mergeableIngresses, virtualServerExes) + lbc.updateResourcesStatusAndEvents(resources, warnings, updateErr) + + // TransportServers are processed separately: each affected TS gets enqueued in service handlers } -// IsExternalServiceForStatus matches the service specified by the external-service arg +// IsExternalServiceForStatus matches the service specified by the external-service cli arg func (lbc *LoadBalancerController) IsExternalServiceForStatus(svc *api_v1.Service) bool { return lbc.statusUpdater.namespace == svc.Namespace && lbc.statusUpdater.externalServiceName == svc.Name } +// IsExternalServiceKeyForStatus matches the service key specified by the external-service cli arg +func (lbc *LoadBalancerController) IsExternalServiceKeyForStatus(key string) bool { + externalSvcKey := fmt.Sprintf("%s/%s", lbc.statusUpdater.namespace, lbc.statusUpdater.externalServiceName) + return key == externalSvcKey +} + // reportStatusEnabled determines if we should attempt to report status for Ingress resources. func (lbc *LoadBalancerController) reportStatusEnabled() bool { if lbc.reportIngressStatus { @@ -1509,33 +1404,23 @@ func (lbc *LoadBalancerController) syncSecret(task task) { return } - ings, err := lbc.findIngressesForSecret(namespace, name) - if err != nil { - glog.Warningf("Failed to find Ingress resources for Secret %v: %v", key, err) - lbc.syncQueue.RequeueAfter(task, err, 5*time.Second) - } - glog.V(2).Infof("Found %v Ingresses with Secret %v", len(ings), key) - - var virtualServers []*conf_v1.VirtualServer + resources := lbc.configuration.FindResourcesForSecret(namespace, name) if lbc.areCustomResourcesEnabled { - virtualServers = lbc.getVirtualServersForSecret(namespace, name) - secretPols := lbc.getPoliciesForSecret(namespace, name) for _, pol := range secretPols { - vsList := lbc.getVirtualServersForPolicy(pol.Namespace, pol.Name) - virtualServers = append(virtualServers, vsList...) + resources = append(resources, lbc.configuration.FindResourcesForPolicy(pol.Namespace, pol.Name)...) } - virtualServers = removeDuplicateVirtualServers(virtualServers) - - glog.V(2).Infof("Found %v VirtualServers with Secret %v", len(virtualServers), key) + resources = removeDuplicateResources(resources) } + glog.V(2).Infof("Found %v Resources with Secret %v", len(resources), key) + if !secrExists { glog.V(2).Infof("Deleting Secret: %v\n", key) - lbc.handleRegularSecretDeletion(key, ings, virtualServers) + lbc.handleRegularSecretDeletion(key, resources) if lbc.isSpecialSecret(key) { glog.Warningf("A special TLS Secret %v was removed. Retaining the Secret.", key) } @@ -1548,65 +1433,44 @@ func (lbc *LoadBalancerController) syncSecret(task task) { if lbc.isSpecialSecret(key) { lbc.handleSpecialSecretUpdate(secret) - // we don't return here in case the special secret is also used in Ingress or VirtualServer resources. + // we don't return here in case the special secret is also used in resources. } - if len(ings)+len(virtualServers) > 0 { - lbc.handleSecretUpdate(secret, ings, virtualServers) + if len(resources) > 0 { + lbc.handleSecretUpdate(secret, resources) } } -func removeDuplicateVirtualServers(virtualServers []*conf_v1.VirtualServer) []*conf_v1.VirtualServer { +func removeDuplicateResources(resources []Resource) []Resource { encountered := make(map[string]bool) - var uniqueVirtualServers []*conf_v1.VirtualServer - for _, vs := range virtualServers { - vsKey := fmt.Sprintf("%v/%v", vs.Namespace, vs.Name) - if !encountered[vsKey] { - encountered[vsKey] = true - uniqueVirtualServers = append(uniqueVirtualServers, vs) + var uniqueResources []Resource + for _, r := range resources { + key := r.GetKeyWithKind() + if !encountered[key] { + encountered[key] = true + uniqueResources = append(uniqueResources, r) } } - return uniqueVirtualServers + return uniqueResources } func (lbc *LoadBalancerController) isSpecialSecret(secretName string) bool { return secretName == lbc.defaultServerSecret || secretName == lbc.wildcardTLSSecret } -func (lbc *LoadBalancerController) handleRegularSecretDeletion(key string, ings []networking.Ingress, virtualServers []*conf_v1.VirtualServer) { - eventType := api_v1.EventTypeWarning - title := "Missing Secret" - message := fmt.Sprintf("Secret %v was removed", key) - state := conf_v1.StateInvalid - - lbc.emitEventForIngresses(eventType, title, message, ings) - lbc.emitEventForVirtualServers(eventType, title, message, virtualServers) - lbc.updateStatusForVirtualServers(state, title, message, virtualServers) - - regular, mergeable := lbc.createIngresses(ings) +func (lbc *LoadBalancerController) handleRegularSecretDeletion(key string, resources []Resource) { + ingressExes, mergeableIngresses, virtualServerExes := lbc.createExtendedResources(resources) - virtualServerExes := lbc.virtualServersToVirtualServerExes(virtualServers) - - eventType = api_v1.EventTypeNormal - title = "Updated" - message = fmt.Sprintf("Configuration was updated due to removed secret %v", key) - - if err := lbc.configurator.DeleteSecret(key, regular, mergeable, virtualServerExes); err != nil { - glog.Errorf("Error when deleting Secret: %v: %v", key, err) - - eventType = api_v1.EventTypeWarning - title = "UpdatedWithError" - message = fmt.Sprintf("Configuration was updated due to removed secret %v, but not applied: %v", key, err) - state = conf_v1.StateInvalid + warnings, deleteErr := lbc.configurator.DeleteSecret(key, ingressExes, mergeableIngresses, virtualServerExes) + if deleteErr != nil { + glog.Errorf("Error when deleting Secret: %v: %v", key, deleteErr) } - lbc.emitEventForIngresses(eventType, title, message, ings) - lbc.emitEventForVirtualServers(eventType, title, message, virtualServers) - lbc.updateStatusForVirtualServers(state, title, message, virtualServers) + lbc.updateResourcesStatusAndEvents(resources, warnings, deleteErr) } -func (lbc *LoadBalancerController) handleSecretUpdate(secret *api_v1.Secret, ings []networking.Ingress, virtualServers []*conf_v1.VirtualServer) { +func (lbc *LoadBalancerController) handleSecretUpdate(secret *api_v1.Secret, resources []Resource) { secretNsName := secret.Namespace + "/" + secret.Name err := lbc.ValidateSecret(secret) @@ -1615,66 +1479,35 @@ func (lbc *LoadBalancerController) handleSecretUpdate(secret *api_v1.Secret, ing glog.Errorf("Couldn't validate secret %v: %v", secretNsName, err) glog.Errorf("Removing invalid secret %v", secretNsName) - lbc.handleRegularSecretDeletion(secretNsName, ings, virtualServers) + lbc.handleRegularSecretDeletion(secretNsName, resources) lbc.recorder.Eventf(secret, api_v1.EventTypeWarning, "Rejected", "%v was rejected: %v", secretNsName, err) return } - eventType := api_v1.EventTypeNormal - title := "Updated" - message := fmt.Sprintf("Configuration was updated due to updated secret %v", secretNsName) - state := conf_v1.StateValid + var warnings configs.Warnings + var addOrUpdateErr error // we can safely ignore the error because the secret is valid in this function kind, _ := GetSecretKind(secret) if kind == JWK { - virtualServerExes := lbc.virtualServersToVirtualServerExes(virtualServers) - - err := lbc.configurator.AddOrUpdateJWKSecret(secret, virtualServerExes) - if err != nil { - glog.Errorf("Error when updating Secret %v: %v", secretNsName, err) - lbc.recorder.Eventf(secret, api_v1.EventTypeWarning, "UpdatedWithError", "%v was updated, but not applied: %v", secretNsName, err) - - eventType = api_v1.EventTypeWarning - title = "UpdatedWithError" - message = fmt.Sprintf("Configuration was updated due to updated secret %v, but not applied: %v", secretNsName, err) - state = conf_v1.StateInvalid - } + _, _, virtualServerExes := lbc.createExtendedResources(resources) + warnings, addOrUpdateErr = lbc.configurator.AddOrUpdateJWKSecret(secret, virtualServerExes) } else if kind == IngressMTLS { - virtualServerExes := lbc.virtualServersToVirtualServerExes(virtualServers) - - err := lbc.configurator.AddOrUpdateIngressMTLSSecret(secret, virtualServerExes) - if err != nil { - glog.Errorf("Error when updating Secret %v: %v", secretNsName, err) - lbc.recorder.Eventf(secret, api_v1.EventTypeWarning, "UpdatedWithError", "%v was updated, but not applied: %v", secretNsName, err) - - eventType = api_v1.EventTypeWarning - title = "UpdatedWithError" - message = fmt.Sprintf("Configuration was updated due to updated secret %v, but not applied: %v", secretNsName, err) - state = conf_v1.StateInvalid - } + _, _, virtualServerExes := lbc.createExtendedResources(resources) + warnings, addOrUpdateErr = lbc.configurator.AddOrUpdateIngressMTLSSecret(secret, virtualServerExes) } else { - regular, mergeable := lbc.createIngresses(ings) - - virtualServerExes := lbc.virtualServersToVirtualServerExes(virtualServers) - - err := lbc.configurator.AddOrUpdateTLSSecret(secret, regular, mergeable, virtualServerExes) - if err != nil { - glog.Errorf("Error when updating Secret %v: %v", secretNsName, err) - lbc.recorder.Eventf(secret, api_v1.EventTypeWarning, "UpdatedWithError", "%v was updated, but not applied: %v", secretNsName, err) + ingressExes, mergeableIngresses, virtualServerExes := lbc.createExtendedResources(resources) + warnings, addOrUpdateErr = lbc.configurator.AddOrUpdateTLSSecret(secret, ingressExes, mergeableIngresses, virtualServerExes) + } - eventType = api_v1.EventTypeWarning - title = "UpdatedWithError" - message = fmt.Sprintf("Configuration was updated due to updated secret %v, but not applied: %v", secretNsName, err) - state = conf_v1.StateInvalid - } + if addOrUpdateErr != nil { + glog.Errorf("Error when updating Secret %v: %v", secretNsName, addOrUpdateErr) + lbc.recorder.Eventf(secret, api_v1.EventTypeWarning, "UpdatedWithError", "%v was updated, but not applied: %v", secretNsName, addOrUpdateErr) } - lbc.emitEventForIngresses(eventType, title, message, ings) - lbc.emitEventForVirtualServers(eventType, title, message, virtualServers) - lbc.updateStatusForVirtualServers(state, title, message, virtualServers) + lbc.updateResourcesStatusAndEvents(resources, warnings, addOrUpdateErr) } func (lbc *LoadBalancerController) handleSpecialSecretUpdate(secret *api_v1.Secret) { @@ -1704,27 +1537,6 @@ func (lbc *LoadBalancerController) handleSpecialSecretUpdate(secret *api_v1.Secr lbc.recorder.Eventf(secret, api_v1.EventTypeNormal, "Updated", "the special Secret %v was updated", secretNsName) } -func (lbc *LoadBalancerController) emitEventForIngresses(eventType string, title string, message string, ings []networking.Ingress) { - for _, ing := range ings { - lbc.recorder.Eventf(&ing, eventType, title, message) - if isMinion(&ing) { - master, err := lbc.FindMasterForMinion(&ing) - if err != nil { - glog.Errorf("Ignoring Ingress %v(Minion): %v", ing.Name, err) - continue - } - masterMsg := fmt.Sprintf("%v for Minion %v/%v", message, ing.Namespace, ing.Name) - lbc.recorder.Eventf(master, eventType, title, masterMsg) - } - } -} - -func (lbc *LoadBalancerController) emitEventForVirtualServers(eventType string, title string, message string, virtualServers []*conf_v1.VirtualServer) { - for _, vs := range virtualServers { - lbc.recorder.Eventf(vs, eventType, title, message) - } -} - func getStatusFromEventTitle(eventTitle string) string { switch eventTitle { case "AddedOrUpdatedWithError", "Rejected", "NoVirtualServersFound", "Missing Secret", "UpdatedWithError": @@ -1806,468 +1618,82 @@ func (lbc *LoadBalancerController) updateVirtualServerRoutesStatusFromEvents() e for _, event := range events.Items { if event.CreationTimestamp.After(timestamp) { latestEvent = event - } - } - - err = lbc.statusUpdater.UpdateVirtualServerRouteStatus(vsr, getStatusFromEventTitle(latestEvent.Reason), latestEvent.Reason, latestEvent.Message) - if err != nil { - allErrs = append(allErrs, err) - } - } - - if len(allErrs) > 0 { - return fmt.Errorf("not all VirtualServerRoutes statuses were updated: %v", allErrs) - } - - return nil -} - -func (lbc *LoadBalancerController) updateStatusForVirtualServers(state string, reason string, message string, virtualServers []*conf_v1.VirtualServer) { - for _, vs := range virtualServers { - err := lbc.statusUpdater.UpdateVirtualServerStatus(vs, state, reason, message) - if err != nil { - glog.Errorf("Error when updating the status for VirtualServer %v/%v: %v", vs.Namespace, vs.Name, err) - } - } -} - -func (lbc *LoadBalancerController) createIngresses(ings []networking.Ingress) (regular []configs.IngressEx, mergeable []configs.MergeableIngresses) { - for i := range ings { - if isMaster(&ings[i]) { - mergeableIng, err := lbc.createMergableIngresses(&ings[i]) - if err != nil { - glog.Errorf("Ignoring Ingress %v(Master): %v", ings[i].Name, err) - continue - } - mergeable = append(mergeable, *mergeableIng) - continue - } - - if isMinion(&ings[i]) { - master, err := lbc.FindMasterForMinion(&ings[i]) - if err != nil { - glog.Errorf("Ignoring Ingress %v(Minion): %v", ings[i].Name, err) - continue - } - mergeableIng, err := lbc.createMergableIngresses(master) - if err != nil { - glog.Errorf("Ignoring Ingress %v(Master): %v", master.Name, err) - continue - } - - mergeable = append(mergeable, *mergeableIng) - continue - } - - ingEx, err := lbc.createIngress(&ings[i]) - if err != nil { - glog.Errorf("Ignoring Ingress %v/%v: $%v", ings[i].Namespace, ings[i].Name, err) - } - regular = append(regular, *ingEx) - } - - return regular, mergeable -} - -func (lbc *LoadBalancerController) findIngressesForSecret(secretNamespace string, secretName string) (ings []networking.Ingress, error error) { - allIngs, err := lbc.ingressLister.List() - if err != nil { - return nil, fmt.Errorf("Couldn't get the list of Ingress resources: %v", err) - } - -items: - for _, ing := range allIngs.Items { - if ing.Namespace != secretNamespace { - continue - } - - if !lbc.HasCorrectIngressClass(&ing) { - continue - } - - if !isMinion(&ing) { - if !lbc.configurator.HasIngress(&ing) { - continue - } - for _, tls := range ing.Spec.TLS { - if tls.SecretName == secretName { - ings = append(ings, ing) - continue items - } - } - if lbc.isNginxPlus { - if jwtKey, exists := ing.Annotations[configs.JWTKeyAnnotation]; exists { - if jwtKey == secretName { - ings = append(ings, ing) - } - } - } - continue - } - - // we're dealing with a minion - // minions can only have JWT secrets - if lbc.isNginxPlus { - master, err := lbc.FindMasterForMinion(&ing) - if err != nil { - glog.Infof("Ignoring Ingress %v(Minion): %v", ing.Name, err) - continue - } - - if !lbc.configurator.HasMinion(master, &ing) { - continue - } - - if jwtKey, exists := ing.Annotations[configs.JWTKeyAnnotation]; exists { - if jwtKey == secretName { - ings = append(ings, ing) - } - } - } - } - - return ings, nil -} - -// EnqueueIngressForService enqueues the ingress for the given service -func (lbc *LoadBalancerController) EnqueueIngressForService(svc *api_v1.Service) { - ings := lbc.getIngressesForService(svc) - for _, ing := range ings { - if !lbc.HasCorrectIngressClass(&ing) { - continue - } - if isMinion(&ing) { - master, err := lbc.FindMasterForMinion(&ing) - if err != nil { - glog.Errorf("Ignoring Ingress %v(Minion): %v", ing.Name, err) - continue - } - ing = *master - } - if !lbc.configurator.HasIngress(&ing) { - continue - } - lbc.syncQueue.Enqueue(&ing) - - } -} - -// EnqueueVirtualServersForService enqueues VirtualServers for the given service. -func (lbc *LoadBalancerController) EnqueueVirtualServersForService(service *api_v1.Service) { - virtualServers := lbc.getVirtualServersForService(service) - for _, vs := range virtualServers { - lbc.syncQueue.Enqueue(vs) - } -} - -// EnqueueTransportServerForService enqueues TransportServers for the given service. -func (lbc *LoadBalancerController) EnqueueTransportServerForService(service *api_v1.Service) { - transportServers := lbc.getTransportServersForService(service) - for _, ts := range transportServers { - lbc.syncQueue.Enqueue(ts) - } -} - -func (lbc *LoadBalancerController) getIngressesForService(svc *api_v1.Service) []networking.Ingress { - ings, err := lbc.ingressLister.GetServiceIngress(svc) - if err != nil { - glog.V(3).Infof("For service %v: %v", svc.Name, err) - return nil - } - return ings -} - -func (lbc *LoadBalancerController) getIngressForEndpoints(obj interface{}) []networking.Ingress { - var ings []networking.Ingress - endp := obj.(*api_v1.Endpoints) - svcKey := endp.GetNamespace() + "/" + endp.GetName() - svcObj, svcExists, err := lbc.svcLister.GetByKey(svcKey) - if err != nil { - glog.V(3).Infof("error getting service %v from the cache: %v\n", svcKey, err) - } else { - if svcExists { - ings = append(ings, lbc.getIngressesForService(svcObj.(*api_v1.Service))...) - } - } - return ings -} - -func (lbc *LoadBalancerController) getVirtualServersForEndpoints(endpoints *api_v1.Endpoints) []*conf_v1.VirtualServer { - svcKey := fmt.Sprintf("%s/%s", endpoints.Namespace, endpoints.Name) - - svc, exists, err := lbc.svcLister.GetByKey(svcKey) - if err != nil { - glog.V(3).Infof("Error getting service %v from the cache: %v", svcKey, err) - return nil - } - if !exists { - glog.V(3).Infof("Service %v doesn't exist", svcKey) - return nil - } - - return lbc.getVirtualServersForService(svc.(*api_v1.Service)) -} - -func (lbc *LoadBalancerController) getVirtualServersForService(service *api_v1.Service) []*conf_v1.VirtualServer { - var result []*conf_v1.VirtualServer - - allVirtualServers := lbc.getVirtualServers() - - // find VirtualServers that reference VirtualServerRoutes that reference the service - virtualServerRoutes := findVirtualServerRoutesForService(lbc.getVirtualServerRoutes(), service) - for _, vsr := range virtualServerRoutes { - virtualServers := findVirtualServersForVirtualServerRoute(allVirtualServers, vsr) - result = append(result, virtualServers...) - } - - // find VirtualServers that reference the service - virtualServers := findVirtualServersForService(lbc.getVirtualServers(), service) - result = append(result, virtualServers...) - - return result -} - -func findVirtualServersForService(virtualServers []*conf_v1.VirtualServer, service *api_v1.Service) []*conf_v1.VirtualServer { - var result []*conf_v1.VirtualServer - - for _, vs := range virtualServers { - if vs.Namespace != service.Namespace { - continue - } - - isReferenced := false - for _, u := range vs.Spec.Upstreams { - if u.Service == service.Name { - isReferenced = true - break - } - } - if !isReferenced { - continue - } - - result = append(result, vs) - } - - return result -} - -func (lbc *LoadBalancerController) getTransportServersForEndpoints(endpoints *api_v1.Endpoints) []*conf_v1alpha1.TransportServer { - svcKey := fmt.Sprintf("%s/%s", endpoints.Namespace, endpoints.Name) - - svc, exists, err := lbc.svcLister.GetByKey(svcKey) - if err != nil { - glog.V(3).Infof("Error getting service %v from the cache: %v", svcKey, err) - return nil - } - if !exists { - glog.V(3).Infof("Service %v doesn't exist", svcKey) - return nil - } - - return lbc.getTransportServersForService(svc.(*api_v1.Service)) -} - -func (lbc *LoadBalancerController) getTransportServersForService(service *api_v1.Service) []*conf_v1alpha1.TransportServer { - filtered := lbc.filterOutTransportServersWithNonExistingListener(lbc.getTransportServers()) - return findTransportServersForService(filtered, service) -} - -func findTransportServersForService(transportServers []*conf_v1alpha1.TransportServer, service *api_v1.Service) []*conf_v1alpha1.TransportServer { - var result []*conf_v1alpha1.TransportServer - - for _, ts := range transportServers { - if ts.Namespace != service.Namespace { - continue - } - - for _, u := range ts.Spec.Upstreams { - if u.Service == service.Name { - result = append(result, ts) - break - } - } - } - - return result -} - -func (lbc *LoadBalancerController) filterOutTransportServersWithNonExistingListener(transportServers []*conf_v1alpha1.TransportServer) []*conf_v1alpha1.TransportServer { - var result []*conf_v1alpha1.TransportServer - - for _, ts := range transportServers { - if lbc.configurator.CheckIfListenerExists(&ts.Spec.Listener) { - result = append(result, ts) - } else { - glog.V(3).Infof("Ignoring TransportServer %s/%s references a non-existing listener", ts.Namespace, ts.Name) - } - } - - return result -} - -func findVirtualServerRoutesForService(virtualServerRoutes []*conf_v1.VirtualServerRoute, service *api_v1.Service) []*conf_v1.VirtualServerRoute { - var result []*conf_v1.VirtualServerRoute - - for _, vsr := range virtualServerRoutes { - if vsr.Namespace != service.Namespace { - continue - } - - isReferenced := false - for _, u := range vsr.Spec.Upstreams { - if u.Service == service.Name { - isReferenced = true - break - } - } - if !isReferenced { - continue - } - - result = append(result, vsr) - } - - return result -} - -func (lbc *LoadBalancerController) getVirtualServersForSecret(secretNamespace string, secretName string) []*conf_v1.VirtualServer { - virtualServers := lbc.getVirtualServers() - return findVirtualServersForSecret(virtualServers, secretNamespace, secretName) -} - -func findVirtualServersForSecret(virtualServers []*conf_v1.VirtualServer, secretNamespace string, secretName string) []*conf_v1.VirtualServer { - var result []*conf_v1.VirtualServer - - for _, vs := range virtualServers { - if vs.Spec.TLS == nil { - continue - } - if vs.Spec.TLS.Secret == "" { - continue - } - - if vs.Namespace == secretNamespace && vs.Spec.TLS.Secret == secretName { - result = append(result, vs) - } - } - - return result -} - -func (lbc *LoadBalancerController) getVirtualServersForPolicy(policyNamespace string, policyName string) []*conf_v1.VirtualServer { - var result []*conf_v1.VirtualServer - - allVirtualServers := lbc.getVirtualServers() - - // find VirtualServers that reference VirtualServerRoutes that reference the policy - virtualServerRoutes := findVirtualServerRoutesForPolicy(lbc.getVirtualServerRoutes(), policyNamespace, policyName) - for _, vsr := range virtualServerRoutes { - virtualServers := findVirtualServersForVirtualServerRoute(allVirtualServers, vsr) - result = append(result, virtualServers...) - } - - // find VirtualServers that reference the policy - virtualServers := findVirtualServersForPolicy(lbc.getVirtualServers(), policyNamespace, policyName) - result = append(result, virtualServers...) - - return result -} - -func findVirtualServersForPolicy(virtualServers []*conf_v1.VirtualServer, policyNamespace string, policyName string) []*conf_v1.VirtualServer { - var result []*conf_v1.VirtualServer - - for _, vs := range virtualServers { - if isPolicyReferenced(vs.Spec.Policies, vs.Namespace, policyNamespace, policyName) { - result = append(result, vs) - continue + } } - for _, r := range vs.Spec.Routes { - if isPolicyReferenced(r.Policies, vs.Namespace, policyNamespace, policyName) { - result = append(result, vs) - break - } + err = lbc.statusUpdater.UpdateVirtualServerRouteStatus(vsr, getStatusFromEventTitle(latestEvent.Reason), latestEvent.Reason, latestEvent.Message) + if err != nil { + allErrs = append(allErrs, err) } } - return result -} + if len(allErrs) > 0 { + return fmt.Errorf("not all VirtualServerRoutes statuses were updated: %v", allErrs) + } -func isPolicyReferenced(policies []conf_v1.PolicyReference, resourceNamespace string, policyNamespace string, policyName string) bool { - for _, p := range policies { - namespace := p.Namespace - if namespace == "" { - namespace = resourceNamespace - } + return nil +} - if p.Name == policyName && namespace == policyNamespace { - return true - } +// EnqueueTransportServerForService enqueues TransportServers for the given service. +func (lbc *LoadBalancerController) EnqueueTransportServerForService(service *api_v1.Service) { + transportServers := lbc.getTransportServersForService(service) + for _, ts := range transportServers { + lbc.syncQueue.Enqueue(ts) } - - return false } -func findVirtualServerRoutesForPolicy(virtualServerRoutes []*conf_v1.VirtualServerRoute, policyNamespace string, policyName string) []*conf_v1.VirtualServerRoute { - var result []*conf_v1.VirtualServerRoute +func (lbc *LoadBalancerController) getTransportServersForEndpoints(endpoints *api_v1.Endpoints) []*conf_v1alpha1.TransportServer { + svcKey := fmt.Sprintf("%s/%s", endpoints.Namespace, endpoints.Name) - for _, vsr := range virtualServerRoutes { - for _, r := range vsr.Spec.Subroutes { - if isPolicyReferenced(r.Policies, vsr.Namespace, policyNamespace, policyName) { - result = append(result, vsr) - break - } - } + svc, exists, err := lbc.svcLister.GetByKey(svcKey) + if err != nil { + glog.V(3).Infof("Error getting service %v from the cache: %v", svcKey, err) + return nil + } + if !exists { + glog.V(3).Infof("Service %v doesn't exist", svcKey) + return nil } - return result + return lbc.getTransportServersForService(svc.(*api_v1.Service)) } -func (lbc *LoadBalancerController) getVirtualServers() []*conf_v1.VirtualServer { - var virtualServers []*conf_v1.VirtualServer +func (lbc *LoadBalancerController) getTransportServersForService(service *api_v1.Service) []*conf_v1alpha1.TransportServer { + filtered := lbc.filterOutTransportServersWithNonExistingListener(lbc.getTransportServers()) + return findTransportServersForService(filtered, service) +} - for _, obj := range lbc.virtualServerLister.List() { - vs := obj.(*conf_v1.VirtualServer) +func findTransportServersForService(transportServers []*conf_v1alpha1.TransportServer, service *api_v1.Service) []*conf_v1alpha1.TransportServer { + var result []*conf_v1alpha1.TransportServer - if !lbc.HasCorrectIngressClass(vs) { - glog.V(3).Infof("Ignoring VirtualServer %v based on class %v", vs.Name, vs.Spec.IngressClass) + for _, ts := range transportServers { + if ts.Namespace != service.Namespace { continue } - err := lbc.virtualServerValidator.ValidateVirtualServer(vs) - if err != nil { - glog.V(3).Infof("Skipping invalid VirtualServer %s/%s: %v", vs.Namespace, vs.Name, err) - continue + for _, u := range ts.Spec.Upstreams { + if u.Service == service.Name { + result = append(result, ts) + break + } } - - virtualServers = append(virtualServers, vs) } - return virtualServers + return result } -func (lbc *LoadBalancerController) getVirtualServerRoutes() []*conf_v1.VirtualServerRoute { - var virtualServerRoutes []*conf_v1.VirtualServerRoute - - for _, obj := range lbc.virtualServerRouteLister.List() { - vsr := obj.(*conf_v1.VirtualServerRoute) - - if !lbc.HasCorrectIngressClass(vsr) { - glog.V(3).Infof("Ignoring VirtualServerRoute %v based on class %v", vsr.Name, vsr.Spec.IngressClass) - continue - } +func (lbc *LoadBalancerController) filterOutTransportServersWithNonExistingListener(transportServers []*conf_v1alpha1.TransportServer) []*conf_v1alpha1.TransportServer { + var result []*conf_v1alpha1.TransportServer - err := lbc.virtualServerValidator.ValidateVirtualServerRoute(vsr) - if err != nil { - glog.V(3).Infof("Skipping invalid VirtualServerRoute %s/%s: %v", vsr.Namespace, vsr.Name, err) - continue + for _, ts := range transportServers { + if lbc.configurator.CheckIfListenerExists(&ts.Spec.Listener) { + result = append(result, ts) + } else { + glog.V(3).Infof("Ignoring TransportServer %s/%s references a non-existing listener", ts.Namespace, ts.Name) } - - virtualServerRoutes = append(virtualServerRoutes, vsr) } - return virtualServerRoutes + return result } func (lbc *LoadBalancerController) getTransportServers() []*conf_v1alpha1.TransportServer { @@ -2288,42 +1714,6 @@ func (lbc *LoadBalancerController) getTransportServers() []*conf_v1alpha1.Transp return transportServers } -func (lbc *LoadBalancerController) enqueueVirtualServersForVirtualServerRouteKey(key string) int { - virtualServers := findVirtualServersForVirtualServerRouteKey(lbc.getVirtualServers(), key) - - for _, vs := range virtualServers { - lbc.syncQueue.Enqueue(vs) - } - - return len(virtualServers) -} - -func findVirtualServersForVirtualServerRoute(virtualServers []*conf_v1.VirtualServer, virtualServerRoute *conf_v1.VirtualServerRoute) []*conf_v1.VirtualServer { - key := fmt.Sprintf("%s/%s", virtualServerRoute.Namespace, virtualServerRoute.Name) - return findVirtualServersForVirtualServerRouteKey(virtualServers, key) -} - -func findVirtualServersForVirtualServerRouteKey(virtualServers []*conf_v1.VirtualServer, key string) []*conf_v1.VirtualServer { - var result []*conf_v1.VirtualServer - - for _, vs := range virtualServers { - for _, r := range vs.Spec.Routes { - // if route is defined without a namespace, use the namespace of VirtualServer. - vsrKey := r.Route - if !strings.Contains(r.Route, "/") { - vsrKey = fmt.Sprintf("%s/%s", vs.Namespace, r.Route) - } - - if vsrKey == key { - result = append(result, vs) - break - } - } - } - - return result -} - func getIPAddressesFromEndpoints(endpoints []podEndpoint) []string { var endps []string for _, ep := range endpoints { @@ -2383,9 +1773,27 @@ func (lbc *LoadBalancerController) getAndValidateIngressMTLSSecret(secretKey str return secret, nil } -func (lbc *LoadBalancerController) createIngress(ing *networking.Ingress) (*configs.IngressEx, error) { +func (lbc *LoadBalancerController) createMergeableIngresses(fullIng *FullIngress) *configs.MergeableIngresses { + // for master Ingress, validMinionPaths are nil + masterIngressEx := lbc.createIngressEx(fullIng.Ingress, fullIng.ValidHosts, nil) + + var minions []*configs.IngressEx + + for _, fm := range fullIng.Minions { + minions = append(minions, lbc.createIngressEx(fm.Ingress, fullIng.ValidHosts, fm.ValidPaths)) + } + + return &configs.MergeableIngresses{ + Master: masterIngressEx, + Minions: minions, + } +} + +func (lbc *LoadBalancerController) createIngressEx(ing *networking.Ingress, validHosts map[string]bool, validMinionPaths map[string]bool) *configs.IngressEx { ingEx := &configs.IngressEx{ - Ingress: ing, + Ingress: ing, + ValidHosts: validHosts, + ValidMinionPaths: validMinionPaths, } ingEx.TLSSecrets = make(map[string]*api_v1.Secret) @@ -2487,18 +1895,24 @@ func (lbc *LoadBalancerController) createIngress(ing *networking.Ingress) (*conf } } - validRules := 0 for _, rule := range ing.Spec.Rules { - if rule.IngressRuleValue.HTTP == nil { + if !validHosts[rule.Host] { + glog.V(3).Infof("Skipping host %s for Ingress %s", rule.Host, ing.Name) continue } - if rule.Host == "" { - return nil, fmt.Errorf("Ingress rule contains empty host") + // check if rule has any paths + if rule.IngressRuleValue.HTTP == nil { + continue } for _, path := range rule.HTTP.Paths { podEndps := []podEndpoint{} + if validMinionPaths != nil && !validMinionPaths[path.Path] { + glog.V(3).Infof("Skipping path %s for minion Ingress %s", path.Path, ing.Name) + continue + } + var external bool svc, err := lbc.getServiceForIngressBackend(&path.Backend, ing.Namespace) if err != nil { @@ -2536,15 +1950,9 @@ func (lbc *LoadBalancerController) createIngress(ing *networking.Ingress) (*conf } } } - - validRules++ } - if validRules == 0 { - return nil, fmt.Errorf("Ingress contains no valid rules") - } - - return ingEx, nil + return ingEx } func (lbc *LoadBalancerController) getAppProtectLogConfAndDst(ing *networking.Ingress) (logConf *unstructured.Unstructured, logDst string, err error) { @@ -2600,28 +2008,7 @@ func (lbc *LoadBalancerController) getAppProtectPolicy(ing *networking.Ingress) return apPolicy, nil } -type virtualServerRouteError struct { - VirtualServerRouteNsName string - VirtualServerRoute *conf_v1.VirtualServerRoute - Error error -} - -func newVirtualServerRouteErrorFromNsName(nsName string, err error) virtualServerRouteError { - return virtualServerRouteError{ - VirtualServerRouteNsName: nsName, - Error: err, - } -} - -func newVirtualServerRouteErrorFromVSR(virtualServerRoute *conf_v1.VirtualServerRoute, err error) virtualServerRouteError { - return virtualServerRouteError{ - VirtualServerRoute: virtualServerRoute, - VirtualServerRouteNsName: fmt.Sprintf("%s/%s", virtualServerRoute.Namespace, virtualServerRoute.Name), - Error: err, - } -} - -func (lbc *LoadBalancerController) createVirtualServer(virtualServer *conf_v1.VirtualServer) (*configs.VirtualServerEx, []virtualServerRouteError) { +func (lbc *LoadBalancerController) createVirtualServerEx(virtualServer *conf_v1.VirtualServer, virtualServerRoutes []*conf_v1.VirtualServerRoute) *configs.VirtualServerEx { virtualServerEx := configs.VirtualServerEx{ VirtualServer: virtualServer, JWTKeys: make(map[string]*api_v1.Secret), @@ -2692,62 +2079,15 @@ func (lbc *LoadBalancerController) createVirtualServer(virtualServer *conf_v1.Vi } } - var virtualServerRoutes []*conf_v1.VirtualServerRoute - var virtualServerRouteErrors []virtualServerRouteError - for _, r := range virtualServer.Spec.Routes { vsRoutePolicies, policyErrors := lbc.getPolicies(r.Policies, virtualServer.Namespace) for _, err := range policyErrors { glog.Warningf("Error getting policy for VirtualServer %s/%s: %v", virtualServer.Namespace, virtualServer.Name, err) } policies = append(policies, vsRoutePolicies...) + } - err = lbc.addJWTSecrets(vsRoutePolicies, virtualServerEx.JWTKeys) - if err != nil { - glog.Warningf("Error getting JWT secrets for VirtualServer %v/%v: %v", virtualServer.Namespace, virtualServer.Name, err) - } - - if r.Route == "" { - continue - } - - vsrKey := r.Route - - // if route is defined without a namespace, use the namespace of VirtualServer. - if !strings.Contains(r.Route, "/") { - vsrKey = fmt.Sprintf("%s/%s", virtualServer.Namespace, r.Route) - } - - obj, exists, err := lbc.virtualServerRouteLister.GetByKey(vsrKey) - if err != nil { - glog.Warningf("Failed to get VirtualServerRoute %s for VirtualServer %s/%s: %v", vsrKey, virtualServer.Namespace, virtualServer.Name, err) - virtualServerRouteErrors = append(virtualServerRouteErrors, newVirtualServerRouteErrorFromNsName(vsrKey, err)) - continue - } - - if !exists { - glog.Warningf("VirtualServer %s/%s references VirtualServerRoute %s that doesn't exist", virtualServer.Name, virtualServer.Namespace, vsrKey) - virtualServerRouteErrors = append(virtualServerRouteErrors, newVirtualServerRouteErrorFromNsName(vsrKey, errors.New("VirtualServerRoute doesn't exist"))) - continue - } - - vsr := obj.(*conf_v1.VirtualServerRoute) - - if !lbc.HasCorrectIngressClass(vsr) { - glog.Warningf("Ignoring VirtualServerRoute %v based on class %v", vsr.Name, vsr.Spec.IngressClass) - virtualServerRouteErrors = append(virtualServerRouteErrors, newVirtualServerRouteErrorFromVSR(vsr, errors.New("VirtualServerRoute with incorrect class name"))) - continue - } - - err = lbc.virtualServerValidator.ValidateVirtualServerRouteForVirtualServer(vsr, virtualServer.Spec.Host, r.Path) - if err != nil { - glog.Warningf("VirtualServer %s/%s references invalid VirtualServerRoute %s: %v", virtualServer.Name, virtualServer.Namespace, vsrKey, err) - virtualServerRouteErrors = append(virtualServerRouteErrors, newVirtualServerRouteErrorFromVSR(vsr, err)) - continue - } - - virtualServerRoutes = append(virtualServerRoutes, vsr) - + for _, vsr := range virtualServerRoutes { for _, sr := range vsr.Spec.Subroutes { vsrSubroutePolicies, policyErrors := lbc.getPolicies(sr.Policies, vsr.Namespace) for _, err := range policyErrors { @@ -2800,7 +2140,7 @@ func (lbc *LoadBalancerController) createVirtualServer(virtualServer *conf_v1.Vi virtualServerEx.Policies = createPolicyMap(policies) virtualServerEx.PodsByIP = podsByIP - return &virtualServerEx, virtualServerRouteErrors + return &virtualServerEx } func createPolicyMap(policies []*conf_v1alpha1.Policy) map[string]*conf_v1alpha1.Policy { @@ -3320,133 +2660,6 @@ func (lbc *LoadBalancerController) ValidateSecret(secret *api_v1.Secret) error { } // getMinionsForHost returns a list of all minion ingress resources for a given master -func (lbc *LoadBalancerController) getMinionsForMaster(master *configs.IngressEx) ([]*configs.IngressEx, error) { - ings, err := lbc.ingressLister.List() - if err != nil { - return []*configs.IngressEx{}, err - } - - // ingresses are sorted by creation time - sort.Slice(ings.Items[:], func(i, j int) bool { - return ings.Items[i].CreationTimestamp.Time.UnixNano() < ings.Items[j].CreationTimestamp.Time.UnixNano() - }) - - var minions []*configs.IngressEx - var minionPaths = make(map[string]*networking.Ingress) - - for i := range ings.Items { - if !lbc.HasCorrectIngressClass(&ings.Items[i]) { - continue - } - if !isMinion(&ings.Items[i]) { - continue - } - if ings.Items[i].Spec.Rules[0].Host != master.Ingress.Spec.Rules[0].Host { - continue - } - if len(ings.Items[i].Spec.Rules) != 1 { - glog.Errorf("Ingress Resource %v/%v with the 'nginx.org/mergeable-ingress-type' annotation must contain only one host", ings.Items[i].Namespace, ings.Items[i].Name) - continue - } - if ings.Items[i].Spec.Rules[0].HTTP == nil { - glog.Errorf("Ingress Resource %v/%v with the 'nginx.org/mergeable-ingress-type' annotation set to 'minion' must contain a Path", ings.Items[i].Namespace, ings.Items[i].Name) - continue - } - - uniquePaths := []networking.HTTPIngressPath{} - for _, path := range ings.Items[i].Spec.Rules[0].HTTP.Paths { - if val, ok := minionPaths[path.Path]; ok { - glog.Errorf("Ingress Resource %v/%v with the 'nginx.org/mergeable-ingress-type' annotation set to 'minion' cannot contain the same path as another ingress resource, %v/%v.", - ings.Items[i].Namespace, ings.Items[i].Name, val.Namespace, val.Name) - glog.Errorf("Path %s for Ingress Resource %v/%v will be ignored", path.Path, ings.Items[i].Namespace, ings.Items[i].Name) - } else { - minionPaths[path.Path] = &ings.Items[i] - uniquePaths = append(uniquePaths, path) - } - } - ings.Items[i].Spec.Rules[0].HTTP.Paths = uniquePaths - - ingEx, err := lbc.createIngress(&ings.Items[i]) - if err != nil { - glog.Errorf("Error creating ingress resource %v/%v: %v", ings.Items[i].Namespace, ings.Items[i].Name, err) - continue - } - if len(ingEx.TLSSecrets) > 0 { - glog.Errorf("Ingress Resource %v/%v with the 'nginx.org/mergeable-ingress-type' annotation set to 'minion' cannot contain TLS Secrets", ingEx.Ingress.Namespace, ingEx.Ingress.Name) - continue - } - minions = append(minions, ingEx) - } - - return minions, nil -} - -// FindMasterForMinion returns a master for a given minion -func (lbc *LoadBalancerController) FindMasterForMinion(minion *networking.Ingress) (*networking.Ingress, error) { - ings, err := lbc.ingressLister.List() - if err != nil { - return &networking.Ingress{}, err - } - - for i := range ings.Items { - if !lbc.HasCorrectIngressClass(&ings.Items[i]) { - continue - } - if !lbc.configurator.HasIngress(&ings.Items[i]) { - continue - } - if !isMaster(&ings.Items[i]) { - continue - } - if ings.Items[i].Spec.Rules[0].Host != minion.Spec.Rules[0].Host { - continue - } - return &ings.Items[i], nil - } - - err = fmt.Errorf("Could not find a Master for Minion: '%v/%v'", minion.Namespace, minion.Name) - return nil, err -} - -func (lbc *LoadBalancerController) createMergableIngresses(master *networking.Ingress) (*configs.MergeableIngresses, error) { - mergeableIngresses := configs.MergeableIngresses{} - - if len(master.Spec.Rules) != 1 { - err := fmt.Errorf("Ingress Resource %v/%v with the 'nginx.org/mergeable-ingress-type' annotation must contain only one host", master.Namespace, master.Name) - return &mergeableIngresses, err - } - - var empty networking.HTTPIngressRuleValue - if master.Spec.Rules[0].HTTP != nil { - if master.Spec.Rules[0].HTTP != &empty { - if len(master.Spec.Rules[0].HTTP.Paths) != 0 { - err := fmt.Errorf("Ingress Resource %v/%v with the 'nginx.org/mergeable-ingress-type' annotation set to 'master' cannot contain Paths", master.Namespace, master.Name) - return &mergeableIngresses, err - } - } - } - - // Makes sure there is an empty path assigned to a master, to allow for lbc.createIngress() to pass - master.Spec.Rules[0].HTTP = &networking.HTTPIngressRuleValue{ - Paths: []networking.HTTPIngressPath{}, - } - - masterIngEx, err := lbc.createIngress(master) - if err != nil { - err := fmt.Errorf("Error creating Ingress Resource %v/%v: %v", master.Namespace, master.Name, err) - return &mergeableIngresses, err - } - mergeableIngresses.Master = masterIngEx - - minions, err := lbc.getMinionsForMaster(masterIngEx) - if err != nil { - err = fmt.Errorf("Error Obtaining Ingress Resources: %v", err) - return &mergeableIngresses, err - } - mergeableIngresses.Minions = minions - - return &mergeableIngresses, nil -} func formatWarningMessages(w []string) string { return strings.Join(w, "; ") @@ -3477,12 +2690,12 @@ func (lbc *LoadBalancerController) syncAppProtectPolicy(task task) { return } - ings := lbc.findIngressesForAppProtectResource(namespace, name, configs.AppProtectPolicyAnnotation) + resources := lbc.configuration.FindResourcesForAppProtectPolicy(namespace, name) - glog.V(2).Infof("Found %v Ingresses with App Protect Policy %v", len(ings), key) + glog.V(2).Infof("Found %v resources with App Protect Policy %v", len(resources), key) if !polExists { - err = lbc.handleAppProtectPolicyDeletion(key, ings) + err = lbc.handleAppProtectPolicyDeletion(key, resources) if err != nil { glog.Errorf("Error deleting AppProtectPolicy %v: %v", key, err) } @@ -3491,64 +2704,44 @@ func (lbc *LoadBalancerController) syncAppProtectPolicy(task task) { policy := obj.(*unstructured.Unstructured) - err = ValidateAppProtectPolicy(policy) - if err != nil { - err = lbc.handleAppProtectPolicyDeletion(key, ings) + validationErr := ValidateAppProtectPolicy(policy) + if validationErr != nil { + err = lbc.handleAppProtectPolicyDeletion(key, resources) if err != nil { glog.Errorf("Error deleting AppProtectPolicy %v after it failed to validate: %v", key, err) } - lbc.recorder.Eventf(policy, api_v1.EventTypeWarning, "Rejected", "%v was rejected: %v", key, err) + lbc.recorder.Eventf(policy, api_v1.EventTypeWarning, "Rejected", "%v was rejected: %v", key, validationErr) return } - err = lbc.handleAppProtectPolicyUpdate(policy, ings) + + err = lbc.handleAppProtectPolicyUpdate(policy, resources) if err != nil { lbc.recorder.Eventf(policy, api_v1.EventTypeWarning, "AddedOrUpdatedWithError", "App Protect Policy %v was added or updated with error: %v", key, err) glog.Errorf("Error adding or updating AppProtectPolicy %v: %v", key, err) return } + lbc.recorder.Eventf(policy, api_v1.EventTypeNormal, "AddedOrUpdated", "AppProtectPolicy %v was added or updated", key) } -func (lbc *LoadBalancerController) handleAppProtectPolicyUpdate(pol *unstructured.Unstructured, ings []networking.Ingress) error { - regular, mergeable := lbc.createIngresses(ings) - polNsName := pol.GetNamespace() + "/" + pol.GetName() - - eventType := api_v1.EventTypeNormal - title := "Updated" - message := fmt.Sprintf("Configuration was updated due to updated App Protect Policy %v", polNsName) +func (lbc *LoadBalancerController) handleAppProtectPolicyUpdate(pol *unstructured.Unstructured, resources []Resource) error { + // VirtualServers don't support AppProtect policies + ingressExes, mergeableIngresses, _ := lbc.createExtendedResources(resources) - err := lbc.configurator.AddOrUpdateAppProtectResource(pol, regular, mergeable) - if err != nil { - eventType = api_v1.EventTypeWarning - title = "UpdatedWithError" - message = fmt.Sprintf("Configuration was updated due to updated App Protect Policy %v, but not applied: %v", polNsName, err) - lbc.emitEventForIngresses(eventType, title, message, ings) - return err - } + updateErr := lbc.configurator.AddOrUpdateAppProtectResource(pol, ingressExes, mergeableIngresses) + lbc.updateResourcesStatusAndEvents(resources, configs.Warnings{}, updateErr) - lbc.emitEventForIngresses(eventType, title, message, ings) - return nil + return updateErr } -func (lbc *LoadBalancerController) handleAppProtectPolicyDeletion(key string, ings []networking.Ingress) error { - regular, mergeable := lbc.createIngresses(ings) - - eventType := api_v1.EventTypeNormal - title := "Updated" - message := fmt.Sprintf("Configuration was updated due to deleted App Protect Policy %v", key) - - err := lbc.configurator.DeleteAppProtectPolicy(key, regular, mergeable) - if err != nil { - eventType = api_v1.EventTypeWarning - title = "UpdatedWithError" - message = fmt.Sprintf("Configuration was updated due to deleted App Protect Policy %v, but not applied: %v", key, err) - lbc.emitEventForIngresses(eventType, title, message, ings) - return err - } +func (lbc *LoadBalancerController) handleAppProtectPolicyDeletion(key string, resources []Resource) error { + // VirtualServers don't support AppProtect policies + ingressExes, mergeableIngresses, _ := lbc.createExtendedResources(resources) - lbc.emitEventForIngresses(eventType, title, message, ings) - return nil + deleteErr := lbc.configurator.DeleteAppProtectPolicy(key, ingressExes, mergeableIngresses) + lbc.updateResourcesStatusAndEvents(resources, configs.Warnings{}, deleteErr) + return deleteErr } func (lbc *LoadBalancerController) syncAppProtectLogConf(task task) { @@ -3566,13 +2759,13 @@ func (lbc *LoadBalancerController) syncAppProtectLogConf(task task) { return } - ings := lbc.findIngressesForAppProtectResource(namespace, name, configs.AppProtectLogConfAnnotation) + resources := lbc.configuration.FindResourcesForAppProtectLogConf(namespace, name) - glog.V(2).Infof("Found %v Ingresses with App Protect LogConfig %v", len(ings), key) + glog.V(2).Infof("Found %v resources with App Protect LogConfig %v", len(resources), key) if !confExists { glog.V(3).Infof("Deleting AppProtectLogConf %v", key) - err = lbc.handleAppProtectLogConfDeletion(key, ings) + err = lbc.appProtectLogConfDeletion(key, resources) if err != nil { glog.Errorf("Error deleting App Protect LogConfig %v: %v", key, err) } @@ -3581,80 +2774,44 @@ func (lbc *LoadBalancerController) syncAppProtectLogConf(task task) { logConf := obj.(*unstructured.Unstructured) - err = ValidateAppProtectLogConf(logConf) - if err != nil { - err = lbc.handleAppProtectLogConfDeletion(key, ings) + validationErr := ValidateAppProtectLogConf(logConf) + if validationErr != nil { + err = lbc.appProtectLogConfDeletion(key, resources) if err != nil { glog.Errorf("Error deleting App Protect LogConfig %v after it failed to validate: %v", key, err) } + lbc.recorder.Eventf(logConf, api_v1.EventTypeWarning, "Rejected", "%v was rejected: %v", key, validationErr) return } - err = lbc.handleAppProtectLogConfUpdate(logConf, ings) + + err = lbc.appProtectLogConfUpdate(logConf, resources) if err != nil { lbc.recorder.Eventf(logConf, api_v1.EventTypeWarning, "AddedOrUpdatedWithError", "App Protect Log Configuration %v was added or updated with error: %v", key, err) glog.V(3).Infof("Error adding or updating AppProtectLogConf %v : %v", key, err) return } + lbc.recorder.Eventf(logConf, api_v1.EventTypeNormal, "AddedOrUpdated", "AppProtectLogConfig %v was added or updated", key) } -func (lbc *LoadBalancerController) handleAppProtectLogConfUpdate(logConf *unstructured.Unstructured, ings []networking.Ingress) error { - logConfNsName := logConf.GetNamespace() + "/" + logConf.GetName() - - eventType := api_v1.EventTypeNormal - title := "Updated" - message := fmt.Sprintf("Configuration was updated due to updated App Protect Log Configuration %v", logConfNsName) +func (lbc *LoadBalancerController) appProtectLogConfUpdate(logConf *unstructured.Unstructured, resources []Resource) error { + // VirtualServers don't support AppProtectLogConf + ingressExes, mergeableIngresses, _ := lbc.createExtendedResources(resources) - regular, mergeable := lbc.createIngresses(ings) - err := lbc.configurator.AddOrUpdateAppProtectResource(logConf, regular, mergeable) - if err != nil { - eventType = api_v1.EventTypeWarning - title = "UpdatedWithError" - message = fmt.Sprintf("Configuration was updated due to updated App Protect Log Configuration %v, but not applied: %v", logConfNsName, err) - lbc.emitEventForIngresses(eventType, title, message, ings) - return err - } + updateErr := lbc.configurator.AddOrUpdateAppProtectResource(logConf, ingressExes, mergeableIngresses) + lbc.updateResourcesStatusAndEvents(resources, configs.Warnings{}, updateErr) - lbc.emitEventForIngresses(eventType, title, message, ings) - return nil + return updateErr } -func (lbc *LoadBalancerController) handleAppProtectLogConfDeletion(key string, ings []networking.Ingress) error { - eventType := api_v1.EventTypeNormal - title := "Updated" - message := fmt.Sprintf("Configuration was updated due to deleted App Protect Log Configuration %v", key) - - regular, mergeable := lbc.createIngresses(ings) - err := lbc.configurator.DeleteAppProtectLogConf(key, regular, mergeable) - if err != nil { - eventType = api_v1.EventTypeWarning - title = "UpdatedWithError" - message = fmt.Sprintf("Configuration was updated due to deleted App Protect Log Configuration %v, but not applied: %v", key, err) - lbc.emitEventForIngresses(eventType, title, message, ings) - return err - } +func (lbc *LoadBalancerController) appProtectLogConfDeletion(key string, resources []Resource) error { + // VirtualServers don't support AppProtectLogConf + ingressExes, mergeableIngresses, _ := lbc.createExtendedResources(resources) - lbc.emitEventForIngresses(eventType, title, message, ings) - return nil -} + deleteErr := lbc.configurator.DeleteAppProtectLogConf(key, ingressExes, mergeableIngresses) + lbc.updateResourcesStatusAndEvents(resources, configs.Warnings{}, deleteErr) -func (lbc *LoadBalancerController) findIngressesForAppProtectResource(namespace string, name string, annotationRef string) (apIngs []networking.Ingress) { - ings, mIngs := lbc.GetManagedIngresses() - for i := range ings { - if pol, exists := ings[i].Annotations[annotationRef]; exists { - if pol == namespace+"/"+name || (namespace == ings[i].Namespace && pol == name) { - apIngs = append(apIngs, ings[i]) - } - } - } - for _, mIng := range mIngs { - if pol, exists := mIng.Master.Ingress.Annotations[annotationRef]; exists { - if pol == namespace+"/"+name || (mIng.Master.Ingress.Namespace == namespace && pol == name) { - apIngs = append(apIngs, *mIng.Master.Ingress) - } - } - } - return apIngs + return deleteErr } // IsNginxReady returns ready status of NGINX diff --git a/internal/k8s/controller_test.go b/internal/k8s/controller_test.go index 905d297243..3187e754de 100644 --- a/internal/k8s/controller_test.go +++ b/internal/k8s/controller_test.go @@ -7,8 +7,6 @@ import ( "sort" "strings" "testing" - "time" - "unsafe" "github.com/nginxinc/kubernetes-ingress/internal/configs" "github.com/nginxinc/kubernetes-ingress/internal/configs/version1" @@ -21,7 +19,6 @@ import ( v1 "k8s.io/api/core/v1" networking "k8s.io/api/networking/v1beta1" meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/fields" "k8s.io/apimachinery/pkg/util/intstr" "k8s.io/client-go/kubernetes/fake" "k8s.io/client-go/tools/cache" @@ -335,499 +332,6 @@ func TestHasCorrectIngressClassVS(t *testing.T) { } } -func TestCreateMergableIngresses(t *testing.T) { - cafeMaster, coffeeMinion, teaMinion, lbc := getMergableDefaults() - - err := lbc.ingressLister.Add(&cafeMaster) - if err != nil { - t.Errorf("Error adding Ingress %v to the ingress lister: %v", &cafeMaster.Name, err) - } - - err = lbc.ingressLister.Add(&coffeeMinion) - if err != nil { - t.Errorf("Error adding Ingress %v to the ingress lister: %v", &coffeeMinion.Name, err) - } - - err = lbc.ingressLister.Add(&teaMinion) - if err != nil { - t.Errorf("Error adding Ingress %v to the ingress lister: %v", &teaMinion.Name, err) - } - - mergeableIngresses, err := lbc.createMergableIngresses(&cafeMaster) - if err != nil { - t.Errorf("Error creating Mergable Ingresses: %v", err) - } - if mergeableIngresses.Master.Ingress.Name != cafeMaster.Name && mergeableIngresses.Master.Ingress.Namespace != cafeMaster.Namespace { - t.Errorf("Master %s not set properly", cafeMaster.Name) - } - - if len(mergeableIngresses.Minions) != 2 { - t.Errorf("Invalid amount of minions in mergeableIngresses: %v", mergeableIngresses.Minions) - } - - coffeeCount := 0 - teaCount := 0 - for _, minion := range mergeableIngresses.Minions { - if minion.Ingress.Name == coffeeMinion.Name { - coffeeCount++ - } else if minion.Ingress.Name == teaMinion.Name { - teaCount++ - } else { - t.Errorf("Invalid Minion %s exists", minion.Ingress.Name) - } - } - - if coffeeCount != 1 { - t.Errorf("Invalid amount of coffee Minions, amount %d", coffeeCount) - } - - if teaCount != 1 { - t.Errorf("Invalid amount of tea Minions, amount %d", teaCount) - } -} - -func TestCreateMergableIngressesInvalidMaster(t *testing.T) { - cafeMaster, _, _, lbc := getMergableDefaults() - - // Test Error when Master has a Path - cafeMaster.Spec.Rules = []networking.IngressRule{ - { - Host: "ok.com", - IngressRuleValue: networking.IngressRuleValue{ - HTTP: &networking.HTTPIngressRuleValue{ - Paths: []networking.HTTPIngressPath{ - { - Path: "/coffee", - Backend: networking.IngressBackend{ - ServiceName: "coffee-svc", - ServicePort: intstr.IntOrString{ - StrVal: "80", - }, - }, - }, - }, - }, - }, - }, - } - err := lbc.ingressLister.Add(&cafeMaster) - if err != nil { - t.Errorf("Error adding Ingress %v to the ingress lister: %v", &cafeMaster.Name, err) - } - - expected := fmt.Errorf("Ingress Resource %v/%v with the 'nginx.org/mergeable-ingress-type' annotation set to 'master' cannot contain Paths", cafeMaster.Namespace, cafeMaster.Name) - _, err = lbc.createMergableIngresses(&cafeMaster) - if !reflect.DeepEqual(err, expected) { - t.Errorf("Error Validating the Ingress Resource: \n Expected: %s \n Obtained: %s", expected, err) - } -} - -func TestFindMasterForMinion(t *testing.T) { - cafeMaster, coffeeMinion, teaMinion, lbc := getMergableDefaults() - - // Makes sure there is an empty path assigned to a master, to allow for lbc.createIngress() to pass - cafeMaster.Spec.Rules[0].HTTP = &networking.HTTPIngressRuleValue{ - Paths: []networking.HTTPIngressPath{}, - } - - err := lbc.ingressLister.Add(&cafeMaster) - if err != nil { - t.Errorf("Error adding Ingress %v to the ingress lister: %v", &cafeMaster.Name, err) - } - - err = lbc.ingressLister.Add(&coffeeMinion) - if err != nil { - t.Errorf("Error adding Ingress %v to the ingress lister: %v", &coffeeMinion.Name, err) - } - - err = lbc.ingressLister.Add(&teaMinion) - if err != nil { - t.Errorf("Error adding Ingress %v to the ingress lister: %v", &teaMinion.Name, err) - } - - master, err := lbc.FindMasterForMinion(&coffeeMinion) - if err != nil { - t.Errorf("Error finding master for %s(Minion): %v", coffeeMinion.Name, err) - } - if master.Name != cafeMaster.Name && master.Namespace != cafeMaster.Namespace { - t.Errorf("Invalid Master found. Obtained %+v, Expected %+v", master, cafeMaster) - } - - master, err = lbc.FindMasterForMinion(&teaMinion) - if err != nil { - t.Errorf("Error finding master for %s(Minion): %v", teaMinion.Name, err) - } - if master.Name != cafeMaster.Name && master.Namespace != cafeMaster.Namespace { - t.Errorf("Invalid Master found. Obtained %+v, Expected %+v", master, cafeMaster) - } -} - -func TestFindMasterForMinionNoMaster(t *testing.T) { - _, coffeeMinion, teaMinion, lbc := getMergableDefaults() - - err := lbc.ingressLister.Add(&coffeeMinion) - if err != nil { - t.Errorf("Error adding Ingress %v to the ingress lister: %v", &coffeeMinion.Name, err) - } - - err = lbc.ingressLister.Add(&teaMinion) - if err != nil { - t.Errorf("Error adding Ingress %v to the ingress lister: %v", &teaMinion.Name, err) - } - - expected := fmt.Errorf("Could not find a Master for Minion: '%v/%v'", coffeeMinion.Namespace, coffeeMinion.Name) - _, err = lbc.FindMasterForMinion(&coffeeMinion) - if !reflect.DeepEqual(err, expected) { - t.Errorf("Expected: %s \nObtained: %s", expected, err) - } - - expected = fmt.Errorf("Could not find a Master for Minion: '%v/%v'", teaMinion.Namespace, teaMinion.Name) - _, err = lbc.FindMasterForMinion(&teaMinion) - if !reflect.DeepEqual(err, expected) { - t.Errorf("Error master found for %s(Minion): %v", teaMinion.Name, err) - } -} - -func TestFindMasterForMinionInvalidMinion(t *testing.T) { - cafeMaster, coffeeMinion, _, lbc := getMergableDefaults() - - // Makes sure there is an empty path assigned to a master, to allow for lbc.createIngress() to pass - cafeMaster.Spec.Rules[0].HTTP = &networking.HTTPIngressRuleValue{ - Paths: []networking.HTTPIngressPath{}, - } - - coffeeMinion.Spec.Rules = []networking.IngressRule{ - { - Host: "ok.com", - }, - } - - err := lbc.ingressLister.Add(&cafeMaster) - if err != nil { - t.Errorf("Error adding Ingress %v to the ingress lister: %v", &cafeMaster.Name, err) - } - - err = lbc.ingressLister.Add(&coffeeMinion) - if err != nil { - t.Errorf("Error adding Ingress %v to the ingress lister: %v", &coffeeMinion.Name, err) - } - - master, err := lbc.FindMasterForMinion(&coffeeMinion) - if err != nil { - t.Errorf("Error finding master for %s(Minion): %v", coffeeMinion.Name, err) - } - if master.Name != cafeMaster.Name && master.Namespace != cafeMaster.Namespace { - t.Errorf("Invalid Master found. Obtained %+v, Expected %+v", master, cafeMaster) - } -} - -func TestGetMinionsForMaster(t *testing.T) { - cafeMaster, coffeeMinion, teaMinion, lbc := getMergableDefaults() - - // Makes sure there is an empty path assigned to a master, to allow for lbc.createIngress() to pass - cafeMaster.Spec.Rules[0].HTTP = &networking.HTTPIngressRuleValue{ - Paths: []networking.HTTPIngressPath{}, - } - - err := lbc.ingressLister.Add(&cafeMaster) - if err != nil { - t.Errorf("Error adding Ingress %v to the ingress lister: %v", &cafeMaster.Name, err) - } - - err = lbc.ingressLister.Add(&coffeeMinion) - if err != nil { - t.Errorf("Error adding Ingress %v to the ingress lister: %v", &coffeeMinion.Name, err) - } - - err = lbc.ingressLister.Add(&teaMinion) - if err != nil { - t.Errorf("Error adding Ingress %v to the ingress lister: %v", &teaMinion.Name, err) - } - - cafeMasterIngEx, err := lbc.createIngress(&cafeMaster) - if err != nil { - t.Errorf("Error creating %s(Master): %v", cafeMaster.Name, err) - } - - minions, err := lbc.getMinionsForMaster(cafeMasterIngEx) - if err != nil { - t.Errorf("Error getting Minions for %s(Master): %v", cafeMaster.Name, err) - } - - if len(minions) != 2 { - t.Errorf("Invalid amount of minions: %+v", minions) - } - - coffeeCount := 0 - teaCount := 0 - for _, minion := range minions { - if minion.Ingress.Name == coffeeMinion.Name { - coffeeCount++ - } else if minion.Ingress.Name == teaMinion.Name { - teaCount++ - } else { - t.Errorf("Invalid Minion %s exists", minion.Ingress.Name) - } - } - - if coffeeCount != 1 { - t.Errorf("Invalid amount of coffee Minions, amount %d", coffeeCount) - } - - if teaCount != 1 { - t.Errorf("Invalid amount of tea Minions, amount %d", teaCount) - } -} - -func TestGetMinionsForMasterInvalidMinion(t *testing.T) { - cafeMaster, coffeeMinion, teaMinion, lbc := getMergableDefaults() - - // Makes sure there is an empty path assigned to a master, to allow for lbc.createIngress() to pass - cafeMaster.Spec.Rules[0].HTTP = &networking.HTTPIngressRuleValue{ - Paths: []networking.HTTPIngressPath{}, - } - - teaMinion.Spec.Rules = []networking.IngressRule{ - { - Host: "ok.com", - }, - } - - err := lbc.ingressLister.Add(&cafeMaster) - if err != nil { - t.Errorf("Error adding Ingress %v to the ingress lister: %v", &cafeMaster.Name, err) - } - - err = lbc.ingressLister.Add(&coffeeMinion) - if err != nil { - t.Errorf("Error adding Ingress %v to the ingress lister: %v", &coffeeMinion.Name, err) - } - - err = lbc.ingressLister.Add(&teaMinion) - if err != nil { - t.Errorf("Error adding Ingress %v to the ingress lister: %v", &teaMinion.Name, err) - } - - cafeMasterIngEx, err := lbc.createIngress(&cafeMaster) - if err != nil { - t.Errorf("Error creating %s(Master): %v", cafeMaster.Name, err) - } - - minions, err := lbc.getMinionsForMaster(cafeMasterIngEx) - if err != nil { - t.Errorf("Error getting Minions for %s(Master): %v", cafeMaster.Name, err) - } - - if len(minions) != 1 { - t.Errorf("Invalid amount of minions: %+v", minions) - } - - coffeeCount := 0 - teaCount := 0 - for _, minion := range minions { - if minion.Ingress.Name == coffeeMinion.Name { - coffeeCount++ - } else if minion.Ingress.Name == teaMinion.Name { - teaCount++ - } else { - t.Errorf("Invalid Minion %s exists", minion.Ingress.Name) - } - } - - if coffeeCount != 1 { - t.Errorf("Invalid amount of coffee Minions, amount %d", coffeeCount) - } - - if teaCount != 0 { - t.Errorf("Invalid amount of tea Minions, amount %d", teaCount) - } -} - -func TestGetMinionsForMasterConflictingPaths(t *testing.T) { - cafeMaster, coffeeMinion, teaMinion, lbc := getMergableDefaults() - - // Makes sure there is an empty path assigned to a master, to allow for lbc.createIngress() to pass - cafeMaster.Spec.Rules[0].HTTP = &networking.HTTPIngressRuleValue{ - Paths: []networking.HTTPIngressPath{}, - } - - coffeeMinion.Spec.Rules[0].HTTP.Paths = append(coffeeMinion.Spec.Rules[0].HTTP.Paths, networking.HTTPIngressPath{ - Path: "/tea", - Backend: networking.IngressBackend{ - ServiceName: "tea-svc", - ServicePort: intstr.IntOrString{ - StrVal: "80", - }, - }, - }) - - err := lbc.ingressLister.Add(&cafeMaster) - if err != nil { - t.Errorf("Error adding Ingress %v to the ingress lister: %v", &cafeMaster.Name, err) - } - - err = lbc.ingressLister.Add(&coffeeMinion) - if err != nil { - t.Errorf("Error adding Ingress %v to the ingress lister: %v", &coffeeMinion.Name, err) - } - - err = lbc.ingressLister.Add(&teaMinion) - if err != nil { - t.Errorf("Error adding Ingress %v to the ingress lister: %v", &teaMinion.Name, err) - } - - cafeMasterIngEx, err := lbc.createIngress(&cafeMaster) - if err != nil { - t.Errorf("Error creating %s(Master): %v", cafeMaster.Name, err) - } - - minions, err := lbc.getMinionsForMaster(cafeMasterIngEx) - if err != nil { - t.Errorf("Error getting Minions for %s(Master): %v", cafeMaster.Name, err) - } - - if len(minions) != 2 { - t.Errorf("Invalid amount of minions: %+v", minions) - } - - coffeePathCount := 0 - teaPathCount := 0 - for _, minion := range minions { - for _, path := range minion.Ingress.Spec.Rules[0].HTTP.Paths { - if path.Path == "/coffee" { - coffeePathCount++ - } else if path.Path == "/tea" { - teaPathCount++ - } else { - t.Errorf("Invalid Path %s exists", path.Path) - } - } - } - - if coffeePathCount != 1 { - t.Errorf("Invalid amount of coffee paths, amount %d", coffeePathCount) - } - - if teaPathCount != 1 { - t.Errorf("Invalid amount of tea paths, amount %d", teaPathCount) - } -} - -func getMergableDefaults() (cafeMaster, coffeeMinion, teaMinion networking.Ingress, lbc LoadBalancerController) { - cafeMaster = networking.Ingress{ - TypeMeta: meta_v1.TypeMeta{}, - ObjectMeta: meta_v1.ObjectMeta{ - Name: "cafe-master", - Namespace: "default", - Annotations: map[string]string{ - "kubernetes.io/ingress.class": "nginx", - "nginx.org/mergeable-ingress-type": "master", - }, - }, - Spec: networking.IngressSpec{ - Rules: []networking.IngressRule{ - { - Host: "ok.com", - }, - }, - }, - Status: networking.IngressStatus{}, - } - coffeeMinion = networking.Ingress{ - TypeMeta: meta_v1.TypeMeta{}, - ObjectMeta: meta_v1.ObjectMeta{ - Name: "coffee-minion", - Namespace: "default", - Annotations: map[string]string{ - "kubernetes.io/ingress.class": "nginx", - "nginx.org/mergeable-ingress-type": "minion", - }, - }, - Spec: networking.IngressSpec{ - Rules: []networking.IngressRule{ - { - Host: "ok.com", - IngressRuleValue: networking.IngressRuleValue{ - HTTP: &networking.HTTPIngressRuleValue{ - Paths: []networking.HTTPIngressPath{ - { - Path: "/coffee", - Backend: networking.IngressBackend{ - ServiceName: "coffee-svc", - ServicePort: intstr.IntOrString{ - StrVal: "80", - }, - }, - }, - }, - }, - }, - }, - }, - }, - Status: networking.IngressStatus{}, - } - teaMinion = networking.Ingress{ - TypeMeta: meta_v1.TypeMeta{}, - ObjectMeta: meta_v1.ObjectMeta{ - Name: "tea-minion", - Namespace: "default", - Annotations: map[string]string{ - "kubernetes.io/ingress.class": "nginx", - "nginx.org/mergeable-ingress-type": "minion", - }, - }, - Spec: networking.IngressSpec{ - Rules: []networking.IngressRule{ - { - Host: "ok.com", - IngressRuleValue: networking.IngressRuleValue{ - HTTP: &networking.HTTPIngressRuleValue{ - Paths: []networking.HTTPIngressPath{ - { - Path: "/tea", - }, - }, - }, - }, - }, - }, - }, - Status: networking.IngressStatus{}, - } - - ingExMap := make(map[string]*configs.IngressEx) - cafeMasterIngEx, _ := lbc.createIngress(&cafeMaster) - ingExMap["default-cafe-master"] = cafeMasterIngEx - - cnf := configs.NewConfigurator(&nginx.LocalManager{}, &configs.StaticConfigParams{}, &configs.ConfigParams{}, &configs.GlobalConfigParams{}, &version1.TemplateExecutor{}, &version2.TemplateExecutor{}, false, false, nil, false, nil, false) - - // edit private field ingresses to use in testing - pointerVal := reflect.ValueOf(cnf) - val := reflect.Indirect(pointerVal) - - field := val.FieldByName("ingresses") - ptrToField := unsafe.Pointer(field.UnsafeAddr()) - realPtrToField := (*map[string]*configs.IngressEx)(ptrToField) - *realPtrToField = ingExMap - - fakeClient := fake.NewSimpleClientset() - lbc = LoadBalancerController{ - client: fakeClient, - ingressClass: "nginx", - configurator: cnf, - metricsCollector: collectors.NewControllerFakeCollector(), - } - lbc.svcLister, _ = cache.NewInformer( - cache.NewListWatchFromClient(lbc.client.NetworkingV1beta1().RESTClient(), "services", "default", fields.Everything()), - &networking.Ingress{}, time.Duration(1), nil) - lbc.ingressLister.Store, _ = cache.NewInformer( - cache.NewListWatchFromClient(lbc.client.NetworkingV1beta1().RESTClient(), "ingresses", "default", fields.Everything()), - &networking.Ingress{}, time.Duration(1), nil) - - return -} - func TestComparePorts(t *testing.T) { scenarios := []struct { sp v1.ServicePort @@ -888,1090 +392,234 @@ func TestComparePorts(t *testing.T) { // Fall back on ServicePort.Port if TargetPort is empty v1.ServicePort{ Name: "name", - Port: 80, - }, - v1.ContainerPort{ - Name: "name", - ContainerPort: 80, - }, - true, - }, - { - // TargetPort intval mismatch - v1.ServicePort{ - TargetPort: intstr.FromInt(80), - }, - v1.ContainerPort{ - ContainerPort: 81, - }, - false, - }, - { - // don't match empty ports - v1.ServicePort{}, - v1.ContainerPort{}, - false, - }, - } - - for _, scen := range scenarios { - if scen.expected != compareContainerPortAndServicePort(scen.cp, scen.sp) { - t.Errorf("Expected: %v, ContainerPort: %v, ServicePort: %v", scen.expected, scen.cp, scen.sp) - } - } -} - -func TestFindProbeForPods(t *testing.T) { - pods := []*v1.Pod{ - { - Spec: v1.PodSpec{ - Containers: []v1.Container{ - { - ReadinessProbe: &v1.Probe{ - Handler: v1.Handler{ - HTTPGet: &v1.HTTPGetAction{ - Path: "/", - Host: "asdf.com", - Port: intstr.IntOrString{ - IntVal: 80, - }, - }, - }, - PeriodSeconds: 42, - }, - Ports: []v1.ContainerPort{ - { - Name: "name", - ContainerPort: 80, - Protocol: v1.ProtocolTCP, - HostIP: "1.2.3.4", - }, - }, - }, - }, - }, - }, - } - svcPort := v1.ServicePort{ - TargetPort: intstr.FromInt(80), - } - probe := findProbeForPods(pods, &svcPort) - if probe == nil || probe.PeriodSeconds != 42 { - t.Errorf("ServicePort.TargetPort as int match failed: %+v", probe) - } - - svcPort = v1.ServicePort{ - TargetPort: intstr.FromString("name"), - Protocol: v1.ProtocolTCP, - } - probe = findProbeForPods(pods, &svcPort) - if probe == nil || probe.PeriodSeconds != 42 { - t.Errorf("ServicePort.TargetPort as string failed: %+v", probe) - } - - svcPort = v1.ServicePort{ - TargetPort: intstr.FromInt(80), - Protocol: v1.ProtocolTCP, - } - probe = findProbeForPods(pods, &svcPort) - if probe == nil || probe.PeriodSeconds != 42 { - t.Errorf("ServicePort.TargetPort as int failed: %+v", probe) - } - - svcPort = v1.ServicePort{ - Port: 80, - } - probe = findProbeForPods(pods, &svcPort) - if probe == nil || probe.PeriodSeconds != 42 { - t.Errorf("ServicePort.Port should match if TargetPort is not set: %+v", probe) - } - - svcPort = v1.ServicePort{ - TargetPort: intstr.FromString("wrong_name"), - } - probe = findProbeForPods(pods, &svcPort) - if probe != nil { - t.Errorf("ServicePort.TargetPort should not have matched string: %+v", probe) - } - - svcPort = v1.ServicePort{ - TargetPort: intstr.FromInt(22), - } - probe = findProbeForPods(pods, &svcPort) - if probe != nil { - t.Errorf("ServicePort.TargetPort should not have matched int: %+v", probe) - } - - svcPort = v1.ServicePort{ - Port: 22, - } - probe = findProbeForPods(pods, &svcPort) - if probe != nil { - t.Errorf("ServicePort.Port mismatch: %+v", probe) - } - -} - -func TestGetServicePortForIngressPort(t *testing.T) { - fakeClient := fake.NewSimpleClientset() - cnf := configs.NewConfigurator(&nginx.LocalManager{}, &configs.StaticConfigParams{}, &configs.ConfigParams{}, &configs.GlobalConfigParams{}, &version1.TemplateExecutor{}, &version2.TemplateExecutor{}, false, false, nil, false, nil, false) - lbc := LoadBalancerController{ - client: fakeClient, - ingressClass: "nginx", - configurator: cnf, - metricsCollector: collectors.NewControllerFakeCollector(), - } - svc := v1.Service{ - TypeMeta: meta_v1.TypeMeta{}, - ObjectMeta: meta_v1.ObjectMeta{ - Name: "coffee-svc", - Namespace: "default", - }, - Spec: v1.ServiceSpec{ - Ports: []v1.ServicePort{ - { - Name: "foo", - Port: 80, - TargetPort: intstr.FromInt(22), - }, - }, - }, - Status: v1.ServiceStatus{}, - } - ingSvcPort := intstr.FromString("foo") - svcPort := lbc.getServicePortForIngressPort(ingSvcPort, &svc) - if svcPort == nil || svcPort.Port != 80 { - t.Errorf("TargetPort string match failed: %+v", svcPort) - } - - ingSvcPort = intstr.FromInt(80) - svcPort = lbc.getServicePortForIngressPort(ingSvcPort, &svc) - if svcPort == nil || svcPort.Port != 80 { - t.Errorf("TargetPort int match failed: %+v", svcPort) - } - - ingSvcPort = intstr.FromInt(22) - svcPort = lbc.getServicePortForIngressPort(ingSvcPort, &svc) - if svcPort != nil { - t.Errorf("Mismatched ints should not return port: %+v", svcPort) - } - ingSvcPort = intstr.FromString("bar") - svcPort = lbc.getServicePortForIngressPort(ingSvcPort, &svc) - if svcPort != nil { - t.Errorf("Mismatched strings should not return port: %+v", svcPort) - } -} - -func TestFindIngressesForSecret(t *testing.T) { - testCases := []struct { - secret v1.Secret - ingress networking.Ingress - expectedToFind bool - desc string - }{ - { - secret: v1.Secret{ - ObjectMeta: meta_v1.ObjectMeta{ - Name: "my-tls-secret", - Namespace: "namespace-1", - }, - }, - ingress: networking.Ingress{ - ObjectMeta: meta_v1.ObjectMeta{ - Name: "my-ingress", - Namespace: "namespace-1", - }, - Spec: networking.IngressSpec{ - TLS: []networking.IngressTLS{ - { - SecretName: "my-tls-secret", - }, - }, - }, - }, - expectedToFind: true, - desc: "an Ingress references a TLS Secret that exists in the Ingress namespace", - }, - { - secret: v1.Secret{ - ObjectMeta: meta_v1.ObjectMeta{ - Name: "my-tls-secret", - Namespace: "namespace-1", - }, - }, - ingress: networking.Ingress{ - ObjectMeta: meta_v1.ObjectMeta{ - Name: "my-ingress", - Namespace: "namespace-2", - }, - Spec: networking.IngressSpec{ - TLS: []networking.IngressTLS{ - { - SecretName: "my-tls-secret", - }, - }, - }, - }, - expectedToFind: false, - desc: "an Ingress references a TLS Secret that exists in a different namespace", - }, - { - secret: v1.Secret{ - ObjectMeta: meta_v1.ObjectMeta{ - Name: "my-jwk-secret", - Namespace: "namespace-1", - }, - }, - ingress: networking.Ingress{ - ObjectMeta: meta_v1.ObjectMeta{ - Name: "my-ingress", - Namespace: "namespace-1", - Annotations: map[string]string{ - configs.JWTKeyAnnotation: "my-jwk-secret", - }, - }, - }, - expectedToFind: true, - desc: "an Ingress references a JWK Secret that exists in the Ingress namespace", - }, - { - secret: v1.Secret{ - ObjectMeta: meta_v1.ObjectMeta{ - Name: "my-jwk-secret", - Namespace: "namespace-1", - }, - }, - ingress: networking.Ingress{ - ObjectMeta: meta_v1.ObjectMeta{ - Name: "my-ingress", - Namespace: "namespace-2", - Annotations: map[string]string{ - configs.JWTKeyAnnotation: "my-jwk-secret", - }, - }, - }, - expectedToFind: false, - desc: "an Ingress references a JWK secret that exists in a different namespace", - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - fakeClient := fake.NewSimpleClientset() - - templateExecutor, err := version1.NewTemplateExecutor("../configs/version1/nginx-plus.tmpl", "../configs/version1/nginx-plus.ingress.tmpl") - if err != nil { - t.Fatalf("templateExecutor could not start: %v", err) - } - - templateExecutorV2, err := version2.NewTemplateExecutor("../configs/version2/nginx-plus.virtualserver.tmpl", "../configs/version2/nginx-plus.transportserver.tmpl") - if err != nil { - t.Fatalf("templateExecutorV2 could not start: %v", err) - } - - manager := nginx.NewFakeManager("/etc/nginx") - - cnf := configs.NewConfigurator(manager, &configs.StaticConfigParams{}, &configs.ConfigParams{}, &configs.GlobalConfigParams{}, templateExecutor, templateExecutorV2, false, false, nil, false, nil, false) - lbc := LoadBalancerController{ - client: fakeClient, - ingressClass: "nginx", - configurator: cnf, - isNginxPlus: true, - metricsCollector: collectors.NewControllerFakeCollector(), - } - - lbc.ingressLister.Store, _ = cache.NewInformer( - cache.NewListWatchFromClient(lbc.client.NetworkingV1beta1().RESTClient(), "ingresses", "default", fields.Everything()), - &networking.Ingress{}, time.Duration(1), nil) - - lbc.secretLister.Store, lbc.secretController = cache.NewInformer( - cache.NewListWatchFromClient(lbc.client.CoreV1().RESTClient(), "secrets", "default", fields.Everything()), - &v1.Secret{}, time.Duration(1), nil) - - ngxIngress := &configs.IngressEx{ - Ingress: &test.ingress, - TLSSecrets: map[string]*v1.Secret{ - test.secret.Name: &test.secret, - }, - } - - err = cnf.AddOrUpdateIngress(ngxIngress) - if err != nil { - t.Fatalf("Ingress was not added: %v", err) - } - - err = lbc.ingressLister.Add(&test.ingress) - if err != nil { - t.Errorf("Error adding Ingress %v to the ingress lister: %v", &test.ingress.Name, err) - } - - err = lbc.secretLister.Add(&test.secret) - if err != nil { - t.Errorf("Error adding Secret %v to the secret lister: %v", &test.secret.Name, err) - } - - ings, err := lbc.findIngressesForSecret(test.secret.Namespace, test.secret.Name) - if err != nil { - t.Fatalf("Couldn't find Ingress resource: %v", err) - } - - if len(ings) > 0 { - if !test.expectedToFind { - t.Fatalf("Expected 0 ingresses. Got: %v", len(ings)) - } - if len(ings) != 1 { - t.Fatalf("Expected 1 ingress. Got: %v", len(ings)) - } - if ings[0].Name != test.ingress.Name || ings[0].Namespace != test.ingress.Namespace { - t.Fatalf("Expected: %v/%v. Got: %v/%v.", test.ingress.Namespace, test.ingress.Name, ings[0].Namespace, ings[0].Name) - } - } else if test.expectedToFind { - t.Fatal("Expected 1 ingress. Got: 0") - } - }) - } -} - -func TestFindIngressesForSecretWithMinions(t *testing.T) { - testCases := []struct { - secret v1.Secret - ingress networking.Ingress - expectedToFind bool - desc string - }{ - { - secret: v1.Secret{ - ObjectMeta: meta_v1.ObjectMeta{ - Name: "my-jwk-secret", - Namespace: "default", - }, - }, - ingress: networking.Ingress{ - ObjectMeta: meta_v1.ObjectMeta{ - Name: "cafe-ingress-tea-minion", - Namespace: "default", - Annotations: map[string]string{ - "kubernetes.io/ingress.class": "nginx", - "nginx.org/mergeable-ingress-type": "minion", - configs.JWTKeyAnnotation: "my-jwk-secret", - }, - }, - Spec: networking.IngressSpec{ - Rules: []networking.IngressRule{ - { - Host: "cafe.example.com", - IngressRuleValue: networking.IngressRuleValue{ - HTTP: &networking.HTTPIngressRuleValue{ - Paths: []networking.HTTPIngressPath{ - { - Path: "/tea", - Backend: networking.IngressBackend{ - ServiceName: "tea-svc", - ServicePort: intstr.FromString("80"), - }, - }, - }, - }, - }, - }, - }, - }, - }, - expectedToFind: true, - desc: "a minion Ingress references a JWK Secret that exists in the Ingress namespace", - }, - { - secret: v1.Secret{ - ObjectMeta: meta_v1.ObjectMeta{ - Name: "my-jwk-secret", - Namespace: "namespace-1", - }, - }, - ingress: networking.Ingress{ - ObjectMeta: meta_v1.ObjectMeta{ - Name: "cafe-ingress-tea-minion", - Namespace: "default", - Annotations: map[string]string{ - "kubernetes.io/ingress.class": "nginx", - "nginx.org/mergeable-ingress-type": "minion", - configs.JWTKeyAnnotation: "my-jwk-secret", - }, - }, - Spec: networking.IngressSpec{ - Rules: []networking.IngressRule{ - { - Host: "cafe.example.com", - IngressRuleValue: networking.IngressRuleValue{ - HTTP: &networking.HTTPIngressRuleValue{ - Paths: []networking.HTTPIngressPath{ - { - Path: "/tea", - Backend: networking.IngressBackend{ - ServiceName: "tea-svc", - ServicePort: intstr.FromString("80"), - }, - }, - }, - }, - }, - }, - }, - }, - }, - expectedToFind: false, - desc: "a Minion references a JWK secret that exists in a different namespace", - }, - } - - master := networking.Ingress{ - ObjectMeta: meta_v1.ObjectMeta{ - Name: "cafe-ingress-master", - Namespace: "default", - Annotations: map[string]string{ - "kubernetes.io/ingress.class": "nginx", - "nginx.org/mergeable-ingress-type": "master", - }, - }, - Spec: networking.IngressSpec{ - Rules: []networking.IngressRule{ - { - Host: "cafe.example.com", - IngressRuleValue: networking.IngressRuleValue{ - HTTP: &networking.HTTPIngressRuleValue{ // HTTP must not be nil for Master - Paths: []networking.HTTPIngressPath{}, - }, - }, - }, - }, - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - fakeClient := fake.NewSimpleClientset() - - templateExecutor, err := version1.NewTemplateExecutor("../configs/version1/nginx-plus.tmpl", "../configs/version1/nginx-plus.ingress.tmpl") - if err != nil { - t.Fatalf("templateExecutor could not start: %v", err) - } - - templateExecutorV2, err := version2.NewTemplateExecutor("../configs/version2/nginx-plus.virtualserver.tmpl", "../configs/version2/nginx-plus.transportserver.tmpl") - if err != nil { - t.Fatalf("templateExecutorV2 could not start: %v", err) - } - - manager := nginx.NewFakeManager("/etc/nginx") - - cnf := configs.NewConfigurator(manager, &configs.StaticConfigParams{}, &configs.ConfigParams{}, &configs.GlobalConfigParams{}, templateExecutor, templateExecutorV2, false, false, nil, false, nil, false) - lbc := LoadBalancerController{ - client: fakeClient, - ingressClass: "nginx", - configurator: cnf, - isNginxPlus: true, - metricsCollector: collectors.NewControllerFakeCollector(), - } - - lbc.ingressLister.Store, _ = cache.NewInformer( - cache.NewListWatchFromClient(lbc.client.NetworkingV1beta1().RESTClient(), "ingresses", "default", fields.Everything()), - &networking.Ingress{}, time.Duration(1), nil) - - lbc.secretLister.Store, lbc.secretController = cache.NewInformer( - cache.NewListWatchFromClient(lbc.client.CoreV1().RESTClient(), "secrets", "default", fields.Everything()), - &v1.Secret{}, time.Duration(1), nil) - - mergeable := &configs.MergeableIngresses{ - Master: &configs.IngressEx{ - Ingress: &master, - }, - Minions: []*configs.IngressEx{ - { - Ingress: &test.ingress, - JWTKey: configs.JWTKey{ - Name: test.secret.Name, - }, - }, - }, - } - - err = cnf.AddOrUpdateMergeableIngress(mergeable) - if err != nil { - t.Fatalf("Ingress was not added: %v", err) - } - - err = lbc.ingressLister.Add(&master) - if err != nil { - t.Errorf("Error adding Ingress %v to the ingress lister: %v", &master.Name, err) - } - - err = lbc.ingressLister.Add(&test.ingress) - if err != nil { - t.Errorf("Error adding Ingress %v to the ingress lister: %v", &test.ingress.Name, err) - } - - err = lbc.secretLister.Add(&test.secret) - if err != nil { - t.Errorf("Error adding Secret %v to the secret lister: %v", &test.secret.Name, err) - } - - ings, err := lbc.findIngressesForSecret(test.secret.Namespace, test.secret.Name) - if err != nil { - t.Fatalf("Couldn't find Ingress resource: %v", err) - } - - if len(ings) > 0 { - if !test.expectedToFind { - t.Fatalf("Expected 0 ingresses. Got: %v", len(ings)) - } - if len(ings) != 1 { - t.Fatalf("Expected 1 ingress. Got: %v", len(ings)) - } - if ings[0].Name != test.ingress.Name || ings[0].Namespace != test.ingress.Namespace { - t.Fatalf("Expected: %v/%v. Got: %v/%v.", test.ingress.Namespace, test.ingress.Name, ings[0].Namespace, ings[0].Name) - } - } else if test.expectedToFind { - t.Fatal("Expected 1 ingress. Got: 0") - } - }) - } -} - -func TestFindVirtualServersForService(t *testing.T) { - vs1 := conf_v1.VirtualServer{ - ObjectMeta: meta_v1.ObjectMeta{ - Name: "vs-1", - Namespace: "ns-1", - }, - Spec: conf_v1.VirtualServerSpec{ - Upstreams: []conf_v1.Upstream{ - { - Service: "test-service", - }, - }, - }, - } - vs2 := conf_v1.VirtualServer{ - ObjectMeta: meta_v1.ObjectMeta{ - Name: "vs-2", - Namespace: "ns-1", - }, - Spec: conf_v1.VirtualServerSpec{ - Upstreams: []conf_v1.Upstream{ - { - Service: "some-service", - }, - }, - }, - } - vs3 := conf_v1.VirtualServer{ - ObjectMeta: meta_v1.ObjectMeta{ - Name: "vs-3", - Namespace: "ns-2", - }, - Spec: conf_v1.VirtualServerSpec{ - Upstreams: []conf_v1.Upstream{ - { - Service: "test-service", - }, - }, - }, - } - virtualServers := []*conf_v1.VirtualServer{&vs1, &vs2, &vs3} - - service := v1.Service{ - ObjectMeta: meta_v1.ObjectMeta{ - Name: "test-service", - Namespace: "ns-1", - }, - } - - expected := []*conf_v1.VirtualServer{&vs1} - - result := findVirtualServersForService(virtualServers, &service) - if !reflect.DeepEqual(result, expected) { - t.Errorf("findVirtualServersForService returned %v but expected %v", result, expected) - } -} - -func TestFindVirtualServerRoutesForService(t *testing.T) { - vsr1 := conf_v1.VirtualServerRoute{ - ObjectMeta: meta_v1.ObjectMeta{ - Name: "vsr-1", - Namespace: "ns-1", - }, - Spec: conf_v1.VirtualServerRouteSpec{ - Upstreams: []conf_v1.Upstream{ - { - Service: "test-service", - }, - }, - }, - } - vsr2 := conf_v1.VirtualServerRoute{ - ObjectMeta: meta_v1.ObjectMeta{ - Name: "vsr-2", - Namespace: "ns-1", - }, - Spec: conf_v1.VirtualServerRouteSpec{ - Upstreams: []conf_v1.Upstream{ - { - Service: "some-service", - }, - }, - }, - } - vsr3 := conf_v1.VirtualServerRoute{ - ObjectMeta: meta_v1.ObjectMeta{ - Name: "vrs-3", - Namespace: "ns-2", - }, - Spec: conf_v1.VirtualServerRouteSpec{ - Upstreams: []conf_v1.Upstream{ - { - Service: "test-service", - }, - }, - }, - } - virtualServerRoutes := []*conf_v1.VirtualServerRoute{&vsr1, &vsr2, &vsr3} - - service := v1.Service{ - ObjectMeta: meta_v1.ObjectMeta{ - Name: "test-service", - Namespace: "ns-1", - }, - } - - expected := []*conf_v1.VirtualServerRoute{&vsr1} - - result := findVirtualServerRoutesForService(virtualServerRoutes, &service) - if !reflect.DeepEqual(result, expected) { - t.Errorf("findVirtualServerRoutesForService returned %v but expected %v", result, expected) - } -} - -func TestFindOrphanedVirtualServerRoute(t *testing.T) { - vsr1 := conf_v1.VirtualServerRoute{ - ObjectMeta: meta_v1.ObjectMeta{ - Name: "vsr-1", - Namespace: "ns-1", - }, - } - - vsr2 := conf_v1.VirtualServerRoute{ - ObjectMeta: meta_v1.ObjectMeta{ - Name: "vsr-2", - Namespace: "ns-1", - }, - } - - vsr3 := conf_v1.VirtualServerRoute{ - ObjectMeta: meta_v1.ObjectMeta{ - Name: "vsr-3", - Namespace: "ns-2", - }, - } - - vsr4 := conf_v1.VirtualServerRoute{ - ObjectMeta: meta_v1.ObjectMeta{ - Name: "vsr-4", - Namespace: "ns-1", - }, - } - - vsr5 := conf_v1.VirtualServerRoute{ - ObjectMeta: meta_v1.ObjectMeta{ - Name: "vsr-5", - Namespace: "ns-3", - }, - } - - vsrs := []*conf_v1.VirtualServerRoute{&vsr1, &vsr2, &vsr3, &vsr4, &vsr5} - - handledVSRs := []*conf_v1.VirtualServerRoute{&vsr3} - - expected := []*conf_v1.VirtualServerRoute{&vsr1, &vsr2, &vsr4, &vsr5} - - result := findOrphanedVirtualServerRoutes(vsrs, handledVSRs) - - if !reflect.DeepEqual(result, expected) { - t.Errorf("findOrphanedVirtualServerRoutes return %v but expected %v", result, expected) - } -} - -func TestFindTransportServersForService(t *testing.T) { - ts1 := conf_v1alpha1.TransportServer{ - ObjectMeta: meta_v1.ObjectMeta{ - Name: "ts-1", - Namespace: "ns-1", - }, - Spec: conf_v1alpha1.TransportServerSpec{ - Upstreams: []conf_v1alpha1.Upstream{ - { - Service: "test-service", - }, - }, - }, - } - ts2 := conf_v1alpha1.TransportServer{ - ObjectMeta: meta_v1.ObjectMeta{ - Name: "ts-2", - Namespace: "ns-1", - }, - Spec: conf_v1alpha1.TransportServerSpec{ - Upstreams: []conf_v1alpha1.Upstream{ - { - Service: "some-service", - }, - }, - }, - } - ts3 := conf_v1alpha1.TransportServer{ - ObjectMeta: meta_v1.ObjectMeta{ - Name: "ts-3", - Namespace: "ns-2", - }, - Spec: conf_v1alpha1.TransportServerSpec{ - Upstreams: []conf_v1alpha1.Upstream{ - { - Service: "test-service", - }, - }, - }, - } - transportServers := []*conf_v1alpha1.TransportServer{&ts1, &ts2, &ts3} - - service := v1.Service{ - ObjectMeta: meta_v1.ObjectMeta{ - Name: "test-service", - Namespace: "ns-1", - }, - } - - expected := []*conf_v1alpha1.TransportServer{&ts1} - - result := findTransportServersForService(transportServers, &service) - if !reflect.DeepEqual(result, expected) { - t.Errorf("findTransportServersForService returned %v but expected %v", result, expected) - } -} - -func TestFindVirtualServersForSecret(t *testing.T) { - vs1 := conf_v1.VirtualServer{ - ObjectMeta: meta_v1.ObjectMeta{ - Name: "vs-1", - Namespace: "ns-1", - }, - Spec: conf_v1.VirtualServerSpec{ - TLS: nil, - }, - } - vs2 := conf_v1.VirtualServer{ - ObjectMeta: meta_v1.ObjectMeta{ - Name: "vs-2", - Namespace: "ns-1", - }, - Spec: conf_v1.VirtualServerSpec{ - TLS: &conf_v1.TLS{ - Secret: "", - }, - }, - } - vs3 := conf_v1.VirtualServer{ - ObjectMeta: meta_v1.ObjectMeta{ - Name: "vs-3", - Namespace: "ns-1", - }, - Spec: conf_v1.VirtualServerSpec{ - TLS: &conf_v1.TLS{ - Secret: "some-secret", - }, - }, - } - vs4 := conf_v1.VirtualServer{ - ObjectMeta: meta_v1.ObjectMeta{ - Name: "vs-4", - Namespace: "ns-1", - }, - Spec: conf_v1.VirtualServerSpec{ - TLS: &conf_v1.TLS{ - Secret: "test-secret", - }, - }, - } - vs5 := conf_v1.VirtualServer{ - ObjectMeta: meta_v1.ObjectMeta{ - Name: "vs-5", - Namespace: "ns-2", - }, - Spec: conf_v1.VirtualServerSpec{ - TLS: &conf_v1.TLS{ - Secret: "test-secret", - }, - }, - } - - virtualServers := []*conf_v1.VirtualServer{&vs1, &vs2, &vs3, &vs4, &vs5} - - expected := []*conf_v1.VirtualServer{&vs4} - - result := findVirtualServersForSecret(virtualServers, "ns-1", "test-secret") - if !reflect.DeepEqual(result, expected) { - t.Errorf("findVirtualServersForSecret returned %v but expected %v", result, expected) - } -} - -func TestFindVirtualServersForPolicy(t *testing.T) { - vs1 := conf_v1.VirtualServer{ - ObjectMeta: meta_v1.ObjectMeta{ - Name: "vs-1", - Namespace: "ns-1", - }, - Spec: conf_v1.VirtualServerSpec{ - Policies: []conf_v1.PolicyReference{ - { - Name: "test-policy", - Namespace: "ns-1", - }, - }, - }, - } - vs2 := conf_v1.VirtualServer{ - ObjectMeta: meta_v1.ObjectMeta{ - Name: "vs-2", - Namespace: "ns-1", - }, - Spec: conf_v1.VirtualServerSpec{ - Policies: []conf_v1.PolicyReference{ - { - Name: "some-policy", - Namespace: "ns-1", - }, - }, - }, - } - vs3 := conf_v1.VirtualServer{ - ObjectMeta: meta_v1.ObjectMeta{ - Name: "vs-3", - Namespace: "ns-1", - }, - Spec: conf_v1.VirtualServerSpec{ - Routes: []conf_v1.Route{ - { - Policies: []conf_v1.PolicyReference{ - { - Name: "test-policy", - Namespace: "ns-1", - }, - }, - }, - }, - }, - } - vs4 := conf_v1.VirtualServer{ - ObjectMeta: meta_v1.ObjectMeta{ - Name: "vs-4", - Namespace: "ns-1", - }, - Spec: conf_v1.VirtualServerSpec{ - Routes: []conf_v1.Route{ - { - Policies: []conf_v1.PolicyReference{ - { - Name: "some-policy", - Namespace: "ns-1", - }, - }, - }, - }, - }, - } - - virtualServers := []*conf_v1.VirtualServer{&vs1, &vs2, &vs3, &vs4} - - expected := []*conf_v1.VirtualServer{&vs1, &vs3} - - result := findVirtualServersForPolicy(virtualServers, "ns-1", "test-policy") - if !reflect.DeepEqual(result, expected) { - t.Errorf("findVirtualServersForPolicy() returned %v but expected %v", result, expected) - } -} - -func TestIsPolicyIsReferenced(t *testing.T) { - tests := []struct { - policies []conf_v1.PolicyReference - resourceNamespace string - policyNamespace string - policyName string - expected bool - msg string - }{ - { - policies: []conf_v1.PolicyReference{ - { - Name: "test-policy", - }, + Port: 80, }, - resourceNamespace: "ns-1", - policyNamespace: "ns-1", - policyName: "test-policy", - expected: true, - msg: "reference with implicit namespace", - }, - { - policies: []conf_v1.PolicyReference{ - { - Name: "test-policy", - Namespace: "ns-1", - }, + v1.ContainerPort{ + Name: "name", + ContainerPort: 80, }, - resourceNamespace: "ns-1", - policyNamespace: "ns-1", - policyName: "test-policy", - expected: true, - msg: "reference with explicit namespace", + true, }, { - policies: []conf_v1.PolicyReference{ - { - Name: "test-policy", - }, + // TargetPort intval mismatch + v1.ServicePort{ + TargetPort: intstr.FromInt(80), + }, + v1.ContainerPort{ + ContainerPort: 81, }, - resourceNamespace: "ns-2", - policyNamespace: "ns-1", - policyName: "test-policy", - expected: false, - msg: "wrong namespace with implicit namespace", + false, }, { - policies: []conf_v1.PolicyReference{ - { - Name: "test-policy", - Namespace: "ns-2", - }, - }, - resourceNamespace: "ns-2", - policyNamespace: "ns-1", - policyName: "test-policy", - expected: false, - msg: "wrong namespace with explicit namespace", + // don't match empty ports + v1.ServicePort{}, + v1.ContainerPort{}, + false, }, } - for _, test := range tests { - result := isPolicyReferenced(test.policies, test.resourceNamespace, test.policyNamespace, test.policyName) - if result != test.expected { - t.Errorf("isPolicyReferenced() returned %v but expected %v for the case of %s", result, - test.expected, test.msg) + for _, scen := range scenarios { + if scen.expected != compareContainerPortAndServicePort(scen.cp, scen.sp) { + t.Errorf("Expected: %v, ContainerPort: %v, ServicePort: %v", scen.expected, scen.cp, scen.sp) } } } -func TestFindVirtualServerRoutesForPolicy(t *testing.T) { - vsr1 := conf_v1.VirtualServerRoute{ - ObjectMeta: meta_v1.ObjectMeta{ - Name: "vsr-1", - Namespace: "ns-1", - }, - Spec: conf_v1.VirtualServerRouteSpec{ - Subroutes: []conf_v1.Route{ - { - Policies: []conf_v1.PolicyReference{ - { - Name: "test-policy", - Namespace: "ns-1", +func TestFindProbeForPods(t *testing.T) { + pods := []*v1.Pod{ + { + Spec: v1.PodSpec{ + Containers: []v1.Container{ + { + ReadinessProbe: &v1.Probe{ + Handler: v1.Handler{ + HTTPGet: &v1.HTTPGetAction{ + Path: "/", + Host: "asdf.com", + Port: intstr.IntOrString{ + IntVal: 80, + }, + }, + }, + PeriodSeconds: 42, + }, + Ports: []v1.ContainerPort{ + { + Name: "name", + ContainerPort: 80, + Protocol: v1.ProtocolTCP, + HostIP: "1.2.3.4", + }, }, }, }, }, }, } - vsr2 := conf_v1.VirtualServerRoute{ + svcPort := v1.ServicePort{ + TargetPort: intstr.FromInt(80), + } + probe := findProbeForPods(pods, &svcPort) + if probe == nil || probe.PeriodSeconds != 42 { + t.Errorf("ServicePort.TargetPort as int match failed: %+v", probe) + } + + svcPort = v1.ServicePort{ + TargetPort: intstr.FromString("name"), + Protocol: v1.ProtocolTCP, + } + probe = findProbeForPods(pods, &svcPort) + if probe == nil || probe.PeriodSeconds != 42 { + t.Errorf("ServicePort.TargetPort as string failed: %+v", probe) + } + + svcPort = v1.ServicePort{ + TargetPort: intstr.FromInt(80), + Protocol: v1.ProtocolTCP, + } + probe = findProbeForPods(pods, &svcPort) + if probe == nil || probe.PeriodSeconds != 42 { + t.Errorf("ServicePort.TargetPort as int failed: %+v", probe) + } + + svcPort = v1.ServicePort{ + Port: 80, + } + probe = findProbeForPods(pods, &svcPort) + if probe == nil || probe.PeriodSeconds != 42 { + t.Errorf("ServicePort.Port should match if TargetPort is not set: %+v", probe) + } + + svcPort = v1.ServicePort{ + TargetPort: intstr.FromString("wrong_name"), + } + probe = findProbeForPods(pods, &svcPort) + if probe != nil { + t.Errorf("ServicePort.TargetPort should not have matched string: %+v", probe) + } + + svcPort = v1.ServicePort{ + TargetPort: intstr.FromInt(22), + } + probe = findProbeForPods(pods, &svcPort) + if probe != nil { + t.Errorf("ServicePort.TargetPort should not have matched int: %+v", probe) + } + + svcPort = v1.ServicePort{ + Port: 22, + } + probe = findProbeForPods(pods, &svcPort) + if probe != nil { + t.Errorf("ServicePort.Port mismatch: %+v", probe) + } + +} + +func TestGetServicePortForIngressPort(t *testing.T) { + fakeClient := fake.NewSimpleClientset() + cnf := configs.NewConfigurator(&nginx.LocalManager{}, &configs.StaticConfigParams{}, &configs.ConfigParams{}, &configs.GlobalConfigParams{}, &version1.TemplateExecutor{}, &version2.TemplateExecutor{}, false, false, nil, false, nil, false) + lbc := LoadBalancerController{ + client: fakeClient, + ingressClass: "nginx", + configurator: cnf, + metricsCollector: collectors.NewControllerFakeCollector(), + } + svc := v1.Service{ + TypeMeta: meta_v1.TypeMeta{}, ObjectMeta: meta_v1.ObjectMeta{ - Name: "vsr-2", - Namespace: "ns-1", + Name: "coffee-svc", + Namespace: "default", }, - Spec: conf_v1.VirtualServerRouteSpec{ - Subroutes: []conf_v1.Route{ + Spec: v1.ServiceSpec{ + Ports: []v1.ServicePort{ { - Policies: []conf_v1.PolicyReference{ - { - Name: "some-policy", - Namespace: "ns-1", - }, - }, + Name: "foo", + Port: 80, + TargetPort: intstr.FromInt(22), }, }, }, + Status: v1.ServiceStatus{}, + } + ingSvcPort := intstr.FromString("foo") + svcPort := lbc.getServicePortForIngressPort(ingSvcPort, &svc) + if svcPort == nil || svcPort.Port != 80 { + t.Errorf("TargetPort string match failed: %+v", svcPort) } - virtualServerRoutes := []*conf_v1.VirtualServerRoute{&vsr1, &vsr2} - - expected := []*conf_v1.VirtualServerRoute{&vsr1} + ingSvcPort = intstr.FromInt(80) + svcPort = lbc.getServicePortForIngressPort(ingSvcPort, &svc) + if svcPort == nil || svcPort.Port != 80 { + t.Errorf("TargetPort int match failed: %+v", svcPort) + } - result := findVirtualServerRoutesForPolicy(virtualServerRoutes, "ns-1", "test-policy") - if !reflect.DeepEqual(result, expected) { - t.Errorf("findVirtualServerRoutesForPolicy() returned %v but expected %v", result, expected) + ingSvcPort = intstr.FromInt(22) + svcPort = lbc.getServicePortForIngressPort(ingSvcPort, &svc) + if svcPort != nil { + t.Errorf("Mismatched ints should not return port: %+v", svcPort) + } + ingSvcPort = intstr.FromString("bar") + svcPort = lbc.getServicePortForIngressPort(ingSvcPort, &svc) + if svcPort != nil { + t.Errorf("Mismatched strings should not return port: %+v", svcPort) } } -func TestFindVirtualServersForVirtualServerRoute(t *testing.T) { - vs1 := conf_v1.VirtualServer{ +func TestFindTransportServersForService(t *testing.T) { + ts1 := conf_v1alpha1.TransportServer{ ObjectMeta: meta_v1.ObjectMeta{ - Name: "vs-1", + Name: "ts-1", Namespace: "ns-1", }, - Spec: conf_v1.VirtualServerSpec{ - Routes: []conf_v1.Route{ + Spec: conf_v1alpha1.TransportServerSpec{ + Upstreams: []conf_v1alpha1.Upstream{ { - Path: "/", - Route: "default/test", + Service: "test-service", }, }, }, } - vs2 := conf_v1.VirtualServer{ + ts2 := conf_v1alpha1.TransportServer{ ObjectMeta: meta_v1.ObjectMeta{ - Name: "vs-2", + Name: "ts-2", Namespace: "ns-1", }, - Spec: conf_v1.VirtualServerSpec{ - Routes: []conf_v1.Route{ + Spec: conf_v1alpha1.TransportServerSpec{ + Upstreams: []conf_v1alpha1.Upstream{ { - Path: "/", - Route: "some-ns/test", + Service: "some-service", }, }, }, } - vs3 := conf_v1.VirtualServer{ + ts3 := conf_v1alpha1.TransportServer{ ObjectMeta: meta_v1.ObjectMeta{ - Name: "vs-3", - Namespace: "ns-1", + Name: "ts-3", + Namespace: "ns-2", }, - Spec: conf_v1.VirtualServerSpec{ - Routes: []conf_v1.Route{ + Spec: conf_v1alpha1.TransportServerSpec{ + Upstreams: []conf_v1alpha1.Upstream{ { - Path: "/", - Route: "default/test", + Service: "test-service", }, }, }, } + transportServers := []*conf_v1alpha1.TransportServer{&ts1, &ts2, &ts3} - vsr := conf_v1.VirtualServerRoute{ + service := v1.Service{ ObjectMeta: meta_v1.ObjectMeta{ - Name: "test", - Namespace: "default", + Name: "test-service", + Namespace: "ns-1", }, } - virtualServers := []*conf_v1.VirtualServer{&vs1, &vs2, &vs3} - - expected := []*conf_v1.VirtualServer{&vs1, &vs3} + expected := []*conf_v1alpha1.TransportServer{&ts1} - result := findVirtualServersForVirtualServerRoute(virtualServers, &vsr) + result := findTransportServersForService(transportServers, &service) if !reflect.DeepEqual(result, expected) { - t.Errorf("findVirtualServersForVirtualServerRoute returned %v but expected %v", result, expected) + t.Errorf("findTransportServersForService returned %v but expected %v", result, expected) } } @@ -2353,94 +1001,77 @@ func policyMapToString(policies map[string]*conf_v1alpha1.Policy) string { return b.String() } -func TestRemoveDuplicateVirtualServers(t *testing.T) { +type testResource struct { + keyWithKind string +} + +func (*testResource) GetObjectMeta() *meta_v1.ObjectMeta { + return nil +} + +func (t *testResource) GetKeyWithKind() string { + return t.keyWithKind +} + +func (*testResource) AcquireHost(string) { +} + +func (*testResource) ReleaseHost(string) { +} + +func (*testResource) Wins(Resource) bool { + return false +} + +func (*testResource) IsSame(Resource) bool { + return false +} + +func (*testResource) AddWarning(string) { +} + +func (*testResource) IsEqual(Resource) bool { + return false +} + +func (t *testResource) String() string { + return t.keyWithKind +} + +func TestRemoveDuplicateResources(t *testing.T) { tests := []struct { - virtualServers []*conf_v1.VirtualServer - expected []*conf_v1.VirtualServer + resources []Resource + expected []Resource }{ { - []*conf_v1.VirtualServer{ - { - ObjectMeta: meta_v1.ObjectMeta{ - Name: "vs-1", - Namespace: "ns-1", - }, - }, - { - ObjectMeta: meta_v1.ObjectMeta{ - Name: "vs-2", - Namespace: "ns-1", - }, - }, - { - ObjectMeta: meta_v1.ObjectMeta{ - Name: "vs-2", - Namespace: "ns-1", - }, - }, - { - ObjectMeta: meta_v1.ObjectMeta{ - Name: "vs-3", - Namespace: "ns-1", - }, - }, + resources: []Resource{ + &testResource{keyWithKind: "VirtualServer/ns-1/vs-1"}, + &testResource{keyWithKind: "VirtualServer/ns-1/vs-2"}, + &testResource{keyWithKind: "VirtualServer/ns-1/vs-2"}, + &testResource{keyWithKind: "VirtualServer/ns-1/vs-3"}, }, - []*conf_v1.VirtualServer{ - { - ObjectMeta: meta_v1.ObjectMeta{ - Name: "vs-1", - Namespace: "ns-1", - }, - }, - { - ObjectMeta: meta_v1.ObjectMeta{ - Name: "vs-2", - Namespace: "ns-1", - }, - }, - { - ObjectMeta: meta_v1.ObjectMeta{ - Name: "vs-3", - Namespace: "ns-1", - }, - }, + expected: []Resource{ + &testResource{keyWithKind: "VirtualServer/ns-1/vs-1"}, + &testResource{keyWithKind: "VirtualServer/ns-1/vs-2"}, + &testResource{keyWithKind: "VirtualServer/ns-1/vs-3"}, }, }, { - []*conf_v1.VirtualServer{ - { - ObjectMeta: meta_v1.ObjectMeta{ - Name: "vs-3", - Namespace: "ns-2", - }, - }, - { - ObjectMeta: meta_v1.ObjectMeta{ - Name: "vs-3", - Namespace: "ns-1", - }, - }, + resources: []Resource{ + &testResource{keyWithKind: "VirtualServer/ns-2/vs-3"}, + &testResource{keyWithKind: "VirtualServer/ns-1/vs-3"}, }, - []*conf_v1.VirtualServer{ - { - ObjectMeta: meta_v1.ObjectMeta{ - Name: "vs-3", - Namespace: "ns-2", - }, - }, - { - ObjectMeta: meta_v1.ObjectMeta{ - Name: "vs-3", - Namespace: "ns-1", - }, - }, + expected: []Resource{ + &testResource{keyWithKind: "VirtualServer/ns-2/vs-3"}, + &testResource{keyWithKind: "VirtualServer/ns-1/vs-3"}, }, }, } + for _, test := range tests { - result := removeDuplicateVirtualServers(test.virtualServers) + result := removeDuplicateResources(test.resources) if !reflect.DeepEqual(result, test.expected) { - t.Errorf("removeDuplicateVirtualServers() returned \n%v but expected \n%v", result, test.expected) + t.Errorf("removeDuplicateResources() returned \n%v but expected \n%v", result, test.expected) } } } diff --git a/internal/k8s/handlers.go b/internal/k8s/handlers.go index df068c4fdc..965e5d3a89 100644 --- a/internal/k8s/handlers.go +++ b/internal/k8s/handlers.go @@ -97,10 +97,6 @@ func createIngressHandlers(lbc *LoadBalancerController) cache.ResourceEventHandl return cache.ResourceEventHandlerFuncs{ AddFunc: func(obj interface{}) { ingress := obj.(*networking.Ingress) - if !lbc.HasCorrectIngressClass(ingress) { - glog.Infof("Ignoring Ingress %v based on Annotation %v", ingress.Name, ingressClassKey) - return - } glog.V(3).Infof("Adding Ingress: %v", ingress.Name) lbc.AddSyncQueue(obj) }, @@ -118,28 +114,12 @@ func createIngressHandlers(lbc *LoadBalancerController) cache.ResourceEventHandl return } } - if !lbc.HasCorrectIngressClass(ingress) { - return - } - if isMinion(ingress) { - master, err := lbc.FindMasterForMinion(ingress) - if err != nil { - glog.Infof("Ignoring Ingress %v(Minion): %v", ingress.Name, err) - return - } - glog.V(3).Infof("Removing Ingress: %v(Minion) for %v(Master)", ingress.Name, master.Name) - lbc.AddSyncQueue(master) - } else { - glog.V(3).Infof("Removing Ingress: %v", ingress.Name) - lbc.AddSyncQueue(obj) - } + glog.V(3).Infof("Removing Ingress: %v", ingress.Name) + lbc.AddSyncQueue(obj) }, UpdateFunc: func(old, current interface{}) { c := current.(*networking.Ingress) o := old.(*networking.Ingress) - if !lbc.HasCorrectIngressClass(c) { - return - } if hasChanges(o, c) { glog.V(3).Infof("Ingress %v changed, syncing", c.Name) lbc.AddSyncQueue(c) @@ -196,23 +176,25 @@ func createSecretHandlers(lbc *LoadBalancerController) cache.ResourceEventHandle } // createServiceHandlers builds the handler funcs for services. -// In the handlers below, we need to enqueue Ingress or other affected resources -// so that we can catch a change like a change of the port field of a service port (for such a change Kubernetes doesn't +// +// In the update handlers below we catch two cases: +// (1) the service is the external service +// (2) the service had a change like a change of the port field of a service port (for such a change Kubernetes doesn't // update the corresponding endpoints resource, that we monitor as well) // or a change of the externalName field of an ExternalName service. +// +// In both cases we enqueue the service to be processed by syncService +// Also, because TransportServers aren't processed by syncService, +// we enqueue them, so they get processed by syncTransportServer. func createServiceHandlers(lbc *LoadBalancerController) cache.ResourceEventHandlerFuncs { return cache.ResourceEventHandlerFuncs{ AddFunc: func(obj interface{}) { svc := obj.(*v1.Service) - if lbc.IsExternalServiceForStatus(svc) { - lbc.AddSyncQueue(svc) - return - } + glog.V(3).Infof("Adding service: %v", svc.Name) - lbc.EnqueueIngressForService(svc) + lbc.AddSyncQueue(svc) if lbc.areCustomResourcesEnabled { - lbc.EnqueueVirtualServersForService(svc) lbc.EnqueueTransportServerForService(svc) } }, @@ -230,16 +212,11 @@ func createServiceHandlers(lbc *LoadBalancerController) cache.ResourceEventHandl return } } - if lbc.IsExternalServiceForStatus(svc) { - lbc.AddSyncQueue(svc) - return - } glog.V(3).Infof("Removing service: %v", svc.Name) - lbc.EnqueueIngressForService(svc) + lbc.AddSyncQueue(svc) if lbc.areCustomResourcesEnabled { - lbc.EnqueueVirtualServersForService(svc) lbc.EnqueueTransportServerForService(svc) } }, @@ -253,10 +230,9 @@ func createServiceHandlers(lbc *LoadBalancerController) cache.ResourceEventHandl oldSvc := old.(*v1.Service) if hasServiceChanges(oldSvc, curSvc) { glog.V(3).Infof("Service %v changed, syncing", curSvc.Name) - lbc.EnqueueIngressForService(curSvc) + lbc.AddSyncQueue(curSvc) if lbc.areCustomResourcesEnabled { - lbc.EnqueueVirtualServersForService(curSvc) lbc.EnqueueTransportServerForService(curSvc) } } @@ -320,10 +296,6 @@ func createVirtualServerHandlers(lbc *LoadBalancerController) cache.ResourceEven return cache.ResourceEventHandlerFuncs{ AddFunc: func(obj interface{}) { vs := obj.(*conf_v1.VirtualServer) - if !lbc.HasCorrectIngressClass(vs) { - glog.Infof("Ignoring VirtualServer %v based on class %v", vs.Name, vs.Spec.IngressClass) - return - } glog.V(3).Infof("Adding VirtualServer: %v", vs.Name) lbc.AddSyncQueue(vs) }, @@ -341,20 +313,12 @@ func createVirtualServerHandlers(lbc *LoadBalancerController) cache.ResourceEven return } } - if !lbc.HasCorrectIngressClass(vs) { - glog.Infof("Ignoring VirtualServer %v based on class %v", vs.Name, vs.Spec.IngressClass) - return - } glog.V(3).Infof("Removing VirtualServer: %v", vs.Name) lbc.AddSyncQueue(vs) }, UpdateFunc: func(old, cur interface{}) { curVs := cur.(*conf_v1.VirtualServer) oldVs := old.(*conf_v1.VirtualServer) - if !lbc.HasCorrectIngressClass(curVs) { - glog.Infof("Ignoring VirtualServer %v based on class %v", curVs.Name, curVs.Spec.IngressClass) - return - } if !reflect.DeepEqual(oldVs.Spec, curVs.Spec) { glog.V(3).Infof("VirtualServer %v changed, syncing", curVs.Name) lbc.AddSyncQueue(curVs) @@ -367,10 +331,6 @@ func createVirtualServerRouteHandlers(lbc *LoadBalancerController) cache.Resourc return cache.ResourceEventHandlerFuncs{ AddFunc: func(obj interface{}) { vsr := obj.(*conf_v1.VirtualServerRoute) - if !lbc.HasCorrectIngressClass(vsr) { - glog.Infof("Ignoring VirtualServerRoute %v based on class %v", vsr.Name, vsr.Spec.IngressClass) - return - } glog.V(3).Infof("Adding VirtualServerRoute: %v", vsr.Name) lbc.AddSyncQueue(vsr) }, @@ -388,20 +348,12 @@ func createVirtualServerRouteHandlers(lbc *LoadBalancerController) cache.Resourc return } } - if !lbc.HasCorrectIngressClass(vsr) { - glog.Infof("Ignoring VirtualServerRoute %v based on class %v", vsr.Name, vsr.Spec.IngressClass) - return - } glog.V(3).Infof("Removing VirtualServerRoute: %v", vsr.Name) lbc.AddSyncQueue(vsr) }, UpdateFunc: func(old, cur interface{}) { curVsr := cur.(*conf_v1.VirtualServerRoute) oldVsr := old.(*conf_v1.VirtualServerRoute) - if !lbc.HasCorrectIngressClass(curVsr) { - glog.Infof("Ignoring VirtualServerRoute %v based on class %v", curVsr.Name, curVsr.Spec.IngressClass) - return - } if !reflect.DeepEqual(oldVsr.Spec, curVsr.Spec) { glog.V(3).Infof("VirtualServerRoute %v changed, syncing", curVsr.Name) lbc.AddSyncQueue(curVsr) diff --git a/internal/k8s/leader.go b/internal/k8s/leader.go index ad20d1c052..45e6832e02 100644 --- a/internal/k8s/leader.go +++ b/internal/k8s/leader.go @@ -51,10 +51,11 @@ func createLeaderHandler(lbc *LoadBalancerController) leaderelection.LeaderCallb OnStartedLeading: func(ctx context.Context) { glog.V(3).Info("started leading") if lbc.reportIngressStatus { - glog.V(3).Info("updating ingress status") + ingresses := lbc.configuration.GetResourcesWithFilter(resourceFilter{Ingresses: true}) - ingresses, mergeableIngresses := lbc.GetManagedIngresses() - err := lbc.UpdateManagedAndMergeableIngresses(ingresses, mergeableIngresses) + glog.V(3).Infof("Updating status for %v Ingresses", len(ingresses)) + + err := lbc.statusUpdater.UpdateExternalEndpointsForResources(ingresses) if err != nil { glog.V(3).Infof("error updating status when starting leading: %v", err) } @@ -62,6 +63,7 @@ func createLeaderHandler(lbc *LoadBalancerController) leaderelection.LeaderCallb if lbc.areCustomResourcesEnabled { glog.V(3).Info("updating VirtualServer and VirtualServerRoutes status") + err := lbc.updateVirtualServersStatusFromEvents() if err != nil { glog.V(3).Infof("error updating VirtualServers status when starting leading: %v", err) diff --git a/internal/k8s/reference_checkers.go b/internal/k8s/reference_checkers.go index 0f17eaae0e..14061f4f5a 100644 --- a/internal/k8s/reference_checkers.go +++ b/internal/k8s/reference_checkers.go @@ -207,3 +207,18 @@ func (rc *appProtectResourceReferenceChecker) IsReferencedByVirtualServer(namesp func (rc *appProtectResourceReferenceChecker) IsReferencedByVirtualServerRoute(namespace string, name string, vsr *v1.VirtualServerRoute) bool { return false } + +func isPolicyReferenced(policies []v1.PolicyReference, resourceNamespace string, policyNamespace string, policyName string) bool { + for _, p := range policies { + namespace := p.Namespace + if namespace == "" { + namespace = resourceNamespace + } + + if p.Name == policyName && namespace == policyNamespace { + return true + } + } + + return false +} diff --git a/internal/k8s/reference_checkers_test.go b/internal/k8s/reference_checkers_test.go index 1b9525651a..846b14b72b 100644 --- a/internal/k8s/reference_checkers_test.go +++ b/internal/k8s/reference_checkers_test.go @@ -884,3 +884,73 @@ func TestAppProtectResourceIsReferenced(t *testing.T) { t.Error("IsReferencedByVirtualServer() returned true but expected false") } } + +func TestIsPolicyIsReferenced(t *testing.T) { + tests := []struct { + policies []conf_v1.PolicyReference + resourceNamespace string + policyNamespace string + policyName string + expected bool + msg string + }{ + { + policies: []conf_v1.PolicyReference{ + { + Name: "test-policy", + }, + }, + resourceNamespace: "ns-1", + policyNamespace: "ns-1", + policyName: "test-policy", + expected: true, + msg: "reference with implicit namespace", + }, + { + policies: []conf_v1.PolicyReference{ + { + Name: "test-policy", + Namespace: "ns-1", + }, + }, + resourceNamespace: "ns-1", + policyNamespace: "ns-1", + policyName: "test-policy", + expected: true, + msg: "reference with explicit namespace", + }, + { + policies: []conf_v1.PolicyReference{ + { + Name: "test-policy", + }, + }, + resourceNamespace: "ns-2", + policyNamespace: "ns-1", + policyName: "test-policy", + expected: false, + msg: "wrong namespace with implicit namespace", + }, + { + policies: []conf_v1.PolicyReference{ + { + Name: "test-policy", + Namespace: "ns-2", + }, + }, + resourceNamespace: "ns-2", + policyNamespace: "ns-1", + policyName: "test-policy", + expected: false, + msg: "wrong namespace with explicit namespace", + }, + } + + for _, test := range tests { + result := isPolicyReferenced(test.policies, test.resourceNamespace, test.policyNamespace, test.policyName) + if result != test.expected { + t.Errorf("isPolicyReferenced() returned %v but expected %v for the case of %s", result, + test.expected, test.msg) + } + } +} diff --git a/internal/k8s/status.go b/internal/k8s/status.go index 03076b3360..cdb83d43e7 100644 --- a/internal/k8s/status.go +++ b/internal/k8s/status.go @@ -9,7 +9,6 @@ import ( "strings" "github.com/golang/glog" - "github.com/nginxinc/kubernetes-ingress/internal/configs" conf_v1 "github.com/nginxinc/kubernetes-ingress/pkg/apis/configuration/v1" v1 "github.com/nginxinc/kubernetes-ingress/pkg/apis/configuration/v1" k8s_nginx "github.com/nginxinc/kubernetes-ingress/pkg/client/clientset/versioned" @@ -20,6 +19,7 @@ import ( "k8s.io/apimachinery/pkg/util/intstr" "k8s.io/client-go/kubernetes" + "k8s.io/client-go/tools/cache" ) // statusUpdater reports Ingress, VirtualServer and VirtualServerRoute status information via the kubernetes @@ -35,34 +35,61 @@ type statusUpdater struct { externalEndpoints []v1.ExternalEndpoint status []api_v1.LoadBalancerIngress keyFunc func(obj interface{}) (string, error) - ingLister *storeToIngressLister + ingressLister *storeToIngressLister + virtualServerLister cache.Store + virtualServerRouteLister cache.Store confClient k8s_nginx.Interface } -// UpdateManagedAndMergeableIngresses handles the full return format of LoadBalancerController.getManagedIngresses -func (su *statusUpdater) UpdateManagedAndMergeableIngresses(managedIngresses []networking.Ingress, mergableIngExes map[string]*configs.MergeableIngresses) error { - ings := []networking.Ingress{} - ings = append(ings, managedIngresses...) - for _, mergableIngEx := range mergableIngExes { - for _, minion := range mergableIngEx.Minions { - ings = append(ings, *minion.Ingress) +func (su *statusUpdater) UpdateExternalEndpointsForResources(resource []Resource) error { + failed := false + + for _, r := range resource { + err := su.UpdateExternalEndpointsForResource(r) + if err != nil { + failed = true } } - return su.BulkUpdateIngressStatus(ings) + + if failed { + return fmt.Errorf("not all Resources updated") + } + + return nil } -// UpdateMergableIngresses is a convience passthru to update Ingresses with our configs.MergableIngresses type -func (su *statusUpdater) UpdateMergableIngresses(mergableIngresses *configs.MergeableIngresses) error { - ings := []networking.Ingress{} - ingExes := []*configs.IngressEx{} +func (su *statusUpdater) UpdateExternalEndpointsForResource(r Resource) error { + switch impl := r.(type) { + case *FullIngress: + var ings []networking.Ingress + ings = append(ings, *impl.Ingress) + + for _, fm := range impl.Minions { + ings = append(ings, *fm.Ingress) + } - ingExes = append(ingExes, mergableIngresses.Master) - ingExes = append(ingExes, mergableIngresses.Minions...) + return su.BulkUpdateIngressStatus(ings) + case *FullVirtualServer: + failed := false - for _, ingEx := range ingExes { - ings = append(ings, *ingEx.Ingress) + err := su.updateVirtualServerExternalEndpoints(impl.VirtualServer) + if err != nil { + failed = true + } + + for _, vsr := range impl.VirtualServerRoutes { + err := su.updateVirtualServerRouteExternalEndpoints(vsr) + if err != nil { + failed = true + } + } + + if failed { + return fmt.Errorf("not all Resources updated") + } } - return su.BulkUpdateIngressStatus(ings) + + return nil } // ClearIngressStatus clears the Ingress status. @@ -77,18 +104,13 @@ func (su *statusUpdater) UpdateIngressStatus(ing networking.Ingress) error { // updateIngressWithStatus sets the provided status on the selected Ingress. func (su *statusUpdater) updateIngressWithStatus(ing networking.Ingress, status []api_v1.LoadBalancerIngress) error { - if reflect.DeepEqual(ing.Status.LoadBalancer.Ingress, status) { - return nil - } - - // Get a pristine Ingress from the Store. Required because annotations can be modified - // for mergable Ingress objects and the update status API call will update annotations, not just status. + // Get an up-to-date Ingress from the Store key, err := su.keyFunc(&ing) if err != nil { glog.V(3).Infof("error getting key for ing: %v", err) return err } - ingCopy, exists, err := su.ingLister.GetByKeySafe(key) + ingCopy, exists, err := su.ingressLister.GetByKeySafe(key) if err != nil { glog.V(3).Infof("error getting ing from Store by key: %v", err) return err @@ -98,6 +120,11 @@ func (su *statusUpdater) updateIngressWithStatus(ing networking.Ingress, status return nil } + // No need to update status + if reflect.DeepEqual(ingCopy.Status.LoadBalancer.Ingress, status) { + return nil + } + ingCopy.Status.LoadBalancer.Ingress = status clientIngress := su.client.NetworkingV1beta1().Ingresses(ingCopy.Namespace) _, err = clientIngress.UpdateStatus(context.TODO(), ingCopy, metav1.UpdateOptions{}) @@ -314,17 +341,29 @@ func hasVsStatusChanged(vs *conf_v1.VirtualServer, state string, reason string, // UpdateVirtualServerStatus updates the status of a VirtualServer. func (su *statusUpdater) UpdateVirtualServerStatus(vs *conf_v1.VirtualServer, state string, reason string, message string) error { - if !hasVsStatusChanged(vs, state, reason, message) { + // Get an up-to-date VirtualServer from the Store + vsLatest, exists, err := su.virtualServerLister.Get(vs) + if err != nil { + glog.V(3).Infof("error getting VirtualServer from Store: %v", err) + return err + } + if !exists { + glog.V(3).Infof("VirtualServer doesn't exist in Store") + return nil + } + + vsCopy := vsLatest.(*conf_v1.VirtualServer).DeepCopy() + + if !hasVsStatusChanged(vsCopy, state, reason, message) { return nil } - vsCopy := vs.DeepCopy() vsCopy.Status.State = state vsCopy.Status.Reason = reason vsCopy.Status.Message = message vsCopy.Status.ExternalEndpoints = su.externalEndpoints - _, err := su.confClient.K8sV1().VirtualServers(vsCopy.Namespace).UpdateStatus(context.TODO(), vsCopy, metav1.UpdateOptions{}) + _, err = su.confClient.K8sV1().VirtualServers(vsCopy.Namespace).UpdateStatus(context.TODO(), vsCopy, metav1.UpdateOptions{}) if err != nil { glog.V(3).Infof("error setting VirtualServer %v/%v status, retrying: %v", vsCopy.Namespace, vsCopy.Name, err) return su.retryUpdateVirtualServerStatus(vsCopy) @@ -360,14 +399,26 @@ func (su *statusUpdater) UpdateVirtualServerRouteStatusWithReferencedBy(vsr *con referencedByString = fmt.Sprintf("%v/%v", vs.Namespace, vs.Name) } - vsrCopy := vsr.DeepCopy() + // Get an up-to-date VirtualServerRoute from the Store + vsrLatest, exists, err := su.virtualServerRouteLister.Get(vsr) + if err != nil { + glog.V(3).Infof("error getting VirtualServerRoute from Store: %v", err) + return err + } + if !exists { + glog.V(3).Infof("VirtualServerRoute doesn't exist in Store") + return nil + } + + vsrCopy := vsrLatest.(*conf_v1.VirtualServerRoute).DeepCopy() + vsrCopy.Status.State = state vsrCopy.Status.Reason = reason vsrCopy.Status.Message = message vsrCopy.Status.ReferencedBy = referencedByString vsrCopy.Status.ExternalEndpoints = su.externalEndpoints - _, err := su.confClient.K8sV1().VirtualServerRoutes(vsrCopy.Namespace).UpdateStatus(context.TODO(), vsrCopy, metav1.UpdateOptions{}) + _, err = su.confClient.K8sV1().VirtualServerRoutes(vsrCopy.Namespace).UpdateStatus(context.TODO(), vsrCopy, metav1.UpdateOptions{}) if err != nil { glog.V(3).Infof("error setting VirtualServerRoute %v/%v status, retrying: %v", vsrCopy.Namespace, vsrCopy.Name, err) return su.retryUpdateVirtualServerRouteStatus(vsrCopy) @@ -379,18 +430,29 @@ func (su *statusUpdater) UpdateVirtualServerRouteStatusWithReferencedBy(vsr *con // This method does not clear or update the referencedBy field of the status. // If you need to update the referencedBy field, use UpdateVirtualServerRouteStatusWithReferencedBy instead. func (su *statusUpdater) UpdateVirtualServerRouteStatus(vsr *conf_v1.VirtualServerRoute, state string, reason string, message string) error { + // Get an up-to-date VirtualServerRoute from the Store + vsrLatest, exists, err := su.virtualServerRouteLister.Get(vsr) + if err != nil { + glog.V(3).Infof("error getting VirtualServerRoute from Store: %v", err) + return err + } + if !exists { + glog.V(3).Infof("VirtualServerRoute doesn't exist in Store") + return nil + } - if !hasVsrStatusChanged(vsr, state, reason, message, "") { + vsrCopy := vsrLatest.(*conf_v1.VirtualServerRoute).DeepCopy() + + if !hasVsrStatusChanged(vsrCopy, state, reason, message, "") { return nil } - vsrCopy := vsr.DeepCopy() vsrCopy.Status.State = state vsrCopy.Status.Reason = reason vsrCopy.Status.Message = message vsrCopy.Status.ExternalEndpoints = su.externalEndpoints - _, err := su.confClient.K8sV1().VirtualServerRoutes(vsrCopy.Namespace).UpdateStatus(context.TODO(), vsrCopy, metav1.UpdateOptions{}) + _, err = su.confClient.K8sV1().VirtualServerRoutes(vsrCopy.Namespace).UpdateStatus(context.TODO(), vsrCopy, metav1.UpdateOptions{}) if err != nil { glog.V(3).Infof("error setting VirtualServerRoute %v/%v status, retrying: %v", vsrCopy.Namespace, vsrCopy.Name, err) return su.retryUpdateVirtualServerRouteStatus(vsrCopy) @@ -399,10 +461,21 @@ func (su *statusUpdater) UpdateVirtualServerRouteStatus(vsr *conf_v1.VirtualServ } func (su *statusUpdater) updateVirtualServerExternalEndpoints(vs *conf_v1.VirtualServer) error { - vsCopy := vs.DeepCopy() + // Get a pristine VirtualServer from the Store + vsLatest, exists, err := su.virtualServerLister.Get(vs) + if err != nil { + glog.V(3).Infof("error getting VirtualServer from Store: %v", err) + return err + } + if !exists { + glog.V(3).Infof("VirtualServer doesn't exist in Store") + return nil + } + + vsCopy := vsLatest.(*conf_v1.VirtualServer).DeepCopy() vsCopy.Status.ExternalEndpoints = su.externalEndpoints - _, err := su.confClient.K8sV1().VirtualServers(vsCopy.Namespace).UpdateStatus(context.TODO(), vsCopy, metav1.UpdateOptions{}) + _, err = su.confClient.K8sV1().VirtualServers(vsCopy.Namespace).UpdateStatus(context.TODO(), vsCopy, metav1.UpdateOptions{}) if err != nil { glog.V(3).Infof("error setting VirtualServer %v/%v status, retrying: %v", vsCopy.Namespace, vsCopy.Name, err) return su.retryUpdateVirtualServerStatus(vsCopy) @@ -411,47 +484,26 @@ func (su *statusUpdater) updateVirtualServerExternalEndpoints(vs *conf_v1.Virtua } func (su *statusUpdater) updateVirtualServerRouteExternalEndpoints(vsr *conf_v1.VirtualServerRoute) error { - vsrCopy := vsr.DeepCopy() - vsrCopy.Status.ExternalEndpoints = su.externalEndpoints - - _, err := su.confClient.K8sV1().VirtualServerRoutes(vsrCopy.Namespace).UpdateStatus(context.TODO(), vsrCopy, metav1.UpdateOptions{}) + // Get an up-to-date VirtualServerRoute from the Store + vsrLatest, exists, err := su.virtualServerRouteLister.Get(vsr) if err != nil { - glog.V(3).Infof("error setting VirtualServerRoute %v/%v status, retrying: %v", vsrCopy.Namespace, vsrCopy.Name, err) - return su.retryUpdateVirtualServerRouteStatus(vsrCopy) + glog.V(3).Infof("error getting VirtualServerRoute from Store: %v", err) + return err } - return err -} - -// UpdateVsVsrExternalEndpoints updates all the external endpoints for the given VirtualServer and VirtualServerRoutes statuses. -func (su *statusUpdater) UpdateVsVsrExternalEndpoints(vss []*conf_v1.VirtualServer, vsrs []*conf_v1.VirtualServerRoute) error { - if len(vss) < 1 && len(vsrs) < 1 { - glog.V(3).Info("no VirtualServers or VirtualServerRoutes to update") + if !exists { + glog.V(3).Infof("VirtualServerRoute doesn't exist in Store") return nil } - var errorMsg string - for _, vs := range vss { - err := su.updateVirtualServerExternalEndpoints(vs) - if err != nil { - errorMsg = "not all VirtualServers updated" - } - } - - for _, vsr := range vsrs { - err := su.updateVirtualServerRouteExternalEndpoints(vsr) - if err != nil { - if errorMsg != "" { - errorMsg += ", " - } - errorMsg += "not all VirtualServerRoutes updated" - } - } + vsrCopy := vsrLatest.(*conf_v1.VirtualServerRoute).DeepCopy() + vsrCopy.Status.ExternalEndpoints = su.externalEndpoints - if errorMsg != "" { - return fmt.Errorf(errorMsg) + _, err = su.confClient.K8sV1().VirtualServerRoutes(vsrCopy.Namespace).UpdateStatus(context.TODO(), vsrCopy, metav1.UpdateOptions{}) + if err != nil { + glog.V(3).Infof("error setting VirtualServerRoute %v/%v status, retrying: %v", vsrCopy.Namespace, vsrCopy.Name, err) + return su.retryUpdateVirtualServerRouteStatus(vsrCopy) } - - return nil + return err } func (su *statusUpdater) generateExternalEndpointsFromStatus(status []api_v1.LoadBalancerIngress) []conf_v1.ExternalEndpoint { diff --git a/internal/k8s/status_test.go b/internal/k8s/status_test.go index 273800a3cd..b96b35c1b9 100644 --- a/internal/k8s/status_test.go +++ b/internal/k8s/status_test.go @@ -51,7 +51,7 @@ func TestStatusUpdate(t *testing.T) { namespace: "namespace", externalServiceName: "service-name", externalStatusAddress: "123.123.123.123", - ingLister: &ingLister, + ingressLister: &ingLister, keyFunc: cache.DeletionHandlingMetaNamespaceKeyFunc, } err = su.ClearIngressStatus(ing) diff --git a/internal/k8s/task_queue.go b/internal/k8s/task_queue.go index 5729cbbac5..eafa637877 100644 --- a/internal/k8s/task_queue.go +++ b/internal/k8s/task_queue.go @@ -106,8 +106,6 @@ type kind int const ( // ingress resource ingress = iota - // ingressMinion resource, which is a Minion Ingress resource - ingressMinion // endpoints resource endpoints // configMap resource @@ -143,12 +141,7 @@ func newTask(key string, obj interface{}) (task, error) { var k kind switch t := obj.(type) { case *networking.Ingress: - ing := obj.(*networking.Ingress) - if isMinion(ing) { - k = ingressMinion - } else { - k = ingress - } + k = ingress case *v1.Endpoints: k = endpoints case *v1.ConfigMap: From dd7abbbe60d9ff861b757b00e26b15df507bbeed Mon Sep 17 00:00:00 2001 From: Michael Pleshakov Date: Thu, 24 Sep 2020 12:06:33 -0700 Subject: [PATCH 3/3] Update python tests --- tests/suite/custom_assertions.py | 20 ++++++++ tests/suite/test_ac_policies.py | 2 +- tests/suite/test_annotations.py | 8 +-- tests/suite/test_app_protect.py | 4 +- tests/suite/test_v_s_route.py | 49 +++++++++++-------- .../suite/test_v_s_route_canned_responses.py | 2 +- tests/suite/test_v_s_route_error_pages.py | 4 +- tests/suite/test_v_s_route_externalname.py | 10 ++-- tests/suite/test_v_s_route_redirects.py | 2 +- tests/suite/test_v_s_route_regexp_location.py | 7 ++- tests/suite/test_v_s_route_status.py | 22 ++++----- .../suite/test_v_s_route_upstream_options.py | 8 +-- .../test_virtual_server_canned_responses.py | 2 +- .../test_virtual_server_configmap_keys.py | 14 +++--- .../suite/test_virtual_server_error_pages.py | 2 +- .../test_virtual_server_external_name.py | 9 ++-- tests/suite/test_virtual_server_redirects.py | 2 +- .../test_virtual_server_upstream_options.py | 8 +-- tests/suite/test_virtual_server_validation.py | 2 +- 19 files changed, 100 insertions(+), 77 deletions(-) diff --git a/tests/suite/custom_assertions.py b/tests/suite/custom_assertions.py index 7b11d79bea..5f93a277be 100644 --- a/tests/suite/custom_assertions.py +++ b/tests/suite/custom_assertions.py @@ -47,6 +47,26 @@ def assert_event_and_count(event_text, count, events_list) -> None: pytest.fail(f"Failed to find the event \"{event_text}\" in the list. Exiting...") +def assert_event_with_full_equality_and_count(event_text, count, events_list) -> None: + """ + Search for the event in the list and compare its counter with an expected value. + + :param event_text: event text + :param count: expected value + :param events_list: list of events + :return: + """ + + for i in range(len(events_list) - 1, -1, -1): + # some events have trailing whitespace + message_stripped = events_list[i].message.rstrip() + + if event_text == message_stripped: + assert events_list[i].count == count + return + pytest.fail(f"Failed to find the event \"{event_text}\" in the list. Exiting...") + + def assert_event_and_get_count(event_text, events_list) -> int: """ Search for the event in the list and return its counter. diff --git a/tests/suite/test_ac_policies.py b/tests/suite/test_ac_policies.py index bd5385e5e6..36d97304a5 100644 --- a/tests/suite/test_ac_policies.py +++ b/tests/suite/test_ac_policies.py @@ -354,7 +354,7 @@ def test_deleted_policy( assert resp.status_code == 500 and "500 Internal Server Error" in resp.text assert ( vs_info["status"]["state"] == "Warning" - and vs_info["status"]["reason"] == "UpdatedWithWarning" + and vs_info["status"]["reason"] == "AddedOrUpdatedWithWarning" ) def test_route_override_spec( diff --git a/tests/suite/test_annotations.py b/tests/suite/test_annotations.py index 7fd2f1c328..34f46824e4 100644 --- a/tests/suite/test_annotations.py +++ b/tests/suite/test_annotations.py @@ -101,13 +101,13 @@ def annotations_setup(request, ic_pod_name = get_first_pod_name(kube_apis.v1, ingress_controller_prerequisites.namespace) upstream_names = [] if request.param == 'mergeable': - event_text = f"Configuration for {test_namespace}/{ingress_name}(Master) was added or updated" - error_text = f"{event_text} but was not applied: Error reloading NGINX" + event_text = f"Configuration for {test_namespace}/{ingress_name} was added or updated" + error_text = f"{event_text} ; but was not applied: Error reloading NGINX" for minion in minions_info: upstream_names.append(f"{test_namespace}-{minion['name']}-{ingress_host}-{minion['svc_name']}-80") else: event_text = f"Configuration for {test_namespace}/{ingress_name} was added or updated" - error_text = f"{event_text}, but not applied: Error reloading NGINX" + error_text = f"{event_text} ; but was not applied: Error reloading NGINX" upstream_names.append(f"{test_namespace}-{ingress_name}-{ingress_host}-backend1-svc-80") upstream_names.append(f"{test_namespace}-{ingress_name}-{ingress_host}-backend2-svc-80") @@ -147,7 +147,7 @@ def annotations_grpc_setup(request, f"{TEST_DATA}/common/configmap-with-grpc.yaml") ic_pod_name = get_first_pod_name(kube_apis.v1, ingress_controller_prerequisites.namespace) event_text = f"Configuration for {test_namespace}/{ingress_name} was added or updated" - error_text = f"{event_text}, but not applied: Error reloading NGINX" + error_text = f"{event_text} ; but was not applied: Error reloading NGINX" def fin(): print("Clean up gRPC Annotations Example:") diff --git a/tests/suite/test_app_protect.py b/tests/suite/test_app_protect.py index 90a909ea7d..2ff3ef48c5 100644 --- a/tests/suite/test_app_protect.py +++ b/tests/suite/test_app_protect.py @@ -88,13 +88,13 @@ def backend_setup(request, kube_apis, ingress_controller_endpoint, test_namespac def fin(): print("Clean up:") + src_ing_yaml = f"{TEST_DATA}/appprotect/appprotect-ingress.yaml" + delete_items_from_yaml(kube_apis, src_ing_yaml, test_namespace) delete_ap_policy(kube_apis.custom_objects, pol_name, test_namespace) delete_ap_logconf(kube_apis.custom_objects, log_name, test_namespace) delete_common_app(kube_apis, "simple", test_namespace) src_sec_yaml = f"{TEST_DATA}/appprotect/appprotect-secret.yaml" delete_items_from_yaml(kube_apis, src_sec_yaml, test_namespace) - src_ing_yaml = f"{TEST_DATA}/appprotect/appprotect-ingress.yaml" - delete_items_from_yaml(kube_apis, src_ing_yaml, test_namespace) request.addfinalizer(fin) diff --git a/tests/suite/test_v_s_route.py b/tests/suite/test_v_s_route.py index 758c84738b..28bba3f296 100644 --- a/tests/suite/test_v_s_route.py +++ b/tests/suite/test_v_s_route.py @@ -3,7 +3,8 @@ from kubernetes.client.rest import ApiException from settings import TEST_DATA -from suite.custom_assertions import assert_event_and_count, assert_event_and_get_count +from suite.custom_assertions import assert_event_and_count, assert_event_and_get_count, \ + assert_event_with_full_equality_and_count from suite.custom_resources_utils import create_virtual_server_from_yaml, \ delete_virtual_server, create_v_s_route_from_yaml, delete_v_s_route, get_vs_nginx_template_conf, \ patch_v_s_route_from_yaml @@ -51,6 +52,7 @@ def test_responses_and_events_in_flow(self, kube_apis, vsr_2_name = f"{v_s_route_setup.route_s.namespace}/{v_s_route_setup.route_s.name}" vsr_1_event_text = f"Configuration for {vsr_1_name} was added or updated" vs_event_text = f"Configuration for {vs_name} was added or updated" + vs_warning_event_text = f"Configuration for {vs_name} was added or updated with warning(s): VirtualServerRoute {vsr_1_name} doesn't exist or invalid" vsr_2_event_text = f"Configuration for {vsr_2_name} was added or updated" initial_config = get_vs_nginx_template_conf(kube_apis.v1, v_s_route_setup.namespace, @@ -91,7 +93,7 @@ def test_responses_and_events_in_flow(self, kube_apis, events_ns_1 = get_events(kube_apis.v1, v_s_route_setup.route_m.namespace) events_ns_2 = get_events(kube_apis.v1, v_s_route_setup.route_s.namespace) assert_event_and_count(vsr_1_event_text, initial_count_vsr_1 + 1, events_ns_1) - assert_event_and_count(vs_event_text, initial_count_vs + 1, events_ns_1) + assert_event_with_full_equality_and_count(vs_event_text, initial_count_vs + 1, events_ns_1) # 2nd VSRoute gets an event about update too assert_event_and_count(vsr_2_event_text, initial_count_vsr_2 + 1, events_ns_2) @@ -111,7 +113,7 @@ def test_responses_and_events_in_flow(self, kube_apis, events_ns_1 = get_events(kube_apis.v1, v_s_route_setup.route_m.namespace) events_ns_2 = get_events(kube_apis.v1, v_s_route_setup.route_s.namespace) assert_event_and_count(vsr_1_event_text, initial_count_vsr_1 + 2, events_ns_1) - assert_event_and_count(vs_event_text, initial_count_vs + 2, events_ns_1) + assert_event_with_full_equality_and_count(vs_event_text, initial_count_vs + 2, events_ns_1) assert_event_and_count(vsr_2_event_text, initial_count_vsr_2 + 2, events_ns_2) print("\nStep 4: update one backend service port and check") @@ -128,7 +130,7 @@ def test_responses_and_events_in_flow(self, kube_apis, events_ns_1 = get_events(kube_apis.v1, v_s_route_setup.route_m.namespace) events_ns_2 = get_events(kube_apis.v1, v_s_route_setup.route_s.namespace) assert_event_and_count(vsr_1_event_text, initial_count_vsr_1 + 3, events_ns_1) - assert_event_and_count(vs_event_text, initial_count_vs + 3, events_ns_1) + assert_event_with_full_equality_and_count(vs_event_text, initial_count_vs + 3, events_ns_1) assert_event_and_count(vsr_2_event_text, initial_count_vsr_2 + 3, events_ns_2) print("\nStep 5: restore backend service and check") @@ -145,7 +147,7 @@ def test_responses_and_events_in_flow(self, kube_apis, events_ns_1 = get_events(kube_apis.v1, v_s_route_setup.route_m.namespace) events_ns_2 = get_events(kube_apis.v1, v_s_route_setup.route_s.namespace) assert_event_and_count(vsr_1_event_text, initial_count_vsr_1 + 4, events_ns_1) - assert_event_and_count(vs_event_text, initial_count_vs + 4, events_ns_1) + assert_event_with_full_equality_and_count(vs_event_text, initial_count_vs + 4, events_ns_1) assert_event_and_count(vsr_2_event_text, initial_count_vsr_2 + 4, events_ns_2) print("\nStep 6: remove VSRoute and check") @@ -169,7 +171,9 @@ def test_responses_and_events_in_flow(self, kube_apis, events_ns_2 = get_events(kube_apis.v1, v_s_route_setup.route_s.namespace) assert_locations_not_in_config(new_config, v_s_route_setup.route_m.paths) assert_event_and_count(vsr_1_event_text, initial_count_vsr_1 + 4, events_ns_1) - assert_event_and_count(vs_event_text, initial_count_vs + 5, events_ns_1) + assert_event_with_full_equality_and_count(vs_event_text, initial_count_vs + 4, events_ns_1) + # a warning event because the VS references a non-existing VSR + assert_event_with_full_equality_and_count(vs_warning_event_text, 1, events_ns_1) assert_event_and_count(vsr_2_event_text, initial_count_vsr_2 + 5, events_ns_2) print("\nStep 7: restore VSRoute and check") @@ -193,7 +197,7 @@ def test_responses_and_events_in_flow(self, kube_apis, events_ns_2 = get_events(kube_apis.v1, v_s_route_setup.route_s.namespace) assert_locations_in_config(new_config, v_s_route_setup.route_m.paths) assert_event_and_count(vsr_1_event_text, 1, events_ns_1) - assert_event_and_count(vs_event_text, initial_count_vs + 6, events_ns_1) + assert_event_with_full_equality_and_count(vs_event_text, initial_count_vs + 5, events_ns_1) assert_event_and_count(vsr_2_event_text, initial_count_vsr_2 + 6, events_ns_2) print("\nStep 8: remove one backend service and check") @@ -211,7 +215,7 @@ def test_responses_and_events_in_flow(self, kube_apis, events_ns_1 = get_events(kube_apis.v1, v_s_route_setup.route_m.namespace) events_ns_2 = get_events(kube_apis.v1, v_s_route_setup.route_s.namespace) assert_event_and_count(vsr_1_event_text, 2, events_ns_1) - assert_event_and_count(vs_event_text, initial_count_vs + 7, events_ns_1) + assert_event_with_full_equality_and_count(vs_event_text, initial_count_vs + 6, events_ns_1) assert_event_and_count(vsr_2_event_text, initial_count_vsr_2 + 7, events_ns_2) print("\nStep 9: restore backend service and check") @@ -227,7 +231,7 @@ def test_responses_and_events_in_flow(self, kube_apis, events_ns_1 = get_events(kube_apis.v1, v_s_route_setup.route_m.namespace) events_ns_2 = get_events(kube_apis.v1, v_s_route_setup.route_s.namespace) assert_event_and_count(vsr_1_event_text, 3, events_ns_1) - assert_event_and_count(vs_event_text, initial_count_vs + 8, events_ns_1) + assert_event_with_full_equality_and_count(vs_event_text, initial_count_vs + 7, events_ns_1) assert_event_and_count(vsr_2_event_text, initial_count_vsr_2 + 8, events_ns_2) print("\nStep 10: remove VS and check") @@ -245,7 +249,7 @@ def test_responses_and_events_in_flow(self, kube_apis, list0_list_ns_1 = get_events(kube_apis.v1, v_s_route_setup.route_m.namespace) list0_list_ns_2 = get_events(kube_apis.v1, v_s_route_setup.route_s.namespace) assert_event_and_count(vsr_1_event_text, 3, list0_list_ns_1) - assert_event_and_count(vs_event_text, initial_count_vs + 8, list0_list_ns_1) + assert_event_with_full_equality_and_count(vs_event_text, initial_count_vs + 7, list0_list_ns_1) assert_event_and_count(vsr_2_event_text, initial_count_vsr_2 + 8, list0_list_ns_2) print("\nStep 11: restore VS and check") @@ -263,7 +267,7 @@ def test_responses_and_events_in_flow(self, kube_apis, list1_list_ns_1 = get_events(kube_apis.v1, v_s_route_setup.route_m.namespace) list1_list_ns_2 = get_events(kube_apis.v1, v_s_route_setup.route_s.namespace) assert_event_and_count(vsr_1_event_text, 4, list1_list_ns_1) - assert_event_and_count(vs_event_text, 1, list1_list_ns_1) + assert_event_with_full_equality_and_count(vs_event_text, 1, list1_list_ns_1) assert_event_and_count(vsr_2_event_text, initial_count_vsr_2 + 9, list1_list_ns_2) @@ -291,9 +295,7 @@ def test_vsr_without_vs(self, kube_apis, ingress_controller_prerequisites.namespace) new_list_ns_3 = get_events(kube_apis.v1, test_namespace) assert_locations_not_in_config(new_config, vsr_paths) - assert_event_and_count(f"No VirtualServer references VirtualServerRoute {test_namespace}/{vsr_name}", - 1, - new_list_ns_3) + assert_event_and_count(f"VirtualServer {v_s_route_setup.namespace}/{v_s_route_setup.vs_name} ignores VirtualServerRoute", 1, new_list_ns_3) @pytest.mark.parametrize("route_yaml", [f"{TEST_DATA}/virtual-server-route/route-single-invalid-host.yaml", f"{TEST_DATA}/virtual-server-route/route-single-duplicate-path.yaml"]) @@ -317,12 +319,19 @@ def test_make_existing_vsr_invalid(self, kube_apis, new_vsr_events = get_events(kube_apis.v1, v_s_route_setup.route_s.namespace) assert_locations_not_in_config(new_config, v_s_route_setup.route_s.paths) text = f"{v_s_route_setup.route_s.namespace}/{v_s_route_setup.route_s.name}" - assert_event_and_count(f"Ignored VirtualServerRoute {text}", + assert_event_and_count(f"Configuration for {v_s_route_setup.namespace}/{v_s_route_setup.vs_name} was added or updated with warning(s)", 1, new_vs_events) - assert_event_and_count(f"Ignored by VirtualServer {v_s_route_setup.namespace}/{v_s_route_setup.vs_name}", - 1, - new_vsr_events) + + if route_yaml == f"{TEST_DATA}/virtual-server-route/route-single-invalid-host.yaml": + assert_event_and_count(f"VirtualServer is invalid or doesn't exist", + 1, + new_vsr_events) + else: + assert_event_and_count(f"VirtualServerRoute {text} was rejected with error", + 1, + new_vsr_events) + def test_openapi_validation_flow(self, kube_apis, ingress_controller_prerequisites, crd_ingress_controller, v_s_route_setup): @@ -367,8 +376,8 @@ def test_create_invalid_vsr(self, kube_apis, route_yaml = f"{TEST_DATA}/virtual-server-route/route-single-duplicate-path.yaml" ic_pod_name = get_first_pod_name(kube_apis.v1, ingress_controller_prerequisites.namespace) text = f"{v_s_route_setup.route_s.namespace}/{v_s_route_setup.route_s.name}" - vs_event_text = f"Ignored VirtualServerRoute {text}: spec.subroutes[1].path: Duplicate value: \"/backend2\"" - vsr_event_text = f"Ignored by VirtualServer {v_s_route_setup.namespace}/{v_s_route_setup.vs_name}" + vs_event_text = f"Configuration for {v_s_route_setup.namespace}/{v_s_route_setup.vs_name} was added or updated with warning(s)" + vsr_event_text = f"VirtualServerRoute {text} was rejected with error: spec.subroutes[1].path: Duplicate value: \"/backend2\"" delete_v_s_route(kube_apis.custom_objects, v_s_route_setup.route_s.name, v_s_route_setup.route_s.namespace) diff --git a/tests/suite/test_v_s_route_canned_responses.py b/tests/suite/test_v_s_route_canned_responses.py index 9d8bac3855..f9a9fb8323 100644 --- a/tests/suite/test_v_s_route_canned_responses.py +++ b/tests/suite/test_v_s_route_canned_responses.py @@ -101,7 +101,7 @@ def test_validation_flow(self, kube_apis, crd_ingress_controller, v_s_route_setu req_host = f"{v_s_route_setup.public_endpoint.public_ip}:{v_s_route_setup.public_endpoint.port}" req_url = f"http://{req_host}{v_s_route_setup.route_s.paths[0]}" text = f"{v_s_route_setup.namespace}/{v_s_route_setup.route_m.name}" - vsr_m_event_text = f"VirtualServerRoute {text} is invalid and was rejected: " + vsr_m_event_text = f"VirtualServerRoute {text} was rejected with error:" vsr_src = f"{TEST_DATA}/virtual-server-route-canned-responses/route-multiple-invalid.yaml" patch_v_s_route_from_yaml(kube_apis.custom_objects, v_s_route_setup.route_m.name, vsr_src, v_s_route_setup.namespace) diff --git a/tests/suite/test_v_s_route_error_pages.py b/tests/suite/test_v_s_route_error_pages.py index 991fcd1ef6..af622c767d 100644 --- a/tests/suite/test_v_s_route_error_pages.py +++ b/tests/suite/test_v_s_route_error_pages.py @@ -73,8 +73,8 @@ def test_validation_event_flow(self, kube_apis, ingress_controller_prerequisites ] text_s = f"{v_s_route_setup.route_s.namespace}/{v_s_route_setup.route_s.name}" text_m = f"{v_s_route_setup.route_m.namespace}/{v_s_route_setup.route_m.name}" - vsr_s_event_text = f"VirtualServerRoute {text_s} is invalid and was rejected: " - vsr_m_event_text = f"VirtualServerRoute {text_m} is invalid and was rejected: " + vsr_s_event_text = f"VirtualServerRoute {text_s} was rejected with error:" + vsr_m_event_text = f"VirtualServerRoute {text_m} was rejected with error:" patch_v_s_route_from_yaml(kube_apis.custom_objects, v_s_route_setup.route_s.name, f"{TEST_DATA}/virtual-server-route-error-pages/route-single-invalid.yaml", diff --git a/tests/suite/test_v_s_route_externalname.py b/tests/suite/test_v_s_route_externalname.py index 9e4d74ff54..4ca02a8fc7 100644 --- a/tests/suite/test_v_s_route_externalname.py +++ b/tests/suite/test_v_s_route_externalname.py @@ -136,13 +136,13 @@ def test_events_flows(self, kube_apis, text_vsr = f"{vsr_externalname_setup.route.namespace}/{vsr_externalname_setup.route.name}" text_vs = f"{vsr_externalname_setup.namespace}/{vsr_externalname_setup.vs_name}" vsr_event_text = f"Configuration for {text_vsr} was added or updated" + vsr_event_warning_text = f"Configuration for {text_vsr} was added or updated with warning(s):" vs_event_text = f"Configuration for {text_vs} was added or updated" - vs_event_update_text = f"Configuration for {text_vs} was updated" wait_before_test(5) initial_events = get_events(kube_apis.v1, vsr_externalname_setup.route.namespace) initial_count_vsr = assert_event_and_get_count(vsr_event_text, initial_events) + initial_warning_count_vsr = assert_event_and_get_count(vsr_event_warning_text, initial_events) initial_count_vs = assert_event_and_get_count(vs_event_text, initial_events) - initial_count_vs_up = assert_event_and_get_count(vs_event_update_text, initial_events) print("Step 1: Update external host in externalName service") external_svc = read_service(kube_apis.v1, vsr_externalname_setup.external_svc, vsr_externalname_setup.namespace) @@ -156,10 +156,8 @@ def test_events_flows(self, kube_apis, events_step_1 = get_events(kube_apis.v1, vsr_externalname_setup.route.namespace) assert_event_and_count(vsr_event_text, initial_count_vsr + 1, events_step_1) assert_event_and_count(vs_event_text, initial_count_vs + 1, events_step_1) - assert_event_and_count(vs_event_update_text, initial_count_vs_up, events_step_1) print("Step 2: Remove resolver from ConfigMap to trigger an error") - vsr_event_warning_text = f"Configuration for {text_vsr} was updated with warning(s):" config_map_name = ingress_controller_prerequisites.config_map["metadata"]["name"] replace_configmap(kube_apis.v1, config_map_name, ingress_controller_prerequisites.namespace, @@ -167,5 +165,5 @@ def test_events_flows(self, kube_apis, wait_before_test(5) events_step_2 = get_events(kube_apis.v1, vsr_externalname_setup.route.namespace) - assert_event_and_count(vsr_event_warning_text, 1, events_step_2) - assert_event_and_count(vs_event_update_text, initial_count_vs_up + 1, events_step_2) + assert_event_and_count(vsr_event_warning_text, initial_warning_count_vsr + 1, events_step_2) + assert_event_and_count(vs_event_text, initial_count_vs + 2, events_step_2) diff --git a/tests/suite/test_v_s_route_redirects.py b/tests/suite/test_v_s_route_redirects.py index 6c25fb5eb9..c4fa11ed8b 100644 --- a/tests/suite/test_v_s_route_redirects.py +++ b/tests/suite/test_v_s_route_redirects.py @@ -70,7 +70,7 @@ def test_validation_flow(self, kube_apis, crd_ingress_controller, v_s_route_setu req_host = f"{v_s_route_setup.public_endpoint.public_ip}:{v_s_route_setup.public_endpoint.port}" req_url = f"http://{req_host}{v_s_route_setup.route_s.paths[0]}" text = f"{v_s_route_setup.namespace}/{v_s_route_setup.route_m.name}" - event_text = f"VirtualServerRoute {text} is invalid and was rejected: " + event_text = f"VirtualServerRoute {text} was rejected with error:" invalid_fields = [ "spec.subroutes[0].action.redirect.code", "spec.subroutes[1].action.redirect.url" ] diff --git a/tests/suite/test_v_s_route_regexp_location.py b/tests/suite/test_v_s_route_regexp_location.py index 46d99504aa..9c7d117633 100644 --- a/tests/suite/test_v_s_route_regexp_location.py +++ b/tests/suite/test_v_s_route_regexp_location.py @@ -61,7 +61,7 @@ def test_flow_for_invalid_vs(self, kube_apis, v_s_route_setup, v_s_route_app_setup): ic_pod_name = get_first_pod_name(kube_apis.v1, ingress_controller_prerequisites.namespace) text_vs = f"{v_s_route_setup.namespace}/{v_s_route_setup.vs_name}" - vs_event_text = f'VirtualServer {text_vs} is invalid and was rejected: ' \ + vs_event_text = f'VirtualServer {text_vs} was rejected with error: ' \ f'spec.routes[1].path: Duplicate value: "=/exact-match$request"' vs_src_yaml = f"{TEST_DATA}" \ f"/virtual-server-route-regexp-location/standard/virtual-server-invalid-duplicate-routes.yaml" @@ -80,9 +80,8 @@ def test_flow_for_invalid_vsr(self, kube_apis, ic_pod_name = get_first_pod_name(kube_apis.v1, ingress_controller_prerequisites.namespace) text_vs = f"{v_s_route_setup.namespace}/{v_s_route_setup.vs_name}" text_vsr_s = f"{v_s_route_setup.route_m.namespace}/{v_s_route_setup.route_m.name}" - vs_event_text = f'Ignored by VirtualServer {text_vs}: spec.subroutes: Invalid value: "subroutes": ' \ - f'must have only one subroute if regex match or exact match are being used' - vsr_event_text = f'VirtualServerRoute {text_vsr_s} is invalid and was rejected: ' \ + vs_event_text = f'Configuration for {text_vs} was added or updated with warning(s)' + vsr_event_text = f'VirtualServerRoute {text_vsr_s} was rejected with error: ' \ f'spec.subroutes[1].path: Duplicate value: "=/backends/exact-match$request"' vs_src_yaml = f"{TEST_DATA}" \ f"/virtual-server-route-regexp-location/standard/virtual-server-exact.yaml" diff --git a/tests/suite/test_v_s_route_status.py b/tests/suite/test_v_s_route_status.py index 4df0d80efa..a59436423d 100644 --- a/tests/suite/test_v_s_route_status.py +++ b/tests/suite/test_v_s_route_status.py @@ -126,14 +126,14 @@ def test_status_invalid( assert ( response_m["status"] and response_m["status"]["reason"] == "Rejected" - and response_m["status"]["referencedBy"] + and not response_m["status"]["referencedBy"] and response_m["status"]["state"] == "Invalid" ) assert ( response_s["status"] and response_s["status"]["reason"] == "Rejected" - and response_s["status"]["referencedBy"] + and not response_s["status"]["referencedBy"] and response_s["status"]["state"] == "Invalid" ) @@ -184,9 +184,9 @@ def test_status_invalid_prefix( assert ( response_s["status"] - and response_s["status"]["reason"] == "AddedOrUpdated" - and response_s["status"]["referencedBy"] - and response_s["status"]["state"] == "Valid" + and response_s["status"]["reason"] == "Ignored" + and not response_s["status"]["referencedBy"] + and response_s["status"]["state"] == "Warning" ) def test_status_invalid_vsr_in_vs( @@ -217,16 +217,16 @@ def test_status_invalid_vsr_in_vs( self.patch_valid_vs(kube_apis, v_s_route_setup) assert ( response_m["status"] - and response_m["status"]["reason"] == "NoVirtualServerFound" + and response_m["status"]["reason"] == "Ignored" and not response_m["status"]["referencedBy"] - and response_m["status"]["state"] == "Invalid" + and response_m["status"]["state"] == "Warning" ) assert ( response_s["status"] - and response_s["status"]["reason"] == "NoVirtualServerFound" + and response_s["status"]["reason"] == "Ignored" and not response_s["status"]["referencedBy"] - and response_s["status"]["state"] == "Invalid" + and response_s["status"]["state"] == "Warning" ) def test_status_remove_vs( @@ -259,12 +259,12 @@ def test_status_remove_vs( response_m["status"] and response_m["status"]["reason"] == "NoVirtualServerFound" and not response_m["status"]["referencedBy"] - and response_m["status"]["state"] == "Invalid" + and response_m["status"]["state"] == "Warning" ) assert ( response_s["status"] and response_s["status"]["reason"] == "NoVirtualServerFound" and not response_s["status"]["referencedBy"] - and response_s["status"]["state"] == "Invalid" + and response_s["status"]["state"] == "Warning" ) diff --git a/tests/suite/test_v_s_route_upstream_options.py b/tests/suite/test_v_s_route_upstream_options.py index 3fe89ec08c..8f4e382519 100644 --- a/tests/suite/test_v_s_route_upstream_options.py +++ b/tests/suite/test_v_s_route_upstream_options.py @@ -283,8 +283,8 @@ def test_event_message_and_config(self, kube_apis, ingress_controller_prerequisi ] text_s = f"{v_s_route_setup.route_s.namespace}/{v_s_route_setup.route_s.name}" text_m = f"{v_s_route_setup.route_m.namespace}/{v_s_route_setup.route_m.name}" - vsr_s_event_text = f"VirtualServerRoute {text_s} is invalid and was rejected: " - vsr_m_event_text = f"VirtualServerRoute {text_m} is invalid and was rejected: " + vsr_s_event_text = f"VirtualServerRoute {text_s} was rejected with error:" + vsr_m_event_text = f"VirtualServerRoute {text_m} was rejected with error:" patch_v_s_route_from_yaml(kube_apis.custom_objects, v_s_route_setup.route_s.name, f"{TEST_DATA}/virtual-server-route-upstream-options/route-single-invalid-keys.yaml", @@ -508,8 +508,8 @@ def test_validation_flow(self, kube_apis, ingress_controller_prerequisites, ] text_s = f"{v_s_route_setup.route_s.namespace}/{v_s_route_setup.route_s.name}" text_m = f"{v_s_route_setup.route_m.namespace}/{v_s_route_setup.route_m.name}" - vsr_s_event_text = f"VirtualServerRoute {text_s} is invalid and was rejected: " - vsr_m_event_text = f"VirtualServerRoute {text_m} is invalid and was rejected: " + vsr_s_event_text = f"VirtualServerRoute {text_s} was rejected with error:" + vsr_m_event_text = f"VirtualServerRoute {text_m} was rejected with error:" patch_v_s_route_from_yaml(kube_apis.custom_objects, v_s_route_setup.route_s.name, f"{TEST_DATA}/virtual-server-route-upstream-options/plus-route-s-invalid-keys.yaml", diff --git a/tests/suite/test_virtual_server_canned_responses.py b/tests/suite/test_virtual_server_canned_responses.py index 7cbbdfa6b9..5183b70741 100644 --- a/tests/suite/test_virtual_server_canned_responses.py +++ b/tests/suite/test_virtual_server_canned_responses.py @@ -89,7 +89,7 @@ def test_validation_flow(self, kube_apis, crd_ingress_controller, virtual_server "spec.routes[0].action.return.code", "spec.routes[0].action.return.body" ] text = f"{virtual_server_setup.namespace}/{virtual_server_setup.vs_name}" - vs_event_text = f"VirtualServer {text} is invalid and was rejected: " + vs_event_text = f"VirtualServer {text} was rejected with error:" vs_src = f"{TEST_DATA}/virtual-server-canned-responses/virtual-server-invalid.yaml" patch_virtual_server_from_yaml(kube_apis.custom_objects, virtual_server_setup.vs_name, vs_src, virtual_server_setup.namespace) diff --git a/tests/suite/test_virtual_server_configmap_keys.py b/tests/suite/test_virtual_server_configmap_keys.py index c0f7cd78ec..e418f72c39 100644 --- a/tests/suite/test_virtual_server_configmap_keys.py +++ b/tests/suite/test_virtual_server_configmap_keys.py @@ -9,8 +9,8 @@ def assert_update_events_emitted(virtual_server_setup, new_list, previous_list, expected_amount): item_name = f"{virtual_server_setup.namespace}/{virtual_server_setup.vs_name}" - text_valid = f"Configuration for {item_name} was updated" - text_invalid = "was updated but was not applied" + text_valid = f"Configuration for {item_name} was added or updated" + text_invalid = "but was not applied" new_event = new_list[len(new_list) - 1] assert len(new_list) - len(previous_list) == expected_amount assert text_valid in new_event.message and text_invalid not in new_event.message @@ -18,7 +18,7 @@ def assert_update_events_emitted(virtual_server_setup, new_list, previous_list, def assert_not_applied_events_emitted(virtual_server_setup, new_list, previous_list, expected_amount): item_name = f"{virtual_server_setup.namespace}/{virtual_server_setup.vs_name}" - text_invalid = f"Configuration for {item_name} was updated but was not applied" + text_invalid = f"Configuration for {item_name} was added or updated ; but was not applied" new_event = new_list[len(new_list) - 1] assert len(new_list) - len(previous_list) == expected_amount assert text_invalid in new_event.message @@ -26,8 +26,8 @@ def assert_not_applied_events_emitted(virtual_server_setup, new_list, previous_l def assert_update_event_count_increased(virtual_server_setup, new_list, previous_list): item_name = f"{virtual_server_setup.namespace}/{virtual_server_setup.vs_name}" - text_valid = f"Configuration for {item_name} was updated" - text_invalid = "was updated but was not applied" + text_valid = f"Configuration for {item_name} was added or updated" + text_invalid = "but was not applied" for i in range(len(previous_list)-1, 0, -1): if text_valid in previous_list[i].message and text_invalid not in previous_list[i].message: assert new_list[i].count - previous_list[i].count == 1, "We expect the counter to increase" @@ -158,7 +158,7 @@ def test_keys(self, cli_arguments, kube_apis, ingress_controller_prerequisites, virtual_server_setup.vs_name, ic_pod_name, ingress_controller_prerequisites.namespace) - assert_update_events_emitted(virtual_server_setup, step_1_events, initial_list, ic_pods_amount) + assert_update_event_count_increased(virtual_server_setup, step_1_events, initial_list) assert_keys_without_validation(step_1_config, expected_values) print("Step 2: update ConfigMap with invalid keys without validation rules") @@ -299,7 +299,7 @@ def test_ssl_keys(self, cli_arguments, kube_apis, ingress_controller_prerequisit virtual_server_setup.vs_name, ic_pod_name, ingress_controller_prerequisites.namespace) - assert_update_events_emitted(virtual_server_setup, step_1_events, initial_list, ic_pods_amount) + assert_update_event_count_increased(virtual_server_setup, step_1_events, initial_list) assert_ssl_keys(step_1_config) print("Step 2: update ConfigMap with invalid ssl keys") diff --git a/tests/suite/test_virtual_server_error_pages.py b/tests/suite/test_virtual_server_error_pages.py index 31252d4791..ac7305af79 100644 --- a/tests/suite/test_virtual_server_error_pages.py +++ b/tests/suite/test_virtual_server_error_pages.py @@ -59,7 +59,7 @@ def test_validation_event_flow(self, kube_apis, ingress_controller_prerequisites "spec.routes[1].errorPages[0].return.headers[0].value: Invalid value: \"schema\"" ] text = f"{virtual_server_setup.namespace}/{virtual_server_setup.vs_name}" - vs_event_text = f"VirtualServer {text} is invalid and was rejected: " + vs_event_text = f"VirtualServer {text} was rejected with error:" vs_file = f"{TEST_DATA}/virtual-server-error-pages/virtual-server-invalid.yaml" patch_virtual_server_from_yaml(kube_apis.custom_objects, virtual_server_setup.vs_name, diff --git a/tests/suite/test_virtual_server_external_name.py b/tests/suite/test_virtual_server_external_name.py index f34ac69142..c6ba17fb26 100644 --- a/tests/suite/test_virtual_server_external_name.py +++ b/tests/suite/test_virtual_server_external_name.py @@ -2,7 +2,7 @@ from settings import TEST_DATA from suite.custom_assertions import assert_event_and_count, assert_event_and_get_count, wait_and_assert_status_code, \ - wait_for_event_count_increases + wait_for_event_count_increases, assert_event_with_full_equality_and_count from suite.custom_resources_utils import get_vs_nginx_template_conf from suite.resources_utils import replace_configmap_from_yaml, \ ensure_connection_to_public_endpoint, replace_configmap, create_service_from_yaml, get_first_pod_name, get_events, \ @@ -88,11 +88,9 @@ def test_events_flows(self, kube_apis, ingress_controller_prerequisites, crd_ingress_controller, virtual_server_setup, vs_externalname_setup): text = f"{virtual_server_setup.namespace}/{virtual_server_setup.vs_name}" vs_event_text = f"Configuration for {text} was added or updated" - vs_event_update_text = f"Configuration for {text} was updated" wait_before_test(5) events_vs = get_events(kube_apis.v1, virtual_server_setup.namespace) initial_count = assert_event_and_get_count(vs_event_text, events_vs) - initial_count_up = assert_event_and_get_count(vs_event_update_text, events_vs) print("Step 1: Update external host in externalName service") external_svc = read_service(kube_apis.v1, vs_externalname_setup.external_svc, virtual_server_setup.namespace) @@ -103,11 +101,10 @@ def test_events_flows(self, kube_apis, ingress_controller_prerequisites, wait_for_event_count_increases(kube_apis, vs_event_text, initial_count, virtual_server_setup.namespace) events_step_1 = get_events(kube_apis.v1, virtual_server_setup.namespace) assert_event_and_count(vs_event_text, initial_count + 1, events_step_1) - assert_event_and_count(vs_event_update_text, initial_count_up, events_step_1) print("Step 2: Remove resolver from ConfigMap to trigger an error") config_map_name = ingress_controller_prerequisites.config_map["metadata"]["name"] - vs_event_warning_text = f"Configuration for {text} was updated with warning(s):" + vs_event_warning_text = f"Configuration for {text} was added or updated ; with warning(s):" replace_configmap(kube_apis.v1, config_map_name, ingress_controller_prerequisites.namespace, ingress_controller_prerequisites.config_map) @@ -115,4 +112,4 @@ def test_events_flows(self, kube_apis, ingress_controller_prerequisites, events_step_2 = get_events(kube_apis.v1, virtual_server_setup.namespace) assert_event_and_count(vs_event_warning_text, 1, events_step_2) - assert_event_and_count(vs_event_update_text, initial_count_up, events_step_2) + assert_event_with_full_equality_and_count(vs_event_text, initial_count + 1, events_step_2) diff --git a/tests/suite/test_virtual_server_redirects.py b/tests/suite/test_virtual_server_redirects.py index 9c57436af8..1ff01fff1c 100644 --- a/tests/suite/test_virtual_server_redirects.py +++ b/tests/suite/test_virtual_server_redirects.py @@ -62,7 +62,7 @@ def test_update(self, kube_apis, crd_ingress_controller, virtual_server_setup): def test_validation_flow(self, kube_apis, crd_ingress_controller, virtual_server_setup): text = f"{virtual_server_setup.namespace}/{virtual_server_setup.vs_name}" - event_text = f"VirtualServer {text} is invalid and was rejected: " + event_text = f"VirtualServer {text} was rejected with error:" invalid_fields = ["spec.routes[0].action.redirect.code", "spec.routes[1].action.redirect.url"] vs_src = f"{TEST_DATA}/virtual-server-redirects/virtual-server-invalid.yaml" patch_virtual_server_from_yaml(kube_apis.custom_objects, virtual_server_setup.vs_name, vs_src, diff --git a/tests/suite/test_virtual_server_upstream_options.py b/tests/suite/test_virtual_server_upstream_options.py index b407923553..d46d84c2de 100644 --- a/tests/suite/test_virtual_server_upstream_options.py +++ b/tests/suite/test_virtual_server_upstream_options.py @@ -123,7 +123,7 @@ def test_when_option_in_config_map_only(self, kube_apis, ingress_controller_prer crd_ingress_controller, virtual_server_setup, restore_configmap, config_map_file, expected_strings, unexpected_strings): text = f"{virtual_server_setup.namespace}/{virtual_server_setup.vs_name}" - vs_event_text = f"Configuration for {text} was updated" + vs_event_text = f"Configuration for {text} was added or updated" print(f"Case 3: key specified in ConfigMap, no option in VS") patch_virtual_server_from_yaml(kube_apis.custom_objects, virtual_server_setup.vs_name, f"{TEST_DATA}/virtual-server-upstream-options/standard/virtual-server.yaml", @@ -233,7 +233,7 @@ def test_event_message_and_config(self, kube_apis, ingress_controller_prerequisi "upstreams[1].buffers.number", "upstreams[1].buffers.size", "upstreams[1].buffer-size" ] text = f"{virtual_server_setup.namespace}/{virtual_server_setup.vs_name}" - vs_event_text = f"VirtualServer {text} is invalid and was rejected: " + vs_event_text = f"VirtualServer {text} was rejected with error:" vs_file = f"{TEST_DATA}/virtual-server-upstream-options/virtual-server-with-invalid-keys.yaml" patch_virtual_server_from_yaml(kube_apis.custom_objects, virtual_server_setup.vs_name, @@ -366,7 +366,7 @@ def test_slow_start_warning(self, kube_apis, ingress_controller_prerequisites, crd_ingress_controller, virtual_server_setup, options): ic_pod_name = get_first_pod_name(kube_apis.v1, ingress_controller_prerequisites.namespace) text = f"{virtual_server_setup.namespace}/{virtual_server_setup.vs_name}" - vs_event_text = f"Configuration for {text} was added or updated with warning(s): Slow start will be disabled" + vs_event_text = f"Configuration for {text} was added or updated ; with warning(s): Slow start will be disabled" print(f"Case 0: verify a warning") new_body = generate_item_with_upstream_options( f"{TEST_DATA}/virtual-server-upstream-options/standard/virtual-server.yaml", @@ -410,7 +410,7 @@ def test_validation_flow(self, kube_apis, ingress_controller_prerequisites, "upstreams[1].sessionCookie.expires", "upstreams[1].sessionCookie.domain" ] text = f"{virtual_server_setup.namespace}/{virtual_server_setup.vs_name}" - vs_event_text = f"VirtualServer {text} is invalid and was rejected: " + vs_event_text = f"VirtualServer {text} was rejected with error:" vs_file = f"{TEST_DATA}/virtual-server-upstream-options/plus-virtual-server-with-invalid-keys.yaml" patch_virtual_server_from_yaml(kube_apis.custom_objects, virtual_server_setup.vs_name, diff --git a/tests/suite/test_virtual_server_validation.py b/tests/suite/test_virtual_server_validation.py index 8df14d3a0e..4b6639eedc 100644 --- a/tests/suite/test_virtual_server_validation.py +++ b/tests/suite/test_virtual_server_validation.py @@ -10,7 +10,7 @@ def assert_reject_events_emitted(virtual_server_setup, new_list, previous_list, expected_amount): item_name = f"{virtual_server_setup.namespace}/{virtual_server_setup.vs_name}" - text_invalid = f"VirtualServer {item_name} is invalid and was rejected" + text_invalid = f"VirtualServer {item_name} was rejected with error" new_event = new_list[len(new_list) - 1] assert len(new_list) - len(previous_list) == expected_amount assert text_invalid in new_event.message