Skip to content

Commit 903acf3

Browse files
authored
[v3] Add validator in render v2. (#5942)
* [v3] Add the Kptfile struct to render. There are two approaches to read Kptfile. One is via templating which we can add comments to help users understand the Kptfile. The other is via go struct which is more accurate and less error-prone. Considering the reliability of the Kpt deprecation policies (the api schema is not expected to change and if it is changed it will be backward compatible. If not backward compatible, the deprecation has a year-long period) * [v3] Add validator. Write the skaffold validation rule to Kptfile pipeline.
1 parent 8c0e186 commit 903acf3

File tree

4 files changed

+243
-23
lines changed

4 files changed

+243
-23
lines changed

pkg/skaffold/render/renderer/renderer.go

+44-7
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919
"context"
2020
"fmt"
2121
"io"
22+
"io/ioutil"
2223
"os"
2324
"os/exec"
2425
"path/filepath"
@@ -30,6 +31,7 @@ import (
3031
"github.com/GoogleContainerTools/skaffold/pkg/skaffold/kubernetes/manifest"
3132
"github.com/GoogleContainerTools/skaffold/pkg/skaffold/render/generate"
3233
"github.com/GoogleContainerTools/skaffold/pkg/skaffold/render/kptfile"
34+
"github.com/GoogleContainerTools/skaffold/pkg/skaffold/render/validate"
3335
latestV2 "github.com/GoogleContainerTools/skaffold/pkg/skaffold/schema/latest/v2"
3436
"github.com/GoogleContainerTools/skaffold/pkg/skaffold/util"
3537
)
@@ -44,17 +46,37 @@ type Renderer interface {
4446
}
4547

4648
// NewSkaffoldRenderer creates a new Renderer object from the latestV2 API schema.
47-
func NewSkaffoldRenderer(config *latestV2.RenderConfig, workingDir string) Renderer {
49+
func NewSkaffoldRenderer(config *latestV2.RenderConfig, workingDir string) (Renderer, error) {
4850
// TODO(yuwenma): return instance of kpt-managed mode or skaffold-managed mode defer to the config.Path fields.
4951
// The alpha implementation only has skaffold-managed mode.
5052
// TODO(yuwenma): The current work directory may not be accurate if users use --filepath flag.
5153
hydrationDir := filepath.Join(workingDir, DefaultHydrationDir)
52-
generator := generate.NewGenerator(workingDir, *config.Generate)
53-
return &SkaffoldRenderer{Generator: *generator, workingDir: workingDir, hydrationDir: hydrationDir}
54+
55+
var generator *generate.Generator
56+
if config.Generate == nil {
57+
// If render.generate is not given, default to current working directory.
58+
defaultManifests := filepath.Join(workingDir, "*.yaml")
59+
generator = generate.NewGenerator(workingDir, latestV2.Generate{Manifests: []string{defaultManifests}})
60+
} else {
61+
generator = generate.NewGenerator(workingDir, *config.Generate)
62+
}
63+
64+
var validator *validate.Validator
65+
if config.Validate != nil {
66+
var err error
67+
validator, err = validate.NewValidator(*config.Validate)
68+
if err != nil {
69+
return nil, err
70+
}
71+
} else {
72+
validator, _ = validate.NewValidator([]latestV2.Validator{})
73+
}
74+
return &SkaffoldRenderer{Generator: *generator, Validator: *validator, workingDir: workingDir, hydrationDir: hydrationDir}, nil
5475
}
5576

5677
type SkaffoldRenderer struct {
5778
generate.Generator
79+
validate.Validator
5880
workingDir string
5981
hydrationDir string
6082
labels map[string]string
@@ -68,14 +90,16 @@ func (r *SkaffoldRenderer) prepareHydrationDir(ctx context.Context) error {
6890
if _, err := os.Stat(r.hydrationDir); os.IsNotExist(err) {
6991
logrus.Debugf("creating render directory: %v", r.hydrationDir)
7092
if err := os.MkdirAll(r.hydrationDir, os.ModePerm); err != nil {
71-
return fmt.Errorf("creating cache directory for hydration: %w", err)
93+
return fmt.Errorf("creating render directory for hydration: %w", err)
7294
}
7395
}
7496
kptFilePath := filepath.Join(r.hydrationDir, kptfile.KptFileName)
7597
if _, err := os.Stat(kptFilePath); os.IsNotExist(err) {
7698
cmd := exec.CommandContext(ctx, "kpt", "pkg", "init", r.hydrationDir)
7799
if _, err := util.RunCmdOut(cmd); err != nil {
78-
return err
100+
// TODO: user error. need manual init
101+
return fmt.Errorf("unable to initialize kpt directory in %v, please manually run `kpt pkg init %v`",
102+
kptFilePath, kptFilePath)
79103
}
80104
}
81105
return nil
@@ -111,10 +135,23 @@ func (r *SkaffoldRenderer) Render(ctx context.Context, out io.Writer, builds []g
111135
defer file.Close()
112136
kfConfig := &kptfile.KptFile{}
113137
if err := yaml.NewDecoder(file).Decode(&kfConfig); err != nil {
114-
return err
138+
// TODO: user error.
139+
return fmt.Errorf("unable to parse %v: %w, please check if the kptfile is updated to new apiVersion > v1alpha2",
140+
kptFilePath, err)
141+
}
142+
if kfConfig.Pipeline == nil {
143+
kfConfig.Pipeline = &kptfile.Pipeline{}
115144
}
116145

117-
// TODO: Update the Kptfile with the new validators.
146+
kfConfig.Pipeline.Validators = r.GetDeclarativeValidators()
118147
// TODO: Update the Kptfile with the new mutators.
148+
149+
configByte, err := yaml.Marshal(kfConfig)
150+
if err != nil {
151+
return fmt.Errorf("unable to marshal Kptfile config %v", kfConfig)
152+
}
153+
if err = ioutil.WriteFile(kptFilePath, configByte, 0644); err != nil {
154+
return fmt.Errorf("unable to update %v", kptFilePath)
155+
}
119156
return nil
120157
}

pkg/skaffold/render/renderer/renderer_test.go

+90-16
Original file line numberDiff line numberDiff line change
@@ -57,21 +57,95 @@ metadata:
5757
`
5858
)
5959

60-
func TestRender_StoredInCache(t *testing.T) {
61-
testutil.Run(t, "", func(t *testutil.T) {
62-
r := NewSkaffoldRenderer(&latestV2.RenderConfig{Generate: &latestV2.Generate{
63-
Manifests: []string{"pod.yaml"}}}, "")
64-
fakeCmd := testutil.CmdRunOut(fmt.Sprintf("kpt pkg init %v", DefaultHydrationDir), "")
65-
t.Override(&util.DefaultExecCommand, fakeCmd)
66-
t.NewTempDir().
67-
Write("pod.yaml", podYaml).
68-
Write(filepath.Join(DefaultHydrationDir, kptfile.KptFileName), initKptfile).
69-
Touch("empty.ignored").
70-
Chdir()
60+
func TestRender(t *testing.T) {
61+
tests := []struct {
62+
description string
63+
renderConfig *latestV2.RenderConfig
64+
originalKptfile string
65+
updatedKptfile string
66+
}{
67+
{
68+
description: "single manifests, no hydration rule",
69+
renderConfig: &latestV2.RenderConfig{
70+
Generate: &latestV2.Generate{Manifests: []string{"pod.yaml"}},
71+
},
72+
originalKptfile: initKptfile,
73+
updatedKptfile: `apiVersion: kpt.dev/v1alpha2
74+
kind: Kptfile
75+
metadata:
76+
name: skaffold
77+
pipeline: {}
78+
`,
79+
},
80+
81+
{
82+
description: "manifests not given.",
83+
renderConfig: &latestV2.RenderConfig{},
84+
originalKptfile: initKptfile,
85+
updatedKptfile: `apiVersion: kpt.dev/v1alpha2
86+
kind: Kptfile
87+
metadata:
88+
name: skaffold
89+
pipeline: {}
90+
`,
91+
},
92+
{
93+
description: "single manifests with validation rule.",
94+
renderConfig: &latestV2.RenderConfig{
95+
Generate: &latestV2.Generate{Manifests: []string{"pod.yaml"}},
96+
Validate: &[]latestV2.Validator{{Name: "kubeval"}},
97+
},
98+
originalKptfile: initKptfile,
99+
updatedKptfile: `apiVersion: kpt.dev/v1alpha2
100+
kind: Kptfile
101+
metadata:
102+
name: skaffold
103+
pipeline:
104+
validators:
105+
- image: gcr.io/kpt-fn/kubeval:v0.1
106+
`,
107+
},
108+
{
109+
description: "Validation rule needs to be updated.",
110+
renderConfig: &latestV2.RenderConfig{
111+
Generate: &latestV2.Generate{Manifests: []string{"pod.yaml"}},
112+
Validate: &[]latestV2.Validator{{Name: "kubeval"}},
113+
},
114+
originalKptfile: `apiVersion: kpt.dev/v1alpha2
115+
kind: Kptfile
116+
metadata:
117+
name: skaffold
118+
pipeline:
119+
validators:
120+
- image: gcr.io/kpt-fn/SOME-OTHER-FUNC
121+
`,
122+
updatedKptfile: `apiVersion: kpt.dev/v1alpha2
123+
kind: Kptfile
124+
metadata:
125+
name: skaffold
126+
pipeline:
127+
validators:
128+
- image: gcr.io/kpt-fn/kubeval:v0.1
129+
`,
130+
},
131+
}
132+
for _, test := range tests {
133+
testutil.Run(t, test.description, func(t *testutil.T) {
134+
r, err := NewSkaffoldRenderer(test.renderConfig, "")
135+
t.CheckNoError(err)
136+
fakeCmd := testutil.CmdRunOut(fmt.Sprintf("kpt pkg init %v", DefaultHydrationDir), "")
137+
t.Override(&util.DefaultExecCommand, fakeCmd)
138+
t.NewTempDir().
139+
Write("pod.yaml", podYaml).
140+
Write(filepath.Join(DefaultHydrationDir, kptfile.KptFileName), test.originalKptfile).
141+
Touch("empty.ignored").
142+
Chdir()
71143

72-
var b bytes.Buffer
73-
err := r.Render(context.Background(), &b, []graph.Artifact{{ImageName: "leeroy-web", Tag: "leeroy-web:v1"}})
74-
t.CheckNoError(err)
75-
t.CheckFileExistAndContent(filepath.Join(DefaultHydrationDir, dryFileName), []byte(labeledPodYaml))
76-
})
144+
var b bytes.Buffer
145+
err = r.Render(context.Background(), &b, []graph.Artifact{{ImageName: "leeroy-web", Tag: "leeroy-web:v1"}})
146+
t.CheckNoError(err)
147+
t.CheckFileExistAndContent(filepath.Join(DefaultHydrationDir, dryFileName), []byte(labeledPodYaml))
148+
t.CheckFileExistAndContent(filepath.Join(DefaultHydrationDir, kptfile.KptFileName), []byte(test.updatedKptfile))
149+
})
150+
}
77151
}
+52
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/*
2+
Copyright 2021 The Skaffold 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 validate
17+
18+
import (
19+
"fmt"
20+
21+
"github.com/GoogleContainerTools/skaffold/pkg/skaffold/render/kptfile"
22+
latestV2 "github.com/GoogleContainerTools/skaffold/pkg/skaffold/schema/latest/v2"
23+
)
24+
25+
var validatorWhitelist = map[string]kptfile.Function{
26+
"kubeval": {Image: "gcr.io/kpt-fn/kubeval:v0.1"},
27+
// TODO: Add conftest validator in kpt catalog.
28+
}
29+
30+
// NewValidator instantiates a Validator object.
31+
func NewValidator(config []latestV2.Validator) (*Validator, error) {
32+
var fns []kptfile.Function
33+
for _, c := range config {
34+
fn, ok := validatorWhitelist[c.Name]
35+
if !ok {
36+
// TODO: kpt user error
37+
return nil, fmt.Errorf("unsupported validator %v", c.Name)
38+
}
39+
fns = append(fns, fn)
40+
}
41+
return &Validator{kptFn: fns}, nil
42+
}
43+
44+
type Validator struct {
45+
kptFn []kptfile.Function
46+
}
47+
48+
// GetDeclarativeValidators transforms and returns the skaffold validators defined in skaffold.yaml
49+
func (v *Validator) GetDeclarativeValidators() []kptfile.Function {
50+
// TODO: guarantee the v.kptFn is updated once users changed skaffold.yaml file.
51+
return v.kptFn
52+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/*
2+
Copyright 2021 The Skaffold 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 validate
17+
18+
import (
19+
"testing"
20+
21+
latestV2 "github.com/GoogleContainerTools/skaffold/pkg/skaffold/schema/latest/v2"
22+
"github.com/GoogleContainerTools/skaffold/testutil"
23+
)
24+
25+
func TestValidatorInit(t *testing.T) {
26+
tests := []struct {
27+
description string
28+
config []latestV2.Validator
29+
shouldErr bool
30+
}{
31+
{
32+
description: "no validation",
33+
config: []latestV2.Validator{},
34+
shouldErr: false,
35+
},
36+
{
37+
description: "kubeval validator",
38+
config: []latestV2.Validator{
39+
{Name: "kubeval"},
40+
},
41+
shouldErr: false,
42+
},
43+
{
44+
description: "invalid validator",
45+
config: []latestV2.Validator{
46+
{Name: "bad-validator"},
47+
},
48+
shouldErr: true,
49+
},
50+
}
51+
for _, test := range tests {
52+
testutil.Run(t, test.description, func(t *testutil.T) {
53+
_, err := NewValidator(test.config)
54+
t.CheckError(test.shouldErr, err)
55+
})
56+
}
57+
}

0 commit comments

Comments
 (0)