Skip to content

Commit

Permalink
Utility to create service accounts
Browse files Browse the repository at this point in the history
  • Loading branch information
fejta committed Aug 22, 2019
1 parent 99dd875 commit 8139040
Show file tree
Hide file tree
Showing 3 changed files with 232 additions and 0 deletions.
1 change: 1 addition & 0 deletions experiment/BUILD.bazel
Expand Up @@ -66,6 +66,7 @@ filegroup(
"//experiment/logviewer/gcsreader:all-srcs",
"//experiment/manual-trigger:all-srcs",
"//experiment/resultstore:all-srcs",
"//experiment/service-account-creator:all-srcs",
"//experiment/slack-oncall-updater:all-srcs",
"//experiment/tracer:all-srcs",
],
Expand Down
33 changes: 33 additions & 0 deletions experiment/service-account-creator/BUILD.bazel
@@ -0,0 +1,33 @@
load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")

go_library(
name = "go_default_library",
srcs = ["main.go"],
importpath = "k8s.io/test-infra/experiment/service-account-creator",
visibility = ["//visibility:private"],
deps = [
"//prow/flagutil:go_default_library",
"//vendor/github.com/sirupsen/logrus:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/util/sets:go_default_library",
],
)

go_binary(
name = "service-account-creator",
embed = [":go_default_library"],
visibility = ["//visibility:public"],
)

filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)

filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)
198 changes: 198 additions & 0 deletions experiment/service-account-creator/main.go
@@ -0,0 +1,198 @@
/*
Copyright 2019 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package main

import (
"errors"
"flag"
"fmt"
"os"
"os/exec"
"regexp"

"github.com/sirupsen/logrus"
"k8s.io/apimachinery/pkg/util/sets"

"k8s.io/test-infra/prow/flagutil"
)

var re = regexp.MustCompile(`^([^@]+)@(.+)\.iam\.gserviceaccount\.com$`)

// ensureGloud ensures gcloud on path or prints a note of how to install.
func ensureGcloud() error {
const binary = "gcloud"
if _, err := exec.LookPath(binary); err != nil {
return fmt.Errorf("%s: %s", binary, "https://cloud.google.com/sdk/gcloud")
}
return nil
}

type options struct {
project string
serviceAccount string
addRoles flagutil.Strings
removeRoles flagutil.Strings
adds sets.String
removes sets.String
serviceAccountPrefix string
serviceAccountProject string
}

func (o options) validate() error {
if o.project == "" {
return errors.New("empty --project")
}
if o.serviceAccount == "" {
return errors.New("empty --service-account")
}
adds := o.addRoles.Strings()
removes := o.removeRoles.Strings()
if len(adds)+len(removes) == 0 {
return errors.New("--add or --remove required")
}

o.adds = sets.NewString(adds...)
o.removes = sets.NewString(removes...)
if both := o.adds.Intersection(o.removes); len(both) > 0 {
return fmt.Errorf("Cannot both add and remove roles: %v", both.List())
}
mat := re.FindStringSubmatch(o.serviceAccount)
if mat != nil {
o.serviceAccountPrefix = mat[1]
o.serviceAccountProject = mat[2]
}
return nil
}

func addFlags(fs *flag.FlagSet) *options {
var o options
fs.StringVar(&o.project, "project", "", "GCP project to change roles on")
fs.StringVar(&o.serviceAccount, "service-account", "", "Service account member to change")
fs.Var(&o.addRoles, "add", "Append to the list of roles to add")
fs.Var(&o.removeRoles, "remove", "Append to the list of roles to remove")
return &o
}

// gcloud iam service-accounts create erick-test --project=fejta-prod
func create(project, prefix string) error {
create := exec.Command("gcloud", "iam", "service-accounts", "create", "-f", "--project="+project, prefix)
create.Stderr = os.Stderr
if err := create.Start(); err != nil {
return fmt.Errorf("start: %v", err)
}
return create.Wait()
}

// gcloud iam service-accounts describe erick-test2@fejta-prod.iam.gserviceaccount.com --project=fejta-prod
func describe(user string) error {
desc := exec.Command("gcloud", "iam", "service-accounts", "describe", user)
desc.Stderr = os.Stderr
if err := desc.Start(); err != nil {
return fmt.Errorf("start: %v", err)
}
return desc.Wait()
}

// gcloud projects fejta-prod add-iam-policy-binding --member=serviceAccount:erick-test2@fejta-prod.iam.gserviceaccount.com --role=ROLE
func addPolicy(project, member, role string) error {
add := exec.Command("gcloud", "projects", project, "add-iam-policy-binding", "--member="+member, "--role="+role)
add.Stderr = os.Stderr
if err := add.Start(); err != nil {
return fmt.Errorf("start: %v", err)
}
return add.Wait()
}

// gcloud projects fejta-prod remove-iam-policy-binding --member=serviceAccount:erick-test2@fejta-prod.iam.gserviceaccount.com --role=ROLE
func removePolicy(project, member, role string) error {
remove := exec.Command("gcloud", "projects", project, "remove-iam-policy-binding", "--member="+member, "--role="+role)
remove.Stderr = os.Stderr
if err := remove.Start(); err != nil {
return fmt.Errorf("start: %v", err)
}
return remove.Wait()
}

func main() {
fs := flag.NewFlagSet(os.Args[0], flag.ExitOnError)
opt := addFlags(fs)
fs.Parse(os.Args[1:])
if err := opt.validate(); err != nil {
logrus.WithError(err).Fatal("Bad flags")
}
if err := run(*opt); err != nil {
logrus.WithError(err).Fatal("Failed")
}
}

func run(o options) error {
if err := ensureGcloud(); err != nil {
fmt.Println("gcloud is required, please install:")
fmt.Println(" *", err)
return errors.New("missing gcloud")
}

user := o.serviceAccount
if err := describe(user); err != nil {
if o.serviceAccountProject == "" {
logrus.WithField("serviceAccount", user).Warn("Cannot parse prefix and project from service account")
return fmt.Errorf("validate account pre-existence: %v", err)
}
if cerr := create(o.serviceAccountProject, o.serviceAccountPrefix); err != nil {
return fmt.Errorf("create account: %v", cerr)
}
}
if err := describe(user); err != nil {
return fmt.Errorf("validate account: %v", err)
}

member := "serviceAccount:" + user
project := o.project

var addErrors []error
var removeErrors []error
for role := range o.adds {
if err := addPolicy(project, member, role); err != nil {
logrus.WithFields(logrus.Fields{
"project": project,
"member": member,
"role": role,
}).Warn("Could not add policy")
addErrors = append(addErrors, err)
}
}

if n := len(addErrors); n > 0 {
return fmt.Errorf("%d add errors: %v", n, addErrors)
}

for role := range o.removes {
if err := removePolicy(project, member, role); err != nil {
logrus.WithFields(logrus.Fields{
"project": project,
"member": member,
"role": role,
}).Warn("Could not remove policy")
removeErrors = append(removeErrors, err)
}
}
if n := len(removeErrors); n > 0 {
return fmt.Errorf("%d remove errors: %v", n, removeErrors)
}
return nil

}

0 comments on commit 8139040

Please sign in to comment.