Repository: apigee/registry (go)

Issue:
Registry tool: flatten `bulk` out of `registry upload` and bring subcommands up.
After #925 is merged, the `registry upload` subcommand will consist of `csv` and the bulk uploaders. With `registry apply` becoming our recommended mechanism for adding information to the registry, I think it would be reasonable to treat the `registry upload` subcommands as an opinionated set of special case handlers, and that it would make sense to have `csv` alongside `discovery`, `openapi`, and `proto`. It wouldn't be hard to argue that `csv` is also a bulk uploader, so there's not much conceptual difference among these commands -- all are opinionated uploaders that simplify special-case situations that we think are noteworthy enough to directly support.

@theganyo @seaneganx @giteshk - thoughts?

Pull request description:
Remove the "registry upload bulk" subcommand, moving its contents up to "registry upload"
Fixes #926. I also tweaked the `registry upload csv` command description to match the style of the others.

Known followup: #929

I think the code coverage change is noise - the small number uncovered lines are from a file that was deleted in the merge.

Patch:
diff --git a/cmd/registry/README.md b/cmd/registry/README.md
index cbaa21c..96b973a 100644
--- a/cmd/registry/README.md
+++ b/cmd/registry/README.md
@@ -68,43 +68,43 @@ The `registry` tool currently includes several demonstration subcommands that
 upload API descriptions into a registry and one subcommmand (`registry apply`)
 that is likely to become a recommended way to populate an API registry.
 
