This is an automated email from the ASF dual-hosted git repository.
ocket8888 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/trafficcontrol.git
The following commit(s) were added to refs/heads/master by this push:
new 1c206fe5cb Add t3c metadata file (#6905)
1c206fe5cb is described below
commit 1c206fe5cb4a81ab1e9d7e71ec49a8782f60def3
Author: Robert O Butts <[email protected]>
AuthorDate: Fri Jul 22 12:25:34 2022 -0600
Add t3c metadata file (#6905)
---
cache-config/t3c-apply/t3c-apply.go | 105 ++++++++++--
cache-config/t3c-apply/torequest/torequest.go | 15 +-
cache-config/t3cutil/t3cutil.go | 182 +++++++++++++++++++++
cache-config/t3cutil/t3cutil_test.go | 72 ++++++++
.../ort-tests/t3c-create-metadata-file_test.go | 61 +++++++
5 files changed, 416 insertions(+), 19 deletions(-)
diff --git a/cache-config/t3c-apply/t3c-apply.go
b/cache-config/t3c-apply/t3c-apply.go
index ca06e0bf43..afdfc12146 100644
--- a/cache-config/t3c-apply/t3c-apply.go
+++ b/cache-config/t3c-apply/t3c-apply.go
@@ -20,8 +20,12 @@ package main
*/
import (
+ "encoding/json"
+ "errors"
"fmt"
+ "io/ioutil"
"os"
+ "path/filepath"
"time"
"github.com/apache/trafficcontrol/cache-config/t3c-apply/config"
@@ -84,6 +88,7 @@ func main() {
// DO NOT call os.Exit within this function; return the code instead.
// Returns the application exit code.
func Main() int {
+
var syncdsUpdate torequest.UpdateStatus
var lock util.FileLock
cfg, err := config.GetCfg(Version, GitRevision)
@@ -106,6 +111,20 @@ func Main() int {
}
log.Infoln("Acquired app lock")
+ // Note failing to load old metadata is not fatal!
+ // oldMetaData must always be checked for nil before usage!
+ oldMetaData, err := LoadMetaData(cfg)
+ if err != nil {
+ log.Errorln("Failed to load old metadata file,
metadata-dependent functionality disabled: " + err.Error())
+ }
+
+ // Note we only write the metadata file after acquiring the app lock.
+ // We don't want to write a metadata file if we didn't run because
another t3c-apply
+ // was already running.
+ metaData := t3cutil.NewApplyMetaData()
+
+ metaData.ServerHostName = cfg.CacheHostName
+
if cfg.UseGit == config.UseGitYes {
err := util.EnsureConfigDirIsGitRepo(cfg)
if err != nil {
@@ -158,18 +177,19 @@ func Main() int {
if err != nil {
log.Errorln("Checking revalidate state: " + err.Error())
- return GitCommitAndExit(ExitCodeRevalidationError,
FailureExitMsg, cfg)
+ return GitCommitAndExit(ExitCodeRevalidationError,
FailureExitMsg, cfg, metaData, oldMetaData)
}
if syncdsUpdate == torequest.UpdateTropsNotNeeded {
log.Infoln("Checking revalidate state: returned
UpdateTropsNotNeeded")
- return GitCommitAndExit(ExitCodeRevalidationError,
SuccessExitMsg, cfg)
+ metaData.Succeeded = true
+ return GitCommitAndExit(ExitCodeRevalidationError,
SuccessExitMsg, cfg, metaData, oldMetaData)
}
} else {
- syncdsUpdate, err = trops.CheckSyncDSState()
+ syncdsUpdate, err = trops.CheckSyncDSState(metaData)
if err != nil {
log.Errorln("Checking syncds state: " + err.Error())
- return GitCommitAndExit(ExitCodeSyncDSError,
FailureExitMsg, cfg)
+ return GitCommitAndExit(ExitCodeSyncDSError,
FailureExitMsg, cfg, metaData, oldMetaData)
}
if !cfg.IgnoreUpdateFlag && cfg.Files ==
t3cutil.ApplyFilesFlagAll && syncdsUpdate == torequest.UpdateTropsNotNeeded {
// If touching remap.config fails, we want to still try
to restart services
@@ -189,14 +209,16 @@ func Main() int {
}
if err := trops.StartServices(&syncdsUpdate);
err != nil {
log.Errorln("failed to start services:
" + err.Error())
- return
GitCommitAndExit(ExitCodeServicesError, PostConfigFailureExitMsg, cfg)
+ metaData.PartialSuccess = true
+ return
GitCommitAndExit(ExitCodeServicesError, PostConfigFailureExitMsg, cfg,
metaData, oldMetaData)
}
}
finalMsg := SuccessExitMsg
if postConfigFail {
finalMsg = PostConfigFailureExitMsg
}
- return GitCommitAndExit(ExitCodeSuccess, finalMsg, cfg)
+ metaData.Succeeded = true
+ return GitCommitAndExit(ExitCodeSuccess, finalMsg, cfg,
metaData, oldMetaData)
}
}
@@ -208,14 +230,14 @@ func Main() int {
err = trops.ProcessPackages()
if err != nil {
log.Errorf("Error processing packages: %s\n", err)
- return GitCommitAndExit(ExitCodePackagingError,
FailureExitMsg, cfg)
+ return GitCommitAndExit(ExitCodePackagingError,
FailureExitMsg, cfg, metaData, oldMetaData)
}
// check and make sure packages are enabled for startup
err = trops.CheckSystemServices()
if err != nil {
log.Errorf("Error verifying system services: %s\n",
err.Error())
- return GitCommitAndExit(ExitCodeServicesError,
FailureExitMsg, cfg)
+ return GitCommitAndExit(ExitCodeServicesError,
FailureExitMsg, cfg, metaData, oldMetaData)
}
}
@@ -223,9 +245,9 @@ func Main() int {
err = trops.GetConfigFileList()
if err != nil {
log.Errorf("Getting config file list: %s\n", err)
- return GitCommitAndExit(ExitCodeConfigFilesError,
FailureExitMsg, cfg)
+ return GitCommitAndExit(ExitCodeConfigFilesError,
FailureExitMsg, cfg, metaData, oldMetaData)
}
- syncdsUpdate, err = trops.ProcessConfigFiles()
+ syncdsUpdate, err = trops.ProcessConfigFiles(metaData)
if err != nil {
log.Errorf("Error while processing config files: %s\n",
err.Error())
}
@@ -248,7 +270,8 @@ func Main() int {
if err := trops.StartServices(&syncdsUpdate); err != nil {
log.Errorln("failed to start services: " + err.Error())
- return GitCommitAndExit(ExitCodeServicesError,
PostConfigFailureExitMsg, cfg)
+ metaData.PartialSuccess = true
+ return GitCommitAndExit(ExitCodeServicesError,
PostConfigFailureExitMsg, cfg, metaData, oldMetaData)
}
// start 'teakd' if installed.
@@ -279,7 +302,8 @@ func Main() int {
log.Errorf("failed to update Traffic Ops: %s\n", err.Error())
}
- return GitCommitAndExit(ExitCodeSuccess, SuccessExitMsg, cfg)
+ metaData.Succeeded = true
+ return GitCommitAndExit(ExitCodeSuccess, SuccessExitMsg, cfg, metaData,
oldMetaData)
}
func LogPanic(f func() int) (exitCode int) {
@@ -297,7 +321,16 @@ func LogPanic(f func() int) (exitCode int) {
// GitCommitAndExit attempts to git commit all changes, and logs any error.
// It then logs exitMsg at the Info level, and returns exitCode.
// This is a helper function, to reduce the duplicated commit-log-return into
a single line.
-func GitCommitAndExit(exitCode int, exitMsg string, cfg config.Cfg) int {
+func GitCommitAndExit(exitCode int, exitMsg string, cfg config.Cfg, metaData
*t3cutil.ApplyMetaData, oldMetaData *t3cutil.ApplyMetaData) int {
+
+ // metadata isn't actually part of git, but we always want to write it
before committing to git, so this is the right place
+
+ // files previously dropped never become "unmanaged",
+ // and if we delete them they're removed from oldMetaData as well as
the new,
+ // so add the old files to the new metadata.
+ // This is especially important for reval runs, which don't add all
files.
+ metaData.OwnedFilePaths = t3cutil.CombineOwnedFilePaths(metaData,
oldMetaData)
+ WriteMetaData(cfg, metaData)
success := exitCode == ExitCodeSuccess
if cfg.UseGit == config.UseGitYes || cfg.UseGit == config.UseGitAuto {
if err := util.MakeGitCommitAll(cfg, util.GitChangeIsSelf,
success); err != nil {
@@ -330,3 +363,49 @@ func CheckMaxmindUpdate(cfg config.Cfg) bool {
return result
}
+
+const MetaDataFileName = `t3c-apply-metadata.json`
+const MetaDataFileMode = 0600
+
+// WriteMetaData writes the metaData file.
+//
+// The metadata file is written in the ATS config directory, so it's versioned
+// with git.
+//
+// On error, an error is written to the log, but no error is returned.
+func WriteMetaData(cfg config.Cfg, metaData *t3cutil.ApplyMetaData) {
+ metaData.SetTime(time.Now())
+ bts, err := metaData.Format()
+ if err != nil {
+ log.Errorln("formatting metadata file: " + err.Error())
+ return
+ }
+
+ metaDataFilePath := GetMetaDataFilePath(cfg)
+
+ if err := ioutil.WriteFile(metaDataFilePath, bts, MetaDataFileMode);
err != nil {
+ log.Errorln("writing metadata file '" + metaDataFilePath + "':
" + err.Error())
+ return
+ }
+}
+
+func LoadMetaData(cfg config.Cfg) (*t3cutil.ApplyMetaData, error) {
+ metaDataFilePath := GetMetaDataFilePath(cfg)
+
+ bts, err := ioutil.ReadFile(metaDataFilePath)
+ if err != nil {
+ return nil, errors.New("reading metadata file '" +
metaDataFilePath + "': " + err.Error())
+ }
+
+ metaData := &t3cutil.ApplyMetaData{}
+
+ if err := json.Unmarshal(bts, &metaData); err != nil {
+ return nil, errors.New("unmarshalling metadata file: " +
err.Error())
+ }
+
+ return metaData, nil
+}
+
+func GetMetaDataFilePath(cfg config.Cfg) string {
+ return filepath.Join(cfg.TsConfigDir, MetaDataFileName)
+}
diff --git a/cache-config/t3c-apply/torequest/torequest.go
b/cache-config/t3c-apply/torequest/torequest.go
index e61168209e..e0eef3f267 100644
--- a/cache-config/t3c-apply/torequest/torequest.go
+++ b/cache-config/t3c-apply/torequest/torequest.go
@@ -725,8 +725,9 @@ func (r *TrafficOpsReq) CheckRevalidateState(sleepOverride
bool) (UpdateStatus,
return updateStatus, nil
}
-// CheckSYncDSState retrieves and returns the DS Update status from Traffic
Ops.
-func (r *TrafficOpsReq) CheckSyncDSState() (UpdateStatus, error) {
+// CheckSyncDSState retrieves and returns the DS Update status from Traffic
Ops.
+// The metaData is this run's metadata. It must not be nil, and this function
may add to it.
+func (r *TrafficOpsReq) CheckSyncDSState(metaData *t3cutil.ApplyMetaData)
(UpdateStatus, error) {
updateStatus := UpdateTropsNotNeeded
randDispSec := time.Duration(0)
log.Debugln("Checking syncds state.")
@@ -763,7 +764,7 @@ func (r *TrafficOpsReq) CheckSyncDSState() (UpdateStatus,
error) {
}
} else if !r.Cfg.IgnoreUpdateFlag {
log.Errorln("no queued update needs to be applied.
Running revalidation before exiting.")
- r.RevalidateWhileSleeping()
+ r.RevalidateWhileSleeping(metaData)
return UpdateTropsNotNeeded, nil
} else {
log.Errorln("Traffic Ops is signaling that no update is
waiting to be applied.")
@@ -793,7 +794,7 @@ func (r *TrafficOpsReq) CheckReloadRestart(data
[]FileRestartData) RestartData {
}
// ProcessConfigFiles processes all config files retrieved from Traffic Ops.
-func (r *TrafficOpsReq) ProcessConfigFiles() (UpdateStatus, error) {
+func (r *TrafficOpsReq) ProcessConfigFiles(metaData *t3cutil.ApplyMetaData)
(UpdateStatus, error) {
var updateStatus UpdateStatus = UpdateTropsNotNeeded
log.Infoln(" ======== Start processing config files ========")
@@ -832,6 +833,8 @@ func (r *TrafficOpsReq) ProcessConfigFiles() (UpdateStatus,
error) {
shouldRestartReload := ShouldReloadRestart{[]FileRestartData{}}
for _, cfg := range r.configFiles {
+ metaData.OwnedFilePaths = append(metaData.OwnedFilePaths,
cfg.Path) // all config files are added to OwnedFiles, even if they aren't
changed on disk.
+
if cfg.ChangeNeeded &&
!cfg.ChangeApplied &&
cfg.AuditComplete &&
@@ -1016,7 +1019,7 @@ func (r *TrafficOpsReq) ProcessPackages() error {
return nil
}
-func (r *TrafficOpsReq) RevalidateWhileSleeping() (UpdateStatus, error) {
+func (r *TrafficOpsReq) RevalidateWhileSleeping(metaData
*t3cutil.ApplyMetaData) (UpdateStatus, error) {
updateStatus, err := r.CheckRevalidateState(true)
if err != nil {
return updateStatus, err
@@ -1032,7 +1035,7 @@ func (r *TrafficOpsReq) RevalidateWhileSleeping()
(UpdateStatus, error) {
return updateStatus, err
}
- updateStatus, err := r.ProcessConfigFiles()
+ updateStatus, err := r.ProcessConfigFiles(metaData)
if err != nil {
return updateStatus, err
}
diff --git a/cache-config/t3cutil/t3cutil.go b/cache-config/t3cutil/t3cutil.go
index 782548301f..57532d6e05 100644
--- a/cache-config/t3cutil/t3cutil.go
+++ b/cache-config/t3cutil/t3cutil.go
@@ -21,6 +21,7 @@ package t3cutil
import (
"bytes"
+ "encoding/json"
"errors"
"fmt"
"html"
@@ -29,8 +30,10 @@ import (
"os"
"os/exec"
"regexp"
+ "sort"
"strings"
"syscall"
+ "time"
)
type ATSConfigFile struct {
@@ -228,3 +231,182 @@ func UserAgentStr(appName string, versionNum string,
gitRevision string) string
}
return appName + "/" + versionNum + ".." + gitRevision
}
+
+// NewApplyMetaData creates a new, empty ApplyMetaData object.
+func NewApplyMetaData() *ApplyMetaData {
+ return &ApplyMetaData{
+ Version: MetaDataVersion,
+ InstalledPackages: []string{}, // construct a slice, so JSON
serializes '[]' not 'null'.
+ OwnedFilePaths: []string{}, // construct a slice, so JSON
serializes '[]' not 'null'.
+ }
+}
+
+// MetaDataVersion is the version of the metadata file.
+// This should update the major version with breaking changes,
+// and t3c versions should strive to maintain compatibility
+// at least one major version back, so features like tracking
+// t3c-owned files continue to work through upgrades.
+const MetaDataVersion = "1.0"
+
+// ApplyMetaData is metadata about a t3c-apply run.
+// Always use NewApplyMetaData, don't use a literal to construct a new object.
+type ApplyMetaData struct {
+ // Version is the metadata version of this metadata object or file. See
MetaDataVersion.
+ Version string `json:"version"`
+
+ // ServerFQDN is the FQDN of this server.
+ // The primary purpose of this field is to allow distinguishing
+ // metadata files from different servers.
+ ServerHostName string `json:"server-hostname"`
+
+ // Time is an RFC3339Nano timestamp of the time t3c-apply ran for this
metadata.
+ // This should be treated as approximate, as it could be the start
time, end time, or
+ // any inexact time in-between.
+ // However, times of different metadata files should always be
monotonically increasing.
+ Time string `json:"time"`
+
+ // ReloadedATS is whether this run restarted ATS.
+ // Note this is whether ATS was actually restarted, not whether it
would have been,
+ // e.g. because of --report-only or --service-action.
+ ReloadedATS bool `json:"reloaded-ats"`
+
+ // RestartedATS is whether this run restarted ATS.
+ // Note this is whether ATS was actually restarted, not whether it
would have been,
+ // e.g. because of --report-only or --service-action.
+ RestartedATS bool `json:"restarted-ats"`
+
+ // UnsetUpdateFlag is whether this t3c-apply run unset the update flag
for this server.
+ // Note this is whether the flag was actually unset, not whether it
would have been e.g.
+ // because of --no-unset-update-flag or --report-only.
+ UnsetUpdateFlag bool `json:"unset-update-flag"`
+
+ // UnsetRevalFlag is whether this t3c-apply run unset the revalidate
flag for this server.
+ // Note this is whether the flag was actually unset, not whether it
would have been e.g.
+ // because of --no-unset-reval-flag or --report-only.
+ UnsetRevalFlag bool `json:"unset-reval-flag"`
+
+ // InstalledPackages is which yum packages were installed.
+ // Note this packages actually installed, not what would have been e.g.
+ // because of --install-packages=false or --report-only.
+ InstalledPackages []string `json:"installed-packages"`
+
+ // OwnedFilePaths is the list of files t3c-apply produced in this run.
+ //
+ // This can be used to know which files in the ATS config directory
were produced by t3c,
+ // and which were produced by some other means.
+ //
+ // Note this is all files produced, not necessarily all files written
to disk. This
+ // will include files generated, but not changed on disk because they
had no
+ // semantic diff from the existing file.
+ //
+ // This may be used in the future for t3c-apply to delete files
produced by a previous
+ // run which no longer exist (for example, Header Rewrites from a
Delivery Service
+ // no longer assigned to this server).
+ //
+ // Files are the full path and file name.
+ OwnedFilePaths []string `json:"owned-files"`
+
+ // Succeeded is whether this t3c-apply run generally succeeded.
+ //
+ // Note not all scenarios are black or white success-or-fail.
+ // For example, files may be successfully created, but reloading ATS
may fail.
+ // In these scenarios, t3c-apply will attempt to set Succeeded to false,
+ // but also attempt to set other metadata about what was actually
performed.
+ //
+ // But when partial failure occurrs, nothing is guaranteed in the
metadata.
+ // Operators should consider the logs authoritative over the metadata.
+ Succeeded bool `json:"succeeded"`
+
+ // PartialSuccess indicates that some actions were successful, but
+ // later actions failed.
+ //
+ // This is a bad place to be, because it means some things were changed,
+ // but not everything that needed to be. This is often not fatal,
because,
+ // for example, if config files were changed by ATS failed to reload,
+ // those config files typically needed placed anyway.
+ //
+ // But nevertheless, partial success is potentially catastrophic, and
operators
+ // are strongly encouraged to set alarms and read logs in the event it
occurs,
+ // to determine what was changed, what failed, and what actions need
taken.
+ PartialSuccess bool `json:"partial-success"`
+}
+
+// Format prints the ApplyMetaData in a format designed to be written to a
file,
+// and structured but pretty-printed to work well with line-based diffs (e.g.
in git).
+func (md *ApplyMetaData) Format() ([]byte, error) {
+ bts, err := json.MarshalIndent(md, "", " ")
+ if err != nil {
+ return nil, errors.New("marshalling metadata file: " +
err.Error())
+ }
+ bts = append(bts, '\n') // newline at the end of the file, so it's a
valid POSIX text file
+
+ return bts, nil
+}
+
+// SetTime sets the Time field in the prescribed format, based on the given
time.
+// To set to the current time, call SetTime(time.Now()).
+// The format is UTC RFC3339Nano. See ApplyMetaData.
+func (md *ApplyMetaData) SetTime(tm time.Time) {
+ md.Time = tm.UTC().Format(time.RFC3339Nano)
+}
+
+// CombineOwnedFilePaths combines the owned file paths of two metadata objects.
+//
+// This is primarily useful when a config run, such as revalidate, adds owned
files, but not
+// all owned files, but we don't want to write metadata incidating we don't
own existing files,
+// so this can be used to combine the new files with the previous metadata.
+//
+// Both am and bm are may be nil, in which case the files from the non-nil
object is returned,
+// or an empty array if both are nil.
+func CombineOwnedFilePaths(am *ApplyMetaData, bm *ApplyMetaData) []string {
+ if am == nil && bm == nil {
+ return []string{}
+ } else if am == nil {
+ sort.Strings(bm.OwnedFilePaths) // the func guarantees the
returned array will always be sorted
+ return bm.OwnedFilePaths
+ } else if bm == nil {
+ sort.Strings(am.OwnedFilePaths) // the func guarantees the
returned array will always be sorted
+ return am.OwnedFilePaths
+ }
+ return sortAndCombineStrs(am.OwnedFilePaths, bm.OwnedFilePaths)
+}
+
+// sortAndCombineStrs sorts as and bs, and then returns an array containing
+// the unique strings in each, without duplicates.
+func sortAndCombineStrs(as []string, bs []string) []string {
+ sort.Strings(as)
+ sort.Strings(bs)
+ combined := []string{}
+ ai := 0
+ bi := 0
+ for ai < len(as) && bi < len(bs) {
+ if as[ai] == bs[bi] {
+ combined = append(combined, as[ai])
+ ai++
+ bi++
+ continue
+ }
+ // at this point we know they don't match
+ // so add the lesser, increment it, and loop (but don't add or
increment the greater)
+ if as[ai] < bs[bi] {
+ combined = append(combined, as[ai])
+ ai++
+ continue
+ }
+ combined = append(combined, bs[bi])
+ bi++
+ }
+
+ // at this point, we added everything up to the end of one of the
arrays,
+ // but potentially not the other. So add the remaining strings in the
other
+
+ for ai < len(as) {
+ combined = append(combined, as[ai])
+ ai++
+ }
+ for bi < len(bs) {
+ combined = append(combined, bs[bi])
+ bi++
+ }
+ return combined
+}
diff --git a/cache-config/t3cutil/t3cutil_test.go
b/cache-config/t3cutil/t3cutil_test.go
new file mode 100644
index 0000000000..e4096b4aff
--- /dev/null
+++ b/cache-config/t3cutil/t3cutil_test.go
@@ -0,0 +1,72 @@
+package t3cutil
+
+/*
+ * 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"
+)
+
+func TestSortAndCombineStrs(t *testing.T) {
+ type Expected struct {
+ InputA []string
+ InputB []string
+ Expected []string
+ }
+ expecteds := []Expected{
+ {
+ InputA: []string{"foo", "bar", "baz"},
+ InputB: []string{"alpha", "bar", "beta"},
+ Expected: []string{"alpha", "bar", "baz", "beta",
"foo"},
+ },
+ {
+ InputA: []string{"remap.config",
"regex_revalidate.config", "parent.config"},
+ InputB: []string{"regex_revalidate.config"},
+ Expected: []string{"parent.config",
"regex_revalidate.config", "remap.config"},
+ },
+ {
+ InputA: []string{"remap.config", "parent.config"},
+ InputB: []string{"regex_revalidate.config"},
+ Expected: []string{"parent.config",
"regex_revalidate.config", "remap.config"},
+ },
+ {
+ InputA: []string{},
+ InputB: []string{"remap.config",
"regex_revalidate.config", "parent.config"},
+ Expected: []string{"parent.config",
"regex_revalidate.config", "remap.config"},
+ },
+ {
+ InputA: []string{"remap.config",
"regex_revalidate.config", "parent.config"},
+ InputB: []string{},
+ Expected: []string{"parent.config",
"regex_revalidate.config", "remap.config"},
+ },
+ {
+ InputA: []string{},
+ InputB: []string{},
+ Expected: []string{},
+ },
+ }
+
+ for _, ex := range expecteds {
+ actual := sortAndCombineStrs(ex.InputA, ex.InputB)
+ if !reflect.DeepEqual(actual, ex.Expected) {
+ t.Errorf("sortAndCombineStrs(%+v,%+v) expected %+v
actual %+v", ex.InputA, ex.InputB, ex.Expected, actual)
+ }
+ }
+}
diff --git a/cache-config/testing/ort-tests/t3c-create-metadata-file_test.go
b/cache-config/testing/ort-tests/t3c-create-metadata-file_test.go
new file mode 100644
index 0000000000..745fd89650
--- /dev/null
+++ b/cache-config/testing/ort-tests/t3c-create-metadata-file_test.go
@@ -0,0 +1,61 @@
+package orttest
+
+/*
+ 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.
+*/
+
+import (
+ "encoding/json"
+ "io/ioutil"
+ "path/filepath"
+ "testing"
+
+ "github.com/apache/trafficcontrol/cache-config/testing/ort-tests/tcdata"
+ "github.com/apache/trafficcontrol/cache-config/testing/ort-tests/util"
+)
+
+func TestT3cCreateMetaDataFile(t *testing.T) {
+ // t3c should create a metadata file
+ tcd.WithObjs(t, []tcdata.TCObj{
+ tcdata.CDNs, tcdata.Types, tcdata.Tenants, tcdata.Parameters,
+ tcdata.Profiles, tcdata.ProfileParameters,
+ tcdata.Divisions, tcdata.Regions, tcdata.PhysLocations,
+ tcdata.CacheGroups, tcdata.Servers, tcdata.Topologies,
+ tcdata.DeliveryServices}, func() {
+
+ err := t3cUpdateCreateEmptyFile(DefaultCacheHostName, "badass")
+ if err != nil {
+ t.Fatalf("t3c badass failed: %v", err)
+ }
+
+ const metaDataFileName = `t3c-apply-metadata.json`
+
+ filePath := filepath.Join(TestConfigDir, metaDataFileName)
+
+ if !util.FileExists(filePath) {
+ t.Fatalf("missing metadata file '%s'", filePath)
+ }
+
+ mdFileBts, err := ioutil.ReadFile(filePath)
+ if err != nil {
+ t.Fatalf("reading file '%s': %v", filePath, err)
+ }
+
+ // Test that the file is a valid JSON object.
+ // Other than that, we don't want to assert any particular data.
+ mdObj := map[string]interface{}{}
+ if err := json.Unmarshal(mdFileBts, &mdObj); err != nil {
+ t.Errorf("expected metadata file '%s' to be a json
object, actual: %s", filePath, err)
+ }
+ })
+}