Skip to content

Commit 52ea51a

Browse files
authored
Merge branch 'master' into debug-iterations
2 parents 6eb1581 + 0ba3ee7 commit 52ea51a

File tree

21 files changed

+770
-56
lines changed

21 files changed

+770
-56
lines changed

DEVELOPMENT.md

+52
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,58 @@ For more details behind the logic of config changes see [the Skaffold config man
103103

104104
We build the API directly through gRPC, which gets translated into REST API through a reverse proxy gateway library. If you make changes to the [proto/skaffold.proto](https://github.com/GoogleContainerTools/skaffold/blob/master/proto/skaffold.proto) file you can run `./hack/generate-proto.sh` to generate the equivalent Go code.
105105

106+
## Adding actionable error messages to code.
107+
Skaffold has a built-in framework to provide actionable error messages for user to help bootstrap skaffold errors.
108+
109+
Also, [v1.19.0](https://github.com/GoogleContainerTools/skaffold/releases/tag/v1.19.0) onwards, skaffold is collecting failure error codes to help the team get more insights into common failure scenarios.
110+
111+
To take advantage of this framework, contributors can simply use the [`ErrDef`](https://github.com/GoogleContainerTools/skaffold/blob/master/pkg/skaffold/errors/err_def.go#L32) struct to throw meaningful actionable error messages and
112+
improve user experience.
113+
114+
e.g In this example [PR](https://github.com/GoogleContainerTools/skaffold/pull/5273),
115+
1. The contributor created 3 distinct error codes in [skaffold.proto](https://github.com/GoogleContainerTools/skaffold/pull/5088/files#diff-3883fe4549a47ae73a7a3a0afc00896b197d5ba8570906ba423769cf5a93a26f)
116+
```
117+
// Docker build error when listing containers.
118+
INIT_DOCKER_NETWORK_LISTING_CONTAINERS = 122;
119+
// Docker build error indicating an invalid container name (or id).
120+
INIT_DOCKER_NETWORK_INVALID_CONTAINER_NAME = 123;
121+
// Docker build error indicating the container referenced does not exists in the docker context used.
122+
INIT_DOCKER_NETWORK_CONTAINER_DOES_NOT_EXIST = 124
123+
```
124+
The `INIT` in this case stands for skaffold `INIT` phase which includes, parsing of skaffold config and creating a skaffold runner.
125+
The other valid phases are `BUILD`, `DEPLOY`, `STATUSCHECK`. Complete list [here](https://skaffold.dev/docs/references/api/grpc/#statuscode)
126+
2. Run `hack/generate_proto.sh`. These will generate go code and structs for the newly added proto fields.
127+
```shell script
128+
git status
129+
modified: docs/content/en/api/skaffold.swagger.json
130+
modified: docs/content/en/docs/references/api/grpc.md
131+
modified: proto/skaffold.pb.go
132+
modified: proto/skaffold.proto
133+
```
134+
3. The contributor then used these error codes when creating an error in their [proposed code change](https://github.com/GoogleContainerTools/skaffold/pull/5088/files#diff-3fc5246574bf7367a232c6d682b22a4e22795d52eb1c81fe2c27ff052939d507R220).
135+
They used the constructor `sErrors.NewError` in [pkg/skaffold/errors](https://github.com/GoogleContainerTools/skaffold/blob/54466ff6983e9fcf977d6e549119b4c1c4dc9e2b/pkg/skaffold/errors/err_def.go#L57) to inantiate an object of struct `ErrDef`.
136+
`ErrDef` implements the golang `error` interface.
137+
```
138+
err := sErrors.NewError(fmt.Errorf(errMsg),
139+
proto.ActionableErr{
140+
Message: errMsg,
141+
ErrCode: proto.StatusCode_INIT_DOCKER_NETWORK_INVALID_CONTAINER_NAME,
142+
Suggestions: []*proto.Suggestion{
143+
{
144+
SuggestionCode: proto.SuggestionCode_FIX_DOCKER_NETWORK_CONTAINER_NAME,
145+
Action: "Please fix the docker network container name and try again",
146+
},
147+
},
148+
}))
149+
```
150+
151+
With above two changes, skaffold will now show a meaning full error message when this error condition is met.
152+
```shell script
153+
skaffold dev
154+
invalid skaffold config: container 'does-not-exits' not found, required by image 'skaffold-example' for docker network stack sharing.
155+
Please fix the docker network container name and try again.
156+
```
157+
106158
## Building skaffold
107159

108160
To build with your local changes you have two options:

cmd/skaffold/app/cmd/flags.go

+8
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,14 @@ var flagRegistry = []Flag{
120120
FlagAddMethod: "StringVar",
121121
DefinedOn: []string{"dev", "build", "run", "debug"},
122122
},
123+
{
124+
Name: "remote-cache-dir",
125+
Usage: "Specify the location of the git repositories cache (default $HOME/.skaffold/repos)",
126+
Value: &opts.RepoCacheDir,
127+
DefValue: "",
128+
FlagAddMethod: "StringVar",
129+
DefinedOn: []string{"all"},
130+
},
123131
{
124132
Name: "insecure-registry",
125133
Usage: "Target registries for built images which are not secure",

cmd/skaffold/app/cmd/parse_config.go

+58-11
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,10 @@ import (
2424
"sort"
2525
"strings"
2626

27+
"github.com/sirupsen/logrus"
28+
2729
"github.com/GoogleContainerTools/skaffold/pkg/skaffold/config"
30+
"github.com/GoogleContainerTools/skaffold/pkg/skaffold/git"
2831
"github.com/GoogleContainerTools/skaffold/pkg/skaffold/schema"
2932
"github.com/GoogleContainerTools/skaffold/pkg/skaffold/schema/defaults"
3033
"github.com/GoogleContainerTools/skaffold/pkg/skaffold/schema/latest"
@@ -45,9 +48,20 @@ type configOpts struct {
4548
isDependency bool
4649
}
4750

51+
// record captures the state of referenced configs.
52+
type record struct {
53+
appliedProfiles map[string]string // config -> list of applied profiles
54+
configNameToFile map[string]string // configName -> file path
55+
cachedRepos map[string]interface{} // git repo -> cache path or error
56+
}
57+
58+
func newRecord() *record {
59+
return &record{appliedProfiles: make(map[string]string), configNameToFile: make(map[string]string), cachedRepos: make(map[string]interface{})}
60+
}
61+
4862
func getAllConfigs(opts config.SkaffoldOptions) ([]*latest.SkaffoldConfig, error) {
4963
cOpts := configOpts{file: opts.ConfigurationFile, selection: nil, profiles: opts.Profiles, isRequired: false, isDependency: false}
50-
cfgs, err := getConfigs(cOpts, opts, make(map[string]string), make(map[string]string))
64+
cfgs, err := getConfigs(cOpts, opts, newRecord())
5165
if err != nil {
5266
return nil, err
5367
}
@@ -61,7 +75,7 @@ func getAllConfigs(opts config.SkaffoldOptions) ([]*latest.SkaffoldConfig, error
6175
}
6276

6377
// getConfigs recursively parses all configs and their dependencies in the specified `skaffold.yaml`
64-
func getConfigs(cfgOpts configOpts, opts config.SkaffoldOptions, appliedProfiles map[string]string, configNameToFile map[string]string) ([]*latest.SkaffoldConfig, error) {
78+
func getConfigs(cfgOpts configOpts, opts config.SkaffoldOptions, r *record) ([]*latest.SkaffoldConfig, error) {
6579
parsed, err := schema.ParseConfigAndUpgrade(cfgOpts.file, latest.Version)
6680
if err != nil {
6781
return nil, err
@@ -80,7 +94,7 @@ func getConfigs(cfgOpts configOpts, opts config.SkaffoldOptions, appliedProfiles
8094
var configs []*latest.SkaffoldConfig
8195
for i, cfg := range parsed {
8296
config := cfg.(*latest.SkaffoldConfig)
83-
processed, err := processEachConfig(config, cfgOpts, opts, appliedProfiles, configNameToFile, i)
97+
processed, err := processEachConfig(config, cfgOpts, opts, r, i)
8498
if err != nil {
8599
return nil, err
86100
}
@@ -90,14 +104,14 @@ func getConfigs(cfgOpts configOpts, opts config.SkaffoldOptions, appliedProfiles
90104
}
91105

92106
// processEachConfig processes each parsed config by applying profiles and recursively processing it's dependencies
93-
func processEachConfig(config *latest.SkaffoldConfig, cfgOpts configOpts, opts config.SkaffoldOptions, appliedProfiles map[string]string, configNameToFile map[string]string, index int) ([]*latest.SkaffoldConfig, error) {
107+
func processEachConfig(config *latest.SkaffoldConfig, cfgOpts configOpts, opts config.SkaffoldOptions, r *record, index int) ([]*latest.SkaffoldConfig, error) {
94108
// check that the same config name isn't repeated in multiple files.
95109
if config.Metadata.Name != "" {
96-
prevConfig, found := configNameToFile[config.Metadata.Name]
110+
prevConfig, found := r.configNameToFile[config.Metadata.Name]
97111
if found && prevConfig != cfgOpts.file {
98112
return nil, fmt.Errorf("skaffold config named %q found in multiple files: %q and %q", config.Metadata.Name, prevConfig, cfgOpts.file)
99113
}
100-
configNameToFile[config.Metadata.Name] = cfgOpts.file
114+
r.configNameToFile[config.Metadata.Name] = cfgOpts.file
101115
}
102116

103117
// configSelection specifies the exact required configs in this file. Empty configSelection means that all configs are required.
@@ -124,13 +138,13 @@ func processEachConfig(config *latest.SkaffoldConfig, cfgOpts configOpts, opts c
124138
}
125139

126140
sort.Strings(profiles)
127-
if revisit, err := checkRevisit(config, profiles, appliedProfiles, cfgOpts.file, required, index); revisit {
141+
if revisit, err := checkRevisit(config, profiles, r.appliedProfiles, cfgOpts.file, required, index); revisit {
128142
return nil, err
129143
}
130144

131145
var configs []*latest.SkaffoldConfig
132146
for _, d := range config.Dependencies {
133-
depConfigs, err := processEachDependency(d, cfgOpts.file, required, profiles, opts, appliedProfiles, configNameToFile)
147+
depConfigs, err := processEachDependency(d, cfgOpts.file, required, profiles, opts, r)
134148
if err != nil {
135149
return nil, err
136150
}
@@ -144,7 +158,7 @@ func processEachConfig(config *latest.SkaffoldConfig, cfgOpts configOpts, opts c
144158
}
145159

146160
// processEachDependency parses a config dependency with the calculated set of activated profiles.
147-
func processEachDependency(d latest.ConfigDependency, fPath string, required bool, profiles []string, opts config.SkaffoldOptions, appliedProfiles, configNameToFile map[string]string) ([]*latest.SkaffoldConfig, error) {
161+
func processEachDependency(d latest.ConfigDependency, fPath string, required bool, profiles []string, opts config.SkaffoldOptions, r *record) ([]*latest.SkaffoldConfig, error) {
148162
var depProfiles []string
149163
for _, ap := range d.ActiveProfiles {
150164
if len(ap.ActivatedBy) == 0 {
@@ -159,27 +173,60 @@ func processEachDependency(d latest.ConfigDependency, fPath string, required boo
159173
}
160174
}
161175
path := d.Path
176+
177+
if d.GitRepo != nil {
178+
cachePath, err := cacheRepo(*d.GitRepo, opts, r)
179+
if err != nil {
180+
return nil, fmt.Errorf("caching remote dependency %s: %w", d.GitRepo.Repo, err)
181+
}
182+
path = cachePath
183+
}
184+
162185
if path == "" {
163186
// empty path means configs in the same file
164187
path = fPath
165188
}
166189
fi, err := os.Stat(path)
167190
if err != nil {
168191
if os.IsNotExist(errors.Unwrap(err)) {
169-
return nil, fmt.Errorf("could not find skaffold config %s that is referenced as a dependency in config %s", d.Path, fPath)
192+
return nil, fmt.Errorf("could not find skaffold config %s that is referenced as a dependency in config %s", path, fPath)
170193
}
171194
return nil, fmt.Errorf("parsing dependencies for skaffold config %s: %w", fPath, err)
172195
}
173196
if fi.IsDir() {
174197
path = filepath.Join(path, "skaffold.yaml")
175198
}
176-
depConfigs, err := getConfigs(configOpts{file: path, selection: d.Names, profiles: depProfiles, isRequired: required, isDependency: path != fPath}, opts, appliedProfiles, configNameToFile)
199+
depConfigs, err := getConfigs(configOpts{file: path, selection: d.Names, profiles: depProfiles, isRequired: required, isDependency: path != fPath}, opts, r)
177200
if err != nil {
178201
return nil, err
179202
}
180203
return depConfigs, nil
181204
}
182205

206+
// cacheRepo downloads the referenced git repository to skaffold's cache if required and returns the path to the target configuration file in that repository.
207+
func cacheRepo(g latest.GitInfo, opts config.SkaffoldOptions, r *record) (string, error) {
208+
key := fmt.Sprintf("%s@%s", g.Repo, g.Ref)
209+
if p, found := r.cachedRepos[key]; found {
210+
switch v := p.(type) {
211+
case string:
212+
return filepath.Join(v, g.Path), nil
213+
case error:
214+
return "", v
215+
default:
216+
logrus.Fatalf("unable to check download status of repo %s at ref %s", g.Repo, g.Ref)
217+
return "", nil
218+
}
219+
} else {
220+
p, err := git.SyncRepo(g, opts)
221+
if err != nil {
222+
r.cachedRepos[key] = err
223+
return "", err
224+
}
225+
r.cachedRepos[key] = p
226+
return filepath.Join(p, g.Path), nil
227+
}
228+
}
229+
183230
// checkRevisit ensures that each config is activated with the same set of active profiles
184231
// It returns true if this config was visited once before. It additionally returns an error if the previous visit was with a different set of active profiles.
185232
func checkRevisit(config *latest.SkaffoldConfig, profiles []string, appliedProfiles map[string]string, file string, required bool, index int) (bool, error) {

cmd/skaffold/app/cmd/parse_config_test.go

+38-3
Original file line numberDiff line numberDiff line change
@@ -26,14 +26,15 @@ import (
2626
"github.com/google/go-cmp/cmp"
2727

2828
"github.com/GoogleContainerTools/skaffold/pkg/skaffold/config"
29+
"github.com/GoogleContainerTools/skaffold/pkg/skaffold/git"
2930
"github.com/GoogleContainerTools/skaffold/pkg/skaffold/schema/latest"
3031
"github.com/GoogleContainerTools/skaffold/pkg/skaffold/util"
3132
"github.com/GoogleContainerTools/skaffold/testutil"
3233
)
3334

3435
const (
3536
template = `
36-
apiVersion: skaffold/v2beta11
37+
apiVersion: %s
3738
kind: Config
3839
metadata:
3940
name: %s
@@ -329,6 +330,37 @@ requires:
329330
},
330331
err: errors.New("did not find any configs matching selection [cfg3]"),
331332
},
333+
{
334+
description: "remote dependencies",
335+
documents: []document{
336+
{path: "skaffold.yaml", configs: []mockCfg{{name: "cfg00", requiresStanza: `
337+
requires:
338+
- path: doc1
339+
`}, {name: "cfg01", requiresStanza: ""}}},
340+
{path: "doc1/skaffold.yaml", configs: []mockCfg{{name: "cfg10", requiresStanza: `
341+
requires:
342+
- git:
343+
repo: doc2
344+
path: skaffold.yaml
345+
ref: main
346+
configs: [cfg21]
347+
`}, {name: "cfg11", requiresStanza: `
348+
requires:
349+
- git:
350+
repo: doc2
351+
ref: main
352+
configs: [cfg21]
353+
`}}},
354+
{path: "doc2/skaffold.yaml", configs: []mockCfg{{name: "cfg20", requiresStanza: ""}, {name: "cfg21", requiresStanza: ""}}},
355+
},
356+
expected: []*latest.SkaffoldConfig{
357+
createCfg("cfg21", "image21", "doc2", nil),
358+
createCfg("cfg10", "image10", "doc1", []latest.ConfigDependency{{GitRepo: &latest.GitInfo{Repo: "doc2", Path: "skaffold.yaml", Ref: "main"}, Names: []string{"cfg21"}}}),
359+
createCfg("cfg11", "image11", "doc1", []latest.ConfigDependency{{GitRepo: &latest.GitInfo{Repo: "doc2", Ref: "main"}, Names: []string{"cfg21"}}}),
360+
createCfg("cfg00", "image00", ".", []latest.ConfigDependency{{Path: "doc1"}}),
361+
createCfg("cfg01", "image01", ".", nil),
362+
},
363+
},
332364
}
333365

334366
for _, test := range tests {
@@ -338,7 +370,7 @@ requires:
338370
var cfgs []string
339371
for j, c := range d.configs {
340372
id := fmt.Sprintf("%d%d", i, j)
341-
s := fmt.Sprintf(template, c.name, c.requiresStanza, id, id, id)
373+
s := fmt.Sprintf(template, latest.Version, c.name, c.requiresStanza, id, id, id)
342374
cfgs = append(cfgs, s)
343375
}
344376
tmpDir.Write(d.path, strings.Join(cfgs, "\n---\n"))
@@ -355,10 +387,13 @@ requires:
355387
wd, _ := util.RealWorkDir()
356388
c.Build.Artifacts[0].Workspace = filepath.Join(wd, dir)
357389
for i := range c.Dependencies {
390+
if c.Dependencies[i].Path == "" {
391+
continue
392+
}
358393
c.Dependencies[i].Path = filepath.Join(wd, dir, c.Dependencies[i].Path)
359394
}
360395
}
361-
396+
t.Override(&git.SyncRepo, func(g latest.GitInfo, _ config.SkaffoldOptions) (string, error) { return g.Repo, nil })
362397
cfgs, err := getAllConfigs(config.SkaffoldOptions{
363398
Command: "dev",
364399
ConfigurationFile: test.documents[0].path,

0 commit comments

Comments
 (0)