github.com/GoogleContainerTools/skaffold@v1.39.18/pkg/skaffold/build/jib/jib.go (about) 1 /* 2 Copyright 2019 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 jib 18 19 import ( 20 "bytes" 21 "context" 22 "encoding/json" 23 "errors" 24 "fmt" 25 "os" 26 "os/exec" 27 "path/filepath" 28 "regexp" 29 "sort" 30 "strings" 31 "time" 32 33 "github.com/google/go-containerregistry/pkg/name" 34 35 "github.com/GoogleContainerTools/skaffold/pkg/skaffold/docker" 36 "github.com/GoogleContainerTools/skaffold/pkg/skaffold/output/log" 37 "github.com/GoogleContainerTools/skaffold/pkg/skaffold/schema/latest" 38 "github.com/GoogleContainerTools/skaffold/pkg/skaffold/util" 39 "github.com/GoogleContainerTools/skaffold/pkg/skaffold/walk" 40 ) 41 42 const ( 43 dotDotSlash = ".." + string(filepath.Separator) 44 ) 45 46 // PluginType defines the different supported Jib plugins. 47 type PluginType string 48 49 const ( 50 JibMaven PluginType = "maven" 51 JibGradle PluginType = "gradle" 52 ) 53 54 // IsKnown checks that the num value is a known value (vs 0 or an unknown value). 55 func (t PluginType) IsKnown() bool { 56 switch t { 57 case JibMaven, JibGradle: 58 return true 59 } 60 return false 61 } 62 63 // Name provides a human-oriented label for a plugin type. 64 func PluginName(t PluginType) string { 65 switch t { 66 case JibMaven: 67 return "Jib Maven Plugin" 68 case JibGradle: 69 return "Jib Gradle Plugin" 70 } 71 panic("Unknown Jib Plugin Type: " + string(t)) 72 } 73 74 // filesLists contains cached build/input dependencies 75 type filesLists struct { 76 // BuildDefinitions lists paths to build definitions that trigger a call out to Jib to refresh the pathMap, as well as a rebuild, upon changing 77 BuildDefinitions []string `json:"build"` 78 79 // Inputs lists paths to build dependencies that trigger a rebuild upon changing 80 Inputs []string `json:"inputs"` 81 82 // Results lists paths to files that should be ignored when checking for changes to rebuild 83 Results []string `json:"ignore"` 84 85 // BuildFileTimes keeps track of the last modification time of each build file 86 BuildFileTimes map[string]time.Time 87 } 88 89 // watchedFiles maps from project name to watched files 90 var watchedFiles = map[projectKey]filesLists{} 91 92 type projectKey string 93 94 func getProjectKey(workspace string, a *latest.JibArtifact) projectKey { 95 return projectKey(workspace + "+" + a.Project) 96 } 97 98 func GetBuildDefinitions(workspace string, a *latest.JibArtifact) []string { 99 return watchedFiles[getProjectKey(workspace, a)].BuildDefinitions 100 } 101 102 // GetDependencies returns a list of files to watch for changes to rebuild 103 func GetDependencies(ctx context.Context, workspace string, artifact *latest.JibArtifact) ([]string, error) { 104 t, err := DeterminePluginType(ctx, workspace, artifact) 105 if err != nil { 106 return nil, unableToDeterminePluginType(workspace, err) 107 } 108 switch t { 109 case JibMaven: 110 return getDependenciesMaven(ctx, workspace, artifact) 111 case JibGradle: 112 return getDependenciesGradle(ctx, workspace, artifact) 113 default: 114 return nil, unknownPluginType(workspace) 115 } 116 } 117 118 // DeterminePluginType tries to determine the Jib plugin type for the given artifact. 119 func DeterminePluginType(ctx context.Context, workspace string, artifact *latest.JibArtifact) (PluginType, error) { 120 if !JVMFound(ctx) { 121 return "", errors.New("no working JVM available") 122 } 123 124 // check if explicitly specified 125 if artifact != nil { 126 if t := PluginType(artifact.Type); t.IsKnown() { 127 return t, nil 128 } 129 } 130 131 // check for typical gradle files 132 for _, gradleFile := range []string{"build.gradle", "build.gradle.kts", "gradle.properties", "settings.gradle", "gradlew", "gradlew.bat", "gradlew.cmd"} { 133 if util.IsFile(filepath.Join(workspace, gradleFile)) { 134 return JibGradle, nil 135 } 136 } 137 // check for typical maven files; .mvn is a directory used for polyglot maven 138 if util.IsFile(filepath.Join(workspace, "pom.xml")) || util.IsDir(filepath.Join(workspace, ".mvn")) { 139 return JibMaven, nil 140 } 141 return "", fmt.Errorf("unable to determine Jib plugin type for %s", workspace) 142 } 143 144 // getDependencies returns a list of files to watch for changes to rebuild 145 func getDependencies(ctx context.Context, workspace string, cmd exec.Cmd, a *latest.JibArtifact) ([]string, error) { 146 var dependencyList []string 147 files, ok := watchedFiles[getProjectKey(workspace, a)] 148 if !ok { 149 files = filesLists{} 150 } 151 152 if len(files.Inputs) == 0 && len(files.BuildDefinitions) == 0 { 153 // Make sure build file modification time map is setup 154 if files.BuildFileTimes == nil { 155 files.BuildFileTimes = make(map[string]time.Time) 156 } 157 158 // Refresh dependency list if empty 159 if err := refreshDependencyList(ctx, &files, cmd); err != nil { 160 return nil, fmt.Errorf("initial Jib dependency refresh failed: %w", err) 161 } 162 } else if err := walkFiles(workspace, files.BuildDefinitions, files.Results, func(path string, info os.FileInfo) error { 163 // Walk build files to check for changes 164 if val, ok := files.BuildFileTimes[path]; !ok || info.ModTime() != val { 165 return refreshDependencyList(ctx, &files, cmd) 166 } 167 return nil 168 }); err != nil { 169 return nil, fmt.Errorf("failed to walk Jib build files for changes: %w", err) 170 } 171 172 // Walk updated files to build dependency list 173 if err := walkFiles(workspace, files.Inputs, files.Results, func(path string, info os.FileInfo) error { 174 dependencyList = append(dependencyList, path) 175 return nil 176 }); err != nil { 177 return nil, fmt.Errorf("failed to walk Jib input files to build dependency list: %w", err) 178 } 179 if err := walkFiles(workspace, files.BuildDefinitions, files.Results, func(path string, info os.FileInfo) error { 180 dependencyList = append(dependencyList, path) 181 files.BuildFileTimes[path] = info.ModTime() 182 return nil 183 }); err != nil { 184 return nil, fmt.Errorf("failed to walk Jib build files to build dependency list: %w", err) 185 } 186 187 // Store updated files list information 188 watchedFiles[getProjectKey(workspace, a)] = files 189 190 sort.Strings(dependencyList) 191 return dependencyList, nil 192 } 193 194 // refreshDependencyList calls out to Jib to update files with the latest list of files/directories to watch. 195 func refreshDependencyList(ctx context.Context, files *filesLists, cmd exec.Cmd) error { 196 stdout, err := util.RunCmdOut(ctx, &cmd) 197 if err != nil { 198 return fmt.Errorf("failed to get Jib dependencies: %w", err) 199 } 200 201 // Search for Jib's output JSON. Jib's Maven/Gradle output takes the following form: 202 // ... 203 // BEGIN JIB JSON 204 // {"build":["/paths","/to","/buildFiles"],"inputs":["/paths","/to","/inputs"],"ignore":["/paths","/to","/ignore"]} 205 // ... 206 // To parse the output, search for "BEGIN JIB JSON", then unmarshal the next line into the pathMap struct. 207 matches := regexp.MustCompile(`BEGIN JIB JSON\r?\n({.*})`).FindSubmatch(stdout) 208 if len(matches) == 0 { 209 return errors.New("failed to get Jib dependencies") 210 } 211 212 line := bytes.ReplaceAll(matches[1], []byte(`\`), []byte(`\\`)) 213 return json.Unmarshal(line, &files) 214 } 215 216 // walkFiles walks through a list of files and directories and performs a callback on each of the files 217 func walkFiles(workspace string, watchedFiles []string, ignoredFiles []string, callback func(path string, info os.FileInfo) error) error { 218 // Skaffold prefers to deal with relative paths. In *practice*, Jib's dependencies 219 // are *usually* absolute (relative to the root) and canonical (with all symlinks expanded). 220 // But that's not guaranteed, so we try to relativize paths against the workspace as 221 // both an absolute path and as a canonicalized workspace. 222 workspaceRoots, err := calculateRoots(workspace) 223 if err != nil { 224 return fmt.Errorf("unable to resolve workspace %q: %w", workspace, err) 225 } 226 227 for _, dep := range watchedFiles { 228 if isIgnored(dep, ignoredFiles) { 229 continue 230 } 231 232 // Resolves directories recursively. 233 info, err := os.Stat(dep) 234 if err != nil { 235 if os.IsNotExist(err) { 236 log.Entry(context.TODO()).Debugf("could not stat dependency: %s", err) 237 continue // Ignore files that don't exist 238 } 239 return fmt.Errorf("unable to stat file %q: %w", dep, err) 240 } 241 242 // Process file 243 if !info.IsDir() { 244 // try to relativize the path: an error indicates that the file cannot 245 // be made relative to the roots, and so we just use the full path 246 if relative, err := relativize(dep, workspaceRoots...); err == nil { 247 dep = relative 248 } 249 if err := callback(dep, info); err != nil { 250 return err 251 } 252 continue 253 } 254 255 notIgnored := func(path string, info walk.Dirent) (bool, error) { 256 if isIgnored(path, ignoredFiles) { 257 return false, filepath.SkipDir 258 } 259 260 return true, nil 261 } 262 263 // Process directory 264 if err = walk.From(dep).Unsorted().When(notIgnored).WhenIsFile().Do(func(path string, info walk.Dirent) error { 265 stat, err := os.Stat(path) 266 if err != nil { 267 return nil // Ignore 268 } 269 270 // try to relativize the path: an error indicates that the file cannot 271 // be made relative to the roots, and so we just use the full path 272 if relative, err := relativize(path, workspaceRoots...); err == nil { 273 path = relative 274 } 275 return callback(path, stat) 276 }); err != nil { 277 return fmt.Errorf("filepath walk: %w", err) 278 } 279 } 280 return nil 281 } 282 283 // isIgnored tests a path for whether or not it should be ignored according to a list of ignored files/directories 284 func isIgnored(path string, ignoredFiles []string) bool { 285 for _, ignored := range ignoredFiles { 286 if strings.HasPrefix(path, ignored) { 287 return true 288 } 289 } 290 return false 291 } 292 293 // calculateRoots returns a list of possible symlink-expanded paths 294 func calculateRoots(path string) ([]string, error) { 295 path, err := filepath.Abs(path) 296 if err != nil { 297 return nil, fmt.Errorf("unable to resolve %q: %w", path, err) 298 } 299 canonical, err := filepath.EvalSymlinks(path) 300 if err != nil { 301 return nil, fmt.Errorf("unable to canonicalize workspace %q: %w", path, err) 302 } 303 if path == canonical { 304 return []string{path}, nil 305 } 306 return []string{canonical, path}, nil 307 } 308 309 // relativize tries to make path relative to one of the given roots 310 func relativize(path string, roots ...string) (string, error) { 311 if !filepath.IsAbs(path) { 312 return path, nil 313 } 314 for _, root := range roots { 315 // check that the path can be made relative and is contained (since `filepath.Rel("/a", "/b") => "../b"`) 316 if rel, err := filepath.Rel(root, path); err == nil && !strings.HasPrefix(rel, dotDotSlash) { 317 return rel, nil 318 } 319 } 320 return "", errors.New("could not relativize path") 321 } 322 323 // isOnInsecureRegistry checks if the given image specifies an insecure registry 324 func isOnInsecureRegistry(image string, insecureRegistries map[string]bool) (bool, error) { 325 ref, err := name.ParseReference(image) 326 if err != nil { 327 return false, err 328 } 329 330 return docker.IsInsecure(ref, insecureRegistries), nil 331 } 332 333 // 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. 334 func baseImageArg(a *latest.JibArtifact, r ArtifactResolver, deps []*latest.ArtifactDependency, pushImages bool) (string, bool) { 335 if a.BaseImage == "" { 336 return "", false 337 } 338 for _, d := range deps { 339 if a.BaseImage != d.Alias { 340 continue 341 } 342 img, found := r.GetImageTag(d.ImageName) 343 if !found { 344 log.Entry(context.TODO()).Fatalf("failed to resolve build result for required artifact %q", d.ImageName) 345 } 346 if pushImages { 347 // pull image from the registry (prefix `registry://` is optional) 348 return fmt.Sprintf("-Djib.from.image=%s", img), true 349 } 350 // must use `docker://` prefix to retrieve image from the local docker daemon 351 return fmt.Sprintf("-Djib.from.image=docker://%s", img), true 352 } 353 return fmt.Sprintf("-Djib.from.image=%s", a.BaseImage), true 354 }