Skip to content
Draft
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
28 changes: 21 additions & 7 deletions app/cli/cmd/policy_develop_init.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,29 +29,41 @@ func newPolicyDevelopInitCmd() *cobra.Command {
name string
description string
directory string
policyType string
)

cmd := &cobra.Command{
Use: "init",
Short: "Initialize a new policy",
Long: `Initialize a new policy by creating template policy files in the specified directory.
By default, it creates chainloop-policy.yaml and chainloop-policy.rego files.`,
Example: `
# Initialize in current directory with separate files

Policy Types:
rego - Create a Rego-based policy using Open Policy Agent (default)
wasm-go - Create a WebAssembly policy using Go and TinyGo
wasm-js - Create a WebAssembly policy using JavaScript and Extism`,
Example: ` # Initialize a Rego policy in current directory (default)
chainloop policy develop init

# Initialize in specific directory with embedded format and policy name
chainloop policy develop init --directory ./policies --embedded --name mypolicy`,
# Initialize a Rego policy with custom name
chainloop policy develop init --type rego --name my-policy

# Initialize a WASM Go policy
chainloop policy develop init --type wasm-go --name my-policy --directory ./policies

# Initialize a WASM JS policy
chainloop policy develop init --type wasm-js --name validation --description "My validation policy"`,
RunE: func(_ *cobra.Command, _ []string) error {
if directory == "" {
directory = "."
}

opts := &action.PolicyInitOpts{
Force: force,
Embedded: embedded,
Name: name,
Description: description,
Directory: directory,
PolicyType: policyType,
}

policyInit, err := action.NewPolicyInit(opts, ActionOpts)
Expand All @@ -70,9 +82,11 @@ By default, it creates chainloop-policy.yaml and chainloop-policy.rego files.`,
}

cmd.Flags().BoolVarP(&force, "force", "f", false, "overwrite existing files")
cmd.Flags().BoolVar(&embedded, "embedded", false, "initialize an embedded policy (single YAML file)")
cmd.Flags().BoolVar(&embedded, "embedded", false, "initialize an embedded policy (single YAML file, Rego only)")
cmd.Flags().StringVar(&name, "name", "", "name of the policy")
cmd.Flags().StringVar(&description, "description", "", "description of the policy")
cmd.Flags().StringVar(&directory, "directory", "", "directory for policy")
cmd.Flags().StringVar(&directory, "directory", "", "directory for policy files")
cmd.Flags().StringVar(&policyType, "type", "", "policy type: rego (default), wasm-go, or wasm-js")

