Repository: apigee/registry (go)

Issue:
Registry tool: add project and location config properties
From https://github.com/apigee/registry/issues/625#issue-1282776780:
> In many situations it would be useful for the registry tool to have a default project and location. For example:
registry list apis is much preferable to registry list projects/PROJECTID/locations/global/apis and the --parent argument to registry apply seem needlessly tedious. We might set these with new environment variables APG_REGISTRY_PROJECT and APG_REGISTRY_LOCATION, but now that the generated apg client is merged into the registry tool, it's easier for us to explore other options like using a configuration file to store this information.

Pull request:
add project, location config and allow short names
This isn't as elegant as I'd like, but I didn't see a good integration point that could fix it as a cross-cutting concern. Reworking the naming package to require configuration didn't seem reasonable. One potential alternative to this rather brute-force approach would be to rework the configured clients to include naming lookups... however, right now they are direct pointers to the generated clients... so we'd have to wrap them in our own client structs instead and change the references. I can investigate that (or other ideas) if the solution in this PR isn't desireable.

Fixes #677

Patch:
diff --git a/cmd/registry/cmd/annotate/annotate.go b/cmd/registry/cmd/annotate/annotate.go
index 09b2622..4cb98be 100644
--- a/cmd/registry/cmd/annotate/annotate.go
+++ b/cmd/registry/cmd/annotate/annotate.go
@@ -41,6 +41,12 @@ func Command() *cobra.Command {
 		Args:  cobra.MinimumNArgs(2),
 		Run: func(cmd *cobra.Command, args []string) {
 			ctx := cmd.Context()
+			c, err := connection.ActiveConfig()
+			if err != nil {
+				log.FromContext(ctx).WithError(err).Fatal("Failed to get config")
+			}
+			args[0] = c.FQName(args[0])
+
 			client, err := connection.NewRegistryClient(ctx)
 			if err != nil {
 				log.FromContext(ctx).WithError(err).Fatal("Failed to get client")
diff --git a/cmd/registry/cmd/compute/complexity.go b/cmd/registry/cmd/compute/complexity.go
index 07caa9e..2ce82da 100644
--- a/cmd/registry/cmd/compute/complexity.go
+++ b/cmd/registry/cmd/compute/complexity.go
@@ -36,9 +36,15 @@ func complexityCommand() *cobra.Command {
 	return &cobra.Command{
 		Use:   "complexity",
 		Short: "Compute complexity metrics of API specs",
-		Args:  cobra.MinimumNArgs(1),
+		Args:  cobra.ExactArgs(1),
 		Run: func(cmd *cobra.Command, args []string) {
 			ctx := cmd.Context()
+			c, err := connection.ActiveConfig()
+			if err != nil {
+				log.FromContext(ctx).WithError(err).Fatal("Failed to get config")
+			}
+			args[0] = c.FQName(args[0])
+
 			filter, err := cmd.Flags().GetString("filter")
 			if err != nil {
 				log.FromContext(ctx).WithError(err).Fatal("Failed to get filter from flags")
@@ -58,7 +64,7 @@ func complexityCommand() *cobra.Command {
 
 			spec, err := names.ParseSpec(args[0])
 			if err != nil {
-				return // TODO: Log an error.
+				log.FromContext(ctx).WithError(err).Fatal("Failed parse")
 			}
 
 			// Iterate through a collection of specs and summarize each.
diff --git a/cmd/registry/cmd/compute/conformance.go b/cmd/registry/cmd/compute/conformance.go
index 896241c..9d28128 100644
--- a/cmd/registry/cmd/compute/conformance.go
+++ b/cmd/registry/cmd/compute/conformance.go
@@ -35,9 +35,15 @@ func conformanceCommand() *cobra.Command {
 	cmd := &cobra.Command{
 		Use:   "conformance",
 		Short: "Compute lint results for API specs",
-		Args:  cobra.MinimumNArgs(1),
+		Args:  cobra.ExactArgs(1),
 		Run: func(cmd *cobra.Command, args []string) {
 			ctx := cmd.Context()
+			c, err := connection.ActiveConfig()
+			if err != nil {
+				log.FromContext(ctx).WithError(err).Fatal("Failed to get config")
+			}
+			args[0] = c.FQName(args[0])
+
 			filter, err := cmd.Flags().GetString("filter")
 			if err != nil {
 				log.FromContext(ctx).WithError(err).Fatal("Failed to get filter from flags")
diff --git a/cmd/registry/cmd/compute/lint.go b/cmd/registry/cmd/compute/lint.go
index 331e51c..f208054 100644
--- a/cmd/registry/cmd/compute/lint.go
+++ b/cmd/registry/cmd/compute/lint.go
@@ -32,9 +32,15 @@ func lintCommand() *cobra.Command {
 	cmd := &cobra.Command{
 		Use:   "lint",
 		Short: "Compute lint results for API specs",
-		Args:  cobra.MinimumNArgs(1),
+		Args:  cobra.ExactArgs(1),
 		Run: func(cmd *cobra.Command, args []string) {
 			ctx := cmd.Context()
+			c, err := connection.ActiveConfig()
+			if err != nil {
+				log.FromContext(ctx).WithError(err).Fatal("Failed to get config")
+			}
+			args[0] = c.FQName(args[0])
+
 			filter, err := cmd.Flags().GetString("filter")
 			if err != nil {
 				log.FromContext(ctx).WithError(err).Fatal("Failed to get filter from flags")
@@ -54,7 +60,7 @@ func lintCommand() *cobra.Command {
 
 			spec, err := names.ParseSpec(args[0])
 			if err != nil {
-				return // TODO: Log an error.
+				log.FromContext(ctx).WithError(err).Fatal("Failed parse")
 			}
 
 			// Iterate through a collection of specs and evaluate each.
diff --git a/cmd/registry/cmd/compute/lintstats.go b/cmd/registry/cmd/compute/lintstats.go
index b228f14..ccd1104 100644
--- a/cmd/registry/cmd/compute/lintstats.go
+++ b/cmd/registry/cmd/compute/lintstats.go
@@ -40,9 +40,15 @@ func lintStatsCommand() *cobra.Command {
 	cmd := &cobra.Command{
 		Use:   "lintstats",
 		Short: "Compute summaries of linter runs",
-		Args:  cobra.MinimumNArgs(1),
+		Args:  cobra.ExactArgs(1),
 		Run: func(cmd *cobra.Command, args []string) {
 			ctx := cmd.Context()
+			c, err := connection.ActiveConfig()
+			if err != nil {
+				log.FromContext(ctx).WithError(err).Fatal("Failed to get config")
+			}
+			args[0] = c.FQName(args[0])
+
 			filter, err := cmd.Flags().GetString("filter")
 			if err != nil {
 				log.FromContext(ctx).WithError(err).Fatal("Failed to get filter from flags")
diff --git a/cmd/registry/cmd/compute/references.go b/cmd/registry/cmd/compute/references.go
index 9eb4448..9de4d95 100644
--- a/cmd/registry/cmd/compute/references.go
+++ b/cmd/registry/cmd/compute/references.go
@@ -31,9 +31,15 @@ func referencesCommand() *cobra.Command {
 	return &cobra.Command{
 		Use:   "references",
 		Short: "Compute references of API specs",
-		Args:  cobra.MinimumNArgs(1),
+		Args:  cobra.ExactArgs(1),
 		Run: func(cmd *cobra.Command, args []string) {
 			ctx := cmd.Context()
+			c, err := connection.ActiveConfig()
+			if err != nil {
+				log.FromContext(ctx).WithError(err).Fatal("Failed to get config")
+			}
+			args[0] = c.FQName(args[0])
+
 			filter, err := cmd.Flags().GetString("filter")
 			if err != nil {
 				log.FromContext(ctx).WithError(err).Fatal("Failed to get filter from flags")
@@ -53,7 +59,7 @@ func referencesCommand() *cobra.Command {
 
 			spec, err := names.ParseSpec(args[0])
 			if err != nil {
-				return // TODO: Log an error.
+				log.FromContext(ctx).WithError(err).Fatal("Failed parse")
 			}
 
 			// Iterate through a collection of specs and compute references for each
diff --git a/cmd/registry/cmd/compute/vocabulary.go b/cmd/registry/cmd/compute/vocabulary.go
index bf76c58..0fad200 100644
--- a/cmd/registry/cmd/compute/vocabulary.go
+++ b/cmd/registry/cmd/compute/vocabulary.go
@@ -40,6 +40,12 @@ func vocabularyCommand() *cobra.Command {
 		Args:  cobra.MinimumNArgs(1),
 		Run: func(cmd *cobra.Command, args []string) {
 			ctx := cmd.Context()
+			c, err := connection.ActiveConfig()
+			if err != nil {
+				log.FromContext(ctx).WithError(err).Fatal("Failed to get config")
+			}
+			path := c.FQName(args[0])
+
 			filter, err := cmd.Flags().GetString("filter")
 			if err != nil {
 				log.FromContext(ctx).WithError(err).Fatal("Failed to get filter from flags")
@@ -57,9 +63,9 @@ func vocabularyCommand() *cobra.Command {
 			taskQueue, wait := core.WorkerPool(ctx, 64)
 			defer wait()
 
-			spec, err := names.ParseSpec(args[0])
+			spec, err := names.ParseSpec(path)
 			if err != nil {
-				return // TODO: Log an error.
+				log.FromContext(ctx).WithError(err).Fatal("Failed parse")
 			}
 
 			// Iterate through a collection of specs and summarize each.
diff --git a/cmd/registry/cmd/count/revisions.go b/cmd/registry/cmd/count/revisions.go
index e530ed3..19f3bb3 100644
--- a/cmd/registry/cmd/count/revisions.go
+++ b/cmd/registry/cmd/count/revisions.go
@@ -36,6 +36,12 @@ func revisionsCommand() *cobra.Command {
 		Args:  cobra.ExactArgs(1),
 		Run: func(cmd *cobra.Command, args []string) {
 			ctx := cmd.Context()
+			c, err := connection.ActiveConfig()
+			if err != nil {
+				log.FromContext(ctx).WithError(err).Fatal("Failed to get config")
+			}
+			args[0] = c.FQName(args[0])
+
 			client, err := connection.NewRegistryClient(ctx)
 			if err != nil {
 				log.FromContext(ctx).WithError(err).Fatal("Failed to get client")
@@ -44,8 +50,7 @@ func revisionsCommand() *cobra.Command {
 			taskQueue, wait := core.WorkerPool(ctx, 64)
 			defer wait()
 			// Generate tasks.
-			name := args[0]
-			if spec, err := names.ParseSpec(name); err == nil {
+			if spec, err := names.ParseSpec(args[0]); err == nil {
 				err = core.ListSpecs(ctx, client, spec, filter, func(spec *rpc.ApiSpec) error {
 					taskQueue <- &countSpecRevisionsTask{
 						client:     client,
@@ -57,7 +62,7 @@ func revisionsCommand() *cobra.Command {
 				if err != nil {
 					log.FromContext(ctx).WithError(err).Fatal("Failed to list API specs")
 				}
-			} else if deployment, err := names.ParseDeployment(name); err == nil {
+			} else if deployment, err := names.ParseDeployment(args[0]); err == nil {
 				err = core.ListDeployments(ctx, client, deployment, filter, func(deployment *rpc.ApiDeployment) error {
 					taskQueue <- &countDeploymentRevisionsTask{
 						client:           client,
diff --git a/cmd/registry/cmd/count/versions.go b/cmd/registry/cmd/count/versions.go
index 8d29cfb..fcc5145 100644
--- a/cmd/registry/cmd/count/versions.go
+++ b/cmd/registry/cmd/count/versions.go
@@ -33,9 +33,15 @@ func versionsCommand() *cobra.Command {
 	cmd := &cobra.Command{
 		Use:   "versions",
 		Short: "Count the number of versions of specified APIs",
-		Args:  cobra.MinimumNArgs(1),
+		Args:  cobra.ExactArgs(1),
 		Run: func(cmd *cobra.Command, args []string) {
 			ctx := cmd.Context()
+			c, err := connection.ActiveConfig()
+			if err != nil {
+				log.FromContext(ctx).WithError(err).Fatal("Failed to get config")
+			}
+			args[0] = c.FQName(args[0])
+
 			client, err := connection.NewRegistryClient(ctx)
 			if err != nil {
 				log.FromContext(ctx).WithError(err).Fatal("Failed to get client")
@@ -46,7 +52,7 @@ func versionsCommand() *cobra.Command {
 
 			api, err := names.ParseApi(args[0])
 			if err != nil {
-				return // TODO: Log an error.
+				log.FromContext(ctx).WithError(err).Fatal("Failed parse")
 			}
 
 			// Iterate through a collection of APIs and count the number of versions of each.
diff --git a/cmd/registry/cmd/delete/delete.go b/cmd/registry/cmd/delete/delete.go
index 9fb13e5..9ce451f 100644
--- a/cmd/registry/cmd/delete/delete.go
+++ b/cmd/registry/cmd/delete/delete.go
@@ -32,9 +32,15 @@ func Command() *cobra.Command {
 	cmd := &cobra.Command{
 		Use:   "delete",
 		Short: "Delete resources from the API Registry",
-		Args:  cobra.MinimumNArgs(1),
+		Args:  cobra.ExactArgs(1),
 		Run: func(cmd *cobra.Command, args []string) {
 			ctx := cmd.Context()
+			c, err := connection.ActiveConfig()
+			if err != nil {
+				log.FromContext(ctx).WithError(err).Fatal("Failed to get config")
+			}
+			args[0] = c.FQName(args[0])
+
 			client, err := connection.NewRegistryClient(ctx)
 			if err != nil {
 				log.FromContext(ctx).WithError(err).Fatal("Failed to get client")
diff --git a/cmd/registry/cmd/diff/diff.go b/cmd/registry/cmd/diff/diff.go
index bdae810..327a405 100644
--- a/cmd/registry/cmd/diff/diff.go
+++ b/cmd/registry/cmd/diff/diff.go
@@ -39,6 +39,14 @@ func Command() *cobra.Command {
 		Args:  cobra.ExactArgs(2),
 		Run: func(cmd *cobra.Command, args []string) {
 			ctx := cmd.Context()
+			c, err := connection.ActiveConfig()
+			if err != nil {
+				log.FromContext(ctx).WithError(err).Fatal("Failed to get config")
+			}
+			for i := range args {
+				args[i] = c.FQName(args[i])
+			}
+
 			client, err := connection.NewRegistryClient(ctx)
 			if err != nil {
 				log.FromContext(ctx).WithError(err).Fatal("Failed to get client")
diff --git a/cmd/registry/cmd/export/csv.go b/cmd/registry/cmd/export/csv.go
index 8476933..5ab544c 100644
--- a/cmd/registry/cmd/export/csv.go
+++ b/cmd/registry/cmd/export/csv.go
@@ -39,7 +39,12 @@ func csvCommand() *cobra.Command {
 		Use:   "csv [--filter expression] parent ...",
 		Short: "Export API specs to a CSV file",
 		Args: func(cmd *cobra.Command, args []string) error {
+			c, err := connection.ActiveConfig()
+			if err != nil {
+				return fmt.Errorf("Failed to get config: %s", err)
+			}
 			for _, parent := range args {
+				parent = c.FQName(parent)
 				if _, err := names.ParseVersion(parent); err != nil {
 					return fmt.Errorf("invalid parent argument %q", parent)
 				}
diff --git a/cmd/registry/cmd/export/sheet.go b/cmd/registry/cmd/export/sheet.go
index 773efda..bb0ab5f 100644
--- a/cmd/registry/cmd/export/sheet.go
+++ b/cmd/registry/cmd/export/sheet.go
@@ -44,6 +44,14 @@ func sheetCommand() *cobra.Command {
 		Args:  cobra.MinimumNArgs(1),
 		Run: func(cmd *cobra.Command, args []string) {
 			ctx := cmd.Context()
+			c, err := connection.ActiveConfig()
+			if err != nil {
+				log.FromContext(ctx).WithError(err).Fatal("Failed to get config")
+			}
+			for i := range args {
+				args[i] = c.FQName(args[i])
+			}
+
 			var path string
 			client, err := connection.NewRegistryClient(ctx)
 			if err != nil {
diff --git a/cmd/registry/cmd/export/yaml.go b/cmd/registry/cmd/export/yaml.go
index 98d3dfa..ed7e151 100644
--- a/cmd/registry/cmd/export/yaml.go
+++ b/cmd/registry/cmd/export/yaml.go
@@ -31,28 +31,29 @@ func yamlCommand() *cobra.Command {
 	cmd := &cobra.Command{
 		Use:   "yaml RESOURCE",
 		Short: "Export a subtree of the registry as YAML",
-		Args:  cobra.MinimumNArgs(1),
+		Args:  cobra.ExactArgs(1),
 		Run: func(cmd *cobra.Command, args []string) {
 			ctx := cmd.Context()
-			client, err := connection.NewRegistryClient(ctx)
+			c, err := connection.ActiveConfig()
 			if err != nil {
-				log.FromContext(ctx).WithError(err).Fatal("Failed to get client")
+				log.FromContext(ctx).WithError(err).Fatal("Failed to get config")
 			}
+			args[0] = c.FQName(args[0])
 
-			var name string
-			if len(args) > 0 {
-				name = args[0]
+			client, err := connection.NewRegistryClient(ctx)
+			if err != nil {
+				log.FromContext(ctx).WithError(err).Fatal("Failed to get client")
 			}
 
 			taskQueue, wait := core.WorkerPool(ctx, jobs)
 			defer wait()
 
-			if project, err := names.ParseProject(name); err == nil {
+			if project, err := names.ParseProject(args[0]); err == nil {
 				err = patch.ExportProject(ctx, client, project, taskQueue)
 				if err != nil {
 					log.FromContext(ctx).WithError(err).Fatal("Failed to export project YAML")
 				}
-			} else if api, err := names.ParseApi(name); err == nil {
+			} else if api, err := names.ParseApi(c.FQName(args[0])); err == nil {
 				err = core.GetAPI(ctx, client, api, func(message *rpc.Api) error {
 					bytes, _, err := patch.ExportAPI(ctx, client, message)
 					if err != nil {
@@ -64,7 +65,7 @@ func yamlCommand() *cobra.Command {
 				if err != nil {
 					log.FromContext(ctx).WithError(err).Fatal("Failed to export API YAML")
 				}
-			} else if artifact, err := names.ParseArtifact(name); err == nil {
+			} else if artifact, err := names.ParseArtifact(c.FQName(args[0])); err == nil {
 				err = core.GetArtifact(ctx, client, artifact, false, func(message *rpc.Artifact) error {
 					bytes, _, err := patch.ExportArtifact(ctx, client, message)
 					if err != nil {
@@ -78,7 +79,7 @@ func yamlCommand() *cobra.Command {
 					log.FromContext(ctx).WithError(err).Fatal("Failed to export artifact YAML")
 				}
 			} else {
-				log.Fatalf(ctx, "Unsupported entity %+s", name)
+				log.Fatalf(ctx, "Unsupported entity %+s", args[0])
 			}
 		},
 	}
diff --git a/cmd/registry/cmd/get/get.go b/cmd/registry/cmd/get/get.go
index bf56ca4..f8edcb6 100644
--- a/cmd/registry/cmd/get/get.go
+++ b/cmd/registry/cmd/get/get.go
@@ -27,9 +27,15 @@ func Command() *cobra.Command {
 	cmd := &cobra.Command{
 		Use:   "get",
 		Short: "Get resources from the API Registry",
-		Args:  cobra.MinimumNArgs(1),
+		Args:  cobra.ExactArgs(1),
 		Run: func(cmd *cobra.Command, args []string) {
 			ctx := cmd.Context()
+			c, err := connection.ActiveConfig()
+			if err != nil {
+				log.FromContext(ctx).WithError(err).Fatal("Failed to get config")
+			}
+			args[0] = c.FQName(args[0])
+
 			client, err := connection.NewRegistryClient(ctx)
 			if err != nil {
 				log.FromContext(ctx).WithError(err).Fatal("Failed to get client")
@@ -39,35 +45,30 @@ func Command() *cobra.Command {
 				log.FromContext(ctx).WithError(err).Fatal("Failed to get client")
 			}
 
-			var name string
-			if len(args) > 0 {
-				name = args[0]
-			}
-
 			var err2 error
-			if project, err := names.ParseProject(name); err == nil {
+			if project, err := names.ParseProject(args[0]); err == nil {
 				err2 = core.GetProject(ctx, adminClient, project, core.PrintProjectDetail)
-			} else if api, err := names.ParseApi(name); err == nil {
+			} else if api, err := names.ParseApi(args[0]); err == nil {
 				err2 = core.GetAPI(ctx, client, api, core.PrintAPIDetail)
-			} else if deployment, err := names.ParseDeployment(name); err == nil {
+			} else if deployment, err := names.ParseDeployment(args[0]); err == nil {
 				err2 = core.GetDeployment(ctx, client, deployment, core.PrintDeploymentDetail)
-			} else if deployment, err := names.ParseDeploymentRevision(name); err == nil {
+			} else if deployment, err := names.ParseDeploymentRevision(args[0]); err == nil {
 				err2 = core.GetDeploymentRevision(ctx, client, deployment, core.PrintDeploymentDetail)
-			} else if version, err := names.ParseVersion(name); err == nil {
+			} else if version, err := names.ParseVersion(args[0]); err == nil {
 				err2 = core.GetVersion(ctx, client, version, core.PrintVersionDetail)
-			} else if spec, err := names.ParseSpec(name); err == nil {
+			} else if spec, err := names.ParseSpec(args[0]); err == nil {
 				if getContents {
 					err2 = core.GetSpec(ctx, client, spec, getContents, core.PrintSpecContents)
 				} else {
 					err2 = core.GetSpec(ctx, client, spec, getContents, core.PrintSpecDetail)
 				}
-			} else if spec, err := names.ParseSpecRevision(name); err == nil {
+			} else if spec, err := names.ParseSpecRevision(args[0]); err == nil {
 				if getContents {
 					err2 = core.GetSpecRevision(ctx, client, spec, getContents, core.PrintSpecContents)
 				} else {
 					err2 = core.GetSpecRevision(ctx, client, spec, getContents, core.PrintSpecDetail)
 				}
-			} else if artifact, err := names.ParseArtifact(name); err == nil {
+			} else if artifact, err := names.ParseArtifact(args[0]); err == nil {
 				if getContents {
 					err2 = core.GetArtifact(ctx, client, artifact, getContents, core.PrintArtifactContents)
 				} else {
diff --git a/cmd/registry/cmd/index/index.go b/cmd/registry/cmd/index/index.go
index b4f5941..f58f93f 100644
--- a/cmd/registry/cmd/index/index.go
+++ b/cmd/registry/cmd/index/index.go
@@ -41,7 +41,13 @@ func Command() *cobra.Command {
 func collectInputIndexes(ctx context.Context, client connection.RegistryClient, args []string, filter string) ([]string, []*rpc.Index) {
 	inputNames := make([]string, 0)
 	inputs := make([]*rpc.Index, 0)
+	c, err := connection.ActiveConfig()
+	if err != nil {
+		log.FromContext(ctx).WithError(err).Fatal("Failed to get config")
+	}
+
 	for _, name := range args {
+		name = c.FQName(name)
 		artifact, err := names.ParseArtifact(name)
 		if err != nil {
 			continue
diff --git a/cmd/registry/cmd/label/label.go b/cmd/registry/cmd/label/label.go
index dd05cb6..13abd54 100644
--- a/cmd/registry/cmd/label/label.go
+++ b/cmd/registry/cmd/label/label.go
@@ -41,6 +41,12 @@ func Command() *cobra.Command {
 		Args:  cobra.MinimumNArgs(2),
 		Run: func(cmd *cobra.Command, args []string) {
 			ctx := cmd.Context()
+			c, err := connection.ActiveConfig()
+			if err != nil {
+				log.FromContext(ctx).WithError(err).Fatal("Failed to get config")
+			}
+			args[0] = c.FQName(args[0])
+
 			client, err := connection.NewRegistryClient(ctx)
 			if err != nil {
 				log.FromContext(ctx).WithError(err).Fatal("Failed to get client")
diff --git a/cmd/registry/cmd/list/list.go b/cmd/registry/cmd/list/list.go
index 3fbc8ad..821df88 100644
--- a/cmd/registry/cmd/list/list.go
+++ b/cmd/registry/cmd/list/list.go
@@ -31,9 +31,15 @@ func Command() *cobra.Command {
 	cmd := &cobra.Command{
 		Use:   "list",
 		Short: "List resources in the API Registry",
-		Args:  cobra.MinimumNArgs(1),
+		Args:  cobra.ExactArgs(1),
 		Run: func(cmd *cobra.Command, args []string) {
 			ctx := cmd.Context()
+			c, err := connection.ActiveConfig()
+			if err != nil {
+				log.FromContext(ctx).WithError(err).Fatal("Failed to get config")
+			}
+			args[0] = c.FQName(args[0])
+
 			client, err := connection.NewRegistryClient(ctx)
 			if err != nil {
 				log.FromContext(ctx).WithError(err).Fatal("Failed to get client")
@@ -76,7 +82,7 @@ func matchAndHandleListCmd(
 	}
 
 	// Then try to match resource names.
-	if project, err := names.ParseProjectCollection(name); err == nil {
+	if project, err := names.ParseProject(name); err == nil {
 		return core.ListProjects(ctx, adminClient, project, filter, core.PrintProject)
 	} else if api, err := names.ParseApi(name); err == nil {
 		return core.ListAPIs(ctx, client, api, filter, core.PrintAPI)
diff --git a/cmd/registry/cmd/resolve/resolve.go b/cmd/registry/cmd/resolve/resolve.go
index a6fb9d1..ffd370e 100644
--- a/cmd/registry/cmd/resolve/resolve.go
+++ b/cmd/registry/cmd/resolve/resolve.go
@@ -58,9 +58,15 @@ func Command() *cobra.Command {
 	cmd := &cobra.Command{
 		Use:   "resolve MANIFEST_RESOURCE",
 		Short: "resolve the dependencies and update the registry state (experimental)",
-		Args:  cobra.MinimumNArgs(1),
+		Args:  cobra.ExactArgs(1),
 		Run: func(cmd *cobra.Command, args []string) {
 			ctx := cmd.Context()
+			c, err := connection.ActiveConfig()
+			if err != nil {
+				log.FromContext(ctx).WithError(err).Fatal("Failed to get config")
+			}
+			args[0] = c.FQName(args[0])
+
 			name, err := names.ParseArtifact(args[0])
 			if err != nil {
 				log.FromContext(ctx).WithError(err).Fatal("Invalid manifest resource name")
diff --git a/cmd/registry/cmd/vocabulary/vocabulary.go b/cmd/registry/cmd/vocabulary/vocabulary.go
index ec5af63..26de935 100644
--- a/cmd/registry/cmd/vocabulary/vocabulary.go
+++ b/cmd/registry/cmd/vocabulary/vocabulary.go
@@ -46,9 +46,15 @@ func Command() *cobra.Command {
 }
 
 func collectInputVocabularies(ctx context.Context, client connection.RegistryClient, args []string, filter string) ([]string, []*metrics.Vocabulary) {
+	c, err := connection.ActiveConfig()
+	if err != nil {
+		log.FromContext(ctx).WithError(err).Fatal("Failed to get config")
+	}
+
 	inputNames := make([]string, 0)
 	inputs := make([]*metrics.Vocabulary, 0)
 	for _, name := range args {
+		name = c.FQName(name)
 		artifact, err := names.ParseArtifact(name)
 		if err != nil {
 			continue
diff --git a/pkg/config/config.go b/pkg/config/config.go
index 7b9d1ee..bbee70a 100644
--- a/pkg/config/config.go
+++ b/pkg/config/config.go
@@ -50,9 +50,11 @@ func init() {
 
 func CreateFlagSet() *pflag.FlagSet {
 	flags := pflag.NewFlagSet("registry", pflag.ExitOnError)
-	flags.StringP("config", "c", "", "Name of a configuration profile or path to config file")
+	flags.StringP("config", "c", "", "name of a configuration profile or path to config file")
 	flags.String("registry.address", "", "the server and port of the registry api (eg. localhost:8080)")
 	flags.Bool("registry.insecure", false, "if specified, client connects via http (not https)")
+	flags.String("registry.location", "", "the registry location")
+	flags.String("registry.project", "", "the registry project")
 	flags.String("registry.token", "", "the token to use for authorization to registry")
 	return flags
 }
diff --git a/pkg/config/configuration.go b/pkg/config/configuration.go
index d24f111..6d29f82 100644
--- a/pkg/config/configuration.go
+++ b/pkg/config/configuration.go
@@ -35,7 +35,9 @@ type Configuration struct {
 type Registry struct {
 	Address  string `mapstructure:"address" yaml:"address"`   // service address
 	Insecure bool   `mapstructure:"insecure" yaml:"insecure"` // if true, connect over HTTP
-	Token    string `mapstructure:"token" yaml:"-"`           // generated from TokenSource
+	Location string `mapstructure:"location" yaml:"location"`
+	Project  string `mapstructure:"project" yaml:"project"`
+	Token    string `mapstructure:"token" yaml:"-"` // generated from TokenSource
 }
 
 // Write stores the Configuration in the passed file name.
diff --git a/pkg/connection/config.go b/pkg/connection/config.go
index 08e4b57..c6bc2e3 100644
--- a/pkg/connection/config.go
+++ b/pkg/connection/config.go
@@ -15,6 +15,9 @@
 package connection
 
 import (
+	"path"
+	"strings"
+
 	"github.com/apigee/registry/pkg/config"
 )
 
@@ -22,6 +25,8 @@ import (
 type Config struct {
 	Address  string `mapstructure:"address"`  // service address
 	Insecure bool   `mapstructure:"insecure"` // if true, connect over HTTP
+	Location string `mapstructure:"location"` // optional
+	Project  string `mapstructure:"project"`  // optional
 	Token    string `mapstructure:"token"`    // bearer token
 }
 
@@ -43,12 +48,28 @@ func ReadConfig(name string) (Config, error) {
 		return Config{}, err
 	}
 
-	reg := c.Registry
 	config := Config{
-		Address:  reg.Address,
-		Insecure: reg.Insecure,
-		Token:    reg.Token,
+		Address:  c.Registry.Address,
+		Insecure: c.Registry.Insecure,
+		Location: c.Registry.Location,
+		Project:  c.Registry.Project,
+		Token:    c.Registry.Token,
 	}
 
 	return config, err
 }
+
+// FQName ensures the project and location, if available,
+// are properly included to make ensure the resource name
+// is fully qualified.
+func (c Config) FQName(name string) string {
+	name = strings.TrimPrefix(name, "/")
+	if !strings.HasPrefix(name, "projects") && c.Project != "" {
+		if strings.HasPrefix(name, "locations") {
+			name = path.Join("projects", c.Project, name)
+		} else if c.Location != "" {
+			name = path.Join("projects", c.Project, "locations", c.Location, name)
+		}
+	}
+	return name
+}


Test patch:
diff --git a/pkg/config/configuration_test.go b/pkg/config/configuration_test.go
index e3e3827..f70af9b 100644
--- a/pkg/config/configuration_test.go
+++ b/pkg/config/configuration_test.go
@@ -143,6 +143,8 @@ func TestSettingsDirectRead(t *testing.T) {
 		Registry: config.Registry{
 			Address:  "localhost:8080",
 			Insecure: true,
+			Location: "location",
+			Project:  "project",
 		},
 	}
 	err := c.Write("good")
@@ -178,6 +180,8 @@ func TestSettingsFlags(t *testing.T) {
 		Registry: config.Registry{
 			Address:  "localhost:8080",
 			Insecure: true,
+			Location: "location",
+			Project:  "project",
 			Token:    "token",
 		},
 	}
@@ -185,6 +189,8 @@ func TestSettingsFlags(t *testing.T) {
 		"test",
 		"--registry.address", want.Registry.Address,
 		"--registry.insecure", strconv.FormatBool(want.Registry.Insecure),
+		"--registry.location", want.Registry.Location,
+		"--registry.project", want.Registry.Project,
 		"--registry.token", want.Registry.Token,
 	}
 	config.Flags = config.CreateFlagSet()
@@ -227,12 +233,16 @@ func TestManipulations(t *testing.T) {
 		Registry: config.Registry{
 			Address:  "address",
 			Insecure: true,
+			Location: "location",
+			Project:  "project",
 			Token:    "token",
 		},
 	}
 	want := map[string]interface{}{
 		"registry.address":  c.Registry.Address,
 		"registry.insecure": c.Registry.Insecure,
+		"registry.location": c.Registry.Location,
+		"registry.project":  c.Registry.Project,
 		"registry.token":    c.Registry.Token,
 		"token-source":      "",
 	}
diff --git a/pkg/connection/config_test.go b/pkg/connection/config_test.go
new file mode 100644
index 0000000..6aa130f
--- /dev/null
+++ b/pkg/connection/config_test.go
@@ -0,0 +1,72 @@
+// Copyright 2022 Google LLC. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package connection
+
+import (
+	"testing"
+)
+
+func TestFQNamePartialConfig(t *testing.T) {
+	data := []struct {
+		input string
+		want  string
+	}{
+		{"projects/project1", "projects/project1"},
+		{"/projects/project1", "projects/project1"},
+		{"locations/foo", "locations/foo"},
+		{"/locations/foo", "locations/foo"},
+		{"apis/foo", "apis/foo"},
+		{"/apis/foo", "apis/foo"},
+	}
+
+	c := Config{
+		Location: "location",
+	}
+	for _, d := range data {
+		t.Run(d.input, func(t *testing.T) {
+			got := c.FQName(d.input)
+			if d.want != got {
+				t.Errorf("want: %q, got: %q", d.want, got)
+			}
+		})
+	}
+}
+
+func TestFQNameFullConfig(t *testing.T) {
+	data := []struct {
+		input string
+		want  string
+	}{
+		{"projects/foo", "projects/foo"},
+		{"/projects/foo", "projects/foo"},
+		{"", "projects/project1/locations/location1"},
+		{"/", "projects/project1/locations/location1"},
+		{"locations/foo", "projects/project1/locations/foo"},
+		{"/locations/foo", "projects/project1/locations/foo"},
+		{"projects/foo/locations/bar", "projects/foo/locations/bar"},
+		{"/apis/foo", "projects/project1/locations/location1/apis/foo"},
+	}
+
+	c := Config{
+		Project:  "project1",
+		Location: "location1",
+	}
+	for _, d := range data {
+		got := c.FQName(d.input)
+		if d.want != got {
+			t.Errorf("for: %q, want: %q, got: %q", d.input, d.want, got)
+		}
+	}
+}