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

     1  /*
     2  Copyright 2019 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  	"errors"
    21  	"flag"
    22  	"fmt"
    23  	"os"
    24  	"os/exec"
    25  	"regexp"
    26  
    27  	"github.com/sirupsen/logrus"
    28  	"k8s.io/apimachinery/pkg/util/sets"
    29  
    30  	"sigs.k8s.io/prow/pkg/flagutil"
    31  )
    32  
    33  var re = regexp.MustCompile(`^([^@]+)@(.+)\.iam\.gserviceaccount\.com$`)
    34  
    35  // ensureGloud ensures gcloud on path or prints a note of how to install.
    36  func ensureGcloud() error {
    37  	const binary = "gcloud"
    38  	if _, err := exec.LookPath(binary); err != nil {
    39  		return fmt.Errorf("%s: %s", binary, "https://cloud.google.com/sdk/gcloud")
    40  	}
    41  	return nil
    42  }
    43  
    44  type options struct {
    45  	project               string
    46  	serviceAccount        string
    47  	addRoles              flagutil.Strings
    48  	removeRoles           flagutil.Strings
    49  	adds                  sets.Set[string]
    50  	removes               sets.Set[string]
    51  	serviceAccountPrefix  string
    52  	serviceAccountProject string
    53  }
    54  
    55  func (o options) validate() error {
    56  	if o.project == "" {
    57  		return errors.New("empty --project")
    58  	}
    59  	if o.serviceAccount == "" {
    60  		return errors.New("empty --service-account")
    61  	}
    62  	adds := o.addRoles.Strings()
    63  	removes := o.removeRoles.Strings()
    64  	if len(adds)+len(removes) == 0 {
    65  		return errors.New("--add or --remove required")
    66  	}
    67  
    68  	o.adds = sets.New[string](adds...)
    69  	o.removes = sets.New[string](removes...)
    70  	if both := o.adds.Intersection(o.removes); len(both) > 0 {
    71  		return fmt.Errorf("cannot both add and remove roles: %v", sets.List(both))
    72  	}
    73  	mat := re.FindStringSubmatch(o.serviceAccount)
    74  	if mat != nil {
    75  		o.serviceAccountPrefix = mat[1]
    76  		o.serviceAccountProject = mat[2]
    77  	}
    78  	return nil
    79  }
    80  
    81  func addFlags(fs *flag.FlagSet) *options {
    82  	var o options
    83  	fs.StringVar(&o.project, "project", "", "GCP project to change roles on")
    84  	fs.StringVar(&o.serviceAccount, "service-account", "", "Service account member to change")
    85  	fs.Var(&o.addRoles, "add", "Append to the list of roles to add")
    86  	fs.Var(&o.removeRoles, "remove", "Append to the list of roles to remove")
    87  	return &o
    88  }
    89  
    90  // gcloud iam service-accounts create erick-test --project=fejta-prod
    91  func create(project, prefix string) error {
    92  	create := exec.Command("gcloud", "iam", "service-accounts", "create", "-f", "--project="+project, prefix)
    93  	create.Stderr = os.Stderr
    94  	if err := create.Start(); err != nil {
    95  		return fmt.Errorf("start: %w", err)
    96  	}
    97  	return create.Wait()
    98  }
    99  
   100  // gcloud iam service-accounts describe erick-test2@fejta-prod.iam.gserviceaccount.com --project=fejta-prod
   101  func describe(user string) error {
   102  	desc := exec.Command("gcloud", "iam", "service-accounts", "describe", user)
   103  	desc.Stderr = os.Stderr
   104  	if err := desc.Start(); err != nil {
   105  		return fmt.Errorf("start: %w", err)
   106  	}
   107  	return desc.Wait()
   108  }
   109  
   110  // gcloud projects fejta-prod add-iam-policy-binding --member=serviceAccount:erick-test2@fejta-prod.iam.gserviceaccount.com --role=ROLE
   111  func addPolicy(project, member, role string) error {
   112  	add := exec.Command("gcloud", "projects", project, "add-iam-policy-binding", "--member="+member, "--role="+role)
   113  	add.Stderr = os.Stderr
   114  	if err := add.Start(); err != nil {
   115  		return fmt.Errorf("start: %w", err)
   116  	}
   117  	return add.Wait()
   118  }
   119  
   120  // gcloud projects fejta-prod remove-iam-policy-binding --member=serviceAccount:erick-test2@fejta-prod.iam.gserviceaccount.com --role=ROLE
   121  func removePolicy(project, member, role string) error {
   122  	remove := exec.Command("gcloud", "projects", project, "remove-iam-policy-binding", "--member="+member, "--role="+role)
   123  	remove.Stderr = os.Stderr
   124  	if err := remove.Start(); err != nil {
   125  		return fmt.Errorf("start: %w", err)
   126  	}
   127  	return remove.Wait()
   128  }
   129  
   130  func main() {
   131  	fs := flag.NewFlagSet(os.Args[0], flag.ExitOnError)
   132  	opt := addFlags(fs)
   133  	fs.Parse(os.Args[1:])
   134  	if err := opt.validate(); err != nil {
   135  		logrus.WithError(err).Fatal("Bad flags")
   136  	}
   137  	if err := run(*opt); err != nil {
   138  		logrus.WithError(err).Fatal("Failed")
   139  	}
   140  }
   141  
   142  func run(o options) error {
   143  	if err := ensureGcloud(); err != nil {
   144  		fmt.Println("gcloud is required, please install:")
   145  		fmt.Println("  *", err)
   146  		return errors.New("missing gcloud")
   147  	}
   148  
   149  	user := o.serviceAccount
   150  	if err := describe(user); err != nil {
   151  		if o.serviceAccountProject == "" {
   152  			logrus.WithField("serviceAccount", user).Warn("Cannot parse prefix and project from service account")
   153  			return fmt.Errorf("validate account pre-existence: %w", err)
   154  		}
   155  		if cerr := create(o.serviceAccountProject, o.serviceAccountPrefix); cerr != nil {
   156  			return fmt.Errorf("create account: %w", cerr)
   157  		}
   158  	}
   159  	if err := describe(user); err != nil {
   160  		return fmt.Errorf("validate account: %w", err)
   161  	}
   162  
   163  	member := "serviceAccount:" + user
   164  	project := o.project
   165  
   166  	var addErrors []error
   167  	var removeErrors []error
   168  	for role := range o.adds {
   169  		if err := addPolicy(project, member, role); err != nil {
   170  			logrus.WithFields(logrus.Fields{
   171  				"project": project,
   172  				"member":  member,
   173  				"role":    role,
   174  			}).Warn("Could not add policy")
   175  			addErrors = append(addErrors, err)
   176  		}
   177  	}
   178  
   179  	if n := len(addErrors); n > 0 {
   180  		return fmt.Errorf("%d add errors: %v", n, addErrors)
   181  	}
   182  
   183  	for role := range o.removes {
   184  		if err := removePolicy(project, member, role); err != nil {
   185  			logrus.WithFields(logrus.Fields{
   186  				"project": project,
   187  				"member":  member,
   188  				"role":    role,
   189  			}).Warn("Could not remove policy")
   190  			removeErrors = append(removeErrors, err)
   191  		}
   192  	}
   193  	if n := len(removeErrors); n > 0 {
   194  		return fmt.Errorf("%d remove errors: %v", n, removeErrors)
   195  	}
   196  	return nil
   197  
   198  }