Skip to content

Commit

Permalink
all: extendable externally usable API
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
zegl committed May 8, 2024
1 parent d9007f5 commit 79c943f
Show file tree
Hide file tree
Showing 19 changed files with 315 additions and 189 deletions.
20 changes: 11 additions & 9 deletions cmd/kube-score/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"github.com/zegl/kube-score/renderer/json_v2"
"github.com/zegl/kube-score/renderer/sarif"
"github.com/zegl/kube-score/score"
"github.com/zegl/kube-score/score/checks"
"github.com/zegl/kube-score/scorecard"
"golang.org/x/term"
)
Expand Down Expand Up @@ -174,7 +175,7 @@ Use "-" as filename to read from STDIN.`, execName(binName))

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

cnf := config.Configuration{
AllFiles: allFilePointers,
VerboseOutput: *verboseOutput,
runConfig := &config.RunConfiguration{
IgnoreContainerCpuLimitRequirement: *ignoreContainerCpuLimit,
IgnoreContainerMemoryLimitRequirement: *ignoreContainerMemoryLimit,
IgnoredTests: ignoredTests,
EnabledOptionalTests: enabledOptionalTests,
UseIgnoreChecksAnnotation: !*disableIgnoreChecksAnnotation,
UseOptionalChecksAnnotation: !*disableOptionalChecksAnnotation,
KubernetesVersion: kubeVer,
}

p, err := parser.New()
p, err := parser.New(&parser.Config{
VerboseOutput: *verboseOutput,
})
if err != nil {
return fmt.Errorf("failed to initializer parser: %w", err)
}

parsedFiles, err := p.ParseFiles(cnf)
parsedFiles, err := p.ParseFiles(allFilePointers)
if err != nil {
return fmt.Errorf("failed to parse files: %w", err)
}

scoreCard, err := score.Score(parsedFiles, cnf)
checks := score.RegisterAllChecks(parsedFiles, &checks.Config{IgnoredTests: ignoredTests}, runConfig)

scoreCard, err := score.Score(parsedFiles, checks, runConfig)
if err != nil {
return err
}
Expand Down Expand Up @@ -290,7 +292,7 @@ func listChecks(binName string, args []string) error {
return nil
}

allChecks := score.RegisterAllChecks(parser.Empty(), config.Configuration{})
allChecks := score.RegisterAllChecks(parser.Empty(), nil, nil)

output := csv.NewWriter(os.Stdout)
for _, c := range allChecks.All() {
Expand Down
7 changes: 1 addition & 6 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,11 @@ import (
"fmt"
"strconv"
"strings"

ks "github.com/zegl/kube-score/domain"
)

type Configuration struct {
AllFiles []ks.NamedReader
VerboseOutput int
type RunConfiguration struct {
IgnoreContainerCpuLimitRequirement bool
IgnoreContainerMemoryLimitRequirement bool
IgnoredTests map[string]struct{}
EnabledOptionalTests map[string]struct{}
UseIgnoreChecksAnnotation bool
UseOptionalChecksAnnotation bool
Expand Down
1 change: 1 addition & 0 deletions domain/kube-score.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package domain

import (
"io"

autoscalingv1 "k8s.io/api/autoscaling/v1"

appsv1 "k8s.io/api/apps/v1"
Expand Down
68 changes: 68 additions & 0 deletions examples/custom_check.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package examples

import (
"bytes"
"io"
"strings"

"github.com/zegl/kube-score/config"
"github.com/zegl/kube-score/domain"
"github.com/zegl/kube-score/parser"
"github.com/zegl/kube-score/score"
"github.com/zegl/kube-score/score/checks"
"github.com/zegl/kube-score/scorecard"

v1 "k8s.io/api/apps/v1"
)

type namedReader struct {
io.Reader
name string
}

func (n namedReader) Name() string {
return n.name
}

// ExampleCheckObject shows how kube-score can be extended with a custom check function
//
// In this example, raw is a YAML encoded Kubernetes object
func ExampleCheckObject(raw []byte) (*scorecard.Scorecard, error) {
parser, err := parser.New(nil)
if err != nil {
return nil, err
}

reader := bytes.NewReader(raw)

// Parse all objects to read
allObjects, err := parser.ParseFiles(
[]domain.NamedReader{
namedReader{
Reader: reader,
name: "input",
},
},
)
if err != nil {
return nil, err
}

// Register check functions to run
checks := checks.New(nil)
checks.RegisterDeploymentCheck("custom-deployment-check", "A custom kube-score check function", customDeploymentCheck)

return score.Score(allObjects, checks, &config.RunConfiguration{})
}

func customDeploymentCheck(d v1.Deployment) (scorecard.TestScore, error) {
if strings.Contains(d.Name, "foo") {
return scorecard.TestScore{
Grade: scorecard.GradeCritical,
Comments: []scorecard.TestScoreComment{{
Summary: "Deployments names can not contian 'foo'",
}}}, nil
}

return scorecard.TestScore{Grade: scorecard.GradeAllOK}, nil
}
64 changes: 64 additions & 0 deletions examples/custom_check_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package examples

import (
"testing"

"github.com/stretchr/testify/assert"
"github.com/zegl/kube-score/scorecard"
)

func TestExampleCheckObjectAllOK(t *testing.T) {
card, err := ExampleCheckObject([]byte(`
apiVersion: apps/v1
kind: Deployment
metadata:
name: example
spec:
replicas: 10
template:
metadata:
labels:
app: foo
spec:
containers:
- name: foobar
image: foo:bar`))

assert.NoError(t, err)

assert.Len(t, *card, 1)

for _, v := range *card {
assert.Len(t, v.Checks, 1)
assert.Equal(t, "custom-deployment-check", v.Checks[0].Check.ID)
assert.Equal(t, scorecard.GradeAllOK, v.Checks[0].Grade)
}
}

func TestExampleCheckObjectErrorNameContainsFoo(t *testing.T) {
card, err := ExampleCheckObject([]byte(`
apiVersion: apps/v1
kind: Deployment
metadata:
name: example-foo
spec:
replicas: 10
template:
metadata:
labels:
app: foo
spec:
containers:
- name: foobar
image: foo:bar`))

assert.NoError(t, err)

assert.Len(t, *card, 1)

for _, v := range *card {
assert.Len(t, v.Checks, 1)
assert.Equal(t, "custom-deployment-check", v.Checks[0].Check.ID)
assert.Equal(t, scorecard.GradeCritical, v.Checks[0].Grade)
}
}
29 changes: 19 additions & 10 deletions parser/parse.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ import (
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/runtime/serializer"

"github.com/zegl/kube-score/config"
ks "github.com/zegl/kube-score/domain"
"github.com/zegl/kube-score/parser/internal"
internalcronjob "github.com/zegl/kube-score/parser/internal/cronjob"
Expand All @@ -40,15 +39,25 @@ import (
type Parser struct {
scheme *runtime.Scheme
codecs serializer.CodecFactory
config *Config
}

type Config struct {
VerboseOutput int
}

type schemaAdderFunc func(scheme *runtime.Scheme) error

func New() (*Parser, error) {
func New(config *Config) (*Parser, error) {
if config == nil {
config = &Config{}
}

scheme := runtime.NewScheme()
p := &Parser{
scheme: scheme,
codecs: serializer.NewCodecFactory(scheme),
config: config,
}
if err := p.addToScheme(); err != nil {
return nil, fmt.Errorf("failed to init: %w", err)
Expand Down Expand Up @@ -146,10 +155,10 @@ func Empty() ks.AllTypes {
return &parsedObjects{}
}

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

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

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

func (p *Parser) detectAndDecode(cnf config.Configuration, s *parsedObjects, fileName string, fileOffset int, raw []byte) error {
func (p *Parser) detectAndDecode(s *parsedObjects, fileName string, fileOffset int, raw []byte) error {
var detect detectKind
err := yaml.Unmarshal(raw, &detect)
if err != nil {
Expand All @@ -198,15 +207,15 @@ func (p *Parser) detectAndDecode(cnf config.Configuration, s *parsedObjects, fil
return err
}
for _, listItem := range list.Items {
err := p.detectAndDecode(cnf, s, fileName, fileOffset, listItem.Raw)
err := p.detectAndDecode(s, fileName, fileOffset, listItem.Raw)
if err != nil {
return err
}
}
return nil
}

err = p.decodeItem(cnf, s, detectedVersion, fileName, fileOffset, raw)
err = p.decodeItem(s, detectedVersion, fileName, fileOffset, raw)
if err != nil {
return err
}
Expand Down Expand Up @@ -241,7 +250,7 @@ func detectFileLocation(fileName string, fileOffset int, fileContents []byte) ks
}
}

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

default:
if cnf.VerboseOutput > 1 {
if p.config.VerboseOutput > 1 {
log.Printf("Unknown datatype: %s", detectedVersion.String())
}
}
Expand Down
9 changes: 4 additions & 5 deletions parser/parse_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import (
"os"
"testing"

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

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

parser, err := New()
parser, err := New(nil)
assert.NoError(t, err)

for _, tc := range cases {
fp, err := os.Open(tc.fname)
assert.Nil(t, err)
_, err = parser.ParseFiles(config.Configuration{
AllFiles: []ks.NamedReader{fp},
})
_, err = parser.ParseFiles(
[]ks.NamedReader{fp},
)
if tc.expected == nil {
assert.Nil(t, err)
} else {
Expand Down
4 changes: 2 additions & 2 deletions renderer/sarif/sarif.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,10 +83,10 @@ func Output(input *scorecard.Scorecard) io.Reader {
Conversion: sarif.Conversion{
Tool: sarif.Tool{
Driver: sarif.Driver{
Name: "kube-score", },
Name: "kube-score"},
},
},
Results: results,
Results: results,
}
res := sarif.Sarif{
Runs: []sarif.Run{run},
Expand Down
6 changes: 2 additions & 4 deletions score/apps_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,20 +108,18 @@ func TestStatefulsetSelectorLabels(t *testing.T) {

func TestStatefulsetTemplateIgnores(t *testing.T) {
t.Parallel()
skipped := wasSkipped(t, config.Configuration{
skipped := wasSkipped(t, []ks.NamedReader{testFile("statefulset-nested-ignores.yaml")}, nil, &config.RunConfiguration{
UseIgnoreChecksAnnotation: true,
UseOptionalChecksAnnotation: true,
AllFiles: []ks.NamedReader{testFile("statefulset-nested-ignores.yaml")},
}, "Container Image Tag")
assert.True(t, skipped)
}

func TestStatefulsetTemplateIgnoresNotIgnoredWhenFlagDisabled(t *testing.T) {
t.Parallel()
skipped := wasSkipped(t, config.Configuration{
skipped := wasSkipped(t, []ks.NamedReader{testFile("statefulset-nested-ignores.yaml")}, nil, &config.RunConfiguration{
UseIgnoreChecksAnnotation: false,
UseOptionalChecksAnnotation: true,
AllFiles: []ks.NamedReader{testFile("statefulset-nested-ignores.yaml")},
}, "Container Image Tag")
assert.False(t, skipped)
}

0 comments on commit 79c943f

Please sign in to comment.