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/02/11 16:27:42 UTC

[GitHub] [trafficcontrol] mattjackson220 opened a new pull request #6557: Added CDNi ability to negotiate higher capacities

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


   <!--
   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 negotiate higher capacity limits for CDNi integration. It adds 3 new endpoints:
   - one to negotiate total limits asynchronously
   - one to negotiate limits per host asynchronously 
   - one to approve or deny requests
   
   
   <!-- **^ 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
   - CDNi
   
   
   ## 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/CI/configuration` and `/OC/CI/configuration/{host}` endpoints work to kick off an asynchronous request for different capacity limits.  Verify that the `/OC/CI/configuration/request/{id}/{approved}` endpoint works to both approve and deny a request. If approved, the `/OC/FCI/advertisement` endpoint should reflect the update.
   For this PR, values must be manually added to the DB (future PR will add in ability to add these). Verify that the json returned is formatted per the SVA spec titled Capacity Insights Interface.
   [sva2020.030.12.2.docx](https://github.com/apache/trafficcontrol/files/8050113/sva2020.030.12.2.1.docx)
   
   ## 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
   - [x] 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. -->
   - [x] 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] mattjackson220 commented on a change in pull request #6557: Added CDNi ability to negotiate higher capacities

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



##########
File path: traffic_ops/traffic_ops_golang/cdni/shared.go
##########
@@ -68,17 +82,388 @@ func GetCapabilities(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
+	ucdn, err := checkBearerToken(bearerToken, inf)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, err)
+		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 PutHostConfiguration(w http.ResponseWriter, r *http.Request) {
+	inf, userErr, sysErr, errCode := api.NewInfo(r, []string{"host"}, nil)
+	if userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr)
+		return
+	}
+	defer inf.Close()
+
+	host := inf.Params["host"]
+	if errCode, userErr, sysErr := validateHostExists(host, inf.Tx.Tx); userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr)
+		return
+	}
+
+	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
+	}
+
+	ucdn, err := checkBearerToken(bearerToken, inf)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, err)
+		return
+	}
+
+	var genericHostRequest GenericHostMetadata
+	err = json.NewDecoder(r.Body).Decode(&genericHostRequest)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("decoding host json request: %w", err))
+		return
+	}
+
+	db, err := api.GetDB(r.Context())
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("getting async db: %w", err))
+		return
+	}
+	asyncTx, err := db.Begin()
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("getting async tx: %w", err))
+		return
+	}
+
+	asyncStatusId, errCode, userErr, sysErr := api.InsertAsyncStatus(asyncTx, "CDNi host configuration update request received.")
+	if userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr)
+		return
+	}
+
+	//var typeToValueMap map[string]GenericRequestedLimits
+	//for _, hostUpdate := range genericHostRequest.HostMetadata.Metadata {
+	//	typeToValueMap[hostUpdate.Type] = hostUpdate.Value
+	//}
+
+	data := genericHostRequest.HostMetadata.Metadata
+
+	_, err = inf.Tx.Tx.Query(InsertCapabilityUpdateQuery, ucdn, data, asyncStatusId, hostConfigLabel, host)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("inserting capability update request into queue: %w", err))
+		return
+	}
+
+	var alerts tc.Alerts
+	alerts.AddAlert(tc.Alert{
+		Text:  "CDNi configuration update request received. Status updates can be found here: " + api.CurrentAsyncEndpoint + strconv.Itoa(asyncStatusId),
+		Level: tc.SuccessLevel.String(),
+	})
+
+	w.Header().Add("Location", api.CurrentAsyncEndpoint+strconv.Itoa(asyncStatusId))
+	api.WriteAlerts(w, r, http.StatusAccepted, alerts)
+}
+
+func PutConfiguration(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
+	}
+
+	ucdn, err := checkBearerToken(bearerToken, inf)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, err)
+		return
+	}
+
+	var genericRequest GenericRequestMetadata
+	err = json.NewDecoder(r.Body).Decode(&genericRequest)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("decoding json request: %w", err))
+		return
+	}
+
+	db, err := api.GetDB(r.Context())
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("getting async db: %w", err))
+		return
+	}
+	asyncTx, err := db.Begin()
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("getting async tx: %w", err))
+		return
+	}
+
+	asyncStatusId, errCode, userErr, sysErr := api.InsertAsyncStatus(asyncTx, "CDNi configuration update request received.")
+	if userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr)
+		return
+	}
+
+	data := genericRequest.Metadata
+
+	_, err = inf.Tx.Tx.Query(InsertCapabilityUpdateQuery, ucdn, data, asyncStatusId, SupportedGenericMetadataType(genericRequest.Type), genericRequest.Host)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("inserting capability update request into queue: %w", err))
+		return
+	}
+
+	var alerts tc.Alerts
+	alerts.AddAlert(tc.Alert{
+		Text:  "CDNi configuration update request received. Status updates can be found here: " + api.CurrentAsyncEndpoint + strconv.Itoa(asyncStatusId),
+		Level: tc.SuccessLevel.String(),
+	})
+
+	w.Header().Add("Location", api.CurrentAsyncEndpoint+strconv.Itoa(asyncStatusId))
+	api.WriteAlerts(w, r, http.StatusAccepted, alerts)
+}
+
+func PutConfigurationResponse(w http.ResponseWriter, r *http.Request) {
+	inf, userErr, sysErr, errCode := api.NewInfo(r, []string{"approved", "id"}, []string{"id"})
+	if userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr)
+		return
+	}
+	defer inf.Close()
+
+	reqId := inf.IntParams["id"]
+	approvedString := inf.Params["approved"]
+	approved, err := strconv.ParseBool(approvedString)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusBadRequest, errors.New("approved parameter must be a boolean"), nil)
+		return
+	}
+
+	db, err := api.GetDB(r.Context())
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("getting async db: %w", err))
+		return
+	}
+
+	rows, err := inf.Tx.Tx.Query(SelectCapabilityUpdateQuery, reqId)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("querying for capability update request: %w", err))
+		return
+	}
+	defer rows.Close()
+	var ucdn string
+	var data json.RawMessage
+	var host string
+	var asyncId int
+	var requestType string
+	count := 0
+	for rows.Next() {
+		if err := rows.Scan(&ucdn, &data, &asyncId, &requestType, &host); err != nil {
+			api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("scanning db rows: %w", err))
+			return
+		}
+		count++
+	}
+	if count == 0 {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusNotFound, fmt.Errorf("no configuration request for that id"), nil)
+		return
+	}
+
+	if !approved {
+		if asyncErr := api.UpdateAsyncStatus(db, api.AsyncFailed, "Requested configuration update has been denied.", asyncId, true); asyncErr != nil {
+			log.Errorf("updating async status for id %v: %v", asyncId, asyncErr)
+		}
+		status, err := deleteCapabilityRequest(reqId, inf.Tx.Tx)
+		if err != nil {
+			api.HandleErr(w, r, inf.Tx.Tx, status, nil, fmt.Errorf("deleting configuration request from queue: %w", err))
+			return
+		}
+		api.WriteResp(w, r, "Successfully denied configuration update request.")
+		return
+	}
+
+	var updatedDataList []GenericMetadata
+	if err = json.Unmarshal(data, &updatedDataList); err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("unmarshalling data for configuration update: %w", err))
+		return
+	}
+
+	var unsupportedTypes []string
+	for _, updatedData := range updatedDataList {
+		if !updatedData.Type.isValid() {
+			unsupportedTypes = append(unsupportedTypes, string(updatedData.Type))
+		}
+	}
+
+	if len(unsupportedTypes) != 0 {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusBadRequest, fmt.Errorf("unsupported generic metadata types found: %v", strings.Join(unsupportedTypes, ", ")), nil)
+		return
+	}
+
+	for _, updatedData := range updatedDataList {
+		switch updatedData.Type {
+		case MiRequestedCapacityLimits:
+			var capacityRequestedLimits CapacityRequestedLimits
+			if err = json.Unmarshal(updatedData.Value, &capacityRequestedLimits); err != nil {
+				api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("unmarshalling data for configuration update: %w", err))
+				return
+			}
+			for _, capLim := range capacityRequestedLimits.RequestedLimits {
+				capId, err := getCapabilityIdFromFootprints(capLim, ucdn, inf)
+				if err != nil {
+					api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("finding capability for given information: %w", err))
+					return
+				}
+
+				query := UpdateTotalLimitsByCapabilityAndLimitTypeQuery
+				queryParams := []interface{}{capLim.LimitValue, capId, capLim.LimitType}
+				if host != "" {
+					query = UpdateHostLimitsByCapabilityAndLimitTypeQuery
+					queryParams = []interface{}{capLim.LimitValue, capId, capLim.LimitType, host}
+				}
+
+				result, err := inf.Tx.Tx.Exec(query, queryParams...)
+				if err != nil {
+					api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("updating capacity: %w", err))
+					return
+				}
+
+				if rowsAffected, err := result.RowsAffected(); err != nil {
+					api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("updating capacity: getting rows affected: %w", err))
+					return
+				} else if rowsAffected < 1 {
+					api.HandleErr(w, r, inf.Tx.Tx, http.StatusNotFound, fmt.Errorf("no capacity found for update: host: %s, type: %s, limit: %v", host, updatedData.Type, capLim), nil)
+					return
+				} else if rowsAffected > 1 {
+					api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("capacity update affected too many rows: %d", rowsAffected))
+					return
+				}
+			}
+		}
+	}
+
+	if asyncErr := api.UpdateAsyncStatus(db, api.AsyncSucceeded, "Capacity requested update has been completed.", asyncId, true); asyncErr != nil {
+		log.Errorf("updating async status for id %v: %v", asyncId, asyncErr)
+	}
+	status, err := deleteCapabilityRequest(reqId, inf.Tx.Tx)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, status, nil, fmt.Errorf("deleting capacity request from queue: %w", err))
+		return
+	}
+	api.WriteResp(w, r, "Successfully updated configuration.")
+}
+
+func getCapabilityIdFromFootprints(updatedData CapacityLimit, ucdn string, inf *api.APIInfo) (int, error) {
+	var potentialCapabilityIds []int
+	for _, footprint := range updatedData.Footprints {
+		selectQuery := `SELECT footprint_type, footprint_value, capability_id FROM cdni_footprints WHERE ucdn = $1 AND footprint_type = $2 AND footprint_value = `
+		valuesAsQuery := "ARRAY['" + strings.Join(footprint.FootprintValue, "','") + "']"
+		rows, err := inf.Tx.Tx.Query(selectQuery+valuesAsQuery, ucdn, footprint.FootprintType)
+		if err != nil {
+			return 0, fmt.Errorf("querying for capacity update request: %w", err)
+		}
+		defer rows.Close()
+		var capabilityIds []int
+		rowCount := 0
+		for rows.Next() {
+			var capabilityId int
+			var footprint Footprint
+			if err := rows.Scan(&footprint.FootprintType, pq.Array(&footprint.FootprintValue), &capabilityId); err != nil {
+				return 0, fmt.Errorf("scanning db rows: %w", err)
+			}
+			rowCount++
+			capabilityIds = append(capabilityIds, capabilityId)
+		}
+
+		if rowCount == 0 {
+			return 0, fmt.Errorf("no capabilities found for footprint: %v, %v", footprint.FootprintType, footprint.FootprintValue)
+		}
+		var newPotentials []int
+		for _, capId := range capabilityIds {
+			if len(potentialCapabilityIds) == 0 {
+				newPotentials = append(newPotentials, capId)
+			} else {
+				if util.IntInArray(potentialCapabilityIds, capId) {
+					newPotentials = append(newPotentials, capId)
+				}
+			}
+		}
+		if len(newPotentials) == 0 {
+			return 0, fmt.Errorf("no capabilities found that match all footprints: %v", updatedData.Footprints)
+		}
+		potentialCapabilityIds = newPotentials

Review comment:
       yea its super confusing and also im very open to better ways to do this if you have any. 
   
   for this, the footprints can overlap and can be linked to multiple capabilities in the long run. right now this is just for Capacity but there will be a lot more as i keep going. plus each capability can have multiple footprints. so for this code im looping through the footprints that have the correct uCDN, type, and value based on the request, then making a list of potential capability ids based on all of the footprints that came linked to the info in the request.  
   
   this way if you send a request linked to footprints like `[ {"footprint-type": ipv4cidr", "footprint-value": ["127.0.0.1", "127.0.0.2" ] }, {"footprint-type": "countrycode", "footprint-value": ["us"] } ]` then it wont get capabilities linke to just the countrycode footprint or just the ipv4cidr footprint or other capabilities linked to those plus more.
   
   does that make any sense?




-- 
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 #6557: Added CDNi ability to negotiate higher capacities

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



##########
File path: traffic_ops/traffic_ops_golang/cdni/shared.go
##########
@@ -154,18 +149,19 @@ func PutHostConfiguration(w http.ResponseWriter, r *http.Request) {
 		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("getting async tx: %w", err))
 		return
 	}
+	logTx, err := db.Begin()

Review comment:
       I think its because of the way I wrote the async status endpoints way back when. since those hold the `defer tx.Commit` i think that requires all of these to be separate. i never really thought of it that way until right now so ill go in and fix it when i have time to test everywhere that uses it




-- 
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 #6557: Added CDNi ability to negotiate higher capacities

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



##########
File path: traffic_ops/traffic_ops_golang/cdni/shared.go
##########
@@ -154,18 +149,19 @@ func PutHostConfiguration(w http.ResponseWriter, r *http.Request) {
 		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("getting async tx: %w", err))
 		return
 	}
+	logTx, err := db.Begin()

Review comment:
       Oh yeah, it makes sense for when you have to hand off a DB transaction to a goroutine for it to be completed asynchronously, but I think when it's all handled synchronously all in the same goroutine it can use the same transaction.




-- 
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 #6557: Added CDNi ability to negotiate higher capacities

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



##########
File path: docs/source/api/v4/oc_ci_configuration_host.rst
##########
@@ -0,0 +1,107 @@
+..
+..
+.. Licensed 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.
+..
+
+.. _to-api-oc-fci-configuration-host:
+
+********************************
+``OC/CI/configuration/{{host}}``
+********************************
+
+``PUT``
+=======
+Triggers an asynchronous task to update the configuration for the :abbr:`uCDN (Upstream Content Delivery Network)` and the specified host by adding the request to a queue to be reviewed later. This returns a 202 Accepted status and an endpoint to be used for status updates.
+
+:Auth. Required: Yes
+:Roles Required: "admin" or "operations"
+:Permissions Required: CDNI:READ

Review comment:
       should this be update instead of read?

##########
File path: docs/source/api/v4/oc_ci_configuration.rst
##########
@@ -0,0 +1,101 @@
+..
+..
+.. Licensed 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.
+..
+
+.. _to-api-oc-fci-configuration:
+
+***********************
+``OC/CI/configuration``
+***********************
+
+``PUT``
+=======
+Triggers an asynchronous task to update the configuration for the :abbr:`uCDN (Upstream Content Delivery Network)` by adding the request to a queue to be reviewed later. This returns a 202 Accepted status and an endpoint to be used for status updates.
+
+:Auth. Required: Yes
+:Roles Required: "admin" or "operations"
+:Permissions Required: CDNI:READ

Review comment:
       should this be update instead of read?

##########
File path: traffic_ops/traffic_ops_golang/cdni/shared.go
##########
@@ -68,17 +82,388 @@ func GetCapabilities(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
+	ucdn, err := checkBearerToken(bearerToken, inf)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, err)
+		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 PutHostConfiguration(w http.ResponseWriter, r *http.Request) {
+	inf, userErr, sysErr, errCode := api.NewInfo(r, []string{"host"}, nil)
+	if userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr)
+		return
+	}
+	defer inf.Close()
+
+	host := inf.Params["host"]
+	if errCode, userErr, sysErr := validateHostExists(host, inf.Tx.Tx); userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr)
+		return
+	}
+
+	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
+	}
+
+	ucdn, err := checkBearerToken(bearerToken, inf)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, err)
+		return
+	}
+
+	var genericHostRequest GenericHostMetadata
+	err = json.NewDecoder(r.Body).Decode(&genericHostRequest)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("decoding host json request: %w", err))
+		return
+	}
+
+	db, err := api.GetDB(r.Context())
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("getting async db: %w", err))
+		return
+	}
+	asyncTx, err := db.Begin()
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("getting async tx: %w", err))
+		return
+	}
+
+	asyncStatusId, errCode, userErr, sysErr := api.InsertAsyncStatus(asyncTx, "CDNi host configuration update request received.")
+	if userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr)
+		return
+	}
+
+	//var typeToValueMap map[string]GenericRequestedLimits
+	//for _, hostUpdate := range genericHostRequest.HostMetadata.Metadata {
+	//	typeToValueMap[hostUpdate.Type] = hostUpdate.Value
+	//}
+
+	data := genericHostRequest.HostMetadata.Metadata
+
+	_, err = inf.Tx.Tx.Query(InsertCapabilityUpdateQuery, ucdn, data, asyncStatusId, hostConfigLabel, host)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("inserting capability update request into queue: %w", err))
+		return
+	}
+
+	var alerts tc.Alerts
+	alerts.AddAlert(tc.Alert{
+		Text:  "CDNi configuration update request received. Status updates can be found here: " + api.CurrentAsyncEndpoint + strconv.Itoa(asyncStatusId),
+		Level: tc.SuccessLevel.String(),
+	})
+
+	w.Header().Add("Location", api.CurrentAsyncEndpoint+strconv.Itoa(asyncStatusId))
+	api.WriteAlerts(w, r, http.StatusAccepted, alerts)
+}
+
+func PutConfiguration(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
+	}
+
+	ucdn, err := checkBearerToken(bearerToken, inf)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, err)
+		return
+	}
+
+	var genericRequest GenericRequestMetadata
+	err = json.NewDecoder(r.Body).Decode(&genericRequest)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("decoding json request: %w", err))
+		return
+	}
+
+	db, err := api.GetDB(r.Context())
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("getting async db: %w", err))
+		return
+	}
+	asyncTx, err := db.Begin()
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("getting async tx: %w", err))
+		return
+	}
+
+	asyncStatusId, errCode, userErr, sysErr := api.InsertAsyncStatus(asyncTx, "CDNi configuration update request received.")
+	if userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr)
+		return
+	}
+
+	data := genericRequest.Metadata
+
+	_, err = inf.Tx.Tx.Query(InsertCapabilityUpdateQuery, ucdn, data, asyncStatusId, SupportedGenericMetadataType(genericRequest.Type), genericRequest.Host)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("inserting capability update request into queue: %w", err))
+		return
+	}
+
+	var alerts tc.Alerts
+	alerts.AddAlert(tc.Alert{
+		Text:  "CDNi configuration update request received. Status updates can be found here: " + api.CurrentAsyncEndpoint + strconv.Itoa(asyncStatusId),
+		Level: tc.SuccessLevel.String(),
+	})
+
+	w.Header().Add("Location", api.CurrentAsyncEndpoint+strconv.Itoa(asyncStatusId))

Review comment:
       We should add a `"Location"` constant to lib/go-rfc/http.go and use that here (there are some other places that should use it as well but not necessary for this PR)

##########
File path: traffic_ops/traffic_ops_golang/cdni/shared.go
##########
@@ -68,17 +82,388 @@ func GetCapabilities(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
+	ucdn, err := checkBearerToken(bearerToken, inf)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, err)
+		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 PutHostConfiguration(w http.ResponseWriter, r *http.Request) {
+	inf, userErr, sysErr, errCode := api.NewInfo(r, []string{"host"}, nil)
+	if userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr)
+		return
+	}
+	defer inf.Close()
+
+	host := inf.Params["host"]
+	if errCode, userErr, sysErr := validateHostExists(host, inf.Tx.Tx); userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr)
+		return
+	}
+
+	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
+	}
+
+	ucdn, err := checkBearerToken(bearerToken, inf)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, err)
+		return
+	}
+
+	var genericHostRequest GenericHostMetadata
+	err = json.NewDecoder(r.Body).Decode(&genericHostRequest)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("decoding host json request: %w", err))
+		return
+	}
+
+	db, err := api.GetDB(r.Context())
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("getting async db: %w", err))
+		return
+	}
+	asyncTx, err := db.Begin()
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("getting async tx: %w", err))
+		return
+	}
+
+	asyncStatusId, errCode, userErr, sysErr := api.InsertAsyncStatus(asyncTx, "CDNi host configuration update request received.")
+	if userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr)
+		return
+	}
+
+	//var typeToValueMap map[string]GenericRequestedLimits
+	//for _, hostUpdate := range genericHostRequest.HostMetadata.Metadata {
+	//	typeToValueMap[hostUpdate.Type] = hostUpdate.Value
+	//}
+
+	data := genericHostRequest.HostMetadata.Metadata
+
+	_, err = inf.Tx.Tx.Query(InsertCapabilityUpdateQuery, ucdn, data, asyncStatusId, hostConfigLabel, host)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("inserting capability update request into queue: %w", err))
+		return
+	}
+
+	var alerts tc.Alerts
+	alerts.AddAlert(tc.Alert{
+		Text:  "CDNi configuration update request received. Status updates can be found here: " + api.CurrentAsyncEndpoint + strconv.Itoa(asyncStatusId),
+		Level: tc.SuccessLevel.String(),
+	})
+
+	w.Header().Add("Location", api.CurrentAsyncEndpoint+strconv.Itoa(asyncStatusId))

Review comment:
       We should add a `"Location"` constant to lib/go-rfc/http.go and use that here (there are some other places that should use it as well but not necessary for this PR)

