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 9604dc2f01 Added description field to server capabilities (#7213)
9604dc2f01 is described below
commit 9604dc2f0150c3c12cad81fdcbfdac5ec6dfee57
Author: Rima Shah <[email protected]>
AuthorDate: Fri Dec 16 14:01:54 2022 -0700
Added description field to server capabilities (#7213)
* Added description field to server capabilities
* Added description field to server capabilities docs
* Removed required checks for description field
* Addressed review comments.
* Updated cruder functionality and routes
* Using description in TP tests and updated sort order in SC table
* Removed description from v3
* Updated test in v4/v5 to new struct V41
* Fixed go vet and CIAB and TP
* Added descriptive tests for functions
* Fixed Tests(v4/v5)
* Response is not null
* Not break v4 client methods
* Added DeleteServerCapability() for 5
* TP fixes
* Addressed review comments
* Set Location header for CreateServerCapabilities() and added check for
sql.ErrNoRows
* Added unit test in db_helpers
* Addressed review comments-1
* Updated timestamp on db migration files.
* Reverting error message punctuation since it causes TP errors.
---
CHANGELOG.md | 1 +
docs/source/api/v4/server_capabilities.rst | 29 ++-
docs/source/api/v5/server_capabilities.rst | 14 +-
infrastructure/cdn-in-a-box/enroller/enroller.go | 6 +-
lib/go-tc/server_capabilities.go | 21 ++
...1500_add_description_field_in_sc_table.down.sql | 18 ++
...121500_add_description_field_in_sc_table.up.sql | 18 ++
.../testing/api/v4/server_capabilities_test.go | 49 +++--
traffic_ops/testing/api/v4/traffic_control_test.go | 2 +-
.../testing/api/v5/server_capabilities_test.go | 42 ++--
traffic_ops/testing/api/v5/tc-fixtures.json | 18 +-
traffic_ops/testing/api/v5/traffic_control_test.go | 2 +-
.../traffic_ops_golang/dbhelpers/db_helpers.go | 15 ++
.../dbhelpers/db_helpers_test.go | 52 +++++
traffic_ops/traffic_ops_golang/routing/routes.go | 13 +-
.../servercapability/servercapability.go | 212 ++++++++++++++++++++-
traffic_ops/v4-client/servercapability.go | 28 +++
traffic_ops/v5-client/servercapability.go | 12 +-
.../form.serverCapability.tpl.html | 6 +
.../table.serverCapabilities.tpl.html | 4 +-
.../test/integration/Data/servercapabilities.ts | 16 ++
.../PageObjects/ServerCapabilitiesPage.po.ts | 4 +-
.../integration/specs/ServerCapabilities.spec.ts | 2 +-
23 files changed, 527 insertions(+), 57 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4429531560..dffadde926 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,7 @@ The format is based on [Keep a
Changelog](http://keepachangelog.com/en/1.0.0/).
## [unreleased]
### Added
+- [#6234](https://github.com/apache/trafficcontrol/issues/6234) *Traffic Ops,
Traffic Portal* Added description field to Server Capabilities
- [#6033](https://github.com/apache/trafficcontrol/issues/6033) *Traffic Ops,
Traffic Portal* Added ability to assign multiple servers per capability.
- [#7081](https://github.com/apache/trafficcontrol/issues/7081) *Traffic
Router* Added better log messages for TR connection exceptions.
- [#7089](https://github.com/apache/trafficcontrol/issues/7089) *Traffic
Router* Added the ability to specify HTTPS certificate attributes.
diff --git a/docs/source/api/v4/server_capabilities.rst
b/docs/source/api/v4/server_capabilities.rst
index 2f8915ea9d..a2472f83ad 100644
--- a/docs/source/api/v4/server_capabilities.rst
+++ b/docs/source/api/v4/server_capabilities.rst
@@ -50,6 +50,10 @@ Request Structure
Response Structure
------------------
:name: The name of this :term:`Server Capability`
+:description: The description of this :term:`Server Capability`
+
+ .. versionadded:: 4.1
+
:lastUpdated: The date and time at which this :term:`Server Capability` was
last updated, in ISO-like format
.. code-block:: http
@@ -71,6 +75,7 @@ Response Structure
"response": [
{
"name": "RAM",
+ "description": "ram server capability",
"lastUpdated": "2019-10-07 20:38:24+00"
}
]
@@ -88,6 +93,10 @@ Create a new :term:`Server Capability`.
Request Structure
-----------------
:name: The name of the :term:`Server Capability`
+:description: The description of this :term:`Server Capability`
+
+ .. versionadded:: 4.1
+
.. code-block:: http
:caption: Request Example
@@ -101,12 +110,17 @@ Request Structure
Content-Type: application/json
{
- "name": "RAM"
+ "name": "RAM",
+ "description": "ram server capability",
}
Response Structure
------------------
:name: The name of this :term:`Server Capability`
+:description: The description of this :term:`Server Capability`
+
+ .. versionadded:: 4.1
+
:lastUpdated: The date and time at which this :term:`Server Capability` was
last updated, in ISO-like format
.. code-block:: http
@@ -133,6 +147,7 @@ Response Structure
],
"response": {
"name": "RAM",
+ "description": "ram server capability",
"lastUpdated": "2019-10-07 22:10:00+00"
}
}
@@ -149,6 +164,10 @@ Update an existing :term:`Server Capability`.
Request Structure
-----------------
:name: The name of the :term:`Server Capability`
+:description: The description of this :term:`Server Capability`
+
+ .. versionadded:: 4.1
+
.. code-block:: http
:caption: Request Example
@@ -162,12 +181,17 @@ Request Structure
Content-Type: application/json
{
- "name": "HDD"
+ "name": "HDD",
+ "description": "HDD server capability"
}
Response Structure
------------------
:name: The name of this :term:`Server Capability`
+:description: The description of this :term:`Server Capability`
+
+ .. versionadded:: 4.1
+
:lastUpdated: The date and time at which this :term:`Server Capability` was
last updated, in ISO-like format
.. code-block:: http
@@ -194,6 +218,7 @@ Response Structure
],
"response": {
"name": "HDD",
+ "description": "HDD server capability",
"lastUpdated": "2021-03-03 21:22:08+00"
}
}
diff --git a/docs/source/api/v5/server_capabilities.rst
b/docs/source/api/v5/server_capabilities.rst
index a6d99a6145..4827ca8ad7 100644
--- a/docs/source/api/v5/server_capabilities.rst
+++ b/docs/source/api/v5/server_capabilities.rst
@@ -50,6 +50,7 @@ Request Structure
Response Structure
------------------
:name: The name of this :term:`Server Capability`
+:description: The description of this :term:`Server Capability`
:lastUpdated: The date and time at which this :term:`Server Capability` was
last updated, in ISO-like format
.. code-block:: http
@@ -71,6 +72,7 @@ Response Structure
"response": [
{
"name": "RAM",
+ "description": "ram server capability",
"lastUpdated": "2019-10-07 20:38:24+00"
}
]
@@ -88,6 +90,7 @@ Create a new :term:`Server Capability`.
Request Structure
-----------------
:name: The name of the :term:`Server Capability`
+:description: The description of this :term:`Server Capability`
.. code-block:: http
:caption: Request Example
@@ -101,12 +104,14 @@ Request Structure
Content-Type: application/json
{
- "name": "RAM"
+ "name": "RAM",
+ "description": "ram server capability",
}
Response Structure
------------------
:name: The name of this :term:`Server Capability`
+:description: The description of this :term:`Server Capability`
:lastUpdated: The date and time at which this :term:`Server Capability` was
last updated, in ISO-like format
.. code-block:: http
@@ -133,6 +138,7 @@ Response Structure
],
"response": {
"name": "RAM",
+ "description": "ram server capability",
"lastUpdated": "2019-10-07 22:10:00+00"
}
}
@@ -149,6 +155,7 @@ Update an existing :term:`Server Capability`.
Request Structure
-----------------
:name: The name of the :term:`Server Capability`
+:description: The description of this :term:`Server Capability`
.. code-block:: http
:caption: Request Example
@@ -162,12 +169,14 @@ Request Structure
Content-Type: application/json
{
- "name": "HDD"
+ "name": "HDD",
+ "description": "HDD server capability"
}
Response Structure
------------------
:name: The name of this :term:`Server Capability`
+:description: The description of this :term:`Server Capability`
:lastUpdated: The date and time at which this :term:`Server Capability` was
last updated, in ISO-like format
.. code-block:: http
@@ -194,6 +203,7 @@ Response Structure
],
"response": {
"name": "HDD",
+ "description": "HDD server capability",
"lastUpdated": "2021-03-03 21:22:08+00"
}
}
diff --git a/infrastructure/cdn-in-a-box/enroller/enroller.go
b/infrastructure/cdn-in-a-box/enroller/enroller.go
index 0633ab51bb..c3a031afed 100644
--- a/infrastructure/cdn-in-a-box/enroller/enroller.go
+++ b/infrastructure/cdn-in-a-box/enroller/enroller.go
@@ -771,10 +771,10 @@ func enrollServer(toSession *session, r io.Reader) error {
return err
}
-// enrollServerCapability takes a json file and creates a ServerCapability
object using the TO API
+// enrollServerCapability takes a json file and creates a ServerCapabilityV41
object using the TO API
func enrollServerCapability(toSession *session, r io.Reader) error {
dec := json.NewDecoder(r)
- var s tc.ServerCapability
+ var s tc.ServerCapabilityV41
err := dec.Decode(&s)
if err != nil {
err = fmt.Errorf("error decoding Server Capability: %v", err)
@@ -782,7 +782,7 @@ func enrollServerCapability(toSession *session, r
io.Reader) error {
return err
}
- alerts, _, err := toSession.CreateServerCapability(s,
client.RequestOptions{})
+ alerts, _, err := toSession.CreateServerCapabilityV41(s,
client.RequestOptions{})
if err != nil {
err = fmt.Errorf("error creating Server Capability: %v -
alerts: %+v", err, alerts.Alerts)
log.Infoln(err)
diff --git a/lib/go-tc/server_capabilities.go b/lib/go-tc/server_capabilities.go
index 45ab68b2c2..328cb6933f 100644
--- a/lib/go-tc/server_capabilities.go
+++ b/lib/go-tc/server_capabilities.go
@@ -25,14 +25,35 @@ type ServerCapabilitiesResponse struct {
Alerts
}
+// ServerCapabilitiesResponseV41 contains the result data from a GET(v4.1 and
above) /server_capabilities request.
+type ServerCapabilitiesResponseV41 struct {
+ Response []ServerCapabilityV41 `json:"response"`
+ Alerts
+}
+
// ServerCapability contains information about a given ServerCapability in
Traffic Ops.
type ServerCapability struct {
Name string `json:"name" db:"name"`
LastUpdated *TimeNoMod `json:"lastUpdated" db:"last_updated"`
}
+// ServerCapabilityV4 is an alias for the latest minor version for the major
version 4.
+type ServerCapabilityV4 ServerCapabilityV41
+
+// ServerCapabilityV41 contains information (in-addition to description) about
a given ServerCapability in Traffic Ops.
+type ServerCapabilityV41 struct {
+ ServerCapability
+ Description string `json:"description" db:"description"`
+}
+
// ServerCapabilityDetailResponse contains the result data from a POST
/server_capabilities request.
type ServerCapabilityDetailResponse struct {
Response ServerCapability `json:"response"`
Alerts
}
+
+// ServerCapabilityDetailResponseV41 contains the result data from a POST(v4.1
and above) /server_capabilities request.
+type ServerCapabilityDetailResponseV41 struct {
+ Response ServerCapabilityV41 `json:"response"`
+ Alerts
+}
diff --git
a/traffic_ops/app/db/migrations/2022121612121500_add_description_field_in_sc_table.down.sql
b/traffic_ops/app/db/migrations/2022121612121500_add_description_field_in_sc_table.down.sql
new file mode 100644
index 0000000000..1974712758
--- /dev/null
+++
b/traffic_ops/app/db/migrations/2022121612121500_add_description_field_in_sc_table.down.sql
@@ -0,0 +1,18 @@
+/*
+ * 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.
+ */
+
+ALTER TABLE public.server_capability DROP column description;
diff --git
a/traffic_ops/app/db/migrations/2022121612121500_add_description_field_in_sc_table.up.sql
b/traffic_ops/app/db/migrations/2022121612121500_add_description_field_in_sc_table.up.sql
new file mode 100644
index 0000000000..1713df8842
--- /dev/null
+++
b/traffic_ops/app/db/migrations/2022121612121500_add_description_field_in_sc_table.up.sql
@@ -0,0 +1,18 @@
+/*
+ * 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.
+ */
+
+ALTER TABLE public.server_capability ADD COLUMN IF NOT EXISTS description text
NOT NULL DEFAULT '';
diff --git a/traffic_ops/testing/api/v4/server_capabilities_test.go
b/traffic_ops/testing/api/v4/server_capabilities_test.go
index c34e926387..e3c60fcd86 100644
--- a/traffic_ops/testing/api/v4/server_capabilities_test.go
+++ b/traffic_ops/testing/api/v4/server_capabilities_test.go
@@ -36,7 +36,7 @@ func TestServerCapabilities(t *testing.T) {
currentTime := time.Now().UTC().Add(-15 * time.Second)
currentTimeRFC := currentTime.Format(time.RFC1123)
- methodTests := utils.TestCase[client.Session,
client.RequestOptions, tc.ServerCapability]{
+ methodTests := utils.TestCase[client.Session,
client.RequestOptions, tc.ServerCapabilityV41]{
"GET": {
"OK when VALID request": {
ClientSession: TOSession,
@@ -57,20 +57,28 @@ func TestServerCapabilities(t *testing.T) {
"POST": {
"BAD REQUEST when ALREADY EXISTS": {
ClientSession: TOSession,
- RequestBody:
tc.ServerCapability{Name: "foo"},
- Expectations:
utils.CkRequest(utils.HasError(), utils.HasStatus(http.StatusBadRequest)),
+ RequestBody: tc.ServerCapabilityV41{
+ ServerCapability:
tc.ServerCapability{Name: "foo"},
+ Description: "foo servers",
+ },
+ Expectations:
utils.CkRequest(utils.HasError(), utils.HasStatus(http.StatusBadRequest)),
},
"BAD REQUEST when INVALID NAME": {
ClientSession: TOSession,
- RequestBody:
tc.ServerCapability{Name: "b@dname"},
- Expectations:
utils.CkRequest(utils.HasError(), utils.HasStatus(http.StatusBadRequest)),
+ RequestBody: tc.ServerCapabilityV41{
+ ServerCapability:
tc.ServerCapability{Name: "b@dname"},
+ Description: "Server
Capability",
+ },
+ Expectations:
utils.CkRequest(utils.HasError(), utils.HasStatus(http.StatusBadRequest)),
},
},
"PUT": {
"OK when VALID request": {
ClientSession: TOSession,
RequestOpts:
client.RequestOptions{QueryParameters: url.Values{"name": {"blah"}}},
- RequestBody:
tc.ServerCapability{Name: "newname"},
+ RequestBody: tc.ServerCapabilityV41{
+ ServerCapability:
tc.ServerCapability{Name: "newname"},
+ Description: "Server
Capability for new name"},
Expectations:
utils.CkRequest(utils.NoError(), utils.HasStatus(http.StatusOK),
validateServerCapabilitiesUpdateFields(map[string]interface{}{"Name":
"newname"}),
validateSSCFieldsOnServerCapabilityUpdate("newname",
map[string]interface{}{"ServerCapability": "newname"})),
@@ -78,8 +86,11 @@ func TestServerCapabilities(t *testing.T) {
"BAD REQUEST when NAME DOESNT EXIST": {
ClientSession: TOSession,
RequestOpts:
client.RequestOptions{QueryParameters: url.Values{"name": {"invalid"}}},
- RequestBody:
tc.ServerCapability{Name: "newname"},
- Expectations:
utils.CkRequest(utils.HasError(), utils.HasStatus(http.StatusBadRequest)),
+ RequestBody: tc.ServerCapabilityV41{
+ ServerCapability:
tc.ServerCapability{Name: "newname"},
+ Description: "Server
Capability for new name",
+ },
+ Expectations:
utils.CkRequest(utils.HasError(), utils.HasStatus(http.StatusBadRequest)),
},
"PRECONDITION FAILED when updating with IMS &
IUS Headers": {
ClientSession: TOSession,
@@ -87,7 +98,10 @@ func TestServerCapabilities(t *testing.T) {
QueryParameters:
url.Values{"name": {"disk"}},
Header:
http.Header{rfc.IfUnmodifiedSince: {currentTimeRFC}},
},
- RequestBody: tc.ServerCapability{Name:
"newname"},
+ RequestBody: tc.ServerCapabilityV41{
+ ServerCapability:
tc.ServerCapability{Name: "newname"},
+ Description: "Server
Capability for new name",
+ },
Expectations:
utils.CkRequest(utils.HasError(),
utils.HasStatus(http.StatusPreconditionFailed)),
},
"PRECONDITION FAILED when updating with IFMATCH
ETAG Header": {
@@ -96,7 +110,10 @@ func TestServerCapabilities(t *testing.T) {
QueryParameters:
url.Values{"name": {"disk"}},
Header:
http.Header{rfc.IfMatch: {rfc.ETag(currentTime)}},
},
- RequestBody: tc.ServerCapability{Name:
"newname"},
+ RequestBody: tc.ServerCapabilityV41{
+ ServerCapability:
tc.ServerCapability{Name: "newname"},
+ Description: "Server
Capability for new name",
+ },
Expectations:
utils.CkRequest(utils.HasError(),
utils.HasStatus(http.StatusPreconditionFailed)),
},
},
@@ -120,21 +137,21 @@ func TestServerCapabilities(t *testing.T) {
switch method {
case "GET":
t.Run(name, func(t *testing.T) {
- resp, reqInf, err :=
testCase.ClientSession.GetServerCapabilities(testCase.RequestOpts)
+ resp, reqInf, err :=
testCase.ClientSession.GetServerCapabilitiesV41(testCase.RequestOpts)
for _, check := range
testCase.Expectations {
check(t,
reqInf, resp.Response, resp.Alerts, err)
}
})
case "POST":
t.Run(name, func(t *testing.T) {
- resp, reqInf, err :=
testCase.ClientSession.CreateServerCapability(testCase.RequestBody,
testCase.RequestOpts)
+ resp, reqInf, err :=
testCase.ClientSession.CreateServerCapabilityV41(testCase.RequestBody,
testCase.RequestOpts)
for _, check := range
testCase.Expectations {
check(t,
reqInf, resp.Response, resp.Alerts, err)
}
})
case "PUT":
t.Run(name, func(t *testing.T) {
- resp, reqInf, err :=
testCase.ClientSession.UpdateServerCapability(testCase.RequestOpts.QueryParameters["name"][0],
testCase.RequestBody, testCase.RequestOpts)
+ resp, reqInf, err :=
testCase.ClientSession.UpdateServerCapabilityV41(testCase.RequestOpts.QueryParameters["name"][0],
testCase.RequestBody, testCase.RequestOpts)
for _, check := range
testCase.Expectations {
check(t,
reqInf, resp.Response, resp.Alerts, err)
}
@@ -156,7 +173,7 @@ func TestServerCapabilities(t *testing.T) {
func validateServerCapabilitiesUpdateFields(expectedResp
map[string]interface{}) utils.CkReqFunc {
return func(t *testing.T, _ toclientlib.ReqInf, resp interface{}, _
tc.Alerts, _ error) {
assert.RequireNotNil(t, resp, "Expected Server Capabilities
response to not be nil.")
- serverCapabilitiesResp := resp.(tc.ServerCapability)
+ serverCapabilitiesResp := resp.(tc.ServerCapabilityV41)
for field, expected := range expectedResp {
switch field {
case "Name":
@@ -172,7 +189,7 @@ func validateServerCapabilitiesSort() utils.CkReqFunc {
return func(t *testing.T, _ toclientlib.ReqInf, resp interface{},
alerts tc.Alerts, _ error) {
assert.RequireNotNil(t, resp, "Expected Server Capabilities
response to not be nil.")
var serverCapabilityNames []string
- serverCapabilitiesResp := resp.([]tc.ServerCapability)
+ serverCapabilitiesResp := resp.([]tc.ServerCapabilityV41)
for _, serverCapability := range serverCapabilitiesResp {
serverCapabilityNames = append(serverCapabilityNames,
serverCapability.Name)
}
@@ -182,7 +199,7 @@ func validateServerCapabilitiesSort() utils.CkReqFunc {
func CreateTestServerCapabilities(t *testing.T) {
for _, sc := range testData.ServerCapabilities {
- resp, _, err := TOSession.CreateServerCapability(sc,
client.RequestOptions{})
+ resp, _, err := TOSession.CreateServerCapabilityV41(sc,
client.RequestOptions{})
assert.RequireNoError(t, err, "Unexpected error creating Server
Capability '%s': %v - alerts: %+v", sc.Name, err, resp.Alerts)
}
}
diff --git a/traffic_ops/testing/api/v4/traffic_control_test.go
b/traffic_ops/testing/api/v4/traffic_control_test.go
index cd08c61af9..6449c3f8d5 100644
--- a/traffic_ops/testing/api/v4/traffic_control_test.go
+++ b/traffic_ops/testing/api/v4/traffic_control_test.go
@@ -47,7 +47,7 @@ type TrafficControl struct {
Roles []tc.RoleV4
`json:"roles"`
Servers []tc.ServerV40
`json:"servers"`
ServerServerCapabilities
[]tc.ServerServerCapability `json:"serverServerCapabilities"`
- ServerCapabilities []tc.ServerCapability
`json:"serverCapabilities"`
+ ServerCapabilities
[]tc.ServerCapabilityV41 `json:"serverCapabilities"`
ServiceCategories []tc.ServiceCategory
`json:"serviceCategories"`
Statuses []tc.StatusNullable
`json:"statuses"`
StaticDNSEntries []tc.StaticDNSEntry
`json:"staticdnsentries"`
diff --git a/traffic_ops/testing/api/v5/server_capabilities_test.go
b/traffic_ops/testing/api/v5/server_capabilities_test.go
index a1b9f899f3..97182eabf7 100644
--- a/traffic_ops/testing/api/v5/server_capabilities_test.go
+++ b/traffic_ops/testing/api/v5/server_capabilities_test.go
@@ -36,7 +36,7 @@ func TestServerCapabilities(t *testing.T) {
currentTime := time.Now().UTC().Add(-15 * time.Second)
currentTimeRFC := currentTime.Format(time.RFC1123)
- methodTests := utils.TestCase[client.Session,
client.RequestOptions, tc.ServerCapability]{
+ methodTests := utils.TestCase[client.Session,
client.RequestOptions, tc.ServerCapabilityV41]{
"GET": {
"OK when VALID request": {
ClientSession: TOSession,
@@ -57,20 +57,29 @@ func TestServerCapabilities(t *testing.T) {
"POST": {
"BAD REQUEST when ALREADY EXISTS": {
ClientSession: TOSession,
- RequestBody:
tc.ServerCapability{Name: "foo"},
- Expectations:
utils.CkRequest(utils.HasError(), utils.HasStatus(http.StatusBadRequest)),
+ RequestBody: tc.ServerCapabilityV41{
+ ServerCapability:
tc.ServerCapability{Name: "foo"},
+ Description: "foo servers",
+ },
+ Expectations:
utils.CkRequest(utils.HasError(), utils.HasStatus(http.StatusBadRequest)),
},
"BAD REQUEST when INVALID NAME": {
ClientSession: TOSession,
- RequestBody:
tc.ServerCapability{Name: "b@dname"},
- Expectations:
utils.CkRequest(utils.HasError(), utils.HasStatus(http.StatusBadRequest)),
+ RequestBody: tc.ServerCapabilityV41{
+ ServerCapability:
tc.ServerCapability{Name: "b@dname"},
+ Description: "Server
Capability",
+ },
+ Expectations:
utils.CkRequest(utils.HasError(), utils.HasStatus(http.StatusBadRequest)),
},
},
"PUT": {
"OK when VALID request": {
ClientSession: TOSession,
RequestOpts:
client.RequestOptions{QueryParameters: url.Values{"name": {"blah"}}},
- RequestBody:
tc.ServerCapability{Name: "newname"},
+ RequestBody: tc.ServerCapabilityV41{
+ ServerCapability:
tc.ServerCapability{Name: "newname"},
+ Description: "Server
Capability for new name",
+ },
Expectations:
utils.CkRequest(utils.NoError(), utils.HasStatus(http.StatusOK),
validateServerCapabilitiesUpdateFields(map[string]interface{}{"Name":
"newname"}),
validateSSCFieldsOnServerCapabilityUpdate("newname",
map[string]interface{}{"ServerCapability": "newname"})),
@@ -78,8 +87,11 @@ func TestServerCapabilities(t *testing.T) {
"BAD REQUEST when NAME DOESNT EXIST": {
ClientSession: TOSession,
RequestOpts:
client.RequestOptions{QueryParameters: url.Values{"name": {"invalid"}}},
- RequestBody:
tc.ServerCapability{Name: "newname"},
- Expectations:
utils.CkRequest(utils.HasError(), utils.HasStatus(http.StatusBadRequest)),
+ RequestBody: tc.ServerCapabilityV41{
+ ServerCapability:
tc.ServerCapability{Name: "newname"},
+ Description: "Server
Capability for new name",
+ },
+ Expectations:
utils.CkRequest(utils.HasError(), utils.HasStatus(http.StatusBadRequest)),
},
"PRECONDITION FAILED when updating with IMS &
IUS Headers": {
ClientSession: TOSession,
@@ -87,7 +99,10 @@ func TestServerCapabilities(t *testing.T) {
QueryParameters:
url.Values{"name": {"disk"}},
Header:
http.Header{rfc.IfUnmodifiedSince: {currentTimeRFC}},
},
- RequestBody: tc.ServerCapability{Name:
"newname"},
+ RequestBody: tc.ServerCapabilityV41{
+ ServerCapability:
tc.ServerCapability{Name: "newname"},
+ Description: "Server
Capability for new name",
+ },
Expectations:
utils.CkRequest(utils.HasError(),
utils.HasStatus(http.StatusPreconditionFailed)),
},
"PRECONDITION FAILED when updating with IFMATCH
ETAG Header": {
@@ -96,7 +111,10 @@ func TestServerCapabilities(t *testing.T) {
QueryParameters:
url.Values{"name": {"disk"}},
Header:
http.Header{rfc.IfMatch: {rfc.ETag(currentTime)}},
},
- RequestBody: tc.ServerCapability{Name:
"newname"},
+ RequestBody: tc.ServerCapabilityV41{
+ ServerCapability:
tc.ServerCapability{Name: "newname"},
+ Description: "Server
Capability for new name",
+ },
Expectations:
utils.CkRequest(utils.HasError(),
utils.HasStatus(http.StatusPreconditionFailed)),
},
},
@@ -156,7 +174,7 @@ func TestServerCapabilities(t *testing.T) {
func validateServerCapabilitiesUpdateFields(expectedResp
map[string]interface{}) utils.CkReqFunc {
return func(t *testing.T, _ toclientlib.ReqInf, resp interface{}, _
tc.Alerts, _ error) {
assert.RequireNotNil(t, resp, "Expected Server Capabilities
response to not be nil.")
- serverCapabilitiesResp := resp.(tc.ServerCapability)
+ serverCapabilitiesResp := resp.(tc.ServerCapabilityV41)
for field, expected := range expectedResp {
switch field {
case "Name":
@@ -172,7 +190,7 @@ func validateServerCapabilitiesSort() utils.CkReqFunc {
return func(t *testing.T, _ toclientlib.ReqInf, resp interface{},
alerts tc.Alerts, _ error) {
assert.RequireNotNil(t, resp, "Expected Server Capabilities
response to not be nil.")
var serverCapabilityNames []string
- serverCapabilitiesResp := resp.([]tc.ServerCapability)
+ serverCapabilitiesResp := resp.([]tc.ServerCapabilityV41)
for _, serverCapability := range serverCapabilitiesResp {
serverCapabilityNames = append(serverCapabilityNames,
serverCapability.Name)
}
diff --git a/traffic_ops/testing/api/v5/tc-fixtures.json
b/traffic_ops/testing/api/v5/tc-fixtures.json
index 18dc10c256..8df76f8d48 100644
--- a/traffic_ops/testing/api/v5/tc-fixtures.json
+++ b/traffic_ops/testing/api/v5/tc-fixtures.json
@@ -5431,22 +5431,28 @@
],
"serverCapabilities": [
{
- "name": "foo"
+ "name": "foo",
+ "description": "server capability foo"
},
{
- "name": "bar"
+ "name": "bar",
+ "description": "server capability bar"
},
{
- "name": "ram"
+ "name": "ram",
+ "description": "server capability ram"
},
{
- "name": "disk"
+ "name": "disk",
+ "description": "server capability disk"
},
{
- "name": "asdf"
+ "name": "asdf",
+ "description": "server capability asdf"
},
{
- "name": "blah"
+ "name": "blah",
+ "description": "server capability blah"
}
],
"serverServerCapabilities": [
diff --git a/traffic_ops/testing/api/v5/traffic_control_test.go
b/traffic_ops/testing/api/v5/traffic_control_test.go
index acabaac327..bb747942e6 100644
--- a/traffic_ops/testing/api/v5/traffic_control_test.go
+++ b/traffic_ops/testing/api/v5/traffic_control_test.go
@@ -47,7 +47,7 @@ type TrafficControl struct {
Roles []tc.RoleV4
`json:"roles"`
Servers []tc.ServerV4
`json:"servers"`
ServerServerCapabilities
[]tc.ServerServerCapability `json:"serverServerCapabilities"`
- ServerCapabilities []tc.ServerCapability
`json:"serverCapabilities"`
+ ServerCapabilities
[]tc.ServerCapabilityV41 `json:"serverCapabilities"`
ServiceCategories []tc.ServiceCategory
`json:"serviceCategories"`
Statuses []tc.StatusNullable
`json:"statuses"`
StaticDNSEntries []tc.StaticDNSEntry
`json:"staticdnsentries"`
diff --git a/traffic_ops/traffic_ops_golang/dbhelpers/db_helpers.go
b/traffic_ops/traffic_ops_golang/dbhelpers/db_helpers.go
index d4e493fa7b..39f6487efa 100644
--- a/traffic_ops/traffic_ops_golang/dbhelpers/db_helpers.go
+++ b/traffic_ops/traffic_ops_golang/dbhelpers/db_helpers.go
@@ -2172,3 +2172,18 @@ func GetProfileIDDesc(tx *sql.Tx, name string) (id int,
desc string) {
}
return
}
+
+// GetSCInfo confirms whether the server capability exists, and an error (if
one occurs).
+func GetSCInfo(tx *sql.Tx, name string) (bool, error) {
+ var count int
+ if err := tx.QueryRow("SELECT count(name) FROM server_capability AS sc
WHERE sc.name=$1", name).Scan(&count); err != nil {
+ return false, fmt.Errorf("error getting server capability info:
%w", err)
+ }
+ if count == 0 {
+ return false, nil
+ }
+ if count != 1 {
+ return false, fmt.Errorf("getting server capability 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 b388e8e995..282311caca 100644
--- a/traffic_ops/traffic_ops_golang/dbhelpers/db_helpers_test.go
+++ b/traffic_ops/traffic_ops_golang/dbhelpers/db_helpers_test.go
@@ -21,6 +21,7 @@ package dbhelpers
import (
"context"
+ "database/sql"
"errors"
"reflect"
"strconv"
@@ -408,3 +409,54 @@ func TestGetCDNIDFromName(t *testing.T) {
}
}
+
+func TestGetSCInfo(t *testing.T) {
+ var testCases = []struct {
+ description string
+ name string
+ expectedError error
+ exists bool
+ }{
+ {
+ description: "Success: Get valid SC",
+ name: "hdd",
+ expectedError: nil,
+ exists: true,
+ },
+ {
+ description: "Failure: SC not in DB",
+ name: "disk",
+ 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 := GetSCInfo(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("getSCInfo 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 ff4e4b0dcb..3f8c11a0f2 100644
--- a/traffic_ops/traffic_ops_golang/routing/routes.go
+++ b/traffic_ops/traffic_ops_golang/routing/routes.go
@@ -325,10 +325,10 @@ func Routes(d ServerData) ([]Route, http.Handler, error) {
{Version: api.Version{Major: 5, Minor: 0}, Method:
http.MethodDelete, Path: `servers/{id}$`, Handler: server.Delete,
RequiredPrivLevel: auth.PrivLevelOperations, RequiredPermissions:
[]string{"SERVER:DELETE", "SERVER:READ", "DELIVERY-SERVICE:READ", "CDN:READ",
"PHYSICAL-LOCATION:READ", "CACHE-GROUP:READ", "TYPE:READ", "PROFILE:READ"},
Authenticated: Authenticated, Middlewares: nil, ID: 49232223331},
//Server Capability
- {Version: api.Version{Major: 5, Minor: 0}, Method:
http.MethodGet, Path: `server_capabilities$`, Handler:
api.ReadHandler(&servercapability.TOServerCapability{}), RequiredPrivLevel:
auth.PrivLevelReadOnly, RequiredPermissions:
[]string{"SERVER-CAPABILITY:READ"}, Authenticated: Authenticated, Middlewares:
nil, ID: 41040739131},
- {Version: api.Version{Major: 5, Minor: 0}, Method:
http.MethodPost, Path: `server_capabilities$`, Handler:
api.CreateHandler(&servercapability.TOServerCapability{}), RequiredPrivLevel:
auth.PrivLevelOperations, RequiredPermissions:
[]string{"SERVER-CAPABILITY:CREATE", "SERVER-CAPABILITY:READ"}, Authenticated:
Authenticated, Middlewares: nil, ID: 407447070831},
- {Version: api.Version{Major: 5, Minor: 0}, Method:
http.MethodPut, Path: `server_capabilities$`, Handler:
api.UpdateHandler(&servercapability.TOServerCapability{}), RequiredPrivLevel:
auth.PrivLevelOperations, RequiredPermissions:
[]string{"SERVER-CAPABILITY:UPDATE", "SERVER-CAPABILITY:READ"}, Authenticated:
Authenticated, Middlewares: nil, ID: 425437701091},
- {Version: api.Version{Major: 5, Minor: 0}, Method:
http.MethodDelete, Path: `server_capabilities$`, Handler:
api.DeleteHandler(&servercapability.TOServerCapability{}), RequiredPrivLevel:
auth.PrivLevelOperations, RequiredPermissions:
[]string{"SERVER-CAPABILITY:DELETE", "SERVER-CAPABILITY:READ"}, Authenticated:
Authenticated, Middlewares: nil, ID: 43641503831},
+ {Version: api.Version{Major: 5, Minor: 0}, Method:
http.MethodGet, Path: `server_capabilities$`, Handler:
servercapability.GetServerCapability, RequiredPrivLevel:
auth.PrivLevelReadOnly, RequiredPermissions:
[]string{"SERVER-CAPABILITY:READ"}, Authenticated: Authenticated, Middlewares:
nil, ID: 41040739131},
+ {Version: api.Version{Major: 5, Minor: 0}, Method:
http.MethodPost, Path: `server_capabilities$`, Handler:
servercapability.CreateServerCapability, RequiredPrivLevel:
auth.PrivLevelOperations, RequiredPermissions:
[]string{"SERVER-CAPABILITY:CREATE", "SERVER-CAPABILITY:READ"}, Authenticated:
Authenticated, Middlewares: nil, ID: 407447070831},
+ {Version: api.Version{Major: 5, Minor: 0}, Method:
http.MethodPut, Path: `server_capabilities$`, Handler:
servercapability.UpdateServerCapability, RequiredPrivLevel:
auth.PrivLevelOperations, RequiredPermissions:
[]string{"SERVER-CAPABILITY:UPDATE", "SERVER-CAPABILITY:READ"}, Authenticated:
Authenticated, Middlewares: nil, ID: 425437701091},
+ {Version: api.Version{Major: 5, Minor: 0}, Method:
http.MethodDelete, Path: `server_capabilities$`, Handler:
servercapability.DeleteServerCapability, RequiredPrivLevel:
auth.PrivLevelOperations, RequiredPermissions:
[]string{"SERVER-CAPABILITY:DELETE", "SERVER-CAPABILITY:READ"}, Authenticated:
Authenticated, Middlewares: nil, ID: 43641503831},
{Version: api.Version{Major: 5, Minor: 0}, Method:
http.MethodPost, Path: `multiple_servers_capabilities/?$`, Handler:
server.AssignMultipleServersCapabilities, RequiredPrivLevel:
auth.PrivLevelOperations, RequiredPermissions: []string{"SERVER:READ",
"SERVER:CREATE", "SERVER-CAPABILITY:READ", "SERVER-CAPABILITY:CREATE"},
Authenticated: Authenticated, Middlewares: nil, ID: 407924192581},
{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},
@@ -531,6 +531,11 @@ func Routes(d ServerData) ([]Route, http.Handler, error) {
{Version: api.Version{Major: 4, Minor: 1}, Method:
http.MethodPost, Path: `multiple_servers_capabilities/?$`, Handler:
server.AssignMultipleServersCapabilities, RequiredPrivLevel:
auth.PrivLevelOperations, RequiredPermissions: []string{"SERVER:READ",
"SERVER:CREATE", "SERVER-CAPABILITY:READ", "SERVER-CAPABILITY:CREATE"},
Authenticated: Authenticated, Middlewares: nil, ID: 40792419258},
{Version: api.Version{Major: 4, Minor: 1}, 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: 40792419278},
+ //Server Capability
+ {Version: api.Version{Major: 4, Minor: 1}, Method:
http.MethodGet, Path: `server_capabilities$`, Handler:
servercapability.GetServerCapability, RequiredPrivLevel:
auth.PrivLevelReadOnly, RequiredPermissions:
[]string{"SERVER-CAPABILITY:READ"}, Authenticated: Authenticated, Middlewares:
nil, ID: 4104073912},
+ {Version: api.Version{Major: 4, Minor: 1}, Method:
http.MethodPost, Path: `server_capabilities$`, Handler:
servercapability.CreateServerCapability, RequiredPrivLevel:
auth.PrivLevelOperations, RequiredPermissions:
[]string{"SERVER-CAPABILITY:CREATE", "SERVER-CAPABILITY:READ"}, Authenticated:
Authenticated, Middlewares: nil, ID: 40744707025},
+ {Version: api.Version{Major: 4, Minor: 1}, Method:
http.MethodPut, Path: `server_capabilities$`, Handler:
servercapability.UpdateServerCapability, RequiredPrivLevel:
auth.PrivLevelOperations, RequiredPermissions:
[]string{"SERVER-CAPABILITY:UPDATE", "SERVER-CAPABILITY:READ"}, Authenticated:
Authenticated, Middlewares: nil, ID: 425437701023},
+
// CDNI integration
{Version: api.Version{Major: 4, Minor: 0}, Method:
http.MethodGet, Path: `OC/FCI/advertisement/?$`, Handler: cdni.GetCapabilities,
RequiredPrivLevel: auth.PrivLevelReadOnly, RequiredPermissions:
[]string{"CDNI-CAPACITY:READ"}, Authenticated: Authenticated, Middlewares: nil,
ID: 541357729077},
{Version: api.Version{Major: 4, Minor: 0}, Method:
http.MethodPut, Path: `OC/CI/configuration/?$`, Handler: cdni.PutConfiguration,
RequiredPrivLevel: auth.PrivLevelReadOnly, RequiredPermissions:
[]string{"CDNI-CAPACITY:UPDATE"}, Authenticated: Authenticated, Middlewares:
nil, ID: 541357729078},
diff --git
a/traffic_ops/traffic_ops_golang/servercapability/servercapability.go
b/traffic_ops/traffic_ops_golang/servercapability/servercapability.go
index cd4f122d5e..8d9d60e442 100644
--- a/traffic_ops/traffic_ops_golang/servercapability/servercapability.go
+++ b/traffic_ops/traffic_ops_golang/servercapability/servercapability.go
@@ -20,6 +20,9 @@ package servercapability
*/
import (
+ "database/sql"
+ "encoding/json"
+ "errors"
"fmt"
"net/http"
"time"
@@ -135,7 +138,7 @@ func (v *TOServerCapability) Update(h http.Header) (error,
error, int) {
return userErr, sysErr, errCode
}
- // udpate server capability name
+ // update server capability name
rows, err := v.ReqInfo.Tx.Query(v.updateQuery(), v.RequestedName,
v.Name)
if err != nil {
return nil, fmt.Errorf("server capability update: error setting
the name for server capability %v: %v", v.Name, err.Error()),
http.StatusInternalServerError
@@ -151,6 +154,213 @@ func (v *TOServerCapability) Update(h http.Header)
(error, error, int) {
return nil, nil, http.StatusOK
}
+func GetServerCapability(w http.ResponseWriter, r *http.Request) {
+ var sc tc.ServerCapabilityV4
+ inf, userErr, sysErr, errCode := api.NewInfo(r, nil, nil)
+ tx := inf.Tx
+ if userErr != nil || sysErr != nil {
+ api.HandleErr(w, r, tx.Tx, errCode, userErr, sysErr)
+ return
+ }
+ defer inf.Close()
+
+ // Query Parameters to Database Query column mappings
+ queryParamsToQueryCols := map[string]dbhelpers.WhereColumnInfo{
+ "name": {Column: "sc.name", Checker: nil},
+ }
+ if _, ok := inf.Params["orderby"]; !ok {
+ inf.Params["orderby"] = "name"
+ }
+ where, orderBy, pagination, queryValues, errs :=
dbhelpers.BuildWhereAndOrderByAndPagination(inf.Params, queryParamsToQueryCols)
+ if len(errs) > 0 {
+ api.HandleErr(w, r, tx.Tx, http.StatusBadRequest,
util.JoinErrs(errs), nil)
+ return
+ }
+
+ selectQuery := "SELECT name, description, last_updated FROM
server_capability sc"
+ query := selectQuery + where + orderBy + pagination
+ rows, err := tx.NamedQuery(query, queryValues)
+ if err != nil {
+ api.HandleErr(w, r, tx.Tx, http.StatusInternalServerError, nil,
fmt.Errorf("server capability read: error getting server capability(ies): %w",
err))
+ return
+ }
+ defer log.Close(rows, "unable to close DB connection")
+
+ scList := []tc.ServerCapabilityV4{}
+ for rows.Next() {
+ if err = rows.Scan(&sc.Name, &sc.Description, &sc.LastUpdated);
err != nil {
+ api.HandleErr(w, r, tx.Tx,
http.StatusInternalServerError, nil, fmt.Errorf("error getting server
capability(ies): %w", err))
+ return
+ }
+ scList = append(scList, sc)
+ }
+
+ api.WriteResp(w, r, scList)
+ return
+}
+
+func UpdateServerCapability(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, "server_capability")
+ if userErr != nil || sysErr != nil {
+ api.HandleErr(w, r, tx, errCode, userErr, sysErr)
+ return
+ }
+
+ //update name and description of a capability
+ query := `UPDATE server_capability sc SET
+ name = $1,
+ description = $2
+ WHERE sc.name = $3
+ RETURNING sc.name, sc.description, sc.last_updated`
+
+ err := tx.QueryRow(query, sc.Name, sc.Description,
requestedName).Scan(&sc.Name, &sc.Description, &sc.LastUpdated)
+ if err != nil {
+ if errors.Is(err, sql.ErrNoRows) {
+ api.HandleErr(w, r, tx, http.StatusBadRequest,
fmt.Errorf("server capability with name: %s not found", sc.Name), nil)
+ return
+ }
+ usrErr, sysErr, code := api.ParseDBError(err)
+ api.HandleErr(w, r, tx, code, usrErr, sysErr)
+ return
+ }
+ alerts := tc.CreateAlerts(tc.SuccessLevel, "server capability was
updated")
+ api.WriteAlertsObj(w, r, http.StatusOK, alerts, sc)
+ return
+}
+
+func CreateServerCapability(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 capability already exists
+ var count int
+ err := tx.QueryRow("SELECT count(*) from server_capability where name =
$1", sc.Name).Scan(&count)
+ if err != nil {
+ api.HandleErr(w, r, tx, http.StatusInternalServerError, nil,
fmt.Errorf("error: %w, when checking if server capability with name %s exists",
err, sc.Name))
+ return
+ }
+ if count == 1 {
+ api.HandleErr(w, r, tx, http.StatusBadRequest,
fmt.Errorf("server_capability name '%s' already exists.", sc.Name), nil)
+ return
+ }
+
+ // create server capability
+ query := `INSERT INTO server_capability (name, description) VALUES ($1,
$2) RETURNING last_updated`
+ err = tx.QueryRow(query, sc.Name, sc.Description).Scan(&sc.LastUpdated)
+ if err != nil {
+ if errors.Is(err, sql.ErrNoRows) {
+ api.HandleErr(w, r, tx, http.StatusInternalServerError,
fmt.Errorf("error: %w in creating server capability 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, "server capability was
created.")
+ w.Header().Set("Location",
fmt.Sprintf("/api/%d.%d/server_capabilities?name=%s", inf.Version.Major,
inf.Version.Minor, sc.Name))
+ api.WriteAlertsObj(w, r, http.StatusCreated, alerts, sc)
+ return
+}
+
+func DeleteServerCapability(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.GetSCInfo(tx, name)
+ if err != nil {
+ api.HandleErr(w, r, tx, http.StatusInternalServerError, nil,
err)
+ return
+ }
+ if !exists {
+ if name != "" {
+ api.HandleErr(w, r, tx, http.StatusNotFound,
fmt.Errorf("no server capability exists by name: %s", name), nil)
+ return
+ } else {
+ api.HandleErr(w, r, tx, http.StatusBadRequest,
fmt.Errorf("no server capability exists for empty name: %s", name), nil)
+ return
+ }
+ }
+
+ assignedServer := 0
+ if err := inf.Tx.Get(&assignedServer, "SELECT count(server) FROM
server_server_capability ssc WHERE ssc.server_capability=$1", name); err != nil
{
+ api.HandleErr(w, r, tx, http.StatusInternalServerError, nil,
fmt.Errorf("server capability delete, counting assigned servers: %w", err))
+ return
+ } else if assignedServer != 0 {
+ api.HandleErr(w, r, tx, http.StatusBadRequest, fmt.Errorf("can
not delete a server capability with %d assigned servers", assignedServer), nil)
+ return
+ }
+
+ res, err := tx.Exec("DELETE FROM server_capability 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 server capability: %w", err))
+ return
+ }
+ if rowsAffected == 0 {
+ api.HandleErr(w, r, tx, http.StatusInternalServerError,
fmt.Errorf("no rows deleted for server capability"), nil)
+ return
+ }
+ alerts := tc.CreateAlerts(tc.SuccessLevel, "server capability was
deleted.")
+ api.WriteAlertsObj(w, r, http.StatusOK, alerts, name)
+ return
+}
+
+func readAndValidateJsonStruct(r *http.Request) (tc.ServerCapabilityV41,
error) {
+ var sc tc.ServerCapabilityV41
+ if err := json.NewDecoder(r.Body).Decode(&sc); err != nil {
+ userErr := fmt.Errorf("error decoding POST request body into
ServerCapabilityV41 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
+}
+
func (v *TOServerCapability) SelectMaxLastUpdatedQuery(where, orderBy,
pagination, tableName string) string {
return `SELECT max(t) from (
SELECT max(sc.last_updated) as t from server_capability sc ` +
where + orderBy + pagination +
diff --git a/traffic_ops/v4-client/servercapability.go
b/traffic_ops/v4-client/servercapability.go
index 511b42e52c..1b41e18827 100644
--- a/traffic_ops/v4-client/servercapability.go
+++ b/traffic_ops/v4-client/servercapability.go
@@ -27,20 +27,37 @@ import (
const apiServerCapabilities = "/server_capabilities"
// CreateServerCapability creates the given Server Capability.
+// Note: Call Signature changed in v5, accepts tc.ServerCapabilityV41 instead
of tc.ServerCapability
func (to *Session) CreateServerCapability(sc tc.ServerCapability, opts
RequestOptions) (tc.ServerCapabilityDetailResponse, toclientlib.ReqInf, error) {
var scResp tc.ServerCapabilityDetailResponse
reqInf, err := to.post(apiServerCapabilities, opts, sc, &scResp)
return scResp, reqInf, err
}
+// CreateServerCapabilityV41 creates the given Server Capability V41 (includes
description).
+func (to *Session) CreateServerCapabilityV41(sc tc.ServerCapabilityV41, opts
RequestOptions) (tc.ServerCapabilityDetailResponseV41, toclientlib.ReqInf,
error) {
+ var scResp tc.ServerCapabilityDetailResponseV41
+ reqInf, err := to.post(apiServerCapabilities, opts, sc, &scResp)
+ return scResp, reqInf, err
+}
+
// GetServerCapabilities returns all the Server Capabilities in Traffic Ops.
+// Note: Call Signature changed in v5, accepts
tc.ServerCapabilitiesResponseV41 instead of tc.ServerCapabilitiesResponse
func (to *Session) GetServerCapabilities(opts RequestOptions)
(tc.ServerCapabilitiesResponse, toclientlib.ReqInf, error) {
var data tc.ServerCapabilitiesResponse
reqInf, err := to.get(apiServerCapabilities, opts, &data)
return data, reqInf, err
}
+// GetServerCapabilitiesV41 returns all the Server Capabilities V41 (includes
description) in Traffic Ops.
+func (to *Session) GetServerCapabilitiesV41(opts RequestOptions)
(tc.ServerCapabilitiesResponseV41, toclientlib.ReqInf, error) {
+ var data tc.ServerCapabilitiesResponseV41
+ reqInf, err := to.get(apiServerCapabilities, opts, &data)
+ return data, reqInf, err
+}
+
// UpdateServerCapability updates a Server Capability by name.
+// Note: Call Signature changed in v5, accepts tc.ServerCapabilityV41 instead
of tc.ServerCapability
func (to *Session) UpdateServerCapability(name string, sc tc.ServerCapability,
opts RequestOptions) (tc.ServerCapabilityDetailResponse, toclientlib.ReqInf,
error) {
if opts.QueryParameters == nil {
opts.QueryParameters = url.Values{}
@@ -51,6 +68,17 @@ func (to *Session) UpdateServerCapability(name string, sc
tc.ServerCapability, o
return data, reqInf, err
}
+// UpdateServerCapabilityV41 updates a Server Capability V41 (includes
description) by name.
+func (to *Session) UpdateServerCapabilityV41(name string, sc
tc.ServerCapabilityV41, opts RequestOptions)
(tc.ServerCapabilityDetailResponseV41, toclientlib.ReqInf, error) {
+ if opts.QueryParameters == nil {
+ opts.QueryParameters = url.Values{}
+ }
+ opts.QueryParameters.Set("name", name)
+ var data tc.ServerCapabilityDetailResponseV41
+ reqInf, err := to.put(apiServerCapabilities, opts, sc, &data)
+ return data, reqInf, err
+}
+
// DeleteServerCapability deletes the given server capability by name.
func (to *Session) DeleteServerCapability(name string, opts RequestOptions)
(tc.Alerts, toclientlib.ReqInf, error) {
if opts.QueryParameters == nil {
diff --git a/traffic_ops/v5-client/servercapability.go
b/traffic_ops/v5-client/servercapability.go
index 511b42e52c..9e9d8567c3 100644
--- a/traffic_ops/v5-client/servercapability.go
+++ b/traffic_ops/v5-client/servercapability.go
@@ -27,26 +27,26 @@ import (
const apiServerCapabilities = "/server_capabilities"
// CreateServerCapability creates the given Server Capability.
-func (to *Session) CreateServerCapability(sc tc.ServerCapability, opts
RequestOptions) (tc.ServerCapabilityDetailResponse, toclientlib.ReqInf, error) {
- var scResp tc.ServerCapabilityDetailResponse
+func (to *Session) CreateServerCapability(sc tc.ServerCapabilityV41, opts
RequestOptions) (tc.ServerCapabilityDetailResponseV41, toclientlib.ReqInf,
error) {
+ var scResp tc.ServerCapabilityDetailResponseV41
reqInf, err := to.post(apiServerCapabilities, opts, sc, &scResp)
return scResp, reqInf, err
}
// GetServerCapabilities returns all the Server Capabilities in Traffic Ops.
-func (to *Session) GetServerCapabilities(opts RequestOptions)
(tc.ServerCapabilitiesResponse, toclientlib.ReqInf, error) {
- var data tc.ServerCapabilitiesResponse
+func (to *Session) GetServerCapabilities(opts RequestOptions)
(tc.ServerCapabilitiesResponseV41, toclientlib.ReqInf, error) {
+ var data tc.ServerCapabilitiesResponseV41
reqInf, err := to.get(apiServerCapabilities, opts, &data)
return data, reqInf, err
}
// UpdateServerCapability updates a Server Capability by name.
-func (to *Session) UpdateServerCapability(name string, sc tc.ServerCapability,
opts RequestOptions) (tc.ServerCapabilityDetailResponse, toclientlib.ReqInf,
error) {
+func (to *Session) UpdateServerCapability(name string, sc
tc.ServerCapabilityV41, opts RequestOptions)
(tc.ServerCapabilityDetailResponseV41, toclientlib.ReqInf, error) {
if opts.QueryParameters == nil {
opts.QueryParameters = url.Values{}
}
opts.QueryParameters.Set("name", name)
- var data tc.ServerCapabilityDetailResponse
+ var data tc.ServerCapabilityDetailResponseV41
reqInf, err := to.put(apiServerCapabilities, opts, sc, &data)
return data, reqInf, err
}
diff --git
a/traffic_portal/app/src/common/modules/form/serverCapability/form.serverCapability.tpl.html
b/traffic_portal/app/src/common/modules/form/serverCapability/form.serverCapability.tpl.html
index f2875fda32..0d8c85d548 100644
---
a/traffic_portal/app/src/common/modules/form/serverCapability/form.serverCapability.tpl.html
+++
b/traffic_portal/app/src/common/modules/form/serverCapability/form.serverCapability.tpl.html
@@ -41,6 +41,12 @@ under the License.
<span ng-show="hasError(serverCapabilityForm.name)"
class="form-control-feedback"><i class="fa fa-times"></i></span>
</div>
</div>
+ <div class="form-group">
+ <label label for="description" class="control-label col-md-2
col-sm-2 col-xs-12" >Description *</label>
+ <div class="col-md-10 col-sm-10 col-xs-12">
+ <textarea id="description" name="description" rows="3"
cols="17" class="form-control" ng-model="serverCapability.description"
maxlength="256"></textarea>
+ </div>
+ </div>
<div class="modal-footer">
<button type="button" class="btn btn-danger"
ng-if="!settings.isNew"
ng-click="confirmDelete(serverCapability)">Delete</button>
<button type="button" class="btn btn-success"
ng-if="settings.isNew" ng-disabled="serverCapabilityForm.$pristine ||
serverCapabilityForm.$invalid"
ng-click="save(serverCapability)">{{settings.saveLabel}}</button>
diff --git
a/traffic_portal/app/src/common/modules/table/serverCapabilities/table.serverCapabilities.tpl.html
b/traffic_portal/app/src/common/modules/table/serverCapabilities/table.serverCapabilities.tpl.html
index 130cd308b5..ae73d1649a 100644
---
a/traffic_portal/app/src/common/modules/table/serverCapabilities/table.serverCapabilities.tpl.html
+++
b/traffic_portal/app/src/common/modules/table/serverCapabilities/table.serverCapabilities.tpl.html
@@ -34,11 +34,13 @@ under the License.
<thead>
<tr>
<th>Name</th>
+ <th>Description</th>
</tr>
</thead>
<tbody>
- <tr ng-click="editServerCapability(sc.name)" ng-repeat="sc in
::serverCapabilities" context-menu="contextMenuItems">
+ <tr ng-click="editServerCapability(sc.name)" ng-repeat="sc in
::serverCapabilities | orderBy :'name'" context-menu="contextMenuItems">
<td name="name"
data-search="^{{::sc.name}}$">{{::sc.name}}</td>
+ <td name="name"
data-search="^{{::sc.description}}$">{{::sc.description}}</td>
</tr>
</tbody>
</table>
diff --git a/traffic_portal/test/integration/Data/servercapabilities.ts
b/traffic_portal/test/integration/Data/servercapabilities.ts
index 9ec2c40dfb..148cf4427e 100644
--- a/traffic_portal/test/integration/Data/servercapabilities.ts
+++ b/traffic_portal/test/integration/Data/servercapabilities.ts
@@ -55,41 +55,49 @@ export const serverCapabilities = {
{
description: "can create a server
capability",
name: "TP_SC",
+ capabilityDescription: "Server
Capability",
validationMessage: "server capability
was created."
},
{
description: "can create multiple
server capabilities",
name: "TP_SC_2",
+ capabilityDescription: "Server
Capability 2",
validationMessage: "server capability
was created."
},
{
description: "can create multiple
server capabilities",
name: "TP_SC_3",
+ capabilityDescription: "Server
Capability 3",
validationMessage: "server capability
was created."
},
{
description: "can handle creating
existing server capability",
name: "TP_SC_2",
+ capabilityDescription: "Server
Capability 2",
validationMessage: "server_capability
name 'TP_SC_2' already exists."
},
{
description: "can handle invalid period
in server capability",
name: "TP.AUTOMATED",
+ capabilityDescription: "Server
Capability Automated",
validationMessage: "Must be
alphamumeric with no spaces. Dashes and underscores also allowed."
},
{
description: "can handle invalid space
in server capability",
name: "TP AUTOMATED",
+ capabilityDescription: "Server
Capability Automated",
validationMessage: "Must be
alphamumeric with no spaces. Dashes and underscores also allowed."
},
{
description: "can handle invalid
character in server capability",
name: "TP#AUTOMATED",
+ capabilityDescription: "Server
Capability Automated",
validationMessage: "Must be
alphamumeric with no spaces. Dashes and underscores also allowed."
},
{
description: "can handle an empty text
field",
name: "",
+ capabilityDescription: "",
validationMessage: "Required"
}
],
@@ -132,6 +140,7 @@ export const serverCapabilities = {
{
description: "can handle readonly role
creating a server capability",
name: "TP_SC",
+ capabilityDescription: "Server
Capability",
validationMessage: "missing required
Permissions: SERVER-CAPABILITY:CREATE"
}
],
@@ -162,36 +171,43 @@ export const serverCapabilities = {
{
description: "can create a server
capability",
name: "TP_SC",
+ capabilityDescription: "Server
Capability",
validationMessage: "server capability
was created."
},
{
description: "can create multiple
server capabilities",
name: "TP_SC_2",
+ capabilityDescription: "Server
Capability 2",
validationMessage: "server capability
was created."
},
{
description: "can handle creating
existing server capability",
name: "TP_SC_2",
+ capabilityDescription: "Server
Capability 2",
validationMessage: "server_capability
name 'TP_SC_2' already exists."
},
{
description: "can handle invalid period
in server capability",
name: "TP.AUTOMATED",
+ capabilityDescription: "Server
Capability Automated",
validationMessage: "Must be
alphamumeric with no spaces. Dashes and underscores also allowed."
},
{
description: "can handle invalid space
in server capability",
name: "TP AUTOMATED",
+ capabilityDescription: "Server
Capability Automated",
validationMessage: "Must be
alphamumeric with no spaces. Dashes and underscores also allowed."
},
{
description: "can handle invalid
character in server capability",
name: "TP#AUTOMATED",
+ capabilityDescription: "Server
Capability Automated",
validationMessage: "Must be
alphamumeric with no spaces. Dashes and underscores also allowed."
},
{
description: "can handle an empty text
field",
name: "",
+ capabilityDescription: "",
validationMessage: "Required"
}
],
diff --git
a/traffic_portal/test/integration/PageObjects/ServerCapabilitiesPage.po.ts
b/traffic_portal/test/integration/PageObjects/ServerCapabilitiesPage.po.ts
index 8bba678f4e..1eb7bbc5bb 100644
--- a/traffic_portal/test/integration/PageObjects/ServerCapabilitiesPage.po.ts
+++ b/traffic_portal/test/integration/PageObjects/ServerCapabilitiesPage.po.ts
@@ -26,6 +26,7 @@ export class ServerCapabilitiesPage extends BasePage{
private btnCreateServerCapabilities =
element(by.name('createServerCapabilityButton'));
private txtSCName = element(by.id("name"))
+ private txtSCDescription = element(by.id("description"))
private searchFilter =
element(by.id('serverCapabilitiesTable_filter')).element(by.css('label input'));
private btnDelete = element(by.buttonText('Delete'))
private txtConfirmCapabilitiesName =
element(by.name('confirmWithNameInput'));
@@ -41,7 +42,7 @@ export class ServerCapabilitiesPage extends BasePage{
await snp.ClickConfigureMenu();
}
- public async CreateServerCapabilities(nameSC: string,
outputMessage:string){
+ public async CreateServerCapabilities(nameSC: string, descriptionSC:
string, outputMessage:string){
let result = false
let basePage = new BasePage();
let snp= new SideNavigationPage();
@@ -50,6 +51,7 @@ export class ServerCapabilitiesPage extends BasePage{
if(name != this.randomize){
await this.txtSCName.sendKeys(name);
}
+ await this.txtSCDescription.sendKeys(descriptionSC);
if(outputMessage == await(basePage.GetBlankErrorMessage()) ||
outputMessage == await(basePage.GetSyntaxErrorMessage())) {
await snp.NavigateToServerCapabilitiesPage();
result = true;
diff --git a/traffic_portal/test/integration/specs/ServerCapabilities.spec.ts
b/traffic_portal/test/integration/specs/ServerCapabilities.spec.ts
index dc9f92f722..6bbc4a9789 100644
--- a/traffic_portal/test/integration/specs/ServerCapabilities.spec.ts
+++ b/traffic_portal/test/integration/specs/ServerCapabilities.spec.ts
@@ -47,7 +47,7 @@ serverCapabilities.tests.forEach(serverCapabilitiesData => {
});
serverCapabilitiesData.add.forEach(add => {
it(add.description, async () => {
- expect(await
serverCapabilitiesPage.CreateServerCapabilities(add.name,
add.validationMessage)).toBe(true);
+ expect(await
serverCapabilitiesPage.CreateServerCapabilities(add.name,
add.capabilityDescription, add.validationMessage)).toBe(true);
await serverCapabilitiesPage.OpenServerCapabilityPage();
});
});