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 Aug 27, 2024
1 parent b037752 commit 3cdef30
Show file tree
Hide file tree
Showing 9 changed files with 561 additions and 0 deletions.
18 changes: 18 additions & 0 deletions cmd/bootstrap/cmd.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package bootstrap

import (
"github.com/spf13/cobra"
quickstartcluster "github.com/openshift/rosa/cmd/bootstrap/quick-start-cluster"
)

func NewRosaBootstrapCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "bootstrap",
Short: "bootstrap AWS resource",
Long: "bootstrap AWS resource",
Args: cobra.NoArgs,
Hidden: true,
}
cmd.AddCommand(quickstartcluster.Cmd)
return cmd
}
217 changes: 217 additions & 0 deletions cmd/bootstrap/quick-start-cluster/cmd.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
package quickstartcluster

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

"github.com/openshift/rosa/pkg/rosa"
"github.com/spf13/cobra"
"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"
"github.com/openshift/rosa/pkg/output"
"github.com/sirupsen/logrus"
)

var Cmd = makeCmd()

func makeCmd() *cobra.Command {
return &cobra.Command{
Use: "quick-start-cluster",
Short: "Bootstrap quick start cluster",
Long: "Bootstrap quick start hcp cluster.",
Example: ` # Create a quick-start-cluster
rosa bootstrap quick-start-cluster`,
Run: run,
Args: cobra.NoArgs,
}
}

var args struct {
namePrefix string
vpcCidr string
availabilityZonesCount int
tags map[string]string
}

func init() {
flags := Cmd.Flags()
flags.StringVar(
&args.namePrefix,
"name",
"dmoskale-example",
"Prefix for naming resources",
)
flags.StringVar(
&args.vpcCidr,
"vpc-cidr",
"10.0.0.0/16",
"CIDR block for the VPC",
)
flags.IntVar(
&args.availabilityZonesCount,
"availability-zones-count",
2,
"Number of availability zones",
)
flags.StringToStringVar(
&args.tags,
"tags",
nil,
"Additional tags",
)
output.AddFlag(Cmd)
}


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

err := createVPCStack("cmd/bootstrap/quick-start-cluster/vpc.yaml", args.namePrefix, args.vpcCidr, args.availabilityZonesCount, args.tags)
if err != nil {
r.Reporter.Errorf(err.Error())
os.Exit(1)
}

r.Reporter.Infof("VPC created")
r.Reporter.Infof("To view all created resource stacks, run `aws cloudformation list-stacks --stack-status-filter CREATE_COMPLETE`")
r.Reporter.Infof("To delete all created resource stacks, run `aws cloudformation delete-stack --stack-name %s`", args.namePrefix)
}

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

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

// Convert tags map to comma-delimited string
var tagsList []string
for key, value := range tags {
tagsList = append(tagsList, fmt.Sprintf("%s=%s", key, value))
}
tagsString := strings.Join(tagsList, ",")
logger.Infof("Tags: %s", tagsString)

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

// Create the stack
logger.Info("Creating CloudFormation stack")
_, err = cfClient.CreateStack(context.TODO(), &cloudformation.CreateStackInput{
StackName: aws.String(namePrefix),
TemplateBody: aws.String(string(templateBody)),
Parameters: []cfTypes.Parameter{
{
ParameterKey: aws.String("NamePrefix"),
ParameterValue: aws.String(namePrefix),
},
{
ParameterKey: aws.String("VpcCidr"),
ParameterValue: aws.String(vpcCidr),
},
{
ParameterKey: aws.String("AvailabilityZonesCount"),
ParameterValue: aws.String(fmt.Sprintf("%d", availabilityZonesCount)),
},
{
ParameterKey: aws.String("Tags"),
ParameterValue: aws.String(fmt.Sprintf("%v", tagsString)),
},
},
Capabilities: []cfTypes.Capability{
cfTypes.CapabilityCapabilityIam,
cfTypes.CapabilityCapabilityNamedIam,
},
})
if err != nil {
logger.Errorf("Failed to create CloudFormation stack: %v", err)
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, namePrefix, logger)
}
}()