##########
File path: traffic_ops/traffic_ops_golang/cdni/shared.go
##########
@@ -68,17 +82,388 @@ func GetCapabilities(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
+	ucdn, err := checkBearerToken(bearerToken, inf)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, err)
+		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 PutHostConfiguration(w http.ResponseWriter, r *http.Request) {
+	inf, userErr, sysErr, errCode := api.NewInfo(r, []string{"host"}, nil)
+	if userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr)
+		return
+	}
+	defer inf.Close()
+
+	host := inf.Params["host"]
+	if errCode, userErr, sysErr := validateHostExists(host, inf.Tx.Tx); userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr)
+		return
+	}
+
+	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
+	}
+
+	ucdn, err := checkBearerToken(bearerToken, inf)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, err)
+		return
+	}
+
+	var genericHostRequest GenericHostMetadata
+	err = json.NewDecoder(r.Body).Decode(&genericHostRequest)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("decoding host json request: %w", err))
+		return
+	}
+
+	db, err := api.GetDB(r.Context())
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("getting async db: %w", err))
+		return
+	}
+	asyncTx, err := db.Begin()
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("getting async tx: %w", err))
+		return
+	}
+
+	asyncStatusId, errCode, userErr, sysErr := api.InsertAsyncStatus(asyncTx, "CDNi host configuration update request received.")
+	if userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr)
+		return
+	}
+
+	//var typeToValueMap map[string]GenericRequestedLimits
+	//for _, hostUpdate := range genericHostRequest.HostMetadata.Metadata {
+	//	typeToValueMap[hostUpdate.Type] = hostUpdate.Value
+	//}
+
+	data := genericHostRequest.HostMetadata.Metadata
+
+	_, err = inf.Tx.Tx.Query(InsertCapabilityUpdateQuery, ucdn, data, asyncStatusId, hostConfigLabel, host)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("inserting capability update request into queue: %w", err))
+		return
+	}
+
+	var alerts tc.Alerts
+	alerts.AddAlert(tc.Alert{
+		Text:  "CDNi configuration update request received. Status updates can be found here: " + api.CurrentAsyncEndpoint + strconv.Itoa(asyncStatusId),
+		Level: tc.SuccessLevel.String(),
+	})
+
+	w.Header().Add("Location", api.CurrentAsyncEndpoint+strconv.Itoa(asyncStatusId))
+	api.WriteAlerts(w, r, http.StatusAccepted, alerts)
+}
+
+func PutConfiguration(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
+	}
+
+	ucdn, err := checkBearerToken(bearerToken, inf)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, err)
+		return
+	}
+
+	var genericRequest GenericRequestMetadata
+	err = json.NewDecoder(r.Body).Decode(&genericRequest)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("decoding json request: %w", err))
+		return
+	}
+
+	db, err := api.GetDB(r.Context())
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("getting async db: %w", err))
+		return
+	}
+	asyncTx, err := db.Begin()
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("getting async tx: %w", err))
+		return
+	}
+
+	asyncStatusId, errCode, userErr, sysErr := api.InsertAsyncStatus(asyncTx, "CDNi configuration update request received.")
+	if userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr)
+		return
+	}
+
+	data := genericRequest.Metadata
+
+	_, err = inf.Tx.Tx.Query(InsertCapabilityUpdateQuery, ucdn, data, asyncStatusId, SupportedGenericMetadataType(genericRequest.Type), genericRequest.Host)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("inserting capability update request into queue: %w", err))
+		return
+	}
+
+	var alerts tc.Alerts
+	alerts.AddAlert(tc.Alert{
+		Text:  "CDNi configuration update request received. Status updates can be found here: " + api.CurrentAsyncEndpoint + strconv.Itoa(asyncStatusId),
+		Level: tc.SuccessLevel.String(),
+	})
+
+	w.Header().Add("Location", api.CurrentAsyncEndpoint+strconv.Itoa(asyncStatusId))
+	api.WriteAlerts(w, r, http.StatusAccepted, alerts)
+}
+
+func PutConfigurationResponse(w http.ResponseWriter, r *http.Request) {
+	inf, userErr, sysErr, errCode := api.NewInfo(r, []string{"approved", "id"}, []string{"id"})
+	if userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr)
+		return
+	}
+	defer inf.Close()
+
+	reqId := inf.IntParams["id"]
+	approvedString := inf.Params["approved"]
+	approved, err := strconv.ParseBool(approvedString)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusBadRequest, errors.New("approved parameter must be a boolean"), nil)
+		return
+	}
+
+	db, err := api.GetDB(r.Context())
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("getting async db: %w", err))
+		return
+	}
+
+	rows, err := inf.Tx.Tx.Query(SelectCapabilityUpdateQuery, reqId)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("querying for capability update request: %w", err))
+		return
+	}
+	defer rows.Close()
+	var ucdn string
+	var data json.RawMessage
+	var host string
+	var asyncId int
+	var requestType string
+	count := 0
+	for rows.Next() {
+		if err := rows.Scan(&ucdn, &data, &asyncId, &requestType, &host); err != nil {
+			api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("scanning db rows: %w", err))
+			return
+		}
+		count++
+	}
+	if count == 0 {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusNotFound, fmt.Errorf("no configuration request for that id"), nil)
+		return
+	}
+
+	if !approved {
+		if asyncErr := api.UpdateAsyncStatus(db, api.AsyncFailed, "Requested configuration update has been denied.", asyncId, true); asyncErr != nil {
+			log.Errorf("updating async status for id %v: %v", asyncId, asyncErr)
+		}
+		status, err := deleteCapabilityRequest(reqId, inf.Tx.Tx)
+		if err != nil {
+			api.HandleErr(w, r, inf.Tx.Tx, status, nil, fmt.Errorf("deleting configuration request from queue: %w", err))
+			return
+		}
+		api.WriteResp(w, r, "Successfully denied configuration update request.")
+		return
+	}
+
+	var updatedDataList []GenericMetadata
+	if err = json.Unmarshal(data, &updatedDataList); err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("unmarshalling data for configuration update: %w", err))
+		return
+	}
+
+	var unsupportedTypes []string
+	for _, updatedData := range updatedDataList {
+		if !updatedData.Type.isValid() {
+			unsupportedTypes = append(unsupportedTypes, string(updatedData.Type))
+		}
+	}
+
+	if len(unsupportedTypes) != 0 {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusBadRequest, fmt.Errorf("unsupported generic metadata types found: %v", strings.Join(unsupportedTypes, ", ")), nil)
+		return
+	}
+
+	for _, updatedData := range updatedDataList {
+		switch updatedData.Type {
+		case MiRequestedCapacityLimits:
+			var capacityRequestedLimits CapacityRequestedLimits
+			if err = json.Unmarshal(updatedData.Value, &capacityRequestedLimits); err != nil {
+				api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("unmarshalling data for configuration update: %w", err))
+				return
+			}
+			for _, capLim := range capacityRequestedLimits.RequestedLimits {
+				capId, err := getCapabilityIdFromFootprints(capLim, ucdn, inf)
+				if err != nil {
+					api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("finding capability for given information: %w", err))
+					return
+				}
+
+				query := UpdateTotalLimitsByCapabilityAndLimitTypeQuery
+				queryParams := []interface{}{capLim.LimitValue, capId, capLim.LimitType}
+				if host != "" {
+					query = UpdateHostLimitsByCapabilityAndLimitTypeQuery
+					queryParams = []interface{}{capLim.LimitValue, capId, capLim.LimitType, host}
+				}
+
+				result, err := inf.Tx.Tx.Exec(query, queryParams...)
+				if err != nil {
+					api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("updating capacity: %w", err))
+					return
+				}
+
+				if rowsAffected, err := result.RowsAffected(); err != nil {
+					api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("updating capacity: getting rows affected: %w", err))
+					return
+				} else if rowsAffected < 1 {
+					api.HandleErr(w, r, inf.Tx.Tx, http.StatusNotFound, fmt.Errorf("no capacity found for update: host: %s, type: %s, limit: %v", host, updatedData.Type, capLim), nil)
+					return
+				} else if rowsAffected > 1 {
+					api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("capacity update affected too many rows: %d", rowsAffected))
+					return
+				}
+			}
+		}
+	}
+
+	if asyncErr := api.UpdateAsyncStatus(db, api.AsyncSucceeded, "Capacity requested update has been completed.", asyncId, true); asyncErr != nil {
+		log.Errorf("updating async status for id %v: %v", asyncId, asyncErr)
+	}
+	status, err := deleteCapabilityRequest(reqId, inf.Tx.Tx)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, status, nil, fmt.Errorf("deleting capacity request from queue: %w", err))
+		return
+	}
+	api.WriteResp(w, r, "Successfully updated configuration.")
+}
+
+func getCapabilityIdFromFootprints(updatedData CapacityLimit, ucdn string, inf *api.APIInfo) (int, error) {
+	var potentialCapabilityIds []int
+	for _, footprint := range updatedData.Footprints {
+		selectQuery := `SELECT footprint_type, footprint_value, capability_id FROM cdni_footprints WHERE ucdn = $1 AND footprint_type = $2 AND footprint_value = `
+		valuesAsQuery := "ARRAY['" + strings.Join(footprint.FootprintValue, "','") + "']"
+		rows, err := inf.Tx.Tx.Query(selectQuery+valuesAsQuery, ucdn, footprint.FootprintType)
+		if err != nil {
+			return 0, fmt.Errorf("querying for capacity update request: %w", err)
+		}
+		defer rows.Close()
+		var capabilityIds []int
+		rowCount := 0
+		for rows.Next() {
+			var capabilityId int
+			var footprint Footprint
+			if err := rows.Scan(&footprint.FootprintType, pq.Array(&footprint.FootprintValue), &capabilityId); err != nil {
+				return 0, fmt.Errorf("scanning db rows: %w", err)
+			}
+			rowCount++
+			capabilityIds = append(capabilityIds, capabilityId)
+		}
+
+		if rowCount == 0 {
+			return 0, fmt.Errorf("no capabilities found for footprint: %v, %v", footprint.FootprintType, footprint.FootprintValue)
+		}
+		var newPotentials []int
+		for _, capId := range capabilityIds {
+			if len(potentialCapabilityIds) == 0 {
+				newPotentials = append(newPotentials, capId)
+			} else {
+				if util.IntInArray(potentialCapabilityIds, capId) {
+					newPotentials = append(newPotentials, capId)
+				}
+			}
+		}
+		if len(newPotentials) == 0 {
+			return 0, fmt.Errorf("no capabilities found that match all footprints: %v", updatedData.Footprints)
+		}
+		potentialCapabilityIds = newPotentials
+	}
+	if len(potentialCapabilityIds) == 0 {
+		return 0, fmt.Errorf("no capabilities found that match all footprints: %v", updatedData.Footprints)
+	}
+	if len(potentialCapabilityIds) > 1 {
+		return 0, fmt.Errorf("more than 1 capabilities found that match all footprints: %v", updatedData.Footprints)

Review comment:
       capabilities -> capability 

##########
File path: traffic_ops/traffic_ops_golang/cdni/shared.go
##########
@@ -68,17 +82,388 @@ func GetCapabilities(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
+	ucdn, err := checkBearerToken(bearerToken, inf)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, err)
+		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 PutHostConfiguration(w http.ResponseWriter, r *http.Request) {
+	inf, userErr, sysErr, errCode := api.NewInfo(r, []string{"host"}, nil)
+	if userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr)
+		return
+	}
+	defer inf.Close()
+
+	host := inf.Params["host"]
+	if errCode, userErr, sysErr := validateHostExists(host, inf.Tx.Tx); userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr)
+		return
+	}
+
+	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
+	}
+
+	ucdn, err := checkBearerToken(bearerToken, inf)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, err)
+		return
+	}
+
+	var genericHostRequest GenericHostMetadata
+	err = json.NewDecoder(r.Body).Decode(&genericHostRequest)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("decoding host json request: %w", err))
+		return
+	}
+
+	db, err := api.GetDB(r.Context())
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("getting async db: %w", err))
+		return
+	}
+	asyncTx, err := db.Begin()
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("getting async tx: %w", err))
+		return
+	}
+
+	asyncStatusId, errCode, userErr, sysErr := api.InsertAsyncStatus(asyncTx, "CDNi host configuration update request received.")
+	if userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr)
+		return
+	}
+
+	//var typeToValueMap map[string]GenericRequestedLimits
+	//for _, hostUpdate := range genericHostRequest.HostMetadata.Metadata {
+	//	typeToValueMap[hostUpdate.Type] = hostUpdate.Value
+	//}
+
+	data := genericHostRequest.HostMetadata.Metadata
+
+	_, err = inf.Tx.Tx.Query(InsertCapabilityUpdateQuery, ucdn, data, asyncStatusId, hostConfigLabel, host)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("inserting capability update request into queue: %w", err))
+		return
+	}
+
+	var alerts tc.Alerts
+	alerts.AddAlert(tc.Alert{
+		Text:  "CDNi configuration update request received. Status updates can be found here: " + api.CurrentAsyncEndpoint + strconv.Itoa(asyncStatusId),
+		Level: tc.SuccessLevel.String(),
+	})
+
+	w.Header().Add("Location", api.CurrentAsyncEndpoint+strconv.Itoa(asyncStatusId))
+	api.WriteAlerts(w, r, http.StatusAccepted, alerts)
+}
+
+func PutConfiguration(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
+	}
+
+	ucdn, err := checkBearerToken(bearerToken, inf)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, err)
+		return
+	}
+
+	var genericRequest GenericRequestMetadata
+	err = json.NewDecoder(r.Body).Decode(&genericRequest)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("decoding json request: %w", err))
+		return
+	}
+
+	db, err := api.GetDB(r.Context())
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("getting async db: %w", err))
+		return
+	}
+	asyncTx, err := db.Begin()
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("getting async tx: %w", err))
+		return
+	}
+
+	asyncStatusId, errCode, userErr, sysErr := api.InsertAsyncStatus(asyncTx, "CDNi configuration update request received.")
+	if userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr)
+		return
+	}
+
+	data := genericRequest.Metadata
+
+	_, err = inf.Tx.Tx.Query(InsertCapabilityUpdateQuery, ucdn, data, asyncStatusId, SupportedGenericMetadataType(genericRequest.Type), genericRequest.Host)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("inserting capability update request into queue: %w", err))
+		return
+	}
+
+	var alerts tc.Alerts
+	alerts.AddAlert(tc.Alert{
+		Text:  "CDNi configuration update request received. Status updates can be found here: " + api.CurrentAsyncEndpoint + strconv.Itoa(asyncStatusId),
+		Level: tc.SuccessLevel.String(),
+	})
+
+	w.Header().Add("Location", api.CurrentAsyncEndpoint+strconv.Itoa(asyncStatusId))
+	api.WriteAlerts(w, r, http.StatusAccepted, alerts)
+}
+
+func PutConfigurationResponse(w http.ResponseWriter, r *http.Request) {
+	inf, userErr, sysErr, errCode := api.NewInfo(r, []string{"approved", "id"}, []string{"id"})
+	if userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr)
+		return
+	}
+	defer inf.Close()
+
+	reqId := inf.IntParams["id"]
+	approvedString := inf.Params["approved"]
+	approved, err := strconv.ParseBool(approvedString)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusBadRequest, errors.New("approved parameter must be a boolean"), nil)
+		return
+	}
+
+	db, err := api.GetDB(r.Context())
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("getting async db: %w", err))
+		return
+	}
+
+	rows, err := inf.Tx.Tx.Query(SelectCapabilityUpdateQuery, reqId)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("querying for capability update request: %w", err))
+		return
+	}
+	defer rows.Close()
+	var ucdn string
+	var data json.RawMessage
+	var host string
+	var asyncId int
+	var requestType string
+	count := 0
+	for rows.Next() {
+		if err := rows.Scan(&ucdn, &data, &asyncId, &requestType, &host); err != nil {
+			api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("scanning db rows: %w", err))
+			return
+		}
+		count++
+	}
+	if count == 0 {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusNotFound, fmt.Errorf("no configuration request for that id"), nil)
+		return
+	}
+
+	if !approved {
+		if asyncErr := api.UpdateAsyncStatus(db, api.AsyncFailed, "Requested configuration update has been denied.", asyncId, true); asyncErr != nil {
+			log.Errorf("updating async status for id %v: %v", asyncId, asyncErr)
+		}
+		status, err := deleteCapabilityRequest(reqId, inf.Tx.Tx)
+		if err != nil {
+			api.HandleErr(w, r, inf.Tx.Tx, status, nil, fmt.Errorf("deleting configuration request from queue: %w", err))
+			return
+		}
+		api.WriteResp(w, r, "Successfully denied configuration update request.")
+		return
+	}
+
+	var updatedDataList []GenericMetadata
+	if err = json.Unmarshal(data, &updatedDataList); err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("unmarshalling data for configuration update: %w", err))
+		return
+	}
+
+	var unsupportedTypes []string
+	for _, updatedData := range updatedDataList {
+		if !updatedData.Type.isValid() {
+			unsupportedTypes = append(unsupportedTypes, string(updatedData.Type))
+		}
+	}
+
+	if len(unsupportedTypes) != 0 {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusBadRequest, fmt.Errorf("unsupported generic metadata types found: %v", strings.Join(unsupportedTypes, ", ")), nil)
+		return
+	}
+
+	for _, updatedData := range updatedDataList {
+		switch updatedData.Type {
+		case MiRequestedCapacityLimits:
+			var capacityRequestedLimits CapacityRequestedLimits
+			if err = json.Unmarshal(updatedData.Value, &capacityRequestedLimits); err != nil {
+				api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("unmarshalling data for configuration update: %w", err))
+				return
+			}
+			for _, capLim := range capacityRequestedLimits.RequestedLimits {
+				capId, err := getCapabilityIdFromFootprints(capLim, ucdn, inf)
+				if err != nil {
+					api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("finding capability for given information: %w", err))
+					return
+				}
+
+				query := UpdateTotalLimitsByCapabilityAndLimitTypeQuery
+				queryParams := []interface{}{capLim.LimitValue, capId, capLim.LimitType}
+				if host != "" {
+					query = UpdateHostLimitsByCapabilityAndLimitTypeQuery
+					queryParams = []interface{}{capLim.LimitValue, capId, capLim.LimitType, host}
+				}
+
+				result, err := inf.Tx.Tx.Exec(query, queryParams...)
+				if err != nil {
+					api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("updating capacity: %w", err))
+					return
+				}
+
+				if rowsAffected, err := result.RowsAffected(); err != nil {
+					api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("updating capacity: getting rows affected: %w", err))
+					return
+				} else if rowsAffected < 1 {
+					api.HandleErr(w, r, inf.Tx.Tx, http.StatusNotFound, fmt.Errorf("no capacity found for update: host: %s, type: %s, limit: %v", host, updatedData.Type, capLim), nil)
+					return
+				} else if rowsAffected > 1 {
+					api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("capacity update affected too many rows: %d", rowsAffected))
+					return
+				}
+			}
+		}
+	}
+
+	if asyncErr := api.UpdateAsyncStatus(db, api.AsyncSucceeded, "Capacity requested update has been completed.", asyncId, true); asyncErr != nil {
+		log.Errorf("updating async status for id %v: %v", asyncId, asyncErr)
+	}
+	status, err := deleteCapabilityRequest(reqId, inf.Tx.Tx)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, status, nil, fmt.Errorf("deleting capacity request from queue: %w", err))
+		return
+	}
+	api.WriteResp(w, r, "Successfully updated configuration.")
+}
+
+func getCapabilityIdFromFootprints(updatedData CapacityLimit, ucdn string, inf *api.APIInfo) (int, error) {
+	var potentialCapabilityIds []int
+	for _, footprint := range updatedData.Footprints {
+		selectQuery := `SELECT footprint_type, footprint_value, capability_id FROM cdni_footprints WHERE ucdn = $1 AND footprint_type = $2 AND footprint_value = `
+		valuesAsQuery := "ARRAY['" + strings.Join(footprint.FootprintValue, "','") + "']"

Review comment:
       manually forming this array here seems weird -- can we not use `pq.Array` and pass it as a parameter?

##########
File path: traffic_ops/traffic_ops_golang/cdni/shared.go
##########
@@ -68,17 +82,388 @@ func GetCapabilities(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
+	ucdn, err := checkBearerToken(bearerToken, inf)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, err)
+		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 PutHostConfiguration(w http.ResponseWriter, r *http.Request) {
+	inf, userErr, sysErr, errCode := api.NewInfo(r, []string{"host"}, nil)
+	if userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr)
+		return
+	}
+	defer inf.Close()
+
+	host := inf.Params["host"]
+	if errCode, userErr, sysErr := validateHostExists(host, inf.Tx.Tx); userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr)
+		return
+	}
+
+	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
+	}
+
+	ucdn, err := checkBearerToken(bearerToken, inf)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, err)
+		return
+	}
+
+	var genericHostRequest GenericHostMetadata
+	err = json.NewDecoder(r.Body).Decode(&genericHostRequest)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("decoding host json request: %w", err))
+		return
+	}
+
+	db, err := api.GetDB(r.Context())
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("getting async db: %w", err))
+		return
+	}
+	asyncTx, err := db.Begin()
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("getting async tx: %w", err))
+		return
+	}
+
+	asyncStatusId, errCode, userErr, sysErr := api.InsertAsyncStatus(asyncTx, "CDNi host configuration update request received.")
+	if userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr)
+		return
+	}
+
+	//var typeToValueMap map[string]GenericRequestedLimits
+	//for _, hostUpdate := range genericHostRequest.HostMetadata.Metadata {
+	//	typeToValueMap[hostUpdate.Type] = hostUpdate.Value
+	//}

Review comment:
       commented code

##########
File path: lib/go-util/str.go
##########
@@ -52,6 +52,16 @@ func StrInArray(strs []string, s string) bool {
 	return false
 }
 
+// IntInArray returns whether i is one of the ints in list.

Review comment:
       This function probably doesn't belong in the `str.go` file -- perhaps `num.go` instead?

