Skip to content

Commit

Permalink
OCM-9995 | feat: Adding rosa quickstart POC
Browse files Browse the repository at this point in the history
  • Loading branch information
den-rgb committed Sep 11, 2024
1 parent 90494f7 commit d07d9c2
Show file tree
Hide file tree
Showing 3 changed files with 404 additions and 0 deletions.
275 changes: 275 additions & 0 deletions cmd/bootstrap/cmd.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,275 @@
package bootstrap

import (
"context"
"fmt"
"os"
"strings"
"time"

"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/service/cloudformation"
cfTypes "github.com/aws/aws-sdk-go-v2/service/cloudformation/types"
common "github.com/openshift-online/ocm-common/pkg/aws/validations"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"

"github.com/openshift/rosa/pkg/aws/tags"
"github.com/openshift/rosa/pkg/ocm"
"github.com/openshift/rosa/pkg/rosa"
)

func NewRosaBootstrapCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "bootstrap",
Short: "Bootstrap quick start vpc",
Long: "Bootstrap quick start vpc. This vpc can be used to create a HCP cluster",
Example: ` # Create a rosa-quickstart-vpc
rosa bootstrap rosa-quickstart-default-vpc --param Name=rosa-quickstart-default --param VpcCidr=10.0.0.0/16 ` +
`--param Region=us-west-2 --param AvailabilityZonesCount=2 --param Tags=Key1=Value1`,
Args: cobra.ExactArgs(1),
Hidden: true,
Run: run,
}
cmd.Flags().StringArrayVar(&args.params, "param", []string{}, "List of parameters")
return cmd
}

var args struct {
params []string
}

func run(cmd *cobra.Command, arg []string) {
r := rosa.NewRuntime().WithAWS().WithOCM()
defer r.Cleanup()

orgID, _, err := r.OCMClient.GetCurrentOrganization()
if err != nil {
r.Reporter.Errorf(err.Error())
os.Exit(1)
}

parsedParams, parsedTags := parseParams(args.params)

// Extract the first non-`--param` argument to use as the template command
var templateCommand string
for _, arg := range arg {
if !strings.HasPrefix(arg, "--param") {
templateCommand = arg
break
}
}

templateFile := selectTemplate(templateCommand)
if templateFile == "" {
r.Reporter.Errorf("No suitable template found for the specified parameters")
os.Exit(1)
}

r.OCMClient.LogEvent("RosaQuickStartVpc",
map[string]string{
ocm.Account: r.Creator.AccountID,
ocm.Organization: orgID,
"template": templateFile,
},
)

err = createVPCStack(templateFile, parsedParams, parsedTags)
if err != nil {
r.Reporter.Errorf(err.Error())
os.Exit(1)
}

r.Reporter.Infof("VPC created")
}

// parseParams converts the list of parameter strings into a map and sets default values
func parseParams(params []string) (map[string]string, map[string]string) {
result := map[string]string{
"Name": "rosa-quickstart-default",
"VpcCidr": "10.0.0.0/16",
"Region": "us-west-2",
"AvailabilityZonesCount": "1",
}

// Set default tags from the getTags function
defaultTags := getTags()
userTags := map[string]string{}

for _, param := range params {
parts := strings.SplitN(param, "=", 2)
if len(parts) == 2 {
if parts[0] == "Tags" {
tagEntries := strings.Split(parts[1], ",")
for _, entry := range tagEntries {
tagParts := strings.SplitN(entry, "=", 2)
if len(tagParts) == 2 {
userTags[tagParts[0]] = tagParts[1]
}
}
} else {
result[parts[0]] = parts[1]
}
}
}

// Combine default tags and user tags
combinedTags := mergeTags(defaultTags, userTags)

return result, combinedTags
}

// mergeTags combines default tags and user tags, giving precedence to user tags in case of conflicts
func mergeTags(defaultTags, userTags map[string]string) map[string]string {
combinedTags := make(map[string]string)
for k, v := range defaultTags {
combinedTags[k] = v
}
for k, v := range userTags {
combinedTags[k] = v
}
return combinedTags
}

// selectTemplate selects the appropriate template file based on the parameters
func selectTemplate(command string) string {
return fmt.Sprintf("cmd/bootstrap/templates/%s/cloudformation.yaml", command)
}

