-
Notifications
You must be signed in to change notification settings - Fork 24
/
lint.go
209 lines (192 loc) · 5.92 KB
/
lint.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
package cmd
import (
"fmt"
"io/ioutil"
"log"
"os"
"path/filepath"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"github.com/jetstack/preflight/pkg/lint"
"github.com/jetstack/preflight/pkg/packagesources/local"
"github.com/jetstack/preflight/pkg/packaging"
"github.com/open-policy-agent/opa/ast"
"gopkg.in/yaml.v2"
)
var lintCmd = &cobra.Command{
Use: "lint",
Short: "Lint a Preflight package for errors",
Long: `The linter is designed for use in development and
continious integration environments.
The linter will check for common issues with packages:
- That a correctly structured policy-manifest.yaml file is present
- That at least one .rego file is present with OPA rules
- That at least one .rego test file is present
- That the rego files use the correct module name`,
Run: func(cmd *cobra.Command, args []string) {
if len(args) == 0 {
// Fail if given no input
log.Fatal("No packages provided for linting")
} else {
// Run lint on each path and collect errors
lintErrors := make([]lint.LintError, 0)
for _, packagePath := range args {
errors := LintPackage(packagePath)
lintErrors = append(lintErrors, errors...)
}
for _, le := range lintErrors {
// If verbose mode was enabled, print out full errors
// where available.
if le.Err == nil || !viper.GetBool("verbose") {
log.Printf("Lint: %s - %s", le.PackagePath, le.Lint)
} else {
log.Printf("Lint: %s - %s: %s", le.PackagePath, le.Lint, le.Err)
}
}
// Return a nonzero exit code if there were any lint errors at all
if len(lintErrors) > 0 {
log.Fatal("Encountered lint errors")
} else {
log.Printf("All packages passed linting :)")
}
}
},
}
// LintPackage performs linting checks on a Preflight
// package on-disk. Returning a list of lint issues as
// strings.
func LintPackage(packagePath string) []lint.LintError {
log.Printf("Linting package %s", packagePath)
lints := make([]lint.LintError, 0)
addLint := func(l string) {
lints = append(lints, lint.LintError{PackagePath: packagePath, Lint: l})
}
addLintE := func(l string, e error) {
lints = append(lints, lint.LintError{PackagePath: packagePath, Lint: l, Err: e})
}
// Try to open this directory
fi, err := os.Stat(packagePath)
if err != nil {
addLintE("Unable to read package path", err)
return lints
}
if !fi.IsDir() {
addLint("Package path is not a directory")
return lints
}
// Try to read the manifest
manifestPath := filepath.Join(packagePath, "policy-manifest.yaml")
fi, err = os.Stat(manifestPath)
if err != nil {
addLintE("Unable to read manifest path", err)
return lints
}
manifestBytes, err := ioutil.ReadFile(manifestPath)
if err != nil {
addLintE("Unable to open manifest for reading", err)
return lints
}
var manifest packaging.PolicyManifest
err = yaml.Unmarshal(manifestBytes, &manifest)
if err != nil {
addLintE("Unable to parse manifest YAML", err)
return lints
}
manifestLints := lint.LintPolicyManifest(manifest)
for i, _ := range manifestLints {
manifestLints[i].PackagePath = packagePath
}
lints = append(lints, manifestLints...)
// Look for rego and rego test files
files, err := ioutil.ReadDir(packagePath)
if err != nil {
addLintE("Unable to read directory", err)
return lints
}
policyFiles := make(map[string]string, 0)
testFiles := make(map[string]string, 0)
for _, fi := range files {
path := filepath.Join(packagePath, fi.Name())
if local.IsPolicyFile(fi) {
policyBytes, err := ioutil.ReadFile(path)
if err != nil {
addLintE("Unable to read Open Policy Agent policy (`.rego`) file", err)
} else {
policyFiles[fi.Name()] = string(policyBytes)
}
} else if local.IsPolicyTestFile(fi) {
testBytes, err := ioutil.ReadFile(path)
if err != nil {
addLintE("Unable to read Open Policy Agent policy test (`_test.rego`) file", err)
} else {
testFiles[fi.Name()] = string(testBytes)
}
}
}
if len(policyFiles) == 0 {
addLint("Unable to find any Open Policy Agent policy (`.rego`) files")
}
if len(testFiles) == 0 {
addLint("Unable to find any Open Policy Agent policy test (`_test.rego`) files")
}
// Parse the Rego files
compiler, err := ast.CompileModules(policyFiles)
if err != nil {
addLintE("Unable to compile Open Policy Agent policy", err)
} else {
// I want what you'd call a "set" in other languages.
rules := make(map[lint.RuleName]struct{}, 0)
for moduleName, module := range compiler.Modules {
log.Printf("Found module %s", moduleName)
for _, rule := range module.Rules {
ruleName, err := lint.NewRuleNameFromRego(string(rule.Head.Name))
if err == nil {
rules[*ruleName] = struct{}{}
}
}
}
// turn the map[string]struct{} into a []string of unique entries
rulesNames := make([]lint.RuleName, len(rules))
i := 0
for ruleName, _ := range rules {
rulesNames[i] = ruleName
i++
}
// Get the ones from the manifest
manifestRNs := lint.CollectManifestRuleNames(manifest)
// We have two sets and want the symetric difference. (In mathmatical terms)
// This method has terrible algorithmic complexity [O(n^2)], but n is at most 100 so who cares.
// Look for manifest RNs not in Rego
for _, mr := range manifestRNs {
var found = false
for _, rr := range rulesNames {
if mr == rr {
found = true
break
}
}
if !found {
addLint(fmt.Sprintf("Rule %s declared in Manifest, but not present in rego code", mr))
}
}
// Do the same in inverse, looking for rego RNs not in manifest
for _, rr := range rulesNames {
var found = false
for _, mr := range manifestRNs {
if mr == rr {
found = true
break
}
}
if !found {
addLint(fmt.Sprintf("Rule %s declared in rego code, but not present in Manifest", rr))
}
}
}
return lints
}
func init() {
packageCmd.AddCommand(lintCmd)
lintCmd.Flags().BoolP("verbose", "v", false, "Print full errors for any lint failures (if available)")
viper.BindPFlag("verbose", lintCmd.Flags().Lookup("verbose"))
}