Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

holo-files: Support patch files (#5) #42

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
149 changes: 51 additions & 98 deletions cmd/holo-files/internal/impl/resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,118 +22,71 @@
package impl

import (
"bytes"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"

"github.com/holocm/holo/cmd/holo-files/internal/common"
)

//Resource represents a single file in $HOLO_RESOURCE_DIR. The string
//stored in it is the path to the repo file (also accessible as Path()).
type Resource string

//NewResource creates a Resource instance when its path in the file system is
//known.
func NewResource(path string) Resource {
return Resource(path)
//Resource represents a single file in $HOLO_RESOURCE_DIR.
type Resource interface {
// Path returns the path to this resource in the file system.
Path() string

// Disambiguator returns the disambiguator, i.e. the Path()
// element before the EntityPath() that disambiguates multiple
// resources for the same entity.
Disambiguator() string

// EntityPath returns the path to the corresponding entity.
EntityPath() string

// ApplicationStrategy returns the human-readable name for the
// strategy that will be employed to apply this resource.
ApplicationStrategy() string

// DiscardsPreviousBuffer indicates whether applying this
// resource will discard the previous file buffer (and thus
// the effect of all previous resources). This is used as a
// hint by the application algorithm to decide whether
// application steps can be skipped completely.
DiscardsPreviousBuffer() bool

// ApplyTo applies this Resource to a file buffer, as part of
// the `holo apply` algorithm.
ApplyTo(entityBuffer common.FileBuffer) (common.FileBuffer, error)
}

//Path returns the path to this resource in the file system.
func (resource Resource) Path() string {
return string(resource)
type rawResource struct {
path string
disambiguator string
entityPath string
}

//EntityPath returns the path to the corresponding entity.
func (resource Resource) EntityPath() string {
//the optional ".holoscript" suffix appears only on resources
path := resource.Path()
path = strings.TrimSuffix(path, ".holoscript")
func (resource rawResource) Path() string { return resource.path }
func (resource rawResource) Disambiguator() string { return resource.disambiguator }
func (resource rawResource) EntityPath() string { return resource.entityPath }

//make path relative
//NewResource creates a Resource instance when its path in the file system is
//known.
func NewResource(path string) Resource {
relPath, _ := filepath.Rel(common.ResourceDirectory(), path)
//remove the disambiguation path element to get to the relPath for the ConfigFile
//e.g. path = '/usr/share/holo/files/23-foo/etc/foo.conf'
// -> relPath = '23-foo/etc/foo.conf'
// -> relPath = 'etc/foo.conf'
segments := strings.SplitN(relPath, fmt.Sprintf("%c", filepath.Separator), 2)
relPath = segments[1]

return relPath
}

//Disambiguator returns the disambiguator, i.e. the Path() element before the
//EntityPath() that disambiguates multiple resources for the same entity.
func (resource Resource) Disambiguator() string {
//make path relative to ResourceDirectory()
relPath, _ := filepath.Rel(common.ResourceDirectory(), resource.Path())
//the disambiguator is the first path element in there
segments := strings.SplitN(relPath, fmt.Sprintf("%c", filepath.Separator), 2)
return segments[0]
}

//ApplicationStrategy returns the human-readable name for the strategy that
//will be employed to apply this repo file.
func (resource Resource) ApplicationStrategy() string {
if strings.HasSuffix(resource.Path(), ".holoscript") {
return "passthru"
segments := strings.SplitN(relPath, string(filepath.Separator), 2)
ext := filepath.Ext(segments[1])
raw := rawResource{
path: path,
disambiguator: segments[0],
entityPath: strings.TrimSuffix(segments[1], ext),
}
return "apply"
}

//DiscardsPreviousBuffer indicates whether applying this file will discard the
//previous file buffer (and thus the effect of all previous application steps).
//This is used as a hint by the application algorithm to decide whether
//application steps can be skipped completely.
func (resource Resource) DiscardsPreviousBuffer() bool {
return resource.ApplicationStrategy() == "apply"
}

//ApplyTo applies this Resource to a file buffer, as part of the `holo apply`
//algorithm. Regular repofiles will replace the file buffer, while a holoscript
//will be executed on the file buffer to obtain the new buffer.
func (resource Resource) ApplyTo(entityBuffer common.FileBuffer) (common.FileBuffer, error) {
if resource.ApplicationStrategy() == "apply" {
resourceBuffer, err := common.NewFileBuffer(resource.Path())
if err != nil {
return common.FileBuffer{}, err
}
entityBuffer.Contents = resourceBuffer.Contents
entityBuffer.Mode = (entityBuffer.Mode &^ os.ModeType) | (resourceBuffer.Mode & os.ModeType)

//since Linux disregards mode flags on symlinks and always reports 0777 perms,
//normalize the mode thusly to make FileBuffer.EqualTo() work reliably
if entityBuffer.Mode&os.ModeSymlink != 0 {
entityBuffer.Mode = os.ModeSymlink | os.ModePerm
}
return entityBuffer, nil
switch ext {
case ".holoscript":
return Holoscript{raw}
case ".patch":
return Patchfile{raw}
default:
raw.entityPath += ext
return StaticResource{raw}
}

//application of a holoscript requires file contents
entityBuffer, err := entityBuffer.ResolveSymlink()
if err != nil {
return common.FileBuffer{}, err
}

//run command, fetch result file into buffer (not into the entity
//directly, in order not to corrupt the file there if the script run fails)
var stdout bytes.Buffer
cmd := exec.Command(resource.Path())
cmd.Stdin = strings.NewReader(entityBuffer.Contents)
cmd.Stdout = &stdout
cmd.Stderr = os.Stderr
err = cmd.Run()
if err != nil {
return common.FileBuffer{}, fmt.Errorf("execution of %s failed: %s", resource.Path(), err.Error())
}

//result is the stdout of the script
entityBuffer.Mode &^= os.ModeType
entityBuffer.Contents = stdout.String()
return entityBuffer, nil
}

//Resources holds a slice of Resource instances, and implements some methods
Expand Down
111 changes: 111 additions & 0 deletions cmd/holo-files/internal/impl/resource_patch.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
/*******************************************************************************
*
* Copyright 2017-2018 Luke Shumaker <[email protected]>
*
* This file is part of Holo.
*
* Holo is free software: you can redistribute it and/or modify it under the
* terms of the GNU General Public License as published by the Free Software
* Foundation, either version 3 of the License, or (at your option) any later
* version.
*
* Holo is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
* A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with
* Holo. If not, see <http://www.gnu.org/licenses/>.
*
*******************************************************************************/

package impl

import (
"fmt"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"strings"

"github.com/holocm/holo/cmd/holo-files/internal/common"
)

// Patchfile is a Resource that is a `patch(1)` file that edits the
// current version of the entity.
type Patchfile struct{ rawResource }

// ApplicationStrategy implements the Resource interface.
func (resource Patchfile) ApplicationStrategy() string { return "patch" }

// DiscardsPreviousBuffer implements the Resource interface.
func (resource Patchfile) DiscardsPreviousBuffer() bool { return false }

// ApplyTo implements the Resource interface.
func (resource Patchfile) ApplyTo(entityBuffer common.FileBuffer) (common.FileBuffer, error) {
// `patch` requires that the file it's operating on be a real
// file (not a pipe). So, we'll write entityBuffer to a
// temporary file, run `patch`, then read it back.

// We really only normally need 1 temporary file, but:
// 1. since common.FileBuffer.Write removes the file and then
// re-creates it, that's a bit racy
// 2. The only way to limit patch to operating on a single
// file is to name that file on the command line, but
// doing that prevents it from unlinking the file, which
// prevents type changes.
//
// Using a temporary directory lets us easily work around both
// of these issues. Unfortunately, this allows the patch to
// create new files other than the one for the entity we are
// applying. However, it can't escape the temporary
// directory, so we'll just "allow" that, and document that we
// ignore those files.
targetDir, err := ioutil.TempDir(os.Getenv("HOLO_CACHE_DIR"), "patch-target.")
if err != nil {
return common.FileBuffer{}, err
}
defer os.RemoveAll(targetDir)
targetPath := filepath.Join(targetDir, filepath.Base(entityBuffer.Path))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

targetPath is a bit confusing since we already have a "target path" in our jargon at some other point. Maybe workDir and workPath, or tempDir and tempPath?


// Write entityBuffer to the temporary file
err = entityBuffer.Write(targetPath)
if err != nil {
return common.FileBuffer{}, err
}

// Run `patch` on the temporary file
patchfile, err := filepath.Abs(resource.Path())
if err != nil {
return common.FileBuffer{}, err
}
cmd := exec.Command("patch",
"-N",
"-i", patchfile,
)
cmd.Dir = targetDir
cmd.Stdout = os.Stderr
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should swallow the success-case output (patching file $filename) here, instead of just sending it to our stderr. Writing something on stderr causes Holo (the frontend) to always show the full "Working on $entity" display, even if we're not changing the entity.

Swallowing the output in the happy path would also make the tests slightly more portable since we can match and swallow both the GNU patch success message and the Busybox patch success message (and later also the BSD one).

cmd.Stderr = os.Stderr
err = cmd.Run()
if err != nil {
return common.FileBuffer{}, fmt.Errorf("execution failed: %s: %s", strings.Join(cmd.Args, " "), err.Error())
}

// Read the result back
//
// Allow `patch` to override everything but the filepath:
// - file type (changable with git-style "deleted file
// mode"/"new file mode" lines, which are implemented by at
// least GNU patch, if not in strict POSIX mode)
// - file permissions (changable with git-style "new mode"
// lines, which are implemented by at least GNU patch)
// - UID/GID (I don't know of a patch syntax that does this,
// but maybe it will exist in the future)
// - contents (obviously)
targetBuffer, err := common.NewFileBuffer(targetPath)
if err != nil {
return common.FileBuffer{}, err
}
targetBuffer.Path = entityBuffer.Path
return targetBuffer, nil
}
68 changes: 68 additions & 0 deletions cmd/holo-files/internal/impl/resource_script.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/*******************************************************************************
*
* Copyright 2015 Stefan Majewsky <[email protected]>
* Copyright 2017 Luke Shumaker <[email protected]>
*
* This file is part of Holo.
*
* Holo is free software: you can redistribute it and/or modify it under the
* terms of the GNU General Public License as published by the Free Software
* Foundation, either version 3 of the License, or (at your option) any later
* version.
*
* Holo is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
* A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with
* Holo. If not, see <http://www.gnu.org/licenses/>.
*
*******************************************************************************/

package impl

import (
"bytes"
"fmt"
"os"
"os/exec"
"strings"

"github.com/holocm/holo/cmd/holo-files/internal/common"
)

// Holoscript is a Resource that is a script that edits the current
// version of the entity.
type Holoscript struct{ rawResource }

// ApplicationStrategy implements the Resource interface.
func (resource Holoscript) ApplicationStrategy() string { return "passthru" }

// DiscardsPreviousBuffer implements the Resource interface.
func (resource Holoscript) DiscardsPreviousBuffer() bool { return false }

// ApplyTo implements the Resource interface.
func (resource Holoscript) ApplyTo(entityBuffer common.FileBuffer) (common.FileBuffer, error) {
//application of a holoscript requires file contents
entityBuffer, err := entityBuffer.ResolveSymlink()
if err != nil {
return common.FileBuffer{}, err
}

//run command, fetch result file into buffer (not into the entity
//directly, in order not to corrupt the file there if the script run fails)
var stdout bytes.Buffer
cmd := exec.Command(resource.Path())
cmd.Stdin = strings.NewReader(entityBuffer.Contents)
cmd.Stdout = &stdout
cmd.Stderr = os.Stderr
err = cmd.Run()
if err != nil {
return common.FileBuffer{}, fmt.Errorf("execution of %s failed: %s", resource.Path(), err.Error())
}

//result is the stdout of the script
entityBuffer.Mode &^= os.ModeType
entityBuffer.Contents = stdout.String()
return entityBuffer, nil
}
Loading