Skip to content

Commit

Permalink
Add ci user to AWS
Browse files Browse the repository at this point in the history
Now creates ECR repos and makes a CI user during the bootstrap, storing the credentials in the secrets manager
Cleaned up the create process a bit
  • Loading branch information
bmonkman committed Dec 6, 2019
1 parent 2ba4753 commit d3ba846
Show file tree
Hide file tree
Showing 10 changed files with 239 additions and 70 deletions.
101 changes: 62 additions & 39 deletions cmd/create.go
Expand Up @@ -35,62 +35,82 @@ func Create(projectName string, outDir string, t *templator.Templator) string {
log.Fatalln(aurora.Red(emoji.Sprintf(":exclamation: Error creating root: %v ", err)))
}

// @TODO : Clean up the following aws stuff
projectConfig := defaultProjConfig(projectName)

chooseCloudProvider(&projectConfig)

s := secrets.GetSecrets(rootDir)

fillProviderDetails(&projectConfig, s)

var wg sync.WaitGroup
util.TemplateFileIfDoesNotExist(rootDir, util.CommitYml, t.Commit0, &wg, projectConfig)
util.TemplateFileIfDoesNotExist(rootDir, ".gitignore", t.GitIgnore, &wg, projectName)

wg.Wait()
return rootDir
}

func chooseCloudProvider(projectConfig *util.ProjectConfiguration) {
providerPrompt := promptui.Select{
Label: "Select Cloud Provider",
Items: []string{"Amazon AWS", "Google GCP", "Microsoft Azure"},
}

_, _, err = providerPrompt.Run()

regionPrompt := promptui.Select{
Label: "Select AWS Region ",
Items: []string{"us-west-1", "us-west-2", "us-east-1", "us-east-2", "ca-central-1",
"eu-central-1", "eu-west-1", "ap-east-1", "ap-south-1"},
}

_, regionResult, err := regionPrompt.Run()

_, providerResult, err := providerPrompt.Run()
if err != nil {
log.Fatalf("Prompt failed %v\n", err)
panic(err)
}

s := secrets.GetSecrets(rootDir)

sess, err := session.NewSession(&aws.Config{
Region: aws.String(regionResult),
Credentials: credentials.NewStaticCredentials(s.AWS.AccessKeyID, s.AWS.SecretAccessKey, ""),
})
if providerResult == "Amazon AWS" {
// @TODO : Move this stuff from util into another package
projectConfig.Infrastructure.AWS = &util.AWS{}
regionPrompt := promptui.Select{
Label: "Select AWS Region ",
Items: []string{"us-west-1", "us-west-2", "us-east-1", "us-east-2", "ca-central-1",
"eu-central-1", "eu-west-1", "ap-east-1", "ap-south-1"},
}

svc := sts.New(sess)
input := &sts.GetCallerIdentityInput{}
_, regionResult, err := regionPrompt.Run()

awsCaller, err := svc.GetCallerIdentity(input)
if err != nil {
if aerr, ok := err.(awserr.Error); ok {
switch aerr.Code() {
default:
log.Fatalf(aerr.Error())
}
} else {
log.Fatalf(err.Error())
if err != nil {
log.Fatalf("Prompt failed %v\n", err)
panic(err)
}
}

defaultProjConfig := defaultProjConfig(projectName)

defaultProjConfig.Infrastructure.AWS.Region = regionResult
if awsCaller != nil && awsCaller.Account != nil {
defaultProjConfig.Infrastructure.AWS.AccountID = *awsCaller.Account
projectConfig.Infrastructure.AWS.Region = regionResult
} else {
log.Fatalf("Only the AWS provider is available at this time")
}
}

var wg sync.WaitGroup
util.TemplateFileIfDoesNotExist(rootDir, util.CommitYml, t.Commit0, &wg, defaultProjConfig)
util.TemplateFileIfDoesNotExist(rootDir, ".gitignore", t.GitIgnore, &wg, projectName)
func fillProviderDetails(projectConfig *util.ProjectConfiguration, s secrets.Secrets) {
if projectConfig.Infrastructure.AWS != nil {
sess, err := session.NewSession(&aws.Config{
Region: aws.String(projectConfig.Infrastructure.AWS.Region),
Credentials: credentials.NewStaticCredentials(s.AWS.AccessKeyID, s.AWS.SecretAccessKey, ""),
})

svc := sts.New(sess)
input := &sts.GetCallerIdentityInput{}

awsCaller, err := svc.GetCallerIdentity(input)
if err != nil {
if aerr, ok := err.(awserr.Error); ok {
switch aerr.Code() {
default:
log.Fatalf(aerr.Error())
}
} else {
log.Fatalf(err.Error())
}
}

wg.Wait()
return rootDir
if awsCaller != nil && awsCaller.Account != nil {
projectConfig.Infrastructure.AWS.AccountID = *awsCaller.Account
}
}
}

func defaultProjConfig(projectName string) util.ProjectConfiguration {
Expand All @@ -109,6 +129,9 @@ func defaultProjConfig(projectName string) util.ProjectConfiguration {
Language: "go",
GitRepo: "github.com/test/repo",
}},
Infrastructure: util.Infrastructure{
AWS: nil,
},
}
}

