You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@trafficcontrol.apache.org by zr...@apache.org on 2023/08/28 19:59:16 UTC

[trafficcontrol] branch master updated: Use RFC 3339 for lastUpdated timestamp in /server_server_capabilities (#7744)

This is an automated email from the ASF dual-hosted git repository.

zrhoffman pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/trafficcontrol.git


The following commit(s) were added to refs/heads/master by this push:
     new 62bd082cb4 Use RFC 3339 for lastUpdated timestamp in /server_server_capabilities (#7744)
62bd082cb4 is described below

commit 62bd082cb451fd25a5633de03b90077f5070e334
Author: Zach Hoffman <zr...@apache.org>
AuthorDate: Mon Aug 28 13:59:10 2023 -0600

    Use RFC 3339 for lastUpdated timestamp in /server_server_capabilities (#7744)
    
    * Use RFC 3339 for lastUpdated timestamp in /server_server_capabilities
    
    * Match function name in Godoc
---
 CHANGELOG.md                                       |   1 +
 docs/source/api/v5/server_server_capabilities.rst  |  10 +-
 lib/go-tc/server_server_capability.go              |  26 ++
 .../api/v5/server_server_capabilities_test.go      |  20 +-
 traffic_ops/testing/api/v5/traffic_control_test.go |   2 +-
 traffic_ops/traffic_ops_golang/routing/routes.go   |   6 +-
 .../server/servers_server_capability.go            | 282 +++++++++++++++++++++
 .../server/servers_server_capability_test.go       | 179 +++++++++++++
 .../v5-client/server_server_capabilities.go        |   6 +-
 9 files changed, 510 insertions(+), 22 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6dd1d9e647..a435a14d61 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -100,6 +100,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/).
     - [#7691](https://github.com/apache/trafficcontrol/pull/7691) *Traffic Ops* Fixed `/topologies` v5 APIs to respond with `RFC3339` timestamps.
     - [#7413](https://github.com/apache/trafficcontrol/issues/7413) *Traffic Ops* Fixed `/service_category` v5 APIs to respond with `RFC3339` timestamps.
     - [#7413](https://github.com/apache/trafficcontrol/issues/7706) *Traffic Ops* Fixed `/statuses` v5 APIs to respond with `RFC3339` timestamps.
+- [#7743](https://github.com/apache/trafficcontrol/issues/7743) *Traffic Ops* Fixes /server_server_capabilities apis to respond with RFC3339 date/time format
 - [#7730](https://github.com/apache/trafficcontrol/pull/7730) *Traffic Monitor* Fixed the panic seen in TM when `plugin.system_stats.timestamp_ms` appears as float and not string.
 - [#4393](https://github.com/apache/trafficcontrol/issues/4393) *Traffic Ops* Fixed the error code and alert structure when TO is queried for a delivery service with no ssl keys.
 - [#7623](https://github.com/apache/trafficcontrol/pull/7623) *Traffic Ops* Removed TryIfModifiedSinceQuery from servicecategories.go and reused from ims.go
diff --git a/docs/source/api/v5/server_server_capabilities.rst b/docs/source/api/v5/server_server_capabilities.rst
index 07af380aec..9a626d96a8 100644
--- a/docs/source/api/v5/server_server_capabilities.rst
+++ b/docs/source/api/v5/server_server_capabilities.rst
@@ -66,7 +66,7 @@ Response Structure
 ------------------
 :serverHostName:   The server's host name
 :serverId:         The server's integral, unique identifier
-:lastUpdated:      The date and time at which this association between the server and the :term:`Server Capability` was last updated, in :ref:`non-rfc-datetime`
+:lastUpdated:      The date and time at which this association between the server and the :term:`Server Capability` was last updated, in :rfc:`3339` format
 :serverCapability: The :term:`Server Capability`'s name
 
 .. code-block:: http
@@ -87,13 +87,13 @@ Response Structure
 	{
 		"response": [
 			{
-				"lastUpdated": "2019-10-07 22:05:31+00",
+				"lastUpdated": "2023-08-09T14:25:11.017999Z",
 				"serverHostName": "atlanta-org-1",
 				"serverId": 260,
 				"serverCapability": "ram"
 			},
 			{
-				"lastUpdated": "2019-10-07 22:05:31+00",
+				"lastUpdated": "2023-08-09T14:25:11.017999Z",
 				"serverHostName": "atlanta-org-2",
 				"serverId": 261,
 				"serverCapability": "disk"
@@ -136,7 +136,7 @@ Request Structure
 Response Structure
 ------------------
 :serverId:         The integral, unique identifier of the newly associated server
-:lastUpdated:      The date and time at which this association between the server and the :term:`Server Capability` was last updated, in :ref:`non-rfc-datetime`
+:lastUpdated:      The date and time at which this association between the server and the :term:`Server Capability` was last updated, in :rfc:`3339` format
 :serverCapability: The :term:`Server Capability`'s name
 
 .. code-block:: http
@@ -162,7 +162,7 @@ Response Structure
 			}
 		],
 		"response": {
-			"lastUpdated": "2019-10-07 22:15:11+00",
+			"lastUpdated": "2023-08-09T14:25:11.017999Z",
 			"serverId": 1,
 			"serverCapability": "disk"
 		}
diff --git a/lib/go-tc/server_server_capability.go b/lib/go-tc/server_server_capability.go
index 88bb755b89..42a4d7b25b 100644
--- a/lib/go-tc/server_server_capability.go
+++ b/lib/go-tc/server_server_capability.go
@@ -19,6 +19,20 @@ package tc
  * under the License.
  */
 
+import "time"
+
+// ServerServerCapabilityV5 is a ServerServerCapability as it appears in version 5 of the
+// Traffic Ops API - it always points to the highest minor version in APIv5.
+type ServerServerCapabilityV5 = ServerServerCapabilityV50
+
+// ServerServerCapabilityV50 represents an association between a server capability and a server.
+type ServerServerCapabilityV50 struct {
+	LastUpdated      *time.Time `json:"lastUpdated" db:"last_updated"`
+	Server           *string    `json:"serverHostName,omitempty" db:"host_name"`
+	ServerID         *int       `json:"serverId" db:"server"`
+	ServerCapability *string    `json:"serverCapability" db:"server_capability"`
+}
+
 // ServerServerCapability represents an association between a server capability and a server.
 type ServerServerCapability struct {
 	LastUpdated      *TimeNoMod `json:"lastUpdated" db:"last_updated"`
@@ -35,6 +49,18 @@ type MultipleServersCapabilities struct {
 	PageType           string   `json:"pageType"`
 }
 
+// ServerServerCapabilitiesResponseV5 is the type of a response from the
+// /api/5.x/server_server_capabilities Traffic Ops endpoint.
+// It always points to the type for the latest minor version of APIv5.
+type ServerServerCapabilitiesResponseV5 = ServerServerCapabilitiesResponseV50
+
+// ServerServerCapabilitiesResponseV50 is the type of a response from Traffic
+// Ops to a request made to its /api/5.0/server_server_capabilities.
+type ServerServerCapabilitiesResponseV50 struct {
+	Response []ServerServerCapabilityV5 `json:"response"`
+	Alerts
+}
+
 // ServerServerCapabilitiesResponse is the type of a response from Traffic
 // Ops to a request made to its /server_server_capabilities.
 type ServerServerCapabilitiesResponse struct {
diff --git a/traffic_ops/testing/api/v5/server_server_capabilities_test.go b/traffic_ops/testing/api/v5/server_server_capabilities_test.go
index dcab7cf026..f112d34b6a 100644
--- a/traffic_ops/testing/api/v5/server_server_capabilities_test.go
+++ b/traffic_ops/testing/api/v5/server_server_capabilities_test.go
@@ -38,7 +38,7 @@ func TestServerServerCapabilities(t *testing.T) {
 		currentTime := time.Now().UTC().Add(-15 * time.Second)
 		tomorrow := currentTime.AddDate(0, 0, 1).Format(time.RFC1123)
 
-		methodTests := utils.TestCase[client.Session, client.RequestOptions, tc.ServerServerCapability]{
+		methodTests := utils.TestCase[client.Session, client.RequestOptions, tc.ServerServerCapabilityV5]{
 			"GET": {
 				"NOT MODIFIED when NO CHANGES made": {
 					ClientSession: TOSession,
@@ -102,7 +102,7 @@ func TestServerServerCapabilities(t *testing.T) {
 			"POST": {
 				"BAD REQUEST when ALREADY EXISTS": {
 					ClientSession: TOSession,
-					RequestBody: tc.ServerServerCapability{
+					RequestBody: tc.ServerServerCapabilityV5{
 						ServerID:         util.Ptr(GetServerID(t, "dtrc-mid-01")()),
 						ServerCapability: util.Ptr("disk"),
 					},
@@ -110,21 +110,21 @@ func TestServerServerCapabilities(t *testing.T) {
 				},
 				"BAD REQUEST when MISSING SERVER ID": {
 					ClientSession: TOSession,
-					RequestBody: tc.ServerServerCapability{
+					RequestBody: tc.ServerServerCapabilityV5{
 						Server: util.Ptr("disk"),
 					},
 					Expectations: utils.CkRequest(utils.HasError(), utils.HasStatus(http.StatusBadRequest)),
 				},
 				"BAD REQUEST when MISSING SERVER CAPABILITY": {
 					ClientSession: TOSession,
-					RequestBody: tc.ServerServerCapability{
+					RequestBody: tc.ServerServerCapabilityV5{
 						ServerID: util.Ptr(GetServerID(t, "dtrc-mid-01")()),
 					},
 					Expectations: utils.CkRequest(utils.HasError(), utils.HasStatus(http.StatusBadRequest)),
 				},
 				"NOT FOUND when SERVER CAPABILITY DOESNT EXIST": {
 					ClientSession: TOSession,
-					RequestBody: tc.ServerServerCapability{
+					RequestBody: tc.ServerServerCapabilityV5{
 						ServerID:         util.Ptr(GetServerID(t, "dtrc-mid-01")()),
 						ServerCapability: util.Ptr("bogus"),
 					},
@@ -132,7 +132,7 @@ func TestServerServerCapabilities(t *testing.T) {
 				},
 				"NOT FOUND when SERVER DOESNT EXIST": {
 					ClientSession: TOSession,
-					RequestBody: tc.ServerServerCapability{
+					RequestBody: tc.ServerServerCapabilityV5{
 						ServerID:         util.Ptr(99999999),
 						ServerCapability: util.Ptr("bogus"),
 					},
@@ -140,7 +140,7 @@ func TestServerServerCapabilities(t *testing.T) {
 				},
 				"BAD REQUEST when SERVER TYPE NOT EDGE or MID": {
 					ClientSession: TOSession,
-					RequestBody: tc.ServerServerCapability{
+					RequestBody: tc.ServerServerCapabilityV5{
 						ServerID:         util.Ptr(GetServerID(t, "trafficvault")()),
 						ServerCapability: util.Ptr("bogus"),
 					},
@@ -225,7 +225,7 @@ func TestServerServerCapabilities(t *testing.T) {
 func validateServerServerCapabilitiesFields(expectedResp map[string]interface{}) utils.CkReqFunc {
 	return func(t *testing.T, _ toclientlib.ReqInf, resp interface{}, _ tc.Alerts, _ error) {
 		assert.RequireNotNil(t, resp, "Expected Server Server Capabilities response to not be nil.")
-		serverServerCapabilityResponse := resp.([]tc.ServerServerCapability)
+		serverServerCapabilityResponse := resp.([]tc.ServerServerCapabilityV5)
 		for field, expected := range expectedResp {
 			for _, serverServerCapability := range serverServerCapabilityResponse {
 				switch field {
@@ -261,7 +261,7 @@ func validateServerServerCapabilitiesSort() utils.CkReqFunc {
 	return func(t *testing.T, _ toclientlib.ReqInf, resp interface{}, alerts tc.Alerts, _ error) {
 		assert.RequireNotNil(t, resp, "Expected Server Server Capabilities response to not be nil.")
 		var serverNames []string
-		serverServerCapabilityResponse := resp.([]tc.ServerServerCapability)
+		serverServerCapabilityResponse := resp.([]tc.ServerServerCapabilityV5)
 		for _, serverServerCapability := range serverServerCapabilityResponse {
 			assert.RequireNotNil(t, serverServerCapability.Server, "Expected Server to not be nil.")
 			serverNames = append(serverNames, *serverServerCapability.Server)
@@ -272,7 +272,7 @@ func validateServerServerCapabilitiesSort() utils.CkReqFunc {
 
 func validateServerServerCapabilitiesPagination(paginationParam string) utils.CkReqFunc {
 	return func(t *testing.T, _ toclientlib.ReqInf, resp interface{}, _ tc.Alerts, _ error) {
-		paginationResp := resp.([]tc.ServerServerCapability)
+		paginationResp := resp.([]tc.ServerServerCapabilityV5)
 
 		opts := client.NewRequestOptions()
 		opts.QueryParameters.Set("orderby", "serverId")
diff --git a/traffic_ops/testing/api/v5/traffic_control_test.go b/traffic_ops/testing/api/v5/traffic_control_test.go
index 4154bb7f50..7d9547fa38 100644
--- a/traffic_ops/testing/api/v5/traffic_control_test.go
+++ b/traffic_ops/testing/api/v5/traffic_control_test.go
@@ -46,7 +46,7 @@ type TrafficControl struct {
 	Regions                                           []tc.RegionV5                           `json:"regions"`
 	Roles                                             []tc.RoleV4                             `json:"roles"`
 	Servers                                           []tc.ServerV5                           `json:"servers"`
-	ServerServerCapabilities                          []tc.ServerServerCapability             `json:"serverServerCapabilities"`
+	ServerServerCapabilities                          []tc.ServerServerCapabilityV5           `json:"serverServerCapabilities"`
 	ServerCapabilities                                []tc.ServerCapabilityV5                 `json:"serverCapabilities"`
 	ServiceCategories                                 []tc.ServiceCategoryV5                  `json:"serviceCategories"`
 	Statuses                                          []tc.StatusV5                           `json:"statuses"`
diff --git a/traffic_ops/traffic_ops_golang/routing/routes.go b/traffic_ops/traffic_ops_golang/routing/routes.go
index 0355a22164..eec686a680 100644
--- a/traffic_ops/traffic_ops_golang/routing/routes.go
+++ b/traffic_ops/traffic_ops_golang/routing/routes.go
@@ -331,9 +331,9 @@ func Routes(d ServerData) ([]Route, http.Handler, error) {
 		{Version: api.Version{Major: 5, Minor: 0}, Method: http.MethodDelete, Path: `multiple_servers_capabilities/?$`, Handler: server.DeleteMultipleServersCapabilities, RequiredPrivLevel: auth.PrivLevelOperations, RequiredPermissions: []string{"SERVER:READ", "SERVER:DELETE", "SERVER-CAPABILITY:READ", "SERVER-CAPABILITY:DELETE"}, Authenticated: Authenticated, Middlewares: nil, ID: 407924192781},
 
 		//Server Server Capabilities: CRUD
-		{Version: api.Version{Major: 5, Minor: 0}, Method: http.MethodGet, Path: `server_server_capabilities/?$`, Handler: api.ReadHandler(&server.TOServerServerCapability{}), RequiredPrivLevel: auth.PrivLevelReadOnly, RequiredPermissions: []string{"SERVER:READ", "SERVER-CAPABILITY:READ"}, Authenticated: Authenticated, Middlewares: nil, ID: 480023188931},
-		{Version: api.Version{Major: 5, Minor: 0}, Method: http.MethodPost, Path: `server_server_capabilities/?$`, Handler: api.CreateHandler(&server.TOServerServerCapability{}), RequiredPrivLevel: auth.PrivLevelOperations, RequiredPermissions: []string{"SERVER:UPDATE", "SERVER:READ", "SERVER-CAPABILITY:READ"}, Authenticated: Authenticated, Middlewares: nil, ID: 429316683431},
-		{Version: api.Version{Major: 5, Minor: 0}, Method: http.MethodDelete, Path: `server_server_capabilities/?$`, Handler: api.DeleteHandler(&server.TOServerServerCapability{}), RequiredPrivLevel: auth.PrivLevelOperations, RequiredPermissions: []string{"SERVER:UPDATE", "SERVER:READ", "SERVER-CAPABILITY:READ"}, Authenticated: Authenticated, Middlewares: nil, ID: 405871405831},
+		{Version: api.Version{Major: 5, Minor: 0}, Method: http.MethodGet, Path: `server_server_capabilities/?$`, Handler: api.ReadHandler(&server.TOServerServerCapabilityV5{}), RequiredPrivLevel: auth.PrivLevelReadOnly, RequiredPermissions: []string{"SERVER:READ", "SERVER-CAPABILITY:READ"}, Authenticated: Authenticated, Middlewares: nil, ID: 480023188931},
+		{Version: api.Version{Major: 5, Minor: 0}, Method: http.MethodPost, Path: `server_server_capabilities/?$`, Handler: api.CreateHandler(&server.TOServerServerCapabilityV5{}), RequiredPrivLevel: auth.PrivLevelOperations, RequiredPermissions: []string{"SERVER:UPDATE", "SERVER:READ", "SERVER-CAPABILITY:READ"}, Authenticated: Authenticated, Middlewares: nil, ID: 429316683431},
+		{Version: api.Version{Major: 5, Minor: 0}, Method: http.MethodDelete, Path: `server_server_capabilities/?$`, Handler: api.DeleteHandler(&server.TOServerServerCapabilityV5{}), RequiredPrivLevel: auth.PrivLevelOperations, RequiredPermissions: []string{"SERVER:UPDATE", "SERVER:READ", "SERVER-CAPABILITY:READ"}, Authenticated: Authenticated, Middlewares: nil, ID: 405871405831},
 
 		//Status: CRUD
 		{Version: api.Version{Major: 5, Minor: 0}, Method: http.MethodGet, Path: `statuses/?$`, Handler: api.ReadHandler(&status.TOStatusV5{}), RequiredPrivLevel: auth.PrivLevelReadOnly, RequiredPermissions: []string{"STATUS:READ"}, Authenticated: Authenticated, Middlewares: nil, ID: 424490565631},
diff --git a/traffic_ops/traffic_ops_golang/server/servers_server_capability.go b/traffic_ops/traffic_ops_golang/server/servers_server_capability.go
index b1a5ee56ab..5c8d0c507a 100644
--- a/traffic_ops/traffic_ops_golang/server/servers_server_capability.go
+++ b/traffic_ops/traffic_ops_golang/server/servers_server_capability.go
@@ -48,6 +48,288 @@ const (
 	ServerHostNameQueryParam   = "serverHostName"
 )
 
+type TOServerServerCapabilityV5 struct {
+	api.APIInfoImpl `json:"-"`
+	tc.ServerServerCapabilityV5
+}
+
+func (ssc *TOServerServerCapabilityV5) SetLastUpdated(t tc.TimeNoMod) { ssc.LastUpdated = &t.Time }
+func (ssc *TOServerServerCapabilityV5) NewReadObj() interface{} {
+	return &tc.ServerServerCapabilityV5{}
+}
+func (ssc *TOServerServerCapabilityV5) SelectQuery() string { return scSelectQuery() }
+func (ssc *TOServerServerCapabilityV5) ParamColumns() map[string]dbhelpers.WhereColumnInfo {
+	return map[string]dbhelpers.WhereColumnInfo{
+		ServerCapabilityQueryParam: dbhelpers.WhereColumnInfo{Column: "sc.server_capability"},
+		ServerQueryParam:           dbhelpers.WhereColumnInfo{Column: "s.id", Checker: api.IsInt},
+		ServerHostNameQueryParam:   dbhelpers.WhereColumnInfo{Column: "s.host_name"},
+	}
+
+}
+func (ssc *TOServerServerCapabilityV5) DeleteQuery() string { return scDeleteQuery() }
+func (ssc TOServerServerCapabilityV5) GetKeyFieldsInfo() []api.KeyFieldInfo {
+	return []api.KeyFieldInfo{
+		{Field: ServerQueryParam, Func: api.GetIntKey},
+		{Field: ServerCapabilityQueryParam, Func: api.GetStringKey},
+	}
+}
+
+// Need to satisfy Identifier interface but is a no-op as path does not have Update
+func (ssc TOServerServerCapabilityV5) GetKeys() (map[string]interface{}, bool) {
+	if ssc.ServerID == nil {
+		return map[string]interface{}{ServerQueryParam: 0}, false
+	}
+	if ssc.ServerCapability == nil {
+		return map[string]interface{}{ServerCapabilityQueryParam: 0}, false
+	}
+	return map[string]interface{}{
+		ServerQueryParam:           *ssc.ServerID,
+		ServerCapabilityQueryParam: *ssc.ServerCapability,
+	}, true
+}
+
+func (ssc *TOServerServerCapabilityV5) SetKeys(keys map[string]interface{}) {
+	sID, _ := keys[ServerQueryParam].(int)
+	ssc.ServerID = &sID
+
+	sc, _ := keys[ServerCapabilityQueryParam].(string)
+	ssc.ServerCapability = &sc
+}
+
+func (ssc *TOServerServerCapabilityV5) GetAuditName() string {
+	if ssc.ServerCapability != nil {
+		return *ssc.ServerCapability
+	}
+	return "unknown"
+}
+
+func (ssc *TOServerServerCapabilityV5) GetType() string {
+	return "server server_capability"
+}
+
+// Validate fulfills the api.Validator interface.
+func (ssc TOServerServerCapabilityV5) Validate() (error, error) {
+	errs := validation.Errors{
+		ServerQueryParam:           validation.Validate(ssc.ServerID, validation.Required),
+		ServerCapabilityQueryParam: validation.Validate(ssc.ServerCapability, validation.Required),
+	}
+
+	return util.JoinErrs(tovalidate.ToErrors(errs)), nil
+}
+
+func (ssc *TOServerServerCapabilityV5) Read(h http.Header, useIMS bool) ([]interface{}, error, error, int, *time.Time) {
+	api.DefaultSort(ssc.APIInfo(), "serverHostName")
+	return api.GenericRead(h, ssc, useIMS)
+}
+func (v *TOServerServerCapabilityV5) SelectMaxLastUpdatedQuery(where, orderBy, pagination, tableName string) string {
+	return `SELECT max(t) from (
+		SELECT max(sc.last_updated) as t from server_server_capability sc
+JOIN server s ON sc.server = s.id ` + where + orderBy + pagination +
+		` UNION ALL
+	select max(last_updated) as t from last_deleted l where l.table_name='server_server_capability') as res`
+}
+
+func (ssc *TOServerServerCapabilityV5) Delete() (error, error, int) {
+	tenantIDs, err := tenant.GetUserTenantIDListTx(ssc.APIInfo().Tx.Tx, ssc.APIInfo().User.TenantID)
+	if err != nil {
+		return nil, fmt.Errorf("deleting servers_server_capability: %v", err), http.StatusInternalServerError
+	}
+	accessibleTenants := make(map[int]struct{}, len(tenantIDs))
+	for _, id := range tenantIDs {
+		accessibleTenants[id] = struct{}{}
+	}
+	userErr, sysErr, status := checkTopologyBasedDSRequiredCapabilitiesV5(ssc, accessibleTenants)
+	if userErr != nil || sysErr != nil {
+		return userErr, sysErr, status
+	}
+
+	userErr, sysErr, status = checkDSRequiredCapabilitiesV5(ssc, accessibleTenants)
+	if userErr != nil || sysErr != nil {
+		return userErr, sysErr, status
+	}
+
+	if ssc.ServerID != nil {
+		cdnName, err := dbhelpers.GetCDNNameFromServerID(ssc.APIInfo().Tx.Tx, int64(*ssc.ServerID))
+		if err != nil {
+			return nil, err, http.StatusInternalServerError
+		}
+		userErr, sysErr, errCode := dbhelpers.CheckIfCurrentUserCanModifyCDN(ssc.APIInfo().Tx.Tx, string(cdnName), ssc.APIInfo().User.UserName)
+		if userErr != nil || sysErr != nil {
+			return userErr, sysErr, errCode
+		}
+	}
+	return api.GenericDelete(ssc)
+}
+
+func checkTopologyBasedDSRequiredCapabilitiesV5(ssc *TOServerServerCapabilityV5, accessibleTenants map[int]struct{}) (error, error, int) {
+	dsRows, err := ssc.APIInfo().Tx.Tx.Query(getTopologyBasedDSesReqCapQuery(), ssc.ServerID, ssc.ServerCapability)
+	if err != nil {
+		return nil, fmt.Errorf("querying topology-based DSes with the required capability %s: %v", *ssc.ServerCapability, err), http.StatusInternalServerError
+	}
+	defer log.Close(dsRows, "closing dsRows in checkTopologyBasedDSRequiredCapabilitiesV5")
+
+	xmlidToTopology := make(map[string]string)
+	xmlidToTenantID := make(map[string]int)
+	xmlidToReqCaps := make(map[string][]string)
+	for dsRows.Next() {
+		xmlID := ""
+		topology := ""
+		tenantID := 0
+		reqCaps := []string{}
+		if err := dsRows.Scan(&xmlID, &topology, &tenantID, pq.Array(&reqCaps)); err != nil {
+			return nil, fmt.Errorf("scanning dsRows in checkTopologyBasedDSRequiredCapabilitiesV5: %v", err), http.StatusInternalServerError
+		}
+		xmlidToTenantID[xmlID] = tenantID
+		xmlidToTopology[xmlID] = topology
+		xmlidToReqCaps[xmlID] = reqCaps
+	}
+	if len(xmlidToTopology) == 0 {
+		return nil, nil, http.StatusOK
+	}
+
+	serverRows, err := ssc.APIInfo().Tx.Tx.Query(getServerCapabilitiesOfCachegoupQuery(), ssc.ServerID, ssc.ServerCapability)
+	if err != nil {
+		return nil, fmt.Errorf("querying server capabilitites of server %d's cachegroup: %v", *ssc.ServerID, err), http.StatusInternalServerError
+	}
+	defer log.Close(serverRows, "closing serverRows in checkTopologyBasedDSRequiredCapabilitiesV5")
+
+	serverIDToCapabilities := make(map[int]map[string]struct{})
+	for serverRows.Next() {
+		serverID := 0
+		capabilities := []string{}
+		if err := serverRows.Scan(&serverID, pq.Array(&capabilities)); err != nil {
+			return nil, fmt.Errorf("scanning serverRows in checkTopologyBasedDSRequiredCapabilitiesV5: %v", err), http.StatusInternalServerError
+		}
+		serverIDToCapabilities[serverID] = make(map[string]struct{})
+		for _, c := range capabilities {
+			serverIDToCapabilities[serverID][c] = struct{}{}
+		}
+	}
+
+	unsatisfiedDSes := []string{}
+	for ds, dsReqCaps := range xmlidToReqCaps {
+		dsIsSatisfied := false
+		for _, serverCaps := range serverIDToCapabilities {
+			serverHasCapabilities := true
+			for _, dsReqCap := range dsReqCaps {
+				if _, ok := serverCaps[dsReqCap]; !ok {
+					serverHasCapabilities = false
+					break
+				}
+			}
+			if serverHasCapabilities {
+				dsIsSatisfied = true
+				break
+			}
+		}
+		if !dsIsSatisfied {
+			unsatisfiedDSes = append(unsatisfiedDSes, ds)
+		}
+	}
+	if len(unsatisfiedDSes) == 0 {
+		return nil, nil, http.StatusOK
+	}
+
+	dsStrings := make([]string, 0, len(unsatisfiedDSes))
+	for _, ds := range unsatisfiedDSes {
+		if _, ok := accessibleTenants[xmlidToTenantID[ds]]; ok {
+			dsStrings = append(dsStrings, "(xml_id = "+ds+", topology = "+xmlidToTopology[ds]+")")
+		}
+	}
+	return fmt.Errorf("this capability is required by delivery services, but there are no other servers in this server's cachegroup to satisfy them %s", strings.Join(dsStrings, ", ")), nil, http.StatusBadRequest
+}
+
+func checkDSRequiredCapabilitiesV5(ssc *TOServerServerCapabilityV5, accessibleTenants map[int]struct{}) (error, error, int) {
+	// Ensure that the user is not removing a server capability from the server
+	// that is required by the delivery services the server is assigned to (if applicable)
+	dsIDs := []int64{}
+	if err := ssc.APIInfo().Tx.Tx.QueryRow(checkDSReqCapQuery(), ssc.ServerID, ssc.ServerCapability).Scan(pq.Array(&dsIDs)); err != nil {
+		return nil, fmt.Errorf("checking removing server server capability would still suffice delivery service requried capabilites: %v", err), http.StatusInternalServerError
+	}
+
+	if len(dsIDs) > 0 {
+		return ssc.buildDSReqCapError(dsIDs, accessibleTenants)
+	}
+	return nil, nil, http.StatusOK
+}
+
+func (ssc *TOServerServerCapabilityV5) buildDSReqCapError(dsIDs []int64, accessibleTenants map[int]struct{}) (error, error, int) {
+
+	dsTenantIDs, err := getDSTenantIDsByIDs(ssc.APIInfo().Tx, dsIDs)
+	if err != nil {
+		return nil, err, http.StatusInternalServerError
+	}
+
+	authDSIDs := []string{}
+
+	for _, dsTenantID := range dsTenantIDs {
+		if _, ok := accessibleTenants[dsTenantID.TenantID]; ok {
+			if ok {
+				authDSIDs = append(authDSIDs, strconv.Itoa(dsTenantID.ID))
+			}
+			continue
+		}
+	}
+
+	dsStr := "delivery services"
+	if len(authDSIDs) > 0 {
+		dsStr = fmt.Sprintf("the delivery services %v", strings.Join(authDSIDs, ","))
+	}
+	return fmt.Errorf("cannot remove the capability %v from the server %v as the server is assigned to %v that require it", *ssc.ServerCapability, *ssc.ServerID, dsStr), nil, http.StatusBadRequest
+}
+
+func (ssc *TOServerServerCapabilityV5) Create() (error, error, int) {
+	tx := ssc.APIInfo().Tx
+
+	// Check existence prior to checking type
+	_, exists, err := dbhelpers.GetServerNameFromID(tx.Tx, int64(*ssc.ServerID))
+	if err != nil {
+		return nil, err, http.StatusInternalServerError
+	}
+	if !exists {
+		return fmt.Errorf("server %v does not exist", *ssc.ServerID), nil, http.StatusNotFound
+	}
+
+	// Ensure type is correct
+	var sidList []int64
+	sidList = append(sidList, int64(*ssc.ServerID))
+	errCode, userErr, sysErr := checkServerType(tx.Tx, sidList)
+	if userErr != nil || sysErr != nil {
+		return userErr, sysErr, errCode
+
+	}
+
+	cdnName, err := dbhelpers.GetCDNNameFromServerID(tx.Tx, int64(*ssc.ServerID))
+	if err != nil {
+		return nil, err, http.StatusInternalServerError
+	}
+	userErr, sysErr, errCode = dbhelpers.CheckIfCurrentUserCanModifyCDN(tx.Tx, string(cdnName), ssc.APIInfo().User.UserName)
+	if userErr != nil || sysErr != nil {
+		return userErr, sysErr, errCode
+	}
+
+	resultRows, err := tx.NamedQuery(scInsertQuery(), ssc)
+	if err != nil {
+		return api.ParseDBError(err)
+	}
+	defer resultRows.Close()
+
+	rowsAffected := 0
+	for resultRows.Next() {
+		rowsAffected++
+		if err := resultRows.StructScan(&ssc); err != nil {
+			return nil, errors.New(ssc.GetType() + " create scanning: " + err.Error()), http.StatusInternalServerError
+		}
+	}
+	if rowsAffected == 0 {
+		return nil, errors.New(ssc.GetType() + " create: no " + ssc.GetType() + " was inserted, no rows was returned"), http.StatusInternalServerError
+	} else if rowsAffected > 1 {
+		return nil, errors.New("too many rows returned from " + ssc.GetType() + " insert"), http.StatusInternalServerError
+	}
+
+	return nil, nil, http.StatusOK
+}
+
 type (
 	TOServerServerCapability struct {
 		api.APIInfoImpl `json:"-"`
diff --git a/traffic_ops/traffic_ops_golang/server/servers_server_capability_test.go b/traffic_ops/traffic_ops_golang/server/servers_server_capability_test.go
index a6ee9e46b2..89c7dc34ae 100644
--- a/traffic_ops/traffic_ops_golang/server/servers_server_capability_test.go
+++ b/traffic_ops/traffic_ops_golang/server/servers_server_capability_test.go
@@ -35,6 +35,185 @@ import (
 	"gopkg.in/DATA-DOG/go-sqlmock.v1"
 )
 
+func getTestSSCsV5() []tc.ServerServerCapabilityV5 {
+	sscs := []tc.ServerServerCapabilityV5{}
+	testSSC := tc.ServerServerCapabilityV5{
+		LastUpdated:      util.Ptr(time.Now()),
+		Server:           util.StrPtr("test"),
+		ServerID:         util.IntPtr(1),
+		ServerCapability: util.StrPtr("test"),
+	}
+	sscs = append(sscs, testSSC)
+
+	testSSC1 := testSSC
+	testSSC1.ServerCapability = util.Ptr("blah")
+	sscs = append(sscs, testSSC1)
+
+	return sscs
+}
+
+func TestReadSCsV5(t *testing.T) {
+	mockDB, mock, err := sqlmock.New()
+	if err != nil {
+		t.Fatalf("an error '%v' was not expected when opening a stub database connection", err)
+	}
+	defer mockDB.Close()
+
+	db := sqlx.NewDb(mockDB, "sqlmock")
+	defer db.Close()
+
+	testSCs := getTestSSCsV5()
+	rows := sqlmock.NewRows([]string{"server_capability", "server", "last_updated"})
+
+	for _, ts := range testSCs {
+		rows = rows.AddRow(
+			ts.ServerCapability,
+			ts.ServerID,
+			ts.LastUpdated)
+	}
+	mock.ExpectBegin()
+	mock.ExpectQuery("SELECT").WillReturnRows(rows)
+	mock.ExpectCommit()
+
+	reqInfo := api.APIInfo{Tx: db.MustBegin(), Params: map[string]string{"serverId": "1"}}
+	obj := TOServerServerCapabilityV5{
+		api.APIInfoImpl{ReqInfo: &reqInfo},
+		tc.ServerServerCapabilityV5{},
+	}
+	sscs, userErr, sysErr, _, _ := obj.Read(nil, false)
+	if userErr != nil || sysErr != nil {
+		t.Errorf("Read expected: no errors, actual: %v %v", userErr, sysErr)
+	}
+
+	if len(sscs) != 2 {
+		t.Errorf("ServerServerCapabilityV5.Read expected: len(scs) == 1, actual: %v", len(sscs))
+	}
+}
+
+func TestInterfacesV5(t *testing.T) {
+	var i interface{}
+	i = &TOServerServerCapabilityV5{}
+
+	if _, ok := i.(api.Creator); !ok {
+		t.Errorf("ServerServerCapabilityV5 must be Creator")
+	}
+	if _, ok := i.(api.Reader); !ok {
+		t.Errorf("ServerServerCapabilityV5 must be Reader")
+	}
+	if _, ok := i.(api.Deleter); !ok {
+		t.Errorf("ServerServerCapabilityV5 must be Deleter")
+	}
+	if _, ok := i.(api.Identifier); !ok {
+		t.Errorf("ServerServerCapabilityV5 must be Identifier")
+	}
+}
+
+func TestFuncsV5(t *testing.T) {
+	if strings.Index(scSelectQuery(), "SELECT") != 0 {
+		t.Errorf("expected selectQuery to start with SELECT")
+	}
+	if strings.Index(scInsertQuery(), "INSERT") != 0 {
+		t.Errorf("expected insertQuery to start with INSERT")
+	}
+	if strings.Index(scDeleteQuery(), "DELETE") != 0 {
+		t.Errorf("expected deleteQuery to start with DELETE")
+	}
+}
+
+func TestValidateV5(t *testing.T) {
+	testSSC := tc.ServerServerCapabilityV5{
+		LastUpdated:      util.Ptr(time.Now()),
+		Server:           util.StrPtr("test1"),
+		ServerID:         util.IntPtr(1),
+		ServerCapability: util.StrPtr("abc"),
+	}
+	testTOSSC := TOServerServerCapabilityV5{
+		ServerServerCapabilityV5: testSSC,
+	}
+
+	err, _ := testTOSSC.Validate()
+	errs := test.SortErrors(test.SplitErrors(err))
+
+	if len(errs) > 0 {
+		t.Errorf(`expected no errors,  got %v`, errs)
+	}
+}
+
+func TestCheckExistingServerV5(t *testing.T) {
+	mockDB, mock, err := sqlmock.New()
+	if err != nil {
+		t.Fatalf("an error '%s' was not expected when opening a stub database connection", err)
+	}
+	defer mockDB.Close()
+
+	db := sqlx.NewDb(mockDB, "sqlmock")
+	defer db.Close()
+
+	mock.ExpectBegin()
+	rows := sqlmock.NewRows([]string{"host_name"})
+	rows.AddRow("test")
+	mock.ExpectQuery("SELECT host_name").WithArgs(1).WillReturnRows(rows)
+
+	rows1 := sqlmock.NewRows([]string{"name"})
+	rows1.AddRow("ALL")
+	mock.ExpectQuery("SELECT name").WithArgs(1).WillReturnRows(rows1)
+
+	rows2 := sqlmock.NewRows([]string{"username", "soft", "shared_usernames"})
+	rows2.AddRow("user1", false, []byte("{}"))
+	mock.ExpectQuery("SELECT c.username, c.soft").WithArgs("ALL").WillReturnRows(rows2)
+	mock.ExpectCommit()
+
+	testSCCs := getTestSSCsV5()
+	var sids []int64
+	sids = append(sids, int64(*testSCCs[0].ServerID))
+	code, usrErr, sysErr := checkExistingServer(db.MustBegin().Tx, sids, "user1")
+	if usrErr != nil {
+		t.Errorf("server not found, error:%v", usrErr)
+	}
+	if sysErr != nil {
+		t.Errorf("unable to check if server exists, error:%v", sysErr)
+	}
+	if code != http.StatusOK {
+		t.Errorf("existing server check failed, expected:%d, got:%d", http.StatusOK, code)
+	}
+}
+
+func TestCheckServerTypeV5(t *testing.T) {
+	mockDB, mock, err := sqlmock.New()
+	if err != nil {
+		t.Fatalf("an error '%s' was not expected when opening a stub database connection", err)
+	}
+	defer mockDB.Close()
+
+	db := sqlx.NewDb(mockDB, "sqlmock")
+	defer db.Close()
+
+	testSCCs := getTestSSCsV5()
+	testSCCs[1].ServerID = util.Ptr(2)
+	testSCCs[1].Server = util.Ptr("foo")
+
+	mock.ExpectBegin()
+	rows := sqlmock.NewRows([]string{"array_agg"})
+	var sids []int64
+	for i, _ := range testSCCs {
+		sids = append(sids, int64(*testSCCs[i].ServerID))
+	}
+	rows.AddRow([]byte("{1,2}"))
+	mock.ExpectQuery("SELECT array_agg").WithArgs(pq.Array(sids)).WillReturnRows(rows)
+	mock.ExpectCommit()
+
+	code, usrErr, sysErr := checkServerType(db.MustBegin().Tx, sids)
+	if usrErr != nil {
+		t.Errorf("mismatch in server type, error:%v", usrErr)
+	}
+	if sysErr != nil {
+		t.Errorf("unable to check if server type exists, error:%v", sysErr)
+	}
+	if code != http.StatusOK {
+		t.Errorf("server type check failed, expected:%d, got:%d", http.StatusOK, code)
+	}
+}
+
 func getTestSSCs() []tc.ServerServerCapability {
 	sscs := []tc.ServerServerCapability{}
 	testSSC := tc.ServerServerCapability{
diff --git a/traffic_ops/v5-client/server_server_capabilities.go b/traffic_ops/v5-client/server_server_capabilities.go
index 15bd1efbc2..d0d125e44d 100644
--- a/traffic_ops/v5-client/server_server_capabilities.go
+++ b/traffic_ops/v5-client/server_server_capabilities.go
@@ -32,7 +32,7 @@ const apiServerServerCapabilities = "/server_server_capabilities"
 const apiMultipleServersCapabilities = "/multiple_servers_capabilities"
 
 // CreateServerServerCapability assigns a Server Capability to a Server.
-func (to *Session) CreateServerServerCapability(ssc tc.ServerServerCapability, opts RequestOptions) (tc.Alerts, toclientlib.ReqInf, error) {
+func (to *Session) CreateServerServerCapability(ssc tc.ServerServerCapabilityV5, opts RequestOptions) (tc.Alerts, toclientlib.ReqInf, error) {
 	var alerts tc.Alerts
 	reqInf, err := to.post(apiServerServerCapabilities, opts, ssc, &alerts)
 	return alerts, reqInf, err
@@ -52,8 +52,8 @@ func (to *Session) DeleteServerServerCapability(serverID int, serverCapability s
 
 // GetServerServerCapabilities retrieves a list of Server Capabilities that are
 // assigned to Servers.
-func (to *Session) GetServerServerCapabilities(opts RequestOptions) (tc.ServerServerCapabilitiesResponse, toclientlib.ReqInf, error) {
-	var resp tc.ServerServerCapabilitiesResponse
+func (to *Session) GetServerServerCapabilities(opts RequestOptions) (tc.ServerServerCapabilitiesResponseV5, toclientlib.ReqInf, error) {
+	var resp tc.ServerServerCapabilitiesResponseV5
 	reqInf, err := to.get(apiServerServerCapabilities, opts, &resp)
 	return resp, reqInf, err
 }