You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@trafficcontrol.apache.org by sr...@apache.org on 2022/10/18 17:26:47 UTC

[trafficcontrol] branch master updated: Override all Delivery Service TTLs in a CDN's Snapshot with a given value (#7143)

This is an automated email from the ASF dual-hosted git repository.

srijeet0406 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 3aaf465079 Override all Delivery Service TTLs in a CDN's Snapshot with a given value (#7143)
3aaf465079 is described below

commit 3aaf4650795f64c3562639a08a88f4428fea961b
Author: Zach Hoffman <zr...@apache.org>
AuthorDate: Tue Oct 18 17:26:41 2022 +0000

    Override all Delivery Service TTLs in a CDN's Snapshot with a given value (#7143)
    
    * CDN TTLOverride field
    
    * Override all Delivery Service TTLs in a CDN's Snapshot with given value
    
    * Only use TTLOverride field in API version 4.1 and up
    
    * Key fields
    
    * Correct validation message
    
    * Use math.Min() for comparisons
    
    * Remove duplicate word
    
    * Use abbreviation for `CDN`, not glossary
    
    * Refactor version-sensitive formatted query strings
    
    * Reword
    
    * Use Math.Min() in more places
---
 CHANGELOG.md                                       |  1 +
 docs/source/api/v4/cdns.rst                        |  8 +-
 docs/source/api/v5/cdns.rst                        |  8 +-
 lib/go-tc/cdns.go                                  |  8 ++
 lib/go-tc/crconfig.go                              |  3 +
 ...22101700451329_add_ttl_override_to_cdn.down.sql | 19 +++++
 ...2022101700451329_add_ttl_override_to_cdn.up.sql | 19 +++++
 traffic_ops/traffic_ops_golang/api/api.go          |  8 ++
 traffic_ops/traffic_ops_golang/cdn/cdns.go         | 66 ++++++++++++---
 traffic_ops/traffic_ops_golang/cdn/cdns_test.go    | 16 ++--
 .../traffic_ops_golang/crconfig/crconfig.go        |  4 +-
 .../traffic_ops_golang/crconfig/deliveryservice.go | 48 +++++++++--
 .../crconfig/deliveryservice_test.go               | 95 ++++++++++++++++------
 traffic_ops/traffic_ops_golang/crconfig/servers.go | 10 ++-
 .../traffic_ops_golang/crconfig/servers_test.go    | 19 +++--
 .../src/common/modules/form/cdn/form.cdn.tpl.html  | 13 +++
 traffic_portal/test/integration/Data/cdn.ts        | 24 +++++-
 .../test/integration/PageObjects/CDNPage.po.ts     | 19 ++++-
 18 files changed, 314 insertions(+), 74 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 313b5e3f8b..1007e96db9 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -17,6 +17,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/).
 - [#6033](https://github.com/apache/trafficcontrol/issues/6033) *Traffic Ops, Traffic Portal* Added ability to assign multiple server capabilities to a server.
 - [#7032](https://github.com/apache/trafficcontrol/issues/7032) *Cache Config* Add t3c-apply flag to use local ATS version for config generation rather than Server package Parameter, to allow managing the ATS OS package via external tools. See 'man t3c-apply' and 'man t3c-generate' for details.
 - [#7097](https://github.com/apache/trafficcontrol/issues/7097) *Traffic Ops, Traffic Portal, t3c* Added the `regional` field to Delivery Services, which affects whether `maxOriginConnections` should be per Cache Group
+- [#2388](https://github.com/apache/trafficcontrol/issues/2388) *Trafic Ops, Traffic Portal* Added the `TTLOverride` field to CDNs, which lets you override all TTLs in all Delivery Services of a CDN's snapshot with a single value
 
 ### Changed
 - [#7063](https://github.com/apache/trafficcontrol/pull/7063) *Traffic Ops* Python client now uses Traffic Ops API 4.1 by default.
diff --git a/docs/source/api/v4/cdns.rst b/docs/source/api/v4/cdns.rst
index 8e13605df5..9726b9c439 100644
--- a/docs/source/api/v4/cdns.rst
+++ b/docs/source/api/v4/cdns.rst
@@ -67,6 +67,7 @@ Response Structure
 :id:            The integral, unique identifier for the CDN
 :lastUpdated:   Date and time when the CDN was last modified in :ref:`non-rfc-datetime`
 :name:          The name of the CDN
+:ttlOverride:	A :abbr:`TTL (Time To Live)` value, in seconds, that, if set, overrides all set TTL values on :term:`Delivery Services` in this :abbr:`CDN (Content Delivery Network)`
 
 .. code-block:: http
 	:caption: Response Example
@@ -89,7 +90,8 @@ Response Structure
 			"domainName": "-",
 			"id": 1,
 			"lastUpdated": "2018-11-14 18:21:06+00",
-			"name": "ALL"
+			"name": "ALL",
+			"ttlOverride": 60
 		},
 		{
 			"dnssecEnabled": false,
@@ -115,6 +117,7 @@ Request Structure
 :dnssecEnabled: If ``true``, this CDN will use DNSSEC, if ``false`` it will not
 :domainName:    The top-level domain (TLD) belonging to the new CDN
 :name:          Name of the new CDN
+:ttlOverride:	A :abbr:`TTL (Time To Live)` value, in seconds, that, if set, overrides all set TTL values on :term:`Delivery Services` in this :abbr:`CDN (Content Delivery Network)`
 
 .. code-block:: http
 	:caption: Request Structure
@@ -135,6 +138,7 @@ Response Structure
 :domainName:    The top-level domain (TLD) assigned to the newly created CDN
 :id:            An integral, unique identifier for the newly created CDN
 :name:          The newly created CDN's name
+:ttlOverride:	A :abbr:`TTL (Time To Live)` value, in seconds, that, if set, overrides all set TTL values on :term:`Delivery Services` in this :abbr:`CDN (Content Delivery Network)`
 
 
 .. code-block:: http
@@ -163,5 +167,5 @@ Response Structure
 		"domainName": "quest",
 		"id": 3,
 		"lastUpdated": "2018-11-14 20:49:28+00",
-		"name": "test"
+		"name": "test",
 	}}
diff --git a/docs/source/api/v5/cdns.rst b/docs/source/api/v5/cdns.rst
index d76df84255..5791859b19 100644
--- a/docs/source/api/v5/cdns.rst
+++ b/docs/source/api/v5/cdns.rst
@@ -67,6 +67,7 @@ Response Structure
 :id:            The integral, unique identifier for the CDN
 :lastUpdated:   Date and time when the CDN was last modified in :ref:`non-rfc-datetime`
 :name:          The name of the CDN
+:ttlOverride:	A :abbr:`TTL (Time To Live)` value, in seconds, that, if set, overrides all set TTL values on :term:`Delivery Services` in this :abbr:`CDN (Content Delivery Network)`
 
 .. code-block:: http
 	:caption: Response Example
@@ -89,7 +90,8 @@ Response Structure
 			"domainName": "-",
 			"id": 1,
 			"lastUpdated": "2018-11-14 18:21:06+00",
-			"name": "ALL"
+			"name": "ALL",
+			"ttlOverride": 60
 		},
 		{
 			"dnssecEnabled": false,
@@ -115,6 +117,7 @@ Request Structure
 :dnssecEnabled: If ``true``, this CDN will use DNSSEC, if ``false`` it will not
 :domainName:    The top-level domain (TLD) belonging to the new CDN
 :name:          Name of the new CDN
+:ttlOverride:	A :abbr:`TTL (Time To Live)` value, in seconds, that, if set, overrides all set TTL values on :term:`Delivery Services` in this :abbr:`CDN (Content Delivery Network)`
 
 .. code-block:: http
 	:caption: Request Structure
@@ -135,6 +138,7 @@ Response Structure
 :domainName:    The top-level domain (TLD) assigned to the newly created CDN
 :id:            An integral, unique identifier for the newly created CDN
 :name:          The newly created CDN's name
+:ttlOverride:	A :abbr:`TTL (Time To Live)` value, in seconds, that, if set, overrides all set TTL values on :term:`Delivery Services` in this :abbr:`CDN (Content Delivery Network)`
 
 
 .. code-block:: http
@@ -163,5 +167,5 @@ Response Structure
 		"domainName": "quest",
 		"id": 3,
 		"lastUpdated": "2018-11-14 20:49:28+00",
-		"name": "test"
+		"name": "test",
 	}}
diff --git a/lib/go-tc/cdns.go b/lib/go-tc/cdns.go
index 9a377bb472..8fe0f70f4c 100644
--- a/lib/go-tc/cdns.go
+++ b/lib/go-tc/cdns.go
@@ -71,6 +71,10 @@ type CDN struct {
 	//
 	// required: true
 	Name string `json:"name" db:"name"`
+
+	// TTLOverride
+	//
+	TTLOverride int `json:"ttlOverride,omitempty" db:"ttl_override"`
 }
 
 // CDNNullable is identical to CDN except that its fields are reference values,
@@ -102,6 +106,10 @@ type CDNNullable struct {
 	//
 	// required: true
 	Name *string `json:"name" db:"name"`
+
+	// TTLOverride
+	//
+	TTLOverride *int `json:"ttlOverride,omitempty" db:"ttl_override"`
 }
 
 // CDNSSLKeysResponse is the structure of the Traffic Ops API's response to
diff --git a/lib/go-tc/crconfig.go b/lib/go-tc/crconfig.go
index eaa226a5f2..291d8d8505 100644
--- a/lib/go-tc/crconfig.go
+++ b/lib/go-tc/crconfig.go
@@ -58,6 +58,9 @@ type CRConfigConfig struct {
 	ZoneManagerThreadpoolScale                 *string      `json:"zonemanager.threadpool.scale,omitempty"`
 }
 
+const CrConfigTTLSOA = "tld.ttls.SOA"
+const CrConfigTTLNS = "tld.ttls.NS"
+
 // CRConfigTTL defines the Time-To-Live (TTL) of various record types served by
 // Traffic Router.
 //
diff --git a/traffic_ops/app/db/migrations/2022101700451329_add_ttl_override_to_cdn.down.sql b/traffic_ops/app/db/migrations/2022101700451329_add_ttl_override_to_cdn.down.sql
new file mode 100644
index 0000000000..1640cc53b6
--- /dev/null
+++ b/traffic_ops/app/db/migrations/2022101700451329_add_ttl_override_to_cdn.down.sql
@@ -0,0 +1,19 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership.  The ASF
+ * licenses this file to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+ALTER TABLE public.cdn
+DROP COLUMN ttl_override;
diff --git a/traffic_ops/app/db/migrations/2022101700451329_add_ttl_override_to_cdn.up.sql b/traffic_ops/app/db/migrations/2022101700451329_add_ttl_override_to_cdn.up.sql
new file mode 100644
index 0000000000..99d1db790d
--- /dev/null
+++ b/traffic_ops/app/db/migrations/2022101700451329_add_ttl_override_to_cdn.up.sql
@@ -0,0 +1,19 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership.  The ASF
+ * licenses this file to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+ALTER TABLE public.cdn
+ADD COLUMN ttl_override BIGINT DEFAULT NULL;
diff --git a/traffic_ops/traffic_ops_golang/api/api.go b/traffic_ops/traffic_ops_golang/api/api.go
index 0ca5d1df9c..d1e8a9d783 100644
--- a/traffic_ops/traffic_ops_golang/api/api.go
+++ b/traffic_ops/traffic_ops_golang/api/api.go
@@ -799,6 +799,14 @@ type Version struct {
 	Minor uint64
 }
 
+func (v *Version) LessThan(otherVersion *Version) bool {
+	return v.Major < otherVersion.Major || (v.Major == otherVersion.Major && v.Minor < otherVersion.Minor)
+}
+
+func (v *Version) GreaterThanOrEqualTo(otherVersion *Version) bool {
+	return !v.LessThan(otherVersion)
+}
+
 // GetRequestedAPIVersion returns a pointer to the requested API Version from the request if it exists or returns nil otherwise.
 func GetRequestedAPIVersion(path string) *Version {
 	pathParts := strings.Split(path, "/")
diff --git a/traffic_ops/traffic_ops_golang/cdn/cdns.go b/traffic_ops/traffic_ops_golang/cdn/cdns.go
index 51a357a96d..9b0e44954d 100644
--- a/traffic_ops/traffic_ops_golang/cdn/cdns.go
+++ b/traffic_ops/traffic_ops_golang/cdn/cdns.go
@@ -20,6 +20,7 @@ package cdn
  */
 
 import (
+	"fmt"
 	"net/http"
 	"strconv"
 	"strings"
@@ -53,18 +54,22 @@ func (v *TOCDN) SelectMaxLastUpdatedQuery(where, orderBy, pagination, tableName
 }
 
 func (v *TOCDN) SetLastUpdated(t tc.TimeNoMod) { v.LastUpdated = &t }
-func (v *TOCDN) InsertQuery() string           { return insertQuery() }
+func (v *TOCDN) InsertQuery() string           { return insertQuery(v.APIInfo().Version) }
 func (v *TOCDN) NewReadObj() interface{}       { return &tc.CDNNullable{} }
-func (v *TOCDN) SelectQuery() string           { return selectQuery() }
+func (v *TOCDN) SelectQuery() string           { return selectQuery(v.APIInfo().Version) }
 func (v *TOCDN) ParamColumns() map[string]dbhelpers.WhereColumnInfo {
-	return map[string]dbhelpers.WhereColumnInfo{
+	columnInfo := map[string]dbhelpers.WhereColumnInfo{
 		"domainName":    dbhelpers.WhereColumnInfo{Column: "domain_name"},
 		"dnssecEnabled": dbhelpers.WhereColumnInfo{Column: "dnssec_enabled"},
 		"id":            dbhelpers.WhereColumnInfo{Column: "id", Checker: api.IsInt},
 		"name":          dbhelpers.WhereColumnInfo{Column: "name"},
 	}
+	if v.APIInfo().Version.GreaterThanOrEqualTo(&api.Version{Major: 4, Minor: 1}) {
+		columnInfo["ttlOverride"] = dbhelpers.WhereColumnInfo{Column: "ttl_override", Checker: api.IsInt}
+	}
+	return columnInfo
 }
-func (v *TOCDN) UpdateQuery() string { return updateQuery() }
+func (v *TOCDN) UpdateQuery() string { return updateQuery(v.APIInfo().Version) }
 func (v *TOCDN) DeleteQuery() string { return deleteQuery() }
 
 func (cdn TOCDN) GetKeyFieldsInfo() []api.KeyFieldInfo {
@@ -128,11 +133,17 @@ func (cdn TOCDN) Validate() (error, error) {
 		"name":       validation.Validate(cdn.Name, validation.Required, validName),
 		"domainName": validation.Validate(cdn.DomainName, validation.Required, validDomainName),
 	}
+	if cdn.APIInfo().Version.GreaterThanOrEqualTo(&api.Version{Major: 4, Minor: 1}) {
+		errs["ttlOverride"] = validation.Validate(cdn.TTLOverride, validation.By(tovalidate.IsGreaterThanZero))
+	}
 	return util.JoinErrs(tovalidate.ToErrors(errs)), nil
 }
 
 func (cdn *TOCDN) Create() (error, error, int) {
 	*cdn.DomainName = strings.ToLower(*cdn.DomainName)
+	if cdn.APIInfo().Version.LessThan(&api.Version{Major: 4, Minor: 1}) {
+		cdn.TTLOverride = nil
+	}
 	return api.GenericCreate(cdn)
 }
 
@@ -149,6 +160,9 @@ func (cdn *TOCDN) Update(h http.Header) (error, error, int) {
 		}
 	}
 	*cdn.DomainName = strings.ToLower(*cdn.DomainName)
+	if cdn.APIInfo().Version.LessThan(&api.Version{Major: 4, Minor: 1}) {
+		cdn.TTLOverride = nil
+	}
 	return api.GenericUpdate(h, cdn)
 }
 
@@ -162,37 +176,65 @@ func (cdn *TOCDN) Delete() (error, error, int) {
 	return api.GenericDelete(cdn)
 }
 
-func selectQuery() string {
+func formatQueryByAPIVersion(apiVersion *api.Version, minimumAPIVersion *api.Version, queryFormatString string, columnStrs []string, lowAPIVersionColumnStrs []string) string {
+	if apiVersion.LessThan(&api.Version{Major: 4, Minor: 1}) {
+		for index, _ := range columnStrs {
+			columnStrs[index] = lowAPIVersionColumnStrs[index]
+		}
+	}
+	columnStrArgs := make([]interface{}, len(columnStrs))
+	for index, _ := range columnStrs {
+		columnStrArgs[index] = columnStrs[index]
+	}
+	query := fmt.Sprintf(queryFormatString, columnStrArgs...)
+	return query
+}
+
+func selectQuery(apiVersion *api.Version) string {
 	query := `SELECT
 dnssec_enabled,
 domain_name,
 id,
 last_updated,
+%s
 name
 
 FROM cdn c`
-	return query
+	return formatQueryByAPIVersion(apiVersion, &api.Version{Major: 4, Minor: 1}, query, []string{`
+			ttl_override,
+`}, []string{``})
 }
 
-func updateQuery() string {
+func updateQuery(apiVersion *api.Version) string {
 	query := `UPDATE
 cdn SET
 dnssec_enabled=:dnssec_enabled,
 domain_name=:domain_name,
 name=:name
+%s
 WHERE id=:id RETURNING last_updated`
-	return query
+	return formatQueryByAPIVersion(apiVersion, &api.Version{Major: 4, Minor: 1}, query, []string{`,
+ttl_override=:ttl_override
+`}, []string{``})
 }
 
-func insertQuery() string {
+func insertQuery(apiVersion *api.Version) string {
 	query := `INSERT INTO cdn (
 dnssec_enabled,
 domain_name,
-name) VALUES (
+name
+%s
+) VALUES (
 :dnssec_enabled,
 :domain_name,
-:name) RETURNING id,last_updated`
-	return query
+:name
+%s
+) RETURNING id,last_updated`
+	return formatQueryByAPIVersion(apiVersion, &api.Version{Major: 4, Minor: 1}, query, []string{`,
+ttl_override
+`, `,
+:ttl_override
+`}, []string{``, ``})
 }
 
 func deleteQuery() string {
diff --git a/traffic_ops/traffic_ops_golang/cdn/cdns_test.go b/traffic_ops/traffic_ops_golang/cdn/cdns_test.go
index baa10197c3..e4a3296251 100644
--- a/traffic_ops/traffic_ops_golang/cdn/cdns_test.go
+++ b/traffic_ops/traffic_ops_golang/cdn/cdns_test.go
@@ -42,6 +42,7 @@ func getTestCDNs() []tc.CDN {
 		ID:            1,
 		Name:          "cdn1",
 		LastUpdated:   tc.TimeNoMod{Time: time.Now()},
+		TTLOverride:   50,
 	}
 	cdns = append(cdns, testCDN)
 
@@ -75,13 +76,14 @@ func TestReadCDNs(t *testing.T) {
 			ts.ID,
 			ts.LastUpdated,
 			ts.Name,
+			ts.TTLOverride,
 		)
 	}
 	mock.ExpectBegin()
 	mock.ExpectQuery("SELECT").WillReturnRows(rows)
 	mock.ExpectCommit()
 
-	reqInfo := api.APIInfo{Tx: db.MustBegin(), Params: map[string]string{"dsId": "1"}}
+	reqInfo := api.APIInfo{Tx: db.MustBegin(), Params: map[string]string{"dsId": "1"}, Version: &api.Version{Major: 5, Minor: 0}}
 	obj := TOCDN{
 		api.APIInfoImpl{ReqInfo: &reqInfo},
 		tc.CDNNullable{},
@@ -97,13 +99,14 @@ func TestReadCDNs(t *testing.T) {
 }
 
 func TestFuncs(t *testing.T) {
-	if strings.Index(selectQuery(), "SELECT") != 0 {
+	apiVersion := &api.Version{Major: 4, Minor: 1}
+	if strings.Index(selectQuery(apiVersion), "SELECT") != 0 {
 		t.Errorf("expected selectQuery to start with SELECT")
 	}
-	if strings.Index(insertQuery(), "INSERT") != 0 {
+	if strings.Index(insertQuery(apiVersion), "INSERT") != 0 {
 		t.Errorf("expected insertQuery to start with INSERT")
 	}
-	if strings.Index(updateQuery(), "UPDATE") != 0 {
+	if strings.Index(updateQuery(apiVersion), "UPDATE") != 0 {
 		t.Errorf("expected updateQuery to start with UPDATE")
 	}
 	if strings.Index(deleteQuery(), "DELETE") != 0 {
@@ -135,7 +138,8 @@ func TestInterfaces(t *testing.T) {
 func TestValidate(t *testing.T) {
 	// invalid name, empty domainname
 	n := "not_a_valid_cdn"
-	c := TOCDN{CDNNullable: tc.CDNNullable{Name: &n}}
+	reqInfo := api.APIInfo{Tx: nil, Params: map[string]string{"dsId": "1"}, Version: &api.Version{Major: 5, Minor: 0}}
+	c := TOCDN{CDNNullable: tc.CDNNullable{Name: &n}, APIInfoImpl: api.APIInfoImpl{ReqInfo: &reqInfo}}
 	err, _ := c.Validate()
 	errs := util.JoinErrsStr(test.SortErrors(test.SplitErrors(err)))
 
@@ -151,7 +155,7 @@ func TestValidate(t *testing.T) {
 	//  name,  domainname both valid
 	n = "This.is.2.a-Valid---CDNNAME."
 	d := `awesome-cdn.example.net`
-	c = TOCDN{CDNNullable: tc.CDNNullable{Name: &n, DomainName: &d}}
+	c = TOCDN{CDNNullable: tc.CDNNullable{Name: &n, DomainName: &d}, APIInfoImpl: api.APIInfoImpl{ReqInfo: &reqInfo}}
 	err, _ = c.Validate()
 	if err != nil {
 		t.Errorf("expected nil, got %s", err)
diff --git a/traffic_ops/traffic_ops_golang/crconfig/crconfig.go b/traffic_ops/traffic_ops_golang/crconfig/crconfig.go
index 456feaef76..fc1ac53004 100644
--- a/traffic_ops/traffic_ops_golang/crconfig/crconfig.go
+++ b/traffic_ops/traffic_ops_golang/crconfig/crconfig.go
@@ -35,7 +35,7 @@ func Make(tx *sql.Tx, cdn, user, toHost, toVersion string, useClientReqHost bool
 	crc := tc.CRConfig{}
 	err := error(nil)
 
-	cdnDomain, dnssecEnabled, err := getCDNInfo(cdn, tx)
+	cdnDomain, dnssecEnabled, ttlOverride, err := getCDNInfo(cdn, tx)
 	if err != nil {
 		return nil, errors.New("Error getting CDN info: " + err.Error())
 	}
@@ -50,7 +50,7 @@ func Make(tx *sql.Tx, cdn, user, toHost, toVersion string, useClientReqHost bool
 	if crc.EdgeLocations, crc.RouterLocations, err = makeLocations(cdn, tx); err != nil {
 		return nil, errors.New("Error getting Edge Locations: " + err.Error())
 	}
-	if crc.DeliveryServices, err = makeDSes(cdn, cdnDomain, tx); err != nil {
+	if crc.DeliveryServices, err = makeDSes(cdn, cdnDomain, ttlOverride, tx); err != nil {
 		return nil, errors.New("Error getting Delivery Services: " + err.Error())
 	}
 	if crc.Topologies, err = topology.MakeTopologies(tx); err != nil {
diff --git a/traffic_ops/traffic_ops_golang/crconfig/deliveryservice.go b/traffic_ops/traffic_ops_golang/crconfig/deliveryservice.go
index 91b556f449..dfd45ebaa7 100644
--- a/traffic_ops/traffic_ops_golang/crconfig/deliveryservice.go
+++ b/traffic_ops/traffic_ops_golang/crconfig/deliveryservice.go
@@ -23,6 +23,7 @@ import (
 	"database/sql"
 	"errors"
 	"fmt"
+	"math"
 	"strconv"
 	"strings"
 	"time"
@@ -43,14 +44,25 @@ const DefaultTLDTTLNS = 3600 * time.Second
 const GeoProviderMaxmindStr = "maxmindGeolocationService"
 const GeoProviderNeustarStr = "neustarGeolocationService"
 
-func makeDSes(cdn string, domain string, tx *sql.Tx) (map[string]tc.CRConfigDeliveryService, error) {
+func makeDSes(cdn string, domain string, ttlOverride int, tx *sql.Tx) (map[string]tc.CRConfigDeliveryService, error) {
 	dses := map[string]tc.CRConfigDeliveryService{}
 
 	admin := CDNSOAAdmin
-	expireSecondsStr := strconv.Itoa(int(CDNSOAExpire / time.Second))
-	minimumSecondsStr := strconv.Itoa(int(CDNSOAMinimum / time.Second))
-	refreshSecondsStr := strconv.Itoa(int(CDNSOARefresh / time.Second))
-	retrySecondsStr := strconv.Itoa(int(CDNSOARetry / time.Second))
+	expireSeconds := int(CDNSOAExpire / time.Second)
+	minimumSeconds := int(CDNSOAMinimum / time.Second)
+	refreshSeconds := int(CDNSOARefresh / time.Second)
+	retrySeconds := int(CDNSOARetry / time.Second)
+	if ttlOverride > 0 {
+		expireSeconds = int(math.Min(float64(ttlOverride), float64(expireSeconds)))
+		minimumSeconds = int(math.Min(float64(ttlOverride), float64(minimumSeconds)))
+		refreshSeconds = int(math.Min(float64(ttlOverride), float64(refreshSeconds)))
+		retrySeconds = int(math.Min(float64(ttlOverride), float64(retrySeconds)))
+	}
+
+	expireSecondsStr := strconv.Itoa(expireSeconds)
+	minimumSecondsStr := strconv.Itoa(minimumSeconds)
+	refreshSecondsStr := strconv.Itoa(refreshSeconds)
+	retrySecondsStr := strconv.Itoa(retrySeconds)
 	cdnSOA := &tc.SOA{
 		Admin:          &admin,
 		ExpireSeconds:  &expireSecondsStr,
@@ -76,6 +88,8 @@ func makeDSes(cdn string, domain string, tx *sql.Tx) (map[string]tc.CRConfigDeli
 		return nil, errors.New("getting deliveryservice parameters: " + err.Error())
 	}
 
+	ttlOverrideDuration := time.Duration(ttlOverride) * time.Second
+
 	dsParams, err := getDSParams(serverParams)
 	if err != nil {
 		return nil, errors.New("getting deliveryservice server parameters: " + err.Error())
@@ -213,6 +227,10 @@ AND d.active = true
 			return nil, errors.New("scanning deliveryservice: " + err.Error())
 		}
 
+		if ttlOverride > 0 && ds.TTL != nil {
+			*ds.TTL = int(math.Min(float64(ttlOverride), float64(*ds.TTL)))
+		}
+
 		// TODO prevent (lat XOR lon) in the Tx and UI
 		if missLat.Valid && missLon.Valid {
 			ds.MissLocation = &tc.CRConfigLatitudeLongitudeShort{Lat: missLat.Float64, Lon: missLon.Float64}
@@ -313,7 +331,13 @@ AND d.active = true
 				}
 			}
 		}
+		if ttlOverride > 0 && ttlOverrideDuration < nsSeconds {
+			nsSeconds = ttlOverrideDuration
+		}
 		nsSecondsStr := strconv.Itoa(int(nsSeconds / time.Second))
+		if ttlOverride > 0 && ttlOverrideDuration < soaSeconds {
+			soaSeconds = ttlOverrideDuration
+		}
 		soaSecondsStr := strconv.Itoa(int(soaSeconds / time.Second))
 		ttlStr := ""
 		if ds.TTL != nil {
@@ -336,6 +360,9 @@ AND d.active = true
 			}
 			if dnsBypassTTL.Valid {
 				i := int(dnsBypassTTL.Int64)
+				if ttlOverride > 0 {
+					i = int(math.Min(float64(ttlOverride), float64(i)))
+				}
 				bypassDest.TTL = &i
 			}
 			if dnsBypassCName.Valid && dnsBypassCName.String != "" {
@@ -411,6 +438,13 @@ AND d.active = true
 		}
 
 		ds.StaticDNSEntries = staticDNSEntries[tc.DeliveryServiceName(xmlID)]
+		if ttlOverride > 0 {
+			for xmlID := range staticDNSEntries {
+				for index := range staticDNSEntries[xmlID] {
+					staticDNSEntries[xmlID][index].TTL = ttlOverride
+				}
+			}
+		}
 
 		dses[xmlID] = ds
 	}
@@ -534,8 +568,8 @@ func getDSParams(serverParams map[string]map[string]string) (map[string]string,
 		"tld.soa.minimum":   struct{}{},
 		"tld.soa.refresh":   struct{}{},
 		"tld.soa.retry":     struct{}{},
-		"tld.ttls.SOA":      struct{}{},
-		"tld.ttls.NS":       struct{}{},
+		tc.CrConfigTTLSOA:   struct{}{},
+		tc.CrConfigTTLNS:    struct{}{},
 		"LogRequestHeaders": struct{}{},
 	}
 	dsParams := map[string]string{}
diff --git a/traffic_ops/traffic_ops_golang/crconfig/deliveryservice_test.go b/traffic_ops/traffic_ops_golang/crconfig/deliveryservice_test.go
index fe0744e77e..2e5b9c9ab2 100644
--- a/traffic_ops/traffic_ops_golang/crconfig/deliveryservice_test.go
+++ b/traffic_ops/traffic_ops_golang/crconfig/deliveryservice_test.go
@@ -22,6 +22,7 @@ package crconfig
 import (
 	"context"
 	"encoding/json"
+	"math"
 	"reflect"
 	"strconv"
 	"strings"
@@ -35,19 +36,45 @@ import (
 	"gopkg.in/DATA-DOG/go-sqlmock.v1"
 )
 
-func randDS() tc.CRConfigDeliveryService {
+func randDS(ttlOverride int) tc.CRConfigDeliveryService {
 	// truePtr := true
 	falseStrPtr := "false"
 	// numStr := "42"
 	ttlAdmin := "traffic_ops"
-	ttlExpire := "604800"
-	ttlMinimum := "30"
-	ttlRefresh := "28800"
-	ttlRetry := "7200"
-	ttl := util.IntPtr(test.RandInt())
-	ttlStr := strconv.Itoa(*ttl)
-	ttlNS := "3600"
-	ttlSOA := "86400"
+	ttlExpire := 604800
+	ttlMinimum := 30
+	ttlRefresh := 28800
+	ttlRetry := 7200
+	if ttlOverride > 0 {
+		ttlExpire = int(math.Min(float64(ttlOverride), float64(ttlExpire)))
+		ttlMinimum = int(math.Min(float64(ttlOverride), float64(ttlMinimum)))
+		ttlRefresh = int(math.Min(float64(ttlOverride), float64(ttlRefresh)))
+		ttlRetry = int(math.Min(float64(ttlOverride), float64(ttlRetry)))
+	}
+
+	ttlExpireStr := strconv.Itoa(ttlExpire)
+	ttlMinimumStr := strconv.Itoa(ttlMinimum)
+	ttlRefreshStr := strconv.Itoa(ttlRefresh)
+	ttlRetryStr := strconv.Itoa(ttlRetry)
+
+	var ttl *int
+	var ttlStr string
+	var ttlNS, ttlSOA string
+	var staticDNSTTL int
+
+	if ttlOverride > 0 {
+		ttl = util.Ptr(ttlOverride)
+		ttlStr = strconv.Itoa(*ttl)
+		ttlNS = ttlStr
+		ttlSOA = ttlStr
+		staticDNSTTL = ttlOverride
+	} else {
+		ttl = util.IntPtr(test.RandInt())
+		ttlStr = strconv.Itoa(*ttl)
+		ttlNS = "3600"
+		ttlSOA = "86400"
+		staticDNSTTL = test.RandInt()
+	}
 	geoProviderStr := GeoProviderMaxmindStr
 	ecsEnabled := false
 	return tc.CRConfigDeliveryService{
@@ -76,10 +103,10 @@ func randDS() tc.CRConfigDeliveryService {
 		RequiredCapabilities: test.RandStrArray(),
 		Soa: &tc.SOA{
 			Admin:          &ttlAdmin,
-			ExpireSeconds:  &ttlExpire,
-			MinimumSeconds: &ttlMinimum,
-			RefreshSeconds: &ttlRefresh,
-			RetrySeconds:   &ttlRetry,
+			ExpireSeconds:  &ttlExpireStr,
+			MinimumSeconds: &ttlMinimumStr,
+			RefreshSeconds: &ttlRefreshStr,
+			RetrySeconds:   &ttlRetryStr,
 		},
 		SSLEnabled: false,
 		EcsEnabled: &ecsEnabled,
@@ -110,7 +137,7 @@ func randDS() tc.CRConfigDeliveryService {
 		StaticDNSEntries: []tc.CRConfigStaticDNSEntry{
 			tc.CRConfigStaticDNSEntry{
 				Name:  test.RandStr(),
-				TTL:   test.RandInt(),
+				TTL:   staticDNSTTL,
 				Type:  test.RandStr(),
 				Value: test.RandStr(),
 			},
@@ -118,10 +145,10 @@ func randDS() tc.CRConfigDeliveryService {
 	}
 }
 
-func ExpectedMakeDSes() map[string]tc.CRConfigDeliveryService {
+func ExpectedMakeDSes(ttlOverride int) map[string]tc.CRConfigDeliveryService {
 	return map[string]tc.CRConfigDeliveryService{
-		"ds1": randDS(),
-		"ds2": randDS(),
+		"ds1": randDS(ttlOverride),
+		"ds2": randDS(ttlOverride),
 	}
 }
 
@@ -211,7 +238,11 @@ func TestMakeDSesGeoLimit(t *testing.T) {
 	cdn := "mycdn"
 	domain := "mycdn.invalid"
 
-	expected := ExpectedMakeDSes()
+	ttlOverride := 0
+	if test.RandBool() {
+		ttlOverride = 1 + test.RandIntn(200)
+	}
+	expected := ExpectedMakeDSes(ttlOverride)
 	delete(expected, "ds2")
 	expectedDS := expected["ds1"]
 	geoEnabled := make([]tc.CRConfigGeoEnabled, 0)
@@ -245,7 +276,7 @@ func TestMakeDSesGeoLimit(t *testing.T) {
 	}
 	defer tx.Commit()
 
-	actual, err := makeDSes(cdn, domain, tx)
+	actual, err := makeDSes(cdn, domain, ttlOverride, tx)
 	if err != nil {
 		t.Fatalf("makeDSes expected: nil error, actual: %v", err)
 	}
@@ -281,7 +312,11 @@ func TestMakeDSes(t *testing.T) {
 	cdn := "mycdn"
 	domain := "mycdn.invalid"
 
-	expected := ExpectedMakeDSes()
+	ttlOverride := 0
+	if test.RandBool() {
+		ttlOverride = 1 + test.RandIntn(200)
+	}
+	expected := ExpectedMakeDSes(ttlOverride)
 	expectedParams := ExpectedGetServerProfileParams(expected)
 	expectedDSParams, err := getDSParams(expectedParams)
 	if err != nil {
@@ -305,7 +340,7 @@ func TestMakeDSes(t *testing.T) {
 	}
 	defer tx.Commit()
 
-	actual, err := makeDSes(cdn, domain, tx)
+	actual, err := makeDSes(cdn, domain, ttlOverride, tx)
 	if err != nil {
 		t.Fatalf("makeDSes expected: nil error, actual: %v", err)
 	}
@@ -358,7 +393,11 @@ func TestGetServerProfileParams(t *testing.T) {
 
 	cdn := "mycdn"
 
-	expectedMakeDSes := ExpectedMakeDSes()
+	ttlOverride := 0
+	if test.RandBool() {
+		ttlOverride = 1 + test.RandIntn(200)
+	}
+	expectedMakeDSes := ExpectedMakeDSes(ttlOverride)
 	expected := ExpectedGetServerProfileParams(expectedMakeDSes)
 
 	mock.ExpectBegin()
@@ -442,7 +481,11 @@ func TestGetDSRegexesDomains(t *testing.T) {
 	cdn := "mycdn"
 	domain := "mycdn.invalid"
 
-	expectedMakeDSes := ExpectedMakeDSes()
+	ttlOverride := 0
+	if test.RandBool() {
+		ttlOverride = 1 + test.RandIntn(200)
+	}
+	expectedMakeDSes := ExpectedMakeDSes(ttlOverride)
 	expectedServerProfileParams := ExpectedGetServerProfileParams(expectedMakeDSes)
 	expectedDSParams, err := getDSParams(expectedServerProfileParams)
 	if err != nil {
@@ -511,7 +554,11 @@ func TestGetStaticDNSEntries(t *testing.T) {
 
 	cdn := "mycdn"
 
-	expectedMakeDSes := ExpectedMakeDSes()
+	ttlOverride := 0
+	if test.RandBool() {
+		ttlOverride = 1 + test.RandIntn(200)
+	}
+	expectedMakeDSes := ExpectedMakeDSes(ttlOverride)
 	expected := ExpectedGetStaticDNSEntries(expectedMakeDSes)
 
 	mock.ExpectBegin()
diff --git a/traffic_ops/traffic_ops_golang/crconfig/servers.go b/traffic_ops/traffic_ops_golang/crconfig/servers.go
index 28f17b8195..16b52ab285 100644
--- a/traffic_ops/traffic_ops_golang/crconfig/servers.go
+++ b/traffic_ops/traffic_ops_golang/crconfig/servers.go
@@ -405,13 +405,15 @@ and (st.name = 'REPORTED' or st.name = 'ONLINE' or st.name = 'ADMIN_DOWN')
 }
 
 // getCDNInfo returns the CDN domain, and whether DNSSec is enabled
-func getCDNInfo(cdn string, tx *sql.Tx) (string, bool, error) {
+func getCDNInfo(cdn string, tx *sql.Tx) (string, bool, int, error) {
 	domain := ""
 	dnssec := false
-	if err := tx.QueryRow(`select domain_name, dnssec_enabled from cdn where name = $1`, cdn).Scan(&domain, &dnssec); err != nil {
-		return "", false, errors.New("Error querying CDN domain name: " + err.Error())
+	ttlOverride := sql.NullInt64{}
+	err := tx.QueryRow(`select domain_name, dnssec_enabled, ttl_override from cdn where name = $1`, cdn).Scan(&domain, &dnssec, &ttlOverride)
+	if err != nil {
+		err = errors.New("Error querying CDN domain name: " + err.Error())
 	}
-	return domain, dnssec, nil
+	return domain, dnssec, int(ttlOverride.Int64), err
 }
 
 // getCDNNameFromID returns the CDN name given the ID, false if the no CDN with the given ID exists, and an error if the database query fails.
diff --git a/traffic_ops/traffic_ops_golang/crconfig/servers_test.go b/traffic_ops/traffic_ops_golang/crconfig/servers_test.go
index 117596a483..3a2577b54c 100644
--- a/traffic_ops/traffic_ops_golang/crconfig/servers_test.go
+++ b/traffic_ops/traffic_ops_golang/crconfig/servers_test.go
@@ -540,13 +540,13 @@ func TestGetServerDSes(t *testing.T) {
 	}
 }
 
-func ExpectedGetCDNInfo() (string, bool) {
-	return test.RandStr(), test.RandBool()
+func ExpectedGetCDNInfo() (string, bool, int) {
+	return test.RandStr(), test.RandBool(), test.RandInt()
 }
 
-func MockGetCDNInfo(mock sqlmock.Sqlmock, expectedDomain string, expectedDNSSECEnabled bool, cdn string) {
-	rows := sqlmock.NewRows([]string{"domain_name", "dnssec_enabled"})
-	rows = rows.AddRow(expectedDomain, expectedDNSSECEnabled)
+func MockGetCDNInfo(mock sqlmock.Sqlmock, expectedDomain string, expectedDNSSECEnabled bool, expectedTTLOverride int, cdn string) {
+	rows := sqlmock.NewRows([]string{"domain_name", "dnssec_enabled", "ttl_override"})
+	rows = rows.AddRow(expectedDomain, expectedDNSSECEnabled, expectedTTLOverride)
 	mock.ExpectQuery("select").WithArgs(cdn).WillReturnRows(rows)
 }
 
@@ -560,8 +560,8 @@ func TestGetCDNInfo(t *testing.T) {
 	cdn := "mycdn"
 
 	mock.ExpectBegin()
-	expectedDomain, expectedDNSSECEnabled := ExpectedGetCDNInfo()
-	MockGetCDNInfo(mock, expectedDomain, expectedDNSSECEnabled, cdn)
+	expectedDomain, expectedDNSSECEnabled, expectedTTLOverride := ExpectedGetCDNInfo()
+	MockGetCDNInfo(mock, expectedDomain, expectedDNSSECEnabled, expectedTTLOverride, cdn)
 	mock.ExpectCommit()
 
 	dbCtx, cancelTx := context.WithTimeout(context.TODO(), 10*time.Second)
@@ -572,7 +572,7 @@ func TestGetCDNInfo(t *testing.T) {
 	}
 	defer tx.Commit()
 
-	actualDomain, actualDNSSECEnabled, err := getCDNInfo(cdn, tx)
+	actualDomain, actualDNSSECEnabled, actualTTLOverride, err := getCDNInfo(cdn, tx)
 	if err != nil {
 		t.Fatalf("getCDNInfo expected: nil error, actual: %v", err)
 	}
@@ -583,6 +583,9 @@ func TestGetCDNInfo(t *testing.T) {
 	if expectedDNSSECEnabled != actualDNSSECEnabled {
 		t.Errorf("getCDNInfo expected: %v, actual: %v", expectedDNSSECEnabled, actualDNSSECEnabled)
 	}
+	if expectedTTLOverride != actualTTLOverride {
+		t.Errorf("getCDNInfo expected: %v, actual: %v", expectedTTLOverride, actualTTLOverride)
+	}
 }
 
 func ExpectedGetCDNNameFromID() string {
diff --git a/traffic_portal/app/src/common/modules/form/cdn/form.cdn.tpl.html b/traffic_portal/app/src/common/modules/form/cdn/form.cdn.tpl.html
index 14c08530ee..e8ec59d2c6 100644
--- a/traffic_portal/app/src/common/modules/form/cdn/form.cdn.tpl.html
+++ b/traffic_portal/app/src/common/modules/form/cdn/form.cdn.tpl.html
@@ -82,6 +82,19 @@ under the License.
                     <small class="input-error" ng-show="hasPropertyError(cdnForm.dnssecEnabled, 'required')">Required</small>
                 </div>
             </div>
+            <div class="form-group" ng-class="{'has-error': hasError(cdnForm.ttlOverride), 'has-feedback': hasError(cdnForm.ttlOverride)}">
+                <label class="has-tooltip control-label col-md-2 col-sm-2 col-xs-12">TTL Override<div class="helptooltip">
+                            <div class="helptext">Setting the <abbr title="Time To Live">TTL</abbr> Override for this CDN overrides every TTL value of every Delivery Service in the CDN's Snapshot with this value.
+                            <br><br>
+                            Operators may find this feature useful for temporarily lowering the TTL values in order to perform a Traffic Router upgrade.
+                            </div>
+                        </div>
+                </label>
+                <div class="col-md-10 col-sm-10 col-xs-12">
+                    <input name="ttlOverride" type="number" class="form-control" ng-model="cdn.ttlOverride" step="1" min="1">
+                    <span ng-show="hasError(cdnForm.ttlOverride)" class="form-control-feedback"><i class="fa fa-times"></i></span>
+                </div>
+            </div>
             <div class="modal-footer">
                 <button type="button" class="btn btn-danger" ng-show="!settings.isNew" ng-click="confirmDelete(cdn)">Delete</button>
                 <button type="button" class="btn btn-success" ng-disabled="cdnForm.$pristine || cdnForm.$invalid" ng-click="save(cdn)">{{settings.saveLabel}}</button>
diff --git a/traffic_portal/test/integration/Data/cdn.ts b/traffic_portal/test/integration/Data/cdn.ts
index be41e689db..369a37ea78 100644
--- a/traffic_portal/test/integration/Data/cdn.ts
+++ b/traffic_portal/test/integration/Data/cdn.ts
@@ -68,12 +68,18 @@ export const cdns = {
 					NewName: "",
 					validationMessage: "Cleared CDN server updates"
 				},
+				{
+					description: "update cdn ttl override",
+					Name: "TPCDN2",
+					NewName: "",
+					validationMessage: "cdn was updated."
+				},
 				{
 					description: "update cdn name",
 					Name: "TPCDN1",
 					NewName: "TPNewCDN1",
 					validationMessage: "cdn was updated."
-				}
+				},
 			],
 			remove: [
 				{
@@ -120,12 +126,18 @@ export const cdns = {
 					NewName: "",
 					validationMessage: "missing required Permissions: SERVER:QUEUE"
 				},
+				{
+					description: "update cdn ttl override",
+					Name: "TPCDN2",
+					NewName: "",
+					validationMessage: "missing required Permissions: CDN:UPDATE"
+				},
 				{
 					description: "update cdn name",
 					Name: "TPCDN2",
 					NewName: "TPNewCDN2",
 					validationMessage: "missing required Permissions: CDN:UPDATE"
-				}
+				},
 			],
 			remove: [
 				{
@@ -185,12 +197,18 @@ export const cdns = {
 					NewName: "",
 					validationMessage: "Cleared CDN server updates"
 				},
+				{
+					description: "update cdn ttl override",
+					Name: "TPCDN4",
+					NewName: "",
+					validationMessage: "cdn was updated."
+				},
 				{
 					description: "update cdn name",
 					Name: "TPCDN3",
 					NewName: "TPNewCDN3",
 					validationMessage: "cdn was updated."
-				}
+				},
 			],
 			remove: [
 				{
diff --git a/traffic_portal/test/integration/PageObjects/CDNPage.po.ts b/traffic_portal/test/integration/PageObjects/CDNPage.po.ts
index 40891936ce..dfc01bd33b 100644
--- a/traffic_portal/test/integration/PageObjects/CDNPage.po.ts
+++ b/traffic_portal/test/integration/PageObjects/CDNPage.po.ts
@@ -17,7 +17,7 @@
  * under the License.
  */
 import { browser, by, element } from "protractor";
-import { randomize } from "../config";
+import { randomize, twoNumberRandomize } from "../config";
 import { SideNavigationPage } from "./SideNavigationPage.po";
 
 interface CDN {
@@ -25,6 +25,7 @@ interface CDN {
 	DNSSEC: string;
 	Domain: string;
 	Name: string;
+	TTLOverride?: string;
 	validationMessage?: string;
 }
 
@@ -38,6 +39,7 @@ interface UpdateCDN {
 export class CDNPage extends SideNavigationPage {
 
 	private readonly txtCDNName = element(by.name("name"));
+	private readonly txtCDNTTLOverride = element(by.name("ttlOverride"));
 
 	/**
 	 * Navigates the browser to the CDNs table page.
@@ -58,11 +60,15 @@ export class CDNPage extends SideNavigationPage {
 		await this.openCDNsPage();
 		await element(by.buttonText("More")).click();
 		await element(by.linkText("Create New CDN")).click();
-		await Promise.all([
+		const actions = [
 			this.txtCDNName.sendKeys(cdn.Name + randomize),
 			element(by.name("domainName")).sendKeys(cdn.Domain),
-			element(by.name("dnssecEnabled")).sendKeys(cdn.DNSSEC)
-		]);
+			element(by.name("dnssecEnabled")).sendKeys(cdn.DNSSEC),
+		];
+		if (cdn.TTLOverride !== undefined) {
+			actions.push(element(this.txtCDNTTLOverride).sendKeys(cdn.TTLOverride));
+		}
+		await Promise.all(actions);
 		await this.ClickCreate();
 		return this.GetOutputMessage().then(v => cdn.validationMessage === v);
 	}
@@ -125,6 +131,11 @@ export class CDNPage extends SideNavigationPage {
 				await this.txtCDNName.sendKeys(cdn.NewName + randomize);
 				await this.ClickUpdate();
 				break;
+			case "update cdn ttl override":
+				await this.txtCDNTTLOverride.clear();
+				await this.txtCDNTTLOverride.sendKeys(twoNumberRandomize);
+				await this.ClickUpdate();
+				break;
 			default:
 				throw new Error(`unhandleable description: '${cdn.description}'`);
 		}