github.com/GoogleContainerTools/skaffold@v1.39.18/pkg/skaffold/build/jib/sync.go (about)

     1  /*
     2  Copyright 2020 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  	"context"
    21  	"encoding/json"
    22  	"errors"
    23  	"fmt"
    24  	"os"
    25  	"os/exec"
    26  	"path/filepath"
    27  	"regexp"
    28  	"time"
    29  
    30  	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/filemon"
    31  	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/output/log"
    32  	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/schema/latest"
    33  	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/util"
    34  )
    35  
    36  var syncLists = map[projectKey]SyncMap{}
    37  
    38  type SyncMap map[string]SyncEntry
    39  
    40  type SyncEntry struct {
    41  	Dest     []string
    42  	FileTime time.Time
    43  	IsDirect bool
    44  }
    45  
    46  type JSONSyncMap struct {
    47  	Direct    []JSONSyncEntry `json:"direct"`
    48  	Generated []JSONSyncEntry `json:"generated"`
    49  }
    50  
    51  type JSONSyncEntry struct {
    52  	Src  string `json:"src"`
    53  	Dest string `json:"dest"`
    54  }
    55  
    56  var InitSync = initSync
    57  
    58  func initSync(ctx context.Context, workspace string, a *latest.JibArtifact) error {
    59  	syncMap, err := getSyncMapFunc(ctx, workspace, a)
    60  	if err != nil {
    61  		return fmt.Errorf("failed to initialize sync state for %q: %w", workspace, err)
    62  	}
    63  	syncLists[getProjectKey(workspace, a)] = *syncMap
    64  	return nil
    65  }
    66  
    67  var GetSyncDiff = getSyncDiff
    68  
    69  // returns toCopy, toDelete, error
    70  func getSyncDiff(ctx context.Context, workspace string, a *latest.JibArtifact, e filemon.Events) (map[string][]string, map[string][]string, error) {
    71  	// no deletions allowed
    72  	if len(e.Deleted) != 0 {
    73  		// change into logging
    74  		log.Entry(ctx).Debug("Deletions are not supported by jib auto sync at the moment")
    75  		return nil, nil, nil
    76  	}
    77  
    78  	// if anything that was modified was a build file, do NOT sync, do a rebuild
    79  	buildFiles := GetBuildDefinitions(workspace, a)
    80  	for _, f := range e.Modified {
    81  		f, err := toAbs(f)
    82  		if err != nil {
    83  			return nil, nil, fmt.Errorf("failed to calculate absolute path: %w", err)
    84  		}
    85  		for _, bf := range buildFiles {
    86  			if f == bf {
    87  				return nil, nil, nil
    88  			}
    89  		}
    90  	}
    91  
    92  	currSyncMap := syncLists[getProjectKey(workspace, a)]
    93  
    94  	// if we're only dealing with 1. modified and 2. directly syncable files,
    95  	// then we can sync those files directly without triggering a build
    96  	if len(e.Deleted) == 0 && len(e.Added) == 0 {
    97  		matches := make(map[string][]string)
    98  		for _, f := range e.Modified {
    99  			f, err := toAbs(f)
   100  			if err != nil {
   101  				return nil, nil, fmt.Errorf("failed to calculate absolute path: %w", err)
   102  			}
   103  			if val, ok := currSyncMap[f]; ok {
   104  				if !val.IsDirect {
   105  					break
   106  				}
   107  				matches[f] = val.Dest
   108  				// if we decide that we don't need to do a call to getSyncMapFromSystem,
   109  				// (which would update these file times), we have to update
   110  				// our state for these files manually here.
   111  				infog, err := os.Stat(f)
   112  				if err != nil {
   113  					return nil, nil, fmt.Errorf("could not obtain file mod time: %w", err)
   114  				}
   115  				val.FileTime = infog.ModTime()
   116  				currSyncMap[f] = val
   117  			} else {
   118  				break
   119  			}
   120  		}
   121  		if len(matches) == len(e.Modified) {
   122  			return matches, nil, nil
   123  		}
   124  	}
   125  
   126  	// we need to do another build and get a new sync map
   127  	nextSyncMap, err := getSyncMapFunc(ctx, workspace, a)
   128  	if err != nil {
   129  		return nil, nil, err
   130  	}
   131  	syncLists[getProjectKey(workspace, a)] = *nextSyncMap
   132  
   133  	toCopy := make(map[string][]string)
   134  
   135  	// calculate the diff of the syncmaps
   136  	for k, v := range *nextSyncMap {
   137  		if curr, ok := currSyncMap[k]; ok {
   138  			if v.FileTime != curr.FileTime {
   139  				// file updated
   140  				toCopy[k] = v.Dest
   141  			}
   142  		} else {
   143  			// new file was created
   144  			toCopy[k] = v.Dest
   145  		}
   146  	}
   147  
   148  	return toCopy, nil, nil
   149  }
   150  
   151  // for testing
   152  var (
   153  	getSyncMapFunc = getSyncMap
   154  )
   155  
   156  func getSyncMap(ctx context.Context, workspace string, artifact *latest.JibArtifact) (*SyncMap, error) {
   157  	// cmd will hold context that identifies the project
   158  	cmd, err := getSyncMapCommand(ctx, workspace, artifact)
   159  	if err != nil {
   160  		return nil, fmt.Errorf("failed to get sync command: %w", err)
   161  	}
   162  
   163  	sm, err := getSyncMapFromSystem(ctx, cmd)
   164  	if err != nil {
   165  		return nil, fmt.Errorf("failed to obtain sync map from jib builder: %w", err)
   166  	}
   167  	return sm, nil
   168  }
   169  
   170  func getSyncMapCommand(ctx context.Context, workspace string, artifact *latest.JibArtifact) (*exec.Cmd, error) {
   171  	t, err := DeterminePluginType(ctx, workspace, artifact)
   172  	if err != nil {
   173  		return nil, err
   174  	}
   175  
   176  	switch t {
   177  	case JibMaven:
   178  		return getSyncMapCommandMaven(ctx, workspace, artifact), nil
   179  	case JibGradle:
   180  		return getSyncMapCommandGradle(ctx, workspace, artifact), nil
   181  	default:
   182  		return nil, fmt.Errorf("unable to handle Jib builder type %s for %s", t, workspace)
   183  	}
   184  }
   185  
   186  func getSyncMapFromSystem(ctx context.Context, cmd *exec.Cmd) (*SyncMap, error) {
   187  	jsm := JSONSyncMap{}
   188  	stdout, err := util.RunCmdOut(ctx, cmd)
   189  	if err != nil {
   190  		return nil, fmt.Errorf("failed to get Jib sync map: %w", err)
   191  	}
   192  
   193  	matches := regexp.MustCompile(`BEGIN JIB JSON: SYNCMAP/1\r?\n({.*})`).FindSubmatch(stdout)
   194  	if len(matches) == 0 {
   195  		return nil, errors.New("failed to get Jib Sync data")
   196  	}
   197  
   198  	if err := json.Unmarshal(matches[1], &jsm); err != nil {
   199  		return nil, fmt.Errorf("failed to unmarshal jib sync JSON: %w", err)
   200  	}
   201  
   202  	sm := make(SyncMap)
   203  	if err := sm.addEntries(jsm.Direct, true); err != nil {
   204  		return nil, fmt.Errorf("failed to add jib json direct entries to sync state: %w", err)
   205  	}
   206  	if err := sm.addEntries(jsm.Generated, false); err != nil {
   207  		return nil, fmt.Errorf("failed to add jib json generated entries to sync state: %w", err)
   208  	}
   209  	return &sm, nil
   210  }
   211  
   212  func (sm SyncMap) addEntries(entries []JSONSyncEntry, direct bool) error {
   213  	for _, entry := range entries {
   214  		info, err := os.Stat(entry.Src)
   215  		if err != nil {
   216  			return fmt.Errorf("could not obtain file mod time for %q: %w", entry.Src, err)
   217  		}
   218  		sm[entry.Src] = SyncEntry{
   219  			Dest:     []string{entry.Dest},
   220  			FileTime: info.ModTime(),
   221  			IsDirect: direct,
   222  		}
   223  	}
   224  	return nil
   225  }
   226  
   227  func toAbs(f string) (string, error) {
   228  	if !filepath.IsAbs(f) {
   229  		af, err := filepath.Abs(f)
   230  		if err != nil {
   231  			return "", fmt.Errorf("failed to calculate absolute path: %w", err)
   232  		}
   233  		return af, nil
   234  	}
   235  	return f, nil
   236  }