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 }