pkg/brownfield/routing_rules.go (176 lines of code) (raw):
// -------------------------------------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.
// --------------------------------------------------------------------------------------------
package brownfield
import (
"strings"
n "github.com/Azure/azure-sdk-for-go/services/network/mgmt/2021-03-01/network"
"k8s.io/klog/v2"
"github.com/Azure/application-gateway-kubernetes-ingress/pkg/controllererrors"
"github.com/Azure/application-gateway-kubernetes-ingress/pkg/utils"
)
type ruleName string
type rulesByName map[ruleName]n.ApplicationGatewayRequestRoutingRule
type ruleToTargets map[ruleName][]Target
// GetBlacklistedRoutingRules filters the given list of routing rules to the list rules that AGIC is allowed to manage.
func (er ExistingResources) GetBlacklistedRoutingRules() ([]n.ApplicationGatewayRequestRoutingRule, []n.ApplicationGatewayRequestRoutingRule) {
// TODO(draychev): make a method of ExistingResources
blacklist := GetTargetBlacklist(er.ProhibitedTargets)
if blacklist == nil {
return nil, er.RoutingRules
}
ruleToTargets, _ := er.getRuleToTargets()
klog.V(3).Infof("[brownfield] Rule to Targets map: %+v", ruleToTargets)
// Figure out if the given routing rule is blacklisted. It will be if it has a host/path that
// has been referenced in a AzureIngressProhibitedTarget CRD (even if it has some other paths that are not)
isBlacklisted := func(rule n.ApplicationGatewayRequestRoutingRule) bool {
targetsForRule := ruleToTargets[ruleName(*rule.Name)]
for _, target := range targetsForRule {
if target.IsBlacklisted(blacklist) {
klog.V(3).Infof("[brownfield] Routing Rule %s is blacklisted", *rule.Name)
return true
}
}
klog.V(3).Infof("[brownfield] Routing Rule %s is NOT blacklisted", *rule.Name)
return false
}
var blacklistedRules []n.ApplicationGatewayRequestRoutingRule
var nonBlacklistedRules []n.ApplicationGatewayRequestRoutingRule
for _, rule := range er.RoutingRules {
if isBlacklisted(rule) {
blacklistedRules = append(blacklistedRules, rule)
continue
}
nonBlacklistedRules = append(nonBlacklistedRules, rule)
}
return blacklistedRules, nonBlacklistedRules
}
// MergeRules merges list of lists of rules into a single list, maintaining uniqueness.
func MergeRules(appGw *n.ApplicationGateway, ruleBuckets ...[]n.ApplicationGatewayRequestRoutingRule) []n.ApplicationGatewayRequestRoutingRule {
uniq := make(map[string]*n.ApplicationGatewayRequestRoutingRule)
for _, bucket := range ruleBuckets {
for idx := range bucket {
rule := &bucket[idx]
// If two rules share the listener, we merge them. We keep the existing rule and merge the url path maps.
if existingRule, exists := uniq[*rule.HTTPListener.ID]; exists {
uniq[*rule.HTTPListener.ID] = mergeRoutingRules(appGw, existingRule, rule)
} else {
uniq[*rule.HTTPListener.ID] = rule
}
}
}
var merged []n.ApplicationGatewayRequestRoutingRule
for _, rule := range uniq {
merged = append(merged, *rule)
}
return merged
}
// LogRules emits a few log lines detailing what rules are created, blacklisted, and removed from ARM.
func LogRules(existingBlacklisted []n.ApplicationGatewayRequestRoutingRule, existingNonBlacklisted []n.ApplicationGatewayRequestRoutingRule, managedRules []n.ApplicationGatewayRequestRoutingRule) {
var garbage []n.ApplicationGatewayRequestRoutingRule
blacklistedSet := indexRulesByName(existingBlacklisted)
managedSet := indexRulesByName(managedRules)
for ruleName, rule := range indexRulesByName(existingNonBlacklisted) {
_, existsInBlacklist := blacklistedSet[ruleName]
_, existsInNewRules := managedSet[ruleName]
if !existsInBlacklist && !existsInNewRules {
garbage = append(garbage, rule)
}
}
klog.V(3).Info("[brownfield] Rules AGIC created: ", getRuleNames(managedRules))
klog.V(3).Info("[brownfield] Existing Blacklisted Rules AGIC will retain: ", getRuleNames(existingBlacklisted))
klog.V(3).Info("[brownfield] Existing Rules AGIC will remove: ", getRuleNames(garbage))
}
// mergeRoutingRules merges two routing rules by merging their pathRules
func mergeRoutingRules(appGw *n.ApplicationGateway, firstRoutingRule *n.ApplicationGatewayRequestRoutingRule, secondRoutingRule *n.ApplicationGatewayRequestRoutingRule) *n.ApplicationGatewayRequestRoutingRule {
if firstRoutingRule.RuleType == n.ApplicationGatewayRequestRoutingRuleTypeBasic &&
secondRoutingRule.RuleType == n.ApplicationGatewayRequestRoutingRuleTypePathBasedRouting {
return mergeRoutingRules(appGw, secondRoutingRule, firstRoutingRule)
}
if firstRoutingRule.RuleType == n.ApplicationGatewayRequestRoutingRuleTypePathBasedRouting {
// Get the url path map of the first rule
klog.V(3).Infof("[brownfield] Merging path based rule %s with rule %s", *firstRoutingRule.Name, *secondRoutingRule.Name)
firstPathMap := lookupPathMap(appGw.URLPathMaps, firstRoutingRule.URLPathMap.ID)
if secondRoutingRule.RuleType == n.ApplicationGatewayRequestRoutingRuleTypeBasic {
// Replace the default values from the second rule
klog.V(3).Infof("[brownfield] Merging path map %s with rule %s", *firstPathMap.Name, *secondRoutingRule.Name)
mergePathMapsWithBasicRule(firstPathMap, secondRoutingRule)
return firstRoutingRule
}
if *firstRoutingRule.URLPathMap.ID == *secondRoutingRule.URLPathMap.ID {
// No merge needed as both routing rule point to the same URL Path map
klog.V(3).Infof("[brownfield] Rule %s and rule %s share the same path map %s; Skipping merge", *firstRoutingRule.Name, *secondRoutingRule.Name, *firstPathMap.Name)
return firstRoutingRule
}
// Get the url path map for the second rule
secondPathMap := lookupPathMap(appGw.URLPathMaps, secondRoutingRule.URLPathMap.ID)
// Merge the path rules from second path map to first path map
klog.V(3).Infof("[brownfield] Merging path map %s with path map %s", *firstPathMap.Name, *secondPathMap.Name)
firstPathMap.PathRules = mergePathRules(firstPathMap.PathRules, secondPathMap.PathRules)
// Delete the second path map
klog.V(3).Infof("[brownfield] Deleting path map %s", *secondPathMap.Name)
appGw.URLPathMaps = deletePathMap(appGw.URLPathMaps, secondPathMap.ID)
}
return firstRoutingRule
}
func getRuleNames(cert []n.ApplicationGatewayRequestRoutingRule) string {
var names []string
for _, p := range cert {
names = append(names, *p.Name)
}
if len(names) == 0 {
return "n/a"
}
return strings.Join(names, ", ")
}
func indexRulesByName(rules []n.ApplicationGatewayRequestRoutingRule) rulesByName {
indexed := make(rulesByName)
for _, rule := range rules {
indexed[ruleName(*rule.Name)] = rule
}
return indexed
}
func (er ExistingResources) getHostNamesForRoutingRule(rule n.ApplicationGatewayRequestRoutingRule) ([]string, error) {
listenerName := listenerName(utils.GetLastChunkOfSlashed(*rule.HTTPListener.ID))
if listener, found := er.getListenersByName()[listenerName]; !found {
e := controllererrors.NewErrorf(
controllererrors.ErrorGeneratingListeners,
"[brownfield] Could not find listener %s in index", listenerName,
)
klog.Errorf(e.Error())
return []string{""}, e
} else if listener.HostName != nil {
return []string{*listener.HostName}, nil
} else if listener.HostNames != nil && len(*listener.HostNames) > 0 {
return *listener.HostNames, nil
}
return []string{""}, nil
}
// getRuleToTargets creates a map from backend pool to targets this backend pool is responsible for.
// We rely on the configuration that AGIC has already constructed: Frontend Listener, Routing Rules, etc.
// We use the Listener to obtain the target hostname, the RoutingRule to get the URL etc.
func (er ExistingResources) getRuleToTargets() (ruleToTargets, pathmapToTargets) {
ruleToTargets := make(ruleToTargets)
pathMapToTargets := make(pathmapToTargets)
for _, rule := range er.RoutingRules {
if rule.HTTPListener == nil || rule.HTTPListener.ID == nil {
continue
}
HostNames, err := er.getHostNamesForRoutingRule(rule)
if err != nil {
klog.Errorf("[brownfield] Could not obtain hostname for rule %s; Skipping rule", ruleName(*rule.Name))
continue
}
for _, hostName := range HostNames {
// Regardless of whether we have a URL PathMap or not. This matches the default backend pool.
ruleToTargets[ruleName(*rule.Name)] = append(ruleToTargets[ruleName(*rule.Name)], Target{
Hostname: hostName,
// Path deliberately omitted
})
}
// SSL Redirects do not have BackendAddressPool
if rule.URLPathMap != nil {
// Follow the path map
pathMapName, pathRules := er.getPathRules(rule)
for _, pathRule := range pathRules {
if pathRule.Paths == nil {
klog.V(3).Infof("[brownfield] Path Rule %+v does not have paths list", *pathRule.Name)
continue
}
for _, path := range *pathRule.Paths {
for _, hostName := range HostNames {
target := Target{hostName, TargetPath(path)}
ruleToTargets[ruleName(*rule.Name)] = append(ruleToTargets[ruleName(*rule.Name)], target)
pathMapToTargets[pathMapName] = append(pathMapToTargets[pathMapName], target)
}
}
}
}
}
return ruleToTargets, pathMapToTargets
}
func (er ExistingResources) getPathRules(rule n.ApplicationGatewayRequestRoutingRule) (urlPathMapName, []n.ApplicationGatewayPathRule) {
pathMapName := urlPathMapName(utils.GetLastChunkOfSlashed(*rule.URLPathMap.ID))
pathMapsByName := er.getURLPathMapsByName()
if pathMap, ok := pathMapsByName[pathMapName]; ok {
return pathMapName, *pathMap.PathRules
}
klog.Errorf("[brownfield] Did not find URLPathMap with ID %s", pathMapName)
return pathMapName, []n.ApplicationGatewayPathRule{}
}