You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@trafficcontrol.apache.org by ra...@apache.org on 2022/02/14 23:34:17 UTC
[trafficcontrol] branch master updated: Added CDNi ability to negotiate higher capacities (#6557)
This is an automated email from the ASF dual-hosted git repository.
rawlin pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/trafficcontrol.git
The following commit(s) were added to refs/heads/master by this push:
new 661cc1e Added CDNi ability to negotiate higher capacities (#6557)
661cc1e is described below
commit 661cc1e9f2a2fc41b4d296ff016b71c6c7fa5f0b
Author: mattjackson220 <33...@users.noreply.github.com>
AuthorDate: Mon Feb 14 16:33:58 2022 -0700
Added CDNi ability to negotiate higher capacities (#6557)
* Added CDNi ability to negotiate higher capacities
* updated per comments
* rewrote confusing code to find capability id from array of footprints
---
CHANGELOG.md | 1 +
docs/source/admin/cdni.rst | 24 +
docs/source/api/v4/oc_ci_configuration.rst | 101 ++++
docs/source/api/v4/oc_ci_configuration_host.rst | 107 +++++
.../v4/oc_ci_configuration_request_id_approved.rst | 60 +++
.../traffic_router_golang/httpsrvr/httpsrvr.go | 3 +-
lib/go-rfc/http.go | 1 +
...022020114365100_capacity_updates_queue.down.sql | 18 +
.../2022020114365100_capacity_updates_queue.up.sql | 28 ++
.../traffic_ops_golang/cdn/dnssecrefresh.go | 3 +-
traffic_ops/traffic_ops_golang/cdni/shared.go | 512 +++++++++++++++++++--
.../traffic_ops_golang/deliveryservice/acme.go | 5 +-
.../deliveryservice/autorenewcerts.go | 3 +-
traffic_ops/traffic_ops_golang/routing/routes.go | 3 +
14 files changed, 820 insertions(+), 49 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3634127..2e40b4f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/).
## [unreleased]
### Added
- Added a new Traffic Ops endpoint to `GET` capacity and telemetry data for CDNi integration.
+- Added a Traffic Ops endpoints to `PUT` a requested configuration change for a full configuration or per host and an endpoint to approve or deny the request.
### Fixed
- Update traffic_portal dependencies to mitigate `npm audit` issues.
diff --git a/docs/source/admin/cdni.rst b/docs/source/admin/cdni.rst
index 306ae47..b18db1e 100644
--- a/docs/source/admin/cdni.rst
+++ b/docs/source/admin/cdni.rst
@@ -34,3 +34,27 @@ For our use case, it is assumed that :abbr:`ATC (Apache Traffic Control)` is the
.. seealso:: :ref:`to-api-oc-fci-advertisement`
The advertisement response is unique for the :abbr:`uCDN (Upstream Content Delivery Network)` and contains the complete footprint and capabilities information structure the :abbr:`dCDN (Downstream Content Delivery Network)` wants to expose. This endpoint will return an array of generic :abbr:`FCI (Footprint and Capabilities Advertisement Interface)` base objects, including type, value, and footprint for each. Currently supported base object types are `FCI.Capacitiy` and `FCI.Telemetry` b [...]
+
+/OC/CI/configuration
+====================
+.. seealso:: :ref:`to-api-oc-fci-configuration`
+
+An endpoint that is used to push (``PUT``), fetch (``GET``), or delete (``DELETE``) the entire metadata set for a given :abbr:`uCDN (Upstream Content Delivery Network)` from a :abbr:`JWT (JSON Web Token)`. This puts the requested change into a queue to be reviewed later and returns an endpoint to view the asynchronous status updates.
+
+.. Note:: This is under construction. Currently only ``PUT`` is supported and in a very limited sense.
+
+/OC/CI/configuration/{{host}}
+=============================
+.. seealso:: :ref:`to-api-oc-fci-configuration-host`
+
+An endpoint that is used to push (``PUT``), fetch (``GET``), or delete (``DELETE``) the metadata set that is attached to host name for a given :abbr:`uCDN (Upstream Content Delivery Network)` from a :abbr:`JWT (JSON Web Token)`. This puts the requested change into a queue to be reviewed later and returns an endpoint to view the asynchronous status updates.
+
+.. Note:: This is under construction. Currently only ``PUT`` is supported and in a very limited sense.
+
+/OC/CI/configuration/request/{{id}}/{{approved}}
+================================================
+.. seealso:: :ref:`to-api-oc-fci-configuration-request-id-approved`
+
+This endpoint allows a user to approve or deny a queued update request from the previous endpoints. A denial will result in the removal from the queue and a ``FAILED`` status update. An approval will result in the changes being made to the configuration and a ``SUCCEEDED`` status update.
+
+.. Note:: This is under construction and only supports very limited metadata field and limited configuration updates.
diff --git a/docs/source/api/v4/oc_ci_configuration.rst b/docs/source/api/v4/oc_ci_configuration.rst
new file mode 100644
index 0000000..25d9dfc
--- /dev/null
+++ b/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:UPDATE
+: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
+
+ PUT /api/4.0/oc/ci/configuration HTTP/1.1
+ Host: trafficops.infra.ciab.test
+ User-Agent: curl/7.47.0
+ Accept: */*
+ Cookie: mojolicious=...
+ Content-Length: 181
+ Content-Type: application/json
+
+ {
+ "type": "MI.HostMetadata",
+ "host": "example.com",
+ "metadata": [
+ {
+ "generic-metadata-type": "MI.RequestedCapacityLimits",
+ "generic-metadata-value": {
+ "requested-limits": [
+ {
+ "limit-type": "egress",
+ "limit-value": 20000,
+ "footprints": [
+ {
+ "footprint-type": "ipv4cidr",
+ "footprint-value": [
+ "127.0.0.1",
+ "127.0.0.2"
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ }
+ ]
+ }
+
+Response Structure
+------------------
+
+.. code-block:: http
+ :caption: Response Example
+
+ HTTP/1.1 202 Accepted
+ Content-Type: application/json
+
+ { "alerts": [
+ {
+ "text": "CDNi configuration update request received. Status updates can be found here: /api/4.0/async_status/1",
+ "level": "success"
+ }
+ ]}
diff --git a/docs/source/api/v4/oc_ci_configuration_host.rst b/docs/source/api/v4/oc_ci_configuration_host.rst
new file mode 100644
index 0000000..a44ae961
--- /dev/null
+++ b/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:UPDATE
+: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
+
+ PUT /api/4.0/oc/ci/configuration/example.com HTTP/1.1
+ Host: trafficops.infra.ciab.test
+ User-Agent: curl/7.47.0
+ Accept: */*
+ Cookie: mojolicious=...
+ Content-Length: 181
+ Content-Type: application/json
+
+ {
+ "type": "MI.HostMetadata",
+ "host-metadata": [
+ {
+ "generic-metadata-type": "MI.RequestedCapacityLimits",
+ "generic-metadata-value": {
+ "requested-limits": [
+ {
+ "limit-type": "egress",
+ "limit-value": 20000,
+ "footprints": [
+ {
+ "footprint-type": "ipv4cidr",
+ "footprint-value": [
+ "127.0.0.1",
+ "127.0.0.2"
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ }
+ ]
+ }
+
+Response Structure
+------------------
+
+.. code-block:: http
+ :caption: Response Example
+
+ HTTP/1.1 202 Accepted
+ Content-Type: application/json
+
+ { "alerts": [
+ {
+ "text": "CDNi configuration update request received. Status updates can be found here: /api/4.0/async_status/1",
+ "level": "success"
+ }
+ ]}
diff --git a/docs/source/api/v4/oc_ci_configuration_request_id_approved.rst b/docs/source/api/v4/oc_ci_configuration_request_id_approved.rst
new file mode 100644
index 0000000..c709871
--- /dev/null
+++ b/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:ADMIN
+:Response Type: Object
+
+Request Structure
+-----------------
+.. table:: Request Path Parameters
+
+ +-----------+----------------------------------------------------------------------------------------+
+ | Name | Description |
+ +===========+========================================================================================+
+ | id | The integral identifier for the configuration update request to be approved or denied. |
+ +-----------+----------------------------------------------------------------------------------------+
+ | approved | A boolean for whether to approve a configuration change request or not. |
+ +-----------+----------------------------------------------------------------------------------------+
+
+Response Structure
+------------------
+
+.. code-block:: http
+ :caption: Response Example For Approved Change
+
+ HTTP/1.1 200 OK
+ Content-Type: application/json
+
+ { "response": "Successfully updated configuration." }
+
+.. code-block:: http
+ :caption: Response Example For Denied Change
+
+ HTTP/1.1 200 OK
+ Content-Type: application/json
+
+ { "response": "Successfully denied configuration update request." }
diff --git a/experimental/traffic_router_golang/httpsrvr/httpsrvr.go b/experimental/traffic_router_golang/httpsrvr/httpsrvr.go
index a535211..785ce2d 100644
--- a/experimental/traffic_router_golang/httpsrvr/httpsrvr.go
+++ b/experimental/traffic_router_golang/httpsrvr/httpsrvr.go
@@ -34,6 +34,7 @@ import (
"github.com/apache/trafficcontrol/experimental/traffic_router_golang/nextcache"
"github.com/apache/trafficcontrol/lib/go-log"
+ "github.com/apache/trafficcontrol/lib/go-rfc"
"github.com/apache/trafficcontrol/lib/go-tc"
)
@@ -146,7 +147,7 @@ func getHandler(
newURL += "?" + r.URL.RawQuery
}
- w.Header().Add("Location", newURL)
+ w.Header().Add(rfc.Location, newURL)
w.WriteHeader(http.StatusFound)
}
}
diff --git a/lib/go-rfc/http.go b/lib/go-rfc/http.go
index 2ae1065..08fa281 100644
--- a/lib/go-rfc/http.go
+++ b/lib/go-rfc/http.go
@@ -41,6 +41,7 @@ const (
UserAgent = "User-Agent" // RFC7231§5.5.3
Vary = "Vary" // RFC7231§7.1.4
Age = "Age" // RFC7234§5.1
+ Location = "Location" // RFC7231§7.1.2
)
// These are (some) valid values for content encoding and MIME types, for
diff --git a/traffic_ops/app/db/migrations/2022020114365100_capacity_updates_queue.down.sql b/traffic_ops/app/db/migrations/2022020114365100_capacity_updates_queue.down.sql
new file mode 100644
index 0000000..381453a
--- /dev/null
+++ b/traffic_ops/app/db/migrations/2022020114365100_capacity_updates_queue.down.sql
@@ -0,0 +1,18 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. The ASF
+ * licenses this file to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+DROP TABLE IF EXISTS cdni_capability_updates;
diff --git a/traffic_ops/app/db/migrations/2022020114365100_capacity_updates_queue.up.sql b/traffic_ops/app/db/migrations/2022020114365100_capacity_updates_queue.up.sql
new file mode 100644
index 0000000..7ea094f
--- /dev/null
+++ b/traffic_ops/app/db/migrations/2022020114365100_capacity_updates_queue.up.sql
@@ -0,0 +1,28 @@
+/*
+ * 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.
+ */
+
+CREATE TABLE IF NOT EXISTS cdni_capability_updates (
+ id bigserial NOT NULL,
+ request_type text NOT NULL,
+ ucdn text NOT NULL,
+ host text,
+ data json NOT NULL,
+ async_status_id bigint NOT NULL,
+ last_updated timestamp with time zone DEFAULT now() NOT NULL,
+ CONSTRAINT pk_cdni_capability_updates PRIMARY KEY (id),
+ CONSTRAINT fk_cdni_capability_updates_async FOREIGN KEY (async_status_id) REFERENCES async_status(id) ON UPDATE CASCADE ON DELETE CASCADE
+);
diff --git a/traffic_ops/traffic_ops_golang/cdn/dnssecrefresh.go b/traffic_ops/traffic_ops_golang/cdn/dnssecrefresh.go
index 4db4e24..25c10ff 100644
--- a/traffic_ops/traffic_ops_golang/cdn/dnssecrefresh.go
+++ b/traffic_ops/traffic_ops_golang/cdn/dnssecrefresh.go
@@ -30,6 +30,7 @@ import (
"time"
"github.com/apache/trafficcontrol/lib/go-log"
+ "github.com/apache/trafficcontrol/lib/go-rfc"
"github.com/apache/trafficcontrol/lib/go-tc"
"github.com/apache/trafficcontrol/lib/go-util"
"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/api"
@@ -63,7 +64,7 @@ func RefreshDNSSECKeysV4(w http.ResponseWriter, r *http.Request) {
if started {
message = "Starting DNSSEC key refresh in the background. This may take a few minutes. Status updates can be found here: " + api.CurrentAsyncEndpoint + strconv.Itoa(asyncStatusID)
}
- w.Header().Add("Location", api.CurrentAsyncEndpoint+strconv.Itoa(asyncStatusID))
+ w.Header().Add(rfc.Location, api.CurrentAsyncEndpoint+strconv.Itoa(asyncStatusID))
api.WriteAlerts(w, r, http.StatusAccepted, tc.CreateAlerts(tc.SuccessLevel, message))
}
diff --git a/traffic_ops/traffic_ops_golang/cdni/shared.go b/traffic_ops/traffic_ops_golang/cdni/shared.go
index 83dee5f..bcf3161 100644
--- a/traffic_ops/traffic_ops_golang/cdni/shared.go
+++ b/traffic_ops/traffic_ops_golang/cdni/shared.go
@@ -21,34 +21,50 @@ package cdni
import (
"database/sql"
+ "encoding/json"
"errors"
"fmt"
- "github.com/lib/pq"
"net/http"
+ "strconv"
+ "strings"
"time"
"github.com/apache/trafficcontrol/lib/go-log"
+ "github.com/apache/trafficcontrol/lib/go-rfc"
+ "github.com/apache/trafficcontrol/lib/go-tc"
"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/api"
"github.com/dgrijalva/jwt-go"
+ "github.com/lib/pq"
)
-const CapabilityQuery = `SELECT id, type, ucdn FROM cdni_capabilities WHERE type = $1 AND ucdn = $2`
-const AllFootprintQuery = `SELECT footprint_type, footprint_value::text[], capability_id FROM cdni_footprints`
+const (
+ CapabilityQuery = `SELECT id, type, ucdn FROM cdni_capabilities WHERE type = $1 AND ucdn = $2`
+ AllFootprintQuery = `SELECT footprint_type, footprint_value::text[], capability_id FROM cdni_footprints`
-const totalLimitsQuery = `
+ totalLimitsQuery = `
SELECT limit_type, maximum_hard, maximum_soft, ctl.telemetry_id, ctl.telemetry_metric, t.id, t.type, tm.name, ctl.capability_id
FROM cdni_total_limits AS ctl
LEFT JOIN cdni_telemetry as t ON telemetry_id = t.id
LEFT JOIN cdni_telemetry_metrics as tm ON telemetry_metric = tm.name`
-const hostLimitsQuery = `
+ hostLimitsQuery = `
SELECT limit_type, maximum_hard, maximum_soft, chl.telemetry_id, chl.telemetry_metric, t.id, t.type, tm.name, host, chl.capability_id
FROM cdni_host_limits AS chl
LEFT JOIN cdni_telemetry as t ON telemetry_id = t.id
LEFT JOIN cdni_telemetry_metrics as tm ON telemetry_metric = tm.name
ORDER BY host DESC`
+ InsertCapabilityUpdateQuery = `INSERT INTO cdni_capability_updates (ucdn, data, async_status_id, request_type, host) VALUES ($1, $2, $3, $4, $5)`
+ SelectCapabilityUpdateQuery = `SELECT ucdn, data, async_status_id, request_type, host FROM cdni_capability_updates WHERE id = $1`
+ DeleteCapabilityUpdateQuery = `DELETE FROM cdni_capability_updates WHERE id = $1`
+ UpdateTotalLimitsByCapabilityAndLimitTypeQuery = `UPDATE cdni_total_limits SET maximum_hard = $1 WHERE capability_id = $2 AND limit_type = $3`
+ UpdateHostLimitsByCapabilityAndLimitTypeQuery = `UPDATE cdni_host_limits SET maximum_hard = $1 WHERE capability_id = $2 AND limit_type = $3 AND host = $4`
+ hostQuery = `SELECT count(*) FROM cdni_host_limits WHERE host = $1`
+
+ hostConfigLabel = "hostConfigUpdate"
+)
+
func GetCapabilities(w http.ResponseWriter, r *http.Request) {
inf, userErr, sysErr, errCode := api.NewInfo(r, nil, nil)
if userErr != nil || sysErr != nil {
@@ -57,9 +73,50 @@ func GetCapabilities(w http.ResponseWriter, r *http.Request) {
}
defer inf.Close()
- bearerToken := r.Header.Get("Authorization")
- if bearerToken == "" {
- api.HandleErr(w, r, inf.Tx.Tx, http.StatusBadRequest, errors.New("bearer token header is required"), nil)
+ 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(r.Header.Get("Authorization"), 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
}
@@ -68,17 +125,365 @@ func GetCapabilities(w http.ResponseWriter, r *http.Request) {
return
}
+ ucdn, err := checkBearerToken(r.Header.Get("Authorization"), 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
+ }
+ logTx, err := db.Begin()
+ if err != nil {
+ api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("getting log tx: %w", err))
+ return
+ }
+ defer logTx.Commit()
+
+ 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
+ }
+
+ 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
+ }
+
+ msg := "CDNi configuration update request received. Status updates can be found here: " + api.CurrentAsyncEndpoint + strconv.Itoa(asyncStatusId)
+ api.CreateChangeLogRawTx(api.ApiChange, msg, inf.User, logTx)
+
+ var alerts tc.Alerts
+ alerts.AddAlert(tc.Alert{
+ Text: msg,
+ Level: tc.SuccessLevel.String(),
+ })
+
+ w.Header().Add(rfc.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()
+
+ 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(r.Header.Get("Authorization"), 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
+ }
+ logTx, err := db.Begin()
+ if err != nil {
+ api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("getting log tx: %w", err))
+ return
+ }
+ defer logTx.Commit()
+
+ 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
+ }
+
+ msg := "CDNi configuration update request received. Status updates can be found here: " + api.CurrentAsyncEndpoint + strconv.Itoa(asyncStatusId)
+ api.CreateChangeLogRawTx(api.ApiChange, msg, inf.User, logTx)
+ var alerts tc.Alerts
+ alerts.AddAlert(tc.Alert{
+ Text: msg,
+ Level: tc.SuccessLevel.String(),
+ })
+
+ w.Header().Add(rfc.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
+ }
+
+ logTx, err := db.Begin()
+ if err != nil {
+ api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("getting log tx: %w", err))
+ return
+ }
+ defer logTx.Commit()
+
+ 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 log.Close(rows, "closing capabilities update query")
+ 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 %d: %s", asyncId, asyncErr.Error())
+ }
+ 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
+ }
+ msg := "Successfully denied configuration update request."
+ api.CreateChangeLogRawTx(api.ApiChange, msg, inf.User, inf.Tx.Tx)
+ api.WriteResp(w, r, msg)
+ 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
+ }
+ msg := "Successfully updated configuration."
+ api.CreateChangeLogRawTx(api.ApiChange, msg, inf.User, logTx)
+ api.WriteResp(w, r, msg)
+}
+
+func getCapabilityIdFromFootprints(updatedData CapacityLimit, ucdn string, inf *api.APIInfo) (int, error) {
+ tableAbbr := ""
+ selectClause := ""
+ whereClause := ""
+ var queryParams []interface{}
+ paramCount := 1
+
+ for i, footprint := range updatedData.Footprints {
+ if i == 0 {
+ tableAbbr = "f"
+ selectClause = "SELECT " + tableAbbr + ".capability_id FROM cdni_footprints as " + tableAbbr
+ whereClause = " WHERE " + tableAbbr + ".ucdn = $" + strconv.Itoa(paramCount) + " AND " + tableAbbr + ".footprint_type = $" + strconv.Itoa(paramCount+1) + " AND " + tableAbbr + ".footprint_value = $" + strconv.Itoa(paramCount+2) + "::text[]"
+ } else {
+ oldTableAbbr := tableAbbr
+ tableAbbr = tableAbbr + "f"
+ selectClause = selectClause + " JOIN cdni_footprints as " + tableAbbr + " on " + tableAbbr + ".capability_id = " + oldTableAbbr + ".capability_id"
+ whereClause = whereClause + " AND " + tableAbbr + ".ucdn = $" + strconv.Itoa(paramCount) + " AND " + tableAbbr + ".footprint_type = $" + strconv.Itoa(paramCount+1) + " AND " + tableAbbr + ".footprint_value = $" + strconv.Itoa(paramCount+2) + "::text[]"
+ }
+ paramCount = paramCount + 3
+ queryParams = append(queryParams, ucdn)
+ queryParams = append(queryParams, footprint.FootprintType)
+ queryParams = append(queryParams, pq.Array(footprint.FootprintValue))
+ }
+
+ selectQuery := selectClause + whereClause + " AND (SELECT count(*) from cdni_footprints as c where c.capability_id = f.capability_id) = " + strconv.Itoa(len(updatedData.Footprints))
+ rows, err := inf.Tx.Tx.Query(selectQuery, queryParams...)
+ if err != nil {
+ return 0, fmt.Errorf("querying for capacity update request: %w", err)
+ }
+ defer log.Close(rows, "closing footprints query")
+ var capabilityIds []int
+ rowCount := 0
+ for rows.Next() {
+ var capabilityId int
+ if err := rows.Scan(&capabilityId); err != nil {
+ return 0, fmt.Errorf("scanning db rows: %w", err)
+ }
+ rowCount++
+ capabilityIds = append(capabilityIds, capabilityId)
+ }
+
+ if len(capabilityIds) == 0 {
+ return 0, fmt.Errorf("no capabilities found that match all footprints: %v", updatedData.Footprints)
+ }
+ if len(capabilityIds) > 1 {
+ return 0, fmt.Errorf("more than 1 capability found that match all footprints: %v", updatedData.Footprints)
+ }
+ return capabilityIds[0], nil
+}
+
+func deleteCapabilityRequest(reqId int, tx *sql.Tx) (int, error) {
+ result, err := tx.Exec(DeleteCapabilityUpdateQuery, reqId)
+ if err != nil {
+ return http.StatusInternalServerError, fmt.Errorf("deleting configuration update: %w", err)
+ }
+
+ if rowsAffected, err := result.RowsAffected(); err != nil {
+ return http.StatusInternalServerError, fmt.Errorf("deleting configuration update: getting rows affected: %w", err)
+ } else if rowsAffected < 1 {
+ return http.StatusNotFound, errors.New("no configuration update with that key found")
+ } else if rowsAffected > 1 {
+ return http.StatusInternalServerError, fmt.Errorf("delete affected too many rows: %d", rowsAffected)
+ }
+
+ return http.StatusOK, nil
+}
+
+func validateHostExists(host string, tx *sql.Tx) (int, error, error) {
+ count := 0
+ if err := tx.QueryRow(hostQuery, host).Scan(&count); err != nil {
+ return http.StatusInternalServerError, nil, fmt.Errorf("querying if host %s exists: %w", host, err)
+ }
+ if count == 0 {
+ return http.StatusBadRequest, fmt.Errorf("No data found for host: %s", host), nil
+ }
+ return http.StatusOK, nil, nil
+}
+
+func checkBearerToken(bearerToken string, inf *api.APIInfo) (string, error) {
+ if bearerToken == "" {
+ return "", errors.New("bearer token header is required")
+ }
+
claims := jwt.MapClaims{}
token, err := jwt.ParseWithClaims(bearerToken, claims, func(token *jwt.Token) (interface{}, error) {
return []byte(inf.Config.Cdni.JwtDecodingSecret), nil
})
if err != nil {
- api.HandleErr(w, r, nil, http.StatusInternalServerError, fmt.Errorf("parsing claims: %w", err), nil)
- return
+ return "", fmt.Errorf("parsing claims: %w", err)
}
if !token.Valid {
- api.HandleErr(w, r, nil, http.StatusInternalServerError, errors.New("invalid token"), nil)
- return
+ return "", errors.New("invalid token")
}
var expirationFloat float64
@@ -88,20 +493,17 @@ func GetCapabilities(w http.ResponseWriter, r *http.Request) {
switch key {
case "iss":
if _, ok := val.(string); !ok {
- api.HandleErr(w, r, nil, http.StatusBadRequest, errors.New("invalid token - iss (Issuer) must be a string"), nil)
- return
+ return "", errors.New("invalid token - iss (Issuer) must be a string")
}
ucdn = val.(string)
case "aud":
if _, ok := val.(string); !ok {
- api.HandleErr(w, r, nil, http.StatusBadRequest, errors.New("invalid token - aud (Audience) must be a string"), nil)
- return
+ return "", errors.New("invalid token - aud (Audience) must be a string")
}
dcdn = val.(string)
case "exp":
if _, ok := val.(float64); !ok {
- api.HandleErr(w, r, nil, http.StatusBadRequest, errors.New("invalid token - exp (Expiration) must be a float64"), nil)
- return
+ return "", errors.New("invalid token - exp (Expiration) must be a float64")
}
expirationFloat = val.(float64)
}
@@ -110,38 +512,16 @@ func GetCapabilities(w http.ResponseWriter, r *http.Request) {
expiration := int64(expirationFloat)
if expiration < time.Now().Unix() {
- api.HandleErr(w, r, nil, http.StatusForbidden, errors.New("token is expired"), nil)
- return
+ return "", errors.New("token is expired")
}
if dcdn != inf.Config.Cdni.DCdnId {
- api.HandleErr(w, r, nil, http.StatusForbidden, errors.New("invalid token - incorrect dcdn"), nil)
- return
+ return "", errors.New("invalid token - incorrect dcdn")
}
if ucdn == "" {
- api.HandleErr(w, r, nil, http.StatusForbidden, errors.New("invalid token - empty ucdn field"), nil)
- return
- }
-
- capacities, err := getCapacities(inf, ucdn)
- if err != nil {
- api.HandleErr(w, r, nil, http.StatusInternalServerError, err, nil)
- return
- }
-
- telemetries, err := getTelemetries(inf, ucdn)
- if err != nil {
- api.HandleErr(w, r, nil, http.StatusInternalServerError, err, nil)
- return
+ return "", errors.New("invalid token - empty ucdn field")
}
- 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)
+ return ucdn, nil
}
func getFootprintMap(tx *sql.Tx) (map[int][]Footprint, error) {
@@ -318,6 +698,20 @@ const (
FciCapacityLimits = "FCI.CapacityLimits"
)
+type SupportedGenericMetadataType string
+
+const (
+ MiRequestedCapacityLimits SupportedGenericMetadataType = "MI.RequestedCapacityLimits"
+)
+
+func (s SupportedGenericMetadataType) isValid() bool {
+ switch s {
+ case MiRequestedCapacityLimits:
+ return true
+ }
+ return false
+}
+
type TelemetrySourceType string
const (
@@ -332,3 +726,33 @@ const (
Asn = "asn"
CountryCode = "countrycode"
)
+
+type GenericHostMetadata struct {
+ Host string `json:"host"`
+ HostMetadata HostMetadataList `json:"host-metadata"`
+}
+
+type GenericRequestMetadata struct {
+ Type string `json:"type"`
+ Metadata json.RawMessage `json:"metadata"`
+ Host string `json:"host,omitempty"`
+}
+
+type HostMetadataList struct {
+ Metadata json.RawMessage `json:"metadata"`
+}
+
+type GenericMetadata struct {
+ Type SupportedGenericMetadataType `json:"generic-metadata-type"`
+ Value json.RawMessage `json:"generic-metadata-value"`
+}
+
+type CapacityRequestedLimits struct {
+ RequestedLimits []CapacityLimit `json:"requested-limits"`
+}
+
+type CapacityLimit struct {
+ LimitType string `json:"limit-type"`
+ LimitValue int64 `json:"limit-value"`
+ Footprints []Footprint `json:"footprints"`
+}
diff --git a/traffic_ops/traffic_ops_golang/deliveryservice/acme.go b/traffic_ops/traffic_ops_golang/deliveryservice/acme.go
index e89b8e6..4483248 100644
--- a/traffic_ops/traffic_ops_golang/deliveryservice/acme.go
+++ b/traffic_ops/traffic_ops_golang/deliveryservice/acme.go
@@ -36,6 +36,7 @@ import (
"time"
"github.com/apache/trafficcontrol/lib/go-log"
+ "github.com/apache/trafficcontrol/lib/go-rfc"
"github.com/apache/trafficcontrol/lib/go-tc"
"github.com/apache/trafficcontrol/lib/go-util"
"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/api"
@@ -228,7 +229,7 @@ func GenerateAcmeCertificates(w http.ResponseWriter, r *http.Request) {
Level: tc.SuccessLevel.String(),
})
- w.Header().Add("Location", api.CurrentAsyncEndpoint+strconv.Itoa(asyncStatusId))
+ w.Header().Add(rfc.Location, api.CurrentAsyncEndpoint+strconv.Itoa(asyncStatusId))
api.WriteAlerts(w, r, http.StatusAccepted, alerts)
}
@@ -321,7 +322,7 @@ func GenerateLetsEncryptCertificates(w http.ResponseWriter, r *http.Request) {
Level: tc.SuccessLevel.String(),
})
- w.Header().Add("Location", api.CurrentAsyncEndpoint+strconv.Itoa(asyncStatusId))
+ w.Header().Add(rfc.Location, api.CurrentAsyncEndpoint+strconv.Itoa(asyncStatusId))
api.WriteAlerts(w, r, http.StatusAccepted, alerts)
}
diff --git a/traffic_ops/traffic_ops_golang/deliveryservice/autorenewcerts.go b/traffic_ops/traffic_ops_golang/deliveryservice/autorenewcerts.go
index 85aa3cf..ea6108a 100644
--- a/traffic_ops/traffic_ops_golang/deliveryservice/autorenewcerts.go
+++ b/traffic_ops/traffic_ops_golang/deliveryservice/autorenewcerts.go
@@ -28,6 +28,7 @@ import (
"time"
"github.com/apache/trafficcontrol/lib/go-log"
+ "github.com/apache/trafficcontrol/lib/go-rfc"
"github.com/apache/trafficcontrol/lib/go-tc"
"github.com/apache/trafficcontrol/lib/go-util"
"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/api"
@@ -135,7 +136,7 @@ func renewCertificates(w http.ResponseWriter, r *http.Request, deprecated bool)
Level: tc.SuccessLevel.String(),
})
- w.Header().Add("Location", api.CurrentAsyncEndpoint+strconv.Itoa(asyncStatusId))
+ w.Header().Add(rfc.Location, api.CurrentAsyncEndpoint+strconv.Itoa(asyncStatusId))
api.WriteAlerts(w, r, http.StatusAccepted, alerts)
}
diff --git a/traffic_ops/traffic_ops_golang/routing/routes.go b/traffic_ops/traffic_ops_golang/routing/routes.go
index 8252faa..7915524 100644
--- a/traffic_ops/traffic_ops_golang/routing/routes.go
+++ b/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:UPDATE"}, 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:UPDATE"}, 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:ADMIN"}, Authenticated: Authenticated, Middlewares: nil, ID: 541357729080},
// SSL Keys
{Version: api.Version{Major: 4, Minor: 0}, Method: http.MethodGet, Path: `sslkey_expirations/?$`, Handler: deliveryservice.GetSSlKeyExpirationInformation, RequiredPrivLevel: auth.PrivLevelAdmin, RequiredPermissions: []string{"SSL-KEY-EXPIRATION:READ"}, Authenticated: Authenticated, Middlewares: nil, ID: 41357729075},