Repository: kinvolk/lokomotive
Pull request #794 (2020-08-11 18:13:51)

Refactor Terraform executor
This PR paves the way towards https://github.com/kinvolk/lokomotive/issues/716. By making `terraform.Executor` accept an arbitrary series of "steps" we can now declaratively define the infrastructure pieces a platform is composed of.

Example:

```go
steps := []terraform.ExecutionStep{
    {
        Description: "create controllers",
        Args: []string{
            "apply",
            "-auto-approve",
            fmt.Sprintf("-target=module.packet-%s.packet_device.controllers", c.ClusterName),
        },
    },
    {
        Description: "create DNS records",
        Args:        []string{"apply", "-auto-approve", "-target=module.dns"},
    },
    {
        Description: "refresh Terraform state",
        Args:        []string{"refresh"},
    },
    {
        Description:      "complete infrastructure creation",
        Args:             []string{"apply", "-auto-approve"},
        // Verify DNS entries were manually created by the user before proceeding.
        PreExecutionHook: dns.ManualConfigPrompt(&c.DNS),
    },
}
```

I've deliberately avoided refactoring all the various platforms to keep the scope of this PR narrow. I've only refactored `platform/packet` (which is our most complex platform implementation at the moment) to demonstrate how the new structure can be used. The rest of the platforms will be refactored in subsequent PRs.


