Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions cmd/ctrlc/root/apply/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -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, workspaceID.String(), config.Policies)

return nil
}
151 changes: 151 additions & 0 deletions cmd/ctrlc/root/apply/policy.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
package apply

import (
"context"
"fmt"
"sync"
"time"

"github.com/charmbracelet/log"
"github.com/ctrlplanedev/cli/internal/api"
)

func processAllPolicies(
ctx context.Context,
client *api.ClientWithResponses,
workspaceID string,
policies []Policy,
) {
if len(policies) == 0 {
return
}

var wg sync.WaitGroup
for _, policy := range policies {
wg.Add(1)
policy.WorkspaceId = workspaceID
go processPolicy(ctx, client, policy, &wg)
Comment on lines +24 to +27
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Avoid clobbering a workspace ID provided in YAML

policy.WorkspaceId = workspaceID always overwrites the value that may already be present on the incoming Policy object.
That makes it impossible for users to set workspaceId per-policy in their YAML file – behaviour that other resources honour.

Consider preserving the YAML value and only falling back to the CLI/flag value when the field is empty:

-        policy.WorkspaceId = workspaceID
+        if policy.WorkspaceId == "" {
+            policy.WorkspaceId = workspaceID    // fallback only
+        }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
for _, policy := range policies {
wg.Add(1)
policy.WorkspaceId = workspaceID
go processPolicy(ctx, client, policy, &wg)
for _, policy := range policies {
wg.Add(1)
if policy.WorkspaceId == "" {
policy.WorkspaceId = workspaceID // fallback only
}
go processPolicy(ctx, client, policy, &wg)
}
🤖 Prompt for AI Agents
In cmd/ctrlc/root/apply/policy.go around lines 24 to 27, the code
unconditionally overwrites the policy's WorkspaceId with the workspaceID
variable, ignoring any value set in the YAML. Modify the code to check if
policy.WorkspaceId is empty before assigning workspaceID, so that the YAML value
is preserved if present and the CLI/flag value is used only as a fallback.

}
wg.Wait()
}

func processPolicy(
ctx context.Context,
client *api.ClientWithResponses,
policy Policy,
policyWg *sync.WaitGroup,
) {
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)
return
}
}

func createPolicyRequestBody(policy Policy) api.UpsertPolicyJSONRequestBody {
// 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([]api.VersionRoleApproval, len(policy.VersionRoleApprovals))
for i, approval := range policy.VersionRoleApprovals {
count := approval.RequiredApprovalsCount
versionRoleApprovals[i] = api.VersionRoleApproval{
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: policy.WorkspaceId,
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")
}
Comment on lines +131 to +151
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add error details from API response

The error handling in upsertPolicy could be improved by including error details from the API response when available, rather than just returning the status code.

 if resp.StatusCode() >= 400 {
-	return "", fmt.Errorf("API returned error status: %d", resp.StatusCode())
+	errorMsg := "Unknown error"
+	if resp.HTTPResponse != nil && resp.HTTPResponse.Body != nil {
+		// Read error details if possible
+		defer resp.HTTPResponse.Body.Close()
+		body, err := io.ReadAll(resp.HTTPResponse.Body)
+		if err == nil && len(body) > 0 {
+			errorMsg = string(body)
+		}
+	}
+	return "", fmt.Errorf("API returned error status: %d, details: %s", resp.StatusCode(), errorMsg)
 }

Don't forget to add the import for io.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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")
}
import (
// ... other imports ...
"io"
)
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 {
errorMsg := "Unknown error"
if resp.HTTPResponse != nil && resp.HTTPResponse.Body != nil {
// Read error details if possible
defer resp.HTTPResponse.Body.Close()
body, err := io.ReadAll(resp.HTTPResponse.Body)
if err == nil && len(body) > 0 {
errorMsg = string(body)
}
}
return "", fmt.Errorf("API returned error status: %d, details: %s", resp.StatusCode(), errorMsg)
}
if resp.JSON200 != nil {
return resp.JSON200.Id.String(), nil
}
return "", fmt.Errorf("unexpected response format")
}
🤖 Prompt for AI Agents
In cmd/ctrlc/root/apply/policy.go around lines 132 to 152, enhance the error
handling in upsertPolicy by extracting and including detailed error information
from the API response body when the status code indicates an error (>= 400).
Modify the error return to include this detail instead of only the status code.
Also, add the necessary import for the "io" package to support reading the
response body.

49 changes: 49 additions & 0 deletions cmd/ctrlc/root/apply/types.go
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -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"`
}
15 changes: 5 additions & 10 deletions internal/api/client.gen.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.