sigs.k8s.io/cluster-api-provider-aws@v1.5.5/pkg/cloud/services/ssm/secret.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 ssm
    18  
    19  import (
    20  	"fmt"
    21  	"path"
    22  	"regexp"
    23  
    24  	"github.com/aws/aws-sdk-go/aws"
    25  	"github.com/aws/aws-sdk-go/service/ssm"
    26  	kerrors "k8s.io/apimachinery/pkg/util/errors"
    27  	"k8s.io/apimachinery/pkg/util/uuid"
    28  
    29  	infrav1 "sigs.k8s.io/cluster-api-provider-aws/api/v1beta1"
    30  	"sigs.k8s.io/cluster-api-provider-aws/pkg/cloud/awserrors"
    31  	"sigs.k8s.io/cluster-api-provider-aws/pkg/cloud/converters"
    32  	"sigs.k8s.io/cluster-api-provider-aws/pkg/cloud/scope"
    33  	"sigs.k8s.io/cluster-api-provider-aws/pkg/cloud/services/wait"
    34  	"sigs.k8s.io/cluster-api-provider-aws/pkg/internal/bytes"
    35  )
    36  
    37  const (
    38  	entryPrefix = "aws.cluster.x-k8s.io"
    39  
    40  	// max byte size for ssm is 4KB else we cross into the advanced-parameter tier.
    41  	maxSecretSizeBytes = 4000
    42  )
    43  
    44  var (
    45  	prefixRe        = regexp.MustCompile(`(?i)^[\/]?(aws|ssm)[.]?`)
    46  	retryableErrors = []string{
    47  		ssm.ErrCodeParameterLimitExceeded,
    48  	}
    49  )
    50  
    51  // Create stores data in AWS SSM for a given machine, chunking at 4kb per secret. The prefix of the secret
    52  // ARN and the number of chunks are returned.
    53  func (s *Service) Create(m *scope.MachineScope, data []byte) (string, int32, error) {
    54  	// Build the tags to apply to the secret.
    55  	additionalTags := m.AdditionalTags()
    56  	additionalTags[infrav1.ClusterAWSCloudProviderTagKey(s.scope.Name())] = string(infrav1.ResourceLifecycleOwned)
    57  	tags := infrav1.Build(infrav1.BuildParams{
    58  		ClusterName: s.scope.Name(),
    59  		Lifecycle:   infrav1.ResourceLifecycleOwned,
    60  		Name:        aws.String(m.Name()),
    61  		Role:        aws.String(m.Role()),
    62  		Additional:  additionalTags,
    63  	})
    64  
    65  	// Build the prefix.
    66  	prefix := m.GetSecretPrefix()
    67  	if prefix == "" {
    68  		prefix = path.Join(entryPrefix, string(uuid.NewUUID()))
    69  	}
    70  	// SSM Validation does not allow (/)aws|ssm in the beginning of the string
    71  	prefix = prefixRe.ReplaceAllString(prefix, "")
    72  	// Because the secret name has a slash in it, whole name must validate as a full path
    73  	if prefix[0] != byte('/') {
    74  		prefix = "/" + prefix
    75  	}
    76  
    77  	// Split the data into chunks and create the secrets on demand.
    78  	chunks := int32(0)
    79  	var err error
    80  	bytes.Split(data, true, maxSecretSizeBytes, func(chunk []byte) {
    81  		name := fmt.Sprintf("%s/%d", prefix, chunks)
    82  		retryFunc := func() (bool, error) { return s.retryableCreateSecret(name, chunk, tags) }
    83  		// Default timeout is 5 mins, but if SSM has got to the state where the timeout is reached,
    84  		// makes sense to slow down machine creation until AWS weather improves.
    85  		if err = wait.WaitForWithRetryable(wait.NewBackoff(), retryFunc, retryableErrors...); err != nil {
    86  			return
    87  		}
    88  		chunks++
    89  	})
    90  
    91  	return prefix, chunks, err
    92  }
    93  
    94  // retryableCreateSecret is a function to be passed into a waiter. In a separate function for ease of reading.
    95  func (s *Service) retryableCreateSecret(name string, chunk []byte, tags infrav1.Tags) (bool, error) {
    96  	_, err := s.SSMClient.PutParameter(&ssm.PutParameterInput{
    97  		Name:  aws.String(name),
    98  		Value: aws.String(string(chunk)),
    99  		Tags:  converters.MapToSSMTags(tags),
   100  		Type:  aws.String("SecureString"),
   101  	})
   102  	if err != nil {
   103  		return false, err
   104  	}
   105  	return true, err
   106  }
   107  
   108  // forceDeleteSecretEntry deletes a single secret, ignoring if it is absent.
   109  func (s *Service) forceDeleteSecretEntry(name string) error {
   110  	_, err := s.SSMClient.DeleteParameter(&ssm.DeleteParameterInput{
   111  		Name: aws.String(name),
   112  	})
   113  	if awserrors.IsNotFound(err) {
   114  		return nil
   115  	}
   116  	return err
   117  }
   118  
   119  // Delete the secret belonging to a machine from AWS SSM.
   120  func (s *Service) Delete(m *scope.MachineScope) error {
   121  	var errs []error
   122  	for i := int32(0); i < m.GetSecretCount(); i++ {
   123  		if err := s.forceDeleteSecretEntry(fmt.Sprintf("%s/%d", m.GetSecretPrefix(), i)); err != nil {
   124  			errs = append(errs, err)
   125  		}
   126  	}
   127  
   128  	return kerrors.NewAggregate(errs)
   129  }