├── .dockerignore ├── .github ├── dependabot.yml └── workflows │ ├── build_and_test.yml │ ├── codeql-analysis.yml │ ├── dependabot_updates.yml │ └── publish.yml ├── Dockerfile ├── LICENSE ├── README.md ├── app.go ├── assets └── screenshot.jpg ├── docker-compose.yaml ├── go.mod └── go.sum /.dockerignore: -------------------------------------------------------------------------------- 1 | * 2 | 3 | !app.go 4 | !go.mod 5 | !go.sum 6 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "gomod" 9 | directory: "/" 10 | schedule: 11 | interval: "monthly" 12 | - package-ecosystem: "github-actions" 13 | directory: "/" 14 | schedule: 15 | interval: "monthly" 16 | -------------------------------------------------------------------------------- /.github/workflows/build_and_test.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build and test a Go application 2 | name: Build and Test 3 | 4 | on: 5 | push: 6 | branches: [master] 7 | pull_request: 8 | branches: [master] 9 | 10 | jobs: 11 | build_and_test: 12 | name: Build and Test 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - name: Checkout repo 17 | uses: actions/checkout@v4 18 | 19 | - name: Set up Go 20 | uses: actions/setup-go@v5 21 | with: 22 | go-version: 1.18 23 | check-latest: true 24 | cache: true 25 | 26 | - name: Build 27 | run: go build -v app.go 28 | 29 | # - name: Test 30 | # run: go test -v ./... 31 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # This action runs GitHub's industry-leading semantic code analysis engine, CodeQL, against a repository's source code to find security vulnerabilities. 2 | # It then automatically uploads the results to GitHub so they can be displayed in the repository's security tab. 3 | # https://github.com/github/codeql-action 4 | 5 | name: Code Scanning 6 | 7 | on: 8 | push: 9 | branches: [master] 10 | pull_request: 11 | branches: [master] 12 | 13 | jobs: 14 | codeql_analysis: 15 | name: Code Scanning 16 | # CodeQL runs on ubuntu-latest, windows-latest, and macos-latest 17 | runs-on: ubuntu-latest 18 | 19 | permissions: 20 | # required for all workflows 21 | security-events: write 22 | 23 | # only required for workflows in private repositories 24 | actions: read 25 | contents: read 26 | 27 | steps: 28 | - name: Checkout repository 29 | uses: actions/checkout@v4 30 | 31 | # Initializes the CodeQL tools for scanning. 32 | - name: Initialize CodeQL 33 | uses: github/codeql-action/init@v3 34 | # Override language selection by uncommenting this and choosing your languages 35 | # with: 36 | # languages: go, javascript, csharp, python, cpp, java 37 | 38 | # Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java). 39 | # If this step fails, then you should remove it and run the build manually (see below). 40 | - name: Autobuild 41 | uses: github/codeql-action/autobuild@v3 42 | 43 | # ℹ️ Command-line programs to run using the OS shell. 44 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 45 | 46 | # ✏️ If the Autobuild fails above, remove it and uncomment the following 47 | # three lines and modify them (or add more) to build your code if your 48 | # project uses a compiled language 49 | 50 | #- run: | 51 | # make bootstrap 52 | # make release 53 | 54 | - name: Perform CodeQL Analysis 55 | uses: github/codeql-action/analyze@v3 56 | -------------------------------------------------------------------------------- /.github/workflows/dependabot_updates.yml: -------------------------------------------------------------------------------- 1 | # Merge dependabot updates automatically 2 | # https://docs.github.com/en/code-security/dependabot/working-with-dependabot/automating-dependabot-with-github-actions 3 | 4 | name: Dependabot Auto-Merge 5 | on: pull_request 6 | 7 | permissions: 8 | contents: write 9 | pull-requests: write 10 | 11 | jobs: 12 | dependabot_updates: 13 | name: Dependabot Auto-Merge 14 | runs-on: ubuntu-latest 15 | if: ${{ github.actor == 'dependabot[bot]' }} 16 | 17 | steps: 18 | - name: Dependabot metadata 19 | id: metadata 20 | uses: dependabot/fetch-metadata@v2.4.0 21 | with: 22 | github-token: "${{ secrets.GITHUB_TOKEN }}" 23 | 24 | - name: Enable auto-merge for Dependabot PRs 25 | # if: ${{contains(steps.metadata.outputs.dependency-names, 'my-dependency') && steps.metadata.outputs.update-type == 'version-update:semver-patch'}} 26 | run: gh pr merge --auto --squash "$PR_URL" 27 | env: 28 | PR_URL: ${{github.event.pull_request.html_url}} 29 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 30 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build and publish a Docker Image to GitHub Container Registry 2 | # See https://github.com/docker/build-push-action 3 | 4 | name: Publish Image 5 | 6 | on: 7 | release: 8 | types: [published] 9 | 10 | env: 11 | REGISTRY: ghcr.io 12 | IMAGE_NAME: rafhaanshah/container-mon 13 | 14 | jobs: 15 | push_to_registry: 16 | name: Push Docker Image to GitHub Container Registry 17 | runs-on: ubuntu-latest 18 | 19 | steps: 20 | - name: Set version variable 21 | id: version 22 | run: | 23 | echo "tag=${GITHUB_REF#refs/*/}" 24 | echo "tag=${GITHUB_REF#refs/*/}" >> $GITHUB_OUTPUT 25 | 26 | - name: Set up QEMU 27 | uses: docker/setup-qemu-action@v3 28 | 29 | - name: Set up Docker Buildx 30 | uses: docker/setup-buildx-action@v3 31 | 32 | - name: Login to GitHub Container Registry 33 | uses: docker/login-action@v3 34 | with: 35 | registry: ${{ env.REGISTRY }} 36 | username: ${{ github.actor }} 37 | password: ${{ secrets.GITHUB_TOKEN }} 38 | 39 | - name: Build and push 40 | id: docker_build 41 | uses: docker/build-push-action@v6 42 | with: 43 | platforms: linux/amd64,linux/arm64,linux/arm/v7 44 | push: true 45 | tags: | 46 | ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.tag }} 47 | ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest 48 | 49 | - name: Print image digest 50 | run: echo ${{ steps.docker_build.outputs.digest }} 51 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Build the app 2 | FROM golang:1.18-alpine AS build 3 | 4 | WORKDIR /build 5 | COPY . . 6 | RUN go build app.go 7 | 8 | # Run the app 9 | FROM alpine:latest 10 | 11 | LABEL org.opencontainers.image.source https://github.com/RafhaanShah/Container-Mon 12 | 13 | WORKDIR /app 14 | COPY --from=build /build/app /app 15 | ENTRYPOINT ["./app"] 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) [2020] [Rafhaan Shah] 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # Container-Mon 3 | 4 | Get notified when your [Docker](https://www.docker.com/) containers are unhealthy. 5 | 6 | ![](/assets/screenshot.jpg) 7 | 8 | ## Prerequisites 9 | - Have [Go](https://golang.org/) 1.18+ or [Docker](https://www.docker.com/) installed 10 | - A notification service supported by [Shoutrrr](https://containrrr.dev/shoutrrr/services/overview/) and the required API keys or other configuration for your chosen service (e.g: Telegram, Discord, Slack, Teams etc) 11 | 12 | ## Configuration 13 | All configuration is done via environment variables, see the table below for all options and default values. Only `CONTAINERMON_NOTIFICATION_URL` is mandatory, all other fields are optional. 14 | | Name | Type | Default Value | Description | 15 | |---------------------------------|--------|-----------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 16 | | CONTAINERMON\_FAIL\_LIMIT | Int | 1 | Number of consecutive 'unhealthy' checks to reach before sending a notification | 17 | | CONTAINERMON\_CRON | String | */5 * * * * | Standard [Cron](https://crontab.guru/#*/5_*_*_*_*) schedule of when to run healthchecks | 18 | | CONTAINERMON\_NOTIFICATION\_URL | String | N/A | Notification URL for [Shoutrrr](https://containrrr\.dev/shoutrrr/services/overview/). Multiple services can be used with the `\|` (pipe) character as a separator. | 19 | | CONTAINERMON\_USE\_LABELS | Bool | false | If `true` will only monitor containers with the label `containermon.enable=true` set | 20 | | CONTAINERMON\_NOTIFY\_HEALTHY | Bool | true | If `true` will send a notification when an 'unhealthy' container returns to being 'healthy' | 21 | | CONTAINERMON\_CHECK\_STOPPED | Bool | true | If `true` will consider `stopped` containers as 'unhealthy'\. If `false`, you will only be notified for containers that have a `healthcheck` set | 22 | | CONTAINERMON\_MESSAGE\_PREFIX | String | N/A | Custom text to be prefixed to all notification messages. | 23 | | DOCKER\_HOST | String | /var/run/docker\.sock | Path for the Docker API socket | 24 | | DOCKER\_API\_VERSION | String | docker default | Docker API version to use | 25 | | DOCKER\_CERT\_PATH | String | docker default | Path to load the TLS certificates from | 26 | | DOCKER\_TLS\_VERIFY | Bool | false | Enable or disable TLS verification | | | 27 | 28 | ## Usage 29 | - Stand-alone: 30 | `go run app.go` 31 | - Docker: 32 | ``` 33 | docker run \ 34 | -v /var/run/docker.sock:/var/run/docker.sock \ 35 | -e CONTAINERMON_NOTIFICATION_URL=telegram://token@telegram?channels=channel-1 \ 36 | ghcr.io/rafhaanshah/container-mon:latest 37 | ``` 38 | - Docker-Compose: 39 | ``` 40 | version: "3.8" 41 | services: 42 | container-mon: 43 | container_name: container-mon 44 | image: ghcr.io/rafhaanshah/container-mon:latest 45 | restart: unless-stopped 46 | volumes: 47 | - /var/run/docker.sock:/var/run/docker.sock 48 | environment: 49 | - CONTAINERMON_NOTIFICATION_URL=telegram://token@telegram?channels=channel-1 50 | ``` 51 | 52 | ## Troubleshooting 53 | - Docker API version issues: if you get error messages like `client version 1.43 is too new. Maximum supported API version is 1.42` then please set the `DOCKER_API_VERSION` environment variable to the latest version supported by your Docker engine (e.g. `DOCKER_API_VERSION=1.42`, which you can check by running `docker version`. 54 | - Notifier issues: please check if your URL works with the Shoutrrr CLI from [here](https://containrrr.dev/shoutrrr/0.7/getting-started/#through_the_cli). 55 | 56 | ## Security Considerations 57 | - It can be considered a security risk to directly map your Docket socket inside a container. A proxy such as [Socket-Proxy](https://github.com/Tecnativa/docker-socket-proxy) can be used to give fine-grained access to parts of the Docker API, this application only needs to be able to read a list of running containers -> 58 | ``` 59 | docker run \ 60 | -e DOCKER_HOST=tcp://socket-proxy:2375 61 | ... 62 | ``` 63 | - This container runs as `root` by default to access the Docker socket. You may run it as another user that has access to the socket as described here: [Running a Docker container as a non-root user](https://medium.com/redbubble/running-a-docker-container-as-a-non-root-user-7d2e00f8ee15) -> 64 | ``` 65 | docker run \ 66 | -u $(id -u):$(stat -c '%g' "/var/run/docker.sock") \ 67 | ... 68 | ``` 69 | 70 | ## License 71 | [MIT](https://choosealicense.com/licenses/mit/) 72 | -------------------------------------------------------------------------------- /app.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | "strconv" 8 | "strings" 9 | "time" 10 | 11 | "github.com/containrrr/shoutrrr" 12 | "github.com/docker/docker/api/types" 13 | "github.com/docker/docker/api/types/filters" 14 | "github.com/docker/docker/client" 15 | "github.com/robfig/cron/v3" 16 | ) 17 | 18 | const ( 19 | containerLabel = "containermon.enable" 20 | senderSplitter = "|" 21 | envFailLimit = "CONTAINERMON_FAIL_LIMIT" 22 | envCronSchedule = "CONTAINERMON_CRON" 23 | envNotificationURL = "CONTAINERMON_NOTIFICATION_URL" 24 | envUseLabels = "CONTAINERMON_USE_LABELS" 25 | envNotifyWhenHealthy = "CONTAINERMON_NOTIFY_HEALTHY" 26 | envCheckStoppedContainers = "CONTAINERMON_CHECK_STOPPED" 27 | envMessagePrefix = "CONTAINERMON_MESSAGE_PREFIX" 28 | ) 29 | 30 | type config struct { 31 | failLimit int 32 | cronSchedule string 33 | notificationURL string 34 | useLabels bool 35 | notifyWhenHealthy bool 36 | checkStoppedContainers bool 37 | messagePrefix string 38 | } 39 | 40 | func main() { 41 | fmt.Println("Starting up Container-Mon") 42 | 43 | conf := getConfig() 44 | cli := getDockerClient() 45 | ctx := context.Background() 46 | cMap := make(map[string]int) 47 | 48 | cr := cron.New() 49 | cr.AddFunc(conf.cronSchedule, func() { 50 | err := checkContainers(ctx, cli, conf, cMap) 51 | if err != nil { 52 | fmt.Println(fmt.Sprintf("Error checking containers: %v", err)) 53 | } 54 | }) 55 | 56 | cr.Run() 57 | } 58 | 59 | func checkContainers(ctx context.Context, cli *client.Client, conf config, cMap map[string]int) error { 60 | // Get the list of containers 61 | cList, err := getContainers(ctx, cli, conf.useLabels, conf.checkStoppedContainers) 62 | if err != nil { 63 | return err 64 | } 65 | 66 | // If the map has containers that are not in the new list, remove them 67 | for cID := range cMap { 68 | if !containerInList(cID, cList) { 69 | delete(cMap, cID) 70 | } 71 | } 72 | 73 | // If the list has containers that are not in the map, add them with a fail count of 0 74 | for _, c := range cList { 75 | if _, ok := cMap[c.ID]; !ok { 76 | cMap[c.ID] = 0 77 | } 78 | } 79 | 80 | // Loop over the list of containers 81 | for _, c := range cList { 82 | // If the container is healthy, set the fail count to 0 83 | if isHealthy(ctx, cli, c) { 84 | // If it was previously unhealthy, notify that it is now healthy 85 | if conf.notifyWhenHealthy && cMap[c.ID] < 0 { 86 | notify(conf.notificationURL, c.Names[0], true, conf.messagePrefix) 87 | } 88 | cMap[c.ID] = 0 89 | } else { 90 | // If the container is not healthy and we have not yet sent a notification for it, add +1 to it's fail count 91 | if cMap[c.ID] > -1 { 92 | count := cMap[c.ID] + 1 93 | cMap[c.ID] = count 94 | // If the fail count has reached the max count, send a notification and set the count to -1 95 | if count >= conf.failLimit { 96 | cMap[c.ID] = -1 97 | notify(conf.notificationURL, c.Names[0], false, conf.messagePrefix) 98 | } 99 | } 100 | } 101 | } 102 | 103 | return nil 104 | } 105 | 106 | func containerInList(a string, list []types.Container) bool { 107 | for _, b := range list { 108 | if b.ID == a { 109 | return true 110 | } 111 | } 112 | 113 | return false 114 | } 115 | 116 | func getContainers(ctx context.Context, cli *client.Client, filterByLabel bool, checkStoppedContainers bool) ([]types.Container, error) { 117 | args := filters.NewArgs() 118 | if filterByLabel { 119 | args.Add("label", fmt.Sprintf("%v=true", containerLabel)) 120 | } 121 | 122 | return cli.ContainerList(ctx, types.ContainerListOptions{ 123 | All: checkStoppedContainers, 124 | Filters: args, 125 | }) 126 | } 127 | 128 | func isHealthy(ctx context.Context, cli *client.Client, c types.Container) bool { 129 | running := c.State == "running" 130 | 131 | containerJSON, err := cli.ContainerInspect(ctx, c.ID) 132 | if err != nil { 133 | return running 134 | } 135 | 136 | health := containerJSON.State.Health 137 | if health == nil { 138 | return running 139 | } 140 | 141 | if health.Status == types.NoHealthcheck || health.Status == types.Starting { 142 | return running 143 | } 144 | 145 | healthy := health.Status == types.Healthy 146 | return healthy 147 | } 148 | 149 | func getConfig() config { 150 | c := config{ 151 | failLimit: getEnvInt(envFailLimit, 1), 152 | cronSchedule: getEnv(envCronSchedule, "*/5 * * * *", true), 153 | notificationURL: getEnv(envNotificationURL, "", true), 154 | useLabels: getEnvBool(envUseLabels, false), 155 | notifyWhenHealthy: getEnvBool(envNotifyWhenHealthy, true), 156 | checkStoppedContainers: getEnvBool(envCheckStoppedContainers, true), 157 | messagePrefix: getEnv(envMessagePrefix, "", false), 158 | } 159 | 160 | fmt.Println("Using config:") 161 | fmt.Println(fmt.Sprintf(" - failure limit: %v", c.failLimit)) 162 | fmt.Println(fmt.Sprintf(" - cron schedule: %v", c.cronSchedule)) 163 | fmt.Println(fmt.Sprintf(" - notification service: %v", strings.Split(c.notificationURL, "://")[0])) 164 | fmt.Println(fmt.Sprintf(" - use labels: %v", c.useLabels)) 165 | fmt.Println(fmt.Sprintf(" - notify when healthy: %v", c.notifyWhenHealthy)) 166 | fmt.Println(fmt.Sprintf(" - check stopped containers: %v", c.checkStoppedContainers)) 167 | fmt.Println(fmt.Sprintf(" - message prefix: %v", c.messagePrefix)) 168 | 169 | return c 170 | } 171 | 172 | func getEnvInt(key string, fallback int) int { 173 | if value, ok := os.LookupEnv(key); ok { 174 | ret, err := strconv.Atoi(value) 175 | if err == nil { 176 | return ret 177 | } 178 | } 179 | 180 | return fallback 181 | } 182 | 183 | func getEnvBool(key string, fallback bool) bool { 184 | if value, ok := os.LookupEnv(key); ok { 185 | ret, err := strconv.ParseBool(value) 186 | if err == nil { 187 | return ret 188 | } 189 | } 190 | 191 | return fallback 192 | } 193 | 194 | func getEnv(key string, fallback string, trim bool) string { 195 | if value, ok := os.LookupEnv(key); ok { 196 | if trim { 197 | return strings.TrimSpace(value) 198 | } else { 199 | return value 200 | } 201 | } 202 | 203 | return fallback 204 | } 205 | 206 | func getDockerClient() *client.Client { 207 | cli, err := client.NewEnvClient() 208 | if err != nil { 209 | println("Error getting Docker Client, exiting...") 210 | panic(err) 211 | } 212 | 213 | return cli 214 | } 215 | 216 | func notify(notificationURL string, containerName string, healthy bool, messagePrefix string) { 217 | msg := fmt.Sprintf("%vContainer %v is not healthy", messagePrefix, containerName) 218 | if healthy { 219 | msg = fmt.Sprintf("%vContainer %v is back to healthy", messagePrefix, containerName) 220 | } 221 | 222 | currentTime := time.Now().Format(time.RFC3339) 223 | fmt.Println(fmt.Sprintf("%s | %s", currentTime, msg)) 224 | 225 | senders := strings.Split(notificationURL, senderSplitter) 226 | for i := range senders { 227 | err := shoutrrr.Send(senders[i], msg) 228 | if err != nil { 229 | fmt.Println(fmt.Sprintf("Error sending notification: %v", err)) 230 | } 231 | } 232 | } 233 | -------------------------------------------------------------------------------- /assets/screenshot.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RafhaanShah/Container-Mon/f74d5386eb1157d3abc63c5ed649e8eb1b77146b/assets/screenshot.jpg -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: "3.8" 2 | 3 | services: 4 | container-mon: 5 | container_name: container-mon 6 | image: ghcr.io/rafhaanshah/container-mon:latest 7 | restart: unless-stopped 8 | volumes: 9 | - /var/run/docker.sock:/var/run/docker.sock 10 | environment: 11 | - CONTAINERMON_FAIL_LIMIT=1 12 | - CONTAINERMON_CRON=*/5 * * * * 13 | - CONTAINERMON_NOTIFICATION_URL=telegram://token@telegram?channels=channel-1 14 | - CONTAINERMON_USE_LABELS=false 15 | - CONTAINERMON_NOTIFY_HEALTHY=true 16 | - CONTAINERMON_CHECK_STOPPED=true 17 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/rafhaanshah/container-mon 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/containrrr/shoutrrr v0.8.0 7 | github.com/docker/docker v24.0.9+incompatible 8 | github.com/robfig/cron/v3 v3.0.1 9 | ) 10 | 11 | require ( 12 | github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect 13 | github.com/Microsoft/go-winio v0.6.0 // indirect 14 | github.com/docker/distribution v2.8.2+incompatible // indirect 15 | github.com/docker/go-connections v0.4.0 // indirect 16 | github.com/docker/go-units v0.5.0 // indirect 17 | github.com/fatih/color v1.15.0 // indirect 18 | github.com/gogo/protobuf v1.3.2 // indirect 19 | github.com/mattn/go-colorable v0.1.13 // indirect 20 | github.com/mattn/go-isatty v0.0.17 // indirect 21 | github.com/moby/term v0.0.0-20210610120745-9d4ed1856297 // indirect 22 | github.com/morikuni/aec v1.0.0 // indirect 23 | github.com/opencontainers/go-digest v1.0.0 // indirect 24 | github.com/opencontainers/image-spec v1.1.0-rc2 // indirect 25 | github.com/pkg/errors v0.9.1 // indirect 26 | github.com/pmezard/go-difflib v1.0.0 // indirect 27 | golang.org/x/mod v0.9.0 // indirect 28 | golang.org/x/net v0.38.0 // indirect 29 | golang.org/x/sys v0.31.0 // indirect 30 | golang.org/x/time v0.3.0 // indirect 31 | golang.org/x/tools v0.7.0 // indirect 32 | gotest.tools/v3 v3.0.3 // indirect 33 | ) 34 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/Azure/go-ansiterm v0.0.0-20210608223527-2377c96fe795/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= 2 | github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= 3 | github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= 4 | github.com/Microsoft/go-winio v0.6.0 h1:slsWYD/zyx7lCXoZVlvQrj0hPTM1HI4+v1sIda2yDvg= 5 | github.com/Microsoft/go-winio v0.6.0/go.mod h1:cTAf44im0RAYeL23bpB+fzCyDH2MJiz2BO69KH/soAE= 6 | github.com/containrrr/shoutrrr v0.8.0 h1:mfG2ATzIS7NR2Ec6XL+xyoHzN97H8WPjir8aYzJUSec= 7 | github.com/containrrr/shoutrrr v0.8.0/go.mod h1:ioyQAyu1LJY6sILuNyKaQaw+9Ttik5QePU8atnAdO2o= 8 | github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 9 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 10 | github.com/docker/distribution v2.8.2+incompatible h1:T3de5rq0dB1j30rp0sA2rER+m322EBzniBPB6ZIzuh8= 11 | github.com/docker/distribution v2.8.2+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= 12 | github.com/docker/docker v24.0.9+incompatible h1:HPGzNmwfLZWdxHqK9/II92pyi1EpYKsAqcl4G0Of9v0= 13 | github.com/docker/docker v24.0.9+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= 14 | github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= 15 | github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= 16 | github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= 17 | github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= 18 | github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs= 19 | github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw= 20 | github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0= 21 | github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= 22 | github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= 23 | github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 24 | github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= 25 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 26 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 27 | github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= 28 | github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 h1:yAJXTCF9TqKcTiHJAE8dj7HMvPfh66eeA2JYW7eFpSE= 29 | github.com/jarcoal/httpmock v1.3.0 h1:2RJ8GP0IIaWwcC9Fp2BmVi8Kog3v2Hn7VXM3fTd+nuc= 30 | github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= 31 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 32 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 33 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 34 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 35 | github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= 36 | github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 37 | github.com/moby/term v0.0.0-20210610120745-9d4ed1856297 h1:yH0SvLzcbZxcJXho2yh7CqdENGMQe73Cw3woZBpPli0= 38 | github.com/moby/term v0.0.0-20210610120745-9d4ed1856297/go.mod h1:vgPCkQMyxTZ7IDy8SXRufE172gr8+K/JE/7hHFxHW3A= 39 | github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= 40 | github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= 41 | github.com/onsi/ginkgo/v2 v2.9.2 h1:BA2GMJOtfGAfagzYtrAlufIP0lq6QERkFmHLMLPwFSU= 42 | github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE= 43 | github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= 44 | github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= 45 | github.com/opencontainers/image-spec v1.1.0-rc2 h1:2zx/Stx4Wc5pIPDvIxHXvXtQFW/7XWJGmnM7r3wg034= 46 | github.com/opencontainers/image-spec v1.1.0-rc2/go.mod h1:3OVijpioIKYWTqjiG0zfF6wvoJ4fAXGbjdZuI2NgsRQ= 47 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 48 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 49 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 50 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 51 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 52 | github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= 53 | github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= 54 | github.com/sirupsen/logrus v1.7.0 h1:ShrD1U9pZB12TX0cVy0DtePoCH97K8EtX+mg7ZARUtM= 55 | github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 56 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 57 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 58 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 59 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 60 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 61 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 62 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 63 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 64 | golang.org/x/mod v0.9.0 h1:KENHtAZL2y3NLMYZeHY9DW8HW8V+kQyJsY/V9JlKvCs= 65 | golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 66 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 67 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 68 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 69 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 70 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 71 | golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= 72 | golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= 73 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 74 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 75 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 76 | golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= 77 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 78 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 79 | golang.org/x/sys v0.0.0-20200831180312-196b9ba8737a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 80 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 81 | golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 82 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 83 | golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= 84 | golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 85 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 86 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 87 | golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= 88 | golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= 89 | golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 90 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 91 | golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 92 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 93 | golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 94 | golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 95 | golang.org/x/tools v0.7.0 h1:W4OVu8VVOaIO0yzWMNdepAulS7YfoS3Zabrm8DOXXU4= 96 | golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s= 97 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 98 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 99 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 100 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 101 | google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= 102 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 103 | gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk= 104 | gotest.tools/v3 v3.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0= 105 | gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8= 106 | --------------------------------------------------------------------------------