-
Notifications
You must be signed in to change notification settings - Fork 194
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
OCM-9995 | feat: Adding rosa quickstart POC
- Loading branch information
den-rgb
committed
Sep 11, 2024
1 parent
90494f7
commit d07d9c2
Showing
3 changed files
with
404 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
Oops, something went wrong.