You are viewing a plain text version of this content. The canonical link for it is here.
Posted to issues@trafficcontrol.apache.org by GitBox <gi...@apache.org> on 2022/01/26 21:10:42 UTC

[GitHub] [trafficcontrol] mattjackson220 opened a new pull request #6524: Added CDNi Capacity and Telemetry

mattjackson220 opened a new pull request #6524:
URL: https://github.com/apache/trafficcontrol/pull/6524


   <!--
   Thank you for contributing! Please be sure to read our contribution guidelines: https://github.com/apache/trafficcontrol/blob/master/CONTRIBUTING.md
   If this closes or relates to an existing issue, please reference it using one of the following:
   
   Closes: #ISSUE
   Related: #ISSUE
   
   If this PR fixes a security vulnerability, DO NOT submit! Instead, contact
   the Apache Traffic Control Security Team at security@trafficcontrol.apache.org and follow the
   guidelines at https://apache.org/security regarding vulnerability disclosure.
   -->
   
   This PR adds the ability to GET the Capacity and Telemetry data for CDNi integration. This is the first step toward future development toward CDNi functionality.
   
   
   <!-- **^ Add meaningful description above** --><hr/>
   
   ## Which Traffic Control components are affected by this PR?
   <!-- Please delete all components from this list that are NOT affected by this PR.
   Feel free to add the name of a tool or script that is affected but not on the list.
   -->
   - Documentation
   - Traffic Ops
   
   ## What is the best way to verify this PR?
   <!-- Please include here ALL the steps necessary to test your PR.
   If your PR has tests (and most should), provide the steps needed to run the tests.
   If not, please provide step-by-step instructions to test the PR manually and explain why your PR does not need tests. -->
   
   Run TO and verify that the new `/OC/FCI/advertisements` endpoint works as expected. For this PR, values must be manually added to the DB (future PR will add in ability to add / change these).  Verify that the json returned is formatted per the SVA spec titled Capacity Insights Interface.
   
   ## If this is a bugfix, which Traffic Control versions contained the bug?
   <!-- Delete this section if the PR is not a bugfix, or if the bug is only in the master branch.
   Examples:
   - 5.1.2
   - 5.1.3 (RC1)
    -->
   
   
   ## PR submission checklist
   - [] This PR has tests <!-- If not, please delete this text and explain why this PR does not need tests. -->
   - [x] This PR has documentation <!-- If not, please delete this text and explain why this PR does not need documentation. -->
   - [x] This PR has a CHANGELOG.md entry <!-- A fix for a bug from an ATC release, an improvement, or a new feature should have a changelog entry. -->
   - [] This PR **DOES NOT FIX A SERIOUS SECURITY VULNERABILITY** (see [the Apache Software Foundation's security guidelines](https://apache.org/security) for details)
   
   <!--
   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.
   -->
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@trafficcontrol.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [trafficcontrol] rawlinp commented on a change in pull request #6524: Added CDNi Capacity and Telemetry

Posted by GitBox <gi...@apache.org>.
rawlinp commented on a change in pull request #6524:
URL: https://github.com/apache/trafficcontrol/pull/6524#discussion_r803005190



##########
File path: traffic_ops/app/conf/cdn.conf
##########
@@ -98,5 +98,9 @@
         "organization" : "",
         "country" : "",
         "state" : ""
+    },
+    "cdni" : {

Review comment:
       These new cdn.conf options could use some documentation

##########
File path: traffic_ops/traffic_ops_golang/routing/routes.go
##########
@@ -130,6 +131,9 @@ func Routes(d ServerData) ([]Route, http.Handler, error) {
 		 * 4.x API
 		 */
 
+		// CDNI integration
+		{api.Version{Major: 4, Minor: 0}, http.MethodGet, `OC/FCI/advertisement/?$`, cdni.GetCapabilities, auth.PrivLevelReadOnly, []string{"CDNI-CAPACITY:READ"}, Authenticated, nil, 541357729077},

Review comment:
       Is `/OC/FCI/advertisement` the required path per the spec? Does it matter that the route is prefixed with `/api/4.0`?
   
   Also, I think all the routes were updated recently to include field names, so it would be good to rebase and match the new convention.

##########
File path: traffic_ops/traffic_ops_golang/cdni/capacity.go
##########
@@ -0,0 +1,177 @@
+package cdni
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import (
+	"fmt"
+
+	"github.com/lib/pq"
+
+	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/api"

Review comment:
       nit: I think these imports (and in the other added go files) are in the wrong order -- generally they should be stdlib, internal, external

##########
File path: traffic_ops/traffic_ops_golang/cdni/capacity.go
##########
@@ -0,0 +1,177 @@
+package cdni
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import (
+	"fmt"
+
+	"github.com/lib/pq"
+
+	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/api"
+)
+
+const totalLimitsQuery = `SELECT limit_type, maximum_hard, maximum_soft, ctl.telemetry_id, ctl.telemetry_metric, t.id, t.type, tm.name FROM cdni_total_limits AS ctl LEFT JOIN cdni_telemetry as t ON telemetry_id = t.id LEFT JOIN cdni_telemetry_metrics as tm ON telemetry_metric = tm.name WHERE ctl.capability_id = $1`
+const hostLimitsQuery = `SELECT limit_type, maximum_hard, maximum_soft, chl.telemetry_id, chl.telemetry_metric, t.id, t.type, tm.name, host FROM cdni_host_limits AS chl LEFT JOIN cdni_telemetry as t ON telemetry_id = t.id LEFT JOIN cdni_telemetry_metrics as tm ON telemetry_metric = tm.name WHERE chl.capability_id = $1 ORDER BY host DESC`
+
+func GetCapacities(inf *api.APIInfo, ucdn string) (Capabilities, error) {
+	capRows, err := inf.Tx.Tx.Query(CapabilityQuery, FciCapacityLimits, ucdn)
+	if err != nil {
+		return Capabilities{}, fmt.Errorf("querying capabilities: %w", err)
+	}
+	defer capRows.Close()

Review comment:
       nit: could use `log.Close`

##########
File path: traffic_ops/traffic_ops_golang/cdni/capacity.go
##########
@@ -0,0 +1,177 @@
+package cdni
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import (
+	"fmt"
+
+	"github.com/lib/pq"
+
+	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/api"
+)
+
+const totalLimitsQuery = `SELECT limit_type, maximum_hard, maximum_soft, ctl.telemetry_id, ctl.telemetry_metric, t.id, t.type, tm.name FROM cdni_total_limits AS ctl LEFT JOIN cdni_telemetry as t ON telemetry_id = t.id LEFT JOIN cdni_telemetry_metrics as tm ON telemetry_metric = tm.name WHERE ctl.capability_id = $1`
+const hostLimitsQuery = `SELECT limit_type, maximum_hard, maximum_soft, chl.telemetry_id, chl.telemetry_metric, t.id, t.type, tm.name, host FROM cdni_host_limits AS chl LEFT JOIN cdni_telemetry as t ON telemetry_id = t.id LEFT JOIN cdni_telemetry_metrics as tm ON telemetry_metric = tm.name WHERE chl.capability_id = $1 ORDER BY host DESC`

Review comment:
       nit: these could be easier to read if they were formatted across more lines

##########
File path: traffic_ops/traffic_ops_golang/cdni/capacity.go
##########
@@ -0,0 +1,177 @@
+package cdni
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import (
+	"fmt"
+
+	"github.com/lib/pq"
+
+	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/api"
+)
+
+const totalLimitsQuery = `SELECT limit_type, maximum_hard, maximum_soft, ctl.telemetry_id, ctl.telemetry_metric, t.id, t.type, tm.name FROM cdni_total_limits AS ctl LEFT JOIN cdni_telemetry as t ON telemetry_id = t.id LEFT JOIN cdni_telemetry_metrics as tm ON telemetry_metric = tm.name WHERE ctl.capability_id = $1`
+const hostLimitsQuery = `SELECT limit_type, maximum_hard, maximum_soft, chl.telemetry_id, chl.telemetry_metric, t.id, t.type, tm.name, host FROM cdni_host_limits AS chl LEFT JOIN cdni_telemetry as t ON telemetry_id = t.id LEFT JOIN cdni_telemetry_metrics as tm ON telemetry_metric = tm.name WHERE chl.capability_id = $1 ORDER BY host DESC`
+
+func GetCapacities(inf *api.APIInfo, ucdn string) (Capabilities, error) {
+	capRows, err := inf.Tx.Tx.Query(CapabilityQuery, FciCapacityLimits, ucdn)
+	if err != nil {
+		return Capabilities{}, fmt.Errorf("querying capabilities: %w", err)
+	}
+	defer capRows.Close()
+	capabilities := []CapabilityQueryResponse{}
+	for capRows.Next() {
+		var capability CapabilityQueryResponse
+		if err := capRows.Scan(&capability.Id, &capability.Type, &capability.UCdn); err != nil {
+			return Capabilities{}, fmt.Errorf("scanning db rows: %w", err)
+		}
+		capabilities = append(capabilities, capability)
+	}
+
+	fciCaps := Capabilities{}
+
+	for _, cap := range capabilities {
+		fciCap := Capability{}
+		footRows, err := inf.Tx.Tx.Query(FootprintQuery, cap.Id)
+		if err != nil {
+			return Capabilities{}, fmt.Errorf("querying footprints: %w", err)
+		}
+		defer footRows.Close()
+		footprints := []Footprint{}
+		for footRows.Next() {
+			var footprint Footprint
+			if err := footRows.Scan(&footprint.FootprintType, pq.Array(&footprint.FootprintValue)); err != nil {
+				return Capabilities{}, fmt.Errorf("scanning db rows: %w", err)
+			}
+			footprints = append(footprints, footprint)
+		}
+
+		fciCap.Footprints = footprints
+
+		tlRows, err := inf.Tx.Tx.Query(totalLimitsQuery, cap.Id)
+		if err != nil {
+			return Capabilities{}, fmt.Errorf("querying total limits: %w", err)
+		}
+
+		defer tlRows.Close()

Review comment:
       nit: could use `log.Close`

##########
File path: traffic_ops/traffic_ops_golang/cdni/capacity.go
##########
@@ -0,0 +1,177 @@
+package cdni
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import (
+	"fmt"
+
+	"github.com/lib/pq"
+
+	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/api"
+)
+
+const totalLimitsQuery = `SELECT limit_type, maximum_hard, maximum_soft, ctl.telemetry_id, ctl.telemetry_metric, t.id, t.type, tm.name FROM cdni_total_limits AS ctl LEFT JOIN cdni_telemetry as t ON telemetry_id = t.id LEFT JOIN cdni_telemetry_metrics as tm ON telemetry_metric = tm.name WHERE ctl.capability_id = $1`
+const hostLimitsQuery = `SELECT limit_type, maximum_hard, maximum_soft, chl.telemetry_id, chl.telemetry_metric, t.id, t.type, tm.name, host FROM cdni_host_limits AS chl LEFT JOIN cdni_telemetry as t ON telemetry_id = t.id LEFT JOIN cdni_telemetry_metrics as tm ON telemetry_metric = tm.name WHERE chl.capability_id = $1 ORDER BY host DESC`
+
+func GetCapacities(inf *api.APIInfo, ucdn string) (Capabilities, error) {
+	capRows, err := inf.Tx.Tx.Query(CapabilityQuery, FciCapacityLimits, ucdn)
+	if err != nil {
+		return Capabilities{}, fmt.Errorf("querying capabilities: %w", err)
+	}
+	defer capRows.Close()
+	capabilities := []CapabilityQueryResponse{}
+	for capRows.Next() {
+		var capability CapabilityQueryResponse
+		if err := capRows.Scan(&capability.Id, &capability.Type, &capability.UCdn); err != nil {
+			return Capabilities{}, fmt.Errorf("scanning db rows: %w", err)
+		}
+		capabilities = append(capabilities, capability)
+	}
+
+	fciCaps := Capabilities{}
+
+	for _, cap := range capabilities {
+		fciCap := Capability{}
+		footRows, err := inf.Tx.Tx.Query(FootprintQuery, cap.Id)

Review comment:
       Rather than running multiple DB queries (here, L68, and L83) in a loop, is it possible to come up with a single query here (or a constant number of queries) that returns all the data we need?

##########
File path: traffic_ops/traffic_ops_golang/cdni/shared.go
##########
@@ -0,0 +1,199 @@
+package cdni
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import (
+	"errors"
+	"net/http"
+	"time"
+
+	"github.com/dgrijalva/jwt-go"
+
+	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/api"
+)
+
+const CapabilityQuery = `SELECT id, type, ucdn FROM cdni_capabilities WHERE type = $1 AND ucdn = $2`
+const FootprintQuery = `SELECT footprint_type, footprint_value::text[] FROM cdni_footprints WHERE capability_id = $1`
+
+func GetCapabilities(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()
+
+	bearerToken := r.Header.Get("Authorization")
+
+	if inf.Config.Cdni == nil || inf.Config.Cdni.JwtDecodingSecret == "" || inf.Config.Cdni.DCdnId == "" {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, errors.New("cdn.conf does not contain CDNi information"))
+		return
+	}
+
+	claims := jwt.MapClaims{}
+	token, err := jwt.ParseWithClaims(bearerToken, claims, func(token *jwt.Token) (interface{}, error) {
+		return []byte(inf.Config.Cdni.JwtDecodingSecret), nil
+	})
+	if err != nil {
+		api.HandleErr(w, r, nil, http.StatusInternalServerError, errors.New("parsing claims"), nil)
+		return
+	}
+	if !token.Valid {
+		api.HandleErr(w, r, nil, http.StatusInternalServerError, errors.New("invalid token"), nil)
+		return
+	}
+
+	var expirationFloat float64
+	var ucdn string
+	var dcdn string
+	for key, val := range claims {
+		switch key {
+		case "iss":
+			ucdn = val.(string)
+		case "aud":
+			dcdn = val.(string)
+		case "exp":
+			expirationFloat = val.(float64)

Review comment:
       Should we use non-panicking type assertions here instead? Are these being parsed from trusted input?
   
   Also, it's not clear what `"iss"`, `"aud"`, and `"exp"` are short for -- do they have constants defined in the `jwt` library?

##########
File path: traffic_ops/traffic_ops_golang/cdni/capacity.go
##########
@@ -0,0 +1,177 @@
+package cdni
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import (
+	"fmt"
+
+	"github.com/lib/pq"
+
+	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/api"
+)
+
+const totalLimitsQuery = `SELECT limit_type, maximum_hard, maximum_soft, ctl.telemetry_id, ctl.telemetry_metric, t.id, t.type, tm.name FROM cdni_total_limits AS ctl LEFT JOIN cdni_telemetry as t ON telemetry_id = t.id LEFT JOIN cdni_telemetry_metrics as tm ON telemetry_metric = tm.name WHERE ctl.capability_id = $1`
+const hostLimitsQuery = `SELECT limit_type, maximum_hard, maximum_soft, chl.telemetry_id, chl.telemetry_metric, t.id, t.type, tm.name, host FROM cdni_host_limits AS chl LEFT JOIN cdni_telemetry as t ON telemetry_id = t.id LEFT JOIN cdni_telemetry_metrics as tm ON telemetry_metric = tm.name WHERE chl.capability_id = $1 ORDER BY host DESC`
+
+func GetCapacities(inf *api.APIInfo, ucdn string) (Capabilities, error) {

Review comment:
       nit: this function appears to only be used within the `cdni` package -- can it be private instead?

##########
File path: traffic_ops/traffic_ops_golang/cdni/telemetry.go
##########
@@ -0,0 +1,112 @@
+package cdni
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import (
+	"errors"
+	"fmt"
+
+	"github.com/lib/pq"
+
+	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/api"
+)
+
+func GetTelemetries(inf *api.APIInfo, ucdn string) (Capabilities, error) {

Review comment:
       nit: this function appears to only be used within the `cdni` package -- can it be private instead?

##########
File path: traffic_ops/traffic_ops_golang/cdni/telemetry.go
##########
@@ -0,0 +1,112 @@
+package cdni
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import (
+	"errors"
+	"fmt"
+
+	"github.com/lib/pq"
+
+	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/api"
+)
+
+func GetTelemetries(inf *api.APIInfo, ucdn string) (Capabilities, error) {
+	capRows, err := inf.Tx.Tx.Query(CapabilityQuery, FciTelemetry, ucdn)
+	if err != nil {
+		return Capabilities{}, fmt.Errorf("querying capabilities: %w", err)
+	}
+	defer capRows.Close()
+	capabilities := []CapabilityQueryResponse{}
+	for capRows.Next() {
+		var capability CapabilityQueryResponse
+		if err := capRows.Scan(&capability.Id, &capability.Type, &capability.UCdn); err != nil {
+			return Capabilities{}, fmt.Errorf("scanning db rows: %w", err)
+		}
+		capabilities = append(capabilities, capability)
+	}
+
+	fciCaps := Capabilities{}
+
+	for _, cap := range capabilities {
+		fciCap := Capability{}
+		footRows, err := inf.Tx.Tx.Query(FootprintQuery, cap.Id)
+		if err != nil {
+			return Capabilities{}, fmt.Errorf("querying footprints: %w", err)
+		}
+		defer footRows.Close()

Review comment:
       nit: could use `log.Close`

##########
File path: traffic_ops/traffic_ops_golang/cdni/telemetry.go
##########
@@ -0,0 +1,112 @@
+package cdni
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import (
+	"errors"
+	"fmt"
+
+	"github.com/lib/pq"
+
+	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/api"
+)
+
+func GetTelemetries(inf *api.APIInfo, ucdn string) (Capabilities, error) {
+	capRows, err := inf.Tx.Tx.Query(CapabilityQuery, FciTelemetry, ucdn)
+	if err != nil {
+		return Capabilities{}, fmt.Errorf("querying capabilities: %w", err)
+	}
+	defer capRows.Close()
+	capabilities := []CapabilityQueryResponse{}
+	for capRows.Next() {
+		var capability CapabilityQueryResponse
+		if err := capRows.Scan(&capability.Id, &capability.Type, &capability.UCdn); err != nil {
+			return Capabilities{}, fmt.Errorf("scanning db rows: %w", err)
+		}
+		capabilities = append(capabilities, capability)
+	}
+
+	fciCaps := Capabilities{}
+
+	for _, cap := range capabilities {
+		fciCap := Capability{}
+		footRows, err := inf.Tx.Tx.Query(FootprintQuery, cap.Id)
+		if err != nil {
+			return Capabilities{}, fmt.Errorf("querying footprints: %w", err)
+		}
+		defer footRows.Close()
+		footprints := []Footprint{}
+		for footRows.Next() {
+			var footprint Footprint
+			if err := footRows.Scan(&footprint.FootprintType, pq.Array(&footprint.FootprintValue)); err != nil {
+				return Capabilities{}, fmt.Errorf("scanning db rows: %w", err)
+			}
+			footprints = append(footprints, footprint)
+		}
+
+		fciCap.Footprints = footprints
+
+		rows, err := inf.Tx.Tx.Query(`SELECT id, type FROM cdni_telemetry WHERE capability_id = $1`, cap.Id)
+		if err != nil {
+			return Capabilities{}, errors.New("querying cdni telemetry: " + err.Error())
+		}
+		defer rows.Close()
+		returnList := []Telemetry{}
+		telemetryList := []Telemetry{}
+		for rows.Next() {
+			telemetry := Telemetry{}
+			if err := rows.Scan(&telemetry.Id, &telemetry.Type); err != nil {
+				return Capabilities{}, errors.New("scanning telemetry: " + err.Error())
+			}
+			telemetryList = append(telemetryList, telemetry)
+		}
+
+		for _, t := range telemetryList {
+			tmRows, err := inf.Tx.Tx.Query(`SELECT name, time_granularity, data_percentile, latency FROM cdni_telemetry_metrics WHERE telemetry_id = $1`, t.Id)
+			if err != nil {
+				return Capabilities{}, errors.New("querying cdni telemetry metrics: " + err.Error())
+			}
+			defer tmRows.Close()

Review comment:
       nit: could use `log.Close`

##########
File path: traffic_ops/traffic_ops_golang/cdni/telemetry.go
##########
@@ -0,0 +1,112 @@
+package cdni
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import (
+	"errors"
+	"fmt"
+
+	"github.com/lib/pq"
+
+	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/api"
+)
+
+func GetTelemetries(inf *api.APIInfo, ucdn string) (Capabilities, error) {
+	capRows, err := inf.Tx.Tx.Query(CapabilityQuery, FciTelemetry, ucdn)
+	if err != nil {
+		return Capabilities{}, fmt.Errorf("querying capabilities: %w", err)
+	}
+	defer capRows.Close()
+	capabilities := []CapabilityQueryResponse{}
+	for capRows.Next() {
+		var capability CapabilityQueryResponse
+		if err := capRows.Scan(&capability.Id, &capability.Type, &capability.UCdn); err != nil {
+			return Capabilities{}, fmt.Errorf("scanning db rows: %w", err)
+		}
+		capabilities = append(capabilities, capability)
+	}
+
+	fciCaps := Capabilities{}
+
+	for _, cap := range capabilities {
+		fciCap := Capability{}
+		footRows, err := inf.Tx.Tx.Query(FootprintQuery, cap.Id)
+		if err != nil {
+			return Capabilities{}, fmt.Errorf("querying footprints: %w", err)
+		}
+		defer footRows.Close()
+		footprints := []Footprint{}
+		for footRows.Next() {
+			var footprint Footprint
+			if err := footRows.Scan(&footprint.FootprintType, pq.Array(&footprint.FootprintValue)); err != nil {
+				return Capabilities{}, fmt.Errorf("scanning db rows: %w", err)
+			}
+			footprints = append(footprints, footprint)
+		}
+
+		fciCap.Footprints = footprints
+
+		rows, err := inf.Tx.Tx.Query(`SELECT id, type FROM cdni_telemetry WHERE capability_id = $1`, cap.Id)
+		if err != nil {
+			return Capabilities{}, errors.New("querying cdni telemetry: " + err.Error())
+		}
+		defer rows.Close()

Review comment:
       nit: could use `log.Close`

##########
File path: traffic_ops/traffic_ops_golang/cdni/telemetry.go
##########
@@ -0,0 +1,112 @@
+package cdni
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import (
+	"errors"
+	"fmt"
+
+	"github.com/lib/pq"
+
+	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/api"
+)
+
+func GetTelemetries(inf *api.APIInfo, ucdn string) (Capabilities, error) {
+	capRows, err := inf.Tx.Tx.Query(CapabilityQuery, FciTelemetry, ucdn)
+	if err != nil {
+		return Capabilities{}, fmt.Errorf("querying capabilities: %w", err)
+	}
+	defer capRows.Close()
+	capabilities := []CapabilityQueryResponse{}
+	for capRows.Next() {
+		var capability CapabilityQueryResponse
+		if err := capRows.Scan(&capability.Id, &capability.Type, &capability.UCdn); err != nil {
+			return Capabilities{}, fmt.Errorf("scanning db rows: %w", err)
+		}
+		capabilities = append(capabilities, capability)
+	}
+
+	fciCaps := Capabilities{}
+
+	for _, cap := range capabilities {
+		fciCap := Capability{}
+		footRows, err := inf.Tx.Tx.Query(FootprintQuery, cap.Id)

Review comment:
       Similar to a prior comment, is it possible to come up with a single query here (or a constant number of queries) that returns all the data we need instead of making a lot of small queries?

##########
File path: traffic_ops/traffic_ops_golang/cdni/shared.go
##########
@@ -0,0 +1,199 @@
+package cdni
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import (
+	"errors"
+	"net/http"
+	"time"
+
+	"github.com/dgrijalva/jwt-go"
+
+	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/api"
+)
+
+const CapabilityQuery = `SELECT id, type, ucdn FROM cdni_capabilities WHERE type = $1 AND ucdn = $2`
+const FootprintQuery = `SELECT footprint_type, footprint_value::text[] FROM cdni_footprints WHERE capability_id = $1`
+
+func GetCapabilities(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()
+
+	bearerToken := r.Header.Get("Authorization")
+
+	if inf.Config.Cdni == nil || inf.Config.Cdni.JwtDecodingSecret == "" || inf.Config.Cdni.DCdnId == "" {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, errors.New("cdn.conf does not contain CDNi information"))
+		return
+	}
+
+	claims := jwt.MapClaims{}
+	token, err := jwt.ParseWithClaims(bearerToken, claims, func(token *jwt.Token) (interface{}, error) {
+		return []byte(inf.Config.Cdni.JwtDecodingSecret), nil
+	})
+	if err != nil {
+		api.HandleErr(w, r, nil, http.StatusInternalServerError, errors.New("parsing claims"), nil)
+		return
+	}
+	if !token.Valid {
+		api.HandleErr(w, r, nil, http.StatusInternalServerError, errors.New("invalid token"), nil)
+		return
+	}
+
+	var expirationFloat float64
+	var ucdn string
+	var dcdn string
+	for key, val := range claims {
+		switch key {
+		case "iss":
+			ucdn = val.(string)
+		case "aud":
+			dcdn = val.(string)
+		case "exp":
+			expirationFloat = val.(float64)
+		}
+	}
+
+	expiration := int64(expirationFloat)
+
+	if expiration < time.Now().Unix() {
+		api.HandleErr(w, r, nil, http.StatusForbidden, errors.New("token is expired"), nil)
+		return
+	}
+	if dcdn != inf.Config.Cdni.DCdnId {
+		api.HandleErr(w, r, nil, http.StatusForbidden, errors.New("invalid token"), nil)
+		return
+	}
+	if ucdn == "" {
+		api.HandleErr(w, r, nil, http.StatusForbidden, errors.New("invalid token"), nil)
+		return
+	}
+
+	capacities, err := GetCapacities(inf, ucdn)
+	if err != nil {
+		api.HandleErr(w, r, nil, http.StatusInternalServerError, err, nil)
+		return
+	}
+
+	telemetries, err := GetTelemetries(inf, ucdn)
+	if err != nil {
+		api.HandleErr(w, r, nil, http.StatusInternalServerError, err, nil)
+		return
+	}
+
+	fciCaps := Capabilities{}
+	capsList := []Capability{}

Review comment:
       nit: this could use `make([]Capability, 0, len(capacities.Capabilities) + len(telemetries.Capabilities))` to reduce allocations

##########
File path: traffic_ops/traffic_ops_golang/cdni/capacity.go
##########
@@ -0,0 +1,177 @@
+package cdni
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import (
+	"fmt"
+
+	"github.com/lib/pq"
+
+	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/api"
+)
+
+const totalLimitsQuery = `SELECT limit_type, maximum_hard, maximum_soft, ctl.telemetry_id, ctl.telemetry_metric, t.id, t.type, tm.name FROM cdni_total_limits AS ctl LEFT JOIN cdni_telemetry as t ON telemetry_id = t.id LEFT JOIN cdni_telemetry_metrics as tm ON telemetry_metric = tm.name WHERE ctl.capability_id = $1`
+const hostLimitsQuery = `SELECT limit_type, maximum_hard, maximum_soft, chl.telemetry_id, chl.telemetry_metric, t.id, t.type, tm.name, host FROM cdni_host_limits AS chl LEFT JOIN cdni_telemetry as t ON telemetry_id = t.id LEFT JOIN cdni_telemetry_metrics as tm ON telemetry_metric = tm.name WHERE chl.capability_id = $1 ORDER BY host DESC`
+
+func GetCapacities(inf *api.APIInfo, ucdn string) (Capabilities, error) {
+	capRows, err := inf.Tx.Tx.Query(CapabilityQuery, FciCapacityLimits, ucdn)
+	if err != nil {
+		return Capabilities{}, fmt.Errorf("querying capabilities: %w", err)
+	}
+	defer capRows.Close()
+	capabilities := []CapabilityQueryResponse{}
+	for capRows.Next() {
+		var capability CapabilityQueryResponse
+		if err := capRows.Scan(&capability.Id, &capability.Type, &capability.UCdn); err != nil {
+			return Capabilities{}, fmt.Errorf("scanning db rows: %w", err)
+		}
+		capabilities = append(capabilities, capability)
+	}
+
+	fciCaps := Capabilities{}
+
+	for _, cap := range capabilities {
+		fciCap := Capability{}
+		footRows, err := inf.Tx.Tx.Query(FootprintQuery, cap.Id)
+		if err != nil {
+			return Capabilities{}, fmt.Errorf("querying footprints: %w", err)
+		}
+		defer footRows.Close()
+		footprints := []Footprint{}
+		for footRows.Next() {
+			var footprint Footprint
+			if err := footRows.Scan(&footprint.FootprintType, pq.Array(&footprint.FootprintValue)); err != nil {
+				return Capabilities{}, fmt.Errorf("scanning db rows: %w", err)
+			}
+			footprints = append(footprints, footprint)
+		}
+
+		fciCap.Footprints = footprints
+
+		tlRows, err := inf.Tx.Tx.Query(totalLimitsQuery, cap.Id)
+		if err != nil {
+			return Capabilities{}, fmt.Errorf("querying total limits: %w", err)
+		}
+
+		defer tlRows.Close()
+		totalLimits := []TotalLimitsQueryResponse{}
+		for tlRows.Next() {
+			var totalLimit TotalLimitsQueryResponse
+			if err := tlRows.Scan(&totalLimit.LimitType, &totalLimit.MaximumHard, &totalLimit.MaximumSoft, &totalLimit.TelemetryId, &totalLimit.TelemetryMetic, &totalLimit.Id, &totalLimit.Type, &totalLimit.Name); err != nil {
+				return Capabilities{}, fmt.Errorf("scanning db rows: %w", err)
+			}
+			totalLimits = append(totalLimits, totalLimit)
+		}
+
+		hlRows, err := inf.Tx.Tx.Query(hostLimitsQuery, cap.Id)
+		if err != nil {
+			return Capabilities{}, fmt.Errorf("querying host limits: %w", err)
+		}
+
+		defer hlRows.Close()

Review comment:
       nit: could use `log.Close`

##########
File path: traffic_ops/traffic_ops_golang/cdni/shared.go
##########
@@ -0,0 +1,199 @@
+package cdni
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import (
+	"errors"
+	"net/http"
+	"time"
+
+	"github.com/dgrijalva/jwt-go"
+
+	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/api"
+)
+
+const CapabilityQuery = `SELECT id, type, ucdn FROM cdni_capabilities WHERE type = $1 AND ucdn = $2`
+const FootprintQuery = `SELECT footprint_type, footprint_value::text[] FROM cdni_footprints WHERE capability_id = $1`
+
+func GetCapabilities(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()
+
+	bearerToken := r.Header.Get("Authorization")
+
+	if inf.Config.Cdni == nil || inf.Config.Cdni.JwtDecodingSecret == "" || inf.Config.Cdni.DCdnId == "" {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, errors.New("cdn.conf does not contain CDNi information"))
+		return
+	}
+
+	claims := jwt.MapClaims{}
+	token, err := jwt.ParseWithClaims(bearerToken, claims, func(token *jwt.Token) (interface{}, error) {
+		return []byte(inf.Config.Cdni.JwtDecodingSecret), nil
+	})
+	if err != nil {
+		api.HandleErr(w, r, nil, http.StatusInternalServerError, errors.New("parsing claims"), nil)
+		return
+	}
+	if !token.Valid {
+		api.HandleErr(w, r, nil, http.StatusInternalServerError, errors.New("invalid token"), nil)
+		return
+	}
+
+	var expirationFloat float64
+	var ucdn string
+	var dcdn string
+	for key, val := range claims {
+		switch key {
+		case "iss":
+			ucdn = val.(string)
+		case "aud":
+			dcdn = val.(string)
+		case "exp":
+			expirationFloat = val.(float64)
+		}
+	}
+
+	expiration := int64(expirationFloat)
+
+	if expiration < time.Now().Unix() {
+		api.HandleErr(w, r, nil, http.StatusForbidden, errors.New("token is expired"), nil)
+		return
+	}
+	if dcdn != inf.Config.Cdni.DCdnId {
+		api.HandleErr(w, r, nil, http.StatusForbidden, errors.New("invalid token"), nil)

Review comment:
       Do we want to hide the fact that the DCdnId didn't match from the user? 

##########
File path: traffic_ops/traffic_ops_golang/cdni/shared.go
##########
@@ -0,0 +1,199 @@
+package cdni
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import (
+	"errors"
+	"net/http"
+	"time"
+
+	"github.com/dgrijalva/jwt-go"
+
+	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/api"
+)
+
+const CapabilityQuery = `SELECT id, type, ucdn FROM cdni_capabilities WHERE type = $1 AND ucdn = $2`
+const FootprintQuery = `SELECT footprint_type, footprint_value::text[] FROM cdni_footprints WHERE capability_id = $1`
+
+func GetCapabilities(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()
+
+	bearerToken := r.Header.Get("Authorization")
+
+	if inf.Config.Cdni == nil || inf.Config.Cdni.JwtDecodingSecret == "" || inf.Config.Cdni.DCdnId == "" {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, errors.New("cdn.conf does not contain CDNi information"))
+		return
+	}
+
+	claims := jwt.MapClaims{}
+	token, err := jwt.ParseWithClaims(bearerToken, claims, func(token *jwt.Token) (interface{}, error) {
+		return []byte(inf.Config.Cdni.JwtDecodingSecret), nil
+	})
+	if err != nil {
+		api.HandleErr(w, r, nil, http.StatusInternalServerError, errors.New("parsing claims"), nil)
+		return
+	}
+	if !token.Valid {
+		api.HandleErr(w, r, nil, http.StatusInternalServerError, errors.New("invalid token"), nil)
+		return
+	}
+
+	var expirationFloat float64
+	var ucdn string
+	var dcdn string
+	for key, val := range claims {
+		switch key {
+		case "iss":
+			ucdn = val.(string)
+		case "aud":
+			dcdn = val.(string)
+		case "exp":
+			expirationFloat = val.(float64)
+		}
+	}
+
+	expiration := int64(expirationFloat)
+
+	if expiration < time.Now().Unix() {
+		api.HandleErr(w, r, nil, http.StatusForbidden, errors.New("token is expired"), nil)
+		return
+	}
+	if dcdn != inf.Config.Cdni.DCdnId {
+		api.HandleErr(w, r, nil, http.StatusForbidden, errors.New("invalid token"), nil)
+		return
+	}
+	if ucdn == "" {
+		api.HandleErr(w, r, nil, http.StatusForbidden, errors.New("invalid token"), nil)

Review comment:
       Do we want to hide the fact that the ucdn was empty from the user? 

##########
File path: traffic_ops/traffic_ops_golang/cdni/capacity.go
##########
@@ -0,0 +1,177 @@
+package cdni
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import (
+	"fmt"
+
+	"github.com/lib/pq"
+
+	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/api"
+)
+
+const totalLimitsQuery = `SELECT limit_type, maximum_hard, maximum_soft, ctl.telemetry_id, ctl.telemetry_metric, t.id, t.type, tm.name FROM cdni_total_limits AS ctl LEFT JOIN cdni_telemetry as t ON telemetry_id = t.id LEFT JOIN cdni_telemetry_metrics as tm ON telemetry_metric = tm.name WHERE ctl.capability_id = $1`
+const hostLimitsQuery = `SELECT limit_type, maximum_hard, maximum_soft, chl.telemetry_id, chl.telemetry_metric, t.id, t.type, tm.name, host FROM cdni_host_limits AS chl LEFT JOIN cdni_telemetry as t ON telemetry_id = t.id LEFT JOIN cdni_telemetry_metrics as tm ON telemetry_metric = tm.name WHERE chl.capability_id = $1 ORDER BY host DESC`
+
+func GetCapacities(inf *api.APIInfo, ucdn string) (Capabilities, error) {
+	capRows, err := inf.Tx.Tx.Query(CapabilityQuery, FciCapacityLimits, ucdn)
+	if err != nil {
+		return Capabilities{}, fmt.Errorf("querying capabilities: %w", err)
+	}
+	defer capRows.Close()
+	capabilities := []CapabilityQueryResponse{}
+	for capRows.Next() {
+		var capability CapabilityQueryResponse
+		if err := capRows.Scan(&capability.Id, &capability.Type, &capability.UCdn); err != nil {
+			return Capabilities{}, fmt.Errorf("scanning db rows: %w", err)
+		}
+		capabilities = append(capabilities, capability)
+	}
+
+	fciCaps := Capabilities{}
+
+	for _, cap := range capabilities {
+		fciCap := Capability{}
+		footRows, err := inf.Tx.Tx.Query(FootprintQuery, cap.Id)
+		if err != nil {
+			return Capabilities{}, fmt.Errorf("querying footprints: %w", err)
+		}
+		defer footRows.Close()

Review comment:
       nit: could use `log.Close`

##########
File path: traffic_ops/traffic_ops_golang/cdni/shared.go
##########
@@ -0,0 +1,199 @@
+package cdni
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import (
+	"errors"
+	"net/http"
+	"time"
+
+	"github.com/dgrijalva/jwt-go"
+
+	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/api"
+)
+
+const CapabilityQuery = `SELECT id, type, ucdn FROM cdni_capabilities WHERE type = $1 AND ucdn = $2`
+const FootprintQuery = `SELECT footprint_type, footprint_value::text[] FROM cdni_footprints WHERE capability_id = $1`
+
+func GetCapabilities(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()
+
+	bearerToken := r.Header.Get("Authorization")

Review comment:
       Since this header seems required, should we immediately return an error here if it's empty? Otherwise, I think it would return a probably uninformative error on L55.

##########
File path: traffic_ops/traffic_ops_golang/cdni/telemetry.go
##########
@@ -0,0 +1,112 @@
+package cdni
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import (
+	"errors"
+	"fmt"
+
+	"github.com/lib/pq"
+
+	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/api"
+)
+
+func GetTelemetries(inf *api.APIInfo, ucdn string) (Capabilities, error) {
+	capRows, err := inf.Tx.Tx.Query(CapabilityQuery, FciTelemetry, ucdn)
+	if err != nil {
+		return Capabilities{}, fmt.Errorf("querying capabilities: %w", err)
+	}
+	defer capRows.Close()

Review comment:
       nit: could use `log.Close`

##########
File path: traffic_ops/traffic_ops_golang/cdni/shared.go
##########
@@ -0,0 +1,199 @@
+package cdni
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import (
+	"errors"
+	"net/http"
+	"time"
+
+	"github.com/dgrijalva/jwt-go"
+
+	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/api"
+)
+
+const CapabilityQuery = `SELECT id, type, ucdn FROM cdni_capabilities WHERE type = $1 AND ucdn = $2`
+const FootprintQuery = `SELECT footprint_type, footprint_value::text[] FROM cdni_footprints WHERE capability_id = $1`
+
+func GetCapabilities(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()
+
+	bearerToken := r.Header.Get("Authorization")
+
+	if inf.Config.Cdni == nil || inf.Config.Cdni.JwtDecodingSecret == "" || inf.Config.Cdni.DCdnId == "" {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, errors.New("cdn.conf does not contain CDNi information"))
+		return
+	}
+
+	claims := jwt.MapClaims{}
+	token, err := jwt.ParseWithClaims(bearerToken, claims, func(token *jwt.Token) (interface{}, error) {
+		return []byte(inf.Config.Cdni.JwtDecodingSecret), nil
+	})
+	if err != nil {
+		api.HandleErr(w, r, nil, http.StatusInternalServerError, errors.New("parsing claims"), nil)

Review comment:
       Should this returned error include the error message from `jwt.ParseWithClaims`?




-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@trafficcontrol.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [trafficcontrol] ocket8888 commented on pull request #6524: Added CDNi Capacity and Telemetry

Posted by GitBox <gi...@apache.org>.
ocket8888 commented on pull request #6524:
URL: https://github.com/apache/trafficcontrol/pull/6524#issuecomment-1022614271


   > ... for CDNi integration
   
   What's that?


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@trafficcontrol.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [trafficcontrol] rawlinp commented on a change in pull request #6524: Added CDNi Capacity and Telemetry

Posted by GitBox <gi...@apache.org>.
rawlinp commented on a change in pull request #6524:
URL: https://github.com/apache/trafficcontrol/pull/6524#discussion_r804068000



##########
File path: traffic_ops/traffic_ops_golang/cdni/shared.go
##########
@@ -0,0 +1,360 @@
+package cdni
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import (
+	"database/sql"
+	"errors"
+	"fmt"
+	"github.com/lib/pq"
+	"net/http"
+	"time"
+
+	"github.com/apache/trafficcontrol/lib/go-log"
+	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/api"
+
+	"github.com/dgrijalva/jwt-go"
+)
+
+const CapabilityQuery = `SELECT id, type, ucdn FROM cdni_capabilities WHERE type = $1 AND ucdn = $2`
+const AllFootprintQuery = `SELECT footprint_type, footprint_value::text[], capability_id FROM cdni_footprints`
+
+const totalLimitsQuery = `
+SELECT limit_type, maximum_hard, maximum_soft, ctl.telemetry_id, ctl.telemetry_metric, t.id, t.type, tm.name, ctl.capability_id 
+FROM cdni_total_limits AS ctl 
+LEFT JOIN cdni_telemetry as t ON telemetry_id = t.id 
+LEFT JOIN cdni_telemetry_metrics as tm ON telemetry_metric = tm.name`
+
+const hostLimitsQuery = `
+SELECT limit_type, maximum_hard, maximum_soft, chl.telemetry_id, chl.telemetry_metric, t.id, t.type, tm.name, host, chl.capability_id 
+FROM cdni_host_limits AS chl 
+LEFT JOIN cdni_telemetry as t ON telemetry_id = t.id 
+LEFT JOIN cdni_telemetry_metrics as tm ON telemetry_metric = tm.name 
+ORDER BY host DESC`
+
+func GetCapabilities(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()
+
+	bearerToken := r.Header.Get("Authorization")
+	if bearerToken == "" {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusBadRequest, errors.New("bearer token header is required"), nil)
+		return
+	}
+
+	if inf.Config.Cdni == nil || inf.Config.Cdni.JwtDecodingSecret == "" || inf.Config.Cdni.DCdnId == "" {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, errors.New("cdn.conf does not contain CDNi information"))
+		return
+	}
+
+	claims := jwt.MapClaims{}
+	token, err := jwt.ParseWithClaims(bearerToken, claims, func(token *jwt.Token) (interface{}, error) {
+		return []byte(inf.Config.Cdni.JwtDecodingSecret), nil
+	})
+	if err != nil {
+		api.HandleErr(w, r, nil, http.StatusInternalServerError, fmt.Errorf("parsing claims: %w", err), nil)
+		return
+	}
+	if !token.Valid {
+		api.HandleErr(w, r, nil, http.StatusInternalServerError, errors.New("invalid token"), nil)
+		return
+	}
+
+	var expirationFloat float64
+	var ucdn string
+	var dcdn string
+	for key, val := range claims {
+		switch key {
+		case "iss":
+			if _, ok := val.(string); !ok {
+				api.HandleErr(w, r, nil, http.StatusBadRequest, errors.New("invalid token - iss (Issuer) must be a string"), nil)
+				return
+			}
+			ucdn = val.(string)
+		case "aud":
+			if _, ok := val.(string); !ok {
+				api.HandleErr(w, r, nil, http.StatusBadRequest, errors.New("invalid token - aud (Audience) must be a string"), nil)
+				return
+			}
+			dcdn = val.(string)
+		case "exp":
+			if _, ok := val.(float64); !ok {
+				api.HandleErr(w, r, nil, http.StatusBadRequest, errors.New("invalid token - exp (Expiration) must be a float64"), nil)
+				return
+			}
+			expirationFloat = val.(float64)
+		}
+	}
+
+	expiration := int64(expirationFloat)
+
+	if expiration < time.Now().Unix() {
+		api.HandleErr(w, r, nil, http.StatusForbidden, errors.New("token is expired"), nil)
+		return
+	}
+	if dcdn != inf.Config.Cdni.DCdnId {
+		api.HandleErr(w, r, nil, http.StatusForbidden, errors.New("invalid token - incorrect dcdn"), nil)
+		return
+	}
+	if ucdn == "" {
+		api.HandleErr(w, r, nil, http.StatusForbidden, errors.New("invalid token - empty ucdn field"), nil)
+		return
+	}
+
+	capacities, err := getCapacities(inf, ucdn)
+	if err != nil {
+		api.HandleErr(w, r, nil, http.StatusInternalServerError, err, nil)
+		return
+	}
+
+	telemetries, err := getTelemetries(inf, ucdn)
+	if err != nil {
+		api.HandleErr(w, r, nil, http.StatusInternalServerError, err, nil)
+		return
+	}
+
+	fciCaps := Capabilities{}
+	capsList := make([]Capability, 0, len(capacities.Capabilities)+len(telemetries.Capabilities))
+	capsList = append(capsList, capacities.Capabilities...)
+	capsList = append(capsList, telemetries.Capabilities...)
+
+	fciCaps.Capabilities = capsList
+
+	api.WriteRespRaw(w, r, fciCaps)
+}
+
+func getFootprintMap(tx *sql.Tx) (map[int][]Footprint, error) {
+	footRows, err := tx.Query(AllFootprintQuery)
+	if err != nil {
+		return nil, fmt.Errorf("querying footprints: %w", err)
+	}
+	defer log.Close(footRows, "closing foorpint query")
+	footprintMap := map[int][]Footprint{}
+	for footRows.Next() {
+		var footprint Footprint
+		if err := footRows.Scan(&footprint.FootprintType, pq.Array(&footprint.FootprintValue), &footprint.CapabilityId); err != nil {
+			return nil, fmt.Errorf("scanning db rows: %w", err)
+		}
+		val, ok := footprintMap[footprint.CapabilityId]
+		if !ok {
+			footprintMap[footprint.CapabilityId] = []Footprint{footprint}
+		} else {
+			val = append(val, footprint)
+			footprintMap[footprint.CapabilityId] = val
+		}
+	}
+
+	return footprintMap, nil
+}
+
+func getTotalLimitsMap(tx *sql.Tx) (map[int][]TotalLimitsQueryResponse, error) {
+	tlRows, err := tx.Query(totalLimitsQuery)
+	if err != nil {
+		return nil, fmt.Errorf("querying total limits: %w", err)
+	}
+
+	defer log.Close(tlRows, "closing total capacity limits query")
+	totalLimitsMap := map[int][]TotalLimitsQueryResponse{}
+	for tlRows.Next() {
+		var totalLimit TotalLimitsQueryResponse
+		if err := tlRows.Scan(&totalLimit.LimitType, &totalLimit.MaximumHard, &totalLimit.MaximumSoft, &totalLimit.TelemetryId, &totalLimit.TelemetryMetic, &totalLimit.Id, &totalLimit.Type, &totalLimit.Name, &totalLimit.CapabilityId); err != nil {
+			return nil, fmt.Errorf("scanning db rows: %w", err)
+		}
+
+		val, ok := totalLimitsMap[totalLimit.CapabilityId]
+		if !ok {
+			totalLimitsMap[totalLimit.CapabilityId] = []TotalLimitsQueryResponse{totalLimit}
+		} else {
+			val = append(val, totalLimit)
+			totalLimitsMap[totalLimit.CapabilityId] = val

Review comment:
       I believe my prior comment applies here as well

##########
File path: traffic_ops/traffic_ops_golang/cdni/shared.go
##########
@@ -0,0 +1,360 @@
+package cdni
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import (
+	"database/sql"
+	"errors"
+	"fmt"
+	"github.com/lib/pq"
+	"net/http"
+	"time"
+
+	"github.com/apache/trafficcontrol/lib/go-log"
+	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/api"
+
+	"github.com/dgrijalva/jwt-go"
+)
+
+const CapabilityQuery = `SELECT id, type, ucdn FROM cdni_capabilities WHERE type = $1 AND ucdn = $2`
+const AllFootprintQuery = `SELECT footprint_type, footprint_value::text[], capability_id FROM cdni_footprints`
+
+const totalLimitsQuery = `
+SELECT limit_type, maximum_hard, maximum_soft, ctl.telemetry_id, ctl.telemetry_metric, t.id, t.type, tm.name, ctl.capability_id 
+FROM cdni_total_limits AS ctl 
+LEFT JOIN cdni_telemetry as t ON telemetry_id = t.id 
+LEFT JOIN cdni_telemetry_metrics as tm ON telemetry_metric = tm.name`
+
+const hostLimitsQuery = `
+SELECT limit_type, maximum_hard, maximum_soft, chl.telemetry_id, chl.telemetry_metric, t.id, t.type, tm.name, host, chl.capability_id 
+FROM cdni_host_limits AS chl 
+LEFT JOIN cdni_telemetry as t ON telemetry_id = t.id 
+LEFT JOIN cdni_telemetry_metrics as tm ON telemetry_metric = tm.name 
+ORDER BY host DESC`
+
+func GetCapabilities(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()
+
+	bearerToken := r.Header.Get("Authorization")
+	if bearerToken == "" {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusBadRequest, errors.New("bearer token header is required"), nil)
+		return
+	}
+
+	if inf.Config.Cdni == nil || inf.Config.Cdni.JwtDecodingSecret == "" || inf.Config.Cdni.DCdnId == "" {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, errors.New("cdn.conf does not contain CDNi information"))
+		return
+	}
+
+	claims := jwt.MapClaims{}
+	token, err := jwt.ParseWithClaims(bearerToken, claims, func(token *jwt.Token) (interface{}, error) {
+		return []byte(inf.Config.Cdni.JwtDecodingSecret), nil
+	})
+	if err != nil {
+		api.HandleErr(w, r, nil, http.StatusInternalServerError, fmt.Errorf("parsing claims: %w", err), nil)
+		return
+	}
+	if !token.Valid {
+		api.HandleErr(w, r, nil, http.StatusInternalServerError, errors.New("invalid token"), nil)
+		return
+	}
+
+	var expirationFloat float64
+	var ucdn string
+	var dcdn string
+	for key, val := range claims {
+		switch key {
+		case "iss":
+			if _, ok := val.(string); !ok {
+				api.HandleErr(w, r, nil, http.StatusBadRequest, errors.New("invalid token - iss (Issuer) must be a string"), nil)
+				return
+			}
+			ucdn = val.(string)
+		case "aud":
+			if _, ok := val.(string); !ok {
+				api.HandleErr(w, r, nil, http.StatusBadRequest, errors.New("invalid token - aud (Audience) must be a string"), nil)
+				return
+			}
+			dcdn = val.(string)
+		case "exp":
+			if _, ok := val.(float64); !ok {
+				api.HandleErr(w, r, nil, http.StatusBadRequest, errors.New("invalid token - exp (Expiration) must be a float64"), nil)
+				return
+			}
+			expirationFloat = val.(float64)
+		}
+	}
+
+	expiration := int64(expirationFloat)
+
+	if expiration < time.Now().Unix() {
+		api.HandleErr(w, r, nil, http.StatusForbidden, errors.New("token is expired"), nil)
+		return
+	}
+	if dcdn != inf.Config.Cdni.DCdnId {
+		api.HandleErr(w, r, nil, http.StatusForbidden, errors.New("invalid token - incorrect dcdn"), nil)
+		return
+	}
+	if ucdn == "" {
+		api.HandleErr(w, r, nil, http.StatusForbidden, errors.New("invalid token - empty ucdn field"), nil)
+		return
+	}
+
+	capacities, err := getCapacities(inf, ucdn)
+	if err != nil {
+		api.HandleErr(w, r, nil, http.StatusInternalServerError, err, nil)
+		return
+	}
+
+	telemetries, err := getTelemetries(inf, ucdn)
+	if err != nil {
+		api.HandleErr(w, r, nil, http.StatusInternalServerError, err, nil)
+		return
+	}
+
+	fciCaps := Capabilities{}
+	capsList := make([]Capability, 0, len(capacities.Capabilities)+len(telemetries.Capabilities))
+	capsList = append(capsList, capacities.Capabilities...)
+	capsList = append(capsList, telemetries.Capabilities...)
+
+	fciCaps.Capabilities = capsList
+
+	api.WriteRespRaw(w, r, fciCaps)
+}
+
+func getFootprintMap(tx *sql.Tx) (map[int][]Footprint, error) {
+	footRows, err := tx.Query(AllFootprintQuery)
+	if err != nil {
+		return nil, fmt.Errorf("querying footprints: %w", err)
+	}
+	defer log.Close(footRows, "closing foorpint query")
+	footprintMap := map[int][]Footprint{}
+	for footRows.Next() {
+		var footprint Footprint
+		if err := footRows.Scan(&footprint.FootprintType, pq.Array(&footprint.FootprintValue), &footprint.CapabilityId); err != nil {
+			return nil, fmt.Errorf("scanning db rows: %w", err)
+		}
+		val, ok := footprintMap[footprint.CapabilityId]
+		if !ok {
+			footprintMap[footprint.CapabilityId] = []Footprint{footprint}
+		} else {
+			val = append(val, footprint)
+			footprintMap[footprint.CapabilityId] = val

Review comment:
       I believe you might be able to replace this with this:
   ```
   footPrintMap[footprint.CapabilityId] = append(footPrintMap[footprint.CapabilityId], footprint)
   ```
   because `append` can handle null/empty slices properly (no need to initialize them manually first): https://go.dev/play/p/7SUhd7bDEus

##########
File path: traffic_ops/traffic_ops_golang/cdni/shared.go
##########
@@ -0,0 +1,360 @@
+package cdni
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import (
+	"database/sql"
+	"errors"
+	"fmt"
+	"github.com/lib/pq"
+	"net/http"
+	"time"
+
+	"github.com/apache/trafficcontrol/lib/go-log"
+	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/api"
+
+	"github.com/dgrijalva/jwt-go"
+)
+
+const CapabilityQuery = `SELECT id, type, ucdn FROM cdni_capabilities WHERE type = $1 AND ucdn = $2`
+const AllFootprintQuery = `SELECT footprint_type, footprint_value::text[], capability_id FROM cdni_footprints`
+
+const totalLimitsQuery = `
+SELECT limit_type, maximum_hard, maximum_soft, ctl.telemetry_id, ctl.telemetry_metric, t.id, t.type, tm.name, ctl.capability_id 
+FROM cdni_total_limits AS ctl 
+LEFT JOIN cdni_telemetry as t ON telemetry_id = t.id 
+LEFT JOIN cdni_telemetry_metrics as tm ON telemetry_metric = tm.name`
+
+const hostLimitsQuery = `
+SELECT limit_type, maximum_hard, maximum_soft, chl.telemetry_id, chl.telemetry_metric, t.id, t.type, tm.name, host, chl.capability_id 
+FROM cdni_host_limits AS chl 
+LEFT JOIN cdni_telemetry as t ON telemetry_id = t.id 
+LEFT JOIN cdni_telemetry_metrics as tm ON telemetry_metric = tm.name 
+ORDER BY host DESC`
+
+func GetCapabilities(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()
+
+	bearerToken := r.Header.Get("Authorization")
+	if bearerToken == "" {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusBadRequest, errors.New("bearer token header is required"), nil)
+		return
+	}
+
+	if inf.Config.Cdni == nil || inf.Config.Cdni.JwtDecodingSecret == "" || inf.Config.Cdni.DCdnId == "" {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, errors.New("cdn.conf does not contain CDNi information"))
+		return
+	}
+
+	claims := jwt.MapClaims{}
+	token, err := jwt.ParseWithClaims(bearerToken, claims, func(token *jwt.Token) (interface{}, error) {
+		return []byte(inf.Config.Cdni.JwtDecodingSecret), nil
+	})
+	if err != nil {
+		api.HandleErr(w, r, nil, http.StatusInternalServerError, fmt.Errorf("parsing claims: %w", err), nil)
+		return
+	}
+	if !token.Valid {
+		api.HandleErr(w, r, nil, http.StatusInternalServerError, errors.New("invalid token"), nil)
+		return
+	}
+
+	var expirationFloat float64
+	var ucdn string
+	var dcdn string
+	for key, val := range claims {
+		switch key {
+		case "iss":
+			if _, ok := val.(string); !ok {
+				api.HandleErr(w, r, nil, http.StatusBadRequest, errors.New("invalid token - iss (Issuer) must be a string"), nil)
+				return
+			}
+			ucdn = val.(string)
+		case "aud":
+			if _, ok := val.(string); !ok {
+				api.HandleErr(w, r, nil, http.StatusBadRequest, errors.New("invalid token - aud (Audience) must be a string"), nil)
+				return
+			}
+			dcdn = val.(string)
+		case "exp":
+			if _, ok := val.(float64); !ok {
+				api.HandleErr(w, r, nil, http.StatusBadRequest, errors.New("invalid token - exp (Expiration) must be a float64"), nil)
+				return
+			}
+			expirationFloat = val.(float64)
+		}
+	}
+
+	expiration := int64(expirationFloat)
+
+	if expiration < time.Now().Unix() {
+		api.HandleErr(w, r, nil, http.StatusForbidden, errors.New("token is expired"), nil)
+		return
+	}
+	if dcdn != inf.Config.Cdni.DCdnId {
+		api.HandleErr(w, r, nil, http.StatusForbidden, errors.New("invalid token - incorrect dcdn"), nil)
+		return
+	}
+	if ucdn == "" {
+		api.HandleErr(w, r, nil, http.StatusForbidden, errors.New("invalid token - empty ucdn field"), nil)
+		return
+	}
+
+	capacities, err := getCapacities(inf, ucdn)
+	if err != nil {
+		api.HandleErr(w, r, nil, http.StatusInternalServerError, err, nil)
+		return
+	}
+
+	telemetries, err := getTelemetries(inf, ucdn)
+	if err != nil {
+		api.HandleErr(w, r, nil, http.StatusInternalServerError, err, nil)
+		return
+	}
+
+	fciCaps := Capabilities{}
+	capsList := make([]Capability, 0, len(capacities.Capabilities)+len(telemetries.Capabilities))
+	capsList = append(capsList, capacities.Capabilities...)
+	capsList = append(capsList, telemetries.Capabilities...)
+
+	fciCaps.Capabilities = capsList
+
+	api.WriteRespRaw(w, r, fciCaps)
+}
+
+func getFootprintMap(tx *sql.Tx) (map[int][]Footprint, error) {
+	footRows, err := tx.Query(AllFootprintQuery)
+	if err != nil {
+		return nil, fmt.Errorf("querying footprints: %w", err)
+	}
+	defer log.Close(footRows, "closing foorpint query")
+	footprintMap := map[int][]Footprint{}
+	for footRows.Next() {
+		var footprint Footprint
+		if err := footRows.Scan(&footprint.FootprintType, pq.Array(&footprint.FootprintValue), &footprint.CapabilityId); err != nil {
+			return nil, fmt.Errorf("scanning db rows: %w", err)
+		}
+		val, ok := footprintMap[footprint.CapabilityId]
+		if !ok {
+			footprintMap[footprint.CapabilityId] = []Footprint{footprint}
+		} else {
+			val = append(val, footprint)
+			footprintMap[footprint.CapabilityId] = val
+		}
+	}
+
+	return footprintMap, nil
+}
+
+func getTotalLimitsMap(tx *sql.Tx) (map[int][]TotalLimitsQueryResponse, error) {
+	tlRows, err := tx.Query(totalLimitsQuery)
+	if err != nil {
+		return nil, fmt.Errorf("querying total limits: %w", err)
+	}
+
+	defer log.Close(tlRows, "closing total capacity limits query")
+	totalLimitsMap := map[int][]TotalLimitsQueryResponse{}
+	for tlRows.Next() {
+		var totalLimit TotalLimitsQueryResponse
+		if err := tlRows.Scan(&totalLimit.LimitType, &totalLimit.MaximumHard, &totalLimit.MaximumSoft, &totalLimit.TelemetryId, &totalLimit.TelemetryMetic, &totalLimit.Id, &totalLimit.Type, &totalLimit.Name, &totalLimit.CapabilityId); err != nil {
+			return nil, fmt.Errorf("scanning db rows: %w", err)
+		}
+
+		val, ok := totalLimitsMap[totalLimit.CapabilityId]
+		if !ok {
+			totalLimitsMap[totalLimit.CapabilityId] = []TotalLimitsQueryResponse{totalLimit}
+		} else {
+			val = append(val, totalLimit)
+			totalLimitsMap[totalLimit.CapabilityId] = val
+		}
+	}
+
+	return totalLimitsMap, nil
+}
+
+func getHostLimitsMap(tx *sql.Tx) (map[int][]HostLimitsResponse, error) {
+	hlRows, err := tx.Query(hostLimitsQuery)
+	if err != nil {
+		return nil, fmt.Errorf("querying host limits: %w", err)
+	}
+
+	defer log.Close(hlRows, "closing host capacity limits query")
+	hostLimitsMap := map[int][]HostLimitsResponse{}
+	for hlRows.Next() {
+		var hostLimit HostLimitsResponse
+		if err := hlRows.Scan(&hostLimit.LimitType, &hostLimit.MaximumHard, &hostLimit.MaximumSoft, &hostLimit.TelemetryId, &hostLimit.TelemetryMetic, &hostLimit.Id, &hostLimit.Type, &hostLimit.Name, &hostLimit.Host, &hostLimit.CapabilityId); err != nil {
+			return nil, fmt.Errorf("scanning db rows: %w", err)
+		}
+		val, ok := hostLimitsMap[hostLimit.CapabilityId]
+		if !ok {
+			hostLimitsMap[hostLimit.CapabilityId] = []HostLimitsResponse{hostLimit}
+		} else {
+			val = append(val, hostLimit)
+			hostLimitsMap[hostLimit.CapabilityId] = val

Review comment:
       I believe my prior comment applies here as well

##########
File path: traffic_ops/traffic_ops_golang/cdni/shared.go
##########
@@ -0,0 +1,360 @@
+package cdni
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import (
+	"database/sql"
+	"errors"
+	"fmt"
+	"github.com/lib/pq"
+	"net/http"
+	"time"
+
+	"github.com/apache/trafficcontrol/lib/go-log"
+	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/api"
+
+	"github.com/dgrijalva/jwt-go"
+)
+
+const CapabilityQuery = `SELECT id, type, ucdn FROM cdni_capabilities WHERE type = $1 AND ucdn = $2`
+const AllFootprintQuery = `SELECT footprint_type, footprint_value::text[], capability_id FROM cdni_footprints`
+
+const totalLimitsQuery = `
+SELECT limit_type, maximum_hard, maximum_soft, ctl.telemetry_id, ctl.telemetry_metric, t.id, t.type, tm.name, ctl.capability_id 
+FROM cdni_total_limits AS ctl 
+LEFT JOIN cdni_telemetry as t ON telemetry_id = t.id 
+LEFT JOIN cdni_telemetry_metrics as tm ON telemetry_metric = tm.name`
+
+const hostLimitsQuery = `
+SELECT limit_type, maximum_hard, maximum_soft, chl.telemetry_id, chl.telemetry_metric, t.id, t.type, tm.name, host, chl.capability_id 
+FROM cdni_host_limits AS chl 
+LEFT JOIN cdni_telemetry as t ON telemetry_id = t.id 
+LEFT JOIN cdni_telemetry_metrics as tm ON telemetry_metric = tm.name 
+ORDER BY host DESC`
+
+func GetCapabilities(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()
+
+	bearerToken := r.Header.Get("Authorization")
+	if bearerToken == "" {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusBadRequest, errors.New("bearer token header is required"), nil)
+		return
+	}
+
+	if inf.Config.Cdni == nil || inf.Config.Cdni.JwtDecodingSecret == "" || inf.Config.Cdni.DCdnId == "" {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, errors.New("cdn.conf does not contain CDNi information"))
+		return
+	}
+
+	claims := jwt.MapClaims{}
+	token, err := jwt.ParseWithClaims(bearerToken, claims, func(token *jwt.Token) (interface{}, error) {
+		return []byte(inf.Config.Cdni.JwtDecodingSecret), nil
+	})
+	if err != nil {
+		api.HandleErr(w, r, nil, http.StatusInternalServerError, fmt.Errorf("parsing claims: %w", err), nil)
+		return
+	}
+	if !token.Valid {
+		api.HandleErr(w, r, nil, http.StatusInternalServerError, errors.New("invalid token"), nil)
+		return
+	}
+
+	var expirationFloat float64
+	var ucdn string
+	var dcdn string
+	for key, val := range claims {
+		switch key {
+		case "iss":
+			if _, ok := val.(string); !ok {
+				api.HandleErr(w, r, nil, http.StatusBadRequest, errors.New("invalid token - iss (Issuer) must be a string"), nil)
+				return
+			}
+			ucdn = val.(string)
+		case "aud":
+			if _, ok := val.(string); !ok {
+				api.HandleErr(w, r, nil, http.StatusBadRequest, errors.New("invalid token - aud (Audience) must be a string"), nil)
+				return
+			}
+			dcdn = val.(string)
+		case "exp":
+			if _, ok := val.(float64); !ok {
+				api.HandleErr(w, r, nil, http.StatusBadRequest, errors.New("invalid token - exp (Expiration) must be a float64"), nil)
+				return
+			}
+			expirationFloat = val.(float64)
+		}
+	}
+
+	expiration := int64(expirationFloat)
+
+	if expiration < time.Now().Unix() {
+		api.HandleErr(w, r, nil, http.StatusForbidden, errors.New("token is expired"), nil)
+		return
+	}
+	if dcdn != inf.Config.Cdni.DCdnId {
+		api.HandleErr(w, r, nil, http.StatusForbidden, errors.New("invalid token - incorrect dcdn"), nil)
+		return
+	}
+	if ucdn == "" {
+		api.HandleErr(w, r, nil, http.StatusForbidden, errors.New("invalid token - empty ucdn field"), nil)
+		return
+	}
+
+	capacities, err := getCapacities(inf, ucdn)
+	if err != nil {
+		api.HandleErr(w, r, nil, http.StatusInternalServerError, err, nil)
+		return
+	}
+
+	telemetries, err := getTelemetries(inf, ucdn)
+	if err != nil {
+		api.HandleErr(w, r, nil, http.StatusInternalServerError, err, nil)
+		return
+	}
+
+	fciCaps := Capabilities{}
+	capsList := make([]Capability, 0, len(capacities.Capabilities)+len(telemetries.Capabilities))
+	capsList = append(capsList, capacities.Capabilities...)
+	capsList = append(capsList, telemetries.Capabilities...)
+
+	fciCaps.Capabilities = capsList
+
+	api.WriteRespRaw(w, r, fciCaps)
+}
+
+func getFootprintMap(tx *sql.Tx) (map[int][]Footprint, error) {
+	footRows, err := tx.Query(AllFootprintQuery)
+	if err != nil {
+		return nil, fmt.Errorf("querying footprints: %w", err)
+	}
+	defer log.Close(footRows, "closing foorpint query")
+	footprintMap := map[int][]Footprint{}
+	for footRows.Next() {
+		var footprint Footprint
+		if err := footRows.Scan(&footprint.FootprintType, pq.Array(&footprint.FootprintValue), &footprint.CapabilityId); err != nil {
+			return nil, fmt.Errorf("scanning db rows: %w", err)
+		}
+		val, ok := footprintMap[footprint.CapabilityId]
+		if !ok {
+			footprintMap[footprint.CapabilityId] = []Footprint{footprint}
+		} else {
+			val = append(val, footprint)
+			footprintMap[footprint.CapabilityId] = val
+		}
+	}
+
+	return footprintMap, nil
+}
+
+func getTotalLimitsMap(tx *sql.Tx) (map[int][]TotalLimitsQueryResponse, error) {
+	tlRows, err := tx.Query(totalLimitsQuery)
+	if err != nil {
+		return nil, fmt.Errorf("querying total limits: %w", err)
+	}
+
+	defer log.Close(tlRows, "closing total capacity limits query")
+	totalLimitsMap := map[int][]TotalLimitsQueryResponse{}
+	for tlRows.Next() {
+		var totalLimit TotalLimitsQueryResponse
+		if err := tlRows.Scan(&totalLimit.LimitType, &totalLimit.MaximumHard, &totalLimit.MaximumSoft, &totalLimit.TelemetryId, &totalLimit.TelemetryMetic, &totalLimit.Id, &totalLimit.Type, &totalLimit.Name, &totalLimit.CapabilityId); err != nil {
+			return nil, fmt.Errorf("scanning db rows: %w", err)
+		}
+
+		val, ok := totalLimitsMap[totalLimit.CapabilityId]
+		if !ok {
+			totalLimitsMap[totalLimit.CapabilityId] = []TotalLimitsQueryResponse{totalLimit}
+		} else {
+			val = append(val, totalLimit)
+			totalLimitsMap[totalLimit.CapabilityId] = val
+		}
+	}
+
+	return totalLimitsMap, nil
+}
+
+func getHostLimitsMap(tx *sql.Tx) (map[int][]HostLimitsResponse, error) {
+	hlRows, err := tx.Query(hostLimitsQuery)
+	if err != nil {
+		return nil, fmt.Errorf("querying host limits: %w", err)
+	}
+
+	defer log.Close(hlRows, "closing host capacity limits query")
+	hostLimitsMap := map[int][]HostLimitsResponse{}
+	for hlRows.Next() {
+		var hostLimit HostLimitsResponse
+		if err := hlRows.Scan(&hostLimit.LimitType, &hostLimit.MaximumHard, &hostLimit.MaximumSoft, &hostLimit.TelemetryId, &hostLimit.TelemetryMetic, &hostLimit.Id, &hostLimit.Type, &hostLimit.Name, &hostLimit.Host, &hostLimit.CapabilityId); err != nil {
+			return nil, fmt.Errorf("scanning db rows: %w", err)
+		}
+		val, ok := hostLimitsMap[hostLimit.CapabilityId]
+		if !ok {
+			hostLimitsMap[hostLimit.CapabilityId] = []HostLimitsResponse{hostLimit}
+		} else {
+			val = append(val, hostLimit)
+			hostLimitsMap[hostLimit.CapabilityId] = val
+		}
+	}
+
+	return hostLimitsMap, nil
+}
+
+func getTelemetriesMap(tx *sql.Tx) (map[int][]Telemetry, error) {
+	rows, err := tx.Query(`SELECT id, type, capability_id FROM cdni_telemetry`)
+	if err != nil {
+		return nil, errors.New("querying cdni telemetry: " + err.Error())
+	}
+	defer log.Close(rows, "closing telemetry query")
+
+	telemetryMap := map[int][]Telemetry{}
+	for rows.Next() {
+		telemetry := Telemetry{}
+		if err := rows.Scan(&telemetry.Id, &telemetry.Type, &telemetry.CapabilityId); err != nil {
+			return nil, errors.New("scanning telemetry: " + err.Error())
+		}
+		val, ok := telemetryMap[telemetry.CapabilityId]
+		if !ok {
+			telemetryMap[telemetry.CapabilityId] = []Telemetry{telemetry}
+		} else {
+			val = append(val, telemetry)
+			telemetryMap[telemetry.CapabilityId] = val

Review comment:
       I believe my prior comment applies here as well

##########
File path: traffic_ops/traffic_ops_golang/cdni/shared.go
##########
@@ -0,0 +1,360 @@
+package cdni
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import (
+	"database/sql"
+	"errors"
+	"fmt"
+	"github.com/lib/pq"
+	"net/http"
+	"time"
+
+	"github.com/apache/trafficcontrol/lib/go-log"
+	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/api"
+
+	"github.com/dgrijalva/jwt-go"
+)
+
+const CapabilityQuery = `SELECT id, type, ucdn FROM cdni_capabilities WHERE type = $1 AND ucdn = $2`
+const AllFootprintQuery = `SELECT footprint_type, footprint_value::text[], capability_id FROM cdni_footprints`
+
+const totalLimitsQuery = `
+SELECT limit_type, maximum_hard, maximum_soft, ctl.telemetry_id, ctl.telemetry_metric, t.id, t.type, tm.name, ctl.capability_id 
+FROM cdni_total_limits AS ctl 
+LEFT JOIN cdni_telemetry as t ON telemetry_id = t.id 
+LEFT JOIN cdni_telemetry_metrics as tm ON telemetry_metric = tm.name`
+
+const hostLimitsQuery = `
+SELECT limit_type, maximum_hard, maximum_soft, chl.telemetry_id, chl.telemetry_metric, t.id, t.type, tm.name, host, chl.capability_id 
+FROM cdni_host_limits AS chl 
+LEFT JOIN cdni_telemetry as t ON telemetry_id = t.id 
+LEFT JOIN cdni_telemetry_metrics as tm ON telemetry_metric = tm.name 
+ORDER BY host DESC`
+
+func GetCapabilities(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()
+
+	bearerToken := r.Header.Get("Authorization")
+	if bearerToken == "" {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusBadRequest, errors.New("bearer token header is required"), nil)
+		return
+	}
+
+	if inf.Config.Cdni == nil || inf.Config.Cdni.JwtDecodingSecret == "" || inf.Config.Cdni.DCdnId == "" {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, errors.New("cdn.conf does not contain CDNi information"))
+		return
+	}
+
+	claims := jwt.MapClaims{}
+	token, err := jwt.ParseWithClaims(bearerToken, claims, func(token *jwt.Token) (interface{}, error) {
+		return []byte(inf.Config.Cdni.JwtDecodingSecret), nil
+	})
+	if err != nil {
+		api.HandleErr(w, r, nil, http.StatusInternalServerError, fmt.Errorf("parsing claims: %w", err), nil)
+		return
+	}
+	if !token.Valid {
+		api.HandleErr(w, r, nil, http.StatusInternalServerError, errors.New("invalid token"), nil)
+		return
+	}
+
+	var expirationFloat float64
+	var ucdn string
+	var dcdn string
+	for key, val := range claims {
+		switch key {
+		case "iss":
+			if _, ok := val.(string); !ok {
+				api.HandleErr(w, r, nil, http.StatusBadRequest, errors.New("invalid token - iss (Issuer) must be a string"), nil)
+				return
+			}
+			ucdn = val.(string)
+		case "aud":
+			if _, ok := val.(string); !ok {
+				api.HandleErr(w, r, nil, http.StatusBadRequest, errors.New("invalid token - aud (Audience) must be a string"), nil)
+				return
+			}
+			dcdn = val.(string)
+		case "exp":
+			if _, ok := val.(float64); !ok {
+				api.HandleErr(w, r, nil, http.StatusBadRequest, errors.New("invalid token - exp (Expiration) must be a float64"), nil)
+				return
+			}
+			expirationFloat = val.(float64)
+		}
+	}
+
+	expiration := int64(expirationFloat)
+
+	if expiration < time.Now().Unix() {
+		api.HandleErr(w, r, nil, http.StatusForbidden, errors.New("token is expired"), nil)
+		return
+	}
+	if dcdn != inf.Config.Cdni.DCdnId {
+		api.HandleErr(w, r, nil, http.StatusForbidden, errors.New("invalid token - incorrect dcdn"), nil)
+		return
+	}
+	if ucdn == "" {
+		api.HandleErr(w, r, nil, http.StatusForbidden, errors.New("invalid token - empty ucdn field"), nil)
+		return
+	}
+
+	capacities, err := getCapacities(inf, ucdn)
+	if err != nil {
+		api.HandleErr(w, r, nil, http.StatusInternalServerError, err, nil)
+		return
+	}
+
+	telemetries, err := getTelemetries(inf, ucdn)
+	if err != nil {
+		api.HandleErr(w, r, nil, http.StatusInternalServerError, err, nil)
+		return
+	}
+
+	fciCaps := Capabilities{}
+	capsList := make([]Capability, 0, len(capacities.Capabilities)+len(telemetries.Capabilities))
+	capsList = append(capsList, capacities.Capabilities...)
+	capsList = append(capsList, telemetries.Capabilities...)
+
+	fciCaps.Capabilities = capsList
+
+	api.WriteRespRaw(w, r, fciCaps)
+}
+
+func getFootprintMap(tx *sql.Tx) (map[int][]Footprint, error) {
+	footRows, err := tx.Query(AllFootprintQuery)
+	if err != nil {
+		return nil, fmt.Errorf("querying footprints: %w", err)
+	}
+	defer log.Close(footRows, "closing foorpint query")
+	footprintMap := map[int][]Footprint{}
+	for footRows.Next() {
+		var footprint Footprint
+		if err := footRows.Scan(&footprint.FootprintType, pq.Array(&footprint.FootprintValue), &footprint.CapabilityId); err != nil {
+			return nil, fmt.Errorf("scanning db rows: %w", err)
+		}
+		val, ok := footprintMap[footprint.CapabilityId]
+		if !ok {
+			footprintMap[footprint.CapabilityId] = []Footprint{footprint}
+		} else {
+			val = append(val, footprint)
+			footprintMap[footprint.CapabilityId] = val
+		}
+	}
+
+	return footprintMap, nil
+}
+
+func getTotalLimitsMap(tx *sql.Tx) (map[int][]TotalLimitsQueryResponse, error) {
+	tlRows, err := tx.Query(totalLimitsQuery)
+	if err != nil {
+		return nil, fmt.Errorf("querying total limits: %w", err)
+	}
+
+	defer log.Close(tlRows, "closing total capacity limits query")
+	totalLimitsMap := map[int][]TotalLimitsQueryResponse{}
+	for tlRows.Next() {
+		var totalLimit TotalLimitsQueryResponse
+		if err := tlRows.Scan(&totalLimit.LimitType, &totalLimit.MaximumHard, &totalLimit.MaximumSoft, &totalLimit.TelemetryId, &totalLimit.TelemetryMetic, &totalLimit.Id, &totalLimit.Type, &totalLimit.Name, &totalLimit.CapabilityId); err != nil {
+			return nil, fmt.Errorf("scanning db rows: %w", err)
+		}
+
+		val, ok := totalLimitsMap[totalLimit.CapabilityId]
+		if !ok {
+			totalLimitsMap[totalLimit.CapabilityId] = []TotalLimitsQueryResponse{totalLimit}
+		} else {
+			val = append(val, totalLimit)
+			totalLimitsMap[totalLimit.CapabilityId] = val
+		}
+	}
+
+	return totalLimitsMap, nil
+}
+
+func getHostLimitsMap(tx *sql.Tx) (map[int][]HostLimitsResponse, error) {
+	hlRows, err := tx.Query(hostLimitsQuery)
+	if err != nil {
+		return nil, fmt.Errorf("querying host limits: %w", err)
+	}
+
+	defer log.Close(hlRows, "closing host capacity limits query")
+	hostLimitsMap := map[int][]HostLimitsResponse{}
+	for hlRows.Next() {
+		var hostLimit HostLimitsResponse
+		if err := hlRows.Scan(&hostLimit.LimitType, &hostLimit.MaximumHard, &hostLimit.MaximumSoft, &hostLimit.TelemetryId, &hostLimit.TelemetryMetic, &hostLimit.Id, &hostLimit.Type, &hostLimit.Name, &hostLimit.Host, &hostLimit.CapabilityId); err != nil {
+			return nil, fmt.Errorf("scanning db rows: %w", err)
+		}
+		val, ok := hostLimitsMap[hostLimit.CapabilityId]
+		if !ok {
+			hostLimitsMap[hostLimit.CapabilityId] = []HostLimitsResponse{hostLimit}
+		} else {
+			val = append(val, hostLimit)
+			hostLimitsMap[hostLimit.CapabilityId] = val
+		}
+	}
+
+	return hostLimitsMap, nil
+}
+
+func getTelemetriesMap(tx *sql.Tx) (map[int][]Telemetry, error) {
+	rows, err := tx.Query(`SELECT id, type, capability_id FROM cdni_telemetry`)
+	if err != nil {
+		return nil, errors.New("querying cdni telemetry: " + err.Error())
+	}
+	defer log.Close(rows, "closing telemetry query")
+
+	telemetryMap := map[int][]Telemetry{}
+	for rows.Next() {
+		telemetry := Telemetry{}
+		if err := rows.Scan(&telemetry.Id, &telemetry.Type, &telemetry.CapabilityId); err != nil {
+			return nil, errors.New("scanning telemetry: " + err.Error())
+		}
+		val, ok := telemetryMap[telemetry.CapabilityId]
+		if !ok {
+			telemetryMap[telemetry.CapabilityId] = []Telemetry{telemetry}
+		} else {
+			val = append(val, telemetry)
+			telemetryMap[telemetry.CapabilityId] = val
+		}
+	}
+
+	return telemetryMap, nil
+}
+
+func getTelemetryMetricsMap(tx *sql.Tx) (map[string][]Metric, error) {
+	tmRows, err := tx.Query(`SELECT name, time_granularity, data_percentile, latency, telemetry_id FROM cdni_telemetry_metrics`)
+	if err != nil {
+		return nil, errors.New("querying cdni telemetry metrics: " + err.Error())
+	}
+	defer log.Close(tmRows, "closing telemetry metrics query")
+
+	telemetryMetricMap := map[string][]Metric{}
+	for tmRows.Next() {
+		metric := Metric{}
+		if err := tmRows.Scan(&metric.Name, &metric.TimeGranularity, &metric.DataPercentile, &metric.Latency, &metric.TelemetryId); err != nil {
+			return nil, errors.New("scanning telemetry metric: " + err.Error())
+		}
+		val, ok := telemetryMetricMap[metric.TelemetryId]
+		if !ok {
+			telemetryMetricMap[metric.TelemetryId] = []Metric{metric}
+		} else {
+			val = append(val, metric)
+			telemetryMetricMap[metric.TelemetryId] = val
+		}

Review comment:
       I believe my prior comment applies here as well




-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@trafficcontrol.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [trafficcontrol] rawlinp merged pull request #6524: Added CDNi Capacity and Telemetry

Posted by GitBox <gi...@apache.org>.
rawlinp merged pull request #6524:
URL: https://github.com/apache/trafficcontrol/pull/6524


   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@trafficcontrol.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [trafficcontrol] mattjackson220 commented on a change in pull request #6524: Added CDNi Capacity and Telemetry

Posted by GitBox <gi...@apache.org>.
mattjackson220 commented on a change in pull request #6524:
URL: https://github.com/apache/trafficcontrol/pull/6524#discussion_r803894386



##########
File path: traffic_ops/traffic_ops_golang/cdni/shared.go
##########
@@ -0,0 +1,199 @@
+package cdni
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import (
+	"errors"
+	"net/http"
+	"time"
+
+	"github.com/dgrijalva/jwt-go"
+
+	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/api"
+)
+
+const CapabilityQuery = `SELECT id, type, ucdn FROM cdni_capabilities WHERE type = $1 AND ucdn = $2`
+const FootprintQuery = `SELECT footprint_type, footprint_value::text[] FROM cdni_footprints WHERE capability_id = $1`
+
+func GetCapabilities(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()
+
+	bearerToken := r.Header.Get("Authorization")
+
+	if inf.Config.Cdni == nil || inf.Config.Cdni.JwtDecodingSecret == "" || inf.Config.Cdni.DCdnId == "" {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, errors.New("cdn.conf does not contain CDNi information"))
+		return
+	}
+
+	claims := jwt.MapClaims{}
+	token, err := jwt.ParseWithClaims(bearerToken, claims, func(token *jwt.Token) (interface{}, error) {
+		return []byte(inf.Config.Cdni.JwtDecodingSecret), nil
+	})
+	if err != nil {
+		api.HandleErr(w, r, nil, http.StatusInternalServerError, errors.New("parsing claims"), nil)
+		return
+	}
+	if !token.Valid {
+		api.HandleErr(w, r, nil, http.StatusInternalServerError, errors.New("invalid token"), nil)
+		return
+	}
+
+	var expirationFloat float64
+	var ucdn string
+	var dcdn string
+	for key, val := range claims {
+		switch key {
+		case "iss":
+			ucdn = val.(string)
+		case "aud":
+			dcdn = val.(string)
+		case "exp":
+			expirationFloat = val.(float64)

Review comment:
       👍 
   
   yea those are defined in the JWT RFC and their specific values are defined in the SVA spec for the capacity and configuration APIs. I'll add documentation for those so its more clear




-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@trafficcontrol.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [trafficcontrol] mattjackson220 commented on a change in pull request #6524: Added CDNi Capacity and Telemetry

Posted by GitBox <gi...@apache.org>.
mattjackson220 commented on a change in pull request #6524:
URL: https://github.com/apache/trafficcontrol/pull/6524#discussion_r803863586



##########
File path: traffic_ops/traffic_ops_golang/routing/routes.go
##########
@@ -130,6 +131,9 @@ func Routes(d ServerData) ([]Route, http.Handler, error) {
 		 * 4.x API
 		 */
 
+		// CDNI integration
+		{api.Version{Major: 4, Minor: 0}, http.MethodGet, `OC/FCI/advertisement/?$`, cdni.GetCapabilities, auth.PrivLevelReadOnly, []string{"CDNI-CAPACITY:READ"}, Authenticated, nil, 541357729077},

Review comment:
       yea its a required path per the spec. i dont see anything specifying whether it cannot have a prefix or not so i figured it would be ok as is since uCDNs can use the /api/4.0 in their base url




-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@trafficcontrol.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [trafficcontrol] ocket8888 commented on a change in pull request #6524: Added CDNi Capacity and Telemetry

Posted by GitBox <gi...@apache.org>.
ocket8888 commented on a change in pull request #6524:
URL: https://github.com/apache/trafficcontrol/pull/6524#discussion_r793062243



##########
File path: docs/source/admin/cdni.rst
##########
@@ -0,0 +1,36 @@
+..

Review comment:
       This document is inaccessible to anyone interacting with the documentation in HTML form, because it's not included in any tables of contents. You'd have to put the URL in manually.




-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@trafficcontrol.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org