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  }