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 0ca1357  Validate DS Topologies when deleting a server capability 
(#5091)
0ca1357 is described below

commit 0ca1357cef54998e41a86dfa5f58bf458225e8a6
Author: Rawlin Peters <[email protected]>
AuthorDate: Thu Oct 1 14:23:09 2020 -0600

    Validate DS Topologies when deleting a server capability (#5091)
    
    * Validate DS Topologies when deleting a server capability
    
    There must be at least one server in a cachegroup that can satisfy the
    required capabilities of the delivery services the cachegroup is
    assigned to via topologies. Otherwise, the topologies would no longer be
    valid w.r.t. the required capabilities of the delivery services.
    
    * Address review feedback
---
 CHANGELOG.md                                       |   1 +
 .../deliveryservices_required_capabilities_test.go |  26 +-
 .../testing/api/v3/serverservercapability_test.go  | 165 +++++-
 traffic_ops/testing/api/v3/tc-fixtures.json        | 555 +++++++++++++++++++++
 traffic_ops/testing/api/v3/traffic_control_test.go |  73 +--
 traffic_ops/testing/api/v3/withobjs_test.go        |  72 +--
 .../deliveryservices_required_capabilities.go      |   5 +-
 .../server/servers_server_capability.go            | 173 ++++++-
 traffic_ops/traffic_ops_golang/tenant/tenancy.go   |  12 -
 9 files changed, 943 insertions(+), 139 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3662550..e2f72bb 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -19,6 +19,7 @@ The format is based on [Keep a 
Changelog](http://keepachangelog.com/en/1.0.0/).
     - Traffic Ops: Added support for `topology` query parameter for `GET 
/api/3.0/servers` to return all servers whose cachegroups are in a given 
topology.
     - Traffic Ops: Added new topology-based delivery service fields for header 
rewrites: `firstHeaderRewrite`, `innerHeaderRewrite`, `lastHeaderRewrite`
     - Traffic Ops: Added validation to prohibit assigning caches to 
topology-based delivery services
+    - Traffic Ops: Added validation to prohibit removing a capability from a 
server if no other server in the same cachegroup can satisfy the required 
capabilities of the delivery services assigned to it via topologies.
     - Traffic Ops: Consider Topologies parentage when queueing or checking 
server updates
     - ORT: Added Topologies to Config Generation.
     - Traffic Portal: Added the ability to create, read, update and delete 
flexible topologies.
diff --git 
a/traffic_ops/testing/api/v3/deliveryservices_required_capabilities_test.go 
b/traffic_ops/testing/api/v3/deliveryservices_required_capabilities_test.go
index afb9091..d1e2be3 100644
--- a/traffic_ops/testing/api/v3/deliveryservices_required_capabilities_test.go
+++ b/traffic_ops/testing/api/v3/deliveryservices_required_capabilities_test.go
@@ -17,13 +17,13 @@ package v3
 
 import (
        "fmt"
-       "github.com/apache/trafficcontrol/lib/go-rfc"
        "net/http"
        "net/url"
        "strings"
        "testing"
        "time"
 
+       "github.com/apache/trafficcontrol/lib/go-rfc"
        "github.com/apache/trafficcontrol/lib/go-tc"
        "github.com/apache/trafficcontrol/lib/go-util"
 )
@@ -144,7 +144,7 @@ func GetTestDeliveryServicesRequiredCapabilities(t 
*testing.T) {
 
        for _, tc := range testCases {
                t.Run(tc.description, func(t *testing.T) {
-                       capabilities, _, err := 
TOSession.GetDeliveryServicesRequiredCapabilities(tc.capability.DeliveryServiceID,
 tc.capability.XMLID, tc.capability.RequiredCapability)
+                       capabilities, _, err := 
TOSession.GetDeliveryServicesRequiredCapabilitiesWithHdr(tc.capability.DeliveryServiceID,
 tc.capability.XMLID, tc.capability.RequiredCapability, nil)
                        if err != nil {
                                t.Fatalf("%s; got err= %v; expected err= nil", 
tc.description, err)
                        }
@@ -208,6 +208,20 @@ func GetTestDeliveryServicesRequiredCapabilitiesIMS(t 
*testing.T) {
                })
        }
 }
+func CreateTestTopologyBasedDeliveryServicesRequiredCapabilities(t *testing.T) 
{
+       for _, td := range 
testData.TopologyBasedDeliveryServicesRequiredCapabilities {
+
+               c := tc.DeliveryServicesRequiredCapability{
+                       DeliveryServiceID:  helperGetDeliveryServiceID(t, 
td.XMLID),
+                       RequiredCapability: td.RequiredCapability,
+               }
+
+               _, _, err := 
TOSession.CreateDeliveryServicesRequiredCapability(c)
+               if err != nil {
+                       t.Fatalf("cannot create delivery service required 
capability: %v", err)
+               }
+       }
+}
 
 func CreateTestDeliveryServicesRequiredCapabilities(t *testing.T) {
        data := testData.DeliveryServicesRequiredCapabilities
@@ -301,7 +315,7 @@ func InvalidDeliveryServicesRequiredCapabilityAddition(t 
*testing.T) {
        // Tests that a capability cannot be made required if the DS's services 
do not have it assigned
 
        // Get Delivery Capability for a DS
-       capabilities, _, err := 
TOSession.GetDeliveryServicesRequiredCapabilities(nil, util.StrPtr("ds1"), nil)
+       capabilities, _, err := 
TOSession.GetDeliveryServicesRequiredCapabilitiesWithHdr(nil, 
util.StrPtr("ds1"), nil, nil)
        if err != nil {
                t.Fatalf("cannot GET delivery service required capabilities: 
%v", err)
        }
@@ -313,7 +327,7 @@ func InvalidDeliveryServicesRequiredCapabilityAddition(t 
*testing.T) {
        // TODO: DON'T hard-code hostnames!
        params := url.Values{}
        params.Add("hostName", "atlanta-edge-01")
-       resp, _, err := TOSession.GetServers(&params)
+       resp, _, err := TOSession.GetServersWithHdr(&params, nil)
        if err != nil {
                t.Fatalf("cannot GET Server by hostname: %v", err)
        }
@@ -389,7 +403,7 @@ func InvalidDeliveryServicesRequiredCapabilityAddition(t 
*testing.T) {
 
 func DeleteTestDeliveryServicesRequiredCapabilities(t *testing.T) {
        // Get Required Capabilities to delete them
-       capabilities, _, err := 
TOSession.GetDeliveryServicesRequiredCapabilities(nil, nil, nil)
+       capabilities, _, err := 
TOSession.GetDeliveryServicesRequiredCapabilitiesWithHdr(nil, nil, nil, nil)
        if err != nil {
                t.Fatalf(err.Error())
        }
@@ -445,7 +459,7 @@ func helperGetDeliveryServiceID(t *testing.T, xmlID 
*string) *int {
        if xmlID == nil {
                t.Fatal("xml id must not be nil")
        }
-       ds, _, err := TOSession.GetDeliveryServiceByXMLIDNullable(*xmlID)
+       ds, _, err := 
TOSession.GetDeliveryServiceByXMLIDNullableWithHdr(*xmlID, nil)
        if err != nil {
                t.Fatal(err)
        }
diff --git a/traffic_ops/testing/api/v3/serverservercapability_test.go 
b/traffic_ops/testing/api/v3/serverservercapability_test.go
index c419422..66742d8 100644
--- a/traffic_ops/testing/api/v3/serverservercapability_test.go
+++ b/traffic_ops/testing/api/v3/serverservercapability_test.go
@@ -16,12 +16,12 @@ package v3
 */
 
 import (
-       "github.com/apache/trafficcontrol/lib/go-rfc"
        "net/http"
        "net/url"
        "testing"
        "time"
 
+       "github.com/apache/trafficcontrol/lib/go-rfc"
        "github.com/apache/trafficcontrol/lib/go-tc"
        "github.com/apache/trafficcontrol/lib/go-util"
 )
@@ -33,6 +33,12 @@ func TestServerServerCapabilities(t *testing.T) {
        })
 }
 
+func TestServerServerCapabilitiesForTopologies(t *testing.T) {
+       WithObjs(t, []TCObj{CDNs, Types, Tenants, Parameters, Profiles, 
Statuses, Divisions, Regions, PhysLocations, CacheGroups, Servers, Topologies, 
DeliveryServices, ServerCapabilities, ServerServerCapabilitiesForTopologies, 
TopologyBasedDeliveryServiceRequiredCapabilities}, func() {
+               DeleteTestServerServerCapabilitiesForTopologiesValidation(t)
+       })
+}
+
 func GetTestServerServerCapabilitiesIMS(t *testing.T) {
        var header http.Header
        header = make(map[string][]string)
@@ -58,7 +64,7 @@ func CreateTestServerServerCapabilities(t *testing.T) {
                        t.Fatalf("server-server-capability structure had nil 
server")
                }
                params.Set("hostName", *ssc.Server)
-               resp, _, err := TOSession.GetServers(&params)
+               resp, _, err := TOSession.GetServersWithHdr(&params, nil)
                if err != nil {
                        t.Fatalf("cannot GET Server by hostname '%s': %v - %v", 
*ssc.Server, err, resp.Alerts)
                }
@@ -126,7 +132,7 @@ func CreateTestServerServerCapabilities(t *testing.T) {
        // Attempt to assign a server capability to a non MID/EDGE server
        // TODO: DON'T hard-code server hostnames!
        params.Set("hostName", "riak")
-       resp, _, err := TOSession.GetServers(&params)
+       resp, _, err := TOSession.GetServersWithHdr(&params, nil)
        if err != nil {
                t.Fatalf("cannot GET Server by hostname 'riak': %v - %v", err, 
resp.Alerts)
        }
@@ -147,7 +153,7 @@ func CreateTestServerServerCapabilities(t *testing.T) {
 
 func GetTestServerServerCapabilities(t *testing.T) {
        // Get All Server Capabilities
-       sscs, _, err := TOSession.GetServerServerCapabilities(nil, nil, nil)
+       sscs, _, err := TOSession.GetServerServerCapabilitiesWithHdr(nil, nil, 
nil, nil)
        if err != nil {
                t.Fatalf("cannot GET server capabilities assigned to servers: 
%v", err)
        }
@@ -158,41 +164,44 @@ func GetTestServerServerCapabilities(t *testing.T) {
                t.Errorf("expect %v server capabilities assigned to servers 
received %v ", len(testData.ServerServerCapabilities), len(sscs))
        }
 
-       checkResp := func(t *testing.T, sscs []tc.ServerServerCapability) {
-               if sscs == nil {
-                       t.Fatal("returned server capabilities assigned to 
servers was nil\n")
-               }
-               if len(sscs) != 1 {
-                       t.Errorf("expect 1 server capabilities assigned to 
server received %v ", len(sscs))
-               }
-       }
-
        for _, ssc := range sscs {
                // Get assigned Server Capabilities by server id
-               sscs, _, err := 
TOSession.GetServerServerCapabilities(ssc.ServerID, nil, nil)
+               sscs, _, err := 
TOSession.GetServerServerCapabilitiesWithHdr(ssc.ServerID, nil, nil, nil)
                if err != nil {
                        t.Fatalf("cannot GET server capabilities assigned to 
servers by server ID %v: %v", *ssc.ServerID, err)
                }
-               checkResp(t, sscs)
+               for _, s := range sscs {
+                       if *s.ServerID != *ssc.ServerID {
+                               t.Errorf("GET server server capabilities by 
serverID returned non-matching server ID: %d", *s.ServerID)
+                       }
+               }
                // Get assigned Server Capabilities by host name
-               sscs, _, err = TOSession.GetServerServerCapabilities(nil, 
ssc.Server, nil)
+               sscs, _, err = 
TOSession.GetServerServerCapabilitiesWithHdr(nil, ssc.Server, nil, nil)
                if err != nil {
                        t.Fatalf("cannot GET server capabilities assigned to 
servers by server host name %v: %v", *ssc.Server, err)
                }
-               checkResp(t, sscs)
+               for _, s := range sscs {
+                       if *s.Server != *ssc.Server {
+                               t.Errorf("GET server server capabilities by 
serverHostName returned non-matching server hostname: %s", *s.Server)
+                       }
+               }
 
                // Get assigned Server Capabilities by server capability
-               sscs, _, err = TOSession.GetServerServerCapabilities(nil, nil, 
ssc.ServerCapability)
+               sscs, _, err = 
TOSession.GetServerServerCapabilitiesWithHdr(nil, nil, ssc.ServerCapability, 
nil)
                if err != nil {
                        t.Fatalf("cannot GET server capabilities assigned to 
servers by server capability %v: %v", *ssc.ServerCapability, err)
                }
-               checkResp(t, sscs)
+               for _, s := range sscs {
+                       if *s.ServerCapability != *ssc.ServerCapability {
+                               t.Errorf("GET server server capabilities by 
server capability returned non-matching server capability: %s", 
*s.ServerCapability)
+                       }
+               }
        }
 }
 
 func DeleteTestServerServerCapabilities(t *testing.T) {
        // Get Server Capabilities to delete them
-       sscs, _, err := TOSession.GetServerServerCapabilities(nil, nil, nil)
+       sscs, _, err := TOSession.GetServerServerCapabilitiesWithHdr(nil, nil, 
nil, nil)
        if err != nil {
                t.Fatalf("cannot GET server capabilities assigned to servers: 
%v", err)
        }
@@ -200,19 +209,40 @@ func DeleteTestServerServerCapabilities(t *testing.T) {
                t.Fatal("returned server capabilities assigned to servers was 
nil\n")
        }
 
+       dses, _, err := TOSession.GetDeliveryServicesV30WithHdr(nil, nil)
+       if err != nil {
+               t.Fatalf("cannot GET delivery services: %v", err)
+       }
+       dsIDtoDS := make(map[int]tc.DeliveryServiceNullableV30, len(dses))
+       for _, ds := range dses {
+               dsIDtoDS[*ds.ID] = ds
+       }
+
        // Assign servers to DSes that have the capability required
        // Used to make sure we block server server_capability DELETE in that 
case
        dsServers := []tc.DeliveryServiceServer{}
+       assignedServers := make(map[int]bool)
        for _, ssc := range sscs {
 
-               dsReqCapResp, _, err := 
TOSession.GetDeliveryServicesRequiredCapabilities(nil, nil, 
ssc.ServerCapability)
+               dsReqCapResp, _, err := 
TOSession.GetDeliveryServicesRequiredCapabilitiesWithHdr(nil, nil, 
ssc.ServerCapability, nil)
                if err != nil {
                        t.Fatalf("cannot GET delivery service required 
capabilities: %v", err)
                }
                if len(dsReqCapResp) == 0 {
-                       t.Fatalf("at least one delivery service needs the 
capability %v required", *ssc.ServerCapability)
+                       // capability is not required by any delivery service
+                       continue
+               }
+               var dsReqCap tc.DeliveryServicesRequiredCapability
+               for _, dsrc := range dsReqCapResp {
+                       if dsIDtoDS[*dsrc.DeliveryServiceID].Topology == nil {
+                               dsReqCap = dsrc
+                               break
+                       }
+               }
+               if dsReqCap.DeliveryServiceID == nil {
+                       // didn't find a non-topology-based dsReqCap for this 
ssc
+                       continue
                }
-               dsReqCap := dsReqCapResp[0]
 
                // Assign server to ds
                _, _, err = 
TOSession.CreateDeliveryServiceServers(*dsReqCap.DeliveryServiceID, 
[]int{*ssc.ServerID}, false)
@@ -223,13 +253,19 @@ func DeleteTestServerServerCapabilities(t *testing.T) {
                        Server:          ssc.ServerID,
                        DeliveryService: dsReqCap.DeliveryServiceID,
                })
+               assignedServers[*ssc.ServerID] = true
+       }
+       if len(dsServers) == 0 {
+               t.Fatalf("test requires at least one server with a capability 
that is required by at least one delivery service")
        }
 
        // Delete should fail as their delivery services now require the 
capabilities
        for _, ssc := range sscs {
-               _, _, err := 
TOSession.DeleteServerServerCapability(*ssc.ServerID, *ssc.ServerCapability)
-               if err == nil {
-                       t.Fatalf("should have gotten error when using DELETE on 
the server capability %v from server %v as it is required by associated dses", 
*ssc.ServerCapability, *ssc.Server)
+               if assignedServers[*ssc.ServerID] {
+                       _, _, err := 
TOSession.DeleteServerServerCapability(*ssc.ServerID, *ssc.ServerCapability)
+                       if err == nil {
+                               t.Fatalf("should have gotten error when using 
DELETE on the server capability %v from server %v as it is required by 
associated dses", *ssc.ServerCapability, *ssc.Server)
+                       }
                }
        }
 
@@ -250,3 +286,80 @@ func DeleteTestServerServerCapabilities(t *testing.T) {
        }
 
 }
+
+func DeleteTestServerServerCapabilitiesForTopologiesValidation(t *testing.T) {
+       // dtrc-edge-01 and dtrc-edge-02 (capabilities = ram, disk) are 
assigned to
+       // ds-top-req-cap (topology = top-for-ds-req; required capabilities = 
ram, disk) and
+       // ds-top-req-cap2 (topology = top-for-ds-req2; required capabilities = 
ram)
+       var edge1 tc.ServerNullable
+       var edge2 tc.ServerNullable
+
+       servers, _, err := TOSession.GetServersWithHdr(nil, nil)
+       if err != nil {
+               t.Fatalf("cannot GET servers: %v", err)
+       }
+       for _, s := range servers.Response {
+               if *s.HostName == "dtrc-edge-01" {
+                       edge1 = s
+               }
+               if *s.HostName == "dtrc-edge-02" {
+                       edge2 = s
+               }
+       }
+       if edge1.HostName == nil || edge2.HostName == nil {
+               t.Fatalf("expected servers with hostName dtrc-edge-01 and 
dtrc-edge-02")
+       }
+
+       // delete should succeed because dtrc-edge-02 still has the required 
capabilities
+       // for ds-top-req-cap and ds-top-req-cap2 within the cachegroup
+       _, _, err = TOSession.DeleteServerServerCapability(*edge1.ID, "ram")
+       if err != nil {
+               t.Fatalf("when deleting server server capability, expected: nil 
error, actual: %v", err)
+       }
+
+       // delete should fail because dtrc-edge-02 is the last server in the 
cachegroup that
+       // has ds-top-req-cap's required capabilities
+       _, reqInf, err := TOSession.DeleteServerServerCapability(*edge2.ID, 
"ram")
+       if err == nil {
+               t.Fatalf("when deleting server server capability, expected: 
error, actual: nil")
+       }
+       if reqInf.StatusCode != http.StatusBadRequest {
+               t.Errorf("when deleting server server capability, expected 
status code: %d, actual: %d", http.StatusBadRequest, reqInf.StatusCode)
+       }
+
+       // delete should fail because dtrc-edge-02 is the last server in the 
cachegroup that
+       // has ds-top-req-cap's required capabilities
+       _, r, err := TOSession.DeleteServerServerCapability(*edge2.ID, "disk")
+       if err == nil {
+               t.Fatalf("when deleting required server server capability, 
expected: error, actual: nil")
+       }
+       if r.StatusCode != http.StatusBadRequest {
+               t.Errorf("when deleting required server server capability, 
expected status code: %d, actual: %d", http.StatusBadRequest, reqInf.StatusCode)
+       }
+
+       // delete should succeed because dtrc-edge-02 still has the required 
capabilities
+       // for ds-top-req-cap and ds-top-req-cap2 within the cachegroup
+       _, _, err = TOSession.DeleteServerServerCapability(*edge1.ID, "disk")
+       if err != nil {
+               t.Fatalf("when deleting server server capability, expected: nil 
error, actual: %v", err)
+       }
+}
+
+func DeleteTestServerServerCapabilitiesForTopologies(t *testing.T) {
+       // Get Server Capabilities to delete them
+       sscs, _, err := TOSession.GetServerServerCapabilitiesWithHdr(nil, nil, 
nil, nil)
+       if err != nil {
+               t.Fatalf("cannot GET server capabilities assigned to servers: 
%v", err)
+       }
+       if sscs == nil {
+               t.Fatal("returned server capabilities assigned to servers was 
nil\n")
+       }
+
+       for _, ssc := range sscs {
+               _, _, err := 
TOSession.DeleteServerServerCapability(*ssc.ServerID, *ssc.ServerCapability)
+               if err != nil {
+                       t.Errorf("could not DELETE the server capability %v 
from server %v: %v", *ssc.ServerCapability, *ssc.Server, err)
+               }
+       }
+
+}
diff --git a/traffic_ops/testing/api/v3/tc-fixtures.json 
b/traffic_ops/testing/api/v3/tc-fixtures.json
index 5561716..fcf7d5c 100644
--- a/traffic_ops/testing/api/v3/tc-fixtures.json
+++ b/traffic_ops/testing/api/v3/tc-fixtures.json
@@ -201,6 +201,27 @@
             "name": "topology-mid-cg-07",
             "shortName": "tm7",
             "typeName": "MID_LOC"
+        },
+        {
+            "latitude": 0,
+            "longitude": 0,
+            "name": "dtrc1",
+            "shortName": "dtrc1",
+            "typeName": "MID_LOC"
+        },
+        {
+            "latitude": 0,
+            "longitude": 0,
+            "name": "dtrc2",
+            "shortName": "dtrc2",
+            "typeName": "EDGE_LOC"
+        },
+        {
+            "latitude": 0,
+            "longitude": 0,
+            "name": "dtrc3",
+            "shortName": "dtrc3",
+            "typeName": "EDGE_LOC"
         }
     ],
     "cdns": [
@@ -863,6 +884,134 @@
             "checkPath": "",
             "consistentHashQueryParams": [],
             "deepCachingType": "NEVER",
+            "displayName": "ds-top-req-cap",
+            "dnsBypassCname": null,
+            "dnsBypassIp": "",
+            "dnsBypassIp6": "",
+            "dnsBypassTtl": 30,
+            "dscp": 40,
+            "edgeHeaderRewrite": null,
+            "fqPacingRate": 0,
+            "geoLimit": 0,
+            "geoLimitCountries": "",
+            "geoLimitRedirectURL": null,
+            "geoProvider": 0,
+            "globalMaxMbps": 0,
+            "globalMaxTps": 0,
+            "httpBypassFqdn": "",
+            "infoUrl": "TBD",
+            "initialDispersion": 1,
+            "ipv6RoutingEnabled": true,
+            "lastUpdated": "2018-04-06 16:48:51+00",
+            "logsEnabled": false,
+            "longDesc": "",
+            "longDesc1": "",
+            "longDesc2": "",
+            "matchList": [
+                {
+                    "pattern": ".*\\.ds-top-req-cap\\..*",
+                    "setNumber": 0,
+                    "type": "HOST_REGEXP"
+                }
+            ],
+            "maxDnsAnswers": 0,
+            "midHeaderRewrite": null,
+            "missLat": 41.881944,
+            "missLong": -87.627778,
+            "multiSiteOrigin": false,
+            "orgServerFqdn": "http://example.org";,
+            "originShield": null,
+            "profileDescription": null,
+            "profileName": null,
+            "protocol": 0,
+            "qstringIgnore": 0,
+            "rangeRequestHandling": 0,
+            "regexRemap": null,
+            "regionalGeoBlocking": false,
+            "remapText": null,
+            "routingName": "cdn",
+            "signed": false,
+            "signingAlgorithm": null,
+            "sslKeyVersion": 0,
+            "tenant": "tenant1",
+            "tenantName": "tenant1",
+            "topology": "top-for-ds-req",
+            "type": "HTTP",
+            "xmlId": "ds-top-req-cap",
+            "anonymousBlockingEnabled": false
+        },
+        {
+            "active": true,
+            "cdnName": "cdn1",
+            "cacheurl": "",
+            "ccrDnsTtl": 3600,
+            "checkPath": "",
+            "consistentHashQueryParams": [],
+            "deepCachingType": "NEVER",
+            "displayName": "ds-top-req-cap2",
+            "dnsBypassCname": null,
+            "dnsBypassIp": "",
+            "dnsBypassIp6": "",
+            "dnsBypassTtl": 30,
+            "dscp": 40,
+            "edgeHeaderRewrite": null,
+            "fqPacingRate": 0,
+            "geoLimit": 0,
+            "geoLimitCountries": "",
+            "geoLimitRedirectURL": null,
+            "geoProvider": 0,
+            "globalMaxMbps": 0,
+            "globalMaxTps": 0,
+            "httpBypassFqdn": "",
+            "infoUrl": "TBD",
+            "initialDispersion": 1,
+            "ipv6RoutingEnabled": true,
+            "lastUpdated": "2018-04-06 16:48:51+00",
+            "logsEnabled": false,
+            "longDesc": "",
+            "longDesc1": "",
+            "longDesc2": "",
+            "matchList": [
+                {
+                    "pattern": ".*\\.ds-top-req-cap2\\..*",
+                    "setNumber": 0,
+                    "type": "HOST_REGEXP"
+                }
+            ],
+            "maxDnsAnswers": 0,
+            "midHeaderRewrite": null,
+            "missLat": 41.881944,
+            "missLong": -87.627778,
+            "multiSiteOrigin": false,
+            "orgServerFqdn": "http://example.org";,
+            "originShield": null,
+            "profileDescription": null,
+            "profileName": null,
+            "protocol": 0,
+            "qstringIgnore": 0,
+            "rangeRequestHandling": 0,
+            "regexRemap": null,
+            "regionalGeoBlocking": false,
+            "remapText": null,
+            "routingName": "cdn",
+            "signed": false,
+            "signingAlgorithm": null,
+            "sslKeyVersion": 0,
+            "tenant": "tenant1",
+            "tenantName": "tenant1",
+            "topology": "top-for-ds-req2",
+            "type": "HTTP",
+            "xmlId": "ds-top-req-cap2",
+            "anonymousBlockingEnabled": false
+        },
+        {
+            "active": true,
+            "cdnName": "cdn1",
+            "cacheurl": "",
+            "ccrDnsTtl": 3600,
+            "checkPath": "",
+            "consistentHashQueryParams": [],
+            "deepCachingType": "NEVER",
             "displayName": "ds-client-steering",
             "dnsBypassCname": null,
             "dnsBypassIp": "",
@@ -959,6 +1108,20 @@
             "RequiredCapability": "bar"
         }
     ],
+    "topologyBasedDeliveryServicesRequiredCapabilities": [
+        {
+            "xmlID": "ds-top-req-cap",
+            "RequiredCapability": "ram"
+        },
+        {
+            "xmlID": "ds-top-req-cap",
+            "RequiredCapability": "disk"
+        },
+        {
+            "xmlID": "ds-top-req-cap2",
+            "RequiredCapability": "ram"
+        }
+    ],
     "divisions": [
         {
             "name": "division1"
@@ -2653,6 +2816,312 @@
             "tcpPort": 80,
             "type": "MID",
             "updPending": false
+        },
+        {
+            "cachegroup": "dtrc1",
+            "cdnName": "cdn1",
+            "domainName": "kabletown.net",
+            "hostName": "dtrc-mid-01",
+            "httpsPort": 443,
+            "interfaces": [
+                {
+                    "ipAddresses": [
+                        {
+                            "address": "2001:db8:dead:beef::2/64",
+                            "gateway": "2001:db8:dead:beef::1",
+                            "serviceAddress": false
+                        },
+                        {
+                            "address": "192.0.2.2/24",
+                            "gateway": "192.0.2.1",
+                            "serviceAddress": true
+                        }
+                    ],
+                    "monitor": true,
+                    "mtu": 9000,
+                    "name": "bond0"
+                }
+            ],
+            "physLocation": "Denver",
+            "profile": "MID1",
+            "rack": "RR 119.02",
+            "revalPending": false,
+            "status": "REPORTED",
+            "tcpPort": 80,
+            "type": "MID",
+            "updPending": false
+        },
+        {
+            "cachegroup": "dtrc1",
+            "cdnName": "cdn1",
+            "domainName": "kabletown.net",
+            "hostName": "dtrc-mid-02",
+            "httpsPort": 443,
+            "interfaces": [
+                {
+                    "ipAddresses": [
+                        {
+                            "address": "2001:db8:dead:beef::3/64",
+                            "gateway": "2001:db8:dead:beef::1",
+                            "serviceAddress": false
+                        },
+                        {
+                            "address": "192.0.2.3/24",
+                            "gateway": "192.0.2.1",
+                            "serviceAddress": true
+                        }
+                    ],
+                    "monitor": true,
+                    "mtu": 9000,
+                    "name": "bond0"
+                }
+            ],
+            "physLocation": "Denver",
+            "profile": "MID1",
+            "rack": "RR 119.02",
+            "revalPending": false,
+            "status": "REPORTED",
+            "tcpPort": 80,
+            "type": "MID",
+            "updPending": false
+        },
+        {
+            "cachegroup": "dtrc1",
+            "cdnName": "cdn1",
+            "domainName": "kabletown.net",
+            "hostName": "dtrc-mid-03",
+            "httpsPort": 443,
+            "interfaces": [
+                {
+                    "ipAddresses": [
+                        {
+                            "address": "2001:db8:dead:beef::4/64",
+                            "gateway": "2001:db8:dead:beef::1",
+                            "serviceAddress": false
+                        },
+                        {
+                            "address": "192.0.2.4/24",
+                            "gateway": "192.0.2.1",
+                            "serviceAddress": true
+                        }
+                    ],
+                    "monitor": true,
+                    "mtu": 9000,
+                    "name": "bond0"
+                }
+            ],
+            "physLocation": "Denver",
+            "profile": "MID1",
+            "rack": "RR 119.02",
+            "revalPending": false,
+            "status": "REPORTED",
+            "tcpPort": 80,
+            "type": "MID",
+            "updPending": false
+        },
+        {
+            "cachegroup": "dtrc2",
+            "cdnName": "cdn1",
+            "domainName": "kabletown.net",
+            "hostName": "dtrc-edge-01",
+            "httpsPort": 443,
+            "interfaces": [
+                {
+                    "ipAddresses": [
+                        {
+                            "address": "2001:db8:dead:beef::5/64",
+                            "gateway": "2001:db8:dead:beef::1",
+                            "serviceAddress": false
+                        },
+                        {
+                            "address": "192.0.2.5/24",
+                            "gateway": "192.0.2.1",
+                            "serviceAddress": true
+                        }
+                    ],
+                    "monitor": true,
+                    "mtu": 9000,
+                    "name": "bond0"
+                }
+            ],
+            "physLocation": "Denver",
+            "profile": "EDGE1",
+            "rack": "RR 119.02",
+            "revalPending": false,
+            "status": "REPORTED",
+            "tcpPort": 80,
+            "type": "EDGE",
+            "updPending": false
+        },
+        {
+            "cachegroup": "dtrc2",
+            "cdnName": "cdn1",
+            "domainName": "kabletown.net",
+            "hostName": "dtrc-edge-02",
+            "httpsPort": 443,
+            "interfaces": [
+                {
+                    "ipAddresses": [
+                        {
+                            "address": "2001:db8:dead:beef::6/64",
+                            "gateway": "2001:db8:dead:beef::1",
+                            "serviceAddress": false
+                        },
+                        {
+                            "address": "192.0.2.6/24",
+                            "gateway": "192.0.2.1",
+                            "serviceAddress": true
+                        }
+                    ],
+                    "monitor": true,
+                    "mtu": 9000,
+                    "name": "bond0"
+                }
+            ],
+            "physLocation": "Denver",
+            "profile": "EDGE1",
+            "rack": "RR 119.02",
+            "revalPending": false,
+            "status": "REPORTED",
+            "tcpPort": 80,
+            "type": "EDGE",
+            "updPending": false
+        },
+        {
+            "cachegroup": "dtrc2",
+            "cdnName": "cdn1",
+            "domainName": "kabletown.net",
+            "hostName": "dtrc-edge-03",
+            "httpsPort": 443,
+            "interfaces": [
+                {
+                    "ipAddresses": [
+                        {
+                            "address": "2001:db8:dead:beef::7/64",
+                            "gateway": "2001:db8:dead:beef::1",
+                            "serviceAddress": false
+                        },
+                        {
+                            "address": "192.0.2.7/24",
+                            "gateway": "192.0.2.1",
+                            "serviceAddress": true
+                        }
+                    ],
+                    "monitor": true,
+                    "mtu": 9000,
+                    "name": "bond0"
+                }
+            ],
+            "physLocation": "Denver",
+            "profile": "EDGE1",
+            "rack": "RR 119.02",
+            "revalPending": false,
+            "status": "REPORTED",
+            "tcpPort": 80,
+            "type": "EDGE",
+            "updPending": false
+        },
+        {
+            "cachegroup": "dtrc3",
+            "cdnName": "cdn1",
+            "domainName": "kabletown.net",
+            "hostName": "dtrc-edge-04",
+            "httpsPort": 443,
+            "interfaces": [
+                {
+                    "ipAddresses": [
+                        {
+                            "address": "2001:db8:dead:beef::8/64",
+                            "gateway": "2001:db8:dead:beef::1",
+                            "serviceAddress": false
+                        },
+                        {
+                            "address": "192.0.2.8/24",
+                            "gateway": "192.0.2.1",
+                            "serviceAddress": true
+                        }
+                    ],
+                    "monitor": true,
+                    "mtu": 9000,
+                    "name": "bond0"
+                }
+            ],
+            "physLocation": "Denver",
+            "profile": "EDGE1",
+            "rack": "RR 119.02",
+            "revalPending": false,
+            "status": "REPORTED",
+            "tcpPort": 80,
+            "type": "EDGE",
+            "updPending": false
+        },
+        {
+            "cachegroup": "dtrc3",
+            "cdnName": "cdn1",
+            "domainName": "kabletown.net",
+            "hostName": "dtrc-edge-05",
+            "httpsPort": 443,
+            "interfaces": [
+                {
+                    "ipAddresses": [
+                        {
+                            "address": "2001:db8:dead:beef::9/64",
+                            "gateway": "2001:db8:dead:beef::1",
+                            "serviceAddress": false
+                        },
+                        {
+                            "address": "192.0.2.9/24",
+                            "gateway": "192.0.2.1",
+                            "serviceAddress": true
+                        }
+                    ],
+                    "monitor": true,
+                    "mtu": 9000,
+                    "name": "bond0"
+                }
+            ],
+            "physLocation": "Denver",
+            "profile": "EDGE1",
+            "rack": "RR 119.02",
+            "revalPending": false,
+            "status": "REPORTED",
+            "tcpPort": 80,
+            "type": "EDGE",
+            "updPending": false
+        },
+        {
+            "cachegroup": "dtrc3",
+            "cdnName": "cdn1",
+            "domainName": "kabletown.net",
+            "hostName": "dtrc-edge-06",
+            "httpsPort": 443,
+            "interfaces": [
+                {
+                    "ipAddresses": [
+                        {
+                            "address": "2001:db8:dead:beef::10/64",
+                            "gateway": "2001:db8:dead:beef::1",
+                            "serviceAddress": false
+                        },
+                        {
+                            "address": "192.0.2.10/24",
+                            "gateway": "192.0.2.1",
+                            "serviceAddress": true
+                        }
+                    ],
+                    "monitor": true,
+                    "mtu": 9000,
+                    "name": "bond0"
+                }
+            ],
+            "physLocation": "Denver",
+            "profile": "EDGE1",
+            "rack": "RR 119.02",
+            "revalPending": false,
+            "status": "REPORTED",
+            "tcpPort": 80,
+            "type": "EDGE",
+            "updPending": false
         }
     ],
     "serverCapabilities": [
@@ -2661,6 +3130,12 @@
         },
         {
             "name": "bar"
+        },
+        {
+            "name": "ram"
+        },
+        {
+            "name": "disk"
         }
     ],
     "serverServerCapabilities": [
@@ -2671,6 +3146,54 @@
         {
             "serverHostName": "atlanta-org-2",
             "serverCapability": "bar"
+        },
+        {
+            "serverHostName": "dtrc-mid-01",
+            "serverCapability": "ram"
+        },
+        {
+            "serverHostName": "dtrc-mid-01",
+            "serverCapability": "disk"
+        },
+        {
+            "serverHostName": "dtrc-mid-02",
+            "serverCapability": "ram"
+        },
+        {
+            "serverHostName": "dtrc-mid-02",
+            "serverCapability": "disk"
+        },
+        {
+            "serverHostName": "dtrc-edge-01",
+            "serverCapability": "ram"
+        },
+        {
+            "serverHostName": "dtrc-edge-01",
+            "serverCapability": "disk"
+        },
+        {
+            "serverHostName": "dtrc-edge-02",
+            "serverCapability": "ram"
+        },
+        {
+            "serverHostName": "dtrc-edge-02",
+            "serverCapability": "disk"
+        },
+        {
+            "serverHostName": "dtrc-edge-04",
+            "serverCapability": "ram"
+        },
+        {
+            "serverHostName": "dtrc-edge-04",
+            "serverCapability": "disk"
+        },
+        {
+            "serverHostName": "dtrc-edge-05",
+            "serverCapability": "ram"
+        },
+        {
+            "serverHostName": "dtrc-edge-05",
+            "serverCapability": "disk"
         }
     ],
     "serviceCategories": [
@@ -2940,6 +3463,38 @@
                     "parents": []
                 }
             ]
+        },
+        {
+            "name": "top-for-ds-req",
+            "description": "a topology",
+            "nodes": [
+                {
+                    "cachegroup": "dtrc1",
+                    "parents": []
+                },
+                {
+                    "cachegroup": "dtrc2",
+                    "parents": [0]
+                },
+                {
+                    "cachegroup": "dtrc3",
+                    "parents": [0]
+                }
+            ]
+        },
+        {
+            "name": "top-for-ds-req2",
+            "description": "a topology",
+            "nodes": [
+                {
+                    "cachegroup": "dtrc1",
+                    "parents": []
+                },
+                {
+                    "cachegroup": "dtrc2",
+                    "parents": [0]
+                }
+            ]
         }
     ],
     "types": [
diff --git a/traffic_ops/testing/api/v3/traffic_control_test.go 
b/traffic_ops/testing/api/v3/traffic_control_test.go
index 478ad01..9b858a9 100644
--- a/traffic_ops/testing/api/v3/traffic_control_test.go
+++ b/traffic_ops/testing/api/v3/traffic_control_test.go
@@ -21,40 +21,41 @@ import (
 
 // TrafficControl - maps to the tc-fixtures.json file
 type TrafficControl struct {
-       ASNs                                 []tc.ASN                           
     `json:"asns"`
-       CDNs                                 []tc.CDN                           
     `json:"cdns"`
-       CacheGroups                          []tc.CacheGroupNullable            
     `json:"cachegroups"`
-       CacheGroupParameterRequests          []tc.CacheGroupParameterRequest    
     `json:"cachegroupParameters"`
-       Capabilities                         []tc.Capability                    
     `json:"capability"`
-       Coordinates                          []tc.Coordinate                    
     `json:"coordinates"`
-       DeliveryServicesRegexes              []tc.DeliveryServiceRegexesTest    
     `json:"deliveryServicesRegexes"`
-       DeliveryServiceRequests              []tc.DeliveryServiceRequest        
     `json:"deliveryServiceRequests"`
-       DeliveryServiceRequestComments       []tc.DeliveryServiceRequestComment 
     `json:"deliveryServiceRequestComments"`
-       DeliveryServices                     []tc.DeliveryServiceNullableV30    
     `json:"deliveryservices"`
-       DeliveryServicesRequiredCapabilities 
[]tc.DeliveryServicesRequiredCapability 
`json:"deliveryservicesRequiredCapabilities"`
-       Divisions                            []tc.Division                      
     `json:"divisions"`
-       Federations                          []tc.CDNFederation                 
     `json:"federations"`
-       FederationResolvers                  []tc.FederationResolver            
     `json:"federation_resolvers"`
-       Origins                              []tc.Origin                        
     `json:"origins"`
-       Profiles                             []tc.Profile                       
     `json:"profiles"`
-       Parameters                           []tc.Parameter                     
     `json:"parameters"`
-       ProfileParameters                    []tc.ProfileParameter              
     `json:"profileParameters"`
-       PhysLocations                        []tc.PhysLocation                  
     `json:"physLocations"`
-       Regions                              []tc.Region                        
     `json:"regions"`
-       Roles                                []tc.Role                          
     `json:"roles"`
-       Servers                              []tc.ServerNullable                
     `json:"servers"`
-       ServerServerCapabilities             []tc.ServerServerCapability        
     `json:"serverServerCapabilities"`
-       ServerCapabilities                   []tc.ServerCapability              
     `json:"serverCapabilities"`
-       ServiceCategories                    []tc.ServiceCategory               
     `json:"serviceCategories"`
-       Statuses                             []tc.StatusNullable                
     `json:"statuses"`
-       StaticDNSEntries                     []tc.StaticDNSEntry                
     `json:"staticdnsentries"`
-       StatsSummaries                       []tc.StatsSummary                  
     `json:"statsSummaries"`
-       Tenants                              []tc.Tenant                        
     `json:"tenants"`
-       ServerCheckExtensions                []tc.ServerCheckExtensionNullable  
     `json:"servercheck_extensions"`
-       Topologies                           []tc.Topology                      
     `json:"topologies"`
-       Types                                []tc.Type                          
     `json:"types"`
-       SteeringTargets                      []tc.SteeringTargetNullable        
     `json:"steeringTargets"`
-       Serverchecks                         []tc.ServercheckRequestNullable    
     `json:"serverchecks"`
-       Users                                []tc.User                          
     `json:"users"`
-       InvalidationJobs                     []tc.InvalidationJobInput          
     `json:"invalidationJobs"`
+       ASNs                                              []tc.ASN              
                  `json:"asns"`
+       CDNs                                              []tc.CDN              
                  `json:"cdns"`
+       CacheGroups                                       
[]tc.CacheGroupNullable                 `json:"cachegroups"`
+       CacheGroupParameterRequests                       
[]tc.CacheGroupParameterRequest         `json:"cachegroupParameters"`
+       Capabilities                                      []tc.Capability       
                  `json:"capability"`
+       Coordinates                                       []tc.Coordinate       
                  `json:"coordinates"`
+       DeliveryServicesRegexes                           
[]tc.DeliveryServiceRegexesTest         `json:"deliveryServicesRegexes"`
+       DeliveryServiceRequests                           
[]tc.DeliveryServiceRequest             `json:"deliveryServiceRequests"`
+       DeliveryServiceRequestComments                    
[]tc.DeliveryServiceRequestComment      `json:"deliveryServiceRequestComments"`
+       DeliveryServices                                  
[]tc.DeliveryServiceNullableV30         `json:"deliveryservices"`
+       DeliveryServicesRequiredCapabilities              
[]tc.DeliveryServicesRequiredCapability 
`json:"deliveryservicesRequiredCapabilities"`
+       TopologyBasedDeliveryServicesRequiredCapabilities 
[]tc.DeliveryServicesRequiredCapability 
`json:"topologyBasedDeliveryServicesRequiredCapabilities"`
+       Divisions                                         []tc.Division         
                  `json:"divisions"`
+       Federations                                       []tc.CDNFederation    
                  `json:"federations"`
+       FederationResolvers                               
[]tc.FederationResolver                 `json:"federation_resolvers"`
+       Origins                                           []tc.Origin           
                  `json:"origins"`
+       Profiles                                          []tc.Profile          
                  `json:"profiles"`
+       Parameters                                        []tc.Parameter        
                  `json:"parameters"`
+       ProfileParameters                                 []tc.ProfileParameter 
                  `json:"profileParameters"`
+       PhysLocations                                     []tc.PhysLocation     
                  `json:"physLocations"`
+       Regions                                           []tc.Region           
                  `json:"regions"`
+       Roles                                             []tc.Role             
                  `json:"roles"`
+       Servers                                           []tc.ServerNullable   
                  `json:"servers"`
+       ServerServerCapabilities                          
[]tc.ServerServerCapability             `json:"serverServerCapabilities"`
+       ServerCapabilities                                []tc.ServerCapability 
                  `json:"serverCapabilities"`
+       ServiceCategories                                 []tc.ServiceCategory  
                  `json:"serviceCategories"`
+       Statuses                                          []tc.StatusNullable   
                  `json:"statuses"`
+       StaticDNSEntries                                  []tc.StaticDNSEntry   
                  `json:"staticdnsentries"`
+       StatsSummaries                                    []tc.StatsSummary     
                  `json:"statsSummaries"`
+       Tenants                                           []tc.Tenant           
                  `json:"tenants"`
+       ServerCheckExtensions                             
[]tc.ServerCheckExtensionNullable       `json:"servercheck_extensions"`
+       Topologies                                        []tc.Topology         
                  `json:"topologies"`
+       Types                                             []tc.Type             
                  `json:"types"`
+       SteeringTargets                                   
[]tc.SteeringTargetNullable             `json:"steeringTargets"`
+       Serverchecks                                      
[]tc.ServercheckRequestNullable         `json:"serverchecks"`
+       Users                                             []tc.User             
                  `json:"users"`
+       InvalidationJobs                                  
[]tc.InvalidationJobInput               `json:"invalidationJobs"`
 }
diff --git a/traffic_ops/testing/api/v3/withobjs_test.go 
b/traffic_ops/testing/api/v3/withobjs_test.go
index 27c8917..49fb511 100644
--- a/traffic_ops/testing/api/v3/withobjs_test.go
+++ b/traffic_ops/testing/api/v3/withobjs_test.go
@@ -60,6 +60,7 @@ const (
        ServerCapabilities
        ServerChecks
        ServerServerCapabilities
+       ServerServerCapabilitiesForTopologies
        Servers
        ServiceCategories
        Statuses
@@ -68,6 +69,7 @@ const (
        Tenants
        ServerCheckExtensions
        Topologies
+       TopologyBasedDeliveryServiceRequiredCapabilities
        Types
        Users
 )
@@ -78,38 +80,40 @@ type TCObjFuncs struct {
 }
 
 var withFuncs = map[TCObj]TCObjFuncs{
-       CacheGroups:                          {CreateTestCacheGroups, 
DeleteTestCacheGroups},
-       CacheGroupsDeliveryServices:          
{CreateTestCachegroupsDeliveryServices, DeleteTestCachegroupsDeliveryServices},
-       CacheGroupParameters:                 {CreateTestCacheGroupParameters, 
DeleteTestCacheGroupParameters},
-       CDNs:                                 {CreateTestCDNs, DeleteTestCDNs},
-       CDNFederations:                       {CreateTestCDNFederations, 
DeleteTestCDNFederations},
-       Coordinates:                          {CreateTestCoordinates, 
DeleteTestCoordinates},
-       DeliveryServices:                     {CreateTestDeliveryServices, 
DeleteTestDeliveryServices},
-       DeliveryServicesRegexes:              
{CreateTestDeliveryServicesRegexes, DeleteTestDeliveryServicesRegexes},
-       DeliveryServiceRequests:              
{CreateTestDeliveryServiceRequests, DeleteTestDeliveryServiceRequests},
-       DeliveryServiceRequestComments:       
{CreateTestDeliveryServiceRequestComments, 
DeleteTestDeliveryServiceRequestComments},
-       DeliveryServicesRequiredCapabilities: 
{CreateTestDeliveryServicesRequiredCapabilities, 
DeleteTestDeliveryServicesRequiredCapabilities},
-       Divisions:                            {CreateTestDivisions, 
DeleteTestDivisions},
-       FederationUsers:                      {CreateTestFederationUsers, 
DeleteTestFederationUsers},
-       FederationResolvers:                  {CreateTestFederationResolvers, 
DeleteTestFederationResolvers},
-       Origins:                              {CreateTestOrigins, 
DeleteTestOrigins},
-       Parameters:                           {CreateTestParameters, 
DeleteTestParameters},
-       PhysLocations:                        {CreateTestPhysLocations, 
DeleteTestPhysLocations},
-       Profiles:                             {CreateTestProfiles, 
DeleteTestProfiles},
-       ProfileParameters:                    {CreateTestProfileParameters, 
DeleteTestProfileParameters},
-       Regions:                              {CreateTestRegions, 
DeleteTestRegions},
-       Roles:                                {CreateTestRoles, 
DeleteTestRoles},
-       ServerCapabilities:                   {CreateTestServerCapabilities, 
DeleteTestServerCapabilities},
-       ServerChecks:                         {CreateTestServerChecks, 
DeleteTestServerChecks},
-       ServerServerCapabilities:             
{CreateTestServerServerCapabilities, DeleteTestServerServerCapabilities},
-       Servers:                              {CreateTestServers, 
DeleteTestServers},
-       ServiceCategories:                    {CreateTestServiceCategories, 
DeleteTestServiceCategories},
-       Statuses:                             {CreateTestStatuses, 
DeleteTestStatuses},
-       StaticDNSEntries:                     {CreateTestStaticDNSEntries, 
DeleteTestStaticDNSEntries},
-       SteeringTargets:                      {SetupSteeringTargets, 
DeleteTestSteeringTargets},
-       Tenants:                              {CreateTestTenants, 
DeleteTestTenants},
-       ServerCheckExtensions:                {CreateTestServerCheckExtensions, 
DeleteTestServerCheckExtensions},
-       Topologies:                           {CreateTestTopologies, 
DeleteTestTopologies},
-       Types:                                {CreateTestTypes, 
DeleteTestTypes},
-       Users:                                {CreateTestUsers, 
ForceDeleteTestUsers},
+       CacheGroups:                           {CreateTestCacheGroups, 
DeleteTestCacheGroups},
+       CacheGroupsDeliveryServices:           
{CreateTestCachegroupsDeliveryServices, DeleteTestCachegroupsDeliveryServices},
+       CacheGroupParameters:                  {CreateTestCacheGroupParameters, 
DeleteTestCacheGroupParameters},
+       CDNs:                                  {CreateTestCDNs, DeleteTestCDNs},
+       CDNFederations:                        {CreateTestCDNFederations, 
DeleteTestCDNFederations},
+       Coordinates:                           {CreateTestCoordinates, 
DeleteTestCoordinates},
+       DeliveryServices:                      {CreateTestDeliveryServices, 
DeleteTestDeliveryServices},
+       DeliveryServicesRegexes:               
{CreateTestDeliveryServicesRegexes, DeleteTestDeliveryServicesRegexes},
+       DeliveryServiceRequests:               
{CreateTestDeliveryServiceRequests, DeleteTestDeliveryServiceRequests},
+       DeliveryServiceRequestComments:        
{CreateTestDeliveryServiceRequestComments, 
DeleteTestDeliveryServiceRequestComments},
+       DeliveryServicesRequiredCapabilities:  
{CreateTestDeliveryServicesRequiredCapabilities, 
DeleteTestDeliveryServicesRequiredCapabilities},
+       Divisions:                             {CreateTestDivisions, 
DeleteTestDivisions},
+       FederationUsers:                       {CreateTestFederationUsers, 
DeleteTestFederationUsers},
+       FederationResolvers:                   {CreateTestFederationResolvers, 
DeleteTestFederationResolvers},
+       Origins:                               {CreateTestOrigins, 
DeleteTestOrigins},
+       Parameters:                            {CreateTestParameters, 
DeleteTestParameters},
+       PhysLocations:                         {CreateTestPhysLocations, 
DeleteTestPhysLocations},
+       Profiles:                              {CreateTestProfiles, 
DeleteTestProfiles},
+       ProfileParameters:                     {CreateTestProfileParameters, 
DeleteTestProfileParameters},
+       Regions:                               {CreateTestRegions, 
DeleteTestRegions},
+       Roles:                                 {CreateTestRoles, 
DeleteTestRoles},
+       ServerCapabilities:                    {CreateTestServerCapabilities, 
DeleteTestServerCapabilities},
+       ServerChecks:                          {CreateTestServerChecks, 
DeleteTestServerChecks},
+       ServerServerCapabilities:              
{CreateTestServerServerCapabilities, DeleteTestServerServerCapabilities},
+       ServerServerCapabilitiesForTopologies: 
{CreateTestServerServerCapabilities, 
DeleteTestServerServerCapabilitiesForTopologies},
+       Servers:                               {CreateTestServers, 
DeleteTestServers},
+       ServiceCategories:                     {CreateTestServiceCategories, 
DeleteTestServiceCategories},
+       Statuses:                              {CreateTestStatuses, 
DeleteTestStatuses},
+       StaticDNSEntries:                      {CreateTestStaticDNSEntries, 
DeleteTestStaticDNSEntries},
+       SteeringTargets:                       {SetupSteeringTargets, 
DeleteTestSteeringTargets},
+       Tenants:                               {CreateTestTenants, 
DeleteTestTenants},
+       ServerCheckExtensions:                 
{CreateTestServerCheckExtensions, DeleteTestServerCheckExtensions},
+       Topologies:                            {CreateTestTopologies, 
DeleteTestTopologies},
+       TopologyBasedDeliveryServiceRequiredCapabilities: 
{CreateTestTopologyBasedDeliveryServicesRequiredCapabilities, 
DeleteTestDeliveryServicesRequiredCapabilities},
+       Types: {CreateTestTypes, DeleteTestTypes},
+       Users: {CreateTestUsers, ForceDeleteTestUsers},
 }
diff --git 
a/traffic_ops/traffic_ops_golang/deliveryservice/deliveryservices_required_capabilities.go
 
b/traffic_ops/traffic_ops_golang/deliveryservice/deliveryservices_required_capabilities.go
index 44333eb..c08b281 100644
--- 
a/traffic_ops/traffic_ops_golang/deliveryservice/deliveryservices_required_capabilities.go
+++ 
b/traffic_ops/traffic_ops_golang/deliveryservice/deliveryservices_required_capabilities.go
@@ -23,19 +23,20 @@ import (
        "database/sql"
        "errors"
        "fmt"
-       "github.com/apache/trafficcontrol/lib/go-log"
-       
"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/util/ims"
        "net/http"
        "strconv"
        "strings"
        "time"
 
+       "github.com/apache/trafficcontrol/lib/go-log"
        "github.com/apache/trafficcontrol/lib/go-tc"
        "github.com/apache/trafficcontrol/lib/go-tc/tovalidate"
        "github.com/apache/trafficcontrol/lib/go-util"
        "github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/api"
        
"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/dbhelpers"
        "github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/tenant"
+       
"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/util/ims"
+
        validation "github.com/go-ozzo/ozzo-validation"
        "github.com/lib/pq"
 )
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 c905d40..1ae6268 100644
--- a/traffic_ops/traffic_ops_golang/server/servers_server_capability.go
+++ b/traffic_ops/traffic_ops_golang/server/servers_server_capability.go
@@ -27,15 +27,16 @@ import (
        "strings"
        "time"
 
-       "github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/tenant"
-       "github.com/jmoiron/sqlx"
-
+       "github.com/apache/trafficcontrol/lib/go-log"
        "github.com/apache/trafficcontrol/lib/go-tc"
        "github.com/apache/trafficcontrol/lib/go-tc/tovalidate"
        "github.com/apache/trafficcontrol/lib/go-util"
        "github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/api"
        
"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/dbhelpers"
+       "github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/tenant"
+
        validation "github.com/go-ozzo/ozzo-validation"
+       "github.com/jmoiron/sqlx"
        "github.com/lib/pq"
 )
 
@@ -52,8 +53,8 @@ type (
        }
 
        DSTenant struct {
-               TenantID int64 `db:"tenant_id"`
-               ID       int64 `db:"id"`
+               TenantID int `db:"tenant_id"`
+               ID       int `db:"id"`
        }
 )
 
@@ -137,22 +138,120 @@ JOIN server s ON sc.server = s.id ` + where + orderBy + 
pagination +
 }
 
 func (ssc *TOServerServerCapability) 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 := 
checkTopologyBasedDSRequiredCapabilities(ssc, accessibleTenants)
+       if userErr != nil || sysErr != nil {
+               return userErr, sysErr, status
+       }
+
+       userErr, sysErr, status = checkDSRequiredCapabilities(ssc, 
accessibleTenants)
+       if userErr != nil || sysErr != nil {
+               return userErr, sysErr, status
+       }
+
+       return api.GenericDelete(ssc)
+}
+
+func checkTopologyBasedDSRequiredCapabilities(ssc *TOServerServerCapability, 
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 
checkTopologyBasedDSRequiredCapabilities")
+
+       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 
checkTopologyBasedDSRequiredCapabilities: %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 
checkTopologyBasedDSRequiredCapabilities")
+
+       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 
checkTopologyBasedDSRequiredCapabilities: %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 checkDSRequiredCapabilities(ssc *TOServerServerCapability, 
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.QueryRow(checkDSReqCapQuery(), ssc.ServerID, 
ssc.ServerCapability).Scan(pq.Array(&dsIDs)); err != nil {
+       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)
+               return ssc.buildDSReqCapError(dsIDs, accessibleTenants)
        }
-
-       // Delete association
-       return api.GenericDelete(ssc)
+       return nil, nil, http.StatusOK
 }
 
-func (ssc *TOServerServerCapability) buildDSReqCapError(dsIDs []int64) (error, 
error, int) {
+func (ssc *TOServerServerCapability) buildDSReqCapError(dsIDs []int64, 
accessibleTenants map[int]struct{}) (error, error, int) {
 
        dsTenantIDs, err := getDSTenantIDsByIDs(ssc.APIInfo().Tx, dsIDs)
        if err != nil {
@@ -160,23 +259,14 @@ func (ssc *TOServerServerCapability) 
buildDSReqCapError(dsIDs []int64) (error, e
        }
 
        authDSIDs := []string{}
-       checkedTenants := map[int64]bool{}
 
        for _, dsTenantID := range dsTenantIDs {
-               if auth, ok := checkedTenants[dsTenantID.TenantID]; ok { // No 
need to check tenant again
-                       if auth {
-                               authDSIDs = append(authDSIDs, 
strconv.FormatInt(dsTenantID.ID, 10))
+               if _, ok := accessibleTenants[dsTenantID.TenantID]; ok {
+                       if ok {
+                               authDSIDs = append(authDSIDs, 
strconv.Itoa(dsTenantID.ID))
                        }
                        continue
                }
-               authorized, err := 
tenant.IsResourceAuthorizedToUserTx(int(dsTenantID.TenantID), 
ssc.APIInfo().User, ssc.APIInfo().Tx.Tx)
-               if err != nil {
-                       return nil, fmt.Errorf("checking tenancy on delivery 
service: %v", err), http.StatusInternalServerError
-               }
-               if authorized {
-                       authDSIDs = append(authDSIDs, 
strconv.FormatInt(dsTenantID.ID, 10))
-               }
-               checkedTenants[dsTenantID.TenantID] = authorized
        }
 
        dsStr := "delivery services"
@@ -275,6 +365,42 @@ SELECT ARRAY(
        AND dsrc.required_capability = $2)`
 }
 
+// get the topology-based DSes (with all their required capabilities) that a 
given
+// server is assigned to, filtered by the given capability
+func getTopologyBasedDSesReqCapQuery() string {
+       return `
+SELECT
+  ds.xml_id,
+  ds.topology,
+  ds.tenant_id,
+  ARRAY_AGG(dsrc.required_capability) AS req_caps
+FROM server s
+JOIN cachegroup c ON s.cachegroup = c.id
+JOIN topology_cachegroup tc ON c.name = tc.cachegroup
+JOIN deliveryservice ds ON ds.topology = tc.topology
+JOIN deliveryservices_required_capability dsrc ON dsrc.deliveryservice_id = 
ds.id
+WHERE s.id = $1
+GROUP BY ds.xml_id, ds.tenant_id, ds.topology
+HAVING $2 = ANY(ARRAY_AGG(dsrc.required_capability))
+`
+}
+
+// get all the capabilities of the servers in a given server's cachegroup
+// that have a given capability
+func getServerCapabilitiesOfCachegoupQuery() string {
+       return `
+SELECT s.id, ARRAY_AGG(ssc.server_capability) AS capabilities
+FROM server s
+JOIN cachegroup c ON c.id = s.cachegroup AND c.id = (SELECT cachegroup FROM 
server WHERE server.id = $1)
+JOIN server_server_capability ssc ON ssc.server = s.id
+WHERE
+  s.cdn_id = (SELECT cdn_id FROM server WHERE server.id = $1)
+  AND s.id != $1
+GROUP BY s.id
+HAVING $2 = ANY(ARRAY_AGG(ssc.server_capability));
+`
+}
+
 func getDSTenantIDsByIDs(tx *sqlx.Tx, dsIDs []int64) ([]DSTenant, error) {
        dsTenantIDs := []DSTenant{}
 
@@ -287,6 +413,7 @@ func getDSTenantIDsByIDs(tx *sqlx.Tx, dsIDs []int64) 
([]DSTenant, error) {
        if err != nil {
                return nil, fmt.Errorf("querying tenant IDs for delivery 
service IDs: %v", err)
        }
+       defer log.Close(resultRows, "closing resultRows in getDSTenantIDsByIDs")
 
        for resultRows.Next() {
                dsTenantID := DSTenant{}
diff --git a/traffic_ops/traffic_ops_golang/tenant/tenancy.go 
b/traffic_ops/traffic_ops_golang/tenant/tenancy.go
index 15c6843..c40643c 100644
--- a/traffic_ops/traffic_ops_golang/tenant/tenancy.go
+++ b/traffic_ops/traffic_ops_golang/tenant/tenancy.go
@@ -58,18 +58,6 @@ func GetDeliveryServiceTenantInfo(xmlID string, tx *sql.Tx) 
(*DeliveryServiceTen
        return &ds, nil
 }
 
-func GetDeliveryServiceTenantInfoID(tx *sql.Tx, dsID int) 
(*DeliveryServiceTenantInfo, error) {
-       ds := DeliveryServiceTenantInfo{}
-       ds.ID = util.IntPtr(dsID)
-       if err := tx.QueryRow(`SELECT tenant_id FROM deliveryservice where id = 
$1`, &ds.ID).Scan(&ds.TenantID); err != nil {
-               if err == sql.ErrNoRows {
-                       return &ds, errors.New("a deliveryservice with id '" + 
strconv.Itoa(dsID) + "' was not found")
-               }
-               return nil, errors.New("querying tenant id from delivery 
service: " + err.Error())
-       }
-       return &ds, nil
-}
-
 // Check checks that the given user has access to the given XMLID. Returns a 
user error, system error,
 // and the HTTP status code to be returned to the user if an error occurred. 
On success, the user error
 // and system error will both be nil, and the error code should be ignored.

Reply via email to