From 28ef330301bc124dbbcf911ec313e2017a93e214 Mon Sep 17 00:00:00 2001 From: Peter Baumgartner Date: Tue, 2 Feb 2021 08:13:14 -0700 Subject: [PATCH] Init cmd (#11) * Add init command Squashes the 3 commands all users need to run into one. * Avoid prompting for cluster name in create region * Update init text * docs: comments --- .github/workflows/functional_tests.yml | 18 +++--- cmd/accountSetup.go | 41 -------------- cmd/create.go | 51 +++++++++++++++-- cmd/createCluster.go | 3 + cmd/createRegion.go | 16 ++++-- cmd/init.go | 78 ++++++++++++++++++++++++++ cmd/root.go | 2 +- 7 files changed, 147 insertions(+), 62 deletions(-) delete mode 100644 cmd/accountSetup.go create mode 100644 cmd/init.go diff --git a/.github/workflows/functional_tests.yml b/.github/workflows/functional_tests.yml index 232908f..7eecce7 100644 --- a/.github/workflows/functional_tests.yml +++ b/.github/workflows/functional_tests.yml @@ -21,9 +21,14 @@ jobs: name: Build run: go build - - name: Account Setup - run: ./apppack account-setup --region us-east-1 --dockerhub-username $DOCKERHUB_USERNAME --dockerhub-access-token $DOCKERHUB_ACCESS_TOKEN | tee account_setup_output.txt - timeout-minutes: 6 + name: AppPack Init + run: | + ./apppack init --region us-east-1 \ + --dockerhub-username $DOCKERHUB_USERNAME \ + --dockerhub-access-token $DOCKERHUB_ACCESS_TOKEN \ + --domain testclusters.apppack.io \ + --instance-class t3.micro | tee account_setup_output.txt + timeout-minutes: 9 env: DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} DOCKERHUB_ACCESS_TOKEN: ${{ secrets.DOCKERHUB_ACCESS_TOKEN }} @@ -41,13 +46,6 @@ jobs: AWS_ACCESS_KEY_ID: ${{ secrets.APPPACK_AWS_ACCESS_KEY_ID }} AWS_SECRET_ACCESS_KEY: ${{ secrets.APPPACK_AWS_SECRET_ACCESS_KEY }} AWS_DEFAULT_REGION: us-east-1 - - - name: Create cluster - run: ./apppack create cluster --region us-east-1 --domain testclusters.apppack.io --instance-class t3.micro - timeout-minutes: 8 - env: - AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} - AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - name: Create app run: | diff --git a/cmd/accountSetup.go b/cmd/accountSetup.go deleted file mode 100644 index 2249108..0000000 --- a/cmd/accountSetup.go +++ /dev/null @@ -1,41 +0,0 @@ -/* -Copyright © 2021 NAME HERE - -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" - - "github.com/spf13/cobra" -) - -// accountSetupCmd represents the accountSetup command -var accountSetupCmd = &cobra.Command{ - Use: "account-setup", - Short: "setup your AppPack account and create initial resources", - Long: "*Requires AWS credentials.*\n\nThis is a shortcut for `apppack create account && apppack create region`", - Run: func(cmd *cobra.Command, args []string) { - accountCmd.Run(cmd, []string{}) - fmt.Println("") - createRegionCmd.Run(cmd, []string{}) - }, -} - -func init() { - rootCmd.AddCommand(accountSetupCmd) - accountSetupCmd.Flags().StringP("dockerhub-username", "u", "", "Docker Hub username") - accountSetupCmd.Flags().StringP("dockerhub-access-token", "t", "", "Docker Hub Access Token (https://hub.docker.com/settings/security)") - accountSetupCmd.Flags().StringVar(®ion, "region", "", "AWS region to create resources in") -} diff --git a/cmd/create.go b/cmd/create.go index dc64571..ec4152e 100644 --- a/cmd/create.go +++ b/cmd/create.go @@ -28,11 +28,13 @@ import ( "github.com/AlecAivazis/survey/v2" "github.com/apppackio/apppack/auth" "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/awserr" "github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go/service/cloudformation" "github.com/aws/aws-sdk-go/service/codebuild" "github.com/aws/aws-sdk-go/service/dynamodb" "github.com/aws/aws-sdk-go/service/dynamodb/dynamodbattribute" + "github.com/aws/aws-sdk-go/service/iam" "github.com/aws/aws-sdk-go/service/route53" "github.com/aws/aws-sdk-go/service/ssm" "github.com/getsentry/sentry-go" @@ -186,6 +188,46 @@ func awsSession() (*session.Session, error) { } +// hasApppackOIDC checks for existence of our OIDC Provider +// usually we check for the existence of a Cfn Stack, but these resources are global +// and Stacks are per-region, so we need to check for this resource directly +func hasApppackOIDC(sess *session.Session) (*bool, error) { + iamSvc := iam.New(sess) + resp, err := iamSvc.ListOpenIDConnectProviders(&iam.ListOpenIDConnectProvidersInput{}) + if err != nil { + return nil, err + } + for _, r := range resp.OpenIDConnectProviderList { + oidcProvider, err := iamSvc.GetOpenIDConnectProvider(&iam.GetOpenIDConnectProviderInput{ + OpenIDConnectProviderArn: r.Arn, + }) + if err != nil { + return nil, err + } + if *oidcProvider.Url == "auth.apppack.io/" { + return aws.Bool(true), nil + } + } + return aws.Bool(false), nil +} + +// stackExists checks if a named Cfn Stack already exists in the region +func stackExists(sess *session.Session, stackName string) (*bool, error) { + cfnSvc := cloudformation.New(sess) + _, err := cfnSvc.DescribeStacks(&cloudformation.DescribeStacksInput{ + StackName: &stackName, + }) + if err != nil { + if aerr, ok := err.(awserr.Error); ok { + if fmt.Sprint(aerr.Code()) == "ValidationError" { + return aws.Bool(false), nil + } + } + return nil, err + } + return aws.Bool(true), nil +} + type stackItem struct { PrimaryID string `json:"primary_id"` SecondaryID string `json:"secondary_id"` @@ -601,12 +643,9 @@ var accountCmd = &cobra.Command{ Run: func(cmd *cobra.Command, args []string) { sess, err := awsSession() checkErr(err) - ssmSvc := ssm.New(sess) - _, err = ssmSvc.GetParameter(&ssm.GetParameterInput{ - Name: aws.String("/apppack/account"), - }) - - if err == nil { + alreadyInstalled, err := hasApppackOIDC(sess) + checkErr(err) + if *alreadyInstalled { checkErr(fmt.Errorf("account already exists")) } if createChangeSet { diff --git a/cmd/createCluster.go b/cmd/createCluster.go index f65966c..6f62802 100644 --- a/cmd/createCluster.go +++ b/cmd/createCluster.go @@ -154,6 +154,9 @@ var createClusterCmd = &cobra.Command{ func init() { createCmd.AddCommand(createClusterCmd) + // All flags need to be added to `initCmd` as well so it can call this cmd createClusterCmd.Flags().StringP("domain", "d", "", "parent domain for apps in the cluster") + initCmd.Flags().StringP("domain", "d", "", "parent domain for apps in the cluster") createClusterCmd.Flags().StringP("instance-class", "i", "t3.medium", "autoscaling instance class -- see https://aws.amazon.com/ec2/pricing/on-demand/") + initCmd.Flags().StringP("instance-class", "i", "t3.medium", "autoscaling instance class -- see https://aws.amazon.com/ec2/pricing/on-demand/") } diff --git a/cmd/createRegion.go b/cmd/createRegion.go index 4087814..3c501fe 100644 --- a/cmd/createRegion.go +++ b/cmd/createRegion.go @@ -18,6 +18,7 @@ package cmd import ( "fmt" + "github.com/AlecAivazis/survey/v2" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/cloudformation" "github.com/aws/aws-sdk-go/service/ssm" @@ -31,10 +32,14 @@ var createRegionCmd = &cobra.Command{ Long: "*Requires AWS credentials.*", DisableFlagsInUseLine: true, Run: func(cmd *cobra.Command, args []string) { - answers, err := askForMissingArgs(cmd, nil) - checkErr(err) sess, err := awsSession() checkErr(err) + questions := []*survey.Question{} + answers := make(map[string]interface{}) + addQuestionFromFlag(cmd.Flags().Lookup("dockerhub-username"), &questions, nil) + addQuestionFromFlag(cmd.Flags().Lookup("dockerhub-access-token"), &questions, nil) + err = survey.Ask(questions, &answers) + checkErr(err) ssmSvc := ssm.New(sess) if createChangeSet { fmt.Println("Creating Cloudformation Change Set for region-level resources...") @@ -49,7 +54,7 @@ var createRegionCmd = &cobra.Command{ } _, err = ssmSvc.PutParameter(&ssm.PutParameterInput{ Name: aws.String("/apppack/account/dockerhub-access-token"), - Value: getArgValue(cmd, answers, "dockerhub-access-token", true), + Value: getArgValue(cmd, &answers, "dockerhub-access-token", true), Type: aws.String("SecureString"), Tags: tags, }) @@ -65,7 +70,7 @@ var createRegionCmd = &cobra.Command{ Parameters: []*cloudformation.Parameter{ { ParameterKey: aws.String("DockerhubUsername"), - ParameterValue: getArgValue(cmd, answers, "dockerhub-username", true), + ParameterValue: getArgValue(cmd, &answers, "dockerhub-username", true), }, }, Capabilities: []*string{aws.String("CAPABILITY_IAM")}, @@ -78,7 +83,10 @@ var createRegionCmd = &cobra.Command{ func init() { createCmd.AddCommand(createRegionCmd) + // All flags need to be added to `initCmd` as well so it can call this cmd createRegionCmd.Flags().StringP("dockerhub-username", "u", "", "Docker Hub username") + initCmd.Flags().StringP("dockerhub-username", "u", "", "Docker Hub username") createRegionCmd.Flags().StringP("dockerhub-access-token", "t", "", "Docker Hub Access Token (https://hub.docker.com/settings/security)") + initCmd.Flags().StringP("dockerhub-access-token", "t", "", "Docker Hub Access Token (https://hub.docker.com/settings/security)") } diff --git a/cmd/init.go b/cmd/init.go new file mode 100644 index 0000000..a4b0433 --- /dev/null +++ b/cmd/init.go @@ -0,0 +1,78 @@ +/* +Copyright © 2021 NAME HERE + +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" + + "github.com/logrusorgru/aurora" + "github.com/spf13/cobra" +) + +// initCmd represents the init command +var initCmd = &cobra.Command{ + Use: "init", + Short: "setup your AppPack account and create initial resources", + Long: "*Requires AWS credentials.*\n\nThis is a shortcut for `apppack create account && apppack create region && apppack create cluster`", + Run: func(cmd *cobra.Command, args []string) { + sess, err := awsSession() + checkErr(err) + fmt.Print(aurora.Faint("==="), aurora.Bold(aurora.Blue("Welcome to AppPack!")), " 🎉\n\n") + fmt.Println("This will step you through the intial AppPack setup process.") + fmt.Println("Before getting started, make sure you've taken care of the prerequisites (https://docs.apppack.io/setup/#prerequisites).") + fmt.Printf("This process should take less than 10 minutes. After that, you'll be ready to start installing apps on your cluster.\n\n") + alreadyInstalled, err := hasApppackOIDC(sess) + checkErr(err) + if *alreadyInstalled { + fmt.Println("It looks like you've already setup your global AppPack account resources.") + fmt.Printf("Skipping %s\n", aurora.Bold("apppack create account")) + } else { + fmt.Printf("running %s...\n", aurora.White("apppack create account")) + accountCmd.Run(cmd, []string{}) + } + + fmt.Println("") + alreadyInstalled, err = stackExists(sess, fmt.Sprintf("apppack-region-%s", *sess.Config.Region)) + if *alreadyInstalled { + fmt.Printf("It looks like you've already setup the %s region resources.\n", *sess.Config.Region) + fmt.Printf("Skipping %s\n", aurora.Bold("apppack create region")) + } else { + fmt.Printf("running %s...\n", aurora.White("apppack create region")) + createRegionCmd.Run(cmd, []string{}) + } + + fmt.Println("") + clusterName := cmd.Flags().Lookup("cluster-name").Value.String() + alreadyInstalled, err = stackExists(sess, fmt.Sprintf("apppack-cluster-%s", clusterName)) + if *alreadyInstalled { + fmt.Printf("It looks like you've already setup a cluster named %s.\n", clusterName) + fmt.Printf("Skipping %s\n", aurora.Bold(fmt.Sprintf("apppack create cluster %s", clusterName))) + } else { + fmt.Printf("running %s...\n", aurora.White(fmt.Sprintf("apppack create cluster %s", clusterName))) + createClusterCmd.Run(cmd, []string{clusterName}) + } + + fmt.Println("") + printSuccess("AppPack initialization complete") + fmt.Print("You can now start installing apps onto your cluster.\n") + }, +} + +func init() { + rootCmd.AddCommand(initCmd) + initCmd.Flags().StringVar(®ion, "region", "", "AWS region to create resources in") + initCmd.Flags().String("cluster-name", "apppack", "name of initial cluster") +} diff --git a/cmd/root.go b/cmd/root.go index 59f299c..c8b2682 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -32,7 +32,7 @@ const () var cfgFile string const ( - timeFmt = "Jan 02, 2006 15:04:05 -0700" + timeFmt = "Jan 02, 2006 15:04:05 -0700" ) // AppName is used to hold the `--app-name` flag