diff --git a/api/v1beta1/awscluster_conversion.go b/api/v1beta1/awscluster_conversion.go index 680c3f5f6b..616a585329 100644 --- a/api/v1beta1/awscluster_conversion.go +++ b/api/v1beta1/awscluster_conversion.go @@ -50,6 +50,10 @@ func (src *AWSCluster) ConvertTo(dstRaw conversion.Hub) error { } dst.Spec.Partition = restored.Spec.Partition + for role, sg := range restored.Status.Network.SecurityGroups { + dst.Status.Network.SecurityGroups[role] = sg + } + return nil } @@ -70,6 +74,7 @@ func restoreControlPlaneLoadBalancer(restored, dst *infrav2.AWSLoadBalancerSpec) dst.LoadBalancerType = restored.LoadBalancerType dst.DisableHostsRewrite = restored.DisableHostsRewrite dst.PreserveClientIP = restored.PreserveClientIP + dst.AdditionalIngressRules = restored.AdditionalIngressRules } // ConvertFrom converts the v1beta1 AWSCluster receiver to a v1beta1 AWSCluster. diff --git a/api/v1beta1/conversion.go b/api/v1beta1/conversion.go index a8cf22f98b..2f7c76b006 100644 --- a/api/v1beta1/conversion.go +++ b/api/v1beta1/conversion.go @@ -78,3 +78,7 @@ func Convert_v1beta2_LoadBalancer_To_v1beta1_ClassicELB(in *v1beta2.LoadBalancer out.SubnetIDs = in.SubnetIDs return nil } + +func Convert_v1beta2_IngressRule_To_v1beta1_IngressRule(in *v1beta2.IngressRule, out *IngressRule, s conversion.Scope) error { + return autoConvert_v1beta2_IngressRule_To_v1beta1_IngressRule(in, out, s) +} diff --git a/api/v1beta1/zz_generated.conversion.go b/api/v1beta1/zz_generated.conversion.go index f6dd46b9b4..c04b82ab03 100644 --- a/api/v1beta1/zz_generated.conversion.go +++ b/api/v1beta1/zz_generated.conversion.go @@ -460,11 +460,6 @@ func RegisterConversions(s *runtime.Scheme) error { }); err != nil { return err } - if err := s.AddGeneratedConversionFunc((*v1beta2.IngressRule)(nil), (*IngressRule)(nil), func(a, b interface{}, scope conversion.Scope) error { - return Convert_v1beta2_IngressRule_To_v1beta1_IngressRule(a.(*v1beta2.IngressRule), b.(*IngressRule), scope) - }); err != nil { - return err - } if err := s.AddGeneratedConversionFunc((*Instance)(nil), (*v1beta2.Instance)(nil), func(a, b interface{}, scope conversion.Scope) error { return Convert_v1beta1_Instance_To_v1beta2_Instance(a.(*Instance), b.(*v1beta2.Instance), scope) }); err != nil { @@ -585,6 +580,11 @@ func RegisterConversions(s *runtime.Scheme) error { }); err != nil { return err } + if err := s.AddConversionFunc((*v1beta2.IngressRule)(nil), (*IngressRule)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1beta2_IngressRule_To_v1beta1_IngressRule(a.(*v1beta2.IngressRule), b.(*IngressRule), scope) + }); err != nil { + return err + } if err := s.AddConversionFunc((*v1beta2.Instance)(nil), (*Instance)(nil), func(a, b interface{}, scope conversion.Scope) error { return Convert_v1beta2_Instance_To_v1beta1_Instance(a.(*v1beta2.Instance), b.(*Instance), scope) }); err != nil { @@ -1219,6 +1219,7 @@ func autoConvert_v1beta2_AWSLoadBalancerSpec_To_v1beta1_AWSLoadBalancerSpec(in * out.Subnets = *(*[]string)(unsafe.Pointer(&in.Subnets)) out.HealthCheckProtocol = (*ClassicELBProtocol)(unsafe.Pointer(in.HealthCheckProtocol)) out.AdditionalSecurityGroups = *(*[]string)(unsafe.Pointer(&in.AdditionalSecurityGroups)) + // WARNING: in.AdditionalIngressRules requires manual conversion: does not exist in peer-type // WARNING: in.LoadBalancerType requires manual conversion: does not exist in peer-type // WARNING: in.DisableHostsRewrite requires manual conversion: does not exist in peer-type // WARNING: in.PreserveClientIP requires manual conversion: does not exist in peer-type @@ -1936,14 +1937,10 @@ func autoConvert_v1beta2_IngressRule_To_v1beta1_IngressRule(in *v1beta2.IngressR out.CidrBlocks = *(*[]string)(unsafe.Pointer(&in.CidrBlocks)) out.IPv6CidrBlocks = *(*[]string)(unsafe.Pointer(&in.IPv6CidrBlocks)) out.SourceSecurityGroupIDs = *(*[]string)(unsafe.Pointer(&in.SourceSecurityGroupIDs)) + // WARNING: in.SourceSecurityGroupRoles requires manual conversion: does not exist in peer-type return nil } -// Convert_v1beta2_IngressRule_To_v1beta1_IngressRule is an autogenerated conversion function. -func Convert_v1beta2_IngressRule_To_v1beta1_IngressRule(in *v1beta2.IngressRule, out *IngressRule, s conversion.Scope) error { - return autoConvert_v1beta2_IngressRule_To_v1beta1_IngressRule(in, out, s) -} - func autoConvert_v1beta1_Instance_To_v1beta2_Instance(in *Instance, out *v1beta2.Instance, s conversion.Scope) error { out.ID = in.ID out.State = v1beta2.InstanceState(in.State) @@ -2034,7 +2031,19 @@ func Convert_v1beta2_NetworkSpec_To_v1beta1_NetworkSpec(in *v1beta2.NetworkSpec, } func autoConvert_v1beta1_NetworkStatus_To_v1beta2_NetworkStatus(in *NetworkStatus, out *v1beta2.NetworkStatus, s conversion.Scope) error { - out.SecurityGroups = *(*map[v1beta2.SecurityGroupRole]v1beta2.SecurityGroup)(unsafe.Pointer(&in.SecurityGroups)) + if in.SecurityGroups != nil { + in, out := &in.SecurityGroups, &out.SecurityGroups + *out = make(map[v1beta2.SecurityGroupRole]v1beta2.SecurityGroup, len(*in)) + for key, val := range *in { + newVal := new(v1beta2.SecurityGroup) + if err := Convert_v1beta1_SecurityGroup_To_v1beta2_SecurityGroup(&val, newVal, s); err != nil { + return err + } + (*out)[v1beta2.SecurityGroupRole(key)] = *newVal + } + } else { + out.SecurityGroups = nil + } if err := Convert_v1beta1_ClassicELB_To_v1beta2_LoadBalancer(&in.APIServerELB, &out.APIServerELB, s); err != nil { return err } @@ -2047,7 +2056,19 @@ func Convert_v1beta1_NetworkStatus_To_v1beta2_NetworkStatus(in *NetworkStatus, o } func autoConvert_v1beta2_NetworkStatus_To_v1beta1_NetworkStatus(in *v1beta2.NetworkStatus, out *NetworkStatus, s conversion.Scope) error { - out.SecurityGroups = *(*map[SecurityGroupRole]SecurityGroup)(unsafe.Pointer(&in.SecurityGroups)) + if in.SecurityGroups != nil { + in, out := &in.SecurityGroups, &out.SecurityGroups + *out = make(map[SecurityGroupRole]SecurityGroup, len(*in)) + for key, val := range *in { + newVal := new(SecurityGroup) + if err := Convert_v1beta2_SecurityGroup_To_v1beta1_SecurityGroup(&val, newVal, s); err != nil { + return err + } + (*out)[SecurityGroupRole(key)] = *newVal + } + } else { + out.SecurityGroups = nil + } if err := Convert_v1beta2_LoadBalancer_To_v1beta1_ClassicELB(&in.APIServerELB, &out.APIServerELB, s); err != nil { return err } @@ -2101,7 +2122,17 @@ func Convert_v1beta2_S3Bucket_To_v1beta1_S3Bucket(in *v1beta2.S3Bucket, out *S3B func autoConvert_v1beta1_SecurityGroup_To_v1beta2_SecurityGroup(in *SecurityGroup, out *v1beta2.SecurityGroup, s conversion.Scope) error { out.ID = in.ID out.Name = in.Name - out.IngressRules = *(*v1beta2.IngressRules)(unsafe.Pointer(&in.IngressRules)) + if in.IngressRules != nil { + in, out := &in.IngressRules, &out.IngressRules + *out = make(v1beta2.IngressRules, len(*in)) + for i := range *in { + if err := Convert_v1beta1_IngressRule_To_v1beta2_IngressRule(&(*in)[i], &(*out)[i], s); err != nil { + return err + } + } + } else { + out.IngressRules = nil + } out.Tags = *(*v1beta2.Tags)(unsafe.Pointer(&in.Tags)) return nil } @@ -2114,7 +2145,17 @@ func Convert_v1beta1_SecurityGroup_To_v1beta2_SecurityGroup(in *SecurityGroup, o func autoConvert_v1beta2_SecurityGroup_To_v1beta1_SecurityGroup(in *v1beta2.SecurityGroup, out *SecurityGroup, s conversion.Scope) error { out.ID = in.ID out.Name = in.Name - out.IngressRules = *(*IngressRules)(unsafe.Pointer(&in.IngressRules)) + if in.IngressRules != nil { + in, out := &in.IngressRules, &out.IngressRules + *out = make(IngressRules, len(*in)) + for i := range *in { + if err := Convert_v1beta2_IngressRule_To_v1beta1_IngressRule(&(*in)[i], &(*out)[i], s); err != nil { + return err + } + } + } else { + out.IngressRules = nil + } out.Tags = *(*Tags)(unsafe.Pointer(&in.Tags)) return nil } diff --git a/api/v1beta2/awscluster_types.go b/api/v1beta2/awscluster_types.go index dfa3292578..203dafcc05 100644 --- a/api/v1beta2/awscluster_types.go +++ b/api/v1beta2/awscluster_types.go @@ -208,6 +208,11 @@ type AWSLoadBalancerSpec struct { // +optional AdditionalSecurityGroups []string `json:"additionalSecurityGroups,omitempty"` + // AdditionalIngressRules sets the additional ingress rules for the control plane load balancer. If no source security group ids are specified, the + // default control plane security group will be used. + // +optional + AdditionalIngressRules []IngressRule `json:"additionalIngressRules,omitempty"` + // LoadBalancerType sets the type for a load balancer. The default type is classic. // +kubebuilder:default=classic // +kubebuilder:validation:Enum:=classic;elb;alb;nlb diff --git a/api/v1beta2/awscluster_webhook.go b/api/v1beta2/awscluster_webhook.go index 089c9242af..0c7831ef53 100644 --- a/api/v1beta2/awscluster_webhook.go +++ b/api/v1beta2/awscluster_webhook.go @@ -56,6 +56,7 @@ func (r *AWSCluster) ValidateCreate() error { allErrs = append(allErrs, r.Spec.AdditionalTags.Validate()...) allErrs = append(allErrs, r.Spec.S3Bucket.Validate()...) allErrs = append(allErrs, r.validateNetwork()...) + allErrs = append(allErrs, r.validateAdditionalIngressRules()...) return aggregateObjErrors(r.GroupVersionKind().GroupKind(), r.Name, allErrs) } @@ -190,3 +191,19 @@ func (r *AWSCluster) validateNetwork() field.ErrorList { } return allErrs } + +func (r *AWSCluster) validateAdditionalIngressRules() field.ErrorList { + var allErrs field.ErrorList + + if r.Spec.ControlPlaneLoadBalancer == nil { + return allErrs + } + + for _, rule := range r.Spec.ControlPlaneLoadBalancer.AdditionalIngressRules { + if (rule.CidrBlocks != nil || rule.IPv6CidrBlocks != nil) && (rule.SourceSecurityGroupIDs != nil || rule.SourceSecurityGroupRoles != nil) { + allErrs = append(allErrs, field.Invalid(field.NewPath("additionalIngressRules"), r.Spec.ControlPlaneLoadBalancer.AdditionalIngressRules, "CIDR blocks and security group IDs or security group roles cannot be used together")) + } + } + + return allErrs +} diff --git a/api/v1beta2/awscluster_webhook_test.go b/api/v1beta2/awscluster_webhook_test.go index e1d123e8aa..30b20c16f0 100644 --- a/api/v1beta2/awscluster_webhook_test.go +++ b/api/v1beta2/awscluster_webhook_test.go @@ -251,6 +251,90 @@ func TestAWSClusterValidateCreate(t *testing.T) { }, wantErr: true, }, + { + name: "rejects additional ingress rules with cidr block and source security group id", + cluster: &AWSCluster{ + Spec: AWSClusterSpec{ + ControlPlaneLoadBalancer: &AWSLoadBalancerSpec{ + AdditionalIngressRules: []IngressRule{ + { + Protocol: SecurityGroupProtocolTCP, + CidrBlocks: []string{"test"}, + SourceSecurityGroupIDs: []string{"test"}, + }, + }, + }, + }, + }, + wantErr: true, + }, + { + name: "rejects additional ingress rules with cidr block and source security group id and role", + cluster: &AWSCluster{ + Spec: AWSClusterSpec{ + ControlPlaneLoadBalancer: &AWSLoadBalancerSpec{ + AdditionalIngressRules: []IngressRule{ + { + Protocol: SecurityGroupProtocolTCP, + IPv6CidrBlocks: []string{"test"}, + SourceSecurityGroupIDs: []string{"test"}, + SourceSecurityGroupRoles: []SecurityGroupRole{SecurityGroupBastion}, + }, + }, + }, + }, + }, + wantErr: true, + }, + { + name: "accepts additional ingress rules with cidr block", + cluster: &AWSCluster{ + Spec: AWSClusterSpec{ + ControlPlaneLoadBalancer: &AWSLoadBalancerSpec{ + AdditionalIngressRules: []IngressRule{ + { + Protocol: SecurityGroupProtocolTCP, + CidrBlocks: []string{"test"}, + }, + }, + }, + }, + }, + wantErr: false, + }, + { + name: "accepts additional ingress rules with source security group role", + cluster: &AWSCluster{ + Spec: AWSClusterSpec{ + ControlPlaneLoadBalancer: &AWSLoadBalancerSpec{ + AdditionalIngressRules: []IngressRule{ + { + Protocol: SecurityGroupProtocolTCP, + SourceSecurityGroupRoles: []SecurityGroupRole{SecurityGroupBastion}, + }, + }, + }, + }, + }, + wantErr: false, + }, + { + name: "accepts additional ingress rules with source security group id and role", + cluster: &AWSCluster{ + Spec: AWSClusterSpec{ + ControlPlaneLoadBalancer: &AWSLoadBalancerSpec{ + AdditionalIngressRules: []IngressRule{ + { + Protocol: SecurityGroupProtocolTCP, + SourceSecurityGroupIDs: []string{"test"}, + SourceSecurityGroupRoles: []SecurityGroupRole{SecurityGroupBastion}, + }, + }, + }, + }, + }, + wantErr: false, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/api/v1beta2/network_types.go b/api/v1beta2/network_types.go index 7e4a6b46b1..48e1dc6b55 100644 --- a/api/v1beta2/network_types.go +++ b/api/v1beta2/network_types.go @@ -462,6 +462,7 @@ type RouteTable struct { } // SecurityGroupRole defines the unique role of a security group. +// +kubebuilder:validation:Enum=bastion;node;controlplane;apiserver-lb;lb;node-eks-additional type SecurityGroupRole string var ( @@ -530,10 +531,15 @@ var ( // IngressRule defines an AWS ingress rule for security groups. type IngressRule struct { - Description string `json:"description"` - Protocol SecurityGroupProtocol `json:"protocol"` - FromPort int64 `json:"fromPort"` - ToPort int64 `json:"toPort"` + // Description provides extended information about the ingress rule. + Description string `json:"description"` + // Protocol is the protocol for the ingress rule. Accepted values are "-1" (all), "4" (IP in IP),"tcp", "udp", "icmp", and "58" (ICMPv6). + // +kubebuilder:validation:Enum="-1";"4";tcp;udp;icmp;"58" + Protocol SecurityGroupProtocol `json:"protocol"` + // FromPort is the start of port range. + FromPort int64 `json:"fromPort"` + // ToPort is the end of port range. + ToPort int64 `json:"toPort"` // List of CIDR blocks to allow access from. Cannot be specified with SourceSecurityGroupID. // +optional @@ -546,6 +552,11 @@ type IngressRule struct { // The security group id to allow access from. Cannot be specified with CidrBlocks. // +optional SourceSecurityGroupIDs []string `json:"sourceSecurityGroupIds,omitempty"` + + // The security group role to allow access from. Cannot be specified with CidrBlocks. + // The field will be combined with source security group IDs if specified. + // +optional + SourceSecurityGroupRoles []SecurityGroupRole `json:"sourceSecurityGroupRoles,omitempty"` } // String returns a string representation of the ingress rule. diff --git a/api/v1beta2/zz_generated.deepcopy.go b/api/v1beta2/zz_generated.deepcopy.go index 111a21f199..9d14c7a8cf 100644 --- a/api/v1beta2/zz_generated.deepcopy.go +++ b/api/v1beta2/zz_generated.deepcopy.go @@ -574,6 +574,13 @@ func (in *AWSLoadBalancerSpec) DeepCopyInto(out *AWSLoadBalancerSpec) { *out = make([]string, len(*in)) copy(*out, *in) } + if in.AdditionalIngressRules != nil { + in, out := &in.AdditionalIngressRules, &out.AdditionalIngressRules + *out = make([]IngressRule, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AWSLoadBalancerSpec. @@ -1298,6 +1305,11 @@ func (in *IngressRule) DeepCopyInto(out *IngressRule) { *out = make([]string, len(*in)) copy(*out, *in) } + if in.SourceSecurityGroupRoles != nil { + in, out := &in.SourceSecurityGroupRoles, &out.SourceSecurityGroupRoles + *out = make([]SecurityGroupRole, len(*in)) + copy(*out, *in) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IngressRule. diff --git a/config/crd/bases/controlplane.cluster.x-k8s.io_awsmanagedcontrolplanes.yaml b/config/crd/bases/controlplane.cluster.x-k8s.io_awsmanagedcontrolplanes.yaml index ae6e869a15..f2cd9335f9 100644 --- a/config/crd/bases/controlplane.cluster.x-k8s.io_awsmanagedcontrolplanes.yaml +++ b/config/crd/bases/controlplane.cluster.x-k8s.io_awsmanagedcontrolplanes.yaml @@ -1372,8 +1372,11 @@ spec: type: string type: array description: + description: Description provides extended information + about the ingress rule. type: string fromPort: + description: FromPort is the start of port range. format: int64 type: integer ipv6CidrBlocks: @@ -1383,8 +1386,16 @@ spec: type: string type: array protocol: - description: SecurityGroupProtocol defines the protocol - type for a security group rule. + description: Protocol is the protocol for the ingress + rule. Accepted values are "-1" (all), "4" (IP in + IP),"tcp", "udp", "icmp", and "58" (ICMPv6). + enum: + - "-1" + - "4" + - tcp + - udp + - icmp + - "58" type: string sourceSecurityGroupIds: description: The security group id to allow access @@ -1392,7 +1403,25 @@ spec: items: type: string type: array + sourceSecurityGroupRoles: + description: The security group role to allow access + from. Cannot be specified with CidrBlocks. The field + will be combined with source security group IDs + if specified. + items: + description: SecurityGroupRole defines the unique + role of a security group. + enum: + - bastion + - node + - controlplane + - apiserver-lb + - lb + - node-eks-additional + type: string + type: array toPort: + description: ToPort is the end of port range. format: int64 type: integer required: @@ -2801,8 +2830,11 @@ spec: type: string type: array description: + description: Description provides extended information + about the ingress rule. type: string fromPort: + description: FromPort is the start of port range. format: int64 type: integer ipv6CidrBlocks: @@ -2812,8 +2844,16 @@ spec: type: string type: array protocol: - description: SecurityGroupProtocol defines the protocol - type for a security group rule. + description: Protocol is the protocol for the ingress + rule. Accepted values are "-1" (all), "4" (IP in + IP),"tcp", "udp", "icmp", and "58" (ICMPv6). + enum: + - "-1" + - "4" + - tcp + - udp + - icmp + - "58" type: string sourceSecurityGroupIds: description: The security group id to allow access @@ -2821,7 +2861,25 @@ spec: items: type: string type: array + sourceSecurityGroupRoles: + description: The security group role to allow access + from. Cannot be specified with CidrBlocks. The field + will be combined with source security group IDs + if specified. + items: + description: SecurityGroupRole defines the unique + role of a security group. + enum: + - bastion + - node + - controlplane + - apiserver-lb + - lb + - node-eks-additional + type: string + type: array toPort: + description: ToPort is the end of port range. format: int64 type: integer required: diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_awsclusters.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_awsclusters.yaml index a8b56d5dfb..bb02f3d908 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_awsclusters.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_awsclusters.yaml @@ -966,6 +966,80 @@ spec: description: ControlPlaneLoadBalancer is optional configuration for customizing control plane behavior. properties: + additionalIngressRules: + description: AdditionalIngressRules sets the additional ingress + rules for the control plane load balancer. If no source security + group ids are specified, the default control plane security + group will be used. + items: + description: IngressRule defines an AWS ingress rule for security + groups. + properties: + cidrBlocks: + description: List of CIDR blocks to allow access from. Cannot + be specified with SourceSecurityGroupID. + items: + type: string + type: array + description: + description: Description provides extended information about + the ingress rule. + type: string + fromPort: + description: FromPort is the start of port range. + format: int64 + type: integer + ipv6CidrBlocks: + description: List of IPv6 CIDR blocks to allow access from. + Cannot be specified with SourceSecurityGroupID. + items: + type: string + type: array + protocol: + description: Protocol is the protocol for the ingress rule. + Accepted values are "-1" (all), "4" (IP in IP),"tcp", + "udp", "icmp", and "58" (ICMPv6). + enum: + - "-1" + - "4" + - tcp + - udp + - icmp + - "58" + type: string + sourceSecurityGroupIds: + description: The security group id to allow access from. + Cannot be specified with CidrBlocks. + items: + type: string + type: array + sourceSecurityGroupRoles: + description: The security group role to allow access from. + Cannot be specified with CidrBlocks. The field will be + combined with source security group IDs if specified. + items: + description: SecurityGroupRole defines the unique role + of a security group. + enum: + - bastion + - node + - controlplane + - apiserver-lb + - lb + - node-eks-additional + type: string + type: array + toPort: + description: ToPort is the end of port range. + format: int64 + type: integer + required: + - description + - fromPort + - protocol + - toPort + type: object + type: array additionalSecurityGroups: description: AdditionalSecurityGroups sets the security groups used by the load balancer. Expected to be security group IDs @@ -1836,8 +1910,11 @@ spec: type: string type: array description: + description: Description provides extended information + about the ingress rule. type: string fromPort: + description: FromPort is the start of port range. format: int64 type: integer ipv6CidrBlocks: @@ -1847,8 +1924,16 @@ spec: type: string type: array protocol: - description: SecurityGroupProtocol defines the protocol - type for a security group rule. + description: Protocol is the protocol for the ingress + rule. Accepted values are "-1" (all), "4" (IP in + IP),"tcp", "udp", "icmp", and "58" (ICMPv6). + enum: + - "-1" + - "4" + - tcp + - udp + - icmp + - "58" type: string sourceSecurityGroupIds: description: The security group id to allow access @@ -1856,7 +1941,25 @@ spec: items: type: string type: array + sourceSecurityGroupRoles: + description: The security group role to allow access + from. Cannot be specified with CidrBlocks. The field + will be combined with source security group IDs + if specified. + items: + description: SecurityGroupRole defines the unique + role of a security group. + enum: + - bastion + - node + - controlplane + - apiserver-lb + - lb + - node-eks-additional + type: string + type: array toPort: + description: ToPort is the end of port range. format: int64 type: integer required: diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_awsclustertemplates.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_awsclustertemplates.yaml index cd4769eced..cc410f8148 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_awsclustertemplates.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_awsclustertemplates.yaml @@ -559,6 +559,81 @@ spec: description: ControlPlaneLoadBalancer is optional configuration for customizing control plane behavior. properties: + additionalIngressRules: + description: AdditionalIngressRules sets the additional + ingress rules for the control plane load balancer. If + no source security group ids are specified, the default + control plane security group will be used. + items: + description: IngressRule defines an AWS ingress rule + for security groups. + properties: + cidrBlocks: + description: List of CIDR blocks to allow access + from. Cannot be specified with SourceSecurityGroupID. + items: + type: string + type: array + description: + description: Description provides extended information + about the ingress rule. + type: string + fromPort: + description: FromPort is the start of port range. + format: int64 + type: integer + ipv6CidrBlocks: + description: List of IPv6 CIDR blocks to allow access + from. Cannot be specified with SourceSecurityGroupID. + items: + type: string + type: array + protocol: + description: Protocol is the protocol for the ingress + rule. Accepted values are "-1" (all), "4" (IP + in IP),"tcp", "udp", "icmp", and "58" (ICMPv6). + enum: + - "-1" + - "4" + - tcp + - udp + - icmp + - "58" + type: string + sourceSecurityGroupIds: + description: The security group id to allow access + from. Cannot be specified with CidrBlocks. + items: + type: string + type: array + sourceSecurityGroupRoles: + description: The security group role to allow access + from. Cannot be specified with CidrBlocks. The + field will be combined with source security group + IDs if specified. + items: + description: SecurityGroupRole defines the unique + role of a security group. + enum: + - bastion + - node + - controlplane + - apiserver-lb + - lb + - node-eks-additional + type: string + type: array + toPort: + description: ToPort is the end of port range. + format: int64 + type: integer + required: + - description + - fromPort + - protocol + - toPort + type: object + type: array additionalSecurityGroups: description: AdditionalSecurityGroups sets the security groups used by the load balancer. Expected to be security diff --git a/docs/book/src/topics/bring-your-own-aws-infrastructure.md b/docs/book/src/topics/bring-your-own-aws-infrastructure.md index a3a6b4aa0b..cf3237a3db 100644 --- a/docs/book/src/topics/bring-your-own-aws-infrastructure.md +++ b/docs/book/src/topics/bring-your-own-aws-infrastructure.md @@ -159,6 +159,17 @@ spec: As control plane instances are added or removed, Cluster API will register and deregister them, respectively, with the Classic ELB. +It's also possible to specify custom ingress rules for the control plane load balancer. To do so, add this to the AWSCluster specification: + +```yaml +spec: + additionalIngressRules: + - description: "example ingress rule" + protocol: "-1" # all + fromPort: 7777 + toPort: 7777 +``` + > **WARNING:** Using an existing Classic ELB is an advanced feature. **If you use an existing Classic ELB, you must correctly configure it, and attach subnets to it.** > >An incorrectly configured Classic ELB can easily lead to a non-functional cluster. We strongly recommend you let Cluster API create the Classic ELB. diff --git a/pkg/cloud/services/securitygroup/securitygroups.go b/pkg/cloud/services/securitygroup/securitygroups.go index d1c7b9a24d..f4a760c3a7 100644 --- a/pkg/cloud/services/securitygroup/securitygroups.go +++ b/pkg/cloud/services/securitygroup/securitygroups.go @@ -519,6 +519,20 @@ func (s *Service) getSecurityGroupIngressRules(role infrav1.SecurityGroupRole) ( if s.scope.Bastion().Enabled { rules = append(rules, s.defaultSSHIngressRule(s.scope.SecurityGroups()[infrav1.SecurityGroupBastion].ID)) } + if s.scope.ControlPlaneLoadBalancer() != nil { + additionalIngressRules := s.scope.ControlPlaneLoadBalancer().AdditionalIngressRules + for i := range additionalIngressRules { + if additionalIngressRules[i].SourceSecurityGroupIDs == nil && additionalIngressRules[i].SourceSecurityGroupRoles == nil { // if the rule doesn't have a source security group, use the control plane security group + additionalIngressRules[i].SourceSecurityGroupIDs = []string{s.scope.SecurityGroups()[infrav1.SecurityGroupControlPlane].ID} + continue + } + + for _, sourceSGRole := range additionalIngressRules[i].SourceSecurityGroupRoles { + additionalIngressRules[i].SourceSecurityGroupIDs = append(additionalIngressRules[i].SourceSecurityGroupIDs, s.scope.SecurityGroups()[sourceSGRole].ID) + } + } + rules = append(rules, s.scope.ControlPlaneLoadBalancer().AdditionalIngressRules...) + } return append(cniRules, rules...), nil case infrav1.SecurityGroupNode: diff --git a/pkg/cloud/services/securitygroup/securitygroups_test.go b/pkg/cloud/services/securitygroup/securitygroups_test.go index 26ae6311e2..af2c9e8f09 100644 --- a/pkg/cloud/services/securitygroup/securitygroups_test.go +++ b/pkg/cloud/services/securitygroup/securitygroups_test.go @@ -17,6 +17,7 @@ limitations under the License. package securitygroup import ( + "reflect" "strings" "testing" @@ -612,6 +613,166 @@ func TestControlPlaneSecurityGroupNotOpenToAnyCIDR(t *testing.T) { } } +func TestAdditionalControlPlaneSecurityGroup(t *testing.T) { + scheme := runtime.NewScheme() + _ = infrav1.AddToScheme(scheme) + + testCases := []struct { + name string + controlPlaneLBSpec *infrav1.AWSLoadBalancerSpec + expectedAdditionalIngresRule infrav1.IngressRule + }{ + { + name: "default control plane security group is used", + controlPlaneLBSpec: &infrav1.AWSLoadBalancerSpec{ + AdditionalIngressRules: []infrav1.IngressRule{ + { + Description: "test", + Protocol: infrav1.SecurityGroupProtocolTCP, + FromPort: 9345, + ToPort: 9345, + }, + }, + }, + expectedAdditionalIngresRule: infrav1.IngressRule{ + Description: "test", + Protocol: infrav1.SecurityGroupProtocolTCP, + FromPort: 9345, + ToPort: 9345, + SourceSecurityGroupIDs: []string{"cp-sg-id"}, + }, + }, + { + name: "custom security group id is used", + controlPlaneLBSpec: &infrav1.AWSLoadBalancerSpec{ + AdditionalIngressRules: []infrav1.IngressRule{ + { + Description: "test", + Protocol: infrav1.SecurityGroupProtocolTCP, + FromPort: 9345, + ToPort: 9345, + SourceSecurityGroupIDs: []string{"test"}, + }, + }, + }, + expectedAdditionalIngresRule: infrav1.IngressRule{ + Description: "test", + Protocol: infrav1.SecurityGroupProtocolTCP, + FromPort: 9345, + ToPort: 9345, + SourceSecurityGroupIDs: []string{"test"}, + }, + }, + { + name: "another security group role is used", + controlPlaneLBSpec: &infrav1.AWSLoadBalancerSpec{ + AdditionalIngressRules: []infrav1.IngressRule{ + { + Description: "test", + Protocol: infrav1.SecurityGroupProtocolTCP, + FromPort: 9345, + ToPort: 9345, + SourceSecurityGroupRoles: []infrav1.SecurityGroupRole{infrav1.SecurityGroupNode}, + }, + }, + }, + expectedAdditionalIngresRule: infrav1.IngressRule{ + Description: "test", + Protocol: infrav1.SecurityGroupProtocolTCP, + FromPort: 9345, + ToPort: 9345, + SourceSecurityGroupIDs: []string{"node-sg-id"}, + }, + }, + { + name: "another security group role and a custom security group id is used", + controlPlaneLBSpec: &infrav1.AWSLoadBalancerSpec{ + AdditionalIngressRules: []infrav1.IngressRule{ + { + Description: "test", + Protocol: infrav1.SecurityGroupProtocolTCP, + FromPort: 9345, + ToPort: 9345, + SourceSecurityGroupIDs: []string{"test"}, + SourceSecurityGroupRoles: []infrav1.SecurityGroupRole{infrav1.SecurityGroupNode}, + }, + }, + }, + expectedAdditionalIngresRule: infrav1.IngressRule{ + Description: "test", + Protocol: infrav1.SecurityGroupProtocolTCP, + FromPort: 9345, + ToPort: 9345, + SourceSecurityGroupIDs: []string{"test", "node-sg-id"}, + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + cs, err := scope.NewClusterScope(scope.ClusterScopeParams{ + Client: fake.NewClientBuilder().WithScheme(scheme).Build(), + Cluster: &clusterv1.Cluster{ + ObjectMeta: metav1.ObjectMeta{Name: "test-cluster"}, + }, + AWSCluster: &infrav1.AWSCluster{ + Spec: infrav1.AWSClusterSpec{ + ControlPlaneLoadBalancer: tc.controlPlaneLBSpec, + }, + Status: infrav1.AWSClusterStatus{ + Network: infrav1.NetworkStatus{ + SecurityGroups: map[infrav1.SecurityGroupRole]infrav1.SecurityGroup{ + infrav1.SecurityGroupControlPlane: { + ID: "cp-sg-id", + }, + infrav1.SecurityGroupNode: { + ID: "node-sg-id", + }, + }, + }, + }, + }, + }) + if err != nil { + t.Fatalf("Failed to create test context: %v", err) + } + + s := NewService(cs, testSecurityGroupRoles) + rules, err := s.getSecurityGroupIngressRules(infrav1.SecurityGroupControlPlane) + if err != nil { + t.Fatalf("Failed to lookup controlplane security group ingress rules: %v", err) + } + + found := false + for _, r := range rules { + if r.Description == "test" { + found = true + + if r.Protocol != tc.expectedAdditionalIngresRule.Protocol { + t.Fatalf("Expected protocol %s, got %s", tc.expectedAdditionalIngresRule.Protocol, r.Protocol) + } + + if r.FromPort != tc.expectedAdditionalIngresRule.FromPort { + t.Fatalf("Expected from port %d, got %d", tc.expectedAdditionalIngresRule.FromPort, r.FromPort) + } + + if r.ToPort != tc.expectedAdditionalIngresRule.ToPort { + t.Fatalf("Expected to port %d, got %d", tc.expectedAdditionalIngresRule.ToPort, r.ToPort) + } + + if !reflect.DeepEqual(r.SourceSecurityGroupIDs, tc.expectedAdditionalIngresRule.SourceSecurityGroupIDs) { + t.Fatalf("Expected source security group IDs %v, got %v", tc.expectedAdditionalIngresRule.SourceSecurityGroupIDs, r.SourceSecurityGroupIDs) + } + } + } + + if !found { + t.Fatal("Additional ingress rule was not found") + } + }) + } +} + func TestDeleteSecurityGroups(t *testing.T) { mockCtrl := gomock.NewController(t) defer mockCtrl.Finish()