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)
+               }
+       })
+}

Reply via email to