sigs.k8s.io/cluster-api-provider-aws@v1.5.5/cmd/clusterawsadm/cloudformation/service/service.go (about)

     1  /*
     2  Copyright 2020 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 cloudformation provides the API operation methods for making requests to
    18  // AWS CloudFormation.
    19  package cloudformation
    20  
    21  import (
    22  	"fmt"
    23  	"os"
    24  	"text/tabwriter"
    25  
    26  	"github.com/aws/aws-sdk-go/aws"
    27  	cfn "github.com/aws/aws-sdk-go/service/cloudformation"
    28  	"github.com/aws/aws-sdk-go/service/cloudformation/cloudformationiface"
    29  	go_cfn "github.com/awslabs/goformation/v4/cloudformation"
    30  	"github.com/pkg/errors"
    31  	"k8s.io/klog/v2"
    32  	"k8s.io/utils/pointer"
    33  
    34  	"sigs.k8s.io/cluster-api-provider-aws/pkg/cloud/awserrors"
    35  )
    36  
    37  // Service holds a collection of interfaces.
    38  // The interfaces are broken down like this to group functions together.
    39  // One alternative is to have a large list of functions from the ec2 client.
    40  type Service struct {
    41  	CFN cloudformationiface.CloudFormationAPI
    42  }
    43  
    44  // NewService returns a new service given the CloudFormation api client.
    45  func NewService(i cloudformationiface.CloudFormationAPI) *Service {
    46  	return &Service{
    47  		CFN: i,
    48  	}
    49  }
    50  
    51  // ReconcileBootstrapStack creates or updates bootstrap CloudFormation.
    52  func (s *Service) ReconcileBootstrapStack(stackName string, t go_cfn.Template, tags map[string]string) error {
    53  	yaml, err := t.YAML()
    54  	processedYaml := string(yaml)
    55  	if err != nil {
    56  		return errors.Wrap(err, "failed to generate AWS CloudFormation YAML")
    57  	}
    58  
    59  	stackTags := []*cfn.Tag{}
    60  	for k, v := range tags {
    61  		stackTags = append(stackTags, &cfn.Tag{
    62  			Key:   pointer.StringPtr(k),
    63  			Value: pointer.StringPtr(v),
    64  		})
    65  	}
    66  	if err := s.createStack(stackName, processedYaml, stackTags); err != nil { // nolint:nestif
    67  		if code, _ := awserrors.Code(errors.Cause(err)); code == "AlreadyExistsException" {
    68  			klog.Infof("AWS Cloudformation stack %q already exists, updating", stackName)
    69  			updateErr := s.updateStack(stackName, processedYaml, stackTags)
    70  			if updateErr != nil {
    71  				code, ok := awserrors.Code(errors.Cause(updateErr))
    72  				message := awserrors.Message(errors.Cause(updateErr))
    73  				if !ok || code != "ValidationError" || message != "No updates are to be performed." {
    74  					return updateErr
    75  				}
    76  			}
    77  			return nil
    78  		}
    79  		return err
    80  	}
    81  	return nil
    82  }
    83  
    84  func (s *Service) createStack(stackName, yaml string, tags []*cfn.Tag) error {
    85  	input := &cfn.CreateStackInput{
    86  		Capabilities: aws.StringSlice([]string{cfn.CapabilityCapabilityIam, cfn.CapabilityCapabilityNamedIam}),
    87  		TemplateBody: aws.String(yaml),
    88  		StackName:    aws.String(stackName),
    89  		Tags:         tags,
    90  	}
    91  	klog.V(2).Infof("creating AWS CloudFormation stack %q", stackName)
    92  	if _, err := s.CFN.CreateStack(input); err != nil {
    93  		return errors.Wrap(err, "failed to create AWS CloudFormation stack")
    94  	}
    95  
    96  	desInput := &cfn.DescribeStacksInput{StackName: aws.String(stackName)}
    97  	klog.V(2).Infof("waiting for stack %q to create", stackName)
    98  	if err := s.CFN.WaitUntilStackCreateComplete(desInput); err != nil {
    99  		return errors.Wrap(err, "failed to create AWS CloudFormation stack")
   100  	}
   101  
   102  	klog.V(2).Infof("stack %q created", stackName)
   103  	return nil
   104  }
   105  
   106  func (s *Service) updateStack(stackName, yaml string, tags []*cfn.Tag) error {
   107  	input := &cfn.UpdateStackInput{
   108  		Capabilities: aws.StringSlice([]string{cfn.CapabilityCapabilityIam, cfn.CapabilityCapabilityNamedIam}),
   109  		TemplateBody: aws.String(yaml),
   110  		StackName:    aws.String(stackName),
   111  		Tags:         tags,
   112  	}
   113  	klog.V(2).Infof("updating AWS CloudFormation stack %q", stackName)
   114  	if _, err := s.CFN.UpdateStack(input); err != nil {
   115  		return errors.Wrap(err, "failed to update AWS CloudFormation stack")
   116  	}
   117  	desInput := &cfn.DescribeStacksInput{StackName: aws.String(stackName)}
   118  	klog.V(2).Infof("waiting for stack %q to update", stackName)
   119  	if err := s.CFN.WaitUntilStackUpdateComplete(desInput); err != nil {
   120  		return errors.Wrap(err, "failed to update AWS CloudFormation stack")
   121  	}
   122  
   123  	klog.V(2).Infof("stack %q updated", stackName)
   124  	return nil
   125  }
   126  
   127  // DeleteStack deletes a cloudformation stack.
   128  func (s *Service) DeleteStack(stackName string, retainResources []*string) error {
   129  	klog.V(2).Infof("deleting AWS CloudFormation stack %q", stackName)
   130  	var err error
   131  	if retainResources == nil {
   132  		_, err = s.CFN.DeleteStack(&cfn.DeleteStackInput{StackName: aws.String(stackName)})
   133  	} else {
   134  		_, err = s.CFN.DeleteStack(&cfn.DeleteStackInput{StackName: aws.String(stackName), RetainResources: retainResources})
   135  	}
   136  	if err != nil {
   137  		return errors.Wrap(err, "failed to delete AWS CloudFormation stack")
   138  	}
   139  
   140  	klog.V(2).Infof("waiting for stack %q to delete", stackName)
   141  	if err := s.CFN.WaitUntilStackDeleteComplete(&cfn.DescribeStacksInput{StackName: aws.String(stackName)}); err != nil {
   142  		return errors.Wrap(err, "failed to delete AWS CloudFormation stack")
   143  	}
   144  
   145  	klog.V(2).Infof("stack %q deleted", stackName)
   146  	return nil
   147  }
   148  
   149  // ShowStackResources prints out in tabular format the resources in the
   150  // stack.
   151  func (s *Service) ShowStackResources(stackName string) error {
   152  	input := &cfn.DescribeStackResourcesInput{
   153  		StackName: aws.String(stackName),
   154  	}
   155  	out, err := s.CFN.DescribeStackResources(input)
   156  	if err != nil {
   157  		return errors.Wrap(err, "unable to describe stack resources")
   158  	}
   159  
   160  	fmt.Print("\nFollowing resources are in the stack: \n\n")
   161  
   162  	w := tabwriter.NewWriter(os.Stdout, 0, 0, 1, ' ', tabwriter.Debug)
   163  
   164  	fmt.Fprintln(w, "Resource\tType\tStatus")
   165  
   166  	for _, r := range out.StackResources {
   167  		fmt.Fprintf(w, "%s\t%s\t%s\n",
   168  			aws.StringValue(r.ResourceType),
   169  			aws.StringValue(r.PhysicalResourceId),
   170  			aws.StringValue(r.ResourceStatus))
   171  
   172  		switch aws.StringValue(r.ResourceStatus) {
   173  		case cfn.ResourceStatusCreateComplete, cfn.ResourceStatusUpdateComplete:
   174  			continue
   175  		default:
   176  			fmt.Println(aws.StringValue(r.ResourceStatusReason))
   177  		}
   178  	}
   179  
   180  	w.Flush()
   181  
   182  	fmt.Print("\n\n")
   183  
   184  	return nil
   185  }