diff --git a/example-ttps/actions/change-directory/basic.yaml b/example-ttps/actions/change-directory/basic.yaml new file mode 100644 index 00000000..4e130304 --- /dev/null +++ b/example-ttps/actions/change-directory/basic.yaml @@ -0,0 +1,23 @@ +--- +api_version: 2.0 +uuid: 236f8a86-8034-43aa-a6e5-979337c375a4 +name: change_directory_example +description: | + This TTP shows you how to use the change_directory action type + to change the working directory for all future actions. If you specify cleanup + as default, the directory will be reverted during the cleanup phase. This may + be useful if any of your cleanups make assumptions about their working directory. +args: + - name: cd_destination + description: this argument is where we will try to cd to + default: /tmp +steps: + - name: "Initial directory" + inline: | + echo "Current working directory is: \"$(pwd)\"" + - name: "cd" + cd: {{.Args.cd_destination}} + cleanup: default + - name: "New directory" + inline: | + echo "Current working directory is: \"$(pwd)\"" diff --git a/pkg/blocks/changedirectory.go b/pkg/blocks/changedirectory.go new file mode 100644 index 00000000..abdc8cc1 --- /dev/null +++ b/pkg/blocks/changedirectory.go @@ -0,0 +1,117 @@ +/* +Copyright © 2024-present, Meta Platforms, Inc. and affiliates +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ + +package blocks + +import ( + "errors" + "fmt" + + "github.com/facebookincubator/ttpforge/pkg/logging" + "github.com/spf13/afero" +) + +// ChangeDirectoryStep is a step that changes the current working directory +type ChangeDirectoryStep struct { + actionDefaults `yaml:",inline"` + Cd string `yaml:"cd"` + PreviousDir string + PreviousCDStep *ChangeDirectoryStep + FileSystem afero.Fs `yaml:"-,omitempty"` +} + +// NewChangeDirectoryStep creates a new ChangeDirectoryStep instance with an initialized Act struct. +func NewChangeDirectoryStep() *ChangeDirectoryStep { + return &ChangeDirectoryStep{} +} + +// IsNil checks if a ChangeDirectoryStep is considered empty or unitializied +func (step *ChangeDirectoryStep) IsNil() bool { + return step.Cd == "" +} + +// Validate validates the ChangeDirectoryStep, checking for the necessary attributes and dependencies. +// +// **Returns:** +// +// error: error if validation fails, nil otherwise +func (step *ChangeDirectoryStep) Validate(_ TTPExecutionContext) error { + // If this has a parent cd step, hold off on validation until execute + if step.PreviousCDStep != nil { + return nil + } + + // Check if cd is provided + if step.Cd == "" { + err := errors.New("cd must be provided") + return err + } + + // Check if cd is a valid directory + fsys := step.FileSystem + if fsys == nil { + fsys = afero.NewOsFs() + } + + exists, err := afero.DirExists(fsys, step.Cd) + if err != nil { + return err + } + + if !exists { + return fmt.Errorf("directory \"%s\" does not exist", step.Cd) + } + + return nil +} + +// Execute runs the ChangeDirectoryStep, changing the current working directory and returns an error if any occur. +// +// **Returns:** +// +// ActResult: the result of the action +// error: error if execution fails, nil otherwise +func (step *ChangeDirectoryStep) Execute(ctx TTPExecutionContext) (*ActResult, error) { + // If this has a parent, then it's a cleanup step, so we need to grab the previous dir from it + if step.PreviousCDStep != nil { + if step.PreviousCDStep.PreviousDir == "" { + return nil, fmt.Errorf("no previous directory found in parent cd step") + } + step.Cd = step.PreviousCDStep.PreviousDir + } + + logging.L().Infof("Changing directory to %s", step.Cd) + + if step.Cd == "" { + return nil, fmt.Errorf("empty cd value in Execute(...)") + } + + // Set workdir to the current cd value and store the previous workdir + step.PreviousDir = ctx.Vars.WorkDir + ctx.Vars.WorkDir = step.Cd + + return &ActResult{}, nil +} + +// GetDefaultCleanupAction sets the directory back to the previous directory +func (step *ChangeDirectoryStep) GetDefaultCleanupAction() Action { + return &ChangeDirectoryStep{ + PreviousCDStep: step, + } +} diff --git a/pkg/blocks/changedirectory_test.go b/pkg/blocks/changedirectory_test.go new file mode 100644 index 00000000..174a8bc1 --- /dev/null +++ b/pkg/blocks/changedirectory_test.go @@ -0,0 +1,121 @@ +/* +Copyright © 2023-present, Meta Platforms, Inc. and affiliates +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ + +package blocks + +import ( + "testing" + + "github.com/facebookincubator/ttpforge/pkg/testutils" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestChangeDirectoryExecute(t *testing.T) { + + testCases := []struct { + name string + description string + step *ChangeDirectoryStep + fsysContents map[string][]byte + expectedError bool + startingDir string + }{ + { + name: "Change directory to valid directory", + description: "Change directory and expect successful change of workdir", + step: &ChangeDirectoryStep{ + Cd: "/tmp", + }, + fsysContents: map[string][]byte{ + "/home/testuser/test": []byte("test"), + "/tmp/test": []byte("test"), + }, + expectedError: false, + startingDir: "/home/testuser/", + }, + { + name: "Change directory to invalid directory", + description: "Try to change directory to invalid directory and expect error", + step: &ChangeDirectoryStep{ + Cd: "/doesntexist", + }, + fsysContents: map[string][]byte{ + "/home/testuser/test": []byte("test"), + "/tmp/test": []byte("test"), + }, + expectedError: true, + startingDir: "/home/testuser/", + }, + { + name: "Change directory with no given directory", + description: "Try to change directory to no directory and expect error", + step: &ChangeDirectoryStep{ + Cd: "", + }, + fsysContents: map[string][]byte{ + "/home/testuser/test": []byte("test"), + "/tmp/test": []byte("test"), + }, + expectedError: true, + startingDir: "/home/testuser/", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + // Prep filesystem + fsys, err := testutils.MakeAferoTestFs(tc.fsysContents) + require.NoError(t, err) + tc.step.FileSystem = fsys + + // validate and check error + execCtx := NewTTPExecutionContext() + execCtx.Vars.WorkDir = tc.startingDir + err = tc.step.Validate(execCtx) + + if tc.expectedError && err != nil { + require.Error(t, err) + return + } + require.NoError(t, err) + + // execute and check error + _, err = tc.step.Execute(execCtx) + + if tc.expectedError && err != nil { + require.Error(t, err) + return + } + require.NoError(t, err) + + // check current working directory + assert.Equal(t, tc.step.Cd, execCtx.Vars.WorkDir) + + // cleanup and check error + err = tc.step.GetDefaultCleanupAction().Validate(execCtx) + require.NoError(t, err) + _, err = tc.step.GetDefaultCleanupAction().Execute(execCtx) + require.NoError(t, err) + + // expect working directory to be rolled back to starting directory + assert.Equal(t, tc.startingDir, execCtx.Vars.WorkDir) + }) + } +} diff --git a/pkg/blocks/step.go b/pkg/blocks/step.go index 38f641d3..6aae35ee 100644 --- a/pkg/blocks/step.go +++ b/pkg/blocks/step.go @@ -250,6 +250,7 @@ func (s *Step) ParseAction(node *yaml.Node) (Action, error) { actionCandidates := []Action{ NewBasicStep(), + NewChangeDirectoryStep(), NewFileStep(), NewSubTTPStep(), NewEditStep(),