// Wait until the stack is created
waiter := cloudformation.NewStackCreateCompleteWaiter(cfClient)
err = waiter.Wait(context.TODO(), &cloudformation.DescribeStacksInput{
StackName: aws.String(namePrefix),
}, 10*time.Minute, func(o *cloudformation.StackCreateCompleteWaiterOptions) {
o.MinDelay = 30 * time.Second
o.MaxDelay = 1 * time.Minute
})
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
}
}
58 changes: 58 additions & 0 deletions cmd/bootstrap/quick-start-cluster/elastic_ips.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
AWSTemplateFormatVersion: '2010-09-09'
Description: Elastic IPs

Parameters:
NamePrefix:
Type: String
Description: Prefix for naming resources
AvailabilityZonesCount:
Type: Number
Description: Number of availability zones
Tags:
Type: CommaDelimitedList
Description: Tags for resources

Conditions:
HasTags: !Not [!Equals [!Join [",", !Ref Tags], ""]]

Resources:
ElasticIP1:
Type: AWS::EC2::EIP
Properties:
Domain: vpc
Tags:
- Key: Name
Value: !Sub "${NamePrefix}-eip-1"
- !If
- HasTags
- Key: !Select [0, !Ref Tags]
Value: !Select [1, !Ref Tags]
- Key: Empty
Value: Empty

ElasticIP2:
Type: AWS::EC2::EIP
Properties:
Domain: vpc
Tags:
- Key: Name
Value: !Sub "${NamePrefix}-eip-2"
- !If
- HasTags
- Key: !Select [0, !Ref Tags]
Value: !Select [1, !Ref Tags]
- Key: Empty
Value: Empty

Outputs:
EIP1AllocationId:
Description: Allocation ID for ElasticIP1
Value: !GetAtt ElasticIP1.AllocationId
Export:
Name: !Sub "${NamePrefix}-EIP1-AllocationId"

EIP2AllocationId:
Description: Allocation ID for ElasticIP2
Value: !GetAtt ElasticIP2.AllocationId
Export:
Name: !Sub "${NamePrefix}-EIP2-AllocationId"
47 changes: 47 additions & 0 deletions cmd/bootstrap/quick-start-cluster/nat_gateways.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
AWSTemplateFormatVersion: '2010-09-09'
Description: NAT Gateways

Parameters:
VpcId:
Type: String
Description: VPC ID
PublicSubnets:
Type: CommaDelimitedList
Description: Public Subnet IDs
ElasticIPs:
Type: CommaDelimitedList
Description: Comma-delimited list of Elastic IP allocation IDs
NamePrefix:
Type: String
Description: Prefix for naming resources
AvailabilityZonesCount:
Type: Number
Description: Number of availability zones
Tags:
Type: CommaDelimitedList
Description: Tags for resources

Conditions:
HasTags: !Not [!Equals [!Join [",", !Ref Tags], ""]]

Resources:
NatGateway:
Type: 'AWS::EC2::NatGateway'
Properties:
AllocationId: !Select [0, !Ref ElasticIPs]
SubnetId: !Select [0, !Ref PublicSubnets]
Tags:
- Key: Name
Value: !Sub "${NamePrefix}-nat-1"
- !If
- HasTags
- Key: !Select [0, !Ref Tags]
Value: !Select [1, !Ref Tags]
- Key: Empty
Value: Empty

Outputs:
NatGatewayIds:
Description: The NAT Gateway IDs
Value: !Ref NatGateway

39 changes: 39 additions & 0 deletions cmd/bootstrap/quick-start-cluster/private_route_tables.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
AWSTemplateFormatVersion: '2010-09-09'
Description: Private Route Tables

Parameters:
VpcId:
Type: String
Description: VPC ID
NamePrefix:
Type: String
Description: Prefix for naming resources
AvailabilityZonesCount:
Type: Number
Description: Number of availability zones
Tags:
Type: CommaDelimitedList
Description: Tags for resources

Conditions:
HasTags: !Not [!Equals [!Join [",", !Ref Tags], ""]]

Resources:
PrivateRouteTable:
Type: 'AWS::EC2::RouteTable'
Properties:
VpcId: !Ref VpcId
Tags:
- Key: Name
Value: !Sub "${NamePrefix}-private-rtb-1"
- !If
- HasTags
- Key: !Select [0, !Ref Tags]
Value: !Select [1, !Ref Tags]
- Key: Empty
Value: Empty

Outputs:
RouteTableIds:
Description: The Private Route Table IDs
Value: !Ref PrivateRouteTable
Loading

0 comments on commit 3cdef30

Please sign in to comment.