Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions api/v1alpha1/trafficmanagerprofile_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,17 @@ type TrafficManagerProfileSpec struct {
MonitorConfig *MonitorConfig `json:"monitorConfig,omitempty"`
}

// MonitorConfigCustomHeader defines a custom header for endpoint monitoring.
type MonitorConfigCustomHeader struct {
// Name of the header
// +kubebuilder:validation:MinLength=1
Name string `json:"name"`

// Value of the header
// +kubebuilder:validation:MinLength=1
Value string `json:"value"`
}

// MonitorConfig defines the endpoint monitoring settings of the Traffic Manager profile.
// https://learn.microsoft.com/en-us/azure/traffic-manager/traffic-manager-monitoring
type MonitorConfig struct {
Expand Down Expand Up @@ -76,6 +87,13 @@ type MonitorConfig struct {
// +kubebuilder:default="HTTP"
Protocol *TrafficManagerMonitorProtocol `json:"protocol,omitempty"`

// Custom headers used for probing endpoints, such as Host headers.
// +optional
// +listType=map
// +listMapKey=name
// +kubebuilder:validation:MaxItems=8
CustomHeaders []MonitorConfigCustomHeader `json:"customHeaders,omitempty"`

// The monitor timeout for endpoints in this profile. This is the time that Traffic Manager allows endpoints in this profile
// to response to the health check.
// +optional
Expand Down
22 changes: 21 additions & 1 deletion api/v1alpha1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

18 changes: 18 additions & 0 deletions api/v1beta1/trafficmanagerprofile_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,17 @@ type TrafficManagerProfileSpec struct {
MonitorConfig *MonitorConfig `json:"monitorConfig,omitempty"`
}

// MonitorConfigCustomHeader defines a custom header for endpoint monitoring.
type MonitorConfigCustomHeader struct {
// Name of the header
// +kubebuilder:validation:MinLength=1
Name string `json:"name"`

// Value of the header
// +kubebuilder:validation:MinLength=1
Value string `json:"value"`
}

// MonitorConfig defines the endpoint monitoring settings of the Traffic Manager profile.
// https://learn.microsoft.com/en-us/azure/traffic-manager/traffic-manager-monitoring
// +kubebuilder:validation:XValidation:rule="has(self.intervalInSeconds) && self.intervalInSeconds == 30 ? (!has(self.timeoutInSeconds) || (self.timeoutInSeconds >= 5 && self.timeoutInSeconds <= 10)) : true",message="timeoutInSeconds must be between 5 and 10 when intervalInSeconds is 30"
Expand Down Expand Up @@ -84,6 +95,13 @@ type MonitorConfig struct {
// +kubebuilder:default="HTTP"
Protocol *TrafficManagerMonitorProtocol `json:"protocol,omitempty"`

// Custom headers used for probing endpoints, such as Host headers.
// +optional
// +listType=map
// +listMapKey=name
// +kubebuilder:validation:MaxItems=8
CustomHeaders []MonitorConfigCustomHeader `json:"customHeaders,omitempty"`

// The monitor timeout for endpoints in this profile. This is the time that Traffic Manager allows endpoints in this profile
// to response to the health check.
// +optional
Expand Down
22 changes: 21 additions & 1 deletion api/v1beta1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,30 @@ spec:
description: The endpoint monitoring settings of the Traffic Manager
profile.
properties:
customHeaders:
description: Custom headers used for probing endpoints, such as
Host headers.
items:
description: MonitorConfigCustomHeader defines a custom header
for endpoint monitoring.
properties:
name:
description: Name of the header
minLength: 1
type: string
value:
description: Value of the header
minLength: 1
type: string
required:
- name
- value
type: object
maxItems: 8
type: array
x-kubernetes-list-map-keys:
- name
x-kubernetes-list-type: map
intervalInSeconds:
default: 30
description: |-
Expand Down Expand Up @@ -253,6 +277,30 @@ spec:
description: The endpoint monitoring settings of the Traffic Manager
profile.
properties:
customHeaders:
description: Custom headers used for probing endpoints, such as
Host headers.
items:
description: MonitorConfigCustomHeader defines a custom header
for endpoint monitoring.
properties:
name:
description: Name of the header
minLength: 1
type: string
value:
description: Value of the header
minLength: 1
type: string
required:
- name
- value
type: object
maxItems: 8
type: array
x-kubernetes-list-map-keys:
- name
x-kubernetes-list-type: map
intervalInSeconds:
default: 30
description: |-
Expand Down
102 changes: 85 additions & 17 deletions pkg/controllers/hub/trafficmanagerprofile/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,15 @@ import (
"context"
"errors"
"fmt"
"sort"
"strconv"
"time"

"github.com/Azure/azure-sdk-for-go/sdk/azcore"
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/trafficmanager/armtrafficmanager"
"github.com/prometheus/client_golang/prometheus"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/equality"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
Expand Down Expand Up @@ -242,41 +244,90 @@ func (r *Reconciler) handleUpdate(ctx context.Context, profile *fleetnetv1beta1.
// by ignoring others.
// The desired profile is built by the controllers and all the required fields should not be nil.
func equalAzureTrafficManagerProfile(current, desired armtrafficmanager.Profile) bool {
// location and dnsConfig (excluding TTL) is immutable
if current.Properties == nil || current.Properties.MonitorConfig == nil || current.Properties.ProfileStatus == nil || current.Properties.TrafficRoutingMethod == nil || current.Properties.DNSConfig == nil {
// Check required properties
if !hasRequiredProperties(current) {
return false
}

if current.Properties.MonitorConfig.IntervalInSeconds == nil || current.Properties.MonitorConfig.Path == nil ||
current.Properties.MonitorConfig.Port == nil || current.Properties.MonitorConfig.Protocol == nil ||
current.Properties.MonitorConfig.TimeoutInSeconds == nil || current.Properties.MonitorConfig.ToleratedNumberOfFailures == nil {
// Compare monitor config
if !equalMonitorConfig(current.Properties.MonitorConfig, desired.Properties.MonitorConfig) {
return false
}

if *current.Properties.MonitorConfig.IntervalInSeconds != *desired.Properties.MonitorConfig.IntervalInSeconds ||
*current.Properties.MonitorConfig.Path != *desired.Properties.MonitorConfig.Path ||
*current.Properties.MonitorConfig.Port != *desired.Properties.MonitorConfig.Port ||
*current.Properties.MonitorConfig.Protocol != *desired.Properties.MonitorConfig.Protocol ||
*current.Properties.MonitorConfig.TimeoutInSeconds != *desired.Properties.MonitorConfig.TimeoutInSeconds ||
*current.Properties.MonitorConfig.ToleratedNumberOfFailures != *desired.Properties.MonitorConfig.ToleratedNumberOfFailures {
if *current.Properties.ProfileStatus != *desired.Properties.ProfileStatus {
return false
}

if *current.Properties.ProfileStatus != *desired.Properties.ProfileStatus || *current.Properties.TrafficRoutingMethod != *desired.Properties.TrafficRoutingMethod {
if *current.Properties.TrafficRoutingMethod != *desired.Properties.TrafficRoutingMethod {
return false
}

if current.Properties.DNSConfig.TTL == nil || *current.Properties.DNSConfig.TTL != *desired.Properties.DNSConfig.TTL {
return false
}

if current.Tags == nil {
// Compare tags
if !desiredTagsExistInCurrentTags(current.Tags, desired.Tags) {
return false
}

return true
}

// hasRequiredProperties checks if the profile has all required properties.
func hasRequiredProperties(profile armtrafficmanager.Profile) bool {
return profile.Properties != nil &&
profile.Properties.MonitorConfig != nil &&
profile.Properties.ProfileStatus != nil &&
profile.Properties.TrafficRoutingMethod != nil &&
profile.Properties.DNSConfig != nil
}

// equalMonitorConfig compares the monitoring configuration.
func equalMonitorConfig(current, desired *armtrafficmanager.MonitorConfig) bool {
if current.IntervalInSeconds == nil || current.Path == nil ||
current.Port == nil || current.Protocol == nil ||
current.TimeoutInSeconds == nil || current.ToleratedNumberOfFailures == nil {
return false
}

// Check basic monitor config fields
if *current.IntervalInSeconds != *desired.IntervalInSeconds ||
*current.Path != *desired.Path ||
*current.Port != *desired.Port ||
*current.Protocol != *desired.Protocol ||
*current.TimeoutInSeconds != *desired.TimeoutInSeconds ||
*current.ToleratedNumberOfFailures != *desired.ToleratedNumberOfFailures {
return false
}

for key, value := range desired.Tags {
currentValue := current.Tags[key]
if (value == nil && currentValue != nil) || (value != nil && currentValue == nil) || (currentValue == nil || *currentValue != *value) {
// Also check custom headers
return equalMonitorConfigWithCustomHeaders(current.CustomHeaders, desired.CustomHeaders)
}

func equalMonitorConfigWithCustomHeaders(current, desired []*armtrafficmanager.MonitorConfigCustomHeadersItem) bool {
// Sort the slices to ensure the order does not affect the comparison.
sort.Slice(current, func(i, j int) bool {
return *current[i].Name < *current[j].Name
})
sort.Slice(desired, func(i, j int) bool {
return *desired[i].Name < *desired[j].Name
})
// equality.Semantic.DeepEqual cannot compare the slices without the order.
return equality.Semantic.DeepEqual(current, desired)
}

// desiredTagsExistInCurrentTags checks if all desired tags exist in current tags with the same value.
func desiredTagsExistInCurrentTags(currentTags, desiredTags map[string]*string) bool {
if currentTags == nil {
return false
}

for key, value := range desiredTags {
currentValue := currentTags[key]
if (value == nil && currentValue != nil) ||
(value != nil && currentValue == nil) ||
(currentValue == nil || *currentValue != *value) {
return false
}
}
Expand Down Expand Up @@ -349,7 +400,9 @@ func (r *Reconciler) updateProfileStatus(ctx context.Context, profile *fleetnetv
func generateAzureTrafficManagerProfile(profile *fleetnetv1beta1.TrafficManagerProfile) armtrafficmanager.Profile {
mc := profile.Spec.MonitorConfig
namespacedName := types.NamespacedName{Name: profile.Name, Namespace: profile.Namespace}
return armtrafficmanager.Profile{

// Build the Azure Traffic Manager profile
tmProfile := armtrafficmanager.Profile{
Location: ptr.To("global"),
Properties: &armtrafficmanager.ProfileProperties{
DNSConfig: &armtrafficmanager.DNSConfig{
Expand All @@ -372,6 +425,20 @@ func generateAzureTrafficManagerProfile(profile *fleetnetv1beta1.TrafficManagerP
objectmeta.AzureTrafficManagerProfileTagKey: ptr.To(namespacedName.String()),
},
}

// Add custom headers if specified
if len(mc.CustomHeaders) > 0 {
customHeaders := make([]*armtrafficmanager.MonitorConfigCustomHeadersItem, 0, len(mc.CustomHeaders))
for _, header := range mc.CustomHeaders {
customHeaders = append(customHeaders, &armtrafficmanager.MonitorConfigCustomHeadersItem{
Name: ptr.To(header.Name),
Value: ptr.To(header.Value),
})
}
tmProfile.Properties.MonitorConfig.CustomHeaders = customHeaders
}

return tmProfile
}

// buildAzureTrafficManagerProfileRequest assumes desired is always valid.
Expand All @@ -391,6 +458,7 @@ func buildAzureTrafficManagerProfileRequest(current, desired armtrafficmanager.P
current.Properties.MonitorConfig.Protocol = desired.Properties.MonitorConfig.Protocol
current.Properties.MonitorConfig.TimeoutInSeconds = desired.Properties.MonitorConfig.TimeoutInSeconds
current.Properties.MonitorConfig.ToleratedNumberOfFailures = desired.Properties.MonitorConfig.ToleratedNumberOfFailures
current.Properties.MonitorConfig.CustomHeaders = desired.Properties.MonitorConfig.CustomHeaders
}
current.Properties.ProfileStatus = desired.Properties.ProfileStatus
current.Properties.TrafficRoutingMethod = desired.Properties.TrafficRoutingMethod
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,13 @@ var _ = Describe("Test TrafficManagerProfile Controller", func() {
It("Updating trafficManagerProfile spec to valid and validating trafficManagerProfile status", func() {
profile.Spec.MonitorConfig.IntervalInSeconds = ptr.To[int64](30)
profile.Spec.MonitorConfig.TimeoutInSeconds = ptr.To[int64](10)
// add custom headers
profile.Spec.MonitorConfig.CustomHeaders = []fleetnetv1beta1.MonitorConfigCustomHeader{
{
Name: "Host",
Value: "myapp.example.com",
},
}
Expect(k8sClient.Update(ctx, profile)).Should(Succeed(), "failed to update the trafficManagerProfile")

want := fleetnetv1beta1.TrafficManagerProfile{
Expand All @@ -210,7 +217,8 @@ var _ = Describe("Test TrafficManagerProfile Controller", func() {
validator.ValidateTrafficManagerProfile(ctx, k8sClient, &want, timeout)

By("By validating the status metrics")
// It overwrites the previous one as they have the same condition.
// Generation is updated.
wantMetrics = append(wantMetrics, generateMetrics(profile, want.Status.Conditions[0]))
validateTrafficManagerProfileMetricsEmitted(wantMetrics...)

By("By validating events")
Expand Down
Loading
Loading