cmd/rootCmd.go (146 lines of code) (raw):
// MIT License
//
// Copyright (c) Microsoft Corporation.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE
package main
import (
"fmt"
"os"
"path/filepath"
"strings"
"github.com/Azure/mpf/pkg/domain"
"github.com/Azure/mpf/pkg/infrastructure/mpfSharedUtils"
"github.com/google/uuid"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"github.com/spf13/viper"
log "github.com/sirupsen/logrus"
)
var (
defaultConfigFilename = "stingoftheviper"
envPrefix = "MPF"
replaceHyphenWithCamelCase = false
flgSubscriptionID string
flgTenantID string
flgSPClientID string
flgSPObjectID string
flgSPClientSecret string
flgShowDetailedOutput bool
flgJSONOutput bool
flgVerbose bool
flgDebug bool
// RootCmd *cobra.Command
)
func NewRootCommand() *cobra.Command {
rootCmd := &cobra.Command{
Use: "azmpf",
Short: "Find minimum permissions required for Azure deployments",
Long: `Find minimum permissions required for Azure deployments including ARM and Terraform based deployments. For example:
This CLI allows you to find the minimum permissions required for Azure deployments including ARM and Terraform based deployments.
A Service Principal is required to run this CLI. All permissions associated with the Service principal are initially wiped by this command:`,
Example: `azmpf arm --subscriptionID <subscriptionID> --tenantID <tenantID> --spClientID <spClientID> --spObjectID <spObjectID> --spClientSecret <spClientSecret>
az-mpm terraform --subscriptionID <subscriptionID> --tenantID <tenantID> --spClientID <spClientID> --spObjectID <spObjectID> --spClientSecret <spClientSecret> --executablePath <executablePath> --workingDir <workingDir> --varFilePath <varFilePath>
`,
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
return initializeConfig(cmd)
},
Run: func(cmd *cobra.Command, args []string) {
},
}
// Define cobra flags, the default value has the lowest (least significant) precedence
rootCmd.PersistentFlags().StringVarP(&flgSubscriptionID, "subscriptionID", "s", "", "Azure Subscription ID")
rootCmd.PersistentFlags().StringVarP(&flgTenantID, "tenantID", "", "", "Azure Tenant ID")
rootCmd.PersistentFlags().StringVarP(&flgSPClientID, "spClientID", "", "", "Service Principal Client ID")
rootCmd.PersistentFlags().StringVarP(&flgSPObjectID, "spObjectID", "", "", "Service Principal Object ID")
rootCmd.PersistentFlags().StringVarP(&flgSPClientSecret, "spClientSecret", "", "", "Service Principal Client Secret")
rootCmd.PersistentFlags().BoolVarP(&flgShowDetailedOutput, "showDetailedOutput", "", false, "Show detailed output")
rootCmd.PersistentFlags().BoolVarP(&flgJSONOutput, "jsonOutput", "", false, "Output in JSON format")
rootCmd.PersistentFlags().BoolVarP(&flgVerbose, "verbose", "v", false, "verbose output")
rootCmd.PersistentFlags().BoolVarP(&flgDebug, "debug", "d", false, "debug output")
err := rootCmd.MarkPersistentFlagRequired("subscriptionID")
if err != nil {
log.Errorf("Error marking flag required for subscription ID: %v\n", err)
}
err = rootCmd.MarkPersistentFlagRequired("tenantID")
if err != nil {
log.Errorf("Error marking flag required for tenant ID: %v\n", err)
}
err = rootCmd.MarkPersistentFlagRequired("spClientID")
if err != nil {
log.Errorf("Error marking flag required for SP client ID: %v\n", err)
}
err = rootCmd.MarkPersistentFlagRequired("spObjectID")
if err != nil {
log.Errorf("Error marking flag required for SP object ID: %v\n", err)
}
err = rootCmd.MarkPersistentFlagRequired("spClientSecret")
if err != nil {
log.Errorf("Error marking flag required for SP client secret: %v\n", err)
}
rootCmd.MarkFlagsMutuallyExclusive("showDetailedOutput", "jsonOutput")
// Add subcommands
rootCmd.AddCommand(NewARMCommand())
rootCmd.AddCommand(NewBicepCommand())
rootCmd.AddCommand(NewTerraformCommand())
return rootCmd
}
func initializeConfig(cmd *cobra.Command) error {
v := viper.New()
v.SetConfigName(defaultConfigFilename)
v.AddConfigPath(".")
if err := v.ReadInConfig(); err != nil {
// It's okay if there isn't a config file
if _, ok := err.(viper.ConfigFileNotFoundError); !ok {
return err
}
}
v.SetEnvPrefix(envPrefix)
v.SetEnvKeyReplacer(strings.NewReplacer("-", "_"))
v.AutomaticEnv()
bindFlags(cmd, v)
return nil
}
// Bind each cobra flag to its associated viper configuration (config file and environment variable)
func bindFlags(cmd *cobra.Command, v *viper.Viper) {
cmd.Flags().VisitAll(func(f *pflag.Flag) {
// Determine the naming convention of the flags when represented in the config file
configName := f.Name
// If using camelCase in the config file, replace hyphens with a camelCased string.
// Since viper does case-insensitive comparisons, we don't need to bother fixing the case, and only need to remove the hyphens.
if replaceHyphenWithCamelCase {
configName = strings.ReplaceAll(f.Name, "-", "")
}
// Apply the viper config value to the flag when the flag is not set and viper has a value
if !f.Changed && v.IsSet(configName) {
val := v.Get(configName)
err := cmd.Flags().Set(f.Name, fmt.Sprintf("%v", val))
if err != nil {
log.Errorf("Error setting flag %s: %v\n", f.Name, err)
}
}
})
}
func setLogLevel() {
if flgVerbose {
log.SetLevel(log.InfoLevel)
}
if flgDebug {
log.SetLevel(log.DebugLevel)
}
}
func getRootMPFConfig() domain.MPFConfig {
mpfRole := domain.Role{}
roleDefUUID, _ := uuid.NewRandom()
mpfRole.RoleDefinitionID = roleDefUUID.String()
mpfRole.RoleDefinitionName = fmt.Sprintf("tmp-rol-%s", mpfSharedUtils.GenerateRandomString(7))
mpfRole.RoleDefinitionResourceID = fmt.Sprintf("/subscriptions/%s/providers/Microsoft.Authorization/roleDefinitions/%s", flgSubscriptionID, mpfRole.RoleDefinitionID)
log.Infoln("roleDefinitionResourceID:", mpfRole.RoleDefinitionResourceID)
return domain.MPFConfig{
SubscriptionID: flgSubscriptionID,
TenantID: flgTenantID,
SP: domain.ServicePrincipal{
SPClientID: flgSPClientID,
SPObjectID: flgSPObjectID,
SPClientSecret: flgSPClientSecret,
},
Role: mpfRole,
}
}
func getAbsolutePath(path string) (string, error) {
absPath := path
if !filepath.IsAbs(path) {
absWorkingDir, err := os.Getwd()
if err != nil {
return "", err
}
absPath = absWorkingDir + "/" + absPath
}
return absPath, nil
}