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

shamrick 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 2a43682512 Cachegroups rfc3339 apiv5 (#7605)
2a43682512 is described below

commit 2a43682512eaf0004b5ff8581fff0b6293f96b92
Author: Kurtis Michie <[email protected]>
AuthorDate: Tue Aug 1 13:14:19 2023 -0600

    Cachegroups rfc3339 apiv5 (#7605)
    
    * Changed non-rfc-datetime to rfc:3339 date formats for v5
    
    * Added V5 rfc:339 structs to cachegroup.go and updated corresponding V5 
test file to test
    
    * Updated V5 traffic_control_test.go to test updated V5 cachegroup api
    
    * Changed V5 Client and its subsidiaries to use new V5 api
    
    * Changed routes.go V5 handlers to use new API functions in cachegroups.go
    
    * Created new API functions for V5
    
    * Applied changes for merge conflict in db_helpers.go
    
    * Applied changes for merge conflict in db_helpers_test.go
    
    * Arranged imports
    
    * Rolled back "Date: ..." It is not needed
    
    * Changelog addition
    
    * Corrected insert statement for CreateCacheGroup function
    
    * Implemented additional checks and missing value populators for V5 Create, 
Update, and Delete API functions
    
    * Updated V5 Get call to be appropriate.  Code not done yet.
    
    * Debugging and fixing test failures
    
    * Fixed api test failures. Ready for review
    
    * Fixed missing bracket from merge
    
    * Resolved PR change requests
    
    * Resolved PR change requests
---
 CHANGELOG.md                                       |   1 +
 docs/source/api/v5/cachegroups.rst                 |  10 +-
 docs/source/api/v5/cachegroups_id.rst              |   6 +-
 lib/go-tc/cachegroups.go                           | 108 +++-
 traffic_ops/testing/api/v5/cachegroups_test.go     |  20 +-
 traffic_ops/testing/api/v5/cdn_locks_test.go       |   2 +-
 traffic_ops/testing/api/v5/traffic_control_test.go |   2 +-
 .../traffic_ops_golang/cachegroup/cachegroups.go   | 548 ++++++++++++++++++++-
 .../traffic_ops_golang/dbhelpers/db_helpers.go     |  57 +++
 .../dbhelpers/db_helpers_test.go                   |  51 ++
 traffic_ops/traffic_ops_golang/routing/routes.go   |   8 +-
 traffic_ops/v5-client/cachegroup.go                |  12 +-
 12 files changed, 792 insertions(+), 33 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index b8f9a07712..48d38885cd 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -66,6 +66,7 @@ The format is based on [Keep a 
Changelog](http://keepachangelog.com/en/1.0.0/).
 - [#7469](https://github.com/apache/trafficcontrol/pull/7469) *Traffic Ops* 
Changed logic to not report empty or missing cookies into TO error.log.
 - [#7586](https://github.com/apache/trafficcontrol/pull/7586) *Traffic Ops* 
Add permission to Operations Role to read from dnsseckeys endpoint.
 - [#7600](https://github.com/apache/trafficcontrol/pull/7600) *t3c* changed 
default go-direct command line arg to be old to avoid unexpected config changes 
upon upgrade.
+- [##7605](https://github.com/apache/trafficcontrol/pull/#7605) *Traffic Ops* 
Fixes `cachegroups_request_comments` v5 apis to respond with `RFC3339` 
date/time Format.
 - [#7621](https://github.com/apache/trafficcontrol/pull/7621) *Traffic Ops* 
Use ID token for OAuth authentication, not Access Token
 
 ### Fixed
diff --git a/docs/source/api/v5/cachegroups.rst 
b/docs/source/api/v5/cachegroups.rst
index 5c56b51f74..24e113a333 100644
--- a/docs/source/api/v5/cachegroups.rst
+++ b/docs/source/api/v5/cachegroups.rst
@@ -71,7 +71,7 @@ Response Structure
 :fallbacks:                     An array of strings that are :ref:`Cache Group 
names <cache-group-name>` that are registered as :ref:`cache-group-fallbacks` 
for this :term:`Cache Group`\ [#fallbacks]_
 :fallbackToClosest:             A boolean value that defines the 
:ref:`cache-group-fallback-to-closest` behavior of this :term:`Cache Group`\ 
[#fallbacks]_
 :id:                            An integer that is the :ref:`cache-group-id` 
of the :term:`Cache Group`
-:lastUpdated:                   The time and date at which this entry was last 
updated in :ref:`non-rfc-datetime`
+:lastUpdated:                   The time and date at which this entry was last 
updated in :rfc:`3339`
 :latitude:                      A floating-point :ref:`cache-group-latitude` 
for the :term:`Cache Group`
 :localizationMethods:           An array of 
:ref:`cache-group-localization-methods` as strings
 :longitude:                     A floating-point :ref:`cache-group-longitude` 
for the :term:`Cache Group`
@@ -116,7 +116,7 @@ Response Structure
                        "localizationMethods": [],
                        "typeName": "EDGE_LOC",
                        "typeId": 23,
-                       "lastUpdated": "2018-11-07 14:45:43+00",
+                       "lastUpdated": "2023-05-30T19:52:58.183642+00:00",
                        "fallbacks": []
                }
        ]}
@@ -182,7 +182,7 @@ Response Structure
 :fallbacks:                     An array of strings that are :ref:`Cache Group 
names <cache-group-name>` that are registered as :ref:`cache-group-fallbacks` 
for this :term:`Cache Group`\ [#fallbacks]_
 :fallbackToClosest:             A boolean value that defines the 
:ref:`cache-group-fallback-to-closest` behavior of this :term:`Cache Group`\ 
[#fallbacks]_
 :id:                            An integer that is the :ref:`cache-group-id` 
of the :term:`Cache Group`
-:lastUpdated:                   The time and date at which this entry was last 
updated in :ref:`non-rfc-datetime`
+:lastUpdated:                   The time and date at which this entry was last 
updated in :rfc:`3339`
 :latitude:                      A floating-point :ref:`cache-group-latitude` 
for the :term:`Cache Group`
 :localizationMethods:           An array of 
:ref:`cache-group-localization-methods` as strings
 :longitude:                     A floating-point :ref:`cache-group-longitude` 
for the :term:`Cache Group`
@@ -207,7 +207,7 @@ Response Structure
        Set-Cookie: mojolicious=...; Path=/; Expires=Mon, 18 Nov 2019 17:40:54 
GMT; Max-Age=3600; HttpOnly
        Whole-Content-Sha512: 
YvZlh3rpfl3nBq6SbNVhbkt3IvckbB9amqGW2JhLxWK9K3cxjBq5J2sIHBUhrLKUhE9afpxtvaYrLRxjt1/YMQ==
        X-Server-Name: traffic_ops_golang/
-       Date: Wed, 07 Nov 2018 22:11:50 GMT
+       Date: Wed, 07 Nov 2018 19:46:36 GMT
        Content-Length: 379
 
        { "alerts": [
@@ -234,7 +234,7 @@ Response Structure
                ],
                "typeName": "EDGE_LOC",
                "typeId": 23,
-               "lastUpdated": "2019-12-02 22:21:08+00",
+               "lastUpdated": "2023-05-30T19:52:58.183642+00:00",
                "fallbacks": []
        }}
 
diff --git a/docs/source/api/v5/cachegroups_id.rst 
b/docs/source/api/v5/cachegroups_id.rst
index eb14b0df63..3f7cdcb78d 100644
--- a/docs/source/api/v5/cachegroups_id.rst
+++ b/docs/source/api/v5/cachegroups_id.rst
@@ -84,7 +84,7 @@ Response Structure
 :fallbacks:                     An array of strings that are :ref:`Cache Group 
names <cache-group-name>` that are registered as :ref:`cache-group-fallbacks` 
for this :term:`Cache Group`\ [#fallbacks]_
 :fallbackToClosest:             A boolean value that defines the 
:ref:`cache-group-fallback-to-closest` behavior of this :term:`Cache Group`\ 
[#fallbacks]_
 :id:                            An integer that is the :ref:`cache-group-id` 
of the :term:`Cache Group`
-:lastUpdated:                   The time and date at which this entry was last 
updated in :ref:`non-rfc-datetime`
+:lastUpdated:                   The time and date at which this entry was last 
updated in :rfc:`3339`
 :latitude:                      A floating-point :ref:`cache-group-latitude` 
for the :term:`Cache Group`
 :localizationMethods:           An array of 
:ref:`cache-group-localization-methods` as strings
 :longitude:                     A floating-point :ref:`cache-group-longitude` 
for the :term:`Cache Group`
@@ -135,7 +135,7 @@ Response Structure
                ],
                "typeName": "EDGE_LOC",
                "typeId": 23,
-               "lastUpdated": "2018-11-14 19:14:28+00"
+               "lastUpdated": "2023-05-30T19:52:58.183642+00:00"
        }}
 
 
@@ -181,7 +181,7 @@ Response Structure
        Set-Cookie: mojolicious=...; Path=/; Expires=Mon, 18 Nov 2019 17:40:54 
GMT; Max-Age=3600; HttpOnly
        Whole-Content-Sha512: 
5jZBgO7h1eNF70J/cmlbi3Hf9KJPx+WLMblH/pSKF3FWb/10GUHIN35ZOB+lN5LZYCkmk3izGbTFkiruG8I41Q==
        X-Server-Name: traffic_ops_golang/
-       Date: Wed, 14 Nov 2018 20:31:04 GMT
+       Date: Wed, 14 Nov 2018 19:14:28 GMT
        Content-Length: 57
 
        { "alerts": [
diff --git a/lib/go-tc/cachegroups.go b/lib/go-tc/cachegroups.go
index 81b05ee94e..baf01a57f2 100644
--- a/lib/go-tc/cachegroups.go
+++ b/lib/go-tc/cachegroups.go
@@ -19,7 +19,11 @@ package tc
  * under the License.
  */
 
-import "github.com/apache/trafficcontrol/lib/go-util"
+import (
+       "time"
+
+       "github.com/apache/trafficcontrol/lib/go-util"
+)
 
 // CacheGroupsResponse is a list of CacheGroups as a response.
 type CacheGroupsResponse struct {
@@ -93,3 +97,105 @@ type CachegroupQueueUpdatesRequest struct {
        CDN    *CDNName         `json:"cdn"`
        CDNID  *util.JSONIntStr `json:"cdnId"`
 }
+
+// CacheGroupsResponseV50 is a list of CacheGroups as a response.
+type CacheGroupsResponseV50 struct {
+       Response []CacheGroupV50 `json:"response"`
+       Alerts
+}
+
+// CacheGroupsNullableResponseV50 is a response with a list of 
CacheGroupNullables.
+// Traffic Ops API responses instead uses an interface hold a list of
+// TOCacheGroups.
+type CacheGroupsNullableResponseV50 struct {
+       Response []CacheGroupNullableV50 `json:"response"`
+       Alerts
+}
+
+// CacheGroupDetailResponseV50 is the JSON object returned for a single Cache 
Group.
+type CacheGroupDetailResponseV50 struct {
+       Response CacheGroupNullableV50 `json:"response"`
+       Alerts
+}
+
+// CacheGroupV50 contains information about a given cache group in Traffic Ops.
+type CacheGroupV50 struct {
+       ID                          int                  `json:"id" db:"id"`
+       Name                        string               `json:"name" db:"name"`
+       ShortName                   string               `json:"shortName" 
db:"short_name"`
+       Latitude                    float64              `json:"latitude" 
db:"latitude"`
+       Longitude                   float64              `json:"longitude" 
db:"longitude"`
+       ParentName                  string               
`json:"parentCachegroupName" db:"parent_cachegroup_name"`
+       ParentCachegroupID          int                  
`json:"parentCachegroupId" db:"parent_cachegroup_id"`
+       SecondaryParentName         string               
`json:"secondaryParentCachegroupName" db:"secondary_parent_cachegroup_name"`
+       SecondaryParentCachegroupID int                  
`json:"secondaryParentCachegroupId" db:"secondary_parent_cachegroup_id"`
+       FallbackToClosest           bool                 
`json:"fallbackToClosest" db:"fallback_to_closest"`
+       LocalizationMethods         []LocalizationMethod 
`json:"localizationMethods" db:"localization_methods"`
+       Type                        string               `json:"typeName" 
db:"type_name"` // aliased to type_name to disambiguate struct scans due to 
join on 'type' table
+       TypeID                      int                  `json:"typeId" 
db:"type_id"`     // aliased to type_id to disambiguate struct scans due join 
on 'type' table
+       LastUpdated                 time.Time            `json:"lastUpdated" 
db:"last_updated"`
+       Fallbacks                   []string             `json:"fallbacks" 
db:"fallbacks"`
+}
+
+// CacheGroupNullableV50 contains information about a given cache group in 
Traffic Ops.
+// Unlike CacheGroup, CacheGroupNullable's fields are nullable.
+type CacheGroupNullableV50 struct {
+       ID                          *int                  `json:"id" db:"id"`
+       Name                        *string               `json:"name" 
db:"name"`
+       ShortName                   *string               `json:"shortName" 
db:"short_name"`
+       Latitude                    *float64              `json:"latitude" 
db:"latitude"`
+       Longitude                   *float64              `json:"longitude" 
db:"longitude"`
+       ParentName                  *string               
`json:"parentCachegroupName" db:"parent_cachegroup_name"`
+       ParentCachegroupID          *int                  
`json:"parentCachegroupId" db:"parent_cachegroup_id"`
+       SecondaryParentName         *string               
`json:"secondaryParentCachegroupName" db:"secondary_parent_cachegroup_name"`
+       SecondaryParentCachegroupID *int                  
`json:"secondaryParentCachegroupId" db:"secondary_parent_cachegroup_id"`
+       FallbackToClosest           *bool                 
`json:"fallbackToClosest" db:"fallback_to_closest"`
+       LocalizationMethods         *[]LocalizationMethod 
`json:"localizationMethods" db:"localization_methods"`
+       Type                        *string               `json:"typeName" 
db:"type_name"` // aliased to type_name to disambiguate struct scans due to 
join on 'type' table
+       TypeID                      *int                  `json:"typeId" 
db:"type_id"`     // aliased to type_id to disambiguate struct scans due join 
on 'type' table
+       LastUpdated                 *time.Time            `json:"lastUpdated" 
db:"last_updated"`
+       Fallbacks                   *[]string             `json:"fallbacks" 
db:"fallbacks"`
+}
+
+// CachegroupTrimmedNameV50 is useful when the only info about a cache group 
you
+// want to return is its name.
+type CachegroupTrimmedNameV50 struct {
+       Name string `json:"name"`
+}
+
+// CachegroupQueueUpdatesRequestV50 holds info relating to the
+// cachegroups/{{ID}}/queue_update TO route.
+type CachegroupQueueUpdatesRequestV50 struct {
+       Action string           `json:"action"`
+       CDN    *CDNName         `json:"cdn"`
+       CDNID  *util.JSONIntStr `json:"cdnId"`
+}
+
+// CacheGroupsResponseV5 is the type of response from the cachegroups
+// Traffic Ops endpoint.
+// It always points to the type for the latest minor version of 
CacheGroupsResponseV5x APIv5.
+type CacheGroupsResponseV5 = CacheGroupsResponseV50
+
+// CacheGroupsNullableResponseV5 is the type of response from the cachegroups
+// Traffic Ops endpoint.
+// It always points to the type for the latest minor version of 
CacheGroupsNullableResponseV5x APIv5.
+type CacheGroupsNullableResponseV5 = CacheGroupsNullableResponseV50
+
+// CacheGroupDetailResponseV5 is the type of response from the cachegroups
+// Traffic Ops endpoint.
+// It always points to the type for the latest minor version of 
CacheGroupDetailResponseV5x APIv5.
+type CacheGroupDetailResponseV5 = CacheGroupDetailResponseV50
+
+// CacheGroupV5 always points to the type for the latest minor version of 
CacheGroupV5x APIv5.
+type CacheGroupV5 = CacheGroupV50
+
+// CacheGroupNullableV5 always points to the type for the latest minor version 
of CacheGroupNullableV5x APIv5.
+type CacheGroupNullableV5 = CacheGroupNullableV50
+
+// CachegroupTrimmedNameV5 always points to the type for the latest minor 
version of CachegroupTrimmedNameV5x APIv5.
+type CachegroupTrimmedNameV5 = CachegroupTrimmedNameV50
+
+// CachegroupQueueUpdatesRequestV5 is the type of request to the cachegroups
+// Traffic Ops endpoint.
+// It always points to the type for the latest minor version of 
CachegroupQueueUpdatesRequestV5x APIv5.
+type CachegroupQueueUpdatesRequestV5 = CachegroupQueueUpdatesRequestV50
diff --git a/traffic_ops/testing/api/v5/cachegroups_test.go 
b/traffic_ops/testing/api/v5/cachegroups_test.go
index f7a8657401..a4a8b18676 100644
--- a/traffic_ops/testing/api/v5/cachegroups_test.go
+++ b/traffic_ops/testing/api/v5/cachegroups_test.go
@@ -38,7 +38,7 @@ func TestCacheGroups(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.CacheGroupNullable]{
+               methodTests := utils.TestCase[client.Session, 
client.RequestOptions, tc.CacheGroupNullableV5]{
                        "GET": {
                                "OK when VALID NAME parameter AND Lat/Long are 
0": {
                                        ClientSession: TOSession,
@@ -152,7 +152,7 @@ func TestCacheGroups(t *testing.T) {
                        "PUT": {
                                "OK when VALID request": {
                                        EndpointID: GetCacheGroupId(t, 
"cachegroup1"), ClientSession: TOSession,
-                                       RequestBody: tc.CacheGroupNullable{
+                                       RequestBody: tc.CacheGroupNullableV5{
                                                Latitude:            
util.Ptr(17.5),
                                                Longitude:           
util.Ptr(17.5),
                                                Name:                
util.Ptr("cachegroup1"),
@@ -166,7 +166,7 @@ func TestCacheGroups(t *testing.T) {
                                },
                                "OK when updating CG with null Lat/Long": {
                                        EndpointID: GetCacheGroupId(t, 
"nullLatLongCG"), ClientSession: TOSession,
-                                       RequestBody: tc.CacheGroupNullable{
+                                       RequestBody: tc.CacheGroupNullableV5{
                                                Name:      
util.Ptr("nullLatLongCG"),
                                                ShortName: util.Ptr("null-ll"),
                                                Type:      util.Ptr("EDGE_LOC"),
@@ -177,7 +177,7 @@ func TestCacheGroups(t *testing.T) {
                                },
                                "BAD REQUEST when updating TYPE of CG in 
TOPOLOGY": {
                                        EndpointID: GetCacheGroupId(t, 
"topology-edge-cg-01"), ClientSession: TOSession,
-                                       RequestBody: tc.CacheGroupNullable{
+                                       RequestBody: tc.CacheGroupNullableV5{
                                                Latitude:  util.Ptr(0.0),
                                                Longitude: util.Ptr(0.0),
                                                Name:      
util.Ptr("topology-edge-cg-01"),
@@ -190,7 +190,7 @@ func TestCacheGroups(t *testing.T) {
                                "PRECONDITION FAILED when updating with IMS & 
IUS Headers": {
                                        EndpointID: GetCacheGroupId(t, 
"cachegroup1"), ClientSession: TOSession,
                                        RequestOpts: 
client.RequestOptions{Header: http.Header{rfc.IfUnmodifiedSince: 
{currentTimeRFC}}},
-                                       RequestBody: tc.CacheGroupNullable{
+                                       RequestBody: tc.CacheGroupNullableV5{
                                                Name:      
util.Ptr("cachegroup1"),
                                                ShortName: 
util.Ptr("changeName"),
                                                Type:      util.Ptr("EDGE_LOC"),
@@ -201,7 +201,7 @@ func TestCacheGroups(t *testing.T) {
                                "PRECONDITION FAILED when updating with IFMATCH 
ETAG Header": {
                                        EndpointID: GetCacheGroupId(t, 
"cachegroup1"), ClientSession: TOSession,
                                        RequestOpts: 
client.RequestOptions{Header: http.Header{rfc.IfMatch: 
{rfc.ETag(currentTime)}}},
-                                       RequestBody: tc.CacheGroupNullable{
+                                       RequestBody: tc.CacheGroupNullableV5{
                                                Name:      
util.Ptr("cachegroup1"),
                                                ShortName: 
util.Ptr("changeName"),
                                                Type:      util.Ptr("EDGE_LOC"),
@@ -270,7 +270,7 @@ func TestCacheGroups(t *testing.T) {
 
 func ValidateExpectedField(field string, expected string) utils.CkReqFunc {
        return func(t *testing.T, _ toclientlib.ReqInf, resp interface{}, _ 
tc.Alerts, _ error) {
-               cgResp := resp.([]tc.CacheGroupNullable)
+               cgResp := resp.([]tc.CacheGroupNullableV5)
                cg := cgResp[0]
                switch field {
                case "Name":
@@ -287,7 +287,7 @@ func ValidateExpectedField(field string, expected string) 
utils.CkReqFunc {
 
 func ValidateResponseFields() utils.CkReqFunc {
        return func(t *testing.T, _ toclientlib.ReqInf, resp interface{}, _ 
tc.Alerts, _ error) {
-               cgResp := resp.([]tc.CacheGroupNullable)
+               cgResp := resp.([]tc.CacheGroupNullableV5)
                cg := cgResp[0]
                assert.NotNil(t, cg.ID, "Expected response id to not be nil")
                assert.NotNil(t, cg.Latitude, "Expected latitude to not be nil")
@@ -299,7 +299,7 @@ func ValidateResponseFields() utils.CkReqFunc {
 
 func ValidatePagination(paginationParam string) utils.CkReqFunc {
        return func(t *testing.T, _ toclientlib.ReqInf, resp interface{}, _ 
tc.Alerts, _ error) {
-               paginationResp := resp.([]tc.CacheGroupNullable)
+               paginationResp := resp.([]tc.CacheGroupNullableV5)
 
                opts := client.NewRequestOptions()
                opts.QueryParameters.Set("orderby", "id")
@@ -370,7 +370,7 @@ func CreateTestCacheGroups(t *testing.T) {
 }
 
 func DeleteTestCacheGroups(t *testing.T) {
-       var parentlessCacheGroups []tc.CacheGroupNullable
+       var parentlessCacheGroups []tc.CacheGroupNullableV5
        opts := client.NewRequestOptions()
 
        // delete the edge caches.
diff --git a/traffic_ops/testing/api/v5/cdn_locks_test.go 
b/traffic_ops/testing/api/v5/cdn_locks_test.go
index 1fd951aceb..583d22cb41 100644
--- a/traffic_ops/testing/api/v5/cdn_locks_test.go
+++ b/traffic_ops/testing/api/v5/cdn_locks_test.go
@@ -568,7 +568,7 @@ func TestCDNLocks(t *testing.T) {
                                                        }
                                                },
                                                "CACHE GROUP UPDATE": func(t 
*testing.T) {
-                                                       cacheGroup := 
tc.CacheGroupNullable{}
+                                                       cacheGroup := 
tc.CacheGroupNullableV5{}
                                                        err = 
json.Unmarshal(dat, &cacheGroup)
                                                        assert.NoError(t, err, 
"Error occurred when unmarshalling request body: %v", err)
                                                        resp, reqInf, err := 
testCase.ClientSession.UpdateCacheGroup(testCase.EndpointID(), cacheGroup, 
testCase.RequestOpts)
diff --git a/traffic_ops/testing/api/v5/traffic_control_test.go 
b/traffic_ops/testing/api/v5/traffic_control_test.go
index d26b9c2e64..1ed3ad0b05 100644
--- a/traffic_ops/testing/api/v5/traffic_control_test.go
+++ b/traffic_ops/testing/api/v5/traffic_control_test.go
@@ -24,7 +24,7 @@ type TrafficControl struct {
        ASNs                                              []tc.ASNV5            
                  `json:"asns"`
        CDNs                                              []tc.CDNV5            
                  `json:"cdns"`
        CDNLocks                                          []tc.CDNLock          
                  `json:"cdnlocks"`
-       CacheGroups                                       
[]tc.CacheGroupNullable                 `json:"cachegroups"`
+       CacheGroups                                       
[]tc.CacheGroupNullableV5               `json:"cachegroups"`
        Capabilities                                      []tc.Capability       
                  `json:"capability"`
        Coordinates                                       []tc.CoordinateV5     
                  `json:"coordinates"`
        DeliveryServicesRegexes                           
[]tc.DeliveryServiceRegexesTest         `json:"deliveryServicesRegexes"`
diff --git a/traffic_ops/traffic_ops_golang/cachegroup/cachegroups.go 
b/traffic_ops/traffic_ops_golang/cachegroup/cachegroups.go
index ec2484c1ce..f1d62d45b1 100644
--- a/traffic_ops/traffic_ops_golang/cachegroup/cachegroups.go
+++ b/traffic_ops/traffic_ops_golang/cachegroup/cachegroups.go
@@ -21,6 +21,7 @@ package cachegroup
 
 import (
        "database/sql"
+       "encoding/json"
        "errors"
        "fmt"
        "net/http"
@@ -28,14 +29,13 @@ import (
        "strings"
        "time"
 
-       
"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/util/ims"
-
        "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/util/ims"
 
        validation "github.com/go-ozzo/ozzo-validation"
        "github.com/jmoiron/sqlx"
@@ -47,6 +47,64 @@ type TOCacheGroup struct {
        tc.CacheGroupNullable
 }
 
+type TOCacheGroupV5 struct {
+       api.APIInfoImpl `json:"-"`
+       tc.CacheGroupNullableV5
+}
+
+// Downgrade will convert an instance of CacheGroupNullableV5 to 
CacheGroupNullable.
+// Note that this function does a shallow copy of the requested and original 
Cache Group structures.
+func Downgrade(cgV5 tc.CacheGroupNullableV5) TOCacheGroup {
+       var cg TOCacheGroup
+       cg.ID = util.CopyIfNotNil(cgV5.ID)
+       cg.Name = util.CopyIfNotNil(cgV5.Name)
+       cg.ShortName = util.CopyIfNotNil(cgV5.ShortName)
+       cg.Latitude = util.CopyIfNotNil(cgV5.Latitude)
+       cg.Longitude = util.CopyIfNotNil(cgV5.Longitude)
+       cg.ParentName = util.CopyIfNotNil(cgV5.ParentName)
+       cg.ParentCachegroupID = util.CopyIfNotNil(cgV5.ParentCachegroupID)
+       cg.SecondaryParentName = util.CopyIfNotNil(cgV5.SecondaryParentName)
+       cg.SecondaryParentCachegroupID = 
util.CopyIfNotNil(cgV5.SecondaryParentCachegroupID)
+       cg.FallbackToClosest = util.CopyIfNotNil(cgV5.FallbackToClosest)
+       cg.LocalizationMethods = util.CopyIfNotNil(cgV5.LocalizationMethods)
+       cg.Type = util.CopyIfNotNil(cgV5.Type)
+       cg.TypeID = util.CopyIfNotNil(cgV5.TypeID)
+       if cgV5.LastUpdated != nil {
+               cg.LastUpdated = tc.TimeNoModFromTime(*cgV5.LastUpdated)
+       }
+       cg.Fallbacks = util.CopyIfNotNil(cgV5.Fallbacks)
+       return cg
+}
+
+// Upgrade will convert an instance of CacheGroupNullable to 
CacheGroupNullableV5.
+// Note that this function does a shallow copy of the requested and original 
Cache Group structures.
+func (cg TOCacheGroup) Upgrade() (tc.CacheGroupNullableV5, error) {
+       var cgV5 tc.CacheGroupNullableV5
+       cgV5.ID = util.CopyIfNotNil(cg.ID)
+       cgV5.Name = util.CopyIfNotNil(cg.Name)
+       cgV5.ShortName = util.CopyIfNotNil(cg.ShortName)
+       cgV5.Latitude = util.CopyIfNotNil(cg.Latitude)
+       cgV5.Longitude = util.CopyIfNotNil(cg.Longitude)
+       cgV5.ParentName = util.CopyIfNotNil(cg.ParentName)
+       cgV5.ParentCachegroupID = util.CopyIfNotNil(cg.ParentCachegroupID)
+       cgV5.SecondaryParentName = util.CopyIfNotNil(cg.SecondaryParentName)
+       cgV5.SecondaryParentCachegroupID = 
util.CopyIfNotNil(cg.SecondaryParentCachegroupID)
+       cgV5.FallbackToClosest = util.CopyIfNotNil(cg.FallbackToClosest)
+       cgV5.LocalizationMethods = util.CopyIfNotNil(cg.LocalizationMethods)
+       cgV5.Type = util.CopyIfNotNil(cg.Type)
+       cgV5.TypeID = util.CopyIfNotNil(cg.TypeID)
+       if cg.LastUpdated != nil {
+               cgV5.LastUpdated = &cg.LastUpdated.Time
+               t, err := util.ConvertTimeFormat(*cgV5.LastUpdated, 
time.RFC3339)
+               if err != nil {
+                       return cgV5, err
+               }
+               cgV5.LastUpdated = t
+       }
+       cgV5.Fallbacks = util.CopyIfNotNil(cg.Fallbacks)
+       return cgV5, nil
+}
+
 func (cg TOCacheGroup) GetKeyFieldsInfo() []api.KeyFieldInfo {
        return []api.KeyFieldInfo{{Field: "id", Func: api.GetIntKey}}
 }
@@ -897,3 +955,489 @@ last_updated`
 func DeleteQuery() string {
        return `DELETE FROM cachegroup WHERE id=$1`
 }
+
+// GetCacheGroup [Version : V5] function Process the *http.Request and writes 
the response. It uses getCacheGroup function.
+func GetCacheGroup(w http.ResponseWriter, r *http.Request) {
+       inf, userErr, sysErr, errCode := api.NewInfo(r, nil, nil)
+       if userErr != nil || sysErr != nil {
+               api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr)
+               return
+       }
+       defer inf.Close()
+
+       code := http.StatusOK
+       useIMS := false
+       config, e := api.GetConfig(r.Context())
+       if e == nil && config != nil {
+               useIMS = config.UseIMS
+       } else {
+               log.Warnf("Couldn't get config %v", e)
+       }
+
+       var maxTime time.Time
+       var usrErr error
+       var syErr error
+
+       var cgList []interface{}
+
+       tx := inf.Tx
+
+       cgList, maxTime, code, usrErr, syErr = getCacheGroup(tx, inf.Params, 
useIMS, r.Header)
+       if code == http.StatusNotModified {
+               w.WriteHeader(code)
+               api.WriteResp(w, r, []tc.CacheGroupV5{})
+               return
+       }
+
+       if code == http.StatusBadRequest {
+               api.HandleErr(w, r, inf.Tx.Tx, http.StatusBadRequest, usrErr, 
nil)
+               return
+       }
+
+       if sysErr != nil {
+               api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, 
nil, syErr)
+               return
+       }
+
+       if maxTime != (time.Time{}) && api.SetLastModifiedHeader(r, useIMS) {
+               api.AddLastModifiedHdr(w, maxTime)
+       }
+
+       api.WriteResp(w, r, cgList)
+}
+
+func getCacheGroup(tx *sqlx.Tx, params map[string]string, useIMS bool, header 
http.Header) ([]interface{}, time.Time, int, error, error) {
+       //func getCacheGroup(tx *sqlx.Tx, params map[string]string, useIMS 
bool, header http.Header) ([]tc.CacheGroupV5, time.Time, int, error, error) {
+       var runSecond bool
+       var maxTime time.Time
+       cgList := []interface{}{}
+
+       // Query Parameters to Database Query column mappings
+       queryParamsToQueryCols := map[string]dbhelpers.WhereColumnInfo{
+               "id":        {Column: "cachegroup.id", Checker: api.IsInt},
+               "name":      {Column: "cachegroup.name"},
+               "shortName": {Column: "cachegroup.short_name"},
+               "type":      {Column: "cachegroup.type"},
+               "topology":  {Column: "topology_cachegroup.topology"},
+       }
+       if _, ok := params["orderby"]; !ok {
+               params["orderby"] = "name"
+       }
+       where, orderBy, pagination, queryValues, errs := 
dbhelpers.BuildWhereAndOrderByAndPagination(params, queryParamsToQueryCols)
+       if len(errs) > 0 {
+               return nil, time.Time{}, http.StatusBadRequest, 
util.JoinErrs(errs), nil
+       }
+
+       if useIMS {
+               runSecond, maxTime = ims.TryIfModifiedSinceQuery(tx, header, 
queryValues, selectMaxLastUpdatedQuery(where))
+               if !runSecond {
+                       log.Debugln("IMS HIT")
+                       return cgList, maxTime, http.StatusNotModified, nil, nil
+               }
+               log.Debugln("IMS MISS")
+       } else {
+               log.Debugln("Non IMS request")
+       }
+       baseSelect := SelectQuery()
+       if _, ok := params["topology"]; ok {
+               baseSelect += `
+               LEFT JOIN topology_cachegroup ON cachegroup.name = 
topology_cachegroup.cachegroup
+               `
+       }
+       // If the type cannot be converted to an int, return 400
+       if cgType, ok := params["type"]; ok {
+               _, err := strconv.Atoi(cgType)
+               if err != nil {
+                       return nil, time.Time{}, http.StatusBadRequest, nil, 
fmt.Errorf("cachegroup read: converting cachegroup type to integer " + 
err.Error())
+               }
+       }
+
+       query := baseSelect + where + orderBy + pagination
+       rows, err := tx.NamedQuery(query, queryValues)
+       if err != nil {
+               return nil, time.Time{}, http.StatusInternalServerError, nil, 
err
+       }
+       defer rows.Close()
+
+       for rows.Next() {
+               var cg TOCacheGroupV5
+               lms := make([]tc.LocalizationMethod, 0)
+               cgfs := make([]string, 0)
+               if err = rows.Scan(
+                       &cg.ID,
+                       &cg.Name,
+                       &cg.ShortName,
+                       &cg.Latitude,
+                       &cg.Longitude,
+                       pq.Array(&lms),
+                       &cg.ParentCachegroupID,
+                       &cg.ParentName,
+                       &cg.SecondaryParentCachegroupID,
+                       &cg.SecondaryParentName,
+                       &cg.Type,
+                       &cg.TypeID,
+                       &cg.LastUpdated,
+                       pq.Array(&cgfs),
+                       &cg.FallbackToClosest,
+               ); err != nil {
+                       return nil, time.Time{}, 
http.StatusInternalServerError, nil, err
+               }
+               cg.LocalizationMethods = &lms
+               cg.Fallbacks = &cgfs
+               cgList = append(cgList, cg)
+       }
+
+       return cgList, maxTime, http.StatusOK, nil, nil
+}
+
+// CreateCacheGroup [Version : V5] function creates the cache group with the 
passed name.
+func CreateCacheGroup(w http.ResponseWriter, r *http.Request) {
+       inf, userErr, sysErr, errCode := api.NewInfo(r, nil, nil)
+       if userErr != nil || sysErr != nil {
+               api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr)
+               return
+       }
+       defer inf.Close()
+       tx := inf.Tx.Tx
+
+       cg, readValErr := readAndValidateJsonStruct(r)
+       if readValErr != nil {
+               api.HandleErr(w, r, tx, http.StatusBadRequest, readValErr, nil)
+               return
+       }
+
+       // check if cache group already exists
+       var exists bool
+       err := tx.QueryRow(`SELECT EXISTS(SELECT * from cachegroup where name = 
$1)`, cg.Name).Scan(&exists)
+       if err != nil {
+               api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, 
fmt.Errorf("error: %w, when checking if cache group with name %s exists", err, 
*cg.Name))
+               return
+       }
+       if exists {
+               api.HandleErr(w, r, tx, http.StatusBadRequest, 
fmt.Errorf("cache group name '%s' already exists.", *cg.Name), nil)
+               return
+       }
+
+       // create cache group
+       query := InsertQuery()
+
+       err = tx.QueryRow(
+               query,
+               cg.Name,
+               cg.ShortName,
+               cg.TypeID,
+               cg.ParentCachegroupID,
+               cg.SecondaryParentCachegroupID,
+               cg.FallbackToClosest,
+       ).Scan(
+               &cg.ID,
+               &cg.Type,
+               &cg.ParentName,
+               &cg.SecondaryParentName,
+       )
+       if err != nil {
+               if errors.Is(err, sql.ErrNoRows) {
+                       api.HandleErr(w, r, tx, http.StatusInternalServerError, 
fmt.Errorf("error: %w in creating cache group with name: %s", err, *cg.Name), 
nil)
+                       return
+               }
+               usrErr, sysErr, code := api.ParseDBError(err)
+               api.HandleErr(w, r, tx, code, usrErr, sysErr)
+               return
+       }
+
+       dgCg := Downgrade(cg)
+       dgCg.ReqInfo = inf
+       coordinateID, err := dgCg.createCoordinate()
+       if err != nil {
+               api.HandleErr(w, r, tx, http.StatusInternalServerError, 
fmt.Errorf("cachegroup create: creating coord: "+err.Error()), nil)
+               return
+       }
+
+       checkLastUpdated := `UPDATE cachegroup SET coordinate=$1 WHERE id=$2 
RETURNING last_updated`
+
+       err = tx.QueryRow(
+               checkLastUpdated,
+               coordinateID,
+               *cg.ID,
+       ).Scan(
+               &cg.LastUpdated,
+       )
+
+       if err != nil {
+               api.HandleErr(w, r, tx, http.StatusInternalServerError, 
fmt.Errorf("followup update during cachegroup create: %v", err), nil)
+               return
+       }
+
+       if err = dgCg.createLocalizationMethods(); err != nil {
+               api.HandleErr(w, r, tx, http.StatusInternalServerError, 
fmt.Errorf("creating cachegroup: creating localization methods: "+err.Error()), 
nil)
+               return
+       }
+
+       if err = dgCg.createCacheGroupFallbacks(); err != nil {
+               api.HandleErr(w, r, tx, http.StatusInternalServerError, 
fmt.Errorf("creating cachegroup: creating cache group fallbacks: 
"+err.Error()), nil)
+               return
+       }
+
+       cg, err = dgCg.Upgrade()
+       if err != nil {
+               api.HandleErr(w, r, tx, http.StatusInternalServerError, 
fmt.Errorf("converting cachegroup: converting cache group upgrade: 
"+err.Error()), nil)
+               return
+       }
+
+       alerts := tc.CreateAlerts(tc.SuccessLevel, "cache group was created.")
+       w.Header().Set("Location", 
fmt.Sprintf("/api/%d.%d/cachegroups?name=%s", inf.Version.Major, 
inf.Version.Minor, *cg.Name))
+       api.WriteAlertsObj(w, r, http.StatusCreated, alerts, cg)
+       return
+}
+
+// UpdateCacheGroup [Version : V5] function updates the name of the cache 
group passed.
+func UpdateCacheGroup(w http.ResponseWriter, r *http.Request) {
+       inf, userErr, sysErr, errCode := api.NewInfo(r, nil, nil)
+       if userErr != nil || sysErr != nil {
+               api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr)
+               return
+       }
+       defer inf.Close()
+       tx := inf.Tx.Tx
+
+       cg, readValErr := readAndValidateJsonStruct(r)
+       if readValErr != nil {
+               api.HandleErr(w, r, tx, http.StatusBadRequest, readValErr, nil)
+               return
+       }
+
+       ID := inf.Params["id"]
+       id, err := strconv.Atoi(ID)
+       if err != nil {
+               api.HandleErr(w, r, tx, http.StatusUnprocessableEntity, 
fmt.Errorf("update cachegroup: converted to type int: "+err.Error()), nil)
+               return
+       }
+
+       // check if the entity was already updated
+       userErr, sysErr, errCode = api.CheckIfUnModified(r.Header, inf.Tx, id, 
"cachegroup")
+       if userErr != nil || sysErr != nil {
+               api.HandleErr(w, r, tx, errCode, userErr, sysErr)
+               return
+       }
+
+       dgCg := Downgrade(cg)
+       dgCg.ReqInfo = inf
+
+       keyFields := dgCg.GetKeyFieldsInfo() //expecting a slice of the key 
fields info which is a struct with the field name and a function to convert a 
string into a {}interface of the right type. in most that will be 
[{Field:"id",Func: func(s string)({}interface,error){return strconv.Atoi(s)}}]
+       // ignoring ok value -- will be checked after param processing
+
+       keys := make(map[string]interface{}) // a map of keyField to keyValue 
where keyValue is an {}interface
+       for _, kf := range keyFields {
+               paramKey := inf.Params[kf.Field]
+               if paramKey == "" {
+                       api.HandleErr(w, r, inf.Tx.Tx, http.StatusBadRequest, 
errors.New("missing key: "+kf.Field), nil)
+                       return
+               }
+
+               paramValue, err := kf.Func(paramKey)
+               if err != nil {
+                       api.HandleErr(w, r, inf.Tx.Tx, http.StatusBadRequest, 
errors.New("failed to parse key: "+kf.Field), nil)
+                       return
+               }
+
+               if paramValue != "" {
+                       // if key's value provided in params,  overwrite it and 
ignore that provided in JSON
+                       keys[kf.Field] = paramValue
+               }
+       }
+
+       // check that all keys were properly filled in
+       dgCg.SetKeys(keys)
+       _, ok := dgCg.GetKeys()
+       if !ok {
+               api.HandleErr(w, r, inf.Tx.Tx, http.StatusBadRequest, 
errors.New("unable to parse required keys from request body"), nil)
+               return
+       }
+
+       // check if user can modify cache group
+       userErr, sysErr, errCode = 
dbhelpers.CheckIfCurrentUserCanModifyCachegroup(dgCg.ReqInfo.Tx.Tx, *dgCg.ID, 
dgCg.ReqInfo.User.UserName)
+       if sysErr != nil {
+               api.HandleErr(w, r, tx, errCode, fmt.Errorf("update cachegroup: 
checking if user can modify: "+sysErr.Error()), sysErr)
+               return
+       }
+       if userErr != nil {
+               api.HandleErr(w, r, tx, errCode, fmt.Errorf("update cachegroup: 
checking if user can modify: "+userErr.Error()), userErr)
+               return
+       }
+
+       coordinateID, userErr, sysErr, errCode := dgCg.handleCoordinateUpdate()
+       if sysErr != nil {
+               api.HandleErr(w, r, tx, errCode, fmt.Errorf("update cachegroup: 
updating coordinate: "+sysErr.Error()), sysErr)
+               return
+       }
+       if userErr != nil {
+               api.HandleErr(w, r, tx, errCode, fmt.Errorf("update cachegroup: 
updating coordinate: "+userErr.Error()), userErr)
+               return
+       }
+
+       err = dgCg.ValidateTypeInTopology()
+       if err != nil {
+               api.HandleErr(w, r, tx, http.StatusBadRequest, 
fmt.Errorf("update cachegroup: validating type in topology: "+err.Error()), err)
+               return
+       }
+
+       //update cache group
+       query := UpdateQuery()
+
+       err = tx.QueryRow(
+               query,
+               dgCg.Name,
+               dgCg.ShortName,
+               coordinateID,
+               dgCg.ParentCachegroupID,
+               dgCg.SecondaryParentCachegroupID,
+               dgCg.TypeID,
+               dgCg.FallbackToClosest,
+               dgCg.ID,
+       ).Scan(
+               &dgCg.Type,
+               &dgCg.ParentName,
+               &dgCg.SecondaryParentName,
+               &dgCg.LastUpdated,
+       )
+       if err != nil {
+               if errors.Is(err, sql.ErrNoRows) {
+                       api.HandleErr(w, r, tx, http.StatusNotFound, 
fmt.Errorf("cache group with name: %s not found", *dgCg.Name), nil)
+                       return
+               }
+               usrErr, sysErr, code := api.ParseDBError(err)
+               api.HandleErr(w, r, tx, code, usrErr, sysErr)
+               return
+       }
+
+       if err = dgCg.createLocalizationMethods(); err != nil {
+               api.HandleErr(w, r, tx, http.StatusInternalServerError, 
fmt.Errorf("creating cachegroup: creating localization methods: "+err.Error()), 
nil)
+               return
+       }
+
+       if err = dgCg.createCacheGroupFallbacks(); err != nil {
+               api.HandleErr(w, r, tx, http.StatusInternalServerError, 
fmt.Errorf("creating cachegroup: creating cache group fallbacks: 
"+err.Error()), nil)
+               return
+       }
+
+       cg, err = dgCg.Upgrade()
+       if err != nil {
+               api.HandleErr(w, r, tx, http.StatusInternalServerError, 
fmt.Errorf("converting cachegroup: converting cache group upgrade: 
"+err.Error()), nil)
+               return
+       }
+
+       alerts := tc.CreateAlerts(tc.SuccessLevel, "cache group was updated")
+       api.WriteAlertsObj(w, r, http.StatusOK, alerts, cg)
+       return
+}
+
+// DeleteCacheGroup [Version : V5] function deletes the cache group passed.
+func DeleteCacheGroup(w http.ResponseWriter, r *http.Request) {
+       inf, userErr, sysErr, errCode := api.NewInfo(r, nil, nil)
+       tx := inf.Tx.Tx
+       if userErr != nil || sysErr != nil {
+               api.HandleErr(w, r, tx, errCode, userErr, sysErr)
+               return
+       }
+       defer inf.Close()
+
+       ID := inf.Params["id"]
+       id, err := strconv.Atoi(ID)
+       if err != nil {
+               api.HandleErr(w, r, tx, http.StatusUnprocessableEntity, 
fmt.Errorf("delete cachegroup: converted to type int: "+err.Error()), nil)
+               return
+       }
+
+       inUse, err := isUsed(inf.Tx, id)
+       if inUse {
+               api.HandleErr(w, r, tx, http.StatusBadRequest, nil, nil)
+               return
+       }
+       if err != nil {
+               api.HandleErr(w, r, tx, http.StatusInternalServerError, 
fmt.Errorf("cachegroup delete: checking use: "+err.Error()), nil)
+               return
+       }
+
+       coordinateID, err := dbhelpers.GetCoordinateID(inf.Tx.Tx, id)
+       if err == sql.ErrNoRows {
+               api.HandleErr(w, r, tx, http.StatusNotFound, fmt.Errorf("no 
cachegroup with that id found"), nil)
+               return
+       }
+       if err != nil {
+               api.HandleErr(w, r, tx, http.StatusInternalServerError, 
fmt.Errorf("cachegroup delete: deleting cachegroup: "+err.Error()), nil)
+               return
+       }
+
+       // check if user can modify cache group
+       userErr, sysErr, errCode = 
dbhelpers.CheckIfCurrentUserCanModifyCachegroup(inf.Tx.Tx, id, 
inf.User.UserName)
+       if userErr != nil {
+               api.HandleErr(w, r, tx, errCode, fmt.Errorf("cachegroup delete: 
getting coord: "+userErr.Error()), nil)
+               return
+       }
+       if sysErr != nil {
+               api.HandleErr(w, r, tx, errCode, fmt.Errorf("cachegroup delete: 
getting coord: "+sysErr.Error()), nil)
+               return
+       }
+
+       if err = dbhelpers.DeleteCoordinate(inf.Tx.Tx, id, *coordinateID); err 
!= nil {
+               api.HandleErr(w, r, tx, http.StatusInternalServerError, 
fmt.Errorf("cachegroup delete: deleting coord: "+err.Error()), nil)
+               return
+       }
+
+       res, err := tx.Exec("DELETE FROM cachegroup AS cg WHERE cg.ID=$1", ID)
+       if err != nil {
+               api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, 
err)
+               return
+       }
+       rowsAffected, err := res.RowsAffected()
+       if err != nil {
+               api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, 
fmt.Errorf("determining rows affected for delete cachegroup: %w", err))
+               return
+       }
+       if rowsAffected == 0 {
+               api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, 
fmt.Errorf("no rows deleted for cachegroup"))
+               return
+       }
+
+       alertMessage := fmt.Sprintf("%s was deleted.", ID)
+       alerts := tc.CreateAlerts(tc.SuccessLevel, alertMessage)
+       api.WriteAlerts(w, r, http.StatusOK, alerts)
+       return
+}
+
+// readAndValidateJsonStruct populates select missing fields and validates 
JSON body
+func readAndValidateJsonStruct(r *http.Request) (tc.CacheGroupNullableV5, 
error) {
+       var cg tc.CacheGroupNullableV5
+       if err := json.NewDecoder(r.Body).Decode(&cg); err != nil {
+               userErr := fmt.Errorf("error decoding POST request body into 
CacheGroupV5 struct %w", err)
+               return cg, userErr
+       }
+
+       if cg.Latitude == nil {
+               cg.Latitude = util.Ptr(0.0)
+       }
+       if cg.Longitude == nil {
+               cg.Longitude = util.Ptr(0.0)
+       }
+       if cg.LocalizationMethods == nil {
+               cg.LocalizationMethods = &[]tc.LocalizationMethod{}
+       }
+       if cg.Fallbacks == nil {
+               cg.Fallbacks = &[]string{}
+       }
+       if cg.FallbackToClosest == nil {
+               fbc := true
+               cg.FallbackToClosest = &fbc
+       }
+
+       // validate JSON body
+       rule := 
validation.NewStringRule(tovalidate.IsAlphanumericUnderscoreDash, "must consist 
of only alphanumeric, dash, or underscore characters")
+       errs := tovalidate.ToErrors(validation.Errors{
+               "name": validation.Validate(cg.Name, validation.Required, rule),
+       })
+       if len(errs) > 0 {
+               userErr := util.JoinErrs(errs)
+               return cg, userErr
+       }
+       return cg, nil
+}
diff --git a/traffic_ops/traffic_ops_golang/dbhelpers/db_helpers.go 
b/traffic_ops/traffic_ops_golang/dbhelpers/db_helpers.go
index 94f026b800..c70056279b 100644
--- a/traffic_ops/traffic_ops_golang/dbhelpers/db_helpers.go
+++ b/traffic_ops/traffic_ops_golang/dbhelpers/db_helpers.go
@@ -2218,6 +2218,21 @@ func ASNExists(tx *sql.Tx, id string) (bool, error) {
        return true, nil
 }
 
+// CacheGroupExists confirms whether the cache group exists, and an error (if 
one occurs).
+func CacheGroupExists(tx *sql.Tx, name string) (bool, error) {
+       var count int
+       if err := tx.QueryRow("SELECT count(name) FROM cachegroup AS cg WHERE 
cg.name=$1", name).Scan(&count); err != nil {
+               return false, fmt.Errorf("error getting cache group info: %w", 
err)
+       }
+       if count == 0 {
+               return false, nil
+       }
+       if count != 1 {
+               return false, fmt.Errorf("getting cache group info - expected 
row count: 1, actual: %d", count)
+       }
+       return true, nil
+}
+
 // DivisionExists confirms whether the division exists, and an error (if one 
occurs).
 func DivisionExists(tx *sql.Tx, id string) (bool, error) {
        var count int
@@ -2247,3 +2262,45 @@ func PhysLocationExists(tx *sql.Tx, id string) (bool, 
error) {
        }
        return true, nil
 }
+
+// GetCoordinateID obtains coordinateID, and an error (if one occurs)
+func GetCoordinateID(tx *sql.Tx, id int) (*int, error) {
+       q := `SELECT coordinate FROM cachegroup WHERE id = $1`
+
+       var coordinateID *int
+       if err := tx.QueryRow(q, id).Scan(&coordinateID); err != nil {
+               return nil, err
+       }
+
+       return coordinateID, nil
+}
+
+// DeleteCoordinate deletes coordinate by id, and an error (if one occurs)
+func DeleteCoordinate(tx *sql.Tx, cacheGroupID int, coordinateID int) error {
+       q := `UPDATE cachegroup SET coordinate = NULL WHERE id = $1`
+       result, err := tx.Exec(q, cacheGroupID)
+       if err != nil {
+               return fmt.Errorf("updating cachegroup %d coordinate to null: 
%s", cacheGroupID, err.Error())
+       }
+       rowsAffected, err := result.RowsAffected()
+       if err != nil {
+               return fmt.Errorf("updating cachegroup %d coordinate to null, 
getting rows affected: %s", coordinateID, err.Error())
+       }
+       if rowsAffected == 0 {
+               return fmt.Errorf("updating cachegroup %d coordinate to null, 
zero rows affected", coordinateID)
+       }
+
+       q = `DELETE FROM coordinate WHERE id = $1`
+       result, err = tx.Exec(q, coordinateID)
+       if err != nil {
+               return fmt.Errorf("delete coordinate %d for cachegroup %d: %s", 
coordinateID, coordinateID, err.Error())
+       }
+       rowsAffected, err = result.RowsAffected()
+       if err != nil {
+               return fmt.Errorf("delete coordinate %d for cachegroup %d, 
getting rows affected: %s", coordinateID, coordinateID, err.Error())
+       }
+       if rowsAffected == 0 {
+               return fmt.Errorf("delete coordinate %d for cachegroup %d, zero 
rows affected", coordinateID, coordinateID)
+       }
+       return nil
+}
diff --git a/traffic_ops/traffic_ops_golang/dbhelpers/db_helpers_test.go 
b/traffic_ops/traffic_ops_golang/dbhelpers/db_helpers_test.go
index 0c9bda1586..3f9d2123dd 100644
--- a/traffic_ops/traffic_ops_golang/dbhelpers/db_helpers_test.go
+++ b/traffic_ops/traffic_ops_golang/dbhelpers/db_helpers_test.go
@@ -563,6 +563,57 @@ func TestASNExists(t *testing.T) {
        }
 }
 
+func TestCacheGroupExistsExists(t *testing.T) {
+       var testCases = []struct {
+               description   string
+               name          string
+               expectedError error
+               exists        bool
+       }{
+               {
+                       description:   "Success: Get valid Cache Group",
+                       name:          "testCacheGroup1",
+                       expectedError: nil,
+                       exists:        true,
+               },
+               {
+                       description:   "Failure: Cache Group not in DB",
+                       name:          "testCacheGroup2",
+                       expectedError: sql.ErrNoRows,
+                       exists:        false,
+               },
+       }
+       for _, testCase := range testCases {
+               t.Run(testCase.description, func(t *testing.T) {
+                       mockDB, mock, err := sqlmock.New()
+                       if err != nil {
+                               t.Fatalf("an error '%s' was not expected when 
opening a stub database connection", err)
+                       }
+                       defer mockDB.Close()
+
+                       db := sqlx.NewDb(mockDB, "sqlmock")
+                       defer db.Close()
+
+                       mock.ExpectBegin()
+                       rows := sqlmock.NewRows([]string{"count"})
+                       if testCase.exists {
+                               rows = rows.AddRow(1)
+                       }
+                       mock.ExpectQuery("SELECT").WillReturnRows(rows)
+                       mock.ExpectCommit()
+
+                       cgExists, err := CacheGroupExists(db.MustBegin().Tx, 
testCase.name)
+                       if testCase.exists != cgExists {
+                               t.Errorf("Expected return exists: %t, actual 
%t", testCase.exists, cgExists)
+                       }
+
+                       if !errors.Is(err, testCase.expectedError) {
+                               t.Errorf("CacheGroupExists expected: %s, 
actual: %s", testCase.expectedError, err)
+                       }
+               })
+       }
+}
+
 func TestDivisionExists(t *testing.T) {
        var testCases = []struct {
                description   string
diff --git a/traffic_ops/traffic_ops_golang/routing/routes.go 
b/traffic_ops/traffic_ops_golang/routing/routes.go
index 1de8191a96..f62a77db47 100644
--- a/traffic_ops/traffic_ops_golang/routing/routes.go
+++ b/traffic_ops/traffic_ops_golang/routing/routes.go
@@ -177,10 +177,10 @@ func Routes(d ServerData) ([]Route, http.Handler, error) {
                {Version: api.Version{Major: 5, Minor: 0}, Method: 
http.MethodGet, Path: `caches/stats/?$`, Handler: cachesstats.Get, 
RequiredPrivLevel: auth.PrivLevelReadOnly, RequiredPermissions: 
[]string{"CACHE-GROUP:READ", "PROFILE:READ"}, Authenticated: Authenticated, 
Middlewares: nil, ID: 481320658831},
 
                //CacheGroup: CRUD
-               {Version: api.Version{Major: 5, Minor: 0}, Method: 
http.MethodGet, Path: `cachegroups/?$`, Handler: 
api.ReadHandler(&cachegroup.TOCacheGroup{}), RequiredPrivLevel: 
auth.PrivLevelReadOnly, RequiredPermissions: []string{"CACHE-GROUP:READ", 
"TYPE:READ"}, Authenticated: Authenticated, Middlewares: nil, ID: 42307911031},
-               {Version: api.Version{Major: 5, Minor: 0}, Method: 
http.MethodPut, Path: `cachegroups/{id}$`, Handler: 
api.UpdateHandler(&cachegroup.TOCacheGroup{}), RequiredPrivLevel: 
auth.PrivLevelOperations, RequiredPermissions: []string{"CACHE-GROUP:UPDATE", 
"CACHE-GROUP:READ", "TYPE:READ"}, Authenticated: Authenticated, Middlewares: 
nil, ID: 41295454631},
-               {Version: api.Version{Major: 5, Minor: 0}, Method: 
http.MethodPost, Path: `cachegroups/?$`, Handler: 
api.CreateHandler(&cachegroup.TOCacheGroup{}), RequiredPrivLevel: 
auth.PrivLevelOperations, RequiredPermissions: []string{"CACHE-GROUP:CREATE", 
"CACHE-GROUP:READ", "TYPE:READ"}, Authenticated: Authenticated, Middlewares: 
nil, ID: 4298266531},
-               {Version: api.Version{Major: 5, Minor: 0}, Method: 
http.MethodDelete, Path: `cachegroups/{id}$`, Handler: 
api.DeleteHandler(&cachegroup.TOCacheGroup{}), RequiredPrivLevel: 
auth.PrivLevelOperations, RequiredPermissions: []string{"CACHE-GROUP:DELETE", 
"CACHE-GROUP:READ"}, Authenticated: Authenticated, Middlewares: nil, ID: 
42786936531},
+               {Version: api.Version{Major: 5, Minor: 0}, Method: 
http.MethodGet, Path: `cachegroups/?$`, Handler: cachegroup.GetCacheGroup, 
RequiredPrivLevel: auth.PrivLevelReadOnly, RequiredPermissions: 
[]string{"CACHE-GROUP:READ", "TYPE:READ"}, Authenticated: Authenticated, 
Middlewares: nil, ID: 42307911031},
+               {Version: api.Version{Major: 5, Minor: 0}, Method: 
http.MethodPut, Path: `cachegroups/{id}$`, Handler: 
cachegroup.UpdateCacheGroup, RequiredPrivLevel: auth.PrivLevelOperations, 
RequiredPermissions: []string{"CACHE-GROUP:UPDATE", "CACHE-GROUP:READ", 
"TYPE:READ"}, Authenticated: Authenticated, Middlewares: nil, ID: 41295454631},
+               {Version: api.Version{Major: 5, Minor: 0}, Method: 
http.MethodPost, Path: `cachegroups/?$`, Handler: cachegroup.CreateCacheGroup, 
RequiredPrivLevel: auth.PrivLevelOperations, RequiredPermissions: 
[]string{"CACHE-GROUP:CREATE", "CACHE-GROUP:READ", "TYPE:READ"}, Authenticated: 
Authenticated, Middlewares: nil, ID: 4298266531},
+               {Version: api.Version{Major: 5, Minor: 0}, Method: 
http.MethodDelete, Path: `cachegroups/{id}$`, Handler: 
cachegroup.DeleteCacheGroup, RequiredPrivLevel: auth.PrivLevelOperations, 
RequiredPermissions: []string{"CACHE-GROUP:DELETE", "CACHE-GROUP:READ"}, 
Authenticated: Authenticated, Middlewares: nil, ID: 42786936531},
 
                {Version: api.Version{Major: 5, Minor: 0}, Method: 
http.MethodPost, Path: `cachegroups/{id}/queue_update$`, Handler: 
cachegroup.QueueUpdates, RequiredPrivLevel: auth.PrivLevelOperations, 
RequiredPermissions: []string{"CACHE-GROUP:READ", "CDN:READ", "SERVER:READ", 
"SERVER:QUEUE"}, Authenticated: Authenticated, Middlewares: nil, ID: 
407164411031},
                {Version: api.Version{Major: 5, Minor: 0}, Method: 
http.MethodPost, Path: `cachegroups/{id}/deliveryservices/?$`, Handler: 
cachegroup.DSPostHandlerV40, RequiredPrivLevel: auth.PrivLevelOperations, 
RequiredPermissions: []string{"CACHE-GROUP:UPDATE", "DELIVERY-SERVICE:UPDATE", 
"CACHE-GROUP:READ", "DELIVERY-SERVICE:READ"}, Authenticated: Authenticated, 
Middlewares: nil, ID: 452024043131},
diff --git a/traffic_ops/v5-client/cachegroup.go 
b/traffic_ops/v5-client/cachegroup.go
index 0d290fa66b..d660349e71 100644
--- a/traffic_ops/v5-client/cachegroup.go
+++ b/traffic_ops/v5-client/cachegroup.go
@@ -27,8 +27,8 @@ import (
 const apiCachegroups = "/cachegroups"
 
 // CreateCacheGroup creates the given Cache Group.
-func (to *Session) CreateCacheGroup(cachegroup tc.CacheGroupNullable, opts 
RequestOptions) (tc.CacheGroupDetailResponse, toclientlib.ReqInf, error) {
-       var resp tc.CacheGroupDetailResponse
+func (to *Session) CreateCacheGroup(cachegroup tc.CacheGroupNullableV5, opts 
RequestOptions) (tc.CacheGroupDetailResponseV5, toclientlib.ReqInf, error) {
+       var resp tc.CacheGroupDetailResponseV5
        if cachegroup.TypeID == nil && cachegroup.Type != nil {
                opts := NewRequestOptions()
                opts.QueryParameters.Set("name", *cachegroup.Type)
@@ -78,16 +78,16 @@ func (to *Session) CreateCacheGroup(cachegroup 
tc.CacheGroupNullable, opts Reque
 
 // UpdateCacheGroup replaces the Cache Group identified by the given ID with
 // the given Cache Group.
-func (to *Session) UpdateCacheGroup(id int, cachegroup tc.CacheGroupNullable, 
opts RequestOptions) (tc.CacheGroupDetailResponse, toclientlib.ReqInf, error) {
+func (to *Session) UpdateCacheGroup(id int, cachegroup 
tc.CacheGroupNullableV5, opts RequestOptions) (tc.CacheGroupDetailResponseV5, 
toclientlib.ReqInf, error) {
        route := fmt.Sprintf("%s/%d", apiCachegroups, id)
-       var cachegroupResp tc.CacheGroupDetailResponse
+       var cachegroupResp tc.CacheGroupDetailResponseV5
        reqInf, err := to.put(route, opts, cachegroup, &cachegroupResp)
        return cachegroupResp, reqInf, err
 }
 
 // GetCacheGroups retrieves Cache Groups configured in Traffic Ops.
-func (to *Session) GetCacheGroups(opts RequestOptions) 
(tc.CacheGroupsNullableResponse, toclientlib.ReqInf, error) {
-       var data tc.CacheGroupsNullableResponse
+func (to *Session) GetCacheGroups(opts RequestOptions) 
(tc.CacheGroupsNullableResponseV5, toclientlib.ReqInf, error) {
+       var data tc.CacheGroupsNullableResponseV5
        reqInf, err := to.get(apiCachegroups, opts, &data)
        return data, reqInf, err
 }

Reply via email to