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 }