Skip to content

Commit 2bd68bc

Browse files
Merge pull request #27 from ctrlplanedev/apply-policy-yaml
feat: ctrlc apply policy
2 parents bdba1e9 + 990ad2c commit 2bd68bc

4 files changed

Lines changed: 206 additions & 10 deletions

File tree

cmd/ctrlc/root/apply/cmd.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ func runApply(filePath string) error {
4545
processAllSystems(ctx, client, workspaceID, config.Systems)
4646
processResourceProvider(ctx, client, workspaceID.String(), config.Providers)
4747
processResourceRelationships(ctx, client, workspaceID.String(), config.Relationships)
48+
processAllPolicies(ctx, client, workspaceID.String(), config.Policies)
4849

4950
return nil
5051
}

cmd/ctrlc/root/apply/policy.go

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
package apply
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"sync"
7+
"time"
8+
9+
"github.com/charmbracelet/log"
10+
"github.com/ctrlplanedev/cli/internal/api"
11+
)
12+
13+
func processAllPolicies(
14+
ctx context.Context,
15+
client *api.ClientWithResponses,
16+
workspaceID string,
17+
policies []Policy,
18+
) {
19+
if len(policies) == 0 {
20+
return
21+
}
22+
23+
var wg sync.WaitGroup
24+
for _, policy := range policies {
25+
wg.Add(1)
26+
policy.WorkspaceId = workspaceID
27+
go processPolicy(ctx, client, policy, &wg)
28+
}
29+
wg.Wait()
30+
}
31+
32+
func processPolicy(
33+
ctx context.Context,
34+
client *api.ClientWithResponses,
35+
policy Policy,
36+
policyWg *sync.WaitGroup,
37+
) {
38+
defer policyWg.Done()
39+
40+
if policy.Name == "" {
41+
log.Error("Policy name is required", "policy", policy)
42+
return
43+
}
44+
45+
body := createPolicyRequestBody(policy)
46+
if _, err := upsertPolicy(ctx, client, body); err != nil {
47+
log.Error("Failed to create policy", "name", policy.Name, "error", err)
48+
return
49+
}
50+
}
51+
52+
func createPolicyRequestBody(policy Policy) api.UpsertPolicyJSONRequestBody {
53+
// Convert targets
54+
targets := make([]api.PolicyTarget, len(policy.Targets))
55+
for i, target := range policy.Targets {
56+
targets[i] = api.PolicyTarget{
57+
DeploymentSelector: target.DeploymentSelector,
58+
EnvironmentSelector: target.EnvironmentSelector,
59+
ResourceSelector: target.ResourceSelector,
60+
}
61+
}
62+
63+
// Convert deny windows
64+
denyWindows := make([]struct {
65+
Dtend *time.Time `json:"dtend,omitempty"`
66+
Rrule *map[string]interface{} `json:"rrule,omitempty"`
67+
TimeZone string `json:"timeZone"`
68+
}, len(policy.DenyWindows))
69+
for i, window := range policy.DenyWindows {
70+
rrule := window.Rrule
71+
denyWindows[i] = struct {
72+
Dtend *time.Time `json:"dtend,omitempty"`
73+
Rrule *map[string]interface{} `json:"rrule,omitempty"`
74+
TimeZone string `json:"timeZone"`
75+
}{
76+
Dtend: window.Dtend,
77+
Rrule: &rrule,
78+
TimeZone: window.TimeZone,
79+
}
80+
}
81+
82+
// Convert version any approval
83+
var versionAnyApprovals *api.VersionAnyApproval
84+
if policy.VersionAnyApprovals != nil {
85+
versionAnyApprovals = &api.VersionAnyApproval{
86+
RequiredApprovalsCount: policy.VersionAnyApprovals.RequiredApprovalsCount,
87+
}
88+
}
89+
90+
versionUserApprovals := make([]api.VersionUserApproval, len(policy.VersionUserApprovals))
91+
for i, approval := range policy.VersionUserApprovals {
92+
versionUserApprovals[i] = api.VersionUserApproval{
93+
UserId: approval.UserId,
94+
}
95+
}
96+
97+
versionRoleApprovals := make([]api.VersionRoleApproval, len(policy.VersionRoleApprovals))
98+
for i, approval := range policy.VersionRoleApprovals {
99+
count := approval.RequiredApprovalsCount
100+
versionRoleApprovals[i] = api.VersionRoleApproval{
101+
RequiredApprovalsCount: count,
102+
RoleId: approval.RoleId,
103+
}
104+
}
105+
106+
// Create deployment version selector if present
107+
var deploymentVersionSelector *api.DeploymentVersionSelector
108+
if policy.DeploymentVersionSelector != nil {
109+
deploymentVersionSelector = &api.DeploymentVersionSelector{
110+
Name: policy.DeploymentVersionSelector.Name,
111+
DeploymentVersionSelector: policy.DeploymentVersionSelector.DeploymentVersionSelector,
112+
Description: policy.DeploymentVersionSelector.Description,
113+
}
114+
}
115+
116+
return api.UpsertPolicyJSONRequestBody{
117+
Name: policy.Name,
118+
Description: policy.Description,
119+
Priority: policy.Priority,
120+
Enabled: policy.Enabled,
121+
WorkspaceId: policy.WorkspaceId,
122+
Targets: targets,
123+
DenyWindows: &denyWindows,
124+
DeploymentVersionSelector: deploymentVersionSelector,
125+
VersionAnyApprovals: versionAnyApprovals,
126+
VersionUserApprovals: &versionUserApprovals,
127+
VersionRoleApprovals: &versionRoleApprovals,
128+
}
129+
}
130+
131+
func upsertPolicy(
132+
ctx context.Context,
133+
client *api.ClientWithResponses,
134+
policy api.UpsertPolicyJSONRequestBody,
135+
) (string, error) {
136+
resp, err := client.UpsertPolicyWithResponse(ctx, policy)
137+
138+
if err != nil {
139+
return "", fmt.Errorf("API request failed: %w", err)
140+
}
141+
142+
if resp.StatusCode() >= 400 {
143+
return "", fmt.Errorf("API returned error status: %d", resp.StatusCode())
144+
}
145+
146+
if resp.JSON200 != nil {
147+
return resp.JSON200.Id.String(), nil
148+
}
149+
150+
return "", fmt.Errorf("unexpected response format")
151+
}

