You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@trafficcontrol.apache.org by de...@apache.org on 2018/05/30 16:12:57 UTC

[incubator-trafficcontrol] 02/03: Add TO Go deliveryservices routes

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

dewrich pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/incubator-trafficcontrol.git

commit 75a949dedb31337ab6aba666daebeae705af97cb
Author: Robert Butts <ro...@apache.org>
AuthorDate: Thu Apr 12 08:29:30 2018 -0600

    Add TO Go deliveryservices routes
---
 .rat-excludes                                      |    1 +
 lib/go-tc/deliveryservice_requests.go              |   52 +-
 lib/go-tc/deliveryservice_ssl_keys.go              |   26 +
 lib/go-tc/deliveryservices.go                      |  255 +++--
 traffic_ops/app/lib/TrafficOpsRoutes.pm            |    5 -
 traffic_ops/traffic_ops_golang/api/change_log.go   |    7 +-
 .../traffic_ops_golang/api/shared_handlers.go      |   53 +-
 traffic_ops/traffic_ops_golang/crconfig/handler.go |    8 +-
 .../traffic_ops_golang/dbhelpers/db_helpers.go     |   26 +
 .../deliveryservice/deliveryservices.go            |  406 -------
 .../deliveryservice/deliveryservicesv12.go         |  433 ++++++++
 .../deliveryservice/deliveryservicesv13.go         | 1119 ++++++++++++++++++++
 .../traffic_ops_golang/deliveryservice/dnssec.go   |  206 ++++
 .../deliveryservice/request/requests_test.go       |   16 +-
 .../deliveryservice/request/validate.go            |    7 +-
 .../traffic_ops_golang/deliveryservices_keys.go    |  107 +-
 traffic_ops/traffic_ops_golang/riaksvc/dsutil.go   |  140 +++
 .../traffic_ops_golang/riaksvc/riak_services.go    |  110 +-
 traffic_ops/traffic_ops_golang/routes.go           |   26 +-
 traffic_ops/traffic_ops_golang/urisigning.go       |    6 +-
 20 files changed, 2320 insertions(+), 689 deletions(-)

diff --git a/.rat-excludes b/.rat-excludes
index 44a7533..e461914 100644
--- a/.rat-excludes
+++ b/.rat-excludes
@@ -70,3 +70,4 @@ asn1-ber.v1(?:                       MIT. Properly documented in LICENSE ){0}
 bytefmt(?:                           Apache 2.0. Properly documented in LICENSE ){0}
 siphash(?:                           CC0. Properly documented in LICENSE ){0}
 bbolt(?:                             MIT. Properly documented in LICENSE ){0}
+miekg(?:                             BSD 3-clause. Properly documented in LICENSE ){0}
diff --git a/lib/go-tc/deliveryservice_requests.go b/lib/go-tc/deliveryservice_requests.go
index 7b2a4cf..9aae4bd 100644
--- a/lib/go-tc/deliveryservice_requests.go
+++ b/lib/go-tc/deliveryservice_requests.go
@@ -32,37 +32,37 @@ type IDNoMod int
 // DeliveryServiceRequest is used as part of the workflow to create,
 // modify, or delete a delivery service.
 type DeliveryServiceRequest struct {
-	AssigneeID      int             `json:"assigneeId,omitempty"`
-	Assignee        string          `json:"assignee,omitempty"`
-	AuthorID        IDNoMod         `json:"authorId"`
-	Author          string          `json:"author"`
-	ChangeType      string          `json:"changeType"`
-	CreatedAt       *TimeNoMod      `json:"createdAt"`
-	ID              int             `json:"id"`
-	LastEditedBy    string          `json:"lastEditedBy,omitempty"`
-	LastEditedByID  IDNoMod         `json:"lastEditedById,omitempty"`
-	LastUpdated     *TimeNoMod      `json:"lastUpdated"`
-	DeliveryService DeliveryService `json:"deliveryService"`
-	Status          RequestStatus   `json:"status"`
-	XMLID           string          `json:"-" db:"xml_id"`
+	AssigneeID      int                `json:"assigneeId,omitempty"`
+	Assignee        string             `json:"assignee,omitempty"`
+	AuthorID        IDNoMod            `json:"authorId"`
+	Author          string             `json:"author"`
+	ChangeType      string             `json:"changeType"`
+	CreatedAt       *TimeNoMod         `json:"createdAt"`
+	ID              int                `json:"id"`
+	LastEditedBy    string             `json:"lastEditedBy,omitempty"`
+	LastEditedByID  IDNoMod            `json:"lastEditedById,omitempty"`
+	LastUpdated     *TimeNoMod         `json:"lastUpdated"`
+	DeliveryService DeliveryServiceV13 `json:"deliveryService"` // TODO version DeliveryServiceRequest
+	Status          RequestStatus      `json:"status"`
+	XMLID           string             `json:"-" db:"xml_id"`
 }
 
 // DeliveryServiceRequestNullable is used as part of the workflow to create,
 // modify, or delete a delivery service.
 type DeliveryServiceRequestNullable struct {
-	AssigneeID      *int                     `json:"assigneeId,omitempty" db:"assignee_id"`
-	Assignee        *string                  `json:"assignee,omitempty"`
-	AuthorID        *IDNoMod                 `json:"authorId" db:"author_id"`
-	Author          *string                  `json:"author"`
-	ChangeType      *string                  `json:"changeType" db:"change_type"`
-	CreatedAt       *TimeNoMod               `json:"createdAt" db:"created_at"`
-	ID              *int                     `json:"id" db:"id"`
-	LastEditedBy    *string                  `json:"lastEditedBy"`
-	LastEditedByID  *IDNoMod                 `json:"lastEditedById" db:"last_edited_by_id"`
-	LastUpdated     *TimeNoMod               `json:"lastUpdated" db:"last_updated"`
-	DeliveryService *DeliveryServiceNullable `json:"deliveryService" db:"deliveryservice"`
-	Status          *RequestStatus           `json:"status" db:"status"`
-	XMLID           *string                  `json:"-" db:"xml_id"`
+	AssigneeID      *int                        `json:"assigneeId,omitempty" db:"assignee_id"`
+	Assignee        *string                     `json:"assignee,omitempty"`
+	AuthorID        *IDNoMod                    `json:"authorId" db:"author_id"`
+	Author          *string                     `json:"author"`
+	ChangeType      *string                     `json:"changeType" db:"change_type"`
+	CreatedAt       *TimeNoMod                  `json:"createdAt" db:"created_at"`
+	ID              *int                        `json:"id" db:"id"`
+	LastEditedBy    *string                     `json:"lastEditedBy"`
+	LastEditedByID  *IDNoMod                    `json:"lastEditedById" db:"last_edited_by_id"`
+	LastUpdated     *TimeNoMod                  `json:"lastUpdated" db:"last_updated"`
+	DeliveryService *DeliveryServiceNullableV13 `json:"deliveryService" db:"deliveryservice"` // TODO version DeliveryServiceRequest
+	Status          *RequestStatus              `json:"status" db:"status"`
+	XMLID           *string                     `json:"-" db:"xml_id"`
 }
 
 // UnmarshalJSON implements the json.Unmarshaller interface to suppress unmarshalling for IDNoMod
diff --git a/lib/go-tc/deliveryservice_ssl_keys.go b/lib/go-tc/deliveryservice_ssl_keys.go
index 9f47f70..830bebe 100644
--- a/lib/go-tc/deliveryservice_ssl_keys.go
+++ b/lib/go-tc/deliveryservice_ssl_keys.go
@@ -84,3 +84,29 @@ func (v *DeliveryServiceSSLKeys) UnmarshalJSON(b []byte) (err error) {
 	}
 	return err
 }
+
+// DNSSECKeys is the DNSSEC keys object stored in Riak. The map key strings are both DeliveryServiceNames and CDNNames.
+type DNSSECKeys map[string]DNSSECKeySet
+
+type DNSSECKeySet struct {
+	ZSK []DNSSECKey `json:"zsk"`
+	KSK []DNSSECKey `json:"ksk"`
+}
+
+type DNSSECKey struct {
+	InceptionDateUnix  int64 `json:"inceptionDate"`
+	ExpirationDateUnix int64 `json:"expirationDate"`
+	Name               string `json:"name"`
+	TTLSeconds         uint64 `json:"ttl,string"`
+	Status             string `json:"status"`
+	EffectiveDateUnix  int64 `json:"effectiveDate"`
+	Public             string `json:"public"`
+	Private            string `json:"private"`
+	DSRecord           *DNSSECKeyDSRecord `json:"dsRecord,omitempty"`
+}
+
+type DNSSECKeyDSRecord struct {
+	Algorithm  int64  `json:"algorithm,string"`
+	DigestType int64  `json:"digestType,string"`
+	Digest     string `json:"digest"`
+}
diff --git a/lib/go-tc/deliveryservices.go b/lib/go-tc/deliveryservices.go
index fa3bbf1..7990998 100644
--- a/lib/go-tc/deliveryservices.go
+++ b/lib/go-tc/deliveryservices.go
@@ -56,126 +56,153 @@ type DeleteDeliveryServiceResponse struct {
 }
 
 // DeliveryService ...
+// TODO move contents to DeliveryServiceV12, fix references, and remove
 type DeliveryService struct {
-	Active                   bool                   `json:"active"`
-	AnonymousBlockingEnabled bool                   `json:"anonymousBlockingEnabled"`
-	CacheURL                 string                 `json:"cacheurl"`
-	CCRDNSTTL                int                    `json:"ccrDnsTtl"`
-	CDNID                    int                    `json:"cdnId"`
-	CDNName                  string                 `json:"cdnName"`
-	CheckPath                string                 `json:"checkPath"`
-	DeepCachingType          DeepCachingType        `json:"deepCachingType"`
-	DisplayName              string                 `json:"displayName"`
-	DNSBypassCname           string                 `json:"dnsBypassCname"`
-	DNSBypassIP              string                 `json:"dnsBypassIp"`
-	DNSBypassIP6             string                 `json:"dnsBypassIp6"`
-	DNSBypassTTL             int                    `json:"dnsBypassTtl"`
-	DSCP                     int                    `json:"dscp"`
-	EdgeHeaderRewrite        string                 `json:"edgeHeaderRewrite"`
-	ExampleURLs              []string               `json:"exampleURLs"`
-	GeoLimit                 int                    `json:"geoLimit"`
-	FQPacingRate             int                    `json:"fqPacingRate"`
-	GeoProvider              int                    `json:"geoProvider"`
-	GlobalMaxMBPS            int                    `json:"globalMaxMbps"`
-	GlobalMaxTPS             int                    `json:"globalMaxTps"`
-	HTTPBypassFQDN           string                 `json:"httpBypassFqdn"`
-	ID                       int                    `json:"id"`
-	InfoURL                  string                 `json:"infoUrl"`
-	InitialDispersion        float32                `json:"initialDispersion"`
-	IPV6RoutingEnabled       bool                   `json:"ipv6RoutingEnabled"`
-	LastUpdated              *TimeNoMod             `json:"lastUpdated" db:"last_updated"`
-	LogsEnabled              bool                   `json:"logsEnabled"`
-	LongDesc                 string                 `json:"longDesc"`
-	LongDesc1                string                 `json:"longDesc1"`
-	LongDesc2                string                 `json:"longDesc2"`
-	MatchList                []DeliveryServiceMatch `json:"matchList,omitempty"`
-	MaxDNSAnswers            int                    `json:"maxDnsAnswers"`
-	MidHeaderRewrite         string                 `json:"midHeaderRewrite"`
-	MissLat                  float64                `json:"missLat"`
-	MissLong                 float64                `json:"missLong"`
-	MultiSiteOrigin          bool                   `json:"multiSiteOrigin"`
-	OrgServerFQDN            string                 `json:"orgServerFqdn"`
-	ProfileDesc              string                 `json:"profileDescription"`
-	ProfileID                int                    `json:"profileId,omitempty"`
-	ProfileName              string                 `json:"profileName"`
-	Protocol                 int                    `json:"protocol"`
-	QStringIgnore            int                    `json:"qstringIgnore"`
-	RangeRequestHandling     int                    `json:"rangeRequestHandling"`
-	RegexRemap               string                 `json:"regexRemap"`
-	RegionalGeoBlocking      bool                   `json:"regionalGeoBlocking"`
-	RemapText                string                 `json:"remapText"`
-	RoutingName              string                 `json:"routingName"`
-	SigningAlgorithm         string                 `json:"signingAlgorithm" db:"signing_algorithm"`
-	TypeID                   int                    `json:"typeId"`
-	Type                     string                 `json:"type"`
-	TRResponseHeaders        string                 `json:"trResponseHeaders"`
-	TenantID                 int                    `json:"tenantId,omitempty"`
-	XMLID                    string                 `json:"xmlId"`
+	Active               bool                   `json:"active"`
+	CacheURL             string                 `json:"cacheurl"`
+	CCRDNSTTL            int                    `json:"ccrDnsTtl"`
+	CDNID                int                    `json:"cdnId"`
+	CDNName              string                 `json:"cdnName"`
+	CheckPath            string                 `json:"checkPath"`
+	DeepCachingType      DeepCachingType        `json:"deepCachingType"`
+	DisplayName          string                 `json:"displayName"`
+	DNSBypassCname       string                 `json:"dnsBypassCname"`
+	DNSBypassIP          string                 `json:"dnsBypassIp"`
+	DNSBypassIP6         string                 `json:"dnsBypassIp6"`
+	DNSBypassTTL         int                    `json:"dnsBypassTtl"`
+	DSCP                 int                    `json:"dscp"`
+	EdgeHeaderRewrite    string                 `json:"edgeHeaderRewrite"`
+	ExampleURLs          []string               `json:"exampleURLs"`
+	GeoLimit             int                    `json:"geoLimit"`
+	FQPacingRate         int                    `json:"fqPacingRate"`
+	GeoProvider          int                    `json:"geoProvider"`
+	GlobalMaxMBPS        int                    `json:"globalMaxMbps"`
+	GlobalMaxTPS         int                    `json:"globalMaxTps"`
+	HTTPBypassFQDN       string                 `json:"httpBypassFqdn"`
+	ID                   int                    `json:"id"`
+	InfoURL              string                 `json:"infoUrl"`
+	InitialDispersion    float32                `json:"initialDispersion"`
+	IPV6RoutingEnabled   bool                   `json:"ipv6RoutingEnabled"`
+	LastUpdated          *TimeNoMod             `json:"lastUpdated" db:"last_updated"`
+	LogsEnabled          bool                   `json:"logsEnabled"`
+	LongDesc             string                 `json:"longDesc"`
+	LongDesc1            string                 `json:"longDesc1"`
+	LongDesc2            string                 `json:"longDesc2"`
+	MatchList            []DeliveryServiceMatch `json:"matchList,omitempty"`
+	MaxDNSAnswers        int                    `json:"maxDnsAnswers"`
+	MidHeaderRewrite     string                 `json:"midHeaderRewrite"`
+	MissLat              float64                `json:"missLat"`
+	MissLong             float64                `json:"missLong"`
+	MultiSiteOrigin      bool                   `json:"multiSiteOrigin"`
+	OrgServerFQDN        string                 `json:"orgServerFqdn"`
+	ProfileDesc          string                 `json:"profileDescription"`
+	ProfileID            int                    `json:"profileId,omitempty"`
+	ProfileName          string                 `json:"profileName"`
+	Protocol             int                    `json:"protocol"`
+	QStringIgnore        int                    `json:"qstringIgnore"`
+	RangeRequestHandling int                    `json:"rangeRequestHandling"`
+	RegexRemap           string                 `json:"regexRemap"`
+	RegionalGeoBlocking  bool                   `json:"regionalGeoBlocking"`
+	RemapText            string                 `json:"remapText"`
+	RoutingName          string                 `json:"routingName"`
+	SigningAlgorithm     string                 `json:"signingAlgorithm" db:"signing_algorithm"`
+	TypeID               int                    `json:"typeId"`
+	Type                 string                 `json:"type"`
+	TRResponseHeaders    string                 `json:"trResponseHeaders"`
+	TenantID             int                    `json:"tenantId,omitempty"`
+	XMLID                string                 `json:"xmlId"`
+}
+
+type DeliveryServiceV12 struct {
+	DeliveryService
+}
+
+type DeliveryServiceV13 struct {
+	DeliveryServiceV12
+	AnonymousBlockingEnabled bool            `json:"anonymousBlockingEnabled"`
+	DeepCachingType          DeepCachingType `json:"deepCachingType"`
+	FQPacingRate             int             `json:"fqPacingRate,omitempty"`
+	SigningAlgorithm         string          `json:"signingAlgorithm" db:"signing_algorithm"`
+	TenantName               string          `json:"tenantName,omitempty"`
+	TRRequestHeaders         string          `json:"trRequestHeaders,omitempty"`
+	TRResponseHeaders        string          `json:"trResponseHeaders,omitempty"`
 }
 
 // DeliveryServiceNullable - a version of the deliveryservice that allows for all fields to be null
