diff --git a/api/v1alpha2/linodemachine_types.go b/api/v1alpha2/linodemachine_types.go index 05869a332..c103c4811 100644 --- a/api/v1alpha2/linodemachine_types.go +++ b/api/v1alpha2/linodemachine_types.go @@ -67,7 +67,7 @@ type LinodeMachineSpec struct { PrivateIP *bool `json:"privateIP,omitempty"` // Tags is a list of tags to apply to the Linode instance. Tags []string `json:"tags,omitempty"` - // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="Value is immutable" + // FirewallID is the id of the cloud firewall to apply to the Linode Instance FirewallID int `json:"firewallID,omitempty"` // OSDisk is configuration for the root disk that includes the OS, // if not specified this defaults to whatever space is not taken up by the DataDisks diff --git a/api/v1alpha2/linodemachinetemplate_types.go b/api/v1alpha2/linodemachinetemplate_types.go index 7701de399..a67064d36 100644 --- a/api/v1alpha2/linodemachinetemplate_types.go +++ b/api/v1alpha2/linodemachinetemplate_types.go @@ -33,6 +33,10 @@ type LinodeMachineTemplateStatus struct { // +optional Tags []string `json:"tags,omitempty"` + // Firewall ID that is currently applied to the LinodeMachineTemplate. + // +optional + FirewallID int `json:"firewallID,omitempty"` + // Conditions represent the latest available observations of a LinodeMachineTemplate's current state. // +optional Conditions []metav1.Condition `json:"conditions,omitempty"` diff --git a/clients/clients.go b/clients/clients.go index 234450642..6b6a50507 100644 --- a/clients/clients.go +++ b/clients/clients.go @@ -55,6 +55,8 @@ type LinodeInstanceClient interface { GetRegion(ctx context.Context, regionID string) (*linodego.Region, error) GetImage(ctx context.Context, imageID string) (*linodego.Image, error) GetType(ctx context.Context, typeID string) (*linodego.LinodeType, error) + ListInstanceFirewalls(ctx context.Context, linodeID int, opts *linodego.ListOptions) ([]linodego.Firewall, error) + UpdateInstanceFirewalls(ctx context.Context, linodeID int, opts linodego.InstanceFirewallUpdateOptions) ([]linodego.Firewall, error) } // LinodeVPCClient defines the methods that interact with Linode's VPC service. diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_linodemachines.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_linodemachines.yaml index aaaefa4a5..32a92ace3 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_linodemachines.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_linodemachines.yaml @@ -164,10 +164,9 @@ spec: - message: Value is immutable rule: self == oldSelf firewallID: + description: FirewallID is the id of the cloud firewall to apply to + the Linode Instance type: integer - x-kubernetes-validations: - - message: Value is immutable - rule: self == oldSelf firewallRef: description: FirewallRef is a reference to a firewall object. This makes the linode use the specified firewall. diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_linodemachinetemplates.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_linodemachinetemplates.yaml index 9c674af88..52efc9f41 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_linodemachinetemplates.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_linodemachinetemplates.yaml @@ -154,10 +154,9 @@ spec: - message: Value is immutable rule: self == oldSelf firewallID: + description: FirewallID is the id of the cloud firewall to + apply to the Linode Instance type: integer - x-kubernetes-validations: - - message: Value is immutable - rule: self == oldSelf firewallRef: description: FirewallRef is a reference to a firewall object. This makes the linode use the specified firewall. @@ -541,6 +540,9 @@ spec: - type type: object type: array + firewallID: + description: Firewall ID that is currently applied to the LinodeMachineTemplate. + type: integer tags: description: tags that are currently applied to the LinodeMachineTemplate. items: diff --git a/docs/src/reference/out.md b/docs/src/reference/out.md index 2895d1ada..c9515e4e6 100644 --- a/docs/src/reference/out.md +++ b/docs/src/reference/out.md @@ -629,7 +629,7 @@ _Appears in:_ | `backupsEnabled` _boolean_ | | | | | `privateIP` _boolean_ | | | | | `tags` _string array_ | Tags is a list of tags to apply to the Linode instance. | | | -| `firewallID` _integer_ | | | | +| `firewallID` _integer_ | FirewallID is the id of the cloud firewall to apply to the Linode Instance | | | | `osDisk` _[InstanceDisk](#instancedisk)_ | OSDisk is configuration for the root disk that includes the OS,
if not specified this defaults to whatever space is not taken up by the DataDisks | | | | `dataDisks` _object (keys:string, values:[InstanceDisk](#instancedisk))_ | DataDisks is a map of any additional disks to add to an instance,
The sum of these disks + the OSDisk must not be more than allowed on a linodes plan | | | | `diskEncryption` _string_ | DiskEncryption determines if the disks of the instance should be encrypted. The default is disabled. | | Enum: [enabled disabled]
| @@ -755,6 +755,7 @@ _Appears in:_ | Field | Description | Default | Validation | | --- | --- | --- | --- | | `tags` _string array_ | tags that are currently applied to the LinodeMachineTemplate. | | | +| `firewallID` _integer_ | Firewall ID that is currently applied to the LinodeMachineTemplate. | | | | `conditions` _[Condition](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.32/#condition-v1-meta) array_ | Conditions represent the latest available observations of a LinodeMachineTemplate's current state. | | | diff --git a/e2e/linodemachine-controller/linodemachine-vpcref-integration/chainsaw-test.yaml b/e2e/linodemachine-controller/linodemachine-vpcref-integration/chainsaw-test.yaml index 085638aa5..9d19f16b6 100755 --- a/e2e/linodemachine-controller/linodemachine-vpcref-integration/chainsaw-test.yaml +++ b/e2e/linodemachine-controller/linodemachine-vpcref-integration/chainsaw-test.yaml @@ -43,6 +43,10 @@ spec: - describe: apiVersion: infrastructure.cluster.x-k8s.io/v1alpha2 kind: LinodeVPC + - podLogs: + namespace: capl-system + selector: control-plane=controller-manager + tail: 250 - name: Create LinodeMachine with VPCRef try: - apply: @@ -56,6 +60,10 @@ spec: - describe: apiVersion: cluster.x-k8s.io/v1beta1 kind: Machine + - podLogs: + namespace: capl-system + selector: control-plane=controller-manager + tail: 250 - name: Check if the Linodes & VPC were created try: - script: diff --git a/e2e/linodemachine-controller/minimal-linodemachine/chainsaw-test.yaml b/e2e/linodemachine-controller/minimal-linodemachine/chainsaw-test.yaml index aa2ea1925..5c69f2437 100755 --- a/e2e/linodemachine-controller/minimal-linodemachine/chainsaw-test.yaml +++ b/e2e/linodemachine-controller/minimal-linodemachine/chainsaw-test.yaml @@ -31,6 +31,10 @@ spec: - describe: apiVersion: cluster.x-k8s.io/v1beta1 kind: Cluster + - podLogs: + namespace: capl-system + selector: control-plane=controller-manager + tail: 250 - name: Create LinodeMachine resource try: - apply: @@ -41,6 +45,10 @@ spec: - describe: apiVersion: infrastructure.cluster.x-k8s.io/v1alpha2 kind: LinodeMachineTemplate + - podLogs: + namespace: capl-system + selector: control-plane=controller-manager + tail: 250 - describe: apiVersion: controlplane.cluster.x-k8s.io/v1beta1 kind: KubeadmControlPlane diff --git a/go.mod b/go.mod index fb61515bc..85f5420d0 100644 --- a/go.mod +++ b/go.mod @@ -15,7 +15,7 @@ require ( github.com/go-logr/logr v1.4.3 github.com/google/go-cmp v0.7.0 github.com/google/uuid v1.6.0 - github.com/linode/linodego v1.53.1-0.20250709175023-9b152d30578c + github.com/linode/linodego v1.53.1-0.20250728194520-172cba1c457a github.com/onsi/ginkgo/v2 v2.23.4 github.com/onsi/gomega v1.38.0 github.com/stretchr/testify v1.10.0 @@ -64,7 +64,7 @@ require ( github.com/x448/float16 v0.8.4 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 // indirect - golang.org/x/sync v0.15.0 // indirect + golang.org/x/sync v0.16.0 // indirect gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect k8s.io/apiserver v0.33.0 // indirect k8s.io/component-base v0.33.0 // indirect @@ -130,11 +130,11 @@ require ( go.uber.org/multierr v1.11.0 // indirect go.uber.org/ratelimit v0.2.0 // indirect go.uber.org/zap v1.27.0 // indirect - golang.org/x/net v0.41.0 // indirect + golang.org/x/net v0.42.0 // indirect golang.org/x/oauth2 v0.30.0 // indirect - golang.org/x/sys v0.33.0 // indirect - golang.org/x/term v0.32.0 // indirect - golang.org/x/text v0.26.0 // indirect + golang.org/x/sys v0.34.0 // indirect + golang.org/x/term v0.33.0 // indirect + golang.org/x/text v0.27.0 // indirect golang.org/x/time v0.9.0 // indirect golang.org/x/tools v0.34.0 // indirect gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect diff --git a/go.sum b/go.sum index e132d27fb..bcc169f9f 100644 --- a/go.sum +++ b/go.sum @@ -177,8 +177,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= -github.com/linode/linodego v1.53.1-0.20250709175023-9b152d30578c h1:WlZm+YNHBuphycMZG2s2+F04hx2wx1ShuOwPAIInjP8= -github.com/linode/linodego v1.53.1-0.20250709175023-9b152d30578c/go.mod h1:bI949fZaVchjWyKIA08hNyvAcV6BAS+PM2op3p7PAWA= +github.com/linode/linodego v1.53.1-0.20250728194520-172cba1c457a h1:5PaGcDTgxlOZOaYNChSKHnzZp4oKFvzqEn8TQ7hv2Pg= +github.com/linode/linodego v1.53.1-0.20250728194520-172cba1c457a/go.mod h1:VHlFAbhj18634Cd7B7L5D723kFKFQMOxzIutSMcWsB4= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= @@ -333,8 +333,8 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM= -golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U= +golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM= +golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY= golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= @@ -346,30 +346,30 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= -golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= +golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs= +golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8= golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8= -golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= +golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= -golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg= -golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ= +golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA= +golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/term v0.33.0 h1:NuFncQrRcaRvVmgRkvM3j/F00gWIAlcmlB8ACEKmGIg= +golang.org/x/term v0.33.0/go.mod h1:s18+ql9tYWp1IfpV9DmCtQDDSRBUjKaw9M1eAv5UeF0= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= -golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= +golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4= +golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU= golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/internal/controller/linodemachine_controller.go b/internal/controller/linodemachine_controller.go index 6405f3099..d4e36363b 100644 --- a/internal/controller/linodemachine_controller.go +++ b/internal/controller/linodemachine_controller.go @@ -772,6 +772,11 @@ func (r *LinodeMachineReconciler) reconcileUpdate(ctx context.Context, logger lo } } + res, err := r.reconcileFirewallID(ctx, logger, machineScope, instanceID) + if err != nil || !res.IsZero() { + return res, err + } + // Clean up bootstrap data after instance creation. if linodeInstance.Status == linodego.InstanceRunning && machineScope.Machine.Status.Phase == "Running" { if err := deleteBootstrapData(ctx, machineScope); err != nil { @@ -782,6 +787,41 @@ func (r *LinodeMachineReconciler) reconcileUpdate(ctx context.Context, logger lo return ctrl.Result{}, nil } +func (r *LinodeMachineReconciler) reconcileFirewallID(ctx context.Context, logger logr.Logger, machineScope *scope.MachineScope, instanceID int) (ctrl.Result, error) { + // get the instance's firewalls + firewalls, err := machineScope.LinodeClient.ListInstanceFirewalls(ctx, instanceID, nil) + if err != nil { + logger.Error(err, "Failed to list firewalls for Linode instance") + return ctrl.Result{RequeueAfter: reconciler.DefaultMachineControllerWaitForRunningDelay}, nil + } + + attachedFWIDs := make([]int, 0, len(firewalls)) + for _, fw := range firewalls { + attachedFWIDs = append(attachedFWIDs, fw.ID) + } + + var desiredFWIDs []int + if machineScope.LinodeMachine.Spec.FirewallID != 0 { + desiredFWIDs = []int{machineScope.LinodeMachine.Spec.FirewallID} + } else { + desiredFWIDs = []int{} + } + + // update the firewallID if needed. + if !slices.Equal(attachedFWIDs, desiredFWIDs) { + _, err := machineScope.LinodeClient.UpdateInstanceFirewalls(ctx, instanceID, + linodego.InstanceFirewallUpdateOptions{ + FirewallIDs: desiredFWIDs, + }, + ) + if err != nil { + logger.Error(err, "Failed to update firewalls for Linode instance") + return ctrl.Result{}, err + } + } + return ctrl.Result{}, nil +} + func (r *LinodeMachineReconciler) reconcileDelete( ctx context.Context, logger logr.Logger, diff --git a/internal/controller/linodemachine_controller_test.go b/internal/controller/linodemachine_controller_test.go index f01fbcfe0..0a8a0ad3c 100644 --- a/internal/controller/linodemachine_controller_test.go +++ b/internal/controller/linodemachine_controller_test.go @@ -1733,6 +1733,8 @@ var _ = Describe("machine-update", Ordered, Label("machine", "machine-update"), Status: linodego.InstanceProvisioning, Updated: util.Pointer(time.Now()), }, nil) + mck.LinodeClient.EXPECT().ListInstanceFirewalls(ctx, 11111, nil).Return( + []linodego.Firewall{}, nil) }), Result("machine status updated", func(ctx context.Context, mck Mock) { linodeMachine.Spec.ProviderID = util.Pointer("linode://11111") @@ -1777,6 +1779,8 @@ var _ = Describe("machine-update", Ordered, Label("machine", "machine-update"), Status: linodego.InstanceRunning, Updated: util.Pointer(time.Now()), }, nil) + mck.LinodeClient.EXPECT().ListInstanceFirewalls(ctx, 11111, nil).Return( + []linodego.Firewall{}, nil) }), Result("machine tag is updated", func(ctx context.Context, mck Mock) { linodeMachine.Spec.ProviderID = util.Pointer("linode://11111") @@ -1787,6 +1791,178 @@ var _ = Describe("machine-update", Ordered, Label("machine", "machine-update"), Expect(linodeMachine.Status.Tags).To(Equal([]string{"test-tag"})) }), ), + Path( + Call("machine firewall is updated", func(ctx context.Context, mck Mock) { + mck.LinodeClient.EXPECT().GetInstance(ctx, 11111).Return( + &linodego.Instance{ + ID: 11111, + IPv4: []*net.IP{ptr.To(net.IPv4(192, 168, 0, 2))}, + IPv6: "fd00::", + Tags: []string{"test-cluster-2"}, + Status: linodego.InstanceRunning, + Updated: util.Pointer(time.Now()), + }, nil) + mck.LinodeClient.EXPECT().UpdateInstance(ctx, 11111, gomock.Any()).Return( + &linodego.Instance{ + ID: 11111, + IPv4: []*net.IP{ptr.To(net.IPv4(192, 168, 0, 2))}, + IPv6: "fd00::", + Tags: []string{"test-cluster-2", "test-tag"}, + Status: linodego.InstanceRunning, + Updated: util.Pointer(time.Now()), + }, nil) + mck.LinodeClient.EXPECT().ListInstanceFirewalls(ctx, 11111, nil).Return( + []linodego.Firewall{ + {ID: 5}, // Instance currently has firewall ID 5 + }, nil) + mck.LinodeClient.EXPECT().UpdateInstanceFirewalls(ctx, 11111, linodego.InstanceFirewallUpdateOptions{ + FirewallIDs: []int{10}, // Update to firewall ID 10 + }).Return(nil, nil) + }), + Result("machine firewall is updated", func(ctx context.Context, mck Mock) { + linodeMachine.Spec.FirewallID = 10 // Set new firewall ID + _, err := reconciler.reconcile(ctx, logr.Logger{}, mScope) + Expect(err).NotTo(HaveOccurred()) + }), + ), + Path( + Call("machine firewall update applied when multiple firewall already attached", func(ctx context.Context, mck Mock) { + mck.LinodeClient.EXPECT().GetInstance(ctx, 11111).Return( + &linodego.Instance{ + ID: 11111, + IPv4: []*net.IP{ptr.To(net.IPv4(192, 168, 0, 2))}, + IPv6: "fd00::", + Tags: []string{"test-cluster-2"}, + Status: linodego.InstanceRunning, + Updated: util.Pointer(time.Now()), + }, nil) + mck.LinodeClient.EXPECT().UpdateInstance(ctx, 11111, gomock.Any()).Return( + &linodego.Instance{ + ID: 11111, + IPv4: []*net.IP{ptr.To(net.IPv4(192, 168, 0, 2))}, + IPv6: "fd00::", + Tags: []string{"test-cluster-2", "test-tag"}, + Status: linodego.InstanceRunning, + Updated: util.Pointer(time.Now()), + }, nil) + mck.LinodeClient.EXPECT().ListInstanceFirewalls(ctx, 11111, nil).Return( + []linodego.Firewall{ + {ID: 10}, // Instance already has the desired firewall ID 10 + {ID: 15}, // Additional firewall + }, nil) + mck.LinodeClient.EXPECT().UpdateInstanceFirewalls(ctx, 11111, linodego.InstanceFirewallUpdateOptions{ + FirewallIDs: []int{10}, // Update to firewall ID 10 + }).Return(nil, nil) + }), + Result("machine firewall update skipped when firewall already attached", func(ctx context.Context, mck Mock) { + linodeMachine.Spec.FirewallID = 10 // Firewall ID already attached + _, err := reconciler.reconcile(ctx, logr.Logger{}, mScope) + Expect(err).NotTo(HaveOccurred()) + }), + ), + Path( + Call("machine firewall update called even when FirewallID is zero", func(ctx context.Context, mck Mock) { + mck.LinodeClient.EXPECT().GetInstance(ctx, 11111).Return( + &linodego.Instance{ + ID: 11111, + IPv4: []*net.IP{ptr.To(net.IPv4(192, 168, 0, 2))}, + IPv6: "fd00::", + Tags: []string{"test-cluster-2"}, + Status: linodego.InstanceRunning, + Updated: util.Pointer(time.Now()), + }, nil) + mck.LinodeClient.EXPECT().UpdateInstance(ctx, 11111, gomock.Any()).Return( + &linodego.Instance{ + ID: 11111, + IPv4: []*net.IP{ptr.To(net.IPv4(192, 168, 0, 2))}, + IPv6: "fd00::", + Tags: []string{"test-cluster-2", "test-tag"}, + Status: linodego.InstanceRunning, + Updated: util.Pointer(time.Now()), + }, nil) + mck.LinodeClient.EXPECT().ListInstanceFirewalls(ctx, 11111, nil).Return( + []linodego.Firewall{ + {ID: 5}, // Instance has existing firewall + }, nil) + // UpdateInstanceFirewalls WILL be called since 0 is not in the attachedFirewalls list + mck.LinodeClient.EXPECT().UpdateInstanceFirewalls(ctx, 11111, linodego.InstanceFirewallUpdateOptions{ + FirewallIDs: []int{}, + }).Return(nil, nil) + }), + Result("machine firewall gets cleared when firewallID is set to 0", func(ctx context.Context, mck Mock) { + linodeMachine.Spec.FirewallID = 0 + _, err := reconciler.reconcile(ctx, logr.Logger{}, mScope) + Expect(err).NotTo(HaveOccurred()) + }), + ), + OneOf( + Path( + Call("machine firewall list fails", func(ctx context.Context, mck Mock) { + mck.LinodeClient.EXPECT().GetInstance(ctx, 11111).Return( + &linodego.Instance{ + ID: 11111, + IPv4: []*net.IP{ptr.To(net.IPv4(192, 168, 0, 2))}, + IPv6: "fd00::", + Tags: []string{"test-cluster-2"}, + Status: linodego.InstanceRunning, + Updated: util.Pointer(time.Now()), + }, nil) + mck.LinodeClient.EXPECT().UpdateInstance(ctx, 11111, gomock.Any()).Return( + &linodego.Instance{ + ID: 11111, + IPv4: []*net.IP{ptr.To(net.IPv4(192, 168, 0, 2))}, + IPv6: "fd00::", + Tags: []string{"test-cluster-2", "test-tag"}, + Status: linodego.InstanceRunning, + Updated: util.Pointer(time.Now()), + }, nil) + mck.LinodeClient.EXPECT().ListInstanceFirewalls(ctx, 11111, nil).Return( + nil, &linodego.Error{Code: http.StatusInternalServerError}) + }), + Result("machine firewall list error requeues", func(ctx context.Context, mck Mock) { + linodeMachine.Spec.FirewallID = 10 + res, err := reconciler.reconcile(ctx, mck.Logger(), mScope) + Expect(err).NotTo(HaveOccurred()) + Expect(res.RequeueAfter).To(Equal(rutil.DefaultMachineControllerWaitForRunningDelay)) + Expect(mck.Logs()).To(ContainSubstring("Failed to list firewalls for Linode instance")) + }), + ), + Path( + Call("machine firewall update fails", func(ctx context.Context, mck Mock) { + mck.LinodeClient.EXPECT().GetInstance(ctx, 11111).Return( + &linodego.Instance{ + ID: 11111, + IPv4: []*net.IP{ptr.To(net.IPv4(192, 168, 0, 2))}, + IPv6: "fd00::", + Tags: []string{"test-cluster-2"}, + Status: linodego.InstanceRunning, + Updated: util.Pointer(time.Now()), + }, nil) + mck.LinodeClient.EXPECT().UpdateInstance(ctx, 11111, gomock.Any()).Return( + &linodego.Instance{ + ID: 11111, + IPv4: []*net.IP{ptr.To(net.IPv4(192, 168, 0, 2))}, + IPv6: "fd00::", + Tags: []string{"test-cluster-2", "test-tag"}, + Status: linodego.InstanceRunning, + Updated: util.Pointer(time.Now()), + }, nil) + mck.LinodeClient.EXPECT().ListInstanceFirewalls(ctx, 11111, nil).Return( + []linodego.Firewall{ + {ID: 5}, // Instance currently has firewall ID 5 + }, nil) + mck.LinodeClient.EXPECT().UpdateInstanceFirewalls(ctx, 11111, linodego.InstanceFirewallUpdateOptions{ + FirewallIDs: []int{10}, + }).Return(nil, &linodego.Error{Code: http.StatusBadRequest}) + }), + Result("machine firewall update error requeues", func(ctx context.Context, mck Mock) { + linodeMachine.Spec.FirewallID = 10 + _, err := reconciler.reconcile(ctx, mck.Logger(), mScope) + Expect(err).To(HaveOccurred()) + Expect(mck.Logs()).To(ContainSubstring("Failed to update firewalls for Linode instance")) + }), + ), + ), ) }) diff --git a/internal/controller/linodemachinetemplate_controller.go b/internal/controller/linodemachinetemplate_controller.go index f28b85eaf..dfad855f0 100644 --- a/internal/controller/linodemachinetemplate_controller.go +++ b/internal/controller/linodemachinetemplate_controller.go @@ -129,10 +129,20 @@ func (lmtr *LinodeMachineTemplateReconciler) reconcile(ctx context.Context, lmtS if !slices.Equal(lmtScope.LinodeMachineTemplate.Spec.Template.Spec.Tags, lmtScope.LinodeMachineTemplate.Status.Tags) { err := lmtr.reconcileTags(ctx, lmtScope.LinodeMachineTemplate, &machine) if err != nil { - lmtr.Logger.Error(err, "Failed to add tags to LinodeMachine", "template", lmtScope.LinodeMachineTemplate.Name, "machine", machine.Name) + lmtr.Logger.Error(err, "Failed to update tags on LinodeMachine", "template", lmtScope.LinodeMachineTemplate.Name, "machine", machine.Name) outErr = errors.Join(outErr, err) + failureReason = "FailedToPatchLinodeMachine" + return ctrl.Result{}, outErr + } + } + if lmtScope.LinodeMachineTemplate.Spec.Template.Spec.FirewallID != lmtScope.LinodeMachineTemplate.Status.FirewallID { + err := lmtr.reconcileFirewallID(ctx, lmtScope.LinodeMachineTemplate, &machine) + if err != nil { + lmtr.Logger.Error(err, "Failed to update FirewallID on LinodeMachine", "template", lmtScope.LinodeMachineTemplate.Name, "machine", machine.Name) + outErr = errors.Join(outErr, err) failureReason = "FailedToPatchLinodeMachine" + return ctrl.Result{}, outErr } } } @@ -142,9 +152,10 @@ func (lmtr *LinodeMachineTemplateReconciler) reconcile(ctx context.Context, lmtS return ctrl.Result{}, nil } - // update the LMT status.tags if all the linodeMachines spec.tags is successfully updated. + // update the LMT status if all the linodeMachines are successfully updated. if outErr == nil { lmtScope.LinodeMachineTemplate.Status.Tags = slices.Clone(lmtScope.LinodeMachineTemplate.Spec.Template.Spec.Tags) + lmtScope.LinodeMachineTemplate.Status.FirewallID = lmtScope.LinodeMachineTemplate.Spec.Template.Spec.FirewallID lmtr.Logger.Info("Successfully reconciled LinodeMachineTemplate", "name", lmtScope.LinodeMachineTemplate.Name) } else { lmtr.Logger.Error(outErr, "Error in reconciling LinodeMachineTemplate, retrying..", "name", lmtScope.LinodeMachineTemplate.Name) @@ -159,11 +170,23 @@ func (lmtr *LinodeMachineTemplateReconciler) reconcileTags(ctx context.Context, } machine.Spec.Tags = lmt.Spec.Template.Spec.Tags - if err := helper.Patch(ctx, machine); err != nil { return fmt.Errorf("failed to patch LinodeMachine %s with new tags: %w", machine.Name, err) } lmtr.Logger.Info("Patched LinodeMachine with new tags", "machine", machine.Name, "tags", lmt.Spec.Template.Spec.Tags) + + return nil +} + +func (lmtr *LinodeMachineTemplateReconciler) reconcileFirewallID(ctx context.Context, lmt *infrav1alpha2.LinodeMachineTemplate, machine *infrav1alpha2.LinodeMachine) error { + helper, err := patch.NewHelper(machine, lmtr.Client) + if err != nil { + return fmt.Errorf("failed to init patch helper: %w", err) + } + machine.Spec.FirewallID = lmt.Spec.Template.Spec.FirewallID + if err := helper.Patch(ctx, machine); err != nil { + return fmt.Errorf("failed to patch LinodeMachine %s with new firewallID: %w", machine.Name, err) + } return nil } diff --git a/internal/controller/linodemachinetemplate_controller_test.go b/internal/controller/linodemachinetemplate_controller_test.go index 94df095f8..092921bfd 100644 --- a/internal/controller/linodemachinetemplate_controller_test.go +++ b/internal/controller/linodemachinetemplate_controller_test.go @@ -78,6 +78,35 @@ var _ = Describe("lifecycle", Ordered, Label("LinodeMachineTemplateReconciler", Tags: []string{"test-tag1"}, }, }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "machine-template-with-firewall-id", + Namespace: "default", + }, + Spec: infrav1alpha2.LinodeMachineTemplateSpec{ + Template: infrav1alpha2.LinodeMachineTemplateResource{ + Spec: infrav1alpha2.LinodeMachineSpec{ + FirewallID: 12345, + }, + }, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "machine-template-no-firewall-change", + Namespace: "default", + }, + Spec: infrav1alpha2.LinodeMachineTemplateSpec{ + Template: infrav1alpha2.LinodeMachineTemplateResource{ + Spec: infrav1alpha2.LinodeMachineSpec{ + FirewallID: 67890, + }, + }, + }, + Status: infrav1alpha2.LinodeMachineTemplateStatus{ + FirewallID: 67890, + }, + }, } linodeMachines := []infrav1alpha2.LinodeMachine{ @@ -99,6 +128,24 @@ var _ = Describe("lifecycle", Ordered, Label("LinodeMachineTemplateReconciler", }, }, }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "machine-3", + Namespace: "default", + Annotations: map[string]string{ + clusterv1.TemplateClonedFromNameAnnotation: "machine-template-with-firewall-id", + }, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "machine-4", + Namespace: "default", + Annotations: map[string]string{ + clusterv1.TemplateClonedFromNameAnnotation: "machine-template-no-firewall-change", + }, + }, + }, } BeforeAll(func(ctx SpecContext) { @@ -198,6 +245,54 @@ var _ = Describe("lifecycle", Ordered, Label("LinodeMachineTemplateReconciler", Expect(mck.Logs()).NotTo(ContainSubstring("Patched LinodeMachine with new tags")) }), ), + Path( + Call("machine template update firewall ID", func(ctx context.Context, mck Mock) {}), + Result("success", func(ctx context.Context, mck Mock) { + lmtScope, _ := scope.NewMachineTemplateScope( + ctx, + scope.MachineTemplateScopeParams{ + Client: k8sClient, + LinodeMachineTemplate: &machineTemplates[3], + }, + ) + reconciler = LinodeMachineTemplateReconciler{ + Logger: mck.Logger(), + Client: k8sClient, + } + + res, err := reconciler.reconcile(ctx, lmtScope) + Expect(err).NotTo(HaveOccurred()) + Expect(res).To(Equal(ctrl.Result{})) + + // get the updated machineTemplate + updatedMachineTemplate := &infrav1alpha2.LinodeMachineTemplate{} + Expect(k8sClient.Get(ctx, client.ObjectKey{ + Name: machineTemplates[3].Name, + Namespace: machineTemplates[3].Namespace, + }, updatedMachineTemplate)).To(Succeed()) + Expect(updatedMachineTemplate.Status.FirewallID).To(Equal(updatedMachineTemplate.Spec.Template.Spec.FirewallID)) + }), + ), + Path( + Call("machine template no firewall ID update", func(ctx context.Context, mck Mock) {}), + Result("success", func(ctx context.Context, mck Mock) { + patchHelper, err := patch.NewHelper(&machineTemplates[4], k8sClient) + Expect(err).NotTo(HaveOccurred()) + + lmtScope = scope.MachineTemplateScope{ + PatchHelper: patchHelper, + LinodeMachineTemplate: &machineTemplates[4], + } + reconciler = LinodeMachineTemplateReconciler{ + Logger: mck.Logger(), + Client: k8sClient, + } + + res, err := reconciler.reconcile(ctx, &lmtScope) + Expect(err).NotTo(HaveOccurred()) + Expect(res).To(Equal(ctrl.Result{})) + }), + ), ), ) diff --git a/mock/client.go b/mock/client.go index f70305470..b6f1f4c34 100644 --- a/mock/client.go +++ b/mock/client.go @@ -696,6 +696,21 @@ func (mr *MockLinodeClientMockRecorder) ListInstanceConfigs(ctx, linodeID, opts return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListInstanceConfigs", reflect.TypeOf((*MockLinodeClient)(nil).ListInstanceConfigs), ctx, linodeID, opts) } +// ListInstanceFirewalls mocks base method. +func (m *MockLinodeClient) ListInstanceFirewalls(ctx context.Context, linodeID int, opts *linodego.ListOptions) ([]linodego.Firewall, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListInstanceFirewalls", ctx, linodeID, opts) + ret0, _ := ret[0].([]linodego.Firewall) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListInstanceFirewalls indicates an expected call of ListInstanceFirewalls. +func (mr *MockLinodeClientMockRecorder) ListInstanceFirewalls(ctx, linodeID, opts any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListInstanceFirewalls", reflect.TypeOf((*MockLinodeClient)(nil).ListInstanceFirewalls), ctx, linodeID, opts) +} + // ListInstances mocks base method. func (m *MockLinodeClient) ListInstances(ctx context.Context, opts *linodego.ListOptions) ([]linodego.Instance, error) { m.ctrl.T.Helper() @@ -886,6 +901,21 @@ func (mr *MockLinodeClientMockRecorder) UpdateInstanceConfig(ctx, linodeID, conf return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateInstanceConfig", reflect.TypeOf((*MockLinodeClient)(nil).UpdateInstanceConfig), ctx, linodeID, configID, opts) } +// UpdateInstanceFirewalls mocks base method. +func (m *MockLinodeClient) UpdateInstanceFirewalls(ctx context.Context, linodeID int, opts linodego.InstanceFirewallUpdateOptions) ([]linodego.Firewall, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpdateInstanceFirewalls", ctx, linodeID, opts) + ret0, _ := ret[0].([]linodego.Firewall) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// UpdateInstanceFirewalls indicates an expected call of UpdateInstanceFirewalls. +func (mr *MockLinodeClientMockRecorder) UpdateInstanceFirewalls(ctx, linodeID, opts any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateInstanceFirewalls", reflect.TypeOf((*MockLinodeClient)(nil).UpdateInstanceFirewalls), ctx, linodeID, opts) +} + // UpdateObjectStorageBucketAccess mocks base method. func (m *MockLinodeClient) UpdateObjectStorageBucketAccess(ctx context.Context, clusterOrRegionID, label string, opts linodego.ObjectStorageBucketUpdateAccessOptions) error { m.ctrl.T.Helper() @@ -1291,6 +1321,21 @@ func (mr *MockLinodeInstanceClientMockRecorder) ListInstanceConfigs(ctx, linodeI return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListInstanceConfigs", reflect.TypeOf((*MockLinodeInstanceClient)(nil).ListInstanceConfigs), ctx, linodeID, opts) } +// ListInstanceFirewalls mocks base method. +func (m *MockLinodeInstanceClient) ListInstanceFirewalls(ctx context.Context, linodeID int, opts *linodego.ListOptions) ([]linodego.Firewall, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListInstanceFirewalls", ctx, linodeID, opts) + ret0, _ := ret[0].([]linodego.Firewall) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListInstanceFirewalls indicates an expected call of ListInstanceFirewalls. +func (mr *MockLinodeInstanceClientMockRecorder) ListInstanceFirewalls(ctx, linodeID, opts any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListInstanceFirewalls", reflect.TypeOf((*MockLinodeInstanceClient)(nil).ListInstanceFirewalls), ctx, linodeID, opts) +} + // ListInstances mocks base method. func (m *MockLinodeInstanceClient) ListInstances(ctx context.Context, opts *linodego.ListOptions) ([]linodego.Instance, error) { m.ctrl.T.Helper() @@ -1350,6 +1395,21 @@ func (mr *MockLinodeInstanceClientMockRecorder) UpdateInstanceConfig(ctx, linode return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateInstanceConfig", reflect.TypeOf((*MockLinodeInstanceClient)(nil).UpdateInstanceConfig), ctx, linodeID, configID, opts) } +// UpdateInstanceFirewalls mocks base method. +func (m *MockLinodeInstanceClient) UpdateInstanceFirewalls(ctx context.Context, linodeID int, opts linodego.InstanceFirewallUpdateOptions) ([]linodego.Firewall, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpdateInstanceFirewalls", ctx, linodeID, opts) + ret0, _ := ret[0].([]linodego.Firewall) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// UpdateInstanceFirewalls indicates an expected call of UpdateInstanceFirewalls. +func (mr *MockLinodeInstanceClientMockRecorder) UpdateInstanceFirewalls(ctx, linodeID, opts any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateInstanceFirewalls", reflect.TypeOf((*MockLinodeInstanceClient)(nil).UpdateInstanceFirewalls), ctx, linodeID, opts) +} + // MockLinodeVPCClient is a mock of LinodeVPCClient interface. type MockLinodeVPCClient struct { ctrl *gomock.Controller diff --git a/observability/wrappers/linodeclient/linodeclient.gen.go b/observability/wrappers/linodeclient/linodeclient.gen.go index bdb352d8e..2a8ff21e5 100644 --- a/observability/wrappers/linodeclient/linodeclient.gen.go +++ b/observability/wrappers/linodeclient/linodeclient.gen.go @@ -1144,6 +1144,32 @@ func (_d LinodeClientWithTracing) ListInstanceConfigs(ctx context.Context, linod return _d.LinodeClient.ListInstanceConfigs(ctx, linodeID, opts) } +// ListInstanceFirewalls implements _sourceClients.LinodeClient +func (_d LinodeClientWithTracing) ListInstanceFirewalls(ctx context.Context, linodeID int, opts *linodego.ListOptions) (fa1 []linodego.Firewall, err error) { + ctx, _span := tracing.Start(ctx, "_sourceClients.LinodeClient.ListInstanceFirewalls") + defer func() { + if _d._spanDecorator != nil { + _d._spanDecorator(_span, map[string]interface{}{ + "ctx": ctx, + "linodeID": linodeID, + "opts": opts}, map[string]interface{}{ + "fa1": fa1, + "err": err}) + } + + if err != nil { + _span.RecordError(err) + _span.SetAttributes( + attribute.String("event", "error"), + attribute.String("message", err.Error()), + ) + } + + _span.End() + }() + return _d.LinodeClient.ListInstanceFirewalls(ctx, linodeID, opts) +} + // ListInstances implements _sourceClients.LinodeClient func (_d LinodeClientWithTracing) ListInstances(ctx context.Context, opts *linodego.ListOptions) (ia1 []linodego.Instance, err error) { ctx, _span := tracing.Start(ctx, "_sourceClients.LinodeClient.ListInstances") @@ -1430,6 +1456,32 @@ func (_d LinodeClientWithTracing) UpdateInstanceConfig(ctx context.Context, lino return _d.LinodeClient.UpdateInstanceConfig(ctx, linodeID, configID, opts) } +// UpdateInstanceFirewalls implements _sourceClients.LinodeClient +func (_d LinodeClientWithTracing) UpdateInstanceFirewalls(ctx context.Context, linodeID int, opts linodego.InstanceFirewallUpdateOptions) (fa1 []linodego.Firewall, err error) { + ctx, _span := tracing.Start(ctx, "_sourceClients.LinodeClient.UpdateInstanceFirewalls") + defer func() { + if _d._spanDecorator != nil { + _d._spanDecorator(_span, map[string]interface{}{ + "ctx": ctx, + "linodeID": linodeID, + "opts": opts}, map[string]interface{}{ + "fa1": fa1, + "err": err}) + } + + if err != nil { + _span.RecordError(err) + _span.SetAttributes( + attribute.String("event", "error"), + attribute.String("message", err.Error()), + ) + } + + _span.End() + }() + return _d.LinodeClient.UpdateInstanceFirewalls(ctx, linodeID, opts) +} + // UpdateObjectStorageBucketAccess implements _sourceClients.LinodeClient func (_d LinodeClientWithTracing) UpdateObjectStorageBucketAccess(ctx context.Context, clusterOrRegionID string, label string, opts linodego.ObjectStorageBucketUpdateAccessOptions) (err error) { ctx, _span := tracing.Start(ctx, "_sourceClients.LinodeClient.UpdateObjectStorageBucketAccess")