Skip to content

Commit ef23116

Browse files
committed
openstack: Implement network detection
Move the network detection logic from the legacy controller [1] in-tree. [1] https://github.com/openshift/cluster-api-provider-openstack/blob/19b666d6f/openshift/pkg/infraclustercontroller/openstackcluster.go#L262-L305 Signed-off-by: Stephen Finucane <[email protected]>
1 parent 2d14b30 commit ef23116

File tree

200 files changed

+30127
-2
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

200 files changed

+30127
-2
lines changed

go.mod

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ require (
1010
github.com/golangci/golangci-lint v1.64.8
1111
github.com/google/go-cmp v0.7.0
1212
github.com/google/uuid v1.6.0
13+
github.com/gophercloud/gophercloud/v2 v2.7.0
1314
github.com/klauspost/compress v1.18.0
1415
github.com/metal3-io/cluster-api-provider-metal3/api v1.10.1
1516
github.com/onsi/ginkgo/v2 v2.23.4
@@ -134,6 +135,7 @@ require (
134135
github.com/go-xmlfmt/xmlfmt v1.1.3 // indirect
135136
github.com/gobwas/glob v0.2.3 // indirect
136137
github.com/gofrs/flock v0.12.1 // indirect
138+
github.com/gofrs/uuid/v5 v5.3.0 // indirect
137139
github.com/gogo/protobuf v1.3.2 // indirect
138140
github.com/golang/mock v1.7.0-rc.1 // indirect
139141
github.com/golangci/dupl v0.0.0-20250308024227-f665c8d69b32 // indirect
@@ -147,7 +149,7 @@ require (
147149
github.com/google/cel-go v0.23.2 // indirect
148150
github.com/google/gnostic-models v0.6.9 // indirect
149151
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 // indirect
150-
github.com/gophercloud/gophercloud/v2 v2.7.0 // indirect
152+
github.com/gophercloud/utils/v2 v2.0.0-20241209100706-e3a3b7c07d26 // indirect
151153
github.com/gordonklaus/ineffassign v0.1.0 // indirect
152154
github.com/gostaticanalysis/analysisutil v0.7.1 // indirect
153155
github.com/gostaticanalysis/comment v1.5.0 // indirect
@@ -273,6 +275,7 @@ require (
273275
go.opentelemetry.io/otel/trace v1.36.0 // indirect
274276
go.opentelemetry.io/proto/otlp v1.6.0 // indirect
275277
go.uber.org/automaxprocs v1.6.0 // indirect
278+
go.uber.org/mock v0.5.2 // indirect
276279
go.uber.org/multierr v1.11.0 // indirect
277280
go.uber.org/zap v1.27.0 // indirect
278281
golang.org/x/crypto v0.39.0 // indirect

go.sum

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,8 @@ github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
225225
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
226226
github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E=
227227
github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0=
228+
github.com/gofrs/uuid/v5 v5.3.0 h1:m0mUMr+oVYUdxpMLgSYCZiXe7PuVPnI94+OMeVBNedk=
229+
github.com/gofrs/uuid/v5 v5.3.0/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8=
228230
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
229231
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
230232
github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
@@ -273,6 +275,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
273275
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
274276
github.com/gophercloud/gophercloud/v2 v2.7.0 h1:o0m4kgVcPgHlcXiWAjoVxGd8QCmvM5VU+YM71pFbn0E=
275277
github.com/gophercloud/gophercloud/v2 v2.7.0/go.mod h1:Ki/ILhYZr/5EPebrPL9Ej+tUg4lqx71/YH2JWVeU+Qk=
278+
github.com/gophercloud/utils/v2 v2.0.0-20241209100706-e3a3b7c07d26 h1:N65GYmx5LrMeYdeXcxMESDU+2pDyAOXlFNlHl7siUwM=
279+
github.com/gophercloud/utils/v2 v2.0.0-20241209100706-e3a3b7c07d26/go.mod h1:7SHUbtoiSYINNKgAVxse+PMhIio05IK7shHy8DVRaN0=
276280
github.com/gordonklaus/ineffassign v0.1.0 h1:y2Gd/9I7MdY1oEIt+n+rowjBNDcLQq3RsH5hwJd0f9s=
277281
github.com/gordonklaus/ineffassign v0.1.0/go.mod h1:Qcp2HIAYhR7mNUVSIxZww3Guk4it82ghYcEXIAk+QT0=
278282
github.com/gostaticanalysis/analysisutil v0.7.1 h1:ZMCjoue3DtDWQ5WyU16YbjbQEQ3VuzwxALrpYd+HeKk=
@@ -620,6 +624,8 @@ go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
620624
go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
621625
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
622626
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
627+
go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko=
628+
go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o=
623629
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
624630
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
625631
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=

pkg/controllers/infracluster/openstack.go

Lines changed: 188 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,28 +13,45 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1313
See the License for the specific language governing permissions and
1414
limitations under the License.
1515
*/
16+
1617
package infracluster
1718

1819
import (
1920
"context"
2021
"errors"
2122
"fmt"
23+
"net"
24+
"slices"
25+
"strings"
2226

2327
"github.com/go-logr/logr"
2428
configv1 "github.com/openshift/api/config/v1"
29+
mapiv1beta1 "github.com/openshift/api/machine/v1beta1"
2530
apierrors "k8s.io/apimachinery/pkg/api/errors"
2631
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2732
"k8s.io/klog/v2"
2833
"k8s.io/utils/ptr"
2934
clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
3035
"sigs.k8s.io/controller-runtime/pkg/client"
3136

37+
"github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/layer3/routers"
38+
"github.com/gophercloud/gophercloud/v2/openstack/networking/v2/ports"
39+
"github.com/gophercloud/gophercloud/v2/openstack/networking/v2/subnets"
3240
openstackv1 "sigs.k8s.io/cluster-api-provider-openstack/api/v1beta1"
41+
openstackclients "sigs.k8s.io/cluster-api-provider-openstack/pkg/clients"
42+
openstackscope "sigs.k8s.io/cluster-api-provider-openstack/pkg/scope"
3343
)
3444

3545
var (
3646
errUnsupportedOpenStackLoadBalancerType = errors.New("unsupported load balancer type for OpenStack")
3747
errOpenStackNoAPIServerInternalIPs = errors.New("no APIServerInternalIPs available")
48+
errOpenStackNoDefaultRouter = errors.New("unable to determine default router from control plane machines")
49+
errOpenStackNoDefaultSubnet = errors.New("unable to determine default subnet from control plane machines")
50+
errOpenStackNoControlPlaneMachines = errors.New("no control plane machines found")
51+
)
52+
53+
var (
54+
scopeCacheMaxSize int //nolint:gochecknoglobals
3855
)
3956

4057
// ensureOpenStackCluster ensures the OpenStackCluster object exists.
@@ -113,7 +130,6 @@ func (r *InfraClusterController) ensureOpenStackCluster(ctx context.Context, log
113130
},
114131
// NOTE(stephenfin): We deliberately don't add subnet here: CAPO will use all subnets in network,
115132
// which should also cover dual stack deployments. Everything else is populated below.
116-
// FIXME(stephenfin): Populate these.
117133
Network: nil,
118134
Router: nil,
119135
ExternalNetwork: nil,
@@ -122,6 +138,40 @@ func (r *InfraClusterController) ensureOpenStackCluster(ctx context.Context, log
122138
},
123139
}
124140

141+
// FIXME(stephenfin): Where can I source caCertificates from? The legacy infracluster controller
142+
// had the same issue.
143+
caCertificates := []byte{} // PEM encoded CA certificates
144+
scopeFactory := openstackscope.NewFactory(scopeCacheMaxSize)
145+
146+
scope, err := scopeFactory.NewClientScopeFromObject(ctx, r.Client, caCertificates, log, target)
147+
if err != nil {
148+
return nil, fmt.Errorf("creating OpenStack client: %w", err)
149+
}
150+
151+
networkClient, err := scope.NewNetworkClient()
152+
if err != nil {
153+
return nil, fmt.Errorf("creating OpenStack compute service: %w", err)
154+
}
155+
156+
defaultSubnet, err := getDefaultSubnetFromMachines(ctx, log, r.Client, networkClient, platformStatus)
157+
if err != nil {
158+
return nil, err
159+
}
160+
// NOTE(stephenfin): As noted previously, we deliberately *do not* add subnet here: CAPO will
161+
// use all subnets in network, which should also cover dual-stack deployments
162+
target.Spec.Network = &openstackv1.NetworkParam{ID: ptr.To(defaultSubnet.NetworkID)}
163+
164+
router, err := getDefaultRouterFromSubnet(ctx, networkClient, defaultSubnet)
165+
if err != nil {
166+
return nil, err
167+
}
168+
169+
target.Spec.Router = &openstackv1.RouterParam{ID: ptr.To(router.ID)}
170+
// NOTE(stephenfin): The only reason we set ExternalNetworkID in the cluster spec is to avoid
171+
// an error reconciling the external network if it isn't set. If CAPO ever no longer requires
172+
// this we can just not set it and remove much of the code above. We don't actually use it.
173+
target.Spec.ExternalNetwork = &openstackv1.NetworkParam{ID: ptr.To(router.GatewayInfo.NetworkID)}
174+
125175
if err := r.Create(ctx, target); err != nil {
126176
return nil, fmt.Errorf("failed to create InfraCluster: %w", err)
127177
}
@@ -130,3 +180,140 @@ func (r *InfraClusterController) ensureOpenStackCluster(ctx context.Context, log
130180

131181
return target, nil
132182
}
183+
184+
// getDefaultRouterFromSubnet attempts to infer the default router used for
185+
// the network by looking for ports with the given subnet and gateway IP
186+
// associated with them.
187+
func getDefaultRouterFromSubnet(_ context.Context, networkClient openstackclients.NetworkClient, subnet *subnets.Subnet) (*routers.Router, error) {
188+
// Find the port which owns the subnet's gateway IP
189+
ports, err := networkClient.ListPort(ports.ListOpts{
190+
NetworkID: subnet.NetworkID,
191+
FixedIPs: []ports.FixedIPOpts{
192+
{
193+
IPAddress: subnet.GatewayIP,
194+
// XXX: We should search on both subnet and IP
195+
// address here, but can't because of
196+
// https://github.com/gophercloud/gophercloud/issues/2807
197+
// SubnetID: subnet.ID,
198+
},
199+
},
200+
})
201+
if err != nil {
202+
return nil, fmt.Errorf("listing ports: %w", err)
203+
}
204+
205+
if len(ports) == 0 {
206+
return nil, fmt.Errorf("%w: no ports found for subnet %s", errOpenStackNoDefaultRouter, subnet.ID)
207+
}
208+
209+
if len(ports) > 1 {
210+
return nil, fmt.Errorf("%w: multiple ports found for subnet %s", errOpenStackNoDefaultRouter, subnet.ID)
211+
}
212+
213+
routerID := ports[0].DeviceID
214+
215+
router, err := networkClient.GetRouter(routerID)
216+
if err != nil {
217+
return nil, fmt.Errorf("getting router %s: %w", routerID, err)
218+
}
219+
220+
if router.GatewayInfo.NetworkID == "" {
221+
return nil, fmt.Errorf("%w: router %s does not have an external gateway", errOpenStackNoDefaultRouter, routerID)
222+
}
223+
224+
return router, nil
225+
}
226+
227+
// getDefaultSubnetFromMachines attempts to infer the default cluster subnet by
228+
// directly examining the control plane machines. Specifically it looks for a
229+
// subnet attached to a control plane machine whose CIDR contains the API
230+
// loadbalancer internal VIP.
231+
//
232+
// This heuristic is only valid when the API loadbalancer type is
233+
// LoadBalancerTypeOpenShiftManagedDefault.
234+
//
235+
//nolint:gocognit,funlen
236+
func getDefaultSubnetFromMachines(ctx context.Context, log logr.Logger, kubeclient client.Client, networkClient openstackclients.NetworkClient, platformStatus *configv1.OpenStackPlatformStatus) (*subnets.Subnet, error) {
237+
mapiMachines := mapiv1beta1.MachineList{}
238+
if err := kubeclient.List(
239+
ctx,
240+
&mapiMachines,
241+
client.InNamespace("openshift-machine-api"),
242+
client.MatchingLabels{"machine.openshift.io/cluster-api-machine-role": "master"},
243+
); err != nil {
244+
return nil, fmt.Errorf("listing control plane machines: %w", err)
245+
}
246+
247+
if len(mapiMachines.Items) == 0 {
248+
return nil, errOpenStackNoControlPlaneMachines
249+
}
250+
251+
apiServerInternalIPs := make([]net.IP, len(platformStatus.APIServerInternalIPs))
252+
for i, ipStr := range platformStatus.APIServerInternalIPs {
253+
apiServerInternalIPs[i] = net.ParseIP(ipStr)
254+
}
255+
256+
for _, mapiMachine := range mapiMachines.Items {
257+
log := log.WithValues("machine", mapiMachine.Name)
258+
259+
providerID := mapiMachine.Spec.ProviderID
260+
if providerID == nil {
261+
log.V(3).Info("Skipping machine: providerID is not set")
262+
continue
263+
}
264+
265+
if !strings.HasPrefix(*providerID, "openstack:///") {
266+
log.V(2).Info("Skipping machine: providerID has unexpected format", "providerID", *providerID)
267+
continue
268+
}
269+
270+
instanceID := (*providerID)[len("openstack:///"):]
271+
272+
portOpts := ports.ListOpts{
273+
DeviceID: instanceID,
274+
}
275+
276+
ports, err := networkClient.ListPort(portOpts)
277+
if err != nil {
278+
return nil, fmt.Errorf("listing ports for instance %s: %w", instanceID, err)
279+
}
280+
281+
if len(ports) == 0 {
282+
return nil, fmt.Errorf("%w: no ports found for instance %s", errOpenStackNoDefaultSubnet, instanceID)
283+
}
284+
285+
for _, port := range ports {
286+
log := log.WithValues("port", port.ID)
287+
288+
for _, fixedIP := range port.FixedIPs {
289+
if fixedIP.SubnetID == "" {
290+
continue
291+
}
292+
293+
subnet, err := networkClient.GetSubnet(fixedIP.SubnetID)
294+
if err != nil {
295+
return nil, fmt.Errorf("getting subnet %s: %w", fixedIP.SubnetID, err)
296+
}
297+
298+
_, cidr, err := net.ParseCIDR(subnet.CIDR)
299+
if err != nil {
300+
return nil, fmt.Errorf("parsing subnet CIDR %s: %w", subnet.CIDR, err)
301+
}
302+
303+
if slices.ContainsFunc(
304+
apiServerInternalIPs, func(ip net.IP) bool { return cidr.Contains(ip) },
305+
) {
306+
return subnet, nil
307+
}
308+
309+
log.V(6).Info("subnet does not match any APIServerInternalIPs", "subnet", subnet.CIDR)
310+
}
311+
312+
log.V(6).Info("port does not match any APIServerInternalIPs")
313+
}
314+
315+
log.V(6).Info("machine does not match any APIServerInternalIPs")
316+
}
317+
318+
return nil, fmt.Errorf("%w: no matching subnets found", errOpenStackNoDefaultSubnet)
319+
}

vendor/github.com/gofrs/uuid/v5/.gitignore

Lines changed: 15 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

vendor/github.com/gofrs/uuid/v5/.pre-commit-config.yaml

Lines changed: 14 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

vendor/github.com/gofrs/uuid/v5/LICENSE

Lines changed: 20 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)