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 }