github.com/replicatedhq/ship@v0.55.0/pkg/lifecycle/unfork/unforker.go (about) 1 package unfork 2 3 import ( 4 "context" 5 "os" 6 "path" 7 "path/filepath" 8 "sort" 9 "strings" 10 "time" 11 12 "github.com/go-kit/kit/log" 13 "github.com/go-kit/kit/log/level" 14 "github.com/pkg/errors" 15 "github.com/spf13/afero" 16 yaml "gopkg.in/yaml.v3" 17 "k8s.io/client-go/kubernetes/scheme" 18 kustomizepatch "sigs.k8s.io/kustomize/pkg/patch" 19 "sigs.k8s.io/kustomize/pkg/types" 20 21 "github.com/replicatedhq/ship/pkg/api" 22 "github.com/replicatedhq/ship/pkg/constants" 23 "github.com/replicatedhq/ship/pkg/lifecycle" 24 "github.com/replicatedhq/ship/pkg/lifecycle/daemon/daemontypes" 25 "github.com/replicatedhq/ship/pkg/patch" 26 "github.com/replicatedhq/ship/pkg/state" 27 "github.com/replicatedhq/ship/pkg/util" 28 ) 29 30 func NewDaemonUnforker(logger log.Logger, daemon daemontypes.Daemon, fs afero.Afero, stateManager state.Manager, patcher patch.Patcher) lifecycle.Unforker { 31 return &daemonunforker{ 32 Unforker: Unforker{ 33 Logger: logger, 34 FS: fs, 35 State: stateManager, 36 Patcher: patcher, 37 }, 38 Daemon: daemon, 39 } 40 } 41 42 // unforker will *try* to pull in the Kustomize libs from kubernetes-sigs/kustomize, 43 // if not we'll have to fork. for now it just explodes 44 type daemonunforker struct { 45 Unforker 46 Daemon daemontypes.Daemon 47 } 48 49 func (l *daemonunforker) Execute(ctx context.Context, release *api.Release, step api.Unfork) error { 50 daemonExitedChan := l.Daemon.EnsureStarted(ctx, release) 51 err := l.awaitUnforkerSaved(ctx, daemonExitedChan) 52 if err != nil { 53 return errors.Wrap(err, "ensure daemon \"started\"") 54 } 55 56 return l.Unforker.Execute(ctx, release, step) 57 } 58 59 // hack -- get the root path off a render step to tell if we should prefix kustomize outputs 60 func (l *Unforker) getPotentiallyChrootedFs(release *api.Release) (afero.Afero, error) { 61 renderRoot := constants.InstallerPrefixPath 62 renderStep := release.FindRenderStep() 63 if renderStep == nil || renderStep.Root == "./" || renderStep.Root == "." { 64 return l.FS, nil 65 } 66 if renderStep.Root != "" { 67 renderRoot = renderStep.Root 68 } 69 70 fs := afero.Afero{Fs: afero.NewBasePathFs(l.FS, renderRoot)} 71 err := fs.MkdirAll("/", 0755) 72 if err != nil { 73 return afero.Afero{}, errors.Wrap(err, "mkdir fs root") 74 } 75 return fs, nil 76 } 77 78 func (l *daemonunforker) awaitUnforkerSaved(ctx context.Context, daemonExitedChan chan error) error { 79 debug := level.Debug(log.With(l.Logger, "struct", "kustomizer", "method", "unforker.save.await")) 80 for { 81 select { 82 case <-ctx.Done(): 83 debug.Log("event", "ctx.done") 84 return ctx.Err() 85 case err := <-daemonExitedChan: 86 debug.Log("event", "daemon.exit") 87 if err != nil { 88 return err 89 } 90 return errors.New("daemon exited") 91 case <-l.Daemon.UnforkSavedChan(): 92 debug.Log("event", "unfork.finalized") 93 return nil 94 case <-time.After(10 * time.Second): 95 debug.Log("waitingFor", "unfork.finalized") 96 } 97 } 98 } 99 100 func (l *Unforker) writeBase(step api.Unfork) error { 101 debug := level.Debug(log.With(l.Logger, "method", "writeBase")) 102 103 currentState, err := l.State.CachedState() 104 if err != nil { 105 return errors.Wrap(err, "load state") 106 } 107 108 currentKustomize := currentState.CurrentKustomize() 109 if currentKustomize == nil { 110 currentKustomize = &state.Kustomize{} 111 } 112 shipOverlay := currentKustomize.Ship() 113 114 baseKustomization := types.Kustomization{} 115 if err := l.FS.Walk( 116 step.UpstreamBase, 117 func(targetPath string, info os.FileInfo, err error) error { 118 if err != nil { 119 debug.Log("event", "walk.fail", "path", targetPath) 120 return errors.Wrap(err, "failed to walk path") 121 } 122 relativePath, err := filepath.Rel(step.UpstreamBase, targetPath) 123 if err != nil { 124 debug.Log("event", "relativepath.fail", "base", step.UpstreamBase, "target", targetPath) 125 return errors.Wrap(err, "failed to get relative path") 126 } 127 if l.shouldAddFileToBase(step.UpstreamBase, shipOverlay.ExcludedBases, relativePath) { 128 baseKustomization.Resources = append(baseKustomization.Resources, relativePath) 129 } 130 return nil 131 }, 132 ); err != nil { 133 return err 134 } 135 136 if len(baseKustomization.Resources) == 0 { 137 return errors.New("Base directory is empty") 138 } 139 140 marshalled, err := util.MarshalIndent(2, baseKustomization) 141 if err != nil { 142 return errors.Wrap(err, "marshal base kustomization.yaml") 143 } 144 145 // write base kustomization 146 name := path.Join(step.UpstreamBase, "kustomization.yaml") 147 err = l.FS.WriteFile(name, []byte(marshalled), 0666) 148 if err != nil { 149 return errors.Wrapf(err, "write file %s", name) 150 } 151 return nil 152 } 153 154 func (l *Unforker) shouldAddFileToBase(basePath string, excludedBases []string, targetPath string) bool { 155 baseFs := afero.Afero{Fs: afero.NewBasePathFs(l.FS, basePath)} 156 return util.ShouldAddFileToBase(&baseFs, excludedBases, targetPath) 157 } 158 159 func (l *Unforker) writePatches(fs afero.Afero, shipOverlay state.Overlay, destDir string) (relativePatchPaths []kustomizepatch.StrategicMerge, err error) { 160 patches, err := l.writeFileMap(fs, shipOverlay.Patches, destDir) 161 if err != nil { 162 return nil, errors.Wrapf(err, "write file map to %s", destDir) 163 } 164 for _, p := range patches { 165 relativePatchPaths = append(relativePatchPaths, kustomizepatch.StrategicMerge(p)) 166 } 167 return 168 } 169 170 func (l *Unforker) writeResources(fs afero.Afero, shipOverlay state.Overlay, destDir string) (relativeResourcePaths []string, err error) { 171 return l.writeFileMap(fs, shipOverlay.Resources, destDir) 172 } 173 174 func (l *Unforker) writeFileMap(fs afero.Afero, files map[string]string, destDir string) (paths []string, err error) { 175 debug := level.Debug(log.With(l.Logger, "method", "writeResources")) 176 177 var keys []string 178 for k := range files { 179 keys = append(keys, k) 180 } 181 sort.Strings(keys) 182 183 for _, file := range keys { 184 contents := files[file] 185 186 name := path.Join(destDir, file) 187 err := l.writeFile(fs, name, contents) 188 if err != nil { 189 debug.Log("event", "write", "name", name) 190 return []string{}, errors.Wrapf(err, "write resource %s", name) 191 } 192 193 relativePatchPath, err := filepath.Rel(destDir, name) 194 if err != nil { 195 return []string{}, errors.Wrap(err, "unable to determine relative path") 196 } 197 paths = append(paths, relativePatchPath) 198 } 199 200 return paths, nil 201 202 } 203 204 func (l *Unforker) writeFile(fs afero.Afero, name string, contents string) error { 205 debug := level.Debug(log.With(l.Logger, "method", "writeFile")) 206 207 destDir := filepath.Dir(name) 208 209 // make the dir 210 err := l.FS.MkdirAll(destDir, 0777) 211 if err != nil { 212 debug.Log("event", "mkdir.fail", "dir", destDir) 213 return errors.Wrapf(err, "make dir %s", destDir) 214 } 215 216 // write the file 217 err = l.FS.WriteFile(name, []byte(contents), 0666) 218 if err != nil { 219 return errors.Wrapf(err, "write patch %s", name) 220 } 221 debug.Log("event", "patch.written", "patch", name) 222 return nil 223 } 224 225 func (l *Unforker) writeOverlay(step api.Unfork, relativePatchPaths []kustomizepatch.StrategicMerge, relativeResourcePaths []string) error { 226 // just always make a new kustomization.yaml for now 227 kustomization := types.Kustomization{ 228 Bases: []string{ 229 filepath.Join("../../", step.UpstreamBase), 230 }, 231 PatchesStrategicMerge: relativePatchPaths, 232 Resources: relativeResourcePaths, 233 } 234 235 marshalled, err := util.MarshalIndent(2, kustomization) 236 if err != nil { 237 return errors.Wrap(err, "marshal kustomization.yaml") 238 } 239 240 name := path.Join(step.OverlayPath(), "kustomization.yaml") 241 err = l.FS.WriteFile(name, []byte(marshalled), 0666) 242 if err != nil { 243 return errors.Wrapf(err, "write file %s", name) 244 } 245 246 return nil 247 } 248 249 func (l *Unforker) generatePatchesAndExcludeBases(fs afero.Afero, step api.Unfork, upstreamMap map[util.MinimalK8sYaml]string) (*state.Kustomize, error) { 250 debug := level.Debug(log.With(l.Logger, "struct", "unforker", "handler", "generatePatchesAndExcludeBases")) 251 252 kustomize := &state.Kustomize{} 253 overlay := kustomize.Ship() 254 255 if err := l.FS.Walk( 256 step.ForkedBase, 257 func(targetPath string, info os.FileInfo, err error) error { 258 if err != nil { 259 debug.Log("event", "walk.fail", "path", targetPath) 260 return errors.Wrap(err, "walk path") 261 } 262 263 // ignore non-yaml 264 if filepath.Ext(targetPath) != ".yaml" && filepath.Ext(targetPath) != ".yml" { 265 return nil 266 } 267 268 if info.Mode().IsDir() { 269 return nil 270 } 271 272 if strings.HasSuffix(info.Name(), "CustomResourceDefinitions.yaml") { 273 // custom resource definitions file - multidoc yaml and not something we support editing currently 274 debug.Log("event", "relativepath.skip", "base", step.ForkedBase, "target", targetPath) 275 return nil 276 } 277 278 relativePath, err := filepath.Rel(step.ForkedBase, targetPath) 279 if err != nil { 280 debug.Log("event", "relativepath.fail", "base", step.ForkedBase, "target", targetPath) 281 return errors.Wrap(err, "get relative path") 282 } 283 284 forkedData, err := fs.ReadFile(targetPath) 285 if err != nil { 286 return errors.Wrap(err, "read forked") 287 } 288 289 forkedResource, err := util.NewKubernetesResource(forkedData) 290 if err != nil { 291 return errors.Wrapf(err, "create new k8s resource %s", targetPath) 292 } 293 294 if _, err := scheme.Scheme.New(util.ToGroupVersionKind(forkedResource.Id().Gvk())); err != nil { 295 // Ignore all non-k8s resources 296 return nil 297 } 298 299 forkedMinimal := util.MinimalK8sYaml{} 300 if err := yaml.Unmarshal(forkedData, &forkedMinimal); err != nil { 301 return errors.Wrap(err, "read forked minimal") 302 } 303 304 _, fileName := path.Split(relativePath) 305 fileName = string(filepath.Separator) + fileName 306 upstreamPath := l.findMatchingUpstreamPath(upstreamMap, forkedMinimal) 307 if upstreamPath == "" { 308 // If no equivalent upstream file exists, it must be a brand new file. 309 overlay.Resources[fileName] = string(forkedData) 310 debug.Log("event", "resource.saved", "resource", fileName) 311 return nil 312 } 313 314 upstreamData, err := fs.ReadFile(upstreamPath) 315 if err != nil { 316 return errors.Wrap(err, "read upstream") 317 } 318 319 patch, err := l.Patcher.CreateTwoWayMergePatch(upstreamData, forkedData) 320 if err != nil { 321 return errors.Wrap(err, "create patch") 322 } 323 324 includePatch, err := containsNonGVK(patch) 325 if err != nil { 326 return errors.Wrap(err, "contains non gvk") 327 } 328 329 if includePatch { 330 overlay.Patches[fileName] = string(patch) 331 if err := l.FS.WriteFile(path.Join(step.OverlayPath(), fileName), patch, 0755); err != nil { 332 return errors.Wrap(err, "write overlay") 333 } 334 } 335 336 return nil 337 }, 338 ); err != nil { 339 return nil, err 340 } 341 342 excludedBases := []string{} 343 for _, upstream := range upstreamMap { 344 relPathToBase, err := filepath.Rel(constants.KustomizeBasePath, upstream) 345 if err != nil { 346 return nil, errors.Wrapf(err, "relative path to base %s", upstream) 347 } 348 excludedBases = append(excludedBases, string(filepath.Separator)+relPathToBase) 349 } 350 351 sort.Strings(excludedBases) 352 overlay.ExcludedBases = excludedBases 353 354 kustomize.Overlays = map[string]state.Overlay{ 355 "ship": overlay, 356 } 357 358 err := l.State.SaveKustomize(kustomize) 359 if err != nil { 360 return nil, errors.Wrap(err, "save new state") 361 } 362 363 return kustomize, nil 364 } 365 366 func (l *Unforker) mapUpstream(upstreamMap map[util.MinimalK8sYaml]string, upstreamPath string) error { 367 isDir, err := l.FS.IsDir(upstreamPath) 368 if err != nil { 369 return errors.Wrapf(err, "is dir %s", upstreamPath) 370 } 371 372 if isDir { 373 files, err := l.FS.ReadDir(upstreamPath) 374 if err != nil { 375 return errors.Wrapf(err, "read dir %s", upstreamPath) 376 } 377 378 for _, file := range files { 379 if err := l.mapUpstream(upstreamMap, filepath.Join(upstreamPath, file.Name())); err != nil { 380 return err 381 } 382 } 383 } else { 384 upstreamB, err := l.FS.ReadFile(upstreamPath) 385 if err != nil { 386 return errors.Wrapf(err, "read file %s", upstreamPath) 387 } 388 389 upstreamResource, err := util.NewKubernetesResource(upstreamB) 390 if err == nil { 391 if _, err := scheme.Scheme.New(util.ToGroupVersionKind(upstreamResource.Id().Gvk())); err == nil { 392 upstreamMinimal := util.MinimalK8sYaml{} 393 if err := yaml.Unmarshal(upstreamB, &upstreamMinimal); err != nil { 394 return errors.Wrapf(err, "unmarshal file %s", upstreamPath) 395 } 396 397 upstreamMap[upstreamMinimal] = upstreamPath 398 } 399 } 400 } 401 402 return nil 403 } 404 405 func (l *Unforker) findMatchingUpstreamPath(upstreamMap map[util.MinimalK8sYaml]string, forkedMinimal util.MinimalK8sYaml) string { 406 for upstreamMinimal, upstreamPath := range upstreamMap { 407 kindsMatch := upstreamMinimal.Kind == forkedMinimal.Kind 408 namespacesMatch := upstreamMinimal.Metadata.Namespace == forkedMinimal.Metadata.Namespace 409 410 upstreamNameLen := len(upstreamMinimal.Metadata.Name) 411 forkedNameLen := len(forkedMinimal.Metadata.Name) 412 413 nameSuffix := strings.TrimSpace(upstreamMinimal.Metadata.Name) 414 longerName := forkedMinimal.Metadata.Name 415 if upstreamNameLen > forkedNameLen { 416 nameSuffix = strings.TrimSpace(forkedMinimal.Metadata.Name) 417 longerName = upstreamMinimal.Metadata.Name 418 } 419 420 namesMatch := strings.HasSuffix(longerName, nameSuffix) 421 if kindsMatch && namespacesMatch && namesMatch { 422 delete(upstreamMap, upstreamMinimal) 423 return upstreamPath 424 } 425 } 426 427 return "" 428 } 429 430 func containsNonGVK(data []byte) (bool, error) { 431 gvk := []string{ 432 "apiVersion", 433 "kind", 434 "metadata", 435 } 436 437 unmarshalled := make(map[string]interface{}) 438 err := yaml.Unmarshal(data, &unmarshalled) 439 if err != nil { 440 return false, errors.Wrap(err, "unmarshal patch") 441 } 442 443 keys := make([]string, 0) 444 for k := range unmarshalled { 445 keys = append(keys, k) 446 } 447 448 for key := range keys { 449 isGvk := false 450 for gvkKey := range gvk { 451 if key == gvkKey { 452 isGvk = true 453 } 454 } 455 456 if !isGvk { 457 return true, nil 458 } 459 } 460 461 return false, nil 462 }