From 1d468753d88ea24ac7e1914f32796a5f4acc9113 Mon Sep 17 00:00:00 2001 From: Adam Date: Thu, 14 May 2026 16:10:05 -0500 Subject: [PATCH] feat: add IAM workflow validation and fix partial role ARN handling - Added validate() to SpinUpEKSIAMInput (requires ClusterName, Environment, Team) and SpinDownEKSIAMInput (requires both role names); both IAM workflows now return NonRetryableApplicationError on invalid input - SpinUpInput.validate() now rejects the case where exactly one of ClusterRoleARN/NodeRoleARN is set, preventing silent role recreation - Renamed SpinUpIAMInput/Output and SpinDownIAMInput to SpinUpEKSIAMInput/Output and SpinDownEKSIAMInput to reflect that these roles are EKS-specific Co-Authored-By: Claude Sonnet 4.6 --- internal/workflows/iam.go | 46 +++++++++++++++++++++++----- internal/workflows/infrastructure.go | 12 +++++--- 2 files changed, 46 insertions(+), 12 deletions(-) diff --git a/internal/workflows/iam.go b/internal/workflows/iam.go index eafa506..bb89ae3 100644 --- a/internal/workflows/iam.go +++ b/internal/workflows/iam.go @@ -4,6 +4,7 @@ import ( "fmt" "github.com/Amertz08/gitops-example/internal/activities" + "go.temporal.io/sdk/temporal" "go.temporal.io/sdk/workflow" ) @@ -11,29 +12,56 @@ const eksTrustPolicy = `{"Version":"2012-10-17","Statement":[{"Effect":"Allow"," const ec2TrustPolicy = `{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":{"Service":"ec2.amazonaws.com"},"Action":"sts:AssumeRole"}]}` -type SpinUpIAMInput struct { +type SpinUpEKSIAMInput struct { Region string ClusterName string Environment string Team string } -type SpinUpIAMOutput struct { +func (i SpinUpEKSIAMInput) validate() error { + switch { + case i.ClusterName == "": + return fmt.Errorf("ClusterName is required") + case i.Environment == "": + return fmt.Errorf("Environment is required") + case i.Team == "": + return fmt.Errorf("Team is required") + } + return nil +} + +type SpinUpEKSIAMOutput struct { ClusterRoleARN string ClusterRoleName string NodeRoleARN string NodeRoleName string } -type SpinDownIAMInput struct { +type SpinDownEKSIAMInput struct { ClusterRoleName string NodeRoleName string } +func (i SpinDownEKSIAMInput) validate() error { + switch { + case i.ClusterRoleName == "": + return fmt.Errorf("ClusterRoleName is required") + case i.NodeRoleName == "": + return fmt.Errorf("NodeRoleName is required") + } + return nil +} + func SpinUpIAMWorkflow( ctx workflow.Context, - input SpinUpIAMInput, -) (output SpinUpIAMOutput, err error) { + input SpinUpEKSIAMInput, +) (output SpinUpEKSIAMOutput, err error) { + if valErr := input.validate(); valErr != nil { + err = temporal.NewNonRetryableApplicationError(valErr.Error(), "InvalidInput", valErr) + return + } + ctx = workflow.WithActivityOptions(ctx, activityOptions) aws := &activities.AWSActivities{} logger := workflow.GetLogger(ctx) @@ -103,7 +131,7 @@ func SpinUpIAMWorkflow( } }) - output = SpinUpIAMOutput{ + output = SpinUpEKSIAMOutput{ ClusterRoleARN: clusterRoleARN, ClusterRoleName: clusterRoleName, NodeRoleARN: nodeRoleARN, @@ -112,7 +140,11 @@ func SpinUpIAMWorkflow( return } -func SpinDownIAMWorkflow(ctx workflow.Context, input SpinDownIAMInput) error { +func SpinDownIAMWorkflow(ctx workflow.Context, input SpinDownEKSIAMInput) error { + if err := input.validate(); err != nil { + return temporal.NewNonRetryableApplicationError(err.Error(), "InvalidInput", err) + } + ctx = workflow.WithActivityOptions(ctx, activityOptions) aws := &activities.AWSActivities{} logger := workflow.GetLogger(ctx) diff --git a/internal/workflows/infrastructure.go b/internal/workflows/infrastructure.go index 5d95901..883a1eb 100644 --- a/internal/workflows/infrastructure.go +++ b/internal/workflows/infrastructure.go @@ -37,6 +37,8 @@ func (i SpinUpInput) validate() error { return fmt.Errorf("Team is required") case i.NodeCount <= 0: return fmt.Errorf("NodeCount must be greater than 0") + case (i.ClusterRoleARN == "") != (i.NodeRoleARN == ""): + return fmt.Errorf("ClusterRoleARN and NodeRoleARN must both be provided or both be empty") } for idx, sc := range i.Subnets { if strings.TrimSpace(sc.CIDR) == "" { @@ -90,9 +92,9 @@ func SpinUpWorkflow(ctx workflow.Context, input SpinUpInput) (err error) { clusterRoleARN := input.ClusterRoleARN nodeRoleARN := input.NodeRoleARN - if clusterRoleARN == "" || nodeRoleARN == "" { - var iamOut SpinUpIAMOutput - if err = workflow.ExecuteChildWorkflow(ctx, SpinUpIAMWorkflow, SpinUpIAMInput{ + if clusterRoleARN == "" { + var iamOut SpinUpEKSIAMOutput + if err = workflow.ExecuteChildWorkflow(ctx, SpinUpIAMWorkflow, SpinUpEKSIAMInput{ Region: input.Region, ClusterName: input.ClusterName, Environment: input.Environment, @@ -111,7 +113,7 @@ func SpinUpWorkflow(ctx workflow.Context, input SpinUpInput) (err error) { if err := workflow.ExecuteChildWorkflow( cctx, SpinDownIAMWorkflow, - SpinDownIAMInput{ + SpinDownEKSIAMInput{ ClusterRoleName: iamOut.ClusterRoleName, NodeRoleName: iamOut.NodeRoleName, }, @@ -185,7 +187,7 @@ func SpinDownWorkflow(ctx workflow.Context, input SpinDownInput) error { logger.Info("network torn down", "vpcID", input.VpcID) if input.ClusterRoleName != "" || input.NodeRoleName != "" { - if err := workflow.ExecuteChildWorkflow(ctx, SpinDownIAMWorkflow, SpinDownIAMInput{ + if err := workflow.ExecuteChildWorkflow(ctx, SpinDownIAMWorkflow, SpinDownEKSIAMInput{ ClusterRoleName: input.ClusterRoleName, NodeRoleName: input.NodeRoleName, }).Get(ctx, nil); err != nil {