From 079b1bb1bbc23c57ae5705c9f9ddaebd8256a5d9 Mon Sep 17 00:00:00 2001 From: Aditya Choudhari Date: Sat, 17 May 2025 12:24:53 -0700 Subject: [PATCH 1/3] feat: ctrlc apply policy --- cmd/ctrlc/root/apply/cmd.go | 1 + cmd/ctrlc/root/apply/policy.go | 152 +++++++++++++++++++++++++++++++++ cmd/ctrlc/root/apply/types.go | 49 +++++++++++ internal/api/client.gen.go | 8 +- 4 files changed, 205 insertions(+), 5 deletions(-) create mode 100644 cmd/ctrlc/root/apply/policy.go diff --git a/cmd/ctrlc/root/apply/cmd.go b/cmd/ctrlc/root/apply/cmd.go index 4c70f84..543047b 100644 --- a/cmd/ctrlc/root/apply/cmd.go +++ b/cmd/ctrlc/root/apply/cmd.go @@ -45,6 +45,7 @@ func runApply(filePath string) error { processAllSystems(ctx, client, workspaceID, config.Systems) processResourceProvider(ctx, client, workspaceID.String(), config.Providers) processResourceRelationships(ctx, client, workspaceID.String(), config.Relationships) + processAllPolicies(ctx, client, config.Policies) return nil } diff --git a/cmd/ctrlc/root/apply/policy.go b/cmd/ctrlc/root/apply/policy.go new file mode 100644 index 0000000..869acae --- /dev/null +++ b/cmd/ctrlc/root/apply/policy.go @@ -0,0 +1,152 @@ +package apply + +import ( + "context" + "fmt" + "sync" + "time" + + "github.com/charmbracelet/log" + "github.com/ctrlplanedev/cli/internal/api" + "github.com/spf13/viper" +) + +func processAllPolicies( + ctx context.Context, + client *api.ClientWithResponses, + policies []Policy, +) { + if len(policies) == 0 { + return + } + + var wg sync.WaitGroup + for _, policy := range policies { + wg.Add(1) + go processPolicy(ctx, client, policy, &wg) + } + wg.Wait() +} + +func processPolicy( + ctx context.Context, + client *api.ClientWithResponses, + policy Policy, + policyWg *sync.WaitGroup, +) { + defer policyWg.Done() + + body := createPolicyRequestBody(policy) + if _, err := upsertPolicy(ctx, client, body); err != nil { + log.Error("Failed to create policy", "name", policy.Name, "error", err) + return + } +} + +func createPolicyRequestBody(policy Policy) api.UpsertPolicyJSONRequestBody { + workspace := viper.GetString("workspace") + // Convert targets + targets := make([]api.PolicyTarget, len(policy.Targets)) + for i, target := range policy.Targets { + targets[i] = api.PolicyTarget{ + DeploymentSelector: target.DeploymentSelector, + EnvironmentSelector: target.EnvironmentSelector, + ResourceSelector: target.ResourceSelector, + } + } + + // Convert deny windows + denyWindows := make([]struct { + Dtend *time.Time `json:"dtend,omitempty"` + Rrule *map[string]interface{} `json:"rrule,omitempty"` + TimeZone string `json:"timeZone"` + }, len(policy.DenyWindows)) + for i, window := range policy.DenyWindows { + rrule := window.RRule + denyWindows[i] = struct { + Dtend *time.Time `json:"dtend,omitempty"` + Rrule *map[string]interface{} `json:"rrule,omitempty"` + TimeZone string `json:"timeZone"` + }{ + Dtend: window.DtEnd, + Rrule: &rrule, + TimeZone: window.TimeZone, + } + } + + // Convert version any approval + var versionAnyApprovals api.VersionAnyApproval + if policy.VersionAnyApprovals != nil { + versionAnyApprovals = api.VersionAnyApproval{ + RequiredApprovalsCount: policy.VersionAnyApprovals.RequiredApprovalsCount, + } + } + + versionUserApprovals := make([]api.VersionUserApproval, len(policy.VersionUserApprovals)) + for i, approval := range policy.VersionUserApprovals { + versionUserApprovals[i] = api.VersionUserApproval{ + UserId: approval.UserId, + } + } + + versionRoleApprovals := make([]struct { + RequiredApprovalsCount *float32 `json:"requiredApprovalsCount,omitempty"` + RoleId string `json:"roleId"` + }, len(policy.VersionRoleApprovals)) + for i, approval := range policy.VersionRoleApprovals { + count := approval.RequiredApprovalsCount + versionRoleApprovals[i] = struct { + RequiredApprovalsCount *float32 `json:"requiredApprovalsCount,omitempty"` + RoleId string `json:"roleId"` + }{ + RequiredApprovalsCount: &count, + RoleId: approval.RoleId, + } + } + + // Create deployment version selector if present + var deploymentVersionSelector *api.DeploymentVersionSelector + if policy.DeploymentVersionSelector != nil { + deploymentVersionSelector = &api.DeploymentVersionSelector{ + Name: policy.DeploymentVersionSelector.Name, + DeploymentVersionSelector: policy.DeploymentVersionSelector.DeploymentVersionSelector, + Description: policy.DeploymentVersionSelector.Description, + } + } + + return api.UpsertPolicyJSONRequestBody{ + Name: policy.Name, + Description: policy.Description, + Priority: policy.Priority, + Enabled: policy.Enabled, + WorkspaceId: workspace, + Targets: targets, + DenyWindows: &denyWindows, + DeploymentVersionSelector: deploymentVersionSelector, + VersionAnyApprovals: &versionAnyApprovals, + VersionUserApprovals: &versionUserApprovals, + VersionRoleApprovals: &versionRoleApprovals, + } +} + +func upsertPolicy( + ctx context.Context, + client *api.ClientWithResponses, + policy api.UpsertPolicyJSONRequestBody, +) (string, error) { + resp, err := client.UpsertPolicyWithResponse(ctx, policy) + + if err != nil { + return "", fmt.Errorf("API request failed: %w", err) + } + + if resp.StatusCode() >= 400 { + return "", fmt.Errorf("API returned error status: %d", resp.StatusCode()) + } + + if resp.JSON200 != nil { + return resp.JSON200.Id.String(), nil + } + + return "", fmt.Errorf("unexpected response format") +} diff --git a/cmd/ctrlc/root/apply/types.go b/cmd/ctrlc/root/apply/types.go index ec1d3ea..2314f8a 100644 --- a/cmd/ctrlc/root/apply/types.go +++ b/cmd/ctrlc/root/apply/types.go @@ -1,10 +1,13 @@ package apply +import "time" + // Config represents the structure of the YAML file type Config struct { Systems []System `yaml:"systems"` Providers ResourceProvider `yaml:"resourceProvider"` Relationships []ResourceRelationship `yaml:"relationshipRules"` + Policies []Policy `yaml:"policies,omitempty"` } type System struct { @@ -99,3 +102,49 @@ type ResourceRelationship struct { MetadataKeysMatch []string `yaml:"metadataKeysMatch"` DependencyType string `yaml:"dependencyType"` } + +// Policy structs +type Policy struct { + Name string `yaml:"name"` + Description *string `yaml:"description,omitempty"` + Priority *float32 `yaml:"priority,omitempty"` + Enabled *bool `yaml:"enabled,omitempty"` + WorkspaceId string `yaml:"workspaceId"` + Targets []PolicyTarget `yaml:"targets"` + DenyWindows []DenyWindow `yaml:"denyWindows,omitempty"` + DeploymentVersionSelector *DeploymentVersionSelector `yaml:"deploymentVersionSelector,omitempty"` + VersionAnyApprovals *VersionAnyApproval `yaml:"versionAnyApprovals,omitempty"` + VersionUserApprovals []VersionUserApproval `yaml:"versionUserApprovals,omitempty"` + VersionRoleApprovals []VersionRoleApproval `yaml:"versionRoleApprovals,omitempty"` +} + +type PolicyTarget struct { + DeploymentSelector *map[string]any `yaml:"deploymentSelector,omitempty"` + EnvironmentSelector *map[string]any `yaml:"environmentSelector,omitempty"` + ResourceSelector *map[string]any `yaml:"resourceSelector,omitempty"` +} + +type DenyWindow struct { + TimeZone string `yaml:"timeZone"` + RRule map[string]any `yaml:"rrule"` + DtEnd *time.Time `yaml:"dtend,omitempty"` +} + +type DeploymentVersionSelector struct { + Name string `yaml:"name"` + DeploymentVersionSelector map[string]any `yaml:"deploymentVersionSelector"` + Description *string `yaml:"description,omitempty"` +} + +type VersionAnyApproval struct { + RequiredApprovalsCount float32 `yaml:"requiredApprovalsCount"` +} + +type VersionUserApproval struct { + UserId string `yaml:"userId"` +} + +type VersionRoleApproval struct { + RoleId string `yaml:"roleId"` + RequiredApprovalsCount float32 `yaml:"requiredApprovalsCount"` +} diff --git a/internal/api/client.gen.go b/internal/api/client.gen.go index f6ace76..41db7fd 100644 --- a/internal/api/client.gen.go +++ b/internal/api/client.gen.go @@ -480,7 +480,7 @@ type Policy1 struct { Name string `json:"name"` Priority float32 `json:"priority"` Targets []PolicyTarget `json:"targets"` - VersionAnyApprovals *[]VersionAnyApproval `json:"versionAnyApprovals,omitempty"` + VersionAnyApprovals *VersionAnyApproval `json:"versionAnyApprovals,omitempty"` VersionRoleApprovals []VersionRoleApproval `json:"versionRoleApprovals"` VersionUserApprovals []VersionUserApproval `json:"versionUserApprovals"` WorkspaceId openapi_types.UUID `json:"workspaceId"` @@ -873,10 +873,8 @@ type UpsertPolicyJSONBody struct { Name string `json:"name"` Priority *float32 `json:"priority,omitempty"` Targets []PolicyTarget `json:"targets"` - VersionAnyApprovals *[]struct { - RequiredApprovalsCount *float32 `json:"requiredApprovalsCount,omitempty"` - } `json:"versionAnyApprovals,omitempty"` - VersionRoleApprovals *[]struct { + VersionAnyApprovals *VersionAnyApproval `json:"versionAnyApprovals,omitempty"` + VersionRoleApprovals *[]struct { RequiredApprovalsCount *float32 `json:"requiredApprovalsCount,omitempty"` RoleId string `json:"roleId"` } `json:"versionRoleApprovals,omitempty"` From 94361f41e3e934bc46222defedc22cea7af1c053 Mon Sep 17 00:00:00 2001 From: Aditya Choudhari Date: Sat, 17 May 2025 12:42:39 -0700 Subject: [PATCH 2/3] rabbit comments --- cmd/ctrlc/root/apply/cmd.go | 2 +- cmd/ctrlc/root/apply/policy.go | 21 +++++++++++++-------- cmd/ctrlc/root/apply/types.go | 4 ++-- 3 files changed, 16 insertions(+), 11 deletions(-) diff --git a/cmd/ctrlc/root/apply/cmd.go b/cmd/ctrlc/root/apply/cmd.go index 543047b..eddc469 100644 --- a/cmd/ctrlc/root/apply/cmd.go +++ b/cmd/ctrlc/root/apply/cmd.go @@ -45,7 +45,7 @@ func runApply(filePath string) error { processAllSystems(ctx, client, workspaceID, config.Systems) processResourceProvider(ctx, client, workspaceID.String(), config.Providers) processResourceRelationships(ctx, client, workspaceID.String(), config.Relationships) - processAllPolicies(ctx, client, config.Policies) + processAllPolicies(ctx, client, workspaceID.String(), config.Policies) return nil } diff --git a/cmd/ctrlc/root/apply/policy.go b/cmd/ctrlc/root/apply/policy.go index 869acae..94e7904 100644 --- a/cmd/ctrlc/root/apply/policy.go +++ b/cmd/ctrlc/root/apply/policy.go @@ -8,12 +8,12 @@ import ( "github.com/charmbracelet/log" "github.com/ctrlplanedev/cli/internal/api" - "github.com/spf13/viper" ) func processAllPolicies( ctx context.Context, client *api.ClientWithResponses, + workspaceID string, policies []Policy, ) { if len(policies) == 0 { @@ -23,6 +23,7 @@ func processAllPolicies( var wg sync.WaitGroup for _, policy := range policies { wg.Add(1) + policy.WorkspaceId = workspaceID go processPolicy(ctx, client, policy, &wg) } wg.Wait() @@ -36,6 +37,11 @@ func processPolicy( ) { defer policyWg.Done() + if policy.Name == "" { + log.Error("Policy name is required", "policy", policy) + return + } + body := createPolicyRequestBody(policy) if _, err := upsertPolicy(ctx, client, body); err != nil { log.Error("Failed to create policy", "name", policy.Name, "error", err) @@ -44,7 +50,6 @@ func processPolicy( } func createPolicyRequestBody(policy Policy) api.UpsertPolicyJSONRequestBody { - workspace := viper.GetString("workspace") // Convert targets targets := make([]api.PolicyTarget, len(policy.Targets)) for i, target := range policy.Targets { @@ -62,22 +67,22 @@ func createPolicyRequestBody(policy Policy) api.UpsertPolicyJSONRequestBody { TimeZone string `json:"timeZone"` }, len(policy.DenyWindows)) for i, window := range policy.DenyWindows { - rrule := window.RRule + rrule := window.Rrule denyWindows[i] = struct { Dtend *time.Time `json:"dtend,omitempty"` Rrule *map[string]interface{} `json:"rrule,omitempty"` TimeZone string `json:"timeZone"` }{ - Dtend: window.DtEnd, + Dtend: window.Dtend, Rrule: &rrule, TimeZone: window.TimeZone, } } // Convert version any approval - var versionAnyApprovals api.VersionAnyApproval + var versionAnyApprovals *api.VersionAnyApproval if policy.VersionAnyApprovals != nil { - versionAnyApprovals = api.VersionAnyApproval{ + versionAnyApprovals = &api.VersionAnyApproval{ RequiredApprovalsCount: policy.VersionAnyApprovals.RequiredApprovalsCount, } } @@ -119,11 +124,11 @@ func createPolicyRequestBody(policy Policy) api.UpsertPolicyJSONRequestBody { Description: policy.Description, Priority: policy.Priority, Enabled: policy.Enabled, - WorkspaceId: workspace, + WorkspaceId: policy.WorkspaceId, Targets: targets, DenyWindows: &denyWindows, DeploymentVersionSelector: deploymentVersionSelector, - VersionAnyApprovals: &versionAnyApprovals, + VersionAnyApprovals: versionAnyApprovals, VersionUserApprovals: &versionUserApprovals, VersionRoleApprovals: &versionRoleApprovals, } diff --git a/cmd/ctrlc/root/apply/types.go b/cmd/ctrlc/root/apply/types.go index 2314f8a..3608206 100644 --- a/cmd/ctrlc/root/apply/types.go +++ b/cmd/ctrlc/root/apply/types.go @@ -126,8 +126,8 @@ type PolicyTarget struct { type DenyWindow struct { TimeZone string `yaml:"timeZone"` - RRule map[string]any `yaml:"rrule"` - DtEnd *time.Time `yaml:"dtend,omitempty"` + Rrule map[string]any `yaml:"rrule"` + Dtend *time.Time `yaml:"dtend,omitempty"` } type DeploymentVersionSelector struct { From 990ad2cf424e8dacb144cc80275196018118e9d9 Mon Sep 17 00:00:00 2001 From: Aditya Choudhari Date: Sat, 17 May 2025 12:56:15 -0700 Subject: [PATCH 3/3] fix version role approval stuff --- cmd/ctrlc/root/apply/policy.go | 12 +++--------- internal/api/client.gen.go | 9 +++------ 2 files changed, 6 insertions(+), 15 deletions(-) diff --git a/cmd/ctrlc/root/apply/policy.go b/cmd/ctrlc/root/apply/policy.go index 94e7904..0fb34b5 100644 --- a/cmd/ctrlc/root/apply/policy.go +++ b/cmd/ctrlc/root/apply/policy.go @@ -94,17 +94,11 @@ func createPolicyRequestBody(policy Policy) api.UpsertPolicyJSONRequestBody { } } - versionRoleApprovals := make([]struct { - RequiredApprovalsCount *float32 `json:"requiredApprovalsCount,omitempty"` - RoleId string `json:"roleId"` - }, len(policy.VersionRoleApprovals)) + versionRoleApprovals := make([]api.VersionRoleApproval, len(policy.VersionRoleApprovals)) for i, approval := range policy.VersionRoleApprovals { count := approval.RequiredApprovalsCount - versionRoleApprovals[i] = struct { - RequiredApprovalsCount *float32 `json:"requiredApprovalsCount,omitempty"` - RoleId string `json:"roleId"` - }{ - RequiredApprovalsCount: &count, + versionRoleApprovals[i] = api.VersionRoleApproval{ + RequiredApprovalsCount: count, RoleId: approval.RoleId, } } diff --git a/internal/api/client.gen.go b/internal/api/client.gen.go index 41db7fd..2988ca0 100644 --- a/internal/api/client.gen.go +++ b/internal/api/client.gen.go @@ -874,12 +874,9 @@ type UpsertPolicyJSONBody struct { Priority *float32 `json:"priority,omitempty"` Targets []PolicyTarget `json:"targets"` VersionAnyApprovals *VersionAnyApproval `json:"versionAnyApprovals,omitempty"` - VersionRoleApprovals *[]struct { - RequiredApprovalsCount *float32 `json:"requiredApprovalsCount,omitempty"` - RoleId string `json:"roleId"` - } `json:"versionRoleApprovals,omitempty"` - VersionUserApprovals *[]VersionUserApproval `json:"versionUserApprovals,omitempty"` - WorkspaceId string `json:"workspaceId"` + VersionRoleApprovals *[]VersionRoleApproval `json:"versionRoleApprovals,omitempty"` + VersionUserApprovals *[]VersionUserApproval `json:"versionUserApprovals,omitempty"` + WorkspaceId string `json:"workspaceId"` } // UpdatePolicyJSONBody defines parameters for UpdatePolicy.