cmd/ctrlc/root/apply/types.go

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
package apply
22

3+
import "time"
4+
35
// Config represents the structure of the YAML file
46
type Config struct {
57
Systems []System `yaml:"systems"`
68
Providers ResourceProvider `yaml:"resourceProvider"`
79
Relationships []ResourceRelationship `yaml:"relationshipRules"`
10+
Policies []Policy `yaml:"policies,omitempty"`
811
}
912

1013
type System struct {
@@ -99,3 +102,49 @@ type ResourceRelationship struct {
99102
MetadataKeysMatch []string `yaml:"metadataKeysMatch"`
100103
DependencyType string `yaml:"dependencyType"`
101104
}
105+
106+
// Policy structs
107+
type Policy struct {
108+
Name string `yaml:"name"`
109+
Description *string `yaml:"description,omitempty"`
110+
Priority *float32 `yaml:"priority,omitempty"`
111+
Enabled *bool `yaml:"enabled,omitempty"`
112+
WorkspaceId string `yaml:"workspaceId"`
113+
Targets []PolicyTarget `yaml:"targets"`
114+
DenyWindows []DenyWindow `yaml:"denyWindows,omitempty"`
115+
DeploymentVersionSelector *DeploymentVersionSelector `yaml:"deploymentVersionSelector,omitempty"`
116+
VersionAnyApprovals *VersionAnyApproval `yaml:"versionAnyApprovals,omitempty"`
117+
VersionUserApprovals []VersionUserApproval `yaml:"versionUserApprovals,omitempty"`
118+
VersionRoleApprovals []VersionRoleApproval `yaml:"versionRoleApprovals,omitempty"`
119+
}
120+
121+
type PolicyTarget struct {
122+
DeploymentSelector *map[string]any `yaml:"deploymentSelector,omitempty"`
123+
EnvironmentSelector *map[string]any `yaml:"environmentSelector,omitempty"`
124+
ResourceSelector *map[string]any `yaml:"resourceSelector,omitempty"`
125+
}
126+
127+
type DenyWindow struct {
128+
TimeZone string `yaml:"timeZone"`
129+
Rrule map[string]any `yaml:"rrule"`
130+
Dtend *time.Time `yaml:"dtend,omitempty"`
131+
}
132+
133+
type DeploymentVersionSelector struct {
134+
Name string `yaml:"name"`
135+
DeploymentVersionSelector map[string]any `yaml:"deploymentVersionSelector"`
136+
Description *string `yaml:"description,omitempty"`
137+
}
138+
139+
type VersionAnyApproval struct {
140+
RequiredApprovalsCount float32 `yaml:"requiredApprovalsCount"`
141+
}
142+
143+
type VersionUserApproval struct {
144+
UserId string `yaml:"userId"`
145+
}
146+
147+
type VersionRoleApproval struct {
148+
RoleId string `yaml:"roleId"`
149+
RequiredApprovalsCount float32 `yaml:"requiredApprovalsCount"`
150+
}

internal/api/client.gen.go

Lines changed: 5 additions & 10 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)