Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support immutable fields check for Subnet/Subnetset #627

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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
6 changes: 6 additions & 0 deletions pkg/controllers/subnet/subnet_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,12 @@ func (r *SubnetReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr
updateFail(r, &ctx, obj, err.Error())
return ResultNormal, nil
}
if errors.As(err, &util.ImmutableFieldModifyError{}) {
log.Error(err, "not allowed to modify immutable field, would not retry", "subnet", req.NamespacedName)
updateFail(r, &ctx, obj, err.Error())
return ResultNormal, nil
}

log.Error(err, "operate failed, would retry exponentially", "subnet", req.NamespacedName)
updateFail(r, &ctx, obj, "")
return ResultRequeue, err
Expand Down
17 changes: 16 additions & 1 deletion pkg/controllers/subnetset/subnetset_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import (
"github.com/vmware-tanzu/nsx-operator/pkg/metrics"
servicecommon "github.com/vmware-tanzu/nsx-operator/pkg/nsx/services/common"
"github.com/vmware-tanzu/nsx-operator/pkg/nsx/services/subnet"
"github.com/vmware-tanzu/nsx-operator/pkg/nsx/util"
)

var (
Expand Down Expand Up @@ -106,6 +107,19 @@ func (r *SubnetSetReconciler) Reconcile(ctx context.Context, req ctrl.Request) (
if err := r.SubnetService.UpdateSubnetSetTags(obj.Namespace, nsxSubnets, tags); err != nil {
log.Error(err, "failed to update subnetset tags")
}

// If there is already a nsx subnet allocated from the subnetset,
// It's needed to check immutable fields to avoid change.
isForVM := true
if obj.Name == servicecommon.DefaultPodSubnetSet {
isForVM = false
}
err := r.SubnetService.ValidateSubnetSetImmutableFields(obj, nsxSubnets[0], isForVM)
if errors.As(err, &util.ImmutableFieldModifyError{}) {
log.Error(err, "not allowed to modify immutable field, would not retry", "subnetset", req.NamespacedName)
updateFail(r, &ctx, obj, err.Error())
return ResultNormal, nil
}
}
updateSuccess(r, &ctx, obj)
} else {
Expand Down Expand Up @@ -306,7 +320,8 @@ func (r *SubnetSetReconciler) DeleteSubnetForSubnetSet(obj v1alpha1.SubnetSet, u

func StartSubnetSetController(mgr ctrl.Manager, subnetService *subnet.SubnetService,
subnetPortService servicecommon.SubnetPortServiceProvider, vpcService servicecommon.VPCServiceProvider,
enableWebhook bool) error {
enableWebhook bool,
) error {
subnetsetReconciler := &SubnetSetReconciler{
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),
Expand Down
3 changes: 3 additions & 0 deletions pkg/nsx/services/common/services.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import (
"sigs.k8s.io/controller-runtime/pkg/client"

"github.com/vmware/vsphere-automation-sdk-go/services/nsxt/model"

"github.com/vmware-tanzu/nsx-operator/pkg/apis/v1alpha1"
)

// VPCServiceProvider provides to methods other controllers and services.
Expand All @@ -27,6 +29,7 @@ type SubnetServiceProvider interface {
GetSubnetsByIndex(key, value string) []*model.VpcSubnet
CreateOrUpdateSubnet(obj client.Object, vpcInfo VPCResourceInfo, tags []model.Tag) (string, error)
GenerateSubnetNSTags(obj client.Object, nsUID string) []model.Tag
ValidateSubnetSetImmutableFields(*v1alpha1.SubnetSet, *model.VpcSubnet, bool) error
}

type SubnetPortServiceProvider interface {
Expand Down
18 changes: 10 additions & 8 deletions pkg/nsx/services/subnet/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,20 +50,22 @@ func (service *SubnetService) buildSubnet(obj client.Object, tags []model.Tag) (
switch o := obj.(type) {
case *v1alpha1.Subnet:
nsxSubnet = &model.VpcSubnet{
Id: String(service.BuildSubnetID(o)),
AccessMode: String(util.Capitalize(string(o.Spec.AccessMode))),
DhcpConfig: service.buildDHCPConfig(o.Spec.DHCPConfig.EnableDHCP, int64(o.Spec.IPv4SubnetSize-4)),
DisplayName: String(service.buildSubnetName(o)),
Id: String(service.BuildSubnetID(o)),
AccessMode: String(util.Capitalize(string(o.Spec.AccessMode))),
Ipv4SubnetSize: Int64(int64(o.Spec.IPv4SubnetSize)),
DhcpConfig: service.buildDHCPConfig(o.Spec.DHCPConfig.EnableDHCP, int64(o.Spec.IPv4SubnetSize-4)),
DisplayName: String(service.buildSubnetName(o)),
}
staticIpAllocation = o.Spec.AdvancedConfig.StaticIPAllocation.Enable
nsxSubnet.IpAddresses = o.Spec.IPAddresses
case *v1alpha1.SubnetSet:
index := uuid.NewString()
nsxSubnet = &model.VpcSubnet{
Id: String(service.buildSubnetSetID(o, index)),
AccessMode: String(util.Capitalize(string(o.Spec.AccessMode))),
DhcpConfig: service.buildDHCPConfig(o.Spec.DHCPConfig.EnableDHCP, int64(o.Spec.IPv4SubnetSize-4)),
DisplayName: String(service.buildSubnetSetName(o, index)),
Id: String(service.buildSubnetSetID(o, index)),
AccessMode: String(util.Capitalize(string(o.Spec.AccessMode))),
Ipv4SubnetSize: Int64(int64(o.Spec.IPv4SubnetSize)),
DhcpConfig: service.buildDHCPConfig(o.Spec.DHCPConfig.EnableDHCP, int64(o.Spec.IPv4SubnetSize-4)),
DisplayName: String(service.buildSubnetSetName(o, index)),
}
staticIpAllocation = o.Spec.AdvancedConfig.StaticIPAllocation.Enable
default:
Expand Down
60 changes: 60 additions & 0 deletions pkg/nsx/services/subnet/subnet.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"github.com/vmware-tanzu/nsx-operator/pkg/nsx/services/common"
"github.com/vmware-tanzu/nsx-operator/pkg/nsx/services/realizestate"
nsxutil "github.com/vmware-tanzu/nsx-operator/pkg/nsx/util"
"github.com/vmware-tanzu/nsx-operator/pkg/util"
)

var (
Expand Down Expand Up @@ -94,8 +95,16 @@ func (service *SubnetService) CreateOrUpdateSubnet(obj client.Object, vpcInfo co
if existingSubnet == nil {
changed = true
} else {
// If there is already a nsx subnet created,
// It's needed to check immutable fields to avoid change.
err = service.validateSubnetImmutableFields(existingSubnet, nsxSubnet)
if err != nil {
log.Error(err, "failed to validate Subnet immutable fields")
return "", err
}
changed = common.CompareResource(SubnetToComparable(existingSubnet), SubnetToComparable(nsxSubnet))
}

if !changed {
log.Info("subnet not changed, skip updating", "subnet.Id", uid)
return uid, nil
Expand Down Expand Up @@ -418,3 +427,54 @@ func (service *SubnetService) UpdateSubnetSetTags(ns string, vpcSubnets []*model
}
return nil
}

func (service *SubnetService) validateSubnetImmutableFields(existingNsxSubnet *model.VpcSubnet, nsxSubnet *model.VpcSubnet) error {
errorMsg := ""

if *existingNsxSubnet.AccessMode != *nsxSubnet.AccessMode {
errorMsg = fmt.Sprintf("Subnet CR AccessMode cannot be modified")
return nsxutil.ImmutableFieldModifyError{Desc: errorMsg}
}

if *existingNsxSubnet.DhcpConfig.EnableDhcp != *nsxSubnet.DhcpConfig.EnableDhcp {
errorMsg = fmt.Sprintf("Subnet CR DHCPConfig cannot be modified")
return nsxutil.ImmutableFieldModifyError{Desc: errorMsg}
}

if existingNsxSubnet.Ipv4SubnetSize != nil && nsxSubnet.Ipv4SubnetSize != nil {
if *existingNsxSubnet.Ipv4SubnetSize != *nsxSubnet.Ipv4SubnetSize {
errorMsg = fmt.Sprintf("Subnet CR Ipv4SubnetSize cannot be modified")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we shouldn't use the common logic in NSX operator to check the error. Consider the following case: the customer changed the subnet size from A to B, then the NSX operator updates the error on the status; however, the customer unluckily forgets the original correct size value A, then it seems that the subnet can never recover?
So it looks that we'd better use the webhook to do the check?

return nsxutil.ImmutableFieldModifyError{Desc: errorMsg}
}
}

if !util.CompareStringArrays(existingNsxSubnet.IpAddresses, nsxSubnet.IpAddresses) {
errorMsg = fmt.Sprintf("Subnet CR IpAddresses cannot be modified")
return nsxutil.ImmutableFieldModifyError{Desc: errorMsg}
}
return nil
}

func (service *SubnetService) ValidateSubnetSetImmutableFields(subnetSet *v1alpha1.SubnetSet, nsxSubnet *model.VpcSubnet, isForVM bool) error {
errorMsg := ""

if isForVM {
// If allocating the subnet from subnetset for POD, it's allowed to change the AccessMode.
// For VM subnet, it's disallowed to change AccessMode
if util.Capitalize(string(subnetSet.Spec.AccessMode)) != *(nsxSubnet.AccessMode) {
errorMsg = fmt.Sprintf("Subnetset CR AccessMode cannot be modified when VM Subnet exists")
return nsxutil.ImmutableFieldModifyError{Desc: errorMsg}
}
}

if subnetSet.Spec.DHCPConfig.EnableDHCP != *nsxSubnet.DhcpConfig.EnableDhcp {
errorMsg = fmt.Sprintf("Subnetset CR DHCPConfig cannot be modified when Subnet exists")
return nsxutil.ImmutableFieldModifyError{Desc: errorMsg}
}

if int64(subnetSet.Spec.IPv4SubnetSize) != *nsxSubnet.Ipv4SubnetSize {
errorMsg = fmt.Sprintf("Subnetset CR Ipv4SubnetSize cannot be modified when Subnet exists")
return nsxutil.ImmutableFieldModifyError{Desc: errorMsg}
}
return nil
}
6 changes: 6 additions & 0 deletions pkg/nsx/util/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -585,6 +585,12 @@ type ExceedTagsError struct {

func (err ExceedTagsError) Error() string { return err.Desc }

type ImmutableFieldModifyError struct {
Desc string
}

func (err ImmutableFieldModifyError) Error() string { return err.Desc }

type Status struct {
Code uint32
Message string
Expand Down
8 changes: 8 additions & 0 deletions pkg/util/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import (
"errors"
"fmt"
"net"
"slices"
"sort"
"strconv"
"strings"

Expand Down Expand Up @@ -511,3 +513,9 @@ func Capitalize(s string) string {
}
return strings.ToUpper(s[:1]) + s[1:]
}

func CompareStringArrays(arr1, arr2 []string) bool {
sort.Strings(arr1)
sort.Strings(arr2)
return slices.Equal(arr1, arr2)
}
22 changes: 21 additions & 1 deletion pkg/util/utils_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,6 @@ func TestUtil_IsNsInSystemNamespace(t *testing.T) {
ns = types.NamespacedName{Namespace: "sys-ns", Name: "dummy"}

isCRInSysNs, err = IsSystemNamespace(client, ns.Namespace, nil)

if err != nil {
t.Fatalf(err.Error())
}
Expand Down Expand Up @@ -639,3 +638,24 @@ func Test_calculateOffsetIP(t *testing.T) {
})
}
}

func Test_CompareStringArrays(t *testing.T) {
type args struct {
s1 []string
s2 []string
}
tests := []struct {
name string
args args
want bool
}{
{"1", args{[]string{"172.10.20.0/16", "172.20.30.0/16"}, []string{"172.20.30.0/16", "172.10.20.0/16"}}, true},
{"2", args{[]string{"172.10.20.0/16", "172.20.30.0/16"}, []string{"172.10.20.0/16", "172.10.40.0/16"}}, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equal(t, tt.want, CompareStringArrays(tt.args.s1, tt.args.s2), "CompareStringArrays(%v, %v)", tt.args.s1, tt.args.s2)
assert.Equal(t, tt.want, CompareStringArrays(tt.args.s1, tt.args.s2), "CompareStringArrays(%v, %v)", tt.args.s1, tt.args.s2)
})
}
}
2 changes: 1 addition & 1 deletion test/e2e/manifest/testVPC/customize_networkconfig.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ spec:
defaultGatewayPath: /infra/tier-0s/PLR
# nsx-operator-ci would replace '{edge-cluster-id}' with real edge-cluster-id of testbed
edgeClusterPath: /infra/sites/default/enforcement-points/default/edge-clusters/{edge-cluster-id}
defaultIPv4SubnetSize: 26
defaultIPv4SubnetSize: 32
nsxtProject: /orgs/default/projects/nsx_operator_e2e_test
externalIPv4Blocks:
- /infra/ip-blocks/e2e_test_external_ip_blk
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ spec:
defaultGatewayPath: /infra/tier-0s/PLR
# nsx-operator-ci would replace '{edge-cluster-id}' with real edge-cluster-id of testbed
edgeClusterPath: /infra/sites/default/enforcement-points/default/edge-clusters/{edge-cluster-id}
defaultIPv4SubnetSize: 26
defaultIPv4SubnetSize: 32
nsxtProject: /orgs/default/projects/nsx_operator_e2e_test
externalIPv4Blocks:
- /infra/ip-blocks/e2e_test_external_ip_blk
Expand Down
2 changes: 1 addition & 1 deletion test/e2e/manifest/testVPC/default_networkconfig.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ spec:
defaultGatewayPath: /infra/tier-0s/PLR
# nsx-operator-ci would replace '{edge-cluster-id}' with real edge-cluster-id of testbed
edgeClusterPath: /infra/sites/default/enforcement-points/default/edge-clusters/{edge-cluster-id}
defaultIPv4SubnetSize: 26
defaultIPv4SubnetSize: 32
nsxtProject: /orgs/default/projects/nsx_operator_e2e_test
externalIPv4Blocks:
- /infra/ip-blocks/e2e_test_external_ip_blk
Expand Down
2 changes: 1 addition & 1 deletion test/e2e/manifest/testVPC/system_networkconfig.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ spec:
defaultGatewayPath: /infra/tier-0s/PLR
# nsx-operator-ci would replace '{edge-cluster-id}' with real edge-cluster-id of testbed
edgeClusterPath: /infra/sites/default/enforcement-points/default/edge-clusters/{edge-cluster-id}
defaultIPv4SubnetSize: 26
defaultIPv4SubnetSize: 32
nsxtProject: /orgs/default/projects/nsx_operator_e2e_test
externalIPv4Blocks:
- /infra/ip-blocks/e2e_test_external_ip_blk
Expand Down
Loading