Skip to content

Commit

Permalink
[Agent] Enable post install hooks (elastic#17241)
Browse files Browse the repository at this point in the history
[Agent] Enable post install hooks (elastic#17241)
  • Loading branch information
michalpristas committed Apr 1, 2020
1 parent 1dff73f commit 5c2eb42
Show file tree
Hide file tree
Showing 10 changed files with 408 additions and 10 deletions.
1 change: 1 addition & 0 deletions x-pack/agent/CHANGELOG.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,6 @@

- Generate index name in a format type-dataset-namespace {pull}16903[16903]
- OS agnostic default configuration {pull}17016[17016]
- Introduced post install hooks {pull}17241[17241]
- Support for config constraints {pull}17112[17112]
- Display the stability of the agent at enroll and start. {pull}17336[17336]
13 changes: 7 additions & 6 deletions x-pack/agent/pkg/agent/program/spec.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,13 @@ var ErrMissingWhen = errors.New("program must define a 'When' expression")
// NOTE: Current spec are build at compile time, we want to revisit that to allow other program
// to register their spec in a secure way.
type Spec struct {
Name string `yaml:"name"`
Cmd string `yaml:"cmd"`
Configurable string `yaml:"configurable"`
Args []string `yaml:"args"`
Rules *transpiler.RuleList `yaml:"rules"`
When string `yaml:"when"`
Name string `yaml:"name"`
Cmd string `yaml:"cmd"`
Configurable string `yaml:"configurable"`
Args []string `yaml:"args"`
Rules *transpiler.RuleList `yaml:"rules"`
PostInstallSteps *transpiler.StepList `yaml:"post_install"`
When string `yaml:"when"`
}

// ReadSpecs reads all the specs that match the provided globbing path.
Expand Down
12 changes: 12 additions & 0 deletions x-pack/agent/pkg/agent/program/spec_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ func TestSerialization(t *testing.T) {
"log",
),
),
PostInstallSteps: transpiler.NewStepList(
transpiler.DeleteFile("d-1", true),
transpiler.MoveFile("m-1", "m-2", false),
),
When: "1 == 1",
}
yml := `name: hello
Expand Down Expand Up @@ -85,6 +89,14 @@ rules:
key: type
values:
- log
post_install:
- delete_file:
path: d-1
fail_on_missing: true
- move_file:
path: m-1
target: m-2
fail_on_missing: false
when: 1 == 1
`
t.Run("serialization", func(t *testing.T) {
Expand Down
2 changes: 1 addition & 1 deletion x-pack/agent/pkg/agent/program/supported.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion x-pack/agent/pkg/agent/transpiler/rules.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ func (r *RuleList) MarshalYAML() (interface{}, error) {
return doc, nil
}

// UnmarshalYAML unmashal a YAML document into a RuleList.
// UnmarshalYAML unmarshal a YAML document into a RuleList.
func (r *RuleList) UnmarshalYAML(unmarshal func(interface{}) error) error {
var unpackTo []map[string]interface{}

Expand Down
248 changes: 248 additions & 0 deletions x-pack/agent/pkg/agent/transpiler/steps.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,248 @@
// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
// or more contributor license agreements. Licensed under the Elastic License;
// you may not use this file except in compliance with the Elastic License.

package transpiler

import (
"fmt"
"os"
"path/filepath"
"runtime"
"strings"

"gopkg.in/yaml.v2"
)

// StepList is a container that allow the same tree to be executed on multiple defined Step.
type StepList struct {
Steps []Step
}

// NewStepList returns a new list of rules to be executed.
func NewStepList(steps ...Step) *StepList {
return &StepList{Steps: steps}
}

// Step is an execution step which needs to be run.
type Step interface {
Execute(rootDir string) error
}

// Execute executes a list of steps.
func (r *StepList) Execute(rootDir string) error {
var err error
for _, step := range r.Steps {
err = step.Execute(rootDir)
if err != nil {
return err
}
}

return nil
}

// MarshalYAML marsharl a steps list to YAML.
func (r *StepList) MarshalYAML() (interface{}, error) {
doc := make([]map[string]Step, 0, len(r.Steps))

for _, step := range r.Steps {
var name string
switch step.(type) {
case *DeleteFileStep:
name = "delete_file"
case *MoveFileStep:
name = "move_file"

default:
return nil, fmt.Errorf("unknown rule of type %T", step)
}

subdoc := map[string]Step{
name: step,
}

doc = append(doc, subdoc)
}
return doc, nil
}

// UnmarshalYAML unmarshal a YAML document into a RuleList.
func (r *StepList) UnmarshalYAML(unmarshal func(interface{}) error) error {
var unpackTo []map[string]interface{}

err := unmarshal(&unpackTo)
if err != nil {
return err
}

// NOTE: this is a bit of a hack because I want to make sure
// the unpack strategy stay in the struct implementation and yaml
// doesn't have a RawMessage similar to the JSON package, so partial unpack
// is not possible.
unpack := func(in interface{}, out interface{}) error {
b, err := yaml.Marshal(in)
if err != nil {
return err
}
return yaml.Unmarshal(b, out)
}

var steps []Step

for _, m := range unpackTo {
ks := keys(m)
if len(ks) > 1 {
return fmt.Errorf("unknown rule identifier, expecting one identifier and received %d", len(ks))
}

name := ks[0]
fields := m[name]

var s Step
switch name {
case "delete_file":
s = &DeleteFileStep{}
case "move_file":
s = &MoveFileStep{}
default:
return fmt.Errorf("unknown rule of type %s", name)
}

if err := unpack(fields, s); err != nil {
return err
}

steps = append(steps, s)
}
r.Steps = steps
return nil
}

// DeleteFileStep removes a file from disk.
type DeleteFileStep struct {
Path string
// FailOnMissing fails if file is already missing
FailOnMissing bool `yaml:"fail_on_missing" config:"fail_on_missing"`
}

// Execute executes delete file step.
func (r *DeleteFileStep) Execute(rootDir string) error {
path, isSubpath := joinPaths(rootDir, r.Path)
if !isSubpath {
return fmt.Errorf("invalid path value for operation 'Delete': %s", path)
}

err := os.Remove(path)

if os.IsNotExist(err) && r.FailOnMissing {
// is not found and should be reported
return err
}

if err != nil && !os.IsNotExist(err) {
// report others
return err
}

return nil
}

// DeleteFile creates a DeleteFileStep
func DeleteFile(path string, failOnMissing bool) *DeleteFileStep {
return &DeleteFileStep{
Path: path,
FailOnMissing: failOnMissing,
}
}

// MoveFileStep moves a file to a new location.
type MoveFileStep struct {
Path string
Target string
// FailOnMissing fails if file is already missing
FailOnMissing bool `yaml:"fail_on_missing" config:"fail_on_missing"`
}

// Execute executes move file step.
func (r *MoveFileStep) Execute(rootDir string) error {
path, isSubpath := joinPaths(rootDir, r.Path)
if !isSubpath {
return fmt.Errorf("invalid path value for operation 'Move': %s", path)
}

target, isSubpath := joinPaths(rootDir, r.Target)
if !isSubpath {
return fmt.Errorf("invalid target value for operation 'Move': %s", target)
}

err := os.Rename(path, target)

if os.IsNotExist(err) && r.FailOnMissing {
// is not found and should be reported
return err
}

if err != nil && !os.IsNotExist(err) {
// report others
return err
}

return nil
}

// MoveFile creates a MoveFileStep
func MoveFile(path, target string, failOnMissing bool) *MoveFileStep {
return &MoveFileStep{
Path: path,
Target: target,
FailOnMissing: failOnMissing,
}
}

// joinPaths joins paths and returns true if path is subpath of rootDir
func joinPaths(rootDir, path string) (string, bool) {
if !filepath.IsAbs(path) {
path = filepath.Join(rootDir, path)
}

absRoot := filepath.Clean(filepath.FromSlash(rootDir))
absPath := filepath.Clean(filepath.FromSlash(path))

// path on windows are case insensitive
if !isFsCaseSensitive(rootDir) {
absRoot = strings.ToLower(absRoot)
absPath = strings.ToLower(absPath)
}

return absPath, strings.HasPrefix(absPath, absRoot)
}

func isFsCaseSensitive(rootDir string) bool {
defaultCaseSens := runtime.GOOS != "windows" && runtime.GOOS != "darwin"

dir := filepath.Dir(rootDir)
base := filepath.Base(rootDir)
// if rootdir not exist create it
if _, err := os.Stat(rootDir); os.IsNotExist(err) {
os.MkdirAll(rootDir, 0775)
defer os.RemoveAll(rootDir)
}

lowDir := filepath.Join(base, strings.ToLower(dir))
upDir := filepath.Join(base, strings.ToUpper(dir))

if _, err := os.Stat(rootDir); err != nil {
return defaultCaseSens
}

// check lower/upper dir
if _, lowErr := os.Stat(lowDir); os.IsNotExist(lowErr) {
return true
}
if _, upErr := os.Stat(upDir); os.IsNotExist(upErr) {
return true
}

return defaultCaseSens
}
66 changes: 66 additions & 0 deletions x-pack/agent/pkg/agent/transpiler/steps_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
// or more contributor license agreements. Licensed under the Elastic License;
// you may not use this file except in compliance with the Elastic License.

package transpiler

import (
"fmt"
"runtime"
"testing"

"github.com/stretchr/testify/assert"
)

func TestIsSubpath(t *testing.T) {
testCases := map[string][]struct {
root string
path string
resultPath string
isSubpath bool
}{
"linux": {
{"/", "a", "/a", true},
{"/a", "b", "/a/b", true},
{"/a", "b/c", "/a/b/c", true},
{"/a/b", "/a/c", "/a/c", false},
{"/a/b", "/a/b/../c", "/a/c", false},
{"/a/b", "../c", "/a/c", false},
{"/a", "/a/b/c", "/a/b/c", true},
{"/a", "/A/b/c", "/A/b/c", false},
},
"darwin": {
{"/", "a", "/a", true},
{"/a", "b", "/a/b", true},
{"/a", "b/c", "/a/b/c", true},
{"/a/b", "/a/c", "/a/c", false},
{"/a/b", "/a/b/../c", "/a/c", false},
{"/a/b", "../c", "/a/c", false},
{"/a", "/a/b/c", "/a/b/c", true},
{"/a", "/A/b/c", "/a/b/c", true},
},
"windows": {
{"/", "a", "\\a", true},
{"/a", "b", "\\a\\b", true},
{"/a", "b/c", "\\a\\b\\c", true},
{"/a/b", "/a/c", "\\a\\c", false},
{"/a/b", "/a/b/../c", "\\a\\c", false},
{"/a/b", "../c", "\\a\\c", false},
{"/a", "/a/b/c", "\\a\\b\\c", true},
{"/a", "/A/b/c", "\\a\\b\\c", true},
},
}

osSpecificTests, found := testCases[runtime.GOOS]
if !found {
return
}

for _, test := range osSpecificTests {
t.Run(fmt.Sprintf("[%s]'%s-%s'", runtime.GOOS, test.root, test.path), func(t *testing.T) {
newPath, result := joinPaths(test.root, test.path)
assert.Equal(t, test.resultPath, newPath)
assert.Equal(t, test.isSubpath, result)
})
}
}
Loading

0 comments on commit 5c2eb42

Please sign in to comment.