Skip to content

Commit cea1e93

Browse files
authored
Merge pull request #388 from dlorenc/tags
Add yamltags
2 parents 6ad057a + 76b368f commit cea1e93

File tree

7 files changed

+447
-14
lines changed

7 files changed

+447
-14
lines changed

examples/annotated-skaffold.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ build:
1010
gitCommit: {}
1111

1212
# Tag the image with the checksum of the built image (image id).
13-
sha256: {}
13+
# sha256: {}
1414

1515
# Tag the image with a configurable template string.
1616
# The template must be in the golang text/template syntax: https://golang.org/pkg/text/template/

pkg/skaffold/config/config_test.go

+19
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,20 @@ build:
4141
deploy:
4242
kubectl: {}
4343
`
44+
// This config has two tag policies set.
45+
invalidConfig = `
46+
apiVersion: skaffold/v1alpha2
47+
kind: Config
48+
build:
49+
tagPolicy:
50+
sha256: {}
51+
gitCommit: {}
52+
artifacts:
53+
- imageName: example
54+
deploy:
55+
name: example
56+
`
57+
4458
completeConfig = `
4559
apiVersion: skaffold/v1alpha2
4660
kind: Config
@@ -151,6 +165,11 @@ func TestParseConfig(t *testing.T) {
151165
config: badConfig,
152166
shouldErr: true,
153167
},
168+
{
169+
description: "two taggers defined",
170+
config: invalidConfig,
171+
shouldErr: true,
172+
},
154173
}
155174

