Skip to content
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
2 changes: 1 addition & 1 deletion .github/workflows/e2e.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ jobs:
with:
enable_workaround_docker_io: 'false'
branch: ${{ matrix.openstack_version }}
enabled_services: "openstack-cli-server,neutron-trunk,neutron-port-trusted-vif"
enabled_services: "openstack-cli-server,neutron-trunk,neutron-port-trusted-vif,neutron-uplink-status-propagation"
conf_overrides: |
enable_plugin neutron https://github.com/openstack/neutron ${{ matrix.openstack_version }}

Expand Down
9 changes: 9 additions & 0 deletions api/v1alpha1/port_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,15 @@ type PortResourceSpec struct {
// +optional
// +kubebuilder:validation:XValidation:rule="self == oldSelf",message="valueSpecs is immutable"
ValueSpecs []PortValueSpec `json:"valueSpecs,omitempty"`

// propagateUplinkStatus represents the uplink status propagation of
// the port.
// The field is now immutable due to a limitation on
// Dalmatian (2024.2) release, we should address this later.
// https://github.com/k-orc/openstack-resource-controller/pull/641#discussion_r2905989136
// +optional
// +kubebuilder:validation:XValidation:rule="self == oldSelf",message="propagateUplinkStatus is immutable"
PropagateUplinkStatus *bool `json:"propagateUplinkStatus,omitempty"`
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

We've discussed this privately already, after you brought the issue to my attention: gophercloud implements the PropagateUplinkStatus in the response Port structure as a bool while it should really be a *bool with omitempty, and because of this we can't reliably know the status for PropagateUplinkStatus. I suggest that until the issue if fixed in gophercloud, we make that field immutable.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

It's been a while and I don't remember all of the discussion. From reading the PR comments again, I believe we're past the gophercloud limitation (thanks to custom unmarshalling) but we still need to make the field immutable because dalmatien doesn't support updating the field. If that's effectively the case, we should add that as a comment so we know when we can change that behavior.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yes, you're absolutely right. I'll add this comment.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done.

}

