This is an automated email from the ASF dual-hosted git repository.
zrhoffman pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/trafficcontrol.git
The following commit(s) were added to refs/heads/master by this push:
new 62bd082cb4 Use RFC 3339 for lastUpdated timestamp in
/server_server_capabilities (#7744)
62bd082cb4 is described below
commit 62bd082cb451fd25a5633de03b90077f5070e334
Author: Zach Hoffman <[email protected]>
AuthorDate: Mon Aug 28 13:59:10 2023 -0600
Use RFC 3339 for lastUpdated timestamp in /server_server_capabilities
(#7744)
* Use RFC 3339 for lastUpdated timestamp in /server_server_capabilities
* Match function name in Godoc
---
CHANGELOG.md | 1 +
docs/source/api/v5/server_server_capabilities.rst | 10 +-
lib/go-tc/server_server_capability.go | 26 ++
.../api/v5/server_server_capabilities_test.go | 20 +-
traffic_ops/testing/api/v5/traffic_control_test.go | 2 +-
traffic_ops/traffic_ops_golang/routing/routes.go | 6 +-
.../server/servers_server_capability.go | 282 +++++++++++++++++++++
.../server/servers_server_capability_test.go | 179 +++++++++++++
.../v5-client/server_server_capabilities.go | 6 +-
9 files changed, 510 insertions(+), 22 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6dd1d9e647..a435a14d61 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -100,6 +100,7 @@ The format is based on [Keep a
Changelog](http://keepachangelog.com/en/1.0.0/).
- [#7691](https://github.com/apache/trafficcontrol/pull/7691) *Traffic
Ops* Fixed `/topologies` v5 APIs to respond with `RFC3339` timestamps.
- [#7413](https://github.com/apache/trafficcontrol/issues/7413) *Traffic
Ops* Fixed `/service_category` v5 APIs to respond with `RFC3339` timestamps.
- [#7413](https://github.com/apache/trafficcontrol/issues/7706) *Traffic
Ops* Fixed `/statuses` v5 APIs to respond with `RFC3339` timestamps.
+- [#7743](https://github.com/apache/trafficcontrol/issues/7743) *Traffic Ops*
Fixes /server_server_capabilities apis to respond with RFC3339 date/time format
- [#7730](https://github.com/apache/trafficcontrol/pull/7730) *Traffic
Monitor* Fixed the panic seen in TM when `plugin.system_stats.timestamp_ms`
appears as float and not string.
- [#4393](https://github.com/apache/trafficcontrol/issues/4393) *Traffic Ops*
Fixed the error code and alert structure when TO is queried for a delivery
service with no ssl keys.
- [#7623](https://github.com/apache/trafficcontrol/pull/7623) *Traffic Ops*
Removed TryIfModifiedSinceQuery from servicecategories.go and reused from ims.go
diff --git a/docs/source/api/v5/server_server_capabilities.rst
b/docs/source/api/v5/server_server_capabilities.rst
index 07af380aec..9a626d96a8 100644
--- a/docs/source/api/v5/server_server_capabilities.rst
+++ b/docs/source/api/v5/server_server_capabilities.rst
@@ -66,7 +66,7 @@ Response Structure
------------------
:serverHostName: The server's host name
:serverId: The server's integral, unique identifier
-:lastUpdated: The date and time at which this association between the
server and the :term:`Server Capability` was last updated, in
:ref:`non-rfc-datetime`
+:lastUpdated: The date and time at which this association between the
server and the :term:`Server Capability` was last updated, in :rfc:`3339` format
:serverCapability: The :term:`Server Capability`'s name
.. code-block:: http
@@ -87,13 +87,13 @@ Response Structure
{
"response": [
{
- "lastUpdated": "2019-10-07 22:05:31+00",
+ "lastUpdated": "2023-08-09T14:25:11.017999Z",
"serverHostName": "atlanta-org-1",
"serverId": 260,
"serverCapability": "ram"
},
{
- "lastUpdated": "2019-10-07 22:05:31+00",
+ "lastUpdated": "2023-08-09T14:25:11.017999Z",
"serverHostName": "atlanta-org-2",
"serverId": 261,
"serverCapability": "disk"
@@ -136,7 +136,7 @@ Request Structure
Response Structure
------------------
:serverId: The integral, unique identifier of the newly associated
server
-:lastUpdated: The date and time at which this association between the
server and the :term:`Server Capability` was last updated, in
:ref:`non-rfc-datetime`
+:lastUpdated: The date and time at which this association between the
server and the :term:`Server Capability` was last updated, in :rfc:`3339` format
:serverCapability: The :term:`Server Capability`'s name
.. code-block:: http
@@ -162,7 +162,7 @@ Response Structure
}
],
"response": {
- "lastUpdated": "2019-10-07 22:15:11+00",
+ "lastUpdated": "2023-08-09T14:25:11.017999Z",
"serverId": 1,
"serverCapability": "disk"
}
diff --git a/lib/go-tc/server_server_capability.go
b/lib/go-tc/server_server_capability.go
index 88bb755b89..42a4d7b25b 100644
--- a/lib/go-tc/server_server_capability.go
+++ b/lib/go-tc/server_server_capability.go
@@ -19,6 +19,20 @@ package tc
* under the License.
*/
+import "time"
+
+// ServerServerCapabilityV5 is a ServerServerCapability as it appears in
version 5 of the
+// Traffic Ops API - it always points to the highest minor version in APIv5.
+type ServerServerCapabilityV5 = ServerServerCapabilityV50
+
+// ServerServerCapabilityV50 represents an association between a server
capability and a server.
+type ServerServerCapabilityV50 struct {
+ LastUpdated *time.Time `json:"lastUpdated" db:"last_updated"`
+ Server *string `json:"serverHostName,omitempty"
db:"host_name"`
+ ServerID *int `json:"serverId" db:"server"`
+ ServerCapability *string `json:"serverCapability"
db:"server_capability"`
+}
+
// ServerServerCapability represents an association between a server
capability and a server.
type ServerServerCapability struct {
LastUpdated *TimeNoMod `json:"lastUpdated" db:"last_updated"`
@@ -35,6 +49,18 @@ type MultipleServersCapabilities struct {
PageType string `json:"pageType"`
}
+// ServerServerCapabilitiesResponseV5 is the type of a response from the
+// /api/5.x/server_server_capabilities Traffic Ops endpoint.
+// It always points to the type for the latest minor version of APIv5.
+type ServerServerCapabilitiesResponseV5 = ServerServerCapabilitiesResponseV50
+
+// ServerServerCapabilitiesResponseV50 is the type of a response from Traffic
+// Ops to a request made to its /api/5.0/server_server_capabilities.
+type ServerServerCapabilitiesResponseV50 struct {
+ Response []ServerServerCapabilityV5 `json:"response"`
+ Alerts
+}
+
// ServerServerCapabilitiesResponse is the type of a response from Traffic
// Ops to a request made to its /server_server_capabilities.
type ServerServerCapabilitiesResponse struct {
diff --git a/traffic_ops/testing/api/v5/server_server_capabilities_test.go
b/traffic_ops/testing/api/v5/server_server_capabilities_test.go
index dcab7cf026..f112d34b6a 100644
--- a/traffic_ops/testing/api/v5/server_server_capabilities_test.go
+++ b/traffic_ops/testing/api/v5/server_server_capabilities_test.go
@@ -38,7 +38,7 @@ func TestServerServerCapabilities(t *testing.T) {
currentTime := time.Now().UTC().Add(-15 * time.Second)
tomorrow := currentTime.AddDate(0, 0, 1).Format(time.RFC1123)
- methodTests := utils.TestCase[client.Session,
client.RequestOptions, tc.ServerServerCapability]{
+ methodTests := utils.TestCase[client.Session,
client.RequestOptions, tc.ServerServerCapabilityV5]{
"GET": {
"NOT MODIFIED when NO CHANGES made": {
ClientSession: TOSession,
@@ -102,7 +102,7 @@ func TestServerServerCapabilities(t *testing.T) {
"POST": {
"BAD REQUEST when ALREADY EXISTS": {
ClientSession: TOSession,
- RequestBody: tc.ServerServerCapability{
+ RequestBody:
tc.ServerServerCapabilityV5{
ServerID:
util.Ptr(GetServerID(t, "dtrc-mid-01")()),
ServerCapability:
util.Ptr("disk"),
},
@@ -110,21 +110,21 @@ func TestServerServerCapabilities(t *testing.T) {
},
"BAD REQUEST when MISSING SERVER ID": {
ClientSession: TOSession,
- RequestBody: tc.ServerServerCapability{
+ RequestBody:
tc.ServerServerCapabilityV5{
Server: util.Ptr("disk"),
},
Expectations:
utils.CkRequest(utils.HasError(), utils.HasStatus(http.StatusBadRequest)),
},
"BAD REQUEST when MISSING SERVER CAPABILITY": {
ClientSession: TOSession,
- RequestBody: tc.ServerServerCapability{
+ RequestBody:
tc.ServerServerCapabilityV5{
ServerID:
util.Ptr(GetServerID(t, "dtrc-mid-01")()),
},
Expectations:
utils.CkRequest(utils.HasError(), utils.HasStatus(http.StatusBadRequest)),
},
"NOT FOUND when SERVER CAPABILITY DOESNT
EXIST": {
ClientSession: TOSession,
- RequestBody: tc.ServerServerCapability{
+ RequestBody:
tc.ServerServerCapabilityV5{
ServerID:
util.Ptr(GetServerID(t, "dtrc-mid-01")()),
ServerCapability:
util.Ptr("bogus"),
},
@@ -132,7 +132,7 @@ func TestServerServerCapabilities(t *testing.T) {
},
"NOT FOUND when SERVER DOESNT EXIST": {
ClientSession: TOSession,
- RequestBody: tc.ServerServerCapability{
+ RequestBody:
tc.ServerServerCapabilityV5{
ServerID:
util.Ptr(99999999),
ServerCapability:
util.Ptr("bogus"),
},
@@ -140,7 +140,7 @@ func TestServerServerCapabilities(t *testing.T) {
},
"BAD REQUEST when SERVER TYPE NOT EDGE or MID":
{
ClientSession: TOSession,
- RequestBody: tc.ServerServerCapability{
+ RequestBody:
tc.ServerServerCapabilityV5{
ServerID:
util.Ptr(GetServerID(t, "trafficvault")()),
ServerCapability:
util.Ptr("bogus"),
},
@@ -225,7 +225,7 @@ func TestServerServerCapabilities(t *testing.T) {
func validateServerServerCapabilitiesFields(expectedResp
map[string]interface{}) utils.CkReqFunc {
return func(t *testing.T, _ toclientlib.ReqInf, resp interface{}, _
tc.Alerts, _ error) {
assert.RequireNotNil(t, resp, "Expected Server Server
Capabilities response to not be nil.")
- serverServerCapabilityResponse :=
resp.([]tc.ServerServerCapability)
+ serverServerCapabilityResponse :=
resp.([]tc.ServerServerCapabilityV5)
for field, expected := range expectedResp {
for _, serverServerCapability := range
serverServerCapabilityResponse {
switch field {
@@ -261,7 +261,7 @@ func validateServerServerCapabilitiesSort() utils.CkReqFunc
{
return func(t *testing.T, _ toclientlib.ReqInf, resp interface{},
alerts tc.Alerts, _ error) {
assert.RequireNotNil(t, resp, "Expected Server Server
Capabilities response to not be nil.")
var serverNames []string
- serverServerCapabilityResponse :=
resp.([]tc.ServerServerCapability)
+ serverServerCapabilityResponse :=
resp.([]tc.ServerServerCapabilityV5)
for _, serverServerCapability := range
serverServerCapabilityResponse {
assert.RequireNotNil(t, serverServerCapability.Server,
"Expected Server to not be nil.")
serverNames = append(serverNames,
*serverServerCapability.Server)
@@ -272,7 +272,7 @@ func validateServerServerCapabilitiesSort() utils.CkReqFunc
{
func validateServerServerCapabilitiesPagination(paginationParam string)
utils.CkReqFunc {
return func(t *testing.T, _ toclientlib.ReqInf, resp interface{}, _
tc.Alerts, _ error) {
- paginationResp := resp.([]tc.ServerServerCapability)
+ paginationResp := resp.([]tc.ServerServerCapabilityV5)
opts := client.NewRequestOptions()
opts.QueryParameters.Set("orderby", "serverId")
diff --git a/traffic_ops/testing/api/v5/traffic_control_test.go
b/traffic_ops/testing/api/v5/traffic_control_test.go
index 4154bb7f50..7d9547fa38 100644
--- a/traffic_ops/testing/api/v5/traffic_control_test.go
+++ b/traffic_ops/testing/api/v5/traffic_control_test.go
@@ -46,7 +46,7 @@ type TrafficControl struct {
Regions []tc.RegionV5
`json:"regions"`
Roles []tc.RoleV4
`json:"roles"`
Servers []tc.ServerV5
`json:"servers"`
- ServerServerCapabilities
[]tc.ServerServerCapability `json:"serverServerCapabilities"`
+ ServerServerCapabilities
[]tc.ServerServerCapabilityV5 `json:"serverServerCapabilities"`
ServerCapabilities
[]tc.ServerCapabilityV5 `json:"serverCapabilities"`
ServiceCategories
[]tc.ServiceCategoryV5 `json:"serviceCategories"`
Statuses []tc.StatusV5
`json:"statuses"`
diff --git a/traffic_ops/traffic_ops_golang/routing/routes.go
b/traffic_ops/traffic_ops_golang/routing/routes.go
index 0355a22164..eec686a680 100644
--- a/traffic_ops/traffic_ops_golang/routing/routes.go
+++ b/traffic_ops/traffic_ops_golang/routing/routes.go
@@ -331,9 +331,9 @@ func Routes(d ServerData) ([]Route, http.Handler, error) {
{Version: api.Version{Major: 5, Minor: 0}, Method:
http.MethodDelete, Path: `multiple_servers_capabilities/?$`, Handler:
server.DeleteMultipleServersCapabilities, RequiredPrivLevel:
auth.PrivLevelOperations, RequiredPermissions: []string{"SERVER:READ",
"SERVER:DELETE", "SERVER-CAPABILITY:READ", "SERVER-CAPABILITY:DELETE"},
Authenticated: Authenticated, Middlewares: nil, ID: 407924192781},
//Server Server Capabilities: CRUD
- {Version: api.Version{Major: 5, Minor: 0}, Method:
http.MethodGet, Path: `server_server_capabilities/?$`, Handler:
api.ReadHandler(&server.TOServerServerCapability{}), RequiredPrivLevel:
auth.PrivLevelReadOnly, RequiredPermissions: []string{"SERVER:READ",
"SERVER-CAPABILITY:READ"}, Authenticated: Authenticated, Middlewares: nil, ID:
480023188931},
- {Version: api.Version{Major: 5, Minor: 0}, Method:
http.MethodPost, Path: `server_server_capabilities/?$`, Handler:
api.CreateHandler(&server.TOServerServerCapability{}), RequiredPrivLevel:
auth.PrivLevelOperations, RequiredPermissions: []string{"SERVER:UPDATE",
"SERVER:READ", "SERVER-CAPABILITY:READ"}, Authenticated: Authenticated,
Middlewares: nil, ID: 429316683431},
- {Version: api.Version{Major: 5, Minor: 0}, Method:
http.MethodDelete, Path: `server_server_capabilities/?$`, Handler:
api.DeleteHandler(&server.TOServerServerCapability{}), RequiredPrivLevel:
auth.PrivLevelOperations, RequiredPermissions: []string{"SERVER:UPDATE",
"SERVER:READ", "SERVER-CAPABILITY:READ"}, Authenticated: Authenticated,
Middlewares: nil, ID: 405871405831},
+ {Version: api.Version{Major: 5, Minor: 0}, Method:
http.MethodGet, Path: `server_server_capabilities/?$`, Handler:
api.ReadHandler(&server.TOServerServerCapabilityV5{}), RequiredPrivLevel:
auth.PrivLevelReadOnly, RequiredPermissions: []string{"SERVER:READ",
"SERVER-CAPABILITY:READ"}, Authenticated: Authenticated, Middlewares: nil, ID:
480023188931},
+ {Version: api.Version{Major: 5, Minor: 0}, Method:
http.MethodPost, Path: `server_server_capabilities/?$`, Handler:
api.CreateHandler(&server.TOServerServerCapabilityV5{}), RequiredPrivLevel:
auth.PrivLevelOperations, RequiredPermissions: []string{"SERVER:UPDATE",
"SERVER:READ", "SERVER-CAPABILITY:READ"}, Authenticated: Authenticated,
Middlewares: nil, ID: 429316683431},
+ {Version: api.Version{Major: 5, Minor: 0}, Method:
http.MethodDelete, Path: `server_server_capabilities/?$`, Handler:
api.DeleteHandler(&server.TOServerServerCapabilityV5{}), RequiredPrivLevel:
auth.PrivLevelOperations, RequiredPermissions: []string{"SERVER:UPDATE",
"SERVER:READ", "SERVER-CAPABILITY:READ"}, Authenticated: Authenticated,
Middlewares: nil, ID: 405871405831},
//Status: CRUD
{Version: api.Version{Major: 5, Minor: 0}, Method:
http.MethodGet, Path: `statuses/?$`, Handler:
api.ReadHandler(&status.TOStatusV5{}), RequiredPrivLevel:
auth.PrivLevelReadOnly, RequiredPermissions: []string{"STATUS:READ"},
Authenticated: Authenticated, Middlewares: nil, ID: 424490565631},
diff --git a/traffic_ops/traffic_ops_golang/server/servers_server_capability.go
b/traffic_ops/traffic_ops_golang/server/servers_server_capability.go
index b1a5ee56ab..5c8d0c507a 100644
--- a/traffic_ops/traffic_ops_golang/server/servers_server_capability.go
+++ b/traffic_ops/traffic_ops_golang/server/servers_server_capability.go
@@ -48,6 +48,288 @@ const (
ServerHostNameQueryParam = "serverHostName"
)
+type TOServerServerCapabilityV5 struct {
+ api.APIInfoImpl `json:"-"`
+ tc.ServerServerCapabilityV5
+}
+
+func (ssc *TOServerServerCapabilityV5) SetLastUpdated(t tc.TimeNoMod) {
ssc.LastUpdated = &t.Time }
+func (ssc *TOServerServerCapabilityV5) NewReadObj() interface{} {
+ return &tc.ServerServerCapabilityV5{}
+}
+func (ssc *TOServerServerCapabilityV5) SelectQuery() string { return
scSelectQuery() }
+func (ssc *TOServerServerCapabilityV5) ParamColumns()
map[string]dbhelpers.WhereColumnInfo {
+ return map[string]dbhelpers.WhereColumnInfo{
+ ServerCapabilityQueryParam: dbhelpers.WhereColumnInfo{Column:
"sc.server_capability"},
+ ServerQueryParam: dbhelpers.WhereColumnInfo{Column:
"s.id", Checker: api.IsInt},
+ ServerHostNameQueryParam: dbhelpers.WhereColumnInfo{Column:
"s.host_name"},
+ }
+
+}
+func (ssc *TOServerServerCapabilityV5) DeleteQuery() string { return
scDeleteQuery() }
+func (ssc TOServerServerCapabilityV5) GetKeyFieldsInfo() []api.KeyFieldInfo {
+ return []api.KeyFieldInfo{
+ {Field: ServerQueryParam, Func: api.GetIntKey},
+ {Field: ServerCapabilityQueryParam, Func: api.GetStringKey},
+ }
+}
+
+// Need to satisfy Identifier interface but is a no-op as path does not have
Update
+func (ssc TOServerServerCapabilityV5) GetKeys() (map[string]interface{}, bool)
{
+ if ssc.ServerID == nil {
+ return map[string]interface{}{ServerQueryParam: 0}, false
+ }
+ if ssc.ServerCapability == nil {
+ return map[string]interface{}{ServerCapabilityQueryParam: 0},
false
+ }
+ return map[string]interface{}{
+ ServerQueryParam: *ssc.ServerID,
+ ServerCapabilityQueryParam: *ssc.ServerCapability,
+ }, true
+}
+
+func (ssc *TOServerServerCapabilityV5) SetKeys(keys map[string]interface{}) {
+ sID, _ := keys[ServerQueryParam].(int)
+ ssc.ServerID = &sID
+
+ sc, _ := keys[ServerCapabilityQueryParam].(string)
+ ssc.ServerCapability = &sc
+}
+
+func (ssc *TOServerServerCapabilityV5) GetAuditName() string {
+ if ssc.ServerCapability != nil {
+ return *ssc.ServerCapability
+ }
+ return "unknown"
+}
+
+func (ssc *TOServerServerCapabilityV5) GetType() string {
+ return "server server_capability"
+}
+
+// Validate fulfills the api.Validator interface.
+func (ssc TOServerServerCapabilityV5) Validate() (error, error) {
+ errs := validation.Errors{
+ ServerQueryParam: validation.Validate(ssc.ServerID,
validation.Required),
+ ServerCapabilityQueryParam:
validation.Validate(ssc.ServerCapability, validation.Required),
+ }
+
+ return util.JoinErrs(tovalidate.ToErrors(errs)), nil
+}
+
+func (ssc *TOServerServerCapabilityV5) Read(h http.Header, useIMS bool)
([]interface{}, error, error, int, *time.Time) {
+ api.DefaultSort(ssc.APIInfo(), "serverHostName")
+ return api.GenericRead(h, ssc, useIMS)
+}
+func (v *TOServerServerCapabilityV5) SelectMaxLastUpdatedQuery(where, orderBy,
pagination, tableName string) string {
+ return `SELECT max(t) from (
+ SELECT max(sc.last_updated) as t from server_server_capability
sc
+JOIN server s ON sc.server = s.id ` + where + orderBy + pagination +
+ ` UNION ALL
+ select max(last_updated) as t from last_deleted l where
l.table_name='server_server_capability') as res`
+}
+
+func (ssc *TOServerServerCapabilityV5) Delete() (error, error, int) {
+ tenantIDs, err := tenant.GetUserTenantIDListTx(ssc.APIInfo().Tx.Tx,
ssc.APIInfo().User.TenantID)
+ if err != nil {
+ return nil, fmt.Errorf("deleting servers_server_capability:
%v", err), http.StatusInternalServerError
+ }
+ accessibleTenants := make(map[int]struct{}, len(tenantIDs))
+ for _, id := range tenantIDs {
+ accessibleTenants[id] = struct{}{}
+ }
+ userErr, sysErr, status :=
checkTopologyBasedDSRequiredCapabilitiesV5(ssc, accessibleTenants)
+ if userErr != nil || sysErr != nil {
+ return userErr, sysErr, status
+ }
+
+ userErr, sysErr, status = checkDSRequiredCapabilitiesV5(ssc,
accessibleTenants)
+ if userErr != nil || sysErr != nil {
+ return userErr, sysErr, status
+ }
+
+ if ssc.ServerID != nil {
+ cdnName, err :=
dbhelpers.GetCDNNameFromServerID(ssc.APIInfo().Tx.Tx, int64(*ssc.ServerID))
+ if err != nil {
+ return nil, err, http.StatusInternalServerError
+ }
+ userErr, sysErr, errCode :=
dbhelpers.CheckIfCurrentUserCanModifyCDN(ssc.APIInfo().Tx.Tx, string(cdnName),
ssc.APIInfo().User.UserName)
+ if userErr != nil || sysErr != nil {
+ return userErr, sysErr, errCode
+ }
+ }
+ return api.GenericDelete(ssc)
+}
+
+func checkTopologyBasedDSRequiredCapabilitiesV5(ssc
*TOServerServerCapabilityV5, accessibleTenants map[int]struct{}) (error, error,
int) {
+ dsRows, err :=
ssc.APIInfo().Tx.Tx.Query(getTopologyBasedDSesReqCapQuery(), ssc.ServerID,
ssc.ServerCapability)
+ if err != nil {
+ return nil, fmt.Errorf("querying topology-based DSes with the
required capability %s: %v", *ssc.ServerCapability, err),
http.StatusInternalServerError
+ }
+ defer log.Close(dsRows, "closing dsRows in
checkTopologyBasedDSRequiredCapabilitiesV5")
+
+ xmlidToTopology := make(map[string]string)
+ xmlidToTenantID := make(map[string]int)
+ xmlidToReqCaps := make(map[string][]string)
+ for dsRows.Next() {
+ xmlID := ""
+ topology := ""
+ tenantID := 0
+ reqCaps := []string{}
+ if err := dsRows.Scan(&xmlID, &topology, &tenantID,
pq.Array(&reqCaps)); err != nil {
+ return nil, fmt.Errorf("scanning dsRows in
checkTopologyBasedDSRequiredCapabilitiesV5: %v", err),
http.StatusInternalServerError
+ }
+ xmlidToTenantID[xmlID] = tenantID
+ xmlidToTopology[xmlID] = topology
+ xmlidToReqCaps[xmlID] = reqCaps
+ }
+ if len(xmlidToTopology) == 0 {
+ return nil, nil, http.StatusOK
+ }
+
+ serverRows, err :=
ssc.APIInfo().Tx.Tx.Query(getServerCapabilitiesOfCachegoupQuery(),
ssc.ServerID, ssc.ServerCapability)
+ if err != nil {
+ return nil, fmt.Errorf("querying server capabilitites of server
%d's cachegroup: %v", *ssc.ServerID, err), http.StatusInternalServerError
+ }
+ defer log.Close(serverRows, "closing serverRows in
checkTopologyBasedDSRequiredCapabilitiesV5")
+
+ serverIDToCapabilities := make(map[int]map[string]struct{})
+ for serverRows.Next() {
+ serverID := 0
+ capabilities := []string{}
+ if err := serverRows.Scan(&serverID, pq.Array(&capabilities));
err != nil {
+ return nil, fmt.Errorf("scanning serverRows in
checkTopologyBasedDSRequiredCapabilitiesV5: %v", err),
http.StatusInternalServerError
+ }
+ serverIDToCapabilities[serverID] = make(map[string]struct{})
+ for _, c := range capabilities {
+ serverIDToCapabilities[serverID][c] = struct{}{}
+ }
+ }
+
+ unsatisfiedDSes := []string{}
+ for ds, dsReqCaps := range xmlidToReqCaps {
+ dsIsSatisfied := false
+ for _, serverCaps := range serverIDToCapabilities {
+ serverHasCapabilities := true
+ for _, dsReqCap := range dsReqCaps {
+ if _, ok := serverCaps[dsReqCap]; !ok {
+ serverHasCapabilities = false
+ break
+ }
+ }
+ if serverHasCapabilities {
+ dsIsSatisfied = true
+ break
+ }
+ }
+ if !dsIsSatisfied {
+ unsatisfiedDSes = append(unsatisfiedDSes, ds)
+ }
+ }
+ if len(unsatisfiedDSes) == 0 {
+ return nil, nil, http.StatusOK
+ }
+
+ dsStrings := make([]string, 0, len(unsatisfiedDSes))
+ for _, ds := range unsatisfiedDSes {
+ if _, ok := accessibleTenants[xmlidToTenantID[ds]]; ok {
+ dsStrings = append(dsStrings, "(xml_id = "+ds+",
topology = "+xmlidToTopology[ds]+")")
+ }
+ }
+ return fmt.Errorf("this capability is required by delivery services,
but there are no other servers in this server's cachegroup to satisfy them %s",
strings.Join(dsStrings, ", ")), nil, http.StatusBadRequest
+}
+
+func checkDSRequiredCapabilitiesV5(ssc *TOServerServerCapabilityV5,
accessibleTenants map[int]struct{}) (error, error, int) {
+ // Ensure that the user is not removing a server capability from the
server
+ // that is required by the delivery services the server is assigned to
(if applicable)
+ dsIDs := []int64{}
+ if err := ssc.APIInfo().Tx.Tx.QueryRow(checkDSReqCapQuery(),
ssc.ServerID, ssc.ServerCapability).Scan(pq.Array(&dsIDs)); err != nil {
+ return nil, fmt.Errorf("checking removing server server
capability would still suffice delivery service requried capabilites: %v",
err), http.StatusInternalServerError
+ }
+
+ if len(dsIDs) > 0 {
+ return ssc.buildDSReqCapError(dsIDs, accessibleTenants)
+ }
+ return nil, nil, http.StatusOK
+}
+
+func (ssc *TOServerServerCapabilityV5) buildDSReqCapError(dsIDs []int64,
accessibleTenants map[int]struct{}) (error, error, int) {
+
+ dsTenantIDs, err := getDSTenantIDsByIDs(ssc.APIInfo().Tx, dsIDs)
+ if err != nil {
+ return nil, err, http.StatusInternalServerError
+ }
+
+ authDSIDs := []string{}
+
+ for _, dsTenantID := range dsTenantIDs {
+ if _, ok := accessibleTenants[dsTenantID.TenantID]; ok {
+ if ok {
+ authDSIDs = append(authDSIDs,
strconv.Itoa(dsTenantID.ID))
+ }
+ continue
+ }
+ }
+
+ dsStr := "delivery services"
+ if len(authDSIDs) > 0 {
+ dsStr = fmt.Sprintf("the delivery services %v",
strings.Join(authDSIDs, ","))
+ }
+ return fmt.Errorf("cannot remove the capability %v from the server %v
as the server is assigned to %v that require it", *ssc.ServerCapability,
*ssc.ServerID, dsStr), nil, http.StatusBadRequest
+}
+
+func (ssc *TOServerServerCapabilityV5) Create() (error, error, int) {
+ tx := ssc.APIInfo().Tx
+
+ // Check existence prior to checking type
+ _, exists, err := dbhelpers.GetServerNameFromID(tx.Tx,
int64(*ssc.ServerID))
+ if err != nil {
+ return nil, err, http.StatusInternalServerError
+ }
+ if !exists {
+ return fmt.Errorf("server %v does not exist", *ssc.ServerID),
nil, http.StatusNotFound
+ }
+
+ // Ensure type is correct
+ var sidList []int64
+ sidList = append(sidList, int64(*ssc.ServerID))
+ errCode, userErr, sysErr := checkServerType(tx.Tx, sidList)
+ if userErr != nil || sysErr != nil {
+ return userErr, sysErr, errCode
+
+ }
+
+ cdnName, err := dbhelpers.GetCDNNameFromServerID(tx.Tx,
int64(*ssc.ServerID))
+ if err != nil {
+ return nil, err, http.StatusInternalServerError
+ }
+ userErr, sysErr, errCode =
dbhelpers.CheckIfCurrentUserCanModifyCDN(tx.Tx, string(cdnName),
ssc.APIInfo().User.UserName)
+ if userErr != nil || sysErr != nil {
+ return userErr, sysErr, errCode
+ }
+
+ resultRows, err := tx.NamedQuery(scInsertQuery(), ssc)
+ if err != nil {
+ return api.ParseDBError(err)
+ }
+ defer resultRows.Close()
+
+ rowsAffected := 0
+ for resultRows.Next() {
+ rowsAffected++
+ if err := resultRows.StructScan(&ssc); err != nil {
+ return nil, errors.New(ssc.GetType() + " create
scanning: " + err.Error()), http.StatusInternalServerError
+ }
+ }
+ if rowsAffected == 0 {
+ return nil, errors.New(ssc.GetType() + " create: no " +
ssc.GetType() + " was inserted, no rows was returned"),
http.StatusInternalServerError
+ } else if rowsAffected > 1 {
+ return nil, errors.New("too many rows returned from " +
ssc.GetType() + " insert"), http.StatusInternalServerError
+ }
+
+ return nil, nil, http.StatusOK
+}
+
type (
TOServerServerCapability struct {
api.APIInfoImpl `json:"-"`
diff --git
a/traffic_ops/traffic_ops_golang/server/servers_server_capability_test.go
b/traffic_ops/traffic_ops_golang/server/servers_server_capability_test.go
index a6ee9e46b2..89c7dc34ae 100644
--- a/traffic_ops/traffic_ops_golang/server/servers_server_capability_test.go
+++ b/traffic_ops/traffic_ops_golang/server/servers_server_capability_test.go
@@ -35,6 +35,185 @@ import (
"gopkg.in/DATA-DOG/go-sqlmock.v1"
)
+func getTestSSCsV5() []tc.ServerServerCapabilityV5 {
+ sscs := []tc.ServerServerCapabilityV5{}
+ testSSC := tc.ServerServerCapabilityV5{
+ LastUpdated: util.Ptr(time.Now()),
+ Server: util.StrPtr("test"),
+ ServerID: util.IntPtr(1),
+ ServerCapability: util.StrPtr("test"),
+ }
+ sscs = append(sscs, testSSC)
+
+ testSSC1 := testSSC
+ testSSC1.ServerCapability = util.Ptr("blah")
+ sscs = append(sscs, testSSC1)
+
+ return sscs
+}
+
+func TestReadSCsV5(t *testing.T) {
+ 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()
+
+ testSCs := getTestSSCsV5()
+ rows := sqlmock.NewRows([]string{"server_capability", "server",
"last_updated"})
+
+ for _, ts := range testSCs {
+ rows = rows.AddRow(
+ ts.ServerCapability,
+ ts.ServerID,
+ ts.LastUpdated)
+ }
+ mock.ExpectBegin()
+ mock.ExpectQuery("SELECT").WillReturnRows(rows)
+ mock.ExpectCommit()
+
+ reqInfo := api.APIInfo{Tx: db.MustBegin(), Params:
map[string]string{"serverId": "1"}}
+ obj := TOServerServerCapabilityV5{
+ api.APIInfoImpl{ReqInfo: &reqInfo},
+ tc.ServerServerCapabilityV5{},
+ }
+ sscs, userErr, sysErr, _, _ := obj.Read(nil, false)
+ if userErr != nil || sysErr != nil {
+ t.Errorf("Read expected: no errors, actual: %v %v", userErr,
sysErr)
+ }
+
+ if len(sscs) != 2 {
+ t.Errorf("ServerServerCapabilityV5.Read expected: len(scs) ==
1, actual: %v", len(sscs))
+ }
+}
+
+func TestInterfacesV5(t *testing.T) {
+ var i interface{}
+ i = &TOServerServerCapabilityV5{}
+
+ if _, ok := i.(api.Creator); !ok {
+ t.Errorf("ServerServerCapabilityV5 must be Creator")
+ }
+ if _, ok := i.(api.Reader); !ok {
+ t.Errorf("ServerServerCapabilityV5 must be Reader")
+ }
+ if _, ok := i.(api.Deleter); !ok {
+ t.Errorf("ServerServerCapabilityV5 must be Deleter")
+ }
+ if _, ok := i.(api.Identifier); !ok {
+ t.Errorf("ServerServerCapabilityV5 must be Identifier")
+ }
+}
+
+func TestFuncsV5(t *testing.T) {
+ if strings.Index(scSelectQuery(), "SELECT") != 0 {
+ t.Errorf("expected selectQuery to start with SELECT")
+ }
+ if strings.Index(scInsertQuery(), "INSERT") != 0 {
+ t.Errorf("expected insertQuery to start with INSERT")
+ }
+ if strings.Index(scDeleteQuery(), "DELETE") != 0 {
+ t.Errorf("expected deleteQuery to start with DELETE")
+ }
+}
+
+func TestValidateV5(t *testing.T) {
+ testSSC := tc.ServerServerCapabilityV5{
+ LastUpdated: util.Ptr(time.Now()),
+ Server: util.StrPtr("test1"),
+ ServerID: util.IntPtr(1),
+ ServerCapability: util.StrPtr("abc"),
+ }
+ testTOSSC := TOServerServerCapabilityV5{
+ ServerServerCapabilityV5: testSSC,
+ }
+
+ err, _ := testTOSSC.Validate()
+ errs := test.SortErrors(test.SplitErrors(err))
+
+ if len(errs) > 0 {
+ t.Errorf(`expected no errors, got %v`, errs)
+ }
+}
+
+func TestCheckExistingServerV5(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{"host_name"})
+ rows.AddRow("test")
+ mock.ExpectQuery("SELECT host_name").WithArgs(1).WillReturnRows(rows)
+
+ rows1 := sqlmock.NewRows([]string{"name"})
+ rows1.AddRow("ALL")
+ mock.ExpectQuery("SELECT name").WithArgs(1).WillReturnRows(rows1)
+
+ rows2 := sqlmock.NewRows([]string{"username", "soft",
"shared_usernames"})
+ rows2.AddRow("user1", false, []byte("{}"))
+ mock.ExpectQuery("SELECT c.username,
c.soft").WithArgs("ALL").WillReturnRows(rows2)
+ mock.ExpectCommit()
+
+ testSCCs := getTestSSCsV5()
+ var sids []int64
+ sids = append(sids, int64(*testSCCs[0].ServerID))
+ code, usrErr, sysErr := checkExistingServer(db.MustBegin().Tx, sids,
"user1")
+ if usrErr != nil {
+ t.Errorf("server not found, error:%v", usrErr)
+ }
+ if sysErr != nil {
+ t.Errorf("unable to check if server exists, error:%v", sysErr)
+ }
+ if code != http.StatusOK {
+ t.Errorf("existing server check failed, expected:%d, got:%d",
http.StatusOK, code)
+ }
+}
+
+func TestCheckServerTypeV5(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()
+
+ testSCCs := getTestSSCsV5()
+ testSCCs[1].ServerID = util.Ptr(2)
+ testSCCs[1].Server = util.Ptr("foo")
+
+ mock.ExpectBegin()
+ rows := sqlmock.NewRows([]string{"array_agg"})
+ var sids []int64
+ for i, _ := range testSCCs {
+ sids = append(sids, int64(*testSCCs[i].ServerID))
+ }
+ rows.AddRow([]byte("{1,2}"))
+ mock.ExpectQuery("SELECT
array_agg").WithArgs(pq.Array(sids)).WillReturnRows(rows)
+ mock.ExpectCommit()
+
+ code, usrErr, sysErr := checkServerType(db.MustBegin().Tx, sids)
+ if usrErr != nil {
+ t.Errorf("mismatch in server type, error:%v", usrErr)
+ }
+ if sysErr != nil {
+ t.Errorf("unable to check if server type exists, error:%v",
sysErr)
+ }
+ if code != http.StatusOK {
+ t.Errorf("server type check failed, expected:%d, got:%d",
http.StatusOK, code)
+ }
+}
+
func getTestSSCs() []tc.ServerServerCapability {
sscs := []tc.ServerServerCapability{}
testSSC := tc.ServerServerCapability{
diff --git a/traffic_ops/v5-client/server_server_capabilities.go
b/traffic_ops/v5-client/server_server_capabilities.go
index 15bd1efbc2..d0d125e44d 100644
--- a/traffic_ops/v5-client/server_server_capabilities.go
+++ b/traffic_ops/v5-client/server_server_capabilities.go
@@ -32,7 +32,7 @@ const apiServerServerCapabilities =
"/server_server_capabilities"
const apiMultipleServersCapabilities = "/multiple_servers_capabilities"
// CreateServerServerCapability assigns a Server Capability to a Server.
-func (to *Session) CreateServerServerCapability(ssc tc.ServerServerCapability,
opts RequestOptions) (tc.Alerts, toclientlib.ReqInf, error) {
+func (to *Session) CreateServerServerCapability(ssc
tc.ServerServerCapabilityV5, opts RequestOptions) (tc.Alerts,
toclientlib.ReqInf, error) {
var alerts tc.Alerts
reqInf, err := to.post(apiServerServerCapabilities, opts, ssc, &alerts)
return alerts, reqInf, err
@@ -52,8 +52,8 @@ func (to *Session) DeleteServerServerCapability(serverID int,
serverCapability s
// GetServerServerCapabilities retrieves a list of Server Capabilities that are
// assigned to Servers.
-func (to *Session) GetServerServerCapabilities(opts RequestOptions)
(tc.ServerServerCapabilitiesResponse, toclientlib.ReqInf, error) {
- var resp tc.ServerServerCapabilitiesResponse
+func (to *Session) GetServerServerCapabilities(opts RequestOptions)
(tc.ServerServerCapabilitiesResponseV5, toclientlib.ReqInf, error) {
+ var resp tc.ServerServerCapabilitiesResponseV5
reqInf, err := to.get(apiServerServerCapabilities, opts, &resp)
return resp, reqInf, err
}