diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d008c55..8c5556f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,28 +2,33 @@ name: CI on: push: - tags: [ v* ] - branches: [ main ] + tags: [v*] + branches: [main] pull_request: - branches: [ main ] + branches: [main] -jobs: +env: + # Default registry - can be overridden by repository variables + REGISTRY: ${{ vars.REGISTRY || 'ghcr.io' }} + # Default image name - can be overridden by repository variables + IMAGE_NAME: ${{ vars.IMAGE_NAME || github.repository }} +jobs: tests: name: tests runs-on: ubuntu-latest steps: - - name: Checkout - uses: actions/checkout@v4 + - name: Checkout + uses: actions/checkout@v4 - - name: Set up Go 1.x - uses: actions/setup-go@v5 - with: - go-version-file: 'go.mod' - - - name: Run test.sh - run: | - scripts/test.sh + - name: Set up Go 1.x + uses: actions/setup-go@v5 + with: + go-version-file: "go.mod" + + - name: Run test.sh + run: | + scripts/test.sh build: name: Build @@ -33,46 +38,51 @@ jobs: contents: write pull-requests: read checks: write - needs: [ tests ] + needs: [tests] steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Set up Go 1.x - uses: actions/setup-go@v5 - with: - go-version-file: 'go.mod' + - name: Checkout + uses: actions/checkout@v4 - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 + - name: Set up Go 1.x + uses: actions/setup-go@v5 + with: + go-version-file: "go.mod" - - name: Docker Login - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 - - name: golangci-lint - uses: golangci/golangci-lint-action@v6 + - name: Docker Login + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} - - name: Prepare a snapshot release - if: ${{ !startsWith(github.ref, 'refs/tags/v') }} - uses: goreleaser/goreleaser-action@v3 - with: - version: latest - args: release --clean --snapshot - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - DOCKER_CLI_EXPERIMENTAL: "enabled" + - name: golangci-lint + uses: golangci/golangci-lint-action@v6 - - name: Release a new version - if: ${{ startsWith(github.ref, 'refs/tags/v') }} - uses: goreleaser/goreleaser-action@v4 - with: - version: latest - args: release --clean - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - DOCKER_CLI_EXPERIMENTAL: "enabled" + - name: Prepare a snapshot release + if: ${{ !startsWith(github.ref, 'refs/tags/v') }} + uses: goreleaser/goreleaser-action@v3 + with: + version: latest + args: release --clean --snapshot + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + DOCKER_CLI_EXPERIMENTAL: "enabled" + REGISTRY: ${{ env.REGISTRY }} + IMAGE_NAME: ${{ env.IMAGE_NAME }} + GITHUB_REPOSITORY_OWNER: ${{ github.repository_owner }} + - name: Release a new version + if: ${{ startsWith(github.ref, 'refs/tags/v') }} + uses: goreleaser/goreleaser-action@v4 + with: + version: latest + args: release --clean + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + DOCKER_CLI_EXPERIMENTAL: "enabled" + REGISTRY: ${{ env.REGISTRY }} + IMAGE_NAME: ${{ env.IMAGE_NAME }} + GITHUB_REPOSITORY_OWNER: ${{ github.repository_owner }} diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 2394bc8..fcf48fd 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -31,7 +31,7 @@ changelog: use: github dockers: -- image_templates: ["ghcr.io/fivexl/{{ .ProjectName }}:{{ .Version }}-amd64"] +- image_templates: ["{{ .Env.REGISTRY }}/{{ .Env.IMAGE_NAME }}:{{ .Version }}-amd64"] goos: linux goarch: amd64 dockerfile: Dockerfile @@ -40,13 +40,13 @@ dockers: - --platform=linux/amd64 - --label=org.opencontainers.image.title={{ .ProjectName }} - --label=org.opencontainers.image.description={{ .ProjectName }} - - --label=org.opencontainers.image.url=https://github.com/fivexl/{{ .ProjectName }} - - --label=org.opencontainers.image.source=https://github.com/fivexl/{{ .ProjectName }} + - --label=org.opencontainers.image.url=https://github.com/{{ .Env.GITHUB_REPOSITORY_OWNER }}/{{ .ProjectName }} + - --label=org.opencontainers.image.source=https://github.com/{{ .Env.GITHUB_REPOSITORY_OWNER }}/{{ .ProjectName }} - --label=org.opencontainers.image.version={{ .Version }} - --label=org.opencontainers.image.created={{ time "2006-01-02T15:04:05Z07:00" }} - --label=org.opencontainers.image.revision={{ .FullCommit }} - --label=org.opencontainers.image.licenses=Apache-2.0 -- image_templates: ["ghcr.io/fivexl/{{ .ProjectName }}:{{ .Version }}-arm64v8"] +- image_templates: ["{{ .Env.REGISTRY }}/{{ .Env.IMAGE_NAME }}:{{ .Version }}-arm64v8"] goos: linux goarch: arm64 dockerfile: Dockerfile @@ -55,18 +55,18 @@ dockers: - --platform=linux/arm64/v8 - --label=org.opencontainers.image.title={{ .ProjectName }} - --label=org.opencontainers.image.description={{ .ProjectName }} - - --label=org.opencontainers.image.url=https://github.com/fivexl/{{ .ProjectName }} - - --label=org.opencontainers.image.source=https://github.com/fivexl/{{ .ProjectName }} + - --label=org.opencontainers.image.url=https://github.com/{{ .Env.GITHUB_REPOSITORY_OWNER }}/{{ .ProjectName }} + - --label=org.opencontainers.image.source=https://github.com/{{ .Env.GITHUB_REPOSITORY_OWNER }}/{{ .ProjectName }} - --label=org.opencontainers.image.version={{ .Version }} - --label=org.opencontainers.image.created={{ time "2006-01-02T15:04:05Z07:00" }} - --label=org.opencontainers.image.revision={{ .FullCommit }} - --label=org.opencontainers.image.licenses=Apache-2.0 docker_manifests: -- name_template: ghcr.io/fivexl/{{ .ProjectName }}:{{ .Version }} +- name_template: "{{ .Env.REGISTRY }}/{{ .Env.IMAGE_NAME }}:{{ .Version }}" image_templates: - - ghcr.io/fivexl/{{ .ProjectName }}:{{ .Version }}-amd64 - - ghcr.io/fivexl/{{ .ProjectName }}:{{ .Version }}-arm64v8 -- name_template: ghcr.io/fivexl/{{ .ProjectName }}:latest + - "{{ .Env.REGISTRY }}/{{ .Env.IMAGE_NAME }}:{{ .Version }}-amd64" + - "{{ .Env.REGISTRY }}/{{ .Env.IMAGE_NAME }}:{{ .Version }}-arm64v8" +- name_template: "{{ .Env.REGISTRY }}/{{ .Env.IMAGE_NAME }}:latest" image_templates: - - ghcr.io/fivexl/{{ .ProjectName }}:{{ .Version }}-amd64 - - ghcr.io/fivexl/{{ .ProjectName }}:{{ .Version }}-arm64v8 + - "{{ .Env.REGISTRY }}/{{ .Env.IMAGE_NAME }}:{{ .Version }}-amd64" + - "{{ .Env.REGISTRY }}/{{ .Env.IMAGE_NAME }}:{{ .Version }}-arm64v8" diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..de23726 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,174 @@ +# Contributing to LProbe + +Thank you for your interest in contributing to LProbe! This document provides guidelines for contributing to the project, including how to publish Docker images to your own GitHub Container Registry. + +## Publishing Custom Docker Images + +This project has been configured to allow contributors to easily publish Docker images to their own GitHub Container Registry (GHCR) space. This is useful when you want to: + +- Test your changes in your own environment +- Maintain a fork with custom modifications +- Publish images for your own use or organization + +### Quick Setup + +To publish Docker images to your own GHCR space, follow these steps: + +#### 1. Fork the Repository + +First, fork the repository to your GitHub account if you haven't already. + +#### 2. Configure Repository Variables + +In your forked repository, set up the following repository variables: + +1. Go to your repository on GitHub +2. Navigate to **Settings** → **Secrets and variables** → **Actions** → **Variables** +3. Click **New repository variable** +4. Add the following variables: + +| Variable | Value | Description | +|----------|-------|-------------| +| `REGISTRY` | `ghcr.io` | The container registry to use (GHCR is recommended) | +| `IMAGE_NAME` | `yourusername/lprobe` | Your custom image name (replace `yourusername` with your GitHub username) | + +**Example:** +- `REGISTRY`: `ghcr.io` +- `IMAGE_NAME`: `JohnPreston/lprobe` + +#### 3. Trigger a Build + +Once the variables are set, you can trigger a Docker image build by: + +- **Creating a release tag**: Push a tag matching `v*` (e.g., `v1.0.0`, `v2.1.3`) to trigger a full release +- **Pushing to main branch**: This will create snapshot builds for testing + +### Example Workflow + +After setting up the variables, when you push a tag like `v1.0.0` to your fork: + +1. The GitHub Actions workflow will automatically start +2. Docker images will be built for both `amd64` and `arm64` architectures +3. Images will be pushed to: `ghcr.io/yourusername/lprobe:v1.0.0` and `ghcr.io/yourusername/lprobe:latest` +4. Multi-architecture manifests will be created to support both platforms + +### Using Your Custom Images + +Once your images are built, you can pull and use them: + +```bash +# Pull your custom image +docker pull ghcr.io/yourusername/lprobe:latest + +# Use it in your deployments +docker run --rm ghcr.io/yourusername/lprobe:latest -url http://localhost:8080/health +``` + +### Alternative Registries + +While GHCR is the default, you can configure the project to publish to any Docker-compatible registry: + +#### Docker Hub + +Set your repository variables to: +- `REGISTRY`: `docker.io` +- `IMAGE_NAME`: `yourusername/lprobe` + +#### Google Container Registry (GCR) + +Set your repository variables to: +- `REGISTRY`: `gcr.io` +- `IMAGE_NAME`: `your-project/lprobe` + +#### Private Registries + +For private registries, you'll also need to configure authentication: + +1. Add your registry credentials as repository **secrets** (not variables): + - `DOCKER_USERNAME`: Your registry username + - `DOCKER_PASSWORD`: Your registry password/token + +2. Update the workflow to use these secrets for authentication. + +### Testing Changes Locally + +Before publishing images, you can test your changes locally: + +```bash +# Build the binary +go build -o lprobe *.go + +# Test the binary +./lprobe -url http://localhost:8080/health + +# Build Docker image locally +docker build -t lprobe:test . + +# Test the Docker image +docker run --rm lprobe:test -url http://host.docker.internal:8080/health +``` + +### Submitting Changes Upstream + +When you're ready to contribute your changes back to the upstream repository: + +1. Ensure your changes follow the project's coding standards +2. Add tests if applicable +3. Update documentation as needed +4. Submit a pull request with a clear description of your changes + +The configurable registry setup ensures that: +- The upstream repository continues to work with its original settings +- Your changes don't break existing functionality +- Contributors can easily test and publish their own versions + +### Troubleshooting + +#### Build Failures + +If your builds fail, check: +1. Repository variables are correctly set +2. Your fork has the necessary permissions +3. The GoReleaser configuration is valid + +#### Permission Issues + +Ensure your GitHub Actions have the necessary permissions: +1. Go to **Settings** → **Actions** → **General** +2. Under **Workflow permissions**, select **Read and write permissions** + +#### Image Pull Issues + +If you can't pull your images: +1. Check that the images were successfully built and pushed +2. Verify your repository is public (or you have access to private repositories) +3. Ensure you're using the correct image name and tag + +## General Contribution Guidelines + +### Code Style + +- Follow the existing code style and conventions +- Add comments for complex logic +- Ensure your code is properly tested + +### Testing + +- Run the test suite before submitting changes: `./scripts/test.sh` +- Add new tests for new functionality +- Ensure all tests pass + +### Documentation + +- Update relevant documentation when making changes +- Add clear comments for complex features +- Keep README and other docs up to date + +### Pull Requests + +- Create descriptive pull requests +- Link to relevant issues +- Provide clear descriptions of changes +- Be responsive to review comments + +Thank you for contributing to LProbe! 🚀 \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index f3d5d53..f5d73a3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,2 +1,2 @@ FROM scratch -COPY lprobe /lprobe \ No newline at end of file +COPY lprobe /lprobe diff --git a/examples/Dockerfile b/examples/Dockerfile index 262d617..ab88ef5 100644 --- a/examples/Dockerfile +++ b/examples/Dockerfile @@ -2,10 +2,10 @@ FROM nginx:alpine ARG LPROBE_VERSION=v0.0.6 ARG TARGETPLATFORM RUN case ${TARGETPLATFORM} in \ - "linux/amd64") LPROBE_ARCH=amd64 ;; \ - "linux/arm64") LPROBE_ARCH=arm64 ;; \ + "linux/amd64") LPROBE_ARCH=amd64 ;; \ + "linux/arm64") LPROBE_ARCH=arm64 ;; \ esac \ - && wget -qO/bin/lprobe https://github.com/fivexl/lprobe/releases/download/${LPROBE_VERSION}/lprobe-linux-${LPROBE_ARCH} \ - && chmod +x /bin/lprobe \ - && rm -f /usr/bin/wget + && wget -qO/bin/lprobe https://github.com/fivexl/lprobe/releases/download/${LPROBE_VERSION}/lprobe-linux-${LPROBE_ARCH} \ + && chmod +x /bin/lprobe \ + && rm -f /usr/bin/wget HEALTHCHECK --interval=15s --timeout=5s --start-period=5s --retries=3 CMD [ "lprobe", "-mode=http", "-port=80", "-endpoint=/" ] diff --git a/go.mod b/go.mod index 566e1de..4b3f131 100644 --- a/go.mod +++ b/go.mod @@ -1,23 +1,23 @@ module lprobe -go 1.24 +go 1.24.0 -toolchain go1.24.1 +toolchain go1.24.9 require ( - github.com/spiffe/go-spiffe/v2 v2.5.0 + github.com/spiffe/go-spiffe/v2 v2.6.0 golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 - google.golang.org/grpc v1.72.0 + google.golang.org/grpc v1.76.0 ) require ( github.com/Microsoft/go-winio v0.6.2 // indirect - github.com/go-jose/go-jose/v4 v4.1.0 // indirect + github.com/go-jose/go-jose/v4 v4.1.3 // indirect github.com/zeebo/errs v1.4.0 // indirect - golang.org/x/net v0.39.0 // indirect - golang.org/x/sync v0.13.0 // indirect - golang.org/x/sys v0.32.0 // indirect - golang.org/x/text v0.24.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20250422160041-2d3770c4ea7f // indirect - google.golang.org/protobuf v1.36.6 // indirect + golang.org/x/net v0.46.0 // indirect + golang.org/x/sync v0.17.0 // indirect + golang.org/x/sys v0.37.0 // indirect + golang.org/x/text v0.30.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251103181224-f26f9409b101 // indirect + google.golang.org/protobuf v1.36.10 // indirect ) diff --git a/go.sum b/go.sum index 35bdcf4..12fb003 100644 --- a/go.sum +++ b/go.sum @@ -4,8 +4,11 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/go-jose/go-jose/v4 v4.1.0 h1:cYSYxd3pw5zd2FSXk2vGdn9igQU2PS8MuxrCOCl0FdY= github.com/go-jose/go-jose/v4 v4.1.0/go.mod h1:GG/vqmYm3Von2nYiB2vGTXzdoNKE5tix5tuc6iAd+sw= +github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs= +github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= @@ -18,37 +21,66 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/spiffe/go-spiffe/v2 v2.5.0 h1:N2I01KCUkv1FAjZXJMwh95KK1ZIQLYbPfhaxw8WS0hE= github.com/spiffe/go-spiffe/v2 v2.5.0/go.mod h1:P+NxobPc6wXhVtINNtFjNWGBTreew1GBUCwT2wPmb7g= +github.com/spiffe/go-spiffe/v2 v2.6.0 h1:l+DolpxNWYgruGQVV0xsfeya3CsC7m8iBzDnMpsbLuo= +github.com/spiffe/go-spiffe/v2 v2.6.0/go.mod h1:gm2SeUoMZEtpnzPNs2Csc0D/gX33k1xIx7lEzqblHEs= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/zeebo/errs v1.4.0 h1:XNdoD/RRMKP7HD0UhJnIzUy74ISdGGxURlYG8HSWSfM= github.com/zeebo/errs v1.4.0/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtCw4= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY= go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI= +go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ= +go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ= go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE= +go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE= +go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A= go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU= +go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI= +go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E= go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk= go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w= +go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc= +go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM= go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k= go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE= +go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4= +go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 h1:R84qjqJb5nVJMxqWYb3np9L5ZsaDtB+a39EqjV0JSUM= golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:S9Xr4PYopiDyqSyp5NjCrhFrqg6A5zA2E/iPHPhqnS8= golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= +golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4= +golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610= golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= +golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= +golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= +golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= +golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= google.golang.org/genproto/googleapis/rpc v0.0.0-20250422160041-2d3770c4ea7f h1:N/PrbTw4kdkqNRzVfWPrBekzLuarFREcbFOiOLkXon4= google.golang.org/genproto/googleapis/rpc v0.0.0-20250422160041-2d3770c4ea7f/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251103181224-f26f9409b101 h1:tRPGkdGHuewF4UisLzzHHr1spKw92qLM98nIzxbC0wY= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251103181224-f26f9409b101/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= google.golang.org/grpc v1.72.0 h1:S7UkcVa60b5AAQTaO6ZKamFp1zMZSU0fGDK2WZLbBnM= google.golang.org/grpc v1.72.0/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM= +google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A= +google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c= +google.golang.org/grpc v1.78.0-dev h1:UzAT8HvkQqIq/Vz9yfqEyqY5D7el592lPn36ed6A1O0= +google.golang.org/grpc v1.78.0-dev/go.mod h1:NB6dcQMVFQaMB1udD7SAsZ1WWFITVyFdZ0/jw+12ERQ= google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= +google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= +google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/init.go b/init.go index 8a13405..6c777ca 100644 --- a/init.go +++ b/init.go @@ -18,6 +18,8 @@ import ( "flag" "fmt" "log" + "net" + "net/url" "os" "strings" "time" @@ -48,6 +50,8 @@ var ( flVerbose bool flGZIP bool flSPIFFE bool + flURL string + flHost string ) const ( @@ -55,6 +59,12 @@ const ( LocalAddress = "127.0.0.1" // LocalAddress6 IPv6 to call LocalAddress6 = "[::1]" + // AWS metadata IPv4 address + AWSMetadataIPv4 = "169.254.169.254" + // AWS EKS metadata IPv4 address + AWSEKSIPv4 = "169.254.170.2" + // AWS metadata IPv6 address + AWSMetadataIPv6 = "fd00:ec2::254" // StatusInvalidArguments indicates specified invalid arguments. StatusInvalidArguments = 1 // StatusConnectionFailure indicates connection failed. @@ -71,7 +81,60 @@ func getSupportedModes() []string { return []string{"http", "grpc"} } +func validateHostname(hostname string) error { + if hostname == "" { + return nil + } + + // Check if it's already an IP address + if ip := net.ParseIP(hostname); ip != nil { + return validateIPAddress(ip) + } + + // Resolve hostname to IP addresses + ips, err := net.LookupIP(hostname) + if err != nil { + return fmt.Errorf("failed to resolve hostname %q: %v", hostname, err) + } + + // Check if any resolved IP is allowed + for _, ip := range ips { + if err := validateIPAddress(ip); err != nil { + return fmt.Errorf("hostname %q resolves to disallowed IP %s: %v", hostname, ip, err) + } + } + + return nil +} + +func validateIPAddress(ip net.IP) error { + // Check IPv4 addresses + if ip.To4() != nil { + if ip.String() == LocalAddress || ip.String() == AWSMetadataIPv4 || ip.String() == AWSEKSIPv4 { + return nil + } + return fmt.Errorf("IPv4 address %s is not allowed (only localhost and AWS metadata endpoints permitted)", ip) + } + + // Check IPv6 addresses + if ip.To16() != nil { + // Remove zone index if present (e.g., ::1%eth0) + ipWithoutZone := net.IP(ip.To16()) + if ipWithoutZone.String() == "::1" || ipWithoutZone.String() == AWSMetadataIPv6 { + return nil + } + return fmt.Errorf("IPv6 address %s is not allowed (only localhost and AWS metadata endpoints permitted)", ip) + } + + return fmt.Errorf("invalid IP address format: %s", ip) +} + func getAddr() string { + // If URL is provided, use hostname from URL (already validated) + if flURL != "" && flHost != "" { + return flHost + } + // Otherwise use localhost if flIPv6 { return LocalAddress6 } @@ -107,6 +170,7 @@ func init() { flagSet.BoolVar(&flVerbose, "v", false, "verbose logs") flagSet.BoolVar(&flGZIP, "gzip", false, "use GZIPCompressor for requests and GZIPDecompressor for response (default: false)") flagSet.BoolVar(&flSPIFFE, "spiffe", false, "use SPIFFE to obtain mTLS credentials") + flagSet.StringVar(&flURL, "url", "", "Complete URL to check (e.g., http://example.com:8080/path)") err := flagSet.Parse(os.Args[1:]) if err != nil { @@ -118,6 +182,49 @@ func init() { os.Exit(StatusInvalidArguments) } + // Parse URL if provided + if flURL != "" { + parsedURL, err := url.Parse(flURL) + if err != nil { + argError("invalid URL format: %v", err) + } + + // Set mode based on scheme + if parsedURL.Scheme == "https" { + flTLS = true + } else if parsedURL.Scheme != "http" { + argError("unsupported URL scheme: %s (only http and https are supported)", parsedURL.Scheme) + } + + // Override port if specified in URL + if parsedURL.Port() != "" { + fmt.Sscanf(parsedURL.Port(), "%d", &flPort) + } else if parsedURL.Scheme == "https" { + flPort = 443 + } else if parsedURL.Scheme == "http" { + flPort = 80 + } + + // Override endpoint if path is specified + if parsedURL.Path != "" { + flEndpoint = parsedURL.Path + } + + // Set hostname from URL + if parsedURL.Hostname() != "" { + hostname := parsedURL.Hostname() + if err := validateHostname(hostname); err != nil { + argError("hostname validation failed: %v", err) + } + flHost = hostname + } + + // Set server name for TLS verification if hostname is available + if flTLS && parsedURL.Hostname() != "" { + flTLSServerName = parsedURL.Hostname() + } + } + if !slices.Contains(getSupportedModes(), flMode) { argError("Unsupported -mode. Please use one of %v", getSupportedModes()) } @@ -189,5 +296,4 @@ func (s *rpcHeaders) Set(value string) error { trimmed := strings.TrimLeftFunc(parts[1], unicode.IsSpace) s.Append(parts[0], trimmed) return nil -} - +} \ No newline at end of file diff --git a/scripts/test.sh b/scripts/test.sh index adab9f9..e3e24be 100755 --- a/scripts/test.sh +++ b/scripts/test.sh @@ -63,6 +63,30 @@ if [ "$?" != 1 ]; then exit 1 fi +### URL HTTP Check Test +./lprobe -url http://127.0.0.1:8080/ -v +if [ "$?" != 0 ]; then + echo "URL HTTP test failed" + docker stop nginx-lprobe-test + exit 1 +fi + +### URL HTTP with custom path Check Test +./lprobe -url http://127.0.0.1:8080/ -v +if [ "$?" != 0 ]; then + echo "URL HTTP path test failed" + docker stop nginx-lprobe-test + exit 1 +fi + +### FAIL URL HTTP Check Test +./lprobe -url http://127.0.0.1:7777/ -v +if [ "$?" != 1 ]; then + echo "FAIL URL HTTP test failed" + docker stop nginx-lprobe-test + exit 1 +fi + ## Stop docker containers docker stop nginx-lprobe-test docker stop grpc-lprobe-test