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

fix: fix directive execution engine bug that was creating a file instead of a dir #6

Closed
Closed
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
53 changes: 53 additions & 0 deletions internal/directives/copy_directive.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package directives

import (
"context"
"errors"
"fmt"
)

func init() {
// Register the copy directive with the builtins registry.
builtins.RegisterDirective(&copyDirective{})
}

// copyDirective is a directive that copies a file or directory.
type copyDirective struct{}

// copyConfig is the configuration for the copy directive.
type copyConfig struct {
// InPath is the path to the file or directory to copy.
InPath string `json:"inPath"`
// OutPath is the path to the destination file or directory.
OutPath string `json:"outPath"`
}

// Validate validates the copy configuration, returning an error if it is invalid.
func (c *copyConfig) Validate() error {
var err []error
if c.InPath == "" {
err = append(err, errors.New("inPath is required"))
}
if c.OutPath == "" {
err = append(err, errors.New("outPath is required"))
}
return errors.Join(err...)
}

func (d *copyDirective) Name() string {
return "copy"
}

func (d *copyDirective) Run(_ context.Context, stepCtx *StepContext) (Result, error) {
cfg, err := configToStruct[copyConfig](stepCtx.Config)
if err != nil {
return ResultFailure, fmt.Errorf("could not convert config into copy config: %w", err)
}
if err = cfg.Validate(); err != nil {
return ResultFailure, fmt.Errorf("invalid copy config: %w", err)
}

// TODO: add implementation here

return ResultSuccess, nil
}
95 changes: 95 additions & 0 deletions internal/directives/directive.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package directives

import (
"context"
"encoding/json"

"k8s.io/apimachinery/pkg/runtime"
)

// StepContext is a type that represents the context in which a step is
// executed.
type StepContext struct {
// WorkDir is the root directory for the execution of a step.
WorkDir string
// SharedState is the state shared between steps.
SharedState State
// Alias is the alias of the step that is currently being executed.
Alias string
// Config is the configuration of the step that is currently being
// executed.
Config Config
}

// State is a type that represents shared state between steps.
// It is not safe for concurrent use at present, as we expect steps to
// be executed sequentially.
type State map[string]any

// Set stores a value in the shared state.
func (s State) Set(key string, value any) {
s[key] = value
}

// Get retrieves a value from the shared state.
func (s State) Get(key string) (any, bool) {
value, ok := s[key]
return value, ok
}

// Config is a map of configuration values that can be passed to a step.
// The keys and values are arbitrary, and the step is responsible for
// interpreting them.
type Config map[string]any

// DeepCopy returns a deep copy of the configuration.
func (c Config) DeepCopy() Config {
if c == nil {
return nil
}
// TODO(hidde): we piggyback on the runtime package for now, as we expect
// the configuration to originate from a Kubernetes API object. We should
// consider writing our own implementation in the future.
return runtime.DeepCopyJSON(c)
}

// Result is a type that represents the result of a Directive.
type Result string

const (
// ResultSuccess is the result of a successful directive.
ResultSuccess Result = "Success"
// ResultFailure is the result of a failed directive.
ResultFailure Result = "Failure"
)

// Directive is an interface that a directive must implement. A directive is
// a responsible for executing a specific action, and may modify the provided
// context to allow subsequent directives to access the results of its
// execution.
type Directive interface {
// Name returns the name of the directive.
Name() string
// Run executes the directive using the provided context and configuration.
Run(ctx context.Context, stepCtx *StepContext) (Result, error)
}

// configToStruct converts a Config to a (typed) configuration struct.
func configToStruct[T any](c Config) (T, error) {
var result T

// Convert the map to JSON
jsonData, err := json.Marshal(c)
if err != nil {
return result, err
}

// Unmarshal the JSON data into the struct
err = json.Unmarshal(jsonData, &result)
if err != nil {
return result, err
}

return result, nil
}

238 changes: 238 additions & 0 deletions internal/directives/directive_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,238 @@
package directives

