diff --git a/api/v1alpha1/securitygroup_types.go b/api/v1alpha1/securitygroup_types.go index f3909eb7..75dac526 100644 --- a/api/v1alpha1/securitygroup_types.go +++ b/api/v1alpha1/securitygroup_types.go @@ -26,40 +26,88 @@ type RuleDirection string // +kubebuilder:validation:MaxLength:=16 type Protocol string +const ( + ProtocolANY Protocol = "any" + ProtocolAH Protocol = "ah" + ProtocolDCCP Protocol = "dccp" + ProtocolEGP Protocol = "egp" + ProtocolESP Protocol = "esp" + ProtocolGRE Protocol = "gre" + ProtocolICMP Protocol = "icmp" + ProtocolICMPV6 Protocol = "icmpv6" + ProtocolIGMP Protocol = "igmp" + ProtocolIPIP Protocol = "ipip" + ProtocolIPV6ENCAP Protocol = "ipv6-encap" + ProtocolIPV6FRAG Protocol = "ipv6-frag" + ProtocolIPV6ICMP Protocol = "ipv6-icmp" + ProtocolIPV6NONXT Protocol = "ipv6-nonxt" + ProtocolIPV6OPTS Protocol = "ipv6-opts" + ProtocolIPV6ROUTE Protocol = "ipv6-route" + ProtocolOSPF Protocol = "ospf" + ProtocolPGM Protocol = "pgm" + ProtocolRSVP Protocol = "rsvp" + ProtocolSCTP Protocol = "sctp" + ProtocolTCP Protocol = "tcp" + ProtocolUDP Protocol = "udp" + ProtocolUDPLITE Protocol = "udplite" + ProtocolVRRP Protocol = "vrrp" +) + // +kubebuilder:validation:Enum:=IPv4;IPv6 // +kubebuilder:validation:MinLength:=1 -// +kubebuilder:validation:MaxLength:=16 +// +kubebuilder:validation:MaxLength:=4 type Ethertype string +const ( + EtherTypeIPv4 Ethertype = "IPv4" + EtherTypeIPv6 Ethertype = "IPv6" +) + // SecurityGroupRule defines a Security Group rule // +kubebuilder:validation:MinProperties:=1 +// +kubebuilder:validation:XValidation:rule="has(self.portRangeMin) == has(self.portRangeMax)",message="PortRangeMin and PortRangeMax should exist together" +// +kubebuilder:validation:XValidation:rule="(!has(self.portRangeMin) || !has(self.portRangeMax)) || has(self.protocol)",message="Protocol must exist when PortRangeMin or PortRangeMax exist" +// +kubebuilder:validation:XValidation:rule="(!has(self.portRangeMin) || !has(self.portRangeMax))|| (self.portRangeMin <= self.portRangeMax)",message="PortRangeMax should be equal or greater than PortRangeMin" +// +kubebuilder:validation:XValidation:rule="!has(self.protocol) || !(self.protocol == 'icmp' || self.protocol == 'icmpv6') || (self.portRangeMin >= 0 && self.portRangeMin <= 255 && self.portRangeMax >= 0 && self.portRangeMax <= 255)",message="When protocol is ICMP or ICMPv6 PortRangeMin and PortRangeMax should be between 0 and 255" +// +kubebuilder:validation:XValidation:rule="!has(self.remoteIPPrefix) || (isCIDR(self.remoteIPPrefix) && cidr(self.remoteIPPrefix).ip().family() == 4 && self.ethertype == 'IPv4') || (isCIDR(self.remoteIPPrefix) && cidr(self.remoteIPPrefix).ip().family() == 6 && self.ethertype == 'IPv6')",message="RemoteIPPrefix should be a valid CIDR and match the Ethertype" type SecurityGroupRule struct { // Description of the existing resource // +optional + Description *OpenStackDescription `json:"description,omitempty"` // Direction represents the direction in which the security group rule // is applied. Can be ingress or egress. + // +optional Direction *RuleDirection `json:"direction,omitempty"` // RemoteAddressGroupId (Not in gophercloud) - // RemoteGroupID RemoteGroupID *UUID `json:"remoteGroupID,omitempty"` - // RemoteIPPrefix + // RemoteIPPrefix is an IP address block. Should match the Ethertype (IPv4 or IPv6) + // +optional RemoteIPPrefix *CIDR `json:"remoteIPPrefix,omitempty"` // Protocol is the IP protocol can be represented by a string, an // integer, or null + // +optional Protocol *Protocol `json:"protocol,omitempty"` // EtherType must be IPv4 or IPv6, and addresses represented in CIDR // must match the ingress or egress rules. - Ethertype *Ethertype `json:"ethertype,omitempty"` - - PortRangeMin *int32 `json:"portRangeMin,omitempty"` - PortRangeMax *int32 `json:"portRangeMax,omitempty"` + // +kubebuilder:validation:Required + Ethertype *Ethertype `json:"ethertype"` + // If the protocol is [tcp, udp, dccp sctp,udplite] this value must be less than + // or equal to the PortRangeMax attribute value. + // If the protocol is ICMP, this value must be an ICMP type + // +optional + PortRangeMin *uint16 `json:"portRangeMin,omitempty"` + // If the protocol is [tcp, udp, dccp sctp,udplite] this value must be greater than + // or equal to the PortRangeMin attribute value. + // If the protocol is ICMP, this value must be an ICMP type + // +optional + PortRangeMax *uint16 `json:"portRangeMax,omitempty"` } type SecurityGroupRuleStatus struct { diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index ee57c9e8..2103d602 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -2154,12 +2154,12 @@ func (in *SecurityGroupRule) DeepCopyInto(out *SecurityGroupRule) { } if in.PortRangeMin != nil { in, out := &in.PortRangeMin, &out.PortRangeMin - *out = new(int32) + *out = new(uint16) **out = **in } if in.PortRangeMax != nil { in, out := &in.PortRangeMax, &out.PortRangeMax - *out = new(int32) + *out = new(uint16) **out = **in } } diff --git a/cmd/models-schema/zz_generated.openapi.go b/cmd/models-schema/zz_generated.openapi.go index 67891ad4..6ecc7855 100644 --- a/cmd/models-schema/zz_generated.openapi.go +++ b/cmd/models-schema/zz_generated.openapi.go @@ -4300,9 +4300,8 @@ func schema_k_orc_openstack_resource_controller_api_v1alpha1_SecurityGroupRule(r Properties: map[string]spec.Schema{ "description": { SchemaProps: spec.SchemaProps{ - Description: "Description of the existing resource", - Type: []string{"string"}, - Format: "", + Type: []string{"string"}, + Format: "", }, }, "direction": { @@ -4314,14 +4313,14 @@ func schema_k_orc_openstack_resource_controller_api_v1alpha1_SecurityGroupRule(r }, "remoteGroupID": { SchemaProps: spec.SchemaProps{ - Description: "RemoteGroupID", + Description: "RemoteAddressGroupId (Not in gophercloud) RemoteGroupID", Type: []string{"string"}, Format: "", }, }, "remoteIPPrefix": { SchemaProps: spec.SchemaProps{ - Description: "RemoteIPPrefix", + Description: "RemoteIPPrefix is an IP address block. Should match the Ethertype (IPv4 or IPv6)", Type: []string{"string"}, Format: "", }, @@ -4342,17 +4341,20 @@ func schema_k_orc_openstack_resource_controller_api_v1alpha1_SecurityGroupRule(r }, "portRangeMin": { SchemaProps: spec.SchemaProps{ - Type: []string{"integer"}, - Format: "int32", + Description: "If the protocol is [tcp, udp, dccp sctp,udplite] this value must be less than or equal to the PortRangeMax attribute value. If the protocol is ICMP, this value must be an ICMP type", + Type: []string{"integer"}, + Format: "int32", }, }, "portRangeMax": { SchemaProps: spec.SchemaProps{ - Type: []string{"integer"}, - Format: "int32", + Description: "If the protocol is [tcp, udp, dccp sctp,udplite] this value must be greater than or equal to the PortRangeMin attribute value. If the protocol is ICMP, this value must be an ICMP type", + Type: []string{"integer"}, + Format: "int32", }, }, }, + Required: []string{"ethertype"}, }, }, } diff --git a/config/crd/bases/openstack.k-orc.cloud_securitygroups.yaml b/config/crd/bases/openstack.k-orc.cloud_securitygroups.yaml index 1ea033b3..caed412c 100644 --- a/config/crd/bases/openstack.k-orc.cloud_securitygroups.yaml +++ b/config/crd/bases/openstack.k-orc.cloud_securitygroups.yaml @@ -232,7 +232,6 @@ spec: minProperties: 1 properties: description: - description: Description of the existing resource maxLength: 1024 minLength: 1 type: string @@ -253,14 +252,20 @@ spec: enum: - IPv4 - IPv6 - maxLength: 16 + maxLength: 4 minLength: 1 type: string portRangeMax: - format: int32 + description: |- + If the protocol is [tcp, udp, dccp sctp,udplite] this value must be greater than + or equal to the PortRangeMin attribute value. + If the protocol is ICMP, this value must be an ICMP type type: integer portRangeMin: - format: int32 + description: |- + If the protocol is [tcp, udp, dccp sctp,udplite] this value must be less than + or equal to the PortRangeMax attribute value. + If the protocol is ICMP, this value must be an ICMP type type: integer protocol: description: |- @@ -271,17 +276,44 @@ spec: pattern: \b([01]?[0-9][0-9]?|2[0-4][0-9]|25[0-5])\b|any|ah|dccp|egp|esp|gre|icmp|icmpv6|igmp|ipip|ipv6-encap|ipv6-frag|ipv6-icmp|ipv6-nonxt|ipv6-opts|ipv6-route|ospf|pgm|rsvp|sctp|tcp|udp|udplite|vrrp type: string remoteGroupID: - description: RemoteGroupID + description: |- + RemoteAddressGroupId (Not in gophercloud) + RemoteGroupID format: uuid maxLength: 36 type: string remoteIPPrefix: - description: RemoteIPPrefix + description: RemoteIPPrefix is an IP address block. Should + match the Ethertype (IPv4 or IPv6) format: cidr maxLength: 49 minLength: 1 type: string + required: + - ethertype type: object + x-kubernetes-validations: + - message: PortRangeMin and PortRangeMax should exist together + rule: has(self.portRangeMin) == has(self.portRangeMax) + - message: Protocol must exist when PortRangeMin or PortRangeMax + exist + rule: (!has(self.portRangeMin) || !has(self.portRangeMax)) + || has(self.protocol) + - message: PortRangeMax should be equal or greater than PortRangeMin + rule: (!has(self.portRangeMin) || !has(self.portRangeMax))|| + (self.portRangeMin <= self.portRangeMax) + - message: When protocol is ICMP or ICMPv6 PortRangeMin and + PortRangeMax should be between 0 and 255 + rule: '!has(self.protocol) || !(self.protocol == ''icmp'' + || self.protocol == ''icmpv6'') || (self.portRangeMin >= + 0 && self.portRangeMin <= 255 && self.portRangeMax >= 0 + && self.portRangeMax <= 255)' + - message: RemoteIPPrefix should be a valid CIDR and match the + Ethertype + rule: '!has(self.remoteIPPrefix) || (isCIDR(self.remoteIPPrefix) + && cidr(self.remoteIPPrefix).ip().family() == 4 && self.ethertype + == ''IPv4'') || (isCIDR(self.remoteIPPrefix) && cidr(self.remoteIPPrefix).ip().family() + == 6 && self.ethertype == ''IPv6'')' maxItems: 256 type: array x-kubernetes-list-type: atomic diff --git a/examples/bases/cirros/securitygroup.yaml b/examples/bases/cirros/securitygroup.yaml index 553bba07..019bd5b4 100644 --- a/examples/bases/cirros/securitygroup.yaml +++ b/examples/bases/cirros/securitygroup.yaml @@ -12,7 +12,7 @@ spec: stateful: true rules: - direction: ingress - protocol: tcp - portRangeMin: 22 - portRangeMax: 22 + portRangeMin: 254 + portRangeMax: 255 ethertype: IPv4 + RemoteIPPrefix: "foo" diff --git a/pkg/clients/applyconfiguration/api/v1alpha1/securitygrouprule.go b/pkg/clients/applyconfiguration/api/v1alpha1/securitygrouprule.go index 9b950f4e..ac64ebe5 100644 --- a/pkg/clients/applyconfiguration/api/v1alpha1/securitygrouprule.go +++ b/pkg/clients/applyconfiguration/api/v1alpha1/securitygrouprule.go @@ -31,8 +31,8 @@ type SecurityGroupRuleApplyConfiguration struct { RemoteIPPrefix *v1alpha1.CIDR `json:"remoteIPPrefix,omitempty"` Protocol *v1alpha1.Protocol `json:"protocol,omitempty"` Ethertype *v1alpha1.Ethertype `json:"ethertype,omitempty"` - PortRangeMin *int32 `json:"portRangeMin,omitempty"` - PortRangeMax *int32 `json:"portRangeMax,omitempty"` + PortRangeMin *uint16 `json:"portRangeMin,omitempty"` + PortRangeMax *uint16 `json:"portRangeMax,omitempty"` } // SecurityGroupRuleApplyConfiguration constructs a declarative configuration of the SecurityGroupRule type for use with @@ -92,7 +92,7 @@ func (b *SecurityGroupRuleApplyConfiguration) WithEthertype(value v1alpha1.Ether // WithPortRangeMin sets the PortRangeMin field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the PortRangeMin field is set to the value of the last call. -func (b *SecurityGroupRuleApplyConfiguration) WithPortRangeMin(value int32) *SecurityGroupRuleApplyConfiguration { +func (b *SecurityGroupRuleApplyConfiguration) WithPortRangeMin(value uint16) *SecurityGroupRuleApplyConfiguration { b.PortRangeMin = &value return b } @@ -100,7 +100,7 @@ func (b *SecurityGroupRuleApplyConfiguration) WithPortRangeMin(value int32) *Sec // WithPortRangeMax sets the PortRangeMax field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the PortRangeMax field is set to the value of the last call. -func (b *SecurityGroupRuleApplyConfiguration) WithPortRangeMax(value int32) *SecurityGroupRuleApplyConfiguration { +func (b *SecurityGroupRuleApplyConfiguration) WithPortRangeMax(value uint16) *SecurityGroupRuleApplyConfiguration { b.PortRangeMax = &value return b } diff --git a/test/apivalidations/securitygroup_test.go b/test/apivalidations/securitygroup_test.go new file mode 100644 index 00000000..e9fb61d3 --- /dev/null +++ b/test/apivalidations/securitygroup_test.go @@ -0,0 +1,320 @@ +/* +Copyright 2024 The ORC Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package apivalidations + +import ( + "context" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + corev1 "k8s.io/api/core/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + + orcv1alpha1 "github.com/k-orc/openstack-resource-controller/api/v1alpha1" + applyconfigv1alpha1 "github.com/k-orc/openstack-resource-controller/pkg/clients/applyconfiguration/api/v1alpha1" +) + +const ( + securityGroupName = "sg-foo" + securityGroupID = "265c9e4f-0f5a-46e4-9f3f-fb8de25ae12f" +) + +func securityGroupStub(namespace *corev1.Namespace) *orcv1alpha1.SecurityGroup { + obj := &orcv1alpha1.SecurityGroup{} + obj.Name = securityGroupName + obj.Namespace = namespace.Name + return obj +} + +func testSecurityGroupResource() *applyconfigv1alpha1.SecurityGroupResourceSpecApplyConfiguration { + return applyconfigv1alpha1.SecurityGroupResourceSpec().WithRules() +} + +func baseSecurityGroupPatch(securityGroup client.Object) *applyconfigv1alpha1.SecurityGroupApplyConfiguration { + return applyconfigv1alpha1.SecurityGroup(securityGroup.GetName(), securityGroup.GetNamespace()). + WithSpec(applyconfigv1alpha1.SecurityGroupSpec(). + WithCloudCredentialsRef(testCredentials())) +} + +func baseSGRulePatchSpec() *applyconfigv1alpha1.SecurityGroupRuleApplyConfiguration { + return applyconfigv1alpha1.SecurityGroupRule().WithEthertype(orcv1alpha1.EtherTypeIPv4) +} + +func testSecurityGroupImport() *applyconfigv1alpha1.SecurityGroupImportApplyConfiguration { + return applyconfigv1alpha1.SecurityGroupImport().WithID(securityGroupID) +} + +var _ = Describe("ORC SecurityGroup API validations", func() { + var namespace *corev1.Namespace + BeforeEach(func() { + namespace = createNamespace() + }) + + It("should allow to create a minimal security group and managementPolicy should default to managed", func(ctx context.Context) { + securityGroup := securityGroupStub(namespace) + patch := baseSecurityGroupPatch(securityGroup) + patch.Spec.WithResource(applyconfigv1alpha1.SecurityGroupResourceSpec()) + Expect(applyObj(ctx, securityGroup, patch)).To(Succeed()) + Expect(securityGroup.Spec.ManagementPolicy).To(Equal(orcv1alpha1.ManagementPolicyManaged)) + }) + + It("should require import for unmanaged", func(ctx context.Context) { + securityGroup := securityGroupStub(namespace) + patch := baseSecurityGroupPatch(securityGroup) + patch.Spec.WithManagementPolicy(orcv1alpha1.ManagementPolicyUnmanaged) + Expect(applyObj(ctx, securityGroup, patch)).NotTo(Succeed()) + + patch.Spec.WithImport(testSecurityGroupImport()) + Expect(applyObj(ctx, securityGroup, patch)).To(Succeed()) + }) + + It("should not permit unmanaged with resource", func(ctx context.Context) { + securityGroup := securityGroupStub(namespace) + patch := baseSecurityGroupPatch(securityGroup) + patch.Spec. + WithManagementPolicy(orcv1alpha1.ManagementPolicyUnmanaged). + WithImport(testSecurityGroupImport()). + WithResource(testSecurityGroupResource()) + Expect(applyObj(ctx, securityGroup, patch)).NotTo(Succeed()) + }) + + It("should not permit empty import", func(ctx context.Context) { + securityGroup := securityGroupStub(namespace) + patch := baseSecurityGroupPatch(securityGroup) + patch.Spec. + WithManagementPolicy(orcv1alpha1.ManagementPolicyUnmanaged). + WithImport(applyconfigv1alpha1.SecurityGroupImport()) + Expect(applyObj(ctx, securityGroup, patch)).NotTo(Succeed()) + }) + + It("should not permit empty import filter", func(ctx context.Context) { + securityGroup := securityGroupStub(namespace) + patch := baseSecurityGroupPatch(securityGroup) + patch.Spec. + WithManagementPolicy(orcv1alpha1.ManagementPolicyUnmanaged). + WithImport(applyconfigv1alpha1.SecurityGroupImport(). + WithFilter(applyconfigv1alpha1.SecurityGroupFilter())) + Expect(applyObj(ctx, securityGroup, patch)).NotTo(Succeed()) + }) + + It("should permit import filter with name", func(ctx context.Context) { + securityGroup := securityGroupStub(namespace) + patch := baseSecurityGroupPatch(securityGroup) + patch.Spec. + WithManagementPolicy(orcv1alpha1.ManagementPolicyUnmanaged). + WithImport(applyconfigv1alpha1.SecurityGroupImport(). + WithFilter(applyconfigv1alpha1.SecurityGroupFilter().WithName("foo"))) + Expect(applyObj(ctx, securityGroup, patch)).To(Succeed()) + }) + + It("should require resource for managed", func(ctx context.Context) { + securityGroup := securityGroupStub(namespace) + patch := baseSecurityGroupPatch(securityGroup) + patch.Spec.WithManagementPolicy(orcv1alpha1.ManagementPolicyManaged) + Expect(applyObj(ctx, securityGroup, patch)).NotTo(Succeed()) + + patch.Spec.WithResource(testSecurityGroupResource()) + Expect(applyObj(ctx, securityGroup, patch)).To(Succeed()) + }) + + It("should not permit managed with import", func(ctx context.Context) { + securityGroup := securityGroupStub(namespace) + patch := baseSecurityGroupPatch(securityGroup) + patch.Spec. + WithImport(testSecurityGroupImport()). + WithManagementPolicy(orcv1alpha1.ManagementPolicyManaged). + WithResource(testSecurityGroupResource()) + Expect(applyObj(ctx, securityGroup, patch)).NotTo(Succeed()) + }) + + It("should not permit managedOptions for unmanaged", func(ctx context.Context) { + securityGroup := securityGroupStub(namespace) + patch := baseSecurityGroupPatch(securityGroup) + patch.Spec. + WithImport(testSecurityGroupImport()). + WithManagementPolicy(orcv1alpha1.ManagementPolicyUnmanaged). + WithManagedOptions(applyconfigv1alpha1.ManagedOptions(). + WithOnDelete(orcv1alpha1.OnDeleteDetach)) + Expect(applyObj(ctx, securityGroup, patch)).NotTo(Succeed()) + }) + + It("should permit managedOptions for managed", func(ctx context.Context) { + securityGroup := securityGroupStub(namespace) + patch := baseSecurityGroupPatch(securityGroup) + patch.Spec.WithResource(applyconfigv1alpha1.SecurityGroupResourceSpec()) + patch.Spec. + WithManagedOptions(applyconfigv1alpha1.ManagedOptions(). + WithOnDelete(orcv1alpha1.OnDeleteDetach)).WithResource( + applyconfigv1alpha1.SecurityGroupResourceSpec()) + Expect(applyObj(ctx, securityGroup, patch)).To(Succeed()) + Expect(securityGroup.Spec.ManagedOptions.OnDelete).To(Equal(orcv1alpha1.OnDelete("detach"))) + }) + + It("should not permit invalid direction", func(ctx context.Context) { + securityGroup := securityGroupStub(namespace) + patch := baseSecurityGroupPatch(securityGroup) + patch.Spec.WithResource(applyconfigv1alpha1.SecurityGroupResourceSpec().WithRules(baseSGRulePatchSpec().WithDirection("foo"))) + Expect(applyObj(ctx, securityGroup, patch)).NotTo(Succeed(), "create security group") + }) + + It("should permit valid direction", func(ctx context.Context) { + securityGroup := securityGroupStub(namespace) + patch := baseSecurityGroupPatch(securityGroup) + patch.Spec.WithResource(applyconfigv1alpha1.SecurityGroupResourceSpec().WithRules(baseSGRulePatchSpec().WithDirection("ingress"))) + Expect(applyObj(ctx, securityGroup, patch)).To(Succeed(), "create security group") + patch.Spec.WithResource(applyconfigv1alpha1.SecurityGroupResourceSpec().WithRules(baseSGRulePatchSpec().WithDirection("egress"))) + Expect(applyObj(ctx, securityGroup, patch)).To(Succeed(), "create security group") + }) + + It("should not permit invalid ethertype", func(ctx context.Context) { + securityGroup := securityGroupStub(namespace) + patch := baseSecurityGroupPatch(securityGroup) + patch.Spec.WithResource(applyconfigv1alpha1.SecurityGroupResourceSpec().WithRules(applyconfigv1alpha1.SecurityGroupRule().WithEthertype("foo"))) + Expect(applyObj(ctx, securityGroup, patch)).NotTo(Succeed(), "create security group") + }) + + It("should not permit no ethertype", func(ctx context.Context) { + securityGroup := securityGroupStub(namespace) + patch := baseSecurityGroupPatch(securityGroup) + patch.Spec.WithResource(applyconfigv1alpha1.SecurityGroupResourceSpec().WithRules(applyconfigv1alpha1.SecurityGroupRule())) + Expect(applyObj(ctx, securityGroup, patch)).NotTo(Succeed(), "create security group") + }) + + It("should permit valid ethertype", func(ctx context.Context) { + securityGroup := securityGroupStub(namespace) + patch := baseSecurityGroupPatch(securityGroup) + patch.Spec.WithResource(applyconfigv1alpha1.SecurityGroupResourceSpec().WithRules(applyconfigv1alpha1.SecurityGroupRule().WithEthertype(orcv1alpha1.EtherTypeIPv6))) + Expect(applyObj(ctx, securityGroup, patch)).To(Succeed(), "create security group") + patch.Spec.WithResource(applyconfigv1alpha1.SecurityGroupResourceSpec().WithRules(applyconfigv1alpha1.SecurityGroupRule().WithEthertype(orcv1alpha1.EtherTypeIPv6))) + Expect(applyObj(ctx, securityGroup, patch)).To(Succeed(), "create security group") + }) + + DescribeTable("should permit valid protocol", + func(ctx context.Context, protocol orcv1alpha1.Protocol, ethertype orcv1alpha1.Ethertype) { + securityGroup := securityGroupStub(namespace) + patch := baseSecurityGroupPatch(securityGroup) + patch.Spec.WithResource(applyconfigv1alpha1.SecurityGroupResourceSpec().WithRules(applyconfigv1alpha1.SecurityGroupRule().WithEthertype(ethertype))) + Expect(applyObj(ctx, securityGroup, patch)).To(Succeed(), "create security group") + }, + Entry(string(orcv1alpha1.ProtocolANY), orcv1alpha1.ProtocolANY, orcv1alpha1.EtherTypeIPv4), + Entry(string(orcv1alpha1.ProtocolAH), orcv1alpha1.ProtocolAH, orcv1alpha1.EtherTypeIPv4), + Entry(string(orcv1alpha1.ProtocolDCCP), orcv1alpha1.ProtocolDCCP, orcv1alpha1.EtherTypeIPv4), + Entry(string(orcv1alpha1.ProtocolEGP), orcv1alpha1.ProtocolEGP, orcv1alpha1.EtherTypeIPv4), + Entry(string(orcv1alpha1.ProtocolESP), orcv1alpha1.ProtocolESP, orcv1alpha1.EtherTypeIPv4), + Entry(string(orcv1alpha1.ProtocolGRE), orcv1alpha1.ProtocolGRE, orcv1alpha1.EtherTypeIPv4), + Entry(string(orcv1alpha1.ProtocolICMP), orcv1alpha1.ProtocolICMP, orcv1alpha1.EtherTypeIPv4), + Entry(string(orcv1alpha1.ProtocolICMPV6), orcv1alpha1.ProtocolICMPV6, orcv1alpha1.EtherTypeIPv4), + Entry(string(orcv1alpha1.ProtocolIGMP), orcv1alpha1.ProtocolIGMP, orcv1alpha1.EtherTypeIPv4), + Entry(string(orcv1alpha1.ProtocolIPIP), orcv1alpha1.ProtocolIPIP, orcv1alpha1.EtherTypeIPv4), + Entry(string(orcv1alpha1.ProtocolOSPF), orcv1alpha1.ProtocolOSPF, orcv1alpha1.EtherTypeIPv4), + Entry(string(orcv1alpha1.ProtocolPGM), orcv1alpha1.ProtocolPGM, orcv1alpha1.EtherTypeIPv4), + Entry(string(orcv1alpha1.ProtocolRSVP), orcv1alpha1.ProtocolRSVP, orcv1alpha1.EtherTypeIPv4), + Entry(string(orcv1alpha1.ProtocolSCTP), orcv1alpha1.ProtocolSCTP, orcv1alpha1.EtherTypeIPv4), + Entry(string(orcv1alpha1.ProtocolTCP), orcv1alpha1.ProtocolTCP, orcv1alpha1.EtherTypeIPv4), + Entry(string(orcv1alpha1.ProtocolUDP), orcv1alpha1.ProtocolUDP, orcv1alpha1.EtherTypeIPv4), + Entry(string(orcv1alpha1.ProtocolUDPLITE), orcv1alpha1.ProtocolUDPLITE, orcv1alpha1.EtherTypeIPv4), + Entry(string(orcv1alpha1.ProtocolVRRP), orcv1alpha1.ProtocolVRRP, orcv1alpha1.EtherTypeIPv4), + Entry(string(orcv1alpha1.ProtocolIPV6ENCAP), orcv1alpha1.ProtocolIPV6ENCAP, orcv1alpha1.EtherTypeIPv6), + Entry(string(orcv1alpha1.ProtocolIPV6FRAG), orcv1alpha1.ProtocolIPV6FRAG, orcv1alpha1.EtherTypeIPv6), + Entry(string(orcv1alpha1.ProtocolIPV6ICMP), orcv1alpha1.ProtocolIPV6ICMP, orcv1alpha1.EtherTypeIPv6), + Entry(string(orcv1alpha1.ProtocolIPV6NONXT), orcv1alpha1.ProtocolIPV6NONXT, orcv1alpha1.EtherTypeIPv6), + Entry(string(orcv1alpha1.ProtocolIPV6OPTS), orcv1alpha1.ProtocolIPV6OPTS, orcv1alpha1.EtherTypeIPv6), + Entry(string(orcv1alpha1.ProtocolIPV6ROUTE), orcv1alpha1.ProtocolIPV6ROUTE, orcv1alpha1.EtherTypeIPv6), + ) + + It("should permit valid numeric protocol and reject invalid numeric protocol", func(ctx context.Context) { + securityGroup := securityGroupStub(namespace) + patch := baseSecurityGroupPatch(securityGroup) + sgRulePatchSpec := applyconfigv1alpha1.SecurityGroupRule().WithEthertype(orcv1alpha1.EtherTypeIPv4) + patch.Spec.WithResource(applyconfigv1alpha1.SecurityGroupResourceSpec().WithRules(sgRulePatchSpec.WithProtocol("0"))) + Expect(applyObj(ctx, securityGroup, patch)).To(Succeed(), "create security group") + patch.Spec.WithResource(applyconfigv1alpha1.SecurityGroupResourceSpec().WithRules(sgRulePatchSpec.WithProtocol("255"))) + Expect(applyObj(ctx, securityGroup, patch)).To(Succeed(), "create security group") + patch.Spec.WithResource(applyconfigv1alpha1.SecurityGroupResourceSpec().WithRules(sgRulePatchSpec.WithProtocol("256"))) + Expect(applyObj(ctx, securityGroup, patch)).NotTo(Succeed(), "create security group") + }) + + It("should not permit invalid protocol", func(ctx context.Context) { + securityGroup := securityGroupStub(namespace) + patch := baseSecurityGroupPatch(securityGroup) + patch.Spec.WithResource(applyconfigv1alpha1.SecurityGroupResourceSpec().WithRules(baseSGRulePatchSpec().WithProtocol("foo"))) + Expect(applyObj(ctx, securityGroup, patch)).NotTo(Succeed(), "create security group") + }) + + It("should reject port ranges without protocol", func(ctx context.Context) { + securityGroup := securityGroupStub(namespace) + patch := baseSecurityGroupPatch(securityGroup) + sgRulePatchSpec := applyconfigv1alpha1.SecurityGroupRule().WithEthertype(orcv1alpha1.EtherTypeIPv4) + patch.Spec.WithResource(applyconfigv1alpha1.SecurityGroupResourceSpec().WithRules(sgRulePatchSpec.WithPortRangeMin(50).WithPortRangeMax(50))) + Expect(applyObj(ctx, securityGroup, patch)).NotTo(Succeed(), "create security group") + }) + + It("should permit valid port range min and max", func(ctx context.Context) { + securityGroup := securityGroupStub(namespace) + patch := baseSecurityGroupPatch(securityGroup) + sgRulePatchSpec := baseSGRulePatchSpec().WithProtocol(orcv1alpha1.ProtocolTCP) + patch.Spec.WithResource(applyconfigv1alpha1.SecurityGroupResourceSpec().WithRules(sgRulePatchSpec.WithPortRangeMin(22).WithPortRangeMax(23))) + Expect(applyObj(ctx, securityGroup, patch)).To(Succeed(), "create security group") + patch.Spec.WithResource(applyconfigv1alpha1.SecurityGroupResourceSpec().WithRules(sgRulePatchSpec.WithPortRangeMin(22).WithPortRangeMax(22))) + Expect(applyObj(ctx, securityGroup, patch)).To(Succeed(), "create security group") + }) + + It("should reject invalid port range min or max", func(ctx context.Context) { + securityGroup := securityGroupStub(namespace) + patch := baseSecurityGroupPatch(securityGroup) + sgRulePatchSpec := baseSGRulePatchSpec().WithProtocol(orcv1alpha1.ProtocolTCP) + patch.Spec.WithResource(applyconfigv1alpha1.SecurityGroupResourceSpec().WithRules(sgRulePatchSpec.WithPortRangeMin(51).WithPortRangeMax(50))) + Expect(applyObj(ctx, securityGroup, patch)).NotTo(Succeed(), "create security group") + patch.Spec.WithResource(applyconfigv1alpha1.SecurityGroupResourceSpec().WithRules(sgRulePatchSpec.WithPortRangeMax(50))) + Expect(applyObj(ctx, securityGroup, patch)).NotTo(Succeed(), "create security group") + patch.Spec.WithResource(applyconfigv1alpha1.SecurityGroupResourceSpec().WithRules(sgRulePatchSpec.WithPortRangeMin(51))) + Expect(applyObj(ctx, securityGroup, patch)).NotTo(Succeed(), "create security group") + }) + + It("should reject invalid CIDR for RemoteIPPrefix", func(ctx context.Context) { + securityGroup := securityGroupStub(namespace) + patch := baseSecurityGroupPatch(securityGroup) + sgRulePatchSpec := baseSGRulePatchSpec().WithProtocol(orcv1alpha1.ProtocolTCP) + patch.Spec.WithResource(applyconfigv1alpha1.SecurityGroupResourceSpec().WithRules(sgRulePatchSpec.WithPortRangeMin(22).WithPortRangeMax(22).WithRemoteIPPrefix("foo"))) + Expect(applyObj(ctx, securityGroup, patch)).NotTo(Succeed(), "create security group") + }) + + It("should reject CIDR for RemoteIPPrefix that doesn't match the ethertype", func(ctx context.Context) { + var sgRulePatchSpec *applyconfigv1alpha1.SecurityGroupRuleApplyConfiguration + securityGroup := securityGroupStub(namespace) + patch := baseSecurityGroupPatch(securityGroup) + sgRulePatchSpec = applyconfigv1alpha1.SecurityGroupRule().WithEthertype(orcv1alpha1.EtherTypeIPv6) + patch.Spec.WithResource(applyconfigv1alpha1.SecurityGroupResourceSpec().WithRules(sgRulePatchSpec.WithPortRangeMin(22).WithPortRangeMax(22).WithRemoteIPPrefix("192.168.0.1/24"))) + Expect(applyObj(ctx, securityGroup, patch)).NotTo(Succeed(), "create security group") + sgRulePatchSpec = applyconfigv1alpha1.SecurityGroupRule().WithEthertype(orcv1alpha1.EtherTypeIPv4) + patch.Spec.WithResource(applyconfigv1alpha1.SecurityGroupResourceSpec().WithRules(sgRulePatchSpec.WithPortRangeMin(22).WithPortRangeMax(22).WithRemoteIPPrefix("2001:db8::/47"))) + Expect(applyObj(ctx, securityGroup, patch)).NotTo(Succeed(), "create security group") + }) + + It("should permit valid CIDR that matches the ethertype", func(ctx context.Context) { + var sgRulePatchSpec *applyconfigv1alpha1.SecurityGroupRuleApplyConfiguration + securityGroup := securityGroupStub(namespace) + patch := baseSecurityGroupPatch(securityGroup) + sgRulePatchSpec = applyconfigv1alpha1.SecurityGroupRule().WithEthertype(orcv1alpha1.EtherTypeIPv4) + patch.Spec.WithResource(applyconfigv1alpha1.SecurityGroupResourceSpec().WithRules(sgRulePatchSpec.WithRemoteIPPrefix("192.168.0.1/24"))) + Expect(applyObj(ctx, securityGroup, patch)).To(Succeed(), "create security group") + sgRulePatchSpec = applyconfigv1alpha1.SecurityGroupRule().WithEthertype(orcv1alpha1.EtherTypeIPv6) + patch.Spec.WithResource(applyconfigv1alpha1.SecurityGroupResourceSpec().WithRules(sgRulePatchSpec.WithRemoteIPPrefix("2001:db8::/47"))) + Expect(applyObj(ctx, securityGroup, patch)).To(Succeed(), "create security group") + }) + +})