Skip to content

Commit f252dd8

Browse files
authored
feat(policy): add policy develop lint (#2250)
Signed-off-by: Sylwester Piskozub <[email protected]>
1 parent acf0a9b commit f252dd8

File tree

7 files changed

+679
-152
lines changed

7 files changed

+679
-152
lines changed

app/cli/cmd/policy_develop.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,6 @@ Refer to https://docs.chainloop.dev/guides/custom-policies
2828
`,
2929
}
3030

31-
cmd.AddCommand(newPolicyDevelopInitCmd())
31+
cmd.AddCommand(newPolicyDevelopInitCmd(), newPolicyDevelopLintCmd())
3232
return cmd
3333
}

app/cli/cmd/policy_develop_lint.go

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
//
2+
// Copyright 2025 The Chainloop Authors.
3+
//
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
//
8+
// http://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
16+
package cmd
17+
18+
import (
19+
"fmt"
20+
21+
"github.com/chainloop-dev/chainloop/app/cli/internal/action"
22+
"github.com/spf13/cobra"
23+
)
24+
25+
func newPolicyDevelopLintCmd() *cobra.Command {
26+
var (
27+
policyPath string
28+
format bool
29+
)
30+
31+
cmd := &cobra.Command{
32+
Use: "lint",
33+
Short: "Lint chainloop policy structure and content",
34+
Long: `Performs comprehensive validation of:
35+
- *.yaml files (schema validation)
36+
- *.rego (formatting, linting, structure)`,
37+
RunE: func(cmd *cobra.Command, _ []string) error {
38+
a, err := action.NewPolicyLint(actionOpts)
39+
if err != nil {
40+
return fmt.Errorf("failed to initialize linter: %w", err)
41+
}
42+
43+
result, err := a.Run(cmd.Context(), &action.PolicyLintOpts{
44+
PolicyPath: policyPath,
45+
Format: format,
46+
})
47+
if err != nil {
48+
return fmt.Errorf("linting failed: %w", err)
49+
}
50+
51+
if result.Valid {
52+
logger.Info().Msg("policy is valid!")
53+
return nil
54+
}
55+
56+
// Print all validation errors
57+
for _, err := range result.Errors {
58+
logger.Error().Msg(err)
59+
}
60+
return fmt.Errorf("policy validation failed with %d issues", len(result.Errors))
61+
},
62+
}
63+
64+
cmd.Flags().StringVarP(&policyPath, "policy", "p", ".", "Path to policy directory")
65+
cmd.Flags().BoolVar(&format, "format", false, "Auto-format file with opa fmt")
66+
return cmd
67+
}

app/cli/documentation/cli-reference.mdx

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2870,6 +2870,44 @@ Options inherited from parent commands
28702870
-y, --yes Skip confirmation
28712871
```
28722872

2873+
#### chainloop policy develop lint
2874+
2875+
Lint chainloop policy structure and content
2876+
2877+
Synopsis
2878+
2879+
Performs comprehensive validation of:
2880+
- *.yaml files (schema validation)
2881+
- *.rego (formatting, linting, structure)
2882+
2883+
```
2884+
chainloop policy develop lint [flags]
2885+
```
2886+
2887+
Options
2888+
2889+
```
2890+
--format Auto-format file with opa fmt
2891+
-h, --help help for lint
2892+
-p, --policy string Path to policy directory (default ".")
2893+
```
2894+
2895+
Options inherited from parent commands
2896+
2897+
```
2898+
--artifact-cas string URL for the Artifacts Content Addressable Storage API ($CHAINLOOP_ARTIFACT_CAS_API) (default "api.cas.chainloop.dev:443")
2899+
--artifact-cas-ca string CUSTOM CA file for the Artifacts CAS API (optional) ($CHAINLOOP_ARTIFACT_CAS_API_CA)
2900+
-c, --config string Path to an existing config file (default is $HOME/.config/chainloop/config.toml)
2901+
--control-plane string URL for the Control Plane API ($CHAINLOOP_CONTROL_PLANE_API) (default "api.cp.chainloop.dev:443")
2902+
--control-plane-ca string CUSTOM CA file for the Control Plane API (optional) ($CHAINLOOP_CONTROL_PLANE_API_CA)
2903+
--debug Enable debug/verbose logging mode
2904+
-i, --insecure Skip TLS transport during connection to the control plane ($CHAINLOOP_API_INSECURE)
2905+
-n, --org string organization name
2906+
-o, --output string Output format, valid options are json and table (default "table")
2907+
-t, --token string API token. NOTE: Alternatively use the env variable CHAINLOOP_TOKEN
2908+
-y, --yes Skip confirmation
2909+
```
2910+
28732911
### chainloop policy help
28742912

28752913
Help about any command
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
//
2+
// Copyright 2025 The Chainloop Authors.
3+
//
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
//
8+
// http://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
16+
package action
17+
18+
import (
19+
"context"
20+
"fmt"
21+
"path/filepath"
22+
23+
"github.com/chainloop-dev/chainloop/app/cli/internal/policydevel"
24+
)
25+
26+
type PolicyLintOpts struct {
27+
PolicyPath string
28+
Format bool
29+
}
30+
31+
type PolicyLintResult struct {
32+
Valid bool
33+
Errors []string
34+
}
35+
36+
type PolicyLint struct {
37+
*ActionsOpts
38+
}
39+
40+
func NewPolicyLint(actionOpts *ActionsOpts) (*PolicyLint, error) {
41+
return &PolicyLint{
42+
ActionsOpts: actionOpts,
43+
}, nil
44+
}
45+
46+
func (action *PolicyLint) Run(_ context.Context, opts *PolicyLintOpts) (*PolicyLintResult, error) {
47+
// Resolve absolute path to policy directory
48+
absPath, err := filepath.Abs(opts.PolicyPath)
49+
if err != nil {
50+
return nil, fmt.Errorf("resolving absolute path: %w", err)
51+
}
52+
53+
// Read policies
54+
policy, err := policydevel.Lookup(absPath, opts.Format)
55+
if err != nil {
56+
return nil, fmt.Errorf("loading policy: %w", err)
57+
}
58+
59+
// Run all validations
60+
policy.Validate()
61+
62+
// Prepare result
63+
result := &PolicyLintResult{
64+
Valid: !policy.HasErrors(),
65+
Errors: make([]string, 0, len(policy.Errors)),
66+
}
67+
68+
// Convert validation errors to strings
69+
for _, err := range policy.Errors {
70+
result.Errors = append(result.Errors, err.Error())
71+
}
72+
73+
return result, nil
74+
}

0 commit comments

Comments
 (0)