Patch:
diff --git a/pkg/assets/assets.go b/pkg/assets/assets.go
index 0703bfff..7c253eef 100644
--- a/pkg/assets/assets.go
+++ b/pkg/assets/assets.go
@@ -12,8 +12,6 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-// Package assets handles Lokomotive assets. Operations such as storing files as binary data and
-// extracting files from memory to disk belong in this package.
 package assets
 
 import (
diff --git a/pkg/components/flatcar-linux-update-operator/component.go b/pkg/components/flatcar-linux-update-operator/component.go
index ae496fcf..08fcc73f 100644
--- a/pkg/components/flatcar-linux-update-operator/component.go
+++ b/pkg/components/flatcar-linux-update-operator/component.go
@@ -24,6 +24,7 @@ import (
 	"github.com/kinvolk/lokomotive/pkg/assets"
 	"github.com/kinvolk/lokomotive/pkg/components"
 	"github.com/kinvolk/lokomotive/pkg/k8sutil"
+	"github.com/kinvolk/lokomotive/pkg/util/walkers"
 )
 
 const componentName = "flatcar-linux-update-operator"
@@ -44,7 +45,7 @@ func (c *component) LoadConfig(configBody *hcl.Body, evalContext *hcl.EvalContex
 
 func (c *component) RenderManifests() (map[string]string, error) {
 	ret := make(map[string]string)
-	walk := assets.DumpingWalker(ret, ".yaml")
+	walk := walkers.DumpingWalker(ret, ".yaml")
 	if err := assets.Assets.WalkFiles(fmt.Sprintf("/components/%s/manifests", componentName), walk); err != nil {
 		return nil, errors.Wrap(err, "failed to walk assets")
 	}
diff --git a/pkg/components/util/helm.go b/pkg/components/util/helm.go
index e785d1de..d8448731 100644
--- a/pkg/components/util/helm.go
+++ b/pkg/components/util/helm.go
@@ -32,6 +32,7 @@ import (
 	"github.com/kinvolk/lokomotive/pkg/assets"
 	"github.com/kinvolk/lokomotive/pkg/components"
 	"github.com/kinvolk/lokomotive/pkg/k8sutil"
+	"github.com/kinvolk/lokomotive/pkg/util/walkers"
 )
 
 // LoadChartFromAssets takes in an asset location and returns a Helm
@@ -45,7 +46,7 @@ func LoadChartFromAssets(location string) (*chart.Chart, error) {
 
 	// Rendered files could contain secret data, only allow the
 	// current user but not others
-	walk := assets.CopyingWalker(tmpDir, 0700)
+	walk := walkers.CopyingWalker(tmpDir, 0700)
 	if err := assets.Assets.WalkFiles(location, walk); err != nil {
 		return nil, errors.Wrap(err, "walking assets")
 	}
diff --git a/pkg/dns/dns.go b/pkg/dns/dns.go
index 88603191..618279f3 100644
--- a/pkg/dns/dns.go
+++ b/pkg/dns/dns.go
@@ -15,11 +15,14 @@
 package dns
 
 import (
+	"bufio"
 	"encoding/json"
 	"fmt"
 	"net"
+	"os"
 	"reflect"
 	"sort"
+	"strings"
 
 	"github.com/kinvolk/lokomotive/pkg/terraform"
 	"github.com/pkg/errors"
@@ -61,41 +64,50 @@ func (c *Config) Validate() error {
 	return fmt.Errorf("invalid DNS provider %q", c.Provider)
 }
 
-// AskToConfigure reads the required DNS entries from a Terraform output,
-// asks the user to configure them and checks if the configuration is correct.
-func (c *Config) AskToConfigure(ex *terraform.Executor) error {
-	dnsEntries, err := readDNSEntries(ex)
-	if err != nil {
-		return err
-	}
+// ManualConfigPrompt returns a terraform.ExecutionHook which prompts the user to configure DNS
+// entries manually and verifies the entries were created successfully.
+func (c *Config) ManualConfigPrompt() terraform.ExecutionHook {
+	return func(ex *terraform.Executor) error {
+		dnsEntries, err := readDNSEntries(ex)
+		if err != nil {
+			return err
+		}
 
-	fmt.Printf("Please configure the following DNS entries at the DNS provider which hosts %q:\n", c.Zone)
-	prettyPrintDNSEntries(dnsEntries)
+		fmt.Printf("Please configure the following DNS entries at the DNS provider which hosts %q:\n", c.Zone)
+		prettyPrintDNSEntries(dnsEntries)
 
-	for {
-		fmt.Printf("Press Enter to check the entries or type \"skip\" to continue the installation: ")
+		for {
+			fmt.Printf("Press Enter to check the entries or type \"skip\" to continue the installation: ")
 
-		var input string
-		fmt.Scanln(&input)
+			var input string
 
-		if input == "skip" {
-			break
-		} else if input != "" {
-			continue
-		}
+			reader := bufio.NewReader(os.Stdin)
 
-		if checkDNSEntries(dnsEntries) {
-			break
+			input, err := reader.ReadString('\n')
+			if err != nil {
+				return fmt.Errorf("reading user input: %w", err)
+			}
+
+			v := strings.TrimSpace(input)
+			if v == "skip" {
+				break
+			} else if v != "" {
+				continue
+			}
+
+			if checkDNSEntries(dnsEntries) {
+				break
+			}
+
+			fmt.Println("Entries are not correctly configured, please verify.")
 		}
 
-		fmt.Println("Entries are not correctly configured, please verify.")
+		return nil
 	}
-
-	return nil
 }
 
 func readDNSEntries(ex *terraform.Executor) ([]dnsEntry, error) {
-	output, err := ex.ExecuteSync("output", "-json", "dns_entries")
+	output, err := ex.OutputBytes("dns_entries")
 	if err != nil {
 		return nil, errors.Wrap(err, "failed to get DNS entries")
 	}
diff --git a/pkg/install/install.go b/pkg/install/install.go
index 5448f936..f22d5441 100644
--- a/pkg/install/install.go
+++ b/pkg/install/install.go
@@ -21,6 +21,7 @@ import (
 
 	"github.com/kinvolk/lokomotive/pkg/assets"
 	"github.com/kinvolk/lokomotive/pkg/util"
+	"github.com/kinvolk/lokomotive/pkg/util/walkers"
 )
 
 // PrepareTerraformRootDir creates a directory named path including all
@@ -49,7 +50,7 @@ func PrepareTerraformRootDir(path string) error {
 // lokoctl binary or from the filesystem, depending on whether the
 // LOKOCTL_USE_FS_ASSETS environment variable was specified.
 func PrepareLokomotiveTerraformModuleAt(path string) error {
-	walk := assets.CopyingWalker(path, 0755)
+	walk := walkers.CopyingWalker(path, 0755)
 	if err := assets.Assets.WalkFiles("/lokomotive-kubernetes", walk); err != nil {
 		return errors.Wrap(err, "failed to walk assets")
 	}
diff --git a/pkg/platform/packet/packet.go b/pkg/platform/packet/packet.go
index fa14b1be..94606316 100644
--- a/pkg/platform/packet/packet.go
+++ b/pkg/platform/packet/packet.go
@@ -263,35 +263,35 @@ func (c *config) terraformSmartApply(ex *terraform.Executor, dc dns.Config) erro
 		return ex.Apply()
 	}
 
-	arguments := []string{"apply", "-auto-approve"}
-
-	// Create controllers. We need the controllers' IP addresses before we can
-	// apply the 'dns' module.
-	arguments = append(arguments, fmt.Sprintf("-target=module.packet-%s.packet_device.controllers", c.ClusterName))
-	if err := ex.Execute(arguments...); err != nil {
-		return errors.Wrap(err, "creating controllers")
-	}
-
-	// Apply 'dns' module.
-	arguments = append(arguments, "-target=module.dns")
-	if err := ex.Execute(arguments...); err != nil {
-		return errors.Wrap(err, "applying 'dns' module")
-	}
-
-	// Run `terraform refresh`. This is required in order to make the outputs from the previous
-	// apply operations available.
-	// TODO: Likely caused by https://github.com/hashicorp/terraform/issues/23158.
-	if err := ex.Execute("refresh"); err != nil {
-		return errors.Wrap(err, "refreshing")
-	}
-
-	// Prompt user to configure DNS.
-	if err := dc.AskToConfigure(ex); err != nil {
-		return errors.Wrap(err, "prompting for manual DNS configuration")
-	}
-
-	// Finish deployment.
-	return ex.Apply()
+	steps := []terraform.ExecutionStep{
+		// We need the controllers' IP addresses before we can apply the 'dns' module.
+		{
+			Description: "create controllers",
+			Args: []string{
+				"apply",
+				"-auto-approve",
+				fmt.Sprintf("-target=module.packet-%s.packet_device.controllers", c.ClusterName),
+			},
+		},
+		{
+			Description: "construct DNS records",
+			Args:        []string{"apply", "-auto-approve", "-target=module.dns"},
+		},
+		// Run `terraform refresh`. This is required in order to make the outputs from the previous
+		// apply operations available.
+		// TODO: Likely caused by https://github.com/hashicorp/terraform/issues/23158.
+		{
+			Description: "refresh Terraform state",
+			Args:        []string{"refresh"},
+		},
+		{
+			Description:      "complete infrastructure creation",
+			Args:             []string{"apply", "-auto-approve"},
+			PreExecutionHook: c.DNS.ManualConfigPrompt(),
+		},
+	}
+
+	return ex.Execute(steps...)
 }
 
 // terraformAddDeps adds explicit dependencies to cluster nodes so nodes
diff --git a/pkg/terraform/executor.go b/pkg/terraform/executor.go
index 3b559113..8fc0eb45 100644
--- a/pkg/terraform/executor.go
+++ b/pkg/terraform/executor.go
@@ -70,6 +70,34 @@ const (
 	ExecutionStatusFailure ExecutionStatus = "Failure"
 )
 
+// ExecutionHook represents a callback function which should be run prior to executing a Terraform
+// operation.
+type ExecutionHook func(*Executor) error
+
+// ExecutionStep represents a single Terraform operation.
+type ExecutionStep struct {
+	// A short string describing the step in a way that is meaningful to the user. The string
+	// should begin with a lowercase letter, be in the imperative tense and have no period at the
+	// end.
+	//
+	// Examples:
+	// - "create DNS resources"
+	// - "deploy virtual machines"
+	Description string
+	// A list of arguments to be passed to the `terraform` command. Note that for "apply"
+	// operations the "-auto-approve" argument should always be included to avoid halting the
+	// Terraform execution with interactive prompts.
+	//
+	// Examples:
+	// - []string{"apply", "-target=module.foo", "-auto-approve"}
+	// - []string{"refresh"}
+	// - []string{"apply", "-auto-approve"}
+	Args []string
+	// A function which should be run prior to executing the Terraform operation. If specified and
+	// the function returns an error, execution is halted.
+	PreExecutionHook ExecutionHook
+}
+
 // Executor enables calling Terraform from Go, across platforms, with any
 // additional providers/provisioners that the currently executing binary
 // exposes.
@@ -126,25 +154,28 @@ func NewExecutor(conf Config) (*Executor, error) {
 	return ex, nil
 }
 
-// Init() is a wrapper function that runs
-// `terraform init`.
+// Init is a wrapper function that runs `terraform init`.
 func (ex *Executor) Init() error {
-	ex.logger.Println("Initializing Terraform working directory")
-	return ex.Execute("init")
+	return ex.Execute(ExecutionStep{
+		Description: "initialize Terraform",
+		Args:        []string{"init"},
+	})
 }
 
-// Apply() is a wrapper function that runs
-// `terraform apply -auto-approve`.
+// Apply is a wrapper function that runs `terraform apply -auto-approve`.
 func (ex *Executor) Apply() error {
-	ex.logger.Println("Applying Terraform configuration. This creates infrastructure so it might take a long time...")
-	return ex.Execute("apply", "-auto-approve")
+	return ex.Execute(ExecutionStep{
+		Description: "create infrastructure",
+		Args:        []string{"apply", "-auto-approve"},
+	})
 }
 
-// Destroy() is a wrapper function that runs
-// `terraform destroy -auto-approve`.
+// Destroy is a wrapper function that runs `terraform destroy -auto-approve`.
 func (ex *Executor) Destroy() error {
-	ex.logger.Println("Destroying Terraform-managed infrastructure")
-	return ex.Execute("destroy", "-auto-approve")
+	return ex.Execute(ExecutionStep{
+		Description: "destroy infrastructure",
+		Args:        []string{"destroy", "-auto-approve"},
+	})
 }
 
 // tailFile will indefinitely tail logs from the given file path, until
@@ -176,14 +207,27 @@ func tailFile(path string, done chan struct{}, wg *sync.WaitGroup) {
 	wg.Done()
 }
 
-// Execute runs the given command and arguments against Terraform, and returns
-// any errors that occur during the execution.
-//
-// An error is returned if the Terraform binary could not be found, or if the
-// Terraform call itself failed, in which case, details can be found in the
-// output.
-func (ex *Executor) Execute(args ...string) error {
-	return ex.execute(ex.verbose, args...)
+// Execute accepts one or more ExecutionSteps and executes them sequentially in the order they were
+// provided. If a step has a PreExecutionHook defined, the hook is run prior to executing the step.
+// If any error is encountered, the error is returned and the execution is halted.
+func (ex *Executor) Execute(steps ...ExecutionStep) error {
+	for _, s := range steps {
+		if s.PreExecutionHook != nil {
+			ex.logger.Printf("Running pre-execution hook for step %q", s.Description)
+
+			if err := s.PreExecutionHook(ex); err != nil {
+				return fmt.Errorf("running pre-execution hook: %w", err)
+			}
+		}
+
+		ex.logger.Printf("Executing step %q", s.Description)
+
+		if err := ex.execute(ex.verbose, s.Args...); err != nil {
+			return err
+		}
+	}
+
+	return nil
 }
 
 func (ex *Executor) executeVerbose(args ...string) error {
@@ -191,9 +235,12 @@ func (ex *Executor) executeVerbose(args ...string) error {
 }
 
 func (ex *Executor) execute(verbose bool, args ...string) error {
-	pid, done, err := ex.ExecuteAsync(args...)
+	pid, done, err := ex.executeAsync(args...)
 	if err != nil {
-		return fmt.Errorf("failed executing Terraform command with arguments '%s' in directory %s: %w", strings.Join(args, " "), ex.WorkingDirectory(), err)
+		return fmt.Errorf(
+			"executing Terraform with arguments '%s' in directory %s: %w",
+			strings.Join(args, " "), ex.WorkingDirectory(), err,
+		)
 	}
 
 	var wg sync.WaitGroup
@@ -280,17 +327,17 @@ func (ex *Executor) LoadVars() (map[string]interface{}, error) {
 	return nil, errors.New("Could not parse config as JSON object")
 }
 
-// ExecuteAsync runs the given command and arguments against Terraform, and returns
+// executeAsync runs the given command and arguments against Terraform, and returns
 // an identifier that can be used to read the output of the process as it is
 // executed and after.
 //
-// ExecuteAsync is non-blocking, and takes a lock in the execution path.
+// This function is non-blocking, and takes a lock in the execution path.
 // Locking is handled by Terraform itself.
 //
 // An error is returned if the Terraform binary could not be found, or if the
 // Terraform call itself failed, in which case, details can be found in the
 // output.
-func (ex *Executor) ExecuteAsync(args ...string) (int, chan struct{}, error) {
+func (ex *Executor) executeAsync(args ...string) (int, chan struct{}, error) {
 	cmd := ex.generateCommand(args...)
 	rPipe, wPipe := io.Pipe()
 	cmd.Stdout = wPipe
@@ -334,8 +381,8 @@ func (ex *Executor) ExecuteAsync(args ...string) (int, chan struct{}, error) {
 	return cmd.Process.Pid, done, nil
 }
 
-// ExecuteSync is like Execute, but synchronous.
-func (ex *Executor) ExecuteSync(args ...string) ([]byte, error) {
+// executeSync is like executeAsync, but synchronous.
+func (ex *Executor) executeSync(args ...string) ([]byte, error) {
 	// Initialize the signal handler.
 	h := signalHandler(ex.logger)
 
@@ -351,7 +398,11 @@ func (ex *Executor) ExecuteSync(args ...string) ([]byte, error) {
 func (ex *Executor) Plan() error {
 	ex.logger.Println("Generating Terraform execution plan")
 
-	if err := ex.Execute("refresh"); err != nil {
+	s := ExecutionStep{
+		Description: "refresh Terraform state",
+		Args:        []string{"refresh"},
+	}
+	if err := ex.Execute(s); err != nil {
 		return err
 	}
 
@@ -361,7 +412,7 @@ func (ex *Executor) Plan() error {
 // Output gets output value from Terraform in JSON format and tries to unmarshal it
 // to a given struct.
 func (ex *Executor) Output(key string, s interface{}) error {
-	o, err := ex.ExecuteSync("output", "-json", key)
+	o, err := ex.executeSync("output", "-json", key)
 	if err != nil {
 		return fmt.Errorf("failed getting Terraform output for key %q: %w", key, err)
 	}
@@ -369,6 +420,16 @@ func (ex *Executor) Output(key string, s interface{}) error {
 	return json.Unmarshal(o, s)
 }
 
+// OutputBytes returns the value of the Terraform output key in JSON format as a byte slice.
+func (ex *Executor) OutputBytes(key string) ([]byte, error) {
+	o, err := ex.executeSync("output", "-json", key)
+	if err != nil {
+		return []byte{}, fmt.Errorf("getting Terraform output for key %q: %w", key, err)
+	}
+
+	return o, nil
+}
+
 // GenerateCommand prepares a Terraform command with the given arguments
 // by setting up the command, configuration, working directory
 // (so the files such as terraform.tfstate are stored at the right place) and
@@ -463,7 +524,7 @@ func (ex *Executor) logPath(id int) string {
 }
 
 func (ex *Executor) checkVersion() error {
-	vOutput, err := ex.ExecuteSync("--version")
+	vOutput, err := ex.executeSync("--version")
 	if err != nil {
 		return fmt.Errorf("Error checking Terraform version: %w", err)
 	}
diff --git a/pkg/assets/walkers.go b/pkg/util/walkers/copying.go
similarity index 56%
rename from pkg/assets/walkers.go
rename to pkg/util/walkers/copying.go
index 09e23d96..dd82612b 100644
--- a/pkg/assets/walkers.go
+++ b/pkg/util/walkers/copying.go
@@ -12,66 +12,35 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package assets
+package walkers
 
 import (
 	"fmt"
 	"io"
-	"io/ioutil"
 	"os"
 	"path/filepath"
+
+	"github.com/pkg/errors"
+
+	"github.com/kinvolk/lokomotive/pkg/assets"
 )
 
-// CopyingWalker returns a WalkFunc which writes the given file to disk.
-func CopyingWalker(path string, newDirPerms os.FileMode) WalkFunc {
+func CopyingWalker(path string, newDirPerms os.FileMode) assets.WalkFunc {
 	return func(fileName string, fileInfo os.FileInfo, r io.ReadSeeker, err error) error {
 		if err != nil {
-			return fmt.Errorf("error while walking at %q: %w", fileName, err)
+			return errors.Wrapf(err, "error during walking at %q", fileName)
 		}
 
 		fileName = filepath.Join(path, fileName)
 
 		if err := os.MkdirAll(filepath.Dir(fileName), newDirPerms); err != nil {
-			return fmt.Errorf("failed to create dir: %w", err)
+			return errors.Wrap(err, "failed to create dir")
 		}
 
 		return writeFile(fileName, r)
 	}
 }
 
-// DumpingWalker returns a WalkFunc which sets the contents of the given file in a map.
-func DumpingWalker(contentsMap map[string]string, allowedExts ...string) WalkFunc {
-	var extsMap map[string]struct{}
-
-	if len(allowedExts) > 0 {
-		extsMap = make(map[string]struct{}, len(allowedExts))
-		for _, ext := range allowedExts {
-			extsMap[ext] = struct{}{}
-		}
-	}
-
-	return func(fileName string, fileInfo os.FileInfo, r io.ReadSeeker, err error) error {
-		if err != nil {
-			return fmt.Errorf("error while walking at %q: %w", fileName, err)
-		}
-
-		if extsMap != nil {
-			if _, ok := extsMap[filepath.Ext(fileName)]; !ok {
-				return nil
-			}
-		}
-
-		contents, err := ioutil.ReadAll(r)
-		if err != nil {
-			return fmt.Errorf("failed to read %q: %w", fileName, err)
-		}
-
-		contentsMap[fileName] = string(contents)
-
-		return nil
-	}
-}
-
 // writeFile writes data from given io.Reader to the file and makes sure, that
 // this is the only content stored in the file.
 func writeFile(p string, r io.Reader) error {
diff --git a/pkg/util/walkers/dumping.go b/pkg/util/walkers/dumping.go
new file mode 100644
index 00000000..e0074ebc
--- /dev/null
+++ b/pkg/util/walkers/dumping.go
@@ -0,0 +1,56 @@
+// Copyright 2020 The Lokomotive Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package walkers
+
+import (
+	"io"
+	"io/ioutil"
+	"os"
+	"path/filepath"
+
+	"github.com/pkg/errors"
+
+	"github.com/kinvolk/lokomotive/pkg/assets"
+)
+
+func DumpingWalker(contentsMap map[string]string, allowedExts ...string) assets.WalkFunc {
+	var extsMap map[string]struct{}
+
+	if len(allowedExts) > 0 {
+		extsMap = make(map[string]struct{}, len(allowedExts))
+		for _, ext := range allowedExts {
+			extsMap[ext] = struct{}{}
+		}
+	}
+	return func(fileName string, fileInfo os.FileInfo, r io.ReadSeeker, err error) error {
+		if err != nil {
+			return errors.Wrapf(err, "error during walking at %q", fileName)
+		}
+
+		if extsMap != nil {
+			if _, ok := extsMap[filepath.Ext(fileName)]; !ok {
+				return nil
+			}
+		}
+
+		contents, err := ioutil.ReadAll(r)
+		if err != nil {
+			return errors.Wrapf(err, "failed to read %q", fileName)
+		}
+
+		contentsMap[fileName] = string(contents)
+		return nil
+	}
+}


Test patch:
diff --git a/pkg/terraform/executor_test.go b/pkg/terraform/executor_test.go
index ef1879fa..1baad2c6 100644
--- a/pkg/terraform/executor_test.go
+++ b/pkg/terraform/executor_test.go
@@ -33,7 +33,7 @@ func executor(t *testing.T) *Executor {
 func TestExecuteCheckErrors(t *testing.T) {
 	ex := executor(t)
 
-	if err := ex.Execute("apply"); err == nil {
+	if err := ex.Apply(); err == nil {
 		t.Fatalf("Applying on empty directory should fail")
 	}
 }
diff --git a/pkg/assets/walkers_internal_test.go b/pkg/util/walkers/copying_test.go
similarity index 99%
rename from pkg/assets/walkers_internal_test.go
rename to pkg/util/walkers/copying_test.go
index 24aea1ef..b4b2555d 100644
--- a/pkg/assets/walkers_internal_test.go
+++ b/pkg/util/walkers/copying_test.go
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package assets
+package walkers
 
 import (
 	"io/ioutil"