You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@trafficcontrol.apache.org by ra...@apache.org on 2021/07/08 21:47:00 UTC

[trafficcontrol] branch master updated: Per-Delivery Service TLS versions (#5922)

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

rawlin 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 a7a0ddb  Per-Delivery Service TLS versions (#5922)
a7a0ddb is described below

commit a7a0ddb71b4c9f71ea95d43dce6cb6686a516c93
Author: ocket8888 <oc...@apache.org>
AuthorDate: Thu Jul 8 15:46:48 2021 -0600

    Per-Delivery Service TLS versions (#5922)
    
    * Add migration for DS TLS versions
    
    * Update DS model in APIv4
    
    * Add TLSVersions handling and validation for invalid versions to /deliveryservices
    
    * Fix incorrect responses from /deliveryservices/{ID}/safe
    
    Previously, it would return APIv3.1 structures for API version 1.5, 2.0,
    3.0, 3.1, and 4.0 (as well as unrecognized versions). It now returns the
    appropriate version structures for each requested version.
    
    This fixes #5891
    
    * Fix DSRs storing/returning APIv4 DSes with empty tlsVersions instead of null
    
    * Fix being unable to update DSes with no TLS versions
    
    * Add ATC "known" TLS versions and a warning generation function for possibly insecure version sets
    
    * Add tlsVersions warnings to /deliveryservices
    
    * Add validation that prevents tlsVersions on STEERING/CLIENT_STEERING delivery services
    
    * Fix a bug where adding TLS versions returns a 500 ISE response
    
    * Fix indirection of non-pointer value
    
    * Add TLS versions tests to the v4 API client
    
    * Remove 'lastUpdated' timestamps from testing data
    
    This breaks parsing when the API endpoints that use this data change
    from our custom format to RFC3339.
    
    * Update /deliveryservices documentation
    
    * Update /deliveryservices/{{ID}} documentation
    
    * Update /deliveryservices/{{ID}}/safe documentation
    
    * Update /deliveryservice_requests documentation
    
    * Update /deliveryservice_requests/{{ID}}/status documentation
    
    * Update /deliveryservice_requests/{{ID}}/assign documentation
    
    * Update /servers/{{ID}}/deliveryservices documentation
    
    * Add section about TLS versions to the Delivery Service overview docs
    
    * Updated CHANGELOG
    
    * Revert non-nullable DS fields
    
    * Change version checking to proper upgrade to preserve existing pattern
    
    * Rename function to make its purpose clear
    
    * Move comment into GoDoc where it's more useful
    
    * Use Exec for queries that don't return rows
    
    * Consolidate TLS Versions warnings into a single function
    
    That function is now a method of a Delivery Service, which means the
    call signatures of various functions in the
    github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/deliveryservice
    package no longer need to be changed.
    
    * re-use query for TLS Versions
    
    * Fix tests for new query structure
    
    * Revert all breaking DS changes
    
    * Fix not saving DS changes
    
    * Fix unit test failure
    
    * Fix integration test missing required DS fields
---
 CHANGELOG.md                                       |   5 +-
 docs/source/api/v4/deliveryservice_requests.rst    |  28 +-
 .../api/v4/deliveryservice_requests_id_assign.rst  |   8 +-
 .../api/v4/deliveryservice_requests_id_status.rst  |   8 +-
 docs/source/api/v4/deliveryservices.rst            | 489 +++++++-----
 docs/source/api/v4/deliveryservices_id.rst         | 270 ++++++-
 docs/source/api/v4/deliveryservices_id_safe.rst    | 197 ++---
 docs/source/api/v4/servers_id_deliveryservices.rst | 111 +--
 docs/source/overview/delivery_services.rst         |  12 +
 lib/go-tc/deliveryservices.go                      | 494 ++++++++++--
 lib/go-tc/deliveryservices_test.go                 | 844 +++++++++++++++++++++
 .../2021061100000000_ds_tls_versions.sql           |  91 +++
 .../v4/deliveryservice_request_comments_test.go    |   2 +-
 .../api/v4/deliveryservice_requests_test.go        |  12 +-
 .../testing/api/v4/deliveryservices_test.go        | 167 +++-
 traffic_ops/testing/api/v4/tc-fixtures.json        |  98 ---
 .../traffic_ops_golang/dbhelpers/db_helpers.go     |   8 +-
 .../deliveryservice/deliveryservices.go            | 200 ++++-
 .../deliveryservice/deliveryservices_test.go       |  39 +
 .../deliveryservice/request/requests.go            |  13 +
 .../deliveryservice/request/validate.go            |   2 +-
 .../traffic_ops_golang/deliveryservice/safe.go     |  38 +-
 traffic_ops/v4-client/deliveryservice.go           |   8 +-
 23 files changed, 2480 insertions(+), 664 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8c84f72..667e696 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -51,6 +51,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/).
 - Added `traffic_ops/app/db/traffic_vault_migrate` to help with migrating Traffic Ops Traffic Vault backends
 - Added a tool at `/traffic_ops/app/db/reencrypt` to re-encrypt the data in the Postgres Traffic Vault with a new key.
 - Enhanced ort integration test for reload states
+- Added a new field to Delivery Services - `tlsVersions` - that explicitly lists the TLS versions that may be used to retrieve their content from Cache Servers.
 
 ### Fixed
 - [#5690](https://github.com/apache/trafficcontrol/issues/5690) - Fixed github action for added/modified db migration file.
@@ -76,7 +77,9 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/).
 - [#5965](https://github.com/apache/trafficcontrol/issues/5965) - Fixed Traffic Ops /deliveryserviceservers If-Modified-Since requests.
 - Fixed t3c to create config files and directories as ats.ats
 - Fixed t3c-apply service restart and ats config reload logic.
-- Reduced TR dns.max-threads ansible default from 10000 to 100. 
+- Reduced TR dns.max-threads ansible default from 10000 to 100.
+- [#5981](https://github.com/apache/trafficcontrol/issues/5891) - `/deliveryservices/{{ID}}/safe` returns incorrect response for the requested API version
+- [#5984](https://github.com/apache/trafficcontrol/issues/5894) - `/servers/{{ID}}/deliveryservices` returns incorrect response for the requested API version
 
 ### Changed
 - Updated the Traffic Ops Python client to 3.0
diff --git a/docs/source/api/v4/deliveryservice_requests.rst b/docs/source/api/v4/deliveryservice_requests.rst
index 60edef2..ce89718 100644
--- a/docs/source/api/v4/deliveryservice_requests.rst
+++ b/docs/source/api/v4/deliveryservice_requests.rst
@@ -135,7 +135,7 @@ The response is an array of representations of :term:`Delivery Service Requests`
 			"innerHeaderRewrite": null,
 			"ipv6RoutingEnabled": true,
 			"lastHeaderRewrite": null,
-			"lastUpdated": "0001-01-01 00:00:00+00",
+			"lastUpdated": "0001-01-01T00:00:00Z",
 			"logsEnabled": true,
 			"longDesc": "Apachecon North America 2018",
 			"matchList": [
@@ -187,7 +187,8 @@ The response is an array of representations of :term:`Delivery Service Requests`
 				"zyx"
 			],
 			"maxOriginConnections": 0,
-			"ecsEnabled": false
+			"ecsEnabled": false,
+			"tlsVersions": null
 		},
 		"status": "draft"
 	}]}
@@ -249,7 +250,7 @@ The request must be a well-formed representation of a :term:`Delivery Service Re
 			"innerHeaderRewrite": null,
 			"ipv6RoutingEnabled": true,
 			"lastHeaderRewrite": null,
-			"lastUpdated": "2020-02-13 16:43:54+00",
+			"lastUpdated": "2020-02-13T16:43:54Z",
 			"logsEnabled": true,
 			"longDesc": "Apachecon North America 2018",
 			"matchList": [
@@ -302,7 +303,8 @@ The request must be a well-formed representation of a :term:`Delivery Service Re
 			],
 			"maxOriginConnections": 0,
 			"ecsEnabled": false,
-			"serviceCategory": null
+			"serviceCategory": null,
+			"tlsVersions": null
 		}
 	}
 
@@ -372,7 +374,7 @@ The response will be a representation of the created :term:`Delivery Service Req
 				"innerHeaderRewrite": null,
 				"ipv6RoutingEnabled": true,
 				"lastHeaderRewrite": null,
-				"lastUpdated": "0001-01-01 00:00:00+00",
+				"lastUpdated": "0001-01-01T00:00:00Z",
 				"logsEnabled": true,
 				"longDesc": "Apachecon North America 2018",
 				"matchList": [
@@ -424,7 +426,8 @@ The response will be a representation of the created :term:`Delivery Service Req
 					"zyx"
 				],
 				"maxOriginConnections": 0,
-				"ecsEnabled": false
+				"ecsEnabled": false,
+				"tlsVersions": null
 			},
 			"original": {
 				"active": true,
@@ -455,7 +458,7 @@ The response will be a representation of the created :term:`Delivery Service Req
 				"innerHeaderRewrite": null,
 				"ipv6RoutingEnabled": true,
 				"lastHeaderRewrite": null,
-				"lastUpdated": "2020-02-13 16:43:54+00",
+				"lastUpdated": "2020-02-13T16:43:54Z",
 				"logsEnabled": true,
 				"longDesc": "Apachecon North America 2018",
 				"matchList": [
@@ -508,7 +511,8 @@ The response will be a representation of the created :term:`Delivery Service Req
 				],
 				"maxOriginConnections": 0,
 				"ecsEnabled": false,
-				"serviceCategory": null
+				"serviceCategory": null,
+				"tlsVersions": null
 			},
 			"status": "draft"
 		}
@@ -628,7 +632,7 @@ The response is a full representation of the edited :term:`Delivery Service Requ
 			"infoUrl": null,
 			"initialDispersion": 1,
 			"ipv6RoutingEnabled": true,
-			"lastUpdated": "2020-09-25 02:09:54+00",
+			"lastUpdated": "2020-09-25T02:09:54Z",
 			"logsEnabled": true,
 			"longDesc": "Apachecon North America 2018",
 			"matchList": [
@@ -685,7 +689,8 @@ The response is a full representation of the edited :term:`Delivery Service Requ
 			"firstHeaderRewrite": null,
 			"innerHeaderRewrite": null,
 			"lastHeaderRewrite": null,
-			"serviceCategory": null
+			"serviceCategory": null,
+			"tlsVersions": null
 		},
 		"requested": {
 			"active": true,
@@ -756,7 +761,8 @@ The response is a full representation of the edited :term:`Delivery Service Requ
 			"firstHeaderRewrite": null,
 			"innerHeaderRewrite": null,
 			"lastHeaderRewrite": null,
-			"serviceCategory": null
+			"serviceCategory": null,
+			"tlsVersions": null
 		},
 		"status": "draft"
 	}}
diff --git a/docs/source/api/v4/deliveryservice_requests_id_assign.rst b/docs/source/api/v4/deliveryservice_requests_id_assign.rst
index 4c6216a..8c7acce 100644
--- a/docs/source/api/v4/deliveryservice_requests_id_assign.rst
+++ b/docs/source/api/v4/deliveryservice_requests_id_assign.rst
@@ -169,7 +169,7 @@ The response contains a full representation of the newly assigned :term:`Deliver
 			"infoUrl": null,
 			"initialDispersion": 1,
 			"ipv6RoutingEnabled": true,
-			"lastUpdated": "2020-09-25 02:09:54+00",
+			"lastUpdated": "2020-09-25T02:09:54Z",
 			"logsEnabled": true,
 			"longDesc": "Apachecon North America 2018",
 			"matchList": [
@@ -226,7 +226,8 @@ The response contains a full representation of the newly assigned :term:`Deliver
 			"firstHeaderRewrite": null,
 			"innerHeaderRewrite": null,
 			"lastHeaderRewrite": null,
-			"serviceCategory": null
+			"serviceCategory": null,
+			"tlsVersions": null
 		},
 		"requested": {
 			"active": true,
@@ -297,7 +298,8 @@ The response contains a full representation of the newly assigned :term:`Deliver
 			"firstHeaderRewrite": null,
 			"innerHeaderRewrite": null,
 			"lastHeaderRewrite": null,
-			"serviceCategory": null
+			"serviceCategory": null,
+			"tlsVersions": null
 		},
 		"status": "draft"
 	}}
diff --git a/docs/source/api/v4/deliveryservice_requests_id_status.rst b/docs/source/api/v4/deliveryservice_requests_id_status.rst
index d982174..1ecd5f4 100644
--- a/docs/source/api/v4/deliveryservice_requests_id_status.rst
+++ b/docs/source/api/v4/deliveryservice_requests_id_status.rst
@@ -166,7 +166,7 @@ The response is a full representation of the modified :term:`DSR`.
 			"infoUrl": null,
 			"initialDispersion": 1,
 			"ipv6RoutingEnabled": true,
-			"lastUpdated": "2020-09-25 02:09:54+00",
+			"lastUpdated": "2020-09-25T02:09:54Z",
 			"logsEnabled": true,
 			"longDesc": "Apachecon North America 2018",
 			"matchList": [
@@ -223,7 +223,8 @@ The response is a full representation of the modified :term:`DSR`.
 			"firstHeaderRewrite": null,
 			"innerHeaderRewrite": null,
 			"lastHeaderRewrite": null,
-			"serviceCategory": null
+			"serviceCategory": null,
+			"tlsVersions": null
 		},
 		"requested": {
 			"active": true,
@@ -294,7 +295,8 @@ The response is a full representation of the modified :term:`DSR`.
 			"firstHeaderRewrite": null,
 			"innerHeaderRewrite": null,
 			"lastHeaderRewrite": null,
-			"serviceCategory": null
+			"serviceCategory": null,
+			"tlsVersions": null
 		},
 		"status": "submitted"
 	}}
diff --git a/docs/source/api/v4/deliveryservices.rst b/docs/source/api/v4/deliveryservices.rst
index db62e94..fb7f676 100644
--- a/docs/source/api/v4/deliveryservices.rst
+++ b/docs/source/api/v4/deliveryservices.rst
@@ -69,6 +69,17 @@ Request Structure
 	| active            | no       | Show only the :term:`Delivery Services` that have :ref:`ds-active` set or not based on this boolean (whether or not they are active)    |
 	+-------------------+----------+-----------------------------------------------------------------------------------------------------------------------------------------+
 
+.. code-block:: http
+	:caption: Request Example
+
+	GET /api/4.0/deliveryservices?xmlId=demo2 HTTP/1.1
+	Host: trafficops.infra.ciab.test
+	User-Agent: python-requests/2.24.0
+	Accept-Encoding: gzip, deflate
+	Accept: */*
+	Connection: keep-alive
+	Cookie: mojolicious=...
+
 Response Structure
 ------------------
 :active:                   A boolean that defines :ref:`ds-active`.
@@ -104,10 +115,14 @@ Response Structure
 :innerHeaderRewrite:        A set of :ref:`ds-inner-header-rw-rules`
 :ipv6RoutingEnabled:        A boolean that defines the :ref:`ds-ipv6-routing` setting on this :term:`Delivery Service`
 :lastHeaderRewrite:         A set of :ref:`ds-last-header-rw-rules`
-:lastUpdated:               The date and time at which this :term:`Delivery Service` was last updated, in :ref:`non-rfc-datetime`
-:logsEnabled:               A boolean that defines the :ref:`ds-logs-enabled` setting on this :term:`Delivery Service`
-:longDesc:                  The :ref:`ds-longdesc` of this :term:`Delivery Service`
-:matchList:                 The :term:`Delivery Service`'s :ref:`ds-matchlist`
+:lastUpdated:               The date and time at which this :term:`Delivery Service` was last updated, in :rfc:3339 format
+
+	.. versionchanged:: 4.0
+		Prior to API version 4.0, this property used :ref:`non-rfc-datetime`.
+
+:logsEnabled: A boolean that defines the :ref:`ds-logs-enabled` setting on this :term:`Delivery Service`
+:longDesc:    The :ref:`ds-longdesc` of this :term:`Delivery Service`
+:matchList:   The :term:`Delivery Service`'s :ref:`ds-matchlist`
 
 	:pattern:   A regular expression - the use of this pattern is dependent on the ``type`` field (backslashes are escaped)
 	:setNumber: An integer that provides explicit ordering of :ref:`ds-matchlist` items - this is used as a priority ranking by Traffic Router, and is not guaranteed to correspond to the ordering of items in the array.
@@ -137,12 +152,16 @@ Response Structure
 :rangeSliceBlockSize:   An integer that defines the byte block size for the ATS Slice Plugin. It can only and must be set if ``rangeRequestHandling`` is set to 3.
 :sslKeyVersion:         This integer indicates the :ref:`ds-ssl-key-version`
 :tenantId:              The integral, unique identifier of the :ref:`ds-tenant` who owns this :term:`Delivery Service`
-:topology:              The unique name of the :term:`Topology` that this :term:`Delivery Service` is assigned to
-:trRequestHeaders:      If defined, this defines the :ref:`ds-tr-req-headers` used by Traffic Router for this :term:`Delivery Service`
-:trResponseHeaders:     If defined, this defines the :ref:`ds-tr-resp-headers` used by Traffic Router for this :term:`Delivery Service`
-:type:                  The :ref:`ds-types` of this :term:`Delivery Service`
-:typeId:                The integral, unique identifier of the :ref:`ds-types` of this :term:`Delivery Service`
-:xmlId:                 This :term:`Delivery Service`'s :ref:`ds-xmlid`
+:tlsVersions:           A list of explicitly supported :ref:`ds-tls-versions`
+
+	.. versionadded:: 4.0
+
+:topology:          The unique name of the :term:`Topology` that this :term:`Delivery Service` is assigned to
+:trRequestHeaders:  If defined, this defines the :ref:`ds-tr-req-headers` used by Traffic Router for this :term:`Delivery Service`
+:trResponseHeaders: If defined, this defines the :ref:`ds-tr-resp-headers` used by Traffic Router for this :term:`Delivery Service`
+:type:              The :ref:`ds-types` of this :term:`Delivery Service`
+:typeId:            The integral, unique identifier of the :ref:`ds-types` of this :term:`Delivery Service`
+:xmlId:             This :term:`Delivery Service`'s :ref:`ds-xmlid`
 
 .. code-block:: http
 	:caption: Response Example
@@ -152,99 +171,98 @@ Response Structure
 	Access-Control-Allow-Headers: Origin, X-Requested-With, Content-Type, Accept, Set-Cookie, Cookie
 	Access-Control-Allow-Methods: POST,GET,OPTIONS,PUT,DELETE
 	Access-Control-Allow-Origin: *
+	Content-Encoding: gzip
 	Content-Type: application/json
-	Set-Cookie: mojolicious=...; Path=/; Expires=Mon, 18 Nov 2019 17:40:54 GMT; Max-Age=3600; HttpOnly
-	Whole-Content-Sha512: mCLMjvACRKHNGP/OSx4javkOtxxzyiDdQzsV78IamUhVmvyKyKaCeOKRmpsG69w+nhh3OkPZ6e9MMeJpcJSKcA==
+	Permissions-Policy: interest-cohort=()
+	Set-Cookie: mojolicious=...; Path=/; Expires=Mon, 07 Jun 2021 22:52:20 GMT; Max-Age=3600; HttpOnly
+	Vary: Accept-Encoding
 	X-Server-Name: traffic_ops_golang/
-	Date: Thu, 15 Nov 2018 19:04:29 GMT
-	Transfer-Encoding: chunked
+	Date: Mon, 07 Jun 2021 21:52:20 GMT
+	Content-Length: 847
 
-	{ "response": [{
-		"active": true,
-		"anonymousBlockingEnabled": false,
-		"cacheurl": null,
-		"ccrDnsTtl": null,
-		"cdnId": 2,
-		"cdnName": "CDN-in-a-Box",
-		"checkPath": null,
-		"displayName": "Demo 1",
-		"dnsBypassCname": null,
-		"dnsBypassIp": null,
-		"dnsBypassIp6": null,
-		"dnsBypassTtl": null,
-		"dscp": 0,
-		"edgeHeaderRewrite": null,
-		"firstHeaderRewrite": null,
-		"geoLimit": 0,
-		"geoLimitCountries": null,
-		"geoLimitRedirectURL": null,
-		"geoProvider": 0,
-		"globalMaxMbps": null,
-		"globalMaxTps": null,
-		"httpBypassFqdn": null,
-		"id": 1,
-		"infoUrl": null,
-		"initialDispersion": 1,
-		"innerHeaderRewrite": null,
-		"ipv6RoutingEnabled": true,
-		"lastHeaderRewrite": null,
-		"lastUpdated": "2019-05-15 14:32:05+00",
-		"logsEnabled": true,
-		"longDesc": "Apachecon North America 2018",
-		"matchList": [
-			{
-				"type": "HOST_REGEXP",
-				"setNumber": 0,
-				"pattern": ".*\\.demo1\\..*"
-			}
-		],
-		"maxDnsAnswers": null,
-		"midHeaderRewrite": null,
-		"missLat": 42,
-		"missLong": -88,
-		"multiSiteOrigin": false,
-		"originShield": null,
-		"orgServerFqdn": "http://origin.infra.ciab.test",
-		"profileDescription": null,
-		"profileId": null,
-		"profileName": null,
-		"protocol": 2,
-		"qstringIgnore": 0,
-		"rangeRequestHandling": 0,
-		"regexRemap": null,
-		"regionalGeoBlocking": false,
-		"remapText": null,
-		"routingName": "video",
-		"signed": false,
-		"sslKeyVersion": null,
-		"tenantId": 1,
-		"type": "HTTP",
-		"typeId": 1,
-		"xmlId": "demo1",
-		"exampleURLs": [
-			"http://video.demo1.mycdn.ciab.test",
-			"https://video.demo1.mycdn.ciab.test"
-		],
-		"deepCachingType": "NEVER",
-		"fqPacingRate": null,
-		"signingAlgorithm": null,
-		"tenant": "root",
-		"trResponseHeaders": null,
-		"trRequestHeaders": null,
-		"consistentHashRegex": null,
-		"consistentHashQueryParams": [
-			"abc",
-			"pdq",
-			"xxx",
-			"zyx"
-		],
-		"maxOriginConnections": 0,
-		"maxRequestHeaderBytes": 131072,
-		"ecsEnabled": false,
-		"rangeSliceBlockSize": null,
-		"topology": null
-		"serviceCategory": null
-	}]}
+	{ "response": [
+		{
+			"active": true,
+			"anonymousBlockingEnabled": false,
+			"ccrDnsTtl": null,
+			"cdnId": 2,
+			"cdnName": "CDN-in-a-Box",
+			"checkPath": null,
+			"consistentHashQueryParams": [],
+			"consistentHashRegex": null,
+			"deepCachingType": "NEVER",
+			"displayName": "Demo 2",
+			"dnsBypassCname": null,
+			"dnsBypassIp": null,
+			"dnsBypassIp6": null,
+			"dnsBypassTtl": null,
+			"dscp": 0,
+			"ecsEnabled": false,
+			"edgeHeaderRewrite": null,
+			"exampleURLs": [
+				"http://video.demo2.mycdn.ciab.test",
+				"https://video.demo2.mycdn.ciab.test"
+			],
+			"firstHeaderRewrite": null,
+			"fqPacingRate": null,
+			"geoLimit": 0,
+			"geoLimitCountries": null,
+			"geoLimitRedirectURL": null,
+			"geoProvider": 0,
+			"globalMaxMbps": null,
+			"globalMaxTps": null,
+			"httpBypassFqdn": null,
+			"id": 1,
+			"infoUrl": null,
+			"initialDispersion": 1,
+			"innerHeaderRewrite": null,
+			"ipv6RoutingEnabled": true,
+			"lastHeaderRewrite": null,
+			"lastUpdated": "2021-06-07T21:50:03.009954Z",
+			"logsEnabled": true,
+			"longDesc": "DNS Delivery Service for use with a Federation",
+			"matchList": [
+				{
+					"type": "HOST_REGEXP",
+					"setNumber": 0,
+					"pattern": ".*\\.demo2\\..*"
+				}
+			],
+			"maxDnsAnswers": null,
+			"maxOriginConnections": 0,
+			"maxRequestHeaderBytes": 0,
+			"midHeaderRewrite": null,
+			"missLat": 42,
+			"missLong": -88,
+			"multiSiteOrigin": true,
+			"originShield": null,
+			"orgServerFqdn": "http://origin.infra.ciab.test",
+			"profileDescription": null,
+			"profileId": null,
+			"profileName": null,
+			"protocol": 2,
+			"qstringIgnore": 0,
+			"rangeRequestHandling": 0,
+			"rangeSliceBlockSize": null,
+			"regexRemap": null,
+			"regionalGeoBlocking": false,
+			"remapText": null,
+			"routingName": "video",
+			"serviceCategory": null,
+			"signed": false,
+			"signingAlgorithm": null,
+			"sslKeyVersion": null,
+			"tenant": "root",
+			"tenantId": 1,
+			"tlsVersions": null,
+			"topology": "demo1-top",
+			"trResponseHeaders": null,
+			"trRequestHeaders": null,
+			"type": "DNS",
+			"typeId": 5,
+			"xmlId": "demo2"
+		}
+	]}
 
 
 ``POST``
@@ -308,56 +326,99 @@ Request Structure
 :serviceCategory:           The name of the :ref:`ds-service-category` with which the :term:`Delivery Service` is associated - or ``null`` if there is to be no such category
 :signed:                    ``true`` if  and only if ``signingAlgorithm`` is not ``null``, ``false`` otherwise
 :signingAlgorithm:          Either a :ref:`ds-signing-algorithm` or ``null`` to indicate URL/URI signing is not implemented on this :term:`Delivery Service`
-:rangeSliceBlockSize:      An integer that defines the byte block size for the ATS Slice Plugin. It can only and must be set if ``rangeRequestHandling`` is set to 3. It can only be between (inclusive) 262144 (256KB) - 33554432 (32MB).
+:rangeSliceBlockSize:       An integer that defines the byte block size for the ATS Slice Plugin. It can only and must be set if ``rangeRequestHandling`` is set to 3. It can only be between (inclusive) 262144 (256KB) - 33554432 (32MB).
 :sslKeyVersion:             This integer indicates the :ref:`ds-ssl-key-version`
 :tenantId:                  The integral, unique identifier of the :ref:`ds-tenant` who owns this :term:`Delivery Service`
-:topology:                  The unique name of the :term:`Topology` that this :term:`Delivery Service` is assigned to
-:trRequestHeaders:          If defined, this defines the :ref:`ds-tr-req-headers` used by Traffic Router for this :term:`Delivery Service`
-:trResponseHeaders:         If defined, this defines the :ref:`ds-tr-resp-headers` used by Traffic Router for this :term:`Delivery Service`
-:type:                      The :ref:`ds-types` of this :term:`Delivery Service`
-:typeId:                    The integral, unique identifier of the :ref:`ds-types` of this :term:`Delivery Service`
-:xmlId:                     This :term:`Delivery Service`'s :ref:`ds-xmlid`
+:tlsVersions:               An array of explicitly supported :ref:`ds-tls-versions`
+
+	.. versionadded:: 4.0
+
+:topology:          The unique name of the :term:`Topology` that this :term:`Delivery Service` is assigned to
+:trRequestHeaders:  If defined, this defines the :ref:`ds-tr-req-headers` used by Traffic Router for this :term:`Delivery Service`
+:trResponseHeaders: If defined, this defines the :ref:`ds-tr-resp-headers` used by Traffic Router for this :term:`Delivery Service`
+:type:              The :ref:`ds-types` of this :term:`Delivery Service`
+:typeId:            The integral, unique identifier of the :ref:`ds-types` of this :term:`Delivery Service`
+:xmlId:             This :term:`Delivery Service`'s :ref:`ds-xmlid`
 
 .. code-block:: http
 	:caption: Request Example
 
 	POST /api/4.0/deliveryservices HTTP/1.1
-	Host: trafficops.infra.ciab.test
-	User-Agent: curl/7.47.0
+	User-Agent: python-requests/2.24.0
+	Accept-Encoding: gzip, deflate
 	Accept: */*
+	Connection: keep-alive
 	Cookie: mojolicious=...
-	Content-Length: 761
+	Content-Length: 1602
 	Content-Type: application/json
+	Host: trafficops.infra.ciab.test
 
 	{
 		"active": false,
 		"anonymousBlockingEnabled": false,
+		"ccrDnsTtl": null,
 		"cdnId": 2,
+		"checkPath": null,
+		"consistentHashRegex": null,
+		"consistentHashQueryParams": [],
 		"deepCachingType": "NEVER",
 		"displayName": "test",
+		"dnsBypassCname": null,
+		"dnsBypassIp": null,
+		"dnsBypassIp6": null,
+		"dnsBypassTtl": null,
 		"dscp": 0,
 		"ecsEnabled": true,
+		"edgeHeaderRewrite": null,
+		"firstHeaderRewrite": null,
+		"fqPacingRate": null,
 		"geoLimit": 0,
+		"geoLimitCountries": null,
+		"geoLimitRedirectUrl": null,
 		"geoProvider": 0,
+		"globalMaxMbps": null,
+		"globalMaxTps": null,
+		"httpBypassFqdn": null,
+		"infoUrl": null,
 		"initialDispersion": 1,
+		"innerHeaderRewrite": null,
 		"ipv6RoutingEnabled": false,
+		"lastHeaderRewrite": null,
 		"logsEnabled": true,
 		"longDesc": "A Delivery Service created expressly for API documentation examples",
+		"longDesc1": null,
+		"longDesc2": null,
+		"maxDnsAnswers": null,
 		"missLat": 0,
 		"missLong": 0,
 		"maxOriginConnections": 0,
 		"maxRequestHeaderBytes": 131072,
+		"midHeaderRewrite": null,
 		"multiSiteOrigin": false,
 		"orgServerFqdn": "http://origin.infra.ciab.test",
+		"originShield": null,
+		"profileId": null,
 		"protocol": 0,
 		"qstringIgnore": 0,
 		"rangeRequestHandling": 0,
+		"regexRemap": null,
 		"regionalGeoBlocking": false,
 		"routingName": "test",
 		"serviceCategory": null,
 		"signed": false,
+		"signingAlgorithm": null,
+		"rangeSliceBlockSize": null,
+		"sslKeyVersion": null,
 		"tenant": "root",
 		"tenantId": 1,
+		"tlsVersions": [
+			"1.2",
+			"1.3"
+		],
+		"topology": null,
+		"trRequestHeaders": null,
+		"trResponseHeaders": null,
+		"type": "HTTP",
 		"typeId": 1,
 		"xmlId": "test"
 	}
@@ -398,10 +459,14 @@ Response Structure
 :innerHeaderRewrite:        A set of :ref:`ds-inner-header-rw-rules`
 :ipv6RoutingEnabled:        A boolean that defines the :ref:`ds-ipv6-routing` setting on this :term:`Delivery Service`
 :lastHeaderRewrite:         A set of :ref:`ds-last-header-rw-rules`
-:lastUpdated:               The date and time at which this :term:`Delivery Service` was last updated, in :ref:`non-rfc-datetime`
-:logsEnabled:               A boolean that defines the :ref:`ds-logs-enabled` setting on this :term:`Delivery Service`
-:longDesc:                  The :ref:`ds-longdesc` of this :term:`Delivery Service`
-:matchList:                 The :term:`Delivery Service`'s :ref:`ds-matchlist`
+:lastUpdated:               The date and time at which this :term:`Delivery Service` was last updated, in :rfc:3339 format
+
+	.. versionchanged:: 4.0
+		Prior to API version 4.0, this property used :ref:`non-rfc-datetime`.
+
+:logsEnabled: A boolean that defines the :ref:`ds-logs-enabled` setting on this :term:`Delivery Service`
+:longDesc:    The :ref:`ds-longdesc` of this :term:`Delivery Service`
+:matchList:   The :term:`Delivery Service`'s :ref:`ds-matchlist`
 
 	:pattern:   A regular expression - the use of this pattern is dependent on the ``type`` field (backslashes are escaped)
 	:setNumber: An integer that provides explicit ordering of :ref:`ds-matchlist` items - this is used as a priority ranking by Traffic Router, and is not guaranteed to correspond to the ordering of items in the array.
@@ -431,111 +496,129 @@ Response Structure
 :rangeSliceBlockSize:   An integer that defines the byte block size for the ATS Slice Plugin. It can only and must be set if ``rangeRequestHandling`` is set to 3.
 :sslKeyVersion:         This integer indicates the :ref:`ds-ssl-key-version`
 :tenantId:              The integral, unique identifier of the :ref:`ds-tenant` who owns this :term:`Delivery Service`
-:topology:              The unique name of the :term:`Topology` that this :term:`Delivery Service` is assigned to
-:trRequestHeaders:      If defined, this defines the :ref:`ds-tr-req-headers` used by Traffic Router for this :term:`Delivery Service`
-:trResponseHeaders:     If defined, this defines the :ref:`ds-tr-resp-headers` used by Traffic Router for this :term:`Delivery Service`
-:type:                  The :ref:`ds-types` of this :term:`Delivery Service`
-:typeId:                The integral, unique identifier of the :ref:`ds-types` of this :term:`Delivery Service`
-:xmlId:                 This :term:`Delivery Service`'s :ref:`ds-xmlid`
+:tlsVersions:           An array of explicitly supported :ref:`ds-tls-versions`
+
+	.. versionadded:: 4.0
+
+:topology:          The unique name of the :term:`Topology` that this :term:`Delivery Service` is assigned to
+:trRequestHeaders:  If defined, this defines the :ref:`ds-tr-req-headers` used by Traffic Router for this :term:`Delivery Service`
+:trResponseHeaders: If defined, this defines the :ref:`ds-tr-resp-headers` used by Traffic Router for this :term:`Delivery Service`
+:type:              The :ref:`ds-types` of this :term:`Delivery Service`
+:typeId:            The integral, unique identifier of the :ref:`ds-types` of this :term:`Delivery Service`
+:xmlId:             This :term:`Delivery Service`'s :ref:`ds-xmlid`
 
 .. code-block:: http
 	:caption: Response Example
 