import (
"testing"

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

func TestState_Set(t *testing.T) {
tests := []struct {
name string
setup func() State
key string
value any
expected any
}{
{
name: "Set string value",
setup: func() State { return make(State) },
key: "key1",
value: "value1",
expected: "value1",
},
{
name: "Set integer value",
setup: func() State { return make(State) },
key: "key2",
value: 42,
expected: 42,
},
{
name: "Set slice value",
setup: func() State { return make(State) },
key: "key3",
value: []string{"a", "b", "c"},
expected: []string{"a", "b", "c"},
},
{
name: "Set map value",
setup: func() State { return make(State) },
key: "key4",
value: map[string]int{"a": 1, "b": 2},
expected: map[string]int{"a": 1, "b": 2},
},
{
name: "Set nil value",
setup: func() State { return make(State) },
key: "key5",
value: nil,
expected: nil,
},
{
name: "Overwrite existing value",
setup: func() State {
s := make(State)
s["key"] = "initial_value"
return s
},
key: "key",
value: "new_value",
expected: "new_value",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
state := tt.setup()
state.Set(tt.key, tt.value)
assert.Equal(t, tt.expected, state[tt.key])
})
}
}

func TestState_Get(t *testing.T) {
tests := []struct {
name string
setup func() State
key string
expected any
exists bool
}{
{
name: "Get existing string value",
setup: func() State {
s := make(State)
s["key1"] = "value1"
return s
},
key: "key1",
expected: "value1",
exists: true,
},
{
name: "Get existing integer value",
setup: func() State {
s := make(State)
s["key2"] = 42
return s
},
key: "key2",
expected: 42,
exists: true,
},
{
name: "Get existing slice value",
setup: func() State {
s := make(State)
s["key3"] = []string{"a", "b", "c"}
return s
},
key: "key3",
expected: []string{"a", "b", "c"},
exists: true,
},
{
name: "Get existing map value",
setup: func() State {
s := make(State)
s["key4"] = map[string]int{"a": 1, "b": 2}
return s
},
key: "key4",
expected: map[string]int{"a": 1, "b": 2},
exists: true,
},
{
name: "Get existing nil value",
setup: func() State {
s := make(State)
s["key5"] = nil
return s
},
key: "key5",
expected: nil,
exists: true,
},
{
name: "Get non-existent key",
setup: func() State {
return make(State)
},
key: "non_existent",
expected: nil,
exists: false,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
state := tt.setup()
value, ok := state.Get(tt.key)

assert.Equal(t, tt.expected, value)
assert.Equal(t, tt.exists, ok)
})
}
}

func TestConfig_DeepCopy(t *testing.T) {
tests := []struct {
name string
config Config
assertions func(*testing.T, Config, Config)
}{
{
name: "nil config",
config: nil,
assertions: func(t *testing.T, _, copied Config) {
assert.Nil(t, copied, "Expected nil result for nil input")
},
},
{
name: "empty config",
config: Config{},
assertions: func(t *testing.T, original, copied Config) {
assert.Empty(t, copied, "Expected empty result for empty input")
assert.NotSame(t, original, copied, "Expected a new instance, not the same reference")
},
},
{
name: "simple config",
config: Config{
"key1": "value1",
"key2": int64(42),
"key3": true,
},
assertions: func(t *testing.T, original, copied Config) {
assert.Equal(t, original, copied, "Expected equal content")
assert.NotSame(t, original, copied, "Expected a new instance, not the same reference")

// Modify original to ensure deep copy
original["key1"] = "modified"
assert.NotEqual(t, original, copied, "Modifying original should not affect the copy")
},
},
{
name: "nested config",
config: Config{
"key1": "value1",
"key2": map[string]any{
"nested1": "nestedValue1",
"nested2": int64(99),
},
"key3": []any{int64(1), int64(2), int64(3)},
},
assertions: func(t *testing.T, original, copied Config) {
assert.Equal(t, original, copied, "Expected equal content")
assert.NotSame(t, original, copied, "Expected a new instance, not the same reference")

// Check nested map
originalNested := original["key2"].(map[string]any) // nolint: forcetypeassert
copiedNested := copied["key2"].(map[string]any) // nolint: forcetypeassert
assert.Equal(t, originalNested, copiedNested, "Expected equal nested content")
assert.NotSame(t, originalNested, copiedNested, "Expected a new instance for nested map")

// Modify original nested map
originalNested["nested1"] = "modified"
assert.NotEqual(t, originalNested, copiedNested, "Modifying original nested map should not affect the copy")

// Check slice
originalSlice := original["key3"].([]any) // nolint: forcetypeassert
copiedSlice := copied["key3"].([]any) // nolint: forcetypeassert
assert.Equal(t, originalSlice, copiedSlice, "Expected equal slice content")
assert.NotSame(t, originalSlice, copiedSlice, "Expected a new instance for slice")

// Modify original slice
originalSlice[0] = 999
assert.NotEqual(t, originalSlice, copiedSlice, "Modifying original slice should not affect the copy")
},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tt.assertions(t, tt.config, tt.config.DeepCopy())
})
}
}
Loading