cmd/setupgh.go (288 lines of code) (raw):
package cmd
import (
"context"
"errors"
"fmt"
"os"
"path/filepath"
"strings"
"unicode"
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources"
"github.com/Azure/draft/pkg/cred"
"github.com/Azure/draft/pkg/prompts"
"github.com/manifoldco/promptui"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/Azure/draft/pkg/providers"
"github.com/Azure/draft/pkg/spinner"
"k8s.io/apimachinery/pkg/util/validation"
)
func newSetUpCmd() *cobra.Command {
sc := &providers.SetUpCmd{}
// setup-ghCmd represents the setup-gh command
var cmd = &cobra.Command{
Use: "setup-gh",
Short: "Automates the Github OIDC setup process",
Long: `This command will automate the Github OIDC setup process by creating an Azure Active Directory
application and service principle, and will configure that application to trust github.`,
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
gh := providers.NewGhClient()
azCred, err := cred.GetCred()
if err != nil {
return fmt.Errorf("getting credentials: %w", err)
}
az, err := providers.NewAzClient(azCred)
if err != nil {
return fmt.Errorf("creating azure client: %w", err)
}
sc.AzClient = az
err = fillSetUpConfig(sc, gh, az)
if err != nil {
return fmt.Errorf("filling setup config: %w", err)
}
s := spinner.CreateSpinner("--> Setting up Github OIDC...")
s.Start()
err = runProviderSetUp(ctx, sc, s, gh, az)
s.Stop()
if err != nil {
return err
}
log.Info("Draft has successfully set up Github OIDC for your project 😃")
log.Info("Use 'draft generate-workflow' to generate a Github workflow to build and deploy an application on AKS.")
return nil
},
}
f := cmd.Flags()
f.StringVarP(&sc.AppName, "app", "a", emptyDefaultFlagValue, "specify the Azure Active Directory application name")
f.StringVarP(&sc.SubscriptionID, "subscription-id", "s", emptyDefaultFlagValue, "specify the Azure subscription ID")
f.StringVarP(&sc.ResourceGroupName, "resource-group", "r", emptyDefaultFlagValue, "specify the Azure resource group name")
f.StringVarP(&sc.Repo, "gh-repo", "g", emptyDefaultFlagValue, "specify the github repository link")
sc.Provider = provider
return cmd
}
func fillSetUpConfig(sc *providers.SetUpCmd, gh providers.GhClient, az providers.AzClientInterface) error {
if sc.TenantId == "" {
tenandId, err := providers.PromptTenantId(sc.AzClient, context.Background())
if err != nil {
return fmt.Errorf("prompting tenant ID: %w", err)
}
sc.TenantId = tenandId
}
if sc.AppName == "" {
// Set the application name; default is the current directory name plus "-workflow".
// get the current directory name
currentDir, err := os.Getwd()
if err != nil {
return fmt.Errorf("getting current directory: %w", err)
}
defaultAppName := fmt.Sprintf("%s-workflow", filepath.Base(currentDir))
defaultAppName, err = ToValidAppName(defaultAppName)
if err != nil {
log.Debugf("unable to convert default app name %q to a valid name: %v", defaultAppName, err)
log.Debugf("using default app name %q", defaultAppName)
defaultAppName = "my-workflow"
}
appName, err := PromptAppName(sc.AzClient, defaultAppName)
if err != nil {
return fmt.Errorf("prompting app name: %w", err)
}
sc.AppName = appName
}
if sc.SubscriptionID == "" {
if strings.ToLower(sc.Provider) == "azure" {
currentSub, err := az.GetCurrentAzSubscriptionLabel()
if err != nil {
return fmt.Errorf("getting current subscription ID: %w", err)
}
subLabels, err := az.GetAzSubscriptionLabels()
if err != nil {
return fmt.Errorf("getting subscription labels: %w", err)
}
sc.SubscriptionID, err = getAzSubscriptionId(subLabels, currentSub)
if err != nil {
return fmt.Errorf("getting subscription ID: %w", err)
}
} else {
sc.SubscriptionID = getSubscriptionID()
}
}
if sc.ResourceGroupName == "" {
rg, err := PromptResourceGroup(sc.AzClient, sc.SubscriptionID)
if err != nil {
return fmt.Errorf("getting resource group: %w", err)
}
sc.ResourceGroupName = *rg.Name
}
if sc.Repo == "" {
repo, err := PromptGitHubRepoWithOwner(gh)
if err != nil {
return fmt.Errorf("failed to prompt for GitHub repository: %w", err)
}
if repo == "" {
return errors.New("github repo cannot be empty")
}
sc.Repo = repo
}
return nil
}
func ToValidAppName(name string) (string, error) {
// replace all underscores with hyphens
cleanedName := strings.ReplaceAll(name, "_", "-")
// replace all spaces with hyphens
cleanedName = strings.ReplaceAll(cleanedName, " ", "-")
// remove leading non-alphanumeric characters
for i, r := range cleanedName {
if unicode.IsLetter(r) || unicode.IsNumber(r) {
cleanedName = cleanedName[i:]
break
}
}
// remove trailing non-alphanumeric characters
for i := len(cleanedName) - 1; i >= 0; i-- {
r := rune(cleanedName[i])
if unicode.IsLetter(r) || unicode.IsNumber(r) {
cleanedName = cleanedName[:i+1]
break
}
}
// remove all characters except alphanumeric, '-', '.'
var builder strings.Builder
for _, r := range cleanedName {
if unicode.IsLetter(r) || unicode.IsNumber(r) || r == '-' {
builder.WriteRune(r)
}
}
// lowercase the name
cleanedName = strings.ToLower(builder.String())
if err := ValidateAppName(cleanedName); err != nil {
return "", fmt.Errorf("app name '%s' could not be converted to a valid name: %w", name, err)
}
return cleanedName, nil
}
func runProviderSetUp(ctx context.Context, sc *providers.SetUpCmd, s spinner.Spinner, gh providers.GhClient, az providers.AzClientInterface) error {
provider := strings.ToLower(sc.Provider)
if provider == "azure" {
// call azure provider logic
return providers.InitiateAzureOIDCFlow(ctx, sc, s, gh, az)
} else {
// call logic for user-submitted provider
fmt.Printf("The provider is %v\n", sc.Provider)
}
return nil
}
func ValidateAppName(name string) error {
errors := validation.IsDNS1123Label(name)
if len(errors) > 0 {
return fmt.Errorf("invalid app name: %s", strings.Join(errors, ", "))
}
return nil
}
func PromptAppName(az providers.AzClientInterface, defaultAppName string) (string, error) {
appNamePrompt := &promptui.Prompt{
Label: "Enter app registration name",
Validate: ValidateAppName,
Default: defaultAppName,
}
appName, err := appNamePrompt.Run()
if err != nil {
return "", err
}
if az.AzAppExists(appName) {
confirmAppExistsPrompt := promptui.Prompt{
Label: "An app with this name already exists. Would you like to use it?",
IsConfirm: true,
}
_, err := confirmAppExistsPrompt.Run()
if err != nil {
return PromptAppName(az, defaultAppName)
}
} else {
log.Debugf("App %q does not exist, will be created", appName)
}
return appName, nil
}
func getSubscriptionID() string {
validate := func(input string) error {
if input == "" {
return errors.New("invalid subscription id")
}
return nil
}
prompt := promptui.Prompt{
Label: "Enter subscription ID",
Validate: validate,
}
result, err := prompt.Run()
if err != nil {
return err.Error()
}
return result
}
func PromptResourceGroup(az providers.AzClientInterface, subscriptionID string) (armresources.ResourceGroup, error) {
var rg armresources.ResourceGroup
log.Println("Fetching resource groups...")
rgs, err := az.ListResourceGroups(context.Background(), subscriptionID)
if err != nil {
return rg, fmt.Errorf("listing resource groups: %w", err)
}
rg, err = prompts.Select("Please choose the resource group you would like to use", rgs, &prompts.SelectOpt[armresources.ResourceGroup]{
Field: func(rg armresources.ResourceGroup) string {
return *rg.Name + " (" + *rg.Location + ")"
},
})
if err != nil {
return rg, fmt.Errorf("selecting resource group: %w", err)
}
return rg, nil
}
func PromptGitHubRepoWithOwner(gh providers.GhClient) (string, error) {
defaultRepoNameWithOwner, err := gh.GetRepoNameWithOwner()
if err != nil {
return "", err
}
log.Println("Prompting for github repo with owner name...")
repoPrompt := promptui.Prompt{
Label: "Enter github organization and repo organization and repoName",
Validate: func(input string) error {
if !strings.Contains(input, "/") {
return errors.New("github repo cannot be empty")
}
return nil
},
Default: defaultRepoNameWithOwner,
}
repo, err := repoPrompt.Run()
if err != nil {
return "", fmt.Errorf("running repo name with owner prompt: %w", err)
}
log.Debug("Validating github repo...")
if err := gh.IsValidGhRepo(repo); err != nil {
confirmMissingRepoPrompt := promptui.Prompt{
Label: "Unable to confirm this repo exists. Do you want to proceed anyway?",
IsConfirm: true,
}
_, err := confirmMissingRepoPrompt.Run()
if err != nil {
return PromptGitHubRepoWithOwner(gh)
}
} else {
log.Debugf("Github repo %q is valid", repo)
}
return repo, nil
}
func getCloudProvider() string {
selection := &promptui.Select{
Label: "What cloud provider would you like to use?",
Items: []string{"azure"},
}
_, selectResponse, err := selection.Run()
if err != nil {
return err.Error()
}
return selectResponse
}
func getAzSubscriptionId(subLabels []providers.SubLabel, currentSub providers.SubLabel) (string, error) {
subLabel, err := prompts.Select("Please choose the subscription ID you would like to use", subLabels, &prompts.SelectOpt[providers.SubLabel]{
Field: func(subLabel providers.SubLabel) string {
return subLabel.Name + " (" + subLabel.ID + ")"
},
Default: ¤tSub,
})
if err != nil {
return "", fmt.Errorf("selecting subscription ID: %w", err)
}
return subLabel.ID, nil
}
func init() {
rootCmd.AddCommand(newSetUpCmd())
}