pkg/appgw/backendhttpsettings.go (262 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"
"sort"
"strings"
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"
"k8s.io/apimachinery/pkg/util/intstr"
"k8s.io/klog/v2"
"github.com/Azure/application-gateway-kubernetes-ingress/pkg/annotations"
"github.com/Azure/application-gateway-kubernetes-ingress/pkg/brownfield"
"github.com/Azure/application-gateway-kubernetes-ingress/pkg/controllererrors"
"github.com/Azure/application-gateway-kubernetes-ingress/pkg/events"
"github.com/Azure/application-gateway-kubernetes-ingress/pkg/sorter"
)
const (
// DefaultConnDrainTimeoutInSec provides default value for ConnectionDrainTimeout
DefaultConnDrainTimeoutInSec = 30
)
func (c *appGwConfigBuilder) BackendHTTPSettingsCollection(cbCtx *ConfigBuilderContext) error {
agicHTTPSettings, _, _, err := c.getBackendsAndSettingsMap(cbCtx)
if cbCtx.EnvVariables.EnableBrownfieldDeployment {
rCtx := brownfield.NewExistingResources(c.appGw, cbCtx.ProhibitedTargets, nil)
// PathMaps we obtained from App Gateway - we segment them into ones AGIC is and is not allowed to change.
existingBlacklisted, existingNonBlacklisted := rCtx.GetBlacklistedHTTPSettings()
brownfield.LogHTTPSettings(klog.V(3), existingBlacklisted, existingNonBlacklisted, agicHTTPSettings)
// MergePathMaps would produce unique list of routing rules based on Name. Routing rules, which have the same name
// as a managed rule would be overwritten.
agicHTTPSettings = brownfield.MergeHTTPSettings(existingBlacklisted, agicHTTPSettings)
}
if cbCtx.EnvVariables.EnableIstioIntegration {
istioHTTPSettings, _, _, _ := c.getIstioDestinationsAndSettingsMap(cbCtx)
if istioHTTPSettings != nil {
sort.Sort(sorter.BySettingsName(istioHTTPSettings))
}
agicHTTPSettings = append(agicHTTPSettings, istioHTTPSettings...)
}
if agicHTTPSettings != nil {
sort.Sort(sorter.BySettingsName(agicHTTPSettings))
}
c.appGw.BackendHTTPSettingsCollection = &agicHTTPSettings
return err
}
func newServiceSet(services *[]*v1.Service) map[string]*v1.Service {
servicesSet := make(map[string]*v1.Service)
for _, service := range *services {
serviceKey := fmt.Sprintf("%s/%s", service.Namespace, service.Name)
servicesSet[serviceKey] = service
}
return servicesSet
}
func (c *appGwConfigBuilder) getBackendsAndSettingsMap(cbCtx *ConfigBuilderContext) ([]n.ApplicationGatewayBackendHTTPSettings, map[backendIdentifier]*n.ApplicationGatewayBackendHTTPSettings, map[backendIdentifier]serviceBackendPortPair, error) {
if c.mem.settings != nil && c.mem.settingsByBackend != nil && c.mem.serviceBackendPairsByBackend != nil {
return *c.mem.settings, *c.mem.settingsByBackend, *c.mem.serviceBackendPairsByBackend, nil
}
defaultHTTPSetting := defaultBackendHTTPSettings(c.appGwIdentifier, n.ApplicationGatewayProtocolHTTP)
serviceBackendPairMap := make(map[backendIdentifier]serviceBackendPortPair)
backendHTTPSettingsMap := make(map[backendIdentifier]*n.ApplicationGatewayBackendHTTPSettings)
httpSettingsCollection := make(map[string]n.ApplicationGatewayBackendHTTPSettings)
httpSettingsCollection[*defaultHTTPSetting.Name] = defaultHTTPSetting
for backendID := range c.newBackendIdsFiltered(cbCtx) {
backendPort, err := c.resolveBackendPort(backendID)
if err != nil {
c.recorder.Event(backendID.Ingress, v1.EventTypeWarning, events.ReasonPortResolutionError, err.Error())
klog.Error(err.Error())
}
httpSettings := c.generateHTTPSettings(backendID, backendPort, cbCtx)
klog.Infof("Created backend http settings %s for ingress %s/%s and service %s", *httpSettings.Name, backendID.Ingress.Namespace, backendID.Ingress.Name, backendID.serviceKey())
// TODO(aksgupta): Only backend port is used in the output; remove service port.
serviceBackendPairMap[backendID] = serviceBackendPortPair{
ServicePort: backendPort,
BackendPort: backendPort,
}
httpSettingsCollection[*httpSettings.Name] = httpSettings
backendHTTPSettingsMap[backendID] = &httpSettings
}
httpSettings := make([]n.ApplicationGatewayBackendHTTPSettings, 0, len(httpSettingsCollection))
for _, backend := range httpSettingsCollection {
httpSettings = append(httpSettings, backend)
}
c.mem.settings = &httpSettings
c.mem.settingsByBackend = &backendHTTPSettingsMap
c.mem.serviceBackendPairsByBackend = &serviceBackendPairMap
return httpSettings, backendHTTPSettingsMap, serviceBackendPairMap, nil
}
func (c *appGwConfigBuilder) resolveBackendPort(backendID backendIdentifier) (Port, error) {
var e error
service := c.k8sContext.GetService(backendID.serviceKey())
if service == nil {
// This should never happen since newBackendIdsFiltered() already filters out backends for non-existent Services
e = controllererrors.NewErrorf(
controllererrors.ErrorServiceNotFound,
"Service not found %s",
backendID.serviceKey())
backendPort := Port(80)
if backendID.Backend.Service.Port.Name == "" && backendID.Backend.Service.Port.Number < 65536 {
backendPort = Port(backendID.Backend.Service.Port.Number)
}
return backendPort, e
}
// find the target port number for service port specified in the ingress manifest
servicePortInIngress := fmt.Sprint(backendID.Backend.Service.Port.Number)
if backendID.Backend.Service.Port.Name != "" {
servicePortInIngress = fmt.Sprint(backendID.Backend.Service.Port.Name)
}
resolvedBackendPorts := make(map[serviceBackendPortPair]interface{})
for _, servicePort := range service.Spec.Ports {
// ignore UDP ports
if servicePort.Protocol != v1.ProtocolTCP {
continue
}
// match service by either port, port name or target port
if fmt.Sprint(servicePort.Port) != servicePortInIngress &&
servicePort.Name != servicePortInIngress &&
servicePort.TargetPort.String() != servicePortInIngress {
continue
}
// if target port is not specified, use the service port as backend port
if servicePort.TargetPort.String() == "" {
// targetPort is not defined, by default targetPort == port
pair := serviceBackendPortPair{
ServicePort: Port(servicePort.Port),
BackendPort: Port(servicePort.Port),
}
resolvedBackendPorts[pair] = nil
continue
}
// if target port is an int, use it as backend port
if servicePort.TargetPort.Type == intstr.Int {
// port is defined as port number
pair := serviceBackendPortPair{
ServicePort: Port(servicePort.Port),
BackendPort: Port(servicePort.TargetPort.IntVal),
}
resolvedBackendPorts[pair] = nil
continue
}
// if target port is port name, then resolve the port number for the port name
// k8s matches service port name against endpoints port name retrieved by passing backendID service key to endpoint api.
klog.V(3).Infof("resolving port name '%s' for service '%s' and service port '%s' for Ingress '%s'", servicePort.Name, backendID.serviceKey(), serviceBackendPortToStr(backendID.Backend.Service.Port), backendID.Ingress.Name)
targetPortsResolved := c.resolvePortName(servicePort.Name, &backendID)
for targetPort := range targetPortsResolved {
pair := serviceBackendPortPair{
ServicePort: Port(servicePort.Port),
BackendPort: Port(targetPort),
}
resolvedBackendPorts[pair] = nil
}
}
backendPort := Port(65536)
if len(resolvedBackendPorts) == 0 {
e = controllererrors.NewErrorf(
controllererrors.ErrorUnableToResolveBackendPortFromServicePort,
"No port matched %s",
backendID.serviceKey())
if backendID.Backend.Service.Port.Name == "" {
backendPort = Port(backendID.Backend.Service.Port.Number)
}
} else {
var ports []string
for k := range resolvedBackendPorts {
ports = append(ports, string(k.BackendPort))
if k.BackendPort <= backendPort {
backendPort = k.BackendPort
}
}
if len(resolvedBackendPorts) > 1 {
// found more than 1 backend port for the service port which is a conflicting scenario
e = controllererrors.NewErrorf(
controllererrors.ErrorMultipleServiceBackendPortBinding,
"service %s and service port %s has more than one service-backend port binding which is not an ideal scenario, choosing the smallest service-backend port %d. Ports found %s.",
backendID.serviceKey(), serviceBackendPortToStr(backendID.Backend.Service.Port), backendPort, strings.Join(ports, ","),
)
}
}
if backendPort >= Port(65536) {
e = controllererrors.NewErrorf(
controllererrors.ErrorServiceResolvedToInvalidPort,
"service %s and service port %s was resolved to an invalid service-backend port %d, defaulting to port 80",
backendID.serviceKey(), serviceBackendPortToStr(backendID.Backend.Service.Port), backendPort,
)
backendPort = Port(80)
}
return backendPort, e
}
func (c *appGwConfigBuilder) generateHTTPSettings(backendID backendIdentifier, port Port, cbCtx *ConfigBuilderContext) n.ApplicationGatewayBackendHTTPSettings {
httpSettingsName := generateHTTPSettingsName(backendID.serviceFullName(), serviceBackendPortToStr(backendID.Backend.Service.Port), port, backendID.Ingress.Name)
httpSettings := n.ApplicationGatewayBackendHTTPSettings{
Etag: to.StringPtr("*"),
Name: &httpSettingsName,
ID: to.StringPtr(c.appGwIdentifier.HTTPSettingsID(httpSettingsName)),
ApplicationGatewayBackendHTTPSettingsPropertiesFormat: &n.ApplicationGatewayBackendHTTPSettingsPropertiesFormat{
Protocol: n.ApplicationGatewayProtocolHTTP,
Port: to.Int32Ptr(int32(port)),
// setting to default
PickHostNameFromBackendAddress: to.BoolPtr(false),
CookieBasedAffinity: n.ApplicationGatewayCookieBasedAffinityDisabled,
RequestTimeout: to.Int32Ptr(30),
},
}
_, probesMap := c.newProbesMap(cbCtx)
if probesMap[backendID] != nil {
probeName := probesMap[backendID].Name
probeID := c.appGwIdentifier.probeID(*probeName)
httpSettings.ApplicationGatewayBackendHTTPSettingsPropertiesFormat.Probe = resourceRef(probeID)
}
if pathPrefix, err := annotations.BackendPathPrefix(backendID.Ingress); err == nil {
httpSettings.Path = to.StringPtr(pathPrefix)
} else if !controllererrors.IsErrorCode(err, controllererrors.ErrorMissingAnnotation) {
c.recorder.Event(backendID.Ingress, v1.EventTypeWarning, events.ReasonInvalidAnnotation, err.Error())
}
if hostName, err := annotations.BackendHostName(backendID.Ingress); err == nil {
httpSettings.HostName = to.StringPtr(hostName)
} else if !controllererrors.IsErrorCode(err, controllererrors.ErrorMissingAnnotation) {
c.recorder.Event(backendID.Ingress, v1.EventTypeWarning, events.ReasonInvalidAnnotation, err.Error())
}
if isConnDrain, err := annotations.IsConnectionDraining(backendID.Ingress); err == nil && isConnDrain {
httpSettings.ConnectionDraining = &n.ApplicationGatewayConnectionDraining{
Enabled: to.BoolPtr(true),
}
if connDrainTimeout, err := annotations.ConnectionDrainingTimeout(backendID.Ingress); err == nil {
httpSettings.ConnectionDraining.DrainTimeoutInSec = to.Int32Ptr(connDrainTimeout)
} else {
httpSettings.ConnectionDraining.DrainTimeoutInSec = to.Int32Ptr(DefaultConnDrainTimeoutInSec)
}
} else if err != nil && !controllererrors.IsErrorCode(err, controllererrors.ErrorMissingAnnotation) {
c.recorder.Event(backendID.Ingress, v1.EventTypeWarning, events.ReasonInvalidAnnotation, err.Error())
}
if affinity, err := annotations.IsCookieBasedAffinity(backendID.Ingress); err == nil && affinity {
httpSettings.CookieBasedAffinity = n.ApplicationGatewayCookieBasedAffinityEnabled
} else if err != nil && !controllererrors.IsErrorCode(err, controllererrors.ErrorMissingAnnotation) {
c.recorder.Event(backendID.Ingress, v1.EventTypeWarning, events.ReasonInvalidAnnotation, err.Error())
}
if distinctName, err := annotations.IsCookieBasedAffinityDistinctName(backendID.Ingress); err == nil && distinctName {
httpSettings.AffinityCookieName = to.StringPtr(fmt.Sprintf("%s%s", "appgw-affinity-", backendID.serviceFullNameHash()))
} else if err != nil && !controllererrors.IsErrorCode(err, controllererrors.ErrorMissingAnnotation) {
c.recorder.Event(backendID.Ingress, v1.EventTypeWarning, events.ReasonInvalidAnnotation, err.Error())
}
if reqTimeout, err := annotations.RequestTimeout(backendID.Ingress); err == nil {
httpSettings.RequestTimeout = to.Int32Ptr(reqTimeout)
} else if !controllererrors.IsErrorCode(err, controllererrors.ErrorMissingAnnotation) {
c.recorder.Event(backendID.Ingress, v1.EventTypeWarning, events.ReasonInvalidAnnotation, err.Error())
}
// when ingress is defined with backend at port 443 but without annotation backend-protocol set to https.
if int32(port) == 443 {
httpSettings.Protocol = n.ApplicationGatewayProtocolHTTPS
}
// backend protocol take precedence over port
backendProtocol, err := annotations.BackendProtocol(backendID.Ingress)
if err == nil && backendProtocol == annotations.HTTPS {
httpSettings.Protocol = n.ApplicationGatewayProtocolHTTPS
} else if err == nil && backendProtocol == annotations.HTTP {
httpSettings.Protocol = n.ApplicationGatewayProtocolHTTP
} else if err != nil && !controllererrors.IsErrorCode(err, controllererrors.ErrorMissingAnnotation) {
c.recorder.Event(backendID.Ingress, v1.EventTypeWarning, events.ReasonInvalidAnnotation, err.Error())
}
if trustedRootCertificates, err := annotations.GetAppGwTrustedRootCertificate(backendID.Ingress); err == nil {
certificateNames := strings.TrimRight(trustedRootCertificates, ",")
certificateNameList := strings.Split(certificateNames, ",")
var certs []n.SubResource
for _, certName := range certificateNameList {
trustCertID := c.appGwIdentifier.trustedRootCertificateID(certName)
certs = append(certs, *resourceRef(trustCertID))
}
httpSettings.TrustedRootCertificates = &certs
klog.V(3).Infof("Found trusted root certificate(s): %s from ingress: %s/%s", certificateNames, backendID.Ingress.Namespace, backendID.Ingress.Name)
} else if err != nil && !controllererrors.IsErrorCode(err, controllererrors.ErrorMissingAnnotation) {
c.recorder.Event(backendID.Ingress, v1.EventTypeWarning, events.ReasonInvalidAnnotation, err.Error())
}
// To use an HTTP setting with a trusted root certificate, we must either override with a specific domain name or choose "Pick host name from backend target".
if httpSettings.TrustedRootCertificates != nil {
if httpSettings.Protocol == n.ApplicationGatewayProtocolHTTPS && len(*httpSettings.TrustedRootCertificates) > 0 {
if httpSettings.HostName != nil && len(*httpSettings.HostName) > 0 {
httpSettings.PickHostNameFromBackendAddress = to.BoolPtr(false)
} else {
httpSettings.PickHostNameFromBackendAddress = to.BoolPtr(true)
}
}
}
return httpSettings
}