diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml new file mode 100644 index 000000000..7d6b90dc2 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -0,0 +1,57 @@ +name: Bug Report +description: Create a report to help us improve +title: "bug: " +labels: + - kind/bug +body: + - type: textarea + id: description + attributes: + label: Describe the bug * + description: Add logs and screenshots if any. + placeholder: | + A clear and concise description of what the bug is. + validations: + required: true + + - type: textarea + id: reproducing + attributes: + label: Steps To Reproduce + description: Steps to reproduce the behavior. + placeholder: | + 1. Go to '...' + 2. Click on '...' + 3. Scroll down to '...' + 4. See the error + validations: + required: true + + - type: textarea + id: expected + attributes: + label: Expected Behavior + description: A clear and concise description of what you expected to happen. + validations: + required: true + + - type: textarea + id: additional + attributes: + label: Additional Context + description: Add any other context about the problem here. + validations: + required: false + + - type: textarea + id: environment + attributes: + label: Environment + description: | + Please provide details about your environment + placeholder: | + - OS: [e.g. Ubuntu 22.04] + - Kubernetes Version: [e.g. 1.28] + - Toolhive Webhook Version: [e.g. v1.0.0] + validations: + required: false diff --git a/.github/ISSUE_TEMPLATE/epic.yaml b/.github/ISSUE_TEMPLATE/epic.yaml new file mode 100644 index 000000000..c1a4c0e1d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/epic.yaml @@ -0,0 +1,58 @@ +name: Epic +description: Create an epic to track large initiatives +title: "epic: " +labels: + - kind/epic +body: + - type: textarea + id: description + attributes: + label: Description * + description: A clear and concise description of the epic + placeholder: | + Describe the high-level goal or initiative + validations: + required: true + + - type: textarea + id: goals + attributes: + label: Goals + description: What are the main objectives of this epic? + placeholder: | + - Goal 1 + - Goal 2 + - Goal 3 + validations: + required: true + + - type: textarea + id: user_stories + attributes: + label: User Stories + description: List the user stories or sub-tasks that make up this epic + placeholder: | + - [ ] User story 1 + - [ ] User story 2 + - [ ] User story 3 + validations: + required: false + + - type: textarea + id: acceptance_criteria + attributes: + label: Acceptance Criteria + description: What defines completion for this epic? + placeholder: | + - Criteria 1 + - Criteria 2 + validations: + required: false + + - type: textarea + id: additional + attributes: + label: Additional Context + description: Add any other context, dependencies, or technical considerations + validations: + required: false diff --git a/.github/ISSUE_TEMPLATE/feature_request.yaml b/.github/ISSUE_TEMPLATE/feature_request.yaml new file mode 100644 index 000000000..ee61bd328 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yaml @@ -0,0 +1,65 @@ +name: Feature Request +description: Suggest an idea for this project +title: "feat: " +labels: + - kind/feature +body: + - type: textarea + id: problem + attributes: + label: Problem Statement * + description: Is your feature request related to a problem? Please describe. + placeholder: | + A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + validations: + required: true + + - type: textarea + id: solution + attributes: + label: Proposed Solution * + description: Describe the solution you'd like + placeholder: | + A clear and concise description of what you want to happen. + validations: + required: true + + - type: textarea + id: alternatives + attributes: + label: Alternatives Considered + description: Describe alternatives you've considered + placeholder: | + A clear and concise description of any alternative solutions or features you've considered. + validations: + required: false + + - type: textarea + id: additional + attributes: + label: Additional Context + description: Add any other context or screenshots about the feature request here + validations: + required: false + + - type: dropdown + id: priority + attributes: + label: Priority + description: How important is this feature to you? + options: + - Low + - Medium + - High + - Critical + validations: + required: false + + - type: checkboxes + id: willingness + attributes: + label: Contribution + description: Are you willing to contribute to this feature? + options: + - label: I am willing to submit a pull request for this feature + required: false diff --git a/.github/ISSUE_TEMPLATE_EXAMPLE.md b/.github/ISSUE_TEMPLATE_EXAMPLE.md new file mode 100644 index 000000000..a53f9c9ea --- /dev/null +++ b/.github/ISSUE_TEMPLATE_EXAMPLE.md @@ -0,0 +1,118 @@ +--- +title: "feat: Add GitHub Actions CI/CD workflows and complete .github setup" +labels: ["kind/feature", "ci/cd"] +--- + +## Description + +This PR adds complete GitHub Actions CI/CD workflows following the kagenti-operator repository pattern, along with all supporting `.github` configuration files. + +## Changes Made + +### GitHub Workflows + +1. **CI Workflow** ([`.github/workflows/ci.yaml`](.github/workflows/ci.yaml)) + - Runs on pull requests to `main` and `release-*` branches + - Performs linting (`go fmt`), static analysis (`go vet`), and build checks + - Uses Go 1.22 + +2. **GoReleaser Workflow** ([`.github/workflows/goreleaser.yml`](.github/workflows/goreleaser.yml)) + - Triggers on version tags (e.g., `v1.0.0`) + - Builds multi-architecture binaries (Linux/Darwin, amd64/arm64) + - Creates and pushes Docker images to `ghcr.io/kagenti/toolhive-webhook` + - Packages and publishes Helm charts to GHCR + - Creates GitHub releases with changelog + +3. **PR Verifier Workflow** ([`.github/workflows/pr-verifier.yaml`](.github/workflows/pr-verifier.yaml)) + - Validates PR titles follow semantic commit convention + - Supports types: feat, fix, docs, style, refactor, perf, test, build, ci, chore, revert + +4. **Spellcheck Workflow** ([`.github/workflows/spellcheck_action.yml`](.github/workflows/spellcheck_action.yml)) + - Runs on PRs and pushes to main + - Checks spelling in Markdown files using custom wordlist + +### Issue Templates + +- **Bug Report** ([`.github/ISSUE_TEMPLATE/bug_report.yaml`](.github/ISSUE_TEMPLATE/bug_report.yaml)) +- **Epic** ([`.github/ISSUE_TEMPLATE/epic.yaml`](.github/ISSUE_TEMPLATE/epic.yaml)) +- **Feature Request** ([`.github/ISSUE_TEMPLATE/feature_request.yaml`](.github/ISSUE_TEMPLATE/feature_request.yaml)) + +### Spellcheck Configuration + +- **Wordlist** ([`.github/spellcheck/.wordlist.txt`](.github/spellcheck/.wordlist.txt)) - 374 technical terms +- **Config** ([`.github/spellcheck/.spellcheck.yml`](.github/spellcheck/.spellcheck.yml)) - Spellcheck rules for Markdown and text files + +### Other Configuration + +- **Dependabot** ([`.github/dependabot.yaml`](.github/dependabot.yaml)) - Automated dependency updates for Go modules, Docker, and GitHub Actions +- **PR Template** ([`.github/pull_request_template.md`](.github/pull_request_template.md)) - Standardized PR description template + +### Release Configuration + +- **GoReleaser Config** ([`.goreleaser.yaml`](.goreleaser.yaml)) + - Multi-architecture builds for Linux and Darwin (amd64, arm64) + - Docker image creation with multiple tags (version, major, minor, latest) + - Multi-architecture Docker manifests + - Automatic changelog generation + +### Helm Chart Updates + +- **Values** ([`charts/toolhive-webhook/values.yaml`](charts/toolhive-webhook/values.yaml)) + - Updated image repository to `ghcr.io/kagenti/toolhive-webhook` + - Added version placeholder `__PLACEHOLDER__` for automated releases + +- **New Templates**: + - [`mutatingwebhook.yaml`](charts/toolhive-webhook/templates/mutatingwebhook.yaml) - Webhook configuration + - [`validatingwebhook.yaml`](charts/toolhive-webhook/templates/validatingwebhook.yaml) - Validation webhook + - [`clusterrole.yaml`](charts/toolhive-webhook/templates/clusterrole.yaml) - RBAC permissions + - [`clusterrolebinding.yaml`](charts/toolhive-webhook/templates/clusterrolebinding.yaml) - Role binding + - [`certificate-issuer.yaml`](charts/toolhive-webhook/templates/certificate-issuer.yaml) - Self-signed issuer + - [`certificate.yaml`](charts/toolhive-webhook/templates/certificate.yaml) - TLS certificate + +### Documentation + +- **Deployment Guide** ([`DEPLOYMENT.md`](DEPLOYMENT.md)) - Comprehensive deployment instructions including: + - CI/CD workflow documentation + - Release creation process + - Deployment methods (Helm, kubectl, Docker) + - Troubleshooting guide + - Local testing instructions + +## How to Create a Release + +```bash +# Tag and push +git tag -a v1.0.0 -m "Release v1.0.0" +git push origin v1.0.0 +``` + +This automatically: +- Builds binaries for all platforms +- Creates Docker images for `ghcr.io/kagenti/toolhive-webhook:v1.0.0` +- Packages and publishes Helm chart to GHCR +- Creates GitHub release with artifacts and changelog + +## Testing Checklist + +- [ ] CI workflow runs successfully on PR +- [ ] PR verifier validates semantic commit titles +- [ ] Spellcheck passes on documentation +- [ ] GoReleaser dry-run succeeds: `goreleaser release --snapshot --clean` +- [ ] Helm chart templates correctly: `helm template toolhive-webhook ./charts/toolhive-webhook` +- [ ] Helm chart lints cleanly: `helm lint charts/toolhive-webhook` +- [ ] Webhook configurations are created +- [ ] Certificates are generated +- [ ] RBAC resources are applied + +## Related Issues + +Closes #XXX + +## Additional Context + +This implementation follows the same patterns as the `kagenti-operator` repository to ensure consistency across the Kagenti ecosystem. + +## Dependencies + +- **cert-manager** is required for webhook TLS certificates +- **GitHub Container Registry** permissions for publishing images and charts diff --git a/.github/dependabot.yaml b/.github/dependabot.yaml new file mode 100644 index 000000000..b18376c1e --- /dev/null +++ b/.github/dependabot.yaml @@ -0,0 +1,31 @@ +version: 2 +updates: + # Enable version updates for Go modules + - package-ecosystem: "gomod" + directory: "/toolhive-webhook" + schedule: + interval: "weekly" + open-pull-requests-limit: 10 + labels: + - "dependencies" + - "go" + + # Enable version updates for Docker + - package-ecosystem: "docker" + directory: "/toolhive-webhook" + schedule: + interval: "weekly" + open-pull-requests-limit: 5 + labels: + - "dependencies" + - "docker" + + # Enable version updates for GitHub Actions + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + open-pull-requests-limit: 5 + labels: + - "dependencies" + - "github-actions" diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 000000000..4ad0b9013 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,50 @@ +# Description + + + +Fixes # (issue) + +## Type of Change + + + +- [ ] Bug fix (non-breaking change which fixes an issue) +- [ ] New feature (non-breaking change which adds functionality) +- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) +- [ ] Documentation update +- [ ] Refactoring (no functional changes) +- [ ] Performance improvement +- [ ] Test addition or update +- [ ] CI/CD update + +## How Has This Been Tested? + + + +- [ ] Test A +- [ ] Test B + +**Test Configuration**: + +- Kubernetes version: +- Toolhive webhook version: +- OS: + +## Checklist + +- [ ] My code follows the style guidelines of this project +- [ ] I have performed a self-review of my code +- [ ] I have commented my code, particularly in hard-to-understand areas +- [ ] I have made corresponding changes to the documentation +- [ ] My changes generate no new warnings +- [ ] I have added tests that prove my fix is effective or that my feature works +- [ ] New and existing unit tests pass locally with my changes +- [ ] Any dependent changes have been merged and published in downstream modules + +## Screenshots (if appropriate) + + + +## Additional Notes + + diff --git a/.github/spellcheck/.spellcheck.yml b/.github/spellcheck/.spellcheck.yml new file mode 100644 index 000000000..43bcaffac --- /dev/null +++ b/.github/spellcheck/.spellcheck.yml @@ -0,0 +1,31 @@ +matrix: +- name: Markdown + aspell: + lang: en + dictionary: + wordlists: + - .github/spellcheck/.wordlist.txt + encoding: utf-8 + pipeline: + - pyspelling.filters.markdown: + - pyspelling.filters.html: + comments: false + ignores: + - code + - pre + sources: + - '**/*.md' + default_encoding: utf-8 + +- name: Text + aspell: + lang: en + dictionary: + wordlists: + - .github/spellcheck/.wordlist.txt + encoding: utf-8 + pipeline: + - pyspelling.filters.text: + sources: + - '**/*.txt' + default_encoding: utf-8 diff --git a/.github/spellcheck/.wordlist.txt b/.github/spellcheck/.wordlist.txt new file mode 100644 index 000000000..e051bf9a1 --- /dev/null +++ b/.github/spellcheck/.wordlist.txt @@ -0,0 +1,357 @@ +admissionregistration +amd +api +apiextensions +apiserver +apiVersion +apps +arg +Args +argoproj +argo +ARM +auth +authenticator +Autoscaler +Autoscaling +autoscaling +backend +backends +backoff +binaries +ブ +bmp +bool +buildx +builtin +cd +certmanager +cgroup +cgroups +chmod +CI +CipherSuites +cli +cline +ClusterIP +clusterrole +ClusterRole +clusterrolebinding +ClusterRoleBinding +cmd +cni +cni +CNI +coalesce +codecov +config +configmap +ConfigMap +configmaps +ConfigMaps +containerPort +CRD +CRDs +cron +ctrl +customresourcedefinition +CustomResourceDefinition +customresourcedefinitions +daemonset +DaemonSet +datetime +DefaultMode +defrag +deprovision +deprovisioning +dev +didn +dns +docker +Dockerfile +doesn +DSA +ecdsa +ECDSA +ecr +elif +env +Env +EOF +etcd +frontend +gcr +ghodss +ghcr +github +Github +gitignore +gke +golang +goreleaser +GoReleaser +GroupVersion +gzip +Gzip +hardcode +hardcoded +helm +Helm +hostPath +hostPort +http +https +ico +ImagePullSecrets +initContainer +initContainers +inline +io +ip +IPv +ipv +istio +jpg +jpeg +json +Json +Jsonnet +jsonnet +k8s +Kagenti +kagenti +kube +kubectl +kubeconfig +kubebuilder +Kubebuilder +Kubernetes +kubernetes +kustomization +Kustomization +Kustomize +kustomize +labelSelector +lifecycle +linux +listwatch +localhost +localhost +loglevel +logrus +MacOS +macos +marshaling +marshalling +mcpserver +MCPServer +metadata +microservice +microservices +MinReadySeconds +monorepo +mTLS +multiarch +multiline +mutex +Mutex +mutualTLS +MutatingWebhookConfiguration +namespace +Namespace +namespaced +namespaces +Namespaces +netrc +nodePort +NodePort +nolint +nonroot +nsenter +OAuth +oci +OCI +ok +openssh +OpenSSH +os +OS +outDir +param +Params +PersistentVolume +PersistentVolumeClaim +PodDisruptionBudget +PodSecurityPolicy +podSelector +PodSpec +png +PowerShell +prebuilt +prepend +priv +proto +protobuf +psql +ptr +PullPolicy +quickstart +rbac +RBAC +readiness +readonly +ReadOnly +ReadOnlyRootFilesystem +ReadWriteMany +ReadWriteOnce +reconfiguration +recurse +redhat +Redhat +redis +Redis +redoc +ReDoc +refactor +regex +replicaset +ReplicaSet +replicasets +ReplicaSets +repo +repos +RequestHeaderIdentityProvider +resync +retry +rbac +RBAC +RoleBinding +rolebinding +RoleRef +Rollout +rollout +rollouts +rootfs +rs +rsa +RSA +runtime +runtimes +rw +scalability +secretKeyRef +secretRef +selfsigned +semver +serviceAccount +ServiceAccount +serviceaccounts +ServiceAccounts +servicemonitor +ServiceMonitor +sha +sidecar +sigstore +Sigstore +skaffold +Skaffold +skipVerify +ssl +statefulset +StatefulSet +StatefulSets +stdout +stderr +storageclass +StorageClass +StorageClassName +stringData +struct +structs +subchart +subcharts +subcommand +subdirectories +subdirectory +subdomains +submatch +subnet +Subnet +subnets +Subnets +subpath +subpaths +subresource +subresources +sudo +svg +syscall +systemd +tac +tcp +TCP +templatefile +templating +testdata +tgz +tif +tiff +tls +TLS +tmp +TODO +TODOs +Toolhive +toolhive +toolhive +txt +ua +ubuntu +Ubuntu +uint +uname +uncomment +uncommented +undef +unencrypted +unmarshal +Unmarshal +unmarshaling +unmarshalling +untar +uri +url +URL +username +usr +util +utils +uuid +UUID +ValidatingWebhookConfiguration +ValueFrom +valueFrom +versioned +versioning +viper +VolumeMount +webhook +Webhook +webhooks +Webhooks +whitespace +wildcards +workflow +workflows +workqueue +Workqueue +workspace +wsl +WSL +www +x86 +yaml +YAML +yml +YML +zsh diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 000000000..880ffe65c --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,30 @@ +name: CI + +on: + pull_request: + branches: + - main + - "release-*" + +jobs: + ci-checks: + runs-on: ubuntu-latest + name: CI Checks + steps: + - uses: actions/checkout@v5 + + - uses: actions/setup-go@v6 + with: + go-version: '1.22' + + - name: Lint + run: find . -type f -name 'go.mod' -execdir go fmt ./... \; + + - name: Static analysis + run: find . -type f -name 'go.mod' -execdir go vet ./... \; + + - name: Build + run: find . -type f -name 'go.mod' -execdir go build -v ./... \; + # TODO - make sure at least bare bone tests work + # - name: Test + # run: find . -type f -name 'go.mod' -execdir go test -v ./... \; diff --git a/.github/workflows/goreleaser.yml b/.github/workflows/goreleaser.yml new file mode 100644 index 000000000..6e79cdade --- /dev/null +++ b/.github/workflows/goreleaser.yml @@ -0,0 +1,63 @@ +name: goreleaser + +on: + push: + tags: + - 'v*' + +permissions: + contents: write + packages: write + +env: + REGISTRY: ghcr.io + REPO: kagenti/kagenti-extensions + CHARTS_PATH: ./charts + +jobs: + goreleaser: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - uses: actions/setup-go@v6 + with: + go-version: v1.23 + + - name: Delete non-semver tags + run: 'git tag -d $(git tag -l | grep -v "^v") ' + + - name: Run GoReleaser on tag + uses: goreleaser/goreleaser-action@v6 + with: + distribution: goreleaser + version: latest + args: release --timeout 60m --clean + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + USER: ${{ github.actor }} + EMAIL: ${{ github.actor }}@users.noreply.github.com + + - name: Set up Helm + uses: azure/setup-helm@v4 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Login to registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Package and push toolhive webhook chart + run: | + chartVersion=$(echo ${{ github.ref_name }} | cut -c 2-) + chartPackageName="toolhive-webhook-chart-${chartVersion}.tgz" + cd ${{ env.CHARTS_PATH }}/toolhive-webhook + sed -i "s/__PLACEHOLDER__/${chartVersion}/g" values.yaml + helm package . --destination . --version ${chartVersion} --app-version ${chartVersion} + helm push "./${chartPackageName}" oci://${{ env.REGISTRY }}/${{ env.REPO }} diff --git a/.github/workflows/pr-verifier.yaml b/.github/workflows/pr-verifier.yaml new file mode 100644 index 000000000..c0ee67df6 --- /dev/null +++ b/.github/workflows/pr-verifier.yaml @@ -0,0 +1,45 @@ +name: PR Verifier + +on: + pull_request: + branches: + - main + - "release-*" + +jobs: + verify-pr: + runs-on: ubuntu-latest + name: Verify PR + steps: + - uses: actions/checkout@v5 + + - name: Verify PR title + uses: amannn/action-semantic-pull-request@v5 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + types: | + feat + fix + docs + style + refactor + perf + test + build + ci + chore + revert + requireScope: false + subjectPattern: ^[A-Z].+$ + subjectPatternError: | + The subject "{subject}" found in the pull request title "{title}" + didn't match the configured pattern. Please ensure that the subject + starts with an uppercase character. + ignoreLabels: | + bot + ignore-semantic-pull-request + headerPattern: '^(\w*)(?:\(([\w$.\-*/ ]*)\))?: (.*)$' + headerPatternCorrespondence: type, scope, subject + wip: true + validateSingleCommit: false diff --git a/.github/workflows/spellcheck_action.yml b/.github/workflows/spellcheck_action.yml new file mode 100644 index 000000000..a8a6c28e9 --- /dev/null +++ b/.github/workflows/spellcheck_action.yml @@ -0,0 +1,22 @@ +name: Spellcheck Action +on: + pull_request: + branches: + - main + - "release-*" + push: + branches: + - main + +jobs: + spellcheck: + name: Spellcheck + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + + - name: Check spelling + uses: rojopolis/spellcheck-github-actions@0.36.0 + with: + config_path: .github/spellcheck/.spellcheck.yml + task_name: Markdown diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..8ca23d2fc --- /dev/null +++ b/.gitignore @@ -0,0 +1,47 @@ +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool +*.out +cover.out + +# Go workspace file +go.work +go.work.sum + +# Build artifacts +toolhive-webhook/bin/ +toolhive-webhook/dist/ +dist/ + +# Kubernetes generated files +toolhive-webhook/config/crd/bases/ + +# IDE +.idea/ +.vscode/ +*.swp +*.swo +*~ + +# OS +.DS_Store +Thumbs.db + +# Temporary files +*.tmp +*.bak +*.orig + +# Helm +*.tgz + +# Ko +.ko/ diff --git a/.goreleaser.yaml b/.goreleaser.yaml new file mode 100644 index 000000000..ce3778bc8 --- /dev/null +++ b/.goreleaser.yaml @@ -0,0 +1,134 @@ +version: 2 + +before: + hooks: + - go mod tidy + - go mod download + +builds: + - id: toolhive-webhook + main: ./toolhive-webhook/cmd/main.go + binary: manager + env: + - CGO_ENABLED=0 + goos: + - linux + - darwin + goarch: + - amd64 + - arm64 + ldflags: + - -s -w + - -X main.version={{.Version}} + - -X main.commit={{.Commit}} + - -X main.date={{.Date}} + +archives: + - id: toolhive-webhook + format: tar.gz + name_template: >- + {{ .ProjectName }}_ + {{- title .Os }}_ + {{- if eq .Arch "amd64" }}x86_64 + {{- else if eq .Arch "386" }}i386 + {{- else }}{{ .Arch }}{{ end }} + {{- if .Arm }}v{{ .Arm }}{{ end }} + files: + - LICENSE* + - README* + - toolhive-webhook/README.md + +dockers: + - image_templates: + - "ghcr.io/kagenti/kagenti-extensions/toolhive-webhook:{{ .Tag }}-amd64" + - "ghcr.io/kagenti/kagenti-extensions/toolhive-webhook:v{{ .Major }}-amd64" + - "ghcr.io/kagenti/kagenti-extensions/toolhive-webhook:v{{ .Major }}.{{ .Minor }}-amd64" + - "ghcr.io/kagenti/kagenti-extensions/toolhive-webhook:latest-amd64" + use: buildx + dockerfile: toolhive-webhook/Dockerfile + build_flag_templates: + - "--platform=linux/amd64" + - "--label=org.opencontainers.image.created={{.Date}}" + - "--label=org.opencontainers.image.title={{.ProjectName}}" + - "--label=org.opencontainers.image.revision={{.FullCommit}}" + - "--label=org.opencontainers.image.version={{.Version}}" + - "--label=org.opencontainers.image.source={{.GitURL}}" + extra_files: + - toolhive-webhook/go.mod + - toolhive-webhook/go.sum + - toolhive-webhook/cmd + - toolhive-webhook/internal + + - image_templates: + - "ghcr.io/kagenti/kagenti-extensions/toolhive-webhook:{{ .Tag }}-arm64" + - "ghcr.io/kagenti/kagenti-extensions/toolhive-webhook:v{{ .Major }}-arm64" + - "ghcr.io/kagenti/kagenti-extensions/toolhive-webhook:v{{ .Major }}.{{ .Minor }}-arm64" + - "ghcr.io/kagenti/kagenti-extensions/toolhive-webhook:latest-arm64" + use: buildx + goarch: arm64 + dockerfile: toolhive-webhook/Dockerfile + build_flag_templates: + - "--platform=linux/arm64" + - "--label=org.opencontainers.image.created={{.Date}}" + - "--label=org.opencontainers.image.title={{.ProjectName}}" + - "--label=org.opencontainers.image.revision={{.FullCommit}}" + - "--label=org.opencontainers.image.version={{.Version}}" + - "--label=org.opencontainers.image.source={{.GitURL}}" + extra_files: + - toolhive-webhook/go.mod + - toolhive-webhook/go.sum + - toolhive-webhook/cmd + - toolhive-webhook/internal + +docker_manifests: + - name_template: "ghcr.io/kagenti/toolhive-webhook:{{ .Tag }}" + image_templates: + - "ghcr.io/kagenti/kagenti-extensions/toolhive-webhook:{{ .Tag }}-amd64" + - "ghcr.io/kagenti/kagenti-extensions/toolhive-webhook:{{ .Tag }}-arm64" + + - name_template: "ghcr.io/kagenti/toolhive-webhook:v{{ .Major }}" + image_templates: + - "ghcr.io/kagenti/kagenti-extensions/toolhive-webhook:v{{ .Major }}-amd64" + - "ghcr.io/kagenti/kagenti-extensions/toolhive-webhook:v{{ .Major }}-arm64" + + - name_template: "ghcr.io/kagenti/toolhive-webhook:v{{ .Major }}.{{ .Minor }}" + image_templates: + - "ghcr.io/kagenti/kagenti-extensions/toolhive-webhook:v{{ .Major }}.{{ .Minor }}-amd64" + - "ghcr.io/kagenti/kagenti-extensions/toolhive-webhook:v{{ .Major }}.{{ .Minor }}-arm64" + + - name_template: "ghcr.io/kagenti/toolhive-webhook:latest" + image_templates: + - "ghcr.io/kagenti/kagenti-extensions/toolhive-webhook:latest-amd64" + - "ghcr.io/kagenti/kagenti-extensions/toolhive-webhook:latest-arm64" + +checksum: + name_template: 'checksums.txt' + +snapshot: + version_template: "{{ incpatch .Version }}-next" + +changelog: + sort: asc + filters: + exclude: + - '^docs:' + - '^test:' + - '^chore:' + - Merge pull request + - Merge branch + +release: + github: + owner: kagenti + name: kagenti-extensions + draft: false + prerelease: auto + mode: append + header: | + ## ToolHive Webhook Release {{ .Tag }} + + ### What's New + + footer: | + --- + **Full Changelog**: https://github.com/kagenti/kagenti-extensions/compare/{{ .PreviousTag }}...{{ .Tag }} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 000000000..1600b1694 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,42 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v5.0.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-added-large-files + args: ['--maxkb=1024'] + exclude: ^toolhive-webhook/bin/ + - id: check-yaml + # Exclude Helm templates as they contain Go template syntax + exclude: ^charts/.*/templates/.*\.yaml$ + - id: check-json + - id: check-merge-conflict + - id: mixed-line-ending + + - repo: https://github.com/gruntwork-io/pre-commit + rev: v0.1.23 + hooks: + - id: helmlint + files: ^charts/ + + - repo: local + hooks: + - id: go-fmt + name: go-fmt + entry: bash -c 'cd toolhive-webhook && go fmt ./...' + language: system + files: ^toolhive-webhook/.*\.go$ + pass_filenames: false + - id: go-vet + name: go-vet + entry: bash -c 'cd toolhive-webhook && go vet ./...' + language: system + files: ^toolhive-webhook/.*\.go$ + pass_filenames: false + - id: go-mod-tidy + name: go-mod-tidy + entry: bash -c 'cd toolhive-webhook && go mod tidy' + language: system + files: ^toolhive-webhook/(go\.mod|go\.sum)$ + pass_filenames: false diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md new file mode 100644 index 000000000..ef867eabf --- /dev/null +++ b/DEPLOYMENT.md @@ -0,0 +1,150 @@ +# Deployment Guide for ToolHive Webhook + +This repository includes automated CI/CD workflows using GitHub Actions and GoReleaser to build, test, and release the ToolHive webhook. + +## Overview + +The deployment process consists of: + +1. **CI Checks** - Automated on every pull request +2. **Release Process** - Automated on git tag pushes +3. **Docker Images** - Multi-architecture container builds +4. **Helm Charts** - Published to GitHub Container Registry + +## CI/CD Workflows + +### CI Workflow + +**File:** [`.github/workflows/ci.yaml`](.github/workflows/ci.yaml:1) + +Runs on every pull request to `main` or `release-*` branches: + +- **Linting**: Runs `go fmt` on all Go modules +- **Static Analysis**: Runs `go vet` for code quality checks +- **Build**: Compiles all Go modules to verify no build errors + +### Release Workflow + +**File:** [`.github/workflows/goreleaser.yml`](.github/workflows/goreleaser.yml:1) + +Triggered when you push a tag starting with `v` (e.g., `v1.0.0`, `v0.1.2`): + +1. Builds multi-architecture binaries (Linux/Darwin, amd64/arm64) +2. Creates Docker images for `amd64` and `arm64` +3. Pushes images to `ghcr.io/kagenti/kagenti-extensions/toolhive-webhook` +4. Packages and publishes Helm chart +5. Creates GitHub release with changelog + +## How to Create a Release + +### 1. Prepare Your Release + +Ensure all changes are merged to the `main` branch and tests pass. + +### 2. Create and Push a Tag + +```bash +# Tag format: v{MAJOR}.{MINOR}.{PATCH} +git tag -a v1.0.0 -m "Release v1.0.0" +git push origin v1.0.0 +``` + +### 3. Monitor the Release + +1. Go to the **Actions** tab in GitHub +2. Watch the `goreleaser` workflow execute +3. Once complete, check the **Releases** page for the new release + +### 4. Verify Published Artifacts + +After successful release, you'll have: + +- **Docker Images:** + - `ghcr.io/kagenti/kagenti-extensions/toolhive-webhook:v1.0.0` + + +- **Helm Chart:** + - `oci://ghcr.io/kagenti/kagenti-extensions/toolhive-webhook/toolhive-webhook-chart` + +- **Binaries** (attached to GitHub release): + - `toolhive-webhook_Linux_x86_64.tar.gz` + - `toolhive-webhook_Linux_arm64.tar.gz` + - `toolhive-webhook_Darwin_x86_64.tar.gz` + - `toolhive-webhook_Darwin_arm64.tar.gz` + +## GoReleaser Configuration + +**File:** [`.goreleaser.yaml`](.goreleaser.yaml:1) + +Key features: + +- **Multi-architecture builds**: Linux and Darwin for amd64 and arm64 +- **Docker manifest**: Combines platform-specific images +- **Versioning**: Semantic versioning with major/minor tags +- **Helm integration**: Automatic chart packaging and publishing + +### Docker Image Tags + +Each release creates multiple tags: + +- Full version: `v1.0.0` +- Major version: `v1` (updated with each v1.x.x release) +- Minor version: `v1.0` (updated with each v1.0.x release) +- Latest: `latest` (always points to most recent release) + +## Deployment + +Install the latest version: + +```bash +helm install toolhive-webhook \ + oci://ghcr.io/kagenti/kagenti-extensions/toolhive-webhook/toolhive-webhook-chart \ + --version 1.0.0 \ + --namespace toolhive-system \ + --create-namespace +``` + +### Method 2: Using kubectl with Released YAML + +Download and apply the release manifest: + +```bash +kubectl apply -f https://github.com/kagenti/kagenti-extensions/releases/download/v1.0.0/install.yaml +``` + +### Method 3: Using Docker Image Directly + +Pull the image: + +```bash +docker pull ghcr.io/kagenti/kagenti-extensions/toolhive-webhook:v1.0.0 +``` + +## Configuration + +### Required Secrets + +The workflows require the following GitHub secrets: + +- `GITHUB_TOKEN` - Automatically provided by GitHub Actions + - Used for pushing to GHCR and creating releases + +### Environment Variables + +In [`.github/workflows/goreleaser.yml`](.github/workflows/goreleaser.yml:10): + +```yaml +env: + REGISTRY: ghcr.io + REPO: kagenti/toolhive-webhook + CHARTS_PATH: ./charts +``` + +## Local Testing + +### Test Docker Build + +```bash +cd toolhive-webhook +docker build -t toolhive-webhook:test . +``` diff --git a/README.md b/README.md index 119402475..bc405a061 100644 --- a/README.md +++ b/README.md @@ -4,4 +4,4 @@ This repository contains extension projects for the Kagenti ecosystem ## Projects -[toolhive-webhook](./toolhive-webhook/) - Admission webhook for Toolhive MCPServer resources \ No newline at end of file +[toolhive-webhook](./toolhive-webhook/) - Admission webhook for Toolhive MCPServer resources diff --git a/charts/toolhive-webhook/.helmignore b/charts/toolhive-webhook/.helmignore new file mode 100644 index 000000000..0e8a0eb36 --- /dev/null +++ b/charts/toolhive-webhook/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/charts/toolhive-webhook/Chart.yaml b/charts/toolhive-webhook/Chart.yaml new file mode 100644 index 000000000..b62545cf1 --- /dev/null +++ b/charts/toolhive-webhook/Chart.yaml @@ -0,0 +1,11 @@ +apiVersion: v2 +name: toolhive-webhook +description: A Kubernetes admission webhook for MCPServer resources +type: application +version: 0.1.0 +appVersion: "1.16.0" +keywords: + - webhook + - mcp + - toolhive + - kagenti diff --git a/charts/toolhive-webhook/templates/_helpers.yaml b/charts/toolhive-webhook/templates/_helpers.yaml new file mode 100644 index 000000000..26689827e --- /dev/null +++ b/charts/toolhive-webhook/templates/_helpers.yaml @@ -0,0 +1,46 @@ +{{- define "toolhive-webhook.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{- define "toolhive-webhook.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{- define "toolhive-webhook.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{- define "toolhive-webhook.labels" -}} +helm.sh/chart: {{ include "toolhive-webhook.chart" . }} +{{ include "toolhive-webhook.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{- define "toolhive-webhook.selectorLabels" -}} +app.kubernetes.io/name: {{ include "toolhive-webhook.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{- define "toolhive-webhook.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "toolhive-webhook.fullname" . ) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} + +{{- define "toolhive-webhook.namespace" -}} +{{- default (printf "%s-system" (include "toolhive-webhook.name" .)) .Values.namespaceOverride }} +{{- end }} diff --git a/charts/toolhive-webhook/templates/certificate-issuer.yaml b/charts/toolhive-webhook/templates/certificate-issuer.yaml new file mode 100644 index 000000000..91416c4f7 --- /dev/null +++ b/charts/toolhive-webhook/templates/certificate-issuer.yaml @@ -0,0 +1,13 @@ +{{- if .Values.certManager.enabled }} +{{- if .Values.certManager.issuer.create }} +apiVersion: cert-manager.io/v1 +kind: Issuer +metadata: + name: {{ .Values.certManager.issuer.name }} + namespace: {{ include "toolhive-webhook.namespace" . }} + labels: + {{- include "toolhive-webhook.labels" . | nindent 4 }} +spec: + selfSigned: {} +{{- end }} +{{- end }} diff --git a/charts/toolhive-webhook/templates/certificate.yaml b/charts/toolhive-webhook/templates/certificate.yaml new file mode 100644 index 000000000..6abe44687 --- /dev/null +++ b/charts/toolhive-webhook/templates/certificate.yaml @@ -0,0 +1,17 @@ +{{- if .Values.certManager.enabled }} +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + name: {{ include "toolhive-webhook.fullname" . }}-serving-cert + namespace: {{ include "toolhive-webhook.namespace" . }} + labels: + {{- include "toolhive-webhook.labels" . | nindent 4 }} +spec: + dnsNames: + - {{ include "toolhive-webhook.fullname" . }}-webhook-service.{{ include "toolhive-webhook.namespace" . }}.svc + - {{ include "toolhive-webhook.fullname" . }}-webhook-service.{{ include "toolhive-webhook.namespace" . }}.svc.cluster.local + issuerRef: + kind: Issuer + name: {{ .Values.certManager.issuer.name }} + secretName: {{ include "toolhive-webhook.fullname" . }}-webhook-server-cert +{{- end }} diff --git a/charts/toolhive-webhook/templates/clusterrole.yaml b/charts/toolhive-webhook/templates/clusterrole.yaml new file mode 100644 index 000000000..f9bcebd62 --- /dev/null +++ b/charts/toolhive-webhook/templates/clusterrole.yaml @@ -0,0 +1,15 @@ +{{- if .Values.rbac.create }} +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: {{ include "toolhive-webhook.fullname" . }}-manager-role + labels: + {{- include "toolhive-webhook.labels" . | nindent 4 }} +rules: +- apiGroups: [""] + resources: ["pods"] + verbs: ["get", "list", "watch"] +- apiGroups: [""] + resources: ["configmaps"] + verbs: ["get", "list", "watch"] +{{- end }} diff --git a/charts/toolhive-webhook/templates/clusterrolebinding.yaml b/charts/toolhive-webhook/templates/clusterrolebinding.yaml new file mode 100644 index 000000000..e761764ab --- /dev/null +++ b/charts/toolhive-webhook/templates/clusterrolebinding.yaml @@ -0,0 +1,16 @@ +{{- if .Values.rbac.create }} +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: {{ include "toolhive-webhook.fullname" . }}-manager-rolebinding + labels: + {{- include "toolhive-webhook.labels" . | nindent 4 }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: {{ include "toolhive-webhook.fullname" . }}-manager-role +subjects: +- kind: ServiceAccount + name: {{ include "toolhive-webhook.serviceAccountName" . }} + namespace: {{ include "toolhive-webhook.namespace" . }} +{{- end }} diff --git a/charts/toolhive-webhook/templates/deployment.yaml b/charts/toolhive-webhook/templates/deployment.yaml new file mode 100644 index 000000000..f1faf8743 --- /dev/null +++ b/charts/toolhive-webhook/templates/deployment.yaml @@ -0,0 +1,70 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "toolhive-webhook.fullname" . }}-controller-manager + namespace: {{ include "toolhive-webhook.namespace" . }} + labels: + {{- include "toolhive-webhook.labels" . | nindent 4 }} + control-plane: controller-manager +spec: + replicas: {{ .Values.replicaCount }} + selector: + matchLabels: + {{- include "toolhive-webhook.selectorLabels" . | nindent 6 }} + control-plane: controller-manager + template: + metadata: + {{- with .Values.podAnnotations }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + {{- include "toolhive-webhook.selectorLabels" . | nindent 8 }} + control-plane: controller-manager + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "toolhive-webhook.serviceAccountName" . }} + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} + containers: + - name: manager + image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + command: + - {{ .Values.image.command }} + args: + {{- if .Values.metrics.enabled }} + - --metrics-bind-address=:{{ .Values.metrics.port }} + {{- end }} + {{- if .Values.leaderElection.enabled }} + - --leader-elect + {{- end }} + - --health-probe-bind-address=:8081 + - --webhook-cert-path={{ .Values.webhook.certPath }} + {{- if .Values.webhook.enableClientRegistration }} + - --enable-client-registration=true + {{- end }} + ports: + - containerPort: {{ .Values.webhook.port }} + name: webhook-server + protocol: TCP + securityContext: + {{- toYaml .Values.securityContext | nindent 10 }} + livenessProbe: + {{- toYaml .Values.livenessProbe | nindent 10 }} + readinessProbe: + {{- toYaml .Values.readinessProbe | nindent 10 }} + resources: + {{- toYaml .Values.resources | nindent 10 }} + volumeMounts: + - mountPath: {{ .Values.webhook.certPath }} + name: webhook-certs + readOnly: true + volumes: + - name: webhook-certs + secret: + secretName: {{ include "toolhive-webhook.fullname" . }}-webhook-server-cert + terminationGracePeriodSeconds: 10 diff --git a/charts/toolhive-webhook/templates/mutatingwebhook.yaml b/charts/toolhive-webhook/templates/mutatingwebhook.yaml new file mode 100644 index 000000000..f04c784fb --- /dev/null +++ b/charts/toolhive-webhook/templates/mutatingwebhook.yaml @@ -0,0 +1,31 @@ +{{- if .Values.webhook.enabled }} +apiVersion: admissionregistration.k8s.io/v1 +kind: MutatingWebhookConfiguration +metadata: + name: {{ include "toolhive-webhook.fullname" . }}-mutating-webhook-configuration + {{- if .Values.certManager.enabled }} + annotations: + cert-manager.io/inject-ca-from: {{ include "toolhive-webhook.namespace" . }}/{{ include "toolhive-webhook.fullname" . }}-serving-cert + {{- end }} +webhooks: +- admissionReviewVersions: + - v1 + clientConfig: + service: + name: {{ include "toolhive-webhook.fullname" . }}-webhook-service + namespace: {{ include "toolhive-webhook.namespace" . }} + path: /mutate-toolhive-stacklok-dev-v1alpha1-mcpserver + failurePolicy: Fail + name: mmcpserver-v1alpha1.kb.io + rules: + - apiGroups: + - toolhive.stacklok.dev + apiVersions: + - v1alpha1 + operations: + - CREATE + - UPDATE + resources: + - mcpservers + sideEffects: None +{{- end }} diff --git a/charts/toolhive-webhook/templates/namespace.yaml b/charts/toolhive-webhook/templates/namespace.yaml new file mode 100644 index 000000000..22462e6f9 --- /dev/null +++ b/charts/toolhive-webhook/templates/namespace.yaml @@ -0,0 +1,8 @@ +{{- if .Values.namespace.create }} +apiVersion: v1 +kind: Namespace +metadata: + name: {{ include "toolhive-webhook.namespace" . }} + labels: + {{- include "toolhive-webhook.labels" . | nindent 4 }} +{{- end }} diff --git a/charts/toolhive-webhook/templates/service.yaml b/charts/toolhive-webhook/templates/service.yaml new file mode 100644 index 000000000..c0645ea46 --- /dev/null +++ b/charts/toolhive-webhook/templates/service.yaml @@ -0,0 +1,17 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "toolhive-webhook.fullname" . }}-webhook-service + namespace: {{ include "toolhive-webhook.namespace" . }} + labels: + {{- include "toolhive-webhook.labels" . | nindent 4 }} +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.port }} + targetPort: {{ .Values.service.targetPort }} + protocol: TCP + name: webhook + selector: + {{- include "toolhive-webhook.selectorLabels" . | nindent 4 }} + control-plane: controller-manager diff --git a/charts/toolhive-webhook/templates/serviceaccount.yaml b/charts/toolhive-webhook/templates/serviceaccount.yaml new file mode 100644 index 000000000..320e3e339 --- /dev/null +++ b/charts/toolhive-webhook/templates/serviceaccount.yaml @@ -0,0 +1,13 @@ +{{- if .Values.serviceAccount.create -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "toolhive-webhook.serviceAccountName" . }} + namespace: {{ include "toolhive-webhook.namespace" . }} + labels: + {{- include "toolhive-webhook.labels" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +{{- end }} diff --git a/charts/toolhive-webhook/templates/validatingwebhook.yaml b/charts/toolhive-webhook/templates/validatingwebhook.yaml new file mode 100644 index 000000000..89ff408c1 --- /dev/null +++ b/charts/toolhive-webhook/templates/validatingwebhook.yaml @@ -0,0 +1,31 @@ +{{- if .Values.webhook.enabled }} +apiVersion: admissionregistration.k8s.io/v1 +kind: ValidatingWebhookConfiguration +metadata: + name: {{ include "toolhive-webhook.fullname" . }}-validating-webhook-configuration + {{- if .Values.certManager.enabled }} + annotations: + cert-manager.io/inject-ca-from: {{ include "toolhive-webhook.namespace" . }}/{{ include "toolhive-webhook.fullname" . }}-serving-cert + {{- end }} +webhooks: +- admissionReviewVersions: + - v1 + clientConfig: + service: + name: {{ include "toolhive-webhook.fullname" . }}-webhook-service + namespace: {{ include "toolhive-webhook.namespace" . }} + path: /validate-toolhive-stacklok-dev-v1alpha1-mcpserver + failurePolicy: Fail + name: vmcpserver-v1alpha1.kb.io + rules: + - apiGroups: + - toolhive.stacklok.dev + apiVersions: + - v1alpha1 + operations: + - CREATE + - UPDATE + resources: + - mcpservers + sideEffects: None +{{- end }} diff --git a/charts/toolhive-webhook/values.yaml b/charts/toolhive-webhook/values.yaml new file mode 100644 index 000000000..7541cb960 --- /dev/null +++ b/charts/toolhive-webhook/values.yaml @@ -0,0 +1,97 @@ +# Default values for toolhive-webhook. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. + +# This will set the replicaset count more information can be found here: https://kubernetes.io/docs/concepts/workloads/controllers/replicaset/ +replicaCount: 1 + + +image: + repository: ghcr.io/kagenti/kagenti-extensions/toolhive-webhook + pullPolicy: IfNotPresent + tag: "__PLACEHOLDER__" + # Set to /ko-app/cmd for ko-built images, /manager for docker-built images + command: /manager + +imagePullSecrets: [] +nameOverride: "" +fullnameOverride: "" +namespaceOverride: "" + +namespace: + create: true + +webhook: + enabled: true + enableClientRegistration: true + certPath: /tmp/k8s-webhook-server/serving-certs + certName: tls.crt + certKey: tls.key + port: 9443 + +serviceAccount: + create: true + annotations: {} + name: "" + +podAnnotations: + kubectl.kubernetes.io/default-container: manager + +podSecurityContext: + runAsNonRoot: true + seccompProfile: + type: RuntimeDefault + +securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - "ALL" + +service: + type: ClusterIP + port: 443 + targetPort: 9443 + +resources: + limits: + cpu: 500m + memory: 128Mi + requests: + cpu: 10m + memory: 64Mi + +livenessProbe: + httpGet: + path: /healthz + port: 8081 + initialDelaySeconds: 15 + periodSeconds: 20 + +readinessProbe: + httpGet: + path: /readyz + port: 8081 + initialDelaySeconds: 5 + periodSeconds: 10 + +nodeSelector: {} +tolerations: [] +affinity: {} + +metrics: + enabled: true + port: 8443 + secure: true + +leaderElection: + enabled: false + +certManager: + enabled: true + issuer: + create: false + name: selfsigned-issuer + +rbac: + create: true diff --git a/toolhive-webhook/Makefile b/toolhive-webhook/Makefile index 9f4db8edf..e2c0ccad7 100644 --- a/toolhive-webhook/Makefile +++ b/toolhive-webhook/Makefile @@ -1,6 +1,16 @@ # Image URL to use all building/pushing image targets IMG ?= controller:latest +# ko build variables for local development +KO_DOCKER_REPO ?= ko.local +CMD_NAME ?= toolhive-webhook +IMAGE_TAG ?= $(shell git rev-parse --short HEAD) +ARCH ?= $(shell go env GOARCH) +CLUSTER ?= agent-platform +CONTEXT ?= +NAMESPACE ?= kagenti-system +HELM_OPTS ?= --set image.repository=$(KO_DOCKER_REPO)/$(CMD_NAME) --set image.tag=$(IMAGE_TAG) --set image.pullPolicy=IfNotPresent --set namespaceOverride=$(NAMESPACE) --set namespace.create=false --set image.command=/ko-app/cmd + # 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 @@ -157,6 +167,31 @@ deploy: manifests kustomize ## Deploy controller to the K8s cluster specified in undeploy: kustomize ## Undeploy controller from the K8s cluster specified in ~/.kube/config. Call with ignore-not-found=true to ignore resource not found errors during deletion. $(KUSTOMIZE) build config/default | $(KUBECTL) delete --ignore-not-found=$(ignore-not-found) -f - +.PHONY: ko-local-build +ko-local-build: + KO_DOCKER_REPO=${KO_DOCKER_REPO}/${CMD_NAME} ko build -B ./cmd -t ${IMAGE_TAG} --platform linux/${ARCH} + docker tag ${KO_DOCKER_REPO}/${CMD_NAME}/cmd:${IMAGE_TAG} ${KO_DOCKER_REPO}/${CMD_NAME}:${IMAGE_TAG} + +# this is used for local testing +.PHONY: kind-load-image +kind-load-image: + kind load docker-image ${KO_DOCKER_REPO}/${CMD_NAME}:${IMAGE_TAG} --name ${CLUSTER} + +.PHONY: install-local-chart +install-local-chart: kind-load-image + if [ ! -n "$(CONTEXT)" ] ; then helm upgrade --install --create-namespace -n $(NAMESPACE) toolhive-webhook ../charts/toolhive-webhook ${HELM_OPTS} ; else helm --kube-context ${CONTEXT} upgrade --install --create-namespace -n $(NAMESPACE) toolhive-webhook ../charts/toolhive-webhook ${HELM_OPTS} ; fi + +.PHONY: uninstall-local-chart +uninstall-local-chart: ## Uninstall the local Helm chart + if [ ! -n "$(CONTEXT)" ] ; then helm uninstall toolhive-webhook -n $(NAMESPACE) ; else helm --kube-context ${CONTEXT} uninstall toolhive-webhook -n $(NAMESPACE) ; fi + +.PHONY: reinstall-local-chart +reinstall-local-chart: uninstall-local-chart install-local-chart ## Uninstall and reinstall the local Helm chart + +.PHONY: local-dev +local-dev: ko-local-build install-local-chart ## Build with ko and deploy to local Kind cluster + + ##@ Dependencies ## Location to install dependencies to diff --git a/toolhive-webhook/README.md b/toolhive-webhook/README.md index 4e0925bb5..66f87ddd8 100644 --- a/toolhive-webhook/README.md +++ b/toolhive-webhook/README.md @@ -1,154 +1,224 @@ # toolhive-webhook -A Kubernetes admission webhook for [ToolHive](https://github.com/stacklok/toolhive) MCPServer resources that automatically injects Kagenti client registration initContainer to enable registration of the server with Keycloak. +A Kubernetes admission webhook for [ToolHive](https://github.com/stacklok/toolhive) MCPServer resources that automatically injects sidecar containers to enable Keycloak client registration and SPIFFE/SPIRE token exchanges for secure service-to-service authentication within the Kagenti platform. -## Description +## Overview -This webhook provides automatic client registration for MCPServer resources deployed in Kubernetes. When enabled, it mutates MCPServer Custom Resource (CR) to include an initContainer that registers the server as a client in Keycloak before the main container starts. This enables secure, automated service authentication within the Kagenti platform. +This webhook provides security for MCPServer resources by automatically injecting two sidecar containers that handle identity and authentication: -The webhook supports: +1. **`spiffe-helper`** - Obtains SPIFFE Verifiable Identity Documents (SVIDs) from the SPIRE agent via the Workload API +2. **`kagenti-client-registration`** - Registers the MCPServer as an OAuth2 client in Keycloak using the SPIFFE identity -- Automatic injection of `kagenti-client-registration` initContainer +### Why Sidecar Injection? -- Configurable client registration via the `--enable-client-registration` flag +The sidecar approach is necessary because the ToolHive proxy is not currently designed to be easily extensible. Implementing Kagenti's authentication and authorization requirements would require modifications to the ToolHive proxy codebase to add middleware plugin support. By using sidecar containers injected via this webhook, we achieve: -- Shared volume mounting for credential propagation +- **Non-invasive Integration** - No modifications to ToolHive proxy code required +- **Independent Updates** - Kagenti security components can be updated without rebuilding ToolHive +- **Flexibility** - Easy to add, remove, or modify security features without touching MCPServer implementations -- Integration with cert-manager for webhook TLS certificates + +## Architecture + +``` + +┌─────────────────────────────────────────────────────────┐ +│ MCPServer Pod │ +│ │ +│ ┌─────────────────┐ ┌──────────────────────────────┐ │ +│ │ spiffe-helper │ │ kagenti-client-registration │ │ +│ │ │ │ │ │ +│ │ 1. Connects to │ │ 2. Waits for jwt_svid.token │ │ +│ │ SPIRE agent │──│ in /opt/ │ │ +│ │ 2. Gets JWT-SVID│ │ 3. Registers with Keycloak │ │ +│ │ 3. Writes to │ │ using SPIFFE identity │ │ +│ │ /opt/jwt_ │ │ 4. Runs continuously │ │ +│ │ svid.token │ │ │ │ +│ └─────────────────┘ └──────────────────────────────┘ │ +│ │ │ │ +│ ┌────────▼────────────────────────▼───────────────────┐│ +│ │ Shared Volume: svid-output (/opt) ││ +│ └─────────────────────────────────────────────────────┘│ +│ │ +│ ┌────────────────────────────────────┐ │ +│ │ Your MCPServer Container │ │ +│ │ (authenticated via Keycloak) │ │ +│ └────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────┘ + │ │ + ▼ ▼ + SPIRE Agent Socket Keycloak Server + (/run/spire/agent-sockets) (OAuth2/OIDC) +``` + +## Features + +### Automatic Sidecar Injection + +The webhook injects two sidecar containers: + +#### 1. SPIFFE Helper (`spiffe-helper`) + +- **Image**: `ghcr.io/spiffe/spiffe-helper:nightly` +- **Purpose**: Obtains and refreshes JWT-SVIDs from SPIRE +- **Resources**: 50m CPU / 64Mi memory (request), 100m CPU / 128Mi memory (limit) +- **Volumes**: + - `/spiffe-workload-api` - SPIRE agent socket + - `/etc/spiffe-helper` - Configuration + - `/opt` - SVID token output + +#### 2. Client Registration (`kagenti-client-registration`) + +- **Image**: `ghcr.io/kagenti/kagenti/client-registration:latest` +- **Purpose**: Registers MCPServer as Keycloak OAuth2 client using SPIFFE identity +- **Resources**: 50m CPU / 64Mi memory (request), 100m CPU / 128Mi memory (limit) +- **Behavior**: Waits for `/opt/jwt_svid.token`, then registers with Keycloak +- **Volumes**: + - `/opt` - Reads SVID token from spiffe-helper + +### Automatic Volume Configuration + +The webhook automatically adds these volumes: + +- **`shared-data`** - EmptyDir for inter-container communication +- **`spire-agent-socket`** - HostPath to `/run/spire/agent-sockets` for SPIRE agent access +- **`spiffe-helper-config`** - ConfigMap containing SPIFFE helper configuration +- **`svid-output`** - EmptyDir for SVID token exchange between sidecars ### Configuration -The webhook supports the following configuration options: +The webhook supports the following flags: -- `--enable-client-registration`: Enable automatic client registration in Keycloak (default: true) -- `--webhook-cert-path`: Directory containing webhook TLS certificates (default: auto-generated) -- `--webhook-cert-name`: Webhook certificate filename (default: tls.crt) -- `--webhook-cert-key`: Webhook key filename (default: tls.key) +- `--enable-client-registration` - Enable automatic sidecar injection (default: true) +- `--webhook-cert-path` - Directory containing webhook TLS certificates (default: /tmp/k8s-webhook-server/serving-certs) +- `--webhook-cert-name` - Webhook certificate filename (default: tls.crt) +- `--webhook-cert-key` - Webhook key filename (default: tls.key) -The webhook requires a ConfigMap named `environments` in the same namespace with the following keys: -- `KEYCLOAK_URL`: Keycloak server URL -- `KEYCLOAK_REALM`: Keycloak realm name -- `KEYCLOAK_ADMIN_USERNAME`: Admin username for client registration -- `KEYCLOAK_ADMIN_PASSWORD`: Admin password for client registration +3. **SPIRE Agent** - Must be running with socket at `/run/spire/agent-sockets/spire-agent.sock` ## Getting Started ### Prerequisites -- go version v1.24.4+ -- docker version 17.03+. -- kubectl version v1.11.3+. -- Access to a Kubernetes v1.11.3+ cluster. +- Kubernetes v1.11.3+ cluster +- Go v1.22+ (for development) +- Docker v17.03+ (for building images) +- kubectl v1.11.3+ +- cert-manager v1.0+ (for webhook TLS certificates) +- SPIRE agent deployed on cluster nodes +- Keycloak server accessible from the cluster -### To Deploy on the cluster +### Quick Start with Helm -**Build and push your image to the location specified by `IMG`:** +```bash -```sh -make docker-build docker-push IMG=/toolhive-webhook:tag +# Install the webhook using Helm +helm install toolhive-webhook oci://ghcr.io/kagenti/toolhive-webhook/toolhive-webhook-chart \ + --version \ + --namespace kagenti-system \ + --create-namespace ``` -**NOTE:** This image ought to be published in the personal registry you specified. -And it is required to have access to pull the image from the working environment. -Make sure you have the proper permission to the registry if the above commands don’t work. +### Local Development with Kind -**Install the CRDs into the cluster:** +```bash +cd toolhive-webhook -```sh -make install -``` +# Build and deploy to local Kind cluster in one command +make local-dev CLUSTER= -**Deploy the Manager to the cluster with the image specified by `IMG`:** +# Or step by step: +make ko-local-build # Build with ko +make kind-load-image CLUSTER= # Load into Kind +make install-local-chart CLUSTER= # Deploy with Helm -```sh -make deploy IMG=/toolhive-webhook:tag +# Reinstall after changes +make reinstall-local-chart CLUSTER= ``` -> **NOTE**: If you encounter RBAC errors, you may need to grant yourself cluster-admin -privileges or be logged in as admin. +### Traditional Deployment -**Create instances of your solution** -You can apply the samples (examples) from the config/sample: +**Build and push your image:** ```sh -kubectl apply -k config/samples/ +make docker-build docker-push IMG=/toolhive-webhook:tag ``` ->**NOTE**: Ensure that the samples has default values to test it out. - -### To Uninstall - -**Delete the instances (CRs) from the cluster:** +**Deploy using kustomize:** ```sh -kubectl delete -k config/samples/ +make deploy IMG=/toolhive-webhook:tag ``` -**Delete the APIs(CRDs) from the cluster:** +**Or deploy using Helm:** ```sh -make uninstall +helm install toolhive-webhook ./charts/toolhive-webhook \ + --set image.repository=/toolhive-webhook \ + --set image.tag= \ + --namespace kagenti-system \ + --create-namespace ``` -**UnDeploy the controller from the cluster:** +## How It Works -```sh -make undeploy -``` +### Mutation Process -## Project Distribution +1. **MCPServer Created** - User creates a MCPServer custom resource +2. **Webhook Intercepts** - Kubernetes API server sends the resource to the webhook +3. **Containers Injected** - Webhook adds `spiffe-helper` and `kagenti-client-registration` sidecars +4. **Volumes Added** - Required volumes for SPIRE, configuration, and credential sharing +5. **Resource Applied** - Modified MCPServer is created in the cluster -Following the options to release and provide this solution to the users. +### Runtime Behavior -### By providing a bundle with all YAML files +1. **Pod Starts** - All containers start simultaneously +2. **SPIFFE Helper** - Connects to SPIRE agent, obtains JWT-SVID, writes to `/opt/jwt_svid.token` +3. **Client Registration** - Waits for token file, then registers with Keycloak using SPIFFE identity +4. **MCPServer Container** - Starts and can authenticate via Keycloak using registered client credentials -1. Build the installer for the image built and published in the registry: +### Security Model -```sh -make build-installer IMG=/toolhive-webhook:tag -``` +- **SPIFFE/SPIRE** - Provides cryptographic workload identity +- **JWT-SVID** - Short-lived, automatically rotated tokens +- **Keycloak** - Central authentication and authorization +- **Zero Trust** - Every service authenticated via SPIFFE identity +- **No Static Credentials** - All tokens dynamically generated and rotated -**NOTE:** The makefile target mentioned above generates an 'install.yaml' -file in the dist directory. This file contains all the resources built -with Kustomize, which are necessary to install this project without its -dependencies. +## Configuration -2. Using the installer +### Webhook Configuration -Users can just run 'kubectl apply -f ' to install -the project, i.e.: +The webhook can be configured via Helm values or command-line flags: -```sh -kubectl apply -f https://raw.githubusercontent.com//toolhive-webhook//dist/install.yaml +```yaml +# values.yaml +webhook: + enabled: true + enableClientRegistration: true # Enable sidecar injection + certPath: /tmp/k8s-webhook-server/serving-certs + certName: tls.crt + certKey: tls.key + port: 9443 ``` -### By providing a Helm Chart +### Disable Sidecar Injection -1. Build the chart using the optional helm plugin +To disable sidecar injection for specific MCPServers, the webhook can be configured globally: -```sh -kubebuilder edit --plugins=helm/v1-alpha +```yaml +webhook: + enableClientRegistration: false ``` -2. See that a chart was generated under 'dist/chart', and users -can obtain this solution from there. - -**NOTE:** If you change the project, you need to update the Helm Chart -using the same command above to sync the latest changes. Furthermore, -if you create webhooks, you need to use the above command with -the '--force' flag and manually ensure that any custom configuration -previously added to 'dist/chart/values.yaml' or 'dist/chart/manager/manager.yaml' -is manually re-applied afterwards. - -## Contributing - -// TODO(user): Add detailed information on how you would like others to contribute to this project +Or deploy with the flag: +```bash +make deploy IMG= ENABLE_CLIENT_REGISTRATION=false +``` -**NOTE:** Run `make help` for more information on all potential `make` targets -More information can be found via the [Kubebuilder Documentation](https://book.kubebuilder.io/introduction.html) ## License @@ -165,4 +235,3 @@ 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/toolhive-webhook/hack/boilerplate.go.txt b/toolhive-webhook/hack/boilerplate.go.txt index 221dcbe0b..4671de8fb 100644 --- a/toolhive-webhook/hack/boilerplate.go.txt +++ b/toolhive-webhook/hack/boilerplate.go.txt @@ -12,4 +12,4 @@ 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. -*/ \ No newline at end of file +*/ diff --git a/toolhive-webhook/internal/webhook/v1alpha1/mcpserver_webhook.go b/toolhive-webhook/internal/webhook/v1alpha1/mcpserver_webhook.go index 48a6f7a08..47342fe48 100644 --- a/toolhive-webhook/internal/webhook/v1alpha1/mcpserver_webhook.go +++ b/toolhive-webhook/internal/webhook/v1alpha1/mcpserver_webhook.go @@ -50,8 +50,6 @@ func SetupMCPServerWebhookWithManager(mgr ctrl.Manager, registerClient bool) err Complete() } -// TODO(user): EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! - // +kubebuilder:webhook:path=/mutate-toolhive-stacklok-dev-v1alpha1-mcpserver,mutating=true,failurePolicy=fail,sideEffects=None,groups=toolhive.stacklok.dev,resources=mcpservers,verbs=create;update,versions=v1alpha1,name=mmcpserver-v1alpha1.kb.io,admissionReviewVersions=v1 // MCPServerCustomDefaulter struct is responsible for setting default values on the custom resource of the @@ -239,10 +237,6 @@ func (d *MCPServerCustomDefaulter) injectClientRegistrationContainer(mcpserver * Name: "CLIENT_NAME", Value: clientId, }, - { - Name: "CLIENT_ID", - Value: "spiffe://localtest.me/sa/" + mcpserver.Name, - }, { Name: "NAMESPACE", Value: mcpserver.Namespace,