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 }