sigs.k8s.io/cluster-api-provider-aws@v1.5.5/pkg/cloud/services/ec2/bastion.go (about)

     1  /*
     2  Copyright 2018 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 ec2
    18  
    19  import (
    20  	"encoding/base64"
    21  	"fmt"
    22  	"strings"
    23  
    24  	"github.com/aws/aws-sdk-go/aws"
    25  	"github.com/aws/aws-sdk-go/service/ec2"
    26  	"github.com/pkg/errors"
    27  
    28  	infrav1 "sigs.k8s.io/cluster-api-provider-aws/api/v1beta1"
    29  	"sigs.k8s.io/cluster-api-provider-aws/pkg/cloud/awserrors"
    30  	"sigs.k8s.io/cluster-api-provider-aws/pkg/cloud/filter"
    31  	"sigs.k8s.io/cluster-api-provider-aws/pkg/cloud/services/userdata"
    32  	"sigs.k8s.io/cluster-api-provider-aws/pkg/record"
    33  	clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
    34  	"sigs.k8s.io/cluster-api/util/conditions"
    35  )
    36  
    37  const (
    38  	defaultSSHKeyName = "default"
    39  )
    40  
    41  var (
    42  	fallbackBastionInstanceType        = "t3.micro"
    43  	fallbackBastionUsEast1InstanceType = "t2.micro"
    44  )
    45  
    46  // ReconcileBastion ensures a bastion is created for the cluster.
    47  func (s *Service) ReconcileBastion() error {
    48  	if !s.scope.Bastion().Enabled {
    49  		s.scope.V(4).Info("Skipping bastion reconcile")
    50  		_, err := s.describeBastionInstance()
    51  		if err != nil {
    52  			if awserrors.IsNotFound(err) {
    53  				return nil
    54  			}
    55  			return err
    56  		}
    57  		return s.DeleteBastion()
    58  	}
    59  
    60  	s.scope.V(2).Info("Reconciling bastion host")
    61  
    62  	subnets := s.scope.Subnets()
    63  	if len(subnets.FilterPrivate()) == 0 {
    64  		s.scope.V(2).Info("No private subnets available, skipping bastion host")
    65  		return nil
    66  	} else if len(subnets.FilterPublic()) == 0 {
    67  		return errors.New("failed to reconcile bastion host, no public subnets are available")
    68  	}
    69  
    70  	// Describe bastion instance, if any.
    71  	instance, err := s.describeBastionInstance()
    72  	if awserrors.IsNotFound(err) { // nolint:nestif
    73  		if !conditions.Has(s.scope.InfraCluster(), infrav1.BastionHostReadyCondition) {
    74  			conditions.MarkFalse(s.scope.InfraCluster(), infrav1.BastionHostReadyCondition, infrav1.BastionCreationStartedReason, clusterv1.ConditionSeverityInfo, "")
    75  			if err := s.scope.PatchObject(); err != nil {
    76  				return errors.Wrap(err, "failed to patch conditions")
    77  			}
    78  		}
    79  		defaultBastion, err := s.getDefaultBastion(s.scope.Bastion().InstanceType, s.scope.Bastion().AMI)
    80  		if err != nil {
    81  			record.Warnf(s.scope.InfraCluster(), "FailedFetchingBastion", "Failed to fetch default bastion instance: %v", err)
    82  			return err
    83  		}
    84  		instance, err = s.runInstance("bastion", defaultBastion)
    85  		if err != nil {
    86  			record.Warnf(s.scope.InfraCluster(), "FailedCreateBastion", "Failed to create bastion instance: %v", err)
    87  			return err
    88  		}
    89  
    90  		record.Eventf(s.scope.InfraCluster(), "SuccessfulCreateBastion", "Created bastion instance %q", instance.ID)
    91  		s.scope.Info("Created new bastion host", "id", instance.ID)
    92  	} else if err != nil {
    93  		return err
    94  	}
    95  
    96  	// TODO(vincepri): check for possible changes between the default spec and the instance.
    97  
    98  	s.scope.SetBastionInstance(instance.DeepCopy())
    99  	conditions.MarkTrue(s.scope.InfraCluster(), infrav1.BastionHostReadyCondition)
   100  	s.scope.V(2).Info("Reconcile bastion completed successfully")
   101  
   102  	return nil
   103  }
   104  
   105  // DeleteBastion deletes the Bastion instance.
   106  func (s *Service) DeleteBastion() error {
   107  	instance, err := s.describeBastionInstance()
   108  	if err != nil {
   109  		if awserrors.IsNotFound(err) {
   110  			s.scope.V(4).Info("bastion instance does not exist")
   111  			return nil
   112  		}
   113  		return errors.Wrap(err, "unable to describe bastion instance")
   114  	}
   115  
   116  	conditions.MarkFalse(s.scope.InfraCluster(), infrav1.BastionHostReadyCondition, clusterv1.DeletingReason, clusterv1.ConditionSeverityInfo, "")
   117  	if err := s.scope.PatchObject(); err != nil {
   118  		return err
   119  	}
   120  
   121  	if err := s.TerminateInstanceAndWait(instance.ID); err != nil {
   122  		conditions.MarkFalse(s.scope.InfraCluster(), infrav1.BastionHostReadyCondition, "DeletingFailed", clusterv1.ConditionSeverityWarning, err.Error())
   123  		record.Warnf(s.scope.InfraCluster(), "FailedTerminateBastion", "Failed to terminate bastion instance %q: %v", instance.ID, err)
   124  		return errors.Wrap(err, "unable to delete bastion instance")
   125  	}
   126  
   127  	s.scope.SetBastionInstance(nil)
   128  
   129  	conditions.MarkFalse(s.scope.InfraCluster(), infrav1.BastionHostReadyCondition, clusterv1.DeletedReason, clusterv1.ConditionSeverityInfo, "")
   130  	record.Eventf(s.scope.InfraCluster(), "SuccessfulTerminateBastion", "Terminated bastion instance %q", instance.ID)
   131  	s.scope.Info("Deleted bastion host", "id", instance.ID)
   132  
   133  	return nil
   134  }
   135  
   136  func (s *Service) describeBastionInstance() (*infrav1.Instance, error) {
   137  	input := &ec2.DescribeInstancesInput{
   138  		Filters: []*ec2.Filter{
   139  			filter.EC2.ProviderRole(infrav1.BastionRoleTagValue),
   140  			filter.EC2.Cluster(s.scope.Name()),
   141  			filter.EC2.InstanceStates(
   142  				ec2.InstanceStateNamePending,
   143  				ec2.InstanceStateNameRunning,
   144  				ec2.InstanceStateNameStopping,
   145  				ec2.InstanceStateNameStopped,
   146  			),
   147  		},
   148  	}
   149  
   150  	out, err := s.EC2Client.DescribeInstances(input)
   151  	if err != nil {
   152  		record.Eventf(s.scope.InfraCluster(), "FailedDescribeBastionHost", "Failed to describe bastion host: %v", err)
   153  		return nil, errors.Wrap(err, "failed to describe bastion host")
   154  	}
   155  
   156  	// TODO: properly handle multiple bastions found rather than just returning
   157  	// the first non-terminated.
   158  	for _, res := range out.Reservations {
   159  		for _, instance := range res.Instances {
   160  			if aws.StringValue(instance.State.Name) != ec2.InstanceStateNameTerminated {
   161  				return s.SDKToInstance(instance)
   162  			}
   163  		}
   164  	}
   165  
   166  	return nil, awserrors.NewNotFound("bastion host not found")
   167  }
   168  
   169  func (s *Service) getDefaultBastion(instanceType, ami string) (*infrav1.Instance, error) {
   170  	name := fmt.Sprintf("%s-bastion", s.scope.Name())
   171  	userData, _ := userdata.NewBastion(&userdata.BastionInput{})
   172  
   173  	// If SSHKeyName WAS NOT provided, use the defaultSSHKeyName
   174  	keyName := s.scope.SSHKeyName()
   175  	if keyName == nil {
   176  		keyName = aws.String(defaultSSHKeyName)
   177  	}
   178  
   179  	subnet := s.scope.Subnets().FilterPublic()[0]
   180  
   181  	if instanceType == "" {
   182  		if strings.Contains(subnet.AvailabilityZone, "us-east-1") {
   183  			instanceType = fallbackBastionUsEast1InstanceType
   184  		} else {
   185  			instanceType = fallbackBastionInstanceType
   186  		}
   187  	}
   188  
   189  	if ami == "" {
   190  		var err error
   191  		ami, err = s.defaultBastionAMILookup()
   192  		if err != nil {
   193  			return nil, err
   194  		}
   195  	}
   196  
   197  	i := &infrav1.Instance{
   198  		Type:       instanceType,
   199  		SubnetID:   subnet.ID,
   200  		ImageID:    ami,
   201  		SSHKeyName: keyName,
   202  		UserData:   aws.String(base64.StdEncoding.EncodeToString([]byte(userData))),
   203  		SecurityGroupIDs: []string{
   204  			s.scope.Network().SecurityGroups[infrav1.SecurityGroupBastion].ID,
   205  		},
   206  		Tags: infrav1.Build(infrav1.BuildParams{
   207  			ClusterName: s.scope.Name(),
   208  			Lifecycle:   infrav1.ResourceLifecycleOwned,
   209  			Name:        aws.String(name),
   210  			Role:        aws.String(infrav1.BastionRoleTagValue),
   211  			Additional:  s.scope.AdditionalTags(),
   212  		}),
   213  	}
   214  
   215  	return i, nil
   216  }