Skip to content

Commit

Permalink
If two artifacts provide the same file, re-link or re-copy that file …
Browse files Browse the repository at this point in the history
…from the existing artifact after the other was undeployed/uninstalled.
  • Loading branch information
mitchell-as committed Aug 20, 2024
1 parent d9d6352 commit 1b30be2
Show file tree
Hide file tree
Showing 2 changed files with 94 additions and 14 deletions.
13 changes: 11 additions & 2 deletions internal/smartlink/smartlink.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ func Link(src, dest string) error {
// UnlinkContents will unlink the contents of src to dest if the links exist
// WARNING: on windows smartlinks are hard links, and relating hard links back to their source is non-trivial, so instead
// we just delete the target path. If the user modified the target in any way their changes will be lost.
func UnlinkContents(src, dest string) error {
func UnlinkContents(src, dest string, ignorePaths ...string) error {
if !fileutils.DirExists(dest) {
return errs.New("dest dir does not exist: %s", dest)
}
Expand All @@ -89,6 +89,11 @@ func UnlinkContents(src, dest string) error {
return errs.Wrap(err, "Could not resolve src and dest paths")
}

ignore := make(map[string]bool)
for _, path := range ignorePaths {
ignore[path] = true
}

entries, err := os.ReadDir(src)
if err != nil {
return errs.Wrap(err, "Reading dir %s failed", dest)
Expand All @@ -101,8 +106,12 @@ func UnlinkContents(src, dest string) error {
continue
}

if _, yes := ignore[destPath]; yes {
continue
}

if fileutils.IsDir(destPath) {
if err := UnlinkContents(srcPath, destPath); err != nil {
if err := UnlinkContents(srcPath, destPath, ignorePaths...); err != nil {
return err // Not wrapping here cause it'd just repeat the same error due to the recursion
}
} else {
Expand Down
95 changes: 83 additions & 12 deletions pkg/runtime/depot.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,10 @@ type depotConfig struct {
}

type deployment struct {
Type deploymentType `json:"type"`
Path string `json:"path"`
Files []string `json:"files"`
Type deploymentType `json:"type"`
Path string `json:"path"`
Files []string `json:"files"`
InstallDir string `json:"installDir"`
}

type deploymentType string
Expand Down Expand Up @@ -187,9 +188,10 @@ func (d *depot) DeployViaLink(id strfmt.UUID, relativeSrc, absoluteDest string)
d.config.Deployments[id] = []deployment{}
}
d.config.Deployments[id] = append(d.config.Deployments[id], deployment{
Type: deploymentTypeLink,
Path: absoluteDest,
Files: files.RelativePaths(),
Type: deploymentTypeLink,
Path: absoluteDest,
Files: files.RelativePaths(),
InstallDir: relativeSrc,
})

return nil
Expand Down Expand Up @@ -243,9 +245,10 @@ func (d *depot) DeployViaCopy(id strfmt.UUID, relativeSrc, absoluteDest string)
d.config.Deployments[id] = []deployment{}
}
d.config.Deployments[id] = append(d.config.Deployments[id], deployment{
Type: deploymentTypeCopy,
Path: absoluteDest,
Files: files.RelativePaths(),
Type: deploymentTypeCopy,
Path: absoluteDest,
Files: files.RelativePaths(),
InstallDir: relativeSrc,
})

return nil
Expand All @@ -270,16 +273,44 @@ func (d *depot) Undeploy(id strfmt.UUID, relativeSrc, path string) error {
if !ok {
return errs.New("deployment for %s not found in depot", id)
}
deploy := sliceutils.Filter(deployments, func(d deployment) bool { return d.Path == path })
if len(deploy) != 1 {
deployments = sliceutils.Filter(deployments, func(d deployment) bool { return d.Path == path })
if len(deployments) != 1 {
return errs.New("no deployment found for %s in depot", path)
}

// Determine if there are any files provided by another artifact that will need to be re-linked
// or re-copied after this artifact is uninstalled.
relinks, err := d.getSharedFilesToReLink(id, relativeSrc, path)
if err != nil {
return errs.Wrap(err, "failed to get shared files")
}
sharedFiles := make([]string, 0)
for file := range relinks {
sharedFiles = append(sharedFiles, file)
}

// Perform uninstall based on deployment type
if err := smartlink.UnlinkContents(filepath.Join(d.Path(id), relativeSrc), path); err != nil {
if err := smartlink.UnlinkContents(filepath.Join(d.Path(id), relativeSrc), path, sharedFiles...); err != nil {
return errs.Wrap(err, "failed to unlink artifact")
}

// Re-link or re-copy any files provided by other artifacts.
for sharedFile, relinkSrc := range relinks {
if err := os.Remove(sharedFile); err != nil {
return errs.Wrap(err, "failed to remove file")
}
switch deployments[0].Type {
case deploymentTypeLink:
if err := smartlink.Link(relinkSrc, sharedFile); err != nil {
return errs.Wrap(err, "failed to relink file")
}
case deploymentTypeCopy:
if err := fileutils.CopyFile(relinkSrc, sharedFile); err != nil {
return errs.Wrap(err, "failed to re-copy file")
}
}
}

// Write changes to config
d.config.Deployments[id] = sliceutils.Filter(d.config.Deployments[id], func(d deployment) bool { return d.Path != path })

Expand All @@ -300,6 +331,46 @@ func (d *depot) validateVolume(absoluteDest string) error {
return nil
}

// getSharedFilesToReLink returns a map of deployed files to re-link to (or re-copy from) another
// artifact that provides those files. The key is the deployed file path and the value is the
// source path from another artifact.
func (d *depot) getSharedFilesToReLink(id strfmt.UUID, relativeSrc, path string) (map[string]string, error) {
// Map of deployed paths to other sources that provides those paths.
relink := make(map[string]string, 0)

// Get a listing of all files deployed by this artifact.
deployedDir := filepath.Join(d.Path(id), relativeSrc)
deployedFiles, err := fileutils.ListDirSimple(deployedDir, false)
if err != nil {
return nil, errs.Wrap(err, "failed to list depot files for artifact")
}

// For each of those files, find another artifact (if any) that deploys its own copy.
for _, deployedFile := range deployedFiles {
relativeDeployedFile := deployedFile[len(deployedDir)+1:]
for artifactId, artifactDeployments := range d.config.Deployments {
if artifactId == id {
continue
}

for _, deployment := range artifactDeployments {
for _, relativeDeploymentFile := range deployment.Files {
if relativeDeployedFile == relativeDeploymentFile {
// We'll want to relink this other artifact's copy after undeploying the currently deployed version.
deployedFile := filepath.Join(path, relativeDeployedFile)
newSrc := filepath.Join(d.Path(artifactId), deployment.InstallDir, relativeDeployedFile)
logging.Debug("More than one artifact provides '%s'", relativeDeployedFile)
logging.Debug("Will relink %s to %s", deployedFile, newSrc)
relink[deployedFile] = newSrc
}
}
}
}
}

return relink, nil
}

// Save will write config changes to disk (ie. links between depot artifacts and runtimes that use it).
// It will also delete any stale artifacts which are not used by any runtime.
func (d *depot) Save() error {
Expand Down

0 comments on commit 1b30be2

Please sign in to comment.