Repository: puppetlabs/wash
Pull request #416 (2019-07-20 03:47:20)

Namespace external plugin type IDs
This minimizes the chance of conflicting type IDs. Type IDs are
namespaced as <plugin_name>::<raw_type_id>.

Resolves https://github.com/puppetlabs/wash/issues/396

Signed-off-by: Enis Inan <enis.inan@puppet.com>


Patch:
diff --git a/plugin/entrySchema.go b/plugin/entrySchema.go
index 53bd1fe..d2308d6 100644
--- a/plugin/entrySchema.go
+++ b/plugin/entrySchema.go
@@ -47,9 +47,7 @@ func Schema(e Entry) (*EntrySchema, error) {
 				// Create a schema for root so that `stree <mountpoint>` can still display
 				// it.
 				childSchema = NewEntrySchema(root, CName(root))
-				// TODO: Namespace these to something like <root_name>::Root once
-				// https://github.com/puppetlabs/wash/issues/396 is resolved.
-				childSchema.TypeID = "__" + root.name() + "__"
+				childSchema.TypeID = namespace(root.name(), "Root")
 			}
 			childSchema.IsSingleton()
 			schema.Children = append(schema.Children, childSchema.TypeID)
diff --git a/plugin/externalPluginEntry.go b/plugin/externalPluginEntry.go
index 5a3da37..8ee8247 100644
--- a/plugin/externalPluginEntry.go
+++ b/plugin/externalPluginEntry.go
@@ -30,7 +30,6 @@ type externalPlugin interface {
 	// error-prone version of schema here, in the externalPlugin
 	// interface.
 	schema() (*EntrySchema, error)
-	// Capitalize this so it doesn't conflict with the "typeID" field.
 	TypeID() string
 }
 
@@ -75,7 +74,7 @@ func mungeToMethods(input []interface{}) (map[string]interface{}, error) {
 	return methods, nil
 }
 
