You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@trafficcontrol.apache.org by rs...@apache.org on 2023/09/20 16:13:29 UTC

[trafficcontrol] branch master updated: `cdns/{{name}}/federations` and `cdns/{{name}}/federations/{{ID}}` use RFC3339 timestamps (#7806)

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

rshah 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 de900da884 `cdns/{{name}}/federations` and `cdns/{{name}}/federations/{{ID}}` use RFC3339 timestamps (#7806)
de900da884 is described below

commit de900da884f457ebd0039f7874e5c6338c5d10e0
Author: ocket8888 <oc...@apache.org>
AuthorDate: Wed Sep 20 10:13:22 2023 -0600

    `cdns/{{name}}/federations` and `cdns/{{name}}/federations/{{ID}}` use RFC3339 timestamps (#7806)
    
    * Fix 304 Not Modified responses written with body instead of LastModified header
    
    * add V5 structures
    
    * Update GET handlers to serve v5 structures to v5-requesting clients
    
    * Update POST handler to support v5
    
    * Update PUT handler to support v5
    
    * Update DELETE handler to support v5
    
    * Fix inaccuracies in old API version docs for cdnfederations
    
    * Fix RFC roles not using grave accents
    
    * Update client and client tests
    
    * Update CHANGELOG
    
    * Update TOTest
    
    * Fix dependency_license pattern
    
    * Update enroller
    
    * Fix comparison using pointers instead of values in tests
    
    * Fix improperly cased query string parameter
    
    * fix the selectMaxLastUpdatedQuery - and actually use it for that purpose
    
    * Implement IUS/ETag
    
    * Revert embedding queries from files
    
    * Add TTL minimum
    
    * Fix testing data to fit new restriction
---
 .dependency_license                                |   1 +
 CHANGELOG.md                                       |   1 +
 docs/source/api/index.rst                          |   2 +-
 docs/source/api/v3/cdns_name_federations.rst       |  14 +-
 docs/source/api/v4/cdns_name_federations.rst       |  14 +-
 docs/source/api/v4/deliveryservices.rst            |   4 +-
 docs/source/api/v4/deliveryservices_id.rst         |   2 +-
 docs/source/api/v4/deliveryservices_id_safe.rst    |   2 +-
 docs/source/api/v4/servers_id_deliveryservices.rst |   2 +-
 docs/source/api/v5/cdns_name_federations.rst       |  87 +++--
 docs/source/api/v5/cdns_name_federations_id.rst    | 160 +++++++--
 docs/source/api/v5/coordinates.rst                 |   8 +-
 docs/source/api/v5/deliveryservices.rst            |   4 +-
 docs/source/api/v5/deliveryservices_id.rst         |   2 +-
 docs/source/api/v5/deliveryservices_id_safe.rst    |   2 +-
 docs/source/api/v5/servers.rst                     |   4 +-
 docs/source/api/v5/servers_id.rst                  |   4 +-
 docs/source/api/v5/servers_id_deliveryservices.rst |   2 +-
 docs/source/api/v5/statuses_id.rst                 |   2 +-
 infrastructure/cdn-in-a-box/enroller/enroller.go   |  40 +--
 lib/go-tc/federation.go                            |  34 ++
 lib/go-tc/totest/federations.go                    |  12 +-
 lib/go-tc/totest/traffic_control.go                |   2 +-
 traffic_ops/testing/api/v5/cdn_federations_test.go |  54 ++-
 traffic_ops/testing/api/v5/federations_test.go     |   7 +-
 traffic_ops/testing/api/v5/tc-fixtures.json        |  12 +-
 traffic_ops/testing/api/v5/traffic_control_test.go |   2 +-
 traffic_ops/traffic_ops_golang/api/api.go          |   7 +-
 traffic_ops/traffic_ops_golang/api/api_test.go     |   2 +-
 .../cdnfederation/cdnfederations.go                | 384 ++++++++++++++++++---
 .../cdnfederation/cdnfederations_test.go           | 312 +++++++++++++++++
 traffic_ops/traffic_ops_golang/routing/routes.go   |   9 +-
 traffic_ops/traffic_ops_golang/server/servers.go   |   7 +-
 traffic_ops/traffic_ops_golang/util/ims/ims.go     |   7 +-
 traffic_ops/v5-client/cdnfederations.go            |  16 +-
 35 files changed, 985 insertions(+), 239 deletions(-)

diff --git a/.dependency_license b/.dependency_license
index f39518e10a..0adb340f0c 100644
--- a/.dependency_license
+++ b/.dependency_license
@@ -55,6 +55,7 @@ traffic_router/core/src/test/resources/api/.*/steering*, Apache-2.0
 traffic_router/core/src/test/resources/api/.*/federations/all, Apache-2.0
 BUILD_NUMBER$, Apache-2.0
 \.jks, Apache-2.0 # Java Key Store
+traffic_ops/traffic_ops_golang/.*\.sql, Apache-2.0 # embedded sql queries - can't have comment blocks due to github.com/jmoiron/sqlx library limitations
 
 # Images, created for this project or used under an Apache license.
 ^misc/logos/ATC-PNG\.png, Apache-2.0
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3e3bacca95..e86a2b1ba1 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/).
 ## [unreleased]
 - [#7665](https://github.com/apache/trafficcontrol/pull/7665) *Automation* Changes to Ansible role dataset_loader to add ATS 9 support
 - [#7802](https://github.com/apache/trafficcontrol/pull/7802) *Traffic Control Health Client* Fixed ReadMe.md typos and duplicates.
+- [#7806](https://github.com/apache/trafficcontrol/pull/7806) *Traffic Ops* `cdns/{{name}}/federations` and `cdns/{{name}}/federations/{{ID}}` use RFC3339 timestamps in APIv5
 ### Added
 - [#7672](https://github.com/apache/trafficcontrol/pull/7672) *Traffic Control Health Client* Added peer monitor flag while using `strategies.yaml`.
 - [#7609](https://github.com/apache/trafficcontrol/pull/7609) *Traffic Portal* Added Scope Query Param to SSO login.
diff --git a/docs/source/api/index.rst b/docs/source/api/index.rst
index 136db1986e..808847a294 100644
--- a/docs/source/api/index.rst
+++ b/docs/source/api/index.rst
@@ -89,7 +89,7 @@ The following, reserved properties of ``summary`` are guaranteed to always have
 
 Traffic Ops's Custom Date/Time Format
 -------------------------------------
-Traffic Ops will often return responses from its API that include dates. As of the time of this writing, the vast majority of those dates are written in a non-:rfc:3339 format (this is tracked by :issue:`5911`). This is most commonly the case in the ``last_updated`` properties of objects returned as JSON-encoded documents. The format used is :samp:`{YYYY}-{MM}-{DD} {hh}:{mm}:{ss}±{ZZ}`, where ``YYYY`` is the 4-digit year, ``MM`` is the two-digit (zero padded) month, ``DD`` is the two-dig [...]
+Traffic Ops will often return responses from its API that include dates. As of the time of this writing, the vast majority of those dates are written in a non-:RFC:`3339` format (this is tracked by :issue:`5911`). This is most commonly the case in the ``last_updated`` properties of objects returned as JSON-encoded documents. The format used is :samp:`{YYYY}-{MM}-{DD} {hh}:{mm}:{ss}±{ZZ}`, where ``YYYY`` is the 4-digit year, ``MM`` is the two-digit (zero padded) month, ``DD`` is the two-d [...]
 
 .. note:: In practice, all Traffic Ops API responses use the UTC timezone (offset ``+00``), but do note that this custom format is not capable of representing all timezones.
 
diff --git a/docs/source/api/v3/cdns_name_federations.rst b/docs/source/api/v3/cdns_name_federations.rst
index 2167fcb6a0..29cf413b1a 100644
--- a/docs/source/api/v3/cdns_name_federations.rst
+++ b/docs/source/api/v3/cdns_name_federations.rst
@@ -42,7 +42,9 @@ Request Structure
 	+-----------+---------------------------------------------------------------------------------------------------------------+
 	| Name      | Description                                                                                                   |
 	+===========+===============================================================================================================+
-	| id        | Return only the federation that has this id                                                                   |
+	| id        | Return only the :term:`Federation` that has this ID                                                           |
+	+-----------+---------------------------------------------------------------------------------------------------------------+
+	| cname     | Return only those :term:`Federations` that have this CNAME                                                    |
 	+-----------+---------------------------------------------------------------------------------------------------------------+
 	| orderby   | Choose the ordering of the results - must be the name of one of the fields of the objects in the ``response`` |
 	|           | array                                                                                                         |
@@ -75,10 +77,7 @@ Response Structure
 	:id:    The integral, unique identifier for the :term:`Delivery Service`
 	:xmlId: The :term:`Delivery Service`'s uniquely identifying 'xml_id'
 
-:description: An optionally-present field containing a description of the field
-
-	.. note:: This key will only be present if the description was provided when the federation was created. Refer to the ``POST`` method of this endpoint to see how federations can be created.
-
+:description: A human-readable description of the :term:`Federation`. This can be ``null`` as well as an empty string.
 :lastUpdated: The date and time at which this federation was last modified, in :ref:`non-rfc-datetime`
 :ttl:         Time to Live (TTL) for the ``cname``, in hours
 
@@ -158,10 +157,7 @@ Response Structure
 ------------------
 :id:          The integral, unique identifier of the :term:`Federation`
 :cname:       The Canonical Name (CNAME) used by the federation
-:description: An optionally-present field containing a description of the field
-
-	.. note:: This key will only be present if the description was provided when the federation was created
-
+:description: The description of the :term:`Federation`
 :lastUpdated: The date and time at which this federation was last modified, in :ref:`non-rfc-datetime`
 :ttl:         Time to Live (TTL) for the ``cname``, in hours
 
diff --git a/docs/source/api/v4/cdns_name_federations.rst b/docs/source/api/v4/cdns_name_federations.rst
index 00ac1e7f8b..5148a69337 100644
--- a/docs/source/api/v4/cdns_name_federations.rst
+++ b/docs/source/api/v4/cdns_name_federations.rst
@@ -43,7 +43,9 @@ Request Structure
 	+-----------+---------------------------------------------------------------------------------------------------------------+
 	| Name      | Description                                                                                                   |
 	+===========+===============================================================================================================+
-	| id        | Return only the federation that has this id                                                                   |
+	| id        | Return only the :term:`Federation` that has this ID                                                           |
+	+-----------+---------------------------------------------------------------------------------------------------------------+
+	| cname     | Return only those :term:`Federations` that have this CNAME                                                    |
 	+-----------+---------------------------------------------------------------------------------------------------------------+
 	| orderby   | Choose the ordering of the results - must be the name of one of the fields of the objects in the ``response`` |
 	|           | array                                                                                                         |
@@ -76,10 +78,7 @@ Response Structure
 	:id:    The integral, unique identifier for the :term:`Delivery Service`
 	:xmlId: The :term:`Delivery Service`'s uniquely identifying 'xml_id'
 
-:description: An optionally-present field containing a description of the field
-
-	.. note:: This key will only be present if the description was provided when the federation was created. Refer to the ``POST`` method of this endpoint to see how federations can be created.
-
+:description: A human-readable description of the :term:`Federation`. This can be ``null`` as well as an empty string.
 :lastUpdated: The date and time at which this federation was last modified, in :ref:`non-rfc-datetime`
 :ttl:         Time to Live (TTL) for the ``cname``, in hours
 
@@ -160,10 +159,7 @@ Response Structure
 ------------------
 :id:          The intergral, unique identifier of the :term:`Federation`
 :cname:       The Canonical Name (CNAME) used by the federation
-:description: An optionally-present field containing a description of the field
-
-	.. note:: This key will only be present if the description was provided when the federation was created
-
+:description: The description of the :term:`Federation`
 :lastUpdated: The date and time at which this federation was last modified, in :ref:`non-rfc-datetime`
 :ttl:         Time to Live (TTL) for the ``cname``, in hours
 
diff --git a/docs/source/api/v4/deliveryservices.rst b/docs/source/api/v4/deliveryservices.rst
index 9f98b62155..df87c5d9a5 100644
--- a/docs/source/api/v4/deliveryservices.rst
+++ b/docs/source/api/v4/deliveryservices.rst
@@ -116,7 +116,7 @@ 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 :rfc:3339 format
+: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`.
@@ -473,7 +473,7 @@ 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 :rfc:3339 format
+: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`.
diff --git a/docs/source/api/v4/deliveryservices_id.rst b/docs/source/api/v4/deliveryservices_id.rst
index 0865427028..a677c7da27 100644
--- a/docs/source/api/v4/deliveryservices_id.rst
+++ b/docs/source/api/v4/deliveryservices_id.rst
@@ -223,7 +223,7 @@ 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 :rfc:3339 format
+: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`.
diff --git a/docs/source/api/v4/deliveryservices_id_safe.rst b/docs/source/api/v4/deliveryservices_id_safe.rst
index 8bafe33110..bbbc1eea6e 100644
--- a/docs/source/api/v4/deliveryservices_id_safe.rst
+++ b/docs/source/api/v4/deliveryservices_id_safe.rst
@@ -95,7 +95,7 @@ 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 :rfc:3339 format
+: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`.
diff --git a/docs/source/api/v4/servers_id_deliveryservices.rst b/docs/source/api/v4/servers_id_deliveryservices.rst
index 84515033c4..cc4aac7673 100644
--- a/docs/source/api/v4/servers_id_deliveryservices.rst
+++ b/docs/source/api/v4/servers_id_deliveryservices.rst
@@ -103,7 +103,7 @@ 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 :rfc:3339 format
+: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`.
diff --git a/docs/source/api/v5/cdns_name_federations.rst b/docs/source/api/v5/cdns_name_federations.rst
index e9188086fa..d5dce307f0 100644
--- a/docs/source/api/v5/cdns_name_federations.rst
+++ b/docs/source/api/v5/cdns_name_federations.rst
@@ -32,18 +32,24 @@ Request Structure
 -----------------
 .. table:: Request Path Parameters
 
-	+-----------+---------------------------------------------------------------------------------------------------------------+
-	| Name      | Description                                                                                                   |
-	+===========+===============================================================================================================+
-	| name      | The name of the CDN for which federations will be listed                                                      |
-	+-----------+---------------------------------------------------------------------------------------------------------------+
+	+-----------+------------------------------------------------------------------+
+	| Name      | Description                                                      |
+	+===========+==================================================================+
+	| name      | The name of the CDN for which :term:`Federations` will be listed |
+	+-----------+------------------------------------------------------------------+
 
 .. table:: Request Query Parameters
 
 	+-----------+---------------------------------------------------------------------------------------------------------------+
 	| Name      | Description                                                                                                   |
 	+===========+===============================================================================================================+
-	| id        | Return only the federation that has this id                                                                   |
+	| id        | Return only the :term:`Federation` that has this ID                                                           |
+	+-----------+---------------------------------------------------------------------------------------------------------------+
+	| cname     | Return only those :term:`Federations` that have this CNAME                                                    |
+	+-----------+---------------------------------------------------------------------------------------------------------------+
+	| dsID      | Return only those :term:`Federations` assigned to a :term:`Delivery Service` that has this ID                 |
+	+-----------+---------------------------------------------------------------------------------------------------------------+
+	| xmlID     | Return only those :term:`Federations` assigned to a :term:`Delivery Service` that has this :ref:`ds-xmlid`    |
 	+-----------+---------------------------------------------------------------------------------------------------------------+
 	| orderby   | Choose the ordering of the results - must be the name of one of the fields of the objects in the ``response`` |
 	|           | array                                                                                                         |
@@ -70,18 +76,22 @@ Request Structure
 
 Response Structure
 ------------------
-:cname:           The Canonical Name (CNAME) used by the federation
-:deliveryService: An object with keys that provide identifying information for the :term:`Delivery Service` using this federation
+:cname:           The :abbr:`CNAME (Canonical Name)` used by the :term:`Federation`
+:deliveryService: An object with keys that provide identifying information for the :term:`Delivery Service` using this :term:`Federation`
 
 	:id:    The integral, unique identifier for the :term:`Delivery Service`
-	:xmlId: The :term:`Delivery Service`'s uniquely identifying 'xml_id'
+	:xmlID: The :term:`Delivery Service`'s uniquely identifying :ref:`ds-xmlid`
+
+		.. versionchanged:: 5.0
+			Prior to version 5, this field was known by the name ``xmlId`` - improperly formatted camelCase.
 
-:description: An optionally-present field containing a description of the field
+:description: A human-readable description of the :term:`Federation`. This can be ``null`` as well as an empty string.
+:lastUpdated: The date and time at which this :term:`Federation` was last modified, in :RFC:`3339` format
 
-	.. note:: This key will only be present if the description was provided when the federation was created. Refer to the ``POST`` method of this endpoint to see how federations can be created.
+	.. versionchanged:: 5.0
+		In earlier versions of the API, this field was given in :ref:`non-rfc-datetime`.
 
-:lastUpdated: The date and time at which this federation was last modified, in :ref:`non-rfc-datetime`
-:ttl:         Time to Live (TTL) for the ``cname``, in hours
+:ttl: :abbr:`TTL (Time to Live)` for the ``cname``, in hours
 
 .. code-block:: http
 	:caption: Response Example
@@ -102,19 +112,23 @@ Response Structure
 		{
 			"id": 1,
 			"cname": "test.quest.",
-			"ttl": 48,
+			"ttl": 68,
 			"description": "A test federation",
-			"lastUpdated": "2018-12-05 00:05:16+00",
+			"lastUpdated": "2018-12-05T00:05:16Z",
 			"deliveryService": {
 				"id": 1,
-				"xmlId": "demo1"
+				"xmlID": "demo1"
 			}
 		}
 	]}
 
 ``POST``
 ========
-Creates a new federation.
+Creates a new :term:`Federation`.
+
+.. caution:: Despite the URL of this endpoint, this does `**not**` create a :term:`Federation` within any particular CDN. A :term:`Federation` is associated with a CDN purely because any :term:`Delivery Service` to which it is assigned is scoped to a CDN. Therefore, upon creation a :term:`Federation` is not associated with any CDN in particular.
+
+.. warning:: There is no restriction on using the special "ALL" CDN for :term:`Federations` - but this is highly discouraged, because many things treat that CDN specially and may not work properly if it is used as though it were a normal CDN.
 
 :Auth. Required: Yes
 :Roles Required: "admin"
@@ -125,18 +139,21 @@ Request Structure
 -----------------
 .. table:: Request Path Parameters
 
-	+------+----------------------------------------------------------------+
-	| Name | Description                                                    |
-	+======+================================================================+
-	| name | The name of the CDN for which a new federation will be created |
-	+------+----------------------------------------------------------------+
+	+------+------------------------------------------------------------------------+
+	| Name | Description                                                            |
+	+======+========================================================================+
+	| name | The name of the CDN for which a new :term:`Federation` will be created |
+	+------+------------------------------------------------------------------------+
 
-:cname: The Canonical Name (CNAME) used by the federation
+:cname: The :abbr:`CNAME (Canonical Name)` used by the :term:`Federation`
 
-	.. note:: The CNAME must end with a "``.``"
+	.. tip:: The CNAME must end with a "``.``"
 
 :description: An optional description of the federation
-:ttl:         Time to Live (TTL) for the name record used for ``cname``
+:ttl:         Time to Live (TTL) for the name record used for ``cname`` - minimum of 60
+
+	.. versionchanged:: 5.0
+		In earlier API versions, there is no enforced minimum (although Traffic Portal would never allow a value under 60).
 
 .. code-block:: http
 	:caption: Request Example
@@ -151,7 +168,7 @@ Request Structure
 
 	{
 		"cname": "test.quest.",
-		"ttl": 48,
+		"ttl": 68,
 		"description": "A test federation"
 	}
 
@@ -159,14 +176,14 @@ Request Structure
 Response Structure
 ------------------
 :id:          The integral, unique identifier of the :term:`Federation`
-:cname:       The Canonical Name (CNAME) used by the federation
-:description: An optionally-present field containing a description of the field
-
-	.. note:: This key will only be present if the description was provided when the federation was created
+:cname:       The :abbr:`CNAME (Canonical Name)` used by the :term:`Federation`
+:description: The description of the :term:`Federation`
+:lastUpdated: The date and time at which this federation was last modified, in :RFC:`3339` format
 
-:lastUpdated: The date and time at which this federation was last modified, in :ref:`non-rfc-datetime`
-:ttl:         Time to Live (TTL) for the ``cname``, in hours
+	.. versionchanged:: 5.0
+		In earlier versions of the API, this field was given in :ref:`non-rfc-datetime`.
 
+:ttl: :abbr:`TTL (Time to Live)` for the ``cname``, in hours
 
 .. code-block:: http
 	:caption: Response Example
@@ -185,14 +202,14 @@ Response Structure
 
 	{ "alerts": [
 		{
-			"text": "cdnfederation was created.",
+			"text": "Federation was created",
 			"level": "success"
 		}
 	],
 	"response": {
 		"id": 1,
 		"cname": "test.quest.",
-		"ttl": 48,
+		"ttl": 68,
 		"description": "A test federation",
-		"lastUpdated": "2018-12-05 00:05:16+00"
+		"lastUpdated": "2018-12-05T00:05:16Z"
 	}}
diff --git a/docs/source/api/v5/cdns_name_federations_id.rst b/docs/source/api/v5/cdns_name_federations_id.rst
index 6fee0b5b72..89545e92d3 100644
--- a/docs/source/api/v5/cdns_name_federations_id.rst
+++ b/docs/source/api/v5/cdns_name_federations_id.rst
@@ -19,9 +19,87 @@
 ``cdns/{{name}}/federations/{{ID}}``
 ************************************
 
+``GET``
+=======
+Retrieves a list of federations in use by a specific CDN.
+
+.. versionadded:: 5.0
+
+:Auth. Required: Yes
+:Roles Required: None
+:Permissions Required: CDN:READ, FEDERATION:READ, DELIVERY-SERVICE:READ
+:Response Type: Object
+
+Request Structure
+-----------------
+.. table:: Request Path Parameters
+
+	+------+---------------------------------------------------------------------------------------------+
+	| Name | Description                                                                                 |
+	+======+=============================================================================================+
+	| name | The name of the CDN for which the :term:`Federation` identified by ``ID`` will be inspected |
+	+------+---------------------------------------------------------------------------------------------+
+	|  ID  | An integral, unique identifier for the :term:`Federation` to be inspected                  |
+	+------+---------------------------------------------------------------------------------------------+
+
+.. code-block:: http
+	:caption: Request Example
+
+	GET /api/5.0/cdns/CDN-in-a-Box/federations/1 HTTP/1.1
+	Host: trafficops.infra.ciab.test
+	User-Agent: curl/7.62.0
+	Accept: */*
+	Cookie: mojolicious=...
+
+Response Structure
+------------------
+:cname:           The :abbr:`CNAME (Canonical Name)` used by the :term:`Federation`
+:deliveryService: An object with keys that provide identifying information for the :term:`Delivery Service` using this :term:`Federation`
+
+	:id:    The integral, unique identifier for the :term:`Delivery Service`
+	:xmlID: The :term:`Delivery Service`'s uniquely identifying :ref:`ds-xmlid`
+
+		.. versionchanged:: 5.0
+			Prior to version 5, this field was known by the name ``xmlId`` - improperly formatted camelCase.
+
+:description: A human-readable description of the :term:`Federation`. This can be ``null`` as well as an empty string.
+:lastUpdated: The date and time at which this :term:`Federation` was last modified, in :RFC:`3339` format
+
+	.. versionchanged:: 5.0
+		In earlier versions of the API, this field was given in :ref:`non-rfc-datetime`.
+
+:ttl: :abbr:`TTL (Time to Live)` for the ``cname``, in hours
+
+.. code-block:: http
+	:caption: Response Example
+
+	HTTP/1.1 200 OK
+	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-type: application/json
+	set-cookie: mojolicious=...; Path=/; HttpOnly
+	whole-content-sha512: SJA7G+7G5KcOfCtnE3Dq5DCobWtGRUKSppiDkfLZoG5+paq4E1aZGqUb6vGVsd+TpPg75MLlhyqfdfCHnhLX/g==
+	x-server-name: traffic_ops_golang/
+	content-length: 170
+	date: Wed, 05 Dec 2018 00:35:40 GMT
+
+	{ "response": {
+		"id": 1,
+		"cname": "test.quest.",
+		"ttl": 68,
+		"description": "A test federation",
+		"lastUpdated": "2018-12-05T00:05:16Z",
+		"deliveryService": {
+			"id": 1,
+			"xmlID": "demo1"
+		}
+	}}
+
 ``PUT``
 =======
-Updates a federation.
+Updates a :term:`Federation`.
 
 :Auth. Required: Yes
 :Roles Required: "admin"
@@ -32,20 +110,25 @@ Request Structure
 -----------------
 .. table:: Request Path Parameters
 
-	+------+-------------------------------------------------------------------------------------+
-	| Name | Description                                                                         |
-	+======+=====================================================================================+
-	| name | The name of the CDN for which the federation identified by ``ID`` will be inspected |
-	+------+-------------------------------------------------------------------------------------+
-	|  ID  | An integral, unique identifier for the federation to be inspected                   |
-	+------+-------------------------------------------------------------------------------------+
+	+------+-------------------------------------------------------------------------------------------+
+	| Name | Description                                                                               |
+	+======+===========================================================================================+
+	| name | The name of the CDN for which the :term:`Federation` identified by ``ID`` will be updated |
+	+------+-------------------------------------------------------------------------------------------+
+	|  ID  | An integral, unique identifier for the :term:`Federation` to be updated                   |
+	+------+-------------------------------------------------------------------------------------------+
 
-:cname: The Canonical Name (CNAME) used by the federation
+.. caution:: The name of the CDN doesn't actually matter. It doesn't even need to be the name of any existing CDN.
+
+:cname: The :abbr:`CNAME (Canonical Name)` used by the :term:`Federation`
 
 	.. note:: The CNAME must end with a "``.``"
 
 :description: An optional description of the federation
-:ttl:         Time to Live (TTL) for the name record used for ``cname``
+:ttl:         Time to Live (TTL) for the name record used for ``cname`` - minimum of 60
+
+	.. versionchanged:: 5.0
+		In earlier API versions, there is no enforced minimum (although Traffic Portal would never allow a value under 60).
 
 .. code-block:: http
 	:caption: Request Example
@@ -60,20 +143,20 @@ Request Structure
 
 	{
 		"cname": "foo.bar.",
-		"ttl": 48
+		"ttl": 68
 	}
 
 
 Response Structure
 ------------------
-:cname:       The Canonical Name (CNAME) used by the federation
-:description: An optionally-present field containing a description of the field
+:cname:       The :abbr:`CNAME (Canonical Name)` used by the :term:`Federation`
+:description: A human-readable description of the :term:`Federation`. This can be ``null`` as well as an empty string.
+:lastUpdated: The date and time at which this :term:`Federation` was last modified, in :RFC:`3339` format
 
-	.. note:: This key will only be present if the description was provided when the federation was created
-
-:lastUpdated: The date and time at which this federation was last modified, in :ref:`non-rfc-datetime`
-:ttl:         Time to Live (TTL) for the ``cname``, in hours
+	.. versionchanged:: 5.0
+		In earlier versions of the API, this field was given in :ref:`non-rfc-datetime`.
 
+:ttl: :abbr:`TTL (Time to Live)` for the ``cname``, in hours
 
 .. code-block:: http
 	:caption: Response Example
@@ -92,16 +175,16 @@ Response Structure
 
 	{ "alerts": [
 		{
-			"text": "cdnfederation was updated.",
+			"text": "Federation was updated",
 			"level": "success"
 		}
 	],
 	"response": {
 		"id": 1,
 		"cname": "foo.bar.",
-		"ttl": 48,
+		"ttl": 68,
 		"description": null,
-		"lastUpdated": "2018-12-05 01:03:40+00"
+		"lastUpdated": "2018-12-05T01:03:40Z"
 	}}
 
 
@@ -112,19 +195,24 @@ Deletes a specific federation.
 :Auth. Required: Yes
 :Roles Required: "admin"
 :Permissions Required: FEDERATION:DELETE, FEDERATION:READ, CDN:READ
-:Response Type:  ``undefined``
+:Response Type:  Object
+
+.. versionchanged:: 5.0
+	In earlier API versions, no ``response`` property is present in these responses.
 
 Request Structure
 -----------------
 .. table:: Request Path Parameters
 
-	+------+-------------------------------------------------------------------------------------+
-	| Name | Description                                                                         |
-	+======+=====================================================================================+
-	| name | The name of the CDN for which the federation identified by ``ID`` will be inspected |
-	+------+-------------------------------------------------------------------------------------+
-	|  ID  | An integral, unique identifier for the federation to be inspected                   |
-	+------+-------------------------------------------------------------------------------------+
+	+------+-------------------------------------------------------------------------------------------+
+	| Name | Description                                                                               |
+	+======+===========================================================================================+
+	| name | The name of the CDN for which the :term:`Federation` identified by ``ID`` will be deleted |
+	+------+-------------------------------------------------------------------------------------------+
+	|  ID  | An integral, unique identifier for the :term:`Federation` to be deleted                   |
+	+------+-------------------------------------------------------------------------------------------+
+
+.. caution:: The name of the CDN doesn't actually matter. It doesn't even need to be the name of any existing CDN.
 
 .. code-block:: http
 	:caption: Request Example
@@ -137,6 +225,11 @@ Request Structure
 
 Response Structure
 ------------------
+:cname:       The :abbr:`CNAME (Canonical Name)` used by the :term:`Federation`
+:description: A human-readable description of the :term:`Federation`. This can be ``null`` as well as an empty string.
+:lastUpdated: The date and time at which this :term:`Federation` was last modified, in :RFC:`3339` format
+:ttl:         :abbr:`TTL (Time to Live)` for the ``cname``, in hours
+
 .. code-block:: http
 	:caption: Response Example
 
@@ -154,7 +247,14 @@ Response Structure
 
 	{ "alerts": [
 		{
-			"text": "cdnfederation was deleted.",
+			"text": "Federation was deleted",
 			"level": "success"
 		}
-	]}
+	],
+	"response": {
+		"id": 1,
+		"cname": "foo.bar.",
+		"ttl": 68,
+		"description": null,
+		"lastUpdated": "2018-12-05T01:03:40Z"
+	}}
diff --git a/docs/source/api/v5/coordinates.rst b/docs/source/api/v5/coordinates.rst
index dc966fbeab..8f7c885bce 100644
--- a/docs/source/api/v5/coordinates.rst
+++ b/docs/source/api/v5/coordinates.rst
@@ -56,7 +56,7 @@ Request Structure
 Response Structure
 ------------------
 :id:          Integral, unique, identifier for this coordinate pair
-:lastUpdated: The time and date at which this entry was last updated, in :rfc:3339 format
+:lastUpdated: The time and date at which this entry was last updated, in :RFC:`3339` format
 :latitude:    Latitude of the coordinate
 :longitude:   Longitude of the coordinate
 :name:        The name of the coordinate - typically this just reflects the name of the Cache Group for which the coordinate was created
@@ -159,7 +159,7 @@ Request Structure
 Response Structure
 ------------------
 :id:          Integral, unique, identifier for the newly created coordinate pair
-:lastUpdated: The time and date at which this entry was last updated, in :rfc:3339 format
+:lastUpdated: The time and date at which this entry was last updated, in :RFC:`3339` format
 :latitude:    Latitude of the newly created coordinate
 :longitude:   Longitude of the newly created coordinate
 :name:        The name of the coordinate
@@ -233,7 +233,7 @@ Request Structure
 Response Structure
 ------------------
 :id:          Integral, unique, identifier for the coordinate pair
-:lastUpdated: The time and date at which this entry was last updated, in :rfc:3339 format
+:lastUpdated: The time and date at which this entry was last updated, in :RFC:`3339` format
 :latitude:    Latitude of the coordinate
 :longitude:   Longitude of the coordinate
 :name:        The name of the coordinate
@@ -279,7 +279,7 @@ Deletes a coordinate
 Request Structure
 -----------------
 :id:          Integral, unique, identifier for the coordinate pair
-:lastUpdated: The time and date at which this entry was last updated, in :rfc:3339 format
+:lastUpdated: The time and date at which this entry was last updated, in :RFC:`3339` format
 :latitude:    Latitude of the coordinate
 :longitude:   Longitude of the coordinate
 :name:        The name of the coordinate
diff --git a/docs/source/api/v5/deliveryservices.rst b/docs/source/api/v5/deliveryservices.rst
index bc75722c32..21a0c01799 100644
--- a/docs/source/api/v5/deliveryservices.rst
+++ b/docs/source/api/v5/deliveryservices.rst
@@ -116,7 +116,7 @@ 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 :rfc:3339 format
+:lastUpdated:               The date and time at which this :term:`Delivery Service` was last updated, in :RFC:`3339` format
 :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`
@@ -459,7 +459,7 @@ 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 :rfc:3339 format
+:lastUpdated:               The date and time at which this :term:`Delivery Service` was last updated, in :RFC:`3339` format
 :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`
diff --git a/docs/source/api/v5/deliveryservices_id.rst b/docs/source/api/v5/deliveryservices_id.rst
index 52e41c83ae..a832b67bd4 100644
--- a/docs/source/api/v5/deliveryservices_id.rst
+++ b/docs/source/api/v5/deliveryservices_id.rst
@@ -218,7 +218,7 @@ 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 :rfc:3339 format
+:lastUpdated:               The date and time at which this :term:`Delivery Service` was last updated, in :RFC:`3339` format
 :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`
diff --git a/docs/source/api/v5/deliveryservices_id_safe.rst b/docs/source/api/v5/deliveryservices_id_safe.rst
index d5b4cdfe7b..3405c693d6 100644
--- a/docs/source/api/v5/deliveryservices_id_safe.rst
+++ b/docs/source/api/v5/deliveryservices_id_safe.rst
@@ -95,7 +95,7 @@ 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 :rfc:3339 format
+:lastUpdated:               The date and time at which this :term:`Delivery Service` was last updated, in :RFC:`3339` format
 :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`
diff --git a/docs/source/api/v5/servers.rst b/docs/source/api/v5/servers.rst
index 1166d01146..cbf5665c06 100644
--- a/docs/source/api/v5/servers.rst
+++ b/docs/source/api/v5/servers.rst
@@ -142,7 +142,7 @@ Response Structure
 	:routerPortName:    The human-readable name of the router responsible for reaching this server's interface.
 	:routerPortName:    The human-readable name of the port used by the router responsible for reaching this server's interface.
 
-:lastUpdated: The date and time at which this server description was last modified, in :RFC:3339 format
+:lastUpdated: The date and time at which this server description was last modified, in :RFC:`3339` format
 
 	.. versionchanged:: 5.0
 		In earlier versions of the API, this field was given in :ref:`non-rfc-datetime`.
@@ -480,7 +480,7 @@ Response Structure
 	:routerPortName:    The human-readable name of the router responsible for reaching this server's interface.
 	:routerPortName:    The human-readable name of the port used by the router responsible for reaching this server's interface.
 
-:lastUpdated: The date and time at which this server description was last modified, in :RFC:3339 format
+:lastUpdated: The date and time at which this server description was last modified, in :RFC:`3339` format
 
 	.. versionchanged:: 5.0
 		In earlier versions of the API, this field was given in :ref:`non-rfc-datetime`.
diff --git a/docs/source/api/v5/servers_id.rst b/docs/source/api/v5/servers_id.rst
index f6275e6484..2573a4260a 100644
--- a/docs/source/api/v5/servers_id.rst
+++ b/docs/source/api/v5/servers_id.rst
@@ -236,7 +236,7 @@ Response Structure
 	:routerPortName:    The human-readable name of the router responsible for reaching this server's interface.
 	:routerPortName:    The human-readable name of the port used by the router responsible for reaching this server's interface.
 
-:lastUpdated: The date and time at which this server description was last modified, in :RFC:3339 format
+:lastUpdated: The date and time at which this server description was last modified, in :RFC:`3339` format
 
 	.. versionchanged:: 5.0
 		In earlier versions of the API, this field was given in :ref:`non-rfc-datetime`.
@@ -466,7 +466,7 @@ Response Structure
 	:routerPortName:    The human-readable name of the router responsible for reaching this server's interface.
 	:routerPortName:    The human-readable name of the port used by the router responsible for reaching this server's interface.
 
-:lastUpdated: The date and time at which this server description was last modified, in :RFC:3339 format
+:lastUpdated: The date and time at which this server description was last modified, in :RFC:`3339` format
 
 	.. versionchanged:: 5.0
 		In earlier versions of the API, this field was given in :ref:`non-rfc-datetime`.
diff --git a/docs/source/api/v5/servers_id_deliveryservices.rst b/docs/source/api/v5/servers_id_deliveryservices.rst
index d62b6516d6..a43dfd50dd 100644
--- a/docs/source/api/v5/servers_id_deliveryservices.rst
+++ b/docs/source/api/v5/servers_id_deliveryservices.rst
@@ -102,7 +102,7 @@ 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 :rfc:3339 format
+:lastUpdated:               The date and time at which this :term:`Delivery Service` was last updated, in :RFC:`3339` format
 :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`
diff --git a/docs/source/api/v5/statuses_id.rst b/docs/source/api/v5/statuses_id.rst
index dcca4425b3..4742251aff 100644
--- a/docs/source/api/v5/statuses_id.rst
+++ b/docs/source/api/v5/statuses_id.rst
@@ -48,7 +48,7 @@ Response Structure
 ------------------
 :description: A short description of the status
 :id:          The integral, unique identifier of this status
-:lastUpdated: The date and time at which this status was last modified, in :rfc:3339 format
+:lastUpdated: The date and time at which this status was last modified, in :RFC:`3339` format
 :name:        The name of the status
 
 .. code-block:: http
diff --git a/infrastructure/cdn-in-a-box/enroller/enroller.go b/infrastructure/cdn-in-a-box/enroller/enroller.go
index 926a9db478..7af02ccd9d 100644
--- a/infrastructure/cdn-in-a-box/enroller/enroller.go
+++ b/infrastructure/cdn-in-a-box/enroller/enroller.go
@@ -812,7 +812,12 @@ func enrollFederation(toSession *session, r io.Reader) error {
 	}
 	opts := client.NewRequestOptions()
 	for _, mapping := range federation.Mappings {
-		var cdnFederation tc.CDNFederation
+		if mapping.CName == nil || mapping.TTL == nil {
+			err = fmt.Errorf("mapping found with null or undefined CName and/or TTL: %+v", mapping)
+			log.Errorln(err)
+			return err
+		}
+		var cdnFederation tc.CDNFederationV5
 		var cdnName string
 		{
 			xmlID := string(federation.DeliveryService)
@@ -831,9 +836,9 @@ func enrollFederation(toSession *session, r io.Reader) error {
 			}
 			deliveryService := deliveryServices.Response[0]
 			cdnName = *deliveryService.CDNName
-			cdnFederation = tc.CDNFederation{
-				CName: mapping.CName,
-				TTL:   mapping.TTL,
+			cdnFederation = tc.CDNFederationV5{
+				CName: *mapping.CName,
+				TTL:   *mapping.TTL,
 			}
 			resp, _, err := toSession.CreateCDNFederation(cdnFederation, cdnName, client.RequestOptions{})
 			if err != nil {
@@ -842,13 +847,8 @@ func enrollFederation(toSession *session, r io.Reader) error {
 				return err
 			}
 			cdnFederation = resp.Response
-			if cdnFederation.ID == nil {
-				err = fmt.Errorf("federation returned from creation through Traffic Ops with null or undefined ID")
-				log.Infoln(err)
-				return err
-			}
-			if alerts, _, err := toSession.CreateFederationDeliveryServices(*cdnFederation.ID, []int{*deliveryService.ID}, true, client.RequestOptions{}); err != nil {
-				err = fmt.Errorf("assigning Delivery Service %s to Federation with ID %d: %v - alerts: %+v", xmlID, *cdnFederation.ID, err, alerts.Alerts)
+			if alerts, _, err := toSession.CreateFederationDeliveryServices(cdnFederation.ID, []int{*deliveryService.ID}, true, client.RequestOptions{}); err != nil {
+				err = fmt.Errorf("assigning Delivery Service %s to Federation with ID %d: %v - alerts: %+v", xmlID, cdnFederation.ID, err, alerts.Alerts)
 				log.Infoln(err)
 				return err
 			}
@@ -865,10 +865,10 @@ func enrollFederation(toSession *session, r io.Reader) error {
 				log.Infoln(err)
 				return err
 			}
-			resp, _, err := toSession.CreateFederationUsers(*cdnFederation.ID, []int{*user.Response.ID}, true, client.RequestOptions{})
+			resp, _, err := toSession.CreateFederationUsers(cdnFederation.ID, []int{*user.Response.ID}, true, client.RequestOptions{})
 			if err != nil {
 				username := user.Response.Username
-				err = fmt.Errorf("assigning User '%s' to Federation with ID %d: %v - alerts: %+v", username, *cdnFederation.ID, err, resp.Alerts)
+				err = fmt.Errorf("assigning User '%s' to Federation with ID %d: %v - alerts: %+v", username, cdnFederation.ID, err, resp.Alerts)
 				log.Infoln(err)
 				return err
 			}
@@ -885,20 +885,20 @@ func enrollFederation(toSession *session, r io.Reader) error {
 				allResolverIDs = append(allResolverIDs, resolverIDs...)
 			}
 		}
-		if resp, _, err := toSession.AssignFederationFederationResolver(*cdnFederation.ID, allResolverIDs, true, client.RequestOptions{}); err != nil {
-			err = fmt.Errorf("assigning Federation Resolvers to Federation with ID %d: %v - alerts: %+v", *cdnFederation.ID, err, resp.Alerts)
+		if resp, _, err := toSession.AssignFederationFederationResolver(cdnFederation.ID, allResolverIDs, true, client.RequestOptions{}); err != nil {
+			err = fmt.Errorf("assigning Federation Resolvers to Federation with ID %d: %v - alerts: %+v", cdnFederation.ID, err, resp.Alerts)
 			log.Infoln(err)
 			return err
 		}
-		opts.QueryParameters.Set("id", strconv.Itoa(*cdnFederation.ID))
-		response, _, err := toSession.GetCDNFederationsByName(cdnName, opts)
+		opts.QueryParameters.Set("id", strconv.Itoa(cdnFederation.ID))
+		response, _, err := toSession.GetCDNFederations(cdnName, opts)
 		opts.QueryParameters.Del("id")
 		if err != nil {
-			err = fmt.Errorf("getting CDN Federation with ID %d: %v - alerts: %+v", *cdnFederation.ID, err, response.Alerts)
+			err = fmt.Errorf("getting CDN Federation with ID %d: %v - alerts: %+v", cdnFederation.ID, err, response.Alerts)
 			return err
 		}
 		if len(response.Response) < 1 {
-			err = fmt.Errorf("unable to GET a CDN Federation ID %d in CDN %s", *cdnFederation.ID, cdnName)
+			err = fmt.Errorf("unable to GET a CDN Federation ID %d in CDN %s", cdnFederation.ID, cdnName)
 			log.Infoln(err)
 			return err
 		}
@@ -908,7 +908,7 @@ func enrollFederation(toSession *session, r io.Reader) error {
 		enc.SetIndent("", "  ")
 		err = enc.Encode(&cdnFederation)
 		if err != nil {
-			err = fmt.Errorf("encoding CDNFederation %s with ID %d: %v", *cdnFederation.CName, *cdnFederation.ID, err)
+			err = fmt.Errorf("encoding CDNFederation %s with ID %d: %v", cdnFederation.CName, cdnFederation.ID, err)
 			log.Infoln(err)
 			return err
 		}
diff --git a/lib/go-tc/federation.go b/lib/go-tc/federation.go
index 8447ed658b..b565f3fb97 100644
--- a/lib/go-tc/federation.go
+++ b/lib/go-tc/federation.go
@@ -24,6 +24,7 @@ import (
 	"errors"
 	"fmt"
 	"net"
+	"time"
 
 	"github.com/apache/trafficcontrol/lib/go-util"
 
@@ -69,6 +70,39 @@ type CDNFederation struct {
 	*DeliveryServiceIDs `json:"deliveryService,omitempty"`
 }
 
+// CDNFederationDeliveryService holds information about an assigned Delivery
+// Service within a CDNFederationV5 structure.
+type CDNFederationDeliveryService = struct {
+	ID    int    `json:"id" db:"ds_id"`
+	XMLID string `json:"xmlID" db:"xml_id"`
+}
+
+// CDNFederationV5 represents a Federation of some CDN as it appears in version
+// 5 of the Traffic Ops API.
+type CDNFederationV5 struct {
+	ID          int       `json:"id" db:"id"`
+	CName       string    `json:"cname" db:"cname"`
+	TTL         int       `json:"ttl" db:"ttl"`
+	Description *string   `json:"description" db:"description"`
+	LastUpdated time.Time `json:"lastUpdated" db:"last_updated"`
+
+	DeliveryService *CDNFederationDeliveryService `json:"deliveryService,omitempty"`
+}
+
+// CDNFederationsV5Response represents a Traffic Ops APIv5 response to a request
+// for one or more of a CDN's Federations.
+type CDNFederationsV5Response struct {
+	Response []CDNFederationV5 `json:"response"`
+	Alerts
+}
+
+// CDNFederationV5Response represents a Traffic Ops APIv5 response to a request
+// for a single CDN's Federations.
+type CDNFederationV5Response struct {
+	Response CDNFederationV5 `json:"response"`
+	Alerts
+}
+
 // DeliveryServiceIDs are pairs of identifiers for Delivery Services.
 type DeliveryServiceIDs struct {
 	DsId  *int    `json:"id,omitempty" db:"ds_id"`
diff --git a/lib/go-tc/totest/federations.go b/lib/go-tc/totest/federations.go
index 99a1be0bce..9f90efab36 100644
--- a/lib/go-tc/totest/federations.go
+++ b/lib/go-tc/totest/federations.go
@@ -45,16 +45,14 @@ func GetFederationID(t *testing.T, cname string) func() int {
 	}
 }
 
-func setFederationID(t *testing.T, cdnFederation tc.CDNFederation) {
-	assert.RequireNotNil(t, cdnFederation.CName, "Federation CName was nil after posting.")
-	assert.RequireNotNil(t, cdnFederation.ID, "Federation ID was nil after posting.")
-	fedIDs[*cdnFederation.CName] = *cdnFederation.ID
+func setFederationID(t *testing.T, cdnFederation tc.CDNFederationV5) {
+	fedIDs[cdnFederation.CName] = cdnFederation.ID
 }
 
 func CreateTestCDNFederations(t *testing.T, cl *toclient.Session, dat TrafficControl) {
 	for _, federation := range dat.Federations {
 		opts := toclient.NewRequestOptions()
-		opts.QueryParameters.Set("xmlId", *federation.DeliveryServiceIDs.XmlId)
+		opts.QueryParameters.Set("xmlId", federation.DeliveryService.XMLID)
 		dsResp, _, err := cl.GetDeliveryServices(opts)
 		assert.RequireNoError(t, err, "Could not get Delivery Service by XML ID: %v", err)
 		assert.RequireEqual(t, 1, len(dsResp.Response), "Expected one Delivery Service, but got %d", len(dsResp.Response))
@@ -67,7 +65,7 @@ func CreateTestCDNFederations(t *testing.T, cl *toclient.Session, dat TrafficCon
 		setFederationID(t, resp.Response)
 		assert.RequireNotNil(t, resp.Response.ID, "Federation ID was nil after posting.")
 		assert.RequireNotNil(t, dsResp.Response[0].ID, "Delivery Service ID was nil.")
-		_, _, err = cl.CreateFederationDeliveryServices(*resp.Response.ID, []int{*dsResp.Response[0].ID}, false, toclient.NewRequestOptions())
+		_, _, err = cl.CreateFederationDeliveryServices(resp.Response.ID, []int{*dsResp.Response[0].ID}, false, toclient.NewRequestOptions())
 		assert.NoError(t, err, "Could not create Federation Delivery Service: %v", err)
 	}
 }
@@ -79,7 +77,7 @@ func DeleteTestCDNFederations(t *testing.T, cl *toclient.Session) {
 		assert.NoError(t, err, "Cannot delete federation #%d: %v - alerts: %+v", id, err, resp.Alerts)
 
 		opts.QueryParameters.Set("id", strconv.Itoa(id))
-		data, _, err := cl.GetCDNFederationsByName(FederationCDNName, opts)
+		data, _, err := cl.GetCDNFederations(FederationCDNName, opts)
 		assert.Equal(t, 0, len(data.Response), "expected federation to be deleted")
 	}
 	fedIDs = make(map[string]int) // reset the global variable for the next test
diff --git a/lib/go-tc/totest/traffic_control.go b/lib/go-tc/totest/traffic_control.go
index a6260a48d9..bd384718ff 100644
--- a/lib/go-tc/totest/traffic_control.go
+++ b/lib/go-tc/totest/traffic_control.go
@@ -37,7 +37,7 @@ type TrafficControl struct {
 	DeliveryServiceServerAssignments                  []tc.DeliveryServiceServerV5            `json:"deliveryServiceServerAssignments"`
 	TopologyBasedDeliveryServicesRequiredCapabilities []tc.DeliveryServicesRequiredCapability `json:"topologyBasedDeliveryServicesRequiredCapabilities"`
 	Divisions                                         []tc.DivisionV5                         `json:"divisions"`
-	Federations                                       []tc.CDNFederation                      `json:"federations"`
+	Federations                                       []tc.CDNFederationV5                    `json:"federations"`
 	FederationResolvers                               []tc.FederationResolverV5               `json:"federation_resolvers"`
 	Jobs                                              []tc.InvalidationJobCreateV4            `json:"jobs"`
 	Origins                                           []tc.OriginV5                           `json:"origins"`
diff --git a/traffic_ops/testing/api/v5/cdn_federations_test.go b/traffic_ops/testing/api/v5/cdn_federations_test.go
index 2878b3dfe8..d0a554774e 100644
--- a/traffic_ops/testing/api/v5/cdn_federations_test.go
+++ b/traffic_ops/testing/api/v5/cdn_federations_test.go
@@ -45,7 +45,7 @@ func TestCDNFederations(t *testing.T) {
 		currentTimeRFC := currentTime.Format(time.RFC1123)
 		tomorrow := currentTime.AddDate(0, 0, 1).Format(time.RFC1123)
 
-		methodTests := utils.TestCase[client.Session, client.RequestOptions, tc.CDNFederation]{
+		methodTests := utils.TestCase[client.Session, client.RequestOptions, tc.CDNFederationV5]{
 			"GET": {
 				"NOT MODIFIED when NO CHANGES made": {
 					ClientSession: TOSession,
@@ -107,9 +107,9 @@ func TestCDNFederations(t *testing.T) {
 				"OK when VALID request": {
 					EndpointID:    GetFederationID(t, "google.com."),
 					ClientSession: TOSession,
-					RequestBody: tc.CDNFederation{
-						CName:       util.Ptr("new.cname."),
-						TTL:         util.Ptr(34),
+					RequestBody: tc.CDNFederationV5{
+						CName:       "new.cname.",
+						TTL:         64,
 						Description: util.Ptr("updated"),
 					},
 					Expectations: utils.CkRequest(utils.NoError(), utils.HasStatus(http.StatusOK), validateCDNFederationUpdateFields(map[string]interface{}{"CName": "new.cname."})),
@@ -118,9 +118,9 @@ func TestCDNFederations(t *testing.T) {
 					EndpointID:    GetFederationID(t, "booya.com."),
 					ClientSession: TOSession,
 					RequestOpts:   client.RequestOptions{Header: http.Header{rfc.IfUnmodifiedSince: {currentTimeRFC}}},
-					RequestBody: tc.CDNFederation{
-						CName:       util.Ptr("booya.com."),
-						TTL:         util.Ptr(34),
+					RequestBody: tc.CDNFederationV5{
+						CName:       "booya.com.",
+						TTL:         64,
 						Description: util.Ptr("fooya"),
 					},
 					Expectations: utils.CkRequest(utils.HasError(), utils.HasStatus(http.StatusPreconditionFailed)),
@@ -128,9 +128,9 @@ func TestCDNFederations(t *testing.T) {
 				"PRECONDITION FAILED when updating with IFMATCH ETAG Header": {
 					EndpointID:    GetFederationID(t, "booya.com."),
 					ClientSession: TOSession,
-					RequestBody: tc.CDNFederation{
-						CName:       util.Ptr("new.cname."),
-						TTL:         util.Ptr(34),
+					RequestBody: tc.CDNFederationV5{
+						CName:       "new.cname.",
+						TTL:         64,
 						Description: util.Ptr("updated"),
 					},
 					RequestOpts:  client.RequestOptions{Header: http.Header{rfc.IfMatch: {rfc.ETag(currentTime)}}},
@@ -145,7 +145,7 @@ func TestCDNFederations(t *testing.T) {
 					switch method {
 					case "GET":
 						t.Run(name, func(t *testing.T) {
-							resp, reqInf, err := testCase.ClientSession.GetCDNFederationsByName(cdnName, testCase.RequestOpts)
+							resp, reqInf, err := testCase.ClientSession.GetCDNFederations(cdnName, testCase.RequestOpts)
 							for _, check := range testCase.Expectations {
 								check(t, reqInf, resp.Response, resp.Alerts, err)
 							}
@@ -181,12 +181,11 @@ func TestCDNFederations(t *testing.T) {
 func validateCDNFederationUpdateFields(expectedResp map[string]interface{}) utils.CkReqFunc {
 	return func(t *testing.T, _ toclientlib.ReqInf, resp interface{}, _ tc.Alerts, _ error) {
 		assert.RequireNotNil(t, resp, "Expected CDN Federation response to not be nil.")
-		CDNFederationResp := resp.(tc.CDNFederation)
+		CDNFederationResp := resp.(tc.CDNFederationV5)
 		for field, expected := range expectedResp {
 			switch field {
 			case "CName":
-				assert.RequireNotNil(t, CDNFederationResp.CName, "Expected CName to not be nil.")
-				assert.Equal(t, expected, *CDNFederationResp.CName, "Expected CName to be %v, but got %s", expected, *CDNFederationResp.CName)
+				assert.Equal(t, expected, CDNFederationResp.CName, "Expected CName to be %v, but got %s", expected, CDNFederationResp.CName)
 			default:
 				t.Errorf("Expected field: %v, does not exist in response", field)
 			}
@@ -196,11 +195,11 @@ func validateCDNFederationUpdateFields(expectedResp map[string]interface{}) util
 
 func validateCDNFederationPagination(paginationParam string) utils.CkReqFunc {
 	return func(t *testing.T, _ toclientlib.ReqInf, resp interface{}, _ tc.Alerts, _ error) {
-		paginationResp := resp.([]tc.CDNFederation)
+		paginationResp := resp.([]tc.CDNFederationV5)
 
 		opts := client.NewRequestOptions()
 		opts.QueryParameters.Set("orderby", "id")
-		respBase, _, err := TOSession.GetCDNFederationsByName(cdnName, opts)
+		respBase, _, err := TOSession.GetCDNFederations(cdnName, opts)
 		assert.RequireNoError(t, err, "Cannot get Federation Users: %v - alerts: %+v", err, respBase.Alerts)
 
 		CDNfederations := respBase.Response
@@ -220,10 +219,9 @@ func validateCDNFederationCNameSort() utils.CkReqFunc {
 	return func(t *testing.T, _ toclientlib.ReqInf, resp interface{}, alerts tc.Alerts, _ error) {
 		assert.RequireNotNil(t, resp, "Expected CDN Federation response to not be nil.")
 		var federationCNames []string
-		CDNFederationResp := resp.([]tc.CDNFederation)
-		for _, CDNFederation := range CDNFederationResp {
-			assert.RequireNotNil(t, CDNFederation.CName, "Expected CDN Federation CName to not be nil.")
-			federationCNames = append(federationCNames, *CDNFederation.CName)
+		CDNFederationResp := resp.([]tc.CDNFederationV5)
+		for _, CDNFederationV5 := range CDNFederationResp {
+			federationCNames = append(federationCNames, CDNFederationV5.CName)
 		}
 		assert.Equal(t, true, sort.StringsAreSorted(federationCNames), "List is not sorted by their names: %v", federationCNames)
 	}
@@ -233,9 +231,9 @@ func validateCDNFederationIDDescSort() utils.CkReqFunc {
 	return func(t *testing.T, _ toclientlib.ReqInf, resp interface{}, alerts tc.Alerts, _ error) {
 		assert.RequireNotNil(t, resp, "Expected CDN Federation response to not be nil.")
 		var CDNFederationIDs []int
-		CDNFederationResp := resp.([]tc.CDNFederation)
+		CDNFederationResp := resp.([]tc.CDNFederationV5)
 		for _, federation := range CDNFederationResp {
-			CDNFederationIDs = append([]int{*federation.ID}, CDNFederationIDs...)
+			CDNFederationIDs = append([]int{federation.ID}, CDNFederationIDs...)
 		}
 		assert.Equal(t, true, sort.IntsAreSorted(CDNFederationIDs), "List is not sorted by their ids: %v", CDNFederationIDs)
 	}
@@ -249,16 +247,14 @@ func GetFederationID(t *testing.T, cname string) func() int {
 	}
 }
 
-func setFederationID(t *testing.T, cdnFederation tc.CDNFederation) {
-	assert.RequireNotNil(t, cdnFederation.CName, "Federation CName was nil after posting.")
-	assert.RequireNotNil(t, cdnFederation.ID, "Federation ID was nil after posting.")
-	fedIDs[*cdnFederation.CName] = *cdnFederation.ID
+func setFederationID(t *testing.T, cdnFederation tc.CDNFederationV5) {
+	fedIDs[cdnFederation.CName] = cdnFederation.ID
 }
 
 func CreateTestCDNFederations(t *testing.T) {
 	for _, federation := range testData.Federations {
 		opts := client.NewRequestOptions()
-		opts.QueryParameters.Set("xmlId", *federation.DeliveryServiceIDs.XmlId)
+		opts.QueryParameters.Set("xmlId", federation.DeliveryService.XMLID)
 		dsResp, _, err := TOSession.GetDeliveryServices(opts)
 		assert.RequireNoError(t, err, "Could not get Delivery Service by XML ID: %v", err)
 		assert.RequireEqual(t, 1, len(dsResp.Response), "Expected one Delivery Service, but got %d", len(dsResp.Response))
@@ -271,7 +267,7 @@ func CreateTestCDNFederations(t *testing.T) {
 		setFederationID(t, resp.Response)
 		assert.RequireNotNil(t, resp.Response.ID, "Federation ID was nil after posting.")
 		assert.RequireNotNil(t, dsResp.Response[0].ID, "Delivery Service ID was nil.")
-		_, _, err = TOSession.CreateFederationDeliveryServices(*resp.Response.ID, []int{*dsResp.Response[0].ID}, false, client.NewRequestOptions())
+		_, _, err = TOSession.CreateFederationDeliveryServices(resp.Response.ID, []int{*dsResp.Response[0].ID}, false, client.NewRequestOptions())
 		assert.NoError(t, err, "Could not create Federation Delivery Service: %v", err)
 	}
 }
@@ -283,7 +279,7 @@ func DeleteTestCDNFederations(t *testing.T) {
 		assert.NoError(t, err, "Cannot delete federation #%d: %v - alerts: %+v", id, err, resp.Alerts)
 
 		opts.QueryParameters.Set("id", strconv.Itoa(id))
-		data, _, err := TOSession.GetCDNFederationsByName(cdnName, opts)
+		data, _, err := TOSession.GetCDNFederations(cdnName, opts)
 		assert.Equal(t, 0, len(data.Response), "expected federation to be deleted")
 	}
 	fedIDs = make(map[string]int) // reset the global variable for the next test
diff --git a/traffic_ops/testing/api/v5/federations_test.go b/traffic_ops/testing/api/v5/federations_test.go
index 4bf2dce381..51dcb6d5fe 100644
--- a/traffic_ops/testing/api/v5/federations_test.go
+++ b/traffic_ops/testing/api/v5/federations_test.go
@@ -49,18 +49,19 @@ func TestFederations(t *testing.T) {
 						validateAllFederationsFields([]map[string]interface{}{
 							{
 								"DeliveryService": "ds1",
+								// TODO: Why are these hard-coded copies of the test data?
 								"Mappings": []map[string]interface{}{
 									{
 										"CName": "the.cname.com.",
-										"TTL":   48,
+										"TTL":   68,
 									},
 									{
 										"CName": "booya.com.",
-										"TTL":   34,
+										"TTL":   64,
 									},
 									{
 										"CName": "google.com.",
-										"TTL":   30,
+										"TTL":   60,
 									},
 								},
 							},
diff --git a/traffic_ops/testing/api/v5/tc-fixtures.json b/traffic_ops/testing/api/v5/tc-fixtures.json
index 3fd3bba2ae..05a885f81e 100644
--- a/traffic_ops/testing/api/v5/tc-fixtures.json
+++ b/traffic_ops/testing/api/v5/tc-fixtures.json
@@ -1864,26 +1864,26 @@
     "federations": [
         {
             "cname": "the.cname.com.",
-            "ttl": 48,
+            "ttl": 68,
             "description": "the description",
             "deliveryService": {
-                "xmlId": "ds1"
+                "xmlID": "ds1"
             }
         },
         {
             "cname": "booya.com.",
-            "ttl": 34,
+            "ttl": 64,
             "description": "fooya",
             "deliveryService": {
-                "xmlId": "ds1"
+                "xmlID": "ds1"
             }
         },
         {
             "cname": "google.com.",
-            "ttl": 30,
+            "ttl": 60,
             "description": "google",
             "deliveryService": {
-                "xmlId": "ds1"
+                "xmlID": "ds1"
             }
         }
     ],
diff --git a/traffic_ops/testing/api/v5/traffic_control_test.go b/traffic_ops/testing/api/v5/traffic_control_test.go
index ae2a4452ba..3fca22db40 100644
--- a/traffic_ops/testing/api/v5/traffic_control_test.go
+++ b/traffic_ops/testing/api/v5/traffic_control_test.go
@@ -35,7 +35,7 @@ type TrafficControl struct {
 	DeliveryServiceServerAssignments                  []tc.DeliveryServiceServers             `json:"deliveryServiceServerAssignments"`
 	TopologyBasedDeliveryServicesRequiredCapabilities []tc.DeliveryServicesRequiredCapability `json:"topologyBasedDeliveryServicesRequiredCapabilities"`
 	Divisions                                         []tc.DivisionV5                         `json:"divisions"`
-	Federations                                       []tc.CDNFederation                      `json:"federations"`
+	Federations                                       []tc.CDNFederationV5                    `json:"federations"`
 	FederationResolvers                               []tc.FederationResolverV5               `json:"federation_resolvers"`
 	Jobs                                              []tc.InvalidationJobCreateV4            `json:"jobs"`
 	Origins                                           []tc.OriginV5                           `json:"origins"`
diff --git a/traffic_ops/traffic_ops_golang/api/api.go b/traffic_ops/traffic_ops_golang/api/api.go
index 94a37ff538..7c9c014442 100644
--- a/traffic_ops/traffic_ops_golang/api/api.go
+++ b/traffic_ops/traffic_ops_golang/api/api.go
@@ -720,13 +720,14 @@ func (inf APIInfo) WriteOKResponseWithSummary(resp any, count uint64) (int, erro
 }
 
 // WriteNotModifiedResponse writes a 304 Not Modified response with the given
-// object as the 'response' property of the response body.
+// time as the last modified time in the headers.
 //
 // This CANNOT be used by any APIInfo that wasn't constructed for the caller by
 // Wrap - ing a Handler (yet).
-func (inf APIInfo) WriteNotModifiedResponse(resp any) (int, error, error) {
+func (inf APIInfo) WriteNotModifiedResponse(lastModified time.Time) (int, error, error) {
+	inf.w.Header().Set(rfc.LastModified, FormatLastModified(lastModified))
 	inf.w.WriteHeader(http.StatusNotModified)
-	WriteResp(inf.w, inf.request, resp)
+	setRespWritten(inf.request)
 	return http.StatusNotModified, nil, nil
 }
 
diff --git a/traffic_ops/traffic_ops_golang/api/api_test.go b/traffic_ops/traffic_ops_golang/api/api_test.go
index e852cdeaaa..56d8a5ce7c 100644
--- a/traffic_ops/traffic_ops_golang/api/api_test.go
+++ b/traffic_ops/traffic_ops_golang/api/api_test.go
@@ -321,7 +321,7 @@ func TestAPIInfo_WriteNotModifiedResponse(t *testing.T) {
 		request: r,
 		w:       w,
 	}
-	code, userErr, sysErr := inf.WriteNotModifiedResponse("test")
+	code, userErr, sysErr := inf.WriteNotModifiedResponse(time.Time{})
 	if code != http.StatusNotModified {
 		t.Errorf("WriteNotModifiedResponse should return a %d %s code, got: %d %s", http.StatusNotModified, http.StatusText(http.StatusNotModified), code, http.StatusText(code))
 	}
diff --git a/traffic_ops/traffic_ops_golang/cdnfederation/cdnfederations.go b/traffic_ops/traffic_ops_golang/cdnfederation/cdnfederations.go
index b15b61283b..514ae7c5a3 100644
--- a/traffic_ops/traffic_ops_golang/cdnfederation/cdnfederations.go
+++ b/traffic_ops/traffic_ops_golang/cdnfederation/cdnfederations.go
@@ -1,3 +1,5 @@
+// Package cdnfederation is one of many, many packages that contain logic
+// pertaining to federations of CDNs and/or parts of CDNs.
 package cdnfederation
 
 /*
@@ -28,14 +30,19 @@ import (
 	"strings"
 	"time"
 
+	"github.com/apache/trafficcontrol/lib/go-log"
 	"github.com/apache/trafficcontrol/lib/go-tc"
 	"github.com/apache/trafficcontrol/lib/go-tc/tovalidate"
 	"github.com/apache/trafficcontrol/lib/go-util"
 	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/api"
 	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/dbhelpers"
 	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/tenant"
+	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/util/ims"
+
 	"github.com/asaskevich/govalidator"
 	validation "github.com/go-ozzo/ozzo-validation"
+	"github.com/go-ozzo/ozzo-validation/is"
+	"github.com/lib/pq"
 )
 
 // we need a type alias to define functions on
@@ -49,32 +56,103 @@ func (v *TOCDNFederation) GetLastUpdated() (*time.Time, bool, error) {
 	return api.GetLastUpdated(v.APIInfo().Tx, *v.ID, "federation")
 }
 
+func selectMaxLastUpdatedQuery(where, orderBy, pagination string) string {
+	return `
+	SELECT max(t)
+	FROM (
+		(
+			SELECT federation.last_updated AS t
+			FROM federation
+			JOIN federation_deliveryservice fds
+				ON fds.federation = federation.id
+			JOIN deliveryservice ds
+				ON ds.id = fds.deliveryservice
+			JOIN cdn c
+				ON c.id = ds.cdn_id ` + where + orderBy + pagination +
+		`)
+		UNION ALL
+		(
+			SELECT max(last_updated) AS t
+			FROM last_deleted l
+			WHERE l.table_name='federation'
+		)
+	) AS res`
+}
+
 func (v *TOCDNFederation) SetLastUpdated(t tc.TimeNoMod) { v.LastUpdated = &t }
-func (v *TOCDNFederation) InsertQuery() string           { return insertQuery() }
-func (v *TOCDNFederation) SelectMaxLastUpdatedQuery(where, orderBy, pagination, tableName string) string {
-	return `SELECT max(t) from (
-		SELECT max(federation.last_updated) as t from federation
-		join federation_deliveryservice fds on fds.federation = federation.id
-		join deliveryservice ds on ds.id = fds.deliveryservice
-		join cdn c on c.id = ds.cdn_id ` + where + orderBy + pagination +
-		` UNION ALL
-		select max(last_updated) as t from last_deleted l where l.table_name='federation') as res`
+func (*TOCDNFederation) InsertQuery() string {
+	return `
+	INSERT INTO federation (
+	cname,
+ 	ttl,
+ 	description
+  ) VALUES (
+ 	:cname,
+	:ttl,
+	:description
+	) RETURNING id, last_updated`
+}
+func (v *TOCDNFederation) SelectMaxLastUpdatedQuery(where, orderBy, pagination, _ string) string {
+	return selectMaxLastUpdatedQuery(where, orderBy, pagination)
 }
 
 func (v *TOCDNFederation) NewReadObj() interface{} { return &TOCDNFederation{} }
 func (v *TOCDNFederation) SelectQuery() string {
 	return selectByCDNName()
 }
-func (v *TOCDNFederation) ParamColumns() map[string]dbhelpers.WhereColumnInfo {
-	cols := map[string]dbhelpers.WhereColumnInfo{
-		"id":    dbhelpers.WhereColumnInfo{Column: "federation.id", Checker: api.IsInt},
-		"name":  dbhelpers.WhereColumnInfo{Column: "c.name", Checker: nil},
-		"cname": dbhelpers.WhereColumnInfo{Column: "federation.cname", Checker: nil},
+
+func paramColumnInfo(v api.Version) map[string]dbhelpers.WhereColumnInfo {
+	if v.GreaterThanOrEqualTo(&api.Version{Major: 5}) {
+		return map[string]dbhelpers.WhereColumnInfo{
+			"id": {
+				Column:  "federation.id",
+				Checker: api.IsInt,
+			},
+			"name": {
+				Column: "c.name",
+			},
+			"cname": {
+				Column: "federation.cname",
+			},
+			"xmlID": {
+				Column: "ds.xml_id",
+			},
+			"dsID": {
+				Column:  "ds.id",
+				Checker: api.IsInt,
+			},
+		}
+	}
+	return map[string]dbhelpers.WhereColumnInfo{
+		"id": {
+			Column:  "federation.id",
+			Checker: api.IsInt,
+		},
+		"name": {
+			Column:  "c.name",
+			Checker: nil,
+		},
+		"cname": {
+			Column:  "federation.cname",
+			Checker: nil,
+		},
 	}
-	return cols
 }
-func (v *TOCDNFederation) DeleteQuery() string { return deleteQuery() }
-func (v *TOCDNFederation) UpdateQuery() string { return updateQuery() }
+
+func (v *TOCDNFederation) ParamColumns() map[string]dbhelpers.WhereColumnInfo {
+	return paramColumnInfo(*v.ReqInfo.Version)
+}
+func (*TOCDNFederation) DeleteQuery() string { return `DELETE FROM federation WHERE id = :id` }
+func (*TOCDNFederation) UpdateQuery() string {
+	return `
+UPDATE federation SET
+	cname = :cname,
+	ttl = :ttl,
+	description = :description
+WHERE
+  id=:id
+RETURNING last_updated`
+}
 
 // Fufills `Identifier' interface
 func (fed TOCDNFederation) GetKeyFieldsInfo() []api.KeyFieldInfo {
@@ -348,9 +426,8 @@ func selectByID() string {
 	// WHERE federation.id = :id (determined by dbhelper)
 }
 
-func selectByCDNName() string {
-	return `
-	SELECT
+const selectQuery = `
+SELECT
 	ds.tenant_id,
 	federation.id AS id,
 	federation.cname,
@@ -359,37 +436,254 @@ func selectByCDNName() string {
 	federation.last_updated,
 	ds.id AS ds_id,
 	ds.xml_id
-	FROM federation
-	JOIN federation_deliveryservice AS fd ON federation.id = fd.federation
-	JOIN deliveryservice AS ds ON ds.id = fd.deliveryservice
-	JOIN cdn c ON c.id = ds.cdn_id`
-	// WHERE cdn.name = :cdn_name (determined by dbhelper)
+FROM federation
+JOIN
+	federation_deliveryservice AS fd
+	ON federation.id = fd.federation
+JOIN
+	deliveryservice AS ds
+	ON ds.id = fd.deliveryservice
+JOIN
+	cdn AS c
+	ON c.id = ds.cdn_id
+`
+
+const insertQuery = `
+INSERT INTO federation (
+	cname,
+	ttl,
+	"description"
+) VALUES (
+	$1,
+	$2,
+	$3
+)
+RETURNING
+	id,
+	last_updated
+`
+
+func selectByCDNName() string {
+	return selectQuery
 }
 
-func updateQuery() string {
-	return `
+const updateQuery = `
 UPDATE federation SET
-	cname = :cname,
-	ttl = :ttl,
-	description = :description
+	cname = $1,
+	ttl = $2,
+	"description" = $3
 WHERE
-  id=:id
-RETURNING last_updated`
+  id = $4
+RETURNING
+	last_updated
+`
+
+const deleteQuery = `
+DELETE FROM federation
+WHERE id = $1
+RETURNING
+	cname,
+	"description",
+	id,
+	last_updated,
+	ttl
+`
+
+func addTenancyStmt(where string) string {
+	if where == "" {
+		where = "WHERE "
+	} else {
+		where += " AND "
+	}
+	where += "ds.tenant_id = ANY(:tenantIDs)"
+	return where
 }
 
-func insertQuery() string {
-	return `
-	INSERT INTO federation (
-	cname,
- 	ttl,
- 	description
-  ) VALUES (
- 	:cname,
-	:ttl,
-	:description
-	) RETURNING id, last_updated`
+func getCDNFederations(inf *api.APIInfo) ([]tc.CDNFederationV5, time.Time, int, error, error) {
+	tenantList, err := tenant.GetUserTenantIDListTx(inf.Tx.Tx, inf.User.TenantID)
+	if err != nil {
+		return nil, time.Time{}, http.StatusInternalServerError, nil, fmt.Errorf("getting tenant list for user: %w", err)
+	}
+
+	cols := paramColumnInfo(*inf.Version)
+
+	where, orderBy, pagination, queryValues, errs := dbhelpers.BuildWhereAndOrderByAndPagination(inf.Params, cols)
+	if len(errs) > 0 {
+		return nil, time.Time{}, http.StatusBadRequest, util.JoinErrs(errs), nil
+	}
+	queryValues["tenantIDs"] = pq.Array(tenantList)
+
+	where = addTenancyStmt(where)
+
+	if inf.UseIMS() {
+		query := selectMaxLastUpdatedQuery(where, orderBy, pagination)
+		cont, max := ims.TryIfModifiedSinceQuery(inf.Tx, inf.RequestHeaders(), queryValues, query)
+		if !cont {
+			log.Debugln("IMS HIT")
+			return nil, max, http.StatusNotModified, nil, nil
+		}
+		log.Debugln("IMS MISS")
+	} else {
+		log.Debugln("Non IMS request")
+	}
+
+	query := selectQuery + where + orderBy + pagination
+	rows, err := inf.Tx.NamedQuery(query, queryValues)
+	if err != nil {
+		userErr, sysErr, code := api.ParseDBError(err)
+		return nil, time.Time{}, code, userErr, sysErr
+	}
+	defer log.Close(rows, "closing CDNFederation rows")
+
+	ret := []tc.CDNFederationV5{}
+	for rows.Next() {
+		fed := tc.CDNFederationV5{
+			DeliveryService: new(tc.CDNFederationDeliveryService),
+		}
+		var tenantID int
+		err := rows.Scan(
+			&tenantID,
+			&fed.ID,
+			&fed.CName,
+			&fed.TTL,
+			&fed.Description,
+			&fed.LastUpdated,
+			&fed.DeliveryService.ID,
+			&fed.DeliveryService.XMLID,
+		)
+		if err != nil {
+			return nil, time.Time{}, http.StatusInternalServerError, nil, fmt.Errorf("scanning a CDN Federation: %w", err)
+		}
+
+		ret = append(ret, fed)
+	}
+
+	return ret, time.Time{}, http.StatusOK, nil, nil
+}
+
+// Read handles GET requests to `cdns/{{name}}/federations`.
+func Read(inf *api.APIInfo) (int, error, error) {
+	api.DefaultSort(inf, "cname")
+	feds, max, code, userErr, sysErr := getCDNFederations(inf)
+	if userErr != nil || sysErr != nil {
+		return code, userErr, sysErr
+	}
+	if feds == nil {
+		return inf.WriteNotModifiedResponse(max)
+	}
+	return inf.WriteOKResponse(feds)
 }
 
-func deleteQuery() string {
-	return `DELETE FROM federation WHERE id = :id`
+// ReadID handles GET requests to `cdns/{{name}}/federations/{{ID}}`.
+func ReadID(inf *api.APIInfo) (int, error, error) {
+	feds, max, code, userErr, sysErr := getCDNFederations(inf)
+	if userErr != nil || sysErr != nil {
+		return code, userErr, sysErr
+	}
+	if feds == nil {
+		return inf.WriteNotModifiedResponse(max)
+	}
+
+	id := inf.IntParams["id"]
+	if len(feds) == 0 {
+		return http.StatusNotFound, fmt.Errorf("no such Federation #%d in CDN %s", id, inf.Params["name"]), nil
+	}
+	if len(feds) > 1 {
+		return http.StatusInternalServerError, nil, fmt.Errorf("%d CDN federations found by ID: %d", len(feds), id)
+	}
+	return inf.WriteOKResponse(feds[0])
+}
+
+func validate(fed tc.CDNFederationV5) error {
+	endsWithDot := validation.NewStringRule(
+		func(str string) bool {
+			return strings.HasSuffix(str, ".")
+		},
+		"must end with a period",
+	)
+
+	validateErrs := validation.Errors{
+		"cname": validation.Validate(fed.CName, validation.Required, is.DNSName, endsWithDot),
+		"ttl":   validation.Validate(fed.TTL, validation.Required, validation.Min(60)),
+	}
+
+	return util.JoinErrs(tovalidate.ToErrors(validateErrs))
+}
+
+// Create handles POST requests to `cdns/{{name}}/federations`.
+func Create(inf *api.APIInfo) (int, error, error) {
+	var fed tc.CDNFederationV5
+	if err := inf.DecodeBody(&fed); err != nil {
+		return http.StatusBadRequest, fmt.Errorf("parsing request body: %w", err), nil
+	}
+
+	// You can't set this at creation time, but if it was in the request it
+	// would be shown in the response - we're supposed to ignore extra fields.
+	// This doesn't do that exactly, but it helps.
+	fed.DeliveryService = nil
+
+	err := validate(fed)
+	if err != nil {
+		return http.StatusBadRequest, err, nil
+	}
+
+	err = inf.Tx.Tx.QueryRow(insertQuery, fed.CName, fed.TTL, fed.Description).Scan(&fed.ID, &fed.LastUpdated)
+	if err != nil {
+		userErr, sysErr, code := api.ParseDBError(err)
+		return code, userErr, fmt.Errorf("inserting a CDN Federation: %w", sysErr)
+	}
+
+	return inf.WriteCreatedResponse(fed, "Federation was created", "federations/"+strconv.Itoa(fed.ID))
+}
+
+// Update handles PUT requests to `cdns/{{name}}/federations/{{id}}`.
+func Update(inf *api.APIInfo) (int, error, error) {
+	var fed tc.CDNFederationV5
+	if err := inf.DecodeBody(&fed); err != nil {
+		return http.StatusBadRequest, fmt.Errorf("parsing request body: %w", err), nil
+	}
+
+	id := inf.IntParams["id"]
+
+	var lastModified time.Time
+	err := inf.Tx.QueryRow("SELECT last_updated FROM federation WHERE id = $1", id).Scan(&lastModified)
+	if err != nil {
+		return http.StatusInternalServerError, nil, fmt.Errorf("getting last modified time for Federation #%d: %w", id, err)
+	}
+	if !api.IsUnmodified(inf.RequestHeaders(), lastModified) {
+		return http.StatusPreconditionFailed, api.ResourceModifiedError, nil
+	}
+
+	// You can't set this via a PUT request, but if it was in the request it
+	// would be shown in the response - we're supposed to ignore extra fields.
+	// This doesn't do that exactly, but it helps.
+	fed.DeliveryService = nil
+	fed.ID = id
+
+	err = validate(fed)
+	if err != nil {
+		return http.StatusBadRequest, err, nil
+	}
+
+	err = inf.Tx.Tx.QueryRow(updateQuery, fed.CName, fed.TTL, fed.Description, id).Scan(&fed.LastUpdated)
+	if err != nil {
+		userErr, sysErr, code := api.ParseDBError(err)
+		return code, userErr, sysErr
+	}
+
+	return inf.WriteSuccessResponse(fed, "Federation was updated")
+}
+
+// Delete handles DELETE requests to `cdns/{{name}}/federations/{{id}}`.
+func Delete(inf *api.APIInfo) (int, error, error) {
+	id := inf.IntParams["id"]
+
+	var fed tc.CDNFederationV5
+	err := inf.Tx.QueryRow(deleteQuery, id).Scan(&fed.CName, &fed.Description, &fed.ID, &fed.LastUpdated, &fed.TTL)
+	if err != nil {
+		userErr, sysErr, code := api.ParseDBError(err)
+		return code, userErr, sysErr
+	}
+
+	return inf.WriteSuccessResponse(fed, "Federation was deleted")
 }
diff --git a/traffic_ops/traffic_ops_golang/cdnfederation/cdnfederations_test.go b/traffic_ops/traffic_ops_golang/cdnfederation/cdnfederations_test.go
new file mode 100644
index 0000000000..e671122a00
--- /dev/null
+++ b/traffic_ops/traffic_ops_golang/cdnfederation/cdnfederations_test.go
@@ -0,0 +1,312 @@
+package cdnfederation
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 (
+	"bytes"
+	"context"
+	"encoding/json"
+	"errors"
+	"net/http"
+	"net/http/httptest"
+	"reflect"
+	"strings"
+	"testing"
+	"time"
+
+	"github.com/apache/trafficcontrol/lib/go-tc"
+	"github.com/apache/trafficcontrol/lib/go-util"
+	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/api"
+	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/auth"
+	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/config"
+	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/trafficvault/backends/disabled"
+	"github.com/jmoiron/sqlx"
+	"gopkg.in/DATA-DOG/go-sqlmock.v1"
+)
+
+func TestAddTenancyStmt(t *testing.T) {
+	where := ""
+	output := addTenancyStmt(where)
+	if expected := "WHERE ds.tenant_id = ANY(:tenantIDs)"; output != expected {
+		t.Errorf("Incorrect statement from blank WHERE; want: '%s', got: '%s", expected, output)
+	}
+	where = "WHERE cname=:cname"
+	output = addTenancyStmt(where)
+	if expected := "WHERE cname=:cname AND ds.tenant_id = ANY(:tenantIDs)"; output != expected {
+		t.Errorf("Incorrect statement from blank WHERE; want: '%s', got: '%s", expected, output)
+	}
+}
+
+func TestParamColumnInfo(t *testing.T) {
+	params := paramColumnInfo(api.Version{Major: 4})
+	if l := len(params); l != 3 {
+		t.Errorf("Expected versions prior to 5 to support 3 query params, found support for: %d", l)
+	}
+	for _, param := range [3]string{"cname", "id", "name"} {
+		if _, ok := params[param]; !ok {
+			t.Errorf("Expected versions prior to 5 to support the '%s' query param, but support for such wasn't found", param)
+		}
+	}
+
+	params = paramColumnInfo(api.Version{Major: 5})
+	if l := len(params); l != 5 {
+		t.Errorf("Expected versions 5 and later to support 5 query params, found support for: %d", l)
+	}
+	for _, param := range [5]string{"cname", "dsID", "id", "name", "xmlID"} {
+		if _, ok := params[param]; !ok {
+			t.Errorf("Expected versions 5 and later to support the '%s' query param, but support for such wasn't found", param)
+		}
+	}
+}
+
+func getMockTx(t *testing.T) (sqlmock.Sqlmock, *sqlx.Tx, *sqlx.DB) {
+	t.Helper()
+	mockDB, mock, err := sqlmock.New()
+	if err != nil {
+		t.Fatalf("an error '%s' was not expected when opening a stub database connection", err)
+	}
+
+	db := sqlx.NewDb(mockDB, "sqlmock")
+	mock.ExpectBegin()
+
+	return mock, db.MustBegin(), db
+}
+
+func cleanup(t *testing.T, mock sqlmock.Sqlmock, db *sqlx.DB) {
+	t.Helper()
+	mock.ExpectClose()
+	db.Close()
+	if err := mock.ExpectationsWereMet(); err != nil {
+		t.Errorf("expectations were not met: %v", err)
+	}
+}
+
+func gettingUserTenantListFails(t *testing.T) {
+	mock, tx, db := getMockTx(t)
+	defer cleanup(t, mock, db)
+	defer func() {
+		mock.ExpectRollback()
+		tx.Rollback()
+	}()
+
+	err := errors.New("unknown failure")
+	mock.ExpectQuery("WITH RECURSIVE").WillReturnError(err)
+	_, _, code, userErr, sysErr := getCDNFederations(&api.APIInfo{Tx: tx, User: &auth.CurrentUser{TenantID: 1}})
+
+	if code != http.StatusInternalServerError {
+		t.Errorf("Incorrect response code when getting user tenants fails; want: %d, got: %d", http.StatusInternalServerError, code)
+	}
+	if userErr != nil {
+		t.Errorf("Unexpected user-facing error: %v", userErr)
+	}
+	if sysErr == nil {
+		t.Fatal("Expected a system error but didn't get one")
+	}
+
+	// You can't use `errors.Is` here because sqlmock doesn't wrap the error you
+	// give it, so we have to resort to comparing text and praying there's no
+	// weird coincidence going on behind the scenes.
+	if !strings.Contains(sysErr.Error(), err.Error()) {
+		t.Errorf("Incorrect system error returned; want: %v, got: %v", err, sysErr)
+	}
+}
+
+func buildingQueryPartsFails(t *testing.T) {
+	mock, tx, db := getMockTx(t)
+	defer cleanup(t, mock, db)
+	defer func() {
+		mock.ExpectRollback()
+		tx.Rollback()
+	}()
+
+	rows := sqlmock.NewRows([]string{"id"})
+	rows.AddRow(1)
+
+	mock.ExpectQuery("WITH RECURSIVE").WillReturnRows(rows)
+
+	inf := api.APIInfo{
+		Params: map[string]string{
+			"dsID": "not an integer",
+		},
+		Tx:      tx,
+		User:    &auth.CurrentUser{TenantID: 1},
+		Version: &api.Version{Major: 5},
+	}
+	_, _, code, userErr, sysErr := getCDNFederations(&inf)
+	if code != http.StatusBadRequest {
+		t.Errorf("Incorrect response code when getting user tenants fails; want: %d, got: %d", http.StatusBadRequest, code)
+	}
+	if sysErr != nil {
+		t.Errorf("Unexpected system error: %v", sysErr)
+	}
+	if userErr == nil {
+		t.Fatal("Expected a user-facing error, but didn't get one")
+	}
+	if !strings.Contains(userErr.Error(), "dsID") {
+		t.Errorf("Incorrect user error; expected it to mention 'dsID', but it didn't: %v", userErr)
+	}
+}
+
+func everythingWorks(t *testing.T) {
+	mock, tx, db := getMockTx(t)
+	defer cleanup(t, mock, db)
+	defer func() {
+		mock.ExpectCommit()
+		tx.Commit()
+	}()
+
+	rows := sqlmock.NewRows([]string{"id"})
+	rows.AddRow(1)
+
+	mock.ExpectQuery("WITH RECURSIVE").WillReturnRows(rows)
+
+	fedRows := sqlmock.NewRows([]string{"tenant_id", "id", "cname", "ttl", "description", "last_updated", "ds_id", "xml_id"})
+	fed := tc.CDNFederationV5{
+		CName:       "test.quest.",
+		Description: util.Ptr("a non-blank description"),
+		DeliveryService: &tc.CDNFederationDeliveryService{
+			ID:    1,
+			XMLID: "some-xmlid",
+		},
+		ID:          1,
+		LastUpdated: time.Time{}.Add(time.Hour),
+		TTL:         5,
+	}
+
+	fedRows.AddRow(1, fed.ID, fed.CName, fed.TTL, fed.Description, fed.LastUpdated, fed.DeliveryService.ID, fed.DeliveryService.XMLID)
+	mock.ExpectQuery("SELECT").WillReturnRows(fedRows)
+
+	feds, _, _, userErr, sysErr := getCDNFederations(&api.APIInfo{Tx: tx, User: &auth.CurrentUser{TenantID: 1}, Version: &api.Version{Major: 5}})
+	if userErr != nil {
+		t.Errorf("Unexpected user-facing error: %v", userErr)
+	}
+	if sysErr != nil {
+		t.Errorf("Unexpected system error: %v", sysErr)
+	}
+	if l := len(feds); l != 1 {
+		t.Fatalf("Expected one federation to be returned; got: %d", l)
+	}
+	if !reflect.DeepEqual(feds[0], fed) {
+		t.Errorf("expected a federation like '%#v', but instead found: %#v", fed, feds[0])
+	}
+}
+
+func TestGetCDNFederations(t *testing.T) {
+	t.Run("getting user Tenant list fails", gettingUserTenantListFails)
+	t.Run("building where/orderby/pagination fails", buildingQueryPartsFails)
+	t.Run("everything works", everythingWorks)
+}
+
+func wrapContext(r *http.Request, key any, value any) *http.Request {
+	return r.WithContext(context.WithValue(r.Context(), key, value))
+}
+
+func testingInf(t *testing.T, body []byte) (*http.Request, sqlmock.Sqlmock, *sqlx.DB) {
+	t.Helper()
+	mockDB, mock, err := sqlmock.New()
+	if err != nil {
+		t.Fatalf("failed to open a stub database connection: %v", err)
+	}
+
+	db := sqlx.NewDb(mockDB, "sqlmock")
+
+	r := httptest.NewRequest(http.MethodPost, "/api/5.0/cdns/ALL/federations", bytes.NewReader(body))
+	r = wrapContext(r, api.ConfigContextKey, &config.Config{ConfigTrafficOpsGolang: config.ConfigTrafficOpsGolang{DBQueryTimeoutSeconds: 1000}})
+	r = wrapContext(r, api.DBContextKey, db)
+	r = wrapContext(r, api.TrafficVaultContextKey, &disabled.Disabled{})
+	r = wrapContext(r, api.ReqIDContextKey, uint64(0))
+	r = wrapContext(r, auth.CurrentUserKey, auth.CurrentUser{})
+	r = wrapContext(r, api.PathParamsKey, make(map[string]string))
+
+	mock.ExpectBegin()
+
+	return r, mock, db
+}
+
+func TestCreate(t *testing.T) {
+	newFed := tc.CDNFederationV5{
+		CName:       "test.quest.",
+		TTL:         420,
+		Description: nil,
+	}
+	bts, err := json.Marshal(newFed)
+	if err != nil {
+		t.Fatalf("marshaling testing request body: %v", err)
+	}
+
+	newFed.ID = 1
+	newFed.LastUpdated = time.Time{}.Add(time.Hour)
+
+	r, mock, db := testingInf(t, bts)
+	defer cleanup(t, mock, db)
+
+	rows := sqlmock.NewRows([]string{"id", "last_updated"})
+	rows.AddRow(newFed.ID, newFed.LastUpdated)
+	mock.ExpectQuery("INSERT").WillReturnRows(rows)
+
+	f := api.Wrap(Create, nil, nil)
+	w := httptest.NewRecorder()
+	f(w, r)
+
+	if w.Code != http.StatusCreated {
+		t.Errorf("Incorrect response code; want: %d, got: %d", http.StatusCreated, w.Code)
+	}
+
+	var created tc.CDNFederationV5Response
+	err = json.Unmarshal(w.Body.Bytes(), &created)
+	if err != nil {
+		t.Fatalf("Unmarshaling response: %v", err)
+	}
+
+	if created.Response != newFed {
+		t.Errorf("Didn't create the expected Federation; want: %#v, got: %#v", newFed, created.Response)
+	}
+}
+
+func TestValidate(t *testing.T) {
+	fed := tc.CDNFederationV5{
+		CName: "test.quest.",
+		TTL:   60,
+	}
+	err := validate(fed)
+	if err != nil {
+		t.Errorf("Unexpected validation error: %v", err)
+	}
+
+	fed.TTL--
+	err = validate(fed)
+	if err == nil {
+		t.Fatal("Expected an error for TTL below minimum, but didn't get one")
+	}
+	if !strings.Contains(err.Error(), "ttl") {
+		t.Errorf("Expected error message to mention 'ttl': %v", err)
+	}
+
+	fed.TTL = 60
+	fed.CName = "test.quest"
+	err = validate(fed)
+	if err == nil {
+		t.Fatal("Expected an error for a CNAME without a terminating '.', but didn't get one")
+	}
+	if !strings.Contains(err.Error(), "cname") {
+		t.Errorf("Expected error message to mention 'cname': %v", err)
+	}
+
+}
diff --git a/traffic_ops/traffic_ops_golang/routing/routes.go b/traffic_ops/traffic_ops_golang/routing/routes.go
index 2aa1b328e1..23d70f83c6 100644
--- a/traffic_ops/traffic_ops_golang/routing/routes.go
+++ b/traffic_ops/traffic_ops_golang/routing/routes.go
@@ -395,10 +395,11 @@ func Routes(d ServerData) ([]Route, http.Handler, error) {
 		{Version: api.Version{Major: 5, Minor: 0}, Method: http.MethodDelete, Path: `deliveryservices/{xmlID}/urisignkeys$`, Handler: urisigning.RemoveDeliveryServiceURIKeysHandler, RequiredPrivLevel: auth.PrivLevelAdmin, RequiredPermissions: []string{"DS-SECURITY-KEY:DELETE", "DS-SECURITY-KEY:READ", "DELIVERY-SERVICE:READ", "DELIVERY-SERVICE:UPDATE"}, Authenticated: Authenticated, Middlewares: nil, ID: 42992541731},
 
 		// Federations by CDN (the actual table for federation)
-		{Version: api.Version{Major: 5, Minor: 0}, Method: http.MethodGet, Path: `cdns/{name}/federations/?$`, Handler: api.ReadHandler(&cdnfederation.TOCDNFederation{}), RequiredPrivLevel: auth.PrivLevelReadOnly, RequiredPermissions: []string{"CDN:READ", "FEDERATION:READ", "DELIVERY-SERVICE:READ"}, Authenticated: Authenticated, Middlewares: nil, ID: 48922503231},
-		{Version: api.Version{Major: 5, Minor: 0}, Method: http.MethodPost, Path: `cdns/{name}/federations/?$`, Handler: api.CreateHandler(&cdnfederation.TOCDNFederation{}), RequiredPrivLevel: auth.PrivLevelAdmin, RequiredPermissions: []string{"FEDERATION:CREATE", "FEDERATION:READ, CDN:READ"}, Authenticated: Authenticated, Middlewares: nil, ID: 495489421931},
-		{Version: api.Version{Major: 5, Minor: 0}, Method: http.MethodPut, Path: `cdns/{name}/federations/{id}$`, Handler: api.UpdateHandler(&cdnfederation.TOCDNFederation{}), RequiredPrivLevel: auth.PrivLevelAdmin, RequiredPermissions: []string{"FEDERATION:UPDATE", "FEDERATION:READ", "CDN:READ"}, Authenticated: Authenticated, Middlewares: nil, ID: 42606546631},
-		{Version: api.Version{Major: 5, Minor: 0}, Method: http.MethodDelete, Path: `cdns/{name}/federations/{id}$`, Handler: api.DeleteHandler(&cdnfederation.TOCDNFederation{}), RequiredPrivLevel: auth.PrivLevelAdmin, RequiredPermissions: []string{"FEDERATION:DELETE", "FEDERATION:READ", "CDN:READ"}, Authenticated: Authenticated, Middlewares: nil, ID: 444285290231},
+		{Version: api.Version{Major: 5, Minor: 0}, Method: http.MethodGet, Path: `cdns/{name}/federations/?$`, Handler: api.Wrap(cdnfederation.Read, []string{"name"}, nil), RequiredPrivLevel: auth.PrivLevelReadOnly, RequiredPermissions: []string{"CDN:READ", "FEDERATION:READ", "DELIVERY-SERVICE:READ"}, Authenticated: Authenticated, Middlewares: nil, ID: 48922503231},
+		{Version: api.Version{Major: 5, Minor: 0}, Method: http.MethodPost, Path: `cdns/{name}/federations/?$`, Handler: api.Wrap(cdnfederation.Create, nil, nil), RequiredPrivLevel: auth.PrivLevelAdmin, RequiredPermissions: []string{"FEDERATION:CREATE", "FEDERATION:READ, CDN:READ"}, Authenticated: Authenticated, Middlewares: nil, ID: 495489421931},
+		{Version: api.Version{Major: 5, Minor: 0}, Method: http.MethodGet, Path: `cdns/{name}/federations/{id}/?$`, Handler: api.Wrap(cdnfederation.ReadID, []string{"name"}, []string{"id"}), RequiredPrivLevel: auth.PrivLevelReadOnly, RequiredPermissions: []string{"CDN:READ", "FEDERATION:READ", "DELIVERY-SERVICE:READ"}, Authenticated: Authenticated, Middlewares: nil, ID: 48922503232},
+		{Version: api.Version{Major: 5, Minor: 0}, Method: http.MethodPut, Path: `cdns/{name}/federations/{id}$`, Handler: api.Wrap(cdnfederation.Update, nil, []string{"id"}), RequiredPrivLevel: auth.PrivLevelAdmin, RequiredPermissions: []string{"FEDERATION:UPDATE", "FEDERATION:READ", "CDN:READ"}, Authenticated: Authenticated, Middlewares: nil, ID: 42606546631},
+		{Version: api.Version{Major: 5, Minor: 0}, Method: http.MethodDelete, Path: `cdns/{name}/federations/{id}$`, Handler: api.Wrap(cdnfederation.Delete, nil, []string{"id"}), RequiredPrivLevel: auth.PrivLevelAdmin, RequiredPermissions: []string{"FEDERATION:DELETE", "FEDERATION:READ", "CDN:READ"}, Authenticated: Authenticated, Middlewares: nil, ID: 444285290231},
 
 		{Version: api.Version{Major: 5, Minor: 0}, Method: http.MethodPost, Path: `cdns/{name}/dnsseckeys/ksk/generate$`, Handler: cdn.GenerateKSK, RequiredPrivLevel: auth.PrivLevelAdmin, RequiredPermissions: []string{"DNS-SEC:CREATE", "CDN:UPDATE", "CDN:READ"}, Authenticated: Authenticated, Middlewares: nil, ID: 47292428131},
 
diff --git a/traffic_ops/traffic_ops_golang/server/servers.go b/traffic_ops/traffic_ops_golang/server/servers.go
index 718f2d1870..5601df03fb 100644
--- a/traffic_ops/traffic_ops_golang/server/servers.go
+++ b/traffic_ops/traffic_ops_golang/server/servers.go
@@ -674,11 +674,8 @@ func Read(inf *api.APIInfo) (int, error, error) {
 	version := inf.Version
 
 	servers, serverCount, userErr, sysErr, errCode, maxTime := getServers(inf.RequestHeaders(), inf.Params, inf.Tx, inf.User, useIMS, *version, inf.Config.RoleBasedPermissions)
-	if useIMS && maxTime != nil {
-		inf.SetLastModified(*maxTime)
-	}
-	if errCode == http.StatusNotModified {
-		return inf.WriteNotModifiedResponse([]tc.ServerV5{})
+	if useIMS && maxTime != nil && errCode == http.StatusNotModified {
+		return inf.WriteNotModifiedResponse(*maxTime)
 	}
 	if userErr != nil || sysErr != nil {
 		return errCode, userErr, sysErr
diff --git a/traffic_ops/traffic_ops_golang/util/ims/ims.go b/traffic_ops/traffic_ops_golang/util/ims/ims.go
index 53dbbbddbe..f53e76ec7c 100644
--- a/traffic_ops/traffic_ops_golang/util/ims/ims.go
+++ b/traffic_ops/traffic_ops_golang/util/ims/ims.go
@@ -2,6 +2,7 @@ package ims
 
 import (
 	"database/sql"
+	"errors"
 	"net/http"
 	"time"
 
@@ -61,13 +62,13 @@ func TryIfModifiedSinceQuery(tx *sqlx.Tx, h http.Header, queryValues map[string]
 		if rows != nil {
 			defer rows.Close()
 		}
+		if errors.Is(err, sql.ErrNoRows) {
+			return dontRunSecond, maxTime
+		}
 		if err != nil {
 			log.Errorf("Couldn't get the max last updated time: %v", err)
 			return runSecond, maxTime
 		}
-		if err == sql.ErrNoRows {
-			return dontRunSecond, maxTime
-		}
 		// This should only ever contain one row
 		if rows.Next() {
 			v := &LatestTimestamp{}
diff --git a/traffic_ops/v5-client/cdnfederations.go b/traffic_ops/v5-client/cdnfederations.go
index dcd6bfcb66..c574af79eb 100644
--- a/traffic_ops/v5-client/cdnfederations.go
+++ b/traffic_ops/v5-client/cdnfederations.go
@@ -30,8 +30,8 @@ import (
 
 // CreateCDNFederation creates the given Federation in the CDN with the given
 // name.
-func (to *Session) CreateCDNFederation(f tc.CDNFederation, cdnName string, opts RequestOptions) (tc.CreateCDNFederationResponse, toclientlib.ReqInf, error) {
-	var data tc.CreateCDNFederationResponse
+func (to *Session) CreateCDNFederation(f tc.CDNFederationV5, cdnName string, opts RequestOptions) (tc.CDNFederationV5Response, toclientlib.ReqInf, error) {
+	var data tc.CDNFederationV5Response
 	route := "/cdns/" + url.PathEscape(cdnName) + "/federations"
 	inf, err := to.post(route, opts, f, &data)
 	return data, inf, err
@@ -39,8 +39,8 @@ func (to *Session) CreateCDNFederation(f tc.CDNFederation, cdnName string, opts
 
 // GetCDNFederationsByName retrieves all Federations in the CDN with the given
 // name.
-func (to *Session) GetCDNFederationsByName(cdnName string, opts RequestOptions) (tc.CDNFederationResponse, toclientlib.ReqInf, error) {
-	var data tc.CDNFederationResponse
+func (to *Session) GetCDNFederations(cdnName string, opts RequestOptions) (tc.CDNFederationsV5Response, toclientlib.ReqInf, error) {
+	var data tc.CDNFederationsV5Response
 	route := "/cdns/" + url.PathEscape(cdnName) + "/federations"
 	inf, err := to.get(route, opts, &data)
 	return data, inf, err
@@ -48,8 +48,8 @@ func (to *Session) GetCDNFederationsByName(cdnName string, opts RequestOptions)
 
 // UpdateCDNFederation replaces the Federation with the given ID in the CDN
 // with the given name with the provided Federation.
-func (to *Session) UpdateCDNFederation(f tc.CDNFederation, cdnName string, id int, opts RequestOptions) (tc.UpdateCDNFederationResponse, toclientlib.ReqInf, error) {
-	var data tc.UpdateCDNFederationResponse
+func (to *Session) UpdateCDNFederation(f tc.CDNFederationV5, cdnName string, id int, opts RequestOptions) (tc.CDNFederationV5Response, toclientlib.ReqInf, error) {
+	var data tc.CDNFederationV5Response
 	route := fmt.Sprintf("/cdns/%s/federations/%d", url.PathEscape(cdnName), id)
 	inf, err := to.put(route, opts, f, &data)
 	return data, inf, err
@@ -57,8 +57,8 @@ func (to *Session) UpdateCDNFederation(f tc.CDNFederation, cdnName string, id in
 
 // DeleteCDNFederation deletes the Federation with the given ID in the CDN
 // with the given name.
-func (to *Session) DeleteCDNFederation(cdnName string, id int, opts RequestOptions) (tc.DeleteCDNFederationResponse, toclientlib.ReqInf, error) {
-	var data tc.DeleteCDNFederationResponse
+func (to *Session) DeleteCDNFederation(cdnName string, id int, opts RequestOptions) (tc.CDNFederationV5Response, toclientlib.ReqInf, error) {
+	var data tc.CDNFederationV5Response
 	route := fmt.Sprintf("/cdns/%s/federations/%d", url.PathEscape(cdnName), id)
 	inf, err := to.del(route, opts, &data)
 	return data, inf, err