From 8f4b05c9ebdfa5e03d6c189c6fdc3f8c86780460 Mon Sep 17 00:00:00 2001 From: lanycrost Date: Tue, 14 Apr 2026 17:02:45 +0400 Subject: [PATCH 1/2] feat: align http request engram with release standards Release-As: 0.1.0 --- .github/.release-please-manifest.json | 3 + .github/ISSUE_TEMPLATE/bug_report.md | 45 + .github/ISSUE_TEMPLATE/config.yml | 8 + .github/ISSUE_TEMPLATE/docs_request.md | 21 + .github/ISSUE_TEMPLATE/feature_request.md | 33 + .github/PULL_REQUEST_TEMPLATE.md | 33 + .github/dependabot.yml | 51 ++ .github/labels.yml | 84 ++ .github/release-please-config.json | 59 ++ .github/workflows/ci.yaml | 37 + .github/workflows/docker.yml | 85 ++ .github/workflows/label-sync.yml | 26 + .github/workflows/release-please.yml | 138 +++ .gitignore | 30 + .golangci.yml | 54 ++ CHANGELOG.md | 12 + CODE_OF_CONDUCT.md | 94 +++ CONTRIBUTING.md | 63 ++ Dockerfile | 23 +- Engram.yaml | 45 +- LICENSE | 202 +++++ Makefile | 136 +++ README.md | 272 +++--- SECURITY.md | 34 + SUPPORT.md | 36 + conformance_test.go | 29 + go.mod | 107 ++- go.sum | 285 ++++--- main.go | 13 +- pkg/config/config.go | 68 +- pkg/engram/debug.go | 62 ++ pkg/engram/engram.go | 971 +++++++++++++++------- pkg/engram/engram_test.go | 313 +++++++ 33 files changed, 2834 insertions(+), 638 deletions(-) create mode 100644 .github/.release-please-manifest.json create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 .github/ISSUE_TEMPLATE/docs_request.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md create mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 .github/dependabot.yml create mode 100644 .github/labels.yml create mode 100644 .github/release-please-config.json create mode 100644 .github/workflows/ci.yaml create mode 100644 .github/workflows/docker.yml create mode 100644 .github/workflows/label-sync.yml create mode 100644 .github/workflows/release-please.yml create mode 100644 .gitignore create mode 100644 .golangci.yml create mode 100644 CHANGELOG.md create mode 100644 CODE_OF_CONDUCT.md create mode 100644 CONTRIBUTING.md create mode 100644 LICENSE create mode 100644 Makefile create mode 100644 SECURITY.md create mode 100644 SUPPORT.md create mode 100644 conformance_test.go create mode 100644 pkg/engram/debug.go create mode 100644 pkg/engram/engram_test.go diff --git a/.github/.release-please-manifest.json b/.github/.release-please-manifest.json new file mode 100644 index 0000000..e18ee07 --- /dev/null +++ b/.github/.release-please-manifest.json @@ -0,0 +1,3 @@ +{ + ".": "0.0.0" +} diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..344b3c8 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,45 @@ +--- +name: "Bug report" +about: "Report a reproducible issue in an operator, transport, SDK, Engram, or Impulse" +labels: ["kind/bug"] +--- + +## Component(s) +- [ ] bobrapet (Story/StoryRun controllers) +- [ ] bobravoz-grpc (transport operator) +- [ ] bubu-sdk-go +- [ ] Engram (name it below) +- [ ] Impulse (name it below) +- [ ] Docs / website + +If Engram/Impulse: +``` +name: +version/tag: +execution mode (job / deployment / impulse): +``` + +## What happened? +Tell us what broke. Include the Story/StoryRun status, the expected behaviour, and what you observed instead. + +## Minimal reproduction +1. Inputs/Story snippet (YAML or JSON) +2. Commands you ran (`kubectl`, `make`, etc.) +3. Cluster details (Kubernetes version, Kind/Minikube/managed cluster) + +``` +apiVersion: stories.bubustack.io/v1alpha1 +kind: Story +metadata: + name: example +spec: + ... +``` + +## Logs & traces +- `kubectl logs` for controllers or Engrams (set `BUBU_DEBUG=true` if possible) +- Relevant excerpts from `storyrun` / `steprun` status +- TransportBinding / bobravoz logs if streaming is impacted + +## Additional context +Anything else we should know? For example, custom overrides, secrets/providers, or recent upgrades. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..7834681 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,8 @@ +blank_issues_enabled: false +contact_links: + - name: Security reports + url: https://github.com/bubustack/http-request-engram/security/advisories/new + about: Please use our private reporting channel for vulnerabilities. + - name: Architecture & roadmap discussions + url: https://github.com/orgs/bubustack/discussions + about: For proposals that span multiple repositories, start a discussion before opening issues. diff --git a/.github/ISSUE_TEMPLATE/docs_request.md b/.github/ISSUE_TEMPLATE/docs_request.md new file mode 100644 index 0000000..f28d07a --- /dev/null +++ b/.github/ISSUE_TEMPLATE/docs_request.md @@ -0,0 +1,21 @@ +--- +name: "Docs / community update" +about: "Request a documentation, support, or example update" +labels: ["kind/docs"] +--- + +## Area +- [ ] Operator docs / architecture +- [ ] Engram or Impulse README (name it) +- [ ] SDK reference (Go examples) +- [ ] Website / tutorials / blog +- [ ] Community health file (Code of Conduct, Security, Support, etc.) + +## What needs to change? +Link to the existing page/file and tell us what is missing or incorrect. + +## Source of truth +Add the CRD, code snippet, or log output that proves the correct behaviour so we can update the doc with confidence. + +## Suggested fix (optional) +Share wording, diagrams, or commands that would resolve the issue. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..598e647 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,33 @@ +--- +name: "Feature request" +about: "Pitch a new capability for the operator, transport, SDK, Engram, or docs" +labels: ["kind/feature"] +--- + +## Problem statement +What workflow or operational gap are you trying to solve? Include scale, latency, tenancy, or compliance constraints if relevant. + +## Proposed change +Describe the behaviour you’d like to see. If this affects CRDs, Engram templates, or SDK APIs, list the new fields and defaults. + +``` +apiVersion: catalog.bubustack.io/v1alpha1 +kind: EngramTemplate +spec: + with: + newField: ... +``` + +## Affected component(s) +- [ ] bobrapet +- [ ] bobravoz-grpc +- [ ] bubu-sdk-go +- [ ] Engram (name it) +- [ ] Impulse (name it) +- [ ] Docs / website + +## Alternatives considered +What did you try already? Examples: custom Engram, CEL policy, external controller, different transport, etc. + +## Additional context +Links, design docs, screenshots, or related issues/discussions. diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..8556ab4 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,33 @@ +## Summary + + +## Type of change +- [ ] Bug fix +- [ ] New feature +- [ ] Breaking change +- [ ] Documentation / examples +- [ ] Refactor / chore + +## Related issues + + +## How was this tested? +- [ ] Unit tests +- [ ] Integration tests +- [ ] E2E / Kind tests +- [ ] Manual verification +Commands / notes: +```bash +# paste commands you ran (or explain why not) +``` + +## Docs / CRDs impact +- [ ] Docs updated (README/Guides/Support) +- [ ] CRD or Engram/Impulse schema changes +- [ ] Not applicable + +## Checklist +- [ ] Lint/tests ran locally +- [ ] Updated Engram.yaml/CRDs/manifests when behaviour changed +- [ ] Added changelog/release note if required +- [ ] No secrets or sensitive data committed diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..749159c --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,51 @@ +version: 2 +updates: + # Go module dependencies + - package-ecosystem: "gomod" + directory: "/" + schedule: + interval: "weekly" + day: "monday" + open-pull-requests-limit: 10 + groups: + kubernetes: + patterns: + - "k8s.io/*" + - "sigs.k8s.io/*" + opentelemetry: + patterns: + - "go.opentelemetry.io/*" + grpc: + patterns: + - "google.golang.org/grpc" + - "google.golang.org/protobuf" + bobrapet: + patterns: + - "github.com/bubustack/bobrapet" + labels: + - "dependencies" + - "go" + + # GitHub Actions + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + day: "monday" + open-pull-requests-limit: 5 + labels: + - "dependencies" + - "github-actions" + + # Docker base images + - package-ecosystem: "docker" + directory: "/" + schedule: + interval: "weekly" + day: "monday" + open-pull-requests-limit: 5 + labels: + - "dependencies" + - "docker" + + diff --git a/.github/labels.yml b/.github/labels.yml new file mode 100644 index 0000000..0ace1cf --- /dev/null +++ b/.github/labels.yml @@ -0,0 +1,84 @@ +- name: "kind/bug" + color: "d73a4a" + description: "Unexpected behaviour or regression that needs fixing." +- name: "kind/feature" + color: "0e8a16" + description: "New functionality or enhancement request." +- name: "kind/docs" + color: "c5def5" + description: "Documentation, examples, or community health updates." +- name: "kind/refactor" + color: "5319e7" + description: "Code health, cleanup, or non-functional improvements." +- name: "kind/tests" + color: "fbca04" + description: "Testing, CI, or verification-only changes." +- name: "kind/chore" + color: "bfd4f2" + description: "Maintenance, dependency bumps, or release automation." +- name: "dependencies" + color: "0366d6" + description: "Dependency updates raised by automation such as Dependabot." +- name: "go" + color: "00ADD8" + description: "Go module dependency updates." +- name: "github-actions" + color: "000000" + description: "GitHub Actions dependency updates." +- name: "docker" + color: "2496ED" + description: "Docker base image updates." +- name: "area/operator" + color: "0b4f6c" + description: "Bobrapet controller or CRD-level change." +- name: "area/transport" + color: "0b4f6c" + description: "Bobravoz gRPC transport changes." +- name: "area/sdk" + color: "0b4f6c" + description: "Bubu SDK or shared runtime work." +- name: "area/engram" + color: "0b4f6c" + description: "Specific Engram implementation or template change." +- name: "area/impulse" + color: "0b4f6c" + description: "Impulse templates, webhook ingestion, or trigger paths." +- name: "area/docs" + color: "0b4f6c" + description: "Docs site, READMEs, or knowledge base updates." +- name: "priority/critical" + color: "b60205" + description: "Production-impacting issue that needs immediate attention." +- name: "priority/high" + color: "d93f0b" + description: "Important issue to schedule soon." +- name: "priority/medium" + color: "fbca04" + description: "Normal priority item." +- name: "priority/low" + color: "cfd3d7" + description: "Nice-to-have or backlog item." +- name: "status/triage" + color: "ededed" + description: "Issue has not been reviewed yet." +- name: "status/needs-info" + color: "f9d0c4" + description: "Waiting on more information from the reporter." +- name: "status/in-progress" + color: "004d99" + description: "Actively being worked on." +- name: "status/blocked" + color: "5319e7" + description: "Blocked on another issue, dependency, or external signal." +- name: "status/ready" + color: "28a745" + description: "Ready to merge/release once tests pass." +- name: "good first issue" + color: "7057ff" + description: "Small, well-scoped tasks for new contributors." +- name: "help wanted" + color: "008672" + description: "Looking for community contributions." +- name: "triage/needs-owner" + color: "5319e7" + description: "Needs someone to own or shepherd the fix." diff --git a/.github/release-please-config.json b/.github/release-please-config.json new file mode 100644 index 0000000..54ce814 --- /dev/null +++ b/.github/release-please-config.json @@ -0,0 +1,59 @@ +{ + "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json", + "packages": { + ".": { + "release-type": "go", + "package-name": "http-request-engram", + "include-component-in-tag": false, + "changelog-sections": [ + { + "type": "feat", + "section": "Features", + "hidden": false + }, + { + "type": "fix", + "section": "Bug Fixes", + "hidden": false + }, + { + "type": "perf", + "section": "Performance Improvements", + "hidden": false + }, + { + "type": "refactor", + "section": "Code Refactoring", + "hidden": false + }, + { + "type": "docs", + "section": "Documentation", + "hidden": false + }, + { + "type": "test", + "section": "Tests", + "hidden": false + }, + { + "type": "build", + "section": "Build System", + "hidden": false + }, + { + "type": "ci", + "section": "Continuous Integration", + "hidden": false + }, + { + "type": "chore", + "section": "Miscellaneous", + "hidden": false + } + ] + } + }, + "bump-minor-pre-major": true, + "bump-patch-for-minor-pre-major": true +} diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..f6aea5b --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,37 @@ +name: CI + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +permissions: + contents: read + +jobs: + verify: + runs-on: ubuntu-latest + steps: + - name: Clone the code + uses: actions/checkout@v6 + + - name: Setup Go + uses: actions/setup-go@v6 + with: + go-version-file: go.mod + check-latest: true + cache: true + cache-dependency-path: go.sum + + - name: Verify go.mod is tidy + run: go mod tidy -diff + + - name: Check linter configuration + run: make lint-config + + - name: Lint + run: make lint + + - name: Test + run: make test diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml new file mode 100644 index 0000000..2e5e438 --- /dev/null +++ b/.github/workflows/docker.yml @@ -0,0 +1,85 @@ +name: Docker + +on: + pull_request: + branches: + - main + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + packages: write + security-events: write + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + +jobs: + build: + name: Build and Test Image + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v4 + + - name: Extract metadata + id: meta + uses: docker/metadata-action@v6 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=ref,event=pr + type=raw,value=latest,enable={{is_default_branch}} + type=ref,event=branch + type=sha + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=semver,pattern={{major}} + + - name: Build Docker image (no push) + uses: docker/build-push-action@v7 + with: + context: . + platforms: linux/amd64,linux/arm64 + push: false + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + + scan: + name: Security Scan + runs-on: ubuntu-latest + needs: build + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Build image for scanning + uses: docker/build-push-action@v7 + with: + context: . + load: true + tags: ${{ env.IMAGE_NAME }}:scan + + - name: Run Trivy vulnerability scanner + uses: aquasecurity/trivy-action@0.35.0 + with: + image-ref: ${{ env.IMAGE_NAME }}:scan + format: 'sarif' + output: 'trivy-results.sarif' + severity: 'CRITICAL,HIGH' + + - name: Upload Trivy results + uses: github/codeql-action/upload-sarif@v4 + if: always() + with: + sarif_file: 'trivy-results.sarif' diff --git a/.github/workflows/label-sync.yml b/.github/workflows/label-sync.yml new file mode 100644 index 0000000..bee9407 --- /dev/null +++ b/.github/workflows/label-sync.yml @@ -0,0 +1,26 @@ +name: Label Sync + +on: + workflow_dispatch: + push: + branches: [main] + paths: + - '.github/labels.yml' + - '.github/workflows/label-sync.yml' + +jobs: + sync-labels: + name: Sync Repository Labels + runs-on: ubuntu-latest + permissions: + contents: read + issues: write + steps: + - name: Clone the code + uses: actions/checkout@v6 + - name: Sync labels from .github/labels.yml + uses: micnncim/action-label-syncer@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + manifest: .github/labels.yml diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml new file mode 100644 index 0000000..47e9a77 --- /dev/null +++ b/.github/workflows/release-please.yml @@ -0,0 +1,138 @@ +name: Release Please + +on: + push: + branches: + - main + +permissions: + contents: write + packages: write + pull-requests: write + +concurrency: + group: release-please-${{ github.ref }} + cancel-in-progress: true + +env: + GHCR_IMAGE: ghcr.io/${{ github.repository }} + +jobs: + release-please: + runs-on: ubuntu-latest + outputs: + release_created: ${{ steps.release.outputs.release_created }} + tag_name: ${{ steps.release.outputs.tag_name }} + steps: + - name: Release Please + id: release + uses: googleapis/release-please-action@v4 + with: + config-file: .github/release-please-config.json + manifest-file: .github/.release-please-manifest.json + + - name: Checkout code + if: ${{ steps.release.outputs.release_created }} + uses: actions/checkout@v6 + + - name: Set up Go + if: ${{ steps.release.outputs.release_created }} + uses: actions/setup-go@v6 + with: + go-version-file: go.mod + check-latest: true + cache: true + cache-dependency-path: go.sum + + - name: Run lint and tests + if: ${{ steps.release.outputs.release_created }} + run: | + make lint + make test + + - name: Set up Docker Buildx + if: ${{ steps.release.outputs.release_created }} + uses: docker/setup-buildx-action@v4 + + - name: Log in to GitHub Container Registry + if: ${{ steps.release.outputs.release_created }} + uses: docker/login-action@v4 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract Docker metadata + if: ${{ steps.release.outputs.release_created }} + id: meta + uses: docker/metadata-action@v6 + with: + images: ${{ env.GHCR_IMAGE }} + tags: | + type=semver,pattern={{version}},value=${{ steps.release.outputs.tag_name }} + type=semver,pattern={{major}}.{{minor}},value=${{ steps.release.outputs.tag_name }} + type=semver,pattern={{major}},value=${{ steps.release.outputs.tag_name }} + type=raw,value=${{ steps.release.outputs.tag_name }} + type=raw,value=latest + + - name: Build and push Docker image + if: ${{ steps.release.outputs.release_created }} + uses: docker/build-push-action@v7 + with: + context: . + platforms: linux/amd64,linux/arm64 + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + + - name: Detect template manifest + if: ${{ steps.release.outputs.release_created }} + id: template + run: | + set -euo pipefail + if [ -f Engram.yaml ]; then + template_file="Engram.yaml" + template_kind="EngramTemplate" + elif [ -f Impulse.yaml ]; then + template_file="Impulse.yaml" + template_kind="ImpulseTemplate" + else + echo "Expected Engram.yaml or Impulse.yaml at repo root" >&2 + exit 1 + fi + + echo "file=${template_file}" >> "$GITHUB_OUTPUT" + echo "kind=${template_kind}" >> "$GITHUB_OUTPUT" + echo "latest_url=https://github.com/${{ github.repository }}/releases/latest/download/${template_file}" >> "$GITHUB_OUTPUT" + echo "versioned_url=https://github.com/${{ github.repository }}/releases/download/${{ steps.release.outputs.tag_name }}/${template_file}" >> "$GITHUB_OUTPUT" + + - name: Upload template manifest to GitHub release + if: ${{ steps.release.outputs.release_created }} + env: + GH_TOKEN: ${{ github.token }} + run: gh release upload "${{ steps.release.outputs.tag_name }}" "${{ steps.template.outputs.file }}" --clobber + + - name: Summarize release + if: ${{ steps.release.outputs.release_created }} + run: | + { + echo "## Release ${{ steps.release.outputs.tag_name }} 🚀" + echo + echo "- **GitHub Release**: https://github.com/${{ github.repository }}/releases/tag/${{ steps.release.outputs.tag_name }}" + echo "- **Container Image**: \`${{ env.GHCR_IMAGE }}:${{ steps.release.outputs.tag_name }}\`" + echo "- **Template Kind**: \`${{ steps.template.outputs.kind }}\`" + echo "- **Latest Template URL**: ${{ steps.template.outputs.latest_url }}" + echo "- **Pinned Template URL**: ${{ steps.template.outputs.versioned_url }}" + echo + echo "### Install the template" + echo '```bash' + echo "kubectl apply -f ${{ steps.template.outputs.latest_url }}" + echo '```' + echo + echo "### Pull the image" + echo '```bash' + echo "docker pull ${{ env.GHCR_IMAGE }}:${{ steps.release.outputs.tag_name }}" + echo '```' + } >> "$GITHUB_STEP_SUMMARY" diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ac18d26 --- /dev/null +++ b/.gitignore @@ -0,0 +1,30 @@ +# Created by https://www.toptal.com/developers/gitignore/api/go +# Edit at https://www.toptal.com/developers/gitignore?templates=go + +### Go ### +# If you prefer the allow list template instead of the deny list, see community template: +# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore +# +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +# vendor/ + +# Go workspace file +go.work + +# End of https://www.toptal.com/developers/gitignore/api/go + +.DS_Store +bin/ diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..6ef9fd0 --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,54 @@ +version: "2" +run: + allow-parallel-runners: true +linters: + default: none + enable: + - copyloopvar + - dupl + - errcheck + - ginkgolinter + - goconst + - gocyclo + - govet + - ineffassign + - lll + - misspell + - nakedret + - prealloc + - revive + - staticcheck + - unconvert + - unparam + - unused + settings: + gocyclo: + min-complexity: 15 + revive: + rules: + - name: comment-spacings + - name: import-shadowing + exclusions: + generated: lax + rules: + - linters: + - lll + path: api/* + - linters: + - dupl + - lll + path: internal/* + paths: + - third_party$ + - builtin$ + - examples$ +formatters: + enable: + - gofmt + - goimports + exclusions: + generated: lax + paths: + - third_party$ + - builtin$ + - examples$ diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..7c1580f --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,12 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +This file is automatically generated by [Release Please](https://github.com/googleapis/release-please) based on [Conventional Commits](https://www.conventionalcommits.org/). + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +- Development happens on `main`; release notes are generated when a version is cut. diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..9a5b891 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,94 @@ +# Contributor Covenant 3.0 Code of Conduct + +## Our Pledge + +We pledge to make our community welcoming, safe, and equitable for all. + +We are committed to fostering an environment that respects and promotes the dignity, rights, and contributions of all individuals, regardless of characteristics including race, ethnicity, caste, color, age, physical characteristics, neurodiversity, disability, sex or gender, gender identity or expression, sexual orientation, language, philosophy or religion, national or social origin, socio-economic position, level of education, or other status. The same privileges of participation are extended to everyone who participates in good faith and in accordance with this Covenant. + + +## Encouraged Behaviors + +While acknowledging differences in social norms, we all strive to meet our community's expectations for positive behavior. We also understand that our words and actions may be interpreted differently than we intend based on culture, background, or native language. + +With these considerations in mind, we agree to behave mindfully toward each other and act in ways that center our shared values, including: + +1. Respecting the **purpose of our community**, our activities, and our ways of gathering. +2. Engaging **kindly and honestly** with others. +3. Respecting **different viewpoints** and experiences. +4. **Taking responsibility** for our actions and contributions. +5. Gracefully giving and accepting **constructive feedback**. +6. Committing to **repairing harm** when it occurs. +7. Behaving in other ways that promote and sustain the **well-being of our community**. + + +## Restricted Behaviors + +We agree to restrict the following behaviors in our community. Instances, threats, and promotion of these behaviors are violations of this Code of Conduct. + +1. **Harassment.** Violating explicitly expressed boundaries or engaging in unnecessary personal attention after any clear request to stop. +2. **Character attacks.** Making insulting, demeaning, or pejorative comments directed at a community member or group of people. +3. **Stereotyping or discrimination.** Characterizing anyone’s personality or behavior on the basis of immutable identities or traits. +4. **Sexualization.** Behaving in a way that would generally be considered inappropriately intimate in the context or purpose of the community. +5. **Violating confidentiality**. Sharing or acting on someone's personal or private information without their permission. +6. **Endangerment.** Causing, encouraging, or threatening violence or other harm toward any person or group. +7. Behaving in other ways that **threaten the well-being** of our community. + +### Other Restrictions + +1. **Misleading identity.** Impersonating someone else for any reason, or pretending to be someone else to evade enforcement actions. +2. **Failing to credit sources.** Not properly crediting the sources of content you contribute. +3. **Promotional materials**. Sharing marketing or other commercial content in a way that is outside the norms of the community. +4. **Irresponsible communication.** Failing to responsibly present content which includes, links or describes any other restricted behaviors. + + +## Reporting an Issue + +Tensions can occur between community members even when they are trying their best to collaborate. Not every conflict represents a code of conduct violation, and this Code of Conduct reinforces encouraged behaviors and norms that can help avoid conflicts and minimize harm. + +When an incident does occur, it is important to report it promptly. To report a possible violation, please contact the Community Moderators via one of the following channels: + +- Email: community@bubustack.com +- GitHub Discussions: https://github.com/orgs/bubustack/discussions (select the Community Moderation category) + +If you are uncomfortable reporting publicly, email is preferred. We aim to acknowledge reports within 72 hours and will keep reporters updated as appropriate. + +Community Moderators take reports of violations seriously and will make every effort to respond in a timely manner. They will investigate all reports of code of conduct violations, reviewing messages, logs, and recordings, or interviewing witnesses and other participants. Community Moderators will keep investigation and enforcement actions as transparent as possible while prioritizing safety and confidentiality. In order to honor these values, enforcement actions are carried out in private with the involved parties, but communicating to the whole community may be part of a mutually agreed upon resolution. + + +## Addressing and Repairing Harm + +If an investigation by the Community Moderators finds that this Code of Conduct has been violated, the following enforcement ladder may be used to determine how best to repair harm, based on the incident's impact on the individuals involved and the community as a whole. Depending on the severity of a violation, lower rungs on the ladder may be skipped. + +1) Warning + 1) Event: A violation involving a single incident or series of incidents. + 2) Consequence: A private, written warning from the Community Moderators. + 3) Repair: Examples of repair include a private written apology, acknowledgement of responsibility, and seeking clarification on expectations. +2) Temporarily Limited Activities + 1) Event: A repeated incidence of a violation that previously resulted in a warning, or the first incidence of a more serious violation. + 2) Consequence: A private, written warning with a time-limited cooldown period designed to underscore the seriousness of the situation and give the community members involved time to process the incident. The cooldown period may be limited to particular communication channels or interactions with particular community members. + 3) Repair: Examples of repair may include making an apology, using the cooldown period to reflect on actions and impact, and being thoughtful about re-entering community spaces after the period is over. +3) Temporary Suspension + 1) Event: A pattern of repeated violation which the Community Moderators have tried to address with warnings, or a single serious violation. + 2) Consequence: A private written warning with conditions for return from suspension. In general, temporary suspensions give the person being suspended time to reflect upon their behavior and possible corrective actions. + 3) Repair: Examples of repair include respecting the spirit of the suspension, meeting the specified conditions for return, and being thoughtful about how to reintegrate with the community when the suspension is lifted. +4) Permanent Ban + 1) Event: A pattern of repeated code of conduct violations that other steps on the ladder have failed to resolve, or a violation so serious that the Community Moderators determine there is no way to keep the community safe with this person as a member. + 2) Consequence: Access to all community spaces, tools, and communication channels is removed. In general, permanent bans should be rarely used, should have strong reasoning behind them, and should only be resorted to if working through other remedies has failed to change the behavior. + 3) Repair: There is no possible repair in cases of this severity. + +This enforcement ladder is intended as a guideline. It does not limit the ability of Community Managers to use their discretion and judgment, in keeping with the best interests of our community. + + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public or other spaces. Examples of representing our community include using an official email address, posting via an official social media account, or acting as an appointed representative at an online or offline event. + + +## Attribution + +This Code of Conduct is adapted from the Contributor Covenant, version 3.0, permanently available at [https://www.contributor-covenant.org/version/3/0/](https://www.contributor-covenant.org/version/3/0/). + +Contributor Covenant is stewarded by the Organization for Ethical Source and licensed under CC BY-SA 4.0. To view a copy of this license, visit [https://creativecommons.org/licenses/by-sa/4.0/](https://creativecommons.org/licenses/by-sa/4.0/) + +For answers to common questions about Contributor Covenant, see the FAQ at [https://www.contributor-covenant.org/faq](https://www.contributor-covenant.org/faq). Translations are provided at [https://www.contributor-covenant.org/translations](https://www.contributor-covenant.org/translations). Additional enforcement and community guideline resources can be found at [https://www.contributor-covenant.org/resources](https://www.contributor-covenant.org/resources). The enforcement ladder was inspired by the work of [Mozilla’s code of conduct team](https://github.com/mozilla/inclusion). diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..b959650 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,63 @@ +# Contributing to http-request-engram + +Thank you for considering a contribution—your help keeps this repository healthy for every bobrapet Story. + +This guide explains how we work in the `http-request-engram` repository, from filing issues to sending patches. + +## How Can I Contribute? + +### Reporting Bugs + +- Search for an existing report under [Issues](https://github.com/bubustack/http-request-engram/issues). +- If nothing matches, [open a new issue](https://github.com/bubustack/http-request-engram/issues/new) with a clear description, reproduction steps, and both observed and expected behaviour. + +### Suggesting Enhancements + +- Start a thread in [Discussions](https://github.com/orgs/bubustack/discussions) or file a feature request issue. +- Document motivating use-cases, proposed config additions, and any compatibility considerations for existing Stories. + +### Pull Requests + +- Fork the repository and branch from `main`. +- Keep changes focused; additive config should include manifest updates (`Engram.yaml` or `Impulse.yaml`) and README documentation. +- Run the quality gates locally: + - `make lint` + - `make test` + - `make docker-build IMG=ghcr.io//http-request-engram:dev` when binaries or Dockerfile paths change +- Open the PR with a summary, testing evidence, and any follow-up TODOs. + +## Development Workflow + +### Prerequisites + +- Go 1.26+ +- Docker (or another OCI-compatible builder) +- `make` + +### Setup + +1. Fork the repository. +2. Clone your fork: `git clone https://github.com//http-request-engram.git` +3. Enter the repo: `cd http-request-engram` +4. Install tooling: `make lint-config` +5. Confirm builds and tests: `make build && make test` + +### Running Tests + +```bash +make test +``` + +### Commit Message Conventions + +We use [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) so releases and change logs stay automated. + +Examples: +- `feat: add new configuration option` +- `fix: handle edge case in processing` +- `docs: clarify configuration fields` +- `chore: bump SDK to latest` + +### Code of Conduct + +Participation in this project is governed by the [Contributor Covenant Code of Conduct](./CODE_OF_CONDUCT.md). Report unacceptable behaviour to [conduct@bubustack.com](mailto:conduct@bubustack.com). diff --git a/Dockerfile b/Dockerfile index 1001572..2c99fa6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,28 +1,19 @@ # --- Build Stage --- -FROM golang:1.24-alpine AS builder - -# Install git, which is required for Go modules to fetch dependencies. -RUN apk add --no-cache git +FROM golang:1.26-bookworm AS builder WORKDIR /src -# Copy the entire project context. -# This ensures that the local 'replace' directives in go.mod work correctly. +# Copy source code. COPY . . RUN go mod download -# Build the binary from within the engram's directory. -# The Go toolchain will find the parent go.mod and handle the local SDK dependency. -RUN CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH} go build -a -o http-request main.go +# Build the binary. +RUN CGO_ENABLED=0 GOOS=linux go build -o http-request-engram . # --- Final Stage --- -FROM gcr.io/distroless/static-debian12 - -COPY --from=builder /src/http-request /http-request +FROM gcr.io/distroless/static:nonroot -# Set the default execution mode to "batch". -# This can be overridden at runtime by setting the environment variable. -ENV BUBU_EXECUTION_MODE="batch" +COPY --from=builder /src/http-request-engram /http-request-engram -ENTRYPOINT ["/http-request"] +ENTRYPOINT ["/http-request-engram"] diff --git a/Engram.yaml b/Engram.yaml index a7dea8b..0b344d6 100644 --- a/Engram.yaml +++ b/Engram.yaml @@ -1,16 +1,17 @@ ---- -apiVersion: catalog.bubu.sh/v1alpha1 +apiVersion: catalog.bubustack.io/v1alpha1 kind: EngramTemplate metadata: name: http-request + annotations: + registry.bubustack.io/maturity: experimental spec: - version: "0.1.0" + version: 0.1.0 description: "Makes an HTTP request to a specified URL." supportedModes: - job - deployment - statefulset - image: "http-request:latest" + image: ghcr.io/bubustack/http-request-engram:0.1.0 configSchema: type: object properties: @@ -69,7 +70,7 @@ spec: description: "A safeguard limit for the maximum number of pages to fetch." nextURLPath: type: string - description: "For 'nextUrl' mode: JSONPath to the URL for the next page in the response body (e.g., '$.links.next')." + description: "For 'nextUrl' mode: JSONPath to the URL for the next page in the response body (e.g., '$.links.next'). Resolved URLs must remain on the same origin (scheme + host) as the current page URL." updateParam: type: object description: "For 'updateParam' mode: Configuration for the parameter to update on each request." @@ -117,7 +118,7 @@ spec: type: string enum: ["auto", "json", "text", "file"] default: "auto" - description: "The format for the response body." + description: "The format for the response body. 'file' returns an object with base64 data (`encoding`, `data`, `sizeBytes`, optional `contentType`)." outputFieldName: type: string default: "body" @@ -168,11 +169,10 @@ spec: properties: url: type: string - description: "The URL to send the request to." + description: "The URL to send the request to. Optional when the Engram configures defaultURL." method: type: string - description: "The HTTP method to use (GET, POST, etc.)." - default: "GET" + description: "The HTTP method to use (GET, POST, etc.). Defaults to the Engram's defaultMethod config, or GET if unset." headers: type: object description: "A map of HTTP headers to send with the request." @@ -181,8 +181,6 @@ spec: body: type: string description: "The request body (for POST, PUT, etc.)." - required: - - url outputSchema: type: object properties: @@ -196,10 +194,23 @@ spec: type: object description: "A map of the response headers." body: - type: string - description: "The response body." + oneOf: + - type: string + - type: object + properties: + encoding: + type: string + data: + type: string + sizeBytes: + type: integer + contentType: + type: string + description: "The response body. In response.format=file this field is an object with base64 payload metadata." execution: - job: - recommendedRestartPolicy: "Never" - deployment: {} - statefulset: {} + service: + ports: + - name: grpc + protocol: TCP + port: 9000 + targetPort: 9000 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..12b1432 --- /dev/null +++ b/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2025 BubuStack. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..3ae72a5 --- /dev/null +++ b/Makefile @@ -0,0 +1,136 @@ +# Image URL for container targets (override with IMG=) +IMG ?= ghcr.io/bubustack/http-request-engram:dev + +# Container tool to use for image commands (defaults to docker) +CONTAINER_TOOL ?= docker + +# Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set) +ifeq (,$(shell go env GOBIN)) +GOBIN=$(shell go env GOPATH)/bin +else +GOBIN=$(shell go env GOBIN) +endif + +# Setting SHELL to bash allows bash commands to be executed by recipes. +# Options are set to exit when a recipe line exits non-zero or a piped command fails. +SHELL = /usr/bin/env bash -o pipefail +.SHELLFLAGS = -ec + +.PHONY: all +all: build + +##@ General + +# The help target prints out all targets with their descriptions organized +# beneath their categories. The categories are represented by '##@' and the +# target descriptions by '##'. The awk command is responsible for reading the +# entire set of makefiles included in this invocation, looking for lines of the +# file as xyz: ## something, and then pretty-format the target and help. Then, +# if there's a line with ##@ something, that gets pretty-printed as a category. +# More info on the usage of ANSI control characters for terminal formatting: +# https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_parameters +# More info on the awk command: +# http://linuxcommand.org/lc3_adv_awk.php + +.PHONY: help +help: ## Display this help. + @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n"} /^[a-zA-Z_0-9-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST) + +##@ Development + +.PHONY: fmt +fmt: ## Run go fmt against code. + go fmt ./... + +.PHONY: vet +vet: ## Run go vet against code. + go vet ./... + +.PHONY: test +test: fmt vet ## Run tests. + go test ./... + +.PHONY: test-coverage +test-coverage: ## Run tests with coverage profile. + go test -coverprofile=coverage.out ./... + @echo "Coverage profile written to coverage.out" + +.PHONY: lint +lint: golangci-lint ## Run golangci-lint linter + $(GOLANGCI_LINT) run + +.PHONY: lint-fix +lint-fix: golangci-lint ## Run golangci-lint linter and perform fixes + $(GOLANGCI_LINT) run --fix + +.PHONY: lint-config +lint-config: golangci-lint ## Verify golangci-lint linter configuration + $(GOLANGCI_LINT) config verify + +##@ Build + +.PHONY: build +build: fmt vet ## Build all packages. + go build ./... + +.PHONY: run +run: fmt vet ## Run the engram locally (requires kubeconfig) + go run ./main.go + +.PHONY: docker-build +docker-build: ## Build container image with the engram binary. + $(CONTAINER_TOOL) build -t $(IMG) . + +.PHONY: docker-push +docker-push: ## Push the engram image to a registry. + $(CONTAINER_TOOL) push $(IMG) + +PLATFORMS ?= linux/amd64,linux/arm64 +.PHONY: docker-buildx +docker-buildx: ## Build and push the adapter image for multiple platforms. + $(CONTAINER_TOOL) buildx build --platform=$(PLATFORMS) --tag $(IMG) --push . + +.PHONY: tidy +tidy: ## Tidy go.mod and go.sum + go mod tidy + +##@ Clean + +.PHONY: clean +clean: ## Clean build and coverage artifacts + rm -f coverage.out coverage.html + go clean ./... + +##@ Dependencies + +## Location to install dependencies to +LOCALBIN ?= $(shell pwd)/bin +$(LOCALBIN): + mkdir -p $(LOCALBIN) + +## Tool Binaries +GOLANGCI_LINT = $(LOCALBIN)/golangci-lint + +## Tool Versions +GOLANGCI_LINT_VERSION ?= v2.11.4 + +.PHONY: golangci-lint +golangci-lint: $(GOLANGCI_LINT) ## Download golangci-lint locally if necessary. +$(GOLANGCI_LINT): $(LOCALBIN) + $(call go-install-tool,$(GOLANGCI_LINT),github.com/golangci/golangci-lint/v2/cmd/golangci-lint,$(GOLANGCI_LINT_VERSION)) + +# go-install-tool will 'go install' any package with custom target and name of binary, if it doesn't exist +# $1 - target path with name of binary +# $2 - package url which can be installed +# $3 - specific version of package +define go-install-tool +@[ -f "$(1)-$(3)" ] && [ "$$(readlink -- "$(1)" 2>/dev/null)" = "$(1)-$(3)" ] || { \ +set -e; \ +package=$(2)@$(3) ;\ +echo "Downloading $${package}" ;\ +rm -f $(1) ;\ +GOBIN=$(LOCALBIN) go install $${package} ;\ +mv $(1) $(1)-$(3) ;\ +} ;\ +ln -sf $$(realpath $(1)-$(3)) $(1) +endef diff --git a/README.md b/README.md index e3f5bd0..5131534 100644 --- a/README.md +++ b/README.md @@ -1,105 +1,167 @@ -# HTTP Request Engram - -Version: `0.1.0` - -A powerful and flexible Engram for making HTTP requests. This Engram is designed to be a general-purpose tool for interacting with any REST API, webhook, or web service. It supports dual-mode execution for both single-shot jobs and long-running streaming services. - -## Features - -This Engram has been engineered to provide a comprehensive set of features for robust and reliable HTTP communication in a variety of scenarios. - -### Core Features -- **Dual Mode:** Can be run as a `job` for single requests or as a `deployment`/`statefulset` for a persistent streaming service. -- **Flexible Authentication:** - - **Bearer Token:** Provide a `bearer_token` via a secret. - - **Basic Auth:** Provide `basic_username` and `basic_password` via a secret. - - **Custom Header:** Provide a `custom_header_value` via a secret to be sent in a configured header. -- **Dynamic Inputs:** All core request parameters (`url`, `method`, `headers`, `body`, `params`) can be provided dynamically at runtime. - -### Advanced Control -- **Response Handling:** - - Conditionally include/exclude response headers and status code. - - Control response body format (`auto-detect`, `json`, `text`). -- **Error Handling:** A `neverError` mode allows the engram to succeed even on non-2xx status codes, returning the error details in the output. -- **Timeouts:** Configure a timeout for each request. -- **Redirects:** Enable or disable following redirects and set a maximum number of redirects to follow. The engram intelligently forwards authentication headers on cross-domain redirects. -- **SSL Verification:** Option to ignore SSL certificate validation issues. -- **Proxy Support:** Route requests through an HTTP proxy, with support for proxy authentication via secrets. -- **Query Parameter Formatting:** Control how arrays are formatted in query strings (`noBrackets`, `brackets`, `indices`) for compatibility with various web frameworks. - -### Job Mode Features -- **Pagination:** Automatically handle paginated APIs to fetch a complete dataset. - - **`nextUrl` mode:** Follows a URL for the next page found in the response body. - - **`updateParam` mode:** Automatically increments a page/offset parameter or uses a cursor from the response body for subsequent requests. - -### Streaming Mode Features -- **Batching & Rate Limiting:** When in streaming mode, you can configure the number of concurrent requests (`itemsPerBatch`) and the delay between batches (`batchInterval`) to control the load on the downstream service. - -## Configuration (`configSchema`) - -This section details the static, instance-level configuration that can be provided when deploying an `Engram` resource from this template. - -| Parameter | Type | Description | -|---|---|---| -| `defaultUrl` | `string` | A fallback URL to use if one is not provided in the runtime `inputs`. | -| `defaultMethod` | `string` | A fallback HTTP method to use. Defaults to `GET`. | -| `defaultHeaders` | `map[string]string` | A map of headers to apply to every request. Runtime headers will be merged on top. | -| `auth` | `object` | Configures the authentication strategy for the instance. | -| `auth.type` | `string` | The auth type to use. One of `bearer`, `basic`, `customHeader`. | -| `auth.headerName` | `string` | The header name to use when `auth.type` is `customHeader`. | -| `proxy` | `string` | The URL of an HTTP proxy to use. | -| `batching` | `object` | **Streaming Mode Only.** Configures request batching. | -| `batching.itemsPerBatch` | `integer` | Max number of concurrent requests. | -| `batching.batchInterval`| `string` | Delay between batches (e.g., "1s"). | -| `pagination` | `object` | **Job Mode Only.** Configures automatic pagination. | -| `pagination.mode` | `string` | The strategy to use. One of `off`, `nextUrl`, `updateParam`. | -| `pagination.resultsPath` | `string` | A JSONPath expression to find the results array in the response. | -| `pagination.maxPages` | `integer` | A safeguard to limit the number of pages fetched. | -| `...` | `...` | See `Engram.yaml` for full details on `nextUrl` and `updateParam` configuration. | -| `queryParams` | `object` | Configures query parameter formatting. | -| `queryParams.arrayFormat`| `string` | How to format arrays. One of `noBrackets`, `brackets`, `indices`. | -| `response` | `object` | Configures the output structure. | -| `response.includeHeaders`| `boolean`| If `true`, includes response headers in the output. | -| `response.includeStatus`| `boolean`| If `true`, includes status code/text in the output. | -| `response.format`| `string` | The desired format of the response body. One of `auto`, `json`, `text`, `file`. | -| `neverError` | `boolean` | If `true`, the engram will not fail on non-2xx status codes. | -| `timeout` | `string` | The timeout for the request (e.g., "10s"). | -| `redirects` | `object` | Configures redirect behavior. | -| `redirects.follow`| `boolean`| If `true`, the client will follow redirects. | -| `redirects.maxRedirects`| `integer`| The maximum number of redirects to follow. | -| `ignoreSslIssues` | `boolean` | If `true`, SSL certificate validation is skipped. | - -## Secrets (`secretSchema`) - -This engram can be configured with the following secrets: - -| Name | Expected Keys | Description | -|---|---|---| -| `bearerToken` | `bearer_token` | For `bearer` authentication. | -| `basicAuth` | `basic_username`, `basic_password` | For `basic` authentication. | -| `customHeader`| `custom_header_value` | For `customHeader` authentication. | -| `proxyAuth` | `proxy_username`, `proxy_password` | For an authenticating proxy. | - -## Inputs (`inputSchema`) - -This section details the dynamic inputs that can be provided for each execution. - -| Parameter | Type | Description | -|---|---|---| -| `url` | `string` | **Required.** The URL to send the request to. | -| `method` | `string` | The HTTP method. Defaults to the engram's `defaultMethod` or `GET`. | -| `params` | `map[string]interface{}` | A map of query parameters to add to the URL. | -| `headers` | `map[string]interface{}` | A map of headers to send with the request. | -| `body` | `string` | The request body. | - -## Outputs (`outputSchema`) - -The engram will produce an output with the following structure: - -| Parameter | Type | Description | -|---|---|---| -| `statusCode` | `integer` | The HTTP status code of the response. (Optional via `response.includeStatus`) | -| `status` | `string` | The HTTP status text of the response. (Optional via `response.includeStatus`) | -| `headers` | `object` | A map of the response headers. (Optional via `response.includeHeaders`) | -| `body` | `any` | The response body, formatted according to `response.format`. | -| `error` | `string` | If `neverError` is true and an error occurs, this field will contain the error message. | +# 🌐 HTTP Request Engram + +The HTTP Request Engram is a dual-mode component for bobrapet Stories. It turns declarative workflow specs into robust HTTP clients that can run once as a batch Job or continuously as a streaming Deployment. Use it to call REST APIs, webhooks, or internal services without wiring clients by hand. + +## 🌟 Highlights + +- **Dual execution modes** – Ships the same implementation as a batch step (`job`) or a streaming service (`deployment`/`statefulset`) with automatic backpressure. +- **Rich configuration surface** – Auth, pagination, redirects, proxy support, per-field defaults, and response shaping are all driven by Engram `spec.with`. +- **Graceful error handling** – Toggle `neverError` to surface non-2xx responses without failing the step; streaming workers respect context cancellation and propagate structured errors. +- **Pagination toolkit** – Follow `nextUrl`, increment page/offset params, or chase cursors discovered in the response. +- **Observability-ready** – Execution uses the SDK logger/tracer, and responses surface status codes, headers, and optional error metadata for downstream metrics. + +## 🚀 Quick Start + +```bash +# Run linting and basic tests +make lint +go test ./... + +# Build a container image (defaults to ghcr.io/bubustack/http-request-engram:dev) +make docker-build +``` + +Drop the rendered container into a `Story` step by referencing the EngramTemplate and populating the `with` block using the configuration tables below. + +## ⚙️ Configuration (`Engram.spec.with`) + +### Core Settings + +| Field | Type | Description | Default | +| --- | --- | --- | --- | +| `defaultURL` | `string` | Fallback URL if runtime inputs omit `url`. | `""` | +| `defaultMethod` | `string` | HTTP method when inputs omit `method`. | `"GET"` | +| `defaultHeaders` | `map[string]string` | Baseline headers merged with per-request headers. | `{}` | +| `timeout` | `string` | Client timeout (Go duration). | `"10s"` | +| `neverError` | `bool` | When true, non-2xx responses emit an `error` payload but do not fail the step. | `false` | +| `ignoreSSLIssues` | `bool` | Skip TLS verification. Use with caution. | `false` | +| `proxy` | `string` | Proxy URL, e.g. `http://user:pass@host:port`. | `""` | + +### Redirect Handling + +| Field | Type | Description | Default | +| --- | --- | --- | --- | +| `redirects.follow` | `bool` | Whether to follow HTTP redirects automatically. | `true` | +| `redirects.maxRedirects` | `int` | Maximum number of redirects to follow before aborting the request. | `10` | + +### Authentication + +| Field | Type | Description | +| --- | --- | --- | +| `auth.type` | `string` | One of `bearer`, `basic`, `customHeader`. | +| `auth.headerName` | `string` | Required when `auth.type == "customHeader"`. Used as the header name populated from secrets. | + +### Pagination (`job` mode) + +| Field | Type | Description | Default | +| --- | --- | --- | --- | +| `pagination.mode` | `string` | Strategy: `off`, `nextUrl`, or `updateParam`. | `"off"` | +| `pagination.resultsPath` | `string` | JSONPath to the array of results. | `""` | +| `pagination.maxPages` | `int` | Hard stop on page count. | `100` | +| `pagination.nextURLPath` | `string` | JSONPath to the next page URL when using `nextUrl`. For safety, resolved URLs must stay on the same origin (scheme + host) as the current page URL. | `""` | +| `pagination.updateParam` | `object` | Settings when mutating a query param each page. See Engram.yaml for full schema (`name`, `type`, `initialValue`, `increment`, `cursorPath`). | + +### Query Parameters + +| Field | Type | Description | Default | +| --- | --- | --- | --- | +| `queryParams.arrayFormat` | `string` | How to encode array params: `noBrackets`, `brackets`, or `indices`. | `"noBrackets"` | + +### Response Formatting + +| Field | Type | Description | Default | +| --- | --- | --- | --- | +| `response.includeHeaders` | `bool` | Include response headers in output. | `true` | +| `response.includeStatus` | `bool` | Include status code/text in output. | `true` | +| `response.format` | `string` | `auto`, `json`, `text`, or `file`. `file` returns a structured blob map with base64 payload (`encoding`, `data`, `sizeBytes`, and optional `contentType`). | `"auto"` | +| `response.outputFieldName` | `string` | Field used to store the response body. | `"body"` | + +### Streaming Batching + +| Field | Type | Description | Default | +| --- | --- | --- | --- | +| `batching.itemsPerBatch` | `int` | Concurrent requests before pausing. | `50` | +| `batching.batchInterval` | `string` | Sleep duration between batches. | `"1s"` | + +## 🔐 Secrets + +| Purpose | Expected Keys | Notes | +| --- | --- | --- | +| Bearer auth | `bearer_token` | Injects `Authorization: Bearer …` unless already present. | +| Basic auth | `basic_username`, `basic_password` | Injects an RFC 7617 header. | +| Custom header | `custom_header_value` | Writes to `auth.headerName` unless request already set it. | +| Proxy credentials | `proxy_username`, `proxy_password` | Optional when proxy URL omits credentials. | + +## 📥 Inputs + +| Field | Type | Description | +| --- | --- | --- | +| `url` | `string` | Target URL (overrides `defaultURL`). | +| `method` | `string` | HTTP method (overrides `defaultMethod`). | +| `params` | `map[string]any` | Query parameters; arrays obey `queryParams.arrayFormat`. | +| `headers` | `map[string]any` | Per-request headers merged over defaults. | +| `body` | `string \| []byte` | Request payload (forwarded as-is). | + +## 📤 Outputs + +The Engram returns a map containing: + +| Field | Description | +| --- | --- | +| `statusCode`, `status` | Present when `response.includeStatus` is true. | +| `headers` | Response headers joined as comma-delimited strings when `response.includeHeaders` is true. | +| `` | Parsed response body. Defaults to `"body"`. In `response.format=file`, this is an object with `encoding`, `data`, `sizeBytes`, and optional `contentType`. | +| `error` | Only when `neverError` is enabled or body parsing fails; contains `statusCode`, `status`, and/or a `message`. | + +## 🔄 Streaming Mode + +Streaming consumes `engram.InboundMessage` on input and emits plain +`engram.StreamMessage` on output. + +- `Inputs` carries the decoded request map when the caller already resolved structured inputs. +- `Payload` is used as a fallback when `Inputs` is empty and the request arrives as JSON. +- `Metadata` is propagated verbatim to the outbound message for tracing. +- Call `msg.Done()` after successful handling so the SDK can advance delivery acknowledgement for ordered/replay-capable transports. + +The Engram processes messages concurrently (bounded by `itemsPerBatch`), honors `ctx.Done()` for shutdown, and reuses the same response shaping as batch mode. + +Structured JSON streaming responses keep their canonical JSON in `Payload` and +mirror the same bytes into `Binary` with `MimeType: application/json`. Raw +`Binary` without `Payload` is reserved for opaque media or non-JSON blobs. + +## 🧪 Local Development + +- `make lint` – Run golangci-lint with the shared BubuStack config. +- `go test ./...` – Build/tests (no fixtures required). +- `make docker-build` – Build a container image for local clusters. + +## 🧭 Observability & Error Handling + +- Standard logging flows through the SDK logger available via `ExecutionContext`. +- Enable `neverError` to capture non-2xx responses for inspection without failing the Story step. +- When pagination is active, warnings provide JSONPath and state hints to simplify debugging. + +## 🤝 Community & Support + +- [Contributing](./CONTRIBUTING.md) +- [Support](./SUPPORT.md) +- [Security Policy](./SECURITY.md) +- [Code of Conduct](./CODE_OF_CONDUCT.md) +- [Discord](https://discord.gg/dysrB7D8H6) + + +## 📄 License + +Copyright 2025 BubuStack. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..0590d53 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,34 @@ +# Security policy + +## Supported versions + +We currently support only the latest tagged pre-1.0 release line of this component. Upgrade to the newest tagged release before requesting a security fix. + +Kubernetes compatibility follows the bobrapet operator: we target N-2 of upstream stable releases (e.g., when Kubernetes 1.31 is current, we support 1.31, 1.30, 1.29). + +## Reporting a vulnerability + +The BubuStack Team and community take all security vulnerabilities seriously. We appreciate your efforts to responsibly disclose your findings, and will make every effort to acknowledge your contributions. + +To report a security vulnerability, please use the GitHub Security Advisory feature in this repository: + +- https://github.com/bubustack/http-request-engram/security/advisories/new + +**Please do not report security vulnerabilities through public GitHub issues.** + +When reporting a vulnerability, please provide the following information: + +- **A clear description** of the vulnerability and its potential impact. +- **Steps to reproduce** the vulnerability, including any example code, scripts, or configurations. +- **The version(s) of the component** affected. +- **Your contact information** for us to follow up with you. + +## Disclosure process + +1. **Report**: You report the vulnerability through the GitHub Security Advisory feature. +2. **Confirmation**: We will acknowledge your report within 48 hours. +3. **Investigation**: We will investigate the vulnerability and determine its scope and impact. We may contact you for additional information during this phase. +4. **Fix**: We will develop a patch for the vulnerability. +5. **Disclosure**: We will create a security advisory, issue a CVE (if applicable), and release a new version with the patch. We will credit you for your discovery unless you prefer to remain anonymous. + +We aim to resolve high severity vulnerabilities within 30 days, medium within 60 days, and low within 90 days, subject to complexity and scope. We'll keep you informed of progress. diff --git a/SUPPORT.md b/SUPPORT.md new file mode 100644 index 0000000..d5a0016 --- /dev/null +++ b/SUPPORT.md @@ -0,0 +1,36 @@ +# Support + +Thank you for using BubuStack. Here's how you can get help. + +## Self-Service + +- **Documentation**: Our documentation is the best place to start. Find guides, how‑to articles, and references in the `website` repository: https://bubustack.io/docs +- **Roadmap**: https://bubustack.io/docs/community/roadmap +- **Examples**: We provide a collection of example engrams and impulses in the [`BubuStack GitHub organization`](https://github.com/bubustack). + +## Community support + +For questions, discussions, and community support, please use the following channels: + +- **GitHub Issues**: For bug reports and feature requests, please open an issue: https://github.com/bubustack/http-request-engram/issues +- **GitHub Discussions**: For general questions and sharing your projects, please use Discussions: https://github.com/orgs/bubustack/discussions +- **Discord**: https://discord.gg/dysrB7D8H6 + +### Triage and response expectations + +- We review issues and discussions on a best-effort basis. +- We do not currently provide guaranteed response times or commercial SLAs. +- Security reports follow the process in SECURITY.md. + +### Supported versions + +- We support only the latest tagged pre-1.0 release line of this component. +- Kubernetes compatibility target (via the latest supported bobrapet line): N-2 upstream stable releases. + +## Reporting security vulnerabilities + +To report a security vulnerability, please follow the instructions in our [Security Policy](./SECURITY.md). + +### Related documentation + +- Known issues: https://github.com/bubustack/http-request-engram/issues?q=is%3Aissue+is%3Aopen+label%3Abug diff --git a/conformance_test.go b/conformance_test.go new file mode 100644 index 0000000..fb9e4b1 --- /dev/null +++ b/conformance_test.go @@ -0,0 +1,29 @@ +package main + +import ( + "fmt" + "testing" + + "github.com/bubustack/bubu-sdk-go/conformance" + "github.com/bubustack/http-request-engram/pkg/config" + "github.com/bubustack/http-request-engram/pkg/engram" +) + +func TestConformance(t *testing.T) { + suite := conformance.BatchSuite[config.Config, any]{ + Engram: engram.New(), + Config: config.Config{}, + Inputs: map[string]any{}, + ExpectError: true, + ValidateError: func(err error) error { + if err == nil { + return nil + } + if err.Error() != "input 'url' is a required string" { + return fmt.Errorf("unexpected conformance error: %w", err) + } + return nil + }, + } + suite.Run(t) +} diff --git a/go.mod b/go.mod index 1140254..ca8b086 100644 --- a/go.mod +++ b/go.mod @@ -1,34 +1,42 @@ -module http-request +module github.com/bubustack/http-request-engram -go 1.24.6 +go 1.26.2 require ( github.com/PaesslerAG/jsonpath v0.1.1 - github.com/bubustack/bubu-sdk-go v0.0.0-20250928123735-9f239a97269a + github.com/bubustack/bubu-sdk-go v0.1.2 ) require ( + dario.cat/mergo v1.0.1 // indirect + github.com/Masterminds/goutils v1.1.1 // indirect + github.com/Masterminds/semver/v3 v3.4.0 // indirect + github.com/Masterminds/sprig/v3 v3.3.0 // indirect github.com/PaesslerAG/gval v1.0.0 // indirect - github.com/aws/aws-sdk-go-v2 v1.39.2 // indirect - github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.1 // indirect - github.com/aws/aws-sdk-go-v2/config v1.31.11 // indirect - github.com/aws/aws-sdk-go-v2/credentials v1.18.15 // indirect - github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.9 // indirect - github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.9 // indirect - github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.9 // indirect - github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // indirect - github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.9 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.1 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.8.9 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.9 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.9 // indirect - github.com/aws/aws-sdk-go-v2/service/s3 v1.88.3 // indirect - github.com/aws/aws-sdk-go-v2/service/sso v1.29.5 // indirect - github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.1 // indirect - github.com/aws/aws-sdk-go-v2/service/sts v1.38.6 // indirect - github.com/aws/smithy-go v1.23.0 // indirect - github.com/bubustack/bobrapet v0.0.0-20250926120003-fbd39089e093 // indirect - github.com/bubustack/bobravoz-grpc v0.0.0-20250926120155-e667792a5283 // indirect + github.com/aws/aws-sdk-go-v2 v1.41.5 // indirect + github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8 // indirect + github.com/aws/aws-sdk-go-v2/config v1.32.14 // indirect + github.com/aws/aws-sdk-go-v2/credentials v1.19.14 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.21 // indirect + github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.22.12 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21 // indirect + github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6 // indirect + github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.22 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.13 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.21 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.21 // indirect + github.com/aws/aws-sdk-go-v2/service/s3 v1.98.0 // indirect + github.com/aws/aws-sdk-go-v2/service/signin v1.0.9 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.30.15 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.19 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.41.10 // indirect + github.com/aws/smithy-go v1.24.2 // indirect + github.com/bubustack/bobrapet v0.1.6 // indirect + github.com/bubustack/core v0.1.3 // indirect + github.com/bubustack/tractatus v0.1.2 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/emicklei/go-restful/v3 v3.13.0 // indirect github.com/evanphx/json-patch/v5 v5.9.11 // indirect @@ -49,39 +57,56 @@ require ( github.com/go-openapi/swag/stringutils v0.25.1 // indirect github.com/go-openapi/swag/typeutils v0.25.1 // indirect github.com/go-openapi/swag/yamlutils v0.25.1 // indirect - github.com/gogo/protobuf v1.3.2 // indirect github.com/google/gnostic-models v0.7.0 // indirect github.com/google/uuid v1.6.0 // indirect + github.com/huandu/xstrings v1.5.0 // indirect github.com/json-iterator/go v1.1.12 // indirect + github.com/mitchellh/copystructure v1.2.0 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/shopspring/decimal v1.4.0 // indirect + github.com/spf13/cast v1.7.0 // indirect + github.com/spf13/pflag v1.0.9 // indirect + github.com/stretchr/objx v0.5.2 // indirect + github.com/stretchr/testify v1.11.1 // indirect github.com/x448/float16 v0.8.4 // indirect + github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect + github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect + github.com/xeipuuv/gojsonschema v1.2.0 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect - go.opentelemetry.io/otel v1.38.0 // indirect - go.opentelemetry.io/otel/metric v1.38.0 // indirect - go.opentelemetry.io/otel/trace v1.38.0 // indirect + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 // indirect + go.opentelemetry.io/otel v1.43.0 // indirect + go.opentelemetry.io/otel/metric v1.43.0 // indirect + go.opentelemetry.io/otel/trace v1.43.0 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/net v0.44.0 // indirect - golang.org/x/oauth2 v0.31.0 // indirect - golang.org/x/sys v0.36.0 // indirect - golang.org/x/term v0.35.0 // indirect - golang.org/x/text v0.29.0 // indirect + golang.org/x/crypto v0.49.0 // indirect + golang.org/x/net v0.52.0 // indirect + golang.org/x/oauth2 v0.35.0 // indirect + golang.org/x/sync v0.20.0 // indirect + golang.org/x/sys v0.42.0 // indirect + golang.org/x/term v0.41.0 // indirect + golang.org/x/text v0.35.0 // indirect golang.org/x/time v0.13.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20250922171735-9219d122eba9 // indirect - google.golang.org/grpc v1.75.1 // indirect - google.golang.org/protobuf v1.36.9 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 // indirect + google.golang.org/grpc v1.80.0 // indirect + google.golang.org/protobuf v1.36.11 // indirect + gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect - k8s.io/api v0.34.1 // indirect - k8s.io/apimachinery v0.34.1 // indirect - k8s.io/client-go v0.34.1 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + k8s.io/api v0.35.3 // indirect + k8s.io/apimachinery v0.35.3 // indirect + k8s.io/client-go v0.35.3 // indirect k8s.io/klog/v2 v2.130.1 // indirect k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 // indirect - k8s.io/utils v0.0.0-20250820121507-0af2bda4dd1d // indirect - sigs.k8s.io/controller-runtime v0.22.1 // indirect + k8s.io/utils v0.0.0-20260319190234-28399d86e0b5 // indirect + sigs.k8s.io/controller-runtime v0.23.3 // indirect sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect sigs.k8s.io/randfill v1.0.0 // indirect - sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect + sigs.k8s.io/structured-merge-diff/v6 v6.3.2-0.20260122202528-d9cc6641c482 // indirect sigs.k8s.io/yaml v1.6.0 // indirect ) diff --git a/go.sum b/go.sum index 6c83bf1..2114b60 100644 --- a/go.sum +++ b/go.sum @@ -1,52 +1,66 @@ +dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= +dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= +github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= +github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= +github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= +github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs= +github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0= github.com/PaesslerAG/gval v1.0.0 h1:GEKnRwkWDdf9dOmKcNrar9EA1bz1z9DqPIO1+iLzhd8= github.com/PaesslerAG/gval v1.0.0/go.mod h1:y/nm5yEyTeX6av0OfKJNp9rBNj2XrGhAf5+v24IBN1I= github.com/PaesslerAG/jsonpath v0.1.0/go.mod h1:4BzmtoM/PI8fPO4aQGIusjGxGir2BzcV0grWtFzq1Y8= github.com/PaesslerAG/jsonpath v0.1.1 h1:c1/AToHQMVsduPAa4Vh6xp2U0evy4t8SWp8imEsylIk= github.com/PaesslerAG/jsonpath v0.1.1/go.mod h1:lVboNxFGal/VwW6d9JzIy56bUsYAP6tH/x80vjnCseY= -github.com/aws/aws-sdk-go-v2 v1.39.2 h1:EJLg8IdbzgeD7xgvZ+I8M1e0fL0ptn/M47lianzth0I= -github.com/aws/aws-sdk-go-v2 v1.39.2/go.mod h1:sDioUELIUO9Znk23YVmIk86/9DOpkbyyVb1i/gUNFXY= -github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.1 h1:i8p8P4diljCr60PpJp6qZXNlgX4m2yQFpYk+9ZT+J4E= -github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.1/go.mod h1:ddqbooRZYNoJ2dsTwOty16rM+/Aqmk/GOXrK8cg7V00= -github.com/aws/aws-sdk-go-v2/config v1.31.11 h1:6QOO1mP0MgytbfKsL/r/gE1P6/c/4pPzrrU3hKxa5fs= -github.com/aws/aws-sdk-go-v2/config v1.31.11/go.mod h1:KzpDsPX/dLxaUzoqM3sN2NOhbQIW4HW/0W8rQA1YFEs= -github.com/aws/aws-sdk-go-v2/credentials v1.18.15 h1:Gqy7/05KEfUSulSvwxnB7t8DuZMR3ShzNcwmTD6HOLU= -github.com/aws/aws-sdk-go-v2/credentials v1.18.15/go.mod h1:VWDWSRpYHjcjURRaQ7NUzgeKFN8Iv31+EOMT/W+bFyc= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.9 h1:Mv4Bc0mWmv6oDuSWTKnk+wgeqPL5DRFu5bQL9BGPQ8Y= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.9/go.mod h1:IKlKfRppK2a1y0gy1yH6zD+yX5uplJ6UuPlgd48dJiQ= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.9 h1:se2vOWGD3dWQUtfn4wEjRQJb1HK1XsNIt825gskZ970= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.9/go.mod h1:hijCGH2VfbZQxqCDN7bwz/4dzxV+hkyhjawAtdPWKZA= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.9 h1:6RBnKZLkJM4hQ+kN6E7yWFveOTg8NLPHAkqrs4ZPlTU= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.9/go.mod h1:V9rQKRmK7AWuEsOMnHzKj8WyrIir1yUJbZxDuZLFvXI= -github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 h1:bIqFDwgGXXN1Kpp99pDOdKMTTb5d2KyU5X/BZxjOkRo= -github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3/go.mod h1:H5O/EsxDWyU+LP/V8i5sm8cxoZgc2fdNR9bxlOFrQTo= -github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.9 h1:w9LnHqTq8MEdlnyhV4Bwfizd65lfNCNgdlNC6mM5paE= -github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.9/go.mod h1:LGEP6EK4nj+bwWNdrvX/FnDTFowdBNwcSPuZu/ouFys= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.1 h1:oegbebPEMA/1Jny7kvwejowCaHz1FWZAQ94WXFNCyTM= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.1/go.mod h1:kemo5Myr9ac0U9JfSjMo9yHLtw+pECEHsFtJ9tqCEI8= -github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.8.9 h1:by3nYZLR9l8bUH7kgaMU4dJgYFjyRdFEfORlDpPILB4= -github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.8.9/go.mod h1:IWjQYlqw4EX9jw2g3qnEPPWvCE6bS8fKzhMed1OK7c8= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.9 h1:5r34CgVOD4WZudeEKZ9/iKpiT6cM1JyEROpXjOcdWv8= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.9/go.mod h1:dB12CEbNWPbzO2uC6QSWHteqOg4JfBVJOojbAoAUb5I= -github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.9 h1:wuZ5uW2uhJR63zwNlqWH2W4aL4ZjeJP3o92/W+odDY4= -github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.9/go.mod h1:/G58M2fGszCrOzvJUkDdY8O9kycodunH4VdT5oBAqls= -github.com/aws/aws-sdk-go-v2/service/s3 v1.88.3 h1:P18I4ipbk+b/3dZNq5YYh+Hq6XC0vp5RWkLp1tJldDA= -github.com/aws/aws-sdk-go-v2/service/s3 v1.88.3/go.mod h1:Rm3gw2Jov6e6kDuamDvyIlZJDMYk97VeCZ82wz/mVZ0= -github.com/aws/aws-sdk-go-v2/service/sso v1.29.5 h1:WwL5YLHabIBuAlEKRoLgqLz1LxTvCEpwsQr7MiW/vnM= -github.com/aws/aws-sdk-go-v2/service/sso v1.29.5/go.mod h1:5PfYspyCU5Vw1wNPsxi15LZovOnULudOQuVxphSflQA= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.1 h1:5fm5RTONng73/QA73LhCNR7UT9RpFH3hR6HWL6bIgVY= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.1/go.mod h1:xBEjWD13h+6nq+z4AkqSfSvqRKFgDIQeaMguAJndOWo= -github.com/aws/aws-sdk-go-v2/service/sts v1.38.6 h1:p3jIvqYwUZgu/XYeI48bJxOhvm47hZb5HUQ0tn6Q9kA= -github.com/aws/aws-sdk-go-v2/service/sts v1.38.6/go.mod h1:WtKK+ppze5yKPkZ0XwqIVWD4beCwv056ZbPQNoeHqM8= -github.com/aws/smithy-go v1.23.0 h1:8n6I3gXzWJB2DxBDnfxgBaSX6oe0d/t10qGz7OKqMCE= -github.com/aws/smithy-go v1.23.0/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI= +github.com/aws/aws-sdk-go-v2 v1.41.5 h1:dj5kopbwUsVUVFgO4Fi5BIT3t4WyqIDjGKCangnV/yY= +github.com/aws/aws-sdk-go-v2 v1.41.5/go.mod h1:mwsPRE8ceUUpiTgF7QmQIJ7lgsKUPQOUl3o72QBrE1o= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8 h1:eBMB84YGghSocM7PsjmmPffTa+1FBUeNvGvFou6V/4o= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8/go.mod h1:lyw7GFp3qENLh7kwzf7iMzAxDn+NzjXEAGjKS2UOKqI= +github.com/aws/aws-sdk-go-v2/config v1.32.14 h1:opVIRo/ZbbI8OIqSOKmpFaY7IwfFUOCCXBsUpJOwDdI= +github.com/aws/aws-sdk-go-v2/config v1.32.14/go.mod h1:U4/V0uKxh0Tl5sxmCBZ3AecYny4UNlVmObYjKuuaiOo= +github.com/aws/aws-sdk-go-v2/credentials v1.19.14 h1:n+UcGWAIZHkXzYt87uMFBv/l8THYELoX6gVcUvgl6fI= +github.com/aws/aws-sdk-go-v2/credentials v1.19.14/go.mod h1:cJKuyWB59Mqi0jM3nFYQRmnHVQIcgoxjEMAbLkpr62w= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.21 h1:NUS3K4BTDArQqNu2ih7yeDLaS3bmHD0YndtA6UP884g= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.21/go.mod h1:YWNWJQNjKigKY1RHVJCuupeWDrrHjRqHm0N9rdrWzYI= +github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.22.12 h1:vhbHvVM9Til68SOR3Dds7zi51PaUlzexmh4Lf/uv+Ok= +github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.22.12/go.mod h1:jq4soyz7xX5bfkxVKQu1BwkopF2QbQUTs5n7iIg3D8Q= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21 h1:Rgg6wvjjtX8bNHcvi9OnXWwcE0a2vGpbwmtICOsvcf4= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21/go.mod h1:A/kJFst/nm//cyqonihbdpQZwiUhhzpqTsdbhDdRF9c= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21 h1:PEgGVtPoB6NTpPrBgqSE5hE/o47Ij9qk/SEZFbUOe9A= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21/go.mod h1:p+hz+PRAYlY3zcpJhPwXlLC4C+kqn70WIHwnzAfs6ps= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6 h1:qYQ4pzQ2Oz6WpQ8T3HvGHnZydA72MnLuFK9tJwmrbHw= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6/go.mod h1:O3h0IK87yXci+kg6flUKzJnWeziQUKciKrLjcatSNcY= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.22 h1:rWyie/PxDRIdhNf4DzRk0lvjVOqFJuNnO8WwaIRVxzQ= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.22/go.mod h1:zd/JsJ4P7oGfUhXn1VyLqaRZwPmZwg44Jf2dS84Dm3Y= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 h1:5EniKhLZe4xzL7a+fU3C2tfUN4nWIqlLesfrjkuPFTY= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7/go.mod h1:x0nZssQ3qZSnIcePWLvcoFisRXJzcTVvYpAAdYX8+GI= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.13 h1:JRaIgADQS/U6uXDqlPiefP32yXTda7Kqfx+LgspooZM= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.13/go.mod h1:CEuVn5WqOMilYl+tbccq8+N2ieCy0gVn3OtRb0vBNNM= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.21 h1:c31//R3xgIJMSC8S6hEVq+38DcvUlgFY0FM6mSI5oto= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.21/go.mod h1:r6+pf23ouCB718FUxaqzZdbpYFyDtehyZcmP5KL9FkA= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.21 h1:ZlvrNcHSFFWURB8avufQq9gFsheUgjVD9536obIknfM= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.21/go.mod h1:cv3TNhVrssKR0O/xxLJVRfd2oazSnZnkUeTf6ctUwfQ= +github.com/aws/aws-sdk-go-v2/service/s3 v1.98.0 h1:foqo/ocQ7WqKwy3FojGtZQJo0FR4vto9qnz9VaumbCo= +github.com/aws/aws-sdk-go-v2/service/s3 v1.98.0/go.mod h1:uoA43SdFwacedBfSgfFSjjCvYe8aYBS7EnU5GZ/YKMM= +github.com/aws/aws-sdk-go-v2/service/signin v1.0.9 h1:QKZH0S178gCmFEgst8hN0mCX1KxLgHBKKY/CLqwP8lg= +github.com/aws/aws-sdk-go-v2/service/signin v1.0.9/go.mod h1:7yuQJoT+OoH8aqIxw9vwF+8KpvLZ8AWmvmUWHsGQZvI= +github.com/aws/aws-sdk-go-v2/service/sso v1.30.15 h1:lFd1+ZSEYJZYvv9d6kXzhkZu07si3f+GQ1AaYwa2LUM= +github.com/aws/aws-sdk-go-v2/service/sso v1.30.15/go.mod h1:WSvS1NLr7JaPunCXqpJnWk1Bjo7IxzZXrZi1QQCkuqM= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.19 h1:dzztQ1YmfPrxdrOiuZRMF6fuOwWlWpD2StNLTceKpys= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.19/go.mod h1:YO8TrYtFdl5w/4vmjL8zaBSsiNp3w0L1FfKVKenZT7w= +github.com/aws/aws-sdk-go-v2/service/sts v1.41.10 h1:p8ogvvLugcR/zLBXTXrTkj0RYBUdErbMnAFFp12Lm/U= +github.com/aws/aws-sdk-go-v2/service/sts v1.41.10/go.mod h1:60dv0eZJfeVXfbT1tFJinbHrDfSJ2GZl4Q//OSSNAVw= +github.com/aws/smithy-go v1.24.2 h1:FzA3bu/nt/vDvmnkg+R8Xl46gmzEDam6mZ1hzmwXFng= +github.com/aws/smithy-go v1.24.2/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= -github.com/bubustack/bobrapet v0.0.0-20250926120003-fbd39089e093 h1:2yW8Ztbgd9zHMMxbq2JoJegirchidp0HN24tHRORs9A= -github.com/bubustack/bobrapet v0.0.0-20250926120003-fbd39089e093/go.mod h1:zTS1CLr6BrH/YyXsJUZrFZEmVdBFasd3W/03OgtBWWE= -github.com/bubustack/bobravoz-grpc v0.0.0-20250926120155-e667792a5283 h1:84wIPnKlmiXMcS5rra7wI350ODWzXaqtdE1JT+dtDe4= -github.com/bubustack/bobravoz-grpc v0.0.0-20250926120155-e667792a5283/go.mod h1:Tq99fRFQxi03+SEKBR5a9n+LCnGulxxXHlxZ+kOV46Y= -github.com/bubustack/bubu-sdk-go v0.0.0-20250928123735-9f239a97269a h1:wfyXt42KgeQk8GVq1+BVMzRc9ml/8+07e+MQfCOh26U= -github.com/bubustack/bubu-sdk-go v0.0.0-20250928123735-9f239a97269a/go.mod h1:LI96sLirt9AmQjx+9miIFfoe+FStrhHm+vyjJK+WPdk= +github.com/bubustack/bobrapet v0.1.6 h1:kf7A1GsQvpIBgWE8FIoYMke/nMpXgBAzw6+G2tB8HYM= +github.com/bubustack/bobrapet v0.1.6/go.mod h1:2eZ3mnhnvdO5Y1vkCrEDUfg0V9Menv76pdNIlfSEL/M= +github.com/bubustack/bubu-sdk-go v0.1.2 h1:VZTUaMoFxW0LKDTHvT21urtBy3UD7+HZyRYstNZYj7Y= +github.com/bubustack/bubu-sdk-go v0.1.2/go.mod h1:dWqhJcwgAP7NDSq+e1Qt906wQjwx4Z/Y/5EbwNAgD8I= +github.com/bubustack/core v0.1.3 h1:rFyj8EyC0agZZOOw9nGcirdNGqL5ArJUfEFPAAtdpb4= +github.com/bubustack/core v0.1.3/go.mod h1:UlEBsFdlyVdGVZVb9yfBoVM33DyxYQv3n921G1ll7Ng= +github.com/bubustack/tractatus v0.1.2 h1:PtcEisKiWGelflXB4NGtSl1O9G6BUXZ8xKNx4m2hMRQ= +github.com/bubustack/tractatus v0.1.2/go.mod h1:ku8Grbskvqov4CRHasQNJVsf1Ie/FZz9ak3Yap+vX8I= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -56,6 +70,8 @@ github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bF github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU= github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= @@ -97,8 +113,6 @@ github.com/go-openapi/swag/yamlutils v0.25.1 h1:mry5ez8joJwzvMbaTGLhw8pXUnhDK91o github.com/go-openapi/swag/yamlutils v0.25.1/go.mod h1:cm9ywbzncy3y6uPm/97ysW8+wZ09qsks+9RS8fLWKqg= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= -github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= -github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo= @@ -106,18 +120,24 @@ github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7O github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo= -github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= +github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83 h1:z2ogiKUYzX5Is6zr/vP9vJGqPwcdqsWjOt+V8J7+bTc= +github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83/go.mod h1:MxpfABSjhmINe3F1It9d+8exIHFvUqtLIRCdOGNXqiI= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI= +github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= -github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= -github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= +github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= +github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -126,26 +146,28 @@ github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFd github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= -github.com/onsi/ginkgo/v2 v2.22.0 h1:Yed107/8DjTr0lKCNt7Dn8yQ6ybuDRQoMGrNFKzMfHg= -github.com/onsi/ginkgo/v2 v2.22.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo= -github.com/onsi/gomega v1.36.1 h1:bJDPBO7ibjxcbHMgSCoo4Yj18UWbKDlLwX1x9sybDcw= -github.com/onsi/gomega v1.36.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= -github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= -github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/onsi/ginkgo/v2 v2.28.1 h1:S4hj+HbZp40fNKuLUQOYLDgZLwNUVn19N3Atb98NCyI= +github.com/onsi/ginkgo/v2 v2.28.1/go.mod h1:CLtbVInNckU3/+gC8LzkGUb9oF+e8W8TdUsxPwvdOgE= +github.com/onsi/gomega v1.39.1 h1:1IJLAad4zjPn2PsnhH70V4DKRFlrCzGBNrNaru+Vf28= +github.com/onsi/gomega v1.39.1/go.mod h1:hL6yVALoTOxeWudERyfppUcZXjMwIMLnuSfruD2lcfg= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q= -github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0= -github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= -github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= -github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io= -github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I= -github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= -github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= +github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= +github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= +github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs= +github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA= +github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= +github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= -github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= -github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= +github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= +github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w= +github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= +github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY= +github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= @@ -154,20 +176,28 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= -github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= +github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= +github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= -go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= -go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= -go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= -go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= -go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI= -go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg= -go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM= -go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA= -go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= -go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 h1:YH4g8lQroajqUwWbq/tr2QX1JFmEXaDLgG+ew9bLMWo= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0/go.mod h1:fvPi2qXDqFs8M4B4fmJhE92TyQs9Ydjlg3RvfUp+NbQ= +go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= +go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0= +go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM= +go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY= +go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg= +go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg= +go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8= +go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew= +go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A= +go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= @@ -176,83 +206,64 @@ go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I= -golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= -golang.org/x/oauth2 v0.31.0 h1:8Fq0yVZLh4j4YA47vHKFTa9Ew5XIrCP8LC6UeNZnLxo= -golang.org/x/oauth2 v0.31.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= -golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/term v0.35.0 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ= -golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= -golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= +golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4= +golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA= +golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8= +golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w= +golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0= +golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw= +golang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ= +golang.org/x/oauth2 v0.35.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= +golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= +golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= +golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU= +golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A= +golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= +golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= golang.org/x/time v0.13.0 h1:eUlYslOIt32DgYD6utsuUeHs4d7AsEYLuIAdg7FlYgI= golang.org/x/time v0.13.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg= -golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= -gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250922171735-9219d122eba9 h1:V1jCN2HBa8sySkR5vLcCSqJSTMv093Rw9EJefhQGP7M= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250922171735-9219d122eba9/go.mod h1:HSkG/KdJWusxU1F6CNrwNDjBMgisKxGnc5dAZfT0mjQ= -google.golang.org/grpc v1.75.1 h1:/ODCNEuf9VghjgO3rqLcfg8fiOP0nSluljWFlDxELLI= -google.golang.org/grpc v1.75.1/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ= -google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw= -google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= +golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k= +golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0= +gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= +gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 h1:m8qni9SQFH0tJc1X0vmnpw/0t+AImlSvp30sEupozUg= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM= +google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= -gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= +gopkg.in/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnfEbYzo= +gopkg.in/evanphx/json-patch.v4 v4.13.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -k8s.io/api v0.34.1 h1:jC+153630BMdlFukegoEL8E/yT7aLyQkIVuwhmwDgJM= -k8s.io/api v0.34.1/go.mod h1:SB80FxFtXn5/gwzCoN6QCtPD7Vbu5w2n1S0J5gFfTYk= -k8s.io/apiextensions-apiserver v0.34.0 h1:B3hiB32jV7BcyKcMU5fDaDxk882YrJ1KU+ZSkA9Qxoc= -k8s.io/apiextensions-apiserver v0.34.0/go.mod h1:hLI4GxE1BDBy9adJKxUxCEHBGZtGfIg98Q+JmTD7+g0= -k8s.io/apimachinery v0.34.1 h1:dTlxFls/eikpJxmAC7MVE8oOeP1zryV7iRyIjB0gky4= -k8s.io/apimachinery v0.34.1/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw= -k8s.io/client-go v0.34.1 h1:ZUPJKgXsnKwVwmKKdPfw4tB58+7/Ik3CrjOEhsiZ7mY= -k8s.io/client-go v0.34.1/go.mod h1:kA8v0FP+tk6sZA0yKLRG67LWjqufAoSHA2xVGKw9Of8= +k8s.io/api v0.35.3 h1:pA2fiBc6+N9PDf7SAiluKGEBuScsTzd2uYBkA5RzNWQ= +k8s.io/api v0.35.3/go.mod h1:9Y9tkBcFwKNq2sxwZTQh1Njh9qHl81D0As56tu42GA4= +k8s.io/apiextensions-apiserver v0.35.3 h1:2fQUhEO7P17sijylbdwt0nBdXP0TvHrHj0KeqHD8FiU= +k8s.io/apiextensions-apiserver v0.35.3/go.mod h1:tK4Kz58ykRpwAEkXUb634HD1ZAegEElktz/B3jgETd8= +k8s.io/apimachinery v0.35.3 h1:MeaUwQCV3tjKP4bcwWGgZ/cp/vpsRnQzqO6J6tJyoF8= +k8s.io/apimachinery v0.35.3/go.mod h1:jQCgFZFR1F4Ik7hvr2g84RTJSZegBc8yHgFWKn//hns= +k8s.io/client-go v0.35.3 h1:s1lZbpN4uI6IxeTM2cpdtrwHcSOBML1ODNTCCfsP1pg= +k8s.io/client-go v0.35.3/go.mod h1:RzoXkc0mzpWIDvBrRnD+VlfXP+lRzqQjCmKtiwZ8Q9c= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 h1:Y3gxNAuB0OBLImH611+UDZcmKS3g6CthxToOb37KgwE= k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ= -k8s.io/utils v0.0.0-20250820121507-0af2bda4dd1d h1:wAhiDyZ4Tdtt7e46e9M5ZSAJ/MnPGPs+Ki1gHw4w1R0= -k8s.io/utils v0.0.0-20250820121507-0af2bda4dd1d/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= -sigs.k8s.io/controller-runtime v0.22.1 h1:Ah1T7I+0A7ize291nJZdS1CabF/lB4E++WizgV24Eqg= -sigs.k8s.io/controller-runtime v0.22.1/go.mod h1:FwiwRjkRPbiN+zp2QRp7wlTCzbUXxZ/D4OzuQUDwBHY= +k8s.io/utils v0.0.0-20260319190234-28399d86e0b5 h1:kBawHLSnx/mYHmRnNUf9d4CpjREbeZuxoSGOX/J+aYM= +k8s.io/utils v0.0.0-20260319190234-28399d86e0b5/go.mod h1:xDxuJ0whA3d0I4mf/C4ppKHxXynQ+fxnkmQH0vTHnuk= +sigs.k8s.io/controller-runtime v0.23.3 h1:VjB/vhoPoA9l1kEKZHBMnQF33tdCLQKJtydy4iqwZ80= +sigs.k8s.io/controller-runtime v0.23.3/go.mod h1:B6COOxKptp+YaUT5q4l6LqUJTRpizbgf9KSRNdQGns0= sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg= sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= -sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco= -sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= +sigs.k8s.io/structured-merge-diff/v6 v6.3.2-0.20260122202528-d9cc6641c482 h1:2WOzJpHUBVrrkDjU4KBT8n5LDcj824eX0I5UKcgeRUs= +sigs.k8s.io/structured-merge-diff/v6 v6.3.2-0.20260122202528-d9cc6641c482/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/main.go b/main.go index 497690e..fff69c9 100644 --- a/main.go +++ b/main.go @@ -2,13 +2,20 @@ package main import ( "context" - "http-request/pkg/engram" + "log" + "os" + "os/signal" + "syscall" sdk "github.com/bubustack/bubu-sdk-go" + "github.com/bubustack/http-request-engram/pkg/engram" ) func main() { - if err := sdk.Start(context.Background(), engram.New()); err != nil { - panic(err) + ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) + defer cancel() + + if err := sdk.Start(ctx, engram.New()); err != nil { + log.Fatalf("http-request engram failed: %v", err) } } diff --git a/pkg/config/config.go b/pkg/config/config.go index 18a378b..6632b17 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -2,66 +2,66 @@ package config // Config holds the configuration for the HTTPRequestEngram. type Config struct { - DefaultURL string `json:"defaultURL"` - DefaultMethod string `json:"defaultMethod"` - DefaultHeaders map[string]string `json:"defaultHeaders"` - Auth *AuthConfig `json:"auth"` - Response *ResponseConfig `json:"response"` - NeverError bool `json:"neverError"` - Timeout string `json:"timeout"` - Redirects *RedirectsConfig `json:"redirects"` - IgnoreSSLIssues bool `json:"ignoreSSLIssues"` - Proxy string `json:"proxy"` - Batching *BatchingConfig `json:"batching"` - Pagination *PaginationConfig `json:"pagination"` - QueryParams *QueryParamsConfig `json:"queryParams"` + DefaultURL string `json:"defaultURL" mapstructure:"defaultURL"` + DefaultMethod string `json:"defaultMethod" mapstructure:"defaultMethod"` + DefaultHeaders map[string]string `json:"defaultHeaders" mapstructure:"defaultHeaders"` + Auth *AuthConfig `json:"auth" mapstructure:"auth"` + Response *ResponseConfig `json:"response" mapstructure:"response"` + NeverError bool `json:"neverError" mapstructure:"neverError"` + Timeout string `json:"timeout" mapstructure:"timeout"` + Redirects *RedirectsConfig `json:"redirects" mapstructure:"redirects"` + IgnoreSSLIssues bool `json:"ignoreSSLIssues" mapstructure:"ignoreSSLIssues"` + Proxy string `json:"proxy" mapstructure:"proxy"` + Batching *BatchingConfig `json:"batching" mapstructure:"batching"` + Pagination *PaginationConfig `json:"pagination" mapstructure:"pagination"` + QueryParams *QueryParamsConfig `json:"queryParams" mapstructure:"queryParams"` } // AuthConfig defines the structure for authentication settings. type AuthConfig struct { - Type string `json:"type"` - HeaderName string `json:"headerName"` + Type string `json:"type" mapstructure:"type"` + HeaderName string `json:"headerName" mapstructure:"headerName"` } // ResponseConfig defines how the HTTP response should be handled. type ResponseConfig struct { - IncludeHeaders bool `json:"includeHeaders"` - IncludeStatus bool `json:"includeStatus"` - Format string `json:"format"` - OutputFieldName string `json:"outputFieldName"` + IncludeHeaders bool `json:"includeHeaders" mapstructure:"includeHeaders"` + IncludeStatus bool `json:"includeStatus" mapstructure:"includeStatus"` + Format string `json:"format" mapstructure:"format"` + OutputFieldName string `json:"outputFieldName" mapstructure:"outputFieldName"` } // RedirectsConfig defines the behavior for handling HTTP redirects. type RedirectsConfig struct { - Follow bool `json:"follow"` - MaxRedirects int `json:"maxRedirects"` + Follow bool `json:"follow" mapstructure:"follow"` + MaxRedirects int `json:"maxRedirects" mapstructure:"maxRedirects"` } // BatchingConfig defines the behavior for batching requests in streaming mode. type BatchingConfig struct { - ItemsPerBatch int `json:"itemsPerBatch"` - BatchInterval string `json:"batchInterval"` + ItemsPerBatch int `json:"itemsPerBatch" mapstructure:"itemsPerBatch"` + BatchInterval string `json:"batchInterval" mapstructure:"batchInterval"` } // PaginationConfig defines the behavior for paginating API results. type PaginationConfig struct { - Mode string `json:"mode"` - ResultsPath string `json:"resultsPath"` - MaxPages int `json:"maxPages"` - NextURLPath string `json:"nextURLPath"` - UpdateParam *UpdateParamConfig `json:"updateParam"` + Mode string `json:"mode" mapstructure:"mode"` + ResultsPath string `json:"resultsPath" mapstructure:"resultsPath"` + MaxPages int `json:"maxPages" mapstructure:"maxPages"` + NextURLPath string `json:"nextURLPath" mapstructure:"nextURLPath"` + UpdateParam *UpdateParamConfig `json:"updateParam" mapstructure:"updateParam"` } // UpdateParamConfig defines how to update a query parameter for pagination. type UpdateParamConfig struct { - Name string `json:"name"` - Type string `json:"type"` - InitialValue string `json:"initialValue"` - Increment int `json:"increment"` - CursorPath string `json:"cursorPath"` + Name string `json:"name" mapstructure:"name"` + Type string `json:"type" mapstructure:"type"` + InitialValue string `json:"initialValue" mapstructure:"initialValue"` + Increment int `json:"increment" mapstructure:"increment"` + CursorPath string `json:"cursorPath" mapstructure:"cursorPath"` } // QueryParamsConfig defines how query parameters should be formatted. type QueryParamsConfig struct { - ArrayFormat string `json:"arrayFormat"` + ArrayFormat string `json:"arrayFormat" mapstructure:"arrayFormat"` } diff --git a/pkg/engram/debug.go b/pkg/engram/debug.go new file mode 100644 index 0000000..eafc533 --- /dev/null +++ b/pkg/engram/debug.go @@ -0,0 +1,62 @@ +package engram + +import ( + "context" + "log/slog" + "net/http" + "sort" + + sdk "github.com/bubustack/bubu-sdk-go" +) + +func (e *HTTPRequestEngram) debugEnabled(ctx context.Context, logger *slog.Logger) bool { + if sdk.DebugModeEnabled() { + return true + } + if logger == nil { + return false + } + if ctx == nil { + ctx = context.Background() + } + return logger.Enabled(ctx, slog.LevelDebug) +} + +func (e *HTTPRequestEngram) logHTTPRequest(ctx context.Context, logger *slog.Logger, req *http.Request) { + if !e.debugEnabled(ctx, logger) || req == nil { + return + } + headerKeys := make([]string, 0, len(req.Header)) + for key := range req.Header { + headerKeys = append(headerKeys, key) + } + sort.Strings(headerKeys) + logger.Debug("http request dispatch", + slog.String("method", req.Method), + slog.String("url", req.URL.String()), + slog.Int64("contentLength", req.ContentLength), + slog.Any("headers", headerKeys), + ) +} + +func (e *HTTPRequestEngram) logHTTPResponse( + ctx context.Context, + logger *slog.Logger, + resp *http.Response, + bodyBytes int, +) { + if !e.debugEnabled(ctx, logger) || resp == nil { + return + } + headerKeys := make([]string, 0, len(resp.Header)) + for key := range resp.Header { + headerKeys = append(headerKeys, key) + } + sort.Strings(headerKeys) + logger.Debug("http response received", + slog.Int("statusCode", resp.StatusCode), + slog.String("status", resp.Status), + slog.Int("bodyBytes", bodyBytes), + slog.Any("headers", headerKeys), + ) +} diff --git a/pkg/engram/engram.go b/pkg/engram/engram.go index 99a459c..6eb9794 100644 --- a/pkg/engram/engram.go +++ b/pkg/engram/engram.go @@ -1,14 +1,28 @@ +// package engram implements the core logic for the HTTP Request Engram. +// This engram is a versatile, feature-rich utility for interacting with HTTP APIs. +// +// As a masterpiece of the bobrapet ecosystem, it demonstrates several advanced +// architectural patterns: +// - Dual-Mode Implementation: It satisfies both the BatchEngram and StreamingEngram +// interfaces, allowing it to be used for single-shot requests (in Job mode) or +// as a persistent, rate-limited service (in Deployment mode). +// - Rich Configuration: It exposes a wide array of options for handling +// authentication, pagination, proxies, and response formats, making it a true +// "swiss army knife" for HTTP interactions. +// - Robust Error Handling: Features like `neverError` and detailed status +// reporting make it resilient and easy to debug in complex workflows. package engram import ( + "bytes" "context" "crypto/tls" "encoding/base64" "encoding/json" + "errors" "fmt" - "http-request/pkg/config" "io" - "log" + "log/slog" "net/http" "net/url" "strconv" @@ -16,23 +30,39 @@ import ( "sync" "time" + "github.com/bubustack/http-request-engram/pkg/config" + "github.com/PaesslerAG/jsonpath" + sdk "github.com/bubustack/bubu-sdk-go" "github.com/bubustack/bubu-sdk-go/engram" ) -// HTTPRequestEngram implements both BatchEngram and StreamingEngram interfaces. +// HTTPRequestEngram implements both the sdk.BatchEngram and sdk.StreamingEngram +// interfaces, making it a dual-mode component. It holds the static configuration +// and secrets provided at initialization time. type HTTPRequestEngram struct { - config *config.Config + config config.Config secrets *engram.Secrets } +var emitSignalFunc = sdk.EmitSignal + +// New creates a new instance of the HTTPRequestEngram. func New() *HTTPRequestEngram { return &HTTPRequestEngram{} } -// Init is called once when the Engram is initialized. -func (e *HTTPRequestEngram) Init(ctx context.Context, cfg *config.Config, secrets *engram.Secrets) error { +// Init is called once by the SDK runtime. It receives the static configuration +// from the EngramTemplate's `with` block and the resolved secrets. Its primary +// role is to validate the configuration and set sane defaults. +func (e *HTTPRequestEngram) Init(ctx context.Context, cfg config.Config, secrets *engram.Secrets) error { e.config = cfg + if ctx == nil { + ctx = context.Background() + } + if secrets == nil { + secrets = engram.NewSecrets(ctx, map[string]string{}) + } e.secrets = secrets // Store secrets for use in doRequest // Set sane defaults for pointer-based configs @@ -75,11 +105,18 @@ func (e *HTTPRequestEngram) Init(ctx context.Context, cfg *config.Config, secret return nil } -// Process handles the batch execution of the Engram. -func (e *HTTPRequestEngram) Process(ctx context.Context, execCtx *engram.ExecutionContext, i any) (*engram.Result, error) { - inputs, ok := i.(map[string]interface{}) +// Process handles the batch execution of the Engram (Job mode). It is the entry +// point for single, transactional HTTP requests. This method contains the complex +// logic for handling automatic pagination, which allows it to fetch a complete +// dataset from a paginated API in a single StepRun. +func (e *HTTPRequestEngram) Process( + ctx context.Context, + execCtx *engram.ExecutionContext, + i any, +) (*engram.Result, error) { + inputs, ok := i.(map[string]any) if !ok { - return &engram.Result{Error: fmt.Errorf("invalid input type, expected map[string]interface{}, got %T", i)}, nil + return nil, fmt.Errorf("invalid input type, expected map[string]any, got %T", i) } logger := execCtx.Logger() @@ -87,22 +124,35 @@ func (e *HTTPRequestEngram) Process(ctx context.Context, execCtx *engram.Executi // If pagination is off, just do a single request. if e.config.Pagination == nil || e.config.Pagination.Mode == "off" { - output, err := e.doRequest(ctx, inputs) + output, err := e.doRequest(ctx, logger, inputs) if err != nil { - return &engram.Result{Error: err}, nil + return nil, err } logger.Info("HTTP request completed successfully") - return &engram.Result{Data: output}, nil + return engram.NewResultFrom(output), nil } // --- Pagination Logic --- - allResults := make([]interface{}, 0) - var finalOutput map[string]interface{} - // Make a deep copy of the initial inputs to avoid modifying the original map. - currentInputs, err := deepCopyMap(inputs) + finalOutput, err := e.runPagination(ctx, logger, inputs) + if err != nil { + return nil, err + } + return engram.NewResultFrom(finalOutput), nil +} + +func (e *HTTPRequestEngram) runPagination( + ctx context.Context, + logger *slog.Logger, + inputs map[string]any, +) (map[string]any, error) { + allResults := make([]any, 0) + var finalOutput map[string]any + + currentInputs, err := cloneInputs(inputs) if err != nil { - return &engram.Result{Error: fmt.Errorf("failed to copy inputs for pagination: %w", err)}, nil + return nil, fmt.Errorf("clone pagination inputs: %w", err) } + pageCount := 0 paramState := "" if e.config.Pagination.UpdateParam != nil { @@ -110,57 +160,38 @@ func (e *HTTPRequestEngram) Process(ctx context.Context, execCtx *engram.Executi } for { - if pageCount > 0 { // Don't modify params on the first request + if pageCount > 0 { if err := e.updateRequestParams(currentInputs, paramState); err != nil { - return &engram.Result{Error: fmt.Errorf("failed to update pagination params: %w", err)}, nil + return nil, fmt.Errorf("update pagination params: %w", err) } } if pageCount >= e.config.Pagination.MaxPages { - logger.Warn("Pagination stopped: reached max pages limit", "limit", e.config.Pagination.MaxPages) + logger.Warn("Pagination stopped: reached max pages limit", + "limit", e.config.Pagination.MaxPages, + ) break } - var output map[string]interface{} - output, err = e.doRequest(ctx, currentInputs) - if err != nil { - // In pagination, an error on any page fails the whole process. - return &engram.Result{Error: fmt.Errorf("error on page %d: %w", pageCount+1, err)}, nil + output, reqErr := e.doRequest(ctx, logger, currentInputs) + if reqErr != nil { + return nil, fmt.Errorf("page %d request failed: %w", pageCount+1, reqErr) } - // On the last page, we'll use its full output as the base for our final result. finalOutput = output - // Extract results from the current page - var pageResults []interface{} bodyJSON, ok := output[e.config.Response.OutputFieldName] if !ok { - logger.Warn("Pagination: could not find body in response output", "field", e.config.Response.OutputFieldName) + logger.Warn("Pagination: missing body in response output", + "field", e.config.Response.OutputFieldName, + ) break } - if e.config.Pagination.ResultsPath != "" { - v, err := jsonpath.Get(e.config.Pagination.ResultsPath, bodyJSON) - if err != nil { - logger.Warn("Pagination: failed to get results from JSONPath", "path", e.config.Pagination.ResultsPath, "error", err) - break - } - if results, ok := v.([]interface{}); ok { - pageResults = results - } else { - logger.Warn("Pagination: resultsPath did not resolve to an array", "path", e.config.Pagination.ResultsPath) - break - } - } else { - // If no path, assume the whole body is the result set - if results, ok := bodyJSON.([]interface{}); ok { - pageResults = results - } else { - logger.Warn("Pagination: body is not an array and no resultsPath was provided") - // Add the single object to the results and stop. - allResults = append(allResults, bodyJSON) - break - } + pageResults, done, collectErr := e.collectPageResults(bodyJSON, logger) + if collectErr != nil { + logger.Warn("Pagination: failed to collect page results", "error", collectErr) + break } if len(pageResults) == 0 { @@ -169,91 +200,182 @@ func (e *HTTPRequestEngram) Process(ctx context.Context, execCtx *engram.Executi } allResults = append(allResults, pageResults...) + if done { + break + } pageCount++ - // --- Determine next request --- - var hasNext bool - var err error - hasNext, paramState, err = e.prepareNextRequest(currentInputs, bodyJSON, paramState) - if err != nil { - return &engram.Result{Error: fmt.Errorf("failed to prepare next pagination request: %w", err)}, nil + hasNext, nextState, nextErr := e.prepareNextRequest(currentInputs, bodyJSON, paramState) + if nextErr != nil { + return nil, fmt.Errorf("prepare next pagination request: %w", nextErr) } + paramState = nextState if !hasNext { break } } - // Replace the body of the final output with the aggregated results. if finalOutput != nil { finalOutput[e.config.Response.OutputFieldName] = allResults } - logger.Info("HTTP request with pagination completed successfully", "pagesFetched", pageCount, "totalResults", len(allResults)) - return &engram.Result{Data: finalOutput}, nil + logger.Info("HTTP request with pagination completed successfully", + "pagesFetched", pageCount, + "totalResults", len(allResults), + ) + return finalOutput, nil } -func (e *HTTPRequestEngram) prepareNextRequest(currentInputs map[string]interface{}, responseBody interface{}, currentParamState string) (bool, string, error) { - switch e.config.Pagination.Mode { - case "nextUrl": - if e.config.Pagination.NextURLPath == "" { - return false, "", fmt.Errorf("nextUrl mode selected but nextUrlPath is not configured") - } - nextURL, err := jsonpath.Get(e.config.Pagination.NextURLPath, responseBody) - if err != nil || nextURL == nil { - return false, "", nil // No next URL found, we're done. +func cloneInputs(src map[string]any) (map[string]any, error) { + if src == nil { + return nil, nil + } + encoded, err := json.Marshal(src) + if err != nil { + return nil, err + } + var out map[string]any + if err := json.Unmarshal(encoded, &out); err != nil { + return nil, err + } + return out, nil +} + +func (e *HTTPRequestEngram) collectPageResults( + body any, + logger *slog.Logger, +) ([]any, bool, error) { + if e.config.Pagination.ResultsPath != "" { + v, err := jsonpath.Get(e.config.Pagination.ResultsPath, body) + if err != nil { + return nil, true, fmt.Errorf("jsonpath %s: %w", e.config.Pagination.ResultsPath, err) } - if nextURLStr, ok := nextURL.(string); ok && nextURLStr != "" { - currentInputs["url"] = nextURLStr - // Remove query params if we are following a full URL - delete(currentInputs, "params") - return true, "", nil + results, ok := v.([]any) + if !ok { + return nil, true, fmt.Errorf("resultsPath did not resolve to an array") } - return false, "", nil + return results, false, nil + } + + if results, ok := body.([]any); ok { + return results, false, nil + } + + logger.Warn("Pagination: body is not an array and no resultsPath was provided") + return []any{body}, true, nil +} +func (e *HTTPRequestEngram) prepareNextRequest( + currentInputs map[string]any, + responseBody any, + currentParamState string, +) (bool, string, error) { + switch e.config.Pagination.Mode { + case "nextUrl": + return e.prepareNextURLRequest(currentInputs, responseBody) case "updateParam": - p := e.config.Pagination.UpdateParam - if p == nil { - return false, "", fmt.Errorf("updateParam mode selected but no config provided") - } + return e.prepareUpdateParamRequest(responseBody, currentParamState) + default: + return false, "", nil + } +} - switch p.Type { - case "page", "offset": - val, err := strconv.Atoi(currentParamState) - if err != nil { - return false, "", fmt.Errorf("could not parse current param value '%s' as integer: %w", currentParamState, err) - } - nextVal := val + p.Increment - return true, strconv.Itoa(nextVal), nil - case "cursor": - if p.CursorPath == "" { - return false, "", fmt.Errorf("cursor type selected but cursorPath is not configured") - } - nextCursor, err := jsonpath.Get(p.CursorPath, responseBody) - if err != nil || nextCursor == nil { - return false, "", nil // No cursor found, we're done. - } - if nextCursorStr, ok := nextCursor.(string); ok && nextCursorStr != "" { - return true, nextCursorStr, nil - } +func (e *HTTPRequestEngram) prepareNextURLRequest( + currentInputs map[string]any, + responseBody any, +) (bool, string, error) { + if e.config.Pagination.NextURLPath == "" { + return false, "", fmt.Errorf("nextUrl mode selected but nextUrlPath is not configured") + } + + nextURL, err := jsonpath.Get(e.config.Pagination.NextURLPath, responseBody) + if err != nil || nextURL == nil { + return false, "", nil + } + + nextURLStr, ok := nextURL.(string) + if !ok || nextURLStr == "" { + return false, "", nil + } + + currentURLRaw, err := e.resolveURL(currentInputs) + if err != nil { + return false, "", fmt.Errorf("resolve current page URL: %w", err) + } + currentURL, err := url.Parse(currentURLRaw) + if err != nil { + return false, "", fmt.Errorf("parse current page URL %q: %w", currentURLRaw, err) + } + nextParsed, err := url.Parse(nextURLStr) + if err != nil { + return false, "", fmt.Errorf("parse next page URL %q: %w", nextURLStr, err) + } + resolvedNext := currentURL.ResolveReference(nextParsed) + if !sameOrigin(currentURL, resolvedNext) { + return false, "", fmt.Errorf( + "pagination nextUrl origin change is not allowed: %s -> %s", + currentURL.Host, + resolvedNext.Host, + ) + } + + currentInputs["url"] = resolvedNext.String() + delete(currentInputs, "params") + return true, "", nil +} + +func sameOrigin(base *url.URL, next *url.URL) bool { + if base == nil || next == nil { + return false + } + return strings.EqualFold(base.Scheme, next.Scheme) && strings.EqualFold(base.Host, next.Host) +} + +func (e *HTTPRequestEngram) prepareUpdateParamRequest( + responseBody any, + currentParamState string, +) (bool, string, error) { + p := e.config.Pagination.UpdateParam + if p == nil { + return false, "", fmt.Errorf("updateParam mode selected but no config provided") + } + + switch p.Type { + case "page", "offset": + val, err := strconv.Atoi(currentParamState) + if err != nil { + return false, "", fmt.Errorf("parse current param value '%s': %w", currentParamState, err) + } + nextVal := val + p.Increment + return true, strconv.Itoa(nextVal), nil + case "cursor": + if p.CursorPath == "" { + return false, "", fmt.Errorf("cursor type selected but cursorPath is not configured") + } + nextCursor, err := jsonpath.Get(p.CursorPath, responseBody) + if err != nil || nextCursor == nil { return false, "", nil } - + nextCursorStr, ok := nextCursor.(string) + if !ok || nextCursorStr == "" { + return false, "", nil + } + return true, nextCursorStr, nil default: - return false, "", nil + return false, "", fmt.Errorf("unsupported updateParam type %q", p.Type) } - return false, "", nil } -func (e *HTTPRequestEngram) updateRequestParams(inputs map[string]interface{}, paramValue string) error { +func (e *HTTPRequestEngram) updateRequestParams(inputs map[string]any, paramValue string) error { p := e.config.Pagination.UpdateParam if p == nil || p.Name == "" { return fmt.Errorf("updateParam config is missing or invalid") } // Params are expected to be in a specific structure within the inputs map. - params, ok := inputs["params"].(map[string]interface{}) + params, ok := inputs["params"].(map[string]any) if !ok { - params = make(map[string]interface{}) + params = make(map[string]any) } params[p.Name] = paramValue inputs["params"] = params @@ -261,173 +383,233 @@ func (e *HTTPRequestEngram) updateRequestParams(inputs map[string]interface{}, p return nil } -// deepCopyMap creates a deep copy of a map[string]interface{}. -func deepCopyMap(original map[string]interface{}) (map[string]interface{}, error) { - newMap := make(map[string]interface{}) - bytes, err := json.Marshal(original) - if err != nil { - return nil, err - } - err = json.Unmarshal(bytes, &newMap) - if err != nil { - return nil, err - } - return newMap, nil -} - -// Stream handles the streaming execution of the Engram. -func (e *HTTPRequestEngram) Stream(ctx context.Context, in <-chan []byte, out chan<- []byte) error { +// Stream handles the streaming execution of the Engram (Deployment mode). It runs +// a persistent loop that receives input data over a channel, makes HTTP requests, +// and sends the results back on an output channel. This implementation includes +// sophisticated batching and rate-limiting logic to avoid overwhelming a downstream API. +func (e *HTTPRequestEngram) Stream( + ctx context.Context, + in <-chan engram.InboundMessage, + out chan<- engram.StreamMessage, +) error { + logger := sdk.LoggerFromContext(ctx).With( + "component", "http-request-engram", + "mode", "stream", + ) interval, err := time.ParseDuration(e.config.Batching.BatchInterval) - if err != nil { - interval = 1 * time.Second // Fallback - log.Printf("Invalid batchInterval '%s', falling back to 1s: %v", e.config.Batching.BatchInterval, err) + if err != nil || interval <= 0 { + interval = time.Second + logger.Warn("invalid batch interval, using 1s fallback", + slog.String("configured", e.config.Batching.BatchInterval), + slog.Any("parseError", err), + ) } batchSize := e.config.Batching.ItemsPerBatch if batchSize <= 0 { - batchSize = 50 // Fallback + batchSize = 50 } - var wg sync.WaitGroup - batchCounter := 0 - - for data := range in { - // If context is cancelled, we stop accepting new items. - if ctx.Err() != nil { - break - } + var ( + wg sync.WaitGroup + batchCount int + ) + workerCtx, cancelWorkers := context.WithCancel(ctx) + defer cancelWorkers() + errCh := make(chan error, 1) - wg.Add(1) - go func(d []byte) { - defer wg.Done() - var inputs map[string]interface{} - if err := json.Unmarshal(d, &inputs); err != nil { - log.Printf("Error unmarshaling input: %v", err) - return // Don't process this item + for { + select { + case err := <-errCh: + cancelWorkers() + wg.Wait() + return err + case <-ctx.Done(): + cancelWorkers() + wg.Wait() + return ctx.Err() + case msg, ok := <-in: + if !ok { + cancelWorkers() + wg.Wait() + return nil } - gCtx, cancel := context.WithCancel(ctx) - defer cancel() - - output, err := e.doRequest(gCtx, inputs) - if err != nil { - log.Printf("Error processing request: %v", err) - return // Don't send output for this item - } + e.launchStreamWorker(workerCtx, logger, &wg, msg, out, errCh) + batchCount++ - outputBytes, err := json.Marshal(output) - if err != nil { - log.Printf("Error marshaling output: %v", err) - return // Don't send output for this item + if batchCount >= batchSize { + if err := e.waitForBatchDrain(ctx, &wg, interval); err != nil { + return err + } + batchCount = 0 } - out <- outputBytes - }(data) - - batchCounter++ - - if batchCounter >= batchSize { - wg.Wait() // Wait for the current batch to finish processing. - batchCounter = 0 // Reset for the next batch. + } + } +} - select { - case <-time.After(interval): - case <-ctx.Done(): - break +func (e *HTTPRequestEngram) launchStreamWorker( + ctx context.Context, + logger *slog.Logger, + wg *sync.WaitGroup, + msg engram.InboundMessage, + out chan<- engram.StreamMessage, + errCh chan<- error, +) { + message := cloneStreamMessage(msg) + + wg.Add(1) + go func() { + defer wg.Done() + defer message.Done() + + response, emit, err := e.processStreamMessage(ctx, logger, message) + if err != nil { + if ctx.Err() == nil { + select { + case errCh <- err: + default: + } } + return + } + if !emit { + return } - } - wg.Wait() - return ctx.Err() + select { + case out <- response: + case <-ctx.Done(): + } + }() } -func (e *HTTPRequestEngram) doRequest(ctx context.Context, inputs map[string]interface{}) (map[string]interface{}, error) { - // --- 1. Get and validate inputs --- - url, ok := inputs["url"].(string) - if !ok || url == "" { - url = e.config.DefaultURL - } - if url == "" { - return nil, fmt.Errorf("input 'url' is a required string") +func (e *HTTPRequestEngram) waitForBatchDrain(ctx context.Context, wg *sync.WaitGroup, interval time.Duration) error { + wg.Wait() + select { + case <-time.After(interval): + return nil + case <-ctx.Done(): + return ctx.Err() } +} - method, ok := inputs["method"].(string) - if !ok || method == "" { - method = e.config.DefaultMethod +func (e *HTTPRequestEngram) processStreamMessage( + ctx context.Context, + logger *slog.Logger, + message engram.InboundMessage, +) (engram.StreamMessage, bool, error) { + inputs, err := decodeStreamInputs(message) + if err != nil { + return engram.StreamMessage{}, false, err } - method = strings.ToUpper(method) - - // --- 2. Build the HTTP request --- - var bodyReader io.Reader - if body, ok := inputs["body"]; ok { - if bodyStr, isString := body.(string); isString { - bodyReader = strings.NewReader(bodyStr) + if inputs == nil { + if logger != nil { + logger.Warn("stream message missing inputs and payload; skipping", + slog.Any("metadata", message.Metadata), + ) } + return engram.StreamMessage{}, false, nil } - req, err := http.NewRequestWithContext(ctx, method, url, bodyReader) + output, err := e.doRequest(ctx, logger, inputs) if err != nil { - return nil, fmt.Errorf("failed to create http request: %w", err) + return engram.StreamMessage{}, false, err } - // Add query params - if params, ok := inputs["params"].(map[string]interface{}); ok { - q := req.URL.Query() - for key, val := range params { - // Handle array values according to the configured format - if arr, isArr := val.([]interface{}); isArr { - switch e.config.QueryParams.ArrayFormat { - case "brackets": - for _, v := range arr { - q.Add(key+"[]", fmt.Sprintf("%v", v)) - } - case "indices": - for i, v := range arr { - q.Add(fmt.Sprintf("%s[%d]", key, i), fmt.Sprintf("%v", v)) - } - case "noBrackets": - fallthrough - default: - for _, v := range arr { - q.Add(key, fmt.Sprintf("%v", v)) - } - } - } else { - q.Add(key, fmt.Sprintf("%v", val)) - } - } - req.URL.RawQuery = q.Encode() + encoded, err := json.Marshal(output) + if err != nil { + return engram.StreamMessage{}, false, fmt.Errorf("marshal stream output: %w", err) } - // Add headers from config - for key, val := range e.config.DefaultHeaders { - req.Header.Set(key, val) + return engram.StreamMessage{ + Metadata: cloneStreamMetadata(message.Metadata), + Payload: encoded, + Binary: &engram.BinaryFrame{ + Payload: encoded, + MimeType: "application/json", + }, + }, true, nil +} + +func decodeStreamInputs(message engram.InboundMessage) (map[string]any, error) { + raw := message.Inputs + if len(raw) == 0 && len(message.Payload) > 0 { + raw = message.Payload + } + if len(raw) == 0 && message.Binary != nil { + raw = message.Binary.Payload + } + if len(raw) == 0 { + return nil, nil } - // Add headers from input (overwriting config headers) - if headers, ok := inputs["headers"].(map[string]interface{}); ok { - for key, val := range headers { - if valStr, isString := val.(string); isString { - req.Header.Set(key, valStr) - } - } + var inputs map[string]any + if err := json.Unmarshal(raw, &inputs); err != nil { + return nil, fmt.Errorf("unmarshal stream inputs: %w", err) } + return inputs, nil +} - // Apply authentication - if e.config.Auth != nil { - // Don't apply auth if Authorization header is already set, except for custom headers - if req.Header.Get("Authorization") == "" || e.config.Auth.Type == "customHeader" { - if err := e.applyAuth(req); err != nil { - return nil, err - } - } +func cloneStreamMessage(msg engram.InboundMessage) engram.InboundMessage { + clone := msg + clone.Metadata = cloneStreamMetadata(msg.Metadata) + if msg.Binary != nil && len(msg.Binary.Payload) > 0 { + clone.Binary = &engram.BinaryFrame{ + Payload: append([]byte(nil), msg.Binary.Payload...), + MimeType: msg.Binary.MimeType, + } + } else { + clone.Binary = nil + } + if len(msg.Inputs) > 0 { + clone.Inputs = make([]byte, len(msg.Inputs)) + copy(clone.Inputs, msg.Inputs) + } else { + clone.Inputs = nil + } + if len(msg.Payload) > 0 { + clone.Payload = make([]byte, len(msg.Payload)) + copy(clone.Payload, msg.Payload) + } else { + clone.Payload = nil + } + return clone +} + +func cloneStreamMetadata(meta map[string]string) map[string]string { + if len(meta) == 0 { + return nil } + out := make(map[string]string, len(meta)) + for k, v := range meta { + out[k] = v + } + return out +} - if req.Header.Get("User-Agent") == "" { - req.Header.Set("User-Agent", "bubu-http-request-engram/v0.2") +// doRequest is the heart of the engram, containing the logic for a single HTTP +// request. It is called by both `Process` and `Stream`. This function is +// responsible for: +// 1. Assembling the request: Merging default and runtime URLs, headers, and params. +// 2. Applying authentication: Injecting bearer tokens, basic auth, or custom headers. +// 3. Building the HTTP client: Configuring timeouts, proxies, and redirect policies. +// 4. Executing the request and processing the response into the final output structure. +func (e *HTTPRequestEngram) doRequest( + ctx context.Context, + logger *slog.Logger, + inputs map[string]any, +) (map[string]any, error) { + if logger == nil { + logger = sdk.LoggerFromContext(ctx) + } + start := time.Now() + req, err := e.buildHTTPRequest(ctx, inputs) + if err != nil { + return nil, err } + e.logHTTPRequest(ctx, logger, req) - // --- 3. Execute the request --- client, err := e.buildClient() if err != nil { return nil, fmt.Errorf("failed to build http client: %w", err) @@ -436,30 +618,169 @@ func (e *HTTPRequestEngram) doRequest(ctx context.Context, inputs map[string]int resp, err := client.Do(req) if err != nil { if e.config.NeverError { - return map[string]interface{}{"error": err.Error()}, nil + return map[string]any{"error": err.Error()}, nil } return nil, fmt.Errorf("failed to execute http request: %w", err) } - defer resp.Body.Close() + defer func() { + if cerr := resp.Body.Close(); cerr != nil { + logger.Warn("error closing HTTP response body", slog.Any("error", cerr)) + } + }() - if resp.StatusCode < 200 || resp.StatusCode >= 300 { + body, err := io.ReadAll(resp.Body) + if err != nil { + if e.config.NeverError { + return map[string]any{"error": fmt.Sprintf("failed to read response body: %v", err)}, nil + } + return nil, fmt.Errorf("failed to read response body: %w", err) + } + + if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices { if !e.config.NeverError { - respBody, _ := io.ReadAll(resp.Body) - return nil, fmt.Errorf("http request failed with status %s: %s", resp.Status, string(respBody)) + return nil, fmt.Errorf("http request failed with status %s: %s", resp.Status, string(body)) } } + e.logHTTPResponse(ctx, logger, resp, len(body)) - // --- 4. Process the response --- - respBody, err := io.ReadAll(resp.Body) + output, err := e.buildResponsePayload(resp, body) if err != nil { - if e.config.NeverError { - return map[string]interface{}{"error": fmt.Sprintf("failed to read response body: %v", err)}, nil + return nil, err + } + e.emitResponseSignal(ctx, logger, req, resp, len(body), time.Since(start), output) + return output, nil +} + +func (e *HTTPRequestEngram) buildHTTPRequest( + ctx context.Context, + inputs map[string]any, +) (*http.Request, error) { + targetURL, err := e.resolveURL(inputs) + if err != nil { + return nil, err + } + + method := e.resolveMethod(inputs) + bodyReader, err := prepareBodyReader(inputs) + if err != nil { + return nil, err + } + + req, err := http.NewRequestWithContext(ctx, method, targetURL, bodyReader) + if err != nil { + return nil, fmt.Errorf("failed to create http request: %w", err) + } + + e.applyQueryParams(req, inputs) + e.applyHeaders(req, inputs) + e.ensureUserAgent(req) + + if e.shouldApplyAuth(req) { + if err := e.applyAuth(req); err != nil { + return nil, err } - return nil, fmt.Errorf("failed to read response body: %w", err) } - // --- 5. Return the output --- - output := make(map[string]interface{}) + return req, nil +} + +func (e *HTTPRequestEngram) resolveURL(inputs map[string]any) (string, error) { + if raw, ok := inputs["url"].(string); ok && raw != "" { + return raw, nil + } + if e.config.DefaultURL != "" { + return e.config.DefaultURL, nil + } + return "", fmt.Errorf("input 'url' is a required string") +} + +func (e *HTTPRequestEngram) resolveMethod(inputs map[string]any) string { + if method, ok := inputs["method"].(string); ok && method != "" { + return strings.ToUpper(method) + } + return strings.ToUpper(e.config.DefaultMethod) +} + +func prepareBodyReader(inputs map[string]any) (io.Reader, error) { + body, ok := inputs["body"] + if !ok || body == nil { + return nil, nil + } + switch typed := body.(type) { + case string: + return strings.NewReader(typed), nil + case []byte: + return bytes.NewReader(typed), nil + default: + return nil, fmt.Errorf("input 'body' must be a string or []byte, got %T", body) + } +} + +func (e *HTTPRequestEngram) applyQueryParams(req *http.Request, inputs map[string]any) { + params, ok := inputs["params"].(map[string]any) + if !ok || len(params) == 0 { + return + } + + query := req.URL.Query() + for key, val := range params { + if arr, ok := val.([]any); ok { + e.appendArrayParam(query, key, arr) + continue + } + query.Add(key, fmt.Sprintf("%v", val)) + } + req.URL.RawQuery = query.Encode() +} + +func (e *HTTPRequestEngram) appendArrayParam(values url.Values, key string, arr []any) { + for idx, item := range arr { + value := fmt.Sprintf("%v", item) + switch e.config.QueryParams.ArrayFormat { + case "brackets": + values.Add(key+"[]", value) + case "indices": + values.Add(fmt.Sprintf("%s[%d]", key, idx), value) + default: + values.Add(key, value) + } + } +} + +func (e *HTTPRequestEngram) applyHeaders(req *http.Request, inputs map[string]any) { + for key, val := range e.config.DefaultHeaders { + req.Header.Set(key, val) + } + + headers, ok := inputs["headers"].(map[string]any) + if !ok { + return + } + for key, val := range headers { + if strVal, ok := val.(string); ok { + req.Header.Set(key, strVal) + } + } +} + +func (e *HTTPRequestEngram) ensureUserAgent(req *http.Request) { + if req.Header.Get("User-Agent") == "" { + req.Header.Set("User-Agent", "bubu-http-request-engram/v0.2") + } +} + +func (e *HTTPRequestEngram) shouldApplyAuth(req *http.Request) bool { + if e.config.Auth == nil { + return false + } + if e.config.Auth.Type == "customHeader" { + return true + } + return req.Header.Get("Authorization") == "" +} + +func (e *HTTPRequestEngram) buildResponsePayload(resp *http.Response, body []byte) (map[string]any, error) { + output := make(map[string]any) if e.config.Response.IncludeStatus { output["statusCode"] = resp.StatusCode @@ -467,45 +788,122 @@ func (e *HTTPRequestEngram) doRequest(ctx context.Context, inputs map[string]int } if e.config.Response.IncludeHeaders { - respHeaders := make(map[string]interface{}) - for key, values := range resp.Header { - respHeaders[key] = strings.Join(values, ", ") + output["headers"] = flattenHeaders(resp.Header) + } + + field := e.outputFieldName() + value, err := e.formatResponseBody(resp, body) + if err != nil { + if !e.config.NeverError { + return nil, err } - output["headers"] = respHeaders + output[field] = string(body) + output["error"] = map[string]any{"message": err.Error()} + } else { + output[field] = value } - outputFieldName := e.config.Response.OutputFieldName - if outputFieldName == "" { - outputFieldName = "body" + if resp.StatusCode >= http.StatusBadRequest { + message := strings.TrimSpace(string(body)) + if e.config.NeverError { + output["error"] = map[string]any{ + "statusCode": resp.StatusCode, + "status": resp.Status, + "message": message, + } + } else { + return nil, fmt.Errorf("http request failed with status %s: %s", resp.Status, message) + } } + return output, nil +} + +func (e *HTTPRequestEngram) formatResponseBody(resp *http.Response, body []byte) (any, error) { switch e.config.Response.Format { case "json": - var jsonData interface{} - if err := json.Unmarshal(respBody, &jsonData); err != nil { - if e.config.NeverError { - output[outputFieldName] = string(respBody) - output["error"] = "failed to parse response body as JSON" - } else { - return nil, fmt.Errorf("failed to parse response body as JSON: %w", err) + var out any + if err := json.Unmarshal(body, &out); err != nil { + return nil, fmt.Errorf("failed to parse response body as JSON: %w", err) + } + return out, nil + case "text": + return string(body), nil + case "file": + file := map[string]any{ + "encoding": "base64", + "data": base64.StdEncoding.EncodeToString(body), + "sizeBytes": len(body), + } + if resp != nil { + if contentType := strings.TrimSpace(resp.Header.Get("Content-Type")); contentType != "" { + file["contentType"] = contentType } - } else { - output[outputFieldName] = jsonData } - case "text", "file": - output[outputFieldName] = string(respBody) + return file, nil case "auto": - fallthrough + var out any + if err := json.Unmarshal(body, &out); err == nil { + return out, nil + } + return string(body), nil default: - var jsonData interface{} - if err := json.Unmarshal(respBody, &jsonData); err == nil { - output[outputFieldName] = jsonData - } else { - output[outputFieldName] = string(respBody) + return string(body), nil + } +} + +func (e *HTTPRequestEngram) emitResponseSignal( + ctx context.Context, + logger *slog.Logger, + req *http.Request, + resp *http.Response, + bodyBytes int, + latency time.Duration, + output map[string]any, +) { + if req == nil || resp == nil { + return + } + payload := map[string]any{ + "method": strings.ToUpper(req.Method), + "url": req.URL.String(), + "statusCode": resp.StatusCode, + "status": resp.Status, + "durationMs": latency.Milliseconds(), + "bodyBytes": bodyBytes, + } + if ct := resp.Header.Get("Content-Type"); ct != "" { + payload["contentType"] = ct + } + if output != nil { + if errVal, ok := output["error"]; ok { + payload["error"] = errVal + } + } + if err := emitSignalFunc(ctx, "http.response", payload); err != nil && !errors.Is(err, sdk.ErrSignalsUnavailable) { + if logger == nil { + logger = sdk.LoggerFromContext(ctx) } + logger.Warn("http-request: failed to emit response signal", slog.Any("error", err)) } +} - return output, nil +func (e *HTTPRequestEngram) outputFieldName() string { + if e.config.Response.OutputFieldName != "" { + return e.config.Response.OutputFieldName + } + return "body" +} + +func flattenHeaders(headers http.Header) map[string]string { + if len(headers) == 0 { + return nil + } + out := make(map[string]string, len(headers)) + for key, values := range headers { + out[key] = strings.Join(values, ", ") + } + return out } func (e *HTTPRequestEngram) buildClient() (*http.Client, error) { @@ -524,8 +922,8 @@ func (e *HTTPRequestEngram) buildClient() (*http.Client, error) { return nil, fmt.Errorf("invalid proxy URL: %w", err) } - proxyUser, uOk := e.secrets.Get("proxy_username") - proxyPass, pOk := e.secrets.Get("proxy_password") + proxyUser, uOk := e.getSecret("proxy_username") + proxyPass, pOk := e.getSecret("proxy_password") if uOk && pOk { proxyURL.User = url.UserPassword(proxyUser, proxyPass) } @@ -542,22 +940,18 @@ func (e *HTTPRequestEngram) buildClient() (*http.Client, error) { return http.ErrUseLastResponse } } else if e.config.Redirects != nil { - redirectCount := 0 client.CheckRedirect = func(req *http.Request, via []*http.Request) error { - if redirectCount >= e.config.Redirects.MaxRedirects { + if len(via) >= e.config.Redirects.MaxRedirects { return fmt.Errorf("stopped after %d redirects", e.config.Redirects.MaxRedirects) } + // Prevent credential leakage on cross-domain redirects. if len(via) > 0 { lastReq := via[len(via)-1] if req.URL.Host != lastReq.URL.Host { - if authHeader := lastReq.Header.Get("Authorization"); authHeader != "" { - req.Header.Set("Authorization", authHeader) - } + req.Header.Del("Authorization") } } - - redirectCount++ return nil } } @@ -572,12 +966,12 @@ func (e *HTTPRequestEngram) applyAuth(req *http.Request) error { switch e.config.Auth.Type { case "bearer": - if token, ok := e.secrets.Get("bearer_token"); ok { + if token, ok := e.getSecret("bearer_token"); ok { req.Header.Set("Authorization", "Bearer "+token) } case "basic": - user, uOk := e.secrets.Get("basic_username") - pass, pOk := e.secrets.Get("basic_password") + user, uOk := e.getSecret("basic_username") + pass, pOk := e.getSecret("basic_password") if uOk && pOk { auth := base64.StdEncoding.EncodeToString([]byte(user + ":" + pass)) req.Header.Set("Authorization", "Basic "+auth) @@ -586,7 +980,7 @@ func (e *HTTPRequestEngram) applyAuth(req *http.Request) error { if e.config.Auth.HeaderName == "" { return fmt.Errorf("auth type 'customHeader' requires 'headerName' in config") } - if value, ok := e.secrets.Get("custom_header_value"); ok { + if value, ok := e.getSecret("custom_header_value"); ok { if req.Header.Get(e.config.Auth.HeaderName) == "" { req.Header.Set(e.config.Auth.HeaderName, value) } @@ -594,3 +988,10 @@ func (e *HTTPRequestEngram) applyAuth(req *http.Request) error { } return nil } + +func (e *HTTPRequestEngram) getSecret(key string) (string, bool) { + if e == nil || e.secrets == nil { + return "", false + } + return e.secrets.Get(key) +} diff --git a/pkg/engram/engram_test.go b/pkg/engram/engram_test.go new file mode 100644 index 0000000..48c97fb --- /dev/null +++ b/pkg/engram/engram_test.go @@ -0,0 +1,313 @@ +package engram + +import ( + "context" + "encoding/base64" + "encoding/json" + "log/slog" + "net/http" + "net/http/httptest" + "strings" + "sync" + "testing" + "time" + + sdkengram "github.com/bubustack/bubu-sdk-go/engram" + "github.com/bubustack/http-request-engram/pkg/config" +) + +func TestProcessStreamMessageMirrorsPayloadToBinary(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`{"ok":true}`)) + })) + defer server.Close() + + engine := New() + if err := engine.Init(context.Background(), config.Config{}, nil); err != nil { + t.Fatalf("Init returned error: %v", err) + } + + msg, ok, err := engine.processStreamMessage( + context.Background(), + slog.Default(), + sdkengram.InboundMessage{ + StreamMessage: sdkengram.StreamMessage{ + Inputs: []byte(`{"url":"` + server.URL + `"}`), + }, + }, + ) + if err != nil { + t.Fatalf("processStreamMessage returned error: %v", err) + } + if !ok { + t.Fatal("expected processStreamMessage to emit an output") + } + if msg.Binary == nil { + t.Fatal("expected binary payload") + } + if string(msg.Payload) != string(msg.Binary.Payload) { + t.Fatalf("expected mirrored payload, payload=%q binary=%q", string(msg.Payload), string(msg.Binary.Payload)) + } + var output map[string]any + if err := json.Unmarshal(msg.Payload, &output); err != nil { + t.Fatalf("failed to decode output payload: %v", err) + } + if output == nil { + t.Fatal("expected structured output payload") + } +} + +func TestDecodeStreamInputsPrefersPayloadOverBinary(t *testing.T) { + inputs, err := decodeStreamInputs( + sdkengram.NewInboundMessage(sdkengram.StreamMessage{ + Payload: []byte(`{"url":"https://payload.example"}`), + Binary: &sdkengram.BinaryFrame{ + Payload: []byte(`{"url":"https://binary.example"}`), + MimeType: "application/json", + }, + }), + ) + if err != nil { + t.Fatalf("decodeStreamInputs returned error: %v", err) + } + if inputs["url"] != "https://payload.example" { + t.Fatalf("expected payload URL to win, got %#v", inputs["url"]) + } +} + +func TestAuthAndProxyHandleNilSecrets(t *testing.T) { + engine := &HTTPRequestEngram{ + config: config.Config{ + Proxy: "http://proxy.example:8080", + Auth: &config.AuthConfig{Type: "bearer"}, + }, + // secrets intentionally left nil + } + + client, err := engine.buildClient() + if err != nil { + t.Fatalf("buildClient returned error: %v", err) + } + if client == nil { + t.Fatal("expected client") + } + + req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, "https://example.com", nil) + if err != nil { + t.Fatalf("failed to build request: %v", err) + } + if err := engine.applyAuth(req); err != nil { + t.Fatalf("applyAuth returned error: %v", err) + } + if got := req.Header.Get("Authorization"); got != "" { + t.Fatalf("expected Authorization to stay empty without secrets, got %q", got) + } +} + +func TestFormatResponseBodyFileReturnsStructuredBlob(t *testing.T) { + const bodyText = "hello file" + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/pdf") + _, _ = w.Write([]byte(bodyText)) + })) + defer server.Close() + + engine := New() + if err := engine.Init(context.Background(), config.Config{ + Response: &config.ResponseConfig{ + Format: "file", + OutputFieldName: "body", + }, + }, nil); err != nil { + t.Fatalf("Init returned error: %v", err) + } + + output, err := engine.doRequest(context.Background(), slog.Default(), map[string]any{"url": server.URL}) + if err != nil { + t.Fatalf("doRequest returned error: %v", err) + } + got, ok := output["body"].(map[string]any) + if !ok { + t.Fatalf("expected file body map, got %T", output["body"]) + } + if got["encoding"] != "base64" { + t.Fatalf("expected base64 encoding, got %#v", got["encoding"]) + } + if got["data"] != base64.StdEncoding.EncodeToString([]byte(bodyText)) { + t.Fatalf("unexpected file data value: %#v", got["data"]) + } + if got["contentType"] != "application/pdf" { + t.Fatalf("unexpected contentType: %#v", got["contentType"]) + } + if got["sizeBytes"] != len(bodyText) { + t.Fatalf("unexpected sizeBytes: %#v", got["sizeBytes"]) + } +} + +func TestCloneStreamMessageClonesPayload(t *testing.T) { + msg := sdkengram.NewInboundMessage(sdkengram.StreamMessage{ + Payload: []byte(`{"url":"https://example.com"}`), + }) + + clone := cloneStreamMessage(msg) + if string(clone.Payload) != string(msg.Payload) { + t.Fatalf("expected payload to be cloned, got %q want %q", string(clone.Payload), string(msg.Payload)) + } + clone.Payload[0] = 'X' + if string(msg.Payload) == string(clone.Payload) { + t.Fatal("expected cloned payload to be independent from original payload") + } +} + +func TestLaunchStreamWorkerMarksDoneOnWorkerError(t *testing.T) { + engine := New() + if err := engine.Init(context.Background(), config.Config{}, nil); err != nil { + t.Fatalf("Init returned error: %v", err) + } + + doneCh := make(chan struct{}, 1) + msg := sdkengram.BindProcessingReceipt( + sdkengram.NewInboundMessage(sdkengram.StreamMessage{ + Payload: []byte(`{"url":`), // invalid JSON to trigger decode error + }), + func() { + doneCh <- struct{}{} + }, + ) + + var wg sync.WaitGroup + out := make(chan sdkengram.StreamMessage) + errCh := make(chan error, 1) + engine.launchStreamWorker(context.Background(), slog.Default(), &wg, msg, out, errCh) + wg.Wait() + + select { + case <-doneCh: + case <-time.After(2 * time.Second): + t.Fatal("expected message.Done to be called on worker error") + } + + select { + case err := <-errCh: + if err == nil { + t.Fatal("expected non-nil stream worker error") + } + default: + t.Fatal("expected worker error to be reported") + } +} + +func TestLaunchStreamWorkerMarksDoneOnContextCancelWhileSending(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`{"ok":true}`)) + })) + defer server.Close() + + engine := New() + if err := engine.Init(context.Background(), config.Config{}, nil); err != nil { + t.Fatalf("Init returned error: %v", err) + } + + doneCh := make(chan struct{}, 1) + msg := sdkengram.BindProcessingReceipt( + sdkengram.NewInboundMessage(sdkengram.StreamMessage{ + Inputs: []byte(`{"url":"` + server.URL + `"}`), + }), + func() { + doneCh <- struct{}{} + }, + ) + + ctx, cancel := context.WithCancel(context.Background()) + var wg sync.WaitGroup + out := make(chan sdkengram.StreamMessage) // unbuffered and intentionally unread to block send + errCh := make(chan error, 1) + engine.launchStreamWorker(ctx, slog.Default(), &wg, msg, out, errCh) + time.Sleep(50 * time.Millisecond) + cancel() + wg.Wait() + + select { + case <-doneCh: + case <-time.After(2 * time.Second): + t.Fatal("expected message.Done to be called on context cancel path") + } +} + +func TestPrepareNextURLRequestRejectsCrossOriginURL(t *testing.T) { + engine := New() + if err := engine.Init(context.Background(), config.Config{ + Pagination: &config.PaginationConfig{ + Mode: "nextUrl", + NextURLPath: "$.next", + }, + }, nil); err != nil { + t.Fatalf("Init returned error: %v", err) + } + + current := map[string]any{"url": "https://api.example.com/v1/resources?page=1"} + hasNext, _, err := engine.prepareNextURLRequest(current, map[string]any{ + "next": "https://evil.example.com/steal?page=2", + }) + if err == nil { + t.Fatal("expected origin change error") + } + if !strings.Contains(err.Error(), "origin change") { + t.Fatalf("expected origin change error message, got %v", err) + } + if hasNext { + t.Fatal("expected hasNext=false on rejected origin change") + } +} + +func TestPrepareNextURLRequestResolvesRelativeURLOnSameOrigin(t *testing.T) { + engine := New() + if err := engine.Init(context.Background(), config.Config{ + Pagination: &config.PaginationConfig{ + Mode: "nextUrl", + NextURLPath: "$.next", + }, + }, nil); err != nil { + t.Fatalf("Init returned error: %v", err) + } + + current := map[string]any{ + "url": "https://api.example.com/v1/resources?page=1", + "params": map[string]any{"page": "1"}, + } + hasNext, _, err := engine.prepareNextURLRequest(current, map[string]any{ + "next": "/v1/resources?page=2", + }) + if err != nil { + t.Fatalf("expected no error for same-origin relative next URL, got %v", err) + } + if !hasNext { + t.Fatal("expected hasNext=true for valid next URL") + } + if got := current["url"]; got != "https://api.example.com/v1/resources?page=2" { + t.Fatalf("unexpected resolved URL: %#v", got) + } + if _, ok := current["params"]; ok { + t.Fatal("expected params to be removed when next URL is applied") + } +} + +func TestBuildHTTPRequestRejectsUnsupportedBodyType(t *testing.T) { + engine := New() + if err := engine.Init(context.Background(), config.Config{}, nil); err != nil { + t.Fatalf("Init returned error: %v", err) + } + + _, err := engine.buildHTTPRequest(context.Background(), map[string]any{ + "url": "https://api.example.com/v1/resources", + "body": map[string]any{"unsafe": "shape"}, + }) + if err == nil { + t.Fatal("expected error for unsupported body type") + } + if !strings.Contains(err.Error(), "input 'body' must be a string or []byte") { + t.Fatalf("unexpected error: %v", err) + } +} From 1b0f07dc3d1bc5b53c7a821b8207405ed591f208 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 18 Apr 2026 09:08:50 +0000 Subject: [PATCH 2/2] chore(deps): bump github.com/bubustack/bubu-sdk-go from 0.1.2 to 0.1.3 Bumps [github.com/bubustack/bubu-sdk-go](https://github.com/bubustack/bubu-sdk-go) from 0.1.2 to 0.1.3. - [Release notes](https://github.com/bubustack/bubu-sdk-go/releases) - [Changelog](https://github.com/bubustack/bubu-sdk-go/blob/main/CHANGELOG.md) - [Commits](https://github.com/bubustack/bubu-sdk-go/compare/v0.1.2...v0.1.3) --- updated-dependencies: - dependency-name: github.com/bubustack/bubu-sdk-go dependency-version: 0.1.3 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- go.mod | 17 ++++++++--------- go.sum | 38 ++++++++++++++++++-------------------- 2 files changed, 26 insertions(+), 29 deletions(-) diff --git a/go.mod b/go.mod index ca8b086..e58291b 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.26.2 require ( github.com/PaesslerAG/jsonpath v0.1.1 - github.com/bubustack/bubu-sdk-go v0.1.2 + github.com/bubustack/bubu-sdk-go v0.1.3 ) require ( @@ -15,25 +15,24 @@ require ( github.com/PaesslerAG/gval v1.0.0 // indirect github.com/aws/aws-sdk-go-v2 v1.41.5 // indirect github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8 // indirect - github.com/aws/aws-sdk-go-v2/config v1.32.14 // indirect + github.com/aws/aws-sdk-go-v2/config v1.32.15 // indirect github.com/aws/aws-sdk-go-v2/credentials v1.19.14 // indirect github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.21 // indirect - github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.22.12 // indirect + github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.22.14 // indirect github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21 // indirect github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21 // indirect - github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6 // indirect github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.22 // indirect github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 // indirect github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.13 // indirect github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.21 // indirect github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.21 // indirect - github.com/aws/aws-sdk-go-v2/service/s3 v1.98.0 // indirect + github.com/aws/aws-sdk-go-v2/service/s3 v1.99.0 // indirect github.com/aws/aws-sdk-go-v2/service/signin v1.0.9 // indirect github.com/aws/aws-sdk-go-v2/service/sso v1.30.15 // indirect github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.19 // indirect github.com/aws/aws-sdk-go-v2/service/sts v1.41.10 // indirect github.com/aws/smithy-go v1.24.2 // indirect - github.com/bubustack/bobrapet v0.1.6 // indirect + github.com/bubustack/bobrapet v0.1.8 // indirect github.com/bubustack/core v0.1.3 // indirect github.com/bubustack/tractatus v0.1.2 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect @@ -78,7 +77,7 @@ require ( github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect github.com/xeipuuv/gojsonschema v1.2.0 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect - go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 // indirect + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.68.0 // indirect go.opentelemetry.io/otel v1.43.0 // indirect go.opentelemetry.io/otel/metric v1.43.0 // indirect go.opentelemetry.io/otel/trace v1.43.0 // indirect @@ -91,8 +90,8 @@ require ( golang.org/x/sys v0.42.0 // indirect golang.org/x/term v0.41.0 // indirect golang.org/x/text v0.35.0 // indirect - golang.org/x/time v0.13.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 // indirect + golang.org/x/time v0.15.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260406210006-6f92a3bedf2d // indirect google.golang.org/grpc v1.80.0 // indirect google.golang.org/protobuf v1.36.11 // indirect gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect diff --git a/go.sum b/go.sum index 2114b60..dc920a7 100644 --- a/go.sum +++ b/go.sum @@ -15,20 +15,18 @@ github.com/aws/aws-sdk-go-v2 v1.41.5 h1:dj5kopbwUsVUVFgO4Fi5BIT3t4WyqIDjGKCangnV github.com/aws/aws-sdk-go-v2 v1.41.5/go.mod h1:mwsPRE8ceUUpiTgF7QmQIJ7lgsKUPQOUl3o72QBrE1o= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8 h1:eBMB84YGghSocM7PsjmmPffTa+1FBUeNvGvFou6V/4o= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8/go.mod h1:lyw7GFp3qENLh7kwzf7iMzAxDn+NzjXEAGjKS2UOKqI= -github.com/aws/aws-sdk-go-v2/config v1.32.14 h1:opVIRo/ZbbI8OIqSOKmpFaY7IwfFUOCCXBsUpJOwDdI= -github.com/aws/aws-sdk-go-v2/config v1.32.14/go.mod h1:U4/V0uKxh0Tl5sxmCBZ3AecYny4UNlVmObYjKuuaiOo= +github.com/aws/aws-sdk-go-v2/config v1.32.15 h1:i7rHbaySnBXGvCkDndaBU8f3EAlRVgViwNfkwFUrXgE= +github.com/aws/aws-sdk-go-v2/config v1.32.15/go.mod h1:yLJzL0IkI9+4BwjPSOueyHzppJj3t0dhK5tbmmcFk5Q= github.com/aws/aws-sdk-go-v2/credentials v1.19.14 h1:n+UcGWAIZHkXzYt87uMFBv/l8THYELoX6gVcUvgl6fI= github.com/aws/aws-sdk-go-v2/credentials v1.19.14/go.mod h1:cJKuyWB59Mqi0jM3nFYQRmnHVQIcgoxjEMAbLkpr62w= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.21 h1:NUS3K4BTDArQqNu2ih7yeDLaS3bmHD0YndtA6UP884g= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.21/go.mod h1:YWNWJQNjKigKY1RHVJCuupeWDrrHjRqHm0N9rdrWzYI= -github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.22.12 h1:vhbHvVM9Til68SOR3Dds7zi51PaUlzexmh4Lf/uv+Ok= -github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.22.12/go.mod h1:jq4soyz7xX5bfkxVKQu1BwkopF2QbQUTs5n7iIg3D8Q= +github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.22.14 h1:wSr7rcx4tyHFn8C0Due3FJELFc02u6MkfRyVChkqAt4= +github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.22.14/go.mod h1:2FslH0Y9FIQw4YKs3TVb5zEK66FvWvVPYNPqPmqWZqs= github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21 h1:Rgg6wvjjtX8bNHcvi9OnXWwcE0a2vGpbwmtICOsvcf4= github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21/go.mod h1:A/kJFst/nm//cyqonihbdpQZwiUhhzpqTsdbhDdRF9c= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21 h1:PEgGVtPoB6NTpPrBgqSE5hE/o47Ij9qk/SEZFbUOe9A= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21/go.mod h1:p+hz+PRAYlY3zcpJhPwXlLC4C+kqn70WIHwnzAfs6ps= -github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6 h1:qYQ4pzQ2Oz6WpQ8T3HvGHnZydA72MnLuFK9tJwmrbHw= -github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6/go.mod h1:O3h0IK87yXci+kg6flUKzJnWeziQUKciKrLjcatSNcY= github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.22 h1:rWyie/PxDRIdhNf4DzRk0lvjVOqFJuNnO8WwaIRVxzQ= github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.22/go.mod h1:zd/JsJ4P7oGfUhXn1VyLqaRZwPmZwg44Jf2dS84Dm3Y= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 h1:5EniKhLZe4xzL7a+fU3C2tfUN4nWIqlLesfrjkuPFTY= @@ -39,8 +37,8 @@ github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.21 h1:c31//R3x github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.21/go.mod h1:r6+pf23ouCB718FUxaqzZdbpYFyDtehyZcmP5KL9FkA= github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.21 h1:ZlvrNcHSFFWURB8avufQq9gFsheUgjVD9536obIknfM= github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.21/go.mod h1:cv3TNhVrssKR0O/xxLJVRfd2oazSnZnkUeTf6ctUwfQ= -github.com/aws/aws-sdk-go-v2/service/s3 v1.98.0 h1:foqo/ocQ7WqKwy3FojGtZQJo0FR4vto9qnz9VaumbCo= -github.com/aws/aws-sdk-go-v2/service/s3 v1.98.0/go.mod h1:uoA43SdFwacedBfSgfFSjjCvYe8aYBS7EnU5GZ/YKMM= +github.com/aws/aws-sdk-go-v2/service/s3 v1.99.0 h1:hlSuz394kV0vhv9drL5lhuEFbEOEP1VyQpy15qWh1Pk= +github.com/aws/aws-sdk-go-v2/service/s3 v1.99.0/go.mod h1:uoA43SdFwacedBfSgfFSjjCvYe8aYBS7EnU5GZ/YKMM= github.com/aws/aws-sdk-go-v2/service/signin v1.0.9 h1:QKZH0S178gCmFEgst8hN0mCX1KxLgHBKKY/CLqwP8lg= github.com/aws/aws-sdk-go-v2/service/signin v1.0.9/go.mod h1:7yuQJoT+OoH8aqIxw9vwF+8KpvLZ8AWmvmUWHsGQZvI= github.com/aws/aws-sdk-go-v2/service/sso v1.30.15 h1:lFd1+ZSEYJZYvv9d6kXzhkZu07si3f+GQ1AaYwa2LUM= @@ -53,10 +51,10 @@ github.com/aws/smithy-go v1.24.2 h1:FzA3bu/nt/vDvmnkg+R8Xl46gmzEDam6mZ1hzmwXFng= github.com/aws/smithy-go v1.24.2/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= -github.com/bubustack/bobrapet v0.1.6 h1:kf7A1GsQvpIBgWE8FIoYMke/nMpXgBAzw6+G2tB8HYM= -github.com/bubustack/bobrapet v0.1.6/go.mod h1:2eZ3mnhnvdO5Y1vkCrEDUfg0V9Menv76pdNIlfSEL/M= -github.com/bubustack/bubu-sdk-go v0.1.2 h1:VZTUaMoFxW0LKDTHvT21urtBy3UD7+HZyRYstNZYj7Y= -github.com/bubustack/bubu-sdk-go v0.1.2/go.mod h1:dWqhJcwgAP7NDSq+e1Qt906wQjwx4Z/Y/5EbwNAgD8I= +github.com/bubustack/bobrapet v0.1.8 h1:G3aOENZysxKQ2HekUdwza/YX6W0kWyAXcmhfqye7gEw= +github.com/bubustack/bobrapet v0.1.8/go.mod h1:sZIamItzdIMcdZ9T8en1JblpMOJPFJh5S3NK2f8Pwow= +github.com/bubustack/bubu-sdk-go v0.1.3 h1:NTTQb1je0mLX55UkLCU7T6733BukrvvfVoQtAl/2ZAI= +github.com/bubustack/bubu-sdk-go v0.1.3/go.mod h1:jjxWpANv4y5GJYEmVu6LZCcrYgDE5lJqR+tmnxYeb3k= github.com/bubustack/core v0.1.3 h1:rFyj8EyC0agZZOOw9nGcirdNGqL5ArJUfEFPAAtdpb4= github.com/bubustack/core v0.1.3/go.mod h1:UlEBsFdlyVdGVZVb9yfBoVM33DyxYQv3n921G1ll7Ng= github.com/bubustack/tractatus v0.1.2 h1:PtcEisKiWGelflXB4NGtSl1O9G6BUXZ8xKNx4m2hMRQ= @@ -184,16 +182,16 @@ github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17 github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 h1:YH4g8lQroajqUwWbq/tr2QX1JFmEXaDLgG+ew9bLMWo= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0/go.mod h1:fvPi2qXDqFs8M4B4fmJhE92TyQs9Ydjlg3RvfUp+NbQ= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.68.0 h1:0Qx7VGBacMm9ZENQ7TnNObTYI4ShC+lHI16seduaxZo= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.68.0/go.mod h1:Sje3i3MjSPKTSPvVWCaL8ugBzJwik3u4smCjUeuupqg= go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0= go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM= go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY= go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg= go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg= -go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8= -go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew= +go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw= +go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A= go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A= go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= @@ -222,14 +220,14 @@ golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU= golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A= golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= -golang.org/x/time v0.13.0 h1:eUlYslOIt32DgYD6utsuUeHs4d7AsEYLuIAdg7FlYgI= -golang.org/x/time v0.13.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= +golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= +golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k= golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0= gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 h1:m8qni9SQFH0tJc1X0vmnpw/0t+AImlSvp30sEupozUg= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260406210006-6f92a3bedf2d h1:wT2n40TBqFY6wiwazVK9/iTWbsQrgk5ZfCSVFLO9LQA= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260406210006-6f92a3bedf2d/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM= google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=