You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@trafficcontrol.apache.org by ro...@apache.org on 2022/02/22 19:22:18 UTC

[trafficcontrol] branch master updated: adding health-client integration tests (#6564)

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

rob 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 bc0b5fc  adding health-client integration tests (#6564)
bc0b5fc is described below

commit bc0b5fcf35550ac42ff6aff4c4d1f07bd07eb25d
Author: John J. Rushford <jr...@apache.org>
AuthorDate: Tue Feb 22 12:22:03 2022 -0700

    adding health-client integration tests (#6564)
---
 .../health-client-integration-tests/README.md      |  77 ++++++++++
 .../health-client-integration-tests/action.yml     |  22 +++
 .../health-client-integration-tests/main.js        |  34 +++++
 .github/workflows/health-client-tests.yml          | 165 +++++++++++++++++++++
 cache-config/testing/ort-tests/tc-fixtures.json    |   2 +-
 tc-health-client/README.md                         |  14 ++
 tc-health-client/config/config.go                  |   8 +
 tc-health-client/testing/docker/db_init/Dockerfile |  42 ++++++
 tc-health-client/testing/docker/db_init/dbInit.sh  |  30 ++++
 tc-health-client/testing/docker/docker-compose.yml |  83 +++++++++++
 .../testing/docker/health-check-test/Dockerfile    |  69 +++++++++
 .../testing/docker/health-check-test/parent.config |  20 +++
 .../testing/docker/health-check-test/run.sh        |  67 +++++++++
 .../docker/health-check-test/strategies.yaml       |  61 ++++++++
 .../testing/docker/health-check-test/systemctl.sh  |  88 +++++++++++
 .../docker/health-check-test/tc-health-client.json |  13 ++
 .../testing/docker/traffic_ops/Dockerfile          |  84 +++++++++++
 .../docker/traffic_ops/profile.origin.traffic_ops  |  18 +++
 tc-health-client/testing/docker/traffic_ops/run.sh | 108 ++++++++++++++
 tc-health-client/testing/docker/variables.env      |  50 +++++++
 .../testing/tests/conf/docker-edge-cache.conf      |  38 +++++
 tc-health-client/testing/tests/hcutil/hcutil.go    |  42 ++++++
 .../testing/tests/health-client-main_test.go       | 118 +++++++++++++++
 .../testing/tests/health-client-startup_test.go    | 128 ++++++++++++++++
 tc-health-client/tmagent/tmagent.go                |  31 ++++
 25 files changed, 1411 insertions(+), 1 deletion(-)

diff --git a/.github/actions/health-client-integration-tests/README.md b/.github/actions/health-client-integration-tests/README.md
new file mode 100644
index 0000000..5a0cd44
--- /dev/null
+++ b/.github/actions/health-client-integration-tests/README.md
@@ -0,0 +1,77 @@
+<!--
+  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.
+-->
+
+# health-client-integration-tests JavaScript action
+This action runs the Traffic Control Health client  integration tests.
+
+## Inputs
+
+### `Trafficserver RPM`
+**Required** A trafficserver RPM used to install on the Edge server.
+
+### `Trafficcontrol Health cliet RPM`
+**Required** A trafficserver RPM used to install on the Edge server.
+
+## Outputs
+
+### `exit-code`
+1 if the Go program(s) could be built successfully.
+
+## Example usage
+```yaml
+jobs:
+  tests:
+    runs-on: ubuntu-latest
+
+    services:
+      postgres:
+        image: postgres:11.9
+        env:
+          POSTGRES_USER: traffic_ops
+          POSTGRES_PASSWORD: twelve
+          POSTGRES_DB: traffic_ops
+        ports:
+        - 5432:5432
+        options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5
+
+      smtp:
+        image: maildev/maildev:2.0.0-beta3
+        ports:
+          - 25:25
+        options: >-
+          --entrypoint=bin/maildev
+          --user=root
+          --health-cmd="sh -c \"[[ \$(wget -qO- http://smtp/healthz) == true ]]\""
+          --
+          maildev/maildev:2.0.0-beta3
+          --smtp=25
+          --hide-extensions=STARTTLS
+          --web=80
+
+    steps:
+      - name: Checkout
+        uses: actions/checkout@master
+      - name: initialize database
+        uses: ./.github/actions/todb-init
+      - name: Run API v5 tests
+        uses: ./.github/actions/health-client-integration-tests
+        with:
+          version: 5
+          smtp_address: localhost
+```
diff --git a/.github/actions/health-client-integration-tests/action.yml b/.github/actions/health-client-integration-tests/action.yml
new file mode 100644
index 0000000..34e5845
--- /dev/null
+++ b/.github/actions/health-client-integration-tests/action.yml
@@ -0,0 +1,22 @@
+# 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.
+
+name: 'health-client-integration-tests'
+description: 'Runs health client integration tests'
+runs:
+  using: 'node12'
+  main: 'main.js'
diff --git a/.github/actions/health-client-integration-tests/main.js b/.github/actions/health-client-integration-tests/main.js
new file mode 100644
index 0000000..561cf84
--- /dev/null
+++ b/.github/actions/health-client-integration-tests/main.js
@@ -0,0 +1,34 @@
+/*
+* 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.
+*/
+
+"use strict";
+const child_process = require("child_process");
+const spawnOptions = {
+	stdio: "inherit",
+	stderr: "inherit",
+};
+
+const dockerCompose = ["docker-compose", "-f", `${process.env.GITHUB_WORKSPACE}/tc-health-client/testing/docker/docker-compose.yml`];
+
+function runProcess(...commandArguments) {
+	console.info(...commandArguments);
+	const status = child_process.spawnSync(commandArguments[0], commandArguments.slice(1), spawnOptions).status;
+	if (status === 0) {
+		return;
+	}
+	console.error("Child process \"", ...commandArguments, "\" exited with status code", status, "!");
+	process.exit(status ? status : 1);
+}
+
+runProcess(...dockerCompose, "run", "health-check-test");
diff --git a/.github/workflows/health-client-tests.yml b/.github/workflows/health-client-tests.yml
new file mode 100644
index 0000000..850583e
--- /dev/null
+++ b/.github/workflows/health-client-tests.yml
@@ -0,0 +1,165 @@
+# 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.
+
+name: Traffic Control Health Client integration tests
+
+env:
+  RHEL_VERSION: 8
+  ATS_VERSION: 8.1.x
+
+on:
+  workflow_dispatch:
+  push:
+    paths:
+      - .github/workflows/health-client-tests.yml
+      - go.mod
+      - go.sum
+      - GO_VERSION
+      - lib/go-atscfg/**.go
+      - traffic_ops/*client/**.go
+      - traffic_ops/toclientlib/**.go
+      - lib/atscfg-go/**.go
+      - traffic_ops/traffic_ops_golang/**.go
+      - tc-health-client/**.go
+      - tc-health-client/testing/**
+      - vendor/**.go
+      - vendor/modules.txt
+      - .github/actions/build-ats-test-rpm/**
+      - .github/actions/fetch-github-branch-sha/**
+      - .github/actions/health-client-integration-tests/**
+  create:
+  pull_request:
+    paths:
+      - .github/workflows/health-client-tests.yml
+      - go.mod
+      - go.sum
+      - GO_VERSION
+      - lib/go-atscfg/**.go
+      - traffic_ops/*client/**.go
+      - traffic_ops/toclientlib/**.go
+      - lib/atscfg-go/**.go
+      - traffic_ops/traffic_ops_golang/**.go
+      - tc-health-client/**.go
+      - tc-health-client/testing/**
+      - vendor/**.go
+      - vendor/modules.txt
+      - .github/actions/build-ats-test-rpm/**
+      - .github/actions/fetch-github-branch-sha/**
+      - .github/actions/health-client-integration-tests/**
+    types: [opened, reopened, ready_for_review, synchronize]
+
+jobs:
+
+  traffic_ops:
+    if: github.event.pull_request.draft == false
+    runs-on: ubuntu-latest
+    steps:
+      - name: Checkout
+        uses: actions/checkout@v2
+        with:
+          fetch-depth: 5
+      - name: Build RPM
+        uses: ./.github/actions/build-rpms
+        env:
+          ATC_COMPONENT: ${{ github.job }}
+      - name: Upload RPM
+        uses: actions/upload-artifact@v2
+        with:
+          name: ${{ github.job }}
+          path: ${{ github.workspace }}/dist/${{ github.job }}-*.x86_64.rpm
+
+  tc-health-client:
+    if: github.event.pull_request.draft == false
+    runs-on: ubuntu-latest
+    steps:
+      - name: Checkout
+        uses: actions/checkout@v2
+        with:
+          fetch-depth: 5
+      - name: Build RPM
+        uses: ./.github/actions/build-rpms
+        env:
+          NO_SOURCE: 1
+          ATC_COMPONENT: ${{ github.job }}
+      - name: Upload RPM
+        uses: actions/upload-artifact@v2
+        with:
+          name: ${{ github.job }}
+          path: ${{ github.workspace }}/dist/trafficcontrol-health-client-*.x86_64.rpm
+
+  trafficserver:
+    if: github.event.pull_request.draft == false
+    runs-on: ubuntu-latest
+    steps:
+      - name: Checkout
+        uses: actions/checkout@v2
+      - name: Get latest commit sha and latest tag
+        uses: ./.github/actions/fetch-github-branch-sha
+        with:
+          owner: apache
+          repo: trafficserver
+          branch: ${{ env.ATS_VERSION }}
+        id: git-repo-sha
+      - name: Check Cache
+        id: ats-rpm-cache
+        uses: actions/cache@v2
+        with:
+          path: ${{ github.workspace }}/dist
+          key: ${{ steps.git-repo-sha.outputs.sha }}-${{ steps.git-repo-sha.outputs.latest-tag }}-el${{ env.RHEL_VERSION }}-${{ hashFiles('tc-health-client/testing/docker/trafficserver/**') }}
+      - name: Build ATS RPM
+        if: steps.ats-rpm-cache.outputs.cache-hit != 'true'
+        uses: ./.github/actions/build-ats-test-rpm
+        env:
+          ATC_COMPONENT: ${{ github.job }}
+      - name: Upload RPM
+        uses: actions/upload-artifact@v2
+        with:
+          name: ${{ github.job }}
+          path: ${{ github.workspace }}/dist/${{ github.job }}-*.x86_64.rpm
+
+  health-client_tests:
+    if: github.event.pull_request.draft == false
+    runs-on: ubuntu-latest
+    needs:
+      - traffic_ops
+      - tc-health-client
+      - trafficserver
+
+    steps:
+    - name: Checkout
+      uses: actions/checkout@master
+    - name: Download TO RPM
+      uses: actions/download-artifact@v2
+      with:
+        name: traffic_ops
+        path: ${{ github.workspace }}/tc-health-client/testing/docker/traffic_ops
+    - name: Download Health Client RPM
+      uses: actions/download-artifact@v2
+      with:
+        name: tc-health-client
+        path: ${{ github.workspace }}/tc-health-client/testing/docker/health-check-test
+    - name: Download ATS RPM
+      uses: actions/download-artifact@v2
+      with:
+        name: trafficserver
+        path: ${{ github.workspace }}/tc-health-client/testing/docker/health-check-test
+    - name: display directory
+      run: ls -l ${{ github.workspace }}/tc-health-client/testing/docker/health-check-test
+    - name: Build health client test containers
+      run: docker-compose -f ${{ github.workspace }}/tc-health-client/testing/docker/docker-compose.yml build --parallel
+    - name: Run health client integration tests
+      uses: ./.github/actions/health-client-integration-tests
diff --git a/cache-config/testing/ort-tests/tc-fixtures.json b/cache-config/testing/ort-tests/tc-fixtures.json
index 70313d4..4f7098a 100644
--- a/cache-config/testing/ort-tests/tc-fixtures.json
+++ b/cache-config/testing/ort-tests/tc-fixtures.json
@@ -3135,7 +3135,7 @@
       "revalPending": false,
       "routerHostName": "",
       "routerPortName": "",
-      "status": "REPORTED",
+      "status": "ONLINE",
       "tcpPort": 81,
       "type": "RASCAL",
       "updPending": false,
diff --git a/tc-health-client/README.md b/tc-health-client/README.md
index 4e6e272..8853462 100644
--- a/tc-health-client/README.md
+++ b/tc-health-client/README.md
@@ -108,6 +108,8 @@ Sample configuarion file:
     "unavailable-poll-threshold": 2,
     "trafficserver-config-dir": "/opt/trafficserver/etc/trafficserver",
     "trafficserver-bin-dir": "/opt/trafficserver/bin",
+    "poll-state-json-log": "/var/log/trafficcontrol/poll-state.json",
+    "enable-poll-state-log": false
   }
 ```
 
@@ -179,6 +181,18 @@ Sample configuarion file:
   The location on the host where **Traffic Server** **traffic_ctl** tool may
   be found.
 
+### poll-state-json-log ###
+
+  The full path to the polling state file which contains information 
+  about the current status of parents and the health client configuration.
+  Polling state data is written to this file after each polling cycle when
+  enabled, see **enable-poll-state-log**
+
+### enable-poll-state-log ###
+
+  Enable writing the Polling state to the **poll-state-json-log** after
+  eache polling cycle.  Default **false**, disabled
+
 # Files
 
 * /etc/trafficcontrol/tc-health-client.json
diff --git a/tc-health-client/config/config.go b/tc-health-client/config/config.go
index be74cbf..ca6db3a 100644
--- a/tc-health-client/config/config.go
+++ b/tc-health-client/config/config.go
@@ -43,6 +43,7 @@ var tmPollingInterval time.Duration
 var toRequestTimeout time.Duration
 
 const (
+	DefaultPollStateJSONLog         = "/var/log/trafficcontrol/poll-state.json"
 	DefaultConfigFile               = "/etc/trafficcontrol/tc-health-client.json"
 	DefaultLogDirectory             = "/var/log/trafficcontrol"
 	DefaultLogFile                  = "tc-health-client.log"
@@ -67,6 +68,8 @@ type Cfg struct {
 	UnavailablePollThreshold int             `json:"unavailable-poll-threshold"`
 	TrafficServerConfigDir   string          `json:"trafficserver-config-dir"`
 	TrafficServerBinDir      string          `json:"trafficserver-bin-dir"`
+	PollStateJSONLog         string          `json:"poll-state-json-log"`
+	EnablePollStateLog       bool            `json:"enable-poll-state-log"`
 	TrafficMonitors          map[string]bool `json:"trafficmonitors,omitempty"`
 	HealthClientConfigFile   util.ConfigFile
 	CredentialFile           util.ConfigFile
@@ -329,6 +332,9 @@ func LoadConfig(cfg *Cfg) (bool, error) {
 		if cfg.UnavailablePollThreshold == 0 {
 			cfg.UnavailablePollThreshold = DefaultUnavailablePollThreshold
 		}
+		if cfg.PollStateJSONLog == "" {
+			cfg.PollStateJSONLog = DefaultPollStateJSONLog
+		}
 
 		cfg.HealthClientConfigFile.LastModifyTime = modTime
 
@@ -372,6 +378,8 @@ func UpdateConfig(cfg *Cfg, newCfg *Cfg) {
 	cfg.TrafficServerBinDir = newCfg.TrafficServerBinDir
 	cfg.TrafficMonitors = newCfg.TrafficMonitors
 	cfg.HealthClientConfigFile = newCfg.HealthClientConfigFile
+	cfg.PollStateJSONLog = newCfg.PollStateJSONLog
+	cfg.EnablePollStateLog = newCfg.EnablePollStateLog
 }
 
 func Usage() {
diff --git a/tc-health-client/testing/docker/db_init/Dockerfile b/tc-health-client/testing/docker/db_init/Dockerfile
new file mode 100644
index 0000000..e101c4f
--- /dev/null
+++ b/tc-health-client/testing/docker/db_init/Dockerfile
@@ -0,0 +1,42 @@
+# 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.
+
+############################################################
+# Dockerfile to initialized Traffic Ops Database container 
+# Based on CentOS 7.2
+############################################################
+
+FROM centos/systemd
+
+RUN yum install -y https://download.postgresql.org/pub/repos/yum/reporpms/EL-7-x86_64/pgdg-redhat-repo-latest.noarch.rpm
+
+RUN yum -y install \
+  postgresql13 \
+  nmap-ncat \
+  cpanminus && \
+  yum clean all
+
+ENV POSTGRES_HOME $POSTGRES_HOME
+ENV PGPASSWORD $PGPASSWORD 
+ENV DB_USERNAME $DB_USERNAME
+ENV DB_NAME $DB_NAME
+ENV DB_USER_PASS $DB_USER_PASS 
+ENV DB_SERVER $DB_SERVER
+ENV DB_PORT $DB_PORT
+
+ADD db_init/dbInit.sh /
+CMD /dbInit.sh
diff --git a/tc-health-client/testing/docker/db_init/dbInit.sh b/tc-health-client/testing/docker/db_init/dbInit.sh
new file mode 100755
index 0000000..f4392d4
--- /dev/null
+++ b/tc-health-client/testing/docker/db_init/dbInit.sh
@@ -0,0 +1,30 @@
+#!/usr/bin/env bash
+# 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.
+
+############################################################
+# Script for creating the database user account for traffic
+# ops. 
+# Used while the Docker Image is initializing itself
+############################################################
+
+while ! nc $DB_SERVER $DB_PORT </dev/null; do # &>/dev/null; do
+        echo "waiting for $DB_SERVER:$DB_PORT"
+        sleep 3
+done
+psql -h $DB_SERVER -U postgres -c "CREATE USER $DB_USER WITH ENCRYPTED PASSWORD '$DB_USER_PASS'"
+createdb $DB_NAME -h $DB_SERVER -U postgres --owner $DB_USER
diff --git a/tc-health-client/testing/docker/docker-compose.yml b/tc-health-client/testing/docker/docker-compose.yml
new file mode 100644
index 0000000..b096c97
--- /dev/null
+++ b/tc-health-client/testing/docker/docker-compose.yml
@@ -0,0 +1,83 @@
+# 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.
+#
+# Build trafficcontrol:  
+#   Copy the traffic_ops rpm to traffic_ops/traffic_ops.rpm
+#   Copy the trafficcontrol-cache-config rpm to ort_test/trafficcontrol-cache-config.rpm
+#   Copy an ATS rpm to yumserver/test-rpms (the ort_tests
+#     container updates ../ort-tests/tc-fixtures.json with
+#     the corresponding version string)
+#
+#   Run: docker-compose build
+#   Run: docker-compose run ort_test
+#
+
+---
+version: '3.8'
+
+volumes:
+  trafficcontrol:
+  traffic_ops:
+  conf:
+
+services:
+  db:
+    image: postgres:13.2
+    env_file:
+      - variables.env
+    ports: 
+      - "5432:5432"
+
+  db_init:
+    env_file:
+      - variables.env
+    build:
+      context: .
+      dockerfile: db_init/Dockerfile
+    depends_on: 
+      - db
+
+  health-check-test:
+    env_file:
+      - variables.env
+    build:
+      context: .
+      dockerfile: health-check-test/Dockerfile
+      args:
+        - OS_DISTRO=${OS_DISTRO:-rockylinux}
+        - OS_VERSION=${OS_VERSION:-8}
+    depends_on: 
+      - to_server
+    volumes:
+      - ../../..:/root/go/src/github.com/apache/trafficcontrol
+
+  to_server:
+    env_file:
+      - variables.env
+    ports: 
+      - "443:443"
+    build:
+      context: .
+      dockerfile: traffic_ops/Dockerfile
+      args:
+        - OS_DISTRO=${OS_DISTRO:-rockylinux}
+        - OS_VERSION=${OS_VERSION:-8}
+    volumes:
+      - ../../../GO_VERSION:/GO_VERSION
+    depends_on:
+      - db_init
+
diff --git a/tc-health-client/testing/docker/health-check-test/Dockerfile b/tc-health-client/testing/docker/health-check-test/Dockerfile
new file mode 100644
index 0000000..a601987
--- /dev/null
+++ b/tc-health-client/testing/docker/health-check-test/Dockerfile
@@ -0,0 +1,69 @@
+# 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.
+
+# For cache, you may either use (RAM or disk) block devices or disk directories
+# To use RAM block devices, pass them as /dev/ram0 and /dev/ram1 via `docker run --device`
+# To use disk directories, simply don't pass devices, and the container will configure Traffic Server for directories
+
+# Block devices may be created on the native machine with, for example, `modprobe brd`.
+# The recommended minimum size for each block devices is 1G.
+# For example, `sudo modprobe brd rd_size=1048576 rd_nr=2`
+
+ARG OS_DISTRO=rockylinux
+ARG OS_VERSION=8
+FROM ${OS_DISTRO}:${OS_VERSION}
+ARG OS_DISTRO
+ARG OS_VERSION
+# Makes OS_VERSION available in later layers without needing to specify it again
+ENV OS_VERSION=$OS_VERSION
+ENV OS_DISTRO=$OS_DISTRO
+MAINTAINER dev@trafficcontrol.apache.org
+EXPOSE 80 443
+
+# these packages load for both centos 7 and rockylinux 8
+# no checks required at this time.
+RUN yum install -y epel-release && yum repolist && \
+  yum install -y brotli initscripts jansson jansson-devel \
+    git gcc hwloc jq lua luajit man-db tcl && \
+  yum clean all
+
+RUN echo "Using Image ${OS_DISTRO}:${OS_VERSION}"
+
+ADD health-check-test/systemctl.sh /
+RUN cp /usr/bin/systemctl /usr/bin/systemctl.save
+RUN cp /systemctl.sh /usr/bin/systemctl && chmod 0755 /usr/bin/systemctl
+
+# Note that if more than one t3c RPM matches this wildcard, this Dockerfile will
+# break because this will create a directory instead of an RPM file, which it
+# will then fail to install.
+ADD health-check-test/trafficserver-[0-9]*.rpm /trafficserver.rpm
+ADD health-check-test/trafficcontrol-health-client*x86_64.rpm /trafficcontrol-health-client.rpm
+RUN rpm -i /trafficserver.rpm /trafficcontrol-health-client.rpm 
+ADD health-check-test/tc-health-client.json /etc/trafficcontrol/
+
+RUN sed -i 's/HOME\/bin/HOME\/bin:\/usr\/local\/go\/bin:/g' /root/.bash_profile &&\
+  echo "GOPATH=/root/go; export GOPATH" >> /root/.bash_profile &&\
+  echo "StrictHostKeyChecking no" >> /etc/ssh/ssh_config &&\
+  mkdir /root/go
+
+ADD variables.env /
+ADD health-check-test/run.sh /
+ADD health-check-test/parent.config /
+ADD health-check-test/strategies.yaml /
+RUN chmod +x /run.sh
+
+ENTRYPOINT /run.sh
diff --git a/tc-health-client/testing/docker/health-check-test/parent.config b/tc-health-client/testing/docker/health-check-test/parent.config
new file mode 100644
index 0000000..c5dfa5f
--- /dev/null
+++ b/tc-health-client/testing/docker/health-check-test/parent.config
@@ -0,0 +1,20 @@
+#
+# 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.
+#
+dest_domain=foo.com parent="atlanta-mid-16.ga.atlanta.kabletown.net:80|0.5;atlanta-mid-01.ga.atlanta.kabletown.net:80|0.5" rount_robin=consistent_hash
+dest_domain=bar.com parent="dtrc-mid-01.kabletown.net:80|0.5;dtrc-mid-02.kabletown.net:80|0.5" rount_robin=consistent_hash
diff --git a/tc-health-client/testing/docker/health-check-test/run.sh b/tc-health-client/testing/docker/health-check-test/run.sh
new file mode 100644
index 0000000..481d577
--- /dev/null
+++ b/tc-health-client/testing/docker/health-check-test/run.sh
@@ -0,0 +1,67 @@
+#!/bin/bash
+#
+# 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.
+#
+
+GOPATH=/root/go; export GOPATH
+PATH=$PATH:/usr/local/go/bin:; export PATH
+TERM=xterm; export TERM
+
+# set up some convienient links
+/bin/ln -s /root/go/src/github.com/apache/trafficcontrol /trafficcontrol
+
+# install go
+if [[ -f /trafficcontrol/GO_VERSION ]]; then
+  go_version=$(cat /trafficcontrol/GO_VERSION) && \
+  curl -Lo go.tar.gz https://dl.google.com/go/go${go_version}.linux-amd64.tar.gz && \
+  tar -C /usr/local -xvzf go.tar.gz && \
+  ln -s /usr/local/go/bin/go /usr/bin/go && \
+  rm go.tar.gz
+else
+  echo "no GO_VERSION file, unable to install go"
+  exit 1
+fi
+
+# write the to-creds file
+. /variables.env
+echo "#!/bin/bash" > /etc/to-creds
+echo "TO_USER=${TO_ADMIN_USER}" >> /etc/to-creds
+echo "TO_PASS=${TO_ADMIN_PASS}" >> /etc/to-creds
+echo "TO_URL=${TO_URI}" >> /etc/to-creds
+chmod +x /etc/to-creds
+
+/usr/bin/ln -s /trafficcontrol/tc-health-client/testing/tests /tests
+mkdir /tests/conf
+/usr/bin/cp /trafficcontrol/cache-config/testing/ort-tests/conf/docker-edge-cache.conf /tests/conf
+/usr/bin/cp /trafficcontrol/cache-config/testing/ort-tests/tc-fixtures.json /tests
+
+cd "$(realpath /tests)"
+touch test.log && chmod a+rw test.log && nohup tail -f test.log&
+go mod vendor -v
+
+# setup trafficserver config files
+/usr/bin/cp /strategies.yaml /opt/trafficserver/etc/trafficserver/strategies.yaml
+/usr/bin/cp /parent.config /opt/trafficserver/etc/trafficserver/parent.config
+# start trafficserver
+systemctl start trafficserver
+
+go test --cfg=conf/docker-edge-cache.conf 2>&1 >> test.log
+
+rm /tests/tc-fixtures.json
+
+exit 0
diff --git a/tc-health-client/testing/docker/health-check-test/strategies.yaml b/tc-health-client/testing/docker/health-check-test/strategies.yaml
new file mode 100644
index 0000000..6d2a934
--- /dev/null
+++ b/tc-health-client/testing/docker/health-check-test/strategies.yaml
@@ -0,0 +1,61 @@
+#
+# 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.
+#
+hosts:
+  - &mid3
+    host: dtrc-mid-03.kabletown.net
+    protocol:
+      - scheme: http
+        port: 80
+        health_check_url: http://192.168.2.1:80
+      - scheme: https
+        port: 443
+        health_check_url: https://192.168.2.1:443
+  - &mid16
+    host: atlanta-mid-16.ga.atlanta.kabletown.net
+    protocol:
+      - scheme: http
+        port: 80
+        health_check_url: http://192.168.2.2:80
+      - scheme: https
+        port: 443
+        health_check_url: https://192.168.2.2:443
+groups:
+  - &mid-tier
+    - <<: *mid3
+      weight: 0.5
+    - <<: *mid16
+      weight: 0.5
+strategies:
+  - strategy: "peering-g1"
+    policy: consistent_hash
+    cache_peer_result: false
+    go_direct: true
+    parent_is_proxy: true
+    groups: 
+      - *mid-tier
+    scheme: http 
+    failover:
+      max_simple_retries: 2 
+      ring_mode:
+        peering_ring
+      response_codes:
+        - 404
+      health_check:
+        - passive
+        - active
diff --git a/tc-health-client/testing/docker/health-check-test/systemctl.sh b/tc-health-client/testing/docker/health-check-test/systemctl.sh
new file mode 100755
index 0000000..d02c93d
--- /dev/null
+++ b/tc-health-client/testing/docker/health-check-test/systemctl.sh
@@ -0,0 +1,88 @@
+#!/bin/bash
+# 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.
+#
+
+# This is a work around for testing t3c which uses systemctl.
+# systemctl does not work in a container very well so this script
+# replaces systemctl in the container and always returns a 
+# sucessful result to t3c.
+
+USAGE="\nsystemctl COMMAND UNIT\n"
+
+if [ -z $1 ]; then
+  echo -e $USAGE
+  exit 0
+else
+  COMMAND=$1
+  UNIT=$2
+fi
+
+if [ "$COMMAND" != "daemon-reload" ]; then
+  if [ "$UNIT" != "trafficserver" ] && [ "$UNIT" != "tc-health-client" ]; then
+    echo -e "\nFailed to start ${UNIT}: Unit not found"
+    exit 0
+  fi
+fi
+
+case $COMMAND in 
+  daemon-reload)
+    ;;
+  enable)
+    ;;
+  restart)
+    case $UNIT in
+      trafficserver) 
+        /opt/trafficserver/bin/trafficserver restart
+        ;;
+      tc-health-client)
+        kill `cat /var/run/tc-health-client.pid`
+        tc-health-client -vvv &
+        ;;
+    esac
+    ;;
+  status)
+    if [ "${UNIT}" = "trafficserver" ]; then
+      /opt/trafficserver/bin/trafficserver status
+    fi
+    ;;
+  start)
+    case $UNIT in
+      trafficserver) 
+        /opt/trafficserver/bin/trafficserver start
+        ;;
+      tc-health-client)
+        nohup tc-health-client -vvv&
+        ;;
+    esac
+    ;;
+  stop)
+    if [ "${UNIT}" = "trafficserver" ]; then
+      /opt/trafficserver/bin/trafficserver stop
+    fi
+    case $UNIT in
+      trafficserver) 
+        /opt/trafficserver/bin/trafficserver start
+        ;;
+      tc-health-client)
+        kill `cat /var/run/tc-health-client.pid`
+        ;;
+    esac
+    ;;
+esac
+
+exit $?
diff --git a/tc-health-client/testing/docker/health-check-test/tc-health-client.json b/tc-health-client/testing/docker/health-check-test/tc-health-client.json
new file mode 100644
index 0000000..b6335e9
--- /dev/null
+++ b/tc-health-client/testing/docker/health-check-test/tc-health-client.json
@@ -0,0 +1,13 @@
+{
+  "cdn-name": "cdn1",
+  "enable-active-markdowns": true,
+  "reason-code": "active",
+  "to-credential-file": "/etc/to-creds",
+  "to-request-timeout-seconds": "5s",
+  "tm-poll-interval-seconds": "10s",
+  "tm-update-cycles": 20,
+  "unavailable-poll-threshold": 2,
+  "enable-poll-state-log": true,
+  "trafficserver-config-dir": "/opt/trafficserver/etc/trafficserver",
+  "trafficserver-bin-dir": "/opt/trafficserver/bin"
+}
diff --git a/tc-health-client/testing/docker/traffic_ops/Dockerfile b/tc-health-client/testing/docker/traffic_ops/Dockerfile
new file mode 100644
index 0000000..1e999e8
--- /dev/null
+++ b/tc-health-client/testing/docker/traffic_ops/Dockerfile
@@ -0,0 +1,84 @@
+# 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.
+
+############################################################
+# Dockerfile to build Traffic Ops container images
+# Based on CentOS 8
+############################################################
+
+ARG OS_DISTRO=rockylinux
+ARG OS_VERSION=8
+FROM ${OS_DISTRO}:${OS_VERSION}
+ARG OS_DISTRO
+ARG OS_VERSION
+ENV OS_DISTRO=${OS_DISTRO}
+ENV OS_VERSION=${OS_VERSION}
+
+RUN if [[ "${OS_VERSION%%.*}" -eq 7 ]]; then \
+		yum -y install dnf || exit 1; \
+	fi
+
+RUN set -o nounset -o errexit && \
+	mkdir -p /etc/cron.d; \
+	if [[ "${OS_VERSION%%.*}" -eq 7 ]]; then \
+		use_repo=''; \
+		enable_repo=''; \
+		# needed for llvm-toolset-7-clang, which is needed for postgresql13-devel-13.2-1PGDG, required by TO rpm
+		dnf -y install gcc centos-release-scl-rh; \
+	else \
+		use_repo='--repo=pgdg13'; \
+		enable_repo='--enablerepo=powertools'; \
+	fi; \
+	dnf -y install "https://download.postgresql.org/pub/repos/yum/reporpms/EL-${OS_VERSION%%.*}-x86_64/pgdg-redhat-repo-latest.noarch.rpm"; \
+	# libicu required by postgresql13
+	dnf -y install libicu; \
+	dnf -y $use_repo -- install postgresql13; \
+	dnf -y install epel-release; \
+	dnf -y $enable_repo install      \
+		bind-utils           \
+		gettext              \
+		# ip commands is used in set-to-ips-from-dns.sh
+		iproute              \
+		isomd5sum            \
+		jq                   \
+		libidn-devel         \
+		libpcap-devel        \
+		mkisofs              \
+		net-tools            \
+		nmap-ncat            \
+		openssl              \
+		perl-Crypt-ScryptKDF \
+		perl-Digest-SHA1     \
+		perl-JSON-PP         \
+		python3              \
+		# rsync is used to copy certs in "Shared SSL certificate generation" step
+		rsync;               \
+	dnf clean all
+
+EXPOSE 443
+
+WORKDIR /opt/traffic_ops/app
+
+RUN yum -y install procps
+ADD traffic_ops/traffic_ops*x86_64.rpm /traffic_ops.rpm
+RUN yum -y install /traffic_ops.rpm && \
+	rm /traffic_ops.rpm
+
+ADD traffic_ops/run.sh /
+
+EXPOSE 443
+CMD /run.sh
diff --git a/tc-health-client/testing/docker/traffic_ops/profile.origin.traffic_ops b/tc-health-client/testing/docker/traffic_ops/profile.origin.traffic_ops
new file mode 100644
index 0000000..f655f89
--- /dev/null
+++ b/tc-health-client/testing/docker/traffic_ops/profile.origin.traffic_ops
@@ -0,0 +1,18 @@
+{
+    "parameters": [
+        {
+            "config_file": "CRConfig.json",
+            "name": "domain_name",
+            "value": "{{.Domain}}"
+        },
+        {
+            "config_file": "parent.config",
+            "name": "weight",
+            "value": "1.0"
+        }
+    ],
+    "profile": {
+        "description": "Multi site origin profile 1",
+        "name": "ORG1_CDN1"
+    }
+}
diff --git a/tc-health-client/testing/docker/traffic_ops/run.sh b/tc-health-client/testing/docker/traffic_ops/run.sh
new file mode 100755
index 0000000..874aefc
--- /dev/null
+++ b/tc-health-client/testing/docker/traffic_ops/run.sh
@@ -0,0 +1,108 @@
+#!/usr/bin/env bash
+set -x
+# 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.
+
+# Script for running the Dockerfile for Traffic Ops.
+# The Dockerfile sets up a Docker image which can be used for any new Traffic Ops container;
+# This script, which should be run when the container is run (it's the ENTRYPOINT), will configure the container.
+#
+# The following environment variables must be set, ordinarily by `docker run -e` arguments:
+# DB_SERVER
+# DB_PORT
+# DB_ROOT_PASS
+# DB_USER
+# DB_USER_PASS
+# DB_NAME
+# TO_ADMIN_USER
+# TO_ADMIN_PASS
+# CERT_COUNTRY
+# CERT_STATE
+# CERT_CITY
+# CERT_COMPANY
+# TO_DOMAIN
+# TRAFFIC_VAULT_PASS
+
+# Check that env vars are set
+envvars=( DB_SERVER DB_PORT DB_ROOT_PASS DB_USER DB_USER_PASS TO_ADMIN_USER TO_ADMIN_PASS CERT_COUNTRY CERT_STATE CERT_CITY CERT_COMPANY TO_DOMAIN)
+for v in $envvars
+do
+	if [[ -z $$v ]]; then echo "$v is unset"; exit 1; fi
+done
+
+start() {
+  traffic_ops_golang_command=(./bin/traffic_ops_golang -cfg "$CDNCONF" -dbcfg "$DATABASECONF");
+  "${traffic_ops_golang_command[@]}" &
+	exec tail -f $TO_LOG
+}
+
+# generates and saves SSL certificates and database config files.
+init() {
+  # install certificates for TO
+  openssl req -newkey rsa:2048 -nodes -keyout /etc/pki/tls/private/localhost.key -x509 -days 365 \
+    -out /etc/pki/tls/certs/localhost.crt -subj "/C=$CERT_COUNTRY/ST=$CERT_STATE/L=$CERT_CITY/O=$CERT_COMPANY"
+  cp /etc/pki/tls/certs/localhost.crt /etc/pki/tls/certs/ca-bundle.crt
+
+  # update the base_url in cdn.conf
+  sed -i -e "s#http://localhost\:3000#http://${TO_HOSTNAME}\:443#" $CDNCONF
+	sed -i -e 's#https://\[::\]#https://127\.0\.0\.1#' $CDNCONF
+  sed -i -e 's#"use_ims": false,#"use_ims": true,#' $CDNCONF
+  #
+  cat > $DATABASECONF << EOM
+{
+  "type" : "Pg",
+  "description" : "Pg database on localhost:5432",
+  "port" : "$DB_PORT",
+  "dbname" : "$DB_NAME",
+  "password" : "$DB_USER_PASS",
+  "hostname" : "$DB_SERVER",
+  "user" : "$DB_USER"
+}
+EOM
+
+  cat > $DBCONF << EOM
+version: "1.0"
+name: dbconf.yml
+
+development:
+  driver: postgres
+  open: host=$DB_SERVER port=5432 user=traffic_ops password=$DB_USER_PASS dbname=to_development sslmode=disable
+
+test:
+  driver: postgres
+  open: host=$DB_SERVER port=5432 user=traffic_ops password=$DB_USER_PASS dbname=to_test sslmode=disable
+
+integration:
+  driver: postgres
+  open: host=$DB_SERVER port=5432 user=traffic_ops password=$DB_USER_PASS dbname=to_integration sslmode=disable
+
+production:
+  driver: postgres
+  open: host=$DB_SERVER port=5432 user=traffic_ops password=$DB_USER_PASS dbname=traffic_ops sslmode=disable
+EOM
+
+  touch $LOG_DEBUG $LOG_ERROR $LOG_EVENT $LOG_INFO $LOG_WARN $TO_LOG
+}
+
+source /etc/environment
+if [ -z "$INITIALIZED" ]; then init; fi
+
+# create the 'traffic_ops' database, tables and runs migrations
+(cd /opt/traffic_ops/app && db/admin --env=production reset > /admin.log 2>&1)
+
+# start traffic_ops
+start
diff --git a/tc-health-client/testing/docker/variables.env b/tc-health-client/testing/docker/variables.env
new file mode 100644
index 0000000..08ff604
--- /dev/null
+++ b/tc-health-client/testing/docker/variables.env
@@ -0,0 +1,50 @@
+# 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.
+#
+CDNCONF=/opt/traffic_ops/app/conf/cdn.conf
+CERT_COUNTRY=US
+CERT_STATE=Colorado
+CERT_CITY=Denver
+CERT_COMPANY=NotComcast
+DBCONF=/opt/traffic_ops/app/db/dbconf.yml
+DATABASECONF=/opt/traffic_ops/app/conf/production/database.conf
+POSTGRES_HOME=/usr/pgsql-13
+PGPASSWORD=secretrootpass
+POSTGRES_PASSWORD=secretrootpass
+DB_NAME=traffic_ops
+DB_PORT=5432
+DB_ROOT_PASS=null
+DB_USER=traffic_ops
+DB_USER_PASS=twelve
+DB_SERVER=db
+LOG_DEBUG=/var/log/traffic_ops/debug.log
+LOG_ERROR=/var/log/traffic_ops/error.log
+LOG_EVENT=/var/log/traffic_ops/event.log
+LOG_INFO=/var/log/traffic_ops/info.log
+LOG_WARN=/var/log/traffic_ops/warn
+TV_ADMIN_USER=admin
+TV_ADMIN_PASSWORD=tvsecret
+TV_FQDN=localhost
+TO_ADMIN_USER=admin
+TO_ADMIN_PASS=twelve
+TO_HOSTNAME=to_server
+TO_LOG=/var/log/traffic_ops/traffic_ops.log
+TO_DOMAIN=trafficops_default
+TO_URI=https://to_server:443
+X509_CA_PERSIST_DIR=/ca
+X509_CA_PERSIST_ENV_FILE=/ca/environment
+TV_HTTPS_PORT=8088
diff --git a/tc-health-client/testing/tests/conf/docker-edge-cache.conf b/tc-health-client/testing/tests/conf/docker-edge-cache.conf
new file mode 100644
index 0000000..d06370c
--- /dev/null
+++ b/tc-health-client/testing/tests/conf/docker-edge-cache.conf
@@ -0,0 +1,38 @@
+{
+    "default": {
+        "logLocations": {
+            "debug": "stdout",
+            "error": "stdout",
+            "event": "stdout",
+            "info": "stdout",
+            "warning": "stdout"
+        },
+        "session": {
+            "timeoutInSecs": 60
+        },
+        "includeSystemTests": false
+    },
+    "use_ims": true,
+    "trafficOps": {
+        "URL": "https://to_server:443",
+        "password": "twelve",
+        "users": {
+            "disallowed": "disallowed",
+            "operations": "operations",
+            "admin": "admin",
+            "federation": "federation",
+            "portal": "portal",
+            "readOnly": "readOnly",
+            "extension": "extension"
+        }
+    },
+    "trafficOpsDB": {
+        "dbname": "traffic_ops",
+        "description": "Test database to_test",
+        "hostname": "db",
+        "password": "twelve",
+        "port": "5432",
+        "type": "Pg",
+        "user": "traffic_ops"
+    }
+}
diff --git a/tc-health-client/testing/tests/hcutil/hcutil.go b/tc-health-client/testing/tests/hcutil/hcutil.go
new file mode 100644
index 0000000..e2ce6d8
--- /dev/null
+++ b/tc-health-client/testing/tests/hcutil/hcutil.go
@@ -0,0 +1,42 @@
+/*
+   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.
+*/
+
+package hcutil
+
+import (
+	"bytes"
+	"os/exec"
+)
+
+func Do(cmdStr string, args ...string) ([]byte, []byte, int) {
+	cmd := exec.Command(cmdStr, args...)
+
+	var outbuf bytes.Buffer
+	var errbuf bytes.Buffer
+
+	cmd.Stdout = &outbuf
+	cmd.Stderr = &errbuf
+
+	code := 0
+	err := cmd.Run()
+	if err != nil {
+		if exitErr, ok := err.(*exec.ExitError); !ok {
+			return nil, []byte(err.Error()), -1
+		} else {
+			code = exitErr.ExitCode()
+		}
+	}
+
+	return outbuf.Bytes(), errbuf.Bytes(), code
+}
diff --git a/tc-health-client/testing/tests/health-client-main_test.go b/tc-health-client/testing/tests/health-client-main_test.go
new file mode 100644
index 0000000..9685959
--- /dev/null
+++ b/tc-health-client/testing/tests/health-client-main_test.go
@@ -0,0 +1,118 @@
+package hctest
+
+/*
+   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 (
+	"database/sql"
+	"flag"
+	"fmt"
+	"os"
+	"testing"
+	"time"
+
+	"github.com/apache/trafficcontrol/cache-config/testing/ort-tests/config"
+	"github.com/apache/trafficcontrol/cache-config/testing/ort-tests/tcdata"
+	"github.com/apache/trafficcontrol/lib/go-log"
+)
+
+const cfgFmt = `Using Config values:
+  TO Config File:              %s
+  TO Fixtures:                 %s
+	TO URL:                      %s
+	TO Session Timeout In Secs:  %d
+	DB Server:                   %s
+	DB User:                     %s
+	DB Name:                     %s
+	DB Ssl:                      %t
+	UseIMS:                      %t`
+
+var (
+	Config             config.Config
+	testData           tcdata.TrafficControl
+	includeSystemTests bool
+	tcd                *tcdata.TCData
+	TCD                *tcdata.TCData
+)
+
+func TestMain(m *testing.M) {
+	tcd = tcdata.NewTCData()
+	configFileName := flag.String("cfg", "traffic-ops-test.conf", "The config file path")
+	tcFixturesFileName := flag.String("fixtures", "tc-fixtures.json", "The test fixtures")
+	cliIncludeSystemTests := *flag.Bool("includeSystemTests", false, "Whether to enable tests that have environment dependencies beyond a database")
+
+	flag.Parse()
+
+	// Skip loading configuration when run with `go test -list=<pat>`. The -list
+	// flag does not actually run tests, so configuration data is not needed in
+	// that mode. If the user is just trying to list the available tests we
+	// don't want to abort with an error about a bad configuration the user
+	// doesn't care about yet.
+	if f := flag.Lookup("test.list"); f != nil {
+		if f.Value.String() != "" {
+			os.Exit(m.Run())
+		}
+	}
+
+	var err error
+
+	if *tcd.Config, err = config.LoadConfig(*configFileName); err != nil {
+		fmt.Fprintf(os.Stderr, "Error Loading Config: %v\n", err)
+		os.Exit(1)
+	}
+	fmt.Fprintf(os.Stdout, "Config: %v\n", tcd.Config)
+
+	// CLI option overrides config
+	includeSystemTests = tcd.Config.Default.IncludeSystemTests || cliIncludeSystemTests
+
+	if err = log.InitCfg(tcd.Config); err != nil {
+		fmt.Fprintf(os.Stderr, "Error initializing loggers: %v\n", err)
+		os.Exit(1)
+	}
+
+	log.Infof(cfgFmt, *configFileName, *tcFixturesFileName, tcd.Config.TrafficOps.URL, tcd.Config.Default.Session.TimeoutInSecs, tcd.Config.TrafficOpsDB.Hostname, tcd.Config.TrafficOpsDB.User, tcd.Config.TrafficOpsDB.Name, tcd.Config.TrafficOpsDB.SSL, tcd.Config.UseIMS)
+
+	//Load the test data
+	tcd.LoadFixtures(*tcFixturesFileName)
+
+	var db *sql.DB
+	db, err = tcd.OpenConnection()
+	if err != nil {
+		log.Errorf("\nError opening connection to %s - %s, %v\n", tcd.Config.TrafficOps.URL, tcd.Config.TrafficOpsDB.User, err)
+		os.Exit(1)
+	}
+	defer db.Close()
+
+	err = tcd.Teardown(db)
+	if err != nil {
+		log.Errorf("\nError tearingdown data %s - %s, %v\n", tcd.Config.TrafficOps.URL, tcd.Config.TrafficOpsDB.User, err)
+	}
+
+	err = tcd.SetupTestData(db)
+	if err != nil {
+		log.Errorf("setting up data on TO instance %s as DB user '%s' failed: %v\n", tcd.Config.TrafficOps.URL, tcd.Config.TrafficOpsDB.User, err)
+		os.Exit(1)
+	}
+
+	toReqTimeout := time.Second * time.Duration(tcd.Config.Default.Session.TimeoutInSecs)
+	err = tcd.SetupSession(toReqTimeout, tcd.Config.TrafficOps.URL, tcd.Config.TrafficOps.Users.Admin, tcd.Config.TrafficOps.UserPassword)
+	if err != nil {
+		log.Errorf("\nError creating session to %s - %s, %v\n", tcd.Config.TrafficOps.URL, tcd.Config.TrafficOpsDB.User, err)
+		os.Exit(1)
+	}
+
+	// Now run the test case
+	rc := m.Run()
+	os.Exit(rc)
+}
diff --git a/tc-health-client/testing/tests/health-client-startup_test.go b/tc-health-client/testing/tests/health-client-startup_test.go
new file mode 100644
index 0000000..60c198b
--- /dev/null
+++ b/tc-health-client/testing/tests/health-client-startup_test.go
@@ -0,0 +1,128 @@
+/*
+   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.
+*/
+
+package hctest
+
+import (
+	"encoding/json"
+	"fmt"
+	"io/ioutil"
+	"os"
+	"testing"
+	"time"
+
+	"github.com/apache/trafficcontrol/cache-config/testing/ort-tests/tcdata"
+	"github.com/apache/trafficcontrol/tc-health-client/testing/tests/hcutil"
+	"github.com/apache/trafficcontrol/tc-health-client/tmagent"
+)
+
+func startHealthClient() {
+	outbuf, errbuf, result := hcutil.Do("systemctl", "start", "tc-health-client", "-vvv")
+	if result != 0 {
+		fmt.Fprintf(os.Stdout, "Error starting the health-client: %s\n", string(errbuf))
+	} else {
+		fmt.Fprintf(os.Stdout, "the health-client was succesfully started: %s\n", string(outbuf))
+	}
+}
+
+func TestHealthClientStartup(t *testing.T) {
+	tcd.WithObjs(t, []tcdata.TCObj{
+		tcdata.CDNs, tcdata.Types, tcdata.Tenants, tcdata.Parameters,
+		tcdata.Profiles, tcdata.ProfileParameters, tcdata.Statuses,
+		tcdata.Divisions, tcdata.Regions, tcdata.PhysLocations,
+		tcdata.CacheGroups, tcdata.Servers, tcdata.Topologies,
+	}, func() {
+
+		// initialize variables
+		cfg := tmagent.ParentInfo{}
+		pollStateFile := "/var/log/trafficcontrol/poll-state.json"
+		atlantaMid := "atlanta-mid-16.ga.atlanta.kabletown.net"
+		dtrcMid := "dtrc-mid-02.kabletown.net"
+		rascal := "rascal01.kabletown.net"
+
+		waitTime, err := time.ParseDuration("5s")
+		if err != nil {
+			fmt.Fprintf(os.Stdout, "failed to parse a value for waitTime")
+			os.Exit(1)
+		}
+
+		// mark down some parents using ATS traffic_ctl
+		_, errbuf, result := hcutil.Do("/opt/trafficserver/bin/traffic_ctl", "host", "down", "--reason", "active", atlantaMid)
+		if result != 0 {
+			fmt.Fprintf(os.Stdout, "%s\n", string(errbuf))
+			t.Fatalf("unable to mark down parent '%s'\n", atlantaMid)
+		} else {
+			fmt.Fprintf(os.Stdout, "marked down '%s'\n", atlantaMid)
+		}
+		_, errbuf, result = hcutil.Do("/opt/trafficserver/bin/traffic_ctl", "host", "down", "--reason", "active", dtrcMid)
+		if result != 0 {
+			fmt.Fprintf(os.Stdout, "%s\n", string(errbuf))
+			t.Fatalf("unable to mark down parent '%s'\n", dtrcMid)
+		} else {
+			fmt.Fprintf(os.Stdout, "marked down '%s'\n", dtrcMid)
+		}
+
+		// startup the health-client
+		fmt.Fprintf(os.Stdout, "Starting the tc-health-client\n")
+		go startHealthClient()
+
+		// wait for the health client to write it's poll state
+		time.Sleep(waitTime)
+
+		fmt.Fprintf(os.Stdout, "Running tests\n")
+
+		// read the health-client poll-state file.
+		cfg = tmagent.ParentInfo{}
+		content, err := ioutil.ReadFile(pollStateFile)
+		if err != nil {
+			t.Fatalf("could not read the %s file: %s\n", pollStateFile, err.Error())
+		}
+		err = json.Unmarshal(content, &cfg)
+		if err != nil {
+			t.Fatalf("could not unmarshal %s: %s\n", pollStateFile, err.Error())
+		}
+
+		// we marked down mids, now test that the health client read the ATS
+		// Host Status and see's that they are down.
+		parents := cfg.Parents
+		p := parents[atlantaMid]
+		if p.ActiveReason != false {
+			t.Fatalf("Expected %s to be marked down but it's not", atlantaMid)
+		} else {
+			fmt.Fprintf(os.Stdout, "%s is available: %v\n", atlantaMid, p.ActiveReason)
+		}
+		p = parents[dtrcMid]
+		if p.ActiveReason != false {
+			t.Fatalf("Expected %s to be marked down but it's not", dtrcMid)
+		} else {
+			fmt.Fprintf(os.Stdout, "%s is available: %v\n", dtrcMid, p.ActiveReason)
+		}
+
+		// verify that the health-client was able to poll and get an available
+		// traffic monitor from TrafficOps
+		av := cfg.Cfg.TrafficMonitors[rascal]
+		if av != true {
+			t.Fatalf("Expected %s to be available but it's not", rascal)
+		} else {
+			fmt.Fprintf(os.Stdout, "%s is available: %v\n", rascal, av)
+		}
+
+		fmt.Fprintf(os.Stdout, "Stopping the tc-health-client\n")
+		_, errbuf, result = hcutil.Do("/usr/bin/systemctl", "stop", "tc-health-client", "-vvv")
+		if result != 0 {
+			fmt.Fprintf(os.Stdout, "%s\n", string(errbuf))
+			t.Fatalf("unable to stop the tc-health-client\n")
+		}
+	})
+}
diff --git a/tc-health-client/tmagent/tmagent.go b/tc-health-client/tmagent/tmagent.go
index e9211a7..0568587 100644
--- a/tc-health-client/tmagent/tmagent.go
+++ b/tc-health-client/tmagent/tmagent.go
@@ -22,6 +22,7 @@ package tmagent
 import (
 	"bufio"
 	"bytes"
+	"encoding/json"
 	"errors"
 	"fmt"
 	"io/ioutil"
@@ -325,6 +326,15 @@ func (c *ParentInfo) PollAndUpdateCacheStatus() {
 			} else {
 				log.Infoln("updated TrafficMonitor statuses from TrafficOps")
 			}
+
+			// log the poll state data if enabled
+			if c.Cfg.EnablePollStateLog {
+				err = c.WritePollState()
+				if err != nil {
+					log.Errorf("could not write the poll state log: %s\n", err.Error())
+				}
+			}
+
 			time.Sleep(pollingInterval)
 			continue
 		}
@@ -375,6 +385,14 @@ func (c *ParentInfo) PollAndUpdateCacheStatus() {
 			cycleCount++
 		}
 
+		// log the poll state data if enabled
+		if c.Cfg.EnablePollStateLog {
+			err = c.WritePollState()
+			if err != nil {
+				log.Errorf("could not write the poll state log: %s\n", err.Error())
+			}
+		}
+
 		time.Sleep(pollingInterval)
 	}
 }
@@ -418,6 +436,19 @@ func (c *ParentInfo) UpdateParentInfo() error {
 	return nil
 }
 
+func (c *ParentInfo) WritePollState() error {
+	data, err := json.MarshalIndent(c, "", "\t")
+	if err != nil {
+		return fmt.Errorf("marshaling configuration state: %s\n", err.Error())
+	} else {
+		err = os.WriteFile(c.Cfg.PollStateJSONLog, data, 0644)
+		if err != nil {
+			return fmt.Errorf("writing configuration state: %s\n", err.Error())
+		}
+	}
+	return nil
+}
+
 // choose an available trafficmonitor, returns an error if
 // there are none.
 func (c *ParentInfo) findATrafficMonitor() (string, error) {