-- `registry upload bulk openapi` reads OpenAPI descriptions from a directory
+- `registry upload openapi` reads OpenAPI descriptions from a directory
   that follows the style of the
   [APIs-guru/openapi-directory](https://github.com/APIs-guru/openapi-directory)
   repository. To try it, clone the `openapi-directory` repo, change your
-  directory to the repo, and run the `registry upload bulk openapi` command as
+  directory to the repo, and run the `registry upload openapi` command as
   follows:
 
   ```
   git clone https://github.com/apis-guru/openapi-directory
   cd openapi-directory
-  registry upload bulk openapi APIs --project-id $PROJECT_ID
+  registry upload openapi APIs --project-id $PROJECT_ID
   ```
 
   Here `APIs` is a directory in the repo and `$PROJECT_ID` should be set to
   your registry project id.
 
-- `registry upload bulk protos` reads Protocol Buffer API descriptions from a
+- `registry upload protos` reads Protocol Buffer API descriptions from a
   directory that follows the style of the
   [googleapis/googleapis](https://github.com/googleapis/googleapis) repository.
   To try it, clone the `googleapis` repo, change your directory to the root of
-  the repo, and run the `registry upload bulk protos` command as follows:
+  the repo, and run the `registry upload protos` command as follows:
 
   ```
   git clone https://github.com/googleapis/googleapis
   cd googleapis
-  registry upload bulk protos . --project-id $PROJECT_ID
+  registry upload protos . --project-id $PROJECT_ID
   ```
 
   As above, `$PROJECT_ID` should be set to your registry project id.
 
-- `registry upload bulk discovery` reads API descriptions from the
+- `registry upload discovery` reads API descriptions from the
   [Google API Discovery Service](https://developers.google.com/discovery). This
   reads from an online service, so you can try it by simply running the
   following:
 
   ```
-  registry upload bulk discovery --project-id $PROJECT_ID
+  registry upload discovery --project-id $PROJECT_ID
   ```
 
   As above, `$PROJECT_ID` should be set to your registry project id.
diff --git a/cmd/registry/cmd/upload/bulk.go b/cmd/registry/cmd/upload/bulk.go
new file mode 100644
index 0000000..8de2342
--- /dev/null
+++ b/cmd/registry/cmd/upload/bulk.go
@@ -0,0 +1,15 @@
+// Copyright 2020 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 upload
diff --git a/cmd/registry/cmd/upload/bulk/bulk.go b/cmd/registry/cmd/upload/bulk/bulk.go
deleted file mode 100644
index e5115a8..0000000
--- a/cmd/registry/cmd/upload/bulk/bulk.go
+++ /dev/null
@@ -1,71 +0,0 @@
-// Copyright 2020 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 bulk
-
-import (
-	"errors"
-	"fmt"
-
-	"github.com/apigee/registry/log"
-	"github.com/apigee/registry/pkg/connection"
-	"github.com/spf13/cobra"
-)
-
-func Command() *cobra.Command {
-	cmd := &cobra.Command{
-		Use:   "bulk",
-		Short: "Bulk-upload API specs of selected styles",
-	}
-
-	cmd.AddCommand(discoveryCommand())
-	cmd.AddCommand(openAPICommand())
-	cmd.AddCommand(protosCommand())
-
-	cmd.PersistentFlags().String("project-id", "", "Project ID to use for each upload (deprecated)")
-	cmd.PersistentFlags().String("parent", "", "Parent for the upload (projects/PROJECT/locations/LOCATION)")
-	cmd.PersistentFlags().Int("jobs", 10, "Number of upload jobs to run simultaneously")
-	return cmd
-}
-
-func getParent(cmd *cobra.Command) (string, error) {
-	ctx := cmd.Context()
-
-	parent, err := cmd.Flags().GetString("parent")
-	if err != nil {
-		return "", fmt.Errorf("failed to get parent from flags (%s)", err)
-	}
-	projectID, err := cmd.Flags().GetString("project-id")
-	if err != nil {
-		return "", fmt.Errorf("failed to get project-id from flags (%s)", err)
-	}
-	if projectID != "" && parent != "" {
-		return "", errors.New("--project-id cannot be used with --parent")
-	}
-	if parent != "" {
-		return parent, nil
-	} else if projectID != "" {
-		log.FromContext(ctx).Warn("--project-id is deprecated, please use --parent or configure registry.project")
-		return "projects/" + projectID + "/locations/global", nil
-	}
-	c, err := connection.ActiveConfig()
-	if err != nil {
-		return "", fmt.Errorf("unable to identify parent (%s)", err)
-	}
-	parent, err = c.ProjectWithLocation()
-	if err != nil {
-		return "", fmt.Errorf("unable to identify parent: please use --parent or set registry.project in configuration (%s)", err)
-	}
-	return parent, nil
-}
diff --git a/cmd/registry/cmd/upload/csv.go b/cmd/registry/cmd/upload/csv.go
index ebdaa2c..bab2316 100644
--- a/cmd/registry/cmd/upload/csv.go
+++ b/cmd/registry/cmd/upload/csv.go
@@ -42,7 +42,7 @@ func csvCommand() *cobra.Command {
 
 	cmd := &cobra.Command{
 		Use:   "csv file --project-id=value [--delimiter=value]",
-		Short: "Upload API specs from a CSV file",
+		Short: "Upload API descriptions from a CSV file",
 		Args:  cobra.ExactArgs(1),
 		Run: func(cmd *cobra.Command, args []string) {
 			ctx := cmd.Context()
diff --git a/cmd/registry/cmd/upload/bulk/discovery.go b/cmd/registry/cmd/upload/discovery.go
similarity index 98%
rename from cmd/registry/cmd/upload/bulk/discovery.go
rename to cmd/registry/cmd/upload/discovery.go
index 6495413..9c2df13 100644
--- a/cmd/registry/cmd/upload/bulk/discovery.go
+++ b/cmd/registry/cmd/upload/discovery.go
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package bulk
+package upload
 
 import (
 	"context"
@@ -50,7 +50,7 @@ func discoveryCommand() *cobra.Command {
 	cmd := &cobra.Command{
 		Use:   "discovery",
 		Args:  cobra.NoArgs,
-		Short: "Bulk-upload API Discovery documents from the Google API Discovery service",
+		Short: "Upload API Discovery documents from the Google API Discovery service",
 		RunE: func(cmd *cobra.Command, args []string) error {
 			ctx := cmd.Context()
 			parent, err := getParent(cmd)
diff --git a/cmd/registry/cmd/upload/bulk/openapi.go b/cmd/registry/cmd/upload/openapi.go
similarity index 99%
rename from cmd/registry/cmd/upload/bulk/openapi.go
rename to cmd/registry/cmd/upload/openapi.go
index 3c0133e..506439e 100644
--- a/cmd/registry/cmd/upload/bulk/openapi.go
+++ b/cmd/registry/cmd/upload/openapi.go
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package bulk
+package upload
 
 import (
 	"context"
@@ -40,7 +40,7 @@ func openAPICommand() *cobra.Command {
 	var baseURI string
 	cmd := &cobra.Command{
 		Use:   "openapi",
-		Short: "Bulk-upload OpenAPI descriptions from a directory of specs",
+		Short: "Upload OpenAPI descriptions from a directory of specs",
 		Args:  cobra.MinimumNArgs(1),
 		RunE: func(cmd *cobra.Command, args []string) error {
 			ctx := cmd.Context()
diff --git a/cmd/registry/cmd/upload/bulk/protos.go b/cmd/registry/cmd/upload/protos.go
similarity index 99%
rename from cmd/registry/cmd/upload/bulk/protos.go
rename to cmd/registry/cmd/upload/protos.go
index 160ff09..7dd2fa9 100644
--- a/cmd/registry/cmd/upload/bulk/protos.go
+++ b/cmd/registry/cmd/upload/protos.go
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package bulk
+package upload
 
 import (
 	"context"
@@ -57,7 +57,7 @@ func protosCommand() *cobra.Command {
 	var root string
 	cmd := &cobra.Command{
 		Use:   "protos PATH",
-		Short: "Bulk-upload Protocol Buffer descriptions from a directory of specs",
+		Short: "Upload Protocol Buffer descriptions from a directory of specs",
 		Args:  cobra.ExactArgs(1),
 		RunE: func(cmd *cobra.Command, args []string) error {
 			ctx := cmd.Context()
diff --git a/cmd/registry/cmd/upload/bulk/testdata/discovery/apigeeregistry-v1.json b/cmd/registry/cmd/upload/testdata/discovery/apigeeregistry-v1.json
similarity index 100%
rename from cmd/registry/cmd/upload/bulk/testdata/discovery/apigeeregistry-v1.json
rename to cmd/registry/cmd/upload/testdata/discovery/apigeeregistry-v1.json
diff --git a/cmd/registry/cmd/upload/bulk/testdata/discovery/apis.json b/cmd/registry/cmd/upload/testdata/discovery/apis.json
similarity index 100%
rename from cmd/registry/cmd/upload/bulk/testdata/discovery/apis.json
rename to cmd/registry/cmd/upload/testdata/discovery/apis.json
diff --git a/cmd/registry/cmd/upload/bulk/testdata/discovery/discovery-v1.json b/cmd/registry/cmd/upload/testdata/discovery/discovery-v1.json
similarity index 100%
rename from cmd/registry/cmd/upload/bulk/testdata/discovery/discovery-v1.json
rename to cmd/registry/cmd/upload/testdata/discovery/discovery-v1.json
diff --git a/cmd/registry/cmd/upload/bulk/testdata/openapi/apigee/registry/v1/openapi.yaml b/cmd/registry/cmd/upload/testdata/openapi/apigee/registry/v1/openapi.yaml
similarity index 100%
rename from cmd/registry/cmd/upload/bulk/testdata/openapi/apigee/registry/v1/openapi.yaml
rename to cmd/registry/cmd/upload/testdata/openapi/apigee/registry/v1/openapi.yaml
diff --git a/cmd/registry/cmd/upload/bulk/testdata/openapi/petstore/2.0/swagger.yaml b/cmd/registry/cmd/upload/testdata/openapi/petstore/2.0/swagger.yaml
similarity index 100%
rename from cmd/registry/cmd/upload/bulk/testdata/openapi/petstore/2.0/swagger.yaml
rename to cmd/registry/cmd/upload/testdata/openapi/petstore/2.0/swagger.yaml
diff --git a/cmd/registry/cmd/upload/bulk/testdata/openapi/petstore/3.0/openapi.yaml b/cmd/registry/cmd/upload/testdata/openapi/petstore/3.0/openapi.yaml
similarity index 100%
rename from cmd/registry/cmd/upload/bulk/testdata/openapi/petstore/3.0/openapi.yaml
rename to cmd/registry/cmd/upload/testdata/openapi/petstore/3.0/openapi.yaml
diff --git a/cmd/registry/cmd/upload/bulk/testdata/protos/google/api/annotations.proto b/cmd/registry/cmd/upload/testdata/protos/google/api/annotations.proto
similarity index 100%
rename from cmd/registry/cmd/upload/bulk/testdata/protos/google/api/annotations.proto
rename to cmd/registry/cmd/upload/testdata/protos/google/api/annotations.proto
diff --git a/cmd/registry/cmd/upload/bulk/testdata/protos/google/api/client.proto b/cmd/registry/cmd/upload/testdata/protos/google/api/client.proto
similarity index 100%
rename from cmd/registry/cmd/upload/bulk/testdata/protos/google/api/client.proto
rename to cmd/registry/cmd/upload/testdata/protos/google/api/client.proto
diff --git a/cmd/registry/cmd/upload/bulk/testdata/protos/google/api/field_behavior.proto b/cmd/registry/cmd/upload/testdata/protos/google/api/field_behavior.proto
similarity index 100%
rename from cmd/registry/cmd/upload/bulk/testdata/protos/google/api/field_behavior.proto
rename to cmd/registry/cmd/upload/testdata/protos/google/api/field_behavior.proto
diff --git a/cmd/registry/cmd/upload/bulk/testdata/protos/google/api/http.proto b/cmd/registry/cmd/upload/testdata/protos/google/api/http.proto
similarity index 100%
rename from cmd/registry/cmd/upload/bulk/testdata/protos/google/api/http.proto
rename to cmd/registry/cmd/upload/testdata/protos/google/api/http.proto
diff --git a/cmd/registry/cmd/upload/bulk/testdata/protos/google/api/httpbody.proto b/cmd/registry/cmd/upload/testdata/protos/google/api/httpbody.proto
similarity index 100%
rename from cmd/registry/cmd/upload/bulk/testdata/protos/google/api/httpbody.proto
rename to cmd/registry/cmd/upload/testdata/protos/google/api/httpbody.proto
diff --git a/cmd/registry/cmd/upload/bulk/testdata/protos/google/api/resource.proto b/cmd/registry/cmd/upload/testdata/protos/google/api/resource.proto
similarity index 100%
rename from cmd/registry/cmd/upload/bulk/testdata/protos/google/api/resource.proto
rename to cmd/registry/cmd/upload/testdata/protos/google/api/resource.proto
diff --git a/cmd/registry/cmd/upload/bulk/testdata/protos/google/cloud/apigeeregistry/v1/BUILD.bazel b/cmd/registry/cmd/upload/testdata/protos/google/cloud/apigeeregistry/v1/BUILD.bazel
similarity index 100%
rename from cmd/registry/cmd/upload/bulk/testdata/protos/google/cloud/apigeeregistry/v1/BUILD.bazel
rename to cmd/registry/cmd/upload/testdata/protos/google/cloud/apigeeregistry/v1/BUILD.bazel
diff --git a/cmd/registry/cmd/upload/bulk/testdata/protos/google/cloud/apigeeregistry/v1/apigeeregistry_grpc_service_config.json b/cmd/registry/cmd/upload/testdata/protos/google/cloud/apigeeregistry/v1/apigeeregistry_grpc_service_config.json
similarity index 100%
rename from cmd/registry/cmd/upload/bulk/testdata/protos/google/cloud/apigeeregistry/v1/apigeeregistry_grpc_service_config.json
rename to cmd/registry/cmd/upload/testdata/protos/google/cloud/apigeeregistry/v1/apigeeregistry_grpc_service_config.json
diff --git a/cmd/registry/cmd/upload/bulk/testdata/protos/google/cloud/apigeeregistry/v1/apigeeregistry_v1.yaml b/cmd/registry/cmd/upload/testdata/protos/google/cloud/apigeeregistry/v1/apigeeregistry_v1.yaml
similarity index 100%
rename from cmd/registry/cmd/upload/bulk/testdata/protos/google/cloud/apigeeregistry/v1/apigeeregistry_v1.yaml
rename to cmd/registry/cmd/upload/testdata/protos/google/cloud/apigeeregistry/v1/apigeeregistry_v1.yaml
diff --git a/cmd/registry/cmd/upload/bulk/testdata/protos/google/cloud/apigeeregistry/v1/provisioning_service.proto b/cmd/registry/cmd/upload/testdata/protos/google/cloud/apigeeregistry/v1/provisioning_service.proto
similarity index 100%
rename from cmd/registry/cmd/upload/bulk/testdata/protos/google/cloud/apigeeregistry/v1/provisioning_service.proto
rename to cmd/registry/cmd/upload/testdata/protos/google/cloud/apigeeregistry/v1/provisioning_service.proto
diff --git a/cmd/registry/cmd/upload/bulk/testdata/protos/google/cloud/apigeeregistry/v1/registry_models.proto b/cmd/registry/cmd/upload/testdata/protos/google/cloud/apigeeregistry/v1/registry_models.proto
similarity index 100%
rename from cmd/registry/cmd/upload/bulk/testdata/protos/google/cloud/apigeeregistry/v1/registry_models.proto
rename to cmd/registry/cmd/upload/testdata/protos/google/cloud/apigeeregistry/v1/registry_models.proto
diff --git a/cmd/registry/cmd/upload/bulk/testdata/protos/google/cloud/apigeeregistry/v1/registry_service.proto b/cmd/registry/cmd/upload/testdata/protos/google/cloud/apigeeregistry/v1/registry_service.proto
similarity index 100%
rename from cmd/registry/cmd/upload/bulk/testdata/protos/google/cloud/apigeeregistry/v1/registry_service.proto
rename to cmd/registry/cmd/upload/testdata/protos/google/cloud/apigeeregistry/v1/registry_service.proto
diff --git a/cmd/registry/cmd/upload/bulk/testdata/protos/google/example/library/README.md b/cmd/registry/cmd/upload/testdata/protos/google/example/library/README.md
similarity index 100%
rename from cmd/registry/cmd/upload/bulk/testdata/protos/google/example/library/README.md
rename to cmd/registry/cmd/upload/testdata/protos/google/example/library/README.md
diff --git a/cmd/registry/cmd/upload/bulk/testdata/protos/google/example/library/v1/BUILD.bazel b/cmd/registry/cmd/upload/testdata/protos/google/example/library/v1/BUILD.bazel
similarity index 100%
rename from cmd/registry/cmd/upload/bulk/testdata/protos/google/example/library/v1/BUILD.bazel
rename to cmd/registry/cmd/upload/testdata/protos/google/example/library/v1/BUILD.bazel
diff --git a/cmd/registry/cmd/upload/bulk/testdata/protos/google/example/library/v1/library.proto b/cmd/registry/cmd/upload/testdata/protos/google/example/library/v1/library.proto
similarity index 100%
rename from cmd/registry/cmd/upload/bulk/testdata/protos/google/example/library/v1/library.proto
rename to cmd/registry/cmd/upload/testdata/protos/google/example/library/v1/library.proto
diff --git a/cmd/registry/cmd/upload/bulk/testdata/protos/google/example/library/v1/library_example_gapic.yaml b/cmd/registry/cmd/upload/testdata/protos/google/example/library/v1/library_example_gapic.yaml
similarity index 100%
rename from cmd/registry/cmd/upload/bulk/testdata/protos/google/example/library/v1/library_example_gapic.yaml
rename to cmd/registry/cmd/upload/testdata/protos/google/example/library/v1/library_example_gapic.yaml
diff --git a/cmd/registry/cmd/upload/bulk/testdata/protos/google/example/library/v1/library_example_v1.yaml b/cmd/registry/cmd/upload/testdata/protos/google/example/library/v1/library_example_v1.yaml
similarity index 100%
rename from cmd/registry/cmd/upload/bulk/testdata/protos/google/example/library/v1/library_example_v1.yaml
rename to cmd/registry/cmd/upload/testdata/protos/google/example/library/v1/library_example_v1.yaml
diff --git a/cmd/registry/cmd/upload/bulk/testdata/protos/google/example/library/v1/library_grpc_service_config.json b/cmd/registry/cmd/upload/testdata/protos/google/example/library/v1/library_grpc_service_config.json
similarity index 100%
rename from cmd/registry/cmd/upload/bulk/testdata/protos/google/example/library/v1/library_grpc_service_config.json
rename to cmd/registry/cmd/upload/testdata/protos/google/example/library/v1/library_grpc_service_config.json
diff --git a/cmd/registry/cmd/upload/bulk/testdata/protos/google/longrunning/operations.proto b/cmd/registry/cmd/upload/testdata/protos/google/longrunning/operations.proto
similarity index 100%
rename from cmd/registry/cmd/upload/bulk/testdata/protos/google/longrunning/operations.proto
rename to cmd/registry/cmd/upload/testdata/protos/google/longrunning/operations.proto
diff --git a/cmd/registry/cmd/upload/bulk/testdata/protos/google/rpc/status.proto b/cmd/registry/cmd/upload/testdata/protos/google/rpc/status.proto
similarity index 100%
rename from cmd/registry/cmd/upload/bulk/testdata/protos/google/rpc/status.proto
rename to cmd/registry/cmd/upload/testdata/protos/google/rpc/status.proto
diff --git a/cmd/registry/cmd/upload/upload.go b/cmd/registry/cmd/upload/upload.go
index 79866bb..14b6497 100644
--- a/cmd/registry/cmd/upload/upload.go
+++ b/cmd/registry/cmd/upload/upload.go
@@ -15,7 +15,11 @@
 package upload
 
 import (
-	"github.com/apigee/registry/cmd/registry/cmd/upload/bulk"
+	"errors"
+	"fmt"
+
+	"github.com/apigee/registry/log"
+	"github.com/apigee/registry/pkg/connection"
 	"github.com/spf13/cobra"
 )
 
@@ -25,8 +29,45 @@ func Command() *cobra.Command {
 		Short: "Upload information to the API Registry",
 	}
 
-	cmd.AddCommand(bulk.Command())
 	cmd.AddCommand(csvCommand())
+	cmd.AddCommand(discoveryCommand())
+	cmd.AddCommand(openAPICommand())
+	cmd.AddCommand(protosCommand())
+
+	cmd.PersistentFlags().String("project-id", "", "project ID to use for each upload (deprecated)")
+	cmd.PersistentFlags().String("parent", "", "parent for the upload (projects/PROJECT/locations/LOCATION)")
+	cmd.PersistentFlags().IntP("jobs", "j", 10, "number of actions to perform concurrently")
 
 	return cmd
 }
+
+func getParent(cmd *cobra.Command) (string, error) {
+	ctx := cmd.Context()
+
+	parent, err := cmd.Flags().GetString("parent")
+	if err != nil {
+		return "", fmt.Errorf("failed to get parent from flags (%s)", err)
+	}
+	projectID, err := cmd.Flags().GetString("project-id")
+	if err != nil {
+		return "", fmt.Errorf("failed to get project-id from flags (%s)", err)
+	}
+	if projectID != "" && parent != "" {
+		return "", errors.New("--project-id cannot be used with --parent")
+	}
+	if parent != "" {
+		return parent, nil
+	} else if projectID != "" {
+		log.FromContext(ctx).Warn("--project-id is deprecated, please use --parent or configure registry.project")
+		return "projects/" + projectID + "/locations/global", nil
+	}
+	c, err := connection.ActiveConfig()
+	if err != nil {
+		return "", fmt.Errorf("unable to identify parent (%s)", err)
+	}
+	parent, err = c.ProjectWithLocation()
+	if err != nil {
+		return "", fmt.Errorf("unable to identify parent: please use --parent or set registry.project in configuration (%s)", err)
+	}
+	return parent, nil
+}
diff --git a/demos/disco.sh b/demos/disco.sh
index f2d77e5..45b56ed 100755
--- a/demos/disco.sh
+++ b/demos/disco.sh
@@ -31,7 +31,7 @@
 
 # Upload all of the APIs from the Discovery Service at once.
 # This happens in parallel and usually takes a minute or two.
-registry upload bulk discovery \
+registry upload discovery \
 	--project-id $PROJECT
 
 # We can get a list of APIs with the following command:
diff --git a/demos/openapi.sh b/demos/openapi.sh
index f744d56..ef443fe 100755
--- a/demos/openapi.sh
+++ b/demos/openapi.sh
@@ -34,7 +34,7 @@ export COMMIT=`(cd ~/Desktop/openapi-directory; git rev-parse HEAD)`
 
 # Upload all of the APIs in the OpenAPI directory at once.
 # This happens in parallel and usually takes around 2 minutes.
-registry upload bulk openapi \
+registry upload openapi \
 	--project-id $PROJECT ~/Desktop/openapi-directory/APIs \
 	--base-uri https://github.com/APIs-guru/openapi-directory/blob/$COMMIT/APIs 
 
diff --git a/demos/protos.sh b/demos/protos.sh
index 51129df..432c40e 100755
--- a/demos/protos.sh
+++ b/demos/protos.sh
@@ -35,11 +35,11 @@ export COMMIT=`(cd ~/Desktop/googleapis; git rev-parse HEAD)`
 
 # Upload all of the APIs in the googleapis directory at once.
 # This happens in parallel and usually takes less than 10 seconds.
-registry upload bulk protos \
+registry upload protos \
 	--project-id $PROJECT ~/Desktop/googleapis \
 	--base-uri https://github.com/googleapis/googleapis/blob/$COMMIT 
 
-# The `registry upload bulk protos` subcommand automatically generated API ids
+# The `registry upload protos` subcommand automatically generated API ids
 # from the path to the protos in the repo. List the APIs with the following command:
 registry get projects/$PROJECT/locations/global/apis
 


Test patch:
diff --git a/cmd/registry/cmd/upload/bulk/bulk_test.go b/cmd/registry/cmd/upload/bulk/bulk_test.go
deleted file mode 100644
index 71f0469..0000000
--- a/cmd/registry/cmd/upload/bulk/bulk_test.go
+++ /dev/null
@@ -1,130 +0,0 @@
-// 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 bulk
-
-import (
-	"context"
-	"testing"
-
-	"github.com/apigee/registry/pkg/connection"
-	"github.com/apigee/registry/pkg/connection/grpctest"
-	"github.com/apigee/registry/server/registry"
-)
-
-// TestMain will set up a local RegistryServer and grpc.Server for all
-// tests in this package if APG_REGISTRY_ADDRESS env var is not set
-// for the client.
-func TestMain(m *testing.M) {
-	grpctest.TestMain(m, registry.Config{})
-}
-
-func TestParentFromFlags(t *testing.T) {
-	tests := []struct {
-		desc  string
-		args  []string
-		want  string
-		fails bool
-	}{
-		{
-			desc: "parent from deprecated projectid flag",
-			args: []string{"--project-id", "sample"},
-			want: "projects/sample/locations/global",
-		},
-		{
-			desc: "parent from parent flag",
-			args: []string{"--parent", "projects/sample/locations/other"},
-			want: "projects/sample/locations/other",
-		},
-		{
-			desc:  "parent from projectid and parent flags",
-			args:  []string{"--parent", "projects/sample/locations/global", "--project-id", "sample"},
-			fails: true,
-		},
-	}
-	for _, test := range tests {
-		t.Run(test.desc, func(t *testing.T) {
-			cmd := Command()
-			cmd.SetContext(context.Background())
-			if err := cmd.ParseFlags(test.args); err != nil {
-				t.Fatalf("Failed to parse flags")
-			}
-			parent, err := getParent(cmd)
-			if err != nil {
-				if !test.fails {
-					t.Errorf("Get parent unexpectedly failed with error: %s", err)
-				}
-			} else {
-				if test.fails {
-					t.Errorf("Get parent unexpectedly succeeded")
-				} else if parent != test.want {
-					t.Errorf("Get parent: wanted %s, got %s", test.want, parent)
-				}
-			}
-		})
-	}
-}
-
-func TestParentFromConfiguration(t *testing.T) {
-	tests := []struct {
-		desc       string
-		projectId  string
-		locationId string
-		want       string
-		fails      bool
-	}{
-		{
-			desc:       "configured with specified location",
-			projectId:  "sample",
-			locationId: "other",
-			want:       "projects/sample/locations/other",
-		},
-		{
-			desc:       "configured with default location",
-			projectId:  "sample",
-			locationId: "",
-			want:       "projects/sample/locations/global",
-		},
-		{
-			desc:       "configured with no location",
-			projectId:  "",
-			locationId: "",
-			want:       "",
-			fails:      true,
-		},
-	}
-	for _, test := range tests {
-		t.Run(test.desc, func(t *testing.T) {
-			config, err := connection.ActiveConfig()
-			if err != nil {
-				t.Fatalf("Setup: Failed to get registry configuration: %s", err)
-			}
-			config.Project = test.projectId
-			config.Location = test.locationId
-			connection.SetConfig(config)
-			cmd := Command()
-			cmd.SetContext(context.Background())
-			if err := cmd.ParseFlags([]string{}); err != nil {
-				t.Fatalf("Failed to parse flags")
-			}
-			parent, err := getParent(cmd)
-			if test.fails && err == nil {
-				t.Errorf("Get parent was expected to fail and didn't")
-			} else if !test.fails && err != nil {
-				t.Errorf("Get parent unexpectedly failed with error: %s", err)
-			} else if parent != test.want {
-				t.Errorf("Get parent: wanted %s, got %s", test.want, parent)
-			}
-		})
-	}
-}
diff --git a/cmd/registry/cmd/upload/bulk/discovery_test.go b/cmd/registry/cmd/upload/discovery_test.go
similarity index 99%
rename from cmd/registry/cmd/upload/bulk/discovery_test.go
rename to cmd/registry/cmd/upload/discovery_test.go
index 89b19d7..51933e0 100644
--- a/cmd/registry/cmd/upload/bulk/discovery_test.go
+++ b/cmd/registry/cmd/upload/discovery_test.go
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package bulk
+package upload
 
 import (
 	"context"
diff --git a/cmd/registry/cmd/upload/bulk/openapi_test.go b/cmd/registry/cmd/upload/openapi_test.go
similarity index 99%
rename from cmd/registry/cmd/upload/bulk/openapi_test.go
rename to cmd/registry/cmd/upload/openapi_test.go
index d2fff12..0b8871d 100644
--- a/cmd/registry/cmd/upload/bulk/openapi_test.go
+++ b/cmd/registry/cmd/upload/openapi_test.go
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package bulk
+package upload
 
 import (
 	"context"
diff --git a/cmd/registry/cmd/upload/bulk/protos_test.go b/cmd/registry/cmd/upload/protos_test.go
similarity index 99%
rename from cmd/registry/cmd/upload/bulk/protos_test.go
rename to cmd/registry/cmd/upload/protos_test.go
index 0d32033..8f14537 100644
--- a/cmd/registry/cmd/upload/bulk/protos_test.go
+++ b/cmd/registry/cmd/upload/protos_test.go
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package bulk
+package upload
 
 import (
 	"context"
diff --git a/cmd/registry/cmd/upload/upload_test.go b/cmd/registry/cmd/upload/upload_test.go
index e279279..ad806a3 100644
--- a/cmd/registry/cmd/upload/upload_test.go
+++ b/cmd/registry/cmd/upload/upload_test.go
@@ -11,12 +11,13 @@
 // 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 upload
 
 import (
+	"context"
 	"testing"
 
+	"github.com/apigee/registry/pkg/connection"
 	"github.com/apigee/registry/pkg/connection/grpctest"
 	"github.com/apigee/registry/server/registry"
 )
@@ -27,3 +28,103 @@ import (
 func TestMain(m *testing.M) {
 	grpctest.TestMain(m, registry.Config{})
 }
+
+func TestParentFromFlags(t *testing.T) {
+	tests := []struct {
+		desc  string
+		args  []string
+		want  string
+		fails bool
+	}{
+		{
+			desc: "parent from deprecated projectid flag",
+			args: []string{"--project-id", "sample"},
+			want: "projects/sample/locations/global",
+		},
+		{
+			desc: "parent from parent flag",
+			args: []string{"--parent", "projects/sample/locations/other"},
+			want: "projects/sample/locations/other",
+		},
+		{
+			desc:  "parent from projectid and parent flags",
+			args:  []string{"--parent", "projects/sample/locations/global", "--project-id", "sample"},
+			fails: true,
+		},
+	}
+	for _, test := range tests {
+		t.Run(test.desc, func(t *testing.T) {
+			cmd := Command()
+			cmd.SetContext(context.Background())
+			if err := cmd.ParseFlags(test.args); err != nil {
+				t.Fatalf("Failed to parse flags")
+			}
+			parent, err := getParent(cmd)
+			if err != nil {
+				if !test.fails {
+					t.Errorf("Get parent unexpectedly failed with error: %s", err)
+				}
+			} else {
+				if test.fails {
+					t.Errorf("Get parent unexpectedly succeeded")
+				} else if parent != test.want {
+					t.Errorf("Get parent: wanted %s, got %s", test.want, parent)
+				}
+			}
+		})
+	}
+}
+
+func TestParentFromConfiguration(t *testing.T) {
+	tests := []struct {
+		desc       string
+		projectId  string
+		locationId string
+		want       string
+		fails      bool
+	}{
+		{
+			desc:       "configured with specified location",
+			projectId:  "sample",
+			locationId: "other",
+			want:       "projects/sample/locations/other",
+		},
+		{
+			desc:       "configured with default location",
+			projectId:  "sample",
+			locationId: "",
+			want:       "projects/sample/locations/global",
+		},
+		{
+			desc:       "configured with no location",
+			projectId:  "",
+			locationId: "",
+			want:       "",
+			fails:      true,
+		},
+	}
+	for _, test := range tests {
+		t.Run(test.desc, func(t *testing.T) {
+			config, err := connection.ActiveConfig()
+			if err != nil {
+				t.Fatalf("Setup: Failed to get registry configuration: %s", err)
+			}
+			config.Project = test.projectId
+			config.Location = test.locationId
+			connection.SetConfig(config)
+			cmd := Command()
+			cmd.SetContext(context.Background())
+			if err := cmd.ParseFlags([]string{}); err != nil {
+				t.Fatalf("Failed to parse flags")
+			}
+			parent, err := getParent(cmd)
+			if test.fails && err == nil {
+				t.Errorf("Get parent was expected to fail and didn't")
+			} else if !test.fails && err != nil {
+				t.Errorf("Get parent unexpectedly failed with error: %s", err)
+			} else if parent != test.want {
+				t.Errorf("Get parent: wanted %s, got %s", test.want, parent)
+			}
+		})
+	}
+}