Skip to content

Commit

Permalink
Add change directory step (#515)
Browse files Browse the repository at this point in the history
Summary:

Add a cd step that allowers users to change the working directory for all subsequent steps.

Reviewed By: inesusvet

Differential Revision: D63702296
  • Loading branch information
RoboticPrism authored and facebook-github-bot committed Oct 3, 2024
1 parent 1b645b8 commit ff3ac9e
Show file tree
Hide file tree
Showing 4 changed files with 262 additions and 0 deletions.
23 changes: 23 additions & 0 deletions example-ttps/actions/change-directory/basic.yaml
Original file line number Diff line number Diff line change
@@ -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)\""
117 changes: 117 additions & 0 deletions pkg/blocks/changedirectory.go
Original file line number Diff line number Diff line change
@@ -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,
}
}
121 changes: 121 additions & 0 deletions pkg/blocks/changedirectory_test.go
Original file line number Diff line number Diff line change
@@ -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)
})
}
}
1 change: 1 addition & 0 deletions pkg/blocks/step.go
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,7 @@ func (s *Step) ParseAction(node *yaml.Node) (Action, error) {

actionCandidates := []Action{
NewBasicStep(),
NewChangeDirectoryStep(),
NewFileStep(),
NewSubTTPStep(),
NewEditStep(),
Expand Down

0 comments on commit ff3ac9e

Please sign in to comment.