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

Cherry-pick #17241 to 7.x: [Agent] Enable post install hooks #17588

Merged
merged 1 commit into from
Apr 7, 2020
Merged
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
1 change: 1 addition & 0 deletions x-pack/agent/CHANGELOG.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,4 @@
- OS agnostic default configuration {pull}17016[17016]
- Display the stability of the agent at enroll and start. {pull}17336[17336]
- Support for config constraints {pull}17112[17112]
- Introduced post install hooks {pull}17241[17241]
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