-
Notifications
You must be signed in to change notification settings - Fork 11
feat: add cluster deploy command for parallel IAM and VPC deployment #11
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
a4b8129
1148056
04cb4ad
50b4274
689e4d1
7b52ca6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| package cluster | ||
|
|
||
| import "github.com/spf13/cobra" | ||
|
|
||
| // NewClusterCommand creates the cluster command | ||
| func NewClusterCommand() *cobra.Command { | ||
| cmd := &cobra.Command{ | ||
| Use: "cluster", | ||
| Short: "Manage cluster resources", | ||
| Long: `Manage combined cluster resources for ROSA hosted clusters. | ||
|
|
||
| This command group provides operations that coordinate multiple resource types | ||
| (IAM and VPC) for a hosted cluster.`, | ||
| } | ||
|
|
||
| cmd.AddCommand(newDeployCommand()) | ||
|
|
||
| return cmd | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,132 @@ | ||
| package cluster | ||
|
|
||
| import ( | ||
| "context" | ||
| "fmt" | ||
| "strings" | ||
| "sync" | ||
|
|
||
| cmdiam "github.com/openshift-online/rosa-regional-platform-cli/internal/commands/clusteriam" | ||
| cmdvpc "github.com/openshift-online/rosa-regional-platform-cli/internal/commands/clustervpc" | ||
| "github.com/spf13/cobra" | ||
| ) | ||
|
|
||
| // deployOptions composes the individual command option types so that flag | ||
| // definitions are owned exactly once — in the clusteriam and clustervpc | ||
| // packages — and automatically inherited here. | ||
| type deployOptions struct { | ||
| clusterName string | ||
| region string | ||
| iam cmdiam.CreateOptions | ||
| vpc cmdvpc.CreateOptions | ||
| } | ||
|
|
||
| func newDeployCommand() *cobra.Command { | ||
| opts := &deployOptions{} | ||
|
|
||
| cmd := &cobra.Command{ | ||
| Use: "deploy CLUSTER_NAME", | ||
| Short: "Deploy cluster IAM and VPC resources in parallel", | ||
| Long: `Deploy both IAM and VPC resources for a ROSA hosted cluster simultaneously. | ||
|
|
||
| This command runs the cluster-iam and cluster-vpc create operations in parallel, | ||
| reducing total deployment time compared to running them sequentially. | ||
|
|
||
| Example: | ||
| rosactl cluster deploy my-cluster \ | ||
| --oidc-issuer-url https://d1234.cloudfront.net/my-cluster \ | ||
| --region us-east-1 \ | ||
| --availability-zones us-east-1a,us-east-1b,us-east-1c`, | ||
| Args: cobra.ExactArgs(1), | ||
| RunE: func(cmd *cobra.Command, args []string) error { | ||
| opts.clusterName = args[0] | ||
| opts.region, _ = cmd.Flags().GetString("region") | ||
| return runDeploy(cmd.Context(), opts) | ||
| }, | ||
| } | ||
|
|
||
| // Delegate flag registration to each sub-command package so this list | ||
| // never diverges from the individual create commands. | ||
| // --region is inherited from the root persistent flag; no need to add it here. | ||
| cmdiam.AddFlags(cmd, &opts.iam) | ||
| cmdvpc.AddFlags(cmd, &opts.vpc) | ||
|
|
||
| cmd.MarkFlagRequired("oidc-issuer-url") | ||
| cmd.MarkFlagRequired("availability-zones") | ||
|
|
||
| return cmd | ||
| } | ||
|
|
||
| type deployResult struct { | ||
| name string | ||
| err error | ||
| } | ||
|
|
||
| func runDeploy(ctx context.Context, opts *deployOptions) error { | ||
| if opts.region == "" { | ||
| return fmt.Errorf("--region is required") | ||
| } | ||
|
|
||
| // Wire shared fields into the sub-options before dispatch. | ||
| opts.iam.ClusterName = opts.clusterName | ||
| opts.iam.Region = opts.region | ||
| opts.vpc.ClusterName = opts.clusterName | ||
| opts.vpc.Region = opts.region | ||
|
|
||
| fmt.Printf("Deploying cluster resources for: %s\n", opts.clusterName) | ||
| fmt.Printf(" Region: %s\n", opts.region) | ||
| fmt.Printf(" OIDC Issuer: %s\n", opts.iam.OIDCIssuerURL) | ||
| fmt.Printf(" VPC CIDR: %s\n", opts.vpc.VpcCidr) | ||
| fmt.Printf(" Availability Zones: %s\n", opts.vpc.AvailabilityZones) | ||
| fmt.Println() | ||
| fmt.Println("Running IAM and VPC deployments in parallel...") | ||
| fmt.Println() | ||
|
|
||
| // Use a cancellable context so that if one deployment fails the other is | ||
| // aborted rather than running to completion unnecessarily. | ||
| ctx, cancel := context.WithCancel(ctx) | ||
| defer cancel() | ||
|
|
||
| results := make(chan deployResult, 2) | ||
| var wg sync.WaitGroup | ||
|
|
||
| wg.Add(1) | ||
| go func() { | ||
| defer wg.Done() | ||
| err := cmdiam.RunCreate(ctx, &opts.iam) | ||
| if err != nil { | ||
| cancel() | ||
| } | ||
| results <- deployResult{"IAM", err} | ||
| }() | ||
|
|
||
| wg.Add(1) | ||
| go func() { | ||
| defer wg.Done() | ||
| err := cmdvpc.RunCreate(ctx, &opts.vpc) | ||
| if err != nil { | ||
| cancel() | ||
| } | ||
| results <- deployResult{"VPC", err} | ||
| }() | ||
|
|
||
| go func() { | ||
| wg.Wait() | ||
| close(results) | ||
| }() | ||
|
|
||
| var errs []string | ||
| for result := range results { | ||
| if result.err != nil { | ||
| errs = append(errs, fmt.Sprintf("%s: %v", result.name, result.err)) | ||
| } | ||
| } | ||
|
|
||
| if len(errs) > 0 { | ||
| return fmt.Errorf("deployment failed:\n %s", strings.Join(errs, "\n ")) | ||
| } | ||
|
|
||
| fmt.Println() | ||
| fmt.Println("Cluster deployment complete. IAM and VPC resources are ready.") | ||
| return nil | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -10,18 +10,32 @@ import ( | |
| "github.com/spf13/cobra" | ||
| ) | ||
|
|
||
| type createOptions struct { | ||
| clusterName string | ||
| region string | ||
| vpcCidr string | ||
| publicSubnetCidrs string | ||
| privateSubnetCidrs string | ||
| availabilityZones string | ||
| singleNatGateway bool | ||
| // CreateOptions holds options for the cluster-vpc create command. | ||
| // It is exported so that composite commands (e.g. cluster deploy) can reuse | ||
| // the same flag definitions without duplicating them. | ||
| type CreateOptions struct { | ||
| ClusterName string | ||
| Region string | ||
| VpcCidr string | ||
| PublicSubnetCidrs string | ||
| PrivateSubnetCidrs string | ||
| AvailabilityZones string | ||
| SingleNatGateway bool | ||
| } | ||
|
|
||
| // AddFlags registers the VPC-specific flags on cmd bound to opts. | ||
| // --region is intentionally excluded: it is a persistent root-level flag | ||
| // inherited by all commands. | ||
| func AddFlags(cmd *cobra.Command, opts *CreateOptions) { | ||
| cmd.Flags().StringVar(&opts.VpcCidr, "vpc-cidr", "10.0.0.0/16", "CIDR block for the VPC") | ||
| cmd.Flags().StringVar(&opts.PublicSubnetCidrs, "public-subnet-cidrs", "10.0.101.0/24,10.0.102.0/24,10.0.103.0/24", "Comma-separated public subnet CIDRs") | ||
| cmd.Flags().StringVar(&opts.PrivateSubnetCidrs, "private-subnet-cidrs", "10.0.0.0/19,10.0.32.0/19,10.0.64.0/19", "Comma-separated private subnet CIDRs") | ||
| cmd.Flags().StringVar(&opts.AvailabilityZones, "availability-zones", "", "Comma-separated availability zones (optional, auto-detected if empty)") | ||
| cmd.Flags().BoolVar(&opts.SingleNatGateway, "single-nat-gateway", true, "Use single NAT gateway (true=cost savings, false=HA per-AZ)") | ||
| } | ||
|
|
||
| func newCreateCommand() *cobra.Command { | ||
| opts := &createOptions{} | ||
| opts := &CreateOptions{} | ||
|
|
||
| cmd := &cobra.Command{ | ||
| Use: "create CLUSTER_NAME", | ||
|
|
@@ -48,59 +62,59 @@ Example: | |
| --single-nat-gateway=false`, | ||
| Args: cobra.ExactArgs(1), | ||
| RunE: func(cmd *cobra.Command, args []string) error { | ||
| opts.clusterName = args[0] | ||
| return runCreate(cmd.Context(), opts) | ||
| opts.ClusterName = args[0] | ||
| opts.Region, _ = cmd.Flags().GetString("region") | ||
| return RunCreate(cmd.Context(), opts) | ||
| }, | ||
| } | ||
|
|
||
| cmd.Flags().StringVar(&opts.region, "region", "", "AWS region (required)") | ||
| cmd.Flags().StringVar(&opts.vpcCidr, "vpc-cidr", "10.0.0.0/16", "CIDR block for the VPC") | ||
| cmd.Flags().StringVar(&opts.publicSubnetCidrs, "public-subnet-cidrs", "10.0.101.0/24,10.0.102.0/24,10.0.103.0/24", "Comma-separated public subnet CIDRs") | ||
| cmd.Flags().StringVar(&opts.privateSubnetCidrs, "private-subnet-cidrs", "10.0.0.0/19,10.0.32.0/19,10.0.64.0/19", "Comma-separated private subnet CIDRs") | ||
| cmd.Flags().StringVar(&opts.availabilityZones, "availability-zones", "", "Comma-separated availability zones (optional, auto-detected if empty)") | ||
| cmd.Flags().BoolVar(&opts.singleNatGateway, "single-nat-gateway", true, "Use single NAT gateway (true=cost savings, false=HA per-AZ)") | ||
|
|
||
| cmd.MarkFlagRequired("region") | ||
| AddFlags(cmd, opts) | ||
|
|
||
| return cmd | ||
| } | ||
|
|
||
| func runCreate(ctx context.Context, opts *createOptions) error { | ||
| fmt.Printf("🌐 Creating cluster VPC resources for: %s\n", opts.clusterName) | ||
| fmt.Printf(" Region: %s\n", opts.region) | ||
| fmt.Printf(" VPC CIDR: %s\n", opts.vpcCidr) | ||
| fmt.Printf(" Single NAT Gateway: %t\n", opts.singleNatGateway) | ||
| // RunCreate executes the VPC creation workflow. It is exported so that | ||
| // composite commands can invoke it directly. | ||
| func RunCreate(ctx context.Context, opts *CreateOptions) error { | ||
| if opts.Region == "" { | ||
| return fmt.Errorf("--region is required") | ||
| } | ||
|
|
||
| fmt.Printf("🌐 Creating cluster VPC resources for: %s\n", opts.ClusterName) | ||
| fmt.Printf(" Region: %s\n", opts.Region) | ||
| fmt.Printf(" VPC CIDR: %s\n", opts.VpcCidr) | ||
| fmt.Printf(" Single NAT Gateway: %t\n", opts.SingleNatGateway) | ||
| fmt.Println() | ||
|
|
||
| // Load AWS config | ||
| cfg, err := config.LoadDefaultConfig(ctx, config.WithRegion(opts.region)) | ||
| cfg, err := config.LoadDefaultConfig(ctx, config.WithRegion(opts.Region)) | ||
| if err != nil { | ||
| return fmt.Errorf("failed to load AWS config: %w", err) | ||
| } | ||
|
Comment on lines
+78
to
93
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Validate
🤖 Prompt for AI Agents |
||
|
|
||
| // Parse CIDR lists | ||
| publicSubnets := strings.Split(opts.publicSubnetCidrs, ",") | ||
| privateSubnets := strings.Split(opts.privateSubnetCidrs, ",") | ||
| publicSubnets := strings.Split(opts.PublicSubnetCidrs, ",") | ||
| privateSubnets := strings.Split(opts.PrivateSubnetCidrs, ",") | ||
|
|
||
| // Parse availability zones if provided | ||
| var azs []string | ||
| if opts.availabilityZones != "" { | ||
| azs = strings.Split(opts.availabilityZones, ",") | ||
| if opts.AvailabilityZones != "" { | ||
| azs = strings.Split(opts.AvailabilityZones, ",") | ||
| } | ||
|
|
||
| // Create service request | ||
| req := &clustervpc.CreateVPCRequest{ | ||
| ClusterName: opts.clusterName, | ||
| VpcCidr: opts.vpcCidr, | ||
| ClusterName: opts.ClusterName, | ||
| VpcCidr: opts.VpcCidr, | ||
| PublicSubnetCidrs: publicSubnets, | ||
| PrivateSubnetCidrs: privateSubnets, | ||
| AvailabilityZones: azs, | ||
| SingleNatGateway: opts.singleNatGateway, | ||
| SingleNatGateway: opts.SingleNatGateway, | ||
| AWSConfig: cfg, | ||
| } | ||
|
|
||
| fmt.Println("📄 Loading CloudFormation template...") | ||
| fmt.Printf("☁️ Creating CloudFormation stack: rosa-%s-vpc\n", opts.clusterName) | ||
| fmt.Printf("☁️ Creating CloudFormation stack: rosa-%s-vpc\n", opts.ClusterName) | ||
| fmt.Println(" This may take several minutes...") | ||
| fmt.Println() | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Don't advertise
--availability-zonesas optional in the shared helper.AddFlagsnow feedscluster deploytoo, where--availability-zonesis required. Reusing the current description will makerosactl cluster deploy --helpcontradict the command's actual contract.🤖 Prompt for AI Agents