k8s.io/test-infra@v0.0.0-20240520184403-27c6b4c223d8/gencred/merge_kubeconfig_secret.py (about)

     1  #!/usr/bin/env python3
     2  
     3  # Copyright 2020 The Kubernetes Authors.
     4  #
     5  # Licensed under the Apache License, Version 2.0 (the "License");
     6  # you may not use this file except in compliance with the License.
     7  # You may obtain a copy of the License at
     8  #
     9  #     http://www.apache.org/licenses/LICENSE-2.0
    10  #
    11  # Unless required by applicable law or agreed to in writing, software
    12  # distributed under the License is distributed on an "AS IS" BASIS,
    13  # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    14  # See the License for the specific language governing permissions and
    15  # limitations under the License.
    16  
    17  """Merges a kubeconfig file into a kubeconfig file in a k8s secret giving
    18  precedence to the secret."""
    19  
    20  # Requirements: kubectl (pointed at the correct cluster) and base64
    21  # Example usage:
    22  # ./merge_kubeconfig_secret.py \
    23  #   --name=mykube \
    24  #   --namespace=myspace \
    25  #   --src-key=kube-old \
    26  #   --dest-key=kube-new \
    27  #   kubeconfig.yaml
    28  
    29  import argparse
    30  import os
    31  import re
    32  import subprocess
    33  import sys
    34  import tempfile
    35  import time
    36  
    37  reAutoKey = re.compile('^config-(\\d{8})$')
    38  
    39  def call(cmd, **kwargs):
    40      print('>>> %s' % cmd)
    41      return subprocess.run(
    42          cmd,
    43          check=True,
    44          shell=True,
    45          stderr=sys.stderr,
    46          stdout=subprocess.PIPE,
    47          timeout=10,#seconds
    48          universal_newlines=True,
    49          **kwargs,
    50      )
    51  
    52  def main(args):
    53      print(args)
    54      validateArgs(args)
    55      print('Ensuring the kubeconfig current-context is not set.')
    56      call('kubectl config unset current-context')
    57  
    58      if args.auto:
    59          # We need to determine the dest key automatically.
    60          args.dest_key = time.strftime('config-%Y%m%d')
    61          if not args.src_key:
    62              # Also try to automatically determine the src key.
    63              cmd = 'kubectl --context="%s" get secret --namespace "%s" "%s" -o go-template="{{range \\$key, \\$content := .data}}{{\\$key}};{{end}}"' % (args.context, args.namespace, args.name) #pylint: disable=line-too-long
    64              keys = call(cmd).stdout.rstrip(";").split(";")
    65              matches = [key for key in keys if reAutoKey.match(key)]
    66              matches.sort(reverse=True)
    67              if len(matches) == 0:
    68                  raise ValueError('The %s/%s secret does not contain any keys matching the "config-20200730" format. Please try again with --src-key set to the most recent key. Existing keys: %s' % (args.namespace, args.name, keys)) #pylint: disable=line-too-long
    69  
    70              args.src_key = matches[0]
    71          # Only enable pruning if we won't overwrite the source key.
    72          # This ensures that a second update on the same day will still have a
    73          # key to roll back to if needed.
    74          args.prune = args.src_key != args.dest_key
    75          print('Automatic mode: --src-key=%s  --dest-key=%s' % (args.src_key, args.dest_key))
    76  
    77      with tempfile.TemporaryDirectory() as tmpdir:
    78          orig = '%s/original' % (tmpdir)
    79          merged = '%s/merged' % (tmpdir)
    80          # Copy the current secret contents into a temp file.
    81          cmd = 'kubectl --context="%s" get secret --namespace "%s" "%s" -o go-template="{{index .data \\"%s\\"}}" | base64 -d > %s' % (args.context, args.namespace, args.name, args.src_key, orig) #pylint: disable=line-too-long
    82          call(cmd)
    83  
    84          # Merge the existing and new kubeconfigs into another temp file.
    85          env = os.environ.copy()
    86          env['KUBECONFIG'] = '%s:%s' % (orig, args.kubeconfig_to_merge)
    87          call(
    88              'kubectl config view --raw > %s' % (merged),
    89              env=env,
    90          )
    91  
    92          # Update the secret with the merged config.
    93          if args.prune:
    94              # Pruning was request. Remove all keys except for dest and src (if different from dest).
    95              srcflag = ''
    96              if args.src_key != args.dest_key:
    97                  srcflag = '--from-file="%s=%s"' % (args.src_key, orig)
    98              call('kubectl --context="%s" create secret generic --namespace "%s" "%s" --from-file="%s=%s" %s --dry-run -oyaml | kubectl --context="%s" replace -f -' % (args.context, args.namespace, args.name, args.dest_key, merged, srcflag, args.context)) #pylint: disable=line-too-long
    99          else:
   100              content = ''
   101              with open(merged, 'r') as mergedFile:
   102                  yamlPad = '    '
   103                  content = yamlPad + mergedFile.read()
   104                  content = content.replace('\n', '\n' + yamlPad)
   105              call('kubectl --context="%s" patch --namespace "%s" "secret/%s" --patch "stringData:\n  %s: |\n%s\n"' % (args.context, args.namespace, args.name, args.dest_key, content)) #pylint: disable=line-too-long
   106  
   107          print('Successfully updated secret "%s/%s". The new kubeconfig is under the key "%s".' % (args.namespace, args.name, args.dest_key)) #pylint: disable=line-too-long
   108          print('Don\'t forget to update any deployments or podspecs that use the secret to reference the updated key!') #pylint: disable=line-too-long
   109  
   110  def validateArgs(args):
   111      if args.auto:
   112          if args.dest_key:
   113              raise ValueError("--dest-key must be omitted when --auto is used.")
   114      else:
   115          if not args.src_key or not args.dest_key:
   116              raise ValueError("--src-key and --dest-key are required unless --auto is used.")
   117  
   118  
   119  if __name__ == '__main__':
   120      parser = argparse.ArgumentParser(description='Merges the provided kubeconfig file into a kubeconfig file living in a kubernetes secret in order to add new cluster contexts to the secret. Requires kubectl and base64.') #pylint: disable=line-too-long
   121      parser.add_argument(
   122          '--context',
   123          help='The kubectl context of the cluster containing the secret.',
   124          required=True,
   125      )
   126      parser.add_argument(
   127          '--name',
   128          help='The name of the k8s secret containing the kubeconfig file to add to.',
   129          default='kubeconfig',
   130      )
   131      parser.add_argument(
   132          '--namespace',
   133          help='The namespace containing the kubeconfig k8s secret to add to.',
   134          default='default',
   135      )
   136      parser.add_argument(
   137          '--src-key',
   138          help='The key of the source kubeconfig file in the k8s secret.',
   139      )
   140      parser.add_argument(
   141          '--dest-key',
   142          help='The destination key of the merged kubeconfig file in the k8s secret.',
   143      )
   144      parser.add_argument(
   145          'kubeconfig_to_merge',
   146          help='Filepath of the kubeconfig file to merge into the kubeconfig secret.',
   147      )
   148      parser.add_argument(
   149          '--prune',
   150          action='store_true',
   151          help='Remove all secret keys besides the source and dest. This should be used periodically to delete old kubeconfigs and keep the secret size under control.', #pylint: disable=line-too-long
   152      )
   153      parser.add_argument(
   154          '--auto',
   155          action='store_true',
   156          help='Automatically determine --dest-key and optionally --src-key assuming keys are of the form "config-20200730". Pruning is enabled.', #pylint: disable=line-too-long
   157      )
   158  
   159      main(parser.parse_args())