diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 71e06e8..42aa2d8 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -8,7 +8,7 @@ on: - main jobs: - test: + test-unit: name: Unit Tests runs-on: ubuntu-latest steps: @@ -28,6 +28,19 @@ jobs: path: out/coverage/ retention-days: 7 + test-accept: + name: Acceptance Tests + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Run acceptance tests + run: make test-accept + # NOTE: Docker build and push steps are commented out due to org package permissions. # Uncomment this job when you have permission to push to ghcr.io # diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9f53f0a..01e05de 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -15,7 +15,7 @@ on: type: string jobs: - test: + test-unit: name: Unit Tests runs-on: ubuntu-latest steps: @@ -28,13 +28,26 @@ jobs: - name: Run unit tests run: make test-unit + test-accept: + name: Acceptance Tests + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Run acceptance tests + run: make test-accept + # NOTE: Docker build and push job is commented out due to org package permissions. # Uncomment this job when you have permission to push to ghcr.io # # build-and-push: # name: Build and Push # runs-on: ubuntu-latest - # needs: test + # needs: [test-unit, test-accept] # permissions: # contents: read # packages: write @@ -103,7 +116,7 @@ jobs: create-release: name: Create GitHub Release runs-on: ubuntu-latest - needs: test + needs: [test-unit, test-accept] if: startsWith(github.ref, 'refs/tags/') permissions: contents: write diff --git a/Makefile b/Makefile index 5b3a070..7acb3f0 100644 --- a/Makefile +++ b/Makefile @@ -9,7 +9,7 @@ # governing permissions and limitations under the License. SHELL := /bin/bash -.PHONY: build test test-unit test-accept clean help fmt lint vet check build-local ci all +.PHONY: build test test-all test-unit test-accept clean help fmt lint vet check build-local ci all # Project configuration SERVICE_NAME := butler @@ -69,7 +69,11 @@ build-multiplatform: # Test targets using docker buildx bake ######################################################################################################################## -test: test-unit test-accept +test: test-unit + @echo "Unit tests passed" + @echo "Note: Run 'make test-accept' separately for acceptance tests (requires valid TLS certs)" + +test-all: test-unit test-accept @echo "All tests passed" test-unit: @@ -251,7 +255,8 @@ help: @printf " make build-multiplatform Build for amd64 and arm64\n" @printf "\n" @printf "Test targets:\n" - @printf " make test Run all tests (unit + acceptance)\n" + @printf " make test Run unit tests (default)\n" + @printf " make test-all Run all tests (unit + acceptance)\n" @printf " make test-unit Run unit tests in container\n" @printf " make test-accept Run acceptance tests in container\n" @printf " make test-local Run tests locally (no Docker)\n" diff --git a/README.md b/README.md index 57dbac3..8fa9404 100644 --- a/README.md +++ b/README.md @@ -305,6 +305,72 @@ When `skip-butler-header` is enabled: - JSON syntax validation still occurs for `.json` files - Text files are accepted without any validation +### Watch-Only Mode for ConfigMap Monitoring + +Butler supports a **watch-only mode** designed for Kubernetes ConfigMap monitoring scenarios. When enabled, butler monitors files for changes using hash comparison and triggers reloads, **without writing any files to disk**. + +This is particularly useful when: +- Configuration files are mounted from Kubernetes ConfigMaps (read-only) +- You want butler to detect changes and trigger reloads without file I/O overhead +- The source files are already in place and just need change detection + +#### Configuration + +Enable watch-only mode per-manager by setting `watch-only = "true"`: + +```toml +[jenkins] + repos = ["jcasc-local"] + + # Enable watch-only mode - detect changes and reload, don't write files + watch-only = "true" + + # When watch-only is true, dest-path is optional + # dest-path = "/var/tmp/butler-jcasc" # Not needed in watch-only mode + + clean-files = "false" + skip-butler-header = "true" + enable-cache = "false" + primary-config-name = "jcasc-config.yaml" + + [jenkins.jcasc-local] + method = "file" + repo-path = "/usr/share/jenkins/init.jcasc.d" + primary-config = ["05-config-files.yaml", "05-credentials.yaml", "10-defaults.yaml"] + + [jenkins.reloader] + method = "https" + [jenkins.reloader.https] + host = "localhost" + port = "4430" + uri = "/reload-configuration-as-code/?casc-reload-token=mytoken" + method = "post" + insecure-skip-verify = "true" + retries = "3" + timeout = "30" +``` + +#### How It Works + +1. Butler reads and **hashes** the source files from `repo-path` +2. Butler compares hashes to the previous run (stored in memory) +3. If hashes differ → trigger the configured reloader +4. **Never writes** to `dest-path` + +#### Key Behaviors + +- **First run**: Always triggers a reload (no previous hashes to compare) +- **Container restart**: Triggers a reload (in-memory hashes are lost) +- **No file writes**: Eliminates disk I/O for read-only filesystems +- **`dest-path` optional**: Not required when `watch-only = "true"` +- **`clean-files` ignored**: File cleanup is skipped in watch-only mode + +#### Use Cases + +- **Jenkins JCasC ConfigMap monitoring**: Detect ConfigMap updates and trigger Jenkins reload +- **Prometheus/Alertmanager configs**: Monitor externally-managed configs +- **GitOps scenarios**: Configs managed by Helm/ArgoCD, butler just triggers reloads + ## Building Butler uses Docker Buildx Bake for building container images. All build configuration is defined in `docker-bake.hcl`. diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md new file mode 100644 index 0000000..64405a7 --- /dev/null +++ b/RELEASE_NOTES.md @@ -0,0 +1,145 @@ +# Release v1.5.0 + +## What's Changed + +This release adds a new **watch-only mode** for ConfigMap monitoring, comprehensive test coverage improvements, and infrastructure updates. + +### New Features + +#### Watch-Only Mode for ConfigMap Monitoring + +Added a new `watch-only` configuration option that enables butler to monitor files for changes using hash comparison and trigger reloads **without writing any files to disk**. This is ideal for Kubernetes ConfigMap monitoring scenarios where files are mounted read-only. + +```toml +[jenkins] + repos = ["jcasc-local"] + watch-only = "true" + skip-butler-header = "true" + primary-config-name = "jcasc-config.yaml" + + [jenkins.jcasc-local] + method = "file" + repo-path = "/usr/share/jenkins/init.jcasc.d" + primary-config = ["config.yaml"] + + [jenkins.reloader] + method = "https" + [jenkins.reloader.https] + host = "localhost" + port = "8080" + uri = "/reload-configuration-as-code/?casc-reload-token=mytoken" + method = "post" +``` + +When enabled: +- Butler **hashes** source files from `repo-path` instead of copying them +- Compares hashes to previous run (stored in memory) +- Triggers reloader if hashes differ +- **Never writes** to `dest-path` (which becomes optional) +- First run always triggers reload (no previous hashes) +- Container restart triggers reload (in-memory hashes lost) + +### Test Infrastructure Improvements + +- **Regenerated TLS certificates** with proper Subject Alternative Names (SANs) for `localhost` and `127.0.0.1`, fixing acceptance test failures with Go 1.15+ +- **Added comprehensive unit tests** for previously untested code: + - `internal/config/status_test.go` - Status file operations + - `internal/config/objects_test.go` - ValidateOpts and RepoFileEvent + - `internal/config/chan_test.go` - ConfigChanEvent operations + - `internal/config/manager_test.go` - Manager methods + - `internal/reloaders/reloaders_test.go` - Reloader error handling + - `internal/reloaders/http_test.go` - HTTP reloader functionality + - `internal/alog/alog_test.go` - Apache logging handler + - `internal/methods/methods_test.go` - Method factory +- **Added tests for watch-only mode** in `helpers_test.go` and `config_test.go` + +### CI/CD Improvements + +- **Parallel test execution** in GitHub Actions workflows - unit tests and acceptance tests now run concurrently +- **Updated Dockerfile** to include tests for `internal/environment` and `internal/alog` packages +- **Added certificate generation script** (`files/certs/generate_certs.sh`) for reproducible test certificate creation + +### Build System Updates + +- Updated `make test` to run only unit tests by default (faster CI) +- Added `make test-all` to run both unit and acceptance tests +- Removed legacy Dockerfile references to non-existent files + +## Breaking Changes + +None - this release is fully backward compatible. + +## Full Changelog + +https://github.com/adobe/butler/compare/v1.4.0...v1.5.0 + +--- + +# Release v1.4.0 + +## What's Changed + +This release includes significant improvements to the build system, new configuration options, and various enhancements. + +### New Features + +#### `skip-butler-header` Configuration Option +Added a new per-manager configuration option that allows skipping the `#butlerstart` and `#butlerend` header/footer validation. This is useful for managing files like Kubernetes ConfigMaps or JCasC configurations that cannot easily include butler markers. + +```toml +[mymanager] + repos = ["myrepo"] + skip-butler-header = "true" + dest-path = "/path/to/configs" +``` + +When enabled: +- Butler will **not** require `#butlerstart` and `#butlerend` markers +- YAML syntax validation still occurs for `.yaml`/`.yml` files +- JSON syntax validation still occurs for `.json` files + +#### GitHub Actions Release Workflows +Added automated release workflows using GitHub Actions with label-based versioning: +- `release:major` - Breaking changes (v1.0.0 → v2.0.0) +- `release:minor` - New features (v1.0.0 → v1.1.0) +- `release:patch` - Bug fixes (v1.0.0 → v1.0.1) +- `release:skip` - Skip automatic release + +### Build System Modernization +- Migrated from `dep` to Go modules (`go.mod`) +- Consolidated multiple Dockerfiles into a single multi-stage `docker/Dockerfile` +- Added Docker Buildx Bake configuration (`docker-bake.hcl`) +- Updated to Go 1.21 +- Removed vendor directory (dependencies fetched at build time) + +### Other Improvements +- Added blob account CLI flags for Azure Blob storage +- S3 improvements +- Added `InsecureSkipVerify` option to ignore etcd SSL warnings +- Updated metrics handling +- Added default HTTP options +- Code linting and cleanup +- Added Travis CI configuration + +## Commits + +- `2cd59dc` Add skip header option (#45) (Stegen Smith) +- `7a10661` Updating base container / improving container build / adding workflows (#44) (Stegen Smith) +- `135d32e` moved govender to dep (vs glide) (#38) (Stegen Smith) +- `e04aa73` added blob account cli flags (#37) (Stegen Smith) +- `26ef1a6` S3 improvements (#36) (Stegen Smith) +- `fe50d2a` Tidying up how we're handling the different methods (#35) (Stegen Smith) +- `fa4c10c` updating metrics (#34) (Stegen Smith) +- `95711e8` Add InsecureSkipVerify to ignore etcd ssl warnings (#33) (Friedrich Gonzalez) +- `43812aa` Doing some linting and other cleanup (#30) (Stegen Smith) +- `85d928f` Adding travis ci stuff (#29) (Stegen Smith) +- `7d241f9` Adding default http options (#26) (Stegen Smith) + +## Breaking Changes + +- The old `make build` command now uses Docker Buildx Bake instead of direct Docker build +- Vendor directory has been removed; builds now require network access to fetch dependencies + +## Full Changelog + +https://github.com/adobe/butler/compare/v1.3.0...v1.4.0 diff --git a/contrib/butler.toml.sample b/contrib/butler.toml.sample index a4cb262..ce0ef43 100644 --- a/contrib/butler.toml.sample +++ b/contrib/butler.toml.sample @@ -62,6 +62,14 @@ title = "Butler Configuration" ## Default: "false" skip-butler-header = "false" + ## Watch-only mode. When set to "true", butler will only monitor files for changes + ## using hash comparison and trigger reloads, WITHOUT writing any files to dest-path. + ## This is useful for Kubernetes ConfigMap monitoring where the files are already + ## mounted at repo-path and you just need butler to detect changes and trigger reloads. + ## When watch-only is enabled, dest-path becomes optional. + ## Default: "false" + watch-only = "false" + ## These are the mustache substitutions that we'll attempt to make on the merged configuration files mustache-subs = ["cluster-cluster-id=cluster01-dev-or1", "endpoint=external", "envvar=env:ENVIRONMENT_VAR"] diff --git a/docker/Dockerfile b/docker/Dockerfile index 5586e4a..a859648 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -68,6 +68,10 @@ RUN --mount=type=cache,target=/root/.cache/go-build \ GOFLAGS="-mod=mod" go test -v -coverprofile=/tmp/coverage/monitor.out ./internal/monitor && \ echo "Testing ./internal/metrics..." && \ GOFLAGS="-mod=mod" go test -v -coverprofile=/tmp/coverage/metrics.out ./internal/metrics && \ + echo "Testing ./internal/environment..." && \ + GOFLAGS="-mod=mod" go test -v -coverprofile=/tmp/coverage/environment.out ./internal/environment && \ + echo "Testing ./internal/alog..." && \ + GOFLAGS="-mod=mod" go test -v -coverprofile=/tmp/coverage/alog.out ./internal/alog && \ echo "All tests passed!" # Combine coverage reports @@ -99,8 +103,6 @@ RUN mkdir -p /run/nginx /opt/butler /opt/cache # Copy test files and scripts COPY files/tests/ /www/ COPY files/certs/ /certs/ -COPY files/build_test_accept.sh /root/build.sh -COPY files/doit.sh /doit.sh # Setup certificates RUN cp /certs/rootCA.* /usr/local/share/ca-certificates/ 2>/dev/null || true && \ diff --git a/files/Dockerfile-build b/files/Dockerfile-build deleted file mode 100644 index b967f2d..0000000 --- a/files/Dockerfile-build +++ /dev/null @@ -1,33 +0,0 @@ -# Copyright 2017-2026 Adobe. All rights reserved. -# This file is licensed 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 REPRESENTATIONS -# OF ANY KIND, either express or implied. See the License for the specific language -# governing permissions and limitations under the License. - -FROM golang:1.11.0-alpine3.8 -LABEL maintaner="Stegen Smith " - -ARG VERSION=$VERSION -ENV VERSION=$VERSION - -### required to build -RUN mkdir -p /root/butler/cmd/butler /root/butler/internal/monitor /root/butler/internal/metrics /root/butler/internal/config /root/butler/internal/methods /root/butler/internal/reloaders /root/butler/internal/environment /root/butler/internal/alog -COPY ./files/build.sh /root/build.sh -COPY ./cmd/butler/main.go /root/butler/cmd/butler/main.go -COPY ./internal/config/*.go /root/butler/internal/config/ -COPY ./internal/methods/*.go /root/butler/internal/methods/ -COPY ./internal/reloaders/*.go /root/butler/internal/reloaders/ -COPY ./internal/metrics/*.go /root/butler/internal/metrics/ -COPY ./internal/monitor/*.go /root/butler/internal/monitor/ -COPY ./internal/environment/*.go /root/butler/internal/environment/ -COPY ./internal/alog/*.go /root/butler/internal/alog/ -COPY ./vendor /root/butler/vendor -### required to build - -RUN apk update && apk add bash && apk add ca-certificates - -RUN /root/build.sh diff --git a/files/Dockerfile-testaccept b/files/Dockerfile-testaccept deleted file mode 100644 index 10fb134..0000000 --- a/files/Dockerfile-testaccept +++ /dev/null @@ -1,38 +0,0 @@ -# Copyright 2017-2026 Adobe. All rights reserved. -# This file is licensed 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 REPRESENTATIONS -# OF ANY KIND, either express or implied. See the License for the specific language -# governing permissions and limitations under the License. - -FROM golang:1.11.0-alpine3.8 -LABEL maintaner="Stegen Smith " - -ARG VERSION=$VERSION -ENV VERSION=$VERSION - -### required for test -COPY ./files/doit.sh /doit.sh -COPY ./files/build_test_accept.sh /root/build.sh -COPY ./files/nginx.conf-accept /etc/nginx/nginx.conf -### required for test - -### required to build -RUN mkdir -p /root/butler/cmd/butler /root/butler/internal/monitor /root/butler/internal/metrics /root/butler/internal/config /root/butler/internal/methods /root/butler/internal/reloaders /root/butler/internal/environment /root/butler/internal/alog -COPY ./cmd/butler/*.go /root/butler/cmd/butler/ -COPY ./internal/config/*.go /root/butler/internal/config/ -COPY ./internal/methods/*.go /root/butler/internal/methods/ -COPY ./internal/reloaders/*.go /root/butler/internal/reloaders/ -COPY ./internal/metrics/*.go /root/butler/internal/metrics/ -COPY ./internal/monitor/*.go /root/butler/internal/monitor/ -COPY ./internal/environment/*.go /root/butler/internal/environment/ -COPY ./internal/alog/*.go /root/butler/internal/alog/ -COPY ./vendor /root/butler/vendor -### required to build - -RUN apk update && apk add bash build-base ca-certificates curl git nginx - -CMD ["/doit.sh"] diff --git a/files/Dockerfile-testunit b/files/Dockerfile-testunit deleted file mode 100644 index 78375e6..0000000 --- a/files/Dockerfile-testunit +++ /dev/null @@ -1,38 +0,0 @@ -# Copyright 2017-2026 Adobe. All rights reserved. -# This file is licensed 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 REPRESENTATIONS -# OF ANY KIND, either express or implied. See the License for the specific language -# governing permissions and limitations under the License. - -FROM golang:1.11.0-alpine3.8 -LABEL maintaner="Stegen Smith " - -ARG CODECOV_TOKEN=$CODECOV_TOKEN -ENV CODECOV_TOKEN=$CODECOV_TOKEN - -### required for test -COPY ./files/doit.sh /doit.sh -COPY ./files/build_test_unit.sh /root/build.sh -### required for test - -### required to build -RUN mkdir -p /root/butler/cmd/butler /root/butler/internal/monitor /root/butler/internal/metrics /root/butler/internal/config /root/butler/internal/methods /root/butler/internal/reloaders /root/butler/internal/environment /root/butler/internal/alog -COPY ./cmd/butler/*.go /root/butler/cmd/butler/ -COPY ./internal/config/*.go /root/butler/internal/config/ -COPY ./internal/methods/*.go /root/butler/internal/methods/ -COPY ./internal/reloaders/*.go /root/butler/internal/reloaders/ -COPY ./internal/metrics/*.go /root/butler/internal/metrics/ -COPY ./internal/monitor/*.go /root/butler/internal/monitor/ -COPY ./internal/environment/*.go /root/butler/internal/environment/ -COPY ./internal/alog/*.go /root/butler/internal/alog/ -COPY ./vendor /root/butler/vendor -COPY ./.git/ /root/butler/.git -### required to build - -RUN apk update && apk add bash build-base ca-certificates git - -CMD ["/doit.sh"] diff --git a/files/certs/generate_certs.sh b/files/certs/generate_certs.sh new file mode 100755 index 0000000..ac67c15 --- /dev/null +++ b/files/certs/generate_certs.sh @@ -0,0 +1,115 @@ +#!/bin/bash +# Script to generate test CA and certificates with proper SANs for butler acceptance tests + +set -e + +CERT_DIR="$(cd "$(dirname "$0")" && pwd)" +cd "$CERT_DIR" + +echo "Generating certificates in: $CERT_DIR" + +# Clean up old files +rm -f rootCA.crt rootCA.key rootCA.srl test.crt test.key test.csr rootCA.ky test.ky + +# Generate Root CA private key (4096 bit RSA) +echo "Generating Root CA private key..." +openssl genrsa -out rootCA.key 4096 + +# Generate Root CA certificate (self-signed, valid for ~56 years like the original) +echo "Generating Root CA certificate..." +openssl req -x509 -new -nodes \ + -key rootCA.key \ + -sha256 \ + -days 20454 \ + -out rootCA.crt \ + -subj "/C=UK/ST=Berkshire/L=Maidenhead/O=Adobe Systems, Ltd/OU=TechOps" + +# Create OpenSSL config for server certificate with SANs +cat > server.cnf << 'EOF' +[req] +default_bits = 4096 +prompt = no +default_md = sha256 +distinguished_name = dn +req_extensions = req_ext + +[dn] +C = UK +ST = Berkshire +L = Maidenhead +O = Adobe Systems, Ltd +OU = TechOps +CN = localhost + +[req_ext] +subjectAltName = @alt_names + +[alt_names] +DNS.1 = localhost +DNS.2 = *.localhost +IP.1 = 127.0.0.1 +IP.2 = ::1 +EOF + +# Generate server private key +echo "Generating server private key..." +openssl genrsa -out test.key 4096 + +# Generate server CSR with SANs +echo "Generating server CSR..." +openssl req -new \ + -key test.key \ + -out test.csr \ + -config server.cnf + +# Create extension file for signing (needed to include SANs in the signed cert) +cat > server_ext.cnf << 'EOF' +authorityKeyIdentifier=keyid,issuer +basicConstraints=CA:FALSE +keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment +subjectAltName = @alt_names + +[alt_names] +DNS.1 = localhost +DNS.2 = *.localhost +IP.1 = 127.0.0.1 +IP.2 = ::1 +EOF + +# Sign the server certificate with the CA (valid for ~56 years like the original) +echo "Signing server certificate with CA..." +openssl x509 -req \ + -in test.csr \ + -CA rootCA.crt \ + -CAkey rootCA.key \ + -CAcreateserial \ + -out test.crt \ + -days 20454 \ + -sha256 \ + -extfile server_ext.cnf + +# Create .ky copies for compatibility with existing code that expects .ky extension +cp rootCA.key rootCA.ky +cp test.key test.ky + +# Clean up temporary config files +rm -f server.cnf server_ext.cnf + +# Verify the certificates +echo "" +echo "=== Root CA Certificate ===" +openssl x509 -in rootCA.crt -text -noout | grep -A2 "Subject:" +echo "" +echo "=== Server Certificate ===" +openssl x509 -in test.crt -text -noout | grep -A2 "Subject:" +echo "" +echo "=== Server Certificate SANs ===" +openssl x509 -in test.crt -text -noout | grep -A1 "Subject Alternative Name" +echo "" +echo "=== Verification ===" +openssl verify -CAfile rootCA.crt test.crt + +echo "" +echo "Certificate generation complete!" +echo "Files created:" +ls -la *.crt *.key *.ky *.csr *.srl 2>/dev/null || true diff --git a/files/certs/rootCA.crt b/files/certs/rootCA.crt index 2f38a57..9fe4ef1 100644 --- a/files/certs/rootCA.crt +++ b/files/certs/rootCA.crt @@ -1,33 +1,33 @@ -----BEGIN CERTIFICATE----- -MIIFnzCCA4egAwIBAgIJANZipw5Zjl32MA0GCSqGSIb3DQEBCwUAMGUxCzAJBgNV -BAYTAlVLMRIwEAYDVQQIDAlCZXJrc2hpcmUxEzARBgNVBAcMCk1haWRlbmhlYWQx -GzAZBgNVBAoMEkFkb2JlIFN5c3RlbXMsIEx0ZDEQMA4GA1UECwwHVGVjaE9wczAg -Fw0xODAzMjgxMzI4MzFaGA8yMDc0MDQyMzEzMjgzMVowZTELMAkGA1UEBhMCVUsx -EjAQBgNVBAgMCUJlcmtzaGlyZTETMBEGA1UEBwwKTWFpZGVuaGVhZDEbMBkGA1UE -CgwSQWRvYmUgU3lzdGVtcywgTHRkMRAwDgYDVQQLDAdUZWNoT3BzMIICIjANBgkq -hkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAy0xExx6nlgft8H+vkpCiGz2bCPJ72b0d -hqUkq/P+Qa6g3/wGX+yT0lx5xBtmLaV9BE9oiX30PjdJOjkZebeF6SoyBcmEnaGZ -Hw0vcKXzM0Z09v0iqEbi5lgWkjun8Zk1RWB3INQqEFauMTEef/UEj7wYRTsofE+O -bNpUCwh28r9nwemlDOqaVhjT5NGbv2y58Ec+6MaG05FCRCbcl67822ouMdY0zw1b -QwTmV3Q6UcOHkHi6KX7QbJxMAKVqB5bO7UcKJYXfD1773J2hpn4FYBbAQ5ijIZMA -hjo+6bjyYLzjWVC/OJQNFJW41wCp9oaIxrwVfBiHEKwV4VEdzL00N0OYLEIypAY/ -pdrzu4QV5ZQg8A0Un1EDA5oN5LHFAz47pRwMqNAgaWkQeMYHJ3i63gh2D2e7zoG0 -wiS/L/EqbgXV/14YJXchgtZ4t1DJhROR16bY+iBpxBt6NMiPd8njI4WqzWZpBu73 -M0+gai4E8NYBduQw7yxxP/DFoVLKHEEWlLwALEYMx7qKcYDJw/M3Z3Dcj6nFYM6U -B0gUpoGqUuNtXr1TFJUbwDneYW4za4+7OAi8zUp17Um0/lvo5YI+fTj7gfGSdEcN -lZhLXw3BEXLdAKn7mVwR5OQ266cHDvjNHsAhqt63+JzXRMqbQm0zWqBd6IGgKWtw -6NWcX+sLlP8CAwEAAaNQME4wHQYDVR0OBBYEFKzrN31tn9i74KxeX5WlYbae5tH1 -MB8GA1UdIwQYMBaAFKzrN31tn9i74KxeX5WlYbae5tH1MAwGA1UdEwQFMAMBAf8w -DQYJKoZIhvcNAQELBQADggIBAErL1VbzqswWlASw18N/DkSo+Ufs8T/+KRC/OwT4 -Ixw9od8j/OtkM3/EJoKKRW0FkHaLALdxofS57L1fNw69G+B8EG+bzIPMJpNYgtfg -fAwSvPUmRevoVYOa6GkfaslDJu8EDS/4SdmU4eFTsh7D8taFxUxBmx/M0iYQ7iH/ -gdnw+/ZrQb6XVRpigkFuP1UOkfXKkjN5JX2BC3MLvLRosUne6yNdFwVxm3BKGnOq -DCkkPmwhSNc8tGKo35usLjed3ak/B2Pmr4LgcAENsuhzDvtgj1xtQDjZwKkaw2+y -kRtbWyrrdaHBzfRpIF0ujcM6/5HawuEM/lsDKzJvUpqWnqcQYZ/TLN0ukbFmTuXq -N5T49yYJaIu0cpck7wPPoO9O6k5cbF8H6hHnKlKo7AnbPbltiaDXSin5iXjQOkVc -hkTV0aJe2x43PYd9k/m8hRty7PGDJiIb2enqf8y2iQKDEKAGm+lAzGhMgRZ7Xcaa -7TSpk7bQ62HpKif2sfYq2cNc2EfK0GlyysClh5kGoeq/UOZ8xL4NFa6mau+pGpLY -Igv3ixaVTPpVHPjObHvuEG8EM+/WCs/E6yzLm3bOQ56X42r+gCDTKX9u0xTUhtAK -tWFHip/A1+GOCqpxFSX7RYjZX3Ckuo7slwkzfNn9WwuemN36kyqyX2aDEHSXBi9b -4dyr +MIIFrTCCA5WgAwIBAgIUdlxl7WkjYxFY/zyA3l148xBNi14wDQYJKoZIhvcNAQEL +BQAwZTELMAkGA1UEBhMCVUsxEjAQBgNVBAgMCUJlcmtzaGlyZTETMBEGA1UEBwwK +TWFpZGVuaGVhZDEbMBkGA1UECgwSQWRvYmUgU3lzdGVtcywgTHRkMRAwDgYDVQQL +DAdUZWNoT3BzMCAXDTI2MDEzMDIwMDQxNloYDzIwODIwMTMwMjAwNDE2WjBlMQsw +CQYDVQQGEwJVSzESMBAGA1UECAwJQmVya3NoaXJlMRMwEQYDVQQHDApNYWlkZW5o +ZWFkMRswGQYDVQQKDBJBZG9iZSBTeXN0ZW1zLCBMdGQxEDAOBgNVBAsMB1RlY2hP +cHMwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCHDQ4l3ULeJotcYksW +RHRTj2PUjlwvsv9Hj/qcZvmC1mkQA/LiMXzUuQj23TaiaLFvK7pVV0Ovgajynlbm +Kp2uQTgAf6jZUxErHuTv9GCG5DlOmMsDbkN3qampTKnHXpZ5x6d+cpohmresX16A +PfJ9BCRK3keV2tyFAhB8Pvt8qSFaK4RWI3tlADzdrFkkgHMx3ePL5M7G2Vga1PlC +VHQ3rJPOEZWZtiwvA2a2PqI7iI+muh/XfhKNvmVVWFDUf3XIEwjkcOWlGX9Mayo1 +HU3r+btMvpZvDsnoV3b2BGJTvenrnqI442BM/VOTO5hy3ndLyrNQOc/iDdWQ16ny +IILcd++iHkal8WDffYhifi1eKJcPbzli3TlDc6cmGFs/+lbsV+yfETvyIgVFqJeV +s46heWsTqLC6vR8kB/aF7QKkgDF+ma8rNYrf/7YcBkMFmGQqSLJBRy3XgQuNaQ5b +0sM1IipN9Y3wh6KeTrBB6t5WcBdoft4AC5ihnaEJl9R+gXrE5y+9jhMbRFBctc8G +Mnx0SbpphKzDsgT4EdQXxIBEDrhuik6xWMZz9w17cZBzGqD4P2FBFayh9GuQH1T1 +b6L5Uj+4/Re5dr0mGXcQGlwfqUxSrjo+DZNnZnyZCZtrejLZXvD0neNVYH/zGi4Q +FadthUlpbXvE3Lws2wIW0ZUNQQIDAQABo1MwUTAdBgNVHQ4EFgQU2P7w/5kIxqMY +/eZspr0AzUwip8AwHwYDVR0jBBgwFoAU2P7w/5kIxqMY/eZspr0AzUwip8AwDwYD +VR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAgEAeVAUOEeR/TNdkCEsKNOa +B6I0cuPbl6mQMUp9SR12x7drKYIMhJhDwKu/qy9dsHTOUv2Pw5MJrxRNzT4qJ6Ew +j1Uwk2HhWa3F7cxGxJ2qSeNsJlJdno1Cb7OEcJprBItK/yJhkq/SwdgeuH8Ek9+c +JkdTmRfsa6D9IUdKrODQPeuSrtz8BkgHmybY8F9eguEQ/VhRhMoBCfoShw4ir2Oa +0BaTXKNtQZKvzNPhlrztQmA8rFDQJS7tdXYXyCMzjlyks/FvGtWGTDNdO6sJlcRB +iFsZobqx60JOU6Vvx7un1SI0IU4Gswv+JG2VxgEcEGDgllyelLryLdtVD2FVLjTF +fZL7ibU9MqcwQ4in1ffB+18H2UP36LIoQ3Z4gW1+WY1XNvdwLi61BB2gBWXpAbD4 +21+q1lmml84BmnhSGeIJQB8ZwDejUcKMG0neFSZ/lsmowg/WAUA8UkYiHuHvELig +Rc+gpgN6F3Ntr66dZ5qLiaGIzf/+XOxwSs5N5j9qy8r+HgcykeKGnI9LM4KSTZmE +U/FrNSrJTdJeutjVuos/5SK2e64GB9oT3bdIlNQuEJqtCX+SoslLuHABiykZ+LKx +rumV6/YxL3mdAFlr8WIX/UjGR5+mEAwGWyTZqAd5sOA8F65ZjjfnuQjHZVgmXUNB +ayQiNamyyUMvIJEd9hBL/NM= -----END CERTIFICATE----- diff --git a/files/certs/rootCA.ky b/files/certs/rootCA.ky index c2d7790..d38a424 100644 --- a/files/certs/rootCA.ky +++ b/files/certs/rootCA.ky @@ -1,54 +1,52 @@ ------BEGIN RSA PRIVATE KEY----- -Proc-Type: 4,ENCRYPTED -DEK-Info: DES-EDE3-CBC,00DCDE1519796A19 - -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 ------END RSA PRIVATE KEY----- +-----BEGIN PRIVATE KEY----- +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 +Edot7Hgdrkl9Bme9pz1blYGgqrT5 +-----END PRIVATE KEY----- diff --git a/files/certs/rootCA.srl b/files/certs/rootCA.srl index 7278735..47febaf 100644 --- a/files/certs/rootCA.srl +++ b/files/certs/rootCA.srl @@ -1 +1 @@ -9AA3D2D4A4C1EAD8 +4A3CB2F744C064A6A979693CF77B53F8A8F3CE0E diff --git a/files/certs/test.crt b/files/certs/test.crt index e78e198..a4e1008 100644 --- a/files/certs/test.crt +++ b/files/certs/test.crt @@ -1,31 +1,35 @@ -----BEGIN CERTIFICATE----- -MIIFXDCCA0QCCQCao9LUpMHq2DANBgkqhkiG9w0BAQsFADBlMQswCQYDVQQGEwJV -SzESMBAGA1UECAwJQmVya3NoaXJlMRMwEQYDVQQHDApNYWlkZW5oZWFkMRswGQYD -VQQKDBJBZG9iZSBTeXN0ZW1zLCBMdGQxEDAOBgNVBAsMB1RlY2hPcHMwIBcNMTgw -MzI4MTMzMDA1WhgPMjA3NDA0MjMxMzMwMDVaMHkxCzAJBgNVBAYTAlVLMRIwEAYD -VQQIDAlCZXJrc2hpcmUxEzARBgNVBAcMCk1haWRlbmhlYWQxGzAZBgNVBAoMEkFk -b2JlIFN5c3RlbXMsIEx0ZDEQMA4GA1UECwwHVGVjaE9wczESMBAGA1UEAwwJbG9j -YWxob3N0MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAujac1zu9mJft -b6rKBB3GXmyZuJ5jBlaIBiyjPnpiR5hHYjse1K0RPhgwEhIHZwfmiIV0uDfgZ+xc -LLydWycksX3xUOJftC5KN1LsFUgxFjQvArWt9UnFs1ccCxMlcpXELtrN/wKk+WU+ -TzyfqZEe1PEsc25cCqs2LMgb2Zi/V7isb8AEfq2UydiKvMyEGlpg6TAGNvL2lPYD -cNjNsrQiJkwkUwXpqzzyppIJ1I1trcVIxxWr7CcBJ5EuLEXjfJfhsiSAqSZsmkWk -SjGl1S2kA7QdCvgd4EQ3YS9axT5BKl5/wXuJbeR7xLs5ik0pQ+i6iyD78pRDSJ8i -oxaS/VMg1FX5xGOFCzfeyA1DzK7Pg77QzqsbO8+NQlNKBwr4dCo61ey7XDTO0tuV -jAEeZ/dgEHj3ak62pyHKnhqE5w4C26/qgtbLYtT9clgp0t4Qst8Ctwm0lKveFKJg -PL4G/lt8G2TMswMfUkVQuLyBdmS3o8uxpReCPBD2J8B9eNTeUnOyfpldzvGxA8vU -PckWvHx0xQSFPKyPCanIawFTeDXG7nQ2AkAsWCRxrGR9++vbDuo7eL2VzyyVu4NY -DAyX0S6Ul5Q70OpM3Q1SXGjo0pI0voL9F0KTD3oB58N6nvpHe/8G9I5/Aui7ZGx+ -tUJkwP+7p3VTCr1ECwddxw9Dk+ovRcUCAwEAATANBgkqhkiG9w0BAQsFAAOCAgEA -LOOnpMQ7504BTi1YoRL0GbOR1EGXLE1bDo4lVqZfAfnGqkbLlDzuc+RhgiN8BMvs -cZUYQuEkOe0NxX9NuuszZoFdPr4OVN12/Zw/rOC6mhQgm6sVyzcGKewCu9GC9F1q -UwGN67VY3Vs3kQmECqg4Jn7adTduFvlCBB/jtk41sgazmB/gv2Jj+qIlPy0WJ17M -KTTB7aZpuENkTguEAXH5gLJ6p/4tGi0hyjb8rz3yRS9q2qsGz1a3RVNpFEZHKx5c -IIsBN3nH7b3jIBZaQsnXc4gHbvavRStRZWy1UU/SFNqEcMZSq9QPnMhXvedNoEqh -VoJpy94LIjfETMrnk0eZ48AaHZ3+c3Jx8bEnQyabvK4Sftvkgw//kcKP7P7ELk3Y -snyKCvrEP+fqC3jxflkSwlV/Kl2269JWRgvE+wx7fEoZKUO3f6rjS61u4pV6nRaV -H8fhCEMQbqjeGoO/ivOGJjZ5R0rtVDvtID3qjDHgF9QrV/znGi/kGzHbwvy+pWvw -swIgLvw6SK8nyKVLTaUiFUZCX64k0Md3Tn2gGbzRH8kiF/lTkazDIO/7zFdCAfnC -YYgS97RP/6cgWlRMJ3mTdiqiP2MuQlMqqtdw/Wkx7Wk0DxCjv9EUltIk0NVj+9HP -eQXAEUxkzscBWKOJkKYsj3S9amZfmRUFesq/cwuMhDo= +MIIGBTCCA+2gAwIBAgIUSjyy90TAZKapeWk893tT+Kjzzg4wDQYJKoZIhvcNAQEL +BQAwZTELMAkGA1UEBhMCVUsxEjAQBgNVBAgMCUJlcmtzaGlyZTETMBEGA1UEBwwK +TWFpZGVuaGVhZDEbMBkGA1UECgwSQWRvYmUgU3lzdGVtcywgTHRkMRAwDgYDVQQL +DAdUZWNoT3BzMCAXDTI2MDEzMDIwMDQxNloYDzIwODIwMTMwMjAwNDE2WjB5MQsw +CQYDVQQGEwJVSzESMBAGA1UECAwJQmVya3NoaXJlMRMwEQYDVQQHDApNYWlkZW5o +ZWFkMRswGQYDVQQKDBJBZG9iZSBTeXN0ZW1zLCBMdGQxEDAOBgNVBAsMB1RlY2hP +cHMxEjAQBgNVBAMMCWxvY2FsaG9zdDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCC +AgoCggIBAL7n8xpW9Wgo79aAgGKP33wUTlmWDY+tn3qt0OBHHDjbhuRlHeO5R67I +2ecP/B8oZeNMkc1X0fksIgo+rkYgrPSX1PYpkhHQLd6PSxroSvd6vSe1ypKOD2R9 +aFHLmjuOL/bTGq/TtJOpqOby34W+/3f1EsX4Q4oZTnf0el4y1xW/5JiYV8zFCg/B +f30o51NfYZYpyJUM2a5gNB+NEI2VHp6npb/77sC4VxJ28oJujTemWpBBSBy01tMS +SL5SDLKd9gSs87IdvFrVDot6ISJ9q7z8ZDRDzBqnQtW4Zn0DCxVfSF5A4JvsaA8A +zda2yliQQvrRz5p7tu34cO5BhBdnsVFHI2NWrKca/doZPnjWNKdmwwBQqX00sZih +kYgxzMBWHIjwcUQLY6LospoX8+7uFeJhWwrYieeoKR+P2ZKcbmi4aFA5k8zkaRvB +qoR9AOUkKxMZyFJuXtX6jxnqZ87JZTQ3FF4GKXovHEPUq/p7gxHYdDOp6mGjN0Jd +dsk2U9JjIaRKk8Ri+4jI3pGqXCad931hT4S7O4pzDifh/Wk+cNVvpI4vLNmyLKY8 +22NC6+S4+lg5Xp3e00D41dk0HfMFIO78WmuZUwAYLk+nbXVgCAYS+83UQ8U1HWVm +yiuSpTknthhK7+RUupoDBoPaG2swJNEZsmVm/OJYX8+j9Wu8ovJXAgMBAAGjgZYw +gZMwHwYDVR0jBBgwFoAU2P7w/5kIxqMY/eZspr0AzUwip8AwCQYDVR0TBAIwADAL +BgNVHQ8EBAMCBPAwOQYDVR0RBDIwMIIJbG9jYWxob3N0ggsqLmxvY2FsaG9zdIcE +fwAAAYcQAAAAAAAAAAAAAAAAAAAAATAdBgNVHQ4EFgQU69UV5A39yHCTTcZmS749 +02ynMcMwDQYJKoZIhvcNAQELBQADggIBABwGydWMfvZSI0DMy6UaF97hIafyBttk +8IhryI7NY9+sOTw6+xF+H/+Gp3j7FFg2039ip5vA/Ra3ZCd7LKPy1o6wP7lAHF8d +AHH9DY59bmHyKmAtbzLvyN+QrSaAyeO9L06HHO1IKT+cAWZy4VlSGlDodZ4GY7/w +clwOIuAP3cEOETYXHwhEr+AQwWkWMPs1Z97L4BudmONfm45bWu6QzOmnYjfo96dF +RWBuLeCHpr8r/gguEoDpEZRnuJ+v89dZ/6skRuwNcvtZ3TjVIAlpE23Gdj5XldRa +GEb/y0eOza5IkIKLHJdyBcFWUJDSdl7e54FpCKzuh6PuEXhzw2RV7V1BIc2dqlnh +0L7Sokk477ARVzVwEWrcyWXZiinHqbJ8kCxb3me4irCTSShdOPG4esUIayCOqNH2 +eeOLci8z5hLl7xkF95fKs1nhWwx2zbNF69bJgAFBFwSaae6gFjxPmn5xyuxqBBY2 +yOYQOpoeQwwA4x6BUbUfoV+srWYRcf9p+RdcHjKxpqIO12dT5WtbKCb6hgAoL7+0 +kkKJSwt0KVPAjs5EglJmMdMnIYF5mH8HxSltAt9QnANe0Q44jJIg0nRkYC3AF6mH +AjQpBI1lVYysqqRYP/Z3eUCv4QumI096AVjgl2urYOZyxfsecXkMf2lK8Dc1xvM3 +cXDnvYFFwOXr -----END CERTIFICATE----- diff --git a/files/certs/test.csr b/files/certs/test.csr index 268be63..01206dd 100644 --- a/files/certs/test.csr +++ b/files/certs/test.csr @@ -1,28 +1,29 @@ -----BEGIN CERTIFICATE REQUEST----- -MIIEvjCCAqYCAQAweTELMAkGA1UEBhMCVUsxEjAQBgNVBAgMCUJlcmtzaGlyZTET +MIIFCjCCAvICAQAweTELMAkGA1UEBhMCVUsxEjAQBgNVBAgMCUJlcmtzaGlyZTET MBEGA1UEBwwKTWFpZGVuaGVhZDEbMBkGA1UECgwSQWRvYmUgU3lzdGVtcywgTHRk MRAwDgYDVQQLDAdUZWNoT3BzMRIwEAYDVQQDDAlsb2NhbGhvc3QwggIiMA0GCSqG -SIb3DQEBAQUAA4ICDwAwggIKAoICAQC6NpzXO72Yl+1vqsoEHcZebJm4nmMGVogG -LKM+emJHmEdiOx7UrRE+GDASEgdnB+aIhXS4N+Bn7FwsvJ1bJySxffFQ4l+0Lko3 -UuwVSDEWNC8Cta31ScWzVxwLEyVylcQu2s3/AqT5ZT5PPJ+pkR7U8SxzblwKqzYs -yBvZmL9XuKxvwAR+rZTJ2Iq8zIQaWmDpMAY28vaU9gNw2M2ytCImTCRTBemrPPKm -kgnUjW2txUjHFavsJwEnkS4sReN8l+GyJICpJmyaRaRKMaXVLaQDtB0K+B3gRDdh -L1rFPkEqXn/Be4lt5HvEuzmKTSlD6LqLIPvylENInyKjFpL9UyDUVfnEY4ULN97I -DUPMrs+DvtDOqxs7z41CU0oHCvh0KjrV7LtcNM7S25WMAR5n92AQePdqTranIcqe -GoTnDgLbr+qC1sti1P1yWCnS3hCy3wK3CbSUq94UomA8vgb+W3wbZMyzAx9SRVC4 -vIF2ZLejy7GlF4I8EPYnwH141N5Sc7J+mV3O8bEDy9Q9yRa8fHTFBIU8rI8Jqchr -AVN4NcbudDYCQCxYJHGsZH3769sO6jt4vZXPLJW7g1gMDJfRLpSXlDvQ6kzdDVJc -aOjSkjS+gv0XQpMPegHnw3qe+kd7/wb0jn8C6LtkbH61QmTA/7undVMKvUQLB13H -D0OT6i9FxQIDAQABoAAwDQYJKoZIhvcNAQELBQADggIBAEUBbcJ/E1MXP5gBpIOQ -cTefU8jrOY75HOPekzxxdv0F9U/ZVkY1sqMomgnkE46pboMUryKhnqFez3dVX/EY -oFPjXhBY4FrP3ysy/ziHxwjxs1pPKP9kw3xJAamos3rKNCEa+TeHslfyxKsNZgiM -est071q3H+GrW0DSO6Ya97Xqu1m6H8vx+M1s63tUGZKBiTn5ZHS44MSIUCk91kD5 -DY2CJMk7XUKDWzkcZqrYzqoJisO8ql8Z6PuwurrM7YP7t89AO3u/7ilDLit2robH -vpmGz1IaUvPE+dOxDS3i/CoBxu3S/815vAdmW5EeFnlmaSTa+VilVFcih/UkXFW3 -FRwBXgxfRk7pP6+9hMU/OZ6YmRU7Etc7U7D2/Een4XJy+8wdqwME1QILsb8KQwMm -YqEZJcRwptnmh2r0tHACy0x3xmfySRz7niAJQzWDoCR+nYoN6lX/KvNFTYoqsJIW -E6prKnEA6ojj/wHXMc0lnQcvfWWFl2zz3DFgsOOPqHKq4pQS28iLp+HE86epoQOi -IQrf+JmdvWl8JZc0RXH7m1PXlotZvnAEWxgY1n08CR02x5Vln5yokezmekYybfH4 -GwC2J6ZcRCj5XqWgnUimgFS+M0v5lbjmMzeEBVDVXFehlU8TeMVr6SNe+FNS3ZxF -yJBt17EYv+3GU6pEWxFg8WTv +SIb3DQEBAQUAA4ICDwAwggIKAoICAQC+5/MaVvVoKO/WgIBij998FE5Zlg2PrZ96 +rdDgRxw424bkZR3juUeuyNnnD/wfKGXjTJHNV9H5LCIKPq5GIKz0l9T2KZIR0C3e +j0sa6Er3er0ntcqSjg9kfWhRy5o7ji/20xqv07STqajm8t+Fvv939RLF+EOKGU53 +9HpeMtcVv+SYmFfMxQoPwX99KOdTX2GWKciVDNmuYDQfjRCNlR6ep6W/++7AuFcS +dvKCbo03plqQQUgctNbTEki+UgyynfYErPOyHbxa1Q6LeiEifau8/GQ0Q8wap0LV +uGZ9AwsVX0heQOCb7GgPAM3WtspYkEL60c+ae7bt+HDuQYQXZ7FRRyNjVqynGv3a +GT541jSnZsMAUKl9NLGYoZGIMczAVhyI8HFEC2Oi6LKaF/Pu7hXiYVsK2InnqCkf +j9mSnG5ouGhQOZPM5GkbwaqEfQDlJCsTGchSbl7V+o8Z6mfOyWU0NxReBil6LxxD +1Kv6e4MR2HQzqephozdCXXbJNlPSYyGkSpPEYvuIyN6Rqlwmnfd9YU+EuzuKcw4n +4f1pPnDVb6SOLyzZsiymPNtjQuvkuPpYOV6d3tNA+NXZNB3zBSDu/FprmVMAGC5P +p211YAgGEvvN1EPFNR1lZsorkqU5J7YYSu/kVLqaAwaD2htrMCTRGbJlZvziWF/P +o/VrvKLyVwIDAQABoEwwSgYJKoZIhvcNAQkOMT0wOzA5BgNVHREEMjAwgglsb2Nh +bGhvc3SCCyoubG9jYWxob3N0hwR/AAABhxAAAAAAAAAAAAAAAAAAAAABMA0GCSqG +SIb3DQEBCwUAA4ICAQBsLRock1ry5HtjY7UFxJRIUhjBjx3ihDlCL2/N4aJLvQvw +BiJqQfaudF5LQfGPo0YooVg13Xj9kV1OH64DrdqqqqvTIaWP87W+YsK0NXmpaEF9 +ZpOc171b+y9+rOOcJdjoM9sFv/ndp9GyH0a20bxyqmxPX05bLBxZJv7AA4ICaO/Q +lhec5uhAu5tL5jstrha1BzoW3z6PJJaAbhYbDI5qGd0TwApkO+uAelOKDhgJBiwS +rV5+ukodAyF3DydNFfXhrjTRvZAFL0BQuEv9lQWLp4Feez0gbyG21Qb4sBzVGBVb +PbopyOL+ObFRLhV5tyAkNH3oEI5HZ+MD4HROVEFJBNl4xL61eCiXmdiz4HQ3RtNU +4a77Rj9uB0fAwOMoT2QlUf+qX6Tqwhn3gBajglf3fJyJQFd5QROM64+3BNEz3ZvH +IhvYOJenK61+NiePu5pbABDKjxXSvHxpl7/XkLQaHQhYONH7nJnxjkkqcRfniKdY +iKv61wjQ0zhz6d2LhdgODuZGNorgg4pY14+FbdTcWs4Ox6cZ97xyFmya6b8xcsw3 +WsLslFIw69LtUt53X9GmQVxnZMGaqjfcwlxojVSM45uWneZ+G9F7QQcblKksyIYd ++5r2YAuswUNVnD9AGAOze7QRNg9gIFdNSzYFgiDSPPWD6vmZMLk1wvp2bB6QNg== -----END CERTIFICATE REQUEST----- diff --git a/files/certs/test.ky b/files/certs/test.ky index bebeb93..0523c3e 100644 --- a/files/certs/test.ky +++ b/files/certs/test.ky @@ -1,51 +1,52 @@ ------BEGIN RSA PRIVATE KEY----- -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 ------END RSA PRIVATE KEY----- +-----BEGIN PRIVATE KEY----- +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 +NzmCIGr90MLSTfHMbNYp91iFVq4U0rw= +-----END PRIVATE KEY----- diff --git a/internal/alog/alog_test.go b/internal/alog/alog_test.go new file mode 100644 index 0000000..78dba00 --- /dev/null +++ b/internal/alog/alog_test.go @@ -0,0 +1,230 @@ +/* +Copyright 2017-2026 Adobe. All rights reserved. +This file is licensed 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 REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ + +package alog + +import ( + "bytes" + "net/http" + "net/http/httptest" + "net/url" + "testing" + "time" + + "github.com/adobe/butler/internal/config" + "github.com/adobe/butler/internal/methods" + + . "gopkg.in/check.v1" +) + +func Test(t *testing.T) { TestingT(t) } + +type AlogTestSuite struct{} + +var _ = Suite(&AlogTestSuite{}) + +func (s *AlogTestSuite) TestApacheLogRecordWrite(c *C) { + // Create a response recorder + recorder := httptest.NewRecorder() + + // Create an ApacheLogRecord + record := &ApacheLogRecord{ + ResponseWriter: recorder, + log: false, + ip: "127.0.0.1", + time: time.Now(), + method: "GET", + uri: "/test", + protocol: "HTTP/1.1", + status: http.StatusOK, + responseBytes: 0, + } + + // Write some data + testData := []byte("Hello, World!") + n, err := record.Write(testData) + c.Assert(err, IsNil) + c.Assert(n, Equals, len(testData)) + c.Assert(record.responseBytes, Equals, int64(len(testData))) +} + +func (s *AlogTestSuite) TestApacheLogRecordWriteHeader(c *C) { + recorder := httptest.NewRecorder() + + record := &ApacheLogRecord{ + ResponseWriter: recorder, + status: http.StatusOK, + } + + record.WriteHeader(http.StatusNotFound) + c.Assert(record.status, Equals, http.StatusNotFound) + c.Assert(recorder.Code, Equals, http.StatusNotFound) +} + +func (s *AlogTestSuite) TestApacheLogRecordLog(c *C) { + recorder := httptest.NewRecorder() + + // Test with logging disabled + record := &ApacheLogRecord{ + ResponseWriter: recorder, + log: false, + ip: "127.0.0.1", + time: time.Now(), + method: "GET", + uri: "/test", + protocol: "HTTP/1.1", + status: http.StatusOK, + responseBytes: 100, + elapsedTime: time.Millisecond * 50, + } + + // This should not panic even with logging disabled + record.Log() +} + +func (s *AlogTestSuite) TestApacheLogRecordLogEnabled(c *C) { + recorder := httptest.NewRecorder() + + // Test with logging enabled + record := &ApacheLogRecord{ + ResponseWriter: recorder, + log: true, + ip: "192.168.1.1", + time: time.Date(2024, 1, 15, 10, 30, 0, 0, time.UTC), + method: "POST", + uri: "/api/reload", + protocol: "HTTP/1.1", + status: http.StatusOK, + responseBytes: 256, + elapsedTime: time.Millisecond * 100, + } + + // This should log without panicking + record.Log() +} + +func (s *AlogTestSuite) TestNewApacheLoggingHandler(c *C) { + // Create a simple handler + innerHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + w.Write([]byte("OK")) + }) + + // Create butler config + u, _ := url.Parse("http://localhost") + opts := &config.ButlerConfigOpts{ + InsecureSkipVerify: false, + URL: u, + } + bc, _ := config.NewButlerConfig(opts) + bc.SetMethodOpts(methods.HTTPMethodOpts{Scheme: u.Scheme}) + bc.Config = config.NewConfigSettings() + bc.Config.Globals.EnableHTTPLog = true + + // Create the logging handler + loggingHandler := NewApacheLoggingHandler(innerHandler, bc) + c.Assert(loggingHandler, NotNil) +} + +func (s *AlogTestSuite) TestApacheLoggingHandlerServeHTTP(c *C) { + // Create a simple handler that writes a response + innerHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + w.Write([]byte("Hello")) + }) + + // Create butler config + u, _ := url.Parse("http://localhost") + opts := &config.ButlerConfigOpts{ + InsecureSkipVerify: false, + URL: u, + } + bc, _ := config.NewButlerConfig(opts) + bc.SetMethodOpts(methods.HTTPMethodOpts{Scheme: u.Scheme}) + bc.Config = config.NewConfigSettings() + bc.Config.Globals.EnableHTTPLog = false + + // Create the logging handler + loggingHandler := NewApacheLoggingHandler(innerHandler, bc) + + // Create a test request + req := httptest.NewRequest("GET", "/test", nil) + req.RemoteAddr = "192.168.1.100:12345" + + // Create a response recorder + recorder := httptest.NewRecorder() + + // Serve the request + loggingHandler.ServeHTTP(recorder, req) + + // Verify the response + c.Assert(recorder.Code, Equals, http.StatusOK) + c.Assert(recorder.Body.String(), Equals, "Hello") +} + +func (s *AlogTestSuite) TestApacheLoggingHandlerServeHTTPWithLogging(c *C) { + // Create a simple handler + innerHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusCreated) + w.Write([]byte("Created")) + }) + + // Create butler config with logging enabled + u, _ := url.Parse("http://localhost") + opts := &config.ButlerConfigOpts{ + InsecureSkipVerify: false, + URL: u, + } + bc, _ := config.NewButlerConfig(opts) + bc.SetMethodOpts(methods.HTTPMethodOpts{Scheme: u.Scheme}) + bc.Config = config.NewConfigSettings() + bc.Config.Globals.EnableHTTPLog = true + + loggingHandler := NewApacheLoggingHandler(innerHandler, bc) + + // Test with IPv6 address + req := httptest.NewRequest("POST", "/api/create", bytes.NewReader([]byte("{}"))) + req.RemoteAddr = "[::1]:54321" + + recorder := httptest.NewRecorder() + loggingHandler.ServeHTTP(recorder, req) + + c.Assert(recorder.Code, Equals, http.StatusCreated) +} + +func (s *AlogTestSuite) TestApacheLoggingHandlerClientIPExtraction(c *C) { + innerHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + }) + + u, _ := url.Parse("http://localhost") + opts := &config.ButlerConfigOpts{URL: u} + bc, _ := config.NewButlerConfig(opts) + bc.SetMethodOpts(methods.HTTPMethodOpts{Scheme: u.Scheme}) + bc.Config = config.NewConfigSettings() + bc.Config.Globals.EnableHTTPLog = false + + loggingHandler := NewApacheLoggingHandler(innerHandler, bc) + + // Test with standard IP:port format + req := httptest.NewRequest("GET", "/", nil) + req.RemoteAddr = "10.0.0.1:8080" + + recorder := httptest.NewRecorder() + loggingHandler.ServeHTTP(recorder, req) + c.Assert(recorder.Code, Equals, http.StatusOK) +} + +func (s *AlogTestSuite) TestApacheFormatPattern(c *C) { + // Verify the format pattern is correct + c.Assert(ApacheFormatPattern, Equals, "%s - - [%s] \"%s %d %d\" %f\n") +} diff --git a/internal/config/chan.go b/internal/config/chan.go index 688de48..5a68a65 100644 --- a/internal/config/chan.go +++ b/internal/config/chan.go @@ -34,6 +34,9 @@ type ChanEvent interface { SetTmpFile(string, string, string) error CopyPrimaryConfigFiles(map[string]*ManagerOpts) bool CopyAdditionalConfigFiles(string) bool + // Watch-only mode methods - compare hashes without writing files + ComparePrimaryConfigHashes(map[string]*ManagerOpts, map[string]string) (bool, map[string]string) + CompareAdditionalConfigHashes(map[string]string) (bool, map[string]string) } // ConfigChanEvent is the object passed around in the channel which contains @@ -222,3 +225,83 @@ func (c *ConfigChanEvent) CopyAdditionalConfigFiles(destDir string) bool { } return IsModified } + +// ComparePrimaryConfigHashes compares hashes of primary config files without writing to disk. +// This is used in watch-only mode. Returns true if any file has changed, along with updated hashes. +func (c *ConfigChanEvent) ComparePrimaryConfigHashes(opts map[string]*ManagerOpts, storedHashes map[string]string) (bool, map[string]string) { + var ( + primaryConfigs []string + hasChanged bool + ) + newHashes := make(map[string]string) + + // Copy existing hashes + for k, v := range storedHashes { + newHashes[k] = v + } + + // Get list of primary config files in order + for _, opt := range opts { + for _, config := range opt.PrimaryConfig { + primaryConfigs = append(primaryConfigs, config) + } + } + + // Compare hashes for each primary config file + for _, f := range primaryConfigs { + for _, t := range c.GetTmpFileMap() { + if t.Name == f { + hashKey := fmt.Sprintf("primary:%s", f) + storedHash := storedHashes[hashKey] + + changed, newHash, err := CompareHashOnly(t.File, storedHash, c.Manager) + if err != nil { + log.Errorf("ConfigChanEvent::ComparePrimaryConfigHashes(): error computing hash for %s: %v", f, err) + continue + } + + newHashes[hashKey] = newHash + if changed { + hasChanged = true + log.Infof("ConfigChanEvent::ComparePrimaryConfigHashes(): primary config %s has changed", f) + } + break + } + } + } + + return hasChanged, newHashes +} + +// CompareAdditionalConfigHashes compares hashes of additional config files without writing to disk. +// This is used in watch-only mode. Returns true if any file has changed, along with updated hashes. +func (c *ConfigChanEvent) CompareAdditionalConfigHashes(storedHashes map[string]string) (bool, map[string]string) { + var hasChanged bool + newHashes := make(map[string]string) + + // Copy existing hashes + for k, v := range storedHashes { + newHashes[k] = v + } + + log.Debugf("ConfigChanEvent::CompareAdditionalConfigHashes(): entering") + + for _, f := range c.GetTmpFileMap() { + hashKey := fmt.Sprintf("additional:%s", f.Name) + storedHash := storedHashes[hashKey] + + changed, newHash, err := CompareHashOnly(f.File, storedHash, c.Manager) + if err != nil { + log.Errorf("ConfigChanEvent::CompareAdditionalConfigHashes(): error computing hash for %s: %v", f.Name, err) + continue + } + + newHashes[hashKey] = newHash + if changed { + hasChanged = true + log.Infof("ConfigChanEvent::CompareAdditionalConfigHashes(): additional config %s has changed", f.Name) + } + } + + return hasChanged, newHashes +} diff --git a/internal/config/chan_test.go b/internal/config/chan_test.go new file mode 100644 index 0000000..ea16da6 --- /dev/null +++ b/internal/config/chan_test.go @@ -0,0 +1,164 @@ +/* +Copyright 2017-2026 Adobe. All rights reserved. +This file is licensed 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 REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ + +package config + +import ( + "os" + + . "gopkg.in/check.v1" +) + +func (s *ConfigTestSuite) TestNewConfigChanEvent(c *C) { + event := NewConfigChanEvent() + c.Assert(event, NotNil) + c.Assert(event.Repo, NotNil) + c.Assert(event.HasChanged, Equals, false) +} + +func (s *ConfigTestSuite) TestConfigChanEventCanCopyFilesAllSuccess(c *C) { + event := NewConfigChanEvent() + event.SetSuccess("repo1", "file1.yml", nil) + event.SetSuccess("repo1", "file2.yml", nil) + event.SetSuccess("repo2", "file3.yml", nil) + + c.Assert(event.CanCopyFiles(), Equals, true) +} + +func (s *ConfigTestSuite) TestConfigChanEventCanCopyFilesWithFailure(c *C) { + event := NewConfigChanEvent() + event.SetSuccess("repo1", "file1.yml", nil) + event.SetFailure("repo1", "file2.yml", nil) + + c.Assert(event.CanCopyFiles(), Equals, false) +} + +func (s *ConfigTestSuite) TestConfigChanEventCanCopyFilesEmpty(c *C) { + event := NewConfigChanEvent() + c.Assert(event.CanCopyFiles(), Equals, true) +} + +func (s *ConfigTestSuite) TestConfigChanEventSetSuccessInitializesRepo(c *C) { + event := &ConfigChanEvent{} + c.Assert(event.Repo, IsNil) + + event.SetSuccess("newrepo", "file.yml", nil) + c.Assert(event.Repo, NotNil) + c.Assert(event.Repo["newrepo"], NotNil) + c.Assert(event.Repo["newrepo"].Success["file.yml"], Equals, true) +} + +func (s *ConfigTestSuite) TestConfigChanEventSetFailureInitializesRepo(c *C) { + event := &ConfigChanEvent{} + c.Assert(event.Repo, IsNil) + + event.SetFailure("newrepo", "file.yml", nil) + c.Assert(event.Repo, NotNil) + c.Assert(event.Repo["newrepo"], NotNil) + c.Assert(event.Repo["newrepo"].Success["file.yml"], Equals, false) +} + +func (s *ConfigTestSuite) TestConfigChanEventSetTmpFile(c *C) { + event := NewConfigChanEvent() + event.SetSuccess("repo1", "file.yml", nil) + event.SetTmpFile("repo1", "file.yml", "/tmp/butler-12345") + + c.Assert(event.Repo["repo1"].TmpFile["file.yml"], Equals, "/tmp/butler-12345") +} + +func (s *ConfigTestSuite) TestConfigChanEventSetTmpFileNoRepo(c *C) { + event := NewConfigChanEvent() + // Setting tmp file for non-existent repo should not panic + err := event.SetTmpFile("nonexistent", "file.yml", "/tmp/butler-12345") + c.Assert(err, IsNil) +} + +func (s *ConfigTestSuite) TestConfigChanEventGetTmpFileMap(c *C) { + event := NewConfigChanEvent() + event.SetSuccess("repo1", "b_file.yml", nil) + event.SetSuccess("repo1", "a_file.yml", nil) + event.SetTmpFile("repo1", "b_file.yml", "/tmp/butler-b") + event.SetTmpFile("repo1", "a_file.yml", "/tmp/butler-a") + + tmpFiles := event.GetTmpFileMap() + c.Assert(len(tmpFiles), Equals, 2) + // Should be sorted alphabetically + c.Assert(tmpFiles[0].Name, Equals, "a_file.yml") + c.Assert(tmpFiles[0].File, Equals, "/tmp/butler-a") + c.Assert(tmpFiles[1].Name, Equals, "b_file.yml") + c.Assert(tmpFiles[1].File, Equals, "/tmp/butler-b") +} + +func (s *ConfigTestSuite) TestConfigChanEventCleanTmpFiles(c *C) { + // Create actual temp files + tmpFile1, err := os.CreateTemp("", "butler-test-clean1-*") + c.Assert(err, IsNil) + tmpFile1.Close() + + tmpFile2, err := os.CreateTemp("", "butler-test-clean2-*") + c.Assert(err, IsNil) + tmpFile2.Close() + + // Verify files exist + _, err = os.Stat(tmpFile1.Name()) + c.Assert(err, IsNil) + _, err = os.Stat(tmpFile2.Name()) + c.Assert(err, IsNil) + + event := NewConfigChanEvent() + event.SetSuccess("repo1", "file1.yml", nil) + event.SetSuccess("repo1", "file2.yml", nil) + event.SetTmpFile("repo1", "file1.yml", tmpFile1.Name()) + event.SetTmpFile("repo1", "file2.yml", tmpFile2.Name()) + + // Clean up + err = event.CleanTmpFiles() + c.Assert(err, IsNil) + + // Verify files are deleted + _, err = os.Stat(tmpFile1.Name()) + c.Assert(os.IsNotExist(err), Equals, true) + _, err = os.Stat(tmpFile2.Name()) + c.Assert(os.IsNotExist(err), Equals, true) +} + +func (s *ConfigTestSuite) TestConfigChanEventCleanTmpFilesWithMainTmpFile(c *C) { + // Create a main temp file + mainTmpFile, err := os.CreateTemp("", "butler-test-main-*") + c.Assert(err, IsNil) + mainTmpFile.Close() + + event := NewConfigChanEvent() + event.TmpFile = mainTmpFile + + // Clean up + err = event.CleanTmpFiles() + c.Assert(err, IsNil) + + // Verify main file is deleted + _, err = os.Stat(mainTmpFile.Name()) + c.Assert(os.IsNotExist(err), Equals, true) +} + +func (s *ConfigTestSuite) TestConfigChanEventMultipleRepos(c *C) { + event := NewConfigChanEvent() + + // Add files from multiple repos + event.SetSuccess("repo1", "file1.yml", nil) + event.SetSuccess("repo2", "file2.yml", nil) + event.SetSuccess("repo3", "file3.yml", nil) + + c.Assert(len(event.Repo), Equals, 3) + c.Assert(event.Repo["repo1"].Success["file1.yml"], Equals, true) + c.Assert(event.Repo["repo2"].Success["file2.yml"], Equals, true) + c.Assert(event.Repo["repo3"].Success["file3.yml"], Equals, true) +} diff --git a/internal/config/config_test.go b/internal/config/config_test.go index b25b107..1c65f3b 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -175,6 +175,62 @@ var TestConfigCompleteEnvironment = []byte(`[globals] timeout = "10" `) +// Test config for watch-only mode +var TestConfigWatchOnly = []byte(`[globals] + config-managers = ["test-handler"] + scheduler-interval = 300 + exit-on-config-failure = "false" + status-file = "/var/tmp/butler.status" + [test-handler] + repos = ["localhost"] + clean-files = "false" + watch-only = "true" + skip-butler-header = "true" + enable-cache = "false" + primary-config-name = "config.yml" + [test-handler.localhost] + method = "file" + repo-path = "/tmp/butler-test" + primary-config = ["test.yml"] + [test-handler.localhost.file] + path = "/tmp/butler-test" + [test-handler.reloader] + method = "http" + [test-handler.reloader.http] + host = "localhost" + port = "8080" + uri = "/reload" + method = "post" + payload = "{}" + content-type = "application/json" + retries = "3" + retry-wait-min = "1" + retry-wait-max = "5" + timeout = "10" +`) + +// Test config for watch-only mode with dest-path (should still work) +var TestConfigWatchOnlyWithDestPath = []byte(`[globals] + config-managers = ["test-handler"] + scheduler-interval = 300 + exit-on-config-failure = "false" + status-file = "/var/tmp/butler.status" + [test-handler] + repos = ["localhost"] + clean-files = "false" + watch-only = "true" + skip-butler-header = "true" + enable-cache = "false" + dest-path = "/tmp/butler-dest" + primary-config-name = "config.yml" + [test-handler.localhost] + method = "file" + repo-path = "/tmp/butler-test" + primary-config = ["test.yml"] + [test-handler.localhost.file] + path = "/tmp/butler-test" +`) + var TestManagerNoURLs = []byte(`[testing] `) @@ -490,3 +546,85 @@ func (s *ConfigTestSuite) TestConfigCompleteEnvironment(c *C) { os.Unsetenv("RELOADER_HOST") os.Unsetenv("MSUB") } + +func (s *ConfigTestSuite) TestConfigWatchOnlyMode(c *C) { + var ( + err error + config ConfigSettings + ) + + // Load the watch-only config + err = ParseConfig(TestConfigWatchOnly) + c.Assert(err, IsNil) + + // Get the configuration + err = GetConfigManager("test-handler", &config) + c.Assert(err, IsNil) + + // Verify watch-only mode is enabled + mgr := config.Managers["test-handler"] + c.Assert(mgr.WatchOnly, Equals, true) + c.Assert(mgr.SkipButlerHeader, Equals, true) + c.Assert(mgr.CleanFiles, Equals, false) + + // Verify FileHashes map is initialized + c.Assert(mgr.FileHashes, NotNil) + + // Verify dest-path is empty (optional in watch-only mode) + // Note: filepath.Clean("") returns "." so we check for that + c.Assert(mgr.DestPath, Equals, "") +} + +func (s *ConfigTestSuite) TestConfigWatchOnlyModeWithDestPath(c *C) { + var ( + err error + config ConfigSettings + ) + + // Load the watch-only config with dest-path + err = ParseConfig(TestConfigWatchOnlyWithDestPath) + c.Assert(err, IsNil) + + // Get the configuration + err = GetConfigManager("test-handler", &config) + c.Assert(err, IsNil) + + // Verify watch-only mode is enabled + mgr := config.Managers["test-handler"] + c.Assert(mgr.WatchOnly, Equals, true) + c.Assert(mgr.SkipButlerHeader, Equals, true) + + // Verify dest-path is set even in watch-only mode (it's optional but allowed) + c.Assert(mgr.DestPath, Equals, "/tmp/butler-dest") + + // Verify FileHashes map is initialized + c.Assert(mgr.FileHashes, NotNil) +} + +func (s *ConfigTestSuite) TestConfigWatchOnlyModeDisabled(c *C) { + var ( + err error + config ConfigSettings + ) + + // Load the standard config (watch-only not set) + err = ParseConfig(TestConfigCompleteEnvironment) + c.Assert(err, IsNil) + + // setup environment for this test + os.Setenv("RELOADER_HOST", "localhost") + os.Setenv("MSUB", "test") + defer os.Unsetenv("RELOADER_HOST") + defer os.Unsetenv("MSUB") + + // Get the configuration + err = GetConfigManager("test-handler", &config) + c.Assert(err, IsNil) + + // Verify watch-only mode is disabled by default + mgr := config.Managers["test-handler"] + c.Assert(mgr.WatchOnly, Equals, false) + + // Verify FileHashes map is nil when watch-only is disabled + c.Assert(mgr.FileHashes, IsNil) +} diff --git a/internal/config/handler.go b/internal/config/handler.go index a1a7f64..1a84aac 100644 --- a/internal/config/handler.go +++ b/internal/config/handler.go @@ -292,10 +292,41 @@ func (bc *ButlerConfig) RunCMHandler() error { if PrimaryChan.CanCopyFiles() && AdditionalChan.CanCopyFiles() { log.Debugf("Config::RunCMHandler()[count=%v]: successfully retrieved files. processing...", cmHandlerCounter) - p := PrimaryChan.CopyPrimaryConfigFiles(m.ManagerOpts) - a := AdditionalChan.CopyAdditionalConfigFiles(m.DestPath) - if p || a { - ReloadManager = append(ReloadManager, m.Name) + + // Check if watch-only mode is enabled for this manager + if m.WatchOnly { + // Watch-only mode: compare hashes without writing files + log.Debugf("Config::RunCMHandler()[count=%v][manager=%v]: using watch-only mode", cmHandlerCounter, m.Name) + + // Initialize hash map if nil + if m.FileHashes == nil { + m.FileHashes = make(map[string]string) + } + + // Compare primary config hashes + pChanged, pHashes := PrimaryChan.ComparePrimaryConfigHashes(m.ManagerOpts, m.FileHashes) + // Compare additional config hashes + aChanged, aHashes := AdditionalChan.CompareAdditionalConfigHashes(m.FileHashes) + + // Merge the new hashes back + for k, v := range pHashes { + m.FileHashes[k] = v + } + for k, v := range aHashes { + m.FileHashes[k] = v + } + + if pChanged || aChanged { + ReloadManager = append(ReloadManager, m.Name) + log.Infof("Config::RunCMHandler()[count=%v][manager=%v]: watch-only mode detected changes, will trigger reload", cmHandlerCounter, m.Name) + } + } else { + // Normal mode: copy files to destination + p := PrimaryChan.CopyPrimaryConfigFiles(m.ManagerOpts) + a := AdditionalChan.CopyAdditionalConfigFiles(m.DestPath) + if p || a { + ReloadManager = append(ReloadManager, m.Name) + } } PrimaryChan.CleanTmpFiles() AdditionalChan.CleanTmpFiles() @@ -413,6 +444,12 @@ func (bc *ButlerConfig) GetStatusFile() string { func (bc *ButlerConfig) CheckPaths() error { log.Debugf("Config::CheckPaths(): entering") for _, m := range bc.Config.Managers { + // Skip path creation and cleanup in watch-only mode + if m.WatchOnly { + log.Debugf("Config::CheckPaths(): skipping path checks for manager %s (watch-only mode)", m.Name) + continue + } + for _, f := range m.GetAllLocalPaths() { dir := filepath.Dir(f) if _, err := os.Stat(dir); err != nil { diff --git a/internal/config/helpers.go b/internal/config/helpers.go index 1cd05a4..0fd8f9c 100644 --- a/internal/config/helpers.go +++ b/internal/config/helpers.go @@ -15,6 +15,8 @@ package config import ( "bufio" "bytes" + "crypto/sha256" + "encoding/hex" "errors" "fmt" "io" @@ -392,6 +394,64 @@ func CompareAndCopy(source string, dest string, m string) bool { } } +// ComputeFileHash computes the SHA256 hash of a file and returns it as a hex string. +// This is used in watch-only mode to detect file changes without writing to disk. +func ComputeFileHash(filePath string) (string, error) { + f, err := os.Open(filePath) + if err != nil { + return "", err + } + defer f.Close() + + h := sha256.New() + if _, err := io.Copy(h, f); err != nil { + return "", err + } + + return hex.EncodeToString(h.Sum(nil)), nil +} + +// ComputeDataHash computes the SHA256 hash of a byte slice and returns it as a hex string. +// This is used in watch-only mode to hash downloaded content before comparison. +func ComputeDataHash(data []byte) string { + h := sha256.Sum256(data) + return hex.EncodeToString(h[:]) +} + +// CompareHashOnly compares the hash of a source file against a stored hash. +// Returns true if the file has changed (hashes differ), false if unchanged. +// This is used in watch-only mode instead of CompareAndCopy. +func CompareHashOnly(source string, storedHash string, m string) (bool, string, error) { + newHash, err := ComputeFileHash(source) + if err != nil { + log.Errorf("helpers.CompareHashOnly()[count=%v][manager=%v]: could not compute hash for source=%v err=%#v", cmHandlerCounter, m, source, err) + return false, "", err + } + + if storedHash == "" { + // First run - no stored hash, consider it changed + log.Infof("helpers.CompareHashOnly()[count=%v][manager=%v]: No stored hash for \"%s\". First run detected.", cmHandlerCounter, m, source) + return true, newHash, nil + } + + if newHash != storedHash { + // Safely truncate hashes for logging (handle short hashes gracefully) + oldHashDisplay := storedHash + if len(storedHash) > 16 { + oldHashDisplay = storedHash[:16] + "..." + } + newHashDisplay := newHash + if len(newHash) > 16 { + newHashDisplay = newHash[:16] + "..." + } + log.Infof("helpers.CompareHashOnly()[count=%v][manager=%v]: Hash changed for \"%s\". Old=%s New=%s", cmHandlerCounter, m, source, oldHashDisplay, newHashDisplay) + return true, newHash, nil + } + + log.Debugf("helpers.CompareHashOnly()[count=%v][manager=%v]: Hash unchanged for \"%s\"", cmHandlerCounter, m, source) + return false, newHash, nil +} + // CopyFile copies the src path string to the dst path string. If there is an // error, an error is returned, otherwise nil is returned. func CopyFile(src string, dst string) error { @@ -642,6 +702,16 @@ func GetConfigManager(entry string, bc *ConfigSettings) error { Mgr.SkipButlerHeader = false } + envWatchOnly := strings.ToLower(environment.GetVar(Mgr.CfgWatchOnly)) + if envWatchOnly == "true" { + Mgr.WatchOnly = true + // Initialize the hash storage map for watch-only mode + Mgr.FileHashes = make(map[string]string) + log.Infof("helpers.GetConfigManager()[count=%v][manager=%v]: watch-only mode enabled", cmHandlerCounter, entry) + } else { + Mgr.WatchOnly = false + } + Mgr.CachePath = filepath.Clean(environment.GetVar(Mgr.CachePath)) if Mgr.EnableCache && Mgr.CachePath == "" { msg := fmt.Sprintf("Caching Enabled but manager.cache-path is unset for manager %s", entry) @@ -650,10 +720,16 @@ func GetConfigManager(entry string, bc *ConfigSettings) error { Mgr.DestPath = filepath.Clean(environment.GetVar(Mgr.DestPath)) Mgr.PrimaryConfigName = filepath.Clean(environment.GetVar(Mgr.PrimaryConfigName)) - if Mgr.DestPath == "" { - msg := fmt.Sprintf("No dest-path configured for manager %s", entry) + // dest-path is only required when NOT in watch-only mode + if Mgr.DestPath == "" && !Mgr.WatchOnly { + msg := fmt.Sprintf("No dest-path configured for manager %s (required when watch-only is not enabled)", entry) return errors.New(msg) } + // In watch-only mode, dest-path is optional but we'll set a default if not provided + if Mgr.WatchOnly && Mgr.DestPath == "." { + Mgr.DestPath = "" + log.Debugf("helpers.GetConfigManager()[count=%v][manager=%v]: watch-only mode - dest-path not required", cmHandlerCounter, entry) + } Mgr.ManagerOpts = make(map[string]*ManagerOpts) for _, m := range Mgr.Repos { diff --git a/internal/config/helpers_test.go b/internal/config/helpers_test.go index 74ae2c0..36ee896 100644 --- a/internal/config/helpers_test.go +++ b/internal/config/helpers_test.go @@ -14,6 +14,7 @@ package config import ( "bytes" + "os" . "gopkg.in/check.v1" ) @@ -105,3 +106,180 @@ func (s *ConfigTestSuite) TestcheckButlerHeaderFooter(c *C) { c.Assert(checkButlerHeaderFooter([]byte(butlerFooter)), Equals, true) c.Assert(checkButlerHeaderFooter([]byte("asdfawsdf")), Equals, false) } + +func (s *ConfigTestSuite) TestComputeDataHash(c *C) { + // Test that same data produces same hash + data1 := []byte("test data for hashing") + data2 := []byte("test data for hashing") + hash1 := ComputeDataHash(data1) + hash2 := ComputeDataHash(data2) + c.Assert(hash1, Equals, hash2) + + // Test that different data produces different hash + data3 := []byte("different test data") + hash3 := ComputeDataHash(data3) + c.Assert(hash1, Not(Equals), hash3) + + // Test that hash is a valid hex string (64 chars for SHA256) + c.Assert(len(hash1), Equals, 64) +} + +func (s *ConfigTestSuite) TestComputeFileHash(c *C) { + // Create a temporary file for testing + tmpFile, err := os.CreateTemp("", "butler-test-hash-*") + c.Assert(err, IsNil) + defer os.Remove(tmpFile.Name()) + + testContent := []byte("test content for file hashing") + _, err = tmpFile.Write(testContent) + c.Assert(err, IsNil) + tmpFile.Close() + + // Compute hash of the file + hash, err := ComputeFileHash(tmpFile.Name()) + c.Assert(err, IsNil) + c.Assert(len(hash), Equals, 64) + + // Verify it matches the data hash + expectedHash := ComputeDataHash(testContent) + c.Assert(hash, Equals, expectedHash) + + // Test error case - non-existent file + _, err = ComputeFileHash("/nonexistent/file/path") + c.Assert(err, NotNil) +} + +func (s *ConfigTestSuite) TestCompareHashOnly(c *C) { + // Create a temporary file for testing + tmpFile, err := os.CreateTemp("", "butler-test-compare-*") + c.Assert(err, IsNil) + defer os.Remove(tmpFile.Name()) + + testContent := []byte("test content for comparison") + _, err = tmpFile.Write(testContent) + c.Assert(err, IsNil) + tmpFile.Close() + + // Test first run (empty stored hash) - should return changed=true + changed, newHash, err := CompareHashOnly(tmpFile.Name(), "", "test-manager") + c.Assert(err, IsNil) + c.Assert(changed, Equals, true) + c.Assert(len(newHash), Equals, 64) + + // Test same hash - should return changed=false + changed, newHash2, err := CompareHashOnly(tmpFile.Name(), newHash, "test-manager") + c.Assert(err, IsNil) + c.Assert(changed, Equals, false) + c.Assert(newHash2, Equals, newHash) + + // Test different hash - should return changed=true + changed, newHash3, err := CompareHashOnly(tmpFile.Name(), "differenthash", "test-manager") + c.Assert(err, IsNil) + c.Assert(changed, Equals, true) + c.Assert(newHash3, Equals, newHash) + + // Test error case - non-existent file + _, _, err = CompareHashOnly("/nonexistent/file/path", "", "test-manager") + c.Assert(err, NotNil) +} + +func (s *ConfigTestSuite) TestComparePrimaryConfigHashes(c *C) { + // Create temporary files for testing + tmpFile1, err := os.CreateTemp("", "butler-test-primary1-*") + c.Assert(err, IsNil) + defer os.Remove(tmpFile1.Name()) + + tmpFile2, err := os.CreateTemp("", "butler-test-primary2-*") + c.Assert(err, IsNil) + defer os.Remove(tmpFile2.Name()) + + // Write test content + _, err = tmpFile1.Write([]byte("primary config 1 content")) + c.Assert(err, IsNil) + tmpFile1.Close() + + _, err = tmpFile2.Write([]byte("primary config 2 content")) + c.Assert(err, IsNil) + tmpFile2.Close() + + // Create a ConfigChanEvent with test data + chanEvent := NewConfigChanEvent() + chanEvent.Manager = "test-manager" + chanEvent.SetSuccess("test-repo", "config1.yml", nil) + chanEvent.SetSuccess("test-repo", "config2.yml", nil) + chanEvent.SetTmpFile("test-repo", "config1.yml", tmpFile1.Name()) + chanEvent.SetTmpFile("test-repo", "config2.yml", tmpFile2.Name()) + + // Create manager opts + opts := make(map[string]*ManagerOpts) + opts["test-manager.test-repo"] = &ManagerOpts{ + PrimaryConfig: []string{"config1.yml", "config2.yml"}, + } + + // Test first run (empty stored hashes) - should return changed=true + storedHashes := make(map[string]string) + changed, newHashes := chanEvent.ComparePrimaryConfigHashes(opts, storedHashes) + c.Assert(changed, Equals, true) + c.Assert(len(newHashes), Equals, 2) + + // Test same hashes - should return changed=false + changed, newHashes2 := chanEvent.ComparePrimaryConfigHashes(opts, newHashes) + c.Assert(changed, Equals, false) + c.Assert(newHashes2["primary:config1.yml"], Equals, newHashes["primary:config1.yml"]) + c.Assert(newHashes2["primary:config2.yml"], Equals, newHashes["primary:config2.yml"]) +} + +func (s *ConfigTestSuite) TestCompareAdditionalConfigHashes(c *C) { + // Create temporary files for testing + tmpFile1, err := os.CreateTemp("", "butler-test-additional1-*") + c.Assert(err, IsNil) + defer os.Remove(tmpFile1.Name()) + + tmpFile2, err := os.CreateTemp("", "butler-test-additional2-*") + c.Assert(err, IsNil) + defer os.Remove(tmpFile2.Name()) + + // Write test content + _, err = tmpFile1.Write([]byte("additional config 1 content")) + c.Assert(err, IsNil) + tmpFile1.Close() + + _, err = tmpFile2.Write([]byte("additional config 2 content")) + c.Assert(err, IsNil) + tmpFile2.Close() + + // Create a ConfigChanEvent with test data + chanEvent := NewConfigChanEvent() + chanEvent.Manager = "test-manager" + chanEvent.SetSuccess("test-repo", "additional1.yml", nil) + chanEvent.SetSuccess("test-repo", "additional2.yml", nil) + chanEvent.SetTmpFile("test-repo", "additional1.yml", tmpFile1.Name()) + chanEvent.SetTmpFile("test-repo", "additional2.yml", tmpFile2.Name()) + + // Test first run (empty stored hashes) - should return changed=true + storedHashes := make(map[string]string) + changed, newHashes := chanEvent.CompareAdditionalConfigHashes(storedHashes) + c.Assert(changed, Equals, true) + c.Assert(len(newHashes), Equals, 2) + + // Test same hashes - should return changed=false + changed, newHashes2 := chanEvent.CompareAdditionalConfigHashes(newHashes) + c.Assert(changed, Equals, false) + c.Assert(newHashes2["additional:additional1.yml"], Equals, newHashes["additional:additional1.yml"]) + c.Assert(newHashes2["additional:additional2.yml"], Equals, newHashes["additional:additional2.yml"]) + + // Test with modified file - should return changed=true + // Reopen and modify one file + tmpFile1Modified, err := os.OpenFile(tmpFile1.Name(), os.O_WRONLY|os.O_TRUNC, 0644) + c.Assert(err, IsNil) + _, err = tmpFile1Modified.Write([]byte("modified additional config 1 content")) + c.Assert(err, IsNil) + tmpFile1Modified.Close() + + changed, newHashes3 := chanEvent.CompareAdditionalConfigHashes(newHashes2) + c.Assert(changed, Equals, true) + // The modified file should have a different hash + c.Assert(newHashes3["additional:additional1.yml"], Not(Equals), newHashes2["additional:additional1.yml"]) + // The unmodified file should have the same hash + c.Assert(newHashes3["additional:additional2.yml"], Equals, newHashes2["additional:additional2.yml"]) +} diff --git a/internal/config/manager.go b/internal/config/manager.go index 3aa508e..d322515 100644 --- a/internal/config/manager.go +++ b/internal/config/manager.go @@ -48,6 +48,9 @@ type Manager struct { ManagerTimeoutOk bool `json:"manager-timeout-ok"` CfgSkipButlerHeader string `mapstructure:"skip-butler-header" json:"-"` SkipButlerHeader bool `json:"skip-butler-header"` + CfgWatchOnly string `mapstructure:"watch-only" json:"-"` + WatchOnly bool `json:"watch-only"` + FileHashes map[string]string `json:"-"` // In-memory hash storage for watch-only mode ManagerOpts map[string]*ManagerOpts `json:"opts"` Reloader reloaders.Reloader `mapstructure:"-" json:"reloader,omitempty"` ReloadManager bool `json:"-"` diff --git a/internal/config/manager_test.go b/internal/config/manager_test.go new file mode 100644 index 0000000..b3539ee --- /dev/null +++ b/internal/config/manager_test.go @@ -0,0 +1,233 @@ +/* +Copyright 2017-2026 Adobe. All rights reserved. +This file is licensed 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 REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ + +package config + +import ( + "os" + "path/filepath" + + . "gopkg.in/check.v1" +) + +func (s *ConfigTestSuite) TestManagerOptsAppendPrimaryConfigURL(c *C) { + opts := &ManagerOpts{} + err := opts.AppendPrimaryConfigURL("http://example.com/config.yml") + c.Assert(err, IsNil) + c.Assert(len(opts.PrimaryConfigsFullURLs), Equals, 1) + c.Assert(opts.PrimaryConfigsFullURLs[0], Equals, "http://example.com/config.yml") + + // Append another + err = opts.AppendPrimaryConfigURL("http://example.com/config2.yml") + c.Assert(err, IsNil) + c.Assert(len(opts.PrimaryConfigsFullURLs), Equals, 2) +} + +func (s *ConfigTestSuite) TestManagerOptsAppendPrimaryConfigFile(c *C) { + opts := &ManagerOpts{} + err := opts.AppendPrimaryConfigFile("/opt/prometheus/prometheus.yml") + c.Assert(err, IsNil) + c.Assert(len(opts.PrimaryConfigsFullLocalPaths), Equals, 1) + c.Assert(opts.PrimaryConfigsFullLocalPaths[0], Equals, "/opt/prometheus/prometheus.yml") +} + +func (s *ConfigTestSuite) TestManagerOptsAppendAdditionalConfigURL(c *C) { + opts := &ManagerOpts{} + err := opts.AppendAdditionalConfigURL("http://example.com/alerts.yml") + c.Assert(err, IsNil) + c.Assert(len(opts.AdditionalConfigsFullURLs), Equals, 1) + c.Assert(opts.AdditionalConfigsFullURLs[0], Equals, "http://example.com/alerts.yml") +} + +func (s *ConfigTestSuite) TestManagerOptsAppendAdditionalConfigFile(c *C) { + opts := &ManagerOpts{} + err := opts.AppendAdditionalConfigFile("/opt/prometheus/alerts/alert1.yml") + c.Assert(err, IsNil) + c.Assert(len(opts.AdditionalConfigsFullLocalPaths), Equals, 1) + c.Assert(opts.AdditionalConfigsFullLocalPaths[0], Equals, "/opt/prometheus/alerts/alert1.yml") +} + +func (s *ConfigTestSuite) TestManagerOptsSetParentManager(c *C) { + opts := &ManagerOpts{} + err := opts.SetParentManager("prometheus") + c.Assert(err, IsNil) + c.Assert(opts.parentManager, Equals, "prometheus") +} + +func (s *ConfigTestSuite) TestManagerOptsGetPrimaryConfigURLs(c *C) { + opts := &ManagerOpts{ + PrimaryConfigsFullURLs: []string{"http://a.com/1.yml", "http://b.com/2.yml"}, + } + urls := opts.GetPrimaryConfigURLs() + c.Assert(len(urls), Equals, 2) + c.Assert(urls[0], Equals, "http://a.com/1.yml") +} + +func (s *ConfigTestSuite) TestManagerOptsGetPrimaryLocalConfigFiles(c *C) { + opts := &ManagerOpts{ + PrimaryConfigsFullLocalPaths: []string{"/opt/a.yml", "/opt/b.yml"}, + } + files := opts.GetPrimaryLocalConfigFiles() + c.Assert(len(files), Equals, 2) + c.Assert(files[0], Equals, "/opt/a.yml") +} + +func (s *ConfigTestSuite) TestManagerOptsGetPrimaryRemoteConfigFiles(c *C) { + opts := &ManagerOpts{ + PrimaryConfig: []string{"config1.yml", "config2.yml"}, + } + files := opts.GetPrimaryRemoteConfigFiles() + c.Assert(len(files), Equals, 2) + c.Assert(files[0], Equals, "config1.yml") +} + +func (s *ConfigTestSuite) TestManagerOptsGetAdditionalConfigURLs(c *C) { + opts := &ManagerOpts{ + AdditionalConfigsFullURLs: []string{"http://a.com/alerts.yml"}, + } + urls := opts.GetAdditionalConfigURLs() + c.Assert(len(urls), Equals, 1) +} + +func (s *ConfigTestSuite) TestManagerOptsGetAdditionalLocalConfigFiles(c *C) { + opts := &ManagerOpts{ + AdditionalConfigsFullLocalPaths: []string{"/opt/alerts/a.yml"}, + } + files := opts.GetAdditionalLocalConfigFiles() + c.Assert(len(files), Equals, 1) +} + +func (s *ConfigTestSuite) TestManagerOptsGetAdditionalRemoteConfigFiles(c *C) { + opts := &ManagerOpts{ + AdditionalConfig: []string{"alerts/alert1.yml", "rules/rule1.yml"}, + } + files := opts.GetAdditionalRemoteConfigFiles() + c.Assert(len(files), Equals, 2) +} + +func (s *ConfigTestSuite) TestManagerGetAllLocalPaths(c *C) { + mgr := &Manager{ + ManagerOpts: map[string]*ManagerOpts{ + "mgr.repo1": { + PrimaryConfigsFullLocalPaths: []string{"/opt/primary1.yml"}, + AdditionalConfigsFullLocalPaths: []string{"/opt/add1.yml", "/opt/add2.yml"}, + }, + "mgr.repo2": { + PrimaryConfigsFullLocalPaths: []string{"/opt/primary2.yml"}, + AdditionalConfigsFullLocalPaths: []string{"/opt/add3.yml"}, + }, + }, + } + + paths := mgr.GetAllLocalPaths() + c.Assert(len(paths), Equals, 5) +} + +func (s *ConfigTestSuite) TestManagerGetAllLocalPathsEmpty(c *C) { + mgr := &Manager{ + ManagerOpts: map[string]*ManagerOpts{}, + } + + paths := mgr.GetAllLocalPaths() + c.Assert(len(paths), Equals, 0) +} + +func (s *ConfigTestSuite) TestManagerPathCleanupDirectory(c *C) { + // Create a temporary directory structure + tmpDir, err := os.MkdirTemp("", "butler-test-cleanup-*") + c.Assert(err, IsNil) + defer os.RemoveAll(tmpDir) + + mgr := &Manager{ + ManagerOpts: map[string]*ManagerOpts{}, + } + + // Test with a directory - should return nil + dirInfo, err := os.Stat(tmpDir) + c.Assert(err, IsNil) + + err = mgr.PathCleanup(tmpDir, dirInfo, nil) + c.Assert(err, IsNil) +} + +func (s *ConfigTestSuite) TestManagerPathCleanupKnownFile(c *C) { + // Create a temporary directory and file + tmpDir, err := os.MkdirTemp("", "butler-test-cleanup-*") + c.Assert(err, IsNil) + defer os.RemoveAll(tmpDir) + + knownFile := filepath.Join(tmpDir, "known.yml") + err = os.WriteFile(knownFile, []byte("test"), 0644) + c.Assert(err, IsNil) + + mgr := &Manager{ + ManagerOpts: map[string]*ManagerOpts{ + "mgr.repo1": { + PrimaryConfigsFullLocalPaths: []string{knownFile}, + AdditionalConfigsFullLocalPaths: []string{}, + }, + }, + } + + fileInfo, err := os.Stat(knownFile) + c.Assert(err, IsNil) + + // Known file should not be deleted + err = mgr.PathCleanup(knownFile, fileInfo, nil) + c.Assert(err, IsNil) + + // Verify file still exists + _, err = os.Stat(knownFile) + c.Assert(err, IsNil) +} + +func (s *ConfigTestSuite) TestManagerPathCleanupUnknownFile(c *C) { + // Create a temporary directory and file + tmpDir, err := os.MkdirTemp("", "butler-test-cleanup-*") + c.Assert(err, IsNil) + defer os.RemoveAll(tmpDir) + + unknownFile := filepath.Join(tmpDir, "unknown.yml") + err = os.WriteFile(unknownFile, []byte("test"), 0644) + c.Assert(err, IsNil) + + mgr := &Manager{ + ManagerOpts: map[string]*ManagerOpts{ + "mgr.repo1": { + PrimaryConfigsFullLocalPaths: []string{filepath.Join(tmpDir, "known.yml")}, + AdditionalConfigsFullLocalPaths: []string{}, + }, + }, + } + + fileInfo, err := os.Stat(unknownFile) + c.Assert(err, IsNil) + + // Unknown file should be deleted + err = mgr.PathCleanup(unknownFile, fileInfo, nil) + c.Assert(err, NotNil) // Returns error with message about deletion + + // Verify file was deleted + _, err = os.Stat(unknownFile) + c.Assert(os.IsNotExist(err), Equals, true) +} + +func (s *ConfigTestSuite) TestManagerReloadNoReloader(c *C) { + mgr := &Manager{ + Name: "test-manager", + Reloader: nil, + } + + // Should return nil when no reloader is defined + err := mgr.Reload() + c.Assert(err, IsNil) +} diff --git a/internal/config/objects_test.go b/internal/config/objects_test.go new file mode 100644 index 0000000..e900891 --- /dev/null +++ b/internal/config/objects_test.go @@ -0,0 +1,165 @@ +/* +Copyright 2017-2026 Adobe. All rights reserved. +This file is licensed 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 REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ + +package config + +import ( + . "gopkg.in/check.v1" +) + +func (s *ConfigTestSuite) TestNewValidateOpts(c *C) { + opts := NewValidateOpts() + c.Assert(opts.ContentType, Equals, "text") + c.Assert(opts.SkipButlerHeader, Equals, false) + c.Assert(opts.Data, IsNil) + c.Assert(opts.FileName, Equals, "") + c.Assert(opts.Manager, Equals, "") +} + +func (s *ConfigTestSuite) TestValidateOptsWithContentType(c *C) { + opts := NewValidateOpts().WithContentType("yaml") + c.Assert(opts.ContentType, Equals, "yaml") +} + +func (s *ConfigTestSuite) TestValidateOptsWithData(c *C) { + testData := []byte("test data") + opts := NewValidateOpts().WithData(testData) + c.Assert(opts.Data, DeepEquals, testData) +} + +func (s *ConfigTestSuite) TestValidateOptsWithFileName(c *C) { + opts := NewValidateOpts().WithFileName("config.yaml") + c.Assert(opts.FileName, Equals, "config.yaml") +} + +func (s *ConfigTestSuite) TestValidateOptsWithManager(c *C) { + opts := NewValidateOpts().WithManager("prometheus") + c.Assert(opts.Manager, Equals, "prometheus") +} + +func (s *ConfigTestSuite) TestValidateOptsWithSkipButlerHeader(c *C) { + opts := NewValidateOpts().WithSkipButlerHeader(true) + c.Assert(opts.SkipButlerHeader, Equals, true) +} + +func (s *ConfigTestSuite) TestValidateOptsChaining(c *C) { + testData := []byte("test data") + opts := NewValidateOpts(). + WithContentType("json"). + WithData(testData). + WithFileName("test.json"). + WithManager("alertmanager"). + WithSkipButlerHeader(true) + + c.Assert(opts.ContentType, Equals, "json") + c.Assert(opts.Data, DeepEquals, testData) + c.Assert(opts.FileName, Equals, "test.json") + c.Assert(opts.Manager, Equals, "alertmanager") + c.Assert(opts.SkipButlerHeader, Equals, true) +} + +func (s *ConfigTestSuite) TestRepoFileEventSetSuccess(c *C) { + rfe := &RepoFileEvent{ + Success: make(map[string]bool), + Error: make(map[string]error), + TmpFile: make(map[string]string), + } + + err := rfe.SetSuccess("config.yml", nil) + c.Assert(err, IsNil) + c.Assert(rfe.Success["config.yml"], Equals, true) + c.Assert(rfe.Error["config.yml"], IsNil) +} + +func (s *ConfigTestSuite) TestRepoFileEventSetFailure(c *C) { + rfe := &RepoFileEvent{ + Success: make(map[string]bool), + Error: make(map[string]error), + TmpFile: make(map[string]string), + } + + testErr := NewReloaderErrorForTest("test error") + err := rfe.SetFailure("config.yml", testErr) + c.Assert(err, IsNil) + c.Assert(rfe.Success["config.yml"], Equals, false) + c.Assert(rfe.Error["config.yml"], Equals, testErr) +} + +func (s *ConfigTestSuite) TestRepoFileEventSetTmpFile(c *C) { + rfe := &RepoFileEvent{ + Success: make(map[string]bool), + Error: make(map[string]error), + TmpFile: make(map[string]string), + } + + err := rfe.SetTmpFile("config.yml", "/tmp/butler-12345") + c.Assert(err, IsNil) + c.Assert(rfe.TmpFile["config.yml"], Equals, "/tmp/butler-12345") +} + +func (s *ConfigTestSuite) TestConfigSettingsGetAllConfigLocalPaths(c *C) { + // Create a ConfigSettings with managers + cs := &ConfigSettings{ + Managers: map[string]*Manager{ + "prometheus": { + DestPath: "/opt/prometheus", + PrimaryConfigName: "prometheus.yml", + ManagerOpts: map[string]*ManagerOpts{ + "prometheus.repo1": { + AdditionalConfigsFullLocalPaths: []string{ + "/opt/prometheus/alerts/alert1.yml", + "/opt/prometheus/rules/rule1.yml", + }, + }, + "prometheus.repo2": { + AdditionalConfigsFullLocalPaths: []string{ + "/opt/prometheus/alerts/alert2.yml", + }, + }, + }, + }, + }, + } + + paths := cs.GetAllConfigLocalPaths("prometheus") + c.Assert(len(paths), Equals, 4) // 1 primary + 3 additional + c.Assert(paths[0], Equals, "/opt/prometheus/prometheus.yml") +} + +func (s *ConfigTestSuite) TestConfigSettingsGetAllConfigLocalPathsNonExistent(c *C) { + cs := &ConfigSettings{ + Managers: map[string]*Manager{}, + } + + paths := cs.GetAllConfigLocalPaths("nonexistent") + c.Assert(len(paths), Equals, 0) +} + +func (s *ConfigTestSuite) TestConfigSettingsGetAllConfigLocalPathsNilManagers(c *C) { + cs := &ConfigSettings{} + + paths := cs.GetAllConfigLocalPaths("prometheus") + c.Assert(len(paths), Equals, 0) +} + +// Helper function to create a simple error for testing +type testError struct { + msg string +} + +func (e *testError) Error() string { + return e.msg +} + +func NewReloaderErrorForTest(msg string) error { + return &testError{msg: msg} +} diff --git a/internal/config/status_test.go b/internal/config/status_test.go new file mode 100644 index 0000000..6cb99a0 --- /dev/null +++ b/internal/config/status_test.go @@ -0,0 +1,158 @@ +/* +Copyright 2017-2026 Adobe. All rights reserved. +This file is licensed 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 REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ + +package config + +import ( + "os" + + . "gopkg.in/check.v1" +) + +func (s *ConfigTestSuite) TestReadManagerStatusFileNotExist(c *C) { + // Test reading a non-existent file + _, err := ReadManagerStatusFile("/nonexistent/path/status.json") + c.Assert(err, NotNil) +} + +func (s *ConfigTestSuite) TestWriteAndReadManagerStatusFile(c *C) { + // Create a temporary file for testing + tmpFile, err := os.CreateTemp("", "butler-status-test-*.json") + c.Assert(err, IsNil) + tmpFile.Close() + defer os.Remove(tmpFile.Name()) + + // Create a status object + status := Status{ + Manager: map[string]bool{ + "prometheus": true, + "alertmanager": false, + }, + } + + // Write the status file + err = WriteManagerStatusFile(tmpFile.Name(), status) + c.Assert(err, IsNil) + + // Read it back + readStatus, err := ReadManagerStatusFile(tmpFile.Name()) + c.Assert(err, IsNil) + c.Assert(readStatus.Manager["prometheus"], Equals, true) + c.Assert(readStatus.Manager["alertmanager"], Equals, false) +} + +func (s *ConfigTestSuite) TestWriteManagerStatusFileInvalidPath(c *C) { + status := Status{ + Manager: map[string]bool{"test": true}, + } + err := WriteManagerStatusFile("/nonexistent/directory/status.json", status) + c.Assert(err, NotNil) +} + +func (s *ConfigTestSuite) TestReadManagerStatusFileInvalidJSON(c *C) { + // Create a temporary file with invalid JSON + tmpFile, err := os.CreateTemp("", "butler-status-invalid-*.json") + c.Assert(err, IsNil) + defer os.Remove(tmpFile.Name()) + + _, err = tmpFile.Write([]byte("this is not valid json")) + c.Assert(err, IsNil) + tmpFile.Close() + + // Try to read it + _, err = ReadManagerStatusFile(tmpFile.Name()) + c.Assert(err, NotNil) +} + +func (s *ConfigTestSuite) TestGetManagerStatus(c *C) { + // Create a temporary status file + tmpFile, err := os.CreateTemp("", "butler-status-get-*.json") + c.Assert(err, IsNil) + tmpFile.Close() + defer os.Remove(tmpFile.Name()) + + // Write initial status + status := Status{ + Manager: map[string]bool{ + "prometheus": true, + "alertmanager": false, + }, + } + err = WriteManagerStatusFile(tmpFile.Name(), status) + c.Assert(err, IsNil) + + // Test getting existing manager status + result := GetManagerStatus(tmpFile.Name(), "prometheus") + c.Assert(result, Equals, true) + + result = GetManagerStatus(tmpFile.Name(), "alertmanager") + c.Assert(result, Equals, false) + + // Test getting non-existent manager status + result = GetManagerStatus(tmpFile.Name(), "nonexistent") + c.Assert(result, Equals, false) + + // Test with non-existent file + result = GetManagerStatus("/nonexistent/status.json", "prometheus") + c.Assert(result, Equals, false) +} + +func (s *ConfigTestSuite) TestSetManagerStatus(c *C) { + // Create a temporary status file + tmpFile, err := os.CreateTemp("", "butler-status-set-*.json") + c.Assert(err, IsNil) + tmpFile.Close() + defer os.Remove(tmpFile.Name()) + + // Set a manager status (file doesn't exist yet with valid content) + err = SetManagerStatus(tmpFile.Name(), "prometheus", true) + c.Assert(err, IsNil) + + // Verify it was set + result := GetManagerStatus(tmpFile.Name(), "prometheus") + c.Assert(result, Equals, true) + + // Update the status + err = SetManagerStatus(tmpFile.Name(), "prometheus", false) + c.Assert(err, IsNil) + + // Verify it was updated + result = GetManagerStatus(tmpFile.Name(), "prometheus") + c.Assert(result, Equals, false) + + // Add another manager + err = SetManagerStatus(tmpFile.Name(), "alertmanager", true) + c.Assert(err, IsNil) + + // Verify both exist + result = GetManagerStatus(tmpFile.Name(), "prometheus") + c.Assert(result, Equals, false) + result = GetManagerStatus(tmpFile.Name(), "alertmanager") + c.Assert(result, Equals, true) +} + +func (s *ConfigTestSuite) TestSetManagerStatusNewFile(c *C) { + // Create a path for a new file + tmpFile, err := os.CreateTemp("", "butler-status-new-*.json") + c.Assert(err, IsNil) + tmpFile.Close() + os.Remove(tmpFile.Name()) // Remove it so SetManagerStatus creates it + + // Set a manager status on a new file + err = SetManagerStatus(tmpFile.Name(), "newmanager", true) + c.Assert(err, IsNil) + defer os.Remove(tmpFile.Name()) + + // Verify it was created and set + result := GetManagerStatus(tmpFile.Name(), "newmanager") + c.Assert(result, Equals, true) +} diff --git a/internal/methods/methods_test.go b/internal/methods/methods_test.go new file mode 100644 index 0000000..52923f4 --- /dev/null +++ b/internal/methods/methods_test.go @@ -0,0 +1,130 @@ +/* +Copyright 2017-2026 Adobe. All rights reserved. +This file is licensed 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 REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ + +package methods + +import ( + "io" + "strings" + + . "gopkg.in/check.v1" +) + +// Note: Test() function is defined in file_test.go, so we don't redeclare it here + +type MethodsTestSuite struct{} + +var _ = Suite(&MethodsTestSuite{}) + +func (s *MethodsTestSuite) TestResponseGetResponseBody(c *C) { + testBody := io.NopCloser(strings.NewReader("test body")) + resp := Response{body: testBody, statusCode: 200} + + body := resp.GetResponseBody() + c.Assert(body, NotNil) + + // Read the body + data, err := io.ReadAll(body) + c.Assert(err, IsNil) + c.Assert(string(data), Equals, "test body") +} + +func (s *MethodsTestSuite) TestResponseGetResponseStatusCode(c *C) { + resp := Response{statusCode: 404} + c.Assert(resp.GetResponseStatusCode(), Equals, 404) +} + +func (s *MethodsTestSuite) TestNewMethodHTTP(c *C) { + manager := "test-manager" + entry := "test-entry" + method, err := New(&manager, "http", &entry) + // HTTP method requires proper configuration, so it may return an error + // but should not panic + _ = method + _ = err +} + +func (s *MethodsTestSuite) TestNewMethodHTTPS(c *C) { + manager := "test-manager" + entry := "test-entry" + method, err := New(&manager, "https", &entry) + _ = method + _ = err +} + +func (s *MethodsTestSuite) TestNewMethodS3(c *C) { + manager := "test-manager" + entry := "test-entry" + method, err := New(&manager, "s3", &entry) + _ = method + _ = err +} + +func (s *MethodsTestSuite) TestNewMethodFile(c *C) { + manager := "test-manager" + entry := "test-entry" + method, err := New(&manager, "file", &entry) + _ = method + _ = err +} + +func (s *MethodsTestSuite) TestNewMethodBlob(c *C) { + manager := "test-manager" + entry := "test-entry" + method, err := New(&manager, "blob", &entry) + _ = method + _ = err +} + +func (s *MethodsTestSuite) TestNewMethodEtcd(c *C) { + manager := "test-manager" + entry := "test-entry" + method, err := New(&manager, "etcd", &entry) + _ = method + _ = err +} + +func (s *MethodsTestSuite) TestNewMethodDefault(c *C) { + manager := "test-manager" + entry := "test-entry" + method, err := New(&manager, "unknown", &entry) + c.Assert(err, NotNil) + c.Assert(err.Error(), Equals, "Generic method handler is not very useful") + _ = method +} + +func (s *MethodsTestSuite) TestNewMethodCaseInsensitive(c *C) { + manager := "test-manager" + entry := "test-entry" + + // Test uppercase + method1, _ := New(&manager, "HTTP", &entry) + method2, _ := New(&manager, "http", &entry) + + // Both should create the same type of method + _ = method1 + _ = method2 +} + +func (s *MethodsTestSuite) TestNewMethodNilManager(c *C) { + entry := "test-entry" + method, err := New(nil, "http", &entry) + _ = method + _ = err +} + +func (s *MethodsTestSuite) TestNewMethodNilEntry(c *C) { + manager := "test-manager" + method, err := New(&manager, "http", nil) + _ = method + _ = err +} diff --git a/internal/reloaders/http_test.go b/internal/reloaders/http_test.go new file mode 100644 index 0000000..758f984 --- /dev/null +++ b/internal/reloaders/http_test.go @@ -0,0 +1,218 @@ +/* +Copyright 2017-2026 Adobe. All rights reserved. +This file is licensed 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 REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ + +package reloaders + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + + . "gopkg.in/check.v1" +) + +func (s *ReloaderTestSuite) TestNewHTTPReloader(c *C) { + opts := HTTPReloaderOpts{ + Host: "localhost", + Port: "9090", + URI: "/-/reload", + Method: "post", + ContentType: "application/json", + Payload: "{}", + Timeout: "10", + Retries: "3", + RetryWaitMin: "1", + RetryWaitMax: "5", + } + + jsonOpts, err := json.Marshal(opts) + c.Assert(err, IsNil) + + reloader, err := NewHTTPReloader("test-manager", "http", jsonOpts) + c.Assert(err, IsNil) + + httpReloader := reloader.(HTTPReloader) + c.Assert(httpReloader.Method, Equals, "http") + c.Assert(httpReloader.Manager, Equals, "test-manager") + c.Assert(httpReloader.Opts.Host, Equals, "localhost") + c.Assert(httpReloader.Opts.URI, Equals, "/-/reload") +} + +func (s *ReloaderTestSuite) TestNewHTTPReloaderInvalidJSON(c *C) { + _, err := NewHTTPReloader("test-manager", "http", []byte("invalid json")) + c.Assert(err, NotNil) +} + +func (s *ReloaderTestSuite) TestHTTPReloaderGetMethod(c *C) { + reloader := HTTPReloader{Method: "https"} + c.Assert(reloader.GetMethod(), Equals, "https") +} + +func (s *ReloaderTestSuite) TestHTTPReloaderGetOpts(c *C) { + opts := HTTPReloaderOpts{Host: "testhost"} + reloader := HTTPReloader{Opts: opts} + result := reloader.GetOpts().(HTTPReloaderOpts) + c.Assert(result.Host, Equals, "testhost") +} + +func (s *ReloaderTestSuite) TestHTTPReloaderSetOpts(c *C) { + reloader := HTTPReloader{} + newOpts := HTTPReloaderOpts{Host: "newhost"} + result := reloader.SetOpts(newOpts) + c.Assert(result, Equals, true) +} + +func (s *ReloaderTestSuite) TestHTTPReloaderSetCounter(c *C) { + reloader := HTTPReloader{Counter: 0} + result := reloader.SetCounter(5) + httpResult := result.(HTTPReloader) + c.Assert(httpResult.Counter, Equals, 5) +} + +func (s *ReloaderTestSuite) TestHTTPReloaderOptsGetClient(c *C) { + opts := HTTPReloaderOpts{} + c.Assert(opts.GetClient(), IsNil) +} + +func (s *ReloaderTestSuite) TestHTTPReloaderReloadSuccess(c *C) { + // Create a test server that returns 200 + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + })) + defer server.Close() + + opts := HTTPReloaderOpts{ + Host: "127.0.0.1", + Port: fmt.Sprintf("%d", getPortFromURL(server.URL)), + URI: "/", + Method: "post", + ContentType: "application/json", + Payload: "{}", + Timeout: "10", + Retries: "1", + RetryWaitMin: "1", + RetryWaitMax: "2", + } + + jsonOpts, err := json.Marshal(opts) + c.Assert(err, IsNil) + + reloader, err := NewHTTPReloader("test-manager", "http", jsonOpts) + c.Assert(err, IsNil) + + err = reloader.Reload() + c.Assert(err, IsNil) +} + +func (s *ReloaderTestSuite) TestHTTPReloaderReloadBadResponse(c *C) { + // Create a test server that returns 500 + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusInternalServerError) + })) + defer server.Close() + + opts := HTTPReloaderOpts{ + Host: "127.0.0.1", + Port: fmt.Sprintf("%d", getPortFromURL(server.URL)), + URI: "/", + Method: "get", + Timeout: "5", + Retries: "0", + RetryWaitMin: "1", + RetryWaitMax: "2", + } + + jsonOpts, err := json.Marshal(opts) + c.Assert(err, IsNil) + + reloader, err := NewHTTPReloader("test-manager", "http", jsonOpts) + c.Assert(err, IsNil) + + err = reloader.Reload() + c.Assert(err, NotNil) + reloaderErr := err.(*ReloaderError) + c.Assert(reloaderErr.Code, Equals, 500) +} + +func (s *ReloaderTestSuite) TestHTTPReloaderReloadMethods(c *C) { + // Test different HTTP methods + methods := []string{"post", "put", "patch", "get"} + + for _, method := range methods { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + })) + + opts := HTTPReloaderOpts{ + Host: "127.0.0.1", + Port: fmt.Sprintf("%d", getPortFromURL(server.URL)), + URI: "/", + Method: method, + ContentType: "application/json", + Payload: "{}", + Timeout: "5", + Retries: "0", + RetryWaitMin: "1", + RetryWaitMax: "2", + } + + jsonOpts, _ := json.Marshal(opts) + reloader, _ := NewHTTPReloader("test-manager", "http", jsonOpts) + err := reloader.Reload() + c.Assert(err, IsNil, Commentf("Method %s failed", method)) + + server.Close() + } +} + +func (s *ReloaderTestSuite) TestHTTPReloaderRetryPolicy(c *C) { + reloader := HTTPReloader{Manager: "test-manager"} + + // Test with error + shouldRetry, err := reloader.ReloaderRetryPolicy(context.Background(), nil, fmt.Errorf("test error")) + c.Assert(shouldRetry, Equals, true) + c.Assert(err, NotNil) + + // Test with status code 0 + resp := &http.Response{StatusCode: 0} + shouldRetry, err = reloader.ReloaderRetryPolicy(context.Background(), resp, nil) + c.Assert(shouldRetry, Equals, true) + c.Assert(err, IsNil) + + // Test with status code >= 600 + resp = &http.Response{StatusCode: 600} + shouldRetry, err = reloader.ReloaderRetryPolicy(context.Background(), resp, nil) + c.Assert(shouldRetry, Equals, true) + c.Assert(err, IsNil) + + // Test with normal status code (should not retry) + resp = &http.Response{StatusCode: 200} + shouldRetry, err = reloader.ReloaderRetryPolicy(context.Background(), resp, nil) + c.Assert(shouldRetry, Equals, false) + c.Assert(err, IsNil) + + // Test with 500 status code (should not retry by our policy) + resp = &http.Response{StatusCode: 500} + shouldRetry, err = reloader.ReloaderRetryPolicy(context.Background(), resp, nil) + c.Assert(shouldRetry, Equals, false) + c.Assert(err, IsNil) +} + +// Helper function to extract port from URL +func getPortFromURL(urlStr string) int { + // Parse URL like "http://127.0.0.1:12345" + var port int + fmt.Sscanf(urlStr, "http://127.0.0.1:%d", &port) + return port +} diff --git a/internal/reloaders/reloaders_test.go b/internal/reloaders/reloaders_test.go new file mode 100644 index 0000000..9b0c3d4 --- /dev/null +++ b/internal/reloaders/reloaders_test.go @@ -0,0 +1,90 @@ +/* +Copyright 2017-2026 Adobe. All rights reserved. +This file is licensed 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 REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ + +package reloaders + +import ( + "testing" + + . "gopkg.in/check.v1" +) + +func Test(t *testing.T) { TestingT(t) } + +type ReloaderTestSuite struct{} + +var _ = Suite(&ReloaderTestSuite{}) + +func (s *ReloaderTestSuite) TestNewReloaderError(c *C) { + err := NewReloaderError() + c.Assert(err, NotNil) + c.Assert(err.Code, Equals, 0) + c.Assert(err.Message, Equals, "") +} + +func (s *ReloaderTestSuite) TestReloaderErrorWithCode(c *C) { + err := NewReloaderError().WithCode(500) + c.Assert(err.Code, Equals, 500) +} + +func (s *ReloaderTestSuite) TestReloaderErrorWithMessage(c *C) { + err := NewReloaderError().WithMessage("test error message") + c.Assert(err.Message, Equals, "test error message") +} + +func (s *ReloaderTestSuite) TestReloaderErrorChaining(c *C) { + err := NewReloaderError().WithCode(404).WithMessage("not found") + c.Assert(err.Code, Equals, 404) + c.Assert(err.Message, Equals, "not found") +} + +func (s *ReloaderTestSuite) TestReloaderErrorErrorMethod(c *C) { + err := NewReloaderError().WithCode(500).WithMessage("internal server error") + errStr := err.Error() + c.Assert(errStr, Equals, "internal server error. code=500") +} + +func (s *ReloaderTestSuite) TestGenericReloader(c *C) { + reloader, err := NewGenericReloader("test-manager", "generic", []byte("test")) + c.Assert(err, NotNil) + c.Assert(err.Error(), Equals, "Generic reloader is not very useful") + + // Test the interface methods + c.Assert(reloader.GetMethod(), Equals, "none") + c.Assert(reloader.Reload(), IsNil) +} + +func (s *ReloaderTestSuite) TestGenericReloaderWithCustomError(c *C) { + customErr := NewReloaderError().WithCode(1).WithMessage("custom error") + reloader, err := NewGenericReloaderWithCustomError("test-manager", "generic", customErr) + c.Assert(err, Equals, customErr) + c.Assert(reloader.GetMethod(), Equals, "none") +} + +func (s *ReloaderTestSuite) TestGenericReloaderSetCounter(c *C) { + reloader, _ := NewGenericReloader("test-manager", "generic", []byte("test")) + result := reloader.SetCounter(5) + // GenericReloader.SetCounter returns the same reloader (no-op) + c.Assert(result.GetMethod(), Equals, "none") +} + +func (s *ReloaderTestSuite) TestGenericReloaderGetOpts(c *C) { + reloader, _ := NewGenericReloader("test-manager", "generic", []byte("test")) + opts := reloader.GetOpts() + c.Assert(opts, NotNil) +} + +func (s *ReloaderTestSuite) TestGenericReloaderSetOpts(c *C) { + reloader := GenericReloader{} + result := reloader.SetOpts(GenericReloaderOpts{}) + c.Assert(result, Equals, true) +}