return cmd
}
245 changes: 202 additions & 43 deletions app/cli/internal/policydevel/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,38 @@ import (
//go:embed templates/*
var templateFS embed.FS

// PolicyType represents the type of policy to initialize
type PolicyType string

const (
PolicyTypeRego PolicyType = "rego"
PolicyTypeWasmGo PolicyType = "wasm-go"
PolicyTypeWasmJS PolicyType = "wasm-js"
)

const (
policyTemplateRegoPath = "templates/example-policy.rego"
policyTemplatePath = "templates/example-policy.yaml"
// General to all templates
policyYAMLFile = "policy.yaml"

// Rego templates
regoTemplateDir = "templates/rego"
regoPolicyFile = "example-policy.rego"
regoYAMLFile = "example-policy.yaml"

// WASM Go templates
wasmGoTemplateDir = "templates/wasm-go"
wasmGoPolicyFile = "policy.go.tmpl"
wasmGoModFile = "go.mod.tmpl"
wasmGoMakefileFile = "Makefile"

// WASM JS templates
wasmJSTemplateDir = "templates/wasm-js"
wasmJSPolicyFile = "policy.js"
wasmJSPackageFile = "package.json"
wasmJSEsbuildFile = "esbuild.js"
wasmJSDTSFile = "policy.d.ts"

// Defaults
defaultPolicyName = "policy"
defaultPolicyDescription = "Chainloop validation policy"
defaultMaterialKind = "SBOM_CYCLONEDX_JSON"
Expand All @@ -45,90 +74,220 @@ type TemplateData struct {
MaterialKind string
}

type Content struct {
YAML string
Rego string
}

type InitOptions struct {
Directory string
PolicyType PolicyType
Embedded bool
Force bool
Name string
Description string
}

func Initialize(opts *InitOptions) error {
content, err := loadAndProcessTemplates(opts)
// Default to Rego if no type specified
if opts.PolicyType == "" {
opts.PolicyType = PolicyTypeRego
}

// Route to appropriate initializer based on policy type
switch opts.PolicyType {
case PolicyTypeRego:
return initializeRegoPolicy(opts)
case PolicyTypeWasmGo:
return initializeWasmGoPolicy(opts)
case PolicyTypeWasmJS:
return initializeWasmJSPolicy(opts)
default:
return fmt.Errorf("unsupported policy type: %s", opts.PolicyType)
}
}

// initializeRegoPolicy creates a Rego-based policy
func initializeRegoPolicy(opts *InitOptions) error {
// Load templates
regoContent, err := templateFS.ReadFile(filepath.Join(regoTemplateDir, regoPolicyFile))
if err != nil {
return fmt.Errorf("failed to read Rego template: %w", err)
}

yamlContent, err := templateFS.ReadFile(filepath.Join(regoTemplateDir, regoYAMLFile))
if err != nil {
return fmt.Errorf("failed to read YAML template: %w", err)
}

// Prepare template data
data := &TemplateData{
Name: getPolicyName(opts.Name),
Description: getPolicyDescription(opts.Description),
RegoPath: sanitizeName(getPolicyName(opts.Name)) + ".rego",
RegoContent: string(regoContent),
Embedded: opts.Embedded,
MaterialKind: defaultMaterialKind,
}

// Process YAML template
yamlProcessed, err := executeTemplate(string(yamlContent), data)
if err != nil {
return fmt.Errorf("failed to process templates: %w", err)
return fmt.Errorf("failed to process YAML template: %w", err)
}

// Prepare files to write
files := make(map[string]string)
fileNameBase := sanitizeName(getPolicyName(opts.Name))
fileNameBase := sanitizeName(data.Name)

if opts.Embedded {
files[fileNameBase+".yaml"] = content.YAML
} else {
files[fileNameBase+".yaml"] = content.YAML
files[fileNameBase+".rego"] = content.Rego
files[fileNameBase+".yaml"] = yamlProcessed
if !opts.Embedded {
files[fileNameBase+".rego"] = data.RegoContent
}

return writeFiles(opts.Directory, files, opts.Force)
}

func getPolicyName(name string) string {
if name == "" {
return defaultPolicyName
// initializeWasmGoPolicy creates a WASM Go-based policy
func initializeWasmGoPolicy(opts *InitOptions) error {
// Load templates
policyContent, err := templateFS.ReadFile(filepath.Join(wasmGoTemplateDir, wasmGoPolicyFile))
if err != nil {
return fmt.Errorf("failed to read Go policy template: %w", err)
}
return name
}

func getPolicyDescription(description string) string {
if description == "" {
return defaultPolicyDescription
goModContent, err := templateFS.ReadFile(filepath.Join(wasmGoTemplateDir, wasmGoModFile))
if err != nil {
return fmt.Errorf("failed to read go.mod template: %w", err)
}
return description

yamlContent, err := templateFS.ReadFile(filepath.Join(wasmGoTemplateDir, policyYAMLFile))
if err != nil {
return fmt.Errorf("failed to read YAML template: %w", err)
}

makefileContent, err := templateFS.ReadFile(filepath.Join(wasmGoTemplateDir, wasmGoMakefileFile))
if err != nil {
return fmt.Errorf("failed to read Makefile template: %w", err)
}

// Prepare template data
data := &TemplateData{
Name: getPolicyName(opts.Name),
Description: getPolicyDescription(opts.Description),
MaterialKind: defaultMaterialKind,
}

// Process templates
policyProcessed, err := executeTemplate(string(policyContent), data)
if err != nil {
return fmt.Errorf("failed to process policy template: %w", err)
}

goModProcessed, err := executeTemplate(string(goModContent), data)
if err != nil {
return fmt.Errorf("failed to process go.mod template: %w", err)
}

yamlProcessed, err := executeTemplate(string(yamlContent), data)
if err != nil {
return fmt.Errorf("failed to process YAML template: %w", err)
}

makefileProcessed, err := executeTemplate(string(makefileContent), data)
if err != nil {
return fmt.Errorf("failed to process Makefile template: %w", err)
}

// Prepare files to write
files := map[string]string{
"policy.go": policyProcessed,
"go.mod": goModProcessed,
"policy.yaml": yamlProcessed,
"Makefile": makefileProcessed,
}

return writeFiles(opts.Directory, files, opts.Force)
}

func loadAndProcessTemplates(opts *InitOptions) (*Content, error) {
regoContent, err := templateFS.ReadFile(policyTemplateRegoPath)
// initializeWasmJSPolicy creates a WASM JavaScript-based policy
func initializeWasmJSPolicy(opts *InitOptions) error {
// Load templates
policyContent, err := templateFS.ReadFile(filepath.Join(wasmJSTemplateDir, wasmJSPolicyFile))
if err != nil {
return nil, fmt.Errorf("failed to read Rego template: %w", err)
return fmt.Errorf("failed to read JS policy template: %w", err)
}

packageContent, err := templateFS.ReadFile(filepath.Join(wasmJSTemplateDir, wasmJSPackageFile))
if err != nil {
return fmt.Errorf("failed to read package.json template: %w", err)
}

esbuildContent, err := templateFS.ReadFile(filepath.Join(wasmJSTemplateDir, wasmJSEsbuildFile))
if err != nil {
return fmt.Errorf("failed to read esbuild.js template: %w", err)
}

dtsContent, err := templateFS.ReadFile(filepath.Join(wasmJSTemplateDir, wasmJSDTSFile))
if err != nil {
return fmt.Errorf("failed to read policy.d.ts template: %w", err)
}

yamlContent, err := templateFS.ReadFile(filepath.Join(wasmJSTemplateDir, policyYAMLFile))
if err != nil {
return fmt.Errorf("failed to read YAML template: %w", err)
}

// Prepare template data
data := &TemplateData{
Name: getPolicyName(opts.Name),
Description: getPolicyDescription(opts.Description),
RegoPath: sanitizeName(getPolicyName(opts.Name)) + ".rego",
RegoContent: string(regoContent),
Embedded: opts.Embedded,
MaterialKind: defaultMaterialKind,
}

// Process main template
content, err := templateFS.ReadFile(policyTemplatePath)
// Process templates
policyProcessed, err := executeTemplate(string(policyContent), data)
if err != nil {
return nil, fmt.Errorf("failed to read policy template: %w", err)
return fmt.Errorf("failed to process policy template: %w", err)
}

yamlContent, err := executeTemplate(string(content), data)
packageProcessed, err := executeTemplate(string(packageContent), data)
if err != nil {
return nil, fmt.Errorf("failed to execute template: %w", err)
return fmt.Errorf("failed to process package.json template: %w", err)
}

// For non-embedded case, we still need the Rego content to write to file
if !opts.Embedded {
return &Content{
YAML: yamlContent,
Rego: data.RegoContent,
}, nil
dtsProcessed, err := executeTemplate(string(dtsContent), data)
if err != nil {
return fmt.Errorf("failed to process policy.d.ts template: %w", err)
}

yamlProcessed, err := executeTemplate(string(yamlContent), data)
if err != nil {
return fmt.Errorf("failed to process YAML template: %w", err)
}

// Prepare files to write (esbuild.js doesn't need template processing)
files := map[string]string{
"policy.js": policyProcessed,
"package.json": packageProcessed,
"esbuild.js": string(esbuildContent),
"policy.d.ts": dtsProcessed,
"policy.yaml": yamlProcessed,
}

return writeFiles(opts.Directory, files, opts.Force)
}

func getPolicyName(name string) string {
if name == "" {
return defaultPolicyName
}
return name
}

return &Content{YAML: yamlContent}, nil
func getPolicyDescription(description string) string {
if description == "" {
return defaultPolicyDescription
}
return description
}

// Add custom template functions
// executeTemplate processes a template with the given data
func executeTemplate(content string, data *TemplateData) (string, error) {
tmpl := template.New("policy").Funcs(template.FuncMap{
"sanitize": sanitizeName,
Expand Down
26 changes: 0 additions & 26 deletions app/cli/internal/policydevel/init_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,32 +107,6 @@ func TestInitialize(t *testing.T) {
})
}

func TestLoadAndProcessTemplates(t *testing.T) {
t.Run("embedded rego", func(t *testing.T) {
opts := &InitOptions{
Embedded: true,
Name: "embedded-test",
}

content, err := loadAndProcessTemplates(opts)
require.NoError(t, err)
assert.NotEmpty(t, content.YAML)
assert.Empty(t, content.Rego) // Rego file should be empty for embedded
})

t.Run("separate rego file", func(t *testing.T) {
opts := &InitOptions{
Embedded: false,
Name: "separate-rego-test",
}

content, err := loadAndProcessTemplates(opts)
require.NoError(t, err)
assert.NotEmpty(t, content.YAML)
assert.NotEmpty(t, content.Rego)
})
}

func TestExecuteTemplate(t *testing.T) {
testCases := []struct {
name string
Expand Down
Loading
Loading