##########
File path: docs/source/api/v4/oc_ci_configuration.rst
##########
@@ -0,0 +1,101 @@
+..
+..
+.. Licensed 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.
+..
+
+.. _to-api-oc-fci-configuration:
+
+***********************
+``OC/CI/configuration``
+***********************
+
+``PUT``
+=======
+Triggers an asynchronous task to update the configuration for the :abbr:`uCDN (Upstream Content Delivery Network)` by adding the request to a queue to be reviewed later. This returns a 202 Accepted status and an endpoint to be used for status updates.
+
+:Auth. Required: Yes
+:Roles Required: "admin" or "operations"
+:Permissions Required: CDNI:READ
+:Response Type:  Object
+:Headers Required: "Authorization"
+
+Request Structure
+-----------------
+.. table:: Request Required Headers
+
+	+-----------------+------------------------------------------------------------------------------------------------------------------------------+
+	|    Name         | Description                                                                                                                  |
+	+=================+==============================================================================================================================+
+	|  Authorization  | A :abbr:`JWT (JSON Web Token)` provided by the :abbr:`dCDN (Downstream Content Delivery Network)` to identify the            |
+	|                 | :abbr:`uCDN (Upstream Content Delivery Network)`                                                                             |
+	+-----------------+------------------------------------------------------------------------------------------------------------------------------+
+
+:type: A string of the type of metadata to follow. See :rfc:`8006` for possible values. Only a selection of these are supported.
+:host: A string of the domain that the requested updates will change.
+:metadata: An array of generic metadata objects that conform to :rfc:`8006`.
+:generic-metadata-type: A string of the type of metadata to follow conforming to :rfc:`8006`.
+:generic-metadata-value: An array of generic metadata value objects conforming to :rfc:`8006` and :abbr:`SVA (Streaming Video Alliance)` specifications.
+
+.. code-block:: http
+	:caption: Example /OC/CI/configuration Request
+
+	POST /api/4.0/acme_accounts HTTP/1.1

Review comment:
       is this actually a PUT?

