-
Notifications
You must be signed in to change notification settings - Fork 26
/
linter.go
133 lines (119 loc) · 3.64 KB
/
linter.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
package linter
import (
"bytes"
"context"
_ "embed"
"encoding/json"
"fmt"
"log"
"os"
"github.com/buildkite/agent-stack-k8s/v2/internal/controller/scheduler"
"github.com/buildkite/go-buildkite/v3/buildkite"
"github.com/go-playground/validator/v10"
"github.com/spf13/cobra"
"github.com/xeipuuv/gojsonschema"
"sigs.k8s.io/yaml"
)
const (
pipelineSchema = "https://raw.githubusercontent.com/buildkite/pipeline-schema/main/schema.json"
k8sSchema = "https://kubernetesjsonschema.dev/master/_definitions.json"
)
//go:embed schema.json
var schema string
type Options struct {
File string `validate:"required,file"`
}
func (o *Options) AddFlags(cmd *cobra.Command) {
cmd.Flags().StringVarP(&o.File, "file", "f", "", "path to the pipeline file, or {-} for stdin")
}
func (o *Options) Validate() error {
return validator.New().Struct(o)
}
func New() *cobra.Command {
o := &Options{}
cmd := &cobra.Command{
Use: "lint",
Short: "A tool for linting Buildkite pipelines",
SilenceUsage: true,
RunE: func(cmd *cobra.Command, args []string) error {
if err := o.Validate(); err != nil {
return fmt.Errorf("failed to validate config: %w", err)
}
return Lint(cmd.Context(), o)
},
}
o.AddFlags(cmd)
return cmd
}
func Lint(ctx context.Context, options *Options) error {
contents, err := os.ReadFile(options.File)
if err != nil {
return fmt.Errorf("failed to open file: %w", err)
}
var pipeline buildkite.Pipeline
if err := yaml.Unmarshal(contents, &pipeline); err != nil {
return fmt.Errorf("failed to unmarshal pipeline: %w", err)
}
for _, step := range pipeline.Steps {
for name, plugin := range step.Plugins {
if name == "kubernetes" {
asJson, err := json.Marshal(plugin)
if err != nil {
return fmt.Errorf("failed to marshal plugin to json: %w", err)
}
var pluginConfig scheduler.KubernetesPlugin
decoder := json.NewDecoder(bytes.NewReader(asJson))
decoder.DisallowUnknownFields()
if err := decoder.Decode(&pluginConfig); err != nil {
return fmt.Errorf("failed to unmarshal Kubernetes plugin: %w", err)
}
for i, container := range pluginConfig.PodSpec.Containers {
if container.Name == "" {
container.Name = fmt.Sprintf("container-%d", i)
}
}
bs, err := json.Marshal(pluginConfig)
if err != nil {
return fmt.Errorf("failed to remarshal Kubernetes plugin: %w", err)
}
var plugin buildkite.Plugin
if err := json.Unmarshal(bs, &plugin); err != nil {
return fmt.Errorf(
"failed to unmarshal Kubernetes plugin back to buildkite plugin: %w",
err,
)
}
step.Plugins[name] = plugin
}
}
}
schemaLoader := gojsonschema.NewSchemaLoader()
if err := schemaLoader.AddSchema(pipelineSchema, gojsonschema.NewReferenceLoader(pipelineSchema)); err != nil {
return fmt.Errorf("failed to add pipeline schema: %w", err)
}
if err := schemaLoader.AddSchema(k8sSchema, gojsonschema.NewReferenceLoader(k8sSchema)); err != nil {
return fmt.Errorf("failed to add kubernetes schema: %w", err)
}
schema, err := schemaLoader.Compile(gojsonschema.NewStringLoader(schema))
if err != nil {
return fmt.Errorf("failed to compile schemas: %w", err)
}
bs, err := json.Marshal(pipeline)
if err != nil {
return fmt.Errorf("failed to marshal pipeline: %w", err)
}
documentLoader := gojsonschema.NewBytesLoader(bs)
result, err := schema.Validate(documentLoader)
if err != nil {
return fmt.Errorf("failed to validate: %w", err)
}
if result.Valid() {
log.Println("The document is valid")
} else {
log.Println("The document is not valid. see errors:")
for _, desc := range result.Errors() {
log.Printf("- %s", desc)
}
}
return nil
}