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();
                 });
             });

Reply via email to