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 <[email protected]>
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
 }


Reply via email to