Skip to content

Commit

Permalink
Use PrivateDnsName as Node name in nodeadm
Browse files Browse the repository at this point in the history
  • Loading branch information
cartermckinnon committed Mar 8, 2024
1 parent 7d215b2 commit 0d404a2
Show file tree
Hide file tree
Showing 69 changed files with 257 additions and 28,208 deletions.
58 changes: 2 additions & 56 deletions nodeadm/cmd/nodeadm/init/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,9 @@ package init

import (
"context"
"encoding/base64"
"fmt"

"github.com/aws/aws-sdk-go-v2/feature/ec2/imds"
"github.com/aws/aws-sdk-go/service/eks"
"github.com/aws/aws-sdk-go-v2/service/ec2"
"github.com/integrii/flaggy"
"go.uber.org/zap"
"k8s.io/utils/strings/slices"
Expand Down Expand Up @@ -145,7 +143,7 @@ func (c *initCmd) Run(log *zap.Logger, opts *cli.GlobalOptions) error {
// perform in-place updates when allowed by the user
func enrichConfig(log *zap.Logger, cfg *api.NodeConfig) error {
log.Info("Fetching instance details..")
instanceDetails, err := api.GetIMDSInstanceDetails(context.TODO(), imds.New(imds.Options{}))
instanceDetails, err := api.GetInstanceDetails(context.TODO(), imds.New(imds.Options{}), ec2.New(ec2.Options{}))
if err != nil {
return err
}
Expand All @@ -162,55 +160,3 @@ func enrichConfig(log *zap.Logger, cfg *api.NodeConfig) error {
log.Info("Default options populated", zap.Reflect("defaults", cfg.Status.Defaults))
return nil
}

// Discovers all cluster details using a describe call to the eks endpoint and
// updates the value of the config's `ClusterDetails` in-place
func populateClusterDetails(eksClient *eks.EKS, clusterName string, cfg *api.NodeConfig) error {
if err := eksClient.WaitUntilClusterActive(&eks.DescribeClusterInput{Name: &clusterName}); err != nil {
return err
}
describeResponse, err := eksClient.DescribeCluster(&eks.DescribeClusterInput{Name: &clusterName})
if err != nil {
return err
}

ipFamily := *describeResponse.Cluster.KubernetesNetworkConfig.IpFamily

var cidr string
if ipFamily == eks.IpFamilyIpv4 {
cidr = *describeResponse.Cluster.KubernetesNetworkConfig.ServiceIpv4Cidr
} else if ipFamily == eks.IpFamilyIpv6 {
cidr = *describeResponse.Cluster.KubernetesNetworkConfig.ServiceIpv6Cidr
} else {
return fmt.Errorf("bad ipFamily: %s", ipFamily)
}

isOutpost := false
clusterId := cfg.Spec.Cluster.ID
// detect whether the cluster is an aws outpost cluster depending on whether
// the response contains the outpost ID
if outpostId := describeResponse.Cluster.Id; outpostId != nil {
clusterId = *outpostId
isOutpost = true
}

enableOutpost := isOutpost
// respect the user override for enabling the outpost
if enabled := cfg.Spec.Cluster.EnableOutpost; enabled != nil {
enableOutpost = *enabled
}

caCert, err := base64.StdEncoding.DecodeString(*describeResponse.Cluster.CertificateAuthority.Data)
if err != nil {
return err
}

cfg.Spec.Cluster.Name = *describeResponse.Cluster.Name
cfg.Spec.Cluster.APIServerEndpoint = *describeResponse.Cluster.Endpoint
cfg.Spec.Cluster.CertificateAuthority = caCert
cfg.Spec.Cluster.CIDR = cidr
cfg.Spec.Cluster.EnableOutpost = &enableOutpost
cfg.Spec.Cluster.ID = clusterId

return nil
}
4 changes: 2 additions & 2 deletions nodeadm/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ require (
require dario.cat/mergo v1.0.0 // direct

require (
github.com/aws/aws-sdk-go-v2 v1.24.1 // indirect
github.com/aws/aws-sdk-go-v2 v1.24.1
github.com/aws/aws-sdk-go-v2/credentials v1.16.16 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.10 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.10 // indirect
Expand All @@ -54,7 +54,7 @@ require (
github.com/google/go-cmp v0.6.0 // indirect
github.com/google/gofuzz v1.2.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/jmespath/go-jmespath v0.4.0
github.com/json-iterator/go v1.1.12 // indirect
github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
Expand Down
32 changes: 0 additions & 32 deletions nodeadm/internal/api/net.go
Original file line number Diff line number Diff line change
@@ -1,43 +1,11 @@
package api

import (
"context"
"fmt"
"io"
"net"
"strings"

"github.com/aws/aws-sdk-go-v2/feature/ec2/imds"
)

// Fetch information about the ec2 instance using IMDS data.
// This information is stored into the internal config to avoid redundant calls
// to IMDS when looking for instance metadata
func GetIMDSInstanceDetails(ctx context.Context, imdsClient *imds.Client) (*InstanceDetails, error) {
instanceIdenitityDocument, err := imdsClient.GetInstanceIdentityDocument(ctx, &imds.GetInstanceIdentityDocumentInput{})
if err != nil {
return nil, err
}

macResponse, err := imdsClient.GetMetadata(ctx, &imds.GetMetadataInput{Path: "mac"})
if err != nil {
return nil, err
}
mac, err := io.ReadAll(macResponse.Content)
if err != nil {
return nil, err
}

instanceDetails := InstanceDetails{
ID: instanceIdenitityDocument.InstanceID,
Region: instanceIdenitityDocument.Region,
Type: instanceIdenitityDocument.InstanceType,
AvailabilityZone: instanceIdenitityDocument.AvailabilityZone,
MAC: string(mac),
}
return &instanceDetails, nil
}

// Derive the default ClusterIP of the kube-dns service from EKS built-in CoreDNS addon
func (details *ClusterDetails) GetClusterDns() (string, error) {
ipFamily, err := GetCIDRIpFamily(details.CIDR)
Expand Down
66 changes: 66 additions & 0 deletions nodeadm/internal/api/status.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package api

import (
"context"
"fmt"
"io"
"time"

"github.com/aws/aws-sdk-go-v2/feature/ec2/imds"
"github.com/aws/aws-sdk-go-v2/service/ec2"
"github.com/aws/aws-sdk-go/aws"
ec2extra "github.com/awslabs/amazon-eks-ami/nodeadm/internal/aws/ec2"
)

// Fetch information about the ec2 instance using IMDS data.
// This information is stored into the internal config to avoid redundant calls
// to IMDS when looking for instance metadata
func GetInstanceDetails(ctx context.Context, imdsClient *imds.Client, ec2Client *ec2.Client) (*InstanceDetails, error) {
instanceIdenitityDocument, err := imdsClient.GetInstanceIdentityDocument(ctx, &imds.GetInstanceIdentityDocumentInput{})
if err != nil {
return nil, err
}

macResponse, err := imdsClient.GetMetadata(ctx, &imds.GetMetadataInput{Path: "mac"})
if err != nil {
return nil, err
}
mac, err := io.ReadAll(macResponse.Content)
if err != nil {
return nil, err
}

privateDNSName, err := getPrivateDNSName(ec2Client, instanceIdenitityDocument.InstanceID)
if err != nil {
return nil, err
}

return &InstanceDetails{
ID: instanceIdenitityDocument.InstanceID,
Region: instanceIdenitityDocument.Region,
Type: instanceIdenitityDocument.InstanceType,
AvailabilityZone: instanceIdenitityDocument.AvailabilityZone,
MAC: string(mac),
PrivateDNSName: privateDNSName,
}, nil
}

const privateDNSNameAvailableTimeout = 3 * time.Minute

// GetPrivateDNSName returns this instance's private DNS name as reported by the EC2 API, waiting until it's available if necessary.
func getPrivateDNSName(ec2Client *ec2.Client, instanceID string) (string, error) {
w := ec2extra.NewInstanceConditionWaiter(ec2Client, privateDNSNameAvailable)
out, err := w.WaitForOutput(context.TODO(), &ec2.DescribeInstancesInput{InstanceIds: []string{instanceID}}, privateDNSNameAvailableTimeout)
if err != nil {
return "", err
}
privateDNSName := aws.StringValue(out.Reservations[0].Instances[0].PrivateDnsName)
return privateDNSName, nil
}

func privateDNSNameAvailable(out *ec2.DescribeInstancesOutput) (bool, error) {
if len(out.Reservations) != 1 || len(out.Reservations[0].Instances) != 1 {
return false, fmt.Errorf("reservation or instance not found")
}
return aws.StringValue(out.Reservations[0].Instances[0].PrivateDnsName) != "", nil
}
1 change: 1 addition & 0 deletions nodeadm/internal/api/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ type InstanceDetails struct {
Type string `json:"type,omitempty"`
AvailabilityZone string `json:"availabilityZone,omitempty"`
MAC string `json:"mac,omitempty"`
PrivateDNSName string `json:"privateDnsName,omitempty"`
}

type DefaultOptions struct {
Expand Down
180 changes: 180 additions & 0 deletions nodeadm/internal/aws/ec2/instance_waiter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
package ec2

import (
"context"
"errors"
"fmt"
"time"

"github.com/aws/aws-sdk-go-v2/service/ec2"
"github.com/aws/smithy-go"
"github.com/aws/smithy-go/middleware"
smithytime "github.com/aws/smithy-go/time"
smithywaiter "github.com/aws/smithy-go/waiter"
)

type InstanceCondition func(output *ec2.DescribeInstancesOutput) (bool, error)

// InstanceConditionWaiterOptions are options for InstanceConditionWaiter
type InstanceConditionWaiterOptions struct {

// Set of options to modify how an operation is invoked. These apply to all
// operations invoked for this client. Use functional options on operation call to
// modify this list for per operation behavior.
//
// Passing options here is functionally equivalent to passing values to this
// config's ClientOptions field that extend the inner client's APIOptions directly.
APIOptions []func(*middleware.Stack) error

// Functional options to be passed to all operations invoked by this client.
//
// Function values that modify the inner APIOptions are applied after the waiter
// config's own APIOptions modifiers.
ClientOptions []func(*ec2.Options)

// MinDelay is the minimum amount of time to delay between retries. If unset,
// InstanceRunningWaiter will use default minimum delay of 15 seconds. Note that
// MinDelay must resolve to a value lesser than or equal to the MaxDelay.
MinDelay time.Duration

// MaxDelay is the maximum amount of time to delay between retries. If unset or
// set to zero, InstanceRunningWaiter will use default max delay of 120 seconds.
// Note that MaxDelay must resolve to value greater than or equal to the MinDelay.
MaxDelay time.Duration

// LogWaitAttempts is used to enable logging for waiter retry attempts
LogWaitAttempts bool
}

// InstanceConditionWaiter waits for an instance to meet a condition
type InstanceConditionWaiter struct {
client ec2.DescribeInstancesAPIClient
condition InstanceCondition
options InstanceConditionWaiterOptions
}

// NewInstanceConditionWaiter constructs a InstanceConditionWaiter.
func NewInstanceConditionWaiter(client ec2.DescribeInstancesAPIClient, condition InstanceCondition, optFns ...func(*InstanceConditionWaiterOptions)) *InstanceConditionWaiter {
options := InstanceConditionWaiterOptions{}
options.MinDelay = 15 * time.Second
options.MaxDelay = 120 * time.Second

for _, fn := range optFns {
fn(&options)
}

return &InstanceConditionWaiter{
client: client,
condition: condition,
options: options,
}
}

// Wait calls the waiter function for InstanceCondition waiter. The maxWaitDur is
// the maximum wait duration the waiter will wait. The maxWaitDur is required and
// must be greater than zero.
func (w *InstanceConditionWaiter) Wait(ctx context.Context, params *ec2.DescribeInstancesInput, maxWaitDur time.Duration, optFns ...func(*InstanceConditionWaiterOptions)) error {
_, err := w.WaitForOutput(ctx, params, maxWaitDur, optFns...)
return err
}

// WaitForOutput calls the waiter function for InstanceConditionWaiter and returns
// the output of the successful operation. The maxWaitDur is the maximum wait
// duration the waiter will wait. The maxWaitDur is required and must be greater
// than zero.
func (w *InstanceConditionWaiter) WaitForOutput(ctx context.Context, params *ec2.DescribeInstancesInput, maxWaitDur time.Duration, optFns ...func(*InstanceConditionWaiterOptions)) (*ec2.DescribeInstancesOutput, error) {
if maxWaitDur <= 0 {
return nil, fmt.Errorf("maximum wait time for waiter must be greater than zero")
}

options := w.options
for _, fn := range optFns {
fn(&options)
}

if options.MaxDelay <= 0 {
options.MaxDelay = 120 * time.Second
}

if options.MinDelay > options.MaxDelay {
return nil, fmt.Errorf("minimum waiter delay %v must be lesser than or equal to maximum waiter delay of %v.", options.MinDelay, options.MaxDelay)
}

ctx, cancelFn := context.WithTimeout(ctx, maxWaitDur)
defer cancelFn()

logger := smithywaiter.Logger{}
remainingTime := maxWaitDur

var attempt int64
for {
attempt++
apiOptions := options.APIOptions
start := time.Now()

if options.LogWaitAttempts {
logger.Attempt = attempt
apiOptions = append([]func(*middleware.Stack) error{}, options.APIOptions...)
apiOptions = append(apiOptions, logger.AddLogger)
}

out, err := w.client.DescribeInstances(ctx, params, func(o *ec2.Options) {
o.APIOptions = append(o.APIOptions, apiOptions...)
for _, opt := range options.ClientOptions {
opt(o)
}
})

retryable, err := instanceRetryable(err)
if err != nil {
return nil, err
}
if !retryable {
return out, nil
}

conditionMet, err := w.condition(out)
if err != nil {
return nil, err
}
if conditionMet {
return out, nil
}

remainingTime -= time.Since(start)
if remainingTime < options.MinDelay || remainingTime <= 0 {
break
}

// compute exponential backoff between waiter retries
delay, err := smithywaiter.ComputeDelay(
attempt, options.MinDelay, options.MaxDelay, remainingTime,
)
if err != nil {
return nil, fmt.Errorf("error computing waiter delay, %w", err)
}

remainingTime -= delay
// sleep for the delay amount before invoking a request
if err := smithytime.SleepWithContext(ctx, delay); err != nil {
return nil, fmt.Errorf("request cancelled while waiting, %w", err)
}
}
return nil, fmt.Errorf("exceeded max wait time for InstanceCondition waiter")
}

func instanceRetryable(err error) (bool, error) {
if err != nil {
var apiErr smithy.APIError
ok := errors.As(err, &apiErr)
if !ok {
return false, fmt.Errorf("expected err to be of type smithy.APIError, got %w", err)
}

if "InvalidInstanceID.NotFound" == apiErr.ErrorCode() {
return true, nil
}
}

return true, nil
}
Loading

0 comments on commit 0d404a2

Please sign in to comment.