dangogh closed pull request #1889: Add TO Golang CRConfig generation
URL: https://github.com/apache/incubator-trafficcontrol/pull/1889
This is a PR merged from a forked repository.
As GitHub hides the original diff on merge, it is displayed below for
the sake of provenance:
As this is a foreign pull request (from a fork), the diff is supplied
below (as it won't show otherwise due to GitHub magic):
diff --git a/lib/go-tc/crconfig.go b/lib/go-tc/crconfig.go
index 12a7d3ccb..f7b57c786 100644
--- a/lib/go-tc/crconfig.go
+++ b/lib/go-tc/crconfig.go
@@ -21,11 +21,13 @@ package tc
// CRConfig is JSON-serializable as the CRConfig used by Traffic Control.
type CRConfig struct {
- Config CRConfigConfig
`json:"config,omitempty"`
+ // Config is mostly a map of string values, but may contain an 'soa'
key which is a map[string]string, and may contain a 'ttls' key with a value
map[string]string. It might not contain these values, so they must be checked
for, and all values must be checked by the user and an error returned if the
type is unexpected. Be aware, neither the language nor the API provides any
guarantees about the type!
+ Config map[string]interface{}
`json:"config,omitempty"`
ContentServers map[string]CRConfigTrafficOpsServer
`json:"contentServers,omitempty"`
ContentRouters map[string]CRConfigRouter
`json:"contentRouters,omitempty"`
DeliveryServices map[string]CRConfigDeliveryService
`json:"deliveryServices,omitempty"`
EdgeLocations map[string]CRConfigLatitudeLongitude
`json:"edgeLocations,omitempty"`
+ RouterLocations map[string]CRConfigLatitudeLongitude
`json:"trafficRouterLocations,omitempty"`
Monitors map[string]CRConfigMonitor
`json:"monitors,omitempty"`
Stats CRConfigStats
`json:"stats,omitempty"`
}
@@ -65,9 +67,10 @@ type CRConfigTTL struct {
type CRConfigRouterStatus string
type CRConfigRouter struct {
- APIPort *string `json:"apiPort,omitempty"`
+ APIPort *string `json:"api.port,omitempty"`
FQDN *string `json:"fqdn,omitempty"`
- HTTPSPort *int `json:"httpsPort,omitempty"`
+ HTTPSPort *int `json:"httpsPort"`
+ HashCount *int `json:"hashCount,omitempty"`
IP *string `json:"ip,omitempty"`
IP6 *string `json:"ip6,omitempty"`
Location *string `json:"location,omitempty"`
@@ -83,38 +86,68 @@ type CRConfigTrafficOpsServer struct {
Fqdn *string `json:"fqdn,omitempty"`
HashCount *int `json:"hashCount,omitempty"`
HashId *string `json:"hashId,omitempty"`
- HttpsPort *int `json:"httpsPort,omitempty"`
+ HttpsPort *int `json:"httpsPort"`
InterfaceName *string `json:"interfaceName,omitempty"`
Ip *string `json:"ip,omitempty"`
Ip6 *string `json:"ip6,omitempty"`
LocationId *string `json:"locationId,omitempty"`
- Port *int `json:"port,omitempty"`
+ Port *int `json:"port"`
Profile *string `json:"profile,omitempty"`
ServerStatus *CRConfigServerStatus `json:"status,omitempty"`
ServerType *string `json:"type,omitempty"`
DeliveryServices map[string][]string
`json:"deliveryServices,omitempty"`
+ RoutingDisabled int64 `json:"routingDisabled"`
}
//TODO: drichardson - reconcile this with the DeliveryService struct in
deliveryservices.go
type CRConfigDeliveryService struct {
- CoverageZoneOnly *string
`json:"coverageZoneOnly,omitempty"`
- Dispersion *CRConfigDispersion
`json:"dispersion,omitempty"`
- Domains []string
`json:"domains,omitempty"`
- GeoLocationProvider *string
`json:"geoLocationProvider,omitempty"`
- MatchSets []MatchSet
`json:"matchSets,omitempty"`
- MissLocation *CRConfigLatitudeLongitude
`json:"missLocation,omitempty"`
- Protocol *CRConfigDeliveryServiceProtocol
`json:"protocol,omitempty"`
- RegionalGeoBlocking *string
`json:"regionalGeoBlocking,omitempty"`
- ResponseHeaders map[string]string
`json:"responseHeaders,omitempty"`
- Soa *SOA
`json:"soa,omitempty"`
- SSLEnabled *string
`json:"sslEnabled,omitempty"`
- TTL *int
`json:"ttl,omitempty"`
- TTLs *CRConfigTTL
`json:"ttls,omitempty"`
+ CoverageZoneOnly bool
`json:"coverageZoneOnly,string"`
+ Dispersion *CRConfigDispersion
`json:"dispersion,omitempty"`
+ Domains []string
`json:"domains,omitempty"`
+ GeoLocationProvider *string
`json:"geolocationProvider,omitempty"`
+ MatchSets []*MatchSet
`json:"matchsets,omitempty"`
+ MissLocation *CRConfigLatitudeLongitudeShort
`json:"missLocation,omitempty"`
+ Protocol *CRConfigDeliveryServiceProtocol
`json:"protocol,omitempty"`
+ RegionalGeoBlocking *string
`json:"regionalGeoBlocking,omitempty"`
+ ResponseHeaders map[string]string
`json:"responseHeaders,omitempty"`
+ RequestHeaders []string
`json:"requestHeaders,omitempty"`
+ Soa *SOA
`json:"soa,omitempty"`
+ SSLEnabled bool
`json:"sslEnabled,string"`
+ TTL *int
`json:"ttl,omitempty"`
+ TTLs *CRConfigTTL
`json:"ttls,omitempty"`
+ MaxDNSIPsForLocation *int
`json:"maxDnsIpsForLocation,omitempty"`
+ IP6RoutingEnabled *bool
`json:"ip6RoutingEnabled,string,omitempty"`
+ RoutingName *string
`json:"routingName,omitempty"`
+ BypassDestination map[string]*CRConfigBypassDestination
`json:"bypassDestination,omitempty"`
+ DeepCachingType *DeepCachingType
`json:"deepCachingType"`
+ GeoEnabled []CRConfigGeoEnabled
`json:"geoEnabled,omitempty"`
+ GeoLimitRedirectURL *string
`json:"geoLimitRedirectURL,omitempty"`
+ StaticDNSEntries []StaticDNSEntry
`json:"staticDnsEntries,omitempty"`
+}
+
+type CRConfigGeoEnabled struct {
+ CountryCode string `json:"countryCode"`
+}
+
+type StaticDNSEntry struct {
+ Name string `json:"name"`
+ TTL int `json:"ttl"`
+ Type string `json:"type"`
+ Value string `json:"value"`
+}
+
+type CRConfigBypassDestination struct {
+ IP *string `json:"ip,omitempty"` // only used by DNS DSes
+ IP6 *string `json:"ip6,omitempty"` // only used by DNS DSes
+ CName *string `json:"cname,omitempty"` // only used by DNS DSes
+ TTL *int `json:"ttl,omitempty"` // only used by DNS DSes
+ FQDN *string `json:"fqdn,omitempty"` // only used by HTTP DSes
+ Port *string `json:"port,omitempty"` // only used by HTTP DSes
}
type CRConfigDispersion struct {
- Limit int `json:"limit,omitempty"`
- Shuffled *string `json:"shuffled,omitempty"`
+ Limit int `json:"limit"`
+ Shuffled bool `json:"shuffled,string"`
}
type CRConfigLatitudeLongitude struct {
@@ -122,15 +155,20 @@ type CRConfigLatitudeLongitude struct {
Lon float64 `json:"longitude"`
}
+type CRConfigLatitudeLongitudeShort struct {
+ Lat float64 `json:"lat"`
+ Lon float64 `json:"long"`
+}
+
type CRConfigDeliveryServiceProtocol struct {
- AcceptHTTP bool `json:"acceptHttp,string,omitempty"`
- AcceptHTTPS bool `json:"acceptHttps,string,omitempty"`
- RedirectOnHTTPS bool `json:"redirectOnHttps,string,omitempty"`
+ AcceptHTTP *bool `json:"acceptHttp,string,omitempty"`
+ AcceptHTTPS bool `json:"acceptHttps,string"`
+ RedirectOnHTTPS bool `json:"redirectToHttps,string"`
}
type CRConfigMonitor struct {
FQDN *string `json:"fqdn,omitempty"`
- HTTPSPort *int `json:"httpsPort,omitempty"`
+ HTTPSPort *int `json:"httpsPort"`
IP *string `json:"ip,omitempty"`
IP6 *string `json:"ip6,omitempty"`
Location *string `json:"location,omitempty"`
diff --git a/lib/go-tc/servers.go b/lib/go-tc/servers.go
index 7d2e5d5a3..6d9a7a0d6 100644
--- a/lib/go-tc/servers.go
+++ b/lib/go-tc/servers.go
@@ -86,8 +86,8 @@ type ServerNullable struct {
CDNName *string `json:"cdnName" db:"cdn_name"`
DeliveryServices *map[string][]string
`json:"deliveryServices,omitempty"`
DomainName *string `json:"domainName"
db:"domain_name"`
- FQDN *string `json:"fqdn,omitempty"`
- FqdnTime time.Time `json:"-"`
+ FQDN *string `json:"fqdn,omitempty"`
+ FqdnTime time.Time `json:"-"`
GUID *string `json:"guid" db:"guid"`
HostName *string `json:"hostName" db:"host_name"`
HTTPSPort *int `json:"httpsPort" db:"https_port"`
@@ -121,7 +121,7 @@ type ServerNullable struct {
Status *string `json:"status" db:"status"`
StatusID *int `json:"statusId" db:"status_id"`
TCPPort *int `json:"tcpPort" db:"tcp_port"`
- Type string `json:"type" db:"server_type"`
+ Type string `json:"type" db:"server_type"`
TypeID *int `json:"typeId"
db:"server_type_id"`
UpdPending *bool `json:"updPending"
db:"upd_pending"`
XMPPID *string `json:"xmppId" db:"xmpp_id"`
diff --git a/lib/go-tc/traffic_router.go b/lib/go-tc/traffic_router.go
index 8439fc8ba..f636f7a19 100644
--- a/lib/go-tc/traffic_router.go
+++ b/lib/go-tc/traffic_router.go
@@ -24,16 +24,16 @@ import (
)
type SOA struct {
- Admin *string `json:"admin,omitempty"`
- AdminTime time.Time
- ExpireSeconds *string `json:"expire,omitempty"`
- ExpireSecondsTime time.Time
- MinimumSeconds *string `json:"minimum,omitempty"`
- MinimumSecondsTime time.Time
- RefreshSeconds *string `json:"refresh,omitempty"`
- RefreshSecondsTime time.Time
- RetrySeconds *string `json:"retry,omitempty"`
- RetrySecondsTime time.Time
+ Admin *string `json:"admin,omitempty"`
+ AdminTime time.Time `json:"-"`
+ ExpireSeconds *string `json:"expire,omitempty"`
+ ExpireSecondsTime time.Time `json:"-"`
+ MinimumSeconds *string `json:"minimum,omitempty"`
+ MinimumSecondsTime time.Time `json:"-"`
+ RefreshSeconds *string `json:"refresh,omitempty"`
+ RefreshSecondsTime time.Time `json:"-"`
+ RetrySeconds *string `json:"retry,omitempty"`
+ RetrySecondsTime time.Time `json:"-"`
}
// MissLocation ...
@@ -45,13 +45,13 @@ type MissLocation struct {
// MatchSet ...
type MatchSet struct {
Protocol string `json:"protocol"`
- MatchList []MatchList `json:"matchList"`
+ MatchList []MatchList `json:"matchlist"`
}
// MatchList ...
type MatchList struct {
Regex string `json:"regex"`
- MatchType string `json:"matchType"`
+ MatchType string `json:"match-type"`
}
// BypassDestination ...
diff --git a/traffic_ops/app/lib/API/Topology.pm
b/traffic_ops/app/lib/API/Topology.pm
deleted file mode 100644
index ff72b57dd..000000000
--- a/traffic_ops/app/lib/API/Topology.pm
+++ /dev/null
@@ -1,103 +0,0 @@
-package API::Topology;
-#
-##
-## Licensed under the Apache License, Version 2.0 (the "License");
-## you may not use this file except in compliance with the License.
-## You may obtain a copy of the License at
-##
-## http://www.apache.org/licenses/LICENSE-2.0
-##
-## Unless required by applicable law or agreed to in writing, software
-## distributed under the License is distributed on an "AS IS" BASIS,
-## WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-## See the License for the specific language governing permissions and
-## limitations under the License.
-##
-##
-##
-#
-## JvD Note: you always want to put Utils as the first use. Sh*t don't work if
it's after the Mojo lines.
-#
-
-
-use Mojo::Base 'Mojolicious::Controller';
-use JSON;
-use MojoPlugins::Response;
-use UI::Utils;
-use UI::Topology;
-use Data::Dumper;
-
-sub SnapshotCRConfig {
- my $self = shift;
- my $cdn_id = $self->param('id');
- my $cdn_name = $self->param('cdn_name');
- my $cdn;
-
- if ( !&is_oper($self) ) {
- return $self->forbidden("You must be an ADMIN or OPER to perform this
operation!");
- }
-
- if ( defined $cdn_id ) {
- $cdn = $self->db->resultset("Cdn")->find( { id => $cdn_id } );
- $cdn_name = $cdn->name if defined $cdn;
- }
-
- if ( !defined $cdn ) {
- $cdn = $self->db->resultset('Cdn')->find( { name => $cdn_name } );
- if ( !defined($cdn) ) {
- return $self->not_found();
- }
- }
-
- my @cdn_names = $self->db->resultset('Server')->search({ 'type.name' =>
'EDGE' }, { prefetch => [ 'cdn', 'type' ], group_by => 'cdn.name' }
)->get_column('cdn.name')->all();
- my $num = grep /^$cdn_name$/, @cdn_names;
- if ($num <= 0) {
- return $self->alert("CDN_name [" . $cdn_name. "] is not found in edge
server cdn");
- }
-
- my $json = &UI::Topology::gen_crconfig_json($self, $cdn_name);
- &UI::Topology::write_crconfig_json_to_db($self, $cdn_name, $json);
- &UI::Utils::log($self, "Snapshot of CRConfig performed for $cdn_name",
"APICHANGE");
- return $self->success("SUCCESS");
-}
-
-sub get_snapshot {
- my $self = shift;
- my $cdn_name = $self->param('name');
-
- if ( !&is_oper($self) ) {
- return $self->forbidden();
- }
-
- my $cdn = $self->db->resultset('Cdn')->find( { name => $cdn_name } );
- if ( !defined($cdn) ) {
- return $self->not_found();
- }
-
- my $snapshot = $self->db->resultset('Snapshot')->search( { cdn =>
$cdn_name } )->get_column('content')->single();
- if ( !defined($snapshot) ) {
- return $self->success( {} );
- }
-
- $self->success( decode_json($snapshot) );
-}
-
-sub get_new_snapshot {
- my $self = shift;
- my $cdn_name = $self->param('name');
-
- if ( !&is_oper($self) ) {
- return $self->forbidden();
- }
-
- my $cdn = $self->db->resultset('Cdn')->find( { name => $cdn_name } );
- if ( !defined($cdn) ) {
- return $self->not_found();
- }
-
- my $json = &UI::Topology::gen_crconfig_json($self, $cdn_name);
-
- $self->success( $json );
-}
-
-1;
diff --git a/traffic_ops/app/lib/TrafficOpsRoutes.pm
b/traffic_ops/app/lib/TrafficOpsRoutes.pm
index 78b3e05a9..81bf55bce 100644
--- a/traffic_ops/app/lib/TrafficOpsRoutes.pm
+++ b/traffic_ops/app/lib/TrafficOpsRoutes.pm
@@ -338,7 +338,8 @@ sub ui_routes {
$r->get('/tools/queue_updates')->over( authenticated => 1, not_ldap =>
1 )->to( 'Tools#queue_updates', namespace => $namespace );
$r->get('/tools/snapshot_crconfig')->over( authenticated => 1, not_ldap
=> 1 )->to( 'Tools#snapshot_crconfig', namespace => $namespace );
$r->get('/tools/diff_crconfig/:cdn_name')->over( authenticated => 1,
not_ldap => 1 )->to( 'Tools#diff_crconfig_iframe', namespace => $namespace );
- $r->get('/tools/write_crconfig/:cdn_name')->over( authenticated => 1,
not_ldap => 1 )->to( 'Tools#write_crconfig', namespace => $namespace );
+ # flash_and_close is a helper for the traffic_ops_golang migration, to
allow Go handlers to intercept GUI routes, do their work, then redirect to this
to perform the GUI operation
+ $r->get('/tools/flash_and_close/:msg')->over( authenticated => 1,
not_ldap => 1 )->to( 'Tools#flash_and_close', namespace => $namespace );
$r->get('/tools/invalidate_content/')->over( authenticated => 1,
not_ldap => 1 )->to( 'Tools#invalidate_content', namespace => $namespace );
# -- Topology - CCR Config, rewrote in json
@@ -448,13 +449,6 @@ sub api_routes {
# -- CDNS: ROUTING
$r->get("/api/$version/cdns/routing")->over( authenticated => 1,
not_ldap => 1 )->to( 'Cdn#routing', namespace => $namespace );
- # -- CDNS: SNAPSHOT
- $r->get("/api/$version/cdns/:name/snapshot")->over( authenticated => 1,
not_ldap => 1 )->to( 'Topology#get_snapshot', namespace => $namespace );
- $r->get("/api/$version/cdns/:name/snapshot/new")->over( authenticated
=> 1, not_ldap => 1 )->to( 'Topology#get_new_snapshot', namespace => $namespace
);
- $r->put( "/api/$version/cdns/:id/snapshot" => [ id => qr/\d+/ ]
)->over( authenticated => 1, not_ldap => 1 )
- ->to( 'Topology#SnapshotCRConfig', namespace => $namespace );
- $r->put("/api/$version/snapshot/:cdn_name")->over( authenticated => 1,
not_ldap => 1 )->to( 'Topology#SnapshotCRConfig', namespace => $namespace );
-
# -- CDNS: METRICS
#WARNING: this is an intentionally "unauthenticated" route.
$r->get("/api/$version/cdns/metric_types/:metric_type/start_date/:start_date/end_date/:end_date")->to(
'Cdn#metrics', namespace => $namespace );
diff --git a/traffic_ops/app/lib/UI/Tools.pm b/traffic_ops/app/lib/UI/Tools.pm
index 3ab70c18b..639c9ffe0 100644
--- a/traffic_ops/app/lib/UI/Tools.pm
+++ b/traffic_ops/app/lib/UI/Tools.pm
@@ -105,23 +105,11 @@ sub diff_crconfig_iframe {
);
}
-sub write_crconfig {
- my $self = shift;
- my $cdn_name = $self->param('cdn_name');
- my ( $json, $error ) = UI::Topology::gen_crconfig_json( $self, $cdn_name );
- if ( defined $error ) {
- $self->flash( alertmsg => $error );
- }
- else {
- if ( !&is_oper($self) ) {
- $self->flash( alertmsg => "No can do. Get more privs." );
- } else {
- UI::Topology::write_crconfig_json_to_db( $self, $cdn_name, $json );
- &log( $self, "Snapshot CRConfig created.", "OPER" );
- $self->flash( alertmsg => "Successfully wrote CRConfig.json!" );
- }
- }
- return $self->redirect_to('/utils/close_fancybox');
+sub flash_and_close {
+ my $self = shift;
+ my $msg = $self->param('msg');
+ $self->flash( alertmsg => $msg );
+ return $self->redirect_to('/utils/close_fancybox');
}
sub queue_updates {
diff --git a/traffic_ops/app/lib/UI/Topology.pm
b/traffic_ops/app/lib/UI/Topology.pm
index ee0e750f7..aef8cadd0 100644
--- a/traffic_ops/app/lib/UI/Topology.pm
+++ b/traffic_ops/app/lib/UI/Topology.pm
@@ -573,22 +573,6 @@ sub gen_crconfig_json {
return ($data_obj);
}
-sub write_crconfig_json_to_db {
- my $self = shift;
- my $cdn_name = shift;
- my $crconfig_db = shift;
- my $crconfig_json = encode_json($crconfig_db);
-
- my $snapshot = $self->db->resultset('Snapshot')->find( { cdn => $cdn_name
} );
- if ( defined($snapshot) ) {
- $snapshot->update({ content => $crconfig_json });
- } else {
- my $insert = $self->db->resultset('Snapshot')->create( { cdn =>
$cdn_name, content => $crconfig_json } );
- $insert->insert();
- }
-
-}
-
sub diff_crconfig_json {
my $self = shift;
my $json = shift;
diff --git a/traffic_ops/traffic_ops_golang/config/config.go
b/traffic_ops/traffic_ops_golang/config/config.go
index 1a5ebdd0c..f6d0f10ab 100644
--- a/traffic_ops/traffic_ops_golang/config/config.go
+++ b/traffic_ops/traffic_ops_golang/config/config.go
@@ -42,6 +42,7 @@ type Config struct {
// NOTE: don't care about any other fields for now..
RiakAuthOptions *riak.AuthOptions
RiakEnabled bool
+ Version string
}
// ConfigHypnotoad carries http setting for hypnotoad (mojolicious) server
@@ -107,14 +108,14 @@ func (c Config) EventLog() log.LogLocation {
}
// LoadConfig - reads the config file into the Config struct
-func LoadConfig(cdnConfPath string, dbConfPath string, riakConfPath string)
(Config, error) {
+func LoadConfig(cdnConfPath string, dbConfPath string, riakConfPath string,
appVersion string) (Config, error) {
// load json from cdn.conf
confBytes, err := ioutil.ReadFile(cdnConfPath)
if err != nil {
return Config{}, fmt.Errorf("reading CDN conf '%s': %v",
cdnConfPath, err)
}
- var cfg Config
+ cfg := Config{Version: appVersion}
err = json.Unmarshal(confBytes, &cfg)
if err != nil {
return Config{}, fmt.Errorf("unmarshalling '%s': %v",
cdnConfPath, err)
diff --git a/traffic_ops/traffic_ops_golang/config/config_test.go
b/traffic_ops/traffic_ops_golang/config/config_test.go
index 7e7ef8fef..1bd73d6c3 100644
--- a/traffic_ops/traffic_ops_golang/config/config_test.go
+++ b/traffic_ops/traffic_ops_golang/config/config_test.go
@@ -167,6 +167,7 @@ const (
func TestLoadConfig(t *testing.T) {
var err error
var exp string
+ version := "Test Version"
// set up config paths
badPath := "/invalid-path/no-file-exists-here"
@@ -195,35 +196,35 @@ func TestLoadConfig(t *testing.T) {
defer os.Remove(goodRiakCfg) // clean up
// test bad paths
- _, err = LoadConfig(badPath, badPath, badPath)
+ _, err = LoadConfig(badPath, badPath, badPath, version)
exp = fmt.Sprintf("reading CDN conf '%s'", badPath)
if !strings.HasPrefix(err.Error(), exp) {
t.Error("expected", exp, "got", err)
}
// bad json in cdn.conf
- _, err = LoadConfig(badCfg, badCfg, badPath)
+ _, err = LoadConfig(badCfg, badCfg, badPath, version)
exp = fmt.Sprintf("unmarshalling '%s'", badCfg)
if !strings.HasPrefix(err.Error(), exp) {
t.Error("expected", exp, "got", err)
}
// good cdn.conf, bad db conf
- _, err = LoadConfig(goodCfg, badPath, badPath)
+ _, err = LoadConfig(goodCfg, badPath, badPath, version)
exp = fmt.Sprintf("reading db conf '%s'", badPath)
if !strings.HasPrefix(err.Error(), exp) {
t.Error("expected", exp, "got", err)
}
// good cdn.conf, bad json in database.conf
- _, err = LoadConfig(goodCfg, badCfg, badPath)
+ _, err = LoadConfig(goodCfg, badCfg, badPath, version)
exp = fmt.Sprintf("unmarshalling '%s'", badCfg)
if !strings.HasPrefix(err.Error(), exp) {
t.Error("expected", exp, "got", err)
}
// good cdn.conf, good database.conf
- cfg, err = LoadConfig(goodCfg, goodDbCfg, goodRiakCfg)
+ cfg, err = LoadConfig(goodCfg, goodDbCfg, goodRiakCfg, version)
if err != nil {
t.Error("Good config -- unexpected error ", err)
}
diff --git a/traffic_ops/traffic_ops_golang/crconfig/config.go
b/traffic_ops/traffic_ops_golang/crconfig/config.go
new file mode 100644
index 000000000..ffe3126a4
--- /dev/null
+++ b/traffic_ops/traffic_ops_golang/crconfig/config.go
@@ -0,0 +1,94 @@
+package crconfig
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import (
+ "database/sql"
+ "errors"
+ "strings"
+)
+
+func makeCRConfigConfig(cdn string, db *sql.DB, dnssecEnabled bool)
(map[string]interface{}, error) {
+ configParams, err := getConfigParams(cdn, db)
+ if err != nil {
+ return nil, errors.New("Error getting router params: " +
err.Error())
+ }
+ soa := map[string]string{}
+ ttl := map[string]string{}
+ const soaPrefix = "tld.soa."
+ ttlPrefix := "tld.ttls."
+ crConfigConfig := map[string]interface{}{}
+ for k, v := range configParams {
+ if strings.HasPrefix(k, soaPrefix) {
+ soa[k[len(soaPrefix):]] = v
+ } else if strings.HasPrefix(k, ttlPrefix) {
+ ttl[k[len(ttlPrefix):]] = v
+ } else {
+ crConfigConfig[k] = v
+ }
+ }
+ if len(soa) > 0 {
+ crConfigConfig["soa"] = soa
+ }
+ if len(ttl) > 0 {
+ crConfigConfig["ttls"] = ttl
+ }
+
+ dnssecStr := "false"
+ if dnssecEnabled {
+ dnssecStr = "true"
+ }
+ crConfigConfig["dnssec.enabled"] = dnssecStr
+
+ return crConfigConfig, nil
+}
+
+func getConfigParams(cdn string, db *sql.DB) (map[string]string, error) {
+ // TODO change to []struct{string,string} ? Speed might matter.
+ q := `
+select name, value from parameter where id in (
+ select parameter from profile_parameter where profile in (
+ select distinct profile from server where cdn_id = (
+ select id from cdn where name = $1
+ )
+ )
+)
+and config_file = 'CRConfig.json'
+`
+ rows, err := db.Query(q, cdn)
+ if err != nil {
+ return nil, errors.New("Error querying router params: " +
err.Error())
+ }
+ defer rows.Close()
+
+ params := map[string]string{}
+ for rows.Next() {
+ name := ""
+ val := ""
+ if err := rows.Scan(&name, &val); err != nil {
+ return nil, errors.New("Error scanning router param: "
+ err.Error())
+ }
+ params[name] = val
+ }
+ if err := rows.Err(); err != nil {
+ return nil, errors.New("Error iterating router param rows: " +
err.Error())
+ }
+ return params, nil
+}
diff --git a/traffic_ops/traffic_ops_golang/crconfig/config_test.go
b/traffic_ops/traffic_ops_golang/crconfig/config_test.go
new file mode 100644
index 000000000..86f209e8a
--- /dev/null
+++ b/traffic_ops/traffic_ops_golang/crconfig/config_test.go
@@ -0,0 +1,117 @@
+package crconfig
+
+/*
+ * 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 (
+ "reflect"
+ "strings"
+ "testing"
+
+ "gopkg.in/DATA-DOG/go-sqlmock.v1"
+)
+
+func ExpectedGetConfigParams() map[string]string {
+ return map[string]string{
+ "tld.ttls.foo" + *randStr(): *randStr(),
+ "tld.soa.bar" + *randStr(): *randStr(),
+ }
+}
+
+func MockGetConfigParams(mock sqlmock.Sqlmock, expected map[string]string, cdn
string) {
+ rows := sqlmock.NewRows([]string{"name", "value"})
+ for n, v := range expected {
+ rows = rows.AddRow(n, v)
+ }
+ mock.ExpectQuery("select").WithArgs(cdn).WillReturnRows(rows)
+}
+
+func TestGetConfigParams(t *testing.T) {
+ db, mock, err := sqlmock.New()
+ if err != nil {
+ t.Fatalf("an error '%s' was not expected when opening a stub
database connection", err)
+ }
+ defer db.Close()
+
+ cdn := "mycdn"
+
+ expected := ExpectedGetConfigParams()
+ MockGetConfigParams(mock, expected, cdn)
+
+ actual, err := getConfigParams(cdn, db)
+ if err != nil {
+ t.Fatalf("getConfigParams err expected: nil, actual: %v", err)
+ }
+
+ if !reflect.DeepEqual(expected, actual) {
+ t.Errorf("getConfigParams expected: %+v, actual: %+v",
expected, actual)
+ }
+}
+
+const soaPrefix = "tld.soa."
+const ttlPrefix = "tld.ttls."
+
+func ExpectedMakeCRConfigConfig(expectedGetConfigParams map[string]string,
expectedDNSSECEnabled bool) map[string]interface{} {
+ m := map[string]interface{}{}
+ soa := map[string]string{}
+ ttl := map[string]string{}
+ for n, v := range expectedGetConfigParams {
+ if strings.HasPrefix(n, soaPrefix) {
+ soa[n[len(soaPrefix):]] = v
+ } else if strings.HasPrefix(n, ttlPrefix) {
+ ttl[n[len(ttlPrefix):]] = v
+ } else {
+ m[n] = v
+ }
+ }
+ m["soa"] = soa
+ m["ttls"] = ttl
+ if expectedDNSSECEnabled {
+ m["dnssec.enabled"] = "true"
+ } else {
+ m["dnssec.enabled"] = "false"
+ }
+ return m
+}
+
+func TestMakeCRConfigConfig(t *testing.T) {
+ db, mock, err := sqlmock.New()
+ if err != nil {
+ t.Fatalf("an error '%s' was not expected when opening a stub
database connection", err)
+ }
+ defer db.Close()
+
+ cdn := "mycdn"
+ dnssecEnabled := true
+
+ expectedGetConfigParams := ExpectedGetConfigParams()
+ MockGetConfigParams(mock, expectedGetConfigParams, cdn)
+
+ expected := ExpectedMakeCRConfigConfig(expectedGetConfigParams,
dnssecEnabled)
+
+ actual, err := makeCRConfigConfig(cdn, db, dnssecEnabled)
+
+ if err != nil {
+ t.Fatalf("makeCRConfigConfig err expected: nil, actual: %v",
err)
+ }
+
+ if !reflect.DeepEqual(expected, actual) {
+ t.Errorf("makeCRConfigConfig expected: %+v, actual: %+v",
expected, actual)
+ }
+}
diff --git a/traffic_ops/traffic_ops_golang/crconfig/crconfig.go
b/traffic_ops/traffic_ops_golang/crconfig/crconfig.go
new file mode 100644
index 000000000..44c5ea9cb
--- /dev/null
+++ b/traffic_ops/traffic_ops_golang/crconfig/crconfig.go
@@ -0,0 +1,56 @@
+package crconfig
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import (
+ "database/sql"
+ "errors"
+
+ "github.com/apache/incubator-trafficcontrol/lib/go-tc"
+)
+
+func Make(db *sql.DB, cdn, user, toHost, reqPath, toVersion string)
(*tc.CRConfig, error) {
+ crc := tc.CRConfig{}
+ err := error(nil)
+
+ cdnDomain, dnssecEnabled, err := getCDNInfo(cdn, db)
+ if err != nil {
+ return nil, errors.New("Error getting CDN info: " + err.Error())
+ }
+
+ if crc.Config, err = makeCRConfigConfig(cdn, db, dnssecEnabled); err !=
nil {
+ return nil, errors.New("Error getting Config: " + err.Error())
+ }
+
+ if crc.ContentServers, crc.ContentRouters, crc.Monitors, err =
makeCRConfigServers(cdn, db, cdnDomain); err != nil {
+ return nil, errors.New("Error getting Servers: " + err.Error())
+ }
+ if crc.EdgeLocations, crc.RouterLocations, err = makeLocations(cdn,
db); err != nil {
+ return nil, errors.New("Error getting Edge Locations: " +
err.Error())
+ }
+ if crc.DeliveryServices, err = makeDSes(cdn, db); err != nil {
+ return nil, errors.New("Error getting Delivery Services: " +
err.Error())
+ }
+
+ // TODO change to real reqPath, and verify everything works. Currently
emulates the existing TO, in case anything relies on it
+ emulateOldPath := "/tools/write_crconfig/" + cdn
+ crc.Stats = makeStats(cdn, user, toHost, emulateOldPath, toVersion, db)
+ return &crc, nil
+}
diff --git a/traffic_ops/traffic_ops_golang/crconfig/deliveryservice.go
b/traffic_ops/traffic_ops_golang/crconfig/deliveryservice.go
new file mode 100644
index 000000000..0bc15ddc6
--- /dev/null
+++ b/traffic_ops/traffic_ops_golang/crconfig/deliveryservice.go
@@ -0,0 +1,515 @@
+package crconfig
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import (
+ "database/sql"
+ "errors"
+ "strconv"
+ "strings"
+ "time"
+
+ "github.com/apache/incubator-trafficcontrol/lib/go-log"
+ "github.com/apache/incubator-trafficcontrol/lib/go-tc"
+)
+
+const CDNSOAMinimum = 30 * time.Second
+const CDNSOAExpire = 604800 * time.Second
+const CDNSOARetry = 7200 * time.Second
+const CDNSOARefresh = 28800 * time.Second
+const CDNSOAAdmin = "traffic_ops"
+const DefaultTLDTTLSOA = 86400 * time.Second
+const DefaultTLDTTLNS = 3600 * time.Second
+
+const GeoProviderMaxmindStr = "maxmindGeolocationService"
+const GeoProviderNeustarStr = "neustarGeolocationService"
+
+func makeDSes(cdn string, db *sql.DB) (map[string]tc.CRConfigDeliveryService,
error) {
+ dses := map[string]tc.CRConfigDeliveryService{}
+
+ admin := CDNSOAAdmin
+ expireSecondsStr := strconv.Itoa(int(CDNSOAExpire / time.Second))
+ minimumSecondsStr := strconv.Itoa(int(CDNSOAMinimum / time.Second))
+ refreshSecondsStr := strconv.Itoa(int(CDNSOARefresh / time.Second))
+ retrySecondsStr := strconv.Itoa(int(CDNSOARetry / time.Second))
+ cdnSOA := &tc.SOA{
+ Admin: &admin,
+ ExpireSeconds: &expireSecondsStr,
+ MinimumSeconds: &minimumSecondsStr,
+ RefreshSeconds: &refreshSecondsStr,
+ RetrySeconds: &retrySecondsStr,
+ }
+
+ // Note the CRConfig omits acceptHTTP if it's true
+ falsePtr := false
+ protocol0 := &tc.CRConfigDeliveryServiceProtocol{AcceptHTTPS: false,
RedirectOnHTTPS: false}
+ protocol1 := &tc.CRConfigDeliveryServiceProtocol{AcceptHTTP: &falsePtr,
AcceptHTTPS: true, RedirectOnHTTPS: false}
+ protocol2 := &tc.CRConfigDeliveryServiceProtocol{AcceptHTTPS: true,
RedirectOnHTTPS: false}
+ protocol3 := &tc.CRConfigDeliveryServiceProtocol{AcceptHTTPS: true,
RedirectOnHTTPS: true}
+ protocolDefault := protocol0
+
+ geoProvider0 := GeoProviderMaxmindStr
+ geoProvider1 := GeoProviderNeustarStr
+ geoProviderDefault := geoProvider0
+
+ q := `
+select d.xml_id, d.miss_lat, d.miss_long, d.protocol, d.ccr_dns_ttl as ttl,
d.routing_name, d.geo_provider, t.name as type, d.geo_limit,
d.geo_limit_countries, d.geolimit_redirect_url, d.initial_dispersion,
d.regional_geo_blocking, d.tr_response_headers, d.max_dns_answers, p.name as
profile, d.dns_bypass_ip, d.dns_bypass_ip6, d.dns_bypass_ttl,
d.dns_bypass_cname, d.http_bypass_fqdn, d.ipv6_routing_enabled,
d.deep_caching_type, d.tr_request_headers, d.tr_response_headers
+from deliveryservice as d
+inner join type as t on t.id = d.type
+left outer join profile as p on p.id = d.profile
+where d.cdn_id = (select id from cdn where name = $1)
+and d.active = true
+`
+ rows, err := db.Query(q, cdn)
+ if err != nil {
+ return nil, errors.New("querying deliveryservices: " +
err.Error())
+ }
+ defer rows.Close()
+
+ serverParams, err := getServerProfileParams(cdn, db)
+ if err != nil {
+ return nil, errors.New("getting deliveryservice parameters: " +
err.Error())
+ }
+
+ dsParams, err := getDSParams(serverParams)
+ if err != nil {
+ return nil, errors.New("getting deliveryservice server
parameters: " + err.Error())
+ }
+
+ dsmatchsets, dsdomains, err := getDSRegexesDomains(cdn, db, dsParams)
+ if err != nil {
+ return nil, errors.New("getting regex matchsets: " +
err.Error())
+ }
+
+ staticDNSEntries, err := getStaticDNSEntries(cdn, db)
+ if err != nil {
+ return nil, errors.New("getting static DNS entries: " +
err.Error())
+ }
+
+ for rows.Next() {
+ ds := tc.CRConfigDeliveryService{
+ MissLocation: &tc.CRConfigLatitudeLongitudeShort{},
+ Protocol: &tc.CRConfigDeliveryServiceProtocol{},
+ ResponseHeaders: map[string]string{},
+ Soa: cdnSOA,
+ TTLs: &tc.CRConfigTTL{},
+ }
+
+ missLat := sql.NullFloat64{}
+ missLon := sql.NullFloat64{}
+ protocol := sql.NullInt64{}
+ ttl := sql.NullInt64{}
+ geoProvider := sql.NullInt64{}
+ ttype := ""
+ geoLimit := sql.NullInt64{}
+ geoLimitCountries := sql.NullString{}
+ geoLimitRedirectURL := sql.NullString{}
+ dispersion := sql.NullInt64{}
+ geoBlocking := false
+ trRespHdrsStr := sql.NullString{}
+ xmlID := ""
+ maxDNSAnswers := sql.NullInt64{}
+ profile := sql.NullString{}
+ dnsBypassIP := sql.NullString{}
+ dnsBypassIP6 := sql.NullString{}
+ dnsBypassTTL := sql.NullInt64{}
+ dnsBypassCName := sql.NullString{}
+ httpBypassFQDN := sql.NullString{}
+ ip6RoutingEnabled := sql.NullBool{}
+ deepCachingType := sql.NullString{}
+ trRequestHeaders := sql.NullString{}
+ trResponseHeaders := sql.NullString{}
+ if err := rows.Scan(&xmlID, &missLat, &missLon, &protocol,
&ds.TTL, &ds.RoutingName, &geoProvider, &ttype, &geoLimit, &geoLimitCountries,
&geoLimitRedirectURL, &dispersion, &geoBlocking, &trRespHdrsStr,
&maxDNSAnswers, &profile, &dnsBypassIP, &dnsBypassIP6, &dnsBypassTTL,
&dnsBypassCName, &httpBypassFQDN, &ip6RoutingEnabled, &deepCachingType,
&trRequestHeaders, &trResponseHeaders); err != nil {
+ return nil, errors.New("scanning deliveryservice: " +
err.Error())
+ }
+
+ if missLat.Valid {
+ ds.MissLocation.Lat = missLat.Float64
+ }
+ if missLon.Valid {
+ ds.MissLocation.Lon = missLon.Float64
+ }
+ if ttl.Valid {
+ ttl := int(ttl.Int64)
+ ds.TTL = &ttl
+ }
+
+ protocolStr := getProtocolStr(ttype)
+
+ ds.Protocol = protocolDefault
+ if protocol.Valid {
+ switch protocol.Int64 {
+ case 0:
+ ds.Protocol = protocol0
+ case 1:
+ ds.Protocol = protocol1
+ case 2:
+ ds.Protocol = protocol2
+ case 3:
+ ds.Protocol = protocol3
+ }
+ }
+
+ ds.GeoLocationProvider = &geoProviderDefault
+ if geoProvider.Valid {
+ switch geoProvider.Int64 {
+ case 0:
+ ds.GeoLocationProvider = &geoProvider0
+ case 1:
+ ds.GeoLocationProvider = &geoProvider1
+ }
+ }
+
+ if ds.Protocol.AcceptHTTPS {
+ ds.SSLEnabled = true
+ }
+
+ if deepCachingType.Valid {
+ // TODO change to omit Valid check, default to the
default DeepCachingType (NEVER). I'm pretty sure that's what should happen, but
the Valid check emulates the old Perl CRConfig generation
+ t :=
tc.DeepCachingTypeFromString(deepCachingType.String)
+ ds.DeepCachingType = &t
+ }
+
+ ds.GeoLocationProvider = &geoProviderDefault
+
+ if matchsets, ok := dsmatchsets[xmlID]; ok {
+ ds.MatchSets = matchsets
+ } else {
+ log.Warnln("no regex matchsets for delivery service: "
+ xmlID)
+ }
+ if domains, ok := dsdomains[xmlID]; ok {
+ ds.Domains = domains
+ } else {
+ log.Warnln("no host regex for delivery service: " +
xmlID)
+ }
+
+ switch geoLimit.Int64 { // No Valid check - default false and
set countries, if null
+ case 0:
+ ds.CoverageZoneOnly = false
+ case 1:
+ ds.CoverageZoneOnly = true
+ if protocolStr == "HTTP" {
+ ds.GeoLimitRedirectURL =
&geoLimitRedirectURL.String // No Valid check - empty string, if null
+ }
+ default:
+ ds.CoverageZoneOnly = false
+ if protocolStr == "HTTP" {
+ ds.GeoLimitRedirectURL =
&geoLimitRedirectURL.String // No Valid check - empty string, if null
+ }
+ if geoLimitCountries.Valid {
+ for _, code := range
strings.Split(geoLimitCountries.String, ",") {
+ ds.GeoEnabled = append(ds.GeoEnabled,
tc.CRConfigGeoEnabled{CountryCode: strings.TrimSpace(code)})
+ }
+ }
+ }
+
+ nsSeconds := DefaultTLDTTLNS
+ soaSeconds := DefaultTLDTTLSOA
+ if profile.Valid {
+ if sval, ok := dsParams["tld.ttls.SOA"]; ok {
+ if val, err := strconv.Atoi(sval); err == nil {
+ soaSeconds = time.Duration(val) *
time.Second
+ } else {
+ log.Errorln("delivery service " + xmlID
+ " profile " + profile.String + " param tld.ttls.SOA '" + sval + "' not a
number - skipping")
+ }
+ }
+ if sval, ok := dsParams["tld.ttls.NS"]; ok {
+ if val, err := strconv.Atoi(sval); err == nil {
+ nsSeconds = time.Duration(val) *
time.Second
+ } else {
+ log.Errorln("delivery service " + xmlID
+ " profile " + profile.String + " param tld.ttls.NS '" + sval + "' not a
number - skipping")
+ }
+ }
+ }
+ nsSecondsStr := strconv.Itoa(int(nsSeconds / time.Second))
+ soaSecondsStr := strconv.Itoa(int(soaSeconds / time.Second))
+ ttlStr := ""
+ if ds.TTL != nil {
+ ttlStr = strconv.Itoa(*ds.TTL)
+ }
+ ds.TTLs = &tc.CRConfigTTL{
+ ASeconds: &ttlStr,
+ AAAASeconds: &ttlStr,
+ NSSeconds: &nsSecondsStr,
+ SOASeconds: &soaSecondsStr,
+ }
+
+ if protocolStr == "DNS" {
+ bypassDest := &tc.CRConfigBypassDestination{}
+ if dnsBypassIP.String != "" {
+ bypassDest.IP = &dnsBypassIP.String
+ }
+ if dnsBypassIP6.String != "" {
+ bypassDest.IP6 = &dnsBypassIP6.String
+ }
+ if dnsBypassTTL.Valid {
+ i := int(dnsBypassTTL.Int64)
+ bypassDest.TTL = &i
+ }
+ if dnsBypassCName.Valid && dnsBypassCName.String != "" {
+ bypassDest.CName = &dnsBypassCName.String
+ }
+ if *bypassDest != (tc.CRConfigBypassDestination{}) {
+ if ds.BypassDestination == nil {
+ ds.BypassDestination =
map[string]*tc.CRConfigBypassDestination{}
+ }
+ ds.BypassDestination["DNS"] = bypassDest
+ }
+ if maxDNSAnswers.Valid {
+ i := int(maxDNSAnswers.Int64)
+ ds.MaxDNSIPsForLocation = &i
+ }
+ } else if protocolStr == "HTTP" {
+ if httpBypassFQDN.String != "" {
+ if ds.BypassDestination == nil {
+ ds.BypassDestination =
map[string]*tc.CRConfigBypassDestination{}
+ }
+ hostPort :=
strings.Split(httpBypassFQDN.String, ":")
+ bypass := &tc.CRConfigBypassDestination{FQDN:
&hostPort[0]}
+ if len(hostPort) > 1 {
+ bypass.Port = &hostPort[1]
+ }
+ ds.BypassDestination["HTTP"] = bypass
+ }
+ geoBlockingStr := "false"
+ if geoBlocking {
+ geoBlockingStr = "true"
+ }
+ ds.RegionalGeoBlocking = &geoBlockingStr
+ if dispersion.Valid {
+ ds.Dispersion = &tc.CRConfigDispersion{Limit:
int(dispersion.Int64), Shuffled: true}
+ }
+ }
+
+ ds.IP6RoutingEnabled = &ip6RoutingEnabled.Bool // No Valid
check, false if null
+
+ if trResponseHeaders.Valid && trResponseHeaders.String != "" {
+ hdrs := strings.Split(trResponseHeaders.String,
`__RETURN__`)
+ for _, hdr := range hdrs {
+ nameVal := strings.Split(hdr, `:`)
+ name := strings.TrimSpace(nameVal[0])
+ val := ""
+ if len(nameVal) > 1 {
+ val = strings.Trim(nameVal[1], " \n\"")
+ }
+ ds.ResponseHeaders[name] = val
+ }
+ }
+
+ if trRequestHeaders.Valid && trRequestHeaders.String != "" {
+ hdrs := strings.Split(trRequestHeaders.String,
`__RETURN__`)
+ for _, hdr := range hdrs {
+ nameVal := strings.Split(hdr, `:`)
+ name := strings.TrimSpace(nameVal[0])
+ ds.RequestHeaders = append(ds.RequestHeaders,
name)
+ }
+ }
+
+ ds.StaticDNSEntries =
staticDNSEntries[tc.DeliveryServiceName(xmlID)]
+
+ dses[xmlID] = ds
+ }
+
+ if err := rows.Err(); err != nil {
+ return nil, errors.New("iterating deliveryservice rows: " +
err.Error())
+ }
+
+ return dses, nil
+}
+
+func getStaticDNSEntries(cdn string, db *sql.DB)
(map[tc.DeliveryServiceName][]tc.StaticDNSEntry, error) {
+ entries := map[tc.DeliveryServiceName][]tc.StaticDNSEntry{}
+
+ q := `
+select d.xml_id as ds, e.host as name, e.ttl, e.address as value, t.name as
type
+from staticdnsentry as e
+inner join deliveryservice as d on d.id = e.deliveryservice
+inner join type as t on t.id = e.type
+where d.cdn_id = (select id from cdn where name = $1)
+and d.active = true
+`
+ rows, err := db.Query(q, cdn)
+ if err != nil {
+ return nil, errors.New("querying static DNS entries: " +
err.Error())
+ }
+ defer rows.Close()
+
+ for rows.Next() {
+ ds := ""
+ name := ""
+ ttl := 0
+ value := ""
+ ttype := ""
+ if err := rows.Scan(&ds, &name, &ttl, &value, &ttype); err !=
nil {
+ return nil, errors.New("scanning static DNS entries: "
+ err.Error())
+ }
+ ttype = strings.Replace(ttype, "_RECORD", "", -1)
+ entries[tc.DeliveryServiceName(ds)] =
append(entries[tc.DeliveryServiceName(ds)], tc.StaticDNSEntry{Name: name, TTL:
ttl, Value: value, Type: ttype})
+ }
+ return entries, nil
+}
+
+func getProtocolStr(dsType string) string {
+ if strings.HasPrefix(dsType, "DNS") {
+ return "DNS"
+ }
+ return "HTTP"
+}
+
+func getDSRegexesDomains(cdn string, db *sql.DB, dsParams map[string]string)
(map[string][]*tc.MatchSet, map[string][]string, error) {
+ dsmatchsets := map[string][]*tc.MatchSet{}
+ domains := map[string][]string{}
+
+ patternToHostReplacer := strings.NewReplacer(`\`, ``, `.*`, ``, `.`, ``)
+
+ q := `
+select r.pattern, t.name as type, dt.name as dstype, COALESCE(dr.set_number,
0), d.xml_id as dsname
+from regex as r
+inner join deliveryservice_regex as dr on r.id = dr.regex
+inner join deliveryservice as d on d.id = dr.deliveryservice
+inner join type as t on t.id = r.type
+inner join type as dt on dt.id = d.type
+where d.cdn_id = (select id from cdn where name = $1)
+and d.active = true
+order by dr.set_number asc
+`
+ rows, err := db.Query(q, cdn)
+ if err != nil {
+ return nil, nil, errors.New("querying deliveryservices: " +
err.Error())
+ }
+ defer rows.Close()
+
+ for rows.Next() {
+ pattern := ""
+ ttype := ""
+ dstype := ""
+ setnum := 0
+ dsname := ""
+ if err := rows.Scan(&pattern, &ttype, &dstype, &setnum,
&dsname); err != nil {
+ return nil, nil, errors.New("scanning deliveryservice
regexes: " + err.Error())
+ }
+
+ protocolStr := getProtocolStr(dstype)
+
+ for len(dsmatchsets[dsname]) <= setnum {
+ dsmatchsets[dsname] = append(dsmatchsets[dsname], nil)
// TODO change to not insert empties? Current behavior emulates old Perl
CRConfig
+ }
+
+ matchType := ""
+ switch ttype {
+ case "HOST_REGEXP":
+ matchType = "HOST"
+ case "PATH_REGEXP":
+ matchType = "PATH"
+ case "HEADER_REGEXP":
+ matchType = "HEADER"
+ default:
+ log.Infoln("unknown delivery service '" + dsname + "'
regex type: " + ttype + " - skipping") // info, not warn or err, because this
is normal for STEERING_REGEXP (and maybe others in the future)
+ continue
+ }
+
+ if dsmatchsets[dsname][setnum] == nil {
+ dsmatchsets[dsname][setnum] = &tc.MatchSet{}
+ }
+ matchset := dsmatchsets[dsname][setnum]
+ matchset.Protocol = protocolStr
+ matchset.MatchList = append(matchset.MatchList,
tc.MatchList{MatchType: matchType, Regex: pattern})
+
+ domain := ""
+ if val, ok := dsParams["domain_name"]; ok {
+ domain = val
+ }
+
+ if ttype == "HOST_REGEXP" && setnum == 0 {
+ domains[dsname] = append(domains[dsname],
patternToHostReplacer.Replace(pattern)+"."+domain)
+ }
+ }
+ return dsmatchsets, domains, nil
+}
+
+// getDSParams takes a map[serverProfile][paramName]paramVal and returns a
map[paramName]paramVal.
+// The returned map of parameter values is used for DS settings for the
current CDN.
+// If any profiles have conflicting parameters, an error is returned.
+func getDSParams(serverParams map[string]map[string]string)
(map[string]string, error) {
+ dsParamNames := map[string]struct{}{
+ "domain_name": struct{}{},
+ "tld.soa.admin": struct{}{},
+ "tld.soa.expire": struct{}{},
+ "tld.soa.minimum": struct{}{},
+ "tld.soa.refresh": struct{}{},
+ "tld.soa.retry": struct{}{},
+ "tld.ttls.SOA": struct{}{},
+ "tld.ttls.NS": struct{}{},
+ "LogRequestHeaders": struct{}{},
+ }
+ dsParams := map[string]string{}
+ dsParamsOriginalProfile := map[string]string{} // map[paramName]profile
- used exclusively for the error message
+ for profile, profileParams := range serverParams {
+ for paramName, _ := range dsParamNames {
+ paramVal, profileHasParam := profileParams[paramName]
+ if !profileHasParam {
+ continue
+ }
+ if dsParamVal, ok := dsParams[paramName]; ok &&
dsParamVal != paramVal {
+ return nil, errors.New("profiles " + profile +
" and " + dsParamsOriginalProfile[paramName] + " have conflicting values '" +
paramVal + "' and '" + dsParamVal + "'")
+ }
+ dsParams[paramName] = paramVal
+ dsParamsOriginalProfile[paramName] = profile
+ }
+ }
+ return dsParams, nil
+}
+
+// getDSProfileParams returns a map[dsname]map[paramname]paramvalue
+func getServerProfileParams(cdn string, db *sql.DB)
(map[string]map[string]string, error) {
+ q := `
+select parameter.name, parameter.value, profile.name as profile
+from profile
+inner join profile_parameter as pp on pp.profile = profile.id
+inner join parameter on parameter.id = pp.parameter
+where profile.id in (select profile from server where server.cdn_id = (select
id from cdn where name = $1))
+`
+ rows, err := db.Query(q, cdn)
+ if err != nil {
+ return nil, errors.New("querying deliveryservices: " +
err.Error())
+ }
+ defer rows.Close()
+
+ params := map[string]map[string]string{}
+ debugCount := 0
+ for rows.Next() {
+ debugCount++
+ name := ""
+ val := ""
+ profile := ""
+ if err := rows.Scan(&name, &val, &profile); err != nil {
+ return nil, errors.New("scanning deliveryservice
parameters: " + err.Error())
+ }
+ if _, ok := params[profile]; !ok {
+ params[profile] = map[string]string{}
+ }
+ params[profile][name] = val
+ }
+
+ if err := rows.Err(); err != nil {
+ return nil, errors.New("iterating deliveryservice parameter
rows: " + err.Error())
+ }
+ return params, nil
+}
diff --git a/traffic_ops/traffic_ops_golang/crconfig/deliveryservice_test.go
b/traffic_ops/traffic_ops_golang/crconfig/deliveryservice_test.go
new file mode 100644
index 000000000..c7d705774
--- /dev/null
+++ b/traffic_ops/traffic_ops_golang/crconfig/deliveryservice_test.go
@@ -0,0 +1,388 @@
+package crconfig
+
+/*
+ * 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 (
+ "strconv"
+ "strings"
+ "math/rand"
+ "reflect"
+ "testing"
+ "encoding/json"
+
+ "github.com/apache/incubator-trafficcontrol/lib/go-tc"
+
+ "gopkg.in/DATA-DOG/go-sqlmock.v1"
+)
+
+func randStrArr() []string {
+ num := int(rand.Int63n(10))
+ s := []string{}
+ for i := 0; i < num; i++ {
+ s = append(s, *randStr())
+ }
+ return s
+}
+
+func randMatchlistArr() []tc.MatchList {
+ num := int(rand.Int63n(10))
+ arr := []tc.MatchList{}
+ for i := 0; i < num; i++ {
+ arr = append(arr, tc.MatchList{
+ Regex: *randStr(),
+ MatchType: *randStr(),
+ })
+ }
+ return arr
+}
+
+func randMatchsetArr() []*tc.MatchSet {
+ num := int(rand.Int63n(10))
+ httpStr := "HTTP"
+ arr := []*tc.MatchSet{}
+ for i := 0; i < num; i++ {
+ arr = append(arr, &tc.MatchSet{
+ Protocol: httpStr,
+ MatchList: randMatchlistArr(),
+ })
+ }
+ return arr
+}
+
+func randDS() tc.CRConfigDeliveryService {
+ // truePtr := true
+ falseStrPtr := "false"
+ // numStr := "42"
+ ttlAdmin := "traffic_ops"
+ ttlExpire := "604800"
+ ttlMinimum := "30"
+ ttlRefresh := "28800"
+ ttlRetry := "7200"
+ ttl := randInt()
+ ttlStr := strconv.Itoa(*ttl)
+ ttlNS := "3600"
+ ttlSOA := "86400"
+ geoProviderStr := GeoProviderMaxmindStr
+ return tc.CRConfigDeliveryService{
+ CoverageZoneOnly: false,
+ Dispersion: &tc.CRConfigDispersion{
+ Limit: 42,
+ Shuffled: true,
+ },
+ // Domains: []string{"foo"},
+ GeoLocationProvider: &geoProviderStr,
+ // MatchSets: randMatchsetArr(),
+ MissLocation: &tc.CRConfigLatitudeLongitudeShort{
+ Lat: *randFloat64(),
+ Lon: *randFloat64(),
+ },
+ Protocol: &tc.CRConfigDeliveryServiceProtocol{
+ // AcceptHTTP: &truePtr,
+ AcceptHTTPS: false,
+ RedirectOnHTTPS: false,
+ },
+ RegionalGeoBlocking: &falseStrPtr,
+ ResponseHeaders: nil,
+ RequestHeaders: nil,
+ Soa: &tc.SOA{
+ Admin: &ttlAdmin,
+ ExpireSeconds: &ttlExpire,
+ MinimumSeconds: &ttlMinimum,
+ RefreshSeconds: &ttlRefresh,
+ RetrySeconds: &ttlRetry,
+ },
+ SSLEnabled: false,
+ TTL: ttl,
+ TTLs: &tc.CRConfigTTL {
+ ASeconds: &ttlStr,
+ AAAASeconds : &ttlStr,
+ NSSeconds: &ttlNS,
+ SOASeconds: &ttlSOA,
+ },
+ // MaxDNSIPsForLocation: randInt(),
+ IP6RoutingEnabled: randBool(),
+ RoutingName: randStr(),
+ BypassDestination: map[string]*tc.CRConfigBypassDestination {
+ "HTTP": &tc.CRConfigBypassDestination{
+ // IP: randStr(),
+ // IP6: randStr(),
+ // CName: randStr(),
+ // TTL: randInt(),
+ FQDN: randStr(),
+ // Port: randStr(),
+ },
+ },
+ DeepCachingType: nil,
+ GeoEnabled: nil,
+ // GeoLimitRedirectURL: randStr(),
+ StaticDNSEntries: []tc.StaticDNSEntry{
+ tc.StaticDNSEntry{
+ Name: *randStr(),
+ TTL: *randInt(),
+ Type: *randStr(),
+ Value: *randStr(),
+ },
+ },
+ }
+}
+
+func ExpectedMakeDSes() map[string]tc.CRConfigDeliveryService {
+ return map[string]tc.CRConfigDeliveryService{
+ "ds1": randDS(),
+ "ds2": randDS(),
+ }
+}
+
+func MockMakeDSes(mock sqlmock.Sqlmock, expected
map[string]tc.CRConfigDeliveryService, cdn string) {
+ // select d.xml_id, d.miss_lat, d.miss_long, d.protocol, d.ccr_dns_ttl
as ttl, d.routing_name, d.geo_provider, t.name as type, d.geo_limit,
d.geo_limit_countries, d.geolimit_redirect_url, d.initial_dispersion,
d.regional_geo_blocking, d.tr_response_headers, d.max_dns_answers, p.name as
profile, d.dns_bypass_ip, d.dns_bypass_ip6, d.dns_bypass_ttl,
d.dns_bypass_cname, d.http_bypass_fqdn, d.ipv6_routing_enabled,
d.deep_caching_type, d.tr_request_headers, d.tr_response_headers
+
+ rows := sqlmock.NewRows([]string{"xml_id", "miss_lat", "miss_long",
"protocol", "ttl", "routing_name", "geo_provider", "type", "geo_limit",
"geo_limit_countries", "geeo_limit_redirect_url", "initial_dispersion",
"regional_geo_blocking", "tr_response_headers", "max_dns_answers", "profile",
"dns_bypass_ip", "dns_bypass_ip6", "dns_bypass_ttl", "dns_bypass_cname",
"http_bypass_fqdn", "ipv6_routing_enabled", "deep_caching_type",
"tr_request_headers", "tr_response_headers"})
+
+ for dsName, ds := range expected {
+ rows = rows.AddRow(dsName, ds.MissLocation.Lat,
ds.MissLocation.Lon, 0, *ds.TTL, *ds.RoutingName, 0, "HTTP", 0, "", "", 42,
false, "", nil, "", "", "", 0, "", *ds.BypassDestination["HTTP"].FQDN,
*ds.IP6RoutingEnabled, nil, "", "")
+ }
+ mock.ExpectQuery("select").WithArgs(cdn).WillReturnRows(rows)
+}
+
+func TestMakeDSes(t *testing.T) {
+ db, mock, err := sqlmock.New()
+ if err != nil {
+ t.Fatalf("an error '%s' was not expected when opening a stub
database connection", err)
+ }
+ defer db.Close()
+
+ cdn := "mycdn"
+
+ expected := ExpectedMakeDSes()
+ MockMakeDSes(mock, expected, cdn)
+
+ expectedParams := ExpectedGetServerProfileParams(expected)
+ MockGetServerProfileParams(mock, expectedParams, cdn)
+
+ expectedDSParams, err := getDSParams(expectedParams)
+ if err != nil {
+ t.Fatalf("getDSParams error expected: nil, actual: %v", err)
+ }
+ expectedMatchsets, expectedDomains :=
ExpectedGetDSRegexesDomains(expectedDSParams)
+ MockGetDSRegexesDomains(mock, expectedMatchsets, expectedDomains, cdn)
+
+ expectedStaticDNSEntries := ExpectedGetStaticDNSEntries(expected)
+ MockGetStaticDNSEntries(mock, expectedStaticDNSEntries, cdn)
+
+ actual, err := makeDSes(cdn, db)
+ if err != nil {
+ t.Fatalf("makeDSes expected: nil error, actual: %v", err)
+ }
+
+ if len(actual) != len(expected) {
+ t.Fatalf("makeDses len expected: %v, actual: %v",
len(expected), len(actual))
+ }
+
+ for dsName, ds := range expected {
+ actualDS, ok := actual[dsName]
+ if !ok {
+ t.Errorf("makeDSes expected: %v, actual: missing",
dsName)
+ continue
+ }
+ expectedBts, _ := json.MarshalIndent(ds, " ", " ")
+ actualBts, _ := json.MarshalIndent(actualDS, " ", " ")
+ if !reflect.DeepEqual(expectedBts, actualBts) {
+ t.Errorf("makeDSes ds %+v expected: %+v\n\nactual:
%+v\n\n\n", dsName, string(expectedBts), string(actualBts))
+ }
+ }
+}
+
+func ExpectedGetServerProfileParams(expectedMakeDSes
map[string]tc.CRConfigDeliveryService) map[string]map[string]string {
+ expected := map[string]map[string]string{}
+ for dsName, _ := range expectedMakeDSes {
+ expected[dsName] = map[string]string {
+ "param0": "val0",
+ "param1": "val1",
+ }
+ }
+ return expected
+}
+
+func MockGetServerProfileParams(mock sqlmock.Sqlmock, expected
map[string]map[string]string, cdn string) {
+ rows := sqlmock.NewRows([]string{"name", "value", "profile"})
+ for dsName, params := range expected {
+ for param, val := range params {
+ rows = rows.AddRow(param, val, dsName)
+ }
+ }
+ mock.ExpectQuery("select").WithArgs(cdn).WillReturnRows(rows)
+}
+
+func TestGetServerProfileParams(t *testing.T) {
+ db, mock, err := sqlmock.New()
+ if err != nil {
+ t.Fatalf("an error '%s' was not expected when opening a stub
database connection", err)
+ }
+ defer db.Close()
+
+ cdn := "mycdn"
+
+ expectedMakeDSes := ExpectedMakeDSes()
+ expected := ExpectedGetServerProfileParams(expectedMakeDSes)
+ MockGetServerProfileParams(mock, expected, cdn)
+
+ actual, err := getServerProfileParams(cdn, db)
+ if err != nil {
+ t.Fatalf("getServerProfileParams expected: nil error, actual:
%v", err)
+ }
+
+ if len(actual) != len(expected) {
+ t.Fatalf("getServerProfileParams len expected: %v, actual: %v
(%+v)", len(expected), len(actual), actual)
+ }
+
+ for dsName, expectedParams := range expected {
+ actualParams, ok := actual[dsName]
+ if !ok {
+ t.Errorf("getServerProfileParams expected: %v, actual:
missing (actual %+v)", dsName, actual)
+ continue
+ }
+ if !reflect.DeepEqual(expectedParams, actualParams) {
+ t.Errorf("getServerProfileParams ds %+v expected: %+v,
actual: %+v", dsName, expectedParams, actualParams)
+ }
+ }
+}
+
+func ExpectedGetDSRegexesDomains(expectedDSParams map[string]string)
(map[string][]*tc.MatchSet, map[string][]string) {
+ matchsets := map[string][]*tc.MatchSet{}
+ domains := map[string][]string{}
+
+ setnum := 0
+ protocolStr := "HTTP"
+ matchType := "HOST_REGEXP"
+
+ domain := "foo"
+ if val, ok := expectedDSParams["domain_name"]; ok {
+ domain = val
+ }
+
+ for dsName, _ := range expectedDSParams {
+ pattern := `.*\.`+dsName+`\..*`
+
+ matchsets[dsName][setnum] = &tc.MatchSet{}
+ matchset := matchsets[dsName][setnum]
+ matchset.Protocol = protocolStr
+ matchset.MatchList = append(matchset.MatchList,
tc.MatchList{MatchType: matchType, Regex: pattern})
+
+ domains[dsName] = append(domains[dsName],
strings.NewReplacer(`\`, ``, `.*`, ``, `.`, ``).Replace(pattern)+"."+domain)
+ }
+ return matchsets, domains
+}
+
+func MockGetDSRegexesDomains(mock sqlmock.Sqlmock, expectedMatchsets
map[string][]*tc.MatchSet, expectedDomains map[string][]string, cdn string) {
+ rows := sqlmock.NewRows([]string{"pattern", "type", "dstype",
"set_number", "xml_id"})
+ for dsName, matchsets := range expectedMatchsets {
+ for _, matchset := range matchsets {
+ for _, matchlist := range matchset.MatchList {
+ rows = rows.AddRow(matchlist.Regex, "HOST",
"HTTP", 0, dsName)
+ }
+ }
+ }
+ mock.ExpectQuery("select").WithArgs(cdn).WillReturnRows(rows)
+}
+
+func TestGetDSRegexesDomains(t *testing.T) {
+ db, mock, err := sqlmock.New()
+ if err != nil {
+ t.Fatalf("an error '%s' was not expected when opening a stub
database connection", err)
+ }
+ defer db.Close()
+
+ cdn := "mycdn"
+
+
+ expectedMakeDSes := ExpectedMakeDSes()
+ expectedServerProfileParams :=
ExpectedGetServerProfileParams(expectedMakeDSes)
+ expectedDSParams, err := getDSParams(expectedServerProfileParams)
+ if err != nil {
+ t.Fatalf("getDSParams error expected: nil, actual: %v", err)
+ }
+ expectedMatchsets, expectedDomains :=
ExpectedGetDSRegexesDomains(expectedDSParams)
+ MockGetDSRegexesDomains(mock, expectedMatchsets, expectedDomains, cdn)
+
+ actualMatchsets, actualDomains, err := getDSRegexesDomains(cdn, db,
expectedDSParams)
+ if err != nil {
+ t.Fatalf("getDSRegexesDomains expected: nil error, actual: %v",
err)
+ }
+
+ if len(actualMatchsets) != len(expectedMatchsets) {
+ t.Fatalf("getDSRegexesDomains len(matchsets) expected: %v,
actual: %v", len(expectedMatchsets), len(actualMatchsets))
+ }
+ if len(actualDomains) != len(expectedDomains) {
+ t.Fatalf("getDSRegexesDomains len(matchsets) expected: %v,
actual: %v", len(expectedDomains), len(actualDomains))
+ }
+
+ if !reflect.DeepEqual(expectedMatchsets, actualMatchsets) {
+ t.Errorf("getDSRegexesDomains expected: %+v, actual:
%+v", expectedMatchsets, actualMatchsets)
+ }
+ if !reflect.DeepEqual(expectedDomains, actualDomains) {
+ t.Errorf("getDSRegexesDomains expected: %+v, actual:
%+v", expectedDomains, actualDomains)
+ }
+}
+
+func ExpectedGetStaticDNSEntries(expectedMakeDSes
map[string]tc.CRConfigDeliveryService)
(map[tc.DeliveryServiceName][]tc.StaticDNSEntry) {
+ expected := map[tc.DeliveryServiceName][]tc.StaticDNSEntry{}
+ for dsName, ds := range expectedMakeDSes {
+ for _, entry := range ds.StaticDNSEntries {
+ expected[tc.DeliveryServiceName(dsName)] =
append(expected[tc.DeliveryServiceName(dsName)], tc.StaticDNSEntry{Name:
entry.Name, TTL: entry.TTL, Value: entry.Value, Type: entry.Type})
+ }
+ }
+ return expected
+}
+
+func MockGetStaticDNSEntries(mock sqlmock.Sqlmock, expected
map[tc.DeliveryServiceName][]tc.StaticDNSEntry, cdn string) {
+ rows := sqlmock.NewRows([]string{"ds", "name", "ttl", "value", "type"})
+ for dsName, entries := range expected {
+ for _, entry := range entries {
+ rows = rows.AddRow(dsName, entry.Name, entry.TTL,
entry.Value, entry.Type + "_RECORD")
+ }
+ }
+ mock.ExpectQuery("select").WithArgs(cdn).WillReturnRows(rows)
+}
+
+func TestGetStaticDNSEntries(t *testing.T) {
+ db, mock, err := sqlmock.New()
+ if err != nil {
+ t.Fatalf("an error '%s' was not expected when opening a stub
database connection", err)
+ }
+ defer db.Close()
+
+ cdn := "mycdn"
+
+ expectedMakeDSes := ExpectedMakeDSes()
+ expected := ExpectedGetStaticDNSEntries(expectedMakeDSes)
+ MockGetStaticDNSEntries(mock, expected, cdn)
+
+ actual, err := getStaticDNSEntries(cdn, db)
+ if err != nil {
+ t.Fatalf("getStaticDNSEntries expected: nil error, actual: %v",
err)
+ }
+
+ if len(actual) != len(expected) {
+ t.Fatalf("getStaticDNSEntries len expected: %v, actual: %v",
len(expected), len(actual))
+ }
+ if !reflect.DeepEqual(expected, actual) {
+ t.Errorf("getDSRegexesDomains expected: %+v, actual:
%+v", expected, actual)
+ }
+}
diff --git a/traffic_ops/traffic_ops_golang/crconfig/edgelocations.go
b/traffic_ops/traffic_ops_golang/crconfig/edgelocations.go
new file mode 100644
index 000000000..2d43fd654
--- /dev/null
+++ b/traffic_ops/traffic_ops_golang/crconfig/edgelocations.go
@@ -0,0 +1,67 @@
+package crconfig
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import (
+ "database/sql"
+ "errors"
+
+ "github.com/apache/incubator-trafficcontrol/lib/go-tc"
+)
+
+func makeLocations(cdn string, db *sql.DB)
(map[string]tc.CRConfigLatitudeLongitude,
map[string]tc.CRConfigLatitudeLongitude, error) {
+ edgeLocs := map[string]tc.CRConfigLatitudeLongitude{}
+ routerLocs := map[string]tc.CRConfigLatitudeLongitude{}
+
+ // TODO test whether it's faster to do a single query, joining lat/lon
into servers
+ q := `
+select cg.name, t.name as type, cg.latitude, cg.longitude from cachegroup as cg
+inner join server as s on s.cachegroup = cg.id
+inner join type as t on t.id = s.type
+inner join status as st ON st.id = s.status
+where s.cdn_id = (select id from cdn where name = $1)
+and (t.name like 'EDGE%' or t.name = 'CCR')
+and (st.name = 'REPORTED' or st.name = 'ONLINE' or st.name = 'ADMIN_DOWN')
+`
+ // TODO pass edge type prefix, router type name
+ rows, err := db.Query(q, cdn)
+ if err != nil {
+ return nil, nil, errors.New("Error querying cachegroups: " +
err.Error())
+ }
+ defer rows.Close()
+
+ for rows.Next() {
+ cachegroup := ""
+ ttype := ""
+ latlon := tc.CRConfigLatitudeLongitude{}
+ if err := rows.Scan(&cachegroup, &ttype, &latlon.Lat,
&latlon.Lon); err != nil {
+ return nil, nil, errors.New("Error scanning cachegroup:
" + err.Error())
+ }
+ if ttype == RouterTypeName {
+ routerLocs[cachegroup] = latlon
+ } else {
+ edgeLocs[cachegroup] = latlon
+ }
+ }
+ if err := rows.Err(); err != nil {
+ return nil, nil, errors.New("Error iterating cachegroup rows: "
+ err.Error())
+ }
+ return edgeLocs, routerLocs, nil
+}
diff --git a/traffic_ops/traffic_ops_golang/crconfig/edgelocations_test.go
b/traffic_ops/traffic_ops_golang/crconfig/edgelocations_test.go
new file mode 100644
index 000000000..a8b4918fe
--- /dev/null
+++ b/traffic_ops/traffic_ops_golang/crconfig/edgelocations_test.go
@@ -0,0 +1,76 @@
+package crconfig
+
+/*
+ * 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 (
+ "reflect"
+ "testing"
+
+ "github.com/apache/incubator-trafficcontrol/lib/go-tc"
+
+ "gopkg.in/DATA-DOG/go-sqlmock.v1"
+)
+
+func ExpectedMakeLocations() (map[string]tc.CRConfigLatitudeLongitude,
map[string]tc.CRConfigLatitudeLongitude) {
+ return map[string]tc.CRConfigLatitudeLongitude{
+ "cache0": tc.CRConfigLatitudeLongitude{Lat:
*randFloat64(), Lon: *randFloat64()},
+ "cache1": tc.CRConfigLatitudeLongitude{Lat:
*randFloat64(), Lon: *randFloat64()},
+ },
+ map[string]tc.CRConfigLatitudeLongitude{
+ "router0": tc.CRConfigLatitudeLongitude{Lat:
*randFloat64(), Lon: *randFloat64()},
+ "router1": tc.CRConfigLatitudeLongitude{Lat:
*randFloat64(), Lon: *randFloat64()},
+ }
+}
+
+func MockMakeLocations(mock sqlmock.Sqlmock, expectedEdgeLocs
map[string]tc.CRConfigLatitudeLongitude, expectedRouterLocs
map[string]tc.CRConfigLatitudeLongitude, cdn string) {
+ rows := sqlmock.NewRows([]string{"name", "type", "latitude",
"longitude"})
+ for s, l := range expectedEdgeLocs {
+ rows = rows.AddRow(s, EdgeTypePrefix, l.Lat, l.Lon)
+ }
+ for s, l := range expectedRouterLocs {
+ rows = rows.AddRow(s, RouterTypeName, l.Lat, l.Lon)
+ }
+ mock.ExpectQuery("select").WithArgs(cdn).WillReturnRows(rows)
+}
+
+func TestMakeLocations(t *testing.T) {
+ db, mock, err := sqlmock.New()
+ if err != nil {
+ t.Fatalf("an error '%s' was not expected when opening a stub
database connection", err)
+ }
+ defer db.Close()
+
+ cdn := "mycdn"
+
+ expectedEdgeLocs, expectedRouterLocs := ExpectedMakeLocations()
+ MockMakeLocations(mock, expectedEdgeLocs, expectedRouterLocs, cdn)
+
+ actualEdgeLocs, actualRouterLocs, err := makeLocations(cdn, db)
+ if err != nil {
+ t.Fatalf("makeLocations expected: nil error, actual: %v", err)
+ }
+
+ if !reflect.DeepEqual(expectedEdgeLocs, actualEdgeLocs) {
+ t.Errorf("makeLocations expected: %+v, actual: %+v",
expectedEdgeLocs, actualEdgeLocs)
+ }
+ if !reflect.DeepEqual(expectedRouterLocs, actualRouterLocs) {
+ t.Errorf("makeLocations expected: %+v, actual: %+v",
expectedRouterLocs, actualRouterLocs)
+ }
+}
diff --git a/traffic_ops/traffic_ops_golang/crconfig/handler.go
b/traffic_ops/traffic_ops_golang/crconfig/handler.go
new file mode 100644
index 000000000..78b1fd185
--- /dev/null
+++ b/traffic_ops/traffic_ops_golang/crconfig/handler.go
@@ -0,0 +1,216 @@
+package crconfig
+
+/*
+ * 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 (
+ "encoding/json"
+ "errors"
+ "fmt"
+ "net/http"
+ "net/url"
+ "strconv"
+ "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/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/jmoiron/sqlx"
+)
+
+const PrivLevel = auth.PrivLevelAdmin
+
+// Handler creates and serves the CRConfig from the raw SQL data.
+// This MUST only be used for debugging or previewing, the raw un-snapshotted
data MUST NOT be used by any component of the CDN.
+func Handler(db *sqlx.DB, cfg config.Config) http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ start := time.Now()
+ handleErrs := tc.GetHandleErrorsFunc(w, r)
+
+ params, err := api.GetCombinedParams(r)
+ if err != nil {
+ handleErrs(http.StatusInternalServerError, err)
+ return
+ }
+
+ cdn, ok := params["cdn"]
+ if !ok {
+ handleErrs(http.StatusInternalServerError,
errors.New("params missing CDN"))
+ return
+ }
+
+ ctx := r.Context()
+ user, err := auth.GetCurrentUser(ctx)
+ if err != nil {
+ handleErrs(http.StatusInternalServerError, err)
+ return
+ }
+
+ crConfig, err := Make(db.DB, cdn, user.UserName, r.Host,
r.URL.Path, cfg.Version)
+ if err != nil {
+ handleErrs(http.StatusInternalServerError, err)
+ return
+ }
+
+ respBts, err := json.Marshal(crConfig)
+ if err != nil {
+ handleErrs(http.StatusInternalServerError, err)
+ return
+ }
+ log.Infof("CRConfig time to generate: %+v\n", time.Since(start))
+ w.Header().Set("Content-Type", "application/json")
+ fmt.Fprintf(w, "%s", respBts)
+ }
+}
+
+// SnapshotGetHandler gets and serves the CRConfig from the snapshot table.
+func SnapshotGetHandler(db *sqlx.DB, cfg config.Config) http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ handleErrs := tc.GetHandleErrorsFunc(w, r)
+ params, err := api.GetCombinedParams(r)
+ if err != nil {
+ handleErrs(http.StatusInternalServerError, err)
+ return
+ }
+ cdn, ok := params["cdn"]
+ if !ok {
+ handleErrs(http.StatusInternalServerError,
errors.New("params missing CDN"))
+ return
+ }
+
+ snapshot, cdnExists, err := GetSnapshot(db.DB, cdn)
+ if err != nil {
+ handleErrs(http.StatusInternalServerError,
errors.New("getting snapshot: "+err.Error()))
+ return
+ }
+ if !cdnExists {
+ handleErrs(http.StatusNotFound, errors.New("CDN not
found"))
+ return
+ }
+ w.Header().Set("Content-Type", "application/json")
+ fmt.Fprintf(w, "%s", []byte(snapshot))
+ }
+}
+
+// SnapshotHandler creates the CRConfig JSON and writes it to the snapshot
table in the database.
+func SnapshotHandler(db *sqlx.DB, cfg config.Config) http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ handleErrs := tc.GetHandleErrorsFunc(w, r)
+ params, err := api.GetCombinedParams(r)
+ if err != nil {
+ handleErrs(http.StatusInternalServerError, err)
+ return
+ }
+
+ ctx := r.Context()
+ user, err := auth.GetCurrentUser(ctx)
+ if err != nil {
+ handleErrs(http.StatusInternalServerError, err)
+ return
+ }
+
+ cdn, ok := params["cdn"]
+ if !ok {
+ idStr, ok := params["id"]
+ if !ok {
+ handleErrs(http.StatusNotFound,
errors.New("params missing CDN"))
+ return
+ }
+ id, err := strconv.Atoi(idStr)
+ if err != nil {
+ handleErrs(http.StatusNotFound,
errors.New("param CDN ID is not an integer"))
+ return
+ }
+ name, ok, err := getCDNNameFromID(id, db.DB)
+ if err != nil {
+ handleErrs(http.StatusInternalServerError,
errors.New("Error getting CDN name from ID: "+err.Error()))
+ return
+ }
+ if !ok {
+ handleErrs(http.StatusNotFound, errors.New("No
CDN found with that ID"))
+ return
+ }
+ cdn = name
+ }
+
+ crConfig, err := Make(db.DB, cdn, user.UserName, r.Host,
r.URL.Path, cfg.Version)
+ if err != nil {
+ handleErrs(http.StatusInternalServerError, err)
+ return
+ }
+
+ if err := Snapshot(db.DB, crConfig); err != nil {
+ handleErrs(http.StatusInternalServerError, err)
+ return
+ }
+ w.WriteHeader(http.StatusOK) // TODO change to 204 No Content
in new version
+ }
+}
+
+// SnapshotGUIHandler creates the CRConfig JSON and writes it to the snapshot
table in the database. The response emulates the old Perl UI function. This
should go away when the old Perl UI ceases to exist.
+func SnapshotOldGUIHandler(db *sqlx.DB, cfg config.Config) http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ params, err := api.GetCombinedParams(r)
+ if err != nil {
+ log.Errorln(r.RemoteAddr + " unable to get parameters
from request: " + err.Error())
+ writePerlHTMLErr(w, r, err)
+ return
+ }
+
+ cdn, ok := params["cdn"]
+ if !ok {
+ err := errors.New("params missing CDN")
+ log.Errorln(r.RemoteAddr + " " + err.Error())
+ writePerlHTMLErr(w, r, err)
+ return
+ }
+
+ log.Errorln("DEBUG calling crconfig.SnapshotOldGUIHandler CDN "
+ cdn)
+
+ ctx := r.Context()
+ user, err := auth.GetCurrentUser(ctx)
+ if err != nil {
+ log.Errorln(r.RemoteAddr + " getting user: " +
err.Error())
+ writePerlHTMLErr(w, r, err)
+ return
+ }
+
+ crConfig, err := Make(db.DB, cdn, user.UserName, r.Host,
r.URL.Path, cfg.Version)
+ if err != nil {
+ log.Errorln(r.RemoteAddr + " making CRConfig: " +
err.Error())
+ writePerlHTMLErr(w, r, err)
+ return
+ }
+
+ if err := Snapshot(db.DB, crConfig); err != nil {
+ log.Errorln(r.RemoteAddr + " making CRConfig: " +
err.Error())
+ writePerlHTMLErr(w, r, err)
+ return
+ }
+
+ http.Redirect(w, r,
"/tools/flash_and_close/"+url.PathEscape("Successfully wrote the
CRConfig.json!"), http.StatusFound)
+ }
+}
+
+func writePerlHTMLErr(w http.ResponseWriter, r *http.Request, err error) {
+ http.Redirect(w, r, "/tools/flash_and_close/"+url.PathEscape("Error:
"+err.Error()), http.StatusFound)
+}
diff --git a/traffic_ops/traffic_ops_golang/crconfig/servers.go
b/traffic_ops/traffic_ops_golang/crconfig/servers.go
new file mode 100644
index 000000000..6e59fef7a
--- /dev/null
+++ b/traffic_ops/traffic_ops_golang/crconfig/servers.go
@@ -0,0 +1,375 @@
+package crconfig
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import (
+ "database/sql"
+ "errors"
+ "strconv"
+ "strings"
+
+ "github.com/apache/incubator-trafficcontrol/lib/go-log"
+ "github.com/apache/incubator-trafficcontrol/lib/go-tc"
+)
+
+const RouterTypeName = "CCR"
+const MonitorTypeName = "RASCAL"
+const EdgeTypePrefix = "EDGE"
+const MidTypePrefix = "MID"
+
+func makeCRConfigServers(cdn string, db *sql.DB, cdnDomain string) (
+ map[string]tc.CRConfigTrafficOpsServer,
+ map[string]tc.CRConfigRouter,
+ map[string]tc.CRConfigMonitor,
+ error,
+) {
+ allServers, err := getAllServers(cdn, db)
+ if err != nil {
+ return nil, nil, nil, err
+ }
+
+ serverDSes, err := getServerDSes(cdn, db, cdnDomain)
+ if err != nil {
+ return nil, nil, nil, errors.New("getting server
deliveryservices: " + err.Error())
+ }
+
+ servers := map[string]tc.CRConfigTrafficOpsServer{}
+ routers := map[string]tc.CRConfigRouter{}
+ monitors := map[string]tc.CRConfigMonitor{}
+ for host, s := range allServers {
+ switch {
+ case *s.ServerType == RouterTypeName:
+ status := tc.CRConfigRouterStatus(*s.ServerStatus)
+ routers[host] = tc.CRConfigRouter{
+ APIPort: s.APIPort,
+ FQDN: s.Fqdn,
+ HTTPSPort: s.HttpsPort,
+ IP: s.Ip,
+ IP6: s.Ip6,
+ Location: s.LocationId,
+ Port: s.Port,
+ Profile: s.Profile,
+ ServerStatus: &status,
+ }
+ case *s.ServerType == MonitorTypeName:
+ monitors[host] = tc.CRConfigMonitor{
+ FQDN: s.Fqdn,
+ HTTPSPort: s.HttpsPort,
+ IP: s.Ip,
+ IP6: s.Ip6,
+ Location: s.LocationId,
+ Port: s.Port,
+ Profile: s.Profile,
+ ServerStatus: s.ServerStatus,
+ }
+ case strings.HasPrefix(*s.ServerType, EdgeTypePrefix) ||
strings.HasPrefix(*s.ServerType, MidTypePrefix):
+ if s.RoutingDisabled == 0 {
+ s.CRConfigTrafficOpsServer.DeliveryServices =
serverDSes[tc.CacheName(host)]
+ }
+ servers[host] = s.CRConfigTrafficOpsServer
+ }
+ }
+ return servers, routers, monitors, nil
+}
+
+// ServerUnion has all fields from all servers. This is used to select all
server data with a single query, and then convert each to the proper type
afterwards.
+type ServerUnion struct {
+ tc.CRConfigTrafficOpsServer
+ APIPort *string
+}
+
+const DefaultWeightMultiplier = 1000.0
+const DefaultWeight = 0.999
+
+func getAllServers(cdn string, db *sql.DB) (map[string]ServerUnion, error) {
+ servers := map[string]ServerUnion{}
+
+ serverParams, err := getServerParams(cdn, db)
+ if err != nil {
+ return nil, errors.New("Error getting server params: " +
err.Error())
+ }
+
+ // TODO select deliveryservices as array?
+ q := `
+select s.host_name, cg.name as cachegroup, concat(s.host_name, '.',
s.domain_name) as fqdn, s.xmpp_id as hashid, s.https_port, s.interface_name,
s.ip_address, s.ip6_address, s.tcp_port, p.name as profile_name,
cast(p.routing_disabled as int), st.name as status, t.name as type
+from server as s
+inner join cachegroup as cg ON cg.id = s.cachegroup
+inner join type as t on t.id = s.type
+inner join profile as p ON p.id = s.profile
+inner join status as st ON st.id = s.status
+where cdn_id = (select id from cdn where name = $1)
+and (st.name = 'REPORTED' or st.name = 'ONLINE' or st.name = 'ADMIN_DOWN')
+`
+ rows, err := db.Query(q, cdn)
+ if err != nil {
+ return nil, errors.New("Error querying servers: " + err.Error())
+ }
+ defer rows.Close()
+
+ for rows.Next() {
+ port := sql.NullInt64{}
+ ip6 := sql.NullString{}
+ hashId := sql.NullString{}
+ httpsPort := sql.NullInt64{}
+
+ s := ServerUnion{}
+
+ host := ""
+ status := ""
+ if err := rows.Scan(&host, &s.CacheGroup, &s.Fqdn, &hashId,
&httpsPort, &s.InterfaceName, &s.Ip, &ip6, &port, &s.Profile,
&s.RoutingDisabled, &status, &s.ServerType); err != nil {
+ return nil, errors.New("Error scanning server: " +
err.Error())
+ }
+
+ s.LocationId = s.CacheGroup
+
+ serverStatus := tc.CRConfigServerStatus(status)
+ s.ServerStatus = &serverStatus
+ if port.Valid {
+ i := int(port.Int64)
+ s.Port = &i
+ }
+
+ s.Ip6 = &ip6.String // Don't check valid, assign empty string
if null
+
+ if hashId.String != "" {
+ s.HashId = &hashId.String
+ } else {
+ s.HashId = &host
+ }
+
+ if httpsPort.Valid {
+ i := int(httpsPort.Int64)
+ s.HttpsPort = &i
+ }
+
+ params, hasParams := serverParams[host]
+ if hasParams && params.APIPort != nil {
+ s.APIPort = params.APIPort
+ }
+
+ weightMultiplier := DefaultWeightMultiplier
+ if hasParams && params.WeightMultiplier != nil {
+ weightMultiplier = *params.WeightMultiplier
+ }
+ weight := DefaultWeight
+ if hasParams && params.Weight != nil {
+ weight = *params.Weight
+ }
+ hashCount := int(weight * weightMultiplier)
+ s.HashCount = &hashCount
+
+ servers[host] = s
+ }
+ if err := rows.Err(); err != nil {
+ return nil, errors.New("Error iterating router param rows: " +
err.Error())
+ }
+
+ return servers, nil
+}
+
+func getServerDSNames(cdn string, db *sql.DB)
(map[tc.CacheName][]tc.DeliveryServiceName, error) {
+ q := `
+select s.host_name, ds.xml_id
+from deliveryservice_server as dss
+inner join server as s on dss.server = s.id
+inner join deliveryservice as ds on ds.id = dss.deliveryservice
+inner join profile as p on p.id = s.profile
+inner join status as st ON st.id = s.status
+where ds.cdn_id = (select id from cdn where name = $1)
+and ds.active = true
+and p.routing_disabled = false
+and (st.name = 'REPORTED' or st.name = 'ONLINE' or st.name = 'ADMIN_DOWN')
+`
+ rows, err := db.Query(q, cdn)
+ if err != nil {
+ return nil, errors.New("Error querying server deliveryservice
names: " + err.Error())
+ }
+ defer rows.Close()
+
+ serverDSes := map[tc.CacheName][]tc.DeliveryServiceName{}
+ for rows.Next() {
+ ds := ""
+ server := ""
+ if err := rows.Scan(&server, &ds); err != nil {
+ return nil, errors.New("Error scanning server
deliveryservice names: " + err.Error())
+ }
+ serverDSes[tc.CacheName(server)] =
append(serverDSes[tc.CacheName(server)], tc.DeliveryServiceName(ds))
+ }
+ return serverDSes, nil
+}
+
+type DSRouteInfo struct {
+ IsDNS bool
+ IsRaw bool
+ Remap string
+}
+
+func getServerDSes(cdn string, db *sql.DB, domain string)
(map[tc.CacheName]map[string][]string, error) {
+ serverDSNames, err := getServerDSNames(cdn, db)
+ if err != nil {
+ return nil, errors.New("Error getting server deliveryservices:
" + err.Error())
+ }
+
+ q := `
+select ds.xml_id as ds, dt.name as ds_type, ds.routing_name, r.pattern as
pattern
+from regex as r
+inner join type as rt on r.type = rt.id
+inner join deliveryservice_regex as dsr on dsr.regex = r.id
+inner join deliveryservice as ds on ds.id = dsr.deliveryservice
+inner join type as dt on dt.id = ds.type
+where ds.cdn_id = (select id from cdn where name = $1)
+and ds.active = true
+and rt.name = 'HOST_REGEXP'
+order by dsr.set_number asc
+`
+ rows, err := db.Query(q, cdn)
+ if err != nil {
+ return nil, errors.New("Error server deliveryservices: " +
err.Error())
+ }
+ defer rows.Close()
+
+ hostReplacer := strings.NewReplacer(`\`, ``, `.*`, ``)
+
+ dsInfs := map[string][]DSRouteInfo{}
+ for rows.Next() {
+ ds := ""
+ dsType := ""
+ dsPattern := ""
+ dsRoutingName := ""
+ inf := DSRouteInfo{}
+ if err := rows.Scan(&ds, &dsType, &dsRoutingName, &dsPattern);
err != nil {
+ return nil, errors.New("Error scanning server
deliveryservices: " + err.Error())
+ }
+ inf.IsDNS = strings.HasPrefix(dsType, "DNS")
+ inf.IsRaw = !strings.Contains(dsPattern, `.*`)
+ if !inf.IsRaw {
+ host := hostReplacer.Replace(dsPattern)
+ if inf.IsDNS {
+ inf.Remap = dsRoutingName + host + domain
+ } else {
+ inf.Remap = host + domain
+ }
+ } else {
+ inf.Remap = dsPattern
+ }
+ dsInfs[ds] = append(dsInfs[ds], inf)
+ }
+
+ serverDSPatterns := map[tc.CacheName]map[string][]string{}
+ for server, dses := range serverDSNames {
+ for _, dsName := range dses {
+ dsInfList, ok := dsInfs[string(dsName)]
+ if !ok {
+ log.Warnln("Creating CRConfig: deliveryservice
" + string(dsName) + " has no regexes, skipping")
+ continue
+ }
+ for _, dsInf := range dsInfList {
+ if !dsInf.IsRaw && !dsInf.IsDNS {
+ dsInf.Remap = string(server) +
dsInf.Remap
+ }
+ if _, ok := serverDSPatterns[server]; !ok {
+ serverDSPatterns[server] =
map[string][]string{}
+ }
+ serverDSPatterns[server][string(dsName)] =
append(serverDSPatterns[server][string(dsName)], dsInf.Remap)
+ }
+ }
+ }
+ return serverDSPatterns, nil
+}
+
+// ServerParams contains parameter data filled in the CRConfig Servers
objects. If a given param doesn't exist on the given server, it will be nil.
+type ServerParams struct {
+ APIPort *string
+ Weight *float64
+ WeightMultiplier *float64
+}
+
+func getServerParams(cdn string, db *sql.DB) (map[string]ServerParams, error) {
+ params := map[string]ServerParams{}
+
+ q := `
+select s.host_name, p.name, p.value
+from server as s
+left join profile_parameter as pp on pp.profile = s.profile
+left join parameter as p on p.id = pp.parameter
+inner join status as st ON st.id = s.status
+where s.cdn_id = (select id from cdn where name = $1)
+and ((p.config_file = 'CRConfig.json' and (p.name = 'weight' or p.name =
'weightMultiplier')) or (p.name = 'api.port'))
+and (st.name = 'REPORTED' or st.name = 'ONLINE' or st.name = 'ADMIN_DOWN')
+`
+ rows, err := db.Query(q, cdn)
+ if err != nil {
+ return nil, errors.New("Error querying server parameters: " +
err.Error())
+ }
+ defer rows.Close()
+
+ for rows.Next() {
+ server := ""
+ name := ""
+ val := ""
+ if err := rows.Scan(&server, &name, &val); err != nil {
+ return nil, errors.New("Error scanning server
parameters: " + err.Error())
+ }
+
+ param := params[server]
+ switch name {
+ case "api.port":
+ param.APIPort = &val
+ case "weight":
+ i, err := strconv.ParseFloat(val, 64)
+ if err != nil {
+ log.Warnln("Creating CRConfig: server " +
server + " weight param " + val + " not a number, ignoring")
+ continue
+ }
+ param.Weight = &i
+ case "weightMultiplier":
+ i, err := strconv.ParseFloat(val, 64)
+ if err != nil {
+ log.Warnln("Creating CRConfig: server " +
server + " weightMultiplier param " + val + " not a number, ignoring")
+ continue
+ }
+ param.WeightMultiplier = &i
+ }
+ params[server] = param
+ }
+ return params, nil
+}
+
+// getCDNInfo returns the CDN domain, and whether DNSSec is enabled
+func getCDNInfo(cdn string, db *sql.DB) (string, bool, error) {
+ domain := ""
+ dnssec := false
+ if err := db.QueryRow(`select domain_name, dnssec_enabled from cdn
where name = $1`, cdn).Scan(&domain, &dnssec); err != nil {
+ return "", false, errors.New("Error querying CDN domain name: "
+ err.Error())
+ }
+ return domain, dnssec, nil
+}
+
+// getCDNNameFromID returns the CDN name given the ID, false if the no CDN
with the given ID exists, and an error if the database query fails.
+func getCDNNameFromID(id int, db *sql.DB) (string, bool, error) {
+ name := ""
+ if err := db.QueryRow(`select name from cdn where id = $1`,
id).Scan(&name); err != nil {
+ if err == sql.ErrNoRows {
+ return "", false, nil
+ }
+ return "", false, errors.New("Error querying CDN name: " +
err.Error())
+ }
+ return name, true, nil
+}
diff --git a/traffic_ops/traffic_ops_golang/crconfig/servers_test.go
b/traffic_ops/traffic_ops_golang/crconfig/servers_test.go
new file mode 100644
index 000000000..e0a26352b
--- /dev/null
+++ b/traffic_ops/traffic_ops_golang/crconfig/servers_test.go
@@ -0,0 +1,364 @@
+package crconfig
+
+/*
+ * 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 (
+ "math/rand"
+ "reflect"
+ "testing"
+
+ "github.com/apache/incubator-trafficcontrol/lib/go-tc"
+
+ "gopkg.in/DATA-DOG/go-sqlmock.v1"
+)
+
+func randBool() *bool {
+ b := rand.Int()%2 == 0
+ return &b
+}
+func randStr() *string {
+ chars :=
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890-_"
+ num := 100
+ s := ""
+ for i := 0; i < num; i++ {
+ s += string(chars[rand.Intn(len(chars))])
+ }
+ return &s
+}
+func randInt() *int {
+ i := rand.Int()
+ return &i
+}
+func randInt64() *int64 {
+ i := int64(rand.Int63())
+ return &i
+}
+func randFloat64() *float64 {
+ f := rand.Float64()
+ return &f
+}
+
+func randServer() tc.CRConfigTrafficOpsServer {
+ status := tc.CRConfigServerStatus(*randStr())
+ cachegroup := randStr()
+ return tc.CRConfigTrafficOpsServer{
+ CacheGroup: cachegroup,
+ Fqdn: randStr(),
+ HashCount: randInt(),
+ HashId: randStr(),
+ HttpsPort: randInt(),
+ InterfaceName: randStr(),
+ Ip: randStr(),
+ Ip6: randStr(),
+ LocationId: cachegroup,
+ Port: randInt(),
+ Profile: randStr(),
+ ServerStatus: &status,
+ ServerType: randStr(),
+ RoutingDisabled: *randInt64(),
+ }
+}
+
+func ExpectedGetServerParams() map[string]ServerParams {
+ return map[string]ServerParams{
+ "cache0": ServerParams{
+ APIPort: randStr(),
+ Weight: randFloat64(),
+ WeightMultiplier: randFloat64(),
+ },
+ "cache1": ServerParams{
+ APIPort: randStr(),
+ Weight: randFloat64(),
+ WeightMultiplier: randFloat64(),
+ },
+ }
+}
+
+func MockGetServerParams(mock sqlmock.Sqlmock, expected
map[string]ServerParams, cdn string) {
+ rows := sqlmock.NewRows([]string{"host_name", "name", "value"})
+ rows = rows.AddRow("cache0", "api.port", *expected["cache0"].APIPort)
+ rows = rows.AddRow("cache0", "weight", *expected["cache0"].Weight)
+ rows = rows.AddRow("cache0", "weightMultiplier",
*expected["cache0"].WeightMultiplier)
+ rows = rows.AddRow("cache1", "api.port", *expected["cache1"].APIPort)
+ rows = rows.AddRow("cache1", "weight", *expected["cache1"].Weight)
+ rows = rows.AddRow("cache1", "weightMultiplier",
*expected["cache1"].WeightMultiplier)
+ mock.ExpectQuery("select").WithArgs(cdn).WillReturnRows(rows)
+}
+
+func TestGetServerParams(t *testing.T) {
+ db, mock, err := sqlmock.New()
+ if err != nil {
+ t.Fatalf("an error '%s' was not expected when opening a stub
database connection", err)
+ }
+ defer db.Close()
+
+ cdn := "mycdn"
+
+ expected := ExpectedGetServerParams()
+ MockGetServerParams(mock, expected, cdn)
+
+ actual, err := getServerParams(cdn, db)
+ if err != nil {
+ t.Fatalf("getServerParams expected: nil error, actual: %v", err)
+ }
+
+ if len(actual) != len(expected) {
+ t.Fatalf("getServerParams len expected: %v, actual: %v",
len(expected), len(actual))
+ }
+
+ for name, params := range expected {
+ actualParams, ok := actual[name]
+ if !ok {
+ t.Errorf("getServerParams expected: %v, actual:
missing", name)
+ continue
+ }
+ if !reflect.DeepEqual(params, actualParams) {
+ t.Errorf("getServerParams server %+v expected: %+v,
actual: %+v", name, params, actualParams)
+ }
+ }
+}
+
+func ExpectedGetAllServers(params map[string]ServerParams)
map[string]ServerUnion {
+ expected := map[string]ServerUnion{}
+ for name, param := range params {
+ s := ServerUnion{
+ APIPort: param.APIPort,
+ CRConfigTrafficOpsServer: randServer(),
+ }
+ i := int(*param.Weight * *param.WeightMultiplier)
+ s.HashCount = &i
+ expected[name] = s
+ }
+ return expected
+}
+
+func MockGetAllServers(mock sqlmock.Sqlmock, expected map[string]ServerUnion,
cdn string) {
+ rows := sqlmock.NewRows([]string{"host_name", "cachegroup", "fqdn",
"hashid", "https_port", "interface_name", "ip_address", "ip6_address",
"tcp_port", "profile_name", "routing_disabled", "status", "type"})
+ for name, s := range expected {
+ rows = rows.AddRow(name, *s.CacheGroup, *s.Fqdn, *s.HashId,
*s.HttpsPort, *s.InterfaceName, *s.Ip, *s.Ip6, *s.Port, *s.Profile,
s.RoutingDisabled, *s.ServerStatus, *s.ServerType)
+ }
+ mock.ExpectQuery("select").WithArgs(cdn).WillReturnRows(rows)
+}
+
+func TestGetAllServers(t *testing.T) {
+ db, mock, err := sqlmock.New()
+ if err != nil {
+ t.Fatalf("an error '%s' was not expected when opening a stub
database connection", err)
+ }
+ defer db.Close()
+
+ cdn := "mycdn"
+
+ getServerParamsExpected := ExpectedGetServerParams()
+ MockGetServerParams(mock, getServerParamsExpected, cdn)
+
+ expected := ExpectedGetAllServers(getServerParamsExpected)
+ MockGetAllServers(mock, expected, cdn)
+
+ actual, err := getAllServers(cdn, db)
+
+ if err != nil {
+ t.Fatalf("getAllServers expected: nil error, actual: %v", err)
+ }
+
+ if len(actual) != len(expected) {
+ t.Errorf("getAllServers len expected: %v, actual: %v",
len(expected), len(actual))
+ }
+
+ for name, server := range expected {
+ actualServer, ok := actual[name]
+ if !ok {
+ t.Errorf("getAllServers expected: %v, actual: missing",
name)
+ continue
+ }
+ if !reflect.DeepEqual(server, actualServer) {
+ t.Errorf("getAllServers server %v expected: %v, actual:
%v", name, server, actualServer)
+ }
+ }
+}
+
+func ExpectedGetServerDSNames() map[tc.CacheName][]tc.DeliveryServiceName {
+ return map[tc.CacheName][]tc.DeliveryServiceName{
+ "cache0": []tc.DeliveryServiceName{"ds0", "ds1"},
+ "cache1": []tc.DeliveryServiceName{"ds0", "ds1"},
+ }
+}
+
+func MockGetServerDSNames(mock sqlmock.Sqlmock, expected
map[tc.CacheName][]tc.DeliveryServiceName, cdn string) {
+ rows := sqlmock.NewRows([]string{"host_name", "xml_id"})
+ for cache, dses := range expected {
+ for _, ds := range dses {
+ rows = rows.AddRow(cache, ds)
+ }
+ }
+ mock.ExpectQuery("select").WithArgs(cdn).WillReturnRows(rows)
+}
+
+func TestGetServerDSNames(t *testing.T) {
+ db, mock, err := sqlmock.New()
+ if err != nil {
+ t.Fatalf("an error '%s' was not expected when opening a stub
database connection", err)
+ }
+ defer db.Close()
+
+ cdn := "mycdn"
+
+ expected := ExpectedGetServerDSNames()
+ MockGetServerDSNames(mock, expected, cdn)
+
+ actual, err := getServerDSNames(cdn, db)
+
+ if err != nil {
+ t.Fatalf("getServerDSNames expected: nil error, actual: %v",
err)
+ }
+
+ if len(actual) != len(expected) {
+ t.Errorf("getServerDSNames len expected: %v, actual: %v",
len(expected), len(actual))
+ }
+
+ if !reflect.DeepEqual(expected, actual) {
+ t.Errorf("getServerDSNames expected: %v, actual: %v", expected,
actual)
+ }
+}
+
+func ExpectedGetServerDSes(expectedGetServerDSNames
map[tc.CacheName][]tc.DeliveryServiceName) map[tc.CacheName]map[string][]string
{
+ e := map[tc.CacheName]map[string][]string{}
+ for cache, dses := range expectedGetServerDSNames {
+ e[cache] = map[string][]string{}
+ for _, ds := range dses {
+ e[cache][string(ds)] = []string{string(ds) + "regex0",
string(ds) + "regex1"}
+ }
+ }
+ return e
+}
+
+func MockGetServerDSes(mock sqlmock.Sqlmock, expected
map[tc.CacheName]map[string][]string, cdn string) {
+ rows := sqlmock.NewRows([]string{"ds", "ds_type", "routing_name",
"pattern"})
+ dsmap := map[string][]string{}
+ for _, dses := range expected {
+ for ds, patterns := range dses {
+ dsmap[ds] = patterns
+ }
+ }
+
+ for ds, patterns := range dsmap {
+ for _, pattern := range patterns {
+ rows = rows.AddRow(ds, "DNS", "", pattern)
+ }
+ }
+ mock.ExpectQuery("select").WithArgs(cdn).WillReturnRows(rows)
+}
+
+func TestGetServerDSes(t *testing.T) {
+ db, mock, err := sqlmock.New()
+ if err != nil {
+ t.Fatalf("an error '%s' was not expected when opening a stub
database connection", err)
+ }
+ defer db.Close()
+
+ cdn := "mycdn"
+ domain := "mydomain"
+
+ expectedGetServerDSNames := ExpectedGetServerDSNames()
+ MockGetServerDSNames(mock, expectedGetServerDSNames, cdn)
+
+ expected := ExpectedGetServerDSes(expectedGetServerDSNames)
+ MockGetServerDSes(mock, expected, cdn)
+
+ actual, err := getServerDSes(cdn, db, domain)
+
+ if err != nil {
+ t.Fatalf("getServerDSes expected: nil error, actual: %v", err)
+ }
+
+ if !reflect.DeepEqual(expected, actual) {
+ t.Errorf("getServerDSes expected: %v, actual: %v", expected,
actual)
+ }
+}
+
+func ExpectedGetCDNInfo() (string, bool) {
+ return *randStr(), *randBool()
+}
+
+func MockGetCDNInfo(mock sqlmock.Sqlmock, expectedDomain string,
expectedDNSSECEnabled bool, cdn string) {
+ rows := sqlmock.NewRows([]string{"domain_name", "dnssec_enabled"})
+ rows = rows.AddRow(expectedDomain, expectedDNSSECEnabled)
+ mock.ExpectQuery("select").WithArgs(cdn).WillReturnRows(rows)
+}
+
+func TestGetCDNInfo(t *testing.T) {
+ db, mock, err := sqlmock.New()
+ if err != nil {
+ t.Fatalf("an error '%s' was not expected when opening a stub
database connection", err)
+ }
+ defer db.Close()
+
+ cdn := "mycdn"
+
+ expectedDomain, expectedDNSSECEnabled := ExpectedGetCDNInfo()
+ MockGetCDNInfo(mock, expectedDomain, expectedDNSSECEnabled, cdn)
+
+ actualDomain, actualDNSSECEnabled, err := getCDNInfo(cdn, db)
+ if err != nil {
+ t.Fatalf("getCDNInfo expected: nil error, actual: %v", err)
+ }
+
+ if expectedDomain != actualDomain {
+ t.Errorf("getCDNInfo expected: %v, actual: %v", expectedDomain,
actualDomain)
+ }
+ if expectedDNSSECEnabled != actualDNSSECEnabled {
+ t.Errorf("getCDNInfo expected: %v, actual: %v",
expectedDNSSECEnabled, actualDNSSECEnabled)
+ }
+}
+
+func ExpectedGetCDNNameFromID() string {
+ return *randStr()
+}
+
+func MockGetCDNNameFromID(mock sqlmock.Sqlmock, expected string, cdnID int) {
+ rows := sqlmock.NewRows([]string{"name"})
+ rows = rows.AddRow(expected)
+ mock.ExpectQuery("select").WithArgs(cdnID).WillReturnRows(rows)
+}
+
+func TestGetCDNNameFromID(t *testing.T) {
+ db, mock, err := sqlmock.New()
+ if err != nil {
+ t.Fatalf("an error '%s' was not expected when opening a stub
database connection", err)
+ }
+ defer db.Close()
+
+ cdnID := 42
+
+ expected := ExpectedGetCDNNameFromID()
+ MockGetCDNNameFromID(mock, expected, cdnID)
+
+ actual, exists, err := getCDNNameFromID(cdnID, db)
+ if err != nil {
+ t.Fatalf("getCDNNameFromID expected: nil error, actual: %v",
err)
+ }
+ if !exists {
+ t.Fatalf("getCDNNameFromID exists expected: true, actual:
false")
+ }
+
+ if expected != actual {
+ t.Errorf("getCDNNameFromID expected: %v, actual: %v", expected,
actual)
+ }
+}
diff --git a/traffic_ops/traffic_ops_golang/crconfig/snapshot.go
b/traffic_ops/traffic_ops_golang/crconfig/snapshot.go
new file mode 100644
index 000000000..3f24ecf93
--- /dev/null
+++ b/traffic_ops/traffic_ops_golang/crconfig/snapshot.go
@@ -0,0 +1,78 @@
+package crconfig
+
+/*
+ * 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"
+ "time"
+
+ "github.com/apache/incubator-trafficcontrol/lib/go-log"
+ "github.com/apache/incubator-trafficcontrol/lib/go-tc"
+)
+
+// Snapshot takes the CRConfig JSON-serializable object (which may be
generated via crconfig.Make), and writes it to the snapshot table.
+func Snapshot(db *sql.DB, crc *tc.CRConfig) error {
+ log.Errorln("DEBUG calling Snapshot")
+ bts, err := json.Marshal(crc)
+ if err != nil {
+ return errors.New("marshalling JSON: " + err.Error())
+ }
+ date := time.Now()
+ if crc.Stats.DateUnixSeconds != nil {
+ date = time.Unix(*crc.Stats.DateUnixSeconds, 0)
+ }
+ log.Errorf("DEBUG calling Snapshot, writing %+v\n", date)
+ q := `insert into snapshot (cdn, content, last_updated) values ($1, $2,
$3) on conflict(cdn) do update set content=$2, last_updated=$3`
+ if _, err := db.Exec(q, crc.Stats.CDNName, bts, date); err != nil {
+ return errors.New("Error inserting the snapshot into database:
" + err.Error())
+ }
+ return nil
+}
+
+// GetSnapshot gets the snapshot for the given CDN.
+// If the CDN does not exist, false is returned.
+// If the CDN exists, but the snapshot does not, the string for an empty JSON
object "{}" is returned.
+// An error is only returned on database error, never if the CDN or snapshot
does not exist.
+func GetSnapshot(db *sql.DB, cdn string) (string, bool, error) {
+ log.Errorln("DEBUG calling GetSnapshot")
+
+ snapshot := sql.NullString{}
+ // cdn left join snapshot, so we get a row with null if the CDN exists
but the snapshot doesn't, and no rows if the CDN doesn't exist.
+ q := `
+select s.content as snapshot
+from cdn as c
+left join snapshot as s on s.cdn = c.name
+where c.name = $1
+`
+ if err := db.QueryRow(q, cdn).Scan(&snapshot); err != nil {
+ if err == sql.ErrNoRows {
+ // CDN doesn't exist
+ return "", false, nil
+ }
+ return "", false, errors.New("Error querying snapshot: " +
err.Error())
+ }
+ if !snapshot.Valid {
+ // CDN exists, but snapshot doesn't
+ return `{}`, true, nil
+ }
+ return snapshot.String, true, nil
+}
diff --git a/traffic_ops/traffic_ops_golang/crconfig/snapshot_test.go
b/traffic_ops/traffic_ops_golang/crconfig/snapshot_test.go
new file mode 100644
index 000000000..dc8cd22fb
--- /dev/null
+++ b/traffic_ops/traffic_ops_golang/crconfig/snapshot_test.go
@@ -0,0 +1,113 @@
+package crconfig
+
+/*
+ * 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 (
+ "reflect"
+ "encoding/json"
+ "testing"
+ "database/sql/driver"
+ "time"
+
+ "github.com/apache/incubator-trafficcontrol/lib/go-tc"
+
+ "gopkg.in/DATA-DOG/go-sqlmock.v1"
+)
+
+func ExpectedGetSnapshot(crc *tc.CRConfig) ([]byte, error) {
+ return json.Marshal(crc)
+}
+
+func MockGetSnapshot(mock sqlmock.Sqlmock, expected []byte, cdn string) {
+ rows := sqlmock.NewRows([]string{"snapshot"})
+ rows = rows.AddRow(expected)
+ mock.ExpectQuery("select").WithArgs(cdn).WillReturnRows(rows)
+}
+
+func TestGetSnapshot(t *testing.T) {
+ db, mock, err := sqlmock.New()
+ if err != nil {
+ t.Fatalf("an error '%s' was not expected when opening a stub
database connection", err)
+ }
+ defer db.Close()
+
+ cdn := "mycdn"
+
+ crc := &tc.CRConfig{}
+ crc.Stats.CDNName = &cdn
+ expected, err := ExpectedGetSnapshot(crc)
+ if err != nil {
+ t.Fatalf("GetSnapshot creating expected err expected: nil,
actual: %v", err)
+ }
+ MockGetSnapshot(mock, expected, cdn)
+
+ actual, exists, err := GetSnapshot(db, cdn)
+ if err != nil {
+ t.Fatalf("GetSnapshot err expected: nil, actual: %v", err)
+ }
+ if !exists {
+ t.Fatalf("GetSnapshot exists expected: true, actual: false")
+ }
+
+ if !reflect.DeepEqual(string(expected), actual) {
+ t.Errorf("GetSnapshot expected: %+v, actual: %+v",
string(expected), actual)
+ }
+}
+
+type AnyTime struct{}
+// Match satisfies sqlmock.Argument interface
+func (a AnyTime) Match(v driver.Value) bool {
+ _, ok := v.(time.Time)
+ return ok
+}
+
+type Any struct{}
+// Match satisfies sqlmock.Argument interface
+func (a Any) Match(v driver.Value) bool {
+ return true
+}
+
+
+func MockSnapshot(mock sqlmock.Sqlmock, expected []byte, cdn string) {
+ mock.ExpectExec("insert").WithArgs(cdn, expected,
AnyTime{}).WillReturnResult(sqlmock.NewResult(1, 1))
+}
+
+func TestSnapshot(t *testing.T) {
+ db, mock, err := sqlmock.New()
+ if err != nil {
+ t.Fatalf("an error '%s' was not expected when opening a stub
database connection", err)
+ }
+ defer db.Close()
+
+ cdn := "mycdn"
+
+ crc := &tc.CRConfig{}
+ crc.Stats.CDNName = &cdn
+
+ expected, err := ExpectedGetSnapshot(crc)
+ if err != nil {
+ t.Fatalf("GetSnapshot creating expected err expected: nil,
actual: %v", err)
+ }
+ MockSnapshot(mock, expected, cdn)
+
+ if err := Snapshot(db, crc); err != nil {
+ t.Fatalf("GetSnapshot err expected: nil, actual: %v", err)
+ }
+}
diff --git a/traffic_ops/traffic_ops_golang/crconfig/stats.go
b/traffic_ops/traffic_ops_golang/crconfig/stats.go
new file mode 100644
index 000000000..38a095936
--- /dev/null
+++ b/traffic_ops/traffic_ops_golang/crconfig/stats.go
@@ -0,0 +1,39 @@
+package crconfig
+
+/*
+ * 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"
+ "time"
+
+ "github.com/apache/incubator-trafficcontrol/lib/go-tc"
+)
+
+func makeStats(cdn string, user string, host string, path string, version
string, db *sql.DB) tc.CRConfigStats {
+ epoch := time.Now().Unix()
+ return tc.CRConfigStats{
+ CDNName: &cdn,
+ DateUnixSeconds: &epoch,
+ TMHost: &host,
+ TMPath: &path,
+ TMUser: &user,
+ TMVersion: &version,
+ }
+}
diff --git a/traffic_ops/traffic_ops_golang/crconfig/stats_test.go
b/traffic_ops/traffic_ops_golang/crconfig/stats_test.go
new file mode 100644
index 000000000..166df4e5f
--- /dev/null
+++ b/traffic_ops/traffic_ops_golang/crconfig/stats_test.go
@@ -0,0 +1,52 @@
+package crconfig
+
+/*
+ * 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 (
+ "reflect"
+ "time"
+ "testing"
+
+ "github.com/apache/incubator-trafficcontrol/lib/go-tc"
+)
+
+func ExpectedMakeStats() tc.CRConfigStats {
+ return tc.CRConfigStats{
+ CDNName: randStr(),
+ TMHost: randStr(),
+ TMPath: randStr(),
+ TMUser: randStr(),
+ TMVersion: randStr(),
+ }
+}
+
+func TestMakeStats(t *testing.T) {
+ expected := ExpectedMakeStats()
+ start := time.Now()
+ actual := makeStats(*expected.CDNName, *expected.TMUser,
*expected.TMHost, *expected.TMPath, *expected.TMVersion, nil)
+ end := time.Now()
+ expected.DateUnixSeconds = actual.DateUnixSeconds
+ if !reflect.DeepEqual(expected, actual) {
+ t.Errorf("makeStats expected: %+v, actual: %+v", expected,
actual)
+ }
+ if actual.DateUnixSeconds == nil || *actual.DateUnixSeconds <
start.Unix() || *actual.DateUnixSeconds > end.Unix() {
+ t.Errorf("makeStats DateUniSeconds expected: < %+v > %+v,
actual: %+v", start.Unix(), end.Unix(), actual.DateUnixSeconds)
+ }
+}
diff --git a/traffic_ops/traffic_ops_golang/riaksvc/riak_services.go
b/traffic_ops/traffic_ops_golang/riaksvc/riak_services.go
index 978c39f9d..a16c85286 100644
--- a/traffic_ops/traffic_ops_golang/riaksvc/riak_services.go
+++ b/traffic_ops/traffic_ops_golang/riaksvc/riak_services.go
@@ -28,6 +28,7 @@ import (
"time"
"github.com/apache/incubator-trafficcontrol/lib/go-tc"
+
"github.com/basho/riak-go-client"
"github.com/jmoiron/sqlx"
)
diff --git a/traffic_ops/traffic_ops_golang/routes.go
b/traffic_ops/traffic_ops_golang/routes.go
index 46f4fe0b4..16b8ab988 100644
--- a/traffic_ops/traffic_ops_golang/routes.go
+++ b/traffic_ops/traffic_ops_golang/routes.go
@@ -35,6 +35,8 @@ import (
"github.com/apache/incubator-trafficcontrol/traffic_ops/traffic_ops_golang/auth"
"github.com/apache/incubator-trafficcontrol/traffic_ops/traffic_ops_golang/cachegroup"
"github.com/apache/incubator-trafficcontrol/traffic_ops/traffic_ops_golang/cdn"
+
+
"github.com/apache/incubator-trafficcontrol/traffic_ops/traffic_ops_golang/crconfig"
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"
"github.com/apache/incubator-trafficcontrol/traffic_ops/traffic_ops_golang/division"
@@ -65,8 +67,8 @@ func handlerToFunc(handler http.Handler) http.HandlerFunc {
}
}
-// Routes returns the routes, and a catchall route for when no route matches.
-func Routes(d ServerData) ([]Route, http.Handler, error) {
+// Routes returns the API routes, raw non-API root level routes, and a
catchall route for when no route matches.
+func Routes(d ServerData) ([]Route, []RawRoute, http.Handler, error) {
proxyHandler := rootHandler(d)
routes := []Route{
@@ -218,8 +220,23 @@ func Routes(d ServerData) ([]Route, http.Handler, error) {
{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.2, http.MethodGet, `cdns/{cdn}/snapshot/?$`,
crconfig.SnapshotGetHandler(d.DB, d.Config), crconfig.PrivLevel, Authenticated,
nil},
+ {1.2, http.MethodGet, `cdns/{cdn}/snapshot/new/?$`,
crconfig.Handler(d.DB, d.Config), crconfig.PrivLevel, Authenticated, nil},
+ {1.2, http.MethodPut, `cdns/{id}/snapshot/?$`,
crconfig.SnapshotHandler(d.DB, d.Config), crconfig.PrivLevel, Authenticated,
nil},
+ {1.2, http.MethodPut, `snapshot/{cdn}/?$`,
crconfig.SnapshotHandler(d.DB, d.Config), crconfig.PrivLevel, Authenticated,
nil},
}
- return routes, proxyHandler, 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.
+ rawRoutes := []RawRoute{
+ // DEPRECATED - use PUT /api/1.2/snapshot/{cdn}
+ {http.MethodGet, `tools/write_crconfig/{cdn}/?$`,
crconfig.SnapshotOldGUIHandler(d.DB, d.Config), crconfig.PrivLevel,
Authenticated, nil},
+ // DEPRECATED - use GET /api/1.2/cdns/{cdn}/snapshot
+ {http.MethodGet, `CRConfig-Snapshots/{cdn}/CRConfig.json?$`,
crconfig.SnapshotGetHandler(d.DB, d.Config), crconfig.PrivLevel, Authenticated,
nil},
+ }
+
+ return routes, rawRoutes, proxyHandler, nil
}
// RootHandler returns the / handler for the service, which reverse-proxies
the old Perl Traffic Ops
diff --git a/traffic_ops/traffic_ops_golang/routing.go
b/traffic_ops/traffic_ops_golang/routing.go
index 2fce1bbad..7a36a46c3 100644
--- a/traffic_ops/traffic_ops_golang/routing.go
+++ b/traffic_ops/traffic_ops_golang/routing.go
@@ -21,6 +21,7 @@ package main
import (
"context"
+ "fmt"
"net/http"
"regexp"
"sort"
@@ -28,11 +29,9 @@ import (
"strings"
"github.com/apache/incubator-trafficcontrol/lib/go-log"
-
- "fmt"
-
"github.com/apache/incubator-trafficcontrol/traffic_ops/traffic_ops_golang/api"
"github.com/apache/incubator-trafficcontrol/traffic_ops/traffic_ops_golang/config"
+
"github.com/jmoiron/sqlx"
)
@@ -54,6 +53,17 @@ type Route struct {
Middlewares []Middleware
}
+// RawRoute is an HTTP route to be served at the root, rather than under
/api/version. Raw Routes should be rare, and almost exclusively converted old
Perl routes which have yet to be moved to an API path.
+type RawRoute struct {
+ // Order matters! Do not reorder this! Routes() uses positional
construction for readability.
+ Method string
+ Path string
+ Handler http.HandlerFunc
+ RequiredPrivLevel int
+ Authenticated bool
+ Middlewares []Middleware
+}
+
func getDefaultMiddleware() []Middleware {
return []Middleware{wrapHeaders}
}
@@ -91,7 +101,7 @@ type PathHandler struct {
}
// CreateRouteMap returns a map of methods to a slice of paths and handlers;
wrapping the handlers in the appropriate middleware. Uses Semantic Versioning:
routes are added to every subsequent minor version, but not subsequent major
versions. For example, a 1.2 route is added to 1.3 but not 2.1. Also truncates
'2.0' to '2', creating succinct major versions.
-func CreateRouteMap(rs []Route, authBase AuthBase) map[string][]PathHandler {
+func CreateRouteMap(rs []Route, rawRoutes []RawRoute, authBase AuthBase)
map[string][]PathHandler {
// TODO strong types for method, path
versions := getSortedRouteVersions(rs)
m := map[string][]PathHandler{}
@@ -104,25 +114,30 @@ func CreateRouteMap(rs []Route, authBase AuthBase)
map[string][]PathHandler {
}
vstr := strconv.FormatFloat(version, 'f', -1, 64)
path := RoutePrefix + "/" + vstr + "/" + r.Path
-
- middlewares := r.Middlewares
-
- if middlewares == nil {
- middlewares = getDefaultMiddleware()
- }
- if r.Authenticated { //a privLevel of zero is an
unauthenticated endpoint.
- authWrapper :=
authBase.GetWrapper(r.RequiredPrivLevel)
- middlewares = append([]Middleware{authWrapper},
middlewares...)
- }
-
+ middlewares := getRouteMiddleware(r.Middlewares,
authBase, r.Authenticated, r.RequiredPrivLevel)
m[r.Method] = append(m[r.Method], PathHandler{Path:
path, Handler: use(r.Handler, middlewares)})
-
log.Infof("adding route %v %v\n", r.Method, path)
}
}
+ for _, r := range rawRoutes {
+ middlewares := getRouteMiddleware(r.Middlewares, authBase,
r.Authenticated, r.RequiredPrivLevel)
+ m[r.Method] = append(m[r.Method], PathHandler{Path: r.Path,
Handler: use(r.Handler, middlewares)})
+ log.Infof("adding raw route %v %v\n", r.Method, r.Path)
+ }
return m
}
+func getRouteMiddleware(middlewares []Middleware, authBase AuthBase,
authenticated bool, privLevel int) []Middleware {
+ if middlewares == nil {
+ middlewares = getDefaultMiddleware()
+ }
+ if authenticated { // a privLevel of zero is an unauthenticated
endpoint.
+ authWrapper := authBase.GetWrapper(privLevel)
+ middlewares = append([]Middleware{authWrapper}, middlewares...)
+ }
+ return middlewares
+}
+
// CompileRoutes - takes a map of methods to paths and handlers, and returns a
map of methods to CompiledRoutes
func CompileRoutes(routes map[string][]PathHandler) map[string][]CompiledRoute
{
compiledRoutes := map[string][]CompiledRoute{}
@@ -180,7 +195,7 @@ func Handler(routes map[string][]CompiledRoute, catchall
http.Handler, w http.Re
// RegisterRoutes - parses the routes and registers the handlers with the Go
Router
func RegisterRoutes(d ServerData) error {
- routeSlice, catchall, err := Routes(d)
+ routeSlice, rawRoutes, catchall, err := Routes(d)
if err != nil {
return err
}
@@ -191,7 +206,7 @@ func RegisterRoutes(d ServerData) error {
}
authBase := AuthBase{secret: d.Config.Secrets[0],
getCurrentUserInfoStmt: userInfoStmt, override: nil} //we know d.Config.Secrets
is a slice of at least one or start up would fail.
- routes := CreateRouteMap(routeSlice, authBase)
+ routes := CreateRouteMap(routeSlice, rawRoutes, authBase)
compiledRoutes := CompileRoutes(routes)
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
Handler(compiledRoutes, catchall, w, r)
diff --git a/traffic_ops/traffic_ops_golang/routing_test.go
b/traffic_ops/traffic_ops_golang/routing_test.go
index 8537aa18d..32a761e5f 100644
--- a/traffic_ops/traffic_ops_golang/routing_test.go
+++ b/traffic_ops/traffic_ops_golang/routing_test.go
@@ -68,7 +68,8 @@ func TestCreateRouteMap(t *testing.T) {
{1.2, http.MethodGet, `path3`, PathThreeHandler, 0, false,
[]Middleware{}},
}
- routeMap := CreateRouteMap(routes, authBase)
+ rawRoutes := []RawRoute{}
+ routeMap := CreateRouteMap(routes, rawRoutes, authBase)
route1Handler := routeMap["GET"][0].Handler
diff --git a/traffic_ops/traffic_ops_golang/traffic_ops_golang.go
b/traffic_ops/traffic_ops_golang/traffic_ops_golang.go
index a361e14de..9de1055aa 100644
--- a/traffic_ops/traffic_ops_golang/traffic_ops_golang.go
+++ b/traffic_ops/traffic_ops_golang/traffic_ops_golang.go
@@ -62,7 +62,8 @@ func main() {
var cfg config.Config
var err error
var errorToLog error
- if cfg, err = config.LoadConfig(*configFileName, *dbConfigFileName,
*riakConfigFileName); err != nil {
+
+ if cfg, err = config.LoadConfig(*configFileName, *dbConfigFileName,
*riakConfigFileName, version); err != nil {
if !strings.Contains(err.Error(), "riak conf") {
fmt.Println("Error loading config: " + err.Error())
return
----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on GitHub and use the
URL above to go to the specific comment.
For queries about this service, please contact Infrastructure at:
[email protected]
With regards,
Apache Git Services