Repository: interuss/dss (go)

Issue:
Implement versioning API
**Is your feature request related to a problem? Please describe.**
Since a DSS instance is merely a USS system component, versioning the entire USS system would be more straightforward if the InterUSS DSS implementation reported its software version according to a standard API.

**Describe the solution you'd like**
Implement [the InterUSS versioning API](https://github.com/interuss/automated_testing_interfaces/tree/main/versioning) in the InterUSS DSS implementation.

**Describe alternatives you've considered**
The existing [aux API](https://github.com/interuss/dss/blob/ce9e97db6c256c2a6f1d589fcac8cf52d7f7b723/interfaces/aux_/aux_.yaml#L24) does have an existing version endpoint.  In the monitoring repo's [VersionProviderSpecification](https://github.com/interuss/monitoring/blob/69b0cbc76afa29754188d6d2793b8487a11c6a1e/monitoring/uss_qualifier/resources/versioning/client.py#L22), we could add another field named something like `interuss_dss` which would produce a new type of versioning client (perhaps `InterUSSDSSVersionClient`) which would query the existing aux version endpoint.  But, I think that would be backward-looking while transitioning to the standard API would be more forward-looking and encourage USSs to do the same.


Pull request description:
[versioning] add new versioning endpoint
Adds the versioning API to the DSS, according to https://github.com/interuss/automated_testing_interfaces/tree/main/versioning.

This is done by:
 - adding a submodule that points to the versioning endpoint spec [repository](https://github.com/interuss/automated_testing_interfaces)
 - running the generator, moving the generated files to a relevant place and checking them into git
 - serving the new route and returning the current binary version for any requested identity.
 
The endpoint was tested locally via the `build/dev/read_version.sh` test script.

resolves #1047 

Patch:
diff --git a/.gitmodules b/.gitmodules
index 82b3e69..1a33029 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -6,4 +6,7 @@
 	url = https://github.com/uastech/standards
 [submodule "interfaces/rid/v2"]
 	path = interfaces/rid/v2
-	url = https://github.com/uastech/standards
\ No newline at end of file
+	url = https://github.com/uastech/standards
+[submodule "interfaces/automated_testing_interfaces"]
+	path = interfaces/automated_testing_interfaces
+	url = https://github.com/interuss/automated_testing_interfaces.git
diff --git a/build/dev/read_version.sh b/build/dev/read_version.sh
new file mode 100755
index 0000000..d5f5f50
--- /dev/null
+++ b/build/dev/read_version.sh
@@ -0,0 +1,13 @@
+#!/usr/bin/env bash
+
+set -eo pipefail
+
+# Retrieve token from dummy OAuth server
+ACCESS_TOKEN=$(curl --silent \
+    "http://localhost:8085/token?grant_type=client_credentials&scope=interuss.versioning.read_system_versions&intended_audience=localhost&issuer=localhost&sub=check_scd" \
+| python extract_json_field.py 'access_token')
+
+curl --silent -X GET  \
+"http://localhost:8082/versions/local.test.identity" \
+-H "Authorization: Bearer ${ACCESS_TOKEN}" -H "Content-Type: application/json"
+
diff --git a/cmds/core-service/main.go b/cmds/core-service/main.go
index e69017e..572ed11 100644
--- a/cmds/core-service/main.go
+++ b/cmds/core-service/main.go
@@ -20,6 +20,7 @@ import (
 	apiridv1 "github.com/interuss/dss/pkg/api/ridv1"
 	apiridv2 "github.com/interuss/dss/pkg/api/ridv2"
 	apiscdv1 "github.com/interuss/dss/pkg/api/scdv1"
+	apiversioningv1 "github.com/interuss/dss/pkg/api/versioningv1"
 	"github.com/interuss/dss/pkg/auth"
 	aux "github.com/interuss/dss/pkg/aux_"
 	"github.com/interuss/dss/pkg/build"
@@ -33,6 +34,7 @@ import (
 	"github.com/interuss/dss/pkg/scd"
 	scdc "github.com/interuss/dss/pkg/scd/store/cockroach"
 	"github.com/interuss/dss/pkg/version"
+	"github.com/interuss/dss/pkg/versioning"
 	"github.com/interuss/stacktrace"
 	"github.com/robfig/cron/v3"
 	"go.uber.org/zap"
@@ -219,11 +221,12 @@ func RunHTTPServer(ctx context.Context, ctxCanceler func(), address, locality st
 	}
 
 	var (
-		err         error
-		ridV1Server *rid_v1.Server
-		ridV2Server *rid_v2.Server
-		scdV1Server *scd.Server
-		auxV1Server = &aux.Server{}
+		err                error
+		ridV1Server        *rid_v1.Server
+		ridV2Server        *rid_v2.Server
+		scdV1Server        *scd.Server
+		auxV1Server        = &aux.Server{}
+		versioningV1Server = &versioning.Server{}
 	)
 
 	// Initialize remote ID
@@ -253,9 +256,16 @@ func RunHTTPServer(ctx context.Context, ctxCanceler func(), address, locality st
 	}
 
 	auxV1Router := apiauxv1.MakeAPIRouter(auxV1Server, authorizer)
+	versioningV1Router := apiversioningv1.MakeAPIRouter(versioningV1Server, authorizer)
 	ridV1Router := apiridv1.MakeAPIRouter(ridV1Server, authorizer)
 	ridV2Router := apiridv2.MakeAPIRouter(ridV2Server, authorizer)
-	multiRouter := api.MultiRouter{Routers: []api.PartialRouter{&auxV1Router, &ridV1Router, &ridV2Router}}
+	multiRouter := api.MultiRouter{
+		Routers: []api.PartialRouter{
+			&auxV1Router,
+			&versioningV1Router,
+			&ridV1Router,
+			&ridV2Router,
+		}}
 
 	// Initialize strategic conflict detection
 	if *enableSCD {
diff --git a/pkg/api/versioningv1/interface.gen.go b/pkg/api/versioningv1/interface.gen.go
new file mode 100644
index 0000000..eb050bc
--- /dev/null
+++ b/pkg/api/versioningv1/interface.gen.go
@@ -0,0 +1,47 @@
+// This file is auto-generated; do not change as any changes will be overwritten
+package versioning
+
+import (
+	"context"
+	"github.com/interuss/dss/pkg/api"
+)
+
+var (
+	InterussVersioningReadSystemVersionsScope = api.RequiredScope("interuss.versioning.read_system_versions")
+	GetVersionSecurity                        = []api.AuthorizationOption{
+		{
+			"Authority": {InterussVersioningReadSystemVersionsScope},
+		},
+	}
+)
+
+type GetVersionRequest struct {
+	// The system identity/boundary for which a version should be provided, if known.
+	SystemIdentity SystemBoundaryIdentifier
+
+	// The result of attempting to authorize this request
+	Auth api.AuthorizationResult
+}
+type GetVersionResponseSet struct {
+	// This interface successfully provided the version of the system identity/boundary that was requested.
+	Response200 *GetVersionResponse
+
+	// Bearer access token was not provided in Authorization header, token could not be decoded, or token was invalid.
+	Response401 *api.EmptyResponseBody
+
+	// The access token was decoded successfully but did not include a scope appropriate to this endpoint.
+	Response403 *api.EmptyResponseBody
+
+	// The requested system identity/boundary is not known, or the versioning automated testing interface is not available.
+	Response404 *api.EmptyResponseBody
+
+	// Auto-generated internal server error response
+	Response500 *api.InternalServerErrorBody
+}
+
+type Implementation interface {
+	// System version
+	// ---
+	// Get the requested system version.
+	GetVersion(ctx context.Context, req *GetVersionRequest) GetVersionResponseSet
+}
diff --git a/pkg/api/versioningv1/server.gen.go b/pkg/api/versioningv1/server.gen.go
new file mode 100644
index 0000000..219d910
--- /dev/null
+++ b/pkg/api/versioningv1/server.gen.go
@@ -0,0 +1,74 @@
+// This file is auto-generated; do not change as any changes will be overwritten
+package versioning
+
+import (
+	"context"
+	"github.com/interuss/dss/pkg/api"
+	"net/http"
+	"regexp"
+)
+
+type APIRouter struct {
+	Routes         []*api.Route
+	Implementation Implementation
+	Authorizer     api.Authorizer
+}
+
+// *versioning.APIRouter (type defined above) implements the api.PartialRouter interface
+func (s *APIRouter) Handle(w http.ResponseWriter, r *http.Request) bool {
+	for _, route := range s.Routes {
+		if route.Method == r.Method && route.Pattern.MatchString(r.URL.Path) {
+			route.Handler(route.Pattern, w, r)
+			return true
+		}
+	}
+	return false
+}
+
+func (s *APIRouter) GetVersion(exp *regexp.Regexp, w http.ResponseWriter, r *http.Request) {
+	var req GetVersionRequest
+
+	// Authorize request
+	req.Auth = s.Authorizer.Authorize(w, r, GetVersionSecurity)
+
+	// Parse path parameters
+	pathMatch := exp.FindStringSubmatch(r.URL.Path)
+	req.SystemIdentity = SystemBoundaryIdentifier(pathMatch[1])
+
+	// Call implementation
+	ctx, cancel := context.WithCancel(r.Context())
+	defer cancel()
+	response := s.Implementation.GetVersion(ctx, &req)
+
+	// Write response to client
+	if response.Response200 != nil {
+		api.WriteJSON(w, 200, response.Response200)
+		return
+	}
+	if response.Response401 != nil {
+		api.WriteJSON(w, 401, response.Response401)
+		return
+	}
+	if response.Response403 != nil {
+		api.WriteJSON(w, 403, response.Response403)
+		return
+	}
+	if response.Response404 != nil {
+		api.WriteJSON(w, 404, response.Response404)
+		return
+	}
+	if response.Response500 != nil {
+		api.WriteJSON(w, 500, response.Response500)
+		return
+	}
+	api.WriteJSON(w, 500, api.InternalServerErrorBody{ErrorMessage: "Handler implementation did not set a response"})
+}
+
+func MakeAPIRouter(impl Implementation, auth api.Authorizer) APIRouter {
+	router := APIRouter{Implementation: impl, Authorizer: auth, Routes: make([]*api.Route, 1)}
+
+	pattern := regexp.MustCompile("^/versions/(?P<system_identity>[^/]*)$")
+	router.Routes[0] = &api.Route{Method: http.MethodGet, Pattern: pattern, Handler: router.GetVersion}
+
+	return router
+}
diff --git a/pkg/api/versioningv1/types.gen.go b/pkg/api/versioningv1/types.gen.go
new file mode 100644
index 0000000..dd74ff2
--- /dev/null
+++ b/pkg/api/versioningv1/types.gen.go
@@ -0,0 +1,16 @@
+// This file is auto-generated; do not change as any changes will be overwritten
+package versioning
+
+// Identifier of a system boundary, known to both the client and the USS separate from this API, for which this interface can provide a version.  While the format is not prescribed by this API, any value must be URL-safe.  It is recommended to use an approach similar to reverse-order Internet domain names and Java packages where the global scope is described with increasingly-precise identifiers joined by periods.  For instance, the system boundary containing the mandatory Network Identification U-space service might be identified with `gov.eu.uspace.v1.netid` because the authority defining this system boundary is a governmental organization (specifically, the European Union) with requirements imposed on the system under test by the U-space regulation (first version) -- specifically, the Network Identification Service section.
+type SystemBoundaryIdentifier string
+
+// Identifier of a particular version of a system (defined by a known system boundary).  While the format is not prescribed by this API, a semantic version (https://semver.org/) prefixed with a `v` is recommended.
+type VersionIdentifier string
+
+type GetVersionResponse struct {
+	// The requested system identity/boundary.
+	SystemIdentity *SystemBoundaryIdentifier `json:"system_identity,omitempty"`
+
+	// The version of the system with the specified system identity/boundary.
+	SystemVersion *VersionIdentifier `json:"system_version,omitempty"`
+}
diff --git a/pkg/versioning/server.go b/pkg/versioning/server.go
new file mode 100644
index 0000000..ec407ad
--- /dev/null
+++ b/pkg/versioning/server.go
@@ -0,0 +1,44 @@
+package versioning
+
+import (
+	"context"
+	"github.com/interuss/dss/pkg/api"
+	versioning "github.com/interuss/dss/pkg/api/versioningv1"
+	dsserr "github.com/interuss/dss/pkg/errors"
+	"github.com/interuss/dss/pkg/version"
+	"github.com/interuss/stacktrace"
+)
+
+type Server struct {
+}
+
+func (s *Server) GetVersion(ctx context.Context, req *versioning.GetVersionRequest) versioning.GetVersionResponseSet {
+	// This should take care of unauthenticated requests as well as
+	// any request without the proper scope.
+	if req.Auth.Error != nil {
+		resp := versioning.GetVersionResponseSet{}
+		setAuthError(ctx, stacktrace.Propagate(req.Auth.Error, "Auth failed"), &resp.Response401, &resp.Response403, &resp.Response500)
+		return resp
+	}
+
+	// The DSS has no notion of particular system identities: whatever the request, we will
+	// always return the current version of the DSS binary.
+	versionStr := version.Current().String()
+	return versioning.GetVersionResponseSet{
+		Response200: &versioning.GetVersionResponse{
+			SystemIdentity: &req.SystemIdentity,
+			SystemVersion:  (*versioning.VersionIdentifier)(&versionStr),
+		},
+	}
+}
+
+func setAuthError(ctx context.Context, authErr error, resp401, resp403 **api.EmptyResponseBody, resp500 **api.InternalServerErrorBody) {
+	switch stacktrace.GetCode(authErr) {
+	case dsserr.Unauthenticated:
+		*resp401 = &api.EmptyResponseBody{}
+	case dsserr.PermissionDenied:
+		*resp403 = &api.EmptyResponseBody{}
+	default:
+		*resp500 = &api.InternalServerErrorBody{ErrorMessage: *dsserr.Handle(ctx, stacktrace.Propagate(authErr, "Could not perform authorization"))}
+	}
+}


Test patch:
diff --git a/interfaces/automated_testing_interfaces b/interfaces/automated_testing_interfaces
new file mode 160000
index 0000000..4e07f46
--- /dev/null
+++ b/interfaces/automated_testing_interfaces
@@ -0,0 +1,1 @@
+Subproject commit 4e07f46eb4452da761fb1658d3775d801d19312b
diff --git a/pkg/versioning/server_test.go b/pkg/versioning/server_test.go
new file mode 100644
index 0000000..1099e9c
--- /dev/null
+++ b/pkg/versioning/server_test.go
@@ -0,0 +1,31 @@
+package versioning
+
+import (
+	"context"
+	versioning "github.com/interuss/dss/pkg/api/versioningv1"
+	"github.com/interuss/dss/pkg/version"
+	"github.com/stretchr/testify/assert"
+	"testing"
+)
+
+func TestServer_GetVersion(t *testing.T) {
+	s := &Server{}
+
+	got := s.GetVersion(context.Background(),
+		&versioning.GetVersionRequest{
+			SystemIdentity: "empty",
+		}).Response200
+
+	assert.Equal(
+		t,
+		version.Current().String(),
+		string(*got.SystemVersion),
+	)
+
+	assert.Equal(
+		t,
+		"empty",
+		string(*got.SystemIdentity),
+	)
+
+}