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
2 changes: 2 additions & 0 deletions cmd/ctrlc/root/api/create/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"github.com/ctrlplanedev/cli/cmd/ctrlc/root/api/create/deploymentversion"
"github.com/ctrlplanedev/cli/cmd/ctrlc/root/api/create/deploymentversionchannel"
"github.com/ctrlplanedev/cli/cmd/ctrlc/root/api/create/environment"
"github.com/ctrlplanedev/cli/cmd/ctrlc/root/api/create/policy"
"github.com/ctrlplanedev/cli/cmd/ctrlc/root/api/create/relationship"
"github.com/ctrlplanedev/cli/cmd/ctrlc/root/api/create/release"
"github.com/ctrlplanedev/cli/cmd/ctrlc/root/api/create/releasechannel"
Expand All @@ -28,6 +29,7 @@ func NewCreateCmd() *cobra.Command {
cmd.AddCommand(relationship.NewRelationshipCmd())
cmd.AddCommand(release.NewCreateReleaseCmd())
cmd.AddCommand(system.NewCreateSystemCmd())
cmd.AddCommand(policy.NewCreatePolicyCmd())

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

import (
"encoding/json"
"fmt"
"time"

"github.com/MakeNowJust/heredoc/v2"
"github.com/ctrlplanedev/cli/internal/api"
"github.com/ctrlplanedev/cli/internal/cliutil"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)

func NewCreatePolicyCmd() *cobra.Command {
var name string
var workspaceID string
var description string
var priority float32
var enabled bool
var deploymentTargetSelector string
var environmentTargetSelector string
var resourceTargetSelector string
var denyWindows string
var versionAnyApprovals string
var versionUserApprovals string
var versionRoleApprovals string
var deploymentVersionSelector string

cmd := &cobra.Command{
Use: "policy [flags]",
Short: "Create a new policy",
Long: `Create a new policy with specified parameters`,
Example: heredoc.Doc(`
# Create a new policy
$ ctrlc create policy --name my-policy --workspace-id 00000000-0000-0000-0000-000000000000

# Create a new policy with deployment selector
$ ctrlc create policy --name my-policy --workspace-id 00000000-0000-0000-0000-000000000000 --deployment-selector '{"type": "production"}'

# Create a new policy with environment selector
$ ctrlc create policy --name my-policy --workspace-id 00000000-0000-0000-0000-000000000000 --environment-selector '{"name": "prod"}'

# Create a new policy with deny windows
$ ctrlc create policy --name my-policy --workspace-id 00000000-0000-0000-0000-000000000000 --deny-windows '[{"timeZone": "UTC", "rrule": {"freq": "WEEKLY", "byday": ["SA", "SU"]}}]'

# Create a new policy with version approvals
$ ctrlc create policy --name my-policy --workspace-id 00000000-0000-0000-0000-000000000000 --version-any-approvals '{"requiredApprovalsCount": 2}' --version-user-approvals '[{"userId": "user1"}, {"userId": "user2"}]' --version-role-approvals '[{"roleId": "role1", "requiredApprovalsCount": 1}]'
`),
RunE: func(cmd *cobra.Command, args []string) error {
apiURL := viper.GetString("url")
apiKey := viper.GetString("api-key")
client, err := api.NewAPIKeyClientWithResponses(apiURL, apiKey)
if err != nil {
return fmt.Errorf("failed to create API client: %w", err)
}

// Parse selectors from JSON strings
var deploymentSelector *map[string]interface{}
if deploymentTargetSelector != "" {
var parsedSelector map[string]interface{}
if err := json.Unmarshal([]byte(deploymentTargetSelector), &parsedSelector); err != nil {
return fmt.Errorf("invalid deployment target selector JSON: %w", err)
}
deploymentSelector = &parsedSelector
}

var environmentSelector *map[string]interface{}
if environmentTargetSelector != "" {
var parsedSelector map[string]interface{}
if err := json.Unmarshal([]byte(environmentTargetSelector), &parsedSelector); err != nil {
return fmt.Errorf("invalid environment target selector JSON: %w", err)
}
environmentSelector = &parsedSelector
}

var resourceSelector *map[string]interface{}
if resourceTargetSelector != "" {
var parsedSelector map[string]interface{}
if err := json.Unmarshal([]byte(resourceTargetSelector), &parsedSelector); err != nil {
return fmt.Errorf("invalid resource target selector JSON: %w", err)
}
resourceSelector = &parsedSelector
}

// Parse deny windows
var parsedDenyWindows []struct {
Dtend *time.Time `json:"dtend,omitempty"`
Rrule *map[string]interface{} `json:"rrule,omitempty"`
TimeZone string `json:"timeZone"`
}
if denyWindows != "" {
if err := json.Unmarshal([]byte(denyWindows), &parsedDenyWindows); err != nil {
return fmt.Errorf("invalid deny windows JSON: %w", err)
}
}

// Parse version any approvals
var parsedVersionAnyApprovals *[]struct {
RequiredApprovalsCount *float32 `json:"requiredApprovalsCount,omitempty"`
}
if versionAnyApprovals != "" {
var approvals []struct {
RequiredApprovalsCount *float32 `json:"requiredApprovalsCount,omitempty"`
}
if err := json.Unmarshal([]byte(versionAnyApprovals), &approvals); err != nil {
return fmt.Errorf("invalid version any approvals JSON: %w", err)
}
parsedVersionAnyApprovals = &approvals
}

// Parse version user approvals
var parsedVersionUserApprovals []api.VersionUserApproval
if versionUserApprovals != "" {
if err := json.Unmarshal([]byte(versionUserApprovals), &parsedVersionUserApprovals); err != nil {
return fmt.Errorf("invalid version user approvals JSON: %w", err)
}
} else {
parsedVersionUserApprovals = []api.VersionUserApproval{}
}

// Parse version role approvals
var parsedVersionRoleApprovals []struct {
RequiredApprovalsCount *float32 `json:"requiredApprovalsCount,omitempty"`
RoleId *string `json:"roleId,omitempty"`
}
if versionRoleApprovals != "" {
if err := json.Unmarshal([]byte(versionRoleApprovals), &parsedVersionRoleApprovals); err != nil {
return fmt.Errorf("invalid version role approvals JSON: %w", err)
}
} else {
parsedVersionRoleApprovals = []struct {
RequiredApprovalsCount *float32 `json:"requiredApprovalsCount,omitempty"`
RoleId *string `json:"roleId,omitempty"`
}{}
}

// Parse deployment version selector
var parsedDeploymentVersionSelector *api.DeploymentVersionSelector
if deploymentVersionSelector != "" {
var selector map[string]interface{}

if err := json.Unmarshal([]byte(deploymentVersionSelector), &selector); err != nil {
return fmt.Errorf("invalid deployment version selector JSON: %w", err)
}

parsedDeploymentVersionSelector = &api.DeploymentVersionSelector{
DeploymentVersionSelector: selector,
Name: name,
}
}

// Create policy request
body := api.CreatePolicyJSONRequestBody{
Name: name,
WorkspaceId: workspaceID,
Description: &description,
Priority: &priority,
Enabled: &enabled,
Targets: []api.PolicyTarget{
{
DeploymentSelector: deploymentSelector,
EnvironmentSelector: environmentSelector,
ResourceSelector: resourceSelector,
},
},
Comment on lines +160 to +166
Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Consider supporting multiple targets.

The API model appears to support multiple targets (using a slice), but the implementation only creates a single target. If multiple targets should be supported, consider adding flags or accepting a JSON array for all targets.


🏁 Script executed:

#!/bin/bash
# Check if policies can have multiple targets

echo "Checking if the API supports multiple targets..."
rg -A 15 "PolicyTarget" --type go | grep -B 2 -A 2 "Targets.*\[\]"

Length of output: 1149


Action: Enhance support for multiple policy targets

The current implementation in cmd/ctrlc/root/api/create/policy/policy.go creates a single target:

				Targets: []api.PolicyTarget{
					{
						DeploymentSelector:  deploymentSelector,
						EnvironmentSelector: environmentSelector,
						ResourceSelector:    resourceSelector,
					},
				},

However, as confirmed in internal/api/client.gen.go, the API model supports multiple targets via a slice. If the design intent is to allow multiple policy targets, please consider extending the CLI to either add additional flags or allow a JSON array input so that users can specify more than one target.

DenyWindows: parsedDenyWindows,
DeploymentVersionSelector: parsedDeploymentVersionSelector,
VersionAnyApprovals: parsedVersionAnyApprovals,
VersionUserApprovals: parsedVersionUserApprovals,
VersionRoleApprovals: parsedVersionRoleApprovals,
}

resp, err := client.CreatePolicy(cmd.Context(), body)
if err != nil {
return fmt.Errorf("failed to create policy: %w", err)
}

return cliutil.HandleResponseOutput(cmd, resp)
},
}

// Add flags
cmd.Flags().StringVarP(&name, "name", "n", "", "Name of the policy (required)")
cmd.Flags().StringVarP(&workspaceID, "workspace-id", "w", "", "Workspace ID (required)")
cmd.Flags().StringVarP(&description, "description", "d", "", "Description of the policy")
cmd.Flags().Float32VarP(&priority, "priority", "p", 0, "Priority of the policy (default: 0)")
cmd.Flags().BoolVarP(&enabled, "enabled", "e", true, "Whether the policy is enabled (default: true)")
cmd.Flags().StringVar(&deploymentTargetSelector, "deployment-selector", "", "JSON string for deployment target selector")
cmd.Flags().StringVar(&environmentTargetSelector, "environment-selector", "", "JSON string for environment target selector")
cmd.Flags().StringVar(&resourceTargetSelector, "resource-selector", "", "JSON string for resource target selector")
cmd.Flags().StringVar(&denyWindows, "deny-windows", "", "JSON string for deny windows")
cmd.Flags().StringVar(&versionAnyApprovals, "version-any-approvals", "", "JSON string for version any approvals")
cmd.Flags().StringVar(&versionUserApprovals, "version-user-approvals", "", "JSON string for version user approvals")
cmd.Flags().StringVar(&versionRoleApprovals, "version-role-approvals", "", "JSON string for version role approvals")
cmd.Flags().StringVar(&deploymentVersionSelector, "deployment-version-selector", "", "JSON string for deployment version selector")

// Mark required flags
cmd.MarkFlagRequired("name")
cmd.MarkFlagRequired("workspace-id")

return cmd
}
Loading