Expand Down
11 changes: 8 additions & 3 deletions internal/generate/terraform/generate.go
Expand Up @@ -67,9 +67,14 @@ func Init(cfg *config.Commit0Config, pathPrefix string) {
pathPrefix = filepath.Join(pathPrefix, "terraform")

// @TODO : A check here would be nice to see if this stuff exists first, mostly for testing
log.Println(aurora.Cyan(emoji.Sprintf(":alarm_clock: Initializing remote backend...")))
util.ExecuteCommand(exec.Command("terraform", "init"), filepath.Join(pathPrefix, "bootstrap/remote-state"), envars)
util.ExecuteCommand(exec.Command("terraform", "apply", "-auto-approve"), filepath.Join(pathPrefix, "bootstrap/remote-state"), envars)
// log.Println(aurora.Cyan(emoji.Sprintf(":alarm_clock: Initializing remote backend...")))
// util.ExecuteCommand(exec.Command("terraform", "init"), filepath.Join(pathPrefix, "bootstrap/remote-state"), envars)
// util.ExecuteCommand(exec.Command("terraform", "apply", "-auto-approve"), filepath.Join(pathPrefix, "bootstrap/remote-state"), envars)

log.Println("Creating users...")
util.ExecuteCommand(exec.Command("terraform", "init"), filepath.Join(pathPrefix, "bootstrap/create-users"), envars)
util.ExecuteCommand(exec.Command("terraform", "apply", "-auto-approve"), filepath.Join(pathPrefix, "bootstrap/create-users"), envars)

}
}

Expand Down
4 changes: 3 additions & 1 deletion internal/util/projectAttributes.go
@@ -1,5 +1,7 @@
package util

// @TODO : Move this stuff from util into another package

