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  }