Skip to content

Commit 5eec1e7

Browse files
authored
Implement instance v2 interfaces (kubernetes#2133)
* implement instances v2 * fix listopts to use regexp, remove dead code
1 parent a74385e commit 5eec1e7

File tree

3 files changed

+201
-6
lines changed

3 files changed

+201
-6
lines changed

pkg/openstack/instances.go

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -131,13 +131,11 @@ func sortNodeAddresses(addresses []v1.NodeAddress, addressSortOrder string) {
131131
}
132132

133133
// Instances returns an implementation of Instances for OpenStack.
134+
// TODO: v1 instance apis can be deleted after the v2 is verified enough
134135
func (os *OpenStack) Instances() (cloudprovider.Instances, bool) {
135-
return os.instances()
136-
}
137-
138-
// InstancesV2 returns an implementation of InstancesV2 for OpenStack.
139-
// TODO: Support InstancesV2 in the future.
140-
func (os *OpenStack) InstancesV2() (cloudprovider.InstancesV2, bool) {
136+
if os.useV1Instances {
137+
return os.instances()
138+
}
141139
return nil, false
142140
}
143141

pkg/openstack/instancesv2.go

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
/*
2+
Copyright 2023 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package openstack
18+
19+
import (
20+
"context"
21+
"fmt"
22+
sysos "os"
23+
24+
"github.com/gophercloud/gophercloud"
25+
"github.com/gophercloud/gophercloud/openstack/compute/v2/servers"
26+
v1 "k8s.io/api/core/v1"
27+
cloudprovider "k8s.io/cloud-provider"
28+
"k8s.io/cloud-provider-openstack/pkg/client"
29+
"k8s.io/cloud-provider-openstack/pkg/metrics"
30+
"k8s.io/cloud-provider-openstack/pkg/util/errors"
31+
"k8s.io/klog/v2"
32+
)
33+
34+
// InstancesV2 encapsulates an implementation of InstancesV2 for OpenStack.
35+
type InstancesV2 struct {
36+
compute *gophercloud.ServiceClient
37+
region string
38+
regionProviderID bool
39+
networkingOpts NetworkingOpts
40+
}
41+
42+
// InstancesV2 returns an implementation of InstancesV2 for OpenStack.
43+
func (os *OpenStack) InstancesV2() (cloudprovider.InstancesV2, bool) {
44+
if !os.useV1Instances {
45+
return os.instancesv2()
46+
}
47+
return nil, false
48+
}
49+
50+
func (os *OpenStack) instancesv2() (*InstancesV2, bool) {
51+
klog.V(4).Info("openstack.Instancesv2() called")
52+
53+
compute, err := client.NewComputeV2(os.provider, os.epOpts)
54+
if err != nil {
55+
klog.Errorf("unable to access compute v2 API : %v", err)
56+
return nil, false
57+
}
58+
59+
regionalProviderID := false
60+
if isRegionalProviderID := sysos.Getenv(RegionalProviderIDEnv); isRegionalProviderID == "true" {
61+
regionalProviderID = true
62+
}
63+
64+
return &InstancesV2{
65+
compute: compute,
66+
region: os.epOpts.Region,
67+
regionProviderID: regionalProviderID,
68+
networkingOpts: os.networkingOpts,
69+
}, true
70+
}
71+
72+
// InstanceExists indicates whether a given node exists according to the cloud provider
73+
func (i *InstancesV2) InstanceExists(ctx context.Context, node *v1.Node) (bool, error) {
74+
_, err := i.getInstance(ctx, node)
75+
if err == cloudprovider.InstanceNotFound {
76+
klog.V(6).Infof("instance not found for node: %s", node.Name)
77+
return false, nil
78+
}
79+
80+
if err != nil {
81+
return false, err
82+
}
83+
84+
return true, nil
85+
}
86+
87+
// InstanceShutdown returns true if the instance is shutdown according to the cloud provider.
88+
func (i *InstancesV2) InstanceShutdown(ctx context.Context, node *v1.Node) (bool, error) {
89+
server, err := i.getInstance(ctx, node)
90+
if err != nil {
91+
return false, err
92+
}
93+
94+
// SHUTOFF is the only state where we can detach volumes immediately
95+
if server.Status == instanceShutoff {
96+
return true, nil
97+
}
98+
99+
return false, nil
100+
}
101+
102+
// InstanceMetadata returns the instance's metadata.
103+
func (i *InstancesV2) InstanceMetadata(ctx context.Context, node *v1.Node) (*cloudprovider.InstanceMetadata, error) {
104+
srv, err := i.getInstance(ctx, node)
105+
if err != nil {
106+
return nil, err
107+
}
108+
server := ServerAttributesExt{}
109+
if srv != nil {
110+
server = *srv
111+
}
112+
113+
instanceType, err := srvInstanceType(i.compute, &server.Server)
114+
if err != nil {
115+
return nil, err
116+
}
117+
118+
interfaces, err := getAttachedInterfacesByID(i.compute, server.ID)
119+
if err != nil {
120+
return nil, err
121+
}
122+
123+
addresses, err := nodeAddresses(&server.Server, interfaces, i.networkingOpts)
124+
if err != nil {
125+
return nil, err
126+
}
127+
128+
return &cloudprovider.InstanceMetadata{
129+
ProviderID: i.makeInstanceID(&server.Server),
130+
InstanceType: instanceType,
131+
NodeAddresses: addresses,
132+
Zone: server.AvailabilityZone,
133+
Region: i.region,
134+
}, nil
135+
}
136+
137+
func (i *InstancesV2) makeInstanceID(srv *servers.Server) string {
138+
if i.regionProviderID {
139+
return fmt.Sprintf("%s://%s/%s", ProviderName, i.region, srv.ID)
140+
}
141+
return fmt.Sprintf("%s:///%s", ProviderName, srv.ID)
142+
}
143+
144+
func (i *InstancesV2) getInstance(ctx context.Context, node *v1.Node) (*ServerAttributesExt, error) {
145+
if node.Spec.ProviderID == "" {
146+
opt := servers.ListOpts{
147+
Name: fmt.Sprintf("^%s$", node.Name),
148+
}
149+
mc := metrics.NewMetricContext("server", "list")
150+
allPages, err := servers.List(i.compute, opt).AllPages()
151+
if mc.ObserveRequest(err) != nil {
152+
return nil, fmt.Errorf("error listing servers %v: %v", opt, err)
153+
}
154+
155+
serverList := []ServerAttributesExt{}
156+
err = servers.ExtractServersInto(allPages, &serverList)
157+
if err != nil {
158+
return nil, fmt.Errorf("error extracting servers from pages: %v", err)
159+
}
160+
if len(serverList) == 0 {
161+
return nil, cloudprovider.InstanceNotFound
162+
}
163+
if len(serverList) > 1 {
164+
return nil, fmt.Errorf("getInstance: multiple instances found")
165+
}
166+
return &serverList[0], nil
167+
}
168+
169+
instanceID, instanceRegion, err := instanceIDFromProviderID(node.Spec.ProviderID)
170+
if err != nil {
171+
return nil, err
172+
}
173+
174+
if instanceRegion != "" && instanceRegion != i.region {
175+
return nil, fmt.Errorf("ProviderID \"%s\" didn't match supported region \"%s\"", node.Spec.ProviderID, i.region)
176+
}
177+
178+
server := ServerAttributesExt{}
179+
mc := metrics.NewMetricContext("server", "get")
180+
err = servers.Get(i.compute, instanceID).ExtractInto(&server)
181+
if mc.ObserveRequest(err) != nil {
182+
if errors.IsNotFound(err) {
183+
return nil, cloudprovider.InstanceNotFound
184+
}
185+
return nil, err
186+
}
187+
return &server, nil
188+
}

pkg/openstack/openstack.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"fmt"
2222
"io"
2323
"os"
24+
"strings"
2425
"time"
2526

2627
"github.com/gophercloud/gophercloud"
@@ -147,6 +148,7 @@ type OpenStack struct {
147148
// InstanceID of the server where this OpenStack object is instantiated.
148149
localInstanceID string
149150
kclient kubernetes.Interface
151+
useV1Instances bool // TODO: v1 instance apis can be deleted after the v2 is verified enough
150152
}
151153

152154
// Config is used to read and store information from the cloud configuration file
@@ -270,6 +272,12 @@ func NewOpenStack(cfg Config) (*OpenStack, error) {
270272
}
271273
provider.HTTPClient.Timeout = cfg.Metadata.RequestTimeout.Duration
272274

275+
useV1Instances := false
276+
v1instances := os.Getenv("OS_V1_INSTANCES")
277+
if strings.ToLower(v1instances) == "true" {
278+
useV1Instances = true
279+
}
280+
273281
os := OpenStack{
274282
provider: provider,
275283
epOpts: &gophercloud.EndpointOpts{
@@ -280,6 +288,7 @@ func NewOpenStack(cfg Config) (*OpenStack, error) {
280288
routeOpts: cfg.Route,
281289
metadataOpts: cfg.Metadata,
282290
networkingOpts: cfg.Networking,
291+
useV1Instances: useV1Instances,
283292
}
284293

285294
// ini file doesn't support maps so we are reusing top level sub sections

0 commit comments

Comments
 (0)