// Package project contains convenience functions for working with GCP projects, eg creation, deletion, // updating billing accounts package project import ( "fmt" "log" "math/rand" "regexp" "time" cb "google.golang.org/api/cloudbilling/v1" crm "google.golang.org/api/cloudresourcemanager/v1" ) const ( // ProjectIDRegexpString is the regular expression that represents valid GCP // Project ID strings, per https://cloud.google.com/resource-manager/docs/creating-managing-projects#before_you_begin ProjectIDRegexpString = `^[a-z][-a-z0-9]{5,29}[a-z0-9]$` ) func init() { _, err := regexp.Compile(ProjectIDRegexpString) if err != nil { log.Fatal("project.init: failed to compile the ProjectID regular expression") } } // CreateWithBillingAccount creates a new gcp project and associates it with the given billing // account. func CreateWithBillingAccount(crmSvc *crm.Service, cbSvc *cb.APIService, name, folderID, billing string) (*crm.Project, error) { // stay <= 30 characters for gcp naming requirements, with enough room for sufficient // randomness if len(name) > 19 { return nil, fmt.Errorf("len of project name cant be > 19, save room for random id generation") } projectID := fmt.Sprintf("%s-%s", name, RandAN(29-len(name))) project := &crm.Project{ Name: name, ProjectId: projectID, Parent: &crm.ResourceId{ Id: folderID, Type: "folder", }, } createProjectOp, err := crmSvc.Projects.Create(project).Do() if err != nil { return nil, err } for { if createProjectOp.Done { break } createProjectOp, err = crmSvc.Operations.Get(createProjectOp.Name).Do() if err != nil { return nil, err } time.Sleep(5 * time.Second) } newProject, err := crmSvc.Projects.Get(projectID).Do() if err != nil { return nil, err } pbi := &cb.ProjectBillingInfo{ BillingAccountName: fmt.Sprintf("billingAccounts/%s", billing), } _, err = cbSvc.Projects.UpdateBillingInfo(fmt.Sprintf("projects/%s", projectID), pbi).Do() if err != nil { return nil, err } return newProject, nil } // DeleteProject deletes a gcp project associated to the // provided project id. func DeleteProject(crmSvc *crm.Service, projectID string) error { _, err := crmSvc.Projects.Delete(projectID).Do() if err != nil { return err } return nil } // generates random names that conform to gcp project naming requirements func RandAN(n int) string { var runes = []rune("abcdefghijklmnopqrstuvwxyz0123456789") genName := make([]rune, n) for i := range genName { genName[i] = runes[rand.Intn(len(runes))] // #nosec G404 - random string is cosmetic } return string(genName) } // IsValidProjectID checks the input string against the regular expression for // valid GCP Project IDs func IsValidProjectID(id string) error { // We can drop the error because we confirm the Regexp string compiles during // init, which is the only type of error regexp.MatchString can return ok, _ := regexp.MatchString(ProjectIDRegexpString, id) if !ok { return &IDValidationError{id} } return nil } // IDValidationError is a contextualized validation error for the Project ID type IDValidationError struct { id string } // Error prints the contextualized validaton error message for the Project ID func (e *IDValidationError) Error() string { return fmt.Sprintf("invalid GCP Project ID: %s", e.id) }