diff --git a/metadata/interface.go b/metadata/interface.go
index bfa05ea8fc856f788d3d05f6ee1dea8965311e94..bc58d16ba114ffbfb78da45f94ba63046a38a5ed 100644
--- a/metadata/interface.go
+++ b/metadata/interface.go
@@ -11,11 +11,15 @@ var appFS afero.Fs
 // intent, and make code easier to read.
 type AbsPath string
 
+// AbsPaths is a slice of `AbsPath`.
+type AbsPaths []string
+
 // Manager abstracts over a ksonnet application's metadata, allowing users to do
 // things like: create and delete environments; search for prototypes; vendor
 // libraries; and other non-core-application tasks.
 type Manager interface {
 	Root() AbsPath
+	ComponentPaths() (AbsPaths, error)
 	//
 	// TODO: Fill in methods as we need them.
 	//
diff --git a/metadata/manager.go b/metadata/manager.go
index 7f60687810cb3ba2a42362a68b6d75724e6c230e..2bfb04c78525d726e38df915eb27a0e391b0a903 100644
--- a/metadata/manager.go
+++ b/metadata/manager.go
@@ -125,6 +125,25 @@ func (m *manager) Root() AbsPath {
 	return m.rootPath
 }
 
+func (m *manager) ComponentPaths() (AbsPaths, error) {
+	paths := []string{}
+	err := afero.Walk(m.appFS, string(m.componentsPath), func(path string, info os.FileInfo, err error) error {
+		if err != nil {
+			return err
+		}
+
+		if !info.IsDir() {
+			paths = append(paths, path)
+		}
+		return nil
+	})
+	if err != nil {
+		return nil, err
+	}
+
+	return paths, nil
+}
+
 func (m *manager) cacheClusterSpecData(name string, specData []byte) error {
 	envPath := string(appendToAbsPath(m.schemaDir, name))
 	err := m.appFS.MkdirAll(envPath, os.ModePerm)
diff --git a/metadata/manager_test.go b/metadata/manager_test.go
index 5dabda9ff14d45333d6c4cad62039c7dd560061a..1e591ac85ce206ee7ad320c63868d20986e56c14 100644
--- a/metadata/manager_test.go
+++ b/metadata/manager_test.go
@@ -3,6 +3,7 @@ package metadata
 import (
 	"fmt"
 	"os"
+	"sort"
 	"testing"
 
 	"github.com/spf13/afero"
@@ -125,6 +126,59 @@ func TestFindSuccess(t *testing.T) {
 	findSuccess(t, appPath, appFile)
 }
 
+func TestComponentPaths(t *testing.T) {
+	spec, err := parseClusterSpec(fmt.Sprintf("file:%s", blankSwagger), testFS)
+	if err != nil {
+		t.Fatalf("Failed to parse cluster spec: %v", err)
+	}
+
+	appPath := AbsPath("/componentPaths")
+	m, err := initManager(appPath, spec, testFS)
+	if err != nil {
+		t.Fatalf("Failed to init cluster spec: %v", err)
+	}
+
+	// Create empty app file.
+	components := appendToAbsPath(appPath, componentsDir)
+	appFile1 := appendToAbsPath(components, "component1.jsonnet")
+	f1, err := testFS.OpenFile(string(appFile1), os.O_RDONLY|os.O_CREATE, 0777)
+	if err != nil {
+		t.Fatalf("Failed to touch app file '%s'\n%v", appFile1, err)
+	}
+	f1.Close()
+
+	// Create empty file in a nested directory.
+	appSubdir := appendToAbsPath(components, "appSubdir")
+	err = testFS.MkdirAll(string(appSubdir), os.ModePerm)
+	if err != nil {
+		t.Fatalf("Failed to create directory '%s'\n%v", appSubdir, err)
+	}
+	appFile2 := appendToAbsPath(appSubdir, "component2.jsonnet")
+	f2, err := testFS.OpenFile(string(appFile2), os.O_RDONLY|os.O_CREATE, 0777)
+	if err != nil {
+		t.Fatalf("Failed to touch app file '%s'\n%v", appFile1, err)
+	}
+	f2.Close()
+
+	// Create a directory that won't be listed in the call to `ComponentPaths`.
+	unlistedDir := string(appendToAbsPath(components, "doNotListMe"))
+	err = testFS.MkdirAll(unlistedDir, os.ModePerm)
+	if err != nil {
+		t.Fatalf("Failed to create directory '%s'\n%v", unlistedDir, err)
+	}
+
+	paths, err := m.ComponentPaths()
+	if err != nil {
+		t.Fatalf("Failed to find component paths: %v", err)
+	}
+
+	sort.Slice(paths, func(i, j int) bool { return paths[i] < paths[j] })
+
+	if len(paths) != 2 || paths[0] != string(appFile2) || paths[1] != string(appFile1) {
+		t.Fatalf("m.ComponentPaths failed; expected '%s', got '%s'", []string{string(appFile1), string(appFile2)}, paths)
+	}
+}
+
 func TestFindFailure(t *testing.T) {
 	findFailure := func(t *testing.T, currDir AbsPath) {
 		_, err := findManager(currDir, testFS)