From 73fd4c3fa30184c24da75ae3eedf959bd5ee1dc6 Mon Sep 17 00:00:00 2001 From: Zach Hoffman Date: Fri, 25 Aug 2023 09:32:06 -0600 Subject: [PATCH] Use RFC 3339 for lastUpdated timestamp in /server_server_capabilities --- CHANGELOG.md | 1 + .../api/v5/server_server_capabilities.rst | 10 +- lib/go-tc/server_server_capability.go | 26 ++ .../api/v5/server_server_capabilities_test.go | 20 +- .../testing/api/v5/traffic_control_test.go | 2 +- .../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 | 10 +- 9 files changed, 512 insertions(+), 24 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 783e8a05e0..d974566f55 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -102,6 +102,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/). - [#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 - [#7413](https://github.com/apache/trafficcontrol/issues/7706) *Traffic Ops* Fixes /statuses apis to respond with RFC3339 date/time format +- [#7743](https://github.com/apache/trafficcontrol/issues/7706) *Traffic Ops* Fixes /server_server_capabilities 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/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 6112ea16ac..db03e1bc94 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.ServerV4 `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 7f2c8ecbbd..441faa9871 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..b2b7713bbd 100644 --- a/traffic_ops/v5-client/server_server_capabilities.go +++ b/traffic_ops/v5-client/server_server_capabilities.go @@ -31,14 +31,14 @@ const apiServerServerCapabilities = "/server_server_capabilities" // apiMultipleServersCapabilities is the API version-relative path to the /multiple_servers_capabilities API endpoint. 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) { +// CreateServerServerCapabilityV5 assigns a Server Capability to a Server. +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 } -// DeleteServerServerCapability unassigns a Server Capability from a Server. +// DeleteServerServerCapabilityV5 unassigns a Server Capability from a Server. func (to *Session) DeleteServerServerCapability(serverID int, serverCapability string, opts RequestOptions) (tc.Alerts, toclientlib.ReqInf, error) { if opts.QueryParameters == nil { opts.QueryParameters = url.Values{} @@ -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 }