type PortResourceStatus struct {
Expand Down
5 changes: 5 additions & 0 deletions api/v1alpha1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions cmd/models-schema/zz_generated.openapi.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 11 additions & 0 deletions config/crd/bases/openstack.k-orc.cloud_ports.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -377,6 +377,17 @@ spec:
x-kubernetes-validations:
- message: projectRef is immutable
rule: self == oldSelf
propagateUplinkStatus:
description: |-
propagateUplinkStatus represents the uplink status propagation of
the port.
The field is now immutable due to a limitation on
Dalmatian (2024.2) release, we should address this later.
https://github.com/k-orc/openstack-resource-controller/pull/641#discussion_r2905989136
type: boolean
x-kubernetes-validations:
- message: propagateUplinkStatus is immutable
rule: self == oldSelf
securityGroupRefs:
description: |-
securityGroupRefs are references to the security groups associated
Expand Down
15 changes: 8 additions & 7 deletions internal/controllers/port/actuator.go
Original file line number Diff line number Diff line change
Expand Up @@ -249,13 +249,14 @@ func (actuator portActuator) CreateResource(ctx context.Context, obj *orcv1alpha
}

createOpts := ports.CreateOpts{
NetworkID: *network.Status.ID,
Name: getResourceName(obj),
Description: string(ptr.Deref(resource.Description, "")),
ProjectID: projectID,
AdminStateUp: resource.AdminStateUp,
MACAddress: resource.MACAddress,
ValueSpecs: valueSpecs,
NetworkID: *network.Status.ID,
Name: getResourceName(obj),
Description: string(ptr.Deref(resource.Description, "")),
ProjectID: projectID,
AdminStateUp: resource.AdminStateUp,
MACAddress: resource.MACAddress,
ValueSpecs: valueSpecs,
PropagateUplinkStatus: resource.PropagateUplinkStatus,
}

if len(resource.AllowedAddressPairs) > 0 {
Expand Down
1 change: 0 additions & 1 deletion internal/controllers/port/actuator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -462,7 +462,6 @@ func TestHandleTrustedVIFUpdate(t *testing.T) {
}

updateOpts := handlePortTrustedVIFUpdate(&ports.UpdateOpts{}, resource, osResource)

got, _ := needsUpdate(updateOpts)
if got != tt.expectChange {
t.Errorf("expected needsUpdate=%v, got %v", tt.expectChange, got)
Expand Down
5 changes: 4 additions & 1 deletion internal/controllers/port/status.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,6 @@ func (portStatusWriter) ApplyResourceStatus(log logr.Logger, osResource *osResou
WithNetworkID(osResource.NetworkID).
WithTags(osResource.Tags...).
WithSecurityGroups(osResource.SecurityGroups...).
WithPropagateUplinkStatus(osResource.PropagateUplinkStatus).
WithVNICType(osResource.VNICType).
WithPortSecurityEnabled(osResource.PortSecurityEnabled).
WithRevisionNumber(int64(osResource.RevisionNumber)).
Expand Down Expand Up @@ -108,5 +107,9 @@ func (portStatusWriter) ApplyResourceStatus(log logr.Logger, osResource *osResou
resourceStatus.WithTrustedVIF(*osResource.PortTrustedVIF)
}

if osResource.PropagateUplinkStatusPtr != nil {
resourceStatus.WithPropagateUplinkStatus(*osResource.PropagateUplinkStatusPtr)
}

statusApply.WithResource(resourceStatus)
}
Original file line number Diff line number Diff line change
Expand Up @@ -87,3 +87,4 @@ spec:
macAddress: fa:16:3e:23:fd:d7
hostID:
id: devstack
propagateUplinkStatus: false
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ status:
name: port-create-minimal
adminStateUp: true
portSecurityEnabled: true
propagateUplinkStatus: false
propagateUplinkStatus: true
Comment thread
winiciusallan marked this conversation as resolved.
revisionNumber: 1
status: DOWN
vnicType: normal
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ status:
description: Port from "create sriov" test
adminStateUp: true
portSecurityEnabled: false
propagateUplinkStatus: false
propagateUplinkStatus: true
status: DOWN
vnicType: direct
tags:
Expand Down
2 changes: 1 addition & 1 deletion internal/controllers/port/tests/port-update/00-assert.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ status:
name: port-update
adminStateUp: true
portSecurityEnabled: false
propagateUplinkStatus: false
propagateUplinkStatus: true
revisionNumber: 1
status: DOWN
vnicType: normal
Expand Down
1 change: 0 additions & 1 deletion internal/controllers/port/tests/port-update/01-assert.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ status:
description: port-update-updated
adminStateUp: true
portSecurityEnabled: true
propagateUplinkStatus: false
status: DOWN
vnicType: direct
allowedAddressPairs:
Expand Down
4 changes: 2 additions & 2 deletions internal/controllers/port/tests/port-update/02-assert.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ status:
name: port-update
adminStateUp: true
portSecurityEnabled: false
propagateUplinkStatus: false
propagateUplinkStatus: true
status: DOWN
vnicType: normal
conditions:
Expand All @@ -33,8 +33,8 @@ status:
status: "True"
reason: Success
- type: Progressing
message: OpenStack resource is up to date
status: "False"
message: OpenStack resource is up to date
reason: Success
---
apiVersion: openstack.k-orc.cloud/v1alpha1
Expand Down
46 changes: 44 additions & 2 deletions internal/osclients/networking.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package osclients

import (
"context"
"encoding/json"
"fmt"
"iter"

Expand Down Expand Up @@ -58,6 +59,30 @@ type PortExt struct {
portsecurity.PortSecurityExt
portsbinding.PortsBindingExt
portstrustedvif.PortTrustedVIFExt

PropagateUplinkStatusPtr *bool `json:"propagate_uplink_status,omitempty"`
}

func (p *PortExt) UnmarshalJSON(b []byte) error {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This custom unmarshaller omits portsecurity.PortSecurityExt or portsbinding.PortsBindingExt meaning that they'll always have the zero value (it's weird that we do not detect this bug via the tests, don't we have coverage for those fields?).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

It is something that I was in doubt about... I'm not sure, and if you can correct me if I'm wrong, I'll appreciate it, but it looks like Gophercloud uses reflection to populate the result rather than simply using a json.Unmarshal, am I wrong?

https://github.com/gophercloud/gophercloud/blob/main/results.go#L70

You're right that they will always have the zero value, but maybe in the above phase, the fields are populated using reflection. I need to better understand this. Any initial thought?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

A well crafted test should give us the answer.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Sorry for the late response.

The idea of this test is to show that using ports.Get(...).ExtractInto(&p), the fields are populated even though they're not explicitly set in the UnmarshalJSON function, as you highlighted in your initial comment, unlike using json.Unmarshal directly, which calls the custom Unmarshal and ignores the PortSecurityEnabled and PortBindings.

https://go.dev/play/p/iUMKGSjAFoo

Copy link
Copy Markdown
Contributor Author

@winiciusallan winiciusallan Apr 23, 2026

Choose a reason for hiding this comment

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

After understanding more about how the parse actually works, I've decided to add PortSecurityExt and PortsBindingExt to the Unmarshal function.

This helps us especially when listing ports because we are not using the default Gophercloud's Extract mechanisms, and we do need to populate fields using the unmarshaler function.

if err := json.Unmarshal(b, &p.Port); err != nil {
return err
}
if err := json.Unmarshal(b, &p.PortSecurityExt); err != nil {
return err
}
if err := json.Unmarshal(b, &p.PortsBindingExt); err != nil {
return err
}

var tmp struct {
PropagateUplinkStatusPtr *bool `json:"propagate_uplink_status"`
}
if err := json.Unmarshal(b, &tmp); err != nil {
return err
}

p.PropagateUplinkStatusPtr = tmp.PropagateUplinkStatusPtr
return nil
}

type NetworkClient interface {
Expand Down Expand Up @@ -182,13 +207,30 @@ func (c networkClient) UpdateFloatingIP(ctx context.Context, id string, opts flo

func (c networkClient) ListPort(ctx context.Context, opts ports.ListOptsBuilder) iter.Seq2[*PortExt, error] {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

We shouldn't have to update ListPort I believe.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

IIRC, there was a failing test, and the solution was to change the ListPort function, but I'll double-check this to confirm what exactly the error was.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

After discussion, we found a potential bug in Gophercloud, and a PR has been created. More information is there.

gophercloud/gophercloud#3726

extractPortExt := func(p pagination.Page) ([]PortExt, error) {
var resources []PortExt
err := ports.ExtractPortsInto(p, &resources)
bodyMap, ok := p.(ports.PortPage).Body.(map[string]interface{})
if !ok {
return nil, fmt.Errorf("unexpected body type: %T", p.(ports.PortPage).Body)
}

portsData, ok := bodyMap["ports"]
if !ok {
return nil, fmt.Errorf("ports key not found in response")
}

// Marshal and unmarshal to trigger UnmarshalJSON
jsonData, err := json.Marshal(portsData)
if err != nil {
return nil, err
}

var resources []PortExt
if err := json.Unmarshal(jsonData, &resources); err != nil {
return nil, err
}

return resources, nil
}

pager := ports.List(c.serviceClient, opts)
return func(yield func(*PortExt, error) bool) {
_ = pager.EachPage(ctx, yieldPage(extractPortExt, yield))
Expand Down
39 changes: 24 additions & 15 deletions pkg/clients/applyconfiguration/api/v1alpha1/portresourcespec.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions pkg/clients/applyconfiguration/internal/internal.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions website/docs/crd-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -2741,6 +2741,7 @@ _Appears in:_
| `hostID` _[HostID](#hostid)_ | hostID specifies the host where the port will be bound.<br />Note that when the port is attached to a server, OpenStack may<br />rebind the port to the server's actual compute host, which may<br />differ from the specified hostID if no matching scheduler hint<br />is used. In this case the port's status will reflect the actual<br />binding host, not the value specified here. | | MaxProperties: 1 <br />MinProperties: 1 <br />Optional: \{\} <br /> |
| `trustedVIF` _boolean_ | trustedVIF indicates whether the VF for the port will become<br />trusted by physical function to perform some privileged<br />operations. Only admin users can create ports with this field. | | Optional: \{\} <br /> |
| `valueSpecs` _[PortValueSpec](#portvaluespec) array_ | valueSpecs are extra parameters to include in the API request<br />with OpenStack. This is an extension point for the API, so what<br />they do and if they are supported, depends on the specific<br />OpenStack implementation. This was meant to work similar to the<br />property on Heat port resource. Since this depends on the<br />underlying implementation, we can't predict its fields, and<br />therefore, we don't know how to reconcile them in advance. Use<br />this field wisely and be aware of the expected behavior. | | MaxItems: 128 <br />Optional: \{\} <br /> |
| `propagateUplinkStatus` _boolean_ | propagateUplinkStatus represents the uplink status propagation of<br />the port.<br />The field is now immutable due to a limitation on<br />Dalmatian (2024.2) release, we should address this later.<br />https://github.com/k-orc/openstack-resource-controller/pull/641#discussion_r2905989136 | | Optional: \{\} <br /> |


#### PortResourceStatus
Expand Down
Loading