+// TODO move contents to DeliveryServiceNullableV12, fix references, and remove
 type DeliveryServiceNullable struct {
 	// NOTE: the db: struct tags are used for testing to map to their equivalent database column (if there is one)
 	//
-	Active                   *bool                   `json:"active" db:"active"`
-	AnonymousBlockingEnabled *bool                   `json:"anonymousBlockingEnabled" db:"anonymous_blocking_enabled"`
-	CacheURL                 *string                 `json:"cacheurl" db:"cacheurl"`
-	CCRDNSTTL                *int                    `json:"ccrDnsTtl" db:"ccr_dns_ttl"`
-	CDNID                    *int                    `json:"cdnId" db:"cdn_id"`
-	CDNName                  *string                 `json:"cdnName"`
-	CheckPath                *string                 `json:"checkPath" db:"check_path"`
-	DeepCachingType          *DeepCachingType        `json:"deepCachingType" db:"deep_caching_type"`
-	DisplayName              *string                 `json:"displayName" db:"display_name"`
-	DNSBypassCNAME           *string                 `json:"dnsBypassCname" db:"dns_bypass_cname"`
-	DNSBypassIP              *string                 `json:"dnsBypassIp" db:"dns_bypass_ip"`
-	DNSBypassIP6             *string                 `json:"dnsBypassIp6" db:"dns_bypass_ip6"`
-	DNSBypassTTL             *int                    `json:"dnsBypassTtl" db:"dns_bypass_ttl"`
-	DSCP                     *int                    `json:"dscp" db:"dscp"`
-	EdgeHeaderRewrite        *string                 `json:"edgeHeaderRewrite" db:"edge_header_rewrite"`
-	FQPacingRate             *int                    `json:"fqPacingRate" db:"fq_pacing_rate"`
-	GeoLimit                 *int                    `json:"geoLimit" db:"geo_limit"`
-	GeoLimitCountries        *string                 `json:"geoLimitCountries" db:"geo_limit_countries"`
-	GeoLimitRedirectURL      *string                 `json:"geoLimitRedirectURL" db:"geolimit_redirect_url"`
-	GeoProvider              *int                    `json:"geoProvider" db:"geo_provider"`
-	GlobalMaxMBPS            *int                    `json:"globalMaxMbps" db:"global_max_mbps"`
-	GlobalMaxTPS             *int                    `json:"globalMaxTps" db:"global_max_tps"`
-	HTTPBypassFQDN           *string                 `json:"httpBypassFqdn" db:"http_bypass_fqdn"`
-	ID                       *int                    `json:"id" db:"id"`
-	InfoURL                  *string                 `json:"infoUrl" db:"info_url"`
-	InitialDispersion        *int                    `json:"initialDispersion" db:"initial_dispersion"`
-	IPV6RoutingEnabled       *bool                   `json:"ipv6RoutingEnabled" db:"ipv6_routing_enabled"`
-	LastUpdated              *TimeNoMod              `json:"lastUpdated" db:"last_updated"`
-	LogsEnabled              *bool                   `json:"logsEnabled" db:"logs_enabled"`
-	LongDesc                 *string                 `json:"longDesc" db:"long_desc"`
-	LongDesc1                *string                 `json:"longDesc1" db:"long_desc_1"`
-	LongDesc2                *string                 `json:"longDesc2" db:"long_desc_2"`
-	MatchList                *[]DeliveryServiceMatch `json:"matchList"`
-	MaxDNSAnswers            *int                    `json:"maxDnsAnswers" db:"max_dns_answers"`
-	MidHeaderRewrite         *string                 `json:"midHeaderRewrite" db:"mid_header_rewrite"`
-	MissLat                  *float64                `json:"missLat" db:"miss_lat"`
-	MissLong                 *float64                `json:"missLong" db:"miss_long"`
-	MultiSiteOrigin          *bool                   `json:"multiSiteOrigin" db:"multi_site_origin"`
-	MultiSiteOriginAlgorithm *int                    `json:"multiSiteOriginAlgorithm" db:"multi_site_origin_algorithm"`
-	OriginShield             *string                 `json:"originShield" db:"origin_shield"`
-	OrgServerFQDN            *string                 `json:"orgServerFqdn" db:"org_server_fqdn"`
-	ProfileDesc              *string                 `json:"profileDescription"`
-	ProfileID                *int                    `json:"profileId" db:"profile"`
-	ProfileName              *string                 `json:"profileName"`
-	Protocol                 *int                    `json:"protocol" db:"protocol"`
-	QStringIgnore            *int                    `json:"qstringIgnore" db:"qstring_ignore"`
-	RangeRequestHandling     *int                    `json:"rangeRequestHandling" db:"range_request_handling"`
-	RegexRemap               *string                 `json:"regexRemap" db:"regex_remap"`
-	RegionalGeoBlocking      *bool                   `json:"regionalGeoBlocking" db:"regional_geo_blocking"`
-	RemapText                *string                 `json:"remapText" db:"remap_text"`
-	RoutingName              *string                 `json:"routingName" db:"routing_name"`
-	SigningAlgorithm         *string                 `json:"signingAlgorithm" db:"signing_algorithm"`
-	SSLKeyVersion            *int                    `json:"sslKeyVersion" db:"ssl_key_version"`
-	TRRequestHeaders         *string                 `json:"trRequestHeaders" db:"tr_request_headers"`
-	TRResponseHeaders        *string                 `json:"trResponseHeaders" db:"tr_response_headers"`
-	TenantID                 *int                    `json:"tenantId" db:"tenant_id"`
-	TypeName                 *string                 `json:"typeName"`
-	TypeID                   *int                    `json:"typeId" db:"type"`
-	XMLID                    *string                 `json:"xmlId" db:"xml_id"`
+	Active               *bool                   `json:"active" db:"active"`
+	CacheURL             *string                 `json:"cacheurl" db:"cacheurl"`
+	CCRDNSTTL            *int                    `json:"ccrDnsTtl" db:"ccr_dns_ttl"`
+	CDNID                *int                    `json:"cdnId" db:"cdn_id"`
+	CDNName              *string                 `json:"cdnName"`
+	CheckPath            *string                 `json:"checkPath" db:"check_path"`
+	DisplayName          *string                 `json:"displayName" db:"display_name"`
+	DNSBypassCNAME       *string                 `json:"dnsBypassCname" db:"dns_bypass_cname"`
+	DNSBypassIP          *string                 `json:"dnsBypassIp" db:"dns_bypass_ip"`
+	DNSBypassIP6         *string                 `json:"dnsBypassIp6" db:"dns_bypass_ip6"`
+	DNSBypassTTL         *int                    `json:"dnsBypassTtl" db:"dns_bypass_ttl"`
+	DSCP                 *int                    `json:"dscp" db:"dscp"`
+	EdgeHeaderRewrite    *string                 `json:"edgeHeaderRewrite" db:"edge_header_rewrite"`
+	FQPacingRate         *int                    `json:"fqPacingRate" db:"fq_pacing_rate"`
+	GeoLimit             *int                    `json:"geoLimit" db:"geo_limit"`
+	GeoLimitCountries    *string                 `json:"geoLimitCountries" db:"geo_limit_countries"`
+	GeoLimitRedirectURL  *string                 `json:"geoLimitRedirectURL" db:"geolimit_redirect_url"`
+	GeoProvider          *int                    `json:"geoProvider" db:"geo_provider"`
+	GlobalMaxMBPS        *int                    `json:"globalMaxMbps" db:"global_max_mbps"`
+	GlobalMaxTPS         *int                    `json:"globalMaxTps" db:"global_max_tps"`
+	HTTPBypassFQDN       *string                 `json:"httpBypassFqdn" db:"http_bypass_fqdn"`
+	ID                   *int                    `json:"id" db:"id"`
+	InfoURL              *string                 `json:"infoUrl" db:"info_url"`
+	InitialDispersion    *int                    `json:"initialDispersion" db:"initial_dispersion"`
+	IPV6RoutingEnabled   *bool                   `json:"ipv6RoutingEnabled" db:"ipv6_routing_enabled"`
+	LastUpdated          *TimeNoMod              `json:"lastUpdated" db:"last_updated"`
+	LogsEnabled          *bool                   `json:"logsEnabled" db:"logs_enabled"`
+	LongDesc             *string                 `json:"longDesc" db:"long_desc"`
+	LongDesc1            *string                 `json:"longDesc1" db:"long_desc_1"`
+	LongDesc2            *string                 `json:"longDesc2" db:"long_desc_2"`
+	MatchList            *[]DeliveryServiceMatch `json:"matchList"`
+	MaxDNSAnswers        *int                    `json:"maxDnsAnswers" db:"max_dns_answers"`
+	MidHeaderRewrite     *string                 `json:"midHeaderRewrite" db:"mid_header_rewrite"`
+	MissLat              *float64                `json:"missLat" db:"miss_lat"`
+	MissLong             *float64                `json:"missLong" db:"miss_long"`
+	MultiSiteOrigin      *bool                   `json:"multiSiteOrigin" db:"multi_site_origin"`
+	OriginShield         *string                 `json:"originShield" db:"origin_shield"`
+	OrgServerFQDN        *string                 `json:"orgServerFqdn" db:"org_server_fqdn"`
+	ProfileDesc          *string                 `json:"profileDescription"`
+	ProfileID            *int                    `json:"profileId" db:"profile"`
+	ProfileName          *string                 `json:"profileName"`
+	Protocol             *int                    `json:"protocol" db:"protocol"`
+	QStringIgnore        *int                    `json:"qstringIgnore" db:"qstring_ignore"`
+	RangeRequestHandling *int                    `json:"rangeRequestHandling" db:"range_request_handling"`
+	RegexRemap           *string                 `json:"regexRemap" db:"regex_remap"`
+	RegionalGeoBlocking  *bool                   `json:"regionalGeoBlocking" db:"regional_geo_blocking"`
+	RemapText            *string                 `json:"remapText" db:"remap_text"`
+	RoutingName          *string                 `json:"routingName" db:"routing_name"`
+	Signed               bool                    `json:"signed"`
+	SSLKeyVersion        *int                    `json:"sslKeyVersion" db:"ssl_key_version"`
+	TenantID             *int                    `json:"tenantId" db:"tenant_id"`
+	Type                 *string                 `json:"type"`
+	TypeID               *int                    `json:"typeId" db:"type"`
+	XMLID                *string                 `json:"xmlId" db:"xml_id"`
+	ExampleURLs          []string                `json:"exampleURLs"`
+}
+
+type DeliveryServiceNullableV12 struct {
+	DeliveryServiceNullable
+}
+
+type DeliveryServiceNullableV13 struct {
+	DeliveryServiceNullableV12
+	AnonymousBlockingEnabled *bool            `json:"anonymousBlockingEnabled" db:"anonymous_blocking_enabled"`
+	DeepCachingType          *DeepCachingType `json:"deepCachingType" db:"deep_caching_type"`
+	FQPacingRate             *int             `json:"fqPacingRate,omitempty"`
+	SigningAlgorithm         *string          `json:"signingAlgorithm" db:"signing_algorithm"`
+	Tenant                   *string          `json:"tenant,omitempty"`
+	TRResponseHeaders        *string          `json:"trResponseHeaders,omitempty"`
+	TRRequestHeaders         *string          `json:"trRequestHeaders,omitempty"`
 }
 
 // Value implements the driver.Valuer interface
@@ -282,5 +309,3 @@ type DeliveryServiceRouting struct {
 	RegionalAlternate int     `json:"regionalAlternate"`
 	RegionalDenied    int     `json:"regionalDenied"`
 }