// createVPCStack creates a CloudFormation stack to deploy a VPC
func createVPCStack(templateFile string, params map[string]string, tags map[string]string) error {
// Load the AWS configuration
logger := logrus.New()
logger.SetLevel(logrus.DebugLevel)
cfg, err := config.LoadDefaultConfig(context.TODO(), config.WithRegion(params["Region"]))
if err != nil {
return fmt.Errorf("unable to load SDK config, %v", err)
}

// Read the CloudFormation template
templateBody, err := os.ReadFile(templateFile)
if err != nil {
return fmt.Errorf("unable to read template file, %v", err)
}

var cfTags []cfTypes.Tag
for k, v := range tags {
cfTags = append(cfTags, cfTypes.Tag{
Key: aws.String(k),
Value: aws.String(v),
})
}

// Create a CloudFormation client
logger.Info("Creating CloudFormation client")
cfClient := cloudformation.NewFromConfig(cfg)

// Create a slice for CloudFormation parameters
var cfParams []cfTypes.Parameter
for k, v := range params {
cfParams = append(cfParams, cfTypes.Parameter{
ParameterKey: aws.String(k),
ParameterValue: aws.String(v),
})
}

// Create the stack
logger.Info("Creating CloudFormation stack")
_, err = cfClient.CreateStack(context.TODO(), &cloudformation.CreateStackInput{
StackName: aws.String(params["Name"]),
TemplateBody: aws.String(string(templateBody)),
Parameters: cfParams,
Tags: cfTags,
Capabilities: []cfTypes.Capability{
cfTypes.CapabilityCapabilityIam,
cfTypes.CapabilityCapabilityNamedIam,
},
})
if err != nil {
logger.Errorf("Failed to create CloudFormation stack: %v", err)
logger.Infof("To view all created resource stacks, run `aws cloudformation list-stacks " +
"--stack-status-filter CREATE_COMPLETE`")
logger.Infof("To delete all created resource stacks, run `aws cloudformation delete-stack --stack-name %s`",
params["Name"])
return fmt.Errorf("failed to create stack, %v", err)
}

// Fetch and log stack events periodically
go func() {
ticker := time.NewTicker(10 * time.Second)
defer ticker.Stop()
for {
<-ticker.C
logStackEvents(cfClient, params["Name"], logger)
}
}()

// Wait until the stack is created
waiter := cloudformation.NewStackCreateCompleteWaiter(cfClient)
err = waiter.Wait(context.TODO(), &cloudformation.DescribeStacksInput{
StackName: aws.String(params["Name"]),
}, 10*time.Minute, func(o *cloudformation.StackCreateCompleteWaiterOptions) {
o.MinDelay = 30 * time.Second
o.MaxDelay = 60 * time.Second
})
if err != nil {
return fmt.Errorf("failed to wait for stack creation, %v", err)
}

return nil
}

const (
ColorReset = "\033[0m"
ColorRed = "\033[31m"
ColorGreen = "\033[32m"
ColorYellow = "\033[33m"
)

// logStackEvents fetches and logs stack events
func logStackEvents(cfClient *cloudformation.Client, stackName string, logger *logrus.Logger) {
events, err := cfClient.DescribeStackEvents(context.TODO(), &cloudformation.DescribeStackEventsInput{
StackName: aws.String(stackName),
})
if err != nil {
logger.Errorf("Failed to describe stack events: %v", err)
return
}

// Group events by resource and keep the latest event for each resource
latestEvents := make(map[string]cfTypes.StackEvent)
for _, event := range events.StackEvents {
resource := aws.ToString(event.LogicalResourceId)
if existingEvent, exists := latestEvents[resource]; !exists || event.Timestamp.After(*existingEvent.Timestamp) {
latestEvents[resource] = event
}
}

logger.Info("---------------------------------------------")
// Log the latest event for each resource with color
for resource, event := range latestEvents {
statusColor := getStatusColor(event.ResourceStatus)
logger.Infof("Resource: %s, Status: %s%s%s, Reason: %s", resource, statusColor, event.ResourceStatus, ColorReset,
aws.ToString(event.ResourceStatusReason))
}
}

func getStatusColor(status cfTypes.ResourceStatus) string {
switch status {
case cfTypes.ResourceStatusCreateComplete, cfTypes.ResourceStatusUpdateComplete:
return ColorGreen
case cfTypes.ResourceStatusCreateFailed, cfTypes.ResourceStatusDeleteFailed, cfTypes.ResourceStatusUpdateFailed:
return ColorRed
default:
return ColorYellow
}
}

func getTags() map[string]string {
tagsList := make(map[string]string)
tagsList[common.ManagedPolicies] = tags.True
tagsList[tags.HypershiftPolicies] = tags.True

return tagsList
}
Loading

0 comments on commit d07d9c2

Please sign in to comment.