k8s.io/test-infra@v0.0.0-20240520184403-27c6b4c223d8/experiment/clustersecretbackup/main.go (about)

     1  /*
     2  Copyright 2021 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 main
    18  
    19  import (
    20  	"context"
    21  	"encoding/json"
    22  	"errors"
    23  	"fmt"
    24  	"os"
    25  	"strings"
    26  	"time"
    27  
    28  	flag "github.com/spf13/pflag"
    29  
    30  	"github.com/sirupsen/logrus"
    31  	corev1 "k8s.io/api/core/v1"
    32  	_ "k8s.io/client-go/plugin/pkg/client/auth" // Enable all auth provider plugins
    33  	"k8s.io/client-go/tools/clientcmd"
    34  	"k8s.io/client-go/tools/clientcmd/api"
    35  	"k8s.io/test-infra/experiment/clustersecretbackup/secretmanager"
    36  	"k8s.io/test-infra/gencred/pkg/util"
    37  
    38  	ctrlruntimeclient "sigs.k8s.io/controller-runtime/pkg/client"
    39  )
    40  
    41  var (
    42  	defaultSecretLabels = map[string]string{
    43  		"update_time": time.Now().Format("2006-01-02-15-04-05"),
    44  		"type":        "prow_backup",
    45  		"source":      "",
    46  	}
    47  )
    48  
    49  // options are the available command-line flags.
    50  type options struct {
    51  	project            string
    52  	clusterContext     string
    53  	namespaces         []string
    54  	secrets            map[string]string
    55  	update             bool
    56  	dryRun             bool
    57  	emitExternalSecret bool
    58  	skipServiceAccount bool
    59  }
    60  
    61  type client struct {
    62  	kubeClient          ctrlruntimeclient.Client
    63  	secretmanagerClient secretmanager.ClientInterface
    64  	allSi               *corev1.SecretList
    65  	options
    66  }
    67  
    68  func escape(s string) string {
    69  	return strings.ReplaceAll(s, ".", "__d_o_t__")
    70  }
    71  
    72  func (c *client) gsmSecretName(clusterSecret *corev1.Secret) string {
    73  	// Use cluster name, namespace and secret name is almost unique identifier.
    74  	// However, if consider GCP allow creating clusters with the same name under
    75  	// different zones, probably will need to add zones to this. Will address if
    76  	// ever needed.
    77  	return fmt.Sprintf("%s__%s__%s", c.clusterContext, clusterSecret.Namespace, escape(clusterSecret.Name))
    78  }
    79  
    80  // gatherOptions parses the command-line flags.
    81  func gatherOptions(fs *flag.FlagSet, args ...string) options {
    82  	var o options
    83  	fs.StringVar(&o.project, "project", "", "GCP project used for backing up secrets")
    84  	fs.StringVar(&o.clusterContext, "cluster-context", "", "cluster context name used for backing up secrets, must be full form such as <PROVIDER>_<PROJECT>_<ZONE>_<CLUSTER>")
    85  	fs.StringSliceVar(&o.namespaces, "namespace", []string{}, "namespace to backup, can be passed in repeatedly")
    86  	fs.StringToStringVar(&o.secrets, "secret-name", nil, "namespace:name of secrets to be backed up, in the form of --secret-name=<namespace>=<name>. By default all secrets in the chosen namespace(s) are backed up.")
    87  	fs.BoolVar(&o.update, "update", false, "Controls whether update existing secret or not, if false then secret will only be created")
    88  	fs.BoolVar(&o.dryRun, "dryrun", false, "Controls whether this is dry run or not")
    89  	fs.BoolVar(&o.skipServiceAccount, "skip-sa", true, "Controls whether to skip service account tokens")
    90  	fs.BoolVar(&o.emitExternalSecret, "emit-external-secret", false, "Controls whether to output an ExternalSecret referencing the Secret")
    91  	fs.Parse(args)
    92  
    93  	return o
    94  }
    95  
    96  // validateFlags validates the command-line flags.
    97  func (o *options) validateFlags() error {
    98  	if len(o.project) == 0 {
    99  		return errors.New("--project must be provided")
   100  	}
   101  	if len(o.clusterContext) == 0 {
   102  		return errors.New("--cluster-context must be provided")
   103  	}
   104  	return nil
   105  }
   106  
   107  func newClient(o options) (*client, error) {
   108  	secretmanagerClient, err := secretmanager.NewClient(o.project, o.dryRun)
   109  	if err != nil {
   110  		return nil, fmt.Errorf("failed creating secret manager client: %w", err)
   111  	}
   112  
   113  	kubeClient, err := newKubeClients(o.clusterContext)
   114  	if err != nil {
   115  		return nil, fmt.Errorf("failed creating kube client: %w", err)
   116  	}
   117  	return &client{
   118  		kubeClient:          kubeClient,
   119  		secretmanagerClient: secretmanagerClient,
   120  		options:             o,
   121  	}, nil
   122  }
   123  
   124  func newKubeClients(clusterContext string) (ctrlruntimeclient.Client, error) {
   125  	loader := clientcmd.NewDefaultClientConfigLoadingRules()
   126  
   127  	cfg, err := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(
   128  		loader, &clientcmd.ConfigOverrides{
   129  			// Enforcing clusterContext in the full form of cluster context,
   130  			// instead of short names for kubectl.
   131  			Context:        api.Context{Cluster: clusterContext},
   132  			CurrentContext: clusterContext,
   133  		}).ClientConfig()
   134  	if err != nil {
   135  		return nil, fmt.Errorf("failed create rest config: %v ------ Did you supply the full form of cluster context name instead of short hand?", err)
   136  	}
   137  	return ctrlruntimeclient.New(cfg, ctrlruntimeclient.Options{})
   138  }
   139  
   140  // process merges secret into a new secret for write.
   141  func (c *client) updateSingleSecret(ctx context.Context, clusterSecret *corev1.Secret) error {
   142  	secretID := c.gsmSecretName(clusterSecret)
   143  	// Google secret manager expects pure string instead of map[string][]byte,
   144  	// so translate *corev1.Secret.Data to map[string]string, this will also be
   145  	// what kubernetes external secret expects.
   146  	stringData := map[string]string{}
   147  	for key, val := range clusterSecret.Data {
   148  		stringData[key] = string(val)
   149  	}
   150  	payload, err := json.Marshal(stringData)
   151  	if err != nil {
   152  		return fmt.Errorf("failed marshal secret %s: %w", clusterSecret.Name, err)
   153  	}
   154  	log := logrus.WithFields(logrus.Fields{
   155  		"project":     c.project,
   156  		"cluster":     c.clusterContext,
   157  		"namespace":   clusterSecret.Namespace,
   158  		"secret-name": clusterSecret.Name,
   159  		"gsm-secret":  secretID,
   160  	})
   161  	if sat := "kubernetes.io/service-account-token"; string(clusterSecret.Type) == sat {
   162  		log.Infof("Skipping: the secret type is %s", sat)
   163  		return nil
   164  	}
   165  	if c.dryRun {
   166  		log.Info("[Dryrun]: backing up secret")
   167  	}
   168  
   169  	ss, err := c.secretmanagerClient.ListSecrets(ctx)
   170  	if err != nil {
   171  		return err
   172  	}
   173  	var found bool
   174  	for _, s := range ss {
   175  		if strings.HasSuffix(s.Name, fmt.Sprintf("/%s", secretID)) {
   176  			found = true
   177  		}
   178  	}
   179  	if found && !c.update {
   180  		log.Info("Skipping: the secret already exist and --update is not true")
   181  		return nil
   182  	}
   183  	// Now create or update
   184  	if !found {
   185  		log.Info("Creating secret in GSM")
   186  		if _, err = c.secretmanagerClient.CreateSecret(ctx, secretID); err != nil {
   187  			return err
   188  		}
   189  	}
   190  	log.Info("Create secret version in GSM")
   191  	if err := c.secretmanagerClient.AddSecretVersion(ctx, secretID, payload); err != nil {
   192  		return err
   193  	}
   194  	return c.secretmanagerClient.AddSecretLabel(ctx, secretID, defaultSecretLabels)
   195  }
   196  
   197  func (c *client) updateAllSecrets(ctx context.Context, allowed map[string]string) error {
   198  	for _, secret := range c.allSi.Items {
   199  		if allowed != nil {
   200  			if val, ok := allowed[secret.Namespace]; !ok || val != secret.Name {
   201  				continue
   202  			}
   203  		}
   204  		if c.skipServiceAccount && secret.Type == corev1.SecretTypeServiceAccountToken {
   205  			continue
   206  		}
   207  		if err := c.updateSingleSecret(ctx, &secret); err != nil {
   208  			return err
   209  		}
   210  		if c.emitExternalSecret {
   211  			fmt.Printf(`apiVersion: kubernetes-client.io/v1
   212  kind: ExternalSecret
   213  metadata:
   214    name: "%s"
   215    namespace: "%s"
   216  spec:
   217    backendType: gcpSecretsManager
   218    projectId: "%s"
   219    dataFrom:
   220    - "%s" # Secret name in GSM
   221  ---
   222  `, secret.Name, secret.Namespace, c.project, c.gsmSecretName(&secret))
   223  		}
   224  	}
   225  	return nil
   226  }
   227  
   228  func (c *client) loadClusterSecrets(ctx context.Context) error {
   229  	c.allSi = &corev1.SecretList{}
   230  	var listOptions []ctrlruntimeclient.ListOption
   231  	for _, ns := range c.namespaces {
   232  		listOptions = append(listOptions, &ctrlruntimeclient.ListOptions{
   233  			Namespace: ns,
   234  		})
   235  	}
   236  	return c.kubeClient.List(ctx, c.allSi, listOptions...)
   237  }
   238  
   239  func main() {
   240  	o := gatherOptions(flag.NewFlagSet(os.Args[0], flag.ExitOnError), os.Args[1:]...)
   241  	if err := o.validateFlags(); err != nil {
   242  		util.PrintErrAndExit(err)
   243  	}
   244  	defaultSecretLabels["source"] = o.clusterContext
   245  
   246  	ctx := context.Background()
   247  
   248  	c, err := newClient(o)
   249  	if err != nil {
   250  		logrus.WithError(err).Fatal("Failed creating client")
   251  	}
   252  
   253  	if err := c.loadClusterSecrets(ctx); err != nil {
   254  		logrus.WithError(err).Fatal("Failed listing secrets")
   255  	}
   256  
   257  	if err := c.updateAllSecrets(ctx, o.secrets); err != nil {
   258  		logrus.WithError(err).Fatal("Failed updating secrets")
   259  	}
   260  }