-
-
diff --git a/traffic_ops/app/lib/TrafficOpsRoutes.pm b/traffic_ops/app/lib/TrafficOpsRoutes.pm
index 5c944b9..9ed303c 100644
--- a/traffic_ops/app/lib/TrafficOpsRoutes.pm
+++ b/traffic_ops/app/lib/TrafficOpsRoutes.pm
@@ -501,12 +501,7 @@ sub api_routes {
 
 	# -- DELIVERYSERVICES
 	# -- DELIVERYSERVICES: CRUD
-	$r->get("/api/$version/deliveryservices")->over( authenticated => 1, not_ldap => 1 )->to( 'Deliveryservice#index', namespace => $namespace );
-	$r->get( "/api/$version/deliveryservices/:id" => [ id => qr/\d+/ ] )->over( authenticated => 1, not_ldap => 1 )->to( 'Deliveryservice#show', namespace => $namespace );
-	$r->post("/api/$version/deliveryservices")->over( authenticated => 1, not_ldap => 1 )->to( 'Deliveryservice#create', namespace => $namespace );
-	$r->put("/api/$version/deliveryservices/:id" => [ id => qr/\d+/ ] )->over( authenticated => 1, not_ldap => 1 )->to( 'Deliveryservice#update', namespace => $namespace );
 	$r->put("/api/$version/deliveryservices/:id/safe" => [ id => qr/\d+/ ] )->over( authenticated => 1, not_ldap => 1 )->to( 'Deliveryservice#safe_update', namespace => $namespace );
-	$r->delete("/api/$version/deliveryservices/:id" => [ id => qr/\d+/ ] )->over( authenticated => 1, not_ldap => 1 )->to( 'Deliveryservice#delete', namespace => $namespace );
 
 	# get all delivery services associated with a server (from deliveryservice_server table)
 	$r->get( "/api/$version/servers/:id/deliveryservices" => [ id => qr/\d+/ ] )->over( authenticated => 1, not_ldap => 1 )->to( 'Deliveryservice#get_deliveryservices_by_serverId', namespace => $namespace );
diff --git a/traffic_ops/traffic_ops_golang/api/change_log.go b/traffic_ops/traffic_ops_golang/api/change_log.go
index 1b30429..5cbede6 100644
--- a/traffic_ops/traffic_ops_golang/api/change_log.go
+++ b/traffic_ops/traffic_ops_golang/api/change_log.go
@@ -85,9 +85,8 @@ func CreateChangeLogMsg(level string, user auth.CurrentUser, db *sqlx.DB, msg st
 	return nil
 }
 
-func CreateChangeLogRaw(level string, message string, user auth.CurrentUser, db *sql.DB) error {
-	if _, err := db.Exec(`INSERT INTO log (level, message, tm_user) VALUES ($1, $2, $3)`, level, message, user.ID); err != nil {
-		return fmt.Errorf("inserting change log level '%v' message '%v' user '%v': %v", level, message, user.ID, err)
+func CreateChangeLogRaw(level string, msg string, user auth.CurrentUser, db *sql.DB) {
+	if _, err := db.Exec(`INSERT INTO log (level, message, tm_user) VALUES ($1, $2, $3)`, level, msg, user.ID); err != nil {
+		log.Errorln("Inserting change log level '" + level + "' message '" + msg + "' user '" + user.UserName + "': " + err.Error())
 	}
-	return nil
 }
diff --git a/traffic_ops/traffic_ops_golang/api/shared_handlers.go b/traffic_ops/traffic_ops_golang/api/shared_handlers.go
index 408bab8..9ec3cb9 100644
--- a/traffic_ops/traffic_ops_golang/api/shared_handlers.go
+++ b/traffic_ops/traffic_ops_golang/api/shared_handlers.go
@@ -103,14 +103,9 @@ func GetCombinedParams(r *http.Request) (map[string]string, error) {
 //      we lose the ability to unmarshal the struct if a struct implementing the interface is passed in,
 //      because when when it is de-referenced it is a pointer to an interface. A new copy is created so that
 //      there are no issues with concurrent goroutines
-func decodeAndValidateRequestBody(r *http.Request, v Validator, db *sqlx.DB) (interface{}, []error) {
-	typ := reflect.TypeOf(v)
-	if typ.Kind() == reflect.Ptr {
-		typ = typ.Elem()
-	}
-	payload := reflect.New(typ).Interface()
+func decodeAndValidateRequestBody(r *http.Request, v Validator, db *sqlx.DB, user auth.CurrentUser) (interface{}, []error) {
+	payload := reflect.Indirect(reflect.ValueOf(v)).Addr().Interface() // does a shallow copy v's internal struct members
 	defer r.Body.Close()
-
 	if err := json.NewDecoder(r.Body).Decode(payload); err != nil {
 		return nil, []error{err}
 	}
@@ -175,17 +170,6 @@ func UpdateHandler(typeRef Updater, db *sqlx.DB) http.HandlerFunc {
 	return func(w http.ResponseWriter, r *http.Request) {
 		//create error function with ResponseWriter and Request
 		handleErrs := tc.GetHandleErrorsFunc(w, r)
-		//create local instance of the shared typeRef pointer
-		//no operations should be made on the typeRef
-		//decode the body and validate the request struct
-		decoded, errs := decodeAndValidateRequestBody(r, typeRef, db)
-		if len(errs) > 0 {
-			handleErrs(http.StatusBadRequest, errs...)
-			return
-		}
-		u := decoded.(Updater)
-		//now we have a validated local object to update
-
 		//collect path parameters and user from context
 		ctx := r.Context()
 		params, err := GetCombinedParams(r)
@@ -201,11 +185,23 @@ func UpdateHandler(typeRef Updater, db *sqlx.DB) http.HandlerFunc {
 			return
 		}
 
+		//create local instance of the shared typeRef pointer
+		//no operations should be made on the typeRef
+		//decode the body and validate the request struct
+		decoded, errs := decodeAndValidateRequestBody(r, typeRef, db, *user)
+		if len(errs) > 0 {
+			handleErrs(http.StatusBadRequest, errs...)
+			return
+		}
+		u := decoded.(Updater)
+		//now we have a validated local object to update
+
 		keyFields := u.GetKeyFieldsInfo() //expecting a slice of the key fields info which is a struct with the field name and a function to convert a string into a {}interface of the right type. in most that will be [{Field:"id",Func: func(s string)({}interface,error){return strconv.Atoi(s)}}]
 		keys, ok := u.GetKeys()           // a map of keyField to keyValue where keyValue is an {}interface
 		if !ok {
 			log.Errorf("unable to parse keys from request: %++v", u)
 			handleErrs(http.StatusBadRequest, errors.New("unable to parse required keys from request body"))
+			return // TODO verify?
 		}
 		for _, keyFieldInfo := range keyFields {
 			paramKey := params[keyFieldInfo.Field]
@@ -219,6 +215,7 @@ func UpdateHandler(typeRef Updater, db *sqlx.DB) http.HandlerFunc {
 			if err != nil {
 				log.Errorf("failed to parse key %s: %s", keyFieldInfo.Field, err)
 				handleErrs(http.StatusBadRequest, errors.New("failed to parse key: "+keyFieldInfo.Field))
+				return
 			}
 
 			if paramValue != keys[keyFieldInfo.Field] {
@@ -252,7 +249,7 @@ func UpdateHandler(typeRef Updater, db *sqlx.DB) http.HandlerFunc {
 		resp := struct {
 			Response interface{} `json:"response"`
 			tc.Alerts
-		}{u, tc.CreateAlerts(tc.SuccessLevel, u.GetType()+" was updated.")}
+		}{[]interface{}{u}, tc.CreateAlerts(tc.SuccessLevel, u.GetType()+" was updated.")}
 
 		respBts, err := json.Marshal(resp)
 		if err != nil {
@@ -361,8 +358,16 @@ func CreateHandler(typeRef Creator, db *sqlx.DB) http.HandlerFunc {
 	return func(w http.ResponseWriter, r *http.Request) {
 		handleErrs := tc.GetHandleErrorsFunc(w, r)
 
+		ctx := r.Context()
+		user, err := auth.GetCurrentUser(ctx)
+		if err != nil {
+			log.Errorf("unable to retrieve current user from context: %s", err)
+			handleErrs(http.StatusInternalServerError, err)
+			return
+		}
+
 		//decode the body and validate the request struct
-		decoded, errs := decodeAndValidateRequestBody(r, typeRef, db)
+		decoded, errs := decodeAndValidateRequestBody(r, typeRef, db, *user)
 		if len(errs) > 0 {
 			handleErrs(http.StatusBadRequest, errs...)
 			return
@@ -371,14 +376,6 @@ func CreateHandler(typeRef Creator, db *sqlx.DB) http.HandlerFunc {
 		log.Debugf("%++v", i)
 		//now we have a validated local object to insert
 
-		ctx := r.Context()
-		user, err := auth.GetCurrentUser(ctx)
-		if err != nil {
-			log.Errorf("unable to retrieve current user from context: %s", err)
-			handleErrs(http.StatusInternalServerError, err)
-			return
-		}
-
 		// if the object has tenancy enabled, check that user is able to access the tenant
 		if t, ok := i.(Tenantable); ok {
 			authorized, err := t.IsTenantAuthorized(*user, db)
diff --git a/traffic_ops/traffic_ops_golang/crconfig/handler.go b/traffic_ops/traffic_ops_golang/crconfig/handler.go
index 6822b60..6dfd461 100644
--- a/traffic_ops/traffic_ops_golang/crconfig/handler.go
+++ b/traffic_ops/traffic_ops_golang/crconfig/handler.go
@@ -193,9 +193,7 @@ func SnapshotHandler(db *sqlx.DB, cfg config.Config) http.HandlerFunc {
 			handleErrs(http.StatusInternalServerError, err)
 			return
 		}
-		if err := api.CreateChangeLogRaw(api.ApiChange, "Snapshot of CRConfig performed for "+cdn, *user, db.DB); err != nil {
-			log.Errorln("creating CRConfig snapshot changelog: " + err.Error())
-		}
+		api.CreateChangeLogRaw(api.ApiChange, "Snapshot of CRConfig performed for "+cdn, *user, db.DB)
 		w.WriteHeader(http.StatusOK) // TODO change to 204 No Content in new version
 	}
 }
@@ -240,9 +238,7 @@ func SnapshotOldGUIHandler(db *sqlx.DB, cfg config.Config) http.HandlerFunc {
 			writePerlHTMLErr(w, r, err)
 			return
 		}
-		if err := api.CreateChangeLogRaw(api.ApiChange, "Snapshot of CRConfig performed for "+cdn, *user, db.DB); err != nil {
-			log.Errorln("creating CRConfig snapshot changelog: " + err.Error())
-		}
+		api.CreateChangeLogRaw(api.ApiChange, "Snapshot of CRConfig performed for "+cdn, *user, db.DB)
 		http.Redirect(w, r, "/tools/flash_and_close/"+url.PathEscape("Successfully wrote the CRConfig.json!"), http.StatusFound)
 	}
 }
diff --git a/traffic_ops/traffic_ops_golang/dbhelpers/db_helpers.go b/traffic_ops/traffic_ops_golang/dbhelpers/db_helpers.go
index 0c8774b..3349504 100644
--- a/traffic_ops/traffic_ops_golang/dbhelpers/db_helpers.go
+++ b/traffic_ops/traffic_ops_golang/dbhelpers/db_helpers.go
@@ -20,12 +20,14 @@ package dbhelpers
  */
 
 import (
+	"database/sql"
 	"errors"
 	"strings"
 
 	"github.com/apache/incubator-trafficcontrol/lib/go-log"
 	"github.com/apache/incubator-trafficcontrol/lib/go-tc"
 
+	"github.com/jmoiron/sqlx"
 	"github.com/lib/pq"
 )
 
@@ -118,3 +120,27 @@ func ParsePQUniqueConstraintError(err *pq.Error) (error, tc.ApiErrorType) {
 	log.Error.Printf("failed to parse unique constraint from pq error: %v", err)
 	return tc.DBError, tc.SystemError
 }
+
+// FinishTx commits the transaction if commit is true when it's called, otherwise it rolls back the transaction. This is designed to be called in a defer.
+func FinishTx(tx *sql.Tx, commit *bool) {
+	if tx == nil {
+		return
+	}
+	if !*commit {
+		tx.Rollback()
+		return
+	}
+	tx.Commit()
+}
+
+// FinishTxX commits the transaction if commit is true when it's called, otherwise it rolls back the transaction. This is designed to be called in a defer.
+func FinishTxX(tx *sqlx.Tx, commit *bool) {
+	if tx == nil {
+		return
+	}
+	if !*commit {
+		tx.Rollback()
+		return
+	}
+	tx.Commit()
+}
diff --git a/traffic_ops/traffic_ops_golang/deliveryservice/deliveryservices.go b/traffic_ops/traffic_ops_golang/deliveryservice/deliveryservices.go
deleted file mode 100644
index 9df72a9..0000000
--- a/traffic_ops/traffic_ops_golang/deliveryservice/deliveryservices.go
+++ /dev/null
@@ -1,406 +0,0 @@
-package deliveryservice
-
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-import (
-	"errors"
-	"fmt"
-	"regexp"
-	"strings"
-
-	"github.com/apache/incubator-trafficcontrol/lib/go-log"
-	"github.com/apache/incubator-trafficcontrol/lib/go-tc"
-	"github.com/asaskevich/govalidator"
-	validation "github.com/go-ozzo/ozzo-validation"
-	"github.com/go-ozzo/ozzo-validation/is"
-
-	"github.com/apache/incubator-trafficcontrol/traffic_ops/traffic_ops_golang/api"
-	"github.com/apache/incubator-trafficcontrol/traffic_ops/traffic_ops_golang/auth"
-	"github.com/apache/incubator-trafficcontrol/traffic_ops/traffic_ops_golang/dbhelpers"
-	"github.com/apache/incubator-trafficcontrol/traffic_ops/traffic_ops_golang/tenant"
-	"github.com/apache/incubator-trafficcontrol/traffic_ops/traffic_ops_golang/tovalidate"
-	"github.com/jmoiron/sqlx"
-	"github.com/lib/pq"
-)
-
-//we need a type alias to define functions on
-type TODeliveryService tc.DeliveryServiceNullable
-
-//the refType is passed into the handlers where a copy of its type is used to decode the json.
-var refType = TODeliveryService(tc.DeliveryServiceNullable{})
-
-func GetRefType() *TODeliveryService {
-	return &refType
-}
-
-func (ds TODeliveryService) GetKeyFieldsInfo() []api.KeyFieldInfo {
-	return []api.KeyFieldInfo{{"id", api.GetIntKey}}
-}
-
-//Implementation of the Identifier, Validator interface functions
-func (ds TODeliveryService) GetKeys() (map[string]interface{}, bool) {
-	if ds.ID == nil {
-		return map[string]interface{}{"id": 0}, false
-	}
-	return map[string]interface{}{"id": *ds.ID}, true
-}
-
-func (ds *TODeliveryService) SetKeys(keys map[string]interface{}) {
-	i, _ := keys["id"].(int) //this utilizes the non panicking type assertion, if the thrown away ok variable is false i will be the zero of the type, 0 here.
-	ds.ID = &i
-}
-
-func (ds *TODeliveryService) GetAuditName() string {
-	if ds.XMLID != nil {
-		return *ds.XMLID
-	}
-	return ""
-}
-
-func (ds *TODeliveryService) GetType() string {
-	return "ds"
-}
-
-func Validate(db *sqlx.DB, ds *tc.DeliveryServiceNullable) []error {
-	if ds == nil {
-		return []error{}
-	}
-	tods := TODeliveryService(*ds)
-	return tods.Validate(db)
-}
-
-func (ds *TODeliveryService) Validate(db *sqlx.DB) []error {
-
-	// Custom Examples:
-	// Just add isCIDR as a parameter to Validate()
-	// isCIDR := validation.NewStringRule(govalidator.IsCIDR, "must be a valid CIDR address")
-	isHost := validation.NewStringRule(govalidator.IsHost, "must be a valid hostname")
-	noPeriods := validation.NewStringRule(tovalidate.NoPeriods, "cannot contain periods")
-	noSpaces := validation.NewStringRule(tovalidate.NoSpaces, "cannot contain spaces")
-	neverOrAlways := validation.NewStringRule(tovalidate.IsOneOfStringICase("NEVER", "ALWAYS"),
-		"must be one of 'NEVER' or 'ALWAYS'")
-
-	// Validate that the required fields are sent first to prevent panics below
-	errs := validation.Errors{
-		"active":                   validation.Validate(ds.Active, validation.NotNil),
-		"anonymousBlockingEnabled": validation.Validate(ds.AnonymousBlockingEnabled, validation.NotNil),
-		"cdnId":                    validation.Validate(ds.CDNID, validation.Required),
-		"displayName":              validation.Validate(ds.DisplayName, validation.Required, validation.Length(1, 48)),
-		"deepCachingType":          validation.Validate(neverOrAlways),
-		"dnsBypassIp":              validation.Validate(ds.DNSBypassIP, is.IP),
-		"dnsBypassIp6":             validation.Validate(ds.DNSBypassIP6, is.IPv6),
-		"dscp":                     validation.Validate(ds.DSCP, validation.NotNil, validation.Min(0)),
-		"geoLimit":                 validation.Validate(ds.GeoLimit, validation.NotNil),
-		"geoProvider":              validation.Validate(ds.GeoProvider, validation.NotNil),
-		"infoUrl":                  validation.Validate(ds.InfoURL, is.URL),
-		"logsEnabled":              validation.Validate(ds.LogsEnabled, validation.NotNil),
-		"orgServerFqdn":            validation.Validate(ds.OrgServerFQDN, is.URL),
-		"regionalGeoBlocking":      validation.Validate(ds.RegionalGeoBlocking, validation.NotNil),
-		"routingName":              validation.Validate(ds.RoutingName, isHost, noPeriods, validation.Length(1, 48)),
-		"typeId":                   validation.Validate(ds.TypeID, validation.Required, validation.Min(1)),
-		"xmlId":                    validation.Validate(ds.XMLID, noSpaces, noPeriods, validation.Length(1, 48)),
-	}
-
-	if errs != nil {
-		return tovalidate.ToErrors(errs)
-	}
-
-	errsResponse := ds.validateTypeFields(db)
-	if errsResponse != nil {
-		return errsResponse
-	}
-
-	return nil
-}
-
-func (ds *TODeliveryService) validateTypeFields(db *sqlx.DB) []error {
-	fmt.Printf("validateTypeFields\n")
-	// Validate the TypeName related fields below
-	var typeName string
-	var err error
-	DNSRegexType := "^DNS.*$"
-	HTTPRegexType := "^HTTP.*$"
-	SteeringRegexType := "^STEERING.*$"
-
-	if db != nil && ds.TypeID != nil {
-		typeID := *ds.TypeID
-		typeName, err = getTypeName(db, typeID)
-		if err != nil {
-			return []error{err}
-		}
-	}
-
-	if typeName != "" {
-		errs := validation.Errors{
-			"initialDispersion": validation.Validate(ds.InitialDispersion,
-				validation.By(requiredIfMatchesTypeName([]string{DNSRegexType, HTTPRegexType}, typeName))),
-			"ipv6RoutingEnabled": validation.Validate(ds.IPV6RoutingEnabled,
-				validation.By(requiredIfMatchesTypeName([]string{SteeringRegexType, DNSRegexType, HTTPRegexType}, typeName))),
-			"missLat": validation.Validate(ds.MissLat,
-				validation.By(requiredIfMatchesTypeName([]string{DNSRegexType, HTTPRegexType}, typeName))),
-			"missLong": validation.Validate(ds.MissLong,
-				validation.By(requiredIfMatchesTypeName([]string{DNSRegexType, HTTPRegexType}, typeName))),
-			"multiSiteOrigin": validation.Validate(ds.MultiSiteOrigin,
-				validation.By(requiredIfMatchesTypeName([]string{DNSRegexType, HTTPRegexType}, typeName))),
-			"orgServerFqdn": validation.Validate(ds.OrgServerFQDN,
-				validation.By(requiredIfMatchesTypeName([]string{DNSRegexType, HTTPRegexType}, typeName))),
-			"protocol": validation.Validate(ds.Protocol,
-				validation.By(requiredIfMatchesTypeName([]string{SteeringRegexType, DNSRegexType, HTTPRegexType}, typeName))),
-			"qstringIgnore": validation.Validate(ds.QStringIgnore,
-				validation.By(requiredIfMatchesTypeName([]string{DNSRegexType, HTTPRegexType}, typeName))),
-			"rangeRequestHandling": validation.Validate(ds.RangeRequestHandling,
-				validation.By(requiredIfMatchesTypeName([]string{DNSRegexType, HTTPRegexType}, typeName))),
-		}
-		return tovalidate.ToErrors(errs)
-	}
-	return nil
-}
-
-func requiredIfMatchesTypeName(patterns []string, typeName string) func(interface{}) error {
-	return func(value interface{}) error {
-
-		pattern := strings.Join(patterns, "|")
-		var err error
-		var match bool
-		if typeName != "" {
-			match, err = regexp.MatchString(pattern, typeName)
-			if match {
-				return fmt.Errorf("is required if type is '%s'", typeName)
-			}
-		}
-		return err
-	}
-}
-
-// TODO: drichardson - refactor to the types.go once implemented.
-func getTypeName(db *sqlx.DB, typeID int) (string, error) {
-
-	query := `SELECT name from type where id=$1`
-
-	var rows *sqlx.Rows
-	var err error
-
-	rows, err = db.Queryx(query, typeID)
-	if err != nil {
-		return "", err
-	}
-	defer rows.Close()
-
-	typeResults := []tc.Type{}
-	for rows.Next() {
-		var s tc.Type
-		if err = rows.StructScan(&s); err != nil {
-			return "", fmt.Errorf("getting Type: %v", err)
-		}
-		typeResults = append(typeResults, s)
-	}
-
-	typeName := typeResults[0].Name
-	return typeName, err
-}
-
-//The TODeliveryService implementation of the Updater interface
-//all implementations of Updater should use transactions and return the proper errorType
-//ParsePQUniqueConstraintError is used to determine if a delivery service with conflicting values exists
-//if so, it will return an errorType of DataConflict and the type should be appended to the
-//generic error message returned
-func (ds *TODeliveryService) Update(db *sqlx.DB, user auth.CurrentUser) (error, tc.ApiErrorType) {
-	tx, err := db.Beginx()
-	defer func() {
-		if tx == nil {
-			return
-		}
-		if err != nil {
-			tx.Rollback()
-			return
-		}
-		tx.Commit()
-	}()
-
-	if err != nil {
-		log.Error.Printf("could not begin transaction: %v", err)
-		return tc.DBError, tc.SystemError
-	}
-	log.Debugf("about to run exec query: %s with ds: %++v", updateDSQuery(), ds)
-	resultRows, err := tx.NamedQuery(updateDSQuery(), ds)
-	if err != nil {
-		if err, ok := err.(*pq.Error); ok {
-			err, eType := dbhelpers.ParsePQUniqueConstraintError(err)
-			if eType == tc.DataConflictError {
-				return errors.New("a delivery service with " + err.Error()), eType
-			}
-			return err, eType
-		}
-		log.Errorf("received error: %++v from update execution", err)
-		return tc.DBError, tc.SystemError
-	}
-	var lastUpdated tc.TimeNoMod
-	rowsAffected := 0
-	for resultRows.Next() {
-		rowsAffected++
-		if err := resultRows.Scan(&lastUpdated); err != nil {
-			log.Error.Printf("could not scan lastUpdated from insert: %s\n", err)
-			return tc.DBError, tc.SystemError
-		}
-	}
-	log.Debugf("lastUpdated: %++v", lastUpdated)
-	ds.LastUpdated = &lastUpdated
-	if rowsAffected != 1 {
-		if rowsAffected < 1 {
-			return errors.New("no delivery service found with this id"), tc.DataMissingError
-		}
-		return fmt.Errorf("this update affected too many rows: %d", rowsAffected), tc.SystemError
-	}
-	return nil, tc.NoError
-}
-
-// Create implements the Creator interface.
-//all implementations of Creator should use transactions and return the proper errorType
-//ParsePQUniqueConstraintError is used to determine if a ds with conflicting values exists
-//if so, it will return an errorType of DataConflict and the type should be appended to the
-//generic error message returned
-//The insert sql returns the id and lastUpdated values of the newly inserted ds and have
-//to be added to the struct
-func (ds *TODeliveryService) Create(db *sqlx.DB, user auth.CurrentUser) (error, tc.ApiErrorType) {
-	tx, err := db.Beginx()
-	defer func() {
-		if tx == nil {
-			return
-		}
-		if err != nil {
-			tx.Rollback()
-			return
-		}
-		tx.Commit()
-	}()
-
-	if err != nil {
-		log.Error.Printf("could not begin transaction: %v", err)
-		return tc.DBError, tc.SystemError
-	}
-	fmt.Printf("ds ---> %v\n", ds)
-	resultRows, err := tx.NamedQuery(insertDSQuery(), ds)
-	if err != nil {
-		if pqerr, ok := err.(*pq.Error); ok {
-			err, eType := dbhelpers.ParsePQUniqueConstraintError(pqerr)
-			return errors.New("a delivery service with " + err.Error()), eType
-		}
-		log.Errorf("received non pq error: %++v from create execution", err)
-		return tc.DBError, tc.SystemError
-	}
-	var id int
-	var lastUpdated tc.TimeNoMod
-	rowsAffected := 0
-	for resultRows.Next() {
-		rowsAffected++
-		if err := resultRows.Scan(&id, &lastUpdated); err != nil {
-			log.Error.Printf("could not scan id from insert: %s\n", err)
-			return tc.DBError, tc.SystemError
-		}
-	}
-	if rowsAffected == 0 {
-		err = errors.New("no delivery service was inserted, no id was returned")
-		log.Errorln(err)
-		return tc.DBError, tc.SystemError
-	} else if rowsAffected > 1 {
-		err = errors.New("too many ids returned from delivery service insert")
-		log.Errorln(err)
-		return tc.DBError, tc.SystemError
-	}
-	ds.SetKeys(map[string]interface{}{"id": id})
-	ds.LastUpdated = &lastUpdated
-	return nil, tc.NoError
-}
-
-//The DeliveryService implementation of the Deleter interface
-//all implementations of Deleter should use transactions and return the proper errorType
-func (ds *TODeliveryService) Delete(db *sqlx.DB, user auth.CurrentUser) (error, tc.ApiErrorType) {
-	tx, err := db.Beginx()
-	defer func() {
-		if tx == nil {
-			return
-		}
-		if err != nil {
-			tx.Rollback()
-			return
-		}
-		tx.Commit()
-	}()
-
-	if err != nil {
-		log.Error.Printf("could not begin transaction: %v", err)
-		return tc.DBError, tc.SystemError
-	}
-	log.Debugf("about to run exec query: %s with Delivery Service: %++v", deleteDSQuery(), ds)
-	result, err := tx.NamedExec(deleteDSQuery(), ds)
-	if err != nil {
-		log.Errorf("received error: %++v from delete execution", err)
-		return tc.DBError, tc.SystemError
-	}
-	rowsAffected, err := result.RowsAffected()
-	if err != nil {
-		return tc.DBError, tc.SystemError
-	}
-	if rowsAffected != 1 {
-		if rowsAffected < 1 {
-			return errors.New("no delivery service with that id found"), tc.DataMissingError
-		}
-		return fmt.Errorf("this create affected too many rows: %d", rowsAffected), tc.SystemError
-	}
-	return nil, tc.NoError
-}
-
-// IsTenantAuthorized implements the Tenantable interface to ensure the user is authorized on the deliveryservice tenant
-func (ds *TODeliveryService) IsTenantAuthorized(user auth.CurrentUser, db *sqlx.DB) (bool, error) {
-	if ds.TenantID == nil {
-		log.Debugf("tenantID is nil")
-		return false, errors.New("tenantID is nil")
-	}
-	return tenant.IsResourceAuthorizedToUser(*ds.TenantID, user, db)
-}
-
-//TODO: drichardson - plumb these out!
-func updateDSQuery() string {
-	query := `UPDATE
-cdn SET
-dnssec_enabled=:dnssec_enabled,
-domain_name=:domain_name,
-name=:name
-WHERE id=:id RETURNING last_updated`
-	return query
-}
-
-//TODO: drichardson - plumb these out!
-func insertDSQuery() string {
-	query := `INSERT INTO deliveryservice (
-dnssec_enabled,
-domain_name,
-name) VALUES (
-:dnssec_enabled,
-:domain_name,
-:name) RETURNING id,last_updated`
-	return query
-}
-
-func deleteDSQuery() string {
-	query := `DELETE FROM deliveryservice
-WHERE id=:id`
-	return query
-}
diff --git a/traffic_ops/traffic_ops_golang/deliveryservice/deliveryservicesv12.go b/traffic_ops/traffic_ops_golang/deliveryservice/deliveryservicesv12.go
new file mode 100644
index 0000000..c5c7ca8
--- /dev/null
+++ b/traffic_ops/traffic_ops_golang/deliveryservice/deliveryservicesv12.go
@@ -0,0 +1,433 @@
+package deliveryservice
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import (
+	"database/sql"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"net/http"
+	"regexp"
+	"strings"
+
+	"github.com/apache/incubator-trafficcontrol/lib/go-log"
+	"github.com/apache/incubator-trafficcontrol/lib/go-tc"
+	"github.com/apache/incubator-trafficcontrol/lib/go-util"
+	"github.com/apache/incubator-trafficcontrol/traffic_ops/traffic_ops_golang/api"
+	"github.com/apache/incubator-trafficcontrol/traffic_ops/traffic_ops_golang/auth"
+	"github.com/apache/incubator-trafficcontrol/traffic_ops/traffic_ops_golang/config"
+	"github.com/apache/incubator-trafficcontrol/traffic_ops/traffic_ops_golang/tenant"
+	"github.com/apache/incubator-trafficcontrol/traffic_ops/traffic_ops_golang/tovalidate"
+
+	"github.com/asaskevich/govalidator"
+	"github.com/go-ozzo/ozzo-validation"
+	"github.com/jmoiron/sqlx"
+)
+
+type TODeliveryServiceV12 struct {
+	tc.DeliveryServiceNullableV12
+	Cfg config.Config
+	DB  *sqlx.DB
+}
+
+func GetRefTypeV12(cfg config.Config, db *sqlx.DB) *TODeliveryServiceV12 {
+	return &TODeliveryServiceV12{Cfg: cfg, DB: db}
+}
+
+func (ds TODeliveryServiceV12) GetKeyFieldsInfo() []api.KeyFieldInfo {
+	return []api.KeyFieldInfo{{"id", api.GetIntKey}}
+}
+
+func (ds TODeliveryServiceV12) GetKeys() (map[string]interface{}, bool) {
+	if ds.ID == nil {
+		return map[string]interface{}{"id": 0}, false
+	}
+	return map[string]interface{}{"id": *ds.ID}, true
+}
+
+func (ds *TODeliveryServiceV12) SetKeys(keys map[string]interface{}) {
+	i, _ := keys["id"].(int) //this utilizes the non panicking type assertion, if the thrown away ok variable is false i will be the zero of the type, 0 here.
+	ds.ID = &i
+}
+
+func (ds *TODeliveryServiceV12) GetAuditName() string {
+	if ds.XMLID != nil {
+		return *ds.XMLID
+	}
+	return ""
+}
+
+func (ds *TODeliveryServiceV12) GetType() string {
+	return "ds"
+}
+
+func ValidateV12(db *sqlx.DB, ds *tc.DeliveryServiceNullableV12) []error {
+	if ds == nil {
+		return []error{}
+	}
+	tods := TODeliveryServiceV12{DB: db} // TODO pass config?
+	return tods.Validate(db)
+}
+
+func (ds *TODeliveryServiceV12) Sanitize(db *sqlx.DB) {
+	sanitizeV12(&ds.DeliveryServiceNullableV12)
+}
+
+func sanitizeV12(ds *tc.DeliveryServiceNullableV12) {
+	if ds.GeoLimitCountries != nil {
+		*ds.GeoLimitCountries = strings.ToUpper(strings.Replace(*ds.GeoLimitCountries, " ", "", -1))
+	}
+	if ds.ProfileID != nil && *ds.ProfileID == -1 {
+		ds.ProfileID = nil
+	}
+	if ds.EdgeHeaderRewrite != nil && strings.TrimSpace(*ds.EdgeHeaderRewrite) == "" {
+		ds.EdgeHeaderRewrite = nil
+	}
+	if ds.MidHeaderRewrite != nil && strings.TrimSpace(*ds.MidHeaderRewrite) == "" {
+		ds.MidHeaderRewrite = nil
+	}
+}
+
+// getDSTenantIDByID returns the tenant ID, whether the delivery service exists, and any error.
+// Note the id may be nil, even if true is returned, if the delivery service exists but its tenant_id field is null.
+func getDSTenantIDByID(db *sql.DB, id int) (*int, bool, error) {
+	tenantID := (*int)(nil)
+	if err := db.QueryRow(`SELECT tenant_id FROM deliveryservice where id = $1`, id).Scan(&tenantID); err != nil {
+		if err == sql.ErrNoRows {
+			return nil, false, nil
+		}
+		return nil, false, fmt.Errorf("querying tenant ID for delivery service ID '%v': %v", id, err)
+	}
+	return tenantID, true, nil
+}
+
+// getDSTenantIDByName returns the tenant ID, whether the delivery service exists, and any error.
+// Note the id may be nil, even if true is returned, if the delivery service exists but its tenant_id field is null.
+func getDSTenantIDByName(db *sql.DB, name string) (*int, bool, error) {
+	tenantID := (*int)(nil)
+	if err := db.QueryRow(`SELECT tenant_id FROM deliveryservice where xml_id = $1`, name).Scan(&tenantID); err != nil {
+		if err == sql.ErrNoRows {
+			return nil, false, nil
+		}
+		return nil, false, fmt.Errorf("querying tenant ID for delivery service name '%v': %v", name, err)
+	}
+	return tenantID, true, nil
+}
+
+// LoadXMLID loads the DeliveryService's xml_id from the database, from the ID. Returns whether the delivery service was found, and any error.
+func (ds *TODeliveryServiceV12) LoadXMLID(db *sqlx.DB) (bool, error) {
+	if ds.ID == nil {
+		return false, errors.New("missing ID")
+	}
+
+	xmlID := ""
+	if err := db.QueryRow(`SELECT xml_id FROM deliveryservice where id = $1`, ds.ID).Scan(&xmlID); err != nil {
+		if err == sql.ErrNoRows {
+			return false, nil
+		}
+		return false, fmt.Errorf("querying xml_id for delivery service ID '%v': %v", *ds.ID, err)
+	}
+	ds.XMLID = &xmlID
+	return true, nil
+}
+
+// IsTenantAuthorized checks that the user is authorized for both the delivery service's existing tenant, and the new tenant they're changing it to (if different).
+func (ds *TODeliveryServiceV12) IsTenantAuthorized(user auth.CurrentUser, db *sqlx.DB) (bool, error) {
+	return isTenantAuthorized(user, db, &ds.DeliveryServiceNullableV12)
+}
+
+func isTenantAuthorized(user auth.CurrentUser, db *sqlx.DB, ds *tc.DeliveryServiceNullableV12) (bool, error) {
+	if ds.ID == nil && ds.XMLID == nil {
+		return false, errors.New("delivery service has no ID or XMLID")
+	}
+
+	existingID, err := (*int)(nil), error(nil)
+	if ds.ID != nil {
+		existingID, _, err = getDSTenantIDByID(db.DB, *ds.ID) // ignore exists return - if the DS is new, we only need to check the user input tenant
+	} else {
+		existingID, _, err = getDSTenantIDByName(db.DB, *ds.XMLID) // ignore exists return - if the DS is new, we only need to check the user input tenant
+	}
+	if err != nil {
+		return false, errors.New("getting tenant ID: " + err.Error())
+	}
+
+	if ds.TenantID == nil {
+		ds.TenantID = existingID
+	}
+	if existingID != nil && existingID != ds.TenantID {
+		userAuthorizedForExistingDSTenant, err := tenant.IsResourceAuthorizedToUser(*existingID, user, db)
+		if err != nil {
+			return false, errors.New("checking authorization for existing DS ID: " + err.Error())
+		}
+		if !userAuthorizedForExistingDSTenant {
+			return false, nil
+		}
+	}
+	if ds.TenantID != nil {
+		userAuthorizedForNewDSTenant, err := tenant.IsResourceAuthorizedToUser(*ds.TenantID, user, db)
+		if err != nil {
+			return false, errors.New("checking authorization for new DS ID: " + err.Error())
+		}
+		if !userAuthorizedForNewDSTenant {
+			return false, nil
+		}
+	}
+	return true, nil
+}
+
+func (ds *TODeliveryServiceV12) Validate(db *sqlx.DB) []error {
+	return validateV12(db, &ds.DeliveryServiceNullableV12)
+}
+
+func validateV12(db *sqlx.DB, ds *tc.DeliveryServiceNullableV12) []error {
+	sanitizeV12(ds)
+	// Custom Examples:
+	// Just add isCIDR as a parameter to Validate()
+	// isCIDR := validation.NewStringRule(govalidator.IsCIDR, "must be a valid CIDR address")
+	isDNSName := validation.NewStringRule(govalidator.IsDNSName, "must be a valid hostname")
+	noPeriods := validation.NewStringRule(tovalidate.NoPeriods, "cannot contain periods")
+	noSpaces := validation.NewStringRule(tovalidate.NoSpaces, "cannot contain spaces")
+
+	// Validate that the required fields are sent first to prevent panics below
+	errs := validation.Errors{
+		"active":              validation.Validate(ds.Active, validation.NotNil),
+		"cdnId":               validation.Validate(ds.CDNID, validation.Required),
+		"displayName":         validation.Validate(ds.DisplayName, validation.Required, validation.Length(1, 48)),
+		"dscp":                validation.Validate(ds.DSCP, validation.NotNil, validation.Min(0)),
+		"geoLimit":            validation.Validate(ds.GeoLimit, validation.NotNil),
+		"geoProvider":         validation.Validate(ds.GeoProvider, validation.NotNil),
+		"logsEnabled":         validation.Validate(ds.LogsEnabled, validation.NotNil),
+		"regionalGeoBlocking": validation.Validate(ds.RegionalGeoBlocking, validation.NotNil),
+		"routingName":         validation.Validate(ds.RoutingName, isDNSName, noPeriods, validation.Length(1, 48)),
+		"typeId":              validation.Validate(ds.TypeID, validation.Required, validation.Min(1)),
+		"xmlId":               validation.Validate(ds.XMLID, noSpaces, noPeriods, validation.Length(1, 48)),
+	}
+	if errs != nil {
+		return tovalidate.ToErrors(errs)
+	}
+	errsResponse := validateTypeFields(db, ds)
+	if errsResponse != nil {
+		return errsResponse
+	}
+
+	return nil
+}
+
+func validateTypeFields(db *sqlx.DB, ds *tc.DeliveryServiceNullableV12) []error {
+	fmt.Printf("validateTypeFields\n")
+	// Validate the TypeName related fields below
+	var typeName string
+	var err error
+	DNSRegexType := "^DNS.*$"
+	HTTPRegexType := "^HTTP.*$"
+	SteeringRegexType := "^STEERING.*$"
+
+	if db != nil && ds == nil || ds.TypeID != nil {
+		typeID := *ds.TypeID
+		typeName, err = getTypeName(db, typeID)
+		if err != nil {
+			return []error{err}
+		}
+	}
+
+	if typeName != "" {
+		errs := validation.Errors{
+			"initialDispersion": validation.Validate(ds.InitialDispersion,
+				validation.By(requiredIfMatchesTypeName([]string{DNSRegexType, HTTPRegexType}, typeName))),
+			"ipv6RoutingEnabled": validation.Validate(ds.IPV6RoutingEnabled,
+				validation.By(requiredIfMatchesTypeName([]string{SteeringRegexType, DNSRegexType, HTTPRegexType}, typeName))),
+			"missLat": validation.Validate(ds.MissLat,
+				validation.By(requiredIfMatchesTypeName([]string{DNSRegexType, HTTPRegexType}, typeName))),
+			"missLong": validation.Validate(ds.MissLong,
+				validation.By(requiredIfMatchesTypeName([]string{DNSRegexType, HTTPRegexType}, typeName))),
+			"multiSiteOrigin": validation.Validate(ds.MultiSiteOrigin,
+				validation.By(requiredIfMatchesTypeName([]string{DNSRegexType, HTTPRegexType}, typeName))),
+			"orgServerFqdn": validation.Validate(ds.OrgServerFQDN,
+				validation.By(requiredIfMatchesTypeName([]string{DNSRegexType, HTTPRegexType}, typeName))),
+			"protocol": validation.Validate(ds.Protocol,
+				validation.By(requiredIfMatchesTypeName([]string{SteeringRegexType, DNSRegexType, HTTPRegexType}, typeName))),
+			"qstringIgnore": validation.Validate(ds.QStringIgnore,
+				validation.By(requiredIfMatchesTypeName([]string{DNSRegexType, HTTPRegexType}, typeName))),
+			"rangeRequestHandling": validation.Validate(ds.RangeRequestHandling,
+				validation.By(requiredIfMatchesTypeName([]string{DNSRegexType, HTTPRegexType}, typeName))),
+		}
+		return tovalidate.ToErrors(errs)
+	}
+	return nil
+}
+
+func requiredIfMatchesTypeName(patterns []string, typeName string) func(interface{}) error {
+	return func(value interface{}) error {
+
+		pattern := strings.Join(patterns, "|")
+		var err error
+		var match bool
+		if typeName != "" {
+			match, err = regexp.MatchString(pattern, typeName)
+			if match {
+				return fmt.Errorf("is required if type is '%s'", typeName)
+			}
+		}
+		return err
+	}
+}
+
+// TODO: drichardson - refactor to the types.go once implemented.
+func getTypeName(db *sqlx.DB, typeID int) (string, error) {
+
+	query := `SELECT name from type where id=$1`
+
+	var rows *sqlx.Rows
+	var err error
+
+	rows, err = db.Queryx(query, typeID)
+	if err != nil {
+		return "", err
+	}
+	defer rows.Close()
+
+	typeResults := []tc.Type{}
+	for rows.Next() {
+		var s tc.Type
+		if err = rows.StructScan(&s); err != nil {
+			return "", fmt.Errorf("getting Type: %v", err)
+		}
+		typeResults = append(typeResults, s)
+	}
+
+	typeName := typeResults[0].Name
+	return typeName, err
+}
+
+func CreateV12(db *sqlx.DB, cfg config.Config) http.HandlerFunc {
+	return func(w http.ResponseWriter, r *http.Request) {
+		defer r.Body.Close()
+		user, err := auth.GetCurrentUser(r.Context())
+		if err != nil {
+			api.HandleErr(w, r, http.StatusInternalServerError, nil, errors.New("getting user: "+err.Error()))
+			return
+		}
+
+		ds := tc.DeliveryServiceNullableV12{}
+		if err := json.NewDecoder(r.Body).Decode(&ds); err != nil {
+			api.HandleErr(w, r, http.StatusBadRequest, errors.New("malformed JSON: "+err.Error()), nil)
+			return
+		}
+
+		if errs := validateV12(db, &ds); len(errs) > 0 {
+			api.HandleErr(w, r, http.StatusBadRequest, errors.New("invalid request: "+util.JoinErrs(errs).Error()), nil)
+			return
+		}
+
+		dsv13 := tc.DeliveryServiceNullableV13{DeliveryServiceNullableV12: tc.DeliveryServiceNullableV12(ds)}
+
+		if authorized, err := isTenantAuthorized(*user, db, &ds); err != nil {
+			api.HandleErr(w, r, http.StatusInternalServerError, nil, errors.New("checking tenant: "+err.Error()))
+			return
+		} else if !authorized {
+			api.HandleErr(w, r, http.StatusForbidden, errors.New("not authorized on this tenant"), nil)
+			return
+		}
+
+		dsv13, errCode, userErr, sysErr := create(db.DB, cfg, user, dsv13)
+		if userErr != nil || sysErr != nil {
+			api.HandleErr(w, r, errCode, userErr, sysErr)
+			return
+		}
+		api.WriteResp(w, r, []tc.DeliveryServiceNullableV12{dsv13.DeliveryServiceNullableV12})
+	}
+}
+
+func (ds *TODeliveryServiceV12) Read(db *sqlx.DB, params map[string]string, user auth.CurrentUser) ([]interface{}, []error, tc.ApiErrorType) {
+	returnable := []interface{}{}
+	dses, errs, errType := readGetDeliveryServices(params, db)
+	if len(errs) > 0 {
+		for _, err := range errs {
+			if err.Error() == `id cannot parse to integer` {
+				return nil, []error{errors.New("Resource not found.")}, tc.DataMissingError //matches perl response
+			}
+		}
+		return nil, errs, errType
+	}
+
+	dses, err := filterAuthorized(dses, user, db)
+	if err != nil {
+		log.Errorln("Checking tenancy: " + err.Error())
+		return nil, []error{errors.New("Error checking tenancy.")}, tc.SystemError
+	}
+
+	for _, ds := range dses {
+		returnable = append(returnable, ds.DeliveryServiceNullableV12)
+	}
+	return returnable, nil, tc.NoError
+}
+
+func (ds *TODeliveryServiceV12) Delete(db *sqlx.DB, user auth.CurrentUser) (error, tc.ApiErrorType) {
+	v13 := &TODeliveryServiceV13{
+		Cfg: ds.Cfg,
+		DB:  ds.DB,
+		DeliveryServiceNullableV13: tc.DeliveryServiceNullableV13{
+			DeliveryServiceNullableV12: ds.DeliveryServiceNullableV12,
+		},
+	}
+	err, errType := v13.Delete(db, user)
+	ds.DeliveryServiceNullableV12 = v13.DeliveryServiceNullableV12 // TODO avoid copy
+	return err, errType
+}
+
+func UpdateV12(db *sqlx.DB, cfg config.Config) http.HandlerFunc {
+	return func(w http.ResponseWriter, r *http.Request) {
+		defer r.Body.Close()
+		user, err := auth.GetCurrentUser(r.Context())
+		if err != nil {
+			api.HandleErr(w, r, http.StatusInternalServerError, nil, errors.New("getting user: "+err.Error()))
+			return
+		}
+
+		ds := tc.DeliveryServiceNullableV12{}
+		if err := json.NewDecoder(r.Body).Decode(&ds); err != nil {
+			api.HandleErr(w, r, http.StatusBadRequest, errors.New("malformed JSON: "+err.Error()), nil)
+			return
+		}
+
+		if errs := validateV12(db, &ds); len(errs) > 0 {
+			api.HandleErr(w, r, http.StatusBadRequest, errors.New("invalid request: "+util.JoinErrs(errs).Error()), nil)
+			return
+		}
+
+		dsv13 := tc.DeliveryServiceNullableV13{DeliveryServiceNullableV12: tc.DeliveryServiceNullableV12(ds)}
+
+		if authorized, err := isTenantAuthorized(*user, db, &ds); err != nil {
+			api.HandleErr(w, r, http.StatusInternalServerError, nil, errors.New("checking tenant: "+err.Error()))
+			return
+		} else if !authorized {
+			api.HandleErr(w, r, http.StatusForbidden, errors.New("not authorized on this tenant"), nil)
+			return
+		}
+
+		dsv13, errCode, userErr, sysErr := update(db.DB, cfg, *user, &dsv13)
+		if userErr != nil || sysErr != nil {
+			api.HandleErr(w, r, errCode, userErr, sysErr)
+			return
+		}
+		api.WriteResp(w, r, []tc.DeliveryServiceNullableV12{dsv13.DeliveryServiceNullableV12})
+	}
+}
diff --git a/traffic_ops/traffic_ops_golang/deliveryservice/deliveryservicesv13.go b/traffic_ops/traffic_ops_golang/deliveryservice/deliveryservicesv13.go
new file mode 100644
index 0000000..b8fab5f
--- /dev/null
+++ b/traffic_ops/traffic_ops_golang/deliveryservice/deliveryservicesv13.go
@@ -0,0 +1,1119 @@
+package deliveryservice
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import (
+	"database/sql"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"net/http"
+	"strconv"
+	"strings"
+
+	"github.com/apache/incubator-trafficcontrol/lib/go-log"
+	"github.com/apache/incubator-trafficcontrol/lib/go-tc"
+	"github.com/apache/incubator-trafficcontrol/lib/go-util"
+	"github.com/apache/incubator-trafficcontrol/traffic_ops/traffic_ops_golang/api"
+	"github.com/apache/incubator-trafficcontrol/traffic_ops/traffic_ops_golang/auth"
+	"github.com/apache/incubator-trafficcontrol/traffic_ops/traffic_ops_golang/config"
+	"github.com/apache/incubator-trafficcontrol/traffic_ops/traffic_ops_golang/dbhelpers"
+	"github.com/apache/incubator-trafficcontrol/traffic_ops/traffic_ops_golang/riaksvc"
+	"github.com/apache/incubator-trafficcontrol/traffic_ops/traffic_ops_golang/tenant"
+	"github.com/apache/incubator-trafficcontrol/traffic_ops/traffic_ops_golang/tovalidate"
+
+	"github.com/go-ozzo/ozzo-validation"
+	"github.com/jmoiron/sqlx"
+	"github.com/lib/pq"
+)
+
+//we need a type alias to define functions on
+type TODeliveryServiceV13 struct {
+	tc.DeliveryServiceNullableV13
+	Cfg config.Config
+	DB  *sqlx.DB
+}
+
+func (ds *TODeliveryServiceV13) V12() *TODeliveryServiceV12 {
+	return &TODeliveryServiceV12{DeliveryServiceNullableV12: ds.DeliveryServiceNullableV12, DB: ds.DB, Cfg: ds.Cfg}
+}
+
+func (ds TODeliveryServiceV13) MarshalJSON() ([]byte, error) {
+	return json.Marshal(ds.DeliveryServiceNullableV13)
+}
+func (ds *TODeliveryServiceV13) UnmarshalJSON(data []byte) error {
+	return json.Unmarshal(data, ds.DeliveryServiceNullableV13)
+}
+
+func GetRefTypeV13(cfg config.Config, db *sqlx.DB) *TODeliveryServiceV13 {
+	return &TODeliveryServiceV13{Cfg: cfg, DB: db}
+}
+
+func (ds TODeliveryServiceV13) GetKeyFieldsInfo() []api.KeyFieldInfo {
+	return ds.V12().GetKeyFieldsInfo()
+}
+
+//Implementation of the Identifier, Validator interface functions
+func (ds TODeliveryServiceV13) GetKeys() (map[string]interface{}, bool) {
+	return ds.V12().GetKeys()
+}
+
+func (ds *TODeliveryServiceV13) SetKeys(keys map[string]interface{}) {
+	ds.V12().SetKeys(keys)
+}
+
+func (ds *TODeliveryServiceV13) GetAuditName() string {
+	return ds.V12().GetAuditName()
+}
+
+func (ds *TODeliveryServiceV13) GetType() string {
+	return ds.V12().GetType()
+}
+
+func ValidateV13(db *sqlx.DB, ds *tc.DeliveryServiceNullableV13) []error {
+	if ds == nil {
+		return []error{}
+	}
+	tods := TODeliveryServiceV13{DeliveryServiceNullableV13: *ds, DB: db} // TODO set Cfg?
+	return tods.Validate(db)
+}
+
+func (ds *TODeliveryServiceV13) Sanitize(db *sqlx.DB) { sanitizeV13(&ds.DeliveryServiceNullableV13) }
+
+func sanitizeV13(ds *tc.DeliveryServiceNullableV13) {
+	sanitizeV12(&ds.DeliveryServiceNullableV12)
+	signedAlgorithm := "url_sig"
+	if ds.Signed && (ds.SigningAlgorithm == nil || *ds.SigningAlgorithm == "") {
+		ds.SigningAlgorithm = &signedAlgorithm
+	}
+	if !ds.Signed && ds.SigningAlgorithm != nil && *ds.SigningAlgorithm == signedAlgorithm {
+		ds.Signed = true
+	}
+	if ds.DeepCachingType == nil {
+		s := tc.DeepCachingType("")
+		ds.DeepCachingType = &s
+	}
+	*ds.DeepCachingType = tc.DeepCachingTypeFromString(string(*ds.DeepCachingType))
+}
+
+func (ds *TODeliveryServiceV13) Validate(db *sqlx.DB) []error {
+	return validateV13(db, &ds.DeliveryServiceNullableV13)
+}
+
+func validateV13(db *sqlx.DB, ds *tc.DeliveryServiceNullableV13) []error {
+	sanitizeV13(ds)
+	neverOrAlways := validation.NewStringRule(tovalidate.IsOneOfStringICase("NEVER", "ALWAYS"),
+		"must be one of 'NEVER' or 'ALWAYS'")
+	errs := tovalidate.ToErrors(validation.Errors{
+		"deepCachingType": validation.Validate(ds.DeepCachingType, neverOrAlways),
+	})
+	oldErrs := validateV12(db, &ds.DeliveryServiceNullableV12)
+	return append(errs, oldErrs...)
+}
+
+// Create implements the Creator interface.
+//all implementations of Creator should use transactions and return the proper errorType
+//ParsePQUniqueConstraintError is used to determine if a ds with conflicting values exists
+//if so, it will return an errorType of DataConflict and the type should be appended to the
+//generic error message returned
+//The insert sql returns the id and lastUpdated values of the newly inserted ds and have
+//to be added to the struct
+// func (ds *TODeliveryServiceV13) Create(db *sqlx.DB, user auth.CurrentUser) (error, tc.ApiErrorType) { //
+//
+// 	TODO allow users to post names (type, cdn, etc) and get the IDs from the names. This isn't trivial to do in a single query, without dynamically building the entire insert query, and ideally inserting would be one query. But it'd be much more convenient for users. Alternatively, remove IDs from the database entirely and use real candidate keys.
+func CreateV13(db *sqlx.DB, cfg config.Config) http.HandlerFunc {
+	return func(w http.ResponseWriter, r *http.Request) {
+		defer r.Body.Close()
+		user, err := auth.GetCurrentUser(r.Context())
+		if err != nil {
+			api.HandleErr(w, r, http.StatusInternalServerError, nil, errors.New("getting user: "+err.Error()))
+			return
+		}
+
+		ds := tc.DeliveryServiceNullableV13{}
+		if err := json.NewDecoder(r.Body).Decode(&ds); err != nil {
+			api.HandleErr(w, r, http.StatusBadRequest, errors.New("malformed JSON: "+err.Error()), nil)
+			return
+		}
+
+		if errs := validateV13(db, &ds); len(errs) > 0 {
+			api.HandleErr(w, r, http.StatusBadRequest, errors.New("invalid request: "+util.JoinErrs(errs).Error()), nil)
+			return
+		}
+
+		if authorized, err := isTenantAuthorized(*user, db, &ds.DeliveryServiceNullableV12); err != nil {
+			api.HandleErr(w, r, http.StatusInternalServerError, nil, errors.New("checking tenant: "+err.Error()))
+			return
+		} else if !authorized {
+			api.HandleErr(w, r, http.StatusForbidden, errors.New("not authorized on this tenant"), nil)
+			return
+		}
+
+		ds, errCode, userErr, sysErr := create(db.DB, cfg, user, ds)
+		if userErr != nil || sysErr != nil {
+			api.HandleErr(w, r, errCode, userErr, sysErr)
+			return
+		}
+		api.WriteResp(w, r, []tc.DeliveryServiceNullableV13{ds})
+	}
+}
+
+// create creates the given ds in the database, and returns the DS with its id and other fields created on insert set. On error, the HTTP status cdoe, user error, and system error are returned. The status code SHOULD NOT be used, if both errors are nil.
+func create(db *sql.DB, cfg config.Config, user *auth.CurrentUser, ds tc.DeliveryServiceNullableV13) (tc.DeliveryServiceNullableV13, int, error, error) {
+	// TODO change DeepCachingType to implement sql.Valuer and sql.Scanner, so sqlx struct scan can be used.
+	deepCachingType := tc.DeepCachingType("").String()
+	if ds.DeepCachingType != nil {
+		deepCachingType = ds.DeepCachingType.String() // necessary, because DeepCachingType's default needs to insert the string, not "", and Query doesn't call .String().
+	}
+
+	tx, err := db.Begin()
+	if err != nil {
+		return tc.DeliveryServiceNullableV13{}, http.StatusInternalServerError, nil, errors.New("could not begin transaction: " + err.Error())
+	}
+	commitTx := false
+	defer dbhelpers.FinishTx(tx, &commitTx)
+
+	resultRows, err := tx.Query(insertQuery(), &ds.Active, &ds.CacheURL, &ds.CCRDNSTTL, &ds.CDNID, &ds.CheckPath, &deepCachingType, &ds.DisplayName, &ds.DNSBypassCNAME, &ds.DNSBypassIP, &ds.DNSBypassIP6, &ds.DNSBypassTTL, &ds.DSCP, &ds.EdgeHeaderRewrite, &ds.GeoLimitRedirectURL, &ds.GeoLimit, &ds.GeoLimitCountries, &ds.GeoProvider, &ds.GlobalMaxMBPS, &ds.GlobalMaxTPS, &ds.FQPacingRate, &ds.HTTPBypassFQDN, &ds.InfoURL, &ds.InitialDispersion, &ds.IPV6RoutingEnabled, &ds.LogsEnabled, &ds.LongD [...]
+
+	if err != nil {
+		if pqerr, ok := err.(*pq.Error); ok {
+			err, _ := dbhelpers.ParsePQUniqueConstraintError(pqerr)
+			return tc.DeliveryServiceNullableV13{}, http.StatusInternalServerError, nil, errors.New("a delivery service with " + err.Error())
+		}
+		return tc.DeliveryServiceNullableV13{}, http.StatusInternalServerError, nil, errors.New("inserting ds: " + err.Error())
+	}
+
+	id := 0
+	lastUpdated := tc.TimeNoMod{}
+	if !resultRows.Next() {
+		return tc.DeliveryServiceNullableV13{}, http.StatusInternalServerError, nil, errors.New("no deliveryservice request inserted, no id was returned")
+	}
+	if err := resultRows.Scan(&id, &lastUpdated); err != nil {
+		return tc.DeliveryServiceNullableV13{}, http.StatusInternalServerError, nil, errors.New("could not scan id from insert: " + err.Error())
+	}
+	if resultRows.Next() {
+		return tc.DeliveryServiceNullableV13{}, http.StatusInternalServerError, nil, errors.New("too many ids returned from deliveryservice request insert")
+	}
+	ds.ID = &id
+
+	if ds.ID == nil {
+		return tc.DeliveryServiceNullableV13{}, http.StatusInternalServerError, nil, errors.New("missing id after insert")
+	}
+	if ds.XMLID == nil {
+		return tc.DeliveryServiceNullableV13{}, http.StatusInternalServerError, nil, errors.New("missing xml_id after insert")
+	}
+	if ds.TypeID == nil {
+		return tc.DeliveryServiceNullableV13{}, http.StatusInternalServerError, nil, errors.New("missing type after insert")
+	}
+	dsType, err := getTypeNameFromID(*ds.TypeID, tx)
+	if err != nil {
+		return tc.DeliveryServiceNullableV13{}, http.StatusInternalServerError, nil, errors.New("getting delivery service type: " + err.Error())
+	}
+	ds.Type = &dsType
+	if ds.Protocol == nil {
+		return tc.DeliveryServiceNullableV13{}, http.StatusInternalServerError, nil, errors.New("missing protocol after insert")
+	}
+
+	if err := createDefaultRegex(tx, *ds.ID, *ds.XMLID); err != nil {
+		return tc.DeliveryServiceNullableV13{}, http.StatusInternalServerError, nil, errors.New("creating default regex: " + err.Error())
+	}
+
+	matchlists, err := readGetDeliveryServicesMatchLists([]string{*ds.XMLID}, tx)
+	if err != nil {
+		return tc.DeliveryServiceNullableV13{}, http.StatusInternalServerError, nil, errors.New("creating DS: reading matchlists: " + err.Error())
+	}
+	if matchlist, ok := matchlists[*ds.XMLID]; !ok {
+		return tc.DeliveryServiceNullableV13{}, http.StatusInternalServerError, nil, errors.New("creating DS: reading matchlists: not found")
+	} else {
+		ds.MatchList = &matchlist
+	}
+
+	cdnName, cdnDomain, dnssecEnabled, err := getCDNNameDomainDNSSecEnabled(*ds.ID, tx)
+	if err != nil {
+		return tc.DeliveryServiceNullableV13{}, http.StatusInternalServerError, nil, errors.New("creating DS: getting CDN info: " + err.Error())
+	}
+
+	ds.ExampleURLs = makeExampleURLs(*ds.Protocol, *ds.Type, *ds.RoutingName, *ds.MatchList, cdnDomain)
+
+	if err := ensureHeaderRewriteParams(tx, *ds.ID, *ds.XMLID, ds.EdgeHeaderRewrite, edgeTier, *ds.Type); err != nil {
+		return tc.DeliveryServiceNullableV13{}, http.StatusInternalServerError, nil, errors.New("creating edge header rewrite parameters: " + err.Error())
+	}
+	if err := ensureHeaderRewriteParams(tx, *ds.ID, *ds.XMLID, ds.MidHeaderRewrite, midTier, *ds.Type); err != nil {
+		return tc.DeliveryServiceNullableV13{}, http.StatusInternalServerError, nil, errors.New("creating mid header rewrite parameters: " + err.Error())
+	}
+	if err := ensureRegexRemapParams(tx, *ds.ID, *ds.XMLID, ds.RegexRemap); err != nil {
+		return tc.DeliveryServiceNullableV13{}, http.StatusInternalServerError, nil, errors.New("creating regex remap parameters: " + err.Error())
+	}
+	if err := ensureCacheURLParams(tx, *ds.ID, *ds.XMLID, ds.CacheURL); err != nil {
+		return tc.DeliveryServiceNullableV13{}, http.StatusInternalServerError, nil, errors.New("creating cache url parameters: " + err.Error())
+	}
+	if err := createDNSSecKeys(tx, cfg, *ds.ID, *ds.XMLID, *ds.Protocol, cdnName, cdnDomain, dnssecEnabled, ds.ExampleURLs); err != nil {
+		return tc.DeliveryServiceNullableV13{}, http.StatusInternalServerError, nil, errors.New("creating DNSSEC keys: " + err.Error())
+	}
+	ds.LastUpdated = &lastUpdated
+	commitTx = true
+	api.CreateChangeLogRaw(api.ApiChange, "Created ds: "+*ds.XMLID+" id: "+strconv.Itoa(*ds.ID), *user, db)
+	return ds, http.StatusOK, nil, nil
+}
+
+func (ds *TODeliveryServiceV13) Read(db *sqlx.DB, params map[string]string, user auth.CurrentUser) ([]interface{}, []error, tc.ApiErrorType) {
+	returnable := []interface{}{}
+	dses, errs, errType := readGetDeliveryServices(params, db)
+	if len(errs) > 0 {
+		for _, err := range errs {
+			if err.Error() == `id cannot parse to integer` { // TODO create const for string
+				return nil, []error{errors.New("Resource not found.")}, tc.DataMissingError //matches perl response
+			}
+		}
+		return nil, errs, errType
+	}
+
+	dses, err := filterAuthorized(dses, user, db)
+	if err != nil {
+		log.Errorln("Checking tenancy: " + err.Error())
+		return nil, []error{errors.New("Error checking tenancy.")}, tc.SystemError
+	}
+
+	for _, ds := range dses {
+		returnable = append(returnable, ds)
+	}
+	return returnable, nil, tc.NoError
+}
+
+func createDefaultRegex(tx *sql.Tx, dsID int, xmlID string) error {
+	regexStr := `.*\.` + xmlID + `\..*`
+	regexID := 0
+	if err := tx.QueryRow(`INSERT INTO regex (type, pattern) VALUES ((select id from type where name = 'HOST_REGEXP'), $1::text) RETURNING id`, regexStr).Scan(&regexID); err != nil {
+		return errors.New("insert regex: " + err.Error())
+	}
+	if _, err := tx.Exec(`INSERT INTO deliveryservice_regex (deliveryservice, regex, set_number) VALUES ($1::bigint, $2::bigint, 0)`, dsID, regexID); err != nil {
+		return errors.New("executing parameter query to insert location: " + err.Error())
+	}
+	return nil
+}
+
+func getOldHostName(id int, tx *sql.Tx) (string, error) {
+	q := `
+SELECT ds.xml_id, ds.protocol, type.name, ds.routing_name, cdn.domain_name
+FROM  deliveryservice as ds
+JOIN type ON ds.type = type.id
+JOIN cdn ON ds.cdn_id = cdn.id
+WHERE ds.id=$1
+`
+	xmlID := ""
+	protocol := sql.NullInt64{}
+	dsType := ""
+	routingName := ""
+	cdnDomain := ""
+	if err := tx.QueryRow(q, id).Scan(&xmlID, &protocol, &dsType, &routingName, &cdnDomain); err != nil {
+		return "", fmt.Errorf("querying delivery service %v host name: "+err.Error()+"\n", id)
+	}
+	matchLists, err := readGetDeliveryServicesMatchLists([]string{xmlID}, tx)
+	if err != nil {
+		return "", errors.New("getting delivery services matchlist: " + err.Error())
+	}
+	matchList, ok := matchLists[xmlID]
+	if !ok {
+		return "", errors.New("delivery service has no match lists (is your delivery service missing regexes?)")
+	}
+	host, err := getHostName(int(protocol.Int64), dsType, routingName, matchList, cdnDomain) // protocol defaults to 0: doesn't need to check Valid()
+	if err != nil {
+		return "", errors.New("getting hostname: " + err.Error())
+	}
+	return host, nil
+}
+
+func getTypeNameFromID(id int, tx *sql.Tx) (string, error) {
+	// TODO combine with getOldHostName, to only make one query?
+	name := ""
+	if err := tx.QueryRow(`SELECT name FROM type WHERE id = $1`, id).Scan(&name); err != nil {
+		return "", fmt.Errorf("querying type ID %v: "+err.Error()+"\n", id)
+	}
+	return name, nil
+}
+
+func UpdateV13(db *sqlx.DB, cfg config.Config) http.HandlerFunc {
+	return func(w http.ResponseWriter, r *http.Request) {
+		defer r.Body.Close()
+		user, err := auth.GetCurrentUser(r.Context())
+		if err != nil {
+			api.HandleErr(w, r, http.StatusInternalServerError, nil, errors.New("getting user: "+err.Error()))
+			return
+		}
+
+		ds := tc.DeliveryServiceNullableV13{}
+		if err := json.NewDecoder(r.Body).Decode(&ds); err != nil {
+			api.HandleErr(w, r, http.StatusBadRequest, errors.New("malformed JSON: "+err.Error()), nil)
+			return
+		}
+
+		if authorized, err := isTenantAuthorized(*user, db, &ds.DeliveryServiceNullableV12); err != nil {
+			api.HandleErr(w, r, http.StatusInternalServerError, nil, errors.New("checking tenant: "+err.Error()))
+			return
+		} else if !authorized {
+			api.HandleErr(w, r, http.StatusForbidden, errors.New("not authorized on this tenant"), nil)
+			return
+		}
+
+		ds, errCode, userErr, sysErr := update(db.DB, cfg, *user, &ds)
+		if userErr != nil || sysErr != nil {
+			api.HandleErr(w, r, errCode, userErr, sysErr)
+			return
+		}
+		api.WriteResp(w, r, []tc.DeliveryServiceNullableV13{ds})
+	}
+}
+
+func update(db *sql.DB, cfg config.Config, user auth.CurrentUser, ds *tc.DeliveryServiceNullableV13) (tc.DeliveryServiceNullableV13, int, error, error) {
+	tx, err := db.Begin()
+	if err != nil {
+		return tc.DeliveryServiceNullableV13{}, http.StatusInternalServerError, nil, errors.New("could not begin transaction: " + err.Error())
+	}
+	commitTx := false
+	defer dbhelpers.FinishTx(tx, &commitTx)
+
+	if ds.XMLID == nil {
+		return tc.DeliveryServiceNullableV13{}, http.StatusBadRequest, errors.New("missing xml_id"), nil
+	}
+	if ds.ID == nil {
+		return tc.DeliveryServiceNullableV13{}, http.StatusBadRequest, errors.New("missing id"), nil
+	}
+
+	oldHostName, err := getOldHostName(*ds.ID, tx)
+	if err != nil {
+		return tc.DeliveryServiceNullableV13{}, http.StatusInternalServerError, nil, errors.New("getting existing delivery service hostname: " + err.Error())
+	}
+
+	// TODO change DeepCachingType to implement sql.Valuer and sql.Scanner, so sqlx struct scan can be used.
+	deepCachingType := tc.DeepCachingType("").String()
+	if ds.DeepCachingType != nil {
+		deepCachingType = ds.DeepCachingType.String() // necessary, because DeepCachingType's default needs to insert the string, not "", and Query doesn't call .String().
+	}
+
+	resultRows, err := tx.Query(updateDSQuery(), &ds.Active, &ds.CacheURL, &ds.CCRDNSTTL, &ds.CDNID, &ds.CheckPath, &deepCachingType, &ds.DisplayName, &ds.DNSBypassCNAME, &ds.DNSBypassIP, &ds.DNSBypassIP6, &ds.DNSBypassTTL, &ds.DSCP, &ds.EdgeHeaderRewrite, &ds.GeoLimitRedirectURL, &ds.GeoLimit, &ds.GeoLimitCountries, &ds.GeoProvider, &ds.GlobalMaxMBPS, &ds.GlobalMaxTPS, &ds.FQPacingRate, &ds.HTTPBypassFQDN, &ds.InfoURL, &ds.InitialDispersion, &ds.IPV6RoutingEnabled, &ds.LogsEnabled, &ds.Lon [...]
+
+	if err != nil {
+		if err, ok := err.(*pq.Error); ok {
+			err, eType := dbhelpers.ParsePQUniqueConstraintError(err)
+			if eType == tc.DataConflictError {
+				return tc.DeliveryServiceNullableV13{}, http.StatusBadRequest, errors.New("a delivery service with " + err.Error()), nil
+			}
+			return tc.DeliveryServiceNullableV13{}, http.StatusInternalServerError, nil, errors.New("query updating delivery service: pq: " + err.Error())
+		}
+		return tc.DeliveryServiceNullableV13{}, http.StatusInternalServerError, nil, errors.New("query updating delivery service: " + err.Error())
+	}
+	if !resultRows.Next() {
+		return tc.DeliveryServiceNullableV13{}, http.StatusNotFound, errors.New("no delivery service found with this id"), nil
+	}
+	lastUpdated := tc.TimeNoMod{}
+	if err := resultRows.Scan(&lastUpdated); err != nil {
+		return tc.DeliveryServiceNullableV13{}, http.StatusInternalServerError, nil, errors.New("scan updating delivery service: " + err.Error())
+	}
+	if resultRows.Next() {
+		xmlID := ""
+		if ds.XMLID != nil {
+			xmlID = *ds.XMLID
+		}
+		return tc.DeliveryServiceNullableV13{}, http.StatusInternalServerError, nil, errors.New("updating delivery service " + xmlID + ": " + "this update affected too many rows: > 1")
+	}
+
+	if ds.ID == nil {
+		return tc.DeliveryServiceNullableV13{}, http.StatusInternalServerError, nil, errors.New("missing id after update")
+	}
+	if ds.XMLID == nil {
+		return tc.DeliveryServiceNullableV13{}, http.StatusInternalServerError, nil, errors.New("missing xml_id after update")
+	}
+	if ds.TypeID == nil {
+		return tc.DeliveryServiceNullableV13{}, http.StatusInternalServerError, nil, errors.New("missing type after update")
+	}
+	if ds.Protocol == nil {
+		return tc.DeliveryServiceNullableV13{}, http.StatusInternalServerError, nil, errors.New("missing protocol after update")
+	}
+	if ds.RoutingName == nil {
+		return tc.DeliveryServiceNullableV13{}, http.StatusInternalServerError, nil, errors.New("missing routing name after update")
+	}
+	dsType, err := getTypeNameFromID(*ds.TypeID, tx)
+	if err != nil {
+		return tc.DeliveryServiceNullableV13{}, http.StatusInternalServerError, nil, errors.New("getting delivery service type after update: " + err.Error())
+	}
+	ds.Type = &dsType
+
+	cdnDomain, err := getCDNDomain(*ds.ID, db) // need to get the domain again, in case it changed.
+	if err != nil {
+		return tc.DeliveryServiceNullableV13{}, http.StatusInternalServerError, nil, errors.New("getting CDN domain after update: " + err.Error())
+	}
+
+	newHostName, err := getHostName(*ds.Protocol, *ds.Type, *ds.RoutingName, *ds.MatchList, cdnDomain)
+	if err != nil {
+		return tc.DeliveryServiceNullableV13{}, http.StatusInternalServerError, nil, errors.New("getting hostname after update: " + err.Error())
+	}
+
+	matchLists, err := readGetDeliveryServicesMatchLists([]string{*ds.XMLID}, tx)
+	if err != nil {
+		return tc.DeliveryServiceNullableV13{}, http.StatusInternalServerError, nil, errors.New("getting matchlists after update: " + err.Error())
+	}
+	if ml, ok := matchLists[*ds.XMLID]; !ok {
+		return tc.DeliveryServiceNullableV13{}, http.StatusInternalServerError, nil, errors.New("no matchlists after update")
+	} else {
+		ds.MatchList = &ml
+	}
+
+	if oldHostName != newHostName {
+		if err := updateSSLKeys(ds, newHostName, db, cfg); err != nil {
+			return tc.DeliveryServiceNullableV13{}, http.StatusInternalServerError, nil, errors.New("updating delivery service " + *ds.XMLID + ": updating SSL keys: " + err.Error())
+		}
+	}
+
+	if err := ensureHeaderRewriteParams(tx, *ds.ID, *ds.XMLID, ds.EdgeHeaderRewrite, edgeTier, *ds.Type); err != nil {
+		return tc.DeliveryServiceNullableV13{}, http.StatusInternalServerError, nil, errors.New("creating edge header rewrite parameters: " + err.Error())
+	}
+	if err := ensureHeaderRewriteParams(tx, *ds.ID, *ds.XMLID, ds.MidHeaderRewrite, midTier, *ds.Type); err != nil {
+		return tc.DeliveryServiceNullableV13{}, http.StatusInternalServerError, nil, errors.New("creating mid header rewrite parameters: " + err.Error())
+	}
+	if err := ensureRegexRemapParams(tx, *ds.ID, *ds.XMLID, ds.RegexRemap); err != nil {
+		return tc.DeliveryServiceNullableV13{}, http.StatusInternalServerError, nil, errors.New("creating mid regex remap parameters: " + err.Error())
+	}
+	if err := ensureCacheURLParams(tx, *ds.ID, *ds.XMLID, ds.CacheURL); err != nil {
+		return tc.DeliveryServiceNullableV13{}, http.StatusInternalServerError, nil, errors.New("creating mid cacheurl parameters: " + err.Error())
+	}
+	ds.LastUpdated = &lastUpdated
+	commitTx = true
+	api.CreateChangeLogRaw(api.ApiChange, "Updated ds: "+*ds.XMLID+" id: "+strconv.Itoa(*ds.ID), user, db)
+	return *ds, http.StatusOK, nil, nil
+}
+
+//The DeliveryService implementation of the Deleter interface
+//all implementations of Deleter should use transactions and return the proper errorType
+func (ds *TODeliveryServiceV13) Delete(db *sqlx.DB, user auth.CurrentUser) (error, tc.ApiErrorType) {
+	log.Debugln("TODeliveryServiceV13.Delete calling id '%v' xmlid '%v'\n", ds.ID, ds.XMLID)
+	// return nil, tc.NoError // debug
+
+	tx, err := db.Begin()
+	if err != nil {
+		log.Errorln("could not begin transaction: " + err.Error())
+		return tc.DBError, tc.SystemError
+	}
+	commitTx := false
+	defer dbhelpers.FinishTx(tx, &commitTx)
+
+	if err != nil {
+		log.Errorln("could not begin transaction: " + err.Error())
+		return tc.DBError, tc.SystemError
+	}
+
+	if ds.ID == nil {
+		log.Errorln("TODeliveryServiceV13.Delete called with nil ID")
+		return tc.DBError, tc.DataMissingError
+	} else if ok, err := ds.V12().LoadXMLID(db); err != nil {
+		log.Errorln("TODeliveryServiceV13.Delete ID '" + string(*ds.ID) + "' loading XML ID: " + err.Error())
+		return tc.DBError, tc.SystemError
+	} else if !ok {
+		log.Errorln("TODeliveryServiceV13.Delete ID '" + string(*ds.ID) + "' had no delivery service!")
+		return tc.DBError, tc.DataMissingError
+	} else if ds.XMLID == nil {
+		// should never happen
+		log.Errorf("TODeliveryServiceV13.Delete ID '%v' loaded nil XML ID!", *ds.ID)
+		return tc.DBError, tc.SystemError
+	}
+
+	result, err := tx.Exec(`DELETE FROM deliveryservice WHERE id=$1`, ds.ID)
+	if err != nil {
+		log.Errorln("TODeliveryServiceV13.Delete deleting delivery service: " + err.Error())
+		return tc.DBError, tc.SystemError
+	}
+	rowsAffected, err := result.RowsAffected()
+	if err != nil {
+		return tc.DBError, tc.SystemError
+	}
+	if rowsAffected != 1 {
+		if rowsAffected < 1 {
+			return errors.New("no delivery service with that id found"), tc.DataMissingError
+		}
+		return fmt.Errorf("this create affected too many rows: %d", rowsAffected), tc.SystemError
+	}
+
+	if _, err := tx.Exec(`DELETE FROM deliveryservice_regex WHERE deliveryservice=$1`, ds.ID); err != nil {
+		log.Errorln("TODeliveryServiceV13.Delete deleting delivery service regexes: " + err.Error())
+		return tc.DBError, tc.SystemError
+	}
+
+	paramConfigFilePrefixes := []string{"hdr_rw_", "hdr_rw_mid_", "regex_remap_", "cacheurl_"}
+	configFiles := []string{}
+	for _, prefix := range paramConfigFilePrefixes {
+		configFiles = append(configFiles, prefix+*ds.XMLID+".config")
+	}
+
+	if _, err := tx.Exec(`DELETE FROM parameter WHERE name = 'location' AND config_file = ANY($1)`, pq.Array(configFiles)); err != nil {
+		log.Errorln("TODeliveryServiceV13.Delete deleting delivery service parameters: " + err.Error())
+		return tc.DBError, tc.SystemError
+	}
+
+	commitTx = true
+	return nil, tc.NoError
+}
+
+// IsTenantAuthorized implements the Tenantable interface to ensure the user is authorized on the deliveryservice tenant
+func (ds *TODeliveryServiceV13) IsTenantAuthorized(user auth.CurrentUser, db *sqlx.DB) (bool, error) {
+	return ds.V12().IsTenantAuthorized(user, db)
+}
+
+func filterAuthorized(dses []tc.DeliveryServiceNullableV13, user auth.CurrentUser, db *sqlx.DB) ([]tc.DeliveryServiceNullableV13, error) {
+	newDSes := []tc.DeliveryServiceNullableV13{}
+	for _, ds := range dses {
+		// TODO add/use a helper func to make a single SQL call, for performance
+		ok, err := tenant.IsResourceAuthorizedToUser(*ds.TenantID, user, db)
+		if err != nil {
+			if ds.XMLID == nil {
+				return nil, errors.New("isResourceAuthorized for delivery service with nil XML ID: " + err.Error())
+			} else {
+				return nil, errors.New("isResourceAuthorized for '" + *ds.XMLID + "': " + err.Error())
+			}
+		}
+		if !ok {
+			continue
+		}
+		newDSes = append(newDSes, ds)
+	}
+	return newDSes, nil
+}
+
+func readGetDeliveryServices(params map[string]string, db *sqlx.DB) ([]tc.DeliveryServiceNullableV13, []error, tc.ApiErrorType) {
+	// Query Parameters to Database Query column mappings
+	// see the fields mapped in the SQL query
+	queryParamsToSQLCols := map[string]dbhelpers.WhereColumnInfo{
+		"id":       dbhelpers.WhereColumnInfo{"ds.id", api.IsInt},
+		"hostName": dbhelpers.WhereColumnInfo{"s.host_name", nil},
+	}
+
+	where, orderBy, queryValues, errs := dbhelpers.BuildWhereAndOrderBy(params, queryParamsToSQLCols)
+	if len(errs) > 0 {
+		return nil, errs, tc.DataConflictError
+	}
+
+	query := selectQuery() + where + orderBy
+
+	tx, err := db.Beginx()
+	if err != nil {
+		log.Errorln("could not begin transaction: " + err.Error())
+		return nil, []error{tc.DBError}, tc.SystemError
+	}
+	commitTx := false
+	defer dbhelpers.FinishTxX(tx, &commitTx)
+
+	rows, err := tx.NamedQuery(query, queryValues)
+	if err != nil {
+		return nil, []error{fmt.Errorf("querying: %v", err)}, tc.SystemError
+	}
+	defer rows.Close()
+
+	dses := []tc.DeliveryServiceNullableV13{}
+	dsCDNDomains := map[string]string{}
+	for rows.Next() {
+		ds := tc.DeliveryServiceNullableV13{}
+		cdnDomain := ""
+		err := rows.Scan(&ds.Active, &ds.CacheURL, &ds.CCRDNSTTL, &ds.CDNID, &ds.CDNName, &ds.CheckPath, &ds.DeepCachingType, &ds.DisplayName, &ds.DNSBypassCNAME, &ds.DNSBypassIP, &ds.DNSBypassIP6, &ds.DNSBypassTTL, &ds.DSCP, &ds.EdgeHeaderRewrite, &ds.GeoLimitRedirectURL, &ds.GeoLimit, &ds.GeoLimitCountries, &ds.GeoProvider, &ds.GlobalMaxMBPS, &ds.GlobalMaxTPS, &ds.FQPacingRate, &ds.HTTPBypassFQDN, &ds.ID, &ds.InfoURL, &ds.InitialDispersion, &ds.IPV6RoutingEnabled, &ds.LastUpdated, &ds.LogsEn [...]
+		if err != nil {
+			return nil, []error{fmt.Errorf("getting delivery services: %v", err)}, tc.SystemError
+		}
+		dsCDNDomains[*ds.XMLID] = cdnDomain
+		if ds.DeepCachingType != nil {
+			*ds.DeepCachingType = tc.DeepCachingTypeFromString(string(*ds.DeepCachingType))
+		}
+		ds.Signed = ds.SigningAlgorithm != nil && *ds.SigningAlgorithm == "url_sig"
+		dses = append(dses, ds)
+	}
+
+	dsNames := make([]string, len(dses), len(dses))
+	for i, ds := range dses {
+		dsNames[i] = *ds.XMLID
+	}
+
+	matchLists, err := readGetDeliveryServicesMatchLists(dsNames, tx.Tx)
+	if err != nil {
+		return nil, []error{errors.New("getting delivery service matchlists: " + err.Error())}, tc.SystemError
+	}
+	for i, ds := range dses {
+		matchList, ok := matchLists[*ds.XMLID]
+		if !ok {
+			continue
+		}
+		ds.MatchList = &matchList
+		dsProtocol := 0
+		if ds.Protocol != nil {
+			dsProtocol = *ds.Protocol
+		}
+		ds.ExampleURLs = makeExampleURLs(dsProtocol, *ds.Type, *ds.RoutingName, *ds.MatchList, dsCDNDomains[*ds.XMLID])
+		dses[i] = ds
+	}
+
+	commitTx = true
+	return dses, nil, tc.NoError
+}
+
+func updateSSLKeys(ds *tc.DeliveryServiceNullableV13, hostName string, db *sql.DB, cfg config.Config) error {
+	if ds.XMLID == nil {
+		return errors.New("delivery services has no XMLID!")
+	}
+	key, ok, err := riaksvc.GetDeliveryServiceSSLKeysObj(*ds.XMLID, "latest", db, cfg.RiakAuthOptions)
+	if err != nil {
+		return errors.New("getting SSL key: " + err.Error())
+	}
+	if !ok {
+		return nil // no keys to update
+	}
+	key.DeliveryService = *ds.XMLID
+	key.Hostname = hostName
+	if err := riaksvc.PutDeliveryServiceSSLKeysObj(key, db, cfg.RiakAuthOptions); err != nil {
+		return errors.New("putting updated SSL key: " + err.Error())
+	}
+	return nil
+}
+
+func getHostName(dsProtocol int, dsType string, dsRoutingName string, dsMatchList []tc.DeliveryServiceMatch, cdnDomain string) (string, error) {
+	exampleURLs := makeExampleURLs(dsProtocol, dsType, dsRoutingName, dsMatchList, cdnDomain)
+
+	exampleURL := ""
+	if dsProtocol == 2 {
+		if len(exampleURLs) < 2 {
+			return "", errors.New("missing example URLs (does your delivery service have matchsets?)")
+		}
+		exampleURL = exampleURLs[1]
+	} else {
+		if len(exampleURLs) < 1 {
+			return "", errors.New("missing example URLs (does your delivery service have matchsets?)")
+		}
+		exampleURL = exampleURLs[0]
+	}
+
+	host := strings.NewReplacer(`http://`, ``, `https://`, ``).Replace(exampleURL)
+	if strings.HasPrefix(dsType, "HTTP") {
+		if firstDot := strings.Index(host, "."); firstDot == -1 {
+			host = "*" // TODO warn? error?
+		} else {
+			host = "*" + host[firstDot:]
+		}
+	}
+	return host, nil
+}
+
+func getCDNDomain(dsID int, db *sql.DB) (string, error) {
+	q := `SELECT cdn.domain_name from cdn where cdn.id = (SELECT ds.cdn_id from deliveryservice as ds where ds.id = $1)`
+	cdnDomain := ""
+	if err := db.QueryRow(q, dsID).Scan(&cdnDomain); err != nil {
+		return "", fmt.Errorf("getting CDN domain for delivery service '%v': "+err.Error(), dsID)
+	}
+	return cdnDomain, nil
+}
+
+func getCDNNameDomainDNSSecEnabled(dsID int, tx *sql.Tx) (string, string, bool, error) {
+	q := `SELECT cdn.name, cdn.domain_name, cdn.dnssec_enabled from cdn where cdn.id = (SELECT ds.cdn_id from deliveryservice as ds where ds.id = $1)`
+	cdnName := ""
+	cdnDomain := ""
+	dnssecEnabled := false
+	if err := tx.QueryRow(q, dsID).Scan(&cdnName, &cdnDomain, &dnssecEnabled); err != nil {
+		return "", "", false, fmt.Errorf("getting dnssec_enabled for delivery service '%v': "+err.Error(), dsID)
+	}
+	return cdnName, cdnDomain, dnssecEnabled, nil
+}
+
+func makeExampleURLs(protocol int, dsType string, routingName string, matchList []tc.DeliveryServiceMatch, cdnDomain string) []string {
+	examples := []string{}
+	scheme := ""
+	scheme2 := ""
+	switch protocol {
+	case 0:
+		scheme = "http"
+	case 1:
+		scheme = "https"
+	case 2:
+		fallthrough
+	case 3:
+		scheme = "http"
+		scheme2 = "https"
+	default:
+		scheme = "http"
+	}
+	dsIsDNS := strings.HasPrefix(strings.ToLower(dsType), "DNS")
+	regexReplacer := strings.NewReplacer(`\`, ``, `.*`, ``, `.`, ``)
+	for _, match := range matchList {
+		switch {
+		case dsIsDNS:
+			fallthrough
+		case match.Type == `HOST_REGEXP`:
+			host := regexReplacer.Replace(match.Pattern)
+			if match.SetNumber == 0 {
+				examples = append(examples, scheme+`://`+routingName+`.`+host+`.`+cdnDomain)
+				if scheme2 != "" {
+					examples = append(examples, scheme2+`://`+routingName+`.`+host+`.`+cdnDomain)
+				}
+				continue
+			}
+			examples = append(examples, scheme+`://`+match.Pattern)
+			if scheme2 != "" {
+				examples = append(examples, scheme2+`://`+match.Pattern)
+			}
+		case match.Type == `PATH_REGEXP`:
+			examples = append(examples, match.Pattern)
+		}
+	}
+	return examples
+}
+
+func readGetDeliveryServicesMatchLists(dses []string, tx *sql.Tx) (map[string][]tc.DeliveryServiceMatch, error) {
+	q := `
+SELECT ds.xml_id as ds_name, t.name as type, r.pattern, COALESCE(dsr.set_number, 0)
+FROM regex as r
+JOIN deliveryservice_regex as dsr ON dsr.regex = r.id
+JOIN deliveryservice as ds on ds.id = dsr.deliveryservice
+JOIN type as t ON r.type = t.id
+WHERE ds.xml_id = ANY($1)
+`
+	rows, err := tx.Query(q, pq.Array(dses))
+	if err != nil {
+		return nil, errors.New("getting delivery service regexes: " + err.Error())
+	}
+
+	matches := map[string][]tc.DeliveryServiceMatch{}
+	for rows.Next() {
+		m := tc.DeliveryServiceMatch{}
+		dsName := ""
+		if err := rows.Scan(&dsName, &m.Type, &m.Pattern, &m.SetNumber); err != nil {
+			return nil, errors.New("scanning delivery service regexes: " + err.Error())
+		}
+		matches[dsName] = append(matches[dsName], m)
+	}
+	return matches, nil
+}
+
+type tierType int
+
+const (
+	midTier tierType = iota
+	edgeTier
+)
+
+func ensureHeaderRewriteParams(tx *sql.Tx, dsID int, xmlID string, hdrRW *string, tier tierType, dsType string) error {
+	if tier == midTier && strings.Contains(dsType, "LIVE") && !strings.Contains(dsType, "NATNL") {
+		return nil // live local DSes don't get remap rules
+	}
+	configFile := "hdr_rw_" + xmlID + ".config"
+	if tier == midTier {
+		configFile = "hdr_rw_mid_" + xmlID + ".config"
+	}
+	if hdrRW == nil || *hdrRW == "" {
+		return deleteLocationParam(tx, configFile)
+	}
+	locationParamID, err := ensureLocation(tx, configFile)
+	if err != nil {
+		return err
+	}
+	if tier != midTier {
+		return createDSLocationProfileParams(tx, locationParamID, dsID)
+	}
+	profileParameterQuery := `
+INSERT INTO profile_parameter (profile, parameter)
+SELECT DISTINCT(profile), $1::bigint FROM server
+WHERE server.type IN (SELECT id from type where type.name like 'MID%' and type.use_in_table = 'server')
+AND server.cdn_id = (select cdn_id from deliveryservice where id = $2)
+ON CONFLICT DO NOTHING
+`
+	if _, err := tx.Exec(profileParameterQuery, locationParamID, dsID); err != nil {
+		return fmt.Errorf("parameter query to insert profile_parameters query '"+profileParameterQuery+"' location parameter ID '%v' delivery service ID '%v': %v", locationParamID, dsID, err)
+	}
+	return nil
+}
+
+func ensureRegexRemapParams(tx *sql.Tx, dsID int, xmlID string, regexRemap *string) error {
+	configFile := "regex_remap_" + xmlID + ".config"
+	if regexRemap == nil || *regexRemap == "" {
+		return deleteLocationParam(tx, configFile)
+	}
+	locationParamID, err := ensureLocation(tx, configFile)
+	if err != nil {
+		return err
+	}
+	return createDSLocationProfileParams(tx, locationParamID, dsID)
+}
+
+func ensureCacheURLParams(tx *sql.Tx, dsID int, xmlID string, cacheURL *string) error {
+	configFile := "cacheurl_" + xmlID + ".config"
+	if cacheURL == nil || *cacheURL == "" {
+		return deleteLocationParam(tx, configFile)
+	}
+	locationParamID, err := ensureLocation(tx, configFile)
+	if err != nil {
+		return err
+	}
+	return createDSLocationProfileParams(tx, locationParamID, dsID)
+}
+
+// createDSLocationProfileParams adds the given parameter to all profiles assigned to servers which are assigned to the given delivery service.
+func createDSLocationProfileParams(tx *sql.Tx, locationParamID int, deliveryServiceID int) error {
+	profileParameterQuery := `
+INSERT INTO profile_parameter (profile, parameter)
+SELECT DISTINCT(profile), $1::bigint FROM server
+WHERE server.id IN (SELECT server from deliveryservice_server where deliveryservice = $2)
+ON CONFLICT DO NOTHING
+`
+	if _, err := tx.Exec(profileParameterQuery, locationParamID, deliveryServiceID); err != nil {
+		return errors.New("inserting profile_parameters: " + err.Error())
+	}
+	return nil
+}
+
+// ensureLocation ensures a location parameter exists for the given config file. If not, it creates one, with the same value as the 'remap.config' file parameter. Returns the ID of the location parameter.
+func ensureLocation(tx *sql.Tx, configFile string) (int, error) {
+	atsConfigLocation := ""
+	if err := tx.QueryRow(`SELECT value FROM parameter WHERE name = 'location' AND config_file = 'remap.config'`).Scan(&atsConfigLocation); err != nil {
+		if err == sql.ErrNoRows {
+			return 0, errors.New("executing parameter query for ATS config location: parameter missing (do you have a name=location config_file=remap.config parameter?")
+		}
+		return 0, errors.New("executing parameter query for ATS config location: " + err.Error())
+	}
+	atsConfigLocation = strings.TrimRight(atsConfigLocation, `/`)
+
+	locationParamID := 0
+	existingLocationErr := tx.QueryRow(`SELECT id FROM parameter WHERE name = 'location' AND config_file = $1`, configFile).Scan(&locationParamID)
+	if existingLocationErr != nil && existingLocationErr != sql.ErrNoRows {
+		return 0, errors.New("executing parameter query for existing location: " + existingLocationErr.Error())
+	}
+
+	if existingLocationErr == sql.ErrNoRows {
+		resultRows, err := tx.Query(`INSERT INTO parameter (config_file, name, value) VALUES ($1, 'location', $2) RETURNING id`, configFile, atsConfigLocation)
+		if err != nil {
+			return 0, errors.New("executing parameter query to insert location: " + err.Error())
+		}
+		defer resultRows.Close()
+		if !resultRows.Next() {
+			return 0, errors.New("parameter query to insert location didn't return id")
+		}
+		if err := resultRows.Scan(&locationParamID); err != nil {
+			return 0, errors.New("parameter query to insert location returned id scan: " + err.Error())
+		}
+		if resultRows.Next() {
+			return 0, errors.New("parameter query to insert location returned too many rows (>1)")
+		}
+	}
+	return locationParamID, nil
+}
+
+func deleteLocationParam(tx *sql.Tx, configFile string) error {
+	id := 0
+	err := tx.QueryRow(`DELETE FROM parameter WHERE name = 'location' AND config_file = $1 RETURNING id`, configFile).Scan(&id)
+	if err == sql.ErrNoRows {
+		return nil
+	}
+	if err != nil {
+		log.Errorln("deleting name=location config_file=" + configFile + " parameter: " + err.Error())
+		return errors.New("executing parameter delete: " + err.Error())
+	}
+	if _, err := tx.Exec(`DELETE FROM profile_parameter WHERE parameter = $1`, id); err != nil {
+		log.Errorf("deleting parameter name=location config_file=%v id=%v profile_parameter: %v", configFile, id, err)
+		return errors.New("executing parameter profile_parameter delete: " + err.Error())
+	}
+	return nil
+}
+
+func selectQuery() string {
+	return `
+SELECT
+ds.active,
+ds.cacheurl,
+ds.ccr_dns_ttl,
+ds.cdn_id,
+cdn.name as cdnName,
+ds.check_path,
+ds.deep_caching_type,
+ds.display_name,
+ds.dns_bypass_cname,
+ds.dns_bypass_ip,
+ds.dns_bypass_ip6,
+ds.dns_bypass_ttl,
+ds.dscp,
+ds.edge_header_rewrite,
+ds.geolimit_redirect_url,
+ds.geo_limit,
+ds.geo_limit_countries,
+ds.geo_provider,
+ds.global_max_mbps,
+ds.global_max_tps,
+ds.fq_pacing_rate,
+ds.http_bypass_fqdn,
+ds.id,
+ds.info_url,
+ds.initial_dispersion,
+ds.ipv6_routing_enabled,
+ds.last_updated,
+ds.logs_enabled,
+ds.long_desc,
+ds.long_desc_1,
+ds.long_desc_2,
+ds.max_dns_answers,
+ds.mid_header_rewrite,
+COALESCE(ds.miss_lat, 0.0),
+COALESCE(ds.miss_long, 0.0),
+ds.multi_site_origin,
+ds.org_server_fqdn,
+ds.origin_shield,
+ds.profile as profileID,
+profile.name as profile_name,
+profile.description  as profile_description,
+ds.protocol,
+ds.qstring_ignore,
+ds.range_request_handling,
+ds.regex_remap,
+ds.regional_geo_blocking,
+ds.remap_text,
+ds.routing_name,
+ds.signing_algorithm,
+ds.ssl_key_version,
+ds.tenant_id,
+tenant.name,
+ds.tr_request_headers,
+ds.tr_response_headers,
+type.name,
+ds.type as type_id,
+ds.xml_id,
+cdn.domain_name as cdn_domain
+from deliveryservice as ds
+JOIN type ON ds.type = type.id
+JOIN cdn ON ds.cdn_id = cdn.id
+LEFT JOIN profile ON ds.profile = profile.id
+LEFT JOIN tenant ON ds.tenant_id = tenant.id
+`
+}
+
+func updateDSQuery() string {
+	return `
+UPDATE
+deliveryservice SET
+active=$1,
+cacheurl=$2,
+ccr_dns_ttl=$3,
+cdn_id=$4,
+check_path=$5,
+deep_caching_type=$6,
+display_name=$7,
+dns_bypass_cname=$8,
+dns_bypass_ip=$9,
+dns_bypass_ip6=$10,
+dns_bypass_ttl=$11,
+dscp=$12,
+edge_header_rewrite=$13,
+geolimit_redirect_url=$14,
+geo_limit=$15,
+geo_limit_countries=$16,
+geo_provider=$17,
+global_max_mbps=$18,
+global_max_tps=$19,
+fq_pacing_rate=$20,
+http_bypass_fqdn=$21,
+info_url=$22,
+initial_dispersion=$23,
+ipv6_routing_enabled=$24,
+logs_enabled=$25,
+long_desc=$26,
+long_desc_1=$27,
+long_desc_2=$28,
+max_dns_answers=$29,
+mid_header_rewrite=$30,
+miss_lat=$31,
+miss_long=$32,
+multi_site_origin=$33,
+org_server_fqdn=$34,
+origin_shield=$35,
+profile=$36,
+protocol=$37,
+qstring_ignore=$38,
+range_request_handling=$39,
+regex_remap=$40,
+regional_geo_blocking=$41,
+remap_text=$42,
+routing_name=$43,
+signing_algorithm=$44,
+ssl_key_version=$45,
+tenant_id=$46,
+tr_request_headers=$47,
+tr_response_headers=$48,
+type=$49,
+xml_id=$50
+WHERE id=$51
+RETURNING last_updated
+`
+}
+
+func insertQuery() string {
+	return `
+INSERT INTO deliveryservice (
+active,
+cacheurl,
+ccr_dns_ttl,
+cdn_id,
+check_path,
+deep_caching_type,
+display_name,
+dns_bypass_cname,
+dns_bypass_ip,
+dns_bypass_ip6,
+dns_bypass_ttl,
+dscp,
+edge_header_rewrite,
+geolimit_redirect_url,
+geo_limit,
+geo_limit_countries,
+geo_provider,
+global_max_mbps,
+global_max_tps,
+fq_pacing_rate,
+http_bypass_fqdn,
+info_url,
+initial_dispersion,
+ipv6_routing_enabled,
+logs_enabled,
+long_desc,
+long_desc_1,
+long_desc_2,
+max_dns_answers,
+mid_header_rewrite,
+miss_lat,
+miss_long,
+multi_site_origin,
+org_server_fqdn,
+origin_shield,
+profile,
+protocol,
+qstring_ignore,
+range_request_handling,
+regex_remap,
+regional_geo_blocking,
+remap_text,
+routing_name,
+signing_algorithm,
+ssl_key_version,
+tenant_id,
+tr_request_headers,
+tr_response_headers,
+type,
+xml_id
+)
+VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19,$20,$21,$22,$23,$24,$25,$26,$27,$28,$29,$30,$31,$32,$33,$34,$35,$36,$37,$38,$39,$40,$41,$42,$43,$44,$45,$46,$47,$48,$49,$50)
+RETURNING id, last_updated
+`
+}
diff --git a/traffic_ops/traffic_ops_golang/deliveryservice/dnssec.go b/traffic_ops/traffic_ops_golang/deliveryservice/dnssec.go
new file mode 100644
index 0000000..b0c45a1
--- /dev/null
+++ b/traffic_ops/traffic_ops_golang/deliveryservice/dnssec.go
@@ -0,0 +1,206 @@
+package deliveryservice
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import (
+	"database/sql"
+	"encoding/base64"
+	"errors"
+	"strings"
+	"time"
+
+	"github.com/apache/incubator-trafficcontrol/lib/go-log"
+	"github.com/apache/incubator-trafficcontrol/lib/go-tc"
+	"github.com/apache/incubator-trafficcontrol/traffic_ops/traffic_ops_golang/config"
+	"github.com/apache/incubator-trafficcontrol/traffic_ops/traffic_ops_golang/riaksvc"
+
+	"github.com/miekg/dns"
+)
+
+func createDNSSecKeys(tx *sql.Tx, cfg config.Config, dsID int, xmlID string, dsProtocol int, cdnName string, cdnDomain string, dnssecEnabled bool, exampleURLs []string) error {
+	if !dnssecEnabled {
+		return nil
+	}
+
+	keys, ok, err := riaksvc.GetDNSSECKeys(cdnName, tx, cfg.RiakAuthOptions)
+	if err != nil {
+		log.Errorln("Getting DNSSec keys from Riak: " + err.Error())
+		return errors.New("getting DNSSec keys from Riak: " + err.Error())
+	}
+	if !ok {
+		log.Errorln("Getting DNSSec keys from Riak: no DNSSec keys found")
+		return errors.New("getting DNSSec keys from Riak: no DNSSec keys found")
+	}
+
+	cdnKeys, ok := keys[cdnName]
+	// TODO warn and continue?
+	if !ok {
+		log.Errorln("Getting DNSSec keys from Riak: no DNSSec keys for CDN '" + cdnName + "'")
+		return errors.New("getting DNSSec keys from Riak: no DNSSec keys for CDN")
+	}
+	if len(cdnKeys.ZSK) == 0 {
+		log.Errorln("Getting DNSSec keys from Riak: no DNSSec ZSK keys for CDN '" + cdnName + "'")
+		return errors.New("getting DNSSec keys from Riak: no DNSSec ZSK keys for CDN")
+	}
+	if len(cdnKeys.KSK) == 0 {
+		log.Errorln("Getting DNSSec keys from Riak: no DNSSec ZSK keys for CDN '" + cdnName + "'")
+		return errors.New("getting DNSSec keys from Riak: no DNSSec ZSK keys for CDN")
+	}
+
+	kExpDays := getKeyExpirationDays(cdnKeys.KSK, dnssecDefaultKSKExpirationDays)
+	zExpDays := getKeyExpirationDays(cdnKeys.ZSK, dnssecDefaultZSKExpirationDays)
+	ttl := getKeyTTL(cdnKeys.KSK, dnssecDefaultTTL)
+	dsName, err := getDSDomainName(exampleURLs)
+	if err != nil {
+		log.Errorln("creating DS domain name: " + err.Error())
+		return errors.New("creating DS domain name: " + err.Error())
+	}
+	inception := time.Now()
+	zExpiration := inception.Add(time.Duration(zExpDays) * time.Hour * 24)
+	kExpiration := inception.Add(time.Duration(kExpDays) * time.Hour * 24)
+
+	tld := false
+	effectiveDate := inception
+	zsk, err := getDNSSECKeys(dnssecZSKType, dsName, ttl, inception, zExpiration, dnssecKeyStatusNew, effectiveDate, tld)
+	if err != nil {
+		log.Errorln("getting DNSSEC keys for ZSK: " + err.Error())
+		return errors.New("getting DNSSEC keys for ZSK: " + err.Error())
+	}
+	ksk, err := getDNSSECKeys(dnssecKSKType, dsName, ttl, inception, kExpiration, dnssecKeyStatusNew, effectiveDate, tld)
+	if err != nil {
+		log.Errorln("getting DNSSEC keys for KSK: " + err.Error())
+		return errors.New("getting DNSSEC keys for KSK: " + err.Error())
+	}
+	keys[xmlID] = tc.DNSSECKeySet{ZSK: []tc.DNSSECKey{zsk}, KSK: []tc.DNSSECKey{ksk}}
+
+	if err := riaksvc.PutDNSSECKeys(keys, cdnName, tx, cfg.RiakAuthOptions); err != nil {
+		log.Errorln("putting Riak DNSSEC keys: " + err.Error())
+		return errors.New("putting Riak DNSSEC keys: " + err.Error())
+	}
+	return nil
+}
+
+func getDNSSECKeys(keyType string, dsName string, ttl uint64, inception time.Time, expiration time.Time, status string, effectiveDate time.Time, tld bool) (tc.DNSSECKey, error) {
+	key := tc.DNSSECKey{
+		InceptionDateUnix:  inception.Unix(),
+		ExpirationDateUnix: expiration.Unix(),
+		Name:               dsName + ".",
+		TTLSeconds:         ttl,
+		Status:             status,
+		EffectiveDateUnix:  effectiveDate.Unix(),
+	}
+	isKSK := keyType != dnssecZSKType
+	err := error(nil)
+	key.Public, key.Private, key.DSRecord, err = genKeys(dsName, isKSK, ttl, tld)
+	return key, err
+}
+
+// genKeys generates keys for DNSSEC for a delivery service. Returns the public key, private key, and DS record (which will be nil if ksk or tld is false).
+// This emulates the old Perl Traffic Ops behavior: the public key is of the RFC1035 single-line zone file format, base64 encoded; the private key is of the BIND private-key-file format, base64 encoded; the DSRecord contains the algorithm, digest type, and digest.
+func genKeys(dsName string, ksk bool, ttl uint64, tld bool) (string, string, *tc.DNSSECKeyDSRecord, error) {
+	bits := 1024
+	flags := 256
+	algorithm := dns.RSASHA1 // 5 - http://www.iana.org/assignments/dns-sec-alg-numbers/dns-sec-alg-numbers.xhtml
+	protocol := 3
+
+	if ksk {
+		flags |= 1
+		bits *= 2
+	}
+
+	dnskey := dns.DNSKEY{
+		Hdr: dns.RR_Header{
+			Name:   dsName,
+			Rrtype: dns.TypeDNSKEY,
+			Class:  dns.ClassINET,
+			Ttl:    uint32(ttl),
+		},
+		Flags:     uint16(flags),
+		Protocol:  uint8(protocol),
+		Algorithm: algorithm,
+	}
+
+	priKey, err := dnskey.Generate(bits)
+	if err != nil {
+		return "", "", nil, errors.New("error generating DNS key: " + err.Error())
+	}
+
+	priKeyStr := dnskey.PrivateKeyString(priKey) // BIND9 private-key-file format; cooresponds to Perl Net::DNS::SEC::Private->generate_rsa.dump_rsa_priv
+	priKeyStrBase64 := base64.StdEncoding.EncodeToString([]byte(priKeyStr))
+
+	pubKeyStr := dnskey.String() // RFC1035 single-line zone file format; cooresponds to Perl Net::DNS::RR.plain
+	pubKeyStrBase64 := base64.StdEncoding.EncodeToString([]byte(pubKeyStr))
+
+	keyDS := (*tc.DNSSECKeyDSRecord)(nil)
+	if ksk && tld {
+		dsRecord := dnskey.ToDS(dns.SHA1) // TODO update to SHA512
+		keyDS = &tc.DNSSECKeyDSRecord{Algorithm: int64(dsRecord.Algorithm), DigestType: int64(dsRecord.DigestType), Digest: dsRecord.Digest}
+	}
+
+	return pubKeyStrBase64, priKeyStrBase64, keyDS, nil
+}
+
+// TODO change ttl to time.Duration
+
+const dnssecKSKType = "ksk"
+const dnssecZSKType = "zsk"
+
+func getDSDomainName(dsExampleURLs []string) (string, error) {
+	if len(dsExampleURLs) == 0 {
+		return "", errors.New("no example URLs")
+	}
+
+	dsName := dsExampleURLs[0] + "."
+	firstDot := strings.Index(dsName, ".")
+	if firstDot == -1 {
+		return "", errors.New("malformed example URL, no dots")
+	}
+	if len(dsName) < firstDot+2 {
+		return "", errors.New("malformed example URL, nothing after first dot")
+	}
+	dsName = dsName[firstDot+1:]
+	return dsName, nil
+}
+
+const dnssecKeyStatusNew = "new"
+const secondsPerDay = 86400
+const dnssecDefaultKSKExpirationDays = 365
+const dnssecDefaultZSKExpirationDays = 30
+const dnssecDefaultTTL = 60
+
+func getKeyExpirationDays(keys []tc.DNSSECKey, defaultExpirationDays uint64) uint64 {
+	for _, key := range keys {
+		if key.Status != dnssecKeyStatusNew {
+			continue
+		}
+		return uint64((key.ExpirationDateUnix - key.InceptionDateUnix) / secondsPerDay)
+	}
+	return defaultExpirationDays
+}
+
+func getKeyTTL(keys []tc.DNSSECKey, defaultTTL uint64) uint64 {
+	for _, key := range keys {
+		if key.Status != dnssecKeyStatusNew {
+			continue
+		}
+		return key.TTLSeconds
+	}
+	return defaultTTL
+}
diff --git a/traffic_ops/traffic_ops_golang/deliveryservice/request/requests_test.go b/traffic_ops/traffic_ops_golang/deliveryservice/request/requests_test.go
index 7abb25c..993351c 100644
--- a/traffic_ops/traffic_ops_golang/deliveryservice/request/requests_test.go
+++ b/traffic_ops/traffic_ops_golang/deliveryservice/request/requests_test.go
@@ -73,14 +73,14 @@ func TestGetDeliveryServiceRequest(t *testing.T) {
 	}
 
 	expectedErrors := []string{
-	/*
-		`'regionalGeoBlocking' is required`,
-		`'xmlId' cannot contain spaces`,
-		`'dscp' is required`,
-		`'displayName' cannot be blank`,
-		`'geoProvider' is required`,
-		`'typeId' is required`,
-	*/
+		/*
+			`'regionalGeoBlocking' is required`,
+			`'xmlId' cannot contain spaces`,
+			`'dscp' is required`,
+			`'displayName' cannot be blank`,
+			`'geoProvider' is required`,
+			`'typeId' is required`,
+		*/
 	}
 
 	r.SetKeys(map[string]interface{}{"id": 10})
diff --git a/traffic_ops/traffic_ops_golang/deliveryservice/request/validate.go b/traffic_ops/traffic_ops_golang/deliveryservice/request/validate.go
index 50ae622..74fd1b2 100644
--- a/traffic_ops/traffic_ops_golang/deliveryservice/request/validate.go
+++ b/traffic_ops/traffic_ops_golang/deliveryservice/request/validate.go
@@ -24,10 +24,11 @@ import (
 	"fmt"
 	"strconv"
 
-	tc "github.com/apache/incubator-trafficcontrol/lib/go-tc"
+	"github.com/apache/incubator-trafficcontrol/lib/go-tc"
 	"github.com/apache/incubator-trafficcontrol/traffic_ops/traffic_ops_golang/deliveryservice"
 	"github.com/apache/incubator-trafficcontrol/traffic_ops/traffic_ops_golang/tovalidate"
-	validation "github.com/go-ozzo/ozzo-validation"
+
+	"github.com/go-ozzo/ozzo-validation"
 	"github.com/jmoiron/sqlx"
 )
 
@@ -61,7 +62,7 @@ func (req *TODeliveryServiceRequest) Validate(db *sqlx.DB) []error {
 	errs := tovalidate.ToErrors(errMap)
 
 	// ensure the deliveryservice requested is valid
-	e := deliveryservice.Validate(db, req.DeliveryService)
+	e := deliveryservice.ValidateV13(db, req.DeliveryService)
 	errs = append(errs, e...)
 
 	return errs
diff --git a/traffic_ops/traffic_ops_golang/deliveryservices_keys.go b/traffic_ops/traffic_ops_golang/deliveryservices_keys.go
index dadf694..577e272 100644
--- a/traffic_ops/traffic_ops_golang/deliveryservices_keys.go
+++ b/traffic_ops/traffic_ops_golang/deliveryservices_keys.go
@@ -26,6 +26,7 @@ import (
 	"encoding/base64"
 	"encoding/json"
 	"encoding/pem"
+	"errors"
 	"fmt"
 	"io/ioutil"
 	"net/http"
@@ -38,15 +39,11 @@ import (
 	"github.com/apache/incubator-trafficcontrol/traffic_ops/traffic_ops_golang/config"
 	"github.com/apache/incubator-trafficcontrol/traffic_ops/traffic_ops_golang/riaksvc"
 	"github.com/apache/incubator-trafficcontrol/traffic_ops/traffic_ops_golang/tenant"
-	"github.com/basho/riak-go-client"
 	"github.com/jmoiron/sqlx"
 )
 
 // Delivery Services: SSL Keys.
 
-// SSLKeysBucket ...
-const SSLKeysBucket = "ssl"
-
 // returns the cdn_id found by domainname.
 func getCDNIDByDomainname(domainName string, db *sqlx.DB) (sql.NullInt64, error) {
 	cdnQuery := `SELECT id from cdn WHERE domain_name = $1`
@@ -100,60 +97,34 @@ func getXMLID(cdnID sql.NullInt64, hostRegex string, db *sqlx.DB) (sql.NullStrin
 	return xmlID, nil
 }
 
-func getDeliveryServiceSSLKeysByXMLID(xmlID string, version string, db *sqlx.DB, cfg config.Config) ([]byte, error) {
-	var respBytes []byte
-	// create and start a cluster
-	cluster, err := riaksvc.GetRiakCluster(db, cfg.RiakAuthOptions)
-	if err != nil {
+func getDeliveryServiceSSLKeysByXMLID(xmlID string, version string, db *sql.DB, cfg config.Config) ([]byte, error) {
+	if cfg.RiakEnabled == false {
+		err := errors.New("Riak is not configured!")
+		log.Errorln("getting delivery services SSL keys: " + err.Error())
 		return nil, err
 	}
-	if err = cluster.Start(); err != nil {
-		return nil, err
-	}
-	defer func() {
-		if err := cluster.Stop(); err != nil {
-			log.Errorf("%v\n", err)
-		}
-	}()
-
-	if version == "" {
-		xmlID = xmlID + "-latest"
-	} else {
-		xmlID = xmlID + "-" + version
-	}
-
-	// get the deliveryservice ssl keys by xmlID and version
-	ro, err := riaksvc.FetchObjectValues(xmlID, SSLKeysBucket, cluster)
+	key, ok, err := riaksvc.GetDeliveryServiceSSLKeysObj(xmlID, version, db, cfg.RiakAuthOptions)
 	if err != nil {
+		log.Errorln("getting delivery service keys: " + err.Error())
 		return nil, err
 	}
-
-	// no keys we're found
-	if ro == nil {
+	if !ok {
 		alert := tc.CreateAlerts(tc.InfoLevel, "no object found for the specified key")
-		respBytes, err = json.Marshal(alert)
+		respBytes, err := json.Marshal(alert)
 		if err != nil {
 			log.Errorf("failed to marshal an alert response: %s\n", err)
 			return nil, err
 		}
-	} else { // keys were found
-		var key tc.DeliveryServiceSSLKeys
-
-		// unmarshal into a response tc.DeliveryServiceSSLKeysResponse object.
-		if err := json.Unmarshal(ro[0].Value, &key); err != nil {
-			log.Errorf("failed at unmarshaling sslkey response: %s\n", err)
-			return nil, err
-		}
-		resp := tc.DeliveryServiceSSLKeysResponse{
-			Response: key,
-		}
-		respBytes, err = json.Marshal(resp)
-		if err != nil {
-			log.Errorf("failed to marshal a sslkeys response: %s\n", err)
-			return nil, err
-		}
+		return respBytes, nil
 	}
 
+	respBytes := []byte{}
+	resp := tc.DeliveryServiceSSLKeysResponse{Response: key}
+	respBytes, err = json.Marshal(resp)
+	if err != nil {
+		log.Errorf("failed to marshal a sslkeys response: %s\n", err)
+		return nil, err
+	}
 	return respBytes, nil
 }
 
@@ -245,8 +216,12 @@ func verifyAndEncodeCertificate(certificate string, rootCA string) (string, erro
 func addDeliveryServiceSSLKeysHandler(db *sqlx.DB, cfg config.Config) http.HandlerFunc {
 	return func(w http.ResponseWriter, r *http.Request) {
 		handleErr := tc.GetHandleErrorsFunc(w, r)
-		var keysObj tc.DeliveryServiceSSLKeys
-
+		if !cfg.RiakEnabled {
+			err := errors.New("Riak is not configured!")
+			log.Errorln("adding Riak SSL keys for delivery service: " + err.Error())
+			handleErr(http.StatusInternalServerError, err)
+			return
+		}
 		defer r.Body.Close()
 
 		ctx := r.Context()
@@ -262,7 +237,7 @@ func addDeliveryServiceSSLKeysHandler(db *sqlx.DB, cfg config.Config) http.Handl
 			return
 		}
 
-		// unmarshal the request
+		keysObj := tc.DeliveryServiceSSLKeys{}
 		if err := json.Unmarshal(data, &keysObj); err != nil {
 			log.Errorf("ERROR: could not unmarshal the request, %v\n", err)
 			handleErr(http.StatusBadRequest, err)
@@ -301,34 +276,8 @@ func addDeliveryServiceSSLKeysHandler(db *sqlx.DB, cfg config.Config) http.Handl
 			return
 		}
 
-		// create and start a cluster
-		cluster, err := riaksvc.GetRiakCluster(db, cfg.RiakAuthOptions)
-		if err != nil {
-			handleErr(http.StatusInternalServerError, err)
-			return
-		}
-		if err = cluster.Start(); err != nil {
-			handleErr(http.StatusInternalServerError, err)
-			return
-		}
-		defer func() {
-			if err := cluster.Stop(); err != nil {
-				log.Errorf("%v\n", err)
-			}
-		}()
-
-		// create a storage object and store the data
-		obj := &riak.Object{
-			ContentType:     "text/json",
-			Charset:         "utf-8",
-			ContentEncoding: "utf-8",
-			Key:             keysObj.DeliveryService,
-			Value:           []byte(keysJSON),
-		}
-
-		err = riaksvc.SaveObject(obj, SSLKeysBucket, cluster)
-		if err != nil {
-			log.Errorf("%v\n", err)
+		if err := riaksvc.PutDeliveryServiceSSLKeysObj(keysObj, db.DB, cfg.RiakAuthOptions); err != nil {
+			log.Errorln("putting Riak SSL keys for delivery service '" + keysObj.DeliveryService + "': " + err.Error())
 			handleErr(http.StatusInternalServerError, err)
 			return
 		}
@@ -430,7 +379,7 @@ func getDeliveryServiceSSLKeysByHostNameHandler(db *sqlx.DB, cfg config.Config)
 						return
 					}
 				}
-				respBytes, err = getDeliveryServiceSSLKeysByXMLID(xmlID, version, db, cfg)
+				respBytes, err = getDeliveryServiceSSLKeysByXMLID(xmlID, version, db.DB, cfg)
 				if err != nil {
 					handleErr(http.StatusInternalServerError, err)
 					return
@@ -485,7 +434,7 @@ func getDeliveryServiceSSLKeysByXMLIDHandler(db *sqlx.DB, cfg config.Config) htt
 			}
 		}
 
-		respBytes, err = getDeliveryServiceSSLKeysByXMLID(xmlID, version, db, cfg)
+		respBytes, err = getDeliveryServiceSSLKeysByXMLID(xmlID, version, db.DB, cfg)
 		if err != nil {
 			handleErr(http.StatusInternalServerError, err)
 			return
diff --git a/traffic_ops/traffic_ops_golang/riaksvc/dsutil.go b/traffic_ops/traffic_ops_golang/riaksvc/dsutil.go
new file mode 100644
index 0000000..8532a53
--- /dev/null
+++ b/traffic_ops/traffic_ops_golang/riaksvc/dsutil.go
@@ -0,0 +1,140 @@
+package riaksvc
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import (
+	"database/sql"
+	"encoding/json"
+	"errors"
+
+	"github.com/apache/incubator-trafficcontrol/lib/go-log"
+	"github.com/apache/incubator-trafficcontrol/lib/go-tc"
+
+	"github.com/basho/riak-go-client"
+)
+
+const DeliveryServiceSSLKeysBucket = "ssl"
+const DNSSECKeysBucket = "dnssec"
+
+func GetDeliveryServiceSSLKeysObj(xmlID string, version string, db *sql.DB, authOpts *riak.AuthOptions) (tc.DeliveryServiceSSLKeys, bool, error) {
+	key := tc.DeliveryServiceSSLKeys{}
+	if version == "" {
+		xmlID += "-latest"
+	} else {
+		xmlID += "-" + version
+	}
+	found := false
+	err := WithCluster(db, authOpts, func(cluster StorageCluster) error {
+		// get the deliveryservice ssl keys by xmlID and version
+		ro, err := FetchObjectValues(xmlID, DeliveryServiceSSLKeysBucket, cluster)
+		if err != nil {
+			return err
+		}
+		if len(ro) == 0 {
+			return nil // not found
+		}
+		if err := json.Unmarshal(ro[0].Value, &key); err != nil {
+			log.Errorf("failed at unmarshaling sslkey response: %s\n", err)
+			return errors.New("unmarshalling Riak result: " + err.Error())
+		}
+		found = true
+		return nil
+	})
+	if err != nil {
+		return key, false, err
+	}
+	return key, found, nil
+}
+
+func PutDeliveryServiceSSLKeysObj(key tc.DeliveryServiceSSLKeys, db *sql.DB, authOpts *riak.AuthOptions) error {
+	keyJSON, err := json.Marshal(&key)
+	if err != nil {
+		return errors.New("marshalling key: " + err.Error())
+	}
+	err = WithCluster(db, authOpts, func(cluster StorageCluster) error {
+		obj := &riak.Object{
+			ContentType:     "text/json",
+			Charset:         "utf-8",
+			ContentEncoding: "utf-8",
+			Key:             key.DeliveryService,
+			Value:           []byte(keyJSON),
+		}
+		if err = SaveObject(obj, DeliveryServiceSSLKeysBucket, cluster); err != nil {
+			return errors.New("saving Riak object: " + err.Error())
+		}
+		return nil
+	})
+	return err
+}
+
+func GetDNSSECKeys(cdnName string, tx *sql.Tx, authOpts *riak.AuthOptions) (tc.DNSSECKeys, bool, error) {
+	key := tc.DNSSECKeys{}
+	found := false
+	log.Errorln("riaksvc.GetDNSSECKeys calling")
+	err := WithClusterTx(tx, authOpts, func(cluster StorageCluster) error {
+		log.Errorln("riaksvc.GetDNSSECKeys in WithClusterTx")
+		ro, err := FetchObjectValues(cdnName, DNSSECKeysBucket, cluster)
+		log.Errorln("riaksvc.GetDNSSECKeys fetched object values")
+		if err != nil {
+			log.Errorln("riaksvc.GetDNSSECKeys fetched object values returning err")
+			return err
+		}
+		if len(ro) == 0 {
+			log.Errorln("riaksvc.GetDNSSECKeys returning nil, len(ro) is 0")
+			return nil // not found
+		}
+		log.Errorln("riaksvc.GetDNSSECKeys unmarshalling")
+		if err := json.Unmarshal(ro[0].Value, &key); err != nil {
+			log.Errorln("Unmarshaling Riak dnssec response: " + err.Error())
+			return errors.New("unmarshalling Riak dnssec response: " + err.Error())
+		}
+		log.Errorln("riaksvc.GetDNSSECKeys unmarshalled, found true, returning nil err")
+		found = true
+		return nil
+	})
+	log.Errorln("riaksvc.GetDNSSECKeys out of WithCluster")
+	if err != nil {
+		log.Errorln("riaksvc.GetDNSSECKeys WithCluster err, returning err")
+		return key, false, err
+	}
+	log.Errorln("riaksvc.GetDNSSECKeys returning success")
+	return key, found, nil
+}
+
+func PutDNSSECKeys(keys tc.DNSSECKeys, cdnName string, tx *sql.Tx, authOpts *riak.AuthOptions) error {
+	keyJSON, err := json.Marshal(&keys)
+	if err != nil {
+		return errors.New("marshalling keys: " + err.Error())
+	}
+	err = WithClusterTx(tx, authOpts, func(cluster StorageCluster) error {
+		obj := &riak.Object{
+			ContentType:     "application/json",
+			Charset:         "utf-8",
+			ContentEncoding: "utf-8",
+			Key:             cdnName,
+			Value:           []byte(keyJSON),
+		}
+		if err = SaveObject(obj, DNSSECKeysBucket, cluster); err != nil {
+			return errors.New("saving Riak object: " + err.Error())
+		}
+		return nil
+	})
+	return err
+}
diff --git a/traffic_ops/traffic_ops_golang/riaksvc/riak_services.go b/traffic_ops/traffic_ops_golang/riaksvc/riak_services.go
index a16c852..c10f2ef 100644
--- a/traffic_ops/traffic_ops_golang/riaksvc/riak_services.go
+++ b/traffic_ops/traffic_ops_golang/riaksvc/riak_services.go
@@ -21,12 +21,14 @@ package riaksvc
 
 import (
 	"crypto/tls"
+	"database/sql"
 	"encoding/json"
 	"errors"
 	"fmt"
 	"io/ioutil"
 	"time"
 
+	"github.com/apache/incubator-trafficcontrol/lib/go-log"
 	"github.com/apache/incubator-trafficcontrol/lib/go-tc"
 
 	"github.com/basho/riak-go-client"
@@ -165,7 +167,7 @@ func SaveObject(obj *riak.Object, bucket string, cluster StorageCluster) error {
 }
 
 // returns a riak cluster of online riak nodes.
-func GetRiakCluster(db *sqlx.DB, authOptions *riak.AuthOptions) (StorageCluster, error) {
+func GetRiakCluster(db *sql.DB, authOptions *riak.AuthOptions) (StorageCluster, error) {
 	riakServerQuery := `
 		SELECT s.host_name, s.domain_name FROM server s
 		INNER JOIN type t on s.type = t.id
@@ -216,3 +218,109 @@ func GetRiakCluster(db *sqlx.DB, authOptions *riak.AuthOptions) (StorageCluster,
 
 	return RiakStorageCluster{Cluster: cluster}, err
 }
+
+func WithCluster(db *sql.DB, authOpts *riak.AuthOptions, f func(StorageCluster) error) error {
+	log.Errorf("DEBUG WithCluster authOps: %++v\n", authOpts)
+	cluster, err := GetRiakCluster(db, authOpts)
+	if err != nil {
+		return errors.New("getting riak cluster: " + err.Error())
+	}
+	if err = cluster.Start(); err != nil {
+		return errors.New("starting riak cluster: " + err.Error())
+	}
+	defer func() {
+		if err := cluster.Stop(); err != nil {
+			log.Errorln("error stopping Riak cluster: " + err.Error())
+		}
+	}()
+	return f(cluster)
+}
+
+func WithClusterX(db *sqlx.DB, authOpts *riak.AuthOptions, f func(StorageCluster) error) error {
+	log.Errorf("DEBUG WithCluster authOps: %++v\n", authOpts)
+	cluster, err := GetRiakCluster(db.DB, authOpts)
+	if err != nil {
+		return errors.New("getting riak cluster: " + err.Error())
+	}
+	if err = cluster.Start(); err != nil {
+		return errors.New("starting riak cluster: " + err.Error())
+	}
+	defer func() {
+		if err := cluster.Stop(); err != nil {
+			log.Errorln("error stopping Riak cluster: " + err.Error())
+		}
+	}()
+	return f(cluster)
+}
+
+// returns a riak cluster of online riak nodes.
+func GetRiakClusterTx(tx *sql.Tx, authOptions *riak.AuthOptions) (StorageCluster, error) {
+	// TODO remove duplication with GetRiakCluster
+
+	riakServerQuery := `
+		SELECT s.host_name, s.domain_name FROM server s
+		INNER JOIN type t on s.type = t.id
+		INNER JOIN status st on s.status = st.id
+		WHERE t.name = 'RIAK' AND st.name = 'ONLINE'
+		`
+
+	if authOptions == nil {
+		return nil, errors.New("ERROR: no riak auth information from riak.conf, cannot authenticate to any riak servers")
+	}
+
+	var nodes []*riak.Node
+	rows, err := tx.Query(riakServerQuery)
+	if err != nil {
+		return nil, err
+	}
+	defer rows.Close()
+
+	for rows.Next() {
+		var s tc.Server
+		var n *riak.Node
+		if err := rows.Scan(&s.HostName, &s.DomainName); err != nil {
+			return nil, err
+		}
+		addr := fmt.Sprintf("%s.%s:%d", s.HostName, s.DomainName, RiakPort)
+		nodeOpts := &riak.NodeOptions{
+			RemoteAddress: addr,
+			AuthOptions:   authOptions,
+		}
+		nodeOpts.AuthOptions.TlsConfig.ServerName = fmt.Sprintf("%s.%s", s.HostName, s.DomainName)
+		n, err := riak.NewNode(nodeOpts)
+		if err != nil {
+			return nil, err
+		}
+		nodes = append(nodes, n)
+	}
+
+	if len(nodes) == 0 {
+		return nil, errors.New("ERROR: no available riak servers")
+	}
+
+	opts := &riak.ClusterOptions{
+		Nodes:             nodes,
+		ExecutionAttempts: MaxCommandExecutionAttempts,
+	}
+
+	cluster, err := riak.NewCluster(opts)
+
+	return RiakStorageCluster{Cluster: cluster}, err
+}
+
+func WithClusterTx(tx *sql.Tx, authOpts *riak.AuthOptions, f func(StorageCluster) error) error {
+	log.Errorf("DEBUG WithCluster authOps: %++v\n", authOpts)
+	cluster, err := GetRiakClusterTx(tx, authOpts)
+	if err != nil {
+		return errors.New("getting riak cluster: " + err.Error())
+	}
+	if err = cluster.Start(); err != nil {
+		return errors.New("starting riak cluster: " + err.Error())
+	}
+	defer func() {
+		if err := cluster.Stop(); err != nil {
+			log.Errorln("error stopping Riak cluster: " + err.Error())
+		}
+	}()
+	return f(cluster)
+}
diff --git a/traffic_ops/traffic_ops_golang/routes.go b/traffic_ops/traffic_ops_golang/routes.go
index 6976ca4..a1bf346 100644
--- a/traffic_ops/traffic_ops_golang/routes.go
+++ b/traffic_ops/traffic_ops_golang/routes.go
@@ -37,6 +37,7 @@ import (
 	"github.com/apache/incubator-trafficcontrol/traffic_ops/traffic_ops_golang/cdn"
 	"github.com/apache/incubator-trafficcontrol/traffic_ops/traffic_ops_golang/coordinate"
 	"github.com/apache/incubator-trafficcontrol/traffic_ops/traffic_ops_golang/crconfig"
+	"github.com/apache/incubator-trafficcontrol/traffic_ops/traffic_ops_golang/deliveryservice"
 	dsrequest "github.com/apache/incubator-trafficcontrol/traffic_ops/traffic_ops_golang/deliveryservice/request"
 	"github.com/apache/incubator-trafficcontrol/traffic_ops/traffic_ops_golang/deliveryservice/request/comment"
 	dsserver "github.com/apache/incubator-trafficcontrol/traffic_ops/traffic_ops_golang/deliveryservice/servers"
@@ -268,16 +269,31 @@ func Routes(d ServerData) ([]Route, []RawRoute, http.Handler, error) {
 		{1.1, http.MethodPost, `parameterprofile/?$`, profileparameter.PostParamProfile(d.DB.DB), auth.PrivLevelOperations, Authenticated, nil},
 		{1.1, http.MethodDelete, `profileparameters/{profileId}/{parameterId}$`, api.DeleteHandler(profileparameter.GetRefType(), d.DB), auth.PrivLevelOperations, Authenticated, nil},
 
-		//SSLKeys deliveryservice endpoints here that are marked  marked as '-wip' need to have tenancy checks added
-		{1.3, http.MethodGet, `deliveryservices-wip/xmlId/{xmlID}/sslkeys$`, getDeliveryServiceSSLKeysByXMLIDHandler(d.DB, d.Config), auth.PrivLevelAdmin, Authenticated, nil},
-		{1.3, http.MethodGet, `deliveryservices-wip/hostname/{hostName}/sslkeys$`, getDeliveryServiceSSLKeysByHostNameHandler(d.DB, d.Config), auth.PrivLevelAdmin, Authenticated, nil},
-		{1.3, http.MethodPost, `deliveryservices-wip/hostname/{hostName}/sslkeys/add$`, addDeliveryServiceSSLKeysHandler(d.DB, d.Config), auth.PrivLevelAdmin, Authenticated, nil},
-
 		//CRConfig
 		{1.1, http.MethodGet, `cdns/{cdn}/snapshot/?$`, crconfig.SnapshotGetHandler(d.DB, d.Config), crconfig.PrivLevel, Authenticated, nil},
 		{1.1, http.MethodGet, `cdns/{cdn}/snapshot/new/?$`, crconfig.Handler(d.DB, d.Config), crconfig.PrivLevel, Authenticated, nil},
 		{1.1, http.MethodPut, `cdns/{id}/snapshot/?$`, crconfig.SnapshotHandler(d.DB, d.Config), crconfig.PrivLevel, Authenticated, nil},
 		{1.1, http.MethodPut, `snapshot/{cdn}/?$`, crconfig.SnapshotHandler(d.DB, d.Config), crconfig.PrivLevel, Authenticated, nil},
+
+		//SSLKeys deliveryservice endpoints here that are marked  marked as '-wip' need to have tenancy checks added
+		{1.3, http.MethodGet, `deliveryservices-wip/xmlId/{xmlID}/sslkeys$`, getDeliveryServiceSSLKeysByXMLIDHandler(d.DB, d.Config), auth.PrivLevelAdmin, Authenticated, nil},
+		{1.3, http.MethodGet, `deliveryservices-wip/hostname/{hostName}/sslkeys$`, getDeliveryServiceSSLKeysByHostNameHandler(d.DB, d.Config), auth.PrivLevelAdmin, Authenticated, nil},
+		{1.3, http.MethodPost, `deliveryservices-wip/hostname/{hostName}/sslkeys/add$`, addDeliveryServiceSSLKeysHandler(d.DB, d.Config), auth.PrivLevelAdmin, Authenticated, nil},
+
+		//DeliveryServices
+		{1.3, http.MethodGet, `deliveryservices/?(\.json)?$`, api.ReadHandler(deliveryservice.GetRefTypeV13(d.Config, d.DB), d.DB), auth.PrivLevelReadOnly, Authenticated, nil},
+		{1.1, http.MethodGet, `deliveryservices/?(\.json)?$`, api.ReadHandler(deliveryservice.GetRefTypeV12(d.Config, d.DB), d.DB), auth.PrivLevelReadOnly, Authenticated, nil},
+		{1.3, http.MethodGet, `deliveryservices/{id}$`, api.ReadHandler(deliveryservice.GetRefTypeV13(d.Config, d.DB), d.DB), auth.PrivLevelReadOnly, Authenticated, nil},
+		{1.1, http.MethodGet, `deliveryservices/{id}$`, api.ReadHandler(deliveryservice.GetRefTypeV12(d.Config, d.DB), d.DB), auth.PrivLevelReadOnly, Authenticated, nil},
+		{1.3, http.MethodPost, `deliveryservices/?(\.json)?$`, deliveryservice.CreateV13(d.DB, d.Config), auth.PrivLevelOperations, Authenticated, nil},
+		{1.1, http.MethodPost, `deliveryservices/?(\.json)?$`, deliveryservice.CreateV12(d.DB, d.Config), auth.PrivLevelOperations, Authenticated, nil},
+		{1.3, http.MethodPut, `deliveryservices/{id}$`, deliveryservice.UpdateV13(d.DB, d.Config), auth.PrivLevelOperations, Authenticated, nil},
+		{1.1, http.MethodPut, `deliveryservices/{id}$`, deliveryservice.UpdateV12(d.DB, d.Config), auth.PrivLevelOperations, Authenticated, nil},
+		{1.3, http.MethodDelete, `deliveryservices/{id}$`, api.DeleteHandler(deliveryservice.GetRefTypeV13(d.Config, d.DB), d.DB), auth.PrivLevelOperations, Authenticated, nil},
+		{1.1, http.MethodDelete, `deliveryservices/{id}$`, api.DeleteHandler(deliveryservice.GetRefTypeV12(d.Config, d.DB), d.DB), auth.PrivLevelOperations, Authenticated, nil},
+
+		//System
+		{1.1, http.MethodGet, `system/info/?(\.json)?$`, systeminfo.Handler(d.DB), auth.PrivLevelReadOnly, Authenticated, nil},
 	}
 
 	// rawRoutes are served at the root path. These should be almost exclusively old Perl pre-API routes, which have yet to be converted in all clients. New routes should be in the versioned API path.
diff --git a/traffic_ops/traffic_ops_golang/urisigning.go b/traffic_ops/traffic_ops_golang/urisigning.go
index 50679fd..5bf4082 100644
--- a/traffic_ops/traffic_ops_golang/urisigning.go
+++ b/traffic_ops/traffic_ops_golang/urisigning.go
@@ -90,7 +90,7 @@ func getURIsignkeysHandler(db *sqlx.DB, cfg config.Config) http.HandlerFunc {
 		}
 
 		// create and start a cluster
-		cluster, err := riaksvc.GetRiakCluster(db, cfg.RiakAuthOptions)
+		cluster, err := riaksvc.GetRiakCluster(db.DB, cfg.RiakAuthOptions)
 		if err != nil {
 			handleErr(http.StatusInternalServerError, err)
 			return
@@ -172,7 +172,7 @@ func removeDeliveryServiceURIKeysHandler(db *sqlx.DB, cfg config.Config) http.Ha
 		}
 
 		// create and start a cluster
-		cluster, err := riaksvc.GetRiakCluster(db, cfg.RiakAuthOptions)
+		cluster, err := riaksvc.GetRiakCluster(db.DB, cfg.RiakAuthOptions)
 		if err != nil {
 			handleErr(http.StatusInternalServerError, err)
 			return
@@ -281,7 +281,7 @@ func saveDeliveryServiceURIKeysHandler(db *sqlx.DB, cfg config.Config) http.Hand
 		}
 
 		// create and start a cluster
-		cluster, err := riaksvc.GetRiakCluster(db, cfg.RiakAuthOptions)
+		cluster, err := riaksvc.GetRiakCluster(db.DB, cfg.RiakAuthOptions)
 		if err != nil {
 			handleErr(http.StatusInternalServerError, err)
 			return

-- 
To stop receiving notification emails like this one, please contact
dewrich@apache.org.