-	HTTP/1.1 200 OK
+	HTTP/1.1 201 Created
 	Access-Control-Allow-Credentials: true
 	Access-Control-Allow-Headers: Origin, X-Requested-With, Content-Type, Accept, Set-Cookie, Cookie
 	Access-Control-Allow-Methods: POST,GET,OPTIONS,PUT,DELETE
 	Access-Control-Allow-Origin: *
+	Content-Encoding: gzip
 	Content-Type: application/json
-	Set-Cookie: mojolicious=...; Path=/; Expires=Mon, 18 Nov 2019 17:40:54 GMT; Max-Age=3600; HttpOnly
-	Whole-Content-Sha512: SVveQ5hGwfPv8N5APUskwLOzwrTUVA+z8wuFLsSLCr1/vVnFJJ0VQOGMUctg1NbqhAuQ795MJmuuAaAwR8dSOQ==
+	Location: /api/4.0/deliveryservices?id=6
+	Permissions-Policy: interest-cohort=()
+	Set-Cookie: mojolicious=...; Path=/; Expires=Mon, 07 Jun 2021 23:37:37 GMT; Max-Age=3600; HttpOnly
+	Vary: Accept-Encoding
 	X-Server-Name: traffic_ops_golang/
-	Date: Mon, 19 Nov 2018 19:45:49 GMT
-	Content-Length: 1404
+	Date: Mon, 07 Jun 2021 22:37:37 GMT
+	Content-Length: 903
 
 	{ "alerts": [
 		{
-			"text": "Deliveryservice creation was successful.",
+			"text": "tlsVersions has no effect on 'HTTP' Delivery Services",
+			"level": "warning"
+		},
+		{
+			"text": "Delivery Service creation was successful",
 			"level": "success"
 		}
 	],
-	"response": [
-		{
-			"active": false,
-			"anonymousBlockingEnabled": false,
-			"cacheurl": null,
-			"ccrDnsTtl": null,
-			"cdnId": 2,
-			"cdnName": "CDN-in-a-Box",
-			"checkPath": null,
-			"displayName": "test",
-			"dnsBypassCname": null,
-			"dnsBypassIp": null,
-			"dnsBypassIp6": null,
-			"dnsBypassTtl": null,
-			"dscp": 0,
-			"edgeHeaderRewrite": null,
-			"firstHeaderRewrite": null,
-			"geoLimit": 0,
-			"geoLimitCountries": null,
-			"geoLimitRedirectURL": null,
-			"geoProvider": 0,
-			"globalMaxMbps": null,
-			"globalMaxTps": null,
-			"httpBypassFqdn": null,
-			"id": 2,
-			"infoUrl": null,
-			"initialDispersion": 1,
-			"innerHeaderRewrite": null,
-			"ipv6RoutingEnabled": false,
-			"lastHeaderRewrite": null,
-			"lastUpdated": "2018-11-19 19:45:49+00",
-			"logsEnabled": true,
-			"longDesc": "A Delivery Service created expressly for API documentation examples",
-			"matchList": [
-				{
-					"type": "HOST_REGEXP",
-					"setNumber": 0,
-					"pattern": ".*\\.test\\..*"
-				}
-			],
-			"maxDnsAnswers": null,
-			"maxOriginConnections": 0,
-			"maxRequestHeaderBytes": 131072,
-			"midHeaderRewrite": null,
-			"missLat": -1,
-			"missLong": -1,
-			"multiSiteOrigin": false,
-			"originShield": null,
-			"orgServerFqdn": "http://origin.infra.ciab.test",
-			"profileDescription": null,
-			"profileId": null,
-			"profileName": null,
-			"protocol": 0,
-			"qstringIgnore": 0,
-			"rangeRequestHandling": 0,
-			"regexRemap": null,
-			"regionalGeoBlocking": false,
-			"remapText": null,
-			"routingName": "test",
-			"serviceCategory": null,
-			"signed": false,
-			"sslKeyVersion": null,
-			"tenantId": 1,
-			"type": "HTTP",
-			"typeId": 1,
-			"xmlId": "test",
-			"exampleURLs": [
-				"http://test.test.mycdn.ciab.test"
-			],
-			"deepCachingType": "NEVER",
-			"signingAlgorithm": null,
-			"tenant": "root",
-			"ecsEnabled": true,
-			"rangeSliceBlockSize": null,
-			"topology": null
-		}
-	]}
+	"response": [{
+		"active": false,
+		"anonymousBlockingEnabled": false,
+		"ccrDnsTtl": null,
+		"cdnId": 2,
+		"cdnName": null,
+		"checkPath": null,
+		"consistentHashQueryParams": [],
+		"consistentHashRegex": null,
+		"deepCachingType": "NEVER",
+		"displayName": "test",
+		"dnsBypassCname": null,
+		"dnsBypassIp": null,
+		"dnsBypassIp6": null,
+		"dnsBypassTtl": null,
+		"dscp": 0,
+		"ecsEnabled": true,
+		"edgeHeaderRewrite": null,
+		"exampleURLs": [
+			"http://test.test.mycdn.ciab.test"
+		],
+		"firstHeaderRewrite": null,
+		"fqPacingRate": null,
+		"geoLimit": 0,
+		"geoLimitCountries": null,
+		"geoLimitRedirectURL": null,
+		"geoProvider": 0,
+		"globalMaxMbps": null,
+		"globalMaxTps": null,
+		"httpBypassFqdn": null,
+		"id": 6,
+		"infoUrl": null,
+		"initialDispersion": 1,
+		"innerHeaderRewrite": null,
+		"ipv6RoutingEnabled": false,
+		"lastHeaderRewrite": null,
+		"lastUpdated": "2021-06-07T22:37:37.187822Z",
+		"logsEnabled": true,
+		"longDesc": "A Delivery Service created expressly for API documentation examples",
+		"matchList": [
+			{
+				"type": "HOST_REGEXP",
+				"setNumber": 0,
+				"pattern": ".*\\.test\\..*"
+			}
+		],
+		"maxDnsAnswers": null,
+		"maxOriginConnections": 0,
+		"maxRequestHeaderBytes": 131072,
+		"midHeaderRewrite": null,
+		"missLat": 0,
+		"missLong": 0,
+		"multiSiteOrigin": false,
+		"originShield": null,
+		"orgServerFqdn": "http://origin.infra.ciab.test",
+		"profileDescription": null,
+		"profileId": null,
+		"profileName": null,
+		"protocol": 0,
+		"qstringIgnore": 0,
+		"rangeRequestHandling": 0,
+		"rangeSliceBlockSize": null,
+		"regexRemap": null,
+		"regionalGeoBlocking": false,
+		"remapText": null,
+		"routingName": "test",
+		"serviceCategory": null,
+		"signed": false,
+		"signingAlgorithm": null,
+		"sslKeyVersion": null,
+		"tenant": "root",
+		"tenantId": 1,
+		"tlsVersions": [
+			"1.2",
+			"1.3"
+		],
+		"topology": null,
+		"trResponseHeaders": null,
+		"trRequestHeaders": null,
+		"type": "HTTP",
+		"typeId": 1,
+		"xmlId": "test"
+	}]}
+
 
 .. [#tenancy] Only those :term:`Delivery Services` assigned to :term:`Tenants` that are the requesting user's :term:`Tenant` or children thereof will appear in the output of a ``GET`` request, and the same constraints are placed on the allowed values of the ``tenantId`` field of a ``POST`` request to create a new :term:`Delivery Service`
 .. [#geoLimit] These fields must be defined if and only if ``geoLimit`` is non-zero
diff --git a/docs/source/api/v4/deliveryservices_id.rst b/docs/source/api/v4/deliveryservices_id.rst
index 8cfe335..259792d 100644
--- a/docs/source/api/v4/deliveryservices_id.rst
+++ b/docs/source/api/v4/deliveryservices_id.rst
@@ -25,7 +25,7 @@ Allows users to edit an existing :term:`Delivery Service`.
 
 :Auth. Required: Yes
 :Roles Required: "admin" or "operations"\ [#tenancy]_
-:Response Type:  **NOT PRESENT** - Despite returning a ``200 OK`` response (rather than e.g. a ``204 NO CONTENT`` response), this endpoint does **not** return a representation of the modified resource in its payload, and instead returns nothing - not even a success message.
+:Response Type:  Array (should always have a length of exactly one on success)
 
 Request Structure
 -----------------
@@ -81,18 +81,22 @@ Request Structure
 :remapText:                 :ref:`ds-raw-remap`
 :routingName:               The :ref:`ds-routing-name` of this :term:`Delivery Service`
 
-		.. note:: If the Delivery Service has SSL Keys, then routingName is not allowed to change as that would invalidate the SSL Key
+		.. note:: If the Delivery Service has SSL Keys, then ``routingName`` is not allowed to change as that would invalidate the SSL Key
 
-:signed:                    ``true`` if  and only if ``signingAlgorithm`` is not ``null``, ``false`` otherwise
-:signingAlgorithm:          Either a :ref:`ds-signing-algorithm` or ``null`` to indicate URL/URI signing is not implemented on this :term:`Delivery Service`
-:rangeSliceBlockSize:      An integer that defines the byte block size for the ATS Slice Plugin. It can only and must be set if ``rangeRequestHandling`` is set to 3. It can only be between (inclusive) 262144 (256KB) - 33554432 (32MB).
-:sslKeyVersion:             This integer indicates the :ref:`ds-ssl-key-version`
-:tenantId:                  The integral, unique identifier of the :ref:`ds-tenant` who owns this :term:`Delivery Service`
-:topology:                  The unique name of the :term:`Topology` that this :term:`Delivery Service` is assigned to
-:trRequestHeaders:          If defined, this defines the :ref:`ds-tr-req-headers` used by Traffic Router for this :term:`Delivery Service`
-:trResponseHeaders:         If defined, this defines the :ref:`ds-tr-resp-headers` used by Traffic Router for this :term:`Delivery Service`
-:typeId:                    The integral, unique identifier of the :ref:`ds-types` of this :term:`Delivery Service`
-:xmlId:                     This :term:`Delivery Service`'s :ref:`ds-xmlid`
+:signed:              ``true`` if  and only if ``signingAlgorithm`` is not ``null``, ``false`` otherwise
+:signingAlgorithm:    Either a :ref:`ds-signing-algorithm` or ``null`` to indicate URL/URI signing is not implemented on this :term:`Delivery Service`
+:rangeSliceBlockSize: An integer that defines the byte block size for the ATS Slice Plugin. It can only and must be set if ``rangeRequestHandling`` is set to 3. It can only be between (inclusive) 262144 (256KB) - 33554432 (32MB).
+:sslKeyVersion:       This integer indicates the :ref:`ds-ssl-key-version`
+:tenantId:            The integral, unique identifier of the :ref:`ds-tenant` who owns this :term:`Delivery Service`
+:tlsVersions:         An array of explicitly supported :ref:`ds-tls-versions`
+
+	.. versionadded:: 4.0
+
+:topology:          The unique name of the :term:`Topology` that this :term:`Delivery Service` is assigned to
+:trRequestHeaders:  If defined, this defines the :ref:`ds-tr-req-headers` used by Traffic Router for this :term:`Delivery Service`
+:trResponseHeaders: If defined, this defines the :ref:`ds-tr-resp-headers` used by Traffic Router for this :term:`Delivery Service`
+:typeId:            The integral, unique identifier of the :ref:`ds-types` of this :term:`Delivery Service`
+:xmlId:             This :term:`Delivery Service`'s :ref:`ds-xmlid`
 
 	.. note:: While this field **must** be present, it is **not** allowed to change; this must be the same as the ``xml_id`` the :term:`Delivery Service` already has. This should almost never be different from the :term:`Delivery Service`'s ``displayName``.
 
@@ -100,49 +104,169 @@ Request Structure
 .. code-block:: http
 	:caption: Request Example
 
-	PUT /api/4.0/deliveryservices/1 HTTP/1.1
+	PUT /api/4.0/deliveryservices/6 HTTP/1.1
 	Host: trafficops.infra.ciab.test
-	User-Agent: curl/7.47.0
+	User-Agent: python-requests/2.24.0
+	Accept-Encoding: gzip, deflate
 	Accept: */*
+	Connection: keep-alive
 	Cookie: mojolicious=...
-	Content-Length: 761
+	Content-Length: 1585
 	Content-Type: application/json
 
 	{
-		"active": true,
+		"active": false,
 		"anonymousBlockingEnabled": false,
+		"ccrDnsTtl": null,
 		"cdnId": 2,
-		"cdnName": "CDN-in-a-Box",
+		"checkPath": null,
+		"consistentHashRegex": null,
+		"consistentHashQueryParams": [],
 		"deepCachingType": "NEVER",
-		"displayName": "demo",
+		"displayName": "test",
+		"dnsBypassCname": null,
+		"dnsBypassIp": null,
+		"dnsBypassIp6": null,
+		"dnsBypassTtl": null,
 		"dscp": 0,
 		"ecsEnabled": true,
+		"edgeHeaderRewrite": null,
+		"firstHeaderRewrite": null,
+		"fqPacingRate": null,
 		"geoLimit": 0,
+		"geoLimitCountries": null,
+		"geoLimitRedirectUrl": null,
 		"geoProvider": 0,
+		"globalMaxMbps": null,
+		"globalMaxTps": null,
+		"httpBypassFqdn": null,
+		"infoUrl": null,
 		"initialDispersion": 1,
+		"innerHeaderRewrite": null,
 		"ipv6RoutingEnabled": false,
-		"lastUpdated": "2018-11-14 18:21:17+00",
+		"lastHeaderRewrite": null,
 		"logsEnabled": true,
 		"longDesc": "A Delivery Service created expressly for API documentation examples",
-		"missLat": -1,
-		"missLong": -1,
+		"longDesc1": null,
+		"longDesc2": null,
+		"maxDnsAnswers": null,
+		"missLat": 0,
+		"missLong": 0,
+		"maxOriginConnections": 0,
+		"maxRequestHeaderBytes": 131072,
+		"midHeaderRewrite": null,
 		"multiSiteOrigin": false,
 		"orgServerFqdn": "http://origin.infra.ciab.test",
+		"originShield": null,
+		"profileId": null,
 		"protocol": 0,
 		"qstringIgnore": 0,
 		"rangeRequestHandling": 0,
+		"regexRemap": null,
 		"regionalGeoBlocking": false,
-		"routingName": "video",
+		"routingName": "test",
+		"serviceCategory": null,
 		"signed": false,
+		"signingAlgorithm": null,
+		"rangeSliceBlockSize": null,
+		"sslKeyVersion": null,
 		"tenant": "root",
 		"tenantId": 1,
+		"tlsVersions": null,
+		"topology": null,
+		"trRequestHeaders": null,
+		"trResponseHeaders": null,
+		"type": "HTTP",
 		"typeId": 1,
-		"xmlId": "demo1"
+		"xmlId": "test"
 	}
 
 
 Response Structure
 ------------------
+:active:                   A boolean that defines :ref:`ds-active`.
+:anonymousBlockingEnabled: A boolean that defines :ref:`ds-anonymous-blocking`
+:ccrDnsTtl:                 The :ref:`ds-dns-ttl` - named "ccrDnsTtl" for legacy reasons
+:cdnId:                     The integral, unique identifier of the :ref:`ds-cdn` to which the :term:`Delivery Service` belongs
+:cdnName:                   Name of the :ref:`ds-cdn` to which the :term:`Delivery Service` belongs
+:checkPath:                 A :ref:`ds-check-path`
+:consistentHashRegex:       A :ref:`ds-consistent-hashing-regex`
+:consistentHashQueryParams: An array of :ref:`ds-consistent-hashing-qparams`
+:deepCachingType:           The :ref:`ds-deep-caching` setting for this :term:`Delivery Service`
+:displayName:               The :ref:`ds-display-name`
+:dnsBypassCname:            A :ref:`ds-dns-bypass-cname`
+:dnsBypassIp:               A :ref:`ds-dns-bypass-ip`
+:dnsBypassIp6:              A :ref:`ds-dns-bypass-ipv6`
+:dnsBypassTtl:              The :ref:`ds-dns-bypass-ttl`
+:dscp:                      A :ref:`ds-dscp` to be used within the :term:`Delivery Service`
+:ecsEnabled:                A boolean that defines the :ref:`ds-ecs` setting on this :term:`Delivery Service`
+:edgeHeaderRewrite:         A set of :ref:`ds-edge-header-rw-rules`
+:exampleURLs:               An array of :ref:`ds-example-urls`
+:firstHeaderRewrite:        A set of :ref:`ds-first-header-rw-rules`
+:fqPacingRate:              The :ref:`ds-fqpr`
+:geoLimit:                  An integer that defines the :ref:`ds-geo-limit`
+:geoLimitCountries:         A string containing a comma-separated list defining the :ref:`ds-geo-limit-countries`
+:geoLimitRedirectUrl:       A :ref:`ds-geo-limit-redirect-url`
+:geoProvider:               The :ref:`ds-geo-provider`
+:globalMaxMbps:             The :ref:`ds-global-max-mbps`
+:globalMaxTps:              The :ref:`ds-global-max-tps`
+:httpBypassFqdn:            A :ref:`ds-http-bypass-fqdn`
+:id:                        An integral, unique identifier for this :term:`Delivery Service`
+:infoUrl:                   An :ref:`ds-info-url`
+:initialDispersion:         The :ref:`ds-initial-dispersion`
+:innerHeaderRewrite:        A set of :ref:`ds-inner-header-rw-rules`
+:ipv6RoutingEnabled:        A boolean that defines the :ref:`ds-ipv6-routing` setting on this :term:`Delivery Service`
+:lastHeaderRewrite:         A set of :ref:`ds-last-header-rw-rules`
+:lastUpdated:               The date and time at which this :term:`Delivery Service` was last updated, in :rfc:3339 format
+
+	.. versionchanged:: 4.0
+		Prior to API version 4.0, this property used :ref:`non-rfc-datetime`.
+
+:logsEnabled: A boolean that defines the :ref:`ds-logs-enabled` setting on this :term:`Delivery Service`
+:longDesc:    The :ref:`ds-longdesc` of this :term:`Delivery Service`
+:longDesc1:   The :ref:`ds-longdesc2` of this :term:`Delivery Service`
+:longDesc2:   The :ref:`ds-longdesc3` of this :term:`Delivery Service`
+:matchList:   The :term:`Delivery Service`'s :ref:`ds-matchlist`
+
+	:pattern:   A regular expression - the use of this pattern is dependent on the ``type`` field (backslashes are escaped)
+	:setNumber: An integer that provides explicit ordering of :ref:`ds-matchlist` items - this is used as a priority ranking by Traffic Router, and is not guaranteed to correspond to the ordering of items in the array.
+	:type:      The type of match performed using ``pattern``.
+
+:maxDnsAnswers:         The :ref:`ds-max-dns-answers` allowed for this :term:`Delivery Service`
+:maxOriginConnections:  The :ref:`ds-max-origin-connections`
+:maxRequestHeaderBytes: The :ref:`ds-max-request-header-bytes`
+:midHeaderRewrite:      A set of :ref:`ds-mid-header-rw-rules`
+:missLat:               The :ref:`ds-geo-miss-default-latitude` used by this :term:`Delivery Service`
+:missLong:              The :ref:`ds-geo-miss-default-longitude` used by this :term:`Delivery Service`
+:multiSiteOrigin:       A boolean that defines the use of :ref:`ds-multi-site-origin` by this :term:`Delivery Service`
+:orgServerFqdn:         The :ref:`ds-origin-url`
+:originShield:          A :ref:`ds-origin-shield` string
+:profileDescription:    The :ref:`profile-description` of the :ref:`ds-profile` with which this :term:`Delivery Service` is associated
+:profileId:             The :ref:`profile-id` of the :ref:`ds-profile` with which this :term:`Delivery Service` is associated
+:profileName:           The :ref:`profile-name` of the :ref:`ds-profile` with which this :term:`Delivery Service` is associated
+:protocol:              An integral, unique identifier that corresponds to the :ref:`ds-protocol` used by this :term:`Delivery Service`
+:qstringIgnore:         An integral, unique identifier that corresponds to the :ref:`ds-qstring-handling` setting on this :term:`Delivery Service`
+:rangeRequestHandling:  An integral, unique identifier that corresponds to the :ref:`ds-range-request-handling` setting on this :term:`Delivery Service`
+:regexRemap:            A :ref:`ds-regex-remap`
+:regionalGeoBlocking:   A boolean defining the :ref:`ds-regionalgeo` setting on this :term:`Delivery Service`
+:remapText:             :ref:`ds-raw-remap`
+:serviceCategory:       The name of the :ref:`ds-service-category` with which the :term:`Delivery Service` is associated
+:signed:                ``true`` if  and only if ``signingAlgorithm`` is not ``null``, ``false`` otherwise
+:signingAlgorithm:      Either a :ref:`ds-signing-algorithm` or ``null`` to indicate URL/URI signing is not implemented on this :term:`Delivery Service`
+:rangeSliceBlockSize:   An integer that defines the byte block size for the ATS Slice Plugin. It can only and must be set if ``rangeRequestHandling`` is set to 3.
+:sslKeyVersion:         This integer indicates the :ref:`ds-ssl-key-version`
+:tenantId:              The integral, unique identifier of the :ref:`ds-tenant` who owns this :term:`Delivery Service`
+:tlsVersions:           An array of explicitly supported :ref:`ds-tls-versions`
+
+	.. versionadded:: 4.0
+
+:topology:          The unique name of the :term:`Topology` that this :term:`Delivery Service` is assigned to
+:trRequestHeaders:  If defined, this defines the :ref:`ds-tr-req-headers` used by Traffic Router for this :term:`Delivery Service`
+:trResponseHeaders: If defined, this defines the :ref:`ds-tr-resp-headers` used by Traffic Router for this :term:`Delivery Service`
+:type:              The :ref:`ds-types` of this :term:`Delivery Service`
+:typeId:            The integral, unique identifier of the :ref:`ds-types` of this :term:`Delivery Service`
+:xmlId:             This :term:`Delivery Service`'s :ref:`ds-xmlid`
+
 .. code-block:: http
 	:caption: Response Example
 
@@ -151,12 +275,102 @@ Response Structure
 	Access-Control-Allow-Headers: Origin, X-Requested-With, Content-Type, Accept, Set-Cookie, Cookie
 	Access-Control-Allow-Methods: POST,GET,OPTIONS,PUT,DELETE
 	Access-Control-Allow-Origin: *
-	Set-Cookie: mojolicious=...; Path=/; Expires=Mon, 18 Nov 2019 17:40:54 GMT; Max-Age=3600; HttpOnly
-	Whole-Content-Sha512: z4PhNX7vuL3xVChQ1m2AB9Yg5AULVxXcg/SpIdNs6c5H0NE8XYXysP+DGNKHfuwvY7kxvUdBeoGlODJ6+SfaPg==
+	Content-Encoding: gzip
+	Content-Type: application/json
+	Permissions-Policy: interest-cohort=()
+	Set-Cookie: mojolicious=...; Path=/; Expires=Tue, 08 Jun 2021 00:34:04 GMT; Max-Age=3600; HttpOnly
+	Vary: Accept-Encoding
+	Whole-Content-Sha512: tTncbRoJR+pyykVbEc6nWyoFnhlJzsbge9hVZfw+WK28rzSGECZ/Q4zXTQtFjHWY5G+0Rk4w9GKrSFK3k+u5Ng==
 	X-Server-Name: traffic_ops_golang/
-	Date: Tue, 20 Nov 2018 14:12:25 GMT
-	Content-Length: 0
-	Content-Type: text/plain; charset=utf-8
+	Date: Mon, 07 Jun 2021 23:34:04 GMT
+	Content-Length: 840
+
+	{ "alerts": [
+		{
+			"text": "Delivery Service update was successful",
+			"level": "success"
+		}
+	],
+	"response": [{
+		"active": false,
+		"anonymousBlockingEnabled": false,
+		"ccrDnsTtl": null,
+		"cdnId": 2,
+		"cdnName": null,
+		"checkPath": null,
+		"consistentHashQueryParams": [],
+		"consistentHashRegex": null,
+		"deepCachingType": "NEVER",
+		"displayName": "test",
+		"dnsBypassCname": null,
+		"dnsBypassIp": null,
+		"dnsBypassIp6": null,
+		"dnsBypassTtl": null,
+		"dscp": 0,
+		"ecsEnabled": true,
+		"edgeHeaderRewrite": null,
+		"exampleURLs": null,
+		"firstHeaderRewrite": null,
+		"fqPacingRate": null,
+		"geoLimit": 0,
+		"geoLimitCountries": null,
+		"geoLimitRedirectURL": null,
+		"geoProvider": 0,
+		"globalMaxMbps": null,
+		"globalMaxTps": null,
+		"httpBypassFqdn": null,
+		"id": 6,
+		"infoUrl": null,
+		"initialDispersion": 1,
+		"innerHeaderRewrite": null,
+		"ipv6RoutingEnabled": false,
+		"lastHeaderRewrite": null,
+		"lastUpdated": "2021-06-07T23:34:04.831215Z",
+		"logsEnabled": true,
+		"longDesc": "A Delivery Service created expressly for API documentation examples",
+		"longDesc1": null,
+		"longDesc2": null,
+		"matchList": [
+			{
+				"type": "HOST_REGEXP",
+				"setNumber": 0,
+				"pattern": ".*\\.test\\..*"
+			}
+		],
+		"maxDnsAnswers": null,
+		"maxOriginConnections": 0,
+		"maxRequestHeaderBytes": 131072,
+		"midHeaderRewrite": null,
+		"missLat": 0,
+		"missLong": 0,
+		"multiSiteOrigin": false,
+		"originShield": null,
+		"orgServerFqdn": "http://origin.infra.ciab.test",
+		"profileDescription": null,
+		"profileId": null,
+		"profileName": null,
+		"protocol": 0,
+		"qstringIgnore": 0,
+		"rangeRequestHandling": 0,
+		"rangeSliceBlockSize": null,
+		"regexRemap": null,
+		"regionalGeoBlocking": false,
+		"remapText": null,
+		"routingName": "test",
+		"serviceCategory": null,
+		"signed": false,
+		"signingAlgorithm": null,
+		"sslKeyVersion": null,
+		"tenant": "root",
+		"tenantId": 1,
+		"tlsVersions": null,
+		"topology": null,
+		"trResponseHeaders": null,
+		"trRequestHeaders": null,
+		"type": "HTTP",
+		"typeId": 1,
+		"xmlId": "test"
+	}]}
 
 
 ``DELETE``
diff --git a/docs/source/api/v4/deliveryservices_id_safe.rst b/docs/source/api/v4/deliveryservices_id_safe.rst
index 1e56b88..bf7c8dc 100644
--- a/docs/source/api/v4/deliveryservices_id_safe.rst
+++ b/docs/source/api/v4/deliveryservices_id_safe.rst
@@ -51,6 +51,7 @@ Request Structure
 	Connection: keep-alive
 	Cookie: mojolicious=...
 	Content-Length: 132
+	Content-Type: application/json
 
 	{
 		"displayName": "test",
@@ -93,7 +94,11 @@ Response Structure
 :innerHeaderRewrite:        A set of :ref:`ds-inner-header-rw-rules`
 :ipv6RoutingEnabled:        A boolean that defines the :ref:`ds-ipv6-routing` setting on this :term:`Delivery Service`
 :lastHeaderRewrite:         A set of :ref:`ds-last-header-rw-rules`
-:lastUpdated:               The date and time at which this :term:`Delivery Service` was last updated, in :ref:`non-rfc-datetime`
+:lastUpdated:               The date and time at which this :term:`Delivery Service` was last updated, in :rfc:3339 format
+
+	.. versionchanged:: 4.0
+		Prior to API version 4.0, this property used :ref:`non-rfc-datetime`.
+
 :logsEnabled:               A boolean that defines the :ref:`ds-logs-enabled` setting on this :term:`Delivery Service`
 :longDesc:                  The :ref:`ds-longdesc` of this :term:`Delivery Service`
 :matchList:                 The :term:`Delivery Service`'s :ref:`ds-matchlist`
@@ -124,6 +129,10 @@ Response Structure
 :rangeSliceBlockSize: An integer that defines the byte block size for the ATS Slice Plugin. It can only and must be set if ``rangeRequestHandling`` is set to 3.
 :sslKeyVersion:        This integer indicates the :ref:`ds-ssl-key-version`
 :tenantId:             The integral, unique identifier of the :ref:`ds-tenant` who owns this :term:`Delivery Service`
+:tlsVersions:           A list of explicitly supported :ref:`ds-tls-versions`
+
+	.. versionadded:: 4.0
+
 :topology:             The unique name of the :term:`Topology` that this :term:`Delivery Service` is assigned to
 :trRequestHeaders:     If defined, this defines the :ref:`ds-tr-req-headers` used by Traffic Router for this :term:`Delivery Service`
 :trResponseHeaders:    If defined, this defines the :ref:`ds-tr-resp-headers` used by Traffic Router for this :term:`Delivery Service`
@@ -137,102 +146,98 @@ Response Structure
 	HTTP/1.1 200 OK
 	Content-Encoding: gzip
 	Content-Type: application/json
-	Set-Cookie: mojolicious=...; Path=/; Expires=Mon, 10 Feb 2020 16:33:03 GMT; Max-Age=3600; HttpOnly
+	Permissions-Policy: interest-cohort=()
+	Set-Cookie: mojolicious=...; Path=/; Expires=Tue, 08 Jun 2021 00:53:26 GMT; Max-Age=3600; HttpOnly
+	Vary: Accept-Encoding
+	Whole-Content-Sha512: Ys/SfWWijsXCNXEqZ84oldfyXTgMe8UE/wWb53VU39OH7kWOXF1BH5Hg7Y40nCgXoWEqcaBq5+WCZg0bYuJdAA==
 	X-Server-Name: traffic_ops_golang/
-	Date: Mon, 10 Feb 2020 15:33:03 GMT
-	Content-Length: 853
-
-	{ "alerts": [
-		{
-			"text": "Delivery Service safe update successful.",
-			"level": "success"
-		}
-	],
-	"response": [
-		{
-			"active": true,
-			"anonymousBlockingEnabled": false,
-			"cacheurl": null,
-			"ccrDnsTtl": null,
-			"cdnId": 2,
-			"cdnName": "CDN-in-a-Box",
-			"checkPath": null,
-			"displayName": "test",
-			"dnsBypassCname": null,
-			"dnsBypassIp": null,
-			"dnsBypassIp6": null,
-			"dnsBypassTtl": null,
-			"dscp": 0,
-			"edgeHeaderRewrite": null,
-			"firstHeaderRewrite": null,
-			"geoLimit": 0,
-			"geoLimitCountries": null,
-			"geoLimitRedirectURL": null,
-			"geoProvider": 0,
-			"globalMaxMbps": null,
-			"globalMaxTps": null,
-			"httpBypassFqdn": null,
-			"id": 1,
-			"infoUrl": "this is not even a real URL",
-			"initialDispersion": 1,
-			"innerHeaderRewrite": null,
-			"ipv6RoutingEnabled": true,
-			"lastHeaderRewrite": null,
-			"lastUpdated": "2020-02-10 15:33:03+00",
-			"logsEnabled": true,
-			"longDesc": "this is a description of the delivery service",
-			"matchList": [
-				{
-					"type": "HOST_REGEXP",
-					"setNumber": 0,
-					"pattern": ".*\\.demo1\\..*"
-				}
-			],
-			"maxDnsAnswers": null,
-			"midHeaderRewrite": null,
-			"missLat": 42,
-			"missLong": -88,
-			"multiSiteOrigin": false,
-			"originShield": null,
-			"orgServerFqdn": "http://origin.infra.ciab.test",
-			"profileDescription": null,
-			"profileId": null,
-			"profileName": null,
-			"protocol": 2,
-			"qstringIgnore": 0,
-			"rangeRequestHandling": 0,
-			"regexRemap": null,
-			"regionalGeoBlocking": false,
-			"remapText": null,
-			"routingName": "video",
-			"signed": false,
-			"sslKeyVersion": 1,
-			"tenantId": 1,
-			"type": "HTTP",
-			"typeId": 1,
-			"xmlId": "demo1",
-			"exampleURLs": [
-				"http://video.demo1.mycdn.ciab.test",
-				"https://video.demo1.mycdn.ciab.test"
-			],
-			"deepCachingType": "NEVER",
-			"fqPacingRate": null,
-			"signingAlgorithm": null,
-			"tenant": "root",
-			"trResponseHeaders": null,
-			"trRequestHeaders": null,
-			"consistentHashRegex": null,
-			"consistentHashQueryParams": [
-				"abc",
-				"pdq",
-				"xxx",
-				"zyx"
-			],
-			"maxOriginConnections": 0,
-			"ecsEnabled": false,
-			"rangeSliceBlockSize": null,
-			"topology": null
-		}
+	Date: Mon, 07 Jun 2021 23:53:26 GMT
+	Content-Length: 903
+
+	{ "alerts": [{
+		"text": "Delivery Service safe update successful.",
+		"level": "success"
+	}],
+	"response": [{
+		"active": true,
+		"anonymousBlockingEnabled": false,
+		"ccrDnsTtl": null,
+		"cdnId": 2,
+		"cdnName": "CDN-in-a-Box",
+		"checkPath": null,
+		"consistentHashQueryParams": [],
+		"consistentHashRegex": null,
+		"deepCachingType": "NEVER",
+		"displayName": "test",
+		"dnsBypassCname": null,
+		"dnsBypassIp": null,
+		"dnsBypassIp6": null,
+		"dnsBypassTtl": null,
+		"dscp": 0,
+		"ecsEnabled": false,
+		"edgeHeaderRewrite": null,
+		"exampleURLs": [
+			"http://video.demo2.mycdn.ciab.test",
+			"https://video.demo2.mycdn.ciab.test"
+		],
+		"firstHeaderRewrite": null,
+		"fqPacingRate": null,
+		"geoLimit": 0,
+		"geoLimitCountries": null,
+		"geoLimitRedirectURL": null,
+		"geoProvider": 0,
+		"globalMaxMbps": null,
+		"globalMaxTps": null,
+		"httpBypassFqdn": null,
+		"id": 1,
+		"infoUrl": "this is not even a real URL",
+		"initialDispersion": 1,
+		"innerHeaderRewrite": null,
+		"ipv6RoutingEnabled": true,
+		"lastHeaderRewrite": null,
+		"lastUpdated": "2021-06-07T23:53:26.139899Z",
+		"logsEnabled": true,
+		"longDesc": "this is a description of the delivery service",
+		"matchList": [
+			{
+				"type": "HOST_REGEXP",
+				"setNumber": 0,
+				"pattern": ".*\\.demo2\\..*"
+			}
+		],
+		"maxDnsAnswers": null,
+		"maxOriginConnections": 0,
+		"maxRequestHeaderBytes": 0,
+		"midHeaderRewrite": null,
+		"missLat": 42,
+		"missLong": -88,
+		"multiSiteOrigin": true,
+		"originShield": null,
+		"orgServerFqdn": "http://origin.infra.ciab.test",
+		"profileDescription": null,
+		"profileId": null,
+		"profileName": null,
+		"protocol": 2,
+		"qstringIgnore": 0,
+		"rangeRequestHandling": 0,
+		"rangeSliceBlockSize": null,
+		"regexRemap": null,
+		"regionalGeoBlocking": false,
+		"remapText": null,
+		"routingName": "video",
+		"serviceCategory": null,
+		"signed": false,
+		"signingAlgorithm": null,
+		"sslKeyVersion": null,
+		"tenant": "root",
+		"tenantId": 1,
+		"tlsVersions": null,
+		"topology": "demo1-top",
+		"trResponseHeaders": null,
+		"trRequestHeaders": null,
+		"type": "DNS",
+		"typeId": 5,
+		"xmlId": "demo2"
 	]}
 
 .. [#tenancy] Only those :term:`Delivery Services` assigned to :term:`Tenants` that are the requesting user's :term:`Tenant` or children thereof may be modified with this endpoint.
diff --git a/docs/source/api/v4/servers_id_deliveryservices.rst b/docs/source/api/v4/servers_id_deliveryservices.rst
index 529b3cb..7add8a8 100644
--- a/docs/source/api/v4/servers_id_deliveryservices.rst
+++ b/docs/source/api/v4/servers_id_deliveryservices.rst
@@ -59,10 +59,12 @@ Request Structure
 .. code-block:: http
 	:caption: Request Example
 
-	GET /api/4.0/servers/9/deliveryservices HTTP/1.1
+	GET /api/4.0/servers/10/deliveryservices HTTP/1.1
 	Host: trafficops.infra.ciab.test
-	User-Agent: curl/7.47.0
+	User-Agent: python-requests/2.24.0
+	Accept-Encoding: gzip, deflate
 	Accept: */*
+	Connection: keep-alive
 	Cookie: mojolicious=...
 
 Response Structure
@@ -100,10 +102,14 @@ Response Structure
 :innerHeaderRewrite:        A set of :ref:`ds-inner-header-rw-rules`
 :ipv6RoutingEnabled:        A boolean that defines the :ref:`ds-ipv6-routing` setting on this :term:`Delivery Service`
 :lastHeaderRewrite:         A set of :ref:`ds-last-header-rw-rules`
-:lastUpdated:               The date and time at which this :term:`Delivery Service` was last updated, in :ref:`non-rfc-datetime`
-:logsEnabled:               A boolean that defines the :ref:`ds-logs-enabled` setting on this :term:`Delivery Service`
-:longDesc:                  The :ref:`ds-longdesc` of this :term:`Delivery Service`
-:matchList:                 The :term:`Delivery Service`'s :ref:`ds-matchlist`
+:lastUpdated:               The date and time at which this :term:`Delivery Service` was last updated, in :rfc:3339 format
+
+	.. versionchanged:: 4.0
+		Prior to API version 4.0, this property used :ref:`non-rfc-datetime`.
+
+:logsEnabled: A boolean that defines the :ref:`ds-logs-enabled` setting on this :term:`Delivery Service`
+:longDesc:    The :ref:`ds-longdesc` of this :term:`Delivery Service`
+:matchList:   The :term:`Delivery Service`'s :ref:`ds-matchlist`
 
 	:pattern:   A regular expression - the use of this pattern is dependent on the ``type`` field (backslashes are escaped)
 	:setNumber: An integer that provides explicit ordering of :ref:`ds-matchlist` items - this is used as a priority ranking by Traffic Router, and is not guaranteed to correspond to the ordering of items in the array.
@@ -131,12 +137,16 @@ Response Structure
 :rangeSliceBlockSize: An integer that defines the byte block size for the ATS Slice Plugin. It can only and must be set if ``rangeRequestHandling`` is set to 3.
 :sslKeyVersion:        This integer indicates the :ref:`ds-ssl-key-version`
 :tenantId:             The integral, unique identifier of the :ref:`ds-tenant` who owns this :term:`Delivery Service`
-:topology:             The unique name of the :term:`Topology` that this :term:`Delivery Service` is assigned to
-:trRequestHeaders:     If defined, this defines the :ref:`ds-tr-req-headers` used by Traffic Router for this :term:`Delivery Service`
-:trResponseHeaders:    If defined, this defines the :ref:`ds-tr-resp-headers` used by Traffic Router for this :term:`Delivery Service`
-:type:                 The :ref:`ds-types` of this :term:`Delivery Service`
-:typeId:               The integral, unique identifier of the :ref:`ds-types` of this :term:`Delivery Service`
-:xmlId:                This :term:`Delivery Service`'s :ref:`ds-xmlid`
+:tlsVersions:           A list of explicitly supported :ref:`ds-tls-versions`
+
+	.. versionadded:: 4.0
+
+:topology:          The unique name of the :term:`Topology` that this :term:`Delivery Service` is assigned to
+:trRequestHeaders:  If defined, this defines the :ref:`ds-tr-req-headers` used by Traffic Router for this :term:`Delivery Service`
+:trResponseHeaders: If defined, this defines the :ref:`ds-tr-resp-headers` used by Traffic Router for this :term:`Delivery Service`
+:type:              The :ref:`ds-types` of this :term:`Delivery Service`
+:typeId:            The integral, unique identifier of the :ref:`ds-types` of this :term:`Delivery Service`
+:xmlId:             This :term:`Delivery Service`'s :ref:`ds-xmlid`
 
 .. code-block:: http
 	:caption: Response Example
@@ -146,29 +156,39 @@ Response Structure
 	Access-Control-Allow-Headers: Origin, X-Requested-With, Content-Type, Accept, Set-Cookie, Cookie
 	Access-Control-Allow-Methods: POST,GET,OPTIONS,PUT,DELETE
 	Access-Control-Allow-Origin: *
+	Content-Encoding: gzip
 	Content-Type: application/json
-	Set-Cookie: mojolicious=...; Path=/; Expires=Mon, 18 Nov 2019 17:40:54 GMT; Max-Age=3600; HttpOnly
-	Whole-Content-Sha512: CFmtW41aoDezCYxtAXnS54dfFOD6jdxDJ2/LMpbBqnndy5kac7JQhdFAWF109sl95XVSUV85JHFzXZTw/mJabQ==
+	Permissions-Policy: interest-cohort=()
+	Set-Cookie: mojolicious=...; Path=/; Expires=Tue, 08 Jun 2021 01:15:07 GMT; Max-Age=3600; HttpOnly
+	Vary: Accept-Encoding
+	Whole-Content-Sha512: RO4tVfDdqx0rEU9BqlRmvsYXmVgVNkivqr6LhJlMulfR+1bLGivP8z93jy3N9bejcMdQwl1RwJojM3MbwgXcqA==
 	X-Server-Name: traffic_ops_golang/
-	Date: Mon, 10 Jun 2019 17:01:30 GMT
-	Content-Length: 1500
+	Date: Tue, 08 Jun 2021 00:15:07 GMT
+	Content-Length: 806
 
-	{ "response": [ {
-		"active": true,
+	{ "response": [{
+		"active": false,
 		"anonymousBlockingEnabled": false,
-		"cacheurl": null,
-		"ccrDnsTtl": null,
+		"ccrDnsTtl": 3600,
 		"cdnId": 2,
 		"cdnName": "CDN-in-a-Box",
 		"checkPath": null,
-		"displayName": "Demo 1",
+		"consistentHashQueryParams": [],
+		"consistentHashRegex": null,
+		"deepCachingType": "NEVER",
+		"displayName": "test",
 		"dnsBypassCname": null,
 		"dnsBypassIp": null,
 		"dnsBypassIp6": null,
 		"dnsBypassTtl": null,
 		"dscp": 0,
+		"ecsEnabled": false,
 		"edgeHeaderRewrite": null,
+		"exampleURLs": [
+			"http://cdn.test.mycdn.ciab.test"
+		],
 		"firstHeaderRewrite": null,
+		"fqPacingRate": null,
 		"geoLimit": 0,
 		"geoLimitCountries": null,
 		"geoLimitRedirectURL": null,
@@ -176,66 +196,55 @@ Response Structure
 		"globalMaxMbps": null,
 		"globalMaxTps": null,
 		"httpBypassFqdn": null,
-		"id": 1,
+		"id": 7,
 		"infoUrl": null,
 		"initialDispersion": 1,
 		"innerHeaderRewrite": null,
 		"ipv6RoutingEnabled": true,
 		"lastHeaderRewrite": null,
-		"lastUpdated": "2019-06-10 15:14:29+00",
-		"logsEnabled": true,
+		"lastUpdated": "2021-06-08T00:14:04.959292Z",
+		"logsEnabled": false,
 		"longDesc": "Apachecon North America 2018",
 		"matchList": [
 			{
 				"type": "HOST_REGEXP",
 				"setNumber": 0,
-				"pattern": ".*\\.demo1\\..*"
+				"pattern": ".*\\.test\\..*"
 			}
 		],
 		"maxDnsAnswers": null,
+		"maxOriginConnections": 0,
+		"maxRequestHeaderBytes": 0,
 		"midHeaderRewrite": null,
-		"missLat": 42,
-		"missLong": -88,
+		"missLat": 41.881944,
+		"missLong": -87.627778,
 		"multiSiteOrigin": false,
 		"originShield": null,
 		"orgServerFqdn": "http://origin.infra.ciab.test",
 		"profileDescription": null,
 		"profileId": null,
 		"profileName": null,
-		"protocol": 2,
+		"protocol": 0,
 		"qstringIgnore": 0,
 		"rangeRequestHandling": 0,
+		"rangeSliceBlockSize": null,
 		"regexRemap": null,
 		"regionalGeoBlocking": false,
 		"remapText": null,
-		"routingName": "video",
+		"routingName": "cdn",
+		"serviceCategory": null,
 		"signed": false,
-		"sslKeyVersion": 1,
-		"tenantId": 1,
-		"type": "HTTP",
-		"typeId": 1,
-		"xmlId": "demo1",
-		"exampleURLs": [
-			"http://video.demo1.mycdn.ciab.test",
-			"https://video.demo1.mycdn.ciab.test"
-		],
-		"deepCachingType": "NEVER",
-		"fqPacingRate": null,
 		"signingAlgorithm": null,
+		"sslKeyVersion": null,
 		"tenant": "root",
+		"tenantId": 1,
+		"tlsVersions": null,
+		"topology": null,
 		"trResponseHeaders": null,
 		"trRequestHeaders": null,
-		"consistentHashRegex": null,
-		"consistentHashQueryParams": [
-			"abc",
-			"pdq",
-			"xxx",
-			"zyx"
-		],
-		"maxOriginConnections": 0,
-		"ecsEnabled": false,
-		"rangeSliceBlockSize": null,
-		"topology": null
+		"type": "HTTP",
+		"typeId": 1,
+		"xmlId": "test"
 	}]}
 
 
diff --git a/docs/source/overview/delivery_services.rst b/docs/source/overview/delivery_services.rst
index b489cb0..7591a66 100644
--- a/docs/source/overview/delivery_services.rst
+++ b/docs/source/overview/delivery_services.rst
@@ -795,6 +795,18 @@ The :term:`Tenant` who owns this Delivery Service. They (and their parents, if a
 	| TenantID | Go code and :ref:`to-api` requests/responses | Integral, unique identifier (``bigint``, ``int`` etc.) |
 	+----------+----------------------------------------------+--------------------------------------------------------+
 
+.. _ds-tls-versions:
+
+TLS Versions
+------------
+The versions of TLS that can be used in HTTP requests to :term:`Edge-tier cache servers` for this Delivery Service's content can be limited using this property. When a Delivery Service has this property set to anything other than a ``null`` value, it lists the versions that will be allowed. Any versions can be added to the supported set, so long as they are of the form :samp:`{Major}.{Minor}`, e.g. ``1.1`` or ``42.0``. When this is a ``null`` value, no restrictions are placed on the TLS  [...]
+
+.. impl-detail:: Traffic Ops will accept empty arrays as a synonym for ``null`` in requests, but will always represent them as ``null`` in responses. Note that this means it's impossible to create a Delivery Service that explicitly supports no TLS versions - the proper way to disable HTTPS for a Delivery Service is to set its Protocol_ accordingly.
+
+A Delivery Service that has a Type_ of ``STEERING`` or ``CLIENT_STEERING`` may not legally be set to have a TLS Versions property that is non-``null``.
+
+.. warning:: Using this setting may cause old clients that only support archaic TLS versions to break suddenly. Be sure that the security increase is worth this risk.
+
 .. _ds-topology:
 
 Topology
diff --git a/lib/go-tc/deliveryservices.go b/lib/go-tc/deliveryservices.go
index 662eb61..de34134 100644
--- a/lib/go-tc/deliveryservices.go
+++ b/lib/go-tc/deliveryservices.go
@@ -6,6 +6,7 @@ import (
 	"encoding/json"
 	"errors"
 	"fmt"
+	"strings"
 
 	"github.com/apache/trafficcontrol/lib/go-util"
 )
@@ -186,6 +187,8 @@ type DeliveryServiceV31 struct {
 
 // DeliveryServiceFieldsV31 contains additions to delivery services in api v3.1
 type DeliveryServiceFieldsV31 struct {
+	// MaxRequestHeaderBytes is the maximum size (in bytes) of the request
+	// header that is allowed for this Delivery Service.
 	MaxRequestHeaderBytes *int `json:"maxRequestHeaderBytes" db:"max_request_header_bytes"`
 }
 
@@ -198,30 +201,182 @@ type DeliveryServiceV40 struct {
 	DeliveryServiceFieldsV14
 	DeliveryServiceFieldsV13
 	DeliveryServiceNullableFieldsV11
+
+	// TLSVersions is the list of explicitly supported TLS versions for cache
+	// servers serving the Delivery Service's content.
+	TLSVersions []string `json:"tlsVersions" db:"tls_versions"`
 }
 
 // DeliveryServiceV4 is a Delivery Service as it appears in version 4 of the
 // Traffic Ops API - it always points to the highest minor version in APIv4.
 type DeliveryServiceV4 = DeliveryServiceV40
 
+// These are the TLS Versions known by Apache Traffic Control to exist.
+const (
+	// Deprecated: TLS version 1.0 is known to be insecure.
+	TLSVersion10 = "1.0"
+	// Deprecated: TLS version 1.1 is known to be insecure.
+	TLSVersion11 = "1.1"
+	TLSVersion12 = "1.2"
+	TLSVersion13 = "1.3"
+)
+
+func newerTLSVersionsDisallowedMessage(old string, newer []string) string {
+	l := len(newer)
+	if l < 1 {
+		return ""
+	}
+
+	var msg strings.Builder
+	msg.WriteString("old TLS version ")
+	msg.WriteString(old)
+	msg.WriteString(" is allowed, but newer version")
+	if l > 1 {
+		msg.WriteRune('s')
+	}
+	msg.WriteRune(' ')
+	msg.WriteString(newer[0])
+	if l > 1 {
+		msg.WriteString(", ")
+		if l > 2 {
+			msg.WriteString(newer[1])
+			msg.WriteString(", and ")
+			msg.WriteString(newer[2])
+		} else {
+			msg.WriteString("and ")
+			msg.WriteString(newer[1])
+		}
+		msg.WriteString(" are ")
+	} else {
+		msg.WriteString(" is ")
+	}
+	msg.WriteString("disallowed; this configuration may be insecure")
+
+	return msg.String()
+}
+
+// TLSVersionsAlerts generates warning-level alerts for the Delivery Service's
+// TLS versions array. It will warn if newer versions are disallowed while
+// older, less secure versions are allowed, if there are unrecognized versions
+// present, if the Delivery Service's Protocol does not make use of TLS
+// Versions, and whenever TLSVersions are explicitly set at all.
+//
+// This does NOT verify that the Delivery Service's TLS versions are _valid_,
+// it ONLY creates warnings based on conditions that are possibly detrimental
+// to CDN operation, but can, in fact, work.
+func (ds DeliveryServiceV4) TLSVersionsAlerts() Alerts {
+	vers := ds.TLSVersions
+	messages := []string{}
+
+	if len(vers) > 0 {
+		messages = append(messages, "setting TLS Versions that are explicitly supported may break older clients that can't use the specified versions")
+	} else {
+		return Alerts{Alerts: []Alert{}}
+	}
+
+	found := map[string]bool{
+		TLSVersion10: false,
+		TLSVersion11: false,
+		TLSVersion12: false,
+		TLSVersion13: false,
+	}
+
+	for _, v := range vers {
+		switch v {
+		case TLSVersion10:
+			found[TLSVersion10] = true
+		case TLSVersion11:
+			found[TLSVersion11] = true
+		case TLSVersion12:
+			found[TLSVersion12] = true
+		case TLSVersion13:
+			found[TLSVersion13] = true
+		default:
+			messages = append(messages, "unknown TLS version '"+v+"' - possible typo")
+		}
+	}
+
+	if found[TLSVersion10] {
+		var newerDisallowed []string
+		if !found[TLSVersion11] {
+			newerDisallowed = append(newerDisallowed, TLSVersion11)
+		}
+		if !found[TLSVersion12] {
+			newerDisallowed = append(newerDisallowed, TLSVersion12)
+		}
+		if !found[TLSVersion13] {
+			newerDisallowed = append(newerDisallowed, TLSVersion13)
+		}
+		msg := newerTLSVersionsDisallowedMessage(TLSVersion10, newerDisallowed)
+		if msg != "" {
+			messages = append(messages, msg)
+		}
+	} else if found[TLSVersion11] {
+		var newerDisallowed []string
+		if !found[TLSVersion12] {
+			newerDisallowed = append(newerDisallowed, TLSVersion12)
+		}
+		if !found[TLSVersion13] {
+			newerDisallowed = append(newerDisallowed, TLSVersion13)
+		}
+		msg := newerTLSVersionsDisallowedMessage(TLSVersion11, newerDisallowed)
+		if msg != "" {
+			messages = append(messages, msg)
+		}
+	} else if found[TLSVersion12] {
+		var newerDisallowed []string
+		if !found[TLSVersion13] {
+			newerDisallowed = append(newerDisallowed, TLSVersion13)
+		}
+		msg := newerTLSVersionsDisallowedMessage(TLSVersion12, newerDisallowed)
+		if msg != "" {
+			messages = append(messages, msg)
+		}
+	}
+
+	if ds.Protocol != nil && *ds.Protocol == DSProtocolHTTP {
+		messages = append(messages, "tlsVersions has no effect on Delivery Services with Protocol '0' (HTTP_ONLY)")
+	}
+
+	return CreateAlerts(WarnLevel, messages...)
+}
+
 type DeliveryServiceV30 struct {
 	DeliveryServiceNullableV15
 	DeliveryServiceFieldsV30
 }
 
-// DeliveryServiceFieldsV30 contains additions to delivery services in api v3.0
+// DeliveryServiceFieldsV30 contains additions to delivery services in api v3.0.
 type DeliveryServiceFieldsV30 struct {
-	Topology           *string `json:"topology" db:"topology"`
+	// FirstHeaderRewrite is a "header rewrite rule" used by ATS at the first
+	// caching layer encountered in the Delivery Service's Topology, or nil if
+	// there is no such rule. This has no effect on Delivery Services that don't
+	// employ Topologies.
 	FirstHeaderRewrite *string `json:"firstHeaderRewrite" db:"first_header_rewrite"`
+	// InnerHeaderRewrite is a "header rewrite rule" used by ATS at all caching
+	// layers encountered in the Delivery Service's Topology except the first
+	// and last, or nil if there is no such rule. This has no effect on Delivery
+	// Services that don't employ Topologies.
 	InnerHeaderRewrite *string `json:"innerHeaderRewrite" db:"inner_header_rewrite"`
-	LastHeaderRewrite  *string `json:"lastHeaderRewrite" db:"last_header_rewrite"`
-	ServiceCategory    *string `json:"serviceCategory" db:"service_category"`
+	// LastHeaderRewrite is a "header rewrite rule" used by ATS at the first
+	// caching layer encountered in the Delivery Service's Topology, or nil if
+	// there is no such rule. This has no effect on Delivery Services that don't
+	// employ Topologies.
+	LastHeaderRewrite *string `json:"lastHeaderRewrite" db:"last_header_rewrite"`
+	// ServiceCategory defines a category to which a Delivery Service may
+	// belong, which will cause HTTP Responses containing content for the
+	// Delivery Service to have the "X-CDN-SVC" header with a value that is the
+	// XMLID of the Delivery Service.
+	ServiceCategory *string `json:"serviceCategory" db:"service_category"`
+	// Topology is the name of the Topology used by the Delivery Service, or nil
+	// if no Topology is used.
+	Topology *string `json:"topology" db:"topology"`
 }
 
 // DeliveryServiceNullableV30 is the aliased structure that we should be using for all api 3.x delivery structure operations
 // This type should always alias the latest 3.x minor version struct. For ex, if you wanted to create a DeliveryServiceV32 struct, you would do the following:
 // type DeliveryServiceNullableV30 DeliveryServiceV32
-// DeliveryServiceV32 = DeliveryServiceV31 + the new fields
+// DeliveryServiceV32 = DeliveryServiceV31 + the new fields.
 type DeliveryServiceNullableV30 DeliveryServiceV31
 
 // Deprecated: Use versioned structures only from now on.
@@ -231,9 +386,17 @@ type DeliveryServiceNullableV15 struct {
 	DeliveryServiceFieldsV15
 }
 
-// DeliveryServiceFieldsV15 contains additions to delivery services in api v1.5
+// DeliveryServiceFieldsV15 contains additions to delivery services in api v1.5.
 type DeliveryServiceFieldsV15 struct {
-	EcsEnabled          bool `json:"ecsEnabled" db:"ecs_enabled"`
+	// EcsEnabled describes whether or not the Traffic Router's EDNS0 Client
+	// Subnet extensions should be enabled when serving DNS responses for this
+	// Delivery Service. Even if this is true, the Traffic Router may still
+	// have the extensions unilaterally disabled in its own configuration.
+	EcsEnabled bool `json:"ecsEnabled" db:"ecs_enabled"`
+	// RangeSliceBlockSize defines the size of range request blocks - or
+	// "slices" - used by the "slice" plugin. This has no effect if
+	// RangeRequestHandling does not point to exactly 3. This may never legally
+	// point to a value less than zero.
 	RangeSliceBlockSize *int `json:"rangeSliceBlockSize" db:"range_slice_block_size"`
 }
 
@@ -242,11 +405,21 @@ type DeliveryServiceNullableV14 struct {
 	DeliveryServiceFieldsV14
 }
 
-// DeliveryServiceFieldsV14 contains additions to delivery services in api v1.4
+// DeliveryServiceFieldsV14 contains additions to delivery services in api v1.4.
 type DeliveryServiceFieldsV14 struct {
-	ConsistentHashRegex       *string  `json:"consistentHashRegex"`
+	// ConsistentHashRegex is used by Traffic Router to extract meaningful parts
+	// of a client's request URI for HTTP-routed Delivery Services before
+	// hashing the request to find a cache server to which to direct the client.
+	ConsistentHashRegex *string `json:"consistentHashRegex"`
+	// ConsistentHashQueryParams is a list of al of the query string parameters
+	// which ought to be considered by Traffic Router in client request URIs for
+	// HTTP-routed Delivery Services in the hashing process.
 	ConsistentHashQueryParams []string `json:"consistentHashQueryParams"`
-	MaxOriginConnections      *int     `json:"maxOriginConnections" db:"max_origin_connections"`
+	// MaxOriginConnections defines the total maximum  number of connections
+	// that the highest caching layer ("Mid-tier" in a non-Topology context) is
+	// allowed to have concurrently open to the Delivery Service's Origin. This
+	// may never legally point to a value less than 0.
+	MaxOriginConnections *int `json:"maxOriginConnections" db:"max_origin_connections"`
 }
 
 type DeliveryServiceNullableV13 struct {
@@ -254,14 +427,34 @@ type DeliveryServiceNullableV13 struct {
 	DeliveryServiceFieldsV13
 }
 
-// DeliveryServiceFieldsV13 contains additions to delivery services in api v1.3
+// DeliveryServiceFieldsV13 contains additions to delivery services in api v1.3.
 type DeliveryServiceFieldsV13 struct {
-	DeepCachingType   *DeepCachingType `json:"deepCachingType" db:"deep_caching_type"`
-	FQPacingRate      *int             `json:"fqPacingRate" db:"fq_pacing_rate"`
-	SigningAlgorithm  *string          `json:"signingAlgorithm" db:"signing_algorithm"`
-	Tenant            *string          `json:"tenant"`
-	TRResponseHeaders *string          `json:"trResponseHeaders"`
-	TRRequestHeaders  *string          `json:"trRequestHeaders"`
+	// DeepCachingType may only legally point to 'ALWAYS' or 'NEVER', which
+	// define whether "deep caching" may or may not be used for this Delivery
+	// Service, respectively.
+	DeepCachingType *DeepCachingType `json:"deepCachingType" db:"deep_caching_type"`
+	// FQPacingRate sets the maximum bytes per second a cache server will deliver
+	// on any single TCP connection for this Delivery Service. This may never
+	// legally point to a value less than zero.
+	FQPacingRate *int `json:"fqPacingRate" db:"fq_pacing_rate"`
+	// SigningAlgorithm is the name of the algorithm used to sign CDN URIs for
+	// this Delivery Service's content, or nil if no URI signing is done for the
+	// Delivery Service. This may only point to the values "url_sig" or
+	// "uri_signing".
+	SigningAlgorithm *string `json:"signingAlgorithm" db:"signing_algorithm"`
+	// Tenant is the Tenant to which the Delivery Service belongs.
+	Tenant *string `json:"tenant"`
+	// TRResponseHeaders is a set of headers (separated by CRLF pairs as per the
+	// HTTP spec) and their values (separated by a colon as per the HTTP spec)
+	// which will be sent by Traffic Router in HTTP responses to client requests
+	// for this Delivery Service's content. This has no effect on DNS-routed or
+	// un-routed Delivery Service Types.
+	TRResponseHeaders *string `json:"trResponseHeaders"`
+	// TRRequestHeaders is an "array" of HTTP headers which should be logged
+	// from client HTTP requests for this Delivery Service's content by Traffic
+	// Router, separated by newlines. This has no effect on DNS-routed or
+	// un-routed Delivery Service Types.
+	TRRequestHeaders *string `json:"trRequestHeaders"`
 }
 
 type DeliveryServiceNullableV12 struct {
@@ -270,67 +463,220 @@ type DeliveryServiceNullableV12 struct {
 
 // DeliveryServiceNullableV11 is a version of the deliveryservice that allows
 // for all fields to be null.
-// TODO move contents to DeliveryServiceNullableV12, fix references, and remove
+//
+// TODO: move contents to DeliveryServiceNullableV12, fix references, and remove this.
 type DeliveryServiceNullableV11 struct {
 	DeliveryServiceNullableFieldsV11
 	DeliveryServiceRemovedFieldsV11
 }
 
 type DeliveryServiceNullableFieldsV11 struct {
-	Active                   *bool                   `json:"active" db:"active"`
-	AnonymousBlockingEnabled *bool                   `json:"anonymousBlockingEnabled" db:"anonymous_blocking_enabled"`
-	CCRDNSTTL                *int                    `json:"ccrDnsTtl" db:"ccr_dns_ttl"`
-	CDNID                    *int                    `json:"cdnId" db:"cdn_id"`
-	CDNName                  *string                 `json:"cdnName"`
-	CheckPath                *string                 `json:"checkPath" db:"check_path"`
-	DisplayName              *string                 `json:"displayName" db:"display_name"`
-	DNSBypassCNAME           *string                 `json:"dnsBypassCname" db:"dns_bypass_cname"`
-	DNSBypassIP              *string                 `json:"dnsBypassIp" db:"dns_bypass_ip"`
-	DNSBypassIP6             *string                 `json:"dnsBypassIp6" db:"dns_bypass_ip6"`
-	DNSBypassTTL             *int                    `json:"dnsBypassTtl" db:"dns_bypass_ttl"`
-	DSCP                     *int                    `json:"dscp" db:"dscp"`
-	EdgeHeaderRewrite        *string                 `json:"edgeHeaderRewrite" db:"edge_header_rewrite"`
-	GeoLimit                 *int                    `json:"geoLimit" db:"geo_limit"`
-	GeoLimitCountries        *string                 `json:"geoLimitCountries" db:"geo_limit_countries"`
-	GeoLimitRedirectURL      *string                 `json:"geoLimitRedirectURL" db:"geolimit_redirect_url"`
-	GeoProvider              *int                    `json:"geoProvider" db:"geo_provider"`
-	GlobalMaxMBPS            *int                    `json:"globalMaxMbps" db:"global_max_mbps"`
-	GlobalMaxTPS             *int                    `json:"globalMaxTps" db:"global_max_tps"`
-	HTTPBypassFQDN           *string                 `json:"httpBypassFqdn" db:"http_bypass_fqdn"`
-	ID                       *int                    `json:"id" db:"id"`
-	InfoURL                  *string                 `json:"infoUrl" db:"info_url"`
-	InitialDispersion        *int                    `json:"initialDispersion" db:"initial_dispersion"`
-	IPV6RoutingEnabled       *bool                   `json:"ipv6RoutingEnabled" db:"ipv6_routing_enabled"`
-	LastUpdated              *TimeNoMod              `json:"lastUpdated" db:"last_updated"`
-	LogsEnabled              *bool                   `json:"logsEnabled" db:"logs_enabled"`
-	LongDesc                 *string                 `json:"longDesc" db:"long_desc"`
-	LongDesc1                *string                 `json:"longDesc1,omitempty" db:"long_desc_1"`
-	LongDesc2                *string                 `json:"longDesc2,omitempty" db:"long_desc_2"`
-	MatchList                *[]DeliveryServiceMatch `json:"matchList"`
-	MaxDNSAnswers            *int                    `json:"maxDnsAnswers" db:"max_dns_answers"`
-	MidHeaderRewrite         *string                 `json:"midHeaderRewrite" db:"mid_header_rewrite"`
-	MissLat                  *float64                `json:"missLat" db:"miss_lat"`
-	MissLong                 *float64                `json:"missLong" db:"miss_long"`
-	MultiSiteOrigin          *bool                   `json:"multiSiteOrigin" db:"multi_site_origin"`
-	OriginShield             *string                 `json:"originShield" db:"origin_shield"`
-	OrgServerFQDN            *string                 `json:"orgServerFqdn" db:"org_server_fqdn"`
-	ProfileDesc              *string                 `json:"profileDescription"`
-	ProfileID                *int                    `json:"profileId" db:"profile"`
-	ProfileName              *string                 `json:"profileName"`
-	Protocol                 *int                    `json:"protocol" db:"protocol"`
-	QStringIgnore            *int                    `json:"qstringIgnore" db:"qstring_ignore"`
-	RangeRequestHandling     *int                    `json:"rangeRequestHandling" db:"range_request_handling"`
-	RegexRemap               *string                 `json:"regexRemap" db:"regex_remap"`
-	RegionalGeoBlocking      *bool                   `json:"regionalGeoBlocking" db:"regional_geo_blocking"`
-	RemapText                *string                 `json:"remapText" db:"remap_text"`
-	RoutingName              *string                 `json:"routingName" db:"routing_name"`
-	Signed                   bool                    `json:"signed"`
-	SSLKeyVersion            *int                    `json:"sslKeyVersion" db:"ssl_key_version"`
-	TenantID                 *int                    `json:"tenantId" db:"tenant_id"`
-	Type                     *DSType                 `json:"type"`
-	TypeID                   *int                    `json:"typeId" db:"type"`
-	XMLID                    *string                 `json:"xmlId" db:"xml_id"`
-	ExampleURLs              []string                `json:"exampleURLs"`
+	// Active dictates whether the Delivery Service is routed by Traffic Router.
+	Active *bool `json:"active" db:"active"`
+	// AnonymousBlockingEnabled sets whether or not anonymized IP addresses
+	// (e.g. Tor exit nodes) should be restricted from accessing the Delivery
+	// Service's content.
+	AnonymousBlockingEnabled *bool `json:"anonymousBlockingEnabled" db:"anonymous_blocking_enabled"`
+	// CCRDNSTTL sets the Time-to-Live - in seconds - for DNS responses for this
+	// Delivery Service from Traffic Router.
+	CCRDNSTTL *int `json:"ccrDnsTtl" db:"ccr_dns_ttl"`
+	// CDNID is the integral, unique identifier for the CDN to which the
+	// Delivery Service belongs.
+	CDNID *int `json:"cdnId" db:"cdn_id"`
+	// CDNName is the name of the CDN to which the Delivery Service belongs.
+	CDNName *string `json:"cdnName"`
+	// CheckPath is a path which may be requested of the Delivery Service's
+	// origin to ensure it's working properly.
+	CheckPath *string `json:"checkPath" db:"check_path"`
+	// DisplayName is a human-friendly name that might be used in some UIs
+	// somewhere.
+	DisplayName *string `json:"displayName" db:"display_name"`
+	// DNSBypassCNAME is a fully qualified domain name to be used in a CNAME
+	// record presented to clients in bypass scenarios.
+	DNSBypassCNAME *string `json:"dnsBypassCname" db:"dns_bypass_cname"`
+	// DNSBypassIP is an IPv4 address to be used in an A record presented to
+	// clients in bypass scenarios.
+	DNSBypassIP *string `json:"dnsBypassIp" db:"dns_bypass_ip"`
+	// DNSBypassIP6 is an IPv6 address to be used in an AAAA record presented to
+	// clients in bypass scenarios.
+	DNSBypassIP6 *string `json:"dnsBypassIp6" db:"dns_bypass_ip6"`
+	// DNSBypassTTL sets the Time-to-Live - in seconds - of DNS responses from
+	// the Traffic Router that contain records for bypass destinations.
+	DNSBypassTTL *int `json:"dnsBypassTtl" db:"dns_bypass_ttl"`
+	// DSCP sets the Differentiated Services Code Point for IP packets
+	// transferred between clients, origins, and cache servers when obtaining
+	// and serving content for this Delivery Service.
+	// See Also: https://en.wikipedia.org/wiki/Differentiated_services
+	DSCP *int `json:"dscp" db:"dscp"`
+	// EdgeHeaderRewrite is a "header rewrite rule" used by ATS at the Edge-tier
+	// of caching. This has no effect on Delivery Services that don't use a
+	// Topology.
+	EdgeHeaderRewrite *string `json:"edgeHeaderRewrite" db:"edge_header_rewrite"`
+	// ExampleURLs is a list of all of the URLs from which content may be
+	// requested from the Delivery Service.
+	ExampleURLs []string `json:"exampleURLs"`
+	// GeoLimit defines whether or not access to a Delivery Service's content
+	// should be limited based on the requesting client's geographic location.
+	// Despite that this is a pointer to an arbitrary integer, the only valid
+	// values are 0 (which indicates that content should not be limited
+	// geographically), 1 (which indicates that content should only be served to
+	// clients whose IP addresses can be found within a Coverage Zone File), and
+	// 2 (which indicates that content should be served to clients whose IP
+	// addresses can be found within a Coverage Zone File OR are allowed access
+	// according to the "array" in GeoLimitCountries).
+	GeoLimit *int `json:"geoLimit" db:"geo_limit"`
+	// GeoLimitCountries is an "array" of "country codes" that itemizes the
+	// countries within which the Delivery Service's content ought to be made
+	// available. This has no effect if GeoLimit is not a pointer to exactly the
+	// value 2.
+	GeoLimitCountries *string `json:"geoLimitCountries" db:"geo_limit_countries"`
+	// GeoLimitRedirectURL is a URL to which clients will be redirected if their
+	// access to the Delivery Service's content is blocked by GeoLimit rules.
+	GeoLimitRedirectURL *string `json:"geoLimitRedirectURL" db:"geolimit_redirect_url"`
+	// GeoProvider names the type of database to be used for providing IP
+	// address-to-geographic-location mapping for this Delivery Service. The
+	// only valid values to which it may point are 0 (which indicates the use of
+	// a MaxMind GeoIP2 database) and 1 (which indicates the use of a Neustar
+	// GeoPoint IP address database).
+	GeoProvider *int `json:"geoProvider" db:"geo_provider"`
+	// GlobalMaxMBPS defines a maximum number of MegaBytes Per Second which may
+	// be served for the Delivery Service before redirecting clients to bypass
+	// locations.
+	GlobalMaxMBPS *int `json:"globalMaxMbps" db:"global_max_mbps"`
+	// GlobalMaxTPS defines a maximum number of Transactions Per Second which
+	// may be served for the Delivery Service before redirecting clients to
+	// bypass locations.
+	GlobalMaxTPS *int `json:"globalMaxTps" db:"global_max_tps"`
+	// HTTPBypassFQDN is a network location to which clients will be redirected
+	// in bypass scenarios using HTTP "Location" headers and appropriate
+	// redirection response codes.
+	HTTPBypassFQDN *string `json:"httpBypassFqdn" db:"http_bypass_fqdn"`
+	// ID is an integral, unique identifier for the Delivery Service.
+	ID *int `json:"id" db:"id"`
+	// InfoURL is a URL to which operators or clients may be directed to obtain
+	// further information about a Delivery Service.
+	InfoURL *string `json:"infoUrl" db:"info_url"`
+	// InitialDispersion sets the number of cache servers within the first
+	// caching layer ("Edge-tier" in a non-Topology context) across which
+	// content will be dispersed per Cache Group.
+	InitialDispersion *int `json:"initialDispersion" db:"initial_dispersion"`
+	// IPV6RoutingEnabled controls whether or not routing over IPv6 should be
+	// done for this Delivery Service.
+	IPV6RoutingEnabled *bool `json:"ipv6RoutingEnabled" db:"ipv6_routing_enabled"`
+	// LastUpdated is the time and date at which the Delivery Service was last
+	// updated.
+	LastUpdated *TimeNoMod `json:"lastUpdated" db:"last_updated"`
+	// LogsEnabled controls nothing. It is kept only for legacy compatibility.
+	LogsEnabled *bool `json:"logsEnabled" db:"logs_enabled"`
+	// LongDesc is a description of the Delivery Service, having arbitrary
+	// length.
+	LongDesc *string `json:"longDesc" db:"long_desc"`
+	// LongDesc1 is a description of the Delivery Service, having arbitrary
+	// length.
+	LongDesc1 *string `json:"longDesc1" db:"long_desc_1"`
+	// LongDesc2 is a description of the Delivery Service, having arbitrary
+	// length.
+	LongDesc2 *string `json:"longDesc2" db:"long_desc_2"`
+	// MatchList is a list of Regular Expressions used for routing the Delivery
+	// Service. Order matters, and the array is not allowed to be sparse.
+	MatchList *[]DeliveryServiceMatch `json:"matchList"`
+	// MaxDNSAnswers sets the maximum number of records which should be returned
+	// by Traffic Router in DNS responses to requests for resolving names for
+	// this Delivery Service.
+	MaxDNSAnswers *int `json:"maxDnsAnswers" db:"max_dns_answers"`
+	// MidHeaderRewrite is a "header rewrite rule" used by ATS at the Mid-tier
+	// of caching. This has no effect on Delivery Services that don't use a
+	// Topology.
+	MidHeaderRewrite *string `json:"midHeaderRewrite" db:"mid_header_rewrite"`
+	// MissLat is a latitude to default to for clients of this Delivery Service
+	// when geolocation attempts fail.
+	MissLat *float64 `json:"missLat" db:"miss_lat"`
+	// MissLong is a longitude to default to for clients of this Delivery
+	// Service when geolocation attempts fail.
+	MissLong *float64 `json:"missLong" db:"miss_long"`
+	// MultiSiteOrigin determines whether or not the Delivery Service makes use
+	// of "Multi-Site Origin".
+	MultiSiteOrigin *bool `json:"multiSiteOrigin" db:"multi_site_origin"`
+	// OriginShield is a field that does nothing. It is kept only for legacy
+	// compatibility reasons.
+	OriginShield *string `json:"originShield" db:"origin_shield"`
+	// OrgServerFQDN is the URL - NOT Fully Qualified Domain Name - of the
+	// origin of the Delivery Service's content.
+	OrgServerFQDN *string `json:"orgServerFqdn" db:"org_server_fqdn"`
+	// ProfileDesc is the Description of the Profile used by the Delivery
+	// Service, if any.
+	ProfileDesc *string `json:"profileDescription"`
+	// ProfileID is the integral, unique identifier of the Profile used by the
+	// Delivery Service, if any.
+	ProfileID *int `json:"profileId" db:"profile"`
+	// ProfileName is the Name of the Profile used by the Delivery Service, if
+	// any.
+	ProfileName *string `json:"profileName"`
+	// Protocol defines the protocols by which caching servers may communicate
+	// with clients. The valid values to which it may point are 0 (which implies
+	// that only HTTP may be used), 1 (which implies that only HTTPS may be
+	// used), 2 (which implies that either HTTP or HTTPS may be used), and 3
+	// (which implies that clients using HTTP must be redirected to use HTTPS,
+	// while communications over HTTPS may proceed as normal).
+	Protocol *int `json:"protocol" db:"protocol"`
+	// QStringIgnore sets how query strings in HTTP requests to cache servers
+	// from clients are treated. The only valid values to which it may point are
+	// 0 (which implies that all caching layers will pass query strings in
+	// upstream requests and use them in the cache key), 1 (which implies that
+	// all caching layers will pass query strings in upstream requests, but not
+	// use them in cache keys), and 2 (which implies that the first encountered
+	// caching layer - "Edge-tier" in a non-Topology context - will strip query
+	// strings, effectively preventing them from being passed in upstream
+	// requests, and not use them in the cache key).
+	QStringIgnore *int `json:"qstringIgnore" db:"qstring_ignore"`
+	// RangeRequestHandling defines how HTTP GET requests with a Range header
+	// will be handled by cache servers serving the Delivery Service's content.
+	// The only valid values to which it may point are 0 (which implies that
+	// Range requests will not be cached at all), 1 (which implies that the
+	// background_fetch plugin will be used to service the range request while
+	// caching the whole object), 2 (which implies that the cache_range_requests
+	// plugin will be used to cache ranges as unique objects), and 3 (which
+	// implies that the slice plugin will be used to slice range based requests
+	// into deterministic chunks.)
+	RangeRequestHandling *int `json:"rangeRequestHandling" db:"range_request_handling"`
+	// Regex Remap is a raw line to be inserted into "regex_remap.config" on the
+	// cache server. Care is necessitated in its use, because the input is in no
+	// way restricted, validated, or limited in scope to the Delivery Service.
+	RegexRemap *string `json:"regexRemap" db:"regex_remap"`
+	// RegionalGeoBlocking defines whether or not whatever Regional Geo Blocking
+	// rules are configured on the Traffic Router serving content for this
+	// Delivery Service will have an effect on the traffic of this Delivery
+	// Service.
+	RegionalGeoBlocking *bool `json:"regionalGeoBlocking" db:"regional_geo_blocking"`
+	// RemapText is raw text to insert in "remap.config" on the cache servers
+	// serving content for this Delivery Service. Care is necessitated in its
+	// use, because the input is in no way restricted, validated, or limited in
+	// scope to the Delivery Service.
+	RemapText *string `json:"remapText" db:"remap_text"`
+	// RoutingName defines the lowest-level DNS label used by the Delivery
+	// Service, e.g. if trafficcontrol.apache.org were a Delivery Service, it
+	// would have a RoutingName of "trafficcontrol".
+	RoutingName *string `json:"routingName" db:"routing_name"`
+	// Signed is a legacy field. It is allowed to be `true` if and only if
+	// SigningAlgorithm is not nil.
+	Signed bool `json:"signed"`
+	// SSLKeyVersion incremented whenever Traffic Portal generates new SSL keys
+	// for the Delivery Service, effectively making it a "generational" marker.
+	SSLKeyVersion *int `json:"sslKeyVersion" db:"ssl_key_version"`
+	// TenantID is the integral, unique identifier for the Tenant to which the
+	// Delivery Service belongs.
+	TenantID *int `json:"tenantId" db:"tenant_id"`
+	// Type describes how content is routed and cached for this Delivery Service
+	// as well as what other properties have any meaning.
+	Type *DSType `json:"type"`
+	// TypeID is an integral, unique identifier for the Tenant to which the
+	// Delivery Service belongs.
+	TypeID *int `json:"typeId" db:"type"`
+	// XMLID is a unique identifier that is also the second lowest-level DNS
+	// label used by the Delivery Service. For example, if a Delivery Service's
+	// content may be requested from video.demo1.mycdn.ciab.test, it may be
+	// inferred that the Delivery Service's XMLID is demo1.
+	XMLID *string `json:"xmlId" db:"xml_id"`
 }
 
 // DeliveryServiceRemovedFieldsV11 contains additions to delivery services in api v1.1 that were later removed
@@ -347,6 +693,7 @@ func (ds *DeliveryServiceV4) RemoveLD1AndLD2() DeliveryServiceV4 {
 }
 
 // DowngradeToV3 converts the 4.x DS to a 3.x DS
+// DowngradeToV3 converts the 4.x DS to a 3.x DS.
 func (ds *DeliveryServiceV4) DowngradeToV3() DeliveryServiceNullableV30 {
 	return DeliveryServiceNullableV30{
 		DeliveryServiceV30: DeliveryServiceV30{
@@ -370,7 +717,7 @@ func (ds *DeliveryServiceV4) DowngradeToV3() DeliveryServiceNullableV30 {
 	}
 }
 
-// UpgradeToV4 converts the 3.x DS to a 4.x DS
+// UpgradeToV4 converts the 3.x DS to a 4.x DS.
 func (ds *DeliveryServiceNullableV30) UpgradeToV4() DeliveryServiceV4 {
 	return DeliveryServiceV4{
 		DeliveryServiceFieldsV31:         ds.DeliveryServiceFieldsV31,
@@ -379,6 +726,7 @@ func (ds *DeliveryServiceNullableV30) UpgradeToV4() DeliveryServiceV4 {
 		DeliveryServiceFieldsV14:         ds.DeliveryServiceFieldsV14,
 		DeliveryServiceFieldsV13:         ds.DeliveryServiceFieldsV13,
 		DeliveryServiceNullableFieldsV11: ds.DeliveryServiceNullableFieldsV11,
+		TLSVersions:                      nil,
 	}
 }
 
diff --git a/lib/go-tc/deliveryservices_test.go b/lib/go-tc/deliveryservices_test.go
new file mode 100644
index 0000000..3ba0f6a
--- /dev/null
+++ b/lib/go-tc/deliveryservices_test.go
@@ -0,0 +1,844 @@
+package tc
+
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import (
+	"fmt"
+	"testing"
+)
+
+func compareV31DSes(a, b DeliveryServiceNullableV30, t *testing.T) {
+	if (a.Active == nil && b.Active != nil) || (a.Active != nil && b.Active == nil) {
+		t.Error("Mismatched 'Active' property; one was nil but the other was not.")
+	} else if a.Active != nil && *a.Active != *b.Active {
+		t.Errorf("Mismatched 'Active' property; one was '%v', the other was '%v'", *a.Active, *b.Active)
+	}
+	if (a.AnonymousBlockingEnabled == nil && b.AnonymousBlockingEnabled != nil) || (a.AnonymousBlockingEnabled != nil && b.AnonymousBlockingEnabled == nil) {
+		t.Error("Mismatched 'AnonymousBlockingEnabled' property; one was nil but the other was not.")
+	} else if a.AnonymousBlockingEnabled != nil && *a.AnonymousBlockingEnabled != *b.AnonymousBlockingEnabled {
+		t.Errorf("Mismatched 'AnonymousBlockingEnabled' property; one was '%v', the other was '%v'", *a.AnonymousBlockingEnabled, *b.AnonymousBlockingEnabled)
+	}
+	if (a.CCRDNSTTL == nil && b.CCRDNSTTL != nil) || (a.CCRDNSTTL != nil && b.CCRDNSTTL == nil) {
+		t.Error("Mismatched 'CCRDNSTTL' property; one was nil but the other was not.")
+	} else if a.CCRDNSTTL != nil && *a.CCRDNSTTL != *b.CCRDNSTTL {
+		t.Errorf("Mismatched 'CCRDNSTTL' property; one was '%v', the other was '%v'", *a.CCRDNSTTL, *b.CCRDNSTTL)
+	}
+	if (a.CDNID == nil && b.CDNID != nil) || (a.CDNID != nil && b.CDNID == nil) {
+		t.Error("Mismatched 'CDNID' property; one was nil but the other was not.")
+	} else if a.CDNID != nil && *a.CDNID != *b.CDNID {
+		t.Errorf("Mismatched 'CDNID' property; one was '%v', the other was '%v'", *a.CDNID, *b.CDNID)
+	}
+	if (a.CDNName == nil && b.CDNName != nil) || (a.CDNName != nil && b.CDNName == nil) {
+		t.Error("Mismatched 'CDNName' property; one was nil but the other was not.")
+	} else if a.CDNName != nil && *a.CDNName != *b.CDNName {
+		t.Errorf("Mismatched 'CDNName' property; one was '%v', the other was '%v'", *a.CDNName, *b.CDNName)
+	}
+	if (a.CheckPath == nil && b.CheckPath != nil) || (a.CheckPath != nil && b.CheckPath == nil) {
+		t.Error("Mismatched 'CheckPath' property; one was nil but the other was not.")
+	} else if a.CheckPath != nil && *a.CheckPath != *b.CheckPath {
+		t.Errorf("Mismatched 'CheckPath' property; one was '%v', the other was '%v'", *a.CheckPath, *b.CheckPath)
+	}
+	if len(a.ConsistentHashQueryParams) != len(b.ConsistentHashQueryParams) {
+		t.Errorf("Mismatched 'ConsistentHashQueryParams' property; one contained %d but the other contained %d.", len(a.ConsistentHashQueryParams), len(b.ConsistentHashQueryParams))
+	} else {
+		for i, qp := range a.ConsistentHashQueryParams {
+			if qp != b.ConsistentHashQueryParams[i] {
+				t.Errorf("Mismatched 'ConsistentHashQueryParams[%d]'; one was %s, but the other was %s", i, qp, b.ConsistentHashQueryParams[i])
+			}
+		}
+	}
+	if (a.ConsistentHashRegex == nil && b.ConsistentHashRegex != nil) || (a.ConsistentHashRegex != nil && b.ConsistentHashRegex == nil) {
+		t.Error("Mismatched 'ConsistentHashRegex' property; one was nil but the other was not.")
+	} else if a.ConsistentHashRegex != nil && *a.ConsistentHashRegex != *b.ConsistentHashRegex {
+		t.Errorf("Mismatched 'ConsistentHashRegex' property; one was '%v', the other was '%v'", *a.ConsistentHashRegex, *b.ConsistentHashRegex)
+	}
+	if (a.DeepCachingType == nil && b.DeepCachingType != nil) || (a.DeepCachingType != nil && b.DeepCachingType == nil) {
+		t.Error("Mismatched 'DeepCachingType' property; one was nil but the other was not.")
+	} else if a.DeepCachingType != nil && *a.DeepCachingType != *b.DeepCachingType {
+		t.Errorf("Mismatched 'DeepCachingType' property; one was '%v', the other was '%v'", *a.DeepCachingType, *b.DeepCachingType)
+	}
+	if (a.DisplayName == nil && b.DisplayName != nil) || (a.DisplayName != nil && b.DisplayName == nil) {
+		t.Error("Mismatched 'DisplayName' property; one was nil but the other was not.")
+	} else if a.DisplayName != nil && *a.DisplayName != *b.DisplayName {
+		t.Errorf("Mismatched 'DisplayName' property; one was '%v', the other was '%v'", *a.DisplayName, *b.DisplayName)
+	}
+	if (a.DNSBypassCNAME == nil && b.DNSBypassCNAME != nil) || (a.DNSBypassCNAME != nil && b.DNSBypassCNAME == nil) {
+		t.Error("Mismatched 'DNSBypassCNAME' property; one was nil but the other was not.")
+	} else if a.DNSBypassCNAME != nil && *a.DNSBypassCNAME != *b.DNSBypassCNAME {
+		t.Errorf("Mismatched 'DNSBypassCNAME' property; one was '%v', the other was '%v'", *a.DNSBypassCNAME, *b.DNSBypassCNAME)
+	}
+	if (a.DNSBypassIP6 == nil && b.DNSBypassIP6 != nil) || (a.DNSBypassIP6 != nil && b.DNSBypassIP6 == nil) {
+		t.Error("Mismatched 'DNSBypassIP6' property; one was nil but the other was not.")
+	} else if a.DNSBypassIP6 != nil && *a.DNSBypassIP6 != *b.DNSBypassIP6 {
+		t.Errorf("Mismatched 'DNSBypassIP6' property; one was '%v', the other was '%v'", *a.DNSBypassIP6, *b.DNSBypassIP6)
+	}
+	if (a.DNSBypassIP == nil && b.DNSBypassIP != nil) || (a.DNSBypassIP != nil && b.DNSBypassIP == nil) {
+		t.Error("Mismatched 'DNSBypassIP' property; one was nil but the other was not.")
+	} else if a.DNSBypassIP != nil && *a.DNSBypassIP != *b.DNSBypassIP {
+		t.Errorf("Mismatched 'DNSBypassIP' property; one was '%v', the other was '%v'", *a.DNSBypassIP, *b.DNSBypassIP)
+	}
+	if (a.DNSBypassTTL == nil && b.DNSBypassTTL != nil) || (a.DNSBypassTTL != nil && b.DNSBypassTTL == nil) {
+		t.Error("Mismatched 'DNSBypassTTL' property; one was nil but the other was not.")
+	} else if a.DNSBypassTTL != nil && *a.DNSBypassTTL != *b.DNSBypassTTL {
+		t.Errorf("Mismatched 'DNSBypassTTL' property; one was '%v', the other was '%v'", *a.DNSBypassTTL, *b.DNSBypassTTL)
+	}
+	if (a.DSCP == nil && b.DSCP != nil) || (a.DSCP != nil && b.DSCP == nil) {
+		t.Error("Mismatched 'DSCP' property; one was nil but the other was not.")
+	} else if a.DSCP != nil && *a.DSCP != *b.DSCP {
+		t.Errorf("Mismatched 'DSCP' property; one was '%v', the other was '%v'", *a.DSCP, *b.DSCP)
+	}
+	if a.EcsEnabled != b.EcsEnabled {
+		t.Errorf("Mismatched 'EcsEnabled' property; one was '%v', the other was '%v'", a.EcsEnabled, b.EcsEnabled)
+	}
+	if (a.EdgeHeaderRewrite == nil && b.EdgeHeaderRewrite != nil) || (a.EdgeHeaderRewrite != nil && b.EdgeHeaderRewrite == nil) {
+		t.Error("Mismatched 'EdgeHeaderRewrite' property; one was nil but the other was not.")
+	} else if a.EdgeHeaderRewrite != nil && *a.EdgeHeaderRewrite != *b.EdgeHeaderRewrite {
+		t.Errorf("Mismatched 'EdgeHeaderRewrite' property; one was '%v', the other was '%v'", *a.EdgeHeaderRewrite, *b.EdgeHeaderRewrite)
+	}
+	if len(a.ExampleURLs) != len(b.ExampleURLs) {
+		t.Errorf("Mismatched 'ExampleURLs' property; one contained %d but the other contained %d", len(a.ExampleURLs), len(b.ExampleURLs))
+	} else {
+		for i, eu := range a.ExampleURLs {
+			if eu != b.ExampleURLs[i] {
+				t.Errorf("Mismatched 'ExampleURLs[%d]' property; one was '%v', the other was '%v'", i, eu, b.ExampleURLs[i])
+			}
+		}
+	}
+	if (a.FirstHeaderRewrite == nil && b.FirstHeaderRewrite != nil) || (a.FirstHeaderRewrite != nil && b.FirstHeaderRewrite == nil) {
+		t.Error("Mismatched 'FirstHeaderRewrite' property; one was nil but the other was not.")
+	} else if a.FirstHeaderRewrite != nil && *a.FirstHeaderRewrite != *b.FirstHeaderRewrite {
+		t.Errorf("Mismatched 'FirstHeaderRewrite' property; one was '%v', the other was '%v'", *a.FirstHeaderRewrite, *b.FirstHeaderRewrite)
+	}
+	if (a.FQPacingRate == nil && b.FQPacingRate != nil) || (a.FQPacingRate != nil && b.FQPacingRate == nil) {
+		t.Error("Mismatched 'FQPacingRate' property; one was nil but the other was not.")
+	} else if a.FQPacingRate != nil && *a.FQPacingRate != *b.FQPacingRate {
+		t.Errorf("Mismatched 'FQPacingRate' property; one was '%v', the other was '%v'", *a.FQPacingRate, *b.FQPacingRate)
+	}
+	if (a.GeoLimit == nil && b.GeoLimit != nil) || (a.GeoLimit != nil && b.GeoLimit == nil) {
+		t.Error("Mismatched 'GeoLimit' property; one was nil but the other was not.")
+	} else if a.GeoLimit != nil && *a.GeoLimit != *b.GeoLimit {
+		t.Errorf("Mismatched 'GeoLimit' property; one was '%v', the other was '%v'", *a.GeoLimit, *b.GeoLimit)
+	}
+	if (a.GeoLimitCountries == nil && b.GeoLimitCountries != nil) || (a.GeoLimitCountries != nil && b.GeoLimitCountries == nil) {
+		t.Error("Mismatched 'GeoLimitCountries' property; one was nil but the other was not.")
+	} else if a.GeoLimitCountries != nil && *a.GeoLimitCountries != *b.GeoLimitCountries {
+		t.Errorf("Mismatched 'GeoLimitCountries' property; one was '%v', the other was '%v'", *a.GeoLimitCountries, *b.GeoLimitCountries)
+	}
+	if (a.GeoLimitRedirectURL == nil && b.GeoLimitRedirectURL != nil) || (a.GeoLimitRedirectURL != nil && b.GeoLimitRedirectURL == nil) {
+		t.Error("Mismatched 'GeoLimitRedirectURL' property; one was nil but the other was not.")
+	} else if a.GeoLimitRedirectURL != nil && *a.GeoLimitRedirectURL != *b.GeoLimitRedirectURL {
+		t.Errorf("Mismatched 'GeoLimitRedirectURL' property; one was '%v', the other was '%v'", *a.GeoLimitRedirectURL, *b.GeoLimitRedirectURL)
+	}
+	if (a.GeoProvider == nil && b.GeoProvider != nil) || (a.GeoProvider != nil && b.GeoProvider == nil) {
+		t.Error("Mismatched 'GeoProvider' property; one was nil but the other was not.")
+	} else if a.GeoProvider != nil && *a.GeoProvider != *b.GeoProvider {
+		t.Errorf("Mismatched 'GeoProvider' property; one was '%v', the other was '%v'", *a.GeoProvider, *b.GeoProvider)
+	}
+	if (a.GlobalMaxMBPS == nil && b.GlobalMaxMBPS != nil) || (a.GlobalMaxMBPS != nil && b.GlobalMaxMBPS == nil) {
+		t.Error("Mismatched 'GlobalMaxMBPS' property; one was nil but the other was not.")
+	} else if a.GlobalMaxMBPS != nil && *a.GlobalMaxMBPS != *b.GlobalMaxMBPS {
+		t.Errorf("Mismatched 'GlobalMaxMBPS' property; one was '%v', the other was '%v'", *a.GlobalMaxMBPS, *b.GlobalMaxMBPS)
+	}
+	if (a.GlobalMaxTPS == nil && b.GlobalMaxTPS != nil) || (a.GlobalMaxTPS != nil && b.GlobalMaxTPS == nil) {
+		t.Error("Mismatched 'GlobalMaxTPS' property; one was nil but the other was not.")
+	} else if a.GlobalMaxTPS != nil && *a.GlobalMaxTPS != *b.GlobalMaxTPS {
+		t.Errorf("Mismatched 'GlobalMaxTPS' property; one was '%v', the other was '%v'", *a.GlobalMaxTPS, *b.GlobalMaxTPS)
+	}
+	if (a.HTTPBypassFQDN == nil && b.HTTPBypassFQDN != nil) || (a.HTTPBypassFQDN != nil && b.HTTPBypassFQDN == nil) {
+		t.Error("Mismatched 'HTTPBypassFQDN' property; one was nil but the other was not.")
+	} else if a.HTTPBypassFQDN != nil && *a.HTTPBypassFQDN != *b.HTTPBypassFQDN {
+		t.Errorf("Mismatched 'HTTPBypassFQDN' property; one was '%v', the other was '%v'", *a.HTTPBypassFQDN, *b.HTTPBypassFQDN)
+	}
+	if (a.ID == nil && b.ID != nil) || (a.ID != nil && b.ID == nil) {
+		t.Error("Mismatched 'ID' property; one was nil but the other was not.")
+	} else if a.ID != nil && *a.ID != *b.ID {
+		t.Errorf("Mismatched 'ID' property; one was '%v', the other was '%v'", *a.ID, *b.ID)
+	}
+	if (a.InfoURL == nil && b.InfoURL != nil) || (a.InfoURL != nil && b.InfoURL == nil) {
+		t.Error("Mismatched 'InfoURL' property; one was nil but the other was not.")
+	} else if a.InfoURL != nil && *a.InfoURL != *b.InfoURL {
+		t.Errorf("Mismatched 'InfoURL' property; one was '%v', the other was '%v'", *a.InfoURL, *b.InfoURL)
+	}
+	if (a.InitialDispersion == nil && b.InitialDispersion != nil) || (a.InitialDispersion != nil && b.InitialDispersion == nil) {
+		t.Error("Mismatched 'InitialDispersion' property; one was nil but the other was not.")
+	} else if a.InitialDispersion != nil && *a.InitialDispersion != *b.InitialDispersion {
+		t.Errorf("Mismatched 'InitialDispersion' property; one was '%v', the other was '%v'", *a.InitialDispersion, *b.InitialDispersion)
+	}
+	if (a.InnerHeaderRewrite == nil && b.InnerHeaderRewrite != nil) || (a.InnerHeaderRewrite != nil && b.InnerHeaderRewrite == nil) {
+		t.Error("Mismatched 'InnerHeaderRewrite' property; one was nil but the other was not.")
+	} else if a.InnerHeaderRewrite != nil && *a.InnerHeaderRewrite != *b.InnerHeaderRewrite {
+		t.Errorf("Mismatched 'InnerHeaderRewrite' property; one was '%v', the other was '%v'", *a.InnerHeaderRewrite, *b.InnerHeaderRewrite)
+	}
+	if (a.IPV6RoutingEnabled == nil && b.IPV6RoutingEnabled != nil) || (a.IPV6RoutingEnabled != nil && b.IPV6RoutingEnabled == nil) {
+		t.Error("Mismatched 'IPV6RoutingEnabled' property; one was nil but the other was not.")
+	} else if a.IPV6RoutingEnabled != nil && *a.IPV6RoutingEnabled != *b.IPV6RoutingEnabled {
+		t.Errorf("Mismatched 'IPV6RoutingEnabled' property; one was '%v', the other was '%v'", *a.IPV6RoutingEnabled, *b.IPV6RoutingEnabled)
+	}
+	if (a.LastHeaderRewrite == nil && b.LastHeaderRewrite != nil) || (a.LastHeaderRewrite != nil && b.LastHeaderRewrite == nil) {
+		t.Error("Mismatched 'LastHeaderRewrite' property; one was nil but the other was not.")
+	} else if a.LastHeaderRewrite != nil && *a.LastHeaderRewrite != *b.LastHeaderRewrite {
+		t.Errorf("Mismatched 'LastHeaderRewrite' property; one was '%v', the other was '%v'", *a.LastHeaderRewrite, *b.LastHeaderRewrite)
+	}
+	if (a.LastUpdated == nil && b.LastUpdated != nil) || (a.LastUpdated != nil && b.LastUpdated == nil) {
+		t.Error("Mismatched 'LastUpdated' property; one was nil but the other was not.")
+	} else if a.LastUpdated != nil && *a.LastUpdated != *b.LastUpdated {
+		t.Errorf("Mismatched 'LastUpdated' property; one was '%v', the other was '%v'", *a.LastUpdated, *b.LastUpdated)
+	}
+	if (a.LogsEnabled == nil && b.LogsEnabled != nil) || (a.LogsEnabled != nil && b.LogsEnabled == nil) {
+		t.Error("Mismatched 'LogsEnabled' property; one was nil but the other was not.")
+	} else if a.LogsEnabled != nil && *a.LogsEnabled != *b.LogsEnabled {
+		t.Errorf("Mismatched 'LogsEnabled' property; one was '%v', the other was '%v'", *a.LogsEnabled, *b.LogsEnabled)
+	}
+	if (a.LongDesc1 == nil && b.LongDesc1 != nil) || (a.LongDesc1 != nil && b.LongDesc1 == nil) {
+		t.Error("Mismatched 'LongDesc1' property; one was nil but the other was not.")
+	} else if a.LongDesc1 != nil && *a.LongDesc1 != *b.LongDesc1 {
+		t.Errorf("Mismatched 'LongDesc1' property; one was '%v', the other was '%v'", *a.LongDesc1, *b.LongDesc1)
+	}
+	if (a.LongDesc2 == nil && b.LongDesc2 != nil) || (a.LongDesc2 != nil && b.LongDesc2 == nil) {
+		t.Error("Mismatched 'LongDesc2' property; one was nil but the other was not.")
+	} else if a.LongDesc2 != nil && *a.LongDesc2 != *b.LongDesc2 {
+		t.Errorf("Mismatched 'LongDesc2' property; one was '%v', the other was '%v'", *a.LongDesc2, *b.LongDesc2)
+	}
+	if (a.LongDesc == nil && b.LongDesc != nil) || (a.LongDesc != nil && b.LongDesc == nil) {
+		t.Error("Mismatched 'LongDesc' property; one was nil but the other was not.")
+	} else if a.LongDesc != nil && *a.LongDesc != *b.LongDesc {
+		t.Errorf("Mismatched 'LongDesc' property; one was '%v', the other was '%v'", *a.LongDesc, *b.LongDesc)
+	}
+	if (a.MatchList != nil && b.MatchList == nil) || (a.MatchList == nil && b.MatchList != nil) {
+		t.Error("Mismatched 'MatchList' property; one was nil but the other was not")
+	} else if a.MatchList != nil {
+		if len(*a.MatchList) != len(*b.MatchList) {
+			t.Errorf("Mismatched 'MatchList' property; one contained %d but the other contained %d", len(*a.MatchList), len(*b.MatchList))
+		} else {
+			for i, m := range *a.MatchList {
+				if m != (*b.MatchList)[i] {
+					t.Errorf("Mismatched 'MatchList[%d]' property; one was '%v', the other was '%v'", i, m, (*b.MatchList)[i])
+				}
+			}
+		}
+	}
+	if (a.MaxDNSAnswers == nil && b.MaxDNSAnswers != nil) || (a.MaxDNSAnswers != nil && b.MaxDNSAnswers == nil) {
+		t.Error("Mismatched 'MaxDNSAnswers' property; one was nil but the other was not.")
+	} else if a.MaxDNSAnswers != nil && *a.MaxDNSAnswers != *b.MaxDNSAnswers {
+		t.Errorf("Mismatched 'MaxDNSAnswers' property; one was '%v', the other was '%v'", *a.MaxDNSAnswers, *b.MaxDNSAnswers)
+	}
+	if (a.MaxOriginConnections == nil && b.MaxOriginConnections != nil) || (a.MaxOriginConnections != nil && b.MaxOriginConnections == nil) {
+		t.Error("Mismatched 'MaxOriginConnections' property; one was nil but the other was not.")
+	} else if a.MaxOriginConnections != nil && *a.MaxOriginConnections != *b.MaxOriginConnections {
+		t.Errorf("Mismatched 'MaxOriginConnections' property; one was '%v', the other was '%v'", *a.MaxOriginConnections, *b.MaxOriginConnections)
+	}
+	if (a.MaxRequestHeaderBytes == nil && b.MaxRequestHeaderBytes != nil) || (a.MaxRequestHeaderBytes != nil && b.MaxRequestHeaderBytes == nil) {
+		t.Error("Mismatched 'MaxRequestHeaderBytes' property; one was nil but the other was not.")
+	} else if a.MaxRequestHeaderBytes != nil && *a.MaxRequestHeaderBytes != *b.MaxRequestHeaderBytes {
+		t.Errorf("Mismatched 'MaxRequestHeaderBytes' property; one was '%v', the other was '%v'", *a.MaxRequestHeaderBytes, *b.MaxRequestHeaderBytes)
+	}
+	if (a.MidHeaderRewrite == nil && b.MidHeaderRewrite != nil) || (a.MidHeaderRewrite != nil && b.MidHeaderRewrite == nil) {
+		t.Error("Mismatched 'MidHeaderRewrite' property; one was nil but the other was not.")
+	} else if a.MidHeaderRewrite != nil && *a.MidHeaderRewrite != *b.MidHeaderRewrite {
+		t.Errorf("Mismatched 'MidHeaderRewrite' property; one was '%v', the other was '%v'", *a.MidHeaderRewrite, *b.MidHeaderRewrite)
+	}
+	if (a.MissLat == nil && b.MissLat != nil) || (a.MissLat != nil && b.MissLat == nil) {
+		t.Error("Mismatched 'MissLat' property; one was nil but the other was not.")
+	} else if a.MissLat != nil && *a.MissLat != *b.MissLat {
+		t.Errorf("Mismatched 'MissLat' property; one was '%v', the other was '%v'", *a.MissLat, *b.MissLat)
+	}
+	if (a.MissLong == nil && b.MissLong != nil) || (a.MissLong != nil && b.MissLong == nil) {
+		t.Error("Mismatched 'MissLong' property; one was nil but the other was not.")
+	} else if a.MissLong != nil && *a.MissLong != *b.MissLong {
+		t.Errorf("Mismatched 'MissLong' property; one was '%v', the other was '%v'", *a.MissLong, *b.MissLong)
+	}
+	if (a.MultiSiteOrigin == nil && b.MultiSiteOrigin != nil) || (a.MultiSiteOrigin != nil && b.MultiSiteOrigin == nil) {
+		t.Error("Mismatched 'MultiSiteOrigin' property; one was nil but the other was not.")
+	} else if a.MultiSiteOrigin != nil && *a.MultiSiteOrigin != *b.MultiSiteOrigin {
+		t.Errorf("Mismatched 'MultiSiteOrigin' property; one was '%v', the other was '%v'", *a.MultiSiteOrigin, *b.MultiSiteOrigin)
+	}
+	if (a.OrgServerFQDN == nil && b.OrgServerFQDN != nil) || (a.OrgServerFQDN != nil && b.OrgServerFQDN == nil) {
+		t.Error("Mismatched 'OrgServerFQDN' property; one was nil but the other was not.")
+	} else if a.OrgServerFQDN != nil && *a.OrgServerFQDN != *b.OrgServerFQDN {
+		t.Errorf("Mismatched 'OrgServerFQDN' property; one was '%v', the other was '%v'", *a.OrgServerFQDN, *b.OrgServerFQDN)
+	}
+	if (a.OriginShield == nil && b.OriginShield != nil) || (a.OriginShield != nil && b.OriginShield == nil) {
+		t.Error("Mismatched 'OriginShield' property; one was nil but the other was not.")
+	} else if a.OriginShield != nil && *a.OriginShield != *b.OriginShield {
+		t.Errorf("Mismatched 'OriginShield' property; one was '%v', the other was '%v'", *a.OriginShield, *b.OriginShield)
+	}
+	if (a.ProfileDesc == nil && b.ProfileDesc != nil) || (a.ProfileDesc != nil && b.ProfileDesc == nil) {
+		t.Error("Mismatched 'ProfileDesc' property; one was nil but the other was not.")
+	} else if a.ProfileDesc != nil && *a.ProfileDesc != *b.ProfileDesc {
+		t.Errorf("Mismatched 'ProfileDesc' property; one was '%v', the other was '%v'", *a.ProfileDesc, *b.ProfileDesc)
+	}
+	if (a.ProfileID == nil && b.ProfileID != nil) || (a.ProfileID != nil && b.ProfileID == nil) {
+		t.Error("Mismatched 'ProfileID' property; one was nil but the other was not.")
+	} else if a.ProfileID != nil && *a.ProfileID != *b.ProfileID {
+		t.Errorf("Mismatched 'ProfileID' property; one was '%v', the other was '%v'", *a.ProfileID, *b.ProfileID)
+	}
+	if (a.ProfileName == nil && b.ProfileName != nil) || (a.ProfileName != nil && b.ProfileName == nil) {
+		t.Error("Mismatched 'ProfileName' property; one was nil but the other was not.")
+	} else if a.ProfileName != nil && *a.ProfileName != *b.ProfileName {
+		t.Errorf("Mismatched 'ProfileName' property; one was '%v', the other was '%v'", *a.ProfileName, *b.ProfileName)
+	}
+	if (a.Protocol == nil && b.Protocol != nil) || (a.Protocol != nil && b.Protocol == nil) {
+		t.Error("Mismatched 'Protocol' property; one was nil but the other was not.")
+	} else if a.Protocol != nil && *a.Protocol != *b.Protocol {
+		t.Errorf("Mismatched 'Protocol' property; one was '%v', the other was '%v'", *a.Protocol, *b.Protocol)
+	}
+	if (a.QStringIgnore == nil && b.QStringIgnore != nil) || (a.QStringIgnore != nil && b.QStringIgnore == nil) {
+		t.Error("Mismatched 'QStringIgnore' property; one was nil but the other was not.")
+	} else if a.QStringIgnore != nil && *a.QStringIgnore != *b.QStringIgnore {
+		t.Errorf("Mismatched 'QStringIgnore' property; one was '%v', the other was '%v'", *a.QStringIgnore, *b.QStringIgnore)
+	}
+	if (a.RangeRequestHandling == nil && b.RangeRequestHandling != nil) || (a.RangeRequestHandling != nil && b.RangeRequestHandling == nil) {
+		t.Error("Mismatched 'RangeRequestHandling' property; one was nil but the other was not.")
+	} else if a.RangeRequestHandling != nil && *a.RangeRequestHandling != *b.RangeRequestHandling {
+		t.Errorf("Mismatched 'RangeRequestHandling' property; one was '%v', the other was '%v'", *a.RangeRequestHandling, *b.RangeRequestHandling)
+	}
+	if (a.RangeSliceBlockSize == nil && b.RangeSliceBlockSize != nil) || (a.RangeSliceBlockSize != nil && b.RangeSliceBlockSize == nil) {
+		t.Error("Mismatched 'RangeSliceBlockSize' property; one was nil but the other was not.")
+	} else if a.RangeSliceBlockSize != nil && *a.RangeSliceBlockSize != *b.RangeSliceBlockSize {
+		t.Errorf("Mismatched 'RangeSliceBlockSize' property; one was '%v', the other was '%v'", *a.RangeSliceBlockSize, *b.RangeSliceBlockSize)
+	}
+	if (a.RegexRemap == nil && b.RegexRemap != nil) || (a.RegexRemap != nil && b.RegexRemap == nil) {
+		t.Error("Mismatched 'RegexRemap' property; one was nil but the other was not.")
+	} else if a.RegexRemap != nil && *a.RegexRemap != *b.RegexRemap {
+		t.Errorf("Mismatched 'RegexRemap' property; one was '%v', the other was '%v'", *a.RegexRemap, *b.RegexRemap)
+	}
+	if (a.RegionalGeoBlocking == nil && b.RegionalGeoBlocking != nil) || (a.RegionalGeoBlocking != nil && b.RegionalGeoBlocking == nil) {
+		t.Error("Mismatched 'RegionalGeoBlocking' property; one was nil but the other was not.")
+	} else if a.RegionalGeoBlocking != nil && *a.RegionalGeoBlocking != *b.RegionalGeoBlocking {
+		t.Errorf("Mismatched 'RegionalGeoBlocking' property; one was '%v', the other was '%v'", *a.RegionalGeoBlocking, *b.RegionalGeoBlocking)
+	}
+	if (a.RemapText == nil && b.RemapText != nil) || (a.RemapText != nil && b.RemapText == nil) {
+		t.Error("Mismatched 'RemapText' property; one was nil but the other was not.")
+	} else if a.RemapText != nil && *a.RemapText != *b.RemapText {
+		t.Errorf("Mismatched 'RemapText' property; one was '%v', the other was '%v'", *a.RemapText, *b.RemapText)
+	}
+	if (a.RoutingName == nil && b.RoutingName != nil) || (a.RoutingName != nil && b.RoutingName == nil) {
+		t.Error("Mismatched 'RoutingName' property; one was nil but the other was not.")
+	} else if a.RoutingName != nil && *a.RoutingName != *b.RoutingName {
+		t.Errorf("Mismatched 'RoutingName' property; one was '%v', the other was '%v'", *a.RoutingName, *b.RoutingName)
+	}
+	if (a.ServiceCategory == nil && b.ServiceCategory != nil) || (a.ServiceCategory != nil && b.ServiceCategory == nil) {
+		t.Error("Mismatched 'ServiceCategory' property; one was nil but the other was not.")
+	} else if a.ServiceCategory != nil && *a.ServiceCategory != *b.ServiceCategory {
+		t.Errorf("Mismatched 'ServiceCategory' property; one was '%v', the other was '%v'", *a.ServiceCategory, *b.ServiceCategory)
+	}
+	if a.Signed != b.Signed {
+		t.Errorf("Mismatched 'Signed' property; one was '%v', the other was '%v'", a.Signed, b.Signed)
+	}
+	if (a.SigningAlgorithm == nil && b.SigningAlgorithm != nil) || (a.SigningAlgorithm != nil && b.SigningAlgorithm == nil) {
+		t.Error("Mismatched 'SigningAlgorithm' property; one was nil but the other was not.")
+	} else if a.SigningAlgorithm != nil && *a.SigningAlgorithm != *b.SigningAlgorithm {
+		t.Errorf("Mismatched 'SigningAlgorithm' property; one was '%v', the other was '%v'", *a.SigningAlgorithm, *b.SigningAlgorithm)
+	}
+	if (a.SSLKeyVersion == nil && b.SSLKeyVersion != nil) || (a.SSLKeyVersion != nil && b.SSLKeyVersion == nil) {
+		t.Error("Mismatched 'SSLKeyVersion' property; one was nil but the other was not.")
+	} else if a.SSLKeyVersion != nil && *a.SSLKeyVersion != *b.SSLKeyVersion {
+		t.Errorf("Mismatched 'SSLKeyVersion' property; one was '%v', the other was '%v'", *a.SSLKeyVersion, *b.SSLKeyVersion)
+	}
+	if (a.Tenant == nil && b.Tenant != nil) || (a.Tenant != nil && b.Tenant == nil) {
+		t.Error("Mismatched 'Tenant' property; one was nil but the other was not.")
+	} else if a.Tenant != nil && *a.Tenant != *b.Tenant {
+		t.Errorf("Mismatched 'Tenant' property; one was '%v', the other was '%v'", *a.Tenant, *b.Tenant)
+	}
+	if (a.TenantID == nil && b.TenantID != nil) || (a.TenantID != nil && b.TenantID == nil) {
+		t.Error("Mismatched 'TenantID' property; one was nil but the other was not.")
+	} else if a.TenantID != nil && *a.TenantID != *b.TenantID {
+		t.Errorf("Mismatched 'TenantID' property; one was '%v', the other was '%v'", *a.TenantID, *b.TenantID)
+	}
+	if (a.Topology == nil && b.Topology != nil) || (a.Topology != nil && b.Topology == nil) {
+		t.Error("Mismatched 'Topology' property; one was nil but the other was not.")
+	} else if a.Topology != nil && *a.Topology != *b.Topology {
+		t.Errorf("Mismatched 'Topology' property; one was '%v', the other was '%v'", *a.Topology, *b.Topology)
+	}
+	if (a.TRRequestHeaders == nil && b.TRRequestHeaders != nil) || (a.TRRequestHeaders != nil && b.TRRequestHeaders == nil) {
+		t.Error("Mismatched 'TRRequestHeaders' property; one was nil but the other was not.")
+	} else if a.TRRequestHeaders != nil && *a.TRRequestHeaders != *b.TRRequestHeaders {
+		t.Errorf("Mismatched 'TRRequestHeaders' property; one was '%v', the other was '%v'", *a.TRRequestHeaders, *b.TRRequestHeaders)
+	}
+	if (a.TRResponseHeaders == nil && b.TRResponseHeaders != nil) || (a.TRResponseHeaders != nil && b.TRResponseHeaders == nil) {
+		t.Error("Mismatched 'TRResponseHeaders' property; one was nil but the other was not.")
+	} else if a.TRResponseHeaders != nil && *a.TRResponseHeaders != *b.TRResponseHeaders {
+		t.Errorf("Mismatched 'TRResponseHeaders' property; one was '%v', the other was '%v'", *a.TRResponseHeaders, *b.TRResponseHeaders)
+	}
+	if (a.Type == nil && b.Type != nil) || (a.Type != nil && b.Type == nil) {
+		t.Error("Mismatched 'Type' property; one was nil but the other was not.")
+	} else if a.Type != nil && *a.Type != *b.Type {
+		t.Errorf("Mismatched 'Type' property; one was '%v', the other was '%v'", *a.Type, *b.Type)
+	}
+	if (a.TypeID == nil && b.TypeID != nil) || (a.TypeID != nil && b.TypeID == nil) {
+		t.Error("Mismatched 'TypeID' property; one was nil but the other was not.")
+	} else if a.TypeID != nil && *a.TypeID != *b.TypeID {
+		t.Errorf("Mismatched 'TypeID' property; one was '%v', the other was '%v'", *a.TypeID, *b.TypeID)
+	}
+	if (a.XMLID == nil && b.XMLID != nil) || (a.XMLID != nil && b.XMLID == nil) {
+		t.Error("Mismatched 'XMLID' property; one was nil but the other was not.")
+	} else if a.XMLID != nil && *a.XMLID != *b.XMLID {
+		t.Errorf("Mismatched 'XMLID' property; one was '%v', the other was '%v'", *a.XMLID, *b.XMLID)
+	}
+}
+
+// This gets equivalent legacy and new Delivery Services, for testing comparisons.
+func dsUpgradeAndDowngradeTestingPair() (DeliveryServiceNullableV30, DeliveryServiceV4) {
+	anonymousBlockingEnabled := false
+	cacheURL := "testquest"
+	cCRDNSTTL := 42
+	cdnID := -12
+	cdnName := "cdnName"
+	checkPath := "checkPath"
+	consistentHashQueryParams := []string{"consistent", "hash", "query", "params"}
+	consistentHashRegex := "consistentHashRegex"
+	deepCachingType := DeepCachingTypeNever
+	displayName := "displayName"
+	dnsBypassCNAME := "dnsBypassCNAME"
+	dnsBypassIP := "dnsBypassIP"
+	dnsBypassIP6 := "dnsBypassIP6"
+	dnsBypassTTL := 100
+	dscp := -69
+	ecsEnabled := true
+	edgeHeaderRewrite := "edgeHeaderRewrite"
+	exampleURLs := []string{"http://example", "https://URLs"}
+	firstHeaderRewrite := "firstHeaderRewrite"
+	fqPacingRate := 1337
+	geoLimit := 2
+	geoLimitCountries := "geo,Limit,Countries"
+	geoLimitRedirectURL := "wss://geoLimitRedirectURL"
+	geoProvider := 1
+	globalMaxMBPS := -72485
+	globalMaxTPS := 867
+	hTTPBypassFQDN := "hTTPBypassFQDN"
+	id := -1551
+	infoURL := "infoURL"
+	initialDispersion := 65154
+	innerHeaderRewrite := "innerHeaderRewrite"
+	ipv6RoutingEnabled := false
+	lastHeaderRewrite := "lastHeaderRewrite"
+	lastUpdated := NewTimeNoMod()
+	logsEnabled := true
+	longDesc := "longDesc"
+	longDesc1 := "longDesc1"
+	longDesc2 := "longDesc2"
+	maxDNSAnswers := -76675
+	maxOriginConnections := 6514684
+	maxRequestHeaderBytes := 555
+	midHeaderRewrite := "midHeaderRewrite"
+	missLat := -98.171455
+	missLong := 42.122167
+	multiSiteOrigin := false
+	originShield := "originShield"
+	orgServerFQDN := "orgServerFQDN"
+	profileDesc := "profileDesc"
+	profileID := -4657
+	profileName := "profileName"
+	protocol := 87487
+	qstringIgnore := -474
+	rangeRequestHandling := 16716
+	rangeSliceBlockSize := -92559
+	regexRemap := "regexRemap"
+	regionalGeoBlocking := true
+	remapText := "remapText"
+	routingName := "routingName"
+	serviceCategory := "serviceCategory"
+	signed := false
+	signingAlgorithm := "signingAlgorithm"
+	sSLKeyVersion := 721574
+	tenant := "tenant"
+	tenantID := -6551
+	topology := "topology"
+	trResponseHeaders := "trResponseHeaders"
+	trRequestHeaders := "trRequestHeaders"
+	typ := DSTypeDNS
+	typeID := 22
+	xmlid := "xmlid"
+
+	newDS := DeliveryServiceV4{}
+	newDS.Active = new(bool)
+	newDS.AnonymousBlockingEnabled = &anonymousBlockingEnabled
+	newDS.CCRDNSTTL = &cCRDNSTTL
+	newDS.CDNID = &cdnID
+	newDS.CDNName = &cdnName
+	newDS.CheckPath = &checkPath
+	newDS.ConsistentHashQueryParams = consistentHashQueryParams
+	newDS.ConsistentHashRegex = &consistentHashRegex
+	newDS.DeepCachingType = &deepCachingType
+	newDS.DisplayName = &displayName
+	newDS.DNSBypassCNAME = &dnsBypassCNAME
+	newDS.DNSBypassIP = &dnsBypassIP
+	newDS.DNSBypassIP6 = &dnsBypassIP6
+	newDS.DNSBypassTTL = &dnsBypassTTL
+	newDS.DSCP = &dscp
+	newDS.EcsEnabled = ecsEnabled
+	newDS.EdgeHeaderRewrite = &edgeHeaderRewrite
+	newDS.ExampleURLs = exampleURLs
+	newDS.FirstHeaderRewrite = &firstHeaderRewrite
+	newDS.FQPacingRate = &fqPacingRate
+	newDS.GeoLimit = &geoLimit
+	newDS.GeoLimitCountries = &geoLimitCountries
+	newDS.GeoLimitRedirectURL = &geoLimitRedirectURL
+	newDS.GeoProvider = &geoProvider
+	newDS.GlobalMaxMBPS = &globalMaxMBPS
+	newDS.GlobalMaxTPS = &globalMaxTPS
+	newDS.HTTPBypassFQDN = &hTTPBypassFQDN
+	newDS.ID = &id
+	newDS.InfoURL = &infoURL
+	newDS.InitialDispersion = &initialDispersion
+	newDS.InnerHeaderRewrite = &innerHeaderRewrite
+	newDS.IPV6RoutingEnabled = &ipv6RoutingEnabled
+	newDS.LastHeaderRewrite = &lastHeaderRewrite
+	newDS.LastUpdated = lastUpdated
+	newDS.LogsEnabled = &logsEnabled
+	newDS.LongDesc = &longDesc
+	newDS.LongDesc1 = &longDesc1
+	newDS.LongDesc2 = &longDesc2
+	newDS.MatchList = nil
+	newDS.MaxDNSAnswers = &maxDNSAnswers
+	newDS.MaxOriginConnections = &maxOriginConnections
+	newDS.MaxRequestHeaderBytes = &maxRequestHeaderBytes
+	newDS.MidHeaderRewrite = &midHeaderRewrite
+	newDS.MissLat = &missLat
+	newDS.MissLong = &missLong
+	newDS.MultiSiteOrigin = &multiSiteOrigin
+	newDS.OriginShield = &originShield
+	newDS.OrgServerFQDN = &orgServerFQDN
+	newDS.ProfileDesc = &profileDesc
+	newDS.ProfileID = &profileID
+	newDS.ProfileName = &profileName
+	newDS.Protocol = &protocol
+	newDS.QStringIgnore = &qstringIgnore
+	newDS.RangeRequestHandling = &rangeRequestHandling
+	newDS.RangeSliceBlockSize = &rangeSliceBlockSize
+	newDS.RegexRemap = &regexRemap
+	newDS.RegionalGeoBlocking = &regionalGeoBlocking
+	newDS.RemapText = &remapText
+	newDS.RoutingName = &routingName
+	newDS.ServiceCategory = &serviceCategory
+	newDS.Signed = signed
+	newDS.SigningAlgorithm = &signingAlgorithm
+	newDS.SSLKeyVersion = &sSLKeyVersion
+	newDS.Tenant = &tenant
+	newDS.TenantID = &tenantID
+	newDS.TLSVersions = []string{"1.0", "1.1", "1.2", "1.3"}
+	newDS.Topology = &topology
+	newDS.TRResponseHeaders = &trResponseHeaders
+	newDS.TRRequestHeaders = &trRequestHeaders
+	newDS.Type = &typ
+	newDS.TypeID = &typeID
+	newDS.XMLID = &xmlid
+
+	active := false
+	oldDS := DeliveryServiceNullableV30{
+		DeliveryServiceV30: DeliveryServiceV30{
+			DeliveryServiceNullableV15: DeliveryServiceNullableV15{
+				DeliveryServiceNullableV14: DeliveryServiceNullableV14{
+					DeliveryServiceNullableV13: DeliveryServiceNullableV13{
+						DeliveryServiceNullableV12: DeliveryServiceNullableV12{
+							DeliveryServiceNullableV11: DeliveryServiceNullableV11{
+								DeliveryServiceNullableFieldsV11: DeliveryServiceNullableFieldsV11{
+									Active:                   &active,
+									AnonymousBlockingEnabled: &anonymousBlockingEnabled,
+									CCRDNSTTL:                &cCRDNSTTL,
+									CDNID:                    &cdnID,
+									CDNName:                  &cdnName,
+									CheckPath:                &checkPath,
+									DisplayName:              &displayName,
+									DNSBypassCNAME:           &dnsBypassCNAME,
+									DNSBypassIP:              &dnsBypassIP,
+									DNSBypassIP6:             &dnsBypassIP6,
+									DNSBypassTTL:             &dnsBypassTTL,
+									DSCP:                     &dscp,
+									EdgeHeaderRewrite:        &edgeHeaderRewrite,
+									ExampleURLs:              exampleURLs,
+									GeoLimit:                 &geoLimit,
+									GeoLimitCountries:        &geoLimitCountries,
+									GeoLimitRedirectURL:      &geoLimitRedirectURL,
+									GeoProvider:              &geoProvider,
+									GlobalMaxMBPS:            &globalMaxMBPS,
+									GlobalMaxTPS:             &globalMaxTPS,
+									HTTPBypassFQDN:           &hTTPBypassFQDN,
+									ID:                       &id,
+									InfoURL:                  &infoURL,
+									InitialDispersion:        &initialDispersion,
+									IPV6RoutingEnabled:       &ipv6RoutingEnabled,
+									LastUpdated:              lastUpdated,
+									LogsEnabled:              &logsEnabled,
+									LongDesc:                 &longDesc,
+									LongDesc1:                &longDesc1,
+									LongDesc2:                &longDesc2,
+									MatchList:                nil,
+									MaxDNSAnswers:            &maxDNSAnswers,
+									MidHeaderRewrite:         &midHeaderRewrite,
+									MissLat:                  &missLat,
+									MissLong:                 &missLong,
+									MultiSiteOrigin:          &multiSiteOrigin,
+									OriginShield:             &originShield,
+									OrgServerFQDN:            &orgServerFQDN,
+									ProfileDesc:              &profileDesc,
+									ProfileID:                &profileID,
+									ProfileName:              &profileName,
+									Protocol:                 &protocol,
+									QStringIgnore:            &qstringIgnore,
+									RangeRequestHandling:     &rangeRequestHandling,
+									RegexRemap:               &regexRemap,
+									RegionalGeoBlocking:      &regionalGeoBlocking,
+									RemapText:                &remapText,
+									RoutingName:              &routingName,
+									Signed:                   signed,
+									SSLKeyVersion:            &sSLKeyVersion,
+									TenantID:                 &tenantID,
+									Type:                     &typ,
+									TypeID:                   &typeID,
+									XMLID:                    &xmlid,
+								},
+								DeliveryServiceRemovedFieldsV11: DeliveryServiceRemovedFieldsV11{
+									CacheURL: &cacheURL,
+								},
+							},
+						},
+						DeliveryServiceFieldsV13: DeliveryServiceFieldsV13{
+							DeepCachingType:   &deepCachingType,
+							FQPacingRate:      &fqPacingRate,
+							SigningAlgorithm:  &signingAlgorithm,
+							Tenant:            &tenant,
+							TRResponseHeaders: &trResponseHeaders,
+							TRRequestHeaders:  &trRequestHeaders,
+						},
+					},
+					DeliveryServiceFieldsV14: DeliveryServiceFieldsV14{
+						ConsistentHashQueryParams: consistentHashQueryParams,
+						ConsistentHashRegex:       &consistentHashRegex,
+						MaxOriginConnections:      &maxOriginConnections,
+					},
+				},
+				DeliveryServiceFieldsV15: DeliveryServiceFieldsV15{
+					EcsEnabled:          ecsEnabled,
+					RangeSliceBlockSize: &rangeSliceBlockSize,
+				},
+			},
+			DeliveryServiceFieldsV30: DeliveryServiceFieldsV30{
+				FirstHeaderRewrite: &firstHeaderRewrite,
+				InnerHeaderRewrite: &innerHeaderRewrite,
+				LastHeaderRewrite:  &lastHeaderRewrite,
+				ServiceCategory:    &serviceCategory,
+				Topology:           &topology,
+			},
+		},
+		DeliveryServiceFieldsV31: DeliveryServiceFieldsV31{
+			MaxRequestHeaderBytes: &maxRequestHeaderBytes,
+		},
+	}
+
+	return oldDS, newDS
+}
+
+func TestDeliveryServiceUpgradeAndDowngrade(t *testing.T) {
+	oldDS, newDS := dsUpgradeAndDowngradeTestingPair()
+	compareV31DSes(oldDS, newDS.DowngradeToV3(), t)
+
+	nullableOldDS := DeliveryServiceNullableV30(oldDS)
+	upgraded := nullableOldDS.UpgradeToV4()
+	compareV31DSes(upgraded.DowngradeToV3(), newDS.DowngradeToV3(), t)
+
+	downgraded := newDS.DowngradeToV3()
+	upgraded = downgraded.UpgradeToV4()
+	downgraded = upgraded.DowngradeToV3()
+	compareV31DSes(oldDS, downgraded, t)
+
+	upgraded = nullableOldDS.UpgradeToV4()
+	downgraded = newDS.DowngradeToV3()
+	tmp := downgraded.UpgradeToV4()
+	downgraded = tmp.DowngradeToV3()
+	compareV31DSes(upgraded.DowngradeToV3(), downgraded, t)
+
+	if oldDS.CacheURL == nil {
+		oldDS.CacheURL = new(string)
+		*oldDS.CacheURL = "testquest"
+	}
+
+	upgraded = oldDS.UpgradeToV4()
+	downgraded = upgraded.DowngradeToV3()
+	if downgraded.CacheURL != nil {
+		t.Error("Expected 'cacheurl' to be null after upgrade then downgrade because it doesn't exist in APIv4, but it wasn't")
+	}
+
+	downgraded = newDS.DowngradeToV3()
+	upgraded = downgraded.UpgradeToV4()
+	if upgraded.TLSVersions != nil {
+		t.Errorf("Expected 'tlsVersions' to be nil after upgrade, because all TLS versions are implicitly supported for an APIv3 DS; found: %v", upgraded.TLSVersions)
+	}
+}
+
+func expectOnlyWarnings(a Alerts) func(*testing.T) {
+	return func(t *testing.T) {
+		found := 0
+		for _, alert := range a.Alerts {
+			if alert.Level != WarnLevel.String() {
+				found++
+			}
+		}
+
+		if found > 0 {
+			t.Errorf("Expected only warning-level alerts, found %d that were not warning-level: %v", found, a.Alerts)
+		}
+	}
+}
+
+func containsWarning(a Alerts, expected string) func(*testing.T) {
+	return func(t *testing.T) {
+		for _, alert := range a.Alerts {
+			if alert.Level == WarnLevel.String() && alert.Text == expected {
+				return
+			}
+		}
+		t.Errorf("Expected to find a warning-level Alert containing the message '%s', but didn't: %v", expected, a.Alerts)
+	}
+}
+
+const nonNilTLSVersionsWarningMessage = "setting TLS Versions that are explicitly supported may break older clients that can't use the specified versions"
+
+func TestTLSVersionsAlerts(t *testing.T) {
+	var ds DeliveryServiceV40
+	alerts := ds.TLSVersionsAlerts()
+	if alerts.HasAlerts() {
+		t.Errorf("nil versions should not produce any warnings, but these were generated: %v", alerts.Alerts)
+	}
+	ds.TLSVersions = make([]string, 0, 3)
+	alerts = ds.TLSVersionsAlerts()
+	if alerts.HasAlerts() {
+		t.Errorf("empty versions should not produce any warnings, but these were generated: %v", alerts.Alerts)
+	}
+
+	ds.Protocol = new(int)
+	*ds.Protocol = 0
+	expected := "old TLS version 1.0 is allowed, but newer versions 1.1, 1.2, and 1.3 are disallowed; this configuration may be insecure"
+	ds.TLSVersions = append(ds.TLSVersions, TLSVersion10)
+	alerts = ds.TLSVersionsAlerts()
+	if len(alerts.Alerts) != 3 {
+		t.Errorf("expected allowing only TLS v1.0 to generate exactly three warnings, got %d: %v", len(alerts.Alerts), alerts)
+	} else {
+		t.Run("only TLS version 1.0 allowed - returns warnings", expectOnlyWarnings(alerts))
+		t.Run("only TLS version 1.0 allowed - has expected warning", containsWarning(alerts, expected))
+		expected = "tlsVersions has no effect on Delivery Services with Protocol '0' (HTTP_ONLY)"
+		t.Run("only TLS version 1.0 allowed - has warning about Protocol '0'", containsWarning(alerts, expected))
+		t.Run("only TLS version 1.0 allowed - has warning about non-nil tlsVersions", containsWarning(alerts, nonNilTLSVersionsWarningMessage))
+	}
+
+	ds.Protocol = nil
+	expected = "old TLS version 1.0 is allowed, but newer versions 1.1, and 1.3 are disallowed; this configuration may be insecure"
+	ds.TLSVersions = append(ds.TLSVersions, TLSVersion12)
+	alerts = ds.TLSVersionsAlerts()
+	if len(alerts.Alerts) != 2 {
+		t.Errorf("expected allowing only TLS v1.0 and 1.2 to generate exactly two warnings, got %d: %v", len(alerts.Alerts), alerts)
+	} else {
+		t.Run("only TLS versions 1.0 and 1.2 allowed - returns warnings", expectOnlyWarnings(alerts))
+		t.Run("only TLS versions 1.0 and 1.2 allowed - has expected warning", containsWarning(alerts, expected))
+		t.Run("only TLS versions 1.0 and 1.2 allowed - has warning about non-nil tlsVersions", containsWarning(alerts, nonNilTLSVersionsWarningMessage))
+	}
+
+	expected = "old TLS version 1.0 is allowed, but newer version 1.3 is disallowed; this configuration may be insecure"
+	ds.TLSVersions = append(ds.TLSVersions, TLSVersion11)
+	alerts = ds.TLSVersionsAlerts()
+	if len(alerts.Alerts) != 2 {
+		t.Errorf("expected disallowing only TLS v1.3 to generate exactly two warnings, got %d: %v", len(alerts.Alerts), alerts)
+	} else {
+		t.Run("only TLS version 1.3 disallowed - returns warnings", expectOnlyWarnings(alerts))
+		t.Run("only TLS version 1.3 disallowed - has expected warning", containsWarning(alerts, expected))
+		t.Run("only TLS version 1.3 disallowed - has warning about non-nil tlsVersions", containsWarning(alerts, nonNilTLSVersionsWarningMessage))
+	}
+
+	expected = "old TLS version 1.1 is allowed, but newer versions 1.2, and 1.3 are disallowed; this configuration may be insecure"
+	ds.TLSVersions = make([]string, 0, 2)
+	ds.TLSVersions = append(ds.TLSVersions, TLSVersion11)
+	alerts = ds.TLSVersionsAlerts()
+	if len(alerts.Alerts) != 2 {
+		t.Errorf("expected allowing only TLS v1.1 to generate exactly two warnings, got %d: %v", len(alerts.Alerts), alerts)
+	} else {
+		t.Run("only TLS version 1.1 allowed - returns warnings", expectOnlyWarnings(alerts))
+		t.Run("only TLS version 1.1 allowed - has expected warning", containsWarning(alerts, expected))
+		t.Run("only TLS version 1.1 allowed - has warning about non-nil tlsVersions", containsWarning(alerts, nonNilTLSVersionsWarningMessage))
+	}
+
+	expected = "old TLS version 1.1 is allowed, but newer version 1.3 is disallowed; this configuration may be insecure"
+	ds.TLSVersions = append(ds.TLSVersions, TLSVersion12)
+	alerts = ds.TLSVersionsAlerts()
+	if len(alerts.Alerts) != 2 {
+		t.Errorf("expected allowing only TLS v1.1 and 1.2 to generate exactly two warning, got %d: %v", len(alerts.Alerts), alerts)
+	} else {
+		t.Run("only TLS versions 1.1 and 1.2 allowed - returns warnings", expectOnlyWarnings(alerts))
+		t.Run("only TLS versions 1.1 and 1.2 allowed - has expected warning", containsWarning(alerts, expected))
+		t.Run("only TLS versions 1.1 and 1.2 allowed - has warning about non-nil tlsVersions", containsWarning(alerts, nonNilTLSVersionsWarningMessage))
+	}
+
+	expected = "old TLS version 1.2 is allowed, but newer version 1.3 is disallowed; this configuration may be insecure"
+	ds.TLSVersions = make([]string, 0, 4)
+	ds.TLSVersions = append(ds.TLSVersions, TLSVersion12)
+	alerts = ds.TLSVersionsAlerts()
+	if len(alerts.Alerts) != 2 {
+		t.Errorf("expected allowing only TLS v1.2 to generate exactly two warnings, got %d: %v", len(alerts.Alerts), alerts)
+	} else {
+		t.Run("only TLS version 1.2 allowed - returns warnings", expectOnlyWarnings(alerts))
+		t.Run("only TLS version 1.2 allowed - has expected warning", containsWarning(alerts, expected))
+		t.Run("only TLS version 1.2 allowed - has warning about non-nil tlsVersions", containsWarning(alerts, nonNilTLSVersionsWarningMessage))
+	}
+
+	ds.TLSVersions = append(ds.TLSVersions, TLSVersion13)
+	alerts = ds.TLSVersionsAlerts()
+	if len(alerts.Alerts) != 1 {
+		t.Errorf("Expected allowing TLS versions 1.2 and 1.3 to generate exactly one warning, got %d: %v", len(alerts.Alerts), alerts.Alerts)
+	} else {
+		t.Run("only TLS versions 1.2 and 1.3 allowed - has warning about non-nil tlsVersions", containsWarning(alerts, nonNilTLSVersionsWarningMessage))
+	}
+
+	expected = "unknown TLS version '13.37' - possible typo"
+	ds.TLSVersions = append(ds.TLSVersions, "13.37")
+	alerts = ds.TLSVersionsAlerts()
+	if len(alerts.Alerts) != 2 {
+		t.Errorf("expected allowing an unknown TLS version to generate exactly two warnings, got %d: %v", len(alerts.Alerts), alerts.Alerts)
+	} else {
+		t.Run("unknown TLS version allowed - returns warnings", expectOnlyWarnings(alerts))
+		t.Run("unknown TLS version allowed - has expected warning", containsWarning(alerts, expected))
+		t.Run("unknown TLS version allowed - has warning about non-nil tlsVersions", containsWarning(alerts, nonNilTLSVersionsWarningMessage))
+	}
+
+	ds.TLSVersions = append(ds.TLSVersions, TLSVersion10)
+	alerts = ds.TLSVersionsAlerts()
+	if len(alerts.Alerts) != 3 {
+		t.Errorf("expected allowing an unknown TLS version and disallowing only version 1.1 to generate exactly three warnings, got %d: %v", len(alerts.Alerts), alerts.Alerts)
+	} else {
+		t.Run("unknown TLS version and disallowed v1.1 - returns warnings", expectOnlyWarnings(alerts))
+		t.Run("unknown TLS version and disallowed v1.1 - has expected warning", containsWarning(alerts, expected))
+		expected = "old TLS version 1.0 is allowed, but newer version 1.1 is disallowed; this configuration may be insecure"
+		t.Run("unknown TLS version and disallowed v1.1 - has second expected warning", containsWarning(alerts, expected))
+		t.Run("unknown TLS version and disallowed v1.1 - has warning about non-nil tlsVersions", containsWarning(alerts, nonNilTLSVersionsWarningMessage))
+	}
+}
+
+func BenchmarkTLSVersionsAlerts(b *testing.B) {
+	versions := make([]string, 0, 101)
+	for major := 1; major <= 10; major++ {
+		for minor := 0; minor < 10; minor++ {
+			if major == 1 && minor == 1 {
+				// skip this one to force generating some more warnings
+				continue
+			}
+			versions = append(versions, fmt.Sprintf("%d.%d", major, minor))
+		}
+	}
+	ds := DeliveryServiceV4{TLSVersions: versions}
+
+	b.ReportAllocs()
+	b.ResetTimer()
+	for i := 0; i < b.N; i++ {
+		ds.TLSVersionsAlerts()
+	}
+}
diff --git a/traffic_ops/app/db/migrations/2021061100000000_ds_tls_versions.sql b/traffic_ops/app/db/migrations/2021061100000000_ds_tls_versions.sql
new file mode 100644
index 0000000..a1c40de
--- /dev/null
+++ b/traffic_ops/app/db/migrations/2021061100000000_ds_tls_versions.sql
@@ -0,0 +1,91 @@
+-- syntax:postgresql
+/*
+	Licensed under the Apache License, Version 2.0 (the "License");
+	you may not use this file except in compliance with the License.
+	You may obtain a copy of the License at
+		http://www.apache.org/licenses/LICENSE-2.0
+	Unless required by applicable law or agreed to in writing, software
+	distributed under the License is distributed on an "AS IS" BASIS,
+	WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+	See the License for the specific language governing permissions and
+	limitations under the License.
+*/
+
+-- +goose Up
+CREATE TABLE IF NOT EXISTS public.deliveryservice_tls_version (
+	deliveryservice bigint NOT NULL REFERENCES public.deliveryservice(id) ON DELETE CASCADE ON UPDATE CASCADE,
+	tls_version text NOT NULL CHECK (tls_version <> ''),
+	PRIMARY KEY (deliveryservice, tls_version)
+);
+
+-- +goose StatementBegin
+CREATE OR REPLACE FUNCTION update_ds_timestamp_on_insert()
+	RETURNS trigger
+	AS $$
+BEGIN
+	UPDATE public.deliveryservice
+	SET last_updated=now()
+	WHERE id IN (
+		SELECT deliveryservice
+		FROM new_table
+	);
+	RETURN NULL;
+END;
+$$ LANGUAGE plpgsql;
+-- +goose StatementEnd
+
+-- +goose StatementBegin
+CREATE OR REPLACE FUNCTION update_ds_timestamp_on_delete()
+	RETURNS trigger
+	AS $$
+BEGIN
+	UPDATE public.deliveryservice
+	SET last_updated=now()
+	WHERE id IN (
+		SELECT deliveryservice
+		FROM old_table
+	);
+	RETURN NULL;
+END;
+$$ LANGUAGE plpgsql;
+-- +goose StatementEnd
+
+CREATE TRIGGER update_ds_timestamp_on_tls_version_insertion
+	AFTER INSERT ON public.deliveryservice_tls_version
+	REFERENCING NEW TABLE AS new_table
+	FOR EACH STATEMENT EXECUTE PROCEDURE update_ds_timestamp_on_insert();
+
+CREATE TRIGGER update_ds_timestamp_on_tls_version_delete
+	AFTER DELETE ON public.deliveryservice_tls_version
+	REFERENCING OLD TABLE AS old_table
+	FOR EACH STATEMENT EXECUTE PROCEDURE update_ds_timestamp_on_delete();
+
+UPDATE public.deliveryservice_request
+SET
+	deliveryservice = jsonb_set(deliveryservice, '{tlsVersions}', 'null')
+WHERE
+	deliveryservice IS NOT NULL;
+UPDATE public.deliveryservice_request
+SET
+	original = jsonb_set(original, '{tlsVersions}', 'null')
+WHERE
+	original IS NOT NULL;
+
+-- +goose Down
+UPDATE public.deliveryservice_request
+SET
+	deliveryservice = deliveryservice - 'tlsVersions'
+WHERE
+	deliveryservice IS NOT NULL;
+
+UPDATE public.deliveryservice_request
+SET
+	original = original - 'tlsVersions'
+WHERE
+	original IS NOT NULL;
+
+DROP TRIGGER IF EXISTS update_ds_timestamp_on_tls_version_insertion_or_update ON public.deliveryservice_tls_version;
+DROP TRIGGER IF EXISTS update_ds_timestamp_on_tls_version_delete ON public.deliveryservice_tls_version;
+DROP TABLE IF EXISTS public.deliveryservice_tls_version;
+DROP FUNCTION IF EXISTS update_ds_timestamp_on_insert;
+DROP FUNCTION IF EXISTS update_ds_timestamp_on_delete;
diff --git a/traffic_ops/testing/api/v4/deliveryservice_request_comments_test.go b/traffic_ops/testing/api/v4/deliveryservice_request_comments_test.go
index edc5103..dd5c410 100644
--- a/traffic_ops/testing/api/v4/deliveryservice_request_comments_test.go
+++ b/traffic_ops/testing/api/v4/deliveryservice_request_comments_test.go
@@ -106,7 +106,7 @@ func CreateTestDeliveryServiceRequestComments(t *testing.T) {
 	}
 	resetDS(ds)
 	if ds == nil || ds.XMLID == nil {
-		t.Fatal("first DSR in the test data had a nil DeliveryService or one with no XMLID")
+		t.Fatal("first DSR in the test data had a nil Delivery Service, or one with no XMLID")
 	}
 
 	opts := client.NewRequestOptions()
diff --git a/traffic_ops/testing/api/v4/deliveryservice_requests_test.go b/traffic_ops/testing/api/v4/deliveryservice_requests_test.go
index 1f2652f..9f6f60c 100644
--- a/traffic_ops/testing/api/v4/deliveryservice_requests_test.go
+++ b/traffic_ops/testing/api/v4/deliveryservice_requests_test.go
@@ -148,7 +148,7 @@ func UpdateTestDeliveryServiceRequestsWithHeaders(t *testing.T, header http.Head
 	}
 	resetDS(ds)
 	if ds == nil || ds.XMLID == nil {
-		t.Fatalf("the %dth DSR in the test data had no DeliveryService - or that DeliveryService had no XMLID", dsrGood)
+		t.Fatalf("the %dth DSR in the test data had no Delivery Service, or that Delivery Service had a null or undefined XMLID", dsrGood)
 	}
 	opts := client.NewRequestOptions()
 	opts.Header = header
@@ -202,7 +202,7 @@ func GetTestDeliveryServiceRequestsIMSAfterChange(t *testing.T, header http.Head
 
 	resetDS(ds)
 	if ds == nil || ds.XMLID == nil {
-		t.Fatalf("the %dth DSR in the test data had no DeliveryService - or that DeliveryService had no XMLID", dsrGood)
+		t.Fatalf("the %dth DSR in the test data had no Delivery Service, or that Delivery Service had a null or undefined XMLID", dsrGood)
 	}
 
 	opts := client.NewRequestOptions()
@@ -312,7 +312,7 @@ func TestDeliveryServiceRequestTypeFields(t *testing.T) {
 		}
 		resetDS(ds)
 		if ds == nil || ds.XMLID == nil {
-			t.Fatalf("the %dth DSR in the test data had no DeliveryService - or that DeliveryService had no XMLID", dsrBadTenant)
+			t.Fatalf("the %dth DSR in the test data had no Delivery Service, or that Delivery Service had a null or undefined XMLID", dsrBadTenant)
 		}
 
 		resp, _, err := TOSession.CreateDeliveryServiceRequest(dsr, client.RequestOptions{})
@@ -522,7 +522,7 @@ func GetTestDeliveryServiceRequestsIMS(t *testing.T) {
 	}
 	resetDS(ds)
 	if ds == nil || ds.XMLID == nil {
-		t.Fatalf("the %dth DSR in the test data had no DeliveryService - or that DeliveryService had no XMLID", dsrGood)
+		t.Fatalf("the %dth DSR in the test data had no Delivery Service, or that Delivery Service had null or undefined XMLID", dsrGood)
 	}
 
 	opts.QueryParameters.Set("xmlId", *ds.XMLID)
@@ -551,7 +551,7 @@ func GetTestDeliveryServiceRequests(t *testing.T) {
 	resetDS(ds)
 
 	if ds == nil || ds.XMLID == nil {
-		t.Fatalf("the %dth DSR in the test data had no DeliveryService - or that DeliveryService had no XMLID", dsrGood)
+		t.Fatalf("the %dth DSR in the test data had no Delivery Service, or that Delivery Service had a null or undefined XMLID", dsrGood)
 	}
 
 	opts := client.NewRequestOptions()
@@ -580,7 +580,7 @@ func UpdateTestDeliveryServiceRequests(t *testing.T) {
 
 	resetDS(ds)
 	if ds == nil || ds.XMLID == nil {
-		t.Fatalf("the %dth DSR in the test data had no DeliveryService - or that DeliveryService had no XMLID", dsrGood)
+		t.Fatalf("the %dth DSR in the test data had no Delivery Service, or that Delivery Service had a null or undefined XMLID", dsrGood)
 	}
 
 	opts := client.NewRequestOptions()
diff --git a/traffic_ops/testing/api/v4/deliveryservices_test.go b/traffic_ops/testing/api/v4/deliveryservices_test.go
index 0630a2e..eda6ed2 100644
--- a/traffic_ops/testing/api/v4/deliveryservices_test.go
+++ b/traffic_ops/testing/api/v4/deliveryservices_test.go
@@ -90,6 +90,7 @@ func TestDeliveryServices(t *testing.T) {
 		t.Run("GET request using the 'type' query string parameter", GetDeliveryServiceByValidType)
 		t.Run("GET request using the 'xmlId' query string parameter", GetDeliveryServiceByValidXmlId)
 		t.Run("Descending order sorted response to GET request", SortTestDeliveryServicesDesc)
+		t.Run("TLS Versions property", addTLSVersionsToDeliveryService)
 	})
 }
 
@@ -195,7 +196,7 @@ func UpdateTestDeliveryServicesWithHeaders(t *testing.T, header http.Header) {
 	}
 	firstDS := testData.DeliveryServices[0]
 	if firstDS.XMLID == nil {
-		t.Fatalf("couldn't get the xml ID of test DS")
+		t.Fatal("Found a Delivery Service in testing data with null or undefined XMLID")
 	}
 
 	opts := client.RequestOptions{Header: header}
@@ -214,7 +215,7 @@ func UpdateTestDeliveryServicesWithHeaders(t *testing.T, header http.Header) {
 		}
 	}
 	if !found {
-		t.Fatalf("GET Delivery Services missing: %v", *firstDS.XMLID)
+		t.Fatalf("GET Delivery Services missing: %s", *firstDS.XMLID)
 	}
 	if remoteDS.ID == nil {
 		t.Fatalf("Traffic Ops returned a representation for Delivery Service '%s' that had a null or undefined ID", *firstDS.XMLID)
@@ -272,15 +273,11 @@ func createBlankCDN(cdnName string, t *testing.T) tc.CDN {
 }
 
 func cleanUp(t *testing.T, ds tc.DeliveryServiceV4, oldCDNID int, newCDNID int, sslKeyVersions []string) {
-	if ds.XMLID == nil {
-		t.Error("Cannot clean up Delivery Service with nil XMLID")
+	if ds.ID == nil || ds.XMLID == nil {
+		t.Error("Cannot clean up Delivery Service with nil ID and/or XMLID")
 		return
 	}
 	xmlid := *ds.XMLID
-	if ds.ID == nil {
-		t.Error("Cannot clean up Delivery Service with nil ID")
-		return
-	}
 	id := *ds.ID
 
 	opts := client.NewRequestOptions()
@@ -334,6 +331,7 @@ func cleanUp(t *testing.T, ds tc.DeliveryServiceV4, oldCDNID int, newCDNID int,
 //    XMLID
 //
 // BUT, will ALWAYS have nil MaxRequestHeaderBytes.
+// Note that the Tenant is hard-coded to #1.
 func getCustomDS(cdnID, typeID int, displayName, routingName, orgFQDN, dsID string) tc.DeliveryServiceV4 {
 	customDS := tc.DeliveryServiceV4{}
 	customDS.Active = util.BoolPtr(true)
@@ -530,7 +528,7 @@ func DeliveryServiceSSLKeys(t *testing.T) {
 	}
 
 	if err != nil || dsSSLKey == nil {
-		t.Fatalf("unable to get DS %v SSL key: %v", *ds.XMLID, err)
+		t.Fatalf("unable to get DS %s SSL key: %v", *ds.XMLID, err)
 	}
 	if dsSSLKey.Certificate.Key == "" {
 		t.Errorf("expected a valid key but got nothing")
@@ -578,7 +576,7 @@ func DeliveryServiceSSLKeys(t *testing.T) {
 	}
 
 	if err != nil || dsSSLKey == nil {
-		t.Fatalf("unable to get DS %v SSL key: %v", *ds.XMLID, err)
+		t.Fatalf("unable to get DS %s SSL key: %v", *ds.XMLID, err)
 	}
 	if dsSSLKey.Certificate.Key == "" {
 		t.Errorf("expected a valid key but got nothing")
@@ -617,11 +615,8 @@ func SSLDeliveryServiceCDNUpdateTest(t *testing.T) {
 		t.Fatalf("Expected Delivery Service creation to create exactly one Delivery Service, Traffic Ops indicates %d were created", len(resp.Response))
 	}
 	ds := resp.Response[0]
-	if ds.XMLID == nil {
-		t.Fatal("Traffic Ops created a Delivery Service with no XMLID")
-	}
-	if ds.ID == nil {
-		t.Fatal("Traffic Ops created a Delivery Service with no ID")
+	if ds.ID == nil || ds.XMLID == nil {
+		t.Fatal("Traffic Ops created a Delivery Service with null or undefined XMLID and/or ID")
 	}
 	ds.CDNName = &oldCdn.Name
 
@@ -729,7 +724,7 @@ func PostDeliveryServiceTest(t *testing.T) {
 	}
 	ds := testData.DeliveryServices[0]
 	if ds.XMLID == nil {
-		t.Fatal("Testing Delivery Service had no XMLID")
+		t.Fatal("Found Delivery Service in testing data with null or undefined XMLID")
 	}
 	xmlid := *ds.XMLID + "-topology-test"
 
@@ -768,6 +763,10 @@ func CreateTestDeliveryServices(t *testing.T) {
 	}
 	for _, ds := range testData.DeliveryServices {
 		ds = ds.RemoveLD1AndLD2()
+		if ds.XMLID == nil {
+			t.Error("Found a Delivery Service in testing data with null or undefined XMLID")
+			continue
+		}
 		resp, _, err := TOSession.CreateDeliveryService(ds, client.RequestOptions{})
 		if err != nil {
 			t.Errorf("could not create Delivery Service '%s': %v - alerts: %+v", *ds.XMLID, err, resp.Alerts)
@@ -803,7 +802,7 @@ func GetTestDeliveryServices(t *testing.T) {
 	actualDSMap := make(map[string]tc.DeliveryServiceV4, len(actualDSes.Response))
 	for _, ds := range actualDSes.Response {
 		if ds.XMLID == nil {
-			t.Error("Traffic Ops returned representation of a Delivery Service with null or undefined XMLID")
+			t.Error("Traffic Ops returned a representation of a Delivery Service with null or undefined XMLID")
 			continue
 		}
 		actualDSMap[*ds.XMLID] = ds
@@ -870,12 +869,8 @@ func GetTestDeliveryServicesCapacity(t *testing.T) {
 	}
 	actualDSMap := map[string]tc.DeliveryServiceV4{}
 	for _, ds := range actualDSes.Response {
-		if ds.XMLID == nil {
-			t.Error("Traffic Ops returned a representation for a Delivery Service with null or undefined XMLID")
-			continue
-		}
-		if ds.ID == nil {
-			t.Error("Traffic Ops returned a representation for a Delivery Service with null or undefined ID")
+		if ds.ID == nil || ds.XMLID == nil {
+			t.Error("Traffic Ops returned a representation for a Delivery Service with null or undefined XMLID and/or ID")
 			continue
 		}
 		actualDSMap[*ds.XMLID] = ds
@@ -1377,7 +1372,7 @@ func UpdateDeliveryServiceWithInvalidSliceRangeRequest(t *testing.T) {
 		}
 	}
 	if !found {
-		t.Fatalf("GET Delivery Services missing: %v", *dsXML)
+		t.Fatalf("GET Delivery Services missing: %s", *dsXML)
 	}
 
 	testCases := []struct {
@@ -1556,7 +1551,7 @@ func DeleteTestDeliveryServices(t *testing.T) {
 	}
 	for _, testDS := range testData.DeliveryServices {
 		if testDS.XMLID == nil {
-			t.Errorf("testing Delivery Service has no XMLID")
+			t.Error("Found a Delivery Service in testing data with null or undefined XMLID")
 			continue
 		}
 		var ds tc.DeliveryServiceV4
@@ -1573,7 +1568,7 @@ func DeleteTestDeliveryServices(t *testing.T) {
 			}
 		}
 		if !found {
-			t.Errorf("DeliveryService not found in Traffic Ops: %v", *testDS.XMLID)
+			t.Errorf("Delivery Service not found in Traffic Ops: %s", *testDS.XMLID)
 			continue
 		}
 
@@ -1616,7 +1611,7 @@ func DeliveryServiceMinorVersionsTest(t *testing.T) {
 	}
 	testDS := testData.DeliveryServices[4]
 	if testDS.XMLID == nil {
-		t.Fatal("expected XMLID: ds-test-minor-versions, actual: <nil>")
+		t.Fatal("Found a Delivery Service in testing data with a null or undefined XMLID")
 	}
 	if *testDS.XMLID != "ds-test-minor-versions" {
 		t.Errorf("expected XMLID: ds-test-minor-versions, actual: %s", *testDS.XMLID)
@@ -1941,12 +1936,12 @@ func GetTestDeliveryServicesURLSignatureKeys(t *testing.T) {
 	}
 	firstDS := testData.DeliveryServices[0]
 	if firstDS.XMLID == nil {
-		t.Fatal("couldn't get the xml ID of test DS")
+		t.Fatal("Found a Delivery Service in testing data with a null or undefined XMLID")
 	}
 
 	_, _, err := TOSession.GetDeliveryServiceURLSignatureKeys(*firstDS.XMLID, client.RequestOptions{})
 	if err != nil {
-		t.Error("failed to get url sig keys: " + err.Error())
+		t.Errorf("failed to get url sig keys: %v", err)
 	}
 }
 
@@ -1955,8 +1950,9 @@ func CreateTestDeliveryServicesURLSignatureKeys(t *testing.T) {
 		t.Fatal("couldn't get the xml ID of test DS")
 	}
 	firstDS := testData.DeliveryServices[0]
+
 	if firstDS.XMLID == nil {
-		t.Fatal("couldn't get the xml ID of test DS")
+		t.Fatal("Found a Delivery Service in testing data with a null or undefined XMLID")
 	}
 
 	resp, _, err := TOSession.CreateDeliveryServiceURLSignatureKeys(*firstDS.XMLID, client.RequestOptions{})
@@ -2002,8 +1998,9 @@ func DeleteTestDeliveryServicesURLSignatureKeys(t *testing.T) {
 		t.Fatal("couldn't get the xml ID of test DS")
 	}
 	firstDS := testData.DeliveryServices[0]
+
 	if firstDS.XMLID == nil {
-		t.Fatal("couldn't get the xml ID of test DS")
+		t.Fatal("Found a Delivery Service in testing data with a null or undefined XMLID")
 	}
 
 	resp, _, err := TOSession.DeleteDeliveryServiceURLSignatureKeys(*firstDS.XMLID, client.RequestOptions{})
@@ -2018,8 +2015,9 @@ func GetTestDeliveryServicesURISigningKeys(t *testing.T) {
 		t.Fatal("couldn't get the xml ID of test DS")
 	}
 	firstDS := testData.DeliveryServices[0]
+
 	if firstDS.XMLID == nil {
-		t.Fatal("couldn't get the xml ID of test DS")
+		t.Fatal("Found a Delivery Service in testing data with a null or undefined XMLID")
 	}
 
 	_, _, err := TOSession.GetDeliveryServiceURISigningKeys(*firstDS.XMLID, client.RequestOptions{})
@@ -2065,7 +2063,7 @@ func CreateTestDeliveryServicesURISigningKeys(t *testing.T) {
 	}
 	firstDS := testData.DeliveryServices[0]
 	if firstDS.XMLID == nil {
-		t.Fatal("couldn't get the xml ID of test DS")
+		t.Fatal("Found a Delivery Service in testing data with a null or undefined XMLID")
 	}
 
 	var keyset map[string]tc.URISignerKeyset
@@ -2138,7 +2136,7 @@ func DeleteTestDeliveryServicesURISigningKeys(t *testing.T) {
 	}
 	firstDS := testData.DeliveryServices[0]
 	if firstDS.XMLID == nil {
-		t.Fatal("couldn't get the xml ID of test DS")
+		t.Fatal("Found a Delivery Service in testing data with a null or undefined XMLID")
 	}
 
 	resp, _, err := TOSession.DeleteDeliveryServiceURISigningKeys(*firstDS.XMLID, client.RequestOptions{})
@@ -2177,7 +2175,7 @@ func GetDeliveryServiceByLogsEnabled(t *testing.T) {
 	}
 	firstDS := testData.DeliveryServices[0]
 	if firstDS.LogsEnabled == nil {
-		t.Fatal("Logs Enabled is nil in the pre-requisites ")
+		t.Fatal("Found a Delivery Service in testing data with a null or undefined LogsEnabled")
 	}
 
 	opts := client.NewRequestOptions()
@@ -2324,8 +2322,9 @@ func GetDeliveryServiceByValidXmlId(t *testing.T) {
 		t.Fatal("Need at least one Delivery Service to test getting Delivery Services filtered by XMLID")
 	}
 	firstDS := testData.DeliveryServices[0]
+
 	if firstDS.XMLID == nil {
-		t.Errorf("XML ID is nil in the Pre-requisites")
+		t.Fatal("Found a Delivery Service in testing data with a null or undefined XMLID")
 	}
 
 	opts := client.NewRequestOptions()
@@ -2379,3 +2378,99 @@ func SortTestDeliveryServicesDesc(t *testing.T) {
 		}
 	}
 }
+
+func addTLSVersionsToDeliveryService(t *testing.T) {
+	me, _, err := TOSession.GetUserCurrent(client.RequestOptions{})
+	if err != nil {
+		t.Fatalf("Failed to get current User: %v - alerts: %+v", err, me.Alerts)
+	}
+	if me.Response.Tenant == nil || me.Response.TenantID == nil {
+		t.Fatal("Traffic Ops returned a representation for the current user with null or undefined tenant and/or tenantID")
+	}
+
+	var ds tc.DeliveryServiceV4
+	ds.Active = new(bool)
+	ds.CDNName = new(string)
+	ds.DisplayName = new(string)
+	ds.DSCP = new(int)
+	ds.GeoLimit = new(int)
+	ds.GeoProvider = new(int)
+	ds.InitialDispersion = new(int)
+	ds.IPV6RoutingEnabled = new(bool)
+	ds.LogsEnabled = new(bool)
+	ds.MissLat = new(float64)
+	ds.MissLong = new(float64)
+	ds.MultiSiteOrigin = new(bool)
+	ds.OrgServerFQDN = new(string)
+	ds.Protocol = new(int)
+	ds.QStringIgnore = new(int)
+	ds.RangeRequestHandling = new(int)
+	ds.RegionalGeoBlocking = new(bool)
+	ds.Tenant = new(string)
+	ds.TenantID = me.Response.TenantID
+	ds.TLSVersions = []string{
+		"1.1",
+	}
+	ds.Type = new(tc.DSType)
+	ds.XMLID = new(string)
+	*ds.DSCP = 1
+	*ds.InitialDispersion = 1
+	*ds.Tenant = *me.Response.Tenant
+	*ds.DisplayName = "ds-test-tls-versions"
+	*ds.XMLID = "ds-test-tls-versions"
+
+	cdns, _, err := TOSession.GetCDNs(client.RequestOptions{})
+	if err != nil {
+		t.Fatalf("Failed to get CDNs: %v - alerts: %+v", err, cdns.Alerts)
+	}
+	if len(cdns.Response) < 1 {
+		t.Fatalf("Need at least one CDN to exist in order to test Delivery Service TLS Versions")
+	}
+	ds.CDNID = &cdns.Response[0].ID
+	*ds.CDNName = cdns.Response[0].Name
+
+	*ds.Type = "STEERING"
+	opts := client.NewRequestOptions()
+	opts.QueryParameters.Set("name", string(*ds.Type))
+	types, _, err := TOSession.GetTypes(opts)
+	if err != nil {
+		t.Fatalf("Failed to get Types: %v - alerts: %+v", err, types.Alerts)
+	}
+	if len(types.Response) != 1 {
+		t.Fatalf("Expected exactly one Type to exist named 'STEERING', found: %d", len(types.Response))
+	}
+	ds.TypeID = &types.Response[0].ID
+
+	_, _, err = TOSession.CreateDeliveryService(ds, client.RequestOptions{})
+	if err == nil {
+		t.Error("Expected an error creating a STEERING Delivery Service with explicit TLS Versions, but didn't")
+	} else if !strings.Contains(err.Error(), "'tlsVersions' must be 'null' for STEERING-Type") {
+		t.Errorf("Expected an error about non-null TLS Versions for STEERING-Type Delivery Services, got: %v", err)
+	}
+
+	*ds.Type = "HTTP"
+	opts.QueryParameters.Set("name", string(*ds.Type))
+	types, _, err = TOSession.GetTypes(opts)
+	if err != nil {
+		t.Fatalf("Failed to get Types: %v - alerts: %+v", err, types.Alerts)
+	}
+	if len(types.Response) != 1 {
+		t.Fatalf("Expected exactly one Type to exist named 'HTTP', found: %d", len(types.Response))
+	}
+	ds.TypeID = &types.Response[0].ID
+
+	*ds.OrgServerFQDN = "https://origin.test"
+	resp, _, err := TOSession.CreateDeliveryService(ds, client.RequestOptions{})
+	if err != nil {
+		t.Errorf("Unexpected error creating a Delivery Service: %v - alerts: %+v", err, resp.Alerts)
+	} else if len(resp.Response) != 1 {
+		t.Errorf("Expected creating a new Delivery Service to create exactly one Delivery Service, but Traffic Ops indicated that %d were created", len(resp.Response))
+	} else if resp.Response[0].ID == nil {
+		t.Error("Traffic Ops returned a representation for a created Delivery Service that had null or undefined ID")
+	} else {
+		alerts, _, err := TOSession.DeleteDeliveryService(*resp.Response[0].ID, client.RequestOptions{})
+		if err != nil {
+			t.Errorf("Failed to clean up newly created Delivery Service: %v - alerts: %+v", err, alerts.Alerts)
+		}
+	}
+}
diff --git a/traffic_ops/testing/api/v4/tc-fixtures.json b/traffic_ops/testing/api/v4/tc-fixtures.json
index a648a78..52ac6ec 100644
--- a/traffic_ops/testing/api/v4/tc-fixtures.json
+++ b/traffic_ops/testing/api/v4/tc-fixtures.json
@@ -442,7 +442,6 @@
             "infoUrl": "TBD",
             "initialDispersion": 1,
             "ipv6RoutingEnabled": true,
-            "lastUpdated": "2018-04-06 16:48:51+00",
             "logsEnabled": false,
             "longDesc": "d s 1",
             "longDesc1": "ds1",
@@ -509,7 +508,6 @@
             "infoUrl": "TBD",
             "initialDispersion": 1,
             "ipv6RoutingEnabled": true,
-            "lastUpdated": "2018-04-06 16:48:51+00",
             "logsEnabled": false,
             "longDesc": "d s 1",
             "longDesc1": "ds2",
@@ -577,7 +575,6 @@
             "infoUrl": "TBD",
             "initialDispersion": 1,
             "ipv6RoutingEnabled": true,
-            "lastUpdated": "2018-04-06 16:48:51+00",
             "logsEnabled": false,
             "longDesc": "d s 3",
             "longDesc1": "ds3",
@@ -641,7 +638,6 @@
             "infoUrl": "",
             "initialDispersion": 1,
             "ipv6RoutingEnabled": true,
-            "lastUpdated": "2018-04-06 16:48:51+00",
             "logsEnabled": false,
             "longDesc": "",
             "longDesc1": "",
@@ -762,7 +758,6 @@
             "infoUrl": "TBD",
             "initialDispersion": 1,
             "ipv6RoutingEnabled": true,
-            "lastUpdated": "2018-04-06 16:48:51+00",
             "logsEnabled": false,
             "longDesc": "mso DS 1",
             "longDesc1": "msods1",
@@ -829,7 +824,6 @@
             "infoUrl": "TBD",
             "initialDispersion": 1,
             "ipv6RoutingEnabled": true,
-            "lastUpdated": "2018-04-06 16:48:51+00",
             "logsEnabled": false,
             "longDesc": "d s 1",
             "longDesc1": "ds1nat",
@@ -892,7 +886,6 @@
             "infoUrl": "TBD",
             "initialDispersion": 1,
             "ipv6RoutingEnabled": true,
-            "lastUpdated": "2018-04-06 16:48:51+00",
             "logsEnabled": false,
             "longDesc": "d s top",
             "longDesc1": "ds top",
@@ -959,7 +952,6 @@
             "infoUrl": "TBD",
             "initialDispersion": 1,
             "ipv6RoutingEnabled": true,
-            "lastUpdated": "2018-04-06 16:48:51+00",
             "logsEnabled": false,
             "longDesc": "",
             "longDesc1": "",
@@ -1023,7 +1015,6 @@
             "infoUrl": "TBD",
             "initialDispersion": 1,
             "ipv6RoutingEnabled": true,
-            "lastUpdated": "2018-04-06 16:48:51+00",
             "logsEnabled": false,
             "longDesc": "",
             "longDesc1": "",
@@ -1087,7 +1078,6 @@
             "infoUrl": "TBD",
             "initialDispersion": 1,
             "ipv6RoutingEnabled": true,
-            "lastUpdated": "2018-04-06 16:48:51+00",
             "logsEnabled": false,
             "longDesc": "d s client-steering",
             "longDesc1": "ds client-steering",
@@ -1150,7 +1140,6 @@
             "infoUrl": "TBD",
             "initialDispersion": 1,
             "ipv6RoutingEnabled": true,
-            "lastUpdated": "2018-04-06 16:48:51+00",
             "logsEnabled": false,
             "longDesc": "",
             "longDesc1": "",
@@ -1214,7 +1203,6 @@
             "infoUrl": "TBD",
             "initialDispersion": 1,
             "ipv6RoutingEnabled": true,
-            "lastUpdated": "2018-04-06 16:48:51+00",
             "logsEnabled": false,
             "longDesc": "",
             "longDesc1": "",
@@ -1278,7 +1266,6 @@
             "infoUrl": "TBD",
             "initialDispersion": 1,
             "ipv6RoutingEnabled": true,
-            "lastUpdated": "2018-04-06 16:48:51+00",
             "logsEnabled": false,
             "longDesc": "",
             "longDesc1": "",
@@ -1342,7 +1329,6 @@
             "infoUrl": "TBD",
             "initialDispersion": 1,
             "ipv6RoutingEnabled": true,
-            "lastUpdated": "2018-04-06 16:48:51+00",
             "logsEnabled": false,
             "longDesc": "",
             "longDesc1": "",
@@ -1410,7 +1396,6 @@
             "infoUrl": "TBD",
             "initialDispersion": 1,
             "ipv6RoutingEnabled": true,
-            "lastUpdated": "2018-04-06 15:48:51+00",
             "logsEnabled": false,
             "longDesc": "d s inactive",
             "longDesc1": "dsinactive",
@@ -1502,7 +1487,6 @@
             "infoUrl": "TBD",
             "initialDispersion": 1,
             "ipv6RoutingEnabled": true,
-            "lastUpdated": "2018-04-06 16:48:51+00",
             "logsEnabled": false,
             "longDesc": "d s 4",
             "longDesc1": "ds4",
@@ -1682,119 +1666,102 @@
     "parameters": [
         {
             "configFile": "rascal.properties",
-            "lastUpdated": "2018-01-19T19:01:21.489534+00:00",
             "name": "history.count",
             "secure": false,
             "value": "30"
         },
         {
             "configFile": "records.config",
-            "lastUpdated": "2018-01-19T19:01:21.434425+00:00",
             "name": "CONFIG proxy.config.allocator.enable_reclaim",
             "secure": false,
             "value": "INT 0"
         },
         {
             "configFile": "records.config",
-            "lastUpdated": "2018-01-19T19:01:21.435957+00:00",
             "name": "CONFIG proxy.config.allocator.max_overage",
             "secure": false,
             "value": "INT 3"
         },
         {
             "configFile": "records.config",
-            "lastUpdated": "2018-01-19T19:01:21.437496+00:00",
             "name": "CONFIG proxy.config.diags.show_location",
             "secure": false,
             "value": "INT 0"
         },
         {
             "configFile": "records.config",
-            "lastUpdated": "2018-01-19T19:01:21.439033+00:00",
             "name": "CONFIG proxy.config.http.cache.allow_empty_doc",
             "secure": false,
             "value": "INT 0"
         },
         {
             "configFile": "records.config",
-            "lastUpdated": "2018-01-19T19:01:21.440502+00:00",
             "name": "LOCAL proxy.config.cache.interim.storage",
             "secure": false,
             "value": "STRING NULL"
         },
         {
             "configFile": "records.config",
-            "lastUpdated": "2018-01-19T19:01:21.441933+00:00",
             "name": "CONFIG proxy.config.http.parent_proxy.file",
             "secure": false,
             "value": "STRING parent.config"
         },
         {
             "configFile": "plugin.config",
-            "lastUpdated": "2018-01-19T19:01:21.447837+00:00",
             "name": "astats_over_http.so",
             "secure": false,
             "value": "_astats 33.101.99.100,172.39.19.39,172.39.19.49,172.39.19.49,172.39.29.49"
         },
         {
             "configFile": "logs_xml.config",
-            "lastUpdated": "2018-01-19T19:01:21.461206+00:00",
             "name": "LogFormat.Name",
             "secure": false,
             "value": "custom_ats_2"
         },
         {
             "configFile": "logs_xml.config",
-            "lastUpdated": "2018-01-19T19:01:21.462772+00:00",
             "name": "LogObject.Format",
             "secure": false,
             "value": "custom_ats_2"
         },
         {
             "configFile": "logs_xml.config",
-            "lastUpdated": "2018-01-19T19:01:21.464259+00:00",
             "name": "LogObject.Filename",
             "secure": false,
             "value": "custom_ats_2"
         },
         {
             "configFile": "records.config",
-            "lastUpdated": "2018-01-19T19:01:21.467349+00:00",
             "name": "CONFIG proxy.config.cache.control.filename",
             "secure": false,
             "value": "STRING cache.config"
         },
         {
             "configFile": "plugin.config",
-            "lastUpdated": "2018-01-19T19:01:21.469075+00:00",
             "name": "regex_revalidate.so",
             "secure": false,
             "value": "--config regex_revalidate.config"
         },
         {
             "configFile": "records.config",
-            "lastUpdated": "2018-01-19T19:01:21.49285+00:00",
             "name": "CONFIG proxy.config.hostdb.storage_size",
             "secure": false,
             "value": "INT 33554432"
         },
         {
             "configFile": "regex_revalidate.config",
-            "lastUpdated": "2018-01-19T19:01:21.496195+00:00",
             "name": "maxRevalDurationDays",
             "secure": false,
             "value": "90"
         },
         {
             "configFile": "package",
-            "lastUpdated": "2018-01-19T19:01:21.499423+00:00",
             "name": "trafficserver",
             "secure": false,
             "value": "5.3.2-765.f4354b9.el7.centos.x86_64"
         },
         {
             "configFile": "global",
-            "lastUpdated": "2020-04-21T05:19:43.853831+00:00",
             "name": "tm.instance_name",
             "secure": false,
             "value": "Traffic Ops API Tests"
@@ -1806,7 +1773,6 @@
             "city": "Denver",
             "comments": null,
             "email": null,
-            "lastUpdated": "2018-01-19T21:19:32.081465+00:00",
             "name": "Denver",
             "phone": "303-111-1111",
             "poc": null,
@@ -1820,7 +1786,6 @@
             "city": "Boulder",
             "comments": null,
             "email": null,
-            "lastUpdated": "2018-01-19T21:19:32.086195+00:00",
             "name": "Boulder",
             "phone": "303-222-2222",
             "poc": null,
@@ -1834,7 +1799,6 @@
             "city": "Atlanta",
             "comments": null,
             "email": null,
-            "lastUpdated": "2018-01-19T21:19:32.089538+00:00",
             "name": "HotAtlanta",
             "phone": "404-222-2222",
             "poc": null,
@@ -2220,7 +2184,6 @@
         {
             "cdnName": "cdn1",
             "description": "edge1 description",
-            "lastUpdated": "2018-03-02T17:27:11.818418+00:00",
             "name": "EDGE1",
             "routing_disabled": false,
             "type": "ATS_PROFILE",
@@ -2248,7 +2211,6 @@
         {
             "cdnName": "cdn2",
             "description": "edge2 description",
-            "lastUpdated": "2018-03-02T17:27:11.818418+00:00",
             "name": "EDGEInCDN2",
             "routing_disabled": false,
             "type": "ATS_PROFILE"
@@ -2256,7 +2218,6 @@
         {
             "cdnName": "cdn4",
             "description": "edge2 description",
-            "lastUpdated": "2018-03-02T17:27:11.818418+00:00",
             "name": "EDGE2",
             "routing_disabled": false,
             "type": "ATS_PROFILE"
@@ -2271,7 +2232,6 @@
         {
             "cdnName": "cdn1",
             "description": "mid description",
-            "lastUpdated": "2018-03-02T17:27:11.80173+00:00",
             "name": "MID1",
             "routing_disabled": false,
             "type": "ATS_PROFILE"
@@ -2279,7 +2239,6 @@
         {
             "cdnName": "cdn2",
             "description": "mid description",
-            "lastUpdated": "2018-03-02T17:27:11.80173+00:00",
             "name": "MID2",
             "routing_disabled": false,
             "type": "ATS_PROFILE"
@@ -2287,7 +2246,6 @@
         {
             "cdnName": "cdn1",
             "description": "origin description",
-            "lastUpdated": "2018-03-02T17:27:11.80173+00:00",
             "name": "ORIGIN1",
             "routing_disabled": false,
             "type": "ORG_PROFILE"
@@ -2295,7 +2253,6 @@
         {
             "cdnName": "cdn1",
             "description": "cdn1 description",
-            "lastUpdated": "2018-03-02T17:27:11.80452+00:00",
             "name": "CCR1",
             "routing_disabled": false,
             "type": "TR_PROFILE"
@@ -2303,7 +2260,6 @@
         {
             "cdnName": "cdn2",
             "description": "cdn2 description",
-            "lastUpdated": "2018-03-02T17:27:11.807948+00:00",
             "name": "CCR2",
             "routing_disabled": false,
             "type": "TR_PROFILE"
@@ -2311,7 +2267,6 @@
         {
             "cdnName": "cdn1",
             "description": "rascal description",
-            "lastUpdated": "2018-03-02T17:27:11.813052+00:00",
             "name": "RASCAL1",
             "routing_disabled": false,
             "type": "TM_PROFILE",
@@ -2330,14 +2285,12 @@
                 },
                 {
                     "configFile": "rascal-config.txt",
-                    "lastUpdated": "2018-01-19T19:01:21.472279+00:00",
                     "name": "peers.polling.interval",
                     "secure": false,
                     "value": "60"
                 },
                 {
                     "configFile": "rascal-config.txt",
-                    "lastUpdated": "2018-01-19T19:01:21.472279+00:00",
                     "name": "health.polling.interval",
                     "secure": false,
                     "value": "30"
@@ -2347,7 +2300,6 @@
         {
             "cdnName": "cdn1",
             "description": "mso origin description",
-            "lastUpdated": "2018-03-02T17:27:11.80173+00:00",
             "name": "MSO",
             "routing_disabled": false,
             "type": "ORG_PROFILE"
@@ -2434,7 +2386,6 @@
                     "routerPort": "9000"
                 }
             ],
-            "lastUpdated": "2018-03-28T17:30:00.220351+00:00",
             "mgmtIpAddress": "",
             "mgmtIpGateway": "",
             "mgmtIpNetmask": "",
@@ -2479,7 +2430,6 @@
                     "routerPort": "9001"
                 }
             ],
-            "lastUpdated": "2018-03-28T17:30:00.220351+00:00",
             "mgmtIpAddress": "",
             "mgmtIpGateway": "",
             "mgmtIpNetmask": "",
@@ -2529,7 +2479,6 @@
                     "routerPort": "9002"
                 }
             ],
-            "lastUpdated": "2018-03-28T17:30:00.220351+00:00",
             "mgmtIpAddress": "",
             "mgmtIpGateway": "",
             "mgmtIpNetmask": "",
@@ -2579,7 +2528,6 @@
                     "routerPort": "9003"
                 }
             ],
-            "lastUpdated": "2018-03-28T17:30:00.220351+00:00",
             "mgmtIpAddress": "",
             "mgmtIpGateway": "",
             "mgmtIpNetmask": "",
@@ -2629,7 +2577,6 @@
                     "routerPort": "9004"
                 }
             ],
-            "lastUpdated": "2018-03-28T17:30:00.220351+00:00",
             "mgmtIpAddress": "",
             "mgmtIpGateway": "",
             "mgmtIpNetmask": "",
@@ -2679,7 +2626,6 @@
                     "routerPort": "9005"
                 }
             ],
-            "lastUpdated": "2018-03-28T17:30:00.220351+00:00",
             "mgmtIpAddress": "",
             "mgmtIpGateway": "",
             "mgmtIpNetmask": "",
@@ -2730,7 +2676,6 @@
                 }
             ],
             "ipNetmask": "255.255.255.252",
-            "lastUpdated": "2018-03-28T17:30:00.220351+00:00",
             "mgmtIpAddress": "",
             "mgmtIpGateway": "",
             "mgmtIpNetmask": "",
@@ -2780,7 +2725,6 @@
                     "routerPort": "9007"
                 }
             ],
-            "lastUpdated": "2018-03-28T17:30:00.220351+00:00",
             "mgmtIpAddress": "",
             "mgmtIpGateway": "",
             "mgmtIpNetmask": "",
@@ -2830,7 +2774,6 @@
                     "routerPort": "9008"
                 }
             ],
-            "lastUpdated": "2018-03-28T17:30:00.220351+00:00",
             "mgmtIpAddress": "",
             "mgmtIpGateway": "",
             "mgmtIpNetmask": "",
@@ -2880,7 +2823,6 @@
                     "routerPort": "9009"
                 }
             ],
-            "lastUpdated": "2018-03-28T17:30:00.220351+00:00",
             "mgmtIpAddress": "",
             "mgmtIpGateway": "",
             "mgmtIpNetmask": "",
@@ -2930,7 +2872,6 @@
                     "routerPort": "9010"
                 }
             ],
-            "lastUpdated": "2018-03-28T17:30:00.220351+00:00",
             "mgmtIpAddress": "",
             "mgmtIpGateway": "",
             "mgmtIpNetmask": "",
@@ -2980,7 +2921,6 @@
                     "routerPort": "9011"
                 }
             ],
-            "lastUpdated": "2018-03-28T17:30:00.220351+00:00",
             "mgmtIpAddress": "",
             "mgmtIpGateway": "",
             "mgmtIpNetmask": "",
@@ -3030,7 +2970,6 @@
                     "routerPort": "9012"
                 }
             ],
-            "lastUpdated": "2018-03-28T17:30:00.220351+00:00",
             "mgmtIpAddress": "",
             "mgmtIpGateway": "",
             "mgmtIpNetmask": "",
@@ -3129,7 +3068,6 @@
                     "routerPort": "9014"
                 }
             ],
-            "lastUpdated": "2018-03-28T17:30:00.220351+00:00",
             "mgmtIpAddress": "",
             "mgmtIpGateway": "",
             "mgmtIpNetmask": "",
@@ -3179,7 +3117,6 @@
                     "routerPort": "9015"
                 }
             ],
-            "lastUpdated": "2018-03-28T17:30:00.220351+00:00",
             "mgmtIpAddress": "",
             "mgmtIpGateway": "",
             "mgmtIpNetmask": "",
@@ -4060,7 +3997,6 @@
                     "routerPort": "9038"
                 }
             ],
-            "lastUpdated": "2018-03-28T17:30:00.220351+00:00",
             "mgmtIpAddress": "",
             "mgmtIpGateway": "",
             "mgmtIpNetmask": "",
@@ -4606,7 +4542,6 @@
                     "routerPort": "9000"
                 }
             ],
-            "lastUpdated": "2018-03-28T17:30:00.220351+00:00",
             "mgmtIpAddress": "",
             "mgmtIpGateway": "",
             "mgmtIpNetmask": "",
@@ -4656,7 +4591,6 @@
                     "routerPort": "9011"
                 }
             ],
-            "lastUpdated": "2018-03-28T17:30:00.220351+00:00",
             "mgmtIpAddress": "",
             "mgmtIpGateway": "",
             "mgmtIpNetmask": "",
@@ -5115,193 +5049,161 @@
     "types": [
         {
             "description": "Host header regular expression",
-            "lastUpdated": "2018-03-02T19:13:46.788583+00:00",
             "name": "HOST_REGEXP",
             "useInTable": "regex"
         },
         {
             "description": "DNS Content routing, RAM cache, National",
-            "lastUpdated": "2018-03-02T19:13:46.792319+00:00",
             "name": "DNS_LIVE_NATNL",
             "useInTable": "deliveryservice"
         },
         {
             "description": "Other CDN (CDS-IS, Akamai, etc)",
-            "lastUpdated": "2018-03-02T19:13:46.793921+00:00",
             "name": "OTHER_CDN",
             "useInTable": "server"
         },
         {
             "description": "Client-Controlled Steering Delivery Service",
-            "lastUpdated": "2018-03-02T19:13:46.795291+00:00",
             "name": "CLIENT_STEERING",
             "useInTable": "deliveryservice"
         },
         {
             "description": "influxdb type",
-            "lastUpdated": "2018-03-02T19:13:46.796707+00:00",
             "name": "INFLUXDB",
             "useInTable": "server"
         },
         {
             "description": "riak type",
-            "lastUpdated": "2018-03-02T19:13:46.798008+00:00",
             "name": "RIAK",
             "useInTable": "server"
         },
         {
             "description": "Origin",
-            "lastUpdated": "2018-03-02T19:13:46.799404+00:00",
             "name": "ORG",
             "useInTable": "server"
         },
         {
             "description": "HTTP Content routing cache in RAM ",
-            "lastUpdated": "2018-03-02T19:13:46.800738+00:00",
             "name": "HTTP_LIVE",
             "useInTable": "deliveryservice"
         },
         {
             "description": "Active Directory User",
-            "lastUpdated": "2018-03-02T19:13:46.802044+00:00",
             "name": "ACTIVE_DIRECTORY",
             "useInTable": "tm_user"
         },
         {
             "description": "federation type resolve4",
-            "lastUpdated": "2018-03-02T19:13:46.803471+00:00",
             "name": "RESOLVE4",
             "useInTable": "federation"
         },
         {
             "description": "Static DNS A entry",
-            "lastUpdated": "2018-03-02T19:13:46.804776+00:00",
             "name": "A_RECORD",
             "useInTable": "staticdnsentry"
         },
         {
             "description": "Local User",
-            "lastUpdated": "2018-03-02T19:13:46.806035+00:00",
             "name": "LOCAL",
             "useInTable": "tm_user"
         },
         {
             "description": "Weighted steering target",
-            "lastUpdated": "2018-03-02T19:13:46.80748+00:00",
             "name": "STEERING_WEIGHT",
             "useInTable": "steering_target"
         },
         {
             "description": "HTTP Content routing, RAM cache, National",
-            "lastUpdated": "2018-03-02T19:13:46.808911+00:00",
             "name": "HTTP_LIVE_NATNL",
             "useInTable": "deliveryservice"
         },
         {
             "description": "Ops hosts for management",
-            "lastUpdated": "2018-03-02T19:13:46.810576+00:00",
             "name": "TOOLS_SERVER",
             "useInTable": "server"
         },
         {
             "description": "Path regular expression",
-            "lastUpdated": "2018-03-02T19:13:46.812049+00:00",
             "name": "PATH_REGEXP",
             "useInTable": "regex"
         },
         {
             "description": "Static DNS CNAME entry",
-            "lastUpdated": "2018-03-02T19:13:46.813461+00:00",
             "name": "CNAME_RECORD",
             "useInTable": "staticdnsentry"
         },
         {
             "description": "Kabletown Content Router",
-            "lastUpdated": "2018-03-02T19:13:46.814833+00:00",
             "name": "CCR",
             "useInTable": "server"
         },
         {
             "description": "Origin Cachegroup",
-            "lastUpdated": "2018-03-02T19:13:46.816199+00:00",
             "name": "ORG_LOC",
             "useInTable": "cachegroup"
         },
         {
             "description": "Mid Cachegroup",
-            "lastUpdated": "2018-03-02T19:13:46.816199+00:00",
             "name": "MID_LOC",
             "useInTable": "cachegroup"
         },
         {
             "description": "Edge Cache",
-            "lastUpdated": "2018-03-02T19:13:46.817689+00:00",
             "name": "EDGE",
             "useInTable": "server"
         },
         {
             "description": "Ordered steering target",
-            "lastUpdated": "2018-03-02T19:13:46.81913+00:00",
             "name": "STEERING_ORDER",
             "useInTable": "steering_target"
         },
         {
             "description": "DNS Content Routing",
-            "lastUpdated": "2018-03-02T19:13:46.820528+00:00",
             "name": "DNS",
             "useInTable": "deliveryservice"
         },
         {
             "description": "federation type resolve6",
-            "lastUpdated": "2018-03-02T19:13:46.822161+00:00",
             "name": "RESOLVE6",
             "useInTable": "federation"
         },
         {
             "description": "Static DNS AAAA entry",
-            "lastUpdated": "2018-03-02T19:13:46.823506+00:00",
             "name": "AAAA_RECORD",
             "useInTable": "staticdnsentry"
         },
         {
             "description": "HTTP Content Routing, no caching",
-            "lastUpdated": "2018-03-02T19:13:46.824798+00:00",
             "name": "HTTP_NO_CACHE",
             "useInTable": "deliveryservice"
         },
         {
             "description": "any_map type",
-            "lastUpdated": "2018-03-02T19:13:46.826411+00:00",
             "name": "ANY_MAP",
             "useInTable": "deliveryservice"
         },
         {
             "description": "Steering Delivery Service",
-            "lastUpdated": "2018-03-02T19:13:46.827779+00:00",
             "name": "STEERING",
             "useInTable": "deliveryservice"
         },
         {
             "description": "Edge Cachegroup",
-            "lastUpdated": "2018-03-02T19:13:46.829249+00:00",
             "name": "EDGE_LOC",
             "useInTable": "cachegroup"
         },
         {
             "description": "HTTP Content routing cache ",
-            "lastUpdated": "2018-03-02T19:13:46.830862+00:00",
             "name": "HTTP",
             "useInTable": "deliveryservice"
         },
         {
             "description": "Mid Tier Cache",
-            "lastUpdated": "2018-03-02T19:13:46.832327+00:00",
             "name": "MID",
             "useInTable": "server"
         },
         {
             "description": "Traffic Monitor (Rascal)",
-            "lastUpdated": "2018-03-02T19:13:46.832327+00:00",
             "name": "RASCAL",
             "useInTable": "server"
         }
diff --git a/traffic_ops/traffic_ops_golang/dbhelpers/db_helpers.go b/traffic_ops/traffic_ops_golang/dbhelpers/db_helpers.go
index 2f84ba2..575cd3b 100644
--- a/traffic_ops/traffic_ops_golang/dbhelpers/db_helpers.go
+++ b/traffic_ops/traffic_ops_golang/dbhelpers/db_helpers.go
@@ -908,8 +908,10 @@ func TopologyExists(tx *sql.Tx, name string) (bool, error) {
 	return count > 0, err
 }
 
-// CheckTopology returns an error if the given Topology does not exist or if one of the Topology's Cache Groups is
-// empty with respect to the Delivery Service's CDN.
+// CheckTopology returns an error if the given Topology does not exist or if
+// one of the Topology's Cache Groups is empty with respect to the Delivery
+// Service's CDN. Note that this can panic if ds does not have a properly set
+// CDNID.
 func CheckTopology(tx *sqlx.Tx, ds tc.DeliveryServiceV4) (int, error, error) {
 	statusCode, userErr, sysErr := http.StatusOK, error(nil), error(nil)
 
@@ -926,7 +928,7 @@ func CheckTopology(tx *sqlx.Tx, ds tc.DeliveryServiceV4) (int, error, error) {
 	}
 
 	if err = topology_validation.CheckForEmptyCacheGroups(tx, cacheGroupIDs, []int{*ds.CDNID}, true, []int{}); err != nil {
-		return http.StatusBadRequest, fmt.Errorf("empty cachegroups in Topology %s found for CDN %d: %s", *ds.Topology, *ds.CDNID, err.Error()), nil
+		return http.StatusBadRequest, fmt.Errorf("empty cachegroups in Topology %s found for CDN %d: %w", *ds.Topology, *ds.CDNID, err), nil
 	}
 
 	return statusCode, userErr, sysErr
diff --git a/traffic_ops/traffic_ops_golang/deliveryservice/deliveryservices.go b/traffic_ops/traffic_ops_golang/deliveryservice/deliveryservices.go
index 68ad789..2515fdc 100644
--- a/traffic_ops/traffic_ops_golang/deliveryservice/deliveryservices.go
+++ b/traffic_ops/traffic_ops_golang/deliveryservice/deliveryservices.go
@@ -110,6 +110,24 @@ func (ds *TODeliveryService) IsTenantAuthorized(user *auth.CurrentUser) (bool, e
 	return isTenantAuthorized(ds.ReqInfo, &ds.DeliveryServiceV4)
 }
 
+const baseTLSVersionsQuery = `SELECT ARRAY_AGG(tls_version ORDER BY tls_version) FROM deliveryservice_tls_version`
+
+const getTLSVersionsQuery = baseTLSVersionsQuery + `
+WHERE deliveryservice = $1
+`
+
+// GetDSTLSVersions retrieves the TLS versions explicitly supported by a
+// Delivery Service identified by dsID. This will panic if handed a nil
+// transaction.
+func GetDSTLSVersions(dsID int, tx *sql.Tx) ([]string, error) {
+	var vers []string
+	err := tx.QueryRow(getTLSVersionsQuery, dsID).Scan(pq.Array(&vers))
+	if err != nil {
+		err = fmt.Errorf("querying: %w", err)
+	}
+	return vers, err
+}
+
 func CreateV12(w http.ResponseWriter, r *http.Request) {
 	inf, userErr, sysErr, errCode := api.NewInfo(r, nil, nil)
 	if userErr != nil || sysErr != nil {
@@ -129,7 +147,7 @@ func CreateV12(w http.ResponseWriter, r *http.Request) {
 		api.HandleErr(w, r, inf.Tx.Tx, status, userErr, sysErr)
 		return
 	}
-	api.WriteRespAlertObj(w, r, tc.SuccessLevel, "Deliveryservice creation was successful.", []tc.DeliveryServiceNullableV12{*res})
+	api.WriteRespAlertObj(w, r, tc.SuccessLevel, "Delivery Service creation was successful", []tc.DeliveryServiceNullableV12{*res})
 }
 func CreateV13(w http.ResponseWriter, r *http.Request) {
 	inf, userErr, sysErr, errCode := api.NewInfo(r, nil, nil)
@@ -150,7 +168,7 @@ func CreateV13(w http.ResponseWriter, r *http.Request) {
 		api.HandleErr(w, r, inf.Tx.Tx, status, userErr, sysErr)
 		return
 	}
-	api.WriteRespAlertObj(w, r, tc.SuccessLevel, "Deliveryservice creation was successful.", []tc.DeliveryServiceNullableV13{*res})
+	api.WriteRespAlertObj(w, r, tc.SuccessLevel, "Delivery Service creation was successful", []tc.DeliveryServiceNullableV13{*res})
 }
 func CreateV14(w http.ResponseWriter, r *http.Request) {
 	inf, userErr, sysErr, errCode := api.NewInfo(r, nil, nil)
@@ -171,7 +189,7 @@ func CreateV14(w http.ResponseWriter, r *http.Request) {
 		api.HandleErr(w, r, inf.Tx.Tx, status, userErr, sysErr)
 		return
 	}
-	api.WriteRespAlertObj(w, r, tc.SuccessLevel, "Deliveryservice creation was successful.", []tc.DeliveryServiceNullableV14{*res})
+	api.WriteRespAlertObj(w, r, tc.SuccessLevel, "Delivery Service creation was successful", []tc.DeliveryServiceNullableV14{*res})
 }
 
 // 	TODO allow users to post names (type, cdn, etc) and get the IDs from the names. This isn't trivial to do in a single query, without dynamically building the entire insert query, and ideally inserting would be one query. But it'd be much more convenient for users. Alternatively, remove IDs from the database entirely and use real candidate keys.
@@ -194,7 +212,7 @@ func CreateV15(w http.ResponseWriter, r *http.Request) {
 		api.HandleErr(w, r, inf.Tx.Tx, status, userErr, sysErr)
 		return
 	}
-	api.WriteRespAlertObj(w, r, tc.SuccessLevel, "Deliveryservice creation was successful.", []tc.DeliveryServiceNullableV15{*res})
+	api.WriteRespAlertObj(w, r, tc.SuccessLevel, "Delivery Service creation was successful", []tc.DeliveryServiceNullableV15{*res})
 }
 func CreateV30(w http.ResponseWriter, r *http.Request) {
 	inf, userErr, sysErr, errCode := api.NewInfo(r, nil, nil)
@@ -215,7 +233,7 @@ func CreateV30(w http.ResponseWriter, r *http.Request) {
 		api.HandleErr(w, r, inf.Tx.Tx, status, userErr, sysErr)
 		return
 	}
-	api.WriteRespAlertObj(w, r, tc.SuccessLevel, "Deliveryservice creation was successful.", []tc.DeliveryServiceV30{*res})
+	api.WriteRespAlertObj(w, r, tc.SuccessLevel, "Delivery Service creation was successful", []tc.DeliveryServiceV30{*res})
 }
 func CreateV31(w http.ResponseWriter, r *http.Request) {
 	inf, userErr, sysErr, errCode := api.NewInfo(r, nil, nil)
@@ -236,7 +254,7 @@ func CreateV31(w http.ResponseWriter, r *http.Request) {
 		api.HandleErr(w, r, inf.Tx.Tx, status, userErr, sysErr)
 		return
 	}
-	api.WriteRespAlertObj(w, r, tc.SuccessLevel, "Deliveryservice creation was successful.", []tc.DeliveryServiceV31{*res})
+	api.WriteRespAlertObj(w, r, tc.SuccessLevel, "Delivery Service creation was successful", []tc.DeliveryServiceV31{*res})
 }
 func CreateV40(w http.ResponseWriter, r *http.Request) {
 	inf, userErr, sysErr, errCode := api.NewInfo(r, nil, nil)
@@ -257,7 +275,10 @@ func CreateV40(w http.ResponseWriter, r *http.Request) {
 		api.HandleErr(w, r, inf.Tx.Tx, status, userErr, sysErr)
 		return
 	}
-	api.WriteRespAlertObj(w, r, tc.SuccessLevel, "Deliveryservice creation was successful.", []tc.DeliveryServiceV40{*res})
+	alerts := res.TLSVersionsAlerts()
+	alerts.AddNewAlert(tc.SuccessLevel, "Delivery Service creation was successful")
+	w.Header().Set("Location", fmt.Sprintf("/api/4.0/deliveryservices?id=%d", *res.ID))
+	api.WriteAlertsObj(w, r, http.StatusCreated, alerts, []tc.DeliveryServiceV40{*res})
 }
 
 func createV12(w http.ResponseWriter, r *http.Request, inf *api.APIInfo, reqDS tc.DeliveryServiceNullableV12) (*tc.DeliveryServiceNullableV12, int, error, error) {
@@ -328,14 +349,42 @@ func createV31(w http.ResponseWriter, r *http.Request, inf *api.APIInfo, dsV31 t
 	return &oldRes, status, userErr, sysErr
 }
 
+// 'ON CONFLICT DO NOTHING' should be unnecessary because all data should be
+// dumped from the table before re-insertion, but it's also harmless because
+// the only conflict that could occur is a fully duplicate row, which is fine
+// since we're intending to create that data anyway. Although it is weird.
+const insertTLSVersionsQuery = `
+INSERT INTO public.deliveryservice_tls_version (deliveryservice, tls_version)
+	SELECT
+		$1 AS deliveryservice,
+		UNNEST($2::text[]) AS tls_version
+ON CONFLICT DO NOTHING
+`
+
+func recreateTLSVersions(versions []string, dsid int, tx *sql.Tx) error {
+	_, err := tx.Exec(`DELETE FROM public.deliveryservice_tls_version WHERE deliveryservice = $1`, dsid)
+	if err != nil {
+		return fmt.Errorf("cleaning up existing TLS version for DS #%d: %w", dsid, err)
+	}
+
+	if len(versions) < 1 {
+		return nil
+	}
+
+	_, err = tx.Exec(insertTLSVersionsQuery, dsid, pq.Array(versions))
+	if err != nil {
+		return fmt.Errorf("inserting new TLS versions: %w", err)
+	}
+	return nil
+}
+
 // create creates the given ds in the database, and returns the DS with its id and other fields created on insert set. On error, the HTTP status code, user error, and system error are returned. The status code SHOULD NOT be used, if both errors are nil.
 func createV40(w http.ResponseWriter, r *http.Request, inf *api.APIInfo, dsV40 tc.DeliveryServiceV40, omitExtraLongDescFields bool) (*tc.DeliveryServiceV40, int, error, error) {
-	var resultRows *sql.Rows
-	var err error
 	user := inf.User
 	tx := inf.Tx.Tx
 	ds := tc.DeliveryServiceV4(dsV40)
-	if err := Validate(tx, &ds); err != nil {
+	err := Validate(tx, &ds)
+	if err != nil {
 		return nil, http.StatusBadRequest, errors.New("invalid request: " + err.Error()), nil
 	}
 
@@ -354,6 +403,7 @@ func createV40(w http.ResponseWriter, r *http.Request, inf *api.APIInfo, dsV40 t
 	if errCode, userErr, sysErr := dbhelpers.CheckTopology(inf.Tx, ds); userErr != nil || sysErr != nil {
 		return nil, errCode, userErr, sysErr
 	}
+	var resultRows *sql.Rows
 	if omitExtraLongDescFields {
 		if ds.LongDesc1 != nil || ds.LongDesc2 != nil {
 			return nil, http.StatusBadRequest, errors.New("the longDesc1 and longDesc2 fields are no longer supported in API 4.0 onwards"), nil
@@ -488,7 +538,7 @@ func createV40(w http.ResponseWriter, r *http.Request, inf *api.APIInfo, dsV40 t
 	defer resultRows.Close()
 
 	id := 0
-	lastUpdated := tc.TimeNoMod{}
+	var lastUpdated tc.TimeNoMod
 	if !resultRows.Next() {
 		return nil, http.StatusInternalServerError, nil, errors.New("no deliveryservice request inserted, no id was returned")
 	}
@@ -507,14 +557,24 @@ func createV40(w http.ResponseWriter, r *http.Request, inf *api.APIInfo, dsV40 t
 		return nil, http.StatusInternalServerError, nil, errors.New("missing xml_id after insert")
 	}
 	if ds.TypeID == nil {
-		return nil, http.StatusInternalServerError, nil, errors.New("missing type after insert")
+		return nil, http.StatusInternalServerError, nil, errors.New("missing type id after insert")
+	}
+	if ds.RoutingName == nil {
+		return nil, http.StatusInternalServerError, nil, errors.New("missing routing name after insert")
 	}
+
 	dsType, err := getTypeFromID(*ds.TypeID, tx)
 	if err != nil {
 		return nil, http.StatusInternalServerError, nil, errors.New("getting delivery service type: " + err.Error())
 	}
 	ds.Type = &dsType
 
+	if len(ds.TLSVersions) < 1 {
+		ds.TLSVersions = nil
+	} else if err = recreateTLSVersions(ds.TLSVersions, *ds.ID, tx); err != nil {
+		return nil, http.StatusInternalServerError, nil, fmt.Errorf("creating TLS versions for new Delivery Service: %w", err)
+	}
+
 	if err := createDefaultRegex(tx, *ds.ID, *ds.XMLID); err != nil {
 		return nil, http.StatusInternalServerError, nil, errors.New("creating default regex: " + err.Error())
 	}
@@ -563,7 +623,8 @@ func createV40(w http.ResponseWriter, r *http.Request, inf *api.APIInfo, dsV40 t
 		return nil, http.StatusInternalServerError, nil, errors.New("error writing to audit log: " + err.Error())
 	}
 
-	dsV40 = tc.DeliveryServiceV40(ds)
+	dsV40 = ds
+
 	return &dsV40, http.StatusOK, nil, nil
 }
 
@@ -659,7 +720,7 @@ func UpdateV12(w http.ResponseWriter, r *http.Request) {
 		api.HandleErr(w, r, inf.Tx.Tx, status, userErr, sysErr)
 		return
 	}
-	api.WriteRespAlertObj(w, r, tc.SuccessLevel, "Deliveryservice update was successful.", []tc.DeliveryServiceNullableV12{*res})
+	api.WriteRespAlertObj(w, r, tc.SuccessLevel, "Delivery Service update was successful", []tc.DeliveryServiceNullableV12{*res})
 }
 func UpdateV13(w http.ResponseWriter, r *http.Request) {
 	inf, userErr, sysErr, errCode := api.NewInfo(r, nil, []string{"id"})
@@ -683,7 +744,7 @@ func UpdateV13(w http.ResponseWriter, r *http.Request) {
 		api.HandleErr(w, r, inf.Tx.Tx, status, userErr, sysErr)
 		return
 	}
-	api.WriteRespAlertObj(w, r, tc.SuccessLevel, "Deliveryservice update was successful.", []tc.DeliveryServiceNullableV13{*res})
+	api.WriteRespAlertObj(w, r, tc.SuccessLevel, "Delivery Service update was successful", []tc.DeliveryServiceNullableV13{*res})
 }
 func UpdateV14(w http.ResponseWriter, r *http.Request) {
 	inf, userErr, sysErr, errCode := api.NewInfo(r, nil, []string{"id"})
@@ -707,7 +768,7 @@ func UpdateV14(w http.ResponseWriter, r *http.Request) {
 		api.HandleErr(w, r, inf.Tx.Tx, status, userErr, sysErr)
 		return
 	}
-	api.WriteRespAlertObj(w, r, tc.SuccessLevel, "Deliveryservice update was successful.", []tc.DeliveryServiceNullableV14{*res})
+	api.WriteRespAlertObj(w, r, tc.SuccessLevel, "Delivery Service update was successful", []tc.DeliveryServiceNullableV14{*res})
 }
 func UpdateV15(w http.ResponseWriter, r *http.Request) {
 	inf, userErr, sysErr, errCode := api.NewInfo(r, nil, []string{"id"})
@@ -731,7 +792,7 @@ func UpdateV15(w http.ResponseWriter, r *http.Request) {
 		api.HandleErr(w, r, inf.Tx.Tx, status, userErr, sysErr)
 		return
 	}
-	api.WriteRespAlertObj(w, r, tc.SuccessLevel, "Deliveryservice update was successful.", []tc.DeliveryServiceNullableV15{*res})
+	api.WriteRespAlertObj(w, r, tc.SuccessLevel, "Delivery Service update was successful", []tc.DeliveryServiceNullableV15{*res})
 }
 func UpdateV30(w http.ResponseWriter, r *http.Request) {
 	inf, userErr, sysErr, errCode := api.NewInfo(r, nil, []string{"id"})
@@ -755,7 +816,7 @@ func UpdateV30(w http.ResponseWriter, r *http.Request) {
 		api.HandleErr(w, r, inf.Tx.Tx, status, userErr, sysErr)
 		return
 	}
-	api.WriteRespAlertObj(w, r, tc.SuccessLevel, "Deliveryservice update was successful.", []tc.DeliveryServiceV30{*res})
+	api.WriteRespAlertObj(w, r, tc.SuccessLevel, "Delivery Service update was successful", []tc.DeliveryServiceV30{*res})
 }
 func UpdateV31(w http.ResponseWriter, r *http.Request) {
 	inf, userErr, sysErr, errCode := api.NewInfo(r, nil, []string{"id"})
@@ -778,7 +839,7 @@ func UpdateV31(w http.ResponseWriter, r *http.Request) {
 		api.HandleErr(w, r, inf.Tx.Tx, status, userErr, sysErr)
 		return
 	}
-	api.WriteRespAlertObj(w, r, tc.SuccessLevel, "Deliveryservice update was successful.", []tc.DeliveryServiceV31{*res})
+	api.WriteRespAlertObj(w, r, tc.SuccessLevel, "Delivery Service update was successful", []tc.DeliveryServiceV31{*res})
 }
 func UpdateV40(w http.ResponseWriter, r *http.Request) {
 	inf, userErr, sysErr, errCode := api.NewInfo(r, nil, []string{"id"})
@@ -801,7 +862,9 @@ func UpdateV40(w http.ResponseWriter, r *http.Request) {
 		api.HandleErr(w, r, inf.Tx.Tx, status, userErr, sysErr)
 		return
 	}
-	api.WriteRespAlertObj(w, r, tc.SuccessLevel, "Deliveryservice update was successful.", []tc.DeliveryServiceV40{*res})
+	alerts := res.TLSVersionsAlerts()
+	alerts.AddNewAlert(tc.SuccessLevel, "Delivery Service update was successful")
+	api.WriteAlertsObj(w, r, http.StatusOK, alerts, []tc.DeliveryServiceV40{*res})
 }
 
 func updateV12(w http.ResponseWriter, r *http.Request, inf *api.APIInfo, reqDS *tc.DeliveryServiceNullableV12) (*tc.DeliveryServiceNullableV12, int, error, error) {
@@ -955,13 +1018,22 @@ WHERE
 func updateV31(w http.ResponseWriter, r *http.Request, inf *api.APIInfo, dsV31 *tc.DeliveryServiceV31) (*tc.DeliveryServiceV31, int, error, error) {
 	dsNull := tc.DeliveryServiceNullableV30(*dsV31)
 	ds := dsNull.UpgradeToV4()
-	dsV40 := tc.DeliveryServiceV40(ds)
+	dsV40 := ds
+	if dsV40.ID == nil {
+		return nil, http.StatusInternalServerError, nil, errors.New("cannot update a Delivery Service with nil ID")
+	}
+
 	tx := inf.Tx.Tx
+	var sysErr error
+	if dsV40.TLSVersions, sysErr = GetDSTLSVersions(*dsV40.ID, tx); sysErr != nil {
+		return nil, http.StatusInternalServerError, nil, fmt.Errorf("getting TLS versions for DS #%d in API version < 4.0: %w", *dsV40.ID, sysErr)
+	}
+
 	res, status, usrErr, sysErr := updateV40(w, r, inf, &dsV40, false)
-	if res == nil {
+	if res == nil || usrErr != nil || sysErr != nil {
 		return nil, status, usrErr, sysErr
 	}
-	ds = tc.DeliveryServiceV4(*res)
+	ds = *res
 	if dsV31.CacheURL != nil {
 		_, err := tx.Exec("UPDATE deliveryservice SET cacheurl = $1 WHERE ID = $2",
 			&dsV31.CacheURL,
@@ -980,8 +1052,6 @@ func updateV31(w http.ResponseWriter, r *http.Request, inf *api.APIInfo, dsV31 *
 	return &oldRes, http.StatusOK, nil, nil
 }
 func updateV40(w http.ResponseWriter, r *http.Request, inf *api.APIInfo, dsV40 *tc.DeliveryServiceV40, omitExtraLongDescFields bool) (*tc.DeliveryServiceV40, int, error, error) {
-	var resultRows *sql.Rows
-	var err error
 	tx := inf.Tx.Tx
 	user := inf.User
 	ds := tc.DeliveryServiceV4(*dsV40)
@@ -1021,7 +1091,7 @@ func updateV40(w http.ResponseWriter, r *http.Request, inf *api.APIInfo, dsV40 *
 			return nil, errCode, userErr, sysErr
 		}
 		if sslKeysExist, err = getSSLVersion(*ds.XMLID, tx); err != nil {
-			return nil, http.StatusInternalServerError, nil, fmt.Errorf("querying delivery service with sslKeyVersion failed: %s", err)
+			return nil, http.StatusInternalServerError, nil, fmt.Errorf("querying delivery service with sslKeyVersion failed: %w", err)
 		}
 		if ds.CDNID == nil {
 			return nil, http.StatusBadRequest, errors.New("invalid request: 'cdnId' cannot be blank"), nil
@@ -1075,6 +1145,7 @@ func updateV40(w http.ResponseWriter, r *http.Request, inf *api.APIInfo, dsV40 *
 		}
 	}
 
+	var resultRows *sql.Rows
 	if omitExtraLongDescFields {
 		if ds.LongDesc1 != nil || ds.LongDesc2 != nil {
 			return nil, http.StatusBadRequest, errors.New("the longDesc1 and longDesc2 fields are no longer supported in API 4.0 onwards"), nil
@@ -1210,7 +1281,7 @@ func updateV40(w http.ResponseWriter, r *http.Request, inf *api.APIInfo, dsV40 *
 	if !resultRows.Next() {
 		return nil, http.StatusNotFound, errors.New("no delivery service found with this id"), nil
 	}
-	lastUpdated := tc.TimeNoMod{}
+	var lastUpdated tc.TimeNoMod
 	if err := resultRows.Scan(&lastUpdated); err != nil {
 		return nil, http.StatusInternalServerError, nil, errors.New("scan updating delivery service: " + err.Error())
 	}
@@ -1220,20 +1291,29 @@ func updateV40(w http.ResponseWriter, r *http.Request, inf *api.APIInfo, dsV40 *
 			xmlID = *ds.XMLID
 		}
 		return nil, http.StatusInternalServerError, nil, errors.New("updating delivery service " + xmlID + ": " + "this update affected too many rows: > 1")
-	}
 
+	}
 	if ds.ID == nil {
 		return nil, http.StatusInternalServerError, nil, errors.New("missing id after update")
 	}
 	if ds.XMLID == nil {
-		return nil, http.StatusInternalServerError, nil, errors.New("missing xml_id after update")
+		return nil, http.StatusInternalServerError, nil, errors.New("missing XMLID after update")
 	}
 	if ds.TypeID == nil {
-		return nil, http.StatusInternalServerError, nil, errors.New("missing type after update")
+		return nil, http.StatusInternalServerError, nil, errors.New("missing type id after update")
 	}
 	if ds.RoutingName == nil {
 		return nil, http.StatusInternalServerError, nil, errors.New("missing routing name after update")
 	}
+
+	if len(ds.TLSVersions) < 1 {
+		ds.TLSVersions = nil
+	}
+	err = recreateTLSVersions(ds.TLSVersions, *ds.ID, tx)
+	if err != nil {
+		return nil, http.StatusInternalServerError, nil, fmt.Errorf("updating TLS versions for DS #%d: %w", *ds.ID, err)
+	}
+
 	newDSType, err := getTypeFromID(*ds.TypeID, tx)
 	if err != nil {
 		return nil, http.StatusInternalServerError, nil, errors.New("getting delivery service type after update: " + err.Error())
@@ -1270,7 +1350,7 @@ func updateV40(w http.ResponseWriter, r *http.Request, inf *api.APIInfo, dsV40 *
 	// the update may change or delete the query params -- delete existing and re-add if any provided
 	q := `DELETE FROM deliveryservice_consistent_hash_query_param WHERE deliveryservice_id = $1`
 	if res, err := tx.Exec(q, *ds.ID); err != nil {
-		return nil, http.StatusInternalServerError, nil, fmt.Errorf("deleting consistent hash query params for ds %s: %s", *ds.XMLID, err.Error())
+		return nil, http.StatusInternalServerError, nil, fmt.Errorf("deleting consistent hash query params for ds %s: %w", *ds.XMLID, err)
 	} else if c, _ := res.RowsAffected(); c > 0 {
 		api.CreateChangeLogRawTx(api.ApiChange, "DS: "+*ds.XMLID+", ID: "+strconv.Itoa(*ds.ID)+", ACTION: Deleted "+strconv.FormatInt(c, 10)+" consistent hash query params", user, tx)
 	}
@@ -1283,6 +1363,7 @@ func updateV40(w http.ResponseWriter, r *http.Request, inf *api.APIInfo, dsV40 *
 	if err := api.CreateChangeLogRawErr(api.ApiChange, "Updated ds: "+*ds.XMLID+" id: "+strconv.Itoa(*ds.ID), user, tx); err != nil {
 		return nil, http.StatusInternalServerError, nil, errors.New("writing change log entry: " + err.Error())
 	}
+
 	dsV40 = (*tc.DeliveryServiceV40)(&ds)
 	return dsV40, http.StatusOK, nil, nil
 }
@@ -1450,6 +1531,8 @@ func requiredIfMatchesTypeName(patterns []string, typeName string) func(interfac
 	}
 }
 
+var validTLSVersionPattern = regexp.MustCompile(`^\d+\.\d+$`)
+
 func Validate(tx *sql.Tx, ds *tc.DeliveryServiceV4) error {
 	sanitize(ds)
 	neverOrAlways := validation.NewStringRule(tovalidate.IsOneOfStringICase("NEVER", "ALWAYS"),
@@ -1472,6 +1555,25 @@ func Validate(tx *sql.Tx, ds *tc.DeliveryServiceV4) error {
 		"routingName":         validation.Validate(ds.RoutingName, isDNSName, noPeriods, validation.Length(1, 48)),
 		"typeId":              validation.Validate(ds.TypeID, validation.Required, validation.Min(1)),
 		"xmlId":               validation.Validate(ds.XMLID, validation.Required, noSpaces, noPeriods, validation.Length(1, 48)),
+		"tlsVersions": validation.Validate(ds.TLSVersions, validation.By(
+			func(value interface{}) error {
+				vers, ok := value.([]string)
+				if !ok {
+					return fmt.Errorf("must be an array of string, got: %T", value)
+				}
+				seen := make(map[string]struct{}, len(vers))
+				for _, tlsVersion := range vers {
+					if _, ok := seen[tlsVersion]; ok {
+						return fmt.Errorf("duplicate version '%s'", tlsVersion)
+					}
+					seen[tlsVersion] = struct{}{}
+					if !validTLSVersionPattern.Match([]byte(tlsVersion)) {
+						return fmt.Errorf("invalid TLS version '%s'", tlsVersion)
+					}
+				}
+				return nil
+			},
+		)),
 	})
 	if err := validateTopologyFields(ds); err != nil {
 		errs = append(errs, err)
@@ -1588,6 +1690,18 @@ func validateTypeFields(tx *sql.Tx, ds *tc.DeliveryServiceV4) error {
 			validation.By(requiredIfMatchesTypeName([]string{DNSRegexType, HTTPRegexType}, typeName))),
 		"rangeRequestHandling": validation.Validate(ds.RangeRequestHandling,
 			validation.By(requiredIfMatchesTypeName([]string{DNSRegexType, HTTPRegexType}, typeName))),
+		"tlsVersions": validation.Validate(
+			&ds.TLSVersions,
+			validation.By(
+				func(value interface{}) error {
+					vers := value.(*[]string)
+					if vers != nil && len(*vers) > 0 && tc.DSType(typeName).IsSteering() {
+						return errors.New("must be 'null' for STEERING-Type and CLIENT_STEERING-Type Delivery Services")
+					}
+					return nil
+				},
+			),
+		),
 		"topology": validation.Validate(ds,
 			validation.By(func(dsi interface{}) error {
 				ds := dsi.(*tc.DeliveryServiceV4)
@@ -1660,7 +1774,7 @@ func updatePrimaryOrigin(tx *sql.Tx, user *auth.CurrentUser, ds tc.DeliveryServi
 	count := 0
 	q := `SELECT count(*) FROM origin WHERE deliveryservice = $1 AND is_primary`
 	if err := tx.QueryRow(q, *ds.ID).Scan(&count); err != nil {
-		return fmt.Errorf("querying existing primary origin for ds %s: %s", *ds.XMLID, err.Error())
+		return fmt.Errorf("querying existing primary origin for ds %s: %w", *ds.XMLID, err)
 	}
 
 	if ds.OrgServerFQDN == nil || *ds.OrgServerFQDN == "" {
@@ -1668,7 +1782,7 @@ func updatePrimaryOrigin(tx *sql.Tx, user *auth.CurrentUser, ds tc.DeliveryServi
 			// the update is removing the existing orgServerFQDN, so the existing row needs to be deleted
 			q = `DELETE FROM origin WHERE deliveryservice = $1 AND is_primary`
 			if _, err := tx.Exec(q, *ds.ID); err != nil {
-				return fmt.Errorf("deleting primary origin for ds %s: %s", *ds.XMLID, err.Error())
+				return fmt.Errorf("deleting primary origin for ds %s: %w", *ds.XMLID, err)
 			}
 			api.CreateChangeLogRawTx(api.ApiChange, "DS: "+*ds.XMLID+", ID: "+strconv.Itoa(*ds.ID)+", ACTION: Deleted primary origin", user, tx)
 		}
@@ -1688,7 +1802,7 @@ func updatePrimaryOrigin(tx *sql.Tx, user *auth.CurrentUser, ds tc.DeliveryServi
 	name := ""
 	q = `UPDATE origin SET protocol = $1, fqdn = $2, port = $3 WHERE is_primary AND deliveryservice = $4 RETURNING name`
 	if err := tx.QueryRow(q, protocol, fqdn, port, *ds.ID).Scan(&name); err != nil {
-		return fmt.Errorf("update primary origin for ds %s from '%s': %s", *ds.XMLID, *ds.OrgServerFQDN, err.Error())
+		return fmt.Errorf("update primary origin for ds %s from '%s': %w", *ds.XMLID, *ds.OrgServerFQDN, err)
 	}
 
 	api.CreateChangeLogRawTx(api.ApiChange, "DS: "+*ds.XMLID+", ID: "+strconv.Itoa(*ds.ID)+", ACTION: Updated primary origin: "+name, user, tx)
@@ -1806,6 +1920,7 @@ func GetDeliveryServices(query string, queryValues map[string]interface{}, tx *s
 			&ds.SSLKeyVersion,
 			&ds.TenantID,
 			&ds.Tenant,
+			pq.Array(&ds.TLSVersions),
 			&ds.Topology,
 			&ds.TRRequestHeaders,
 			&ds.TRResponseHeaders,
@@ -1835,8 +1950,13 @@ func GetDeliveryServices(query string, queryValues map[string]interface{}, tx *s
 		if ds.DeepCachingType != nil {
 			*ds.DeepCachingType = tc.DeepCachingTypeFromString(string(*ds.DeepCachingType))
 		}
+
 		ds.Signed = ds.SigningAlgorithm != nil && *ds.SigningAlgorithm == tc.SigningAlgorithmURLSig
 
+		if len(ds.TLSVersions) < 1 {
+			ds.TLSVersions = nil
+		}
+
 		dses = append(dses, ds)
 	}
 
@@ -1882,7 +2002,7 @@ func getCDNNameDomainDNSSecEnabled(dsID int, tx *sql.Tx) (string, string, bool,
 	return cdnName, cdnDomain, dnssecEnabled, nil
 }
 
-// makeExampleURLs creates the example URLs for a delivery service. The dsProtocol may be nil, if the delivery service type doesn't have a protocol (e.g. ANY_MAP).
+// MakeExampleURLs creates the example URLs for a delivery service. The dsProtocol may be nil, if the delivery service type doesn't have a protocol (e.g. ANY_MAP).
 func MakeExampleURLs(protocol *int, dsType tc.DSType, routingName string, matchList []tc.DeliveryServiceMatch, cdnDomain string) []string {
 	examples := []string{}
 	scheme := ""
@@ -2119,10 +2239,13 @@ func deleteLocationParam(tx *sql.Tx, configFile string) error {
 	return nil
 }
 
-// getTenantID returns the tenant Id of the given delivery service. Note it may return a nil id and nil error, if the tenant ID in the database is nil.
+// getTenantID returns the tenant Id of the given delivery service.
+// Note it may return a nil id and nil error, if the tenant ID in the database
+// is nil.
+// This will panic if the transaction is nil.
 func getTenantID(tx *sql.Tx, ds *tc.DeliveryServiceV4) (*int, error) {
-	if ds.ID == nil && ds.XMLID == nil {
-		return nil, errors.New("delivery service has no ID or XMLID")
+	if ds == nil || (ds.ID == nil && ds.XMLID == nil) {
+		return nil, errors.New("delivery service was nil, or had nil identifiers (ID and XMLID)")
 	}
 	if ds.ID != nil {
 		existingID, _, err := getDSTenantIDByID(tx, *ds.ID) // ignore exists return - if the DS is new, we only need to check the user input tenant
@@ -2327,6 +2450,7 @@ ds.active,
 	ds.ssl_key_version,
 	ds.tenant_id,
 	tenant.name,
+	(` + baseTLSVersionsQuery + ` WHERE deliveryservice = ds.id) AS tls_versions,
 	ds.topology,
 	ds.tr_request_headers,
 	ds.tr_response_headers,
diff --git a/traffic_ops/traffic_ops_golang/deliveryservice/deliveryservices_test.go b/traffic_ops/traffic_ops_golang/deliveryservice/deliveryservices_test.go
index cb0adef..5498c8e 100644
--- a/traffic_ops/traffic_ops_golang/deliveryservice/deliveryservices_test.go
+++ b/traffic_ops/traffic_ops_golang/deliveryservice/deliveryservices_test.go
@@ -20,8 +20,10 @@ package deliveryservice
  */
 
 import (
+	"fmt"
 	"net/http"
 	"reflect"
+	"strings"
 	"testing"
 	"time"
 
@@ -126,6 +128,41 @@ func TestGetDeliveryServicesMatchLists(t *testing.T) {
 	}
 }
 
+func TestGetDSTLSVersions(t *testing.T) {
+	mockDB, mock, err := sqlmock.New()
+	if err != nil {
+		t.Fatalf("Unexpected error opening a stub database connection: %v", err)
+	}
+	defer func() {
+		if err := mockDB.Close(); err != nil {
+			t.Errorf("Failed to close database: %v", err)
+		}
+	}()
+
+	db := sqlx.NewDb(mockDB, "sqlmock")
+	defer func() {
+		if err := db.Close(); err != nil {
+			t.Errorf("Failed to close sqlx DB handle: %v", err)
+		}
+	}()
+
+	rows := sqlmock.NewRows([]string{"tls_version"})
+	expected := []string{"1.0", "1.1", "1.2", "1.3"}
+	rows.AddRow(fmt.Sprintf("{%s}", strings.Join(expected, ",")))
+
+	mock.ExpectBegin()
+	mock.ExpectQuery("SELECT").WillReturnRows(rows)
+
+	vers, err := GetDSTLSVersions(0, db.MustBegin().Tx)
+	if err != nil {
+		t.Errorf("Unexpected error getting DS TLS Versions: %v", err)
+	} else if len(vers) != 4 {
+		t.Errorf("Expected to get 4 TLS versions, got: %d", len(vers))
+	} else if !reflect.DeepEqual(expected, vers) {
+		t.Errorf("Incorrect TLS versions returned, expected: %+v - actual: %+v", expected, vers)
+	}
+}
+
 func TestMakeExampleURLs(t *testing.T) {
 	expected := []string{
 		`http://routing-name.ds-name.domain-name.invalid`,
@@ -289,6 +326,7 @@ func TestReadGetDeliveryServices(t *testing.T) {
 		"ssl_key_version",
 		"tenant_id",
 		"tenant.name",
+		"tls_versions",
 		"topology",
 		"tr_request_headers",
 		"tr_response_headers",
@@ -360,6 +398,7 @@ func TestReadGetDeliveryServices(t *testing.T) {
 		nil,
 		1,
 		"test",
+		"{}",
 		nil,
 		nil,
 		nil,
diff --git a/traffic_ops/traffic_ops_golang/deliveryservice/request/requests.go b/traffic_ops/traffic_ops_golang/deliveryservice/request/requests.go
index 09e65f0..4c33da6 100644
--- a/traffic_ops/traffic_ops_golang/deliveryservice/request/requests.go
+++ b/traffic_ops/traffic_ops_golang/deliveryservice/request/requests.go
@@ -463,12 +463,18 @@ func createV4(w http.ResponseWriter, r *http.Request, inf *api.APIInfo) (result
 			api.HandleErr(w, r, tx, http.StatusBadRequest, errors.New("the longDesc1 and longDesc2 fields are no longer supported in API 4.0 onwards"), nil)
 			return
 		}
+		if len(dsr.Original.TLSVersions) < 1 {
+			dsr.Original.TLSVersions = nil
+		}
 	}
 	if dsr.Requested != nil {
 		if dsr.Requested.LongDesc1 != nil || dsr.Requested.LongDesc2 != nil {
 			api.HandleErr(w, r, tx, http.StatusBadRequest, errors.New("the longDesc1 and longDesc2 fields are no longer supported in API 4.0 onwards"), nil)
 			return
 		}
+		if len(dsr.Requested.TLSVersions) < 1 {
+			dsr.Requested.TLSVersions = nil
+		}
 	}
 	errCode, userErr, sysErr := insert(&dsr, inf)
 	if userErr != nil || sysErr != nil {
@@ -731,6 +737,13 @@ func putV40(w http.ResponseWriter, r *http.Request, inf *api.APIInfo) (result ds
 	dsr.LastEditedByID = new(int)
 	*dsr.LastEditedByID = inf.User.ID
 
+	if dsr.Requested != nil && len(dsr.Requested.TLSVersions) < 1 {
+		dsr.Requested.TLSVersions = nil
+	}
+	if dsr.Original != nil && len(dsr.Original.TLSVersions) < 1 {
+		dsr.Original.TLSVersions = nil
+	}
+
 	args := []interface{}{
 		dsr.AssigneeID,
 		dsr.ChangeType,
diff --git a/traffic_ops/traffic_ops_golang/deliveryservice/request/validate.go b/traffic_ops/traffic_ops_golang/deliveryservice/request/validate.go
index 49e47ea..8bf4dc6 100644
--- a/traffic_ops/traffic_ops_golang/deliveryservice/request/validate.go
+++ b/traffic_ops/traffic_ops_golang/deliveryservice/request/validate.go
@@ -56,7 +56,7 @@ func validateLegacy(dsr tc.DeliveryServiceRequestNullable, tx *sql.Tx) error {
 		}
 		toStatus, ok := s.(*tc.RequestStatus)
 		if !ok {
-			return fmt.Errorf("Expected *tc.RequestStatus type,  got %T", s)
+			return fmt.Errorf("expected *tc.RequestStatus type, got %T", s)
 		}
 		return fromStatus.ValidTransition(*toStatus)
 	}
diff --git a/traffic_ops/traffic_ops_golang/deliveryservice/safe.go b/traffic_ops/traffic_ops_golang/deliveryservice/safe.go
index bd9a891..616d793 100644
--- a/traffic_ops/traffic_ops_golang/deliveryservice/safe.go
+++ b/traffic_ops/traffic_ops_golang/deliveryservice/safe.go
@@ -125,17 +125,39 @@ func UpdateSafe(w http.ResponseWriter, r *http.Request) {
 		ds = ds.RemoveLD1AndLD2()
 	}
 	alertMsg := "Delivery Service safe update successful."
-	if inf.Version != nil && inf.Version.Major == 1 && inf.Version.Minor < 5 {
-		switch inf.Version.Minor {
+	if inf.Version == nil {
+		log.Warnln("API version found to be null in DS safe update")
+		api.WriteRespAlertObj(w, r, tc.SuccessLevel, alertMsg, dses)
+	} else {
+		switch inf.Version.Major {
+		// treat unknown versions as latest
+		default:
+			fallthrough
 		case 4:
-			api.WriteRespAlertObj(w, r, tc.SuccessLevel, alertMsg, []tc.DeliveryServiceNullableV14{ds.DowngradeToV3().DeliveryServiceNullableV14})
+			api.WriteRespAlertObj(w, r, tc.SuccessLevel, alertMsg, dses)
 		case 3:
-			api.WriteRespAlertObj(w, r, tc.SuccessLevel, alertMsg, []tc.DeliveryServiceNullableV13{ds.DowngradeToV3().DeliveryServiceNullableV13})
-		default:
-			api.WriteRespAlertObj(w, r, tc.SuccessLevel, alertMsg, []tc.DeliveryServiceNullableV12{ds.DowngradeToV3().DeliveryServiceNullableV12})
+			if inf.Version.Minor >= 1 {
+				api.WriteRespAlertObj(w, r, tc.SuccessLevel, alertMsg, []tc.DeliveryServiceV31{tc.DeliveryServiceV31(ds.DowngradeToV3())})
+			}
+			api.WriteRespAlertObj(w, r, tc.SuccessLevel, alertMsg, []tc.DeliveryServiceV30{ds.DowngradeToV3().DeliveryServiceV30})
+		case 2:
+			api.WriteRespAlertObj(w, r, tc.SuccessLevel, alertMsg, []tc.DeliveryServiceNullableV15{ds.DowngradeToV3().DeliveryServiceNullableV15})
+		case 1:
+			switch inf.Version.Minor {
+			default:
+				fallthrough
+			case 5:
+				api.WriteRespAlertObj(w, r, tc.SuccessLevel, alertMsg, []tc.DeliveryServiceNullableV15{ds.DowngradeToV3().DeliveryServiceNullableV15})
+			case 4:
+				api.WriteRespAlertObj(w, r, tc.SuccessLevel, alertMsg, []tc.DeliveryServiceNullableV14{ds.DowngradeToV3().DeliveryServiceNullableV14})
+			case 3:
+				api.WriteRespAlertObj(w, r, tc.SuccessLevel, alertMsg, []tc.DeliveryServiceNullableV13{ds.DowngradeToV3().DeliveryServiceNullableV13})
+			case 2:
+				fallthrough
+			case 1:
+				api.WriteRespAlertObj(w, r, tc.SuccessLevel, alertMsg, []tc.DeliveryServiceNullableV12{ds.DowngradeToV3().DeliveryServiceNullableV12})
+			}
 		}
-	} else {
-		api.WriteRespAlertObj(w, r, tc.SuccessLevel, alertMsg, []tc.DeliveryServiceNullableV30{ds.DowngradeToV3()})
 	}
 
 	api.CreateChangeLogRawTx(api.ApiChange, fmt.Sprintf("DS: %s, ID: %d, ACTION: Updated safe fields", *ds.XMLID, *ds.ID), inf.User, tx)
diff --git a/traffic_ops/v4-client/deliveryservice.go b/traffic_ops/v4-client/deliveryservice.go
index a77bc1b..7976311 100644
--- a/traffic_ops/v4-client/deliveryservice.go
+++ b/traffic_ops/v4-client/deliveryservice.go
@@ -75,7 +75,7 @@ const (
 	// apiDeliveryServiceGenerateSSLKeys is the API path on which Traffic Ops will generate new SSL keys.
 	apiDeliveryServiceGenerateSSLKeys = apiDeliveryServices + "/sslkeys/generate"
 
-	// apiDeliveryServiceAddSSLKeys is the API path on which Traffic Ops will add SSL keys
+	// apiDeliveryServiceAddSSLKeys is the API path on which Traffic Ops will add SSL keys.
 	apiDeliveryServiceAddSSLKeys = apiDeliveryServices + "/sslkeys/add"
 
 	// apiDeliveryServiceURISigningKeys is the API path on which Traffic Ops serves information
@@ -257,7 +257,7 @@ func (to *Session) GenerateSSLKeysForDS(
 	return response, reqInf, err
 }
 
-// AddSSLKeysForDS adds SSL Keys for the given DS
+// AddSSLKeysForDS adds SSL Keys for the given Delivery Service.
 func (to *Session) AddSSLKeysForDS(request tc.DeliveryServiceAddSSLKeysReq, opts RequestOptions) (tc.SSLKeysAddResponse, toclientlib.ReqInf, error) {
 	var response tc.SSLKeysAddResponse
 	reqInf, err := to.post(apiDeliveryServiceAddSSLKeys, opts, request, &response)
@@ -322,7 +322,7 @@ func (to *Session) GetDeliveryServiceURISigningKeys(dsName string, opts RequestO
 }
 
 // CreateDeliveryServiceURISigningKeys creates new URI-signing keys used by the Delivery Service
-// identified by the XMLID 'dsXMLID'
+// identified by the XMLID 'dsXMLID'.
 func (to *Session) CreateDeliveryServiceURISigningKeys(dsXMLID string, body map[string]tc.URISignerKeyset, opts RequestOptions) (tc.Alerts, toclientlib.ReqInf, error) {
 	var alerts tc.Alerts
 	reqInf, err := to.post(fmt.Sprintf(apiDeliveryServicesURISigningKeys, url.PathEscape(dsXMLID)), opts, body, &alerts)
@@ -330,7 +330,7 @@ func (to *Session) CreateDeliveryServiceURISigningKeys(dsXMLID string, body map[
 }
 
 // DeleteDeliveryServiceURISigningKeys deletes the URI-signing keys used by the Delivery Service
-// identified by the XMLID 'dsXMLID'
+// identified by the XMLID 'dsXMLID'.
 func (to *Session) DeleteDeliveryServiceURISigningKeys(dsXMLID string, opts RequestOptions) (tc.Alerts, toclientlib.ReqInf, error) {
 	var alerts tc.Alerts
 	reqInf, err := to.del(fmt.Sprintf(apiDeliveryServicesURISigningKeys, url.PathEscape(dsXMLID)), opts, &alerts)