##########
File path: traffic_ops/traffic_ops_golang/routing/routes.go
##########
@@ -133,6 +133,9 @@ func Routes(d ServerData) ([]Route, http.Handler, error) {
 
 		// CDNI integration
 		{Version: api.Version{Major: 4, Minor: 0}, Method: http.MethodGet, Path: `OC/FCI/advertisement/?$`, Handler: cdni.GetCapabilities, RequiredPrivLevel: auth.PrivLevelReadOnly, RequiredPermissions: []string{"CDNI-CAPACITY:READ"}, Authenticated: Authenticated, Middlewares: nil, ID: 541357729077},
+		{Version: api.Version{Major: 4, Minor: 0}, Method: http.MethodPut, Path: `OC/CI/configuration/?$`, Handler: cdni.PutConfiguration, RequiredPrivLevel: auth.PrivLevelReadOnly, RequiredPermissions: []string{"CDNI-CAPACITY:READ"}, Authenticated: Authenticated, Middlewares: nil, ID: 541357729078},
+		{Version: api.Version{Major: 4, Minor: 0}, Method: http.MethodPut, Path: `OC/CI/configuration/{host}?$`, Handler: cdni.PutHostConfiguration, RequiredPrivLevel: auth.PrivLevelReadOnly, RequiredPermissions: []string{"CDNI-CAPACITY:READ"}, Authenticated: Authenticated, Middlewares: nil, ID: 541357729079},

Review comment:
       should these be "update" instead of "read"?

##########
File path: traffic_ops/traffic_ops_golang/cdni/shared.go
##########
@@ -49,6 +54,15 @@ 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`
 
+const InsertCapabilityUpdateQuery = `INSERT INTO cdni_capability_updates (ucdn, data, async_status_id, request_type, host) VALUES ($1, $2, $3, $4, $5)`
+const SelectCapabilityUpdateQuery = `SELECT ucdn, data, async_status_id, request_type, host FROM cdni_capability_updates WHERE id = $1`
+const DeleteCapabilityUpdateQuery = `DELETE FROM cdni_capability_updates WHERE id = $1`
+const UpdateTotalLimitsByCapabilityAndLimitTypeQuery = `UPDATE cdni_total_limits SET maximum_hard = $1 WHERE capability_id = $2 AND limit_type = $3`
+const UpdateHostLimitsByCapabilityAndLimitTypeQuery = `UPDATE cdni_host_limits SET maximum_hard = $1 WHERE capability_id = $2 AND limit_type = $3 AND host = $4`
+const hostQuery = `SELECT count(*) FROM cdni_host_limits WHERE host = $1`
+
+const hostConfigLabel = "hostConfigUpdate"

Review comment:
       nit: all these constants could go into a single `const (...)` declaration block

##########
File path: traffic_ops/traffic_ops_golang/cdni/shared.go
##########
@@ -68,17 +82,388 @@ func GetCapabilities(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
+	ucdn, err := checkBearerToken(bearerToken, inf)

Review comment:
       `checkBearerToken` should probably also do the `if bearerToken == ""` check above -- should we just pass `r.Header.Get("Authorization")` to it? That way the other routes will do the same check since they use the same function.

##########
File path: traffic_ops/traffic_ops_golang/routing/routes.go
##########
@@ -133,6 +133,9 @@ func Routes(d ServerData) ([]Route, http.Handler, error) {
 
 		// CDNI integration
 		{Version: api.Version{Major: 4, Minor: 0}, Method: http.MethodGet, Path: `OC/FCI/advertisement/?$`, Handler: cdni.GetCapabilities, RequiredPrivLevel: auth.PrivLevelReadOnly, RequiredPermissions: []string{"CDNI-CAPACITY:READ"}, Authenticated: Authenticated, Middlewares: nil, ID: 541357729077},
+		{Version: api.Version{Major: 4, Minor: 0}, Method: http.MethodPut, Path: `OC/CI/configuration/?$`, Handler: cdni.PutConfiguration, RequiredPrivLevel: auth.PrivLevelReadOnly, RequiredPermissions: []string{"CDNI-CAPACITY:READ"}, Authenticated: Authenticated, Middlewares: nil, ID: 541357729078},
+		{Version: api.Version{Major: 4, Minor: 0}, Method: http.MethodPut, Path: `OC/CI/configuration/{host}?$`, Handler: cdni.PutHostConfiguration, RequiredPrivLevel: auth.PrivLevelReadOnly, RequiredPermissions: []string{"CDNI-CAPACITY:READ"}, Authenticated: Authenticated, Middlewares: nil, ID: 541357729079},
+		{Version: api.Version{Major: 4, Minor: 0}, Method: http.MethodPut, Path: `OC/CI/configuration/request/{id}/{approved}?$`, Handler: cdni.PutConfigurationResponse, RequiredPrivLevel: auth.PrivLevelAdmin, RequiredPermissions: []string{"CDNI-CAPACITY:UPDATED"}, Authenticated: Authenticated, Middlewares: nil, ID: 541357729080},

Review comment:
       should this be "update" instead of "updated"?

##########
File path: traffic_ops/traffic_ops_golang/cdni/shared.go
##########
@@ -68,17 +82,388 @@ func GetCapabilities(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
+	ucdn, err := checkBearerToken(bearerToken, inf)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, err)
+		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 PutHostConfiguration(w http.ResponseWriter, r *http.Request) {
+	inf, userErr, sysErr, errCode := api.NewInfo(r, []string{"host"}, nil)
+	if userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr)
+		return
+	}
+	defer inf.Close()
+
+	host := inf.Params["host"]
+	if errCode, userErr, sysErr := validateHostExists(host, inf.Tx.Tx); userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr)
+		return
+	}
+
+	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
+	}
+
+	ucdn, err := checkBearerToken(bearerToken, inf)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, err)
+		return
+	}
+
+	var genericHostRequest GenericHostMetadata
+	err = json.NewDecoder(r.Body).Decode(&genericHostRequest)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("decoding host json request: %w", err))
+		return
+	}
+
+	db, err := api.GetDB(r.Context())
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("getting async db: %w", err))
+		return
+	}
+	asyncTx, err := db.Begin()
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("getting async tx: %w", err))
+		return
+	}
+
+	asyncStatusId, errCode, userErr, sysErr := api.InsertAsyncStatus(asyncTx, "CDNi host configuration update request received.")
+	if userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr)
+		return
+	}
+
+	//var typeToValueMap map[string]GenericRequestedLimits
+	//for _, hostUpdate := range genericHostRequest.HostMetadata.Metadata {
+	//	typeToValueMap[hostUpdate.Type] = hostUpdate.Value
+	//}
+
+	data := genericHostRequest.HostMetadata.Metadata
+
+	_, err = inf.Tx.Tx.Query(InsertCapabilityUpdateQuery, ucdn, data, asyncStatusId, hostConfigLabel, host)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("inserting capability update request into queue: %w", err))
+		return
+	}
+
+	var alerts tc.Alerts
+	alerts.AddAlert(tc.Alert{
+		Text:  "CDNi configuration update request received. Status updates can be found here: " + api.CurrentAsyncEndpoint + strconv.Itoa(asyncStatusId),
+		Level: tc.SuccessLevel.String(),
+	})
+

Review comment:
       Do we need a changelog entry for this operation?

##########
File path: docs/source/api/v4/oc_ci_configuration_request_id_approved.rst
##########
@@ -0,0 +1,60 @@
+..
+..
+.. Licensed 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.
+..
+
+.. _to-api-oc-fci-configuration-request-id-approved:
+
+***************************************************
+``OC/CI/configuration/request/{{id}}/{{approved}}``
+***************************************************
+
+``PUT``
+=======
+Triggers an asynchronous task to update the configuration for the :abbr:`uCDN (Upstream Content Delivery Network)` and the specified host by adding the request to a queue to be reviewed later. This returns a 202 Accepted status and an endpoint to be used for status updates.
+
+:Auth. Required: Yes
+:Roles Required: "admin"
+:Permissions Required: CDNI-CAPACITY:UPDATED

Review comment:
       updated -> update?

##########
File path: docs/source/api/v4/oc_ci_configuration_host.rst
##########
@@ -0,0 +1,107 @@
+..
+..
+.. Licensed 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.
+..
+
+.. _to-api-oc-fci-configuration-host:
+
+********************************
+``OC/CI/configuration/{{host}}``
+********************************
+
+``PUT``
+=======
+Triggers an asynchronous task to update the configuration for the :abbr:`uCDN (Upstream Content Delivery Network)` and the specified host by adding the request to a queue to be reviewed later. This returns a 202 Accepted status and an endpoint to be used for status updates.
+
+:Auth. Required: Yes
+:Roles Required: "admin" or "operations"
+:Permissions Required: CDNI:READ
+:Response Type:  Object
+:Headers Required: "Authorization"
+
+Request Structure
+-----------------
+.. table:: Request Required Headers
+
+	+-----------------+------------------------------------------------------------------------------------------------------------------------------+
+	|    Name         | Description                                                                                                                  |
+	+=================+==============================================================================================================================+
+	|  Authorization  | A :abbr:`JWT (JSON Web Token)` provided by the :abbr:`dCDN (Downstream Content Delivery Network)` to identify the            |
+	|                 | :abbr:`uCDN (Upstream Content Delivery Network)`                                                                             |
+	+-----------------+------------------------------------------------------------------------------------------------------------------------------+
+
+.. table:: Request Path Parameters
+
+	+-------+-----------------------------------------------------------------------------------+
+	| Name  |                 Description                                                       |
+	+=======+===================================================================================+
+	|  host | The text identifier for the host domain to be updated with the new configuration. |
+	+-------+-----------------------------------------------------------------------------------+
+
+:type: A string of the type of metadata to follow. See :rfc:`8006` for possible values. Only a selection of these are supported.
+:host-metadata: An array of generic metadata objects that conform to :rfc:`8006`.
+:generic-metadata-type: A string of the type of metadata to follow conforming to :rfc:`8006`.
+:generic-metadata-value: An array of generic metadata value objects conforming to :rfc:`8006` and :abbr:`SVA (Streaming Video Alliance)` specifications.
+
+.. code-block:: http
+	:caption: Example /OC/CI/configuration Request
+
+	POST /api/4.0/acme_accounts HTTP/1.1

Review comment:
       is this actually a PUT?

##########
File path: traffic_ops/traffic_ops_golang/cdni/shared.go
##########
@@ -68,17 +82,388 @@ func GetCapabilities(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
+	ucdn, err := checkBearerToken(bearerToken, inf)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, err)
+		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 PutHostConfiguration(w http.ResponseWriter, r *http.Request) {
+	inf, userErr, sysErr, errCode := api.NewInfo(r, []string{"host"}, nil)
+	if userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr)
+		return
+	}
+	defer inf.Close()
+
+	host := inf.Params["host"]
+	if errCode, userErr, sysErr := validateHostExists(host, inf.Tx.Tx); userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr)
+		return
+	}
+
+	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
+	}
+
+	ucdn, err := checkBearerToken(bearerToken, inf)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, err)
+		return
+	}
+
+	var genericHostRequest GenericHostMetadata
+	err = json.NewDecoder(r.Body).Decode(&genericHostRequest)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("decoding host json request: %w", err))
+		return
+	}
+
+	db, err := api.GetDB(r.Context())
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("getting async db: %w", err))
+		return
+	}
+	asyncTx, err := db.Begin()
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("getting async tx: %w", err))
+		return
+	}
+
+	asyncStatusId, errCode, userErr, sysErr := api.InsertAsyncStatus(asyncTx, "CDNi host configuration update request received.")
+	if userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr)
+		return
+	}
+
+	//var typeToValueMap map[string]GenericRequestedLimits
+	//for _, hostUpdate := range genericHostRequest.HostMetadata.Metadata {
+	//	typeToValueMap[hostUpdate.Type] = hostUpdate.Value
+	//}
+
+	data := genericHostRequest.HostMetadata.Metadata
+
+	_, err = inf.Tx.Tx.Query(InsertCapabilityUpdateQuery, ucdn, data, asyncStatusId, hostConfigLabel, host)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("inserting capability update request into queue: %w", err))
+		return
+	}
+
+	var alerts tc.Alerts
+	alerts.AddAlert(tc.Alert{
+		Text:  "CDNi configuration update request received. Status updates can be found here: " + api.CurrentAsyncEndpoint + strconv.Itoa(asyncStatusId),
+		Level: tc.SuccessLevel.String(),
+	})
+
+	w.Header().Add("Location", api.CurrentAsyncEndpoint+strconv.Itoa(asyncStatusId))
+	api.WriteAlerts(w, r, http.StatusAccepted, alerts)
+}
+
+func PutConfiguration(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
+	}
+
+	ucdn, err := checkBearerToken(bearerToken, inf)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, err)
+		return
+	}
+
+	var genericRequest GenericRequestMetadata
+	err = json.NewDecoder(r.Body).Decode(&genericRequest)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("decoding json request: %w", err))
+		return
+	}
+
+	db, err := api.GetDB(r.Context())
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("getting async db: %w", err))
+		return
+	}
+	asyncTx, err := db.Begin()
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("getting async tx: %w", err))
+		return
+	}
+
+	asyncStatusId, errCode, userErr, sysErr := api.InsertAsyncStatus(asyncTx, "CDNi configuration update request received.")
+	if userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr)
+		return
+	}
+
+	data := genericRequest.Metadata
+
+	_, err = inf.Tx.Tx.Query(InsertCapabilityUpdateQuery, ucdn, data, asyncStatusId, SupportedGenericMetadataType(genericRequest.Type), genericRequest.Host)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("inserting capability update request into queue: %w", err))
+		return
+	}
+
+	var alerts tc.Alerts
+	alerts.AddAlert(tc.Alert{
+		Text:  "CDNi configuration update request received. Status updates can be found here: " + api.CurrentAsyncEndpoint + strconv.Itoa(asyncStatusId),
+		Level: tc.SuccessLevel.String(),
+	})
+

Review comment:
       Do we need a changelog entry for this operation?

##########
File path: traffic_ops/traffic_ops_golang/cdni/shared.go
##########
@@ -68,17 +82,388 @@ func GetCapabilities(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
+	ucdn, err := checkBearerToken(bearerToken, inf)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, err)
+		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 PutHostConfiguration(w http.ResponseWriter, r *http.Request) {
+	inf, userErr, sysErr, errCode := api.NewInfo(r, []string{"host"}, nil)
+	if userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr)
+		return
+	}
+	defer inf.Close()
+
+	host := inf.Params["host"]
+	if errCode, userErr, sysErr := validateHostExists(host, inf.Tx.Tx); userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr)
+		return
+	}
+
+	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
+	}
+
+	ucdn, err := checkBearerToken(bearerToken, inf)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, err)
+		return
+	}
+
+	var genericHostRequest GenericHostMetadata
+	err = json.NewDecoder(r.Body).Decode(&genericHostRequest)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("decoding host json request: %w", err))
+		return
+	}
+
+	db, err := api.GetDB(r.Context())
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("getting async db: %w", err))
+		return
+	}
+	asyncTx, err := db.Begin()
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("getting async tx: %w", err))
+		return
+	}
+
+	asyncStatusId, errCode, userErr, sysErr := api.InsertAsyncStatus(asyncTx, "CDNi host configuration update request received.")
+	if userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr)
+		return
+	}
+
+	//var typeToValueMap map[string]GenericRequestedLimits
+	//for _, hostUpdate := range genericHostRequest.HostMetadata.Metadata {
+	//	typeToValueMap[hostUpdate.Type] = hostUpdate.Value
+	//}
+
+	data := genericHostRequest.HostMetadata.Metadata
+
+	_, err = inf.Tx.Tx.Query(InsertCapabilityUpdateQuery, ucdn, data, asyncStatusId, hostConfigLabel, host)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("inserting capability update request into queue: %w", err))
+		return
+	}
+
+	var alerts tc.Alerts
+	alerts.AddAlert(tc.Alert{
+		Text:  "CDNi configuration update request received. Status updates can be found here: " + api.CurrentAsyncEndpoint + strconv.Itoa(asyncStatusId),
+		Level: tc.SuccessLevel.String(),
+	})
+
+	w.Header().Add("Location", api.CurrentAsyncEndpoint+strconv.Itoa(asyncStatusId))
+	api.WriteAlerts(w, r, http.StatusAccepted, alerts)
+}
+
+func PutConfiguration(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
+	}
+
+	ucdn, err := checkBearerToken(bearerToken, inf)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, err)
+		return
+	}
+
+	var genericRequest GenericRequestMetadata
+	err = json.NewDecoder(r.Body).Decode(&genericRequest)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("decoding json request: %w", err))
+		return
+	}
+
+	db, err := api.GetDB(r.Context())
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("getting async db: %w", err))
+		return
+	}
+	asyncTx, err := db.Begin()
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("getting async tx: %w", err))
+		return
+	}
+
+	asyncStatusId, errCode, userErr, sysErr := api.InsertAsyncStatus(asyncTx, "CDNi configuration update request received.")
+	if userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr)
+		return
+	}
+
+	data := genericRequest.Metadata
+
+	_, err = inf.Tx.Tx.Query(InsertCapabilityUpdateQuery, ucdn, data, asyncStatusId, SupportedGenericMetadataType(genericRequest.Type), genericRequest.Host)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("inserting capability update request into queue: %w", err))
+		return
+	}
+
+	var alerts tc.Alerts
+	alerts.AddAlert(tc.Alert{
+		Text:  "CDNi configuration update request received. Status updates can be found here: " + api.CurrentAsyncEndpoint + strconv.Itoa(asyncStatusId),
+		Level: tc.SuccessLevel.String(),
+	})
+
+	w.Header().Add("Location", api.CurrentAsyncEndpoint+strconv.Itoa(asyncStatusId))
+	api.WriteAlerts(w, r, http.StatusAccepted, alerts)
+}
+
+func PutConfigurationResponse(w http.ResponseWriter, r *http.Request) {
+	inf, userErr, sysErr, errCode := api.NewInfo(r, []string{"approved", "id"}, []string{"id"})
+	if userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr)
+		return
+	}
+	defer inf.Close()
+
+	reqId := inf.IntParams["id"]
+	approvedString := inf.Params["approved"]
+	approved, err := strconv.ParseBool(approvedString)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusBadRequest, errors.New("approved parameter must be a boolean"), nil)
+		return
+	}
+
+	db, err := api.GetDB(r.Context())
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("getting async db: %w", err))
+		return
+	}
+
+	rows, err := inf.Tx.Tx.Query(SelectCapabilityUpdateQuery, reqId)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("querying for capability update request: %w", err))
+		return
+	}
+	defer rows.Close()
+	var ucdn string
+	var data json.RawMessage
+	var host string
+	var asyncId int
+	var requestType string
+	count := 0
+	for rows.Next() {
+		if err := rows.Scan(&ucdn, &data, &asyncId, &requestType, &host); err != nil {
+			api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("scanning db rows: %w", err))
+			return
+		}
+		count++
+	}
+	if count == 0 {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusNotFound, fmt.Errorf("no configuration request for that id"), nil)
+		return
+	}
+
+	if !approved {
+		if asyncErr := api.UpdateAsyncStatus(db, api.AsyncFailed, "Requested configuration update has been denied.", asyncId, true); asyncErr != nil {
+			log.Errorf("updating async status for id %v: %v", asyncId, asyncErr)
+		}
+		status, err := deleteCapabilityRequest(reqId, inf.Tx.Tx)
+		if err != nil {
+			api.HandleErr(w, r, inf.Tx.Tx, status, nil, fmt.Errorf("deleting configuration request from queue: %w", err))
+			return
+		}
+		api.WriteResp(w, r, "Successfully denied configuration update request.")
+		return
+	}
+
+	var updatedDataList []GenericMetadata
+	if err = json.Unmarshal(data, &updatedDataList); err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("unmarshalling data for configuration update: %w", err))
+		return
+	}
+
+	var unsupportedTypes []string
+	for _, updatedData := range updatedDataList {
+		if !updatedData.Type.isValid() {
+			unsupportedTypes = append(unsupportedTypes, string(updatedData.Type))
+		}
+	}
+
+	if len(unsupportedTypes) != 0 {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusBadRequest, fmt.Errorf("unsupported generic metadata types found: %v", strings.Join(unsupportedTypes, ", ")), nil)
+		return
+	}
+
+	for _, updatedData := range updatedDataList {
+		switch updatedData.Type {
+		case MiRequestedCapacityLimits:
+			var capacityRequestedLimits CapacityRequestedLimits
+			if err = json.Unmarshal(updatedData.Value, &capacityRequestedLimits); err != nil {
+				api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("unmarshalling data for configuration update: %w", err))
+				return
+			}
+			for _, capLim := range capacityRequestedLimits.RequestedLimits {
+				capId, err := getCapabilityIdFromFootprints(capLim, ucdn, inf)
+				if err != nil {
+					api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("finding capability for given information: %w", err))
+					return
+				}
+
+				query := UpdateTotalLimitsByCapabilityAndLimitTypeQuery
+				queryParams := []interface{}{capLim.LimitValue, capId, capLim.LimitType}
+				if host != "" {
+					query = UpdateHostLimitsByCapabilityAndLimitTypeQuery
+					queryParams = []interface{}{capLim.LimitValue, capId, capLim.LimitType, host}
+				}
+
+				result, err := inf.Tx.Tx.Exec(query, queryParams...)
+				if err != nil {
+					api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("updating capacity: %w", err))
+					return
+				}
+
+				if rowsAffected, err := result.RowsAffected(); err != nil {
+					api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("updating capacity: getting rows affected: %w", err))
+					return
+				} else if rowsAffected < 1 {
+					api.HandleErr(w, r, inf.Tx.Tx, http.StatusNotFound, fmt.Errorf("no capacity found for update: host: %s, type: %s, limit: %v", host, updatedData.Type, capLim), nil)
+					return
+				} else if rowsAffected > 1 {
+					api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("capacity update affected too many rows: %d", rowsAffected))
+					return
+				}
+			}
+		}
+	}
+
+	if asyncErr := api.UpdateAsyncStatus(db, api.AsyncSucceeded, "Capacity requested update has been completed.", asyncId, true); asyncErr != nil {
+		log.Errorf("updating async status for id %v: %v", asyncId, asyncErr)
+	}
+	status, err := deleteCapabilityRequest(reqId, inf.Tx.Tx)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, status, nil, fmt.Errorf("deleting capacity request from queue: %w", err))
+		return
+	}

Review comment:
       Do we need a changelog entry for this operation?

##########
File path: traffic_ops/traffic_ops_golang/cdni/shared.go
##########
@@ -68,17 +82,388 @@ func GetCapabilities(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
+	ucdn, err := checkBearerToken(bearerToken, inf)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, err)
+		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 PutHostConfiguration(w http.ResponseWriter, r *http.Request) {
+	inf, userErr, sysErr, errCode := api.NewInfo(r, []string{"host"}, nil)
+	if userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr)
+		return
+	}
+	defer inf.Close()
+
+	host := inf.Params["host"]
+	if errCode, userErr, sysErr := validateHostExists(host, inf.Tx.Tx); userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr)
+		return
+	}
+
+	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
+	}
+
+	ucdn, err := checkBearerToken(bearerToken, inf)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, err)
+		return
+	}
+
+	var genericHostRequest GenericHostMetadata
+	err = json.NewDecoder(r.Body).Decode(&genericHostRequest)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("decoding host json request: %w", err))
+		return
+	}
+
+	db, err := api.GetDB(r.Context())
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("getting async db: %w", err))
+		return
+	}
+	asyncTx, err := db.Begin()
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("getting async tx: %w", err))
+		return
+	}
+
+	asyncStatusId, errCode, userErr, sysErr := api.InsertAsyncStatus(asyncTx, "CDNi host configuration update request received.")
+	if userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr)
+		return
+	}
+
+	//var typeToValueMap map[string]GenericRequestedLimits
+	//for _, hostUpdate := range genericHostRequest.HostMetadata.Metadata {
+	//	typeToValueMap[hostUpdate.Type] = hostUpdate.Value
+	//}
+
+	data := genericHostRequest.HostMetadata.Metadata
+
+	_, err = inf.Tx.Tx.Query(InsertCapabilityUpdateQuery, ucdn, data, asyncStatusId, hostConfigLabel, host)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("inserting capability update request into queue: %w", err))
+		return
+	}
+
+	var alerts tc.Alerts
+	alerts.AddAlert(tc.Alert{
+		Text:  "CDNi configuration update request received. Status updates can be found here: " + api.CurrentAsyncEndpoint + strconv.Itoa(asyncStatusId),
+		Level: tc.SuccessLevel.String(),
+	})
+
+	w.Header().Add("Location", api.CurrentAsyncEndpoint+strconv.Itoa(asyncStatusId))
+	api.WriteAlerts(w, r, http.StatusAccepted, alerts)
+}
+
+func PutConfiguration(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
+	}
+
+	ucdn, err := checkBearerToken(bearerToken, inf)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, err)
+		return
+	}
+
+	var genericRequest GenericRequestMetadata
+	err = json.NewDecoder(r.Body).Decode(&genericRequest)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("decoding json request: %w", err))
+		return
+	}
+
+	db, err := api.GetDB(r.Context())
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("getting async db: %w", err))
+		return
+	}
+	asyncTx, err := db.Begin()
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("getting async tx: %w", err))
+		return
+	}
+
+	asyncStatusId, errCode, userErr, sysErr := api.InsertAsyncStatus(asyncTx, "CDNi configuration update request received.")
+	if userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr)
+		return
+	}
+
+	data := genericRequest.Metadata
+
+	_, err = inf.Tx.Tx.Query(InsertCapabilityUpdateQuery, ucdn, data, asyncStatusId, SupportedGenericMetadataType(genericRequest.Type), genericRequest.Host)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("inserting capability update request into queue: %w", err))
+		return
+	}
+
+	var alerts tc.Alerts
+	alerts.AddAlert(tc.Alert{
+		Text:  "CDNi configuration update request received. Status updates can be found here: " + api.CurrentAsyncEndpoint + strconv.Itoa(asyncStatusId),
+		Level: tc.SuccessLevel.String(),
+	})
+
+	w.Header().Add("Location", api.CurrentAsyncEndpoint+strconv.Itoa(asyncStatusId))
+	api.WriteAlerts(w, r, http.StatusAccepted, alerts)
+}
+
+func PutConfigurationResponse(w http.ResponseWriter, r *http.Request) {
+	inf, userErr, sysErr, errCode := api.NewInfo(r, []string{"approved", "id"}, []string{"id"})
+	if userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr)
+		return
+	}
+	defer inf.Close()
+
+	reqId := inf.IntParams["id"]
+	approvedString := inf.Params["approved"]
+	approved, err := strconv.ParseBool(approvedString)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusBadRequest, errors.New("approved parameter must be a boolean"), nil)
+		return
+	}
+
+	db, err := api.GetDB(r.Context())
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("getting async db: %w", err))
+		return
+	}
+
+	rows, err := inf.Tx.Tx.Query(SelectCapabilityUpdateQuery, reqId)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("querying for capability update request: %w", err))
+		return
+	}
+	defer rows.Close()
+	var ucdn string
+	var data json.RawMessage
+	var host string
+	var asyncId int
+	var requestType string
+	count := 0
+	for rows.Next() {
+		if err := rows.Scan(&ucdn, &data, &asyncId, &requestType, &host); err != nil {
+			api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("scanning db rows: %w", err))
+			return
+		}
+		count++
+	}
+	if count == 0 {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusNotFound, fmt.Errorf("no configuration request for that id"), nil)
+		return
+	}
+
+	if !approved {
+		if asyncErr := api.UpdateAsyncStatus(db, api.AsyncFailed, "Requested configuration update has been denied.", asyncId, true); asyncErr != nil {
+			log.Errorf("updating async status for id %v: %v", asyncId, asyncErr)
+		}
+		status, err := deleteCapabilityRequest(reqId, inf.Tx.Tx)
+		if err != nil {
+			api.HandleErr(w, r, inf.Tx.Tx, status, nil, fmt.Errorf("deleting configuration request from queue: %w", err))
+			return
+		}
+		api.WriteResp(w, r, "Successfully denied configuration update request.")
+		return
+	}
+
+	var updatedDataList []GenericMetadata
+	if err = json.Unmarshal(data, &updatedDataList); err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("unmarshalling data for configuration update: %w", err))
+		return
+	}
+
+	var unsupportedTypes []string
+	for _, updatedData := range updatedDataList {
+		if !updatedData.Type.isValid() {
+			unsupportedTypes = append(unsupportedTypes, string(updatedData.Type))
+		}
+	}
+
+	if len(unsupportedTypes) != 0 {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusBadRequest, fmt.Errorf("unsupported generic metadata types found: %v", strings.Join(unsupportedTypes, ", ")), nil)
+		return
+	}
+
+	for _, updatedData := range updatedDataList {
+		switch updatedData.Type {
+		case MiRequestedCapacityLimits:
+			var capacityRequestedLimits CapacityRequestedLimits
+			if err = json.Unmarshal(updatedData.Value, &capacityRequestedLimits); err != nil {
+				api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("unmarshalling data for configuration update: %w", err))
+				return
+			}
+			for _, capLim := range capacityRequestedLimits.RequestedLimits {
+				capId, err := getCapabilityIdFromFootprints(capLim, ucdn, inf)
+				if err != nil {
+					api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("finding capability for given information: %w", err))
+					return
+				}
+
+				query := UpdateTotalLimitsByCapabilityAndLimitTypeQuery
+				queryParams := []interface{}{capLim.LimitValue, capId, capLim.LimitType}
+				if host != "" {
+					query = UpdateHostLimitsByCapabilityAndLimitTypeQuery
+					queryParams = []interface{}{capLim.LimitValue, capId, capLim.LimitType, host}
+				}
+
+				result, err := inf.Tx.Tx.Exec(query, queryParams...)
+				if err != nil {
+					api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("updating capacity: %w", err))
+					return
+				}
+
+				if rowsAffected, err := result.RowsAffected(); err != nil {
+					api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("updating capacity: getting rows affected: %w", err))
+					return
+				} else if rowsAffected < 1 {
+					api.HandleErr(w, r, inf.Tx.Tx, http.StatusNotFound, fmt.Errorf("no capacity found for update: host: %s, type: %s, limit: %v", host, updatedData.Type, capLim), nil)
+					return
+				} else if rowsAffected > 1 {
+					api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("capacity update affected too many rows: %d", rowsAffected))
+					return
+				}
+			}
+		}
+	}
+
+	if asyncErr := api.UpdateAsyncStatus(db, api.AsyncSucceeded, "Capacity requested update has been completed.", asyncId, true); asyncErr != nil {
+		log.Errorf("updating async status for id %v: %v", asyncId, asyncErr)
+	}
+	status, err := deleteCapabilityRequest(reqId, inf.Tx.Tx)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, status, nil, fmt.Errorf("deleting capacity request from queue: %w", err))
+		return
+	}
+	api.WriteResp(w, r, "Successfully updated configuration.")
+}
+
+func getCapabilityIdFromFootprints(updatedData CapacityLimit, ucdn string, inf *api.APIInfo) (int, error) {
+	var potentialCapabilityIds []int
+	for _, footprint := range updatedData.Footprints {
+		selectQuery := `SELECT footprint_type, footprint_value, capability_id FROM cdni_footprints WHERE ucdn = $1 AND footprint_type = $2 AND footprint_value = `
+		valuesAsQuery := "ARRAY['" + strings.Join(footprint.FootprintValue, "','") + "']"
+		rows, err := inf.Tx.Tx.Query(selectQuery+valuesAsQuery, ucdn, footprint.FootprintType)
+		if err != nil {
+			return 0, fmt.Errorf("querying for capacity update request: %w", err)
+		}
+		defer rows.Close()

Review comment:
       could use `log.Close`

##########
File path: traffic_ops/traffic_ops_golang/cdni/shared.go
##########
@@ -68,17 +82,388 @@ func GetCapabilities(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
+	ucdn, err := checkBearerToken(bearerToken, inf)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, err)
+		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 PutHostConfiguration(w http.ResponseWriter, r *http.Request) {
+	inf, userErr, sysErr, errCode := api.NewInfo(r, []string{"host"}, nil)
+	if userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr)
+		return
+	}
+	defer inf.Close()
+
+	host := inf.Params["host"]
+	if errCode, userErr, sysErr := validateHostExists(host, inf.Tx.Tx); userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr)
+		return
+	}
+
+	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
+	}
+
+	ucdn, err := checkBearerToken(bearerToken, inf)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, err)
+		return
+	}
+
+	var genericHostRequest GenericHostMetadata
+	err = json.NewDecoder(r.Body).Decode(&genericHostRequest)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("decoding host json request: %w", err))
+		return
+	}
+
+	db, err := api.GetDB(r.Context())
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("getting async db: %w", err))
+		return
+	}
+	asyncTx, err := db.Begin()
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("getting async tx: %w", err))
+		return
+	}
+
+	asyncStatusId, errCode, userErr, sysErr := api.InsertAsyncStatus(asyncTx, "CDNi host configuration update request received.")
+	if userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr)
+		return
+	}
+
+	//var typeToValueMap map[string]GenericRequestedLimits
+	//for _, hostUpdate := range genericHostRequest.HostMetadata.Metadata {
+	//	typeToValueMap[hostUpdate.Type] = hostUpdate.Value
+	//}
+
+	data := genericHostRequest.HostMetadata.Metadata
+
+	_, err = inf.Tx.Tx.Query(InsertCapabilityUpdateQuery, ucdn, data, asyncStatusId, hostConfigLabel, host)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("inserting capability update request into queue: %w", err))
+		return
+	}
+
+	var alerts tc.Alerts
+	alerts.AddAlert(tc.Alert{
+		Text:  "CDNi configuration update request received. Status updates can be found here: " + api.CurrentAsyncEndpoint + strconv.Itoa(asyncStatusId),
+		Level: tc.SuccessLevel.String(),
+	})
+
+	w.Header().Add("Location", api.CurrentAsyncEndpoint+strconv.Itoa(asyncStatusId))
+	api.WriteAlerts(w, r, http.StatusAccepted, alerts)
+}
+
+func PutConfiguration(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
+	}
+
+	ucdn, err := checkBearerToken(bearerToken, inf)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, err)
+		return
+	}
+
+	var genericRequest GenericRequestMetadata
+	err = json.NewDecoder(r.Body).Decode(&genericRequest)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("decoding json request: %w", err))
+		return
+	}
+
+	db, err := api.GetDB(r.Context())
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("getting async db: %w", err))
+		return
+	}
+	asyncTx, err := db.Begin()
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("getting async tx: %w", err))
+		return
+	}
+
+	asyncStatusId, errCode, userErr, sysErr := api.InsertAsyncStatus(asyncTx, "CDNi configuration update request received.")
+	if userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr)
+		return
+	}
+
+	data := genericRequest.Metadata
+
+	_, err = inf.Tx.Tx.Query(InsertCapabilityUpdateQuery, ucdn, data, asyncStatusId, SupportedGenericMetadataType(genericRequest.Type), genericRequest.Host)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("inserting capability update request into queue: %w", err))
+		return
+	}
+
+	var alerts tc.Alerts
+	alerts.AddAlert(tc.Alert{
+		Text:  "CDNi configuration update request received. Status updates can be found here: " + api.CurrentAsyncEndpoint + strconv.Itoa(asyncStatusId),
+		Level: tc.SuccessLevel.String(),
+	})
+
+	w.Header().Add("Location", api.CurrentAsyncEndpoint+strconv.Itoa(asyncStatusId))
+	api.WriteAlerts(w, r, http.StatusAccepted, alerts)
+}
+
+func PutConfigurationResponse(w http.ResponseWriter, r *http.Request) {
+	inf, userErr, sysErr, errCode := api.NewInfo(r, []string{"approved", "id"}, []string{"id"})
+	if userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr)
+		return
+	}
+	defer inf.Close()
+
+	reqId := inf.IntParams["id"]
+	approvedString := inf.Params["approved"]
+	approved, err := strconv.ParseBool(approvedString)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusBadRequest, errors.New("approved parameter must be a boolean"), nil)
+		return
+	}
+
+	db, err := api.GetDB(r.Context())
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("getting async db: %w", err))
+		return
+	}
+
+	rows, err := inf.Tx.Tx.Query(SelectCapabilityUpdateQuery, reqId)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("querying for capability update request: %w", err))
+		return
+	}
+	defer rows.Close()

Review comment:
       could use `log.Close`

##########
File path: traffic_ops/traffic_ops_golang/cdni/shared.go
##########
@@ -68,17 +82,388 @@ func GetCapabilities(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
+	ucdn, err := checkBearerToken(bearerToken, inf)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, err)
+		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 PutHostConfiguration(w http.ResponseWriter, r *http.Request) {
+	inf, userErr, sysErr, errCode := api.NewInfo(r, []string{"host"}, nil)
+	if userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr)
+		return
+	}
+	defer inf.Close()
+
+	host := inf.Params["host"]
+	if errCode, userErr, sysErr := validateHostExists(host, inf.Tx.Tx); userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr)
+		return
+	}
+
+	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
+	}
+
+	ucdn, err := checkBearerToken(bearerToken, inf)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, err)
+		return
+	}
+
+	var genericHostRequest GenericHostMetadata
+	err = json.NewDecoder(r.Body).Decode(&genericHostRequest)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("decoding host json request: %w", err))
+		return
+	}
+
+	db, err := api.GetDB(r.Context())
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("getting async db: %w", err))
+		return
+	}
+	asyncTx, err := db.Begin()
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("getting async tx: %w", err))
+		return
+	}
+
+	asyncStatusId, errCode, userErr, sysErr := api.InsertAsyncStatus(asyncTx, "CDNi host configuration update request received.")
+	if userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr)
+		return
+	}
+
+	//var typeToValueMap map[string]GenericRequestedLimits
+	//for _, hostUpdate := range genericHostRequest.HostMetadata.Metadata {
+	//	typeToValueMap[hostUpdate.Type] = hostUpdate.Value
+	//}
+
+	data := genericHostRequest.HostMetadata.Metadata
+
+	_, err = inf.Tx.Tx.Query(InsertCapabilityUpdateQuery, ucdn, data, asyncStatusId, hostConfigLabel, host)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("inserting capability update request into queue: %w", err))
+		return
+	}
+
+	var alerts tc.Alerts
+	alerts.AddAlert(tc.Alert{
+		Text:  "CDNi configuration update request received. Status updates can be found here: " + api.CurrentAsyncEndpoint + strconv.Itoa(asyncStatusId),
+		Level: tc.SuccessLevel.String(),
+	})
+
+	w.Header().Add("Location", api.CurrentAsyncEndpoint+strconv.Itoa(asyncStatusId))
+	api.WriteAlerts(w, r, http.StatusAccepted, alerts)
+}
+
+func PutConfiguration(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
+	}
+
+	ucdn, err := checkBearerToken(bearerToken, inf)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, err)
+		return
+	}
+
+	var genericRequest GenericRequestMetadata
+	err = json.NewDecoder(r.Body).Decode(&genericRequest)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("decoding json request: %w", err))
+		return
+	}
+
+	db, err := api.GetDB(r.Context())
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("getting async db: %w", err))
+		return
+	}
+	asyncTx, err := db.Begin()
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("getting async tx: %w", err))
+		return
+	}
+
+	asyncStatusId, errCode, userErr, sysErr := api.InsertAsyncStatus(asyncTx, "CDNi configuration update request received.")
+	if userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr)
+		return
+	}
+
+	data := genericRequest.Metadata
+
+	_, err = inf.Tx.Tx.Query(InsertCapabilityUpdateQuery, ucdn, data, asyncStatusId, SupportedGenericMetadataType(genericRequest.Type), genericRequest.Host)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("inserting capability update request into queue: %w", err))
+		return
+	}
+
+	var alerts tc.Alerts
+	alerts.AddAlert(tc.Alert{
+		Text:  "CDNi configuration update request received. Status updates can be found here: " + api.CurrentAsyncEndpoint + strconv.Itoa(asyncStatusId),
+		Level: tc.SuccessLevel.String(),
+	})
+
+	w.Header().Add("Location", api.CurrentAsyncEndpoint+strconv.Itoa(asyncStatusId))
+	api.WriteAlerts(w, r, http.StatusAccepted, alerts)
+}
+
+func PutConfigurationResponse(w http.ResponseWriter, r *http.Request) {
+	inf, userErr, sysErr, errCode := api.NewInfo(r, []string{"approved", "id"}, []string{"id"})
+	if userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr)
+		return
+	}
+	defer inf.Close()
+
+	reqId := inf.IntParams["id"]
+	approvedString := inf.Params["approved"]
+	approved, err := strconv.ParseBool(approvedString)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusBadRequest, errors.New("approved parameter must be a boolean"), nil)
+		return
+	}
+
+	db, err := api.GetDB(r.Context())
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("getting async db: %w", err))
+		return
+	}
+
+	rows, err := inf.Tx.Tx.Query(SelectCapabilityUpdateQuery, reqId)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("querying for capability update request: %w", err))
+		return
+	}
+	defer rows.Close()
+	var ucdn string
+	var data json.RawMessage
+	var host string
+	var asyncId int
+	var requestType string
+	count := 0
+	for rows.Next() {
+		if err := rows.Scan(&ucdn, &data, &asyncId, &requestType, &host); err != nil {
+			api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("scanning db rows: %w", err))
+			return
+		}
+		count++
+	}
+	if count == 0 {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusNotFound, fmt.Errorf("no configuration request for that id"), nil)
+		return
+	}
+
+	if !approved {
+		if asyncErr := api.UpdateAsyncStatus(db, api.AsyncFailed, "Requested configuration update has been denied.", asyncId, true); asyncErr != nil {
+			log.Errorf("updating async status for id %v: %v", asyncId, asyncErr)

Review comment:
       nit: could use stricter formatting rather than `%v`

##########
File path: traffic_ops/traffic_ops_golang/cdni/shared.go
##########
@@ -68,17 +82,388 @@ func GetCapabilities(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
+	ucdn, err := checkBearerToken(bearerToken, inf)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, err)
+		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 PutHostConfiguration(w http.ResponseWriter, r *http.Request) {
+	inf, userErr, sysErr, errCode := api.NewInfo(r, []string{"host"}, nil)
+	if userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr)
+		return
+	}
+	defer inf.Close()
+
+	host := inf.Params["host"]
+	if errCode, userErr, sysErr := validateHostExists(host, inf.Tx.Tx); userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr)
+		return
+	}
+
+	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
+	}
+
+	ucdn, err := checkBearerToken(bearerToken, inf)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, err)
+		return
+	}
+
+	var genericHostRequest GenericHostMetadata
+	err = json.NewDecoder(r.Body).Decode(&genericHostRequest)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("decoding host json request: %w", err))
+		return
+	}
+
+	db, err := api.GetDB(r.Context())
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("getting async db: %w", err))
+		return
+	}
+	asyncTx, err := db.Begin()
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("getting async tx: %w", err))
+		return
+	}
+
+	asyncStatusId, errCode, userErr, sysErr := api.InsertAsyncStatus(asyncTx, "CDNi host configuration update request received.")
+	if userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr)
+		return
+	}
+
+	//var typeToValueMap map[string]GenericRequestedLimits
+	//for _, hostUpdate := range genericHostRequest.HostMetadata.Metadata {
+	//	typeToValueMap[hostUpdate.Type] = hostUpdate.Value
+	//}
+
+	data := genericHostRequest.HostMetadata.Metadata
+
+	_, err = inf.Tx.Tx.Query(InsertCapabilityUpdateQuery, ucdn, data, asyncStatusId, hostConfigLabel, host)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("inserting capability update request into queue: %w", err))
+		return
+	}
+
+	var alerts tc.Alerts
+	alerts.AddAlert(tc.Alert{
+		Text:  "CDNi configuration update request received. Status updates can be found here: " + api.CurrentAsyncEndpoint + strconv.Itoa(asyncStatusId),
+		Level: tc.SuccessLevel.String(),
+	})
+
+	w.Header().Add("Location", api.CurrentAsyncEndpoint+strconv.Itoa(asyncStatusId))
+	api.WriteAlerts(w, r, http.StatusAccepted, alerts)
+}
+
+func PutConfiguration(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
+	}
+
+	ucdn, err := checkBearerToken(bearerToken, inf)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, err)
+		return
+	}
+
+	var genericRequest GenericRequestMetadata
+	err = json.NewDecoder(r.Body).Decode(&genericRequest)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("decoding json request: %w", err))
+		return
+	}
+
+	db, err := api.GetDB(r.Context())
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("getting async db: %w", err))
+		return
+	}
+	asyncTx, err := db.Begin()
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("getting async tx: %w", err))
+		return
+	}
+
+	asyncStatusId, errCode, userErr, sysErr := api.InsertAsyncStatus(asyncTx, "CDNi configuration update request received.")
+	if userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr)
+		return
+	}
+
+	data := genericRequest.Metadata
+
+	_, err = inf.Tx.Tx.Query(InsertCapabilityUpdateQuery, ucdn, data, asyncStatusId, SupportedGenericMetadataType(genericRequest.Type), genericRequest.Host)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("inserting capability update request into queue: %w", err))
+		return
+	}
+
+	var alerts tc.Alerts
+	alerts.AddAlert(tc.Alert{
+		Text:  "CDNi configuration update request received. Status updates can be found here: " + api.CurrentAsyncEndpoint + strconv.Itoa(asyncStatusId),
+		Level: tc.SuccessLevel.String(),
+	})
+
+	w.Header().Add("Location", api.CurrentAsyncEndpoint+strconv.Itoa(asyncStatusId))
+	api.WriteAlerts(w, r, http.StatusAccepted, alerts)
+}
+
+func PutConfigurationResponse(w http.ResponseWriter, r *http.Request) {
+	inf, userErr, sysErr, errCode := api.NewInfo(r, []string{"approved", "id"}, []string{"id"})
+	if userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr)
+		return
+	}
+	defer inf.Close()
+
+	reqId := inf.IntParams["id"]
+	approvedString := inf.Params["approved"]
+	approved, err := strconv.ParseBool(approvedString)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusBadRequest, errors.New("approved parameter must be a boolean"), nil)
+		return
+	}
+
+	db, err := api.GetDB(r.Context())
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("getting async db: %w", err))
+		return
+	}
+
+	rows, err := inf.Tx.Tx.Query(SelectCapabilityUpdateQuery, reqId)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("querying for capability update request: %w", err))
+		return
+	}
+	defer rows.Close()
+	var ucdn string
+	var data json.RawMessage
+	var host string
+	var asyncId int
+	var requestType string
+	count := 0
+	for rows.Next() {
+		if err := rows.Scan(&ucdn, &data, &asyncId, &requestType, &host); err != nil {
+			api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("scanning db rows: %w", err))
+			return
+		}
+		count++
+	}
+	if count == 0 {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusNotFound, fmt.Errorf("no configuration request for that id"), nil)
+		return
+	}
+
+	if !approved {
+		if asyncErr := api.UpdateAsyncStatus(db, api.AsyncFailed, "Requested configuration update has been denied.", asyncId, true); asyncErr != nil {
+			log.Errorf("updating async status for id %v: %v", asyncId, asyncErr)
+		}
+		status, err := deleteCapabilityRequest(reqId, inf.Tx.Tx)
+		if err != nil {
+			api.HandleErr(w, r, inf.Tx.Tx, status, nil, fmt.Errorf("deleting configuration request from queue: %w", err))
+			return
+		}
+		api.WriteResp(w, r, "Successfully denied configuration update request.")
+		return
+	}
+
+	var updatedDataList []GenericMetadata
+	if err = json.Unmarshal(data, &updatedDataList); err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("unmarshalling data for configuration update: %w", err))
+		return
+	}
+
+	var unsupportedTypes []string
+	for _, updatedData := range updatedDataList {
+		if !updatedData.Type.isValid() {
+			unsupportedTypes = append(unsupportedTypes, string(updatedData.Type))
+		}
+	}
+
+	if len(unsupportedTypes) != 0 {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusBadRequest, fmt.Errorf("unsupported generic metadata types found: %v", strings.Join(unsupportedTypes, ", ")), nil)
+		return
+	}
+
+	for _, updatedData := range updatedDataList {
+		switch updatedData.Type {
+		case MiRequestedCapacityLimits:
+			var capacityRequestedLimits CapacityRequestedLimits
+			if err = json.Unmarshal(updatedData.Value, &capacityRequestedLimits); err != nil {
+				api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("unmarshalling data for configuration update: %w", err))
+				return
+			}
+			for _, capLim := range capacityRequestedLimits.RequestedLimits {
+				capId, err := getCapabilityIdFromFootprints(capLim, ucdn, inf)
+				if err != nil {
+					api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("finding capability for given information: %w", err))
+					return
+				}
+
+				query := UpdateTotalLimitsByCapabilityAndLimitTypeQuery
+				queryParams := []interface{}{capLim.LimitValue, capId, capLim.LimitType}
+				if host != "" {
+					query = UpdateHostLimitsByCapabilityAndLimitTypeQuery
+					queryParams = []interface{}{capLim.LimitValue, capId, capLim.LimitType, host}
+				}
+
+				result, err := inf.Tx.Tx.Exec(query, queryParams...)
+				if err != nil {
+					api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("updating capacity: %w", err))
+					return
+				}
+
+				if rowsAffected, err := result.RowsAffected(); err != nil {
+					api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("updating capacity: getting rows affected: %w", err))
+					return
+				} else if rowsAffected < 1 {
+					api.HandleErr(w, r, inf.Tx.Tx, http.StatusNotFound, fmt.Errorf("no capacity found for update: host: %s, type: %s, limit: %v", host, updatedData.Type, capLim), nil)
+					return
+				} else if rowsAffected > 1 {
+					api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("capacity update affected too many rows: %d", rowsAffected))
+					return
+				}
+			}
+		}
+	}
+
+	if asyncErr := api.UpdateAsyncStatus(db, api.AsyncSucceeded, "Capacity requested update has been completed.", asyncId, true); asyncErr != nil {
+		log.Errorf("updating async status for id %v: %v", asyncId, asyncErr)
+	}
+	status, err := deleteCapabilityRequest(reqId, inf.Tx.Tx)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, status, nil, fmt.Errorf("deleting capacity request from queue: %w", err))
+		return
+	}
+	api.WriteResp(w, r, "Successfully updated configuration.")
+}
+
+func getCapabilityIdFromFootprints(updatedData CapacityLimit, ucdn string, inf *api.APIInfo) (int, error) {
+	var potentialCapabilityIds []int
+	for _, footprint := range updatedData.Footprints {
+		selectQuery := `SELECT footprint_type, footprint_value, capability_id FROM cdni_footprints WHERE ucdn = $1 AND footprint_type = $2 AND footprint_value = `
+		valuesAsQuery := "ARRAY['" + strings.Join(footprint.FootprintValue, "','") + "']"
+		rows, err := inf.Tx.Tx.Query(selectQuery+valuesAsQuery, ucdn, footprint.FootprintType)
+		if err != nil {
+			return 0, fmt.Errorf("querying for capacity update request: %w", err)
+		}
+		defer rows.Close()
+		var capabilityIds []int
+		rowCount := 0
+		for rows.Next() {
+			var capabilityId int
+			var footprint Footprint
+			if err := rows.Scan(&footprint.FootprintType, pq.Array(&footprint.FootprintValue), &capabilityId); err != nil {
+				return 0, fmt.Errorf("scanning db rows: %w", err)
+			}
+			rowCount++
+			capabilityIds = append(capabilityIds, capabilityId)
+		}
+
+		if rowCount == 0 {
+			return 0, fmt.Errorf("no capabilities found for footprint: %v, %v", footprint.FootprintType, footprint.FootprintValue)
+		}
+		var newPotentials []int
+		for _, capId := range capabilityIds {
+			if len(potentialCapabilityIds) == 0 {
+				newPotentials = append(newPotentials, capId)
+			} else {
+				if util.IntInArray(potentialCapabilityIds, capId) {
+					newPotentials = append(newPotentials, capId)
+				}
+			}
+		}
+		if len(newPotentials) == 0 {
+			return 0, fmt.Errorf("no capabilities found that match all footprints: %v", updatedData.Footprints)
+		}
+		potentialCapabilityIds = newPotentials

Review comment:
       I'm a little confused by this code. Can you explain what it's doing/checking in terms of capabilities and footprints? 




-- 
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 #6557: Added CDNi ability to negotiate higher capacities

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



##########
File path: traffic_ops/traffic_ops_golang/cdni/shared.go
##########
@@ -68,17 +82,388 @@ func GetCapabilities(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
+	ucdn, err := checkBearerToken(bearerToken, inf)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, err)
+		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 PutHostConfiguration(w http.ResponseWriter, r *http.Request) {
+	inf, userErr, sysErr, errCode := api.NewInfo(r, []string{"host"}, nil)
+	if userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr)
+		return
+	}
+	defer inf.Close()
+
+	host := inf.Params["host"]
+	if errCode, userErr, sysErr := validateHostExists(host, inf.Tx.Tx); userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr)
+		return
+	}
+
+	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
+	}
+
+	ucdn, err := checkBearerToken(bearerToken, inf)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, err)
+		return
+	}
+
+	var genericHostRequest GenericHostMetadata
+	err = json.NewDecoder(r.Body).Decode(&genericHostRequest)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("decoding host json request: %w", err))
+		return
+	}
+
+	db, err := api.GetDB(r.Context())
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("getting async db: %w", err))
+		return
+	}
+	asyncTx, err := db.Begin()
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("getting async tx: %w", err))
+		return
+	}
+
+	asyncStatusId, errCode, userErr, sysErr := api.InsertAsyncStatus(asyncTx, "CDNi host configuration update request received.")
+	if userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr)
+		return
+	}
+
+	//var typeToValueMap map[string]GenericRequestedLimits
+	//for _, hostUpdate := range genericHostRequest.HostMetadata.Metadata {
+	//	typeToValueMap[hostUpdate.Type] = hostUpdate.Value
+	//}
+
+	data := genericHostRequest.HostMetadata.Metadata
+
+	_, err = inf.Tx.Tx.Query(InsertCapabilityUpdateQuery, ucdn, data, asyncStatusId, hostConfigLabel, host)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("inserting capability update request into queue: %w", err))
+		return
+	}
+
+	var alerts tc.Alerts
+	alerts.AddAlert(tc.Alert{
+		Text:  "CDNi configuration update request received. Status updates can be found here: " + api.CurrentAsyncEndpoint + strconv.Itoa(asyncStatusId),
+		Level: tc.SuccessLevel.String(),
+	})
+
+	w.Header().Add("Location", api.CurrentAsyncEndpoint+strconv.Itoa(asyncStatusId))
+	api.WriteAlerts(w, r, http.StatusAccepted, alerts)
+}
+
+func PutConfiguration(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
+	}
+
+	ucdn, err := checkBearerToken(bearerToken, inf)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, err)
+		return
+	}
+
+	var genericRequest GenericRequestMetadata
+	err = json.NewDecoder(r.Body).Decode(&genericRequest)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("decoding json request: %w", err))
+		return
+	}
+
+	db, err := api.GetDB(r.Context())
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("getting async db: %w", err))
+		return
+	}
+	asyncTx, err := db.Begin()
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("getting async tx: %w", err))
+		return
+	}
+
+	asyncStatusId, errCode, userErr, sysErr := api.InsertAsyncStatus(asyncTx, "CDNi configuration update request received.")
+	if userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr)
+		return
+	}
+
+	data := genericRequest.Metadata
+
+	_, err = inf.Tx.Tx.Query(InsertCapabilityUpdateQuery, ucdn, data, asyncStatusId, SupportedGenericMetadataType(genericRequest.Type), genericRequest.Host)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("inserting capability update request into queue: %w", err))
+		return
+	}
+
+	var alerts tc.Alerts
+	alerts.AddAlert(tc.Alert{
+		Text:  "CDNi configuration update request received. Status updates can be found here: " + api.CurrentAsyncEndpoint + strconv.Itoa(asyncStatusId),
+		Level: tc.SuccessLevel.String(),
+	})
+
+	w.Header().Add("Location", api.CurrentAsyncEndpoint+strconv.Itoa(asyncStatusId))
+	api.WriteAlerts(w, r, http.StatusAccepted, alerts)
+}
+
+func PutConfigurationResponse(w http.ResponseWriter, r *http.Request) {
+	inf, userErr, sysErr, errCode := api.NewInfo(r, []string{"approved", "id"}, []string{"id"})
+	if userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr)
+		return
+	}
+	defer inf.Close()
+
+	reqId := inf.IntParams["id"]
+	approvedString := inf.Params["approved"]
+	approved, err := strconv.ParseBool(approvedString)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusBadRequest, errors.New("approved parameter must be a boolean"), nil)
+		return
+	}
+
+	db, err := api.GetDB(r.Context())
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("getting async db: %w", err))
+		return
+	}
+
+	rows, err := inf.Tx.Tx.Query(SelectCapabilityUpdateQuery, reqId)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("querying for capability update request: %w", err))
+		return
+	}
+	defer rows.Close()
+	var ucdn string
+	var data json.RawMessage
+	var host string
+	var asyncId int
+	var requestType string
+	count := 0
+	for rows.Next() {
+		if err := rows.Scan(&ucdn, &data, &asyncId, &requestType, &host); err != nil {
+			api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("scanning db rows: %w", err))
+			return
+		}
+		count++
+	}
+	if count == 0 {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusNotFound, fmt.Errorf("no configuration request for that id"), nil)
+		return
+	}
+
+	if !approved {
+		if asyncErr := api.UpdateAsyncStatus(db, api.AsyncFailed, "Requested configuration update has been denied.", asyncId, true); asyncErr != nil {
+			log.Errorf("updating async status for id %v: %v", asyncId, asyncErr)
+		}
+		status, err := deleteCapabilityRequest(reqId, inf.Tx.Tx)
+		if err != nil {
+			api.HandleErr(w, r, inf.Tx.Tx, status, nil, fmt.Errorf("deleting configuration request from queue: %w", err))
+			return
+		}
+		api.WriteResp(w, r, "Successfully denied configuration update request.")
+		return
+	}
+
+	var updatedDataList []GenericMetadata
+	if err = json.Unmarshal(data, &updatedDataList); err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("unmarshalling data for configuration update: %w", err))
+		return
+	}
+
+	var unsupportedTypes []string
+	for _, updatedData := range updatedDataList {
+		if !updatedData.Type.isValid() {
+			unsupportedTypes = append(unsupportedTypes, string(updatedData.Type))
+		}
+	}
+
+	if len(unsupportedTypes) != 0 {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusBadRequest, fmt.Errorf("unsupported generic metadata types found: %v", strings.Join(unsupportedTypes, ", ")), nil)
+		return
+	}
+
+	for _, updatedData := range updatedDataList {
+		switch updatedData.Type {
+		case MiRequestedCapacityLimits:
+			var capacityRequestedLimits CapacityRequestedLimits
+			if err = json.Unmarshal(updatedData.Value, &capacityRequestedLimits); err != nil {
+				api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("unmarshalling data for configuration update: %w", err))
+				return
+			}
+			for _, capLim := range capacityRequestedLimits.RequestedLimits {
+				capId, err := getCapabilityIdFromFootprints(capLim, ucdn, inf)
+				if err != nil {
+					api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("finding capability for given information: %w", err))
+					return
+				}
+
+				query := UpdateTotalLimitsByCapabilityAndLimitTypeQuery
+				queryParams := []interface{}{capLim.LimitValue, capId, capLim.LimitType}
+				if host != "" {
+					query = UpdateHostLimitsByCapabilityAndLimitTypeQuery
+					queryParams = []interface{}{capLim.LimitValue, capId, capLim.LimitType, host}
+				}
+
+				result, err := inf.Tx.Tx.Exec(query, queryParams...)
+				if err != nil {
+					api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("updating capacity: %w", err))
+					return
+				}
+
+				if rowsAffected, err := result.RowsAffected(); err != nil {
+					api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("updating capacity: getting rows affected: %w", err))
+					return
+				} else if rowsAffected < 1 {
+					api.HandleErr(w, r, inf.Tx.Tx, http.StatusNotFound, fmt.Errorf("no capacity found for update: host: %s, type: %s, limit: %v", host, updatedData.Type, capLim), nil)
+					return
+				} else if rowsAffected > 1 {
+					api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("capacity update affected too many rows: %d", rowsAffected))
+					return
+				}
+			}
+		}
+	}
+
+	if asyncErr := api.UpdateAsyncStatus(db, api.AsyncSucceeded, "Capacity requested update has been completed.", asyncId, true); asyncErr != nil {
+		log.Errorf("updating async status for id %v: %v", asyncId, asyncErr)
+	}
+	status, err := deleteCapabilityRequest(reqId, inf.Tx.Tx)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, status, nil, fmt.Errorf("deleting capacity request from queue: %w", err))
+		return
+	}
+	api.WriteResp(w, r, "Successfully updated configuration.")
+}
+
+func getCapabilityIdFromFootprints(updatedData CapacityLimit, ucdn string, inf *api.APIInfo) (int, error) {
+	var potentialCapabilityIds []int
+	for _, footprint := range updatedData.Footprints {
+		selectQuery := `SELECT footprint_type, footprint_value, capability_id FROM cdni_footprints WHERE ucdn = $1 AND footprint_type = $2 AND footprint_value = `
+		valuesAsQuery := "ARRAY['" + strings.Join(footprint.FootprintValue, "','") + "']"
+		rows, err := inf.Tx.Tx.Query(selectQuery+valuesAsQuery, ucdn, footprint.FootprintType)
+		if err != nil {
+			return 0, fmt.Errorf("querying for capacity update request: %w", err)
+		}
+		defer rows.Close()
+		var capabilityIds []int
+		rowCount := 0
+		for rows.Next() {
+			var capabilityId int
+			var footprint Footprint
+			if err := rows.Scan(&footprint.FootprintType, pq.Array(&footprint.FootprintValue), &capabilityId); err != nil {
+				return 0, fmt.Errorf("scanning db rows: %w", err)
+			}
+			rowCount++
+			capabilityIds = append(capabilityIds, capabilityId)
+		}
+
+		if rowCount == 0 {
+			return 0, fmt.Errorf("no capabilities found for footprint: %v, %v", footprint.FootprintType, footprint.FootprintValue)
+		}
+		var newPotentials []int
+		for _, capId := range capabilityIds {
+			if len(potentialCapabilityIds) == 0 {
+				newPotentials = append(newPotentials, capId)
+			} else {
+				if util.IntInArray(potentialCapabilityIds, capId) {
+					newPotentials = append(newPotentials, capId)
+				}
+			}
+		}
+		if len(newPotentials) == 0 {
+			return 0, fmt.Errorf("no capabilities found that match all footprints: %v", updatedData.Footprints)
+		}
+		potentialCapabilityIds = newPotentials

Review comment:
       yea its super confusing and also im very open to better ways to do this if you have any. 
   
   for this, the footprints can overlap and can be linked to multiple capabilities in the long run. right now this is just for Capacity but there will be a lot more as i keep going. plus each capability can have multiple footprints. so for this code im looping through the footprints that have the correct uCDN, type, and value based on the request, then making a list of potential capability ids based on all of the footprints that came linked to the info in the request.  
   
   this way if you send a request linked to footprints like `[ {footprint-type: ipv4cidr", "footprint-value": ["127.0.0.1", "127.0.0.2" ] }, {footprint-type: countrycode", "footprint-value": ["us"] } ]` then it wont get capabilities linke to just the countrycode footprint or just the ipv4cidr footprint or other capabilities linked to those plus more.
   
   does that make any sense?




-- 
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 #6557: Added CDNi ability to negotiate higher capacities

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



##########
File path: traffic_ops/traffic_ops_golang/cdni/shared.go
##########
@@ -68,17 +82,388 @@ func GetCapabilities(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
+	ucdn, err := checkBearerToken(bearerToken, inf)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, err)
+		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 PutHostConfiguration(w http.ResponseWriter, r *http.Request) {
+	inf, userErr, sysErr, errCode := api.NewInfo(r, []string{"host"}, nil)
+	if userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr)
+		return
+	}
+	defer inf.Close()
+
+	host := inf.Params["host"]
+	if errCode, userErr, sysErr := validateHostExists(host, inf.Tx.Tx); userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr)
+		return
+	}
+
+	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
+	}
+
+	ucdn, err := checkBearerToken(bearerToken, inf)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, err)
+		return
+	}
+
+	var genericHostRequest GenericHostMetadata
+	err = json.NewDecoder(r.Body).Decode(&genericHostRequest)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("decoding host json request: %w", err))
+		return
+	}
+
+	db, err := api.GetDB(r.Context())
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("getting async db: %w", err))
+		return
+	}
+	asyncTx, err := db.Begin()
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("getting async tx: %w", err))
+		return
+	}
+
+	asyncStatusId, errCode, userErr, sysErr := api.InsertAsyncStatus(asyncTx, "CDNi host configuration update request received.")
+	if userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr)
+		return
+	}
+
+	//var typeToValueMap map[string]GenericRequestedLimits
+	//for _, hostUpdate := range genericHostRequest.HostMetadata.Metadata {
+	//	typeToValueMap[hostUpdate.Type] = hostUpdate.Value
+	//}
+
+	data := genericHostRequest.HostMetadata.Metadata
+
+	_, err = inf.Tx.Tx.Query(InsertCapabilityUpdateQuery, ucdn, data, asyncStatusId, hostConfigLabel, host)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("inserting capability update request into queue: %w", err))
+		return
+	}
+
+	var alerts tc.Alerts
+	alerts.AddAlert(tc.Alert{
+		Text:  "CDNi configuration update request received. Status updates can be found here: " + api.CurrentAsyncEndpoint + strconv.Itoa(asyncStatusId),
+		Level: tc.SuccessLevel.String(),
+	})
+
+	w.Header().Add("Location", api.CurrentAsyncEndpoint+strconv.Itoa(asyncStatusId))
+	api.WriteAlerts(w, r, http.StatusAccepted, alerts)
+}
+
+func PutConfiguration(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
+	}
+
+	ucdn, err := checkBearerToken(bearerToken, inf)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, err)
+		return
+	}
+
+	var genericRequest GenericRequestMetadata
+	err = json.NewDecoder(r.Body).Decode(&genericRequest)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("decoding json request: %w", err))
+		return
+	}
+
+	db, err := api.GetDB(r.Context())
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("getting async db: %w", err))
+		return
+	}
+	asyncTx, err := db.Begin()
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("getting async tx: %w", err))
+		return
+	}
+
+	asyncStatusId, errCode, userErr, sysErr := api.InsertAsyncStatus(asyncTx, "CDNi configuration update request received.")
+	if userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr)
+		return
+	}
+
+	data := genericRequest.Metadata
+
+	_, err = inf.Tx.Tx.Query(InsertCapabilityUpdateQuery, ucdn, data, asyncStatusId, SupportedGenericMetadataType(genericRequest.Type), genericRequest.Host)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("inserting capability update request into queue: %w", err))
+		return
+	}
+
+	var alerts tc.Alerts
+	alerts.AddAlert(tc.Alert{
+		Text:  "CDNi configuration update request received. Status updates can be found here: " + api.CurrentAsyncEndpoint + strconv.Itoa(asyncStatusId),
+		Level: tc.SuccessLevel.String(),
+	})
+
+	w.Header().Add("Location", api.CurrentAsyncEndpoint+strconv.Itoa(asyncStatusId))
+	api.WriteAlerts(w, r, http.StatusAccepted, alerts)
+}
+
+func PutConfigurationResponse(w http.ResponseWriter, r *http.Request) {
+	inf, userErr, sysErr, errCode := api.NewInfo(r, []string{"approved", "id"}, []string{"id"})
+	if userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr)
+		return
+	}
+	defer inf.Close()
+
+	reqId := inf.IntParams["id"]
+	approvedString := inf.Params["approved"]
+	approved, err := strconv.ParseBool(approvedString)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusBadRequest, errors.New("approved parameter must be a boolean"), nil)
+		return
+	}
+
+	db, err := api.GetDB(r.Context())
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("getting async db: %w", err))
+		return
+	}
+
+	rows, err := inf.Tx.Tx.Query(SelectCapabilityUpdateQuery, reqId)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("querying for capability update request: %w", err))
+		return
+	}
+	defer rows.Close()
+	var ucdn string
+	var data json.RawMessage
+	var host string
+	var asyncId int
+	var requestType string
+	count := 0
+	for rows.Next() {
+		if err := rows.Scan(&ucdn, &data, &asyncId, &requestType, &host); err != nil {
+			api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("scanning db rows: %w", err))
+			return
+		}
+		count++
+	}
+	if count == 0 {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusNotFound, fmt.Errorf("no configuration request for that id"), nil)
+		return
+	}
+
+	if !approved {
+		if asyncErr := api.UpdateAsyncStatus(db, api.AsyncFailed, "Requested configuration update has been denied.", asyncId, true); asyncErr != nil {
+			log.Errorf("updating async status for id %v: %v", asyncId, asyncErr)
+		}
+		status, err := deleteCapabilityRequest(reqId, inf.Tx.Tx)
+		if err != nil {
+			api.HandleErr(w, r, inf.Tx.Tx, status, nil, fmt.Errorf("deleting configuration request from queue: %w", err))
+			return
+		}
+		api.WriteResp(w, r, "Successfully denied configuration update request.")
+		return
+	}
+
+	var updatedDataList []GenericMetadata
+	if err = json.Unmarshal(data, &updatedDataList); err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("unmarshalling data for configuration update: %w", err))
+		return
+	}
+
+	var unsupportedTypes []string
+	for _, updatedData := range updatedDataList {
+		if !updatedData.Type.isValid() {
+			unsupportedTypes = append(unsupportedTypes, string(updatedData.Type))
+		}
+	}
+
+	if len(unsupportedTypes) != 0 {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusBadRequest, fmt.Errorf("unsupported generic metadata types found: %v", strings.Join(unsupportedTypes, ", ")), nil)
+		return
+	}
+
+	for _, updatedData := range updatedDataList {
+		switch updatedData.Type {
+		case MiRequestedCapacityLimits:
+			var capacityRequestedLimits CapacityRequestedLimits
+			if err = json.Unmarshal(updatedData.Value, &capacityRequestedLimits); err != nil {
+				api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("unmarshalling data for configuration update: %w", err))
+				return
+			}
+			for _, capLim := range capacityRequestedLimits.RequestedLimits {
+				capId, err := getCapabilityIdFromFootprints(capLim, ucdn, inf)
+				if err != nil {
+					api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("finding capability for given information: %w", err))
+					return
+				}
+
+				query := UpdateTotalLimitsByCapabilityAndLimitTypeQuery
+				queryParams := []interface{}{capLim.LimitValue, capId, capLim.LimitType}
+				if host != "" {
+					query = UpdateHostLimitsByCapabilityAndLimitTypeQuery
+					queryParams = []interface{}{capLim.LimitValue, capId, capLim.LimitType, host}
+				}
+
+				result, err := inf.Tx.Tx.Exec(query, queryParams...)
+				if err != nil {
+					api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("updating capacity: %w", err))
+					return
+				}
+
+				if rowsAffected, err := result.RowsAffected(); err != nil {
+					api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("updating capacity: getting rows affected: %w", err))
+					return
+				} else if rowsAffected < 1 {
+					api.HandleErr(w, r, inf.Tx.Tx, http.StatusNotFound, fmt.Errorf("no capacity found for update: host: %s, type: %s, limit: %v", host, updatedData.Type, capLim), nil)
+					return
+				} else if rowsAffected > 1 {
+					api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("capacity update affected too many rows: %d", rowsAffected))
+					return
+				}
+			}
+		}
+	}
+
+	if asyncErr := api.UpdateAsyncStatus(db, api.AsyncSucceeded, "Capacity requested update has been completed.", asyncId, true); asyncErr != nil {
+		log.Errorf("updating async status for id %v: %v", asyncId, asyncErr)
+	}
+	status, err := deleteCapabilityRequest(reqId, inf.Tx.Tx)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, status, nil, fmt.Errorf("deleting capacity request from queue: %w", err))
+		return
+	}
+	api.WriteResp(w, r, "Successfully updated configuration.")
+}
+
+func getCapabilityIdFromFootprints(updatedData CapacityLimit, ucdn string, inf *api.APIInfo) (int, error) {
+	var potentialCapabilityIds []int
+	for _, footprint := range updatedData.Footprints {
+		selectQuery := `SELECT footprint_type, footprint_value, capability_id FROM cdni_footprints WHERE ucdn = $1 AND footprint_type = $2 AND footprint_value = `
+		valuesAsQuery := "ARRAY['" + strings.Join(footprint.FootprintValue, "','") + "']"

Review comment:
       this does work! nice! 




-- 
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 #6557: Added CDNi ability to negotiate higher capacities

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



##########
File path: traffic_ops/traffic_ops_golang/cdni/shared.go
##########
@@ -68,17 +82,388 @@ func GetCapabilities(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
+	ucdn, err := checkBearerToken(bearerToken, inf)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, err)
+		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 PutHostConfiguration(w http.ResponseWriter, r *http.Request) {
+	inf, userErr, sysErr, errCode := api.NewInfo(r, []string{"host"}, nil)
+	if userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr)
+		return
+	}
+	defer inf.Close()
+
+	host := inf.Params["host"]
+	if errCode, userErr, sysErr := validateHostExists(host, inf.Tx.Tx); userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr)
+		return
+	}
+
+	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
+	}
+
+	ucdn, err := checkBearerToken(bearerToken, inf)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, err)
+		return
+	}
+
+	var genericHostRequest GenericHostMetadata
+	err = json.NewDecoder(r.Body).Decode(&genericHostRequest)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("decoding host json request: %w", err))
+		return
+	}
+
+	db, err := api.GetDB(r.Context())
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("getting async db: %w", err))
+		return
+	}
+	asyncTx, err := db.Begin()
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("getting async tx: %w", err))
+		return
+	}
+
+	asyncStatusId, errCode, userErr, sysErr := api.InsertAsyncStatus(asyncTx, "CDNi host configuration update request received.")
+	if userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr)
+		return
+	}
+
+	//var typeToValueMap map[string]GenericRequestedLimits
+	//for _, hostUpdate := range genericHostRequest.HostMetadata.Metadata {
+	//	typeToValueMap[hostUpdate.Type] = hostUpdate.Value
+	//}
+
+	data := genericHostRequest.HostMetadata.Metadata
+
+	_, err = inf.Tx.Tx.Query(InsertCapabilityUpdateQuery, ucdn, data, asyncStatusId, hostConfigLabel, host)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("inserting capability update request into queue: %w", err))
+		return
+	}
+
+	var alerts tc.Alerts
+	alerts.AddAlert(tc.Alert{
+		Text:  "CDNi configuration update request received. Status updates can be found here: " + api.CurrentAsyncEndpoint + strconv.Itoa(asyncStatusId),
+		Level: tc.SuccessLevel.String(),
+	})
+
+	w.Header().Add("Location", api.CurrentAsyncEndpoint+strconv.Itoa(asyncStatusId))
+	api.WriteAlerts(w, r, http.StatusAccepted, alerts)
+}
+
+func PutConfiguration(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
+	}
+
+	ucdn, err := checkBearerToken(bearerToken, inf)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, err)
+		return
+	}
+
+	var genericRequest GenericRequestMetadata
+	err = json.NewDecoder(r.Body).Decode(&genericRequest)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("decoding json request: %w", err))
+		return
+	}
+
+	db, err := api.GetDB(r.Context())
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("getting async db: %w", err))
+		return
+	}
+	asyncTx, err := db.Begin()
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("getting async tx: %w", err))
+		return
+	}
+
+	asyncStatusId, errCode, userErr, sysErr := api.InsertAsyncStatus(asyncTx, "CDNi configuration update request received.")
+	if userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr)
+		return
+	}
+
+	data := genericRequest.Metadata
+
+	_, err = inf.Tx.Tx.Query(InsertCapabilityUpdateQuery, ucdn, data, asyncStatusId, SupportedGenericMetadataType(genericRequest.Type), genericRequest.Host)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("inserting capability update request into queue: %w", err))
+		return
+	}
+
+	var alerts tc.Alerts
+	alerts.AddAlert(tc.Alert{
+		Text:  "CDNi configuration update request received. Status updates can be found here: " + api.CurrentAsyncEndpoint + strconv.Itoa(asyncStatusId),
+		Level: tc.SuccessLevel.String(),
+	})
+
+	w.Header().Add("Location", api.CurrentAsyncEndpoint+strconv.Itoa(asyncStatusId))
+	api.WriteAlerts(w, r, http.StatusAccepted, alerts)
+}
+
+func PutConfigurationResponse(w http.ResponseWriter, r *http.Request) {
+	inf, userErr, sysErr, errCode := api.NewInfo(r, []string{"approved", "id"}, []string{"id"})
+	if userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr)
+		return
+	}
+	defer inf.Close()
+
+	reqId := inf.IntParams["id"]
+	approvedString := inf.Params["approved"]
+	approved, err := strconv.ParseBool(approvedString)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusBadRequest, errors.New("approved parameter must be a boolean"), nil)
+		return
+	}
+
+	db, err := api.GetDB(r.Context())
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("getting async db: %w", err))
+		return
+	}
+
+	rows, err := inf.Tx.Tx.Query(SelectCapabilityUpdateQuery, reqId)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("querying for capability update request: %w", err))
+		return
+	}
+	defer rows.Close()
+	var ucdn string
+	var data json.RawMessage
+	var host string
+	var asyncId int
+	var requestType string
+	count := 0
+	for rows.Next() {
+		if err := rows.Scan(&ucdn, &data, &asyncId, &requestType, &host); err != nil {
+			api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("scanning db rows: %w", err))
+			return
+		}
+		count++
+	}
+	if count == 0 {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusNotFound, fmt.Errorf("no configuration request for that id"), nil)
+		return
+	}
+
+	if !approved {
+		if asyncErr := api.UpdateAsyncStatus(db, api.AsyncFailed, "Requested configuration update has been denied.", asyncId, true); asyncErr != nil {
+			log.Errorf("updating async status for id %v: %v", asyncId, asyncErr)
+		}
+		status, err := deleteCapabilityRequest(reqId, inf.Tx.Tx)
+		if err != nil {
+			api.HandleErr(w, r, inf.Tx.Tx, status, nil, fmt.Errorf("deleting configuration request from queue: %w", err))
+			return
+		}
+		api.WriteResp(w, r, "Successfully denied configuration update request.")
+		return
+	}
+
+	var updatedDataList []GenericMetadata
+	if err = json.Unmarshal(data, &updatedDataList); err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("unmarshalling data for configuration update: %w", err))
+		return
+	}
+
+	var unsupportedTypes []string
+	for _, updatedData := range updatedDataList {
+		if !updatedData.Type.isValid() {
+			unsupportedTypes = append(unsupportedTypes, string(updatedData.Type))
+		}
+	}
+
+	if len(unsupportedTypes) != 0 {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusBadRequest, fmt.Errorf("unsupported generic metadata types found: %v", strings.Join(unsupportedTypes, ", ")), nil)
+		return
+	}
+
+	for _, updatedData := range updatedDataList {
+		switch updatedData.Type {
+		case MiRequestedCapacityLimits:
+			var capacityRequestedLimits CapacityRequestedLimits
+			if err = json.Unmarshal(updatedData.Value, &capacityRequestedLimits); err != nil {
+				api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("unmarshalling data for configuration update: %w", err))
+				return
+			}
+			for _, capLim := range capacityRequestedLimits.RequestedLimits {
+				capId, err := getCapabilityIdFromFootprints(capLim, ucdn, inf)
+				if err != nil {
+					api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("finding capability for given information: %w", err))
+					return
+				}
+
+				query := UpdateTotalLimitsByCapabilityAndLimitTypeQuery
+				queryParams := []interface{}{capLim.LimitValue, capId, capLim.LimitType}
+				if host != "" {
+					query = UpdateHostLimitsByCapabilityAndLimitTypeQuery
+					queryParams = []interface{}{capLim.LimitValue, capId, capLim.LimitType, host}
+				}
+
+				result, err := inf.Tx.Tx.Exec(query, queryParams...)
+				if err != nil {
+					api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("updating capacity: %w", err))
+					return
+				}
+
+				if rowsAffected, err := result.RowsAffected(); err != nil {
+					api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("updating capacity: getting rows affected: %w", err))
+					return
+				} else if rowsAffected < 1 {
+					api.HandleErr(w, r, inf.Tx.Tx, http.StatusNotFound, fmt.Errorf("no capacity found for update: host: %s, type: %s, limit: %v", host, updatedData.Type, capLim), nil)
+					return
+				} else if rowsAffected > 1 {
+					api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("capacity update affected too many rows: %d", rowsAffected))
+					return
+				}
+			}
+		}
+	}
+
+	if asyncErr := api.UpdateAsyncStatus(db, api.AsyncSucceeded, "Capacity requested update has been completed.", asyncId, true); asyncErr != nil {
+		log.Errorf("updating async status for id %v: %v", asyncId, asyncErr)
+	}
+	status, err := deleteCapabilityRequest(reqId, inf.Tx.Tx)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, status, nil, fmt.Errorf("deleting capacity request from queue: %w", err))
+		return
+	}
+	api.WriteResp(w, r, "Successfully updated configuration.")
+}
+
+func getCapabilityIdFromFootprints(updatedData CapacityLimit, ucdn string, inf *api.APIInfo) (int, error) {
+	var potentialCapabilityIds []int
+	for _, footprint := range updatedData.Footprints {
+		selectQuery := `SELECT footprint_type, footprint_value, capability_id FROM cdni_footprints WHERE ucdn = $1 AND footprint_type = $2 AND footprint_value = `
+		valuesAsQuery := "ARRAY['" + strings.Join(footprint.FootprintValue, "','") + "']"

Review comment:
       I tried a lot of different ways to do this and this was the only way i found that works. i really dont like it and would be on board to change it but i cant figure out how to get it to work since the parameter passing adds quotes




-- 
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 #6557: Added CDNi ability to negotiate higher capacities

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



##########
File path: traffic_ops/traffic_ops_golang/cdni/shared.go
##########
@@ -68,17 +82,388 @@ func GetCapabilities(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
+	ucdn, err := checkBearerToken(bearerToken, inf)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, err)
+		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 PutHostConfiguration(w http.ResponseWriter, r *http.Request) {
+	inf, userErr, sysErr, errCode := api.NewInfo(r, []string{"host"}, nil)
+	if userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr)
+		return
+	}
+	defer inf.Close()
+
+	host := inf.Params["host"]
+	if errCode, userErr, sysErr := validateHostExists(host, inf.Tx.Tx); userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr)
+		return
+	}
+
+	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
+	}
+
+	ucdn, err := checkBearerToken(bearerToken, inf)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, err)
+		return
+	}
+
+	var genericHostRequest GenericHostMetadata
+	err = json.NewDecoder(r.Body).Decode(&genericHostRequest)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("decoding host json request: %w", err))
+		return
+	}
+
+	db, err := api.GetDB(r.Context())
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("getting async db: %w", err))
+		return
+	}
+	asyncTx, err := db.Begin()
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("getting async tx: %w", err))
+		return
+	}
+
+	asyncStatusId, errCode, userErr, sysErr := api.InsertAsyncStatus(asyncTx, "CDNi host configuration update request received.")
+	if userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr)
+		return
+	}
+
+	//var typeToValueMap map[string]GenericRequestedLimits
+	//for _, hostUpdate := range genericHostRequest.HostMetadata.Metadata {
+	//	typeToValueMap[hostUpdate.Type] = hostUpdate.Value
+	//}
+
+	data := genericHostRequest.HostMetadata.Metadata
+
+	_, err = inf.Tx.Tx.Query(InsertCapabilityUpdateQuery, ucdn, data, asyncStatusId, hostConfigLabel, host)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("inserting capability update request into queue: %w", err))
+		return
+	}
+
+	var alerts tc.Alerts
+	alerts.AddAlert(tc.Alert{
+		Text:  "CDNi configuration update request received. Status updates can be found here: " + api.CurrentAsyncEndpoint + strconv.Itoa(asyncStatusId),
+		Level: tc.SuccessLevel.String(),
+	})
+
+	w.Header().Add("Location", api.CurrentAsyncEndpoint+strconv.Itoa(asyncStatusId))
+	api.WriteAlerts(w, r, http.StatusAccepted, alerts)
+}
+
+func PutConfiguration(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
+	}
+
+	ucdn, err := checkBearerToken(bearerToken, inf)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, err)
+		return
+	}
+
+	var genericRequest GenericRequestMetadata
+	err = json.NewDecoder(r.Body).Decode(&genericRequest)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("decoding json request: %w", err))
+		return
+	}
+
+	db, err := api.GetDB(r.Context())
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("getting async db: %w", err))
+		return
+	}
+	asyncTx, err := db.Begin()
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("getting async tx: %w", err))
+		return
+	}
+
+	asyncStatusId, errCode, userErr, sysErr := api.InsertAsyncStatus(asyncTx, "CDNi configuration update request received.")
+	if userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr)
+		return
+	}
+
+	data := genericRequest.Metadata
+
+	_, err = inf.Tx.Tx.Query(InsertCapabilityUpdateQuery, ucdn, data, asyncStatusId, SupportedGenericMetadataType(genericRequest.Type), genericRequest.Host)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("inserting capability update request into queue: %w", err))
+		return
+	}
+
+	var alerts tc.Alerts
+	alerts.AddAlert(tc.Alert{
+		Text:  "CDNi configuration update request received. Status updates can be found here: " + api.CurrentAsyncEndpoint + strconv.Itoa(asyncStatusId),
+		Level: tc.SuccessLevel.String(),
+	})
+
+	w.Header().Add("Location", api.CurrentAsyncEndpoint+strconv.Itoa(asyncStatusId))
+	api.WriteAlerts(w, r, http.StatusAccepted, alerts)
+}
+
+func PutConfigurationResponse(w http.ResponseWriter, r *http.Request) {
+	inf, userErr, sysErr, errCode := api.NewInfo(r, []string{"approved", "id"}, []string{"id"})
+	if userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr)
+		return
+	}
+	defer inf.Close()
+
+	reqId := inf.IntParams["id"]
+	approvedString := inf.Params["approved"]
+	approved, err := strconv.ParseBool(approvedString)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusBadRequest, errors.New("approved parameter must be a boolean"), nil)
+		return
+	}
+
+	db, err := api.GetDB(r.Context())
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("getting async db: %w", err))
+		return
+	}
+
+	rows, err := inf.Tx.Tx.Query(SelectCapabilityUpdateQuery, reqId)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("querying for capability update request: %w", err))
+		return
+	}
+	defer rows.Close()
+	var ucdn string
+	var data json.RawMessage
+	var host string
+	var asyncId int
+	var requestType string
+	count := 0
+	for rows.Next() {
+		if err := rows.Scan(&ucdn, &data, &asyncId, &requestType, &host); err != nil {
+			api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("scanning db rows: %w", err))
+			return
+		}
+		count++
+	}
+	if count == 0 {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusNotFound, fmt.Errorf("no configuration request for that id"), nil)
+		return
+	}
+
+	if !approved {
+		if asyncErr := api.UpdateAsyncStatus(db, api.AsyncFailed, "Requested configuration update has been denied.", asyncId, true); asyncErr != nil {
+			log.Errorf("updating async status for id %v: %v", asyncId, asyncErr)
+		}
+		status, err := deleteCapabilityRequest(reqId, inf.Tx.Tx)
+		if err != nil {
+			api.HandleErr(w, r, inf.Tx.Tx, status, nil, fmt.Errorf("deleting configuration request from queue: %w", err))
+			return
+		}
+		api.WriteResp(w, r, "Successfully denied configuration update request.")
+		return
+	}
+
+	var updatedDataList []GenericMetadata
+	if err = json.Unmarshal(data, &updatedDataList); err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("unmarshalling data for configuration update: %w", err))
+		return
+	}
+
+	var unsupportedTypes []string
+	for _, updatedData := range updatedDataList {
+		if !updatedData.Type.isValid() {
+			unsupportedTypes = append(unsupportedTypes, string(updatedData.Type))
+		}
+	}
+
+	if len(unsupportedTypes) != 0 {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusBadRequest, fmt.Errorf("unsupported generic metadata types found: %v", strings.Join(unsupportedTypes, ", ")), nil)
+		return
+	}
+
+	for _, updatedData := range updatedDataList {
+		switch updatedData.Type {
+		case MiRequestedCapacityLimits:
+			var capacityRequestedLimits CapacityRequestedLimits
+			if err = json.Unmarshal(updatedData.Value, &capacityRequestedLimits); err != nil {
+				api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("unmarshalling data for configuration update: %w", err))
+				return
+			}
+			for _, capLim := range capacityRequestedLimits.RequestedLimits {
+				capId, err := getCapabilityIdFromFootprints(capLim, ucdn, inf)
+				if err != nil {
+					api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("finding capability for given information: %w", err))
+					return
+				}
+
+				query := UpdateTotalLimitsByCapabilityAndLimitTypeQuery
+				queryParams := []interface{}{capLim.LimitValue, capId, capLim.LimitType}
+				if host != "" {
+					query = UpdateHostLimitsByCapabilityAndLimitTypeQuery
+					queryParams = []interface{}{capLim.LimitValue, capId, capLim.LimitType, host}
+				}
+
+				result, err := inf.Tx.Tx.Exec(query, queryParams...)
+				if err != nil {
+					api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("updating capacity: %w", err))
+					return
+				}
+
+				if rowsAffected, err := result.RowsAffected(); err != nil {
+					api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("updating capacity: getting rows affected: %w", err))
+					return
+				} else if rowsAffected < 1 {
+					api.HandleErr(w, r, inf.Tx.Tx, http.StatusNotFound, fmt.Errorf("no capacity found for update: host: %s, type: %s, limit: %v", host, updatedData.Type, capLim), nil)
+					return
+				} else if rowsAffected > 1 {
+					api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("capacity update affected too many rows: %d", rowsAffected))
+					return
+				}
+			}
+		}
+	}
+
+	if asyncErr := api.UpdateAsyncStatus(db, api.AsyncSucceeded, "Capacity requested update has been completed.", asyncId, true); asyncErr != nil {
+		log.Errorf("updating async status for id %v: %v", asyncId, asyncErr)
+	}
+	status, err := deleteCapabilityRequest(reqId, inf.Tx.Tx)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, status, nil, fmt.Errorf("deleting capacity request from queue: %w", err))
+		return
+	}
+	api.WriteResp(w, r, "Successfully updated configuration.")
+}
+
+func getCapabilityIdFromFootprints(updatedData CapacityLimit, ucdn string, inf *api.APIInfo) (int, error) {
+	var potentialCapabilityIds []int
+	for _, footprint := range updatedData.Footprints {
+		selectQuery := `SELECT footprint_type, footprint_value, capability_id FROM cdni_footprints WHERE ucdn = $1 AND footprint_type = $2 AND footprint_value = `
+		valuesAsQuery := "ARRAY['" + strings.Join(footprint.FootprintValue, "','") + "']"
+		rows, err := inf.Tx.Tx.Query(selectQuery+valuesAsQuery, ucdn, footprint.FootprintType)
+		if err != nil {
+			return 0, fmt.Errorf("querying for capacity update request: %w", err)
+		}
+		defer rows.Close()
+		var capabilityIds []int
+		rowCount := 0
+		for rows.Next() {
+			var capabilityId int
+			var footprint Footprint
+			if err := rows.Scan(&footprint.FootprintType, pq.Array(&footprint.FootprintValue), &capabilityId); err != nil {
+				return 0, fmt.Errorf("scanning db rows: %w", err)
+			}
+			rowCount++
+			capabilityIds = append(capabilityIds, capabilityId)
+		}
+
+		if rowCount == 0 {
+			return 0, fmt.Errorf("no capabilities found for footprint: %v, %v", footprint.FootprintType, footprint.FootprintValue)
+		}
+		var newPotentials []int
+		for _, capId := range capabilityIds {
+			if len(potentialCapabilityIds) == 0 {
+				newPotentials = append(newPotentials, capId)
+			} else {
+				if util.IntInArray(potentialCapabilityIds, capId) {
+					newPotentials = append(newPotentials, capId)
+				}
+			}
+		}
+		if len(newPotentials) == 0 {
+			return 0, fmt.Errorf("no capabilities found that match all footprints: %v", updatedData.Footprints)
+		}
+		potentialCapabilityIds = newPotentials

Review comment:
       revamped it to work way better with some ugly sql




-- 
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 #6557: Added CDNi ability to negotiate higher capacities

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



##########
File path: lib/go-util/num.go
##########
@@ -193,3 +193,13 @@ func IntSliceToMap(s []int) map[int]struct{} {
 	}
 	return m
 }
+
+// IntInArray returns whether i is one of the ints in list.
+func IntInArray(list []int, i int) bool {
+	for _, str := range list {

Review comment:
       removed this entirely with the rewrite




-- 
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 #6557: Added CDNi ability to negotiate higher capacities

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



##########
File path: traffic_ops/traffic_ops_golang/cdni/shared.go
##########
@@ -68,17 +82,388 @@ func GetCapabilities(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
+	ucdn, err := checkBearerToken(bearerToken, inf)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, err)
+		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 PutHostConfiguration(w http.ResponseWriter, r *http.Request) {
+	inf, userErr, sysErr, errCode := api.NewInfo(r, []string{"host"}, nil)
+	if userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr)
+		return
+	}
+	defer inf.Close()
+
+	host := inf.Params["host"]
+	if errCode, userErr, sysErr := validateHostExists(host, inf.Tx.Tx); userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr)
+		return
+	}
+
+	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
+	}
+
+	ucdn, err := checkBearerToken(bearerToken, inf)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, err)
+		return
+	}
+
+	var genericHostRequest GenericHostMetadata
+	err = json.NewDecoder(r.Body).Decode(&genericHostRequest)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("decoding host json request: %w", err))
+		return
+	}
+
+	db, err := api.GetDB(r.Context())
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("getting async db: %w", err))
+		return
+	}
+	asyncTx, err := db.Begin()
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("getting async tx: %w", err))
+		return
+	}
+
+	asyncStatusId, errCode, userErr, sysErr := api.InsertAsyncStatus(asyncTx, "CDNi host configuration update request received.")
+	if userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr)
+		return
+	}
+
+	//var typeToValueMap map[string]GenericRequestedLimits
+	//for _, hostUpdate := range genericHostRequest.HostMetadata.Metadata {
+	//	typeToValueMap[hostUpdate.Type] = hostUpdate.Value
+	//}
+
+	data := genericHostRequest.HostMetadata.Metadata
+
+	_, err = inf.Tx.Tx.Query(InsertCapabilityUpdateQuery, ucdn, data, asyncStatusId, hostConfigLabel, host)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("inserting capability update request into queue: %w", err))
+		return
+	}
+
+	var alerts tc.Alerts
+	alerts.AddAlert(tc.Alert{
+		Text:  "CDNi configuration update request received. Status updates can be found here: " + api.CurrentAsyncEndpoint + strconv.Itoa(asyncStatusId),
+		Level: tc.SuccessLevel.String(),
+	})
+
+	w.Header().Add("Location", api.CurrentAsyncEndpoint+strconv.Itoa(asyncStatusId))
+	api.WriteAlerts(w, r, http.StatusAccepted, alerts)
+}
+
+func PutConfiguration(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
+	}
+
+	ucdn, err := checkBearerToken(bearerToken, inf)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, err)
+		return
+	}
+
+	var genericRequest GenericRequestMetadata
+	err = json.NewDecoder(r.Body).Decode(&genericRequest)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("decoding json request: %w", err))
+		return
+	}
+
+	db, err := api.GetDB(r.Context())
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("getting async db: %w", err))
+		return
+	}
+	asyncTx, err := db.Begin()
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("getting async tx: %w", err))
+		return
+	}
+
+	asyncStatusId, errCode, userErr, sysErr := api.InsertAsyncStatus(asyncTx, "CDNi configuration update request received.")
+	if userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr)
+		return
+	}
+
+	data := genericRequest.Metadata
+
+	_, err = inf.Tx.Tx.Query(InsertCapabilityUpdateQuery, ucdn, data, asyncStatusId, SupportedGenericMetadataType(genericRequest.Type), genericRequest.Host)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("inserting capability update request into queue: %w", err))
+		return
+	}
+
+	var alerts tc.Alerts
+	alerts.AddAlert(tc.Alert{
+		Text:  "CDNi configuration update request received. Status updates can be found here: " + api.CurrentAsyncEndpoint + strconv.Itoa(asyncStatusId),
+		Level: tc.SuccessLevel.String(),
+	})
+
+	w.Header().Add("Location", api.CurrentAsyncEndpoint+strconv.Itoa(asyncStatusId))
+	api.WriteAlerts(w, r, http.StatusAccepted, alerts)
+}
+
+func PutConfigurationResponse(w http.ResponseWriter, r *http.Request) {
+	inf, userErr, sysErr, errCode := api.NewInfo(r, []string{"approved", "id"}, []string{"id"})
+	if userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr)
+		return
+	}
+	defer inf.Close()
+
+	reqId := inf.IntParams["id"]
+	approvedString := inf.Params["approved"]
+	approved, err := strconv.ParseBool(approvedString)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusBadRequest, errors.New("approved parameter must be a boolean"), nil)
+		return
+	}
+
+	db, err := api.GetDB(r.Context())
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("getting async db: %w", err))
+		return
+	}
+
+	rows, err := inf.Tx.Tx.Query(SelectCapabilityUpdateQuery, reqId)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("querying for capability update request: %w", err))
+		return
+	}
+	defer rows.Close()
+	var ucdn string
+	var data json.RawMessage
+	var host string
+	var asyncId int
+	var requestType string
+	count := 0
+	for rows.Next() {
+		if err := rows.Scan(&ucdn, &data, &asyncId, &requestType, &host); err != nil {
+			api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("scanning db rows: %w", err))
+			return
+		}
+		count++
+	}
+	if count == 0 {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusNotFound, fmt.Errorf("no configuration request for that id"), nil)
+		return
+	}
+
+	if !approved {
+		if asyncErr := api.UpdateAsyncStatus(db, api.AsyncFailed, "Requested configuration update has been denied.", asyncId, true); asyncErr != nil {
+			log.Errorf("updating async status for id %v: %v", asyncId, asyncErr)
+		}
+		status, err := deleteCapabilityRequest(reqId, inf.Tx.Tx)
+		if err != nil {
+			api.HandleErr(w, r, inf.Tx.Tx, status, nil, fmt.Errorf("deleting configuration request from queue: %w", err))
+			return
+		}
+		api.WriteResp(w, r, "Successfully denied configuration update request.")
+		return
+	}
+
+	var updatedDataList []GenericMetadata
+	if err = json.Unmarshal(data, &updatedDataList); err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("unmarshalling data for configuration update: %w", err))
+		return
+	}
+
+	var unsupportedTypes []string
+	for _, updatedData := range updatedDataList {
+		if !updatedData.Type.isValid() {
+			unsupportedTypes = append(unsupportedTypes, string(updatedData.Type))
+		}
+	}
+
+	if len(unsupportedTypes) != 0 {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusBadRequest, fmt.Errorf("unsupported generic metadata types found: %v", strings.Join(unsupportedTypes, ", ")), nil)
+		return
+	}
+
+	for _, updatedData := range updatedDataList {
+		switch updatedData.Type {
+		case MiRequestedCapacityLimits:
+			var capacityRequestedLimits CapacityRequestedLimits
+			if err = json.Unmarshal(updatedData.Value, &capacityRequestedLimits); err != nil {
+				api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("unmarshalling data for configuration update: %w", err))
+				return
+			}
+			for _, capLim := range capacityRequestedLimits.RequestedLimits {
+				capId, err := getCapabilityIdFromFootprints(capLim, ucdn, inf)
+				if err != nil {
+					api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("finding capability for given information: %w", err))
+					return
+				}
+
+				query := UpdateTotalLimitsByCapabilityAndLimitTypeQuery
+				queryParams := []interface{}{capLim.LimitValue, capId, capLim.LimitType}
+				if host != "" {
+					query = UpdateHostLimitsByCapabilityAndLimitTypeQuery
+					queryParams = []interface{}{capLim.LimitValue, capId, capLim.LimitType, host}
+				}
+
+				result, err := inf.Tx.Tx.Exec(query, queryParams...)
+				if err != nil {
+					api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("updating capacity: %w", err))
+					return
+				}
+
+				if rowsAffected, err := result.RowsAffected(); err != nil {
+					api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("updating capacity: getting rows affected: %w", err))
+					return
+				} else if rowsAffected < 1 {
+					api.HandleErr(w, r, inf.Tx.Tx, http.StatusNotFound, fmt.Errorf("no capacity found for update: host: %s, type: %s, limit: %v", host, updatedData.Type, capLim), nil)
+					return
+				} else if rowsAffected > 1 {
+					api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("capacity update affected too many rows: %d", rowsAffected))
+					return
+				}
+			}
+		}
+	}
+
+	if asyncErr := api.UpdateAsyncStatus(db, api.AsyncSucceeded, "Capacity requested update has been completed.", asyncId, true); asyncErr != nil {
+		log.Errorf("updating async status for id %v: %v", asyncId, asyncErr)
+	}
+	status, err := deleteCapabilityRequest(reqId, inf.Tx.Tx)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, status, nil, fmt.Errorf("deleting capacity request from queue: %w", err))
+		return
+	}
+	api.WriteResp(w, r, "Successfully updated configuration.")
+}
+
+func getCapabilityIdFromFootprints(updatedData CapacityLimit, ucdn string, inf *api.APIInfo) (int, error) {
+	var potentialCapabilityIds []int
+	for _, footprint := range updatedData.Footprints {
+		selectQuery := `SELECT footprint_type, footprint_value, capability_id FROM cdni_footprints WHERE ucdn = $1 AND footprint_type = $2 AND footprint_value = `
+		valuesAsQuery := "ARRAY['" + strings.Join(footprint.FootprintValue, "','") + "']"
+		rows, err := inf.Tx.Tx.Query(selectQuery+valuesAsQuery, ucdn, footprint.FootprintType)
+		if err != nil {
+			return 0, fmt.Errorf("querying for capacity update request: %w", err)
+		}
+		defer rows.Close()
+		var capabilityIds []int
+		rowCount := 0
+		for rows.Next() {
+			var capabilityId int
+			var footprint Footprint
+			if err := rows.Scan(&footprint.FootprintType, pq.Array(&footprint.FootprintValue), &capabilityId); err != nil {
+				return 0, fmt.Errorf("scanning db rows: %w", err)
+			}
+			rowCount++
+			capabilityIds = append(capabilityIds, capabilityId)
+		}
+
+		if rowCount == 0 {
+			return 0, fmt.Errorf("no capabilities found for footprint: %v, %v", footprint.FootprintType, footprint.FootprintValue)
+		}
+		var newPotentials []int
+		for _, capId := range capabilityIds {
+			if len(potentialCapabilityIds) == 0 {
+				newPotentials = append(newPotentials, capId)
+			} else {
+				if util.IntInArray(potentialCapabilityIds, capId) {
+					newPotentials = append(newPotentials, capId)
+				}
+			}
+		}
+		if len(newPotentials) == 0 {
+			return 0, fmt.Errorf("no capabilities found that match all footprints: %v", updatedData.Footprints)
+		}
+		potentialCapabilityIds = newPotentials

Review comment:
       yea its super confusing and also im very open to better ways to do this if you have any. 
   
   for this, the footprints can overlap and can be linked to multiple capabilities in the long run. right now this is just for Capacity but there will be a lot more as i keep going. plus each capability can have multiple footprints. so for this code im looping through the footprints that have the correct uCDN, type, and value based on the request, then making a list of potential capability ids based on all of the footprints that came linked to the info in the request.  
   
   this way if you send a request linked to footprints like `[ {footprint-type: ipv4cidr", "footprint-value": ["127.0.0.1", "127.0.0.2" ] }, {footprint-type: "countrycode", "footprint-value": ["us"] } ]` then it wont get capabilities linke to just the countrycode footprint or just the ipv4cidr footprint or other capabilities linked to those plus more.
   
   does that make any sense?




-- 
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 #6557: Added CDNi ability to negotiate higher capacities

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



##########
File path: docs/source/api/v4/oc_ci_configuration_host.rst
##########
@@ -0,0 +1,107 @@
+..
+..
+.. Licensed 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.
+..
+
+.. _to-api-oc-fci-configuration-host:
+
+********************************
+``OC/CI/configuration/{{host}}``
+********************************
+
+``PUT``
+=======
+Triggers an asynchronous task to update the configuration for the :abbr:`uCDN (Upstream Content Delivery Network)` and the specified host by adding the request to a queue to be reviewed later. This returns a 202 Accepted status and an endpoint to be used for status updates.
+
+:Auth. Required: Yes
+:Roles Required: "admin" or "operations"
+:Permissions Required: CDNI:READ

Review comment:
       Oh ok, I can see your reasoning behind that. Because someone else has to approve the update, this isn't really an update. I could go either way, if you want to keep it as-is.




-- 
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 #6557: Added CDNi ability to negotiate higher capacities

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


   


-- 
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 #6557: Added CDNi ability to negotiate higher capacities

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



##########
File path: traffic_ops/traffic_ops_golang/cdni/shared.go
##########
@@ -295,14 +307,16 @@ func PutConfigurationResponse(w http.ResponseWriter, r *http.Request) {
 
 	if !approved {
 		if asyncErr := api.UpdateAsyncStatus(db, api.AsyncFailed, "Requested configuration update has been denied.", asyncId, true); asyncErr != nil {
-			log.Errorf("updating async status for id %v: %v", asyncId, asyncErr)
+			log.Errorf("updating async status for id %d: %w", asyncId, asyncErr)

Review comment:
       This can't use `%w` apparently (that's for wrapping), but `%s` should work

##########
File path: traffic_ops/traffic_ops_golang/cdni/shared.go
##########
@@ -154,18 +149,19 @@ func PutHostConfiguration(w http.ResponseWriter, r *http.Request) {
 		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("getting async tx: %w", err))
 		return
 	}
+	logTx, err := db.Begin()

Review comment:
       I'm confused why we need different `asyncTx`, `logTx`, etc. Why can't we just use `inf.Tx.Tx`?

##########
File path: traffic_ops/traffic_ops_golang/cdni/shared.go
##########
@@ -269,12 +274,19 @@ func PutConfigurationResponse(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
+	logTx, err := db.Begin()

Review comment:
       Ditto prior comment about different transactions

##########
File path: traffic_ops/traffic_ops_golang/cdni/shared.go
##########
@@ -222,6 +219,12 @@ func PutConfiguration(w http.ResponseWriter, r *http.Request) {
 		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("getting async tx: %w", err))
 		return
 	}
+	logTx, err := db.Begin()

Review comment:
       Ditto prior comment about different transactions




-- 
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 #6557: Added CDNi ability to negotiate higher capacities

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



##########
File path: traffic_ops/traffic_ops_golang/cdni/shared.go
##########
@@ -68,17 +82,388 @@ func GetCapabilities(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
+	ucdn, err := checkBearerToken(bearerToken, inf)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, err)
+		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 PutHostConfiguration(w http.ResponseWriter, r *http.Request) {
+	inf, userErr, sysErr, errCode := api.NewInfo(r, []string{"host"}, nil)
+	if userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr)
+		return
+	}
+	defer inf.Close()
+
+	host := inf.Params["host"]
+	if errCode, userErr, sysErr := validateHostExists(host, inf.Tx.Tx); userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr)
+		return
+	}
+
+	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
+	}
+
+	ucdn, err := checkBearerToken(bearerToken, inf)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, err)
+		return
+	}
+
+	var genericHostRequest GenericHostMetadata
+	err = json.NewDecoder(r.Body).Decode(&genericHostRequest)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("decoding host json request: %w", err))
+		return
+	}
+
+	db, err := api.GetDB(r.Context())
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("getting async db: %w", err))
+		return
+	}
+	asyncTx, err := db.Begin()
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("getting async tx: %w", err))
+		return
+	}
+
+	asyncStatusId, errCode, userErr, sysErr := api.InsertAsyncStatus(asyncTx, "CDNi host configuration update request received.")
+	if userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr)
+		return
+	}
+
+	//var typeToValueMap map[string]GenericRequestedLimits
+	//for _, hostUpdate := range genericHostRequest.HostMetadata.Metadata {
+	//	typeToValueMap[hostUpdate.Type] = hostUpdate.Value
+	//}
+
+	data := genericHostRequest.HostMetadata.Metadata
+
+	_, err = inf.Tx.Tx.Query(InsertCapabilityUpdateQuery, ucdn, data, asyncStatusId, hostConfigLabel, host)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("inserting capability update request into queue: %w", err))
+		return
+	}
+
+	var alerts tc.Alerts
+	alerts.AddAlert(tc.Alert{
+		Text:  "CDNi configuration update request received. Status updates can be found here: " + api.CurrentAsyncEndpoint + strconv.Itoa(asyncStatusId),
+		Level: tc.SuccessLevel.String(),
+	})
+
+	w.Header().Add("Location", api.CurrentAsyncEndpoint+strconv.Itoa(asyncStatusId))
+	api.WriteAlerts(w, r, http.StatusAccepted, alerts)
+}
+
+func PutConfiguration(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
+	}
+
+	ucdn, err := checkBearerToken(bearerToken, inf)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, err)
+		return
+	}
+
+	var genericRequest GenericRequestMetadata
+	err = json.NewDecoder(r.Body).Decode(&genericRequest)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("decoding json request: %w", err))
+		return
+	}
+
+	db, err := api.GetDB(r.Context())
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("getting async db: %w", err))
+		return
+	}
+	asyncTx, err := db.Begin()
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("getting async tx: %w", err))
+		return
+	}
+
+	asyncStatusId, errCode, userErr, sysErr := api.InsertAsyncStatus(asyncTx, "CDNi configuration update request received.")
+	if userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr)
+		return
+	}
+
+	data := genericRequest.Metadata
+
+	_, err = inf.Tx.Tx.Query(InsertCapabilityUpdateQuery, ucdn, data, asyncStatusId, SupportedGenericMetadataType(genericRequest.Type), genericRequest.Host)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("inserting capability update request into queue: %w", err))
+		return
+	}
+
+	var alerts tc.Alerts
+	alerts.AddAlert(tc.Alert{
+		Text:  "CDNi configuration update request received. Status updates can be found here: " + api.CurrentAsyncEndpoint + strconv.Itoa(asyncStatusId),
+		Level: tc.SuccessLevel.String(),
+	})
+
+	w.Header().Add("Location", api.CurrentAsyncEndpoint+strconv.Itoa(asyncStatusId))
+	api.WriteAlerts(w, r, http.StatusAccepted, alerts)
+}
+
+func PutConfigurationResponse(w http.ResponseWriter, r *http.Request) {
+	inf, userErr, sysErr, errCode := api.NewInfo(r, []string{"approved", "id"}, []string{"id"})
+	if userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr)
+		return
+	}
+	defer inf.Close()
+
+	reqId := inf.IntParams["id"]
+	approvedString := inf.Params["approved"]
+	approved, err := strconv.ParseBool(approvedString)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusBadRequest, errors.New("approved parameter must be a boolean"), nil)
+		return
+	}
+
+	db, err := api.GetDB(r.Context())
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("getting async db: %w", err))
+		return
+	}
+
+	rows, err := inf.Tx.Tx.Query(SelectCapabilityUpdateQuery, reqId)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("querying for capability update request: %w", err))
+		return
+	}
+	defer rows.Close()
+	var ucdn string
+	var data json.RawMessage
+	var host string
+	var asyncId int
+	var requestType string
+	count := 0
+	for rows.Next() {
+		if err := rows.Scan(&ucdn, &data, &asyncId, &requestType, &host); err != nil {
+			api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("scanning db rows: %w", err))
+			return
+		}
+		count++
+	}
+	if count == 0 {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusNotFound, fmt.Errorf("no configuration request for that id"), nil)
+		return
+	}
+
+	if !approved {
+		if asyncErr := api.UpdateAsyncStatus(db, api.AsyncFailed, "Requested configuration update has been denied.", asyncId, true); asyncErr != nil {
+			log.Errorf("updating async status for id %v: %v", asyncId, asyncErr)
+		}
+		status, err := deleteCapabilityRequest(reqId, inf.Tx.Tx)
+		if err != nil {
+			api.HandleErr(w, r, inf.Tx.Tx, status, nil, fmt.Errorf("deleting configuration request from queue: %w", err))
+			return
+		}
+		api.WriteResp(w, r, "Successfully denied configuration update request.")
+		return
+	}
+
+	var updatedDataList []GenericMetadata
+	if err = json.Unmarshal(data, &updatedDataList); err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("unmarshalling data for configuration update: %w", err))
+		return
+	}
+
+	var unsupportedTypes []string
+	for _, updatedData := range updatedDataList {
+		if !updatedData.Type.isValid() {
+			unsupportedTypes = append(unsupportedTypes, string(updatedData.Type))
+		}
+	}
+
+	if len(unsupportedTypes) != 0 {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusBadRequest, fmt.Errorf("unsupported generic metadata types found: %v", strings.Join(unsupportedTypes, ", ")), nil)
+		return
+	}
+
+	for _, updatedData := range updatedDataList {
+		switch updatedData.Type {
+		case MiRequestedCapacityLimits:
+			var capacityRequestedLimits CapacityRequestedLimits
+			if err = json.Unmarshal(updatedData.Value, &capacityRequestedLimits); err != nil {
+				api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("unmarshalling data for configuration update: %w", err))
+				return
+			}
+			for _, capLim := range capacityRequestedLimits.RequestedLimits {
+				capId, err := getCapabilityIdFromFootprints(capLim, ucdn, inf)
+				if err != nil {
+					api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("finding capability for given information: %w", err))
+					return
+				}
+
+				query := UpdateTotalLimitsByCapabilityAndLimitTypeQuery
+				queryParams := []interface{}{capLim.LimitValue, capId, capLim.LimitType}
+				if host != "" {
+					query = UpdateHostLimitsByCapabilityAndLimitTypeQuery
+					queryParams = []interface{}{capLim.LimitValue, capId, capLim.LimitType, host}
+				}
+
+				result, err := inf.Tx.Tx.Exec(query, queryParams...)
+				if err != nil {
+					api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("updating capacity: %w", err))
+					return
+				}
+
+				if rowsAffected, err := result.RowsAffected(); err != nil {
+					api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("updating capacity: getting rows affected: %w", err))
+					return
+				} else if rowsAffected < 1 {
+					api.HandleErr(w, r, inf.Tx.Tx, http.StatusNotFound, fmt.Errorf("no capacity found for update: host: %s, type: %s, limit: %v", host, updatedData.Type, capLim), nil)
+					return
+				} else if rowsAffected > 1 {
+					api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("capacity update affected too many rows: %d", rowsAffected))
+					return
+				}
+			}
+		}
+	}
+
+	if asyncErr := api.UpdateAsyncStatus(db, api.AsyncSucceeded, "Capacity requested update has been completed.", asyncId, true); asyncErr != nil {
+		log.Errorf("updating async status for id %v: %v", asyncId, asyncErr)
+	}
+	status, err := deleteCapabilityRequest(reqId, inf.Tx.Tx)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, status, nil, fmt.Errorf("deleting capacity request from queue: %w", err))
+		return
+	}
+	api.WriteResp(w, r, "Successfully updated configuration.")
+}
+
+func getCapabilityIdFromFootprints(updatedData CapacityLimit, ucdn string, inf *api.APIInfo) (int, error) {
+	var potentialCapabilityIds []int
+	for _, footprint := range updatedData.Footprints {
+		selectQuery := `SELECT footprint_type, footprint_value, capability_id FROM cdni_footprints WHERE ucdn = $1 AND footprint_type = $2 AND footprint_value = `
+		valuesAsQuery := "ARRAY['" + strings.Join(footprint.FootprintValue, "','") + "']"

Review comment:
       Does it need typecast like `$3::text[]`? Passing arrays into queries is tricky, and I'm not sure we have many (if any) examples of array comparison. If a typecast didn't work, I guess this is ok.




-- 
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 #6557: Added CDNi ability to negotiate higher capacities

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



##########
File path: docs/source/api/v4/oc_ci_configuration_host.rst
##########
@@ -0,0 +1,107 @@
+..
+..
+.. Licensed 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.
+..
+
+.. _to-api-oc-fci-configuration-host:
+
+********************************
+``OC/CI/configuration/{{host}}``
+********************************
+
+``PUT``
+=======
+Triggers an asynchronous task to update the configuration for the :abbr:`uCDN (Upstream Content Delivery Network)` and the specified host by adding the request to a queue to be reviewed later. This returns a 202 Accepted status and an endpoint to be used for status updates.
+
+:Auth. Required: Yes
+:Roles Required: "admin" or "operations"
+:Permissions Required: CDNI:READ

Review comment:
       my thought on these were if you can read the `advertisement` endpoint then you can submit a request for an update but thats a little confusing. ill update it to be `UPDATE` here and then have like an `ADMIN` or something for the people who can approve updates




-- 
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 #6557: Added CDNi ability to negotiate higher capacities

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



##########
File path: lib/go-util/num.go
##########
@@ -193,3 +193,13 @@ func IntSliceToMap(s []int) map[int]struct{} {
 	}
 	return m
 }
+
+// IntInArray returns whether i is one of the ints in list.
+func IntInArray(list []int, i int) bool {
+	for _, str := range list {

Review comment:
       `num` would make more sense than `str`

##########
File path: traffic_ops/traffic_ops_golang/cdni/shared.go
##########
@@ -68,17 +82,388 @@ func GetCapabilities(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
+	ucdn, err := checkBearerToken(bearerToken, inf)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, err)
+		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 PutHostConfiguration(w http.ResponseWriter, r *http.Request) {
+	inf, userErr, sysErr, errCode := api.NewInfo(r, []string{"host"}, nil)
+	if userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr)
+		return
+	}
+	defer inf.Close()
+
+	host := inf.Params["host"]
+	if errCode, userErr, sysErr := validateHostExists(host, inf.Tx.Tx); userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr)
+		return
+	}
+
+	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
+	}
+
+	ucdn, err := checkBearerToken(bearerToken, inf)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, err)
+		return
+	}
+
+	var genericHostRequest GenericHostMetadata
+	err = json.NewDecoder(r.Body).Decode(&genericHostRequest)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("decoding host json request: %w", err))
+		return
+	}
+
+	db, err := api.GetDB(r.Context())
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("getting async db: %w", err))
+		return
+	}
+	asyncTx, err := db.Begin()
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("getting async tx: %w", err))
+		return
+	}
+
+	asyncStatusId, errCode, userErr, sysErr := api.InsertAsyncStatus(asyncTx, "CDNi host configuration update request received.")
+	if userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr)
+		return
+	}
+
+	//var typeToValueMap map[string]GenericRequestedLimits
+	//for _, hostUpdate := range genericHostRequest.HostMetadata.Metadata {
+	//	typeToValueMap[hostUpdate.Type] = hostUpdate.Value
+	//}
+
+	data := genericHostRequest.HostMetadata.Metadata
+
+	_, err = inf.Tx.Tx.Query(InsertCapabilityUpdateQuery, ucdn, data, asyncStatusId, hostConfigLabel, host)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("inserting capability update request into queue: %w", err))
+		return
+	}
+
+	var alerts tc.Alerts
+	alerts.AddAlert(tc.Alert{
+		Text:  "CDNi configuration update request received. Status updates can be found here: " + api.CurrentAsyncEndpoint + strconv.Itoa(asyncStatusId),
+		Level: tc.SuccessLevel.String(),
+	})
+
+	w.Header().Add("Location", api.CurrentAsyncEndpoint+strconv.Itoa(asyncStatusId))
+	api.WriteAlerts(w, r, http.StatusAccepted, alerts)
+}
+
+func PutConfiguration(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
+	}
+
+	ucdn, err := checkBearerToken(bearerToken, inf)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, err)
+		return
+	}
+
+	var genericRequest GenericRequestMetadata
+	err = json.NewDecoder(r.Body).Decode(&genericRequest)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("decoding json request: %w", err))
+		return
+	}
+
+	db, err := api.GetDB(r.Context())
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("getting async db: %w", err))
+		return
+	}
+	asyncTx, err := db.Begin()
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("getting async tx: %w", err))
+		return
+	}
+
+	asyncStatusId, errCode, userErr, sysErr := api.InsertAsyncStatus(asyncTx, "CDNi configuration update request received.")
+	if userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr)
+		return
+	}
+
+	data := genericRequest.Metadata
+
+	_, err = inf.Tx.Tx.Query(InsertCapabilityUpdateQuery, ucdn, data, asyncStatusId, SupportedGenericMetadataType(genericRequest.Type), genericRequest.Host)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("inserting capability update request into queue: %w", err))
+		return
+	}
+
+	var alerts tc.Alerts
+	alerts.AddAlert(tc.Alert{
+		Text:  "CDNi configuration update request received. Status updates can be found here: " + api.CurrentAsyncEndpoint + strconv.Itoa(asyncStatusId),
+		Level: tc.SuccessLevel.String(),
+	})
+
+	w.Header().Add("Location", api.CurrentAsyncEndpoint+strconv.Itoa(asyncStatusId))
+	api.WriteAlerts(w, r, http.StatusAccepted, alerts)
+}
+
+func PutConfigurationResponse(w http.ResponseWriter, r *http.Request) {
+	inf, userErr, sysErr, errCode := api.NewInfo(r, []string{"approved", "id"}, []string{"id"})
+	if userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr)
+		return
+	}
+	defer inf.Close()
+
+	reqId := inf.IntParams["id"]
+	approvedString := inf.Params["approved"]
+	approved, err := strconv.ParseBool(approvedString)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusBadRequest, errors.New("approved parameter must be a boolean"), nil)
+		return
+	}
+
+	db, err := api.GetDB(r.Context())
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("getting async db: %w", err))
+		return
+	}
+
+	rows, err := inf.Tx.Tx.Query(SelectCapabilityUpdateQuery, reqId)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("querying for capability update request: %w", err))
+		return
+	}
+	defer rows.Close()
+	var ucdn string
+	var data json.RawMessage
+	var host string
+	var asyncId int
+	var requestType string
+	count := 0
+	for rows.Next() {
+		if err := rows.Scan(&ucdn, &data, &asyncId, &requestType, &host); err != nil {
+			api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("scanning db rows: %w", err))
+			return
+		}
+		count++
+	}
+	if count == 0 {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusNotFound, fmt.Errorf("no configuration request for that id"), nil)
+		return
+	}
+
+	if !approved {
+		if asyncErr := api.UpdateAsyncStatus(db, api.AsyncFailed, "Requested configuration update has been denied.", asyncId, true); asyncErr != nil {
+			log.Errorf("updating async status for id %v: %v", asyncId, asyncErr)
+		}
+		status, err := deleteCapabilityRequest(reqId, inf.Tx.Tx)
+		if err != nil {
+			api.HandleErr(w, r, inf.Tx.Tx, status, nil, fmt.Errorf("deleting configuration request from queue: %w", err))
+			return
+		}
+		api.WriteResp(w, r, "Successfully denied configuration update request.")
+		return
+	}
+
+	var updatedDataList []GenericMetadata
+	if err = json.Unmarshal(data, &updatedDataList); err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("unmarshalling data for configuration update: %w", err))
+		return
+	}
+
+	var unsupportedTypes []string
+	for _, updatedData := range updatedDataList {
+		if !updatedData.Type.isValid() {
+			unsupportedTypes = append(unsupportedTypes, string(updatedData.Type))
+		}
+	}
+
+	if len(unsupportedTypes) != 0 {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusBadRequest, fmt.Errorf("unsupported generic metadata types found: %v", strings.Join(unsupportedTypes, ", ")), nil)
+		return
+	}
+
+	for _, updatedData := range updatedDataList {
+		switch updatedData.Type {
+		case MiRequestedCapacityLimits:
+			var capacityRequestedLimits CapacityRequestedLimits
+			if err = json.Unmarshal(updatedData.Value, &capacityRequestedLimits); err != nil {
+				api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("unmarshalling data for configuration update: %w", err))
+				return
+			}
+			for _, capLim := range capacityRequestedLimits.RequestedLimits {
+				capId, err := getCapabilityIdFromFootprints(capLim, ucdn, inf)
+				if err != nil {
+					api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("finding capability for given information: %w", err))
+					return
+				}
+
+				query := UpdateTotalLimitsByCapabilityAndLimitTypeQuery
+				queryParams := []interface{}{capLim.LimitValue, capId, capLim.LimitType}
+				if host != "" {
+					query = UpdateHostLimitsByCapabilityAndLimitTypeQuery
+					queryParams = []interface{}{capLim.LimitValue, capId, capLim.LimitType, host}
+				}
+
+				result, err := inf.Tx.Tx.Exec(query, queryParams...)
+				if err != nil {
+					api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("updating capacity: %w", err))
+					return
+				}
+
+				if rowsAffected, err := result.RowsAffected(); err != nil {
+					api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("updating capacity: getting rows affected: %w", err))
+					return
+				} else if rowsAffected < 1 {
+					api.HandleErr(w, r, inf.Tx.Tx, http.StatusNotFound, fmt.Errorf("no capacity found for update: host: %s, type: %s, limit: %v", host, updatedData.Type, capLim), nil)
+					return
+				} else if rowsAffected > 1 {
+					api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("capacity update affected too many rows: %d", rowsAffected))
+					return
+				}
+			}
+		}
+	}
+
+	if asyncErr := api.UpdateAsyncStatus(db, api.AsyncSucceeded, "Capacity requested update has been completed.", asyncId, true); asyncErr != nil {
+		log.Errorf("updating async status for id %v: %v", asyncId, asyncErr)
+	}
+	status, err := deleteCapabilityRequest(reqId, inf.Tx.Tx)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, status, nil, fmt.Errorf("deleting capacity request from queue: %w", err))
+		return
+	}
+	api.WriteResp(w, r, "Successfully updated configuration.")
+}
+
+func getCapabilityIdFromFootprints(updatedData CapacityLimit, ucdn string, inf *api.APIInfo) (int, error) {
+	var potentialCapabilityIds []int
+	for _, footprint := range updatedData.Footprints {
+		selectQuery := `SELECT footprint_type, footprint_value, capability_id FROM cdni_footprints WHERE ucdn = $1 AND footprint_type = $2 AND footprint_value = `
+		valuesAsQuery := "ARRAY['" + strings.Join(footprint.FootprintValue, "','") + "']"
+		rows, err := inf.Tx.Tx.Query(selectQuery+valuesAsQuery, ucdn, footprint.FootprintType)
+		if err != nil {
+			return 0, fmt.Errorf("querying for capacity update request: %w", err)
+		}
+		defer rows.Close()
+		var capabilityIds []int
+		rowCount := 0
+		for rows.Next() {
+			var capabilityId int
+			var footprint Footprint
+			if err := rows.Scan(&footprint.FootprintType, pq.Array(&footprint.FootprintValue), &capabilityId); err != nil {
+				return 0, fmt.Errorf("scanning db rows: %w", err)
+			}
+			rowCount++
+			capabilityIds = append(capabilityIds, capabilityId)
+		}
+
+		if rowCount == 0 {
+			return 0, fmt.Errorf("no capabilities found for footprint: %v, %v", footprint.FootprintType, footprint.FootprintValue)
+		}
+		var newPotentials []int
+		for _, capId := range capabilityIds {
+			if len(potentialCapabilityIds) == 0 {
+				newPotentials = append(newPotentials, capId)
+			} else {
+				if util.IntInArray(potentialCapabilityIds, capId) {
+					newPotentials = append(newPotentials, capId)
+				}
+			}
+		}
+		if len(newPotentials) == 0 {
+			return 0, fmt.Errorf("no capabilities found that match all footprints: %v", updatedData.Footprints)
+		}
+		potentialCapabilityIds = newPotentials

Review comment:
       It might make more sense if I knew the spec better, but don't really want to hold up your PR for that. As long as you think it's per the spec that's good enough for me.




-- 
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 #6557: Added CDNi ability to negotiate higher capacities

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



##########
File path: traffic_ops/traffic_ops_golang/cdni/shared.go
##########
@@ -154,18 +149,19 @@ func PutHostConfiguration(w http.ResponseWriter, r *http.Request) {
 		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("getting async tx: %w", err))
 		return
 	}
+	logTx, err := db.Begin()

Review comment:
       Oh yeah, it makes sense for when you have to hand off a DB transaction to a goroutine for it to be completed asynchronously, but I think when it's all handled synchronously all in the same goroutine it can use the same transaction.




-- 
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 #6557: Added CDNi ability to negotiate higher capacities

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


   


-- 
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