sigs.k8s.io/cluster-api-provider-azure@v1.14.3/azure/services/securitygroups/spec.go (about)

     1  /*
     2  Copyright 2022 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package securitygroups
    18  
    19  import (
    20  	"context"
    21  	"strings"
    22  
    23  	"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v4"
    24  	"github.com/pkg/errors"
    25  	"k8s.io/utils/ptr"
    26  	infrav1 "sigs.k8s.io/cluster-api-provider-azure/api/v1beta1"
    27  	"sigs.k8s.io/cluster-api-provider-azure/azure/converters"
    28  )
    29  
    30  // NSGSpec defines the specification for a security group.
    31  type NSGSpec struct {
    32  	Name                     string
    33  	SecurityRules            infrav1.SecurityRules
    34  	Location                 string
    35  	ClusterName              string
    36  	ResourceGroup            string
    37  	AdditionalTags           infrav1.Tags
    38  	LastAppliedSecurityRules map[string]interface{}
    39  }
    40  
    41  // ResourceName returns the name of the security group.
    42  func (s *NSGSpec) ResourceName() string {
    43  	return s.Name
    44  }
    45  
    46  // ResourceGroupName returns the name of the resource group.
    47  func (s *NSGSpec) ResourceGroupName() string {
    48  	return s.ResourceGroup
    49  }
    50  
    51  // OwnerResourceName is a no-op for security groups.
    52  func (s *NSGSpec) OwnerResourceName() string {
    53  	return ""
    54  }
    55  
    56  // Parameters returns the parameters for the security group.
    57  func (s *NSGSpec) Parameters(ctx context.Context, existing interface{}) (interface{}, error) {
    58  	securityRules := make([]*armnetwork.SecurityRule, 0)
    59  	newAnnotation := map[string]string{}
    60  	var etag *string
    61  
    62  	if existing != nil {
    63  		existingNSG, ok := existing.(armnetwork.SecurityGroup)
    64  		if !ok {
    65  			return nil, errors.Errorf("%T is not a network.SecurityGroup", existing)
    66  		}
    67  		// security group already exists
    68  		// We append the existing NSG etag to the header to ensure we only apply the updates if the NSG has not been modified.
    69  		etag = existingNSG.Etag
    70  		// Check if the expected rules are present
    71  		update := false
    72  
    73  		for _, rule := range s.SecurityRules {
    74  			sdkRule := converters.SecurityRuleToSDK(rule)
    75  			if !ruleExists(existingNSG.Properties.SecurityRules, sdkRule) {
    76  				update = true
    77  				securityRules = append(securityRules, sdkRule)
    78  			}
    79  			newAnnotation[rule.Name] = rule.Description
    80  		}
    81  
    82  		for _, oldRule := range existingNSG.Properties.SecurityRules {
    83  			_, tracked := s.LastAppliedSecurityRules[*oldRule.Name]
    84  			// If rule is owned by CAPZ and applied last, and not found in the new rules, then it has been deleted
    85  			if _, ok := newAnnotation[*oldRule.Name]; !ok && tracked {
    86  				// Rule has been deleted
    87  				update = true
    88  				continue
    89  			}
    90  
    91  			// Add previous rules that haven't been deleted
    92  			securityRules = append(securityRules, oldRule)
    93  		}
    94  
    95  		if !update {
    96  			// Skip update for NSG as the required default rules are present
    97  			return nil, nil
    98  		}
    99  	} else {
   100  		// new security group
   101  		for _, rule := range s.SecurityRules {
   102  			securityRules = append(securityRules, converters.SecurityRuleToSDK(rule))
   103  		}
   104  	}
   105  
   106  	return armnetwork.SecurityGroup{
   107  		Location: ptr.To(s.Location),
   108  		Properties: &armnetwork.SecurityGroupPropertiesFormat{
   109  			SecurityRules: securityRules,
   110  		},
   111  		Etag: etag,
   112  		Tags: converters.TagsToMap(infrav1.Build(infrav1.BuildParams{
   113  			ClusterName: s.ClusterName,
   114  			Lifecycle:   infrav1.ResourceLifecycleOwned,
   115  			Name:        ptr.To(s.Name),
   116  			Additional:  s.AdditionalTags,
   117  		})),
   118  	}, nil
   119  }
   120  
   121  // TODO: review this logic and make sure it is what we want. It seems incorrect to skip rules that don't have a certain protocol, etc.
   122  func ruleExists(rules []*armnetwork.SecurityRule, rule *armnetwork.SecurityRule) bool {
   123  	for _, existingRule := range rules {
   124  		if !strings.EqualFold(ptr.Deref(existingRule.Name, ""), ptr.Deref(rule.Name, "")) {
   125  			continue
   126  		}
   127  		if !strings.EqualFold(ptr.Deref(existingRule.Properties.DestinationPortRange, ""), ptr.Deref(rule.Properties.DestinationPortRange, "")) {
   128  			continue
   129  		}
   130  		if ptr.Deref(existingRule.Properties.Protocol, "") != armnetwork.SecurityRuleProtocolTCP &&
   131  			ptr.Deref(existingRule.Properties.Access, "") != armnetwork.SecurityRuleAccessAllow &&
   132  			ptr.Deref(existingRule.Properties.Direction, "") != armnetwork.SecurityRuleDirectionInbound {
   133  			continue
   134  		}
   135  		if !strings.EqualFold(ptr.Deref(existingRule.Properties.SourcePortRange, ""), "*") &&
   136  			!strings.EqualFold(ptr.Deref(existingRule.Properties.SourceAddressPrefix, ""), "*") &&
   137  			!strings.EqualFold(ptr.Deref(existingRule.Properties.DestinationAddressPrefix, ""), "*") {
   138  			continue
   139  		}
   140  		return true
   141  	}
   142  	return false
   143  }