You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@trafficcontrol.apache.org by oc...@apache.org on 2023/04/19 17:13:59 UTC
[trafficcontrol] branch master updated: Fixes service_category apis to respond with RFC3339 date/time Format (#7408)
This is an automated email from the ASF dual-hosted git repository.
ocket8888 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 664c2d5748 Fixes service_category apis to respond with RFC3339 date/time Format (#7408)
664c2d5748 is described below
commit 664c2d57483bc426cf59b067b76c38820dd8fc8e
Author: Jagan Parthiban <33...@users.noreply.github.com>
AuthorDate: Wed Apr 19 22:43:53 2023 +0530
Fixes service_category apis to respond with RFC3339 date/time Format (#7408)
* This fixes service_category apis to respond with RFC3339 date/time strings. Issue: https://github.com/apache/trafficcontrol/issues/5911
* This fixes service_category apis to respond with RFC3339 date/time strings.
* This fixes service_category apis to respond with RFC3339 date/time strings. This commit removed new struct TimeRFC3339 and uses time.Time instead
* This commit removed new struct TimeRFC3339 and uses time.Time instead
* https://github.com/apache/trafficcontrol/issues/7413
Removing dependency of service catergory on generic cruder for V5 version
* Corrected Error messages statement
* Added Doc content for RFC 3339 Date/Time Format and updated service category docs.
* Updated Http Get operations for Service Category V5 to handle If-Modified-Since request HTTP header
* Added Unit Test Cases for V5 service_category functions
* Updated comments for the V5 service category functions.
* Updated CHANGELOG.md
* Added Unit test for db_helpers.go
* Addressed Code review from the PR.
* Addressed Code review from the PR.
* Addressed Code review from the PR.
* Addressed Code review from the PR.
---
CHANGELOG.md | 1 +
docs/source/api/v5/service_categories.rst | 12 +-
docs/source/api/v5/service_categories_name.rst | 8 +-
lib/go-tc/service_category.go | 34 +++
.../testing/api/v5/servicecategories_test.go | 20 +-
traffic_ops/testing/api/v5/traffic_control_test.go | 2 +-
.../traffic_ops_golang/dbhelpers/db_helpers.go | 15 +
.../dbhelpers/db_helpers_test.go | 51 ++++
traffic_ops/traffic_ops_golang/routing/routes.go | 8 +-
.../servicecategory/servicecategories.go | 319 +++++++++++++++++++++
.../servicecategory/servicecategories_test.go | 146 ++++++++++
traffic_ops/v5-client/serviceCategory.go | 11 +-
12 files changed, 596 insertions(+), 31 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index b997b88acf..aa564ef54c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -53,6 +53,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/).
### Fixed
- [#7441](https://github.com/apache/trafficcontrol/pull/7441) *Traffic Ops* Fixed the invalidation jobs endpoint to respect CDN locks.
+- [#7413](https://github.com/apache/trafficcontrol/issues/7413) *Traffic Ops* Fixes service_category apis to respond with RFC3339 date/time Format
- [#7414](https://github.com/apache/trafficcontrol/pull/7414) * Traffic Portal* Fixed DSR difference for DS required capability.
- [#7130](https://github.com/apache/trafficcontrol/issues/7130) *Traffic Ops* Fixes service_categories response to POST API.
- [#7340](https://github.com/apache/trafficcontrol/pull/7340) *Traffic Router* Fixed TR logging for the `cqhv` field when absent.
diff --git a/docs/source/api/v5/service_categories.rst b/docs/source/api/v5/service_categories.rst
index 7f6aa563b3..28408acdb0 100644
--- a/docs/source/api/v5/service_categories.rst
+++ b/docs/source/api/v5/service_categories.rst
@@ -64,7 +64,7 @@ Request Structure
Response Structure
------------------
:name: This :term:`Service Category`'s name
-:lastUpdated: The date and time at which this :term:`Service Category` was last modified, in :ref:`non-rfc-datetime`
+:lastUpdated: The date and time at which this :term:`Service Category` was last modified, in :rfc:`3339`
.. code-block:: http
:caption: Response Example
@@ -78,13 +78,13 @@ Response Structure
Set-Cookie: mojolicious=...; Path=/; Expires=Mon, 18 Nov 2019 17:40:54 GMT; Max-Age=3600; HttpOnly
Whole-Content-Sha512: Yzr6TfhxgpZ3pbbrr4TRG4wC3PlnHDDzgs2igtz/1ppLSy2MzugqaGW4y5yzwzl5T3+7q6HWej7GQZt1XIVeZQ==
X-Server-Name: traffic_ops_golang/
- Date: Wed, 11 Mar 2020 20:02:47 GMT
+ Date: Wed, 29 Mar 2023 15:56:34 GMT
Content-Length: 102
{
"response": [
{
- "lastUpdated": "2020-03-04 15:46:20-07",
+ "lastUpdated": "2023-03-29T19:43:17.557642+05:30",
"name": "SERVICE_CATEGORY_NAME"
}
]
@@ -121,7 +121,7 @@ Request Structure
Response Structure
------------------
:name: This :term:`Service Category`'s name
-:lastUpdated: The date and time at which this :term:`Service Category` was last modified, in :ref:`non-rfc-datetime`
+:lastUpdated: The date and time at which this :term:`Service Category` was last modified, in :rfc:`3339`
.. code-block:: http
:caption: Response Example
@@ -135,7 +135,7 @@ Response Structure
Set-Cookie: mojolicious=...; Path=/; Expires=Mon, 18 Nov 2019 17:40:54 GMT; Max-Age=3600; HttpOnly
Whole-Content-Sha512: +pJm4c3O+JTaSXNt+LP+u240Ba/SsvSSDOQ4rDc6hcyZ0FIL+iY/WWrMHhpLulRGKGY88bM4YPCMaxGn3FZ9yQ==
X-Server-Name: traffic_ops_golang/
- Date: Wed, 11 Mar 2020 20:12:20 GMT
+ Date: Wed, 29 Mar 2023 15:58:37 GMT
Content-Length: 154
{
@@ -146,7 +146,7 @@ Response Structure
}
],
"response": {
- "lastUpdated": "2020-03-11 14:12:20-06",
+ "lastUpdated": "2023-03-29T21:28:37.884457+05:30",
"name": "SERVICE_CATEGORY_NAME"
}
}
diff --git a/docs/source/api/v5/service_categories_name.rst b/docs/source/api/v5/service_categories_name.rst
index ba16efd3d2..d876ae8860 100644
--- a/docs/source/api/v5/service_categories_name.rst
+++ b/docs/source/api/v5/service_categories_name.rst
@@ -58,7 +58,7 @@ Request Structure
Response Structure
------------------
:name: This :term:`Service Category`'s name
-:lastUpdated: The date and time at which this :term:`Service Category` was last modified, in :ref:`non-rfc-datetime`
+:lastUpdated: The date and time at which this :term:`Service Category` was last modified, in :rfc:`3339`
.. code-block:: http
:caption: Response Example
@@ -72,7 +72,7 @@ Response Structure
Set-Cookie: mojolicious=...; Path=/; Expires=Mon, 18 Nov 2019 17:40:54 GMT; Max-Age=3600; HttpOnly
Whole-Content-Sha512: +pJm4c3O+JTaSXNt+LP+u240Ba/SsvSSDOQ4rDc6hcyZ0FIL+iY/WWrMHhpLulRGKGY88bM4YPCMaxGn3FZ9yQ==
X-Server-Name: traffic_ops_golang/
- Date: Wed, 11 Mar 2020 20:12:20 GMT
+ Date: Wed, 29 Mar 2023 15:58:37 GMT
Content-Length: 189
{
@@ -83,7 +83,7 @@ Response Structure
}
],
"response": {
- "lastUpdated": "2020-03-11 14:12:20-06",
+ "lastUpdated": "2023-03-29T21:28:37.884457+05:30",
"name": "New Name"
}
}
@@ -136,7 +136,7 @@ Response Structure
Set-Cookie: mojolicious=...; Path=/; Expires=Mon, 17 Aug 2020 16:13:31 GMT; Max-Age=3600; HttpOnly
Whole-Content-Sha512: yErJobzG9IA0khvqZQK+Yi7X4pFVvOqxn6PjrdzN5DnKVm/K8Kka3REul1XmKJnMXVRY8RayoEVGDm16mBFe4Q==
X-Server-Name: traffic_ops_golang/
- Date: Mon, 17 Aug 2020 15:13:31 GMT
+ Date: Wed, 29 Mar 2023 15:58:37 GMT
Content-Length: 103
{
diff --git a/lib/go-tc/service_category.go b/lib/go-tc/service_category.go
index 19c2d45ecf..69d0716d2d 100644
--- a/lib/go-tc/service_category.go
+++ b/lib/go-tc/service_category.go
@@ -19,6 +19,8 @@ package tc
* under the License.
*/
+import "time"
+
// ServiceCategoriesResponse is a list of Service Categories as a response.
type ServiceCategoriesResponse struct {
Response []ServiceCategory `json:"response"`
@@ -37,3 +39,35 @@ type ServiceCategory struct {
LastUpdated TimeNoMod `json:"lastUpdated" db:"last_updated"`
Name string `json:"name" db:"name"`
}
+
+// ServiceCategoriesResponseV50 is a list of Service Categories as a response.
+type ServiceCategoriesResponseV50 struct {
+ Response []ServiceCategoryV50 `json:"response"`
+ Alerts
+}
+
+// ServiceCategoryResponseV50 is a single Service Category response for Update and Create to
+// depict what changed.
+type ServiceCategoryResponseV50 struct {
+ Response ServiceCategoryV50 `json:"response"`
+ Alerts
+}
+
+// ServiceCategoryV50 holds the name and last updated time stamp.
+type ServiceCategoryV50 struct {
+ LastUpdated time.Time `json:"lastUpdated" db:"last_updated"`
+ Name string `json:"name" db:"name"`
+}
+
+// ServiceCategoriesResponseV5 is the type of a response from the service_categories
+// Traffic Ops endpoint.
+// It always points to the type for the latest minor version of ServiceCategoriesResponseV5x APIv5.
+type ServiceCategoriesResponseV5 = ServiceCategoriesResponseV50
+
+// ServiceCategoryResponseV5 is the type of a response from the service_categories
+// Traffic Ops endpoint.
+// It always points to the type for the latest minor version of ServiceCategoryResponseV5x APIv5.
+type ServiceCategoryResponseV5 = ServiceCategoryResponseV50
+
+// ServiceCategoryV5 always points to the type for the latest minor version of serviceCategoryV5x APIv5.
+type ServiceCategoryV5 = ServiceCategoryV50
diff --git a/traffic_ops/testing/api/v5/servicecategories_test.go b/traffic_ops/testing/api/v5/servicecategories_test.go
index 9870ff9d08..10921f8c93 100644
--- a/traffic_ops/testing/api/v5/servicecategories_test.go
+++ b/traffic_ops/testing/api/v5/servicecategories_test.go
@@ -37,7 +37,7 @@ func TestServiceCategories(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.ServiceCategory]{
+ methodTests := utils.TestCase[client.Session, client.RequestOptions, tc.ServiceCategoryV5]{
"GET": {
"NOT MODIFIED when NO CHANGES made": {
ClientSession: TOSession,
@@ -103,12 +103,12 @@ func TestServiceCategories(t *testing.T) {
"POST": {
"BAD REQUEST when ALREADY EXISTS": {
ClientSession: TOSession,
- RequestBody: tc.ServiceCategory{Name: "serviceCategory1"},
+ RequestBody: tc.ServiceCategoryV5{Name: "serviceCategory1"},
Expectations: utils.CkRequest(utils.HasError(), utils.HasStatus(http.StatusBadRequest)),
},
"BAD REQUEST when NAME FIELD is BLANK": {
ClientSession: TOSession,
- RequestBody: tc.ServiceCategory{Name: ""},
+ RequestBody: tc.ServiceCategoryV5{Name: ""},
Expectations: utils.CkRequest(utils.HasError(), utils.HasStatus(http.StatusBadRequest)),
},
},
@@ -116,7 +116,7 @@ func TestServiceCategories(t *testing.T) {
"OK when VALID request": {
ClientSession: TOSession,
RequestOpts: client.RequestOptions{QueryParameters: url.Values{"name": {"barServiceCategory2"}}},
- RequestBody: tc.ServiceCategory{Name: "newName"},
+ RequestBody: tc.ServiceCategoryV5{Name: "newName"},
Expectations: utils.CkRequest(utils.NoError(), utils.HasStatus(http.StatusOK),
validateServiceCategoriesUpdateCreateFields("newName", map[string]interface{}{"Name": "newName"})),
},
@@ -126,12 +126,12 @@ func TestServiceCategories(t *testing.T) {
QueryParameters: url.Values{"name": {"serviceCategory1"}},
Header: http.Header{rfc.IfUnmodifiedSince: {currentTimeRFC}},
},
- RequestBody: tc.ServiceCategory{Name: "newName"},
+ RequestBody: tc.ServiceCategoryV5{Name: "newName"},
Expectations: utils.CkRequest(utils.HasError(), utils.HasStatus(http.StatusPreconditionFailed)),
},
"PRECONDITION FAILED when updating with IFMATCH ETAG Header": {
ClientSession: TOSession,
- RequestBody: tc.ServiceCategory{Name: "newName"},
+ RequestBody: tc.ServiceCategoryV5{Name: "newName"},
RequestOpts: client.RequestOptions{
QueryParameters: url.Values{"name": {"serviceCategory1"}},
Header: http.Header{rfc.IfMatch: {rfc.ETag(currentTime)}},
@@ -196,7 +196,7 @@ func TestServiceCategories(t *testing.T) {
func validateServiceCategoriesFields(expectedResp map[string]interface{}) utils.CkReqFunc {
return func(t *testing.T, _ toclientlib.ReqInf, resp interface{}, _ tc.Alerts, _ error) {
assert.RequireNotNil(t, resp, "Expected Service Categories response to not be nil.")
- serviceCategoryResp := resp.([]tc.ServiceCategory)
+ serviceCategoryResp := resp.([]tc.ServiceCategoryV5)
for field, expected := range expectedResp {
for _, serviceCategory := range serviceCategoryResp {
switch field {
@@ -223,7 +223,7 @@ func validateServiceCategoriesUpdateCreateFields(name string, expectedResp map[s
func validateServiceCategoriesPagination(paginationParam string) utils.CkReqFunc {
return func(t *testing.T, _ toclientlib.ReqInf, resp interface{}, _ tc.Alerts, _ error) {
- paginationResp := resp.([]tc.ServiceCategory)
+ paginationResp := resp.([]tc.ServiceCategoryV5)
opts := client.NewRequestOptions()
opts.QueryParameters.Set("orderby", "id")
@@ -247,7 +247,7 @@ func validateServiceCategoriesSort() utils.CkReqFunc {
return func(t *testing.T, _ toclientlib.ReqInf, resp interface{}, alerts tc.Alerts, _ error) {
assert.RequireNotNil(t, resp, "Expected Service Categories response to not be nil.")
var serviceCategoryNames []string
- serviceCategoryResp := resp.([]tc.ServiceCategory)
+ serviceCategoryResp := resp.([]tc.ServiceCategoryV5)
for _, serviceCategory := range serviceCategoryResp {
serviceCategoryNames = append(serviceCategoryNames, serviceCategory.Name)
}
@@ -258,7 +258,7 @@ func validateServiceCategoriesSort() utils.CkReqFunc {
func validateServiceCategoriesDescSort() utils.CkReqFunc {
return func(t *testing.T, _ toclientlib.ReqInf, resp interface{}, alerts tc.Alerts, _ error) {
assert.RequireNotNil(t, resp, "Expected Service Categories response to not be nil.")
- serviceCategoriesDescResp := resp.([]tc.ServiceCategory)
+ serviceCategoriesDescResp := resp.([]tc.ServiceCategoryV5)
var descSortedList []string
var ascSortedList []string
assert.RequireGreaterOrEqual(t, len(serviceCategoriesDescResp), 2, "Need at least 2 Service Categories in Traffic Ops to test desc sort, found: %d", len(serviceCategoriesDescResp))
diff --git a/traffic_ops/testing/api/v5/traffic_control_test.go b/traffic_ops/testing/api/v5/traffic_control_test.go
index bb747942e6..b6673951c8 100644
--- a/traffic_ops/testing/api/v5/traffic_control_test.go
+++ b/traffic_ops/testing/api/v5/traffic_control_test.go
@@ -48,7 +48,7 @@ type TrafficControl struct {
Servers []tc.ServerV4 `json:"servers"`
ServerServerCapabilities []tc.ServerServerCapability `json:"serverServerCapabilities"`
ServerCapabilities []tc.ServerCapabilityV41 `json:"serverCapabilities"`
- ServiceCategories []tc.ServiceCategory `json:"serviceCategories"`
+ ServiceCategories []tc.ServiceCategoryV5 `json:"serviceCategories"`
Statuses []tc.StatusNullable `json:"statuses"`
StaticDNSEntries []tc.StaticDNSEntry `json:"staticdnsentries"`
StatsSummaries []tc.StatsSummary `json:"statsSummaries"`
diff --git a/traffic_ops/traffic_ops_golang/dbhelpers/db_helpers.go b/traffic_ops/traffic_ops_golang/dbhelpers/db_helpers.go
index 39f6487efa..1f0ec7f3a3 100644
--- a/traffic_ops/traffic_ops_golang/dbhelpers/db_helpers.go
+++ b/traffic_ops/traffic_ops_golang/dbhelpers/db_helpers.go
@@ -2187,3 +2187,18 @@ func GetSCInfo(tx *sql.Tx, name string) (bool, error) {
}
return true, nil
}
+
+// ServiceCategoryExists confirms whether the service category exists, and an error (if one occurs).
+func ServiceCategoryExists(tx *sql.Tx, name string) (bool, error) {
+ var count int
+ if err := tx.QueryRow("SELECT count(name) FROM service_category AS sc WHERE sc.name=$1", name).Scan(&count); err != nil {
+ return false, fmt.Errorf("error getting service category info: %w", err)
+ }
+ if count == 0 {
+ return false, nil
+ }
+ if count != 1 {
+ return false, fmt.Errorf("getting service category info - expected row count: 1, actual: %d", count)
+ }
+ return true, 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 282311caca..7409dc0aa9 100644
--- a/traffic_ops/traffic_ops_golang/dbhelpers/db_helpers_test.go
+++ b/traffic_ops/traffic_ops_golang/dbhelpers/db_helpers_test.go
@@ -460,3 +460,54 @@ func TestGetSCInfo(t *testing.T) {
})
}
}
+
+func TestServiceCategoryExists(t *testing.T) {
+ var testCases = []struct {
+ description string
+ name string
+ expectedError error
+ exists bool
+ }{
+ {
+ description: "Success: Get valid Service Category",
+ name: "testServiceCategory1",
+ expectedError: nil,
+ exists: true,
+ },
+ {
+ description: "Failure: Service Category not in DB",
+ name: "testServiceCategory2",
+ 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()
+
+ scExists, err := ServiceCategoryExists(db.MustBegin().Tx, testCase.name)
+ if testCase.exists != scExists {
+ t.Errorf("Expected return exists: %t, actual %t", testCase.exists, scExists)
+ }
+
+ if !errors.Is(err, testCase.expectedError) {
+ t.Errorf("ServiceCategoryExists expected: %s, actual: %s", testCase.expectedError, err)
+ }
+ })
+ }
+}
diff --git a/traffic_ops/traffic_ops_golang/routing/routes.go b/traffic_ops/traffic_ops_golang/routing/routes.go
index 725456bf9e..c5fbb8ba47 100644
--- a/traffic_ops/traffic_ops_golang/routing/routes.go
+++ b/traffic_ops/traffic_ops_golang/routing/routes.go
@@ -424,10 +424,10 @@ func Routes(d ServerData) ([]Route, http.Handler, error) {
{Version: api.Version{Major: 5, Minor: 0}, Method: http.MethodDelete, Path: `deliveryservices/{dsid}/regexes/{regexid}?$`, Handler: deliveryservicesregexes.Delete, RequiredPrivLevel: auth.PrivLevelOperations, RequiredPermissions: []string{"DELIVERY-SERVICE:UPDATE", "DELIVERY-SERVICE:READ", "TYPE:READ"}, Authenticated: Authenticated, Middlewares: nil, ID: 424673166331},
//ServiceCategories
- {Version: api.Version{Major: 5, Minor: 0}, Method: http.MethodGet, Path: `service_categories/?$`, Handler: api.ReadHandler(&servicecategory.TOServiceCategory{}), RequiredPrivLevel: auth.PrivLevelReadOnly, RequiredPermissions: []string{"SERVICE-CATEGORY:READ"}, Authenticated: Authenticated, Middlewares: nil, ID: 40851815431},
- {Version: api.Version{Major: 5, Minor: 0}, Method: http.MethodPut, Path: `service_categories/{name}/?$`, Handler: servicecategory.Update, RequiredPrivLevel: auth.PrivLevelOperations, RequiredPermissions: []string{"SERVICE-CATEGORY:UPDATE", "SERVICE-CATEGORY:READ"}, Authenticated: Authenticated, Middlewares: nil, ID: 4063691411},
- {Version: api.Version{Major: 5, Minor: 0}, Method: http.MethodPost, Path: `service_categories/?$`, Handler: api.CreateHandler(&servicecategory.TOServiceCategory{}), RequiredPrivLevel: auth.PrivLevelOperations, RequiredPermissions: []string{"SERVICE-CATEGORY:CREATE", "SERVICE-CATEGORY:READ"}, Authenticated: Authenticated, Middlewares: nil, ID: 4537138011},
- {Version: api.Version{Major: 5, Minor: 0}, Method: http.MethodDelete, Path: `service_categories/{name}$`, Handler: api.DeleteHandler(&servicecategory.TOServiceCategory{}), RequiredPrivLevel: auth.PrivLevelOperations, RequiredPermissions: []string{"SERVICE-CATEGORY:DELETE", "SERVICE-CATEGORY:READ"}, Authenticated: Authenticated, Middlewares: nil, ID: 43253822381},
+ {Version: api.Version{Major: 5, Minor: 0}, Method: http.MethodGet, Path: `service_categories/?$`, Handler: servicecategory.Get, RequiredPrivLevel: auth.PrivLevelReadOnly, RequiredPermissions: []string{"SERVICE-CATEGORY:READ"}, Authenticated: Authenticated, Middlewares: nil, ID: 40851815431},
+ {Version: api.Version{Major: 5, Minor: 0}, Method: http.MethodPut, Path: `service_categories/{name}/?$`, Handler: servicecategory.UpdateServiceCategory, RequiredPrivLevel: auth.PrivLevelOperations, RequiredPermissions: []string{"SERVICE-CATEGORY:UPDATE", "SERVICE-CATEGORY:READ"}, Authenticated: Authenticated, Middlewares: nil, ID: 4063691411},
+ {Version: api.Version{Major: 5, Minor: 0}, Method: http.MethodPost, Path: `service_categories/?$`, Handler: servicecategory.CreateServiceCategory, RequiredPrivLevel: auth.PrivLevelOperations, RequiredPermissions: []string{"SERVICE-CATEGORY:CREATE", "SERVICE-CATEGORY:READ"}, Authenticated: Authenticated, Middlewares: nil, ID: 4537138011},
+ {Version: api.Version{Major: 5, Minor: 0}, Method: http.MethodDelete, Path: `service_categories/{name}$`, Handler: servicecategory.DeleteServiceCategory, RequiredPrivLevel: auth.PrivLevelOperations, RequiredPermissions: []string{"SERVICE-CATEGORY:DELETE", "SERVICE-CATEGORY:READ"}, Authenticated: Authenticated, Middlewares: nil, ID: 43253822381},
//StaticDNSEntries
{Version: api.Version{Major: 5, Minor: 0}, Method: http.MethodGet, Path: `staticdnsentries/?$`, Handler: api.ReadHandler(&staticdnsentry.TOStaticDNSEntry{}), RequiredPrivLevel: auth.PrivLevelReadOnly, RequiredPermissions: []string{"STATIC-DN:READ", "CACHE-GROUP:READ", "DELIVERY-SERVICE:READ"}, Authenticated: Authenticated, Middlewares: nil, ID: 42893947731},
diff --git a/traffic_ops/traffic_ops_golang/servicecategory/servicecategories.go b/traffic_ops/traffic_ops_golang/servicecategory/servicecategories.go
index 8201de1fd5..2559b7e881 100644
--- a/traffic_ops/traffic_ops_golang/servicecategory/servicecategories.go
+++ b/traffic_ops/traffic_ops_golang/servicecategory/servicecategories.go
@@ -23,15 +23,20 @@ import (
"database/sql"
"encoding/json"
"errors"
+ "fmt"
"net/http"
"time"
+ "github.com/apache/trafficcontrol/lib/go-log"
+ "github.com/apache/trafficcontrol/lib/go-rfc"
"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"
+
validation "github.com/go-ozzo/ozzo-validation"
+ "github.com/jmoiron/sqlx"
)
type TOServiceCategory struct {
@@ -187,3 +192,317 @@ WHERE name=$2 RETURNING last_updated`
func deleteQuery() string {
return `DELETE FROM service_category WHERE name=:name`
}
+
+// Get [Version : V5] function Process the *http.Request and writes the response. It uses GetServiceCategory function.
+func Get(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 scList []tc.ServiceCategoryV5
+
+ tx := inf.Tx
+
+ scList, maxTime, code, usrErr, syErr = GetServiceCategory(tx, inf.Params, useIMS, r.Header)
+ if code == http.StatusNotModified {
+ w.WriteHeader(code)
+ api.WriteResp(w, r, []tc.ServiceCategoryV5{})
+ 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, scList)
+}
+
+// GetServiceCategory [Version : V5] receives transactions from Get function and returns service_categories list.
+func GetServiceCategory(tx *sqlx.Tx, params map[string]string, useIMS bool, header http.Header) ([]tc.ServiceCategoryV5, time.Time, int, error, error) {
+ var runSecond bool
+ var maxTime time.Time
+ scList := []tc.ServiceCategoryV5{}
+
+ selectQuery := `SELECT name, last_updated FROM service_category as sc`
+
+ // Query Parameters to Database Query column mappings
+ queryParamsToQueryCols := map[string]dbhelpers.WhereColumnInfo{
+ "name": {Column: "sc.name", Checker: nil},
+ }
+ 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 = TryIfModifiedSinceQuery(header, tx, where, queryValues)
+ if !runSecond {
+ log.Debugln("IMS HIT")
+ return scList, maxTime, http.StatusNotModified, nil, nil
+ }
+ log.Debugln("IMS MISS")
+ } else {
+ log.Debugln("Non IMS request")
+ }
+ query := selectQuery + 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() {
+ sc := tc.ServiceCategoryV5{}
+ if err = rows.Scan(&sc.Name, &sc.LastUpdated); err != nil {
+ return nil, time.Time{}, http.StatusInternalServerError, nil, err
+ }
+ scList = append(scList, sc)
+ }
+
+ return scList, maxTime, http.StatusOK, nil, nil
+}
+
+// TryIfModifiedSinceQuery [Version : V5] function receives transactions and header from GetServiceCategory function and returns bool value if status is not modified.
+func TryIfModifiedSinceQuery(header http.Header, tx *sqlx.Tx, where string, queryValues map[string]interface{}) (bool, time.Time) {
+ var max time.Time
+ var imsDate time.Time
+ var ok bool
+ imsDateHeader := []string{}
+ runSecond := true
+ dontRunSecond := false
+
+ if header == nil {
+ return runSecond, max
+ }
+
+ imsDateHeader = header[rfc.IfModifiedSince]
+ if len(imsDateHeader) == 0 {
+ return runSecond, max
+ }
+
+ if imsDate, ok = rfc.ParseHTTPDate(imsDateHeader[0]); !ok {
+ log.Warnf("IMS request header date '%s' not parsable", imsDateHeader[0])
+ return runSecond, max
+ }
+
+ imsQuery := `SELECT max(last_updated) as t from service_category sc`
+ query := imsQuery + where
+ rows, err := tx.NamedQuery(query, queryValues)
+
+ if errors.Is(err, sql.ErrNoRows) {
+ return dontRunSecond, max
+ }
+
+ if err != nil {
+ log.Warnf("Couldn't get the max last updated time: %v", err)
+ return runSecond, max
+ }
+
+ defer rows.Close()
+ // This should only ever contain one row
+ if rows.Next() {
+ v := time.Time{}
+ if err = rows.Scan(&v); err != nil {
+ log.Warnf("Failed to parse the max time stamp into a struct %v", err)
+ return runSecond, max
+ }
+
+ max = v
+ // The request IMS time is later than the max of (lastUpdated, deleted_time)
+ if imsDate.After(v) {
+ return dontRunSecond, max
+ }
+ }
+ return runSecond, max
+}
+
+// CreateServiceCategory [Version : V5] function creates the service category with the passed name.
+func CreateServiceCategory(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
+
+ sc, readValErr := readAndValidateJsonStruct(r)
+ if readValErr != nil {
+ api.HandleErr(w, r, tx, http.StatusBadRequest, readValErr, nil)
+ return
+ }
+
+ // check if service category already exists
+ var exists bool
+ err := tx.QueryRow(`SELECT EXISTS(SELECT * from service_category where name = $1)`, sc.Name).Scan(&exists)
+ if err != nil {
+ api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, fmt.Errorf("error: %w, when checking if service category with name %s exists", err, sc.Name))
+ return
+ }
+ if exists {
+ api.HandleErr(w, r, tx, http.StatusBadRequest, fmt.Errorf("service category name '%s' already exists.", sc.Name), nil)
+ return
+ }
+
+ // create service category
+ query := `INSERT INTO service_category (name) VALUES ($1) RETURNING name, last_updated`
+ err = tx.QueryRow(query, sc.Name).Scan(&sc.Name, &sc.LastUpdated)
+ if err != nil {
+ if errors.Is(err, sql.ErrNoRows) {
+ api.HandleErr(w, r, tx, http.StatusInternalServerError, fmt.Errorf("error: %w in creating service category with name: %s", err, sc.Name), nil)
+ return
+ }
+ usrErr, sysErr, code := api.ParseDBError(err)
+ api.HandleErr(w, r, tx, code, usrErr, sysErr)
+ return
+ }
+ alerts := tc.CreateAlerts(tc.SuccessLevel, "service category was created.")
+ w.Header().Set("Location", fmt.Sprintf("/api/%d.%d/service_category?name=%s", inf.Version.Major, inf.Version.Minor, sc.Name))
+ api.WriteAlertsObj(w, r, http.StatusCreated, alerts, sc)
+ return
+}
+
+// UpdateServiceCategory [Version : V5] function updates the name of the service category passed.
+func UpdateServiceCategory(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
+ sc, readValErr := readAndValidateJsonStruct(r)
+ if readValErr != nil {
+ api.HandleErr(w, r, tx, http.StatusBadRequest, readValErr, nil)
+ return
+ }
+
+ requestedName := inf.Params["name"]
+ // check if the entity was already updated
+ userErr, sysErr, errCode = api.CheckIfUnModifiedByName(r.Header, inf.Tx, requestedName, "service_category")
+ if userErr != nil || sysErr != nil {
+ api.HandleErr(w, r, tx, errCode, userErr, sysErr)
+ return
+ }
+
+ //update name of a service category
+ query := `UPDATE service_category sc SET
+ name = $1
+ WHERE sc.name = $2
+ RETURNING sc.name, sc.last_updated`
+
+ err := tx.QueryRow(query, sc.Name, requestedName).Scan(&sc.Name, &sc.LastUpdated)
+ if err != nil {
+ if errors.Is(err, sql.ErrNoRows) {
+ api.HandleErr(w, r, tx, http.StatusNotFound, fmt.Errorf("service category with name: %s not found", requestedName), nil)
+ return
+ }
+ usrErr, sysErr, code := api.ParseDBError(err)
+ api.HandleErr(w, r, tx, code, usrErr, sysErr)
+ return
+ }
+ alerts := tc.CreateAlerts(tc.SuccessLevel, "service category was updated")
+ api.WriteAlertsObj(w, r, http.StatusOK, alerts, sc)
+ return
+}
+
+// DeleteServiceCategory [Version : V5] function deletes the service category passed.
+func DeleteServiceCategory(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()
+
+ name := inf.Params["name"]
+ exists, err := dbhelpers.ServiceCategoryExists(tx, name)
+ if err != nil {
+ api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, err)
+ return
+ }
+ if !exists {
+ api.HandleErr(w, r, tx, http.StatusNotFound, fmt.Errorf("no service category exists for name: %s", name), nil)
+ return
+
+ }
+
+ assignedDeliveryService := 0
+ if err := inf.Tx.Get(&assignedDeliveryService, "SELECT count(service_category) FROM deliveryservice d WHERE d.service_category=$1", name); err != nil {
+ api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, fmt.Errorf("service category delete, counting assigned Delivery Service(s): %w", err))
+ return
+ } else if assignedDeliveryService != 0 {
+ api.HandleErr(w, r, tx, http.StatusBadRequest, fmt.Errorf("can not delete a service category with %d assigned Delivery Service(s)", assignedDeliveryService), nil)
+ return
+ }
+
+ res, err := tx.Exec("DELETE FROM service_category AS sc WHERE sc.name=$1", name)
+ 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 service_category: %w", err))
+ return
+ }
+ if rowsAffected == 0 {
+ api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, fmt.Errorf("no rows deleted for service_category"))
+ return
+ }
+
+ alertMessage := fmt.Sprintf("%s was deleted.", name)
+ alerts := tc.CreateAlerts(tc.SuccessLevel, alertMessage)
+ api.WriteAlerts(w, r, http.StatusOK, alerts)
+ return
+}
+
+func readAndValidateJsonStruct(r *http.Request) (tc.ServiceCategoryV5, error) {
+ var sc tc.ServiceCategoryV5
+ if err := json.NewDecoder(r.Body).Decode(&sc); err != nil {
+ userErr := fmt.Errorf("error decoding POST request body into ServiceCategoryV5 struct %w", err)
+ return sc, userErr
+ }
+
+ // 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(sc.Name, validation.Required, rule),
+ })
+ if len(errs) > 0 {
+ userErr := util.JoinErrs(errs)
+ return sc, userErr
+ }
+ return sc, nil
+}
diff --git a/traffic_ops/traffic_ops_golang/servicecategory/servicecategories_test.go b/traffic_ops/traffic_ops_golang/servicecategory/servicecategories_test.go
new file mode 100644
index 0000000000..889457be2f
--- /dev/null
+++ b/traffic_ops/traffic_ops_golang/servicecategory/servicecategories_test.go
@@ -0,0 +1,146 @@
+package servicecategory
+
+/*
+ * 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 (
+ "net/http"
+ "testing"
+ "time"
+
+ "github.com/apache/trafficcontrol/lib/go-rfc"
+
+ "github.com/jmoiron/sqlx"
+ "gopkg.in/DATA-DOG/go-sqlmock.v1"
+)
+
+func TestTryIfModifiedSinceQuery(t *testing.T) {
+
+ type testStruct struct {
+ ifModifiedSince time.Time
+ setHeader bool
+ setImsDateHeader bool
+ expected bool
+ }
+
+ var testData = []testStruct{
+
+ // When header is not set, runSecond must be true
+ {time.Time{}, false, false, true},
+
+ // When header set but header[If-Modified-Since] is not set, runSecond must be true
+ {time.Time{}, true, false, true},
+
+ // When header set and header[If-Modified-Since] is set, but incorrect time is given then runSecond must be true
+ {time.Time{}, true, true, true},
+
+ // When header set and header[If-Modified-Since] is set with correct time, and If-Modified_since < Max(last_Updated) then runSecond must be false
+ {time.Now().AddDate(0, 00, 01), true, true, false},
+
+ // When header set and header[If-Modified-Since] is set with correct time, and If-Modified_since > Max(last_Updated) then runSecond must be true
+ {time.Now().AddDate(0, 00, -01), true, true, true},
+ }
+
+ var header http.Header
+ lastUpdated := time.Now()
+ for i, _ := range testData {
+
+ mockDB, mock, err := sqlmock.New()
+ if err != nil {
+ t.Fatalf("an error '%v' was not expected when opening a stub database connection", err)
+ }
+ defer mockDB.Close()
+
+ db := sqlx.NewDb(mockDB, "sqlmock")
+ defer db.Close()
+
+ if testData[i].setHeader {
+ header = make(http.Header)
+ }
+
+ if testData[i].setImsDateHeader {
+ timeValue := testData[i].ifModifiedSince.Format("Mon, 02 Jan 2006 15:04:05 MST")
+ header.Set(rfc.IfModifiedSince, timeValue)
+ }
+
+ mock.ExpectBegin()
+ rows := sqlmock.NewRows([]string{"t"})
+ rows.AddRow(lastUpdated)
+ mock.ExpectQuery("SELECT").WithArgs().WillReturnRows(rows)
+
+ where := ""
+ queryValues := map[string]interface{}{}
+
+ runSecond, _ := TryIfModifiedSinceQuery(header, db.MustBegin(), where, queryValues)
+
+ if testData[i].expected != runSecond {
+ t.Errorf("Expected runSecond result doesn't match, got: %t; expected: %t", runSecond, testData[i].expected)
+ }
+
+ }
+
+}
+
+func TestGetServiceCategory(t *testing.T) {
+
+ type testStruct struct {
+ useIms bool
+ expected int
+ }
+
+ var testData = []testStruct{
+ // When useIMS is set to false in system Config
+ {false, 200},
+ // When useIMS is set to true in system Config
+ {true, 200},
+ }
+
+ var header http.Header
+ lastUpdated := time.Now()
+ params := map[string]string{}
+
+ for i, _ := range testData {
+ mockDB, mock, err := sqlmock.New()
+ if err != nil {
+ t.Fatalf("an error '%v' was not expected when opening a stub database connection", err)
+ }
+ defer mockDB.Close()
+
+ db := sqlx.NewDb(mockDB, "sqlmock")
+ defer db.Close()
+
+ header = make(http.Header)
+ ifModifiedSince := time.Now().AddDate(0, 00, 01)
+ timeValue := ifModifiedSince.Format("Mon, 02 Jan 2006 15:04:05 MST")
+ header.Set(rfc.IfModifiedSince, timeValue)
+
+ mock.ExpectBegin()
+ rows := sqlmock.NewRows([]string{"name", "last_updated"})
+ rows.AddRow("testObj1", lastUpdated.AddDate(0, 0, -5))
+ mock.ExpectQuery("SELECT name, last_updated FROM service_category").WithArgs().WillReturnRows(rows)
+
+ _, _, code, _, _ := GetServiceCategory(db.MustBegin(), params, testData[i].useIms, header)
+
+ if testData[i].expected != code {
+ t.Errorf("Expected status code result doesn't match, got: %v; expected: %v", code, testData[i].expected)
+ }
+
+ }
+
+}
diff --git a/traffic_ops/v5-client/serviceCategory.go b/traffic_ops/v5-client/serviceCategory.go
index 31cb964040..1bd32f295e 100644
--- a/traffic_ops/v5-client/serviceCategory.go
+++ b/traffic_ops/v5-client/serviceCategory.go
@@ -28,15 +28,14 @@ import (
const apiServiceCategories = "/service_categories"
// CreateServiceCategory creates the given Service Category.
-func (to *Session) CreateServiceCategory(serviceCategory tc.ServiceCategory, opts RequestOptions) (tc.Alerts, toclientlib.ReqInf, error) {
+func (to *Session) CreateServiceCategory(serviceCategory tc.ServiceCategoryV5, opts RequestOptions) (tc.Alerts, toclientlib.ReqInf, error) {
var alerts tc.Alerts
reqInf, err := to.post(apiServiceCategories, opts, serviceCategory, &alerts)
return alerts, reqInf, err
}
-// UpdateServiceCategory replaces the Service Category with the given Name with
-// the one provided.
-func (to *Session) UpdateServiceCategory(name string, serviceCategory tc.ServiceCategory, opts RequestOptions) (tc.Alerts, toclientlib.ReqInf, error) {
+// UpdateServiceCategory replaces the Service Category with the given Name with the one provided.
+func (to *Session) UpdateServiceCategory(name string, serviceCategory tc.ServiceCategoryV5, opts RequestOptions) (tc.Alerts, toclientlib.ReqInf, error) {
route := fmt.Sprintf("%s/%s", apiServiceCategories, url.PathEscape(name))
var alerts tc.Alerts
reqInf, err := to.put(route, opts, serviceCategory, &alerts)
@@ -44,8 +43,8 @@ func (to *Session) UpdateServiceCategory(name string, serviceCategory tc.Service
}
// GetServiceCategories retrieves Service Categories from Traffic Ops.
-func (to *Session) GetServiceCategories(opts RequestOptions) (tc.ServiceCategoriesResponse, toclientlib.ReqInf, error) {
- var data tc.ServiceCategoriesResponse
+func (to *Session) GetServiceCategories(opts RequestOptions) (tc.ServiceCategoriesResponseV5, toclientlib.ReqInf, error) {
+ var data tc.ServiceCategoriesResponseV5
reqInf, err := to.get(apiServiceCategories, opts, &data)
return data, reqInf, err
}