You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@trafficcontrol.apache.org by oc...@apache.org on 2021/01/13 16:12:02 UTC

[trafficcontrol] 01/03: Add atscfg parent.config topology comment (#5414)

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

ocket8888 pushed a commit to branch 5.0.x
in repository https://gitbox.apache.org/repos/asf/trafficcontrol.git

commit a2ee75cb452deebb9b1e1ded6a1a16b19248d9dd
Author: Robert O Butts <ro...@users.noreply.github.com>
AuthorDate: Wed Jan 13 08:33:46 2021 -0700

    Add atscfg parent.config topology comment (#5414)
    
    (cherry picked from commit c08571586b651372435b627eedcabef479f69519)
---
 lib/go-atscfg/parentdotconfig.go             |  50 +++-
 lib/go-atscfg/parentdotconfig_test.go        | 346 ++++++++++++++++++++++++---
 traffic_ops_ort/atstccfg/cfgfile/wrappers.go |   5 +-
 3 files changed, 365 insertions(+), 36 deletions(-)

diff --git a/lib/go-atscfg/parentdotconfig.go b/lib/go-atscfg/parentdotconfig.go
index 082e1a7..a6e94ac 100644
--- a/lib/go-atscfg/parentdotconfig.go
+++ b/lib/go-atscfg/parentdotconfig.go
@@ -66,6 +66,19 @@ const ParentConfigCacheParamNotAParent = "not_a_parent"
 type OriginHost string
 type OriginFQDN string
 
+// ParentConfigOpts contains settings to configure parent.config generation options.
+type ParentConfigOpts struct {
+	// AddComments is whether to add informative comments to the generated file, about what was generated and why.
+	// Note this does not include the header comment, which is configured separately with HdrComment.
+	// These comments are human-readable and not guarnateed to be consistent between versions. Automating anything based on them is strongly discouraged.
+	AddComments bool
+
+	// HdrComment is the header comment to include at the beginning of the file.
+	// This should be the text desired, without comment syntax (like # or //). The file's comment syntax will be added.
+	// To omit the header comment, pass the empty string.
+	HdrComment string
+}
+
 func MakeParentDotConfig(
 	dses []DeliveryService,
 	server *Server,
@@ -78,7 +91,7 @@ func MakeParentDotConfig(
 	cacheGroupArr []tc.CacheGroupNullable,
 	dss []tc.DeliveryServiceServer,
 	cdn *tc.CDN,
-	hdrComment string,
+	opt ParentConfigOpts,
 ) (Cfg, error) {
 	warnings := []string{}
 
@@ -110,7 +123,10 @@ func MakeParentDotConfig(
 
 	sort.Sort(dsesSortByName(dses))
 
-	hdr := makeHdrComment(hdrComment)
+	hdr := ""
+	if opt.HdrComment != "" {
+		hdr = makeHdrComment(opt.HdrComment)
+	}
 
 	textArr := []string{}
 	processedOriginsToDSNames := map[string]tc.DeliveryServiceName{}
@@ -299,11 +315,12 @@ func MakeParentDotConfig(
 				dsParams,
 				atsMajorVer,
 				dsOrigins[DeliveryServiceID(*ds.ID)],
+				opt.AddComments,
 			)
 			warnings = append(warnings, topoWarnings...)
 			if err != nil {
 				// we don't want to fail generation with an error if one ds is malformed
-				warnings = append(warnings, err.Error()) // GetTopologyParentConfigLine includes error context
+				warnings = append(warnings, err.Error()) // getTopologyParentConfigLine includes error context
 				continue
 			}
 
@@ -330,8 +347,10 @@ func MakeParentDotConfig(
 				if parentSelectAlg := serverParams[ParentConfigParamAlgorithm]; strings.TrimSpace(parentSelectAlg) != "" {
 					algorithm = "round_robin=" + parentSelectAlg
 				}
+				textLine += makeParentComment(opt.AddComments, *ds.XMLID, "")
 				textLine += "dest_domain=" + orgURI.Hostname() + " port=" + orgURI.Port() + " parent=" + *ds.OriginShield + " " + algorithm + " go_direct=true\n"
 			} else if ds.MultiSiteOrigin != nil && *ds.MultiSiteOrigin {
+				textLine += makeParentComment(opt.AddComments, *ds.XMLID, "")
 				textLine += "dest_domain=" + orgURI.Hostname() + " port=" + orgURI.Port() + " "
 				if len(parentInfos) == 0 {
 				}
@@ -343,9 +362,11 @@ func MakeParentDotConfig(
 
 				parents, secondaryParents, parentWarns := getMSOParentStrs(&ds, parentInfos[OriginHost(orgURI.Hostname())], atsMajorVer, dsParams.Algorithm, dsParams.TryAllPrimariesBeforeSecondary)
 				warnings = append(warnings, parentWarns...)
+
 				textLine += parents + secondaryParents + ` round_robin=` + dsParams.Algorithm + ` qstring=` + parentQStr + ` go_direct=false parent_is_proxy=false`
 				textLine += getParentRetryStr(true, atsMajorVer, dsParams.ParentRetry, dsParams.UnavailableServerRetryResponses, dsParams.MaxSimpleRetries, dsParams.MaxUnavailableServerRetries)
 				textLine += "\n" // TODO remove, and join later on "\n" instead of ""?
+
 				textArr = append(textArr, textLine)
 			}
 		} else {
@@ -365,6 +386,7 @@ func MakeParentDotConfig(
 				continue
 			}
 
+			text += makeParentComment(opt.AddComments, *ds.XMLID, "")
 			// TODO encode this in a DSType func, IsGoDirect() ?
 			if *ds.Type == tc.DSTypeHTTPNoCache || *ds.Type == tc.DSTypeHTTPLive || *ds.Type == tc.DSTypeDNSLive {
 				text += `dest_domain=` + orgURI.Hostname() + ` port=` + orgURI.Port() + ` go_direct=true` + "\n"
@@ -392,6 +414,7 @@ func MakeParentDotConfig(
 
 				text += `dest_domain=` + orgURI.Hostname() + ` port=` + orgURI.Port() + ` ` + parents + ` ` + secondaryParents + ` ` + roundRobin + ` ` + goDirect + ` qstring=` + parentQStr + "\n"
 			}
+
 			textArr = append(textArr, text)
 		}
 		processedOriginsToDSNames[*ds.OrgServerFQDN] = tc.DeliveryServiceName(*ds.XMLID)
@@ -418,7 +441,10 @@ func MakeParentDotConfig(
 	}
 
 	sort.Sort(sort.StringSlice(textArr))
-	text := hdr + strings.Join(textArr, "") + defaultDestText
+	text := hdr + strings.Join(textArr, "")
+
+	text += makeParentComment(opt.AddComments, "", "") + defaultDestText
+
 	return Cfg{
 		Text:        text,
 		ContentType: ContentTypeParentDotConfig,
@@ -427,6 +453,17 @@ func MakeParentDotConfig(
 	}, nil
 }
 
+// makeParentComment creates the parent line comment and returns it.
+// If addComments is false, returns the empty string. This exists for composability.
+// Either dsName or topology may be the empty string.
+// The returned comment includes a trailing newline.
+func makeParentComment(addComments bool, dsName string, topology string) string {
+	if !addComments {
+		return ""
+	}
+	return "# ds '" + dsName + "' topology '" + topology + "'" + "\n"
+}
+
 type parentConfigDS struct {
 	Name                 tc.DeliveryServiceName
 	QStringIgnore        tc.QStringIgnore
@@ -695,7 +732,7 @@ func getParentDSParams(ds DeliveryService, profileParentConfigParams map[string]
 	return params, warnings
 }
 
-// GetTopologyParentConfigLine returns the topology parent.config line, any warnings, and any error
+// getTopologyParentConfigLine returns the topology parent.config line, any warnings, and any error
 func getTopologyParentConfigLine(
 	server *Server,
 	servers []Server,
@@ -709,6 +746,7 @@ func getTopologyParentConfigLine(
 	dsParams parentDSParams,
 	atsMajorVer int,
 	dsOrigins map[ServerID]struct{},
+	addComments bool,
 ) (string, []string, error) {
 	warnings := []string{}
 	txt := ""
@@ -728,6 +766,7 @@ func getTopologyParentConfigLine(
 		return "", warnings, errors.New("DS " + *ds.XMLID + " topology '" + *ds.Topology + "' not found in Topologies!")
 	}
 
+	txt += makeParentComment(addComments, *ds.XMLID, *ds.Topology)
 	txt += "dest_domain=" + orgURI.Hostname() + " port=" + orgURI.Port()
 
 	serverPlacement, err := getTopologyPlacement(tc.CacheGroupName(*server.Cachegroup), topology, cacheGroups, ds)
@@ -762,6 +801,7 @@ func getTopologyParentConfigLine(
 	txt += getTopologyParentIsProxyStr(serverPlacement.IsLastCacheTier)
 	txt += getParentRetryStr(serverPlacement.IsLastCacheTier, atsMajorVer, dsParams.ParentRetry, dsParams.UnavailableServerRetryResponses, dsParams.MaxSimpleRetries, dsParams.MaxUnavailableServerRetries)
 	txt += "\n"
+
 	return txt, warnings, nil
 }
 
diff --git a/lib/go-atscfg/parentdotconfig_test.go b/lib/go-atscfg/parentdotconfig_test.go
index 2c2b342..f888b2b 100644
--- a/lib/go-atscfg/parentdotconfig_test.go
+++ b/lib/go-atscfg/parentdotconfig_test.go
@@ -28,7 +28,7 @@ import (
 )
 
 func TestMakeParentDotConfig(t *testing.T) {
-	hdr := "myHeaderComment"
+	hdr := ParentConfigOpts{AddComments: false, HdrComment: "myHeaderComment"}
 
 	ds0 := makeParentDS()
 	ds0Type := tc.DSTypeHTTP
@@ -132,7 +132,7 @@ func TestMakeParentDotConfig(t *testing.T) {
 	}
 	txt := cfg.Text
 
-	testComment(t, txt, hdr)
+	testComment(t, txt, hdr.HdrComment)
 
 	if !strings.Contains(txt, "dest_domain=ds0.example.net") {
 		t.Errorf("expected parent 'dest_domain=ds0.example.net', actual: '%v'", txt)
@@ -146,7 +146,7 @@ func TestMakeParentDotConfig(t *testing.T) {
 }
 
 func TestMakeParentDotConfigCapabilities(t *testing.T) {
-	hdr := "myHeaderComment"
+	hdr := ParentConfigOpts{AddComments: false, HdrComment: "myHeaderComment"}
 
 	ds0 := makeParentDS()
 	ds0Type := tc.DSTypeHTTP
@@ -254,7 +254,7 @@ func TestMakeParentDotConfigCapabilities(t *testing.T) {
 	}
 	txt := cfg.Text
 
-	testComment(t, txt, hdr)
+	testComment(t, txt, hdr.HdrComment)
 
 	lines := strings.Split(txt, "\n")
 
@@ -290,7 +290,7 @@ func TestMakeParentDotConfigCapabilities(t *testing.T) {
 }
 
 func TestMakeParentDotConfigMSOSecondaryParent(t *testing.T) {
-	hdr := "myHeaderComment"
+	hdr := ParentConfigOpts{AddComments: false, HdrComment: "myHeaderComment"}
 
 	ds0 := makeParentDS()
 	ds0Type := tc.DSTypeHTTP
@@ -395,7 +395,7 @@ func TestMakeParentDotConfigMSOSecondaryParent(t *testing.T) {
 	}
 	txt := cfg.Text
 
-	testComment(t, txt, hdr)
+	testComment(t, txt, hdr.HdrComment)
 
 	txtx := strings.Replace(txt, " ", "", -1)
 
@@ -405,7 +405,7 @@ func TestMakeParentDotConfigMSOSecondaryParent(t *testing.T) {
 }
 
 func TestMakeParentDotConfigTopologies(t *testing.T) {
-	hdr := "myHeaderComment"
+	hdr := ParentConfigOpts{AddComments: false, HdrComment: "myHeaderComment"}
 
 	ds0 := makeParentDS()
 	ds0Type := tc.DSTypeHTTP
@@ -528,7 +528,7 @@ func TestMakeParentDotConfigTopologies(t *testing.T) {
 	}
 	txt := cfg.Text
 
-	testComment(t, txt, hdr)
+	testComment(t, txt, hdr.HdrComment)
 
 	if !strings.Contains(txt, "dest_domain=ds0.example.net") {
 		t.Errorf("expected parent 'dest_domain=ds0.example.net', actual: '%v'", txt)
@@ -547,7 +547,7 @@ func TestMakeParentDotConfigTopologies(t *testing.T) {
 
 // TestMakeParentDotConfigNotInTopologies tests when a given edge is NOT in a Topology, that it doesn't add a remap line.
 func TestMakeParentDotConfigNotInTopologies(t *testing.T) {
-	hdr := "myHeaderComment"
+	hdr := ParentConfigOpts{AddComments: false, HdrComment: "myHeaderComment"}
 
 	ds0 := makeParentDS()
 	ds0Type := tc.DSTypeHTTP
@@ -666,7 +666,7 @@ func TestMakeParentDotConfigNotInTopologies(t *testing.T) {
 	}
 	txt := cfg.Text
 
-	testComment(t, txt, hdr)
+	testComment(t, txt, hdr.HdrComment)
 
 	if strings.Contains(txt, "dest_domain=ds0.example.net") {
 		t.Errorf("expected parent 'dest_domain=ds0.example.net' to NOT contain Topology DS without this edge: '%v'", txt)
@@ -677,7 +677,7 @@ func TestMakeParentDotConfigNotInTopologies(t *testing.T) {
 }
 
 func TestMakeParentDotConfigTopologiesCapabilities(t *testing.T) {
-	hdr := "myHeaderComment"
+	hdr := ParentConfigOpts{AddComments: false, HdrComment: "myHeaderComment"}
 
 	ds0 := makeParentDS()
 	ds0.ID = util.IntPtr(42)
@@ -821,7 +821,7 @@ func TestMakeParentDotConfigTopologiesCapabilities(t *testing.T) {
 	}
 	txt := cfg.Text
 
-	testComment(t, txt, hdr)
+	testComment(t, txt, hdr.HdrComment)
 
 	if !strings.Contains(txt, "dest_domain=ds0.example.net") {
 		t.Errorf("expected parent 'dest_domain=ds0.example.net' without required capabilities: '%v'", txt)
@@ -835,7 +835,7 @@ func TestMakeParentDotConfigTopologiesCapabilities(t *testing.T) {
 }
 
 func TestMakeParentDotConfigTopologiesOmitOfflineParents(t *testing.T) {
-	hdr := "myHeaderComment"
+	hdr := ParentConfigOpts{AddComments: false, HdrComment: "myHeaderComment"}
 
 	ds0 := makeParentDS()
 	ds0Type := tc.DSTypeHTTP
@@ -960,7 +960,7 @@ func TestMakeParentDotConfigTopologiesOmitOfflineParents(t *testing.T) {
 	}
 	txt := cfg.Text
 
-	testComment(t, txt, hdr)
+	testComment(t, txt, hdr.HdrComment)
 
 	if !strings.Contains(txt, "dest_domain=ds0.example.net") {
 		t.Errorf("expected parent 'dest_domain=ds0.example.net', actual: '%v'", txt)
@@ -978,7 +978,7 @@ func TestMakeParentDotConfigTopologiesOmitOfflineParents(t *testing.T) {
 }
 
 func TestMakeParentDotConfigTopologiesOmitDifferentCDNParents(t *testing.T) {
-	hdr := "myHeaderComment"
+	hdr := ParentConfigOpts{AddComments: false, HdrComment: "myHeaderComment"}
 
 	ds0 := makeParentDS()
 	ds0Type := tc.DSTypeHTTP
@@ -1104,7 +1104,7 @@ func TestMakeParentDotConfigTopologiesOmitDifferentCDNParents(t *testing.T) {
 	}
 	txt := cfg.Text
 
-	testComment(t, txt, hdr)
+	testComment(t, txt, hdr.HdrComment)
 
 	if !strings.Contains(txt, "dest_domain=ds0.example.net") {
 		t.Errorf("expected parent 'dest_domain=ds0.example.net', actual: '%v'", txt)
@@ -1122,7 +1122,7 @@ func TestMakeParentDotConfigTopologiesOmitDifferentCDNParents(t *testing.T) {
 }
 
 func TestMakeParentDotConfigTopologiesMSO(t *testing.T) {
-	hdr := "myHeaderComment"
+	hdr := ParentConfigOpts{AddComments: false, HdrComment: "myHeaderComment"}
 
 	ds1 := makeParentDS()
 	ds1.ID = util.IntPtr(43)
@@ -1240,7 +1240,7 @@ func TestMakeParentDotConfigTopologiesMSO(t *testing.T) {
 	}
 	txt := cfg.Text
 
-	testComment(t, txt, hdr)
+	testComment(t, txt, hdr.HdrComment)
 
 	if !strings.Contains(txt, "dest_domain=ds1.example.net") {
 		t.Errorf("expected parent 'dest_domain=ds1.example.net', actual: '%v'", txt)
@@ -1254,7 +1254,7 @@ func TestMakeParentDotConfigTopologiesMSO(t *testing.T) {
 }
 
 func TestMakeParentDotConfigTopologiesMSOWithCapabilities(t *testing.T) {
-	hdr := "myHeaderComment"
+	hdr := ParentConfigOpts{AddComments: false, HdrComment: "myHeaderComment"}
 
 	ds1 := makeParentDS()
 	ds1.ID = util.IntPtr(43)
@@ -1381,7 +1381,7 @@ func TestMakeParentDotConfigTopologiesMSOWithCapabilities(t *testing.T) {
 	}
 	txt := cfg.Text
 
-	testComment(t, txt, hdr)
+	testComment(t, txt, hdr.HdrComment)
 
 	if !strings.Contains(txt, "dest_domain=ds1.example.net") {
 		t.Errorf("expected parent 'dest_domain=ds1.example.net', actual: '%v'", txt)
@@ -1395,7 +1395,7 @@ func TestMakeParentDotConfigTopologiesMSOWithCapabilities(t *testing.T) {
 }
 
 func TestMakeParentDotConfigMSOWithCapabilities(t *testing.T) {
-	hdr := "myHeaderComment"
+	hdr := ParentConfigOpts{AddComments: false, HdrComment: "myHeaderComment"}
 
 	ds1 := makeParentDS()
 	ds1.ID = util.IntPtr(43)
@@ -1508,7 +1508,7 @@ func TestMakeParentDotConfigMSOWithCapabilities(t *testing.T) {
 	}
 	txt := cfg.Text
 
-	testComment(t, txt, hdr)
+	testComment(t, txt, hdr.HdrComment)
 
 	if !strings.Contains(txt, "dest_domain=ds1.example.net") {
 		t.Errorf("expected parent 'dest_domain=ds1.example.net', actual: '%v'", txt)
@@ -1522,7 +1522,7 @@ func TestMakeParentDotConfigMSOWithCapabilities(t *testing.T) {
 }
 
 func TestMakeParentDotConfigTopologiesMSOParams(t *testing.T) {
-	hdr := "myHeaderComment"
+	hdr := ParentConfigOpts{AddComments: false, HdrComment: "myHeaderComment"}
 
 	ds1 := makeParentDS()
 	ds1.ID = util.IntPtr(43)
@@ -1672,7 +1672,7 @@ func TestMakeParentDotConfigTopologiesMSOParams(t *testing.T) {
 	}
 	txt := cfg.Text
 
-	testComment(t, txt, hdr)
+	testComment(t, txt, hdr.HdrComment)
 
 	if !strings.Contains(txt, "dest_domain=ds1.example.net") {
 		t.Errorf("expected parent 'dest_domain=ds1.example.net', actual: '%v'", txt)
@@ -1698,7 +1698,7 @@ func TestMakeParentDotConfigTopologiesMSOParams(t *testing.T) {
 }
 
 func TestMakeParentDotConfigTopologiesParams(t *testing.T) {
-	hdr := "myHeaderComment"
+	hdr := ParentConfigOpts{AddComments: false, HdrComment: "myHeaderComment"}
 
 	ds1 := makeParentDS()
 	ds1.ID = util.IntPtr(43)
@@ -1848,7 +1848,7 @@ func TestMakeParentDotConfigTopologiesParams(t *testing.T) {
 	}
 	txt := cfg.Text
 
-	testComment(t, txt, hdr)
+	testComment(t, txt, hdr.HdrComment)
 
 	if !strings.Contains(txt, "dest_domain=ds1.example.net") {
 		t.Errorf("expected parent 'dest_domain=ds1.example.net', actual: '%v'", txt)
@@ -1875,7 +1875,7 @@ func TestMakeParentDotConfigTopologiesParams(t *testing.T) {
 
 func TestMakeParentDotConfigSecondaryMode(t *testing.T) {
 
-	hdr := "myHeaderComment"
+	hdr := ParentConfigOpts{AddComments: false, HdrComment: "myHeaderComment"}
 
 	ds0 := makeParentDS()
 	ds0Type := tc.DSTypeHTTP
@@ -2019,7 +2019,7 @@ func TestMakeParentDotConfigSecondaryMode(t *testing.T) {
 	}
 	txt := cfg.Text
 
-	testComment(t, txt, hdr)
+	testComment(t, txt, hdr.HdrComment)
 
 	if !strings.Contains(txt, "dest_domain=ds0.example.net") {
 		t.Errorf("expected parent 'dest_domain=ds0.example.net', actual: '%v'", txt)
@@ -2036,7 +2036,7 @@ func TestMakeParentDotConfigSecondaryMode(t *testing.T) {
 }
 
 func TestMakeParentDotConfigNoSecondaryMode(t *testing.T) {
-	hdr := "myHeaderComment"
+	hdr := ParentConfigOpts{AddComments: false, HdrComment: "myHeaderComment"}
 
 	ds0 := makeParentDS()
 	ds0Type := tc.DSTypeHTTP
@@ -2174,7 +2174,7 @@ func TestMakeParentDotConfigNoSecondaryMode(t *testing.T) {
 	}
 	txt := cfg.Text
 
-	testComment(t, txt, hdr)
+	testComment(t, txt, hdr.HdrComment)
 
 	if !strings.Contains(txt, "dest_domain=ds0.example.net") {
 		t.Errorf("expected parent 'dest_domain=ds0.example.net', actual: '%v'", txt)
@@ -2188,6 +2188,292 @@ func TestMakeParentDotConfigNoSecondaryMode(t *testing.T) {
 	if strings.Contains(txt, "secondary_mode") {
 		t.Errorf("expected no secondary_mode for DSes without ParentConfigParamSecondaryMode parameter, actual: '%v'", txt)
 	}
+
+	if strings.Contains(txt, `topology 't0'`) {
+		t.Errorf("expected no comment with topology name, actual: '%v'", txt)
+	}
+	if strings.Contains(txt, `ds 'ds1'`) {
+		t.Errorf("expected no comment with delivery service name, actual: '%v'", txt)
+	}
+}
+
+func TestMakeParentDotConfigComments(t *testing.T) {
+	hdr := ParentConfigOpts{AddComments: true, HdrComment: "myHeaderComment"}
+
+	ds0 := makeParentDS()
+	ds0Type := tc.DSTypeHTTP
+	ds0.Type = &ds0Type
+	ds0.QStringIgnore = util.IntPtr(int(tc.QStringIgnoreUseInCacheKeyAndPassUp))
+	ds0.OrgServerFQDN = util.StrPtr("http://ds0.example.net")
+
+	ds1 := makeParentDS()
+	ds1.ID = util.IntPtr(43)
+	ds1Type := tc.DSTypeDNS
+	ds1.Type = &ds1Type
+	ds1.QStringIgnore = util.IntPtr(int(tc.QStringIgnoreDrop))
+	ds1.OrgServerFQDN = util.StrPtr("http://ds1.example.net")
+
+	dses := []DeliveryService{*ds0, *ds1}
+
+	parentConfigParams := []tc.Parameter{
+		{
+			Name:       ParentConfigParamQStringHandling,
+			ConfigFile: "parent.config",
+			Value:      "myQStringHandlingParam",
+			Profiles:   []byte(`["serverprofile"]`),
+		},
+		{
+			Name:       ParentConfigParamAlgorithm,
+			ConfigFile: "parent.config",
+			Value:      tc.AlgorithmConsistentHash,
+			Profiles:   []byte(`["serverprofile"]`),
+		},
+		{
+			Name:       ParentConfigParamQString,
+			ConfigFile: "parent.config",
+			Value:      "myQstringParam",
+			Profiles:   []byte(`["serverprofile"]`),
+		},
+	}
+
+	serverParams := []tc.Parameter{
+		{
+			Name:       "trafficserver",
+			ConfigFile: "package",
+			Value:      "7",
+			Profiles:   []byte(`["global"]`),
+		},
+	}
+
+	server := makeTestParentServer()
+
+	mid0 := makeTestParentServer()
+	mid0.Cachegroup = util.StrPtr("midCG")
+	mid0.HostName = util.StrPtr("mymid0")
+	mid0.ID = util.IntPtr(45)
+	setIP(mid0, "192.168.2.2")
+
+	mid1 := makeTestParentServer()
+	mid1.Cachegroup = util.StrPtr("midCG")
+	mid1.HostName = util.StrPtr("mymid1")
+	mid1.ID = util.IntPtr(46)
+	setIP(mid1, "192.168.2.3")
+
+	servers := []Server{*server, *mid0, *mid1}
+
+	topologies := []tc.Topology{}
+	serverCapabilities := map[int]map[ServerCapability]struct{}{}
+	dsRequiredCapabilities := map[int]map[ServerCapability]struct{}{}
+
+	eCG := &tc.CacheGroupNullable{}
+	eCG.Name = server.Cachegroup
+	eCG.ID = server.CachegroupID
+	eCG.ParentName = mid0.Cachegroup
+	eCG.ParentCachegroupID = mid0.CachegroupID
+	eCGType := tc.CacheGroupEdgeTypeName
+	eCG.Type = &eCGType
+
+	mCG := &tc.CacheGroupNullable{}
+	mCG.Name = mid0.Cachegroup
+	mCG.ID = mid0.CachegroupID
+	mCGType := tc.CacheGroupMidTypeName
+	mCG.Type = &mCGType
+
+	cgs := []tc.CacheGroupNullable{*eCG, *mCG}
+
+	dss := []tc.DeliveryServiceServer{
+		{
+			Server:          util.IntPtr(*server.ID),
+			DeliveryService: util.IntPtr(*ds0.ID),
+		},
+		{
+			Server:          util.IntPtr(*server.ID),
+			DeliveryService: util.IntPtr(*ds1.ID),
+		},
+	}
+	cdn := &tc.CDN{
+		DomainName: "cdndomain.example",
+		Name:       "my-cdn-name",
+	}
+
+	cfg, err := MakeParentDotConfig(dses, server, servers, topologies, serverParams, parentConfigParams, serverCapabilities, dsRequiredCapabilities, cgs, dss, cdn, hdr)
+	if err != nil {
+		t.Fatal(err)
+	}
+	txt := cfg.Text
+
+	testComment(t, txt, hdr.HdrComment)
+
+	if !strings.Contains(txt, "dest_domain=ds0.example.net") {
+		t.Errorf("expected parent 'dest_domain=ds0.example.net', actual: '%v'", txt)
+	}
+	if !strings.Contains(txt, "dest_domain=ds1.example.net") {
+		t.Errorf("expected parent 'dest_domain=ds0.example.net', actual: '%v'", txt)
+	}
+	if !strings.Contains(txt, "qstring=myQStringHandlingParam") {
+		t.Errorf("expected qstring from param 'qstring=myQStringHandlingParam', actual: '%v'", txt)
+	}
+	if !strings.Contains(txt, "# ds 'ds1'") {
+		t.Errorf("expected comment with delivery service name, actual: '%v'", txt)
+	}
+}
+
+func TestMakeParentDotConfigCommentTopology(t *testing.T) {
+	hdr := ParentConfigOpts{AddComments: true, HdrComment: "myHeaderComment"}
+
+	ds0 := makeParentDS()
+	ds0Type := tc.DSTypeHTTP
+	ds0.Type = &ds0Type
+	ds0.QStringIgnore = util.IntPtr(int(tc.QStringIgnoreUseInCacheKeyAndPassUp))
+	ds0.OrgServerFQDN = util.StrPtr("http://ds0.example.net")
+	ds0.ProfileID = util.IntPtr(311)
+	ds0.ProfileName = util.StrPtr("ds0Profile")
+
+	ds1 := makeParentDS()
+	ds1.ID = util.IntPtr(43)
+	ds1Type := tc.DSTypeDNS
+	ds1.Type = &ds1Type
+	ds1.QStringIgnore = util.IntPtr(int(tc.QStringIgnoreDrop))
+	ds1.OrgServerFQDN = util.StrPtr("http://ds1.example.net")
+	ds1.Topology = util.StrPtr("t0")
+	ds1.ProfileID = util.IntPtr(312)
+	ds1.ProfileName = util.StrPtr("ds1Profile")
+
+	dses := []DeliveryService{*ds0, *ds1}
+
+	parentConfigParams := []tc.Parameter{
+		{
+			Name:       ParentConfigParamQStringHandling,
+			ConfigFile: "parent.config",
+			Value:      "myQStringHandlingParam",
+			Profiles:   []byte(`["serverprofile"]`),
+		},
+		{
+			Name:       ParentConfigParamAlgorithm,
+			ConfigFile: "parent.config",
+			Value:      tc.AlgorithmConsistentHash,
+			Profiles:   []byte(`["serverprofile"]`),
+		},
+		{
+			Name:       ParentConfigParamQString,
+			ConfigFile: "parent.config",
+			Value:      "myQstringParam",
+			Profiles:   []byte(`["serverprofile"]`),
+		},
+	}
+
+	serverParams := []tc.Parameter{
+		{
+			Name:       "trafficserver",
+			ConfigFile: "package",
+			Value:      "8",
+			Profiles:   []byte(`["global"]`),
+		},
+	}
+
+	server := makeTestParentServer()
+	server.Cachegroup = util.StrPtr("edgeCG")
+	server.CachegroupID = util.IntPtr(400)
+
+	mid0 := makeTestParentServer()
+	mid0.Cachegroup = util.StrPtr("midCG")
+	mid0.CachegroupID = util.IntPtr(500)
+	mid0.HostName = util.StrPtr("mymid")
+	mid0.ID = util.IntPtr(45)
+	setIP(mid0, "192.168.2.2")
+
+	mid1 := makeTestParentServer()
+	mid1.Cachegroup = util.StrPtr("midCG2")
+	mid1.CachegroupID = util.IntPtr(501)
+	mid1.HostName = util.StrPtr("mymid1")
+	mid1.ID = util.IntPtr(46)
+	setIP(mid1, "192.168.2.3")
+
+	servers := []Server{*server, *mid0, *mid1}
+
+	topologies := []tc.Topology{
+		{
+			Name: "t0",
+			Nodes: []tc.TopologyNode{
+				{
+					Cachegroup: "edgeCG",
+					Parents:    []int{1, 2},
+				},
+				{
+					Cachegroup: "midCG",
+				},
+				{
+					Cachegroup: "midCG2",
+				},
+			},
+		},
+	}
+
+	serverCapabilities := map[int]map[ServerCapability]struct{}{}
+	dsRequiredCapabilities := map[int]map[ServerCapability]struct{}{}
+
+	eCG := &tc.CacheGroupNullable{}
+	eCG.Name = server.Cachegroup
+	eCG.ID = server.CachegroupID
+	eCG.ParentName = mid0.Cachegroup
+	eCG.ParentCachegroupID = mid0.CachegroupID
+	eCG.SecondaryParentName = mid1.Cachegroup
+	eCG.SecondaryParentCachegroupID = mid1.CachegroupID
+	eCGType := tc.CacheGroupEdgeTypeName
+	eCG.Type = &eCGType
+
+	mCG := &tc.CacheGroupNullable{}
+	mCG.Name = mid0.Cachegroup
+	mCG.ID = mid0.CachegroupID
+	mCGType := tc.CacheGroupMidTypeName
+	mCG.Type = &mCGType
+
+	mCG2 := &tc.CacheGroupNullable{}
+	mCG2.Name = mid1.Cachegroup
+	mCG2.ID = mid1.CachegroupID
+	mCGType2 := tc.CacheGroupMidTypeName
+	mCG2.Type = &mCGType2
+
+	cgs := []tc.CacheGroupNullable{*eCG, *mCG, *mCG2}
+
+	dss := []tc.DeliveryServiceServer{
+		{
+			Server:          util.IntPtr(*server.ID),
+			DeliveryService: util.IntPtr(*ds0.ID),
+		},
+		{
+			Server:          util.IntPtr(*server.ID),
+			DeliveryService: util.IntPtr(*ds1.ID),
+		},
+	}
+	cdn := &tc.CDN{
+		DomainName: "cdndomain.example",
+		Name:       "my-cdn-name",
+	}
+
+	cfg, err := MakeParentDotConfig(dses, server, servers, topologies, serverParams, parentConfigParams, serverCapabilities, dsRequiredCapabilities, cgs, dss, cdn, hdr)
+	if err != nil {
+		t.Fatal(err)
+	}
+	txt := cfg.Text
+
+	testComment(t, txt, hdr.HdrComment)
+
+	if !strings.Contains(txt, "dest_domain=ds0.example.net") {
+		t.Errorf("expected parent 'dest_domain=ds0.example.net', actual: '%v'", txt)
+	}
+	if !strings.Contains(txt, "dest_domain=ds1.example.net") {
+		t.Errorf("expected parent 'dest_domain=ds1.example.net', actual: '%v'", txt)
+	}
+	if !strings.Contains(txt, "qstring=myQStringHandlingParam") {
+		t.Errorf("expected qstring from param 'qstring=myQStringHandlingParam', actual: '%v'", txt)
+	}
+	if strings.Contains(txt, "secondary_mode") {
+		t.Errorf("expected no secondary_mode for DSes without ParentConfigParamSecondaryMode parameter, actual: '%v'", txt)
+	}
+	if !strings.Contains(txt, `# ds 'ds1' topology 't0'`) {
+		t.Errorf("expected comment with delivery service and topology, actual: '%v'", txt)
+	}
 }
 
 func makeTestParentServer() *Server {
diff --git a/traffic_ops_ort/atstccfg/cfgfile/wrappers.go b/traffic_ops_ort/atstccfg/cfgfile/wrappers.go
index b189b5b..cd485bd 100644
--- a/traffic_ops_ort/atstccfg/cfgfile/wrappers.go
+++ b/traffic_ops_ort/atstccfg/cfgfile/wrappers.go
@@ -126,7 +126,10 @@ func MakeParentDotConfig(toData *config.TOData, fileName string, hdrCommentTxt s
 		toData.CacheGroups,
 		toData.DeliveryServiceServers,
 		toData.CDN,
-		hdrCommentTxt,
+		atscfg.ParentConfigOpts{
+			HdrComment:  hdrCommentTxt,
+			AddComments: true, // TODO add a CLI flag?
+		},
 	)
 }