pkg/appgw/configbuilder.go (229 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 appgw
import (
"fmt"
"time"
n "github.com/Azure/azure-sdk-for-go/services/network/mgmt/2021-03-01/network"
"github.com/Azure/go-autorest/autorest/to"
v1 "k8s.io/api/core/v1"
networking "k8s.io/api/networking/v1"
"k8s.io/client-go/tools/record"
"k8s.io/klog/v2"
"github.com/Azure/application-gateway-kubernetes-ingress/pkg/annotations"
"github.com/Azure/application-gateway-kubernetes-ingress/pkg/azure"
"github.com/Azure/application-gateway-kubernetes-ingress/pkg/azure/tags"
"github.com/Azure/application-gateway-kubernetes-ingress/pkg/controllererrors"
"github.com/Azure/application-gateway-kubernetes-ingress/pkg/environment"
"github.com/Azure/application-gateway-kubernetes-ingress/pkg/k8scontext"
"github.com/Azure/application-gateway-kubernetes-ingress/pkg/utils"
"github.com/Azure/application-gateway-kubernetes-ingress/pkg/version"
)
// Clock is an interface, which allows you to implement your own Time.
type Clock interface {
Now() time.Time
}
// ConfigBuilder is a builder for application gateway configuration
type ConfigBuilder interface {
PreBuildValidate(cbCtx *ConfigBuilderContext) error
Build(cbCtx *ConfigBuilderContext) (*n.ApplicationGateway, error)
PostBuildValidate(cbCtx *ConfigBuilderContext) error
}
type memoization struct {
listeners *[]n.ApplicationGatewayHTTPListener
listenerConfigs *map[listenerIdentifier]listenerAzConfig
routingRules *[]n.ApplicationGatewayRequestRoutingRule
pathMaps *[]n.ApplicationGatewayURLPathMap
probesByName *map[string]n.ApplicationGatewayProbe
probesByBackend *map[backendIdentifier]*n.ApplicationGatewayProbe
backendIDs *map[backendIdentifier]interface{}
settings *[]n.ApplicationGatewayBackendHTTPSettings
settingsByBackend *map[backendIdentifier]*n.ApplicationGatewayBackendHTTPSettings
serviceBackendPairsByBackend *map[backendIdentifier]serviceBackendPortPair
rewrites *[]n.ApplicationGatewayRewriteRuleSet
pools *[]n.ApplicationGatewayBackendAddressPool
certs *[]n.ApplicationGatewaySslCertificate
redirectConfigs *[]n.ApplicationGatewayRedirectConfiguration
ports *[]n.ApplicationGatewayFrontendPort
}
type appGwConfigBuilder struct {
k8sContext *k8scontext.Context
appGwIdentifier Identifier
appGw n.ApplicationGateway
recorder record.EventRecorder
mem memoization
clock Clock
}
// NewConfigBuilder construct a builder
func NewConfigBuilder(context *k8scontext.Context, appGwIdentifier *Identifier, original *n.ApplicationGateway, recorder record.EventRecorder, clock Clock) ConfigBuilder {
return &appGwConfigBuilder{
k8sContext: context,
appGwIdentifier: *appGwIdentifier,
appGw: *original,
recorder: recorder,
clock: clock,
}
}
// Build gets a pointer to updated ApplicationGatewayPropertiesFormat.
func (c *appGwConfigBuilder) Build(cbCtx *ConfigBuilderContext) (*n.ApplicationGateway, error) {
err := c.HealthProbesCollection(cbCtx)
if err != nil {
e := controllererrors.NewErrorWithInnerError(
controllererrors.ErrorGeneratingProbes,
err,
"unable to generate Health Probes",
)
klog.Errorf(e.Error())
return nil, e
}
err = c.BackendHTTPSettingsCollection(cbCtx)
if err != nil {
e := controllererrors.NewErrorWithInnerError(
controllererrors.ErrorGeneratingBackendSettings,
err,
"unable to generate backend http settings",
)
klog.Errorf(e.Error())
return nil, e
}
// BackendAddressPools depend on BackendHTTPSettings
err = c.BackendAddressPools(cbCtx)
if err != nil {
e := controllererrors.NewErrorWithInnerError(
controllererrors.ErrorCreatingBackendPools,
err,
"unable to generate backend address pools",
)
klog.Errorf(e.Error())
return nil, e
}
// Listener configures the frontend listeners
// This also creates redirection configuration (if TLS is configured and Ingress is annotated).
// This configuration must be attached to request routing rules, which are created in the steps below.
// The order of operations matters.
err = c.Listeners(cbCtx)
if err != nil {
e := controllererrors.NewErrorWithInnerError(
controllererrors.ErrorGeneratingListeners,
err,
"unable to generate frontend listeners",
)
klog.Errorf(e.Error())
return nil, e
}
// Build RewriteRuleSets configuration
err = c.RewriteRuleSets(cbCtx)
if err != nil {
e := controllererrors.NewErrorWithInnerError(
controllererrors.ErrorCreatingRewrites,
err,
"unable to generate rewrite rule sets",
)
klog.Errorf(e.Error())
return nil, e
}
// SSL redirection configurations created elsewhere will be attached to the appropriate rule in this step.
err = c.RequestRoutingRules(cbCtx)
if err != nil {
e := controllererrors.NewErrorWithInnerError(
controllererrors.ErrorGeneratingRoutingRules,
err,
"unable to generate request routing rules",
)
klog.Errorf(e.Error())
return nil, e
}
// Remove unused default pool and settings
c.CleanUpUnusedDefaults()
c.addTags()
return &c.appGw, nil
}
type valFunc func(eventRecorder record.EventRecorder, config *n.ApplicationGatewayPropertiesFormat, envVariables environment.EnvVariables, ingressList []*networking.Ingress, serviceList []*v1.Service) error
// PreBuildValidate runs all the validators that suggest misconfiguration in Kubernetes resources.
func (c *appGwConfigBuilder) PreBuildValidate(cbCtx *ConfigBuilderContext) error {
validationFunctions := []valFunc{
validateServiceDefinition,
}
return c.runValidationFunctions(cbCtx, validationFunctions)
}
// PostBuildValidate runs all the validators on the config constructed to ensure it complies with App Gateway requirements.
func (c *appGwConfigBuilder) PostBuildValidate(cbCtx *ConfigBuilderContext) error {
validationFunctions := []valFunc{
validateURLPathMaps,
}
return c.runValidationFunctions(cbCtx, validationFunctions)
}
func (c *appGwConfigBuilder) runValidationFunctions(cbCtx *ConfigBuilderContext, validationFunctions []valFunc) error {
for _, fn := range validationFunctions {
if err := fn(c.recorder, c.appGw.ApplicationGatewayPropertiesFormat, cbCtx.EnvVariables, cbCtx.IngressList, cbCtx.ServiceList); err != nil {
return err
}
}
return nil
}
// resolvePortName function goes through the endpoints of a given service and
// look for possible port number corresponding to a port name
func (c *appGwConfigBuilder) resolvePortName(portName string, backendID *backendIdentifier) map[int32]interface{} {
resolvedPorts := make(map[int32]interface{})
endpoints, err := c.k8sContext.GetEndpointsByService(backendID.serviceKey())
if err != nil {
klog.Error("Could not fetch endpoint by service key from cache", err)
return resolvedPorts
}
if endpoints == nil {
return resolvedPorts
}
for _, subset := range endpoints.Subsets {
for _, epPort := range subset.Ports {
if epPort.Name == portName {
resolvedPorts[epPort.Port] = nil
}
}
}
return resolvedPorts
}
func generateBackendID(ingress *networking.Ingress, rule *networking.IngressRule, path *networking.HTTPIngressPath, backend *networking.IngressBackend) backendIdentifier {
return backendIdentifier{
serviceIdentifier: serviceIdentifier{
Namespace: ingress.Namespace,
Name: backend.Service.Name,
},
Ingress: ingress,
Rule: rule,
Path: path,
Backend: backend,
}
}
func generateListenerID(ingress *networking.Ingress, rule *networking.IngressRule, protocol n.ApplicationGatewayProtocol, overridePort *Port, usePrivateIP bool) listenerIdentifier {
frontendPort := Port(80)
if protocol == n.ApplicationGatewayProtocolHTTPS {
frontendPort = Port(443)
}
if overridePort != nil {
if *overridePort > 0 && *overridePort < 65000 {
frontendPort = *overridePort
klog.V(3).Infof("Using custom port specified in the override annotation: %d", *overridePort)
} else {
klog.V(3).Infof("Derived listener port from ingress: %d", frontendPort)
}
}
frontendType := FrontendTypePublic
if usePrivateIP {
frontendType = FrontendTypePrivate
}
listenerID := listenerIdentifier{
FrontendPort: frontendPort,
FrontendType: frontendType,
}
var hostNames []string
if rule != nil && rule.Host != "" {
hostNames = append(hostNames, rule.Host)
}
extendedHostNames, err := annotations.GetHostNameExtensions(ingress)
if err != nil && !controllererrors.IsErrorCode(err, controllererrors.ErrorMissingAnnotation) {
klog.V(3).Infof("Error while parsing host name extensions: %s", err)
} else {
if extendedHostNames != nil {
hostNames = append(hostNames, extendedHostNames...)
}
}
hostNames = utils.RemoveDuplicateStrings(hostNames)
listenerID.setHostNames(hostNames)
return listenerID
}
// addTags will add certain tags to Application Gateway
func (c *appGwConfigBuilder) addTags() {
if c.appGw.Tags == nil {
c.appGw.Tags = make(map[string]*string)
}
// Identify the App Gateway as being exclusively managed by a Kubernetes Ingress.
c.appGw.Tags[tags.ManagedByK8sIngress] = to.StringPtr(GetVersion())
if aksResourceID, err := azure.ConvertToClusterResourceGroup(c.k8sContext.GetInfrastructureResourceGroupID()); err == nil {
c.appGw.Tags[tags.IngressForAKSClusterID] = to.StringPtr(aksResourceID)
} else {
klog.V(3).Infof("Error while parsing cluster resource ID for tagging: %s", err)
}
}
// GetVersion returns a string representing the version of AGIC.
func GetVersion() string {
return fmt.Sprintf("%s/%s/%s", version.Version, version.GitCommit, version.BuildDate)
}