Skip to content

Commit 688e2d3

Browse files
committed
all: extendable externally usable API
Refactor/rewrite the Go API (not previously considered to be stable) to be extendable with custom checks written in Go, and to make the API easier to consume. The API is not yet considered to be stable, but this is a good step in that direction.
1 parent 71bf4dc commit 688e2d3

19 files changed

+315
-189
lines changed

cmd/kube-score/main.go

+11-9
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
"github.com/zegl/kube-score/renderer/json_v2"
2121
"github.com/zegl/kube-score/renderer/sarif"
2222
"github.com/zegl/kube-score/score"
23+
"github.com/zegl/kube-score/score/checks"
2324
"github.com/zegl/kube-score/scorecard"
2425
"golang.org/x/term"
2526
)
@@ -174,7 +175,7 @@ Use "-" as filename to read from STDIN.`, execName(binName))
174175

175176
if *allDefaultOptional {
176177
var addOptionalChecks []string
177-
for _, c := range score.RegisterAllChecks(parser.Empty(), config.Configuration{}).All() {
178+
for _, c := range score.RegisterAllChecks(parser.Empty(), nil, nil).All() {
178179
if c.Optional {
179180
addOptionalChecks = append(addOptionalChecks, c.ID)
180181
}
@@ -190,29 +191,30 @@ Use "-" as filename to read from STDIN.`, execName(binName))
190191
return errors.New("Invalid --kubernetes-version. Use on format \"vN.NN\"")
191192
}
192193

