Repository: puppetlabs/wash
Pull request #145 (2019-03-19 22:08:30)

Embed CacheConfig in the Entry interface
This commit removes CacheConfig by embedding its methods in the Entry
interface. Doing so makes the code easier to read and maintain because
it avoids passing around a CacheConfig pointer, and it structurally
captures the fact that all entries have cacheable operations.

Additionally, this commit makes plugin.Entry a sealed interface,
which forces internal plugin authors to use plugin.NewEntry when
creating their objects.


Patch:
diff --git a/fuse/dir.go b/fuse/dir.go
index eaf4a72..07626a9 100644
--- a/fuse/dir.go
+++ b/fuse/dir.go
@@ -22,7 +22,7 @@ var _ = fs.NodeRequestLookuper(&dir{})
 var _ = fs.HandleReadDirAller(&dir{})
 
 func newDir(e plugin.Group) *dir {
-	return &dir{e, e.CacheConfig().ID()}
+	return &dir{e, e.ID()}
 }
 
 func (d *dir) Entry() plugin.Entry {
diff --git a/fuse/file.go b/fuse/file.go
index a58f62f..ef01e7c 100644
--- a/fuse/file.go
+++ b/fuse/file.go
@@ -24,7 +24,7 @@ var _ = fs.NodeGetxattrer(&file{})
 var _ = fs.NodeListxattrer(&file{})
 
 func newFile(e plugin.Entry) *file {
-	return &file{e, e.CacheConfig().ID()}
+	return &file{e, e.ID()}
 }
 
 func (f *file) Entry() plugin.Entry {
diff --git a/plugin/aws/ec2Dir.go b/plugin/aws/ec2Dir.go
index e2d3b17..ff2172c 100644
--- a/plugin/aws/ec2Dir.go
+++ b/plugin/aws/ec2Dir.go
@@ -23,7 +23,7 @@ func newEC2Dir(session *session.Session) *ec2Dir {
 		session:   session,
 		client:    ec2Client.New(session),
 	}
-	ec2Dir.CacheConfig().TurnOffCaching()
+	ec2Dir.TurnOffCaching()
 
 	ec2Dir.entries = []plugin.Entry{
 		newEC2InstancesDir(ec2Dir.session, ec2Dir.client),
diff --git a/plugin/aws/ec2Instance.go b/plugin/aws/ec2Instance.go
index 20ec394..eb884ef 100644
--- a/plugin/aws/ec2Instance.go
+++ b/plugin/aws/ec2Instance.go
@@ -73,7 +73,7 @@ func newEC2Instance(ctx context.Context, ID string, session *session.Session, cl
 		cloudwatchClient: cloudwatchlogs.New(session),
 		attr:             attr,
 	}
-	ec2Instance.CacheConfig().TurnOffCachingFor(plugin.List)
+	ec2Instance.TurnOffCachingFor(plugin.List)
 
 	ec2Instance.entries = []plugin.Entry{
 		newEC2InstanceMetadataJSON(ec2Instance),
diff --git a/plugin/aws/profile.go b/plugin/aws/profile.go
index d82563e..358fc5d 100644
--- a/plugin/aws/profile.go
+++ b/plugin/aws/profile.go
@@ -16,7 +16,7 @@ type profile struct {
 
 func newProfile(ctx context.Context, name string) (*profile, error) {
 	profile := profile{EntryBase: plugin.NewEntry(name)}
-	profile.CacheConfig().TurnOffCaching()
+	profile.TurnOffCaching()
 
 	journal.Record(ctx, "Creating a new AWS session for the %v profile", name)
 
diff --git a/plugin/aws/resourcesDir.go b/plugin/aws/resourcesDir.go
index 0d7d24c..f3e86ec 100644
--- a/plugin/aws/resourcesDir.go
+++ b/plugin/aws/resourcesDir.go
@@ -19,7 +19,7 @@ func newResourcesDir(session *session.Session) *resourcesDir {
 		EntryBase: plugin.NewEntry("resources"),
 		session:   session,
 	}
-	resourcesDir.CacheConfig().TurnOffCaching()
+	resourcesDir.TurnOffCaching()
 
 	resourcesDir.resources = []plugin.Entry{
 		newS3Dir(resourcesDir.session),
diff --git a/plugin/aws/root.go b/plugin/aws/root.go
index 1be8fb3..82fb288 100644
--- a/plugin/aws/root.go
+++ b/plugin/aws/root.go
@@ -41,7 +41,7 @@ func awsCredentialsFile() (string, error) {
 // Init for root
 func (r *Root) Init() error {
 	r.EntryBase = plugin.NewEntry("aws")
-	r.CacheConfig().SetTTLOf(plugin.List, 1*time.Minute)
+	r.SetTTLOf(plugin.List, 1*time.Minute)
 
 	return nil
 }
diff --git a/plugin/aws/s3Object.go b/plugin/aws/s3Object.go
index db78d29..e1011f9 100644
--- a/plugin/aws/s3Object.go
+++ b/plugin/aws/s3Object.go
@@ -33,7 +33,7 @@ func newS3Object(attr plugin.Attributes, metadata plugin.MetadataMap, bucket str
 		key:       key,
 		client:    client,
 	}
-	o.CacheConfig().TurnOffCachingFor(plugin.Metadata)
+	o.TurnOffCachingFor(plugin.Metadata)
 
 	return o
 }
diff --git a/plugin/cache.go b/plugin/cache.go
index 0a6ebb7..7b502da 100644
--- a/plugin/cache.go
+++ b/plugin/cache.go
@@ -2,86 +2,14 @@ package plugin
 
 import (
 	"context"
-	"flag"
 	"regexp"
 	"strings"
-	"time"
 
 	"github.com/puppetlabs/wash/datastore"
 )
 
-type cachedOp int8
-
-const (
-	// List represents Group#List
-	List cachedOp = iota
-	// Open represents Readable#Open
-	Open
-	// Metadata represents Resource#Metadata
-	Metadata
-)
-
-var cachedOpToNameMap = [3]string{"List", "Open", "Metadata"}
-
-// CacheConfig represents an entry's cache configuration
-type CacheConfig struct {
-	ttl [3]time.Duration
-	// id represents the entry's cache ID. It is set in CachedList.
-	id string
-}
-
-func newCacheConfig() *CacheConfig {
-	config := &CacheConfig{}
-
-	for op := range config.ttl {
-		config.SetTTLOf(cachedOp(op), 15*time.Second)
-	}
-
-	return config
-}
-
-func (config *CacheConfig) getTTLOf(op cachedOp) time.Duration {
-	return config.ttl[op]
-}
-
-// SetTTLOf sets the specified op's TTL
-func (config *CacheConfig) SetTTLOf(op cachedOp, ttl time.Duration) {
-	config.ttl[op] = ttl
-}
-
-// TurnOffCachingFor turns off caching for the specified op
-func (config *CacheConfig) TurnOffCachingFor(op cachedOp) {
-	config.SetTTLOf(op, -1)
-}
-
-// TurnOffCaching turns off caching for all ops
-func (config *CacheConfig) TurnOffCaching() {
-	for op := range config.ttl {
-		config.TurnOffCachingFor(cachedOp(op))
-	}
-}
-
-// ID returns the entry's cache ID
-func (config *CacheConfig) ID() string {
-	return config.id
-}
-
-// SetTestID sets the entry's cache ID for testing.
-// It can only be called by the tests.
-func (config *CacheConfig) SetTestID(id string) {
-	if notRunningTests() {
-		panic("SetTestID can be only be called by the tests")
-	}
-
-	config.id = id
-}
-
 var cache datastore.Cache
 
-func notRunningTests() bool {
-	return flag.Lookup("test.v") == nil
-}
-
 // InitCache initializes the cache
 func InitCache() {
 	if notRunningTests() {
@@ -149,11 +77,7 @@ func ClearCacheFor(path string) ([]string, error) {
 	return cache.Delete(rx), nil
 }
 
-func panicNilCacheConfig() {
-	panic("The entry's cache config is nil. Did you initialize the entry's EntryBase field with plugin.NewEntry(<name>)?")
-}
-
-func cachedOpHelper(op cachedOp, entry Entry, generateValue func() (interface{}, error)) (interface{}, error) {
+func cachedOpHelper(op cacheableOp, entry Entry, generateValue func() (interface{}, error)) (interface{}, error) {
 	if cache == nil {
 		if notRunningTests() {
 			panic("The cache was not initialized. You can initialize the cache by invoking plugin.InitCache()")
@@ -162,18 +86,13 @@ func cachedOpHelper(op cachedOp, entry Entry, generateValue func() (interface{},
 		}
 	}
 
-	config := entry.CacheConfig()
-	if config == nil {
-		panicNilCacheConfig()
-	}
-
-	ttl := config.getTTLOf(op)
+	ttl := entry.getTTLOf(op)
 	if ttl < 0 {
 		return generateValue()
 	}
 
-	opName := cachedOpToNameMap[op]
-	return cache.GetOrUpdate(opName+"::"+entry.CacheConfig().ID(), ttl, false, generateValue)
+	opName := cacheableOpToNameMap[op]
+	return cache.GetOrUpdate(opName+"::"+entry.ID(), ttl, false, generateValue)
 }
 
 // CachedList caches a Group object's List method
@@ -185,12 +104,7 @@ func CachedList(ctx context.Context, g Group) ([]Entry, error) {
 		}
 
 		for _, entry := range entries {
-			cacheConfig := entry.CacheConfig()
-			if cacheConfig == nil {
-				panicNilCacheConfig()
-			}
-
-			cacheConfig.id = g.CacheConfig().ID() + "/" + strings.Trim(entry.Name(), "/")
+			entry.setID(g.ID() + "/" + strings.Trim(entry.Name(), "/"))
 		}
 
 		return entries, nil
diff --git a/plugin/docker/root.go b/plugin/docker/root.go
index 73a9ab7..e9947d0 100644
--- a/plugin/docker/root.go
+++ b/plugin/docker/root.go
@@ -23,7 +23,7 @@ func (r *Root) Init() error {
 	}
 
 	r.EntryBase = plugin.NewEntry("docker")
-	r.CacheConfig().TurnOffCaching()
+	r.TurnOffCaching()
 	r.resources = []plugin.Entry{
 		&containers{EntryBase: plugin.NewEntry("containers"), client: dockerCli},
 		&volumes{EntryBase: plugin.NewEntry("volumes"), client: dockerCli},
diff --git a/plugin/docker/volume.go b/plugin/docker/volume.go
index 447edd8..1c678b9 100644
--- a/plugin/docker/volume.go
+++ b/plugin/docker/volume.go
@@ -37,7 +37,7 @@ func newVolume(c *client.Client, v *types.Volume) (*volume, error) {
 		client:    c,
 		startTime: startTime,
 	}
-	vol.CacheConfig().SetTTLOf(plugin.List, 60*time.Second)
+	vol.SetTTLOf(plugin.List, 60*time.Second)
 
 	return vol, nil
 }
diff --git a/plugin/entryBase.go b/plugin/entryBase.go
new file mode 100644
index 0000000..abda9b3
--- /dev/null
+++ b/plugin/entryBase.go
@@ -0,0 +1,87 @@
+package plugin
+
+import (
+	"flag"
+	"time"
+)
+
+type cacheableOp int8
+
+const (
+	// List represents Group#List
+	List cacheableOp = iota
+	// Open represents Readable#Open
+	Open
+	// Metadata represents Resource#Metadata
+	Metadata
+)
+
+var cacheableOpToNameMap = [3]string{"List", "Open", "Metadata"}
+
+// EntryBase implements Entry, making it easy to create new entries.
+// You should use plugin.NewEntry to create new EntryBase objects.
+type EntryBase struct {
+	name string
+	// id represents the entry's wash ID. It is set in CachedList.
+	id  string
+	ttl [3]time.Duration
+}
+
+// NewEntry creates a new entry
+func NewEntry(name string) EntryBase {
+	e := EntryBase{name: name}
+	for op := range e.ttl {
+		e.SetTTLOf(cacheableOp(op), 15*time.Second)
+	}
+
+	return e
+}
+
+// Name returns the entry's name.
+func (e *EntryBase) Name() string {
+	return e.name
+}
+
+// ID returns the entry's wash ID
+func (e *EntryBase) ID() string {
+	return e.id
+}
+
+// SetTTLOf sets the specified op's TTL
+func (e *EntryBase) SetTTLOf(op cacheableOp, ttl time.Duration) {
+	e.ttl[op] = ttl
+}
+
+// TurnOffCachingFor turns off caching for the specified op
+func (e *EntryBase) TurnOffCachingFor(op cacheableOp) {
+	e.SetTTLOf(op, -1)
+}
+
+// TurnOffCaching turns off caching for all ops
+func (e *EntryBase) TurnOffCaching() {
+	for op := range e.ttl {
+		e.TurnOffCachingFor(cacheableOp(op))
+	}
+}
+
+func (e *EntryBase) getTTLOf(op cacheableOp) time.Duration {
+	return e.ttl[op]
+}
+
+func (e *EntryBase) setID(id string) {
+	e.id = id
+}
+
+// SetTestID sets the entry's cache ID for testing.
+// It can only be called by the tests.
+func (e *EntryBase) SetTestID(id string) {
+	if notRunningTests() {
+		panic("SetTestID can be only be called by the tests")
+	}
+
+	e.setID(id)
+}
+
+func notRunningTests() bool {
+	return flag.Lookup("test.v") == nil
+}
diff --git a/plugin/externalPluginEntry.go b/plugin/externalPluginEntry.go
index 02b401d..7c8d512 100644
--- a/plugin/externalPluginEntry.go
+++ b/plugin/externalPluginEntry.go
@@ -59,22 +59,6 @@ type decodedCacheTTLs struct {
 	Metadata time.Duration `json:"metadata"`
 }
 
-func (c decodedCacheTTLs) toCacheConfig() *CacheConfig {
-	config := newCacheConfig()
-
-	if c.List != 0 {
-		config.SetTTLOf(List, c.List*time.Second)
-	}
-	if c.Open != 0 {
-		config.SetTTLOf(Open, c.Open*time.Second)
-	}
-	if c.Metadata != 0 {
-		config.SetTTLOf(Metadata, c.Metadata*time.Second)
-	}
-
-	return config
-}
-
 // decodedExternalPluginEntry describes a decoded serialized entry.
 type decodedExternalPluginEntry struct {
 	Name             string            `json:"name"`
@@ -98,12 +82,13 @@ func (e decodedExternalPluginEntry) toExternalPluginEntry() (*ExternalPluginEntr
 	}
 
 	entry := &ExternalPluginEntry{
-		name:             e.Name,
+		EntryBase:        NewEntry(e.Name),
 		supportedActions: e.SupportedActions,
 		state:            e.State,
 		attr:             attr,
-		cacheConfig:      e.CacheTTLs.toCacheConfig(),
 	}
+	entry.setCacheTTLs(e.CacheTTLs)
+
 	return entry, nil
 }
 
@@ -111,28 +96,28 @@ func (e decodedExternalPluginEntry) toExternalPluginEntry() (*ExternalPluginEntr
 // of its name, its object (as serialized JSON), and the path to its
 // main plugin script.
 type ExternalPluginEntry struct {
+	EntryBase
 	script           ExternalPluginScript
-	washPath         string
-	name             string
 	supportedActions []string
 	state            string
-	cacheConfig      *CacheConfig
 	attr             Attributes
 }
 
-// Name returns the entry's name
-func (e *ExternalPluginEntry) Name() string {
-	return e.name
-}
-
-// CacheConfig returns the entry's cache config
-func (e *ExternalPluginEntry) CacheConfig() *CacheConfig {
-	return e.cacheConfig
+func (e *ExternalPluginEntry) setCacheTTLs(ttls decodedCacheTTLs) {
+	if ttls.List != 0 {
+		e.SetTTLOf(List, ttls.List*time.Second)
+	}
+	if ttls.Open != 0 {
+		e.SetTTLOf(Open, ttls.Open*time.Second)
+	}
+	if ttls.Metadata != 0 {
+		e.SetTTLOf(Metadata, ttls.Metadata*time.Second)
+	}
 }
 
 // List lists the entry's children, if it has any.
 func (e *ExternalPluginEntry) List(ctx context.Context) ([]Entry, error) {
-	stdout, err := e.script.InvokeAndWait(ctx, "list", e.washPath, e.state)
+	stdout, err := e.script.InvokeAndWait(ctx, "list", e.ID(), e.state)
 	if err != nil {
 		return nil, err
 	}
@@ -156,7 +141,6 @@ func (e *ExternalPluginEntry) List(ctx context.Context) ([]Entry, error) {
 			return nil, err
 		}
 		entry.script = e.script
-		entry.washPath = e.washPath + "/" + entry.Name()
 
 		entries[i] = entry
 	}
@@ -166,7 +150,7 @@ func (e *ExternalPluginEntry) List(ctx context.Context) ([]Entry, error) {
 
 // Open returns the entry's content
 func (e *ExternalPluginEntry) Open(ctx context.Context) (SizedReader, error) {
-	stdout, err := e.script.InvokeAndWait(ctx, "read", e.washPath, e.state)
+	stdout, err := e.script.InvokeAndWait(ctx, "read", e.ID(), e.state)
 	if err != nil {
 		return nil, err
 	}
@@ -176,7 +160,7 @@ func (e *ExternalPluginEntry) Open(ctx context.Context) (SizedReader, error) {
 
 // Metadata displays the resource's metadata
 func (e *ExternalPluginEntry) Metadata(ctx context.Context) (MetadataMap, error) {
-	stdout, err := e.script.InvokeAndWait(ctx, "metadata", e.washPath, e.state)
+	stdout, err := e.script.InvokeAndWait(ctx, "metadata", e.ID(), e.state)
 	if err != nil {
 		return nil, err
 	}
@@ -239,7 +223,7 @@ func (e *ExternalPluginEntry) Stream(ctx context.Context) (io.Reader, error) {
 	cmd, stdoutR, stderrR, err := CreateCommand(
 		e.script.Path(),
 		"stream",
-		e.washPath,
+		e.ID(),
 		e.state,
 	)
 
@@ -247,7 +231,7 @@ func (e *ExternalPluginEntry) Stream(ctx context.Context) (io.Reader, error) {
 		return nil, err
 	}
 
-	cmdStr := fmt.Sprintf("%v %v %v %v", e.script.Path(), "stream", e.washPath, e.state)
+	cmdStr := fmt.Sprintf("%v %v %v %v", e.script.Path(), "stream", e.ID(), e.state)
 
 	journal.Record(ctx, "Starting command: %v", cmdStr)
 	if err := cmd.Start(); err != nil {
@@ -364,7 +348,7 @@ func (e *ExternalPluginEntry) Exec(ctx context.Context, cmd string, args []strin
 	cmdObj := exec.Command(
 		e.script.Path(),
 		append(
-			[]string{"exec", e.washPath, e.state, cmd},
+			[]string{"exec", e.ID(), e.state, cmd},
 			args...,
 		)...,
 	)
diff --git a/plugin/externalPluginRoot.go b/plugin/externalPluginRoot.go
index 6db1bfa..2de6ebe 100644
--- a/plugin/externalPluginRoot.go
+++ b/plugin/externalPluginRoot.go
@@ -57,7 +57,6 @@ func (r *ExternalPluginRoot) Init() error {
 
 	r.ExternalPluginEntry = entry
 	r.ExternalPluginEntry.script = script
-	r.ExternalPluginEntry.washPath = "/" + r.Name()
 
 	return nil
 }
diff --git a/plugin/helpers.go b/plugin/helpers.go
index ca47ef9..cf06491 100644
--- a/plugin/helpers.go
+++ b/plugin/helpers.go
@@ -18,11 +18,6 @@ import (
 // DefaultTimeout is the default timeout for prefetching
 var DefaultTimeout = 10 * time.Second
 
-// NewEntry creates a new named entry
-func NewEntry(name string) EntryBase {
-	return EntryBase{name, newCacheConfig()}
-}
-
 // ToMetadata converts an object to a metadata result. If the input is already an array of bytes, it
 // must contain a serialized JSON object. Will panic if given something besides a struct or []byte.
 func ToMetadata(obj interface{}) MetadataMap {
diff --git a/plugin/kubernetes/pvc.go b/plugin/kubernetes/pvc.go
index dff2766..c18eef3 100644
--- a/plugin/kubernetes/pvc.go
+++ b/plugin/kubernetes/pvc.go
@@ -35,7 +35,7 @@ func newPVC(pi typedv1.PersistentVolumeClaimInterface, pd typedv1.PodInterface,
 		podi:      pd,
 		startTime: p.CreationTimestamp.Time,
 	}
-	vol.CacheConfig().SetTTLOf(plugin.List, 60*time.Second)
+	vol.SetTTLOf(plugin.List, 60*time.Second)
 
 	return vol
 }
diff --git a/plugin/kubernetes/root.go b/plugin/kubernetes/root.go
index a669389..09b4700 100644
--- a/plugin/kubernetes/root.go
+++ b/plugin/kubernetes/root.go
@@ -46,7 +46,7 @@ func (r *Root) Init() error {
 	}
 
 	r.EntryBase = plugin.NewEntry("kubernetes")
-	r.CacheConfig().TurnOffCaching()
+	r.TurnOffCaching()
 
 	contexts := make([]plugin.Entry, 0)
 	for name := range raw.Contexts {
diff --git a/plugin/registry.go b/plugin/registry.go
index 915d99c..51f0dd5 100644
--- a/plugin/registry.go
+++ b/plugin/registry.go
@@ -22,8 +22,8 @@ func NewRegistry() *Registry {
 	// CachedList sets the cache IDs of the Plugin roots to
 	// "/<root_name>" (e.g. "/docker", "/kubernetes", "/aws"),
 	// and all other IDs are correctly set to <parent_id> + "/" + <name>.
-	r.CacheConfig().id = ""
-	r.CacheConfig().TurnOffCaching()
+	r.setID("")
+	r.TurnOffCaching()
 
 	return r
 }
diff --git a/plugin/types.go b/plugin/types.go
index 343fce4..e2884df 100644
--- a/plugin/types.go
+++ b/plugin/types.go
@@ -9,25 +9,19 @@ import (
 
 // ==== Wash Protocols and Resources ====
 
-// Entry is a basic named resource type.
+// Entry is a basic named resource type. It is a sealed
+// interface, meaning you must use plugin.NewEntry when
+// creating your plugin objects.
 type Entry interface {
 	Name() string
-	CacheConfig() *CacheConfig
+	ID() string
+	SetTTLOf(op cacheableOp, ttl time.Duration)
+	TurnOffCachingFor(op cacheableOp)
+	TurnOffCaching()
+	getTTLOf(op cacheableOp) time.Duration
+	setID(id string)
 }
 
-// EntryBase implements Entry, making it easy to create new entries.
-// EntryBase includes a default caching configuration.
-type EntryBase struct {
-	name        string
-	cacheConfig *CacheConfig
-}
-
-// Name returns the entry's name.
-func (e *EntryBase) Name() string { return e.name }
-
-// CacheConfig returns the entry's cache config.
-func (e *EntryBase) CacheConfig() *CacheConfig { return e.cacheConfig }
-
 // MetadataMap maps keys to arbitrary structured data.
 type MetadataMap = map[string]interface{}
 
diff --git a/volume/dir.go b/volume/dir.go
index 62db6c3..92f0410 100644
--- a/volume/dir.go
+++ b/volume/dir.go
@@ -31,7 +31,7 @@ func NewDir(name string, attr plugin.Attributes, cb ContentCB, path string, dirs
 		path:      path,
 		dirs:      dirs,
 	}
-	vd.CacheConfig().SetTTLOf(plugin.Open, 60*time.Second)
+	vd.SetTTLOf(plugin.Open, 60*time.Second)
 
 	return vd
 }
diff --git a/volume/file.go b/volume/file.go
index 2e8bb19..4879e1a 100644
--- a/volume/file.go
+++ b/volume/file.go
@@ -26,7 +26,7 @@ func NewFile(name string, attr plugin.Attributes, cb ContentCB, path string) *Fi
 		contentcb: cb,
 		path:      path,
 	}
-	vf.CacheConfig().SetTTLOf(plugin.Open, 60*time.Second)
+	vf.SetTTLOf(plugin.Open, 60*time.Second)
 
 	return vf
 }


Test patch:
diff --git a/api/cache_test.go b/api/cache_test.go
index 06a6b37..40aa342 100644
--- a/api/cache_test.go
+++ b/api/cache_test.go
@@ -90,7 +90,7 @@ func (suite *CacheHandlerTestSuite) TestRejectsGet() {
 func (suite *CacheHandlerTestSuite) TestClearCache() {
 	// Populate the cache with a mocked resource and plugin.Cached*
 	group := newMockedGroup()
-	group.CacheConfig().SetTestID("/dir")
+	group.SetTestID("/dir")
 	group.On("List", mock.Anything).Return([]plugin.Entry{}, nil)
 
 	if children, err := plugin.CachedList(context.Background(), group); suite.Nil(err) {
diff --git a/plugin/cache_test.go b/plugin/cache_test.go
index fae8903..fbb3efa 100644
--- a/plugin/cache_test.go
+++ b/plugin/cache_test.go
@@ -44,25 +44,6 @@ func (suite *CacheTestSuite) TearDownTest() {
 	UnsetTestCache()
 }
 
-func (suite *CacheTestSuite) TestDefaultCacheConfig() {
-	config := newCacheConfig()
-	assertOpTTL := func(op cachedOp, opName string, expectedTTL time.Duration) {
-		actualTTL := config.getTTLOf(op)
-		suite.Equal(
-			expectedTTL,
-			actualTTL,
-			"expected the TTL of %v to be %v, but got %v instead",
-			opName,
-			expectedTTL,
-			actualTTL,
-		)
-	}
-
-	assertOpTTL(List, "List", 15*time.Second)
-	assertOpTTL(Open, "Open", 15*time.Second)
-	assertOpTTL(Metadata, "Metadata", 15*time.Second)
-}
-
 func (suite *CacheTestSuite) opKeysRegex(path string) *regexp.Regexp {
 	rx, err := opKeysRegex(path)
 	if err != nil {
@@ -140,7 +121,7 @@ func (e *cacheTestsMockEntry) Metadata(ctx context.Context) (MetadataMap, error)
 
 type cachedOpFunc func(ctx context.Context, e Entry) (interface{}, error)
 
-func (suite *CacheTestSuite) testCachedOp(op cachedOp, opName string, mockValue interface{}, cachedOp cachedOpFunc) {
+func (suite *CacheTestSuite) testCachedOp(op cacheableOp, opName string, mockValue interface{}, cachedOp cachedOpFunc) {
 	ctx := context.Background()
 
 	// Test that cachedOp panics if the cache == nil
@@ -155,9 +136,9 @@ func (suite *CacheTestSuite) testCachedOp(op cachedOp, opName string, mockValue
 	// Test that cachedOp does _not_ call cache#GetOrUpdate for an
 	// entry that's turned off caching
 	entry := newCacheTestsMockEntry("")
-	entry.CacheConfig().SetTestID("id")
+	entry.SetTestID("id")
 	entry.On(opName, ctx).Return(mockValue, nil)
-	entry.CacheConfig().TurnOffCachingFor(op)
+	entry.TurnOffCachingFor(op)
 	v, err := cachedOp(ctx, entry)
 	if suite.NoError(err) {
 		suite.Equal(mockValue, v)
@@ -168,7 +149,7 @@ func (suite *CacheTestSuite) testCachedOp(op cachedOp, opName string, mockValue
 	// entry that's enabled caching, and that it passes-in the
 	// right arguments.
 	opTTL := 5 * time.Second
-	entry.CacheConfig().SetTTLOf(op, opTTL)
+	entry.SetTTLOf(op, opTTL)
 	entry.On(opName, ctx).Return(mockValue, nil)
 	opKey := opName + "::" + "id"
 	generateValueMatcher := func(generateValue func() (interface{}, error)) bool {
@@ -198,16 +179,8 @@ func (suite *CacheTestSuite) TestCachedList() {
 	// Set-up another mock entry for additional CachedList tests
 	ctx := context.Background()
 	entry := newCacheTestsMockEntry("parent")
-	entry.CacheConfig().SetTestID("parentID")
-	entry.CacheConfig().TurnOffCaching()
-
-	// Test that CachedList panics if the children have a nil
-	// CacheConfig()
-	mockChildren = []Entry{&cacheTestsMockEntry{}}
-	entry.On("List", ctx).Return(mockChildren, nil).Once()
-	suite.Panics(func() {
-		_, _ = CachedList(ctx, entry)
-	})
+	entry.SetTestID("parentID")
+	entry.TurnOffCaching()
 
 	// Test that CachedList sets the children's cache IDs
 	// to <parent_cache_id>/<child_name>
@@ -219,8 +192,8 @@ func (suite *CacheTestSuite) TestCachedList() {
 	children, err := CachedList(ctx, entry)
 	if suite.NoError(err) {
 		suite.Equal(mockChildren, children)
-		suite.Equal("parentID/child1", children[0].CacheConfig().ID())
-		suite.Equal("parentID/child2", children[1].CacheConfig().ID())
+		suite.Equal("parentID/child1", children[0].ID())
+		suite.Equal("parentID/child2", children[1].ID())
 	}
 }
 
diff --git a/plugin/entryBase_test.go b/plugin/entryBase_test.go
new file mode 100644
index 0000000..c6e19d9
--- /dev/null
+++ b/plugin/entryBase_test.go
@@ -0,0 +1,50 @@
+package plugin
+
+import (
+	"testing"
+	"time"
+
+	"github.com/stretchr/testify/suite"
+)
+
+type EntryBaseTestSuite struct {
+	suite.Suite
+}
+
+func (suite *EntryBaseTestSuite) TestNewEntry() {
+	e := NewEntry("foo")
+	assertOpTTL := func(op cacheableOp, opName string, expectedTTL time.Duration) {
+		actualTTL := e.getTTLOf(op)
+		suite.Equal(
+			expectedTTL,
+			actualTTL,
+			"expected the TTL of %v to be %v, but got %v instead",
+			opName,
+			expectedTTL,
+			actualTTL,
+		)
+	}
+
+	suite.Equal("foo", e.Name())
+	assertOpTTL(List, "List", 15*time.Second)
+	assertOpTTL(Open, "Open", 15*time.Second)
+	assertOpTTL(Metadata, "Metadata", 15*time.Second)
+
+	e.setID("/foo")
+	suite.Equal("/foo", e.ID())
+
+	e.SetTTLOf(List, 40*time.Second)
+	assertOpTTL(List, "List", 40*time.Second)
+
+	e.TurnOffCachingFor(List)
+	assertOpTTL(List, "List", -1)
+
+	e.TurnOffCaching()
+	assertOpTTL(Open, "Open", -1)
+	assertOpTTL(Metadata, "Metadata", -1)
+
+}
+
+func TestEntryBase(t *testing.T) {
+	suite.Run(t, new(EntryBaseTestSuite))
+}
diff --git a/plugin/externalPluginEntry_test.go b/plugin/externalPluginEntry_test.go
index 38445ae..a463b4c 100644
--- a/plugin/externalPluginEntry_test.go
+++ b/plugin/externalPluginEntry_test.go
@@ -90,20 +90,6 @@ func (suite *ExternalPluginEntryTestSuite) TestDecodeAttributes() {
 	suite.Error(err)
 }
 
-func (suite *ExternalPluginEntryTestSuite) TestDecodeCacheConfig() {
-	decodedTTLs := decodedCacheTTLs{
-		List:     10,
-		Open:     15,
-		Metadata: 20,
-	}
-
-	config := decodedTTLs.toCacheConfig()
-
-	suite.Equal(decodedTTLs.List*time.Second, config.getTTLOf(List))
-	suite.Equal(decodedTTLs.Open*time.Second, config.getTTLOf(Open))
-	suite.Equal(decodedTTLs.Metadata*time.Second, config.getTTLOf(Metadata))
-}
-
 func (suite *ExternalPluginEntryTestSuite) TestDecodeExternalPluginEntry() {
 	decodedEntry := decodedExternalPluginEntry{}
 
@@ -130,9 +116,9 @@ func (suite *ExternalPluginEntryTestSuite) TestDecodeExternalPluginEntry() {
 	decodedEntry.CacheTTLs = decodedCacheTTLs{List: 1}
 	entryWithCacheConfig, err := decodedEntry.toExternalPluginEntry()
 	if suite.NoError(err) {
-		expectedCacheConfig := newCacheConfig()
-		expectedCacheConfig.SetTTLOf(List, decodedEntry.CacheTTLs.List*time.Second)
-		suite.Equal(expectedCacheConfig, entryWithCacheConfig.CacheConfig())
+		expectedTTLs := NewEntry("").ttl
+		expectedTTLs[List] = decodedEntry.CacheTTLs.List * time.Second
+		suite.Equal(expectedTTLs, entryWithCacheConfig.EntryBase.ttl)
 	}
 
 	decodedEntry.Attributes = decodedAttributes{Size: 10}
@@ -146,14 +132,21 @@ func (suite *ExternalPluginEntryTestSuite) TestDecodeExternalPluginEntry() {
 	suite.Error(err)
 }
 
-func (suite *ExternalPluginEntryTestSuite) TestName() {
-	entry := ExternalPluginEntry{name: "foo"}
-	suite.Equal("foo", entry.Name())
-}
+func (suite *ExternalPluginEntryTestSuite) TestSetCacheTTLs() {
+	decodedTTLs := decodedCacheTTLs{
+		List:     10,
+		Open:     15,
+		Metadata: 20,
+	}
+
+	entry := ExternalPluginEntry{
+		EntryBase: NewEntry("foo"),
+	}
+	entry.setCacheTTLs(decodedTTLs)
 
-func (suite *ExternalPluginEntryTestSuite) TestCacheConfig() {
-	entry := ExternalPluginEntry{cacheConfig: newCacheConfig()}
-	suite.Equal(newCacheConfig(), entry.CacheConfig())
+	suite.Equal(decodedTTLs.List*time.Second, entry.getTTLOf(List))
+	suite.Equal(decodedTTLs.Open*time.Second, entry.getTTLOf(Open))
+	suite.Equal(decodedTTLs.Metadata*time.Second, entry.getTTLOf(Metadata))
 }
 
 // TODO: There's a bit of duplication between TestList, TestOpen,
@@ -165,13 +158,14 @@ func (suite *ExternalPluginEntryTestSuite) TestCacheConfig() {
 func (suite *ExternalPluginEntryTestSuite) TestList() {
 	mockScript := &mockExternalPluginScript{path: "plugin_script"}
 	entry := ExternalPluginEntry{
-		script:   mockScript,
-		washPath: "/foo",
+		EntryBase: NewEntry("foo"),
+		script:    mockScript,
 	}
+	entry.SetTestID("/foo")
 
 	ctx := context.Background()
 	mockInvokeAndWait := func(stdout []byte, err error) {
-		mockScript.OnInvokeAndWait(ctx, "list", entry.washPath, entry.state).Return(stdout, err).Once()
+		mockScript.OnInvokeAndWait(ctx, "list", entry.ID(), entry.state).Return(stdout, err).Once()
 	}
 
 	// Test that if InvokeAndWait errors, then List returns its error
@@ -195,10 +189,8 @@ func (suite *ExternalPluginEntryTestSuite) TestList() {
 	if suite.NoError(err) {
 		expectedEntries := []Entry{
 			&ExternalPluginEntry{
-				name:             "foo",
+				EntryBase:        NewEntry("foo"),
 				supportedActions: []string{"list"},
-				cacheConfig:      newCacheConfig(),
-				washPath:         entry.washPath + "/" + "foo",
 				script:           entry.script,
 			},
 		}
@@ -210,13 +202,14 @@ func (suite *ExternalPluginEntryTestSuite) TestList() {
 func (suite *ExternalPluginEntryTestSuite) TestOpen() {
 	mockScript := &mockExternalPluginScript{path: "plugin_script"}
 	entry := ExternalPluginEntry{
-		script:   mockScript,
-		washPath: "/foo",
+		EntryBase: NewEntry("foo"),
+		script:    mockScript,
 	}
+	entry.SetTestID("/foo")
 
 	ctx := context.Background()
 	mockInvokeAndWait := func(stdout []byte, err error) {
-		mockScript.OnInvokeAndWait(ctx, "read", entry.washPath, entry.state).Return(stdout, err).Once()
+		mockScript.OnInvokeAndWait(ctx, "read", entry.ID(), entry.state).Return(stdout, err).Once()
 	}
 
 	// Test that if InvokeAndWait errors, then Open returns its error
@@ -238,13 +231,14 @@ func (suite *ExternalPluginEntryTestSuite) TestOpen() {
 func (suite *ExternalPluginEntryTestSuite) TestMetadata() {
 	mockScript := &mockExternalPluginScript{path: "plugin_script"}
 	entry := ExternalPluginEntry{
-		script:   mockScript,
-		washPath: "/foo",
+		EntryBase: NewEntry("foo"),
+		script:    mockScript,
 	}
+	entry.SetTestID("/foo")
 
 	ctx := context.Background()
 	mockInvokeAndWait := func(stdout []byte, err error) {
-		mockScript.OnInvokeAndWait(ctx, "metadata", entry.washPath, entry.state).Return(stdout, err).Once()
+		mockScript.OnInvokeAndWait(ctx, "metadata", entry.ID(), entry.state).Return(stdout, err).Once()
 	}
 
 	// Test that if InvokeAndWait errors, then Metadata returns its error
diff --git a/plugin/externalPluginRoot_test.go b/plugin/externalPluginRoot_test.go
index a4e69bc..50bf772 100644
--- a/plugin/externalPluginRoot_test.go
+++ b/plugin/externalPluginRoot_test.go
@@ -15,8 +15,8 @@ type ExternalPluginRootTestSuite struct {
 func (suite *ExternalPluginRootTestSuite) TestInit() {
 	mockScript := &mockExternalPluginScript{path: "plugin_script"}
 	root := &ExternalPluginRoot{&ExternalPluginEntry{
-		script:   mockScript,
-		washPath: "/foo",
+		EntryBase: NewEntry("foo"),
+		script:    mockScript,
 	}}
 
 	mockInvokeAndWait := func(stdout []byte, err error) {
@@ -42,10 +42,8 @@ func (suite *ExternalPluginRootTestSuite) TestInit() {
 	if suite.NoError(err) {
 		expectedRoot := &ExternalPluginRoot{
 			ExternalPluginEntry: &ExternalPluginEntry{
-				name:             "foo",
+				EntryBase:        NewEntry("foo"),
 				supportedActions: []string{"list"},
-				cacheConfig:      newCacheConfig(),
-				washPath:         "/foo",
 				script:           root.script,
 			},
 		}
diff --git a/plugin/helpers_test.go b/plugin/helpers_test.go
index 8160fa3..568ddf9 100644
--- a/plugin/helpers_test.go
+++ b/plugin/helpers_test.go
@@ -114,7 +114,7 @@ func newHelpersTestsMockEntry() *helpersTestsMockEntry {
 		EntryBase: NewEntry("mockEntry"),
 	}
 
-	e.CacheConfig().TurnOffCaching()
+	e.TurnOffCaching()
 
 	return e
 }
@@ -134,7 +134,7 @@ func newHelpersTestsMockGroup() *helpersTestsMockGroup {
 		EntryBase: NewEntry("mockEntry"),
 	}
 
-	e.CacheConfig().TurnOffCaching()
+	e.TurnOffCaching()
 
 	return e
 }