156175
for _, test := range tests {

pkg/skaffold/config/util.go

+4
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import (
2222
"github.com/GoogleContainerTools/skaffold/pkg/skaffold/schema/util"
2323
"github.com/GoogleContainerTools/skaffold/pkg/skaffold/schema/v1alpha1"
2424
"github.com/GoogleContainerTools/skaffold/pkg/skaffold/schema/v1alpha2"
25+
"github.com/GoogleContainerTools/skaffold/pkg/skaffold/yamltags"
2526
)
2627

2728
// Versions is an ordered list of all schema versions.
@@ -45,6 +46,9 @@ func GetConfig(contents []byte, useDefault bool) (util.VersionedConfig, error) {
4546
err := cfg.Parse(contents, useDefault)
4647
if cfg.GetVersion() == version {
4748
// Versions are same hence propagate the parse error.
49+
if err := yamltags.ProcessStruct(cfg); err != nil {
50+
return nil, err
51+
}
4852
return cfg, err
4953
}
5054
}

pkg/skaffold/schema/util/util.go

+3-1
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,9 @@ type Config interface {
3232

3333
func IsOneOf(field reflect.StructField) bool {
3434
for _, tag := range strings.Split(field.Tag.Get("yamltags"), ",") {
35-
if tag == "oneOf" {
35+
tagParts := strings.Split(tag, "=")
36+
37+
if tagParts[0] == "oneOf" {
3638
return true
3739
}
3840
}

pkg/skaffold/schema/v1alpha2/config.go

+12-12
Original file line numberDiff line numberDiff line change
@@ -45,10 +45,10 @@ type BuildConfig struct {
4545

4646
// TagPolicy contains all the configuration for the tagging step
4747
type TagPolicy struct {
48-
GitTagger *GitTagger `yaml:"gitCommit" yamltags:"oneOf"`
49-
ShaTagger *ShaTagger `yaml:"sha256" yamltags:"oneOf"`
50-
EnvTemplateTagger *EnvTemplateTagger `yaml:"envTemplate" yamltags:"oneOf"`
51-
DateTimeTagger *DateTimeTagger `yaml:"dateTime" yamltags:"oneOf"`
48+
GitTagger *GitTagger `yaml:"gitCommit" yamltags:"oneOf=tag"`
49+
ShaTagger *ShaTagger `yaml:"sha256" yamltags:"oneOf=tag"`
50+
EnvTemplateTagger *EnvTemplateTagger `yaml:"envTemplate" yamltags:"oneOf=tag"`
51+
DateTimeTagger *DateTimeTagger `yaml:"dateTime" yamltags:"oneOf=tag"`
5252
}
5353

5454
// ShaTagger contains the configuration for the SHA tagger.
@@ -71,9 +71,9 @@ type DateTimeTagger struct {
7171
// BuildType contains the specific implementation and parameters needed
7272
// for the build step. Only one field should be populated.
7373
type BuildType struct {
74-
LocalBuild *LocalBuild `yaml:"local" yamltags:"oneOf"`
75-
GoogleCloudBuild *GoogleCloudBuild `yaml:"googleCloudBuild" yamltags:"oneOf"`
76-
KanikoBuild *KanikoBuild `yaml:"kaniko" yamltags:"oneOf"`
74+
LocalBuild *LocalBuild `yaml:"local" yamltags:"oneOf=build"`
75+
GoogleCloudBuild *GoogleCloudBuild `yaml:"googleCloudBuild" yamltags:"oneOf=build"`
76+
KanikoBuild *KanikoBuild `yaml:"kaniko" yamltags:"oneOf=build"`
7777
}
7878

7979
// LocalBuild contains the fields needed to do a build on the local docker daemon
@@ -112,9 +112,9 @@ type DeployConfig struct {
112112
// DeployType contains the specific implementation and parameters needed
113113
// for the deploy step. Only one field should be populated.
114114
type DeployType struct {
115-
HelmDeploy *HelmDeploy `yaml:"helm" yamltags:"oneOf"`
116-
KubectlDeploy *KubectlDeploy `yaml:"kubectl" yamltags:"oneOf"`
117-
KustomizeDeploy *KustomizeDeploy `yaml:"kustomize" yamltags:"oneOf"`
115+
HelmDeploy *HelmDeploy `yaml:"helm" yamltags:"oneOf=deploy"`
116+
KubectlDeploy *KubectlDeploy `yaml:"kubectl" yamltags:"oneOf=deploy"`
117+
KustomizeDeploy *KustomizeDeploy `yaml:"kustomize" yamltags:"oneOf=deploy"`
118118
}
119119

120120
// KubectlDeploy contains the configuration needed for deploying with `kubectl apply`
@@ -202,8 +202,8 @@ type Profile struct {
202202
}
203203

204204
type ArtifactType struct {
205-
DockerArtifact *DockerArtifact `yaml:"docker" yamltags:"oneOf"`
206-
BazelArtifact *BazelArtifact `yaml:"bazel" yamltags:"oneOf"`
205+
DockerArtifact *DockerArtifact `yaml:"docker" yamltags:"oneOf=artifact"`
206+
BazelArtifact *BazelArtifact `yaml:"bazel" yamltags:"oneOf=artifact"`
207207
}
208208

209209
type DockerArtifact struct {

pkg/skaffold/yamltags/tags.go

+189
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
/*
2+
Copyright 2018 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+
17+
package yamltags
18+
19+
import (
20+
"errors"
21+
"fmt"
22+
"reflect"
23+
"strconv"
24+
"strings"
25+
)
26+
27+
// ProcessStruct validates and processes the provided pointer to a struct.
28+
func ProcessStruct(s interface{}) error {
29+
parentStruct := reflect.ValueOf(s).Elem()
30+
t := parentStruct.Type()
31+
32+
// Loop through the fields on the struct, looking for tags.
33+
for i := 0; i < t.NumField(); i++ {
34+
f := t.Field(i)
35+
val := parentStruct.Field(i)
36+
field := parentStruct.Type().Field(i)
37+
if tags, ok := f.Tag.Lookup("yamltags"); ok {
38+
if err := ProcessTags(tags, val, parentStruct, field); err != nil {
39+
return err
40+
}
41+
}
42+
// Recurse down the struct
43+
if val.Kind() == reflect.Struct {
44+
if err := ProcessStruct(val.Addr().Interface()); err != nil {
45+
return err
46+
}
47+
}
48+
}
49+
return nil
50+
}
51+
52+
func ProcessTags(yamltags string, val reflect.Value, parentStruct reflect.Value, field reflect.StructField) error {
53+
tags := strings.Split(yamltags, ",")
54+
for _, tag := range tags {
55+
tagParts := strings.Split(tag, "=")
56+
var yt YamlTag
57+
switch tagParts[0] {
58+
case "required":
59+
yt = &RequiredTag{}
60+
case "default":
61+
yt = &DefaultTag{}
62+
case "oneOf":
63+
yt = &OneOfTag{
64+
Field: field,
65+
Parent: parentStruct,
66+
}
67+
}
68+
if err := yt.Load(tagParts); err != nil {
69+
return err
70+
}
71+
if err := yt.Process(val); err != nil {
72+
return err
73+
}
74+
}
75+
return nil
76+
}
77+
78+
type YamlTag interface {
79+
Load([]string) error
80+
Process(reflect.Value) error
81+
}
82+
83+
type RequiredTag struct {
84+
}
85+
86+
func (rt *RequiredTag) Load(s []string) error {
87+
return nil
88+
}
89+
90+
func (rt *RequiredTag) Process(val reflect.Value) error {
91+
if isZeroValue(val) {
92+
return errors.New("required value not set")
93+
}
94+
return nil
95+
}
96+
97+
type DefaultTag struct {
98+
dv string
99+
}
100+
101+
func (dt *DefaultTag) Load(s []string) error {
102+
if len(s) != 2 {
103+
return fmt.Errorf("invalid default tag: %v, expected key=value", s)
104+
}
105+
dt.dv = s[1]
106+
return nil
107+
}
108+
109+
func (dt *DefaultTag) Process(val reflect.Value) error {
110+
if !isZeroValue(val) {
111+
return nil
112+
}
113+
114+
switch val.Kind() {
115+
case reflect.Int, reflect.Int16, reflect.Int32, reflect.Int64:
116+
i, err := strconv.ParseInt(dt.dv, 0, 0)
117+
if err != nil {
118+
return err
119+
}
120+
val.SetInt(i)
121+
case reflect.String:
122+
val.SetString(dt.dv)
123+
}
124+
return nil
125+
}
126+
127+
// A program can have many structs, that each have many oneOfSets
128+
// each oneOfSet is a map of a set name to the list of fields that belong to that set
129+
// only one field in that list can have a non-zero value.
130+
131+
var allOneOfs map[string]map[string][]string
132+
133+
func getOneOfSetsForStruct(structName string) map[string][]string {
134+
_, ok := allOneOfs[structName]
135+
if !ok {
136+
allOneOfs[structName] = map[string][]string{}
137+
}
138+
return allOneOfs[structName]
139+
}
140+
141+
type OneOfTag struct {
142+
Field reflect.StructField
143+
Parent reflect.Value
144+
oneOfSets map[string][]string
145+
setName string
146+
}
147+
148+
func (oot *OneOfTag) Load(s []string) error {
149+
if len(s) != 2 {
150+
return fmt.Errorf("invalid default struct tag: %v, expected key=value", s)
151+
}
152+
oot.setName = s[1]
153+
154+
// Fetch the right oneOfSet for the struct.
155+
structName := oot.Parent.Type().Name()
156+
oot.oneOfSets = getOneOfSetsForStruct(structName)
157+
158+
// Add this field to the oneOfSet
159+
oot.oneOfSets[oot.setName] = append(oot.oneOfSets[oot.setName], oot.Field.Name)
160+
return nil
161+
}
162+
163+
func (oot *OneOfTag) Process(val reflect.Value) error {
164+
if isZeroValue(val) {
165+
return nil
166+
}
167+
168+
// This must exist because process is always called after Load.
169+
oneOfSet := oot.oneOfSets[oot.setName]
170+
for _, otherField := range oneOfSet {
171+
if otherField == oot.Field.Name {
172+
continue
173+
}
174+
field := oot.Parent.FieldByName(otherField)
175+
if !isZeroValue(field) {
176+
return fmt.Errorf("only one element in set %s can be set. got %s and %s", oot.setName, otherField, oot.Field.Name)
177+
}
178+
}
179+
return nil
180+
}
181+
182+
func isZeroValue(val reflect.Value) bool {
183+
zv := reflect.Zero(val.Type()).Interface()
184+
return reflect.DeepEqual(zv, val.Interface())
185+
}
186+
187+
func init() {
188+
allOneOfs = make(map[string]map[string][]string)
189+
}

0 commit comments

Comments
 (0)