/
jib.go
354 lines (307 loc) · 11.6 KB
/
jib.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
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
/*
Copyright 2019 The Skaffold Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package jib
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"os"
"os/exec"
"path/filepath"
"regexp"
"sort"
"strings"
"time"
"github.com/google/go-containerregistry/pkg/name"
"github.com/GoogleContainerTools/skaffold/pkg/skaffold/docker"
"github.com/GoogleContainerTools/skaffold/pkg/skaffold/output/log"
"github.com/GoogleContainerTools/skaffold/pkg/skaffold/schema/latest"
"github.com/GoogleContainerTools/skaffold/pkg/skaffold/util"
"github.com/GoogleContainerTools/skaffold/pkg/skaffold/walk"
)
const (
dotDotSlash = ".." + string(filepath.Separator)
)
// PluginType defines the different supported Jib plugins.
type PluginType string
const (
JibMaven PluginType = "maven"
JibGradle PluginType = "gradle"
)
// IsKnown checks that the num value is a known value (vs 0 or an unknown value).
func (t PluginType) IsKnown() bool {
switch t {
case JibMaven, JibGradle:
return true
}
return false
}
// Name provides a human-oriented label for a plugin type.
func PluginName(t PluginType) string {
switch t {
case JibMaven:
return "Jib Maven Plugin"
case JibGradle:
return "Jib Gradle Plugin"
}
panic("Unknown Jib Plugin Type: " + string(t))
}
// filesLists contains cached build/input dependencies
type filesLists struct {
// BuildDefinitions lists paths to build definitions that trigger a call out to Jib to refresh the pathMap, as well as a rebuild, upon changing
BuildDefinitions []string `json:"build"`
// Inputs lists paths to build dependencies that trigger a rebuild upon changing
Inputs []string `json:"inputs"`
// Results lists paths to files that should be ignored when checking for changes to rebuild
Results []string `json:"ignore"`
// BuildFileTimes keeps track of the last modification time of each build file
BuildFileTimes map[string]time.Time
}
// watchedFiles maps from project name to watched files
var watchedFiles = map[projectKey]filesLists{}
type projectKey string
func getProjectKey(workspace string, a *latest.JibArtifact) projectKey {
return projectKey(workspace + "+" + a.Project)
}
func GetBuildDefinitions(workspace string, a *latest.JibArtifact) []string {
return watchedFiles[getProjectKey(workspace, a)].BuildDefinitions
}
// GetDependencies returns a list of files to watch for changes to rebuild
func GetDependencies(ctx context.Context, workspace string, artifact *latest.JibArtifact) ([]string, error) {
t, err := DeterminePluginType(ctx, workspace, artifact)
if err != nil {
return nil, unableToDeterminePluginType(workspace, err)
}
switch t {
case JibMaven:
return getDependenciesMaven(ctx, workspace, artifact)
case JibGradle:
return getDependenciesGradle(ctx, workspace, artifact)
default:
return nil, unknownPluginType(workspace)
}
}
// DeterminePluginType tries to determine the Jib plugin type for the given artifact.
func DeterminePluginType(ctx context.Context, workspace string, artifact *latest.JibArtifact) (PluginType, error) {
if !JVMFound(ctx) {
return "", errors.New("no working JVM available")
}
// check if explicitly specified
if artifact != nil {
if t := PluginType(artifact.Type); t.IsKnown() {
return t, nil
}
}
// check for typical gradle files
for _, gradleFile := range []string{"build.gradle", "build.gradle.kts", "gradle.properties", "settings.gradle", "gradlew", "gradlew.bat", "gradlew.cmd"} {
if util.IsFile(filepath.Join(workspace, gradleFile)) {
return JibGradle, nil
}
}
// check for typical maven files; .mvn is a directory used for polyglot maven
if util.IsFile(filepath.Join(workspace, "pom.xml")) || util.IsDir(filepath.Join(workspace, ".mvn")) {
return JibMaven, nil
}
return "", fmt.Errorf("unable to determine Jib plugin type for %s", workspace)
}
// getDependencies returns a list of files to watch for changes to rebuild
func getDependencies(ctx context.Context, workspace string, cmd exec.Cmd, a *latest.JibArtifact) ([]string, error) {
var dependencyList []string
files, ok := watchedFiles[getProjectKey(workspace, a)]
if !ok {
files = filesLists{}
}
if len(files.Inputs) == 0 && len(files.BuildDefinitions) == 0 {
// Make sure build file modification time map is setup
if files.BuildFileTimes == nil {
files.BuildFileTimes = make(map[string]time.Time)
}
// Refresh dependency list if empty
if err := refreshDependencyList(ctx, &files, cmd); err != nil {
return nil, fmt.Errorf("initial Jib dependency refresh failed: %w", err)
}
} else if err := walkFiles(workspace, files.BuildDefinitions, files.Results, func(path string, info os.FileInfo) error {
// Walk build files to check for changes
if val, ok := files.BuildFileTimes[path]; !ok || info.ModTime() != val {
return refreshDependencyList(ctx, &files, cmd)
}
return nil
}); err != nil {
return nil, fmt.Errorf("failed to walk Jib build files for changes: %w", err)
}
// Walk updated files to build dependency list
if err := walkFiles(workspace, files.Inputs, files.Results, func(path string, info os.FileInfo) error {
dependencyList = append(dependencyList, path)
return nil
}); err != nil {
return nil, fmt.Errorf("failed to walk Jib input files to build dependency list: %w", err)
}
if err := walkFiles(workspace, files.BuildDefinitions, files.Results, func(path string, info os.FileInfo) error {
dependencyList = append(dependencyList, path)
files.BuildFileTimes[path] = info.ModTime()
return nil
}); err != nil {
return nil, fmt.Errorf("failed to walk Jib build files to build dependency list: %w", err)
}
// Store updated files list information
watchedFiles[getProjectKey(workspace, a)] = files
sort.Strings(dependencyList)
return dependencyList, nil
}
// refreshDependencyList calls out to Jib to update files with the latest list of files/directories to watch.
func refreshDependencyList(ctx context.Context, files *filesLists, cmd exec.Cmd) error {
stdout, err := util.RunCmdOut(ctx, &cmd)
if err != nil {
return fmt.Errorf("failed to get Jib dependencies: %w", err)
}
// Search for Jib's output JSON. Jib's Maven/Gradle output takes the following form:
// ...
// BEGIN JIB JSON
// {"build":["/paths","/to","/buildFiles"],"inputs":["/paths","/to","/inputs"],"ignore":["/paths","/to","/ignore"]}
// ...
// To parse the output, search for "BEGIN JIB JSON", then unmarshal the next line into the pathMap struct.
matches := regexp.MustCompile(`BEGIN JIB JSON\r?\n({.*})`).FindSubmatch(stdout)
if len(matches) == 0 {
return errors.New("failed to get Jib dependencies")
}
line := bytes.ReplaceAll(matches[1], []byte(`\`), []byte(`\\`))
return json.Unmarshal(line, &files)
}
// walkFiles walks through a list of files and directories and performs a callback on each of the files
func walkFiles(workspace string, watchedFiles []string, ignoredFiles []string, callback func(path string, info os.FileInfo) error) error {
// Skaffold prefers to deal with relative paths. In *practice*, Jib's dependencies
// are *usually* absolute (relative to the root) and canonical (with all symlinks expanded).
// But that's not guaranteed, so we try to relativize paths against the workspace as
// both an absolute path and as a canonicalized workspace.
workspaceRoots, err := calculateRoots(workspace)
if err != nil {
return fmt.Errorf("unable to resolve workspace %q: %w", workspace, err)
}
for _, dep := range watchedFiles {
if isIgnored(dep, ignoredFiles) {
continue
}
// Resolves directories recursively.
info, err := os.Stat(dep)
if err != nil {
if os.IsNotExist(err) {
log.Entry(context.TODO()).Debugf("could not stat dependency: %s", err)
continue // Ignore files that don't exist
}
return fmt.Errorf("unable to stat file %q: %w", dep, err)
}
// Process file
if !info.IsDir() {
// try to relativize the path: an error indicates that the file cannot
// be made relative to the roots, and so we just use the full path
if relative, err := relativize(dep, workspaceRoots...); err == nil {
dep = relative
}
if err := callback(dep, info); err != nil {
return err
}
continue
}
notIgnored := func(path string, info walk.Dirent) (bool, error) {
if isIgnored(path, ignoredFiles) {
return false, filepath.SkipDir
}
return true, nil
}
// Process directory
if err = walk.From(dep).Unsorted().When(notIgnored).WhenIsFile().Do(func(path string, info walk.Dirent) error {
stat, err := os.Stat(path)
if err != nil {
return nil // Ignore
}
// try to relativize the path: an error indicates that the file cannot
// be made relative to the roots, and so we just use the full path
if relative, err := relativize(path, workspaceRoots...); err == nil {
path = relative
}
return callback(path, stat)
}); err != nil {
return fmt.Errorf("filepath walk: %w", err)
}
}
return nil
}
// isIgnored tests a path for whether or not it should be ignored according to a list of ignored files/directories
func isIgnored(path string, ignoredFiles []string) bool {
for _, ignored := range ignoredFiles {
if strings.HasPrefix(path, ignored) {
return true
}
}
return false
}
// calculateRoots returns a list of possible symlink-expanded paths
func calculateRoots(path string) ([]string, error) {
path, err := filepath.Abs(path)
if err != nil {
return nil, fmt.Errorf("unable to resolve %q: %w", path, err)
}
canonical, err := filepath.EvalSymlinks(path)
if err != nil {
return nil, fmt.Errorf("unable to canonicalize workspace %q: %w", path, err)
}
if path == canonical {
return []string{path}, nil
}
return []string{canonical, path}, nil
}
// relativize tries to make path relative to one of the given roots
func relativize(path string, roots ...string) (string, error) {
if !filepath.IsAbs(path) {
return path, nil
}
for _, root := range roots {
// check that the path can be made relative and is contained (since `filepath.Rel("/a", "/b") => "../b"`)
if rel, err := filepath.Rel(root, path); err == nil && !strings.HasPrefix(rel, dotDotSlash) {
return rel, nil
}
}
return "", errors.New("could not relativize path")
}
// isOnInsecureRegistry checks if the given image specifies an insecure registry
func isOnInsecureRegistry(image string, insecureRegistries map[string]bool) (bool, error) {
ref, err := name.ParseReference(image)
if err != nil {
return false, err
}
return docker.IsInsecure(ref, insecureRegistries), nil
}
// baseImageArg formats the base image as a build argument. It also replaces the provided base image with an image from the required artifacts if specified.
func baseImageArg(a *latest.JibArtifact, r ArtifactResolver, deps []*latest.ArtifactDependency, pushImages bool) (string, bool) {
if a.BaseImage == "" {
return "", false
}
for _, d := range deps {
if a.BaseImage != d.Alias {
continue
}
img, found := r.GetImageTag(d.ImageName)
if !found {
log.Entry(context.TODO()).Fatalf("failed to resolve build result for required artifact %q", d.ImageName)
}
if pushImages {
// pull image from the registry (prefix `registry://` is optional)
return fmt.Sprintf("-Djib.from.image=%s", img), true
}
// must use `docker://` prefix to retrieve image from the local docker daemon
return fmt.Sprintf("-Djib.from.image=docker://%s", img), true
}
return fmt.Sprintf("-Djib.from.image=%s", a.BaseImage), true
}