-func (e decodedExternalPluginEntry) toExternalPluginEntry(schemaKnown bool, isRoot bool) (*externalPluginEntry, error) {
+func (e decodedExternalPluginEntry) toExternalPluginEntry(pluginName string, schemaKnown bool, isRoot bool) (*externalPluginEntry, error) {
 	if len(e.Name) <= 0 {
 		return nil, fmt.Errorf("the entry name must be provided")
 	}
@@ -128,7 +127,7 @@ func (e decodedExternalPluginEntry) toExternalPluginEntry(schemaKnown bool, isRo
 		methods:     methods,
 		state:       e.State,
 		schemaKnown: schemaKnown,
-		typeID:      e.TypeID,
+		typeID:      namespace(pluginName, e.TypeID),
 	}
 	entry.SetAttributes(e.Attributes)
 	entry.setCacheTTLs(e.CacheTTLs)
@@ -225,12 +224,12 @@ func (e *externalPluginEntry) schema() (*EntrySchema, error) {
 	}
 	var graph *linkedhashmap.Map
 	if e.schemaGraphs != nil {
-		g, ok := e.schemaGraphs[e.typeID]
+		g, ok := e.schemaGraphs[e.TypeID()]
 		if !ok {
 			msg := fmt.Errorf(
 				"e.Schema(): entry schemas were prefetched, but no schema graph was provided for %v (%v)",
 				ID(e),
-				e.typeID,
+				e.rawTypeID(),
 			)
 			panic(msg)
 		}
@@ -238,7 +237,7 @@ func (e *externalPluginEntry) schema() (*EntrySchema, error) {
 		// As a sanity check, ensure that the methods specified in the entry's schema
 		// match the methods specified in the entry instance. Return an error if there
 		// is a mismatch. Hopefully this should never happen.
-		es, _ := graph.Get(e.typeID)
+		es, _ := graph.Get(e.TypeID())
 		schemaMethods := es.(entrySchema).Actions
 		instanceMethods := e.supportedMethods()
 		sort.Strings(schemaMethods)
@@ -246,7 +245,7 @@ func (e *externalPluginEntry) schema() (*EntrySchema, error) {
 		mismatchErr := fmt.Errorf(
 			"%v (%v): the schema's supported methods (%v) don't match the instance's supported methods (%v)",
 			ID(e),
-			e.typeID,
+			e.rawTypeID(),
 			strings.Join(schemaMethods, ", "),
 			strings.Join(instanceMethods, ", "),
 		)
@@ -272,7 +271,7 @@ func (e *externalPluginEntry) schema() (*EntrySchema, error) {
 			err := fmt.Errorf(
 				"%v (%v): failed to retrieve the entry's schema: %v",
 				ID(e),
-				e.typeID,
+				e.rawTypeID(),
 				err,
 			)
 			return nil, err
@@ -282,7 +281,7 @@ func (e *externalPluginEntry) schema() (*EntrySchema, error) {
 			err := fmt.Errorf(
 				"%v (%v): could not decode schema from stdout: %v\nreceived:\n%v\nexpected something like:\n%v",
 				ID(e),
-				e.typeID,
+				e.rawTypeID(),
 				err,
 				strings.TrimSpace(inv.stdout.String()),
 				schemaFormat,
@@ -319,9 +318,10 @@ func (e *externalPluginEntry) List(ctx context.Context) ([]Entry, error) {
 		}
 	}
 
+	pluginName := e.pluginName()
 	entries := make([]Entry, len(decodedEntries))
 	for i, decodedExternalPluginEntry := range decodedEntries {
-		entry, err := decodedExternalPluginEntry.toExternalPluginEntry(e.schemaKnown, false)
+		entry, err := decodedExternalPluginEntry.toExternalPluginEntry(pluginName, e.schemaKnown, false)
 		if err != nil {
 			return nil, err
 		}
@@ -516,12 +516,50 @@ func newStdoutDecodeErr(ctx context.Context, decodedThing string, reason error,
 	return newInvokeError(fmt.Sprintf("could not decode %v from stdout: %v", decodedThing, reason), inv)
 }
 
+func (e *externalPluginEntry) pluginName() string {
+	pluginName, _ := e.splitTypeID()
+	return pluginName
+}
+
+func (e *externalPluginEntry) rawTypeID() string {
+	_, rawTypeID := e.splitTypeID()
+	return rawTypeID
+}
+
+// splits the type ID into a tuple (<plugin_name>, <raw_type_id>)
+func (e *externalPluginEntry) splitTypeID() (string, string) {
+	substrings := strings.SplitN(e.TypeID(), "::", 2)
+	if len(substrings) < 2 {
+		// We can hit this case if only some of the entries specify type IDs.
+		// This is impossible in the (common) entry schema scenario because
+		// entry schema support always requires type IDs. So, it is not an
+		// error.
+		//
+		// If we hit this case, then the concept of a type ID doesn't make
+		// sense so we return empty here. Note that returning substrings[0]
+		// is not a good idea b/c the plugin author could namespace their
+		// type IDs with "::", so substrings[0] would contain that initial
+		// chunk.
+		return "__unknown__", ""
+	}
+	return substrings[0], substrings[1]
+}
+
+func namespace(pluginName string, typeID string) string {
+	if len(typeID) == 0 {
+		typeID = "unknown"
+	}
+	return pluginName + "::" + typeID
+}
+
 func (e *externalPluginEntry) unmarshalSchemaGraph(stdout []byte) (*linkedhashmap.Map, error) {
+	pluginName, rawTypeID := e.splitTypeID()
+
 	// Since we know e's type ID, it is OK if the serialized schema's keys are
 	// out of order. However, the entry schema returned by the Wash API always
 	// ensures that the first key is the entry's type ID. Thus, we unmarshal the
 	// schema as a map[string]interface{} object, then convert it to a linkedhashmap
-	// object so that we ensure the "first key == e.typeID" requirement of the API.
+	// object so that we ensure the "first key == e.TypeID()" requirement of the API.
 	// We'll also validate the unmarshalled schema in the latter conversion.
 	var rawGraph map[string]interface{}
 	if err := json.Unmarshal(stdout, &rawGraph); err != nil {
@@ -530,8 +568,8 @@ func (e *externalPluginEntry) unmarshalSchemaGraph(stdout []byte) (*linkedhashma
 	if len(rawGraph) <= 0 {
 		return nil, fmt.Errorf("expected a non-empty JSON object")
 	}
-	if rawGraph[e.typeID] == nil {
-		return nil, fmt.Errorf("%v's schema is missing", e.typeID)
+	if rawGraph[rawTypeID] == nil {
+		return nil, fmt.Errorf("%v's schema is missing", rawTypeID)
 	}
 
 	// Convert each node in the rawGraph to an EntrySchema object. This step
@@ -542,7 +580,7 @@ func (e *externalPluginEntry) unmarshalSchemaGraph(stdout []byte) (*linkedhashma
 	// populatedTypeIDs/requiredTypeIDs variables.
 	populatedTypeIDs := make(map[string]bool)
 	requiredTypeIDs := map[string]bool{
-		e.typeID: true,
+		rawTypeID: true,
 	}
 	type decodedEntrySchema struct {
 		entrySchema
@@ -562,8 +600,8 @@ func (e *externalPluginEntry) unmarshalSchemaGraph(stdout []byte) (*linkedhashma
 		}
 
 		// Ensure that all required fields are present
-		node.TypeID = typeID
-		populatedTypeIDs[node.TypeID] = true
+		populatedTypeIDs[typeID] = true
+		node.TypeID = namespace(pluginName, typeID)
 		if len(node.Label) <= 0 {
 			return fmt.Errorf("a label must be provided")
 		}
@@ -584,9 +622,12 @@ func (e *externalPluginEntry) unmarshalSchemaGraph(stdout []byte) (*linkedhashma
 			if len(node.Children) <= 0 {
 				return fmt.Errorf("parent entries must specify their children's type IDs")
 			}
+			var namespacedChildren []string
 			for _, child := range node.Children {
 				requiredTypeIDs[child] = true
+				namespacedChildren = append(namespacedChildren, namespace(pluginName, child))
 			}
+			node.Children = namespacedChildren
 		}
 		if node.MetaAttributeSchema != nil && node.MetaAttributeSchema.Type.Type != "object" {
 			return fmt.Errorf("invalid value for the meta attribute schema: expected a JSON object schema but got %v", node.MetaAttributeSchema.Type.Type)
@@ -599,13 +640,13 @@ func (e *externalPluginEntry) unmarshalSchemaGraph(stdout []byte) (*linkedhashma
 		// We don't put node itself in because doing so would marshal its "Methods"
 		// field.
 		node.Actions = node.Methods
-		graph.Put(typeID, node.entrySchema)
+		graph.Put(node.TypeID, node.entrySchema)
 		return nil
 	}
-	if err := putNode(e.typeID, rawGraph[e.typeID]); err != nil {
+	if err := putNode(rawTypeID, rawGraph[rawTypeID]); err != nil {
 		return nil, err
 	}
-	delete(rawGraph, e.typeID)
+	delete(rawGraph, rawTypeID)
 	for typeID, schema := range rawGraph {
 		if err := putNode(typeID, schema); err != nil {
 			return nil, err
diff --git a/plugin/externalPluginRoot.go b/plugin/externalPluginRoot.go
index c3575c6..2015e45 100644
--- a/plugin/externalPluginRoot.go
+++ b/plugin/externalPluginRoot.go
@@ -56,7 +56,7 @@ it's safe to omit name from the response to 'init'`, r.script.Path()))
 	if decodedRoot.Methods == nil {
 		decodedRoot.Methods = []interface{}{"list"}
 	}
-	entry, err := decodedRoot.toExternalPluginEntry(false, true)
+	entry, err := decodedRoot.toExternalPluginEntry(r.Name(), false, true)
 	if err != nil {
 		return err
 	}
@@ -82,7 +82,7 @@ it's safe to omit name from the response to 'init'`, r.script.Path()))
 				schemaFormat,
 			)
 		}
-		r.schemaGraphs = partitionSchemaGraph(graph)
+		r.schemaGraphs = r.partitionSchemaGraph(graph)
 	}
 
 	return nil
@@ -95,7 +95,7 @@ func (r *externalPluginRoot) WrappedTypes() SchemaMap {
 }
 
 // partitionSchemaGraph partitions graph into a map of <type_id> => <schema_graph>
-func partitionSchemaGraph(graph *linkedhashmap.Map) map[string]*linkedhashmap.Map {
+func (r *externalPluginRoot) partitionSchemaGraph(graph *linkedhashmap.Map) map[string]*linkedhashmap.Map {
 	var populate func(*linkedhashmap.Map, entrySchema, map[string]bool)
 	populate = func(g *linkedhashmap.Map, node entrySchema, visited map[string]bool) {
 		if visited[node.TypeID] {


Test patch:
diff --git a/plugin/externalPluginEntry_test.go b/plugin/externalPluginEntry_test.go
index f8f8094..320e2f6 100644
--- a/plugin/externalPluginEntry_test.go
+++ b/plugin/externalPluginEntry_test.go
@@ -67,15 +67,15 @@ type ExternalPluginEntryTestSuite struct {
 func (suite *ExternalPluginEntryTestSuite) TestDecodeExternalPluginEntryRequiredFields() {
 	decodedEntry := decodedExternalPluginEntry{}
 
-	_, err := decodedEntry.toExternalPluginEntry(false, false)
+	_, err := decodedEntry.toExternalPluginEntry("", false, false)
 	suite.Regexp("name", err)
 	decodedEntry.Name = "decodedEntry"
 
-	_, err = decodedEntry.toExternalPluginEntry(false, false)
+	_, err = decodedEntry.toExternalPluginEntry("", false, false)
 	suite.Regexp("methods", err)
 	decodedEntry.Methods = []interface{}{"list"}
 
-	entry, err := decodedEntry.toExternalPluginEntry(false, false)
+	entry, err := decodedEntry.toExternalPluginEntry("", false, false)
 	if suite.NoError(err) {
 		suite.Equal(decodedEntry.Name, entry.name())
 		suite.Equal(1, len(entry.methods))
@@ -90,7 +90,7 @@ func (suite *ExternalPluginEntryTestSuite) TestDecodeExternalPluginEntryExtraFie
 		Methods: []interface{}{"list", "stream"},
 	}
 
-	entry, err := decodedEntry.toExternalPluginEntry(false, false)
+	entry, err := decodedEntry.toExternalPluginEntry("", false, false)
 	if suite.NoError(err) {
 		suite.Equal(decodedEntry.Name, entry.name())
 		suite.Contains(entry.methods, "list")
@@ -112,7 +112,7 @@ func (suite *ExternalPluginEntryTestSuite) TestDecodeExternalPluginEntry_Support
 		Methods: []interface{}{},
 	}
 
-	entry, err := decodedEntry.toExternalPluginEntry(false, false)
+	entry, err := decodedEntry.toExternalPluginEntry("", false, false)
 	if suite.NoError(err) {
 		suite.Equal(decodedEntry.Name, entry.name())
 	}
@@ -125,7 +125,7 @@ func (suite *ExternalPluginEntryTestSuite) TestDecodeExternalPluginEntryWithMeth
 		Methods: []interface{}{[]interface{}{"list", []interface{}{childEntry}}, "read"},
 	}
 
-	entry, err := decodedEntry.toExternalPluginEntry(false, false)
+	entry, err := decodedEntry.toExternalPluginEntry("", false, false)
 	if suite.NoError(err) {
 		suite.Equal(decodedEntry.Name, entry.name())
 		suite.Contains(entry.methods, "list")
@@ -146,7 +146,7 @@ func newMockDecodedEntry(name string) decodedExternalPluginEntry {
 func (suite *ExternalPluginEntryTestSuite) TestDecodeExternalPluginEntryWithState() {
 	decodedEntry := newMockDecodedEntry("name")
 	decodedEntry.State = "some state"
-	entry, err := decodedEntry.toExternalPluginEntry(false, false)
+	entry, err := decodedEntry.toExternalPluginEntry("", false, false)
 	if suite.NoError(err) {
 		suite.Equal(decodedEntry.State, entry.state)
 	}
@@ -155,7 +155,7 @@ func (suite *ExternalPluginEntryTestSuite) TestDecodeExternalPluginEntryWithStat
 func (suite *ExternalPluginEntryTestSuite) TestDecodeExternalPluginEntryWithCacheTTLs() {
 	decodedEntry := newMockDecodedEntry("name")
 	decodedEntry.CacheTTLs = decodedCacheTTLs{List: 1}
-	entry, err := decodedEntry.toExternalPluginEntry(false, false)
+	entry, err := decodedEntry.toExternalPluginEntry("", false, false)
 	if suite.NoError(err) {
 		expectedTTLs := NewEntry("foo").ttl
 		expectedTTLs[ListOp] = decodedEntry.CacheTTLs.List * time.Second
@@ -167,11 +167,11 @@ func (suite *ExternalPluginEntryTestSuite) TestDecodeExternalPluginEntryWithSlas
 	decodedEntry := newMockDecodedEntry("name")
 	decodedEntry.SlashReplacer = "a string"
 	suite.Panics(
-		func() { _, _ = decodedEntry.toExternalPluginEntry(false, false) },
+		func() { _, _ = decodedEntry.toExternalPluginEntry("", false, false) },
 		"e.SlashReplacer: received string a string instead of a character",
 	)
 	decodedEntry.SlashReplacer = ":"
-	entry, err := decodedEntry.toExternalPluginEntry(false, false)
+	entry, err := decodedEntry.toExternalPluginEntry("", false, false)
 	if suite.NoError(err) {
 		suite.Equal(':', entry.slashReplacer())
 	}
@@ -181,7 +181,7 @@ func (suite *ExternalPluginEntryTestSuite) TestDecodeExternalPluginEntryWithAttr
 	decodedEntry := newMockDecodedEntry("name")
 	t := time.Now()
 	decodedEntry.Attributes.SetCtime(t)
-	entry, err := decodedEntry.toExternalPluginEntry(false, false)
+	entry, err := decodedEntry.toExternalPluginEntry("", false, false)
 	if suite.NoError(err) {
 		expectedAttr := EntryAttributes{}
 		expectedAttr.SetCtime(t)
@@ -194,7 +194,7 @@ func (suite *ExternalPluginEntryTestSuite) TestDecodeExternalPluginEntryWithSche
 		Name:    "decodedEntry",
 		Methods: []interface{}{"list"},
 	}
-	entry, err := decodedEntry.toExternalPluginEntry(false, false)
+	entry, err := decodedEntry.toExternalPluginEntry("", false, false)
 	if suite.NoError(err) {
 		suite.False(entry.schemaKnown)
 	}
@@ -205,7 +205,7 @@ func (suite *ExternalPluginEntryTestSuite) TestDecodeExternalPluginEntryWithSche
 		Name:    "decodedEntry",
 		Methods: []interface{}{"list", "schema"},
 	}
-	_, err := decodedEntry.toExternalPluginEntry(false, false)
+	_, err := decodedEntry.toExternalPluginEntry("", false, false)
 	suite.Regexp("decodedEntry.*implements.*schema.*no.*type.*ID", err)
 }
 
@@ -215,7 +215,7 @@ func (suite *ExternalPluginEntryTestSuite) TestDecodeExternalPluginEntryWithSche
 		Methods: []interface{}{"list", "schema"},
 		TypeID:  "foo",
 	}
-	_, err := decodedEntry.toExternalPluginEntry(false, false)
+	_, err := decodedEntry.toExternalPluginEntry("", false, false)
 	suite.Regexp("decodedEntry.*foo.*implements.*schema.*root", err)
 }
 
@@ -225,7 +225,7 @@ func (suite *ExternalPluginEntryTestSuite) TestDecodeExternalPluginEntryWithSche
 		Methods: []interface{}{"list"},
 		TypeID:  "foo",
 	}
-	_, err := decodedEntry.toExternalPluginEntry(true, false)
+	_, err := decodedEntry.toExternalPluginEntry("", true, false)
 	suite.Regexp("decodedEntry.*foo.*must.*implement.*schema", err)
 }
 
@@ -234,7 +234,7 @@ func (suite *ExternalPluginEntryTestSuite) TestDecodeExternalPluginEntryWithSche
 		Name:    "decodedEntry",
 		Methods: []interface{}{"list", "schema"},
 	}
-	_, err := decodedEntry.toExternalPluginEntry(true, false)
+	_, err := decodedEntry.toExternalPluginEntry("", true, false)
 	suite.Regexp("decodedEntry.*implements.*schema.*no.*type.*ID", err)
 }
 
@@ -247,7 +247,7 @@ func (suite *ExternalPluginEntryTestSuite) TestDecodeExternalPluginEntryWithSche
 		},
 		TypeID: "foo",
 	}
-	_, err := decodedEntry.toExternalPluginEntry(true, false)
+	_, err := decodedEntry.toExternalPluginEntry("", true, false)
 	suite.Regexp("decodedEntry.*foo.*plugin.*roots.*support.*prefetching", err)
 }
 
@@ -257,9 +257,10 @@ func (suite *ExternalPluginEntryTestSuite) TestDecodeExternalPluginEntryWithSche
 		Methods: []interface{}{"list", "schema"},
 		TypeID:  "foo",
 	}
-	entry, err := decodedEntry.toExternalPluginEntry(true, false)
+	entry, err := decodedEntry.toExternalPluginEntry("fooPlugin", true, false)
 	if suite.NoError(err) {
 		suite.True(entry.schemaKnown)
+		suite.Equal("fooPlugin::foo", entry.typeID)
 	}
 }
 
@@ -298,7 +299,7 @@ func (suite *ExternalPluginEntryTestSuite) TestSchema_DoesNotImplementSchema_Ret
 func (suite *ExternalPluginEntryTestSuite) TestSchema_Prefetched_PanicsIfNoSchemaGraphWasProvided() {
 	entry := &externalPluginEntry{
 		EntryBase: NewEntry("foo"),
-		typeID:    "fooTypeID",
+		typeID:    "fooPlugin::fooTypeID",
 		methods: map[string]interface{}{
 			"schema": nil,
 		},
@@ -316,7 +317,7 @@ func (suite *ExternalPluginEntryTestSuite) TestSchema_Prefetched_ReturnsTheSchem
 	mockScript := &mockExternalPluginScript{path: "plugin_script"}
 	entry := &externalPluginEntry{
 		EntryBase: NewEntry("foo"),
-		typeID:    "fooTypeID",
+		typeID:    "fooPlugin::fooTypeID",
 		methods: map[string]interface{}{
 			"schema": nil,
 		},
@@ -326,16 +327,16 @@ func (suite *ExternalPluginEntryTestSuite) TestSchema_Prefetched_ReturnsTheSchem
 	entry.SetTestID("/foo")
 	graph := linkedhashmap.New()
 	graph.Put(
-		entry.typeID,
+		entry.TypeID(),
 		entrySchema{
 			Actions: []string{"schema"},
 		},
 	)
-	entry.schemaGraphs[entry.typeID] = graph
+	entry.schemaGraphs[entry.TypeID()] = graph
 
 	s, err := entry.schema()
 	if suite.NoError(err) {
-		suite.Equal(entry.schemaGraphs[entry.typeID], s.graph)
+		suite.Equal(entry.schemaGraphs[entry.TypeID()], s.graph)
 		// Make sure that Wash did not shell out to the plugin script
 		mockScript.AssertNotCalled(suite.T(), "InvokeAndWait")
 	}
@@ -345,7 +346,7 @@ func (suite *ExternalPluginEntryTestSuite) TestSchema_Prefetched_ReturnsErrorIfS
 	mockScript := &mockExternalPluginScript{path: "plugin_script"}
 	entry := &externalPluginEntry{
 		EntryBase: NewEntry("foo"),
-		typeID:    "fooTypeID",
+		typeID:    "fooPlugin::fooTypeID",
 		methods: map[string]interface{}{
 			"schema": nil,
 			"read":   nil,
@@ -356,12 +357,12 @@ func (suite *ExternalPluginEntryTestSuite) TestSchema_Prefetched_ReturnsErrorIfS
 	entry.SetTestID("/foo")
 	graph := linkedhashmap.New()
 	graph.Put(
-		entry.typeID,
+		entry.TypeID(),
 		entrySchema{
 			Actions: []string{"list", "exec"},
 		},
 	)
-	entry.schemaGraphs[entry.typeID] = graph
+	entry.schemaGraphs[entry.TypeID()] = graph
 
 	_, err := entry.schema()
 	suite.Regexp("schema.*methods.*exec.*list.*instance.*methods.*read.*schema", err)
@@ -371,7 +372,7 @@ func (suite *ExternalPluginEntryTestSuite) TestSchema_NotPrefetched_ReturnsError
 	mockScript := &mockExternalPluginScript{path: "plugin_script"}
 	entry := &externalPluginEntry{
 		EntryBase: NewEntry("foo"),
-		typeID:    "fooTypeID",
+		typeID:    "fooPlugin::fooTypeID",
 		methods: map[string]interface{}{
 			"schema": nil,
 		},
@@ -389,7 +390,7 @@ func (suite *ExternalPluginEntryTestSuite) TestSchema_NotPrefetched_ReturnsError
 	mockScript := &mockExternalPluginScript{path: "plugin_script"}
 	entry := &externalPluginEntry{
 		EntryBase: NewEntry("foo"),
-		typeID:    "fooTypeID",
+		typeID:    "fooPlugin::fooTypeID",
 		methods: map[string]interface{}{
 			"schema": nil,
 		},
@@ -407,7 +408,7 @@ func (suite *ExternalPluginEntryTestSuite) TestSchema_NotPrefetched_SuccessfulIn
 	mockScript := &mockExternalPluginScript{path: "plugin_script"}
 	entry := &externalPluginEntry{
 		EntryBase: NewEntry("foo"),
-		typeID:    "fooTypeID",
+		typeID:    "fooPlugin::baz.fooTypeID",
 		methods: map[string]interface{}{
 			"schema": nil,
 		},
@@ -417,20 +418,20 @@ func (suite *ExternalPluginEntryTestSuite) TestSchema_NotPrefetched_SuccessfulIn
 
 	stdout := `
 {
-	"fooTypeID": {
+	"baz.fooTypeID": {
 		"label": "fooEntry",
 		"methods": ["list"],
-		"children": ["barTypeID"],
+		"children": ["baz.barTypeID"],
 		"singleton": true,
 		"meta_attribute_schema": {
 			"type": "object"
 		},
 		"metadata_schema": null
 	},
-	"barTypeID": {
+	"baz.barTypeID": {
 		"label": "barEntry",
 		"methods": ["list"],
-		"children": ["barTypeID"],
+		"children": ["baz.barTypeID"],
 		"singleton": false,
 		"meta_attribute_schema": {
 			"type": "object",
@@ -450,6 +451,7 @@ func (suite *ExternalPluginEntryTestSuite) TestSchema_NotPrefetched_SuccessfulIn
 		schemaJSON, err := json.Marshal(schema)
 		if suite.NoError(err) {
 			stdout = strings.ReplaceAll(stdout, "methods", "actions")
+			stdout = strings.ReplaceAll(stdout, "baz.", "fooPlugin::baz.")
 			suite.JSONEq(stdout, string(schemaJSON))
 		}
 	}
@@ -463,6 +465,7 @@ func (suite *ExternalPluginEntryTestSuite) TestList() {
 		schemaGraphs: map[string]*linkedhashmap.Map{
 			"foo": linkedhashmap.New(),
 		},
+		typeID: "fooPlugin::foo_type",
 	}
 	entry.SetTestID("/foo")
 
@@ -485,7 +488,7 @@ func (suite *ExternalPluginEntryTestSuite) TestList() {
 
 	// Test that List properly decodes the entries from stdout
 	stdout := "[" +
-		"{\"name\":\"foo\",\"methods\":[\"list\"]}" +
+		"{\"name\":\"foo\",\"methods\":[\"list\"],\"type_id\":\"bar\"}" +
 		"]"
 	mockInvokeAndWait([]byte(stdout), nil)
 	entries, err := entry.List(ctx)
@@ -497,6 +500,7 @@ func (suite *ExternalPluginEntryTestSuite) TestList() {
 				methods:      map[string]interface{}{"list": nil},
 				script:       entry.script,
 				schemaGraphs: entry.schemaGraphs,
+				typeID:       "fooPlugin::bar",
 			},
 		}
 
@@ -690,7 +694,7 @@ func (suite *ExternalPluginEntryTestSuite) TestUnmarshalSchemaGraph_ErrorsIfAnEm
 
 func (suite *ExternalPluginEntryTestSuite) TestUnmarshalSchemaGraph_ErrorsIfTypeIDNotPresent() {
 	entry := &externalPluginEntry{
-		typeID: "foo",
+		typeID: "fooPlugin::foo",
 	}
 	stdout := []byte(`
 {
@@ -702,7 +706,7 @@ func (suite *ExternalPluginEntryTestSuite) TestUnmarshalSchemaGraph_ErrorsIfType
 
 func (suite *ExternalPluginEntryTestSuite) TestUnmarshalSchemaGraph_ErrorsOnMalformedSchema() {
 	entry := &externalPluginEntry{
-		typeID: "foo",
+		typeID: "fooPlugin::foo",
 	}
 
 	// Error should indicate that foo's schema is not a JSON object.
@@ -742,7 +746,7 @@ func (suite *ExternalPluginEntryTestSuite) TestUnmarshalSchemaGraph_ErrorsOnMalf
 
 func (suite *ExternalPluginEntryTestSuite) TestUnmarshalSchemaGraph_ErrorsIfLabelNotProvided() {
 	entry := &externalPluginEntry{
-		typeID: "foo",
+		typeID: "fooPlugin::foo",
 	}
 
 	stdout := []byte(`
@@ -756,7 +760,7 @@ func (suite *ExternalPluginEntryTestSuite) TestUnmarshalSchemaGraph_ErrorsIfLabe
 
 func (suite *ExternalPluginEntryTestSuite) TestUnmarshalSchemaGraph_ErrorsIfMethodsNotProvided() {
 	entry := &externalPluginEntry{
-		typeID: "foo",
+		typeID: "fooPlugin::foo",
 	}
 
 	stdout := []byte(`
@@ -772,7 +776,7 @@ func (suite *ExternalPluginEntryTestSuite) TestUnmarshalSchemaGraph_ErrorsIfMeth
 
 func (suite *ExternalPluginEntryTestSuite) TestUnmarshalSchemaGraph_ErrorsIfNotParentAndChildrenProvided() {
 	entry := &externalPluginEntry{
-		typeID: "foo",
+		typeID: "fooPlugin::foo",
 	}
 
 	stdout := []byte(`
@@ -790,7 +794,7 @@ func (suite *ExternalPluginEntryTestSuite) TestUnmarshalSchemaGraph_ErrorsIfNotP
 
 func (suite *ExternalPluginEntryTestSuite) TestUnmarshalSchemaGraph_ErrorsIfParentAndChildrenNotProvided() {
 	entry := &externalPluginEntry{
-		typeID: "foo",
+		typeID: "fooPlugin::foo",
 	}
 
 	stdout := []byte(`
@@ -808,7 +812,7 @@ func (suite *ExternalPluginEntryTestSuite) TestUnmarshalSchemaGraph_ErrorsIfPare
 
 func (suite *ExternalPluginEntryTestSuite) TestUnmarshalSchemaGraph_ErrorsIfMissingChildSchema() {
 	entry := &externalPluginEntry{
-		typeID: "foo",
+		typeID: "fooPlugin::foo",
 	}
 
 	stdout := []byte(`
@@ -826,7 +830,7 @@ func (suite *ExternalPluginEntryTestSuite) TestUnmarshalSchemaGraph_ErrorsIfMiss
 
 func (suite *ExternalPluginEntryTestSuite) TestUnmarshalSchemaGraph_ErrorsIfSchemaIncludesDanglingTypeIDs() {
 	entry := &externalPluginEntry{
-		typeID: "foo",
+		typeID: "fooPlugin::foo",
 	}
 
 	stdout := []byte(`
@@ -856,7 +860,7 @@ func (suite *ExternalPluginEntryTestSuite) TestUnmarshalSchemaGraph_ErrorsIfSche
 
 func (suite *ExternalPluginEntryTestSuite) TestUnmarshalSchemaGraph_ErrorsOnInvalidMetaAttributeSchema() {
 	entry := &externalPluginEntry{
-		typeID: "foo",
+		typeID: "fooPlugin::foo",
 	}
 
 	stdout := []byte(`
@@ -876,7 +880,7 @@ func (suite *ExternalPluginEntryTestSuite) TestUnmarshalSchemaGraph_ErrorsOnInva
 
 func (suite *ExternalPluginEntryTestSuite) TestUnmarshalSchemaGraph_ErrorsOnInvalidMetadataSchema() {
 	entry := &externalPluginEntry{
-		typeID: "foo",
+		typeID: "fooPlugin::foo",
 	}
 
 	stdout := []byte(`
@@ -901,17 +905,18 @@ func (suite *ExternalPluginEntryTestSuite) TestUnmarshalSchemaGraph_ValidInput()
 	stdout := readSchemaFixture(suite.Suite, "externalPluginSchema")
 
 	entry := &externalPluginEntry{
-		typeID: "aws.Root",
+		typeID: "fooPlugin::aws.Root",
 	}
 	actualGraph, err := entry.unmarshalSchemaGraph(stdout)
 	if suite.NoError(err) {
 		// Check that the first key is aws.Root
 		it := actualGraph.Iterator()
 		it.First()
-		suite.Equal(it.Key(), entry.typeID)
+		suite.Equal(it.Key(), entry.TypeID())
 
 		// Now check that all of stdout was successfully unmarshalled.
 		stdout = bytes.ReplaceAll(stdout, []byte("methods"), []byte("actions"))
+		stdout = bytes.ReplaceAll(stdout, []byte("aws."), []byte("fooPlugin::aws."))
 		actualGraphJSON, err := actualGraph.ToJSON()
 		if suite.NoError(err) {
 			suite.JSONEq(string(stdout), string(actualGraphJSON))
diff --git a/plugin/externalPluginRoot_test.go b/plugin/externalPluginRoot_test.go
index c794f36..a973454 100644
--- a/plugin/externalPluginRoot_test.go
+++ b/plugin/externalPluginRoot_test.go
@@ -45,7 +45,7 @@ func (suite *ExternalPluginRootTestSuite) TestInit() {
 	suite.Regexp(regexp.MustCompile("stdout"), err)
 
 	// Test that Init properly decodes the root from stdout
-	stdout := "{}"
+	stdout := "{\"type_id\":\"foo_type\"}"
 	mockInvokeAndWait([]byte(stdout), nil)
 	err = root.Init(nil)
 	if suite.NoError(err) {
@@ -54,6 +54,7 @@ func (suite *ExternalPluginRootTestSuite) TestInit() {
 				EntryBase: NewEntry("foo"),
 				methods:   map[string]interface{}{"list": nil},
 				script:    root.script,
+				typeID:    "foo::foo_type",
 			},
 		}
 
@@ -120,7 +121,7 @@ func (suite *ExternalPluginRootTestSuite) TestInitWithSchema_PrefetchedSchema_Re
 func (suite *ExternalPluginRootTestSuite) TestInitWithSchema_PrefetchedSchema_PartitionsSchemaGraph() {
 	mockScript := &mockExternalPluginScript{path: "plugin_script"}
 	root := &externalPluginRoot{&externalPluginEntry{
-		EntryBase: NewEntry("foo"),
+		EntryBase: NewEntry("fooPlugin"),
 		script:    mockScript,
 	}}
 
@@ -157,11 +158,12 @@ func (suite *ExternalPluginRootTestSuite) TestInitWithSchema_PrefetchedSchema_Pa
 	if err := root.Init(nil); suite.NoError(err) && suite.NotNil(root.schemaGraphs) {
 		// Ensure that the graph of root.schemaGraphs[type_id] has "type_id" as its
 		// first item. We pick an arbitrary type ID here
-		graph := root.schemaGraphs["aws.profile"]
+		typeID := namespace(root.name(), "aws.profile")
+		graph := root.schemaGraphs[typeID]
 		if suite.NotNil(graph) {
 			it := graph.Iterator()
 			it.First()
-			suite.Equal("aws.profile", it.Key())
+			suite.Equal(typeID, it.Key())
 		}
 
 		// Now ensure that the rest of root.schemaGraphs is what we expect. We already
diff --git a/plugin/testdata/externalPluginSchema.json b/plugin/testdata/externalPluginSchema.json
index 15b8f59..0e60cfc 100644
--- a/plugin/testdata/externalPluginSchema.json
+++ b/plugin/testdata/externalPluginSchema.json
@@ -1134,7 +1134,7 @@
         "children": [
             "aws.ec2InstanceConsoleOutput",
             "aws.ec2InstanceMetadataJSON",
-            "volume.FS"
+            "aws.volume.FS"
         ]
     },
     "aws.ec2InstanceConsoleOutput": {
@@ -1175,7 +1175,7 @@
         "metadata_schema": null,
         "children": null
     },
-    "volume.FS": {
+    "aws.volume.FS": {
         "label": "fs",
         "singleton": false,
         "methods": [
@@ -1194,11 +1194,11 @@
         },
         "metadata_schema": null,
         "children": [
-            "volume.dir",
-            "volume.file"
+            "aws.volume.dir",
+            "aws.volume.file"
         ]
     },
-    "volume.dir": {
+    "aws.volume.dir": {
         "label": "dir",
         "singleton": false,
         "methods": [
@@ -1217,11 +1217,11 @@
         },
         "metadata_schema": null,
         "children": [
-            "volume.dir",
-            "volume.file"
+            "aws.volume.dir",
+            "aws.volume.file"
         ]
     },
-    "volume.file": {
+    "aws.volume.file": {
         "label": "file",
         "singleton": false,
         "methods": [
diff --git a/plugin/testdata/externalPluginSchema_PartitionedSchemaGraph.json b/plugin/testdata/externalPluginSchema_PartitionedSchemaGraph.json
index 5da382b..2e14150 100644
--- a/plugin/testdata/externalPluginSchema_PartitionedSchemaGraph.json
+++ b/plugin/testdata/externalPluginSchema_PartitionedSchemaGraph.json
@@ -1,6 +1,6 @@
 {
-    "aws.Root": {
-        "aws.Root": {
+    "fooPlugin::aws.Root": {
+        "fooPlugin::aws.Root": {
             "label": "aws",
             "singleton": true,
             "actions": [
@@ -9,10 +9,10 @@
             "meta_attribute_schema": null,
             "metadata_schema": null,
             "children": [
-                "aws.profile"
+                "fooPlugin::aws.profile"
             ]
         },
-        "aws.profile": {
+        "fooPlugin::aws.profile": {
             "label": "profile",
             "singleton": false,
             "actions": [
@@ -21,10 +21,10 @@
             "meta_attribute_schema": null,
             "metadata_schema": null,
             "children": [
-                "aws.resourcesDir"
+                "fooPlugin::aws.resourcesDir"
             ]
         },
-        "aws.resourcesDir": {
+        "fooPlugin::aws.resourcesDir": {
             "label": "resources",
             "singleton": true,
             "actions": [
@@ -33,11 +33,11 @@
             "meta_attribute_schema": null,
             "metadata_schema": null,
             "children": [
-                "aws.s3Dir",
-                "aws.ec2Dir"
+                "fooPlugin::aws.s3Dir",
+                "fooPlugin::aws.ec2Dir"
             ]
         },
-        "aws.s3Dir": {
+        "fooPlugin::aws.s3Dir": {
             "label": "s3",
             "singleton": true,
             "actions": [
@@ -46,10 +46,10 @@
             "meta_attribute_schema": null,
             "metadata_schema": null,
             "children": [
-                "aws.s3Bucket"
+                "fooPlugin::aws.s3Bucket"
             ]
         },
-        "aws.s3Bucket": {
+        "fooPlugin::aws.s3Bucket": {
             "label": "bucket",
             "singleton": false,
             "actions": [
@@ -58,11 +58,11 @@
             "meta_attribute_schema": null,
             "metadata_schema": null,
             "children": [
-                "aws.s3ObjectPrefix",
-                "aws.s3Object"
+                "fooPlugin::aws.s3ObjectPrefix",
+                "fooPlugin::aws.s3Object"
             ]
         },
-        "aws.s3ObjectPrefix": {
+        "fooPlugin::aws.s3ObjectPrefix": {
             "label": "prefix",
             "singleton": false,
             "actions": [
@@ -71,11 +71,11 @@
             "meta_attribute_schema": null,
             "metadata_schema": null,
             "children": [
-                "aws.s3ObjectPrefix",
-                "aws.s3Object"
+                "fooPlugin::aws.s3ObjectPrefix",
+                "fooPlugin::aws.s3Object"
             ]
         },
-        "aws.s3Object": {
+        "fooPlugin::aws.s3Object": {
             "label": "object",
             "singleton": false,
             "actions": [
@@ -86,7 +86,7 @@
             "metadata_schema": null,
             "children": null
         },
-        "aws.ec2Dir": {
+        "fooPlugin::aws.ec2Dir": {
             "label": "ec2",
             "singleton": true,
             "actions": [
@@ -95,10 +95,10 @@
             "meta_attribute_schema": null,
             "metadata_schema": null,
             "children": [
-                "aws.ec2InstancesDir"
+                "fooPlugin::aws.ec2InstancesDir"
             ]
         },
-        "aws.ec2InstancesDir": {
+        "fooPlugin::aws.ec2InstancesDir": {
             "label": "instances",
             "singleton": true,
             "actions": [
@@ -107,10 +107,10 @@
             "meta_attribute_schema": null,
             "metadata_schema": null,
             "children": [
-                "aws.ec2Instance"
+                "fooPlugin::aws.ec2Instance"
             ]
         },
-        "aws.ec2Instance": {
+        "fooPlugin::aws.ec2Instance": {
             "label": "instance",
             "singleton": false,
             "actions": [
@@ -120,12 +120,12 @@
             "meta_attribute_schema": null,
             "metadata_schema": null,
             "children": [
-                "aws.ec2InstanceConsoleOutput",
-                "aws.ec2InstanceMetadataJSON",
-                "volume.FS"
+                "fooPlugin::aws.ec2InstanceConsoleOutput",
+                "fooPlugin::aws.ec2InstanceMetadataJSON",
+                "fooPlugin::aws.volume.FS"
             ]
         },
-        "aws.ec2InstanceConsoleOutput": {
+        "fooPlugin::aws.ec2InstanceConsoleOutput": {
             "label": "console.out",
             "singleton": false,
             "actions": [
@@ -135,7 +135,7 @@
             "metadata_schema": null,
             "children": null
         },
-        "aws.ec2InstanceMetadataJSON": {
+        "fooPlugin::aws.ec2InstanceMetadataJSON": {
             "label": "metadata.json",
             "singleton": true,
             "actions": [
@@ -145,7 +145,7 @@
             "metadata_schema": null,
             "children": null
         },
-        "volume.FS": {
+        "fooPlugin::aws.volume.FS": {
             "label": "fs",
             "singleton": false,
             "actions": [
@@ -154,11 +154,11 @@
             "meta_attribute_schema": null,
             "metadata_schema": null,
             "children": [
-                "volume.dir",
-                "volume.file"
+                "fooPlugin::aws.volume.dir",
+                "fooPlugin::aws.volume.file"
             ]
         },
-        "volume.dir": {
+        "fooPlugin::aws.volume.dir": {
             "label": "dir",
             "singleton": false,
             "actions": [
@@ -167,11 +167,11 @@
             "meta_attribute_schema": null,
             "metadata_schema": null,
             "children": [
-                "volume.dir",
-                "volume.file"
+                "fooPlugin::aws.volume.dir",
+                "fooPlugin::aws.volume.file"
             ]
         },
-        "volume.file": {
+        "fooPlugin::aws.volume.file": {
             "label": "file",
             "singleton": false,
             "actions": [
@@ -183,8 +183,8 @@
             "children": null
         }
     },
-    "aws.ec2Dir": {
-        "aws.ec2Dir": {
+    "fooPlugin::aws.ec2Dir": {
+        "fooPlugin::aws.ec2Dir": {
             "label": "ec2",
             "singleton": true,
             "actions": [
@@ -193,10 +193,10 @@
             "meta_attribute_schema": null,
             "metadata_schema": null,
             "children": [
-                "aws.ec2InstancesDir"
+                "fooPlugin::aws.ec2InstancesDir"
             ]
         },
-        "aws.ec2InstancesDir": {
+        "fooPlugin::aws.ec2InstancesDir": {
             "label": "instances",
             "singleton": true,
             "actions": [
@@ -205,10 +205,10 @@
             "meta_attribute_schema": null,
             "metadata_schema": null,
             "children": [
-                "aws.ec2Instance"
+                "fooPlugin::aws.ec2Instance"
             ]
         },
-        "aws.ec2Instance": {
+        "fooPlugin::aws.ec2Instance": {
             "label": "instance",
             "singleton": false,
             "actions": [
@@ -218,12 +218,12 @@
             "meta_attribute_schema": null,
             "metadata_schema": null,
             "children": [
-                "aws.ec2InstanceConsoleOutput",
-                "aws.ec2InstanceMetadataJSON",
-                "volume.FS"
+                "fooPlugin::aws.ec2InstanceConsoleOutput",
+                "fooPlugin::aws.ec2InstanceMetadataJSON",
+                "fooPlugin::aws.volume.FS"
             ]
         },
-        "aws.ec2InstanceConsoleOutput": {
+        "fooPlugin::aws.ec2InstanceConsoleOutput": {
             "label": "console.out",
             "singleton": false,
             "actions": [
@@ -233,7 +233,7 @@
             "metadata_schema": null,
             "children": null
         },
-        "aws.ec2InstanceMetadataJSON": {
+        "fooPlugin::aws.ec2InstanceMetadataJSON": {
             "label": "metadata.json",
             "singleton": true,
             "actions": [
@@ -243,7 +243,7 @@
             "metadata_schema": null,
             "children": null
         },
-        "volume.FS": {
+        "fooPlugin::aws.volume.FS": {
             "label": "fs",
             "singleton": false,
             "actions": [
@@ -252,11 +252,11 @@
             "meta_attribute_schema": null,
             "metadata_schema": null,
             "children": [
-                "volume.dir",
-                "volume.file"
+                "fooPlugin::aws.volume.dir",
+                "fooPlugin::aws.volume.file"
             ]
         },
-        "volume.dir": {
+        "fooPlugin::aws.volume.dir": {
             "label": "dir",
             "singleton": false,
             "actions": [
@@ -265,11 +265,11 @@
             "meta_attribute_schema": null,
             "metadata_schema": null,
             "children": [
-                "volume.dir",
-                "volume.file"
+                "fooPlugin::aws.volume.dir",
+                "fooPlugin::aws.volume.file"
             ]
         },
-        "volume.file": {
+        "fooPlugin::aws.volume.file": {
             "label": "file",
             "singleton": false,
             "actions": [
@@ -281,8 +281,8 @@
             "children": null
         }
     },
-    "aws.ec2Instance": {
-        "aws.ec2Instance": {
+    "fooPlugin::aws.ec2Instance": {
+        "fooPlugin::aws.ec2Instance": {
             "label": "instance",
             "singleton": false,
             "actions": [
@@ -292,12 +292,12 @@
             "meta_attribute_schema": null,
             "metadata_schema": null,
             "children": [
-                "aws.ec2InstanceConsoleOutput",
-                "aws.ec2InstanceMetadataJSON",
-                "volume.FS"
+                "fooPlugin::aws.ec2InstanceConsoleOutput",
+                "fooPlugin::aws.ec2InstanceMetadataJSON",
+                "fooPlugin::aws.volume.FS"
             ]
         },
-        "aws.ec2InstanceConsoleOutput": {
+        "fooPlugin::aws.ec2InstanceConsoleOutput": {
             "label": "console.out",
             "singleton": false,
             "actions": [
@@ -307,7 +307,7 @@
             "metadata_schema": null,
             "children": null
         },
-        "aws.ec2InstanceMetadataJSON": {
+        "fooPlugin::aws.ec2InstanceMetadataJSON": {
             "label": "metadata.json",
             "singleton": true,
             "actions": [
@@ -317,7 +317,7 @@
             "metadata_schema": null,
             "children": null
         },
-        "volume.FS": {
+        "fooPlugin::aws.volume.FS": {
             "label": "fs",
             "singleton": false,
             "actions": [
@@ -326,11 +326,11 @@
             "meta_attribute_schema": null,
             "metadata_schema": null,
             "children": [
-                "volume.dir",
-                "volume.file"
+                "fooPlugin::aws.volume.dir",
+                "fooPlugin::aws.volume.file"
             ]
         },
-        "volume.dir": {
+        "fooPlugin::aws.volume.dir": {
             "label": "dir",
             "singleton": false,
             "actions": [
@@ -339,11 +339,11 @@
             "meta_attribute_schema": null,
             "metadata_schema": null,
             "children": [
-                "volume.dir",
-                "volume.file"
+                "fooPlugin::aws.volume.dir",
+                "fooPlugin::aws.volume.file"
             ]
         },
-        "volume.file": {
+        "fooPlugin::aws.volume.file": {
             "label": "file",
             "singleton": false,
             "actions": [
@@ -355,8 +355,8 @@
             "children": null
         }
     },
-    "aws.ec2InstanceConsoleOutput": {
-        "aws.ec2InstanceConsoleOutput": {
+    "fooPlugin::aws.ec2InstanceConsoleOutput": {
+        "fooPlugin::aws.ec2InstanceConsoleOutput": {
             "label": "console.out",
             "singleton": false,
             "actions": [
@@ -367,8 +367,8 @@
             "children": null
         }
     },
-    "aws.ec2InstanceMetadataJSON": {
-        "aws.ec2InstanceMetadataJSON": {
+    "fooPlugin::aws.ec2InstanceMetadataJSON": {
+        "fooPlugin::aws.ec2InstanceMetadataJSON": {
             "label": "metadata.json",
             "singleton": true,
             "actions": [
@@ -379,8 +379,8 @@
             "children": null
         }
     },
-    "aws.ec2InstancesDir": {
-        "aws.ec2InstancesDir": {
+    "fooPlugin::aws.ec2InstancesDir": {
+        "fooPlugin::aws.ec2InstancesDir": {
             "label": "instances",
             "singleton": true,
             "actions": [
@@ -389,10 +389,10 @@
             "meta_attribute_schema": null,
             "metadata_schema": null,
             "children": [
-                "aws.ec2Instance"
+                "fooPlugin::aws.ec2Instance"
             ]
         },
-        "aws.ec2Instance": {
+        "fooPlugin::aws.ec2Instance": {
             "label": "instance",
             "singleton": false,
             "actions": [
@@ -402,12 +402,12 @@
             "meta_attribute_schema": null,
             "metadata_schema": null,
             "children": [
-                "aws.ec2InstanceConsoleOutput",
-                "aws.ec2InstanceMetadataJSON",
-                "volume.FS"
+                "fooPlugin::aws.ec2InstanceConsoleOutput",
+                "fooPlugin::aws.ec2InstanceMetadataJSON",
+                "fooPlugin::aws.volume.FS"
             ]
         },
-        "aws.ec2InstanceConsoleOutput": {
+        "fooPlugin::aws.ec2InstanceConsoleOutput": {
             "label": "console.out",
             "singleton": false,
             "actions": [
@@ -417,7 +417,7 @@
             "metadata_schema": null,
             "children": null
         },
-        "aws.ec2InstanceMetadataJSON": {
+        "fooPlugin::aws.ec2InstanceMetadataJSON": {
             "label": "metadata.json",
             "singleton": true,
             "actions": [
@@ -427,7 +427,7 @@
             "metadata_schema": null,
             "children": null
         },
-        "volume.FS": {
+        "fooPlugin::aws.volume.FS": {
             "label": "fs",
             "singleton": false,
             "actions": [
@@ -436,11 +436,11 @@
             "meta_attribute_schema": null,
             "metadata_schema": null,
             "children": [
-                "volume.dir",
-                "volume.file"
+                "fooPlugin::aws.volume.dir",
+                "fooPlugin::aws.volume.file"
             ]
         },
-        "volume.dir": {
+        "fooPlugin::aws.volume.dir": {
             "label": "dir",
             "singleton": false,
             "actions": [
@@ -449,11 +449,11 @@
             "meta_attribute_schema": null,
             "metadata_schema": null,
             "children": [
-                "volume.dir",
-                "volume.file"
+                "fooPlugin::aws.volume.dir",
+                "fooPlugin::aws.volume.file"
             ]
         },
-        "volume.file": {
+        "fooPlugin::aws.volume.file": {
             "label": "file",
             "singleton": false,
             "actions": [
@@ -465,8 +465,8 @@
             "children": null
         }
     },
-    "aws.profile": {
-        "aws.profile": {
+    "fooPlugin::aws.profile": {
+        "fooPlugin::aws.profile": {
             "label": "profile",
             "singleton": false,
             "actions": [
@@ -475,10 +475,10 @@
             "meta_attribute_schema": null,
             "metadata_schema": null,
             "children": [
-                "aws.resourcesDir"
+                "fooPlugin::aws.resourcesDir"
             ]
         },
-        "aws.resourcesDir": {
+        "fooPlugin::aws.resourcesDir": {
             "label": "resources",
             "singleton": true,
             "actions": [
@@ -487,11 +487,11 @@
             "meta_attribute_schema": null,
             "metadata_schema": null,
             "children": [
-                "aws.s3Dir",
-                "aws.ec2Dir"
+                "fooPlugin::aws.s3Dir",
+                "fooPlugin::aws.ec2Dir"
             ]
         },
-        "aws.s3Dir": {
+        "fooPlugin::aws.s3Dir": {
             "label": "s3",
             "singleton": true,
             "actions": [
@@ -500,10 +500,10 @@
             "meta_attribute_schema": null,
             "metadata_schema": null,
             "children": [
-                "aws.s3Bucket"
+                "fooPlugin::aws.s3Bucket"
             ]
         },
-        "aws.s3Bucket": {
+        "fooPlugin::aws.s3Bucket": {
             "label": "bucket",
             "singleton": false,
             "actions": [
@@ -512,11 +512,11 @@
             "meta_attribute_schema": null,
             "metadata_schema": null,
             "children": [
-                "aws.s3ObjectPrefix",
-                "aws.s3Object"
+                "fooPlugin::aws.s3ObjectPrefix",
+                "fooPlugin::aws.s3Object"
             ]
         },
-        "aws.s3ObjectPrefix": {
+        "fooPlugin::aws.s3ObjectPrefix": {
             "label": "prefix",
             "singleton": false,
             "actions": [
@@ -525,11 +525,11 @@
             "meta_attribute_schema": null,
             "metadata_schema": null,
             "children": [
-                "aws.s3ObjectPrefix",
-                "aws.s3Object"
+                "fooPlugin::aws.s3ObjectPrefix",
+                "fooPlugin::aws.s3Object"
             ]
         },
-        "aws.s3Object": {
+        "fooPlugin::aws.s3Object": {
             "label": "object",
             "singleton": false,
             "actions": [
@@ -540,7 +540,7 @@
             "metadata_schema": null,
             "children": null
         },
-        "aws.ec2Dir": {
+        "fooPlugin::aws.ec2Dir": {
             "label": "ec2",
             "singleton": true,
             "actions": [
@@ -549,10 +549,10 @@
             "meta_attribute_schema": null,
             "metadata_schema": null,
             "children": [
-                "aws.ec2InstancesDir"
+                "fooPlugin::aws.ec2InstancesDir"
             ]
         },
-        "aws.ec2InstancesDir": {
+        "fooPlugin::aws.ec2InstancesDir": {
             "label": "instances",
             "singleton": true,
             "actions": [
@@ -561,10 +561,10 @@
             "meta_attribute_schema": null,
             "metadata_schema": null,
             "children": [
-                "aws.ec2Instance"
+                "fooPlugin::aws.ec2Instance"
             ]
         },
-        "aws.ec2Instance": {
+        "fooPlugin::aws.ec2Instance": {
             "label": "instance",
             "singleton": false,
             "actions": [
@@ -574,12 +574,12 @@
             "meta_attribute_schema": null,
             "metadata_schema": null,
             "children": [
-                "aws.ec2InstanceConsoleOutput",
-                "aws.ec2InstanceMetadataJSON",
-                "volume.FS"
+                "fooPlugin::aws.ec2InstanceConsoleOutput",
+                "fooPlugin::aws.ec2InstanceMetadataJSON",
+                "fooPlugin::aws.volume.FS"
             ]
         },
-        "aws.ec2InstanceConsoleOutput": {
+        "fooPlugin::aws.ec2InstanceConsoleOutput": {
             "label": "console.out",
             "singleton": false,
             "actions": [
@@ -589,7 +589,7 @@
             "metadata_schema": null,
             "children": null
         },
-        "aws.ec2InstanceMetadataJSON": {
+        "fooPlugin::aws.ec2InstanceMetadataJSON": {
             "label": "metadata.json",
             "singleton": true,
             "actions": [
@@ -599,7 +599,7 @@
             "metadata_schema": null,
             "children": null
         },
-        "volume.FS": {
+        "fooPlugin::aws.volume.FS": {
             "label": "fs",
             "singleton": false,
             "actions": [
@@ -608,11 +608,11 @@
             "meta_attribute_schema": null,
             "metadata_schema": null,
             "children": [
-                "volume.dir",
-                "volume.file"
+                "fooPlugin::aws.volume.dir",
+                "fooPlugin::aws.volume.file"
             ]
         },
-        "volume.dir": {
+        "fooPlugin::aws.volume.dir": {
             "label": "dir",
             "singleton": false,
             "actions": [
@@ -621,11 +621,11 @@
             "meta_attribute_schema": null,
             "metadata_schema": null,
             "children": [
-                "volume.dir",
-                "volume.file"
+                "fooPlugin::aws.volume.dir",
+                "fooPlugin::aws.volume.file"
             ]
         },
-        "volume.file": {
+        "fooPlugin::aws.volume.file": {
             "label": "file",
             "singleton": false,
             "actions": [
@@ -637,8 +637,8 @@
             "children": null
         }
     },
-    "aws.resourcesDir": {
-        "aws.resourcesDir": {
+    "fooPlugin::aws.resourcesDir": {
+        "fooPlugin::aws.resourcesDir": {
             "label": "resources",
             "singleton": true,
             "actions": [
@@ -647,11 +647,11 @@
             "meta_attribute_schema": null,
             "metadata_schema": null,
             "children": [
-                "aws.s3Dir",
-                "aws.ec2Dir"
+                "fooPlugin::aws.s3Dir",
+                "fooPlugin::aws.ec2Dir"
             ]
         },
-        "aws.s3Dir": {
+        "fooPlugin::aws.s3Dir": {
             "label": "s3",
             "singleton": true,
             "actions": [
@@ -660,10 +660,10 @@
             "meta_attribute_schema": null,
             "metadata_schema": null,
             "children": [
-                "aws.s3Bucket"
+                "fooPlugin::aws.s3Bucket"
             ]
         },
-        "aws.s3Bucket": {
+        "fooPlugin::aws.s3Bucket": {
             "label": "bucket",
             "singleton": false,
             "actions": [
@@ -672,11 +672,11 @@
             "meta_attribute_schema": null,
             "metadata_schema": null,
             "children": [
-                "aws.s3ObjectPrefix",
-                "aws.s3Object"
+                "fooPlugin::aws.s3ObjectPrefix",
+                "fooPlugin::aws.s3Object"
             ]
         },
-        "aws.s3ObjectPrefix": {
+        "fooPlugin::aws.s3ObjectPrefix": {
             "label": "prefix",
             "singleton": false,
             "actions": [
@@ -685,11 +685,11 @@
             "meta_attribute_schema": null,
             "metadata_schema": null,
             "children": [
-                "aws.s3ObjectPrefix",
-                "aws.s3Object"
+                "fooPlugin::aws.s3ObjectPrefix",
+                "fooPlugin::aws.s3Object"
             ]
         },
-        "aws.s3Object": {
+        "fooPlugin::aws.s3Object": {
             "label": "object",
             "singleton": false,
             "actions": [
@@ -700,7 +700,7 @@
             "metadata_schema": null,
             "children": null
         },
-        "aws.ec2Dir": {
+        "fooPlugin::aws.ec2Dir": {
             "label": "ec2",
             "singleton": true,
             "actions": [
@@ -709,10 +709,10 @@
             "meta_attribute_schema": null,
             "metadata_schema": null,
             "children": [
-                "aws.ec2InstancesDir"
+                "fooPlugin::aws.ec2InstancesDir"
             ]
         },
-        "aws.ec2InstancesDir": {
+        "fooPlugin::aws.ec2InstancesDir": {
             "label": "instances",
             "singleton": true,
             "actions": [
@@ -721,10 +721,10 @@
             "meta_attribute_schema": null,
             "metadata_schema": null,
             "children": [
-                "aws.ec2Instance"
+                "fooPlugin::aws.ec2Instance"
             ]
         },
-        "aws.ec2Instance": {
+        "fooPlugin::aws.ec2Instance": {
             "label": "instance",
             "singleton": false,
             "actions": [
@@ -734,12 +734,12 @@
             "meta_attribute_schema": null,
             "metadata_schema": null,
             "children": [
-                "aws.ec2InstanceConsoleOutput",
-                "aws.ec2InstanceMetadataJSON",
-                "volume.FS"
+                "fooPlugin::aws.ec2InstanceConsoleOutput",
+                "fooPlugin::aws.ec2InstanceMetadataJSON",
+                "fooPlugin::aws.volume.FS"
             ]
         },
-        "aws.ec2InstanceConsoleOutput": {
+        "fooPlugin::aws.ec2InstanceConsoleOutput": {
             "label": "console.out",
             "singleton": false,
             "actions": [
@@ -749,7 +749,7 @@
             "metadata_schema": null,
             "children": null
         },
-        "aws.ec2InstanceMetadataJSON": {
+        "fooPlugin::aws.ec2InstanceMetadataJSON": {
             "label": "metadata.json",
             "singleton": true,
             "actions": [
@@ -759,7 +759,7 @@
             "metadata_schema": null,
             "children": null
         },
-        "volume.FS": {
+        "fooPlugin::aws.volume.FS": {
             "label": "fs",
             "singleton": false,
             "actions": [
@@ -768,11 +768,11 @@
             "meta_attribute_schema": null,
             "metadata_schema": null,
             "children": [
-                "volume.dir",
-                "volume.file"
+                "fooPlugin::aws.volume.dir",
+                "fooPlugin::aws.volume.file"
             ]
         },
-        "volume.dir": {
+        "fooPlugin::aws.volume.dir": {
             "label": "dir",
             "singleton": false,
             "actions": [
@@ -781,11 +781,11 @@
             "meta_attribute_schema": null,
             "metadata_schema": null,
             "children": [
-                "volume.dir",
-                "volume.file"
+                "fooPlugin::aws.volume.dir",
+                "fooPlugin::aws.volume.file"
             ]
         },
-        "volume.file": {
+        "fooPlugin::aws.volume.file": {
             "label": "file",
             "singleton": false,
             "actions": [
@@ -797,8 +797,8 @@
             "children": null
         }
     },
-    "aws.s3Bucket": {
-        "aws.s3Bucket": {
+    "fooPlugin::aws.s3Bucket": {
+        "fooPlugin::aws.s3Bucket": {
             "label": "bucket",
             "singleton": false,
             "actions": [
@@ -807,11 +807,11 @@
             "meta_attribute_schema": null,
             "metadata_schema": null,
             "children": [
-                "aws.s3ObjectPrefix",
-                "aws.s3Object"
+                "fooPlugin::aws.s3ObjectPrefix",
+                "fooPlugin::aws.s3Object"
             ]
         },
-        "aws.s3ObjectPrefix": {
+        "fooPlugin::aws.s3ObjectPrefix": {
             "label": "prefix",
             "singleton": false,
             "actions": [
@@ -820,11 +820,11 @@
             "meta_attribute_schema": null,
             "metadata_schema": null,
             "children": [
-                "aws.s3ObjectPrefix",
-                "aws.s3Object"
+                "fooPlugin::aws.s3ObjectPrefix",
+                "fooPlugin::aws.s3Object"
             ]
         },
-        "aws.s3Object": {
+        "fooPlugin::aws.s3Object": {
             "label": "object",
             "singleton": false,
             "actions": [
@@ -836,8 +836,8 @@
             "children": null
         }
     },
-    "aws.s3Dir": {
-        "aws.s3Dir": {
+    "fooPlugin::aws.s3Dir": {
+        "fooPlugin::aws.s3Dir": {
             "label": "s3",
             "singleton": true,
             "actions": [
@@ -846,10 +846,10 @@
             "meta_attribute_schema": null,
             "metadata_schema": null,
             "children": [
-                "aws.s3Bucket"
+                "fooPlugin::aws.s3Bucket"
             ]
         },
-        "aws.s3Bucket": {
+        "fooPlugin::aws.s3Bucket": {
             "label": "bucket",
             "singleton": false,
             "actions": [
@@ -858,11 +858,11 @@
             "meta_attribute_schema": null,
             "metadata_schema": null,
             "children": [
-                "aws.s3ObjectPrefix",
-                "aws.s3Object"
+                "fooPlugin::aws.s3ObjectPrefix",
+                "fooPlugin::aws.s3Object"
             ]
         },
-        "aws.s3ObjectPrefix": {
+        "fooPlugin::aws.s3ObjectPrefix": {
             "label": "prefix",
             "singleton": false,
             "actions": [
@@ -871,11 +871,11 @@
             "meta_attribute_schema": null,
             "metadata_schema": null,
             "children": [
-                "aws.s3ObjectPrefix",
-                "aws.s3Object"
+                "fooPlugin::aws.s3ObjectPrefix",
+                "fooPlugin::aws.s3Object"
             ]
         },
-        "aws.s3Object": {
+        "fooPlugin::aws.s3Object": {
             "label": "object",
             "singleton": false,
             "actions": [
@@ -887,8 +887,8 @@
             "children": null
         }
     },
-    "aws.s3Object": {
-        "aws.s3Object": {
+    "fooPlugin::aws.s3Object": {
+        "fooPlugin::aws.s3Object": {
             "label": "object",
             "singleton": false,
             "actions": [
@@ -900,8 +900,8 @@
             "children": null
         }
     },
-    "aws.s3ObjectPrefix": {
-        "aws.s3ObjectPrefix": {
+    "fooPlugin::aws.s3ObjectPrefix": {
+        "fooPlugin::aws.s3ObjectPrefix": {
             "label": "prefix",
             "singleton": false,
             "actions": [
@@ -910,11 +910,11 @@
             "meta_attribute_schema": null,
             "metadata_schema": null,
             "children": [
-                "aws.s3ObjectPrefix",
-                "aws.s3Object"
+                "fooPlugin::aws.s3ObjectPrefix",
+                "fooPlugin::aws.s3Object"
             ]
         },
-        "aws.s3Object": {
+        "fooPlugin::aws.s3Object": {
             "label": "object",
             "singleton": false,
             "actions": [
@@ -926,8 +926,8 @@
             "children": null
         }
     },
-    "volume.FS": {
-        "volume.FS": {
+    "fooPlugin::aws.volume.FS": {
+        "fooPlugin::aws.volume.FS": {
             "label": "fs",
             "singleton": false,
             "actions": [
@@ -936,11 +936,11 @@
             "meta_attribute_schema": null,
             "metadata_schema": null,
             "children": [
-                "volume.dir",
-                "volume.file"
+                "fooPlugin::aws.volume.dir",
+                "fooPlugin::aws.volume.file"
             ]
         },
-        "volume.dir": {
+        "fooPlugin::aws.volume.dir": {
             "label": "dir",
             "singleton": false,
             "actions": [
@@ -949,11 +949,11 @@
             "meta_attribute_schema": null,
             "metadata_schema": null,
             "children": [
-                "volume.dir",
-                "volume.file"
+                "fooPlugin::aws.volume.dir",
+                "fooPlugin::aws.volume.file"
             ]
         },
-        "volume.file": {
+        "fooPlugin::aws.volume.file": {
             "label": "file",
             "singleton": false,
             "actions": [
@@ -965,8 +965,8 @@
             "children": null
         }
     },
-    "volume.dir": {
-        "volume.dir": {
+    "fooPlugin::aws.volume.dir": {
+        "fooPlugin::aws.volume.dir": {
             "label": "dir",
             "singleton": false,
             "actions": [
@@ -975,11 +975,11 @@
             "meta_attribute_schema": null,
             "metadata_schema": null,
             "children": [
-                "volume.dir",
-                "volume.file"
+                "fooPlugin::aws.volume.dir",
+                "fooPlugin::aws.volume.file"
             ]
         },
-        "volume.file": {
+        "fooPlugin::aws.volume.file": {
             "label": "file",
             "singleton": false,
             "actions": [
@@ -991,8 +991,8 @@
             "children": null
         }
     },
-    "volume.file": {
-        "volume.file": {
+    "fooPlugin::aws.volume.file": {
+        "fooPlugin::aws.volume.file": {
             "label": "file",
             "singleton": false,
             "actions": [
diff --git a/plugin/testdata/externalPluginSchema_SchemaGraph.json b/plugin/testdata/externalPluginSchema_SchemaGraph.json
index 76d372d..6a09092 100644
--- a/plugin/testdata/externalPluginSchema_SchemaGraph.json
+++ b/plugin/testdata/externalPluginSchema_SchemaGraph.json
@@ -101,7 +101,7 @@
         "children": [
             "aws.ec2InstanceConsoleOutput",
             "aws.ec2InstanceMetadataJSON",
-            "volume.FS"
+            "aws.volume.FS"
         ]
     },
     "aws.ec2InstanceConsoleOutput": {
@@ -120,29 +120,29 @@
         ],
         "children": null
     },
-    "volume.FS": {
+    "aws.volume.FS": {
         "label": "fs",
         "singleton": false,
         "methods": [
             "list"
         ],
         "children": [
-            "volume.dir",
-            "volume.file"
+            "aws.volume.dir",
+            "aws.volume.file"
         ]
     },
-    "volume.dir": {
+    "aws.volume.dir": {
         "label": "dir",
         "singleton": false,
         "methods": [
             "list"
         ],
         "children": [
-            "volume.dir",
-            "volume.file"
+            "aws.volume.dir",
+            "aws.volume.file"
         ]
     },
-    "volume.file": {
+    "aws.volume.file": {
         "label": "file",
         "singleton": false,
         "methods": [