cmd/configure.go (275 lines of code) (raw):
/*
Copyright © 2024 Google LLC
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 cmd
import (
"fmt"
"time"
"github.com/GoogleCloudPlatform/pastures-poc-toolkit/internal/fabric"
"github.com/GoogleCloudPlatform/pastures-poc-toolkit/internal/google"
"github.com/GoogleCloudPlatform/pastures-poc-toolkit/internal/utils"
"github.com/spf13/cobra"
)
const (
pastureVer = "v1.1.4" // x-release-please-version
)
var (
// Global variables for command-line flags
orgDomain string
billingAccountId string
location string
isInternal bool
fabricVer string
prefix string
group string
orgAdminSa string
rehydrate bool
seedVer string
skipSeed bool
// static variables for prerequisites, etc
reqBinaries = map[string]string{
"gcloud": "version",
"terraform": "version",
}
groupIamRoles = []string{
"roles/billing.admin",
"roles/logging.admin",
"roles/iam.organizationRoleAdmin",
"roles/resourcemanager.projectCreator",
"roles/resourcemanager.organizationAdmin",
"roles/resourcemanager.tagAdmin",
"roles/resourcemanager.folderAdmin",
"roles/owner",
}
// global vars for other things TODO: these defaults likely belong somewhere else
gIamRoles = []string{"roles/resourcemanager.organizationAdmin"}
gIamAdditiveRoles = []string{"roles/orgpolicy.policyAdmin"}
// patch FAST not making these unique with prefixes
logSinks = fabric.LogSinks{
"audit-logs": fabric.LogSink{
"filter": "logName:\"/logs/cloudaudit.googleapis.com%2Factivity\"" +
" OR logName:\"/logs/cloudaudit.googleapis.com%2Fsystem_event\"",
"type": "logging",
},
"vpc-sc": fabric.LogSink{
"filter": "protoPayload.metadata.@type=\"type.googleapis.com/google.cloud.audit.VpcServiceControlAuditMetadata\"",
"type": "logging",
},
}
)
// configureCmd represents the configure command
var configureCmd = &cobra.Command{
Use: "configure",
Short: "Initializes environment configuration",
Long: "This command will create an environment and define its " +
"properties in a pasture configuration file, which is " +
"located by default at $HOME/.pastures/pasture.yaml.",
Args: cobra.NoArgs,
Run: func(cmd *cobra.Command, args []string) {
// Check if prereqs are in place
fmt.Println("Running preflight checks")
for k, v := range reqBinaries {
if err := utils.CheckInstalled(k, v); err != nil {
fmt.Println("Preflight checks failed")
cobra.CheckErr(err)
}
}
// Construct path for all config
path, err := utils.ConfigPath()
if err != nil {
cobra.CheckErr(err)
}
fmt.Println("Creating config directory at:", path)
if err := utils.CreateDir(path); err != nil {
fmt.Println("Config directory already exists at:", path)
}
// Create a new variable file instance
vars := fabric.LoadVarsFile(path, prefix)
// TODO: if requested, print the foundations directory path
// (to be enhanced with harvest feature)
// Authorize with Google and get current user
email, err := google.AppDefaultCredentials()
if err != nil {
fmt.Println("Unable to authorize with Google")
cobra.CheckErr(err)
}
// Establish a tfvars file from somewhere
if rehydrate {
fmt.Println("Sourcing existing configuration from GCS bucket")
// download the tfvars file
if err := vars.DownloadFile(); err != nil {
fmt.Println("Cannot download existing pastures configuration")
cobra.CheckErr(err)
}
} else {
fmt.Println("Building a new configuration file")
if err := vars.GetFileMetadata(); err == nil {
err := fmt.Errorf(
"existing pasture for prefix %s found - "+
"try running configure with --rehydrate flag", prefix,
)
cobra.CheckErr(err)
}
// Build fastConfig struct
fastConfig := fabric.NewFastConfig()
if err := fastConfig.SetOrg(orgDomain); err != nil {
cobra.CheckErr(err)
}
fastConfig.SetBilling(billingAccountId, isInternal)
fastConfig.SetUser(email)
// Enable sandbox for seeds
if skipSeed {
fastConfig.SetFeatures(false)
} else {
fastConfig.SetFeatures(true)
}
fastConfig.SetLocations(location)
if err := fastConfig.SetPrefix(prefix); err != nil {
fmt.Println("Prefix must be less than 10 characters")
cobra.CheckErr(err)
}
fastConfig.SetGroups(group)
// Add IAM policies to vars struct
if isInternal {
var adds []*fabric.IamAdditive
for _, r := range gIamRoles { // Authoritative bindings
err := fastConfig.AddIamBinding(
r, []string{"serviceAccount:" + orgAdminSa},
)
if err != nil {
cobra.CheckErr(err)
}
}
// Nonauthoritative bindings
for _, r := range gIamAdditiveRoles {
adds = append(adds, &fabric.IamAdditive{
Role: r,
Member: "serviceAccount:" + orgAdminSa,
})
}
if err := fastConfig.AddIamMember(adds); err != nil {
fmt.Println("Unable to set IAM additive policy")
cobra.CheckErr(err)
}
// Customize log sinks
// TODO: refactor to simple slice and iterate in method
fastConfig.SetLogSinks(prefix, logSinks)
}
fmt.Println("Applying prerequisite roles to group:", group)
if err := google.SetRequiredOrgIAMRoles(
fastConfig.Organization,
group,
groupIamRoles,
); err != nil {
fmt.Println("Unable to apply prerequisite roles to group:", group)
cobra.CheckErr(err)
}
fmt.Println("Waiting for role assignment propagation")
// TODO: 10 seconds may or may not be enough for propagation
time.Sleep(10 * time.Second)
// Write the tfvars file
fmt.Println("Writing configuration file to path:", vars.LocalPath)
vars.AddConfig(fastConfig)
if err := vars.Config.WriteConfig(vars.LocalPath); err != nil {
fmt.Println("Unable to write config file to path")
cobra.CheckErr(err)
}
}
// Init FAST stages
stages := fabric.InitializeFoundationStages(path, prefix, vars)
// Create seed stage shell and append to foundations
if !skipSeed {
stages = append(stages, fabric.NewSeedStage(path))
}
for i, s := range stages {
// clone repositories
if s.Type == "foundation" {
if i > 0 { // we only need to deal with foundation once
continue
} else {
fmt.Printf("Using %s tag for Fabric FAST \n", fabricVer)
s.Repository.SetRef("refs/tags/" + fabricVer)
}
} else if s.Type == "seed" {
// TODO: we don't have a seed name here; just a shell
fmt.Printf(
"Using %s tag for the Pasture seed %s \n",
seedVer, s.Name,
)
s.Repository.SetRef("refs/tags/" + seedVer)
}
fmt.Println("Cloning repository for", s.Type)
if err := s.Repository.Clone(false); err != nil {
fmt.Println("Unable to clone repository")
cobra.CheckErr(err)
}
// symlink relevant subdirs
if err := s.Repository.Link.Link(); err != nil {
fmt.Println("Unable to link repository target to directory")
cobra.CheckErr(err)
}
// configure stage factories
if s.Name == "0-bootstrap" {
fmt.Println("Updating custom role names in custom role factory")
roleFactory := fabric.NewRoleFactory(s.Path)
s.SetFactory(roleFactory)
for _, f := range s.Factories {
f.ApplyFactory(prefix)
}
}
//TODO Delete these unused stanzas?
// ****
// download providers file if rehydrating
// if rehydrate {
// fmt.Println("Sourcing provider file from GCS bucket for stage:", s.Name)
// if err := s.ProviderFile.DownloadFile(); err != nil {
// fmt.Println("Unable to source provider file for stage:", s.Name)
// cobra.CheckErr(err)
// }
// }
// initialize tf directory
// fmt.Println("Initializing FAST stage:", s.Name)
// if err := terraform.TfInit(s.Path, false); err != nil {
// fmt.Printf("Unable to intialize FAST stage: %s", s.Name)
// cobra.CheckErr(err)
// }
// ****
}
fmt.Println("\nPasture configure complete! configuration hydrated...")
},
}
func init() {
// Add the configure command to the root command
RootCmd.AddCommand(configureCmd)
// Define and add flags for the configure command
configureCmd.Flags().
StringVarP(
&orgDomain,
"domain", "d", "", "GCP organization domain name",
)
configureCmd.Flags().
StringVarP(
&billingAccountId,
"billing-account", "b", "", "GCP billing account ID",
)
configureCmd.Flags().
StringVarP(
&location,
"location", "l", "US", "GCP multi-region location code",
)
configureCmd.Flags().
StringVar(
&fabricVer,
"fabric-version", "v32.0.0", "Cloud Foundation Fabric FAST version",
)
configureCmd.Flags().
BoolVarP(
&isInternal,
"internal", "G", false, "Internal use only",
)
configureCmd.Flags().
StringVarP(
&prefix,
"prefix", "p", "",
"Prefix for resources with unique names (max 9 characters)",
)
configureCmd.Flags().
StringVarP(
&group,
"group-owner", "g", "",
"Name of Cloud Identity group that owns the pastures",
)
configureCmd.Flags().
StringVar(
&orgAdminSa,
"org-admin-sa", "",
"Service account email of the internal environment administrator",
)
configureCmd.Flags().
BoolVar(
&rehydrate, "rehydrate", false,
"Restore previous Pastures configuration saved in GCS bucket",
)
configureCmd.Flags().
StringVar(
&seedVer, "seed-version", pastureVer,
"Version of pasture seed terraform modules to use",
)
configureCmd.Flags().
BoolVar(
&skipSeed, "skip-seed", false,
"Limits deployment to FAST foundation only",
)
// One of these flags is required
configureCmd.MarkFlagsOneRequired("domain", "rehydrate")
configureCmd.MarkFlagsMutuallyExclusive("domain", "rehydrate")
// New config flag group
configureCmd.MarkFlagsRequiredTogether(
"domain",
"billing-account",
"group-owner",
)
// Internal environment flag group
configureCmd.MarkFlagsRequiredTogether("internal", "org-admin-sa")
configureCmd.MarkFlagsMutuallyExclusive("rehydrate", "internal")
// These flags are always required
if err := configureCmd.MarkFlagRequired("prefix"); err != nil {
cobra.CheckErr(err)
}
// Hide the internal flags
if err := configureCmd.Flags().MarkHidden("internal"); err != nil {
cobra.CheckErr(err)
}
if err := configureCmd.Flags().MarkHidden("org-admin-sa"); err != nil {
cobra.CheckErr(err)
}
if err := configureCmd.Flags().MarkHidden("skip-seed"); err != nil {
cobra.CheckErr(err)
}
// if err := configureCmd.Flags().MarkHidden("fabric-version"); err != nil {
// cobra.CheckErr(err)
// }
}