193-
cnf := config.Configuration{
194-
AllFiles: allFilePointers,
195-
VerboseOutput: *verboseOutput,
194+
runConfig := &config.RunConfiguration{
196195
IgnoreContainerCpuLimitRequirement: *ignoreContainerCpuLimit,
197196
IgnoreContainerMemoryLimitRequirement: *ignoreContainerMemoryLimit,
198-
IgnoredTests: ignoredTests,
199197
EnabledOptionalTests: enabledOptionalTests,
200198
UseIgnoreChecksAnnotation: !*disableIgnoreChecksAnnotation,
201199
UseOptionalChecksAnnotation: !*disableOptionalChecksAnnotation,
202200
KubernetesVersion: kubeVer,
203201
}
204202

205-
p, err := parser.New()
203+
p, err := parser.New(&parser.Config{
204+
VerboseOutput: *verboseOutput,
205+
})
206206
if err != nil {
207207
return fmt.Errorf("failed to initializer parser: %w", err)
208208
}
209209

210-
parsedFiles, err := p.ParseFiles(cnf)
210+
parsedFiles, err := p.ParseFiles(allFilePointers)
211211
if err != nil {
212212
return fmt.Errorf("failed to parse files: %w", err)
213213
}
214214

215-
scoreCard, err := score.Score(parsedFiles, cnf)
215+
checks := score.RegisterAllChecks(parsedFiles, &checks.Config{IgnoredTests: ignoredTests}, runConfig)
216+
217+
scoreCard, err := score.Score(parsedFiles, checks, runConfig)
216218
if err != nil {
217219
return err
218220
}
@@ -290,7 +292,7 @@ func listChecks(binName string, args []string) error {
290292
return nil
291293
}
292294

293-
allChecks := score.RegisterAllChecks(parser.Empty(), config.Configuration{})
295+
allChecks := score.RegisterAllChecks(parser.Empty(), nil, nil)
294296

295297
output := csv.NewWriter(os.Stdout)
296298
for _, c := range allChecks.All() {

config/config.go

+1-6
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,11 @@ import (
55
"fmt"
66
"strconv"
77
"strings"
8-
9-
ks "github.com/zegl/kube-score/domain"
108
)
119

12-
type Configuration struct {
13-
AllFiles []ks.NamedReader
14-
VerboseOutput int
10+
type RunConfiguration struct {
1511
IgnoreContainerCpuLimitRequirement bool
1612
IgnoreContainerMemoryLimitRequirement bool
17-
IgnoredTests map[string]struct{}
1813
EnabledOptionalTests map[string]struct{}
1914
UseIgnoreChecksAnnotation bool
2015
UseOptionalChecksAnnotation bool

domain/kube-score.go

+1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package domain
22

33
import (
44
"io"
5+
56
autoscalingv1 "k8s.io/api/autoscaling/v1"
67

78
appsv1 "k8s.io/api/apps/v1"

examples/custom_check.go

+68
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
package examples
2+
3+
import (
4+
"bytes"
5+
"io"
6+
"strings"
7+
8+
"github.com/zegl/kube-score/config"
9+
"github.com/zegl/kube-score/domain"
10+
"github.com/zegl/kube-score/parser"
11+
"github.com/zegl/kube-score/score"
12+
"github.com/zegl/kube-score/score/checks"
13+
"github.com/zegl/kube-score/scorecard"
14+
15+
v1 "k8s.io/api/apps/v1"
16+
)
17+
18+
type namedReader struct {
19+
io.Reader
20+
name string
21+
}
22+
23+
func (n namedReader) Name() string {
24+
return n.name
25+
}
26+
27+
// ExampleCheckObject shows how kube-score can be extended with a custom check function
28+
//
29+
// In this example, raw is a YAML encoded Kubernetes object
30+
func ExampleCheckObject(raw []byte) (*scorecard.Scorecard, error) {
31+
parser, err := parser.New(nil)
32+
if err != nil {
33+
return nil, err
34+
}
35+
36+
reader := bytes.NewReader(raw)
37+
38+
// Parse all objects to read
39+
allObjects, err := parser.ParseFiles(
40+
[]domain.NamedReader{
41+
namedReader{
42+
Reader: reader,
43+
name: "input",
44+
},
45+
},
46+
)
47+
if err != nil {
48+
return nil, err
49+
}
50+
51+
// Register check functions to run
52+
checks := checks.New(nil)
53+
checks.RegisterDeploymentCheck("custom-deployment-check", "A custom kube-score check function", customDeploymentCheck)
54+
55+
return score.Score(allObjects, checks, &config.RunConfiguration{})
56+
}
57+
58+
func customDeploymentCheck(d v1.Deployment) (scorecard.TestScore, error) {
59+
if strings.Contains(d.Name, "foo") {
60+
return scorecard.TestScore{
61+
Grade: scorecard.GradeCritical,
62+
Comments: []scorecard.TestScoreComment{{
63+
Summary: "Deployments names can not contian 'foo'",
64+
}}}, nil
65+
}
66+
67+
return scorecard.TestScore{Grade: scorecard.GradeAllOK}, nil
68+
}

examples/custom_check_test.go

+64
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
package examples
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/assert"
7+
"github.com/zegl/kube-score/scorecard"
8+
)
9+
10+
func TestExampleCheckObjectAllOK(t *testing.T) {
11+
card, err := ExampleCheckObject([]byte(`
12+
apiVersion: apps/v1
13+
kind: Deployment
14+
metadata:
15+
name: example
16+
spec:
17+
replicas: 10
18+
template:
19+
metadata:
20+
labels:
21+
app: foo
22+
spec:
23+
containers:
24+
- name: foobar
25+
image: foo:bar`))
26+
27+
assert.NoError(t, err)
28+
29+
assert.Len(t, *card, 1)
30+
31+
for _, v := range *card {
32+
assert.Len(t, v.Checks, 1)
33+
assert.Equal(t, "custom-deployment-check", v.Checks[0].Check.ID)
34+
assert.Equal(t, scorecard.GradeAllOK, v.Checks[0].Grade)
35+
}
36+
}
37+
38+
func TestExampleCheckObjectErrorNameContainsFoo(t *testing.T) {
39+
card, err := ExampleCheckObject([]byte(`
40+
apiVersion: apps/v1
41+
kind: Deployment
42+
metadata:
43+
name: example-foo
44+
spec:
45+
replicas: 10
46+
template:
47+
metadata:
48+
labels:
49+
app: foo
50+
spec:
51+
containers:
52+
- name: foobar
53+
image: foo:bar`))
54+
55+
assert.NoError(t, err)
56+
57+
assert.Len(t, *card, 1)
58+
59+
for _, v := range *card {
60+
assert.Len(t, v.Checks, 1)
61+
assert.Equal(t, "custom-deployment-check", v.Checks[0].Check.ID)
62+
assert.Equal(t, scorecard.GradeCritical, v.Checks[0].Grade)
63+
}
64+
}

parser/parse.go

+19-10
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@ import (
2727
"k8s.io/apimachinery/pkg/runtime/schema"
2828
"k8s.io/apimachinery/pkg/runtime/serializer"
2929

30-
"github.com/zegl/kube-score/config"
3130
ks "github.com/zegl/kube-score/domain"
3231
"github.com/zegl/kube-score/parser/internal"
3332
internalcronjob "github.com/zegl/kube-score/parser/internal/cronjob"
@@ -40,15 +39,25 @@ import (
4039
type Parser struct {
4140
scheme *runtime.Scheme
4241
codecs serializer.CodecFactory
42+
config *Config
43+
}
44+
45+
type Config struct {
46+
VerboseOutput int
4347
}
4448

4549
type schemaAdderFunc func(scheme *runtime.Scheme) error
4650

47-
func New() (*Parser, error) {
51+
func New(config *Config) (*Parser, error) {
52+
if config == nil {
53+
config = &Config{}
54+
}
55+
4856
scheme := runtime.NewScheme()
4957
p := &Parser{
5058
scheme: scheme,
5159
codecs: serializer.NewCodecFactory(scheme),
60+
config: config,
5261
}
5362
if err := p.addToScheme(); err != nil {
5463
return nil, fmt.Errorf("failed to init: %w", err)
@@ -146,10 +155,10 @@ func Empty() ks.AllTypes {
146155
return &parsedObjects{}
147156
}
148157

149-
func (p *Parser) ParseFiles(cnf config.Configuration) (ks.AllTypes, error) {
158+
func (p *Parser) ParseFiles(files []ks.NamedReader) (ks.AllTypes, error) {
150159
s := &parsedObjects{}
151160

152-
for _, namedReader := range cnf.AllFiles {
161+
for _, namedReader := range files {
153162
fullFile, err := io.ReadAll(namedReader)
154163
if err != nil {
155164
return nil, err
@@ -169,7 +178,7 @@ func (p *Parser) ParseFiles(cnf config.Configuration) (ks.AllTypes, error) {
169178
for _, fileContents := range bytes.Split(fullFile, []byte("\n---\n")) {
170179

171180
if len(bytes.TrimSpace(fileContents)) > 0 {
172-
if err := p.detectAndDecode(cnf, s, namedReader.Name(), offset, fileContents); err != nil {
181+
if err := p.detectAndDecode(s, namedReader.Name(), offset, fileContents); err != nil {
173182
return nil, err
174183
}
175184
}
@@ -181,7 +190,7 @@ func (p *Parser) ParseFiles(cnf config.Configuration) (ks.AllTypes, error) {
181190
return s, nil
182191
}
183192

184-
func (p *Parser) detectAndDecode(cnf config.Configuration, s *parsedObjects, fileName string, fileOffset int, raw []byte) error {
193+
func (p *Parser) detectAndDecode(s *parsedObjects, fileName string, fileOffset int, raw []byte) error {
185194
var detect detectKind
186195
err := yaml.Unmarshal(raw, &detect)
187196
if err != nil {
@@ -198,15 +207,15 @@ func (p *Parser) detectAndDecode(cnf config.Configuration, s *parsedObjects, fil
198207
return err
199208
}
200209
for _, listItem := range list.Items {
201-
err := p.detectAndDecode(cnf, s, fileName, fileOffset, listItem.Raw)
210+
err := p.detectAndDecode(s, fileName, fileOffset, listItem.Raw)
202211
if err != nil {
203212
return err
204213
}
205214
}
206215
return nil
207216
}
208217

209-
err = p.decodeItem(cnf, s, detectedVersion, fileName, fileOffset, raw)
218+
err = p.decodeItem(s, detectedVersion, fileName, fileOffset, raw)
210219
if err != nil {
211220
return err
212221
}
@@ -241,7 +250,7 @@ func detectFileLocation(fileName string, fileOffset int, fileContents []byte) ks
241250
}
242251
}
243252

244-
func (p *Parser) decodeItem(cnf config.Configuration, s *parsedObjects, detectedVersion schema.GroupVersionKind, fileName string, fileOffset int, fileContents []byte) error {
253+
func (p *Parser) decodeItem(s *parsedObjects, detectedVersion schema.GroupVersionKind, fileName string, fileOffset int, fileContents []byte) error {
245254
addPodSpeccer := func(ps ks.PodSpecer) {
246255
s.podspecers = append(s.podspecers, ps)
247256
s.bothMetas = append(s.bothMetas, ks.BothMeta{
@@ -418,7 +427,7 @@ func (p *Parser) decodeItem(cnf config.Configuration, s *parsedObjects, detected
418427
s.bothMetas = append(s.bothMetas, ks.BothMeta{TypeMeta: hpa.TypeMeta, ObjectMeta: hpa.ObjectMeta, FileLocationer: h})
419428

420429
default:
421-
if cnf.VerboseOutput > 1 {
430+
if p.config.VerboseOutput > 1 {
422431
log.Printf("Unknown datatype: %s", detectedVersion.String())
423432
}
424433
}

parser/parse_test.go

+4-5
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import (
55
"os"
66
"testing"
77

8-
"github.com/zegl/kube-score/config"
98
ks "github.com/zegl/kube-score/domain"
109

1110
"github.com/stretchr/testify/assert"
@@ -25,15 +24,15 @@ func TestParse(t *testing.T) {
2524
},
2625
}
2726

28-
parser, err := New()
27+
parser, err := New(nil)
2928
assert.NoError(t, err)
3029

3130
for _, tc := range cases {
3231
fp, err := os.Open(tc.fname)
3332
assert.Nil(t, err)
34-
_, err = parser.ParseFiles(config.Configuration{
35-
AllFiles: []ks.NamedReader{fp},
36-
})
33+
_, err = parser.ParseFiles(
34+
[]ks.NamedReader{fp},
35+
)
3736
if tc.expected == nil {
3837
assert.Nil(t, err)
3938
} else {

renderer/sarif/sarif.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -83,10 +83,10 @@ func Output(input *scorecard.Scorecard) io.Reader {
8383
Conversion: sarif.Conversion{
8484
Tool: sarif.Tool{
8585
Driver: sarif.Driver{
86-
Name: "kube-score", },
86+
Name: "kube-score"},
8787
},
8888
},
89-
Results: results,
89+
Results: results,
9090
}
9191
res := sarif.Sarif{
9292
Runs: []sarif.Run{run},

score/apps_test.go

+2-4
Original file line numberDiff line numberDiff line change
@@ -108,20 +108,18 @@ func TestStatefulsetSelectorLabels(t *testing.T) {
108108

109109
func TestStatefulsetTemplateIgnores(t *testing.T) {
110110
t.Parallel()
111-
skipped := wasSkipped(t, config.Configuration{
111+
skipped := wasSkipped(t, []ks.NamedReader{testFile("statefulset-nested-ignores.yaml")}, nil, &config.RunConfiguration{
112112
UseIgnoreChecksAnnotation: true,
113113
UseOptionalChecksAnnotation: true,
114-
AllFiles: []ks.NamedReader{testFile("statefulset-nested-ignores.yaml")},
115114
}, "Container Image Tag")
116115
assert.True(t, skipped)
117116
}
118117

119118
func TestStatefulsetTemplateIgnoresNotIgnoredWhenFlagDisabled(t *testing.T) {
120119
t.Parallel()
121-
skipped := wasSkipped(t, config.Configuration{
120+
skipped := wasSkipped(t, []ks.NamedReader{testFile("statefulset-nested-ignores.yaml")}, nil, &config.RunConfiguration{
122121
UseIgnoreChecksAnnotation: false,
123122
UseOptionalChecksAnnotation: true,
124-
AllFiles: []ks.NamedReader{testFile("statefulset-nested-ignores.yaml")},
125123
}, "Container Image Tag")
126124
assert.False(t, skipped)
127125
}

0 commit comments

Comments
 (0)