Skip to content

Commit 1c1ac06

Browse files
committed
instances: initial implementation of Instances interface
1 parent 35aeabd commit 1c1ac06

File tree

6 files changed

+868
-7
lines changed

6 files changed

+868
-7
lines changed

Diff for: go.mod

+6
Original file line numberDiff line numberDiff line change
@@ -28,14 +28,20 @@ replace (
2828

2929
require (
3030
github.com/aws/aws-sdk-go v1.28.2
31+
github.com/golang/mock v1.3.1
32+
github.com/google/go-cmp v0.4.0
33+
github.com/google/uuid v1.1.1
3134
github.com/spf13/cobra v1.0.0
3235
github.com/spf13/pflag v1.0.5
3336
github.com/stretchr/testify v1.4.0
37+
gopkg.in/gcfg.v1 v1.2.0
38+
k8s.io/api v0.0.0
3439
k8s.io/apimachinery v0.0.0
3540
k8s.io/apiserver v0.0.0
3641
k8s.io/cloud-provider v0.0.0
3742
k8s.io/component-base v0.0.0
3843
k8s.io/klog v1.0.0
44+
k8s.io/klog/v2 v2.2.0
3945
k8s.io/kubernetes v1.19.1
4046
k8s.io/legacy-cloud-providers v0.0.0
4147
k8s.io/utils v0.0.0-20200729134348-d5654de09c73

Diff for: go.sum

+3
Original file line numberDiff line numberDiff line change
@@ -262,7 +262,9 @@ github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7 h1:5ZkaAPbicIKTF
262262
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
263263
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
264264
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
265+
github.com/golang/mock v1.3.1 h1:qGJ6qTW+x6xX/my+8YUVl4WNpX9B7+/l2tRsHGZ7f2s=
265266
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
267+
github.com/golang/mock v1.4.4 h1:l75CXGRSwbaYNpl/Z2X1XIIAMSCquvXgpVZDhwEIJsc=
266268
github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
267269
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
268270
github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg=
@@ -887,6 +889,7 @@ k8s.io/kube-openapi v0.0.0-20200805222855-6aeccd4b50c6 h1:+WnxoVtG8TMiudHBSEtrVL
887889
k8s.io/kube-openapi v0.0.0-20200805222855-6aeccd4b50c6/go.mod h1:UuqjUnNftUyPE5H64/qeyjQoUZhGpeFDVdxjTeEVN2o=
888890
k8s.io/kubernetes v1.19.1 h1:3Gdl9EtBiV3SYuzml1915nFLVxlx08L6bRz1b07C60k=
889891
k8s.io/kubernetes v1.19.1/go.mod h1:yhT1/ltQajQsha3tnYc9QPFYSumGM45nlZdjf7WqE1A=
892+
k8s.io/kubernetes v1.19.2 h1:sEvBYVM1/H5hqejFR10u8ndreYARV3DiTrqi2AY31ok=
890893
k8s.io/kubernetes/staging/src/k8s.io/api v0.0.0-20200909111720-206bcadf021e h1:mdkfNp9Iu1erEZy/9kefsctTTyey3aAMcCQ5VTzScIE=
891894
k8s.io/kubernetes/staging/src/k8s.io/api v0.0.0-20200909111720-206bcadf021e/go.mod h1:Y4VjjNur38HL6/QxaTVK2yno1zjEQlvcvwbbRQs2DtQ=
892895
k8s.io/kubernetes/staging/src/k8s.io/apiextensions-apiserver v0.0.0-20200909111720-206bcadf021e/go.mod h1:BvtZU215FgO19Oy19K6h8qwajFfjxYqGewgjuYHWGRw=

Diff for: pkg/providers/v2/cloud.go

+35-7
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ import (
2828
"github.com/aws/aws-sdk-go/aws/credentials/ec2rolecreds"
2929
"github.com/aws/aws-sdk-go/aws/ec2metadata"
3030
"github.com/aws/aws-sdk-go/aws/session"
31+
"github.com/aws/aws-sdk-go/service/ec2"
32+
3133
cloudprovider "k8s.io/cloud-provider"
3234
)
3335

@@ -46,8 +48,11 @@ var _ cloudprovider.Interface = (*cloud)(nil)
4648

4749
// cloud is the AWS v2 implementation of the cloud provider interface
4850
type cloud struct {
49-
creds *credentials.Credentials
50-
region string
51+
creds *credentials.Credentials
52+
instances cloudprovider.InstancesV2
53+
region string
54+
ec2 EC2
55+
metadata EC2Metadata
5156
}
5257

5358
// EC2Metadata is an abstraction over the AWS metadata service.
@@ -107,10 +112,33 @@ func newCloud() (cloudprovider.Interface, error) {
107112
return nil, err
108113
}
109114

110-
return &cloud{
111-
creds: creds,
112-
region: region,
113-
}, nil
115+
instances, err := newInstances(az, creds)
116+
if err != nil {
117+
return nil, err
118+
}
119+
120+
ec2Sess, err := session.NewSession(&aws.Config{
121+
Region: aws.String(region),
122+
Credentials: creds,
123+
})
124+
if err != nil {
125+
return nil, fmt.Errorf("unable to initialize AWS session: %v", err)
126+
}
127+
128+
ec2Service := ec2.New(ec2Sess)
129+
if err != nil {
130+
return nil, fmt.Errorf("error creating AWS ec2 client: %q", err)
131+
}
132+
133+
awsCloud := &cloud{
134+
creds: creds,
135+
instances: instances,
136+
region: region,
137+
metadata: metadataClient,
138+
ec2: ec2Service,
139+
}
140+
141+
return awsCloud, nil
114142
}
115143

116144
// Initialize passes a Kubernetes clientBuilder interface to the cloud provider
@@ -158,5 +186,5 @@ func (c *cloud) HasClusterID() bool {
158186
// Also returns true if the interface is supported, false otherwise.
159187
// WARNING: InstancesV2 is an experimental interface and is subject to change in v1.20.
160188
func (c *cloud) InstancesV2() (cloudprovider.InstancesV2, bool) {
161-
return nil, false
189+
return c.instances, true
162190
}

Diff for: pkg/providers/v2/instances.go

+292
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,292 @@
1+
/*
2+
Copyright 2020 The Kubernetes Authors.
3+
Licensed under the Apache License, Version 2.0 (the "License");
4+
you may not use this file except in compliance with the License.
5+
You may obtain a copy of the License at
6+
http://www.apache.org/licenses/LICENSE-2.0
7+
Unless required by applicable law or agreed to in writing, software
8+
distributed under the License is distributed on an "AS IS" BASIS,
9+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10+
See the License for the specific language governing permissions and
11+
limitations under the License.
12+
*/
13+
14+
// Package v2 is an out-of-tree only implementation of the AWS cloud provider.
15+
// It is not compatible with v1 and should only be used on new clusters.
16+
package v2
17+
18+
import (
19+
"context"
20+
"errors"
21+
"fmt"
22+
"net"
23+
"strings"
24+
25+
"k8s.io/klog/v2"
26+
27+
"github.com/aws/aws-sdk-go/aws"
28+
"github.com/aws/aws-sdk-go/aws/credentials"
29+
"github.com/aws/aws-sdk-go/aws/session"
30+
"github.com/aws/aws-sdk-go/service/ec2"
31+
32+
v1 "k8s.io/api/core/v1"
33+
cloudprovider "k8s.io/cloud-provider"
34+
)
35+
36+
var InstanceTerminated = errors.New("instance terminated")
37+
38+
// newInstances returns an implementation of cloudprovider.InstancesV2
39+
func newInstances(az string, creds *credentials.Credentials) (cloudprovider.InstancesV2, error) {
40+
region, err := azToRegion(az)
41+
if err != nil {
42+
return nil, err
43+
}
44+
45+
awsConfig := &aws.Config{
46+
Region: aws.String(region),
47+
Credentials: creds,
48+
}
49+
awsConfig = awsConfig.WithCredentialsChainVerboseErrors(true)
50+
51+
sess, err := session.NewSession(awsConfig)
52+
if err != nil {
53+
return nil, fmt.Errorf("error creating new session: %v", err)
54+
}
55+
ec2Service := ec2.New(sess)
56+
57+
return &instances{
58+
availabilityZone: az,
59+
ec2: ec2Service,
60+
region: region,
61+
}, nil
62+
}
63+
64+
// EC2 is an interface defining only the methods we call from the AWS EC2 SDK.
65+
type EC2 interface {
66+
DescribeInstances(request *ec2.DescribeInstancesInput) (*ec2.DescribeInstancesOutput, error)
67+
}
68+
69+
// instances is an implementation of cloudprovider.InstancesV2
70+
type instances struct {
71+
availabilityZone string
72+
ec2 EC2
73+
region string
74+
}
75+
76+
// InstanceExists indicates whether a given node exists according to the cloud provider
77+
func (i *instances) InstanceExists(ctx context.Context, node *v1.Node) (bool, error) {
78+
_, err := i.getInstance(ctx, node)
79+
80+
if err == cloudprovider.InstanceNotFound {
81+
klog.V(6).Infof("instance not found for node: %s", node.Name)
82+
return false, nil
83+
}
84+
85+
if err != nil {
86+
return false, err
87+
}
88+
89+
return true, nil
90+
}
91+
92+
// InstanceShutdown returns true if the instance is shutdown according to the cloud provider.
93+
func (i *instances) InstanceShutdown(ctx context.Context, node *v1.Node) (bool, error) {
94+
ec2Instance, err := i.getInstance(ctx, node)
95+
if err != nil {
96+
if err == cloudprovider.InstanceNotFound {
97+
klog.V(6).Infof("instance not found for node: %s", node.Name)
98+
return false, nil
99+
}
100+
101+
if err == InstanceTerminated {
102+
klog.V(6).Infof("instance terminated for node: %s", node.Name)
103+
return true, nil
104+
}
105+
106+
return false, err
107+
}
108+
109+
if ec2Instance.State != nil {
110+
state := aws.StringValue(ec2Instance.State.Name)
111+
if state == ec2.InstanceStateNameStopping || state == ec2.InstanceStateNameStopped {
112+
return true, nil
113+
}
114+
}
115+
116+
return false, nil
117+
}
118+
119+
// InstanceMetadata returns the instance's metadata.
120+
func (i *instances) InstanceMetadata(ctx context.Context, node *v1.Node) (*cloudprovider.InstanceMetadata, error) {
121+
var err error
122+
var ec2Instance *ec2.Instance
123+
124+
// TODO: support node name policy other than private DNS names
125+
ec2Instance, err = i.getInstance(ctx, node)
126+
if err != nil {
127+
return nil, err
128+
}
129+
130+
nodeAddresses, err := nodeAddressesForInstance(ec2Instance)
131+
if err != nil {
132+
return nil, err
133+
}
134+
135+
providerID, err := getInstanceProviderID(ec2Instance)
136+
if err != nil {
137+
return nil, err
138+
}
139+
140+
metadata := &cloudprovider.InstanceMetadata{
141+
ProviderID: providerID,
142+
InstanceType: aws.StringValue(ec2Instance.InstanceType),
143+
NodeAddresses: nodeAddresses,
144+
}
145+
146+
return metadata, nil
147+
}
148+
149+
// getInstance returns the instance if the instance with the given node info still exists.
150+
// If false an error will be returned, the instance will be immediately deleted by the cloud controller manager.
151+
func (i *instances) getInstance(ctx context.Context, node *v1.Node) (*ec2.Instance, error) {
152+
var request *ec2.DescribeInstancesInput
153+
if node.Spec.ProviderID == "" {
154+
// get Instance by private DNS name
155+
request = &ec2.DescribeInstancesInput{
156+
Filters: []*ec2.Filter{
157+
newEc2Filter("private-dns-name", node.Name),
158+
},
159+
}
160+
klog.V(4).Infof("looking for node by private DNS name %v", node.Name)
161+
} else {
162+
// get Instance by provider ID
163+
instanceID, err := parseInstanceIDFromProviderID(node.Spec.ProviderID)
164+
if err != nil {
165+
return nil, err
166+
}
167+
168+
request = &ec2.DescribeInstancesInput{
169+
InstanceIds: []*string{aws.String(instanceID)},
170+
}
171+
klog.V(4).Infof("looking for node by provider ID %v", node.Spec.ProviderID)
172+
}
173+
174+
instances := []*ec2.Instance{}
175+
var nextToken *string
176+
for {
177+
response, err := i.ec2.DescribeInstances(request)
178+
if err != nil {
179+
return nil, fmt.Errorf("error describing ec2 instances: %v", err)
180+
}
181+
182+
for _, reservation := range response.Reservations {
183+
instances = append(instances, reservation.Instances...)
184+
}
185+
186+
nextToken = response.NextToken
187+
if aws.StringValue(nextToken) == "" {
188+
break
189+
}
190+
request.NextToken = nextToken
191+
}
192+
193+
if len(instances) == 0 {
194+
return nil, cloudprovider.InstanceNotFound
195+
}
196+
197+
if len(instances) > 1 {
198+
return nil, errors.New("getInstance: multiple instances found")
199+
}
200+
201+
state := instances[0].State.Name
202+
if *state == ec2.InstanceStateNameTerminated {
203+
return nil, InstanceTerminated
204+
}
205+
206+
return instances[0], nil
207+
}
208+
209+
// nodeAddresses for Instance returns a list of v1.NodeAddress for the give instance.
210+
// TODO: should we support ExternalIP by default?
211+
func nodeAddressesForInstance(instance *ec2.Instance) ([]v1.NodeAddress, error) {
212+
if instance == nil {
213+
return nil, errors.New("provided instances is nil")
214+
}
215+
216+
addresses := []v1.NodeAddress{}
217+
for _, networkInterface := range instance.NetworkInterfaces {
218+
// skip network interfaces that are not currently in use
219+
if aws.StringValue(networkInterface.Status) != ec2.NetworkInterfaceStatusInUse {
220+
continue
221+
}
222+
223+
for _, privateIP := range networkInterface.PrivateIpAddresses {
224+
if ipAddress := aws.StringValue(privateIP.PrivateIpAddress); ipAddress != "" {
225+
ip := net.ParseIP(ipAddress)
226+
if ip == nil {
227+
return nil, fmt.Errorf("invalid IP address %q from instance %q", ipAddress, aws.StringValue(instance.InstanceId))
228+
}
229+
230+
addresses = append(addresses, v1.NodeAddress{
231+
Type: v1.NodeInternalIP,
232+
Address: ip.String(),
233+
})
234+
}
235+
}
236+
}
237+
238+
return addresses, nil
239+
}
240+
241+
// getInstanceProviderID returns the provider ID of an instance which is ultimately set in the node.Spec.ProviderID field.
242+
// The well-known format for a node's providerID is:
243+
// * aws:///<availability-zone>/<instance-id>
244+
func getInstanceProviderID(instance *ec2.Instance) (string, error) {
245+
if aws.StringValue(instance.Placement.AvailabilityZone) == "" {
246+
return "", errors.New("instance availability zone was not set")
247+
}
248+
249+
if aws.StringValue(instance.InstanceId) == "" {
250+
return "", errors.New("instance ID was not set")
251+
}
252+
253+
return "aws:///" + aws.StringValue(instance.Placement.AvailabilityZone) + "/" + aws.StringValue(instance.InstanceId), nil
254+
}
255+
256+
// parseInstanceIDFromProviderID parses the node's instance ID based on the following formats:
257+
// * aws://<availability-zone>/<instance-id>
258+
// * aws:///<instance-id>
259+
// * <instance-id>
260+
// This function always assumes a valid providerID format was provided.
261+
func parseInstanceIDFromProviderID(providerID string) (string, error) {
262+
// trim the provider name prefix 'aws://', renaming providerID should contain metadata in one of the following formats:
263+
// * <availability-zone>/<instance-id>
264+
// * /<availability-zone>/<instance-id>
265+
// * <instance-id>
266+
instanceID := ""
267+
metadata := strings.Split(strings.TrimPrefix(providerID, "aws://"), "/")
268+
if len(metadata) == 1 {
269+
// instance-id
270+
instanceID = metadata[0]
271+
} else if len(metadata) == 2 {
272+
// az/instance-id
273+
instanceID = metadata[1]
274+
} else if len(metadata) == 3 {
275+
// /az/instance-id
276+
instanceID = metadata[2]
277+
}
278+
279+
return instanceID, nil
280+
}
281+
282+
func newEc2Filter(name string, values ...string) *ec2.Filter {
283+
filter := &ec2.Filter{
284+
Name: aws.String(name),
285+
}
286+
287+
for _, value := range values {
288+
filter.Values = append(filter.Values, aws.String(value))
289+
}
290+
291+
return filter
292+
}

0 commit comments

Comments
 (0)