const (
Go = "go"
React = "react"
Expand Down Expand Up @@ -43,7 +45,7 @@ type ProjectConfiguration struct {
}

type Infrastructure struct {
AWS AWS
AWS *AWS
}
type AWS struct {
AccountID string
Expand Down
99 changes: 77 additions & 22 deletions internal/util/secrets/secrets.go
Expand Up @@ -18,7 +18,9 @@ import (

// Secrets - AWS prompted credentials
type Secrets struct {
AWS AWS
AWS AWS
CircleCIKey string
GithubToken string
}

type AWS struct {
Expand Down Expand Up @@ -47,21 +49,13 @@ func GetSecrets(baseDir string) Secrets {
if err != nil {
log.Fatal(err)
}
credsFile := filepath.Join(usr.HomeDir, ".aws/credentials")

var awsSecrets Secrets
var secrets Secrets

// Load the credentials file to look for profiles
credsFile := filepath.Join(usr.HomeDir, ".aws/credentials")
creds, err := ioutil.ReadFile(credsFile)
profiles, err := GetAWSProfiles()
if err == nil {
// Get all profiles
re := regexp.MustCompile(`\[(.*)\]`)
profileMatches := re.FindAllStringSubmatch(string(creds), -1)
profiles := make([]string, len(profileMatches))
for i, p := range profileMatches {
profiles[i] = p[1]
}

profilePrompt := promptui.Select{
Label: "Select AWS Profile",
Items: profiles,
Expand All @@ -71,7 +65,7 @@ func GetSecrets(baseDir string) Secrets {

creds, err := credentials.NewSharedCredentials(credsFile, profileResult).Get()
if err == nil {
awsSecrets = Secrets{
secrets = Secrets{
AWS: AWS{
AccessKeyID: creds.AccessKeyID,
SecretAccessKey: creds.SecretAccessKey,
Expand All @@ -81,13 +75,51 @@ func GetSecrets(baseDir string) Secrets {
}

// We couldn't load the credentials file, get the user to just paste them
if awsSecrets == (Secrets{}) {
awsSecrets = promptCredentials()
if secrets.AWS == (AWS{}) {
promptAWSCredentials(&secrets)
}

if secrets.CircleCIKey == "" || secrets.GithubToken == "" {
ciPrompt := promptui.Select{
Label: "Which Continuous integration provider do you want to use?",
Items: []string{"CircleCI", "GitHub Actions"},
}

_, ciResult, _ := ciPrompt.Run()

if ciResult == "CircleCI" {
promptCircleCICredentials(&secrets)
} else if ciResult == "GitHub Actions" {
promptGitHubCredentials(&secrets)
}
}

writeSecrets(secretsFile, awsSecrets)
return awsSecrets
writeSecrets(secretsFile, secrets)
return secrets
}
}

// GetAWSProfiles returns a list of AWS forprofiles set up on the user's sytem
func GetAWSProfiles() ([]string, error) {
usr, err := user.Current()
if err != nil {
return nil, err
}

// Load the credentials file to look for profiles
credsFile := filepath.Join(usr.HomeDir, ".aws/credentials")
creds, err := ioutil.ReadFile(credsFile)
if err != nil {
return nil, err
}
// Get all profiles
re := regexp.MustCompile(`\[(.*)\]`)
profileMatches := re.FindAllStringSubmatch(string(creds), -1)
profiles := make([]string, len(profileMatches))
for i, p := range profileMatches {
profiles[i] = p[1]
}
return profiles, nil
}

func readSecrets(secretsFile string) Secrets {
Expand Down Expand Up @@ -122,7 +154,7 @@ func writeSecrets(secretsFile string, s Secrets) {
}
}

func promptCredentials() Secrets {
func promptAWSCredentials(secrets *Secrets) {

validateAKID := func(input string) error {
// 20 uppercase alphanumeric characters
Expand Down Expand Up @@ -167,12 +199,35 @@ func promptCredentials() Secrets {
panic(err)
}

awsSecrets := Secrets{}
awsSecrets.AWS.AccessKeyID = accessKeyIDResult
awsSecrets.AWS.SecretAccessKey = secretAccessKeyResult
secrets.AWS.AccessKeyID = accessKeyIDResult
secrets.AWS.SecretAccessKey = secretAccessKeyResult
}

return awsSecrets
func promptGitHubCredentials(secrets *Secrets) {
}

func promptCircleCICredentials(secrets *Secrets) {
validateKey := func(input string) error {
// 40 base64 characters
var awsSecretAccessKeyPat = regexp.MustCompile(`^[A-Za-z0-9]{40}$`)
if !awsSecretAccessKeyPat.MatchString(input) {
return errors.New("Invalid CircleCI API Key")
}
return nil
}

prompt := promptui.Prompt{
Label: "Please enter your CircleCI API key (you can create one at https://circleci.com/account/api) ",
Validate: validateKey,
}

key, err := prompt.Run()

if err != nil {
log.Fatalf("Prompt failed %v\n", err)
panic(err)
}
secrets.CircleCIKey = key
}

func fileExists(filename string) bool {
Expand Down
2 changes: 2 additions & 0 deletions templates/terraform/bootstrap/create-users/.gitignore
@@ -0,0 +1,2 @@
# The state will have keys in it which can be ignored
terraform.tfstate*
23 changes: 23 additions & 0 deletions templates/terraform/bootstrap/create-users/main.tf
@@ -0,0 +1,23 @@
provider "aws" {
region = "{{ .Config.Infrastructure.AWS.Region }}"
}

# Create the CI User
resource "aws_iam_user" "ci_user" {
name = "ci-user"
}

# Create a keypair to be used by CI systems
resource "aws_iam_access_key" "ci_user" {
user = aws_iam_user.ci_user.name
}

# Add the keys to AWS secrets manager
resource "aws_secretsmanager_secret" "ci_user_keys" {
name = "ci-user-keys"
}

resource "aws_secretsmanager_secret_version" "ci_user_keys" {
secret_id = aws_secretsmanager_secret.ci_user_keys.id
secret_string = jsonencode(map("access_key_id", aws_iam_access_key.ci_user.id, "secret_key", aws_iam_access_key.ci_user.secret))
}
10 changes: 7 additions & 3 deletions templates/terraform/environments/staging/main.tf
Expand Up @@ -19,9 +19,13 @@ module "staging" {
region = "{{ .Config.Infrastructure.AWS.Region }}"
allowed_account_ids = ["{{ .Config.Infrastructure.AWS.AccountId }}"]

{{- if ne .Config.Infrastructure.AWS.EKS.ClusterName "" }}
{{- if ne .Config.Infrastructure.AWS.EKS.ClusterName "" }}
# ECR configuration
ecr_repositories = ["{{ .Config.Infrastructure.AWS.EKS.ClusterName }}"]
ecr_repositories = [
{{- range .Config.Services }}
"{{ .Name }}",
{{- end }}
]

# EKS configuration
eks_worker_instance_type = "t2.small"
Expand All @@ -30,7 +34,7 @@ module "staging" {
# EKS-Optimized AMI for your region: https://docs.aws.amazon.com/eks/latest/userguide/eks-optimized-ami.html
# https://us-east-1.console.aws.amazon.com/systems-manager/parameters/%252Faws%252Fservice%252Feks%252Foptimized-ami%252F1.14%252Famazon-linux-2%252Frecommended%252Fimage_id/description?region=us-east-1
eks_worker_ami = "{{ .Config.Infrastructure.AWS.EKS.WorkerAMI }}"
{{- end }}
{{- end }}

# Client configuration
user_pool = "{{ .Config.Name }}-staging"
Expand Down

0 comments on commit d3ba846

Please sign in to comment.